dsh-plugin-review · git:20261105.d7d824c · 2026-11-05 · sha256 7b8d2c1ad52469f6
dsh-plugin-review git:20261105.d7d824cA
Immutable. This exact content never changes and is served at /api/v1/blob/7b8d2c1ad52469f6.
---
name: dsh-plugin-review
description: DSH 插件代码审查与防御性架构审核车道。基于 Git 范围或工作区源码,检查 Cordis 生命周期泄漏、未逆向注销、单例状态漂移、已废弃模板变量(如 {{cwd}})、Vendored Peer 依赖污染、准入账本覆盖,按 P0/P1/P2/P3 输出精炼结构化报告;提供子 Agent 隔离委派协议(Sub-agent Dispatch Pattern),在后台沙箱运行紧凑分析并回传精炼结果,防止主会话上下文污染。触发词:代码审查、代码审核、code review、插件体检、防御性审查、生命周期检查、单例泄漏、Peer污染、子Agent审查、紧凑报告。
---
# 代码审查与防御性架构车道
在插件适配或交付前,对代码进行深度架构审计。重点排查 Cordis 生命周期泄漏、单例漂移、Vendored Peer 依赖污染以及废弃 API,按统一严重度(P0/P1/P2/P3)给出明确判定。
同时定义**子 Agent 隔离委派协议(Sub-Agent Dispatch Pattern)**:当面对复杂插件或批量巡检时,主 Agent 派发独立子 Agent 执行重型静态分析与冒烟,仅回传结构化紧凑报告(`<20` 行),避免海量日志填满主会话上下文。
以下命令都相对工具根目录执行,工具根即本文件真实路径往上两级、含 `scripts/` 的那一层。脚本用自身文件的绝对路径定位,不必先 cd。`<插件目录>` 指被查插件的仓库根。
## 第一步:会话内极速体检
```sh
node scripts/plugin-audit.mjs <插件目录> --compact
```
使用 `--compact` 参数时,脚本仅输出高密度结构化 JSON 摘要与关键结论,屏蔽全量文件遍历噪音:
```json
{
"plugin": "my-plugin@1.0.0",
"overallScore": 95,
"scores": { "performance": 100, "resilience": 90, "contextBudget": 100, "lifecycle": 90 },
"priorityCounts": { "P0": 0, "P1": 1, "P2": 0, "P3": 0 },
"blockers": [
{
"priority": "P1",
"dimension": "lifecycle",
"file": "lib/index.js",
"title": "事件监听或定时器未绑定清理逻辑",
"detail": "Cordis 插件重载时未清理的监听器与定时器会泄漏,建议注册进 ctx.effect 或提供 dispose。"
}
]
}
```
## 第二步:子 Agent 隔离委派协议(Sub-Agent Dispatch Pattern)
当审查整个插件仓库或运行全流程适配验证时,主 Agent 应按以下协议委派后台子 Agent:
1. **派发子 Agent**:通过 `subagent`(或需要继承本会话上下文时用 `subagent_fork`)唤醒子 Agent,指定只读工作区;需要长期跟进时给它一个 `name`,后续用名字寻址;
2. **执行紧凑流水线**:
```sh
node scripts/run.mjs <插件目录> --compact
```
3. **回传协议**:子 Agent **严禁**直接复制全量调试日志或终端文本到父会话,必须提炼为以下三段式紧凑摘要返回:
- **通过性总账**:宿主版本、窗口、体检分(各维度分)、冒烟与 typecheck 状态;
- **阻断项账本**:仅列出 `P0`(致命阻断)与 `P1`(严重缺陷)条目(文件 + 标题 + 根因);
- **下一步行动**:具体的修改建议(如 peer 替换、dispose 补充、模板变量清理)。
4. **主 Agent 决策**:主会话上下文保持清爽,主 Agent 仅依据紧凑报告决定修改代码或交付用户。
**寻址用名字,不用 Session id(2026-11-05 适配 0.2.2 系列)**:`send_message` 与 `interrupt_agent` 的参数已从 `agent_id` 改为 `agent`,接受名字或 Session id;`list_agents` 的行也从 `kind/id/label/parent/depth` 改成 `{ name, status, description? }`;启动回执是 `started subagent <name>`。旧写法传 `agent_id` 会直接参数校验失败。子 Agent 回传时把「宿主版本、窗口、分维度分」一并带上,是因为这些字段决定了报告该怎么读——脱离版本与窗口的分数没有意义。
## 第三步:四级严重度标准(Priority Standards)
| 级别 | 定义与影响 | 典型案例 | 处理原则 |
| --- | --- | --- | --- |
| **P0** | **阻断发版/启动失败**:宿主启动必崩溃或类型严重分裂 | 强 import 已废弃包、提示词含已移除 `{{cwd}}` 抛错、核心 Peer 依赖写在 `dependencies` 产生 Vendored 污染 | 立即阻断,必须清零后才能交付 |
| **P1** | **资源泄漏/状态异常**:运行时静默失效或跨会话污染 | `ctx.on` / `setInterval` 缺失清理逻辑、准入账本范围未覆盖目标宿主、高频热路径同步 I/O 阻塞 | 适配阶段应优先修复 |
| **P2** | **架构异味/上下文开销**:不影响当前运行但存在隐患 | 模块顶层定义可变全局集合(Map/Set/数组)重载残留、无字节上限文件读取、工具 Schema 文件偏大 | 建议重构,顺带优化 |
| **P3** | **规范建议/提示**:声明性约定或类型维护 | `import type` 引用即将变动包、工具描述中引用已消亡宿主路径、peer 声明严格 semver 提示 | 人工知悉,按需调整 |
## 常见缺陷与标准修复模式
### 1. 核心 Peer 依赖污染(P0)
- **错误**:`package.json` 的 `dependencies` 里声明了 `"cordis": "^4.0.0"` 或 `"@deepseek-ai/dsh": "^0.2.1"`;
- **修复**:将其移至 `peerDependencies`,并在 `devDependencies` 保留开发类型支持。
### 2. 严格模板变量移除(P0)
- **错误**:系统提示词模板中包含 `{{cwd}}`;
- **修复**:删除模板中的 `{{cwd}}` 子句,直接依赖 DSH 运行时每轮注入的上下文。
### 3. Cordis 生命周期清理(P1)
- **错误**:`ctx.on('chat', handler)` 未保存 dispose 句柄;
- **修复**:改用 `ctx.effect(() => { ctx.on('chat', handler) })` 或在 `ctx.on('dispose', () => { ... })` 中解绑。
### 4. 语言缺失不等于删除(P1,来自生态实证)
- **错误**:写 `if (!name) delete ctx.services[name]` 或把空值当成「没配置」跳过,从而连带丢掉合法的空值配置;
- **机制**:空值与缺失是两件事。上游一个真实事故是工具导出索引式配置块时把空值项丢掉,导致从 CLI 启动编辑器后整条 Git 发现链断掉。本生态里对应的是「`Config` 给了默认值却被当成未声明」「`inject` 空数组与不声明混为一谈」;
- **修复**:要区分 `undefined` 与 `''` / `[]` / `0` / `false`;删除判断用「键是否存在」而不是「值是否真」。
### 5. 注册表按名字写入前先查重(P2)
- **错误**:注册工具、服务、命令、slot 时直接覆盖同名条目;
- **机制**:多数宿主的注册表是后写覆盖先写且不报警,两个插件抢同一个名字时表现为「其中一个静默失效」,排查成本极高。上游多个 Agent 宿主已把这条从「静默覆盖」改成「拒绝重复注册并快速失败」;
- **修复**:注册前查重,冲突时抛明确错误或加命名空间前缀,不要静默覆盖。
### 6. 环境变量擦除按变量组原子处理(P1,本工具自身实录)
- **错误**:按名字过滤环境变量(例如「凡名字含 KEY/SECRET/TOKEN 就删」)却只删了组里的部分成员;
- **机制**:git 的索引式配置是一个**原子组**(`GIT_CONFIG_COUNT` + `GIT_CONFIG_KEY_n` + `GIT_CONFIG_VALUE_n`),缺任一键就整体拒跑并 fatal。宿主的子进程凭证擦除正好会摘掉 `KEY_n`(KEY 三个字母既在 git 配置通道名里、也在密钥字段名里),留下计数与值,于是一切 git 子进程全挂;
- **修复**:擦除逻辑按组原子处理;需要起子进程的一方在**自己的边界**上净化(本工具的做法见 `scripts/lib/proc-env.mjs`),不要假设上游环境一定干净。
### 7. 删路径不要跟随链接(P2,常设规则锚点)
- **错误**:对可能是软链/junction 的路径用 `rmSync(p, { recursive: true })`;
- **机制**:POSIX 上会穿过链接删掉目标内容(纯数据丢失),Windows 上对 junction 抛 `ERR_FS_EISDIR`;
- **修复**:先 `lstatSync().isSymbolicLink()`,链接用 `unlinkSync`(只删链接本身、拒绝真实目录),真实目录才递归删。本工具的实现是 `removeDirSafely`。
## 常设规则从哪读(不复制进技能)
上表第 4/5/7 条不是自己编的,出处是上游仓库 `docs/defensive-patterns/`(五篇:package-surface / general / client-components / conversation-and-session / remote-and-api)。那一层是**不随版本变的硬要求**,`docs/upgrade-guide/` 是**随版本变的破坏性变更**,两者互不覆盖:只扫升级指南会漏掉「从来没变过但插件一直违反」的那一半。
读法:先按上表定位到大致类别,再去对应那一篇核对规则原文、判定条件、边界与例外,**以原文为准**;技能里这几条只是指路牌,不是规则的副本——上游改了原文,这里不该跟着漂移。
## 哪些已经自动化,哪些仍要人看
常设规则那层不再全靠人工。`node scripts/standards-scan.mjs <插件目录>` 做掉了照原文读必然得出同一结论的四条,每条都带 `defensive-patterns` 的原文出处(`--rules` 出完整规则表):
| 规则 | 机械判据 | 没查什么 |
| --- | --- | --- |
| `top-level-await` | 缩进为 0 的 `await` | 动态 `import()` 的顶层等待 |
| `loader-entry-shape` | 入口的值导出集合(`export type` 不计) | 导出物是不是「同一个东西的两种写法」 |
| `shared-state-no-owner` | `globalThis._X` 赋值;被导出的模块级 `let`/`var` 在函数里被重新赋值 | 容器被写、但从未被重赋值的一类(写一次的标志位);判据要求「被导出」,所以只在本模块内共享的那种不算 |
| `cross-package-relative-import` | import 说明符以 `/` 开头,或相对路径解析后跑出包根 | 别名(`@/...`)指向包外——要读 tsconfig 才知道 |
**故意没进自动扫描的**:`dispose 必须达到完全停稳`、`在分发器中隔离回调异常`、`slot 注册物是稳定的 function component`、`Store 用法窄而明确`、`Projection 是唯一的读取与计算路径`。这些要判断意图与边界,静态文本判不出可信结论;报错了只会消耗使用者对整个工具的信任。它们留在本车道的表里人工核对。
同一套规则也进了 `plugin-audit` 的第五维(`standards` 分),所以 `run.mjs` 与 `write-gate` 自动带上,不必单独跑。`write-gate` 里它属于**软指标(透传待裁量)**,不是硬门禁——分数低不等于不能发,要人看过才算。