dsh-bottom-info-bar 1.5.0 → 1.7.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 +142 -19
  2. package/lib/index.js +1021 -74
  3. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -6,7 +6,7 @@ var module = { exports: {} }; var exports = module.exports;
6
6
  // - styles.insert(css) → document 注入 <style>(installStyles)
7
7
  // - React 由 bundle 的 require('react') 提供(seed 模块)
8
8
  // 样式策略:① 整个数据令牌加粗(.bi-num 700)② 服务商名加粗 ③ 高峰价与低余额用警示红、空闲价用绿色
9
- // 显示行为:① 本对话花费始终显示——新会话/对话刚开始(尚无记账)时显示"本对话 ¥0.000",
9
+ // 显示行为:① 本会话花费始终显示——新会话/对话刚开始(尚无记账)时显示"本会话 ¥0.000",
10
10
  // hover 仍可查看持久化的 今天/近一月/全部;
11
11
  // ② 完整模式下原生统计行无 steps 门槛,对话刚开始即显示"0 轮 · 0 步"。
12
12
  // 失败策略(AUDIT-CODE-REVIEW 缺陷 #1):逐接口容错——
@@ -70,10 +70,10 @@ function rpc(method, args, externalSignal) {
70
70
 
71
71
  // load() 的逐接口容错状态合并(模块级纯函数,供单测提取):
72
72
  // 成功端点 → 写新值 + 清除错误;失败端点 → 保留旧值(无旧数据则为 null)+ 记录错误信息。
73
- // results 与端点顺序一一对应:balance / pricing / usage / billingMode / sub。
73
+ // results 与端点顺序一一对应:balance / pricing / usage / billingMode / sub / billing。
74
74
  function mergeLoadResults(prev, results) {
75
- const keys = ['balance', 'pricing', 'usage', 'billingMode', 'sub'];
76
- const next = { loading: false, errors: { balance: null, pricing: null, usage: null, billingMode: null, sub: null } };
75
+ const keys = ['balance', 'pricing', 'usage', 'billingMode', 'sub', 'billing'];
76
+ const next = { loading: false, errors: { balance: null, pricing: null, usage: null, billingMode: null, sub: null, billing: null } };
77
77
  for (let i = 0; i < keys.length; i++) {
78
78
  const key = keys[i];
79
79
  const r = results[i];
@@ -117,7 +117,7 @@ function installStyles() {
117
117
  .bi-sep { color: var(--bi-separator); margin: 0 6px; }
118
118
  /* 服务商名等一般强调:加粗 600 */
119
119
  .bi-root b { color: var(--bi-label-primary); font-weight: 600; }
120
- /* 数字:加粗 700(余额/倒计时/本对话花费/原生统计数字) */
120
+ /* 数字:加粗 700(余额/倒计时/本会话花费/原生统计数字) */
121
121
  .bi-root b.bi-num { font-weight: 700; }
122
122
  /* 标签与数据不用字符空格拼接:统一由 4px 布局间距控制,避免中英文/数字字宽造成忽松忽紧。 */
123
123
  .bi-metric { display: inline-flex; align-items: baseline; white-space: nowrap; }
@@ -242,8 +242,8 @@ module.exports = {
242
242
  const usageProj = props.useProjection ? props.useProjection('tokenUsage') : undefined;
243
243
 
244
244
  const [state, setState] = React.useState({
245
- loading: true, balance: null, pricing: null, usage: null, billingMode: null, sub: null,
246
- errors: { balance: null, pricing: null, usage: null, billingMode: null, sub: null },
245
+ loading: true, balance: null, pricing: null, usage: null, billingMode: null, sub: null, billing: null,
246
+ errors: { balance: null, pricing: null, usage: null, billingMode: null, sub: null, billing: null },
247
247
  });
248
248
  // 版本信息由 host 在启动时从 package.json 读取;无论是否有新版,都用于服务商/模型 hover 展示。
249
249
  const [updateInfo, setUpdateInfo] = React.useState(null);
@@ -352,6 +352,7 @@ module.exports = {
352
352
  rpc('getUsageSummary', Object.assign({ sessionId: sessionId }, selectionArgs), signal),
353
353
  rpc('getBillingMode', selectionArgs, signal),
354
354
  rpc('getSubscriptionSnapshot', selectionArgs, signal),
355
+ rpc('getBillingStatus', selectionArgs, signal),
355
356
  ]).then(function (results) {
356
357
  // Do not allow a late A response to overwrite newly active B.
357
358
  if ((signal && signal.aborted) || requestVersion !== loadVersionRef.current) return;
@@ -406,9 +407,13 @@ module.exports = {
406
407
  || state.pricing.provider !== activeSessionModel.provider || state.pricing.model !== activeSessionModel.model)
407
408
  ? { provider: activeSessionModel.provider, model: activeSessionModel.model, providerDisplay: activeSessionModel.providerDisplay, modelDisplay: activeSessionModel.modelDisplay, mode: 'unknown', acceptsImageInput: activeSessionModel.acceptsImageInput }
408
409
  : (waitForSessionModel ? null : state.pricing);
410
+ // v1.6 T7:订阅 provider 集合提取为共享常量,消除两端硬编码漂移
411
+ var SUBSCRIPTION_PROVIDERS = ['codex', 'chatgpt', 'opencode-go', 'opencode', 'openai-codex', 'zai', 'zai-coding-cn', 'xiaomi-token-plan-cn', 'xiaomi-token-plan-sgp', 'xiaomi-token-plan-ams'];
412
+ // v1.7 FR-14:云账单 provider 集合(账单型显示,与余额/额度互斥)
413
+ var BILLING_PROVIDERS = ['together', 'fireworks', 'amazon-bedrock', 'cloudflare-ai-gateway', 'cloudflare-workers-ai'];
409
414
  const visibleBillingMode = activeSessionModel && (!state.billingMode
410
415
  || state.billingMode.provider !== activeSessionModel.provider || state.billingMode.model !== activeSessionModel.model)
411
- ? { provider: activeSessionModel.provider, model: activeSessionModel.model, mode: ['codex', 'chatgpt', 'opencode-go', 'opencode', 'openai-codex'].indexOf(activeSessionModel.provider) >= 0 ? 'subscription' : 'balance' }
416
+ ? { provider: activeSessionModel.provider, model: activeSessionModel.model, mode: SUBSCRIPTION_PROVIDERS.indexOf(activeSessionModel.provider) >= 0 ? 'subscription' : (BILLING_PROVIDERS.indexOf(activeSessionModel.provider) >= 0 ? 'billing' : 'balance') }
412
417
  : (waitForSessionModel ? null : state.billingMode);
413
418
  // ---- 与原生一致格式工具 ----
414
419
  function formatTokens(n) {
@@ -527,7 +532,7 @@ module.exports = {
527
532
  const versionLine = updateInfo && typeof updateInfo.current === 'string'
528
533
  ? '\n插件版本:' + updateInfo.current : '';
529
534
  const provTitle = '服务商:' + provLabel + ' ' + modelLabel + '\n'
530
- + (pr && pr.mode === 'peak-valley' ? '定价:峰谷价(高峰 9-12、14-18 点)'
535
+ + (pr && pr.mode === 'peak-valley' ? '定价:峰谷价(工作日高峰 9-12、14-18 点;周末全天空闲)'
531
536
  : (pr && pr.mode === 'flat' ? '定价:固定价' : '定价:未收录,按默认计'))
532
537
  + versionLine;
533
538
  return React.createElement('span', { key: 'prov', className: 'bi-model-group', title: provTitle },
@@ -537,40 +542,79 @@ module.exports = {
537
542
 
538
543
  // 订阅服务名(订阅制模式下"服务商"指订阅服务本身,不是模型厂商)
539
544
  // Codex 与 ChatGPT 已合并:实际 provider openai-codex / chatgpt 均显示 ChatGPT;codex 保持 Codex
545
+ // v1.6 T7:新增 zai/zai-coding-cn → '智谱';v1.7:小米 Token Plan → '小米 MiMo'
540
546
  function subscriptionServiceName(provider) {
541
547
  if (provider === 'chatgpt' || provider === 'openai-codex') return 'ChatGPT';
542
548
  if (provider === 'codex') return 'Codex';
543
549
  if (provider === 'opencode-go' || provider === 'opencode') return 'OpenCode Go';
550
+ if (provider === 'zai' || provider === 'zai-coding-cn') return '智谱';
551
+ if (provider === 'xiaomi-token-plan-cn' || provider === 'xiaomi-token-plan-sgp' || provider === 'xiaomi-token-plan-ams') return '小米 MiMo';
544
552
  return '订阅';
545
553
  }
546
554
 
555
+ // 账单型服务名(v1.7:云账单 provider 显示品牌名,未知保持兜底)
556
+ function billingServiceName(provider) {
557
+ if (provider === 'together') return 'Together';
558
+ if (provider === 'fireworks') return 'Fireworks';
559
+ if (provider === 'amazon-bedrock') return 'AWS Bedrock';
560
+ if (provider === 'cloudflare-ai-gateway' || provider === 'cloudflare-workers-ai') return 'Cloudflare';
561
+ return '云账单';
562
+ }
563
+
564
+ // 套餐档位短名(JWT 订阅卡:plus/pro/team/enterprise → Plus/Pro/Team/Enterprise;未知返回 null 显示模型名)
565
+ function subscriptionPlanShort(planType) {
566
+ if (typeof planType !== 'string' || planType.length === 0) return null;
567
+ const map = { plus: 'Plus', pro: 'Pro', team: 'Team', enterprise: 'Enterprise' };
568
+ return map[planType.toLowerCase()] || null;
569
+ }
570
+
571
+ // 本地时区 YYYY-MM-DD(订阅到期日)
572
+ function formatDate(ms) {
573
+ if (ms == null || isNaN(ms)) return '—';
574
+ const d = new Date(ms);
575
+ const p = function (x) { return String(x).padStart(2, '0'); };
576
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
577
+ }
578
+
547
579
  // 订阅制模型组:订阅服务名 · 具体模型(如 `OpenCode Go · V4 Flash`、`Codex · GPT 5 Codex`)
580
+ // v1.7 FR-8:codex 源有 JWT 套餐档位时,模型位显示套餐档位(如 `ChatGPT · Plus`),真实信息来自 id_token claims
548
581
  function subscriptionProviderGroup() {
549
582
  const pr = visiblePricing;
550
583
  const serviceName = subscriptionServiceName(visibleBillingMode && visibleBillingMode.provider);
551
- const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
584
+ const subSnapshot = state.sub;
585
+ const planShort = subSnapshot && subSnapshot.planType ? subscriptionPlanShort(subSnapshot.planType) : null;
586
+ const rawModelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
552
587
  : (pr && pr.model ? pr.model : '未知模型');
588
+ const modelLabel = planShort ? planShort : rawModelLabel;
553
589
  const modelName = modelLabelWithoutProvider(modelLabel, serviceName);
554
590
  const versionLine = updateInfo && typeof updateInfo.current === 'string'
555
591
  ? '\n插件版本:' + updateInfo.current : '';
556
- const title = '订阅服务:' + serviceName + '\n模型:' + modelLabel + versionLine;
592
+ const planLine = subSnapshot && subSnapshot.plan ? '\n套餐:' + subSnapshot.plan : '';
593
+ const expiryLine = subSnapshot && subSnapshot.expiryAt ? '\n到期:' + formatDate(subSnapshot.expiryAt) + '(本地时区)' : '';
594
+ const title = '订阅服务:' + serviceName + '\n模型:' + rawModelLabel + planLine + expiryLine + versionLine;
557
595
  return React.createElement('span', { key: 'subprov', className: 'bi-model-group', title: title },
558
596
  React.createElement('b', { className: 'bi-model-provider' }, serviceName),
559
597
  modelDetail(pr, modelName),
560
598
  );
561
599
  }
562
600
 
563
- // ---- 余额制模式(v1.0.0 现状,完全不动):服务商+模型 → 余额 → 时段 → 倒计时 → 本对话花费 ----
601
+ // ---- 余额制模式(v1.0.0 现状,完全不动):服务商+模型 → 余额 → 时段 → 倒计时 → 本会话花费 ----
564
602
  function pushBalanceGroups(groups, trailingErrorGroups) {
565
603
  const bal = state.balance;
566
604
  const errors = state.errors || {};
567
605
  const alertActive = !!(bal && bal.alert && bal.alert.active);
568
606
  groups.push(providerGroup());
569
607
 
608
+ // v1.6 T7:未适配账户渲染"未适配"弱提示
609
+ if (bal && bal.unmapped) {
610
+ trailingErrorGroups.push(React.createElement('span', { className: 'bi-muted', key: 'unmapped', title: '该服务商暂未适配余额查询,后续版本将支持。' }, '未适配'));
611
+ }
570
612
  // 余额(纯金额;hover 仅展示余额,不显示充值/赠金)
571
- if (bal && bal.error && bal.error.kind === 'no-key') {
572
- trailingErrorGroups.push(React.createElement('span', { className: 'bi-err', key: 'nokey', title: '未配置 DeepSeek API Key。请在“设置 → 模型”中填写。' },
573
- '未配置 DEEPSEEK_API_KEY → 设置→模型 填写'));
613
+ // v1.6 T7:未配置提示改为按账户显示凭据名(去掉写死的 DeepSeek 文案)
614
+ else if (bal && bal.error && bal.error.kind === 'no-key') {
615
+ const credName = bal.error.message ? String(bal.error.message).replace('未配置 ', '') : 'API_KEY';
616
+ trailingErrorGroups.push(React.createElement('span', { className: 'bi-err', key: 'nokey', title: '未配置 ' + credName + '。请在"设置 → 模型"中填写。' },
617
+ '未配置 ' + credName + ' → 设置→模型 填写'));
574
618
  } else if (bal && bal.data) {
575
619
  const symbol = bal.currency === 'USD' ? '$' : '¥';
576
620
  const balTitle = bal.estimate
@@ -612,7 +656,7 @@ module.exports = {
612
656
  metric('距' + (peakNow ? '空闲' : '高峰'), fmtCountdown(pr.nextSwitch.at - now))));
613
657
  }
614
658
 
615
- // 本对话花费(只显示钱;hover 浮窗显示 今天 / 近一月 / 全部;金额数字加粗)
659
+ // 本会话花费(只显示钱;hover 浮窗显示 本会话(含子代理)+ 今天 / 近一月 / 全部;金额数字加粗)
616
660
  // 始终显示:新会话/对话刚开始尚无记账时显示 ¥0.000,hover 仍可查看持久化的 今天/近一月/全部
617
661
  const usg = state.usage;
618
662
  if (usg) {
@@ -627,8 +671,10 @@ module.exports = {
627
671
  const month = usg.monthSpend != null ? '近一月 ' + symbol + fmt(usg.monthSpend, 3) : '';
628
672
  const total = usg.totalSpend != null ? '全部 ' + symbol + fmt(usg.totalSpend, 3) : '';
629
673
  const detail = [today, month, total].filter(function (s) { return s.length > 0; }).join(' · ');
630
- groups.push(React.createElement('span', { key: 'convo', title: detail || '本对话花费' },
631
- metric('本对话', costTxt)));
674
+ // v1.7 发布前微调:hover 明确标注"含子代理"——本会话聚合同账户所有记录(同起点后)
675
+ groups.push(React.createElement('span', { key: 'convo',
676
+ title: '本会话 ' + costTxt + '(含子代理)' + (detail ? '\n' + detail : '') },
677
+ metric('本会话', costTxt)));
632
678
  } else if (errors.usage) {
633
679
  // 本次 RPC 失败且无旧数据:只降级花费块,其余端点数据照常渲染
634
680
  trailingErrorGroups.push(React.createElement('span', { className: 'bi-err', key: 'usageerr', title: '花费暂不可用;不会影响对话。' }, '花费获取失败'));
@@ -656,6 +702,11 @@ module.exports = {
656
702
  groups.push(subscriptionProviderGroup());
657
703
  const sub = state.sub;
658
704
  const errors = state.errors || {};
705
+ // v1.7 FR-8:JWT 订阅卡——真实套餐到期日(纯本地解码;无登录态/解析失败不显示此处)
706
+ if (sub && sub.planType && sub.expiryAt) {
707
+ groups.push(React.createElement('span', { key: 'subexp', title: '订阅到期:' + formatDate(sub.expiryAt) + '(本地时区)' },
708
+ metric('到期', formatDate(sub.expiryAt))));
709
+ }
659
710
  if (!sub) {
660
711
  if (errors.sub) {
661
712
  // 本次 RPC 失败且无旧数据:显示失败信息而非永久"加载中…"
@@ -723,18 +774,89 @@ module.exports = {
723
774
  }
724
775
  }
725
776
 
777
+ // ---- v1.7 账单型模式(FR-10~13/FR-14,互斥第三态):
778
+ // 账单服务+模型 → 本月真实花费 →(预算%)→(免费额度+重置倒计时);余额/额度/本会话花费均不显示 ----
779
+ function billingFailureHint(error, provider) {
780
+ const serviceName = billingServiceName(provider);
781
+ const message = error && typeof error.message === 'string' ? error.message : '';
782
+ const statusMatch = message.match(/HTTP (\d{3})/);
783
+ const status = statusMatch ? statusMatch[1] : '';
784
+ if (error && error.kind === 'no-key') return '未配置 ' + message.replace('未配置 ', '') + '。请在"设置 → 模型"中填写。';
785
+ if (status === '403' || /缺少 .*权限/.test(message)) return serviceName + ' 拒绝访问:Token 可能缺少账单读取权限。';
786
+ if (error && error.kind === 'parse') return serviceName + ' 返回的数据暂时无法识别。请稍后再试。';
787
+ if (/timeout|timed out|abort/i.test(message)) return serviceName + ' 响应超时。请检查网络后再试。';
788
+ return serviceName + ' 账单暂不可用。请检查网络与权限后再试。';
789
+ }
790
+
791
+ function pushBillingGroups(groups, trailingErrorGroups) {
792
+ groups.push(billingProviderGroup());
793
+ const bill = state.billing;
794
+ const errors = state.errors || {};
795
+ if (!bill) {
796
+ if (errors.billing) {
797
+ trailingErrorGroups.push(React.createElement('span', { className: 'bi-stale', key: 'billerr', title: billingFailureHint({ kind: 'exception', message: String(errors.billing) }, visibleBillingMode && visibleBillingMode.provider) }, '刷新失败'));
798
+ }
799
+ return;
800
+ }
801
+ const d = bill.data;
802
+ const hasSpend = !!(d && (d.currentPeriodSpend != null || d.usage != null));
803
+ if (bill.error && !hasSpend) {
804
+ trailingErrorGroups.push(React.createElement('span', { className: 'bi-stale', key: 'billstale', title: billingFailureHint(bill.error, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败'));
805
+ return;
806
+ }
807
+ if (hasSpend) {
808
+ const symbol = d.currency === 'CNY' ? '¥' : '$';
809
+ const titleLines = ['账单源:' + billingServiceName(visibleBillingMode && visibleBillingMode.provider)]
810
+ .concat(d.note ? [d.note] : [])
811
+ .concat(d.currentPeriodSpend != null ? ['本月花费:' + symbol + fmt(d.currentPeriodSpend, 2)] : [])
812
+ .concat(d.budgetPercent != null ? ['预算使用:' + fmt(d.budgetPercent, 0) + '%'] : [])
813
+ .concat(d.freeRemaining != null ? ['每日免费额度剩余:' + fmt(d.freeRemaining, 0)] : []);
814
+ const nodes = [];
815
+ if (d.currentPeriodSpend != null) nodes.push(metric('本月', symbol + fmt(d.currentPeriodSpend, 2)));
816
+ else if (d.usage != null) nodes.push(metric('本月用量', fmt(d.usage, 2) + (d.usageUnit ? ' ' + d.usageUnit : '')));
817
+ if (d.budgetPercent != null) nodes.push(' · ', metric('预算', fmt(d.budgetPercent, 0) + '%'));
818
+ // 免费额度仅当接口显式给出(freeRemaining/resetsAt 同时存在)才显示,绝不编造
819
+ if (d.freeRemaining != null && d.resetsAt) {
820
+ nodes.push(' · ', metric('免费', fmt(d.freeRemaining, 0) + ' · 距重置 ' + fmtResetCountdown(d.resetsAt - now)));
821
+ }
822
+ groups.push(React.createElement('span', { key: 'bill', title: titleLines.join('\n') }, ...nodes));
823
+ if (bill.error || errors.billing) {
824
+ trailingErrorGroups.push(React.createElement('span', { className: 'bi-stale', key: 'billstale', title: billingFailureHint(bill.error || { kind: 'exception', message: String(errors.billing || '') }, bill.type || (visibleBillingMode && visibleBillingMode.provider)) }, '刷新失败'));
825
+ }
826
+ }
827
+ }
828
+
829
+ // 账单型模型组:账单服务名 · 具体模型(如 `AWS Bedrock · Claude`)
830
+ function billingProviderGroup() {
831
+ const pr = visiblePricing;
832
+ const serviceName = billingServiceName(visibleBillingMode && visibleBillingMode.provider);
833
+ const modelLabel = (pr && pr.modelDisplay) ? pr.modelDisplay
834
+ : (pr && pr.model ? pr.model : '未知模型');
835
+ const modelName = modelLabelWithoutProvider(modelLabel, serviceName);
836
+ const versionLine = updateInfo && typeof updateInfo.current === 'string'
837
+ ? '\n插件版本:' + updateInfo.current : '';
838
+ const title = '账单服务:' + serviceName + '\n模型:' + modelLabel + versionLine;
839
+ return React.createElement('span', { key: 'billprov', className: 'bi-model-group', title: title },
840
+ React.createElement('b', { className: 'bi-model-provider' }, serviceName),
841
+ modelDetail(pr, modelName),
842
+ );
843
+ }
844
+
726
845
  const groups = [];
727
846
  // 报错不打断主要信息的阅读顺序:统一延后到整行最右侧。
728
847
  const trailingErrorGroups = [];
729
848
  // 两态严格判定:density 只能是 'full' 或 'compact'(host 校验 + 本地防抖保证)
730
849
  const full = displayDensity === 'full';
731
- // 模式互斥:订阅制渲染订阅版 row2,余额制渲染 v1.0.0 现状,绝不叠加
850
+ // 模式互斥:订阅制渲染订阅版 row2,账单制渲染账单版 row2,余额制渲染 v1.0.0 现状——三态绝不叠加(FR-14)
732
851
  const isSub = !!(visibleBillingMode && visibleBillingMode.mode === 'subscription');
852
+ const isBilling = !!(visibleBillingMode && visibleBillingMode.mode === 'billing');
733
853
  // Never paint a loading placeholder. Before the active session's model
734
854
  // is available, leave this compact row empty rather than briefly showing
735
855
  // either a generic loading label or data from the previous session.
736
856
  if (waitForSessionModel) {
737
857
  // Intentionally empty: session model publish fills the row immediately.
858
+ } else if (isBilling) {
859
+ pushBillingGroups(groups, trailingErrorGroups);
738
860
  } else if (isSub) {
739
861
  pushSubscriptionGroups(groups, trailingErrorGroups);
740
862
  } else {
@@ -749,6 +871,7 @@ module.exports = {
749
871
  if (errors.usage) failedLabels.push('花费');
750
872
  if (errors.billingMode) failedLabels.push('模式');
751
873
  if (errors.sub) failedLabels.push('订阅额度');
874
+ if (errors.billing) failedLabels.push('账单');
752
875
  if (failedLabels.length > 0) {
753
876
  trailingErrorGroups.push(React.createElement('span', { className: 'bi-stale', key: 'degraded',
754
877
  title: failedLabels.join('、') + '暂不可用;正在保留上次数据并自动重试。' }, '刷新失败'));