@loommii/dsh-provider-usage 0.7.0 → 0.10.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/README.md CHANGED
@@ -1,9 +1,17 @@
1
1
  # dsh-provider-usage — DSH「用量中心」
2
2
 
3
- > 当前版本 **v0.7.0** · npm `@loommii/dsh-provider-usage` · [更新历史](docs/CHANGELOG.md)
3
+ > 当前版本 **v0.10.0** · npm `@loommii/dsh-provider-usage` · [更新历史](docs/CHANGELOG.md)
4
4
 
5
5
  DSH Web GUI 插件 **「用量中心」**:一个常驻右下角的用量卡片,实时显示你的 AI 服务用量 / 余额,不用再打开网页查;附带本地 Token 统计页,不联网也能看自己今天用了多少。
6
6
 
7
+ ## v0.8.0 更新摘要(适配 dsh 0.1.2-alpha)
8
+
9
+ - **适配 dsh `0.1.2-alpha` 破坏性重构**:修复升级后「设置页无用量中心 / 卡片消失」与
10
+ `duplicate loader entry id` 启动崩溃(根因:v0.6.0 包名迁移时 `cordis.patch.yml` 与
11
+ client 注册 id 仍用旧名,叠加 alpha 新的客户端发现机制导致静默失联)
12
+ - host 侧 API 契约、会话文件解析、平台注入(react / slots)经逐项核对在 alpha.4 全部兼容
13
+ - 完整变更与用户升级指引见 [CHANGELOG](docs/CHANGELOG.md)
14
+
7
15
  ## v0.6.0 更新摘要
8
16
 
9
17
  - **新增提供方 Command Code(订阅+余额混合卡)**:5 小时 / 周 / 月窗口使用量 + 月度剩余额度(USD),主数字为月已用百分比
@@ -20,6 +28,7 @@ DSH Web GUI 插件 **「用量中心」**:一个常驻右下角的用量卡片
20
28
  | OpenCode Go(订阅) | 5 小时 / 7 天 / 月度三个窗口的已用百分比 + 进度条 + 重置倒计时 |
21
29
  | DeepSeek(余额) | 账户余额(CNY) |
22
30
  | Command Code(订阅+余额,自定义) | 5 小时 / 周 / 月窗口使用量 + 月度剩余额度(USD),主数字为本月剩余百分比(3 位小数向下取整) |
31
+ | AMD Radeon Cloud (中国)(余额+配额,自定义) | 今日剩余额度(pts)+ 今日已用/限额 + 单日窗口进度条 + 重置倒计时(无周/月窗口;国际版位置预留) |
23
32
 
24
33
  ## 功能
25
34
 
@@ -72,6 +81,10 @@ dsh plugin --profile web remove @loommii/dsh-provider-usage
72
81
  ## 使用说明
73
82
 
74
83
  - 提供方实例的增删改、Key 设置都在 **设置 → 用量中心** 里完成,改动即时生效,无需重启
84
+ - 每个实例可在**编辑**里用「启用」开关管理状态:**开 = 启用(绿)/ 关 = 暂停(灰)**,保存后生效;
85
+ 新添加的提供方默认启用。暂停后该实例**不再轮询查询**(卡片 30s 自动刷新跳过它),
86
+ 卡片切换菜单里也**不可选**;暂停的若是当前卡片实例会自动切到其他启用实例,全部暂停时卡片隐藏。
87
+ 启用/暂停只影响查询与卡片选择,编辑 / 删除 / Key 配置都原样保留
75
88
  - 设置页有两个标签:「提供方」管理实例,「用量统计」查看本地 Token 统计
76
89
  - 设置保存在本机浏览器中;删掉实例不会上传或泄露任何 Key
77
90
  - 卡片拖动后的位置会自动保存,下次打开还是老位置
package/cordis.patch.yml CHANGED
@@ -1,7 +1,10 @@
1
1
  # dsh-provider-usage M1: 插入插件行(自动 reconcile 进 profile bundles)
2
+ # v0.8.0: name 使用正式包名 @loommii/dsh-provider-usage(与 package.json 一致)。
3
+ # 0.7.0 及以前误用改名前的旧名 dsh-provider-usage,在 dsh 0.1.2-alpha 上
4
+ # 只能靠 profile node_modules 里的旧名残留符号链接解析——重装后插件即失联。
2
5
  - insert:
3
6
  - id: provider-usage
4
- name: dsh-provider-usage
7
+ name: '@loommii/dsh-provider-usage'
5
8
  config:
6
9
  baseUrl: https://opencode.ai/zen/go # {{baseUrl}}/v1/usage 即官方 usage 接口
7
10
  timeoutMs: 15000
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,116 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.10.0(未发布)
4
+
5
+ - **新增提供方 AMD Radeon Cloud (中国)(余额+配额卡)**:实测端点 `GET {baseUrl}/usage`(`https://developer.amd.com.cn/radeon/api/v1/usage`,Bearer API Key)——今日剩余 pts 为主数字(3 位小数向下取整百分比)、副行已用/限额 + 剩余(pts)、单日窗口进度条 + 重置倒计时;透传今日/近24h/近30天/累计时间段汇总与按模型拆分(`by_model`)、单次调用明细(`recent`)
6
+ - **无周/月窗口**:AMD 只按日限额计(`daily_cost_limit/used/remaining_usd`),接口无 weekly/monthly 字段;适配器只画「今日」一行进度条
7
+ - **仅自定义添加**:`customOnly`(同 Command Code),Key 加密存插件私有库,默认引用 `RADEON_API_KEY`
8
+ - **国内 / 国际版本**:AMD Radeon Cloud 分两个版本——当前仅接入**中国版**(`developer.amd.com.cn`,显示名「AMD Radeon Cloud (中国)」),国际版 baseUrl/端点不同,适配器位置已预留(后续新增 `amd-radeon-usage-intl` 条目即可)
9
+ - 调研文档:`tmp/AmDRadeonCloud_usage调研.md`(含实测 curl 与字段表;官方文档页路径 404 已记录)
10
+
11
+ ## v0.9.0(2026-09-08)
12
+
13
+ ### 小功能
14
+
15
+ - **Command Code 官方 logo 替换「CC」文字占位**:卡片头部与切换菜单里的 Command Code 图标
16
+ 由紫色渐变底 + 「CC」文字改为官方 ⌘ 符号(白符号·黑底,viewBox 446×446),SVG 内联渲染、
17
+ 随容器裁切圆角;原始素材归档到 `lib/assets/cmdsymbol-light-black-bg.svg`(随包发布便于溯源)。
18
+
19
+ ## v0.8.0(2026-09-03)
20
+
21
+ ### 修复:重启/存储丢失后提供方清单被静默重建(严重数据丢失)
22
+
23
+ **原先的现象**:在用量中心删除某提供方(如 OpenCode Go)后,浏览器存储里的实例清单一旦丢失
24
+ (`dsh web` 重启且访问 origin 变化、清站点数据、浏览器驱逐临时存储、换浏览器等),下次打开页面
25
+ **已删除的默认实例(OpenCode Go/DeepSeek)会凭空复活,而自定义提供方(如 Command Code,`customOnly`
26
+ 不在默认清单里)永久消失**;其私有库 Key 成为孤儿,只能重新手动添加。用户实测即「之前删了 OpenCode Go,
27
+ 重启后变成 Command Code 被删了」。
28
+
29
+ **根因**:提供方实例清单此前**只存浏览器 localStorage**(host 端零持久化),且
30
+ `loadProviders()` 在存储 key 读取失败时静默走 `DEFAULT_PROVIDERS` 兜底重建默认清单;
31
+ host 也没有任何路由能枚举私有库 Key 供启动对账。
32
+
33
+ **修复后的现象**:提供方实例清单以 host 端 `$DSH_HOME/provider-usage/providers.json` 为
34
+ **单一事实源**(0600,与 credentials.json 同边界),localStorage 降级为写穿透缓存;
35
+ 浏览器存储全部丢失后重启,清单从 host 完整恢复,删除仍然生效、自定义实例不再消失。
36
+
37
+ - **host 新增 `GET/POST /api/provider-usage/providers`**(仅回环;POST 整单覆盖保存,幂等)
38
+ - 条目逐个规范化校验:未知 adapter / 缺 id 的脏条目剔除而非整单拒绝;`paused` 严格归一为布尔;
39
+ `ref/name/source/type` 长度钳制;上限 64 个实例
40
+ - 原子写(tmp → rename)+ 0600;损坏文件改名 `providers.json.corrupt-<ts>` 留证并按缺失处理,
41
+ 不卡死恢复流程;目录缺失时自动创建
42
+ - **client 启动恢复**:`useProviders` 首个订阅者触发一次 `GET /providers` 去重恢复;
43
+ host 有清单 → 应用为当前清单;host 无文件(首次升级 0.8.0)→ 把本地缓存清单一次性写回 host 固化;
44
+ host 失联 → 继续用本地缓存(乐观值),后续写操作会重试落盘
45
+ - **恢复竞态守卫**:恢复响应迟到且期间用户已增删改实例时,放弃 host 过期数据,
46
+ 不把用户刚删的实例复活(按会话内写计数判定)
47
+ - **`setProviders` 支持函数式更新**(修复同族丢更新竞态):所有写方(删除/导入/手动添加/编辑/
48
+ 旧数据自愈)改为基于写入时刻的最新清单计算,快速连删两个实例不再复活第一个、
49
+ 自愈写入不再覆盖刚添加的实例;传数组仍兼容旧用法
50
+ - host 场景 40(路由契约/落盘/跨重启恢复/脏条目剔除/400/损坏隔离,13 断言)
51
+
52
+ ### 小功能
53
+ - **提供方支持启用 / 暂停**:设置 → 用量中心 → 提供方设置,每个实例的**编辑卡**里新增「启用」开关
54
+ - 语义:**开 = 启用(开关亮绿色)/ 关 = 暂停(灰色)**;编辑后点「保存」生效
55
+ - 新添加(导入 / 手动)的提供方**默认启用**
56
+ - 暂停后该实例**不再参与轮询查询**(卡片 30s 自动刷新与设置页打开时的状态查询都跳过它),
57
+ 卡片**切换菜单中不可选**;若暂停的正是卡片当前实例,卡片立即切到另一个启用实例,
58
+ 全部暂停时卡片隐藏;恢复启用后立即重新生效(即时持久化,无需重启/刷新),
59
+ Key 等配置原样保留
60
+ - 视觉:编辑卡内官方样式 switch(开=绿 / 关=灰)+ 状态文案;列表中暂停行名称与状态点
61
+ 置灰为「已暂停」,编辑 / 删除仍可用(启用/暂停只影响查询与卡片选择)
62
+ - 启用/暂停状态随实例清单持久化到 host `providers.json`,跨浏览器/存储丢失后同样保留
63
+
64
+ ### 兼容性修复:适配 dsh 0.1.2-alpha(破坏性变更)
65
+
66
+ 本次 dsh `0.1.1-rc.2 → 0.1.2-alpha.4` 重构后,插件出现两类症状:升级当天 `dsh web` 启动即崩
67
+ (`duplicate loader entry id: provider-usage`),重新安装后变为 **host 半区正常(7 条 API 路由全部注册)
68
+ 但 Web UI 静默失联**(设置页无「用量中心」、无常驻卡片)。两个问题的根源都在插件自身的
69
+ 「包名迁移残留」与 alpha 新的客户端发现机制不兼容,本版全部修复:
70
+
71
+ - **`cordis.patch.yml` 的 entry `name` 改为正式包名 `@loommii/dsh-provider-usage`**
72
+ - v0.6.0 包名迁移(`dsh-provider-usage` → `@loommii/dsh-provider-usage`)时漏改了此文件,
73
+ 此后一直靠 profile `node_modules` 里改名前遗留的旧名符号链接 `dsh-provider-usage → 工作区`
74
+ 才能解析到包(rc.2 环境下侥幸可用)
75
+ - alpha.4 的客户端扫描(`dsh-client-modules` node 半区 `nearestPackage`)从 loader entry
76
+ 的模块位置向上查找 package.json,且要求 `name === entry 名`;旧名在重装后解析失败 →
77
+ 包被判定为「非客户端包」→ 永不进 `window.__DSH_BOOT__` 组合图 → UI 静默消失(host 不受影响)
78
+ - 升级首日的 `duplicate loader entry id` 崩溃亦与残留链接相关(旧名/新名两条路径同时进树,
79
+ loader `EntryGroup.update` 对同 id 双行 fail-loud);清理残留链接 + 本修复后消除
80
+ - **删除 `dsh.client.inject: ["@deepseek-ai/dsh-client-runtime"]` 死引用**
81
+ - alpha 重构将 `dsh-client-runtime` 并入 `dsh-client-modules`,该包已不存在;
82
+ 插件 client 半区从未 require 过 runtime 的任何导出,声明本就多余
83
+ - 两侧加载器对 graph 外的 inject 静默跳过,故这是卫生项而非故障源
84
+ - **`lib/client.js` 的 `__ModuleLoader__.load` 注册 id 同步改为 `@loommii/dsh-provider-usage`**
85
+ - alpha 的 boot graph 行 id = 包名,`serveBundle` 按 id 应答 combo bundle;注册 id 与
86
+ graph 行不一致时模块系统找不到 factory(对官方包此场景 fail-loud,对插件即静默失联)
87
+ - **peerDependencies 放宽**:`@deepseek-ai/dsh-host-webserver` `^0.1.0-rc.6` → `>=0.1.0-rc.6`
88
+ (npm prerelease semver 规则下 `^` 不匹配 `0.1.2-alpha.*`,消除安装时 peer 警告)
89
+
90
+ ### 兼容性核对(对 dsh-v0.1.2-alpha.4 源码逐项验证)
91
+
92
+ - `ctx.webServer.register({kind:'exact'|'prefix', path, handler(req,res)})` 契约不变(`dsh-host-webserver` 健在)
93
+ - `window.__ModuleLoader__.load({id, factory})` 仍是注册协议;`exports.inject`(`['slots']`)仍被
94
+ vendored cordis Loader 消费(`Cr.resolve(n.inject)`)
95
+ - 平台 seed 词 `react` / `react-dom/client` / `@deepseek-ai/cordis` / `dsh-client-ui-slots` 全保留
96
+ (React 仍为 18.3.1)
97
+ - `settings.section` 插槽契约保留(`dsh-client-ui-settings` contract/slots.ts)
98
+ - 会话文件格式:JSONL 后端仍默认(`.jsonl.zstd`、`SESSION_FORMAT_VERSION = 0`、目录布局、
99
+ `assistant/message` → `data.usage{inputTokens,outputTokens,cacheReadTokens}` 事件行全部不变),
100
+ 本地 Token 统计不受影响;alpha.3 已移除可选 SQLite 后端,此前的前瞻风险解除
101
+ - alpha.4 新增的「web 请求一次性 fetch 审批(SSRF 防护)」为 web 客户端侧,不影响 host 侧出站查询
102
+
103
+ ### 用户升级指引(从 ≤0.7.0 升级到 0.8.0)
104
+
105
+ 若升级 dsh 后曾出现 `duplicate loader entry id: provider-usage`,profile 的 `node_modules`
106
+ 里可能残留改名前的旧名符号链接。重装插件即可清理:
107
+
108
+ ```sh
109
+ dsh plugin --profile web remove @loommii/dsh-provider-usage
110
+ dsh plugin --profile web add github:loommii/dsh-provider-usage # 或 npm: @loommii/dsh-provider-usage
111
+ # 若 node_modules 下仍有 dsh-provider-usage/(旧名,非 @loommii scope)残留目录/链接,手动删除
112
+ ```
113
+
3
114
  ## v0.7.0(2026-09-01)
4
115
 
5
116
  ### 小功能
@@ -0,0 +1,18 @@
1
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
2
+ viewBox="580 280 200 200" style="display:block" xml:space="preserve">
3
+ <style type="text/css">
4
+ .st0{fill-rule:evenodd;clip-rule:evenodd;}
5
+ .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
6
+ </style>
7
+ <g>
8
+ <polygon class="st0" points="586,288 778.8,288 778.8,480.8 586,480.8 586,288 "/>
9
+ <path class="st1" d="M658.2,461.3h-10.5l-3.2-7.7h-17.4l-2.9,7.7h-10.3l15.7-39h11.3L658.2,461.3L658.2,461.3L658.2,461.3z
10
+ M635.4,431.6l-5.7,15.2h12L635.4,431.6L635.4,431.6z"/>
11
+ <polygon class="st1" points="696.2,422.3 704.7,422.3 704.7,461.3 695,461.3 695,437 684.4,449.2 682.9,449.2 672.4,437
12
+ 672.4,461.3 662.7,461.3 662.7,422.3 671.2,422.3 683.7,436.8 696.2,422.3 "/>
13
+ <path class="st1" d="M729.3,422.3c14.2,0,21.6,8.9,21.6,19.6c0,11.2-7.1,19.4-22.7,19.4h-16.1v-39H729.3L729.3,422.3L729.3,422.3z
14
+ M721.8,454.2h6.3c9.8,0,12.7-6.6,12.7-12.4c0-6.7-3.6-12.4-12.8-12.4h-6.2V454.2L721.8,454.2z"/>
15
+ <polygon class="st1" points="702.5,333.4 660.1,333.4 634,307.5 728.8,307.4 728.8,402.1 702.5,375.6 702.5,333.4 "/>
16
+ <polygon class="st1" points="660.4,376.4 660.2,338.2 633.3,365 633.3,402.8 672.2,402.8 698.5,376.4 660.4,376.4 "/>
17
+ </g>
18
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg width="446" height="446" viewBox="0 0 446 446" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="446" height="446" fill="black"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M226.665 18.1979H218.375C166.389 18.1979 129.23 18.2366 100.991 22.0332C73.2761 25.7594 56.8935 32.8027 44.8481 44.8481C32.8027 56.8935 25.7594 73.2761 22.0332 100.991C18.2365 129.23 18.1979 166.389 18.1979 218.375V226.665C18.1979 278.651 18.2365 315.809 22.0332 344.048C25.7594 371.764 32.8027 388.146 44.8481 400.192C56.8935 412.237 73.276 419.28 100.991 423.007C129.23 426.803 166.389 426.842 218.375 426.842H226.665C278.651 426.842 315.809 426.803 344.048 423.007C371.764 419.28 388.146 412.237 400.191 400.192C412.237 388.146 419.28 371.764 423.006 344.048C426.803 315.809 426.842 278.651 426.842 226.665V218.375C426.842 166.389 426.803 129.23 423.006 100.991C419.28 73.2761 412.237 56.8935 400.191 44.8481C388.146 32.8027 371.764 25.7594 344.048 22.0332C315.809 18.2366 278.651 18.1979 226.665 18.1979ZM31.9803 31.9803C0 63.9605 0 115.432 0 218.375V226.665C0 329.608 0 381.079 31.9803 413.059C63.9605 445.04 115.432 445.04 218.375 445.04H226.665C329.608 445.04 381.079 445.04 413.059 413.059C445.04 381.079 445.04 329.608 445.04 226.665V218.375C445.04 115.432 445.04 63.9605 413.059 31.9803C381.079 0 329.608 0 226.665 0H218.375C115.432 0 63.9605 0 31.9803 31.9803Z" fill="white"/>
4
+ <path d="M306.202 85.5845C276.837 85.5845 252.95 109.472 252.95 138.837V161.66H192.09V138.837C192.09 109.472 168.202 85.5845 138.837 85.5845C109.472 85.5845 85.5845 109.472 85.5845 138.837C85.5845 168.202 109.472 192.09 138.837 192.09H161.66V252.95H138.837C109.472 252.95 85.5845 276.837 85.5845 306.202C85.5845 335.568 109.472 359.455 138.837 359.455C168.202 359.455 192.09 335.568 192.09 306.202V283.38H252.95V306.202C252.95 335.568 276.837 359.455 306.202 359.455C335.567 359.455 359.455 335.568 359.455 306.202C359.455 276.837 335.567 252.95 306.202 252.95H283.38V192.09H306.202C335.567 192.09 359.455 168.202 359.455 138.837C359.455 109.472 335.567 85.5845 306.202 85.5845ZM283.38 161.66V138.837C283.38 126.209 293.574 116.015 306.202 116.015C318.831 116.015 329.025 126.209 329.025 138.837C329.025 151.466 318.831 161.66 306.202 161.66H283.38ZM138.837 161.66C126.209 161.66 116.015 151.466 116.015 138.837C116.015 126.209 126.209 116.015 138.837 116.015C151.466 116.015 161.66 126.209 161.66 138.837V161.66H138.837ZM192.09 252.95V192.09H252.95V252.95H192.09ZM306.202 329.025C293.574 329.025 283.38 318.831 283.38 306.202V283.38H306.202C318.831 283.38 329.025 293.574 329.025 306.202C329.025 318.831 318.831 329.025 306.202 329.025ZM138.837 329.025C126.209 329.025 116.015 318.831 116.015 306.202C116.015 293.574 126.209 283.38 138.837 283.38H161.66V306.202C161.66 318.831 151.466 329.025 138.837 329.025Z" fill="white"/>
5
+ </svg>
package/lib/client.js CHANGED
@@ -3,9 +3,11 @@
3
3
  // (OpenCode Go 订阅 / DeepSeek 余额),卡片按结果字段自适应渲染(%+倒计时=订阅卡;
4
4
  // 金额+币种=余额卡)。
5
5
  // 设置项全部即时生效(localStorage 持久化,无需重启)。
6
+ // v0.8.0: 注册 id 必须等于包名 @loommii/dsh-provider-usage —— dsh 0.1.2-alpha 的
7
+ // boot graph 行 id = 包名,serveBundle 按 id 应答 bundle,不一致即客户端静默失联。
6
8
 
7
9
  window.__ModuleLoader__.load({
8
- id: 'dsh-provider-usage',
10
+ id: '@loommii/dsh-provider-usage',
9
11
  factory: (require) => {
10
12
  var module = { exports: {} };
11
13
  var exports = module.exports;
@@ -23,9 +25,12 @@ window.__ModuleLoader__.load({
23
25
  const Z_INDEX = 2147483000;
24
26
  // 适配器元数据(与 host ADAPTERS 对应;logo 与显示名用于设置页/菜单)
25
27
  const ADAPTER_META = {
26
- 'usage-percent': { displayName: 'OpenCode Go(订阅额度)' },
28
+ 'usage-percent': { displayName: 'OpenCode Go' },
27
29
  'balance-json': { displayName: 'DeepSeek' },
28
- 'commandcode-credits': { displayName: 'Command Code(订阅+余额)' },
30
+ 'commandcode-credits': { displayName: 'Command Code' },
31
+ // AMD Radeon Cloud 分国内 / 国际两个版本(国内 = developer.amd.com.cn;国际版 baseUrl/端点不同)。
32
+ // 当前仅接入中国版「AMD Radeon Cloud (中国)」;国际版位置预留,后续接入时新增适配器条目即可。
33
+ 'amd-radeon-usage': { displayName: 'AMD Radeon Cloud (中国)' },
29
34
  };
30
35
  // 供应商实例清单(用户可配置:名称可改、可增删、可同类型多个;key 引用走 DSH 凭证)
31
36
  const PROVIDERS_KEY = 'dsh-provider-usage.providers';
@@ -45,6 +50,17 @@ window.__ModuleLoader__.load({
45
50
  // DeepSeek:官方鲸 logo PNG(225×225 RGBA)→ data URI,透明底直接展示
46
51
  const LOGO_DEEPSEEK = 'data:image/png;base64,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';
47
52
 
53
+ // Command Code:官方 ⌘ logo(白符号·黑底,viewBox 446×446)→ 内联 SVG(替换原「CC」文字占位)
54
+ const LOGO_CC = React.createElement('svg', { viewBox: '0 0 446 446', 'aria-hidden': 'true', style: { display: 'block', width: '100%', height: '100%' } },
55
+ React.createElement('rect', { width: '446', height: '446', fill: '#000000' }),
56
+ React.createElement('path', { fillRule: 'evenodd', clipRule: 'evenodd', fill: '#FFFFFF', d: 'M226.665 18.1979H218.375C166.389 18.1979 129.23 18.2366 100.991 22.0332C73.2761 25.7594 56.8935 32.8027 44.8481 44.8481C32.8027 56.8935 25.7594 73.2761 22.0332 100.991C18.2365 129.23 18.1979 166.389 18.1979 218.375V226.665C18.1979 278.651 18.2365 315.809 22.0332 344.048C25.7594 371.764 32.8027 388.146 44.8481 400.192C56.8935 412.237 73.276 419.28 100.991 423.007C129.23 426.803 166.389 426.842 218.375 426.842H226.665C278.651 426.842 315.809 426.803 344.048 423.007C371.764 419.28 388.146 412.237 400.191 400.192C412.237 388.146 419.28 371.764 423.006 344.048C426.803 315.809 426.842 278.651 426.842 226.665V218.375C426.842 166.389 426.803 129.23 423.006 100.991C419.28 73.2761 412.237 56.8935 400.191 44.8481C388.146 32.8027 371.764 25.7594 344.048 22.0332C315.809 18.2366 278.651 18.1979 226.665 18.1979ZM31.9803 31.9803C0 63.9605 0 115.432 0 218.375V226.665C0 329.608 0 381.079 31.9803 413.059C63.9605 445.04 115.432 445.04 218.375 445.04H226.665C329.608 445.04 381.079 445.04 413.059 413.059C445.04 381.079 445.04 329.608 445.04 226.665V218.375C445.04 115.432 445.04 63.9605 413.059 31.9803C381.079 0 329.608 0 226.665 0H218.375C115.432 0 63.9605 0 31.9803 31.9803Z' }),
57
+ React.createElement('path', { fill: '#FFFFFF', d: 'M306.202 85.5845C276.837 85.5845 252.95 109.472 252.95 138.837V161.66H192.09V138.837C192.09 109.472 168.202 85.5845 138.837 85.5845C109.472 85.5845 85.5845 109.472 85.5845 138.837C85.5845 168.202 109.472 192.09 138.837 192.09H161.66V252.95H138.837C109.472 252.95 85.5845 276.837 85.5845 306.202C85.5845 335.568 109.472 359.455 138.837 359.455C168.202 359.455 192.09 335.568 192.09 306.202V283.38H252.95V306.202C252.95 335.568 276.837 359.455 306.202 359.455C335.567 359.455 359.455 335.568 359.455 306.202C359.455 276.837 335.567 252.95 306.202 252.95H283.38V192.09H306.202C335.567 192.09 359.455 168.202 359.455 138.837C359.455 109.472 335.567 85.5845 306.202 85.5845ZM283.38 161.66V138.837C283.38 126.209 293.574 116.015 306.202 116.015C318.831 116.015 329.025 126.209 329.025 138.837C329.025 151.466 318.831 161.66 306.202 161.66H283.38ZM138.837 161.66C126.209 161.66 116.015 151.466 116.015 138.837C116.015 126.209 126.209 116.015 138.837 116.015C151.466 116.015 161.66 126.209 161.66 138.837V161.66H138.837ZM192.09 252.95V192.09H252.95V252.95H192.09ZM306.202 329.025C293.574 329.025 283.38 318.831 283.38 306.202V283.38H306.202C318.831 283.38 329.025 293.574 329.025 306.202C329.025 318.831 318.831 329.025 306.202 329.025ZM138.837 329.025C126.209 329.025 116.015 318.831 116.015 306.202C116.015 293.574 126.209 283.38 138.837 283.38H161.66V306.202C161.66 318.831 151.466 329.025 138.837 329.025Z' }));
58
+
59
+ // AMD Radeon Cloud:官方 SVG logo(黑色方块底 + 白色箭头/AMD 字标),viewBox 裁切为正方形
60
+ // 来源:lib/assets/amd-radeon-logo.svg(由 amd-seeklogo.svg 修正 viewBox 后生成)
61
+ const LOGO_AMD_SVG = 'data:image/svg+xml,' + encodeURIComponent(`<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="580 280 200 200" style="display:block"><style>.st0{fill-rule:evenodd;clip-rule:evenodd}.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFF}</style><g><polygon class="st0" points="586,288 778.8,288 778.8,480.8 586,480.8"/><path class="st1" d="M658.2 461.3h-10.5l-3.2-7.7h-17.4l-2.9 7.7h-10.3l15.7-39h11.3l17.3 39zm-22.8-29.7l-5.7 15.2h12l-6.3-15.2z"/><polygon class="st1" points="696.2 422.3 704.7 422.3 704.7 461.3 695 461.3 695 437 684.4 449.2 682.9 449.2 672.4 437 672.4 461.3 662.7 461.3 662.7 422.3 671.2 422.3 683.7 436.8"/><path class="st1" d="M729.3 422.3c14.2 0 21.6 8.9 21.6 19.6 0 11.2-7.1 19.4-22.7 19.4h-16.1v-39h17.2zm-7.5 31.9h6.3c9.8 0 12.7-6.6 12.7-12.4 0-6.7-3.6-12.4-12.8-12.4h-6.2v24.8z"/><polygon class="st1" points="702.5 333.4 660.1 333.4 634 307.5 728.8 307.4 728.8 402.1 702.5 375.6 702.5 333.4"/><polygon class="st1" points="660.4 376.4 660.2 338.2 633.3 365 633.3 402.8 672.2 402.8 698.5 376.4 660.4 376.4"/></g></svg>`);
62
+ const LOGO_AMD = React.createElement('img', { src: LOGO_AMD_SVG, alt: '', style: { display: 'block', width: '100%', height: '100%' } });
63
+
48
64
  const zh = {
49
65
  loading: '查询中…',
50
66
  refresh: '刷新',
@@ -53,6 +69,7 @@ window.__ModuleLoader__.load({
53
69
  rolling: '5小时滚动',
54
70
  weekly: '7天(每周)',
55
71
  monthly: '每月',
72
+ daily: '今日',
56
73
  used: '已用',
57
74
  errorTitle: '无法获取用量',
58
75
  nav: '用量中心',
@@ -76,8 +93,8 @@ window.__ModuleLoader__.load({
76
93
  '.dsh-pu-card-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}',
77
94
  '.dsh-pu-logo{width:30px;height:30px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden;background:transparent}',
78
95
  '.dsh-pu-logo.go svg,.dsh-pu-logo.deepseek img{display:block}',
79
- '.dsh-pu-cc-logo{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,#5B3A9E,#8E5BD8);color:#fff;font-size:13px;font-weight:800;letter-spacing:.5px;font-family:inherit;flex:none}',
80
- '.dsh-pu-cc-logo-sm{width:22px;height:22px;border-radius:6px;font-size:10px}',
96
+ '.dsh-pu-cc-logo{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:8px;overflow:hidden;flex:none}',
97
+ '.dsh-pu-cc-logo-sm{width:22px;height:22px;border-radius:6px}',
81
98
  '.dsh-pu-cc-sub{display:flex;align-items:center;gap:10px;margin:-6px 0 6px;font-size:11px;color:#C9B8BE;white-space:nowrap}',
82
99
  '.dsh-pu-cc-sub .dsh-pu-cc-used{color:#EFE0E5;font-weight:600}',
83
100
  '.dsh-pu-cc-sub .dsh-pu-cc-remain{color:#FFB3C6;font-weight:600}',
@@ -119,6 +136,20 @@ window.__ModuleLoader__.load({
119
136
  '.zGbnIq_credentialDot.dsh-pu-dot-gray{background:var(--dsw-alias-label-tertiary,#8b93a7)}',
120
137
  '@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
138
  '.ms-plus{color:var(--dsw-alias-brand-primary);font-size:15px;line-height:1}',
139
+ // ── 提供方「启用/暂停」开关(官方 switch 视觉;开=启用=绿 / 关=暂停=灰)──
140
+ '.dsh-pu-switch{box-sizing:border-box;width:36px;height:20px;background:var(--dsw-alias-border-l3);cursor:pointer;border:0;border-radius:10px;flex:none;padding:2px;position:relative}',
141
+ '.dsh-pu-switch:hover:not(:disabled){background:var(--dsw-alias-border-l4)}',
142
+ '.dsh-pu-switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
143
+ '.dsh-pu-switch-on{background:var(--dsw-alias-state-success-primary)}',
144
+ '.dsh-pu-switch-on:hover:not(:disabled){background:var(--dsw-alias-state-success-primary)}',
145
+ '.dsh-pu-switch-thumb{box-sizing:border-box;background:var(--dsw-alias-label-primary-foreground);border-radius:50%;width:16px;height:16px;display:block;transition:transform .12s}',
146
+ '.dsh-pu-switch-on .dsh-pu-switch-thumb{transform:translate(16px)}',
147
+ '.zGbnIq_editorSwitchRow{align-items:center;gap:10px;display:flex}',
148
+ '.dsh-pu-switchText{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}',
149
+ // ── 提供方列表:暂停行(灰点 + 名称变灰,编辑/删除保持可用)──
150
+ '.zGbnIq_rowNameDim{color:var(--dsw-alias-label-tertiary)}',
151
+ '.zGbnIq_rowCard .zGbnIq_credentialDot.dsh-pu-dot-paused{background:var(--dsw-alias-label-tertiary,#8b93a7)}',
152
+ '.zGbnIq_rowPaused .zGbnIq_rowIdentity .zGbnIq_rowName{color:var(--dsw-alias-label-tertiary)}',
122
153
  ].join('');
123
154
  var tag = document.createElement('style');
124
155
  tag.dataset.plugin = 'dsh-provider-usage';
@@ -169,35 +200,67 @@ window.__ModuleLoader__.load({
169
200
  return s;
170
201
  }
171
202
 
172
- // ── 供应商实例清单(localStorage,即时生效)──────────────────────
173
- function loadProviders() {
203
+ // ── 供应商实例清单(v0.8.0:host 端 providers.json 为单一事实源,localStorage 只是写穿透缓存)──
204
+ // 修复「重启后提供方清单被静默重建」:此前清单只存 localStorage,存储 key 一旦丢失
205
+ // (换 origin/清站点数据/浏览器驱逐临时存储),启动就走 DEFAULT_PROVIDERS 兜底 →
206
+ // 已删除的默认实例(OpenCode Go/DeepSeek)复活、自定义实例(如 Command Code)永久消失。
207
+ // 现在启动时从 host 恢复;host 无数据时才一次性迁移本地缓存/默认清单并立即写回 host。
208
+ function normalizeProviders(arr) {
209
+ if (!Array.isArray(arr)) return null;
210
+ return arr.filter(function (p) {
211
+ return p && typeof p === 'object' && typeof p.adapter === 'string' && ADAPTER_META[p.adapter];
212
+ }).map(function (p) {
213
+ var name = typeof p.name === 'string' && p.name ? p.name : (ADAPTER_META[p.adapter] || {}).displayName;
214
+ if (String(p.id || '') === 'deepseek-balance' && name === 'DeepSeek 余额') name = 'DeepSeek'; // 0.4.0 更名
215
+ // source:Key 解析位置(vault=插件私有库直取;dsh=DSH 凭证链);type:创建方式(import=导入 / manual=自定义)
216
+ var source = p.source === 'vault' ? 'vault' : 'dsh';
217
+ // 显式 type 优先;旧数据无 type 时由 source 推导(vault→manual,其余→import)
218
+ var type = p.type === 'manual' || p.type === 'import' ? p.type : (source === 'vault' ? 'manual' : 'import');
219
+ // paused:true=暂停(不轮询查询、卡片菜单不可选);旧数据无该字段 = false(启用)
220
+ return { id: String(p.id || ''), name: name, adapter: p.adapter, ref: typeof p.ref === 'string' ? p.ref : '', source: source, type: type, paused: p.paused === true };
221
+ });
222
+ }
223
+ function readProvidersCache() {
174
224
  try {
175
- var arr = JSON.parse(window.localStorage.getItem(PROVIDERS_KEY) || 'null');
176
- if (Array.isArray(arr)) {
177
- if (arr.length === 0) return []; // 用户删光后保持空清单,不复活默认实例
178
- return arr.filter(function (p) {
179
- return p && typeof p === 'object' && typeof p.adapter === 'string' && ADAPTER_META[p.adapter];
180
- }).map(function (p) {
181
- var name = typeof p.name === 'string' && p.name ? p.name : (ADAPTER_META[p.adapter] || {}).displayName;
182
- if (String(p.id || '') === 'deepseek-balance' && name === 'DeepSeek 余额') name = 'DeepSeek'; // 0.4.0 更名
183
- // source:Key 解析位置(vault=插件私有库直取;dsh=DSH 凭证链);type:创建方式(import=导入 / manual=自定义)
184
- var source = p.source === 'vault' ? 'vault' : 'dsh';
185
- // 显式 type 优先;旧数据无 type 时由 source 推导(vault→manual,其余→import)
186
- var type = p.type === 'manual' || p.type === 'import' ? p.type : (source === 'vault' ? 'manual' : 'import');
187
- return { id: String(p.id || ''), name: name, adapter: p.adapter, ref: typeof p.ref === 'string' ? p.ref : '', source: source, type: type };
188
- });
189
- }
225
+ return normalizeProviders(JSON.parse(window.localStorage.getItem(PROVIDERS_KEY) || 'null'));
190
226
  } catch (e) { /* ignore */ }
191
- return DEFAULT_PROVIDERS.slice();
227
+ return null;
228
+ }
229
+ function saveProvidersCache(list) {
230
+ try { window.localStorage.setItem(PROVIDERS_KEY, JSON.stringify(list)); } catch (e) { /* ignore */ }
192
231
  }
193
- var providersState = loadProviders();
232
+ // host 恢复完成前的乐观缓存:优先用 localStorage 旧数据(多数场景与 host 一致),否则默认清单
233
+ var providersState = readProvidersCache() || DEFAULT_PROVIDERS.slice();
194
234
  var providersListeners = new Set();
235
+ // 本次页面会话内的写入计数:恢复响应迟到时,若期间用户已写入则放弃应用 host 过期数据(防复活)
236
+ var providersMutations = 0;
195
237
  function saveProviders() {
196
- try { window.localStorage.setItem(PROVIDERS_KEY, JSON.stringify(providersState)); } catch (e) { /* ignore */ }
238
+ // 写穿透:localStorage 缓存 + host 落盘(fire-and-forget;host 失联时本地缓存仍可用)
239
+ saveProvidersCache(providersState);
240
+ try {
241
+ fetch('/api/provider-usage/providers', {
242
+ method: 'POST',
243
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
244
+ body: JSON.stringify({ providers: providersState }),
245
+ }).catch(function () { /* ignore */ });
246
+ } catch (e) { /* ignore */ }
247
+ }
248
+ // 内部应用(不记写入数):host 恢复/一次性迁移专用
249
+ function applyProvidersUpdate(update) {
250
+ var next = typeof update === 'function' ? update(providersState) : update;
251
+ providersState = next;
252
+ saveProviders();
253
+ providersListeners.forEach(function (fn) { fn(); });
197
254
  }
198
255
  function getProviders() { return providersState; }
199
- function setProviders(list) {
200
- providersState = list;
256
+ // v0.8.0 修复(丢更新竞态):所有写方共享同一份 providersState 快照,整体覆盖式写入
257
+ // 在写入交错时会把先写的整单覆盖掉(如连删两个实例会复活第一个)。
258
+ // 现支持函数式更新:setProviders(function (prev) { return next; }) —— next 基于写入时刻的最新状态计算;
259
+ // 兼容旧用法:传数组视为恒等函数。
260
+ function setProviders(update) {
261
+ providersMutations += 1;
262
+ var next = typeof update === 'function' ? update(providersState) : update;
263
+ providersState = next;
201
264
  saveProviders();
202
265
  providersListeners.forEach(function (fn) { fn(); });
203
266
  }
@@ -205,15 +268,43 @@ window.__ModuleLoader__.load({
205
268
  providersListeners.add(fn);
206
269
  return function () { providersListeners.delete(fn); };
207
270
  }
271
+ // host 恢复只执行一次(首个订阅者触发;fetch 去重)
272
+ var hostRestoreStarted = false;
208
273
  function useProviders() {
209
274
  var [p, setP] = useState(getProviders);
210
- useEffect(function () { return subscribeProviders(function () { setP(getProviders()); }); }, []);
275
+ useEffect(function () {
276
+ var unsub = subscribeProviders(function () { setP(getProviders()); });
277
+ if (!hostRestoreStarted) {
278
+ hostRestoreStarted = true;
279
+ fetch('/api/provider-usage/providers', { headers: { accept: 'application/json' } })
280
+ .then(function (r) { return r.json().catch(function () { return null; }); })
281
+ .then(function (j) {
282
+ if (!j || !j.ok) return;
283
+ // 恢复期间用户已做过修改(增删改实例)→ 放弃 host 过期数据,避免把用户刚删的实例复活
284
+ if (providersMutations > 0) return;
285
+ if (j.exists && Array.isArray(j.providers)) {
286
+ // 主路径:host 有清单 → 恢复(host 为单一事实源;本地缓存仅作 host 失联时的乐观值)
287
+ applyProvidersUpdate(j.providers);
288
+ } else {
289
+ // 一次性迁移:host 无文件(首次升级到 0.8.0)→ 把当前缓存清单写回 host 固化
290
+ applyProvidersUpdate(getProviders());
291
+ }
292
+ })
293
+ .catch(function () { /* host 失联:继续用本地缓存,后续写操作会重试落盘 */ });
294
+ }
295
+ return unsub;
296
+ }, []);
211
297
  return p;
212
298
  }
213
- /** 当前选中实例;若被删除则回落到第一个。 */
299
+ /** 可用(未暂停)的实例清单:卡片轮询与切换只在这些实例里进行。 */
300
+ function activeProviders(providers) {
301
+ return providers.filter(function (p) { return !p.paused; });
302
+ }
303
+ /** 当前选中实例;若被删除或已暂停则回落到第一个可用实例(全暂停/无实例 → null)。 */
214
304
  function currentProvider(providers, providerId) {
215
- for (var i = 0; i < providers.length; i++) if (providers[i].id === providerId) return providers[i];
216
- return providers[0] || null;
305
+ var actives = activeProviders(providers);
306
+ for (var i = 0; i < actives.length; i++) if (actives[i].id === providerId) return actives[i];
307
+ return actives[0] || null;
217
308
  }
218
309
 
219
310
  // ── 工具 ──────────────────────────────────────────────────────────
@@ -326,7 +417,7 @@ window.__ModuleLoader__.load({
326
417
  return function () { window.removeEventListener('resize', onResize); };
327
418
  }, [s.right, s.bottom, s.visible, data]);
328
419
  var current = currentProvider(providers, s.providerId);
329
- var multiple = providers.length > 1;
420
+ var multiple = activeProviders(providers).length > 1;
330
421
  var curKey = current ? current.id + '|' + current.adapter + '|' + current.ref : 'none';
331
422
 
332
423
  var load = useCallback(function (force) {
@@ -372,12 +463,15 @@ window.__ModuleLoader__.load({
372
463
  var meta = data && data.fetchedAt ? fmtTime(data.fetchedAt) + (data.stale ? ' · ' + zh.stale : '') : zh.loading;
373
464
  var isBalance = current.adapter === 'balance-json';
374
465
  var isCommandCode = current.adapter === 'commandcode-credits';
375
- var logoClass = isBalance ? 'deepseek' : (isCommandCode ? 'commandcode' : 'go');
466
+ var isAmd = current.adapter === 'amd-radeon-usage';
467
+ var logoClass = isBalance ? 'deepseek' : (isCommandCode ? 'commandcode' : (isAmd ? 'amd' : 'go'));
376
468
  var logoEl = isBalance
377
469
  ? React.createElement('img', { src: LOGO_DEEPSEEK, width: 24, height: 24, alt: '' })
378
470
  : (isCommandCode
379
- ? React.createElement('span', { className: 'dsh-pu-cc-logo', 'aria-hidden': 'true' }, 'CC')
380
- : LOGO_OCGO);
471
+ ? React.createElement('span', { className: 'dsh-pu-cc-logo', 'aria-hidden': 'true' }, LOGO_CC)
472
+ : (isAmd
473
+ ? React.createElement('span', { className: 'dsh-pu-cc-logo', 'aria-hidden': 'true' }, LOGO_AMD)
474
+ : LOGO_OCGO));
381
475
 
382
476
  var body = null;
383
477
  if (loading && !data) {
@@ -399,38 +493,44 @@ window.__ModuleLoader__.load({
399
493
  (data.monthly && data.monthly.usedPct !== null
400
494
  ? React.createElement(WindowRow, { key: 'm', label: zh.monthly, win: data.monthly, bar: 'magenta' })
401
495
  : null)].filter(Boolean)
402
- : [React.createElement(WindowRow, { key: 'r', label: zh.rolling, win: data.windows.rolling, bar: 'pink' }),
403
- React.createElement(WindowRow, { key: 'w', label: zh.weekly, win: data.windows.weekly, bar: 'orange' }),
404
- React.createElement(WindowRow, { key: 'm', label: zh.monthly, win: data.windows.monthly, bar: 'magenta' })])
496
+ : (isAmd
497
+ // AMD:单日窗口进度条(对齐网页 Today Used / Remaining;无周/月)
498
+ ? [React.createElement(WindowRow, { key: 'd', label: zh.daily, win: data.windows.daily, bar: 'magenta' })]
499
+ : [React.createElement(WindowRow, { key: 'r', label: zh.rolling, win: data.windows.rolling, bar: 'pink' }),
500
+ React.createElement(WindowRow, { key: 'w', label: zh.weekly, win: data.windows.weekly, bar: 'orange' }),
501
+ React.createElement(WindowRow, { key: 'm', label: zh.monthly, win: data.windows.monthly, bar: 'magenta' })]))
405
502
  : null;
406
503
  var warn = data.isValid === false && data.invalidMessage
407
504
  ? React.createElement('div', { className: 'dsh-pu-error' }, data.invalidMessage)
408
505
  : null;
409
- // Command Code 混合卡:主数字 = 月剩余百分比(3 位小数向下取整),副行 = 已用/总额 + 剩余额度
506
+ // Command Code / AMD 混合卡:主数字 = 剩余百分比(3 位小数向下取整),副行 = 已用/总额 + 剩余额度
507
+ // Command Code 读 monthly.remainingPct;AMD 读 windows.daily.remainingPct(今日)
410
508
  var cc = null;
411
- if (isCommandCode) {
412
- var monthlyInfo = data.monthly || null;
509
+ var remainPct = null;
510
+ if (isCommandCode || isAmd) {
511
+ var pctInfo = isAmd ? (data.windows && data.windows.daily) : (data.monthly || null);
413
512
  // 新服务端直接给 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
513
+ remainPct = pctInfo
514
+ ? (pctInfo.remainingPct !== undefined && pctInfo.remainingPct !== null
515
+ ? pctInfo.remainingPct
516
+ : (pctInfo.usedPct === undefined || pctInfo.usedPct === null
418
517
  ? null
419
- : Math.floor(Math.max(0, Math.min(100, 100 - monthlyInfo.usedPct)) * 1000) / 1000))
518
+ : Math.floor(Math.max(0, Math.min(100, 100 - pctInfo.usedPct)) * 1000) / 1000))
420
519
  : null;
421
- var usedStr = data.used === null ? '--' : Number(data.used).toFixed(2);
422
- var quotaStr = data.totalQuota === null ? '--' : Number(data.totalQuota).toFixed(2);
423
- var remainStr = data.remaining === null ? '--' : Number(data.remaining).toFixed(2);
520
+ var unitSym = isAmd ? '' : '$';
521
+ var usedStr = data.used === null ? '--' : Number(data.used).toFixed(isAmd ? 3 : 2);
522
+ var quotaStr = data.totalQuota === null ? '--' : Number(data.totalQuota).toFixed(isAmd ? 3 : 2);
523
+ var remainStr = data.remaining === null ? '--' : Number(data.remaining).toFixed(isAmd ? 3 : 2);
424
524
  cc = React.createElement('div', { className: 'dsh-pu-cc-sub' },
425
- React.createElement('span', { className: 'dsh-pu-cc-used' }, '已用 $' + usedStr + ' / $' + quotaStr),
426
- React.createElement('span', { className: 'dsh-pu-cc-remain' }, '剩余 $' + remainStr));
525
+ React.createElement('span', { className: 'dsh-pu-cc-used' }, (isAmd ? zh.daily + ' ' : '') + '已用 ' + unitSym + usedStr + ' / ' + unitSym + quotaStr + (isAmd ? ' pts' : '')),
526
+ React.createElement('span', { className: 'dsh-pu-cc-remain' }, (isAmd ? zh.daily + ' ' : '') + '剩余 ' + unitSym + remainStr + (isAmd ? ' pts' : '')));
427
527
  }
428
528
  // 窗口行/主值/警告条已覆盖全部信息,extra 全文不再展示(保持卡片干净)
429
529
  var extra = null;
430
530
  body = React.createElement('div', null,
431
531
  React.createElement('div', { className: 'dsh-pu-remain' },
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)),
532
+ React.createElement('span', { className: 'dsh-pu-remain-big' }, (isCommandCode || isAmd) ? (remainPct === null ? '--' : remainPct.toFixed(3) + '%') : main),
533
+ React.createElement('span', { className: 'dsh-pu-remain-label' }, (isCommandCode || isAmd) ? ' ' + zh.remaining + '(' + (isAmd ? zh.daily : zh.monthly) + ')' : ' ' + mainLabel)),
434
534
  warn,
435
535
  cc,
436
536
  winRows,
@@ -440,13 +540,15 @@ window.__ModuleLoader__.load({
440
540
  var menu = null;
441
541
  if (multiple && menuOpen) {
442
542
  menu = React.createElement('div', { ref: menuRef, className: 'dsh-pu-menu' },
443
- providers.map(function (p) {
543
+ activeProviders(providers).map(function (p) {
444
544
  var active = p.id === current.id;
445
545
  var mini = p.adapter === 'balance-json'
446
546
  ? React.createElement('img', { src: LOGO_DEEPSEEK, width: 16, height: 16, alt: '' })
447
547
  : (p.adapter === 'commandcode-credits'
448
- ? React.createElement('span', { className: 'dsh-pu-cc-logo dsh-pu-cc-logo-sm', 'aria-hidden': 'true' }, 'CC')
449
- : LOGO_OCGO);
548
+ ? React.createElement('span', { className: 'dsh-pu-cc-logo dsh-pu-cc-logo-sm', 'aria-hidden': 'true' }, LOGO_CC)
549
+ : (p.adapter === 'amd-radeon-usage'
550
+ ? React.createElement('span', { className: 'dsh-pu-cc-logo dsh-pu-cc-logo-sm', 'aria-hidden': 'true' }, LOGO_AMD)
551
+ : LOGO_OCGO));
450
552
  return React.createElement('button', {
451
553
  key: p.id, type: 'button', className: 'dsh-pu-menu-item' + (active ? ' active' : ''),
452
554
  onClick: function (e) { if (draggedRef.current) { draggedRef.current = false; return; } e.stopPropagation(); setSettings({ providerId: p.id }); setMenuOpen(false); },
@@ -767,6 +869,8 @@ window.__ModuleLoader__.load({
767
869
  var [queryStatus, setQueryStatus] = useState({});
768
870
  var [errOpen, setErrOpen] = useState({}); // 展开的错误详情(按实例 id)
769
871
  var refsKey = providers.map(function (p) { return p.ref || ''; }).join(',');
872
+ // 暂停态也进依赖:切「恢复/暂停」后让设置页状态查询重新跑(暂停的不查、恢复的马上查)
873
+ var refsPauseKey = providers.map(function (p) { return (p.paused ? '1' : '0') + ':' + (p.ref || ''); }).join(',');
770
874
  // 添加卡:mode = null | 'import' | 'manual';编辑卡:edit = null | { id, kind, adapter, ref }
771
875
  var [mode, setMode] = useState(null);
772
876
  var [edit, setEdit] = useState(null);
@@ -777,14 +881,22 @@ window.__ModuleLoader__.load({
777
881
  var [formError, setFormError] = useState(null);
778
882
 
779
883
  function removeProvider(p) {
780
- var next = providers.filter(function (x) { return x.id !== p.id; });
781
884
  if (p.source === 'vault' && p.ref) {
782
885
  // 删除自定义实例时同步清理插件私有库中的 Key(fire-and-forget)
783
886
  fetch('/api/provider-usage/credentials?ref=' + encodeURIComponent(p.ref), { headers: { accept: 'application/json' } })
784
887
  .catch(function () { /* ignore */ });
785
888
  }
786
- setProviders(next);
787
- if ((s.providerId || '') === p.id) setSettings({ providerId: next.length > 0 ? next[0].id : '' });
889
+ // v0.8.0:函数式更新 —— 基于写入时刻的最新清单计算,连删/交错写入不再互相覆盖
890
+ var next = null;
891
+ setProviders(function (prev) {
892
+ next = prev.filter(function (x) { return x.id !== p.id; });
893
+ return next;
894
+ });
895
+ // 若当前卡片实例被删除,回落到第一个可用实例(跳过已暂停的)
896
+ if (currentProvider(next, s.providerId || '') === null) {
897
+ var first = activeProviders(next)[0];
898
+ setSettings({ providerId: first ? first.id : '' });
899
+ }
788
900
  }
789
901
 
790
902
  function openImport() {
@@ -807,7 +919,7 @@ window.__ModuleLoader__.load({
807
919
  setMode('manual');
808
920
  setEdit(null);
809
921
  setFormError(null);
810
- setForm({ provider: '', name: 'Command Code', adapter: 'commandcode-credits', key: '', nameTouched: false });
922
+ setForm({ provider: '', name: 'AMD Radeon Cloud (中国)', adapter: 'amd-radeon-usage', key: '', nameTouched: false });
811
923
  }
812
924
  function cancelAdd() { setMode(null); setEdit(null); setFormError(null); }
813
925
 
@@ -818,8 +930,8 @@ window.__ModuleLoader__.load({
818
930
  var item = null;
819
931
  for (var i = 0; i < dshItems.length; i++) if (dshItems[i].route === form.provider) item = dshItems[i];
820
932
  if (!item) { setFormError('导入项不存在'); return; }
821
- var inst = { id: item.route + '-' + Date.now(), name: name, adapter: item.adapter, ref: item.ref, source: 'dsh', type: 'import' };
822
- setProviders(providers.concat([inst]));
933
+ var inst = { id: item.route + '-' + Date.now(), name: name, adapter: item.adapter, ref: item.ref, source: 'dsh', type: 'import', paused: false };
934
+ setProviders(function (prev) { return prev.concat([inst]); });
823
935
  setSettings({ providerId: inst.id });
824
936
  setMode(null);
825
937
  }
@@ -840,8 +952,8 @@ window.__ModuleLoader__.load({
840
952
  });
841
953
  var j = await resp.json().catch(function () { return null; });
842
954
  if (!j || !j.ok) { setFormError((j && j.error && j.error.message) || '保存 Key 失败'); return; }
843
- var inst = { id: instId, name: name, adapter: form.adapter, ref: instId, source: 'vault', type: 'manual' };
844
- setProviders(providers.concat([inst]));
955
+ var inst = { id: instId, name: name, adapter: form.adapter, ref: instId, source: 'vault', type: 'manual', paused: false };
956
+ setProviders(function (prev) { return prev.concat([inst]); });
845
957
  setSettings({ providerId: inst.id });
846
958
  setMode(null);
847
959
  } catch (e) {
@@ -851,11 +963,12 @@ window.__ModuleLoader__.load({
851
963
  }
852
964
  }
853
965
 
854
- // ── 编辑:导入的只改名称;自定义(vault)可改名称 + 覆盖 Key ──
966
+ // ── 编辑:导入的只改名称 + 启用状态;自定义(vault)可改名称 + 覆盖 Key + 启用状态 ──
967
+ // enabled = 表单暂存值(false 表示“暂停”);开关只暂存在表单里,点「保存」才真正写入实例
855
968
  function startEdit(p) {
856
969
  setMode(null);
857
- setEdit({ id: p.id, kind: p.type === 'manual' ? 'manual' : 'import', adapter: p.adapter, ref: p.ref });
858
- setForm({ provider: '', name: p.name, adapter: p.adapter, key: '', ref: p.ref, nameTouched: true });
970
+ setEdit({ id: p.id, kind: p.type === 'manual' ? 'manual' : 'import', adapter: p.adapter, ref: p.ref, paused: !!p.paused });
971
+ setForm({ provider: '', name: p.name, adapter: p.adapter, key: '', ref: p.ref, nameTouched: true, enabled: !p.paused, _editId: p.id });
859
972
  setFormError(null);
860
973
  }
861
974
 
@@ -883,10 +996,30 @@ window.__ModuleLoader__.load({
883
996
  setBusy(false);
884
997
  }
885
998
  }
886
- setProviders(providers.map(function (p) { return p.id === edit.id ? Object.assign({}, p, { name: name }) : p; }));
999
+ // 名称 + 启用状态在同一次 setProviders 里写入(避免两次基于旧数组的写入互相覆盖)
1000
+ var enabled2 = form.enabled === false ? false : true;
1001
+ // v0.8.0:函数式更新 —— next 基于写入时刻的最新清单计算,不再依赖渲染闭包的旧 providers
1002
+ var next = null;
1003
+ setProviders(function (prev) {
1004
+ next = prev.map(function (p) {
1005
+ return p.id === edit.id ? Object.assign({}, p, { name: name, paused: !enabled2 }) : p;
1006
+ });
1007
+ return next;
1008
+ });
1009
+ // 卡片当前实例处理(基于更新后的 next):
1010
+ // - 停用的正是当前卡片实例 → 显式切到下一个仍启用的(currentProvider 会自动回落,不能依赖它判空)
1011
+ // - 恢复某实例且此前没有任何可用实例(卡片隐藏态)→ 恢复的这个直接成为卡片当前实例
1012
+ if (!enabled2 && (s.providerId || '') === edit.id) {
1013
+ var fallback = null;
1014
+ var act = activeProviders(next);
1015
+ for (var i = 0; i < act.length; i++) if (act[i].id !== edit.id) { fallback = act[i]; break; }
1016
+ setSettings({ providerId: fallback ? fallback.id : '' });
1017
+ } else if (enabled2 && !currentProvider(next, s.providerId || '')) {
1018
+ setSettings({ providerId: edit.id });
1019
+ }
887
1020
  if (keyChanged) setNonce(function (n) { return n + 1; });
888
1021
  setEdit(null);
889
- setForm({ provider: '', name: '', adapter: 'usage-percent', key: '', ref: '', nameTouched: false });
1022
+ setForm({ provider: '', name: '', adapter: 'usage-percent', key: '', ref: '', nameTouched: false, enabled: true, _editId: null });
890
1023
  }
891
1024
 
892
1025
  // ── 状态查询(打开设置页时:describe + 每实例实际查询)──
@@ -900,6 +1033,7 @@ window.__ModuleLoader__.load({
900
1033
  var jobs = [];
901
1034
  providers.forEach(function (p) {
902
1035
  if (!p.ref) return;
1036
+ if (p.paused) return; // 已暂停的实例不做任何查询(设置页打开时也只显示「已暂停」灰点)
903
1037
  if (configuredMap && !configuredMap[p.ref]) return;
904
1038
  var key = p.adapter + '|' + p.ref;
905
1039
  setQueryStatus(function (prev) {
@@ -941,13 +1075,16 @@ window.__ModuleLoader__.load({
941
1075
  j.refs.forEach(function (x) { map[x.name] = !!x.configured; storeMap[x.name] = x.store; });
942
1076
  setRefStatus(map);
943
1077
  // 旧数据自愈:source 非 vault 但 Key 只存在插件私有库 → 升级为手动实例(可改名称+Key)
944
- var changed = false;
945
- var next = providers.map(function (p) {
946
- if (p.source === 'vault' || !p.ref) return p;
947
- if (storeMap[p.ref] === 'vault') { changed = true; return Object.assign({}, p, { source: 'vault' }); }
948
- return p;
1078
+ // v0.8.0:函数式更新,避免覆盖恢复/添加等交错写入
1079
+ setProviders(function (prev) {
1080
+ var changed = false;
1081
+ var next = prev.map(function (p) {
1082
+ if (p.source === 'vault' || !p.ref) return p;
1083
+ if (storeMap[p.ref] === 'vault') { changed = true; return Object.assign({}, p, { source: 'vault' }); }
1084
+ return p;
1085
+ });
1086
+ return changed ? next : prev;
949
1087
  });
950
- if (changed) setProviders(next);
951
1088
  return startQueries(map, nonce > 0);
952
1089
  }
953
1090
  return startQueries(null, nonce > 0);
@@ -958,7 +1095,7 @@ window.__ModuleLoader__.load({
958
1095
  startQueries(null, nonce > 0);
959
1096
  });
960
1097
  return function () { alive = false; };
961
- }, [refsKey, nonce]);
1098
+ }, [refsKey, refsPauseKey, nonce]);
962
1099
 
963
1100
  var adapterOptions = Object.keys(ADAPTER_META).map(function (a) {
964
1101
  return React.createElement('option', { key: a, value: a }, ADAPTER_META[a].displayName);
@@ -1009,7 +1146,7 @@ window.__ModuleLoader__.load({
1009
1146
  className: 'zGbnIq_input zGbnIq_selectInput', value: form.adapter,
1010
1147
  onChange: function (e) {
1011
1148
  var next = Object.assign({}, form, { adapter: e.target.value });
1012
- if (!form.nameTouched) next.name = e.target.value === 'balance-json' ? 'DeepSeek' : (e.target.value === 'commandcode-credits' ? 'Command Code' : 'OpenCode Go');
1149
+ if (!form.nameTouched) next.name = e.target.value === 'balance-json' ? 'DeepSeek' : (e.target.value === 'commandcode-credits' ? 'Command Code' : (e.target.value === 'amd-radeon-usage' ? 'AMD Radeon Cloud (中国)' : 'OpenCode Go'));
1013
1150
  setForm(next);
1014
1151
  },
1015
1152
  }, adapterOptions)),
@@ -1033,6 +1170,19 @@ window.__ModuleLoader__.load({
1033
1170
  React.createElement('button', { type: 'button', className: 'zGbnIq_primaryButton', disabled: busy, onClick: saveManual }, '保存')));
1034
1171
  } else if (edit) {
1035
1172
  var editManual = edit.kind === 'manual';
1173
+ var enabled = form.enabled === false ? false : true;
1174
+ var enableToggle = React.createElement('div', { className: 'zGbnIq_field' },
1175
+ React.createElement('span', { className: 'zGbnIq_fieldLabel' }, '启用'),
1176
+ React.createElement('label', { className: 'zGbnIq_editorSwitchRow' },
1177
+ React.createElement('button', {
1178
+ type: 'button', role: 'switch',
1179
+ 'aria-checked': enabled ? 'true' : 'false',
1180
+ 'aria-label': enabled ? ('已启用 ' + form.name + ',点击暂停') : ('已暂停 ' + form.name + ',点击启用'),
1181
+ className: 'dsh-pu-switch' + (enabled ? ' dsh-pu-switch-on' : ''),
1182
+ onClick: function () { setForm(Object.assign({}, form, { enabled: !enabled })); },
1183
+ }, React.createElement('span', { className: 'dsh-pu-switch-thumb' })),
1184
+ React.createElement('span', { className: 'dsh-pu-switchText' }, enabled ? '已启用' : '已暂停')),
1185
+ React.createElement('span', { className: 'zGbnIq_advancedHint' }, enabled ? '启用中:正常轮询查询,卡片可切换选择' : '已暂停:不再轮询查询,卡片菜单中不可选(Key 与配置保留)'));
1036
1186
  addCard = React.createElement('div', { className: 'zGbnIq_editor' },
1037
1187
  React.createElement('div', { className: 'zGbnIq_editorHeader' },
1038
1188
  React.createElement('span', { className: 'zGbnIq_editorTitle' }, editManual ? '编辑自定义提供方' : '编辑提供方')),
@@ -1048,6 +1198,7 @@ window.__ModuleLoader__.load({
1048
1198
  className: 'zGbnIq_input', value: form.name,
1049
1199
  onChange: function (e) { setForm(Object.assign({}, form, { name: e.target.value, nameTouched: true })); },
1050
1200
  })),
1201
+ enableToggle,
1051
1202
  editManual ? React.createElement('div', { className: 'zGbnIq_field' },
1052
1203
  React.createElement('span', { className: 'zGbnIq_fieldLabel' }, 'API 密钥'),
1053
1204
  React.createElement('input', {
@@ -1094,7 +1245,8 @@ window.__ModuleLoader__.load({
1094
1245
  : providers.map(function (pl) {
1095
1246
  var qs = pl.ref ? queryStatus[pl.adapter + '|' + pl.ref] : null;
1096
1247
  var refKnownRow = pl.ref ? Object.prototype.hasOwnProperty.call(refStatus, pl.ref) : true;
1097
- var status = pl.ref ? statusOf(refKnownRow, refCheckFailed, !!refStatus[pl.ref], qs) : null;
1248
+ // 已暂停的实例不再轮询/查询,状态点固定为「已暂停」灰点,查询状态一律不展示
1249
+ var status = (pl.paused || !pl.ref) ? null : statusOf(refKnownRow, refCheckFailed, !!refStatus[pl.ref], qs);
1098
1250
  return React.createElement(ProviderRow, {
1099
1251
  key: pl.id,
1100
1252
  provider: pl,
@@ -1142,7 +1294,7 @@ window.__ModuleLoader__.load({
1142
1294
  usagePanel);
1143
1295
  }
1144
1296
  // 提供方单行:状态点 + 名称 + 编辑/删除 + 展开详情
1145
- // 状态判定全部交给 statusOf;本组件只做渲染
1297
+ // 状态判定全部交给 statusOf;本组件只做渲染;启用/暂停开关在编辑弹层里管理
1146
1298
  function ProviderRow(props) {
1147
1299
  var pl = props.provider;
1148
1300
  var status = props.status;
@@ -1150,24 +1302,36 @@ window.__ModuleLoader__.load({
1150
1302
  var onToggle = props.onToggle;
1151
1303
  var onEdit = props.onEdit;
1152
1304
  var onRemove = props.onRemove;
1153
- var dotEl = status.dotCls ? React.createElement('span', {
1154
- className: 'zGbnIq_credentialDot' + status.dotCls,
1155
- role: 'img', 'aria-label': status.label,
1156
- style: status.clickable ? { cursor: 'pointer' } : undefined,
1157
- onClick: status.clickable ? onToggle : undefined,
1158
- }) : null;
1305
+ var paused = pl.paused === true;
1306
+ var dotEl;
1307
+ if (paused) {
1308
+ // 已暂停:灰点「已暂停」,不可展开(暂停时不再查询,无新鲜结果/错误可看)
1309
+ dotEl = React.createElement('span', {
1310
+ className: 'zGbnIq_credentialDot dsh-pu-dot-paused',
1311
+ role: 'img', 'aria-label': '已暂停',
1312
+ });
1313
+ } else if (status && status.dotCls) {
1314
+ dotEl = React.createElement('span', {
1315
+ className: 'zGbnIq_credentialDot' + status.dotCls,
1316
+ role: 'img', 'aria-label': status.label,
1317
+ style: status.clickable ? { cursor: 'pointer' } : undefined,
1318
+ onClick: status.clickable ? onToggle : undefined,
1319
+ });
1320
+ } else {
1321
+ dotEl = null;
1322
+ }
1159
1323
  var detailEl = null;
1160
- if (expanded && status.detailKind) {
1324
+ if (!paused && expanded && status && status.detailKind) {
1161
1325
  if (status.detailKind === 'err') {
1162
1326
  detailEl = React.createElement('p', { className: 'zGbnIq_error', style: { margin: '10px 0 0' } }, status.detailText);
1163
1327
  } else if (status.detailKind === 'resp') {
1164
1328
  detailEl = React.createElement('div', { className: 'zGbnIq_json' }, status.detailText);
1165
1329
  }
1166
1330
  }
1167
- return React.createElement('li', { key: pl.id, className: 'zGbnIq_rowCard' },
1331
+ return React.createElement('li', { key: pl.id, className: 'zGbnIq_rowCard' + (paused ? ' zGbnIq_rowPaused' : '') },
1168
1332
  React.createElement('div', { className: 'zGbnIq_rowHead' },
1169
1333
  React.createElement('span', { className: 'zGbnIq_rowIdentity' },
1170
- React.createElement('span', { className: 'zGbnIq_rowName' }, pl.name),
1334
+ React.createElement('span', { className: 'zGbnIq_rowName' + (paused ? ' zGbnIq_rowNameDim' : '') }, pl.name),
1171
1335
  dotEl),
1172
1336
  React.createElement('span', { className: 'zGbnIq_rowActions' },
1173
1337
  React.createElement('button', { type: 'button', className: 'zGbnIq_secondaryButton', onClick: onEdit }, '编辑'),
@@ -1196,6 +1360,8 @@ window.__ModuleLoader__.load({
1196
1360
  try {
1197
1361
  if (!getSettings().visible) return; // 用户主动隐藏,不自动复活
1198
1362
  if (getProviders().length === 0) return; // 无提供方实例:卡片本就渲染 null,避免 10s 空转重挂
1363
+ // 全部已暂停同理:currentProvider 回落为 null → 卡片渲染 null,无需重挂
1364
+ if (!currentProvider(getProviders(), getSettings().providerId)) return;
1199
1365
  if (rootEl && rootEl.isConnected && document.querySelector('.dsh-pu-card')) return;
1200
1366
  mount();
1201
1367
  } catch (e) {
package/lib/index.js CHANGED
@@ -4,14 +4,15 @@
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 / reset-local-stats(仅回环)。
7
+ // /api/provider-usage/templates / credentials / dsh-providers / credential-refs / reset-local-stats(仅回环)、
8
+ // /api/provider-usage/providers(提供方实例清单持久化,v0.8.0 —— host 端 providers.json 为单一事实源)。
8
9
  // 本地 Token 统计(v0.5.0):/api/provider-usage/local-usage(累计+按模型汇总,按天物化存储),
9
10
  // 数据源 = 自读 $DSH_HOME/sessions 会话文件(fzstd 解压,assistant/message 事件 data.usage),只读、不联网;
10
11
  // 存储 = $DSH_HOME/provider-usage/daily-stats/YYYY-MM-DD.json:历史天封存(deps mtime 校验),只算今天。
11
12
  // 缓存:每 provider 独立 30s 新鲜窗口 + 并发去重 + 3 快照环;stale-while-error。
12
13
 
13
14
  import { join, dirname } from 'node:path'
14
- import { stat as fsStat } from 'node:fs/promises'
15
+ import { stat as fsStat, readFile, writeFile, rename, chmod, mkdir } from 'node:fs/promises'
15
16
  import { readFileSync } from 'node:fs'
16
17
  import { createSecureStore, resolveProviderUsageDir } from './secure-store.js'
17
18
  import * as daily from './daily-stats.js'
@@ -20,8 +21,8 @@ export const name = 'provider-usage'
20
21
 
21
22
  /** 插件版本单一来源:package.json(UA 标识用;bundle 内嵌等读不到时回退字面量)。 */
22
23
  const PLUGIN_VERSION = (() => {
23
- try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '0.7.0' }
24
- catch { return '0.7.0' }
24
+ try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '0.10.0' }
25
+ catch { return '0.10.0' }
25
26
  })()
26
27
  const USER_AGENT = 'dsh-provider-usage/' + PLUGIN_VERSION
27
28
 
@@ -50,7 +51,7 @@ const LOCAL_DEPS_CHECK_MS = 300000
50
51
  const ADAPTERS = [
51
52
  {
52
53
  id: 'usage-percent',
53
- displayName: 'OpenCode Go(订阅额度)',
54
+ displayName: 'OpenCode Go',
54
55
  baseUrl: 'https://opencode.ai/zen/go',
55
56
  usagePath: '/v1/usage',
56
57
  defaultCredentialRef: 'OPENCODE_GO_API_KEY',
@@ -70,7 +71,7 @@ const ADAPTERS = [
70
71
  // Command Code:DSH 非内置提供方(pi-ai 注册表无 commandcode),只允许「添加自定义提供方」
71
72
  // (customOnly),Key 只存插件私有库(settingsProviderKey: null → 凭证链跳过 DSH 模型设置)。
72
73
  id: 'commandcode-credits',
73
- displayName: 'Command Code(订阅+余额)',
74
+ displayName: 'Command Code',
74
75
  baseUrl: 'https://api.commandcode.ai',
75
76
  usagePath: '/alpha/billing/credits',
76
77
  subscriptionPath: '/alpha/billing/subscriptions',
@@ -79,6 +80,21 @@ const ADAPTERS = [
79
80
  customOnly: true,
80
81
  description: 'Command Code 订阅使用量(5小时/周窗口)+ 月度剩余额度(USD)',
81
82
  },
83
+ {
84
+ // AMD Radeon Cloud(Model API):DSH 非内置提供方,只允许「添加自定义提供方」(customOnly),
85
+ // Key 只存插件私有库。实测可用端点 = {baseUrl}/usage(文档页 /api/profile/model-usage 404,见 tmp 调研)。
86
+ // 返回今日剩余额度(pts)+ 今日/近24h/近30天/累计汇总 + 按模型拆分;无周/月窗口。
87
+ // AMD Radeon Cloud 分国内 / 国际两个版本:本条目 = 中国版(developer.amd.com.cn);国际版
88
+ // baseUrl/端点不同,预留位置——后续接入时新增一个 amd-radeon-usage-intl 适配器条目即可。
89
+ id: 'amd-radeon-usage',
90
+ displayName: 'AMD Radeon Cloud (中国)',
91
+ baseUrl: 'https://developer.amd.com.cn/radeon/api/v1',
92
+ usagePath: '/usage',
93
+ defaultCredentialRef: 'RADEON_API_KEY',
94
+ settingsProviderKey: null,
95
+ customOnly: true,
96
+ description: 'AMD Radeon Cloud (中国) Model API 用量/余额(今日剩余 pts + 时间窗口汇总)',
97
+ },
82
98
  ]
83
99
 
84
100
  /** 旧 provider id → 适配器 id(向后兼容 /api/provider-usage/opencode-go 与 query?provider=…)。 */
@@ -87,6 +103,9 @@ const LEGACY_PROVIDER_MAP = { 'opencode-go': 'usage-percent', 'deepseek-balance'
87
103
  /** Command Code 混合卡适配器(模块级常量,避免每次查询重复 find)。 */
88
104
  const COMMANDCODE_ADAPTER = ADAPTERS.find((a) => a.id === 'commandcode-credits') || null
89
105
 
106
+ /** AMD Radeon Cloud 余额卡适配器(模块级常量,避免每次查询重复 find)。 */
107
+ const AMD_RADEON_ADAPTER = ADAPTERS.find((a) => a.id === 'amd-radeon-usage') || null
108
+
90
109
  function isLoopbackRequest(req) {
91
110
  const addr = req.socket && req.socket.remoteAddress
92
111
  if (addr !== '127.0.0.1' && addr !== '::1' && addr !== '::ffff:127.0.0.1') return false
@@ -384,6 +403,95 @@ function parseCommandCodeCredits(creditsBody, subBody) {
384
403
  }
385
404
  }
386
405
 
406
+ // ── AMD Radeon Cloud(v0.10 调研新增):余额+配额卡 ─────────────────────
407
+ // 官方接口(实测):GET {baseUrl}/usage → 今日剩余 pts + 四个时间段汇总 + 按模型拆分。
408
+ // 无周/月窗口(文档页 /api/profile/model-usage 404;接入以实测 /usage 为准,见 tmp 调研)。
409
+ // 数据形态对齐 Command Code 混合卡:主数字 = 今日剩余百分比(3 位小数向下取整),
410
+ // 副行 = 已用/限额 + 剩余(单位 pts,等价 USD,网页用 pts 表述),单日窗口进度条。
411
+
412
+ /** 今日剩余百分比:钳制 [0,100] 后保留 3 位小数向下取整(与 commandcodeMonthlyRemainingPct 同语义)。 */
413
+ function amdRadeonRemainingPct(limitUsd, remainingUsd) {
414
+ const limit = Number(limitUsd)
415
+ if (!Number.isFinite(limit) || !(limit > 0)) return null
416
+ const remaining = Number(remainingUsd)
417
+ if (!Number.isFinite(remaining)) return null
418
+ const pct = Math.max(0, Math.min(100, (remaining / limit) * 100))
419
+ return Math.floor(pct * 1000) / 1000
420
+ }
421
+
422
+ /** 把 /usage 响应换算成结构化结果(余额 + 单日窗口 + 时间段/按模型汇总透传)。 */
423
+ function parseAmdRadeonUsage(body) {
424
+ let data = body
425
+ if (typeof data === 'string') {
426
+ try { data = JSON.parse(data) } catch { return { error: 'bad-json' } }
427
+ }
428
+ const limitUsd = data && data.daily_cost_limit_usd
429
+ const usedUsd = data && data.daily_cost_used_usd
430
+ const remainingUsd = data && data.daily_cost_remaining_usd
431
+ // 文档说 502 时汇总字段全 0 且 status 说明原因;字段缺失视为不可用
432
+ if (limitUsd === undefined || limitUsd === null || usedUsd === undefined || usedUsd === null || remainingUsd === undefined || remainingUsd === null) {
433
+ return { error: 'no-usage' }
434
+ }
435
+ const toNum = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null }
436
+ const limit = toNum(limitUsd)
437
+ const used = toNum(usedUsd)
438
+ const remaining = toNum(remainingUsd)
439
+ const remainingPct = amdRadeonRemainingPct(limit, remaining)
440
+ const usedPct = (limit !== null && used !== null && limit > 0) ? Math.max(0, Math.min(100, (used / limit) * 100)) : null
441
+ const nextResetAt = typeof data.daily_reset_at === 'string' && data.daily_reset_at ? data.daily_reset_at : null
442
+ const periodStartedAt = typeof data.period_started_at === 'string' && data.period_started_at ? data.period_started_at : null
443
+
444
+ // 时间段汇总(today / last_24_hours / last_30_days / all_time 同构)→ 透传原始字段
445
+ const windowOf = (name) => {
446
+ const w = data && data[name]
447
+ if (!w || typeof w !== 'object') return null
448
+ return {
449
+ requests: w.requests,
450
+ errors: w.errors,
451
+ totalTokens: w.total_tokens,
452
+ promptTokens: w.prompt_tokens,
453
+ completionTokens: w.completion_tokens,
454
+ reasoningTokens: w.reasoning_tokens,
455
+ cachedTokens: w.cached_tokens,
456
+ kvCacheHitRate: w.kv_cache_hit_rate,
457
+ cost: w.cost,
458
+ lastRequestAt: w.last_request_at,
459
+ }
460
+ }
461
+
462
+ const extraParts = []
463
+ if (used !== null && limit !== null) extraParts.push('今日已用 ' + used.toFixed(3) + ' / ' + limit.toFixed(3) + ' pts')
464
+ if (remaining !== null) extraParts.push('今日剩余 ' + remaining.toFixed(3) + ' pts')
465
+ if (nextResetAt) extraParts.push('重置 ' + formatCountdown(nextResetAt))
466
+ if (data.rpm_limit !== undefined) extraParts.push('RPM ' + data.rpm_limit)
467
+
468
+ return {
469
+ planName: 'AMD Radeon Cloud (中国)',
470
+ remaining,
471
+ unit: 'pts',
472
+ used,
473
+ totalQuota: limit,
474
+ nextResetAt,
475
+ extra: extraParts.join(';'),
476
+ // 单日窗口:卡片只有一行进度条(对齐网页 "Today Used / Today Remaining / RPM Limit")
477
+ windows: {
478
+ daily: { usedPct, remainingPct, used, cap: limit, resetsAt: nextResetAt, periodStartedAt },
479
+ },
480
+ // 透传时间段汇总与按模型拆分(供副行/展开/本地统计对照)
481
+ periods: {
482
+ today: windowOf('today'),
483
+ last_24_hours: windowOf('last_24_hours'),
484
+ last_30_days: windowOf('last_30_days'),
485
+ all_time: windowOf('all_time'),
486
+ },
487
+ byModel: (Array.isArray(data.by_model) ? data.by_model : null),
488
+ recent: (Array.isArray(data.recent) ? data.recent : null),
489
+ cards: null,
490
+ isValid: true,
491
+ invalidMessage: null,
492
+ }
493
+ }
494
+
387
495
  // ── 插件主体 ────────────────────────────────────────────────────────
388
496
 
389
497
  export function apply(ctx, rawConfig) {
@@ -406,12 +514,67 @@ export function apply(ctx, rawConfig) {
406
514
  ? rawConfig.sessionsDir
407
515
  : join(dirname(resolveProviderUsageDir()), 'sessions'),
408
516
  dailyDir: join(resolveProviderUsageDir(), 'daily-stats'),
517
+ // 提供方实例清单(v0.8.0):host 端单一事实源(localStorage 只是缓存),防重启后清单被静默重建
518
+ providersFile: join(resolveProviderUsageDir(), 'providers.json'),
409
519
  }
410
520
 
411
521
  // cordis 配置的 baseUrl 仅覆盖 opencode-go(历史语义),其余 preset 用内置 URL(插件自维护)
412
522
  const effectiveBaseUrl = (cfg) =>
413
523
  cfg.id === 'usage-percent' && config.baseUrlOverride ? config.baseUrlOverride : cfg.baseUrl
414
524
 
525
+ // ── 提供方实例清单持久化(v0.8.0)─
526
+ // 修复「重启后提供方清单被静默重建」:实例清单此前只存浏览器 localStorage,
527
+ // 存储 key 丢失(换 origin/清站点数据/浏览器驱逐)后 loadProviders() 走 DEFAULT_PROVIDERS
528
+ // 兜底 → 已删除的默认实例复活、自定义实例(如 Command Code)永久消失。
529
+ // 现在 host 端 providers.json 为单一事实源;客户端启动时从 host 恢复,写操作同步落盘。
530
+ const PROVIDER_ADAPTER_IDS = new Set(ADAPTERS.map((a) => a.id))
531
+ const PROVIDERS_MAX = 64
532
+
533
+ /** 规范并校验一个提供方实例(不合法字段一律剔除,而非整单拒绝——避免脏数据把恢复流程卡死)。 */
534
+ function normalizeProviderEntry(raw) {
535
+ if (!raw || typeof raw !== 'object') return null
536
+ if (typeof raw.adapter !== 'string' || !PROVIDER_ADAPTER_IDS.has(raw.adapter)) return null
537
+ const id = typeof raw.id === 'string' && raw.id.trim() !== '' ? raw.id.trim().slice(0, 128) : null
538
+ if (!id) return null
539
+ const name = typeof raw.name === 'string' && raw.name.trim() !== '' ? raw.name.trim().slice(0, 64) : ''
540
+ const ref = typeof raw.ref === 'string' ? raw.ref.slice(0, 128) : ''
541
+ const source = raw.source === 'vault' ? 'vault' : 'dsh'
542
+ const type = raw.type === 'manual' || raw.type === 'import' ? raw.type : (source === 'vault' ? 'manual' : 'import')
543
+ return { id, name, adapter: raw.adapter, ref, source, type, paused: raw.paused === true }
544
+ }
545
+
546
+ /** 读清单:providers.json 缺失/损坏 → null(调用方决定兜底,损坏文件同时改名留证)。 */
547
+ async function readProvidersDoc() {
548
+ let raw
549
+ try {
550
+ raw = await readFile(config.providersFile, 'utf8')
551
+ } catch (e) {
552
+ if (e && e.code === 'ENOENT') return null
553
+ return null // 其它读错误按缺失处理
554
+ }
555
+ try {
556
+ const doc = JSON.parse(raw)
557
+ if (!doc || doc.version !== 1 || !Array.isArray(doc.providers)) throw new Error('bad doc')
558
+ const providers = doc.providers.map(normalizeProviderEntry).filter(Boolean)
559
+ return { version: 1, providers }
560
+ } catch (e) {
561
+ // 损坏文件改名留证(.corrupt-<ts>),下次写盘重建;绝不静默吞掉用户清单
562
+ try { await rename(config.providersFile, config.providersFile + '.corrupt-' + Date.now()) } catch { /* ignore */ }
563
+ return null
564
+ }
565
+ }
566
+
567
+ /** 写清单:原子写(tmp → rename)+ 0600(与 credentials.json 同款边界);目录缺失时先建(0700)。 */
568
+ async function writeProvidersDoc(providers) {
569
+ const doc = { version: 1, providers, savedAt: Date.now() }
570
+ await mkdir(dirname(config.providersFile), { recursive: true, mode: 0o700 }).catch(() => {})
571
+ const tmp = config.providersFile + '.tmp'
572
+ await writeFile(tmp, JSON.stringify(doc, null, 2), { mode: 0o600 })
573
+ await chmod(tmp, 0o600)
574
+ try { await rename(tmp, config.providersFile) } catch (e) { await writeFile(config.providersFile, JSON.stringify(doc, null, 2), { mode: 0o600 }) }
575
+ return doc
576
+ }
577
+
415
578
  // 私有加密凭证库(方案 B,懒初始化:首次使用时生成密钥)
416
579
  let secure = null
417
580
  async function secureStore() {
@@ -490,6 +653,9 @@ export function apply(ctx, rawConfig) {
490
653
  totalQuota: value.totalQuota,
491
654
  nextResetAt: value.nextResetAt,
492
655
  monthly: value.monthly,
656
+ periods: value.periods,
657
+ byModel: value.byModel,
658
+ recent: value.recent,
493
659
  extra: value.extra,
494
660
  windows: value.windows,
495
661
  cards: value.cards,
@@ -629,10 +795,14 @@ export function apply(ctx, rawConfig) {
629
795
  return fail(cfg, state, 'parse', '接口响应不是有效 JSON', res.status)
630
796
  }
631
797
 
632
- const parsed = cfg.id === 'balance-json' ? parseBalance(body) : parseUsage(body)
798
+ const parsed = cfg.id === 'balance-json'
799
+ ? parseBalance(body)
800
+ : (AMD_RADEON_ADAPTER && cfg.id === AMD_RADEON_ADAPTER.id ? parseAmdRadeonUsage(body) : parseUsage(body))
633
801
  if (parsed.error === 'no-usage') {
634
802
  recordSnapshot(key, { attemptAt, httpStatus: res.status, error: 'no-usage' })
635
- return fail(cfg, state, 'parse', '没有找到 OpenCode Go 用量数据', res.status)
803
+ return fail(cfg, state, 'parse', (AMD_RADEON_ADAPTER && cfg.id === AMD_RADEON_ADAPTER.id)
804
+ ? '没有找到 AMD Radeon Cloud (中国) 用量数据(daily_cost_*)'
805
+ : '没有找到 OpenCode Go 用量数据', res.status)
636
806
  }
637
807
  if (parsed.error === 'no-balance') {
638
808
  recordSnapshot(key, { attemptAt, httpStatus: res.status, error: 'no-balance' })
@@ -1154,6 +1324,28 @@ export function apply(ctx, rawConfig) {
1154
1324
  return { ok: true, refs }
1155
1325
  }),
1156
1326
  }),
1327
+ // 提供方实例清单持久化(v0.8.0)─:host 端 providers.json 为单一事实源。
1328
+ // GET 读清单(缺失返回 ok:true + items:null,由客户端走一次性迁移);
1329
+ // POST 整单覆盖保存(幂等)。仅回环,条目逐个规范化,不合法条目剔除。
1330
+ ctx.webServer.register({
1331
+ kind: 'exact',
1332
+ path: '/api/provider-usage/providers',
1333
+ handler: jsonRoute(async (req, body) => {
1334
+ if (req.method === 'POST') {
1335
+ if (!body || !Array.isArray(body.providers)) {
1336
+ return { ok: false, error: { type: 'bad-providers', message: 'providers 必须是数组', httpStatus: 400 } }
1337
+ }
1338
+ if (body.providers.length > PROVIDERS_MAX) {
1339
+ return { ok: false, error: { type: 'too-many-providers', message: '提供方实例最多 ' + PROVIDERS_MAX + ' 个', httpStatus: 400 } }
1340
+ }
1341
+ const providers = body.providers.map(normalizeProviderEntry).filter(Boolean)
1342
+ await writeProvidersDoc(providers)
1343
+ return { ok: true, providers }
1344
+ }
1345
+ const doc = await readProvidersDoc()
1346
+ return { ok: true, providers: doc ? doc.providers : null, exists: doc !== null }
1347
+ }, true),
1348
+ }),
1157
1349
  ]
1158
1350
  return () => { for (const dispose of disposers) { if (typeof dispose === 'function') dispose() } }
1159
1351
  }, 'provider-usage: routes')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loommii/dsh-provider-usage",
3
- "version": "0.7.0",
3
+ "version": "0.10.0",
4
4
  "description": "DSH 插件(用量中心):常驻用量卡片 · 多提供方用量/余额查询(OpenCode Go 订阅 + DeepSeek 余额 + Command Code 订阅/余额,对齐 cc-switch 查询语义)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -48,15 +48,12 @@
48
48
  "patch": "./cordis.patch.yml"
49
49
  },
50
50
  "client": {
51
- "platform": "web",
52
- "inject": [
53
- "@deepseek-ai/dsh-client-runtime"
54
- ]
51
+ "platform": "web"
55
52
  }
56
53
  },
57
54
  "peerDependencies": {
58
55
  "@deepseek-ai/cordis": "^4.0.1",
59
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
56
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.6",
60
57
  "react": "^18.2.0",
61
58
  "react-dom": "^18.2.0"
62
59
  },