opencode-visual-cache 1.3.0 → 1.5.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/dist/index.js CHANGED
@@ -1,6 +1,7 @@
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";
4
5
  // ── terminal-width helpers ────────────────────────────────────────
5
6
  // CJK characters occupy 2 terminal columns; padEnd/padStart count
6
7
  // string length (=1 per char), which breaks alignment with mixed text.
@@ -94,6 +95,14 @@ const ZH_T = {
94
95
  secDetail: "明细",
95
96
  secModel: "模型",
96
97
  secSkills: "已加载技能",
98
+ balTotal: "总余额:",
99
+ balNoKey: "未配置 {p} API Key",
100
+ balLoading: "查询中...",
101
+ balError: "查询失败",
102
+ balErr401: "API Key 无效",
103
+ balErr403: "余额查询被拒绝",
104
+ balErrEmpty: "未获取到余额数据",
105
+ balErrTimeout: "查询超时",
97
106
  };
98
107
  const EN_T = {
99
108
  title: "Token Cache",
@@ -125,6 +134,14 @@ const EN_T = {
125
134
  secDetail: "Detail",
126
135
  secModel: "Model",
127
136
  secSkills: "Loaded Skills",
137
+ balTotal: "Total:",
138
+ balNoKey: "{p} API Key not set",
139
+ balLoading: "Fetching...",
140
+ balError: "Fetch failed",
141
+ balErr401: "Invalid API Key",
142
+ balErr403: "Balance request rejected",
143
+ balErrEmpty: "No balance data",
144
+ balErrTimeout: "Request timed out",
128
145
  };
129
146
  // ── color helpers ────────────────────────────────────────────────
130
147
  /** Extract { r, g, b } (0–255) from a hex string or RGBA-like object. */
@@ -299,6 +316,46 @@ function estimateTokens(text) {
299
316
  const asciiPerToken = jsonLike ? 3.5 : codeLike ? 3.5 : 4;
300
317
  return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.0));
301
318
  }
319
+ const BALANCE_POLL_MS = 5 * 60 * 1000; // 5 minutes
320
+ /**
321
+ * 将余额从来源币种换算为目标币种。
322
+ * DEFAULT_RATES 以 USD=1 为基准:先折算为 USD,再换算到目标币种。
323
+ */
324
+ function convertBalance(target, targetRate, amount, from) {
325
+ if (from === target)
326
+ return amount;
327
+ const fromRate = DEFAULT_RATES[from] ?? 1;
328
+ const usd = from === "USD" ? amount : amount / fromRate;
329
+ return target === "USD" ? usd : usd * targetRate;
330
+ }
331
+ /**
332
+ * 从 OpenCode 已认证的 provider 读取 API key 作为余额查询的自动兜底。
333
+ * 匹配复用前缀逻辑:先精确匹配 id,再前缀匹配(如 moonshotai-cn → moonshot)。
334
+ * key 来源:auth.json(provider.key)或配置(provider.options.apiKey)。
335
+ * 仅当手动配置的 key 缺失时使用;读取失败或未匹配返回空串。
336
+ */
337
+ function findOpencodeKey(api, provider) {
338
+ try {
339
+ const provs = api.state.provider;
340
+ // 大小写不敏感:精确匹配 id,否则前缀匹配(如 moonshotai-cn → moonshot)
341
+ const id = provider.id.toLowerCase();
342
+ const hit = provs.find((p) => p.id.toLowerCase() === id) ?? provs.find((p) => p.id.toLowerCase().startsWith(id));
343
+ if (!hit)
344
+ return "";
345
+ const k = typeof hit.key === "string" ? hit.key : "";
346
+ if (k)
347
+ return k;
348
+ return typeof hit.options?.apiKey === "string" ? hit.options.apiKey : "";
349
+ }
350
+ catch {
351
+ return "";
352
+ }
353
+ }
354
+ /** 货币符号:优先取 /cache-currency 内置映射,未知币种回退为代码。 */
355
+ function balanceSymbol(currency) {
356
+ const sym = CURRENCIES[currency];
357
+ return sym ?? currency + " ";
358
+ }
302
359
  const CURRENCIES = {
303
360
  USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
304
361
  };
@@ -325,7 +382,7 @@ function TokenCachePanel(props) {
325
382
  const [skillsOpen, setSkillsOpen] = createSignal(true);
326
383
  let boxEl;
327
384
  // ── shared signals (de-structured so internal code is unchanged) ──
328
- const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langZH, setLangZH, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, sectionSkills, setSectionSkills, borderVisible, setBorderVisible, } = props.signals;
385
+ const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langZH, setLangZH, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, sectionSkills, setSectionSkills, sectionBalance, setSectionBalance, balanceRefresh, balanceProviderId, setBalanceProviderId, autoBalance, setAutoBalance, balanceCurrency, setBalanceCurrency, borderVisible, setBorderVisible, } = props.signals;
329
386
  // ── reactive translation (follows langZH signal) ──
330
387
  const t = createMemo(() => langZH() ? ZH_T : EN_T);
331
388
  // ── scan session messages reactively ──
@@ -352,6 +409,90 @@ function TokenCachePanel(props) {
352
409
  hasSkills: false,
353
410
  });
354
411
  const [refreshTick, setRefreshTick] = createSignal(0);
412
+ // ── balance state + polling ──────────────────────────────────
413
+ const [balanceState, setBalanceState] = createSignal({
414
+ status: "idle", data: null, lastFetch: 0,
415
+ });
416
+ // 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
417
+ let balanceSeq = 0;
418
+ // 当前 provider 显示名
419
+ const providerName = createMemo(() => getBalanceProvider(balanceProviderId()).name);
420
+ const pollBalance = async () => {
421
+ const provider = getBalanceProvider(balanceProviderId());
422
+ // 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
423
+ const key = props.api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "")
424
+ || findOpencodeKey(props.api, provider);
425
+ if (!key) {
426
+ setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
427
+ return;
428
+ }
429
+ const now = Date.now();
430
+ const prev = balanceState();
431
+ // key 已更换(重新输入)→ 强制重新查询,绕过缓存
432
+ if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS)
433
+ return; // cache still fresh
434
+ const seq = ++balanceSeq;
435
+ setBalanceState({ ...prev, status: "loading", error: undefined, key });
436
+ const controller = new AbortController();
437
+ let timedOut = false;
438
+ const timer = setTimeout(() => { timedOut = true; controller.abort(); }, 10_000);
439
+ try {
440
+ const data = await provider.fetchBalance(key, controller.signal);
441
+ clearTimeout(timer);
442
+ if (seq !== balanceSeq)
443
+ return; // 已被更新的请求取代,丢弃过期结果
444
+ setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key });
445
+ }
446
+ catch (err) {
447
+ clearTimeout(timer);
448
+ if (seq !== balanceSeq)
449
+ return;
450
+ const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "");
451
+ // 失败时清空旧数据,避免显示过期余额
452
+ setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key });
453
+ }
454
+ };
455
+ // Re-fetch when the API key is (re)configured via /cache-balance-key.
456
+ // 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
457
+ // effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
458
+ // 形成无限循环(每次重跑都发起新的 fetch 请求)。
459
+ createEffect(() => {
460
+ void balanceRefresh();
461
+ untrack(() => { void pollBalance(); });
462
+ });
463
+ // 自动切换当前会话的 provider(前缀匹配)。手动切换会关闭此行为。
464
+ // 直接追踪 messages 取最后一条 assistant 消息的 providerID——
465
+ // 不依赖 session.model 的响应式更新(模型切换时该链路可能不触发重算)。
466
+ createEffect(() => {
467
+ if (!autoBalance())
468
+ return;
469
+ const sid = props.signals.overrideSessionId() ?? props.sessionId;
470
+ const msgs = props.api.state.session.messages(sid);
471
+ let pid = "";
472
+ for (let i = msgs.length - 1; i >= 0; i--) {
473
+ const m = msgs[i];
474
+ if (m.role === "assistant" && m.providerID) {
475
+ pid = m.providerID;
476
+ break;
477
+ }
478
+ }
479
+ // 会话尚无 assistant 消息(新会话 / 刚切换模型未对话 / 消息未加载)
480
+ // → 回退到会话级模型元数据,反映当前正在使用的 provider
481
+ if (!pid) {
482
+ try {
483
+ const session = props.api.state.session.get(sid);
484
+ pid = session?.model?.providerID ?? "";
485
+ }
486
+ catch { /* ignore */ }
487
+ }
488
+ if (!pid)
489
+ return;
490
+ const hit = matchBalanceProvider(pid);
491
+ if (hit && hit.id !== balanceProviderId()) {
492
+ setBalanceProviderId(hit.id);
493
+ props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
494
+ }
495
+ });
355
496
  // ── auto-clear override when the user navigates to a different main session ──
356
497
  let lastMainSid = props.sessionId;
357
498
  createEffect(() => {
@@ -602,10 +743,33 @@ function TokenCachePanel(props) {
602
743
  setCurrencySymbol(sym);
603
744
  if (typeof rate === "number" && rate > 0)
604
745
  setExchangeRate(rate);
746
+ const balCur = props.api.kv.get(`${KV_PREFIX}.balance_currency`);
747
+ if (typeof balCur === "string")
748
+ setBalanceCurrency(balCur);
749
+ // Restore balance provider (fall back to default when unknown)
750
+ const savedProvider = props.api.kv.get(`${KV_PREFIX}.balance.provider`);
751
+ if (typeof savedProvider === "string" && balanceProviders.some((p) => p.id === savedProvider)) {
752
+ setBalanceProviderId(savedProvider);
753
+ }
754
+ // Restore auto-switch (default on)
755
+ const savedAuto = props.api.kv.get(`${KV_PREFIX}.balance.auto`);
756
+ if (typeof savedAuto === "boolean")
757
+ setAutoBalance(savedAuto);
758
+ // Migrate legacy DeepSeek key (cache_panel.ds_key → cache_panel.balance.deepseek.key)
759
+ const legacyKey = props.api.kv.get(`${KV_PREFIX}.ds_key`, "");
760
+ if (legacyKey) {
761
+ const dsKey = props.api.kv.get(`${KV_PREFIX}.balance.deepseek.key`, "");
762
+ if (!dsKey)
763
+ props.api.kv.set(`${KV_PREFIX}.balance.deepseek.key`, legacyKey);
764
+ props.api.kv.set(`${KV_PREFIX}.ds_key`, "");
765
+ }
766
+ // 恢复的 provider 可能与默认值不同,强制重新查询
767
+ props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
605
768
  setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
606
769
  setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
607
770
  setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
608
771
  setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)));
772
+ setSectionBalance(Boolean(props.api.kv.get(`${KV_PREFIX}.section.balance`, true)));
609
773
  const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
610
774
  setBorderVisible(bv !== false);
611
775
  // Restore language preference
@@ -663,7 +827,8 @@ function TokenCachePanel(props) {
663
827
  const unsubMsg = props.api.event.on("message.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1); });
664
828
  const unsubSession = props.api.event.on("session.updated", () => { setRefreshTick(v => v + 1); });
665
829
  setRefreshTick(v => v + 1);
666
- onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg(); unsubSession(); });
830
+ const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS);
831
+ onCleanup(() => { clearTimeout(partTimer); clearInterval(balanceTimer); unsubPart(); unsubMsg(); unsubSession(); });
667
832
  });
668
833
  // ── colours ──
669
834
  // Pull from the current theme, auto-desaturate if too punchy,
@@ -732,7 +897,36 @@ function TokenCachePanel(props) {
732
897
  const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
733
898
  const label = truncateVisual(sk.name, maxLabel);
734
899
  return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t().tok) }));
735
- }) })] }) })] })] })] }));
900
+ }) })] }) }), _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().balNoKey.replace("{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: (() => {
901
+ const code = balanceState().error;
902
+ if (code === "401")
903
+ return t().balErr401;
904
+ if (code === "403")
905
+ return t().balErr403;
906
+ if (code === "EMPTY")
907
+ return t().balErrEmpty;
908
+ if (code === "TIMEOUT")
909
+ return t().balErrTimeout;
910
+ return t().balError + (code ? ` (${code})` : "");
911
+ })() })] }) }), _jsx(Show, { when: balanceState().status === "ok" && balanceState().data, children: (() => {
912
+ const list = balanceState().data;
913
+ const pref = balanceCurrency();
914
+ // 偏好币种是 DeepSeek 原生返回的(CNY/USD)→ 直接显示
915
+ const native = pref ? list.find(x => x.currency === pref) : undefined;
916
+ if (native) {
917
+ return (_jsx("text", { fg: pal().text, children: justify(t().balTotal, balanceSymbol(native.currency) + native.total) }));
918
+ }
919
+ // 非原生币种(EUR/JPY/GBP/KRW…)→ 取第一条余额按汇率换算
920
+ const base = list[0];
921
+ const baseAmt = parseFloat(base.total);
922
+ const converted = Number.isFinite(baseAmt)
923
+ ? convertBalance(pref || base.currency, exchangeRate(), baseAmt, base.currency)
924
+ : baseAmt;
925
+ const shown = pref && base.currency !== pref
926
+ ? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
927
+ : base.total;
928
+ return (_jsx("text", { fg: pal().text, children: justify(t().balTotal, balanceSymbol(pref || base.currency) + shown) }));
929
+ })() })] })] })] })] }));
736
930
  }
737
931
  // ---------------------------------------------------------------------------
738
932
  // Plugin entry
@@ -764,6 +958,11 @@ const tui = async (api) => {
764
958
  const [sectionModel, setSectionModel] = createSignal(true);
765
959
  const [sectionDist, setSectionDist] = createSignal(true);
766
960
  const [sectionSkills, setSectionSkills] = createSignal(true);
961
+ const [sectionBalance, setSectionBalance] = createSignal(true);
962
+ const [balanceRefresh, setBalanceRefresh] = createSignal(0);
963
+ const [balanceProviderId, setBalanceProviderId] = createSignal("deepseek");
964
+ const [autoBalance, setAutoBalance] = createSignal(true);
965
+ const [balanceCurrency, setBalanceCurrency] = createSignal("");
767
966
  const [borderVisible, setBorderVisible] = createSignal(true);
768
967
  const [langZH, setLangZH] = createSignal(LANG_ZH);
769
968
  const [overrideSessionId, setOverrideSessionId] = createSignal(undefined);
@@ -775,12 +974,57 @@ const tui = async (api) => {
775
974
  sectionModel, setSectionModel,
776
975
  sectionDist, setSectionDist,
777
976
  sectionSkills, setSectionSkills,
977
+ sectionBalance, setSectionBalance,
978
+ balanceRefresh, setBalanceRefresh,
979
+ balanceProviderId, setBalanceProviderId,
980
+ autoBalance, setAutoBalance,
981
+ balanceCurrency, setBalanceCurrency,
778
982
  borderVisible, setBorderVisible,
779
983
  overrideSessionId, setOverrideSessionId,
780
984
  };
781
985
  api.slots.register(createSidebarSlot(api, signals));
782
986
  // ── slash commands for runtime config ──
783
987
  const KV_PREFIX = "cache_panel";
988
+ /** 菜单中 provider 选项标题:标注 key 来源(手动配置 / OpenCode 自动复用 / 未配置)。 */
989
+ const providerOptionTitle = (p, current) => {
990
+ const zh = langZH();
991
+ const hasManual = !!api.kv.get(`${KV_PREFIX}.balance.${p.id}.key`, "");
992
+ const hasAuto = !hasManual && !!findOpencodeKey(api, p);
993
+ const mark = hasManual
994
+ ? (zh ? "(用户 key)" : " (user key)")
995
+ : hasAuto
996
+ ? (zh ? "(OpenCode)" : " (OpenCode)")
997
+ : (zh ? "(未配置)" : " (not set)");
998
+ return p.name + mark + (current && p.id === current ? " *" : "");
999
+ };
1000
+ /** 弹出指定 provider 的 API Key 输入框(脱敏预填;空清除 / 含 * 保留原 key / 新 key 实时刷新)。 */
1001
+ const promptBalanceKey = (dialog, provider) => {
1002
+ const zh = langZH();
1003
+ const current = api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "");
1004
+ const masked = maskKey(current);
1005
+ dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: provider.name, description: () => _jsx("text", { children: zh ? `输入 ${provider.name} API Key 以显示账户余额(留空清除)` : `Enter your ${provider.name} API key to show account balance (leave empty to clear)` }), placeholder: provider.keyPlaceholder ?? "sk-...", value: masked, onConfirm: (val) => {
1006
+ const input = val.trim();
1007
+ let key;
1008
+ if (input === "") {
1009
+ key = "";
1010
+ }
1011
+ else if (input.includes("*")) {
1012
+ key = current;
1013
+ }
1014
+ else {
1015
+ key = input;
1016
+ }
1017
+ api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key);
1018
+ setBalanceRefresh(v => v + 1);
1019
+ if (key) {
1020
+ api.ui.toast({ message: zh ? "API Key 已保存,正在查询余额..." : "API Key saved, fetching balance..." });
1021
+ }
1022
+ else {
1023
+ api.ui.toast({ message: zh ? "API Key 已清除" : "API Key cleared" });
1024
+ }
1025
+ dialog?.clear();
1026
+ }, onCancel: () => dialog?.clear() })));
1027
+ };
784
1028
  api.command?.register(() => [
785
1029
  {
786
1030
  title: "Cache: Set Currency",
@@ -796,6 +1040,9 @@ const tui = async (api) => {
796
1040
  const defRate = DEFAULT_RATES[opt.value] ?? 1;
797
1041
  api.kv.set(`${KV_PREFIX}.currency`, sym);
798
1042
  api.kv.set(`${KV_PREFIX}.rate`, defRate);
1043
+ // 同步余额显示币种偏好:CNY/USD 原生直显,其余币种按汇率换算
1044
+ api.kv.set(`${KV_PREFIX}.balance_currency`, opt.value);
1045
+ signals.setBalanceCurrency(opt.value);
799
1046
  signals.setCurrencySymbol(sym);
800
1047
  signals.setExchangeRate(defRate);
801
1048
  api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` });
@@ -830,12 +1077,14 @@ const tui = async (api) => {
830
1077
  const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
831
1078
  const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
832
1079
  const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
1080
+ const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true));
833
1081
  const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
834
1082
  dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Toggle Section", options: [
835
1083
  { title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
836
1084
  { title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
837
1085
  { title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
838
1086
  { title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
1087
+ { title: `Balance [${balanceOn ? "ON" : "OFF"}]`, value: "balance" },
839
1088
  { title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
840
1089
  ], onSelect: (opt) => {
841
1090
  if (opt.value === "border") {
@@ -856,6 +1105,8 @@ const tui = async (api) => {
856
1105
  signals.setSectionDist(!cur);
857
1106
  if (opt.value === "skills")
858
1107
  signals.setSectionSkills(!cur);
1108
+ if (opt.value === "balance")
1109
+ signals.setSectionBalance(!cur);
859
1110
  api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
860
1111
  }
861
1112
  dialog?.clear();
@@ -874,9 +1125,10 @@ const tui = async (api) => {
874
1125
  const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
875
1126
  const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
876
1127
  const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
1128
+ const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true));
877
1129
  api.ui.toast({
878
1130
  title: "Cache Panel Config",
879
- message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"}`,
1131
+ message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"} | Balance: ${balance ? "ON" : "OFF"}`,
880
1132
  duration: 8000,
881
1133
  });
882
1134
  dialog?.clear();
@@ -901,6 +1153,82 @@ const tui = async (api) => {
901
1153
  } })));
902
1154
  },
903
1155
  },
1156
+ {
1157
+ title: "Cache: Switch Balance Provider",
1158
+ value: "cache.balance",
1159
+ description: "切换余额提供商 / 自动切换当前会话提供商 | Switch balance provider / auto-switch session provider",
1160
+ slash: { name: "cache-balance" },
1161
+ onSelect: (dialog) => {
1162
+ const zh = langZH();
1163
+ const current = signals.balanceProviderId();
1164
+ const auto = signals.autoBalance();
1165
+ const autoLabel = auto
1166
+ ? (zh ? "自动切换提供商 [开]" : "Auto-switch provider [ON]")
1167
+ : (zh ? "自动切换提供商 [关]" : "Auto-switch provider [OFF]");
1168
+ dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: zh ? "余额提供商 / 自动切换" : "Balance Provider / Auto-switch", options: [
1169
+ {
1170
+ title: autoLabel,
1171
+ value: "__auto__",
1172
+ },
1173
+ ...balanceProviders.map((p) => ({
1174
+ title: providerOptionTitle(p, current),
1175
+ value: p.id,
1176
+ })),
1177
+ ], onSelect: (opt) => {
1178
+ if (opt.value === "__auto__") {
1179
+ const next = !auto;
1180
+ api.kv.set(`${KV_PREFIX}.balance.auto`, next);
1181
+ signals.setAutoBalance(next);
1182
+ api.ui.toast({ message: zh ? `自动切换余额提供商: ${next ? "开" : "关"}` : `Auto-switch balance provider: ${next ? "ON" : "OFF"}` });
1183
+ dialog?.clear();
1184
+ }
1185
+ else {
1186
+ const provider = getBalanceProvider(opt.value);
1187
+ // 手动切换会关闭自动切换
1188
+ api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id);
1189
+ api.kv.set(`${KV_PREFIX}.balance.auto`, false);
1190
+ signals.setBalanceProviderId(provider.id);
1191
+ signals.setAutoBalance(false);
1192
+ // 切换后立即按新 provider 刷新显示(无 key 时显示 idle,避免残留上一 provider 余额)
1193
+ signals.setBalanceRefresh(signals.balanceRefresh() + 1);
1194
+ const hasKey = !!api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "");
1195
+ if (!hasKey) {
1196
+ // 未配置 key → 进入设置流程(对话框保持打开等待输入)
1197
+ promptBalanceKey(dialog, provider);
1198
+ }
1199
+ else {
1200
+ api.ui.toast({ message: zh ? `余额提供商: ${provider.name}(自动切换已关闭)` : `Balance provider: ${provider.name} (auto-switch off)` });
1201
+ dialog?.clear();
1202
+ }
1203
+ }
1204
+ } })));
1205
+ },
1206
+ },
1207
+ {
1208
+ title: "Cache: Set Balance API Key",
1209
+ value: "cache.balance.key",
1210
+ description: "Select a provider and set its API key for balance display",
1211
+ slash: { name: "cache-balance-key" },
1212
+ onSelect: (dialog) => {
1213
+ const zh = langZH();
1214
+ // 步骤 1:选择 provider
1215
+ dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: zh ? "选择余额提供商" : "Select Balance Provider", options: balanceProviders.map((p) => ({
1216
+ title: providerOptionTitle(p),
1217
+ value: p.id,
1218
+ })), onSelect: (opt) => {
1219
+ const provider = getBalanceProvider(opt.value);
1220
+ // 手动指定 provider 会关闭自动切换
1221
+ api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id);
1222
+ api.kv.set(`${KV_PREFIX}.balance.auto`, false);
1223
+ signals.setBalanceProviderId(provider.id);
1224
+ signals.setAutoBalance(false);
1225
+ // 切换后立即刷新显示(防止取消输入时残留上一 provider 的余额)
1226
+ signals.setBalanceRefresh(signals.balanceRefresh() + 1);
1227
+ // 步骤 2:输入 key
1228
+ promptBalanceKey(dialog, provider);
1229
+ } })));
1230
+ },
1231
+ },
904
1232
  {
905
1233
  title: "Cache: Debug Skills Detection",
906
1234
  value: "cache.debug-skills",