手写第一个 Harness 插件:从搭环境到跑通实战

发布时间:2026/8/22 22:55:27
手写第一个 Harness 插件:从搭环境到跑通实战 我第一次看到 Harness 的插件接口时愣了几秒。因为之前在公司的项目里写过插件系统那套 SPIService Provider Interface服务提供者接口机制光是接口定义就有七八个文件还要配 XML 描述符、搞类加载器隔离折腾了两天才跑通第一个 demo。所以我打开 Harness 的文档时已经做好了被摧残的准备。结果文档第一页写着一个插件就是一个函数。我心想你逗我呢但等我装好环境、写了一个最简单的插件、跑起来发现——还真就这么简单。插件到底长什么样先说结论Harness 的插件机制底层用的是 Cordis 框架。cordis 这个词你可能不熟但它的设计理念很直接——一切皆插件不是口号是代码层面的架构选择。你写一个插件本质上就是导出一个apply函数然后在这个函数里注册你的能力。拿我写的第一个插件举例一个给 Agent 统计代码行数的小工具import { Context } from cordis; import { execSync } from child_process; export const name code-stats; export function apply(ctx: Context) { ctx.command(code-stats [dir:text], 统计指定目录的代码信息) .option(detail, -d 显示每个文件的明细) .action(async ({ session, options }, dir .) { session?.send(正在扫描代码目录稍等一下……); const files glob.sync(**/*.{ts,js,py,java,go,rs,cpp,h}, { cwd: dir, ignore: [node_modules/**, dist/**, .git/**, target/**], }); // 你别说第一次跑的时候我忘了加 ignore // 结果 node_modules 里几万个文件直接给我整卡了 // 等了半分钟才反应过来赶紧加了忽略规则重跑 let totalLines 0; let totalFiles 0; const details: string[] []; for (const file of files.slice(0, 200)) { try { const output execSync(wc -l ${dir}/${file}, { encoding: utf-8 }); const lines parseInt(output.trim().split(/\s/)[0]); totalLines lines; totalFiles; if (options.detail) { details.push(${file}: ${lines} 行); } } catch { // 二进制文件跳过没事 } } return 代码统计结果\n━━━━━━━━━━━━━━━━\n扫描目录: ${dir}\n文件类型: .ts .js .py .java .go .rs .cpp .h\n文件总数: ${totalFiles}\n代码行数: ${totalLines}\n${options.detail ? \n details.slice(0, 30).join(\n) : }; }); }这段代码一共 30 多行核心逻辑就三件事用 glob 匹配代码文件、用wc -l数行数、拼一个结果字符串返回。有意思的是我一开始想过用纯 JS 逐行读取文件来统计后来发现直接用wc -l效率高得多——处理 200 个文件不到 0.3 秒。你看写插件有时候不一定要用最正统的方案用最趁手的工具就行。这个 apply 函数到底干了什么上面那段代码里最关键的就是apply(ctx: Context)这个函数。ctx是 Cordis 的上下文对象Context插件运行时的环境容器它像是一个插座——你的插件插上去就能访问 Agent 的指令系统、生命周期、配置管理等等。不需要你手动管理什么依赖注入、服务注册Cordis 全帮你干了。ctx.command()是注册指令的方法。第一个参数是命令名和参数定义[dir:text]表示 dir 参数是可选的文本类型。第二个参数是帮助信息里的描述。option()给命令加选项比如-d显示详细列表。action()是命令执行时的回调函数返回的内容会直接发给用户。你可能会说这不就是写个命令行工具吗对本质上就是。但区别在于——你的插件跑在 Harness Agent 的进程里可以访问 Agent 的上下文、会话、文件系统甚至能调用其他插件的能力。这意味着你写的不是一个孤立的工具而是 Agent 能力的扩展。怎么让它跑起来插件写好了怎么让 Harness 加载它在你的 Harness 项目目录下新建一个plugins/code-stats/index.ts把上面的代码放进去。然后在dsh.config.ts里注册import { defineConfig } from dsh; export default defineConfig({ plugins: [ ./plugins/code-stats, ], });然后启动 Harness在聊天里输入code-stats /path/to/your/project -dAgent 就会调用你的插件返回代码统计结果。笑死我第一次测试的时候因为忘记在dsh.config.ts里注册插件折腾了十分钟一直在想为什么我的命令没生效。后来一看插件根本没加载进来——这大概就是写插件最常遇到的坑一切正常但忘了注册。插件开发现在有哪些坑说实话Harness 的插件机制虽然设计得干净但目前还不是完全体。调试体验是最大的痛点。每次修改插件代码都得重启 Harness 进程才能生效——没有热重载Hot Reload不重启进程就能加载新代码的能力意味着你改一行代码就得等 3-5 秒的重启时间。写多了确实有点烦。文档方面核心的 Cordis API 文档还算完整但 Harness 自己做的一些扩展接口比如会话管理、任务队列的文档就比较零散了。我写这个 code-stats 插件的时候查ctx.command的 option 定义方式翻了三个页面才找到正确的写法。类型定义也有一些小问题。Cordis 的类型系统比较灵活但这也意味着有时候 TypeScript 推断出来的类型跟你预期的不一样。比如action回调里的session参数在某些场景下可能是null但类型定义里没标清楚。这就得靠运行时自己判断了——上面代码里我加了session?.send()的问号调用就是吃过这个亏之后养成的习惯。不过话说回来这些问题对于一个还在快速迭代的开源项目来说其实是正常的。插件的核心接口已经很稳定了上面写的 code-stats 插件从 v0.1 到现在的版本都能直接用。你能用它做什么插件机制最妙的地方在于你可以把任何重复性的工作教给 Agent。比如你可以写一个插件自动拉取 Jira 的未完成任务列表再写一个插件把你每天的代码提交记录汇总成日报。又或者像上面那个 code-stats 一样把代码审查的统计数据直接喂给 Agent让它帮你分析哪些模块的代码需要重构。我自己的下一个目标是写一个代码 diff 分析插件让 Agent 在每次提交前自动审阅变更的代码标注潜在的问题。你觉得这个方向有意思吗还是说你有更想先做的插件需求评论区聊聊呗。