DeepSeek Harness 插件开发进阶:从 Host 工具到 Client UI
一、先理解 DSH 的插件模型
DSH 的插件运行时基于 Cordis。一个插件本质上是一个模块,它导出几样东西:
export const name = 'my-plugin' // 插件名
export const inject = ['tools'] // 依赖哪些服务
export const Config = Schema.object({})// 配置 schema(可选)
export function apply(ctx, config) {} // 插件主体
inject 声明依赖的服务;Cordis 会在这些服务可用时调用 apply,并在插件卸载或依赖消失时释放它注册的资源。Config 用于校验 profile 配置行里的 config。
1.1 Profile、Bundle 与 Patch
DSH 的运行时组合是分层的:
DSH 进程
└─ profile(desktop / web / headless / ...)
├─ package.json
│ └─ dsh.profile.bundles: [bundle A, bundle B, ...]
├─ cordis.patch.yml # 用户自己的覆盖层
└─ node_modules/
├─ @deepseek-ai/dsh-base # 基础 bundle
├─ @deepseek-ai/dsh-web-app # Web 形态 bundle
└─ my-plugin # 你的插件
├─ cordis.patch.yml # 插件要插入的配置行
├─ lib/index.js # Host 侧入口
└─ lib/client.js # Client 侧入口(可选)
- Bundle:一组配置行的集合,通常是一个 npm 包,通过
dsh.bundle.patch指向自己的cordis.patch.yml。 - Patch:按
id定位配置行,可以覆盖、禁用或插入。注意,patch 会替换目标行的整个config,不是深合并;想保留的字段要重写。 - Profile:把若干 bundle 按顺序叠加,再叠加用户自己的
cordis.patch.yml和--patch覆盖层。
加载顺序大致是:bundle 的 patch → profile 的 cordis.patch.yml → home 级 patch → --patch。后写的层覆盖先写的层。
1.2 Host 侧与 Client 侧
一个插件可以只有一个 Host 侧,也可以同时有 Host 和 Client 两侧:
| Host 侧 | Client 侧 | |
|---|---|---|
| 运行环境 | Node.js | 浏览器页面 |
| 入口 | main / exports["."] | exports["./client"] |
| 能做什么 | 工具、命令、服务、会话投影、模型提供方、文件/网络访问 | 页面、面板、设置卡片、聊天区域扩展 |
| 不能做什么 | 不能直接画 UI | 不能直接读文件、跑进程、访问 Node API |
| 依赖声明 | peerDependencies | dsh.client.inject |
两侧通过远程调用(Typert)通信。不要为了省事把 Host 的文件读取逻辑塞进 Client;浏览器侧拿不到 Node 能力。
1.3 常见扩展点
| 扩展点 | 服务 | 典型用途 |
|---|---|---|
| 工具 | ctx.tools | 让模型调用新能力 |
| 斜杠命令 | ctx.commands | 用户直接触发、不经过模型 |
| UI 插槽 | ctx.slots | 新增页面、面板、标签页、设置卡片 |
| 会话投影 | ctx.sessionProjections | 把事件流投影成 UI 可读状态 |
| 设置表单 | ctx.configForms | 插件详情页的配置卡片 |
| 远程调用 | ctx.typert | Host 与 Client 之间的类型化 RPC |
| 模型提供方 | ctx.llm | 注册新的模型路由(进阶) |
二、准备开发环境
2.1 前置条件
- Node.js
^22.19.0或>=24.0.0。 - pnpm。
- 一个可运行的 DSH。开发阶段建议用
webprofile,而不是桌面端持有的desktopprofile。 - TypeScript 和 React 基础。Host 侧可以不用 React,Client 侧需要。
node --version
pnpm --version
dsh --version
2.2 版本对齐
插件会 import DSH 的包,例如 @deepseek-ai/cordis、@deepseek-ai/dsh-tools、@deepseek-ai/schemastery。这些包在运行时由 DSH profile 提供,在开发时作为 peerDependencies / devDependencies 安装,用来做类型检查。
原则:
- 插件的 peer 范围和当前 DSH 版本对齐。
- 不要在一个插件里捆绑另一份
@deepseek-ai/cordis,否则可能出现两个服务容器。 - 升级 DSH 后,重新跑类型检查和一次真实安装测试。
2.3 建一个独立目录
不要把插件源码直接放进 ~/.dsh/profiles/web。在项目目录里开发,再通过本地路径安装:
dsh-plugin-hello/
├─ package.json
├─ cordis.patch.yml
├─ README.md
├─ tsconfig.json
├─ tsdown.config.ts
├─ tsdown.client.config.ts
└─ src/
├─ index.ts
└─ client.tsx
三、最小可用:一个 Host 工具插件
这个例子注册一个 hello_greet 工具、一个 /hello 命令,并在会话里增加一个标签页。先完成 Host 侧,再补 Client 侧。
3.1 package.json
{
"name": "dsh-plugin-hello",
"version": "0.1.0",
"description": "A minimal DeepSeek Harness plugin with one tool, one command and one conversation tab.",
"type": "module",
"main": "lib/index.js",
"exports": {
".": "./lib/index.js",
"./client": "./lib/client.js",
"./package.json": "./package.json"
},
"files": [
"lib",
"cordis.patch.yml",
"README.md"
],
"keywords": [
"dsh-plugin",
"deepseek-harness",
"dsh"
],
"license": "MIT",
"engines": {
"node": "^22.19.0 || >=24.0.0",
"dsh": ">=0.1.5-rc.3"
},
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
},
"client": {
"inject": [
"@deepseek-ai/dsh-client-ui-conversation"
],
"platform": "web"
}
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.2",
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.3",
"@deepseek-ai/schemastery": "^3.18.2",
"react": "^18.3.1"
},
"devDependencies": {
"@deepseek-ai/cordis": "^4.0.2",
"@deepseek-ai/dsh-tools": "0.1.5-rc.3",
"@deepseek-ai/schemastery": "^3.18.2",
"@types/node": "^22.9.0",
"@types/react": "^18.3.31",
"react": "^18.3.1",
"tsdown": "^0.22.2",
"typescript": "^5.6.3"
},
"scripts": {
"build": "tsdown && tsdown -c tsdown.client.config.ts",
"typecheck": "tsc --noEmit"
}
}
几个字段的作用:
dsh.bundle.patch:告诉 DSH 这个包的组合补丁在哪里。dsh.client:声明存在 Client 侧入口;inject是客户端依赖的模块,platform目前是web。exports["./client"]:Client 侧的加载入口,文件名和构建产物要一致。keywords:dsh-plugin和deepseek-harness是插件市场发现插件的关键词。engines.dsh:声明测试过的 DSH 范围,便于用户判断兼容性。
3.2 cordis.patch.yml
- insert:
- id: hello
name: 'dsh-plugin-hello'
config:
greeting: 你好
insert 会向当前 profile 插入一行插件配置。id 在同一 profile 中应唯一;name 是包名;config 会传给 apply 的第二个参数,并先经过 Config 校验。
3.3 Host 入口
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import Schema from '@deepseek-ai/schemastery'
export const name = 'dsh-plugin-hello'
export const inject = ['tools', 'commands']
export const Config = Schema.object({
greeting: Schema.string().default('你好'),
})
export function apply(ctx: Context, config: { greeting: string }) {
ctx.tools.register(defineTool({
name: 'hello_greet',
description: 'Return a short greeting for a person. Use when the user asks for a greeting demo.',
parameters: {
name: {
type: 'string',
required: true,
description: 'Name to greet',
},
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args, exec) {
exec.signal.throwIfAborted()
return `${config.greeting},${args.name}!`
},
}))
ctx.commands.register({
name: 'hello',
description: 'Print a greeting without calling the model',
input: { hint: '<name>' },
handler: ({ rawInput }) => ({
kind: 'success',
text: `${config.greeting},${rawInput.trim() || '世界'}!`,
}),
})
}
逐段说明:
name:插件标识,和包名保持一致最省心。inject:声明依赖tools和commands两个服务。缺少这些服务的 profile 会一直等待依赖,apply不会执行;如果只是想在无头环境使用工具,可以去掉commands依赖。Config:Cordis 用它校验配置行。greeting有默认值,所以 patch 里不写也能启动。apply:插件主体。ctx.tools.register和ctx.commands.register注册的资源会随插件卸载自动释放。defineTool:声明工具名、描述、参数 schema、输出 schema 和execute。参数在执行前会被校验,非法输入变成普通的工具错误,而不是让整轮对话崩溃。output.render:把返回值渲染成模型可读的内容块。这里返回纯文本,所以用text块。exec.signal:调用方的取消信号。工具应尽量在耗时操作前检查它,并把信号传给fetch、readFile等支持取消的 API。ctx.commands.register:注册/hello。命令直接对 Agent 执行,不会产生模型消息;handler返回success或error。
不要小看工具的 description。模型是根据名称、描述和参数 schema 选择工具的;描述写清楚「什么时候用、返回什么」,比堆很多同义工具更有效。
3.4 类型检查与构建
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": [
"src",
"tsdown.config.ts",
"tsdown.client.config.ts"
]
}
Host 构建配置 tsdown.config.ts:
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
outDir: 'lib',
dts: true,
external: [/^@deepseek-ai\//],
})
Client 构建配置 tsdown.client.config.ts:
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/client.tsx'],
format: ['esm'],
outDir: 'lib',
clean: false,
external: [
'react',
'react-dom',
'react/jsx-runtime',
/^@deepseek-ai\//,
],
})
客户端构建使用 clean: false,因为两个构建共用 lib/;否则第二次构建可能清掉 Host 产物。
安装依赖并构建:
pnpm install
pnpm typecheck
pnpm build
构建后应得到 lib/index.js 和 lib/client.js。如果只做 Host 侧,可以暂时去掉 dsh.client、exports["./client"] 和 src/client.tsx。
3.5 本地安装并验证
用 web profile 安装本地目录:
dsh plugin --profile web add E:\code\dsh-plugin-hello
dsh plugin --profile web list --depth 0
检查 profile 的组合清单:
Get-Content "$env:USERPROFILE\.dsh\profiles\web\package.json"
确认 dsh.profile.bundles 里出现了 dsh-plugin-hello。然后启动 Web:
dsh --profile web --no-open --port 8080
打开终端打印的 URL,在对话里说:
请调用 hello_greet 工具,向张三问好。
也可以直接输入:
/hello 张三
两者的区别:
- 工具调用会经过模型:模型决定是否调用、传什么参数。
/hello是命令:不产生模型消息,直接由 Host 执行并返回结果。
如果工具没有出现,先看配置树是否真的包含插件行:
dsh --profile web --dump-config
再检查 ~/.dsh/logs 和 profile 下的 .plugin-manager/logs。新增 bundle 不是热加载,安装后必须重启 profile。
四、Client 侧:给会话加一个标签页
4.1 客户端入口
import type { Context } from '@deepseek-ai/cordis'
export const name = 'dsh-plugin-hello'
export const inject = ['slots']
export function apply(ctx: Context) {
ctx.slots.inject('conversation.view', () =>
ctx.slots.register(
{
name: 'conversation.view',
id: 'hello',
order: 50,
label: () => 'Hello',
},
() => (
<div style={{ padding: 24 }}>
<h3 style={{ margin: '0 0 8px' }}>Hello from dsh-plugin-hello</h3>
<p style={{ margin: 0, opacity: 0.75 }}>
这个面板由插件的客户端入口注册。
</p>
</div>
),
),
)
}
这段代码做了三件事:
- 声明依赖
slots服务。 ctx.slots.inject('conversation.view', ...)等待宿主注册这个插槽。- 在插槽出现后,用
ctx.slots.register注册一个标签页。id必须唯一,order控制顺序,label是标签文本。
插槽名称、可用参数和生命周期由宿主 UI 决定。不同 DSH 版本可能新增或调整插槽;写插件时应以对应 UI 包的 README/类型为准,不要凭猜测使用内部插槽。
4.2 构建与加载
构建后确认 lib/client.js 存在,然后重启 profile 并刷新页面。Client 侧变更通常不会被已经打开的页面自动拾取;开发时可以让构建进程 watch,然后重启/刷新。
如果你在 DSH 源码仓库内开发,并且同时运行了 pnpm run dev:web,客户端插件可以利用 HMR 重载;第三方插件安装在 profile 里时,不要假设一定有同样的热更新体验,按「重新构建 → 重启 → 刷新」的稳妥流程验证。
4.3 插槽与状态
一个常见的误区是「Client 侧直接读文件、发网络请求」。这会绕过 Host 的权限和审计,也不利于跨端兼容。正确做法是:
- Client 只负责展示和交互。
- 需要数据时,通过 Host 暴露的 Remote 方法获取。
- 需要写入时,同样走 Host 服务,让文件策略和审批生效。
如果只是做一个静态说明页,可以完全不请求 Host;一旦涉及会话、文件、设置或凭据,就应该走服务层。
4.4 插件详情页的配置卡片
如果插件需要图形化配置,可以注入 configForms 服务,并在 plugins.bundle.config 插槽注册卡片。dsh-context 的客户端入口就是一个完整范例:它通过 configForms.whileServed([...]) 等待插件配置可用,然后注册卡片。
这部分依赖较新的设置服务,建议先读 @deepseek-ai/dsh-client-ui-settings 和 @deepseek-ai/dsh-client-ui-settings-plugin-inventory 的 README,再决定是否使用。简单的插件可以先只提供 cordis.patch.yml 配置,等稳定后再做 UI。
五、Host 与 Client 之间:Typert RPC
5.1 为什么需要它
Client 侧运行在浏览器里,不能直接调用 Node 服务。DSH 用 Typert 做类型化远程调用:
- Host 侧声明一个 Remote 服务和方法。
- Typert 生成方法描述和 Zod schema。
- Client 侧通过生成的远程客户端调用,参数和返回值在两端校验。
dsh-plugin-shop 就是一个完整例子:Host 侧有商店服务,Client 侧只能调用它暴露的九个 shop/* 方法,不能直接访问文件系统或网络。这种拆分既是架构边界,也是安全边界。
5.2 Host 侧骨架
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
export class HelloRemote extends TypertRemoteService {
@Remote('ping')
async ping(input: { name: string }) {
return { message: `pong, ${input.name}` }
}
}
要点:
- 继承
TypertRemoteService,用@Remote('方法名')标记可被 Client 调用的方法。 - 方法参数和返回值需要能被 schema 描述;生成器会据此产出两端的类型和校验。
- 生成产物通常叫
typert.host.js和typert.remote-client.js,由@deepseek-ai/dsh-typert-generator生成,不要手改。 - 生成产物通过 loader 注册,或在代码里调用
ctx.typert.register(contribution)。 - 只暴露 Client 真正需要的方法。每多一个方法,就多一个攻击面。
装饰器需要 TypeScript 装饰器配置。不同插件的构建配置不完全一样,建议直接参考 dsh-plugin-shop 的 tsconfig、tsdown 配置和生成脚本,而不是从零猜。
5.3 Client 侧调用
Client 侧 import 生成的远程客户端,然后像调用本地异步方法一样调用:
const result = await hello.ping({ name: 'DSH' })
实际导入路径、服务名和调用方式由生成器决定。关键是:Client 只拿到声明过的方法,拿不到 Host 的任意对象。
5.4 什么时候不需要 RPC
- 纯静态页面、说明卡片:不需要。
- 只读取宿主已经提供的 props 或插槽上下文:不需要。
- 需要访问会话、文件、设置、凭据、后台任务:需要,而且应该走 Host 服务。
不要为了「看起来架构完整」而给每个小组件都套一层 RPC;也不要为了省事把敏感逻辑放到 Client。
六、打包、发布与上架
6.1 发布前检查
package.json的name、version、license、repository完整。files包含lib、cordis.patch.yml、README 和必要的静态资源。exports的.和./client指向真实存在的构建产物。dsh.bundle.patch指向的 YAML 能被解析。peerDependencies与当前 DSH 版本匹配。- 在一个干净的测试 profile 中安装、启动、卸载各一次。
pnpm build
npm publish --access public
如果包名已被占用,改用 scope,例如 @your-name/dsh-plugin-hello,并同步修改 cordis.patch.yml 里的 name。
6.2 插件市场如何发现你
dsh-plugin-shop 会从 npm 和 GitHub 收集带有 dsh-plugin、deepseek-harness 关键词/主题的包。一个包要成为可安装插件,至少要有:
dsh.bundle(否则它只是一个普通库)。- 可核验的许可证和仓库。
- 没有会在安装时失败的生命周期脚本或不合理的
workspace:依赖。
市场做的是机械筛选,不是人工安全审计。即使上架,也不代表代码被逐行审查过。
6.3 版本与兼容性
- 用 semver 管理版本;破坏性改动升 major。
engines.dsh写你实际测试过的范围,不要写「全版本兼容」。- 在 README 中写清楚支持的 DSH 版本、profile、Node 版本和外部依赖(例如 ffmpeg)。
- 如果插件依赖某个 DSH 内部插槽或内部服务,明确标注版本范围;这些接口比公开 API 更容易变。
七、调试与排障
| 现象 | 可能原因 | 处理 |
|---|---|---|
--profile 缺失 | 命令没写 profile | 加上 --profile web 或你的 profile 名 |
pnpm not found | 本机没有 pnpm | npm install -g pnpm |
| 插件不在列表 | 安装未完成,或 bundle 未写入 | dsh plugin --profile web list --depth 0,检查 package.json 的 dsh.profile.bundles |
client bundles not found ... lib/client.js | 没构建 Client 侧,或 exports 路径不对 | 补 src/client.tsx 和客户端构建配置 |
| 工具不出现 | Host 插件未加载,或 inject 写错 | dsh --profile web --dump-config,看日志,重启 profile |
| 标签页不出现 | Client 侧未加载、插槽名不对、id 冲突 | 重启并刷新,查看浏览器控制台 |
| 配置被拒绝 | Config 与 patch 的 config 不匹配 | 检查字段名、类型、默认值 |
| 保存设置冲突 | 另一个标签页或外部编辑改了 settings | 重新加载后再保存 |
| peer 版本被拒 | 插件声明的 DSH 版本与当前不符 | 对齐版本,或按插件面板提示处理 |
常用命令:
# 查看已安装插件
dsh plugin --profile web list --depth 0
# 查看组合后的配置树
dsh --profile web --dump-config
# 重新启动
dsh --profile web --no-open --port 8080
日志位置:
~/.dsh/logs:DSH 主日志。~/.dsh/profiles/<profile>/.plugin-manager/logs:插件管理相关日志。- 浏览器开发者工具:Client 侧报错、插槽注册失败、组件异常。
八、安全与质量清单
开发插件时,至少过一遍下面几项:
- 最小依赖:只
inject真正需要的服务,不要为了省事把文件、网络、进程服务全拿一遍。 - 输入校验:工具参数依赖 schema 校验,但业务约束(长度、范围、路径)仍要在
execute里检查。 - 取消与超时:把
exec.signal传给耗时操作;外部请求设置超时,避免插件挂死整个回合。 - 输出上限:大结果要截断或落盘,不要把几 MB 文本直接塞回模型上下文。
- 不记录密钥:日志、错误信息、Client 代码里都不要出现 API Key、Token 或用户隐私。
- Client 侧不信任输入:浏览器侧的参数同样可能被篡改,Host 侧必须重新校验。
- 权限边界:能走 Host 文件策略的操作,不要绕到 Client 或裸
fetch。 - 测试卸载:启用、禁用、更新、卸载各走一遍,确认没有残留注册或重复 id。
- 版本声明:
engines.dsh和 peer 范围写实际测试过的值。 - 许可证与仓库:要让用户能审计你的代码,尤其是会上架市场的插件。
九、参考来源
- DSH 仓库:https://github.com/deepseek-ai/deepseek-harness
- npm 包
@deepseek-ai/dsh:https://www.npmjs.com/package/@deepseek-ai/dsh - 随包文档:
@deepseek-ai/dsh-tools、@deepseek-ai/dsh-commands、@deepseek-ai/dsh-typert-registry、@deepseek-ai/dsh-client-ui-settings-models、@deepseek-ai/dsh-base dsh-plugin-shop源码与 README:https://github.com/LivXue/dsh-plugin-shopdsh-context源码与 README:https://github.com/bowenliang123/dsh-contextdsh-image-gen源码与 README:https://github.com/shanliuling/dsh-image-gen
以上结构和 API 以 2026-10-24 的 0.1.5-rc.3 核验结果为准。插件接口仍在演进,升级 DSH 后请重新阅读对应包的类型声明和 README。