dsh-ds-balance 2.1.3 → 2.3.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 +71 -18
- package/README_en.md +76 -18
- package/lib/adapters/domain-core-store.d.ts +46 -2
- package/lib/adapters/domain-core-store.js +144 -4
- package/lib/adapters/host-account.d.ts +66 -0
- package/lib/adapters/host-account.js +68 -0
- package/lib/client.js +437 -95
- package/lib/domain/account.d.ts +40 -0
- package/lib/domain/account.js +74 -0
- package/lib/domain/balance.d.ts +40 -1
- package/lib/domain/balance.js +20 -1
- package/lib/domain/normalize.d.ts +3 -2
- package/lib/domain/normalize.js +3 -2
- package/lib/domain/severity.d.ts +8 -2
- package/lib/domain/severity.js +11 -4
- package/lib/domain/situation.d.ts +104 -0
- package/lib/domain/situation.js +93 -0
- package/lib/http/handlers.d.ts +1 -1
- package/lib/http/handlers.js +29 -5
- package/lib/http/wire.d.ts +11 -1
- package/lib/http/wire.js +2 -0
- package/lib/index.js +122 -6
- package/lib/ports/account.d.ts +27 -0
- package/lib/ports/account.js +9 -0
- package/lib/ports/core-store.d.ts +15 -0
- package/lib/services/balance-service.d.ts +66 -94
- package/lib/services/balance-service.js +132 -196
- package/lib/services/balance-source.d.ts +187 -0
- package/lib/services/balance-source.js +320 -0
- package/lib/services/scheduler.d.ts +8 -2
- package/lib/services/scheduler.js +1 -1
- package/lib/services/source-readers.d.ts +28 -0
- package/lib/services/source-readers.js +83 -0
- package/lib/services/source-selector.d.ts +50 -0
- package/lib/services/source-selector.js +64 -0
- package/lib/types/client/api-types.d.ts +16 -0
- package/lib/types/client/data.d.ts +12 -3
- package/lib/types/client/index.d.ts +6 -0
- package/lib/types/client/locales.d.ts +10 -18
- package/lib/types/client/mock/index.d.ts +12 -11
- package/lib/types/client/mock/scenarios.d.ts +13 -5
- package/lib/types/client/model.d.ts +24 -25
- package/lib/types/client/route-hint.d.ts +61 -0
- package/lib/types/client/sidebar/BalancePopover.d.ts +7 -0
- package/lib/types/client/sidebar/PercentRing.d.ts +13 -5
- package/lib/types/client/sidebar/SidebarBalance.d.ts +9 -1
- package/lib/types/client/situation.d.ts +164 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +25 -23
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
31
|
> [!NOTE]
|
|
32
|
-
> **余额读自 DeepSeek
|
|
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
|
-
| 余额浮层 | 点条目展开:总额、赠送 /
|
|
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)
|
|
77
|
-
- 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@
|
|
110
|
+
npm install -g @deepseek-ai/dsh@next # 本插件承诺支持的线(RC 线;npm 上没有 rc 这个 tag)
|
|
83
111
|
```
|
|
84
112
|
|
|
85
|
-
|
|
113
|
+
兼容性怎么保证、哪条线该跟,见[版本兼容](#版本兼容)。
|
|
86
114
|
|
|
87
115
|
### 从 npm 安装
|
|
88
116
|
|
|
@@ -104,6 +132,9 @@ dsh plugin --profile web add "$PWD"
|
|
|
104
132
|
|
|
105
133
|
与 npm 那条路一样,重启后生效。
|
|
106
134
|
|
|
135
|
+
⚠️ 上面那步 `npm install` 不能省:`link:` 装法下依赖由仓库自己提供(见 [AGENTS.md](AGENTS.md)),
|
|
136
|
+
漏装则插件显示「未运行」(`ERR_MODULE_NOT_FOUND`)。
|
|
137
|
+
|
|
107
138
|
### 发现与安装
|
|
108
139
|
|
|
109
140
|
- **npm**:[`dsh-ds-balance`](https://www.npmjs.com/package/dsh-ds-balance)
|
|
@@ -113,10 +144,18 @@ dsh plugin --profile web add "$PWD"
|
|
|
113
144
|
|
|
114
145
|
## 版本兼容
|
|
115
146
|
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
147
|
+
- **需要哪一版宿主**:以 [package.json](package.json) 的声明为准,本插件跟的是宿主当前那条 **RC 线**。
|
|
148
|
+
需要配置界面,就把宿主升到那一版或更高:`npm install -g @deepseek-ai/dsh@next`。
|
|
149
|
+
- **声明是「窄」的**:下限是我们实测过的那一版,用 `>=` 而**不设上限** —— 不承诺「以后都兼容」。
|
|
150
|
+
(它罩得住**同一 `major.minor.patch` 内**后续的预发布版本;宿主换一个补丁位再推预发布,
|
|
151
|
+
这条下限就罩不住了,届时要再抬一次。为什么见[兼容性](docs/PUBLISHING.md#兼容性)。)
|
|
152
|
+
- **下限为什么抬上去**:不是换了界面位置,而是**设置接缝换过**(旧宿主上没有本插件依赖的那两样东西,
|
|
153
|
+
浏览器半边会整个不渲染 —— 圆环与浮层也一起消失)。插件不查宿主版本号,而是**当场探测那个能力**;
|
|
154
|
+
拿不到时浮层里会多一行英文提示,说明配置界面为什么不可用、该往哪儿升级。
|
|
155
|
+
技术细节见 [配置表单的能力探测](docs/ARCHITECTURE.md)。
|
|
156
|
+
- **兼容性不是推断出来的**:每周由 [compat.yml](.github/workflows/compat.yml) 在 `next`(RC 线)上
|
|
157
|
+
换包实跑一遍现有测试,另有一步单独判声明面罩不罩得住;红了会开或更新一条固定标题的跟踪 issue。
|
|
158
|
+
当前结论与红了怎么办见 [兼容性](docs/PUBLISHING.md#兼容性)。
|
|
120
159
|
|
|
121
160
|
## 配置
|
|
122
161
|
|
|
@@ -131,9 +170,9 @@ dsh plugin --profile web add "$PWD"
|
|
|
131
170
|
- **连接**:API 地址与凭据,两项都默认留空 —— 地址留空即用 DeepSeek 官方端点,凭据继承官方模型页那一份、只读不可改;
|
|
132
171
|
二级「自定义设置」里只放凭据引用名。
|
|
133
172
|
- **展示**:金额用哪种币种,或让它自动跟随账户。
|
|
134
|
-
- **阈值**:每个币种两档提醒线(预警 /
|
|
135
|
-
|
|
136
|
-
|
|
173
|
+
- **阈值**:每个币种两档提醒线(预警 / 告急)。同一币种内**告急必须严格低于预警**,相等也会被拒绝
|
|
174
|
+
(保存时就会拦下来);但**手改配置文件写成的非法组合不会再报错** ——
|
|
175
|
+
插件读到的那一刻会把这一对**回落成默认值**并在日志里记一次。
|
|
137
176
|
|
|
138
177
|
> **升级提示**:设置项存在插件那一行的配置里,键名跟着宿主升级变过一次,**旧值不会自动迁移** —— 升级后请照上表重填一次。
|
|
139
178
|
- **刷新**:服务端刷新周期、界面轮询周期与手动刷新冷却。冷却从**刷完那一刻**起算,
|
|
@@ -145,7 +184,9 @@ dsh plugin --profile web add "$PWD"
|
|
|
145
184
|
|
|
146
185
|
- 环里填多少 = 当前余额占该币种**预警线**的比例,100% 封顶;告急线不参与画环 —— 它已经决定了颜色。
|
|
147
186
|
- 颜色只表达状态,与金额大小无关:正常、偏低、告急各一色;账户读不到时另画一个带叉号的环。
|
|
148
|
-
-
|
|
187
|
+
- 没配阈值的币种退回按状态定性:**正常画满环,偏低 3/4,告急 1/4**。
|
|
188
|
+
(只有**有余额可看**的那一族画弧 —— 上面三档,加上「数据已过期」时的那份旧数字;
|
|
189
|
+
账户读不到、账户停用、没接入、账户没钱这些**都不画弧**,环靠颜色与中心记号说话。)
|
|
149
190
|
- 为什么颜色不由金额算、阈值为什么只当刻度 → [数据流](docs/ARCHITECTURE.md#数据流)。
|
|
150
191
|
|
|
151
192
|
### 凭据
|
|
@@ -153,6 +194,18 @@ dsh plugin --profile web add "$PWD"
|
|
|
153
194
|
密钥只经 DSH 的凭据通道解析,卡片里的 `API Key` 一栏默认继承官方模型页配好的那一份,且只读不可改。
|
|
154
195
|
由启动环境(环境变量)提供时,字段只读,徽标写明来源。
|
|
155
196
|
|
|
197
|
+
**余额有两条官方取数路,插件自己选**:模型页 / 环境变量配的 API Key 一条,**DSH 账号登录**一条
|
|
198
|
+
(只登录、没配 Key 也能看到余额)。选择顺序是:
|
|
199
|
+
|
|
200
|
+
- **当前会话在用的那条** —— 切会话、换模型都跟着换;
|
|
201
|
+
- 读不到会话就按全局默认模型的路由;
|
|
202
|
+
- 都判不出来时 **Key 优先、账号兜底**;
|
|
203
|
+
- 两条都没有,才是「尚未配置凭据」。
|
|
204
|
+
|
|
205
|
+
浮层标题右边那个括号就是这份数字的来源:`(API Key)` / `(账号登录)`。**显示哪个、刷新就刷哪个、标签也写哪个** —— 三者始终同一条。
|
|
206
|
+
|
|
207
|
+
**这个插件只记官方的那一个数字**,所以首选那条读不到时会自动退到另一条官方路:只要账号或 API Key 有一个能用,就照常显示(哪怕是一份稍旧的快照)。**两条路都拿不到数字,才会提示** —— 那才是真的没接入官方。左栏那个环这时长什么样,见[一眼看懂那个环](#一眼看懂那个环)。
|
|
208
|
+
|
|
156
209
|
**换了 key 多久生效,跟着 DSH 的层走 —— 本插件不额外追:**
|
|
157
210
|
|
|
158
211
|
- **启动环境(环境变量)给的 key:改了要重启 DSH。** 进程的环境在它启动那一刻就定死了,之后再改外面那个变量,跑着的插件看不到。
|
|
@@ -165,8 +218,8 @@ dsh plugin --profile web add "$PWD"
|
|
|
165
218
|
|
|
166
219
|
- **API Key 永不返回界面**:配置接口只回一个固定长度的掩码串,连末几位也不给。
|
|
167
220
|
- 密钥不落日志、不落本插件的文件;设置文件里只有引用名,卡片可以安全截图或分享。
|
|
168
|
-
- 余额快照存在 DSH
|
|
169
|
-
-
|
|
221
|
+
- 余额快照存在 DSH 自己的数据目录里,按凭据派生出的标识分组 —— **换 key 自动开新账本**,旧快照不会被混用。
|
|
222
|
+
- 那份标识还要 DSH home 目录下一个随机盐文件参与派生;**它丢了,旧快照就读不回来**(不会串到别人账上)。
|
|
170
223
|
- 只访问 `api.deepseek.com`,不代理、不转发其他流量。
|
|
171
224
|
|
|
172
225
|
## 许可
|
package/README_en.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
31
|
> [!NOTE]
|
|
32
|
-
> **The balance is read from the official
|
|
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
|
|
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
|
|
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
|
|
77
|
-
- Node `>= 20` (
|
|
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**:
|
|
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@
|
|
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 `
|
|
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
|
|
|
@@ -104,6 +134,10 @@ dsh plugin --profile web add "$PWD"
|
|
|
104
134
|
|
|
105
135
|
Same as the npm route: it takes effect after a restart.
|
|
106
136
|
|
|
137
|
+
⚠️ The `npm install` above is not optional: under the `link:` form the repository supplies its
|
|
138
|
+
own dependencies (see [AGENTS.md](AGENTS.md)) — skip it and the plugin shows as "not running"
|
|
139
|
+
(`ERR_MODULE_NOT_FOUND`).
|
|
140
|
+
|
|
107
141
|
### Discovery
|
|
108
142
|
|
|
109
143
|
- **npm**: [`dsh-ds-balance`](https://www.npmjs.com/package/dsh-ds-balance)
|
|
@@ -113,10 +147,21 @@ The repository carries the GitHub topic [`dsh-plugin`](https://github.com/topics
|
|
|
113
147
|
|
|
114
148
|
## Version compatibility
|
|
115
149
|
|
|
116
|
-
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
150
|
+
- **Which Host version is required**: whatever [package.json](package.json) declares; this plugin follows the host's current **RC line**.
|
|
151
|
+
To get the configuration page, upgrade the Host to that version or higher: `npm install -g @deepseek-ai/dsh@next`.
|
|
152
|
+
- **The declaration is narrow**: the floor is the version we actually tested, written as `>=` with **no ceiling** —
|
|
153
|
+
it claims no "everything in the future counts".
|
|
154
|
+
(It does cover later prereleases **within the same `major.minor.patch`**; once the host moves a patch position and
|
|
155
|
+
ships another prerelease, this floor no longer covers it and has to be raised again. Why: [Compatibility](docs/PUBLISHING.md#兼容性).)
|
|
156
|
+
- **Why the floor moved**: not a change of where the UI lives, but a change of the **settings seam**
|
|
157
|
+
(without the two things this plugin relies on, the whole client half never renders — the ring and the popover go with it).
|
|
158
|
+
The plugin never reads a Host version; it **probes that capability live**, and when it is absent the popover carries
|
|
159
|
+
one extra English line saying why the configuration page is unavailable and where to upgrade.
|
|
160
|
+
Technical detail: see [Architecture](docs/ARCHITECTURE.md).
|
|
161
|
+
- **Compatibility is not inferred**: a weekly [compat.yml](.github/workflows/compat.yml) run swaps packages on both the
|
|
162
|
+
`alpha` and `next` lines and re-runs the existing tests, plus a separate check that the declared range still covers the
|
|
163
|
+
tracked line; a failure opens or updates a tracking issue with a fixed title. Current conclusions and what to do when
|
|
164
|
+
it goes red: see [Compatibility](docs/PUBLISHING.md#兼容性).
|
|
120
165
|
|
|
121
166
|
## Configuration
|
|
122
167
|
|
|
@@ -131,9 +176,8 @@ Open **Plugins → Installed** and step into the **DeepSeek Balance** (shown as
|
|
|
131
176
|
- **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
177
|
The nested "Customised settings" holds only the credential reference name.
|
|
133
178
|
- **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
|
-
|
|
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.
|
|
179
|
+
- **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.
|
|
180
|
+
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
181
|
|
|
138
182
|
> **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
183
|
- **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 +188,9 @@ Saving applies immediately; there is no need to restart DSH.
|
|
|
144
188
|
|
|
145
189
|
- 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
190
|
- 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
|
|
191
|
+
- A currency with no threshold configured falls back to state: **a full ring for normal, 3/4 for low, 1/4 for critical**.
|
|
192
|
+
(Only the family with a balance to look at draws an arc — those three, plus the old number while data is stale;
|
|
193
|
+
an unreadable account, a suspended account, nothing connected, an account with no balance: none of those draw an arc.)
|
|
148
194
|
- Why colour is never computed from an amount, and why thresholds are only a scale → [Data flow](docs/ARCHITECTURE.md#数据流).
|
|
149
195
|
|
|
150
196
|
### Credentials
|
|
@@ -152,6 +198,18 @@ Saving applies immediately; there is no need to restart DSH.
|
|
|
152
198
|
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
199
|
When it comes from the launch environment (an environment variable) the field is read-only and the badge says where it came from.
|
|
154
200
|
|
|
201
|
+
**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:
|
|
202
|
+
|
|
203
|
+
- **the route the current session is on** — switching sessions *or the model in this session* re-reads it at once;
|
|
204
|
+
- with no session readable, the globally selected model's route;
|
|
205
|
+
- with neither decidable, **key first, account as the fallback**;
|
|
206
|
+
- only when both are missing does it read "no credential configured".
|
|
207
|
+
|
|
208
|
+
The parenthesised label to the right of the popover title is where that number came from: `(API key)` / `(account sign-in)`.
|
|
209
|
+
**Whichever is shown is also the one that gets refreshed and named** — display, refresh and label never disagree.
|
|
210
|
+
|
|
211
|
+
**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).
|
|
212
|
+
|
|
155
213
|
**How long a key change takes to land follows DSH's own layers — this plugin deliberately adds nothing on top:**
|
|
156
214
|
|
|
157
215
|
- **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 +222,8 @@ Why it is this way → [Key decisions](docs/ARCHITECTURE.md#关键决策).
|
|
|
164
222
|
|
|
165
223
|
- **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
224
|
- 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
|
|
168
|
-
- That identifier also involves a
|
|
225
|
+
- 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.
|
|
226
|
+
- 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
227
|
- Only `api.deepseek.com` is contacted; nothing is proxied or forwarded.
|
|
170
228
|
|
|
171
229
|
## License
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 打开失败要**降级**而不是抛出去 —— 未观察的 rejection 曾把宿主整个拖下水。
|
|
8
8
|
* @module dsh-ds-balance/adapters/domain-core-store
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
10
|
+
import { type BalanceSnapshot, type 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:
|
|
27
|
+
source: BalanceSource;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* 域声明。
|
|
@@ -41,8 +41,11 @@ export declare const DS_BALANCE_DOMAIN: {
|
|
|
41
41
|
/** 表句柄的最小结构化面;测试用替身实现它。 */
|
|
42
42
|
export interface KvTableLike {
|
|
43
43
|
get(key: string): StoredSnapshot | undefined;
|
|
44
|
+
/** 一次迭代同时拿到 `[key, record]`;比 `keys()` +逐键 `get()` 少一轮查找。 */
|
|
45
|
+
entries(): IterableIterator<[string, StoredSnapshot]>;
|
|
44
46
|
keys(): IterableIterator<string>;
|
|
45
47
|
put(key: string, value: StoredSnapshot): Promise<void>;
|
|
48
|
+
delete(key: string): Promise<boolean>;
|
|
46
49
|
}
|
|
47
50
|
/** 域句柄的最小结构化面。 */
|
|
48
51
|
export interface DomainLike {
|
|
@@ -95,8 +98,49 @@ export declare class DomainCoreStore implements CoreStore {
|
|
|
95
98
|
*
|
|
96
99
|
* **按 `accountTag` 过滤**:凭据轮换后 tag 会变,旧快照不得混用。
|
|
97
100
|
* 用 `snapshotId`(单调可排序)决定新旧,不依赖 `fetchedAt`。
|
|
101
|
+
*
|
|
102
|
+
* **走 `entries()` 而不是 `keys()` + 逐键 `get()`**:后者是每条记录两次查找,
|
|
103
|
+
* 记录数涨上去(保留策略落地前实测过千条)时白费一大半;`entries()` 一次迭代给全。
|
|
98
104
|
*/
|
|
99
105
|
loadLatestSnapshot(accountTag: string): Promise<BalanceSnapshot | null>;
|
|
106
|
+
/**
|
|
107
|
+
* 只保留该账本最近 `keepN` 条,删掉更旧的(跨账本不动)。
|
|
108
|
+
*
|
|
109
|
+
* **批间让出事件循环的理由是摊平写放大,不是防限流**:宿主 json 后端默认 `single`
|
|
110
|
+
* 布局,每次 `delete` 都要原子重写整份单元文件。删几十条若挤在同一个 tick 上,
|
|
111
|
+
* 会连带把同一进程里的取数与轮询一起拖住。所以内部**分批 + 每批之后让出一次** ——
|
|
112
|
+
* 批次大小与间隔属于实现细节,不上端口(领域层只调一次,不需要知道分了几批)。
|
|
113
|
+
*
|
|
114
|
+
* **单次最多删 {@link PRUNE_MAX_PER_CALL} 条**:超出部分留给后续启动或写入路径,
|
|
115
|
+
* 保证任何一次调用的耗时都有确定上界(理由见那个常量)。
|
|
116
|
+
*
|
|
117
|
+
* **零写入的幂等**:没超期时一条都不删、返回 0(超期判定先做完再动手)。
|
|
118
|
+
*
|
|
119
|
+
* @param accountTag - 要修剪的账本。
|
|
120
|
+
* @param keepN - 保留条数(新到旧)。
|
|
121
|
+
* @returns 本次**实际**删掉的条数(可能小于超期总数,因为有单次上限)。
|
|
122
|
+
*/
|
|
123
|
+
pruneByTag(accountTag: string, keepN: number): Promise<number>;
|
|
124
|
+
/**
|
|
125
|
+
* 修剪一个账本的内部实现(不重复 `requireTable` 的开表开销)。
|
|
126
|
+
*/
|
|
127
|
+
private pruneOwned;
|
|
128
|
+
/**
|
|
129
|
+
* 启动时的一次性清理:把**所有**账本桶各修剪一次。
|
|
130
|
+
*
|
|
131
|
+
* 为什么扫全表而不是只清「当前那个账本」:启动这一刻还不知道会用哪个 `accountTag`
|
|
132
|
+
* (要等 reader 给),而首次升级的迁移诉求是「把所有攒下来的桶都收一遍」——
|
|
133
|
+
* 只清一个桶的话,凭据轮换出来的旧桶永远清不掉,文件就一直大。
|
|
134
|
+
*
|
|
135
|
+
* **失败绝不影响 open**:清理抛错只记一条 warn。这里抛出去会让
|
|
136
|
+
* `ensureOpen` 判成打开失败,整个存储被打成降级 —— 那比慢更糟。
|
|
137
|
+
* 删到一半也是安全的:清理幂等,下次启动继续。
|
|
138
|
+
*/
|
|
139
|
+
private pruneAllLedgersOnOpen;
|
|
140
|
+
/** 取出属于该账本的全部记录。 */
|
|
141
|
+
private collectOwned;
|
|
142
|
+
/** 分批删除,每批之后让出一次事件循环。 */
|
|
143
|
+
private deleteInBatches;
|
|
100
144
|
health(): Promise<StoreHealth>;
|
|
101
145
|
/** 幂等关闭。**必须挂在 `ctx.effect` 的 disposer 上。** */
|
|
102
146
|
close(): Promise<void>;
|
|
@@ -9,10 +9,35 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
+
import { SNAPSHOT_KEEP_N, } from '../domain/balance.js';
|
|
12
13
|
import { StorageError, describeError } from '../domain/errors.js';
|
|
13
14
|
import { formatMoney, parseMoney } from '../domain/money.js';
|
|
14
15
|
/** 域里的表名。 */
|
|
15
16
|
export const SNAPSHOT_TABLE = 'snapshots';
|
|
17
|
+
/**
|
|
18
|
+
* 修剪时每批删多少条,之后让出一次事件循环。
|
|
19
|
+
*
|
|
20
|
+
* **实现细节,不上端口**:领域层只调一次 `pruneByTag`,不需要知道分了几批。
|
|
21
|
+
* 取值只是「别让一批太大」的粗界,真实瓶颈是单布局下的整份重写次数,
|
|
22
|
+
* 所以调大调小不影响正确性,只影响同进程别的活被挤住多久。
|
|
23
|
+
*/
|
|
24
|
+
const PRUNE_BATCH_SIZE = 40;
|
|
25
|
+
/**
|
|
26
|
+
* 单次修剪最多删多少条,**之后剩下的交给后续启动或写入路径**。
|
|
27
|
+
*
|
|
28
|
+
* **为什么需要上界**:`single` 布局下每次 `delete` 都要原子重写整份单元文件,
|
|
29
|
+
* 而「删掉一批」的开销随文件体积线性涨。首次升级时存量可能很大(本机实测 9000 条 /
|
|
30
|
+
* 3.8 MB),若一次性清空就是数千次整份重写,**会把宿主连同 Agent 一起卡死**
|
|
31
|
+
* —— 实测现象是侧栏无限转圈、Agent 停止响应,清空数据后立刻恢复。
|
|
32
|
+
*
|
|
33
|
+
* 这个数是**实现约束**不是领域策略:领域层只说「留几条」,「一次最多删多少」
|
|
34
|
+
* 属于「别把自己卡死」的工程判断,所以不上端口(与批次大小同理)。
|
|
35
|
+
*
|
|
36
|
+
* 取 500 的依据:配合 {@link PRUNE_BATCH_SIZE} 是 12.5 批,批次边界清晰;
|
|
37
|
+
* 收敛靠**多次启动叠加**而不是一次删完 —— 清到 20 条这个终态是幂等的,
|
|
38
|
+
* 每次启动删一批,未删完的下次继续。
|
|
39
|
+
*/
|
|
40
|
+
const PRUNE_MAX_PER_CALL = 500;
|
|
16
41
|
const storedBalanceSchema = z.object({
|
|
17
42
|
currency: z.string(),
|
|
18
43
|
total: z.string(),
|
|
@@ -25,7 +50,9 @@ const storedSnapshotSchema = z.object({
|
|
|
25
50
|
fetchedAt: z.number(),
|
|
26
51
|
isAvailable: z.boolean(),
|
|
27
52
|
balances: z.array(storedBalanceSchema),
|
|
28
|
-
|
|
53
|
+
// **取值只增不改**:域版本一动,已落盘的账本会在 open 时直接 `version-mismatch` 拒开。
|
|
54
|
+
// 旧记录里只有 `deepseek-http`,它必须一直是合法值。
|
|
55
|
+
source: z.enum(['deepseek-http', 'deepseek-account']),
|
|
29
56
|
});
|
|
30
57
|
/**
|
|
31
58
|
* 域声明。
|
|
@@ -125,6 +152,10 @@ export class DomainCoreStore {
|
|
|
125
152
|
this.handle = handle;
|
|
126
153
|
this.table = handle.table(SNAPSHOT_TABLE);
|
|
127
154
|
this.openError = undefined;
|
|
155
|
+
// 首次升级的一次性清理:必须在**任何读取之前**做,否则首次
|
|
156
|
+
// loadLatestSnapshot 仍要面对存量的大文件。等下一次抓取再清太晚 ——
|
|
157
|
+
// 那正是「保留策略只在稳态有效、首次升级要等两小时」那个盲区。
|
|
158
|
+
await this.pruneAllLedgersOnOpen(SNAPSHOT_KEEP_N);
|
|
128
159
|
}
|
|
129
160
|
/** 拿到可用表,或在降级状态下抛出可读错误。 */
|
|
130
161
|
async requireTable() {
|
|
@@ -147,19 +178,128 @@ export class DomainCoreStore {
|
|
|
147
178
|
*
|
|
148
179
|
* **按 `accountTag` 过滤**:凭据轮换后 tag 会变,旧快照不得混用。
|
|
149
180
|
* 用 `snapshotId`(单调可排序)决定新旧,不依赖 `fetchedAt`。
|
|
181
|
+
*
|
|
182
|
+
* **走 `entries()` 而不是 `keys()` + 逐键 `get()`**:后者是每条记录两次查找,
|
|
183
|
+
* 记录数涨上去(保留策略落地前实测过千条)时白费一大半;`entries()` 一次迭代给全。
|
|
150
184
|
*/
|
|
151
185
|
async loadLatestSnapshot(accountTag) {
|
|
152
186
|
const table = await this.requireTable();
|
|
153
187
|
let newest = null;
|
|
154
|
-
for (const
|
|
155
|
-
|
|
156
|
-
if (record === undefined || record.accountTag !== accountTag)
|
|
188
|
+
for (const [, record] of table.entries()) {
|
|
189
|
+
if (record.accountTag !== accountTag)
|
|
157
190
|
continue;
|
|
158
191
|
if (newest === null || record.snapshotId > newest.snapshotId)
|
|
159
192
|
newest = record;
|
|
160
193
|
}
|
|
161
194
|
return newest === null ? null : fromStored(newest);
|
|
162
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* 只保留该账本最近 `keepN` 条,删掉更旧的(跨账本不动)。
|
|
198
|
+
*
|
|
199
|
+
* **批间让出事件循环的理由是摊平写放大,不是防限流**:宿主 json 后端默认 `single`
|
|
200
|
+
* 布局,每次 `delete` 都要原子重写整份单元文件。删几十条若挤在同一个 tick 上,
|
|
201
|
+
* 会连带把同一进程里的取数与轮询一起拖住。所以内部**分批 + 每批之后让出一次** ——
|
|
202
|
+
* 批次大小与间隔属于实现细节,不上端口(领域层只调一次,不需要知道分了几批)。
|
|
203
|
+
*
|
|
204
|
+
* **单次最多删 {@link PRUNE_MAX_PER_CALL} 条**:超出部分留给后续启动或写入路径,
|
|
205
|
+
* 保证任何一次调用的耗时都有确定上界(理由见那个常量)。
|
|
206
|
+
*
|
|
207
|
+
* **零写入的幂等**:没超期时一条都不删、返回 0(超期判定先做完再动手)。
|
|
208
|
+
*
|
|
209
|
+
* @param accountTag - 要修剪的账本。
|
|
210
|
+
* @param keepN - 保留条数(新到旧)。
|
|
211
|
+
* @returns 本次**实际**删掉的条数(可能小于超期总数,因为有单次上限)。
|
|
212
|
+
*/
|
|
213
|
+
async pruneByTag(accountTag, keepN) {
|
|
214
|
+
const table = await this.requireTable();
|
|
215
|
+
return this.pruneOwned(table, accountTag, keepN);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 修剪一个账本的内部实现(不重复 `requireTable` 的开表开销)。
|
|
219
|
+
*/
|
|
220
|
+
async pruneOwned(table, accountTag, keepN) {
|
|
221
|
+
if (keepN < 0)
|
|
222
|
+
return 0;
|
|
223
|
+
const owned = this.collectOwned(table, accountTag);
|
|
224
|
+
if (owned.length <= keepN)
|
|
225
|
+
return 0;
|
|
226
|
+
// 先按 snapshotId 排好序再决定删哪些:留下的必然是「最新的 keepN 条」,
|
|
227
|
+
// 而刚写入的那条 id 单调最大,所以永远在保留集里。
|
|
228
|
+
const ordered = owned
|
|
229
|
+
.slice()
|
|
230
|
+
.sort((a, b) => (a.snapshotId < b.snapshotId ? -1 : a.snapshotId > b.snapshotId ? 1 : 0));
|
|
231
|
+
const doomed = ordered.slice(0, ordered.length - keepN).slice(0, PRUNE_MAX_PER_CALL);
|
|
232
|
+
await this.deleteInBatches(table, doomed);
|
|
233
|
+
return doomed.length;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 启动时的一次性清理:把**所有**账本桶各修剪一次。
|
|
237
|
+
*
|
|
238
|
+
* 为什么扫全表而不是只清「当前那个账本」:启动这一刻还不知道会用哪个 `accountTag`
|
|
239
|
+
* (要等 reader 给),而首次升级的迁移诉求是「把所有攒下来的桶都收一遍」——
|
|
240
|
+
* 只清一个桶的话,凭据轮换出来的旧桶永远清不掉,文件就一直大。
|
|
241
|
+
*
|
|
242
|
+
* **失败绝不影响 open**:清理抛错只记一条 warn。这里抛出去会让
|
|
243
|
+
* `ensureOpen` 判成打开失败,整个存储被打成降级 —— 那比慢更糟。
|
|
244
|
+
* 删到一半也是安全的:清理幂等,下次启动继续。
|
|
245
|
+
*/
|
|
246
|
+
async pruneAllLedgersOnOpen(keepN) {
|
|
247
|
+
const table = this.table;
|
|
248
|
+
if (table === null)
|
|
249
|
+
return;
|
|
250
|
+
try {
|
|
251
|
+
const buckets = new Map();
|
|
252
|
+
for (const [, record] of table.entries()) {
|
|
253
|
+
const bucket = buckets.get(record.accountTag);
|
|
254
|
+
if (bucket === undefined)
|
|
255
|
+
buckets.set(record.accountTag, [record]);
|
|
256
|
+
else
|
|
257
|
+
bucket.push(record);
|
|
258
|
+
}
|
|
259
|
+
let removed = 0;
|
|
260
|
+
let remaining = 0;
|
|
261
|
+
for (const accountTag of buckets.keys()) {
|
|
262
|
+
removed += await this.pruneOwned(table, accountTag, keepN);
|
|
263
|
+
}
|
|
264
|
+
for (const bucket of buckets.values())
|
|
265
|
+
remaining += Math.min(bucket.length, keepN);
|
|
266
|
+
if (removed > 0) {
|
|
267
|
+
// 进度日志:删了多少、之后大约还剩多少(每次启动还会再删一批,直到收敛)。
|
|
268
|
+
// 没有它,使用者只会看到「数据怎么还在变多」,无从判断清理是否在推进。
|
|
269
|
+
this.options.logger?.debug('ds-balance: pruned stored snapshots on open', {
|
|
270
|
+
removed,
|
|
271
|
+
remainingApprox: remaining,
|
|
272
|
+
buckets: buckets.size,
|
|
273
|
+
keepN,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
// 刻意只记一条 warn:清理是尽力而为,不该把存储打成降级。
|
|
279
|
+
this.options.logger?.warn('ds-balance: startup snapshot prune failed, continuing', {
|
|
280
|
+
error: describeError(error),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** 取出属于该账本的全部记录。 */
|
|
285
|
+
collectOwned(table, accountTag) {
|
|
286
|
+
const owned = [];
|
|
287
|
+
for (const [, record] of table.entries()) {
|
|
288
|
+
if (record.accountTag === accountTag)
|
|
289
|
+
owned.push(record);
|
|
290
|
+
}
|
|
291
|
+
return owned;
|
|
292
|
+
}
|
|
293
|
+
/** 分批删除,每批之后让出一次事件循环。 */
|
|
294
|
+
async deleteInBatches(table, doomed) {
|
|
295
|
+
for (let offset = 0; offset < doomed.length; offset += PRUNE_BATCH_SIZE) {
|
|
296
|
+
const batch = doomed.slice(offset, offset + PRUNE_BATCH_SIZE);
|
|
297
|
+
for (const record of batch)
|
|
298
|
+
await table.delete(record.snapshotId);
|
|
299
|
+
// 让出一次事件循环:把同 tick 的连续原子重写摊开,别挤住同进程的别的活。
|
|
300
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
163
303
|
async health() {
|
|
164
304
|
if (this.closed)
|
|
165
305
|
return { ok: false, detail: 'closed' };
|