DeepSeek Harness 插件开发简易指南
DeepSeek Harness 插件开发简易指南
一、先理解架构:DSH = Cordis 插件系统 + 补丁式组合
DSH 不是单体应用,而是构建在Cordis 插件框架(@deepseek-ai/cordis,Koishi 系)之上的分层插件树。
| 概念 | 说明 |
|---|---|
| Profile | $DSH_HOME/profiles/<name>/(我的是C:\Users\AIcncc\.dsh\profiles\web)。含package.json(声明dsh.profile.bundles有序组合包列表)+ 用户自己的cordis.patch.yml |
| 组合包(bundle) | 声明了"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }的 npm 包。dsh-base、dsh-web-app都是 bundle |
| 补丁分层 | 空条目根cordis.yml→ 各 bundle 的 patch(按序)→ profile 级cordis.patch.yml→ home 级~/.dsh/cordis.patch.yml→--patchoverlay。后层按id覆盖前层整段config,insert添加新行,支持!!js表达式 |
| 插件行 | 每个插件是一行{ id, name, config?, disabled? }。name是模块说明符,config由插件自己的Config(schemastery)校验 |
| 安装 | dsh plugin --profile web add <pkg>把 pnpm 参数原样转发到 profile 目录,把包装进 profile 的依赖 |
| 激活 | Loader 并发挂载条目,服务可用性驱动激活(inject声明依赖,先有提供方后激活消费方) |
工具目录中几乎所有可见能力(run_code、pwsh、todo_write、subagent、workflow…)都是一个插件包,例如dsh-tool-todo、dsh-tool-pwsh——这就是你插件的参照物。
二、插件的标准形态(源码确认的约定)
第一方插件使用命名空间导出(无默认导出,docs/postmortem/0001规定默认导出会丢失inject):
// lib/index.ts —— 一个最小但完整的工具插件importzfrom'@deepseek-ai/schemastery'import{defineTool}from'@deepseek-ai/dsh-tools'exportconstname='my-tool'// 插件名(kebab-case,全局唯一)exportconstinject=['tools']// 声明注入的服务键,Loader 据此排序exportconstConfig=z.object({// schemastery 配置 schema(可留空对象)greeting:z.string().default('Hello from my plugin'),})exportfunctionapply(ctx:Context,config:ConfigType){ctx.tools.register(defineTool({name:'my_hello',// 模型可见的工具名(snake_case)description:'Say hello. Returns a friendly greeting.',parameters:{name:{type:'string',required:true,description:'Who to greet'},loud:{type:'boolean',description:'Uppercase the greeting'},},output:{schema:{type:'string'},render:(_args,value)=>[{type:'text',text:value}],},asyncexecute(args,exec){// exec.signal 必填且只读——必须观测/转发取消信号constmsg=`${config.greeting},${args.name}!`returnargs.loud?msg.toUpperCase():msg},}))}三条硬性规范(注册表强制校验):
output: { schema, render }必填——没有输出声明注册直接失败;execute只能返回输出 schema 声明的无损 JSON,通过exec.signal协作停止;Config、name、inject、apply四个导出缺一不可(Config可省,但专业插件应带配置)。
三、五类插件(按你的需求选型)
| 类型 | 注入/API | 用途 |
|---|---|---|
| 工具插件(最常见) | ctx.tools.register(),schema 自动流入系统提示词 | 给模型新增能力(读文件、查库、调 API…) |
| 服务插件 | ctx.provide('myService', impl)+ 消费方ctx.inject(['myService']) | 在工具/其他插件之间共享状态,如dsh-session、dsh-jobs-local |
| Skill 插件 | ctx.skills.register(...)或dsh-skill-filesystem目录 | 给 agent 注入指令/方法论(比工具轻量,不进工具列表) |
| 客户端 UI 插件 | dsh-client-*系列,ctx.slots.register(React) | 自定义 Web 界面(会话卡片、设置页、工具调用展示) |
| Host 插件 | ctx.get('webserver')/apiproxy/frontend-static | 起服务、挂路由、托管静态资源 |
| 组合包 bundle | 包内cordis.patch.yml | 把一组插件+配置打包成可复用发行单元 |
工具自带 UI 呈现用presentCall/presentResult返回 card 意图(generic/terminal/read/diff/search/web),UI 无需按工具名写特例。
四、标准开发流程(六步)
1. 初始化工程
mkdirdsh-my-plugin&&cddsh-my-pluginnpminit-ynpmi-Dtypescript tsup @deepseek-ai/cordis @deepseek-ai/dsh-tools @deepseek-ai/schemastery# package.json: "type": "module", "main": "lib/index.js", "types": "lib/index.d.ts"2. 写插件(见上文模板)
3. 构建
tsup lib/index.ts--formatesm--dts--out-dir lib4. 本地安装到 profile(两种方式)
# 方式 A:发布后安装dsh plugin--profilewebadddsh-my-plugin# 方式 B:本地开发(推荐,file: 引用即改即用)cd~/.dsh/profiles/webpnpmaddD:\path\to\dsh-my-plugin5. 注册进加载树——编辑~/.dsh/profiles/web/cordis.patch.yml
# 当前你的文件是 [],改成:-insert:-id:my-pluginname:dsh-my-pluginconfig:greeting:你好要点:
id是 patch 寻址键(后续可用- id: my-plugin+config:覆盖);name必须是 profile 依赖里真实存在的模块说明符。文件热重载(watchUserPatches),改了立即生效,无需重启——但首次安装包后需要重启dsh web。
6. 验证
dsh web --dump-config# 离线合成配置树,确认你的行已合入# 或进入会话后让模型执行 cordis_inspect(自省工具,列出全部已注册工具/服务/插件 fiber)五、"专业标准完整"插件清单(对标dsh-tool-todo/dsh-tool-pwsh)
源码里第一方工具普遍具备以下工程素养,照做即是"专业标准":
- 名称纪律:包名
dsh-*(第三方常dsh-*或dsh-plugin-*),插件namekebab-case 唯一,工具名 snake_case 且描述首句就是完整指令(模型看到的第一句决定它会不会用)。 - Config 带默认值 + 部署语义:
z.object({ allowParallel: z.boolean().default(true) })——配置是部署者政策,不是插件内部细节;配置变更记录进描述(如 todo 的并行策略会改写工具描述)。 - 类型化参数与严格 schema:
defineTool参数用ParameterSchemaSpec;additionalProperties: false封闭对象,让模型写错即失败(INVALID_ARGS),而不是静默吞掉。 - 规范化输出契约:输出
{ schema, render, presentationMeta? };值(机器消费)与呈现(模型消费)分离;render是纯函数,UI 流式回放时会反复调用。 - 错误即结果:可预期的失败返回
{ isError: true, error: { message, info } }而不是抛异常;基础设施失败才抛HarnessError(带name/code)。 - 协作取消:
execute(args, exec)必读exec.signal,把signal透传给底层(readFile(path, { signal })、fetch(url, { signal })),绝不在已启动的 Promise 未结算时提前返回。 - 并发安全声明:可并发的工具实现
isConcurrencySafe(args)返回 true;共享状态竞态必须可交换,否则拒绝。 - 状态写入会话日志而非内存:持久状态用
exec.agent.session.append('my/write', data)(事件溯源),重放/UI 都从事件渲染(todo 的todos投影就是这么做的)。 - 文档与双语文案:
README.md+README.zh.md(含配置表、公开 API、扩展点、模型体验、KV Cache 影响、已知限制)。 - 测试与门禁:schema 验证、执行器单测、
verify门禁(如verify-cordis-catalog防止契约漂移)。
六、调试与快速原型三板斧
- 动态插件(零安装验证):会话里让模型执行
cordis_define(提交 host 半 + 可选浏览器半)→cordis_run沙箱求值 →cordis_stop/cordis_undefine。原型验证用这个,正式落地再走上面六步。注意:动态包不跨重启、不写文件、不会自动变成正式插件。 - 配置排障:
dsh web --dump-config看最终组合树;--dump-default-config看 bundle 层(不含你的 patch)。 - HMR:改
cordis.patch.yml秒级生效;改插件源码需重新 build + 依赖引用为file:时自动跟随。
七、安全边界(必须知道,否则插件不合格)
- 沙箱:文件操作经
dsh-fs-sandbox(当前workspace-write);命令经dsh-bash-sandbox/dsh-pwsh-sandbox。插件不能绕过,只能走sandbox_permissions升级通道(需用户批准)。 - 审批 seam:
ctx.get('approval')(ask/deny/allow);未部署时ask退化为拒绝——插件必须把拒绝当正常路径处理。 - 作用域:普通上下文注册 = 全局;
agent.ctx注册 = 仅该 agent 并遮蔽同名全局。ctx.tools.restrict()是可见性组合,不是权限边界。 - 内容替换不是保密边界:编程消费方不能收到的值,要阻止或替换(post-execute),不能指望 finalizeContent 兜底。
八、实践路径
如果你的环境已就绪(dshCLI、web profile、cordis_inspect自省工具都在)。最省事的上手路线:
- 用
cordis_define/cordis_run在会话里验证一个 30 行的工具原型(比如"读 Excel 并统计"); - 原型通过后,按第四节的六步把它工程化成一个
dsh-<name>npm 包,file:依赖挂进~/.dsh/profiles/web; - 在
cordis.patch.yml插入一行即可被当前 Web 会话加载。
