Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

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

- **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

- **`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=<loaded>` returned an empty `400` while `/usage?session=<unknown>` 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
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

本文件记录本项目所有重要变更。格式遵循 [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

### 变更与优化

- **同步 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

### 修复

- **适配 `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

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Expand Down Expand Up @@ -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
}
}
```
Expand Down Expand Up @@ -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) |
Expand Down
18 changes: 9 additions & 9 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.6
# 方式②:先移除再重装(回到最新版)
dsh plugin --profile web remove dsh-usage-chart
dsh plugin --profile web add dsh-usage-chart
Expand All @@ -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.6`
> (原因与解法见 [FAQ](#常见问题faq) 第一条)。

### 方式二:从 GitHub 安装(源码构建)
Expand Down Expand Up @@ -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
}
}
```
Expand Down Expand Up @@ -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,无原生依赖) |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dsh-usage-chart",
"version": "1.1.2",
"version": "1.1.6",
"description": "DeepSeek 用量 / 成本 / 余额仪表盘 for DSH Web:输入框下方实时指标指示器 + 零依赖 SVG 用量可视化图表(含每轮成本视角、耗时叠加、异常标记、成本徽章、上下文压力条),token 数据来自官方 adapter 上报,成本按官方刊例价 + 用户覆盖 pricing.json 解析,余额来自官方 /user/balance 接口。",
"type": "module",
"sideEffects": false,
Expand Down
27 changes: 25 additions & 2 deletions src/client/UsageIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
50 changes: 25 additions & 25 deletions src/pricing/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ModelPricing> = {
'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<string, ModelPricing> = {
'deepseek-flash': FLASH_PRICING,
'deepseek-v4-pro': {
offPeak: {
cny: { cacheMissInput: 4.5, cacheHitInput: 0.15, output: 13.5 },
Expand All @@ -58,23 +60,21 @@ export const BUILTIN_PRICING: Record<string, ModelPricing> = {
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<T>(table: Record<string, T>, model: string): T | null {
Expand Down
Loading
Loading