deepseek-balance-dsh 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,110 @@
1
+ # Changelog
2
+
3
+ Notable changes to the plugin. The repository is `DSH-DeepSeek-Balance`; the package
4
+ is published on npm as [`deepseek-balance-dsh`](https://www.npmjs.com/package/deepseek-balance-dsh)
5
+ (the npm name `dsh-deepseek-balance` belongs to an unrelated project). Git tags mark
6
+ releases; installation goes through the harness plugin manager or a local checkout
7
+ (see the README).
8
+
9
+ ## 0.2.1 — 2026-10-22
10
+
11
+ **First npm publication.** No behaviour changes; the distribution identity changed.
12
+
13
+ - Renamed the package to `deepseek-balance-dsh` so it can be published: the npm name
14
+ `dsh-deepseek-balance` is held by an unrelated project (published 2026-08-14).
15
+ The repository, the state file (`~/.deepseek-balance.json`) and the agent tool
16
+ (`deepseek_balance`) keep their names, so existing configuration and model usage
17
+ are unaffected.
18
+ - Everything that must match the package name follows it: the client artifact's
19
+ `__ModuleLoader__` id and the `insert` row in the bundled `cordis.patch.yml`.
20
+ **Upgrading requires reinstalling the plugin** (the installed directory name is
21
+ the package name) and restarting the profile — a 0.2.0 install will not load
22
+ `deepseek-balance-dsh` by itself.
23
+ - Added npm metadata (`repository`, `homepage`, `bugs`, `keywords`).
24
+
25
+ ## 0.2.0 — 2026-09-30
26
+
27
+ Security, structure and usability pass driven by an architecture review of the
28
+ 0.1.x code. Verified on deepseek-harness 0.2.0-rc.2 (web and desktop profiles).
29
+
30
+ ### Security
31
+
32
+ - **Every `/dsbal/*` route now passes the host's own request gate**
33
+ (`ctx.connection.admit()`): the same Host/Origin trust fence and browser-cookie
34
+ authentication the `/api` channel uses. Unauthenticated reads answer `401`,
35
+ cross-site requests `403`, a wrong method `405`, and a malformed body `400`
36
+ without touching any state. In 0.1.x the routes were registered bare on the web
37
+ server, so anything that could reach the port could read account data **and
38
+ replace the stored API key** with a cross-site "simple" POST.
39
+ - Credential writes remain user-only: the new agent tool can only *project* state
40
+ through `getState()`, so `setConfig()` is unreachable from a model by
41
+ construction rather than by policy.
42
+ - Input bounds: `fromMs` may not be in the future nor older than 90 days (an
43
+ unbounded value could drive up to 24 upstream usage requests per call),
44
+ credential fields are length-capped, and an API key is shape-checked (`sk-…`)
45
+ when the user saves one — never for values already in the state file.
46
+
47
+ ### Added
48
+
49
+ - **`config:` support** — `pollIntervalMs`, `historyDays`, `usageTtlMs`,
50
+ `usageErrorTtlMs`, `maxSnapshots`, `requestTimeoutMs` and the three endpoint URLs
51
+ are validated at activation and overridable from the profile patch. An invalid
52
+ value fails loudly, naming the field.
53
+ - **Bilingual widget** — ~70 user-visible strings moved into a zh/en table; the
54
+ language follows the host's settings preference, else the browser's, with an
55
+ explicit choice in the panel (remembered locally). Statutory-holiday and pricing
56
+ copy is phrased client-side from structured phase facts.
57
+ - **Plugin metadata** — `icon.svg`, `locale/zh.json`, `locale/en.json` and the
58
+ matching manifest fields, so the plugin manager card has a title, a description
59
+ and artwork instead of falling back to the package name.
60
+ - **Read-only agent tool `deepseek_balance`** — reports the price phase, balance,
61
+ period spend and token counters; `window` (and `fromMs` for `custom`) projects a
62
+ period *without* persisting it. Registered through an optional injection and
63
+ guarded: if the runtime ever stops accepting the definition, or the model-facing
64
+ schema projection drops it, the tool withdraws itself and logs why.
65
+ - **`storageError` in the state payload**, reported separately from upstream API
66
+ failures so the panel can distinguish "the local file is unusable" from "the
67
+ platform API failed".
68
+
69
+ ### Changed
70
+
71
+ - The host half is now an assembly layer over focused modules — `lib/phase.js`
72
+ (pricing phases + holiday calendar), `lib/window.js` (range, snapshot estimate,
73
+ monthly aggregation), `lib/deepseek.js` (HTTP client + parsing), `lib/validate.js`,
74
+ `lib/config.js`, `lib/store.js`, `lib/i18n.js`, `lib/service.js`, `lib/tool.js`.
75
+ `lib/index.js` went from 648 to 154 lines.
76
+ - The test suites import the real modules; they no longer slice source text out of
77
+ `lib/index.js`, so refactors stop failing on "source shape" assertions.
78
+ - The state file carries a `schemaVersion` envelope and is written atomically
79
+ (temp file + rename) by a serialised writer; 0.1.x files load unchanged and are
80
+ upgraded in place.
81
+ - The panel polls at the host-reported `pollIntervalMs` instead of a hard-coded
82
+ 30 s, pauses while the tab is hidden, and derives the countdown from a local
83
+ clock offset-corrected against the host.
84
+
85
+ ### Fixed
86
+
87
+ - A corrupt or future-versioned state file is **quarantined** as
88
+ `….corrupt-<timestamp>` and reported, instead of being silently overwritten by
89
+ the first poll (which also made the old "fresh state" path look like data loss).
90
+ - The panel header no longer wraps in English: the key mask and the language
91
+ choice moved into the settings block, and the header can no longer wrap.
92
+ - The token card no longer claims "add a userToken" when the period's total is
93
+ simply `0` (which is what "today" reads just past Beijing midnight); a failing
94
+ usage endpoint now reports its error instead.
95
+
96
+ ### Tests
97
+
98
+ - 42 → **123 cases** in 9 suites (phase, window, config, store, i18n, service,
99
+ tool, routes, package), all dependency-free Node assertions; CI runs
100
+ `npm run check && npm test` on a Node 20/24 matrix.
101
+
102
+ ## 0.1.0 — 2026-08-22 … 2026-09-29
103
+
104
+ Initial plugin: total/topped-up balance from the official endpoint, period spend
105
+ with a per-model split and the three token counters from the platform's private
106
+ usage endpoints (account-wide, with a balance-snapshot estimate as fallback), the
107
+ peak/off-peak (梁文峰 / 梁文谷) price reminder with Chinese statutory holidays, and a
108
+ draggable bottom-right widget. Along the way: strict `/dsbal/*` request validation,
109
+ a GitHub Actions test workflow, and the `dsh.bundle` declaration that lets the
110
+ plugin manager install the package as a profile layer.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-deepseek-balance contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,267 @@
1
- # Temporary Holding Version
1
+ # dsh-deepseek-balance
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![test](https://github.com/OwlNjust/DSH-DeepSeek-Balance/actions/workflows/test.yml/badge.svg)](https://github.com/OwlNjust/DSH-DeepSeek-Balance/actions/workflows/test.yml)
4
+
5
+ npm package: [`deepseek-balance-dsh`](https://www.npmjs.com/package/deepseek-balance-dsh) — the repository is `DSH-DeepSeek-Balance`; the unrelated npm name `dsh-deepseek-balance` belongs to a different project.
6
+
7
+ Topics: [`dsh-plugin`](https://github.com/topics/dsh-plugin) · [`deepseek-harness`](https://github.com/topics/deepseek-harness) · [`deepseek`](https://github.com/topics/deepseek)
8
+
9
+ > DeepSeek balance & usage monitor for **DeepSeek Harness** — a floating widget at the
10
+ > bottom-right corner with a peak/off-peak (梁文峰 / 梁文谷) price reminder.
11
+ >
12
+ > **[中文](README.zh.md) · English**
13
+
14
+ ---
15
+
16
+ **Installation is one step:** send `https://github.com/OwlNjust/DSH-DeepSeek-Balance`
17
+ to your agent — it knows how to install this plugin. Manual steps are below.
18
+
19
+ ## Installation
20
+
21
+ The plugin has **zero runtime dependencies** and is published on npm as
22
+ [`deepseek-balance-dsh`](https://www.npmjs.com/package/deepseek-balance-dsh). There are two routes — a **bundle-layer** install (npm name or this
23
+ repository) and a **manual patch-layer** install for a local checkout. **Pick exactly one per
24
+ profile**: using both inserts the plugin id twice (see "Agent notes").
25
+
26
+ ### Route A (recommended): install it as a profile *bundle* (deepseek-harness ≥ 0.2.0)
27
+
28
+ The package declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`, so the harness
29
+ plugin manager can install and activate it as a profile layer — **no file editing at all**.
30
+ Either install spec works:
31
+
32
+ - **npm name** (published package):
33
+
34
+ ```sh
35
+ dsh plugin --profile <profile> add deepseek-balance-dsh
36
+ ```
37
+
38
+ - **Git repository** (source of truth):
39
+
40
+ ```sh
41
+ dsh plugin --profile <profile> add https://github.com/OwlNjust/DSH-DeepSeek-Balance
42
+ ```
43
+
44
+ - **Desktop app**: menu bar → “Manage plugins” → enter either
45
+ `deepseek-balance-dsh` or `https://github.com/OwlNjust/DSH-DeepSeek-Balance`
46
+
47
+ The manager adds the package to the profile's `dsh.profile.bundles`, and the `cordis.patch.yml`
48
+ shipped inside the package (it carries the `insert` row that activates the plugin) is applied as
49
+ a bundle layer. After a reload/restart the bottom-right widget appears.
50
+
51
+ ### Route B: local checkout + the profile's patch layer (offline / source debugging)
52
+
53
+ ```sh
54
+ # 1. Clone or copy the repository anywhere
55
+ git clone https://github.com/OwlNjust/DSH-DeepSeek-Balance.git /tmp/dsh-deepseek-balance
56
+
57
+ # 2. Install it into the web profile (file: = snapshot; link: = live symlink)
58
+ cd ~/.dsh/profiles/web
59
+ dsh plugin --profile web add link:/tmp/dsh-deepseek-balance
60
+ ```
61
+
62
+ 3. Append this block to `~/.dsh/profiles/web/cordis.patch.yml` (the profile's
63
+ user patch layer):
64
+
65
+ ```yaml
66
+ - insert:
67
+ - id: deepseek-balance-dsh
68
+ name: deepseek-balance-dsh
69
+ ```
70
+
71
+ 4. Restart the profile:
72
+
73
+ ```sh
74
+ dsh web
75
+ ```
76
+
77
+ 5. Verify (optional): check that the web server answers
78
+
79
+ ```sh
80
+ curl -s http://127.0.0.1:3080/dsbal/state
81
+ ```
82
+
83
+ and that the bottom-right widget appears after a hard page refresh
84
+ (Ctrl+Shift+R). Have the user click the widget → **配置** → paste their
85
+ DeepSeek **API key** (and, optionally, the platform **userToken**) → **保存**.
86
+
87
+ ### Agent notes
88
+
89
+ - The npm package is **`deepseek-balance-dsh`**. Do not publish under the repository name
90
+ `dsh-deepseek-balance`: that name is held by an unrelated project on npm.
91
+ - **Do not mix the routes**: route A already inserts the plugin row from the bundle layer, so
92
+ also appending the same `insert` row by hand inserts the plugin id twice (cordis collapses
93
+ rows that share an id into one, so this **fails silently** — it only leaves a duplicate row
94
+ in the composed tree, which is easy to misread later). When switching, make sure only
95
+ `dsh.profile.bundles` or the profile's `cordis.patch.yml` carries it. One-line self-check —
96
+ the result must be `1`:
97
+
98
+ ```sh
99
+ dsh --profile web --dump-config | grep -c 'id: deepseek-balance-dsh'
100
+ ```
101
+
102
+ If it prints `2`, delete the hand-written block from `cordis.patch.yml` (keep the package in
103
+ `dsh.profile.bundles`, the bundle layer is maintained by the plugin manager) and restart
104
+ `dsh web`. To switch the other way, remove the package from `dsh.profile.bundles` first.
105
+ - Configuration and balance history are stored in `~/.deepseek-balance.json`
106
+ (mode 0600) — never echo its contents.
107
+ - Uninstall: remove the row from `cordis.patch.yml`, delete
108
+ `~/.dsh/profiles/web/node_modules/deepseek-balance-dsh`, restart `dsh web`.
109
+
110
+ ## Requirements
111
+
112
+ - A DeepSeek Harness deployment with the **web profile** (`dsh web`); Node.js ≥ 20
113
+ (adapted and verified on **v0.1.2-rc.1**, re-checked up to **v0.2.0-rc.2** — it has
114
+ worked unchanged across all four versions).
115
+ - A DeepSeek **API key** (platform.deepseek.com → API keys).
116
+ - Optional: platform **userToken** (platform.deepseek.com → DevTools → Application →
117
+ Local Storage) for exact cost/token stats. Without it, consumption falls back
118
+ to balance-snapshot estimates.
119
+
120
+ ## Features
121
+
122
+ - **Balance** — total and topped-up balance via the official `GET /user/balance`, polled every 5 min.
123
+ - **Period consumption** — today / 24h / 7d / custom start date, with per-model breakdown (`/usage/cost`).
124
+ - **Token usage** — cache-hit input / cache-miss input / output, identical to the platform dashboard (`/usage/amount`).
125
+ - **Price window reminder** — 🔴 **梁文峰** (peak: Beijing Mon–Fri 09:00–12:00 & 14:00–18:00,
126
+ **excluding Chinese statutory holidays**, ×2 price) vs 🟢 **梁文谷** (off-peak: everything else,
127
+ **including weekends and statutory holidays all day**, 50% price), live countdown,
128
+ red highlight during peak and a 🎉 badge on holidays. The price windows follow the
129
+ [official pricing docs](https://api-docs.deepseek.com/zh-cn/quick_start/pricing).
130
+ - **Draggable placement** — **right-click press-and-drag** the pill to move it anywhere;
131
+ the position is remembered automatically (left click simply expands the panel).
132
+ Right-clicking the pill never opens the browser menu (the pill is a drag handle by design).
133
+ The expanded panel auto-fits the viewport edges, and the pill is clamped back into view
134
+ after window resizes. A「重置位置(右下角)」button at the panel bottom restores the
135
+ default bottom-right position.
136
+
137
+ ## Usage
138
+
139
+ | Thing | Where |
140
+ | --- | --- |
141
+ | API key / userToken | widget → 配置 |
142
+ | Time window | widget chips (今天 / 24h / 7天 / custom) |
143
+ | Refresh | 刷新 button (or automatic, at the configured `pollIntervalMs` — 5 min by default; paused while the tab is hidden) |
144
+ | Language | follows the host's preference, else the browser's; the `EN/中` button in the panel header switches manually (remembered locally) |
145
+ | Pill summary | bottom-right: current price window + balance + period spend |
146
+
147
+ ## Architecture
148
+
149
+ ```
150
+ host lib/index.js assembly only: validate config → build store/service → register four
151
+ authenticated routes → poll on a timer
152
+ ├─ lib/service.js the operations (getState / refresh / setConfig / setWindow / poll)
153
+ ├─ lib/config.js tunables + DEFAULTS validation (a profile `config:` replaces wholesale)
154
+ ├─ lib/store.js state file: atomic write (temp file + rename), serialised, schemaVersion
155
+ │ envelope, quarantines a corrupt file instead of losing it
156
+ ├─ lib/deepseek.js balance/usage HTTP client (browser-ish headers, 15 s timeout) + parsing
157
+ ├─ lib/phase.js peak/off-peak phases + the statutory-holiday calendar
158
+ ├─ lib/window.js window range, snapshot estimation, daily cost/token aggregation
159
+ ├─ lib/validate.js /dsbal/* request validation (fromMs bounds, credential length/shape)
160
+ └─ lib/i18n.js reads the host's language preference
161
+ client lib/client.js window.__ModuleLoader__ artifact: the shell.overlay widget + zh/en copy
162
+ ```
163
+
164
+ **Authentication**: all four `/dsbal/*` routes pass the host's own gate first —
165
+ `ctx.connection.admit(req)`, the same Host/Origin trust fence and browser-cookie
166
+ authentication the `/api` channel uses. A request without a session cookie gets
167
+ **401**, a cross-site request **403**, a wrong method **405**, and a malformed body
168
+ **400 with no state change**. The credential-writing route is reachable only from the
169
+ authenticated local browser.
170
+
171
+ ## Configuration
172
+
173
+ Override fields per entry id in the profile's patch layer (`config:` replaces the whole
174
+ object, so unlisted fields fall back to the defaults):
175
+
176
+ ```yaml
177
+ - id: deepseek-balance-dsh
178
+ config:
179
+ pollIntervalMs: 900000 # poll cadence, 30s .. 24h, default 5 min
180
+ historyDays: 180 # snapshot retention / custom-window cap, 1 .. 400, default 90
181
+ usageTtlMs: 1800000 # successful usage-response cache, 1min .. 24h, default 1 h
182
+ usageErrorTtlMs: 600000 # failed usage-response cache, 1min .. 24h, default 10 min
183
+ maxSnapshots: 5000 # snapshot cap, 100 .. 100000
184
+ requestTimeoutMs: 15000 # per-request timeout, 1s .. 120s
185
+ ```
186
+
187
+ The endpoints (`balanceUrl` / `usageCostUrl` / `usageAmountUrl`) are overridable too and
188
+ must be https. An invalid value **fails activation naming the field** instead of falling
189
+ back silently.
190
+
191
+ ## Model access (read-only agent tool)
192
+
193
+ The plugin registers one read-only tool through an optional injection, so a profile
194
+ without a tool runtime still activates the widget:
195
+
196
+ | Tool | Parameters | Returns |
197
+ | --- | --- | --- |
198
+ | `deepseek_balance` | `window` (`today`\|`24h`\|`7d`\|`custom`, optional), `fromMs` (epoch ms, only with `custom`) | current price phase, balance, the period's spend and token counters, configuration state and the last error |
199
+
200
+ It is **read-only by construction**: the tool can only reach `service.getState({ window })`,
201
+ which projects a window without persisting it — asking for `7d` does not change the
202
+ widget's selection, and the credential-writing `setConfig()` is not reachable from it
203
+ at all. Tool output is English (it is a model interface, not UI copy) while the panel
204
+ stays bilingual.
205
+
206
+ `defineTool()` lives in `@deepseek-ai/dsh-tools`, which a zero-dependency package — and
207
+ this profile — cannot resolve, so the definition is hand-built from what a raw
208
+ registration is consumed for (`name`, `description`, a full JSON-Schema `parameters`,
209
+ `output.schema`/`output.render`, `execute`). Registration runs inside a try/catch with an
210
+ activation self-check: if the definition no longer satisfies the runtime (registration
211
+ throws) or no longer reaches the model-facing projection (`tools.schemas()` omits it), the
212
+ tool withdraws itself and logs why — it can never break the widget or prompt assembly.
213
+ Verified on an isolated instance: `read-only agent tool deepseek_balance ready
214
+ (visible=true, projected=true)`. A live model turn invoking it is the one step that needs
215
+ an agent session (and therefore a provider credential), so it is not covered by the
216
+ automated checks.
217
+
218
+ ## Notes
219
+
220
+ - `/usage/cost` and `/usage/amount` are **private** platform-dashboard endpoints
221
+ (not in the public docs) and may change; the plugin degrades to estimates
222
+ automatically. Usage figures are **account-wide** — the platform API does not
223
+ filter by API key.
224
+ - A `userToken` expires naturally; usage may lag by up to 1 hour (memory cache);
225
+ the 刷新 button clears it.
226
+ - Keys are stored unencrypted in `~/.deepseek-balance.json` (0600) — never share it.
227
+ - The state file carries a `schemaVersion` envelope and is written **atomically**
228
+ (temp file + rename) with serialised writers. A corrupt or future-versioned file is
229
+ **renamed aside** to `….corrupt-<timestamp>`, the plugin starts from empty state, and
230
+ the panel shows why — nothing is silently discarded.
231
+ - Statutory holidays come from the **annual State Council notice** and are currently
232
+ bundled for **2026**. After the new year the plugin must be updated to know the next
233
+ year's holidays; an unlisted year is flagged「节假日数据待更新」and judged as plain
234
+ Mon–Fri until then.
235
+
236
+ ## Contributing
237
+
238
+ Issues and PRs welcome (peak-hour threshold alerts, per-model filtering, more locales…).
239
+ Keep `lib/client.js` in the `window.__ModuleLoader__` artifact format.
240
+
241
+ Run `npm test` (**123 cases**, zero-dependency Node assertions) after touching any module:
242
+
243
+ | Suite | Cases | Covers |
244
+ | --- | --- | --- |
245
+ | `test/phase.test.mjs` | 28 | weekday/weekend/holiday phases, merged holiday runs, unlisted-year fallback |
246
+ | `test/window.test.mjs` | 14 | Beijing midnight boundaries, snapshot estimate (top-ups excluded), multi-month/year ranges, per-model and token totals |
247
+ | `test/config.test.mjs` | 6 | defaults, wholesale-replacement semantics, unknown fields, range and https validation |
248
+ | `test/store.test.mjs` | 9 | atomic write, serialised writers, mode 0600, corrupt/foreign-file quarantine, diagnosable save failure |
249
+ | `test/i18n.test.mjs` | 5 | BCP 47 normalisation, `settings.describe()` read, safe degradation |
250
+ | `test/service.test.mjs` | 13 | poll/refresh/setConfig/setWindow/getState with an injected HTTP layer (no network) |
251
+ | `test/tool.test.mjs` | 9 | tool definition shape, read-only guarantee (no write call, no key fragment), schema-valid output, argument bounds, English render |
252
+ | `test/routes.test.mjs` | 18 | validation bounds: bad id, non-JSON, wrong type, `fromMs` range, length, key shape |
253
+ | `test/package.test.mjs` | 21 | name/exports, a single `insert` row, client artifact and copy table, non-regression of the auth gate |
254
+
255
+ CI (`.github/workflows/test.yml`) runs `npm run check` + `npm test` on a Node 20/24 matrix.
256
+ Changing the **client artifact format** (`window.__ModuleLoader__.load` + `exports.inject`),
257
+ the **auth gate**, the **state-file format**, the **tool definition** or the **phase
258
+ algorithm** means updating the matching suite; the phase algorithm and the copy table both have structural guards
259
+ (no Chinese literal outside the table, one-to-one zh/en keys).
260
+
261
+ ## Changelog
262
+
263
+ See [CHANGELOG.md](CHANGELOG.md) — current **plugin** release **0.2.0** (not to be confused with the harness version mentioned above).
264
+
265
+ ## License
266
+
267
+ [MIT](LICENSE)
package/README.zh.md ADDED
@@ -0,0 +1,237 @@
1
+ # dsh-deepseek-balance
2
+
3
+ [![test](https://github.com/OwlNjust/DSH-DeepSeek-Balance/actions/workflows/test.yml/badge.svg)](https://github.com/OwlNjust/DSH-DeepSeek-Balance/actions/workflows/test.yml)
4
+
5
+ npm 包名:[`deepseek-balance-dsh`](https://www.npmjs.com/package/deepseek-balance-dsh) —— 仓库名为 `DSH-DeepSeek-Balance`;
6
+ npm 上的 `dsh-deepseek-balance` 是**另一个无关项目**,与本插件无关。
7
+
8
+ 主题:[`dsh-plugin`](https://github.com/topics/dsh-plugin) · [`deepseek-harness`](https://github.com/topics/deepseek-harness) · [`deepseek`](https://github.com/topics/deepseek)
9
+
10
+ > DeepSeek Harness 的 DeepSeek 余额与用量实时监控插件 —— 右下角悬浮小组件,
11
+ > 附高峰/空闲时段(梁文峰 / 梁文谷)价格提醒。
12
+ >
13
+ > 中文 · **[English](README.md)**
14
+
15
+ ---
16
+
17
+ **安装只需一步:** 把 `https://github.com/OwlNjust/DSH-DeepSeek-Balance`
18
+ 发给您的 agent,它就知道如何自动安装本插件。手动安装步骤如下。
19
+
20
+ ## 安装
21
+
22
+ 本插件**零运行时依赖**,已发布到 npm:[`deepseek-balance-dsh`](https://www.npmjs.com/package/deepseek-balance-dsh)。
23
+ 提供两条安装路线——**组合层安装**(npm 包名或本仓库)与**补丁层安装**(本地克隆,改源码调试用)。
24
+ **同一 profile 只能选一条**:两条都用会让插件 id 被插入两次(见「agent 注意事项」)。
25
+
26
+ ### 路线 A(推荐):作为「组合包」安装(deepseek-harness ≥ 0.2.0)
27
+
28
+ 本包声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,所以 harness 的插件管理器
29
+ 可以直接把它当作组合包(profile 层)安装并自动激活——**无需手改任何文件**。两种装法等价:
30
+
31
+ - **npm 包名**(已发布):
32
+
33
+ ```sh
34
+ dsh plugin --profile <profile> add deepseek-balance-dsh
35
+ ```
36
+
37
+ - **Git 仓库**(源码为准):
38
+
39
+ ```sh
40
+ dsh plugin --profile <profile> add https://github.com/OwlNjust/DSH-DeepSeek-Balance
41
+ ```
42
+
43
+ - **桌面端**:菜单栏 →「管理插件 / Manage plugins」→ 填入
44
+ `deepseek-balance-dsh` 或 `https://github.com/OwlNjust/DSH-DeepSeek-Balance`
45
+
46
+ 管理器会把包加入 profile 的 `dsh.profile.bundles`,包内的 `cordis.patch.yml`(含激活插件的
47
+ `insert` 行)随之作为组合层生效。重载/重启 profile 后右下角即出现小组件。
48
+
49
+ ### 路线 B:本地克隆 + 用户补丁层(离线 / 改源码调试用)
50
+
51
+ ```sh
52
+ # 1. 克隆/复制仓库到任意可访问路径
53
+ git clone https://github.com/OwlNjust/DSH-DeepSeek-Balance.git /tmp/dsh-deepseek-balance
54
+
55
+ # 2. 安装到 web profile(file: = 快照;link: = 源码符号链接,改代码即生效)
56
+ cd ~/.dsh/profiles/web
57
+ dsh plugin --profile web add link:/tmp/dsh-deepseek-balance
58
+ ```
59
+
60
+ 3. 把下面这段**追加**到 `~/.dsh/profiles/web/cordis.patch.yml`(profile 的用户补丁层):
61
+
62
+ ```yaml
63
+ - insert:
64
+ - id: deepseek-balance-dsh
65
+ name: deepseek-balance-dsh
66
+ ```
67
+
68
+ 4. 重启 profile:
69
+
70
+ ```sh
71
+ dsh web
72
+ ```
73
+
74
+ 5. 可选验证:确认 web 服务器响应
75
+
76
+ ```sh
77
+ curl -s http://127.0.0.1:3080/dsbal/state
78
+ ```
79
+
80
+ 强制刷新页面(Ctrl+Shift+R)后右下角出现小组件。让用户点击小组件 → 「配置」→
81
+ 粘贴 DeepSeek **API key**(可选再填平台 **userToken**)→「保存」。
82
+
83
+ ### agent 注意事项
84
+
85
+ - npm 包名是 **`deepseek-balance-dsh`**;**不要**用仓库名 `dsh-deepseek-balance` 发布——该名字在 npm 上属于一个无关项目。
86
+ - **两条路线别混用**:路线 A 已由组合层插入插件行,若再手工追加同一 `insert` 行,插件 id 会被
87
+ 插入两次(cordis 按 id 折叠成一份,所以**不会报错**,只是组合树里多出一行、日后极易误判)。
88
+ 切换路线时先确认 `dsh.profile.bundles` 与 profile 的 `cordis.patch.yml` 只有一边生效;
89
+ 一条命令自检,结果必须是 `1`:
90
+
91
+ ```sh
92
+ dsh --profile web --dump-config | grep -c 'id: deepseek-balance-dsh'
93
+ ```
94
+
95
+ 若输出 `2`:删掉 `cordis.patch.yml` 里手写的那段(保留 `dsh.profile.bundles` 里的包,
96
+ 组合层交给插件管理器维护),重启 `dsh web` 生效。反向切换(改回手写行)则先从
97
+ `dsh.profile.bundles` 移除本包。
98
+ - 配置与余额历史保存在 `~/.deepseek-balance.json`(0600),**切勿回显其内容**。
99
+ - 卸载:删除 `cordis.patch.yml` 中的行、删除
100
+ `~/.dsh/profiles/web/node_modules/deepseek-balance-dsh`,重启 `dsh web`。
101
+
102
+ ## 前置条件
103
+
104
+ - deepseek-harness 的 **web profile**(`dsh web`);Node.js ≥ 20
105
+ (本插件在 **v0.1.2-rc.1** 上适配验证,并已复检至 **v0.2.0-rc.2**,四个版本零改动可用)。
106
+ - DeepSeek **API Key**(platform.deepseek.com → API keys)。
107
+ - 可选:平台 **userToken**(platform.deepseek.com → F12 → Application → Local Storage),
108
+ 用于精确统计消耗/Token;未配置时自动回退为余额快照估算。
109
+
110
+ ## 功能
111
+
112
+ - **余额** —— 总余额 / 充值余额,官方 `GET /user/balance`,每 5 分钟刷新。
113
+ - **时间段消耗** —— 今天 / 近24小时 / 近7天 / 自定义起始日期,含按模型明细(`/usage/cost`)。
114
+ - **Token 消耗** —— 输入(命中缓存)/ 输入(未命中缓存)/ 输出,与平台用量页一致(`/usage/amount`)。
115
+ - **价格时段提醒** —— 🔴 **梁文峰**(高峰:北京时间周一至周五 09:00–12:00 与 14:00–18:00,
116
+ **不含中国法定节假日**,价格 ×2)与 🟢 **梁文谷**(空闲:其余时段,**含周末与法定节假日全天**,半价),
117
+ 实时倒计时,高峰红色高亮,法定节假日显示 🎉 标识。
118
+ 时段依据[官方定价文档](https://api-docs.deepseek.com/zh-cn/quick_start/pricing)。
119
+ - **中英双语** —— 界面文案集中于 `lib/client.js` 的文案表;语言跟随宿主设置 → 浏览器语言,
120
+ 面板内可一键切换;插件卡片标题/描述见 `locale/zh.json`、`locale/en.json`,图标 `icon.svg`。
121
+ - **可拖动定位** —— **右键**按住药丸拖动即可移到任意位置,位置自动记忆;左键只负责点击展开面板。
122
+ 药丸上右键不弹浏览器菜单(药丸即拖拽手柄);展开面板时自动避让屏幕边缘,
123
+ 窗口缩放后药丸也会自动夹回可视区域。面板底部提供「重置位置(右下角)」按钮,
124
+ 一键恢复默认位置。
125
+
126
+ ## 使用
127
+
128
+ | 项目 | 位置 |
129
+ | --- | --- |
130
+ | API Key / userToken | 小组件 → 配置 |
131
+ | 时间窗口 | 小组件按钮(今天 / 24h / 7天 / 自定义) |
132
+ | 刷新 | 「刷新」按钮(或自动:按配置的 `pollIntervalMs`,默认 5 分钟;标签页隐藏时暂停) |
133
+ | 语言 | 跟随宿主设置的语言 → 否则浏览器语言;面板标题栏的 `EN/中` 按钮可手动切换(记忆在本机) |
134
+ | 药丸摘要 | 右下角:当前价格时段 + 余额 + 时段消耗 |
135
+
136
+ ## 架构
137
+
138
+ ```
139
+ 宿主 lib/index.js 仅装配:config 校验 → 建 store/service → 注册四条已认证路由 → 定时轮询
140
+ ├─ lib/service.js 操作实现(getState / refresh / setConfig / setWindow / poll)
141
+ ├─ lib/config.js 可调项 DEFAULTS + 校验(profile 的 config: 覆盖,整体替换语义)
142
+ ├─ lib/store.js 状态文件:原子写(临时文件 + rename)、串行化、schemaVersion 信封、损坏隔离
143
+ ├─ lib/deepseek.js 余额/用量 HTTP 客户端(浏览器头 + 15s 超时)与载荷解析
144
+ ├─ lib/phase.js 时段/法定节假日算法(梁文峰/梁文谷)
145
+ ├─ lib/window.js 窗口区间、快照估算、按天聚合计费与 Token
146
+ ├─ lib/validate.js /dsbal/* 请求校验(含 fromMs 范围、凭据长度与 key 形态)
147
+ └─ lib/i18n.js 读取宿主语言偏好
148
+ 客户端 lib/client.js window.__ModuleLoader__ 产物:shell.overlay 悬浮小组件 + 文案表(zh/en)
149
+ ```
150
+
151
+ **认证**:四条 `/dsbal/*` 路由都先过宿主自己的门禁 `ctx.connection.admit(req)`
152
+ (与 `/api` 通道同一套 Host/Origin 信任栅栏 + 浏览器 cookie 认证)。
153
+ 未携带会话 cookie 的请求返回 **401**,跨源请求返回 **403**,方法不符返回 **405**,
154
+ 非法请求体返回 **400 且不改动任何状态**。写入凭据的接口只对已登录的本机浏览器开放。
155
+
156
+ ## 配置
157
+
158
+ 在 profile 的补丁层按 entry id 覆盖(`config:` 是**整体替换**,未写的字段回落默认值):
159
+
160
+ ```yaml
161
+ - id: deepseek-balance-dsh
162
+ config:
163
+ pollIntervalMs: 900000 # 轮询周期,30s ~ 24h,默认 5 分钟
164
+ historyDays: 180 # 余额快照保留天数 / 自定义窗口上限,1 ~ 400,默认 90
165
+ usageTtlMs: 1800000 # 用量接口成功缓存,1min ~ 24h,默认 1 小时
166
+ usageErrorTtlMs: 600000 # 用量接口失败缓存,1min ~ 24h,默认 10 分钟
167
+ maxSnapshots: 5000 # 快照条数上限,100 ~ 100000
168
+ requestTimeoutMs: 15000 # 单次请求超时,1s ~ 120s
169
+ ```
170
+
171
+ 后端地址(`balanceUrl` / `usageCostUrl` / `usageAmountUrl`)也可覆盖,必须是 https。
172
+ 任何非法值都会**让插件激活失败并打印字段名**,而不是静默用默认值。
173
+
174
+ ## 模型访问(只读 agent tool)
175
+
176
+ 插件通过**可选注入**注册一个只读工具(没有 tool 运行时的 profile 仍能正常激活小组件):
177
+
178
+ | 工具 | 参数 | 返回 |
179
+ | --- | --- | --- |
180
+ | `deepseek_balance` | `window`(`today`/`24h`/`7d`/`custom`,可选)、`fromMs`(毫秒时间戳,仅 `custom` 用) | 当前价格时段、余额、该时段消耗与 Token 三类计数、配置状态、最近一次错误 |
181
+
182
+ **只读是结构保证**:工具只能触达 `service.getState({ window })`——它按参数**投影**一个窗口,
183
+ 不落盘、不改动界面当前选择;写入凭据的 `setConfig()` 从工具里根本不可达。
184
+ 工具输出为英文(面向模型,不是 UI 文案),面板保持中英双语。
185
+
186
+ `defineTool()` 位于 `@deepseek-ai/dsh-tools`,零依赖插件(以及本 profile)解析不到,
187
+ 因此该定义是**按裸注册真正被消费的字段**手写的(`name`/`description`/完整 JSON Schema 的
188
+ `parameters`/`output.schema`+`output.render`/`execute`,参数校验在 `execute` 内自行完成)。
189
+ 注册包在 try/catch 里并带**启动自检**:若定义不再满足运行时(注册抛错),或不再进入
190
+ 模型侧 schema 投影(`tools.schemas()` 里没有它),工具会自我撤下并打印原因——
191
+ 它永远不会拖垮小组件或系统提示的组装。已在隔离实例实测:
192
+ `read-only agent tool deepseek_balance ready (visible=true, projected=true)`。
193
+ **唯一未覆盖的一步**是"真实模型回合实际调用它"(需要带凭据的 agent 会话),故不在自动化检查内。
194
+
195
+ ## 注意事项
196
+
197
+ - `/usage/cost` 与 `/usage/amount` 是**平台私有**控制台接口(不在公开文档中),可能变化;
198
+ 不可用时自动回退估算。数据为**账号整体**统计——平台接口不支持按 API Key 筛选。
199
+ - userToken 会自然过期;用量数据最多滞后 1 小时(内存缓存),点「刷新」立即更新。
200
+ - 密钥明文保存于 `~/.deepseek-balance.json`(0600),请勿泄露。
201
+ - 状态文件带 `schemaVersion` 信封并**原子写入**(临时文件 + rename),写入串行化;
202
+ 若文件损坏或版本高于插件,插件会把它**改名隔离**为 `…​.corrupt-<时间戳>`、
203
+ 从空状态启动,并在面板上显示原因——不会静默丢数据。
204
+ - 法定节假日按**国务院办公厅年度通知**内置(当前收录 **2026 年**)。跨年后需更新插件才能
205
+ 识别次年节假日;未收录的年份会标注「节假日数据待更新」并暂按普通周一至周五判定。
206
+
207
+ ## 贡献
208
+
209
+ 欢迎 Issue / PR(高峰阈值提醒、按模型筛选、多语言等)。
210
+ 请保持 `lib/client.js` 的 `window.__ModuleLoader__` 产物格式。
211
+
212
+ 改动任何模块后请跑 `npm test`(**123 项**,全部是零依赖的 Node 内置断言):
213
+
214
+ | 套件 | 项数 | 覆盖 |
215
+ | --- | --- | --- |
216
+ | `test/phase.test.mjs` | 28 | 时段算法:工作日/周末/法定节假日/跨节日合并段/未收录年份回退 |
217
+ | `test/window.test.mjs` | 14 | 北京日界、快照估算(充值不计消耗)、跨月/跨年区间、按模型与 Token 汇总 |
218
+ | `test/config.test.mjs` | 6 | 默认值、整体替换语义、未知字段、范围与 https 校验 |
219
+ | `test/store.test.mjs` | 9 | 原子写、串行写者、权限 0600、损坏/未来版本隔离、写失败可诊断 |
220
+ | `test/i18n.test.mjs` | 5 | BCP 47 归一化、settings.describe() 读取与安全退化 |
221
+ | `test/service.test.mjs` | 13 | poll/refresh/setConfig/setWindow/getState(注入假 HTTP,无网络) |
222
+ | `test/tool.test.mjs` | 9 | 工具定义形状、只读保证(无写调用、不含 key 片段)、结果满足自身 schema、参数边界、英文渲染 |
223
+ | `test/routes.test.mjs` | 18 | 请求校验与边界(非法 id/非 JSON/类型错/fromMs 范围/长度/key 形态) |
224
+ | `test/package.test.mjs` | 21 | 包名与导出、组合层只有一行 insert、客户端产物与文案表、认证门禁不可回归 |
225
+
226
+ CI(`.github/workflows/test.yml`)在 Node 20/24 矩阵上跑 `npm run check` + `npm test`。
227
+ 改动**客户端产物格式**(`window.__ModuleLoader__.load` + `exports.inject`)、
228
+ **认证门禁**、**状态文件格式**、**工具定义**或**时段算法**时,必须同步更新对应套件;
229
+ 时段算法与客户端文案表都有结构性断言兜底(表外中文字面量、zh/en 键一一对应)。
230
+
231
+ ## 变更记录
232
+
233
+ 见 [CHANGELOG.md](CHANGELOG.md)(当前**插件**版本 **0.2.0**,与上文提到的 harness 版本无关)。
234
+
235
+ ## License
236
+
237
+ [MIT](LICENSE)
@@ -0,0 +1,22 @@
1
+ # deepseek-balance-dsh composition rows.
2
+ #
3
+ # This file is the package's own BUNDLE PATCH: package.json declares
4
+ # "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
5
+ # so a harness that manages this package as a profile bundle (plugin manager UI,
6
+ # desktop app, or `dsh plugin --profile <p> add <git-url>`) applies the block
7
+ # below automatically — no hand editing.
8
+ #
9
+ # Manual install (local checkout / development, no bundle management):
10
+ # cd ~/.dsh/profiles/<p>
11
+ # dsh plugin --profile <p> add file:../<path-to>/<checkout> # or: add deepseek-balance-dsh
12
+ # then append the `insert` block below to ~/.dsh/profiles/<p>/cordis.patch.yml
13
+ # (the profile's user patch layer) and restart the profile.
14
+ # Pick ONE route per profile: bundle management already applies this file, so
15
+ # also appending the row by hand would insert the plugin id twice.
16
+ #
17
+ # A single row activates both halves: the host plugin (polling, state file,
18
+ # /dsbal/* routes) and, because the package declares `dsh.client`, the browser
19
+ # roster entry (the bottom-right widget).
20
+ - insert:
21
+ - id: deepseek-balance-dsh
22
+ name: deepseek-balance-dsh