@ychris12138/dsh-usage-stats 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-usage-stats 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 ADDED
@@ -0,0 +1,362 @@
1
+ # dsh-usage-stats
2
+
3
+ [![GitHub Release](https://img.shields.io/github/v/release/Ychris12138/dsh-usage-stats?display_name=tag&sort=semver&color=1f6feb)](https://github.com/Ychris12138/dsh-usage-stats/releases/latest)
4
+ [![CI](https://github.com/Ychris12138/dsh-usage-stats/actions/workflows/ci.yml/badge.svg)](https://github.com/Ychris12138/dsh-usage-stats/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/badge/license-MIT-2da44e)](LICENSE)
6
+
7
+ 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 网页端提供多供应商账户监测与 Token 用量分析。
8
+
9
+ Provider balances, subscription quotas, and token-usage analytics for the DeepSeek Harness Web GUI (`dsh web`).
10
+
11
+ ![dsh-usage-stats v0.2.0 interface preview](docs/images/usage-panel.svg)
12
+
13
+ > 展示图使用脱敏演示数据;插件不会把 API Key、Cookie、管理 PAT 或上游原始响应发送到浏览器。
14
+
15
+ ## 一眼看懂 / At a glance
16
+
17
+ | | 能力 | 说明 |
18
+ | --- | --- | --- |
19
+ | 💳 | 统一账户卡片 | API 供应商显示余额,Token Plan 显示分窗口额度;面板一次只呈现当前供应商 |
20
+ | 📊 | Token 用量分析 | 今日、本月、累计、缓存命中率、月历热图,以及按日期/供应商/模型下钻 |
21
+ | 🔄 | 后台监测 | 服务端启动即刷新,之后每五分钟更新全部已配置账户与本地 Token 聚合 |
22
+ | 🧩 | 可扩展适配器 | 支持 New API、Sub2API、通用余额模板,以及声明式 JSON Pointer 自定义查询 |
23
+ | 🔒 | 本机安全边界 | 五个端点仅接受回环 GET;凭据只在服务端解析并发往校验后的供应商地址 |
24
+
25
+ 界面支持中文和英文。浏览器只请求当前选择的 provider;后台刷新与面板是否打开无关。手动刷新会更新用量、供应商列表,并强制刷新当前账户,不会批量强制请求其他供应商。
26
+
27
+ ## 快速安装 / Quick start
28
+
29
+ 需要 DeepSeek Harness `web` profile(`@deepseek-ai/dsh >= 0.1.0-rc.6`)。
30
+
31
+ ```bash
32
+ dsh plugin --profile web add "github:Ychris12138/dsh-usage-stats"
33
+ ```
34
+
35
+ 然后重启已经运行的 `dsh web`,并在浏览器中硬刷新。侧边栏底部会出现“用量/余额”(Usage/Balance)入口。
36
+
37
+ ### 插件市场 GUI 安装(DSH Community Market,Path A 标准来源)
38
+
39
+ 本仓库按 [DSH Community Market 目录 adapter 指南](https://github.com/anywhere-labs/deepseek-harness-desktop/blob/master/dsh-community-market/docs/catalog-adapter-guide.zh.md) 的**标准来源(Path A)** 接入,无需修改 Market 代码。内置两份目录数据:
40
+
41
+ - `catalog/catalog-source.json` — 来源 manifest(`catalog-source.schema.json` v1.0.0)
42
+ - `catalog/v1/plugins.json` — 标准 provider page(`catalog-provider-page.schema.json` v1.0.0)
43
+
44
+ **使用前提(重要)**:市场托管安装只接受 npm registry 的精确稳定版本,git 条目仅可浏览。`dsh-usage-stats` 这个 npm 名已被其他项目占用,因此目录条目身份使用 `@ychris12138/dsh-usage-stats`(当前可用)。要启用 GUI「安装」按钮,需先发布:
45
+
46
+ 1. 仓库包身份已统一为 `@ychris12138/dsh-usage-stats`;每次发版需同步 `package.json` / `package-lock.json` / `catalog/v1/plugins.json` 的版本。
47
+ 2. 发布 scoped 公共包:`npm publish --access public`。
48
+ 3. 把 `catalog/v1/plugins.json` 内容发布到 `https://ychris12138.github.io/dsh-usage-stats/v1/plugins`(GitHub Pages,manifest 与 endpoint 必须同源、HTTPS 443、无凭据)。
49
+ 4. 在 DSH 插件市场 → 来源管理 → 添加来源,粘贴 manifest URL:`https://ychris12138.github.io/dsh-usage-stats/catalog-source.json`,选择后即可走「可恢复安装边界」GUI 安装。
50
+
51
+ > 若最终包名不同,请同步修改 `catalog-source.json` 的 `providerId`/`transport.endpoint` 与 `catalog/v1/plugins.json` 的身份字段。发布前目录条目可浏览但安装保持禁用(fail-closed,属预期)。
52
+
53
+ 升级或卸载:
54
+
55
+ ```bash
56
+ dsh plugin --profile web update dsh-usage-stats
57
+ dsh plugin --profile web remove dsh-usage-stats
58
+ ```
59
+
60
+ <details>
61
+ <summary><strong>兼容安装器:无法使用 dsh plugin 时展开</strong></summary>
62
+
63
+ PowerShell、命令提示符和 macOS/Linux 终端使用同一条命令:
64
+
65
+ ```bash
66
+ npx --yes github:Ychris12138/dsh-usage-stats
67
+ ```
68
+
69
+ 安装器会把运行文件复制到 `~/.dsh/profiles/node_modules/dsh-usage-stats`,并在 `profiles/web/cordis.patch.yml` 中幂等启用插件。重复运行即可更新,不会重复追加配置。设置了 `DSH_HOME` 时使用该目录。
70
+
71
+ `dsh plugin` 与 `npx` 是两条独立安装路径,请选择其中一种;不要同时保留手工 Cordis entry 和 bundle 注册,否则会重复挂载。
72
+
73
+ ```bash
74
+ # 预览,不修改文件
75
+ npx --yes github:Ychris12138/dsh-usage-stats --dry-run
76
+
77
+ # 检查现有安装
78
+ npx --yes github:Ychris12138/dsh-usage-stats --check
79
+
80
+ # 安装但不修改 Cordis patch
81
+ npx --yes github:Ychris12138/dsh-usage-stats --no-enable
82
+ ```
83
+
84
+ 无法使用 `npx` 时可从源码运行 `node scripts/install.mjs`。
85
+
86
+ </details>
87
+
88
+ ## 支持的账户类型 / Providers
89
+
90
+ 插件自动发现官方 DeepSeek 路由和 `llm-pi-ai` 中的 provider profile。只有存在公开账户接口或显式 monitor 的供应商才会查询远端账户;Token 用量统计不需要额外凭据。
91
+
92
+ | Provider / adapter | 模式 | 默认凭据 | 上游接口 |
93
+ | --- | --- | --- | --- |
94
+ | DeepSeek | 余额 | provider `apiKeyEnv` | `/user/balance` |
95
+ | OpenRouter | 余额 | `OPENROUTER_MANAGEMENT_KEY` | `/api/v1/credits` |
96
+ | Moonshot / Kimi API | 余额 | provider `apiKeyEnv` | `/v1/users/me/balance` |
97
+ | OpenCode Go | 订阅 | `OPENCODE_GO_API_KEY` 或本地 `auth.json` | `/zen/go/v1/usage` |
98
+ | Z.ai / 智谱 | 订阅 | `ZAI_API_KEY` | Coding Plan quota/subscription |
99
+ | Kimi For Coding | 订阅 | `KIMI_API_KEY` | `/coding/v1/usages` |
100
+ | MiniMax Coding Plan | 订阅 | `MINIMAX_API_KEY` | `/v1/token_plan/remains` |
101
+ | New API | 余额 | provider 推理 Token | `/api/usage/token/` |
102
+ | Sub2API / Passion | 自动判别 | provider `apiKeyEnv` | `/v1/usage` |
103
+ | Sub2API 面板(真实) | 余额 | provider 推理 Token | `/user/balance`(复用 apiKey) |
104
+ | General / Declarative | 余额或订阅 | 配置中的 credential ref | 受限 GET + JSON |
105
+
106
+ 没有公开账户接口的供应商仍会正常统计 Token;账户卡片会明确显示“不支持”,不会猜测余额。
107
+
108
+ ## 凭据与供应商配置 / Configuration
109
+
110
+ 凭据由 Harness 从 `~/.dsh/.credentials.yaml` 解析。安装器不会读取、创建或修改该文件。不要把真实 Key、Cookie 或管理令牌提交到 Git、公开 issue,或粘贴给编码 Agent。
111
+
112
+ ### 余额型供应商
113
+
114
+ DeepSeek、Moonshot 等默认复用对应 provider profile 的 `apiKeyEnv`。例如:
115
+
116
+ ```yaml
117
+ # ~/.dsh/.credentials.yaml
118
+ DEEPSEEK_API_KEY: sk-your-key-here
119
+ ```
120
+
121
+ OpenRouter 是明确的例外:官方账户 credits 接口要求 **Management Key**,不能复用普通推理 `OPENROUTER_API_KEY`。插件默认读取独立引用;未配置时显示“未配置”,不会拿推理 Key 试探:
122
+
123
+ ```yaml
124
+ # ~/.dsh/.credentials.yaml
125
+ OPENROUTER_MANAGEMENT_KEY: sk-or-v1-your-management-key
126
+ ```
127
+
128
+ 插件按 `total_credits - total_usage` 显示 OpenRouter 余额,并同时展示累计已用和总 credits。普通 Key 的 `/api/v1/key` 只描述单个 Key 的 spending limit,不会被当作账户余额。自定义引用可在 `monitors.openrouter` 中设置 `adapter: openrouter-balance` 与 `credentialRef`。
129
+
130
+ ### Token Plan 供应商
131
+
132
+ ```yaml
133
+ # ~/.dsh/.credentials.yaml
134
+ OPENCODE_GO_API_KEY: sk-opencode-your-key
135
+ ZAI_API_KEY: your-zai-key
136
+ # 中国区 Z.ai 用户可选;默认 global
137
+ ZAI_API_REGION: bigmodel-cn
138
+ KIMI_API_KEY: your-kimi-key
139
+ MINIMAX_API_KEY: your-minimax-key
140
+ # 中国区 MiniMax 用户可选;默认 global
141
+ MINIMAX_API_REGION: cn
142
+ ```
143
+
144
+ OpenCode Go 依次尝试 Harness credential、`~/.local/share/opencode/auth.json`,最后才使用显式 `OPENCODE_GO_AUTH_COOKIE + OPENCODE_GO_WORKSPACE_ID` 兼容回退。Bearer usage endpoint 目前不是公开 API,可能随上游变化;Cookie 等同登录凭据,不应进入日志或 issue。
145
+
146
+ Z.ai 全球区使用 `api.z.ai`,中国区使用 `open.bigmodel.cn`。MiniMax 优先使用官方 `www.minimax.io` / `www.minimaxi.com` Token Plan 地址,并解析 5 小时与周窗口的剩余比例和重置时间。
147
+
148
+ ### New API、Sub2API 与自定义 monitor
149
+
150
+ 在现有 `name: dsh-usage-stats` Cordis entry 下合并 `config`,不要追加第二个插件 entry。monitor 键必须是 Harness 中真实存在的 provider id;未知 provider、adapter 或非法映射会在路由和 timer 注册前阻止插件启动。例外:monitor 同时显式提供 `usageBaseURL` 与 `credentialRef` 时视为自包含,会在 provider 注册可见前临时物化为 provider(适用于 Harness 设置页里后加载的 provider),此时不要求该 provider 已出现在注册表中。
151
+
152
+ <details>
153
+ <summary><strong>展开 monitor 配置示例</strong></summary>
154
+
155
+ New API 默认用 provider 推理 Token 查询 `/api/usage/token/`,并从 `/api/status` 读取实例自己的 `quota_per_unit`:
156
+
157
+ ```yaml
158
+ # ~/.dsh/profiles/web/cordis.patch.yml
159
+ - insert:
160
+ - id: usage-stats
161
+ name: dsh-usage-stats
162
+ config:
163
+ monitors:
164
+ relay-a: # Harness provider id
165
+ adapter: new-api
166
+ # 仅旧实例的 /api/user/self 回退需要:
167
+ fallbackCredentialRef: RELAY_A_MANAGEMENT_PAT
168
+ ```
169
+
170
+ 只有 `/api/usage/token/` 返回 404/405 且配置了独立管理 PAT 才会 fallback;不会把推理 Token 当管理凭据。旧实例需要 User ID 时可增加 `fallbackUserIdRef`。
171
+
172
+ CC Switch 风格通用余额:
173
+
174
+ ```yaml
175
+ monitors:
176
+ relay-a:
177
+ adapter: general
178
+ warning:
179
+ warnBelow: 5
180
+ criticalBelow: 1
181
+ ```
182
+
183
+ Sub2API 风格 `/v1/usage`:
184
+
185
+ ```yaml
186
+ monitors:
187
+ relay-a:
188
+ adapter: sub2api
189
+ warning:
190
+ warnBelow: 5
191
+ criticalBelow: 1
192
+ ```
193
+
194
+ > 注意:`sub2api` 适配器对应一部分 Sub2API 部署暴露的 `/v1/usage` 协议。真实 Sub2API 面板(Wei-Shaw/sub2api 系)不提供该公开接口,改用 `sub2api-auth` 适配器读取面板自己的余额(见下)。
195
+
196
+ **真实 Sub2API 面板余额(`sub2api-auth`)**:Sub2API 面板把上游订阅统一暴露成 API,但上游供应商通常没有公开余额接口。`sub2api-auth` 用 provider 自己的推理 API Key 查询面板余额,同 CC Switch 的 General 模板:`GET {baseUrl}/user/balance`,`Authorization: Bearer {apiKey}`,读取 `body.balance`。**无需单独的面板凭据** —— provider 在 DSH 模型页里配置的那个 API Key 会被直接复用:
197
+
198
+ ```yaml
199
+ monitors:
200
+ relay-b:
201
+ adapter: sub2api-auth
202
+ ```
203
+
204
+ (可选)若 `/user/balance` 返回 UTF-8 金额对应 `body.unit`,会显示该币种;否则默认 USD。今日已用额度尽量从 `GET /api/v1/usage/stats?period=today` 的 `total_actual_cost` 补充,查询不到也不影响余额展示。
205
+
206
+ **自动识别真实 Sub2API 面板(`sub2api-auth`)**:只要把 Sub2API 面板作为普通 provider 配置进 DSH(带入它的 API Key),插件会探测该 provider 的 `GET /api/v1/settings/public`。若指纹命中真实 Sub2API 面板(返回 `data.affiliate_enabled: boolean`),就自动按 `sub2api-auth` 用该 provider 的 API Key 读取余额,**无需为该 provider 单独写 `adapter`,也无需额外凭据**。显式 `adapter` 始终优先于自动识别;没有配置 API Key 的 provider 绝不会被探测。
207
+
208
+ ```yaml
209
+ # 只要这些(不需要单独的 SUB2API_* 凭据)
210
+ # DSH 模型页里为 Sub2API 面板配置 provider,baseURL 指向面板,API Key 填可用的密钥
211
+ ```
212
+
213
+ Passion(provider id 为 `passion` 或域名为 `*.passionapi.com`)会自动识别。钱包响应显示余额;`quota_limited` 或包含 `subscription` 的响应自动切换为额度窗口。
214
+
215
+ 声明式自定义查询只支持受限 GET + JSON Pointer,不执行 JavaScript:
216
+
217
+ ```yaml
218
+ monitors:
219
+ private-model:
220
+ adapter: declarative
221
+ mode: balance
222
+ request:
223
+ path: /account/balance
224
+ auth:
225
+ type: bearer
226
+ credentialRef: PRIVATE_MODEL_API_KEY
227
+ extract:
228
+ root: /data
229
+ remaining: /available_balance
230
+ used: /used_balance
231
+ total: /total_balance
232
+ currency: /currency
233
+ ```
234
+
235
+ </details>
236
+
237
+ 支持的 adapter:`deepseek-balance`、`openrouter-balance`、`moonshot-balance`、`zai-balance`、`new-api`、`sub2api`、`sub2api-auth`、`general`、`opencode-go`、`zai-token-plan`、`kimi-token-plan`、`minimax-token-plan`、`declarative`。
238
+
239
+ `warning.warnBelow` 与 `warning.criticalBelow` 是余额绝对值阈值。具有总额度的余额和 Token Plan 会自动产生 `normal / warning / critical` 剩余比例状态(默认 30% / 10%)。
240
+
241
+ ## 使用 / Usage
242
+
243
+ 1. 点击侧边栏“用量/余额”。
244
+ 2. 用“当前供应商”切换账户卡片;一次只显示一个 provider。
245
+ 3. 使用 `‹` / `›` 切换月份,点击热图日期查看当天的 provider/model 明细。
246
+ 4. 标题栏刷新会更新 Token、provider 列表,并强制刷新当前账户。
247
+
248
+ “最近 14 天”按本地日历计算,只显示窗口内存在用量的日期;未来时间戳不会计入。同一模型来自不同 provider 时会分别统计,例如 `deepseek-official · deepseek-chat` 与 `ark · deepseek-chat`。
249
+
250
+ ## Agent 友好安装 / Agent-friendly installation
251
+
252
+ <details>
253
+ <summary><strong>复制给 Codex、Claude Code 或其他本地编码 Agent</strong></summary>
254
+
255
+ ```text
256
+ Install or update dsh-usage-stats from:
257
+ https://github.com/Ychris12138/dsh-usage-stats
258
+
259
+ Constraints:
260
+ - Resolve DSH_HOME from the environment; otherwise use ~/.dsh.
261
+ - Do not read, print, edit, or request .credentials.yaml, auth.json, cookies, or any API key.
262
+ - Do not expose the plugin through a reverse proxy.
263
+ - Do not restart or terminate an existing dsh process without asking me.
264
+
265
+ Procedure:
266
+ 1. Confirm node, npx, and dsh are available.
267
+ 2. Prefer `dsh plugin --profile web update dsh-usage-stats` when already installed; otherwise use `dsh plugin --profile web add "github:Ychris12138/dsh-usage-stats"`.
268
+ 3. If unavailable, use: npx --yes github:Ychris12138/dsh-usage-stats
269
+ 4. Do not combine bundle installation with an existing manual dsh-usage-stats Cordis entry.
270
+ 5. For npx, require a verified package and exactly one Cordis entry, then run again with --check.
271
+ 6. Report the installation path and resolved profile paths.
272
+ 7. If dsh web is running, report that a restart is needed and stop.
273
+
274
+ Optional account setup (never handle secret values yourself):
275
+ - OpenRouter account balance requires OPENROUTER_MANAGEMENT_KEY, not the inference key.
276
+ - OpenCode Go may reuse local auth.json or use OPENCODE_GO_API_KEY.
277
+ - Z.ai uses ZAI_API_KEY; China accounts may set ZAI_API_REGION=bigmodel-cn.
278
+ - Kimi and MiniMax use KIMI_API_KEY and MINIMAX_API_KEY.
279
+ - Never ask me to paste a key or browser cookie into chat.
280
+
281
+ Optional monitor setup:
282
+ - Read configured Harness provider ids and ask which id should receive a monitor.
283
+ - Add only non-secret config under the existing dsh-usage-stats Cordis entry.
284
+ - Store credential reference names, never credential values.
285
+ - Validate relative request.path and JSON Pointer fields beginning with /.
286
+ - Do not enable cross-origin, insecure HTTP, or private-network access unless I explicitly request it.
287
+ ```
288
+
289
+ 只获准检查而不能修改时运行:
290
+
291
+ ```bash
292
+ npx --yes github:Ychris12138/dsh-usage-stats --check
293
+ ```
294
+
295
+ 安装器退出码:未知参数返回 `2`;文件、版本或配置验证失败返回非零;成功时输出已验证版本、安装目录和 patch 路径。Agent 无需自行解析或重写 YAML。
296
+
297
+ </details>
298
+
299
+ ## 隐私与安全 / Privacy & security
300
+
301
+ - API Key、OpenCode `auth.json`、Cookie 与管理 PAT 不会进入浏览器响应、插件缓存或日志。
302
+ - Sub2API `sub2api-auth` 复用 provider 自己的推理 API Key(模型页已配置的那个),不会再引入或落盘额外的面板凭据。
303
+ - 自定义 monitor 默认要求 HTTPS、同源相对路径、手动 redirect 和 JSON 响应,body 上限为 1 MiB。
304
+ - 发凭据前会筛选域名的 IPv4/IPv6 解析结果并固定一个允许的连接地址,优先使用公网地址;HTTPS 域名解析到 `198.18.0.0/15` 时可作为 Clash/Mihomo 等代理的 synthetic fake-IP 使用。字面量 `198.18/15`、其他私网/特殊地址仍默认拒绝,防止 DNS rebinding 绕过私网限制。
305
+ - `usageBaseURL` 禁止内嵌 username/password;`Authorization`、`X-API-Key`、`API-Key` 等 header 必须由 credential ref 注入。
306
+ - 五个端点仅接受 GET,并同时校验 peer socket 与 Host;支持 IPv4、IPv4-mapped IPv6 和 `[::1]:port`。
307
+ - 用量缓存 `~/.dsh/storages/usage-stats-cache.json` 只保存聚合 Token、会话 id、不透明 revision 与折叠游标,不保存提示词、回复或文件路径。
308
+
309
+ 本机反向代理会让插件看到代理自身的回环地址。请勿把端点经反向代理暴露到局域网或公网;确需代理时必须在代理层增加可靠认证与访问控制。安全问题请按 [SECURITY.md](SECURITY.md) 私下报告。
310
+
311
+ ## 正确性与数据口径 / Correctness
312
+
313
+ 统计值来自 `assistant/chunk` 或 `assistant/message` 中 provider-reported `usage`,不是本地估算。相同 turn/step 的后续样本会替换旧样本,并按 `provider/model` 归集。
314
+
315
+ - 活跃会话只处理新追加事件。
316
+ - 持久化会话使用不透明 revision;未变化时不重复读取日志。
317
+ - seq 缺口、日志重写或 live/persisted 切换时完整重折叠该会话。
318
+ - 聚合采用 single-flight,并在同一临界区原子保存缓存。
319
+ - `validate:live` 会逐会话比较 raw artifact、`session.history`、插件端点与官方 token projection;缺文件或不一致会返回非零。
320
+
321
+ ## API
322
+
323
+ | Method | Path | Response |
324
+ | --- | --- | --- |
325
+ | `GET` | `/api/usage-stats/usage` | 按日期/provider/model 聚合的 Token 与缓存命中率 |
326
+ | `GET` | `/api/usage-stats/providers` | provider 列表、account mode、adapter、状态与预警摘要 |
327
+ | `GET` | `/api/usage-stats/account?provider=<id>` | 当前 provider 的统一余额或 Token Plan 快照;`refresh=1` 强制刷新 |
328
+ | `GET` | `/api/usage-stats/balance?provider=<id>` | `0.1.x` 余额兼容路由 |
329
+ | `GET` | `/api/usage-stats/subscriptions` | `0.1.x` Token Plan 兼容路由 |
330
+
331
+ 非 GET 返回 `405`,非回环请求返回 `403`;所有响应均为 JSON 并带 `Cache-Control: no-cache`。
332
+
333
+ ## 开发与验证 / Development
334
+
335
+ ```bash
336
+ npm install
337
+ npm run check
338
+ npm test
339
+ npm pack --dry-run
340
+ ```
341
+
342
+ `npm test` 完全离线,覆盖 bundle、客户端渲染与请求竞态、服务端安全边界、余额/Token Plan adapter、缓存和安装器幂等性。真实数据验证需先运行 `dsh web`:
343
+
344
+ ```bash
345
+ npm run validate:live
346
+ node scripts/check-balance.mjs
347
+ ```
348
+
349
+ 所有服务端脚本均遵循 `DSH_HOME`。`check-balance.mjs` 可能显示真实余额,不要把输出粘贴到公开 issue。
350
+
351
+ ## 兼容性与致谢 / Compatibility & credits
352
+
353
+ 当前版本为 `0.2.0`。插件依赖 Harness 客户端模块加载器、Cordis 服务与 session persistence;Harness 预发布接口变化时可能需要同步适配。
354
+
355
+ - [Javis603/token-monitor](https://github.com/Javis603/token-monitor):参考多 provider 配额归一化与 Z.ai 限额解析。
356
+ - [xiaoqi20/dsh-opencode-go-usage](https://github.com/xiaoqi20/dsh-opencode-go-usage):参考 DSH 凭据接入、OpenCode `auth.json` 回退与 Bearer usage endpoint。
357
+
358
+ 本项目重新实现统一 account protocol、adapter 与单供应商 UI,不复制参考项目界面。
359
+
360
+ ## License
361
+
362
+ [MIT](LICENSE)
package/SECURITY.md ADDED
@@ -0,0 +1,17 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please use this repository's private vulnerability-reporting form under the GitHub **Security** tab when it is available. If it is unavailable, open a minimal issue asking the maintainer for a private contact channel; do not include exploit details in that issue.
6
+
7
+ 请优先通过 GitHub **Security** 页面中的私密漏洞报告功能联系维护者。如果该入口不可用,请只创建一个不含利用细节的简短 issue,请求私下沟通渠道。
8
+
9
+ Never include DeepSeek API keys, credentials files, session contents, raw logs, or unredacted balance data in a report. Revoke any credential that may already have been exposed.
10
+
11
+ 报告中不要附带 DeepSeek API key、凭据文件、会话内容、原始日志或未脱敏余额。已经暴露的凭据应立即吊销。
12
+
13
+ ## Scope
14
+
15
+ Security fixes target the latest version on the default branch. The five HTTP endpoints are designed for direct loopback use only; exposing them through a reverse proxy is outside the supported security model unless the proxy adds authentication and access control.
16
+
17
+ Declarative account monitors are trusted local configuration, but they still default to HTTPS, same-origin relative paths, manual redirects, JSON-only responses, and a 1 MiB response limit. Before sending credentials, the plugin filters IPv4/IPv6 DNS answers and pins one validated address for the connection. Public addresses are preferred. For HTTPS hostnames only, an IPv4 address in `198.18.0.0/15` may be accepted as a proxy-synthetic fake-IP mapping for Clash/Mihomo-style TUN DNS. Literal targets in that range remain blocked by default, and other private or special-use addresses still require explicit `allowPrivateNetwork` opt-in. Enabling cross-origin, insecure HTTP, or private-network access expands the trust boundary and should be done only for an endpoint you control.
@@ -0,0 +1,5 @@
1
+ # Mount the usage-stats server plugin when this package is registered as a
2
+ # DeepSeek Harness profile bundle.
3
+ - insert:
4
+ - id: usage-stats
5
+ name: @ychris12138/dsh-usage-stats
@@ -0,0 +1,176 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1400" height="860" viewBox="0 0 1400 860" role="img" aria-labelledby="title desc">
2
+ <title id="title">dsh-usage-stats provider account and token usage panel</title>
3
+ <desc id="desc">A sanitized vector preview of the DeepSeek Harness usage panel showing a provider balance, token totals, a monthly heatmap, and recent usage.</desc>
4
+ <defs>
5
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
6
+ <stop offset="0" stop-color="#f7f9ff"/>
7
+ <stop offset="0.52" stop-color="#f4f7fc"/>
8
+ <stop offset="1" stop-color="#eef3ff"/>
9
+ </linearGradient>
10
+ <linearGradient id="panel" x1="0" y1="0" x2="0" y2="1">
11
+ <stop offset="0" stop-color="#ffffff"/>
12
+ <stop offset="1" stop-color="#fbfcff"/>
13
+ </linearGradient>
14
+ <linearGradient id="account" x1="0" y1="0" x2="1" y2="1">
15
+ <stop offset="0" stop-color="#edf4ff"/>
16
+ <stop offset="0.55" stop-color="#f8fbff"/>
17
+ <stop offset="1" stop-color="#ffffff"/>
18
+ </linearGradient>
19
+ <linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
20
+ <stop offset="0" stop-color="#2368f2"/>
21
+ <stop offset="1" stop-color="#4a8cff"/>
22
+ </linearGradient>
23
+ <filter id="shadow" x="-20%" y="-20%" width="140%" height="160%">
24
+ <feDropShadow dx="0" dy="18" stdDeviation="24" flood-color="#20355d" flood-opacity="0.14"/>
25
+ </filter>
26
+ <filter id="soft" x="-30%" y="-30%" width="160%" height="160%">
27
+ <feDropShadow dx="0" dy="7" stdDeviation="12" flood-color="#2b4d88" flood-opacity="0.10"/>
28
+ </filter>
29
+ <style>
30
+ text { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; fill: #172033; }
31
+ .muted { fill: #718096; }
32
+ .label { fill: #536179; font-size: 14px; font-weight: 600; }
33
+ .tiny { fill: #7d899d; font-size: 12px; }
34
+ .day { fill: #5d687b; font-size: 11px; font-weight: 600; text-anchor: middle; dominant-baseline: middle; }
35
+ .day-light { fill: #ffffff; }
36
+ </style>
37
+ </defs>
38
+
39
+ <rect width="1400" height="860" rx="30" fill="url(#bg)"/>
40
+ <circle cx="1275" cy="95" r="180" fill="#dfeaff" opacity="0.55"/>
41
+ <circle cx="1170" cy="790" r="230" fill="#e6efff" opacity="0.66"/>
42
+ <path d="M1010 0h390v860h-390c84-160 91-302 20-426C969 328 970 183 1010 0Z" fill="#f8faff" opacity="0.72"/>
43
+
44
+ <!-- Harness rail -->
45
+ <rect x="28" y="32" width="66" height="796" rx="24" fill="#ffffff" stroke="#e5eaf3"/>
46
+ <g transform="translate(48 55)">
47
+ <path d="M4 16c2-7 7-11 14-11 4 0 8 2 10 5-5 0-9 2-11 7 4-2 8-2 12 0-3 7-8 10-15 9-6-1-10-4-10-10Z" fill="#172033"/>
48
+ <circle cx="23" cy="12" r="2" fill="#ffffff"/>
49
+ </g>
50
+ <g fill="none" stroke="#56647b" stroke-width="2" stroke-linecap="round">
51
+ <circle cx="61" cy="142" r="11"/><path d="M61 136v12M55 142h12"/>
52
+ <circle cx="61" cy="214" r="11"/><path d="M56 209l10 10M66 209l-10 10"/>
53
+ <rect x="49" y="276" width="24" height="20" rx="5"/><path d="M55 282h12M55 289h8"/>
54
+ </g>
55
+ <rect x="41" y="675" width="40" height="40" rx="13" fill="#eaf2ff"/>
56
+ <g transform="translate(50 684)" fill="none" stroke="#246ff2" stroke-width="2"><ellipse cx="11" cy="5" rx="9" ry="4"/><path d="M2 5v12c0 2 4 4 9 4s9-2 9-4V5M2 11c0 2 4 4 9 4s9-2 9-4"/><circle cx="19" cy="18" r="5" fill="#fff"/><path d="M19 15v6M16 18h6"/></g>
57
+ <g transform="translate(49 758)" fill="none" stroke="#56647b" stroke-width="2"><circle cx="12" cy="12" r="8"/><path d="M12 1v4M12 19v4M1 12h4M19 12h4M4 4l3 3M17 17l3 3M20 4l-3 3M7 17l-3 3"/></g>
58
+
59
+ <!-- Main panel -->
60
+ <g filter="url(#shadow)">
61
+ <rect x="126" y="42" width="720" height="776" rx="24" fill="url(#panel)" stroke="#dfe5ef"/>
62
+ </g>
63
+ <rect x="126" y="42" width="720" height="72" rx="24" fill="#ffffff"/>
64
+ <path d="M126 90h720v24H126Z" fill="#ffffff"/>
65
+ <line x1="126" y1="114" x2="846" y2="114" stroke="#e9edf4"/>
66
+ <g transform="translate(154 66)" fill="none" stroke="#1e293b" stroke-width="2"><ellipse cx="10" cy="5" rx="8" ry="4"/><path d="M2 5v13c0 2 4 4 8 4s8-2 8-4V5M2 11c0 2 4 4 8 4s8-2 8-4"/><circle cx="19" cy="18" r="5" fill="#fff"/><path d="M19 15v6M16 18h6"/></g>
67
+ <text x="190" y="84" font-size="19" font-weight="700">用量与余额</text>
68
+ <text x="754" y="84" font-size="22" class="muted">↻</text>
69
+ <path d="M807 73l12 12M819 73l-12 12" stroke="#8490a3" stroke-width="2" stroke-linecap="round"/>
70
+
71
+ <text x="154" y="144" class="label">供应商账户</text>
72
+ <text x="154" y="172" class="tiny">当前供应商</text>
73
+ <rect x="274" y="152" width="544" height="34" rx="10" fill="#ffffff" stroke="#dce3ee"/>
74
+ <text x="290" y="174" font-size="13" font-weight="600">DeepSeek</text>
75
+ <path d="M791 166l5 5 5-5" fill="none" stroke="#718096" stroke-width="1.5"/>
76
+
77
+ <g filter="url(#soft)">
78
+ <rect x="154" y="202" width="664" height="128" rx="18" fill="url(#account)" stroke="#dbe6f7"/>
79
+ </g>
80
+ <rect x="176" y="222" width="42" height="42" rx="12" fill="url(#accent)"/>
81
+ <text x="197" y="248" text-anchor="middle" fill="#ffffff" font-size="13" font-weight="800">DS</text>
82
+ <text x="232" y="238" font-size="16" font-weight="700">DeepSeek</text>
83
+ <text x="232" y="258" class="tiny">API 余额</text>
84
+ <rect x="735" y="220" width="60" height="24" rx="12" fill="#e7f8ef"/>
85
+ <circle cx="748" cy="232" r="4" fill="#20b26b"/>
86
+ <text x="758" y="236" fill="#16844f" font-size="11" font-weight="700">实时</text>
87
+ <text x="176" y="298" font-size="26" font-weight="760">¥36.44</text>
88
+ <text x="176" y="317" class="tiny">可用余额</text>
89
+ <text x="602" y="284" class="tiny">已用</text>
90
+ <text x="602" y="306" font-size="14" font-weight="650">¥12.80</text>
91
+ <text x="700" y="284" class="tiny">总额</text>
92
+ <text x="700" y="306" font-size="14" font-weight="650">¥49.24</text>
93
+
94
+ <text x="154" y="365" class="label">Token 用量</text>
95
+ <g>
96
+ <rect x="154" y="382" width="204" height="76" rx="15" fill="#ffffff" stroke="#e2e7f0"/>
97
+ <text x="172" y="416" font-size="21" font-weight="750">1,234,567</text><text x="172" y="440" class="tiny">今日</text>
98
+ <rect x="370" y="382" width="204" height="76" rx="15" fill="#ffffff" stroke="#e2e7f0"/>
99
+ <text x="388" y="416" font-size="21" font-weight="750">8,765,432</text><text x="388" y="440" class="tiny">本月</text>
100
+ <rect x="586" y="382" width="232" height="76" rx="15" fill="#ffffff" stroke="#e2e7f0"/>
101
+ <text x="604" y="416" font-size="21" font-weight="750">42,000,000</text><text x="604" y="440" class="tiny">累计</text>
102
+ </g>
103
+ <text x="154" y="482" class="tiny">今日缓存命中率 98.7%</text>
104
+
105
+ <text x="154" y="520" class="label">当月每日用量</text>
106
+ <text x="603" y="520" class="muted" font-size="20">‹</text>
107
+ <text x="652" y="520" font-size="15" font-weight="700">2026年8月</text>
108
+ <text x="792" y="520" class="muted" font-size="20">›</text>
109
+
110
+ <g class="tiny" text-anchor="middle">
111
+ <text x="181" y="554">一</text><text x="267" y="554">二</text><text x="353" y="554">三</text><text x="439" y="554">四</text><text x="525" y="554">五</text><text x="611" y="554">六</text><text x="697" y="554">日</text>
112
+ </g>
113
+ <g>
114
+ <!-- calendar row 1 -->
115
+ <rect x="574" y="570" width="74" height="48" rx="11" fill="#edf0f5"/><text x="611" y="595" class="day">1</text>
116
+ <rect x="660" y="570" width="74" height="48" rx="11" fill="#edf0f5"/><text x="697" y="595" class="day">2</text>
117
+ <!-- row 2 -->
118
+ <rect x="144" y="628" width="74" height="48" rx="11" fill="#edf0f5"/><text x="181" y="653" class="day">3</text>
119
+ <rect x="230" y="628" width="74" height="48" rx="11" fill="#edf0f5"/><text x="267" y="653" class="day">4</text>
120
+ <rect x="316" y="628" width="74" height="48" rx="11" fill="#edf0f5"/><text x="353" y="653" class="day">5</text>
121
+ <rect x="402" y="628" width="74" height="48" rx="11" fill="#edf0f5"/><text x="439" y="653" class="day">6</text>
122
+ <rect x="488" y="628" width="74" height="48" rx="11" fill="#edf0f5"/><text x="525" y="653" class="day">7</text>
123
+ <rect x="574" y="628" width="74" height="48" rx="11" fill="#edf0f5"/><text x="611" y="653" class="day">8</text>
124
+ <rect x="660" y="628" width="74" height="48" rx="11" fill="#edf0f5"/><text x="697" y="653" class="day">9</text>
125
+ <!-- row 3 -->
126
+ <rect x="144" y="686" width="74" height="48" rx="11" fill="#edf0f5"/><text x="181" y="711" class="day">10</text>
127
+ <rect x="230" y="686" width="74" height="48" rx="11" fill="#edf0f5"/><text x="267" y="711" class="day">11</text>
128
+ <rect x="316" y="686" width="74" height="48" rx="11" fill="#dce9ff"/><text x="353" y="711" class="day">12</text>
129
+ <rect x="402" y="686" width="74" height="48" rx="11" fill="#2368f2"/><text x="439" y="711" class="day day-light">13</text>
130
+ <rect x="488" y="686" width="74" height="48" rx="11" fill="#4d8cf5"/><text x="525" y="711" class="day day-light">14</text>
131
+ <rect x="574" y="686" width="74" height="48" rx="11" fill="#a9c8fb" stroke="#2368f2"/><text x="611" y="711" class="day">15</text>
132
+ <rect x="660" y="686" width="74" height="48" rx="11" fill="#edf0f5"/><text x="697" y="711" class="day">16</text>
133
+ </g>
134
+ <g transform="translate(154 758)">
135
+ <text x="0" y="16" class="tiny">少</text>
136
+ <rect x="28" y="5" width="13" height="13" rx="3" fill="#edf0f5"/><rect x="47" y="5" width="13" height="13" rx="3" fill="#dce9ff"/><rect x="66" y="5" width="13" height="13" rx="3" fill="#a9c8fb"/><rect x="85" y="5" width="13" height="13" rx="3" fill="#4d8cf5"/><rect x="104" y="5" width="13" height="13" rx="3" fill="#2368f2"/>
137
+ <text x="126" y="16" class="tiny">多</text>
138
+ </g>
139
+
140
+ <!-- Release callouts -->
141
+ <g transform="translate(922 126)">
142
+ <rect x="0" y="0" width="112" height="30" rx="15" fill="#e7efff"/>
143
+ <text x="56" y="20" text-anchor="middle" fill="#2468db" font-size="12" font-weight="750">v0.2.0</text>
144
+ <text x="0" y="78" font-size="38" font-weight="800">一个面板,</text>
145
+ <text x="0" y="124" font-size="38" font-weight="800">看清每个 Provider</text>
146
+ <text x="0" y="166" font-size="16" class="muted">余额、订阅额度与 Token 用量统一呈现。</text>
147
+ <text x="0" y="192" font-size="16" class="muted">只显示当前选择项,后台每 5 分钟持续刷新。</text>
148
+
149
+ <g transform="translate(0 244)">
150
+ <rect width="390" height="86" rx="18" fill="#ffffff" stroke="#dfe6f2"/>
151
+ <rect x="18" y="18" width="50" height="50" rx="15" fill="#eaf2ff"/>
152
+ <path d="M34 43h18M43 34v18" stroke="#246ff2" stroke-width="3" stroke-linecap="round"/>
153
+ <text x="86" y="39" font-size="15" font-weight="750">统一账户卡片</text>
154
+ <text x="86" y="61" font-size="13" class="muted">API 余额与 Token Plan 使用同一框架</text>
155
+ </g>
156
+ <g transform="translate(0 344)">
157
+ <rect width="390" height="86" rx="18" fill="#ffffff" stroke="#dfe6f2"/>
158
+ <rect x="18" y="18" width="50" height="50" rx="15" fill="#ecf9f2"/>
159
+ <path d="M32 53l9-10 7 6 9-14" fill="none" stroke="#20a566" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
160
+ <text x="86" y="39" font-size="15" font-weight="750">真实用量归集</text>
161
+ <text x="86" y="61" font-size="13" class="muted">按日期、供应商与模型下钻分析</text>
162
+ </g>
163
+ <g transform="translate(0 444)">
164
+ <rect width="390" height="86" rx="18" fill="#ffffff" stroke="#dfe6f2"/>
165
+ <rect x="18" y="18" width="50" height="50" rx="15" fill="#fff4e7"/>
166
+ <path d="M43 29c9 0 16 7 16 16s-7 16-16 16-16-7-16-16 7-16 16-16Z" fill="none" stroke="#e58a25" stroke-width="3"/><path d="M43 36v10l7 4" fill="none" stroke="#e58a25" stroke-width="3" stroke-linecap="round"/>
167
+ <text x="86" y="39" font-size="15" font-weight="750">后台自动刷新</text>
168
+ <text x="86" y="61" font-size="13" class="muted">面板关闭后仍维持账户快照</text>
169
+ </g>
170
+ <g transform="translate(0 568)">
171
+ <circle cx="9" cy="9" r="9" fill="#246ff2"/><path d="M5 9l3 3 6-7" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
172
+ <text x="28" y="14" font-size="14" font-weight="650">DeepSeek Harness Web GUI</text>
173
+ <text x="0" y="48" font-size="12" class="muted">示例数据 · Sanitized demo data</text>
174
+ </g>
175
+ </g>
176
+ </svg>