Files

295 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 变更记录 / 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 <resources/app.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` 两套字典里)。