@loommii/dsh-provider-usage 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +39 -9
- package/docs/CHANGELOG.md +31 -0
- package/lib/client.js +86 -14
- package/lib/daily-stats.js +67 -9
- package/lib/index.js +69 -13
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 loommii
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# dsh-provider-usage — DSH「用量中心」
|
|
2
2
|
|
|
3
|
-
> 当前版本 **v0.
|
|
3
|
+
> 当前版本 **v0.7.0** · npm `@loommii/dsh-provider-usage` · [更新历史](docs/CHANGELOG.md)
|
|
4
4
|
|
|
5
|
-
DSH Web GUI 插件 **「用量中心」**:一个常驻右下角的用量卡片,实时显示你的 AI 服务用量 /
|
|
5
|
+
DSH Web GUI 插件 **「用量中心」**:一个常驻右下角的用量卡片,实时显示你的 AI 服务用量 / 余额,不用再打开网页查;附带本地 Token 统计页,不联网也能看自己今天用了多少。
|
|
6
6
|
|
|
7
7
|
## v0.6.0 更新摘要
|
|
8
8
|
|
|
@@ -19,30 +19,43 @@ DSH Web GUI 插件 **「用量中心」**:一个常驻右下角的用量卡片
|
|
|
19
19
|
|---|---|
|
|
20
20
|
| OpenCode Go(订阅) | 5 小时 / 7 天 / 月度三个窗口的已用百分比 + 进度条 + 重置倒计时 |
|
|
21
21
|
| DeepSeek(余额) | 账户余额(CNY) |
|
|
22
|
-
| Command Code(订阅+余额,自定义) | 5 小时 / 周 / 月窗口使用量 + 月度剩余额度(USD
|
|
22
|
+
| Command Code(订阅+余额,自定义) | 5 小时 / 周 / 月窗口使用量 + 月度剩余额度(USD),主数字为本月剩余百分比(3 位小数向下取整) |
|
|
23
23
|
|
|
24
24
|
## 功能
|
|
25
25
|
|
|
26
26
|
- **常驻用量卡片**:右下角悬浮,一眼看到剩余量;可随意拖动,位置自动记住
|
|
27
27
|
- **自动刷新**:每 30s 自动更新;想看最新数据点卡片上的 ↻ 立即重新查询
|
|
28
28
|
- **多实例**:可以同时配置多个提供方(如多个 OpenCode Go 账号),点卡片 logo / 名称切换查看
|
|
29
|
+
- **本地 Token 统计**:自读本机 `$DSH_HOME/sessions` 会话文件,按天聚合今天的 Token 用量(按提供方分类 + 按模型汇总,汇总表跟随所选提供方),**只读本地、不联网**;数据落盘 `$DSH_HOME/provider-usage/daily-stats/`,历史天封存只算今天;设置页可一键重置统计缓存(不影响会话记录)
|
|
29
30
|
- **状态提示**:正常 / 未更新 / 注意 / 错误,请求中显示「查询中」动画
|
|
30
31
|
- **Key 安全**:密钥只在 DSH 进程内解析使用,界面上一律打码显示;自定义 Key 可加密保存在本机(AES-256 加密),不落明文
|
|
31
32
|
|
|
32
|
-
## 安装
|
|
33
|
+
## 安装 / 卸载
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
# 从 npm 安装
|
|
36
|
-
dsh plugin --profile web add @loommii/dsh-provider-usage
|
|
35
|
+
### GitHub
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
```sh
|
|
38
|
+
# 安装
|
|
39
39
|
dsh plugin --profile web add github:loommii/dsh-provider-usage
|
|
40
40
|
|
|
41
41
|
# 安装指定版本(可选)
|
|
42
42
|
dsh plugin --profile web add github:loommii/dsh-provider-usage#v0.6.0
|
|
43
|
+
|
|
44
|
+
# 卸载
|
|
45
|
+
dsh plugin --profile web remove @loommii/dsh-provider-usage
|
|
43
46
|
```
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
### npm
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
# 安装
|
|
52
|
+
dsh plugin --profile web add @loommii/dsh-provider-usage
|
|
53
|
+
|
|
54
|
+
# 卸载
|
|
55
|
+
dsh plugin --profile web remove @loommii/dsh-provider-usage
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
安装 / 卸载后重启 `dsh web` 即可生效。
|
|
46
59
|
|
|
47
60
|
> npm 包名为 `@loommii/dsh-provider-usage`(无 scope 的 `dsh-provider-usage` 在 npm 上已被他人占用)。
|
|
48
61
|
|
|
@@ -59,5 +72,22 @@ dsh plugin --profile web add github:loommii/dsh-provider-usage#v0.6.0
|
|
|
59
72
|
## 使用说明
|
|
60
73
|
|
|
61
74
|
- 提供方实例的增删改、Key 设置都在 **设置 → 用量中心** 里完成,改动即时生效,无需重启
|
|
75
|
+
- 设置页有两个标签:「提供方」管理实例,「用量统计」查看本地 Token 统计
|
|
62
76
|
- 设置保存在本机浏览器中;删掉实例不会上传或泄露任何 Key
|
|
63
77
|
- 卡片拖动后的位置会自动保存,下次打开还是老位置
|
|
78
|
+
|
|
79
|
+
## 配置项(可选)
|
|
80
|
+
|
|
81
|
+
以下配置写入 profile 的 bundle 配置(`cordis.patch.yml` / 设置页),全部有合理默认值,不配也能用:
|
|
82
|
+
|
|
83
|
+
| 配置项 | 默认值 | 说明 |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `baseUrl` | – | 仅覆盖 OpenCode Go 的查询地址(历史语义);其余提供方用内置官方地址 |
|
|
86
|
+
| `timeoutMs` | `15000` | 出站请求超时(上限 30000) |
|
|
87
|
+
| `subscriptionTimeoutMs` | `5000` | Command Code 订阅端点(次要端点)超时;慢网络可调大 |
|
|
88
|
+
| `maxSessions` | `30` | 本地 Token 统计扫描的会话文件数上限(上限 100) |
|
|
89
|
+
| `sessionsDir` | `$DSH_HOME/sessions` | 本地 Token 统计的会话目录覆盖 |
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
[MIT](LICENSE)
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.7.0(2026-09-01)
|
|
4
|
+
|
|
5
|
+
### 小功能
|
|
6
|
+
- **Command Code 卡片主数字改为「本月剩余百分比」**:原大数字为月已用百分比(整数、四舍五入),现改为月剩余百分比,保留 3 位小数向下取整(如 62.9432148907/70 = 89.91887…% → 显示 89.918%);标签「已用(每月)」→「剩余(每月)」
|
|
7
|
+
- host:`monthly` 新增 `remainingPct` 字段(剩余/总额 ×100,钳 [0,100] 后 `floor` 到 3 位小数);`usedPct` 保留(月窗口行仍显示已用口径)
|
|
8
|
+
- client:大数字读 `remainingPct`;连旧版服务端(无该字段)时按 `100 − usedPct` 同公式兜底;降级(无计划/未知计划)仍显示 `--`
|
|
9
|
+
- 回归:场景 20b/20c/20d 补断言,新增场景 20g(向下取整边界:1/70 → 1.428 非 1.429、69.999/70 → 99.998、满额 100、用完 0、超额钳 0)
|
|
10
|
+
- **用量统计页新增「重置统计」按钮**:「刷新」旁两段式确认(4s 未确认自动解除)后删除按天物化的派生缓存(天文件/游标/哨兵)并清空 30s 查询缓存,下次查询自动从会话原文全量重算;**会话原文只读不动**;天文件损坏 / 迁移异常 / 想强制重建时的自助兜底;新端点 `POST /api/provider-usage/reset-local-stats`(仅回环,POST-only)
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
- **用量统计页「按模型汇总」不随提供方选择切换(恒显示全部提供方的合并数据)**:chips/totals 按 `provider` 过滤,但 `byModel` 恒为全量聚合,且扁平 `{ 模型: 计数 }` 结构无提供方维度、服务端无法过滤
|
|
14
|
+
- host:`byModel` 升级为按提供方嵌套 `{ 提供方: { 模型: 计数 } }`(新会话折叠与天文件落盘同步新形态);响应按选中提供方过滤模型汇总,与 chips/totals 语义对齐;选中提供方恒返回嵌套形态(无数据 = 空对象)
|
|
15
|
+
- 存储迁移(验收回归修复):天文件格式升为 v2(`DAILY_VERSION = 2`);v0.6.1 旧扁平天文件按 deps 重折恢复真实提供方归属(会话文件仍是事实源,重写为嵌套形态);deps 缺失无法重折时才兜底迁入 `unknown` 桶;`.backfilled` 哨兵按版本判定,v1 哨兵触发一次性全量重折
|
|
16
|
+
- client:按选中提供方渲染模型表;「全部」时同名模型跨提供方合并求和展示;按响应形态自动判别、兼容 v0.6.1 旧服务端(无提供方维度时行为同旧版)
|
|
17
|
+
- 回归:host 场景 35(跨提供方同名模型、切换提供方模型表跟随)、场景 36a/36b(旧扁平天文件重折恢复归属 / 无 deps 兜底 unknown)、场景 37(v1 哨兵触发一次性全量重折)、场景 38(重置统计:清缓存重建 + 会话原文完好)
|
|
18
|
+
- 重置统计未清空天文件内存缓存(`dayCache`):重置后立即重查仍以旧天数据做种子,导致「重置对损坏天文件无效」且旧数字残留;现删盘上文件时一并清空(host 场景 39:投毒天文件在重置后从会话原文重建)
|
|
19
|
+
- 重置交互:确认重置后旧数据仍展示到新查询完成(沿用静默刷新策略,与「清空 → 重建」的用户预期相悖);现确认后立即清空展示进入「查询中」重建态。重置完成提示语此前无清除路径会常驻页面;现查询完成后自动消失
|
|
20
|
+
- 测试:user-agent 断言由硬编码 0.6.0 改为动态读取 `package.json` 版本(v0.6.1 升版时漏改,main 上存量失败)
|
|
21
|
+
|
|
22
|
+
## v0.6.1(2026-08-31)
|
|
23
|
+
|
|
24
|
+
### 修复
|
|
25
|
+
- 卡片 hover 标题 `title` 从空字符串改为显示当前提供方名(多 provider 时显示「点击切换」)
|
|
26
|
+
|
|
27
|
+
### 小功能
|
|
28
|
+
- a11y:`prefers-reduced-motion` 媒体查询禁用呼吸动画与进度条扫光(尊重系统级无障碍偏好)
|
|
29
|
+
|
|
30
|
+
### 文档
|
|
31
|
+
- 新增 `LICENSE`(MIT);`package.json` `files` 补全以确保 npm tarball 包含 LICENSE
|
|
32
|
+
- README:顶部副标题加「附带本地 Token 统计页」、功能列表新增 Token 统计要点、安装/卸载章节拆分 GitHub 与 npm 两栏、使用说明补「两个标签」提示、新增「配置项」表格、新增 License 章节
|
|
33
|
+
|
|
3
34
|
## v0.6.0(2026-08-31)
|
|
4
35
|
|
|
5
36
|
> **npm 首发**:本版本同步发布至 npm `@loommii/dsh-provider-usage`(无 scope 的 `dsh-provider-usage` 包名已被他人占用,故改用账号 scope)。npm 与 GitHub 渠道**同号同码**(同一提交、同一 tag `v0.6.0`);scoped 包首次发布需 `--access public`(已在 `publishConfig` 配置),安装方无感。
|
package/lib/client.js
CHANGED
|
@@ -62,13 +62,13 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="' + styleId + '"]') === null) {
|
|
63
63
|
var css = [
|
|
64
64
|
// 官方「模型」设置页完整样式(1:1 注入,dsh-client-ui-settings-models 同款)
|
|
65
|
-
".zGbnIq_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.zGbnIq_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.zGbnIq_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.zGbnIq_notice{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}.zGbnIq_savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}.zGbnIq_rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}.zGbnIq_rowCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}.zGbnIq_rowHead{align-items:center;gap:10px;display:flex}.zGbnIq_rowIdentity{align-items:center;gap:6px;min-width:0;display:inline-flex}.zGbnIq_rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.zGbnIq_rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}.zGbnIq_credentialDot{box-sizing:border-box;border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.zGbnIq_credentialDotConfigured{background:var(--dsw-alias-state-success-primary)}.zGbnIq_credentialDotMissing{background:var(--dsw-alias-state-error-primary)}.zGbnIq_rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.zGbnIq_primaryButton,.zGbnIq_secondaryButton,.zGbnIq_addButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border:none;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.zGbnIq_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.zGbnIq_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.zGbnIq_secondaryButton,.zGbnIq_addButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}.zGbnIq_secondaryButton:hover:not(:disabled),.zGbnIq_addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.zGbnIq_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.zGbnIq_dangerButton{box-sizing:border-box;height:36px;color:var(--dsw-alias-state-error-primary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.zGbnIq_dangerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.zGbnIq_rowActions .zGbnIq_secondaryButton,.zGbnIq_rowActions .zGbnIq_dangerButton{border-radius:14px;height:28px;padding:0 10px;font-size:12px;line-height:18px}.zGbnIq_primaryButton:disabled,.zGbnIq_secondaryButton:disabled,.zGbnIq_dangerButton:disabled,.zGbnIq_addButton:disabled,.zGbnIq_linkButton:disabled,.zGbnIq_addModelButton:disabled{opacity:.4;cursor:default}.zGbnIq_primaryButton:focus-visible,.zGbnIq_secondaryButton:focus-visible,.zGbnIq_dangerButton:focus-visible,.zGbnIq_addButton:focus-visible,.zGbnIq_linkButton:focus-visible,.zGbnIq_addModelButton:focus-visible,.zGbnIq_iconButton:focus-visible,.zGbnIq_customizedSummary:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.zGbnIq_editor{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;display:flex}.zGbnIq_editorHeader{align-items:baseline;gap:8px;display:flex}.zGbnIq_editorTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.zGbnIq_editorRoute{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.zGbnIq_field{flex-direction:column;gap:6px;display:flex}.zGbnIq_fieldLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:10px;font-size:12px;font-weight:500;line-height:18px;display:inline-flex}.zGbnIq_linkButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.zGbnIq_linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.zGbnIq_advancedHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.zGbnIq_editorActions{justify-content:flex-end;gap:8px;display:flex}.zGbnIq_addBlock{flex-direction:column;gap:12px;display:flex}.zGbnIq_addActions{flex-wrap:wrap;gap:10px;display:flex}.zGbnIq_addButton{border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;flex:1 1 0;gap:6px;min-width:180px;height:44px}.zGbnIq_addCard,.zGbnIq_setupCard{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;list-style:none;display:flex}.zGbnIq_addCard .zGbnIq_editor,.zGbnIq_setupCard .zGbnIq_editor{background:0 0;padding:0}.zGbnIq_customized{border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}.zGbnIq_customizedSummary{cursor:pointer;width:fit-content;color:var(--dsw-alias-label-secondary);border-radius:6px;align-items:center;gap:6px;margin-left:-4px;padding:2px 4px;font-size:12px;font-weight:500;line-height:18px;list-style:none;display:flex}.zGbnIq_customizedSummary::-webkit-details-marker{display:none}.zGbnIq_customizedSummary:before{content:\"\";border-bottom:1.5px solid;border-right:1.5px solid;width:5px;height:5px;transition:transform .12s;transform:rotate(-45deg)translate(-1px,-1px)}.zGbnIq_customized[open]>.zGbnIq_customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}.zGbnIq_customizedSummary:hover{color:var(--dsw-alias-label-primary)}.zGbnIq_customizedBody{flex-direction:column;gap:12px;padding-top:12px;display:flex}.zGbnIq_modelCatalog{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:10px;padding-top:12px;display:flex}.zGbnIq_modelCatalogHeading{flex-direction:column;gap:2px;display:flex}.zGbnIq_modelCatalogTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.zGbnIq_modelListHead{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.zGbnIq_modelEntry{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px}.zGbnIq_iconButton{box-sizing:border-box;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.zGbnIq_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.zGbnIq_iconButton:disabled{cursor:default;opacity:.4}.zGbnIq_iconButtonDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}.zGbnIq_modelAdvanced{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;padding:8px 4px 2px;display:grid}.zGbnIq_modelField{flex-direction:column;gap:4px;display:flex}.zGbnIq_modelFieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.zGbnIq_modelEmpty{border:1px dashed var(--dsw-alias-border-l3);text-align:center;border-radius:8px;padding:12px}.zGbnIq_addModelButton{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:14px;align-self:flex-start;align-items:center;gap:4px;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.zGbnIq_addModelButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.zGbnIq_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}select.zGbnIq_input{cursor:pointer;max-width:240px}.zGbnIq_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.zGbnIq_input::placeholder{color:var(--dsw-alias-label-dimmed)}.zGbnIq_input:disabled{opacity:.6;cursor:default}.zGbnIq_selectInput{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 12px center;background-repeat:no-repeat;background-size:12px 12px;padding-right:32px}.zGbnIq_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}.zGbnIq_deleteDialog{width:min(480px,100%)}.zGbnIq_deleteConfirm:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.zGbnIq_deleteConfirm:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.zGbnIq_hiddenLabel{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}@media (prefers-reduced-motion:reduce){.zGbnIq_customizedSummary:before{transition:none}}.zGbnIq_fetchDialog{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);max-width:520px}.zGbnIq_candidateActions{justify-content:flex-end;margin-bottom:6px;display:flex}.zGbnIq_candidateList{flex-direction:column;gap:2px;max-height:320px;margin:0;padding:0;list-style:none;display:flex;overflow-y:auto}.zGbnIq_candidate{border-radius:6px}.zGbnIq_candidateLabel{cursor:pointer;align-items:center;gap:8px;padding:6px 8px;display:flex}.zGbnIq_candidateId{font-family:var(--ds-font-family-code);overflow-wrap:anywhere;flex:auto;font-size:13px}",
|
|
65
|
+
".zGbnIq_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.zGbnIq_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.zGbnIq_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.zGbnIq_notice{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}.zGbnIq_savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}.zGbnIq_rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}.zGbnIq_rowCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}.zGbnIq_rowHead{align-items:center;gap:10px;display:flex}.zGbnIq_rowIdentity{align-items:center;gap:6px;min-width:0;display:inline-flex}.zGbnIq_rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.zGbnIq_rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}.zGbnIq_credentialDot{box-sizing:border-box;border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}.zGbnIq_credentialDotConfigured{background:var(--dsw-alias-state-success-primary)}.zGbnIq_credentialDotMissing{background:var(--dsw-alias-state-error-primary)}.zGbnIq_rowActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.zGbnIq_primaryButton,.zGbnIq_secondaryButton,.zGbnIq_addButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border:none;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.zGbnIq_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.zGbnIq_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.zGbnIq_secondaryButton,.zGbnIq_addButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}.zGbnIq_secondaryButton:hover:not(:disabled),.zGbnIq_addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.zGbnIq_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.zGbnIq_dangerButton{box-sizing:border-box;height:36px;color:var(--dsw-alias-state-error-primary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.zGbnIq_dangerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.zGbnIq_rowActions .zGbnIq_secondaryButton,.zGbnIq_rowActions .zGbnIq_dangerButton{border-radius:14px;height:28px;padding:0 10px;font-size:12px;line-height:18px}.zGbnIq_primaryButton:disabled,.zGbnIq_secondaryButton:disabled,.zGbnIq_dangerButton:disabled,.zGbnIq_addButton:disabled,.zGbnIq_linkButton:disabled,.zGbnIq_addModelButton:disabled{opacity:.4;cursor:default}.zGbnIq_primaryButton:focus-visible,.zGbnIq_secondaryButton:focus-visible,.zGbnIq_dangerButton:focus-visible,.zGbnIq_addButton:focus-visible,.zGbnIq_linkButton:focus-visible,.zGbnIq_addModelButton:focus-visible,.zGbnIq_iconButton:focus-visible,.zGbnIq_customizedSummary:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.zGbnIq_editor{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;display:flex}.zGbnIq_editorHeader{align-items:baseline;gap:8px;display:flex}.zGbnIq_editorTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.zGbnIq_editorRoute{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.zGbnIq_field{flex-direction:column;gap:6px;display:flex}.zGbnIq_fieldLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:10px;font-size:12px;font-weight:500;line-height:18px;display:inline-flex}.zGbnIq_linkButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.zGbnIq_linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.zGbnIq_dangerButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-state-warn-label);font:inherit;cursor:pointer;background:0 0;border:1px solid transparent;border-radius:14px;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex;transition:border-color .15s,color .15s,background .15s}.zGbnIq_dangerButton:hover:not(:disabled){color:var(--dsw-alias-state-danger-primary,var(--dsw-alias-state-warn-label))}.zGbnIq_dangerButton:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.zGbnIq_dangerButton:disabled{opacity:.6;cursor:default}.zGbnIq_dangerButton[data-arm=true]{color:var(--dsw-alias-state-danger-primary,#e5484d);border-color:var(--dsw-alias-state-danger-primary,#e5484d);background:color-mix(in srgb,var(--dsw-alias-state-danger-primary,#e5484d) 8%,transparent)}.zGbnIq_advancedHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.zGbnIq_editorActions{justify-content:flex-end;gap:8px;display:flex}.zGbnIq_addBlock{flex-direction:column;gap:12px;display:flex}.zGbnIq_addActions{flex-wrap:wrap;gap:10px;display:flex}.zGbnIq_addButton{border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;flex:1 1 0;gap:6px;min-width:180px;height:44px}.zGbnIq_addCard,.zGbnIq_setupCard{background:var(--dsw-alias-bg-module-platform);border-radius:12px;flex-direction:column;gap:14px;padding:14px 16px;list-style:none;display:flex}.zGbnIq_addCard .zGbnIq_editor,.zGbnIq_setupCard .zGbnIq_editor{background:0 0;padding:0}.zGbnIq_customized{border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}.zGbnIq_customizedSummary{cursor:pointer;width:fit-content;color:var(--dsw-alias-label-secondary);border-radius:6px;align-items:center;gap:6px;margin-left:-4px;padding:2px 4px;font-size:12px;font-weight:500;line-height:18px;list-style:none;display:flex}.zGbnIq_customizedSummary::-webkit-details-marker{display:none}.zGbnIq_customizedSummary:before{content:\"\";border-bottom:1.5px solid;border-right:1.5px solid;width:5px;height:5px;transition:transform .12s;transform:rotate(-45deg)translate(-1px,-1px)}.zGbnIq_customized[open]>.zGbnIq_customizedSummary:before{transform:rotate(45deg)translate(-1px,-1px)}.zGbnIq_customizedSummary:hover{color:var(--dsw-alias-label-primary)}.zGbnIq_customizedBody{flex-direction:column;gap:12px;padding-top:12px;display:flex}.zGbnIq_modelCatalog{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:10px;padding-top:12px;display:flex}.zGbnIq_modelCatalogHeading{flex-direction:column;gap:2px;display:flex}.zGbnIq_modelCatalogTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.zGbnIq_modelListHead{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.zGbnIq_modelEntry{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px}.zGbnIq_iconButton{box-sizing:border-box;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.zGbnIq_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.zGbnIq_iconButton:disabled{cursor:default;opacity:.4}.zGbnIq_iconButtonDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}.zGbnIq_modelAdvanced{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;padding:8px 4px 2px;display:grid}.zGbnIq_modelField{flex-direction:column;gap:4px;display:flex}.zGbnIq_modelFieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.zGbnIq_modelEmpty{border:1px dashed var(--dsw-alias-border-l3);text-align:center;border-radius:8px;padding:12px}.zGbnIq_addModelButton{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:14px;align-self:flex-start;align-items:center;gap:4px;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.zGbnIq_addModelButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.zGbnIq_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}select.zGbnIq_input{cursor:pointer;max-width:240px}.zGbnIq_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.zGbnIq_input::placeholder{color:var(--dsw-alias-label-dimmed)}.zGbnIq_input:disabled{opacity:.6;cursor:default}.zGbnIq_selectInput{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 12px center;background-repeat:no-repeat;background-size:12px 12px;padding-right:32px}.zGbnIq_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}.zGbnIq_deleteDialog{width:min(480px,100%)}.zGbnIq_deleteConfirm:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.zGbnIq_deleteConfirm:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.zGbnIq_hiddenLabel{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}@media (prefers-reduced-motion:reduce){.zGbnIq_customizedSummary:before{transition:none}}.zGbnIq_fetchDialog{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);max-width:520px}.zGbnIq_candidateActions{justify-content:flex-end;margin-bottom:6px;display:flex}.zGbnIq_candidateList{flex-direction:column;gap:2px;max-height:320px;margin:0;padding:0;list-style:none;display:flex;overflow-y:auto}.zGbnIq_candidate{border-radius:6px}.zGbnIq_candidateLabel{cursor:pointer;align-items:center;gap:8px;padding:6px 8px;display:flex}.zGbnIq_candidateId{font-family:var(--ds-font-family-code);overflow-wrap:anywhere;flex:auto;font-size:13px}",
|
|
66
66
|
// 官方「插件」设置页二级菜单样式(1:1 注入,dsh-client-ui-settings-plugins PluginsSettingsSection 同款)
|
|
67
67
|
".pbvGtq_section{max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.pbvGtq_heading{margin:0;font-size:18px;font-weight:600}.pbvGtq_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px}.pbvGtq_tabs{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:flex-end;gap:22px;margin-top:2px;display:flex}.pbvGtq_tab{color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:0;padding:7px 1px 9px;font-size:13px;line-height:20px;position:relative}.pbvGtq_tab:hover,.pbvGtq_tab[data-active=true]{color:var(--dsw-alias-label-primary)}.pbvGtq_tab[data-active=true]:after,.pbvGtq_tab:focus-visible:after{background:var(--dsw-alias-label-primary);content:\"\";border-radius:2px 2px 0 0;height:2px;position:absolute;bottom:-1px;left:0;right:0}.pbvGtq_tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;color:var(--dsw-alias-label-primary);border-radius:2px}.pbvGtq_panel{min-width:0;padding-top:2px}",
|
|
68
68
|
// 用量统计页样式
|
|
69
69
|
".zGbnIq_statsGrid{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin:12px 0;display:grid}.zGbnIq_stat{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);border-radius:10px;flex-direction:column;gap:3px;padding:10px 12px;display:flex}.zGbnIq_statValue{color:var(--dsw-alias-label-primary);font-size:17px;font-weight:600;font-variant-numeric:tabular-nums;line-height:24px;white-space:nowrap}.zGbnIq_cntCard{cursor:pointer;transition:border-color .15s,background .15s}.zGbnIq_cntCard:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover-solid)}.zGbnIq_cntCard:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.zGbnIq_statLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.zGbnIq_chips{flex-wrap:wrap;gap:6px;margin:10px 0 2px;display:flex}.zGbnIq_chip{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:2px 12px;font-size:12px;line-height:18px}.zGbnIq_chip:hover{color:var(--dsw-alias-label-primary)}.zGbnIq_chip[data-active=true]{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 10%,transparent)}",
|
|
70
70
|
".zGbnIq_json{max-height:260px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;margin:10px 0 0;padding:8px 10px;color:var(--dsw-alias-label-secondary);font-family:var(--ds-font-family-code);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-all}",
|
|
71
|
-
".zGbnIq_progress{height:3px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden;position:relative;margin:8px 0 2px}.zGbnIq_progress i{position:absolute;top:0;bottom:0;width:36%;border-radius:999px;background:var(--dsw-alias-brand-primary,#6c8ef5);animation:zGbnIq-progress 1.2s ease-in-out infinite}@keyframes zGbnIq-progress{0%{left:-36%}100%{left:100%}}",
|
|
71
|
+
".zGbnIq_progress{height:3px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden;position:relative;margin:8px 0 2px}.zGbnIq_progress i{position:absolute;top:0;bottom:0;width:36%;border-radius:999px;background:var(--dsw-alias-brand-primary,#6c8ef5);animation:zGbnIq-progress 1.2s ease-in-out infinite}@keyframes zGbnIq-progress{0%{left:-36%}100%{left:100%}}@media (prefers-reduced-motion:reduce){.zGbnIq_progress i{animation:none;left:32%}}",
|
|
72
72
|
".zGbnIq_modelTable{width:100%;border-collapse:collapse;font-size:12px;line-height:18px}.zGbnIq_modelTable th,.zGbnIq_modelTable td{text-align:left;padding:5px 8px;vertical-align:middle;white-space:nowrap}.zGbnIq_modelTable thead th{color:var(--dsw-alias-label-tertiary);font-weight:500;border-bottom:1px solid var(--dsw-alias-border-l2)}.zGbnIq_modelTable tbody tr{border-bottom:1px solid var(--dsw-alias-border-l2)}.zGbnIq_modelTable tbody tr:last-child{border-bottom:none}.zGbnIq_modelTable tbody tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.zGbnIq_modelTable .zGbnIq_num{text-align:right;font-variant-numeric:tabular-nums;overflow:visible}.zGbnIq_modelTable .zGbnIq_numText{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.zGbnIq_modelTable th.zGbnIq_candidateId{max-width:280px;overflow:visible;font-family:var(--ds-font-family-code);font-weight:400;font-size:13px}.zGbnIq_modelTable .zGbnIq_modelName{display:inline-block;max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.dsh-pu-numCell{cursor:pointer;display:inline-block;max-width:100%;vertical-align:bottom}",
|
|
73
73
|
// ── 常驻用量卡片(Rainytoken 暗色视觉:暖深底/草莓粉/三色窗口条)──
|
|
74
74
|
'.dsh-pu-card{position:fixed;right:16px;bottom:16px;width:304px;box-sizing:border-box;border-radius:16px;pointer-events:auto;background:linear-gradient(180deg,#2A1F25 0%,#241A20 100%);border:1px solid #4D3A42;box-shadow:0 10px 28px rgba(31,20,25,.5),0 2px 6px rgba(31,20,25,.4);padding:14px 14px 12px;font-family:inherit;color:#EFE0E5;font-size:13px;line-height:1.5;touch-action:none;user-select:none;cursor:grab;transition:border-color .2s,transform .2s,box-shadow .2s}',
|
|
@@ -117,7 +117,7 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
// ── 官方样式补充:状态点三态(官方只有 configured/missing)与添加按钮加号 ──
|
|
118
118
|
'.zGbnIq_credentialDot.dsh-pu-dot-loading{background:var(--dsw-alias-state-success-primary,#22c55e);animation:dsh-pu-ms-breathe 1.2s ease-in-out infinite}',
|
|
119
119
|
'.zGbnIq_credentialDot.dsh-pu-dot-gray{background:var(--dsw-alias-label-tertiary,#8b93a7)}',
|
|
120
|
-
'@keyframes dsh-pu-ms-breathe{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.72)}}',
|
|
120
|
+
'@keyframes dsh-pu-ms-breathe{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.72)}}@media (prefers-reduced-motion:reduce){.dsh-pu-chip.dsh-pu-chip-loading i,.zGbnIq_credentialDot.dsh-pu-dot-loading{animation:none}}',
|
|
121
121
|
'.ms-plus{color:var(--dsw-alias-brand-primary);font-size:15px;line-height:1}',
|
|
122
122
|
].join('');
|
|
123
123
|
var tag = document.createElement('style');
|
|
@@ -406,10 +406,18 @@ window.__ModuleLoader__.load({
|
|
|
406
406
|
var warn = data.isValid === false && data.invalidMessage
|
|
407
407
|
? React.createElement('div', { className: 'dsh-pu-error' }, data.invalidMessage)
|
|
408
408
|
: null;
|
|
409
|
-
// Command Code 混合卡:主数字 =
|
|
409
|
+
// Command Code 混合卡:主数字 = 月剩余百分比(3 位小数向下取整),副行 = 已用/总额 + 剩余额度
|
|
410
410
|
var cc = null;
|
|
411
411
|
if (isCommandCode) {
|
|
412
|
-
var
|
|
412
|
+
var monthlyInfo = data.monthly || null;
|
|
413
|
+
// 新服务端直接给 remainingPct;旧服务端只有 usedPct → 按 100−usedPct 兜底(与 host 公式数学等价)
|
|
414
|
+
var remainPct = monthlyInfo
|
|
415
|
+
? (monthlyInfo.remainingPct !== undefined && monthlyInfo.remainingPct !== null
|
|
416
|
+
? monthlyInfo.remainingPct
|
|
417
|
+
: (monthlyInfo.usedPct === undefined || monthlyInfo.usedPct === null
|
|
418
|
+
? null
|
|
419
|
+
: Math.floor(Math.max(0, Math.min(100, 100 - monthlyInfo.usedPct)) * 1000) / 1000))
|
|
420
|
+
: null;
|
|
413
421
|
var usedStr = data.used === null ? '--' : Number(data.used).toFixed(2);
|
|
414
422
|
var quotaStr = data.totalQuota === null ? '--' : Number(data.totalQuota).toFixed(2);
|
|
415
423
|
var remainStr = data.remaining === null ? '--' : Number(data.remaining).toFixed(2);
|
|
@@ -421,8 +429,8 @@ window.__ModuleLoader__.load({
|
|
|
421
429
|
var extra = null;
|
|
422
430
|
body = React.createElement('div', null,
|
|
423
431
|
React.createElement('div', { className: 'dsh-pu-remain' },
|
|
424
|
-
React.createElement('span', { className: 'dsh-pu-remain-big' }, isCommandCode ? (
|
|
425
|
-
React.createElement('span', { className: 'dsh-pu-remain-label' }, isCommandCode ? ' ' + zh.
|
|
432
|
+
React.createElement('span', { className: 'dsh-pu-remain-big' }, isCommandCode ? (remainPct === null ? '--' : remainPct.toFixed(3) + '%') : main),
|
|
433
|
+
React.createElement('span', { className: 'dsh-pu-remain-label' }, isCommandCode ? ' ' + zh.remaining + '(' + zh.monthly + ')' : ' ' + mainLabel)),
|
|
426
434
|
warn,
|
|
427
435
|
cc,
|
|
428
436
|
winRows,
|
|
@@ -457,7 +465,7 @@ window.__ModuleLoader__.load({
|
|
|
457
465
|
React.createElement('div', { className: 'dsh-pu-card-head' },
|
|
458
466
|
React.createElement('button', {
|
|
459
467
|
type: 'button', className: 'dsh-pu-title-btn',
|
|
460
|
-
title: multiple ? '
|
|
468
|
+
title: multiple ? current.name + '(点击切换提供方)' : current.name,
|
|
461
469
|
onClick: function (e) { if (draggedRef.current) { draggedRef.current = false; return; } e.stopPropagation(); if (multiple) setMenuOpen(!menuOpen); },
|
|
462
470
|
},
|
|
463
471
|
React.createElement('span', { className: 'dsh-pu-logo ' + logoClass }, logoEl),
|
|
@@ -542,6 +550,9 @@ window.__ModuleLoader__.load({
|
|
|
542
550
|
var [provider, setProvider] = useState(''); // '' = 全部
|
|
543
551
|
var [range, setRange] = useState('1'); // 默认近 1 天;'0'=全部 / '1'=近1天 / '7'=近7天 / '30'=近30天
|
|
544
552
|
var dataRef = useRef(null); // 轮询静默刷新判定用(避免闭包陈旧)
|
|
553
|
+
var [resetState, setResetState] = useState('idle'); // 重置统计:idle → arm(待确认)→ busy → idle
|
|
554
|
+
var [resetMsg, setResetMsg] = useState(null);
|
|
555
|
+
var resetTimerRef = useRef(null); // 两段式确认的 4s 自动解除
|
|
545
556
|
function setDataBoth(v) { dataRef.current = v; setData(v); }
|
|
546
557
|
function load(force) {
|
|
547
558
|
// 自然日语义:近 1 天 = 今天 00:00 起;近 7/30 天 = 今天 + 前 N-1 个完整自然日(0 点边界)
|
|
@@ -560,7 +571,20 @@ window.__ModuleLoader__.load({
|
|
|
560
571
|
else { setErr('无法读取本地用量统计'); }
|
|
561
572
|
})
|
|
562
573
|
.catch(function () { setErr('读取失败'); })
|
|
563
|
-
.finally(function () { setLoading(false); });
|
|
574
|
+
.finally(function () { setLoading(false); setResetMsg(null); });
|
|
575
|
+
}
|
|
576
|
+
function doReset() {
|
|
577
|
+
if (resetState === 'busy') return;
|
|
578
|
+
setResetState('busy'); setResetMsg(null);
|
|
579
|
+
setDataBoth(null); setLoading(true); // 重置语义:立即清空旧数据展示,进入「查询中」重建态
|
|
580
|
+
fetch('/api/provider-usage/reset-local-stats', { method: 'POST', headers: { accept: 'application/json' } })
|
|
581
|
+
.then(function (r) { return r.json().catch(function () { return null; }); })
|
|
582
|
+
.then(function (j) {
|
|
583
|
+
if (j && j.ok) setResetMsg('已重置(清除 ' + (j.removed || 0) + ' 个缓存文件),正在重新统计…');
|
|
584
|
+
else setResetMsg('重置失败' + (j && j.error && j.error.message ? ':' + j.error.message : ''));
|
|
585
|
+
})
|
|
586
|
+
.catch(function () { setResetMsg('重置失败:网络错误'); })
|
|
587
|
+
.finally(function () { setResetState('idle'); load(true); });
|
|
564
588
|
}
|
|
565
589
|
useEffect(function () { if (active) load(false); }, [active, provider, range]);
|
|
566
590
|
useEffect(function () {
|
|
@@ -574,7 +598,42 @@ window.__ModuleLoader__.load({
|
|
|
574
598
|
var provs = data && Array.isArray(data.providers) ? data.providers.slice() : [];
|
|
575
599
|
provs.sort();
|
|
576
600
|
var byModel = (data && data.byModel && typeof data.byModel === 'object') ? data.byModel : {};
|
|
577
|
-
|
|
601
|
+
// 形态判别:v0.7.0 = 嵌套 { 提供方: { 模型: 计数 } }(顶层值无 requests 数字);
|
|
602
|
+
// v0.6.1 旧服务端 = 扁平 { 模型: 计数 }(顶层值直接是计数条目)。
|
|
603
|
+
function byModelShape(m) {
|
|
604
|
+
for (var k in m) {
|
|
605
|
+
if (!Object.prototype.hasOwnProperty.call(m, k)) continue;
|
|
606
|
+
var v = m[k];
|
|
607
|
+
if (v && typeof v === 'object') return (typeof v.requests === 'number') ? 'flat' : 'nested';
|
|
608
|
+
}
|
|
609
|
+
return 'flat'; // 空对象:无模型行,形态无影响
|
|
610
|
+
}
|
|
611
|
+
var modelShape = byModelShape(byModel);
|
|
612
|
+
var modelData = {}; // 模型名 -> 计数条目(已按当前提供方选择过滤/合并)
|
|
613
|
+
if (modelShape === 'nested') {
|
|
614
|
+
if (provider === '') {
|
|
615
|
+
// 全部:同名模型跨提供方合并求和(unknown 桶=旧归属缺失事件,一并计入)
|
|
616
|
+
var merged = {};
|
|
617
|
+
for (var pk in byModel) {
|
|
618
|
+
if (!Object.prototype.hasOwnProperty.call(byModel, pk)) continue;
|
|
619
|
+
var bucket = byModel[pk];
|
|
620
|
+
if (!bucket || typeof bucket !== 'object' || typeof bucket.requests === 'number') continue;
|
|
621
|
+
for (var mk in bucket) {
|
|
622
|
+
if (!Object.prototype.hasOwnProperty.call(bucket, mk)) continue;
|
|
623
|
+
var e = bucket[mk], g = merged[mk];
|
|
624
|
+
if (!g) g = merged[mk] = { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
|
|
625
|
+
g.requests += e.requests || 0; g.inputTokens += e.inputTokens || 0;
|
|
626
|
+
g.outputTokens += e.outputTokens || 0; g.cacheReadTokens += e.cacheReadTokens || 0;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
modelData = merged;
|
|
630
|
+
} else {
|
|
631
|
+
modelData = (byModel[provider] && typeof byModel[provider] === 'object' && typeof byModel[provider].requests !== 'number') ? byModel[provider] : {};
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
modelData = byModel; // v0.6.1 旧服务端:无提供方维度,行为同旧版
|
|
635
|
+
}
|
|
636
|
+
var modelNames = Object.keys(modelData).sort();
|
|
578
637
|
// ── 按模型汇总表 ──
|
|
579
638
|
function ModelNameCell(props) {
|
|
580
639
|
var name = props.name;
|
|
@@ -637,25 +696,37 @@ window.__ModuleLoader__.load({
|
|
|
637
696
|
}
|
|
638
697
|
|
|
639
698
|
var modelRows = modelNames.map(function (mn) {
|
|
640
|
-
var md =
|
|
699
|
+
var md = modelData[mn] || { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
|
|
641
700
|
var hit = (md.inputTokens + md.cacheReadTokens) > 0 ? md.cacheReadTokens / (md.inputTokens + md.cacheReadTokens) : 0;
|
|
642
701
|
return { name: mn, requests: md.requests, inputTokens: md.inputTokens, outputTokens: md.outputTokens, cacheReadTokens: md.cacheReadTokens, hitRate: fmtPct(hit) };
|
|
643
702
|
});
|
|
644
703
|
|
|
645
704
|
return React.createElement("div", null,
|
|
646
705
|
React.createElement("div", null,
|
|
647
|
-
React.createElement("p", { className: "zGbnIq_intro", style: { margin: "0 0 8px" } }, "统计 DSH 会话日志中的 Token
|
|
706
|
+
React.createElement("p", { className: "zGbnIq_intro", style: { margin: "0 0 8px" } }, "统计 DSH 会话日志中的 Token 用量(本机解析;历史按天存储,只算当天;下方汇总跟随所选提供方)。" + (data && data.days ? " 已聚合 " + data.days + " 天数据。" : "")),
|
|
648
707
|
React.createElement("div", { className: "zGbnIq_rowHead", style: { marginBottom: "4px" } },
|
|
649
708
|
React.createElement("select", { className: "zGbnIq_input zGbnIq_selectInput", style: { width: "auto" }, value: range, onChange: function (e) { setRange(e.target.value); } },
|
|
650
709
|
React.createElement("option", { value: "1" }, "近 1 天"),
|
|
651
710
|
React.createElement("option", { value: "7" }, "近 7 天"),
|
|
652
711
|
React.createElement("option", { value: "30" }, "近 30 天"),
|
|
653
712
|
React.createElement("option", { value: "0" }, "全部时间")),
|
|
654
|
-
React.createElement("button", { type: "button", className: "zGbnIq_linkButton", onClick: function () { load(true); } }, "刷新")
|
|
713
|
+
React.createElement("button", { type: "button", className: "zGbnIq_linkButton", onClick: function () { load(true); } }, "刷新"),
|
|
714
|
+
React.createElement("button", {
|
|
715
|
+
type: "button",
|
|
716
|
+
className: "zGbnIq_dangerButton",
|
|
717
|
+
"data-arm": resetState === 'arm' ? "true" : undefined,
|
|
718
|
+
disabled: resetState === 'busy',
|
|
719
|
+
"aria-label": resetState === 'arm' ? "再次点击确认重置本地统计" : "重置本地统计(清除按天缓存并重新统计,不影响会话记录)",
|
|
720
|
+
onClick: function () {
|
|
721
|
+
if (resetState === 'arm') { doReset(); }
|
|
722
|
+
else { setResetState('arm'); setResetMsg(null); window.clearTimeout(resetTimerRef.current); resetTimerRef.current = window.setTimeout(function () { setResetState('idle'); }, 4000); }
|
|
723
|
+
},
|
|
724
|
+
}, resetState === 'arm' ? "确认重置?" : resetState === 'busy' ? "重置中…" : "重置统计"))),
|
|
655
725
|
loading ? React.createElement("div", null,
|
|
656
726
|
React.createElement("div", { className: "zGbnIq_progress" }, React.createElement("i", null)),
|
|
657
727
|
React.createElement("p", { className: "zGbnIq_advancedHint" }, "查询中…")) : null,
|
|
658
728
|
err ? React.createElement("p", { className: "zGbnIq_error" }, err) : null,
|
|
729
|
+
resetMsg ? React.createElement("p", { className: "zGbnIq_advancedHint" }, resetMsg) : null,
|
|
659
730
|
total ? React.createElement("div", null,
|
|
660
731
|
React.createElement("div", { className: "zGbnIq_chips" }, chips),
|
|
661
732
|
stats,
|
|
@@ -663,7 +734,8 @@ window.__ModuleLoader__.load({
|
|
|
663
734
|
React.createElement("div", { className: "zGbnIq_modelCatalogHeading" },
|
|
664
735
|
React.createElement("span", { className: "zGbnIq_modelCatalogTitle" }, "按模型汇总")),
|
|
665
736
|
React.createElement(ModelSummaryTable, { rows: modelRows }))) : null,
|
|
666
|
-
!total && !loading && !err && data ? React.createElement("div", { className: "zGbnIq_modelEmpty" }, "没有匹配的 DSH 会话。") : null
|
|
737
|
+
!total && !loading && !err && data ? React.createElement("div", { className: "zGbnIq_modelEmpty" }, "没有匹配的 DSH 会话。") : null,
|
|
738
|
+
total && modelRows.length === 0 ? React.createElement("div", { className: "zGbnIq_modelEmpty" }, total.requests > 0 ? "旧版本统计的用量暂无模型归属明细。" : "该时间范围内没有此提供方的模型用量。") : null);
|
|
667
739
|
}
|
|
668
740
|
// 提供方状态归一:dot 颜色/标签/可点击性/展开内容都从这里出,6 个 if-else 收敛为 1 个表
|
|
669
741
|
// level: 'ok' 绿 / 'err' 红 / 'loading' 脉冲灰 / 'unknown' 静默灰
|
package/lib/daily-stats.js
CHANGED
|
@@ -8,7 +8,9 @@ import { promises as fs } from 'node:fs'
|
|
|
8
8
|
import { init as zstdInit, decompress as zstdWasmDec } from '@bokuweb/zstd-wasm'
|
|
9
9
|
import { decompress as zstdFzDec } from 'fzstd'
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// v2(v0.7.0):byModel 升级为按提供方嵌套 { 提供方: { 模型: 计数 } };v1 为扁平 { 模型: 计数 }。
|
|
12
|
+
// .backfilled 哨兵按版本判定:v1 哨兵会触发一次性全量重折(从会话文件恢复提供方归属)。
|
|
13
|
+
export const DAILY_VERSION = 2
|
|
12
14
|
|
|
13
15
|
/** 本地时区日期键:YYYY-MM-DD。 */
|
|
14
16
|
export function dayKey(ms) {
|
|
@@ -152,7 +154,8 @@ export function parseEvents(text) {
|
|
|
152
154
|
/** 把事件流中落在 [dayStart, dayEnd) 的 assistant/message 记账折叠成按天 totals。
|
|
153
155
|
* 归属:message.source.provider/model,缺失回退 request/header;targetProvider 为空 = 全部 provider;
|
|
154
156
|
* 事件 time 缺失按当天计入(宽松)。返回 { byProvider, byModel },键值含
|
|
155
|
-
* { requests, inputTokens, outputTokens, cacheReadTokens }
|
|
157
|
+
* { requests, inputTokens, outputTokens, cacheReadTokens };
|
|
158
|
+
* byModel 为按提供方嵌套:{ <provider>: { <model>: e } }(v0.7.0 起,支持按提供方过滤模型汇总)。 */
|
|
156
159
|
/** 内部:把一条记账累加到 byProvider/byModel 桶。 */
|
|
157
160
|
function bump(map, key, usage) {
|
|
158
161
|
let e = map[key]
|
|
@@ -203,13 +206,16 @@ export function foldEventsByDay(events, dayStart, dayEnd, targetProvider) {
|
|
|
203
206
|
const guess = stepGuess(data, headerProvider, headerModel)
|
|
204
207
|
if (target !== null && guess.stepProvider !== target) continue
|
|
205
208
|
bump(byProvider, guess.stepProvider || 'unknown', usage)
|
|
206
|
-
|
|
209
|
+
const provKey = guess.stepProvider || 'unknown'
|
|
210
|
+
if (!byModel[provKey]) byModel[provKey] = {}
|
|
211
|
+
bump(byModel[provKey], guess.stepModel || 'unknown', usage)
|
|
207
212
|
}
|
|
208
213
|
return { byProvider, byModel }
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
/** 把事件流按天分组折叠(首次全量回填用):返回 { <YYYY-MM-DD>: { byProvider, byModel },
|
|
212
|
-
* unknown: {...} }——time 缺失的事件归 unknown 桶。targetProvider 为空 = 全部 provider。
|
|
217
|
+
* unknown: {...} }——time 缺失的事件归 unknown 桶。targetProvider 为空 = 全部 provider。
|
|
218
|
+
* byModel 按提供方嵌套(v0.7.0 起,见 foldEventsByDay)。 */
|
|
213
219
|
export function foldEventsByDays(events, targetProvider) {
|
|
214
220
|
const target = (typeof targetProvider === 'string' && targetProvider.trim() !== '') ? targetProvider.trim() : null
|
|
215
221
|
const days = {}
|
|
@@ -236,14 +242,29 @@ export function foldEventsByDays(events, targetProvider) {
|
|
|
236
242
|
let bucket = days[key]
|
|
237
243
|
if (!bucket) { bucket = { byProvider: {}, byModel: {} }; days[key] = bucket }
|
|
238
244
|
bump(bucket.byProvider, guess.stepProvider || 'unknown', usage)
|
|
239
|
-
|
|
245
|
+
const provKey = guess.stepProvider || 'unknown'
|
|
246
|
+
if (!bucket.byModel[provKey]) bucket.byModel[provKey] = {}
|
|
247
|
+
bump(bucket.byModel[provKey], guess.stepModel || 'unknown', usage)
|
|
240
248
|
}
|
|
241
249
|
return days
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
/**
|
|
252
|
+
/** 判断 byModel 是否为 v1 扁平形态(顶层直接是计数条目,无提供方层)。 */
|
|
253
|
+
export function isFlatByModel(map) {
|
|
254
|
+
for (const k of Object.keys(map || {})) {
|
|
255
|
+
const v = map[k]
|
|
256
|
+
if (v && typeof v === 'object' && typeof v.requests === 'number') return true
|
|
257
|
+
}
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 把 src 的 byProvider/byModel 累加进 dst(同键相加)。
|
|
262
|
+
* byModel 兼容 v0.6.1 扁平形态({ <model>: e },双向):src 顶层条目并入 unknown 提供方桶,
|
|
263
|
+
* dst 顶层条目迁移到 unknown 桶后再合并(旧天文件被今天的重算复写时保持形态一致)。 */
|
|
245
264
|
export function mergeTotals(dst, src) {
|
|
246
265
|
if (!src) return
|
|
266
|
+
if (!dst.byProvider || typeof dst.byProvider !== 'object') dst.byProvider = {}
|
|
267
|
+
if (!dst.byModel || typeof dst.byModel !== 'object') dst.byModel = {}
|
|
247
268
|
function mergeInto(map, srcMap) {
|
|
248
269
|
for (const k of Object.keys(srcMap || {})) {
|
|
249
270
|
const s = srcMap[k], t = map[k] || (map[k] = { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 })
|
|
@@ -254,7 +275,21 @@ export function mergeTotals(dst, src) {
|
|
|
254
275
|
}
|
|
255
276
|
}
|
|
256
277
|
mergeInto(dst.byProvider, src.byProvider)
|
|
257
|
-
|
|
278
|
+
if (isFlatByModel(dst.byModel)) {
|
|
279
|
+
const legacy = dst.byModel
|
|
280
|
+
dst.byModel = {}
|
|
281
|
+
mergeInto(dst.byModel.unknown || (dst.byModel.unknown = {}), legacy)
|
|
282
|
+
}
|
|
283
|
+
for (const prov of Object.keys(src.byModel || {})) {
|
|
284
|
+
const bucket = src.byModel[prov]
|
|
285
|
+
if (!bucket || typeof bucket !== 'object') continue
|
|
286
|
+
if (typeof bucket.requests === 'number') { mergeInto(dst.byModel.unknown || (dst.byModel.unknown = {}), { [prov]: bucket }); continue }
|
|
287
|
+
if (!dst.byModel[prov] || typeof dst.byModel[prov] !== 'object' || typeof dst.byModel[prov].requests === 'number') {
|
|
288
|
+
const legacy = dst.byModel[prov] && typeof dst.byModel[prov] === 'object' ? dst.byModel[prov] : null
|
|
289
|
+
dst.byModel[prov] = legacy ? { unknown: legacy } : {}
|
|
290
|
+
}
|
|
291
|
+
mergeInto(dst.byModel[prov], bucket)
|
|
292
|
+
}
|
|
258
293
|
}
|
|
259
294
|
|
|
260
295
|
/** 读某天文件;损坏/缺失返回 null。 */
|
|
@@ -289,15 +324,38 @@ export async function saveCursors(dir, cursors) {
|
|
|
289
324
|
await fs.rename(tmp, join(dir, 'cursors.json'))
|
|
290
325
|
}
|
|
291
326
|
|
|
292
|
-
/** 回填哨兵:标记"
|
|
327
|
+
/** 回填哨兵:标记"全量历史回填已完成且天文件为当前格式"(防止每次请求都重扫)。
|
|
328
|
+
* v0.7.0 起(DAILY_VERSION=2)按哨兵内容版本判定:旧版写的 version 1 哨兵视为未完成,
|
|
329
|
+
* 触发一次性全量重折,把 v1 扁平 byModel 天文件重算成嵌套形态(会话文件仍是事实源)。 */
|
|
293
330
|
export async function hasBackfilled(dir) {
|
|
294
|
-
try {
|
|
331
|
+
try {
|
|
332
|
+
const raw = await fs.readFile(join(dir, '.backfilled'), 'utf8')
|
|
333
|
+
const j = JSON.parse(raw)
|
|
334
|
+
return !!(j && typeof j === 'object' && Number(j.version) >= DAILY_VERSION)
|
|
335
|
+
} catch { return false }
|
|
295
336
|
}
|
|
296
337
|
export async function writeBackfilled(dir) {
|
|
297
338
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 })
|
|
298
339
|
await fs.writeFile(join(dir, '.backfilled'), JSON.stringify({ version: DAILY_VERSION, at: Date.now() }) + '\n', { mode: 0o600 })
|
|
299
340
|
}
|
|
300
341
|
|
|
342
|
+
/** 重置本地统计:删除全部派生数据(天文件/游标/回填哨兵/格式标记),不触碰会话原文。
|
|
343
|
+
* 下一次 local-usage 查询会自动从会话文件重新全量回填。返回删除的天文件数。 */
|
|
344
|
+
export async function resetDailyStats(dir) {
|
|
345
|
+
let removed = 0
|
|
346
|
+
let names
|
|
347
|
+
try { names = await fs.readdir(dir) } catch { return 0 }
|
|
348
|
+
for (const n of names) {
|
|
349
|
+
if (n === 'cursors.json' || n === '.backfilled' || n === '.format' || /^\.\d{4}-\d{2}-\d{2}\.tmp-/.test(n)) {
|
|
350
|
+
try { await fs.rm(join(dir, n)); removed++; continue } catch { /* ignore */ }
|
|
351
|
+
}
|
|
352
|
+
if (/^\d{4}-\d{2}-\d{2}\.json$/.test(n)) {
|
|
353
|
+
try { await fs.rm(join(dir, n)); removed++ } catch { /* ignore */ }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return removed
|
|
357
|
+
}
|
|
358
|
+
|
|
301
359
|
/** 校验 deps(path -> mtime):返回变化的与缺失的路径。 */
|
|
302
360
|
export async function validateDeps(deps) {
|
|
303
361
|
const changed = []
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// balance-json(余额型) :DeepSeek {{baseUrl}}/user/balance → balance_infos[]
|
|
5
5
|
// 凭证链 per provider:DSH 设置 provider.apiKeyEnv → 凭证服务 → 环境变量。
|
|
6
6
|
// 路由:/api/provider-usage/opencode-go(兼容旧版)、/api/provider-usage/query?provider=<id>、
|
|
7
|
-
// /api/provider-usage/templates / credentials / dsh-providers / credential-refs(仅回环)。
|
|
7
|
+
// /api/provider-usage/templates / credentials / dsh-providers / credential-refs / reset-local-stats(仅回环)。
|
|
8
8
|
// 本地 Token 统计(v0.5.0):/api/provider-usage/local-usage(累计+按模型汇总,按天物化存储),
|
|
9
9
|
// 数据源 = 自读 $DSH_HOME/sessions 会话文件(fzstd 解压,assistant/message 事件 data.usage),只读、不联网;
|
|
10
10
|
// 存储 = $DSH_HOME/provider-usage/daily-stats/YYYY-MM-DD.json:历史天封存(deps mtime 校验),只算今天。
|
|
@@ -20,8 +20,8 @@ export const name = 'provider-usage'
|
|
|
20
20
|
|
|
21
21
|
/** 插件版本单一来源:package.json(UA 标识用;bundle 内嵌等读不到时回退字面量)。 */
|
|
22
22
|
const PLUGIN_VERSION = (() => {
|
|
23
|
-
try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '0.
|
|
24
|
-
catch { return '0.
|
|
23
|
+
try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '0.7.0' }
|
|
24
|
+
catch { return '0.7.0' }
|
|
25
25
|
})()
|
|
26
26
|
const USER_AGENT = 'dsh-provider-usage/' + PLUGIN_VERSION
|
|
27
27
|
|
|
@@ -298,6 +298,15 @@ function commandcodeMonthlyUsedPct(totalQuota, monthlyCredits) {
|
|
|
298
298
|
return Math.max(0, Math.min(100, ((totalQuota - remaining) / totalQuota) * 100))
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/** 月剩余百分比:钳制 [0,100] 后保留 3 位小数向下取整(如 89.918878… → 89.918);无法推算返回 null。 */
|
|
302
|
+
function commandcodeMonthlyRemainingPct(totalQuota, monthlyCredits) {
|
|
303
|
+
if (totalQuota === null || totalQuota === undefined || !(totalQuota > 0)) return null
|
|
304
|
+
const remaining = Number(monthlyCredits)
|
|
305
|
+
if (!Number.isFinite(remaining)) return null
|
|
306
|
+
const pct = Math.max(0, Math.min(100, (remaining / totalQuota) * 100))
|
|
307
|
+
return Math.floor(pct * 1000) / 1000
|
|
308
|
+
}
|
|
309
|
+
|
|
301
310
|
/** 把 credits + subscriptions 两个端点的响应换算成混合模型(订阅使用量 + 剩余额度)。 */
|
|
302
311
|
function parseCommandCodeCredits(creditsBody, subBody) {
|
|
303
312
|
let credits = creditsBody
|
|
@@ -334,6 +343,7 @@ function parseCommandCodeCredits(creditsBody, subBody) {
|
|
|
334
343
|
? subData.currentPeriodEnd : null
|
|
335
344
|
|
|
336
345
|
const monthlyUsedPct = commandcodeMonthlyUsedPct(totalQuota, monthlyCredits)
|
|
346
|
+
const monthlyRemainingPct = commandcodeMonthlyRemainingPct(totalQuota, monthlyCredits)
|
|
337
347
|
const monthlyUsed = (monthlyUsedPct === null || totalQuota === null) ? null
|
|
338
348
|
: Math.max(0, totalQuota - monthlyCredits)
|
|
339
349
|
|
|
@@ -367,7 +377,7 @@ function parseCommandCodeCredits(creditsBody, subBody) {
|
|
|
367
377
|
fiveHour: windowOf(fiveHourUsed, fiveHourCap, fiveHourResetAt),
|
|
368
378
|
weekly: windowOf(weeklyUsed, weeklyCap, weeklyResetAt),
|
|
369
379
|
},
|
|
370
|
-
monthly: { usedPct: monthlyUsedPct, used: monthlyUsed, totalQuota, remaining: monthlyCredits, resetsAt: nextResetAt },
|
|
380
|
+
monthly: { usedPct: monthlyUsedPct, remainingPct: monthlyRemainingPct, used: monthlyUsed, totalQuota, remaining: monthlyCredits, resetsAt: nextResetAt },
|
|
371
381
|
cards: null,
|
|
372
382
|
isValid: true,
|
|
373
383
|
invalidMessage: null,
|
|
@@ -696,7 +706,7 @@ export function apply(ctx, rawConfig) {
|
|
|
696
706
|
if (state.lastGood) {
|
|
697
707
|
return { ...state.lastGood.payload, cached: true, stale: true, ok: false, error: { type, message, httpStatus: null } }
|
|
698
708
|
}
|
|
699
|
-
return { ok: false, error: { type, message, httpStatus: null }, provider: null, detected: true, days: 0, scanned: 0, totals: null, byModel:
|
|
709
|
+
return { ok: false, error: { type, message, httpStatus: null }, provider: null, detected: true, days: 0, scanned: 0, totals: null, byModel: { unknown: {} }, providers: [], fetchedAt: null, cached: false, stale: false, snapshots: (state.snapshots || []).slice(), parseVersion: LOCAL_PARSE_VERSION }
|
|
700
710
|
}
|
|
701
711
|
|
|
702
712
|
/** 校验待查天的 deps:unique 路径并行 stat,按天判定 mtime 变化/缺失,必要时重算该天并刷新缓存。 */
|
|
@@ -723,7 +733,15 @@ export function apply(ctx, rawConfig) {
|
|
|
723
733
|
if (c.missing) missing.push(p)
|
|
724
734
|
else if (Math.abs(c.currentMtime - Number(data.deps[p])) > 1) changed.push(p)
|
|
725
735
|
}
|
|
726
|
-
|
|
736
|
+
// v1 扁平 byModel(v0.6.1 落盘):deps 仍全部指向存在的会话文件 → 重折恢复提供方归属(一次性,重写为 v2 嵌套)
|
|
737
|
+
let flatLegacy = daily.isFlatByModel(data.byModel || {}) && Object.keys(data.deps || {}).length > 0
|
|
738
|
+
if (flatLegacy) {
|
|
739
|
+
for (const p of Object.keys(data.deps)) {
|
|
740
|
+
const c = current.get(p)
|
|
741
|
+
if (!c || c.missing) { flatLegacy = false; break }
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (changed.length > 0 || missing.length > 0 || flatLegacy) {
|
|
727
745
|
const repaired = await recomputeDay(day, data.deps || {})
|
|
728
746
|
data.byProvider = repaired.byProvider
|
|
729
747
|
data.byModel = repaired.byModel
|
|
@@ -799,19 +817,22 @@ export function apply(ctx, rawConfig) {
|
|
|
799
817
|
const files = await daily.scanSessionFiles(config.sessionsDir, config.maxSessions, todayStart - 3600000)
|
|
800
818
|
const cursors = await daily.loadCursors(config.dailyDir)
|
|
801
819
|
// 今天 totals = 上次累积的天文件 + 本轮新增帧(文件 mtime 未变则完全跳过)
|
|
820
|
+
// 例外:天文件还是 v0.6.1 的扁平 byModel(无提供方维度)→ 不种子、当天全量重折(v2 嵌套),
|
|
821
|
+
// 游标一并重置,避免旧扁平数据进 unknown 桶 / 增量与重折双计
|
|
802
822
|
const prevDay = await cachedDay(toDay)
|
|
803
|
-
const
|
|
823
|
+
const prevFlat = !!(prevDay && daily.isFlatByModel(prevDay.byModel || {}))
|
|
824
|
+
const out = prevDay && !prevFlat ? {
|
|
804
825
|
byProvider: JSON.parse(JSON.stringify(prevDay.byProvider || {})),
|
|
805
826
|
byModel: JSON.parse(JSON.stringify(prevDay.byModel || {})),
|
|
806
827
|
} : { byProvider: {}, byModel: {} }
|
|
807
|
-
const deps = prevDay && prevDay.deps ? { ...prevDay.deps } : {}
|
|
808
|
-
let anyWork =
|
|
828
|
+
const deps = prevDay && prevDay.deps && !prevFlat ? { ...prevDay.deps } : {}
|
|
829
|
+
let anyWork = prevFlat
|
|
809
830
|
for (const f of files) {
|
|
810
831
|
deps[f.path] = f.mtimeMs
|
|
811
832
|
const cur = cursors[f.path]
|
|
812
|
-
if (cur && cur.mtimeMs === f.mtimeMs) continue // 文件无新写入:本轮零成本跳过
|
|
833
|
+
if (!prevFlat && cur && cur.mtimeMs === f.mtimeMs) continue // 文件无新写入:本轮零成本跳过
|
|
813
834
|
anyWork = true
|
|
814
|
-
if (cur && typeof cur.offset === 'number') {
|
|
835
|
+
if (!prevFlat && cur && typeof cur.offset === 'number') {
|
|
815
836
|
const inc = await daily.readSessionFileFrom(f.path, cur.offset)
|
|
816
837
|
if (inc.changed && inc.text) {
|
|
817
838
|
daily.mergeTotals(out, daily.foldEventsByDay(daily.parseEvents(inc.text), todayStart, todayEnd, null))
|
|
@@ -840,6 +861,18 @@ export function apply(ctx, rawConfig) {
|
|
|
840
861
|
return { toDay, out, scanned: files.length }
|
|
841
862
|
}
|
|
842
863
|
|
|
864
|
+
/** 按提供方过滤嵌套 byModel(v0.6.1 扁平旧数据已由 mergeTotals 迁到 unknown 桶)。
|
|
865
|
+
* pick 为空 = 全部提供方(原样返回嵌套形态);选中提供方无数据 = 空对象。
|
|
866
|
+
* 恒返回嵌套形态(选中时只含该提供方键),客户端据此统一判别新旧服务端。 */
|
|
867
|
+
function filterModelByProvider(byModel, pick) {
|
|
868
|
+
const src = byModel && typeof byModel === 'object' ? byModel : {}
|
|
869
|
+
if (!pick) return src
|
|
870
|
+
const out = {}
|
|
871
|
+
const inner = src[pick]
|
|
872
|
+
if (inner && typeof inner === 'object' && typeof inner.requests !== 'number') out[pick] = inner
|
|
873
|
+
return out
|
|
874
|
+
}
|
|
875
|
+
|
|
843
876
|
async function doLocalUsage(target, since, key, state) {
|
|
844
877
|
let st
|
|
845
878
|
try { st = await fsStat(config.sessionsDir) } catch { return localFail(state, 'no-sessions', '未找到 DSH 会话目录:' + config.sessionsDir) }
|
|
@@ -858,12 +891,15 @@ export function apply(ctx, rawConfig) {
|
|
|
858
891
|
await backfillAll(nowMs)
|
|
859
892
|
await daily.writeBackfilled(config.dailyDir)
|
|
860
893
|
}
|
|
894
|
+
// agg = 全量聚合(providers/totals 用);modelAgg = 模型维度聚合(按提供方嵌套,v0.7.0)
|
|
861
895
|
const agg = { byProvider: {}, byModel: {} }
|
|
896
|
+
const modelAgg = { byModel: {} }
|
|
862
897
|
const dayCount = new Set()
|
|
863
898
|
dayCount.add(toDay)
|
|
864
899
|
// 今天:只算今天变过的文件
|
|
865
900
|
const today = await computeToday(nowMs)
|
|
866
901
|
daily.mergeTotals(agg, today.out)
|
|
902
|
+
daily.mergeTotals(modelAgg, { byModel: today.out.byModel || {} })
|
|
867
903
|
// 历史天:读天文件(内存缓存;5 分钟周期内只校验一次 deps;mtime 变化才重算该天)
|
|
868
904
|
const existing = await daily.listDayFiles(config.dailyDir)
|
|
869
905
|
const pending = []
|
|
@@ -883,14 +919,16 @@ export function apply(ctx, rawConfig) {
|
|
|
883
919
|
for (const day of Object.keys(dayData)) {
|
|
884
920
|
const data = dayData[day]
|
|
885
921
|
daily.mergeTotals(agg, { byProvider: data.byProvider || {}, byModel: data.byModel || {} })
|
|
922
|
+
daily.mergeTotals(modelAgg, { byModel: data.byModel || {} })
|
|
886
923
|
dayCount.add(day)
|
|
887
924
|
}
|
|
888
925
|
// unknown 桶(time 缺失事件):并入聚合(不占天数)
|
|
889
926
|
const unknownData = await cachedDay('unknown')
|
|
890
927
|
if (unknownData) {
|
|
891
928
|
daily.mergeTotals(agg, { byProvider: unknownData.byProvider || {}, byModel: unknownData.byModel || {} })
|
|
929
|
+
daily.mergeTotals(modelAgg, { byModel: unknownData.byModel || {} })
|
|
892
930
|
}
|
|
893
|
-
// 组装响应(chips
|
|
931
|
+
// 组装响应(chips/totals/byModel 均按选中提供方过滤;v0.7.0 起 byModel 按提供方嵌套可过滤)
|
|
894
932
|
const providers = Object.keys(agg.byProvider).sort()
|
|
895
933
|
const pick = target ? (agg.byProvider[target] || null) : null
|
|
896
934
|
const t = pick || { requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 }
|
|
@@ -917,7 +955,7 @@ export function apply(ctx, rawConfig) {
|
|
|
917
955
|
? t.cacheReadTokens / (t.inputTokens + t.cacheReadTokens)
|
|
918
956
|
: 0,
|
|
919
957
|
},
|
|
920
|
-
byModel:
|
|
958
|
+
byModel: filterModelByProvider(modelAgg.byModel, target || null),
|
|
921
959
|
providers,
|
|
922
960
|
fetchedAt: Date.now(), cached: false, stale: false,
|
|
923
961
|
snapshots: (state.snapshots || []).slice(),
|
|
@@ -928,6 +966,18 @@ export function apply(ctx, rawConfig) {
|
|
|
928
966
|
return payload
|
|
929
967
|
}
|
|
930
968
|
|
|
969
|
+
/** 重置本地统计:删派生天文件/游标/哨兵(会话原文只读不动),清空 30s 缓存与去重态。 */
|
|
970
|
+
async function resetLocalUsage() {
|
|
971
|
+
const st = await fsStat(config.sessionsDir).then(() => true, () => false)
|
|
972
|
+
if (!st) return { ok: false, error: { type: 'no-sessions', message: '未找到 DSH 会话目录:' + config.sessionsDir, httpStatus: null } }
|
|
973
|
+
const removed = await daily.resetDailyStats(config.dailyDir)
|
|
974
|
+
dayCache.clear() // 天文件内存缓存一并清除:否则重查会用旧天数据做种子,重置对损坏天文件无效
|
|
975
|
+
for (const [k, s] of states) {
|
|
976
|
+
if (k.startsWith('local|')) { s.lastGood = null; s.fetching = null; s.validatedDays = new Set(); s.depsCheckAt = 0 }
|
|
977
|
+
}
|
|
978
|
+
return { ok: true, removed }
|
|
979
|
+
}
|
|
980
|
+
|
|
931
981
|
async function localUsage(params) {
|
|
932
982
|
const target = (params && typeof params.provider === 'string' && params.provider.trim() !== '') ? params.provider.trim() : null
|
|
933
983
|
const since = (params && typeof params.since === 'string' && Number(params.since) > 0) ? Number(params.since) : 0
|
|
@@ -1018,6 +1068,12 @@ export function apply(ctx, rawConfig) {
|
|
|
1018
1068
|
path: '/api/provider-usage/local-usage',
|
|
1019
1069
|
handler: jsonRoute((req) => localUsage(queryParamsFromUrl(req))),
|
|
1020
1070
|
}),
|
|
1071
|
+
// 重置本地统计:删除按天物化的派生数据(天文件/游标/哨兵),下次查询自动全量重算;会话原文只读不动
|
|
1072
|
+
ctx.webServer.register({
|
|
1073
|
+
kind: 'exact',
|
|
1074
|
+
path: '/api/provider-usage/reset-local-stats',
|
|
1075
|
+
handler: jsonRoute(() => resetLocalUsage(), true),
|
|
1076
|
+
}),
|
|
1021
1077
|
|
|
1022
1078
|
// 适配器清单(不含任何 secret;客户端据此渲染"添加供应商"选项)
|
|
1023
1079
|
ctx.webServer.register({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loommii/dsh-provider-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "DSH 插件(用量中心):常驻用量卡片 · 多提供方用量/余额查询(OpenCode Go 订阅 + DeepSeek 余额 + Command Code 订阅/余额,对齐 cc-switch 查询语义)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"lib",
|
|
12
12
|
"cordis.patch.yml",
|
|
13
|
+
"LICENSE",
|
|
13
14
|
"README.md",
|
|
14
15
|
"docs/CHANGELOG.md"
|
|
15
16
|
],
|