# Changelog All notable changes to `@skkjkk/dsh-usage-dashboard` are documented here. ## [0.3.22] - 2026-10-15 ### CI 修复:peer 下限抬得过高导致 `npm ci` ERESOLVE > 0.3.20 把 peer 下限从 `>=0.1.0-rc.6` 抬到 `>=0.1.3-alpha.2`(想与 COMPAT 的实测支持面严格对齐)。 > 代价是 **CI 的第一步 `npm ci` 直接 ERESOLVE,连续两次失败**:`package-lock.json` 里实际解析到的 > `@deepseek-ai/dsh-client-*` 是 **`0.1.0-rc.8`**,而新下限把这一档预发布排除了。本地 `npm install` > 因 node_modules 已存在而不复现 → 典型的「本地绿、CI 红」。 - **范围修正**:`>=0.1.0-rc.6 <0.3.0 || ^0.1.7-rc.1 || ^0.2.0-rc.1`。下限回到原值;两条在用预发布线保留—— 它们才是 G1 真正要解决的问题(旧范围连 npm `latest` 与桌面端 `0.2.0-rc.1` 都不匹配)。 - **护栏 1**:`test/peer-range.test.js` 新增一条**读 `package-lock.json` 的断言**——锁文件里每个 `node_modules/@deepseek-ai/dsh-client-*` 的版本都必须满足 peer 范围。抬下限时它会先失败,而不是等 CI。 semver 矩阵同步更新(`0.1.0-rc.6/rc.8` 必须匹配、`0.1.2` 回到匹配集、`0.1.3-alpha.2` 明确记为不匹配)。 - **护栏 2**:`test/render-snapshot.test.js` 读快照时把 CRLF 归一成 LF。干净 worktree 里 `core.autocrlf=true` 会把检出的快照变成 CRLF,而渲染产物恒为 LF → 快照比较恒假(CI 跑 Ubuntu、不受影响, 但在 Windows 上排查会误判成真实漂移)。 - **未改任何取价/折叠语义**:`CACHE_FORMAT_VERSION` 仍为 20。 - **验证**:在干净 `git worktree` 里逐条复现 CI(`npm ci` → build → 漂移检查 → 单测 → bench → smoke → client-check → verify-pack)全部通过;`package-lock.json` 根版本同步。 ## [0.3.21] - 2026-10-15 ### 定价修正(剩余任务 C3)· `CACHE_FORMAT_VERSION` 19 → 20 > 三处都以**官方页原文**核实后修改(不是第三方目录)。历史成本随之变化,因此磁盘缓存里的 > rollup 必须作废重算——这就是 bump 版本号的原因。逐行出处见 > [pricing/PRICING_VERIFICATION_REPORT.md](pricing/PRICING_VERIFICATION_REPORT.md) §4.5。 - **`ernie-4.5-turbo` 输入/输出:0.56 / 2.24 → 0.8 / 3.2**(缓存读取 0.2 不变)。千帆官方价格页 (2026-07-09 更新)的在线推理价是输入 `0.0008 元/千tokens`、输出 `0.0032`、命中缓存 `0.0002` → ¥0.8 / ¥3.2 / ¥0.2。原值是整行的**7 折**(0.8×0.7、3.2×0.7),即已过期的促销档;原缓存价 0.2 恰好等于官方输入价的 25%,本身就在反证输入价应为 0.8。 - **`gemini-1.5-pro`:24.5 / 73.5 / 6.125 → 8.75 / 35 / 2.1875**。Google 官方归档价格页的 Pay-as-you-go「prompts up to 128k」档是 input $1.25 / output $5.00 / context caching $0.3125 (×7)。原值 24.5/73.5 是**发布期档位**($3.50/$10.50)。>128k 档($2.50/$10.00)不落表: 插件拿不到 prompt 长度分布、阶梯计价未实现(见 AGENTS tripwire),取基础档并在报告记明。 - **`agnes-3.0-flash`:0.35 / 1 / 0.035 → 0 / 0 / 0**。Sapiens 官方 wiki 的 `agnes-3.0-flash` 三列 **Current price** 全为 `$0 / M`(list price 已划掉),并写明「cached input / input / output are currently free for agnes-2.5-flash and agnes-3.0-flash」——与同表 `agnes-2.0-flash` / `agnes-2.5-flash`(0/0/0)口径一致。**这是显式 0(已知免费)**,不进 `CACHE_PRICE_UNKNOWN`, 不会误报「费用可能低估」。 - **未改:Bedrock Nova 的输入价**(理由与探针记录见报告 §4.5)。AWS 官方页把 Nova 分成 Global 跨区与 in-region 两张价表,而本仓库 §2.8 的「官方」读法与 C3 复核的读法互相矛盾;官方价格清单 JSON 的 key 是哈希、`sets` 为空,无法映射型号。在拿到可核对的档位前不动——现有五行 Nova 内部 自洽(缓存 = 输入 × 25%),只有 nova-pro / nova-premier 在两种读法下都一致。 - **守卫**:`test/pricing-cache-unknown.test.js` 的三条断言改为新值(含「免费是已知的 0、不算未知」), bench `[0l]` 的 `scopedPos.agnes3` 同步改为 0 并加了一条输入价断言。`npm test` 全绿 (58 条单测 / bench 全对拍 / smoke 14 条 / client-check 110 条),`verify-pack` 通过。 ## [0.3.20] - 2026-10-15 > 本版 = 0.3.19 之后累积的全部未发布内容(B1/B2 隐私与截图、C1/C2 定价、D1–D3 测试基建、 > E1–E4 脚本债、F1–F5 低优先项、G1/G2 兼容性核验)。同步补打 `v0.3.14` ~ `v0.3.20` 的 tag, > 并把 0.3.14 之后的版本首次发布到 npm(此前 npm `latest` 停在 0.3.13)。 ### 遗留低优先项(剩余任务 F1–F5)与兼容性核验(G1 / G2) > 2026-10-15,随 **0.3.20** 发布。**未 bump `CACHE_FORMAT_VERSION`**(v19 不变:这轮改动没有触碰取价/折叠语义, > 磁盘缓存里的 rollup 与成本数值不受影响)。 > `npm test` 全绿(58 条单测 / bench 全对拍 / smoke 14 条 / client-check 110 条),`verify-pack` 通过。 - **F1 `Date.UTC` 的 0–99 年映射**(`src/core/rollup.js`):`Date.UTC(year, …)` 把 0–99 年映射成 1900+年,于是 `?range=custom&from=<公元 1 年>` 会被**静默截到 1901 年**——桶键全错、桶数远低于 上限,回 200 空结果而不是报错。新增 `utcKey()`:年份 ≥ 100 走 `Date.UTC` 快路径(`hourOf` 在折叠 每个事件时都会被调用,热路径不能多付一次对象分配),更早/负数年份改用 `setUTCFullYear` 显式构造。 桶键恢复正确后,「公元 1 年 → 现在」这种窗口会**真的**超出 `MAX_SERIES_BUCKETS` 而抛 `RangeError` → 路由回 **400 `bad_request`**(语义更清楚,也不再有静默错误数据)。守卫:`test/engine-contracts.test.js` 的 F1 用例 + smoke 新增 `year<100 custom window`;**反证已验**(换回 `Date.UTC` 立即 smoke/单测双红)。 活实例实测:`from=0001-01-01` → `400 {"error":"bad_request","message":"trend range exceeds 50000 buckets"}`。 - **F2 导出阻塞宿主线程**(`src/core/rollup.js` + `src/host.js`):聚合主体下沉为引擎的 `createDetailAggregator(req)`(两阶段 `addRange(rollups, from, to)` / `finish()`;`queryDetail` 与 `queryDetailAll` 改成调用同一个聚合器,一批加到底),宿主导出时**每 25 个会话让出一次事件循环**, 于是长导出只是「慢」而不再在整个聚合期间占满宿主线程。另加行数安全阀 `EXPORT_MAX_ROWS = 100000` + `assertExportRows()`:超限抛 `RangeError` → 400 而不是静默截断(导出丢行比报错更糟)。守卫: 单测「1/2/3/7/整批切分 == 一次取全」「恰好在上限不抛、超一行抛」。**未做**流式响应(该路由模板是 一次性 `JSON.stringify` 返回,改造面远大于收益)。 - **F3 旧缓存文件权限不会随升级收紧**(`src/core/cache-perms.js` + `src/host.js`):`mkdir`/`writeFile` 的 mode 只作用于**新建**对象,旧版本以默认 umask 写下的 `.json` 会一直保持 0644。`initStore` 读到缓存目录清单后先按 0600 统一收紧再读取(单个失败不影响其余;权限收紧失败绝不导致丢缓存)。 守卫:`test/core-cache-perms.test.js`(**任何平台**都断言按 0600 调用了每个 `.json`,因为 Windows 的 chmod 只切只读位、smoke 的 mode 断言在 Windows 上会被跳过)+ smoke 在 POSIX 上用 `stat` 断言真实 mode + smoke 断言宿主 bundle 真的调用了该函数。**反证已验**(去掉 `fsp.chmod` 那行 → 单测 4 红)。 - **F4 三路轮询**:`refreshSeq` 只在筛选变化时自增的方案会让「看着看板时数据不再自动更新」,与 SWR 的 设计意图相反,**不采纳**;改为按 `document.visibilityState === 'hidden'` 暂停 30s 轮询、重新可见时 立即补一次刷新(`src/client/hooks.js` 的纯函数 `isPollingPaused`)。守卫:`test/client-pure.test.js` (含「实现漏接线」检查:产物里必须有 `visibilitychange` listener)。 - **F5 `_seenUsage` 无界增长**:维持**拒绝**「按时间窗淘汰」「区分缺失与显式 0」两条子建议,但先把 内存量出来了——新增 `scripts/oneoff/measure-seen-usage.mjs`,本机 385 会话实测 **38,103 条签名 / 3.6 MB(97 B/条、9.4 KB/会话、最大 778 条/77 KB)**,同批 rollup 的 `evts` 明细是 174,931 条 / 10.2 MB:签名占 evts 的 **34.8%**、占缓存 JSON(14.5 MB)的 25%,与代码注释「与事件数同阶且远小于 evts」一致;按均值外推 10k 会话 ≈ 92 MB 签名,而同一量级的 rollup 本体 ~377 MB。定长哈希能压到 约 1/3,但要 bump 缓存格式、全量重算,并把「零碰撞」换成「极低概率碰撞 → 静默少计成本」,故不改。 - **G1 peer 范围的预发布语义**(`package.json` + `test/peer-range.test.js` + COMPAT.md):实测 (新增 devDep `semver`)确认旧范围 `>=0.1.0-rc.6 <0.3.0` **只匹配稳定版**——连当时的 npm `latest` (`0.1.7-rc.2`)与桌面端 `0.2.0-rc.1` 都不匹配。改为 `>=0.1.3-alpha.2 <0.3.0 || ^0.1.7-rc.1 || ^0.2.0-rc.1`(三个 DSH client 包统一):稳定带全覆盖 + 两条在用预发布线;下限取 COMPAT 验证过的最老版本。测试同时钉住**不可表达**的部分: `0.2.1-rc.1` 这类「未来补丁号的预发布」对任何范围都为 false(8 种候选写法反证,只有 `includePrerelease: true` 才行,而 `peerDependencies` 表达不了;npm 的 peer 校验传的是 loose)。 - **G2 在 DSH 0.2.x 上真实回归**(比要求的基线更新:本机运行实例是 **0.2.0-rc.2**,六个相关子包版本 从运行实例 `app.asar` 读出,`dsh-client-runtime` 确认不存在)。宿主半边:`dev_reload_package` 把新 构建装进活实例后,四条 `/dash-api/*` 全部通过(7d:49 会话 / 12.4 亿 token / 覆盖率 99% / 未匹配 0, 冷 428 ms、热 6–24 ms;导出 30d/90d 8 列)。客户端半边:新增 `scripts/oneoff/verify-client-live.mjs`,把 `lib/client.js` 按运行实例的加载契约装进真实 Chromium、 页面同源代理到活宿主,**23 条断言全绿**(槽注册 name/order/label、样式注入、10 KPI / 21 SVG / 280 日历格 / 20 记录行、无 pageerror / console error / 4xx、点真实按钮导出 CSV 58 行 8 列)。 未做到的一步如实记在 COMPAT.md:桌面端自己的窗口 URL 带 token,取不到,因此「设置壳把 section 画进侧栏」这一下没有自动断言。 - 工程侧:新增 core 模块 `src/core/cache-perms.js`(`verify-pack` 必需文件清单、构建产物守卫同步更新); smoke 由 12 条增至 14 条(`year<100 custom window`、`legacy cache mode 0600`);单测 47 → 58 条。 ### 截图匿名化(隐私) - **问题**:`scripts/capture-screenshots.mjs` 把从真实 `/dash-api/*` 抓的 fixture 逐字渲染,于是 `meta.projects[].title`(真实目录名)、`meta.projects[].id`(绝对 cwd 路径)、`meta.dist.projects[].label` 与 `detail.rows[].project` 都进了 PNG。OCR 在旧图里直接读出 `05-研究生-毕业论文`、`DeepSeek-harness`、`dsh-usage-dashboard` 等真实名。 - **修法**:脚本默认**在渲染前匿名化**——按最宽窗口(90d→today)的项目分布排序,把 44 个 id/title/label 映射为 `project-a`、`project-b`…;`detail.rows[].project` 存的是 **title**,因此按各窗口里的 id↔title 配对走同一映射。只有 `--raw` 才逐字渲染,且会打印"不要提交"的警告。 - **守卫(会真正失败)**:渲染完成、截图之前断言**页面画不出任何真实标识**——收集 body 下全部文本节点(SVG `` 在内,跳过注入的 `