dsh-plugin-manager
一个可实际使用的 DeepSeek Harness WebUI 插件管理器:在浏览器里搜索 npm 仓库、安装、启用/停用、卸载 profile 插件,不再依赖命令行 dsh plugin。
> 界面与提示中英双语(跟随 UI 语言,中文优先;主机端报错也已中文化)。
功能
| 操作 | 说明 |
|---|---|
| 搜索 | 调 npm registry 搜索,只保留声明了 dsh(dsh.bundle / dsh.client)的包,标注 bundle / client 类型;无结果时自动回退到 @deepseek-ai scope 再搜一次 |
| 安装 | 在 profile 目录里执行 pnpm add <spec>,并按 dsh plugin 的规则 reconcile dsh.profile.bundles(声明了 dsh.bundle 的依赖自动进 bundle 层)。bundle 层需重启 dsh 才生效,界面会明确提示 |
| 启用/停用 | 写 profile 的 cordis.patch.yml(- id: xxx, disabled: true/false),对已加载的行热重载、即时生效;disabled: false 能覆盖 base 层里的禁用 |
| 卸载 | 在 profile 目录里执行 pnpm remove <name> 并 reconcile;被移除的 bundle 层仍会驻留到重启 |
| 清单 | 显示 profile 名/目录、bundle 层列表、已安装依赖、以及全部已加载行的 id / 模块名 / 启停状态 / 激活状态 |
安装
开发模式:link:(推荐,改代码/热更新用)
link: 把插件软链回源目录(不拷贝),改 lib/client.js 会被 dsh-client-hmr 检测到并热更新到浏览器,无需刷新:
cd /Users/imissthecat/plugin_dsh/dsh-plugin-manager
dsh plugin --profile web add .等价写法:dsh plugin --profile web add link:$PWD。
> 注:早期版本因 @Remote 标记的模块私有状态必须用 file:;现已改用语义化 ctx.typert.register(...),不再有双依赖问题,link: 完全可用。
生产模式:file:(拷贝一份,脱离源目录)
dsh plugin --profile web add file:/Users/imissthecat/plugin_dsh/dsh-plugin-manager从 npm(如果发布了)
dsh plugin --profile web add dsh-plugin-manager无论哪种方式,装完首次都要重启 dsh web(bundle 层在启动时解析)。
使用
1. 重启后打开 WebUI → 设置 → 插件(Plugins),会多出一个 Plugin manager 标签页。 2. 顶部:输入 npm 包名/版本/git 地址/绝对路径 → Install;或输入关键词 → Search registry,从结果里点 Install。 3. 中部 Loaded plugins:每个已加载行有 启用/停用 状态点和 Disable/Enable 按钮,点击即时生效(热重载)。 4. 底部 Installed packages & bundle layers:列出 bundle 层和已安装依赖,依赖行有 Uninstall 按钮。
> 安全说明:这些 RPC 走 /api 通道,只有 loopback(本机浏览器)能触发;install 用 spawn 数组传参(不经过 shell),并拒绝 - 开头的 spec 和相对路径。不要把这个 web 端口暴露到公网,否则等同于把 pnpm add 交给远端。
工作原理(双面插件)
一个包同时是 bundle 和 client 插件:
package.json
├── dsh.bundle.patch = ./cordis.patch.yml # 插入 host 行
└── dsh.client = { platform: "web", ... } # 让浏览器内核扫描 ./client
cordis.patch.yml # 插入一行: id=plugin-manager, name=dsh-plugin-manager
lib/index.js # HOST 半:提供 pluginManager 服务 + 注册 Typert RPC
lib/client.js # 浏览器半:设置页 tab,通过 ctx.connection.rpc.call 调 /api关键点:HOST 半用 ctx.typert.register({...})(src-json codec)把 pluginManager/* 五个 RPC 注册进宿主网关,而不是 @Remote 装饰器 —— 后者把标记存进 dsh-typert-protocol 的模块私有 WeakMap,一旦插件解析到自己的那份依赖就会对不上号(双依赖问题)。这样本插件只依赖 js-yaml 和 peer @deepseek-ai/cordis-plugin-include(entryListSchema),peer 一律由宿主提供。
目录
dsh-plugin-manager/
├── package.json
├── cordis.patch.yml
├── lib/
│ ├── index.js # host:RPC 服务(list/search/install/uninstall/setEnabled)
│ └── client.js # browser:设置页 React tab(手写 __ModuleLoader__ bundle,无需构建工具)
└── README.md局限与说明
- bundle 层增删需重启:
dsh.profile.bundles在启动时解析,安装/卸载 bundle 只改 manifest + node_modules,重启才真正进入/离开配置树;界面会提示restartNeeded。 - 纯依赖(非 bundle)不会自动出现在树里:装完还需要在
cordis.patch.yml加一行(或用 bundle 引用它)才会加载。 - 启用/停用按本地 row id 写 patch,命中第一个同名 id;dsh 自带 bundle 的 id 唯一,但极端情况下嵌套子行 id 若撞名会有歧义。
- 搜索依赖 npm 的排序:官方
@deepseek-ai/dsh-*包在 npm 搜索里排名靠后,可能搜不到;可直接在 Install 框里输入完整包名安装。 - 安装/卸载会调用本机
pnpm,请确保pnpm在 PATH 上。
热更新 / 热加载
| 改什么 | 生效方式 |
|---|---|
UI 文案/布局(lib/client.js) | 热更新:link: 安装后改源文件,dsh-client-hmr(500ms stat-poll)检测变化→改 rev→SSE 推给浏览器原地替换,无需刷新;file: 安装则改 profile 里的那份拷贝 |
启用/停用某行、配置覆盖(cordis.patch.yml) | 热加载:即时生效(界面「停用/启用」按钮走的就是这条) |
后端逻辑(lib/index.js) | 需重启:Loader 启动时 import 一次 |
| 安装/卸载 bundle 插件 | 需重启:dsh.profile.bundles 启动时解析 |
开发(改这个插件)
cd dsh-plugin-manager
npm install # 装 js-yaml + peer(仅本地类型/语法用;运行时 peer 由宿主提供)
node --check lib/index.js && node --check lib/client.js推荐工作流:用 link: 安装 → 改 lib/client.js 看浏览器自动热更新;改 lib/index.js 后重启 dsh web 生效。