dsh-whale-widget 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/whale-widget.js +54 -6
- package/lib/index.js +127 -29
- package/package.json +1 -1
package/assets/whale-widget.js
CHANGED
|
@@ -6,10 +6,21 @@ window.__dshWhaleWidget = true
|
|
|
6
6
|
// 挂件脚本通过 tapIndex 注入 DSH 的每一个 index 页面(含插件市场等 SPA 视图)。
|
|
7
7
|
// 市场页用 ReactDOM.createPortal 渲染到 document.body,若挂件在此初始化,
|
|
8
8
|
// 会在 body 插入节点并注册全局捕获拦截,干扰 React 渲染树(removeChild 报错、页面空白)。
|
|
9
|
-
// 主聊天界面的特征:composer
|
|
10
|
-
//
|
|
9
|
+
// 主聊天界面的特征:composer 输入区。三种形态都算主界面:
|
|
10
|
+
// (a) 旧版 textarea
|
|
11
|
+
// (b) 旧版 contenteditable 可编辑 div
|
|
12
|
+
// (c) **DSH 0.1.6-alpha.1 起的新版**:<div contenteditable="false" role="textbox"
|
|
13
|
+
// aria-multiline="true" data-composer-input="true">(编辑由 Lexical 接管,所以
|
|
14
|
+
// contenteditable 反而是 false —— 只认前两种会让挂件在新版 DSH 上**完全不初始化**,
|
|
15
|
+
// 见 issue #123)。检测到才继续,否则不碰 DOM、不注册监听。
|
|
11
16
|
function dshwIsChatRoot(r) {
|
|
12
|
-
|
|
17
|
+
if (!r || !r.querySelector) return false
|
|
18
|
+
return !!(
|
|
19
|
+
r.querySelector('textarea') ||
|
|
20
|
+
r.querySelector('[contenteditable="true"]') ||
|
|
21
|
+
r.querySelector('[data-composer-input]') ||
|
|
22
|
+
r.querySelector('[role="textbox"]')
|
|
23
|
+
)
|
|
13
24
|
}
|
|
14
25
|
var dshwStarted = false
|
|
15
26
|
function dshwStartOnce() {
|
|
@@ -1342,6 +1353,17 @@ var rowHide = menuRow()
|
|
|
1342
1353
|
rowHide.appendChild(menuLabel('隐藏菜单按钮'))
|
|
1343
1354
|
rowHide.appendChild(menuHideToggle)
|
|
1344
1355
|
menuBox.appendChild(rowHide)
|
|
1356
|
+
// —— Codex 本机统计开关(issue #116):关掉后宿主不再扫描 ~/.codex/sessions ——
|
|
1357
|
+
var codexStatsToggle = document.createElement('input')
|
|
1358
|
+
codexStatsToggle.type = 'checkbox'
|
|
1359
|
+
codexStatsToggle.className = 'dshwv-check'
|
|
1360
|
+
codexStatsToggle.checked = true
|
|
1361
|
+
codexStatsToggle.title = '关闭后不再读取 ~/.codex/sessions 统计本机 Codex 用量(会话日志很大时建议关闭)'
|
|
1362
|
+
codexStatsToggle.addEventListener('change', function () { setCodexStatsOn(codexStatsToggle.checked) })
|
|
1363
|
+
var rowCodexStats = menuRow()
|
|
1364
|
+
rowCodexStats.appendChild(menuLabel('Codex 本机统计'))
|
|
1365
|
+
rowCodexStats.appendChild(codexStatsToggle)
|
|
1366
|
+
menuBox.appendChild(rowCodexStats)
|
|
1345
1367
|
// —— 资源管理:集中查看/删除已导入的图片与音频(角色图/泡泡图/音频片段/音效组) ——
|
|
1346
1368
|
var rowRes = menuRow()
|
|
1347
1369
|
var resOpenBtn = document.createElement('button')
|
|
@@ -10909,8 +10931,18 @@ function apiCodexDays7(c) {
|
|
|
10909
10931
|
// 列表行摘要
|
|
10910
10932
|
function apiCodexRowText(am) {
|
|
10911
10933
|
var c = am && am.codex
|
|
10912
|
-
if (!c || !c.ok)
|
|
10913
|
-
|
|
10934
|
+
if (!c || !c.ok) {
|
|
10935
|
+
// issue #116:把「被关掉 / 找不到目录 / 出错」区分开,不再是一句笼统的失败
|
|
10936
|
+
if (c && c.disabled) return 'Codex 统计已关闭'
|
|
10937
|
+
return c && c.error ? ('⚠ ' + c.error) : '无 Codex 数据'
|
|
10938
|
+
}
|
|
10939
|
+
var txt = 'Codex 今日 ' + apiFmtTokens(c.todayTokens) + ' · 近7天 ' + apiFmtTokens(apiCodexDays7(c)) + ' tokens'
|
|
10940
|
+
// 护栏的实际情况要能看见(issue #116:原报告抱怨"无开关、无报错、无提示")
|
|
10941
|
+
var notes = []
|
|
10942
|
+
if (Number(c.skipped) > 0) notes.push('已跳过 ' + c.skipped + ' 个超大日志')
|
|
10943
|
+
if (Number(c.deferred) > 0) notes.push('统计更新中')
|
|
10944
|
+
if (notes.length) txt += '(' + notes.join(' · ') + ')'
|
|
10945
|
+
return txt
|
|
10914
10946
|
}
|
|
10915
10947
|
// 第二期:订阅窗口(5h / 周)。host 已把 rate_limits 归一成 { primary, secondary, planType }
|
|
10916
10948
|
function apiCodexWinLabel(w, idx) {
|
|
@@ -12369,6 +12401,9 @@ var costBubbleActive = false
|
|
|
12369
12401
|
var scrollGapOn = false
|
|
12370
12402
|
var scrollGapPx = 17
|
|
12371
12403
|
var menuBtnHide = false // 主菜单开关:隐藏挂件菜单按钮,改为右键小鲸鱼唤出菜单
|
|
12404
|
+
// issue #116:Codex 本机统计开关(默认开)。关掉后宿主完全不扫 ~/.codex/sessions,
|
|
12405
|
+
// 适合会话日志很大的机器(超大日志会让统计扫描变得很重)。
|
|
12406
|
+
var codexStatsOn = true
|
|
12372
12407
|
// —— v734(issue #97 / #88):设置保存的「防覆盖 + 失败可见」——
|
|
12373
12408
|
// #97 根因:首次 GET 还没落地就 PUT,会把内存里的默认值整包写进服务端(重启后设置被洗成默认值)。
|
|
12374
12409
|
// #88 根因:这个 PUT 以前是 fire-and-forget,服务端 500 / {ok:false} 完全没人读。
|
|
@@ -12402,7 +12437,7 @@ function configSaveFailNotice(detail) {
|
|
|
12402
12437
|
'<br>已自动重试一次。若持续失败,请检查 DSH 数据目录是否可写。')
|
|
12403
12438
|
}
|
|
12404
12439
|
function configPayload() {
|
|
12405
|
-
return JSON.stringify({ scale: state.scale, sound: soundOn, vol: soundVol, soundSet: soundSet, usageMode: usageMode, peakMode: peakMode, bubbleOn: bubbleOn, turnCostOn: turnCostOn, turnCostCloseMs: turnCostCloseMs, scrollGapOn: scrollGapOn, scrollGapPx: scrollGapPx, menuBtnHide: menuBtnHide })
|
|
12440
|
+
return JSON.stringify({ scale: state.scale, sound: soundOn, vol: soundVol, soundSet: soundSet, usageMode: usageMode, peakMode: peakMode, bubbleOn: bubbleOn, turnCostOn: turnCostOn, turnCostCloseMs: turnCostCloseMs, scrollGapOn: scrollGapOn, scrollGapPx: scrollGapPx, menuBtnHide: menuBtnHide, codexStatsOn: codexStatsOn })
|
|
12406
12441
|
}
|
|
12407
12442
|
// 真正的 PUT:读响应 → 失败(网络异常 / HTTP!=200 / {ok:false})静默重试一次 → 仍失败才提示
|
|
12408
12443
|
function configPut(payload, retried) {
|
|
@@ -12532,6 +12567,14 @@ function setMenuBtnHide(v) {
|
|
|
12532
12567
|
saveConfig()
|
|
12533
12568
|
applyMenuBtnHideUI()
|
|
12534
12569
|
}
|
|
12570
|
+
// issue #116:Codex 本机统计开关。关掉后宿主不扫描 ~/.codex/sessions;
|
|
12571
|
+
// 打开/关闭都要重取一次模型列表,让 Codex 那行的统计/提示立刻跟着变。
|
|
12572
|
+
function setCodexStatsOn(v) {
|
|
12573
|
+
codexStatsOn = v !== false
|
|
12574
|
+
if (codexStatsToggle) codexStatsToggle.checked = codexStatsOn
|
|
12575
|
+
saveConfig()
|
|
12576
|
+
try { refreshModelList() } catch (err) {}
|
|
12577
|
+
}
|
|
12535
12578
|
function scaleToDisplay(s) {
|
|
12536
12579
|
return Math.round((s - MIN_SCALE) / ((MAX_SCALE - MIN_SCALE) / 19)) + 1
|
|
12537
12580
|
}
|
|
@@ -14765,6 +14808,11 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
14765
14808
|
menuBtnHide = d.menuBtnHide
|
|
14766
14809
|
if (menuHideToggle) menuHideToggle.checked = menuBtnHide
|
|
14767
14810
|
}
|
|
14811
|
+
// issue #116:Codex 本机统计开关(老配置里没有这个键 → 保持默认「开」)
|
|
14812
|
+
if (d && typeof d.codexStatsOn === 'boolean') {
|
|
14813
|
+
codexStatsOn = d.codexStatsOn
|
|
14814
|
+
if (codexStatsToggle) codexStatsToggle.checked = codexStatsOn
|
|
14815
|
+
}
|
|
14768
14816
|
// 无论服务端带没带这个键都要应用一次:触屏上 ☰ 需要常显(issue #91 缺陷2),
|
|
14769
14817
|
// 而旧写法只在键存在时才调用,空配置下按钮永远是透明的。
|
|
14770
14818
|
applyMenuBtnHideUI()
|
package/lib/index.js
CHANGED
|
@@ -929,7 +929,7 @@ export default {
|
|
|
929
929
|
const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
|
|
930
930
|
for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
|
|
931
931
|
return {
|
|
932
|
-
ok: true, version: '0.3.
|
|
932
|
+
ok: true, version: '0.3.6', today: todayData, days7,
|
|
933
933
|
total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
|
|
934
934
|
all: {
|
|
935
935
|
days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
|
|
@@ -1085,14 +1085,14 @@ export default {
|
|
|
1085
1085
|
walk(path.join(root, 'archived_sessions'), 0)
|
|
1086
1086
|
return out
|
|
1087
1087
|
}
|
|
1088
|
-
//
|
|
1089
|
-
|
|
1088
|
+
// 解析单个会话文件的**文本** → { days, rl, rlTs }
|
|
1089
|
+
// issue #116:读取改由调用方异步完成(先判大小、读完让出事件循环),这里只做纯解析。
|
|
1090
|
+
function parseCodexFileText(text) {
|
|
1090
1091
|
const days = {}
|
|
1091
1092
|
let model = 'codex'
|
|
1092
1093
|
let prevTotal = null
|
|
1093
1094
|
let lastRl = null, rlTs = 0
|
|
1094
|
-
|
|
1095
|
-
try { text = fs.readFileSync(file, 'utf8') } catch (err) { return { days, rl: null, rlTs: 0 } }
|
|
1095
|
+
if (typeof text !== 'string' || !text) return { days, rl: null, rlTs: 0 }
|
|
1096
1096
|
const bump = (day, mk, v) => {
|
|
1097
1097
|
days[day] = days[day] || {}
|
|
1098
1098
|
const cur = days[day][mk] || { in: 0, cached: 0, cwrite: 0, out: 0, reason: 0, total: 0, turns: 0 }
|
|
@@ -1194,21 +1194,57 @@ export default {
|
|
|
1194
1194
|
}
|
|
1195
1195
|
}
|
|
1196
1196
|
// 汇总(带增量缓存:只有变化的文件才重新解析)
|
|
1197
|
-
|
|
1197
|
+
// ===== issue #116:Codex 本地统计的护栏 =====
|
|
1198
|
+
// 旧实现是「启动 1.5s + 每 5 分钟」在事件循环上做**同步**全量扫描:递归遍历
|
|
1199
|
+
// ~/.codex/sessions,对每个过期文件 readFileSync + split('\n') + 逐行 JSON.parse。
|
|
1200
|
+
// 会话攒多之后,一轮扫描能冻结事件循环数分钟(整个 dsh web 不响应、满核 CPU),
|
|
1201
|
+
// 而且单个超过 V8 字符串上限(约 512MB)的 rollout 会抛 ERR_STRING_TOO_LONG ——
|
|
1202
|
+
// 由于是在写缓存之前就抛出,那个文件每轮都会被重读,size/mtime 又一直在变,永久复发。
|
|
1203
|
+
// 现在三条护栏:
|
|
1204
|
+
// ① 单文件上限:超过 CODEX_MAX_FILE_BYTES 直接跳过(结果记进缓存,不再重读),
|
|
1205
|
+
// 并在汇总里报 skipped / skippedBytes,界面上能看出"有文件被跳过";
|
|
1206
|
+
// ② 单轮预算:一轮最多读 CODEX_BUDGET_FILES 个 / CODEX_BUDGET_BYTES 字节,超了就
|
|
1207
|
+
// 带 deferred 出结果,本轮立刻返回、稍后再刷新 —— 绝不长时间占住事件循环;
|
|
1208
|
+
// ③ 全程异步(fs.promises)+ 每个文件处理后让出一次事件循环(await setImmediate)。
|
|
1209
|
+
const CODEX_MAX_FILE_BYTES = 32 * 1024 * 1024
|
|
1210
|
+
const CODEX_BUDGET_FILES = 400
|
|
1211
|
+
const CODEX_BUDGET_BYTES = 96 * 1024 * 1024
|
|
1212
|
+
const CODEX_SNAP_TTL = 60 * 1000
|
|
1213
|
+
const yieldLoop = () => new Promise((r) => setImmediate(r))
|
|
1214
|
+
|
|
1215
|
+
// 汇总(带增量缓存:只有变化的文件才重新解析)。异步、受预算约束。
|
|
1216
|
+
async function codexScan() {
|
|
1198
1217
|
const home = codexHome()
|
|
1199
1218
|
if (!home) return { ok: false, error: '未找到 Codex 目录($CODEX_HOME 或 ~/.codex)' }
|
|
1200
1219
|
const cache = readCodexCache()
|
|
1201
1220
|
const files = listCodexSessionFiles(home)
|
|
1202
1221
|
const keep = {}
|
|
1203
|
-
let changed = 0
|
|
1222
|
+
let changed = 0, skipped = 0, skippedBytes = 0, deferred = 0, readBytes = 0, parsedCount = 0
|
|
1204
1223
|
for (const f of files) {
|
|
1205
1224
|
let st = null
|
|
1206
|
-
try { st = fs.
|
|
1225
|
+
try { st = await fs.promises.stat(f) } catch (err) { continue }
|
|
1207
1226
|
const prev = cache.files[f]
|
|
1208
|
-
if (prev && prev.size === st.size && prev.mtimeMs === st.mtimeMs && prev.days) {
|
|
1209
|
-
|
|
1227
|
+
if (prev && prev.size === st.size && prev.mtimeMs === st.mtimeMs && prev.days) {
|
|
1228
|
+
keep[f] = prev
|
|
1229
|
+
if (prev.skip === 'too-big') { skipped++; skippedBytes += Number(st.size) || 0 }
|
|
1230
|
+
continue
|
|
1231
|
+
}
|
|
1232
|
+
// ① 单文件上限:跳过超大文件(errno 不再产生,也不再每轮重读)
|
|
1233
|
+
if (st.size > CODEX_MAX_FILE_BYTES) {
|
|
1234
|
+
keep[f] = { size: st.size, mtimeMs: st.mtimeMs, days: {}, rl: null, rlTs: 0, skip: 'too-big' }
|
|
1235
|
+
skipped++; skippedBytes += st.size; changed++
|
|
1236
|
+
continue
|
|
1237
|
+
}
|
|
1238
|
+
// ② 单轮预算:本轮读够了就停,剩下的留到下一次刷新
|
|
1239
|
+
if (readBytes + st.size > CODEX_BUDGET_BYTES || parsedCount >= CODEX_BUDGET_FILES) { deferred++; continue }
|
|
1240
|
+
let text = ''
|
|
1241
|
+
try { text = await fs.promises.readFile(f, 'utf8') } catch (err) { text = '' }
|
|
1242
|
+
readBytes += st.size
|
|
1243
|
+
const parsed = parseCodexFileText(text)
|
|
1210
1244
|
keep[f] = { size: st.size, mtimeMs: st.mtimeMs, days: parsed.days, rl: parsed.rl || null, rlTs: parsed.rlTs || 0 }
|
|
1211
1245
|
changed++
|
|
1246
|
+
parsedCount++
|
|
1247
|
+
await yieldLoop() // ③ 每个文件让出一次,HTTP 请求不会被整轮扫描堵住
|
|
1212
1248
|
}
|
|
1213
1249
|
if (changed > 0 || Object.keys(cache.files).length !== Object.keys(keep).length) {
|
|
1214
1250
|
writeCodexCache({ version: 1, files: keep, builtAt: Date.now() })
|
|
@@ -1251,6 +1287,9 @@ export default {
|
|
|
1251
1287
|
}
|
|
1252
1288
|
return {
|
|
1253
1289
|
ok: true, home, sessions: files.length, changed,
|
|
1290
|
+
// issue #116:把护栏的实际情况报出来,界面/日志能看出"有文件被跳过或本轮没扫完"
|
|
1291
|
+
skipped, skippedBytes, deferred, readBytes,
|
|
1292
|
+
maxFileBytes: CODEX_MAX_FILE_BYTES,
|
|
1254
1293
|
todayTokens, monthTokens, totalTokens,
|
|
1255
1294
|
outTokens, reasonTokens, cachedTokens,
|
|
1256
1295
|
days7, byModel,
|
|
@@ -1260,23 +1299,70 @@ export default {
|
|
|
1260
1299
|
rateLimitsTs: bestRlTs > 0 ? bestRlTs : 0,
|
|
1261
1300
|
}
|
|
1262
1301
|
}
|
|
1263
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
|
|
1302
|
+
// ===== 汇总快照(issue #116)=====
|
|
1303
|
+
// 关键变化:**任何调用方都不会再触发同步扫描**。
|
|
1304
|
+
// · codexSummaryCached():同步、永不阻塞 —— 直接返回上一次快照;快照过期时
|
|
1305
|
+
// 「顺手发起」一次后台刷新(不等待),所以同步调用点(额度计算)也一样安全;
|
|
1306
|
+
// · codexSummaryEnsured(ms):需要尽量新的地方(探活 / 模型列表)用,等在途扫描,
|
|
1307
|
+
// 最多等 ms 毫秒,超时就用当前快照,绝不无限等;
|
|
1308
|
+
// · 同一时刻只允许一次扫描在跑(去重),预算用完时安排一次稍后的补扫。
|
|
1309
|
+
let codexSnap = null
|
|
1310
|
+
let codexSnapAt = 0
|
|
1311
|
+
let codexScanP = null
|
|
1312
|
+
let codexCatchupT = null
|
|
1313
|
+
// 配置变更(例如用户在设置里关掉 Codex 统计)后必须让快照失效,
|
|
1314
|
+
// 否则下一次请求还会命中关闭前的旧快照、开关看起来"没生效"。
|
|
1315
|
+
function codexInvalidate() { codexSnap = null; codexSnapAt = 0 }
|
|
1316
|
+
function codexStatsOn() {
|
|
1317
|
+
try {
|
|
1318
|
+
const cfg = readSizeConfig()
|
|
1319
|
+
return !cfg || cfg.codexStatsOn !== false
|
|
1320
|
+
} catch (err) { return true }
|
|
1321
|
+
}
|
|
1322
|
+
function codexBackgroundRefresh(delayMs) {
|
|
1323
|
+
if (!codexStatsOn()) {
|
|
1324
|
+
codexSnap = { ok: false, disabled: true, error: 'Codex 统计已在设置里关闭' }
|
|
1325
|
+
codexSnapAt = Date.now()
|
|
1326
|
+
return null
|
|
1327
|
+
}
|
|
1328
|
+
if (codexScanP) return codexScanP
|
|
1329
|
+
if (delayMs > 0) {
|
|
1330
|
+
if (codexCatchupT) return null
|
|
1331
|
+
codexCatchupT = setTimeout(() => { codexCatchupT = null; codexBackgroundRefresh(0) }, delayMs)
|
|
1332
|
+
try { if (codexCatchupT.unref) codexCatchupT.unref() } catch (err) {}
|
|
1333
|
+
return null
|
|
1334
|
+
}
|
|
1335
|
+
codexScanP = codexScan()
|
|
1336
|
+
.then((s) => {
|
|
1337
|
+
codexSnap = s
|
|
1338
|
+
codexSnapAt = Date.now()
|
|
1339
|
+
// 本轮没扫完(预算用尽)→ 稍后补扫,逐步把统计补齐,而不是一次占满事件循环
|
|
1340
|
+
if (s && s.ok && s.deferred > 0) codexBackgroundRefresh(5000)
|
|
1341
|
+
})
|
|
1342
|
+
.catch((err) => {
|
|
1343
|
+
codexSnap = { ok: false, error: String((err && err.message) || err) }
|
|
1344
|
+
codexSnapAt = Date.now()
|
|
1345
|
+
})
|
|
1346
|
+
.finally(() => { codexScanP = null })
|
|
1347
|
+
return codexScanP
|
|
1348
|
+
}
|
|
1266
1349
|
function codexSummaryCached() {
|
|
1267
1350
|
const now = Date.now()
|
|
1268
|
-
if (
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1351
|
+
if (!codexSnap || now - codexSnapAt > CODEX_SNAP_TTL) codexBackgroundRefresh(0)
|
|
1352
|
+
return codexSnap
|
|
1353
|
+
}
|
|
1354
|
+
async function codexSummaryEnsured(maxWaitMs) {
|
|
1355
|
+
const stale = !codexSnap || Date.now() - codexSnapAt > CODEX_SNAP_TTL
|
|
1356
|
+
const p = codexScanP || (stale ? codexBackgroundRefresh(0) : null)
|
|
1357
|
+
if (!p) return codexSnap
|
|
1358
|
+
try { await Promise.race([p, new Promise((r) => setTimeout(r, Math.max(200, maxWaitMs || 2500)))]) } catch (err) {}
|
|
1359
|
+
return codexSnap
|
|
1360
|
+
}
|
|
1361
|
+
// 后台预热:启动后预热一次,并每 5 分钟刷新一次缓存(现在都是异步 + 有预算的,不再阻塞)。
|
|
1276
1362
|
let codexPrewarmT = null, codexPrewarmI = null
|
|
1277
1363
|
try {
|
|
1278
|
-
codexPrewarmT = setTimeout(function () { try {
|
|
1279
|
-
codexPrewarmI = setInterval(function () { try {
|
|
1364
|
+
codexPrewarmT = setTimeout(function () { try { codexBackgroundRefresh(0) } catch (err) {} }, 1500)
|
|
1365
|
+
codexPrewarmI = setInterval(function () { try { codexBackgroundRefresh(0) } catch (err) {} }, 5 * 60 * 1000)
|
|
1280
1366
|
// issue #109:这两个定时器原先不存句柄、也不进 disposers,插件树 dispose 之后
|
|
1281
1367
|
// 事件循环里仍有一个被引用的 interval → 用户退出 dsh 时进程不结束(只能 Ctrl+C)。
|
|
1282
1368
|
// 存句柄并纳入 disposers,卸载时一并清掉;unref 作为兜底(正常运行时由 HTTP 服务维持事件循环)。
|
|
@@ -1285,6 +1371,7 @@ export default {
|
|
|
1285
1371
|
disposers.push(function () {
|
|
1286
1372
|
if (codexPrewarmT !== null) { clearTimeout(codexPrewarmT); codexPrewarmT = null }
|
|
1287
1373
|
if (codexPrewarmI !== null) { clearInterval(codexPrewarmI); codexPrewarmI = null }
|
|
1374
|
+
if (codexCatchupT !== null) { clearTimeout(codexCatchupT); codexCatchupT = null }
|
|
1288
1375
|
})
|
|
1289
1376
|
} catch (err) {}
|
|
1290
1377
|
// ===== 自定义 API 模型注册表(v655)=====
|
|
@@ -1426,7 +1513,7 @@ export default {
|
|
|
1426
1513
|
}
|
|
1427
1514
|
// Codex 模式:本地会话统计即「探活结果」,不需要网络与密钥
|
|
1428
1515
|
if (tpl.kind === 'codex') {
|
|
1429
|
-
const cs =
|
|
1516
|
+
const cs = await codexSummaryEnsured(2500)
|
|
1430
1517
|
const f = (n) => (n >= 100000000 ? (n / 100000000).toFixed(2) + '亿' : (n >= 10000 ? (n / 10000).toFixed(1).replace(/\.0$/, '') + '万' : String(Math.round(Number(n) || 0))))
|
|
1431
1518
|
if (cs && cs.ok) {
|
|
1432
1519
|
return { ok: true, detail: 'Codex 本地会话:今日 ' + f(cs.todayTokens) + ' · 本月 ' + f(cs.monthTokens) + ' · 累计 ' + f(cs.totalTokens) + ' tokens(' + cs.sessions + ' 个会话文件)' }
|
|
@@ -1923,10 +2010,12 @@ export default {
|
|
|
1923
2010
|
entry.planSupport = !!tplM.quota
|
|
1924
2011
|
// Codex 模式:不查余额、不需要密钥,直接给本地会话统计(今日/本月/累计/近7天)
|
|
1925
2012
|
if (tplM.kind === 'codex') {
|
|
1926
|
-
|
|
2013
|
+
// 这里能等:模型列表请求可以稍等一下在途扫描(最多 2.5s),但绝不触发同步扫描
|
|
2014
|
+
if (!codexStats) codexStats = await codexSummaryEnsured(2500)
|
|
1927
2015
|
entry.codex = codexStats
|
|
1928
2016
|
entry.hasKey = true
|
|
1929
|
-
entry.error = codexStats && codexStats.ok ? null
|
|
2017
|
+
entry.error = codexStats && codexStats.ok ? null
|
|
2018
|
+
: (codexStats && codexStats.disabled ? null : ((codexStats && codexStats.error) || '未找到 Codex 会话目录'))
|
|
1930
2019
|
}
|
|
1931
2020
|
// 没有余额接口的厂商(如火山方舟):余额显示「—」,只用会话事件估算
|
|
1932
2021
|
entry.balanceMode = hasBalanceApi ? 'api' : 'events'
|
|
@@ -2005,7 +2094,7 @@ export default {
|
|
|
2005
2094
|
const led = readUsageLedger()
|
|
2006
2095
|
const summary = daySummary(led, todayKey())
|
|
2007
2096
|
return {
|
|
2008
|
-
...visible, version: '0.3.
|
|
2097
|
+
...visible, version: '0.3.6', isPeak: isPeakTime(Math.floor(Date.now() / 1000)),
|
|
2009
2098
|
todayUsage: summary.amount, todayUsageCurrency: summary.currency,
|
|
2010
2099
|
usageSource: summary.source, usageLabel: summary.label, usageMode: 'ledger',
|
|
2011
2100
|
accounting: balanceSummary(led, todayKey()),
|
|
@@ -2056,6 +2145,9 @@ export default {
|
|
|
2056
2145
|
scrollGapOn: parsed.scrollGapOn === true,
|
|
2057
2146
|
scrollGapPx: typeof parsed.scrollGapPx === 'number' ? Math.round(parsed.scrollGapPx) : 17,
|
|
2058
2147
|
menuBtnHide: parsed.menuBtnHide === true,
|
|
2148
|
+
// issue #116:Codex 本地统计可以在设置里关掉(默认开)。关掉后宿主完全不扫
|
|
2149
|
+
// ~/.codex/sessions,也不做后台预热。
|
|
2150
|
+
codexStatsOn: parsed.codexStatsOn !== false,
|
|
2059
2151
|
}
|
|
2060
2152
|
}
|
|
2061
2153
|
} catch (err) {}
|
|
@@ -2063,7 +2155,7 @@ export default {
|
|
|
2063
2155
|
return null
|
|
2064
2156
|
}
|
|
2065
2157
|
|
|
2066
|
-
function writeSizeConfig(scale, sound, vol, soundSet, usageMode, peakMode, bubbleOn, turnCostOn, turnCostCloseMs, scrollGapOn, scrollGapPx, menuBtnHide) {
|
|
2158
|
+
function writeSizeConfig(scale, sound, vol, soundSet, usageMode, peakMode, bubbleOn, turnCostOn, turnCostCloseMs, scrollGapOn, scrollGapPx, menuBtnHide, codexStatsOnArg) {
|
|
2067
2159
|
const um = normalizeUsageMode(usageMode)
|
|
2068
2160
|
const pm = peakMode === 'liangwen' || peakMode === 'qiangqiang' ? peakMode : 'default'
|
|
2069
2161
|
const bo = bubbleOn !== false
|
|
@@ -2072,6 +2164,7 @@ export default {
|
|
|
2072
2164
|
const sgo = scrollGapOn === true
|
|
2073
2165
|
const sgp = typeof scrollGapPx === 'number' && scrollGapPx > 0 ? Math.round(scrollGapPx) : 0
|
|
2074
2166
|
const mbh = menuBtnHide === true
|
|
2167
|
+
const cso = codexStatsOnArg !== false
|
|
2075
2168
|
const body = JSON.stringify({
|
|
2076
2169
|
scale: scale,
|
|
2077
2170
|
sound: sound !== false,
|
|
@@ -2085,6 +2178,7 @@ export default {
|
|
|
2085
2178
|
scrollGapOn: sgo,
|
|
2086
2179
|
scrollGapPx: sgp,
|
|
2087
2180
|
menuBtnHide: mbh,
|
|
2181
|
+
codexStatsOn: cso,
|
|
2088
2182
|
updatedAt: new Date().toISOString(),
|
|
2089
2183
|
})
|
|
2090
2184
|
let lastSizeErr = null
|
|
@@ -2105,6 +2199,7 @@ export default {
|
|
|
2105
2199
|
scrollGapOn: sgo,
|
|
2106
2200
|
scrollGapPx: sgp,
|
|
2107
2201
|
menuBtnHide: mbh,
|
|
2202
|
+
codexStatsOn: cso,
|
|
2108
2203
|
}
|
|
2109
2204
|
} catch (err) { lastSizeErr = err }
|
|
2110
2205
|
}
|
|
@@ -2512,9 +2607,12 @@ export default {
|
|
|
2512
2607
|
pickN(parsed.turnCostCloseMs, old.turnCostCloseMs, 5000),
|
|
2513
2608
|
pickB(parsed.scrollGapOn, old.scrollGapOn, false),
|
|
2514
2609
|
pickN(parsed.scrollGapPx, old.scrollGapPx, 17),
|
|
2515
|
-
pickB(parsed.menuBtnHide, old.menuBtnHide, false)
|
|
2610
|
+
pickB(parsed.menuBtnHide, old.menuBtnHide, false),
|
|
2611
|
+
pickB(parsed.codexStatsOn, old.codexStatsOn, true)
|
|
2516
2612
|
)
|
|
2517
2613
|
res.writeHead(result.ok ? 200 : 500, JSON_HEADERS)
|
|
2614
|
+
// 配置刚变(可能刚关掉/打开 Codex 统计)→ 丢掉旧快照,让下一次请求立刻反映新设置
|
|
2615
|
+
if (result.ok) { try { codexInvalidate() } catch (err) {} }
|
|
2518
2616
|
res.end(JSON.stringify(result))
|
|
2519
2617
|
} catch (err) {
|
|
2520
2618
|
res.writeHead(400, JSON_HEADERS)
|
package/package.json
CHANGED