Developer Preview · Practice Guide

DeepSeek Harness从入门到插件化实践:增强版

从运行时地图走到配置、Tool、Service、Bundle、测试与四个动手实验。每一章都给出方法、示例和可验证的结果。

章节
16 个主题
实验
4 条实践链路
重点
插件生命周期与能力分层
适合
使用者与插件开发者
展开完整目录
  1. 0先选一条学习路线
  2. 1建立运行时地图
  3. 2安装、启动与第一次验证
  4. 3Profile、Bundle 与 patch 如何组合
  5. 4第一个本地插件
  6. 5生命周期、effect 与热替换
  7. 6配置 Schema 让错误尽早暴露
  8. 7开发一个完整 Tool
  9. 8Tool 的取消、策略与 UI 卡片
  10. 9Service、Provider 与 Consumer
  11. 10事件与 waterfall
  12. 11会话日志与模型可见内容
  13. 12把插件打成 Bundle
  14. 13测试插件的四层方法
  15. 14安全审查第三方插件
  16. 15排错速查与诊断顺序
  17. ·实验一:从零加载本地插件
  18. ·实验二:实现 project_brief Tool
  19. ·实验三:切换两个 Provider
  20. ·实验四:打包、安装与移除 Bundle
  21. ·附录 A:常用命令
  22. ·附录 B:术语速查
  23. ·附录 C:官方参考

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,也可完整移除

如果只想快速理解架构,先记住一条式子:

TEXT
Agent = Model + Harness

Harness = 插件树 + 服务依赖 + 事件 + 会话日志 + 权限策略

1. 建立运行时地图

一次任务可以沿着这条主线理解:

TEXT
用户输入
  -> Agent inbox 领取消息
  -> 组装系统提示词、历史与工具 Schema
  -> LLM 请求
  -> 文本回复或工具调用
  -> 工具策略、参数校验与执行
  -> tool/result 写入会话日志
  -> 下一次模型请求或 turn/end

一个 turn 表示一次完整轮次,一个 step 表示一次模型请求及其触发的工具执行。一个轮次可以没有步骤,也可以包含多个步骤。模型决定连续调用工具时,Harness 会把每次工具结果写回会话,再组装下一次请求。

九个常见概念

概念 在运行时负责什么 开发时常见位置
Context 当前插件可见的服务和运行环境 apply(ctx)ctx.toolsctx.on()
Plugin 向运行时贡献能力的模块 导出 apply 的 TypeScript 或 JavaScript 文件
Service 挂在 ctx 上供其他插件调用的能力 ctx.toolsctx.llmctx.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 一套可启动的插件组合 webheadless 或自定义 profile

Tool 是面向模型的 Consumer。斜杠命令面向人,由 ctx.commands 处理。两者都可以调用 Service,进入会话的路径和呈现方式不同。

开发新能力时先定位归属

需求 建议扩展点
新增模型提供方 ctx.llm 注册适配器
新增模型可调用能力 ctx.tools 注册 Tool
新增用户命令 ctx.commands 注册命令
拦截工具执行 监听 tools/pre-executetools/executetools/post-execute
记录持久事实 扩展 SessionEventMap 并写入会话事件
切换本地与远程实现 设计 Service Definition、Provider 与 Consumer
新增浏览器交互 注册 Host 服务和 Web Client 组件

2. 安装、启动与第一次验证

npm 路径

安装 Node.js 后,在准备作为工作区的目录中运行:

SH
npx @deepseek-ai/dsh web

默认访问地址是 http://127.0.0.1:3080。打开 Web UI 后完成两项设置:进入「设置 > 模型」保存可用模型的凭据,通过「选择工作区」添加项目目录。工作区或模型缺失时,输入框会保持不可用。

第一条请求建议范围清楚,并要求返回可核对的路径:

TEXT
请概括这个仓库的目录结构,列出三个主要入口,并给出对应文件路径。

验证时看三处:浏览器里出现回复;工具卡片显示读取或搜索动作;终端没有 MISSING_CREDENTIALUNKNOWN_MODEL 或 Loader 解析错误。

从源码运行

准备二次开发环境时使用源码路径:

SH
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:

SH
pnpm dsh --profile headless "Summarize the repository"

需要真实模型时提供凭据。只想验证配置树和插件加载时,可以先运行 --dump-config,这一阶段不需要发起模型请求。

凭据放在哪里

凭据通过设置与凭据引用提供。源码、patch、演示命令和会话文本中都不应出现真实密钥。Web 客户端只接收脱敏描述符。处理敏感仓库时,还要确认会话日志、导出文件和遥测的存放位置。

3. Profile、Bundle 与 patch 如何组合

运行中的 dsh 是一棵插件树。Profile 描述一套可启动组合,Bundle 贡献一层配置,patch 根据稳定 id 插入、替换、禁用或重组配置项。

最终配置按下面的优先级叠加:

  1. Profile 中列出的 Bundle patch,顺序与 bundles 一致。
  2. Profile 自己的 cordis.patch.yml
  3. $DSH_HOME/cordis.patch.yml
  4. 命令行中的每个 --patch <path>,按参数顺序应用。

查看实际生效的配置:

SH
dsh --profile web --dump-config

输出会标注每层来源。排查插件问题时先确认目标 id 是否存在、来自哪一层、是否被禁用,再检查插件代码。

演示:禁用一项能力

假设 --dump-config 中存在 id: tool-web,可在覆盖层中写:

YAML
- patch:
    id: tool-web
    disabled: true

运行:

SH
dsh --profile web --patch /absolute/path/disable-web.yml --dump-config

预期结果是 tool-web 保留在最终树中,同时标记为禁用。实际 id 以当前输出为准。

覆盖 config 会替换整块对象

假设基础配置是:

YAML
config:
  timeoutMs: 30000
  mode: accurate

上层 patch 只写:

YAML
config:
  mode: fast

最终 config 只剩 mode。需要保留 timeoutMs 时必须重述:

YAML
config:
  timeoutMs: 30000
  mode: fast

这条规则适合写进发布文档,因为用户常把 patch 当成深度合并。

4. 第一个本地插件

函数插件已经覆盖多数扩展场景。插件需要公开 Service 时,再考虑类形式。

创建目录

在仓库根目录运行:

SH
mkdir -p scratch-plugin/src

创建 scratch-plugin/src/my-plugin.ts

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,把路径替换成当前仓库的绝对路径:

YAML
- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

启动:

SH
pnpm dsh web --patch ./scratch-plugin/cordis.yml

预期终端输出:

TEXT
[hello-plugin] plugin loaded

本地绝对路径很重要。Loader 会从 profile 环境解析模块,相对 patch 文件的位置并不等于模块解析基准。

三种插件形态

TS
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 改成抛错,插件会明确进入加载失败:

TS
export function apply() {
  throw new Error('apply exploded')
}

把 patch 中的路径写错,Loader 会通过 logger 报告解析失败。某些启动阶段日志可能早于 console 导出器开始观察,因此看起来像插件没有运行。先检查路径和包名,再检查业务逻辑。

5. 生命周期、effect 与热替换

一个插件实例对应一个 Fiber。常见状态如下:

TEXT
PENDING -> LOADING -> ACTIVE -> UNLOADING -> DISPOSED
                    -> FAILED

PENDING 表示插件已声明,必需服务仍未就绪。服务出现后继续加载;运行期间服务消失时,依赖方会卸载,服务恢复后再次加载。配置校验或 apply 抛错会进入 FAILED

框架已经管理的注册

ctx.on()ctx.tools.register()ctx.plugin() 都会返回或建立可撤销注册。Fiber 卸载时,这些注册会跟着撤销。插件作者无需保存 listener 再手动 removeListener

自己管理的资源要放进 effect

定时器、连接和 watcher 需要显式释放:

TS
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。

验证清理是否有效

  1. apply 中打印 loaded,在 disposer 中打印 disposed
  2. 启动带 --patch 的 Web UI。
  3. 修改插件配置并保存。
  4. 观察一次 disposed 和一次新的 loaded
  5. 等待两个周期,确认 heartbeat 没有成倍增长。

多个清理步骤有严格顺序时,把它们放在同一个 disposer 内并逐个 await

6. 配置 Schema 让错误尽早暴露

插件通过同名的 TypeScript 类型和 Schemastery Schema 接受配置。默认值直接放进 Schema。

TS
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 中传值:

YAML
- insert:
    - id: config-demo
      name: '/absolute/path/to/config-demo.ts'
      config:
        greeting: '你好'
        maxRetries: 5

Schema 会在插件加载时校验配置并填充默认值。普通对象不满足 Standard Schema 接口,不能充当 Config 导出。

需要严格校验时

TS
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

YAML
config:
  timeout: 30000
  mode: turbo

预期结果是插件加载失败并指出联合值不合法。修回 fast 后,插件重新进入 ACTIVE

7. 开发一个完整 Tool

Tool 需要同时考虑模型接口、规范值、模型可见文本、取消与 UI 展示。下面用 project_brief 演示一个结构化返回值工具。

TS
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 可以通过字段读取 titleacceptance,无需解析自然语言。

基础设施失败通过抛异常表达。领域内的正常状态留在规范值中,比如进程以非零状态退出。返回值不符合 output.schema 时,注册表会把这次执行收敛成错误结果。

调用演示

在 Web UI 输入:

TEXT
请调用 project_brief,为“整理 DeepSeek Harness 插件导航”生成一份面向初级开发者的简报,验收项最多 4 条。

预期工具参数接近:

JSON
{
  "goal": "整理 DeepSeek Harness 插件导航",
  "audience": "初级开发者",
  "maxItems": 4
}

预期规范值包含 titlesummaryacceptance 三个字段。

8. Tool 的取消、策略与 UI 卡片

遵守取消信号

网络、文件与长计算要把 exec.signal 传入底层 API。调用被取消时继续工作,会浪费资源,也可能在用户认为操作结束后产生副作用。

TS
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 搜索或抓取 来源列表、抓取摘要、检索类型

presentCallpresentResult 必须是纯函数。它们会在实时渲染和会话回放时重复运行,不能访问网络、会话状态、时钟或随机数。结果期 UI 需要的事实应通过持久化的 presentationMeta 提供。

9. Service、Provider 与 Consumer

可替换能力由三种角色组成:Service Definition 拥有接口、事件和领域类型;Provider 实现接口;Consumer 使用接口并交给模型、用户或上层业务。

下面的 GreeterService 把能力注册到 ctx.greeter

TS
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 只声明服务名:

TS
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 使用事件处理工具结果、模型请求、权限和会话状态。

声明和监听一个事件

TS
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 监听器必须明确是否委托

TS
ctx.on('demo/transform', async (input, next) => {
  if (input.includes('blocked')) return '** blocked **'
  return next()
})

只负责观察或标注的监听器要调用并等待 next()。直接返回代表当前监听器拥有最终决定,并有意短路下游。日志插件忘记 next() 时,默认行为可能静默消失。

演示:包装下游结果

TS
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 和插件入口:

TEXT
hello-plugin/
├── package.json
├── cordis.patch.yml
└── index.js

package.json

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

JS
export const name = 'hello-plugin'

export function apply() {
  console.log('[hello-plugin] plugin loaded')
}

cordis.patch.yml

YAML
- insert:
    - id: hello
      name: dsh-hello-plugin

安装到独立 profile:

SH
dsh plugin --profile demo add ./hello-plugin
dsh --profile demo --dump-config
dsh --profile demo

移除:

SH
dsh plugin --profile demo remove dsh-hello-plugin

从不同来源安装

SH
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 冒烟

单元测试示意

TS
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 安全测试

  1. 创建上下文并挂载插件。
  2. 断言工具或监听器已经注册。
  3. 释放该插件的 Fiber。
  4. 断言注册项消失。
  5. 再次挂载,确认只有一个实例。

模型可见、协议可见或 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()

诊断建议固定成四步:

  1. 运行 dsh --profile <name> --dump-config,确认条目进入最终树。
  2. 检查条目来源、id、模块路径和 disabled
  3. 查看 Fiber 是否为 PENDINGFAILED,再查缺失的 inject
  4. 配置和生命周期正常后,再进入业务代码、模型请求和 Provider 调试。

演示:插件静默不工作

一个 Tool 声明了 inject = ['tools', 'missingService'],配置中没有提供 missingService。该插件会停在 PENDINGapply 不会执行。移除错误依赖或提供对应 Service 后,Fiber 会继续加载。

实验一:从零加载本地插件

目标:创建一个函数插件,通过绝对路径 patch 加载,并观察一次热替换。

步骤:

  1. 创建 scratch-plugin/src/hello.tsscratch-plugin/cordis.yml
  2. apply 中打印版本号 v1
  3. pnpm dsh web --patch ./scratch-plugin/cordis.yml 启动。
  4. 把日志改成 v2 并保存。
  5. 确认旧实例被卸载,终端只出现一次新的 v2

验收:

  • --dump-config 能找到稳定 id
  • 路径来自绝对位置。
  • 热替换后没有重复定时器或重复监听。
  • 恢复错误代码时,Fiber 能进入明确失败状态。

实验二:实现 project_brief Tool

目标:完成第 7 章的结构化 Tool,并验证三条错误路径。

练习:

  1. goalaudience 设为必填字符串。
  2. maxItems 限制在 1 到 6 之间。
  3. 返回对象规范值,render 输出人类可读的 Markdown。
  4. 传入空字符串,确认执行器给出明确错误。
  5. 返回一个缺少 acceptance 的对象,观察输出校验失败。
  6. 触发取消信号,确认底层工作停止。

预期调用:

TEXT
请调用 project_brief,为“做一个插件安全检查器”生成面向插件维护者的简报,验收项 3 条。

验收:程序化调用可以直接读取字段,模型看到的内容由 render 产生,UI 未知时仍能回退到通用卡片。

实验三:切换两个 Provider

目标:同一个 greeter Service 提供中文和英文两个实现,Consumer 不改代码。

建议文件:

TEXT
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。

步骤:

  1. 准备 package.json、构建后入口和 cordis.patch.yml
  2. 在 manifest 中声明 dsh.bundle.patch
  3. 运行 dsh plugin --profile demo add ./dsh-project-brief
  4. --dump-config 确认 Bundle 层与 Tool 行。
  5. 启动 profile 并调用 Tool。
  6. 移除包,再次 dump 配置,确认层和依赖都消失。

验收:安装不依赖 monorepo 相邻文件;发布文件列表包含运行时入口和 patch;卸载后没有残留配置行;从 git 安装时明确记录 prepare 与授权要求。

附录 A:常用命令

SH
# 启动 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 类型,再更新示例。