dsh 插件开发模板,覆盖配置、工具、事件、服务、钩子、浏览器 UI 插槽与斜杠命令。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:kun2-5code/dsh-plugin-template
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 中文
DeepSeek Harness(dsh)插件的开箱即用模版。它在一个最小可安装 bundle 里演示了最常见的几种插件形态:
- Config ——
Config接口加一个 Schemastery schema,其中可实时改写的字段带.volatile(),于是 Plugins 页能在不重启的情况下编辑它们(文档) - Tool ——
ctx.tools.register(defineTool(...))注册一个模型可调用的工具,并带card标签的渲染意图(文档) - Events ——
ctx.on/ctx.emit,用 declaration merging 得到带类型的事件(文档) - Service —— 类形式的插件,向其它插件提供一个服务(文档)
- Hook —— 一个
tools/pre-execute权限拦截器,按配置拒绝工具调用(文档) - 浏览器半边(client) ——
src/client/在 十四个 UI 面 上注册浏览器 UI(索引见 docs/ui-surfaces.md):Plugins 页上的配置表单、侧栏底部动作、输入卡片上方的输入区 Dock、帧级浮层、会话头徽标、输入卡片左右两端的工具位按钮、/dsh-demo的自定义命令行、设置 → 通用 的偏好行、设置 → 插件 的迁移页、设置页头部动作、会话头动作、输入卡片下缘的状态条,以及 AI 回复上的逐消息动作;greet工具另有一个presentResult渲染意图。
模版遵循官方 bundle 分发模型:包声明 dsh.bundle 与 cordis.patch.yml,dsh plugin add 把它作为一层配置激活。
目录结构
dsh-plugin-template/
├── package.json # npm manifest + dsh.bundle / dsh.client declarations + prepare build script
├── tsconfig.json # strict type-check configuration (tsc --noEmit)
├── tsdown.config.ts # build config: Node library (lib/) + client bundle (lib/client.js)
├── vitest.config.ts # unit test config (node by default; specs opt into jsdom)
├── cordis.patch.yml # bundle config layer: inserts the plugin rows
├── locale/ # plugin display metadata read by the Plugins page
│ ├── en.json # meta.title / meta.description (the discovery entry)
│ └── zh.json
├── icon.svg # optional bundle card artwork
├── dev/cordis.yml # local dev overlay (points at source; use with dsh web --patch)
├── docs/
│ ├── ui-surfaces.md # where the plugin registers UI + index of every slot (bilingual: ui-surfaces.zh.md)
├── src/
│ ├── index.ts # main plugin: Config + tool + events + effect
│ ├── commands.ts # host half: demo slash commands /hello (replies world) and /dsh-demo (custom row)
│ ├── service.ts # optional example: Service provider (disabled by default)
│ ├── hook.ts # optional example: hook permission gate (disabled by default)
│ └── client/ # browser half: one module per UI surface (see docs/ui-surfaces.md)
│ ├── index.ts # client entry: inject + apply, registers the locale dictionary and styles
│ ├── constants.ts # shared NAMESPACE + LOCALE_NAMESPACE + DEMO_COMMAND_NAME
│ ├── locales.ts # typed en/zh dictionaries (all user-visible copy lives here)
│ ├── styles.ts # one injected <style> with all dtpl-* classes (theme tokens only)
│ ├── config-card.tsx # plugins.bundle.config: the configuration form on the Plugins page
│ ├── sidebar-action.tsx # sidebar.footer.action
│ ├── input-dock.tsx # conversation.input.dock
│ ├── shell-overlay.tsx # shell.overlay
│ ├── header-utilities.tsx # conversation.session.header.utilities
│ ├── input-left.tsx # conversation.input.left
│ ├── input-right.tsx # conversation.input.right
│ ├── commandview.tsx # conversation.chat.commandview
│ ├── general-item.tsx # settings.general.item
│ ├── plugins-tab.tsx # settings.plugins.tab
│ ├── settings-action.tsx # settings.action
│ ├── header-actions.tsx # conversation.session.header.actions
│ ├── composer-dock.tsx # conversation.composer.dock
│ └── assistant-actions.tsx # conversation.chat.assistant-actions
└── test/smoke.mjs # smoke test on the build output
└── tests/ # unit tests
├── host-half.spec.ts # the host half on a real cordis Context
├── slot-registration.client.spec.ts # every surface registers, and leaves with the fiber
├── config-card.client.spec.tsx # the configuration form's user-visible behavior
├── surfaces.client.spec.tsx # command row, sidebar, input, per-message button
├── locale-and-styles.client.spec.ts # dictionary and stylesheet rules
└── support/ # test doubles: slot registry, locale
快速开始
作为 bundle 安装(给使用者)
在任意目录里把这个包(或你的 fork)装进一个 dsh profile:
# local directory
dsh plugin --profile demo add /path/to/dsh-plugin-template
# or directly from GitHub (replace with your own repo after forking)
dsh plugin --profile demo add github:you/dsh-plugin-template
从 GitHub 安装会拉源码,pnpm 随后执行 prepare(即 tsdown)构建出 lib/。pnpm ≥10 会拒绝第一次 git 依赖的 prepare;把 pnpm 打印出来的包名加进该 profile 的 pnpm-workspace.yaml 再重试:
allowBuilds:
dsh-plugin-template: true
这份白名单授权在安装时执行该包的代码——只允许你信任的源码,并且优先固定到某个提交:
github:you/dsh-plugin-template#<sha>。
确认配置层并启动:
dsh --profile demo --dump-config # should show a "# == dsh-plugin-template" layer
dsh --profile demo
注意:自定义名字的 profile(例如
demo)只含dsh-base,是无 GUI 的。 要用 Web GUI 和下面的配置表单,请用webprofile(= dsh-base+dsh-web-app)——见在 GUI 里测试配置表单。
本地开发(修改插件时)
在 deepseek-harness 源码仓库的根目录,用一个 overlay 直接加载本仓库的源码(无需安装、无需构建):
pnpm dsh web --patch /absolute/path/to/dsh-plugin-template/dev/cordis.yml
把 dev/cordis.yml 里的 name 改成本机上的路径,写成 file:// URL,打开 http://127.0.0.1:3080,让模型调用 greet 工具。裸绝对路径在 Windows 上会失败:Loader 把条目的 name 直接交给 import(),D:\… 会被解析成协议 d:,该条目以 ERR_UNSUPPORTED_ESM_URL_SCHEME 失败,插件根本不会加载。取 URL 形式:node -e "console.log(require('node:url').pathToFileURL('<路径>').href)"。
--patchoverlay 只加载插件的宿主半边(模块解析够不到包级声明)。 要测浏览器半边必须装进 profile(由name: dsh-plugin-template解析)——见下一节。
开发时自己跑检查:
pnpm install
pnpm typecheck
pnpm test:unit
pnpm build
pnpm smoke
pnpm test
typecheck 用 tsc 检查源码、测试与构建配置。test:unit 跑 vitest 那几份 spec;smoke 跑在 lib/ 上;test 按这个顺序全跑一遍。
如果本仓库位于
deepseek-harness检出目录内部(就像在 harness 仓库根目录那样),pnpm install会被上层 workspace 接管,在这里什么也装不上——模版不是 workspace 成员。请用pnpm install --ignore-workspace,让它按自己的 lockfile 装自己的node_modules;或者把模版单独 clone 到别处。
在 GUI 里测试配置表单
表单在浏览器里渲染,依赖 dsh 的 client-modules 按包名发现 dsh.client 声明,所以这个包必须装进 profile(--patch 的源码路径不行):
# 1. Build (produces lib/index.js + lib/client.js)
cd /path/to/dsh-plugin-template && pnpm build
# 2. Install into the web profile (= dsh-base + dsh-web-app, full GUI)
dsh plugin --profile web add /path/to/dsh-plugin-template
# 3. Boot the web GUI (`dsh web` is equivalent to `dsh --profile web`)
dsh web
打开 http://127.0.0.1:3080,进入侧栏的 Plugins 页,选中 Plugin Template:
- 该 bundle 的页面上会渲染出含
greeting、maxRetries、verbose的配置表单; - 改掉
greeting并点保存——部署接受这些值,状态行确认成功; - 回到会话里让模型调用
greet工具——它用的是新的打招呼文案(宿主半边每次调用都读config.greeting.get(),不需要重启); - 改动会落进
$DSH_HOME下的设置文档并在重启后保留。恢复默认会清除该字段,让它重新继承cordis.patch.yml里的值。
没有白名单要改,也不需要重启步骤:只要插件条目的 Config 至少有一个 .volatile() 字段,命名空间就会被自动服务;Plugins 页把 form(已接受的值加一个带 revision 围栏的 mutate)交给这个页面。
改完客户端半边(src/client/)后,重新 pnpm build 并刷新页面(客户端 bundle 的 rev 查询会破缓存)。
改成你自己的插件
- 改名时保持一致:
package.json的name(npm 名,例如dsh-my-plugin)、src/index.ts的name、cordis.patch.yml的id/name。改名也牵动浏览器半边:tsdown.config.ts里客户端 bundle 的id(__ModuleLoader__.load({ id }))、src/client/constants.ts的NAMESPACE(Plugins 页靠它做键)、package.json的dsh.client.inject,以及locale/en.json。改./service子路径时,exports/files也要一起改。 - 改
Config接口与 schema:两次部署之间可能不同的一切都必须是配置字段(设计原则)。用户应当能免重启修改的字段标上.volatile(),并在读取处用.get()。 - 在
src/client/config-card.tsx的表单里为每个新的可编辑字段加一行:标签键、提示键,以及FIELDS/buildOps/draftValue里对应的分支。表单是手写的——它不会从你的 schema 自动渲染。 - 在
apply里注册你的工具:ctx.tools.register(defineTool({...}));execute返回output.schema声明的正典值,output.render是模型可见渲染的纯函数,presentResult是 UI 渲染意图(工具参考)。 - 要向其它插件提供能力时,启用
src/service.ts并在cordis.patch.yml里取消注释它那一行。 - 记得用
declare module '@deepseek-ai/cordis'合并Context/Events类型——这是跨包边界保持类型安全的手段。每个事件要写清@mode,每个 payload 参数要写@param。 - 要拦截工具调用或充当权限闸门时,启用
src/hook.ts(取消注释cordis.patch.yml里那一行):ctx.on('tools/pre-execute', ...)返回{ kind: 'deny', reason }表示拒绝,调用next()表示放行(扩展点手册)。 - 每加一条用户可见文案,就在
src/client/locales.ts的en与zh里各加一个键,并通过注册项locale选项提供的t席位读取;list 槽的标签用 thunk(label: () => t('key')),这样切换语言不需要重新注册。
浏览器半边如何工作
package.json声明dsh.client: { platform: "web" }加exports["./client"]——dsh 的 client-modules 发现它,把lib/client.js当浏览器插件加载;lib/client.js是window.__ModuleLoader__.load({ id, factory })格式的惰性 CJS 工厂。tsdown.config.ts手搓了这个格式;harness 仓库自己的 preset 在packages/client/tsdown.client.ts,没有发布;- 客户端入口(
src/client/index.ts)先通过ctx.effect注册 locale 字典与样式表——两者都随插件一起撤销——再逐个调用各面的register*; - 每一面都通过
ctx.slots.inject(name, () => ctx.slots.register(...))注册:它会等拥有方的声明出现,该声明消失时移除自己的贡献,并随插件 fiber 一起离开; - 运行时浏览器半边只依赖
react,由浏览器平台模块表提供。不在运行时 import 任何@deepseek-ai客户端包——它们只以import type出现,会被类型擦除。改模版时请保持这个纪律。
测试
pnpm test:unit 跑五份 spec。它们用真实的 cordis Context,所以 fiber、effect 与撤销的行为跟 profile 里一致:
tests/host-half.spec.ts—— 宿主半边在真实组装下的行为:greet 工具与两条命令完成注册;打招呼文案是每次调用现读,而不是加载时读一次;fiber 停用后工具随之消失。tests/slot-registration.client.spec.ts—— 十四个面都落在已声明的槽位上;配置表单以包名为键,命令行以命令名为键;插件迁移页的标签是跟随语言的 thunk;撤销之后所有贡献、样式表与字典都不见了。tests/config-card.client.spec.tsx—— 表单的用户可见行为:摘要与页面两种视图、加载中/不可用/只读三种状态、提交哪些写入并带哪个 revision、把字段改回部署默认值、既挡住保存又能被辅助技术读到的校验,以及两条保存失败路径之后表单仍然可用。tests/surfaces.client.spec.tsx—— 命令行的三种状态(执行中、成功、失败)、侧栏按钮在 rail 态仍保有无障碍名称、输入区控件是显式的非 submit 按钮、逐消息按钮能定位到消息却不把 id 印在界面上。tests/locale-and-styles.client.spec.ts—— 客户端两条容易悄悄退化的规则:每个t('…')的键都在字典里;样式表没有字面色值,font-weight不超过 500。
test/smoke.mjs 是另一回事,它跑在 lib/ 上:检查构建产物能加载、工具与命令可用、权限拦截器既会拒绝也会转交。pnpm test 按 typecheck、单测、构建、smoke 的顺序全跑一遍。
tests/support/ 里的替身顶替 harness 的客户端服务。不能用真的:发布出去的客户端入口是浏览器 bundle,在导入时刻就调用 window.__ModuleLoader__.load(...),在 Node 测试里把它物化出来会把第二份 React 拉进同一个进程。替身是 cordis 服务,因此保住了这里真正要紧的性质——贡献挂在调用方 fiber 上并随之撤销——也会对未声明的槽位抛错。它们的注释写明了保真与不保真的部分。
发布
- npm:
pnpm publish(files已包含构建产物、patch、元数据与图标) - tarball:
pnpm pack,然后dsh plugin --profile demo add ./dsh-plugin-template-0.2.0.tgz - git:
dsh plugin add github:you/dsh-plugin-template(配合上面的allowBuilds步骤)
相关文档
- 实时配置表单:adding-a-settings-card.md
- 插件开发导览:basic/index.md
- 插件配置:basic/config.md
- 工具开发:basic/tool.md
- 打包与安装:basic/publish.md
- 插件与生命周期:framework/index.md
- 服务与依赖:framework/service.md
- 事件系统:framework/events.md
- 客户端 UI 插槽:subsystems/slots.md
- Cordis 教程:cordis-tutorial
链接
同类插件
yjh051108/dsh-routing-suite★ 6995
一个仓库三件套:DSH 插件包的运行时注入器(注入、热重载、卸载、开发侧挂区一键转正、路由自愈,外带设置页插件管理:列出、卸载、拖入文件夹内化)、任务感知的思维模式路由 agent 预设(router-standard / router-spec / router-react)、以及分级两级任务协议(commit_star / lock_stage / revise_do / edit_plan / mark_task / redteam_verdict 六个工具,任务状态落盘)。注入器实现直接在库内,安装的是它自己的行为而不是一份依赖清单。
strukto-ai/mirage#dsh★ 3667
把文件系统与 bash 提供者换成 mirage 虚拟工作区:文件工具与 shell 命令作用于挂载的资源(RAM、S3、Redis、Slack、Gmail、Notion、Postgres)而非宿主磁盘,支持按挂载点设置读/写/执行模式、按命令选择沙箱(进程内 monty、pyodide、quickjs;远程 docker、e2b、daytona),并可在虚拟终端中安装 CLI(git、gh、slack、linear、ntn、gws,或自行注册的程序树)作为命令头词。
hust-open-atom-club/oh-dsh★ 325
社区发行版:TUI、桌面端与 Web UI 统一体验,分层安装、一步到位。
weijiafu14/pi2dsh★ 208
Pi Host ABI 兼容引擎:装一次之后,npm 上的 Pi 扩展原包经 `dsh plugin add <pi-package>` 直接作为 DSH 原生插件挂载。已在官方 DSH 上端到端验证 pi-mcp-adapter(完整 MCP 管理面:OAuth、resources、prompts、MCP Apps、elicitation、sampling)、@tintinweb/pi-subagents、pi-code、pi-hermes-memory、pi-background-tasks;`pi2dsh inspect` 在安装前报告一个包的兼容情况。
lire1131/dsh-undo-savepoint★ 167
DSH 撤销/回退系统:配置变更自动存档,一键撤销/恢复/回退到任意版本,支持 WebUI 与离线 CLI/GUI 工具(DSH 启动失败也能救)。
Fishquito7/dsh-skill-mcp-panel★ 158
在 DSH Web 设置中管理技能与 MCP 服务器:技能卡片热启停、工作区作用域、分组、批量迁移与拖拽导入,以及 stdio/HTTP MCP 增删改查、连接测试、密钥脱敏,并附带统一 dsh-panel 命令行。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。