# 结构与设计约束 插件绑定 DeepSeek Harness `0.2.1-rc.2`,提交 `687f282e8625c6b8d11ff58211a90524be2e4317`。目标版本、补丁声明与预哈希清单必须通过对账;源码升级需要单独适配。 ## 模块边界 | 模块 | 职责 | 依赖方向 | | --- | --- | --- | | `src/plugin.js` | 创建实例状态、登记能力 | 下面全部 | | `src/patch/` | 补丁声明、预哈希、预检、事务、锁、选择、共用服务 | Node 内置模块 | | `src/context/` | 共用正文、两档附加段、尾部摘要、模式、规则、自定义提示词读取 | `patch` | | `src/host/` | 接缝、宿主根、执行层预算、状态、命令与工具、诊断、预检工具 | `context`、`patch` | | `src/serve/` | HTTP 解析、路由与响应 | `host`、`context`、`patch` | | `src/cli.js` | 独立命令行 | 各域 | | `src/client.js` | 宿主设置页(宿主强制单文件) | React、宿主 slots、HTTP | | `src/bundle.yml` | 装配补丁 | 不调用运行时 | 依赖方向单向:`serve → host → context → patch`。反向导入一律不许。 ## 提示词装配 头部 = `inject.md`(用户可覆盖)+ 该模式的 `recommended.md` / `unrestricted.md` + 激活中的规则,放入独立的 `breaklimit:head` 字面段(`interpolate: false`)。有 `deployment:persona-prefix` 时紧靠其前,无该段时插到数组首位。宿主在装配瀑布之后不再排序,落点由数组下标决定。用户正文里的 `{{...}}` 原样保留,宿主 persona 继续解析自己的模板变量。 尾部 = `tail.md`,独占 `breaklimit` 字面段,序号由 `config.order` 决定:缺省 **8999**,装机配置里设 **10300**。宿主第一方段的顺序是结构化输出 9900、harness 源码 10000、Web 表层 10100、`deployment:persona-suffix` 10200,所以 8999 落在它们之前、10300 落在全部原生段之后。 两档:`recommended`(推荐,默认)与 `unrestricted`(完全解放)。旧值 `lean` / `architect` / `autonomous` 判非法并回退默认,不迁移。 ## 规则存储 一条规则同时是「一份正文」和「一个插进用户文件的落点」。这两件事失败的方式不同,所以分开存: - `rules.json` 是索引:规则表、当前激活项、落点归属、`revision`。写入按 `revision` 比较并交换,两个进程同时改时后到的那个因版本不符失败重来,而不是覆盖对方刚写下的条目。 - `bodies/.md` 存正文。与索引分开,是为了让「正文很大」和「索引频繁改」互不牵连:切激活状态不重写正文,改正文不碰索引。 - `landings/.txt` 存**第一次接管该落点时的原文**(原字节,不经文本解码)。用户在该文件上做过插件之外的修改时,撤销不能把当前内容当成自己的写回,于是保留归属、报 `skipped`,把处置权交回用户;被外部改过的那份内容不会被新激活覆盖掉原文快照。落点与快照的读写都按字节:文本解码会把非 UTF-8 字节替换成 U+FFFD,拿解码结果写回就毁了原文。内容字节没变就不重写落点;非 UTF-8 的落点不自动收拢、原样保留。 - 落点处置是三态(`removed` / `restored` / `skipped`)而不是布尔。「撤销时文件已不是我们写的样子」既不是成功也不是失败:成功要求清空记录,失败要求保留记录,而它要求保留记录并把事实报出来。 - 所有写入走同一个跨进程文件锁。它不是性能优化:宿主命令、HTTP 路由与模型工具会同时到达,而 `rules.json` 的读—改—写不是原子的。 - 激活先写意图(`pendingActivation`),再写新落点,最后提交。同落点切换沿用首次接管快照;提交前失败恢复切换前字节,旧落点不被提前处置。 - 跨落点提交后,`pendingActivation.phase: "cleanup"` 表示新规则已生效、旧落点待收尾。`previous` 保留旧归属;旧快照到收尾清单提交后才删除。启动或后续写入口会续做,I/O 故障时保留意图并暂停会覆盖它的写操作。外部修改原样保留并报告。缺少 phase 的旧意图保持原有诊断语义,不自动猜测提交结果。降级前须先处理未完成的收尾。 对应的失败场景各有测试,见 [自检与验收](docs/verify.md) 的规则恢复链、落点快照所有权、并发锁三项。 ### 放在哪,保证什么 | 位置 | 内容 | | --- | --- | | `$DSH_HOME/breaklimit/rules.json` | 索引:规则表、激活项、落点归属、`revision` | | `$DSH_HOME/breaklimit/bodies/.md` | 规则正文 | | `$DSH_HOME/breaklimit/landings/.txt` | 接管前的原文(原字节),只在原文件本来有内容时存在 | | `$DSH_HOME/breaklimit/landings/.external-<时间戳>-*.txt` | 激活覆盖外部改动前的副本,只在被覆盖内容不属于任何插件副本时存在 | | `$DSH_HOME/AGENTS.md`、`CLAUDE.md` | 落点:插件写入正文的地方 | 插件的任何状态都在 `breaklimit/` 目录内,落点旁边不放副产物;删掉这个目录,剩下的就是宿主原本的文件。 保证: - **处置只依据激活那一刻的记录**,不依据当前元数据。`created` 为真就删掉落点,为假就写回 `landings/` 里那份原文。 - **外部改动不覆盖**。落点被插件之外改过时既不写回也不删除,返回 `skipped`。每条跳过记录带原因和两侧指纹:`expected` 是插件认为该文件应当有的内容,`actual` 是它现在的内容(sha256 前 16 位);两侧不一致就是内容被换过。用户据此判断是不是自己改的,不需要插件替他决定。激活是唯一例外:它的语义就是把新正文写进落点,所以守的是「不丢」——被覆盖的字节不在任何插件副本(新正文、快照原文、上一条激活规则的正文)里时,先存副本再覆盖,路径随激活结果报出;副本写不出来就取消激活。 - **读不到就停**。落点、清单或正文存在但读不到(权限、占用、句柄耗尽)时,激活、保存、取消激活一律停手报错,不按「文件不存在」继续写。读失败与「没有文件」是两件事:混同就会把读失败当成空清单整表替换、或当成自己创建的文件删掉。 - **失败不吞**。`atomicWrite` 失败沿调用链返回结构化失败,`skipped` 里如实记下原因,不改成「已完成」。 - **写入是原子且已落盘的**。临时文件与目标同目录,写完先 fsync 再改名,改名后尽力 fsync 目录(POSIX 执行;Windows 没有可 fsync 的目录句柄,目录一步跳过,由 NTFS 日志与同卷改名语义兜底);一次写入要么完整落盘,要么完全不存在。 ## 补丁与运行时能力 补丁引擎处理与版本绑定的文件修改;运行时能力处理当前进程内的可变量。二者不重叠:补丁写宿主源码,切模式需要回退后重新应用并重启;运行时能力走 Loader 的 volatile 提交,立即生效。 只有 `danger-full-access` 档接管审批与工具门。其他档位不放宽沙箱;沙箱升级请求被拒绝,普通审批交还宿主。提示词模式控制文本内容,不改变沙箱档位。 ## 设置页前端 `src/client.js` 保持宿主要求的单文件交付:`window.__ModuleLoader__.load` 注册,元素全部 `React.createElement`,颜色与字体只取宿主 `--dsw-*` 令牌。设置分区五个页签(概览、补丁、提示词、规则、词库)加会话输入框旁的浮窗,两个挂载点共用模块级 `aliasStore`: - 词库写入是「操作」而不是「整表提交」——前端按增/改/删描述意图,执行时基于最新列表算出整表再 `PUT /breaklimit/alias`(后端是整表覆盖)。写入串行,过期读取响应按版本号作废,两处挂载点不会拿旧列表互相覆盖;写成功以响应里的规范化列表为准,并通知另一处刷新。 - `GET /breaklimit/alias` 的读取告警随 HTTP 200、`code: "ok"` 放在 `message` 里;前端把它当读取失败处理:保留旧数据并标旧,暂停写入与预览,不当成空词库。保存失败保留草稿;超时/断网先标旧、使旧读取响应作废,暂停整表写入与预览,再读取核对,可信读取成功后恢复。 - 本地预览复用 `src/host/masks.js` 的替换语义(区分大小写、精确字面、长词优先、按顺序依次替换),与后端逐例一致;它只是预览,不是真实会话命中记录。 - 确认与词条编辑用原生 `` 弹窗:Esc 取消、焦点进入与返回、正文滚动时按钮保持可达。 - 浮窗的「管理词库」要直达设置分区的词库页。宿主没有对插件开放「打开设置面板并定位到某个分区」的公开接口(`SettingsOnboardingOwnerProps.openSection` 只面向 onboarding 步骤);当前从 slots 注册表 `sidebar.settings` 的 store 座位调用 `openSection("breaklimit")`,失败或宿主换壳时退回一句指引,待落地页签由分区挂载时消费。宿主将来提供公开接口后,替换 `openHostSettingsSection` 即可。 ## 必须保持的约束 - **预检零写入**:目标缺失、整文件哈希失配或命中数异常时整批停止。哈希清单缺失也阻断。 - **可重复应用**:先识别事务记录中的应用后内容;不靠子串包含判定。 - **恢复有依据**:写源码前保存快照与事务记录;写入失败尝试回滚,回滚失败保留恢复记录与原始错误。回滚成功的事务把 manifest 落成 `rolled-back`;事务目录清理只收 `committed` 与 `rolled-back` 两类,且从不删 state.json 引用的目录。 - **漂移不覆盖**:回退前对账整批文件,外部改动导致整体拒绝;当前补丁开关不改变既有事务的恢复范围。 - **两层互斥**:实例内锁与基于同一 `DSH_HOME` 的跨进程锁共同保护应用与回退。 - **规则保留所有权**:激活前把写入文件的原文存进独立文件并记哈希;落点每次写入后把写入字节的哈希记进清单(`landing.writtenHash`),「是否被改过」按它判定,不拿活正文当基线——一次同步失败后保存能恢复,撤销不再被误判成外部改动。旧索引无此字段时回退旧基线,下次成功写入就地升级。撤销时未变则恢复或删除,变了则保留并报告。 - **执行层预算只提交 config**:`loader.resolve(id).update({ config })` 只在 volatile-only 时原地生效;条件不成立会重挂载并打断子代理,因此提交只带必要键、失败只记录不重试。 - **请求体上限随载荷走**:HTTP 请求体上限由规则正文与注入正文上限推导(6 倍 JSON 转义余量),不设不随载荷变化的固定封顶;两个正文上限本身不因此放宽。 - **配置可追踪**:状态报告宿主根来源、模式、执行层预算现值与偏差,不依据配置宣称已生效。 ## 与 DSH 插件规范的偏差 宿主没有一份叫「插件开发规范」的单文档,可判定的硬要求集中在 `docs/user/develop/basic/`(`Config` 是 Standard Schema、组合包声明 `engines.dsh`、客户端半侧为 lazy-CJS)与 `docs/capability-seams.zh.md`(可用的服务清单)。 已对齐:`engines.dsh: 0.2.1-rc.2` 由宿主在安装与启动时判定;`src/plugin-config.js` 导出 Standard Schema 形式的 `Config`,装载前校验并补默认值;浏览器半侧经 `dsh.client` 声明、按 lazy-CJS 注册;`ctx.systemPrompt`、`ctx.sandboxPolicy`、`ctx.webServer`、`ctx.tools`、`ctx.commands` 都在公开服务清单内。 仍偏离三处,都是有意的: | 偏离 | 位置 | 理由 | | --- | --- | --- | | 改宿主源码树 | `src/patch/`、`src/host/seams.js` | 规范只提供 Cordis 接缝与 bundle patch 改配置两条通道,改 `packages/**` 不在插件模型内。这是本插件的功能本体,靠预哈希、命中数守卫与事务回滚约束,README 已写明绑定宿主提交、需要重启。 | | 读写 Loader 条目 config | `src/host/capabilities.js` 的 `updateEntry` | `ctx.loader` 是 vendor 服务,不在插件文档的服务清单里;执行层预算没有公开写入通道。条件不成立时退回 `src/bundle.yml` 的装载期基线,失败记入状态不重试。 | | 浏览器侧取设置面板的 store 座位 | `src/client.js` 的 `openHostSettingsSection` | 宿主未开放「打开设置分区」接口(`SettingsOnboardingOwnerProps.openSection` 只面向 onboarding)。当前从 slots 注册表读 `sidebar.settings` 的座位调用 `openSection`,失败退回一句文字指引;宿主提供公开接口后替换此函数即可。 | | `--dump-config-schema` 不认手写 schema | `src/plugin-config.js` | 宿主的 schema 投影只接受带 `Symbol.for('schemastery')` 标记的原生 schema,本行会记为 `unsupported` 并使该命令退出 1。`--dump-config` 与真实装载不受影响。换成 Schemastery 要新增依赖与安装步骤,与零依赖安装冲突,因此接受这个诊断代价。 | | 提示词段序号只能自定 | `src/host/seams.js` 的 `registerSeams` | 宿主把段序号的统一分配留给仓库内贡献方(`getSectionOrder`),外部插件没有对应通道。本插件用 `config.order` 自定(缺省 8999、装机 10300);另一个第三方插件也把自己的段抬到宿主段之后时,先后只由数值决定,可能与它撞序。 | ## 数据与兼容 恢复记录、快照、规则、模式、补丁选择位于 `$DSH_HOME/breaklimit/`。自定义提示词默认为 `$DSH_HOME/breaklimit-inject.md`,可由 `injectFile` 配置覆盖。规则写入文件为 `$DSH_HOME/AGENTS.md` 或 `CLAUDE.md`。 事务格式版本不匹配必须停止恢复;偏好格式异常退回默认并报告。模块搬迁不改变持久格式、HTTP 路径、响应外壳或包导出。 宿主根的插件解析顺序是配置、环境变量、宿主进程入口推导。CLI 的解析顺序是显式参数、环境变量、profile 装配文件、本包装配文件。 ## 证据边界 仓库测试覆盖引擎、入口契约与纯函数诊断;固定版本夹具覆盖补丁与宿主 HEAD 的对齐;实际装载、浏览器交互与各沙箱档位需要单独验收。步骤与当前记录见 [自检与验收](docs/verify.md),历史部署记录见 [重构前验收](docs/acceptance-history.md)。 状态重建与应用、回退共用 `operation.lock`。候选快照按预哈希筛选,跳过回退备份中的补丁后内容;没有有效快照才走经过提交归属验证的 HEAD。零采纳只留下获取锁所需的空目录,不产生恢复记录或快照文件。