dsh-bottom-info-bar 1.7.0 → 1.8.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.
Files changed (3) hide show
  1. package/lib/client.js +41 -10
  2. package/lib/index.js +312 -57
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -25,6 +25,12 @@ const HIDE_SPEED_FIELDS = true;
25
25
  // RPC 超时兜底:host 侧 15s 超时之上再留余量;端点挂起时 20s 内必失败,杜绝永久"加载中…"
26
26
  const RPC_TIMEOUT_MS = 20000;
27
27
 
28
+ // 首启强制刷新窗口:页面刚打开/用户手动刷新后的这几秒内,快照请求带 force=true,
29
+ // host 会绕过缓存与失败退避当场重查服务商——用户刷新页面立即看到最新余额/额度,
30
+ // 不必干等后台 60s 自动周期。覆盖窗口需容纳会话模型从空到确定的短暂翻转期。
31
+ const BOOT_AT = Date.now();
32
+ const FORCE_REFRESH_WINDOW_MS = 6000;
33
+
28
34
  // rpc(method, args, externalSignal):
29
35
  // - 超时:20s 未响应 → abort 并以"请求超时"失败(fetch 挂起不阻塞界面)
30
36
  // - 可中止:传入外部 AbortSignal(组件卸载时 abort)→ 立即取消并拒绝"请求已取消"
@@ -343,11 +349,14 @@ module.exports = {
343
349
  const requestVersion = ++loadVersionRef.current;
344
350
  const activeSelection = selection || activeSessionModel;
345
351
  const selectionArgs = activeSelection ? { selection: { provider: activeSelection.provider, model: activeSelection.model } } : {};
352
+ // 首启窗口内(打开/刷新网页头几秒)→ 快照类请求强制重查;之后的 30s 周期轮询走常规缓存节奏
353
+ const force = Date.now() - BOOT_AT < FORCE_REFRESH_WINDOW_MS;
354
+ if (force) { selectionArgs.force = true; }
346
355
  const signal = abortRef.current ? abortRef.current.signal : null;
347
356
  // 逐接口容错:allSettled 等全部 settle(最坏 20s 超时兜底),任一失败只降级该端点,
348
357
  // 不拖垮其他成功数据;合并逻辑在 mergeLoadResults(失败端点保留旧值 + 记录错误)
349
358
  Promise.allSettled([
350
- rpc('getBalanceSnapshot', activeSelection ? { provider: activeSelection.provider } : null, signal),
359
+ rpc('getBalanceSnapshot', activeSelection ? { provider: activeSelection.provider, force: force } : (force ? { force: true } : null), signal),
351
360
  rpc('getPricing', selectionArgs, signal),
352
361
  rpc('getUsageSummary', Object.assign({ sessionId: sessionId }, selectionArgs), signal),
353
362
  rpc('getBillingMode', selectionArgs, signal),
@@ -656,33 +665,39 @@ module.exports = {
656
665
  metric('距' + (peakNow ? '空闲' : '高峰'), fmtCountdown(pr.nextSwitch.at - now))));
657
666
  }
658
667
 
659
- // 本会话花费(只显示钱;hover 浮窗显示 本会话(含子代理)+ 今天 / 近一月 / 全部;金额数字加粗)
668
+ // 本会话花费(公共小部件 pushSessionCost:只显示钱;hover 显示 今天/近一月/全部)
660
669
  // 始终显示:新会话/对话刚开始尚无记账时显示 ¥0.000,hover 仍可查看持久化的 今天/近一月/全部
670
+ pushSessionCost(groups, trailingErrorGroups, !!(bal && bal.currency === 'USD'));
671
+ }
672
+
673
+ // 本会话花费块(余额制 与 订阅·充值余额 形态共用的小部件):
674
+ // 只显示钱;hover 浮窗显示 含子代理说明 + 今天 / 近一月 / 全部;金额数字加粗。
675
+ // usdSymbol:账户币种为美元时,hover 汇总行用 $ 前缀(本会话单值仍按其真实计价币种)
676
+ function pushSessionCost(groups, trailingErrorGroups, usdSymbol) {
661
677
  const usg = state.usage;
678
+ const errs = state.errors || {};
662
679
  if (usg) {
663
680
  const cs = usg.currentSession;
664
681
  const costCNY = cs && cs.costs && cs.costs.CNY != null ? cs.costs.CNY : null;
665
682
  const costUSD = cs && cs.costs && cs.costs.USD != null ? cs.costs.USD : null;
666
- const zeroTxt = (bal && bal.currency === 'USD' ? '$' : '¥') + (0).toFixed(3);
683
+ const symbol = usdSymbol ? '$' : '¥';
667
684
  const costTxt = costCNY != null ? '¥' + costCNY.toFixed(3)
668
- : (costUSD != null ? '$' + costUSD.toFixed(3) : zeroTxt);
669
- const symbol = bal && bal.currency === 'USD' ? '$' : '¥';
685
+ : (costUSD != null ? '$' + costUSD.toFixed(3) : symbol + (0).toFixed(3));
670
686
  const today = usg.todaySpend != null ? '今天 ' + symbol + fmt(usg.todaySpend, 3) : '';
671
687
  const month = usg.monthSpend != null ? '近一月 ' + symbol + fmt(usg.monthSpend, 3) : '';
672
688
  const total = usg.totalSpend != null ? '全部 ' + symbol + fmt(usg.totalSpend, 3) : '';
673
689
  const detail = [today, month, total].filter(function (s) { return s.length > 0; }).join(' · ');
674
- // v1.7 发布前微调:hover 明确标注"含子代理"——本会话聚合同账户所有记录(同起点后)
675
690
  groups.push(React.createElement('span', { key: 'convo',
676
691
  title: '本会话 ' + costTxt + '(含子代理)' + (detail ? '\n' + detail : '') },
677
692
  metric('本会话', costTxt)));
678
- } else if (errors.usage) {
679
- // 本次 RPC 失败且无旧数据:只降级花费块,其余端点数据照常渲染
693
+ } else if (errs.usage) {
680
694
  trailingErrorGroups.push(React.createElement('span', { className: 'bi-err', key: 'usageerr', title: '花费暂不可用;不会影响对话。' }, '花费获取失败'));
681
695
  }
682
696
  }
683
697
 
684
- // ---- 订阅制模式(互斥替换余额制版,row2 只三类信息):
685
- // 订阅服务+模型 → 三窗口额度 → 距重置倒计时(最紧窗口);余额/时段/花费/token 均不显示 ----
698
+ // ---- 订阅制模式(互斥替换余额制版):
699
+ // 套餐额度型:订阅服务+模型 → 三窗口额度 → 距重置倒计时(余额/时段/花费/token 不显示)
700
+ // 充值余额型(如智谱按量账户,windows 空且有 sub.balance):服务商+模型 → 余额 → 本会话花费 ----
686
701
  function subscriptionFailureHint(error, source) {
687
702
  const kind = error && error.kind;
688
703
  const serviceName = subscriptionServiceName(source);
@@ -726,6 +741,22 @@ module.exports = {
726
741
  trailingErrorGroups.push(React.createElement('span', { className: 'bi-stale', key: 'substale', title: subscriptionFailureHint(sub.error, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败'));
727
742
  return;
728
743
  }
744
+ // v1.8:充值余额模式——无额度窗口但有 balance 字段(如智谱普通 API 余额用户)
745
+ // 显示"余额 ¥XX.XX",与 Coding Plan 额度窗口互斥
746
+ if (!hasData && typeof sub.balance === 'number' && isFinite(sub.balance)) {
747
+ const balTxt = '¥' + fmt(sub.balance, 2);
748
+ const titleLines = ['订阅源:' + subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider) + '(' + (sub.plan || '充值余额') + ')',
749
+ '可用余额:' + balTxt];
750
+ groups.push(React.createElement('span', { key: 'subbal', title: titleLines.join('\n') },
751
+ metric('余额', balTxt)));
752
+ // 充值余额用户按量付费,花销与余额同等重要 → 追加公共花费块(含子代理聚合)
753
+ pushSessionCost(groups, trailingErrorGroups, false);
754
+ // host 快照失败(sub.error)或本次 RPC 失败(errors.sub)→ 保留旧数据 + 降级标记
755
+ if (sub.error || errors.sub) {
756
+ trailingErrorGroups.push(React.createElement('span', { className: 'bi-stale', key: 'substale', title: subscriptionFailureHint(sub.error || { kind: 'exception', message: String(errors.sub || '') }, sub.source || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败'));
757
+ }
758
+ return;
759
+ }
729
760
  // 窗口缺失(如 Codex 无 5 小时窗口)→ 跳过窗口组,不占位、不报错
730
761
  if (hasData) {
731
762
  // 简洁模式下选择"时间最短且有重置时刻"的窗口(刷新最快,用户最需关注):
package/lib/index.js CHANGED
@@ -107,6 +107,11 @@ function accountForProvider(pid) {
107
107
  return null
108
108
  }
109
109
 
110
+ // 服务商在账单(usage)里直接报告真实金额时,按服务商固定币种回填。
111
+ // 仅列明确用美元计价的聚合商(如 OpenRouter);未登记的走价目表币种回退。
112
+ // 动机:聚合商路由模型数量庞大、价目无法静态维护——官方报出的钱 > 本地任何换算。
113
+ const PROVIDER_REPORTED_CURRENCY = { openrouter: 'USD' }
114
+
110
115
  // ---------- 双模式纯逻辑(模式检测 / 窗口映射 / 响应解析;单测直接提取) ----------
111
116
 
112
117
  // 窗口时长(秒)→ 窗口键:18000≈5小时 / 604800≈7天 / 2592000≈30天,5% 容差;未知返回 null
@@ -765,13 +770,135 @@ export default {
765
770
  'deepseek-chat': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.5, inputCacheMiss: 2.0, output: 8.0 } },
766
771
  'gpt-4o': { currency: 'USD', mode: 'flat', price: { inputCacheHit: 1.25, inputCacheMiss: 2.5, output: 10.0 } },
767
772
  'gpt-4o-mini': { currency: 'USD', mode: 'flat', price: { inputCacheHit: 0.15, inputCacheMiss: 0.15, output: 0.6 } },
773
+ // ---------- 智谱 BigModel 官方按量价(CNY/百万 tokens;来源 open.bigmodel.cn/pricing,
774
+ // 采集记录与全档分段备注见 docs/research/bigmodel-pricing-202608.md)----------
775
+ // 分段模型主条目取基础档(输入≤32K 等),长上下文档暂不计入——影响与限制见体系审计报告。
776
+ 'glm-5.3': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 2, inputCacheMiss: 8, output: 28 } },
777
+ // flash 存在"5折限时两周"双价:内置表取刊例价 0.8/0.23/2.8(保守);
778
+ // 远程目录按用户当前实扣配促销价(活动窗口约至 2026-09 初,详见 catalog/pricing.json notes)
779
+ 'glm-5.3-flash': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.23, inputCacheMiss: 0.8, output: 2.8 } },
780
+ 'glm-5.2': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 2, inputCacheMiss: 8, output: 28 } },
781
+ 'glm-5.1': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 1.3, inputCacheMiss: 6, output: 24 } },
782
+ 'glm-5-turbo': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 1.2, inputCacheMiss: 5, output: 22 } },
783
+ 'glm-5v-turbo': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 1.2, inputCacheMiss: 5, output: 22 } },
784
+ 'glm-4.7': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.4, inputCacheMiss: 2, output: 8 } },
785
+ 'glm-4.5-air': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.16, inputCacheMiss: 0.8, output: 2 } },
786
+ // ---------- 小米 MiMo 按量官方价(mimo.mi.com/docs/zh-CN/price/pay-as-you-go,2026-08-06 调价后 CNY;
787
+ // 本机 pi-ai 目录里的 cost 字段是 USD 数字——严禁混用,详见体系审计报告)----------
788
+ 'mimo-v2-flash': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.02, inputCacheMiss: 1, output: 2 } },
789
+ 'mimo-v2-omni': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.02, inputCacheMiss: 1, output: 2 } },
790
+ 'mimo-v2-pro': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.025, inputCacheMiss: 3, output: 6 } },
791
+ 'mimo-v2.5': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.02, inputCacheMiss: 1, output: 2 } },
792
+ 'mimo-v2.5-pro': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.025, inputCacheMiss: 3, output: 6 } },
793
+ // mimo-v2.5-pro-ultraspeed:官方仅美元口径、人民币数未独立确认 → 按"不臆测"暂不收录
794
+ // ---------- 阶跃 StepFun 在售文本模型(platform.stepfun.com 定价详情;step-1/2/3 系已于 2026-07 下线)----------
795
+ 'step-3.7-flash': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.27, inputCacheMiss: 1.35, output: 8.1 } },
796
+ 'step-3.5-flash': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.14, inputCacheMiss: 0.7, output: 2.1 } },
797
+ // ---------- Kimi 国内站(provider=moonshotai-cn → api.moonshot.cn 计费 ¥)----------
798
+ // 同型号在 api.moonshot.ai 国际站按 $ 另计:官方 USD 数字待采集后以 'moonshotai:kimi-*'
799
+ // 作用域键收录;裸 kimi-* 键刻意留空,防止币种串档。
800
+ 'moonshotai-cn:kimi-k3': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 2, inputCacheMiss: 20, output: 100 } },
801
+ 'moonshotai-cn:kimi-k2.5': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.7, inputCacheMiss: 4, output: 21 } },
802
+ 'moonshotai-cn:kimi-k2.6': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 1.1, inputCacheMiss: 6.5, output: 27 } },
803
+ 'moonshotai-cn:kimi-k2.7-code': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 1.3, inputCacheMiss: 6.5, output: 27 } },
804
+ 'moonshotai-cn:kimi-k2.7-code-highspeed':{ currency: 'CNY', mode: 'flat', price: { inputCacheHit: 2.6, inputCacheMiss: 13, output: 54 } },
805
+ 'moonshotai-cn:kimi-k2-0711-preview': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.15, inputCacheMiss: 0.6, output: 2.5 } },
806
+ 'moonshotai-cn:kimi-k2-0905-preview': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.15, inputCacheMiss: 0.6, output: 2.5 } },
807
+ 'moonshotai-cn:kimi-k2-thinking': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.15, inputCacheMiss: 0.6, output: 2.5 } },
808
+ 'moonshotai-cn:kimi-k2-thinking-turbo': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.15, inputCacheMiss: 1.15, output: 8 } },
809
+ 'moonshotai-cn:kimi-k2-turbo-preview': { currency: 'CNY', mode: 'flat', price: { inputCacheHit: 0.6, inputCacheMiss: 2.4, output: 10 } },
768
810
  };
769
- function modelCurrency(model) {
770
- const entry = PRICING[model];
811
+ function modelCurrency(provider, model) {
812
+ // 服务商作用域键优先:同名模型跨计费域时(Kimi 国内外),币种必须跟作用域走
813
+ const entry = pricingEntryFor(provider, model);
771
814
  if (entry && entry.currency) return entry.currency;
772
815
  return model && model.indexOf('gpt') === 0 ? 'USD' : 'CNY';
773
816
  }
774
817
  const DEFAULT_MODEL = 'deepseek-v4-flash';
818
+
819
+ // ---------- 远程价目目录(v1.8):内置表兜底 + 启动/定时增量更新 ----------
820
+ // 设计目标(用户铁律):接入新模型/新价格不再依赖插件发版。
821
+ // 机制:启动时先用磁盘缓存离线合并,再异步拉取远程 catalog/pricing.json 增量合并;
822
+ // 每合并一次立即重跑 unpriced 回填 → 新覆盖的模型的历史账目自动补算。
823
+ // 安全边界:仅接受"声明式数字"(统一价 + 白名单币种),绝不执行远程代码;
824
+ // 匿名 GET 不携带任何密钥/用户标识;失败静默降级缓存与内置表,不影响信息栏。
825
+ const REMOTE_PRICING_URL = process.env.DSH_BOTTOM_INFO_BAR_PRICING_URL
826
+ || 'https://raw.githubusercontent.com/songoao25/dsh-bottom-info-bar/main/catalog/pricing.json';
827
+ const REMOTE_PRICING_REFRESH_MS = 6 * 60 * 60 * 1000; // 定时刷新:6 小时
828
+ const PRICING_CACHE_FILE = join(DATA_DIR, 'pricing-cache.json');
829
+ let remotePricingEtag = null;
830
+
831
+ // 校验目录条目:只放行可安全参与计算的声明式数据
832
+ function sanitizeRemotePricingEntries(raw) {
833
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
834
+ const entries = {};
835
+ for (const key of Object.keys(raw)) {
836
+ if (Object.keys(entries).length >= 512) break; // 容量上限,防滥用
837
+ if (!/^[A-Za-z0-9._:-]{1,64}$/.test(key)) continue; // 允许 "provider:model" 作用域键(跨币种同名模型)
838
+ const e = raw[key];
839
+ if (!e || typeof e !== 'object') continue;
840
+ // v1 仅接受统一价(flat);分时价的窗口规则须随框架参数化后再开放远程下发
841
+ if (e.mode !== 'flat' || !e.price || typeof e.price !== 'object') continue;
842
+ if (e.currency !== 'CNY' && e.currency !== 'USD') continue; // 白名单币种
843
+ const hit = Number(e.price.inputCacheHit);
844
+ const miss = Number(e.price.inputCacheMiss);
845
+ const out = Number(e.price.output);
846
+ if (!Number.isFinite(hit) || !Number.isFinite(miss) || !Number.isFinite(out)) continue;
847
+ if (miss <= 0 || hit < 0 || out < 0) continue;
848
+ if (hit > miss) continue; // 缓存命中不可能比未命中更贵:脏数据拒收
849
+ entries[key] = { currency: e.currency, mode: 'flat', price: { inputCacheHit: hit, inputCacheMiss: miss, output: out } };
850
+ }
851
+ return Object.keys(entries).length > 0 ? entries : null;
852
+ }
853
+
854
+ // 合并进运行时价目表并触发回填;返回本次新增/更新的条数
855
+ function applyRemotePricingEntries(entries) {
856
+ if (!entries) return 0;
857
+ let n = 0;
858
+ for (const key of Object.keys(entries)) {
859
+ PRICING[key] = entries[key];
860
+ n++;
861
+ }
862
+ if (n > 0) backfillUnpricedRecords();
863
+ return n;
864
+ }
865
+
866
+ function loadPricingCacheFromDisk() {
867
+ try {
868
+ const parsed = JSON.parse(readFileSync(PRICING_CACHE_FILE, 'utf8'));
869
+ return sanitizeRemotePricingEntries(parsed && parsed.entries);
870
+ } catch (err) { /* 无缓存/损坏 → 内置表兜底 */ }
871
+ return null;
872
+ }
873
+
874
+ function savePricingCacheToDisk(entries) {
875
+ try {
876
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
877
+ writeAndSync(PRICING_CACHE_FILE, JSON.stringify({ fetchedAt: Date.now(), etag: remotePricingEtag, entries: entries }), 'w');
878
+ } catch (err) { /* 缓存写失败不致命 */ }
879
+ }
880
+
881
+ async function refreshRemotePricing(reason) {
882
+ try {
883
+ const headers = {};
884
+ if (remotePricingEtag) headers['If-None-Match'] = remotePricingEtag;
885
+ const res = await fetch(REMOTE_PRICING_URL, { headers, signal: AbortSignal.timeout(10000) });
886
+ if (res.status === 304) return { status: 'fresh' }; // 目录未变
887
+ if (!res.ok) throw new Error('HTTP ' + res.status);
888
+ const body = await res.json();
889
+ const entries = sanitizeRemotePricingEntries(body);
890
+ if (!entries) throw new Error('目录格式不符合规范');
891
+ remotePricingEtag = res.headers.get('etag');
892
+ savePricingCacheToDisk(entries);
893
+ const merged = applyRemotePricingEntries(entries);
894
+ console.warn('[dsh-bottom-info-bar] 远程价目已合并(' + reason + '):' + merged + ' 条有效条目');
895
+ return { status: 'merged', count: merged };
896
+ } catch (err) {
897
+ console.warn('[dsh-bottom-info-bar] 远程价目获取失败(' + reason + '),沿用缓存/内置表:' + String((err && err.message) || err));
898
+ return { status: 'error', message: String((err && err.message) || err) };
899
+ }
900
+ }
901
+
775
902
  const SCENARIOS = [
776
903
  { id: 'qa', label: '日常问答', outputK: 2, inputK: 4 },
777
904
  { id: 'coding', label: '中等编码任务', outputK: 15, inputK: 30 },
@@ -911,7 +1038,7 @@ export default {
911
1038
 
912
1039
  function refreshProviderBalance(pid) {
913
1040
  const prov = PROVIDERS[pid];
914
- if (!prov) return;
1041
+ if (!prov) return Promise.resolve();
915
1042
  const seq = (balanceSeq[pid] || 0) + 1;
916
1043
  balanceSeq[pid] = seq;
917
1044
  if (!prov.balanceAPI) {
@@ -919,9 +1046,10 @@ export default {
919
1046
  const spend = providerSpend(pid);
920
1047
  const total = Math.max(0, prov.initialTopUp - spend);
921
1048
  balances[pid] = { data: { currency: 'USD', total: total, granted: 0, toppedUp: prov.initialTopUp }, fetchedAt: Date.now(), error: null };
922
- return;
1049
+ return Promise.resolve();
923
1050
  }
924
- (async function () {
1051
+ // 返回本次刷新 Promise:强制刷新路径(客户端打开页面)需等待最新结果落快照后再返回
1052
+ return (async function () {
925
1053
  let cred = null;
926
1054
  try {
927
1055
  cred = await ctx.credentials.resolve(prov.credential);
@@ -967,6 +1095,7 @@ export default {
967
1095
  const subscriptionSeq = {}; // 每 source 刷新序号:仅最新一次请求可写入快照
968
1096
  const subscriptionInFlight = {}; // { [sourceKey]: Promise } 并发去重(同一时刻只发一个请求)
969
1097
  const subscriptionRequested = {}; // 仅"客户端请求过"的源进入 60s 周期刷新(余额制下不打扰订阅接口)
1098
+ const subscriptionSourceProvider = {}; // { [sourceKey]: providerId }——记住请求该源时的 provider(zai 系按 provider 路由 host/凭据)
970
1099
  const subscriptionLastFailAt = {}; // { [sourceKey]: ms } 上次订阅刷新失败时刻(失败退避:期内不重试)
971
1100
 
972
1101
  // FR-8 / D7:Codex / ChatGPT 订阅卡(纯本地通道)
@@ -1038,12 +1167,17 @@ export default {
1038
1167
  // host:zai-coding-cn → https://open.bigmodel.cn;zai → https://api.z.ai
1039
1168
  // 认证:两者均 Authorization 裸 API Key,绝无 Bearer 前缀(加 Bearer 会 401)
1040
1169
  async function resolveZaiKey(providerId) {
1170
+ // 凭据与 host 匹配:zai(国际)→ 优先 ZAI_API_KEY;zai-coding-cn(国内)→ 优先 ZAI_CODING_CN_API_KEY。
1171
+ // 用户通常只配一个,回退另一名防止误配。
1172
+ const isCn = providerId === 'zai-coding-cn';
1173
+ const primary = isCn ? 'ZAI_CODING_CN_API_KEY' : 'ZAI_API_KEY';
1174
+ const fallback = isCn ? 'ZAI_API_KEY' : 'ZAI_CODING_CN_API_KEY';
1041
1175
  try {
1042
- const cred = await ctx.credentials.resolve('ZAI_CODING_CN_API_KEY');
1176
+ const cred = await ctx.credentials.resolve(primary);
1043
1177
  if (cred && typeof cred.value === 'string' && cred.value.length > 0) return cred.value;
1044
1178
  } catch (err) { /* 回退 */ }
1045
1179
  try {
1046
- const cred = await ctx.credentials.resolve('ZAI_API_KEY');
1180
+ const cred = await ctx.credentials.resolve(fallback);
1047
1181
  if (cred && typeof cred.value === 'string' && cred.value.length > 0) return cred.value;
1048
1182
  } catch (err) { /* 未配置 */ }
1049
1183
  return null;
@@ -1096,36 +1230,82 @@ export default {
1096
1230
  return { plan: planName, windows: windows };
1097
1231
  }
1098
1232
 
1099
- async function fetchZaiUsage() {
1100
- // 从当前 provider 决定用哪个 host(zai-coding-cn vs zai)
1101
- // 这里通过 subscriptionSourceFor 反推,但实际 RPC 调用时 selection 会传 provider
1102
- // 为简化,先尝试两个 host
1103
- const key = await resolveZaiKey();
1233
+ // 解析智谱充值余额响应(/api/biz/account/query-customer-account-report)
1234
+ // 被 CodexBar PR#3109、CodexMeter PR#2 等多个项目验证的非公开控制台 API
1235
+ function parseZaiBalance(body) {
1236
+ if (!body || typeof body !== 'object') return null;
1237
+ if (body.success === false || body.code !== 200) return null;
1238
+ const d = body.data;
1239
+ if (!d || typeof d !== 'object') return null;
1240
+ // availableBalance 优先(可用余额),fallback 到 balance
1241
+ const bal = typeof d.availableBalance === 'number' ? d.availableBalance
1242
+ : (typeof d.balance === 'number' ? d.balance : null);
1243
+ if (bal === null || !isFinite(bal)) return null;
1244
+ return { balance: bal };
1245
+ }
1246
+
1247
+ async function fetchZaiUsage(providerId) {
1248
+ // 按 provider 路由 host 与凭据:zai-coding-cn → open.bigmodel.cn(国内密钥);
1249
+ // zai → api.z.ai(国际密钥)。两端均裸 API Key(无 Bearer 前缀)。
1250
+ // 自动检测用户类型:先尝试 Coding Plan 额度接口,失败后回退充值余额接口。
1251
+ const resolvedProvider = providerId === 'zai-coding-cn' ? 'zai-coding-cn' : 'zai';
1252
+ const key = await resolveZaiKey(resolvedProvider);
1104
1253
  if (!key) {
1105
- return { error: { kind: 'no-key', message: '未配置智谱 API Key(ZAI_CODING_CN_API_KEY 或 ZAI_API_KEY)' } };
1254
+ return { error: { kind: 'no-key', message: '未配置智谱 API Key(ZAI_API_KEY 或 ZAI_CODING_CN_API_KEY)' } };
1106
1255
  }
1107
- // 尝试两个 host,优先国内
1108
- const hosts = ['https://open.bigmodel.cn', 'https://api.z.ai'];
1109
- for (let i = 0; i < hosts.length; i++) {
1110
- try {
1111
- const res = await fetch(hosts[i] + '/api/monitor/usage/quota/limit', {
1112
- headers: { Authorization: key }, // 裸 API Key,无 Bearer 前缀
1113
- signal: AbortSignal.timeout(15000),
1114
- });
1115
- if (!res.ok) {
1116
- if (i < hosts.length - 1) continue; // 尝试下一个 host
1117
- return { error: { kind: 'http', message: '请求失败(HTTP ' + res.status + ')' } };
1256
+ const host = zaiHostForProvider(resolvedProvider);
1257
+
1258
+ // ---- 第一步:尝试 Coding Plan 额度接口 ----
1259
+ try {
1260
+ const res = await fetch(host + '/api/monitor/usage/quota/limit', {
1261
+ headers: { Authorization: key }, // 裸 API Key,无 Bearer 前缀
1262
+ signal: AbortSignal.timeout(15000),
1263
+ });
1264
+ const body = await res.json().catch(() => null);
1265
+ // 智谱 API 常在 HTTP 200 内返回业务错误({code:401, success:false, msg:...}),需先检查
1266
+ if (body && body.success === false) {
1267
+ const msg = body.msg || body.message || '';
1268
+ // "当前用户不存在 coding plan" → 非订阅用户,回退到充值余额接口
1269
+ if (/不存在.*coding\s*plan|no.*coding\s*plan/i.test(msg)) {
1270
+ return fetchZaiBalanceFallback(host, key, resolvedProvider);
1118
1271
  }
1119
- const body = await res.json();
1120
- const parsed = parseZaiQuota(body);
1121
- if (!parsed) return { error: { kind: 'parse', message: '响应格式异常' } };
1122
- return { data: { provider: 'zai', plan: parsed.plan, windows: parsed.windows } };
1123
- } catch (err) {
1124
- if (i < hosts.length - 1) continue; // 尝试下一个 host
1125
- return { error: { kind: 'exception', message: String((err && err.message) || err) } };
1272
+ if (body.code === 401 || /过期|不正确|unauthorized|expired/i.test(msg))
1273
+ return { error: { kind: 'auth', message: '智谱 API 认证失败(密钥过期或不正确)' } };
1274
+ return { error: { kind: 'http', message: '请求失败(' + (body.code || '') + ' ' + msg + ')' } };
1275
+ }
1276
+ if (!res.ok) return { error: { kind: 'http', message: '请求失败(HTTP ' + res.status + ')' } };
1277
+ const parsed = parseZaiQuota(body);
1278
+ if (!parsed) return { error: { kind: 'parse', message: '响应格式异常' } };
1279
+ return { data: { provider: 'zai', plan: parsed.plan, windows: parsed.windows } };
1280
+ } catch (err) {
1281
+ return { error: { kind: 'exception', message: String((err && err.message) || err) } };
1282
+ }
1283
+ }
1284
+
1285
+ // 智谱充值余额回退:Coding Plan 接口报"不存在 coding plan"时自动查询充值余额
1286
+ async function fetchZaiBalanceFallback(host, key, resolvedProvider) {
1287
+ try {
1288
+ // open.bigmodel.cn 与 www.bigmodel.cn 均可用;国际站 api.z.ai 无此接口
1289
+ const balanceHost = host === 'https://api.z.ai' ? 'https://open.bigmodel.cn' : host;
1290
+ const res = await fetch(balanceHost + '/api/biz/account/query-customer-account-report', {
1291
+ headers: { Authorization: 'Bearer ' + key }, // Bearer 认证(与裸 Key 均可)
1292
+ signal: AbortSignal.timeout(15000),
1293
+ });
1294
+ const body = await res.json().catch(() => null);
1295
+ if (body && body.success === false) {
1296
+ const msg = body.msg || body.message || '';
1297
+ if (body.code === 401 || body.code === 1000 || /过期|不正确|unauthorized|expired/i.test(msg))
1298
+ return { error: { kind: 'auth', message: '智谱 API 认证失败(密钥过期或不正确)' } };
1299
+ return { error: { kind: 'http', message: '请求失败(' + (body.code || '') + ' ' + msg + ')' } };
1126
1300
  }
1301
+ if (!res.ok) return { error: { kind: 'http', message: '请求失败(HTTP ' + res.status + ')' } };
1302
+ const parsed = parseZaiBalance(body);
1303
+ if (!parsed) return { error: { kind: 'parse', message: '响应格式异常' } };
1304
+ // 返回充值余额:balance 字段携带金额,windows 为空(无额度窗口)
1305
+ return { data: { provider: 'zai', plan: '充值余额', windows: [], balance: parsed.balance } };
1306
+ } catch (err) {
1307
+ return { error: { kind: 'exception', message: String((err && err.message) || err) } };
1127
1308
  }
1128
- return { error: { kind: 'exception', message: '所有 host 均失败' } };
1129
1309
  }
1130
1310
 
1131
1311
  // v1.7 通用凭据读取:缺失/读取失败一律 null(错误信息不含密钥)
@@ -1361,13 +1541,14 @@ export default {
1361
1541
  };
1362
1542
 
1363
1543
  // 触发一次刷新(并发去重 + seq 防旧覆盖);返回本次刷新 Promise
1364
- function kickSubscriptionRefresh(sourceKey) {
1544
+ // providerId:需要按 provider 路由 host/凭据的源(如 zai 系)使用;其余源忽略
1545
+ function kickSubscriptionRefresh(sourceKey, providerId) {
1365
1546
  const src = SUBSCRIPTION_SOURCES[sourceKey];
1366
1547
  if (!src) return Promise.resolve();
1367
1548
  if (subscriptionInFlight[sourceKey]) return subscriptionInFlight[sourceKey];
1368
1549
  const seq = (subscriptionSeq[sourceKey] || 0) + 1;
1369
1550
  subscriptionSeq[sourceKey] = seq;
1370
- subscriptionInFlight[sourceKey] = src.fetch().then(function (result) {
1551
+ subscriptionInFlight[sourceKey] = src.fetch(providerId).then(function (result) {
1371
1552
  if (subscriptionSeq[sourceKey] === seq) {
1372
1553
  subscriptions[sourceKey] = mergeSubscriptionResult(subscriptions[sourceKey], result);
1373
1554
  // 失败退避记录:失败记时刻(期内不重试),成功清零
@@ -1394,7 +1575,7 @@ export default {
1394
1575
  if (!subscriptionRequested[sourceKey]) continue;
1395
1576
  const lastFailAt = subscriptionLastFailAt[sourceKey] || 0;
1396
1577
  if (nowMs - lastFailAt < SUBSCRIPTION_RETRY_BACKOFF_MS) continue;
1397
- kickSubscriptionRefresh(sourceKey);
1578
+ kickSubscriptionRefresh(sourceKey, subscriptionSourceProvider[sourceKey]);
1398
1579
  }
1399
1580
  }
1400
1581
 
@@ -1455,15 +1636,16 @@ export default {
1455
1636
  }
1456
1637
 
1457
1638
  // RPC:当前订阅额度快照 + 模式判定(非订阅模式直接返回,不发任何订阅请求)
1458
- async function getSubscriptionSnapshotRpc(selection) {
1639
+ async function getSubscriptionSnapshotRpc(selection, force) {
1459
1640
  const sel = selection || modelSelection();
1460
1641
  const bm = detectBillingMode(sel.provider, config.billingMode);
1461
- const out = { mode: bm.mode, provider: sel.provider, reason: bm.reason, source: null, plan: null, planType: null, expiryAt: null, windows: [], fetchedAt: null, error: null };
1642
+ const out = { mode: bm.mode, provider: sel.provider, reason: bm.reason, source: null, plan: null, planType: null, expiryAt: null, windows: [], balance: null, fetchedAt: null, error: null };
1462
1643
  if (bm.mode !== 'subscription') return out;
1463
1644
  const sourceKey = subscriptionSourceFor(sel.provider);
1464
1645
  if (!sourceKey) return out;
1465
1646
  out.source = sourceKey;
1466
1647
  subscriptionRequested[sourceKey] = true; // 该源进入 60s 周期刷新
1648
+ subscriptionSourceProvider[sourceKey] = sel.provider; // 记住 provider(zai 系路由需要)
1467
1649
  const snap = subscriptions[sourceKey] || { data: null, fetchedAt: null, error: null };
1468
1650
  const nowMs = Date.now();
1469
1651
  const lastFailAt = subscriptionLastFailAt[sourceKey] || 0;
@@ -1471,11 +1653,13 @@ export default {
1471
1653
  // 减少对未公开 wham 接口的请求,也避免"刷新失败"提示随每次轮询反复闪烁(失败期内直接读缓存快照)
1472
1654
  const stale = (!snap.fetchedAt || (nowMs - snap.fetchedAt) > SUBSCRIPTION_REFRESH_MS)
1473
1655
  && (nowMs - lastFailAt) >= SUBSCRIPTION_RETRY_BACKOFF_MS;
1474
- if (stale) {
1475
- const inflight = kickSubscriptionRefresh(sourceKey);
1476
- // 从未成功过(无旧数据)→ 等本次刷新返回最新结果(含错误),避免退避重试后仍返回旧失败快照;
1477
- // 已有旧数据 → 后台刷新,本次直接返回快照(不阻塞轮询)
1478
- if (!snap.data) await inflight;
1656
+ // force(客户端打开/刷新网页后的首启窗口)绕过新鲜度与失败退避,当场重查,
1657
+ // 让用户手动刷新页面时立刻拿到最新额度/余额,而不是干等下一轮自动刷新
1658
+ if (force || stale) {
1659
+ const inflight = kickSubscriptionRefresh(sourceKey, subscriptionSourceProvider[sourceKey]);
1660
+ // 从未成功过(无旧数据)或强制刷新 → 等本次结果返回最新数据(含错误);
1661
+ // 已有旧数据的普通刷新 → 后台刷新,本次直接返回快照(不阻塞轮询)
1662
+ if (!snap.data || force) await inflight;
1479
1663
  }
1480
1664
  const cur = subscriptions[sourceKey] || { data: null, fetchedAt: null, error: null };
1481
1665
  if (cur.data) {
@@ -1483,6 +1667,7 @@ export default {
1483
1667
  out.windows = cur.data.windows;
1484
1668
  out.planType = cur.data.planType;
1485
1669
  out.expiryAt = cur.data.expiryAt;
1670
+ out.balance = typeof cur.data.balance === 'number' ? cur.data.balance : null;
1486
1671
  }
1487
1672
  out.fetchedAt = cur.fetchedAt;
1488
1673
  out.error = cur.error;
@@ -1497,7 +1682,7 @@ export default {
1497
1682
  cloudflare: { fetch: fetchCloudflareBilling }, // cloudflare-ai-gateway / cloudflare-workers-ai 共用
1498
1683
  };
1499
1684
 
1500
- async function getBillingSnapshotRpc(selection) {
1685
+ async function getBillingSnapshotRpc(selection, force) {
1501
1686
  const sel = selection || modelSelection();
1502
1687
  const bm = detectBillingMode(sel.provider, config.billingMode);
1503
1688
  const out = { mode: bm.mode, provider: sel.provider, reason: bm.reason, type: null, data: null, fetchedAt: null, error: null, now: Date.now() };
@@ -1511,9 +1696,10 @@ export default {
1511
1696
  const lastFailAt = billingLastFailAt[key] || 0;
1512
1697
  const stale = (!snap.fetchedAt || (nowMs - snap.fetchedAt) > BILLING_REFRESH_MS)
1513
1698
  && (nowMs - lastFailAt) >= BILLING_RETRY_BACKOFF_MS;
1514
- if (stale) {
1699
+ // force(客户端打开/刷新网页后的首启窗口)绕过新鲜度与失败退避,当场重查最新账单
1700
+ if (force || stale) {
1515
1701
  const inflight = kickBillingRefresh(key);
1516
- if (!snap.data) await inflight;
1702
+ if (!snap.data || force) await inflight;
1517
1703
  }
1518
1704
  const cur = billingSnapshots[key] || { data: null, fetchedAt: null, error: null };
1519
1705
  out.data = cur.data;
@@ -1705,9 +1891,18 @@ export default {
1705
1891
  }
1706
1892
 
1707
1893
  // ---------- 定价计算 ----------
1894
+ // 价目键解析:优先"服务商:模型"作用域键(解决同名模型跨计费域价格/币种不同的问题,
1895
+ // 如 Kimi 同一型号在 api.moonshot.cn=¥ 与 api.moonshot.ai=$ 两套价);
1896
+ // 无作用域键时回退裸模型键。远程目录可下发任意一种形态。
1897
+ function pricingEntryFor(provider, model) {
1898
+ const scoped = PRICING[provider + ':' + model];
1899
+ if (scoped && typeof scoped === 'object') return scoped;
1900
+ return PRICING[model] || null;
1901
+ }
1902
+
1708
1903
  function computePricing(nowMs, selection) {
1709
1904
  const sel = selection || modelSelection();
1710
- const entry = PRICING[sel.model];
1905
+ const entry = pricingEntryFor(sel.provider, sel.model);
1711
1906
  const period = entry && entry.mode === 'peak-valley' ? currentPeriod(nowMs) : 'flat';
1712
1907
  let prices = null;
1713
1908
  if (entry) {
@@ -1782,6 +1977,35 @@ export default {
1782
1977
  let dirty = loadedUsageRecords.migratedLegacyRecord;
1783
1978
  let ledgerError = null;
1784
1979
 
1980
+ // ---------- 一次性回填:unpriced 历史记录按当前价目表补算 ----------
1981
+ // 背景:价目表随适配逐步收录;收录之前落账的记录 pricingStatus='unpriced'(cost=null),
1982
+ // 导致本会话/今日/累计花费长期漏算(例如接入智谱后其历史对话金额恒为 0)。
1983
+ // 规则:① 只补"从未有过价格"的记录,绝不改写已 priced 的历史金额;
1984
+ // ② 仅限带稳定 id 的记录(无 id 的远古记录在快照+流水双源下无法安全幂等);
1985
+ // ③ 补算结果一次性写入快照,此后与正常记录同权参与全部汇总。
1986
+ function backfillUnpricedRecords() {
1987
+ let count = 0;
1988
+ for (let i = 0; i < usageRecords.length; i++) {
1989
+ const rec = usageRecords[i];
1990
+ if (!rec || rec.pricingStatus === 'priced') continue;
1991
+ if (!(typeof rec.id === 'string' && rec.id.length > 0)) continue;
1992
+ if (Number.isFinite(rec.cost) && rec.cost >= 0) continue;
1993
+ const billed = costOf(rec, false);
1994
+ if (billed == null) continue; // 价目表仍无该模型 → 维持 unpriced
1995
+ rec.cost = billed;
1996
+ rec.currency = modelCurrency(rec.provider, rec.model);
1997
+ rec.pricingStatus = 'priced';
1998
+ rec.pricingVersion = 'builtin-backfill-' + packageVersion();
1999
+ count++;
2000
+ }
2001
+ if (count > 0) {
2002
+ dirty = true;
2003
+ scheduleSave();
2004
+ console.warn('[dsh-bottom-info-bar] 价目表回填:' + count + ' 条 unpriced 记录已按官方单价补算');
2005
+ }
2006
+ return count;
2007
+ }
2008
+
1785
2009
  function writeAndSync(filePath, content, flags) {
1786
2010
  let fd = null;
1787
2011
  try {
@@ -1855,6 +2079,8 @@ export default {
1855
2079
  });
1856
2080
  }
1857
2081
 
2082
+ const unpricedWarnedModels = new Set(); // 每个模型只提醒一次,避免高频 warn 刷日志
2083
+
1858
2084
  function recordUsage(options, usage, status) {
1859
2085
  const u = usage || {};
1860
2086
  // 数值清洗:uncachedInputTokens 的 != null 对 NaN 恒真、|| 0 挡不住 Infinity/负值——
@@ -1875,14 +2101,28 @@ export default {
1875
2101
  // Freeze the actual price at usage time. Historical totals must not
1876
2102
  // change merely because the plugin's reference price table is updated.
1877
2103
  const billed = costOf(rec, false);
1878
- if (billed != null) {
1879
- rec.currency = modelCurrency(rec.model);
2104
+ // ① 聚合商在 usage 中直接给出的真实金额(如 OpenRouter 的 usage.cost)——
2105
+ // 官方报出的钱优先级最高,聚合商场景从此免维护静态价目表;
2106
+ // ② 价目表换算(当前各家官方单价);③ 都没有 → unpriced 待启动回填。
2107
+ if (billed == null && typeof u.cost === 'number' && Number.isFinite(u.cost) && u.cost >= 0) {
2108
+ rec.cost = u.cost;
2109
+ rec.currency = PROVIDER_REPORTED_CURRENCY[rec.provider] || modelCurrency(rec.provider, rec.model);
2110
+ rec.pricingStatus = 'priced';
2111
+ rec.pricingVersion = 'provider-reported-' + packageVersion();
2112
+ } else if (billed != null) {
2113
+ rec.currency = modelCurrency(rec.provider, rec.model);
1880
2114
  rec.cost = billed;
1881
2115
  rec.pricingStatus = 'priced';
1882
2116
  rec.pricingVersion = 'builtin-' + packageVersion();
1883
2117
  } else {
1884
2118
  rec.pricingStatus = 'unpriced';
1885
2119
  rec.pricingVersion = null;
2120
+ // 诊断:理论上本插件任一实例的 PRICING 均应同步;若此处为 false 说明存在多副本或键名不匹配。
2121
+ // 每个模型仅首次出现时提醒一次,防止未收录模型(如订阅目录下的混合路由)高频刷日志
2122
+ if (!unpricedWarnedModels.has(rec.model)) {
2123
+ unpricedWarnedModels.add(rec.model);
2124
+ console.warn('[dsh-bottom-info-bar] 记账未计价(每模型仅提示一次):model="' + rec.model + '" 价目表含该模型=' + Boolean(PRICING[rec.model] || PRICING[rec.provider + ':' + rec.model]));
2125
+ }
1886
2126
  }
1887
2127
  const writeError = appendUsageJournal(rec);
1888
2128
  if (writeError) {
@@ -1945,7 +2185,7 @@ export default {
1945
2185
  // v2 ledger records carry the charge as observed. Use it for normal
1946
2186
  // reporting; only the "all-offpeak" forecast intentionally recalculates.
1947
2187
  if (!forceOffpeak && Number.isFinite(record.cost) && record.cost >= 0) return record.cost;
1948
- const entry = PRICING[record.model];
2188
+ const entry = pricingEntryFor(record.provider, record.model);
1949
2189
  if (!entry) return null;
1950
2190
  let p;
1951
2191
  if (entry.mode === 'peak-valley') {
@@ -2063,13 +2303,13 @@ export default {
2063
2303
  const key = balanceProviderKey(sel.provider || config.activeProvider);
2064
2304
  const snap = balances[key];
2065
2305
  if (snap && snap.data && snap.data.currency) return snap.data.currency;
2066
- return modelCurrency(sel.model);
2306
+ return modelCurrency(sel.provider, sel.model);
2067
2307
  }
2068
2308
 
2069
2309
  function recordCurrency(record) {
2070
2310
  return record && typeof record.currency === 'string' && record.currency.length > 0
2071
2311
  ? record.currency
2072
- : modelCurrency(record && record.model);
2312
+ : modelCurrency(record && record.provider, record && record.model);
2073
2313
  }
2074
2314
 
2075
2315
  function spendSummary(nowMs, selection) {
@@ -2203,8 +2443,9 @@ export default {
2203
2443
  let scenarios = [];
2204
2444
  if (balance != null && pricing.prices) {
2205
2445
  const p = pricing.prices;
2206
- const peakPrices = pricing.mode === 'peak-valley' ? PRICING[pricing.model].peak : p;
2207
- const offpeakPrices = pricing.mode === 'peak-valley' ? PRICING[pricing.model].offpeak : p;
2446
+ const pvEntry = pricing.mode === 'peak-valley' ? pricingEntryFor(pricing.provider, pricing.model) : null;
2447
+ const peakPrices = pvEntry ? pvEntry.peak : p;
2448
+ const offpeakPrices = pvEntry ? pvEntry.offpeak : p;
2208
2449
  scenarios = SCENARIOS.map(function (sc) {
2209
2450
  return {
2210
2451
  id: sc.id, label: sc.label, outputK: sc.outputK, inputK: sc.inputK,
@@ -2349,8 +2590,14 @@ export default {
2349
2590
  getUpdateInfo: function () {
2350
2591
  return updateInfoPromise
2351
2592
  },
2352
- getBalanceSnapshot: function (args) {
2593
+ getBalanceSnapshot: async function (args) {
2353
2594
  const pid = args && typeof args === 'object' && args.provider ? String(args.provider) : '';
2595
+ // force:客户端刚打开/刷新网页时的强制刷新——当场重查服务商,不等 60s 周期缓存
2596
+ const force = !!(args && typeof args === 'object' && args.force === true);
2597
+ if (force) {
2598
+ const key = balanceProviderKey(pid || undefined);
2599
+ if (key) await refreshProviderBalance(key);
2600
+ }
2354
2601
  return activeBalanceSummary(pid || undefined, Date.now());
2355
2602
  },
2356
2603
  getPricing: async function (args) {
@@ -2394,11 +2641,13 @@ export default {
2394
2641
  return Object.assign(detectBillingMode(sel.provider, config.billingMode), { model: sel.model });
2395
2642
  },
2396
2643
  getSubscriptionSnapshot: function (args) {
2397
- return getSubscriptionSnapshotRpc(selectionFromArgs(args));
2644
+ const force = !!(args && typeof args === 'object' && args.force === true);
2645
+ return getSubscriptionSnapshotRpc(selectionFromArgs(args), force);
2398
2646
  },
2399
2647
  // v1.7 FR-14:账单型快照(云账单 provider)
2400
2648
  getBillingStatus: function (args) {
2401
- return getBillingSnapshotRpc(selectionFromArgs(args));
2649
+ const force = !!(args && typeof args === 'object' && args.force === true);
2650
+ return getBillingSnapshotRpc(selectionFromArgs(args), force);
2402
2651
  },
2403
2652
  setDisplayMode: function (args) {
2404
2653
  const mode = args && typeof args === 'object' ? args.mode : null;
@@ -2500,7 +2749,13 @@ export default {
2500
2749
  }, 'dsh-bottom-info-bar: Web routes');
2501
2750
  });
2502
2751
 
2503
- // ---------- 启动即刷 + 60s 定时刷新 ----------
2752
+ // ---------- 启动即刷 + 定时刷新 ----------
2753
+ // 价目目录:① 离线兜底(磁盘缓存先同步合并)→ ② 回填 → ③ 异步拉远程增量(成功后再回填一次)
2754
+ const cachedPricing = loadPricingCacheFromDisk();
2755
+ if (cachedPricing) applyRemotePricingEntries(cachedPricing);
2756
+ backfillUnpricedRecords(); // 内置表已覆盖的部分先补算
2757
+ refreshRemotePricing('启动'); // 网络刷新异步进行;成功后内部会再次触发回填
2758
+ ctx.interval(function () { refreshRemotePricing('定时'); }, REMOTE_PRICING_REFRESH_MS);
2504
2759
  refreshAllBalances();
2505
2760
  refreshActiveSubscriptions(); // 惰性:无客户端请求过订阅源则不发起网络请求
2506
2761
  refreshActiveBilling(); // v1.7:账单型同样惰性
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bottom-info-bar",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "DeepSeek Harness 底部信息栏插件:替换对话输入框下方的原生统计栏,显示服务商/具体模型、真实余额、高峰/空闲价格与倒计时、本会话(含子代理)·今日·近一月·全部花费。安装一次,每次打开 DSH 自动生效。",
5
5
  "keywords": [
6
6
  "deepseek",