DeepSeek Harness 的扩展开发手册:一个扩展的完整生态位

2026-08-17

你想给 dsh 加一点东西:拦一次工具调用、往系统提示词里插一段、让某个会话事件流出去落成 JSONL。第一个卡住人的问题不是「怎么写」,而是这段代码该待在哪儿——它是一个工具、一个钩子、一个 UI,还是一个协议驱动?挂错了地方,写得再对也接不上循环。

deepseek-harness 仓库里有一份专门回答这个问题的文档:docs/cookbook/extension-cookbook.md(同目录下有中文版 extension-cookbook.zh.md)。开篇它就把自己的边界写死了——里面的片段省略了 import 和辅助实现,不是能复制粘贴直接跑的代码;工具定义的真源在 docs/cookbook/adding-a-tool.md,系统与扩展点的映射归 docs/architecture.md 管。这份手册只负责一件事:告诉你形态怎么挑。

先把限定摆在前面:该仓库 README 自述处于开发者预览阶段,并明确写了未来会有破坏兼容性的变更。下文出现的扩展点名、服务键、字段名、默认值,全部按仓库快照 47f9438(版本 0.1.0-rc.5)实读,随时可能变。

四种形态,边界比想象中窄

手册把扩展分成四节:工具插件、钩子插件、UI 插件、外部协议驱动。

工具那节短得反常,只有一段话。它说工具注册在 ctx.tools 上,带注解的 defineTool 例子在 adding-a-tool.md 里,那份指南才是真源。真正有信息量的是后半句:ctx.tools.register() 也直接接受原始 JSON Schema 的 ToolDefinition——MCP 来源的工具就是这样进来的;defineTool 只是第一方工具用的类型化辅助函数。也就是说注册表底下是两条并行入口,你从 MCP 桥接过来的工具和仓库自带的 dsh-tool-* 在注册表眼里是同一类东西。

钩子那节的标题是「钩子插件(以权限门禁为例)」,正文特意加了一句纠偏:钩子插件也可以拦截其他扩展点,它本身并不等同于权限门禁。手册给的例子是在 tools/pre-execute 上返回一个类型化决策:

export function apply(ctx: Context) {
  ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
    if (!(await isAllowed(exec))) {
      return { kind: 'deny', reason: 'Denied by policy.' }
    }
    return next()
  })
}

这段的价值不在代码本身,在它下面那段选择规则。手册说这个 waterfall 是可重排的策略层,然后给了四选一:需要单调的最终拒绝,用 ctx.tools.guard();需要包裹实际分发的生命周期(超时、重试、指标),用 tools/execute,并且明写只有 exec.signal 可替换;要显式变换结果,用 tools/post-execute;只是想观察不可变的最终结果,用 tools/result。完整的选择规则被指到 adding-a-tool.md#execution-policy-and-observation

「原生钩子」这个词也在这节被定义清楚了:它就是拦截点上的一个普通 Cordis 插件,不需要外部协议。仓库另有 dsh-hooks-claude-code / dsh-hooks-codex 两个桥接器,负责把外部格式的钩子配置文件映射到这些扩展点上。

「功能 → 机制」那张表是可验证性声明

手册最后一节是一张表,左列产品功能,右列插件机制。表前有一句话值得单独拎出来:每个产品功能都映射到一个文档化扩展点上的监听器,没有任何一行修改循环本身——手册自述这是把「微内核」这个说法变成可检查的东西。

表里有两行特别能说明扩展点的粒度。上下文压缩那行写的是 ctx.compaction seam 加 dsh-compaction-basic,其中自动压力检查跑在串行的 agent/pre-step,溢出恢复跑在 agent/request-error,手动调用方走同一个压缩服务。定时任务那行更直白:定时器触发后,空闲时followup(…, {source: {kind: 'cron', …}})忙碌时改走 inject() 通知。这两行的意思是同一件事在不同时机走不同扩展点,你写监听器时得先想清楚自己要接的是哪个时机。

表前还埋了一处坑,手册用了整整一段来提醒:system-prompt/assemble 是一个整体装配变换,它返回的装配结果具有权威性,因此监听器作者有责任保留活跃的 Code Mode 和结构化输出协议的贡献。翻译成人话——你在这个点上返回什么,什么就是最终的系统提示词,别人先前塞进去的东西你不保留就没了。同一段还给了替代建议:需要在展示、查找、执行三处保持对齐的工具过滤,优先用 ctx.tools.restrict(),别自己在装配阶段删。

生态位的最外圈:让模型自己造一个扩展

以上都是「人写扩展」。仓库里还有一组包把这件事推到了另一端:packages/extensions/ 下的四个包,让 agent 修改自己所在的运行时。注意这里是 packages/*/* 两层结构——按这个两层 glob 我们数出 226 个 package.json(全仓库 git 跟踪的 package.json 一共 248 个),而 packages/extensions/ 这一组下面只有 4 个包:tool-cordiscordis-host-runnercordis-client-runnerui-cordis

packages/extensions/README.zh.md 的分工表:tool-cordis 注册面向模型的工具,cordis-host-runner 提供定义注册表、host 半的 node:vm 沙箱与 request-run 往返,cordis-client-runner 是双半包的浏览器半,ui-cordis 提供浏览器面的操作面板。

真正落地的一处是 cordis_define 的入参。按生成的 docs/tool-catalog.md,它有四个必填字段:pluginnamepurposecodeplugin 是个 oneOf——kind: "new" 时只提交一个 idPrefixkind: "existing" 时提交已有的 pluginId 追加一个新包。code 下面两个可选键 hostclient,schema 描述里写死了它们是「plain JavaScript function body」,不做 TypeScript、JSX 或 import 转换。这个约束在源码里也能对上:packages/extensions/cordis-host-runner/src/index.tsdefine() 里,idPrefix 要过 /^[a-z]{3,6}$/ 这条正则,不过就直接抛错。

配置只有一个字段。packages/extensions/cordis-host-runner/src/index.ts 里写的是 vmTimeoutMs: z.number().min(1).default(5000),README 的配置表也是这一行,并明说「就这一个字段」——因为一次 run 请求等的是人,往返本身没有截止期限。这里必须按 spec 的规矩说清楚:5000 是配置默认值,不是「你用起来会怎样」的保证;而且 README 的已知限制里自述 vmTimeoutMs 只约束同步求值,async 的 host 半函数体会逃出这个上限

四处文档与源码对不上的地方

这套包的文档写得很细,但我们逐处回源时对上了几处差异。按规矩只陈述差异、标出位置,不推断原因。

说法出处文档写的我们在源码里读到的
packages/extensions/tool-cordis/README.zh.md 第 5 行runner 的服务键是 ctx.dynamicsrc/index.tsinject['tools', 'systemPrompt', 'dynamicCordisRunner', 'cordisInspect']
同上,第 5 行与「功能」一节「五个面向模型的工具」,逐条列的是 cordis_inspectcordis_definecordis_runcordis_stopcordis_undefinesrc/index.ts 注册 7 个:cordis_inspect_listcordis_inspect_querycordis_inspect_selfcordis_definecordis_runcordis_stopcordis_undefine
同上,cordis_define 一节「铸出 dyn-<n> 标识」src/registry.tsmintPluginId 拼的是 `${prefix}-${n}`mintPackageIdpkg-<n>mintPluginRunIdrun-<n>
cordis-host-runner/README.zh.md 转发事件一段四条:cordis/request-runcordis/request-run-resolveddynamicCordisRunner/packagedynamicCordisRunner/retractsrc/types.ts 声明六条,后四条名为 cordis/dynamic-packagecordis/dynamic-retractcordis/inspect-querycordis/inspect-query-resolved;白名单 packages/api/remotes/src/remote-events.ts 也是这六条

还有一处是只在一侧出现tool-cordis 的 README(中英两版都有)提到「浏览器确认窗口(ackTimeoutMs)属于 runner 服务」,并把读者指向 cordis-host-runner/README.md#config;而那张配置表里只有 vmTimeoutMs,我们在这两个包的源码里也没有搜到 ackTimeoutMs 这个标识符。以我们实读的源码为准,说完就停。

顺带说一句表里第一行的核对办法:docs/tool-catalog.md 是生成文件,头部注释写明由 scripts/gen-tool-catalog.ts 产出、由 pnpm run verify-tool-catalog 守新鲜度,而且这个生成器不是静态扫描——它会在真实 context 上启动每个工具插件再读 ctx.tools.schemas()。所以它列出的工具名和源码注册的那 7 个是一致的。

这套东西不在任何已交付的组合里

最后一处必须说清楚,因为它直接决定你要不要碰。

docs/tool-catalog.md 的工具包映射表里,@deepseek-ai/dsh-tool-cordis 那一行的部署备注原文写明:它不在任何已交付的插件树里,这是一个刻意的 opt-in,理由是动态包的代码会触达真实运行时。同一行还写了另一件事:一个运行中的包可以注册额外的、模型能看见的工具,直到它被停止、被删除定义或 DSH 重启为止。

tool-cordis 的 README 在「信任立场」一节说得更直接:那个沙箱隔离全局变量,但不是安全边界;Node 全局变量不存在或被重定向到 ctx.fsctx.webctx.bash 等 Cordis 服务,写入 globalThis 的内容保持局部,但 host realm helper 使逃逸成为可能。README 自己给的处置口径是:应当像对待 bash 访问一样对待这套工具集。所以这里不存在「有沙箱所以安全」这回事——它跑的是本机进程,能拿到的服务是活的。

还有两条限制是写给做自动化的人的。第一,带浏览器半的包必须由一个页面来执行,run 会变成一次可作答的往返;在没有页面连接的地方它会一直挂起——README 点名 headless 与 ACP 部署会把这次 run 挂到提问的那一轮次被取消为止。第二,挂起的 run 请求没有超时,它一直等人。README 由此给出的结论是:无人值守的自动化用不了带浏览器半的包。

另外 tool-cordis 的 README 里还记了一条容易踩的边界:动态包拿到的 ctx façade 不公开 effect(),包代码没法注册定制 disposer,受支持的清理路径是 onprovidetools.register

回到最开始那个问题

如果你要往 dsh 里加一段代码,这份手册给的判断顺序其实很清楚:先确认你要做的事在「功能 → 机制」那张表里对应哪一行,照它给的扩展点挂;工具的具体写法去 adding-a-tool.md,别照抄手册里的片段;策略层四个点按「单调拒绝 / 包裹分发 / 变换结果 / 只观察」四选一;碰系统提示词装配时记得你返回的东西是权威的。至于 packages/extensions/ 那一套,它解决的是完全另一个问题——让模型在进程内临时造一个扩展再撤下来——而且它自己文档里就写着不在任何交付组合里,要用得自己显式装上。

上面每一处都能在仓库里找到落点:docs/cookbook/extension-cookbook.md 看形态与那张映射表,docs/tool-catalog.md 看模型实际收到的 schema,packages/extensions/cordis-host-runner/src/index.tsvmTimeoutMs 的默认值和 idPrefix 的正则,packages/extensions/cordis-host-runner/src/registry.ts 看三个 id 怎么铸出来。文档口径与源码有出入时,以你自己打开的那个文件为准。


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。 安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。