DSH plugin 界面开发:设置卡片怎么注册、Host 半与浏览器半怎么配合、client 打包怎么写

插件开发发布于 2026-09-12作者: DeepSeek Plugin 插件市场
DSH pluginDeepSeek Harness插件界面开发设置卡片slots
DeepSeek Harness(DSH)插件界面开发:一个包里写 Host 半(用 settings.installSection 注册命名空间)与浏览器半(向 settings.plugin.item 槽注册卡片),用 ctx.settingsScope 读写,并按 dsh.client 打包 ./client。

DSH plugin 的界面开发不是「写个前端页面」,而是在同一个包里写两半代码:Host 半用 ctx.settings.installSection() 注册一个设置命名空间,浏览器半向 settings.plugin.item 槽注册一张卡片;两半靠同一个命名空间自动配对,无需改动宿主仓库。 无论叫 DSH插件 还是 DeepSeek插件,界面开发的这套两半结构完全一致。

DSH plugin 界面开发的两半结构:一个包,两个入口

设置页由 Host 半与浏览器半组成,缺一半卡片不会出现。 官方 Cookbook 的原话是:Host 服务每个已注册的设置命名空间,「Plugins」区按卡片编辑的命名空间给它们配对,两半都在一个包里——Host 半在 src/,浏览器半在 src/client/,通过 exports['./client'] 导出并用 dsh.client 声明(来源)。

my-plugin/
├── src/index.ts          # Host 半:注册命名空间
├── src/client/index.tsx  # 浏览器半:注册卡片
└── package.json          # exports['./client'] + dsh.client

配对键只有一个:命名空间。 建议把它写成常量(如 MY_PLUGIN_NS = 'my-plugin'),两半引用同一个常量,避免拼写漂移导致「卡片没出现」。

DSH plugin 的 Host 半:注册设置命名空间

已有 cordis.yml 条目的插件走 ctx.settings.installSection(),它会把条目分层放在用户文档之下,并且在没有设置提供方挂载时依然工作。 官方给出的宿主半写法如下:

ts
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-settings'
import z from '@deepseek-ai/schemastery'

export const MY_PLUGIN_NS = 'my-plugin'

export interface Config {
  endpoint?: string
  retries?: number
}

export const Config: z<Config> = z.object({
  endpoint: z.string(),
  retries: z.number().step(1).min(0).default(3),
})

export function apply(ctx: Context, config: Config) {
  let source = () => config
  ctx.inject(['settings'], (settingsCtx) => {
    settingsCtx.settings.installSection(ctx, MY_PLUGIN_NS, Config, config, {
      // schema 表达不了的约束:拒绝这次写入,而不是留到下次使用才报错。
      validate: value => void assertReachable(value.endpoint),
      setSource: (current) => { source = current },
      onChange: () => { rebuildFromSettings(source()) },
    })
  })
}

两个容易忽略的声明:给字段加 role('secret') 会让它的值不出现在任何响应里(卡片要么把它写进 update/mutate 载荷,要么通过 credentials 域引用凭据);applies: 'restart' 告诉配置界面「拥有者要到下次启动才真正生效这次变更」。

DSH plugin 的浏览器半:把卡片挂进槽位

卡片注册进 settings.plugin.item 槽,key 必须与 Host 半的命名空间一致。 官方示例:

ts
import type { Context as ClientContext } from '@deepseek-ai/cordis'
// 仅类型导入:该槽位的声明。跨插件协作一律走 cordis 服务,
// 值导入会被 client 的 bundle-purity 关卡拦下。
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'

export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']

export function apply(ctx: ClientContext): void {
  const card = new MyPluginCardController(ctx.settingsScope.bind({ namespace: MY_PLUGIN_NS }))
  ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
    name: 'settings.plugin.item',
    key: MY_PLUGIN_NS,
    locale: 'settings.myPlugin',
    inject: () => card.inject(),
  }, MyPluginCard))
}

卡片拥有自己内部的一切:外观、控件、文案。注意上面那行 import type {}——跨插件只能用类型导入,值导入会被 bundle-purity 关卡拒绝。

DSH plugin 配置读写语义:value / base / user 三层

作用域快照携带表单需要的三层信息,判断「是否被覆盖」看的是键的存在性而不是值。来源

含义用途
value解析后的生效值表单回显
base组合层展示「默认来自哪里」
user原始用户层键存在 = 该字段被用户覆盖

写操作用 scope.set(field, value) 存单个字段,scope.unset(field) 把它清回组合层。每次写入都用读到的 revision 做栅栏——这能避免两个设置面板同时修改时互相覆盖。

DSH plugin 界面显示规则与打包要求

显示规则很直接:Host 服务该键且卡片注册了该键才渲染;Host 没服务就整张卡片不出现;Host 服务了但没有卡片认领,则什么都不渲染——这正是 ui-themepermissionllm-* 这些命名空间不出现在该标签页的原因。卡片顺序等于注册顺序,带 key 的条目不能自己声明 order

打包三条硬要求来源):

  1. package.jsonexports['./client'] 暴露浏览器半,并声明 dsh.client(含 platform 与它依赖的 client 包);
  2. bundle 必须是加载器期望的 lazy-CJS 工厂产物——仓库外没有公开 preset,需要自行复现同样的输出格式;
  3. 不能跨插件值导入:卡片要自带外观与暂存模型,自己拥有暂存与 revision 栅栏。

只要 cordis.yml 挂上这个插件,它就出现在页面上,无需重建 Web 应用——因为客户端模块系统是扫描已启用条目、按 dsh.client 服务构建产物的。

DSH plugin 界面开发自检与下一步

发布前过一遍

  1. 命名空间常量是否两半共用;
  2. Host 半是否处理了 validate(schema 表达不了的约束)、setSourceonChange
  3. 敏感字段是否加了 role('secret'),需要在下次启动才生效的是否标了 applies: 'restart'
  4. 卡片是否只用类型导入跨插件;
  5. exports['./client']dsh.client 是否齐备,bundle 是否为 lazy-CJS 工厂产物。

工具调用的卡片呈现是另一条线presentCall / presentResult,且必须是纯函数),见 怎么写工具插件。界面之外的配置声明见 插件配置怎么用;打包与发布见 打包成 bundle发布到插件中心

常见问题

DSH plugin 界面开发为什么一定要写两半代码?

**DSH plugin 的设置页由 Host 半与浏览器半共同构成**:Host 半注册命名空间并决定服务哪些配置键,浏览器半注册卡片并决定怎么渲染。**两半靠命名空间自动配对**——「Plugins」区按卡片编辑的命名空间给它们配对,所以你只要在一个包的 src/src/client/ 里各写一半即可,无需改动宿主仓库(来源:官方 Cookbook「添加设置卡片」)。

DSH plugin 的 Host 半怎么注册设置命名空间?

**DSH plugin 的 Host 半用 ctx.settings.installSection() 注册设置命名空间**:传入插件上下文、命名空间常量、Config schema、当前配置与 { validate, setSource, onChange }。判断标准是「这个插件是否已有 cordis.yml 条目」——已有条目的消费方走 installSection 更稳,它会把条目分层放在用户文档之下,并且在没有设置提供方挂载时依然工作(来源:官方 Cookbook「添加设置卡片」)。

DSH plugin 的浏览器半怎么把卡片挂到设置页?

**DSH plugin 的浏览器半把卡片注册进 settings.plugin.item 槽**:用 ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ name, key, locale, inject }, Card)),其中 key 必须是 Host 半注册的同一个命名空间。卡片内部通过 ctx.settingsScope.bind({ namespace }) 拿到作用域来读写配置(来源:官方 Cookbook「添加设置卡片」)。

ctx.settingsScope 的快照里 value、base、user 分别是什么?

**DSH plugin 设置卡片的作用域快照携带表单需要的三层信息**:value 是解析后的生效值、base 是组合层、user 是原始用户层。判断某个字段是否被用户覆盖,看的是 user 里那个键的**存在性**而不是它的值;scope.set(field, value) 写入单个字段,scope.unset(field) 把它清回组合层(来源:官方 Cookbook「添加设置卡片」)。

DSH plugin 发布浏览器半时对打包有什么硬要求?

**DSH plugin 发布浏览器半时对打包有三条硬要求**:① package.jsonexports['./client'] 暴露浏览器半并声明 dsh.client;② bundle 必须是加载器期望的 lazy-CJS 工厂产物;③ 不能跨插件做值导入——client 的 bundle-purity 关卡会拒绝跨插件的 value import,所以卡片要自带自己的外观与暂存模型。仓库内用共享 preset 的 clientBundle(),仓库外需自行复现同样的输出格式(来源:官方 Cookbook「添加设置卡片」)。

相关术语

settings.installSection()
installSection 是 DSH plugin 的 Host 半注册设置命名空间的入口:传入上下文、命名空间、Config schema、当前配置以及 validate / setSource / onChange 回调,让该插件的配置出现在设置页的「Plugins」区。https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-settings-card.md
settings.plugin.item 槽
settings.plugin.item 是设置页「Plugins」区的槽位,DSH plugin 的浏览器半把卡片注册进去;槽位按 key(即命名空间)配对 Host 半,Host 未服务的键不会渲染卡片。https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-settings-card.md
ctx.settingsScope
settingsScope 是 DSH plugin 浏览器半读写配置的作用域,每个写入都用读到的 revision 做栅栏;快照含 value / base / user 三层,set 写单字段、unset 清回组合层。https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-settings-card.md
dsh.client
package.json 里的 dsh.client 字段声明该 DSH plugin 带浏览器半,客户端模块系统据此扫描已启用条目并服务其构建后的 ./client 导出,因此挂进 cordis.yml 即出现在页面上,无需重建 Web 应用。https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-settings-card.md

来源