dsh-hot-reloader

devnpmMIT

Reload a plugin's source the moment a file changes, without restarting the harness. Covers the third-party plugin case the built-in watcher misses because it skips node_modules.

下载量 / Downloads
489

Install

dsh plugin --profile web add dsh-hot-reloader

Sandbox test

This plugin has not been tested yet. Submit a test →

README

dsh-hot-reloader

改完 DSH 插件源码即刻生效,不用再重启 DeepSeek Harness。

[English](README.en.md) | 中文

dsh-hot-reloader — 改完插件源码不用重启(本仓库)

dsh-session-clock — 按会话授时与空闲间隔

dsh-second-opinion — 卡住时把问题交给另一个独立会话,要一份第二意见

dsh-onebot-companion — QQ 通道一体化:出站沉默守卫 / 行为规则 / 对话节奏 / 人设 / 语音


目录

  • [这是什么](#这是什么)
  • [它到底解决了什么问题](#它到底解决了什么问题)
  • [环境要求](#环境要求)
  • [安装](#安装)
  • [使用](#使用)
  • [配置](#配置)
  • [它是怎么做到的](#它是怎么做到的)
  • [已知限制](#已知限制)
  • [实现说明](#实现说明)
  • [License](#license)

这是什么

dsh-hot-reloader 是一个 DeepSeek Harness(DSH)插件,给插件开发者用。

它盯着你所有 DSH 插件的源码目录,一旦发现文件变化,就在运行中的宿主里把那个插件原地重新加载——你保存文件,一秒左右新代码就生效,不需要重启 DSH。

如果你写过 DSH 插件,多半经历过这个循环:

改一行逻辑 → 重启 DeepSeek Harness → 等它起来 → 手动复现刚才的场景 → 发现改错了 → 再来一遍

这个插件把这个循环压缩成「改,然后看结果」。

它到底解决了什么问题

DSH 自带热重载服务 @deepseek-ai/dsh-hmr,但它对绝大多数插件都不会生效。原因很具体——它有两处硬编码把 node_modules 排除在外:

// dsh-hmr/lib/index.js:201 —— loadDependencies()
if (job.url.startsWith("node:") || job.url.includes("/node_modules/")) return;

// dsh-hmr/lib/index.js:478 —— analyzeChanges()
const isExcluded = (url) => url.startsWith("node:") || url.includes("/node_modules/");

而 DSH profile 安装的插件,全部位于:

~/.dsh/profiles/<profile>/node_modules/<包名>/

于是链路是这样断的:

1. 你改了 .../node_modules/dsh-onebot-qq/lib/split.js

2. 文件事件到达 dsh-hmr,但它的监听根 root 在 base 组合包里被定为 [](只热重载配置,不监听源码),且 ignored 默认含 **/node_modules → 事件根本没进来

3. 即便强行把这两个配置打开,模块 URL 里带 /node_modules/,loadDependencies() 立刻 return,依赖集恒为空

4. 紧接着 dsh-hmr/lib/index.js:543 判断:

   if (!dependencies.some((dep) => this.accepted.has(dep))) continue;   // ← 永远 continue

5. 结论:这个插件永远不会进入重载队列。改代码只能重启。

dsh-hot-reloader 重新实现了这条链路,不排除 node_modules。

需要说明的是:这不一定是 dsh-hmr 的 bug——它服务于 profile 配置与宿主自身模块的热重载,排除 node_modules 是保守且合理的选择。只是「用户自己装的第三方插件」这个场景没有被覆盖,本插件补的就是这块。

顺带一提:浏览器侧本来就能热重载

DSH 的 Web 客户端有一套独立的 HMR(@deepseek-ai/dsh-client-hmr),走 /plugins/events SSE 通道。客户端代码的改动原本就不需要重启。

真正的痛点在 host 侧(lib/index.js 这类服务端代码),这正是本插件的目标。

环境要求

DSH 宿主

项 · 版本

DSH · 0.2.0-rc.2(实测于该版本)

Electron · 44.0.0

Node(宿主内嵌) · 24.18.1

V8 · 15.2

Cordis · 4.0.4

前置条件(本插件比别的插件更挑环境,务必先读)

1. 宿主必须带 --expose-internals 启动。

模块替换依赖 Node 的 loader 内部结构。插件构造函数里有这条硬检查:

if (!this.ctx.loader.internal) throw new Error('--expose-internals is required for HMR service')

DSH 桌面版的宿主进程默认就带这个旗标(实测命令行):

DeepSeek Harness.exe --expose-internals ...dsh-desktop-host/lib/index.js ...。

所以正常情况下无需你操心。但如果宿主没带,本插件无法工作——

这不是可以绕过的配置项,是 Node 的模块系统边界。

2. 建议同时启用宿主内置的 @deepseek-ai/dsh-hmr(可选,但推荐)。

本插件复用它的实例:解析模块走它的 _resolve(),替换动作走它的

runExclusive() 事务(避免和内置 HMR 撞车)。

const hmr = ctx.get('hmr')
if (hmr && typeof hmr._resolve === 'function') { /* 用它的解析 */ }
// ...
if (hmr && typeof hmr.runExclusive === 'function') { await hmr.runExclusive(run) }

两条路都有自实现兜底:拿不到 hmr 时退回 ctx.loader.internal.resolveSync

与直接执行,插件照常工作。所以它是优化而非硬依赖。

不过它默认就是启用的,而且它的问题正是本插件存在的理由:

# 内置 HMR 在 dsh-base/cordis.patch.yml 里是这样挂的:
- id: hmr
  name: '@deepseek-ai/dsh-hmr'
  disabled: !!js "!ctx.get('profileContext')"   # 有 profileContext 时启用
  config:
    root: []                                     # ← 只重载 profile 配置,不监听源码模块

root: [] 意味着内置 HMR 不监听任何源码目录;而且它有两处硬编码排除 node_modules

(loadDependencies 与 analyzeChanges),第三方插件永远进不了它的依赖图。

本插件补的就是这块,同时借用它的事务机制。

3. 装在 profile 侧,目录必须在 profile 的 node_modules 之外(或走 link 安装)。

link 安装(link:D:/path/to/plugins/...)是被支持的路径。

若你直接把包放进 node_modules,升级会覆盖它——和所有 profile 插件一样。

4. 零外部依赖。 只用 node:fs / node:path / node:url / node:module。

这不是洁癖:profile 的 node_modules 里解析不到 chokidar / picomatch

(只有 cosmokit、schemastery 等极少数包),这是「为什么用轮询」的根本原因。

不需要的东西

  • 不需要重启 DSH 就能让本插件生效(宿主内置 HMR 会 reconcile profile 清单)。
  • 不需要额外端口,走宿主的 webServer。

安装

dsh plugin --profile <你的profile> add link:<本目录绝对路径>

例如:

dsh plugin --profile desktop add link:D:/path/to/plugins/dsh-hot-reloader

从 npm 安装(如果你是用发布版):

dsh plugin --profile <你的profile> add dsh-hot-reloader

装完之后,profile 清单(dsh.profile.bundles)会被自动更新。宿主自带的 HMR 监听到清单变化后会自己 reconcile —— 这一步同样不需要重启,插件即刻开始工作。

使用

装好之后什么都不用做。 改任意已安装插件的 lib/*.js,一秒左右自动热重载。

重载日志会直接打到宿主日志;也可以随时查状态接口。

观测与手动控制

方法 · 路径 · 用途

GET · /dsh-hot-reloader · 完整状态:监听了哪些包、扫了多少文件、扫描/重载/失败计数、近期日志

POST · /dsh-hot-reloader/reload · 手动重载。body 留空 = 全部;支持 ["包名"] 或 {"packages":["包名"]}

POST · /dsh-hot-reloader/toggle · 暂停 / 恢复自动重载

POST · /dsh-hot-reloader/self-reload · 重载本插件自身(开发这个插件时用)

# 看状态
curl http://127.0.0.1:19387/dsh-hot-reloader

# 只重载某个插件
curl -X POST http://127.0.0.1:19387/dsh-hot-reloader/reload \
     -H 'content-type: application/json' \
     -d '["dsh-onebot-qq"]'

# 全部重载一次
curl -X POST http://127.0.0.1:19387/dsh-hot-reloader/reload

# 临时关掉自动重载(例如你正在做大面积重构,不想每存一次就重载一次)
curl -X POST http://127.0.0.1:19387/dsh-hot-reloader/toggle

GET /dsh-hot-reloader 的返回大致长这样:

{
  "ok": true,
  "plugin": "dsh-hot-reloader",
  "enabled": true,
  "uptimeMs": 55659,
  "scannedFiles": 227,
  "scans": 55,
  "reloads": 3,
  "failures": 0,
  "lastReloadAt": 1791805031234,
  "lastError": null,
  "watch": ["D:\\path\\to\\plugins\\dsh-whale-widget", "..."],
  "packages": [
    { "root": "...", "entries": ["dsh-onebot-qq", "dsh-onebot-qq/settings-remote"] }
  ],
  "recent": [{ "at": 1791805031234, "pkg": "dsh-onebot-qq", "files": 1, "ms": 35, "ok": true }],
  "logs": [{ "at": 179180503

…(truncated)

Rendered from the README published on npm. Formatted only, not edited.

Versions

Published on npm::3,最后更新 2026-11-01

Links

GitHub · Homepage · npm

Same category