先说结论:我的插件一直"看起来很正常"。
- 测试 170 个,全绿
- 双端类型检查,0 错误
- 发布门禁 19 项,0 失败
- README 写得挺全,release notes 一期不落
然后我发了一版,npm 报了个 403。
不是 403 权限不足,是更尴尬的那种:
You cannot publish over the previously published versions: 1.0.0-rc.10
版本号没改。 我改了一大堆代码、写了 commit message 说"bump 到 rc.11",但 package.json 里那一行还是 rc.10。
而我的一句"我改好了",让这件事在几秒钟内暴露了出来。这促使我做了一件更彻底的事:请 AI 对整个插件做一次全面的只读审查。
结果它报出了 58 个问题 ,分四档。其中 3 个是阻断级的 ------而且全在发布链路上,也就是说:就算我代码写得完美,这版也发不出去。
这篇文章就是把这次审查摊开来讲。比起"我做了个多智能体插件",我觉得"我的发布流程其实早就断了,而我以为自己一直在发"这件事更值得写。
一、这个插件是干什么的
一句话:把 DeepSeek Harness 的一个会话,变成一场有主持人的圆桌会议。
你抛议题,AI 当主持人,拉起一圈专家节点在拓扑图上开会------专家能互相看到观点、能反驳,最后由主持人收敛成结论,决策权交回给你。
它有三种模式:
| 模式 | 谁在说话 | 适用 |
|---|---|---|
| 主持人统筹 | 一切经由主持人转达 | 常规评审、方案讨论 |
| 多模型平等 | 专家直达互辩 | 需要真实对抗时(成本高,强制预算上限) |
| 针锋相对 | 专家只攻击"已定稿"的方案 | 方案定稿后挑毛病 |
还有一个我特别喜欢的设定:"针锋相对"模式下,红队专家只挑毛病,禁止提出替代方案。 每条观点你可以单独「支持」或「驳回」,驳回必须写理由,理由会回传给主持人逐条回应。
这一版新增了一个入口改造:左侧导航栏多了「圆桌会议」,点开是一个跨会话、跨工作区的只读面板 ------左边列所有会议,右边直接看拓扑。会话里那个"圆桌会议"标签页依然在,而且这两个入口可以分别关掉。
二、审查是怎么做的
我给它定的框架是六个维度:
- 代码结构与组织(目录、职责、重复代码、死代码)
- 安全与稳定性(未处理异常、异步泄漏、并发冲突、空值)
- 功能与交互(命令能否调用、状态是否一致、有没有"实现了但没接线")
- 性能与资源(重复请求、高频轮询、可缓存未缓存)
- 可维护性(超长函数、魔法值、硬编码)
- DSH 插件规范与发布准备(元信息、版本、文档、卸载清理)
然后我让它完全只读:不许改文件、不许 git 提交、不许重构,只出报告。
分四路并行跑,每路负责一块,最后我来交叉核对。关键是没有让它"猜" ------每条问题都必须带文件路径 + 行号,拿不准的标"待确认"。
我自己也做了一遍抽查:它说"kb.list 缺了闸门而其他 5 个端点都有",我 grep requireActive 一看,确实只有 5 处;它说"快照类型声明和实现不一致",我跑 tsc 发现确实是 0 错误------也就是说那个类型错误一直是静默的。
三、三个阻断级问题(全在发布链路上)
阻断 1:我的 CI 发布通道已经死了一个月
审查报出来的第一条,让我有点意外:
pnpm-lock.yaml整个停在0.1.5-rc.3时代,还残留一个已经从package.json删掉的依赖。
而 CI 里写的是:
yaml
- run: pnpm install --frozen-lockfile
--frozen-lockfile 的语义是:lock 和 package.json 不一致就报错退出。
所以 CI 的第一步就挂了,后面的 pnpm build 和 npm publish 根本走不到。
那为什么我"一直觉得能发"?因为我一直是在本地手动 npm publish 的,从来没走 CI。
这里最值得记的一点是 :我做了四道发布门禁------兼容性矩阵、产物校验、发布护栏、测试------它们全都看不见 lockfile。门禁把"代码-矩阵"三角守得很死,却漏掉了"这个包到底能不能发出去"。
修法:新写了一个 check-lockfile.mjs,只读比对 pnpm-lock.yaml 和 package.json 的依赖声明,挂到 publish:guard 第一位。现在它一跑就报 20 个问题------如实报出来,比装看不见好。
阻断 2:CI 的 npm publish 少了 --tag
yaml
- run: npm publish --provenance --access public # 没有 --tag
而我自己在 release.mjs 的注释里早就写过:
npm 11 起不再按
-rc.N后缀自动推断渠道,不写会直接失败(You must specify a tag using --tag)。
也就是说:我的脚本注释和我的 CI 配置互相矛盾,而我没发现。
两边的后果:新版 npm 下直接失败;旧版 npm 下会把 1.0.0-rc.11 推进 latest------预发布版占据默认安装位,^1.0.0 的人会装到 rc 版。
修法:CI 按版本号后缀显式计算 tag(带 - 走 next,否则 latest),和 release.mjs 共用同一套判断。
阻断 3:latest 标签停在两个版本前
npm dist-tags 一看:
latest: 1.0.0-rc.8 ← 用户默认装到的
next: 1.0.0-rc.10 ← 实际最新
原因很朴素:我每次发布都只执行了 npm publish --tag next,从来没执行过 npm dist-tag add ... latest。
而插件市场是按 dist-tags.latest 采集版本的------所以我市场页面上显示的版本,一直是两个月前的。
修法两条:
release.mjs增加一项检查,打印 latest 与本次版本的差距 ,并给出该执行的dist-tag add命令;- 发布流程里把
dist-tag add写成必做步骤,不再靠记性。
这三个问题有个共同点 :它们都不是"代码写错了",而是"流程的某一环没人看着 "。
代码有测试守,流程没有。
四、三个会真正伤到用户的问题
发布链路是"我发不出去",下面这三个是"用户会踩到"。
问题 A:反馈条关不掉
会议结束后,右下角会弹一个轻量反馈条(好用 / 一般 / 不好)。
我加了关闭按钮。测试了一下,关闭生效。
但它1 秒后又弹回来了。
原因:会议结束后界面在轮询快照,每次轮询都会给 meeting 一个新对象 → React 认为依赖变了 → 那个"弹反馈"的 effect 重跑 → 判断"这个会议还没问过反馈" → 再弹。
而"点关闭"只是把弹窗状态清空,并没有把这场会议标记成"问过了"(那是"已经评过分"才写的)。
修法:加一个 feedbackAutoShown ref,每场会议只自动弹一次。和评审弹窗用的是同一套口径。
这类 bug 的特征:手动点一次永远看不出问题,只有"放在那儿不动"才会暴露。
问题 B:切对话之后,操作到了别人的会议上
这个更严重。
圆桌会议标签页会在同一个组件实例里切换"当前对话"。如果你在对话 A 里选中了会议 X,然后切到对话 B------而 B 里没有会议 X------组件找不到 selectedId,就回落到列表第一场。
那第一场可能是对话 C 的会议。
而界面上有个删除按钮,作用于"当前显示的会议"。
也就是说:你看着自己的对话,点删除,删掉的是别人对话里的会议。
修法:监听 sessionId 变化,换了对话就清空选中态。
问题 C:NaN 会让预算永不熔断
预算工具允许调整轮数和 token 上限。我原来的校验是:
ts
if (typeof args.max_rounds === 'number') fresh.budget.maxRounds = Math.floor(args.max_rounds)
NaN 也是 number。
于是 NaN 会写进 meeting.json,而熔断判断是:
ts
if (budget.usedTokens >= budget.maxTokens) → 闭麦
任何数 >= NaN 恒为 false。
结果:预算永远不会熔断,而界面上显示的是 NaN / 200000。
修法:校验轮数 ≥ 1、token ≥ 1000,和建会、设置页的口径统一。
五、"实现了但没接线"的三处
这类问题最隐蔽:代码写着、类型对、测试通过,但那条路从来没人走。
① 一个死掉的工具函数
members.ts 里有个 nodeActivity(),作用是"根据子代理 id 读真实运行状态"。
全仓库搜了一遍:零调用。
而快照路径自己内联了一份同样的逻辑------而且已经分叉了 :reconcileNodeLiveness 会返回 missing(宿主目录里查不到这个子代理了),而内联的那份只会返回 ready。
前端专门写了处理 missing 的样式......
所以:进程已经死掉的专家节点,界面上永远显示"就绪"。
这正是我在 node-events.ts 的注释里写过"要修"的那个 bug,只是在快照路径上被漏掉了。
② 一个从没被传过的参数
审查提到:快照每 1 秒拉一次,每次都把每场会议的完整记录读一遍、解析一遍,但调用方其实只用最后 20 条。
我实测了一下(8 场会议、341 KB):
全量读盘 + 解析 1.5 -- 3.5 ms / 轮
加缓存后(命中) 0.4 -- 0.6 ms / 轮
现在看无所谓,但它是线性的:单场记录到 5 MB 时,一次轮询就是几百毫秒,而且全部压在宿主的事件循环上。
修法:用 stat 的 size + mtimeMs 做失效键,文件没变就直接返回上次解析的结果(append-only 的 JSONL 只要大小没变内容就不会变)。零新增 IO。
我给它补了 4 个测试,专门守"缓存不能在内容变了之后还吐旧值"------那种 bug 只在真实使用里才看得到。
③ 一个只在旧宿主生效的安全白名单
NODE_ALLOWED_TOOLS:专家节点的工具白名单。
但宿主从 0.2.1 起把 per-child 的 toolFilter 从契约里删掉了。而这个插件现在跑在 0.2.2 上------那个分支根本走不到。
也就是说:专家的工具边界,现在只剩 persona 里的一段文字约束。
这一条我没有删,而是把适用范围写进注释。因为删了等于放弃旧宿主上的硬边界,而且将来要把边界补回新宿主时,这份清单是现成的声明。
真正该修的是"新宿主上的落点",不是删清单。 而这个落点需要宿主支持(preset 只读,插件不能在运行时注册),属于要跟上游协调的事。
六、还修了一批"不够诚实"的地方
真实用量在撒谎
评审记录可以导出 Markdown,里面有一行:
- 真实用量(provider 上报):48231 token
问题是这个数只统计"还活着的专家会话"------主持人自己的消耗不算,已经结束的专家会话也读不到。
而同一份导出里还有一行"发言文本粗估"。两个数字并排出现,读者会理所当然地以为前者是账单。
更糟的是代码写的是 usedTokensReal ?? 0:把"没测到"渲染成了"0 token"。
改成三态之后:
- 一个都没测到 →
不可测(此刻没有可读取的 provider 用量) - 部分测到 →
48231 token(覆盖 3/5 位专家的 live 会话,已结束的会话无法回读,不含主持人自身消耗)
一个失真的指标比没有指标更危险,因为它看起来很精确。
错误提示不说"怎么办"
拉取失败时界面显示「拉取失败,正在重试...」,而两处 catch {} 把 HTTP 状态和错误消息全丢了。
更微妙的是:那条失败提示的 JSX 写在 return 之后 ,永远不会渲染。
修法:保留原因、显示出来、给出下一步("检查插件是否启用,然后刷新页面"),并且连续失败时退避(3 次后退到 5 秒,5 次后退到 15 秒)------接口已经坏了,每秒重试一次只会刷满日志。
一个会杀进程的未处理 Promise
ts
ctx.on('subagent/end', (info) => {
void onNodeEnd(ctx, info) // 没有 .catch
})
而 onNodeEnd 里有两行代码在 try 外面,其中一行会对宿主传来的消息做结构断言。
宿主事件载荷形状不可控。一旦那里抛错,就是一个 unhandled rejection ------Node 24 默认直接杀进程。
修法:两行移进 try,回调补 .catch。
观测失败绝不应该影响宿主,更不应该让宿主崩溃。
七、审查之外,我自己踩的坑
回头看,这次最狼狈的一件事不是审查发现的,而是我在整改过程中把文件改坏了。
改动 rpc.ts(616 行)时,我想把它那个 350 行的 switch 改成查表结构。用脚本做替换,结果把文件搞成了语法错误状态、tsc 报了几十个错误。
最后靠 git checkout -- src/rpc.ts 恢复。
教训:一次性重写大文件,不如分小步改、每步验证。
后来我换了做法:不动那个 switch,只在旁边加一层声明表 + 守卫------把"哪些端点要求会议进行中"从散落的手抄变成一张可比的表,再补 8 个测试守"声明与实现不能漂移"。
效果上,这已经解决了真正的问题(漏写闸门不会有任何报错,曾经真的漏过一个),风险却低得多。
重构的目标是降低未来的成本,不是"结构看起来更整齐"。 如果为了整齐去动一个 350 行、19 个分支的 switch,而手上没有足够的验证手段,那是在给自己制造新问题。
八、这一版的数字
| 项 | 之前 | 现在 |
|---|---|---|
| 测试 | 170 | 182 |
| 发布门禁 | 19 项 | 21 项(新增 README 版本徽章 ×2) |
| 新增门禁 | --- | lockfile 一致性 |
| 修复问题 | --- | 高优先级 8/9、中优先级 8/12、低优先级 6 |
| 改动规模 | --- | 31 个文件,+2229 / −1221 行 |
| 文档 | README/HANDOVER/host-contract 全停在两个月前 | 全部按当前状态重写 |
一个细节:审出来的 58 个问题,我只修了 40 多个 。剩下的(比如"把 tools.ts 拆成 6 个模块"、"把 2000 行的 UI 组件拆开")都是不改行为、只降维护成本的活,我明确排到下一版。
不是所有问题都要一次修完。 但要知道哪些没修、为什么不修。
九、安装
sh
dsh plugin --profile web add @y9931666/dsh-plugin-roundtable@next
从源码:
sh
git clone https://github.com/9931666/dsh-plugin-roundtable
cd dsh-plugin-roundtable
pnpm install && pnpm build
dsh plugin --profile web add .
装完重启 DSH、刷新页面,然后在对话里直接说:
开个圆桌会议,从性能、安全、成本三个角度评审这个方案,最后给我一份汇总报告。
或者定稿之后:
方案已定稿,开个针锋相对评审,拉两位红队专家专门挑毛病。
十、最后
这次审查给我最大的一个认知改变是:
"测试全绿"和"能发布"是两件事;"能发布"和"发出去的是这一版"又是两件事。
我花了很长时间优化代码质量------加了四道门禁、写了 170 个测试、把注释写得像论文。但没有一句话在检查"这个包到底能不能发出去"。
结果就是:代码没问题,流程断了,而我不知道。
所以如果你也在维护一个自己用的项目,我建议你做一件很便宜的事:
问一遍"从改完代码到用户能用上,中间有几步,每一步谁在看着?"
代码有测试看着,那 lockfile 呢?那发布标签呢?那文档里的版本号呢?
那些没人看的地方,就是你会栽的地方。
项目地址:https://github.com/9931666/dsh-plugin-roundtable