dsh-bottom-cost 0.0.0-stage → 0.5.2
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/CHANGELOG.md +129 -0
- package/LICENSE +21 -0
- package/README.md +234 -2
- package/README.zh.md +237 -0
- package/cordis.patch.yml +5 -0
- package/docs/session-cost-panel.png +0 -0
- package/icon.svg +10 -0
- package/lib/client.js +860 -0
- package/lib/fold.js +241 -0
- package/lib/guard.js +69 -0
- package/lib/holidays.js +241 -0
- package/lib/index.js +51 -0
- package/lib/pricebook-store.js +127 -0
- package/lib/pricebook.js +238 -0
- package/lib/pricing.js +128 -0
- package/lib/projection.js +143 -0
- package/lib/routes.js +104 -0
- package/lib/shape.js +148 -0
- package/package.json +69 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本插件的所有显著变更。版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/);
|
|
4
|
+
投影状态的形状变化会体现为 `stateVersion` 递增(旧检查点会自动作废并重放,不需要你手工清理)。
|
|
5
|
+
|
|
6
|
+
## [0.5.2] - 2026-10-14
|
|
7
|
+
|
|
8
|
+
### 文档
|
|
9
|
+
|
|
10
|
+
- **安装方式改为「输入包名 `dsh-bottom-cost`」优先**,仓库地址作为 `dsh web` 上的备选写在同一条里(它要经 codeload 下载 tarball,网络抖动会失败)。包名这条路到处都能用,**包括官方桌面客户端——它只支持 npm 源**。
|
|
11
|
+
- 新增一句「**更新是显式的**」:安装会钉住精确版本(profile 里写成 `"dsh-bottom-cost": "0.5.1"` 这种),新版本要你在插件管理里点**更新**才生效。
|
|
12
|
+
|
|
13
|
+
(纯文档改动;之所以要单独发一个版本,是因为**已经发布的 npm 版本不可覆盖**,npm 页面上的 README 只会随新版本更新。)
|
|
14
|
+
|
|
15
|
+
## [0.5.1] - 2026-10-13
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- **`deepseek-v4-pro` 改为恒按它自己的官方价目计价**,删掉了那条时间栅栏。
|
|
20
|
+
背景:2026-09-14 曾计划下线 Pro、把它的请求按 V4.1-Flash 承接并计费,`@linxin666/dsh-usage`
|
|
21
|
+
据此设了栅栏,本插件早期照抄;**官方随后决定继续提供 Pro、计费方式不变**(严格按自身价目,与 Flash 分开),
|
|
22
|
+
所以旧栅栏的前提已不成立。
|
|
23
|
+
- 影响:栅栏之后的 pro 轮次此前按 Flash 档估算(偏低 3〜7.5 倍),现在按官方 pro 行计算,金额会明显变大。
|
|
24
|
+
- 一并删除:`pricing.js` 的 `V4_PRO_FOLDED_INTO_FLASH_AT_MS` 与 `isDowngradedPro()`、
|
|
25
|
+
面板上的 `按Flash档估` 标记与那行汇总提醒(连同两条文案键)——它们只为栅栏而存在,栅栏没了就是死代码。
|
|
26
|
+
- `priceRowKeyFor(model)` 因此不再需要「调用时刻」这个参数。
|
|
27
|
+
- **`stateVersion` 2 → 3**:桶里的 `row` 是折叠时算好存下的,旧检查点里的 pro 桶仍是 `flash`,
|
|
28
|
+
不 +1 就会一直按旧规则显示。+1 让旧检查点作废并重放一次,**历史轮次也会按新规则重算**
|
|
29
|
+
(这正是这个机制存在的意义);形状未变,只是规则变了。
|
|
30
|
+
|
|
31
|
+
## [0.5.0] - 2026-10-13
|
|
32
|
+
|
|
33
|
+
### 新增
|
|
34
|
+
|
|
35
|
+
- **每轮显示所用模型**(`v4-pro` / `v4-flash` …):跟在轮次号右侧,正好用掉标签与金额之间空出来的那截宽度。
|
|
36
|
+
- 只削掉 `deepseek-` 前缀,不做别的改写;一轮里混用了多个模型就并列(`+`),太长交给标签的省略号。
|
|
37
|
+
- 悬停显示完整模型 id(`title`)。
|
|
38
|
+
- 数据本来就带 `model`(每个计价桶都有),这次只是把它显示出来——**没有改投影状态、schema 或 `stateVersion`**,旧检查点照样有效。
|
|
39
|
+
|
|
40
|
+
## [0.4.0] - 2026-10-13
|
|
41
|
+
|
|
42
|
+
### 新增
|
|
43
|
+
|
|
44
|
+
- **点面板里的「第 N 轮」跳到对话中的那一轮**。做法是**只读地驱动官方自己的轮次导航轨道**:轨道刻度是带 `aria-label`(含轮号)的 `button`,点它即调用官方的 `onNavigate`——已加载的轮次直接落位,**未加载的轮次由官方先取回更早的分页再落位**(官方文案键 `chat.turnNavigation.jumpLoad` 就是为这条路径存在的)。这两条我们都不重新实现。
|
|
45
|
+
- 刻度不在已挂载窗口时(轨道自身也是虚拟挂载),先按官方文档的固定间距(每个已开始轮次一颗刻度、相隔 10px)把轨道滚过去,等一帧再重查,最多三次。
|
|
46
|
+
- 轨道不可用时回落到 transcript 行上的 `data-chat-turn`,并按官方自用的阅读线(视口高度的 20%、上限 96px)对齐。
|
|
47
|
+
- 两处都定位不到时,面板底部给出说明——不静默失败。
|
|
48
|
+
- **不写死任何构建哈希类名**(形如 `xpvNua_mark` 会随版本变),只用语义属性:标签里的轮号、`aria-current`、`data-chat-turn`。不注入节点、不改官方布局。
|
|
49
|
+
- **标题行上的明暗开关(小太阳 / 月亮)**:点击把**本插件自己的内容**切到另一套配色,应用全局主题不受影响。
|
|
50
|
+
- 做法是把官方的主题变量在插件作用域内按另一套重声明(CSS 变量可继承),所以现有规则一行都不用改。
|
|
51
|
+
- 取值**不是自己挑的颜色**,而是从 `@deepseek-ai/dsh-client-ui-theme` 的两套定义里解析出的最终值(含 `var()` 链条:`--dsw-specific-menu` → `--dsw-menu-surface-fill`),因此两种模式下都与官方同一套配色,只是与外部相反。
|
|
52
|
+
- 当前模式直接读官方的 `body[data-ds-dark-theme]` 属性判断(官方切主题靠的就是它),不是猜的;应用主题中途变化也能跟上。
|
|
53
|
+
- **面板与费用胶囊都参与**切换。胶囊平时是透明文字、靠应用那一侧的行背景,切成相反主题后会给它一层主题底色,否则近白的字落在浅色统计栏上会看不清。
|
|
54
|
+
- 状态只存在内存里:重新加载页面后回到「跟随应用」。
|
|
55
|
+
- 顺手修掉一处**一直空转的样式**:`.dshsc_sep` 用了 `--dsw-alias-separator-primary`,而该变量在整个 asar 里都不存在(0 处定义),补上后备值。
|
|
56
|
+
|
|
57
|
+
## [0.3.0] - 2026-10-13
|
|
58
|
+
|
|
59
|
+
### 变更
|
|
60
|
+
|
|
61
|
+
- **插件与仓库改名为 `dsh-bottom-cost`**(原 `dsh-session-cost`)。原因是插件市场里已有一个
|
|
62
|
+
同名条目 [`KIDLi1412/dsh-session-cost`](https://github.com/KIDLi1412/dsh-session-cost),
|
|
63
|
+
两者位置(统计栏同一行)、计价思路与展开交互都相近;同名会让收录评审直接读成"重复",
|
|
64
|
+
而新名字把**位置**(输入框下方那条统计栏)说清楚,与本插件的差异(逐轮明细、
|
|
65
|
+
命中/未命中两项总额、可刷新的官方价目表)一起构成条目描述。
|
|
66
|
+
- GitHub 仓库改名后旧地址会自动 301 跳转,旧链接不会断。
|
|
67
|
+
- **安装路径随之变化**:`node_modules/dsh-session-cost` → `node_modules/dsh-bottom-cost`,
|
|
68
|
+
profile 里 `cordis.patch.yml` 的 loader 行也要换成新名。装过旧版的请重跑一次
|
|
69
|
+
`node tools/install.mjs <profile>`(它会写入新名;旧目录与旧行请删掉,否则宿主会找不到包)。
|
|
70
|
+
- 插件对外标识同步更新:注册名 `dsh-bottom-cost`、HTTP 路由前缀 `/api/dsh-bottom-cost/`、
|
|
71
|
+
数据目录 `<DSH_HOME>/dsh-bottom-cost/`。投影 key `sessionCost` 与 entry id `ui-session-cost`
|
|
72
|
+
**不变**(它们不冲突,也没有对外契约上的必要)。
|
|
73
|
+
- 版本号进到 `0.3.0`:包名变了,对已安装者是实打实的破坏性变更,不宜当作修订版。
|
|
74
|
+
|
|
75
|
+
## [0.2.1] - 2026-10-13
|
|
76
|
+
|
|
77
|
+
### 变更
|
|
78
|
+
|
|
79
|
+
- **去掉唯一的运行时依赖 `zod`,改为自带的 `lib/shape.js`**(约 90 行,零依赖)。
|
|
80
|
+
投影注册表只对 schema 调用 `.parse`,所以契约是鸭子类型的,手写完全够用;
|
|
81
|
+
而 README 首推的复制式安装不带依赖,缺了 zod 插件会在装载阶段静默失败。
|
|
82
|
+
现在 `git clone` + `node tools/install.mjs` **开箱即用**,不需要联网、不需要补装任何包。
|
|
83
|
+
- 形状与 `stateVersion` 均未变化,旧检查点继续有效。
|
|
84
|
+
- 一处**刻意比 zod 更严**:`NaN` / `Infinity` 一律拒绝(会静默污染累计值)。
|
|
85
|
+
- 代价:将来给状态加字段要同步改 `lib/shape.js`(两者同文件、且由 `selftest` / `schema-check` 守着)。
|
|
86
|
+
- `tools/install.mjs`:移除 zod 依赖探测(已无依赖可探测);修掉 `--dry-run` 仍会 `mkdir` 的问题——现在真的不写任何文件。
|
|
87
|
+
|
|
88
|
+
## [0.2.0] - 2026-10-13
|
|
89
|
+
|
|
90
|
+
### 文档
|
|
91
|
+
|
|
92
|
+
- README 改为「安装」置顶,并新增**让你的 AI 帮你安装**(可直接复制给 agent 的一段指令)。
|
|
93
|
+
- 用真实界面截图 `docs/session-cost-panel.png` 替换 ASCII 示意,并补上界面读法
|
|
94
|
+
(统计栏胶囊左右两个数字各是什么、点开后看到什么)与「刷新」按钮的作用说明。
|
|
95
|
+
- 中英双语 README 互切,与同作者的姊妹插件保持一致。
|
|
96
|
+
|
|
97
|
+
### 新增
|
|
98
|
+
|
|
99
|
+
- **刷新价目**:面板里一键抓取官方中文价目页,解析 12 个单价并逐项校验,
|
|
100
|
+
**先把差异列出来给你确认**再采用;落盘 `<DSH_HOME>/dsh-bottom-cost/pricebook.json`,
|
|
101
|
+
重启后仍生效。抓取失败保留当前价目。
|
|
102
|
+
- `deepseek-v4-pro` 的档位标注:落在 2026-09-14 12:00 栅栏之后的轮次会标 `按Flash档估`
|
|
103
|
+
并给出汇总提醒(官方页把 v4-pro 按独立价目列出,与插件逻辑冲突,详见 README)。
|
|
104
|
+
|
|
105
|
+
### 变更
|
|
106
|
+
|
|
107
|
+
- **金额改为浏览器侧按当前价目表现算**,投影状态里只保留 token 事实
|
|
108
|
+
(`stateVersion` 1 → 2)。因此采用新价目后整段对话立刻重算,不会出现两代价格混在一本账里。
|
|
109
|
+
- 面板标题行与价目来源行分行显示;面板固定宽度,任何长文案都不会再撑开它。
|
|
110
|
+
|
|
111
|
+
## 0.1.0 - 2026-10-13(仓库建立前,仅存档)
|
|
112
|
+
|
|
113
|
+
### 新增
|
|
114
|
+
|
|
115
|
+
- 统计栏(`conversation.composer.dock`)里的费用胶囊:本对话累计费用 · 最近一轮费用。
|
|
116
|
+
- 点击向上展开:顶部固定「总命中缓存费用 / 总未命中费用」,下面按轮滚动,每轮三行
|
|
117
|
+
(总费用 / 命中 / 未命中),默认停在最新一轮。
|
|
118
|
+
- 宿主侧 `sessionCost` 会话投影:按轮次折叠 token 事实,自带历史回填与跨重启检查点复用。
|
|
119
|
+
- 峰谷计价:北京时间工作日 09:00-12:00、14:00-18:00 为高峰,法定节假日与周末全天空闲;
|
|
120
|
+
每次调用按**发生时刻**的档位计费。
|
|
121
|
+
- 非 DeepSeek 官方路由一律「未计价」并统计次数,不假装是 0 元。
|
|
122
|
+
|
|
123
|
+
[0.5.2]: https://github.com/LanFeng97/dsh-bottom-cost/releases/tag/v0.5.2
|
|
124
|
+
[0.5.1]: https://github.com/LanFeng97/dsh-bottom-cost/releases/tag/v0.5.1
|
|
125
|
+
[0.5.0]: https://github.com/LanFeng97/dsh-bottom-cost/releases/tag/v0.5.0
|
|
126
|
+
[0.4.0]: https://github.com/LanFeng97/dsh-bottom-cost/releases/tag/v0.4.0
|
|
127
|
+
[0.3.0]: https://github.com/LanFeng97/dsh-bottom-cost/releases/tag/v0.3.0
|
|
128
|
+
[0.2.1]: https://github.com/LanFeng97/dsh-bottom-cost/releases/tag/v0.2.1
|
|
129
|
+
[0.2.0]: https://github.com/LanFeng97/dsh-bottom-cost/releases/tag/v0.2.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LanFeng97
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,235 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-bottom-cost
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
A **session cost** chip for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): it sits in the same row as the built-in stats pills under the composer and answers "what has this conversation cost" and "what did the last turn cost" at a glance, splitting the spend into **cache-hit** and **cache-miss** parts.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
### Option 1 — install it from the DSH UI (simplest)
|
|
12
|
+
|
|
13
|
+
1. Open **插件 → 添加插件** (Plugins → Add plugin) in the sidebar;
|
|
14
|
+
2. Type either one:
|
|
15
|
+
- the package name **`dsh-bottom-cost`** — works everywhere, including the official desktop client (which can only install npm sources), or
|
|
16
|
+
- the repository URL `https://github.com/LanFeng97/dsh-bottom-cost` — works on `dsh web`; the market resolves it to a tarball from codeload, so a flaky network can fail it;
|
|
17
|
+
3. Click **安装** (Install), then **restart DeepSeek Harness** as its banner tells you to (close the running `dsh` process and start it again, e.g. `dsh web`).
|
|
18
|
+
|
|
19
|
+
The host half is a Node module, so **a config reload alone will not import it** — that restart is not optional. Once restarted, open the UI and the chip is there; no page refresh needed.
|
|
20
|
+
|
|
21
|
+
**Updating later is explicit.** An installation pins the exact version (the profile ends up with `"dsh-bottom-cost": "0.5.1"`), so a new release reaches you when you press **更新** (Update) in the plugin manager — nothing changes under you in between.
|
|
22
|
+
|
|
23
|
+
### Option 2 — let your AI install it
|
|
24
|
+
|
|
25
|
+
Paste this whole block into any AI that can read and write your machine (an agent inside DSH, Claude Code, Codex — anything):
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
Install the DSH plugin at https://github.com/LanFeng97/dsh-bottom-cost into my DeepSeek Harness:
|
|
29
|
+
clone the repo, run `node tools/install.mjs`, and tell me when I need to restart DSH.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Option 3 — from the plugin market (once listed)
|
|
33
|
+
|
|
34
|
+
DSH plugins are distributed as packages; look for **dsh-bottom-cost** in [dshmarket](https://github.com/dsh-market/dsh-market). Until it is listed, use option 1 or 2.
|
|
35
|
+
|
|
36
|
+
### Option 4 — from source (developers)
|
|
37
|
+
|
|
38
|
+
For hacking on it, or for a non-default profile:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
git clone https://github.com/LanFeng97/dsh-bottom-cost
|
|
42
|
+
cd dsh-bottom-cost
|
|
43
|
+
node tools/install.mjs # copy into the profile's node_modules + append one loader line
|
|
44
|
+
node tools/install.mjs --dry-run # print what it would do, write nothing
|
|
45
|
+
node tools/install.mjs web # target another profile (default: desktop)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
It is idempotent — re-run it after a `git pull` to sync an update — and it resolves your DSH home the way the harness does (`$DSH_HOME`, then `~/.dsh`). This route needs a restart too. After that, changing `lib/client.js` only needs a page refresh; changing `lib/index.js` still needs a restart.
|
|
49
|
+
|
|
50
|
+
The plugin has **no runtime dependencies at all** (its shape validation is a ~90-line file it ships, `lib/shape.js`), so this route installs nothing from the network. That is also why it installs by **copy** rather than a directory junction: a junction gets realpath-resolved back into the clone, so any future bare import of a third-party package would fail to resolve there at load time.
|
|
51
|
+
|
|
52
|
+
### Cannot install or cannot uninstall? Read this first
|
|
53
|
+
|
|
54
|
+
**The desktop client can only install npm sources.** On the official desktop app, pasting a GitHub URL into "add plugin" may simply not install — that is the client's install boundary, not your network (its own message says so: "This desktop client can only install plugins published to npm"). Install by npm package name instead.
|
|
55
|
+
|
|
56
|
+
**A flaky network breaks the GitHub source.** Behind that box is pnpm: it resolves `owner/repo` to a commit, then downloads a tarball from `codeload.github.com`, and the resolve step has a **6-second timeout**. A hiccup fails the install; retrying usually gets through.
|
|
57
|
+
|
|
58
|
+
**Do not mix the two install routes (important).** Once you installed through the plugin manager / market, **do not also run `tools/install.mjs`** (and do not have an AI run it) — and vice versa. The script appends its own loader row to the profile's `cordis.patch.yml`, while the plugin manager declares the same component too. With the same component declared in **two layers**, the manager refuses to disable or uninstall it:
|
|
59
|
+
|
|
60
|
+
> 卸载失败:其他配置仍在使用这个组合包的组件,请先停用它们
|
|
61
|
+
|
|
62
|
+
(An uninstall failed: another configuration still uses this bundle's components — disable those first.)
|
|
63
|
+
|
|
64
|
+
Its "disable" toggle cannot suppress the other layer either, which is why a **disabled plugin keeps working and survives a restart**.
|
|
65
|
+
|
|
66
|
+
**What to do** (in order):
|
|
67
|
+
|
|
68
|
+
1. Open `<DSH install dir>/dsh/profiles/<profile>/cordis.patch.yml` and **delete this whole block** (comment line plus the three `insert` lines):
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
# dsh-bottom-cost: ...
|
|
72
|
+
- insert:
|
|
73
|
+
- id: ui-session-cost
|
|
74
|
+
name: 'dsh-bottom-cost'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
2. **Fully quit and restart DSH** (a page refresh is not enough);
|
|
78
|
+
3. Back in the plugin manager, disable and uninstall now work;
|
|
79
|
+
4. If it then says the profile patch cannot uniquely address the row, a second declaration exists somewhere else — send me that profile's `cordis.patch.yml` and I will give you the exact edit.
|
|
80
|
+
|
|
81
|
+
**One-line self-check** (in `<DSH install dir>/dsh/profiles/<profile>/`):
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
grep -n "dsh-bottom-cost" cordis.patch.yml package.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A hit in `package.json` = installed by the manager (normal). A hit in **`cordis.patch.yml` too** = the two-layer problem above.
|
|
88
|
+
|
|
89
|
+
## How to read the UI
|
|
90
|
+
|
|
91
|
+
In the screenshot: the bottom line is the **stats row under the composer** — `◎ ¥5.294 · ¥0.9230` is this plugin, sitting in the same row as the built-in `15 轮 388 步 · 228 tok/s` (session stats) and `147M tok · 缓存命中 99%` (token usage) pills, with the context meter at the far right. Above it is the **expanded panel**.
|
|
92
|
+
|
|
93
|
+
Clicking a **Turn N** heading jumps the conversation to that turn: it read-only drives the built-in turn rail on the right, so a turn that is not loaded yet gets paged in by the app itself before landing. When it cannot locate the turn, the panel says so at the bottom instead of failing silently.
|
|
94
|
+
|
|
95
|
+
Each turn also shows **which model it used**, right of the turn number (`v4-pro`, `v4-flash`, …; only the `deepseek-` prefix is trimmed, a turn that mixed models lists them side by side, and hovering reveals the full ids).
|
|
96
|
+
|
|
97
|
+
The **sun (or moon)** at the right of the title row is a light/dark switch that applies **to this plugin's content only**: both the panel and the cost chip in the stats row flip to the other palette, and the app's global theme is untouched. The icon shows the current mode (sun = light, moon = dark), and the colours come from the two official theme definitions — so each mode matches the rest of DSH while being the opposite of it. When the chip is on the opposite theme it gets its own themed background, since near-white text on the light stats row would otherwise be unreadable.
|
|
98
|
+
|
|
99
|
+
**The chip in the stats row**
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
◎ ¥2.922 · ¥0.4877
|
|
103
|
+
↑ left ↑ right
|
|
104
|
+
this conversation's the latest turn's
|
|
105
|
+
total spend spend
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- The **left** number is what this conversation has cost so far; the **right** number is what the most recent turn cost. Both update live as the conversation goes on.
|
|
109
|
+
- With nothing billable yet, the chip is hidden — the same "no content, no render" behaviour as the built-in stats pills.
|
|
110
|
+
|
|
111
|
+
**Click the chip and the panel opens upward**
|
|
112
|
+
|
|
113
|
+
- **Fixed header**: the conversation's total spend, split into **cache-hit** and **cache-miss** (`¥1.609` and `¥1.313` in the screenshot). Those two always add up to the total on the chip — the identity `total = hit + miss` holds by construction, not by rounding luck.
|
|
114
|
+
- **Scrolling list**: **every turn's** spend, three rows each — **total / cache-hit / cache-miss** (turns 7 and 8 in the screenshot). It starts at the newest turn; **scroll up** to walk back through earlier ones.
|
|
115
|
+
- **The scrolling window is exactly two turn blocks tall** — you came here to see this turn and the previous one, and the panel does not have to grow tall for that.
|
|
116
|
+
|
|
117
|
+
**The 刷新 (Refresh) button, top right**
|
|
118
|
+
|
|
119
|
+
It is about the **price list used for pricing**, not about re-counting your conversation. Clicking it makes the host fetch the **official Chinese price page**, parse its 12 unit prices and validate every one, then **show you the diff first** (which line changed, from what to what) — nothing is applied until you click **采用 (Apply)**. That extra step exists because DeepSeek publishes **no machine-readable price API**, only a human-facing docs page, so parsing it can fail; a bad parse applied silently would corrupt every number you look at.
|
|
120
|
+
|
|
121
|
+
The panel's second line — `价目:官方页 · 10/13 02:17` — tells you **which price list is active and when it was fetched**. If it says `价目:内置表`, no fetch has happened yet and the built-in table shipped with the plugin is in use.
|
|
122
|
+
|
|
123
|
+
**Do you need to click it often?** No. Once when the official prices change. After you apply, **the whole conversation is instantly re-priced** (money is computed in the browser from the active price list; history stores only token facts), so you never end up with two generations of prices mixed into one ledger.
|
|
124
|
+
|
|
125
|
+
## Where the numbers come from
|
|
126
|
+
|
|
127
|
+
Only the **official DeepSeek routes** have a published price list, so only they produce money:
|
|
128
|
+
|
|
129
|
+
| | |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| **Hit spend** | cache-read tokens × hit price |
|
|
132
|
+
| **Miss spend** | (uncached input + cache-write) × miss price **+** output tokens × output price |
|
|
133
|
+
| **Peak / off-peak** | Beijing time Mon–Fri 09:00–12:00 and 14:00–18:00 are peak (double price); weekends and Chinese public holidays are off-peak all day |
|
|
134
|
+
| **When it is priced** | At the instant each call happened, taken from the session event's own timestamp — so replaying months-old history is priced with the tariff of *that* moment, not today's |
|
|
135
|
+
| **Other providers** | Never guessed at ¥0: they show as *unpriced*, and the panel reports how many calls were unpriced |
|
|
136
|
+
|
|
137
|
+
Scope is the **current conversation's own turns**. A subagent is a different session and is deliberately not folded in, so the chip's numbers line up one-to-one with the turns you can see.
|
|
138
|
+
|
|
139
|
+
### It is an estimate, not a bill
|
|
140
|
+
|
|
141
|
+
Prices are estimated from token counts and the published tariff. The plugin never reads your balance, never touches credentials, and cannot see what DeepSeek actually deducted.
|
|
142
|
+
|
|
143
|
+
### `deepseek-v4-pro` is priced on its own official row
|
|
144
|
+
|
|
145
|
+
2026-09-14 was meant to retire Pro, serving its requests as V4.1-Flash instead; [`@linxin666/dsh-usage`](https://github.com/zhu1090093659/dsh-web) encoded that as a time fence, and this plugin copied it early on (that is where the old "the tier is disputed" section came from).
|
|
146
|
+
|
|
147
|
+
**The provider then kept Pro available with billing unchanged** — Pro is charged strictly on its own official row, separate from Flash. So 0.5.1 **removed the fence**: a `deepseek-v4-pro` request is always priced on its own row (miss 4.5/9.0, output 13.5/27.0), and the `按Flash档估` mark plus its summary line are gone too, since their premise no longer holds.
|
|
148
|
+
|
|
149
|
+
> If you run into the old "Pro is served by Flash" claim elsewhere, note that it describes the pre-2026-09-14 plan; do not reintroduce the fence.
|
|
150
|
+
|
|
151
|
+
## The full price-refresh flow
|
|
152
|
+
|
|
153
|
+
1. The host fetches the **official Chinese price page** (`api-docs.deepseek.com/zh-cn/quick_start/pricing`, which quotes CNY; the English page quotes USD and is not used).
|
|
154
|
+
2. It parses the table and validates all 12 unit prices — if any single one is missing or implausible, the whole parse is rejected.
|
|
155
|
+
3. It shows you **the diff**; **采用 (Apply)** commits it, **放弃 (Discard)** throws it away.
|
|
156
|
+
|
|
157
|
+
The price list is stored in `<DSH_HOME>/dsh-bottom-cost/pricebook.json` (atomic write) and survives restarts. A failed fetch (offline, timeout, redesigned page) is reported and **keeps the current prices**; the built-in table remains the first-run fallback.
|
|
158
|
+
|
|
159
|
+
### Capability boundary
|
|
160
|
+
|
|
161
|
+
- **One outbound request**, to the public docs page above, **only when you click 刷新**. Nothing else ever leaves the machine.
|
|
162
|
+
- **One file written**: `<DSH_HOME>/dsh-bottom-cost/pricebook.json`.
|
|
163
|
+
- The three price routes are **loopback + same-origin fenced** (the same guard sibling DSH plugins use).
|
|
164
|
+
- The host half reads no credentials, writes no session data, and runs no timers.
|
|
165
|
+
- The **Turn N** headings read-only drive the built-in turn navigation: they read the tick buttons' `aria-label` (which carries the turn number) and `aria-current`, and invoke the tick's own click. If the rail is unavailable it falls back to the `data-chat-turn` anchors on transcript rows. The plugin injects no nodes, changes no layout, and hardcodes no build-hashed class names — but if those semantic attributes ever change, jumping breaks and the panel reports the failure instead of failing silently.
|
|
166
|
+
|
|
167
|
+
### Why the shape validation is hand-rolled
|
|
168
|
+
|
|
169
|
+
DSH's projection registry asks each unit for a `stateSchema` and a `wire.viewSchema`, and **only ever calls `.parse` on them** (six call sites in `@deepseek-ai/dsh-session-projection`; no `safeParse`, no `optional`, no `refine`). That contract is duck-typed, so `lib/shape.js` implements exactly the seven predicates this plugin needs — object, string, finite number, boolean, array, enum, nullable — in about 90 dependency-free lines.
|
|
170
|
+
|
|
171
|
+
The alternative was `zod`, as DSH's own `dsh-token-meter` uses. It is not wrong, but for this plugin it loses: the published package is 840 files / 5.86 MB (even a trimmed v4-ESM subset is 111 files / 886 KB), and the README's primary install path is a *copy* install that does not bring dependencies along — so shipping zod would mean either a large vendored blob, or a pnpm/network step that rewrites your profile's `package.json`. Hand-rolling keeps that path genuinely self-contained. It is slightly *stricter* than zod in one place: `NaN` and `Infinity` are rejected (they would silently poison a running total), and it is covered by the self-test suite directly — which zod, being a third-party import, could not be.
|
|
172
|
+
|
|
173
|
+
## How it works
|
|
174
|
+
|
|
175
|
+
### Official extension points, not DOM injection
|
|
176
|
+
|
|
177
|
+
- The chip registers into the built-in slot **`conversation.composer.dock`** (`order: 1`; the shipped `stats` pills take `order: 0`). That slot *is* the stats row under the composer, a `display:flex; gap:12px` row, so the chip is a first-class sibling of the built-in pills — no DOM poking, no fight with re-renders.
|
|
178
|
+
- The panel reuses `useAnchoredPosition` and `useDismissOnOutsidePointer` from `@deepseek-ai/dsh-client-ui-primitives`, the same pair the built-in stats dialogs use, so placement, clamping, outside-click and Esc behave identically.
|
|
179
|
+
- All styling goes through theme CSS variables (`--dsw-*`), so it follows your theme instead of hard-coding colors.
|
|
180
|
+
|
|
181
|
+
### Token facts travel as a session projection
|
|
182
|
+
|
|
183
|
+
The host registers a **session projection unit** (`sessionCost`) and the browser reads it with `useProjection('sessionCost')`. That buys three things for free:
|
|
184
|
+
|
|
185
|
+
- **History backfill.** The projection registry replays a late-registered unit over the in-memory log on first touch, so after installing or restarting DSH the turns that *already happened* in the current conversation are folded in — the chip does not start at ¥0.
|
|
186
|
+
- **Persistence.** State rides the harness's projection checkpoint cache, so the plugin writes no data files of its own.
|
|
187
|
+
- **Push, not polling.** Values arrive on the session control stream; there is no timer and no fetch loop.
|
|
188
|
+
|
|
189
|
+
### Layered so it can be tested without DSH
|
|
190
|
+
|
|
191
|
+
| File | Role |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `lib/fold.js` | Pure fold: session events → per-turn token facts. Zero dependencies. |
|
|
194
|
+
| `lib/pricing.js` | Pure pricing: peak/off-peak clock, model→price-row decision, tokens→money. Zero dependencies. |
|
|
195
|
+
| `lib/pricebook.js` | Pure price book: built-in table, strict validation, official-page parser. Zero dependencies. |
|
|
196
|
+
| `lib/projection.js` | Thin adapter: the shape schemas + the client view. |
|
|
197
|
+
| `lib/pricebook-store.js` | Fetch, stage a candidate, apply, persist. |
|
|
198
|
+
| `lib/routes.js`, `lib/guard.js` | Loopback-fenced HTTP translation. |
|
|
199
|
+
| `lib/client.js` | The chip and the panel; computes money from facts × the active book. |
|
|
200
|
+
|
|
201
|
+
## Tests
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
npm test # node tools/selftest.mjs — 66 checks, no network
|
|
205
|
+
npm run test:live # node tools/live-check.mjs — fetch the real page, parse, persist, read back
|
|
206
|
+
npm run test:schema # node tools/schema-check.mjs — schema ↔ state ↔ view consistency, from the installed copy
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`selftest` covers peak/off-peak and holiday exceptions, the parser (against a checked-in snapshot of the official page), price-book validation, re-pricing under a different book, per-bucket aggregation, replay idempotence, JSON-safe state, list trimming, the client's i18n key set, and — since the validators are now the plugin's own code — the projection schemas themselves (unknown fields stripped, wrong types rejected with a path, `NaN` refused).
|
|
210
|
+
|
|
211
|
+
`test:schema` is a different kind of check: it imports the copy that is **actually installed in the profile** and drives state → view → schema through it, so it catches both "the installed files can load" and "`viewOf()` and the view schema still agree field-for-field". That second seam broke once in development; it has its own guard now.
|
|
212
|
+
|
|
213
|
+
## Compatibility
|
|
214
|
+
|
|
215
|
+
- DSH **≥ 0.2.0-rc.2** (built and verified against 0.2.0-rc.2).
|
|
216
|
+
- Node **≥ 20**.
|
|
217
|
+
- **No runtime dependencies.** The projection schemas are validated by the plugin's own `lib/shape.js`.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
[MIT](LICENSE) © 2026 LanFeng97.
|
|
222
|
+
|
|
223
|
+
Two pieces are not original to this plugin and keep their upstream credit:
|
|
224
|
+
|
|
225
|
+
- `lib/holidays.js` — Chinese public-holiday dates from [bastengao/chinese-holidays-node](https://github.com/bastengao/chinese-holidays-node) (MIT).
|
|
226
|
+
- The peak/off-peak rules and the price-row decision in `lib/pricing.js`, plus the built-in table in `lib/pricebook.js` — same tariff interpretation as [@linxin666/dsh-usage](https://github.com/zhu1090093659/dsh-web) (Apache-2.0).
|
|
227
|
+
|
|
228
|
+
### No runtime coupling
|
|
229
|
+
|
|
230
|
+
Both bullets above are **credits only**: the rules and data were **copied into this plugin's source**. This plugin does **not `import` dsh-usage, and does not import any `@linxin666/*` package**. Uninstall dsh-usage, upgrade it, rename it — this plugin keeps working. The flip side: when the official prices change, the two **copies** each need updating (that is what "same tariff interpretation" is warning about).
|
|
231
|
+
|
|
232
|
+
**Zero runtime dependencies.** Nothing to install alongside, nothing to keep in sync: the projection schemas come from the plugin's own `lib/shape.js` (see "Why the shape validation is hand-rolled" above).
|
|
233
|
+
## Related
|
|
234
|
+
|
|
235
|
+
- [dsh-workspace-create-dialog](https://github.com/LanFeng97/dsh-workspace-create-dialog) — the sibling plugin by the same author, for the Create Workspace dialog.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# dsh-bottom-cost
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 加一颗**会话费用**胶囊:它坐在输入框正下方那条统计栏里,和内置的统计胶囊同一行,一眼回答「这段对话花了多少钱」与「最近一轮花了多少钱」,并把费用拆成**命中缓存**与**未命中**两笔。
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
### 方式一:在 DSH 界面里装(最简单)
|
|
12
|
+
|
|
13
|
+
1. 侧栏打开**插件 → 添加插件**;
|
|
14
|
+
2. 二选一填进去:
|
|
15
|
+
- 包名 **`dsh-bottom-cost`** —— 到处都能装,**包括官方桌面客户端**(它只支持 npm 源);
|
|
16
|
+
- 仓库地址 `https://github.com/LanFeng97/dsh-bottom-cost` —— 在 `dsh web` 上可用;它要经 codeload 下载 tarball,网络抖动会失败;
|
|
17
|
+
3. 点**安装**,再按它的提示**重启 DeepSeek Harness**(关闭当前 `dsh` 进程后重新运行,例如 `dsh web`)。
|
|
18
|
+
|
|
19
|
+
宿主半区是 Node 模块,**只重载配置不会被导入**,所以这一步重启不能省。重启后打开界面就能看到胶囊,不需要再刷新页面。
|
|
20
|
+
|
|
21
|
+
**以后的更新是显式的。** 安装会钉住精确版本(profile 里会写成 `"dsh-bottom-cost": "0.5.1"`),所以新版本要你在插件管理里点**更新**才会生效——它不会在你不知情的时候自己变。
|
|
22
|
+
|
|
23
|
+
### 方式二:让你的 AI 帮你装
|
|
24
|
+
|
|
25
|
+
把下面这一整段复制给任意一个能读写你电脑的 AI(DSH 里的 agent、Claude Code、Codex 都行),它会自己完成克隆与安装:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
帮我安装 https://github.com/LanFeng97/dsh-bottom-cost 这个插件到我的 DeepSeek Harness:
|
|
29
|
+
克隆仓库,运行 node tools/install.mjs,装好后告诉我需要重启 DSH 才能生效。
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 方式三:从插件市场搜(上架后)
|
|
33
|
+
|
|
34
|
+
DSH 插件以包的形式分发;在 [dshmarket](https://github.com/dsh-market/dsh-market) 里搜索 **dsh-bottom-cost**。上架之前请用方式一或方式二。
|
|
35
|
+
|
|
36
|
+
### 方式四:从源码安装(开发者)
|
|
37
|
+
|
|
38
|
+
要改代码、或要装到非默认 profile 时用这条:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
git clone https://github.com/LanFeng97/dsh-bottom-cost
|
|
42
|
+
cd dsh-bottom-cost
|
|
43
|
+
node tools/install.mjs # 复制进 profile 的 node_modules,并追加一行 loader 行
|
|
44
|
+
node tools/install.mjs --dry-run # 只打印要做什么,不写任何文件
|
|
45
|
+
node tools/install.mjs web # 指定别的 profile(默认 desktop)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
脚本是幂等的,`git pull` 之后再跑一次即可同步更新;它按 `$DSH_HOME`、`~/.dsh` 的顺序解析 DSH 家目录,与 harness 本身一致。装完同样**需要重启**。之后改 `lib/client.js` 只需刷新页面,改 `lib/index.js` 仍需重启。
|
|
49
|
+
|
|
50
|
+
本插件**零运行时依赖**(形状校验用自带的 `lib/shape.js`,约 90 行),所以这条路径不需要联网装任何包。这里用**复制**而不是目录联结也是因此:联结会被 Node realpath 回克隆目录,万一将来引入裸三方导入,装载阶段就会在那里找不到它。
|
|
51
|
+
|
|
52
|
+
### 装不上 / 卸不掉?(先看这里)
|
|
53
|
+
|
|
54
|
+
**桌面客户端只能装 npm 源。** 如果你用的是官方桌面端,往「添加插件」里贴 GitHub 地址可能装不上——这不是网络问题,而是客户端的安装边界(它自己的提示原话就是「这个桌面客户端只能安装已发布到 npm 的插件」)。改用 npm 包名安装。
|
|
55
|
+
|
|
56
|
+
**网络不好时 GitHub 源容易超时。** 「添加插件」背后是 pnpm:它先把 `owner/repo` 解析成具体 commit,再从 `codeload.github.com` 下载 tarball,而解析那一步有 **6 秒超时**。网络抖动就会失败,重试通常能过。
|
|
57
|
+
|
|
58
|
+
**两条安装路线不要混用(重要)。** 用「插件管理 / 插件市场」装过之后,**不要再跑 `tools/install.mjs`**(也别让 AI 跑它),反过来也一样。原因:脚本会往 profile 的 `cordis.patch.yml` 追加一行 loader 行,而插件管理自己也声明这个组件——同一个组件被**两层**声明后,插件管理会拒绝停用与卸载,报的就是:
|
|
59
|
+
|
|
60
|
+
> 卸载失败:其他配置仍在使用这个组合包的组件,请先停用它们
|
|
61
|
+
|
|
62
|
+
这种情况下那个「停用」开关也压不住另一层,所以你会看到**停用后功能照旧、重启后依旧如此**。
|
|
63
|
+
|
|
64
|
+
**遇到上面的报错怎么办**(按顺序):
|
|
65
|
+
|
|
66
|
+
1. 打开 `<DSH 安装目录>/dsh/profiles/<profile>/cordis.patch.yml`,找到这样一段(注释行 + `insert` 三行)并**整段删掉**:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
# dsh-bottom-cost: ...
|
|
70
|
+
- insert:
|
|
71
|
+
- id: ui-session-cost
|
|
72
|
+
name: 'dsh-bottom-cost'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
2. **完全退出 DSH 再启动**(不是刷新页面);
|
|
76
|
+
3. 回到「插件管理」,这时停用与卸载都能正常点;
|
|
77
|
+
4. 若改报「当前 profile 的 patch 无法唯一定位这一项」,说明还有第二处声明——把该 profile 的 `cordis.patch.yml` 内容发我,我按行给处置。
|
|
78
|
+
|
|
79
|
+
**一行自查**(在 `<DSH 安装目录>/dsh/profiles/<profile>/` 下):
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
grep -n "dsh-bottom-cost" cordis.patch.yml package.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`package.json` 里有它 = 插件管理装的(正常);**`cordis.patch.yml` 里也有它 = 两层声明**,就是上面那个问题。
|
|
86
|
+
|
|
87
|
+
## 怎么读这个界面
|
|
88
|
+
|
|
89
|
+
看截图:最下面那一行就是**输入框下方的统计栏**——`◎ ¥5.294 · ¥0.9230` 是本插件,它与内置的 `15 轮 388 步 · 228 tok/s`(会话统计)、`147M tok · 缓存命中 99%`(Token 用量)以及最右边的上下文占用环排在同一行;上面那块是**展开后的面板**。
|
|
90
|
+
|
|
91
|
+
点面板里的**第 N 轮**会跳到对话中的那一轮:它只读地驱动官方右侧那颗「轮次导航」轨道——已加载的轮次直接落位,**还没加载的轮次由官方先取回更早的分页再落位**。定位不到时,面板底部会给出说明,不会静默失败。
|
|
92
|
+
|
|
93
|
+
每一轮的**轮次号右侧**会显示该轮用的是哪个模型(`v4-pro`、`v4-flash`……,只削掉 `deepseek-` 前缀;一轮里混用了多个就并列显示,悬停可看完整 id)。
|
|
94
|
+
|
|
95
|
+
标题行最右边的**小太阳(或月亮)**是明暗开关:它**只切换本插件自己的内容**——面板与底部那颗费用胶囊都会立刻换成另一套配色,应用的全局主题不受影响。图标表示当前模式(太阳=浅色、月亮=深色),配色取自官方主题的两套定义,所以两种模式下都与 DSH 其他界面同一套颜色,只是与本插件之外的界面相反。胶囊切到与外部相反时,会自动带上一层主题底色——否则近白的字落在浅色的统计栏上会看不清。
|
|
96
|
+
|
|
97
|
+
**统计栏里的胶囊**:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
◎ ¥2.922 · ¥0.4877
|
|
101
|
+
↑ 左 ↑ 右
|
|
102
|
+
当前会话的总花费 本轮(最近一轮)的花费
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- **左边**是这段对话累计花了多少;**右边**是最近一轮花了多少。两个数字都随对话推进实时更新。
|
|
106
|
+
- 花费为零、或这段对话还没有产生过计费轮次时,胶囊不显示——与内置统计胶囊「无内容即不渲染」的行为一致。
|
|
107
|
+
|
|
108
|
+
**点一下胶囊,面板贴着你向上展开**:
|
|
109
|
+
|
|
110
|
+
- **顶部固定区**:整段对话的**总花费**按「命中缓存」与「未命中」拆成两笔(截图里的 `¥1.609` 与 `¥1.313`)。这两个数加起来永远等于胶囊左边那个总额——`总 = 命中 + 未命中` 是构造上成立的,不是四舍五入碰巧对上的。
|
|
111
|
+
- **下面的滚动列表**:**每一轮**的花费,每轮三行——**总费用 / 命中 / 未命中**(截图里的第 7、8 轮)。默认停在最新一轮,**向上滚**即可依次回看更早的轮次。
|
|
112
|
+
- **滚动区高度**恰好是两块轮次——你要看的就是「最近这一轮」和「上一轮」,不需要为此把面板拉得很长。
|
|
113
|
+
|
|
114
|
+
**右上角那颗「刷新」**
|
|
115
|
+
|
|
116
|
+
它管的是**计价用的价目表**,不是重新统计你的对话:点击后宿主去抓**官方中文价目页**,解析出 12 个单价并逐项校验,然后**把差异先列给你看**(哪一项从多少变成多少)——你点「采用」才真正生效。这么设计是因为 DeepSeek 没有机读价目 API,只有人读文档页,解析天生可能出错;一次静默的错误解析会污染你看到的每一个金额。
|
|
117
|
+
|
|
118
|
+
面板第二行那条「价目:官方页 · 10/13 02:17」就是在告诉你**当前用的是哪一份价目、什么时候抓的**;显示「价目:内置表」则表示还没抓过,用的是随插件发布的内置表。
|
|
119
|
+
|
|
120
|
+
**要不要经常点它?** 不必。官方调价时点一次即可;点完采用后,**这段对话的全部历史花费会立刻按新价重算**(因为金额是浏览器按当前价目表现算的,历史里只存 token 事实),不会出现新旧两代价格混在一本账里的情况。
|
|
121
|
+
|
|
122
|
+
## 数字是怎么来的
|
|
123
|
+
|
|
124
|
+
只有 **DeepSeek 官方路由**有公开价目表,所以只有它们会产生金额:
|
|
125
|
+
|
|
126
|
+
| | |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| **命中费用** | 缓存读 tokens × 命中价 |
|
|
129
|
+
| **未命中费用** | (未命中输入 + 缓存写)× 未命中输入价 **+** 输出 tokens × 输出价 |
|
|
130
|
+
| **峰谷** | 北京时间周一至周五 09:00-12:00、14:00-18:00 为高峰(双倍价);周末与中国法定节假日全天空闲 |
|
|
131
|
+
| **按什么时刻计价** | 每次调用**发生那一刻**——取会话事件自带的时间戳,所以回放几个月前的历史也按**当时**的档位算,不会拿今天的价套旧账 |
|
|
132
|
+
| **其他 provider** | 绝不猜成 ¥0:显示为**未计价**,面板底部给出未计价的调用次数 |
|
|
133
|
+
|
|
134
|
+
统计范围是**当前会话自身的轮次**。子 agent 是另一个会话,刻意不并入,所以胶囊上的数字与你看到的轮次严格一一对应。
|
|
135
|
+
|
|
136
|
+
### 是估算,不是账单
|
|
137
|
+
|
|
138
|
+
金额由 token 数与公开价目推算。插件不读你的余额、不碰凭据,也无法得知 DeepSeek 实际扣了多少。
|
|
139
|
+
|
|
140
|
+
### `deepseek-v4-pro` 按它自己的官方价目计价
|
|
141
|
+
|
|
142
|
+
2026-09-14 曾计划下线 Pro,把它的请求按 V4.1-Flash 承接并计费;[`@linxin666/dsh-usage`](https://github.com/zhu1090093659/dsh-web) 据此设了一条时间栅栏,本插件早期也照抄了它(当时 README 里那段「档位存在争议」就是这么来的)。
|
|
143
|
+
|
|
144
|
+
**官方随后决定继续提供 Pro、计费方式不变**(Pro 严格按它自己的官方价目计费,与 Flash 分开)。因此 0.5.1 起**删掉了那条栅栏**:`deepseek-v4-pro` 的请求恒计在它自己那一行(未命中 4.5/9.0、输出 13.5/27.0),`按Flash档估` 的标记与汇总提醒也一并移除——它们的前提已不成立。
|
|
145
|
+
|
|
146
|
+
> 如果你从别处又看到"Pro 已被 Flash 承接"的说法,请注意那是 2026-09-14 之前的旧计划;不要再把栅栏加回来。
|
|
147
|
+
|
|
148
|
+
## 刷新价目表的完整流程
|
|
149
|
+
|
|
150
|
+
1. 宿主抓取**官方中文价目页**(`api-docs.deepseek.com/zh-cn/quick_start/pricing`,给人民币;英文页给美元,不用);
|
|
151
|
+
2. 解析那张表并校验全部 12 个单价——任何一个缺失或不合理,整份解析直接作废;
|
|
152
|
+
3. **把差异列给你看**,点 **采用** 才生效,也可以 **放弃**。
|
|
153
|
+
|
|
154
|
+
价目表只存在 `<DSH_HOME>/dsh-bottom-cost/pricebook.json`(原子写),重启后仍然生效;抓取失败(断网、超时、官网改版)会明确报出来并**保留当前价目**;首次启动的兜底始终是内置表。
|
|
155
|
+
|
|
156
|
+
### 能力边界
|
|
157
|
+
|
|
158
|
+
- **只有一次出网请求**:抓上面那个公开文档页,**且只在你点「刷新」时发生**。除此之外它不把任何东西发出去。
|
|
159
|
+
- **只写一个文件**:`<DSH_HOME>/dsh-bottom-cost/pricebook.json`。
|
|
160
|
+
- 三条价目路由都是**仅回环 + 同源**校验(与本工作区既有 DSH 插件同一套围栏)。
|
|
161
|
+
- 宿主半区不读凭据、不写会话数据、不装定时器。
|
|
162
|
+
- 面板里的**第 N 轮**是**只读地**驱动官方的轮次导航:读取刻度按钮的 `aria-label`(含轮号)与 `aria-current`,并调用刻度自己的点击;轨道整个不可用时回落到 transcript 行上的 `data-chat-turn` 锚点。本插件**不注入节点、不改官方布局**,也不写死任何构建哈希类名——但官方若改掉这些语义属性,跳转会失效,届时面板会如实提示定位失败。
|
|
163
|
+
|
|
164
|
+
### 为什么形状校验是手写的
|
|
165
|
+
|
|
166
|
+
DSH 的投影注册表向每个单元索取 `stateSchema` 与 `wire.viewSchema`,并且**只对它们调用 `.parse`**(`@deepseek-ai/dsh-session-projection` 里共 6 处;没有 `safeParse`、没有 `optional`、没有 `refine`)。这个契约是鸭子类型的,所以 `lib/shape.js` 只用约 90 行、零依赖地实现了本插件真正需要的七个谓词:object / string / 有限数字 / boolean / array / 枚举 / nullable。
|
|
167
|
+
|
|
168
|
+
另一条路是 `zod`(DSH 自己的 `dsh-token-meter` 就用它)。那条路不算错,但对本插件不划算:发布的包有 **840 个文件 / 5.86 MB**(哪怕只留 v4 ESM 运行时也有 111 个文件 / 886 KB),而本 README 首推的安装路径是**复制式安装、不带依赖**——要带上 zod 就只能往仓库里塞一大块第三方代码,或者让安装脚本联网跑 `pnpm` 去改写你 profile 的 `package.json`。手写让这条路径真正自足。
|
|
169
|
+
|
|
170
|
+
一处**刻意比 zod 更严**:`NaN` 与 `Infinity` 一律拒绝——它们一旦混进 token 或金额会静默污染累计值。另外,因为校验器是本仓库自己的代码,自测可以**直接测它**(zod 作为三方导入做不到这一点)。
|
|
171
|
+
|
|
172
|
+
## 实现要点
|
|
173
|
+
|
|
174
|
+
### 用官方扩展点,不做 DOM 注入
|
|
175
|
+
|
|
176
|
+
- 胶囊注册进内置 slot **`conversation.composer.dock`**(`order: 1`;随包发布的 `stats` 两颗胶囊是 `order: 0`)。该 slot *就是* 输入框下方那条统计栏,容器是 `display:flex; gap:12px` 的横向行,因此胶囊是内置胶囊的同级兄弟——不用 DOM 插桩,也不怕官方重渲染。
|
|
177
|
+
- 面板复用 `@deepseek-ai/dsh-client-ui-primitives` 的 `useAnchoredPosition` 与 `useDismissOnOutsidePointer`,与内置统计弹层同一套,所以定位、夹取、点击外部关闭、Esc 的行为完全一致。
|
|
178
|
+
- 样式只走主题 CSS 变量(`--dsw-*`),跟随你的主题而不是写死颜色。
|
|
179
|
+
|
|
180
|
+
### token 事实通过会话投影传输
|
|
181
|
+
|
|
182
|
+
宿主注册一个**会话投影单元**(`sessionCost`),浏览器用 `useProjection('sessionCost')` 读。这白拿三件事:
|
|
183
|
+
|
|
184
|
+
- **历史回填**:投影注册表会在首次触碰时把后注册的单元在内存日志上回放一遍,所以装上插件或重启 DSH 之后,当前对话里**已经发生**的轮次也被折进来——胶囊不会从 ¥0 开始。
|
|
185
|
+
- **持久化**:状态搭 harness 的投影检查点缓存,插件自己不写数据文件。
|
|
186
|
+
- **推送而非轮询**:值走会话控制流下发,没有定时器,也没有拉取循环。
|
|
187
|
+
|
|
188
|
+
### 分层,便于脱离 DSH 自测
|
|
189
|
+
|
|
190
|
+
| 文件 | 职责 |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `lib/fold.js` | 纯折叠:会话事件 → 按轮 token 事实。零依赖。 |
|
|
193
|
+
| `lib/pricing.js` | 纯计价:峰谷时钟、模型→价目行判定、tokens→金额。零依赖。 |
|
|
194
|
+
| `lib/pricebook.js` | 纯价目书:内置表、严格校验、官方页解析器。零依赖。 |
|
|
195
|
+
| `lib/projection.js` | 薄适配:形状 schema + 客户端视图。 |
|
|
196
|
+
| `lib/pricebook-store.js` | 抓取、暂存候选、采用、落盘。 |
|
|
197
|
+
| `lib/routes.js`、`lib/guard.js` | 仅回环的 HTTP 翻译层。 |
|
|
198
|
+
| `lib/client.js` | 胶囊与面板;按「事实 × 当前价目书」算钱。 |
|
|
199
|
+
|
|
200
|
+
## 自测
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
npm test # node tools/selftest.mjs —— 66 项,不联网
|
|
204
|
+
npm run test:live # node tools/live-check.mjs —— 抓真页面、解析、落盘、读回
|
|
205
|
+
npm run test:schema # node tools/schema-check.mjs —— schema ↔ 状态 ↔ 视图 一致性(从已安装副本导入)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`selftest` 覆盖峰谷与节假日例外、解析器(对着随仓库附带的官方页快照)、价目书校验、换一本价目书重算、按桶聚合、重放幂等、状态 JSON 安全、明细裁剪、以及客户端文案键集合。
|
|
209
|
+
|
|
210
|
+
`selftest` 覆盖峰谷与节假日例外、解析器(对着随仓库附带的官方页快照)、价目书校验、换一本价目书重算、按桶聚合、重放幂等、状态 JSON 安全、明细裁剪、客户端文案键集合,以及——因为校验器现在是本仓库自己的代码——**投影 schema 本身**(未声明字段被丢弃、类型不符按路径报错、`NaN` 被拒)。
|
|
211
|
+
|
|
212
|
+
`test:schema` 是另一种检查:它导入**profile 里实际装着的那份副本**,把 状态 → 视图 → schema 整条走一遍,因此同时验证「装上的文件能加载」与「`viewOf()` 与视图 schema 仍逐字段一致」。后者在开发中真的挂过一次,现在有专门的守卫。
|
|
213
|
+
|
|
214
|
+
## 兼容性
|
|
215
|
+
|
|
216
|
+
- DSH **≥ 0.2.0-rc.2**(开发与验证均在 0.2.0-rc.2 上完成)。
|
|
217
|
+
- Node **≥ 20**。
|
|
218
|
+
- **零运行时依赖**:投影 schema 由插件自带的 `lib/shape.js` 校验。
|
|
219
|
+
|
|
220
|
+
## 许可
|
|
221
|
+
|
|
222
|
+
[MIT](LICENSE) © 2026 LanFeng97。
|
|
223
|
+
|
|
224
|
+
有两处不是本插件原创,保留其上游署名:
|
|
225
|
+
|
|
226
|
+
- `lib/holidays.js` —— 中国法定节假日日期,来自 [bastengao/chinese-holidays-node](https://github.com/bastengao/chinese-holidays-node)(MIT)。
|
|
227
|
+
- `lib/pricing.js` 的峰谷规则与价目行判定、`lib/pricebook.js` 的内置表 —— 与 [@linxin666/dsh-usage](https://github.com/zhu1090093659/dsh-web)(Apache-2.0)同一套口径。
|
|
228
|
+
|
|
229
|
+
### 零运行时耦合
|
|
230
|
+
|
|
231
|
+
上面两处只是**署名**:规则与数据是**抄进本插件源码的**,本插件**不 `import` dsh-usage,也不 `import` 任何 `@linxin666/*` 包**。把 dsh-usage 卸载掉、或它升级/改名,本插件照常工作——反过来说,官方调价时这两份**副本**要各自更新(这就是「同口径」想表达的意思)。
|
|
232
|
+
|
|
233
|
+
**零运行时依赖。** 没有要顺带安装的东西,也没有要跟进同步的东西:投影 schema 来自插件自带的 `lib/shape.js`(理由见上文「为什么形状校验是手写的」)。
|
|
234
|
+
|
|
235
|
+
## 相关
|
|
236
|
+
|
|
237
|
+
- [dsh-workspace-create-dialog](https://github.com/LanFeng97/dsh-workspace-create-dialog) —— 同一作者的姊妹插件,做「创建工作区」对话框。
|