# dsh-meter 变更记录(CHANGELOG) > 本文档由 `PRD.md` §10「迭代记录」拆分而来,记录每个版本的具体变更与决策理由。 > 当前有效规格见 [PRD.md](PRD.md);新的版本条目请追加在本文档**最上方**(最新在前)。 --- ### v0.3.31(子代理状态点对齐官方 `StateDot` 语义:旋转环=运行中、实心绿点=已结束、中性点=历史冷会话) v0.3.30 的卡片「子代理」小节用自绘状态点(绿=运行中、空心=历史),与官方 `ui-subagent` 的 `StateDot` 语义恰好相反——官方语言里「绿」= 已结束。用户会按官方习惯读错状态,本版把卡片的状态语言换成官方的**同一个组件**。 - **映射(新增 `src/client/subagent-dot.ts`,纯函数)**:`running → ongoing`(旋转环:进行中)、`inactive → done`(实心绿点:轮次已闭合)、`cold → idle`(中性点:冷会话的 live 日志已消失,完成与否**不可知**,绝不冒充 done)。独立成模块只为可测试——`StateDotState` 与 activity 两侧都只做类型导入。 - **每行状态点**:自绘小圆点替换为官方 `StateDot` 组件,套进固定 14px 槽位(对齐官方行的 `rowActivitySlot`:宽旋转环与窄实心点共一条光学轴,行名不错位);`role="img"` + 原 aria-label 保留。`.subsDot/.subsDotOff/.subsDotCold` 三个类合并为 `.subsActivity`,插件不再自绘任何状态色。 - **头部「运行中 N」**:`●` 字形换成官方 `StateDot` 旋转环,计数文字承载语义(dot 为 aria-hidden),两者不会对「运行中」长得不一致。`.subsRunning` 不再涂成功绿(官方语义里绿=已结束),改 tertiary 文字色 + inline-flex;`.subsHead` 对齐从 baseline 改 center。 - **测试**:`pure-check` 新增三条映射断言(running→ongoing / inactive→done / cold→idle)。 - 文档:v0.3.30 小节内「卡片子代理小节」描述同步为官方语义并挂 [subagent-dot.ts](../../src/client/subagent-dot.ts) 链接。 验证:tsc 两套零错误、pnpm test 全 PASS。 --- ### v0.3.30(适配 DSH 0.2.0-rc.1;peer 区间一次放开到整条 0.x,之后 0.x 发版不再改号) DSH 桌面端升到 0.2.0-rc.1 后,内置的 17 个 `@deepseek-ai/dsh-*` 运行时包同步升到 0.2.0-rc.1,而 0.3.29 的 peer 还是 `^0.1.7-rc.2`(语义上只覆盖 <0.2.0),应用内插件管理器的安装前置校验(`evaluatePluginCompatibility`,逐条 peer 区间比对运行时版本、includePrerelease)直接拒绝加载 dsh-meter。 - **兼容性修复(根因)**:peerDependencies 的 16 个 `@deepseek-ai/dsh-*` 区间由 `^0.1.7-rc.2` 放开为 `>=0.1.7-rc.2 <1.0.0`。用应用内实际打包的 `evaluatePluginCompatibility` 对打包产物 manifest 跑 0.2.0-rc.1 → 兼容;对 0.3.29 的 manifest 能复现原始拒绝信息。`workspace:*` 之外这是 DSH 唯一承认的「面向未来的宽区间」写法(`workspace:^`/`~`/`*` 只在源码工作区安装时可用,独立包用不了)。 - **防抖(本版的主要目的)**:0.x 线内 DSH 每次发版(0.2.0-rc.1、未来的 0.3/0.4…0.9)不再需要跟着改 peer 号;`<1.0.0` 保留 1.0 这一道真实分界线,届时只需重新核对一次 API。仍然停留 0.1.7-rc.1 / 0.1.5-rc.2 的老环境会被区间正确拒绝,继续用 0.3.23 / 0.3.21。 - **dev 依赖统一升到 0.2.0-rc.1**(17 个,含 session-projection-cache),pnpm-workspace 的 `minimumReleaseAgeExclude` 全量追加 `0.2.0-rc.1`。 - **0.1.7-rc.2 → 0.2.0-rc.1 API 影响面已逐文件核对(type 层面零破坏)**:两版从 npm 各装一份,对插件用到的 16 个包做 .d.ts 全量 diff——dsh-session 仅新增 `ToolCallRecovery` 导出、dsh-api-remotes 新增一个 type re-export、dsh-client-ui-primitives 新增 `pointerModality`、dsh-client-ui-conversation 只动 composer/draft/input 内部(`conversation.session.header.actions`、`settings.plugin.item` 两个槽位契约逐字未变),其余包逐文件相同;`SESSION_FORMAT_VERSION` 仍是 4。`tsc` 两套零错误 + `pnpm build` + `pnpm smoke` A1–A8/B 全 PASS + `pnpm test` 全 PASS,与 0.3.29 相比代码零改动。 - **文档**:README 版本要求改写为「0.x 整线覆盖 + 1.0 需重核」;迁移小节里过时的「`0.1.0-rc.7`」更正为 `0.2.0-rc.1`。 --- ### v0.3.29(法定节假日日历:节假日不再按高峰计价) 上一版把"节假日未建模"写成了已知缺口。本版按讨论的方案 A 补上:**命名日历 + provider 显式 opt-in**。 - **内置 `cn` 日历**(2025–2026),来源是国务院办公厅两份通知([2025:国办发明电〔2024〕12号](https://www.gov.cn/zhengce/content/202411/content_6986382.htm)、[2026:国办发明电〔2025〕7号](https://www.gov.cn/zhengce/content/202511/content_7047090.htm))。DeepSeek 的两条路由默认挂 `calendar: cn`;**其它 provider 一律不预设** —— 网关的高峰窗口由其运营方决定,替它认定节假日是猜测。 - **只建模「放假日」,绝不建模「调休上班日」**:官方规则是「周一至周五(不含法定节假日)」,而 2026 年六个调休日**全部落在周末**,按规则本来就是空闲。用「工作日库」(chinese-days 之类)会把它们算成高峰,与官方规则相反 —— 这也是不引库、只用一份 ~60 行日期表的理由(其中只有 ~19 天/年是工作日)。 - **语义**:命中日历 = 该 **provider 时区下的本地日** 整天所有 `periods` 窗口不生效,按基准价(空闲)计;`EffectivePrice` 增 `holiday` 标志,只用于让 CLI/UI **显示原因**,价格口径不变(投影状态形状未变,无需升 stateVersion)。 - **性能**:判定复用已有的 `wallClock`(时区格式化器 + 分钟级缓存),本地日期在同一次 `formatToParts` 里取;日历编译成 `Set` 并用 WeakMap 按 `calendars` 对象缓存,所以折叠里每次请求只是「一次格式化 + 一次 Set 查找」,没有新增 Intl 开销。 - **格式**:顶层 `calendars:`(名字 → 日期列表),`providers..calendar: <名字>`。日期支持单日与闭区间,**加不加引号都接受**(YAML 会把未加引号的 `2026-10-01` 解析成日期对象 —— 这是写文件最容易踩的坑,格式显式接受并还原成写下的那天)。日期按 provider 时区判定,不是 UTC 切片。 - **合并语义**:`calendars` 同名取**并集**("追加新年份"天然安全,不会覆盖内置年份);`providers.` 改为**按字段合并** —— 否则文件里只改一个币种就会把内置的 `calendar` 冲掉、节假日静默失效。 - **可见性**(这个功能最大的风险是"日历过期后静默按高峰计价"):设置卡片与会话 catalog 报告日历名、日期数、覆盖区间/年份,并在**当前年份未覆盖**、**provider 引用了未声明的日历**、**有日期无法解析**时显示红字;host 启动日志同样逐条给出。CLI 增加 `--calendar` 报告与 `闲节` 标注(`--at <放假日>` 可直接验证),覆盖判断跟随 `--at` 的年份(所以"2027 覆盖了吗"是一条命令)。 - **测试**:`HOLIDAY CALENDAR CHECK`(国庆/春节整段、**调休上班的周六必须仍是空闲且不得标成节假日**、北京午夜前后各半小时的本地日期判定含同一时刻的 UTC 写法、2025 通知生效、2027 未覆盖按高峰、未 opt-in 的 provider 不受影响、区间展开/跨年/YAML Date/非法日期/倒序区间、覆盖统计不把区间串当日期);文件格式新增 13 组(未加引号/引号两种写法、悬空 calendar 引用必须拒收、非法与倒序日期必须拒收、未被引用的日历允许、并集与字段级合并);冒烟新增 A6/A7(文件日历经**已构建宿主**让国庆按空闲计价并被 catalog 报告;内置日历只作用于 DeepSeek 路由,自带高峰窗口的网关在节假日仍按高峰)。 - 文档:[docs/CONFIGURING.md](CONFIGURING.md) 新增「节假日日历」一节(写法、语义、调休陷阱、更新配方与自检步骤),并修正原先"节假日未建模"的反模式条目。 --- ### v0.3.28(价格表配置文件 + 自检 CLI + 冒烟测试) 目标:别的用户给 Agent 一张截图、一个链接,就能直接把价格写进配置,不必在设置页一格格点;并把"冒烟 + 测试用例"补齐。 - **新增 Agent 可写的价格文件**:默认 `$DSH_HOME/dsh-meter/prices.yaml`(入口配置 `priceFile` 可改;支持绝对路径、`~/`、相对 DSH home)。格式 YAML/JSON,**正文就是价格表本身**;单位是**元/百万 tokens**(与价目页、设置页同一个数字),内部 1e-5 整数单位刻意不暴露——任何价格 > 100000/M 判为"抄了内部单位",**整个文件拒收**。 - **三层优先级:显式配置(profile patch / 设置页保存)> 价格文件 > 内置默认**,按行(provider+model+effort)生效。"显式"取自 `settings.describe()` 的 `base`/`user` 两个原始层——resolved 配置分不出用户的航和内置默认行。于是:Agent 写的文件能覆盖内置默认,却不会被一次 GUI 保存悄悄吃掉;显式的行也不会被文件悄悄改价。设置卡片会显示「价格表来自配置文件 … · N 行生效 · M 行被显式配置覆盖」,不再有隐形来源。 - **坏文件不拖垮插件**:加载永不抛异常;任何错误 → 整文件忽略、回退内置/显式表、写日志(`billing: price file ignored — …`)、设置卡片显示红字。拒收条件:单位超限、重复行、非法 IANA 时区、空表、字段缺失/越界、YAML 语法错、顶层出现 `id`/`name`/`config`(贴了 patch 条目)。 - **改文件不用重启**:刷新路由重读文件;**并且**价格真变了时重挂投影,让 `stateVersion` 带上新表 revision——手工编辑文件不会触发 `settings/document-updated`,不重挂就可能让按旧价折出的 checkpoint 复活,这正是 revision 要防的事。 - **自检 CLI** `scripts/check-prices.mjs`(bin:`dsh-meter-prices`):用**插件同一个加载器**校验,并打印每行在周一 10:00 / 13:00 / 15:00 / 周六 10:00(北京时间)的有效价,便于把抄来的数字逐格对回页面;支持 `--json` / `--at`,退出码 0 通过 / 1 文件有问题 / 2 跑不起来。 - **配置说明** [docs/CONFIGURING.md](CONFIGURING.md):格式规范(字段、单位、上限保护、`tiers` 下标对齐)、三个配方(给链接 / 给截图 / 只给模型名,含查路由 id 的 catalog 命令)、强制自检、回报模板、反模式(不发明价格、不平均、不改 patch、**节假日未建模**要如实说明)、排错表、计价口径。附可运行示例 [docs/examples/prices.deepseek.yaml](examples/prices.deepseek.yaml)。 - **测试**(`pnpm verify` = typecheck + build + smoke + test): - [tests/price-file-check.ts](../tests/price-file-check.ts)(11 组):官方价换算与精度、JSON 输入、**原始单位整行拒收**(含只有大字段超限时才暴露的行)、patch 条目误贴、缺字段/越界/语法错/空表、重复行与未声明 provider、时区拼错、路径解析(默认/DSH_HOME/`~`/相对)、合并语义、ENOENT 非错误。 - [tests/smoke.ts](../tests/smoke.ts)(跑**已构建产物**):宿主 bundle 加载 → 注册 `/billing/api` 与 `billing` 投影 → **真跑一次 1M token 请求**验证三层计价、坏文件降级、文件编辑后由运行中的投影计新价且投影被重挂;客户端 bundle 走 `window.__ModuleLoader__` 交接、声明 `inject`、注册两个槽位。 - 顺带修掉测试自身一个**恒真断言**(`typeof x, 'function' || …` 逗号写错,被 `tsc` 抓到)——正好说明为什么不能只靠"看起来通过"。 --- ### v0.3.27(设置页 provider 头部重排:时区改可搜索下拉,字段一行对齐) 反馈是"错落的排版确实不太好看 / 放有用有必要的"。本版按这个口径重排 provider 头部,同时把时区做成你要的**可输入关键词筛选的下拉**。 - **时区:自由文本框 → 关键词可筛选下拉**。选项取自引擎自己的 `Intl.supportedValuesOf('timeZone')`(本机 418 个)——不手维护时区表,就不会随 tzdata 悄悄漂移。过滤匹配 IANA 名**或城市名**、段首命中优先:输入 `shanghai` 直接命中 `Asia/Shanghai`,输入 `asia/` 列出该区域。键盘 ↑/↓/Enter/Esc 可用;自由文本仍能失焦或回车提交,非法名照旧报错——下拉是便利,不是校验闸门。引擎没有 `supportedValuesOf` 时退化为纯文本框,校验不变。 - **筛选是纯函数**([timezones.ts](src/client/timezones.ts) 的 `matchTimezones()`),新增 `TIMEZONE COMBOBOX CHECK`:城市关键词、大小写、首尾空格、区域前缀、无匹配、limit、段首优先,以及引擎列表本身(含默认时区、每个都能被 `Intl` 接受、每个都带城市标签)。选择器的筛选悄悄失效比纯文本框更糟——用户根本不知道为什么不出现选项。 - **头部重排**:原来是右对齐的一摞——label 在输入框上方的时区列 + 内联 label 的币种 + 一行裸提示 + 一条孤零零的按钮行,三种垂直节奏互不对齐。现在 = **标题行**(名称/展开箭头在左,provider 级动作「按官方规则配置」在右)+ **一行字段**(时区 · 币种 · 单位,同一基线、左对齐,窄了才换行)。单位不再是裸提示,而是与币种并列的带标签字段;按钮从字段流里移出,不再制造第四种节奏。 - 组合框浮层:绝对定位、`--billing-shadow` 阴影、hover/键盘高亮,`role="listbox"`/`role="option"` + `aria-expanded`/`aria-controls`/`aria-selected`/`aria-autocomplete`,选中用 `mousedown`(否则输入框 blur 先关列表)。设置面板内容区是 `overflow-y: auto`,列表在面板底缘会被裁并随滚动露出——普通内联组合框的固有行为,已在代码注释写明。 验证:`tsc` 两套零错误、`pnpm build` 通过、`pnpm test` 28 组全 PASS;用本机 Chrome 无头 + DSH 自己的 `design-platform.css` 令牌按真实尺寸渲染布局截图(改前/改后各一张),并核对了面板实际宽度(800px,卡片内容区约 700px)下字段确实一行放得下。 --- ### v0.3.26(内置默认价格表补齐 DeepSeek 官方两条路由;修复设置页「应用」按钮) **价格:按官方价目表(api-docs.deepseek.com/zh-cn/quick_start/pricing,2026-09)** - 新增 `deepseek-official`(API key 路由)与 `deepseek-account`(登录账号路由)两组默认价——同一上游服务、同一价目表,因此同价: - `deepseek-flash`(V4.1-Flash,1M 上下文 / 最大 384K 输出):空闲 **1/4/0.02**、高峰 **2/8/0.04**(元/百万 tokens) - `deepseek-v4-pro`(V4-Pro-0813):空闲 **4.5/13.5/0.15**、高峰 **9/27/0.30** - 高峰 = 北京时间工作日 09:00–12:00 与 14:00–18:00;空闲价为高峰价的一半;缓存写入不单独计费(0) - 两个**已下线但仍在服务**的旧模型名 `deepseek-v4-flash`、`deepseek-v4-flash-vision-exp` 按官方说明(仍可调用、由 V4.1-Flash 提供服务并按 Flash 价计费)建行——否则记录这些 id 的会话会显示「未登记价格」。 - **未建模法定节假日**:官方高峰定义是「周一至周五(**不含中国法定节假日**)」,插件没有节假日日历,节假日会按高峰计价。这是已知缺口,已写进模块注释而非悄悄放过。 - 官方窗口常量收敛为 `shared.ts` 的 `DEEPSEEK_PEAK_WINDOWS`,内置价格表与设置页的官方规则预设共用一份,两处对"官方"的定义不会再漂移。 **修复设置页的「应用」按钮(三处,都与它读起来的样子不符)** - **只有第一个 provider 有按钮**:官方规则预设原来按 `provider.id === 'deepseek-official'` 精确判断,于是同族的 `deepseek-account` 没有它。两个 DeepSeek 路由并排、只有一个带按钮,看起来就是 UI 坏了。改为按 DeepSeek 路由族(`DEEPSEEK_PROVIDER_IDS`)。 - **它根本不是时区的按钮**:按钮原来贴在时区输入框右边,标签只有一个「应用」,于是被读成"时区要按应用才生效、而且只有第一个 provider 有"——时区本来是失焦即提交。现在移到独立一行,标签改用早已存在却从未被使用的「按官方规则配置」,说明文案写清它保留已有窗口、只在没有时才补。 - **会毁配置的 bug**:预设原来只保留**第一个**工作日窗口、把其余工作日窗口全部丢弃。DeepSeek 官方规则是两段(09–12、14–18),所以在配置正确的行上点一下「应用」会**静默删掉下午高峰**、把高峰覆盖砍半——这是价格变更,不是整理配置。改为保留**全部**工作日窗口(只补全星期掩码),一个工作日窗口都没有时才按官方两段补种(价格取该模型的空闲价,绝不擅自改价)。 - 纯逻辑抽到 [preset.ts](src/client/preset.ts) 的 `officialPeakPeriods()`,新增 `OFFICIAL-RULE PRESET CHECK`(6 组)锁定行为,含"两段官方窗口必须都留下"的回归与"输入不被修改"。 **本机 profile 同步(仓库外,不进版本库)**:desktop / web 两个 `cordis.patch.yml` 的 billing 配置里,`deepseek-official` 原来的 `deepseek-v4-flash` 行是 1.5/4.5/0.05(与现行官方价不符,model id 也是旧名),已换成官方四行;并补上 `deepseek-account` provider 与其四行。改前已备份,改后逐字段校验:其余 entry 与 30 行非 DeepSeek 价格**完全未变**。 --- ### v0.3.25(修复「设置」按钮点错控件;齿轮真正自动跳到 设置→内置插件→计费价格) 0.1.7-rc.2 给设置面板的 `sidebar.settings` 槽位加了 **store seat**(rc.1 没有),seat 的 `openSection(id)` 能开面板并直接选中分区——这正是"自动跳转"一直缺的那块拼图。顺手把齿轮点错控件的老 bug 一起修了。 - **修复(真 bug,非 rc.2 回归)**:旧实现取 `[data-slot="sidebar.settings"]` 里的**第一个 `