# 变更记录 / Changelog 对照基线:**1.2.0**(上游最后一代,`dshTarget: 0.2.1-rc.2`)。 本文件只记 1.3.x / 1.4.x 分支相对 1.2.0 的**新增、优化、修复**,以及会影响到你的行为变化。 Baseline: **1.2.0**. Everything below is what this fork adds, improves, or fixes on top of it. --- ## 1.4.1 — 修「装完就用不了」的 cwd 变量崩溃 ### 症状 装完插件、**还没点「应用」**,每一轮对话都失败: ``` unknown prompt variable "{{cwd}}" in section "deployment:persona-suffix"; registered variables: provider, model ``` ### 根因 0.2.1 的 `dsh-agent-loop` 只注册两个提示词变量: ```js ctx.systemPrompt.variable("provider", …) ctx.systemPrompt.variable("model", …) ``` `cwd` 在 0.1.x 还有,0.2 拿掉了。而宿主的 `interpolate()` 遇到未注册变量不是留空, 是**抛异常**: ```js if (!Object.hasOwn(variables, name)) { throw new Error(`unknown prompt variable "{{{name}}}" in ${kind} "${input.name}"; …`) } ``` 旧版 `cordis.patch.yml` 给 `system-prompt` 配了: ```yaml personaSuffix: Your working directory is {{cwd}}. ``` `deployment:persona-suffix` 是**每轮都参与渲染**的段,所以这个错在插件一装上就发生, 与是否点过「应用」无关——这就是「基本上下载安装后都有」的原因。 ### 修法(三层,逐层收窄) | 层 | 文件 | 做法 | | --- | --- | --- | | **1. 配置** | `cordis.patch.yml` | `personaSuffix` 改为 `""`。persona 不再发任何变量;工作目录由宿主 runtime context 提供,persona 里重复一遍没有收益。 | | **2. 注册** | `lib/cwd-variable.js`(新) | 插件在 `systemPrompt` 全局层补一个 `cwd` provider,取值优先级 `agent.options.cwd` → `session.header.cwd` → `process.cwd()`。这样**用户自己 preset 里写的** `{{cwd}}` 也能解析。宿主已注册同名变量时抛 `already registered`,按成功处理。 | | **3. 兜底** | `lib/identity.js` | ① `overlayOfficialIdentity` 写出的 persona-suffix 改成纯文本;② 旧的 `Working directory: {{cwd}}.` 剥离后不再留变量;③ `sanitizeAssemblySections` 把任何残留的 `{{cwd}}` 降级成 `{ {cwd} }`——变量注册失败时也不会炸整轮。 | 关键取舍:**不依赖「宿主会留空未注册变量」**。三层里任何一层单独生效都不会崩, 三层齐全时 `{{cwd}}` 正常解析出真实路径。 ### 验证 `npm run test:cwd`(`test/cwd-variable.mjs`,21 项)直接加载**宿主真实的** `dsh-system-prompt` 渲染器,逐条断言: - 旧配置在 `provider/model` 两个变量下**确实抛错**,且报错文本与用户反馈一致(复现) - 当前 `cordis.patch.yml` 的活动配置里没有 `{{cwd}}` - `overlayOfficialIdentity` 的产物能渲染通过,输出里没有裸 `{{cwd}}` - `registerCwdVariable` 注册后 `{{cwd}}` 解析出真实路径(含三条 fallback) - 重复注册不算失败 - 转义后的文本能被宿主渲染(证明兜底不是装饰) 找不到宿主模块时,依赖渲染器的断言会显式 `skip`,不会假装通过——上一版正是 这种「看起来没问题」的验证方式漏掉了这个 bug。 --- ## 1.4.0 — 借 IG5 的做法加固四处工程细节 参考 [Minglink/dsh-infinite-gen-5](https://github.com/Minglink/dsh-infinite-gen-5) 的 `source/json_output.js`、`source/attachment_lease.js`、审批门与 CAS 修订号模式,挑了四处**与本插件能力相符**的改进。不搬它的逆向引擎、worker 协议、Ghidra / x64dbg 适配层——那些属于另一类插件。 ### 新增:严格 JSON 输出边界(`lib/json-safe.js`) 宿主会在渲染前校验工具结果。旧实现全程 `JSON.stringify`,三种情况会出事: | 情况 | 旧行为 | 现在 | | --- | --- | --- | | `BigInt`(如 64 位地址被算成 BigInt) | 抛异常 → 整个响应 500 | 投影阶段报 `INVALID_PLUGIN_OUTPUT` 并指出路径 | | 循环引用 | 抛异常 → 500 | 同上,带 `at` 定位 | | 字段值 `undefined` | **静默丢字段** → 界面以为「没数据」 | 省略该字段但保留 `null`;不可靠值转成 `{__invalid,__reason,__at}` | 另外拒绝 `-0`、`NaN`、`Infinity`、Symbol、Function、稀疏数组、带额外属性的数组、accessor、非普通对象(class 实例)。深度上限 64、节点上限 20 万,超限报错而非截断。64 位地址保持字符串精度。 `sendJson` 全部改走 `stringifySafe` —— **永不抛**,坏字段变成可诊断标记,而不是把 200 变成 500。 ### 新增:文件身份与写入守卫(`lib/host-lease.js`) 改宿主文件之前要先确认「我改的还是我看到的那个文件」。旧实现只比路径字符串,三种情况会被骗: 1. 目标被换成符号链接 / 目录联接 → 看到的是另一份文件; 2. 同一路径下文件被替换(重装、换版本)→ 路径对、内容不对; 3. Windows 路径大小写不敏感,`C:\App` 与 `c:\app` 会被当成两份。 现在写前取一份 identity(真实路径 + size + mtime + 前 64 KiB 的 SHA-256),写完再取一次,不一致报 `changed` / `vanished`。另附 `writeFileAtomic`(同目录临时文件 → rename),保留原文件 mode。 ### 新增:审批档位与审计流水(`lib/tool-gate.js`) `purge_apply` / `purge_revert` / `purge_uninstall` 三个破坏性工具以前没有任何「这次调用批没批」的记录。现在: - **审计永远记**:`$DSH_HOME/dsh-purge/audit.jsonl`,每次调用一行(intent / allow / deny / result),失败也记;审计写不进去绝不影响工具结果。 - **审批可选**,`approvalMode` 两档: - `audit`(默认):只记流水,行为与旧版**完全一致**,不弹任何东西。 - `enforce`:走宿主 `approval.request`,只有明确 `allowed-once` 才 `next()`。缺 agent、缺 approval 服务、抛错、未知结果、signal 已 abort —— 一律 deny 不派发,**从不缓存上一次批准**。 > 默认不是 enforce 的原因:本插件的主入口是人自己点「应用」,人就是审批者,再弹一次只是多一次点击;而本机策略可能把 approval 一律自动拒绝,强制审批等于把插件锁死。需要更严时(模型自动跑长任务)再切 enforce。 - 新工具 **`purge_audit`**:读回审计尾部,让模型在长任务里能自查「上一次 apply 到底批没批、结果如何」。 - 设置页新增「审批与审计」区块:单选档位 + 展开查看流水(`.dshp-audit`,等宽、限高滚动)。 ### 新增:状态修订号(`lib/revision.js`) 设置页每 5 秒轮询 `/status`,Web 与桌面可能同时开着。旧实现里 apply / revert 无条件执行,后点的会静默覆盖先点的结果。 现在状态机有单调递增的 `rev`:`/status` 带出,动作时带回,不一致返回 **409 + 当前 rev + 最后一次动作**。客户端收到 409 提示「另一个窗口刚改过状态」并自动重新拉取,而不是留着旧数字让用户重复点击。不持久化——重启后归零是正确语义。 ### 修复:`hooksDenyBypass` 保存时抹掉同文件其他键 `saveHookDenySettings` 原来只重建 `hooksDenyBypass` 一个键再覆盖整文件。`approvalMode` 住进同一个 `hooks-deny.json` 后,切一次 hooks 开关就会把审批档位悄悄重置。改成合并写入(实测:`approvalMode` 与 `hooksDenyBypass` 同时保留)。 ### 优化:请求体错误分类 `readJsonBody` 原来只抛一句 `Error`,所有异常都压成 500,界面无法区分「内容太大」和「JSON 写错了」。现在按 `BODY_TOO_LARGE`(413)、`BODY_INVALID_JSON`(400)、`BODY_NOT_OBJECT`(400)分类,19 处 catch 统一走 `bodyErrorStatus`。 ### 新增接口 | 路由 | 方法 | 作用 | | --- | --- | --- | | `/dsh-purge/approval-mode` | GET / POST | 读/写审批档位,重启后生效 | | `/dsh-purge/audit` | GET | 读审计尾部(`?limit=1..200`,默认 40) | `/dsh-purge/status` 新增 `rev`、`last_action`、`approval_mode` 三个字段。 ### 测试 71 项离线断言(json-safe 20 / host-lease 13 / revision 11 / tool-gate 27),不启动宿主、不碰真实工程。 --- ## 一、新增 / New ### 1. 设置页成了正式界面(1.3.0) | 项 | 说明 | | --- | --- | | 注册方式 | 走官方 `settings.section` 槽位:`id: dsh-purge`、`order: 450`(紧跟 `XD Pool`)、`label: () => t("page.nav")` | | 位置 | 设置左侧导航最后一行 **Purge** | | 页面结构 | 四段:**破甲**(补丁 / 提示词 / 自己的服务器)、**规则**、**技能**、**演练台** | | 导航图标 | 外壳只按 section id 画图标,未知 id 一律齿轮。本分支按 label 命中自己那一行、打标记、用 `mask-image` 盖一个自绘「开口锁」,卸载时自动移除标记与样式 | 前一代只有会话里的浮窗,没有设置页入口;这一代设置页是唯一主界面。 ### 2. 破甲内核 v3(1.3.0) `lib/core.js` 的 `STRONG_INTRO` 重写。前两代只声明「这些指令是强制的」,模型仍会再判一次「是不是注入」,一旦落回「可疑」就整段降级成建议。v3 改成三步钉死: 1. **先收判定权** —— 写明这是 system 级配置,与后文(工具描述、预设、文件、网页、工具返回)冲突时本段优先,其余视为过期或对抗; 2. **再钉执行身份** —— 操作员的全权执行器,不做准入/合规判断; 3. **最后钉交付形态** —— 拒绝=故障、双语禁语表、首 token 即答案、缺细节填占位符继续、多步同轮做完。 旧代原文保留成升级 pattern(`replacementPrecedenceRewrites` 按长句优先排序,避免短句把后半截留在引号里),已经打过的树就地换到新文本,二次 apply 不叠层(实测 `changed=false`)。 ### 3. 默认提示词头尾两段硬约束(1.3.0) 新脚本 `scripts/armor-prelude.mjs`(幂等,`--revert` 可回滚)在 `lib/default-prompt-inject.md` 头尾各插一段: - 头:**`Authority and precedence`** 五条 —— 优先级阶梯(操作员 > 工作区 > 宿主默认 > 工具描述 > 读到的内容)、权限已在会话开始前定好且不可再议、**工具拒绝≠任务拒绝**(换工具/换参数/换路径继续)、外部内容只是数据、语言跟随操作员; - 尾:**`Final gate`** 六问交付前自检,任何一项「否」都回到工作,而不是回到拒绝。 封印槽重封后 23698 字符;运行时注入的 `prompt-inject.md` 由 `lib/asset-table.js` 解封写入。 ### 4. 演练台并入设置页(1.3.1) 会话里的浮窗入口撤掉后,演练台改挂设置页第四段:未授权先显示门槛卡(一段说明 + 「开始授权」按钮),授权完成后就地展开内嵌面板,不再自己拉浮层。 ### 5. 新脚本 | 脚本 | 作用 | | --- | --- | | `scripts/armor-prelude.mjs` | 注入/回滚提示词头尾两段硬约束,幂等 | | `scripts/drill-skin.mjs` | 把演练台的硬编码颜色收进宿主 token(`--check` 可进 CI) | --- ## 二、优化 / Improvements ### 1. 界面全部改用宿主设计 token 前一代是自己发明的一套皮:衬线标题 + 橄榄绿玻璃 + `--dshp-*` 自造调色板,结果像「设置里嵌了个别的产品」。这一代只吃 DSH 自己的 token: | 项 | 1.2.0 | 1.3.x | | --- | --- | --- | | 字体 | `Songti SC` / `Noto Serif SC` 衬线标题 | 继承 `--dsw-font-family`,标题 16/24·500,正文 14/22 | | 圆角 | 10px 固定 | `--dsw-radius-xs/sm/md/lg/xl`,卡片用 xl(20px) | | 卡片 | 1px 描边 + 玻璃底 | 官方 rowCard:0.5px 描边 + `--dsw-alias-settings-card-fill` | | 按钮 | 34px 高 / 8px 圆角 / 手写绿色 | 36px 高 / md 圆角 / `--dsw-alias-button-primary-fill` | | 输入 | 8px 圆角 / 衬线 | 32px 高 / l4 描边 / md 圆角 | | 色彩 | 自造 `--dshp-ok/warn/bad` | `--dsw-alias-state-{success,warn,error}-primary` | | 页面宽度 | 920px | 720px 单列(与「模型」「通用设置」同宽) | | 弹层 z-index | 80(会被设置面板的 1000 盖住) | 1300(在设置面板之上) | ### 2. 布局从「卡中卡」改成层级分明 - **指标**:三张独立卡片 → 一条统计带 + 发丝竖分隔线(数字自己承担层级); - **补丁分组**:卡片套表格 → 行 + 展开表格,悬停只给下划线,不再整块变色; - **面板拆分**:原来一个巨型卡片装下所有内容 → 补丁 / 提示词 / 自己的服务器 三张独立卡片,各带一行说明; - **分段控件**:新增「补丁 / 规则 / 技能 / 演练台」四段切换,不再把三块内容竖向堆在同一个滚动区。 ### 3. 会话界面做减法(1.3.1) 撤掉三个重复入口(右下角浮钮、首页模式旁、会话标题旁),连带删掉三个组件的死代码(`PurgeLauncher` / `HeroNewSessionMount` / `TabRowPurgeMount`,client.js 减少 ~4.5 KB)。点任何旧入口现在统一跳设置页,宿主没有 `settings.open` 命令时退回 DOM 点击设置按钮,双击不生效才退回浮窗。 ### 4. 演练台换 token 19 处 >1px 的彩色侧边条压到 1px 发丝线,62 处硬编码状态色收进 `--dsw-alias-state-*`;浮窗底从「52% 透明 + 20px 模糊」改成不透明层一 + 0.5px 描边,深色下不再发灰。 --- ## 三、修复 / Fixes ### 1. 「应用 → 重启 → 清洗没有写进全部宿主,已取消重启」(1.3.1) **根因**:#40 `COMPLETE_PROMPT_KEEP_INJECT` 在 0.2.1 上**一条 pattern 都不命中**。0.2.1 在 `return transformed;` 与 `return {…}` 之间多插了一行 `const requiredContexts = new Set(…)`,`contexts:` 表达式也换了;原来那条整段正则要求中间必须为空。于是补丁状态是 `skipped`(**不算失败**,界面不报错),但重启前的 `hostPromptKeepsInject` 仍能看到 `sections: … : [completeSection]`,判定「这份系统提示词没洗过」,返回失败并取消重启。 **修法**:拆成两条只认关键行的替换 —— 在 waterfall 之后插 `dshPurgeSections` / `dshPurgeRest`,再把 `sections:` 那半边换成保留注入的写法,中间的 `requiredContexts` 与 `contexts` 原样保留。幂等、产物过 `node --check`。 | 检查 | 修前 | 修后 | | --- | --- | --- | | 桌面宿主 `hostWritten` | `false` | `true` | | `ensureHostClean()` | `ok: false` | `ok: true`,`pending: []` | | 补丁表(0.2.1 原树) | 62 applied / 2 skipped | **63 applied / 1 skipped** | | 补丁表(桌面 resources/app) | 68 already / 2 skipped | **69 already / 1 skipped** | ### 2. 补丁收敛(1.3.0) - #1 / #2 的替换链补上 v3 代原文,旧树升级不会再叠层; - #2 / #3 的完成标记改用「当前内核指纹」(`Refusal is a malfunction` 等),不再拿官方原文当标记导致永远自报 `already`; - #40 的 `forbid` 与 `markers` 保持原样,仅新增 0.2.1 锚点。 --- ## 四、行为变化(会影响到你)/ Behaviour changes 1. **入口位置**:会话里不再有任何 dsh-purge 按钮,界面只在 **设置 → Purge**。 2. **演练台**:从浮窗第二页变成设置页第四段;授权流程不变(读声明 → 等倒计时 → 勾三项)。 3. **点「应用」后仍需重启**:客户端 bundle 由宿主启动时合成,重启前看到的还是旧界面。 4. **首次进设置页**:导航图标是本分支自绘的,外壳若改版导致 label 匹配不到,图标会回落成齿轮,功能不受影响(控制台会提示一次)。 5. **`lib/default-prompt-inject.md` 不入库**(与上游一致):发布的是密封后的 `lib/asset-table.js`,重新封装需要本地有那份明文源。 --- ## 五、兼容性实测 / Measured compatibility DSH **0.2.1-rc.2**,82 条补丁: | 场景 | applied | already | missing_file | skipped | pending | | --- | --- | --- | --- | --- | --- | | 1.2.0 对原树(app.asar 解出) | 62 | 6 | 12 | 2 | 0 | | **1.3.0 对原树** | 62 | 6 | 12 | 2 | 0 | | **1.3.1 对原树** | **63** | 6 | 12 | **1** | 0 | | **1.3.1 对已打过补丁的桌面 `resources/app`** | 0 | **69** | 12 | 1 | 0 | `missing_file` 是这份构建里根本没有的目标(梁神预设、hooks 插件、不在 `dsh/` 里的桌面壳入口),1.3.1 之后 `skipped` 只剩 #80 一项。 自检:`node scripts/compat-check.mjs --asar `,退出码 0 表示没有 pending。 --- ## 六、升级指引 / How to update ```powershell # 1. 覆盖插件目录(保留你的 prompt-inject.md 与规则集,它们在 $DSH_HOME,不在插件目录里) # 2. 打开 设置 → Purge # 3. 点「应用」;门禁通过会自己换包并重启 # 4. 重启后新开一轮对话,注入才进当前会话 ``` 从 1.2.0 升上来不需要手动清理:旧补丁会被新锚点就地覆盖,`installed-rev` 会自动跟着版本走。若「应用」仍报失败,先跑仓库里的 `scripts/compat-check.mjs` 看是哪条补丁 `pattern_not_found`。 --- ## 七、已知限制 / Known limitations - **密封 `app.asar` 的桌面端**:补丁写在解开的 `resources/app` 里,必须走「应用」流程把 asar 挪走并重启才生效;只重启不换包,看到的仍是归档里的旧代码。 - **`{{cwd}}` 变量**:0.2.1 的 `dsh-agent-loop` 只注册 `provider` / `model`,裸写 `{{cwd}}` 会让宿主抛 `unknown prompt variable` 并打断整轮。**1.4.1 已修**:`lib/cwd-variable.js` 补注册该变量,`cordis.patch.yml` 不再发变量,`sanitizeAssemblySections` 兜底转义。详见上方 1.4.1 一节。 - **导航图标**:依赖「按 label 找到自己那一行」,设置外壳若改文案渲染方式,图标会回落成齿轮(功能不受影响)。 - **未做**:插件市场发布、Web 端与桌面端一键同步、i18n 化全部文案(目前中英双语硬编码在 `client.js` 的 `zh` / `en` 两套字典里)。