kyorakuyk
dsh-hotreload-plugin-manager
DeepSeek Harness plugin: hot install / uninstall / update / enable-disable of profile plugin bundles from the running dsh web — no restart, Web UI included.
- Stars
- 2
- Language
- TypeScript
- Created
- Aug 14, 2026
- Updated
- Aug 14, 2026
Introduction
dsh-hotreload-plugin-manager
中文 | English
一个树外(out-of-tree)DeepSeek Harness 插件:在运行中的实例上直接完成插件的热安装、热卸载、禁用/启用——无需重启, 并提供 Web 设置页标签页:
- 🔥 热安装:按 spec(npm 包名 / git URL /
github:owner/repo/file:路径) 安装插件 bundle,立即热生效于运行中的dsh web,且重启后保留。 - 🔥 热卸载:移除 bundle——行与 fiber 即刻卸载、依赖被清理、陈旧的 bundle 层条目一并移除。
- 🔥 禁用 / 启用:在 UI 上一键切换已安装 bundle 的条目开关,状态持久化且立即生效。
- 热更新:热替换 bundle 的补丁行并实时重放配置(模块代码变更按 Node 进程内 缓存特性仍需重启)。
- 零内核改动:纯树外 bundle,dsh checkout 完全不被触碰。
- 双面包:一个 npm 包同时是 host 网关与浏览器设置标签页。
架构
┌────────────────────────────── 浏览器(React)──────────────────────────────┐
│ 设置 → 插件 → "插件管理"标签页(槽位 settings.plugins.tab) │
│ │ fetch('/plugin-manager/*') │
└────────┼──────────────────────────────────────────────────────────────────┘
▼
┌──────────────────────────── dsh web 进程 ──────────────────────────────────┐
│ webserver 路由(node:http)──▶ manager 核心 │
│ ① pnpm add/remove(profile 内的真实依赖) │
│ ② 行写入 profile 用户补丁层(cordis.patch.yml) │
│ ③ 根 Include entry.update → Loader 挂载/卸载 fiber │
│ 网关:TypertRemoteService(源码模式反射,无代码生成) │
└────────────────────────────────────────────────────────────────────────────┘
包是双面的:
- Node 半(
lib/index.js,tsc 编译)——PluginManagerGateway,一个注册为pluginManager的TypertRemoteService。api-gateway 可通过源码模式反射 分发它的@Remote方法(无需生成产物);同时它在 webServer 服务上注册浏览器 HTTP 传输。 - 浏览器半(
lib/client.js,esbuild 打包)——注册进settings.plugins.tab槽位(idmanager)的 Web 设置标签页,由dsh-client-modules发现,并以普通 模块表 bundle(window.__ModuleLoader__.load)形式服务。
工作原理
全部复用 launcher 已有的机制,因此插件不需要对 dsh checkout 做任何改动:
- 安装:在 profile 目录执行
pnpm add <spec>。包成为真实依赖(进入 profile 的node_modules与package.json,pnpm 不会剪除它)。管理器读取 该 bundle 的dsh.bundle.patch行,写入 profile 的用户补丁层 (cordis.patch.yml)。 - 热应用:直接对根 Include 条目调用
entry.update(携带新补丁列表)—— 与 launcher 用户补丁 watcher 相同的调用。管理器自己应用自己的写入,因为文件 watcher 可能漏掉紧跟其后的写入事件。 - 重启持久:用户补丁层在启动时被重新组合,因此已安装 bundle 在重启后保留,
且不触碰
dsh.profile.bundles(也就不会双组合行)。 - 卸载 / 更新:移除或替换行,并执行
pnpm remove/pnpm update(registry 包)或 remove + re-add(file:/link:——pnpm 的增量重拷不会 物化新增文件)。 - 启停:写入用户层禁用行(
{ id, disabled }),按 id 覆盖 bundle 行而不 产生重复。
依赖语义透传给 pnpm。 管理器不解释被安装 bundle 的 manifest:
optionalDependencies 与可选 peer 的行为完全遵循 pnpm 的定义(失败的可选
依赖被跳过、安装继续;profile 使用 autoInstallPeers: false,peer 从不被
自动安装,运行时从 dsh 安装闭包解析)。插件若 import 非闭包 peer,其可用性
由 bundle 作者负责。
浏览器半通过插件自有的同源 HTTP 传输(/plugin-manager/*)与 host 通信,
而不是 Typert remote 管线:npm 上的 dsh-api-gateway 包装不了(其若干依赖
dsh-type-meta、dsh-compact 等未发布),树外无法挂载 client contribution。
host 网关仍注册 Typert 绑定,供进程内消费者使用。
可安装的插件(什么能装)
唯一硬性要求:一个 npm 包,其 package.json 声明
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }(标准 DSH 插件
bundle)。满足即装,插件内部形态不限。
来源形式(spec 白名单,底层透传 pnpm add,pnpm 能解析的规范基本都能用):
- npm 包名(可带版本):
@kyorakuyk/dsh-plugin-manager、@scope/name@^1.0.0 - git URL:
https://github.com/x/y.git、git+ssh://… - GitHub 简写:
github:owner/repo、owner/repo - 本地路径:
file:C:/path/to/bundle、link:…(开发/离线最常用)
插件内部可以是什么(补丁行由根 Include 的插值机制统一处理):
- Host 插件:普通 Cordis 插件、Service、工具注册
- 浏览器 UI 插件:双面
dsh.client包(浏览器半经dsh-client-modules可用) - skill 提供方 / MCP 适配器:挂
dsh-skill-filesystem、dsh-mcp-client的 bundle - 补丁行含
insert/ id-targeted 覆盖 / 禁用行 / group /!!js表达式 - 带
optionalDependencies的 bundle(缺失的可选依赖被跳过、安装继续——已实测)
装不了的(会明确拒绝):
- 无
dsh.bundle.patch的普通库 → 拒绝并自动pnpm remove回滚,不留残渣 - 旧
.dsh-plugin/config.yaml安装格式 → 不支持(DSH 核心已整体移除该路径) - 补丁行 id 与用户层或其他 bundle 冲突 → 拒绝安装(防双组合)
- git 规格装完无法识别包名 → 明确报错 "could not resolve the installed package name"
边界提醒:
- 非闭包 peer:bundle 若 import 既不在 profile 也不在 dsh 安装闭包里的 peer,运行时挂——这是 bundle 作者的责任,管理器不干预
- 模块代码级更新需重启(Node 进程内缓存,见"已知限制")
- 管理器与
dsh pluginCLI 不要对同一 bundle 混用(卸载会自动清理 CLI 残留的 bundle 层条目)
快速开始(其他用户)
前置条件:Node ≥ 22、pnpm ≥ 10、已安装 dsh 并初始化过目标 profile。
① 直接从 GitHub 安装(推荐)——无需克隆本仓库:
dsh plugin --profile web add "github:kyorakuyk/dsh-hotreload-plugin-manager"
# 或完整 git URL:
# dsh plugin --profile web add "https://github.com/kyorakuyk/dsh-hotreload-plugin-manager.git"
② 或者克隆后本地安装:
git clone https://github.com/kyorakuyk/dsh-hotreload-plugin-manager.git
dsh plugin --profile web add "file:$(pwd)/dsh-hotreload-plugin-manager"
③ 或者从 npm 安装(发布后可用):
dsh plugin --profile web add "@kyorakuyk/dsh-plugin-manager"
装完后必须重启 dsh web 实例(新 bundle 需要重启才挂载;仓库已包含构建产物 lib/,无需先构建),浏览器硬刷新一次(Ctrl+Shift+R):
dsh --profile web
然后打开 Web UI → 设置 → 插件 → 插件管理,按 spec 安装其他插件——npm 包名、
git URL、github:owner/repo、或 file:/link: 路径均可。
npm 已发布:
dsh plugin --profile web add "@kyorakuyk/dsh-plugin-manager"。
Web UI
设置 → 插件 → 插件管理:
- 安装输入框(支持上述 spec 形式);
- 已安装列表,每个包带 禁用/启用、更新、卸载 按钮(禁用后显示 "(已禁用)",按钮切换为"启用");
- 手动 刷新 按钮(列表实时反映热变更,无需整页刷新);
- 操作结果内联展示(失败时包含 pnpm 输出尾部)。
HTTP API
| 方法 | 路径 | Body | 结果 |
|---|---|---|---|
| GET | /plugin-manager/list | — | { ok, value: Bundle[] } |
| POST | /plugin-manager/install | { spec } | { ok, message, exitCode?, tail? } |
| POST | /plugin-manager/uninstall | { packageName } | 同上 |
| POST | /plugin-manager/update | { packageName } | 同上 |
| POST | /plugin-manager/setEnabled | { entryId, enabled } | 同上 |
Bundle 形如 { packageName, version?, spec, rowIds, disabled }。
开发
pnpm install
pnpm build # tsc(Node 半)+ esbuild(浏览器 bundle)
pnpm test # vitest:REAL-composition / HTTP / bundle 契约共 22 个测试
pnpm typecheck
发布(npm Trusted Publishing)
发布走 GitHub Actions OIDC 信任发布——无需 token、无需 OTP/恢复码:
npm version 0.1.5 --no-git-tag-version # 升版本
git add package.json pnpm-lock.yaml && git commit -m "pkg: bump to 0.1.5"
git push
git tag v0.1.5 && git push origin v0.1.5 # 触发 .github/workflows/publish.yml
workflow 会:typecheck → build → test → npm publish --provenance(OIDC)
→ 自动创建 GitHub Release。发布失败可重跑:
gh workflow run publish.yml --ref main -f publish_ref=refs/tags/v0.1.5
前置:npm 账号需把本仓库 +
publish.yml注册为该包的 Trusted Publisher (npmjs.com → 包设置 → Trusted Publisher)。
目录结构:
src/ host 半(tsc → lib/*.js)
├── index.ts PluginManagerGateway(TypertRemoteService + @Remote)
├── manager.ts 核心生命周期操作
├── http.ts webserver 路由处理器
└── spec|pnpm|patch-layer|state|types.ts
src/client/index.tsx 浏览器半(esbuild → lib/client.js)
scripts/build-client.mjs esbuild 配置(模块表闭包契约)
tests/ vitest 套件 + fixtures/ 测试夹具
依赖按"运行时共享安装内单一 Cordis 实例"分类:@deepseek-ai/* 包为
peerDependencies(从 dsh 安装闭包解析),js-yaml 是唯一运行时依赖,构建与
测试工具放在 devDependencies。
已知限制与待办
- 远程信任:
/plugin-manager/*路由对任何能触达服务器的人都开放。Web 面 默认仅回环绑定(launcher 拒绝--host 0.0.0.0),但暴露到局域网时不能依赖 /api 信任围栏的保护;按可信权威校验为待办。 - 与 CLI 混用:管理器把 bundle 装为真实依赖,但不加入
dsh.profile.bundles(行在用户补丁层)。之后若再运行dsh pluginCLI,reconcile 会把声明 dsh.bundle 的依赖并入 bundle 层,下一次启动时双组合这些行。每个 bundle 只用 一种机制(卸载时会自动清理 CLI 残留的 bundle 层条目)。 - 无法解析包名的 spec:安装 git URL 时若无法在 profile 的
node_modules中识别出包名,会以 "could not resolve the installed package name" 明确失败。 - 行 id 冲突在安装时被拒绝:补丁行与用户层(或其他 bundle)冲突的 bundle 不会被安装。
- 更新热应用的是组合与配置,不是模块代码。Node 会在进程内缓存已解析的模块
与包 exports,因此 代码 或 exports map 变更的版本需要重启才生效;manager
的 update 热替换补丁行(行集合与行配置),依赖本身经
pnpm update(registry)或 remove + re-add(file:/link:)刷新。 - 启停按行 id 定位条目;非补丁行 id 的生成式 Loader id 无法被切换。
- pnpm ≥ 11 的 supply-chain 策略:默认
minimumReleaseAge(24h)会拒绝 "刚发布"的包(ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION)。管理器执行 pnpm 时统一附加--config.minimumReleaseAge=0跳过冷却(安装是用户主动 操作),并把违规包的 scope 追加到 profilepnpm-workspace.yaml的minimumReleaseAgeExclude(例如@kyorakuyk/*)作为第二道防线;手动 处理时把对应 scope 加入该列表即可。allowBuilds白名单(git 依赖的 构建脚本)需要你在 profile 的pnpm-workspace.yaml里自行批准——管理器 不自动放行构建脚本。 - 暂无插件市场:安装是 spec 驱动的;浏览与精选发现为待办。
- 向运行中的 profile 部署 bundle 变更需要重启。启动清单里的 bundle rev 与 host 半都在进程激活时冻结:把变更后的代码重装进已运行的 profile,服务端按 请求读取新 client bundle,但 host 仍运行旧逻辑(且启动图保持旧 rev);浏览器 也可能在旧 URL 下缓存旧 bundle。重装 host 代码后请重启实例,并硬刷新一次 浏览器;管理器标签页能容忍错配窗口(对缺失的行状态做了防御)。
- in-box 的"插件清单"标签页是即时快照。它在首次挂载时拉取一次 Loader 快照,之后保持挂载(隐藏),因此不会反映在"插件管理"里做的热生命周期变更 ——需要刷新页面。管理器标签页本身是实时的(每次操作后自动重拉,并有手动 刷新按钮)。
Model Experience
无:管理器不注册任何 prompt、工具、消息或 provider 请求。
KV Cache 影响
无;本包从不组装模型输入。
License
MIT