DeepSeek-Harness (dsh) / Cordis 插件开发专用知识库。当用户要求基于 DeepSeek-Harness、dsh、或 Cordis 框架开发插件、工具(tool)、hook拦截、事件监听、子agent(subagent)编排、或询问 ctx.plugin/inject/Service/waterfall/scope 等概念时,必须优先使用本 Skill 内置的概念字典、五类插件范式模板、工作流程和自检清单完成任务,禁止在未查阅本 Skill 前就去翻官方源码或长文档。只有当命中"知识边界"章节列出的触发条件时才降级去读源码/文档,并按指定话术告知用户。
Resources
6Install
npx skillscat add xjiafei/dsh-dev-full-skill Install via the SkillsCat registry.
DeepSeek-Harness 插件开发 Skill
元信息
- Skill 名称:
dsh-dev-full-skill - 能力范围
- 可直接处理:基于 dsh 的业务插件开发——新增/修改 Tool、Hook 拦截(waterfall/serial/bail 监听)、事件发布订阅、Agent 委托编排(subagent/workflow)、自定义 SubagentProvider("Agent Unit"传输后端)、Config Schema 声明、生命周期与 effect 管理、常见故障诊断。
- 超出范围(必须降级):修改 Cordis 内核本身(
vendor/cordis/)、替换agent-loop驱动实现、深入reflect.ts/fiber.ts内部机制、Web Client 具体 Slot 键位的完整清单、跨 profile 的cordis.patch.yml覆盖顺序高级调优、多 Agent Team 协调(experimental 特性)。
- 使用前提
- 适用场景:目标仓库是
deepseek-ai/deepseek-harness(或其 vendored 分支),插件基于@deepseek-ai/cordis框架,以 TypeScript 编写,通过cordis.yml/ctx.plugin()装载。 - 不适用场景:目标其实是 Koishi(同源但独立生态的 Cordis 使用者,API 表层相似但业务包完全不同)、或用户所指的"DeepSeek-Harness"实际是别的同名项目、或框架主版本已发生不兼容升级(见下方版本锚点)。
- 适用场景:目标仓库是
- 版本锚点(本 Skill 提炼时的真实依据,超出此版本范围的行为差异需要降级核实)
@deepseek-ai/cordis4.0.0-rc.7(vendored 自上游cordis)@deepseek-ai/schemastery3.18.0(插件 Config 校验用)- 事实来源:本仓库
docs/architecture.md、docs/cordis-primer.md、docs/cordis-tutorial/*、docs/cordis-api/*、docs/subsystems/*、docs/postmortem/*、docs/defensive-patterns.md、以及packages/*/src/index.ts中的真实源码(如packages/todo/tool-todo、packages/feedback/command-feedback、packages/attachment/attachment-local)。
- 降级规则(触发条件见第 6 章,此处仅提醒:不能模糊触发,必须命中具体清单项)
1. 核心概念精简字典
说明:用户常用的"时空组合""Slot/Hook""Agent Unit""Composition Graph"等词,在 dsh 官方文档里并非都有字面对应的 API。下面每条先给真实落地机制,再指出术语和实现之间的对应/偏差,避免 AI 编造不存在的 API。
一切皆插件(Everything is a Plugin)
dsh 的每个能力——模型适配器、工具注册表、Session 日志、甚至 Agent Loop 本身——都是挂在 Cordis Context 下的插件。没有特权核心可以打补丁,扩展方式永远是"在旁边挂一个新插件",通过声明式的服务依赖(inject)接入。
开发注意点:新增能力 = 新增一个 apply(ctx) 函数或 Service 子类,通过 inject 声明依赖,绝不能直接改被扩展对象的源码包。
时空组合(Space-Time Composability)
Cordis 底层来自论文《A Programming Paradigm for Spatiotemporal Composability》。"空间"轴 = Context 树的横向组合(ctx.extend()/ctx.isolate()/ctx.intercept(),决定"谁能看见哪个 service 实例");"时间"轴 = Fiber 的生命周期(mount → reload → unmount,effect 何时建立、何时撤销)。二者被 ctx.effect() 绑成一个原子操作:一次注册既属于某个 context 位置(空间),又属于该 Fiber 的存活区间(时间)——Fiber 卸载时,那个位置注册的一切自动撤销。
开发注意点:写代码前先问两个问题——"这段注册对谁可见"(空间)、"什么时候该消失"(时间)。
Harness Runtime
特指 Cordis 的 Context + Fiber 运行时(vendored 在 vendor/cordis/,发布为 @deepseek-ai/cordis),加上其上跑的 dsh-* 业务 service 层。开发者始终面对一个 ctx 对象:通过它 provide/consume 服务、监听/派发事件、注册 effect。
Plugin 契约
三种合法形状(可混用,都能传给 ctx.plugin() 或写进 cordis.yml 条目):
- 函数插件:
export const name/inject/Config; export function apply(ctx, config) {} - 对象插件:
{ name, apply(ctx) {} } - Service 子类插件:
class X extends Service { constructor(ctx) { super(ctx, 'name') } }
⚠️ 高危坑:绝不要在函数式插件文件末尾再加一行 export default apply。Cordis Loader 的 unwrapExports 会优先取 .default,导致同文件里的 name/inject/Config 全部作废,插件在一个"零依赖"的空 fiber 里跑,第一次访问任何注入服务就抛 cannot get property "x" without inject(真实事故,见 references/troubleshooting.md #1)。
Slot / Hook 点位
这两个词在 dsh 里其实是两件不同的事,容易搞混:
- Slot 是 Web Client(浏览器前端)专属的 React 组合注册表(
ctx.slots.register()/ctx.slots.inject(),SlotMap声明式合并),只用于浏览器 UI 组合,跟后端逻辑无关。 - 后端的"拦截点"没有一个通用叫
Hook的 API;真正机制是 Cordis 事件的 waterfall/serial/bail 分发模式(如tools/pre-execute、agent/pre-step)。文档里俗称这类监听器为"hook"或"native hook"。另有一族专门叫dsh-hooks-claude-code/dsh-hooks-codex的包,那是把外部 hook 配置文件桥接到这些拦截点的具体实现,不是通用扩展机制本身。
开发注意点:写"Hook 插件"时,你要挂的是一个具体的 waterfall/serial/bail 事件监听器,不是去找一个叫 ctx.hooks 的注册表。
Event Bus 事件模型
ctx.on(name, listener)/ctx.once() 注册监听器(effect 化,插件卸载自动移除)。派发方式五选一,且是事件契约的一部分(新增事件要用 @mode 标注):
| 模式 | 调用 | 语义 |
|---|---|---|
emit |
ctx.emit(name, ...args) |
同步广播,不等待、不收集返回值 |
parallel |
await ctx.parallel(name, ...args) |
所有监听器并发跑,一起等待 |
serial |
await ctx.serial(name, ...args) |
顺序执行并等待;第一个非 null/false/undefined 的返回值中止后续 |
bail |
ctx.bail(name, ...args) |
同 serial,但同步 |
waterfall |
ctx.waterfall(name, ...args, next) |
围绕式中间件;不调用 next() = 故意短路 |
Context 时空上下文
ctx 是一个 Proxy,普通属性读取会走服务解析器;extend()/isolate()/intercept() 各生成一个新的子 context 而不改变父级。isolate(name, label?) 是"空间隔离"的核心——给这个子树一个独立的 service 实现槀位;两次 isolate() 传同一个 label 会合并到同一槀位(容易踩坑,见第 4 章)。
Agent Unit
dsh 文档里没有字面意义上叫 "Agent Unit" 的类。最贴近的真实概念是 Agent 接口(packages/core/agent)——一个存活 agent 的握手:id/session/inbox/status/ctx/cancel()/send()/followup()/steer()/inject()。
"自定义一个新的 Agent Unit" 在 dsh 里的真实落地是给 ctx.subagents 注册一个新的 SubagentProvider(一种新的"子 agent 传输方式",如 spawn-in-process / fork-in-process / ACP / Codex / Claude Code),而不是直接实现 Agent 接口本身——agent-loop 是官方唯一的具体驱动实现,扩展插件应该依赖 agent 而不是 agent-loop,更不应该替换它(那属于内核级改动,见第 6 章降级规则)。
Composition Graph 组合图
同样没有一个字面 API 叫这个名字。开发者实际会遇到的"组合"分三层:
- profile + bundle +
cordis.patch.yml:启动时把多个插件包按顺序层叠成一棵插件树(dsh --profile web --dump-config可打印这棵树)。 - 生成的静态依赖图:
docs/module-graph.md(包间 peer 依赖)和docs/capability-seams.md(哪个包拥有/实现/消费某个ctx.<key>service)。 - 运行时的 Fiber 树:
ctx.registry可遍历,每个 Fiber 有state(见下)。
写插件时"组合"更多体现为"我这个插件该放哪个 profile/bundle 层、patch 覆盖顺序对不对",而不是操作某个 Graph 对象的 API。
Lifecycle 插件生命周期
每个装载的插件实例是一个 Fiber,状态机:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILEDPENDING:声明的inject服务还没到位(悄悄等待,不报错不崩溃——这是合法状态,不是 bug)。FAILED:apply()抛错或 Config 校验失败(硬失败,直接让进程带错误退出,不是跳过)。- 卸载(
UNLOADING)时,effect的 disposer 按注册的逆序执行;多个异步 disposer 会并发跑(顺序敏感的清理要放在同一个 disposer 里await)。
Schema 约束
有两套完全不同的 schema 系统,别搞混:
- 插件 Config:用 Schemastery(
import Schema from '@deepseek-ai/schemastery',或别名z)。Cordis 接受任何 Standard Schema 校验器;把裸 TSinterface当Config导出不会工作。 - Tool 的入参/出参:走
ParameterSchemaSpec/ValueSchemaSpec(@deepseek-ai/dsh-tools自己的 JSON-Schema 子集 DSL,用defineTool推断类型),跟 Schemastery 无关。
校验失败的后果不同:Config 非法 → 插件直接 FAILED,不会半启动;Tool 参数非法 → 单次调用变成 isError 结果,不影响插件本身存活。
2. 插件开发核心范式(最重要章节)
五类范式的完整模板(使用场景 / 契约约束 / 最小可运行模板 / 常见坑)都放进了独立的
references/paradigm-2.*.md文件,按需读取对应那一篇即可,不要求一次性通读全部五篇——这样简单任务(比如只改一个 Tool 插件)不用为它不需要的编排/事件/SubagentProvider 模板付上下文成本。下表是路由表,先判断属于哪一类再去读对应文件:
| 范式 | 使用场景一句话 | 参考文件 |
|---|---|---|
| 2.1 Tool 插件 | 给模型新增一个可调用能力 | references/paradigm-2.1-tool.md |
| 2.2 Hook 拦截插件 | 在工具调用/Agent 请求/Turn 结束等关键点插入允许/拒绝/改写逻辑 | references/paradigm-2.2-hook.md |
| 2.3 时空组合编排插件 | 委托单个子 Agent,或用脚本编排多个子 Agent(ctx.subagents / ctx.workflowEngine) |
references/paradigm-2.3-orchestration.md |
| 2.4 事件消费/发布插件 | 插件间靠自定义事件解耦通信 | references/paradigm-2.4-events.md |
| 2.5 自定义 SubagentProvider | 接入新的子 Agent 执行后端("Agent Unit"传输层) | references/paradigm-2.5-subagent-provider.md |
通用契约要点(五类范式都适用,读完对应模板后仍要回来确认):
- waterfall 监听器不拦截时必须
return next();Config 必须用 Schemastery 而不是裸 interface;SubagentRun/WorkflowRun在所有路径(包括异常路径)都要dispose();result永不 reject,用stopReason判断成败而不是 try/catch。 - 需求跨类型时(例如"一个 Tool,调用后委托子 Agent"),以主范式(通常是 2.1)为骨架,内部嵌套调用其它范式的模式——读两篇参考文件,不用都读。
3. 完整开发工作流(AI 执行步骤)
当用户需求是"基于 dsh 开发一个插件",严格按以下步骤执行:
- 需求拆解:判断这是 2.1~2.5 哪一类(新增能力→2.1;拦截已有行为→2.2;委托/编排多 Agent→2.3;插件间通信→2.4;接入新的子 Agent 执行后端→2.5)。列出需要哪些
ctx.<key>服务(决定inject数组)、需要监听/发布哪些事件(决定 dispatch mode)、Tool 的话需要哪个输入/输出 schema。 - 选型:对照第 2 章模板选定范式;如果需求跨类型(例如"一个 Tool,调用后委托子 Agent"),以主范式(通常是 2.1)为骨架,内部嵌套调用其它范式的模式(如模板 2.3-A)。
- 填充模板:把业务逻辑写进
execute/apply/监听器回调,严格遵守该范式"契约约束"里的每一条(尤其是 waterfall 的next()规则、exec.signal转发、dispose()义务)。 - 定义元数据、Schema、生命周期:
- 导出
name(诊断用)、inject(硬依赖数组); - 如需要配置,导出同名
Config接口 + SchemasterySchema常量; - 如需要跨插件复用的资源(定时器、连接),用
ctx.effect(() => { ...; return dispose })包裹,而不是裸setInterval。
- 导出
- 编写注册代码,接入 Runtime:函数插件直接
export function apply(ctx, config) {};Service 类插件要在某个apply里ctx.plugin(YourService)挂载它。若是独立包,还需要在cordis.yml(或对应 profile 的cordis.patch.yml)里新增一行- name: '...'。 - 编写测试用例:至少覆盖——① 插件在真实 Loader 下能从
PENDING→ACTIVE(而不是只用手搭的ctx.plugin({name,inject,apply})测试,那会掩盖第 6.1 类"默认导出"故障,见references/troubleshooting.md);② waterfall/serial 场景下"允许"和"拒绝"两条分支;③ 插件卸载后 effect 是否正确回收(HMR-safety:dispose fiber 后断言清理完成)。 - 自检清单校验(严格执行第 4 章逐条检查,不通过不交付)。
- 降级判断:过一遍第 6 章的触发条件列表。命中任意一条 → 停止编码,输出降级话术(见第 6 章模板),并明确指出需要查阅的具体模块/文件(例如"
packages/subagent/subagent/src/index.ts的SubagentRuntime.startContinuable实现细节"),不要泛泛地说"建议查看官方文档"。全部未命中 → 直接输出最终插件代码。
4. 插件自检清单
写完插件后逐条检查,任意一条不通过就必须修正,不允许带着已知问题交付:
- Plugin 契约:函数插件没有多余的
export default;Service 子类插件的constructor调用了super(ctx, 'serviceName');inject数组只列出了硬依赖(可选依赖用ctx.get(name)探测,不放进inject)。 - Context 时空读写:需要跨 Agent 隔离状态的地方用了
ctx.isolate(name)或 agent-scoped 注册(通过agent.ctx而不是全局ctx),而不是在插件闭包里用一个跨所有 Agent 共享的可变 Map(除非那确实是有意的全局状态)。检查是否误把两个本应独立的isolate()调用传了相同的label导致意外共享槀位。 - Schema 是否完整定义:Tool 的
parameters/output.schema覆盖了所有分支;Config(如有)导出了同名 SchemasterySchema常量而非裸 interface。 - 生命周期回调:所有非 Cordis 自动管理的资源(定时器/文件监听/外部连接)都包在
ctx.effect()里并返回了 disposer;顺序敏感的多步清理放在同一个 disposer 内await,而不是拆成多个并发跑的 disposer。 - 事件发布订阅无泄漏:
ctx.on()监听器都是通过某个存活插件的 fiber 注册的(天然 effect 化,会跟随插件卸载自动移除);没有游离于任何 Cordis 生命周期之外的手动EventEmitter。长生命周期的后台工作走ctx.jobs而不是裸事件。 - 组合边界清晰:waterfall 监听器该调用
next()的地方都调用了;没有越权直接修改另一个 Agent 的session/inbox(跨 Agent 交互只能走ctx.subagents.sendMessage()/agent.followup()/agent.steer()这类公开 API)。 - 错误处理完备:
execute()/监听器里的异常会被框架 contain(不会拖垮别的监听器),但你自己写的转发/聚合逻辑也做了同样的 try/catch;exec.signal/request.signal被正确转发和响应;一次性委托(SubagentRun/WorkflowRun)在所有路径(包括异常路径)上都调用了dispose()。 - 测试覆盖真实入口:至少一个测试通过真实
cordis.yml+ Loader 装载该插件(不是纯手搭ctx.plugin({...})),能捕捉"默认导出吞掉 inject"一类的真实事故。
5. 常见问题&故障速查
详见 references/troubleshooting.md(含 4 个真实官方 postmortem 的根因与修复方案)。高频摘要:
| 症状 | 根因 | 修复 |
|---|---|---|
| 插件什么都不打印,也不报错 | inject 里的服务没有任何 provider,Fiber 停在 PENDING |
检查依赖的 service 是否真的被装载;用 ctx.registry 遍历打印 PENDING 的 fiber 名 |
访问注入的服务时报 cannot get property "x" without inject |
插件文件末尾多写了 export default apply,Loader 的 unwrapExports 丢弃了 inject/name |
删掉 export default,回到纯 named exports 形式 |
| waterfall 链路"默认行为消失了" | 某个监听器只做了日志/标注但忘记 return next() |
补上 return next();只有故意短路时才能不调用 |
| 子 Agent/工作流跑完了但进程/资源没释放 | SubagentRun/WorkflowRun 没有在所有路径(尤其异常路径)调用 dispose() |
用 try/finally 包裹,finally 里无条件 dispose() |
一个可选服务在插件里用 ctx.xxx 直接访问时报 without inject,但同样的服务在测试里直接用没问题 |
该服务不在 static inject/inject 里,属于"机会性读取",通过某层代理(shadow)访问时祖先链walk不到 |
用 ctx.get('xxx') 而不是 ctx.xxx(get 走全局 isolate-keyed 存储,不受 fiber 拓扑影响) |
disabled: !!js ... 写在 cordis.yml 条目上却总是不生效 |
!!js 只在 config 字段和 disabled 字段本身生效;其它元数据字段(如整条 entry 的开关)不会被解释执行 |
用显式的 overlay/patch 文件区分条件组合,而不是往非 config/disabled 字段塞表达式 |
6. Skill 知识边界 & 降级触发规则【关键】
本 Skill 收录的是官方仓库 docs/、packages/*/src/index.ts 等一手材料里可稳定复用的开发范式,不等于对整个仓库/内核实现的完整掌握。命中以下任一条件,必须停止凭记忆编码,转为查阅源码/文档:
- 用到本 Skill 未收录的具体 Slot ID / Event 名 /
ctx.<key>服务方法签名(例如 Web Client 的某个具体 Slot 键位、某个未在第 2 章事件速查表出现的事件名的完整 payload 形状)。
→ 降级去读:对应子系统的docs/subsystems/<name>.md生成的 "Cordis API" 区块(那是从源码 JSDoc 自动生成的权威签名)。 - 需要使用
vendor/cordis/src/*.ts内部实现细节(Fiber/reflect.ts/context.ts的私有字段、shadow 代理机制的具体走位逻辑)而不是通过公开ctxAPI。
→ 降级去读:vendor/cordis/src/fiber.ts、vendor/cordis/src/reflect.ts,以及docs/postmortem/0001-*.md(其中已经拆解了一次 shadow 机制引发的真实事故)。 - 需要修改 Harness 内核行为,而不是开发一个旁挂的业务插件——例如替换
agent-loop驱动本身、修改dsh-tools的 pipeline 顺序、给Cordis框架本身加新的 dispatch mode。
→ 降级去读:docs/architecture.md"Where new behavior goes"表格,先确认是否真的没有现成扩展点;如确实要改内核,需要人工确认改动范围。 - 框架/仓库版本已升级,API 与本 Skill 记录的版本锚点(Cordis 4.0.0-rc.7 / Schemastery 3.18.0)不一致——例如某个方法签名在新版里变了。
→ 降级去读:先执行pnpm run gen-cordis-catalog或直接读对应docs/cordis-api/*.md顶部的"Generated by..."时间戳/来源,确认当前仓库状态,而不是信任本 Skill 里写死的签名。 - 复杂的多 Profile/Bundle 叠加、
cordis.patch.yml覆盖顺序高级调优,或涉及 Agent Team(experimental 特性)的多 Agent 协调,官方文档也标注为实验性/未被本 Skill 收录的场景。
→ 降级去读:docs/architecture.md"Profiles and bundles"一节 + 目标 bundle 包的cordis.patch.yml源文件;Agent Team 相关看docs/subsystems/agent-team.md(标注为 experimental,行为可能变化)。
触发降级时的固定话术模板:
当前需求超出 Skill 内置知识范围,需要查阅 DeepSeek-Harness 官方源码/文档,重点查阅:【具体文件路径或文档章节,例如
docs/subsystems/tools.md的ctx.toolsCordis API 生成区块】。原因:【命中的具体触发条件编号与说明】。
7. Skill 使用示例
示例 1:开发一个简单日志埋点 Hook 插件(完整流程演示)
用户需求:"给每次工具调用失败都记一条日志,不要影响正常执行。"
- 需求拆解:这是"只读观测,不能改变结果"——对照第 2 章,属于 2.2 Hook 拦截插件里的"只读观测"子类型,应该用
tools/result(emit),不是tools/post-execute(那个是可以改结果的)。 - 选型:模板 2.2,但换成
emit而不是 waterfall 形式。 - 填充模板:
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-failure-logger'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
if (result.isError) {
console.error(`[tool-failure] ${exec.name} failed: ${result.error.message}`)
}
})
}- 元数据/Schema/生命周期:无需 Config;
ctx.on本身是 effect 化的,无需额外ctx.effect()包裹。 - 注册:单文件插件,加入目标 profile 的
cordis.yml:- name: './tool-failure-logger.ts'。 - 测试:注册一个会失败的假 Tool,通过真实 Loader 装载两个插件,断言
console.error被调用且工具调用本身仍然正常完成(isError 结果照常返回给模型)。 - 自检:走一遍第 4 章清单——无需 Schema、无需额外 effect、监听器天然跟随插件卸载移除、没有跨 Agent 越权、
tools/result本身是 emit 不涉及 next()。全部通过。 - 降级判断:未命中任何第 6 章条件,直接交付上面代码。
示例 2:遇到超出 Skill 能力场景,触发降级
用户需求:"我想让 Cordis 支持一种新的事件分发模式,叫 broadcast-with-timeout,改一下框架底层。"
AI 响应:
当前需求超出 Skill 内置知识范围,需要查阅 DeepSeek-Harness 官方源码/文档,重点查阅:
vendor/cordis/src/events.ts(DispatchMode类型定义与EventsService的分发实现)。
原因:命中第 6 章触发条件 3——这是修改 Cordis 框架内核的 dispatch mode 集合,而不是开发一个旁挂的业务插件,本 Skill 的范式模板均基于"内核已有的五种 dispatch mode 不变"这一前提,无法直接指导内核级改动。建议先确认这个新分发模式是否能用现有 waterfall/serial 组合出等价效果(例如在监听器内部自行加超时包装),如确实需要修改框架本身,需要人工评估对整个 vendored Cordis 的影响范围。