开发指南:DeepSeek Harness Cordis 插件运行时工程
插件与 Bundle 实战请配合 7 天学会 DeepSeek Harness Day 2、Day 4、Day 7 及 技能实验室 同步练习。
架构认知见 入门介绍;安装见 快速开始;生产安全见 最佳实践;开源对照见 GitHub 项目导读。
本文定位与读者前提
本文面向已经理解「Agent = Model + Harness」、并在本地成功启动过 Web UI 的工程师。目标不是复述 CLI 命令,而是系统讲授 Cordis 插件运行时 的工程知识:如何写可卸载的插件、如何扩展工具与审批、如何在 Host / Preset / Client 三平面上协作、如何用测试与调试手段把不确定性压到可接受范围。
若你尚未建立 Cordis 五核心概念(插件、服务、Effect、Bundle、事件),建议先阅读 从零到一 Day 1–2,再回来精读本文。本文所有内部链接均使用 /docs/dsh/... 路径,便于在本站侧边栏中跳转。
| 阶段 | 核心产出 | 常见失败 |
|---|---|---|
| 本地插件 | 可挂载/卸载、Teardown 完整 | Effect 泄漏、循环依赖 |
| 工具扩展 | Schema 可被模型稳定调用 | 描述歧义、参数漂移 |
| Bundle | Profile 可 复现 | peerDependencies 未声明 |
| 生产 | 可观测、可 Replay | 绝对路径硬编码 |
第一章:Cordis 插件工程基础
1.1 Everything is a Plugin 的工程含义
DeepSeek Harness 把模型接入、工具管道、会话管理、Web UI 槽位、审批策略都视为 可组合插件。这意味着:
- 能力边界清晰:每个插件只应注册自己领域的服务与副作用。
- 生命周期可逆:卸载插件时,系统必须回到「仿佛从未挂载」的状态(在持久化数据允许的范围内)。
- 依赖可解析:插件通过
inject声明依赖,Cordis 按拓扑排序挂载,避免隐式全局单例。
最小插件骨架如下:
import type { Context, Plugin } from "@cordis/core";
export default {
name: "example-hello",
deps: ["@dsh/core-tools"],
apply(ctx: Context) {
ctx.provide("hello", {
greet(name: string) {
return `Hello, ${name}`;
},
});
},
} satisfies Plugin;
1.2 apply(ctx):插件唯一入口
apply(ctx) 是插件与 Cordis 内核的 唯一契约入口。在此函数内,你应当完成:
| 职责 | API | 禁止事项 |
|---|---|---|
| 注册服务 | ctx.provide(key, impl) | 在 provide 内启动不可停止的后台任务 |
| 声明副作用 | ctx.effect(fn) | 直接 setInterval 而不注册 effect |
| 订阅事件 | ctx.on(event, handler) | 在 handler 内修改全局变量而不清理 |
| 扩展工具 | 注入 tools 服务后注册 | 绕过 sandbox 直接读磁盘 |
| 读取配置 | ctx.config / inject 的配置服务 | 在 apply 顶层 await 长 IO |
挂载顺序:Cordis 根据 deps 与 inject 声明构建依赖图;若 A 依赖 B,则 B 的 apply 先于 A 执行。循环依赖会在启动期抛出明确错误——这是特性,不是缺陷。
1.3 ctx.provide 与服务注册
ctx.provide(serviceKey, implementation) 向 当前作用域 注册服务。服务键建议使用命名空间,例如 tools.registry、llm.router,避免与核心插件冲突。
interface MyAnalyzer {
analyze(text: string): Promise<AnalysisResult>;
}
export default {
name: "my-analyzer",
apply(ctx) {
const analyzer: MyAnalyzer = {
async analyze(text) {
return { tokens: text.length, lang: "zh" };
},
};
ctx.provide("myAnalyzer", analyzer);
},
};
消费服务 的两种方式:
- ctx.inject(key):在
apply内同步声明依赖,Cordis 保证被依赖服务已 provide。 - ctx.get(key):运行时获取;若不存在返回 undefined(用于可选依赖)。
| 模式 | 适用 | 风险 |
|---|---|---|
| inject 必需依赖 | 核心路径 | 缺失依赖启动失败(期望行为) |
| get 可选依赖 | 增强功能 | 需处理 undefined |
| provide 工厂 | 延迟初始化 | 工厂内 effect 仍需 Teardown |
1.4 ctx.inject 与依赖注入语义
inject 不仅是「取服务」,更是 配置期可见的依赖边。在 Bundle 诊断中,inject 边会出现在依赖图里,便于排查「为何某插件在 Minimal 预设未挂载」。
失败模式:
| 现象 | 根因 | 修复 |
|---|---|---|
Service not found: llm | 预设未包含 llm 插件 | 在 Profile 中加入 @dsh/plugin-llm |
| 挂载顺序错误 | deps 未声明 | 在 deps 数组中列出插件名 |
| 拿到 stub 实现 | Mock 插件覆盖 | 检查测试 Bundle 的 patch |
1.5 ctx.on() 与事件总线
Cordis 事件用于 横切关注点:会话生命周期、工具执行前后、审批结果、插件热重载等。订阅务必在 effect 内注册并在 Teardown 中 off。
apply(ctx) {
ctx.effect(() => {
const handler = (payload: ToolExecuteEvent) => {
ctx.logger.info("tool executed", payload.toolName);
};
ctx.on("tool:after", handler);
return () => {
ctx.off("tool:after", handler);
};
});
}
常用事件域(概念性,以官方类型定义为准):
| 事件域 | 典型用途 |
|---|---|
session:* | 创建、Resume、Fork、压缩 |
tool:* | 注册、执行、失败、审批 |
plugin:* | 挂载、卸载、错误 |
llm:* | 请求、流式 chunk、用量 |
agent:* | Spawn、Fork、子 Agent 结束 |
反模式:在 on 回调里 await 长任务而不限流,会阻塞事件环;应投递到内部队列或 ctx.effect 管理的 worker。
1.6 ctx.effect() 与可逆副作用
ctx.effect(setup) 是 Cordis 区别于普通 DI 框架的核心。setup 可返回 Teardown 函数;插件卸载或作用域销毁时,Cordis 逆序调用 Teardown。
apply(ctx) {
ctx.effect(() => {
const timer = setInterval(() => refreshCache(), 30_000);
const sub = externalBus.subscribe(onMessage);
return () => {
clearInterval(timer);
sub.unsubscribe();
};
});
}
Effect 栈模型:
第二章:Effect 泄漏与检测
2.1 什么是 Effect 泄漏
Effect 泄 漏指 Teardown 未执行或未完整执行,导致定时器仍在触发、文件句柄未关闭、重复挂载时双注册工具或监听器、内存与句柄单调上涨。
2.2 泄漏模式对照表
| 模式 | 代码气味 | 后果 |
|---|---|---|
| 裸 setInterval | 不在 effect 中 | 卸载后仍运行 |
| 只 off 不清资源 | Teardown 空函数 | 句柄泄漏 |
| async effect 无取消 | await 长任务 | 卸载后仍写会话 |
| 全局 Map 缓存插件态 | 无 weak ref | 插件无法 GC |
| 重复 provide | 热重载未清理 | 后者覆盖前者,行为随机 |
2.3 检测手段
- 插件树 + 挂载计数:开发 Host 开启 debug,反复挂载/卸载同一插件,观察工具数量与监听器计数是否归零。
- 依赖图 diff:对比挂载前后
dsh plugin graph输出。 - Session Replay 回归:泄漏常表现为 Replay 时重复执行副作用。
- Node 堆快照:对长期运行 Host,强制 GC 后对比 retained size。
- 自动化测试:Vitest 中循环
mount → unmount100 次,断言无 unhandled rejection。
2.4 工程清单:Effect 健康
- 每个
setInterval/setTimeout在 Teardown 中 clear - 每个
ctx.on在 Teardown 中ctx.off - 子进程 / Worker 在 Teardown 中 kill 或 terminate
- async 任务监听 AbortSignal,卸载时 abort
- 不在模块顶层注册副作用
第三章:绝对路径注册与 peerDependencies
3.1 为何强调绝对路径
Harness 在生产环境可能从不同工作目录启动;Bundle 内插件引用若使用相对路径,会导致找不到插件入口、Schema 加载失败、沙箱 allowlist 不一致。
推荐做法:在 dsh.plugin.json 中使用基于 Bundle 根的路径解析;Host 启动时将 Bundle 根注入 DSH_BUNDLE_ROOT。
{
"name": "my-org.analytics",
"entry": "./dist/index.js",
"schemaRoot": "./schemas",
"peerDependencies": {
"@cordis/core": "^1.2.0",
"@dsh/runtime": "^0.8.0"
}
}
3.2 peerDependencies 与 Cordis 版本
插件 不应 把 @cordis/core 打包进 bundle。应:
- 在
peerDependencies声明兼容的 Cordis 主版本。 - 在 CI 中用矩阵对多个
@dsh/runtime版本跑测试。 - 在 README 中维护 兼容性矩阵(见 GitHub 项目导读)。
| Cordis 变更类型 | 插件应对 |
|---|---|
| 服务键重命名 | 提供 adapter 插件 |
| effect 语义收紧 | 补 Teardown 测试 |
| 新 inject 必需项 | 更新 deps 声明 |
第四章:核心服务详解(tools / llm / sessions / sandbox / fs)
Harness 核心插件通过服务暴露能力。自定义插件应 inject 而非复制 这些服务。
4.1 tools 服务
职责:工具注册、Schema 校验、执行管道、与审批/沙箱协作。
const tools = ctx.inject("tools");
tools.register({
name: "read_config",
description: "Read a config key from allowed paths",
parameters: {
type: "object",
properties: {
key: { type: "string", description: "Config key name" },
},
required: ["key"],
},
async execute(args, execCtx) {
return { value: await loadConfig(args.key) };
},
});
4.2 llm 服务
职责:模型路由、流式、Token 计量、多 Provider failover。扩展方式:提供 llm.middleware 插件,在 effect 中注册拦截器,而非 monkey-patch SDK。
4.3 sessions 服务
职责:Append-only Session Log、Resume、Fork、Replay、上下文压缩。
4.4 sandbox 服务
职责:路径 allowlist、命令模板、环境变量过滤、网络 egress 策略。自定义工具 必须 通过 sandbox API 访问文件系统。
4.5 fs 服务
职责:在沙箱约束下的读写、临时目录、原子写入。
| 操作 | 走 sandbox/fs | 直接 Node fs |
|---|---|---|
| 读用户项目内文件 | 是 | 否 |
| 写 Session 附件 | 是 | 否 |
| 读 bundle 内 JSON | 通过 Bundle 根解析 | 谨慎 |
第五章:自定义工具 Schema 与描述工程
模型选工具 几乎完全依赖 名称、描述与 JSON Schema。描述工程的目标是提高 首次调用成功率 与 参数合法率。
5.1 Schema 设计原则
- 字段名与代码一致:
repoPath不要在 Schema 写path。 - required 最小化:能默认值的放 default。
- enum 优于 free text:能枚举的操作类型不要用 string。
- description 写清边界:单位、范围、禁止行为。
- 示例优于长文:在 README 给 2–3 个调用样例。
5.2 描述 anti-pattern
| 问题描述 | 模型行为 |
|---|---|
| "处理文件" | 随机选 read/write/delete |
| 无 sandbox 说明 | 尝试读系统敏感路径 |
| 返回结构未文档化 | 幻觉字段 |
5.3 描述工程清单
- 工具名动词开头、全局唯一
- description 含读写属性与安全边界
- 每个参数有 description
- 复杂工具提供 negative example
- 在 Standard 与 Code 预设各测一轮调用
第六章:审批策略扩展
危险工具进入 审批管道。插件可注册 approval policy provider 扩展策略。
apply(ctx) {
const approval = ctx.inject("approval");
approval.registerPolicy({
id: "org-deny-rm-rf",
priority: 100,
match(call) {
return call.toolName === "shell" && /\brm\s+-rf\b/.test(call.args.command);
},
decide() {
return { action: "deny", reason: "rm -rf blocked by org policy" };
},
});
}
| 策略类型 | 典型 priority |
|---|---|
| 硬 deny | 90–100 |
| 需人工 | 50–70 |
| 审计仅记录 | 10–20 |
失败模式:策略过宽导致 审批疲劳;过严导致 任务无法完成。应对:按预设分级,Code 预设放宽只读工具,Creator 预设收紧网络。
第七章:Bundle 打包与 dsh plugin CLI
7.1 Bundle / Profile / Patch 三层
| 层 | 职责 | 版本化 |
|---|---|---|
| Bundle | 插件集合与默认配置 | 是 |
| Profile | 环境(dev/staging/prod) | 是 |
| Patch | 局部覆盖单个插件配置 | 可选 |
7.2 dsh plugin CLI 常用命令
dsh plugin init my-plugin --template typescript
dsh plugin link .
dsh plugin build
dsh plugin validate
dsh plugin graph --bundle ./bundle.yaml
dsh plugin pack --out ./dist/my-plugin-1.0.0.tgz
7.3 打包注意事项
- external cordis:标记
@cordis/core、@dsh/runtime为 external。 - 复制 schemas:构建脚本将
schemas/复制到dist/。 - 双 target:部分插件需 Node + 浏览器 Client 分包。
第八章:三平面开发(Host / Preset / Client slots)
| 平面 | 运行环境 | 插件示例 |
|---|---|---|
| Host | Node 服务端 | 会话存储、MCP 客户端 |
| Preset | Host 内逻辑配置 | 工具包、模型路由 |
| Client | 浏览器 / TUI | 面板、可视化、快捷键 |
Client 插件 通过 clientSlots 注册 UI 组件,不得 直接访问 Node API;通过 RPC 与 Host 通信。
开发工作流:先在 Minimal 验证 Host 插件 → Standard 测工具协同 → Client dev server 联调槽位 → Creator 仅在隔离环境验收。