DeepSeek Harness 的命令名是 dsh。模型负责推理和生成,Harness 负责把模型接入文件、命令、工具、会话、权限、界面与自动化流程。它基于 Cordis 组织运行时,模型适配器、工具注册表、Agent Loop、会话日志和 Web UI 都以插件形式进入同一棵配置树。
这份增强版保留原教程的学习顺序,同时补上可运行示例、预期输出、验证方法和排错路径。代码以当前官方仓库和中文文档为基线。实际开发时,当前版本的 README、TypeScript 类型与 --dump-config 输出拥有更高优先级。
0. 先选一条学习路线
可以按目标选择章节,省去从头通读的时间。
| 目标 | 建议顺序 | 完成标志 |
|---|---|---|
| 先把产品跑起来 | 1 → 2 → 3 → 15 | Web UI 能发送一次带工具调用的请求 |
| 开发第一个插件 | 1 → 4 → 5 → 6 → 实验一 | 本地插件能被 patch 加载,修改后可热替换 |
| 开发模型 Tool | 4 → 7 → 8 → 13 → 实验二 | 参数会校验,输出可渲染,错误路径有测试 |
| 开发可替换能力 | 9 → 10 → 11 → 实验三 | Consumer 可在两个 Provider 之间切换 |
| 打包和分发 | 6 → 12 → 14 → 实验四 | Bundle 可安装进独立 profile,也可完整移除 |
如果只想快速理解架构,先记住一条式子:
Agent = Model + Harness
Harness = 插件树 + 服务依赖 + 事件 + 会话日志 + 权限策略
1. 建立运行时地图
一次任务可以沿着这条主线理解:
用户输入
-> Agent inbox 领取消息
-> 组装系统提示词、历史与工具 Schema
-> LLM 请求
-> 文本回复或工具调用
-> 工具策略、参数校验与执行
-> tool/result 写入会话日志
-> 下一次模型请求或 turn/end
一个 turn 表示一次完整轮次,一个 step 表示一次模型请求及其触发的工具执行。一个轮次可以没有步骤,也可以包含多个步骤。模型决定连续调用工具时,Harness 会把每次工具结果写回会话,再组装下一次请求。
九个常见概念
| 概念 | 在运行时负责什么 | 开发时常见位置 |
|---|---|---|
| Context | 当前插件可见的服务和运行环境 | apply(ctx)、ctx.tools、ctx.on() |
| Plugin | 向运行时贡献能力的模块 | 导出 apply 的 TypeScript 或 JavaScript 文件 |
| Service | 挂在 ctx 上供其他插件调用的能力 |
ctx.tools、ctx.llm、ctx.sessions |
| inject | 插件声明的必需服务 | export const inject = ['tools'] |
| Event | 插件之间的通知、策略与拦截点 | ctx.emit()、ctx.on()、ctx.waterfall() |
| Effect | 跟随插件实例安装和撤销的资源 | 工具、监听器、定时器、文件 watcher |
| Fiber | 一个已挂载插件实例的生命周期句柄 | PENDING、ACTIVE、FAILED、DISPOSED |
| Bundle | 附带配置层的可分发 npm 包 | package.json 中的 dsh.bundle |
| Profile | 一套可启动的插件组合 | web、headless 或自定义 profile |
Tool 是面向模型的 Consumer。斜杠命令面向人,由 ctx.commands 处理。两者都可以调用 Service,进入会话的路径和呈现方式不同。
开发新能力时先定位归属
| 需求 | 建议扩展点 |
|---|---|
| 新增模型提供方 | 向 ctx.llm 注册适配器 |
| 新增模型可调用能力 | 向 ctx.tools 注册 Tool |
| 新增用户命令 | 向 ctx.commands 注册命令 |
| 拦截工具执行 | 监听 tools/pre-execute、tools/execute 或 tools/post-execute |
| 记录持久事实 | 扩展 SessionEventMap 并写入会话事件 |
| 切换本地与远程实现 | 设计 Service Definition、Provider 与 Consumer |
| 新增浏览器交互 | 注册 Host 服务和 Web Client 组件 |
2. 安装、启动与第一次验证
npm 路径
安装 Node.js 后,在准备作为工作区的目录中运行:
npx @deepseek-ai/dsh web
默认访问地址是 http://127.0.0.1:3080。打开 Web UI 后完成两项设置:进入「设置 > 模型」保存可用模型的凭据,通过「选择工作区」添加项目目录。工作区或模型缺失时,输入框会保持不可用。
第一条请求建议范围清楚,并要求返回可核对的路径:
请概括这个仓库的目录结构,列出三个主要入口,并给出对应文件路径。
验证时看三处:浏览器里出现回复;工具卡片显示读取或搜索动作;终端没有 MISSING_CREDENTIAL、UNKNOWN_MODEL 或 Loader 解析错误。
从源码运行
准备二次开发环境时使用源码路径:
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
当前仓库要求 Node ^22.19 或 >=24。命令变动时先看根目录 README 和 package.json,再运行帮助命令确认入口。
Headless 路径
Headless 适合 CI、脚本和一次性任务,不启动 Web Server:
pnpm dsh --profile headless "Summarize the repository"
需要真实模型时提供凭据。只想验证配置树和插件加载时,可以先运行 --dump-config,这一阶段不需要发起模型请求。
凭据放在哪里
凭据通过设置与凭据引用提供。源码、patch、演示命令和会话文本中都不应出现真实密钥。Web 客户端只接收脱敏描述符。处理敏感仓库时,还要确认会话日志、导出文件和遥测的存放位置。
3. Profile、Bundle 与 patch 如何组合
运行中的 dsh 是一棵插件树。Profile 描述一套可启动组合,Bundle 贡献一层配置,patch 根据稳定 id 插入、替换、禁用或重组配置项。
最终配置按下面的优先级叠加:
- Profile 中列出的 Bundle patch,顺序与
bundles一致。 - Profile 自己的
cordis.patch.yml。 $DSH_HOME/cordis.patch.yml。- 命令行中的每个
--patch <path>,按参数顺序应用。
查看实际生效的配置:
dsh --profile web --dump-config
输出会标注每层来源。排查插件问题时先确认目标 id 是否存在、来自哪一层、是否被禁用,再检查插件代码。
演示:禁用一项能力
假设 --dump-config 中存在 id: tool-web,可在覆盖层中写:
- patch:
id: tool-web
disabled: true
运行:
dsh --profile web --patch /absolute/path/disable-web.yml --dump-config
预期结果是 tool-web 保留在最终树中,同时标记为禁用。实际 id 以当前输出为准。
覆盖 config 会替换整块对象
假设基础配置是:
config:
timeoutMs: 30000
mode: accurate
上层 patch 只写:
config:
mode: fast
最终 config 只剩 mode。需要保留 timeoutMs 时必须重述:
config:
timeoutMs: 30000
mode: fast
这条规则适合写进发布文档,因为用户常把 patch 当成深度合并。
4. 第一个本地插件
函数插件已经覆盖多数扩展场景。插件需要公开 Service 时,再考虑类形式。
创建目录
在仓库根目录运行:
mkdir -p scratch-plugin/src
创建 scratch-plugin/src/my-plugin.ts:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded')
}
name 是诊断元数据。apply(ctx) 在插件加载时运行,必需服务会在这之前准备好。
用 patch 加载本地插件
创建 scratch-plugin/cordis.yml,把路径替换成当前仓库的绝对路径:
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
启动:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
预期终端输出:
[hello-plugin] plugin loaded
本地绝对路径很重要。Loader 会从 profile 环境解析模块,相对 patch 文件的位置并不等于模块解析基准。
三种插件形态
import { Service, type Context } from '@deepseek-ai/cordis'
// 函数插件
export function apply(ctx: Context) {}
// 对象插件
export const objectPlugin = {
name: 'object-plugin',
apply(ctx: Context) {},
}
// Service 子类插件
export class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myService')
}
}
函数形态适合注册工具、监听器和提示词。Service 子类适合公开稳定的 ctx.<key> 能力。一个简单插件无需过早拆成多个包。
故意制造两个错误
把 apply 改成抛错,插件会明确进入加载失败:
export function apply() {
throw new Error('apply exploded')
}
把 patch 中的路径写错,Loader 会通过 logger 报告解析失败。某些启动阶段日志可能早于 console 导出器开始观察,因此看起来像插件没有运行。先检查路径和包名,再检查业务逻辑。
5. 生命周期、effect 与热替换
一个插件实例对应一个 Fiber。常见状态如下:
PENDING -> LOADING -> ACTIVE -> UNLOADING -> DISPOSED
-> FAILED
PENDING 表示插件已声明,必需服务仍未就绪。服务出现后继续加载;运行期间服务消失时,依赖方会卸载,服务恢复后再次加载。配置校验或 apply 抛错会进入 FAILED。
框架已经管理的注册
ctx.on()、ctx.tools.register() 和 ctx.plugin() 都会返回或建立可撤销注册。Fiber 卸载时,这些注册会跟着撤销。插件作者无需保存 listener 再手动 removeListener。
自己管理的资源要放进 effect
定时器、连接和 watcher 需要显式释放:
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
return () => clearInterval(timer)
})
}
修改配置触发热替换时,旧 Fiber 先释放,定时器随之停止,新 Fiber 再创建自己的定时器。HMR 后出现重复日志,通常说明某个资源绕过了 effect。
验证清理是否有效
- 在
apply中打印loaded,在 disposer 中打印disposed。 - 启动带
--patch的 Web UI。 - 修改插件配置并保存。
- 观察一次
disposed和一次新的loaded。 - 等待两个周期,确认 heartbeat 没有成倍增长。
多个清理步骤有严格顺序时,把它们放在同一个 disposer 内并逐个 await。
6. 配置 Schema 让错误尽早暴露
插件通过同名的 TypeScript 类型和 Schemastery Schema 接受配置。默认值直接放进 Schema。
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
verbose: Schema.boolean().default(false),
})
export function apply(ctx: Context, config: Config) {
console.log(`${config.greeting}, retries=${config.maxRetries}`)
}
在 patch 中传值:
- insert:
- id: config-demo
name: '/absolute/path/to/config-demo.ts'
config:
greeting: '你好'
maxRetries: 5
Schema 会在插件加载时校验配置并填充默认值。普通对象不满足 Standard Schema 接口,不能充当 Config 导出。
需要严格校验时
export interface Config {
timeout: number
mode: 'fast' | 'accurate'
}
export const Config: Schema<Config> = Schema.object({
timeout: Schema.number().default(30000),
mode: Schema.union(['fast', 'accurate']).default('fast'),
})
部署间可能变化的数值应成为配置字段。协议常量、安全约束和外部规范可以保持固定。配置引用其他服务或注册项时,在依赖就绪的最早位置校验,错误信息应包含字段名与无效值的范围。
演示:观察一次配置失败
把 mode 写成 turbo:
config:
timeout: 30000
mode: turbo
预期结果是插件加载失败并指出联合值不合法。修回 fast 后,插件重新进入 ACTIVE。
7. 开发一个完整 Tool
Tool 需要同时考虑模型接口、规范值、模型可见文本、取消与 UI 展示。下面用 project_brief 演示一个结构化返回值工具。
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'project-brief-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'project_brief',
description: 'Create a short project brief from a goal and audience.',
parameters: {
goal: {
type: 'string',
required: true,
description: 'The concrete project goal',
},
audience: {
type: 'string',
required: true,
description: 'Who will use the result',
},
maxItems: {
type: 'number',
description: 'Maximum number of acceptance items',
},
},
output: {
schema: {
type: 'object',
properties: {
title: { type: 'string' },
summary: { type: 'string' },
acceptance: { type: 'array', items: { type: 'string' } },
},
required: ['title', 'summary', 'acceptance'],
additionalProperties: false,
},
render: (_args, value) => [{
type: 'text',
text: [
`# ${value.title}`,
value.summary,
...value.acceptance.map((item, index) => `${index + 1}. ${item}`),
].join('\n'),
}],
},
async execute(args, exec) {
if (exec.signal.aborted) throw exec.signal.reason
const limit = Math.max(1, Math.min(args.maxItems ?? 3, 6))
const acceptance = [
`结果适合${args.audience}直接阅读`,
`内容覆盖目标:${args.goal}`,
'关键结论可以从外部结果复核',
'失败状态带有明确错误信息',
].slice(0, limit)
return {
title: `${args.audience}项目简报`,
summary: `围绕“${args.goal}”整理可执行范围。`,
acceptance,
}
},
}))
}
参数在 execute 之前经过统一校验,args 由 Schema 推导。Schema DSL 无法表达的约束仍需在 execute 中检查,比如非空字符串、正数与跨字段关系。
规范值与 render 分工
execute 返回可以被程序直接使用的 JSON 值。output.render 把规范值转换为模型可见内容。后续 Code Mode 可以通过字段读取 title 与 acceptance,无需解析自然语言。
基础设施失败通过抛异常表达。领域内的正常状态留在规范值中,比如进程以非零状态退出。返回值不符合 output.schema 时,注册表会把这次执行收敛成错误结果。
调用演示
在 Web UI 输入:
请调用 project_brief,为“整理 DeepSeek Harness 插件导航”生成一份面向初级开发者的简报,验收项最多 4 条。
预期工具参数接近:
{
"goal": "整理 DeepSeek Harness 插件导航",
"audience": "初级开发者",
"maxItems": 4
}
预期规范值包含 title、summary 和 acceptance 三个字段。
8. Tool 的取消、策略与 UI 卡片
遵守取消信号
网络、文件与长计算要把 exec.signal 传入底层 API。调用被取消时继续工作,会浪费资源,也可能在用户认为操作结束后产生副作用。
async execute(args, exec) {
const response = await fetch(args.url, { signal: exec.signal })
return response.text()
}
策略放在工具流水线
部署策略可以通过事件扩展:
| 扩展点 | 适合做什么 |
|---|---|
tools/pre-execute |
允许、拒绝、询问、参数前置策略 |
ctx.tools.guard() |
最终单调拒绝,后续监听器不能撤销 |
tools/execute |
截止时间、重试、指标与执行包装 |
tools/post-execute |
替换展示内容、阻止结果、附加上下文 |
tools/result |
只读观察归一化结果 |
权限逻辑集中在策略插件后,同一 Tool 可以在 Web、Headless 和 SDK 环境中复用。
UI 渲染意图要提前决定
| 卡片 | 使用场景 | 关键字段 |
|---|---|---|
generic |
普通读取、业务动作、未知工具 | 标题、类型、原始输入、位置 |
terminal |
Shell 命令 | 命令、说明、工作目录、退出信息 |
diff |
创建或修改文件 | 路径、旧文本、新文本、已应用变更 |
search |
grep 或 glob 结果 | 分组匹配、路径、截断标志、总数 |
web |
Web 搜索或抓取 | 来源列表、抓取摘要、检索类型 |
presentCall 和 presentResult 必须是纯函数。它们会在实时渲染和会话回放时重复运行,不能访问网络、会话状态、时钟或随机数。结果期 UI 需要的事实应通过持久化的 presentationMeta 提供。
9. Service、Provider 与 Consumer
可替换能力由三种角色组成:Service Definition 拥有接口、事件和领域类型;Provider 实现接口;Consumer 使用接口并交给模型、用户或上层业务。
下面的 GreeterService 把能力注册到 ctx.greeter:
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
}
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
greet(who: string) {
return `Hello, ${who}`
}
}
export function apply(ctx: Context) {
ctx.plugin(GreeterService)
}
Consumer 只声明服务名:
import type { Context } from '@deepseek-ai/cordis'
import type {} from './greeter.ts'
export const inject = ['greeter']
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('world'))
}
inject 让 Consumer 保持 PENDING,直到 greeter 服务出现。配置文件中的先后顺序不承担依赖编排,真正的顺序来自服务依赖。
什么时候拆成三类包
接口需要多个实现,或 Consumer 要在不改代码的情况下切换实现时,拆分会减少耦合。只有一个短实现时可以先放在同一个包里。拆包前检查三个问题:接口和事件由谁拥有,Provider 是否能独立替换,Consumer 的工具 Schema 是否依赖某个具体 Provider。
10. 事件与 waterfall
事件让插件在不知道监听者的情况下发出通知。Harness 使用事件处理工具结果、模型请求、权限和会话状态。
声明和监听一个事件
import type { Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Events {
'demo/report'(name: string, count: number): void
}
}
export function apply(ctx: Context) {
ctx.on('demo/report', (name, count) => {
console.log(`[demo] ${name}=${count}`)
})
ctx.emit('demo/report', 'tool_call', 2)
}
ctx.on() 属于 effect,监听器会随插件卸载。
四种常用分发模式
| 模式 | 是否等待 | 用法 |
|---|---|---|
emit |
否 | 同步广播,不收集返回值 |
parallel |
是 | 监听器并发运行并一同等待 |
serial |
是 | 按顺序运行,适合顺序决策 |
waterfall |
按监听器实现 | 环绕下游,可转换或短路结果 |
waterfall 监听器必须明确是否委托
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **'
return next()
})
只负责观察或标注的监听器要调用并等待 next()。直接返回代表当前监听器拥有最终决定,并有意短路下游。日志插件忘记 next() 时,默认行为可能静默消失。
演示:包装下游结果
ctx.on('demo/transform', async (_input, next) => {
const result = await next()
return result.toUpperCase()
})
最外层监听器调用 next(),下游完成后再把结果转成大写。调试时可以在每层打印进入和离开日志,确认短路发生在哪个监听器。
11. 会话日志与模型可见内容
会话日志是模型历史的来源。deriveMessages() 从事件日志投影模型消息,原始流式事件用于回放和 UI 呈现。fork、恢复、Transcript、遥测和持久化都依赖同一条事件流。
官方架构约定是「模型可见即已记录」。任何进入模型请求的内容都应能从日志重建。插件增加新的模型可见输入时,需要同时设计持久会话事件,并确保回放后得到同样的模型上下文。
持久事件与实时事件如何区分
| 内容 | 建议落点 |
|---|---|
| 用户消息、助手消息、工具调用与结果 | SessionEventMap 中的持久事件 |
| 当前请求的临时策略 | agent/* 或 tools/* 实时事件 |
| 文件系统、Shell、遥测策略 | 对应能力事件 |
| 下一次请求要看到的插件上下文 | agent.inject(),并落入持久上下文 |
调试会话恢复问题时,先比较原始事件流,再比较投影后的模型消息。直接修改投影结果只能掩盖缺失的持久事实。
12. 把插件打成 Bundle
一个可安装 Bundle 至少包含 manifest、patch 和插件入口:
hello-plugin/
├── package.json
├── cordis.patch.yml
└── index.js
package.json:
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}
index.js:
export const name = 'hello-plugin'
export function apply() {
console.log('[hello-plugin] plugin loaded')
}
cordis.patch.yml:
- insert:
- id: hello
name: dsh-hello-plugin
安装到独立 profile:
dsh plugin --profile demo add ./hello-plugin
dsh --profile demo --dump-config
dsh --profile demo
移除:
dsh plugin --profile demo remove dsh-hello-plugin
从不同来源安装
dsh plugin --profile web add package-name
dsh plugin --profile web add github:you/plugin#<commit>
dsh plugin --profile web add git+https://github.com/you/plugin.git
dsh plugin --profile web add ./plugin-0.1.0.tgz
Git 安装获取源码。TypeScript 包需要在 prepare 中构建出发布入口,pnpm 10 还要求用户显式授权构建脚本。该脚本运行在 Agent 沙箱之外,授权前要检查源码、发布者与锁定版本。npm 预构建包和 tarball 可以省去安装时构建。
13. 测试插件的四层方法
一个工具能注册成功,只说明入口工作。插件开发应同时验证业务值、生命周期、真实组合和用户可见输出。
| 层级 | 主要验证 | 例子 |
|---|---|---|
| 单元测试 | Schema、execute、错误与边界 | 非法参数、取消、返回值校验 |
| 生命周期测试 | 注册能随 Fiber 撤销 | dispose 后工具和监听器消失 |
| Loader 组合测试 | patch、依赖和真实入口 | 通过 cordis.yml 启动并检查输出 |
| 快照或 e2e | 模型、协议或 UI 可见行为 | Transcript、Web 工具卡片、真实 API 冒烟 |
单元测试示意
import { describe, expect, it } from 'vitest'
describe('project_brief', () => {
it('limits acceptance items', async () => {
const result = await executeBrief({
goal: '整理插件导航',
audience: '初级开发者',
maxItems: 2,
})
expect(result.acceptance).toHaveLength(2)
})
})
测试最终行为时,优先从外部重新读取文件或运行命令。只检查 Agent 回复里有没有关键词,无法证明文件、进程或网络状态真的改变。
HMR 安全测试
- 创建上下文并挂载插件。
- 断言工具或监听器已经注册。
- 释放该插件的 Fiber。
- 断言注册项消失。
- 再次挂载,确认只有一个实例。
模型可见、协议可见或 UI 可见的非平凡变更,还需要当前仓库测试策略规定的组装快照。
14. 安全审查第三方插件
插件和依赖可以访问主机能力。安装社区插件前至少检查下面几项:
| 检查项 | 需要确认的内容 |
|---|---|
| 来源 | 仓库归属、发布者、版本标签、提交记录 |
| 构建 | 是否包含 prepare、postinstall 或下载脚本 |
| 权限 | 文件、Shell、网络、凭据和会话访问范围 |
| 依赖 | 新增依赖数量、锁文件、已知高风险包 |
| 配置 | 是否要求明文密钥,是否把密钥写入日志 |
| 持久化 | 会话、缓存、遥测和导出数据存放位置 |
| 更新 | 是否能锁定 commit 或固定版本,如何回滚 |
社区目录适合发现项目,不能替代安全审查。测试新插件时使用独立 profile 和低权限工作区,先运行 --dump-config,再发送只读任务。确认行为符合预期后再开放写入或 Shell。
15. 排错速查与诊断顺序
| 现象 | 先看哪里 | 处理方式 |
|---|---|---|
| Web 输入框不可用 | 工作区与模型 | 选择工作区,配置可用模型 |
MISSING_CREDENTIAL |
模型设置与凭据引用 | 保存 Provider 密钥或提供被引用的环境变量 |
UNKNOWN_MODEL |
Provider 模型列表 | 选择已配置模型或补充自定义模型 |
| 插件没有输出 | --dump-config 与 Fiber |
确认配置行存在,再检查是否停在 PENDING |
| 本地插件无法解析 | patch 中的模块路径 | 使用绝对路径,检查拼写与 ESM 入口 |
| 配置加载失败 | 导出的 Config |
使用 Schemastery Schema,按错误修正字段 |
| 覆盖后字段消失 | 最终 config |
重述需要保留的完整 config |
Git 插件缺少 lib/ |
包的 prepare |
作者提供自包含构建,用户审查后授权 |
| HMR 后重复监听 | effect 所有权 | 把监听器、定时器与 watcher 交给 Fiber |
| waterfall 提前结束 | 监听器实现 | 观察型监听器调用并等待 next() |
诊断建议固定成四步:
- 运行
dsh --profile <name> --dump-config,确认条目进入最终树。 - 检查条目来源、
id、模块路径和disabled。 - 查看 Fiber 是否为
PENDING或FAILED,再查缺失的inject。 - 配置和生命周期正常后,再进入业务代码、模型请求和 Provider 调试。
演示:插件静默不工作
一个 Tool 声明了 inject = ['tools', 'missingService'],配置中没有提供 missingService。该插件会停在 PENDING,apply 不会执行。移除错误依赖或提供对应 Service 后,Fiber 会继续加载。
实验一:从零加载本地插件
目标:创建一个函数插件,通过绝对路径 patch 加载,并观察一次热替换。
步骤:
- 创建
scratch-plugin/src/hello.ts和scratch-plugin/cordis.yml。 - 在
apply中打印版本号v1。 - 用
pnpm dsh web --patch ./scratch-plugin/cordis.yml启动。 - 把日志改成
v2并保存。 - 确认旧实例被卸载,终端只出现一次新的
v2。
验收:
--dump-config能找到稳定id。- 路径来自绝对位置。
- 热替换后没有重复定时器或重复监听。
- 恢复错误代码时,Fiber 能进入明确失败状态。
实验二:实现 project_brief Tool
目标:完成第 7 章的结构化 Tool,并验证三条错误路径。
练习:
- 把
goal与audience设为必填字符串。 - 把
maxItems限制在 1 到 6 之间。 - 返回对象规范值,
render输出人类可读的 Markdown。 - 传入空字符串,确认执行器给出明确错误。
- 返回一个缺少
acceptance的对象,观察输出校验失败。 - 触发取消信号,确认底层工作停止。
预期调用:
请调用 project_brief,为“做一个插件安全检查器”生成面向插件维护者的简报,验收项 3 条。
验收:程序化调用可以直接读取字段,模型看到的内容由 render 产生,UI 未知时仍能回退到通用卡片。
实验三:切换两个 Provider
目标:同一个 greeter Service 提供中文和英文两个实现,Consumer 不改代码。
建议文件:
provider-en.ts
provider-zh.ts
consumer.ts
cordis-en.yml
cordis-zh.yml
两个 Provider 都占用 ctx.greeter,分别返回 Hello, world 与 你好,world。两份配置一次只挂载一个 Provider。Consumer 始终只声明 inject = ['greeter'] 并调用 ctx.greeter.greet()。
验收:切换配置后,Consumer 重新加载并使用新 Provider;移除 Provider 时 Consumer 进入 PENDING;恢复 Provider 后 Consumer 自动重新加载。
实验四:打包、安装与移除 Bundle
目标:把实验二的 Tool 打成 dsh-project-brief Bundle,安装进 demo profile。
步骤:
- 准备
package.json、构建后入口和cordis.patch.yml。 - 在 manifest 中声明
dsh.bundle.patch。 - 运行
dsh plugin --profile demo add ./dsh-project-brief。 - 用
--dump-config确认 Bundle 层与 Tool 行。 - 启动 profile 并调用 Tool。
- 移除包,再次 dump 配置,确认层和依赖都消失。
验收:安装不依赖 monorepo 相邻文件;发布文件列表包含运行时入口和 patch;卸载后没有残留配置行;从 git 安装时明确记录 prepare 与授权要求。
附录 A:常用命令
# 启动 Web UI
npx @deepseek-ai/dsh web
# 从源码启动 Web UI
pnpm dsh web
# 通过本地覆盖层启动
pnpm dsh web --patch /absolute/path/to/cordis.yml
# 查看最终配置树
dsh --profile web --dump-config
# Headless 一次性任务
dsh --profile headless "Summarize the repository"
# 安装、移除 Bundle
dsh plugin --profile demo add ./hello-plugin
dsh plugin --profile demo remove dsh-hello-plugin
# 仓库常用验证
pnpm run test
pnpm run typecheck
pnpm run lint
pnpm run build
附录 B:术语速查
| 术语 | 一句话说明 |
|---|---|
| Cordis | DeepSeek Harness 使用的插件框架 |
| Context | 插件访问服务、事件和生命周期的入口 |
| Fiber | 一次插件挂载的实例与状态 |
| effect | 跟随 Fiber 安装和释放的注册或资源 |
| inject | 插件对 Service 的硬性依赖声明 |
| Tool | 面向模型的可调用 Consumer |
| Service Definition | 接口、类型、事件和 ctx 键的拥有者 |
| Provider | Service 的具体实现 |
| Consumer | 调用 Service 的插件、Tool 或命令 |
| Bundle | 携带 patch 的分发包 |
| Profile | 按顺序组合多个 Bundle 的运行配置 |
| patch | 根据稳定 id 插入、替换或禁用插件行的配置层 |
| SessionEvent | 可持久化并用于回放的会话事实 |
附录 C:官方参考
本文对应 Developer Preview 的当前快照。命令、字段或导出接口不一致时,先看当前版本的官方文档、包 README 和 TypeScript 类型,再更新示例。