dsh-codearts 1.2.5 → 1.2.7

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,57 @@
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.7] - 2026-11-05
7
+
8
+ ### 变更
9
+
10
+ - **侧边栏图标改成「纸 + 斜杠」**:上一版换成的 `</>` 虽然不再和宿主的「编辑」图标撞脸,
11
+ 但三条线在 16px 下就是一个瘦括号,看不出是干什么的。现在画成一张纸(左上缺角、右下
12
+ 收口)里压一道斜杠 —— 纸是产出物,斜杠是「写代码」这个动作。笔画仍是 1px、
13
+ `currentColor` 跟随主题,16 / 18 / 24 / 32 各档都验过不会糊。
14
+ 试过更接近手稿的版本(纸框 + 一对尖括号 + 一条曲线,共 7 条 path),16px 下会黏成
15
+ 一团黑的,所以砍到 3 条。图标本身没有配置项,跟着插件走。
16
+
17
+ ## [1.2.6] - 2026-11-05
18
+
19
+ ### 新增
20
+
21
+ - **用量页两张趋势图**:近 7 天每格一根柱子,柱高就是当天的**缓存命中率**,
22
+ 配色分三档(≥80% 正常 / 50–80% 偏黄 / <50% 偏红),没命中数据的格留灰底;
23
+ 下面再跟一排「今天按小时」的 token 量柱,轴每 6 格标一个钟点。悬停任意一格,
24
+ 底部那行会显示该格明细(输入 / 缓存 / 命中率 / 请求数),和柱子的 `title` 一致。
25
+ 看命中率下滑通常意味着系统提示词前缀变了或上下文变长,比看总量更有诊断价值。
26
+ - **账本按天再切一层小时桶**:`usage.days[date].hours` 记录最近两天各小时的用量
27
+ (两天后自动清掉,只保留按天数据),`/api/codearts/status` 的 `usage` 因此多了
28
+ `recentDays`(7 格、缺日补 0)与 `todayByHour`(固定 24 格、未来钟点补 0)。
29
+ 逐小时口径是从本次改动起才开始记的,之前的钟点不回填,所以当天还没有数据时
30
+ 画「今天还没有逐小时数据(启用前的小时不回填)」而不是一排空柱子。
31
+ - **兼容旧宿主**:客户端对 `recentDays` / `todayByHour` 缺失的情况整体跳过这两块图,
32
+ 旧后端不会让用量页报错。
33
+
34
+ ### 变更
35
+
36
+ - **侧边栏图标的 glyph 从铅笔换成 `</>`**:原先画的是一支笔,和宿主自带的「编辑」
37
+ 图标(`IconEditOutline`)几乎同形,挂在图标列里容易被读成「编辑这一页」而不是
38
+ 「CodeArts 入口」。改成一对尖括号夹一道斜杠 —— 代码编辑器里最通用的「写代码」
39
+ 符号,笔画粗细对齐宿主图标集的 1px,主题色仍走 `currentColor`,深浅色都不出错。
40
+ 图标本身没有配置项,跟着插件走。
41
+
42
+ ### 修复
43
+
44
+ - **进入插件页不再先闪一下「未登录」**:卡片原来只按 `status === null` 决定画什么,
45
+ 而 `null` 既可能是「还没读到」也可能是「读失败」,两种情况都被画成了未登录的登录框,
46
+ 等 `/status` 回来才翻成正常。现在单独记三态(`loading` / `ready` / `failed`):
47
+ 加载期间标题栏写「加载中…」、正文放两条呼吸骨架,只有服务端**明确**回
48
+ `signed-out`(或 `expired`)才显示登录框。
49
+ - **拉取失败不再被当成登出**:以前 `GET /api/codearts/status` 一失败(宿主还没起、
50
+ 路由 404、网络抖动)就直接 `setStatus({status:'signed-out'})`。现在失败只置
51
+ `failed`:标题栏「状态读取失败」,正文给错误原因 + 重试按钮;已经读到过数据的话
52
+ 保持原样,不被一次抖动清空。
53
+ - **再进这一页先用上次的快照**:`/status` 冷的时候要去上游拉积分,近一秒才回来;
54
+ 模块级留一份上次的状态,重新进设置页 / 侧边栏页时按旧值立刻画出来,后台再刷新。
55
+ 骨架只在「这个进程里第一次读」时出现。
56
+
6
57
  ## [1.2.5] - 2026-11-05
7
58
 
8
59
  ### 新增
package/README.en.md CHANGED
@@ -26,6 +26,11 @@ Brings Huawei Cloud CodeArts into DeepSeek Harness (DSH), so the models CodeArts
26
26
  token counts (per model, per account), plus each account’s **session-slot occupancy** —
27
27
  which is exactly why a turn had to wait. CodeArts exposes no quota API, so these are the
28
28
  requests this machine sent through the plugin; the card links to the official usage page.
29
+ - **Usage trends**: two charts on top of the Usage tab — a bar per day for the last seven
30
+ days showing the **cache-hit rate** (three colour bands; a dip means the system-prompt
31
+ prefix changed or the context grew), plus today’s token volume bucketed by hour. Hourly
32
+ buckets start with this version and are not backfilled; hosts that do not return the two
33
+ series simply skip the charts.
29
34
  - **Image input**: models with vision support accept pasted or dropped images in a chat.
30
35
  - **Sidebar icon**: optional. Tick it on the card’s Display tab and a CodeArts icon joins
31
36
  the DSH icon column, opening the full-page version of the settings card in the middle
package/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  - **可选模型**:可在设置卡片里勾选要出现在模型选择器中的模型(配置项 `enabledModels`)。未勾选的模型只是不再被推荐,已经选定它的会话仍可继续使用;不勾选等同于提供全部模型。
14
14
  - **模型清单自动发现**:启动后与每小时各拉一次账号真实可用的模型(内置 / 限时福利 / agent-center 三路合并),失败时退回内置种子清单。
15
15
  - **本机用量**:卡片与 `dsh-codearts usage` 展示今日/累计的请求数与输入输出 token(按模型、按账号),以及每个账号的**会话槽位占用**(占用时正好解释了「为什么刚才要等半分钟」)。CodeArts 没有额度接口,这里只统计本机经插件发出的请求,官方额度明细见卡片上的链接。
16
+ - **用量走势**:用量页顶部两张图 —— 近 7 天的**缓存命中率**按天一根柱子(分三档配色,掉了就说明系统提示词前缀变了或上下文变长),以及今天按小时的 token 量柱。逐小时口径从本版起才开始记,之前的钟点不回填;旧宿主不返回这两个序列时整块跳过,不会报错。
16
17
  - **图片输入**:支持视觉的模型可直接在对话里粘贴或拖入图片。
17
18
  - **侧边栏图标**:可选。在卡片「显示」页勾一下,DSH 左侧图标列里会多一个 CodeArts,点开直接在中间列显示整页版的设置卡片(配置项 `showSidebarPanel`,默认关闭 —— 那一列是全宿主共用的)。
18
19
  - **降级路径**:也可以在卡片里直接粘贴安全令牌,适用于脚本或排查场景。
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-DGIuT79_.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-4B0o-CQN.mjs";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  //#region src/version.ts
5
- const CODEARTS_VERSION = "1.2.5";
5
+ const CODEARTS_VERSION = "1.2.7";
6
6
  //#endregion
7
7
  //#region src/bin.ts
8
8
  /**
@@ -12,6 +12,14 @@ interface CodeArtsSettingsKey {
12
12
  statusExpired: string;
13
13
  descSignedIn: string;
14
14
  descSignedOut: string;
15
+ /** 首次读取状态期间不能说「未登录」,用一个中立词。 */
16
+ statusLoading: string;
17
+ /** 状态接口整个失败(服务没起 / 路由 404)时的标题栏文案。 */
18
+ statusUnavailable: string;
19
+ descLoading: string;
20
+ /** {message} 是底层错误(HTTP 500 / Failed to fetch 之类)。 */
21
+ loadFailed: string;
22
+ retry: string;
15
23
  storedMeta: string;
16
24
  storedMetaNoLabel: string;
17
25
  refresh: string;
@@ -75,6 +83,12 @@ interface CodeArtsSettingsKey {
75
83
  usageTipHit: string;
76
84
  usageStatHit: string;
77
85
  usageStatRequests: string;
86
+ /** 趋势两张图的标题与悬停提示(近 7 天命中率 / 今天逐小时)。 */
87
+ usageTrendWeek: string;
88
+ usageTrendHour: string;
89
+ usageTrendHint: string;
90
+ /** 逐小时是本次新增口径,启用前的小时不回填。 */
91
+ usageTrendHourEmpty: string;
78
92
  usageRequests: string;
79
93
  usageByAccount: string;
80
94
  usageRequestCount: string;
@@ -62,7 +62,7 @@ function subscribeSidebarPanelWanted(listener) {
62
62
  }
63
63
  //#endregion
64
64
  //#region \0dsh-css:/home/runner/work/dsh-codearts/dsh-codearts/src/client/CodeArtsPluginCard.module.css.mjs
65
- const css$1 = ".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}";
65
+ const css$1 = ".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_trendBars{align-items:flex-end;gap:3px;height:44px;margin:8px 0 2px;display:flex}.xoziZG_trendSlot{flex-direction:column;flex:1;justify-content:flex-end;min-width:0;height:100%;display:flex}.xoziZG_trendBar{background:var(--dsw-alias-brand-primary);border-radius:2px 2px 0 0;min-height:2px;transition:height .16s,background .16s}.xoziZG_trendBar[data-tone=mid]{background:var(--dsw-alias-state-warn-label)}.xoziZG_trendBar[data-tone=low]{background:var(--dsw-alias-state-error-primary,var(--dsw-alias-state-warn-label))}.xoziZG_trendBar[data-tone=empty]{background:var(--dsw-alias-border-l2)}.xoziZG_trendAxis{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;gap:3px;font-size:10px;line-height:14px;display:flex}.xoziZG_trendAxisSlot{text-align:center;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.xoziZG_trendTip{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;margin:2px 0 0;font-size:11px;line-height:16px}.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}.xoziZG_skeleton{flex-direction:column;gap:10px;padding:4px 0;display:flex}.xoziZG_skeletonRow{border-radius:var(--dsw-radius-sm,4px);background:var(--dsw-alias-border-l2);height:12px;animation:1.4s ease-in-out infinite xoziZG_skeletonBreath}.xoziZG_skeletonRow:nth-child(2){width:72%}.xoziZG_skeletonRow:nth-child(3){width:46%}@keyframes xoziZG_skeletonBreath{0%,to{opacity:.45}50%{opacity:.9}}@media (prefers-reduced-motion:reduce){.xoziZG_skeletonRow{animation:none}}";
66
66
  const tagId$1 = "dsh-codearts/CodeArtsPluginCard.module.css";
67
67
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
68
68
  const tag = document.createElement("style");
@@ -72,79 +72,88 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
72
72
  document.head.appendChild(tag);
73
73
  }
74
74
  var CodeArtsPluginCard_module_css_default = {
75
- "claimAllRow": "xoziZG_claimAllRow",
76
- "bar": "xoziZG_bar",
75
+ "section": "xoziZG_section",
76
+ "creditBlock": "xoziZG_creditBlock",
77
+ "balance": "xoziZG_balance",
78
+ "usageTipSep": "xoziZG_usageTipSep",
79
+ "trendAxisSlot": "xoziZG_trendAxisSlot",
80
+ "skeletonRow": "xoziZG_skeletonRow",
77
81
  "updated": "xoziZG_updated",
78
- "check": "xoziZG_check",
79
- "expiryLabel": "xoziZG_expiryLabel",
80
- "creditFooter": "xoziZG_creditFooter",
81
- "accountName": "xoziZG_accountName",
82
- "choices": "xoziZG_choices",
83
- "card": "xoziZG_card",
84
- "tabPanel": "xoziZG_tabPanel",
85
- "sectionHead": "xoziZG_sectionHead",
86
- "usageTipRow": "xoziZG_usageTipRow",
87
- "fieldLabel": "xoziZG_fieldLabel",
88
82
  "desc": "xoziZG_desc",
83
+ "creditHint": "xoziZG_creditHint",
84
+ "modelChart": "xoziZG_modelChart",
85
+ "creditRow": "xoziZG_creditRow",
86
+ "bar": "xoziZG_bar",
87
+ "statusLine": "xoziZG_statusLine",
88
+ "trendAxis": "xoziZG_trendAxis",
89
+ "modelShare": "xoziZG_modelShare",
90
+ "statSub": "xoziZG_statSub",
91
+ "tabs": "xoziZG_tabs",
92
+ "accountRow": "xoziZG_accountRow",
93
+ "choices": "xoziZG_choices",
94
+ "usageTip": "xoziZG_usageTip",
95
+ "balanceUnit": "xoziZG_balanceUnit",
96
+ "accountNameRow": "xoziZG_accountNameRow",
97
+ "accountRowActions": "xoziZG_accountRowActions",
98
+ "creditRows": "xoziZG_creditRows",
89
99
  "usageTipName": "xoziZG_usageTipName",
100
+ "creditFooter": "xoziZG_creditFooter",
101
+ "error": "xoziZG_error",
90
102
  "modelFill": "xoziZG_modelFill",
91
- "modelTrack": "xoziZG_modelTrack",
92
- "hint": "xoziZG_hint",
93
- "headText": "xoziZG_headText",
94
- "creditRow": "xoziZG_creditRow",
95
- "accountInfo": "xoziZG_accountInfo",
96
- "modelRow": "xoziZG_modelRow",
97
- "usageTipSep": "xoziZG_usageTipSep",
98
- "creditHint": "xoziZG_creditHint",
103
+ "expiryLabel": "xoziZG_expiryLabel",
104
+ "manualSummary": "xoziZG_manualSummary",
105
+ "expiryRow": "xoziZG_expiryRow",
106
+ "check": "xoziZG_check",
107
+ "link": "xoziZG_link",
108
+ "sectionLabel": "xoziZG_sectionLabel",
109
+ "area": "xoziZG_area",
110
+ "skeleton": "xoziZG_skeleton",
111
+ "trendBars": "xoziZG_trendBars",
112
+ "field": "xoziZG_field",
113
+ "modelName": "xoziZG_modelName",
114
+ "usageTipRow": "xoziZG_usageTipRow",
115
+ "card": "xoziZG_card",
116
+ "meta": "xoziZG_meta",
117
+ "claimAllRow": "xoziZG_claimAllRow",
118
+ "sectionHead": "xoziZG_sectionHead",
119
+ "usageFill": "xoziZG_usageFill",
99
120
  "modelRequests": "xoziZG_modelRequests",
121
+ "trendTip": "xoziZG_trendTip",
122
+ "fieldLabel": "xoziZG_fieldLabel",
100
123
  "statGrid": "xoziZG_statGrid",
101
- "accountCard": "xoziZG_accountCard",
102
- "body": "xoziZG_body",
124
+ "skeletonBreath": "xoziZG_skeletonBreath",
125
+ "modelTokens": "xoziZG_modelTokens",
126
+ "trendBar": "xoziZG_trendBar",
127
+ "tabPanel": "xoziZG_tabPanel",
128
+ "creditLink": "xoziZG_creditLink",
103
129
  "name": "xoziZG_name",
104
- "danger": "xoziZG_danger",
105
- "usageBar": "xoziZG_usageBar",
106
- "accountRowActions": "xoziZG_accountRowActions",
107
- "modelShare": "xoziZG_modelShare",
108
- "sectionLabel": "xoziZG_sectionLabel",
109
- "field": "xoziZG_field",
110
- "statusLine": "xoziZG_statusLine",
111
- "area": "xoziZG_area",
112
- "accountEntry": "xoziZG_accountEntry",
130
+ "accountHint": "xoziZG_accountHint",
113
131
  "choiceRow": "xoziZG_choiceRow",
114
- "accountRow": "xoziZG_accountRow",
115
- "statValue": "xoziZG_statValue",
132
+ "modelTrack": "xoziZG_modelTrack",
133
+ "accountToolbar": "xoziZG_accountToolbar",
134
+ "accountCard": "xoziZG_accountCard",
135
+ "accountEntry": "xoziZG_accountEntry",
136
+ "danger": "xoziZG_danger",
137
+ "trendSlot": "xoziZG_trendSlot",
138
+ "headText": "xoziZG_headText",
139
+ "count": "xoziZG_count",
140
+ "manual": "xoziZG_manual",
141
+ "accountName": "xoziZG_accountName",
142
+ "balanceRow": "xoziZG_balanceRow",
143
+ "avatar": "xoziZG_avatar",
116
144
  "manualForm": "xoziZG_manualForm",
117
- "balance": "xoziZG_balance",
145
+ "hint": "xoziZG_hint",
146
+ "statValue": "xoziZG_statValue",
147
+ "header": "xoziZG_header",
148
+ "accountInfo": "xoziZG_accountInfo",
118
149
  "statLabel": "xoziZG_statLabel",
119
- "error": "xoziZG_error",
120
- "barFill": "xoziZG_barFill",
121
- "creditLink": "xoziZG_creditLink",
122
- "avatar": "xoziZG_avatar",
150
+ "modelRow": "xoziZG_modelRow",
151
+ "body": "xoziZG_body",
123
152
  "balanceMeta": "xoziZG_balanceMeta",
124
- "creditBlock": "xoziZG_creditBlock",
125
- "manualSummary": "xoziZG_manualSummary",
153
+ "barFill": "xoziZG_barFill",
126
154
  "actions": "xoziZG_actions",
127
- "accountToolbar": "xoziZG_accountToolbar",
128
- "balanceRow": "xoziZG_balanceRow",
129
- "meta": "xoziZG_meta",
130
- "expiryRow": "xoziZG_expiryRow",
131
- "count": "xoziZG_count",
132
155
  "statCard": "xoziZG_statCard",
133
- "usageFill": "xoziZG_usageFill",
134
- "modelTokens": "xoziZG_modelTokens",
135
- "tabs": "xoziZG_tabs",
136
- "link": "xoziZG_link",
137
- "creditRows": "xoziZG_creditRows",
138
- "modelChart": "xoziZG_modelChart",
139
- "statSub": "xoziZG_statSub",
140
- "accountNameRow": "xoziZG_accountNameRow",
141
- "balanceUnit": "xoziZG_balanceUnit",
142
- "manual": "xoziZG_manual",
143
- "accountHint": "xoziZG_accountHint",
144
- "header": "xoziZG_header",
145
- "usageTip": "xoziZG_usageTip",
146
- "section": "xoziZG_section",
147
- "modelName": "xoziZG_modelName"
156
+ "usageBar": "xoziZG_usageBar"
148
157
  };
149
158
  //#endregion
150
159
  //#region src/client/CodeArtsPluginCard.tsx
@@ -203,6 +212,12 @@ const DOCK_DEFAULT = {
203
212
  queue: true,
204
213
  sidebar: false
205
214
  };
215
+ /**
216
+ * 上一次读到的状态。`/api/codearts/status` 会顺带去上游拉积分(有 TTL 缓存),
217
+ * 冷的时候要等近一秒;模块级留一份快照,再进这一页就能立刻按旧值画出来,
218
+ * 后台再刷新 —— 比每次都先闪骨架舒服,也不会把「还没读到」误判成未登录。
219
+ */
220
+ let lastStatusSnapshot = null;
206
221
  /** 一个 Tab 面板;未选中的用 hidden 留在 DOM 里,草稿和滚动位置都别丢。 */
207
222
  function TabPanel({ id, labelledBy, active, children }) {
208
223
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -521,6 +536,63 @@ function UsageBars({ rows, t }) {
521
536
  });
522
537
  }
523
538
  /**
539
+ * 趋势柱:近 7 天按天、今天按小时。
540
+ *
541
+ * 两张图共用一套柱子,但编码不同,因为要看的问题不同:
542
+ * - `hit`:柱高就是命中率,颜色分档 —— 命中率一掉就能看见,这是诊断「系统提示词
543
+ * 前缀变了 / 上下文突然变长」最直接的信号;
544
+ * - `volume`:柱高按组内最大值归一化 —— 只用来看「什么时候在跑」,绝对值看悬停。
545
+ *
546
+ * 悬停柱子时下面那行换成该格明细,移开回到提示语。轴标签只画整数倍格,24 格全画会挤成一片。
547
+ */
548
+ function UsageTrend({ points, mode, axisEvery, hint, t }) {
549
+ const [hovered, setHovered] = (0, react.useState)(null);
550
+ const max = points.reduce((peak, point) => Math.max(peak, point.totals.totalTokens), 0);
551
+ const shown = hovered === null ? void 0 : points[hovered];
552
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
553
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
554
+ className: CodeArtsPluginCard_module_css_default.trendBars,
555
+ children: points.map((point, index) => {
556
+ const pct = hitPercentOf(point.totals);
557
+ const share = mode === "hit" ? pct === void 0 ? 0 : Math.max(pct, 2) : max === 0 ? 0 : point.totals.totalTokens / max * 100;
558
+ const tone = mode === "hit" ? pct === void 0 ? "empty" : pct >= 80 ? "ok" : pct >= 50 ? "mid" : "low" : point.totals.totalTokens === 0 ? "empty" : "ok";
559
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
560
+ className: CodeArtsPluginCard_module_css_default.trendSlot,
561
+ title: `${point.label} · ${trendDetail(point.totals, t)}`,
562
+ onMouseEnter: () => setHovered(index),
563
+ onMouseLeave: () => setHovered(null),
564
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
565
+ className: CodeArtsPluginCard_module_css_default.trendBar,
566
+ "data-tone": tone,
567
+ style: { height: `${share}%` }
568
+ })
569
+ }, point.key);
570
+ })
571
+ }),
572
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
573
+ className: CodeArtsPluginCard_module_css_default.trendAxis,
574
+ children: points.map((point, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
575
+ className: CodeArtsPluginCard_module_css_default.trendAxisSlot,
576
+ children: index % axisEvery === 0 ? point.label : ""
577
+ }, point.key))
578
+ }),
579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
580
+ className: CodeArtsPluginCard_module_css_default.trendTip,
581
+ children: shown === void 0 ? hint : `${shown.label} · ${trendDetail(shown.totals, t)}`
582
+ })
583
+ ] });
584
+ }
585
+ /** 一格趋势的明细文案:输入 / 缓存 / 命中率 / 请求数(输出与思考在明细卡里看)。 */
586
+ function trendDetail(totals, t) {
587
+ const pct = hitPercentOf(totals);
588
+ return [
589
+ t("usageStatIn", { n: formatTokens$1(totals.promptTokens) }),
590
+ `${t("usageTipCached")} ${formatTokens$1(totals.cachedTokens ?? 0)}`,
591
+ `${t("usageTipHit")} ${pct === void 0 ? "—" : `${pct}%`}`,
592
+ t("usageRequestCount", { n: formatCount(totals.requests) })
593
+ ].join(" · ");
594
+ }
595
+ /**
524
596
  * 用量区块。
525
597
  *
526
598
  * 只展示**本机经插件发出**的请求(不等于账号总额度 —— 账号额度见上方积分区块);
@@ -610,6 +682,37 @@ function UsageSection({ usage, accounts, t }) {
610
682
  })
611
683
  ]
612
684
  }),
685
+ (usage.recentDays ?? []).length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
686
+ className: CodeArtsPluginCard_module_css_default.hint,
687
+ children: t("usageTrendWeek")
688
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageTrend, {
689
+ mode: "hit",
690
+ axisEvery: 1,
691
+ hint: t("usageTrendHint"),
692
+ points: (usage.recentDays ?? []).map((day) => ({
693
+ key: day.date,
694
+ label: day.date.slice(5),
695
+ totals: day.totals
696
+ })),
697
+ t
698
+ })] }),
699
+ (usage.todayByHour ?? []).length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
700
+ className: CodeArtsPluginCard_module_css_default.hint,
701
+ children: t("usageTrendHour")
702
+ }), (usage.todayByHour ?? []).some((entry) => entry.totals.requests > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageTrend, {
703
+ mode: "volume",
704
+ axisEvery: 6,
705
+ hint: t("usageTrendHint"),
706
+ points: (usage.todayByHour ?? []).map((entry) => ({
707
+ key: String(entry.hour),
708
+ label: String(entry.hour),
709
+ totals: entry.totals
710
+ })),
711
+ t
712
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
713
+ className: CodeArtsPluginCard_module_css_default.trendTip,
714
+ children: t("usageTrendHourEmpty")
715
+ })] }),
613
716
  accounts.length > 1 && accountRows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
614
717
  className: CodeArtsPluginCard_module_css_default.hint,
615
718
  children: t("usageByAccount")
@@ -656,7 +759,7 @@ function UsageSection({ usage, accounts, t }) {
656
759
  });
657
760
  }
658
761
  function CodeArtsPluginCard({ t }) {
659
- const [status, setStatus] = (0, react.useState)(null);
762
+ const [status, setStatus] = (0, react.useState)(lastStatusSnapshot);
660
763
  const [token, setToken] = (0, react.useState)("");
661
764
  const [label, setLabel] = (0, react.useState)("");
662
765
  const [busy, setBusy] = (0, react.useState)(false);
@@ -688,6 +791,13 @@ function CodeArtsPluginCard({ t }) {
688
791
  const [dockWritable, setDockWritable] = (0, react.useState)(false);
689
792
  const [dockSaving, setDockSaving] = (0, react.useState)(false);
690
793
  const [dockError, setDockError] = (0, react.useState)(null);
794
+ /**
795
+ * 首次读取状态的三态。为什么要单独记:`status === null` 只说明「还没读到」,
796
+ * 直接按它画就会先闪一下「未登录」,等数据回来再翻成「已登录」—— 看起来像
797
+ * 掉线又自己好了。只有服务端明确回 signed-out 才允许画登录框。
798
+ */
799
+ const [statusPhase, setStatusPhase] = (0, react.useState)(lastStatusSnapshot === null ? "loading" : "ready");
800
+ const [loadError, setLoadError] = (0, react.useState)(null);
691
801
  async function refreshDock() {
692
802
  try {
693
803
  const res = await fetch("/api/codearts/dock", { headers: { Accept: "application/json" } });
@@ -791,14 +901,16 @@ function CodeArtsPluginCard({ t }) {
791
901
  }
792
902
  async function refresh() {
793
903
  try {
794
- const data = await (await fetch("/api/codearts/status", { headers: { Accept: "application/json" } })).json();
904
+ const res = await fetch("/api/codearts/status", { headers: { Accept: "application/json" } });
905
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
906
+ const data = await res.json();
907
+ lastStatusSnapshot = data;
795
908
  setStatus(data);
796
- } catch {
797
- setStatus({
798
- status: "signed-out",
799
- models: [],
800
- accounts: []
801
- });
909
+ setLoadError(null);
910
+ setStatusPhase("ready");
911
+ } catch (error) {
912
+ setLoadError(error instanceof Error ? error.message : String(error));
913
+ setStatusPhase((phase) => phase === "ready" ? "ready" : "failed");
802
914
  }
803
915
  }
804
916
  async function refreshSelection() {
@@ -1024,6 +1136,10 @@ function CodeArtsPluginCard({ t }) {
1024
1136
  }
1025
1137
  const signedIn = status?.status === "signed-in";
1026
1138
  const expired = status?.status === "expired";
1139
+ /** 首次数据还没回来:标题栏说「加载中」,正文放骨架,别画登录框。 */
1140
+ const loading = statusPhase === "loading";
1141
+ /** 连状态都读不到(服务没起 / 路由 404):给错误和重试,不要假装没登录。 */
1142
+ const failed = statusPhase === "failed" && status === null;
1027
1143
  const accounts = status?.accounts ?? [];
1028
1144
  const accountName = (account) => account.label?.trim() || account.userName?.trim() || account.id;
1029
1145
  /**
@@ -1038,8 +1154,10 @@ function CodeArtsPluginCard({ t }) {
1038
1154
  if (Number.isFinite(exp)) parts.push(exp <= Date.now() ? t("accountExpired", { time: formatStored(exp) }) : t("accountExpires", { time: formatStored(exp) }));
1039
1155
  return parts.join(" · ");
1040
1156
  };
1041
- const dotState = signedIn ? "done" : expired ? "warning" : "idle";
1042
- const statusText = signedIn ? t("statusSignedIn") : expired ? t("statusExpired") : t("statusSignedOut");
1157
+ const dotState = !loading && signedIn ? "done" : !loading && expired ? "warning" : "idle";
1158
+ const statusText = loading ? t("statusLoading") : failed ? t("statusUnavailable") : signedIn ? t("statusSignedIn") : expired ? t("statusExpired") : t("statusSignedOut");
1159
+ /** 读不到状态时别在下面写「去登录」—— 用户可能已经登录了,只是接口没起来。 */
1160
+ const descText = loading ? t("descLoading") : failed ? null : signedIn ? t("descSignedIn") : t("descSignedOut");
1043
1161
  const storedTime = formatStored(status?.storedAt);
1044
1162
  const storedLabel = status?.label?.trim() ?? "";
1045
1163
  const meta = !signedIn || storedTime === "" ? "" : storedLabel === "" ? t("storedMetaNoLabel", { time: storedTime }) : t("storedMeta", {
@@ -1117,15 +1235,15 @@ function CodeArtsPluginCard({ t }) {
1117
1235
  className: CodeArtsPluginCard_module_css_default.meta,
1118
1236
  children: meta
1119
1237
  }),
1120
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1238
+ descText !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1121
1239
  className: CodeArtsPluginCard_module_css_default.desc,
1122
- children: signedIn ? t("descSignedIn") : t("descSignedOut")
1240
+ children: descText
1123
1241
  })
1124
1242
  ]
1125
1243
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1126
1244
  variant: "outline",
1127
1245
  size: "sm",
1128
- disabled: busy,
1246
+ disabled: busy || loading,
1129
1247
  onClick: () => void refresh(),
1130
1248
  children: t("refresh")
1131
1249
  })]
@@ -1136,7 +1254,31 @@ function CodeArtsPluginCard({ t }) {
1136
1254
  className: CodeArtsPluginCard_module_css_default.error,
1137
1255
  children: status.piAiError
1138
1256
  }),
1139
- signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1257
+ failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1258
+ className: CodeArtsPluginCard_module_css_default.section,
1259
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1260
+ className: CodeArtsPluginCard_module_css_default.error,
1261
+ children: t("loadFailed", { message: loadError ?? "" })
1262
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1263
+ className: CodeArtsPluginCard_module_css_default.actions,
1264
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1265
+ variant: "outline",
1266
+ size: "sm",
1267
+ disabled: busy,
1268
+ onClick: () => void refresh(),
1269
+ children: t("retry")
1270
+ })
1271
+ })]
1272
+ }) : loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1273
+ className: CodeArtsPluginCard_module_css_default.skeleton,
1274
+ "aria-busy": "true",
1275
+ "aria-label": t("statusLoading"),
1276
+ children: [
1277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: CodeArtsPluginCard_module_css_default.skeletonRow }),
1278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: CodeArtsPluginCard_module_css_default.skeletonRow }),
1279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: CodeArtsPluginCard_module_css_default.skeletonRow })
1280
+ ]
1281
+ }) : signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1140
1282
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
1141
1283
  className: CodeArtsPluginCard_module_css_default.tabs,
1142
1284
  items: tabs,
@@ -2149,13 +2291,13 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
2149
2291
  }
2150
2292
  var CodeArtsPanel_module_css_default = {
2151
2293
  "icon": "Z66cgG_icon",
2294
+ "page": "Z66cgG_page",
2295
+ "body": "Z66cgG_body",
2296
+ "back": "Z66cgG_back",
2152
2297
  "backCaret": "Z66cgG_backCaret",
2153
2298
  "title": "Z66cgG_title",
2154
2299
  "head": "Z66cgG_head",
2155
- "desc": "Z66cgG_desc",
2156
- "back": "Z66cgG_back",
2157
- "page": "Z66cgG_page",
2158
- "body": "Z66cgG_body"
2300
+ "desc": "Z66cgG_desc"
2159
2301
  };
2160
2302
  //#endregion
2161
2303
  //#region src/client/CodeArtsPanel.tsx
@@ -2170,17 +2312,28 @@ function CodeArtsPanelIcon({ size }) {
2170
2312
  fill: "none",
2171
2313
  "aria-hidden": "true",
2172
2314
  focusable: "false",
2173
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2174
- d: "M9.9 2.6a1.4 1.4 0 0 1 2 0l1.5 1.5a1.4 1.4 0 0 1 0 2l-6.4 6.4-3.6.9a.6.6 0 0 1-.7-.7l.9-3.6 6.3-6.5Z",
2175
- stroke: "currentColor",
2176
- strokeWidth: "1.2",
2177
- strokeLinejoin: "round"
2178
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2179
- d: "M8.9 3.6 12.4 7.1",
2180
- stroke: "currentColor",
2181
- strokeWidth: "1.2",
2182
- strokeLinecap: "round"
2183
- })]
2315
+ children: [
2316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2317
+ d: "M6 2.6h6.5A1.5 1.5 0 0 1 14 4.1V10.8",
2318
+ stroke: "currentColor",
2319
+ strokeWidth: "1",
2320
+ strokeLinecap: "round",
2321
+ strokeLinejoin: "round"
2322
+ }),
2323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2324
+ d: "M2 6.2v6.1a1.5 1.5 0 0 0 1.5 1.5h6.1",
2325
+ stroke: "currentColor",
2326
+ strokeWidth: "1",
2327
+ strokeLinecap: "round",
2328
+ strokeLinejoin: "round"
2329
+ }),
2330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2331
+ d: "M9.3 5.9 6.7 10.9",
2332
+ stroke: "currentColor",
2333
+ strokeWidth: "1",
2334
+ strokeLinecap: "round"
2335
+ })
2336
+ ]
2184
2337
  });
2185
2338
  }
2186
2339
  /** 侧边栏入口页:一个返回按钮 + 标题,正文直接复用设置里那张卡片。 */
@@ -2228,6 +2381,11 @@ const zh = {
2228
2381
  statusExpired: "凭证已过期",
2229
2382
  descSignedIn: "凭证已就绪,勾选的模型会出现在模型选择器里。",
2230
2383
  descSignedOut: "用华为云账号登录,或手动粘贴一个 CodeArts cloud_dragon_token。",
2384
+ statusLoading: "加载中…",
2385
+ statusUnavailable: "状态读取失败",
2386
+ descLoading: "正在读取账号、模型与用量…",
2387
+ loadFailed: "读不到插件状态({message})",
2388
+ retry: "重试",
2231
2389
  storedMeta: "{label} · 存于 {time}",
2232
2390
  storedMetaNoLabel: "存于 {time}",
2233
2391
  refresh: "刷新",
@@ -2292,6 +2450,10 @@ const zh = {
2292
2450
  usageStatHit: "缓存命中",
2293
2451
  usageStatRequests: "今日请求",
2294
2452
  usageRequests: "{n} 次请求",
2453
+ usageTrendWeek: "近 7 天 · 缓存命中率",
2454
+ usageTrendHour: "今天 · 逐小时 token",
2455
+ usageTrendHint: "悬停柱子看那一格明细",
2456
+ usageTrendHourEmpty: "今天还没有逐小时数据(启用前的小时不回填)",
2295
2457
  usageByAccount: "今日按账号",
2296
2458
  usageRequestCount: "{n} 条",
2297
2459
  usageTokens: "输入 {prompt} · 输出 {completion}",
@@ -2366,6 +2528,11 @@ const en = {
2366
2528
  statusExpired: "Credential expired",
2367
2529
  descSignedIn: "Credential is ready. Checked models show up in the model picker.",
2368
2530
  descSignedOut: "Sign in with your Huawei Cloud account, or paste a CodeArts cloud_dragon_token.",
2531
+ statusLoading: "Loading…",
2532
+ statusUnavailable: "Status unavailable",
2533
+ descLoading: "Reading accounts, models and usage…",
2534
+ loadFailed: "Could not read plugin status ({message})",
2535
+ retry: "Retry",
2369
2536
  storedMeta: "{label} · stored {time}",
2370
2537
  storedMetaNoLabel: "stored {time}",
2371
2538
  refresh: "Refresh",
@@ -2430,6 +2597,10 @@ const en = {
2430
2597
  usageStatHit: "Cache hit",
2431
2598
  usageStatRequests: "Today requests",
2432
2599
  usageRequests: "{n} request(s)",
2600
+ usageTrendWeek: "Last 7 days · cache hit rate",
2601
+ usageTrendHour: "Today · tokens by hour",
2602
+ usageTrendHint: "Hover a bar for that slot",
2603
+ usageTrendHourEmpty: "No hourly data yet today (hours before this feature are not backfilled)",
2433
2604
  usageByAccount: "Today by account",
2434
2605
  usageRequestCount: "{n} reqs",
2435
2606
  usageTokens: "in {prompt} · out {completion}",
package/lib/index.d.mts CHANGED
@@ -1347,6 +1347,24 @@ interface UsageSnapshot {
1347
1347
  byAccountToday: Record<string, UsageTotals>;
1348
1348
  /** 账本覆盖的天数(便于「数据从哪天开始」的说明)。 */
1349
1349
  days: number;
1350
+ /**
1351
+ * 最近 {@link TREND_DAYS} 天(含今天,按日期升序)的每日合计;没账目的日子补 0。
1352
+ *
1353
+ * 补 0 而不是跳过:横轴要固定 7 格,否则「昨天没跑」和「昨天丢了」看不出区别。
1354
+ */
1355
+ recentDays: {
1356
+ date: string;
1357
+ totals: UsageTotals;
1358
+ }[];
1359
+ /**
1360
+ * 今天 0 点到 23 点的逐小时合计(固定 24 项,还没到的钟点补 0)。
1361
+ *
1362
+ * 命中率曲线用它 —— 一次长会话把命中率拉低、隔一小时又回来,按天的粒度看不见。
1363
+ */
1364
+ todayByHour: {
1365
+ hour: number;
1366
+ totals: UsageTotals;
1367
+ }[];
1350
1368
  /** 最近一次记账时间(epoch ms)。 */
1351
1369
  lastAt?: number;
1352
1370
  }
@@ -1385,10 +1403,21 @@ declare class CodeArtsUsageTracker {
1385
1403
  noteRequest(accountId: string, model: string, at?: number): void;
1386
1404
  /** 记一次 usage 帧。 */
1387
1405
  noteUsage(accountId: string, model: string, raw: RawUsage | undefined, at?: number): void;
1388
- /** 丢弃保留期之外的天(按日期键比较即可,YYYY-MM-DD 是字典序)。 */
1406
+ /** 一天里的逐小时桶,第一次写的时候才建(老账本没有这个键)。 */
1407
+ private hoursOf;
1408
+ /**
1409
+ * 丢弃保留期之外的天(按日期键比较即可,YYYY-MM-DD 是字典序)。
1410
+ *
1411
+ * 顺带把两天之前的**小时桶**清掉:逐小时数据只有「今天按小时」在画,留着只是
1412
+ * 让 60 天的账本多出一个数量级的体积。
1413
+ */
1389
1414
  private prune;
1390
1415
  /** 当前快照(卡片与 CLI 都读它)。 */
1391
1416
  snapshot(at?: number): UsageSnapshot;
1417
+ /** 最近 TREND_DAYS 天的每日合计(按日期升序,缺账目的日子补 0)。 */
1418
+ private recentDaysOf;
1419
+ /** 今天 0 点起的 24 个钟点(还没到的小时补 0,横轴才有固定的 24 格)。 */
1420
+ private todayByHourOf;
1392
1421
  private scheduleSave;
1393
1422
  /** 立即落盘(关停时调用,别丢最后几秒的账)。 */
1394
1423
  flush(): Promise<void>;
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-DGIuT79_.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-4B0o-CQN.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";
@@ -1650,6 +1650,8 @@ function usageOf(deps) {
1650
1650
  byModelToday: snapshot?.byModelToday ?? [],
1651
1651
  byAccountToday: snapshot?.byAccountToday ?? {},
1652
1652
  days: snapshot?.days ?? 0,
1653
+ recentDays: snapshot?.recentDays ?? [],
1654
+ todayByHour: snapshot?.todayByHour ?? [],
1653
1655
  ...snapshot?.lastAt === void 0 ? {} : { lastAt: snapshot.lastAt },
1654
1656
  slots: slotsOf(deps)
1655
1657
  };
@@ -2769,6 +2769,14 @@ function localDateKey(at = Date.now()) {
2769
2769
  const p = (n) => String(n).padStart(2, "0");
2770
2770
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
2771
2771
  }
2772
+ /** 本地小时键 `00`..`23`(同样按本地时间切,跨零点的那一小时不会错位)。 */
2773
+ function localHourKey(at = Date.now()) {
2774
+ return localHourKeyOfHour(new Date(at).getHours());
2775
+ }
2776
+ /** 0..23 -> `00`..`23`(小时桶的键)。 */
2777
+ function localHourKeyOfHour(hour) {
2778
+ return String(hour).padStart(2, "0");
2779
+ }
2772
2780
  function positiveInt(value) {
2773
2781
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
2774
2782
  }
@@ -2879,9 +2887,14 @@ var CodeArtsUsageTracker = class {
2879
2887
  if (typeof day !== "object" || day === null) continue;
2880
2888
  const models = normalizeStore(day.models);
2881
2889
  const accounts = normalizeStore(day.accounts);
2882
- this.days.set(date, {
2890
+ const hours = day.hours;
2891
+ this.days.set(date, hours === void 0 ? {
2883
2892
  models,
2884
2893
  accounts
2894
+ } : {
2895
+ models,
2896
+ accounts,
2897
+ hours: normalizeStore(hours)
2885
2898
  });
2886
2899
  }
2887
2900
  this.prune(this.now());
@@ -2915,6 +2928,7 @@ var CodeArtsUsageTracker = class {
2915
2928
  const day = this.day(localDateKey(at));
2916
2929
  addTotals(this.bucket(day.models, model), { requests: 1 });
2917
2930
  addTotals(this.bucket(day.accounts, accountId), { requests: 1 });
2931
+ addTotals(this.bucket(this.hoursOf(day), localHourKey(at)), { requests: 1 });
2918
2932
  this.lastAt = at;
2919
2933
  this.prune(at);
2920
2934
  this.scheduleSave();
@@ -2926,15 +2940,28 @@ var CodeArtsUsageTracker = class {
2926
2940
  const day = this.day(localDateKey(at));
2927
2941
  addTotals(this.bucket(day.models, model), usage);
2928
2942
  addTotals(this.bucket(day.accounts, accountId), usage);
2943
+ addTotals(this.bucket(this.hoursOf(day), localHourKey(at)), usage);
2929
2944
  this.lastAt = at;
2930
2945
  this.prune(at);
2931
2946
  this.scheduleSave();
2932
2947
  }
2933
- /** 丢弃保留期之外的天(按日期键比较即可,YYYY-MM-DD 是字典序)。 */
2948
+ /** 一天里的逐小时桶,第一次写的时候才建(老账本没有这个键)。 */
2949
+ hoursOf(day) {
2950
+ day.hours ??= {};
2951
+ return day.hours;
2952
+ }
2953
+ /**
2954
+ * 丢弃保留期之外的天(按日期键比较即可,YYYY-MM-DD 是字典序)。
2955
+ *
2956
+ * 顺带把两天之前的**小时桶**清掉:逐小时数据只有「今天按小时」在画,留着只是
2957
+ * 让 60 天的账本多出一个数量级的体积。
2958
+ */
2934
2959
  prune(at) {
2935
2960
  if (this.days.size === 0) return;
2936
2961
  const cutoff = localDateKey(at - 5184e6);
2937
- for (const date of [...this.days.keys()]) if (date < cutoff) this.days.delete(date);
2962
+ const hourCutoff = localDateKey(at - 1728e5);
2963
+ for (const [date, day] of [...this.days.entries()]) if (date < cutoff) this.days.delete(date);
2964
+ else if (date < hourCutoff && day.hours !== void 0) delete day.hours;
2938
2965
  }
2939
2966
  /** 当前快照(卡片与 CLI 都读它)。 */
2940
2967
  snapshot(at = this.now()) {
@@ -2961,9 +2988,36 @@ var CodeArtsUsageTracker = class {
2961
2988
  })).sort((a, b) => b.totals.totalTokens - a.totals.totalTokens || (a.id < b.id ? -1 : 1)),
2962
2989
  byAccountToday: Object.fromEntries(Object.entries(today?.accounts ?? {}).map(([id, totals]) => [id, { ...totals }])),
2963
2990
  days: this.days.size,
2991
+ recentDays: this.recentDaysOf(at),
2992
+ todayByHour: this.todayByHourOf(today, at),
2964
2993
  ...this.lastAt === void 0 ? {} : { lastAt: this.lastAt }
2965
2994
  };
2966
2995
  }
2996
+ /** 最近 TREND_DAYS 天的每日合计(按日期升序,缺账目的日子补 0)。 */
2997
+ recentDaysOf(at) {
2998
+ const out = [];
2999
+ for (let back = 6; back >= 0; back -= 1) {
3000
+ const date = localDateKey(at - back * 864e5);
3001
+ const day = this.days.get(date);
3002
+ out.push({
3003
+ date,
3004
+ totals: day === void 0 ? emptyTotals() : sumOf(day.models)
3005
+ });
3006
+ }
3007
+ return out;
3008
+ }
3009
+ /** 今天 0 点起的 24 个钟点(还没到的小时补 0,横轴才有固定的 24 格)。 */
3010
+ todayByHourOf(today, at) {
3011
+ const hours = today?.hours ?? {};
3012
+ const currentHour = new Date(at).getHours();
3013
+ return Array.from({ length: 24 }, (_, hour) => {
3014
+ const totals = hours[localHourKeyOfHour(hour)];
3015
+ return {
3016
+ hour,
3017
+ totals: hour > currentHour || totals === void 0 ? emptyTotals() : { ...totals }
3018
+ };
3019
+ });
3020
+ }
2967
3021
  scheduleSave() {
2968
3022
  if (this.path === void 0) return;
2969
3023
  if (this.saveTimer !== void 0) return;
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.5",
5
+ "version": "1.2.7",
6
6
  "packageManager": "pnpm@11.24.0",
7
7
  "author": "elegant01",
8
8
  "keywords": [