Back to home

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,一个注册为 pluginManagerTypertRemoteService。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_modulespackage.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-metadsh-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.gitgit+ssh://…
  • GitHub 简写:github:owner/repoowner/repo
  • 本地路径:file:C:/path/to/bundlelink:…(开发/离线最常用)

插件内部可以是什么(补丁行由根 Include 的插值机制统一处理):

  • Host 插件:普通 Cordis 插件、Service、工具注册
  • 浏览器 UI 插件:双面 dsh.client 包(浏览器半经 dsh-client-modules 可用)
  • skill 提供方 / MCP 适配器:挂 dsh-skill-filesystemdsh-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 安装(推荐)——无需克隆本仓库:

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 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.yamlminimumReleaseAgeExclude(例如 @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