# 变更记录 > 只记**实质变更**(行为、接口、规范),不记错别字与排版微调。 > 每条写清**为什么** —— 为什么比改了什么重要。 --- ## v1.5.1 · 2026-10-13 **纯内容更新:经验库 67 → 77 条(+10)。** 这轮没动代码,所以是补丁号。 ### 新增的 10 条(E68–E77) 前 9 条由**同一天另外几个会话**写入,只有 E77 是本次会话写的 —— 记在这里, 免得以后看这版记录的人误判是谁做的: - E68 服务方法"摘下来调用"丢了 `this`:注册静默失效,而假 ctx 让它永远测不出来 - E69 `dispose` 只 `clear()` 没 `close()`:SQLite 句柄不释放,Windows 上连临时目录都删不掉 - E70 改完规则别批量重算存量:脚本报"有差异"分不清改进与降级 - E71 换了个"存在"的主题 token,元素反而变白/透明 - E72 CSS 写在 JS 模板串里,注释里写了反引号 → 模板串被截断 - E73 拿静态截图判断"哪边是 4K":锐度高不等于分辨率高,先过配准这道闸 - E74 被质疑"前后不一致"时,先核对两次输入是不是同一个,再解释自己 - E75 双拼图判"哪边是 4K"的完整判据:先量尺子定偏移,再配对看 lap_var vs grad_mean - E76 断言绑了"表达式形状"而不是"行为":上游做正确重构就假红 - E77 `String.match()` 不带 `/g` 时返回的不是命中列表:1 处被算成 2 处,脚本据此拒绝写入 ### 数字同步 README / package.json 里的条数、指纹、全文体量已同步到头部的事实源 (**77 条 / 指纹 `78594924` / 全文 185 KB**)。以前是手改几处,现在由一条命令生成 —— 原因是实测到**手改永远在追移动靶**:另一个会话边写库,手改完 75 条、几分钟后库就到 76 条了。 那条命令还会**先断言命中数**,不对就不写文件(避免留下半新半旧的 README)。 --- ## v1.5.0 · 2026-10-12 **「开发诊断」从"给开发者看的诊断页"改成"先回答用户的问题,再把技术细节收起来"。** 起因是用户的一句反馈:文件清单、94 个服务、原始日志尾巴对他是噪音, "这次会话出了什么问题、最近改了什么为什么"才是他真正想看的。 ### 新增:这次会话出了什么问题(`session-digest.js` + `GET /dev-tools/session-digest`) **为什么**:面板原来显示的是**宿主日志**(插件自己说的话)。而用户想问的是 "这次会话里报了什么错、哪些操作失败了" —— 那份数据在**会话日志**里,不在宿主日志里。 **做法**:宿主读 `~/.dsh/sessions/<工作区>/session-/session.v4.jsonl.zstd`,三条判据 (都拿真日志实测过): - `turn/end` 且 `data.reason.kind === 'error'` → 整轮中断 + 错误原文 - `tool/result` 且 `data.message.isError === true` → 工具失败(用 `toolCallId` 关联出工具名) - `approval/decided` 且 `outcome` 含 denied → 审批被拒 **两个坑写进注释了**: - 会话日志是**多帧 zstd**,`zstdDecompressSync` 只解**第一帧** → 必须按魔数切帧, 否则**静默截断**(本会话踩过) - 判据**不能**看"输出正文里有没有 error / `"ok":false`" —— 工具输出里常有这类字面量, 那是假阳性(第一版探针 20 条里就混了误报) ### 新增:最近改了什么、为什么 从 **git** 读(不能从日志读:`workspace/changes` 只带 `{turn}`、`tool/call` 只带工具名, **都没有文件路径**):commit message 是唯一机器可读的"为什么";未提交的改动明确标注 **"这些没有记录原因"** —— 不编。会话的工作区目录从会话事件里的 `cwd` 取 (目录名 slug 是有损的,还原不出路径)。 ### 改:面板重排成"用户视角在前" ① 需要你做什么(只在"要重启"时成一条警告)→ ② 这次会话出了什么问题(默认 10 条、可展开) → ③ 最近改了什么·为什么 → ④ **技术细节(默认折叠)**:环境 / 文件哈希 / 服务 / 经验库 / 宿主日志。 ⚠ ④ **只折叠、不删除** —— 2026-10-12 抓出"部分生效"(E66)和假 token(E67)全靠那些数据。 ### 校验 新增 `verify-session-digest.mjs`(25 项),含两条"防假绿"的断言: "双帧 zstd 必须都解出来"(**带反证**:只解第一帧确实会漏)与 "正文里的 `"ok":false` 不能被误报成失败"。`verify-diagnostics-tab.mjs` 扩到 27 项; **元守卫**提醒新校验器必须登记进 `AGENTS.md`(已补,否则"写好了也不会再跑")。 --- ## v1.4.0 · 2026-10-12 一次**界面重做**:把「开发诊断」按 DSH 的设计语言重画,并顺手修掉一批 **一直在静默回退**的假 token。主线是同一件事:**界面上的每个颜色都该有出处。** ### 修复:7 处不存在的 token 名(浅色主题下边框是深灰) **为什么**:`BASE_CSS` 里写着 `var(--dsw-alias-border-primary, #2b3040)` —— 这个 token **不存在**(真实的叫 `border-l1` / `border-l2`)。CSS 自定义属性解析失败会**静默回退** 到后面那个硬编码深灰:表现是"样式看着怪,但控制台一个字都没有",浅色主题下最明显。 同一批还有自编圆角(9 / 10 / 13px)与自编语义色(`#4ec97e` / `#ffd479` / `#ff9d9d`)。 **做法**:全部换成查证过的主题 token —— 卡片用共享「设置卡片」材质 (`settings-card-fill` / `settings-card-stroke` + `radius-xl`),圆角一律走共享尺度 `radius-xs/sm/md/lg/xl`,语义色走 `state-success/warn/error-primary`, 开关滑块用主题文档点名的 `--dsw-alias-switch-thumb`,控件补上 `--dsw-focus-ring-*` 焦点环;正圆与胶囊半径按规范配上 `corner-shape:round`。设置页与诊断面板同一批改完。 ### 重做:「开发诊断」面板 **为什么**:原来是一列内联样式的裸 div —— 没有标题层级、没有卡片、每处自己写颜色。 **做法**: - 头部:18/600 标题 + 副标题 + H32 刷新按钮(`radius-md` + 焦点环) - **结论卡带语义色徽标**(需重启 / 已生效 / 一致),卡片描边跟着结论走 —— 语义色只有这一处来源,不会各写各的 - 环境 / 文件 / 服务 / 经验库 / 日志统一成「卡片 + 卡内标题 + 行模式」; 文件行用状态字形 + 等宽文件名 + 状态位 - **自适应版式**:窄屏单列、**≥980px 分两栏**(左窄右宽,结论卡跨两栏)。 原来的固定单列 720px 在宽屏上两边全空、信息被拉成一条 - 服务名默认只显示 24 个、可展开 —— 实测 94 个 chip 会把日志挤出屏幕 - 样式搬进 `BASE_CSS` 的 `.dvt-diag-*`,并在面板渲染前**幂等调用 `ensureStyle()`**: 这个 tab 是**注册即出现**的(不像小工具那样依赖开发模式开关),不叫就可能是裸文本 ### 校验器:事实源从"手工白名单"改成"从 asar 抽" **为什么**:`verify-theme-tokens.mjs` 的允许清单是手抄的。给 dev-tools 做 token 化时发现它 **漏了 13 个真实存在、官方页面正在用**的 token(`settings-card-*`、`scrollbar-*`、 `label-tertiary/caption`、`bg-module-platform`、`focus-ring-*`、`font-mono`、`switch-thumb`…), 而它反过来把官方在用的 `--dsw-alias-bg-layer-3` 判成"不存在"。**白名单一定会过期**(E19)。 **做法**:判据改成从打包产物里抽 —— `--dsw-*` 在 CSS 里"名字后面跟冒号"就是定义 (实测抽出 **405 个**)。分两档:有定义 = 权威;只出现过 = 可重绑变量 / 别名(**必须给 fallback**); 哪儿都找不到才判失败。im-bridge 专属的断言改成"目标带 `qqb-` 才跑",现在对任何插件都能用。 `verify-diagnostics-tab.mjs` 同步扩到 **27 项**(新增自适应版式、以及"面板里不许有硬编码色值")。 ### 经验库 66 → 67 条(E67) E67:校验器的事实源写死成手工白名单 —— 真 token 被漏、也被误判为不存在。 --- ## v1.3.2 · 2026-10-12 经验库 65 → 66 条。只加一条,但它是**这次验收差点误判的那个坑**。 ### 新增经验:E66 验收改动生效 —— 客户端刷新 ≠ 宿主重启 **为什么**:改完插件、界面也刷新了,于是以为已经生效 —— 结果**只有一半生效**。 客户端半(`client.js`)是页面加载时取的,刷新就换新;宿主半由**宿主进程**加载, 而 ESM 缓存按文件 URL 键,不整进程重启就一直是旧代码。 最阴的形态是"**部分生效**":同一个进程里有的模块是新的、有的是旧的。 实测(2026-10-12):宿主 19:55:30 启动,而 `app-control.js` 写于 19:45(**生效**)、 `self-check.js` 写于 19:57(**没生效**)→ 于是"新代码写的日志"和"旧代码抛的错" 同时出现,看起来像补丁写错了,其实只是没重启。 **做法**: - 判据用**两个时间戳**:宿主进程启动时间 vs 文件写入时间 (端口 → `Get-NetTCPConnection … OwningProcess` → `Get-Process` 的 `StartTime`) - 更硬的是**哈希不是 mtime** —— `/dev-tools/self-check` 的 `files[].changed` 正是干这个的:**全部 false 才等于"正在跑的就是盘上那份"** - 说话时把两半分开:**刷新页面 = 换客户端半;重启客户端 = 换宿主半** ### 顺带 README / package.json 的经验库数字同步到 66 条 / 指纹 `fa52ac5b` / 全文 159 KB。 --- ## v1.3.1 · 2026-10-12 一个**致命 bug 的修复** + 一处位置调整 + 一条新经验。主线是同一件事: **"一个请求里的错误"不该有能力带走整个宿主进程。** ### 修复:`/self-check` 能把宿主进程杀掉(fatal) **为什么**:这条路由原来在 try 里**先** `res.writeHead(200)`、**再**求值内容。内容一抛异常 就进 catch,而 catch **又写一次头** → `ERR_HTTP_HEADERS_SENT` 从 HTTP 处理器里逃出去 → 宿主进程 `exit 1` → 外壳判定异常、弹故障恢复框 → 用户只能点「禁用第三方插件」, 于是该 profile 里**所有第三方插件被打包摘掉**。实测 2026-10-12 19:39–19:40 一分钟内连崩三次 (客户端点「开发诊断」,它加载时第一个请求就是这条路由)。 **上游诱因**:`index.js` 的一句日志调用了**没 import** 的 `firstNonEmpty`(它定义在 `experience.js`) → `ReferenceError` → 插件"未激活";**但控制接口的启动写在那句日志之前**,端口已经起来了, 留下"状态是失败、端口还在听"的僵尸服务。 **做法**(缺一不可): - 三条路由(`/self-check`、`/log`、`/requirement-template`)改成**先把 body 算完、再写头**,只写一次; `JSON.stringify` 也放进 try —— 返回值里有循环引用时它同样会抛 - 整个请求处理器套**外层 try/catch**:判 `res.headersSent` 决定是收尾还是回 500; 并把**被吞掉的原始异常写进日志**(上次就是因为它被吞掉,现场只剩第二次的错,真因不可见) - 控制接口的启动**挪到 apply 最后** —— 插件没激活,就不该有端口在听 - 补上 `import { firstNonEmpty }` **守卫**:`verify-self-check.mjs` 新增 HTTP 回归 —— 让 `selfCheck` 抛异常 / 返回循环引用 / 正常返回, 断言三种情况都拿到干净的 JSON 错误、且**进程 0 次 uncaughtException**。 原校验器只直接调模块、从不走 HTTP,所以它全绿也拦不住这次崩溃。 ### 调整:「开发诊断」从右侧栏搬到**会话区 tab 条** **为什么**:要的位置是会话区 header 上「上下文」的右边。原实现用 `sidebarRightTabs` + `guide`, **必须先去侧栏引导页挑一次**才会出现 —— 那不是"默认就在那儿"。 **做法**:改注册 `conversation.view`(list 槽,**注册即出现**):`id: dev-tools-diagnostics`、 `order: 30`(官方 `chat` = 0、`trajectory` = 10、dsh-context「上下文」= 20 → 30 正好在它右边)、 `label: 开发诊断`。右侧栏那三处注册(`sidebarRightTabs` + 两个 `sidebar.right.pane.tab*`)**整段删除**, 避免同一个面板挂两处。顺手做了宽视图排版(限宽 900px + 居中 + 内边距)—— 面板原本为窄侧栏设计,搬到宽容器会拉伸得空。`verify-diagnostics-tab.mjs` 的 19 项断言整套换成新契约。 ### 经验库 64 → 65 条(E65) E65:路由里「先 writeHead 再求值」的致命形态 —— 含**判据**(崩溃报告里同时出现真因与 `ERR_HTTP_HEADERS_SENT`,且堆栈指向自己的 writeHead)与三条修法。 ### 顺带 - 两个校验器的清理补上删除 `${TMP}.bak`:`verify-experience-format-gate` / `verify-experience-write` 原来只删临时副本,实测在 `tools/legacy/` 留了两份 ~155 KB 的备份(共 318 KB)。 - README / package.json 的经验库数字同步到 65 条 / 指纹 `d825ad0c` / 全文 155 KB。 --- ## v1.3.0 · 2026-10-12 四个新能力 + 一个元守卫。**主线是同一件事:让"坏了但不报"这类问题自己暴露出来。** ### 新增:生效诊断器(`self-check.js` + `GET /dev-tools/self-check`) **为什么**:改了代码行为没变,于是开始瞎猜(ESM 缓存?路径不对?热重载没开?)。 根因是**没有任何地方记录"正在跑的这一份模块是什么时候加载的"** —— 实测到的现场是 宿主 06:57 启动、两个插件入口的 mtime 是 10:15 / 15:14,跑的都是旧模块, 而这件事在界面上完全看不出来。 **做法**:模块顶层记 `LOADED_AT` 与每个关键文件的**内容哈希**,运行时重算比对。 **用哈希不用 mtime** —— `touch` 或用编辑器重写同样内容都会刷新 mtime 而内容没变, 用 mtime 会假报"未生效"。并明确区分两类文件:代码改了**必须重启**, 运行期读盘的数据改了**立刻生效**。 ### 新增:客户端「开发诊断」tab **为什么**:诊断信息不该散在设置页和日志文件里,该集中在一个地方看。 从右侧栏的**引导页**挑一次即出现(跟官方 Files 那个 tab 一样)。 六区:结论条 / 环境 / 文件比对 / 进程服务 / 经验库 / 宿主日志。 契约照抄隔壁 dsh-context,三条纪律都守了:`kind` 带命名空间(撞车会抛)、 走 `ctx.inject`(可选契约,旧版本上不触发)、整段 try/catch(敌对注册表不该把浏览器搞崩)。 ### 新增:宿主日志落盘通道(`log-channel.js` + `GET /dev-tools/log`) **为什么**:宿主插件的 `console.log` **桌面端不落盘**,所以"插件到底做了什么"完全看不见, 故障表现是"不崩溃但什么都不做",只能靠猜 —— 这个项目为此栽过多次。 写 `~/.dsh/dev-tools.log`(512 KB 上限、滚一代、写失败静默),tab 第 6 区显示最近 200 行。 ### 新增:经验库检索工具 `search_experience` **为什么**:库已 64 条,而注入提示词的索引只放标题(正文要读全文)。 "靠标题扫一眼"到 60 条这个规模开始不够用了。 多词是"任一命中";按**命中字段**加权排序(标题 5 / 症状 3 / 解法 2 / 细节 1)。 第一版只数"命中几个词",实测查「代理 git」时 E33 排到 E32 前面 —— **命中在哪,比命中几次更能说明相关。** ### 新增:校验器元守卫 `verify-verifier-hygiene.mjs`(在开发台,不随插件发) **为什么**:校验器是这套项目的安全网,但**安全网本身没人检查**。 四条判据:失败时非零退出 / 总结行可被机器解析 / `check()` 参数顺序自洽 / 有没有列进例行清单(写好了不加清单 = 永远不再跑)。 它自己第一轮报了 29 条、**其中 28 条是假红**(判据太窄)—— 这恰好是它要抓的现象。 真抓到一条:`verify-qq-connect` 没有总结行,批量跑时那一行会变成 `?`。 ### 经验库:60 → 64 条 新增 E61(发布前检查要扫 git 对象,不是工作目录)、E62(改动没生效的自证方法)、 E63(字符级剥注释器会被正则字面量带偏)、E64(`ctx.reflect.props` 能列服务名、 列不了方法名)。并给 E42(断言写错)补了 5 种新形态、给 E52(假 ctx 只存一份)补了反方向。 --- ## v1.2.0 · 2026-10-12 一次集中修复:13 项(含 1 项实测否决)。**其中大多数问题属于同一类 —— "坏了但不报"或"承诺了但不生效"。** ### 修复:那些"坏了也不报"的地方 - **`findAppPath()` 里删掉硬编码路径** `'D:\DSH\DeepSeek Harness.exe'`。 它不只是可移植性陷阱,更关键的是**永远轮不到**(Electron 下 `process.execPath` 必然是 `.exe`,循环第一个候选就返回)—— "看着像兜底、实际不可达"的守卫比没有更误导。 之所以长期没被发现,是因为"无硬编码路径"那条校验器**只匹配 `C:\Users\<数字>`**。 - **对应地加宽了那条校验器**:现在拦任何"字符串字面量开头的盘符绝对路径"。 **有一条守卫在、却守不住它该守的东西,比没有守卫更危险** —— 你会以为有人看着。 - **`dev-rule.js` 改用 `ctx.inject(['systemPrompt'])`**,不再用 `ctx.get`。 原来的写法**与本仓自己的结论相反**(AGENTS.md 与 index.js 都写着 `ctx.get` 拿不到服务), 而且拿不到时只 `warn` —— 宿主插件的 warn 桌面端不落盘, 于是"开发规矩根本没注入"是**静默发生**的。现在服务就绪时自动注册、消失时自动注销。 - **控制端口失败不再只写一行日志**:错误挂在返回对象的 `listenError` 上, 调用方可据此提示。原先失败只 logLine,而它的后果是**设置页里所有开关都调不通**。 ### 修复:经验索引不再丢条目(本轮影响最大的一条) - **新增"只留标题"这一档截断**,并让剩余预算**从最新往回逐条加回描述深度**。 改之前:四档都装不下就直接丢条目,实测 59 条通用经验里 **24 条永远进不了上下文** (索引顶格在 4796/4800),而且丢的是**最早**的、不是"最不重要的" —— 丢掉的条目不会再回来。 改之后:**59 条全部进上下文**,其中最新的 26 条保留症状与解法,最早的 33 条只有标题。 判据:**漏掉一条的代价,远大于某条描述短一点。** - **索引正文里不再嵌本机绝对路径**。原先把 `D:\...\经验库.md` 原样注入每轮提示词: 泄漏安装路径、**索引长度随路径长短漂移**(省略条数跟着漂)、白占预算。 - **写入改成一次原子替换**:原先要往真文件写两次,中间那一刻"条目已加、头部还是旧的"—— 本文件自己还断言 `headerStale` 正常应为 false,等于承认那个中间态存在。 现在全程只动临时文件,最后 `renameSync` 一次替换(同卷原子),并留一份 `.bak`。 - **新增去重**:同样"症状 + 解法"(规范化后)不再写第二遍,返回已有编号并带 `duplicate: true`。判据用正文而不是标题 —— 标题只是概括、常被改写。 ### 修复:承诺了但不生效的配置(三处) 删掉 `enabled` / `trigger` / `ruleScope` 三个配置字段。它们不是"暂时没用", 而是**永远不起作用**:`enabled` 只被用来打一句日志(真开关在设置页的 localStorage); `trigger` **宿主半从不读它**;`ruleScope` 全库没有第二处引用。 设置页里那组"触发方式"单选按钮也一并撤掉 —— 三个选项里两个从未实现 (UI 上当时自己还写着"尚未实现")。 > **判据:界面上承诺了但不生效的配置,比没有这个配置更糟** —— 用户会以为自己调过了。 ### 修复:客户端与宿主的端口各写一份 客户端原先有 **5 处裸写** `http://127.0.0.1:8800`,而宿主是 `Number(process.env.DEV_TOOLS_PORT ?? 8800)`。改了一边漏了另一边 → 所有请求失败, 而别的校验器照样全绿。现在收敛成一个 `CONTROL_PORT` 常量, 并**新增一条跨端断言**把两边对起来。 (已知限制:客户端读不到宿主的环境变量,所以 `DEV_TOOLS_PORT` 一旦被覆盖就必然连不上 —— 这是刻意取舍,已写在代码注释里。) ### 修复:校验器自己的毛病 - **`verify-injection-size` 假红**:它有一句 `.slice(0, 60) // 与 MAX_ENTRIES 的解析上限对齐`, 而 `MAX_ENTRIES` 已于同日删除(它把解析截断在 60 条,会让新经验"写进去等于没写")。 实现删了、守卫没跟着走 → 断言把正确代码报成坏的。 **教训:断言不要绑死某个实现细节。** - **`verify-client-bundle` 与 `verify-template-hotkey` 补进例行清单**: 客户端半有 1600 行,此前**完全没在清单里**,等于没测(实测 40/40、16/16 都是绿的)。 - **新增去重 / 原子写 / 备份的常驻断言**(写入校验器 32 → 39 项)。 - **新增 README 体量数字的断言**(数字校验器 8 → 11 项):这一组以前没人管, 于是悄悄漂了很久(写着 59 KB / 3.4 万字符,实测 142 KB / 8.0 万字符)。 ### 其它 - 删掉未使用的 `SidebarAccountRow` 组件(109 行)。它与在用的 `run()` **逐字节相同** —— 停放的代码与活代码重复就是维护债。**注释里的教训保留**(` single` 槽位可以遮蔽, 但遮蔽"功能件"等于自己重写那个功能)。 - README 三处过期数字改正。 ### 实测否决的一条 **"注入路径重复解析 3 次 + O(n²)" —— 实测不成立,未改代码。** 实测:完整注入路径 `buildRuleTextUnconditional` **2.36 ms/次**, 其中 `parseExperienceFile` 占 2.32 ms、装配与截断只占 **0.07 ms**。 也就是说:**只解析 1 次**,装配开销可以忽略。 那条 O(n²) 的"连续后缀"循环确实存在,但本版新增的"只留标题"档已让它 在正常规模下根本不会被走到(只有超长库才会)。 > 2.36 ms 要跟"一次模型响应通常几秒"对比着看 —— 它不是瓶颈。 > 加缓存能省下这 2.3 ms,但会引入"缓存过期"这一类新风险,**不划算**。