wacly
dsh-dynamic-plugin-manage
deepseek harness 动态插件管理
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 14, 2026
- Updated
- Aug 14, 2026
Introduction
dsh-dynamic-plugin-manager
给 DeepSeek Harness(dsh)的动态插件补上「持久化」能力:把当前会话里的动态插件(cordis_define 定义的)导出到固定目录,之后(包括重启后)按需选择性地加载重放回来,并在 Web 设置页里提供一个「动态插件」栏目,可以查看所有已保存的动态插件、一键导入到当前会话。
它解决的是 dsh 动态插件的一个短板——cordis_define 只存在进程内存里,重启即消失。本插件在磁盘上建了一个「动态插件仓库」,补齐「源码别丢 + 随时可再装」这一环。
三个工具 + 一个设置页
安装后,模型(AI)可以调用三个工具:
| 工具 | 作用 |
|---|---|
export_dynamic_plugins | 把当前会话的动态插件逐个导出为「包」(每个插件一个目录,可指定包名,默认用 pluginId) |
list_dynamic_plugins | 列出目录里已持久化的包(包名、name、purpose、含 host/client 半边) |
load_dynamic_plugins | 按 names(包名)选择性 define + run 指定的插件 |
同时,Web 页面的「设置」里会新增一个「动态插件」栏目:列出磁盘仓库里所有已保存的插件(名称、用途、host/client 半边标记),每个插件带一个「导入」按钮,一键 define + 运行到当前打开的那个会话。
使用流程示例(对话里直接说即可):
导出:「把当前会话的动态插件导出一下」
选择加载:「先列出可加载的插件」→「加载 xxx 和 yyy 这两个」
设置页的「一键导入」是静默的:它先
define(mint 出新的 pluginId/packageId),再走dynamicCordisRunner.startUserRun这个用户手势路径(结算用agent.inject),所以不会像模型发起load那样触发一条「上下文注入 cordis-host-runner」并让模型回一大段话——你点完「导入」直接就能用插件。而对话里的load_dynamic_plugins工具仍是模型驱动路径(模型本来就参与其中),保持不变。
固定目录
默认 ~/.dsh/dynamic-plugins/,若设置了 $DSH_HOME 则用 $DSH_HOME/dynamic-plugins/。
每个插件是一个包(一个目录),包名即目录名;保存时可指定包名,不指定则默认用 pluginId。包内拆成两部分:
<dir>/<packageName>/
├── define.json # 除 code 外的元信息 + code 文件引用
├── host.js # host 半边源码(有则存在)
└── client.js # client 半边源码(有则存在)
define.json 的内容:
{
"name": "my-tool",
"purpose": "注册一个 xxx 工具",
"idPrefix": "mytool",
"code": {
"host": "host.js",
"client": "client.js"
}
}
code 字段不再是内联源码,而是引用包内对应的代码文件;真正的源码抽离成 host.js / client.js(有需要时也可引用更多文件,code 里的值就是相对包目录的文件路径)。
这份内容就是 cordis_define 需要的全部字段,所以重放时无需原始对话上下文。
兼容性:插件在
list/load时会自动把旧格式的顶层<pluginId>.json(内联 code)迁移成新包格式,迁移前会先写入并校验新包、确认无误才删除旧文件。
工作原理
- 导出:
ctx.dynamicCordisRunner.listPlugins(agent)拿当前会话的插件 → 对每个取currentPackageId(当前版本)或最新版本 →inspectPackage(agent, pluginId, packageId)读出code: { host?, client? }→ 拆成define.json+host.js/client.js写入包目录。 - 加载:读包目录里的
define.json+ 引用的代码文件 → 对每个ctx.dynamicCordisRunner.define({ sessionId, plugin: { kind: 'new', idPrefix }, name, purpose, code })→ctx.dynamicCordisRunner.run(agent, pluginId, packageId, 'run', signal)。 - 设置页:浏览器半通过一个
dynamicPluginsRemote 服务(list/load)访问 host 侧的仓库;load只define(mint 新的 pluginId/packageId),浏览器半再走dynamicCordisRunner.startUserRun这个静默用户手势路径完成运行。
沿用 dsh 的边界约定:
- 加载时插件归属发起加载的那个会话(动态插件是 session 私有的)。
- 带
client半边的插件,run可能返回awaiting-approval,需要在浏览器页面点「运行」完成(纯host插件无此步骤)。 - 每次加载会 mint 新的
pluginId,与上次运行的 id 无关。
目录结构
dsh-dynamic-plugin-manager/
├── package.json # dsh.bundle 清单 + dsh.client 清单 + peer 依赖
├── cordis.patch.yml # bundle 配置层:插入一行挂载本插件(host 半边)
├── tsdown.config.ts # 双半边构建:host + browser client
├── src/index.ts # 三个工具 + dynamicPlugins Remote 服务(host)
├── src/remote.ts # 手写的 Typert Remote 客户端贡献(strict 描述符)
└── src/client/ # 浏览器半边:设置页「动态插件」栏目 + 文案
构建
pnpm install # 或 npm install;只会装 tsdown,@deepseek-ai/* 保持 external 不落地
pnpm run build
产物:
lib/index.js— Host 插件(ESM,@deepseek-ai/*与node:*保持 external,运行时由 dsh 安装解析)。lib/client.js— 浏览器插件(CJS closure-factory,react与@deepseek-ai/*由 shell 的模块表解析,只内联本包自己的文件)。
说明:host 半边的
@Remote标记没有用装饰器语法——rolldown/oxc 会把 TypeScript 装饰器原样输出为 Node 无法解析的原生@decorator语法。插件改为直接调用Remote(...)装饰器工厂(等价于 tsc 的__esDecorate降级),见src/index.ts里的applyRemoteMethods。
安装并运行
# 在 deepseek-harness 仓库根目录
pnpm dsh plugin --profile demo add /absolute/path/to/dsh-dynamic-plugin-manager
pnpm dsh --profile demo --dump-config # 应能看到 # == dsh-dynamic-plugin-manager 一层
pnpm dsh --profile demo web
打开 Web 界面后,进入「设置」→「动态插件」,即可看到仓库里已保存的插件并一键导入到当前会话。
依赖说明:本插件的
package.json故意不声明任何@deepseek-ai/*依赖(不在dependencies/devDependencies/peerDependencies里)。host 半边运行时import的@deepseek-ai/dsh-tools(defineTool)与@deepseek-ai/dsh-typert-protocol(Remote/TypertRemoteService),以及浏览器半边dsh.client声明的api-remotes、client-runtime、client-ui-settings、client-locale,全部是 dsh 安装自身提供的 in-box 包——由 dsh 的node_modules在运行时解析,构建时也通过neverBundle保持 external,因此无需(也不能)从 npm registry 拉取。之所以这么做:这些 rc 包(尤其
@deepseek-ai/dsh-typert-protocol,它最近才从@deepseek-ai/dsh-type-meta改名)并未完整发布到公共 registry;pnpm dsh plugin add会触发pnpm install,一旦在依赖里声明它们,pnpm 就会去 registry 解析并 404(@deepseek-ai/dsh-type-meta: Not Found)。不声明即可让pnpm install/pnpm add只解析tsdown,干净通过。
与官方两套机制的关系
- 它不是把动态插件变成静态 bundle,也不做「启动自动加载」——加载仍是按需、由你(或 AI)显式触发。
- 它只是给动态插件补了「源码落盘」这一环,让「重启后重放」变成一条工具调用(或设置页里的一次点击),而不是靠记忆里的旧对话上下文。