opencode-visual-cache 1.4.0 → 1.6.0
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/README.md +37 -4
- package/README_EN.md +37 -4
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/balance-providers.d.ts +27 -0
- package/dist/balance-providers.js +131 -0
- package/dist/i18n.d.ts +102 -0
- package/dist/i18n.js +368 -0
- package/dist/index.js +585 -283
- package/dist/tui.js +1350 -453
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/balance-providers.ts +153 -0
- package/src/i18n.ts +380 -0
- package/src/index.tsx +687 -343
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/solid/jsx-runtime";
|
|
2
2
|
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, untrack } from "solid-js";
|
|
3
3
|
import { PLUGIN_VERSION } from "./_version";
|
|
4
|
+
import { balanceProviders, getBalanceProvider, maskKey, matchBalanceProvider } from "./balance-providers";
|
|
5
|
+
import { LANG_META, createT, detectLang } from "./i18n";
|
|
4
6
|
// ── terminal-width helpers ────────────────────────────────────────
|
|
5
7
|
// CJK characters occupy 2 terminal columns; padEnd/padStart count
|
|
6
8
|
// string length (=1 per char), which breaks alignment with mixed text.
|
|
@@ -51,97 +53,13 @@ function truncateVisual(s, maxCols) {
|
|
|
51
53
|
}
|
|
52
54
|
return result;
|
|
53
55
|
}
|
|
54
|
-
// ── language override (env: CACHE_TUI_LANG) ──
|
|
55
|
-
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined;
|
|
56
56
|
// ── language ──────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
})();
|
|
67
|
-
const ZH_T = {
|
|
68
|
-
title: "缓存统计",
|
|
69
|
-
hit: "命中率",
|
|
70
|
-
totalHit: "总命中:",
|
|
71
|
-
read: "缓存读:",
|
|
72
|
-
write: "缓存写:",
|
|
73
|
-
miss: "未命中:",
|
|
74
|
-
out: "输出:",
|
|
75
|
-
cost: "费用:",
|
|
76
|
-
saved: "累计节省:",
|
|
77
|
-
model: "模型:",
|
|
78
|
-
provider: "提供商:",
|
|
79
|
-
rate: "单价:",
|
|
80
|
-
hitFolded: "命中",
|
|
81
|
-
inputRate: "输入",
|
|
82
|
-
cacheRate: "缓存",
|
|
83
|
-
writeRate: "写入",
|
|
84
|
-
noData: "等待缓存数据...",
|
|
85
|
-
tok: "tok",
|
|
86
|
-
distTitle: "估算 Token 分布",
|
|
87
|
-
distSys: "系统提示:",
|
|
88
|
-
distUser: "用户:",
|
|
89
|
-
distAgent: "Agent 指令:",
|
|
90
|
-
distTool: "Tool 调用:",
|
|
91
|
-
distRes: "Tool 结果:",
|
|
92
|
-
distTotal: "总计:",
|
|
93
|
-
distOut: "输出:",
|
|
94
|
-
secDetail: "明细",
|
|
95
|
-
secModel: "模型",
|
|
96
|
-
secSkills: "已加载技能",
|
|
97
|
-
balTotal: "总余额:",
|
|
98
|
-
balNoKey: "未配置 API Key",
|
|
99
|
-
balLoading: "查询中...",
|
|
100
|
-
balError: "查询失败",
|
|
101
|
-
balErr401: "API Key 无效",
|
|
102
|
-
balErr403: "余额查询被拒绝",
|
|
103
|
-
balErrEmpty: "未获取到余额数据",
|
|
104
|
-
balErrTimeout: "查询超时",
|
|
105
|
-
};
|
|
106
|
-
const EN_T = {
|
|
107
|
-
title: "Token Cache",
|
|
108
|
-
hit: "Hit",
|
|
109
|
-
totalHit: "Total Hit:",
|
|
110
|
-
read: "Read:",
|
|
111
|
-
write: "Write:",
|
|
112
|
-
miss: "Miss:",
|
|
113
|
-
out: "Out:",
|
|
114
|
-
cost: "Cost:",
|
|
115
|
-
saved: "Total Saved:",
|
|
116
|
-
model: "Model:",
|
|
117
|
-
provider: "Provider:",
|
|
118
|
-
rate: "Rate:",
|
|
119
|
-
hitFolded: "hit",
|
|
120
|
-
inputRate: "in",
|
|
121
|
-
cacheRate: "cache",
|
|
122
|
-
writeRate: "write",
|
|
123
|
-
noData: "Waiting for cache data...",
|
|
124
|
-
tok: "tok",
|
|
125
|
-
distTitle: "Estimated Token Dist.",
|
|
126
|
-
distSys: "System:",
|
|
127
|
-
distUser: "User:",
|
|
128
|
-
distAgent: "Agent Instr:",
|
|
129
|
-
distTool: "Tool Call:",
|
|
130
|
-
distRes: "Tool Result:",
|
|
131
|
-
distTotal: "Total:",
|
|
132
|
-
distOut: "Output:",
|
|
133
|
-
secDetail: "Detail",
|
|
134
|
-
secModel: "Model",
|
|
135
|
-
secSkills: "Loaded Skills",
|
|
136
|
-
balTotal: "Total:",
|
|
137
|
-
balNoKey: "No API Key set",
|
|
138
|
-
balLoading: "Fetching...",
|
|
139
|
-
balError: "Fetch failed",
|
|
140
|
-
balErr401: "Invalid API Key",
|
|
141
|
-
balErr403: "Balance request rejected",
|
|
142
|
-
balErrEmpty: "No balance data",
|
|
143
|
-
balErrTimeout: "Request timed out",
|
|
144
|
-
};
|
|
57
|
+
// 语言初始化:环境变量 CACHE_TUI_LANG 覆盖 → 否则按系统 locale 自动检测。
|
|
58
|
+
// 用户通过 /cache-lang 设置的偏好会在 KV 就绪后优先覆盖(见 tui() 内恢复逻辑)。
|
|
59
|
+
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined;
|
|
60
|
+
const INIT_LANG = DEBUG_LANG !== undefined && LANG_META.some((m) => m.code === DEBUG_LANG)
|
|
61
|
+
? DEBUG_LANG
|
|
62
|
+
: detectLang();
|
|
145
63
|
// ── color helpers ────────────────────────────────────────────────
|
|
146
64
|
/** Extract { r, g, b } (0–255) from a hex string or RGBA-like object. */
|
|
147
65
|
function rgb(raw) {
|
|
@@ -200,7 +118,7 @@ function desaturateTo(raw, maxSat, fallback) {
|
|
|
200
118
|
* converges to within a fraction of an 8‑bit step, eliminating
|
|
201
119
|
* colour banding in edge cases.
|
|
202
120
|
*/
|
|
203
|
-
//
|
|
121
|
+
// Bt.601 luma (perceptual brightness used as the grey anchor)
|
|
204
122
|
const luma = c.r * 0.299 + c.g * 0.587 + c.b * 0.114;
|
|
205
123
|
let lo = 0, hi = 1;
|
|
206
124
|
for (let i = 0; i < 12; i++) {
|
|
@@ -316,27 +234,6 @@ function estimateTokens(text) {
|
|
|
316
234
|
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.0));
|
|
317
235
|
}
|
|
318
236
|
const BALANCE_POLL_MS = 5 * 60 * 1000; // 5 minutes
|
|
319
|
-
async function fetchDeepSeekBalance(apiKey, signal) {
|
|
320
|
-
const res = await fetch("https://api.deepseek.com/user/balance", {
|
|
321
|
-
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
322
|
-
signal,
|
|
323
|
-
});
|
|
324
|
-
if (!res.ok) {
|
|
325
|
-
if (res.status === 401)
|
|
326
|
-
throw new Error("401");
|
|
327
|
-
if (res.status === 402 || res.status === 403)
|
|
328
|
-
throw new Error("403");
|
|
329
|
-
throw new Error(String(res.status));
|
|
330
|
-
}
|
|
331
|
-
const json = await res.json();
|
|
332
|
-
const infos = json.balance_infos ?? [];
|
|
333
|
-
if (infos.length === 0)
|
|
334
|
-
throw new Error("EMPTY");
|
|
335
|
-
return infos.map((info) => ({
|
|
336
|
-
currency: info.currency ?? "CNY",
|
|
337
|
-
total: info.total_balance ?? "0",
|
|
338
|
-
}));
|
|
339
|
-
}
|
|
340
237
|
/**
|
|
341
238
|
* 将余额从来源币种换算为目标币种。
|
|
342
239
|
* DEFAULT_RATES 以 USD=1 为基准:先折算为 USD,再换算到目标币种。
|
|
@@ -348,11 +245,69 @@ function convertBalance(target, targetRate, amount, from) {
|
|
|
348
245
|
const usd = from === "USD" ? amount : amount / fromRate;
|
|
349
246
|
return target === "USD" ? usd : usd * targetRate;
|
|
350
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* 从 OpenCode 已认证的 provider 读取 API key 作为余额查询的自动兜底。
|
|
250
|
+
* 匹配复用前缀逻辑:先精确匹配 id,再前缀匹配(如 moonshotai-cn → moonshot)。
|
|
251
|
+
* key 来源:auth.json(provider.key)或配置(provider.options.apiKey)。
|
|
252
|
+
* 仅当手动配置的 key 缺失时使用;读取失败或未匹配返回空串。
|
|
253
|
+
*/
|
|
254
|
+
function findOpencodeKey(api, provider) {
|
|
255
|
+
try {
|
|
256
|
+
const provs = api.state.provider;
|
|
257
|
+
// 大小写不敏感:精确匹配 id,否则前缀匹配(如 moonshotai-cn → moonshot)
|
|
258
|
+
const id = provider.id.toLowerCase();
|
|
259
|
+
const hit = provs.find((p) => p.id.toLowerCase() === id) ?? provs.find((p) => p.id.toLowerCase().startsWith(id));
|
|
260
|
+
if (!hit)
|
|
261
|
+
return "";
|
|
262
|
+
const k = typeof hit.key === "string" ? hit.key : "";
|
|
263
|
+
if (k)
|
|
264
|
+
return k;
|
|
265
|
+
return typeof hit.options?.apiKey === "string" ? hit.options.apiKey : "";
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
351
271
|
/** 货币符号:优先取 /cache-currency 内置映射,未知币种回退为代码。 */
|
|
352
272
|
function balanceSymbol(currency) {
|
|
353
273
|
const sym = CURRENCIES[currency];
|
|
354
274
|
return sym ?? currency + " ";
|
|
355
275
|
}
|
|
276
|
+
/** 紧凑数字缩写(底部状态栏用):1234 → "1.2K",1234567 → "1.2M"。 */
|
|
277
|
+
function fmtCompact(n) {
|
|
278
|
+
if (n >= 1e6)
|
|
279
|
+
return (n / 1e6).toFixed(1) + "M";
|
|
280
|
+
if (n >= 1e3)
|
|
281
|
+
return (n / 1e3).toFixed(1) + "K";
|
|
282
|
+
return String(Math.round(n));
|
|
283
|
+
}
|
|
284
|
+
/** 余额数值格式化:≥1 或 0 显示固定 2 位小数;小额(<1)保留精度(最多 6 位),避免抹成 0.00。 */
|
|
285
|
+
function formatBalanceAmount(total) {
|
|
286
|
+
const n = parseFloat(total);
|
|
287
|
+
if (!Number.isFinite(n))
|
|
288
|
+
return total;
|
|
289
|
+
if (n === 0 || n >= 1)
|
|
290
|
+
return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
291
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 将余额列表格式化为单行文本。
|
|
295
|
+
* 优先直接显示偏好币种(CNY/USD…);偏好币种为换算币种时按汇率折算第一条余额。
|
|
296
|
+
*/
|
|
297
|
+
function formatBalanceText(list, pref, rate) {
|
|
298
|
+
const native = pref ? list.find((x) => x.currency === pref) : undefined;
|
|
299
|
+
if (native)
|
|
300
|
+
return balanceSymbol(native.currency) + formatBalanceAmount(native.total);
|
|
301
|
+
const base = list[0];
|
|
302
|
+
const baseAmt = parseFloat(base.total);
|
|
303
|
+
const converted = Number.isFinite(baseAmt)
|
|
304
|
+
? convertBalance(pref || base.currency, rate, baseAmt, base.currency)
|
|
305
|
+
: baseAmt;
|
|
306
|
+
const shown = pref && base.currency !== pref
|
|
307
|
+
? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
|
|
308
|
+
: formatBalanceAmount(base.total);
|
|
309
|
+
return balanceSymbol(pref || base.currency) + shown;
|
|
310
|
+
}
|
|
356
311
|
const CURRENCIES = {
|
|
357
312
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
358
313
|
};
|
|
@@ -379,9 +334,9 @@ function TokenCachePanel(props) {
|
|
|
379
334
|
const [skillsOpen, setSkillsOpen] = createSignal(true);
|
|
380
335
|
let boxEl;
|
|
381
336
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
382
|
-
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate,
|
|
383
|
-
// ── reactive translation (follows
|
|
384
|
-
const t =
|
|
337
|
+
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langCode, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, sectionSkills, setSectionSkills, sectionBalance, setSectionBalance, balanceRefresh, balanceProviderId, setBalanceProviderId, autoBalance, setAutoBalance, balanceUnsupported, setBalanceUnsupported, balanceState, balanceCurrency, setBalanceCurrency, borderVisible, setBorderVisible, } = props.signals;
|
|
338
|
+
// ── reactive translation (follows langCode signal) ──
|
|
339
|
+
const t = createT(() => langCode());
|
|
385
340
|
// ── scan session messages reactively ──
|
|
386
341
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
387
342
|
// api.state.session state changes — no event listener needed.
|
|
@@ -406,51 +361,47 @@ function TokenCachePanel(props) {
|
|
|
406
361
|
hasSkills: false,
|
|
407
362
|
});
|
|
408
363
|
const [refreshTick, setRefreshTick] = createSignal(0);
|
|
409
|
-
//
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const key = props.api.kv.get(`${KV_PREFIX}.ds_key`, "");
|
|
417
|
-
if (!key) {
|
|
418
|
-
setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
|
|
364
|
+
// 当前 provider 显示名(余额查询状态为共享信号,见 PanelSignals.balanceState)
|
|
365
|
+
const providerName = createMemo(() => getBalanceProvider(balanceProviderId()).name);
|
|
366
|
+
// 自动切换当前会话的 provider(前缀匹配)。手动切换会关闭此行为。
|
|
367
|
+
// 直接追踪 messages 取最后一条 assistant 消息的 providerID——
|
|
368
|
+
// 不依赖 session.model 的响应式更新(模型切换时该链路可能不触发重算)。
|
|
369
|
+
createEffect(() => {
|
|
370
|
+
if (!autoBalance())
|
|
419
371
|
return;
|
|
372
|
+
const sid = props.signals.overrideSessionId() ?? props.sessionId;
|
|
373
|
+
const msgs = props.api.state.session.messages(sid);
|
|
374
|
+
let pid = "";
|
|
375
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
376
|
+
const m = msgs[i];
|
|
377
|
+
if (m.role === "assistant" && m.providerID) {
|
|
378
|
+
pid = m.providerID;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
420
381
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
let timedOut = false;
|
|
430
|
-
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, 10_000);
|
|
431
|
-
try {
|
|
432
|
-
const data = await fetchDeepSeekBalance(key, controller.signal);
|
|
433
|
-
clearTimeout(timer);
|
|
434
|
-
if (seq !== balanceSeq)
|
|
435
|
-
return; // 已被更新的请求取代,丢弃过期结果
|
|
436
|
-
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key });
|
|
382
|
+
// 会话尚无 assistant 消息(新会话 / 刚切换模型未对话 / 消息未加载)
|
|
383
|
+
// → 回退到会话级模型元数据,反映当前正在使用的 provider
|
|
384
|
+
if (!pid) {
|
|
385
|
+
try {
|
|
386
|
+
const session = props.api.state.session.get(sid);
|
|
387
|
+
pid = session?.model?.providerID ?? "";
|
|
388
|
+
}
|
|
389
|
+
catch { /* ignore */ }
|
|
437
390
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
391
|
+
if (!pid)
|
|
392
|
+
return;
|
|
393
|
+
const hit = matchBalanceProvider(pid);
|
|
394
|
+
if (hit) {
|
|
395
|
+
setBalanceUnsupported(false);
|
|
396
|
+
if (hit.id !== balanceProviderId()) {
|
|
397
|
+
setBalanceProviderId(hit.id);
|
|
398
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// 当前提供商没有余额适配器 → 标记不支持,余额显示 N/A 并停止轮询
|
|
403
|
+
setBalanceUnsupported(true);
|
|
445
404
|
}
|
|
446
|
-
};
|
|
447
|
-
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
448
|
-
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
449
|
-
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
450
|
-
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
451
|
-
createEffect(() => {
|
|
452
|
-
void balanceRefresh();
|
|
453
|
-
untrack(() => { void pollBalance(); });
|
|
454
405
|
});
|
|
455
406
|
// ── auto-clear override when the user navigates to a different main session ──
|
|
456
407
|
let lastMainSid = props.sessionId;
|
|
@@ -489,19 +440,19 @@ function TokenCachePanel(props) {
|
|
|
489
440
|
for (const msg of msgs) {
|
|
490
441
|
if (msg.role !== "assistant")
|
|
491
442
|
continue;
|
|
492
|
-
const
|
|
493
|
-
if (!
|
|
443
|
+
const tok = msg.tokens;
|
|
444
|
+
if (!tok)
|
|
494
445
|
continue;
|
|
495
|
-
const mit = num(
|
|
446
|
+
const mit = num(tok.input) + num(tok.cache?.read) + num(tok.cache?.write), mrt = num(tok.cache?.read);
|
|
496
447
|
if (mit > 0) {
|
|
497
448
|
prevMsgHitRate = lastMsgHitRate;
|
|
498
449
|
lastMsgHitRate = (mrt / mit) * 100;
|
|
499
450
|
}
|
|
500
451
|
if (fallbackTokens) {
|
|
501
|
-
input += num(
|
|
502
|
-
read += num(
|
|
503
|
-
write += num(
|
|
504
|
-
output += num(
|
|
452
|
+
input += num(tok.input);
|
|
453
|
+
read += num(tok.cache?.read);
|
|
454
|
+
write += num(tok.cache?.write);
|
|
455
|
+
output += num(tok.output);
|
|
505
456
|
}
|
|
506
457
|
if (fallbackCost) {
|
|
507
458
|
cost += num(msg.cost);
|
|
@@ -527,7 +478,8 @@ function TokenCachePanel(props) {
|
|
|
527
478
|
break;
|
|
528
479
|
}
|
|
529
480
|
const hitRate = lastMsgHitRate >= 0 ? lastMsgHitRate : 0;
|
|
530
|
-
|
|
481
|
+
// 总命中率分母含缓存写(业界口径:read / (input+read+write))
|
|
482
|
+
const freshTotal = input + read + write, sessionHitRate = freshTotal > 0 ? (read / freshTotal) * 100 : 0;
|
|
531
483
|
const model = mid.split("/").pop() ?? mid, hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0;
|
|
532
484
|
const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0;
|
|
533
485
|
const trend = hasTrendData ? lastMsgHitRate - prevMsgHitRate : 0, providerName = pid || "";
|
|
@@ -626,14 +578,14 @@ function TokenCachePanel(props) {
|
|
|
626
578
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
627
579
|
if (msgs[i].role !== "assistant")
|
|
628
580
|
continue;
|
|
629
|
-
const
|
|
630
|
-
if (
|
|
581
|
+
const tok = msgs[i].tokens;
|
|
582
|
+
if (tok && ((tok.input ?? 0) > 0 || (tok.cache?.read ?? 0) > 0 || (tok.cache?.write ?? 0) > 0)) {
|
|
631
583
|
lastAssMsg = msgs[i];
|
|
632
584
|
break;
|
|
633
585
|
}
|
|
634
586
|
}
|
|
635
|
-
//
|
|
636
|
-
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read);
|
|
587
|
+
// 取最后一条有数据消息的总输入(含缓存读/写)作为当前 context 大小
|
|
588
|
+
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read) + num(lastAssMsg?.tokens?.cache?.write);
|
|
637
589
|
dist.apiOutput = num(lastAssMsg?.tokens?.output);
|
|
638
590
|
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0;
|
|
639
591
|
}
|
|
@@ -705,6 +657,26 @@ function TokenCachePanel(props) {
|
|
|
705
657
|
const balCur = props.api.kv.get(`${KV_PREFIX}.balance_currency`);
|
|
706
658
|
if (typeof balCur === "string")
|
|
707
659
|
setBalanceCurrency(balCur);
|
|
660
|
+
// Restore balance provider (fall back to default when unknown)
|
|
661
|
+
const savedProvider = props.api.kv.get(`${KV_PREFIX}.balance.provider`);
|
|
662
|
+
if (typeof savedProvider === "string" && balanceProviders.some((p) => p.id === savedProvider)) {
|
|
663
|
+
setBalanceProviderId(savedProvider);
|
|
664
|
+
setBalanceUnsupported(false);
|
|
665
|
+
}
|
|
666
|
+
// Restore auto-switch (default on)
|
|
667
|
+
const savedAuto = props.api.kv.get(`${KV_PREFIX}.balance.auto`);
|
|
668
|
+
if (typeof savedAuto === "boolean")
|
|
669
|
+
setAutoBalance(savedAuto);
|
|
670
|
+
// Migrate legacy DeepSeek key (cache_panel.ds_key → cache_panel.balance.deepseek.key)
|
|
671
|
+
const legacyKey = props.api.kv.get(`${KV_PREFIX}.ds_key`, "");
|
|
672
|
+
if (legacyKey) {
|
|
673
|
+
const dsKey = props.api.kv.get(`${KV_PREFIX}.balance.deepseek.key`, "");
|
|
674
|
+
if (!dsKey)
|
|
675
|
+
props.api.kv.set(`${KV_PREFIX}.balance.deepseek.key`, legacyKey);
|
|
676
|
+
props.api.kv.set(`${KV_PREFIX}.ds_key`, "");
|
|
677
|
+
}
|
|
678
|
+
// 恢复的 provider 可能与默认值不同,强制重新查询
|
|
679
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
|
|
708
680
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
|
|
709
681
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
|
|
710
682
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
@@ -712,11 +684,6 @@ function TokenCachePanel(props) {
|
|
|
712
684
|
setSectionBalance(Boolean(props.api.kv.get(`${KV_PREFIX}.section.balance`, true)));
|
|
713
685
|
const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
714
686
|
setBorderVisible(bv !== false);
|
|
715
|
-
// Restore language preference
|
|
716
|
-
const savedLang = props.api.kv.get(`${KV_PREFIX}.lang`);
|
|
717
|
-
if (savedLang === "zh" || savedLang === "en") {
|
|
718
|
-
setLangZH(savedLang === "zh");
|
|
719
|
-
}
|
|
720
687
|
// Restore distribution snapshot so the token distribution block
|
|
721
688
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
722
689
|
const cachedDist = props.api.kv.get(`${KV_PREFIX}.dist_snapshot`);
|
|
@@ -767,8 +734,7 @@ function TokenCachePanel(props) {
|
|
|
767
734
|
const unsubMsg = props.api.event.on("message.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1); });
|
|
768
735
|
const unsubSession = props.api.event.on("session.updated", () => { setRefreshTick(v => v + 1); });
|
|
769
736
|
setRefreshTick(v => v + 1);
|
|
770
|
-
|
|
771
|
-
onCleanup(() => { clearTimeout(partTimer); clearInterval(balanceTimer); unsubPart(); unsubMsg(); unsubSession(); });
|
|
737
|
+
onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg(); unsubSession(); });
|
|
772
738
|
});
|
|
773
739
|
// ── colours ──
|
|
774
740
|
// Pull from the current theme, auto-desaturate if too punchy,
|
|
@@ -798,11 +764,14 @@ function TokenCachePanel(props) {
|
|
|
798
764
|
const gutter = createMemo(() => borderVisible() ? 6 : 0);
|
|
799
765
|
const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - gutter())));
|
|
800
766
|
function trendLabel(t) {
|
|
801
|
-
|
|
767
|
+
// |t| < 0.05 视为无变化:避免显示 "↑0.0%" 的矛盾(箭头存在但数值截断为零)
|
|
768
|
+
if (Math.abs(t) < 0.05)
|
|
769
|
+
return "-";
|
|
770
|
+
return (t > 0 ? "\u2191" : "\u2193") + Math.abs(t).toFixed(1) + "%";
|
|
802
771
|
}
|
|
803
772
|
const barW = createMemo(() => {
|
|
804
773
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0;
|
|
805
|
-
const overhead = visualWidth(t(
|
|
774
|
+
const overhead = visualWidth(t("hit")) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter();
|
|
806
775
|
return Math.max(3, panelWidth() - overhead);
|
|
807
776
|
});
|
|
808
777
|
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
@@ -828,49 +797,184 @@ function TokenCachePanel(props) {
|
|
|
828
797
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
829
798
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
830
799
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
831
|
-
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: t()
|
|
832
|
-
const prefix = " \u21b3 " + (
|
|
800
|
+
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: t("title") }), _jsx(Show, { when: open(), children: _jsxs("span", { style: { fg: dimColor(pal().muted, 0.75) }, children: [" v", PLUGIN_VERSION] }) })] }), _jsxs(Show, { when: !open() && data().hasData, children: [_jsxs(Show, { when: data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t("title")) - visualWidth(pct() + " " + t("hitFolded") + " " + trendLabel(data().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", t("hitFolded")] }), _jsxs("span", { style: { fg: Math.abs(data().trend) >= 0.05 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] })] }), _jsxs(Show, { when: !data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t("title")) - visualWidth(pct() + " " + t("hitFolded")))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", t("hitFolded")] })] })] })] }), _jsxs(Show, { when: open(), children: [_jsx(Show, { when: props.signals.overrideSessionId(), children: (() => {
|
|
801
|
+
const prefix = " \u21b3 " + t("subPrefix");
|
|
833
802
|
const maxSidW = Math.max(6, panelWidth() - visualWidth(prefix));
|
|
834
803
|
return (_jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: prefix }), _jsx("span", { style: { fg: pal().text }, children: truncateVisual(props.signals.overrideSessionId(), maxSidW) })] }));
|
|
835
|
-
})() }), _jsxs(Show, { when: data().hasData, fallback: _jsxs(_Fragment, { children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { style: { fg: pal().muted }, children: t()
|
|
836
|
-
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t(
|
|
804
|
+
})() }), _jsxs(Show, { when: data().hasData, fallback: _jsxs(_Fragment, { children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { style: { fg: pal().muted }, children: t("noData") })] })] }), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsxs("span", { style: { fg: pal().text }, children: [t("hit"), " "] }), _jsxs("span", { style: { fg: hitColor() }, children: ["[", bar(), "] "] }), _jsx("span", { style: { fg: pal().text }, children: pct() }), _jsx(Show, { when: data().hasTrendData, children: _jsxs("span", { style: { fg: Math.abs(data().trend) >= 0.05 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] }) })] }), _jsx("text", { fg: pal().muted, children: justify(t("totalHit"), (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%") }), _jsxs(Show, { when: sectionDetail(), children: [_jsxs("text", { onMouseUp: () => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: detailOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t("secDetail") }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + t("secDetail"))) })] }), _jsxs(Show, { when: detailOpen(), children: [_jsx(Show, { when: data().read > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("read"), fmt(data().read), t("tok")) }) }), _jsx(Show, { when: data().write > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("write"), fmt(data().write), t("tok")) }) }), _jsx("text", { fg: pal().muted, children: justify(t("miss"), fmt(data().freshInput + data().write), t("tok")) }), _jsx("text", { fg: pal().muted, children: justify(t("out"), fmt(data().output), t("tok")) }), _jsx(Show, { when: data().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: t("saved") }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(t("saved")) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate())))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved, currencySymbol(), exchangeRate())] })] }) })] })] }), _jsxs(Show, { when: sectionModel(), children: [_jsxs("text", { onMouseUp: () => setModelOpen((o) => { const n = !o; persistFold("model", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: modelOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t("secModel") }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t("secModel"))) })] }), _jsxs(Show, { when: modelOpen(), children: [_jsx("text", { fg: pal().text, children: justify(t("cost"), fmtCost(data().cost, currencySymbol(), exchangeRate())) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(t("provider"), data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(t("model"), data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(t("rate"), currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t("inputRate")) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t("cacheRate")) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t("writeRate")) }) })] })] })] }), _jsx(Show, { when: sectionDist(), children: _jsxs(Show, { when: data().hasDistData, children: [_jsxs("text", { onMouseUp: () => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: distOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t("distTitle") }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t("distTitle"))) })] }), _jsxs(Show, { when: distOpen(), children: [_jsx(Show, { when: data().dist.system > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distSys"), fmt(data().dist.system), t("tok")) }) }), _jsx(Show, { when: data().dist.user > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distUser"), fmt(data().dist.user), t("tok")) }) }), _jsx(Show, { when: data().dist.agent > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distAgent"), fmt(data().dist.agent), t("tok")) }) }), _jsx(Show, { when: data().dist.toolCall > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distTool"), fmt(data().dist.toolCall), t("tok")) }) }), _jsx(Show, { when: data().dist.toolResult > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distRes"), fmt(data().dist.toolResult), t("tok")) }) }), _jsx("text", { fg: pal().text, children: justify(t("distTotal"), fmt(data().dist.apiInput), t("tok")) })] })] }) }), _jsx(Show, { when: sectionSkills(), children: _jsxs(Show, { when: data().hasSkills, children: [_jsxs("text", { onMouseUp: () => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: skillsOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t("secSkills") }) }), _jsxs("span", { style: { fg: pal().muted }, children: [" (", data().skills.length, ")"] }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t("secSkills") + ` (${data().skills.length})`)) })] }), _jsx(Show, { when: skillsOpen(), children: data().skills.map((sk) => {
|
|
805
|
+
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t("tok"));
|
|
837
806
|
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
|
|
838
807
|
const label = truncateVisual(sk.name, maxLabel);
|
|
839
|
-
return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t(
|
|
840
|
-
}) })] }) }), _jsxs(Show, { when: sectionBalance(), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsx(Show, { when: balanceState().status === "idle", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t(
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const list = balanceState().data;
|
|
853
|
-
const pref = balanceCurrency();
|
|
854
|
-
// 偏好币种是 DeepSeek 原生返回的(CNY/USD)→ 直接显示
|
|
855
|
-
const native = pref ? list.find(x => x.currency === pref) : undefined;
|
|
856
|
-
if (native) {
|
|
857
|
-
return (_jsx("text", { fg: pal().text, children: justify(t().balTotal, balanceSymbol(native.currency) + native.total) }));
|
|
858
|
-
}
|
|
859
|
-
// 非原生币种(EUR/JPY/GBP/KRW…)→ 取第一条余额按汇率换算
|
|
860
|
-
const base = list[0];
|
|
861
|
-
const baseAmt = parseFloat(base.total);
|
|
862
|
-
const converted = Number.isFinite(baseAmt)
|
|
863
|
-
? convertBalance(pref || base.currency, exchangeRate(), baseAmt, base.currency)
|
|
864
|
-
: baseAmt;
|
|
865
|
-
const shown = pref && base.currency !== pref
|
|
866
|
-
? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
|
|
867
|
-
: base.total;
|
|
868
|
-
return (_jsx("text", { fg: pal().text, children: justify(t().balTotal, balanceSymbol(pref || base.currency) + shown) }));
|
|
869
|
-
})() })] })] })] })] }));
|
|
808
|
+
return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t("tok")) }));
|
|
809
|
+
}) })] }) }), _jsxs(Show, { when: sectionBalance(), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsx(Show, { when: balanceUnsupported(), children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balUnsupported") })] }) }), _jsxs(Show, { when: !balanceUnsupported(), children: [_jsx(Show, { when: balanceState().status === "idle", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balNoKey", { p: providerName() }) })] }) }), _jsx(Show, { when: balanceState().status === "loading", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balLoading") })] }) }), _jsx(Show, { when: balanceState().status === "error", children: _jsxs("text", { fg: pal().error, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: (() => {
|
|
810
|
+
const code = balanceState().error;
|
|
811
|
+
if (code === "401")
|
|
812
|
+
return t("balErr401");
|
|
813
|
+
if (code === "403")
|
|
814
|
+
return t("balErr403");
|
|
815
|
+
if (code === "EMPTY")
|
|
816
|
+
return t("balErrEmpty");
|
|
817
|
+
if (code === "TIMEOUT")
|
|
818
|
+
return t("balErrTimeout");
|
|
819
|
+
return t("balError") + (code ? ` (${code})` : "");
|
|
820
|
+
})() })] }) }), _jsx(Show, { when: balanceState().status === "ok" && balanceState().data, children: _jsx("text", { fg: pal().text, children: justify(t("balTotal"), formatBalanceText(balanceState().data, balanceCurrency(), exchangeRate())) }) })] })] })] })] })] }));
|
|
870
821
|
}
|
|
871
822
|
// ---------------------------------------------------------------------------
|
|
872
823
|
// Plugin entry
|
|
873
824
|
// ---------------------------------------------------------------------------
|
|
825
|
+
/**
|
|
826
|
+
* 输入框 hint 行(session_prompt slot 的 hint):单行显示 路径 · 命中率 · 余额 · Tokens。
|
|
827
|
+
* 通过 ui.Prompt 的 hint prop 注入——宿主右侧的 token/commands 提示自动保留,
|
|
828
|
+
* 三合一信息与路径同行显示在中间位置。
|
|
829
|
+
*/
|
|
830
|
+
function BottomStatusBar(props) {
|
|
831
|
+
const KV_PREFIX = "cache_panel";
|
|
832
|
+
const t = createT(() => props.signals.langCode());
|
|
833
|
+
const sid = props.sessionId;
|
|
834
|
+
// ── 命中率(单条口径:最后一条有 token 的 assistant 消息)+ token 汇总 ──
|
|
835
|
+
const stats = createMemo(() => {
|
|
836
|
+
const id = sid;
|
|
837
|
+
if (!id)
|
|
838
|
+
return null;
|
|
839
|
+
const msgs = props.api.state.session.messages(id);
|
|
840
|
+
const session = typeof props.api.state.session.get === "function"
|
|
841
|
+
? props.api.state.session.get(id)
|
|
842
|
+
: undefined;
|
|
843
|
+
let input = session?.tokens?.input ?? 0;
|
|
844
|
+
let read = session?.tokens?.cache?.read ?? 0;
|
|
845
|
+
let write = session?.tokens?.cache?.write ?? 0;
|
|
846
|
+
// 旧 SDK 无 session 聚合字段 → 遍历消息累加(与侧边栏 fallback 一致)
|
|
847
|
+
if (session?.tokens == null) {
|
|
848
|
+
for (const m of msgs) {
|
|
849
|
+
if (m.role !== "assistant")
|
|
850
|
+
continue;
|
|
851
|
+
const tk = m.tokens;
|
|
852
|
+
if (!tk)
|
|
853
|
+
continue;
|
|
854
|
+
input += num(tk.input);
|
|
855
|
+
read += num(tk.cache?.read);
|
|
856
|
+
write += num(tk.cache?.write);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// 从后往前取最后两条有 token 数据的 assistant 消息 → 单条命中率 + 趋势
|
|
860
|
+
// 分母含缓存写(业界口径:read / (input+read+write))
|
|
861
|
+
let hitRate = -1, prevHitRate = -1;
|
|
862
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
863
|
+
const m = msgs[i];
|
|
864
|
+
if (m.role !== "assistant")
|
|
865
|
+
continue;
|
|
866
|
+
const tk = m.tokens;
|
|
867
|
+
if (!tk)
|
|
868
|
+
continue;
|
|
869
|
+
const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write);
|
|
870
|
+
const mrt = num(tk.cache?.read);
|
|
871
|
+
if (mit <= 0)
|
|
872
|
+
continue;
|
|
873
|
+
const rate = (mrt / mit) * 100;
|
|
874
|
+
if (hitRate < 0) {
|
|
875
|
+
hitRate = rate;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
prevHitRate = rate;
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
return { hitRate, prevHitRate, input, read, write };
|
|
882
|
+
});
|
|
883
|
+
// 余额查询状态为共享信号(PanelSignals.balanceState),由 tui() 统一轮询
|
|
884
|
+
// 自动切换 provider(跟随当前会话模型;幂等,与侧边栏共享信号)
|
|
885
|
+
createEffect(() => {
|
|
886
|
+
if (!props.signals.autoBalance())
|
|
887
|
+
return;
|
|
888
|
+
const id = sid;
|
|
889
|
+
if (!id)
|
|
890
|
+
return;
|
|
891
|
+
const msgs = props.api.state.session.messages(id);
|
|
892
|
+
let pid = "";
|
|
893
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
894
|
+
const m = msgs[i];
|
|
895
|
+
if (m.role === "assistant" && m.providerID) {
|
|
896
|
+
pid = m.providerID;
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (!pid) {
|
|
901
|
+
try {
|
|
902
|
+
pid = props.api.state.session.get(id)?.model?.providerID ?? "";
|
|
903
|
+
}
|
|
904
|
+
catch { }
|
|
905
|
+
}
|
|
906
|
+
if (!pid)
|
|
907
|
+
return;
|
|
908
|
+
const hit = matchBalanceProvider(pid);
|
|
909
|
+
if (hit) {
|
|
910
|
+
props.signals.setBalanceUnsupported(false);
|
|
911
|
+
if (hit.id !== props.signals.balanceProviderId()) {
|
|
912
|
+
props.signals.setBalanceProviderId(hit.id);
|
|
913
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
// 当前提供商没有余额适配器 → 标记不支持,余额显示 N/A 并停止轮询
|
|
918
|
+
props.signals.setBalanceUnsupported(true);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
// ── 主题色(与侧边栏同口径)──
|
|
922
|
+
const pal = createMemo(() => {
|
|
923
|
+
const th = props.api.theme.current;
|
|
924
|
+
const sat = (k, fb) => desaturateTo(th[k], MAX_SAT, fb);
|
|
925
|
+
return {
|
|
926
|
+
text: sat("text", FALLBACK.text),
|
|
927
|
+
muted: sat("textMuted", FALLBACK.muted),
|
|
928
|
+
success: sat("success", FALLBACK.success),
|
|
929
|
+
warning: sat("warning", FALLBACK.warning),
|
|
930
|
+
error: sat("error", FALLBACK.error),
|
|
931
|
+
};
|
|
932
|
+
});
|
|
933
|
+
const hitColor = createMemo(() => {
|
|
934
|
+
const r = stats()?.hitRate ?? -1;
|
|
935
|
+
if (r >= 85)
|
|
936
|
+
return pal().success;
|
|
937
|
+
if (r >= 70)
|
|
938
|
+
return pal().warning;
|
|
939
|
+
return pal().error;
|
|
940
|
+
});
|
|
941
|
+
// 命中率趋势:最后一条与上一条的差值;|Δ| < 0.05 视为无变化(null = 不显示)
|
|
942
|
+
const trend = createMemo(() => {
|
|
943
|
+
const s = stats();
|
|
944
|
+
if (!s || s.prevHitRate < 0 || s.hitRate < 0)
|
|
945
|
+
return null;
|
|
946
|
+
const d = s.hitRate - s.prevHitRate;
|
|
947
|
+
return Math.abs(d) < 0.05 ? null : d;
|
|
948
|
+
});
|
|
949
|
+
const balanceText = createMemo(() => {
|
|
950
|
+
const s = props.signals.balanceState();
|
|
951
|
+
if (s.status === "ok" && s.data)
|
|
952
|
+
return formatBalanceText(s.data, props.signals.balanceCurrency(), props.signals.exchangeRate());
|
|
953
|
+
if (s.status === "loading")
|
|
954
|
+
return "\u2026";
|
|
955
|
+
if (s.status === "error")
|
|
956
|
+
return "\u26a0";
|
|
957
|
+
return "-";
|
|
958
|
+
});
|
|
959
|
+
// 路径显示(替换宿主默认 hint 左侧的 cwd 文本)
|
|
960
|
+
const directory = createMemo(() => {
|
|
961
|
+
try {
|
|
962
|
+
return props.api.state.path.directory;
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
return "";
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
// 恢复显隐偏好(默认显示);关闭时回退为仅显示路径,与宿主默认 hint 行一致
|
|
969
|
+
onMount(() => {
|
|
970
|
+
try {
|
|
971
|
+
const v = props.api.kv.get(`${KV_PREFIX}.section.bottom`, true);
|
|
972
|
+
props.signals.setSectionBottom(v !== false);
|
|
973
|
+
}
|
|
974
|
+
catch { }
|
|
975
|
+
});
|
|
976
|
+
return (_jsx(Show, { when: props.signals.sectionBottom(), fallback: _jsx("text", { fg: pal().muted, children: directory() }), children: _jsxs("box", { marginLeft: 1, flexGrow: 1, flexShrink: 0, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: pal().muted, children: directory() }), _jsxs("box", { flexDirection: "row", children: [_jsxs("text", { children: [_jsxs("span", { style: { fg: pal().muted }, children: [t("barHit"), " "] }), _jsx("span", { style: { fg: hitColor() }, children: (stats()?.hitRate ?? -1) >= 0 ? (Math.floor(stats().hitRate * 10) / 10).toFixed(1) + "%" : "--" }), _jsx(Show, { when: trend() !== null, children: _jsx("span", { style: { fg: trend() > 0 ? pal().success : pal().error }, children: " " + (trend() > 0 ? "\u2191" : "\u2193") + Math.abs(trend()).toFixed(1) + "%" }) }), _jsx("span", { style: { fg: pal().muted }, children: " \u00b7 " + t("barTok") + " " }), _jsx("span", { style: { fg: pal().text }, children: stats() ? fmtCompact(stats().input + stats().read + stats().write) : "--" }), _jsxs(Show, { when: !props.signals.balanceUnsupported(), children: [_jsx("span", { style: { fg: pal().muted }, children: " \u00b7 " + t("barBal") + " " }), _jsx("span", { style: { fg: pal().text }, children: balanceText() })] })] }), _jsx("text", { fg: pal().muted, children: " \u00b7 " })] })] }) }));
|
|
977
|
+
}
|
|
874
978
|
function createSidebarSlot(api, signals) {
|
|
875
979
|
let lastSlotSid = "";
|
|
876
980
|
return {
|
|
@@ -899,28 +1003,166 @@ const tui = async (api) => {
|
|
|
899
1003
|
const [sectionDist, setSectionDist] = createSignal(true);
|
|
900
1004
|
const [sectionSkills, setSectionSkills] = createSignal(true);
|
|
901
1005
|
const [sectionBalance, setSectionBalance] = createSignal(true);
|
|
1006
|
+
const [sectionBottom, setSectionBottom] = createSignal(true);
|
|
902
1007
|
const [balanceRefresh, setBalanceRefresh] = createSignal(0);
|
|
1008
|
+
const [balanceProviderId, setBalanceProviderId] = createSignal("deepseek");
|
|
1009
|
+
const [autoBalance, setAutoBalance] = createSignal(true);
|
|
1010
|
+
const [balanceUnsupported, setBalanceUnsupported] = createSignal(false);
|
|
903
1011
|
const [balanceCurrency, setBalanceCurrency] = createSignal("");
|
|
904
1012
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
905
|
-
const [
|
|
1013
|
+
const [langCode, setLangCode] = createSignal(INIT_LANG);
|
|
906
1014
|
const [overrideSessionId, setOverrideSessionId] = createSignal(undefined);
|
|
1015
|
+
// ── 余额查询状态(共享):侧边栏与底部栏读同一份数据,
|
|
1016
|
+
// 避免重复请求导致两处余额不一致 ──
|
|
1017
|
+
const [balanceState, setBalanceState] = createSignal({
|
|
1018
|
+
status: "idle", data: null, lastFetch: 0,
|
|
1019
|
+
});
|
|
1020
|
+
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
1021
|
+
let balanceSeq = 0;
|
|
907
1022
|
const signals = {
|
|
908
1023
|
currencySymbol, setCurrencySymbol,
|
|
909
1024
|
exchangeRate, setExchangeRate,
|
|
910
|
-
|
|
1025
|
+
langCode, setLangCode,
|
|
911
1026
|
sectionDetail, setSectionDetail,
|
|
912
1027
|
sectionModel, setSectionModel,
|
|
913
1028
|
sectionDist, setSectionDist,
|
|
914
1029
|
sectionSkills, setSectionSkills,
|
|
915
1030
|
sectionBalance, setSectionBalance,
|
|
1031
|
+
sectionBottom, setSectionBottom,
|
|
916
1032
|
balanceRefresh, setBalanceRefresh,
|
|
1033
|
+
balanceProviderId, setBalanceProviderId,
|
|
1034
|
+
autoBalance, setAutoBalance,
|
|
1035
|
+
balanceUnsupported, setBalanceUnsupported,
|
|
1036
|
+
balanceState,
|
|
917
1037
|
balanceCurrency, setBalanceCurrency,
|
|
918
1038
|
borderVisible, setBorderVisible,
|
|
919
1039
|
overrideSessionId, setOverrideSessionId,
|
|
920
1040
|
};
|
|
921
1041
|
api.slots.register(createSidebarSlot(api, signals));
|
|
1042
|
+
// 输入框 hint 行(session_prompt slot,replace 模式):
|
|
1043
|
+
// 用宿主同一 Prompt 组件重渲染输入框,仅替换 hint 行左侧——
|
|
1044
|
+
// 在路径与右侧 token/commands 提示之间插入 命中率 · 余额 · Tokens。
|
|
1045
|
+
api.slots.register({
|
|
1046
|
+
order: 55,
|
|
1047
|
+
slots: {
|
|
1048
|
+
session_prompt(_ctx, input) {
|
|
1049
|
+
return (_jsx(api.ui.Prompt, { sessionID: input.session_id, visible: input.visible, disabled: input.disabled, onSubmit: input.on_submit, ref: input.ref, hint: _jsx(BottomStatusBar, { api: api, signals: signals, sessionId: input.session_id }) }));
|
|
1050
|
+
},
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
922
1053
|
// ── slash commands for runtime config ──
|
|
923
1054
|
const KV_PREFIX = "cache_panel";
|
|
1055
|
+
// ── 语言偏好恢复:KV 就绪后优先用户设置(/cache-lang),覆盖自动识别 ──
|
|
1056
|
+
const restoreLang = () => {
|
|
1057
|
+
try {
|
|
1058
|
+
const saved = api.kv.get(`${KV_PREFIX}.lang`);
|
|
1059
|
+
if (saved && LANG_META.some((m) => m.code === saved))
|
|
1060
|
+
setLangCode(saved);
|
|
1061
|
+
}
|
|
1062
|
+
catch { }
|
|
1063
|
+
};
|
|
1064
|
+
if (api.kv.ready) {
|
|
1065
|
+
restoreLang();
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
const langTimer = setInterval(() => {
|
|
1069
|
+
if (api.kv.ready) {
|
|
1070
|
+
clearInterval(langTimer);
|
|
1071
|
+
restoreLang();
|
|
1072
|
+
}
|
|
1073
|
+
}, 10);
|
|
1074
|
+
api.lifecycle.onDispose(() => clearInterval(langTimer));
|
|
1075
|
+
}
|
|
1076
|
+
const pollBalance = async () => {
|
|
1077
|
+
const provider = getBalanceProvider(balanceProviderId());
|
|
1078
|
+
// 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
|
|
1079
|
+
const key = api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1080
|
+
|| findOpencodeKey(api, provider);
|
|
1081
|
+
if (balanceUnsupported()) {
|
|
1082
|
+
setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (!key) {
|
|
1086
|
+
setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const now = Date.now();
|
|
1090
|
+
const prev = balanceState();
|
|
1091
|
+
// key 已更换(重新输入)→ 强制重新查询,绕过缓存
|
|
1092
|
+
if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS)
|
|
1093
|
+
return; // cache still fresh
|
|
1094
|
+
const seq = ++balanceSeq;
|
|
1095
|
+
setBalanceState({ ...prev, status: "loading", error: undefined, key });
|
|
1096
|
+
const controller = new AbortController();
|
|
1097
|
+
let timedOut = false;
|
|
1098
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, 10_000);
|
|
1099
|
+
try {
|
|
1100
|
+
const data = await provider.fetchBalance(key, controller.signal);
|
|
1101
|
+
clearTimeout(timer);
|
|
1102
|
+
if (seq !== balanceSeq)
|
|
1103
|
+
return; // 已被更新的请求取代,丢弃过期结果
|
|
1104
|
+
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key });
|
|
1105
|
+
}
|
|
1106
|
+
catch (err) {
|
|
1107
|
+
clearTimeout(timer);
|
|
1108
|
+
if (seq !== balanceSeq)
|
|
1109
|
+
return;
|
|
1110
|
+
const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "");
|
|
1111
|
+
// 失败时清空旧数据,避免显示过期余额
|
|
1112
|
+
setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key });
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
1116
|
+
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
1117
|
+
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
1118
|
+
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
1119
|
+
createEffect(() => {
|
|
1120
|
+
void balanceRefresh();
|
|
1121
|
+
untrack(() => { void pollBalance(); });
|
|
1122
|
+
});
|
|
1123
|
+
// 定时轮询(5 分钟);随插件生命周期清理
|
|
1124
|
+
const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS);
|
|
1125
|
+
api.lifecycle.onDispose(() => clearInterval(balanceTimer));
|
|
1126
|
+
/** 菜单中 provider 选项标题:标注 key 来源(手动配置 / OpenCode 自动复用 / 未配置)。 */
|
|
1127
|
+
const providerOptionTitle = (p, current) => {
|
|
1128
|
+
const t = createT(() => langCode());
|
|
1129
|
+
const hasManual = !!api.kv.get(`${KV_PREFIX}.balance.${p.id}.key`, "");
|
|
1130
|
+
const hasAuto = !hasManual && !!findOpencodeKey(api, p);
|
|
1131
|
+
const mark = hasManual
|
|
1132
|
+
? t("keyUser")
|
|
1133
|
+
: hasAuto
|
|
1134
|
+
? t("keyOpenCode")
|
|
1135
|
+
: t("keyNotSet");
|
|
1136
|
+
return p.name + mark + (current && p.id === current ? " *" : "");
|
|
1137
|
+
};
|
|
1138
|
+
/** 弹出指定 provider 的 API Key 输入框(脱敏预填;空清除 / 含 * 保留原 key / 新 key 实时刷新)。 */
|
|
1139
|
+
const promptBalanceKey = (dialog, provider) => {
|
|
1140
|
+
const t = createT(() => langCode());
|
|
1141
|
+
const current = api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "");
|
|
1142
|
+
const masked = maskKey(current);
|
|
1143
|
+
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: provider.name, description: () => _jsx("text", { children: t("balKeyPrompt", { p: provider.name }) }), placeholder: provider.keyPlaceholder ?? "sk-...", value: masked, onConfirm: (val) => {
|
|
1144
|
+
const input = val.trim();
|
|
1145
|
+
let key;
|
|
1146
|
+
if (input === "") {
|
|
1147
|
+
key = "";
|
|
1148
|
+
}
|
|
1149
|
+
else if (input.includes("*")) {
|
|
1150
|
+
key = current;
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
key = input;
|
|
1154
|
+
}
|
|
1155
|
+
api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key);
|
|
1156
|
+
setBalanceRefresh(v => v + 1);
|
|
1157
|
+
if (key) {
|
|
1158
|
+
api.ui.toast({ message: t("keySaved") });
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
api.ui.toast({ message: t("keyCleared") });
|
|
1162
|
+
}
|
|
1163
|
+
dialog?.clear();
|
|
1164
|
+
}, onCancel: () => dialog?.clear() })));
|
|
1165
|
+
};
|
|
924
1166
|
api.command?.register(() => [
|
|
925
1167
|
{
|
|
926
1168
|
title: "Cache: Set Currency",
|
|
@@ -932,6 +1174,7 @@ const tui = async (api) => {
|
|
|
932
1174
|
title: `${code} (${sym})`,
|
|
933
1175
|
value: code,
|
|
934
1176
|
})), onSelect: (opt) => {
|
|
1177
|
+
const t = createT(() => langCode());
|
|
935
1178
|
const sym = CURRENCIES[opt.value] ?? "$";
|
|
936
1179
|
const defRate = DEFAULT_RATES[opt.value] ?? 1;
|
|
937
1180
|
api.kv.set(`${KV_PREFIX}.currency`, sym);
|
|
@@ -941,7 +1184,7 @@ const tui = async (api) => {
|
|
|
941
1184
|
signals.setBalanceCurrency(opt.value);
|
|
942
1185
|
signals.setCurrencySymbol(sym);
|
|
943
1186
|
signals.setExchangeRate(defRate);
|
|
944
|
-
api.ui.toast({ message:
|
|
1187
|
+
api.ui.toast({ message: t("currencySet", { v: opt.value, s: sym, r: defRate }) });
|
|
945
1188
|
dialog?.clear();
|
|
946
1189
|
} })));
|
|
947
1190
|
},
|
|
@@ -953,11 +1196,12 @@ const tui = async (api) => {
|
|
|
953
1196
|
slash: { name: "cache-rate" },
|
|
954
1197
|
onSelect: (dialog) => {
|
|
955
1198
|
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: "Exchange Rate", description: () => _jsx("text", { children: "Enter the exchange rate from USD to your currency (e.g. 7.2 for CNY)" }), placeholder: "1.0", value: String(api.kv.get(`${KV_PREFIX}.rate`, 1)), onConfirm: (val) => {
|
|
1199
|
+
const t = createT(() => langCode());
|
|
956
1200
|
const n = parseFloat(val);
|
|
957
1201
|
if (n > 0) {
|
|
958
1202
|
api.kv.set(`${KV_PREFIX}.rate`, n);
|
|
959
1203
|
signals.setExchangeRate(n);
|
|
960
|
-
api.ui.toast({ message:
|
|
1204
|
+
api.ui.toast({ message: t("rateSet", { r: n }) });
|
|
961
1205
|
}
|
|
962
1206
|
dialog?.clear();
|
|
963
1207
|
} })));
|
|
@@ -969,25 +1213,38 @@ const tui = async (api) => {
|
|
|
969
1213
|
description: "Show or hide a sidebar section",
|
|
970
1214
|
slash: { name: "cache-section" },
|
|
971
1215
|
onSelect: (dialog) => {
|
|
1216
|
+
const t = createT(() => langCode());
|
|
972
1217
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
973
1218
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
974
1219
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
975
1220
|
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
976
1221
|
const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true));
|
|
1222
|
+
const bottomOn = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true));
|
|
977
1223
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1224
|
+
const labels = {
|
|
1225
|
+
detail: t("secDetail"),
|
|
1226
|
+
model: t("secModel"),
|
|
1227
|
+
dist: t("distTitle"),
|
|
1228
|
+
skills: t("secSkills"),
|
|
1229
|
+
balance: t("secBalance"),
|
|
1230
|
+
bottom: t("secBottom"),
|
|
1231
|
+
border: t("secBorder"),
|
|
1232
|
+
};
|
|
1233
|
+
const optTitle = (label, on) => `${visualPadEnd(label, 15)}[${on ? "ON" : "OFF"}]`;
|
|
1234
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("secToggle"), options: [
|
|
1235
|
+
{ title: optTitle(labels.detail, detailOn), value: "detail" },
|
|
1236
|
+
{ title: optTitle(labels.model, modelOn), value: "model" },
|
|
1237
|
+
{ title: optTitle(labels.dist, distOn), value: "dist" },
|
|
1238
|
+
{ title: optTitle(labels.skills, skillsOn), value: "skills" },
|
|
1239
|
+
{ title: optTitle(labels.balance, balanceOn), value: "balance" },
|
|
1240
|
+
{ title: optTitle(labels.bottom, bottomOn), value: "bottom" },
|
|
1241
|
+
{ title: optTitle(labels.border, borderOn), value: "border" },
|
|
985
1242
|
], onSelect: (opt) => {
|
|
986
1243
|
if (opt.value === "border") {
|
|
987
1244
|
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
988
1245
|
api.kv.set(`${KV_PREFIX}.border`, !cur);
|
|
989
1246
|
signals.setBorderVisible(!cur);
|
|
990
|
-
api.ui.toast({ message:
|
|
1247
|
+
api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") });
|
|
991
1248
|
}
|
|
992
1249
|
else {
|
|
993
1250
|
const key = `${KV_PREFIX}.section.${opt.value}`;
|
|
@@ -1003,7 +1260,10 @@ const tui = async (api) => {
|
|
|
1003
1260
|
signals.setSectionSkills(!cur);
|
|
1004
1261
|
if (opt.value === "balance")
|
|
1005
1262
|
signals.setSectionBalance(!cur);
|
|
1006
|
-
|
|
1263
|
+
if (opt.value === "bottom")
|
|
1264
|
+
signals.setSectionBottom(!cur);
|
|
1265
|
+
const name = labels[opt.value] ?? opt.value;
|
|
1266
|
+
api.ui.toast({ message: t(!cur ? "sectionShown" : "sectionHidden", { s: name }) });
|
|
1007
1267
|
}
|
|
1008
1268
|
dialog?.clear();
|
|
1009
1269
|
} })));
|
|
@@ -1015,6 +1275,7 @@ const tui = async (api) => {
|
|
|
1015
1275
|
description: "Display the current plugin configuration",
|
|
1016
1276
|
slash: { name: "cache-config" },
|
|
1017
1277
|
onSelect: (dialog) => {
|
|
1278
|
+
const t = createT(() => langCode());
|
|
1018
1279
|
const sym = api.kv.get(`${KV_PREFIX}.currency`) ?? "$";
|
|
1019
1280
|
const rate = api.kv.get(`${KV_PREFIX}.rate`) ?? 1;
|
|
1020
1281
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
@@ -1022,9 +1283,16 @@ const tui = async (api) => {
|
|
|
1022
1283
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
1023
1284
|
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
1024
1285
|
const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true));
|
|
1286
|
+
const bottom = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true));
|
|
1287
|
+
const on = (v) => v ? "ON" : "OFF";
|
|
1025
1288
|
api.ui.toast({
|
|
1026
|
-
title: "
|
|
1027
|
-
message:
|
|
1289
|
+
title: t("panelConfigTitle"),
|
|
1290
|
+
message: t("panelConfigMsg", {
|
|
1291
|
+
c: sym, r: rate,
|
|
1292
|
+
d: on(detail), m: on(model),
|
|
1293
|
+
t: on(dist), k: on(skills),
|
|
1294
|
+
b: on(balance), f: on(bottom),
|
|
1295
|
+
}),
|
|
1028
1296
|
duration: 8000,
|
|
1029
1297
|
});
|
|
1030
1298
|
dialog?.clear();
|
|
@@ -1036,61 +1304,93 @@ const tui = async (api) => {
|
|
|
1036
1304
|
description: "Switch between Chinese and English display",
|
|
1037
1305
|
slash: { name: "cache-lang" },
|
|
1038
1306
|
onSelect: (dialog) => {
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1307
|
+
const t = createT(() => langCode());
|
|
1308
|
+
const cur = langCode();
|
|
1309
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("langTitle"), options: LANG_META.map((m) => ({
|
|
1310
|
+
title: `${visualPadEnd(m.label, 9)}${cur === m.code ? "\u2713" : ""}`,
|
|
1311
|
+
value: m.code,
|
|
1312
|
+
})), onSelect: (opt) => {
|
|
1313
|
+
const code = opt.value;
|
|
1314
|
+
api.kv.set(`${KV_PREFIX}.lang`, code);
|
|
1315
|
+
setLangCode(code);
|
|
1316
|
+
api.ui.toast({ message: t("langSwitched") });
|
|
1048
1317
|
dialog?.clear();
|
|
1049
1318
|
} })));
|
|
1050
1319
|
},
|
|
1051
1320
|
},
|
|
1052
1321
|
{
|
|
1053
|
-
title: "Cache:
|
|
1054
|
-
value: "cache.balance
|
|
1055
|
-
description: "
|
|
1056
|
-
slash: { name: "cache-balance
|
|
1322
|
+
title: "Cache: Switch Balance Provider",
|
|
1323
|
+
value: "cache.balance",
|
|
1324
|
+
description: "切换余额提供商 / 自动切换当前会话提供商 | Switch balance provider / auto-switch session provider",
|
|
1325
|
+
slash: { name: "cache-balance" },
|
|
1057
1326
|
onSelect: (dialog) => {
|
|
1058
|
-
const
|
|
1059
|
-
const current =
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
else if (input.includes("*")) {
|
|
1079
|
-
key = current;
|
|
1080
|
-
}
|
|
1081
|
-
else {
|
|
1082
|
-
key = input;
|
|
1083
|
-
}
|
|
1084
|
-
api.kv.set(`${KV_PREFIX}.ds_key`, key);
|
|
1085
|
-
setBalanceRefresh(v => v + 1);
|
|
1086
|
-
if (key) {
|
|
1087
|
-
api.ui.toast({ message: zh ? "API Key 已保存,正在查询余额..." : "API Key saved, fetching balance..." });
|
|
1327
|
+
const t = createT(() => langCode());
|
|
1328
|
+
const current = signals.balanceProviderId();
|
|
1329
|
+
const auto = signals.autoBalance();
|
|
1330
|
+
const autoLabel = `${t("autoSwitchOpt")} [${auto ? "ON" : "OFF"}]`;
|
|
1331
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("balProvTitle"), options: [
|
|
1332
|
+
{
|
|
1333
|
+
title: autoLabel,
|
|
1334
|
+
value: "__auto__",
|
|
1335
|
+
},
|
|
1336
|
+
...balanceProviders.map((p) => ({
|
|
1337
|
+
title: providerOptionTitle(p, current),
|
|
1338
|
+
value: p.id,
|
|
1339
|
+
})),
|
|
1340
|
+
], onSelect: (opt) => {
|
|
1341
|
+
if (opt.value === "__auto__") {
|
|
1342
|
+
const next = !auto;
|
|
1343
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, next);
|
|
1344
|
+
signals.setAutoBalance(next);
|
|
1345
|
+
api.ui.toast({ message: next ? t("autoSwitchOn") : t("autoSwitchOff") });
|
|
1346
|
+
dialog?.clear();
|
|
1088
1347
|
}
|
|
1089
1348
|
else {
|
|
1090
|
-
|
|
1349
|
+
const provider = getBalanceProvider(opt.value);
|
|
1350
|
+
// 手动切换会关闭自动切换
|
|
1351
|
+
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id);
|
|
1352
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, false);
|
|
1353
|
+
signals.setBalanceProviderId(provider.id);
|
|
1354
|
+
signals.setAutoBalance(false);
|
|
1355
|
+
signals.setBalanceUnsupported(false);
|
|
1356
|
+
// 切换后立即按新 provider 刷新显示(无 key 时显示 idle,避免残留上一 provider 余额)
|
|
1357
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1);
|
|
1358
|
+
const hasKey = !!api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "");
|
|
1359
|
+
if (!hasKey) {
|
|
1360
|
+
// 未配置 key → 进入设置流程(对话框保持打开等待输入)
|
|
1361
|
+
promptBalanceKey(dialog, provider);
|
|
1362
|
+
}
|
|
1363
|
+
else {
|
|
1364
|
+
api.ui.toast({ message: t("providerManual", { p: provider.name }) });
|
|
1365
|
+
dialog?.clear();
|
|
1366
|
+
}
|
|
1091
1367
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1368
|
+
} })));
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
title: "Cache: Set Balance API Key",
|
|
1373
|
+
value: "cache.balance.key",
|
|
1374
|
+
description: "Select a provider and set its API key for balance display",
|
|
1375
|
+
slash: { name: "cache-balance-key" },
|
|
1376
|
+
onSelect: (dialog) => {
|
|
1377
|
+
const t = createT(() => langCode());
|
|
1378
|
+
// 步骤 1:选择 provider
|
|
1379
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("balSelectTitle"), options: balanceProviders.map((p) => ({
|
|
1380
|
+
title: providerOptionTitle(p),
|
|
1381
|
+
value: p.id,
|
|
1382
|
+
})), onSelect: (opt) => {
|
|
1383
|
+
const provider = getBalanceProvider(opt.value);
|
|
1384
|
+
// 手动指定 provider 会关闭自动切换
|
|
1385
|
+
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id);
|
|
1386
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, false);
|
|
1387
|
+
signals.setBalanceProviderId(provider.id);
|
|
1388
|
+
signals.setAutoBalance(false);
|
|
1389
|
+
// 切换后立即刷新显示(防止取消输入时残留上一 provider 的余额)
|
|
1390
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1);
|
|
1391
|
+
// 步骤 2:输入 key
|
|
1392
|
+
promptBalanceKey(dialog, provider);
|
|
1393
|
+
} })));
|
|
1094
1394
|
},
|
|
1095
1395
|
},
|
|
1096
1396
|
{
|
|
@@ -1099,9 +1399,10 @@ const tui = async (api) => {
|
|
|
1099
1399
|
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
1100
1400
|
slash: { name: "cache-debug-skills" },
|
|
1101
1401
|
onSelect: () => {
|
|
1402
|
+
const t = createT(() => langCode());
|
|
1102
1403
|
const rt = api.route.current;
|
|
1103
1404
|
if (rt.name !== "session" || !rt.params) {
|
|
1104
|
-
api.ui.toast({ message: "
|
|
1405
|
+
api.ui.toast({ message: t("runInSession"), variant: "warning" });
|
|
1105
1406
|
return;
|
|
1106
1407
|
}
|
|
1107
1408
|
const sid = String(rt.params.sessionID);
|
|
@@ -1188,7 +1489,7 @@ const tui = async (api) => {
|
|
|
1188
1489
|
return false; seen.add(c.value); return true; });
|
|
1189
1490
|
if (unique.length > 0) {
|
|
1190
1491
|
// ── 有子代理 → DialogSelect 列表选择 ──
|
|
1191
|
-
const
|
|
1492
|
+
const t = createT(() => langCode());
|
|
1192
1493
|
const currentSid = signals.overrideSessionId() ?? api.kv.get(`${KV_PREFIX}.session`, "");
|
|
1193
1494
|
const options = unique.map((c, i) => ({
|
|
1194
1495
|
title: `${i + 1}. ${c.title}`,
|
|
@@ -1197,33 +1498,33 @@ const tui = async (api) => {
|
|
|
1197
1498
|
}));
|
|
1198
1499
|
// 首尾各放一个"回到主会话",长列表时顶部底部均可直达
|
|
1199
1500
|
const backValue = "__main__";
|
|
1200
|
-
const backTitle = `\u2500 ${
|
|
1501
|
+
const backTitle = `\u2500 ${t("backToMainTitle")}`;
|
|
1201
1502
|
options.unshift({ title: backTitle, value: backValue, description: "" });
|
|
1202
1503
|
options.push({ title: backTitle, value: backValue, description: "" });
|
|
1203
1504
|
const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1;
|
|
1204
|
-
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title:
|
|
1505
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("subSelectTitle"), options: options, current: currentIdx >= 0 ? options[currentIdx].value : undefined, onSelect: (opt) => {
|
|
1205
1506
|
if (opt.value === backValue) {
|
|
1206
1507
|
signals.setOverrideSessionId(undefined);
|
|
1207
1508
|
api.kv.set(`${KV_PREFIX}.session`, "");
|
|
1208
|
-
api.ui.toast({ message:
|
|
1509
|
+
api.ui.toast({ message: t("backToMain") });
|
|
1209
1510
|
}
|
|
1210
1511
|
else {
|
|
1211
1512
|
signals.setOverrideSessionId(opt.value);
|
|
1212
1513
|
api.kv.set(`${KV_PREFIX}.session`, opt.value);
|
|
1213
|
-
api.ui.toast({ message: (
|
|
1514
|
+
api.ui.toast({ message: t("subAgentSwitched", { s: opt.value.slice(0, 24) + "\u2026" }) });
|
|
1214
1515
|
}
|
|
1215
1516
|
dialog?.clear();
|
|
1216
1517
|
} })));
|
|
1217
1518
|
}
|
|
1218
1519
|
else {
|
|
1219
1520
|
// ── 无子代理 → DialogPrompt 手动粘贴 ──
|
|
1220
|
-
const
|
|
1221
|
-
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: signals.overrideSessionId() ?
|
|
1521
|
+
const t = createT(() => langCode());
|
|
1522
|
+
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: signals.overrideSessionId() ? t("subSwitchTitle") : t("subViewTitle"), description: () => _jsx("text", { children: t("subNoFound") }), placeholder: "ses_...", value: signals.overrideSessionId() ?? api.kv.get(`${KV_PREFIX}.session`, "") ?? "", onConfirm: (val) => {
|
|
1222
1523
|
const sid = val.trim();
|
|
1223
1524
|
if (sid) {
|
|
1224
1525
|
signals.setOverrideSessionId(sid);
|
|
1225
1526
|
api.kv.set(`${KV_PREFIX}.session`, sid);
|
|
1226
|
-
api.ui.toast({ message: (
|
|
1527
|
+
api.ui.toast({ message: t("subAgentSwitched", { s: sid.slice(0, 24) + "\u2026" }) });
|
|
1227
1528
|
}
|
|
1228
1529
|
dialog?.clear();
|
|
1229
1530
|
}, onCancel: () => dialog?.clear() })));
|
|
@@ -1236,9 +1537,10 @@ const tui = async (api) => {
|
|
|
1236
1537
|
description: "Return to main session stats",
|
|
1237
1538
|
slash: { name: "cache-session-back" },
|
|
1238
1539
|
onSelect: (dialog) => {
|
|
1540
|
+
const t = createT(() => langCode());
|
|
1239
1541
|
signals.setOverrideSessionId(undefined);
|
|
1240
1542
|
api.kv.set(`${KV_PREFIX}.session`, "");
|
|
1241
|
-
api.ui.toast({ message:
|
|
1543
|
+
api.ui.toast({ message: t("backToMain") });
|
|
1242
1544
|
dialog?.clear();
|
|
1243
1545
|
},
|
|
1244
1546
|
},
|