现在的人工智能,本事不小。写诗、写代码、写周报,样样都能来两下。可你要是想让它在你的电脑上替你干点正经活,比如打开你那个仓库,看看代码改了什么,把改动整理成一次像样的提交,它就傻眼了。它太「聪明」了,聪明到没有手。
DeepSeek Harness(下文简称 DSH)解决的就是这个问题。它不满足于给你一个聊天框,它给你一套给 AI 造零件的流水线,这套流水线,叫插件。插件是 DSH 的灵魂,也是它跟「一个普通的网页聊天机器人」最大的区别。
这篇文章就讲讲 DSH 的插件体系是怎么运转的。为了不空谈,我拿自己亲手做的一个插件当例子,一个让 AI 学会管理 git 仓库的插件,叫 dsh-git-manager。从三行代码的骨架,到装进界面、让模型学会记账,全程大概一个下午加一个晚上。看完这篇文章,你会明白,给 AI 造零件这件事,门槛低得超出你的想象。
一、最小插件,三行代码的世界
DSH 的插件体系,地基是一个叫 Cordis 的框架。它的规矩简单得出奇,你导出一个叫 apply 的函数,框架一开机就喊你一声,把这个世界的一串「钥匙」递给你。
import type { Context } from '@deepseek-ai/cordis'export const name = 'hello-plugin'export function apply(ctx: Context) { console.log('[hello-plugin] plugin loaded!')}三行代码,一个插件,就成立了。name 是你的名字,apply 是你开始干活的信号,ctx 是一箱子工具,挂灯笼的钩子、敲钟的锤子、记账的笔,全在里头。你从 ctx 里拿工具,往框架上挂你造的东西,注册一个工具、监听一个事件、开一条路由、往设置面板塞一个选项。
DSH 里跑着的成百上千个能力,从「让模型能搜索网页」到「让界面多一个侧边栏入口」,说到底都是这一套,一个 apply,一个 ctx。DSH 这个「AI 全能助手」的真相,就是一堆插件在 ctx 上各自挂东西。
二、体面的设计,东西挂上去,走的时候自动收
插件装上容易,卸载呢?很多软件的插件系统,卸载等于拆炸弹,注册的东西忘了注销,事件监听器挂在内存里,越攒越多,最后把程序拖死。
DSH 的设计师显然受过这种苦。他们的规矩是,你通过 ctx 挂上去的任何东西,插件卸载时框架自动替你摘下来。 注册工具、监听事件、开路由,统统不用你手动清理。
export function apply(ctx: Context) { // 注册一个工具——卸载时自动撤销,不用你管 ctx.tools.register(myTool) // 自定义资源:告诉框架"我有个定时器,走的时候帮我停掉" ctx.effect(() => { const timer = setInterval(() => { /* ... */ }, 5000) return () => clearInterval(timer) })}这套「自动清理」的设计,体面得像一个好房东,你住进来随便折腾,退租的时候不用打扫,房东自己收拾,还把钥匙交回给你。更妙的是它支持热替换(HMR),你改完插件代码,框架自动把旧的卸掉、把新的装上,中间不用重启整个程序。开发插件的时候,改一行、看一眼、再改一行,循环快得让人上瘾。
三、两面派,一个插件,两个世界
现在到了 DSH 插件最特别的地方。一个带 Web 界面的插件,其实活在两个地方。
一半活在服务器上,叫 host 半区,管真活,跑命令、读文件、跟 git 打交道。另一半活在浏览器里,叫 client 半区,管门面,画按钮、弹窗口、显示界面。一个人白天在柜台后面管账,晚上回家还要把账本誊一遍给人看;两件事都归他管,一个在店里,一个在家。
package.json 里寥寥几行声明,就把两面接上了,
"exports": { ".": { "default": "./lib/index.js" }, // host 半区:干活 "./client": { "default": "./lib/client.js" } // 浏览器半区:给人看},"dsh": { "bundle": { "patch": "./cordis.patch.yml" }, "client": { "inject": ["..."], "platform": "web" }}浏览器那半边更讲究,DSH 的界面有一个叫 __ModuleLoader__ 的加载器,插件包声明了 dsh.client,加载器就会在 /plugins/<id>/client.js 这个地址把你的浏览器半区取来,装进页面。整个过程不用改 DSH 一行源码,插件就是插件,宿主就是宿主,谁也不碰谁。
我的 git 插件就是按这个骨架长出来的,整个架构一张图就能说清。

图 1,一个插件,两个世界。浏览器里的面板和服务器上的服务,通过 /dsh-git 路由通话;干活的是最底下那台真实的 git。
四、给模型装手,插件直接变成模型的能力
普通软件的插件,最多给界面加个按钮。DSH 的插件不一样,它可以直接给模型加「手」。模型没有鼠标,点不了按钮,它想管账只能靠工具。DSH 里定义工具的方式叫 defineTool,一个工具四样东西,名字、说明书、参数表、干活的函数。
export function gitStatusTool(service: GitService) { return defineTool({ name: 'git_status', description: '查看仓库状态:分支、上游、ahead/behind、暂存/未暂存/冲突计数。触发词:git status、查看仓库状态、当前分支。', parameters: { path: { type: 'string', description: '工作区路径(默认当前会话工作区)' }, }, output: { schema: { type: 'object', properties: { /* 规范输出结构 */ } }, render: (_args, value) => [{ type: 'text', text: renderStatus(value) }], }, async execute(args) { return await service.status(args.path ?? sessionCwd) }, })}这套 DSL 有个值得玩味的细节,execute 返回的是结构化数据,render 再把数据翻译成给模型看的文字。 干活和说话分开,账本归账本,广播归广播。还有更好笑的一条,description 一定要写得像广告词,模型是靠这段文字判断「什么时候该用你」的。写得太干巴,你就成了工具箱里永远没人碰的那把螺丝刀。
我最后给管家配了十六件工具,
从此以后,模型看到用户说「帮我提交一下代码」,就知道先调 git_status 看看账,再调 git_stage 把改动收进账本,最后 git_commit 落一笔。一个插件装上,模型就多了一套手艺。
五、站在服务上,插件不是孤岛
插件不是孤岛。DSH 给插件们准备了一排公用服务,tools(工具注册表)、webServer(HTTP 路由)、subprocess(安全地跑子进程)、workspaceRegistry(工作区注册表)、systemPrompt(系统提示词)、settings(设置项)……插件要用哪个,就在入口处声明一下,
export const inject = ['webServer', 'subprocess', 'workspaceRegistry', 'tools', 'systemPrompt']export function apply(ctx: Context) { // 到这里,上面五个服务一定已经就绪了}这个 inject 是 DSH 插件体系里最「懂事」的设计,框架会先确保你要用的服务都准备妥当,才喊你开工。 依赖没就绪?那你就等着,饿不着也抢不着。
我的 git 插件,干活全靠这套服务,ctx.subprocess 去跑真实的 git 命令(而不是自己拼 shell 字符串,拼字符串迟早出事,哪天文件名里带个空格,账就记错了);ctx.workspaceRegistry 做门禁,管家只许在注册过的工作区里跑 git,别的地方一概不碰,像给狗上链子,院子里随便跑,出不了院门。
if (ctx.workspaceRegistry.list().some(ws => ws.path === canonical)) { return { ok: true, canonical }}return { ok: false, error: { code: 'workspace-unknown', message: 'not a workspace' } }一个能替你跑 git 的插件,如果哪天被坏蛋骗了,让它去 /etc 底下「提交」点什么,那乐子就大了。凡是能替你做主的东西,都得先想好它的活动范围。 这一条,是 DSH 插件设计里我最服气的地方,能力给你,边界也给你划好。

图 3,DSH 公用服务一排,插件通过 inject 声明取用;workspace 门控像院墙,院子里随便跑,出不了院门。
还有一件有意思的事,插件不光给模型装手,还能往模型的脑子里塞一张名片。DSH 有个系统提示词服务,插件可以注册一段自我介绍,「我是 git 管理插件,会这些手艺,遇到这些词请想起我」。模型每开一个会话,脑子里就带着所有插件的名片。
ctx.systemPrompt.section({ name: 'plugin:dsh-git-manager', order: 160, text: '本机已安装 dsh-git-manager 插件:git_status / git_commit / git_push ... 用户提到 git、提交、分支、暂存、推送时,优先使用这些工具。',})这就像新来的伙计先跟掌柜的打个招呼,我会记账,会打算盘,有账本的事您喊我。DSH 的聪明之处在于,插件再多,模型也不会乱,每张名片都写得清清楚楚,谁管什么,一目了然。你的插件装上去,模型第一时间就知道自己多了哪门手艺。
六、门面挂上去,框架没留门,你就自己开扇窗
界面是给人用的。DSH 的侧边栏、对话区,都有固定的布局,但是,没有给插件留插槽。 我的面板怎么办?
野蛮施工。直接在 DOM 里塞一个「Git」按钮进去,再派一个哨兵(MutationObserver)盯着,一旦界面框架重渲染把按钮挤掉了,哨兵在同一帧里立刻把它塞回去。
const waitObserver = new MutationObserver(() => { tryPlace() })waitObserver.observe(document.body, { childList: true, subtree: true })这个按钮像一只打不死的小强,你踢它一脚,它马上又爬回来。点开按钮,面板通过插件自己注册的 /dsh-git 路由跟 host 半区通话,中间还有一道「只许本机访问」的围栏(loopback fence),防止局域网里的陌生人摸到你的账本。
用户在面板里点「暂存」到「提交」的完整旅程,画成图,就是这样。

图 2,用户在面板点一下,背后是一串请求,门控校验、git add、git commit,每一步都有回应。
这套「DOM 注入 + 哨兵自愈」的土办法,看着不体面,但很管用。后来我悟出一个道理,框架没给你留门,你就自己开一扇窗。 很多软件的生态,其实都是这么野蛮生长出来的。
七、打包交付,组合包与档案
插件写完了,怎么装进 DSH?DSH 的安装机制有两层,理解它,就理解了整个插件生态的交付方式。
第一层叫组合包(bundle),一个带配置层的 npm 包。包里有一份 cordis.patch.yml,声明「我这个包往配置里插一行插件」,
# cordis.patch.yml —— 我贡献的配置层- insert: - id: git-manager name: '@linxin666/dsh-git-manager'第二层叫档案(profile),一份可启动的组合,由一堆组合包按顺序叠成。安装一个插件,就是往档案里加一个包,
dsh plugin --profile web add link:~/Documents/DSHHarness/dsh-web-ui/packages/dsh-git-manager
图 4,交付链路,组合包叠成档案,dsh plugin add 一键装进 DSH。
正式交付走组合包,开发调试则有更快的路子,DSH 支持在启动命令上直接叠一层配置(--patch),把本地源码路径临时插进插件列表。改完代码,配合前面说的热替换,几乎不用重启就能看到新效果。我用这个模式调了一下午界面,越调越顺手,像木匠在料场里现刨现装,不用等油漆干。等一切稳妥了,再收进组合包正式安装。
重启,打开界面,侧边栏出现一个「Git」。点开,仓库状态、变更列表、提交按钮,一样不缺。我在面板里第一次点下「提交」、看到账本上多了一条记录的时候,说实话,有点感动。人做一件东西,最快乐的时刻,就是它第一次听你话的时候。
八、两个让人记一辈子的坑
写 DSH 插件这一路,有两个坑值得单独讲,它们不是 git 的坑,是 DSH 插件开发本身的坑。
第一个,是「闹鬼」的错误。 代码写完,TypeScript 类型检查,第一次跑报错,第二次不报,第三次又报。同一个文件,同一个命令,结果看心情。折腾半小时真相大白,我的插件有「两个世界」,host 半区要用的 sessions 类型和 client 半区要用的 sessions 类型,是两个不同的定义,它俩不能待在同一个编译程序里,一见面就打架(TypeScript 管这叫 TS2717)。解法也干脆,给两个世界各开一间独立的编译房间(tsconfig 分层),谁也不碍着谁。
// tsconfig.host.json —— 服务器的世界// tsconfig.client.json —— 浏览器的世界// tsconfig.json —— 总管,只管把两个房间连起来这个坑给我的教训是,编译器很少无理由地发神经。 你以为它在抽风,其实它在提醒你,你的架构里有个根本的分歧没处理。这个「双世界」的分歧,正是 DSH 双面插件的代价,也是它的特色。
第二个,是 git 的怪脾气。 git merge 合并冲突,我的代码去 stderr(标准错误流)里找错误信息,没有。找半天,发现冲突信息写在 stdout(标准输出流) 里。git 这个老古董,一辈子没学过「错误要写进错误流」这条规矩,它把吵架的记录当成了正常的广播。教训同样朴素,你去找一个东西,最要紧的是先搞清楚它习惯待在哪里。
为了不再被这些坑反复坑,我写了 31 个测试用例,真的在临时目录里建仓库、改文件、跑 git。测试全绿的那一瞬间,我心里那个舒坦,不亚于把家里打扫干净。
九、零件与手艺
写这个插件,前前后后大半天。有人可能会问,DSH 插件这套体系,到底图个什么?
图的是把 AI 从「聊天框」变成「全能助手」。聊天框是死的,你问,它答。而 DSH 的插件让 AI 有了手,会记账、会管服务器、会查网页、会操作你电脑上的真实文件。每一次给 AI 装上一个插件,就等于教会它一门手艺。
更妙的是,这门手艺的学徒门槛低得惊人。三行代码就能起步,文档齐全,社区里的插件一个比一个能打。你不需要是编译器作者,不需要懂内核,你只需要想清楚一件事,你想让 AI 替你干哪件杂活?
王小波说过一句话,我很喜欢,「我活在世上,无非想要明白些道理,遇见些有趣的事。」开发 DSH 插件,道理我明白了一些,关于门禁、关于两个世界、关于自动清理。有趣的事也遇见了不少,打不死的小强、闹鬼的编译器、写错流的 git。
而更深一层,我其实在想,AI 时代,人到底还能干点什么?我的答案是,给 AI 装手。 机器再聪明,也得有人给它装工具、定规矩、划范围。给它装一双手,让它替我们干杂活,管账、跑腿、扫地,然后我们腾出手来,干那些真正有趣的事。
写代码这些年,我越来越觉得,工具这东西,贵在「顺手」。一双趁手的筷子,比一桌山珍海味更让人踏实。DSH 的插件体系,就像给每个程序员发了一双自己削的筷子,你想让 AI 怎么帮你,你就怎么削。Git 管理只是其中一双;管服务器的、管时间的、管日程的,都有人在做。工具越来越多,AI 越来越能干活,人越来越省心,这大概就是工具史的正常走向。
这件事,以后大概会有很多人做。我是先动手的那一个,我很高兴。
文中代码基于真实项目整理(DeepSeek Harness Web GUI 的 dsh-git-manager 插件),为便于阅读略有精简。DSH 插件机制细节以官方文档为准,deepseek-harness.github.io/deepseek-harness/develop/basic/。
夜雨聆风