dsh-codearts 1.2.3 → 1.2.4

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/CHANGELOG.md CHANGED
@@ -3,6 +3,49 @@
3
3
  本文件记录本插件的重要改动。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
4
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
5
5
 
6
+ ## [1.2.4] - 2026-11-03
7
+
8
+ ### 变更
9
+
10
+ - **登录回调页不再停在一行「可关闭」提示上,标签页会自己关掉**:华为授权完成后浏览器
11
+ 必然落回 `http://127.0.0.1:<port>/oauth/callback` —— redirect_uri 由华为侧指定,登录成功
12
+ 的标志就是这次 302,华为那边没有自己的「登录成功」页可停留;这一页也没有任何后续逻辑,
13
+ `code` 在响应发出的同一时刻就交给了服务端,token 交换、身份探测与卡片刷新都不依赖它。
14
+ 现在页面先显示「正在返回 DeepSeek Harness…」,300ms 后调用 `window.close()` 自动关闭
15
+ 标签页(卡片用 `window.open` 打开授权页,标签页可自关);只有从卡片的手动链接打开、
16
+ 或浏览器拦截脚本关窗时,才会在 1.2s 后退化成「登录成功,本页可关闭」的提示。
17
+
18
+ ### 新增
19
+
20
+ - **用量里记「缓存命中的输入 token」,弹层保持两列、不摊开**:部分上游在流末尾的
21
+ usage 帧里带 `prompt_tokens_details.cached_tokens`(或平铺的 `cached_tokens` /
22
+ `cache_tokens` / `prompt_cache_hit_tokens`),这块以前被丢掉,只留下一个合计。
23
+ 现在解析并存进账本(缓存是输入的子集,脏数据会钳到输入以内),设置卡片的「今日」
24
+ 与「近 30 天」显示成 `输入 x · 输出 y · 缓存 z`;缓存段**始终显示**(没有命中就是
25
+ `缓存 0`,命中率仅在有命中时附上)—— 隐藏 0 会让「没命中」和「上游没下发该字段」
26
+ 看起来一样,反而分不清功能有没有生效。旧账本(没有 `cachedTokens` 字段)读取时按
27
+ 0 补齐,不需要迁移。
28
+ 底部积分弹层只回答「哪个号还有多少积分」,这些用量数字到设置卡片的「用量」页里看。
29
+ `/api/codearts/debug` 另外返回最近 5 帧 usage 的原样内容
30
+ (`usages`),用来判断上游到底有没有下发缓存字段。
31
+ - **底部的积分挪进输入框工具行、权限控制右边**:输入框下方那行还挤着宿主的 tok/s、
32
+ 缓存命中与上下文圆环,模型名一长(`DeepSeek-V4.1-Flash-20261017`)就压不住。现在
33
+ 积分段挂在 `conversation.input.left`(宿主契约里的「composer 工具行左侧紧凑控件」,
34
+ 渲染在权限 + 计划之后),Provider / 模型 / 倍率 / 排队仍留在输入框下方那行 —— 同一个
35
+ 组件的两个实例,用注入的 `zone` 区分,共用同一份数据和两个显隐开关。点开仍是逐号
36
+ 明细:每个号一行「名字 + 余额」,下面一行 11px 灰字 `入 · 出 · 命中 · 条数`(思考
37
+ token 放到设置卡片里),原来那个悬停才出的原生 title 和跟着走的尾巴 `·` 一并去掉。
38
+ - **设置卡片的「用量」页:数字块 + 横条 + 悬停卡片**:顶部三个数字块(今日 token /
39
+ 缓存命中 / 今日请求)替掉原来的「今日合计 / 近 30 天」两行文字;下面仍按账号、按模型
40
+ 出横向柱状图,悬停任意一行弹出明细卡片(输入 / 输出 / 思考 / 缓存命中 / 请求 /
41
+ 命中率,数字精确到个位带千分位),版式照 `agent-acta` 的 `.tok-tip`:名称打头、
42
+ 分隔线分段、数字等宽右对齐。上游没下发缓存字段时写「上游未下发」,不显示成 0%。
43
+ - **同时记下「思考 token」**:上游在 `completion_tokens_details.reasoning_tokens`
44
+ (平铺的 `reasoning_tokens` 也认)里单列推理模型的思考量 —— 实测华为每帧都带,
45
+ 以前整块丢掉,只看得到一个偏大的「输出」。现在存进账本(它是输出的子集,脏数据钳到
46
+ 输出以内),界面统一显示成 `输出 697(思考 505)`,上游不给就只是 `输出 697`;
47
+ 0 不单独成段,也不显示 `思考 0`。旧账本同样按 0 补齐。
48
+
6
49
  ## [1.2.3] - 2026-11-02
7
50
 
8
51
  ### 新增
package/lib/bin.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { R as isBenefitModel, V as CodeArtsAccountStore, W as codeartsAccountsPath, d as fetchCallerIdentity, n as codeartsUsagePath, r as discoverModels, t as CodeArtsUsageTracker } from "./usage-CzJjkqOp.mjs";
2
+ import { R as isBenefitModel, V as CodeArtsAccountStore, W as codeartsAccountsPath, d as fetchCallerIdentity, n as codeartsUsagePath, r as discoverModels, t as CodeArtsUsageTracker } from "./usage-DGIuT79_.mjs";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  //#region src/version.ts
5
- const CODEARTS_VERSION = "1.2.3";
5
+ const CODEARTS_VERSION = "1.2.4";
6
6
  //#endregion
7
7
  //#region src/bin.ts
8
8
  /**
@@ -62,12 +62,28 @@ interface CodeArtsSettingsKey {
62
62
  modelsLoading: string;
63
63
  usageTitle: string;
64
64
  usageToday: string;
65
- usageTodayTotal: string;
66
- usageLast30: string;
67
- usageByAccount: string;
65
+ usageRange: string;
66
+ usageStatTokens: string;
67
+ usageStatIn: string;
68
+ usageStatThinking: string;
69
+ usageTipInput: string;
70
+ usageTipOutput: string;
71
+ usageTipReasoning: string;
72
+ usageTipCached: string;
73
+ usageTipNoCache: string;
74
+ usageTipRequests: string;
75
+ usageTipHit: string;
76
+ usageStatHit: string;
77
+ usageStatRequests: string;
68
78
  usageRequests: string;
79
+ usageByAccount: string;
69
80
  usageRequestCount: string;
70
81
  usageTokens: string;
82
+ /** 带缓存命中输入的那一版(上游不给缓存字段时仍用 usageTokens)。 */
83
+ usageCompact: string;
84
+ /** 缓存命中率(`命中 76%`)。 */
85
+ usageCacheHit: string;
86
+ /** 接在输出数字后面的思考 token(`输出 697(思考 505)`);没有思考量时不拼。 */
71
87
  usageAllTime: string;
72
88
  usageByModel: string;
73
89
  usageSlots: string;
@@ -94,9 +110,6 @@ interface CodeArtsSettingsKey {
94
110
  creditsDockPanelTitle: string;
95
111
  creditsDockRefresh: string;
96
112
  creditsDockRefreshing: string;
97
- creditsDockColAccount: string;
98
- creditsDockColBalance: string;
99
- creditsDockColTokens: string;
100
113
  creditsDockNoTokens: string;
101
114
  creditsDockEmpty: string;
102
115
  creditsDockExpiring: string;
@@ -23,7 +23,7 @@ function stripModelSuffixes(name) {
23
23
  }
24
24
  //#endregion
25
25
  //#region \0dsh-css:/home/runner/work/dsh-codearts/dsh-codearts/src/client/CodeArtsPluginCard.module.css.mjs
26
- const css = ".xoziZG_card{border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-settings-card-fill);max-width:720px}.xoziZG_header{align-items:flex-start;gap:12px;padding:14px 16px;display:flex}.xoziZG_headText{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.xoziZG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:22px}.xoziZG_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.xoziZG_statusLine{color:var(--dsw-alias-label-primary);align-items:center;gap:6px;font-size:14px;font-weight:500;line-height:22px;display:inline-flex}.xoziZG_meta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:18px}.xoziZG_body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 16px}.xoziZG_tabs{margin-top:12px}.xoziZG_tabPanel{padding-top:2px}.xoziZG_accountToolbar{gap:8px;margin-top:12px;display:flex}.xoziZG_accountToolbar>*{flex:1}.xoziZG_manualForm{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);margin-top:12px;padding:12px}.xoziZG_accountCard{flex-direction:column;gap:10px;margin-top:12px;display:flex}.xoziZG_accountEntry{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2)}.xoziZG_accountRow{align-items:center;gap:10px;padding:12px;display:flex}.xoziZG_claimAllRow{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:flex-end;padding:10px 12px 0;display:flex}.xoziZG_avatar{background:var(--dsw-alias-bg-layer-3);width:32px;height:32px;color:var(--dsw-alias-label-primary);border-radius:50%;flex:none;justify-content:center;align-items:center;font-size:14px;font-weight:600;display:inline-flex}.xoziZG_accountInfo{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.xoziZG_accountNameRow{align-items:center;gap:6px;min-width:0;display:flex}.xoziZG_accountName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600;line-height:20px;overflow:hidden}.xoziZG_accountRowActions{flex:none;gap:6px;display:flex}.xoziZG_accountHint{color:var(--dsw-alias-label-tertiary);margin:0;padding:0 12px 12px;font-size:12px;line-height:18px}.xoziZG_creditBlock{border-top:.5px solid var(--dsw-alias-border-l2);padding:12px}.xoziZG_balanceRow{flex-wrap:wrap;align-items:baseline;gap:6px;display:flex}.xoziZG_balance{color:var(--dsw-alias-label-primary);letter-spacing:-.02em;font-variant-numeric:tabular-nums;font-size:28px;font-weight:600;line-height:34px}.xoziZG_balanceUnit{color:var(--dsw-alias-label-secondary);font-size:12px}.xoziZG_balanceMeta{color:var(--dsw-alias-label-tertiary);margin-left:2px;font-size:12px}.xoziZG_updated{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;margin-left:auto;font-size:12px}.xoziZG_creditRows{flex-direction:column;gap:4px;margin-top:8px;display:flex}.xoziZG_creditRow{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;justify-content:space-between;align-items:baseline;gap:12px;font-size:12px;line-height:18px;display:flex}.xoziZG_usageBar{background:var(--dsw-alias-border-l2);border-radius:2px;height:4px;margin-top:4px;display:block;overflow:hidden}.xoziZG_usageFill{background:var(--dsw-alias-brand-primary);border-radius:2px;height:100%;transition:width .2s;display:block}.xoziZG_expiryRow{margin-top:2px}.xoziZG_expiryLabel{color:var(--dsw-alias-state-warn-label)}.xoziZG_bar{background:var(--dsw-alias-bg-layer-3);border-radius:2px;height:4px;margin-top:6px;display:block;overflow:hidden}.xoziZG_barFill{background:var(--dsw-alias-state-warn-primary);border-radius:2px;height:100%;display:block}.xoziZG_creditLink{margin-top:8px;display:inline-block}.xoziZG_creditFooter{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:8px;margin-top:12px;padding-top:10px;display:flex}.xoziZG_creditHint{color:var(--dsw-alias-label-tertiary);margin:8px 0 0;font-size:12px;line-height:18px}.xoziZG_section{margin-top:16px}.xoziZG_sectionHead{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.xoziZG_sectionLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:500}.xoziZG_count{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:12px}.xoziZG_hint{color:var(--dsw-alias-label-secondary);margin:8px 0 0;font-size:13px;line-height:20px}.xoziZG_error{color:var(--dsw-alias-state-error-primary);margin:10px 0 0;font-size:13px;line-height:20px}.xoziZG_actions{flex-wrap:wrap;align-items:center;gap:8px;margin-top:12px;display:flex}.xoziZG_danger{color:var(--dsw-alias-state-error-primary)}.xoziZG_link{appearance:none;color:var(--dsw-alias-brand-text);font:inherit;text-align:left;word-break:break-all;cursor:pointer;background:0 0;border:none;padding:0;font-size:12px}.xoziZG_link:hover{text-decoration:underline}.xoziZG_choices{flex-direction:column;gap:2px;margin-top:8px;display:flex}.xoziZG_choiceRow{border-radius:var(--dsw-radius-sm);align-items:center;gap:8px;margin:0 -6px;padding:5px 6px;display:flex}.xoziZG_choiceRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.xoziZG_modelChart{flex-direction:column;gap:2px;margin-top:8px;display:flex}.xoziZG_modelRow{font-variant-numeric:tabular-nums;grid-template-columns:minmax(0,1.2fr) minmax(48px,1fr) 56px 52px 64px;align-items:center;gap:8px;padding:4px 0;display:grid}.xoziZG_modelName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.xoziZG_modelTrack{background:var(--dsw-alias-border-l2);border-radius:4px;height:8px;display:block;overflow:hidden}.xoziZG_modelFill{background:var(--dsw-alias-brand-primary);border-radius:4px;height:100%;transition:width .16s;display:block}.xoziZG_modelTokens,.xoziZG_modelShare,.xoziZG_modelRequests{color:var(--dsw-alias-label-secondary);text-align:right;white-space:nowrap;font-size:11px;line-height:16px}.xoziZG_modelRequests{color:var(--dsw-alias-label-tertiary)}.xoziZG_check{flex:1;min-width:0}.xoziZG_check>span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.xoziZG_manual{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);margin-top:12px;padding:0 12px}.xoziZG_manual[open]{padding-bottom:12px}.xoziZG_manualSummary{color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:6px;padding:9px 0;font-size:13px;list-style:none;display:flex}.xoziZG_manualSummary::-webkit-details-marker{display:none}.xoziZG_field{flex-direction:column;gap:5px;margin-top:10px;display:flex}.xoziZG_fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px}.xoziZG_area{box-sizing:border-box;resize:vertical;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:20px}.xoziZG_area::placeholder{color:var(--dsw-alias-label-dimmed)}.xoziZG_area:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}";
26
+ const css = ".xoziZG_card{border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-settings-card-fill);max-width:720px}.xoziZG_header{align-items:flex-start;gap:12px;padding:14px 16px;display:flex}.xoziZG_headText{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.xoziZG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:22px}.xoziZG_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}.xoziZG_statusLine{color:var(--dsw-alias-label-primary);align-items:center;gap:6px;font-size:14px;font-weight:500;line-height:22px;display:inline-flex}.xoziZG_meta{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px;line-height:18px}.xoziZG_body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:4px 0 16px}.xoziZG_tabs{margin-top:12px}.xoziZG_tabPanel{padding-top:2px}.xoziZG_accountToolbar{gap:8px;margin-top:12px;display:flex}.xoziZG_accountToolbar>*{flex:1}.xoziZG_manualForm{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);margin-top:12px;padding:12px}.xoziZG_accountCard{flex-direction:column;gap:10px;margin-top:12px;display:flex}.xoziZG_accountEntry{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-2)}.xoziZG_accountRow{align-items:center;gap:10px;padding:12px;display:flex}.xoziZG_claimAllRow{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:flex-end;padding:10px 12px 0;display:flex}.xoziZG_avatar{background:var(--dsw-alias-bg-layer-3);width:32px;height:32px;color:var(--dsw-alias-label-primary);border-radius:50%;flex:none;justify-content:center;align-items:center;font-size:14px;font-weight:600;display:inline-flex}.xoziZG_accountInfo{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.xoziZG_accountNameRow{align-items:center;gap:6px;min-width:0;display:flex}.xoziZG_accountName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600;line-height:20px;overflow:hidden}.xoziZG_accountRowActions{flex:none;gap:6px;display:flex}.xoziZG_accountHint{color:var(--dsw-alias-label-tertiary);margin:0;padding:0 12px 12px;font-size:12px;line-height:18px}.xoziZG_creditBlock{border-top:.5px solid var(--dsw-alias-border-l2);padding:12px}.xoziZG_balanceRow{flex-wrap:wrap;align-items:baseline;gap:6px;display:flex}.xoziZG_balance{color:var(--dsw-alias-label-primary);letter-spacing:-.02em;font-variant-numeric:tabular-nums;font-size:28px;font-weight:600;line-height:34px}.xoziZG_balanceUnit{color:var(--dsw-alias-label-secondary);font-size:12px}.xoziZG_balanceMeta{color:var(--dsw-alias-label-tertiary);margin-left:2px;font-size:12px}.xoziZG_updated{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;margin-left:auto;font-size:12px}.xoziZG_creditRows{flex-direction:column;gap:4px;margin-top:8px;display:flex}.xoziZG_creditRow{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;justify-content:space-between;align-items:baseline;gap:12px;font-size:12px;line-height:18px;display:flex}.xoziZG_usageBar{background:var(--dsw-alias-border-l2);border-radius:2px;height:4px;margin-top:4px;display:block;overflow:hidden}.xoziZG_usageFill{background:var(--dsw-alias-brand-primary);border-radius:2px;height:100%;transition:width .2s;display:block}.xoziZG_expiryRow{margin-top:2px}.xoziZG_expiryLabel{color:var(--dsw-alias-state-warn-label)}.xoziZG_bar{background:var(--dsw-alias-bg-layer-3);border-radius:2px;height:4px;margin-top:6px;display:block;overflow:hidden}.xoziZG_barFill{background:var(--dsw-alias-state-warn-primary);border-radius:2px;height:100%;display:block}.xoziZG_creditLink{margin-top:8px;display:inline-block}.xoziZG_creditFooter{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:8px;margin-top:12px;padding-top:10px;display:flex}.xoziZG_creditHint{color:var(--dsw-alias-label-tertiary);margin:8px 0 0;font-size:12px;line-height:18px}.xoziZG_section{margin-top:16px}.xoziZG_sectionHead{justify-content:space-between;align-items:baseline;gap:12px;display:flex}.xoziZG_sectionLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:500}.xoziZG_count{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:12px}.xoziZG_hint{color:var(--dsw-alias-label-secondary);margin:8px 0 0;font-size:13px;line-height:20px}.xoziZG_error{color:var(--dsw-alias-state-error-primary);margin:10px 0 0;font-size:13px;line-height:20px}.xoziZG_actions{flex-wrap:wrap;align-items:center;gap:8px;margin-top:12px;display:flex}.xoziZG_danger{color:var(--dsw-alias-state-error-primary)}.xoziZG_link{appearance:none;color:var(--dsw-alias-brand-text);font:inherit;text-align:left;word-break:break-all;cursor:pointer;background:0 0;border:none;padding:0;font-size:12px}.xoziZG_link:hover{text-decoration:underline}.xoziZG_choices{flex-direction:column;gap:2px;margin-top:8px;display:flex}.xoziZG_choiceRow{border-radius:var(--dsw-radius-sm);align-items:center;gap:8px;margin:0 -6px;padding:5px 6px;display:flex}.xoziZG_choiceRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.xoziZG_statGrid{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:10px 0 14px;display:grid}.xoziZG_statCard{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);flex-direction:column;gap:1px;padding:8px 10px;display:flex}.xoziZG_statLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.xoziZG_statValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:15px;line-height:20px}.xoziZG_statSub{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;line-height:16px}.xoziZG_modelChart{flex-direction:column;gap:2px;margin-top:8px;display:flex}.xoziZG_modelRow{font-variant-numeric:tabular-nums;border-radius:6px;grid-template-columns:minmax(0,1.2fr) minmax(48px,1fr) 56px 52px 64px;align-items:center;gap:8px;padding:4px 0;display:grid;position:relative}.xoziZG_modelRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.xoziZG_usageTip{z-index:5;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);min-width:180px;max-width:260px;color:var(--dsw-alias-label-secondary);text-align:left;font-variant-numeric:tabular-nums;pointer-events:none;padding:8px 10px;font-size:12px;line-height:1.9;position:absolute;bottom:calc(100% + 6px);right:0;box-shadow:0 10px 28px #00000073}.xoziZG_usageTipName{max-width:240px;color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.xoziZG_usageTipSep{background:var(--dsw-alias-border-l2);height:1px;margin:4px 0}.xoziZG_usageTipRow{justify-content:space-between;gap:24px;display:flex}.xoziZG_usageTipRow span:last-child{color:var(--dsw-alias-label-primary);font-weight:600}.xoziZG_modelName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.xoziZG_modelTrack{background:var(--dsw-alias-border-l2);border-radius:4px;height:8px;display:block;overflow:hidden}.xoziZG_modelFill{background:var(--dsw-alias-brand-primary);border-radius:4px;height:100%;transition:width .16s;display:block}.xoziZG_modelTokens,.xoziZG_modelShare,.xoziZG_modelRequests{color:var(--dsw-alias-label-secondary);text-align:right;white-space:nowrap;font-size:11px;line-height:16px}.xoziZG_modelRequests{color:var(--dsw-alias-label-tertiary)}.xoziZG_check{flex:1;min-width:0}.xoziZG_check>span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.xoziZG_manual{border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);margin-top:12px;padding:0 12px}.xoziZG_manual[open]{padding-bottom:12px}.xoziZG_manualSummary{color:var(--dsw-alias-label-secondary);cursor:pointer;align-items:center;gap:6px;padding:9px 0;font-size:13px;list-style:none;display:flex}.xoziZG_manualSummary::-webkit-details-marker{display:none}.xoziZG_field{flex-direction:column;gap:5px;margin-top:10px;display:flex}.xoziZG_fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px}.xoziZG_area{box-sizing:border-box;resize:vertical;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:20px}.xoziZG_area::placeholder{color:var(--dsw-alias-label-dimmed)}.xoziZG_area:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}";
27
27
  const tagId = "dsh-codearts/CodeArtsPluginCard.module.css";
28
28
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
29
29
  const tag = document.createElement("style");
@@ -33,70 +33,79 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
33
33
  document.head.appendChild(tag);
34
34
  }
35
35
  var CodeArtsPluginCard_module_css_default = {
36
+ "field": "xoziZG_field",
37
+ "manualForm": "xoziZG_manualForm",
38
+ "accountRow": "xoziZG_accountRow",
39
+ "section": "xoziZG_section",
40
+ "expiryLabel": "xoziZG_expiryLabel",
41
+ "check": "xoziZG_check",
42
+ "balance": "xoziZG_balance",
43
+ "accountToolbar": "xoziZG_accountToolbar",
44
+ "barFill": "xoziZG_barFill",
45
+ "statLabel": "xoziZG_statLabel",
46
+ "meta": "xoziZG_meta",
36
47
  "accountNameRow": "xoziZG_accountNameRow",
37
- "balanceRow": "xoziZG_balanceRow",
38
- "modelFill": "xoziZG_modelFill",
39
- "danger": "xoziZG_danger",
40
48
  "expiryRow": "xoziZG_expiryRow",
49
+ "balanceUnit": "xoziZG_balanceUnit",
50
+ "accountInfo": "xoziZG_accountInfo",
51
+ "hint": "xoziZG_hint",
52
+ "count": "xoziZG_count",
53
+ "choices": "xoziZG_choices",
54
+ "usageFill": "xoziZG_usageFill",
55
+ "card": "xoziZG_card",
56
+ "usageTipName": "xoziZG_usageTipName",
57
+ "balanceRow": "xoziZG_balanceRow",
58
+ "creditBlock": "xoziZG_creditBlock",
59
+ "header": "xoziZG_header",
60
+ "statSub": "xoziZG_statSub",
61
+ "usageTip": "xoziZG_usageTip",
62
+ "modelRequests": "xoziZG_modelRequests",
63
+ "usageBar": "xoziZG_usageBar",
64
+ "bar": "xoziZG_bar",
65
+ "creditRow": "xoziZG_creditRow",
66
+ "sectionLabel": "xoziZG_sectionLabel",
41
67
  "accountEntry": "xoziZG_accountEntry",
42
- "accountHint": "xoziZG_accountHint",
43
- "manualSummary": "xoziZG_manualSummary",
44
- "body": "xoziZG_body",
45
- "barFill": "xoziZG_barFill",
68
+ "modelTrack": "xoziZG_modelTrack",
69
+ "link": "xoziZG_link",
46
70
  "updated": "xoziZG_updated",
47
- "error": "xoziZG_error",
48
- "avatar": "xoziZG_avatar",
49
- "balanceMeta": "xoziZG_balanceMeta",
50
71
  "desc": "xoziZG_desc",
51
- "name": "xoziZG_name",
72
+ "avatar": "xoziZG_avatar",
73
+ "creditHint": "xoziZG_creditHint",
52
74
  "tabPanel": "xoziZG_tabPanel",
53
- "accountInfo": "xoziZG_accountInfo",
54
- "card": "xoziZG_card",
55
- "balanceUnit": "xoziZG_balanceUnit",
56
- "sectionHead": "xoziZG_sectionHead",
57
- "choiceRow": "xoziZG_choiceRow",
58
- "balance": "xoziZG_balance",
59
- "creditLink": "xoziZG_creditLink",
60
- "accountRow": "xoziZG_accountRow",
61
- "usageFill": "xoziZG_usageFill",
62
- "modelTrack": "xoziZG_modelTrack",
75
+ "fieldLabel": "xoziZG_fieldLabel",
76
+ "modelName": "xoziZG_modelName",
77
+ "danger": "xoziZG_danger",
78
+ "statValue": "xoziZG_statValue",
63
79
  "area": "xoziZG_area",
64
- "check": "xoziZG_check",
80
+ "claimAllRow": "xoziZG_claimAllRow",
81
+ "tabs": "xoziZG_tabs",
82
+ "usageTipSep": "xoziZG_usageTipSep",
83
+ "name": "xoziZG_name",
65
84
  "headText": "xoziZG_headText",
66
85
  "accountRowActions": "xoziZG_accountRowActions",
67
- "creditHint": "xoziZG_creditHint",
68
- "actions": "xoziZG_actions",
69
- "accountCard": "xoziZG_accountCard",
70
- "claimAllRow": "xoziZG_claimAllRow",
71
- "link": "xoziZG_link",
72
- "modelTokens": "xoziZG_modelTokens",
73
- "sectionLabel": "xoziZG_sectionLabel",
74
- "usageBar": "xoziZG_usageBar",
75
- "section": "xoziZG_section",
76
- "modelName": "xoziZG_modelName",
77
- "meta": "xoziZG_meta",
78
- "bar": "xoziZG_bar",
79
- "header": "xoziZG_header",
80
- "fieldLabel": "xoziZG_fieldLabel",
81
- "choices": "xoziZG_choices",
82
- "creditBlock": "xoziZG_creditBlock",
83
- "creditRow": "xoziZG_creditRow",
84
- "field": "xoziZG_field",
85
- "hint": "xoziZG_hint",
86
- "count": "xoziZG_count",
86
+ "sectionHead": "xoziZG_sectionHead",
87
+ "modelRow": "xoziZG_modelRow",
88
+ "modelFill": "xoziZG_modelFill",
89
+ "accountHint": "xoziZG_accountHint",
90
+ "modelChart": "xoziZG_modelChart",
91
+ "error": "xoziZG_error",
92
+ "body": "xoziZG_body",
93
+ "manualSummary": "xoziZG_manualSummary",
94
+ "balanceMeta": "xoziZG_balanceMeta",
95
+ "statGrid": "xoziZG_statGrid",
96
+ "choiceRow": "xoziZG_choiceRow",
97
+ "modelShare": "xoziZG_modelShare",
87
98
  "accountName": "xoziZG_accountName",
88
- "tabs": "xoziZG_tabs",
89
- "manualForm": "xoziZG_manualForm",
99
+ "modelTokens": "xoziZG_modelTokens",
100
+ "creditLink": "xoziZG_creditLink",
90
101
  "creditFooter": "xoziZG_creditFooter",
91
- "modelRow": "xoziZG_modelRow",
92
102
  "manual": "xoziZG_manual",
93
- "modelChart": "xoziZG_modelChart",
94
- "modelRequests": "xoziZG_modelRequests",
95
- "accountToolbar": "xoziZG_accountToolbar",
96
103
  "statusLine": "xoziZG_statusLine",
97
104
  "creditRows": "xoziZG_creditRows",
98
- "expiryLabel": "xoziZG_expiryLabel",
99
- "modelShare": "xoziZG_modelShare"
105
+ "statCard": "xoziZG_statCard",
106
+ "accountCard": "xoziZG_accountCard",
107
+ "usageTipRow": "xoziZG_usageTipRow",
108
+ "actions": "xoziZG_actions"
100
109
  };
101
110
  //#endregion
102
111
  //#region src/client/CodeArtsPluginCard.tsx
@@ -128,6 +137,10 @@ function formatCount(value) {
128
137
  function formatCredits(value) {
129
138
  return Number.isFinite(value) ? new Intl.NumberFormat().format(Math.round(value)) : "0";
130
139
  }
140
+ /** 浮层里的明细数字要精确到个位并带千分位,不用 k/M 缩写。 */
141
+ function exactCount(value) {
142
+ return formatCredits(value);
143
+ }
131
144
  /** 批量领取的 claimingId 哨兵(真实账号 id 不会等于它)。 */
132
145
  const CLAIM_ALL = "*";
133
146
  /** 一次领取操作的文案:优先用后端逐号结果,老后端只回一条时按 id 归位。 */
@@ -358,12 +371,25 @@ function ManualTokenForm({ t, token, label, busy, onToken, onLabel, onSubmit })
358
371
  });
359
372
  }
360
373
  /**
374
+ * 缓存命中率:缓存输入 / 全部输入,钳在 0~100。
375
+ *
376
+ * 上游没给缓存字段、或今天压根没有输入时返回 undefined —— 统计块里显示「—」,
377
+ * 不要把「没有数据」写成「0%」。
378
+ */
379
+ function hitPercentOf(totals) {
380
+ const cached = totals.cachedTokens ?? 0;
381
+ if (cached <= 0 || totals.promptTokens <= 0) return void 0;
382
+ return Math.min(100, Math.round(cached / totals.promptTokens * 100));
383
+ }
384
+ /**
361
385
  * 横向条形列表:名称 / 条 / token / 占比 / 请求数。
362
386
  *
363
387
  * 条长按「占组内总量的比例」画(不是按最大值归一化),所以几条加起来就是全部。
364
388
  * token 全 0 的日子(只有请求数、没收到 usage 帧)退回按请求数画。
365
389
  */
366
390
  function UsageBars({ rows, t }) {
391
+ /** 当前悬停的那一行;卡片浮层只在它上面出现。 */
392
+ const [hovered, setHovered] = (0, react.useState)(null);
367
393
  const tokenMode = rows.some((row) => row.totals.totalTokens > 0);
368
394
  const valueOf = (row) => tokenMode ? row.totals.totalTokens : row.totals.requests;
369
395
  const sum = rows.reduce((total, row) => total + valueOf(row), 0);
@@ -374,13 +400,18 @@ function UsageBars({ rows, t }) {
374
400
  children: top.map((row) => {
375
401
  const value = valueOf(row);
376
402
  const share = sum > 0 ? value / sum * 100 : 0;
377
- const detail = [t("usageRequests", { n: formatCount(row.totals.requests) }), t("usageTokens", {
378
- prompt: formatTokens$1(row.totals.promptTokens),
379
- completion: formatTokens$1(row.totals.completionTokens)
403
+ const totals = row.totals;
404
+ const cached = totals.cachedTokens;
405
+ const pct = hitPercentOf(totals);
406
+ const detail = [t("usageRequests", { n: formatCount(totals.requests) }), t("usageTokens", {
407
+ prompt: formatTokens$1(totals.promptTokens),
408
+ completion: formatTokens$1(totals.completionTokens)
380
409
  })].join(" · ");
381
410
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
382
411
  className: CodeArtsPluginCard_module_css_default.modelRow,
383
412
  title: `${row.label} · ${detail}`,
413
+ onMouseEnter: () => setHovered(row.id),
414
+ onMouseLeave: () => setHovered((prev) => prev === row.id ? null : prev),
384
415
  children: [
385
416
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
386
417
  className: CodeArtsPluginCard_module_css_default.modelName,
@@ -403,7 +434,43 @@ function UsageBars({ rows, t }) {
403
434
  }),
404
435
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
405
436
  className: CodeArtsPluginCard_module_css_default.modelRequests,
406
- children: t("usageRequestCount", { n: formatCount(row.totals.requests) })
437
+ children: t("usageRequestCount", { n: formatCount(totals.requests) })
438
+ }),
439
+ hovered === row.id && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
440
+ className: CodeArtsPluginCard_module_css_default.usageTip,
441
+ role: "tooltip",
442
+ children: [
443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
444
+ className: CodeArtsPluginCard_module_css_default.usageTipName,
445
+ children: row.label
446
+ }),
447
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: CodeArtsPluginCard_module_css_default.usageTipSep }),
448
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
449
+ className: CodeArtsPluginCard_module_css_default.usageTipRow,
450
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageTipInput") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: exactCount(totals.promptTokens) })]
451
+ }),
452
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
453
+ className: CodeArtsPluginCard_module_css_default.usageTipRow,
454
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageTipOutput") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: exactCount(totals.completionTokens) })]
455
+ }),
456
+ (totals.reasoningTokens ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
457
+ className: CodeArtsPluginCard_module_css_default.usageTipRow,
458
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageTipReasoning") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: exactCount(totals.reasoningTokens ?? 0) })]
459
+ }),
460
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
461
+ className: CodeArtsPluginCard_module_css_default.usageTipRow,
462
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageTipCached") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: cached === void 0 ? t("usageTipNoCache") : exactCount(cached) })]
463
+ }),
464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: CodeArtsPluginCard_module_css_default.usageTipSep }),
465
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
466
+ className: CodeArtsPluginCard_module_css_default.usageTipRow,
467
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageTipRequests") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageRequestCount", { n: formatCount(totals.requests) }) })]
468
+ }),
469
+ pct !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
470
+ className: CodeArtsPluginCard_module_css_default.usageTipRow,
471
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageTipHit") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [pct, "%"] })]
472
+ })
473
+ ]
407
474
  })
408
475
  ]
409
476
  }, row.id);
@@ -418,7 +485,6 @@ function UsageBars({ rows, t }) {
418
485
  * 「打断后重新提问要等」的原因。
419
486
  */
420
487
  function UsageSection({ usage, accounts, t }) {
421
- const total = usage.today.requests;
422
488
  const stateLabel = {
423
489
  ok: t("usageStateOk"),
424
490
  capped: t("usageStateCapped"),
@@ -436,7 +502,9 @@ function UsageSection({ usage, accounts, t }) {
436
502
  label: entry.id.split(" · ")[0],
437
503
  totals: entry.totals
438
504
  }));
439
- const hasUsage = usage.allTime.requests > 0 || total > 0;
505
+ const today = usage.today;
506
+ const hasUsage = usage.allTime.requests > 0 || today.requests > 0;
507
+ const hit = hitPercentOf(today);
440
508
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
441
509
  className: CodeArtsPluginCard_module_css_default.section,
442
510
  children: [
@@ -447,7 +515,10 @@ function UsageSection({ usage, accounts, t }) {
447
515
  children: t("usageTitle")
448
516
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
449
517
  className: CodeArtsPluginCard_module_css_default.count,
450
- children: t("usageRequests", { n: formatCount(total) })
518
+ children: t("usageRange", {
519
+ today: formatCount(today.requests),
520
+ last30: formatCount(usage.last30.requests)
521
+ })
451
522
  })]
452
523
  }),
453
524
  !hasUsage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -455,38 +526,46 @@ function UsageSection({ usage, accounts, t }) {
455
526
  children: t("usageEmpty")
456
527
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
457
528
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
458
- className: CodeArtsPluginCard_module_css_default.choiceRow,
459
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
460
- className: CodeArtsPluginCard_module_css_default.name,
461
- children: t("usageTodayTotal")
462
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
463
- className: CodeArtsPluginCard_module_css_default.meta,
464
- children: [
465
- t("usageRequests", { n: formatCount(total) }),
466
- " · ",
467
- t("usageTokens", {
468
- prompt: formatTokens$1(usage.today.promptTokens),
469
- completion: formatTokens$1(usage.today.completionTokens)
470
- })
471
- ]
472
- })]
473
- }),
474
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
475
- className: CodeArtsPluginCard_module_css_default.choiceRow,
476
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
477
- className: CodeArtsPluginCard_module_css_default.name,
478
- children: t("usageLast30")
479
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
480
- className: CodeArtsPluginCard_module_css_default.meta,
481
- children: [
482
- t("usageRequests", { n: formatCount(usage.last30.requests) }),
483
- " · ",
484
- t("usageTokens", {
485
- prompt: formatTokens$1(usage.last30.promptTokens),
486
- completion: formatTokens$1(usage.last30.completionTokens)
487
- })
488
- ]
489
- })]
529
+ className: CodeArtsPluginCard_module_css_default.statGrid,
530
+ children: [
531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
532
+ className: CodeArtsPluginCard_module_css_default.statCard,
533
+ children: [
534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
535
+ className: CodeArtsPluginCard_module_css_default.statLabel,
536
+ children: t("usageStatTokens")
537
+ }),
538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
539
+ className: CodeArtsPluginCard_module_css_default.statValue,
540
+ children: formatTokens$1(today.totalTokens)
541
+ }),
542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
543
+ className: CodeArtsPluginCard_module_css_default.statSub,
544
+ children: [t("usageStatIn", { n: formatTokens$1(today.promptTokens) }), (today.reasoningTokens ?? 0) > 0 ? ` · ${t("usageStatThinking", { n: formatTokens$1(today.reasoningTokens ?? 0) })}` : ""]
545
+ })
546
+ ]
547
+ }),
548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
549
+ className: CodeArtsPluginCard_module_css_default.statCard,
550
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
551
+ className: CodeArtsPluginCard_module_css_default.statLabel,
552
+ children: t("usageStatHit")
553
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
554
+ className: CodeArtsPluginCard_module_css_default.statValue,
555
+ children: hit === void 0 ? "—" : `${hit}%`
556
+ })]
557
+ }),
558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
559
+ className: CodeArtsPluginCard_module_css_default.statCard,
560
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
561
+ className: CodeArtsPluginCard_module_css_default.statLabel,
562
+ children: t("usageStatRequests")
563
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
564
+ className: CodeArtsPluginCard_module_css_default.statValue,
565
+ children: formatCount(today.requests)
566
+ })]
567
+ })
568
+ ]
490
569
  }),
491
570
  accounts.length > 1 && accountRows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
492
571
  className: CodeArtsPluginCard_module_css_default.hint,
@@ -1494,10 +1573,27 @@ const QUEUE_PATH = "/api/codearts/queue";
1494
1573
  const REFRESH_INTERVAL_MS = 6e4;
1495
1574
  /** 排队位置变得快:官方客户端也是 5 秒一次;这是本机路由,打不疼上游。 */
1496
1575
  const QUEUE_INTERVAL_MS = 5e3;
1497
- /** 弹层宽度固定,好算它会不会顶出屏幕右边。 */
1498
- const PANEL_WIDTH = 280;
1576
+ /**
1577
+ * 弹层宽度:只放「账号 + 积分」两列,所以按同类插件(dsh-connect-trae 的
1578
+ * `min(248px, …)`)取一档窄宽度 —— 这一层回答的是「哪个号还有多少积分」,
1579
+ * 用量数字不该把它撑成一张报表。
1580
+ */
1581
+ const PANEL_WIDTH = 260;
1499
1582
  /** 弹层与那一行之间的缝隙。 */
1500
1583
  const PANEL_GAP = 8;
1584
+ /**
1585
+ * 弹层里每行的列模板:账号 | 积分。
1586
+ *
1587
+ * 数字列用 `auto`:它跟着最宽的那个余额自适应,面板不必为「12,600」预留固定宽度。
1588
+ * 输入 / 输出 / 缓存这些用量数字不进表格,收进整行的悬浮提示({@link usageLine}),
1589
+ * 完整明细在设置卡片里看。
1590
+ */
1591
+ const PANEL_ROW_COLUMNS = "minmax(0, 1fr) auto";
1592
+ /** 数字格统一右对齐 + 等宽数字,列才对得齐。 */
1593
+ const PANEL_NUM_CELL = {
1594
+ justifySelf: "end",
1595
+ fontVariantNumeric: "tabular-nums"
1596
+ };
1501
1597
  /** 那一行在输入框下方,往上开才够空间;给弹层留的最小高度,不够就翻到下面。 */
1502
1598
  const PANEL_MIN_ABOVE = 260;
1503
1599
  function readSelection(useProjection) {
@@ -1535,6 +1631,24 @@ function formatClock(ts) {
1535
1631
  return "";
1536
1632
  }
1537
1633
  }
1634
+ /**
1635
+ * 弹层里那行用量:`入 12.3k · 出 697(思 505)· 命中 94% · 37 条`。
1636
+ *
1637
+ * 刻意比设置卡片里的完整版短:面板只有 260px,把「缓存 11.5k」也列上就会折成
1638
+ * 两三行,比原来那个悬浮提示还难看。缓存值本身由命中率表达(它是输入的占比),
1639
+ * 思考 token 也不在这里——它是输出的子集,放在设置卡片的统计块里。上游不给缓存
1640
+ * 字段就不显示命中率:显示成 0% 会被当成「没命中」而不是「没数据」。
1641
+ */
1642
+ function usageLine(usage, t) {
1643
+ const cached = usage.cachedTokens ?? 0;
1644
+ const parts = [t("usageCompact", {
1645
+ prompt: formatTokens(usage.promptTokens),
1646
+ completion: formatTokens(usage.completionTokens)
1647
+ })];
1648
+ if (cached > 0 && usage.promptTokens > 0) parts.push(t("usageCacheHit", { pct: Math.min(100, Math.round(cached / usage.promptTokens * 100)) }));
1649
+ parts.push(t("usageRequestCount", { n: formatNumber(usage.requests) }));
1650
+ return parts.join(" · ");
1651
+ }
1538
1652
  /** 账号名与设置卡片同一套取法:备注 > 用户名 > id。 */
1539
1653
  function accountName(account) {
1540
1654
  return account.label?.trim() || account.userName?.trim() || account.id;
@@ -1566,21 +1680,20 @@ function rowsOf(data) {
1566
1680
  };
1567
1681
  });
1568
1682
  }
1569
- /** 把弹层钉在按钮边上:优先往上开(那一行本来就在输入框下面),放不下就翻到下面。 */
1570
- function anchorOf(rect) {
1571
- const above = rect.top > PANEL_MIN_ABOVE;
1572
- const width = Math.min(PANEL_WIDTH, window.innerWidth - 16);
1573
- return {
1574
- left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)),
1575
- top: above ? rect.top - PANEL_GAP : rect.bottom + PANEL_GAP,
1576
- above
1577
- };
1683
+ /**
1684
+ * 弹层往上开还是往下开。
1685
+ *
1686
+ * 位置不在这里算:fixed 元素遇到带 transform / backdrop-filter 的祖先时,坐标会
1687
+ * 相对那个祖先而不是视口(工具行就长在输入框卡片里),所以真实偏移要渲染后实测
1688
+ * (见组件里的 layout effect),这里只决定朝哪边翻。
1689
+ */
1690
+ function placeAbove(rect) {
1691
+ return rect.top > PANEL_MIN_ABOVE;
1578
1692
  }
1579
- function CodeArtsCreditDock({ useProjection, t }) {
1693
+ function CodeArtsCreditDock({ useProjection, t, zone = "dock" }) {
1580
1694
  const selection = readSelection(useProjection);
1581
1695
  const active = isCodeArts(selection);
1582
1696
  const [remain, setRemain] = (0, react.useState)(null);
1583
- const [total, setTotal] = (0, react.useState)(0);
1584
1697
  const [rows, setRows] = (0, react.useState)([]);
1585
1698
  /**
1586
1699
  * 模型 id -> 目录条目(显示名 + 已格式化的倍率)。
@@ -1595,6 +1708,15 @@ function CodeArtsCreditDock({ useProjection, t }) {
1595
1708
  });
1596
1709
  const [queued, setQueued] = (0, react.useState)(null);
1597
1710
  const [anchor, setAnchor] = (0, react.useState)(null);
1711
+ /**
1712
+ * 弹层的包含块原点(视口坐标)。
1713
+ *
1714
+ * fixed 只有在祖先没有 transform / backdrop-filter 时才是视口坐标;工具行长在输入框
1715
+ * 卡片里,一旦撞上这类祖先,left/top 就会相对那个祖先算,弹层会飞到窗口外。做法是
1716
+ * 先按 0,0 渲染但不可见,量出面板实际落在哪(就是包含块原点),再把目标坐标减掉它;
1717
+ * 视口情形下量到 (0,0),等于没动。
1718
+ */
1719
+ const [origin, setOrigin] = (0, react.useState)(null);
1598
1720
  const [refreshing, setRefreshing] = (0, react.useState)(false);
1599
1721
  const mounted = (0, react.useRef)(true);
1600
1722
  const triggerRef = (0, react.useRef)(null);
@@ -1632,7 +1754,6 @@ function CodeArtsCreditDock({ useProjection, t }) {
1632
1754
  return;
1633
1755
  }
1634
1756
  setRemain(Math.round(pool.reduce((sum, item) => sum + item.credits.total.remain, 0)));
1635
- setTotal(Math.round(pool.reduce((sum, item) => sum + item.credits.total.total, 0)));
1636
1757
  } catch {}
1637
1758
  }, []);
1638
1759
  (0, react.useEffect)(() => {
@@ -1695,6 +1816,19 @@ function CodeArtsCreditDock({ useProjection, t }) {
1695
1816
  window.removeEventListener("scroll", close, true);
1696
1817
  };
1697
1818
  }, [anchor, close]);
1819
+ (0, react.useLayoutEffect)(() => {
1820
+ if (anchor === null) {
1821
+ setOrigin(null);
1822
+ return;
1823
+ }
1824
+ const el = panelRef.current;
1825
+ if (el === null) return;
1826
+ const rect = el.getBoundingClientRect();
1827
+ setOrigin((prev) => prev !== null && Math.abs(prev.x - rect.left) < .5 && Math.abs(prev.y - rect.top) < .5 ? prev : {
1828
+ x: rect.left,
1829
+ y: rect.top
1830
+ });
1831
+ }, [anchor]);
1698
1832
  (0, react.useEffect)(() => {
1699
1833
  if (!active) return;
1700
1834
  const self = rootRef.current;
@@ -1719,26 +1853,18 @@ function CodeArtsCreditDock({ useProjection, t }) {
1719
1853
  if (!active) return null;
1720
1854
  const showCredits = flags.credits && remain !== null;
1721
1855
  const showQueue = flags.queue && queued !== null;
1856
+ if (zone === "bar" && !showCredits) return null;
1722
1857
  const modelId = currentOf(selection)?.model;
1723
1858
  const entry = modelId === void 0 ? void 0 : modelInfo[modelId];
1724
1859
  const modelLabel = entry === void 0 ? modelId ?? "" : stripModelSuffixes(entry.name);
1725
1860
  const ratio = entry?.ratio;
1726
1861
  const line = buildDockLine({
1727
- ...showCredits ? { credits: t("creditsDock", { remain: formatNumber(remain ?? 0) }) } : {},
1862
+ ...zone === "bar" && showCredits ? { credits: t("creditsDock", { remain: formatNumber(remain ?? 0) }) } : {},
1728
1863
  provider: "CodeArts",
1729
1864
  ...modelLabel === "" ? {} : { model: modelLabel },
1730
1865
  ...ratio === void 0 ? {} : { ratio },
1731
- ...showQueue && queued !== null ? { queue: t("queueDock", { position: queued.position }) } : {}
1866
+ ...zone === "dock" && showQueue && queued !== null ? { queue: t("queueDock", { position: queued.position }) } : {}
1732
1867
  }, t);
1733
- const queueTitle = queued === null ? void 0 : t("queueDockTitle", {
1734
- model: queued.model,
1735
- position: queued.position,
1736
- length: queued.length
1737
- });
1738
- const creditsTitle = showCredits ? t("creditsDockTitle", {
1739
- remain: remain ?? 0,
1740
- total
1741
- }) : void 0;
1742
1868
  const fetchedAt = rows.reduce((latest, row) => Math.max(latest, row.fetchedAt ?? 0), 0);
1743
1869
  const lastUpdated = fetchedAt > 0 ? formatClock(fetchedAt) : "";
1744
1870
  const toggle = (event) => {
@@ -1746,7 +1872,13 @@ function CodeArtsCreditDock({ useProjection, t }) {
1746
1872
  close();
1747
1873
  return;
1748
1874
  }
1749
- setAnchor(anchorOf(event.currentTarget.getBoundingClientRect()));
1875
+ const rect = event.currentTarget.getBoundingClientRect();
1876
+ const above = placeAbove(rect);
1877
+ setAnchor({
1878
+ left: rect.left,
1879
+ top: above ? rect.top - PANEL_GAP : rect.bottom + PANEL_GAP,
1880
+ above
1881
+ });
1750
1882
  };
1751
1883
  const forceRefresh = async () => {
1752
1884
  setRefreshing(true);
@@ -1770,7 +1902,6 @@ function CodeArtsCreditDock({ useProjection, t }) {
1770
1902
  flex: "0 0 auto",
1771
1903
  minWidth: "max-content"
1772
1904
  },
1773
- title: queueTitle ?? creditsTitle,
1774
1905
  children: [
1775
1906
  line.credits !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1776
1907
  ref: triggerRef,
@@ -1778,7 +1909,6 @@ function CodeArtsCreditDock({ useProjection, t }) {
1778
1909
  "aria-haspopup": "dialog",
1779
1910
  "aria-expanded": anchor !== null,
1780
1911
  "aria-label": t("creditsDockPanelTitle"),
1781
- title: creditsTitle,
1782
1912
  onClick: toggle,
1783
1913
  style: {
1784
1914
  all: "unset",
@@ -1790,20 +1920,21 @@ function CodeArtsCreditDock({ useProjection, t }) {
1790
1920
  whiteSpace: "inherit"
1791
1921
  },
1792
1922
  children: line.credits
1793
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1923
+ }), zone === "dock" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1794
1924
  "aria-hidden": "true",
1795
1925
  children: " · "
1796
1926
  })] }),
1797
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: line.rest }),
1927
+ zone === "dock" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: line.rest }),
1798
1928
  anchor !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1799
1929
  ref: panelRef,
1800
1930
  role: "dialog",
1801
1931
  "aria-label": t("creditsDockPanelTitle"),
1802
1932
  style: {
1803
1933
  position: "fixed",
1804
- left: anchor.left,
1805
- top: anchor.top,
1806
- transform: anchor.above ? "translateY(-100%)" : void 0,
1934
+ left: origin === null ? 0 : anchor.left - origin.x,
1935
+ top: origin === null ? 0 : anchor.top - origin.y,
1936
+ transform: anchor.above && origin !== null ? "translateY(-100%)" : void 0,
1937
+ visibility: origin === null ? "hidden" : "visible",
1807
1938
  width: PANEL_WIDTH,
1808
1939
  maxWidth: "calc(100vw - 16px)",
1809
1940
  zIndex: 1e3,
@@ -1842,73 +1973,60 @@ function CodeArtsCreditDock({ useProjection, t }) {
1842
1973
  }),
1843
1974
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1844
1975
  style: {
1845
- display: "grid",
1846
- gridTemplateColumns: "minmax(0, 1fr) auto auto",
1847
- gap: "4px 12px",
1848
- alignItems: "center",
1976
+ display: "flex",
1977
+ flexDirection: "column",
1978
+ gap: 10,
1849
1979
  borderTop: "0.5px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.22))",
1850
1980
  paddingTop: 8,
1981
+ fontSize: 12,
1851
1982
  color: "var(--dsw-alias-label-tertiary)"
1852
1983
  },
1853
- children: [
1854
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("creditsDockColAccount") }),
1855
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1856
- style: { justifySelf: "end" },
1857
- children: t("creditsDockColTokens")
1858
- }),
1859
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1860
- style: { justifySelf: "end" },
1861
- children: t("creditsDockColBalance")
1862
- }),
1863
- rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1864
- style: { gridColumn: "1 / -1" },
1865
- children: t("creditsDockEmpty")
1866
- }),
1867
- rows.map((row) => {
1868
- const detail = row.usage === void 0 ? "" : [t("usageTokens", {
1869
- prompt: formatTokens(row.usage.promptTokens),
1870
- completion: formatTokens(row.usage.completionTokens)
1871
- }), t("usageRequestCount", { n: formatNumber(row.usage.requests) })].join(" · ");
1872
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1873
- style: { display: "contents" },
1874
- children: [
1875
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1876
- style: {
1877
- display: "flex",
1878
- alignItems: "center",
1879
- gap: 6,
1880
- minWidth: 0
1881
- },
1882
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: row.remain === null ? "warning" : "done" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1883
- style: {
1884
- overflow: "hidden",
1885
- textOverflow: "ellipsis",
1886
- whiteSpace: "nowrap",
1887
- color: "var(--dsw-alias-label-primary)"
1888
- },
1889
- title: row.error === void 0 ? row.name : row.error,
1890
- children: row.name
1891
- })]
1892
- }),
1893
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1894
- style: {
1895
- justifySelf: "end",
1896
- fontVariantNumeric: "tabular-nums"
1897
- },
1898
- title: row.usage === void 0 ? t("creditsDockNoTokens") : detail,
1899
- children: row.usage === void 0 ? "—" : formatTokens(row.usage.totalTokens)
1900
- }),
1901
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1902
- style: {
1903
- justifySelf: "end",
1904
- fontVariantNumeric: "tabular-nums"
1905
- },
1906
- children: row.remain === null ? "—" : formatNumber(row.remain)
1907
- })
1908
- ]
1909
- }, row.id);
1910
- })
1911
- ]
1984
+ children: [rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("creditsDockEmpty") }), rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1985
+ style: {
1986
+ display: "flex",
1987
+ flexDirection: "column",
1988
+ gap: 2,
1989
+ minWidth: 0
1990
+ },
1991
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1992
+ style: {
1993
+ display: "grid",
1994
+ gridTemplateColumns: PANEL_ROW_COLUMNS,
1995
+ gap: "0 8px",
1996
+ alignItems: "center",
1997
+ minWidth: 0
1998
+ },
1999
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2000
+ style: {
2001
+ display: "flex",
2002
+ alignItems: "center",
2003
+ gap: 5,
2004
+ minWidth: 0
2005
+ },
2006
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: row.remain === null ? "warning" : "done" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2007
+ style: {
2008
+ overflow: "hidden",
2009
+ textOverflow: "ellipsis",
2010
+ whiteSpace: "nowrap",
2011
+ color: "var(--dsw-alias-label-primary)"
2012
+ },
2013
+ title: row.error === void 0 ? row.name : row.error,
2014
+ children: row.name
2015
+ })]
2016
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2017
+ style: PANEL_NUM_CELL,
2018
+ children: row.remain === null ? "—" : formatNumber(row.remain)
2019
+ })]
2020
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2021
+ style: {
2022
+ paddingLeft: 11,
2023
+ fontSize: 11,
2024
+ lineHeight: "15px",
2025
+ color: "var(--dsw-alias-label-tertiary)"
2026
+ },
2027
+ children: row.usage === void 0 ? t("creditsDockNoTokens") : `${t("usageToday")} ${usageLine(row.usage, t)}`
2028
+ })]
2029
+ }, row.id))]
1912
2030
  }),
1913
2031
  rows.filter((row) => row.expiring > 0).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1914
2032
  style: {
@@ -1993,12 +2111,25 @@ const zh = {
1993
2111
  modelsLoading: "模型勾选区加载中…(若长期不出现,请确认已登录且 DSH 已完全重启)",
1994
2112
  usageTitle: "本机用量",
1995
2113
  usageToday: "今日",
1996
- usageTodayTotal: "今日合计",
1997
- usageLast30: "近 30 天",
1998
- usageByAccount: "今日按账号",
2114
+ usageRange: "今日 {today} 条 · 近 30 天 {last30} 条",
2115
+ usageStatTokens: "今日 token",
2116
+ usageStatIn: "输入 {n}",
2117
+ usageStatThinking: "思考 {n}",
2118
+ usageTipInput: "输入",
2119
+ usageTipOutput: "输出",
2120
+ usageTipReasoning: "思考",
2121
+ usageTipCached: "缓存命中",
2122
+ usageTipNoCache: "上游未下发",
2123
+ usageTipRequests: "请求",
2124
+ usageTipHit: "命中率",
2125
+ usageStatHit: "缓存命中",
2126
+ usageStatRequests: "今日请求",
1999
2127
  usageRequests: "{n} 次请求",
2128
+ usageByAccount: "今日按账号",
2000
2129
  usageRequestCount: "{n} 条",
2001
2130
  usageTokens: "输入 {prompt} · 输出 {completion}",
2131
+ usageCompact: "入 {prompt} · 出 {completion}",
2132
+ usageCacheHit: "命中 {pct}%",
2002
2133
  usageAllTime: "累计({days} 天)",
2003
2134
  usageByModel: "今日按模型",
2004
2135
  usageSlots: "会话槽位",
@@ -2025,9 +2156,6 @@ const zh = {
2025
2156
  creditsDockPanelTitle: "CodeArts 积分",
2026
2157
  creditsDockRefresh: "刷新积分",
2027
2158
  creditsDockRefreshing: "刷新中…",
2028
- creditsDockColAccount: "账号",
2029
- creditsDockColBalance: "剩余积分",
2030
- creditsDockColTokens: "今日 Token",
2031
2159
  creditsDockNoTokens: "今天还没有经插件发出的请求",
2032
2160
  creditsDockEmpty: "暂时没有积分数据",
2033
2161
  creditsDockExpiring: "{account}:{expiring} 积分将在本周期结束时作废",
@@ -2036,9 +2164,9 @@ const zh = {
2036
2164
  dockModel: "Model {model}",
2037
2165
  dockNoModel: "未选择模型",
2038
2166
  tabDisplay: "显示",
2039
- displayTitle: "对话底部显示",
2167
+ displayTitle: "输入框两侧显示",
2040
2168
  displayHint: "这两项只管输入框下方那一行的积分与排队两段;中间那段「Provider CodeArts · Model 模型 · 倍率」一直显示,不受开关影响。底部那条每 60 秒读一次设置,所以关掉最多要等一分钟才见效。",
2041
- displayCreditsToggle: "显示剩余积分(点开可看各账号明细)",
2169
+ displayCreditsToggle: "在输入框工具行显示剩余积分(点开可看各账号明细)",
2042
2170
  displayQueueToggle: "显示上游排队位置",
2043
2171
  displayNotWritable: "这个宿主没给插件开配置存储,开关只能看不能改。",
2044
2172
  displayFailed: "保存失败:{message}",
@@ -2116,12 +2244,25 @@ const en = {
2116
2244
  modelsLoading: "Loading model selection… (if it never appears, make sure you are signed in and DSH was fully restarted)",
2117
2245
  usageTitle: "Local usage",
2118
2246
  usageToday: "Today",
2119
- usageTodayTotal: "Today",
2120
- usageLast30: "Last 30 days",
2121
- usageByAccount: "Today by account",
2247
+ usageRange: "Today {today} · last 30 days {last30}",
2248
+ usageStatTokens: "Today tokens",
2249
+ usageStatIn: "in {n}",
2250
+ usageStatThinking: "thinking {n}",
2251
+ usageTipInput: "Input",
2252
+ usageTipOutput: "Output",
2253
+ usageTipReasoning: "Reasoning",
2254
+ usageTipCached: "Cached",
2255
+ usageTipNoCache: "not reported",
2256
+ usageTipRequests: "Requests",
2257
+ usageTipHit: "Hit rate",
2258
+ usageStatHit: "Cache hit",
2259
+ usageStatRequests: "Today requests",
2122
2260
  usageRequests: "{n} request(s)",
2261
+ usageByAccount: "Today by account",
2123
2262
  usageRequestCount: "{n} reqs",
2124
2263
  usageTokens: "in {prompt} · out {completion}",
2264
+ usageCompact: "in {prompt} · out {completion}",
2265
+ usageCacheHit: "{pct}% cached",
2125
2266
  usageAllTime: "All time ({days} days)",
2126
2267
  usageByModel: "Today by model",
2127
2268
  usageSlots: "Session slots",
@@ -2148,9 +2289,6 @@ const en = {
2148
2289
  creditsDockPanelTitle: "CodeArts credits",
2149
2290
  creditsDockRefresh: "Refresh credits",
2150
2291
  creditsDockRefreshing: "Refreshing…",
2151
- creditsDockColAccount: "Account",
2152
- creditsDockColBalance: "Credits left",
2153
- creditsDockColTokens: "Tokens today",
2154
2292
  creditsDockNoTokens: "No requests through the plugin today yet",
2155
2293
  creditsDockEmpty: "No credit data yet",
2156
2294
  creditsDockExpiring: "{account}: {expiring} credits expire when this cycle ends",
@@ -2159,9 +2297,9 @@ const en = {
2159
2297
  dockModel: "Model {model}",
2160
2298
  dockNoModel: "No model selected",
2161
2299
  tabDisplay: "Display",
2162
- displayTitle: "Under the chat input box",
2300
+ displayTitle: "Around the chat input box",
2163
2301
  displayHint: "These two only control the credits and queue segments of that line under the input box; the middle “Provider CodeArts · Model … · rate” segments always show. The bar re-reads settings every 60 seconds, so hiding the rest can take up to a minute.",
2164
- displayCreditsToggle: "Show remaining credits (click it for per-account details)",
2302
+ displayCreditsToggle: "Show remaining credits in the composer tool row (click for per-account details)",
2165
2303
  displayQueueToggle: "Show the upstream queue position",
2166
2304
  displayNotWritable: "This host gives the plugin nowhere to persist settings, so the switches are read-only.",
2167
2305
  displayFailed: "Failed to save: {message}",
@@ -2194,12 +2332,25 @@ function apply(ctx) {
2194
2332
  key: "dsh-codearts",
2195
2333
  locale: namespace
2196
2334
  }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeArtsPluginCard, { t })));
2335
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
2336
+ name: "conversation.input.left",
2337
+ id: "codearts-credits-bar",
2338
+ order: 20,
2339
+ locale: namespace,
2340
+ inject: () => ({
2341
+ t,
2342
+ zone: "bar"
2343
+ })
2344
+ }, CodeArtsCreditDock));
2197
2345
  ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
2198
2346
  name: "conversation.composer.dock",
2199
2347
  id: "codearts-credits",
2200
2348
  order: 20,
2201
2349
  locale: namespace,
2202
- inject: () => ({ t })
2350
+ inject: () => ({
2351
+ t,
2352
+ zone: "dock"
2353
+ })
2203
2354
  }, CodeArtsCreditDock));
2204
2355
  } catch (error) {
2205
2356
  console.error("[dsh-codearts] client card failed to load (host provider unaffected):", error);
package/lib/index.d.mts CHANGED
@@ -1296,12 +1296,40 @@ interface UsageTotals {
1296
1296
  promptTokens: number;
1297
1297
  completionTokens: number;
1298
1298
  totalTokens: number;
1299
+ /**
1300
+ * 命中上游 prompt 缓存的输入 token(OpenAI 兼容口径)。
1301
+ *
1302
+ * 上游若在 usage 帧里带 `prompt_tokens_details.cached_tokens`(也有平铺成
1303
+ * `cached_tokens` / `cache_tokens` / `prompt_cache_hit_tokens` 的),这里才可能
1304
+ * 是正数;不给就恒为 0。UI 据此决定显不显示「缓存」那一段 —— 不臆造。
1305
+ * 它是 `promptTokens` 的**子集**,不是额外的输入量。
1306
+ */
1307
+ cachedTokens: number;
1308
+ /**
1309
+ * 其中属于「思考」的输出 token(OpenAI 兼容口径:`completion_tokens_details.reasoning_tokens`,
1310
+ * 平铺成顶层 `reasoning_tokens` 的也认)。
1311
+ *
1312
+ * 推理模型(deepseek-v4.1-flash 这类)会把它单列出来,通常照样计费;不单独记就
1313
+ * 只能看见一个偏大的「输出」。它是 `completionTokens` 的**子集**,不是额外的输出量。
1314
+ * 上游不给就恒为 0。
1315
+ */
1316
+ reasoningTokens: number;
1299
1317
  }
1300
1318
  /** 上游 usage 帧里我们关心的字段。 */
1301
1319
  interface RawUsage {
1302
1320
  prompt_tokens?: unknown;
1303
1321
  completion_tokens?: unknown;
1304
1322
  total_tokens?: unknown;
1323
+ /** OpenAI 兼容的嵌套写法:`prompt_tokens_details: { cached_tokens }`。 */
1324
+ prompt_tokens_details?: unknown;
1325
+ /** 平铺写法,几家兼容层各用各的名字,都认。 */
1326
+ cached_tokens?: unknown;
1327
+ cache_tokens?: unknown;
1328
+ prompt_cache_hit_tokens?: unknown;
1329
+ /** 推理模型的嵌套写法:`completion_tokens_details: { reasoning_tokens }`。 */
1330
+ completion_tokens_details?: unknown;
1331
+ /** 平铺的思考 token 别名。 */
1332
+ reasoning_tokens?: unknown;
1305
1333
  }
1306
1334
  interface UsageSnapshot {
1307
1335
  /** 今天的合计。 */
package/lib/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as upstreamErrorMessage, B as modelUnavailableReason, C as isWelfareClaimedStatus, D as prepareChatBody, E as pickDailyWelfareCampaign, F as filterEnabledModels, G as codeartsAuthPath, H as CodeArtsCredentialStore, I as forgetModelUnavailable, K as startLogin, L as isBenefitBlocked, M as FALLBACK_CODEARTS_MODELS, N as clearBenefitBlocked, O as rateLimitFrame, P as creditRatioLabel, R as isBenefitModel, S as isTpmLimit, T as modelUnavailableReasonText, U as MANUAL_ACCOUNT_ID, V as CodeArtsAccountStore, W as codeartsAccountsPath, _ as isIamTokenError, a as SNAP_HOST, b as isParamInvalidFrame, c as claimDailyWelfare, d as fetchCallerIdentity, f as fetchModels, g as isConcurrencyCap, h as firstFrameError, i as refreshCatalog, j as CodeArtsCatalog, k as signedGet, l as clearQueueStatus, m as fetchWelfareCampaigns, n as codeartsUsagePath, o as canonicalModel, p as fetchQueueStatus, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as degradeTokenCeiling, v as isModelNotAccessible, w as maxTokensCeilingFor, x as isQueueableRate, y as isNotMultimodal, z as markModelUnavailable } from "./usage-CzJjkqOp.mjs";
1
+ import { A as upstreamErrorMessage, B as modelUnavailableReason, C as isWelfareClaimedStatus, D as prepareChatBody, E as pickDailyWelfareCampaign, F as filterEnabledModels, G as codeartsAuthPath, H as CodeArtsCredentialStore, I as forgetModelUnavailable, K as startLogin, L as isBenefitBlocked, M as FALLBACK_CODEARTS_MODELS, N as clearBenefitBlocked, O as rateLimitFrame, P as creditRatioLabel, R as isBenefitModel, S as isTpmLimit, T as modelUnavailableReasonText, U as MANUAL_ACCOUNT_ID, V as CodeArtsAccountStore, W as codeartsAccountsPath, _ as isIamTokenError, a as SNAP_HOST, b as isParamInvalidFrame, c as claimDailyWelfare, d as fetchCallerIdentity, f as fetchModels, g as isConcurrencyCap, h as firstFrameError, i as refreshCatalog, j as CodeArtsCatalog, k as signedGet, l as clearQueueStatus, m as fetchWelfareCampaigns, n as codeartsUsagePath, o as canonicalModel, p as fetchQueueStatus, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as degradeTokenCeiling, v as isModelNotAccessible, w as maxTokensCeilingFor, x as isQueueableRate, y as isNotMultimodal, z as markModelUnavailable } from "./usage-DGIuT79_.mjs";
2
2
  import { i as lazyApi, r as createProvider } from "./models-CgKCibpk.mjs";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { randomBytes, timingSafeEqual } from "node:crypto";
@@ -962,6 +962,13 @@ const recentShapes = [];
962
962
  function recentRequestShapes() {
963
963
  return [...recentShapes].reverse();
964
964
  }
965
+ const recentUsages = [];
966
+ /** 探针只留最近 5 帧:够看形状,又不至于把内存堆成日志。 */
967
+ const USAGE_PROBE_LIMIT = 5;
968
+ /** 最近的 usage 原帧,最新在前。 */
969
+ function recentUsageShapes() {
970
+ return [...recentUsages].reverse();
971
+ }
965
972
  function noteRequestShape(raw, model) {
966
973
  let parsed;
967
974
  try {
@@ -1406,7 +1413,16 @@ function createCodeArtsShim(options) {
1406
1413
  if (payload === "" || payload === "[DONE]") continue;
1407
1414
  try {
1408
1415
  const parsed = JSON.parse(payload);
1409
- if (parsed.usage !== void 0 && parsed.usage !== null) usage.noteUsage(attribution.accountId, attribution.model, parsed.usage);
1416
+ if (parsed.usage !== void 0 && parsed.usage !== null) {
1417
+ usage.noteUsage(attribution.accountId, attribution.model, parsed.usage);
1418
+ recentUsages.push({
1419
+ at: Date.now(),
1420
+ accountId: attribution.accountId,
1421
+ model: attribution.model,
1422
+ usage: parsed.usage
1423
+ });
1424
+ while (recentUsages.length > USAGE_PROBE_LIMIT) recentUsages.shift();
1425
+ }
1410
1426
  } catch {}
1411
1427
  }
1412
1428
  }
@@ -1619,7 +1635,9 @@ function usageOf(deps) {
1619
1635
  requests: 0,
1620
1636
  promptTokens: 0,
1621
1637
  completionTokens: 0,
1622
- totalTokens: 0
1638
+ totalTokens: 0,
1639
+ cachedTokens: 0,
1640
+ reasoningTokens: 0
1623
1641
  };
1624
1642
  return {
1625
1643
  today: snapshot?.today ?? noTotals,
@@ -2129,7 +2147,10 @@ function debugHandler(deps) {
2129
2147
  json(res, 403, { error: "request-not-trusted" });
2130
2148
  return;
2131
2149
  }
2132
- json(res, 200, { requests: deps.recentRequests?.() ?? [] });
2150
+ json(res, 200, {
2151
+ requests: deps.recentRequests?.() ?? [],
2152
+ usages: deps.recentUsages?.() ?? []
2153
+ });
2133
2154
  };
2134
2155
  }
2135
2156
  /** Mount the status / models / token routes. */
@@ -2481,6 +2502,7 @@ function apply(ctx, rawConfig) {
2481
2502
  usage: () => usage.snapshot(),
2482
2503
  queue: () => queue.snapshot(),
2483
2504
  recentRequests: () => recentRequestShapes(),
2505
+ recentUsages: () => recentUsageShapes(),
2484
2506
  credits: (signal) => loadCredits(signal),
2485
2507
  refreshCredits: (signal) => {
2486
2508
  credits.invalidate();
@@ -207,6 +207,41 @@ function parseToken(json, dpopJWK, clientId, prev) {
207
207
  dpopPrivateKey: dpopJWK
208
208
  };
209
209
  }
210
+ /**
211
+ * 华为登录后浏览器落回本机回调的落地页。
212
+ *
213
+ * 这一页本身没有任何后续逻辑:`code` 在响应发出的同一时刻就已交给等待队列
214
+ * (见 startLogin 里的 finish),token 交换完全在服务端做,页面关不关都不影响
215
+ * 登录结果。它只是 redirect_uri 必然要渲染的一个落点 —— 华为侧没有「登录成功」
216
+ * 页可停留,成功标志就是这一次 302 到 127.0.0.1。
217
+ *
218
+ * 卡片用 `window.open` 打开授权页,所以这个标签页能自己关掉;用户从卡片的
219
+ * 手动链接打开、或浏览器拦脚本关窗时 close 会失败,1.2s 后把提示换成「可关闭」。
220
+ */
221
+ const CALLBACK_HTML = `<!doctype html>
222
+ <html lang="zh-CN">
223
+ <head>
224
+ <meta charset="utf-8">
225
+ <meta name="viewport" content="width=device-width,initial-scale=1">
226
+ <title>CodeArts 登录</title>
227
+ <style>
228
+ html,body{height:100%}
229
+ body{margin:0;display:flex;align-items:center;justify-content:center;background:#fff;color:#1f2329;
230
+ font:15px/1.6 system-ui,-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}
231
+ p{margin:0;padding:24px;text-align:center}
232
+ </style>
233
+ </head>
234
+ <body>
235
+ <p id="msg">正在返回 DeepSeek Harness…</p>
236
+ <script>
237
+ setTimeout(function () { try { window.close() } catch (e) {} }, 300)
238
+ setTimeout(function () {
239
+ var el = document.getElementById('msg')
240
+ if (el) el.textContent = 'CodeArts 登录成功,本页可关闭,请回到 DeepSeek Harness。'
241
+ }, 1200)
242
+ <\/script>
243
+ </body>
244
+ </html>`;
210
245
  async function startLogin(cfg = defaultLoginConfig()) {
211
246
  const ticketId = randomHex(16);
212
247
  const { verifier, challenge, method } = pkce();
@@ -236,8 +271,11 @@ async function startLogin(cfg = defaultLoginConfig()) {
236
271
  }
237
272
  finish(code);
238
273
  function finish(c) {
239
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
240
- res.end("<h3>CodeArts 登录成功,可关闭此页面。</h3>");
274
+ res.writeHead(200, {
275
+ "Content-Type": "text/html; charset=utf-8",
276
+ "Cache-Control": "no-store"
277
+ });
278
+ res.end(CALLBACK_HTML);
241
279
  if (c) for (const w of waiters.splice(0)) w(c);
242
280
  }
243
281
  });
@@ -2709,7 +2747,9 @@ function emptyTotals() {
2709
2747
  requests: 0,
2710
2748
  promptTokens: 0,
2711
2749
  completionTokens: 0,
2712
- totalTokens: 0
2750
+ totalTokens: 0,
2751
+ cachedTokens: 0,
2752
+ reasoningTokens: 0
2713
2753
  };
2714
2754
  }
2715
2755
  function addTotals(into, add) {
@@ -2717,6 +2757,8 @@ function addTotals(into, add) {
2717
2757
  into.promptTokens += add.promptTokens ?? 0;
2718
2758
  into.completionTokens += add.completionTokens ?? 0;
2719
2759
  into.totalTokens += add.totalTokens ?? 0;
2760
+ into.cachedTokens += add.cachedTokens ?? 0;
2761
+ into.reasoningTokens += add.reasoningTokens ?? 0;
2720
2762
  }
2721
2763
  function mergeTotals(into, from) {
2722
2764
  addTotals(into, from);
@@ -2730,12 +2772,41 @@ function localDateKey(at = Date.now()) {
2730
2772
  function positiveInt(value) {
2731
2773
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
2732
2774
  }
2733
- /** 从上游 usage 帧里取三个计数字段(缺哪个算 0,total 缺失时用 prompt+completion 兜底)。 */
2775
+ /**
2776
+ * 从上游 usage 帧里取缓存命中数。
2777
+ *
2778
+ * 位置见过两种:OpenAI 的 `prompt_tokens_details.cached_tokens`,以及兼容层直接
2779
+ * 平铺在顶层的几种别名。取第一个能读到的。缓存输入必然是输入的子集,所以比
2780
+ * `promptTokens` 还大的脏数据直接钳掉 —— 否则界面上会出现 >100% 的命中率。
2781
+ */
2782
+ function cachedTokensOf(raw, promptTokens) {
2783
+ if (promptTokens <= 0) return 0;
2784
+ const cached = positiveInt(nestedField(raw.prompt_tokens_details, "cached_tokens") ?? raw.cached_tokens ?? raw.cache_tokens ?? raw.prompt_cache_hit_tokens);
2785
+ return Math.min(cached, promptTokens);
2786
+ }
2787
+ /**
2788
+ * 从上游 usage 帧里取思考 token。
2789
+ *
2790
+ * 口径与 {@link cachedTokensOf} 对称:嵌套 `completion_tokens_details.reasoning_tokens`
2791
+ * 优先,顶层 `reasoning_tokens` 兜底,超过输出总量就钳掉(它含在输出里)。
2792
+ */
2793
+ function reasoningTokensOf(raw, completionTokens) {
2794
+ if (completionTokens <= 0) return 0;
2795
+ const reasoning = positiveInt(nestedField(raw.completion_tokens_details, "reasoning_tokens") ?? raw.reasoning_tokens);
2796
+ return Math.min(reasoning, completionTokens);
2797
+ }
2798
+ /** 读 `*_tokens_details` 这类嵌套对象里的一个字段;不是对象(或缺字段)给 undefined。 */
2799
+ function nestedField(details, key) {
2800
+ return typeof details === "object" && details !== null ? details[key] : void 0;
2801
+ }
2802
+ /** 从上游 usage 帧里取计数(缺哪个算 0,total 缺失时用 prompt+completion 兜底)。 */
2734
2803
  function normalizeUsage(raw) {
2735
2804
  if (raw === void 0) return {
2736
2805
  promptTokens: 0,
2737
2806
  completionTokens: 0,
2738
- totalTokens: 0
2807
+ totalTokens: 0,
2808
+ cachedTokens: 0,
2809
+ reasoningTokens: 0
2739
2810
  };
2740
2811
  const promptTokens = positiveInt(raw.prompt_tokens);
2741
2812
  const completionTokens = positiveInt(raw.completion_tokens);
@@ -2743,9 +2814,31 @@ function normalizeUsage(raw) {
2743
2814
  return {
2744
2815
  promptTokens,
2745
2816
  completionTokens,
2746
- totalTokens: total > 0 ? total : promptTokens + completionTokens
2817
+ totalTokens: total > 0 ? total : promptTokens + completionTokens,
2818
+ cachedTokens: cachedTokensOf(raw, promptTokens),
2819
+ reasoningTokens: reasoningTokensOf(raw, completionTokens)
2820
+ };
2821
+ }
2822
+ /** 读一项累计值,缺字段(旧账本没有 cachedTokens / reasoningTokens)就按 0 补齐。 */
2823
+ function normalizeTotals(raw) {
2824
+ return {
2825
+ requests: positiveInt(raw?.requests),
2826
+ promptTokens: positiveInt(raw?.promptTokens),
2827
+ completionTokens: positiveInt(raw?.completionTokens),
2828
+ totalTokens: positiveInt(raw?.totalTokens),
2829
+ cachedTokens: positiveInt(raw?.cachedTokens),
2830
+ reasoningTokens: positiveInt(raw?.reasoningTokens)
2747
2831
  };
2748
2832
  }
2833
+ /** 整张「模型/账号 -> 累计」表补齐字段。 */
2834
+ function normalizeStore(store) {
2835
+ const out = {};
2836
+ for (const [key, totals] of Object.entries(store ?? {})) {
2837
+ if (typeof totals !== "object" || totals === null) continue;
2838
+ out[key] = normalizeTotals(totals);
2839
+ }
2840
+ return out;
2841
+ }
2749
2842
  /**
2750
2843
  * 用量账本。
2751
2844
  *
@@ -2784,8 +2877,8 @@ var CodeArtsUsageTracker = class {
2784
2877
  if (parsed.version !== 1 || typeof parsed.days !== "object" || parsed.days === null) return;
2785
2878
  for (const [date, day] of Object.entries(parsed.days)) {
2786
2879
  if (typeof day !== "object" || day === null) continue;
2787
- const models = day.models ?? {};
2788
- const accounts = day.accounts ?? {};
2880
+ const models = normalizeStore(day.models);
2881
+ const accounts = normalizeStore(day.accounts);
2789
2882
  this.days.set(date, {
2790
2883
  models,
2791
2884
  accounts
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-codearts",
3
3
  "displayName": "DSH CodeArts Connect",
4
4
  "description": "把华为云 CodeArts 接入 DeepSeek Harness —— 内置 OAuth2 登录(PKCE + DPoP),登录后即可在 DSH 对话窗口使用 CodeArts 的模型。",
5
- "version": "1.2.3",
5
+ "version": "1.2.4",
6
6
  "packageManager": "pnpm@11.24.0",
7
7
  "author": "elegant01",
8
8
  "keywords": [