[AI/Agent/DSH] DeepSeek Harness 使用指南

0 序

image

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:放开限制

常见踩坑

  1. Node.js 版本过低(需 22.19+ / 24+)会报错;
  2. pnpm 未启用 corepack ,导致版本不一致;
  3. 沙箱默认 workspace-write,操作工作区外文件会提示 [sandbox: file access denied](策略行为,非 bug);
  4. Windows 上 bash 链自动禁用改用 pwsh,如需 bash 需在 profile patch 中同时禁用 pwsh 行并启用 bash 行;
  5. 同时挂载 dsh-fs-localdsh-fs-sandbox 会因 ctx.fs 重复注册而加载失败(框架 fail-loud,不静默降级);
  6. 预览版会话格式不向后兼容,升级后旧日志可能无法加载。

2 使用指南

主页

image

image

添加工作区

image

调用 Skill (必读)

  • 调用、查看 Skill:键入/即可查看、调用 DSH 加载的 Skill
  • 支持从 ~/.agents/skills/ 目录(跨多 AI Agent 的Skill目录) 识别Skill
  • 添加 Skills
  • 方法1:添加全局的、跨AI Agents的 Skill,可在 ~/.agents/skills/ 目录下添加。
  • 推荐文献

执行用户任务(必读)

  • 例如: 用户任务提示词中,调用用户指定的Skill,输出文件报告。

image

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

image

  • 下载的会话日志

image

设置

通用设置

  • 权限、语言、外观、字号大小、对话显示、繁忙时Enter键的行为

image

  • 配置文件 settings.yaml
ui-onboarding:
  welcomeNoticeVersion: 2026-08-13.1

模型(必读)

创建自定义的模型提供方(硅基流动为例)

  • 创建自定义的模型提供方

以硅基流动的deepseek-ai/DeepSeek-V4-Pro模型为例:

image

  • 创建完成后:

image

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

image

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

image

插件

插件配置

image

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

插件列表

image

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

image

  • 全局插件:系统与所有会话共用

Agent 预设模式x4种:标准模式 / PTC模式 / 极简模式 / 创造模式

image

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

image

  • 插件的本质

在 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! 即为成功。

image

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

image

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.toolsprovider被热替换,本插件自动dispose并重新apply`。
  • defineTool DSL:
字段 作用
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

image

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

image

总结/分析: 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
  • 再运行,查看效果

image

设计原则

  • 无硬编码可调参数:凡是不同部署可能需要不同值的参数,必须定义为 Config 字段。

检验标准:能否在 cordis.yml 中改变这个值而不修改代码?

  • 配置错误要响亮:Schema 在插件加载时执行校验。

不合法配置会让插件加载失败(fiber → FAILED),给出明确错误信息,而非运行时静默行为异常。

HMR 行为

  • 修改 cordis.yml 中的 config 字段后,Cordis 会:
  1. 卸载旧 fiberctx.tools.registerdisposer 自动执行,tool 注销)
  2. 创建新 fiber
  3. 用新 configapply(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)
    },
  }))
}

https://www.xjavascript.com/blog/pg-pool-typescript/

  • 本插件的后续步骤...省略一万字,懂思路就好。
  • 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

image

  • 命令解释:
  1. dsh plugin插件管理子命令;所有子命令都必须绑定某一个 profile
  2. --profile demo选定操作对象是名为demo的 profile
    • 如果demo profile 不存在,自动初始化;初始化会内置基础 bundle @deepseek‑ai/dsh‑baseGitHub。
    • 后续 add/remove/update 全部作用于 ~/.dsh/profiles/demo不会碰 web、headless 以及其他 profile
  3. 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 可用

image

  • 对比: dsh plugin --profile demo add xxx vs. 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

要点提炼 (必读)

  1. 必须导出:nameinjectapply(ctx);这三者是插件最小契约。
  2. 所有副作用(注册工具、事件监听、定时器)必须包裹 ctx.effect(),返回清理函数;插件卸载/热重载自动回滚,避免内存泄漏。
  3. 禁止硬编码 import DSH 内部业务模块;依赖全部通过 inject + ctx.* 获取。
  4. 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 开发循环

  1. 启动 pnpm dsh web --patch ./scratch-plugin/cordis.yml
  2. 修改 scratch-plugin/src/my-plugin.ts
  3. 保存——观察终端中旧插件卸载 + 新插件加载的日志
  4. 在 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(推荐,免写代码、热更新)

  1. 新建文件,两种格式皆可:<技能名>.md(扁平式)或 <技能名>/SKILL.md(目录式)。
  2. frontmatter 必填 name + description,可选 whenToUsedisable-model-invocation(仅允许手动 / 触发)、user-invocable
  3. 保存即自动热更新,无需重启

示例(个人全局,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

X 参考文献

posted @ 2026-09-01 15:28  数据知音  阅读(36)  评论(0)    收藏  举报