dsh-cost-meter 1.5.10 → 1.5.13
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.en.md +7 -7
- package/README.md +7 -7
- package/lib/client.js +39 -10
- package/lib/coding-plans.js +137 -0
- package/lib/index.js +33 -10
- package/lib/pricing.js +6 -4
- package/lib/store.js +49 -7
- package/lib/typert.host.js +9 -3
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Per-conversation cost · daily totals · OpenCode Go subscription quota display · budget with usage percentage · official account balance · custom provider balance · balance progress bar · history · peak/off-peak pricing hours display (peak hours UTC 01:00–04:00, 06:00–10:00) · one-click price sync from the official docs · Codex-style token usage heat grid · multi-vendor model pricing (built-in 90+ model price catalog with auto-matching) · mainstream Coding Plan quota queries & display (Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -29,7 +29,7 @@ English | [中文](README.md)
|
|
|
29
29
|
| Official balance | Sidebar top / Settings page (configurable) | Total / granted / topped-up balance, auto-refresh + manual refresh; optional three-segment progress bar (blue/orange/gray) |
|
|
30
30
|
| Custom provider balance | Sidebar / Settings page (configurable) | Configurable HTTP balance lookup (e.g. LiteLLM); bilingual labels, currency, extract rules; collapsible panel alongside Coding Plan quotas |
|
|
31
31
|
| OpenCode Go quota | Sidebar / Settings / bottom-right dock (configurable) | Rolling-5h / weekly / monthly usage percent and reset times, each window toggleable independently, budget used % can show alongside; key auto-discovered (DSH credential store OPENCODE_GO_API_KEY / env / opencode login) or entered manually |
|
|
32
|
-
| Coding plan quotas | Settings page | Multi-vendor coding-plan quota queries (Anthropic Claude Pro/Max, Z.ai / Zhipu GLM Coding Plan, MiniMax Token Plan, Kimi/Moonshot balance, OpenRouter credits, SiliconFlow balance); per-vendor enable switch and key, credentials only sent to official endpoints; neutral hints when no credentials/subscription |
|
|
32
|
+
| Coding plan quotas | Settings page | Multi-vendor coding-plan quota queries (Anthropic Claude Pro/Max, Z.ai / Zhipu GLM Coding Plan, MiniMax Token Plan, Kimi/Moonshot balance, OpenRouter credits, SiliconFlow balance); per-vendor enable switch and key, credentials only sent to official endpoints; neutral hints when no credentials/subscription; SCNet Token Plan has no quota API — monthly usage is estimated from the local ledger via the official credits deduction table (no credentials needed) |
|
|
33
33
|
| Today's cost | Sidebar bottom (above the settings button) | “Today ¥x”, hover for call count and token details |
|
|
34
34
|
| Budget box | Sidebar bottom (between the balance row and the settings button) | Rounded-square frame: budget, used %, progress bar, today's cost & share of budget, used/limit; ≥80% warning, ≥100% over-budget |
|
|
35
35
|
| Summary cards | Settings page | Today / this month / cumulative cost and call counts |
|
|
@@ -42,7 +42,7 @@ English | [中文](README.md)
|
|
|
42
42
|
| Official price sync | Settings page | Fetches and parses the official pricing page, applies with one click |
|
|
43
43
|
| UI language | Settings → Display settings | Simplified Chinese / English / Follow browser (auto); switches instantly and auto-saves |
|
|
44
44
|
| AI price sync | [prompt](docs/AI-PRICE-SYNC-PROMPT.en.md) | DeepSeek official sync; other providers use the verified official price catalog and manual configuration |
|
|
45
|
-
| Model & Plan adaptation guide | [adaptation doc](docs/model-and-plan-adaptation.en.md) | Adaptation matrix for per-model billing and the
|
|
45
|
+
| Model & Plan adaptation guide | [adaptation doc](docs/model-and-plan-adaptation.en.md) | Adaptation matrix for per-model billing and the 7 Coding Plan vendors, the auto-matching mechanism and price sources ([中文](docs/model-and-plan-adaptation.md)) |
|
|
46
46
|
| Multi-provider billing | Settings / ledger | OpenAI, Anthropic, Google Gemini, Mistral and other providers with input/output, cache and reasoning-token pricing isolated by provider + model |
|
|
47
47
|
| Model-name auto-matching | Settings / ledger | Unknown model ids are matched against the price table: case/spaces/hyphens/dots and bracket annotations (e.g. (go)) are ignored — a normalized-equal or containing name hits (e.g. `gpt5.6 luna(go)`); router providers (opencode/zen etc.) search across all vendors; can be restricted to exact match, and unmatched models can be pinned to a specific entry in Settings |
|
|
48
48
|
| Extended price catalog | Settings → Extended price catalog | Built-in reference catalog grouped by vendor and model family (expandable; vendors collapsed by default); mount entries into billing with one click — mounted third-party models live inside the catalog and stay editable; a per-model “Show directly in Cost settings” toggle chooses which models (DeepSeek included) appear directly in the price table |
|
|
@@ -191,22 +191,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
191
191
|
dsh plugin --profile web add dsh-cost-meter
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
**PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.5.
|
|
194
|
+
**PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.5.13`** — review the script before running):
|
|
195
195
|
|
|
196
196
|
```powershell
|
|
197
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
197
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.13/install.ps1 | iex
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
201
201
|
|
|
202
202
|
```sh
|
|
203
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
203
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.13
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
Without git, use the GitHub tag archive:
|
|
207
207
|
|
|
208
208
|
```sh
|
|
209
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
209
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.13.tar.gz
|
|
210
210
|
```
|
|
211
211
|
|
|
212
212
|
After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow 六家)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
| 官方余额 | 侧边栏顶部 / 设置页(可配) | 总余额 / 赠送 / 充值,自动刷新 + 手动刷新;可选三段进度条(蓝/橙/灰) |
|
|
30
30
|
| 自定义 Provider 余额 | 侧边栏 / 设置页(可配) | 可配置 HTTP 查询任意 Provider 余额(LiteLLM 等);中/英名称、币种、extract 规则;与 Coding Plan 同区可折叠配置 |
|
|
31
31
|
| OpenCode Go 额度 | 侧边栏 / 设置页 / 右下角(dock,可配) | 滚动 5 小时 / 本周 / 本月用量百分比与重置时间,三档可分别开关,可同时显示预算已用%;Key 自动发现(DSH 凭据库 OPENCODE_GO_API_KEY / 环境变量 / opencode 登录态)或手动填写 |
|
|
32
|
-
| Coding Plan 额度 | 设置页 | 多厂商 coding plan 订阅额度查询(Anthropic Claude Pro/Max、Z.ai/智谱 GLM、MiniMax Token Plan、Kimi/Moonshot 余额、OpenRouter credits、SiliconFlow 余额),各家独立启用开关与 Key
|
|
32
|
+
| Coding Plan 额度 | 设置页 | 多厂商 coding plan 订阅额度查询(Anthropic Claude Pro/Max、Z.ai/智谱 GLM、MiniMax Token Plan、Kimi/Moonshot 余额、OpenRouter credits、SiliconFlow 余额),各家独立启用开关与 Key,凭据只发往官方端点;无凭据/无订阅为中性提示;SCNet 超算互联网 Token Plan 无 API 额度端点,按官方 Credits 抵扣表由本地账本估算月度用量(无需凭据) |
|
|
33
33
|
| 当日费用 | 侧边栏底部(设置按钮上方) | 「今日 ¥x」,悬停见调用次数与 token 明细 |
|
|
34
34
|
| 预算图框 | 侧边栏底部(余额行与设置按钮之间) | 圆角方形图框:预算、已用%、进度条、今日费用与占预算%、已用/额度,≥80% 预警、≥100% 超支 |
|
|
35
35
|
| 汇总卡片 | 设置页 | 今日 / 本月 / 累计费用与调用次数 |
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
| 官方价格同步 | 设置页 | 抓取解析官方定价页,一键应用 |
|
|
44
44
|
| 界面语言 | 设置页 → 显示设置 | 简体中文 / English / 跟随浏览器(自动);切换即时生效并自动保存 |
|
|
45
45
|
| AI 价格同步 | [提示词](docs/AI-PRICE-SYNC-PROMPT.md) | DeepSeek 官方同步;其他 provider 使用已核对的官方价格目录与手动配置 |
|
|
46
|
-
| 模型与 Plan 适配说明 | [适配文档](docs/model-and-plan-adaptation.md) | 各厂商模型计费与
|
|
46
|
+
| 模型与 Plan 适配说明 | [适配文档](docs/model-and-plan-adaptation.md) | 各厂商模型计费与 7 家 Coding Plan 的适配矩阵、自动匹配机制与价格来源([English](docs/model-and-plan-adaptation.en.md)) |
|
|
47
47
|
| 多 provider 计费 | 设置页 / 账本 | 支持 OpenAI、Anthropic、Google Gemini、Mistral 等 provider 的 input/output、缓存与 reasoning token 价格,按 provider+model 隔离计费 |
|
|
48
48
|
| 模型名自动匹配 | 设置页 / 账本 | 未知模型 id 自动匹配价格表:忽略大小写/空格/横杠/点号与括号附注,归一化等价或请求名包含表内模型名即命中(如 `gpt5.6 luna(go)`);路由 provider(opencode/zen 等)下跨厂商全库查找;可关闭为仅精确;未命中模型可手动指定计费条目 |
|
|
49
49
|
| 拓展价格表 | 设置页 → 拓展价格表 | 内置各厂商、按模型家族分类的参考价格目录(点开展开,厂商默认折叠);一键挂载参与计费,挂载的第三方模型默认收入表内可编辑;逐模型「在费用设置直接显示」开关自选哪些模型(含 DeepSeek)在「价格表」区直接显示 |
|
|
@@ -193,22 +193,22 @@
|
|
|
193
193
|
dsh plugin --profile web add dsh-cost-meter
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.
|
|
196
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.13`**,建议先下载审阅再运行):
|
|
197
197
|
|
|
198
198
|
```powershell
|
|
199
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
199
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.13/install.ps1 | iex
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
203
203
|
|
|
204
204
|
```sh
|
|
205
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
205
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.13
|
|
206
206
|
```
|
|
207
207
|
|
|
208
208
|
没有 git 时可用 GitHub tag 打包直链:
|
|
209
209
|
|
|
210
210
|
```sh
|
|
211
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
211
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.13.tar.gz
|
|
212
212
|
```
|
|
213
213
|
|
|
214
214
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
package/lib/client.js
CHANGED
|
@@ -473,6 +473,10 @@ window.__ModuleLoader__.load({
|
|
|
473
473
|
codingPlanKimi: 'Kimi / Moonshot',
|
|
474
474
|
codingPlanOpenrouter: 'OpenRouter',
|
|
475
475
|
codingPlanSiliconflow: 'SiliconFlow 硅基流动',
|
|
476
|
+
codingPlanScnet: 'SCNet 超算互联网 Token Plan',
|
|
477
|
+
scnetPlanCreditsLabel: '月度 Credits 额度(基础 60,000 / 标准 240,000 / 高级 600,000)',
|
|
478
|
+
scnetPlanStartLabel: '订阅起始日(可选,格式 YYYY-MM-DD;留空按自然月)',
|
|
479
|
+
scnetLocalNote: 'SCNet 无 API 额度查询端点:按官方 Credits 抵扣表(2026-08-11 生效)由本地账本估算当前计费周期用量,实际消耗以控制台账单为准;抵扣表覆盖的模型自动折算,其余模型不计入。',
|
|
476
480
|
countdownHoursOnly: '{h}小时',
|
|
477
481
|
countdownHourMinute: '{h}小时{m}分',
|
|
478
482
|
countdownMinute: '{m}分钟',
|
|
@@ -766,6 +770,10 @@ window.__ModuleLoader__.load({
|
|
|
766
770
|
codingPlanKimi: 'Kimi / Moonshot',
|
|
767
771
|
codingPlanOpenrouter: 'OpenRouter',
|
|
768
772
|
codingPlanSiliconflow: 'SiliconFlow',
|
|
773
|
+
codingPlanScnet: 'SCNet Token Plan',
|
|
774
|
+
scnetPlanCreditsLabel: 'Monthly credits quota (Basic 60,000 / Standard 240,000 / Pro 600,000)',
|
|
775
|
+
scnetPlanStartLabel: 'Plan start date (optional, YYYY-MM-DD; empty = calendar month)',
|
|
776
|
+
scnetLocalNote: 'SCNet exposes no API quota endpoint: usage for the current billing period is estimated from the local ledger using the official credits deduction table (effective 2026-08-11); actual consumption is subject to the SCNet console. Only models covered by the table are counted.',
|
|
769
777
|
countdownHoursOnly: '{h}h',
|
|
770
778
|
countdownHourMinute: '{h}h {m}m',
|
|
771
779
|
countdownMinute: '{m}m',
|
|
@@ -1036,6 +1044,9 @@ window.__ModuleLoader__.load({
|
|
|
1036
1044
|
display: e.display === 'sidebar' || e.display === 'both' || e.display === 'off' ? e.display : 'settings',
|
|
1037
1045
|
refreshMinutes: typeof e.refreshMinutes === 'number' && Number.isFinite(e.refreshMinutes) ? e.refreshMinutes : 15,
|
|
1038
1046
|
apiKey: typeof e.apiKey === 'string' ? e.apiKey : '',
|
|
1047
|
+
// SCNet 本地计量字段(issue #26):其余厂商无此二键,缺省剔除。
|
|
1048
|
+
...(typeof e.planCredits === 'number' && Number.isFinite(e.planCredits) && e.planCredits > 0 ? { planCredits: e.planCredits } : {}),
|
|
1049
|
+
...(typeof e.planStart === 'string' ? { planStart: e.planStart } : {}),
|
|
1039
1050
|
}
|
|
1040
1051
|
}
|
|
1041
1052
|
}
|
|
@@ -1261,8 +1272,8 @@ window.__ModuleLoader__.load({
|
|
|
1261
1272
|
invocation: { kind: 'direct' },
|
|
1262
1273
|
parameters: [
|
|
1263
1274
|
{ name: 'limit', wire: 'limit', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionLimit', schema: limitCodec } },
|
|
1264
|
-
{ name: 'sort', wire: 'sort', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionSort', schema: sortCodec } },
|
|
1265
|
-
{ name: 'dir', wire: 'dir', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionSortDir', schema: sortCodec } },
|
|
1275
|
+
{ name: 'sort', wire: 'sort', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionSort', schema: sortCodec }, acceptsUndefined: true },
|
|
1276
|
+
{ name: 'dir', wire: 'dir', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-cost-meter#SessionSortDir', schema: sortCodec }, acceptsUndefined: true },
|
|
1266
1277
|
],
|
|
1267
1278
|
result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#TopSessions', schema: topSessionsCodec },
|
|
1268
1279
|
},
|
|
@@ -3399,6 +3410,7 @@ window.__ModuleLoader__.load({
|
|
|
3399
3410
|
{ id: 'kimi', labelKey: 'codingPlanKimi' },
|
|
3400
3411
|
{ id: 'openrouter', labelKey: 'codingPlanOpenrouter' },
|
|
3401
3412
|
{ id: 'siliconflow', labelKey: 'codingPlanSiliconflow' },
|
|
3413
|
+
{ id: 'scnet', labelKey: 'codingPlanScnet' },
|
|
3402
3414
|
]
|
|
3403
3415
|
|
|
3404
3416
|
/** Coding Plan 面板展开状态:localStorage 记住,默认折叠。 */
|
|
@@ -3498,14 +3510,31 @@ window.__ModuleLoader__.load({
|
|
|
3498
3510
|
}),
|
|
3499
3511
|
el('span', null, t('enableCodingPlan'))),
|
|
3500
3512
|
el('button', { className: 'cm-btn small', onClick: () => { void doRefresh(id) }, disabled: busyId !== null || enabled === false }, busyId === id ? t('refreshing') : t('refreshCodingPlan'))),
|
|
3501
|
-
enabled ?
|
|
3502
|
-
el(
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3513
|
+
enabled ? (id === 'scnet'
|
|
3514
|
+
? el(Fragment, null,
|
|
3515
|
+
el('div', { className: 'cm-field' },
|
|
3516
|
+
el('label', null, t('scnetPlanCreditsLabel')),
|
|
3517
|
+
el('input', {
|
|
3518
|
+
className: 'cm-input', type: 'number', min: '1', step: '1000',
|
|
3519
|
+
value: typeof cfgEntry.planCredits === 'number' && Number.isFinite(cfgEntry.planCredits) && cfgEntry.planCredits > 0 ? cfgEntry.planCredits : 240000,
|
|
3520
|
+
onChange: event => setPlan(id, 'planCredits', Number(event.target.value)),
|
|
3521
|
+
})),
|
|
3522
|
+
el('div', { className: 'cm-field' },
|
|
3523
|
+
el('label', null, t('scnetPlanStartLabel')),
|
|
3524
|
+
el('input', {
|
|
3525
|
+
className: 'cm-input', type: 'date',
|
|
3526
|
+
value: typeof cfgEntry.planStart === 'string' ? cfgEntry.planStart : '',
|
|
3527
|
+
onChange: event => setPlan(id, 'planStart', event.target.value),
|
|
3528
|
+
})),
|
|
3529
|
+
el('p', { className: 'cm-note' }, t('scnetLocalNote')))
|
|
3530
|
+
: el('div', { className: 'cm-field' },
|
|
3531
|
+
el('label', null, t('codingPlanKeyLabel')),
|
|
3532
|
+
el('input', {
|
|
3533
|
+
className: 'cm-input', type: 'password',
|
|
3534
|
+
value: typeof cfgEntry.apiKey === 'string' ? cfgEntry.apiKey : '',
|
|
3535
|
+
placeholder: 'sk-…',
|
|
3536
|
+
onChange: event => setPlan(id, 'apiKey', event.target.value),
|
|
3537
|
+
}))) : null,
|
|
3509
3538
|
body,
|
|
3510
3539
|
msgs[id] != null ? el('div', { className: 'cm-msg ' + msgs[id].kind }, msgs[id].text) : null)
|
|
3511
3540
|
}
|
package/lib/coding-plans.js
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* Kimi Code 订阅周窗/5小时窗暂无 API-Key 化公开端点(仅 kimi.com 控制台),以余额窗口接入;
|
|
16
16
|
* - OpenRouter credits 端点 openrouter.ai/api/v1/credits 存活(401,官方文档明确);
|
|
17
17
|
* - SiliconFlow 用户信息端点 api.siliconflow.cn/v1/user/info 存活(30014 Token is invalid);
|
|
18
|
+
* - SCNet(超算互联网)Token Plan 仅有 sk-tp- 专属推理端点(api.scnet.cn),额度用量只在控制台
|
|
19
|
+
* 「模型服务 → Token Plan → 我的订阅/Token 用量」可见,无 API-Key 化查询端点——以本地
|
|
20
|
+
* Credits 计量接入(官方抵扣表折算,见 SCNET_CREDIT_RATES;issue #26);
|
|
18
21
|
* - 百炼 Coding Plan / OpenAI Codex / Gemini Code Assist / GitHub Copilot 个人版暂无 API-Key 化公开用量端点(仅控制台/组织级 API),不接入。
|
|
19
22
|
*/
|
|
20
23
|
|
|
@@ -49,6 +52,13 @@ export const CODING_PLAN_PROVIDERS = {
|
|
|
49
52
|
credentialEnvs: ['SILICONFLOW_API_KEY'],
|
|
50
53
|
keyHint: 'SiliconFlow API Key(sk-*;显示账户余额)',
|
|
51
54
|
},
|
|
55
|
+
scnet: {
|
|
56
|
+
label: 'SCNet 超算互联网 Token Plan',
|
|
57
|
+
credentialEnvs: [],
|
|
58
|
+
// SCNet 未提供 API-Key 化的额度查询端点(仅控制台可见),不走网络:
|
|
59
|
+
// 按官方 Credits 抵扣表(2026-08-11)由本地账本估算,无需任何凭据。
|
|
60
|
+
keyHint: '无需凭据:按官方 Credits 抵扣表(2026-08-11 生效)由本地账本估算月度用量',
|
|
61
|
+
},
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export const CODING_PLAN_PROVIDER_IDS = Object.keys(CODING_PLAN_PROVIDERS)
|
|
@@ -336,6 +346,131 @@ export function parseSiliconFlowInfo(data) {
|
|
|
336
346
|
return win === null ? null : { balance: win }
|
|
337
347
|
}
|
|
338
348
|
|
|
349
|
+
// ── SCNet(超算互联网)Token Plan 本地 Credits 计量(issue #26)──────────────
|
|
350
|
+
//
|
|
351
|
+
// SCNet Token Plan 为 Credits 包月订阅(基础 60,000 / 标准 240,000 / 高级 600,000),
|
|
352
|
+
// 输入(缓存命中+未命中)与输出 Token 按官方抵扣表折算 Credits 从月度额度抵扣;套餐自开通日
|
|
353
|
+
// 起算,有效期至次月对应日 23:59:59(UTC+8),到期清零。平台无 API-Key 化用量端点,故按
|
|
354
|
+
// 抵扣表对本地账本当前计费周期的用量做估算(实际消耗以控制台账单为准)。
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 官方 Credits 抵扣表(2026-08-11 起生效;来源:
|
|
358
|
+
* https://ax.ac.sugon.com/ac/openapi/doc/2.0/moduleapi/plans/token-plan.html)。
|
|
359
|
+
* 单位:Credits / 百万 tokens;input=未命中缓存输入,cachedInput=命中缓存输入,output=输出。
|
|
360
|
+
*/
|
|
361
|
+
export const SCNET_CREDIT_RATES = {
|
|
362
|
+
'GLM-5.2': { input: 7543, cachedInput: 189, output: 26400 },
|
|
363
|
+
'GLM-5.1': { input: 8743, cachedInput: 175, output: 32057 },
|
|
364
|
+
'GLM-5': { input: 8743, cachedInput: 175, output: 32057 },
|
|
365
|
+
'DeepSeek-V4-Pro': { input: 10286, cachedInput: 86, output: 20571 },
|
|
366
|
+
'DeepSeek-V4-Flash': { input: 1200, cachedInput: 24, output: 2400 },
|
|
367
|
+
'DeepSeek-V4-Flash-0731': { input: 1543, cachedInput: 31, output: 3086 },
|
|
368
|
+
'Kimi-K3': { input: 34286, cachedInput: 343, output: 171429 },
|
|
369
|
+
'Kimi-K2.7-Code': { input: 8357, cachedInput: 167, output: 34714 },
|
|
370
|
+
'Kimi-K2.6': { input: 8357, cachedInput: 167, output: 34714 },
|
|
371
|
+
'Kimi-K2.5': { input: 5143, cachedInput: 103, output: 27000 },
|
|
372
|
+
'MiniMax-M3': { input: 3600, cachedInput: 72, output: 14400 },
|
|
373
|
+
'MiniMax-M2.7': { input: 3600, cachedInput: 72, output: 14400 },
|
|
374
|
+
'MiniMax-M2.5': { input: 2520, cachedInput: 50, output: 10080 },
|
|
375
|
+
'Qwen3.8-max': { input: 18514, cachedInput: 231, output: 49371 },
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 套餐档位预设(月度 Credits 额度)。 */
|
|
379
|
+
export const SCNET_TOKEN_PLANS = [
|
|
380
|
+
{ id: 'basic', credits: 60000 },
|
|
381
|
+
{ id: 'standard', credits: 240000 },
|
|
382
|
+
{ id: 'pro', credits: 600000 },
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
/** SCNet 模型名归一:小写并剔除非字母数字(GLM-5.2 → glm52;大小写/连接符差异等价)。 */
|
|
386
|
+
export function scnetCanonModelId(modelId) {
|
|
387
|
+
return String(modelId ?? '').toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const SCNET_RATE_BY_CANON = Object.fromEntries(
|
|
391
|
+
Object.entries(SCNET_CREDIT_RATES).map(([id, rate]) => [scnetCanonModelId(id), rate]),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
/** 按抵扣表折算一组 token 桶的 Credits(cacheWrite 计入未命中输入;reasoning 已含于 output 不重复计)。 */
|
|
395
|
+
export function scnetModelCredits(tokens, rate) {
|
|
396
|
+
const num = value => {
|
|
397
|
+
const n = Number(value)
|
|
398
|
+
return Number.isFinite(n) && n > 0 ? n : 0
|
|
399
|
+
}
|
|
400
|
+
const missInput = num(tokens?.input) + num(tokens?.cacheWrite)
|
|
401
|
+
return (missInput * rate.input + num(tokens?.cacheRead) * rate.cachedInput + num(tokens?.output) * rate.output) / 1_000_000
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 计算当前计费周期(本地时区):套餐自 planStart(YYYY-MM-DD)起算、每月重置,有效期至
|
|
406
|
+
* 次月对应日 23:59:59;planStart 缺省时按自然月。返回 { fromKey, toKeyInclusive, resetsAt }。
|
|
407
|
+
*/
|
|
408
|
+
export function scnetPlanPeriod(nowMs, planStart) {
|
|
409
|
+
const now = new Date(Number.isFinite(Number(nowMs)) ? nowMs : Date.now())
|
|
410
|
+
const pad = n => String(n).padStart(2, '0')
|
|
411
|
+
const keyOf = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
412
|
+
let start = null
|
|
413
|
+
if (typeof planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(planStart)) {
|
|
414
|
+
const parsed = new Date(`${planStart}T00:00:00`)
|
|
415
|
+
if (!Number.isNaN(parsed.getTime())) start = parsed
|
|
416
|
+
}
|
|
417
|
+
if (start === null) start = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
418
|
+
const addMonth = d => {
|
|
419
|
+
const next = new Date(d.getFullYear(), d.getMonth() + 1, 1)
|
|
420
|
+
const day = Math.min(d.getDate(), new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate())
|
|
421
|
+
next.setDate(day)
|
|
422
|
+
return next
|
|
423
|
+
}
|
|
424
|
+
let end = addMonth(start)
|
|
425
|
+
let guard = 0
|
|
426
|
+
while (now.getTime() >= end.getTime() && guard < 1200) {
|
|
427
|
+
start = end
|
|
428
|
+
end = addMonth(start)
|
|
429
|
+
guard += 1
|
|
430
|
+
}
|
|
431
|
+
// 官方语义:有效期至次月对应日 23:59:59(即周期末日终刻)。
|
|
432
|
+
const last = new Date(end.getTime() - 1)
|
|
433
|
+
return { fromKey: keyOf(start), toKeyInclusive: keyOf(last), resetsAt: last.toISOString() }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 汇总本地账本当前计费周期的 SCNet Credits 用量(按模型名匹配抵扣表;跨 provider 归并,
|
|
438
|
+
* 未匹配模型不计)。entry 为 codingPlans.scnet 配置(planCredits 必填、planStart 可选)。
|
|
439
|
+
* 返回 null(planCredits 非法)或 { used, total, percent, resetsAt, byModel, windows }。
|
|
440
|
+
*/
|
|
441
|
+
export function scnetTokenPlanWindows(days, entry, nowMs) {
|
|
442
|
+
const total = Number(entry?.planCredits)
|
|
443
|
+
if (!Number.isFinite(total) || total <= 0) return null
|
|
444
|
+
const period = scnetPlanPeriod(nowMs, entry?.planStart)
|
|
445
|
+
const byModel = {}
|
|
446
|
+
let used = 0
|
|
447
|
+
for (const [date, day] of Object.entries(days ?? {})) {
|
|
448
|
+
if (typeof date !== 'string' || date < period.fromKey || date > period.toKeyInclusive) continue
|
|
449
|
+
for (const [pmKey, buckets] of Object.entries(day?.byProviderModel ?? {})) {
|
|
450
|
+
const model = pmKey.includes(':') ? pmKey.slice(pmKey.indexOf(':') + 1) : pmKey
|
|
451
|
+
const canon = scnetCanonModelId(model)
|
|
452
|
+
const rate = SCNET_RATE_BY_CANON[canon]
|
|
453
|
+
if (rate === undefined || buckets === null || typeof buckets !== 'object') continue
|
|
454
|
+
const credits = scnetModelCredits(buckets, rate)
|
|
455
|
+
used += credits
|
|
456
|
+
byModel[canon] = (byModel[canon] ?? 0) + credits
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const percent = Math.min(100, Math.round((used / total) * 1000) / 10)
|
|
460
|
+
const fmt = n => Math.round(n).toLocaleString('en-US')
|
|
461
|
+
return {
|
|
462
|
+
used,
|
|
463
|
+
total,
|
|
464
|
+
percent,
|
|
465
|
+
resetsAt: period.resetsAt,
|
|
466
|
+
byModel,
|
|
467
|
+
windows: {
|
|
468
|
+
monthly: { percent, resetsAt: period.resetsAt },
|
|
469
|
+
credits: { resetsAt: '', text: `${fmt(used)} / ${fmt(total)} Credits (est.)` },
|
|
470
|
+
},
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
339
474
|
/** 各家固定官方端点(硬编码白名单;region 变体按序尝试)。 */
|
|
340
475
|
export const CODING_PLAN_ENDPOINTS = {
|
|
341
476
|
anthropic: ['https://api.anthropic.com/api/oauth/usage'],
|
|
@@ -354,6 +489,8 @@ export const CODING_PLAN_ENDPOINTS = {
|
|
|
354
489
|
kimi: ['https://api.moonshot.cn/v1/users/me/balance'],
|
|
355
490
|
openrouter: ['https://openrouter.ai/api/v1/credits'],
|
|
356
491
|
siliconflow: ['https://api.siliconflow.cn/v1/user/info'],
|
|
492
|
+
// SCNet 无 API-Key 化额度端点:本地 Credits 计量(见 scnetTokenPlanWindows),不走网络。
|
|
493
|
+
scnet: [],
|
|
357
494
|
}
|
|
358
495
|
|
|
359
496
|
const CODING_PLAN_PARSERS = {
|
package/lib/index.js
CHANGED
|
@@ -18,10 +18,10 @@ import fs from 'node:fs'
|
|
|
18
18
|
import { join } from 'node:path'
|
|
19
19
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
20
20
|
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
21
|
-
import { Ledger, applyConfigPatch, localDayKey, reconcileBalanceDelta, zeroDay } from './store.js'
|
|
21
|
+
import { Ledger, applyConfigPatch, localDayKey, pickBalanceInfo, reconcileBalanceDelta, zeroDay } from './store.js'
|
|
22
22
|
import { backfillLegacyLedger } from './backfill.js'
|
|
23
23
|
import { OFFICIAL_PRICING_URL, normalizePrice, parsePricingHtml, costOf, priceEntryFor, providerPriceEntryFor, buildPriceCatalog } from './pricing.js'
|
|
24
|
-
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, emptyCustomBalance, queryCustomBalance } from './coding-plans.js'
|
|
24
|
+
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, emptyCustomBalance, queryCustomBalance } from './coding-plans.js'
|
|
25
25
|
import { stateSchema } from './typert.host.js'
|
|
26
26
|
|
|
27
27
|
export const name = 'cost-meter'
|
|
@@ -65,6 +65,7 @@ const SERVER_MESSAGES = {
|
|
|
65
65
|
codingPlanDisabled: '{provider} 额度未启用,请先在面板中开启',
|
|
66
66
|
codingPlanRefreshed: '{provider} 额度已刷新',
|
|
67
67
|
codingPlanQueryFailed: '{provider} 额度查询失败:{message}',
|
|
68
|
+
scnetPlanCreditsInvalid: 'SCNet 月度 Credits 额度无效,请填写大于 0 的数值。',
|
|
68
69
|
},
|
|
69
70
|
en: {
|
|
70
71
|
apiKeyMissing: 'DeepSeek API key not configured (configure it in Settings → Models, or export the {env} environment variable)',
|
|
@@ -101,6 +102,7 @@ const SERVER_MESSAGES = {
|
|
|
101
102
|
codingPlanDisabled: '{provider} quota is disabled; enable it in the panel first',
|
|
102
103
|
codingPlanRefreshed: '{provider} quota refreshed',
|
|
103
104
|
codingPlanQueryFailed: '{provider} quota query failed: {message}',
|
|
105
|
+
scnetPlanCreditsInvalid: 'Invalid SCNet monthly credits quota; enter a value greater than 0.',
|
|
104
106
|
},
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -467,7 +469,8 @@ async function queryBalance(ctx, locale) {
|
|
|
467
469
|
})
|
|
468
470
|
if (!response.ok) throw new Error(tmsg(locale, 'balanceHttp', { code: String(response.status) }))
|
|
469
471
|
const data = await response.json()
|
|
470
|
-
|
|
472
|
+
// 多币种账号返回 CNY/USD 两条且顺序不稳定(#24/#25):按余额与币种挑选,不固定取首条。
|
|
473
|
+
const info = pickBalanceInfo(data?.balance_infos)
|
|
471
474
|
if (info === undefined) throw new Error(tmsg(locale, 'balanceNoInfos'))
|
|
472
475
|
const num = value => {
|
|
473
476
|
const parsed = Number(value)
|
|
@@ -525,10 +528,13 @@ function buildState(ledger, balance = emptyBalance(), goQuota = emptyGoQuota(),
|
|
|
525
528
|
const check = stateSchema.safeParse(state)
|
|
526
529
|
if (check.success) return state
|
|
527
530
|
console.warn('[dsh-cost-meter] state 与 codec 漂移,尝试降级恢复可用性:', JSON.stringify(check.error.issues?.slice(0, 3) ?? check.error))
|
|
531
|
+
// 注意剔除键必须用解构省略而非赋 undefined:priceCatalog 是 schema 声明的
|
|
532
|
+
// optional 键,显式 undefined 键会被网关 JSON 安全校验拒绝(值合法但属性不安全)。
|
|
533
|
+
const { priceCatalog: _dropped, ...stateNoCatalog } = state
|
|
528
534
|
const attempts = [
|
|
529
|
-
|
|
530
|
-
{ ...
|
|
531
|
-
{ ...
|
|
535
|
+
stateNoCatalog,
|
|
536
|
+
{ ...stateNoCatalog, codingPlans: {} },
|
|
537
|
+
{ ...stateNoCatalog, codingPlans: {}, balance: emptyBalance(), goQuota: emptyGoQuota(), customBalance: emptyCustomBalance() },
|
|
532
538
|
]
|
|
533
539
|
for (const fallback of attempts) {
|
|
534
540
|
if (stateSchema.safeParse(fallback).success) return fallback
|
|
@@ -716,6 +722,18 @@ function createService(ctx, ledger) {
|
|
|
716
722
|
codingPlanCaches[id] = { fetchedAt: Date.now(), value: emptyCodingPlan() }
|
|
717
723
|
return
|
|
718
724
|
}
|
|
725
|
+
// SCNet 无 API 额度端点(issue #26):按官方 Credits 抵扣表对本地账本同步估算——
|
|
726
|
+
// 纯本地计算开销可忽略,跳过缓存间隔,每次状态组装都随账本最新数据重算。
|
|
727
|
+
if (id === 'scnet') {
|
|
728
|
+
const result = scnetTokenPlanWindows(ledger.days ?? {}, config, Date.now())
|
|
729
|
+
codingPlanCaches[id] = {
|
|
730
|
+
fetchedAt: Date.now(),
|
|
731
|
+
value: result === null
|
|
732
|
+
? { ...emptyCodingPlan(), status: 'off', fetchedAt: Date.now(), message: tmsg(localeOf(ledger.config), 'scnetPlanCreditsInvalid') }
|
|
733
|
+
: { status: 'ok', message: '', fetchedAt: Date.now(), windows: result.windows },
|
|
734
|
+
}
|
|
735
|
+
return
|
|
736
|
+
}
|
|
719
737
|
const interval = Math.max(1, Number(config.refreshMinutes) || 15) * 60_000
|
|
720
738
|
const cache = codingPlanCaches[id]
|
|
721
739
|
if (!force && cache !== undefined && Date.now() - cache.fetchedAt < interval) return
|
|
@@ -926,11 +944,9 @@ function createService(ctx, ledger) {
|
|
|
926
944
|
if (sortKey === 'recent' && !asc) rows.reverse()
|
|
927
945
|
for (const s of rows) {
|
|
928
946
|
if (s === null || typeof s !== 'object') continue
|
|
929
|
-
|
|
947
|
+
const row = {
|
|
930
948
|
date,
|
|
931
949
|
id: String(s.id ?? ''),
|
|
932
|
-
title: typeof s.title === 'string' && s.title.length > 0 ? s.title : undefined,
|
|
933
|
-
at: Number.isFinite(Number(s.at)) && Number(s.at) > 0 ? Number(s.at) : undefined,
|
|
934
950
|
input: s.input ?? 0,
|
|
935
951
|
output: s.output ?? 0,
|
|
936
952
|
cacheRead: s.cacheRead ?? 0,
|
|
@@ -939,7 +955,14 @@ function createService(ctx, ledger) {
|
|
|
939
955
|
calls: s.calls ?? 0,
|
|
940
956
|
cost: s.cost ?? 0,
|
|
941
957
|
byProviderModel: s.byProviderModel ?? {},
|
|
942
|
-
}
|
|
958
|
+
}
|
|
959
|
+
// title/at 缺席时不得写入 undefined 键:网关对返回值做 JSON 安全校验,
|
|
960
|
+
// 显式 undefined 属性会被「undefined is not JSON-safe」拒绝,整个 RPC
|
|
961
|
+
// result-invalid,会话排行面板加载失败(未命名/无时间戳会话即触发)。
|
|
962
|
+
if (typeof s.title === 'string' && s.title.length > 0) row.title = s.title
|
|
963
|
+
const at = Number(s.at)
|
|
964
|
+
if (Number.isFinite(at) && at > 0) row.at = at
|
|
965
|
+
all.push(row)
|
|
943
966
|
}
|
|
944
967
|
}
|
|
945
968
|
if (sortKey === 'cost') all.sort((a, b) => asc ? a.cost - b.cost : b.cost - a.cost)
|
package/lib/pricing.js
CHANGED
|
@@ -248,11 +248,13 @@ export const PROVIDER_MODEL_FAMILIES = {
|
|
|
248
248
|
* 内置只读目录(含 DeepSeek 当前模型);「挂载」= 把条目复制进可编辑价格表。
|
|
249
249
|
*/
|
|
250
250
|
export function buildPriceCatalog() {
|
|
251
|
-
const catalog =
|
|
251
|
+
const catalog = Object.create(null)
|
|
252
|
+
const isUnsafeKey = (key) => key === '__proto__' || key === 'constructor' || key === 'prototype'
|
|
252
253
|
const put = (provider, id, entry) => {
|
|
253
254
|
const family = PROVIDER_MODEL_FAMILIES[provider]?.[id] ?? id
|
|
254
|
-
if (
|
|
255
|
-
if (catalog[provider]
|
|
255
|
+
if (isUnsafeKey(provider) || isUnsafeKey(family) || isUnsafeKey(id)) return
|
|
256
|
+
if (catalog[provider] === undefined) catalog[provider] = Object.create(null)
|
|
257
|
+
if (catalog[provider][family] === undefined) catalog[provider][family] = Object.create(null)
|
|
256
258
|
catalog[provider][family][id] = entry
|
|
257
259
|
}
|
|
258
260
|
for (const [id, entry] of Object.entries(DEFAULT_PRICE_TABLE.models)) put('deepseek', id, entry)
|
|
@@ -654,12 +656,12 @@ export function formatMoney(usdCost, display) {
|
|
|
654
656
|
function decodeEntities(text) {
|
|
655
657
|
return text
|
|
656
658
|
.replace(/ /g, ' ')
|
|
657
|
-
.replace(/&/g, '&')
|
|
658
659
|
.replace(/</g, '<')
|
|
659
660
|
.replace(/>/g, '>')
|
|
660
661
|
.replace(/"/g, '"')
|
|
661
662
|
.replace(/'/g, "'")
|
|
662
663
|
.replace(/—/g, '—')
|
|
664
|
+
.replace(/&/g, '&')
|
|
663
665
|
}
|
|
664
666
|
|
|
665
667
|
function stripTags(html) {
|
package/lib/store.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
priceEntryFor,
|
|
19
19
|
providerPriceEntryFor,
|
|
20
20
|
} from './pricing.js'
|
|
21
|
+
import { CODING_PLAN_PROVIDER_IDS } from './coding-plans.js'
|
|
21
22
|
|
|
22
23
|
const LEDGER_VERSION = 1
|
|
23
24
|
const MAX_SESSIONS_PER_DAY = 200
|
|
@@ -66,6 +67,9 @@ export function defaultConfig() {
|
|
|
66
67
|
anthropic: { enabled: false, display: 'settings', refreshMinutes: 15, apiKey: '' },
|
|
67
68
|
zai: { enabled: false, display: 'settings', refreshMinutes: 15, apiKey: '' },
|
|
68
69
|
minimax: { enabled: false, display: 'settings', refreshMinutes: 15, apiKey: '' },
|
|
70
|
+
// SCNet Token Plan 无 API 额度端点:按官方 Credits 抵扣表本地估算(issue #26)。
|
|
71
|
+
// planCredits=月度 Credits 额度;planStart=订阅起始日(空 = 自然月)。
|
|
72
|
+
scnet: { enabled: false, display: 'settings', refreshMinutes: 15, apiKey: '', planCredits: 240000, planStart: '' },
|
|
69
73
|
},
|
|
70
74
|
balance: {
|
|
71
75
|
display: 'both', // 余额显示位置:sidebar(主页面侧边栏) | settings(设置页) | both | off
|
|
@@ -371,7 +375,7 @@ export function applyConfigPatch(current, patch) {
|
|
|
371
375
|
}
|
|
372
376
|
// codingPlans 逐项清洗:只保留已知提供商;字段非法则回退默认值(凭据只发往各家官方端点)。
|
|
373
377
|
for (const [id, raw] of Object.entries(candidate.codingPlans)) {
|
|
374
|
-
if (!
|
|
378
|
+
if (!CODING_PLAN_PROVIDER_IDS.includes(id) || raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
375
379
|
delete candidate.codingPlans[id]
|
|
376
380
|
continue
|
|
377
381
|
}
|
|
@@ -381,6 +385,12 @@ export function applyConfigPatch(current, patch) {
|
|
|
381
385
|
const minutes = Number(entry.refreshMinutes)
|
|
382
386
|
entry.refreshMinutes = Number.isFinite(minutes) && minutes >= 1 && minutes <= 1440 ? minutes : 15
|
|
383
387
|
entry.apiKey = typeof entry.apiKey === 'string' ? entry.apiKey : ''
|
|
388
|
+
// SCNet 本地计量专用字段:月度 Credits 额度与订阅起始日。
|
|
389
|
+
if (id === 'scnet') {
|
|
390
|
+
const credits = Number(entry.planCredits)
|
|
391
|
+
entry.planCredits = Number.isFinite(credits) && credits > 0 ? credits : 240000
|
|
392
|
+
entry.planStart = typeof entry.planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entry.planStart) ? entry.planStart : ''
|
|
393
|
+
}
|
|
384
394
|
}
|
|
385
395
|
if (!['dock', 'header', 'off'].includes(candidate.position)) errors.push(vmsg(locale, 'position'))
|
|
386
396
|
if (typeof candidate.sidebar !== 'boolean') errors.push(vmsg(locale, 'sidebar'))
|
|
@@ -656,17 +666,22 @@ export function sanitizeConfig(raw) {
|
|
|
656
666
|
out.customBalance = customBalance
|
|
657
667
|
const corner = out.corner
|
|
658
668
|
for (const key of ['enabled', 'goRolling', 'goWeekly', 'goMonthly', 'budget']) corner[key] = corner[key] === true || (corner[key] !== false && key !== 'enabled')
|
|
659
|
-
// codingPlans:逐家收敛(
|
|
669
|
+
// codingPlans:逐家收敛(非法条目整家回落默认;只保留已知提供商)。
|
|
660
670
|
const plans = {}
|
|
661
671
|
if (out.codingPlans !== null && typeof out.codingPlans === 'object') {
|
|
662
672
|
for (const [id, entry] of Object.entries(out.codingPlans)) {
|
|
663
|
-
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) continue
|
|
673
|
+
if (!CODING_PLAN_PROVIDER_IDS.includes(id) || entry === null || typeof entry !== 'object' || Array.isArray(entry)) continue
|
|
664
674
|
plans[id] = {
|
|
665
675
|
enabled: entry.enabled === true,
|
|
666
676
|
display: oneOf(entry.display, ['sidebar', 'settings', 'both', 'off'], 'settings'),
|
|
667
677
|
refreshMinutes: Math.min(1440, Math.max(1, Math.floor(Number(entry.refreshMinutes) || 15))),
|
|
668
678
|
apiKey: typeof entry.apiKey === 'string' ? entry.apiKey : '',
|
|
669
679
|
}
|
|
680
|
+
if (id === 'scnet') {
|
|
681
|
+
const credits = Number(entry.planCredits)
|
|
682
|
+
plans[id].planCredits = Number.isFinite(credits) && credits > 0 ? credits : 240000
|
|
683
|
+
plans[id].planStart = typeof entry.planStart === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(entry.planStart) ? entry.planStart : ''
|
|
684
|
+
}
|
|
670
685
|
}
|
|
671
686
|
}
|
|
672
687
|
out.codingPlans = plans
|
|
@@ -723,7 +738,9 @@ export class Ledger {
|
|
|
723
738
|
const ref = parsed.balanceRef
|
|
724
739
|
if (ref !== null && typeof ref === 'object' && typeof ref.date === 'string'
|
|
725
740
|
&& Number.isFinite(ref.total) && Number.isFinite(ref.granted) && Number.isFinite(ref.topped)) {
|
|
726
|
-
|
|
741
|
+
// 旧账本无 currency 字段:保留 undefined,与新版首次拉取币种不等,
|
|
742
|
+
// reconcile 时自然重置基准一次(旧参考点可能录自 USD 0.00 误读,不可信)。
|
|
743
|
+
balanceRef = { date: ref.date, total: ref.total, granted: ref.granted, topped: ref.topped, currency: typeof ref.currency === 'string' ? ref.currency : undefined, at: Number(ref.at) || 0 }
|
|
727
744
|
}
|
|
728
745
|
}
|
|
729
746
|
}
|
|
@@ -962,9 +979,10 @@ export class Ledger {
|
|
|
962
979
|
/**
|
|
963
980
|
* 余额差交叉校验(issue #18 讨论):用官方余额当日变动反推消费,与本地账本今日合计比对。
|
|
964
981
|
* 仅当余额确实减少时才对账——订阅/Coding Plan 消费不动官方余额,若强行用余额差
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
* @param
|
|
982
|
+
* 替代今日费用会把订阅用户全天消费归零;充值/额度结构变动时重置参考点防误判;
|
|
983
|
+
* 参考点与本次拉取的币种不一致时(多币种条目挑选切换/旧账本无币种标记)同样重置。
|
|
984
|
+
* @param prevRef - 上一参考点({ date, total, granted, topped, currency, at })或 null。
|
|
985
|
+
* @param balance - 本次拉取结果({ currency, totalBalance, grantedBalance, toppedUpBalance })。
|
|
968
986
|
* @param todayCost - 本地账本今日合计费用。
|
|
969
987
|
* @param dayKey - 本地日期键(YYYY-MM-DD)。
|
|
970
988
|
* @param nowMs - 当前时刻。
|
|
@@ -979,12 +997,18 @@ export function reconcileBalanceDelta(prevRef, balance, todayCost, dayKey, nowMs
|
|
|
979
997
|
total: balance.totalBalance,
|
|
980
998
|
granted: Number.isFinite(balance.grantedBalance) ? balance.grantedBalance : 0,
|
|
981
999
|
topped: Number.isFinite(balance.toppedUpBalance) ? balance.toppedUpBalance : 0,
|
|
1000
|
+
currency: typeof balance.currency === 'string' ? balance.currency : '',
|
|
982
1001
|
at: nowMs,
|
|
983
1002
|
}
|
|
984
1003
|
// 新的一天(或首次/参考点形状异常):打基准,不对账。
|
|
985
1004
|
if (prevRef === null || typeof prevRef !== 'object' || prevRef.date !== dayKey) {
|
|
986
1005
|
return { ref: snap, event: { kind: 'baseline' } }
|
|
987
1006
|
}
|
|
1007
|
+
// 币种变化(#24/#25):选中的条目币种切换(或旧参考点无币种标记)时金额不可比,
|
|
1008
|
+
// 重置基准不对账——否则 USD 0.00 误读会让余额差虚高,触发 drift 误报。
|
|
1009
|
+
if (prevRef.currency !== snap.currency) {
|
|
1010
|
+
return { ref: snap, event: { kind: 'structure-reset' } }
|
|
1011
|
+
}
|
|
988
1012
|
// 充值/额度授予变动:充值与授信只会让分项余额增加(消费只会减少),分项变大则旧参考点失效,重置不告警。
|
|
989
1013
|
if (snap.granted > prevRef.granted + 0.009 || snap.topped > prevRef.topped + 0.009) {
|
|
990
1014
|
return { ref: snap, event: { kind: 'structure-reset' } }
|
|
@@ -998,3 +1022,21 @@ export function reconcileBalanceDelta(prevRef, balance, todayCost, dayKey, nowMs
|
|
|
998
1022
|
// ok/drift 都保留当日首次基准,后续拉取继续与早间基线比对。
|
|
999
1023
|
return { ref: prevRef, event: { kind: 'ok', spent, todayCost } }
|
|
1000
1024
|
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* 从官方余额接口的 balance_infos 中挑选要展示的条目(issues #24/#25)。
|
|
1028
|
+
*
|
|
1029
|
+
* 多币种账号同时返回 CNY/USD 两条,且实测两条的排列顺序每次请求不稳定——
|
|
1030
|
+
* 固定取首条会在 USD 排前时读到 0.00,余额在正确值与 0 之间跳变。挑选规则:
|
|
1031
|
+
* - 优先取「有余额」的条目;同有余额时优先 CNY(开放平台主币种,确定性优先);
|
|
1032
|
+
* - 全部为零时优先 CNY(未充值/新账号,保证不随返回顺序跳变);
|
|
1033
|
+
* - 无 CNY 条目时兜底首条(单币种账号行为与旧版一致)。
|
|
1034
|
+
* @param infos - balance_infos 数组(元素含 currency / total_balance 等官方字段)。
|
|
1035
|
+
* @returns 选中的条目,或 undefined(无有效条目,调用方按缺 balance_infos 报错)。
|
|
1036
|
+
*/
|
|
1037
|
+
export function pickBalanceInfo(infos) {
|
|
1038
|
+
const list = Array.isArray(infos) ? infos.filter(entry => entry !== null && typeof entry === 'object') : []
|
|
1039
|
+
const positive = list.filter(entry => Number(entry.total_balance) > 0)
|
|
1040
|
+
const cnyFirst = entries => entries.find(entry => String(entry.currency).toUpperCase() === 'CNY')
|
|
1041
|
+
return cnyFirst(positive) ?? positive[0] ?? cnyFirst(list) ?? list[0]
|
|
1042
|
+
}
|
package/lib/typert.host.js
CHANGED
|
@@ -126,6 +126,9 @@ const configSchema = z.object({
|
|
|
126
126
|
display: z.enum(['sidebar', 'settings', 'both', 'off']).optional(),
|
|
127
127
|
refreshMinutes: num.optional(),
|
|
128
128
|
apiKey: z.string().optional(),
|
|
129
|
+
// SCNet 本地计量专用(issue #26):月度 Credits 额度与订阅起始日;其余厂商无此二键。
|
|
130
|
+
planCredits: num.optional(),
|
|
131
|
+
planStart: z.string().optional(),
|
|
129
132
|
})).optional(),
|
|
130
133
|
balance: z.object({
|
|
131
134
|
display: z.enum(['sidebar', 'settings', 'both', 'off']),
|
|
@@ -358,8 +361,11 @@ export const TYPERT = {
|
|
|
358
361
|
invocation: { kind: 'direct' },
|
|
359
362
|
parameters: [
|
|
360
363
|
{ name: 'limit', wire: 'limit', source: 'json', codec: _limit$codec },
|
|
361
|
-
|
|
362
|
-
|
|
364
|
+
// sort/dir 与服务端函数默认值(sort='cost', dir='desc')对应,声明可缺省:
|
|
365
|
+
// 网关对 args 字段做精确匹配,不声明 acceptsUndefined 时旧客户端单参数调用
|
|
366
|
+
// 会因 missing "sort"/"dir" 被拒,会话排行面板直接加载失败。
|
|
367
|
+
{ name: 'sort', wire: 'sort', source: 'json', codec: _sort$codec, acceptsUndefined: true },
|
|
368
|
+
{ name: 'dir', wire: 'dir', source: 'json', codec: _dir$codec, acceptsUndefined: true },
|
|
363
369
|
],
|
|
364
370
|
result: _topSessions$codec,
|
|
365
371
|
},
|
|
@@ -440,7 +446,7 @@ export const TYPERT = {
|
|
|
440
446
|
{
|
|
441
447
|
kind: 'method',
|
|
442
448
|
name: 'getTopSessions',
|
|
443
|
-
signature: 'getTopSessions(limit: number, sort
|
|
449
|
+
signature: 'getTopSessions(limit: number, sort?: string, dir?: string): TopSessions',
|
|
444
450
|
summary: '跨全部日期按指定排序返回前 N 个会话(不分日期视角)。Return the top N sessions across all days with the given sort.',
|
|
445
451
|
jsDoc: '/**\n * 跨全部日期返回前 N 个会话(每条带所属日期/标题/创建时刻)。\n * @param limit - 返回条数上限(服务端限制 1-500)。\n * @param sort - cost(费用) | time(创建时间) | recent(实时顺序)。\n * @param dir - asc | desc(默认 desc)。\n * @returns 会话列表(含 date/title/at 字段)。\n * Return the top N sessions across all days (each tagged with date/title/createdAt).\n * @param limit - Max rows to return (server clamps to 1-500).\n * @param sort - cost | time | recent (ledger/sidebar order).\n * @param dir - asc | desc (defaults to desc).\n * @returns Session list with date/title/at fields.\n */',
|
|
446
452
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cost-meter",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(
|
|
3
|
+
"version": "1.5.13",
|
|
4
|
+
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(7 家,含 SCNet Token Plan 本地 Credits 计量)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条,界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 7 vendors (incl. local credits metering for the SCNet Token Plan), custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, in a bilingual (Chinese/English) UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|