Files
rime_wanxiang/lua/input_statistics.lua
2026-01-21 17:44:07 +08:00

339 lines
12 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- input_stats.lua
-- Rime 统计增强版 (LevelDB / 滚动时间窗口 / 效率仪表盘 / 汉字提纯)
-- 维度升级1, 2, 3, 4, ≥5 字独立统计
-- UI优化综合数据田字格布局峰值与均速分开显示
-- 逻辑更新:增加平台信息清洗函数 (去除 git hash)
local userdb = require("lib/userdb")
local rime_api = rime_api
-- 1. 初始化数据库
local db = userdb.LevelDb("lua/stats")
-- 硬编码信息
local schema_name = "万象拼音"
-- 注意:这里只获取原始名称,清洗逻辑下放给 process_platform_info
local raw_software_name = rime_api.get_distribution_code_name()
-- -----------------------------------------------------------------------------
-- 平台信息处理中心
-- 在这里添加你所有的字符串清洗/替换逻辑
-- -----------------------------------------------------------------------------
local function process_platform_info(name, ver)
name = name or ""
ver = ver or ""
-- 1. 清洗 Trime/Rime 版本号中的 git hash
-- 目标:将 "v3.3.7-48-gda909f96" 变为 "v3.3.7-48"
-- 逻辑:匹配连字符+g+一串字母数字,并替换为空
ver = ver:gsub("^(.-%-[^%-]+)%-.*$", "%1")
-- 2. 可以在这里修改平台名称
if name == "Weasel" then name = "小狼毫" end
if name == "trime" then name = "同文输入法" end
return name, ver
end
-- -----------------------------------------------------------------------------
-- 汉字识别核心逻辑
-- -----------------------------------------------------------------------------
local function is_chinese_code(c)
return (c >= 0x4E00 and c <= 0x9FFF) or (c >= 0x3400 and c <= 0x4DBF) or
(c >= 0x20000 and c <= 0x2A6DF) or (c >= 0x2A700 and c <= 0x2B73F) or
(c >= 0x2B740 and c <= 0x2B81F) or (c >= 0x2B820 and c <= 0x2CEAF) or
(c >= 0x2CEB0 and c <= 0x2EBEF) or (c >= 0x30000 and c <= 0x3134F) or
(c >= 0x31350 and c <= 0x323AF) or (c >= 0x2EBF0 and c <= 0x2EE5F) or
(c >= 0xF900 and c <= 0xFAFF) or (c >= 0x2F800 and c <= 0x2FA1F) or
(c >= 0x2E80 and c <= 0x2EFF) or (c >= 0x2F00 and c <= 0x2FDF)
end
local function get_pure_chinese_length(text)
local count = 0
for _, code in utf8.codes(text) do
if is_chinese_code(code) then count = count + 1 end
end
return count
end
-- -----------------------------------------------------------------------------
-- 内存缓存:实时分速
-- -----------------------------------------------------------------------------
local speed_buffer = {}
local last_cleanup_ts = 0
local function get_current_kpm(now)
if now - last_cleanup_ts > 5 then
local new_buf = {}
local threshold = now - 60
for _, item in ipairs(speed_buffer) do
if item.ts > threshold then table.insert(new_buf, item) end
end
speed_buffer = new_buf
last_cleanup_ts = now
end
local total = 0
local threshold = now - 60
for _, item in ipairs(speed_buffer) do
if item.ts > threshold then total = total + item.len end
end
return total
end
-- -----------------------------------------------------------------------------
-- 数据库操作
-- -----------------------------------------------------------------------------
local function ensure_db_open()
if not db:loaded() then return db:open() end
return true
end
local function db_get(key)
return tonumber(db:fetch(key)) or 0
end
local function db_incr_day_and_total(key_suffix, amount, day_key)
amount = amount or 1
local d_key = day_key .. key_suffix
db:update(d_key, tostring(db_get(d_key) + amount))
local t_key = "total" .. key_suffix
db:update(t_key, tostring(db_get(t_key) + amount))
end
local function db_set_max_day(key_suffix, new_val, day_key)
local d_key = day_key .. key_suffix
if new_val > db_get(d_key) then db:update(d_key, tostring(new_val)) end
local t_key = "total" .. key_suffix
if new_val > db_get(t_key) then db:update(t_key, tostring(new_val)) end
end
local function clear_all_data()
if not ensure_db_open() then return false end
if db.empty then
db:empty()
speed_buffer = {}
return true
end
local ok, iter = pcall(function() return db:query("") end)
if ok and iter then
local keys = {}
for key, _ in iter do table.insert(keys, key) end
for _, key in ipairs(keys) do db:erase(key) end
speed_buffer = {}
return true
end
return false
end
-- -----------------------------------------------------------------------------
-- 记录逻辑
-- -----------------------------------------------------------------------------
local function record_stats(hanzi_len, code_len)
if not ensure_db_open() then return end
local now = os.time()
local t = os.date("*t", now)
local day_key = string.format("d_%04d%02d%02d", t.year, t.month, t.day)
table.insert(speed_buffer, {ts = now, len = hanzi_len})
local current_kpm = get_current_kpm(now)
db_incr_day_and_total("_len", hanzi_len, day_key)
db_incr_day_and_total("_cnt", 1, day_key)
db_incr_day_and_total("_code", code_len, day_key)
if hanzi_len == 1 then db_incr_day_and_total("_l1", 1, day_key)
elseif hanzi_len == 2 then db_incr_day_and_total("_l2", 1, day_key)
elseif hanzi_len == 3 then db_incr_day_and_total("_l3", 1, day_key)
elseif hanzi_len == 4 then db_incr_day_and_total("_l4", 1, day_key)
elseif hanzi_len > 4 then db_incr_day_and_total("_l_gt4", 1, day_key)
end
db_set_max_day("_spd", current_kpm, day_key)
end
-- -----------------------------------------------------------------------------
-- 聚合查询逻辑
-- -----------------------------------------------------------------------------
local function aggregate_stats(days_lookback)
if not ensure_db_open() then return nil end
if days_lookback == 0 then
local prefix = "total"
return {
len = db_get(prefix .. "_len"),
cnt = db_get(prefix .. "_cnt"),
code = db_get(prefix .. "_code"),
spd = db_get(prefix .. "_spd"),
l1 = db_get(prefix .. "_l1"),
l2 = db_get(prefix .. "_l2"),
l3 = db_get(prefix .. "_l3"),
l4 = db_get(prefix .. "_l4"),
l_gt4 = db_get(prefix .. "_l_gt4")
}
end
local res = {len=0, cnt=0, code=0, spd=0, l1=0, l2=0, l3=0, l4=0, l_gt4=0}
local now_ts = os.time()
for i = 0, days_lookback - 1 do
local target_ts = now_ts - (i * 86400)
local t = os.date("*t", target_ts)
local day_key = string.format("d_%04d%02d%02d", t.year, t.month, t.day)
res.len = res.len + db_get(day_key .. "_len")
res.cnt = res.cnt + db_get(day_key .. "_cnt")
res.code = res.code + db_get(day_key .. "_code")
res.l1 = res.l1 + db_get(day_key .. "_l1")
res.l2 = res.l2 + db_get(day_key .. "_l2")
res.l3 = res.l3 + db_get(day_key .. "_l3")
res.l4 = res.l4 + db_get(day_key .. "_l4")
res.l_gt4 = res.l_gt4 + db_get(day_key .. "_l_gt4")
local daily_spd = db_get(day_key .. "_spd")
if daily_spd > res.spd then res.spd = daily_spd end
end
return res
end
-- -----------------------------------------------------------------------------
-- UI 渲染
-- -----------------------------------------------------------------------------
local function draw_bar(percent)
local length = 10
local filled_len = math.floor((percent / 100) * length)
local empty_len = length - filled_len
return string.rep("", filled_len) .. string.rep("", empty_len)
end
local function format_summary(title, data)
if not data or data.cnt == 0 then return "" .. title .. "暂无数据" end
local avg_code = 0
if data.len > 0 then avg_code = data.code / data.len end
local phrase_rate = 0
if data.len > 0 then phrase_rate = (data.len - data.l1) / data.len * 100 end
-- 估算均速 (优化算法,防止溢出峰值)
local estimated_avg_spd = 0
if data.cnt > 0 then
estimated_avg_spd = math.floor(data.len / ((data.cnt * 2) / 60))
if estimated_avg_spd > data.spd then estimated_avg_spd = math.floor(data.spd * 0.8) end -- 修正为峰值的80%更合理
if estimated_avg_spd == 0 and data.len > 0 then estimated_avg_spd = data.len end
end
local p1 = (data.l1 / data.cnt) * 100
local p2 = (data.l2 / data.cnt) * 100
local p3 = (data.l3 / data.cnt) * 100
local p4 = (data.l4 / data.cnt) * 100
local p_gt4 = (data.l_gt4 / data.cnt) * 100
-- 获取原始版本号
local raw_ver = rime_api.get_distribution_version() or ""
-- 【调用】清洗函数处理平台和版本信息
local clean_name, clean_ver = process_platform_info(raw_software_name, raw_ver)
return string.format(
"※ %s统计 · 效率仪表盘\n" ..
"───────────────\n" ..
"📊 综合数据\n" ..
" 总字数:%d \t 上屏:%d\n" ..
" 峰值速:%d \t 均速:%d\n" ..
"───────────────\n" ..
"⚡ 核心效率\n" ..
" 平均编码:%.2f 键/字\n" ..
" 词组连打:%.1f %%\n" ..
"───────────────\n" ..
"📈 字词分布\n" ..
" [1] %3d%% %s\n" ..
" [2] %3d%% %s\n" ..
" [3] %3d%% %s\n" ..
" [4] %3d%% %s\n" ..
" [≥5] %2d%% %s\n" ..
"───────────────\n" ..
"◉ 方案:%s\n" ..
"◉ 平台:%s %s",
title, data.len, data.cnt,
data.spd, estimated_avg_spd,
avg_code, phrase_rate,
p1, draw_bar(p1),
p2, draw_bar(p2),
p3, draw_bar(p3),
p4, draw_bar(p4),
p_gt4, draw_bar(p_gt4),
schema_name, clean_name, clean_ver -- 使用清洗后的变量
)
end
-- -----------------------------------------------------------------------------
-- Init & Fini
-- -----------------------------------------------------------------------------
local function init(env)
ensure_db_open()
if env.stat_notifier then env.stat_notifier:disconnect() end
local ctx = env.engine.context
env.stat_notifier = ctx.commit_notifier:connect(function(ctx)
local commit_text = ctx:get_commit_text()
if not commit_text or commit_text == "" then return end
if commit_text:sub(1, 1) == "/" then return end
if commit_text:find("^[※◉]") then return end
local hanzi_len = get_pure_chinese_length(commit_text)
if hanzi_len == 0 then return end
local script_text = ctx:get_script_text() or ""
local code_len = string.len(script_text)
if code_len == 0 then code_len = hanzi_len * 2 end
local now_ms = os.clock()
if env.last_commit_time and (now_ms - env.last_commit_time < 0.05) then
if env.last_commit_text == commit_text then return end
end
env.last_commit_time = now_ms
env.last_commit_text = commit_text
record_stats(hanzi_len, code_len)
end)
end
local function fini(env)
if env.stat_notifier then
env.stat_notifier:disconnect()
env.stat_notifier = nil
end
if db and db:loaded() then
db:close()
end
end
local function translator(input, seg, env)
if input:sub(1, 1) ~= "/" then return end
local summary = ""
local data = nil
local title = ""
if input == "/tjql" then
if clear_all_data() then
yield(Candidate("stat", seg.start, seg._end, "※ 统计数据已全部清空。", "🗑️"))
else
yield(Candidate("stat", seg.start, seg._end, "※ 数据清空失败,请检查权限。", ""))
end
return
end
if input == "/rtj" then title = "今日"; data = aggregate_stats(1)
elseif input == "/ztj" then title = "七日"; data = aggregate_stats(7)
elseif input == "/ytj" then title = "卅日"; data = aggregate_stats(30)
elseif input == "/ntj" then title = "本年"; data = aggregate_stats(365)
elseif input == "/ttj" then title = "生涯"; data = aggregate_stats(0)
end
if data then
summary = format_summary(title, data)
yield(Candidate("stat", seg.start, seg._end, summary, "📊"))
end
end
return { init = init, func = translator, fini = fini }