dsh-cost-meter 1.5.12 → 1.5.14

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 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
- [![version](https://img.shields.io/badge/version-1.5.12-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.14-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
11
11
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
12
12
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](https://github.com/deepseek-ai/deepseek-harness)
@@ -29,20 +29,21 @@ 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 |
36
36
  | Token usage stats | Settings page (Cost section) | All-time token totals (input/cache/output/calls) + a Codex-style 26-week daily usage heat grid that fills the settings width; hover a cell for that day's detail |
37
37
  | Today's sessions | Settings page | Per-session call count, input/cache/output tokens and cost |
38
38
  | History | Settings page | Per-day totals; retention days configurable (default 180) |
39
+ | Pre-install history import | Settings page (Data & sync) | One-click replay of all host session logs to import conversations from before the plugin was installed (missing dates are rebuilt whole; existing dates only gain previously unknown sessions; idempotent and never double-counts live metering; costs priced at per-event historical rates) |
39
40
  | Budget settings | Settings page, top | Limit, period (today / month / cumulative / custom date range), used % |
40
41
  | Price table | Settings page | Per-model off-peak / peak prices (input/output shorthand supported; cache prices derived automatically); fully editable |
41
42
  | Peak/off-peak hours display | Settings / budget / today | Shows UTC peak hours 01:00–04:00 and 06:00–10:00 with the current tier; expanded view shows a peak/off-peak period strip (current period + countdown), collapsed (rail) view shows a vertical peak/off-peak progress bar; independently toggleable |
42
43
  | Official price sync | Settings page | Fetches and parses the official pricing page, applies with one click |
43
44
  | UI language | Settings → Display settings | Simplified Chinese / English / Follow browser (auto); switches instantly and auto-saves |
44
45
  | 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 6 Coding Plan vendors, the auto-matching mechanism and price sources ([中文](docs/model-and-plan-adaptation.md)) |
46
+ | 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
47
  | 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
48
  | 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
49
  | 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 +192,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
191
192
  dsh plugin --profile web add dsh-cost-meter
192
193
  ```
193
194
 
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.12`** — review the script before running):
195
+ **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.14`** — review the script before running):
195
196
 
196
197
  ```powershell
197
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.12/install.ps1 | iex
198
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.14/install.ps1 | iex
198
199
  ```
199
200
 
200
201
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
201
202
 
202
203
  ```sh
203
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.12
204
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.14
204
205
  ```
205
206
 
206
207
  Without git, use the GitHub tag archive:
207
208
 
208
209
  ```sh
209
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.12.tar.gz
210
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.14.tar.gz
210
211
  ```
211
212
 
212
213
  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
- [![version](https://img.shields.io/badge/version-1.5.12-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.5.14-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
11
11
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
12
12
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](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
  | 汇总卡片 | 设置页 | 今日 / 本月 / 累计费用与调用次数 |
@@ -37,13 +37,14 @@
37
37
  | 今日会话明细 | 设置页 | 每个会话的调用次数、输入/缓存/输出 token 与费用 |
38
38
  | 历史记录 | 设置页 | 按天汇总,保留天数可配(默认 180 天) |
39
39
  | 历史按模型统计回填 | 设置页(按模型统计) | 按模型统计上线前的旧账本自动回放宿主会话日志重建逐模型 token/费用拆分(旧调用按当时基础价),日志已清理的部分归入「早期未分模型」残差行 |
40
+ | 导入安装前历史 | 设置页(数据与同步) | 一键回放宿主全部会话日志,把未装插件时期的对话导入账本(缺失日期整日重建,已有日期只补未知会话,幂等不与实时计费重复;金额按事件时刻历史价回推) |
40
41
  | 预算设置 | 设置页顶部 | 额度、周期(今日/本月/累计/自定义日期区间)、已用% |
41
42
  | 价格表 | 设置页 | 每模型 谷时/峰时 两档价格(支持 input/output 简写,缓存价自动补齐),增删改自由 |
42
43
  | 峰谷计价时段显示 | 设置页 / 预算 / 今日费用 | 显示 UTC 峰时段 01:00–04:00、06:00–10:00 与当前档位;展开态显示峰时/平价时段条(当前时段 + 倒计时),收起(rail)态显示竖向峰谷进度条,可单独开关 |
43
44
  | 官方价格同步 | 设置页 | 抓取解析官方定价页,一键应用 |
44
45
  | 界面语言 | 设置页 → 显示设置 | 简体中文 / English / 跟随浏览器(自动);切换即时生效并自动保存 |
45
46
  | AI 价格同步 | [提示词](docs/AI-PRICE-SYNC-PROMPT.md) | DeepSeek 官方同步;其他 provider 使用已核对的官方价格目录与手动配置 |
46
- | 模型与 Plan 适配说明 | [适配文档](docs/model-and-plan-adaptation.md) | 各厂商模型计费与 6 家 Coding Plan 的适配矩阵、自动匹配机制与价格来源([English](docs/model-and-plan-adaptation.en.md)) |
47
+ | 模型与 Plan 适配说明 | [适配文档](docs/model-and-plan-adaptation.md) | 各厂商模型计费与 7 家 Coding Plan 的适配矩阵、自动匹配机制与价格来源([English](docs/model-and-plan-adaptation.en.md)) |
47
48
  | 多 provider 计费 | 设置页 / 账本 | 支持 OpenAI、Anthropic、Google Gemini、Mistral 等 provider 的 input/output、缓存与 reasoning token 价格,按 provider+model 隔离计费 |
48
49
  | 模型名自动匹配 | 设置页 / 账本 | 未知模型 id 自动匹配价格表:忽略大小写/空格/横杠/点号与括号附注,归一化等价或请求名包含表内模型名即命中(如 `gpt5.6 luna(go)`);路由 provider(opencode/zen 等)下跨厂商全库查找;可关闭为仅精确;未命中模型可手动指定计费条目 |
49
50
  | 拓展价格表 | 设置页 → 拓展价格表 | 内置各厂商、按模型家族分类的参考价格目录(点开展开,厂商默认折叠);一键挂载参与计费,挂载的第三方模型默认收入表内可编辑;逐模型「在费用设置直接显示」开关自选哪些模型(含 DeepSeek)在「价格表」区直接显示 |
@@ -193,22 +194,22 @@
193
194
  dsh plugin --profile web add dsh-cost-meter
194
195
  ```
195
196
 
196
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.12`**,建议先下载审阅再运行):
197
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.14`**,建议先下载审阅再运行):
197
198
 
198
199
  ```powershell
199
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.12/install.ps1 | iex
200
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.14/install.ps1 | iex
200
201
  ```
201
202
 
202
203
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
203
204
 
204
205
  ```sh
205
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.12
206
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.14
206
207
  ```
207
208
 
208
209
  没有 git 时可用 GitHub tag 打包直链:
209
210
 
210
211
  ```sh
211
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.12.tar.gz
212
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.14.tar.gz
212
213
  ```
213
214
 
214
215
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
package/lib/backfill.js CHANGED
@@ -14,7 +14,7 @@ import { readdirSync, readFileSync, statSync } from 'node:fs'
14
14
  import { join } from 'node:path'
15
15
  import * as zlib from 'node:zlib'
16
16
  import { costOf, providerPriceEntryFor } from './pricing.js'
17
- import { localDayKey } from './store.js'
17
+ import { localDayKey, zeroDay } from './store.js'
18
18
 
19
19
  const ZSTD_MAGIC = 4247762216
20
20
  /** 打包行(文本/推理/工具调用增量游程)不含 header 与 usage,回放时跳过。 */
@@ -448,3 +448,136 @@ function cloneDayMap(pm) {
448
448
  }
449
449
  return out
450
450
  }
451
+
452
+ /** 汇总一组 provider:model 桶的合计(input/output/…/calls/cost)。 */
453
+ function sumDayMap(pm) {
454
+ return Object.values(pm).reduce((acc, b) => {
455
+ acc.input += b.input ?? 0
456
+ acc.output += b.output ?? 0
457
+ acc.cacheRead += b.cacheRead ?? 0
458
+ acc.cacheWrite += b.cacheWrite ?? 0
459
+ acc.reasoning += b.reasoning ?? 0
460
+ acc.calls += b.calls ?? 0
461
+ acc.cost += b.cost ?? 0
462
+ return acc
463
+ }, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, calls: 0, cost: 0 })
464
+ }
465
+
466
+ /** 把合计桶累加到日期/会话条目的顶层字段上。 */
467
+ function addTotalsTo(target, totals) {
468
+ target.input += totals.input
469
+ target.output += totals.output
470
+ target.cacheRead += totals.cacheRead
471
+ target.cacheWrite += totals.cacheWrite
472
+ target.reasoning += totals.reasoning
473
+ target.calls += totals.calls
474
+ target.cost += totals.cost
475
+ }
476
+
477
+ /** 由回放结果构造账本会话条目(title/at 缺席时不写键,与 Typert schema 一致)。 */
478
+ function sessionEntryOf(sessionId, info, totals, pm) {
479
+ const entry = {
480
+ id: sessionId,
481
+ input: totals.input,
482
+ output: totals.output,
483
+ cacheRead: totals.cacheRead,
484
+ cacheWrite: totals.cacheWrite,
485
+ reasoning: totals.reasoning,
486
+ calls: totals.calls,
487
+ cost: totals.cost,
488
+ byProviderModel: cloneDayMap(pm),
489
+ }
490
+ if (typeof info.title === 'string' && info.title.length > 0) entry.title = info.title
491
+ if (Number.isFinite(info.createdAt) && info.createdAt > 0) entry.at = info.createdAt
492
+ return entry
493
+ }
494
+
495
+ /**
496
+ * 导入安装前历史(issue #27):回放全部会话日志,为账本中缺失的日期重建
497
+ * 费用条目。与 backfillLegacyLedger 的区别:后者只补账本已有日期的按模型
498
+ * 拆分,本函数面向「插件未运行时期」的日期,由设置页显式触发。
499
+ *
500
+ * 边界(幂等,绝不与实时计费重复计数):
501
+ * - 缺失日期(账本无条目,或为无任何用量的空日)→ 整日重建(合计 + 会话
502
+ * 明细 + 按模型拆分),金额按事件时刻计价(replaySessionRecords 内完成);
503
+ * - 已有日期 → 只追加账本完全未知的会话(id 不在当日条目中),既有会话
504
+ * 条目与合计结构不动,追加桶并入日期合计与 byProviderModel;
505
+ * - 同一会话跨安装时刻:实时条目已存在,安装前用量不计入(无法安全拆分)。
506
+ * @param ledger - 已打开的账本。
507
+ * @param sessionsRoot - 宿主会话根目录($DSH_HOME/sessions)。
508
+ * @returns { days, sessions, scanned } 重建/追加的日期数、新增会话数与扫描文件数。
509
+ */
510
+ export async function importLegacyHistory(ledger, sessionsRoot) {
511
+ const result = { days: 0, sessions: 0, scanned: 0 }
512
+ const bySession = new Map()
513
+ let scannedCount = 0
514
+ for (const path of listSessionLogs(sessionsRoot)) {
515
+ if ((scannedCount += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
516
+ result.scanned += 1
517
+ let replayed
518
+ try {
519
+ replayed = replaySessionRecords(readSessionRecords(path), ledger.config, null)
520
+ } catch {
521
+ continue // 单文件损坏不阻断整体导入。
522
+ }
523
+ if (replayed.sessionId.length === 0) continue
524
+ const existing = bySession.get(replayed.sessionId)
525
+ if (existing === undefined) {
526
+ bySession.set(replayed.sessionId, { title: replayed.title, createdAt: replayed.createdAt, days: replayed.days })
527
+ continue
528
+ }
529
+ mergeDayMaps(existing.days, replayed.days)
530
+ }
531
+ if (bySession.size === 0) return result
532
+ // 汇总每个日期下有实际用量的会话(桶 calls>0 才参与)。
533
+ const dateSessions = new Map()
534
+ for (const [sessionId, info] of bySession) {
535
+ for (const [date, pm] of Object.entries(info.days)) {
536
+ const totals = sumDayMap(pm)
537
+ if (totals.calls <= 0) continue
538
+ let list = dateSessions.get(date)
539
+ if (list === undefined) dateSessions.set(date, list = [])
540
+ list.push({ sessionId, info, pm, totals })
541
+ }
542
+ }
543
+ for (const [date, list] of dateSessions) {
544
+ const day = ledger.days[date]
545
+ const dayEmpty = day === undefined
546
+ || ((day.calls ?? 0) === 0
547
+ && (Array.isArray(day.sessions) ? day.sessions.every(s => (s?.calls ?? 0) === 0) : true))
548
+ if (dayEmpty) {
549
+ // 缺失/空日:整日重建(合计 + 每会话明细;空日残留的旧 sessions 一并丢弃)。
550
+ const target = zeroDay(date)
551
+ const aggregate = {}
552
+ for (const entry of list) {
553
+ target.sessions.push(sessionEntryOf(entry.sessionId, entry.info, entry.totals, entry.pm))
554
+ mergeBucketsInto(aggregate, entry.pm)
555
+ addTotalsTo(target, entry.totals)
556
+ result.sessions += 1
557
+ }
558
+ target.byProviderModel = aggregate
559
+ ledger.days[date] = target
560
+ result.days += 1
561
+ continue
562
+ }
563
+ // 已有日期:只追加账本完全未知的会话(幂等;已知会话的安装前用量不计)。
564
+ const known = new Set((Array.isArray(day.sessions) ? day.sessions : []).map(s => s?.id))
565
+ for (const entry of list) {
566
+ if (known.has(entry.sessionId)) continue
567
+ day.sessions.push(sessionEntryOf(entry.sessionId, entry.info, entry.totals, entry.pm))
568
+ addTotalsTo(day, entry.totals)
569
+ if (day.byProviderModel === null || typeof day.byProviderModel !== 'object') day.byProviderModel = {}
570
+ mergeBucketsInto(day.byProviderModel, entry.pm)
571
+ result.sessions += 1
572
+ result.days += 1
573
+ }
574
+ }
575
+ if (result.days > 0 || result.sessions > 0) {
576
+ // 日期键按升序重建(账本原序即升序,保证历史记录视图整洁)。
577
+ const ordered = {}
578
+ for (const key of Object.keys(ledger.days).sort()) ordered[key] = ledger.days[key]
579
+ ledger.days = ordered
580
+ ledger.scheduleWrite()
581
+ }
582
+ return result
583
+ }
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}分钟',
@@ -550,6 +554,10 @@ window.__ModuleLoader__.load({
550
554
  cancel: '取消',
551
555
  syncFromDocs: '从官方文档同步价格',
552
556
  confirmReset: '确认清除全部历史?',
557
+ importLegacy: '导入安装前历史',
558
+ confirmImportLegacy: '确认导入?将回放全部会话日志,只补账本缺失的日期与会话,不影响已有记录。',
559
+ legacyImportFailed: '导入安装前历史失败:{message}',
560
+ legacyImportNote: '按当前价目表回推历史费用;已清理的日志无法回放,同一会话跨安装时刻时其安装前用量不计入。',
553
561
  confirmClear: '确认清除',
554
562
  clearAllHistory: '清除全部历史',
555
563
  lastSync: '最近同步:{time}',
@@ -766,6 +774,10 @@ window.__ModuleLoader__.load({
766
774
  codingPlanKimi: 'Kimi / Moonshot',
767
775
  codingPlanOpenrouter: 'OpenRouter',
768
776
  codingPlanSiliconflow: 'SiliconFlow',
777
+ codingPlanScnet: 'SCNet Token Plan',
778
+ scnetPlanCreditsLabel: 'Monthly credits quota (Basic 60,000 / Standard 240,000 / Pro 600,000)',
779
+ scnetPlanStartLabel: 'Plan start date (optional, YYYY-MM-DD; empty = calendar month)',
780
+ 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
781
  countdownHoursOnly: '{h}h',
770
782
  countdownHourMinute: '{h}h {m}m',
771
783
  countdownMinute: '{m}m',
@@ -843,6 +855,10 @@ window.__ModuleLoader__.load({
843
855
  cancel: 'Cancel',
844
856
  syncFromDocs: 'Sync prices from official docs',
845
857
  confirmReset: 'Clear all history?',
858
+ importLegacy: 'Import pre-install history',
859
+ confirmImportLegacy: 'Confirm import? Replays all session logs and only fills dates/sessions missing from the ledger; existing records are untouched.',
860
+ legacyImportFailed: 'Failed to import pre-install history: {message}',
861
+ legacyImportNote: 'Historical costs are estimated with the current price catalog; cleaned logs cannot be replayed, and for a session spanning the install moment its pre-install usage is not counted.',
846
862
  confirmClear: 'Confirm clear',
847
863
  clearAllHistory: 'Clear all history',
848
864
  lastSync: 'Last sync: {time}',
@@ -1036,6 +1052,9 @@ window.__ModuleLoader__.load({
1036
1052
  display: e.display === 'sidebar' || e.display === 'both' || e.display === 'off' ? e.display : 'settings',
1037
1053
  refreshMinutes: typeof e.refreshMinutes === 'number' && Number.isFinite(e.refreshMinutes) ? e.refreshMinutes : 15,
1038
1054
  apiKey: typeof e.apiKey === 'string' ? e.apiKey : '',
1055
+ // SCNet 本地计量字段(issue #26):其余厂商无此二键,缺省剔除。
1056
+ ...(typeof e.planCredits === 'number' && Number.isFinite(e.planCredits) && e.planCredits > 0 ? { planCredits: e.planCredits } : {}),
1057
+ ...(typeof e.planStart === 'string' ? { planStart: e.planStart } : {}),
1039
1058
  }
1040
1059
  }
1041
1060
  }
@@ -1250,6 +1269,11 @@ window.__ModuleLoader__.load({
1250
1269
  invocation: { kind: 'direct' }, parameters: [],
1251
1270
  result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#CostState', schema: stateCodec },
1252
1271
  },
1272
+ {
1273
+ id: 'dsh-cost-meter#costMeter/importLegacyHistory', service: 'costMeter', namespace: 'costMeter', method: 'importLegacyHistory',
1274
+ invocation: { kind: 'direct' }, parameters: [],
1275
+ result: { mode: 'strict', typeSymbol: 'dsh-cost-meter#FetchPricesResult', schema: fetchCodec },
1276
+ },
1253
1277
  {
1254
1278
  id: 'dsh-cost-meter#costMeter/getDaySessions', service: 'costMeter', namespace: 'costMeter', method: 'getDaySessions',
1255
1279
  invocation: { kind: 'direct' },
@@ -3399,6 +3423,7 @@ window.__ModuleLoader__.load({
3399
3423
  { id: 'kimi', labelKey: 'codingPlanKimi' },
3400
3424
  { id: 'openrouter', labelKey: 'codingPlanOpenrouter' },
3401
3425
  { id: 'siliconflow', labelKey: 'codingPlanSiliconflow' },
3426
+ { id: 'scnet', labelKey: 'codingPlanScnet' },
3402
3427
  ]
3403
3428
 
3404
3429
  /** Coding Plan 面板展开状态:localStorage 记住,默认折叠。 */
@@ -3498,14 +3523,31 @@ window.__ModuleLoader__.load({
3498
3523
  }),
3499
3524
  el('span', null, t('enableCodingPlan'))),
3500
3525
  el('button', { className: 'cm-btn small', onClick: () => { void doRefresh(id) }, disabled: busyId !== null || enabled === false }, busyId === id ? t('refreshing') : t('refreshCodingPlan'))),
3501
- enabled ? el('div', { className: 'cm-field' },
3502
- el('label', null, t('codingPlanKeyLabel')),
3503
- el('input', {
3504
- className: 'cm-input', type: 'password',
3505
- value: typeof cfgEntry.apiKey === 'string' ? cfgEntry.apiKey : '',
3506
- placeholder: 'sk-…',
3507
- onChange: event => setPlan(id, 'apiKey', event.target.value),
3508
- })) : null,
3526
+ enabled ? (id === 'scnet'
3527
+ ? el(Fragment, null,
3528
+ el('div', { className: 'cm-field' },
3529
+ el('label', null, t('scnetPlanCreditsLabel')),
3530
+ el('input', {
3531
+ className: 'cm-input', type: 'number', min: '1', step: '1000',
3532
+ value: typeof cfgEntry.planCredits === 'number' && Number.isFinite(cfgEntry.planCredits) && cfgEntry.planCredits > 0 ? cfgEntry.planCredits : 240000,
3533
+ onChange: event => setPlan(id, 'planCredits', Number(event.target.value)),
3534
+ })),
3535
+ el('div', { className: 'cm-field' },
3536
+ el('label', null, t('scnetPlanStartLabel')),
3537
+ el('input', {
3538
+ className: 'cm-input', type: 'date',
3539
+ value: typeof cfgEntry.planStart === 'string' ? cfgEntry.planStart : '',
3540
+ onChange: event => setPlan(id, 'planStart', event.target.value),
3541
+ })),
3542
+ el('p', { className: 'cm-note' }, t('scnetLocalNote')))
3543
+ : el('div', { className: 'cm-field' },
3544
+ el('label', null, t('codingPlanKeyLabel')),
3545
+ el('input', {
3546
+ className: 'cm-input', type: 'password',
3547
+ value: typeof cfgEntry.apiKey === 'string' ? cfgEntry.apiKey : '',
3548
+ placeholder: 'sk-…',
3549
+ onChange: event => setPlan(id, 'apiKey', event.target.value),
3550
+ }))) : null,
3509
3551
  body,
3510
3552
  msgs[id] != null ? el('div', { className: 'cm-msg ' + msgs[id].kind }, msgs[id].text) : null)
3511
3553
  }
@@ -3613,6 +3655,7 @@ window.__ModuleLoader__.load({
3613
3655
  const [message, setMessage] = useState(null)
3614
3656
  const [confirmFetch, setConfirmFetch] = useState(false)
3615
3657
  const [confirmReset, setConfirmReset] = useState(false)
3658
+ const [confirmImport, setConfirmImport] = useState(false)
3616
3659
  const [newModelId, setNewModelId] = useState('')
3617
3660
  const [busy, setBusy] = useState(false)
3618
3661
  // 价格表折叠开关(默认收起,保持设置页简洁;三角按钮展开)。
@@ -3712,6 +3755,21 @@ window.__ModuleLoader__.load({
3712
3755
  setConfirmReset(false)
3713
3756
  }
3714
3757
  }
3758
+ // 导入安装前历史(issue #27):回放宿主全部会话日志,补账本缺失的日期/会话。
3759
+ const doImportLegacy = async () => {
3760
+ if (busy) return
3761
+ setBusy(true)
3762
+ setMessage(null)
3763
+ try {
3764
+ const result = await api.importLegacyHistory()
3765
+ setMessage({ kind: 'ok', text: result.message })
3766
+ } catch (error) {
3767
+ setMessage({ kind: 'err', text: t('legacyImportFailed', { message: error?.message ?? String(error) }) })
3768
+ } finally {
3769
+ setBusy(false)
3770
+ setConfirmImport(false)
3771
+ }
3772
+ }
3715
3773
  const setField = (field, value) => {
3716
3774
  if (draft === null) return
3717
3775
  setDraft({ ...draft, [field]: value })
@@ -4148,6 +4206,12 @@ window.__ModuleLoader__.load({
4148
4206
  el('button', { className: 'cm-btn', onClick: doFetch, disabled: busy }, t('apply')),
4149
4207
  el('button', { className: 'cm-btn', onClick: () => setConfirmFetch(false) }, t('cancel')))
4150
4208
  : el('button', { className: 'cm-btn', onClick: () => setConfirmFetch(true), disabled: busy }, t('syncFromDocs')),
4209
+ confirmImport
4210
+ ? el(Fragment, null,
4211
+ el('span', { className: 'cm-hint' }, t('confirmImportLegacy')),
4212
+ el('button', { className: 'cm-btn', onClick: doImportLegacy, disabled: busy }, t('apply')),
4213
+ el('button', { className: 'cm-btn', onClick: () => setConfirmImport(false) }, t('cancel')))
4214
+ : el('button', { className: 'cm-btn', onClick: () => setConfirmImport(true), disabled: busy }, t('importLegacy')),
4151
4215
  confirmReset
4152
4216
  ? el(Fragment, null,
4153
4217
  el('span', { className: 'cm-hint' }, t('confirmReset')),
@@ -4157,6 +4221,7 @@ window.__ModuleLoader__.load({
4157
4221
  el('p', { className: 'cm-note' },
4158
4222
  t('lastSync', { time: config.fetchedAt !== null ? new Date(config.fetchedAt).toLocaleString() : t('neverSynced') })
4159
4223
  + t('source', { source: config.priceSource === 'official' ? t('sourceOfficial') : t('sourceBundled') })),
4224
+ el('p', { className: 'cm-note' }, t('legacyImportNote')),
4160
4225
  message !== null ? el('div', { className: 'cm-msg ' + message.kind }, message.text) : null))
4161
4226
  }
4162
4227
 
@@ -4228,6 +4293,15 @@ window.__ModuleLoader__.load({
4228
4293
  store.set({ status: 'ready', error: null, state })
4229
4294
  return state
4230
4295
  },
4296
+ // 导入安装前历史(issue #27):返回 { ok, message, state? },成功时刷新本地快照。
4297
+ importLegacyHistory: async () => {
4298
+ const result = await costMeter.importLegacyHistory()
4299
+ if (result === null || typeof result !== 'object' || result.ok !== true) {
4300
+ throw new Error(result?.error?.message ?? result?.value?.message ?? rpcT()('legacyImportFailed', { message: 'RPC failed' }))
4301
+ }
4302
+ if (result.value.state !== undefined) store.set({ status: 'ready', error: null, state: result.value.state })
4303
+ return result.value
4304
+ },
4231
4305
  // 按需拉取某天会话明细(issue #22),返回当日完整记录。
4232
4306
  getDaySessions: async date => call('getDaySessions', [date]),
4233
4307
  // 跨全部日期的会话排行(issue #22 不分日期视角):支持费用/时间升降序与实时顺序。
@@ -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
@@ -19,9 +19,9 @@ 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
21
  import { Ledger, applyConfigPatch, localDayKey, pickBalanceInfo, reconcileBalanceDelta, zeroDay } from './store.js'
22
- import { backfillLegacyLedger } from './backfill.js'
22
+ import { backfillLegacyLedger, importLegacyHistory } 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,10 @@ const SERVER_MESSAGES = {
65
65
  codingPlanDisabled: '{provider} 额度未启用,请先在面板中开启',
66
66
  codingPlanRefreshed: '{provider} 额度已刷新',
67
67
  codingPlanQueryFailed: '{provider} 额度查询失败:{message}',
68
+ scnetPlanCreditsInvalid: 'SCNet 月度 Credits 额度无效,请填写大于 0 的数值。',
69
+ legacyImportDone: '导入完成:更新 {days} 天、新增 {sessions} 个会话(扫描 {scanned} 份会话日志)。',
70
+ legacyImportNone: '没有可导入的安装前历史(扫描 {scanned} 份会话日志,缺失日期为空或已导入)。',
71
+ legacyImportFailed: '导入安装前历史失败:{message}',
68
72
  },
69
73
  en: {
70
74
  apiKeyMissing: 'DeepSeek API key not configured (configure it in Settings → Models, or export the {env} environment variable)',
@@ -101,6 +105,10 @@ const SERVER_MESSAGES = {
101
105
  codingPlanDisabled: '{provider} quota is disabled; enable it in the panel first',
102
106
  codingPlanRefreshed: '{provider} quota refreshed',
103
107
  codingPlanQueryFailed: '{provider} quota query failed: {message}',
108
+ scnetPlanCreditsInvalid: 'Invalid SCNet monthly credits quota; enter a value greater than 0.',
109
+ legacyImportDone: 'Import finished: {days} day(s) updated, {sessions} session(s) added (scanned {scanned} session logs).',
110
+ legacyImportNone: 'No pre-install history to import (scanned {scanned} session logs; missing dates are empty or already imported).',
111
+ legacyImportFailed: 'Failed to import pre-install history: {message}',
104
112
  },
105
113
  }
106
114
 
@@ -720,6 +728,18 @@ function createService(ctx, ledger) {
720
728
  codingPlanCaches[id] = { fetchedAt: Date.now(), value: emptyCodingPlan() }
721
729
  return
722
730
  }
731
+ // SCNet 无 API 额度端点(issue #26):按官方 Credits 抵扣表对本地账本同步估算——
732
+ // 纯本地计算开销可忽略,跳过缓存间隔,每次状态组装都随账本最新数据重算。
733
+ if (id === 'scnet') {
734
+ const result = scnetTokenPlanWindows(ledger.days ?? {}, config, Date.now())
735
+ codingPlanCaches[id] = {
736
+ fetchedAt: Date.now(),
737
+ value: result === null
738
+ ? { ...emptyCodingPlan(), status: 'off', fetchedAt: Date.now(), message: tmsg(localeOf(ledger.config), 'scnetPlanCreditsInvalid') }
739
+ : { status: 'ok', message: '', fetchedAt: Date.now(), windows: result.windows },
740
+ }
741
+ return
742
+ }
723
743
  const interval = Math.max(1, Number(config.refreshMinutes) || 15) * 60_000
724
744
  const cache = codingPlanCaches[id]
725
745
  if (!force && cache !== undefined && Date.now() - cache.fetchedAt < interval) return
@@ -903,6 +923,28 @@ function createService(ctx, ledger) {
903
923
  return build(false)
904
924
  },
905
925
 
926
+ // 导入安装前历史(issue #27):回放宿主全部会话日志,为账本缺失的日期
927
+ // 重建费用条目(幂等:已有日期只追加未知会话,绝不与实时计费重复)。
928
+ async importLegacyHistory() {
929
+ const locale = localeOf(ledger.config)
930
+ try {
931
+ const stats = await importLegacyHistory(ledger, join(resolveDshHome(), 'sessions'))
932
+ const message = stats.days === 0 && stats.sessions === 0
933
+ ? tmsg(locale, 'legacyImportNone', { scanned: stats.scanned })
934
+ : tmsg(locale, 'legacyImportDone', stats)
935
+ return {
936
+ ok: true,
937
+ message,
938
+ state: await build(false),
939
+ }
940
+ } catch (error) {
941
+ return {
942
+ ok: false,
943
+ message: tmsg(locale, 'legacyImportFailed', { message: error instanceof Error ? error.message : String(error) }),
944
+ }
945
+ }
946
+ },
947
+
906
948
  // 按需读取某一天的完整记录(含会话明细;issue #22):history() 输出为轻量副本
907
949
  // 不含会话,历史各天的会话明细由本 RPC 展开时才拉取,避免 state 膨胀。
908
950
  async getDaySessions(date) {
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 (!['anthropic', 'zai', 'minimax'].includes(id) || raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
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
@@ -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']),
@@ -339,6 +342,16 @@ export const TYPERT = {
339
342
  parameters: [],
340
343
  result: _state$codec,
341
344
  },
345
+ {
346
+ // 导入安装前历史(issue #27):回放全部会话日志,只补账本缺失的日期/会话。
347
+ id: 'dsh-cost-meter#costMeter/importLegacyHistory',
348
+ service: 'costMeter',
349
+ namespace: 'costMeter',
350
+ method: 'importLegacyHistory',
351
+ invocation: { kind: 'direct' },
352
+ parameters: [],
353
+ result: _fetch$codec,
354
+ },
342
355
  {
343
356
  id: 'dsh-cost-meter#costMeter/getDaySessions',
344
357
  service: 'costMeter',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.5.12",
4
- "description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(6 )、自定义 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 6 vendors, custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, in a bilingual (Chinese/English) UI.",
3
+ "version": "1.5.14",
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": {