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 是一个编译时类型辅助函数,不是运行时必需品。它做两件事:
- 把简写格式转成标准 JSON Schema
- 提供 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() | 给模型暴露可调用的工具 |
| Hook | ctx.on('tools/pre-execute', ...) | 拦截/审批工具执行 |
| Service | extends Service | 给其他插件提供新能力(ctx.xxx) |
| 提示词 | ctx.systemPrompt.section() | 往系统提示词注入内容 |
| UI | ctx.slots.register() | 往界面插槽添加组件 |
| LLM 适配器 | ctx.llm.registerAdapter() | 接入新的模型 |
| 事件监听 | ctx.on('xxx', ...) | 观察系统行为 |
| MCP | ctx.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,找不到工作区内的包。
解决方案:
- 将插件移到
deepseek-harness目录内 - 手动创建符号链接:
scratch-plugin/node_modules/@deepseek-ai/dsh-tools→ 全局 dsh 安装中的构建产物 - 使用
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 集成是两层:
dsh-runtime包:一个真正的 dsh Bundle,通过dsh plugin --profile open-design add安装- 主应用:独立的桌面应用,通过 stdio JSON 帧协议驱动 dsh 子进程
使用方式:open-design 作为入口,启动 dsh 作为子进程,通过自定义协议通信。用户不会直接运行 dsh --profile open-design。
判断一个项目是否是真正的 dsh 插件:检查是否有 @deepseek-ai/cordis 依赖 + 导出 apply(ctx) 函数。