xjiafei

dsh-dev-full-skill

DeepSeek-Harness (dsh) / Cordis 插件开发专用知识库。当用户要求基于 DeepSeek-Harness、dsh、或 Cordis 框架开发插件、工具(tool)、hook拦截、事件监听、子agent(subagent)编排、或询问 ctx.plugin/inject/Service/waterfall/scope 等概念时,必须优先使用本 Skill 内置的概念字典、五类插件范式模板、工作流程和自检清单完成任务,禁止在未查阅本 Skill 前就去翻官方源码或长文档。只有当命中"知识边界"章节列出的触发条件时才降级去读源码/文档,并按指定话术告知用户。

xjiafei 0 Updated 2d ago

Resources

6
GitHub

Install

npx skillscat add xjiafei/dsh-dev-full-skill

Install via the SkillsCat registry.

SKILL.md

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/cordis 4.0.0-rc.7(vendored 自上游 cordis
    • @deepseek-ai/schemastery 3.18.0(插件 Config 校验用)
    • 事实来源:本仓库 docs/architecture.mddocs/cordis-primer.mddocs/cordis-tutorial/*docs/cordis-api/*docs/subsystems/*docs/postmortem/*docs/defensive-patterns.md、以及 packages/*/src/index.ts 中的真实源码(如 packages/todo/tool-todopackages/feedback/command-feedbackpackages/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 条目):

  1. 函数插件export const name/inject/Config; export function apply(ctx, config) {}
  2. 对象插件{ name, apply(ctx) {} }
  3. 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 里其实是两件不同的事,容易搞混:

  • SlotWeb Client(浏览器前端)专属的 React 组合注册表(ctx.slots.register()/ctx.slots.inject()SlotMap 声明式合并),只用于浏览器 UI 组合,跟后端逻辑无关。
  • 后端的"拦截点"没有一个通用叫 Hook 的 API;真正机制是 Cordis 事件的 waterfall/serial/bail 分发模式(如 tools/pre-executeagent/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 叫这个名字。开发者实际会遇到的"组合"分三层:

  1. profile + bundle + cordis.patch.yml:启动时把多个插件包按顺序层叠成一棵插件树(dsh --profile web --dump-config 可打印这棵树)。
  2. 生成的静态依赖图docs/module-graph.md(包间 peer 依赖)和 docs/capability-seams.md(哪个包拥有/实现/消费某个 ctx.<key> service)。
  3. 运行时的 Fiber 树ctx.registry 可遍历,每个 Fiber 有 state(见下)。

写插件时"组合"更多体现为"我这个插件该放哪个 profile/bundle 层、patch 覆盖顺序对不对",而不是操作某个 Graph 对象的 API。

Lifecycle 插件生命周期

每个装载的插件实例是一个 Fiber,状态机:

PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
                 ↘ FAILED
  • PENDING:声明的 inject 服务还没到位(悄悄等待,不报错不崩溃——这是合法状态,不是 bug)。
  • FAILEDapply() 抛错或 Config 校验失败(硬失败,直接让进程带错误退出,不是跳过)。
  • 卸载(UNLOADING)时,effect 的 disposer 按注册的逆序执行;多个异步 disposer 会并发跑(顺序敏感的清理要放在同一个 disposer 里 await)。

Schema 约束

有两套完全不同的 schema 系统,别搞混:

  1. 插件 Config:用 Schemastery(import Schema from '@deepseek-ai/schemastery',或别名 z)。Cordis 接受任何 Standard Schema 校验器;把裸 TS interfaceConfig 导出不会工作
  2. 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 开发一个插件",严格按以下步骤执行:

  1. 需求拆解:判断这是 2.1~2.5 哪一类(新增能力→2.1;拦截已有行为→2.2;委托/编排多 Agent→2.3;插件间通信→2.4;接入新的子 Agent 执行后端→2.5)。列出需要哪些 ctx.<key> 服务(决定 inject 数组)、需要监听/发布哪些事件(决定 dispatch mode)、Tool 的话需要哪个输入/输出 schema。
  2. 选型:对照第 2 章模板选定范式;如果需求跨类型(例如"一个 Tool,调用后委托子 Agent"),以主范式(通常是 2.1)为骨架,内部嵌套调用其它范式的模式(如模板 2.3-A)。
  3. 填充模板:把业务逻辑写进 execute/apply/监听器回调,严格遵守该范式"契约约束"里的每一条(尤其是 waterfall 的 next() 规则、exec.signal 转发、dispose() 义务)。
  4. 定义元数据、Schema、生命周期
    • 导出 name(诊断用)、inject(硬依赖数组);
    • 如需要配置,导出同名 Config 接口 + Schemastery Schema 常量;
    • 如需要跨插件复用的资源(定时器、连接),用 ctx.effect(() => { ...; return dispose }) 包裹,而不是裸 setInterval
  5. 编写注册代码,接入 Runtime:函数插件直接 export function apply(ctx, config) {};Service 类插件要在某个 applyctx.plugin(YourService) 挂载它。若是独立包,还需要在 cordis.yml(或对应 profile 的 cordis.patch.yml)里新增一行 - name: '...'
  6. 编写测试用例:至少覆盖——① 插件在真实 Loader 下能从 PENDINGACTIVE(而不是只用手搭的 ctx.plugin({name,inject,apply}) 测试,那会掩盖第 6.1 类"默认导出"故障,见 references/troubleshooting.md);② waterfall/serial 场景下"允许"和"拒绝"两条分支;③ 插件卸载后 effect 是否正确回收(HMR-safety:dispose fiber 后断言清理完成)。
  7. 自检清单校验(严格执行第 4 章逐条检查,不通过不交付)。
  8. 降级判断:过一遍第 6 章的触发条件列表。命中任意一条 → 停止编码,输出降级话术(见第 6 章模板),并明确指出需要查阅的具体模块/文件(例如"packages/subagent/subagent/src/index.tsSubagentRuntime.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(如有)导出了同名 Schemastery Schema 常量而非裸 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.xxxget 走全局 isolate-keyed 存储,不受 fiber 拓扑影响)
disabled: !!js ... 写在 cordis.yml 条目上却总是不生效 !!js 只在 config 字段和 disabled 字段本身生效;其它元数据字段(如整条 entry 的开关)不会被解释执行 用显式的 overlay/patch 文件区分条件组合,而不是往非 config/disabled 字段塞表达式

6. Skill 知识边界 & 降级触发规则【关键】

本 Skill 收录的是官方仓库 docs/packages/*/src/index.ts 等一手材料里可稳定复用的开发范式,不等于对整个仓库/内核实现的完整掌握。命中以下任一条件,必须停止凭记忆编码,转为查阅源码/文档:

  1. 用到本 Skill 未收录的具体 Slot ID / Event 名 / ctx.<key> 服务方法签名(例如 Web Client 的某个具体 Slot 键位、某个未在第 2 章事件速查表出现的事件名的完整 payload 形状)。
    → 降级去读:对应子系统的 docs/subsystems/<name>.md 生成的 "Cordis API" 区块(那是从源码 JSDoc 自动生成的权威签名)。
  2. 需要使用 vendor/cordis/src/*.ts 内部实现细节Fiber/reflect.ts/context.ts 的私有字段、shadow 代理机制的具体走位逻辑)而不是通过公开 ctx API。
    → 降级去读:vendor/cordis/src/fiber.tsvendor/cordis/src/reflect.ts,以及 docs/postmortem/0001-*.md(其中已经拆解了一次 shadow 机制引发的真实事故)。
  3. 需要修改 Harness 内核行为,而不是开发一个旁挂的业务插件——例如替换 agent-loop 驱动本身、修改 dsh-tools 的 pipeline 顺序、给 Cordis 框架本身加新的 dispatch mode。
    → 降级去读:docs/architecture.md"Where new behavior goes"表格,先确认是否真的没有现成扩展点;如确实要改内核,需要人工确认改动范围。
  4. 框架/仓库版本已升级,API 与本 Skill 记录的版本锚点(Cordis 4.0.0-rc.7 / Schemastery 3.18.0)不一致——例如某个方法签名在新版里变了。
    → 降级去读:先执行 pnpm run gen-cordis-catalog 或直接读对应 docs/cordis-api/*.md 顶部的"Generated by..."时间戳/来源,确认当前仓库状态,而不是信任本 Skill 里写死的签名。
  5. 复杂的多 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.mdctx.tools Cordis API 生成区块】。原因:【命中的具体触发条件编号与说明】。


7. Skill 使用示例

示例 1:开发一个简单日志埋点 Hook 插件(完整流程演示)

用户需求:"给每次工具调用失败都记一条日志,不要影响正常执行。"

  1. 需求拆解:这是"只读观测,不能改变结果"——对照第 2 章,属于 2.2 Hook 拦截插件里的"只读观测"子类型,应该用 tools/resultemit),不是 tools/post-execute(那个是可以改结果的)。
  2. 选型:模板 2.2,但换成 emit 而不是 waterfall 形式。
  3. 填充模板
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}`)
    }
  })
}
  1. 元数据/Schema/生命周期:无需 Config;ctx.on 本身是 effect 化的,无需额外 ctx.effect() 包裹。
  2. 注册:单文件插件,加入目标 profile 的 cordis.yml- name: './tool-failure-logger.ts'
  3. 测试:注册一个会失败的假 Tool,通过真实 Loader 装载两个插件,断言 console.error 被调用且工具调用本身仍然正常完成(isError 结果照常返回给模型)。
  4. 自检:走一遍第 4 章清单——无需 Schema、无需额外 effect、监听器天然跟随插件卸载移除、没有跨 Agent 越权、tools/result 本身是 emit 不涉及 next()。全部通过。
  5. 降级判断:未命中任何第 6 章条件,直接交付上面代码。

示例 2:遇到超出 Skill 能力场景,触发降级

用户需求:"我想让 Cordis 支持一种新的事件分发模式,叫 broadcast-with-timeout,改一下框架底层。"

AI 响应

当前需求超出 Skill 内置知识范围,需要查阅 DeepSeek-Harness 官方源码/文档,重点查阅:vendor/cordis/src/events.tsDispatchMode 类型定义与 EventsService 的分发实现)。
原因:命中第 6 章触发条件 3——这是修改 Cordis 框架内核的 dispatch mode 集合,而不是开发一个旁挂的业务插件,本 Skill 的范式模板均基于"内核已有的五种 dispatch mode 不变"这一前提,无法直接指导内核级改动。建议先确认这个新分发模式是否能用现有 waterfall/serial 组合出等价效果(例如在监听器内部自行加超时包装),如确实需要修改框架本身,需要人工评估对整个 vendored Cordis 的影响范围。

Categories