DeepSeek Harness 插件开发指南面向已有 Node.js / TypeScript 基础的开发者。读完本文你可以独立完成开发一个 DSH 插件 → 本地调试挂载 → 发布到社区并被他人安装。版本说明DeepSeek Harness 目前处于开发者预览Developer Preview阶段迭代很快官方明确声明会有兼容性破坏变更。文中机制基于deepseek-ai/dsh0.1.0-rc.x 时代的公开资料整理动手前请以官方仓库文档GitHubdeepseek-ai/deepseek-harness和dsh --dump-config的实际输出为准。目录为什么现在是风口核心概念一切皆插件开发环境准备编写你的第一个插件插件开发的硬性规则实战一模型可见的工具插件实战二拦截事件的钩子插件声明式配置Config把插件挂载到 dsh三条路径如何上传 / 发布插件打包规范与发布前检查清单测试与质量门常见问题排查FAQ一、为什么现在是风口DeepSeek Harness命令行简称dsh是 DeepSeek 开源的 Agent 框架agent harness架构上「一切皆插件」模型适配器、工具注册表、会话日志、甚至 Agent 主循环本身都是插件整个产品就是启动时从若干配置层组合出来的一棵插件树。对开发者友好的几点现状核心仓库暂不接受外部 PR官方把贡献路径明确指向生态发布插件、写教程、答社区问题、报 issue。插件就是普通 npm 包没有专门的注册中心发布门槛极低。官方指定的发现渠道只是给 GitHub 仓库打一个dsh-plugin话题Topic就会被社区聚合目录收录。社区内测期间已出现数百个公开插件awesome 目录统计 900但大量细分领域仍是空白。二、核心概念一切皆插件2.1 没有「内核 插件」的分层安装目录下的约 195 个deepseek-ai/*包全部是 Cordis 插件——工具、LLM 适配器、会话持久化、Web 服务器、前端 UI、沙箱策略都不例外。你写的插件和官方的dsh-tool-bash地位完全相同没有「插件 API」和「内核 API」之分。加能力 往组合里加一行改行为 用 patch 覆盖已有的行2.2 底层框架 Cordis五个必须记住的想法想法含义插件一个实现了 Service 的对象最常见是带apply(ctx)的函数也可以是带inject的对象或Service子类上下文Context服务的仓库。服务挂到稳定的ctx.key如ctx.tools、ctx.llm、ctx.sessions插件之间通过 key 找服务不 import 具体实现inject声明插件需要的必需服务。loader 会等待这些服务存在后再执行插件加载顺序由依赖决定而不是文件顺序类型化事件服务通过声明合并定义事件用 emit / waterfall / parallel / serial 分发给监听者可逆的注册工具 schema、监听器等都通过ctx.effect()/ctx.on()注册插件卸载HMR、热重载、关停时一切自动回滚扩展点事件 / 服务就是 dsh 的「API」。改行为时优先挂在扩展点上不要去改主循环。2.3 Profile 与 Bundle两个关键概念概念manifest回答的问题bundle插件分发单元dsh.bundle指向 patch 文件「这个包贡献什么」——一个配置层cordis.patch.yml由 npm 包分发profile可运行组合dsh.profilebundles 列表「哪些 bundle 按什么顺序组成这个运行实例」bundle 是作者分发的单元profile 是用户启动的单元dsh plugin命令负责维护 profile。启动时配置层的叠加顺序后层覆盖前层profile 清单里列出的各 bundle按顺序profile 自己的cordis.patch.yml家目录级$DSH_HOME/cordis.patch.yml对本机所有 profile 生效命令行--patch path覆盖按 argv 顺序查看你的机器实际组合出的插件树dsh--profileweb --dump-config打印出来的任何一行都可以用你自己的 patch 替换。patch 按行的id定位要么整行替换其config不是深合并要么插入新行。三、开发环境准备3.1 前置要求Node.js官方声明范围^22.19.0 || 24.0.0不确定时直接用 Node 24。pnpmdsh plugin子命令会把参数原样转发给 profile 目录里的 pnpm没有 pnpm 会直接报错npminstall-gpnpmDeepSeek API Key运行真实模型时需要把DEEPSEEK_API_KEY放进根目录.envpnpm dsh会自动加载。没有 Key 也可以先写代码、跑单元测试和--dump-config验证。3.2 安装 dsh# 方式一直接从 npm 运行推荐普通开发者npx deepseek-ai/dsh web# 默认在 http://127.0.0.1:3080 启动 Web UI# 方式二克隆源码开发推荐要深度调试的开发者gitclone https://github.com/deepseek-ai/deepseek-harness.gitcddeepseek-harnesspnpminstallpnpmrun build# 不要省只装依赖不构建会导致 Web 页面缺产物pnpmdsh web3.3 建议建一个隔离的调试 profile开发期间用一个独立 profile如--profile dev安装开发中的插件日常使用的webprofile 保持稳定两者互不干扰。四、编写你的第一个插件4.1 最小函数插件创建hello.tsimporttype{Context}fromdeepseek-ai/cordisexportconstnamehelloexportfunctionapply(ctx:Context){ctx.logger.info(hello from my first plugin)}再创建cordis.yml-name:./hello.ts在仓库内可以用 vendored 的 Cordis 启动器直接跑通最小挂载链路不需要 API Keynode--importtsx../../vendor/cordis/bin.js4.2 插件的三种形态import{Service,typeContext}fromdeepseek-ai/cordis// 1. 函数插件最常见推荐默认用它exportfunctionapply(ctx:Context){}// 2. 对象插件带 apply 方法的对象exportconstobjectPlugin{name:object-plugin,apply(ctx:Context){},}// 3. 类插件Service 子类适合对外提供一个 ctx.key 服务exportclassMyServiceextendsService{constructor(ctx:Context){super(ctx,myService)}}4.3 正式插件的四个导出一个正式的函数插件通常导出四个东西importtype{Context}fromdeepseek-ai/cordisimportzfromdeepseek-ai/schemastery/** 插件显示名仅用于诊断。 */exportconstnamemy-plugin/** 声明依赖的必需服务loader 会等它们存在再执行 apply。 */exportconstinject[tools]/** 部署期配置的 schemastery 校验 schema可省略。 */exportinterfaceConfig{greeting:string}exportconstConfig:zConfigz.object({greeting:z.string(),})/** 插件主体注册一切贡献并只注册为可逆 effect。 */exportfunctionapply(ctx:Context,config:Config){ctx.logger.info(config.greeting)}要点inject只声明必需服务可选服务用ctx.get(name)读取。函数插件必须命名导出不要混用默认导出否则 Loader 会丢掉inject元数据。apply签名有Config导出时是(ctx, config)没有时是(ctx)。配置错误要fail loud加载失败会明确报错不会静默跳过。五、插件开发的硬性规则这一节汇总官方文档与社区实践中反复强调的规则违反任何一条都可能导致插件加载失败或行为异常。5.1 注册可逆性一切注册工具、监听器、prompt 段必须通过ctx.effect()/ctx.on()等机制完成插件卸载时自动回滚。ctx.effect()中注册的东西必须有 teardown否则重载或切换 profile 时会留下重复监听器或资源。工具只注册一次注册借用的是只读 definition不要事后改 schema想换工具就释放所属 effect 再注册。5.2 模型可见性规则模型能看到的任何东西都必须能从会话日志重建模型可见 ⟺ 已记录。要给模型加新的可见输入就扩展SessionEventMap加一种新事件类型、从日志渲染而不是绕过日志。durable 会话事件turn/*、step/*、tool/*等追加进会话日志重启后可重建live 事件agent/*、tools/*只做运行期协调。两者分工不能乱。5.3 配置规则「两个部署环境可能需要不同的值」都必须做成Config 字段不能写死在代码里。cordis.yml的!!js只允许出现在插件config和条目disabled下按环境选插件要用 overlay不要滥用!!js。patch 覆盖是整行替换不是深合并覆盖时必须保留行的id。5.4 工具的 execute() 契约args 自动校验defineTool会在execute前校验模型生成的参数。只返回一个规范 JSON 值output.schema定义返回值抛异常 isError领域内的失败结果如非零退出码也要放进规范值返回。遵守exec.signal取消信号触发时必须中止进行中的工作。UI 卡片与模型看到的内容分离模型看到的由output.render决定UI 卡片由presentCall/presentResult返回渲染意图generic/terminal/diff。后台长任务通过ctx.jobs.start()注册模型侧返回带jobId的规范句柄且开关必须由部署配置控制。5.5 waterfall 事件最易踩的坑tools/pre-execute等 waterfall 事件的监听器收到(...args, next)调用next()才把结果传给下一个监听器不调next()直接 return 就是短路截断整条链。这是写钩子插件时最容易犯的错。六、实战一模型可见的工具插件工具是插件最常见的用途。工具注册在ctx.tools上schema 会自动进入 prompt 组装模型就能「看到」它。import{readFile}fromnode:fs/promisesimporttype{Context}fromdeepseek-ai/cordisimport{defineTool}fromdeepseek-ai/dsh-toolsexportconstnamedemo-toolexportconstinject[tools]exportfunctionapply(ctx:Context){ctx.tools.register(defineTool({name:read_file,description:Read a file from disk.,// 模型看到的能力描述要写清前置条件与副作用parameters:{path:{type:string,required:true,description:Absolute path},limit:{type:number},// 可选参数},output:{schema:{type:string},render:(_args,value)[{type:text,text:value}],},asyncexecute(args,exec){// args 已被 defineTool 按 schema 校验并推导类型returnreadFile(args.path,{encoding:utf8,signal:exec.signal})},}))}工具描述description的写作要求说明何时调用、必要前置条件、失败语义与副作用。七、实战二拦截事件的钩子插件不需要新工具、只想在某个环节插一脚时用事件监听器。主循环是事件驱动的钩子插件就是在这些事件上挂监听器。权限门示例——在tools/pre-execute上拦截每一次工具调用importtype{Context}fromdeepseek-ai/cordisimporttype{PreToolDecision,ToolExecution}fromdeepseek-ai/dsh-toolsdeclarefunctionisAllowed(exec:ToolExecution):Promisebooleanexportconstnamepermission-gateexportfunctionapply(ctx:Context){ctx.on(tools/pre-execute,async(exec,next):PromisePreToolDecision{if(!(awaitisAllowed(exec))){return{kind:deny,reason:Denied by policy.}}returnnext()})}常用扩展点速查你要做的用哪个允许 / 拒绝 / 询问工具调用tools/pre-execute返回{kind:deny}/{kind:ask}工具调用必须被最终否决、不可撤销ctx.tools.guard()包裹工具执行生命周期超时/重试/指标tools/execute显式改写工具结果或呈现内容tools/post-execute只观察最终结果审计/捕获tools/result改写模型请求配置agent/requestwaterfall改写/拒绝进入 step 的消息agent/pre-stepwaterfall八、声明式配置Config8.1 定义 schema用deepseek-ai/schemastery它也是 Cordis 的校验器类型和运行时校验合一importzfromdeepseek-ai/schemasteryexportinterfaceConfig{allowParallelInProgress:boolean}exportconstConfig:zConfigz.object({allowParallelInProgress:z.boolean().required(),})8.2 在 cordis.yml 里装配-id:todoname:deepseek-ai/dsh-tool-todoconfig:allowParallelInProgress:true九、把插件挂载到 dsh三条路径路径适用场景做法外置插件推荐大多数场景自研、开源、单独发布独立 npm 包用dsh plugin add安装进 profilepackage.json声明dsh.bundle可自动进 bundle 层临时 overlay调试、演示dsh --profile name --patch ./overlay.yml 任务仓库内包给 dsh 本身贡献代码放packages/group/pkg预览期核心仓库暂不接受外部 PR9.1 方式一正式安装需要 pnpmdsh plugin把参数原样转发给 profile 目录里的 pnpm动词在最后dsh plugin--profilewebadd/path/to/my-plugin# 本地路径dsh plugin--profilewebaddgithub:you/my-plugin# Git 仓库dsh plugin--profilewebaddmy-plugin# npm 包dsh plugin--profilewebadd./my-plugin-0.1.0.tgz# tarballdsh plugin--profileweb remove my-plugin# 卸载相对路径锚定到命令行所在目录。包声明了dsh.bundle的会自动追加进该 profile 的dsh.profile.bundles层栈没声明的包只会作为普通依赖安装并收到警告。9.2 方式二临时 overlay本地开发不需要 pnpm# my-overlay.yml-insert:-id:my-pluginname:/绝对路径/my-plugin/index.jsdsh--profileheadless--patch./my-overlay.yml任务9.3 验证插件已挂载dsh--profileweb --dump-config# 应看到 # your-plugin 层和对应 id 行dsh plugin--profileweb whypackage# 确认依赖关系十、如何上传 / 发布插件DSH 没有专门的插件注册中心——发布 DSH 插件 ≈ 发布一个 npm 包只是包内容遵循插件约定。官方提供三种分发途径核心区别在于是否分发预构建产物方式用户安装命令安装到的是什么是否需要构建授权npm 发布dsh plugin add your-package预构建的lib/代码不需要tarball 交付dsh plugin add ./hello-0.1.0.tgzpnpm pack打出的包不需要Git 安装dsh plugin add github:you/repo源码不是构建产物需要pnpm ≥ 1010.1 发布到 npm推荐给普通用户分发的首选# 1. 准备 npm 账户并登录npmlogin# 2. 先构建再发布prepublishOnly 里做构建也行pnpmbuildnpmpublish# 或 pnpm publish# 3. 验证在某个 profile 里安装确认能挂载dsh plugin--profiledevaddyour-plugin dsh--profiledev --dump-config发布前检查入口正确导出name/inject/applyinject里依赖的服务提供方要声明进package.json版本从 0.x 起步并遵循语义化版本选择明确的开源协议MIT / Apache-2.0 常见。10.2 交付 tarball# 作者侧打出 tgzpnpmpack# 用户侧直接安装 tarball 文件零授权dsh pluginadd./hello-plugin-0.1.0.tgz10.3 Git 安装最灵活但有一道坎Git 安装拉取的是源码没有任何环节替你运行 build 脚本——TypeScript 包到手没有lib/输出加载会失败。所以作者侧必须提供自包含的prepare脚本pnpm 在 git 安装后运行它完成构建。它不能假设仅开发环境存在的上下文比如旁边有一份 monorepo checkout。用户侧pnpm ≥ 10 首次安装会拒绝运行 git 依赖的构建脚本需要在该 profile 的pnpm-workspace.yaml里添加allowBuilds授权按报错提示复制 key 即可。这等于允许该包的代码在你机器上执行。安全建议git 安装时锁定 commit——dsh plugin add github:you/repo#完整commit-sha避免后续推送改变实际运行的代码。10.4 让别人发现你的插件官方指定的发现渠道非常简单——给插件仓库加上 GitHub 话题dsh-plugin。加了话题的仓库会被社区聚合目录awesome 清单、各类插件索引站自动收录。其他渠道GitHub Discussions 社区板块分享插件与反馈DeepSeek Harness Discord 社区第三方插件聚合站会被dsh-plugin话题自动索引十一、打包规范与发布前检查清单一个**标准 DSH 社区插件包bundle**的结构your-plugin/ package.json # 声明 dsh: { bundle: { patch: ./cordis.patch.yml } } # main/types/exports 指向真实生成的 lib/ # files 只收录运行入口/声明/许可证/README/组合层 # Cordis 与 Service Definition 包放 peer dev deps自有实现放 dependencies cordis.patch.yml # bundle 的 patch 层按行 id 插入插件行插件按包名解析 src/index.ts # 函数插件命名导出 name/inject/Config/apply README.md # 服务 API、事件、扩展点、安装命令、Known Limitations LICENSEpackage.json关键片段{name:dsh-your-plugin,version:0.1.0,type:module,main:./lib/index.js,types:./lib/index.d.ts,exports:{.:./lib/index.js},dsh:{bundle:{patch:./cordis.patch.yml}}}cordis.patch.yml-insert:-id:your-pluginname:dsh-your-plugin# 用包名不要用 checkout 相对路径发布前检查清单可直接复制进 PR 描述架构能力缝三角色Service Definition / Provider / Consumer是否设计完整导出与依赖name/inject/Config/apply命名导出完整inject的服务提供方已声明依赖生命周期所有注册可逆HMR 下释放 fiber 后注册消失Config 与错误部署期可变项全部做成配置字段误配置 fail loud工具与 UIexecute()契约遵守output.render与 UI 卡片分离测试与文档行为测试、真实组合测试、README 含 Model Experience 段构建打包安装pnpm pack产物在干净 profile 里能dsh plugin add成功并出现在--dump-config中十二、测试与质量门在仓库内开发时新增/修改包后逐级往上跑本地只跑受影响的CI 才全量pnpmrun constraints# workspace 约束pnpmrun typecheck# strict 类型检查无 any 逃逸pnpmrun lint# oxlintpnpmrun buildpnpmrun hygiene# knip publint NodeNext 消费检查pnpmruntest# vitest 单元测试测试方针要点行为测试描述行为改行为要同步改测试并说明原因。产品可见的插件要有一个真实组合测试通过 Loader 启动cordis.yml而不是只用手搭的ctx.plugin(...)单测。注册可逆性用 HMR 安全测试验证。十三、常见问题排查FAQQdsh plugin报错找不到 pnpmdsh plugin是把参数转发给 profile 目录里的 pnpm 执行的。先npm install -g pnpm。QGit 安装的 TypeScript 插件加载失败Git 安装拉的是源码没人替你跑 build。插件作者要提供自包含prepare脚本用户侧 pnpm ≥ 10 还需要在 profile 的pnpm-workspace.yaml里加allowBuilds授权。不想折腾就改用 npm 包或 tarball。Q怎么确认插件真的挂载了dsh --profile name --dump-config应该能看到# your-plugin层和你的插件行 id。Q插件异常导致 dsh 无法启动如何临时禁用在 profile 的cordis.patch.yml里加一行即可无需卸载-id:your-plugindisabled:trueQpatch 覆盖了配置但没生效patch 是整行替换而非深合并——覆盖时必须保留行的id且被替换字段要全部重述。Qbundle 插件安装后还要手动 insert 吗不要。声明了dsh.bundle的包会被自动注册进 bundle 层栈再往 profile 的cordis.patch.yml手动 insert 同 id 会报 duplicate loader entry id 导致无法启动。Q核心 API 会变吗会。开发者预览阶段官方明确声明有兼容性破坏变更。建议pin 住你实验用的仓库 commit 或包版本以官方文档和--dump-config实际输出为准。