/** * 直聊「保留」计数器: * 按会话记下每个模型被保留(点「保留此回复」)了多少次,存在浏览器缓存里。 * 只算数,不碰 DOM。 */ const STORAGE_KEY = "wa-direct-chat-keep-stats-v1"; function storage() { try { return globalThis.localStorage || null; } catch { return null; } } function readAll() { const ls = storage(); if (!ls) return {}; try { const parsed = JSON.parse(ls.getItem(STORAGE_KEY) || "{}"); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; } catch { return {}; } } function writeAll(all) { const ls = storage(); if (!ls) return; try { ls.setItem(STORAGE_KEY, JSON.stringify(all)); } catch { /* 存不进缓存也不影响当前会话 */ } } function isBucket(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } /** * 模型身份:同一配置同一型号算同一个模型, * 窗口被删掉再加回来(窗口 id 变了)也还认作它。 */ export function keepStatId(w) { const model = String(w?.model || ""); const key = String(w?.profileId || w?.profileName || model || "unknown"); return model && model !== key ? `${key}::${model}` : key; } /** 展示名与型号:配置名 · 型号,重复时只留一个 */ export function keepStatText(w) { const model = String(w?.model || ""); const label = String( w?.profileName || w?.profileId || model || "未知模型", ); return { id: keepStatId(w), label, model: model === label ? "" : model }; } /** 按次数降序,同次数再按名字排 */ export function sortKeepStats(entries) { return [...entries].sort( (a, b) => Number(b.count) - Number(a.count) || String(a.label).localeCompare(String(b.label), "zh-CN"), ); } /** 条状图宽度百分比:独占一行时满格,次数少的也至少留一点可见宽度 */ export function keepStatPercent(count, max) { const top = Number(max) || 0; const value = Number(count) || 0; if (top <= 0 || value <= 0) return 0; return Math.max(6, Math.min(100, Math.round((value / top) * 100))); } export function readKeepStats(conversationId) { if (!conversationId) return []; const bucket = readAll()[conversationId]; if (!isBucket(bucket)) return []; const entries = Object.entries(bucket).map(([id, value]) => ({ id, label: String(value?.label || id), model: String(value?.model || ""), count: Math.max(1, Number(value?.count) || 0), })); return sortKeepStats(entries); } export function keepStatTotal(entries) { return entries.reduce((sum, entry) => sum + (Number(entry.count) || 0), 0); } /** * 加减一次保留次数;减到 0 就把这个模型从记录里删掉。 * @param {string} conversationId * @param {{profileId?: string, profileName?: string, model?: string}} window * @param {number} [delta] 保留 +1、取消保留 -1 */ export function bumpKeepStats(conversationId, window, delta = 1) { if (!conversationId || !window) return readKeepStats(conversationId); const step = Number(delta) || 0; if (!step) return readKeepStats(conversationId); const all = readAll(); const bucket = isBucket(all[conversationId]) ? { ...all[conversationId] } : {}; const { id, label, model } = keepStatText(window); const before = isBucket(bucket[id]) ? Number(bucket[id].count) || 0 : 0; const count = Math.max(0, before + step); if (count > 0) bucket[id] = { label, model, count }; else delete bucket[id]; if (Object.keys(bucket).length) all[conversationId] = bucket; else delete all[conversationId]; writeAll(all); return readKeepStats(conversationId); } /** 清掉一个会话的计数(会话被删时用) */ export function clearKeepStats(conversationId) { if (!conversationId) return; const all = readAll(); if (!(conversationId in all)) return; delete all[conversationId]; writeAll(all); } /** 临时会话存成持久会话时,把计数跟着搬到新会话上 */ export function copyKeepStats(fromId, toId) { if (!fromId || !toId || fromId === toId) return readKeepStats(toId); const all = readAll(); const bucket = all[fromId]; if (!isBucket(bucket)) return readKeepStats(toId); all[toId] = { ...(isBucket(all[toId]) ? all[toId] : {}), ...bucket }; delete all[fromId]; writeAll(all); return readKeepStats(toId); }