Back to home

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_pluginsnames(包名)选择性 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)
  • 设置页:浏览器半通过一个 dynamicPlugins Remote 服务(list / load)访问 host 侧的仓库;loaddefine(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-toolsdefineTool)与 @deepseek-ai/dsh-typert-protocolRemote/TypertRemoteService),以及浏览器半边 dsh.client 声明的 api-remotesclient-runtimeclient-ui-settingsclient-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)显式触发。
  • 它只是给动态插件补了「源码落盘」这一环,让「重启后重放」变成一条工具调用(或设置页里的一次点击),而不是靠记忆里的旧对话上下文。