← 返回列表
未验证
升级插件后免重启 dsh,失败自动回滚
尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/8/22 · 已提供中文文档
无需重启dsh即可热重载升级后的DeepSeek Harness (dsh)插件——安全地原地重载,失败的重载会回滚并标记需要重启。
综合分
28.2
GitHub 分
28.2
用户评分
—
★ Stars
1
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add stuarthu/dsh-hot-reload该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
dsh-hot-reload
实时重载升级后的 DeepSeek Harness (dsh) 插件,无需重启 dsh。
dsh 内置的热重载(cordis-plugin-hmr)会刻意忽略
node_modules,因此升级已安装的插件(dsh plugin add pkg@x)通常需要完整重启 dsh 才能生效。本插件填补了这一空白:它监视你 profile 中的 pnpm-lock.yaml,当已加载插件包的版本发生变化时,它会就地替换正在运行的插件。
行为
在插件包升级时,对每个受影响的插件:
- 实时重载它 —— 使模块缓存失效,重新导入新代码,并就地重新实例化插件 fiber。dsh、你的会话以及所有其他插件都保持运行。
- 如果重载失败 —— 你会保留可用的旧版本,绝不会得到一个死掉的插件,同时会记录一条说明,提示需要手动重启 dsh 才能加载新代码。两种情况均已处理:
- 加载新代码时失败(导入错误、语法错误)会在触及正在运行的插件之前被捕获 —— 旧插件绝不会受到干扰;
- 初始化它时失败(新的 apply 抛出异常,同步或异步)会被回滚 —— 旧版本会被就地重新实例化。
失败的版本不会自动重试 —— 重试会在之后每次 lockfile 写入时再次拆掉可用的插件。安装一个不同的版本,或重启 dsh,才能加载新代码。
有两种情况按设计不会触发重载:
- 已禁用的插件行会被静默跳过。 已禁用的插件并未运行,因此没有可替换的对象 —— 而且重新启用它时,dsh 无论如何都会加载新代码。
- 尚无活跃 fiber 的插件(仍在导入中,或之前加载失败)会被报告为 no live fiber to reload right now 并被搁置。由于没有拆掉任何东西,因此它会在之后每次 lockfile 变化时被重新检查,并且一旦出现正在运行的副本,它就会自行重载。你只会每个版本收到一次通知,而不是每次检查都通知 —— 因此,如果在插件已有足够时间启动后你仍看到同一版本被报告,请重启 dsh。
升级 dsh-hot-reload 自身
dsh-hot-reload 也能热重载它自己。当它自身的包被升级时,正在运行的实例会导入新模块,提交并持久化自己的版本,关闭自己的监视器,然后就地替换自己的 fiber。新实例会重新读取状态文件并打开自己的监视器 —— 旧监视器在新监视器打开之前就已关闭,因此绝不会同时存在多个活跃监视器。
为使这一交接 —— 以及普通重启 —— 安全,该插件将其跟踪的状态保存在一个文件中:profileDir/.dsh-hot-reload-state.json。它保存已提交的 versions、永不重试的 failedVersions,以及仅通知一次的 noticedVersions。该文件以原子方式写入 —— 先写入一个 .tmp 文件
然后重命名覆盖目标文件,因此写入中途崩溃永远不会留下被截断的文件——并在启动时读回,这样新实例会继承上一个实例已提交的内容,而不是把已经加载的插件当作新插件并重新加载它们。
这个状态文件是必需的。如果启动时无法写入它,插件会拒绝启动(抛出异常),而不是仅以内存中的状态运行,因为之后的自我重载或重启会丢失这些状态。
它永远不会替你重启 dsh——重启留给你(以及你的 supervisor,如果有的话)。
你如何看到发生了什么
插件会把每个结果写入 dsh 的日志。但 dsh 不会把日志打印到你的终端,所以那些行很容易被忽略。有两个额外的地方向你展示发生了什么。
1. 你的终端中的一行,针对每个结果。 你在每个 profile 中都会得到这一行:
dsh-hot-reload: hot-reloaded some-plugin@1.2.0 (1 module(s))
2. dsh web 应用中的一条简短弹出消息。 当重载成功时你会得到一条。在新代码没有加载的每种情况下你也会得到一条,因为旧代码仍在运行:
- 重载失败,旧版本被放回
- 插件没有正在运行的副本可供替换
- 插件通过 dsh.hotReload: false 关闭了热重载
- dsh 没有提供重载所需的内部部件
第二种情况每个版本只通告一次,而不是每次检查一次。该情况会在之后每次 lockfile 写入时重试——包括其他包的写入——所以如果没有这个限制,你会一遍又一遍地收到同样的弹出消息,且无法关闭它。其他三种情况每次发生时都会通告。
消息会滑入,停留几秒钟,然后淡出。如果一次升级重载了多个插件,这些消息会排队并依次显示。
web 部分只会在运行 web 服务器的 profile 中加载。它通过 GET /dsh-hot-reload/events 发送消息。没有 web 服务器的 profile,例如 tui,仍然会得到终端行和日志。
消息不会被保存。如果重载发生时没有打开浏览器标签页,那条消息就消失了。日志中仍有记录。
如果你在打开标签页的情况下重启 dsh,标签页会自行打开一个新通道,弹出消息会继续工作。它会尝试大约三分钟。如果之后仍然无法连接,它会向浏览器控制台写入一行并停止尝试;重新加载页面即可重新开始。
如果你想让每一行都出现在终端中
上面的终端行覆盖了每个重载结果——已重载、失败和过期(未尝试——旧代码仍在运行)。要查看此插件写入日志的所有其他内容(其警告和诊断信息),请将 dsh 的控制台 logger 添加到你的 profile。它是一个单独的包:
dsh plugin --profile web add @deepseek-ai/cordis-plugin-logger-console
然后在该 profile 的 cordis.patch.yml 中为它添加一行并重启 dsh:
- insert:
- id: logger-console
name: '@deepseek-ai/cordis-plugin-logger-console'
这会打印所有 dsh 日志行,而不仅仅是此插件的日志行。
全屏配置文件的注意事项。 终端行会直接写入
屏幕。在绘制全屏界面的配置文件中,例如 tui,该
行可能会落在绘制区域的中间,使屏幕看起来异常。它
只在屏幕再次绘制之前看起来异常。
安装
dsh plugin --profile web add dsh-hot-reload
然后重启一次 dsh(bundle 补丁层在启动时加载)。之后,升级
即可实时生效:
dsh plugin --profile web add some-plugin@newer # 自动重新加载
适用于任何配置文件——将 web 替换为你使用的任意配置文件;它会监视
自己加载到的那个配置文件。
兼容性
基于 dsh 0.1.0-rc.6(Node 22 / 24)构建并测试。它深入
cordis/loader 内部——大多与 cordis-plugin-hmr 使用的相同——因此
未来若 dsh 更改其中任何一项,可能需要更新:
| 内部项 | 用途 |
|---|---|
| loader.internal.loadCache | 使 ESM 模块缓存失效 |
| loader.internal.resolve / resolveSync | 将说明符解析为 URL(根据 internal.version 分派) |
| loader.import / loader.unwrapExports | 重新导入新模块并解包其插件导出 |
| registry.plugin / registry.delete | 替换插件实例 |
| fiber.entry、fiber.runtime | 将新插件重新附加到正在运行的行 |
| entry.disabled | 跳过已禁用的行(继承的 getter) |
| entry.options.group | 跳过组容器行 |
Web 应用中的弹出消息(且仅该部分)还使用:
| dsh 部分 | 用途 |
|---|---|
| ctx.webServer.register | 提供消息通道 |
| window.__ModuleLoader__ | 加载浏览器端部分 |
| shell.overlay 插槽 | 将消息放置在应用之上 |
| 来自 @deepseek-ai/dsh-client-ui-primitives 的 Toast | 绘制它 |
该插件会安全失败。如果缺少它所需的某个部分,它会报告“需要重启”
而不是破坏 dsh。弹出消息的行为也是如此。缺少 Web 服务器、
无法加载的浏览器模块、未知插槽、重复注册,或
没有 Toast 的 dsh 构建,都只会让你失去弹出消息。重新加载仍然有效,
Web 应用也仍然会启动。
一个例外:浏览器端部分会向 dsh 请求一个名为 slots 的服务。dsh 的 Web
应用会在任何插件始终无法就绪时拒绝启动。因此,如果未来某个 dsh
构建完全没有 slots 服务,这部分将永远等待,并出现在
dsh 的启动错误列表中。上面列出的其他所有失败都会被捕获,并且只是
什么都不做。
选择退出
知道自己不适合热重载的插件,可以通过在自己的 package.json 中声明以下内容,
强制走需要重启的路径(不尝试重新加载):
{ "dsh": { "hotReload": false } }
配置
在你的配置文件的 cordis.patch.yml 中的 hot-reload 行上设置:
| 键 | 默认值 | 含义 |
|---|---|---|
| debounce | 300 | 锁文件更改后等待多少毫秒再执行操作 |
| profileDir | auto | 要监视的 profile 目录的绝对路径(若省略,则从 loader 基础 URL 自动检测) |
限制——请阅读
此插件是乐观的,而非经过验证的。它会尝试重载,只有在某些东西抛出异常时(或者没有可交换的活跃 fiber 时)才回退到“需要重启”。它不会检测静默泄漏:
- 一个在 cordis 之外获取原始资源的插件——一个裸的
setInterval、一个 net/http 服务器、一个 WebSocketServer、一个 fs.watch、
一个 child_process——没有 ctx.effect 清理器,可以重载而不抛出异常,却让该资源悬空(一个游离的定时器、一个重复的监听器、一个孤立的监视器)。这些会在多次升级中累积,只有最终重启才能清除。
- Cordis 会自动解开插件通过 ctx 注册的一切
(ctx.effect、ctx.on、ctx.provide、工具 schema、适配器),因此
行为良好的插件可以干净地重载。风险仅限于绕过 ctx 的插件。如有疑问,让此类插件设置 dsh.hotReload: false。
- 重载一个持有活跃连接的插件(例如 WebSocket 桥接)会断开并重新建立这些连接;客户端必须重新连接。这是预期行为,不是
错误。
- 重载路径依赖于 兼容性 下列出的 cordis/loader 内部机制。如果它们不可用(没有
--expose-internals 且没有 node-addon-require-builtin 插件),该插件会退化为对每次更改都报告“需要重启”,而不是重载。
- 锁文件只是触发器。版本号是从每个
包已安装的 package.json 中读取的,因为那是唯一能说明一次导入真正会得到什么的文件。在 pnpm 11 上(在 11.21.0 上实测),磁盘上的文件是先写入的,锁文件最后写入,因此当此
插件执行操作时,它读取的版本已经稳定。但这里没有任何东西检查
这一点。如果未来某个 pnpm 先写入锁文件,一次检查可能会读到旧
版本,跳过它,并且再也不看——锁文件是唯一被
监视的东西,所以那次升级会被静默地错过,没有任何消息,直到
你安装另一个版本或重启 dsh。debounce 设置没有
帮助:实测间隔为 2.5–4 秒,远长于任何合理的防抖时间。
- 消息通道(GET /dsh-hot-reload/events)没有密码检查,
与 dsh 自己的 /plugins/events 相同。它会发送插件名称和版本
号。dsh 已经通过其插件列表显示这些信息,所以这没有增加新的
秘密。但如果你将 dsh 绑定到 0.0.0.0,就把它算作又一个你的网络上任何人都可以打开的地址。
- 一个在 bundle 顺序中于 dsh-hot-reload 之后加载的插件,会在启动后第一次锁文件写入时被重载一次——重载到其当前版本——即使那次写入是针对一个无关的包。在此插件跨一个周期跟踪该
包之前,它无法判断正在运行的代码是旧版本还是
当前版本,因此它会重新加载,而不是采用一个可能从未运行过的版本。对于 HMR 安全的插件来说,这只是一次无害的冗余重载。
- 状态文件是必需的。插件会将其跟踪的状态写入
profileDir/.dsh-hot-reload-state.json。如果该文件无法写入——例如因为
profile 目录是只读的——插件会拒绝启动(抛出异常),而不是仅以内存中的状态运行。
范围说明:这处理的是已加载插件的升级。安装一个全新的插件是另一回事(将其行添加到 cordis.patch.yml,dsh 已经会热应用该文件)。
许可证
MIT © Stuart Hu同作者(stuarthu)的其他插件
扫码进群