🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

重载按钮 accoutmissing/dsh-restart-button

DeepSeek Harness
accoutmissing3 天前有提交已提供中文文档扫描:含敏感能力

在侧边栏一键热重载插件更新,必要时才完全重启

29.7/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add accoutmissing/dsh-restart-button
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
dsh 原生插件 · desktop
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 3 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 20 处,展示前 16 条)
  • 写入本地文件
    index.js:95
    if (statSync(LOG_FILE).size > LOG_MAX_BYTES) renameSync(LOG_FILE, `${LOG_FILE}.1`)
  • 读取本地文件
    index.js:95
    if (statSync(LOG_FILE).size > LOG_MAX_BYTES) renameSync(LOG_FILE, `${LOG_FILE}.1`)
  • 写入本地文件
    index.js:99
    appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${oneLine(text)}\n`, 'utf8')
  • 读取本地文件
    index.js:184
    if (typeof exe !== 'string' || exe === '' || !existsSync(exe)) return undefined
  • 读取本地文件
    index.js:369
    const runs = readdirSync(root, { withFileTypes: true })
  • 删除本地文件递归
    index.js:376
    rmSync(join(root, name), { recursive: true, force: true })
  • 写入本地文件
    index.js:400
    mkdirSync(directory, { recursive: true })
  • 写入本地文件
    index.js:418
    writeFileSync(helperPath, script, 'utf8')
  • 写入本地文件
    index.js:419
    writeFileSync(bootPath, bootSource(helperPath), 'utf8')
  • 写入本地文件
    index.js:420
    writeFileSync(launcherPath, launcherSource(shortcutPath, helperPath, bootPath), 'utf8')
  • 执行外部命令来自配置/变量
    index.js:432
    const child = spawn(SHELL, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', launcherPath], {
  • 读取本地文件
    index.js:443
    events.push(`exit code=${String(code)} signal=${String(signal)} lnk=${existsSync(shortcutPath)}`)
  • 读取本地文件
    index.js:480
    return statSync(join(here, 'client.js')).mtimeMs
  • 读取本地文件
    index.js:539
    return statSync(join(here, 'index.js')).mtime.toISOString()
  • 发起网络请求
    client.js:139
    const response = await fetch(SOFT_ROUTE, { method: 'POST', headers: HEADER })
  • 发起网络请求
    client.js:168
    const response = await fetch(HARD_ROUTE, { method: 'POST', headers: HEADER })
扫描于 2026/11/3(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

DSH 桌面版的一键重载/重启按钮:插件更新后原地热重载,热重载不成立时才完全重启。

README

由 DeepSeek 最新模型翻译生成
dsh-restart-button 简体中文 · English 一个重载 / 重启按钮,装在侧边栏底部,给 DeepSeek Harness 桌面版用。 装插件、更新插件,过去都得关掉应用再打开一次。这个插件在侧边栏放了一个按钮替你做这件事——更重要的是,它尽量不重启。 点击 -> 原地热重载插件 (正在跑的东西一样不丢) +-- 热重载不成立 -> 按钮变成「完全重启」再点一次 -> 应用重新拉起 为什么不干脆重启? 因为 app.relaunch() 没有它看起来那么便宜。 替换插件包之后,Harness 自己的 HMR 层可以在运行中的宿主进程内部驱逐已变更的模块再把它们启动起来。正在跑的轮次、子代理、终端、排队中的消息、还没发出去的草稿——全都在,因为什么都没被杀掉。 完全重启是「这件事不可能做到时」的兜底(模块驱逐不掉、包在磁盘上挪了位置),不是默认动作。 所以按钮默认走便宜的那条路,只有当 Harness 明确报告必须重启时才提供贵的那条——hmr.reconcileProfile() 返回 restartReason: 'module-comparison' | 'package-moved' 就属于这种情况。 安装 这是一个 Cordis bundle。
展开完整文档(README 剩余 1 节)
dsh-restart-button

简体中文 · English

一个重载 / 重启按钮,装在侧边栏底部,给 DeepSeek Harness 桌面版用。

装插件、更新插件,过去都得关掉应用再打开一次。这个插件在侧边栏放了一个按钮替你做这件事——更重要的是,它尽量不重启。

点击 -> 原地热重载插件 (正在跑的东西一样不丢) +-- 热重载不成立 -> 按钮变成「完全重启」再点一次 -> 应用重新拉起

为什么不干脆重启?

因为 app.relaunch() 没有它看起来那么便宜。

替换插件包之后,Harness 自己的 HMR 层可以在运行中的宿主进程内部驱逐已变更的模块再把它们启动起来。正在跑的轮次、子代理、终端、排队中的消息、还没发出去的草稿——全都在,因为什么都没被杀掉。

完全重启是「这件事不可能做到时」的兜底(模块驱逐不掉、包在磁盘上挪了位置),不是默认动作。

所以按钮默认走便宜的那条路,只有当 Harness 明确报告必须重启时才提供贵的那条——hmr.reconcileProfile() 返回 restartReason: 'module-comparison' | 'package-moved' 就属于这种情况。

安装

这是一个 Cordis bundle。先从源码检出:

git clone https://github.com/accoutmissing/dsh-restart-button.git

然后用那个目录安装 bundle(Harness 通过它的插件管理器暴露这个能力;由 agent 驱动时就是 plugin_manager 工具):

{ "action": "install_bundle", "target": "D:/path/to/dsh-restart-button" }

安装成功会返回 application: "applied"。按钮立刻出现在侧边栏底部——安装它本身不需要重启。

以后更新:git pull,然后按按钮。

使用

| 按钮显示 | 含义 | 点击会发生什么 |
| ⟳ 刷新插件 | 空闲 | 原地热重载已变更的插件代码 |
| ⟳ 正在刷新… | 进行中 | — |
| ✓ 已刷新 | 完成 | 3 秒后回到空闲 |
| ⟳ 完全重启 | 热重载报告无法原地完成 | 杀掉应用并重新拉起 |
| ⚠ 刷新失败 | 出错(同时写日志) | 3 秒后回到空闲 |

实现

宿主侧(index.js)

通过 webServer 注册三条路由:

| 路由 | 方法 | 用途 |
| /plugins/dsh-restart-button/__state | GET | 自描述:版本、代码代号、能力、pid |
| /plugins/dsh-restart-button/__reload | POST | hmr.runExclusive(() => hmr.reconcileProfile()) |
| /plugins/dsh-restart-button/__restart | POST | 完整 kill 后重新拉起 |

热重载这条路套在 hmr.runExclusive() 里,因为 reconcileProfile() 参与宿主的串行队列;与自动重载撞车时会冒出一个 transactions cannot be nested,这个错误被识别并退避重试(3 次,400 ms × n)。另加 20 秒上限,免得一次卡死的重载把按钮永久占住。

客户端(client.js)

挂在 sidebar.footer.action 上的状态机,order 90。它的状态放在模块级 store,而不是组件 state 里:这个插槽在每次 store 更新时都会被重新投影,组件随之重挂载。用组件 useState 的话,两次点击之间状态会被打回 idle;模块级 store 活得下来。

判断新的客户端代码有没有真的加载

宿主返回 clientStamp——client.js 的 mtime。页面在模块求值的那一刻记下 LOADED_AT。如果 clientStamp > LOADED_AT + 1000 ms,说明这个页面跑的是旧的客户端代码,等插件字节确认是新的之后它自己整页重载。

第一版实现比的是 clientModules.graph().rev。那是错的:rev 只在 HMR 监视器登记重建时才前进,手改 client.js 图表纹丝不动,页面会被永远判成「最新」。现在改成比文件戳。

启动链路(Windows)

这是耗时最长的一部分,也是你打算做类似事情时最值得读的一部分。

插件跑在宿主进程里,而宿主是 Electron 的普通 Node 子进程(ELECTRON_RUN_AS_NODE=1)。它调不到 app.relaunch()。所以重启被交给操作系统:

宿主插件 └─ powershell launch.ps1 生成 .lnk,请 explorer.exe 打开它 └─ explorer.exe 持有快捷方式的启动 └─ wscript.exe 快捷方式目标;GUI 子系统,不建控制台 └─ boot.vbs WScript.Shell.Run "powershell ... ", 0, False └─ powershell restart.ps1 杀应用、等端口释放、重新拉起

四条约束塑造了它,每一条都是撞出来的:

1. detached: true 在这里不能用。 宿主跑在带 KILL_ON_JOB_CLOSE 的 Windows Job Object 里;映射成 DETACHED_PROCESS 的进程在创建瞬间就被终止——没有报错、没有日志、进程表里查不到。启动器必须用普通方式 spawn,然后自己退出。
2. explorer.exe 必须是父进程。 宿主 spawn 出来的任何东西都会继承那个 job object。经过 explorer 一环才能断链。(这也是为什么启动器是一个 .lnk 而不是直接 Start-Process。)
3. 快捷方式必须指向 wscript.exe,不是 powershell.exe。 wscript 是 GUI 子系统程序,永远不分配控制台,所以屏幕上不会闪黑框。实测:指向 powershell 恰好多出一个 CASCADIA_HOSTING_WINDOW_CLASS 窗口;指向 wscript 多出零个。
4. .lnk 的 Arguments 必须用双引号。 用单引号包路径会让 explorer 静默什么都不做——没窗口、没报错、没日志。而真实可执行文件路径里带空格,所以这条是必须修的。

helper 还会在目标进程的 .Path 与预期可执行文件不匹配时拒绝执行,因此它不可能杀错应用。

安全

两条 POST 路由都要求请求头 x-dsh-restart-button 以及 loopback 来源地址。

真正承重的是那个请求头。只查 loopback 只能证明请求来自本机——你访问的任何网站上的一个表单都能 POST 到 127.0.0.1,而一次重启对你正在做的事情就是拒绝服务。自定义请求头在跨源时无法携带,除非有一次服务端永远不会批准的预检,所以真正拦住它的是请求头检查。对写路由发 GET 会得到 405,不是重定向也不是状态页。

日志与临时文件

- ~/.dsh/restart-button.log —— 每次动作一行,到 1 MB 轮转成 .1。每一行都做过消毒:控制字符换成空格,恶意的字段无法伪造日志条目。
- %TEMP%\dsh-restart\<timestamp>\ —— 每次调用生成的 restart.ps1、boot.vbs、launch.ps1 和 launch.log。只保留最近 20 个。

重启路由加 ?dry=1 只生成脚本不执行。加 ?probe=1 会用替身 helper(只记一行日志然后退出)跑完整条启动链路——用于在不真正杀掉应用的前提下验证链路仍然可用。

已实测的行为

| 声明 | 证据 |
| 热重载真的换了代码 | 模块级代号常量在三次修改中依次走过 undefined → gen-1 → gen-2 → gen-3,每次修改后各发一次 POST __reload(200,warnings: 0,pending: 0) |
| 不出现控制台窗口 | 用 EnumWindows 统计可见控制台窗口,前后相比 delta = 0 |
| 完全重启可用 | 自测脚手架:旧 pid 死、新 pid 活;在真实应用里也能从 GUI 端口变化看出来 |
| 完全重启不是黑盒 | 每次运行都有 launch.log + restart.log 记录每一步 |
| CSRF 守门有效 | 不带请求头的 POST:403 missing x-dsh-restart-button header |
| 路由方法守门有效 | 对两条写路由发 GET:405 |

完全重启只在此前的 Windows 上验证过——启动链路本身只针对这个平台。热重载用的是有文档的宿主服务,不限于 Windows,但尚未在其他平台测过。

与 dsh-unlag 的关系

dsh-unlag(作者 regrevia,MIT)用另一种取舍解决同一个底层问题。读它改变了本插件的设计,这笔账具体到能点名。

设计对照的是 dsh-unlag v1.14.16。这不是 fork,没有共同提交历史;是独立实现。

从阅读中吸收的部分——前五项已经接近到该算「借用」而不是「各自想到」:

| 借用 | 在本插件中的位置 |
| transactions? cannot be nested 的重试机制,含正则 | index.js 的 isNestedTransaction() |
| RELOAD_TIMEOUT_MS = 20_000、LOG_MAX_BYTES = 1_000_000 | index.js 常量 |
| oneLine() —— 每个字段进日志行之前先消毒 | index.js 的 oneLine() |
| 用自定义请求头做写路由的 CSRF 守门 | BUTTON_HEADER / fromOwnPage() |
| 热重载优先,而不是当兜底 | softReload(),以及按钮的默认动作 |
| 用代号常量作为「新代码到底有没有加载」的唯一诚实答案 | BUILD |

未采纳: 它的 /plugins 路由 shim。dsh-unlag 为过期版本提供替身模块,因为桌面壳在启动时冻结启动清单,旧页面按旧 revision 请求会 404 并把应用以退出码 1 带走。本插件改走整页重载,所以没有旧客户端代次需要续命——也就不需要在每次上游变更时维护一层平台并不认可的兼容层。

除此之外的一切——启动链路、job object 逃逸、wscript 快捷方式、杀掉再等待再拉起的序列——dsh-unlag 里完全没有。它从不重启。

许可证

MIT
预览分享卡片长什么样 ↓accoutmissing/dsh-restart-button
分享 · DeepSeek Harness Hub
accoutmissing/dsh-restart-button

在侧边栏一键热重载插件更新,必要时才完全重启

这是你的仓库?认领条目

回到顶部