Bundle
@kyorakuyk/dsh-plugin-manager
Web UI plugin hot lifecycle for DeepSeek Harness: hot install / uninstall / update / enable-disable of profile plugin bundles without restart. Out-of-tree dual-face bundle: host gateway + browser settings tab over a plugin-owned HTTP transport.
- Source
- kyorakuyk
- stars
- 3 stars
- License
- MIT
- Updated
- Updated 4 days ago
Readme
# dsh-hotreload-plugin-manager
中文 | [English](README.en.md)
一个**树外(out-of-tree)**[DeepSeek Harness](https://github.com/deepseek-ai/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`
槽位(id `manager`)的 Web 设置标签页,由 `dsh-client-modules` 发现,并以普通
模块表 bundle(`window.__ModuleLoader__.load`)形式服务。
## 工作原理
全部复用 launcher 已有的机制,因此插件不需要对 dsh checkout 做任何改动:
1. **安装**:在 profile 目录执行 `pnpm add <spec>`。包成为**真实依赖**(进入
profile 的 `node_modules` 与 `package.json`,pnpm 不会剪除它)。管理器读取
该 bundle 的 `dsh.bundle.patch` 行,写入 profile 的用户补丁层
(`cordis.patch.yml`)。
2. **热应用**:直接对根 Include 条目调用 `entry.update`(携带新补丁列表)——
与 launcher 用户补丁 watcher 相同的调用。管理器自己应用自己的写入,因为文件
watcher 可能漏掉紧跟其后的写入事件。
3. **重启持久**:用户补丁层在启动时被重新组合,因此已安装 bundle 在重启后保留,
且**不触碰 `dsh.profile.bundles`**(也就不会双组合行)。
4. **卸载 / 更新**:移除或替换行,并执行 `pnpm remove` / `pnpm update`
(registry 包)或 remove + re-add(`file:`/`link:`——pnpm 的增量重拷不会
物化新增文件)。
5. **启停**:写入用户层禁用行(`{ 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 plugin` CLI **不要对同一 bundle 混用**(卸载会自动清理 CLI
残留的 bundle 层条目)
## 快速开始(其他用户)
前置条件:Node ≥ 22、pnpm ≥ 10、已安装 dsh 并初始化过目标 profile。
**① 直接从 GitHub 安装(推荐)**——无需克隆本仓库:
```sh
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"
```
**② 或者克隆后本地安装**:
```sh
git clone https://github.com/kyorakuyk/dsh-hotreload-plugin-manager.git
dsh plugin --profile web add "file:$(pwd)/dsh-hotreload-plugin-manager"
```
**③ 或者从 npm 安装**(发布后可用):
```sh
dsh plugin --profile web add "@kyorakuyk/dsh-plugin-manager"
```
**装完后必须重启 dsh web 实例**(新 bundle 需要重启才挂载;仓库已包含构建产物 `lib/`,无需先构建),浏览器硬刷新一次(`Ctrl+Shift+R`):
```sh
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 }`。
## 开发
```sh
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/恢复码**:
```sh
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。发布失败可重跑:
```sh
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 plugin` CLI,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 追加到 profile `pnpm-workspace.yaml` 的
`minimumReleaseAgeExclude`(例如 `@kyorakuyk/*`)作为第二道防线。带
构建脚本的 git 依赖(`prepare` 等)会被 `allowBuilds` 白名单拦截
(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` / `ERR_PNPM_IGNORED_BUILDS`):
管理器会从 pnpm 输出提取其要求的**精确 spec**(`包名@git+url#sha`),
自动写入 `allowBuilds` 并重试一次。手动处理时把对应 scope/spec 加入
`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
Install
dsh plugin --profile web add github:kyorakuyk/dsh-hotreload-plugin-manager
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install kyorakuyk-dsh-plugin-manager from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.