DSH 快速参考手册

DSH 学习笔记

基于我的理解路径整理,便于日后回顾时快速唤起记忆。


1. dsh 的本质:一个 Cordis 插件组合器

dsh 不是一个独立的框架,它是 Cordis 的"发行版"。

我最初的理解(正确):

  • Cordis = Linux 内核(提供插件生命周期、IoC 容器、事件系统)
  • dsh = Ubuntu(在 Cordis 之上预装了一套 agent 系统)
  • dsh 的插件 = Cordis 的插件,没有区别

关键认知:dsh 没有发明新的插件格式。你写的任何 Cordis 插件都能在 dsh 中运行,反之亦然。dsh 只是用 Cordis 的机制组装了一套 agent 系统(LLM、工具、会话、agent 循环等)。

dsh 的 C/S 架构

我最初困惑于"谁是入口"。后来理解了:dsh 和 Codex 一样,是 C/S 架构。

dsh 内核(agent 引擎,Cordis 插件树)
├── 客户端 1: dsh web           → 浏览器 GUI(自己提供的)
├── 客户端 2: dsh headless      → 命令行(自己提供的)
├── 客户端 3: ACP server        → 外部程序通过协议调用
├── 客户端 4: JSON-RPC server   → 外部程序通过协议调用
└── 客户端 5: open-design       → 外部程序通过 stdio 协议调用

谁是入口取决于你想怎么用:

  • 直接用 dsh → dsh web 或 dsh headless 是入口
  • 通过 open-design 用 → open-design 启动 dsh 子进程
  • 通过 Python SDK 用 → SDK 启动 dsh 子进程

open-design 和 dsh 的关系是双向的:

  • open-design 可以驱动 dsh(open-design 作为客户端,通过 stdio 协议)
  • dsh 可以调用 open-design(通过 MCP 协议,open-design 作为 MCP server)

当前主流 agent 工具(dsh、Claude Code、Codex、Cursor)都在往这个方向走,MCP 协议是核心标准。


2. Profile / Bundle / Patch — 我的理解过程

我最初以为 patch 就是插件。后来发现不对,这里有三个层次:

Bundle  = 一个 npm 包 = 1 个 Patch(YAML 配置)+ 代码 + package.json
Patch   = 一个 YAML 文件,只声明"加载什么插件",不含代码
Profile = 多个 Bundle 的有序叠加 + 自己的 Patch + 环境 Patch

Patch 和代码的关系:YAML 文件只是"指针",指向代码文件或包名。我之前创建的 scratch-plugin/ 目录(包含 cordis.yml + src/myplugin.ts)其实是一个"未打包的 Bundle"——有配置也有代码,只是缺 package.json。

Bundle 不是多个 Patch 的集合,而是 1 个 Patch + 代码打包在一起。Profile 才是多个 Bundle 的集合。

配置加载顺序

后者覆盖前者:

Bundle 1 的 patch(dsh-base)
  ↓
Bundle 2 的 patch(dsh-web-app)
  ↓
Profile 自己的 cordis.patch.yml
  ↓
$DSH_HOME/cordis.patch.yml
  ↓
--patch 命令行 overlay

官方 Profile

只有 2 个:web(浏览器 GUI)和 headless(命令行一次性运行)。


3. 插件开发 — 不需要 clone 仓库

我最初必须 clone 整个 deepseek-harness 仓库才能开发插件,这很麻烦。后来发现有更好的方式:

最简方式:原始 JSON Schema + –patch

// my-plugin.ts — 放在任意目录
import type { Context } from '@deepseek-ai/cordis'  // type-only,运行时消失

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'The name to greet' },
      },
      required: ['name'],
    },
    async execute(args: { name: string }) {
      return `Hello, ${args.name}!`
    },
  })
}
# my-patch.yml
- insert:
    - id: greet-tool
      name: '/absolute/path/to/my-plugin.ts'
dsh web --patch ./my-patch.yml

为什么能这样:

  • import type 运行时被擦除,不触发模块解析,所以不需要 @deepseek-ai/dsh-tools 包
  • ctx.tools.register() 接受原始 JSON Schema(MCP 工具就是这样注册的),不需要 defineTool
  • 不需要 package.json,不需要 node_modules

我踩过的坑:最初用 defineTool 的简写格式直接传给 ctx.tools.register(),报错 schema must be a JSON Schema of 'type: "object"', got 'type: null'。原因是 defineTool 接受简写格式({ name: { type: 'string', required: true } }),但原始注册需要标准 JSON Schema({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] })。

defineTool 是什么

defineTool 是一个编译时类型辅助函数,不是运行时必需品。它做两件事:

  1. 把简写格式转成标准 JSON Schema
  2. 提供 TypeScript 类型推导(args 有完整类型)

运行时效果和手写 JSON Schema 完全一样。


4. TypeScript 结构化类型 — 给 Java 程序员的对照

我是 Java 程序员,最初不理解为什么能"随意改"类型定义。

核心区别:

  • Java:名义类型,必须 implements 接口
  • TypeScript:结构化类型(类似 Go 的隐式接口),结构匹配就行
interface Person { name: string; age: number }
function greet(p: Person) { ... }

// 以下全部合法,不需要 implements:
greet({ name: 'Alice', age: 30 })           // 对象字面量
const dog = { name: 'Rex', age: 5, breed: 'Husky' }
greet(dog)                                   // 多了字段也行

在 dsh 中的应用:ctx.tools.register() 接受一个类型参数,我传一个结构匹配的对象进去,编译器就放行。不需要通过 defineTool 创建,不需要 implements 任何接口。这就是为什么我可以用原始 JSON Schema 替代 defineTool。

所有对象都是这样,这是 TypeScript 的语言规则,不是某个 API 的特殊设计。


5. 插件类型 — 我能扩展什么

除了最基础的工具插件(ctx.tools.register),还有:

类型核心 API作用
工具ctx.tools.register()给模型暴露可调用的工具
Hookctx.on('tools/pre-execute', ...)拦截/审批工具执行
Serviceextends Service给其他插件提供新能力(ctx.xxx)
提示词ctx.systemPrompt.section()往系统提示词注入内容
UIctx.slots.register()往界面插槽添加组件
LLM 适配器ctx.llm.registerAdapter()接入新的模型
事件监听ctx.on('xxx', ...)观察系统行为
MCPctx.tools.register()桥接 MCP server 的工具

6. UI 插件 — Slot 系统

我最初以为 UI 插件是通过 XPath 找到 HTML 位置然后注入代码。完全不是。

dsh 的 Web UI 是 React 应用,用 Slot(插槽)系统管理扩展点。

工作方式:

Session 事件流 → 插件匹配事件 → 构建状态 → 生成视图数据 → React 组件渲染

插件不修改已有的 HTML,而是往 Slot 里添加新组件:

ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
  name: 'conversation.chat.node',
  key: 'my-thing',
}, MyComponent))

Slot 的限制:

  • ✅ 能添加新组件
  • ✅ 能修改组件收到的数据(通过事件拦截)
  • ✅ 能改 CSS(注入 style)
  • ❌ 不能修改已有组件的渲染逻辑(要 fork 源码)
  • ❌ 不能移除已有组件(要 fork 源码)

没有 Slot 的地方:不能通过插件修改,只能 fork 对应的包。

已有的 Slot 大约 25 个,覆盖对话流、输入区、侧边栏、设置页等关键区域。


7. 遇到的问题和解决方案

问题 1:Cannot find package '@deepseek-ai/dsh-tools'

原因:全局安装的 dsh 从插件文件目录向上查找 node_modules,找不到工作区内的包。

解决方案:

  1. 将插件移到 deepseek-harness 目录内
  2. 手动创建符号链接:scratch-plugin/node_modules/@deepseek-ai/dsh-tools → 全局 dsh 安装中的构建产物
  3. 使用 pnpm dsh 而不是全局 dsh(但需要 pnpm install 完成)

最终方案:不用 defineTool,用原始 JSON Schema,彻底避免依赖 @deepseek-ai/dsh-tools。

问题 2:dsh web 启动成功但对话报 schema 错误

原因:把 defineTool 的简写格式直接传给了 ctx.tools.register(),不是合法的 JSON Schema。

解决:改成标准 JSON Schema 格式(type: 'object' + properties + required 数组)。


8. 关于 open-design

open-design 在 GitHub 的 dsh-plugin topic 下,但它不是传统意义上的 dsh 插件(不是运行在 dsh 进程内部的 Cordis 插件)。

它的 dsh 集成是两层:

  1. dsh-runtime 包:一个真正的 dsh Bundle,通过 dsh plugin --profile open-design add 安装
  2. 主应用:独立的桌面应用,通过 stdio JSON 帧协议驱动 dsh 子进程

使用方式:open-design 作为入口,启动 dsh 作为子进程,通过自定义协议通信。用户不会直接运行 dsh --profile open-design。

判断一个项目是否是真正的 dsh 插件:检查是否有 @deepseek-ai/cordis 依赖 + 导出 apply(ctx) 函数。