[AI/Agent/DSH] DeepSeek Harness 使用指南
0 序
-
接续: [AI/Agent] DeepSeek Harness(dsh):一切皆插件的开源 Agent 运行时底座——未来AI Agent/Hardness工程领域的“Linux” - 博客园/数据知音
-
本篇侧重介绍一下安装部署、日常使用。
-
先上效果——执行一个调研任务:

1 安装部署
环境要求(Windows / Linux 通用)
| 依赖 | 版本要求 | 说明 |
|---|---|---|
| Node.js | ^22.19.0 或 >=24.0.0 | CI 覆盖 22.19 / 24 / 26 |
| pnpm | 11.7.0 | 通过 corepack enable 启用 |
| Git | >=2.26 | 支持 worktree 扩展 |
| DEEPSEEK_API_KEY | 可选(Web/headless/ACP/e2e 需要) | 可换 OpenAI 兼容端点 |
部署方式1:npm 一键启动(推荐 / 亲测)
npx @deepseek-ai/dsh web
# 自动启动 Web UI,默认 http://127.0.0.1:3080
# 首次启动后在 Settings → Models 配置 DeepSeek API Key
# 加 --no-open 仅启动服务不打开浏览器
笔者的操作日志: (下载安装资源,需要好一会儿的时间,需要耐心等待)
PS C:\Users\xxx> npx @deepseek-ai/dsh web Need to install the following packages: @deepseek-ai/dsh@0.1.1-rc.2 Ok to proceed? (y) y npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead dsh web: http://127.0.0.1:3080 dsh web: opening the default browser; pass --no-open to disable
输入: http://127.0.0.1:3080 即可访问
部署方式2:从源码构建(推荐 / 亲测)
Windows 注意用 PowerShell 作为命令窗口的运行环境,同样适用
# 安装 corepack : 装到 npm 全局目录(`%APPDATA%\npm`),装完需【重开一个终端】让 PATH 生效;之后项目 `package.json` 若有 `packageManager` 字段,corepack 会自动锁定并切换对应 pnpm 版本。
npm install -g corepack
# 查验 pnpm 是否已安装 | 若未安装,则安装之: npm install -g pnpm (可考虑配置 store / cache / state / bin 等目录到除C盘之外的其他盘,避免C盘系统盘后期越加臃肿)
pnpm --version
----------
# 克隆代码到本地
git clone https://github.com/deepseek-ai/deepseek-harness.git
# 进入项目根目录,之后命令都在该项目作用域内执行(pnpm 在哪个目录执行,就装哪个目录的依赖)
cd deepseek-harness
# 启用 Corepack(Node 自带的 "包管理器管理器"):
## 它会在 `node` 命令旁创建 `pnpm` 的 shim;
### 此后每次敲 `pnpm`,它会自动读取本项目 `package.json` 里的 `packageManager` 字段(如 `pnpm@9.x.x`)、并【切换到该精确版本】(本地没有会自动下载),保证团队 / CI 用同一个 pnpm 版本
## corepack 只是 "帮你安装 / 锁定 pnpm 版本" 的额外工具,不是运行 deepseek-harness 的必需项
### Corepack 只随 Node 14.19.0 ~ <25.0.0 分发,node版本不在此范围的,可跳过此步骤 ——> 可直接跳过 corepack,用 pnpm 就行(推荐)
corepack enable
# 按当前项目清单安装依赖(等价 pnpm i)
pnpm install
# 执行 `package.json` 中 `scripts.build` 定义的构建脚本(通常产出生产构建产物到 dist/)
pnpm run build
# 启动 Web UI(端口 3080) # 等价 `pnpm exec dsh web`:
## 在项目范围内执行 `node_modules/.bin` 里的 `dsh` 命令(deepseek-harness 自带 CLI),`web` 是其子命令,启动端口 3080 的 Web UI。
## 官方机制:命令不与 pnpm 内置命令冲突时,`exec` 可省略
pnpm dsh web
安装过程中,会自动创建
~/.dsh文件夹,来存放全局配置、profiles、会话等元信息。
- 推荐文献
部署方式3:无头(headless)一次性任务
export DEEPSEEK_API_KEY=sk-your-key-here # PowerShell: $env:DEEPSEEK_API_KEY="..."
pnpm dsh --profile headless "summarize this workspace"
部署方式4:ACP 自动化服务 / Python SDK
pnpm run demo:acp # ACP JSON-RPC stdio 服务
# Python SDK 见仓库 python/ 目录 README
关键操作
pnpm dsh web # 启动 Web UI(端口 3080) # 等价 `pnpm exec dsh web`
pnpm dsh --profile web --dump-config # 查看当前 profile 的插件组合树
pnpm run demo:cordis # 自引用 demo:Agent 检查/修改自己的运行时插件
pnpm run mock:llm # 运行 mock LLM 服务器(测试)
pnpm run test / test:coverage / test:e2e / lint / check:all
环境变量
也可不在配置文件中配。在 WEB-UI 上去配置
DEEPSEEK_API_KEY=sk-... # 必填(真实模型调用)
DEEPSEEK_BASE_URL=https://api.deepseek.com # 可选,默认官方 API
DSH_PERMISSION_MODE=danger-full-access # 可选,默认 workspace-write
权限模式
workspace-write:限制写入工作区与临时目录(默认)read-only:禁止写入danger-full-access:放开限制
常见踩坑
- Node.js 版本过低(需 22.19+ / 24+)会报错;
pnpm未启用 corepack ,导致版本不一致;- 沙箱默认
workspace-write,操作工作区外文件会提示[sandbox: file access denied](策略行为,非 bug); - Windows 上 bash 链自动禁用改用 pwsh,如需 bash 需在 profile patch 中同时禁用 pwsh 行并启用 bash 行;
- 同时挂载
dsh-fs-local与dsh-fs-sandbox会因ctx.fs重复注册而加载失败(框架 fail-loud,不静默降级); - 预览版会话格式不向后兼容,升级后旧日志可能无法加载。
2 使用指南
主页


添加工作区

调用 Skill (必读)
- 调用、查看 Skill:键入
/即可查看、调用 DSH 加载的 Skill
- 支持从
~/.agents/skills/目录(跨多 AI Agent 的Skill目录) 识别Skill
- 添加 Skills
- 方法1:添加全局的、跨AI Agents的 Skill,可在
~/.agents/skills/目录下添加。- 推荐文献
执行用户任务(必读)
- 例如: 用户任务提示词中,调用用户指定的Skill,输出文件报告。

- 能够查看到每一步的具体执行细节。

- 下载的会话日志

设置
通用设置
- 权限、语言、外观、字号大小、对话显示、繁忙时Enter键的行为

- 配置文件 settings.yaml
ui-onboarding:
welcomeNoticeVersion: 2026-08-13.1
模型(必读)
创建自定义的模型提供方(硅基流动为例)
- 创建自定义的模型提供方
以硅基流动的
deepseek-ai/DeepSeek-V4-Pro模型为例:
- 硅基流动是一家主要以私有化部署开源模型,并提供 模型API服务的第三方模型供应商/平台。
- 科普博客,参见: [AI/GPT] 硅基流动(SiliconFlow) : AI大模型时代的基础设施(Model API as Service) - 博客园/千千寰宇
- 亦可直接注册平台账号,以获得模型API Key: https://account.siliconflow.cn/

- API协议: openai-completions
- API地址: https://api.siliconflow.cn/v1
- 模型:
deepseek-ai/DeepSeek-V4-Pro
- 创建完成后:

- 对应的配置文件:
- 文件:
~/.dsh/settings.yaml:llm-pi-ai:providers/agent-default-model

- 文件:
~/.dsh/.credentials.yaml:refs

插件
插件配置

- 终端: 限制 agent 运行的每一条命令
- Agent 循环: Agent 如何派发工具调用
- SubAgent: 控制 Agent 为 SubAgent 选择模型的权限
- 网页搜索: DeepSeek 搜索提供方
插件列表

- 会话插件: 由 Agent 预设按会话组成
- 插件模式:支持: 标准模式(默认)、PTC模式、极简模式、创造模式

- 全局插件:系统与所有会话共用
Agent 预设模式x4种:标准模式 / PTC模式 / 极简模式 / 创造模式

3 用户自定义的插件开发(必读)
Step0 从源码构建、并启动
- 环境要求
- Node.js ^22.19 或 >=24
- pnpm(packageManager: "pnpm@11.7.0")
- shell 环境: linux:shell / macos:shell / windows:powershell (笔者使用的 powershell )
- 构建与启动
参考本文档:安装部署章节 - 方式2:从源码构建
构建与启动均没有问题后,即可停止运行——说明所需软件环境已准备妥当。
Step1 开发1款最简最小的插件(hello-plugin)
亦可查看官网的最小插件示例: https://deepseek-harness.github.io/deepseek-harness/reference/cookbook/adding-a-tool

- 插件的本质
在 DeepSeek-Harness 中,插件是一个导出
apply函数的 TypeScript 模块:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// 在这里注册能力
}
DSH 框架在加载时调用
apply,传入ctx——通过它注册的一切在插件卸载时自动清理。
- 插件目录结构
deepseek-harness/
...
scratch-plugin/ : 新建临时的插件文件夹
src/ : 源码
my-plugin.ts : 自定义插件的逻辑
cordis.yml : 符合 cordis 的插件定义文件(元数据文件,指向自定义的插件文件)
创建自定义插件项目的文件夹
- 在DSH仓库的根目录创建临时项目:
mkdir -p scratch-plugin/src
编写插件逻辑(my-plugin.ts)
- 创建
scratch-plugin/src/my-plugin.ts:
vim 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!')
}
注册到 cordis.yml
- 创建
scratch-plugin/cordis.yml,将/absolute/path/to/deepseek-harness替换为DSH仓库内的实际绝对路径:
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
例如,笔者填写的
name值是:'file://F:/Codes-Minor/Github/deepseek-harness/scratch-plugin/src/my-plugin.ts'
注意:插件路径必须是绝对路径。
patch文件贡献配置,不改变loader的模块解析基目录。
启动
pnpm dsh web --patch ./scratch-plugin/cordis.yml
终端打印
[hello-plugin] plugin loaded!即为成功。

访问 http://127.0.0.1:3080 ,在 WEB-UI(
设置-插件-插件列表-全局插件) 亦可查看插件的加载情况

Step2 开发1个 Tool 插件(greet-tool-plugin)
Step1 中的文件名、插件名称、插件ID等没有统一,是为了便于理解其作用,且无需严格一致。
但本步骤中,尽量要达成一致,这样便于日常的理解、规范化管理。
编写 Tool 插件的逻辑(greet-tool-plugin.ts)
- 创建并编写
scratch-plugin/src/greet-tool-plugin.ts
类比:
scratch-plugin/src/my-plugin.ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet-tool-plugin',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: ( _args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
- 关键点解析
inject: ['tools']:声明依赖ctx.tools服务。Cordis 保证该服务就绪后才调apply。如果ctx.tools的provider被热替换,本插件自动dispose并重新apply`。defineToolDSL:
| 字段 | 作用 |
|---|---|
name |
模型可见的工具名(64 字符内,[A-Za-z0-9_-]) |
description |
模型用来决定是否调用的描述 |
parameters |
JSON Schema,自动推导 args 类型并运行时校验 |
output.schema |
规范值的 schema 声明 |
output.render |
将规范值转换为模型可见的 ContentBlock |
execute |
实际执行逻辑,接收校验后的 args + ToolExecution context |
ctx.tools.register()返回disposer:插件卸载时自动调用,tool 从注册表移除,模型下次请求不再看到它。
添加插件的元信息(cordis.yml)
- 在 cordis.yml 中添加插件的元信息
- insert:
- id: tool-greet
name: 'file://F:/Codes-Minor/Github/deepseek-harness/scratch-plugin/src/greet-tool-plugin.ts'
运行、并验证——让DSH主动调用目标插件
- 运行
pnpm dsh web --patch ./scratch-plugin/cordis.yml
- 验证——让DSH主动调用目标插件
打开 http://127.0.0.1:3080 ,在对话中直接说类似的语句:
使用 tool-greet 工具,输入内容是"know-data"、或者说:使用工具欢迎 know-data

DSH 会看到你注册的工具,发起
tool call,然后执行execute函数、并返回结果给模型。

总结/分析: Tool 的执行pipeline
- 你注册的 tool 会走完整的执行管道:
模型返回 tool_call → tools/pre-execute (waterfall)
→ tools/execute (waterfall)
→ 你的 execute() 函数
→ tools/post-execute (waterfall)
→ tool/result (session event)
这意味着:
- 权限策略可以在
tools/pre-execute拦截你的 tool- 审批机制可以在执行前要求用户确认
- 超时/取消通过
ToolExecution.signal传递- 结果自动记录到 session log
和 MCP bridge 注册的 tool、和其他原生 tool——走的是完全相同的管道。
Step3 添加可配置项
- 让问候语可定制。更新插件的
greet-tool-plugin:
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool-plugin'
export const inject = ['tools']
// 定义 Config interface
export interface Config {
greeting: string
emoji: boolean
}
// 导出同名 Schema(Cordis 用它校验 + 填充默认值)
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
emoji: Schema.boolean().default(true),
})
export function apply(ctx: Context, config: Config) {
ctx.tools.register(defineTool({
name: 'greet-tool-plugin',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
const suffix = config.emoji ? ' 👋' : ''
return `${config.greeting}, ${args.name}!${suffix}`
},
}))
}
- 更新
scratch-plugin/cordis.yml:
- insert:
- id: tool-greet
name: 'file://F:/Codes-Minor/Github/deepseek-harness/scratch-plugin/src/greet-tool-plugin.ts'
config:
greeting: 'Hey'
emoji: false
- 再运行,查看效果

设计原则
- 无硬编码可调参数:凡是不同部署可能需要不同值的参数,必须定义为 Config 字段。
检验标准:能否在 cordis.yml 中改变这个值而不修改代码?
- 配置错误要响亮:Schema 在插件加载时执行校验。
不合法配置会让插件加载失败(fiber → FAILED),给出明确错误信息,而非运行时静默行为异常。
HMR 行为
- 修改
cordis.yml中的config字段后,Cordis 会:
- 卸载旧
fiber(ctx.tools.register的disposer自动执行,tool注销)- 创建新
fiber- 用新
config调apply(tool 重新注册)
- 结果:配置变更实时生效,无需重启进程。
Step4 ctx.effect() 管理外部资源
- 假设你的
tool插件 需要维持一个长连接:
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'db-query-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
let pool: ConnectionPool | undefined
ctx.effect(() => {
pool = createConnectionPool({ host: 'localhost', port: 5432 })
return () => {
pool?.close()
pool = undefined
}
})
ctx.tools.register(defineTool({
name: 'db-query-tool-plugin',
description: 'Run a read-only SQL query.',
parameters: {
sql: { type: 'string', required: true, description: 'SQL query' },
},
output: {
schema: { type: 'array', items: { type: 'object' } },
render: (_args, rows) => [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
},
async execute(args) {
if (!pool) throw new Error('Database pool not available')
return await pool.query(args.sql)
},
}))
}
- 本插件的后续步骤...省略一万字,懂思路就好。
ctx.effect的返回函数在以下场景自动执行:
- 插件被手动 dispose
- 依赖的服务消失(provider 热替换)
- HMR 触发配置变更
- 整个应用关闭
你不需要自己跟踪"什么时候该关连接"。
Step5 打包为可安装的 Bundle
Bundle 文件结构
hello-dsh-plugin/
├── package.json # 声明 dsh.bundle
├── cordis.patch.yml # 该 bundle 贡献的配置层
└── src/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" }
}
}
dsh.bundle声明告诉dsh plugin命令:这是一个可安装的组合包。
index.js
import type { Context } from '@deepseek-ai/cordis'
// 插件唯一标识
export const name = 'hello-dsh-plugin'
// 声明依赖:需要 tools 服务才能注册工具
export const inject = ['tools']
export function apply(ctx: Context) {
console.log('[hello-dsh-plugin] ✅ 插件加载完成')
// ctx.effect:登记可逆副作用,插件卸载/热重载自动执行清理
ctx.effect(() => {
// 注册一个可供Agent调用的工具
const toolDef = {
name: "hello-dsh",
description: "Greet someone by name.",
parameters: {
type: "object",
properties: {
name: {
type: "string",
description: "The name to greet",
required: true
}
}
},
async execute(args: { name: string }) {
return {
success: true,
output: `[hello-dsh-plugin] ${args.text}`
}
}
}
ctx.tools.register(toolDef)
// 清理函数:插件卸载时执行
return () => {
ctx.tools.unregister("hello-dsh")
console.log('[hello-dsh-plugin] 🗑️ 插件已卸载,工具注销')
}
})
}
/** 或:
import type { Context } from '@deepseek-ai/cordis'
// 插件唯一标识
export const name = 'demo-dsh-plugin'
// 声明依赖:需要 tools 服务才能注册工具
export const inject = ['tools']
export function apply(ctx) {
ctx.tools.register(defineTool({
name: 'hello-dsh-plugin',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
**/
cordis.patch.yml
- insert:
- id: hello-dsh-plugin
# 这里的 `name` 对应 package.json 中的 `name`
name: dsh-hello-plugin
注意: 这里的
name对应 package.json 中的name。
注意:这里用包名而非【文件路径】 ——Node 的模块解析会在已安装依赖中找到它。
安装到 Profile
- 安装到指定的 profile
//若已全局安装 dsh:
dsh plugin --profile demo add ./hello-dsh-plugin
//若未全局安装、且在dsh项目的源码仓库下:
pnpm dsh plugin --profile demo add ./hello-dsh-plugin

- 命令解释:
dsh plugin:插件管理子命令;所有子命令都必须绑定某一个 profile。--profile demo:选定操作对象是名为demo的 profile。- 如果
demoprofile 不存在,自动初始化;初始化会内置基础 bundle@deepseek‑ai/dsh‑baseGitHub。 - 后续 add/remove/update 全部作用于
~/.dsh/profiles/demo,不会碰 web、headless 以及其他 profile。
- 如果
add ./hello‑dsh-plugin:底层等价于在 demo profile 目录内部执行pnpm add ./hello‑plugin。./hello‑dsh-plugin是本地插件目录,相对路径以你执行命令的当前终端目录解析,不是 profile 目录。- 把 hello‑dsh-plugin 安装到 demo profile 自己的
node_modules。 - 如果插件
package.json带有dsh.bundle字段:自动把这个 bundle 加入dsh.profile.bundles数组,启动时自动加载插件;没有该字段仅作为普通 npm 依赖安装,不会自动进插件树。
- 首次使用会初始化 profile(自动带上
@deepseek-ai/dsh-base作为基础层),pnpm链接该包,并将其追加到dsh.profile.bundles。
- 验证方法:
dsh --profile demo --dump-config # 可以看到 hello 层
dsh --profile demo # 启动并观察 tool 可用

- 对比:
dsh plugin --profile demo add xxxvs.pnpm dsh plugin --profile demo add xxx
pnpm dsh= 调用仓库内apps/cli/src/bin.ts,源码 TS 直接跑,不依赖全局安装dsh plugin add要求目标目录是合法npm包(存在package.json);裸 JS 文件只能用--patch。add后面的路径:是执行命令时终端视角的路径,【跨目录】建议直接写绝对路径,避免相对路径解析错乱。
| 模式 | 调用方式 | 可用目录 |
|---|---|---|
| npm 全局安装版 | dsh |
任意终端目录 |
| 源码 clone 开发版 | pnpm dsh |
仅限仓库根目录| |
原理解释: profile + cordis机制下,插件的加载顺序
1. profile.bundles 列表中各 bundle(按顺序)
2. profile 的 cordis.patch.yml
3. $DSH_HOME/cordis.patch.yml(机器级)
4. --patch overlay(命令行)
后应用的层按行胜出,且替换整个 config(非深度合并)。
原理解释: 分发方式
| 方式 | 命令 | 说明 |
|---|---|---|
| npm 发布 | dsh plugin add your-package | 预构建,最简单 |
| tarball | dsh plugin add ./pkg-0.1.0.tgz | pnpm pack 打包 |
| GitHub | dsh plugin add github:you/repo | 需要 prepare 脚本构建 |
| 本地开发 | dsh plugin add ./local-dir | pnpm link |
要点提炼 (必读)
- 必须导出:
name、inject、apply(ctx);这三者是插件最小契约。 - 所有副作用(注册工具、事件监听、定时器)必须包裹
ctx.effect(),返回清理函数;插件卸载/热重载自动回滚,避免内存泄漏。 - 禁止硬编码 import DSH 内部业务模块;依赖全部通过
inject+ctx.*获取。 - DSH 不存在根目录 cordis.yml;用户扩展一律走外部 patch 文件 +
--patch参数,不修改仓库受控yaml。
DSH插件的三种形态
1)函数形式(推荐大多数场景)
export const name = 'my-plugin'
export const inject = ['tools']
export function apply(ctx: Context) { /* ... */ }
2)对象形式
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) { /* ... */ },
}
3)类的形式
- 类形式(当你要提供服务时)
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
myService: MyService
}
}
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
}
// 其他插件通过 ctx.myService.doSomething() 调用
doSomething() { /* ... */ }
}
类形式让你的插件成为其他插件的依赖——它们
inject: ['myService']就能在apply中使用ctx.myService。
服务隔离
- 同一个服务可以有多个实例,不同插件组看到不同实例:
- id: coding-agent
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 5000
- name: './my-strict-tool.ts'
- id: research-agent
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 60000
- name: './my-research-tool.ts'
两个组各看到自己的 ctx.shell 实例,超时配置互不影响。这是 Cordis 服务隔离的通用能力——适用于 tools、shell、fs、llm 等任何服务。
事件系统
- 插件间的松耦合通信通过事件:
export const inject = ['tools']
export function apply(ctx: Context) {
// 监听 tool 执行后事件
ctx.on('tools/post-execute', (toolName, result) => {
console.log(`Tool ${toolName} executed, result length: ${result.length}`)
})
// Waterfall 事件必须调 next()
ctx.waterfall('tools/pre-execute', async (toolName, args, next) => {
console.log(`About to execute: ${toolName}`)
return next() // 必须调用,否则短路整个链
})
}
- 事件分三种:
emit:广播通知,监听器不影响流程serial:按注册顺序执行,无 next()waterfall:链式传递,必须调 next() 才往下走,可以在中间拦截或改写
调试技巧
1) 查看最终配置树
pnpm dsh web --dump-config
打印出所有层合并后的完整插件树——确认你的插件是否被正确加载。
2) 检查服务注册
- 在插件中
export function apply(ctx: Context) {
console.log('Available services:', Object.keys(ctx.root))
}
3) HMR 开发循环
- 启动
pnpm dsh web --patch ./scratch-plugin/cordis.yml - 修改
scratch-plugin/src/my-plugin.ts - 保存——观察终端中旧插件卸载 + 新插件加载的日志
- 在 Web UI 中立即使用新行为
不需要重启进程。
Z FAQ for DSH 安装&使用
Q: DSH重点进阶方向
| 方向 | 文档 |
|---|---|
| 能力三层拆分/能力的三种角色设计 | develop/practice |
| LLM 适配器 | develop/practice/llm-adapter |
| 事件系统 | develop/framework/events |
| Cordis 框架教程 | develop/cordis-tutorial |
| Tool 编写参考 | reference/cookbook/adding-a-tool |
Q: dsh 如何查看、添加 skill?(必读)
先分清:Skill ≠ Plugin
- Skill(技能):给 Agent 的"说明书"(Markdown 指令集),告诉它怎么完成某类任务。会话里用 斜杠
/调用,或模型根据描述自动加载。 - Plugin(插件):能力的"零件",工具、技能、界面组件都通过插件装配。
在 设置 → 插件 面板管理。
- 综上,"添加 skill"有两条路:直接放 Markdown 文件(官方机制,最轻),或安装带技能/工具的插件(1条命令)。
查看已有 Skill
| 方式 | 操作 |
|---|---|
| 会话内 | 在输入框敲 /,弹出指令、技能列表(推荐) |
| Web UI | 设置 → 插件 面板,查看已安装/启用的技能插件(发布时内置 159 个插件,含技能类) |
| API | POST /api/skill.list {"sessionId":"..."}(实测返回仓库 .agents/skills/ 下全部技能) |
| 文件系统 | 直接看技能目录(下表),默认 11 个 dsh-* 技能来自仓库自带 .agents/skills/ |
技能目录(按优先级从高到低):
| 目录 | 生效范围 |
|---|---|
<项目根>/.dsh/skills/ |
项目专属,最高优先 |
<项目根>/.agents/skills/ |
项目共享 |
~/.dsh/skills/(即 C:\Users\Johnny\.dsh\skills\) |
个人全局 |
~/.agents/skills/ |
全局共享 |
添加 Skill
方法 A:写 Markdown(推荐,免写代码、热更新)
- 新建文件,两种格式皆可:
<技能名>.md(扁平式)或<技能名>/SKILL.md(目录式)。 - frontmatter 必填
name+description,可选whenToUse、disable-model-invocation(仅允许手动/触发)、user-invocable。 - 保存即自动热更新,无需重启。
示例(个人全局,Windows):
---
name: dsh-code-review
description: Use when reviewing a pull request or doing code review
whenToUse: 用户要求审查代码/PR
---
# 代码审查
1. 先读 diff 与相关上下文,再逐文件审查
2. 按 正确性 / 可读性 / 安全隐患 分类输出问题
3. 每条建议给出具体修改示例
保存到 C:\Users\xxx\.dsh\skills\dsh-code-review.md 后,新会话里敲 /dsh-code-review 即可触发;或在任务描述中让模型匹配到该技能自动加载。
方法 B:安装技能/工具插件(一条命令)
# 全局安装 dsh 后(否则用 npx @deepseek-ai/dsh 前缀)
dsh plugin --profile web add @dsh-external/dsh-vision-toolkit
# 一键装多个
dsh plugin add dsh-password dsh-http dsh-case dsh-fmt
装完刷新 Web 界面,到 设置 → 插件 面板启用。社区也有 dsh-plugin-hub(500+ 插件 / 300+ skills)做一键启用/停用。
踩坑说明(实测结论)
- minimal 预设模式下,不带技能插件,只有
standard(及自建预设)挂载了 skill 加载插件; - 项目根按
.git目录识别,非 git 目录不会被当项目根; - frontmatter 缺
name/description的文件会被直接忽略(日志有警告); - 技能目录描述截断 500 字符(正文不限);
- 与 opencode 不同:dsh 的 AGENTS.md / skills 全部以用户角色消息注入(
<available_skills>块),不是塞进系统提示词,感知强度不同; - 第三方技能/插件记得审查权限声明(社区出过"插件误删 400G"事件)。
Q: DSH 项目中 cordis 插件体系的 fiber 到底是指什么?插件、Fiber、Context的关系?Fibe=一个已加载插件实例的生命周期与资源所有权单元 / Fiber 伴随对应的插件从生到死的全部过程(必读)
fiber: 【英译】光纤、纤维- 在 deepseek-harness(DSH)里,Cordis 的 Fiber 不是并发编程里那种"纤程/协程",而是"一个已加载插件实例的生命周期与资源所有权单元"——每次
ctx.plugin(...)挂载一个插件,Cordis 就为它创建一根Fiber,这根Fiber伴随该插件从生到死的全部过程。
特别注意:Fiber 对应的是"插件被挂载(Mount)"这一生命周期事件,而不是"插件被调用(Invoke)"这一次请求事件。
- 挂载(创建 Fiber):当你写下 `ctx.plugin(MyPlugin, config)`` 时,Cordis 会创建一个 Fiber。这相当于把插件"安装"到系统中,分配资源,注册事件监听、工具、服务等。这个 Fiber 会一直存活,直到被显式卸载或父 Context 销毁。
- 调用(执行业务逻辑):当用户在对话中触发了某个功能(比如调用了一个 Tool,或者 Agent Loop 开始跑),这是在执行那个已经挂载好的插件所提供的函数/服务。这就像调用一个普通的对象方法,不会创建新的 Fiber。
Fiber 在 DSH 里的准确定位
- DSH 把 Cordis vendor 进了
@deepseek-ai/cordis,整个 harness 建立在"Cordis 基于Fiber的系统"之上:每个插件实例都是一个 fiber,管理自己的资源和 effects。
三个概念的区别要先分清:
- Plugin = 插件的定义(剧本),一份代码
- Fiber = 这份定义被加载后"活起来"的实例(按剧本开起来的那家店),一个
Plugin可以被多次挂载,从而产生多个Fiber- Context = Fiber 所处的那个"世界/容器",插件通过它访问服务、注册事件
所以,Fiber 解决的核心问题是:插件不仅要【能安装】上,还要能干净地【卸载掉】,并且能在依赖变化时【自动重建】。
Fiber 内部记了什么
每根 Fiber 都携带一组核心状态:
| 字段 | 含义 |
|---|---|
uid |
在注册表中的唯一 ID,根 Fiber 为 0;Provider 的 uid 会进入 Consumer 的 dependency epoch |
ctx |
该插件运行的(从父级派生的)Context |
config |
经过校验的插件配置 |
state |
生命周期状态(见下文六态) |
store |
加载期间所需服务实现的快照(当前这一代依赖到底绑定了谁) |
inertia |
正在进行的加载/卸载操作,防止 HMR 期间生命周期切换踩踏 |
其中 store 是"当前 activation 实际捕获的依赖快照",而 _store 是"resolver 当前能找到的候选 Provider"——这两个区分很关键:Provider 可以在插件执行期间换代,但当前 activation 必须有一个稳定 snapshot,不能一边执行一边把依赖悄悄换成另一代。
Fiber 的六态状态机
Fiber 在六个状态间流转:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
- PENDING:声明了 inject 依赖,但依赖服务还没就绪("我的插件为啥没输出"十有八九卡在这)
- LOADING:正在执行插件 apply 回调
- ACTIVE:加载完成,正常运行
- FAILED:apply 抛异常或配置校验不过
- UNLOADING:disposers 正在逆序执行
- DISPOSED:已彻底释放,不可重启
真正驱动状态切换的:epoch 依赖指纹
这是 Fiber 最精妙的地方。表面是状态机,底层是 epoch 驱动:
Fiber 把它 inject 的每个服务的 Provider 的
uid拼成一个 epoch 字符串,作为"当前绑定的是哪一代依赖组合"的身份指纹。
举例:一个 AgentLoop Fiber 依赖 ctx.llm(Fiber #12)和 ctx.tools(Fiber #25),它的 epoch ≈ :12:25。
- 当
ctx.llm因 HMR / 配置更新被换成 Fiber #37,epoch 变成:37:25 - Cordis 发现 epoch 变了 → 自动 UNLOADING → 清理旧 effects → 用新依赖重新 LOADING → ACTIVE
也就是说:被依赖的 Provider 换代,Consumer Fiber 会自动重建,插件作者不用手写任何重连逻辑。这是 DSH 能做到"运行中改写自己"的底层机制。
为什么 DSH 离不开 Fiber:effect ownership
Fiber 最关键的责任是资源归属与回收。Cordis 里绝大多数注册 API 都自动归属调用方 Fiber:
ctx.on(...)注册的事件监听ctx.plugin(...)挂载的子插件ctx.provide(...)/ 服务注册ctx.tools.register(...)工具注册- Prompt section / Adapter / accessor / Timer / watcher
这些都会作为 effect 被收集进 Fiber 的 disposables 列表。Fiber 卸载时,所有 disposer 按注册的逆序执行(后注册的先清理,天然满足依赖的栈式语义)。
对于 Cordis 不认识的外部资源(裸 setInterval、外部连接、文件 watcher 等),插件作者用 ctx.effect(() => { ...; return () => cleanup() }) 手动登记即可。
DSH 对这块做了加固(Fiber Lifecycle Hardening):修复了 cordis/src/fiber.ts 中可重入销毁的漏洞,Fiber 在 UNLOADING 时拒绝新的 effect 注册,同步 setup 失败会立即回滚;Fiber.update() 改为返回 waterfall 结果,调用方可以 await 重启过程。
这就是为什么 DSH 敢说"everything is a plugin":Agent Loop、LLM 适配器、工具注册表、会话管理、沙箱……全是插件,全都有 Fiber 兜着生命周期。父 Agent 销毁时,子 Agent 与后台 Job 通过 Fiber 的归属链统一收敛。
精炼总结
Fiber = 插件实例 + 生命周期 + 资源所有权的统一边界。它让 DSH 的每一个能力单元都做到:
- 空间可组合:同一插件在不同 Context 中并存,互不干扰(每个 Agent 实例通过 scoped context 拿到独立 Fiber,工具/监听器不串场)
- 时间可组合:插件可装载、热更新、卸载,且副作用完全可逆
所以当你在 DSH 代码里看到
fiber.dispose()、fiber.restart()、fiber.update(config),本质上就是在操作"这根 Fiber 所代表的那个活着的插件实例"的生命周期——而不是在操作线程、协程或普通的 JS 对象。
Y 推荐文献
- DeepSeek-Hardness
浙公网安备 33010602011771号