DeepSeek Harness 插件开发进阶:从 Host 工具到 Client UI

核验日期:2026-10-24。本文基于本机安装的 DSH ​0.1.5-rc.3​ 及其随包 README、类型声明和已安装插件源码编写。DSH 的插件 API 仍在快速演进,示例代码用于说明结构和写法,实际构建请以同版本包的类型定义和插件模板为准。

一、先理解 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。开发阶段建议用 web profile,而不是桌面端持有的 desktop profile。

  • 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 执行并返回结果。

如果工具没有出现,先看配置树是否真的包含插件行:

powershell

复制代码
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>
      ),
    ),
  )
}

这段代码做了三件事:

  1. 声明依赖 slots 服务。
  2. ctx.slots.inject('conversation.view', ...) 等待宿主注册这个插槽。
  3. 在插槽出现后,用 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 侧报错、插槽注册失败、组件异常。

八、安全与质量清单

开发插件时,至少过一遍下面几项:

  1. 最小依赖 :只 inject 真正需要的服务,不要为了省事把文件、网络、进程服务全拿一遍。
  2. 输入校验 :工具参数依赖 schema 校验,但业务约束(长度、范围、路径)仍要在 execute 里检查。
  3. 取消与超时 :把 exec.signal 传给耗时操作;外部请求设置超时,避免插件挂死整个回合。
  4. 输出上限:大结果要截断或落盘,不要把几 MB 文本直接塞回模型上下文。
  5. 不记录密钥:日志、错误信息、Client 代码里都不要出现 API Key、Token 或用户隐私。
  6. Client 侧不信任输入:浏览器侧的参数同样可能被篡改,Host 侧必须重新校验。
  7. 权限边界 :能走 Host 文件策略的操作,不要绕到 Client 或裸 fetch。
  8. 测试卸载:启用、禁用、更新、卸载各走一遍,确认没有残留注册或重复 id。
  9. 版本声明 :engines.dsh 和 peer 范围写实际测试过的值。
  10. 许可证与仓库:要让用户能审计你的代码,尤其是会上架市场的插件。

九、参考来源

相关推荐
season_zhu1 小时前
Skills For Real Engineers 系列之二:grill,把需求想清楚,也别把自己问进死胡同
ios·ai编程
前端双越老师2 小时前
程序员 1v1 访谈系列第七期:失业半年所想到的
agent·ai编程·全栈
mudtools2 小时前
一份声明,多份产物:飞书 SDK 的 AI 工具面是怎么生成的
人工智能·.net·ai编程
xifangge20252 小时前
把 DeepSeek 搬回自己电脑:Ollama+OpenWebUI+AnythingLLM 本地私有部署(含离线一键包)
人工智能·deepseek
kyle~2 小时前
DINOv3 --- 自监督通用视觉特征引擎
人工智能·计算机视觉·ai
香蕉大香蕉(06敬畏市场)2 小时前
高频八股 · Java 并发编程(10):异步、ThreadLocal 与虚拟线程(收官)
java·开发语言·网络·jvm·python·ai·实习
@嵌入式扫地僧2 小时前
实战教程:NVIDIA Jetson Nano 8GB 搭建本地 RAG 大语言模型知识库完整指南
人工智能·ai·jetson nano·1024程序员节·rag
小年糕是糕手2 小时前
【AI】用 Python 做一个 AI 截图检索助手:把收藏夹里的截图找回来
linux·人工智能·python·chatgpt·agent·codex·deepseek
ifenxi爱分析2 小时前
宇树地位不稳、MiniMax掉队、豆包手机遭拒|AI周评
ai