From 6aeb5c53cf4c94af27d4077de40d7cc25cf2b2fe Mon Sep 17 00:00:00 2001 From: Miles <364475182@qq.com> Date: Tue, 8 Sep 2026 10:33:32 +0800 Subject: [PATCH 1/6] fix(host): release v1.1.4 (fix snapshotEvents compatibility with dsh-session >= 0.1.2-rc.1) - fix(host): adapt /usage route to use session.snapshotEvents() for dsh-session >= 0.1.2-rc.1 with fallback to legacy session.events (#7) - test: add coverage for snapshotEvents() preference and fallback under /usage - docs: update README and CHANGELOG to 1.1.4 - chore: bump version to 1.1.4 in package.json and package-lock.json --- CHANGELOG.md | 7 +++++++ CHANGELOG_ZH.md | 7 +++++++ README.md | 4 ++-- README_ZH.md | 6 +++--- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2fba94..288f456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/). 中文版见 [CHANGELOG_ZH.md](./CHANGELOG_ZH.md). +## [1.1.4] - 2026-09-08 + +### Fixed + +- **`dsh-session >= 0.1.2-rc.1` compatibility (snapshotEvents())** — [PR #7](https://github.com/Max-Samson/dsh-usage-chart/pull/7): `dsh-session` removed its public `events` property in `0.1.2-rc.1` (replaced by the `snapshotEvents()` method); the `/usage` route then threw `TypeError: events is not iterable`, which `dsh-host-webserver` wrapped as a bare `400` empty response, and the per-round chart disappeared (the client fell back to the empty observed-rounds path). Reproduced live on `dsh-session 0.1.2-rc.1`: `/usage?session=` returned an empty `400` while `/usage?session=` returned the plugin `404` JSON and folding the same log offline succeeded — confirming the crash was only the new runtime API shape. The host now reads events via capability detection — prefers `session.snapshotEvents()` when the function is present and falls back to the legacy `session.events ?? []` (so a missing API can never crash the route again). The vendored `SessionEventLike` type adds the `time` field (Unix epoch ms) and a `SessionHandle` shape (optional `events` + optional `snapshotEvents`) so `SessionStoreService.get()/list()` stay valid across both API generations. Verified against the upstream `deepseek-harness` source: `Session.events` was removed from `0.1.2-alpha.4` onward (official architecture note `2026-08-21-session-log-read-intent`); the installed `0.1.0-rc.6` still exposes `events`, so the fallback path remains covered. +- **Test coverage for the new `snapshotEvents()` access path**: added `tests/rounds.test.mjs` → `/usage route prefers snapshotEvents() when the new API is present`, mocking a session handle that exposes only `snapshotEvents()` (its `get events()` throws, proving the route never reads the removed property), asserts `time` is consumed (duration/peak billing) and that `snapshotEvents()` wins when both APIs are present. + ## [1.1.2] - 2026-08-26 ### Improved & Fixed diff --git a/CHANGELOG_ZH.md b/CHANGELOG_ZH.md index b4f5c49..cb8dd22 100644 --- a/CHANGELOG_ZH.md +++ b/CHANGELOG_ZH.md @@ -3,6 +3,13 @@ 本文件记录本项目所有重要变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。英文版见 [CHANGELOG.md](./CHANGELOG.md)。 +## [1.1.4] - 2026-09-08 + +### 修复 + +- **适配 `dsh-session >= 0.1.2-rc.1`(改用 `snapshotEvents()`)**——[PR #7](https://github.com/Max-Samson/dsh-usage-chart/pull/7):`dsh-session` 在 `0.1.2-rc.1` 移除了公开的 `events` 属性(改为 `snapshotEvents()` 方法);`/usage` 路由随之抛 `TypeError: events is not iterable`,被 `dsh-host-webserver` 包装成裸 `400` 空响应,每轮图表消失(客户端回退到空的 observed-rounds 路径)。已在 `dsh-session 0.1.2-rc.1` 上实测复现:`/usage?session=<已加载会话>` 返回空 `400`、`/usage?session=<未知>` 返回插件 `404` JSON、离线折叠同一日志却成功——证明崩溃仅源于新版运行时 API 形状。宿主现在通过能力检测读取事件——`snapshotEvents` 为函数时优先调用它,否则回退到旧版 `session.events ?? []`(即便 API 缺失也绝不再导致路由崩溃)。同时为 vendored 的 `SessionEventLike` 类型补齐 `time` 字段(Unix epoch 毫秒),并新增 `SessionHandle` 形状(可选 `events` + 可选 `snapshotEvents`),使 `SessionStoreService.get()/list()` 在新旧两代 API 下均成立。已对照上游 `deepseek-harness` 源码核实:`Session.events` 自 `0.1.2-alpha.4` 起移除(官方架构笔记 `2026-08-21-session-log-read-intent`);本机安装的 `0.1.0-rc.6` 仍暴露 `events`,因此回退分支持续有覆盖。 +- **为新的 `snapshotEvents()` 读取路径补充测试**:新增 `tests/rounds.test.mjs` → `/usage route prefers snapshotEvents() when the new API is present`,mock 一个只暴露 `snapshotEvents()` 的会话句柄(其 `get events()` 会抛错,证明路由绝不读取被移除的属性),断言 `time` 被正确消费(耗时/峰值计费),并验证同时暴露两者时优先使用 `snapshotEvents()`。 + ## [1.1.2] - 2026-08-26 ### 优化与修复 diff --git a/README.md b/README.md index d224a2c..bc3a3a6 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ To update (upgrade to a new version): pnpm may print `Already up to date` when a ```sh # Option ①: pin the target version explicitly -dsh plugin --profile web add dsh-usage-chart@1.1.2 +dsh plugin --profile web add dsh-usage-chart@1.1.4 # Option ②: remove, then re-add (back to latest) dsh plugin --profile web remove dsh-usage-chart dsh plugin --profile web add dsh-usage-chart @@ -278,7 +278,7 @@ dsh-usage-chart/ | Component | Supported | |---|---| -| DSH | ≥ 0.1.0-rc.6, built against the 0.1.x API | +| DSH | ≥ 0.1.0-rc.6, built against the 0.1.x API; adapts to the `dsh-session` `snapshotEvents()` API — `Session.events` was removed in `0.1.2-rc.1`, so the `/usage` route reads events via `snapshotEvents()` with a legacy `events` fallback | | Node.js | ≥ 20 | | Web UI | React 18 / `conversation.composer.dock` + `conversation.chat.assistant-actions` | | OS | macOS, Linux, Windows (pure JavaScript, no native dependencies) | diff --git a/README_ZH.md b/README_ZH.md index 1688e25..ab05fae 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -78,7 +78,7 @@ dsh web --profile web # 启动 DSH Web(已在运行 ```sh # 方式①:显式指定目标版本 -dsh plugin --profile web add dsh-usage-chart@1.1.2 +dsh plugin --profile web add dsh-usage-chart@1.1.4 # 方式②:先移除再重装(回到最新版) dsh plugin --profile web remove dsh-usage-chart dsh plugin --profile web add dsh-usage-chart @@ -91,7 +91,7 @@ dsh plugin --profile web add dsh-usage-chart > ⚠️ **未全局安装 dsh(报 `dsh: command not found` / PowerShell `无法将“dsh”项识别为…`)? > 把上面每条 `dsh` 都写成 `npx --yes @deepseek-ai/dsh`**,例如 -> `npx --yes @deepseek-ai/dsh plugin --profile web add dsh-usage-chart@1.1.2` +> `npx --yes @deepseek-ai/dsh plugin --profile web add dsh-usage-chart@1.1.4` > (原因与解法见 [FAQ](#常见问题faq) 第一条)。 ### 方式二:从 GitHub 安装(源码构建) @@ -331,7 +331,7 @@ dsh-usage-chart/ | 组件 | 支持范围 | |---|---| -| DSH | ≥ 0.1.0-rc.6,当前按 0.1.x API 构建 | +| DSH | ≥ 0.1.0-rc.6,当前按 0.1.x API 构建;已适配 `dsh-session` 的 `snapshotEvents()` API——`Session.events` 自 `0.1.2-rc.1` 起移除,`/usage` 路由通过 `snapshotEvents()` 读取事件,并保留旧版 `events` 属性回退 | | Node.js | ≥ 20 | | Web UI | React 18 / `conversation.composer.dock` + `conversation.chat.assistant-actions` | | 系统 | macOS、Linux、Windows(纯 JavaScript,无原生依赖) | diff --git a/package-lock.json b/package-lock.json index 5f7da2e..26d1a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dsh-usage-chart", - "version": "1.1.2", + "version": "1.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsh-usage-chart", - "version": "1.1.2", + "version": "1.1.4", "license": "MIT", "devDependencies": { "@deepseek-ai/cordis": "^4.0.1", diff --git a/package.json b/package.json index ff30dfa..79ac7df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-usage-chart", - "version": "1.1.2", + "version": "1.1.4", "description": "DeepSeek 用量 / 成本 / 余额仪表盘 for DSH Web:输入框下方实时指标指示器 + 零依赖 SVG 用量可视化图表(含每轮成本视角、耗时叠加、异常标记、成本徽章、上下文压力条),token 数据来自官方 adapter 上报,成本按官方刊例价 + 用户覆盖 pricing.json 解析,余额来自官方 /user/balance 接口。", "type": "module", "sideEffects": false, From 85a7e3f021bc6d0a89b520a9c8c825552f25dac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=9F=E6=B4=BB=E4=B8=8D=E6=98=93=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=8F=B9=E6=B0=94?= <121737713+ChthoIly@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:44:39 -0400 Subject: [PATCH 2/6] adj: add 4.1 flash and price change 0910 --- README.md | 12 +++++----- README_ZH.md | 12 +++++----- src/pricing/source.ts | 50 +++++++++++++++++++++--------------------- tests/core.test.mjs | 13 ++++++----- tests/pricing.test.mjs | 39 +++++++++++++++++--------------- tests/rounds.test.mjs | 38 ++++++++++++++++---------------- 6 files changed, 84 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index bc3a3a6..c7fef32 100644 --- a/README.md +++ b/README.md @@ -148,16 +148,16 @@ Costs are resolved with priority **user override file > builtin list price > fal ```json { - "deepseek-v4-flash": { + "deepseek-flash": { "offPeak": { - "cny": { "cacheMissInput": 1.5, "cacheHitInput": 0.05, "output": 4.5 }, - "usd": { "cacheMissInput": 0.22, "cacheHitInput": 0.007, "output": 0.66 } + "cny": { "cacheMissInput": 1, "cacheHitInput": 0.02, "output": 4 }, + "usd": { "cacheMissInput": 0.15, "cacheHitInput": 0.003, "output": 0.6 } }, "peak": { - "cny": { "cacheMissInput": 3.0, "cacheHitInput": 0.10, "output": 9.0 }, - "usd": { "cacheMissInput": 0.44, "cacheHitInput": 0.014, "output": 1.32 } + "cny": { "cacheMissInput": 2, "cacheHitInput": 0.04, "output": 8 }, + "usd": { "cacheMissInput": 0.3, "cacheHitInput": 0.006, "output": 1.2 } }, - "verifiedAt": 1755100800000 + "verifiedAt": 1788998400000 } } ``` diff --git a/README_ZH.md b/README_ZH.md index ab05fae..5968d4f 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -168,16 +168,16 @@ dsh web --profile web ```json { - "deepseek-v4-flash": { + "deepseek-flash": { "offPeak": { - "cny": { "cacheMissInput": 1.5, "cacheHitInput": 0.05, "output": 4.5 }, - "usd": { "cacheMissInput": 0.22, "cacheHitInput": 0.007, "output": 0.66 } + "cny": { "cacheMissInput": 1, "cacheHitInput": 0.02, "output": 4 }, + "usd": { "cacheMissInput": 0.15, "cacheHitInput": 0.003, "output": 0.6 } }, "peak": { - "cny": { "cacheMissInput": 3.0, "cacheHitInput": 0.10, "output": 9.0 }, - "usd": { "cacheMissInput": 0.44, "cacheHitInput": 0.014, "output": 1.32 } + "cny": { "cacheMissInput": 2, "cacheHitInput": 0.04, "output": 8 }, + "usd": { "cacheMissInput": 0.3, "cacheHitInput": 0.006, "output": 1.2 } }, - "verifiedAt": 1755100800000 + "verifiedAt": 1788998400000 } } ``` diff --git a/src/pricing/source.ts b/src/pricing/source.ts index 8794d9b..70af990 100644 --- a/src/pricing/source.ts +++ b/src/pricing/source.ts @@ -31,23 +31,25 @@ export interface PricingSource { /** * 当前官方在售模型定价表(双币种 / 1M tokens,区分高峰/空闲时段)。 - * 来源:官方定价页(2026-08-26 核验) + * 来源:官方定价页(2026-09-10 核验) * - 中文页 https://api-docs.deepseek.com/zh-cn/quick_start/pricing(CNY 报价) * - 英文页 https://api-docs.deepseek.com/quick_start/pricing(USD 报价) * 高峰时段(北京时间周一至周五 09:00–12:00、14:00–18:00 = UTC 01:00–04:00、06:00–10:00) * 价格为空闲时段的两倍。 */ -export const BUILTIN_PRICING: Record = { - 'deepseek-v4-flash': { - offPeak: { - cny: { cacheMissInput: 1.5, cacheHitInput: 0.05, output: 4.5 }, - usd: { cacheMissInput: 0.22, cacheHitInput: 0.007, output: 0.66 }, - }, - peak: { - cny: { cacheMissInput: 3.0, cacheHitInput: 0.10, output: 9.0 }, - usd: { cacheMissInput: 0.44, cacheHitInput: 0.014, output: 1.32 }, - }, +const FLASH_PRICING: ModelPricing = { + offPeak: { + cny: { cacheMissInput: 1, cacheHitInput: 0.02, output: 4 }, + usd: { cacheMissInput: 0.15, cacheHitInput: 0.003, output: 0.6 }, }, + peak: { + cny: { cacheMissInput: 2, cacheHitInput: 0.04, output: 8 }, + usd: { cacheMissInput: 0.3, cacheHitInput: 0.006, output: 1.2 }, + }, +} + +export const BUILTIN_PRICING: Record = { + 'deepseek-flash': FLASH_PRICING, 'deepseek-v4-pro': { offPeak: { cny: { cacheMissInput: 4.5, cacheHitInput: 0.15, output: 13.5 }, @@ -58,23 +60,21 @@ export const BUILTIN_PRICING: Record = { usd: { cacheMissInput: 1.32, cacheHitInput: 0.044, output: 3.96 }, }, }, - 'deepseek-v4-flash-vision-exp': { - offPeak: { - cny: { cacheMissInput: 1.5, cacheHitInput: 0.05, output: 4.5 }, - usd: { cacheMissInput: 0.22, cacheHitInput: 0.007, output: 0.66 }, - }, - peak: { - cny: { cacheMissInput: 3.0, cacheHitInput: 0.10, output: 9.0 }, - usd: { cacheMissInput: 0.44, cacheHitInput: 0.014, output: 1.32 }, - }, - }, } -/** 内置表的核验日期(来源:官方定价页中/英文版,2026-08-26 抓取核验)。 */ -export const BUILTIN_VERIFIED_AT = Date.parse('2026-08-26T00:00:00Z') +/** + * 旧模型名兼容: + * - `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` 已并入 `deepseek-flash`, + * 仍按同一价格表解析,避免历史会话突然变成「未定价模型」。 + */ +BUILTIN_PRICING['deepseek-v4-flash'] = FLASH_PRICING +BUILTIN_PRICING['deepseek-v4-flash-vision-exp'] = FLASH_PRICING + +/** 内置表的核验日期(来源:官方定价页中/英文版,2026-09-10 抓取核验)。 */ +export const BUILTIN_VERIFIED_AT = Date.parse('2026-09-10T00:00:00Z') -/** 未收录模型回退:按 deepseek-v4-flash 刊例价估算并标记 ≈。 */ -export const FALLBACK_PRICING: ModelPricing = BUILTIN_PRICING['deepseek-v4-flash'] +/** 未收录模型回退:按 deepseek-flash 刊例价估算并标记 ≈。 */ +export const FALLBACK_PRICING: ModelPricing = FLASH_PRICING /** 前缀匹配(如带日期后缀的模型版本);精确匹配优先。 */ function matchEntry(table: Record, model: string): T | null { diff --git a/tests/core.test.mjs b/tests/core.test.mjs index 2e69469..00ca057 100644 --- a/tests/core.test.mjs +++ b/tests/core.test.mjs @@ -72,17 +72,18 @@ test('foldTurnUsage ignores malformed or negative usage samples', () => { }) test('pricing matches the documented DeepSeek V4 prices (CNY + USD, peak/off-peak)', () => { - assert.deepEqual(PRICING['deepseek-v4-flash'], { + assert.deepEqual(PRICING['deepseek-flash'], { offPeak: { - cny: { cacheMissInput: 1.5, cacheHitInput: 0.05, output: 4.5 }, - usd: { cacheMissInput: 0.22, cacheHitInput: 0.007, output: 0.66 }, + cny: { cacheMissInput: 1, cacheHitInput: 0.02, output: 4 }, + usd: { cacheMissInput: 0.15, cacheHitInput: 0.003, output: 0.6 }, }, peak: { - cny: { cacheMissInput: 3.0, cacheHitInput: 0.10, output: 9.0 }, - usd: { cacheMissInput: 0.44, cacheHitInput: 0.014, output: 1.32 }, + cny: { cacheMissInput: 2, cacheHitInput: 0.04, output: 8 }, + usd: { cacheMissInput: 0.3, cacheHitInput: 0.006, output: 1.2 }, }, }) - assert.deepEqual(PRICING['deepseek-v4-flash-vision-exp'], PRICING['deepseek-v4-flash']) + assert.deepEqual(PRICING['deepseek-v4-flash'], PRICING['deepseek-flash']) + assert.deepEqual(PRICING['deepseek-v4-flash-vision-exp'], PRICING['deepseek-flash']) assert.equal(cacheHitPercent({ uncachedInputTokens: 50, outputTokens: 0, cacheReadTokens: 50, cacheWriteTokens: 0 }), 50) // 时刻未知 → 高峰价保守估算:1M 未命中输入 + 1M 输出(v4-pro 高峰 CNY = 9 + 27) assert.equal(estimateCost({ uncachedInputTokens: 1_000_000, outputTokens: 1_000_000, cacheReadTokens: 0, cacheWriteTokens: 0 }, 'deepseek-v4-pro').cny, 36) diff --git a/tests/pricing.test.mjs b/tests/pricing.test.mjs index ca02855..5772038 100644 --- a/tests/pricing.test.mjs +++ b/tests/pricing.test.mjs @@ -19,14 +19,14 @@ import { tierAt, } from '../lib/index.js' -/** 2026-08-26 内置刊例价(双币种 /1M,高峰/空闲)。 */ +/** 2026-09-10 内置刊例价(双币种 /1M,高峰/空闲)。 */ const FLASH_OFF_PEAK = { - cny: { cacheMissInput: 1.5, cacheHitInput: 0.05, output: 4.5 }, - usd: { cacheMissInput: 0.22, cacheHitInput: 0.007, output: 0.66 }, + cny: { cacheMissInput: 1, cacheHitInput: 0.02, output: 4 }, + usd: { cacheMissInput: 0.15, cacheHitInput: 0.003, output: 0.6 }, } const FLASH_PEAK = { - cny: { cacheMissInput: 3.0, cacheHitInput: 0.10, output: 9.0 }, - usd: { cacheMissInput: 0.44, cacheHitInput: 0.014, output: 1.32 }, + cny: { cacheMissInput: 2, cacheHitInput: 0.04, output: 8 }, + usd: { cacheMissInput: 0.3, cacheHitInput: 0.006, output: 1.2 }, } const PRO_OFF_PEAK = { cny: { cacheMissInput: 4.5, cacheHitInput: 0.15, output: 13.5 }, @@ -42,9 +42,10 @@ test('builtin source resolves exact and prefixed models, misses unknown', () => assert.deepEqual(source.resolve('deepseek-v4-pro').pricing.offPeak, PRO_OFF_PEAK) assert.deepEqual(source.resolve('deepseek-v4-pro').pricing.peak, PRO_PEAK) // 前缀匹配(带日期后缀的模型版本) - assert.equal(source.resolve('deepseek-v4-flash-2026-08-01').pricing.offPeak.cny.output, 4.5) - assert.equal(source.resolve('deepseek-v4-flash-2026-08-01').pricing.offPeak.usd.output, 0.66) - assert.equal(source.resolve('deepseek-v4-flash-2026-08-01').pricing.peak.usd.output, 1.32) + assert.equal(source.resolve('deepseek-flash-2026-09-01').pricing.offPeak.cny.output, 4) + assert.equal(source.resolve('deepseek-flash-2026-09-01').pricing.offPeak.usd.output, 0.6) + assert.equal(source.resolve('deepseek-flash-2026-09-01').pricing.peak.usd.output, 1.2) + assert.deepEqual(source.resolve('deepseek-flash').pricing, { offPeak: FLASH_OFF_PEAK, peak: FLASH_PEAK }) assert.deepEqual(source.resolve('deepseek-v4-flash-vision-exp').pricing.offPeak, FLASH_OFF_PEAK) assert.deepEqual(source.resolve('deepseek-v4-flash-vision-exp').pricing.peak, FLASH_PEAK) assert.equal(source.resolve('unknown-model').pricing, null) @@ -106,12 +107,12 @@ test('costSplitAt bills in both official currencies per tier', () => { const usage = { uncachedInputTokens: 1_000_000, outputTokens: 1_000_000, cacheReadTokens: 0, cacheWriteTokens: 0 } const offPeakAt = Date.UTC(1970, 0, 1, 0, 0, 0) // 北京时间 08:00 const peakAt = Date.UTC(1970, 0, 1, 2, 0, 0) // 北京时间 10:00 - // 空闲:flash 未命中 1.5 + 输出 4.5(CNY);0.22 + 0.66(USD) - assert.deepEqual(costSplitAt(usage, PRICING['deepseek-v4-flash'], offPeakAt, 'cny'), { input: 1.5, cacheRead: 0, output: 4.5, total: 6 }) - assert.deepEqual(costSplitAt(usage, PRICING['deepseek-v4-flash'], offPeakAt, 'usd'), { input: 0.22, cacheRead: 0, output: 0.66, total: 0.88 }) + // 空闲:flash 未命中 1 + 输出 4(CNY);0.15 + 0.6(USD) + assert.deepEqual(costSplitAt(usage, PRICING['deepseek-flash'], offPeakAt, 'cny'), { input: 1, cacheRead: 0, output: 4, total: 5 }) + assert.deepEqual(costSplitAt(usage, PRICING['deepseek-flash'], offPeakAt, 'usd'), { input: 0.15, cacheRead: 0, output: 0.6, total: 0.75 }) // 高峰 = 空闲 × 2 - assert.deepEqual(costSplitAt(usage, PRICING['deepseek-v4-flash'], peakAt, 'cny'), { input: 3, cacheRead: 0, output: 9, total: 12 }) - assert.deepEqual(costSplitAt(usage, PRICING['deepseek-v4-flash'], peakAt, 'usd'), { input: 0.44, cacheRead: 0, output: 1.32, total: 1.76 }) + assert.deepEqual(costSplitAt(usage, PRICING['deepseek-flash'], peakAt, 'cny'), { input: 2, cacheRead: 0, output: 8, total: 10 }) + assert.deepEqual(costSplitAt(usage, PRICING['deepseek-flash'], peakAt, 'usd'), { input: 0.3, cacheRead: 0, output: 1.2, total: 1.5 }) }) test('parsePricingFile validates entries and accepts dual-currency + legacy shapes', () => { @@ -239,12 +240,14 @@ test('file source serves prefix matches and refresh after rewrite', async () => }) test('estimateCost / pricingFor keep v0.1 compat semantics', () => { - assert.deepEqual(PRICING['deepseek-v4-flash'], { peak: FLASH_PEAK, offPeak: FLASH_OFF_PEAK }) + assert.deepEqual(PRICING['deepseek-flash'], { peak: FLASH_PEAK, offPeak: FLASH_OFF_PEAK }) + assert.deepEqual(PRICING['deepseek-v4-flash'], PRICING['deepseek-flash']) // 时刻未知 → 按高峰价保守估算:1M 未命中输入 + 1M 输出(v4-pro 高峰 CNY = 9 + 27) assert.equal(estimateCost({ uncachedInputTokens: 1_000_000, outputTokens: 1_000_000, cacheReadTokens: 0, cacheWriteTokens: 0 }, 'deepseek-v4-pro').cny, 36) + assert.equal(pricingFor('deepseek-flash').estimated, false) assert.equal(pricingFor('deepseek-v4-flash').estimated, false) assert.equal(pricingFor('unknown-model').estimated, true) - assert.equal(BUILTIN_VERIFIED_AT, Date.parse('2026-08-26T00:00:00Z')) + assert.equal(BUILTIN_VERIFIED_AT, Date.parse('2026-09-10T00:00:00Z')) }) function responseRecorder() { @@ -274,9 +277,9 @@ test('/pricing route exposes builtin + fallback + models snapshot', async () => const body = JSON.parse(recorder.body) assert.equal(body.ok, true) assert.equal(body.builtinVerifiedAt, BUILTIN_VERIFIED_AT) - assert.equal(body.fallback.pricing.offPeak.cny.output, 4.5) - assert.equal(body.fallback.pricing.offPeak.usd.output, 0.66) - assert.equal(body.fallback.pricing.peak.usd.output, 1.32) + assert.equal(body.fallback.pricing.offPeak.cny.output, 4) + assert.equal(body.fallback.pricing.offPeak.usd.output, 0.6) + assert.equal(body.fallback.pricing.peak.usd.output, 1.2) const models = body.models.map((m) => m.model) assert.ok(models.includes('deepseek-v4-flash')) assert.ok(models.includes('deepseek-v4-pro')) diff --git a/tests/rounds.test.mjs b/tests/rounds.test.mjs index 1aa05a9..3b57d17 100644 --- a/tests/rounds.test.mjs +++ b/tests/rounds.test.mjs @@ -3,14 +3,14 @@ import test from 'node:test' import { apply, foldRounds, foldTurnUsage, tierAt } from '../lib/index.js' -/** 2026-08-17 内置刊例价(双币种 /1M):flash 高峰/空闲。 */ +/** 2026-09-10 内置刊例价(双币种 /1M):flash 高峰/空闲。 */ const FLASH_PEAK = { - cny: { cacheMissInput: 3.0, cacheHitInput: 0.10, output: 9.0 }, - usd: { cacheMissInput: 0.44, cacheHitInput: 0.014, output: 1.32 }, + cny: { cacheMissInput: 2, cacheHitInput: 0.04, output: 8 }, + usd: { cacheMissInput: 0.3, cacheHitInput: 0.006, output: 1.2 }, } const FLASH_OFF_PEAK = { - cny: { cacheMissInput: 1.5, cacheHitInput: 0.05, output: 4.5 }, - usd: { cacheMissInput: 0.22, cacheHitInput: 0.007, output: 0.66 }, + cny: { cacheMissInput: 1, cacheHitInput: 0.02, output: 4 }, + usd: { cacheMissInput: 0.15, cacheHitInput: 0.003, output: 0.6 }, } /** 合成事件流工具:seq 自动递增,time 由调用方给定。 */ @@ -68,12 +68,12 @@ test('foldRounds computes per-round timing, TTFT, TPS, model attribution and cos assert.equal(r1.cost.estimated, false) // 轮 1 开始于 1970-01-01T00:00:01Z = 北京时间 08:00(空闲时段)→ 空闲价 assert.equal(tierAt(r1.startedAt), 'offPeak') - assert.equal(r1.cost.cny.input, (100 / 1_000_000) * 1.5) - assert.equal(r1.cost.cny.cacheRead, (30 / 1_000_000) * 0.05) - assert.equal(r1.cost.cny.output, (20 / 1_000_000) * 4.5) - assert.equal(r1.cost.usd.input, (100 / 1_000_000) * 0.22) - assert.equal(r1.cost.usd.cacheRead, (30 / 1_000_000) * 0.007) - assert.equal(r1.cost.usd.output, (20 / 1_000_000) * 0.66) + assert.equal(r1.cost.cny.input, (100 / 1_000_000) * 1) + assert.equal(r1.cost.cny.cacheRead, (30 / 1_000_000) * 0.02) + assert.equal(r1.cost.cny.output, (20 / 1_000_000) * 4) + assert.equal(r1.cost.usd.input, (100 / 1_000_000) * 0.15) + assert.equal(r1.cost.usd.cacheRead, (30 / 1_000_000) * 0.003) + assert.equal(r1.cost.usd.output, (20 / 1_000_000) * 0.6) const r2 = rounds.find((r) => r.turn === 2) assert.ok(r2, 'round 2 present') @@ -101,11 +101,11 @@ test('foldRounds bills peak vs off-peak rounds by their start time', () => { const offPeak = rounds.find((r) => r.turn === 2) assert.equal(tierAt(peak.startedAt), 'peak') assert.equal(tierAt(offPeak.startedAt), 'offPeak') - // 1M 未命中输入:高峰 = 空闲 × 2(CNY 3.0/1.5;USD 0.44/0.22) - assert.equal(peak.cost.cny.total, 3.0) - assert.equal(peak.cost.usd.total, 0.44) - assert.equal(offPeak.cost.cny.total, 1.5) - assert.equal(offPeak.cost.usd.total, 0.22) + // 1M 未命中输入:高峰 = 空闲 × 2(CNY 2/1;USD 0.3/0.15) + assert.equal(peak.cost.cny.total, 2) + assert.equal(peak.cost.usd.total, 0.3) + assert.equal(offPeak.cost.cny.total, 1) + assert.equal(offPeak.cost.usd.total, 0.15) }) test('foldRounds tolerates missing turn/end (open round) and missing model', () => { @@ -150,9 +150,9 @@ test('foldRounds marks unknown models and uses fallback pricing', () => { assert.equal(rounds[0].cost.unknownModel, true) assert.equal(rounds[0].cost.estimated, true) assert.equal(rounds[0].cost.source, 'fallback') - // 轮开始于北京时间 08:00(空闲)→ 1M token × flash 空闲未命中价(CNY 1.5 / USD 0.22) - assert.equal(rounds[0].cost.cny.total, 1.5) - assert.equal(rounds[0].cost.usd.total, 0.22) + // 轮开始于北京时间 08:00(空闲)→ 1M token × flash 空闲未命中价(CNY 1 / USD 0.15) + assert.equal(rounds[0].cost.cny.total, 1) + assert.equal(rounds[0].cost.usd.total, 0.15) }) test('foldTurnUsage stays compatible with the v0.1 semantics', () => { From ee97a901da26573976b5d08f50f95bd5255bff93 Mon Sep 17 00:00:00 2001 From: Miles <364475182@qq.com> Date: Sat, 12 Sep 2026 01:29:58 +0800 Subject: [PATCH 3/6] chore: tag v1.1.5 --- CHANGELOG.md | 12 ++++++++++++ CHANGELOG_ZH.md | 12 ++++++++++++ README.md | 2 +- README_ZH.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- tests/pricing.test.mjs | 1 + 7 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 288f456..3e3e63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/). 中文版见 [CHANGELOG_ZH.md](./CHANGELOG_ZH.md). +## [1.1.5] - 2026-09-12 + +### Changed + +- **DeepSeek official pricing update (2026-09-10) & `deepseek-flash` support** — [PR #8](https://github.com/Max-Samson/dsh-usage-chart/pull/8): + - **New `deepseek-flash` model pricing**: Updated builtin pricing according to the official DeepSeek pricing table (verified 2026-09-10). The new `deepseek-flash` rates are: + - **Off-peak** (Beijing Mon–Fri non-peak & weekends): CNY 1.0 / 0.02 / 4.0 per 1M tokens (cache-miss input / cache-hit input / output); USD 0.15 / 0.003 / 0.6 per 1M tokens. + - **Peak** (Beijing Mon–Fri 09:00–12:00 & 14:00–18:00): CNY 2.0 / 0.04 / 8.0 per 1M tokens; USD 0.30 / 0.006 / 1.2 per 1M tokens. + - **Backward compatibility for legacy model names**: `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` are mapped to `deepseek-flash` in `BUILTIN_PRICING` so historical session logs continue resolving without falling back to unknown-model estimates. + - **Fallback pricing**: Default unlisted model fallback (`FALLBACK_PRICING`) now points to the new `deepseek-flash` pricing. + - **Docs & test synchronization**: Updated custom `pricing.json` example in `README.md` and `README_ZH.md` (`verifiedAt: 1788998400000`), and aligned test cases across `tests/core.test.mjs`, `tests/pricing.test.mjs`, and `tests/rounds.test.mjs` (`BUILTIN_VERIFIED_AT` set to `2026-09-10T00:00:00Z`). + ## [1.1.4] - 2026-09-08 ### Fixed diff --git a/CHANGELOG_ZH.md b/CHANGELOG_ZH.md index cb8dd22..774017d 100644 --- a/CHANGELOG_ZH.md +++ b/CHANGELOG_ZH.md @@ -3,6 +3,18 @@ 本文件记录本项目所有重要变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。英文版见 [CHANGELOG.md](./CHANGELOG.md)。 +## [1.1.5] - 2026-09-12 + +### 变更与优化 + +- **同步 DeepSeek 官方最新调价(2026-09-10)与 `deepseek-flash` 适配**——[PR #8](https://github.com/Max-Samson/dsh-usage-chart/pull/8): + - **新增 `deepseek-flash` 官方刊例价**:按 DeepSeek 官方 2026-09-10 最新调价更新内置定价表,`deepseek-flash` 刊例价全面下调: + - **空闲时段**(北京时间工作日非高峰与周末全天):人民币 CNY 1.0 / 0.02 / 4.0 每 1M tokens(未命中输入 / 命中输入 / 输出);美元 USD 0.15 / 0.003 / 0.6 每 1M tokens。 + - **高峰时段**(北京时间工作日 09:00–12:00 与 14:00–18:00):人民币 CNY 2.0 / 0.04 / 8.0 每 1M tokens;美元 USD 0.30 / 0.006 / 1.2 每 1M tokens。 + - **历史模型标识平滑兼容**:将 `deepseek-v4-flash` 与 `deepseek-v4-flash-vision-exp` 指向 `deepseek-flash` 价格表,确保历史会话在重新加载与折叠时平滑解析,不被判定为未定价模型。 + - **未收录模型回退**:未收录模型的默认保守估算回退(`FALLBACK_PRICING`)同步切换至新版 `deepseek-flash` 刊例价。 + - **文档与测试同步**:更新 README 中的 `pricing.json` 自定义配置示例与核验时间戳(`verifiedAt: 1788998400000` 即 2026-09-10);全量同步 core、pricing、rounds 单测断言与 `BUILTIN_VERIFIED_AT`。 + ## [1.1.4] - 2026-09-08 ### 修复 diff --git a/README.md b/README.md index c7fef32..5ae1ec2 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ To update (upgrade to a new version): pnpm may print `Already up to date` when a ```sh # Option ①: pin the target version explicitly -dsh plugin --profile web add dsh-usage-chart@1.1.4 +dsh plugin --profile web add dsh-usage-chart@1.1.5 # Option ②: remove, then re-add (back to latest) dsh plugin --profile web remove dsh-usage-chart dsh plugin --profile web add dsh-usage-chart diff --git a/README_ZH.md b/README_ZH.md index 5968d4f..7133e12 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -78,7 +78,7 @@ dsh web --profile web # 启动 DSH Web(已在运行 ```sh # 方式①:显式指定目标版本 -dsh plugin --profile web add dsh-usage-chart@1.1.4 +dsh plugin --profile web add dsh-usage-chart@1.1.5 # 方式②:先移除再重装(回到最新版) dsh plugin --profile web remove dsh-usage-chart dsh plugin --profile web add dsh-usage-chart @@ -91,7 +91,7 @@ dsh plugin --profile web add dsh-usage-chart > ⚠️ **未全局安装 dsh(报 `dsh: command not found` / PowerShell `无法将“dsh”项识别为…`)? > 把上面每条 `dsh` 都写成 `npx --yes @deepseek-ai/dsh`**,例如 -> `npx --yes @deepseek-ai/dsh plugin --profile web add dsh-usage-chart@1.1.4` +> `npx --yes @deepseek-ai/dsh plugin --profile web add dsh-usage-chart@1.1.5` > (原因与解法见 [FAQ](#常见问题faq) 第一条)。 ### 方式二:从 GitHub 安装(源码构建) diff --git a/package-lock.json b/package-lock.json index 26d1a04..dcea77b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dsh-usage-chart", - "version": "1.1.4", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsh-usage-chart", - "version": "1.1.4", + "version": "1.1.5", "license": "MIT", "devDependencies": { "@deepseek-ai/cordis": "^4.0.1", diff --git a/package.json b/package.json index 79ac7df..9d7a47f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-usage-chart", - "version": "1.1.4", + "version": "1.1.5", "description": "DeepSeek 用量 / 成本 / 余额仪表盘 for DSH Web:输入框下方实时指标指示器 + 零依赖 SVG 用量可视化图表(含每轮成本视角、耗时叠加、异常标记、成本徽章、上下文压力条),token 数据来自官方 adapter 上报,成本按官方刊例价 + 用户覆盖 pricing.json 解析,余额来自官方 /user/balance 接口。", "type": "module", "sideEffects": false, diff --git a/tests/pricing.test.mjs b/tests/pricing.test.mjs index 5772038..f3201f0 100644 --- a/tests/pricing.test.mjs +++ b/tests/pricing.test.mjs @@ -281,6 +281,7 @@ test('/pricing route exposes builtin + fallback + models snapshot', async () => assert.equal(body.fallback.pricing.offPeak.usd.output, 0.6) assert.equal(body.fallback.pricing.peak.usd.output, 1.2) const models = body.models.map((m) => m.model) + assert.ok(models.includes('deepseek-flash')) assert.ok(models.includes('deepseek-v4-flash')) assert.ok(models.includes('deepseek-v4-pro')) assert.ok(models.includes('deepseek-v4-flash-vision-exp')) From 4a94ceff494cb79238817bcc155c84ef13bdc7c8 Mon Sep 17 00:00:00 2001 From: rezon-aki Date: Fri, 18 Sep 2026 23:46:48 +0800 Subject: [PATCH 4/6] fix(host): release the pricing file source with the fiber, not at apply time ctx.effect(() => fileSource.dispose(), ...) calls dispose while the fiber is being built and registers undefined as the disposer: the file source is dead before its first read, so pricing.json is never applied and the change watcher never fires. Return a disposer instead. Test mocks now follow cordis semantics (run setup, keep the returned disposer, release it in after()) and a /pricing route regression test covers the override path - it fails against the old form. --- src/index.ts | 3 ++- tests/core.test.mjs | 10 ++++++--- tests/pricing.test.mjs | 46 ++++++++++++++++++++++++++++++++++++++++-- tests/rounds.test.mjs | 13 ++++++++---- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5ef5eb6..eee6186 100644 --- a/src/index.ts +++ b/src/index.ts @@ -364,7 +364,8 @@ export function apply(ctx: Context, config: Config = {}): void { const pricingFile = config.pricingFile?.trim() || defaultPricingFile() const fileSource = filePricingSource(pricingFile) const resolver = createPricingResolver(fileSource) - ctx.effect(() => fileSource.dispose(), 'dsh-usage-chart: pricing file watcher') + // 注意返回的是「返回 disposer 的函数」:写成 () => fileSource.dispose() 会在注册时立刻销毁文件源。 + ctx.effect(() => () => fileSource.dispose(), 'dsh-usage-chart: pricing file watcher') ctx.effect(() => ctx.webServer.register({ kind: 'exact', diff --git a/tests/core.test.mjs b/tests/core.test.mjs index 00ca057..30d3b1e 100644 --- a/tests/core.test.mjs +++ b/tests/core.test.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict' -import test from 'node:test' +import test, { after } from 'node:test' import { DEFAULT_CNY_PER_USD, @@ -16,6 +16,10 @@ import { normalizeCurrency, } from '../lib/index.js' +/** cordis 语义:effect(setup) 立即执行 setup,把返回的 disposer 留到 fiber 销毁时再调用。 */ +const disposers = [] +after(() => { for (const dispose of disposers) if (typeof dispose === 'function') dispose() }) + function responseRecorder() { return { headers: {}, @@ -30,7 +34,7 @@ function responseRecorder() { function mountedRoutes(events = [], config = {}) { const routes = new Map() apply({ - effect(setup) { setup() }, + effect(setup) { disposers.push(setup()) }, get() { return undefined }, webServer: { register(route) { routes.set(route.path, route); return () => {} }, @@ -126,7 +130,7 @@ test('mounted Host routes enforce method and browser-origin guards', async () => async function balanceResponse(extraCtx = {}, config = {}) { const routes = new Map() apply({ - effect(setup) { setup() }, + effect(setup) { disposers.push(setup()) }, get(name) { return name === 'credentials' ? extraCtx.credentials : undefined }, webServer: { register(route) { routes.set(route.path, route); return () => {} } }, sessions: { get() { return undefined } }, diff --git a/tests/pricing.test.mjs b/tests/pricing.test.mjs index f3201f0..e1f9cbf 100644 --- a/tests/pricing.test.mjs +++ b/tests/pricing.test.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { mkdtemp, writeFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import test from 'node:test' +import test, { after } from 'node:test' import { BUILTIN_VERIFIED_AT, @@ -19,6 +19,10 @@ import { tierAt, } from '../lib/index.js' +/** cordis 语义:effect(setup) 立即执行 setup,把返回的 disposer 留到 fiber 销毁时再调用。 */ +const disposers = [] +after(() => { for (const dispose of disposers) if (typeof dispose === 'function') dispose() }) + /** 2026-09-10 内置刊例价(双币种 /1M,高峰/空闲)。 */ const FLASH_OFF_PEAK = { cny: { cacheMissInput: 1, cacheHitInput: 0.02, output: 4 }, @@ -264,7 +268,7 @@ function responseRecorder() { test('/pricing route exposes builtin + fallback + models snapshot', async () => { const routes = new Map() apply({ - effect(setup) { setup() }, + effect(setup) { disposers.push(setup()) }, get() { return undefined }, webServer: { register(route) { routes.set(route.path, route); return () => {} } }, sessions: { get() { return undefined } }, @@ -286,3 +290,41 @@ test('/pricing route exposes builtin + fallback + models snapshot', async () => assert.ok(models.includes('deepseek-v4-pro')) assert.ok(models.includes('deepseek-v4-flash-vision-exp')) }) + +test('/pricing route serves the user pricing.json override (file source survives startup)', async () => { + const dir = await mkdtemp(join(tmpdir(), 'duc-pricing-')) + const pricingFile = join(dir, 'pricing.json') + await writeFile(pricingFile, JSON.stringify({ + models: { + 'deepseek-flash': { + peak: { cny: { cacheMissInput: 19.99, cacheHitInput: 0.04, output: 8 }, usd: { cacheMissInput: 2.99, cacheHitInput: 0.006, output: 1.2 } }, + offPeak: { cny: { cacheMissInput: 9.99, cacheHitInput: 0.02, output: 4 }, usd: { cacheMissInput: 1.49, cacheHitInput: 0.003, output: 0.6 } }, + }, + }, + })) + const routes = new Map() + const disposers = [] + try { + apply({ + // cordis 语义:effect(setup) 立即执行 setup,把返回的 disposer 留到 fiber 销毁时才调用。 + // 写成 () => source.dispose() 会在这里当场销毁文件源 → 覆盖文件永不生效(本测试即为此回归)。 + effect(setup) { disposers.push(setup()) }, + get() { return undefined }, + webServer: { register(route) { routes.set(route.path, route); return () => {} } }, + sessions: { get() { return undefined } }, + }, { pricingFile }) + await new Promise((resolve) => setTimeout(resolve, 50)) + + const recorder = responseRecorder() + await routes.get('/dsh-usage-chart/pricing').handler( + { method: 'GET', url: '/dsh-usage-chart/pricing', headers: { host: 'localhost:3000' } }, + recorder, + ) + const flash = JSON.parse(recorder.body).models.find((m) => m.model === 'deepseek-flash') + assert.equal(flash.source, 'file') + assert.equal(flash.pricing.offPeak.cny.cacheMissInput, 9.99) + } finally { + for (const dispose of disposers) if (typeof dispose === 'function') dispose() + await rm(dir, { recursive: true, force: true }) + } +}) diff --git a/tests/rounds.test.mjs b/tests/rounds.test.mjs index 3b57d17..3308751 100644 --- a/tests/rounds.test.mjs +++ b/tests/rounds.test.mjs @@ -1,8 +1,13 @@ import assert from 'node:assert/strict' -import test from 'node:test' +import test, { after } from 'node:test' import { apply, foldRounds, foldTurnUsage, tierAt } from '../lib/index.js' +/** cordis 语义:effect(setup) 立即执行 setup,把返回的 disposer 留到 fiber 销毁时再调用。 */ +const disposers = [] +after(() => { for (const dispose of disposers) if (typeof dispose === 'function') dispose() }) + + /** 2026-09-10 内置刊例价(双币种 /1M):flash 高峰/空闲。 */ const FLASH_PEAK = { cny: { cacheMissInput: 2, cacheHitInput: 0.04, output: 8 }, @@ -206,7 +211,7 @@ test('/usage route returns rounds with cost and timing', async () => { ] const routes = new Map() apply({ - effect(setup) { setup() }, + effect(setup) { disposers.push(setup()) }, get() { return undefined }, webServer: { register(route) { routes.set(route.path, route); return () => {} } }, sessions: { get(id) { return id === 'session-test' ? { id, events } : undefined } }, @@ -247,7 +252,7 @@ test('/usage route prefers snapshotEvents() when the new API is present', async ] const routes = new Map() apply({ - effect(setup) { setup() }, + effect(setup) { disposers.push(setup()) }, get() { return undefined }, webServer: { register(route) { routes.set(route.path, route); return () => {} } }, // 有 snapshotEvents、无 events —— 必须走 snapshotEvents() 分支,且绝不触碰 events 属性。 @@ -273,7 +278,7 @@ test('/usage route prefers snapshotEvents() when the new API is present', async const both = { id: 'session-test', events: [], snapshotEvents: () => events } const bothRoutes = new Map() apply({ - effect(setup) { setup() }, + effect(setup) { disposers.push(setup()) }, get() { return undefined }, webServer: { register(route) { bothRoutes.set(route.path, route); return () => {} } }, sessions: { get(id) { return id === 'session-test' ? both : undefined } }, From 41391c2182930636aa552b06e7111ecf51593d97 Mon Sep 17 00:00:00 2001 From: Miles <364475182@qq.com> Date: Sat, 19 Sep 2026 13:15:20 +0800 Subject: [PATCH 5/6] chore: tag 1.1.6 --- CHANGELOG.md | 9 +++++++++ CHANGELOG_ZH.md | 9 +++++++++ README.md | 2 +- README_ZH.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3e63a..c4628bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/). 中文版见 [CHANGELOG_ZH.md](./CHANGELOG_ZH.md). +## [1.1.6] - 2026-09-19 + +### Fixed + +- **Host pricing file source lifecycle (`pricing.json` override & change watcher)** — [PR #10](https://github.com/Max-Samson/dsh-usage-chart/pull/10): + - Fixed premature disposal of `filePricingSource` during plugin registration: `ctx.effect(() => fileSource.dispose(), ...)` called `dispose()` synchronously while building the fiber and registered `undefined` as the cleanup handler, aborting initial asynchronous file loading and disabling the file change watcher. Resolved by returning a disposer function: `ctx.effect(() => () => fileSource.dispose(), ...)`. + - Updated test mocks in `tests/core.test.mjs`, `tests/pricing.test.mjs`, and `tests/rounds.test.mjs` to properly track and execute Cordis effect disposers in `after()`. + - Added regression test covering the `/pricing` route with user `pricing.json` overrides enabled at startup. + ## [1.1.5] - 2026-09-12 ### Changed diff --git a/CHANGELOG_ZH.md b/CHANGELOG_ZH.md index 774017d..60d43b4 100644 --- a/CHANGELOG_ZH.md +++ b/CHANGELOG_ZH.md @@ -2,6 +2,15 @@ 本文件记录本项目所有重要变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。英文版见 [CHANGELOG.md](./CHANGELOG.md)。 +## [1.1.6] - 2026-09-19 + +### 修复 + +- **宿主自定义价格文件源生命周期早释问题(`pricing.json` 覆盖与变更监听)**——[PR #10](https://github.com/Max-Samson/dsh-usage-chart/pull/10): + - **修复 `filePricingSource` 启动即被销毁的 Bug**:此前 `ctx.effect(() => fileSource.dispose(), ...)` 在挂载期立即执行了清理调用,导致文件数据源在首次异步读取完成前被截断关闭,致使用户配置的 `pricing.json` 覆盖及变更监听均无法生效。现已修正为返回高阶清理函数 `ctx.effect(() => () => fileSource.dispose(), ...)`,使文件源在 fiber 卸载时才被释放。 + - **纠正单测 Cordis effect Mock 语义**:在 `tests/core.test.mjs`、`tests/pricing.test.mjs` 与 `tests/rounds.test.mjs` 中对齐 Cordis 真实生命周期契约,在 `after()` 中统一执行清理函数。 + - **补充端到端回归用例**:新增 `/pricing route serves the user pricing.json override (file source survives startup)`,验证插件挂载自定义 `pricing.json` 时价格覆盖正常生效且来源正确标记为 `file`。 + ## [1.1.5] - 2026-09-12 diff --git a/README.md b/README.md index 5ae1ec2..bb45e8c 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ To update (upgrade to a new version): pnpm may print `Already up to date` when a ```sh # Option ①: pin the target version explicitly -dsh plugin --profile web add dsh-usage-chart@1.1.5 +dsh plugin --profile web add dsh-usage-chart@1.1.6 # Option ②: remove, then re-add (back to latest) dsh plugin --profile web remove dsh-usage-chart dsh plugin --profile web add dsh-usage-chart diff --git a/README_ZH.md b/README_ZH.md index 7133e12..19aa7d6 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -78,7 +78,7 @@ dsh web --profile web # 启动 DSH Web(已在运行 ```sh # 方式①:显式指定目标版本 -dsh plugin --profile web add dsh-usage-chart@1.1.5 +dsh plugin --profile web add dsh-usage-chart@1.1.6 # 方式②:先移除再重装(回到最新版) dsh plugin --profile web remove dsh-usage-chart dsh plugin --profile web add dsh-usage-chart @@ -91,7 +91,7 @@ dsh plugin --profile web add dsh-usage-chart > ⚠️ **未全局安装 dsh(报 `dsh: command not found` / PowerShell `无法将“dsh”项识别为…`)? > 把上面每条 `dsh` 都写成 `npx --yes @deepseek-ai/dsh`**,例如 -> `npx --yes @deepseek-ai/dsh plugin --profile web add dsh-usage-chart@1.1.5` +> `npx --yes @deepseek-ai/dsh plugin --profile web add dsh-usage-chart@1.1.6` > (原因与解法见 [FAQ](#常见问题faq) 第一条)。 ### 方式二:从 GitHub 安装(源码构建) diff --git a/package-lock.json b/package-lock.json index dcea77b..33d0f6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dsh-usage-chart", - "version": "1.1.5", + "version": "1.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsh-usage-chart", - "version": "1.1.5", + "version": "1.1.6", "license": "MIT", "devDependencies": { "@deepseek-ai/cordis": "^4.0.1", diff --git a/package.json b/package.json index 9d7a47f..d3b90d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-usage-chart", - "version": "1.1.5", + "version": "1.1.6", "description": "DeepSeek 用量 / 成本 / 余额仪表盘 for DSH Web:输入框下方实时指标指示器 + 零依赖 SVG 用量可视化图表(含每轮成本视角、耗时叠加、异常标记、成本徽章、上下文压力条),token 数据来自官方 adapter 上报,成本按官方刊例价 + 用户覆盖 pricing.json 解析,余额来自官方 /user/balance 接口。", "type": "module", "sideEffects": false, From 3851fad718cbe1fc5c410f981fa3f02ce7cb0a52 Mon Sep 17 00:00:00 2001 From: rezon-aki Date: Fri, 18 Sep 2026 23:47:20 +0800 Subject: [PATCH 6/6] fix(client): anchor the fixed panel to its containing block A position:fixed box is laid out against its containing block, not the viewport: any ancestor with transform / filter / backdrop-filter / contain / container-type takes that role. The maid-atelier skin blurs the dock children ([data-slot='conversation.composer.dock'] > *, backdrop-filter: blur(2px)), so the popover was offset by the indicator row's own origin and flew off to the right of the screen. Look up the nearest containing block and convert the viewport anchor into its coordinate space; also guard the clamp range on narrow windows. --- src/client/UsageIndicator.tsx | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/client/UsageIndicator.tsx b/src/client/UsageIndicator.tsx index 30c9877..56b6aed 100644 --- a/src/client/UsageIndicator.tsx +++ b/src/client/UsageIndicator.tsx @@ -50,6 +50,23 @@ function ChartIcon(): JSX.Element { ) } +/** + * fixed 定位的包含块:任一 transform/filter/contain 祖先都会把定位基准从视口改成它自己 + * (皮肤给 dock 子元素加了 backdrop-filter,面板随之整体偏移)。 + */ +function containingBlock(start: HTMLElement): Element | null { + // 属性可能不存在(旧浏览器):undefined 一律按「不创建包含块」处理。 + const creates = (value: string | undefined): boolean => value !== undefined && value !== 'none' && value !== 'normal' + for (let node: Element | null = start; node !== null && node !== document.documentElement; node = node.parentElement) { + const style = getComputedStyle(node) + if (creates(style.transform) || creates(style.translate) || creates(style.rotate) || creates(style.scale) + || creates(style.perspective) || creates(style.filter) || creates(style.backdropFilter) + || creates(style.contain) || creates(style.containerType) + || /transform|perspective|filter/.test(style.willChange)) return node + } + return null +} + export function UsageIndicator(props: DockUsageProps): JSX.Element | null { const { useSession, useProjection, sessionId } = props const locale = useUiLocale() @@ -89,8 +106,14 @@ export function UsageIndicator(props: DockUsageProps): JSX.Element | null { if (el === null) return const r = el.getBoundingClientRect() const width = Math.min(Math.max(r.width, 320), 520, window.innerWidth - 16) - const left = Math.min(Math.max(8, r.left), window.innerWidth - width - 8) - setAnchor({ left, width, bottom: window.innerHeight - r.top + 8 }) + const left = Math.min(Math.max(8, r.left), Math.max(8, window.innerWidth - width - 8)) + // 视口坐标 → 包含块坐标:面板是 fixed,祖先有 transform/filter/contain 时 left/bottom 以它为准。 + const base = containingBlock(el)?.getBoundingClientRect() + setAnchor({ + left: left - (base?.left ?? 0), + width, + bottom: (base?.bottom ?? window.innerHeight) - r.top + 8, + }) }, []) useLayoutEffect(() => {