dsh-ds-balance 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +68 -18
  2. package/README_en.md +72 -18
  3. package/lib/adapters/domain-core-store.d.ts +2 -2
  4. package/lib/adapters/domain-core-store.js +3 -1
  5. package/lib/adapters/host-account.d.ts +66 -0
  6. package/lib/adapters/host-account.js +68 -0
  7. package/lib/client.js +437 -95
  8. package/lib/domain/account.d.ts +40 -0
  9. package/lib/domain/account.js +74 -0
  10. package/lib/domain/balance.d.ts +20 -1
  11. package/lib/domain/normalize.d.ts +3 -2
  12. package/lib/domain/normalize.js +3 -2
  13. package/lib/domain/severity.d.ts +8 -2
  14. package/lib/domain/severity.js +11 -4
  15. package/lib/domain/situation.d.ts +104 -0
  16. package/lib/domain/situation.js +93 -0
  17. package/lib/http/handlers.d.ts +1 -1
  18. package/lib/http/handlers.js +29 -5
  19. package/lib/http/wire.d.ts +11 -1
  20. package/lib/http/wire.js +2 -0
  21. package/lib/index.js +122 -6
  22. package/lib/ports/account.d.ts +27 -0
  23. package/lib/ports/account.js +9 -0
  24. package/lib/services/balance-service.d.ts +66 -94
  25. package/lib/services/balance-service.js +132 -196
  26. package/lib/services/balance-source.d.ts +174 -0
  27. package/lib/services/balance-source.js +294 -0
  28. package/lib/services/scheduler.d.ts +8 -2
  29. package/lib/services/scheduler.js +1 -1
  30. package/lib/services/source-readers.d.ts +26 -0
  31. package/lib/services/source-readers.js +73 -0
  32. package/lib/services/source-selector.d.ts +50 -0
  33. package/lib/services/source-selector.js +64 -0
  34. package/lib/types/client/api-types.d.ts +16 -0
  35. package/lib/types/client/data.d.ts +12 -3
  36. package/lib/types/client/index.d.ts +6 -0
  37. package/lib/types/client/locales.d.ts +10 -18
  38. package/lib/types/client/mock/index.d.ts +12 -11
  39. package/lib/types/client/mock/scenarios.d.ts +13 -5
  40. package/lib/types/client/model.d.ts +24 -25
  41. package/lib/types/client/route-hint.d.ts +61 -0
  42. package/lib/types/client/sidebar/BalancePopover.d.ts +7 -0
  43. package/lib/types/client/sidebar/PercentRing.d.ts +13 -5
  44. package/lib/types/client/sidebar/SidebarBalance.d.ts +9 -1
  45. package/lib/types/client/situation.d.ts +164 -0
  46. package/lib/version.d.ts +1 -1
  47. package/lib/version.js +1 -1
  48. package/package.json +25 -23
package/README.md CHANGED
@@ -29,7 +29,7 @@
29
29
  ---
30
30
 
31
31
  > [!NOTE]
32
- > **余额读自 DeepSeek 官方 `GET /user/balance`**,不是估算。凭据只经 DSH 的凭据通道解析:API Key 不会出现在设置文件里,也不会返回给界面。
32
+ > **余额读自 DeepSeek 官方接口**,不是估算。凭据只经 DSH 的凭据通道解析:API Key 不会出现在设置文件里,也不会返回给界面。
33
33
 
34
34
  余额要有地方看,但不该占地方。左边栏底部一个常驻的状态环,点开是三段金额与数据新鲜度;要改配置,进侧边栏**插件(Plugins)**页里本插件的详情页。
35
35
 
@@ -39,12 +39,40 @@
39
39
  <em>常驻<strong>左边栏底部</strong>,与「使用统计」「设置」并排;点开是余额、赠送 / 充值拆分与数据新鲜度。</em>
40
40
  </p>
41
41
 
42
+ ## 一眼看懂那个环
43
+
44
+ 那个环是「现在是什么情况」,一眼能分出五种,各自对应你该做的事:
45
+
46
+ | 你看到 | 意思 | 你该做什么 |
47
+ |---|---|---|
48
+ | 彩环 + 一段弧 | 有余额,颜色就是多还是少 | **看数字** |
49
+ | 灰环 + 一段缺口在转 | 正在取,还没结果 | **等** |
50
+ | 灰环 + 中间一个 **+** | 没接入官方 | **去配置**(填 API Key 或登录账号) |
51
+ | 灰色空环 | 连上了,账户就是没余额 | **不用管** |
52
+ | 红环 + 中间一个 **叉** | 这里没有可用的数字 | **等、检查,或去查账户** |
53
+
54
+ **鼠标停上去,它会告诉你这一格现在什么意思**(收起态那 56px 轨道、展开态带名字的条目,
55
+ 停上去显示的是同一句话):
56
+
57
+ - **数字可信时**(正常 / 偏低 / 告急)—— 直接显示**金额**;
58
+ - **其余情况** —— 显示这个处境自己的话:「正在获取」「读不到余额」「服务异常」
59
+ 「尚未配置凭据」「账户不可用」「数据已过期」「暂无余额」。
60
+
61
+ **账户停用**与**数据过期**虽然手里有数字,也**不**显示那个数字:
62
+ 停用的账户、过期的快照,报出来的数只会误导(「我有 ¥0」/「我有 ¥110」)。
63
+ 环已经把问题画出来了(红叉 / 旧色),悬停该做的是解释问题。
64
+
65
+ 两种「看不到」分得开:**没接入**提示你去配凭据;**接入了但读不到**说「读不到余额」
66
+ (不写「暂时」—— 有些原因等一下就好,有些得你去改凭据,说「暂时」会把人劝去干等)。
67
+ 那个**叉**既表示「读不到」,也表示「读到了但这个账户不能用」——
68
+ 两者都是「这里没有可用的数字」,都要你去查。
69
+
42
70
  ## 界面一览
43
71
 
44
72
  | 界面 | 一句话 | 你会用它来 |
45
73
  |---|---|---|
46
- | 侧栏圆环 | 常驻左边栏底部的状态环 + 名称,**展开态悬浮条目就报出余额金额**,**环里填了多少 = 余额离该币种预警线还有多远** | 扫一眼就知道还剩多少、离预警线多远 |
47
- | 余额浮层 | 点条目展开:总额、赠送 / 充值拆分、数据新鲜度、手动刷新(带冷却);**右上角图标跳到本插件的配置格**(宿主没有那条跨插件深链服务时退回 Plugins 列表,悬浮文字同步改口径) | 核对具体数字,以及数据是几分钟前的 |
74
+ | 侧栏圆环 | 常驻左边栏底部的状态环 + 名称;**悬停(收起态与展开态一样)显示同一句话**:数字可信时显金额,否则显状态文案;**环里填了多少 = 余额离该币种预警线还有多远** | 扫一眼就知道还剩多少、离预警线多远 |
75
+ | 余额浮层 | 点条目展开:总额、赠送 / 充值拆分、数据新鲜度、手动刷新(带冷却);**右上角图标直达本插件的配置**(宿主版本不支持时退回 Plugins 列表,悬浮文字同步改口径) | 核对具体数字,以及数据是几分钟前的 |
48
76
  | 设置卡片 | 连接 / 展示 / 阈值 / 刷新四组,**默认四组全收起**,点组头逐组展开 | 换端点、换币种、调预警线、调节奏 |
49
77
 
50
78
  三种界面的分工是死的:**圆环回答「大概还剩多少」,浮层回答「具体是多少」,卡片回答「怎么算」。**
@@ -73,16 +101,16 @@
73
101
 
74
102
  ### 前置
75
103
 
76
- - **DSH**:版本范围以 [package.json](package.json) 的 `engines.dsh` 与 `peerDependencies` 为准,本插件跟的是宿主当前那条 alpha 线。
77
- - Node `>= 20`(同上,真源是 `engines.node`)。
104
+ - **DSH**:版本范围以 [package.json](package.json) 的声明为准,本插件跟的是宿主当前那条 **RC 线**(dist-tag 是 `next`)。
105
+ - **Node**:`>= 20`(同一处声明)。
78
106
 
79
- 装宿主时**要显式点名版本线**:`@deepseek-ai/dsh` 的 `latest` 标签比本插件要求的那条线还旧 —— 按默认方式装会落在声明范围之外。
107
+ 装宿主时**要显式点名版本线**:`@deepseek-ai/dsh` 的 `alpha` 与 `latest` 都**不是**我们声明的那条线 —— 按默认方式装会落在声明范围之外。
80
108
 
81
109
  ```bash
82
- npm install -g @deepseek-ai/dsh@alpha # 本插件承诺支持的线
110
+ npm install -g @deepseek-ai/dsh@next # 本插件承诺支持的线(RC 线;npm 上没有 rc 这个 tag)
83
111
  ```
84
112
 
85
- 兼容性不是推断出来的:每周由 [compat.yml](.github/workflows/compat.yml) 在 `alpha` 与 `next` 两条线上换包实跑一遍现有测试,另有一步单独判声明面罩不罩得住;红了会开或更新一条固定标题的跟踪 issue。当前结论与红了怎么办见 [兼容性](docs/PUBLISHING.md#兼容性),宿主版本的分水岭见[版本兼容](#版本兼容)。
113
+ 兼容性怎么保证、哪条线该跟,见[版本兼容](#版本兼容)。
86
114
 
87
115
  ### 从 npm 安装
88
116
 
@@ -113,10 +141,18 @@ dsh plugin --profile web add "$PWD"
113
141
 
114
142
  ## 版本兼容
115
143
 
116
- - 配置界面注册在宿主的 `plugins.bundle.config` 槽,**key 逐字就是包名**:一个 bundle 一份配置,渲染在它自己的详情页里。**这个落点往返过一次**(bundle 槽 → 行槽 → bundle 槽),两次都不是版本问题,是 UX 选择:行槽要多点一次 Configure,而 bundle 槽的座位 props **永远不带 `form`** —— 表单由卡片自己向 `ctx.configForms.get(<本插件那一行的 Loader 条目 id>)` 取。下限的真源是 [package.json](package.json) 的 `engines.dsh`,现状现查 `node scripts/compat-swap.mjs check`。
117
- - 下限抬上去的原因**是设置接缝换了,不是槽**:客户端侧的作用域服务被删、宿主侧 `settings.register` 被删,两半改用新的 `configForms` 与 volatile 配置引用 —— **没有那两样东西,浏览器半边整个不渲染**(圆环与浮层也一起消失)。插件不查宿主版本号:它盯的是 `ctx.inject(['configForms'])` 在窗口内有没有回调(**不盯槽名** —— 两个候选槽在更早的宿主上也在座,槽在不在推不出拿不拿得到表单);拿不到时**浮层里会多一行英文 `[WARN]`**,说明配置界面为什么不可用、该往哪儿升级。
118
- - 需要配置界面,就把宿主升到 `engines.dsh` 声明的那一版或更高:`npm install -g @deepseek-ai/dsh@alpha`。
119
- - 声明是**窄**的:下限是我们实测过的那一版,用 `>=` 而**不设上限** —— 既不承诺「以后都兼容」,也不会因为宿主多推一个预发布段就失效。为什么这么定见[兼容性](docs/PUBLISHING.md#兼容性)。
144
+ - **需要哪一版宿主**:以 [package.json](package.json) 的声明为准,本插件跟的是宿主当前那条 **RC 线**。
145
+ 需要配置界面,就把宿主升到那一版或更高:`npm install -g @deepseek-ai/dsh@next`。
146
+ - **声明是「窄」的**:下限是我们实测过的那一版,用 `>=` 而**不设上限** —— 不承诺「以后都兼容」。
147
+ (它罩得住**同一 `major.minor.patch` 内**后续的预发布版本;宿主换一个补丁位再推预发布,
148
+ 这条下限就罩不住了,届时要再抬一次。为什么见[兼容性](docs/PUBLISHING.md#兼容性)。)
149
+ - **下限为什么抬上去**:不是换了界面位置,而是**设置接缝换过**(旧宿主上没有本插件依赖的那两样东西,
150
+ 浏览器半边会整个不渲染 —— 圆环与浮层也一起消失)。插件不查宿主版本号,而是**当场探测那个能力**;
151
+ 拿不到时浮层里会多一行英文提示,说明配置界面为什么不可用、该往哪儿升级。
152
+ 技术细节见 [配置表单的能力探测](docs/ARCHITECTURE.md)。
153
+ - **兼容性不是推断出来的**:每周由 [compat.yml](.github/workflows/compat.yml) 在 `next`(RC 线)上
154
+ 换包实跑一遍现有测试,另有一步单独判声明面罩不罩得住;红了会开或更新一条固定标题的跟踪 issue。
155
+ 当前结论与红了怎么办见 [兼容性](docs/PUBLISHING.md#兼容性)。
120
156
 
121
157
  ## 配置
122
158
 
@@ -131,9 +167,9 @@ dsh plugin --profile web add "$PWD"
131
167
  - **连接**:API 地址与凭据,两项都默认留空 —— 地址留空即用 DeepSeek 官方端点,凭据继承官方模型页那一份、只读不可改;
132
168
  二级「自定义设置」里只放凭据引用名。
133
169
  - **展示**:金额用哪种币种,或让它自动跟随账户。
134
- - **阈值**:每个币种两档提醒线(预警 / 告急)。同一币种内**告急必须严格低于预警**,相等也会被拒绝。
135
- `POST /api/v1/config` 也按这条判,违反回 `422`;
136
- 但**手改配置文件写成的非法组合不会再报错** —— 宿主那一侧已经不再强制它,插件会在读到的那一刻把这一对**回落成默认值**并在日志里记一次。
170
+ - **阈值**:每个币种两档提醒线(预警 / 告急)。同一币种内**告急必须严格低于预警**,相等也会被拒绝
171
+ (保存时就会拦下来);但**手改配置文件写成的非法组合不会再报错** ——
172
+ 插件读到的那一刻会把这一对**回落成默认值**并在日志里记一次。
137
173
 
138
174
  > **升级提示**:设置项存在插件那一行的配置里,键名跟着宿主升级变过一次,**旧值不会自动迁移** —— 升级后请照上表重填一次。
139
175
  - **刷新**:服务端刷新周期、界面轮询周期与手动刷新冷却。冷却从**刷完那一刻**起算,
@@ -145,7 +181,9 @@ dsh plugin --profile web add "$PWD"
145
181
 
146
182
  - 环里填多少 = 当前余额占该币种**预警线**的比例,100% 封顶;告急线不参与画环 —— 它已经决定了颜色。
147
183
  - 颜色只表达状态,与金额大小无关:正常、偏低、告急各一色;账户读不到时另画一个带叉号的环。
148
- - 没配阈值的币种退回按状态定性:正常与不可用画满环,偏低 3/4,告急 1/4,未知空环。
184
+ - 没配阈值的币种退回按状态定性:**正常画满环,偏低 3/4,告急 1/4**。
185
+ (只有**有余额可看**的那一族画弧 —— 上面三档,加上「数据已过期」时的那份旧数字;
186
+ 账户读不到、账户停用、没接入、账户没钱这些**都不画弧**,环靠颜色与中心记号说话。)
149
187
  - 为什么颜色不由金额算、阈值为什么只当刻度 → [数据流](docs/ARCHITECTURE.md#数据流)。
150
188
 
151
189
  ### 凭据
@@ -153,6 +191,18 @@ dsh plugin --profile web add "$PWD"
153
191
  密钥只经 DSH 的凭据通道解析,卡片里的 `API Key` 一栏默认继承官方模型页配好的那一份,且只读不可改。
154
192
  由启动环境(环境变量)提供时,字段只读,徽标写明来源。
155
193
 
194
+ **余额有两条官方取数路,插件自己选**:模型页 / 环境变量配的 API Key 一条,**DSH 账号登录**一条
195
+ (只登录、没配 Key 也能看到余额)。选择顺序是:
196
+
197
+ - **当前会话在用的那条** —— 切会话、换模型都跟着换;
198
+ - 读不到会话就按全局默认模型的路由;
199
+ - 都判不出来时 **Key 优先、账号兜底**;
200
+ - 两条都没有,才是「尚未配置凭据」。
201
+
202
+ 浮层标题右边那个括号就是这份数字的来源:`(API Key)` / `(账号登录)`。**显示哪个、刷新就刷哪个、标签也写哪个** —— 三者始终同一条。
203
+
204
+ **这个插件只记官方的那一个数字**,所以首选那条读不到时会自动退到另一条官方路:只要账号或 API Key 有一个能用,就照常显示(哪怕是一份稍旧的快照)。**两条路都拿不到数字,才会提示** —— 那才是真的没接入官方。左栏那个环这时长什么样,见[一眼看懂那个环](#一眼看懂那个环)。
205
+
156
206
  **换了 key 多久生效,跟着 DSH 的层走 —— 本插件不额外追:**
157
207
 
158
208
  - **启动环境(环境变量)给的 key:改了要重启 DSH。** 进程的环境在它启动那一刻就定死了,之后再改外面那个变量,跑着的插件看不到。
@@ -165,8 +215,8 @@ dsh plugin --profile web add "$PWD"
165
215
 
166
216
  - **API Key 永不返回界面**:配置接口只回一个固定长度的掩码串,连末几位也不给。
167
217
  - 密钥不落日志、不落本插件的文件;设置文件里只有引用名,卡片可以安全截图或分享。
168
- - 余额快照存在 DSH 自己的数据目录里,按凭据派生出的账本标识分组 —— 换 key 自动开新账本,旧快照不会被混用。
169
- - 账本标识还要一个 `.salt` 文件(在 DSH 的 home 目录下)参与派生;**它丢了,旧快照就读不回来**。
218
+ - 余额快照存在 DSH 自己的数据目录里,按凭据派生出的标识分组 —— **换 key 自动开新账本**,旧快照不会被混用。
219
+ - 那份标识还要 DSH home 目录下一个随机盐文件参与派生;**它丢了,旧快照就读不回来**(不会串到别人账上)。
170
220
  - 只访问 `api.deepseek.com`,不代理、不转发其他流量。
171
221
 
172
222
  ## 许可
package/README_en.md CHANGED
@@ -29,7 +29,7 @@
29
29
  ---
30
30
 
31
31
  > [!NOTE]
32
- > **The balance is read from the official `GET /user/balance`** — not estimated. The credential is resolved only through DSH's credential channel: the API key never lands in a settings file and is never returned to the UI.
32
+ > **The balance is read from the official DeepSeek API** — not estimated. The credential is resolved only through DSH's credential channel: the API key never lands in a settings file and is never returned to the UI.
33
33
 
34
34
  The balance needs somewhere to live that does not take up room. A permanent status ring at the bottom of the sidebar; click it for the three amounts and the freshness of the data. Anything you want to configure lives on the plugin's details page under **Plugins → Installed**.
35
35
 
@@ -39,12 +39,42 @@ The balance needs somewhere to live that does not take up room. A permanent stat
39
39
  <em>A permanent fixture at the <strong>bottom of the sidebar</strong>, alongside Usage statistics and Settings; click it for the balance, the granted / topped-up split, and how fresh the data is.</em>
40
40
  </p>
41
41
 
42
+ ## The ring at a glance
43
+
44
+ That ring is "what is going on right now" — five shapes, each with a different thing for you to do:
45
+
46
+ | What you see | Meaning | What you do |
47
+ |---|---|---|
48
+ | Coloured ring + an arc | There is a balance; the colour is how much | **Read the number** |
49
+ | Grey ring + a gap rotating | Fetching; no answer yet | **Wait** |
50
+ | Grey ring + a **+** in the middle | Nothing connected | **Configure** (an API key, or sign in) |
51
+ | Grey, empty ring | Connected, the account simply has no balance | **Nothing** |
52
+ | Red ring + a **×** in the middle | No usable number here | **Wait, investigate, or check the account** |
53
+
54
+ **Hover it and it tells you what this spot means right now** (the same sentence whether you hover the
55
+ 56px rail ring or the expanded entry with its label):
56
+
57
+ - **When the number is trustworthy** (normal / low / critical) — it shows the **amount**;
58
+ - **otherwise** — it shows that situation's own line: "Loading", "Balance unavailable",
59
+ "Service error", "No credential configured", "Account unavailable", "Data is stale", "No balance".
60
+
61
+ **A suspended account and stale data hold a number too, and still do not show it**:
62
+ a suspended account or an expired snapshot reporting a figure only misleads ("I have ¥0" /
63
+ "I have ¥110"). The ring already draws the problem (red cross / old colour); hovering explains it.
64
+
65
+ The two kinds of "nothing to show" are told apart as well: **not connected** asks you to configure
66
+ a credential; **connected but unreadable** says "Balance unavailable".
67
+ It deliberately does **not** say "temporarily" — some causes clear up on their own, others need you
68
+ to fix a credential, and "temporarily" would send the second group away to wait for nothing.
69
+ The **×** covers both "could not read it" and "read it, but this account cannot be used" —
70
+ both mean "no usable number here", and both send you to investigate.
71
+
42
72
  ## Surface at a glance
43
73
 
44
74
  | Surface | One line | Use it for |
45
75
  |---|---|---|
46
- | Sidebar ring | A status ring plus label at the bottom of the sidebar; **in the expanded state, hovering the entry reports the balance amount**, and **how full it is = how far the balance is from that currency's warning line** | Seeing at a glance how much is left and how close it is to the warning line |
47
- | Balance popover | Click the entry: total, granted / topped-up split, data freshness, manual refresh (with cooldown); **an icon at its top right opens this plugin's configuration page** (on a host without that cross-plugin deep link it falls back to the Plugins list, and its tooltip says so) | Checking the exact figures, and how many minutes old they are |
76
+ | Sidebar ring | A status ring plus label at the bottom of the sidebar; **hovering it (in either state) says what this spot means** — the amount when the number is trustworthy, otherwise the situation's own line; and **how full it is = how far the balance is from that currency's warning line** | Seeing at a glance how much is left and how close it is to the warning line |
77
+ | Balance popover | Click the entry: total, granted / topped-up split, data freshness, manual refresh (with cooldown); **an icon at its top right opens this plugin's configuration page** (on a host that does not support it, it falls back to the Plugins list and its tooltip says so) | Checking the exact figures, and how many minutes old they are |
48
78
  | Settings card | Connection / Display / Thresholds / Refresh, **all collapsed by default**, expand a group from its header | Changing the endpoint, the currency, the warning lines, the cadence |
49
79
 
50
80
  The division of labour is fixed: **the ring answers "roughly how much is left", the popover answers "exactly how much", and the card answers "how is that computed".**
@@ -73,16 +103,16 @@ The division of labour is fixed: **the ring answers "roughly how much is left",
73
103
 
74
104
  ### Requirements
75
105
 
76
- - **DSH**: the range is whatever [package.json](package.json) declares under `engines.dsh` and `peerDependencies`; this plugin follows the alpha line the host is on.
77
- - Node `>= 20` (same source of truth: `engines.node`).
106
+ - **DSH**: the range is whatever [package.json](package.json) declares; this plugin follows the host's current **RC line** (the dist-tag is `next`).
107
+ - **Node**: `>= 20` (declared in the same place).
78
108
 
79
- Install the host by **naming the version line explicitly**: the `latest` tag of `@deepseek-ai/dsh` is older than the line this plugin requires — a default install lands outside the declared range.
109
+ Install the host by **naming the version line explicitly**: neither `alpha` nor `latest` of `@deepseek-ai/dsh` is the line we declare — a default install lands outside the declared range.
80
110
 
81
111
  ```bash
82
- npm install -g @deepseek-ai/dsh@alpha # the line this plugin promises to support
112
+ npm install -g @deepseek-ai/dsh@next # the line this plugin promises to support (the RC line; there is no `rc` tag on npm)
83
113
  ```
84
114
 
85
- Compatibility is measured, not inferred: every week [compat.yml](.github/workflows/compat.yml) swaps packages onto the `alpha` and `next` lines and reruns the existing tests, and one separate job judges whether the declared ranges still cover the line; a red patrol opens or updates a tracking issue with a fixed title. The current verdict, and what to do when it goes red, are in [Compatibility](docs/PUBLISHING.md#兼容性); the Host-version watershed is in [Version compatibility](#version-compatibility).
115
+ Compatibility is measured, not inferred: every week [compat.yml](.github/workflows/compat.yml) swaps packages onto the `next` (RC) line and reruns the existing tests, and one separate job judges whether the declared ranges still cover the line; a red patrol opens or updates a tracking issue with a fixed title. The current verdict, and what to do when it goes red, are in [Compatibility](docs/PUBLISHING.md#兼容性); the Host-version watershed is in [Version compatibility](#version-compatibility).
86
116
 
87
117
  ### From npm
88
118
 
@@ -113,10 +143,21 @@ The repository carries the GitHub topic [`dsh-plugin`](https://github.com/topics
113
143
 
114
144
  ## Version compatibility
115
145
 
116
- - The configuration UI registers into the Host's `plugins.bundle.config` slot, **keyed literally by the package name**: one bundle, one configuration, rendered on that bundle's own details page. **That landing spot travelled once** (bundle slot → row slot → bundle slot) and neither move was about Host versions — it is a UX choice: the row slot costs one extra Configure click, while the bundle slot's owner props **never carry a `form`** — the card fetches it itself through `ctx.configForms.get(<the Loader entry id of this plugin's row>)`. The floor's single source is `engines.dsh` in [package.json](package.json); check the current lines with `node scripts/compat-swap.mjs check`.
117
- - The floor moved **because the settings seam changed, not because of the slot**: the client-side scope service was removed, the Host-side `settings.register` was removed, and both halves moved to the new `configForms` service and volatile config references — **without those two the whole client half never renders** (the ring and the popover go with it). The plugin never reads a Host version: it watches whether `ctx.inject(['configForms'])` calls back within its window (**never the slot name** — both candidate slots are present on earlier Hosts too, and a slot being there says nothing about getting a form). When it never does, **the popover carries one extra English `[WARN]` line** saying why the configuration page is unavailable and where to upgrade.
118
- - To get the configuration page, upgrade the Host to the version `engines.dsh` declares or higher: `npm install -g @deepseek-ai/dsh@alpha`.
119
- - The declaration is **narrow**: the floor is the version we actually tested, written as `>=` with **no ceiling** — it claims neither "everything in the future counts" nor anything earlier. Why it is written that way is in [Compatibility](docs/PUBLISHING.md#兼容性).
146
+ - **Which Host version is required**: whatever [package.json](package.json) declares; this plugin follows the host's current **RC line**.
147
+ To get the configuration page, upgrade the Host to that version or higher: `npm install -g @deepseek-ai/dsh@next`.
148
+ - **The declaration is narrow**: the floor is the version we actually tested, written as `>=` with **no ceiling** —
149
+ it claims no "everything in the future counts".
150
+ (It does cover later prereleases **within the same `major.minor.patch`**; once the host moves a patch position and
151
+ ships another prerelease, this floor no longer covers it and has to be raised again. Why: [Compatibility](docs/PUBLISHING.md#兼容性).)
152
+ - **Why the floor moved**: not a change of where the UI lives, but a change of the **settings seam**
153
+ (without the two things this plugin relies on, the whole client half never renders — the ring and the popover go with it).
154
+ The plugin never reads a Host version; it **probes that capability live**, and when it is absent the popover carries
155
+ one extra English line saying why the configuration page is unavailable and where to upgrade.
156
+ Technical detail: see [Architecture](docs/ARCHITECTURE.md).
157
+ - **Compatibility is not inferred**: a weekly [compat.yml](.github/workflows/compat.yml) run swaps packages on both the
158
+ `alpha` and `next` lines and re-runs the existing tests, plus a separate check that the declared range still covers the
159
+ tracked line; a failure opens or updates a tracking issue with a fixed title. Current conclusions and what to do when
160
+ it goes red: see [Compatibility](docs/PUBLISHING.md#兼容性).
120
161
 
121
162
  ## Configuration
122
163
 
@@ -131,9 +172,8 @@ Open **Plugins → Installed** and step into the **DeepSeek Balance** (shown as
131
172
  - **Connection**: the API base URL and the credential, both blank by default — a blank URL means the official DeepSeek endpoint, and the credential is inherited from the official model page and is read-only.
132
173
  The nested "Customised settings" holds only the credential reference name.
133
174
  - **Display**: which currency to use for amounts, or let it follow the account.
134
- - **Thresholds**: two alert lines per currency (warning / critical). **Within one currency the critical line must be strictly lower than the warning line** — equality is rejected too.
135
- `POST /api/v1/config` enforces the same rule and answers `422` otherwise;
136
- but a hand-edited config file with an illegal pair **no longer errors** — the Host side stopped enforcing it, so the plugin **falls that pair back to its defaults** the moment it reads it, and logs one line.
175
+ - **Thresholds**: two alert lines per currency (warning / critical). **Within one currency the critical line must be strictly lower than the warning line** — equality is rejected too, and saving is blocked while it is.
176
+ But a hand-edited config file with an illegal pair **no longer errors** — the plugin **falls that pair back to its defaults** the moment it reads it, and logs one line.
137
177
 
138
178
  > **Upgrade note**: the settings live in this plugin's own entry config and the key it is filed under changed once; **old values are not migrated** — please fill the table above in once after upgrading.
139
179
  - **Refresh**: the server refresh interval, the UI poll interval and the manual refresh cooldown. The cooldown counts from **the moment the refresh finishes**, so the seconds shown in the popover are the real ones left; if the backend says the refresh did not fire, the UI takes its remaining time instead of pretending otherwise.
@@ -144,7 +184,9 @@ Saving applies immediately; there is no need to restart DSH.
144
184
 
145
185
  - How full the ring is = the current balance as a fraction of that currency's **warning line**, capped at 100%; the critical line takes no part in drawing it — it already decided the colour.
146
186
  - Colour carries state only, never an amount: normal, low and critical each get one hue; an account that cannot be read gets a ring with a cross instead.
147
- - A currency with no threshold configured falls back to state: full ring for normal and unavailable, 3/4 for low, 1/4 for critical, empty for unknown.
187
+ - A currency with no threshold configured falls back to state: **a full ring for normal, 3/4 for low, 1/4 for critical**.
188
+ (Only the family with a balance to look at draws an arc — those three, plus the old number while data is stale;
189
+ an unreadable account, a suspended account, nothing connected, an account with no balance: none of those draw an arc.)
148
190
  - Why colour is never computed from an amount, and why thresholds are only a scale → [Data flow](docs/ARCHITECTURE.md#数据流).
149
191
 
150
192
  ### Credentials
@@ -152,6 +194,18 @@ Saving applies immediately; there is no need to restart DSH.
152
194
  The key is resolved only through DSH's credential channel; the card's "API key" inherits the one already configured on the official model settings page.
153
195
  When it comes from the launch environment (an environment variable) the field is read-only and the badge says where it came from.
154
196
 
197
+ **The balance has two official routes and the plugin picks one itself**: the API key from the model settings page or the launch environment, and the **DSH account sign-in** (signed in without a key is enough). The order is:
198
+
199
+ - **the route the current session is on** — switching sessions *or the model in this session* re-reads it at once;
200
+ - with no session readable, the globally selected model's route;
201
+ - with neither decidable, **key first, account as the fallback**;
202
+ - only when both are missing does it read "no credential configured".
203
+
204
+ The parenthesised label to the right of the popover title is where that number came from: `(API key)` / `(account sign-in)`.
205
+ **Whichever is shown is also the one that gets refreshed and named** — display, refresh and label never disagree.
206
+
207
+ **This plugin only tracks the official number**, so if the preferred route has nothing to show it falls back to the other official one: as long as either the account or an API key works, you still see a balance (even a slightly older snapshot). **You are only told about it when neither route can produce a number** — that is what "not connected to official" means. What the ring looks like at that moment: see [the ring at a glance](#the-ring-at-a-glance).
208
+
155
209
  **How long a key change takes to land follows DSH's own layers — this plugin deliberately adds nothing on top:**
156
210
 
157
211
  - **A key from the launch environment (an environment variable): restart DSH to change it.** A process's environment is fixed the moment it starts; a later change to that variable outside is invisible to the running plugin.
@@ -164,8 +218,8 @@ Why it is this way → [Key decisions](docs/ARCHITECTURE.md#关键决策).
164
218
 
165
219
  - **The API key is never returned to the UI**: the config endpoint returns only a fixed-length mask, not even the last few characters.
166
220
  - The key is never logged and never written to a file owned by this plugin; the settings file holds only a reference name, so the card is safe to screenshot or share.
167
- - Balance snapshots live in DSH's own data directory, grouped by a ledger identifier derived from the credential — changing the key opens a new ledger and old snapshots are never mixed in.
168
- - That identifier also involves a `.salt` file in DSH's home directory; **lose it and old snapshots become unreadable**.
221
+ - Balance snapshots live in DSH's own data directory, grouped by an identifier derived from the credential — **changing the key opens a new ledger** and old snapshots are never mixed in.
222
+ - That identifier also involves a random salt file in DSH's home directory; **lose it and old snapshots become unreadable** (they can never resolve to someone else's).
169
223
  - Only `api.deepseek.com` is contacted; nothing is proxied or forwarded.
170
224
 
171
225
  ## License
@@ -7,7 +7,7 @@
7
7
  * - 打开失败要**降级**而不是抛出去 —— 未观察的 rejection 曾把宿主整个拖下水。
8
8
  * @module dsh-ds-balance/adapters/domain-core-store
9
9
  */
10
- import type { BalanceSnapshot } from '../domain/balance.js';
10
+ import type { BalanceSnapshot, BalanceSource } from '../domain/balance.js';
11
11
  import type { CoreStore, StoreHealth } from '../ports/core-store.js';
12
12
  import type { Logger } from '../ports/logger.js';
13
13
  /** 域里的表名。 */
@@ -24,7 +24,7 @@ export interface StoredSnapshot {
24
24
  granted: string;
25
25
  toppedUp: string;
26
26
  }>;
27
- source: 'deepseek-http';
27
+ source: BalanceSource;
28
28
  }
29
29
  /**
30
30
  * 域声明。
@@ -25,7 +25,9 @@ const storedSnapshotSchema = z.object({
25
25
  fetchedAt: z.number(),
26
26
  isAvailable: z.boolean(),
27
27
  balances: z.array(storedBalanceSchema),
28
- source: z.literal('deepseek-http'),
28
+ // **取值只增不改**:域版本一动,已落盘的账本会在 open 时直接 `version-mismatch` 拒开。
29
+ // 旧记录里只有 `deepseek-http`,它必须一直是合法值。
30
+ source: z.enum(['deepseek-http', 'deepseek-account']),
29
31
  });
30
32
  /**
31
33
  * 域声明。
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 宿主账号服务 → {@link AccountSource}。
3
+ *
4
+ * 这是**可选**接缝:`apply` 里由 `ctx.inject(['deepseekAccount'], …)` 把门,服务缺席时整个对象
5
+ * 不存在,插件退回只看 API Key。
6
+ *
7
+ * 服务面用**鸭子类型**收窄而不是 import 宿主类型:`ctx.deepseekAccount` 的类型来自
8
+ * `@deepseek-ai/dsh-deepseek-account`,而运行时提供方是 `-account-platform` —— 引类型要多挂一个包,
9
+ * 而这里只用到三个方法。运行期关系由 `ctx.inject` 表达,声明面由 `peerDependencies` 表达。
10
+ * @module dsh-ds-balance/adapters/host-account
11
+ */
12
+ import type { AccountWallet } from '../domain/account.js';
13
+ import type { AccountSource } from '../ports/account.js';
14
+ import type { Logger } from '../ports/logger.js';
15
+ /** 宿主单次余额查询的结果形状(对齐 `AccountDetails['balance']`)。 */
16
+ export type HostAccountBalance = {
17
+ readonly status: 'ready';
18
+ readonly value: readonly AccountWallet[];
19
+ readonly bonusWallets: readonly AccountWallet[];
20
+ } | {
21
+ readonly status: 'failed';
22
+ };
23
+ /** 宿主为这次调用要的客户端标识(对齐 `AccountClientMetadata`)。 */
24
+ export interface HostAccountClient {
25
+ readonly version: string;
26
+ readonly locale: string;
27
+ readonly timezoneOffsetSeconds: number;
28
+ }
29
+ /**
30
+ * 宿主账号服务的最小面。
31
+ *
32
+ * 三个方法的语义逐字对齐宿主 `DeepSeekAccount` 的公开面;`null` 一律表示**此刻没有可用账号**
33
+ * (未登录,或凭据在查询途中被换掉),不是「余额为零」。
34
+ */
35
+ export interface HostAccountFace {
36
+ getState(): Promise<{
37
+ readonly status?: string;
38
+ } | undefined>;
39
+ getBalance(client: HostAccountClient): Promise<HostAccountBalance | null>;
40
+ getPlatformSession(): Promise<{
41
+ readonly userId?: string | null;
42
+ } | null>;
43
+ }
44
+ /** 构造参数。 */
45
+ export interface HostAccountOptions {
46
+ service: HostAccountFace;
47
+ /** 上报给 Platform 的客户端版本;用插件版本,与响应里回传的那个同源。 */
48
+ version: string;
49
+ /** 上报给 Platform 的语言;只选服务端文案,界面文案由本插件自己的词典给。 */
50
+ locale?: string;
51
+ /** 上报给 Platform 的时区偏移(秒,东为正)。 */
52
+ timezoneOffsetSeconds?: () => number;
53
+ logger?: Logger | undefined;
54
+ }
55
+ /**
56
+ * 把宿主账号服务收窄成 {@link AccountSource}。
57
+ *
58
+ * `readBalance` 的三种结果各有去处,**不合并**:
59
+ * - `null` → 回 `null`,上层回落另一条取数路;
60
+ * - `failed` → 抛 `UpstreamError`,**不回落**(回落只会把真因换成更难懂的 `NO_KEY`);
61
+ * - `ready` → 原样交出去。
62
+ * @param options - 宿主服务与客户端标识。
63
+ * @returns 端口实现。
64
+ */
65
+ export declare function createAccountSource(options: HostAccountOptions): AccountSource;
66
+ //# sourceMappingURL=host-account.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 宿主账号服务 → {@link AccountSource}。
3
+ *
4
+ * 这是**可选**接缝:`apply` 里由 `ctx.inject(['deepseekAccount'], …)` 把门,服务缺席时整个对象
5
+ * 不存在,插件退回只看 API Key。
6
+ *
7
+ * 服务面用**鸭子类型**收窄而不是 import 宿主类型:`ctx.deepseekAccount` 的类型来自
8
+ * `@deepseek-ai/dsh-deepseek-account`,而运行时提供方是 `-account-platform` —— 引类型要多挂一个包,
9
+ * 而这里只用到三个方法。运行期关系由 `ctx.inject` 表达,声明面由 `peerDependencies` 表达。
10
+ * @module dsh-ds-balance/adapters/host-account
11
+ */
12
+ import { UpstreamError, describeError } from '../domain/errors.js';
13
+ /**
14
+ * 把宿主账号服务收窄成 {@link AccountSource}。
15
+ *
16
+ * `readBalance` 的三种结果各有去处,**不合并**:
17
+ * - `null` → 回 `null`,上层回落另一条取数路;
18
+ * - `failed` → 抛 `UpstreamError`,**不回落**(回落只会把真因换成更难懂的 `NO_KEY`);
19
+ * - `ready` → 原样交出去。
20
+ * @param options - 宿主服务与客户端标识。
21
+ * @returns 端口实现。
22
+ */
23
+ export function createAccountSource(options) {
24
+ const { service } = options;
25
+ const client = () => ({
26
+ version: options.version,
27
+ locale: options.locale ?? 'zh-CN',
28
+ timezoneOffsetSeconds: options.timezoneOffsetSeconds?.() ?? 0,
29
+ });
30
+ return {
31
+ async signedIn() {
32
+ try {
33
+ const state = await service.getState();
34
+ return state?.status === 'credential-stored';
35
+ }
36
+ catch (error) {
37
+ options.logger?.debug('ds-balance: account state unreadable', {
38
+ error: describeError(error),
39
+ });
40
+ return false;
41
+ }
42
+ },
43
+ async accountId() {
44
+ try {
45
+ const session = await service.getPlatformSession();
46
+ const id = session?.userId;
47
+ return typeof id === 'string' && id !== '' ? id : null;
48
+ }
49
+ catch (error) {
50
+ options.logger?.debug('ds-balance: account identity unreadable', {
51
+ error: describeError(error),
52
+ });
53
+ return null;
54
+ }
55
+ },
56
+ async readBalance() {
57
+ const result = await service.getBalance(client());
58
+ if (result === null)
59
+ return null;
60
+ if (result.status === 'failed') {
61
+ // 502 只是给分类用的:账号服务的查询失败属于上游不可用,不是「没有额度」。
62
+ throw new UpstreamError(502, 'account balance query failed');
63
+ }
64
+ return { wallets: result.value, bonusWallets: result.bonusWallets };
65
+ },
66
+ };
67
+ }
68
+ //# sourceMappingURL=host-account.js.map