乐于分享
好东西不私藏

Deepseek Harness插件开发指南(AI总结版)

Deepseek Harness插件开发指南(AI总结版)

DeepSeek Harness 插件开发指南

为什么会有这篇文章:写了两个插件之后我才想起来找插件的脚手架,发现没有;然后我想找个参考资料,仓库里面也没有。但是AI时代了,这不重要,让AI跑一个就好了。以下为Hy3基于仓库内文档和代码总结的(因为限时免费)

适用对象:想在 dsh 上扩展能力的开发者。本文档综合仓库 docs/packages/ 与 apps/cli 的真实规范,给出从"最小插件"到"打包分发"的完整路径,以及正确性自检方法。

一句话前提:在 dsh 里,一切皆插件(everything is a plugin)。产品能力(工具、模型适配器、子智能体、UI、系统提示词段落、钩子)都是挂在 Cordis 配置树上的插件。你写的任何新能力,最终都落成一个 cordis.yml 里的一行 name 指向的 TypeScript 模块。


1. 心智模型:配置树 + 插件模块

dsh 启动时不写死任何能力。它创建一个空的根 Context,加载 cordis.yml(其本质是 Loader 插件),按"层"逐层把插件挂载到树上。每一层是一个 YAML 数组,用三种形态贡献配置:

形态
写法
作用
插入新行
- insert: [{ id, name, config? }]
新增一个插件实例
覆盖整行
- { id: <已有id>, config: {...} }
用 id 命中已有行,整体替换其 config(不深合并)
禁用
- { id: <已有id>, disabled: true }
关闭该插件

示例(本地开发期最简叠加层):

-insert:-id:helloname:'/abs/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

关键约定

  • 插件路径必须绝对;patch 文件只贡献配置,不改变 Loader 解析模块的路径基准。
  • config 是整体替换:你在 patch 里 override 某行时,必须把该行需要的每个 key 都写全,不能只写改动的那个。
  • cordis.yml 的 config 下和 disabled 上允许 !!js 表达式(如 !!js ctx.myAppStartup.port ?? 3080);其他元数据必须是字面量(verify-cordis-config 会拒绝元数据里的表达式节点)。注意:是 !!js,不是 !js

2. 插件模块长什么样

一个插件就是一个导出 apply 函数的 TypeScript 模块。框架在加载插件时调用 apply,并传入 ctx

importtype { Context } from'@deepseek-ai/cordis'exportconst name = 'my-plugin'exportfunctionapply(ctxContext) {// 在这里注册能力console.log('[my-plugin] loaded')}

三种形态(选 function 即可,绝大多数场景够用):

// 对象形态exportdefault { name'my-plugin'inject: ['tools'], apply(ctx) { /* ... */ } }// 类形态(当你要提供一个 service 时)import { ServicetypeContext } from'@deepseek-ai/cordis'exportdefaultclassMyServiceextendsService {static inject = ['tools']constructor(ctxContext) { super(ctx, 'myService') }}

自动清理:通过 ctx 注册的一切(事件监听、工具、定时器)在插件卸载时自动回收。需要显式清理的资源(如网络连接)用 ctx.effect() 提供 disposer:

exportfunctionapply(ctxContext) {  ctx.effect(() => {const timer = setInterval(() =>console.log('heartbeat'), 5000)return() =>clearInterval(timer) // 卸载时执行  })}

3. 第一个可运行插件

# 仓库根目录下mkdir -p scratch-plugin/src

scratch-plugin/src/my-plugin.ts

importtype { Context } from'@deepseek-ai/cordis'exportconst name = 'hello-plugin'exportfunctionapply(ctxContext) {  ctx.effect(() => {console.log('[hello-plugin] plugin loaded!')  })}

scratch-plugin/cordis.yml(用 pwd 拿绝对路径替换):

-insert:-id:helloname:'/abs/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

启动并观察:

pnpm dsh web --patch ./scratch-plugin/cordis.yml# 终端会打印 [hello-plugin] plugin loaded!

4. 插件配置(Schemastery)

任何"两个部署可能想设不同值"的东西,都必须是 config 字段,不能硬编码。

importtype { Context } from'@deepseek-ai/cordis'importSchemafrom'@deepseek-ai/schemastery'exportconst name = 'my-plugin'exportinterfaceConfig {greetingstringmaxRetriesnumbermode'fast' | 'accurate'}exportconstConfigSchema<Config> = Schema.object({greetingSchema.string().default('Hello'),maxRetriesSchema.number().default(3),modeSchema.union(['fast''accurate']).default('fast'),})exportfunctionapply(ctxContextconfigConfig) {// config 已由 schema 校验并填充默认值,类型安全console.log(config.greeting)}

在 patch 行里给 config

-insert:-id:helloname:'./src/my-plugin.ts'config:greeting:'Hi there'maxRetries:5
  • 导出 Config 必须是 Schema 实例,不能是普通对象(Cordis 需要 Standard Schema 接口)。
  • 校验在加载期运行,非法配置会带着可执行的错误信息失败。
  • 改 config 会热替换插件(HMR 生效),因为注册都是 effect,旧实例自动清理。

5. 工具插件(最常见)

工具注册在 ctx.tools 上。第一方工具用 defineTool 这个带类型推断的 helper:

importtype { Context } from'@deepseek-ai/cordis'import { defineTool } from'@deepseek-ai/dsh-tools'exportconst name = 'greet-tool'exportconst inject = ['tools']exportfunctionapply(ctxContext) {  ctx.tools.register(defineTool({name'greet',description'Greet someone by name.',parameters: {name: { type'string'requiredtruedescription'The name to greet' },    },output: {schema: { type'string' },render(_args, value) => [{ type'text'text: value }],    },asyncexecute(args) {return`Hello, ${args.name}!`    },  }))}
  • inject: ['tools'] 让 Cordis 等工具注册表就绪后再跑 apply
  • defineTool 从 parameters 推断并校验 argsexecute 返回 output.schema 声明的值;output.render 把它转成面向模型的内容块。
  • 也可直接 ctx.tools.register() 裸 JSON-Schema ToolDefinition(MCP 工具就是这样进来的)。
  • 进阶:嵌套 schema、后台执行(run_in_background)、执行策略钩子、Code Mode、UI 卡片,见 docs/cookbook/adding-a-tool.md

6. 服务与依赖(capability seam)

一个插件可以把能力作为服务暴露给别的插件;别的插件用 inject 声明依赖。

import { ServicetypeContext } from'@deepseek-ai/cordis'declaremodule'@deepseek-ai/cordis' {interfaceContext { metricsMetricsService }}exportdefaultclassMetricsServiceextendsService {static inject = ['llm']constructor(ctxContext) { super(ctx, 'metrics') } // 'metrics' 即 ctx.metricsrecord(eventstringvaluenumber) { /* ... */ }}

消费者:

exportconst inject = ['metrics']exportfunctionapply(ctxContext) {  ctx.metrics.record('tool_call'1)}

依赖行为

  • inject 是必需依赖:服务缺失时插件等待而非运行;服务消失时依赖方自动卸载、服务回来再重载。
  • 可选依赖:不写 inject,用 ctx.get('metrics')?.record(...) 在使用点查询。
  • 服务隔离cordis.yml 的 group 行 + isolate 可让不同插件组看到同一服务的不同实例(见 docs/user/develop/framework/service.md)。

能力接缝三角色(架构核心):一个可替换能力 = Service Definition(定义)+ Service Provider(实现)+ Consumer(消费) 三件套,缺一不可,只有当三者独立演化时才拆分(shell 三件套是模板)。


7. 钩子与扩展点

新行为应当挂在已文档化的扩展点上,而不是改 agent-loop。核心扩展点(详见 docs/cookbook/extension-cookbook.md 的 feature→mechanism 表):

想要的能力
挂哪
权限/能力级拒绝
tools/pre-execute
 返回 { kind: 'deny' } 或 { kind: 'ask' }
工具超时/重试/指标
tools/execute
(可替换 exec.signal
结果转换
tools/post-execute
只读审计最终产物
tools/result
系统提示词段落
ctx.systemPrompt.section()
子智能体委派
ctx.subagents
 + dsh-tool-subagent
MCP
每服务器一个插件:发现工具 → ctx.tools.register()
UI / 调度任务
session/event
 监听 + agent.followup()/steer()

waterfall 监听器必须调用 next() 才能委托;不调用会短路整条链。


8. LLM 适配器插件

新增模型供应商 = 继承 LlmAdapter 并通过 registerAdapter 注册路由:

importtype { Context } from'@deepseek-ai/cordis'exportconst name = 'llm-my-provider'exportconst inject = ['llm']exportfunctionapply(ctxContext) {  ctx.effect(() => ctx.llm.registerAdapter(    ['my-provider'],                 // 路由键(全局唯一)newMyAdapter(),                 // 继承 LlmAdapter  ))}
  • 参考实现:packages/llm/llm-deepseekpackages/llm/llm-pi-ai
  • 多模型各自不同 baseURL/key = 多个 provider routellm-pi-ai 的 Config.providers 是以 route 为键的字典,每个 route 自带 baseURL + apiKeyEnv(凭据引用,不是密钥本身)。
  • 路由键全局唯一,撞键会导致整层注册原子失败。

9. UI 插件

Host 侧(最简):监听 session/event 流(助手 token、turn/step 边界、工具活动),用 agent.followup()/agent.steer() 把输入喂回去。

exportconst inject = ['agents']exportfunctionapply(ctxContext) {  ctx.on('session/event'(_session, event) => {if (event.type === 'assistant/chunk' && event.data.chunk.type === 'text-delta') {render(event.data.chunk.text)    }  })}

Client 侧(Web 业务行):注册 ConversationNodeDefinition + conversation.chat.node 的 keyed renderer,遵循 docs/cookbook/adding-a-conversation-node.md

  • 客户端包要 dsh.client 声明、exports["./client"]、用共享 tsdown 预设(见 packages/client/AGENTS.md)。

10. 预设(agent preset,零代码)

预设是"把一个智能体的 persona + 工具清单 + 系统提示词"打包成一份 cordis.yml,由 agent-presets 行发现。这是不需要写 TypeScript 的一类扩展:

-id:my-presetname:'@deepseek-ai/cordis-plugin-group'group:trueconfig:-name:'@deepseek-ai/dsh-tool-subagent'config: { /*三个专家实例各带persona*/ }# ... 其他工具

参考:packages/preset/agent-presetsexamples/*/cordis.yml


11. 打包与安装(bundle vs profile)

两个概念、两份 manifest(都写在 package.json 的 dsh 键下):

  • bundle = 你编写并分发的 npm 包,声明 dsh.bundle,贡献一个 cordis.patch.yml 配置层。
  • profile = $DSH_HOME/profiles/<name> 下的目录,声明 dsh.profile.bundles 有序列表,决定组合哪些 bundle。

最小 bundle 包结构:

hello-plugin/├── package.json        # 声明 dsh.bundle├── cordis.patch.yml    # 被 profile 应用的层└── index.js            # patch 行引用的插件模块

package.json

{"name":"dsh-hello-plugin","version":"0.1.0","type":"module","main":"index.js","files":["index.js","cordis.patch.yml"],"dsh":{"bundle":{"patch":"./cordis.patch.yml"}}}

cordis.patch.yml(用包名而非路径,让 Node 解析安装后的代码):

-insert:-id:helloname:dsh-hello-plugin

安装到 profile(首次会初始化 profile,并以 dsh-base 为首个 bundle):

dsh plugin --profile demo add ./hello-plugin# 等价于 pnpm add,并把该 bundle 追加到 dsh.profile.bundles

层叠顺序(后者赢、整行替换):

  1. 1dsh.profile.bundles 列表顺序(先 dsh-base,再按添加顺序)
  2. 2profile 自身的 cordis.patch.yml
  3. 3机器级 $DSH_HOME/cordis.patch.yml
  4. 4每个 --patch <path> overlay(按 argv 顺序)

验证组合而不启动(最接近"正确性检查"的工具):

dsh --profile demo --dump-config   # 打印组合树,标注每行来源文件与 overlay 变更

卸载:dsh plugin --profile demo remove dsh-hello-plugin(同时移除依赖与层)。

从 git 安装有个坑:git 装的是源码不是构建产物,需要作者提供 prepare 脚本自包含构建,且用户需在 pnpm-workspace.yaml 里 allowBuilds 授权(pnpm ≥10 默认拒绝)。不想让用户授权,就发布到 npm(发布时 lib/ 已构建)或发 tarball。


12. 正确性与质量闸门

dsh 没有一键生成正确骨架的命令(见配套文档 plugin-scaffold-design.md),所以"正确"靠你内化约定 + 闸门校验。必须知道的硬约束:

  1. 1包名与 peerDependency:每个包是 @deepseek-ai/dsh-<name>@deepseek-ai/cordis 同时出现在 peerDependencies 和 devDependencies(同版本范围)。vendored 包 rescoped 且 private: true
  2. 2resolver manifest 依赖:在 cordis.yml 里用绝对/相对路径引用的"裸插件(bare plugin)",必须出现在其 resolver manifest 的 dependencies 里——verify-cordis-config 强制校验。把包放进 packages/<group>/<pkg>(pnpm workspace + tsconfig wildcard 自动发现)即可满足。
  3. 3ESM 全程"type": "module",本地相对导入用显式 .ts 后缀(export * from './types.ts');dsh CLI 源码启动走 tsx 的 ESM-only 钩子,被它加载的模块必须保持 ESM。
  4. 4注册即 effect:所有贡献走 ctx.effect()/ctx.on()
  5. 5可执行闸门pnpm run hygiene 内含):
    • 0verify-cordis-config
      :校验 Loader 元数据里没有表达式节点、裸插件依赖齐全。
    • 0constraints / check-workspace-constraints.ts:校验 private:true、版本与根一致、type:modulemain/types/exports 形态、files 清单等包不变量。
    • 0knip(死代码)、publintverify-package-invariantsverify-built-package-invariants
  6. 6README 必需结构:服务 API、config、事件、扩展点、设计笔记 + gated 的 Model Experience 段 + Known Limitations and Deferred Work 段(除非加入 allowlist)。

新包标准校验流程:

pnpm installpnpm run doc-syncpnpm run constraints && pnpm run typecheck && pnpm run lintpnpm run build && pnpm run hygiene

13. 开发者自检清单(避坑)

  • 插件模块导出了 name 与 apply(或 default 对象/类)。
  • 依赖的服务都写进 inject;可选依赖用 ctx.get()
  • 所有部署可变值都是 Config 字段(Schemastery),没有硬编码 TIMEOUT = 30000
  • patch 里 override 已有行时,config 写全了所有 key(整体替换语义)。
  • cordis.yml 里表达式用 !!js 而非 !js,且只出现在 config/disabled 上。
  • bundle 包 package.json 声明了 dsh.bundle.patch,且 cordis.patch.yml 用包名引用而非裸路径。
  • 裸路径插件已加入 resolver manifest 的 dependencies
  • 本地先 dsh --profile <x> --dump-config 确认层叠正确,再启动。
  • 新行为挂在扩展点上,没有改 agent-loop
  • 跑过 pnpm run hygiene 全绿。

14. 参考文档索引

  • 新手:docs/user/develop/basic/(第一个插件 / 工具 / 配置 / 打包)、docs/cordis-tutorial/
  • 框架:docs/user/develop/framework/(service、events)、docs/cordis-primer.mddocs/architecture.md
  • Cookbook:docs/cookbook/(extension-cookbook、adding-a-package、adding-a-tool、adding-an-llm-adapter、adding-a-conversation-node)
  • CLI:apps/cli/reference/README.md
  • 真实模板:packages/subagent/tool-subagent(工具+子智能体)、packages/llm/llm-pi-ai(多模型适配器)