开发指南: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 访问文件系统。