# 设计说明:把「刷新 / 重启」放进 DSH 桌面端的「应用」菜单 > **插件只是载体,补丁才是本体。** 这个包不带任何 UI,它带的是: > 1. 一个能直接干活的工具 `dsh_desktop_menu`(status / apply / revert); > 2. 完整的推导过程(本文)与给 AI 的照抄步骤([RECIPE.md](RECIPE.md)); > 3. 等价的独立 Python 脚本([standalone/](standalone/)),不装插件也能照做。 > > 换一台装了 DSH 桌面端的电脑,AI 读这两页 + 调工具,就能把那两项写进「应用」菜单。 ## 1. 事实:菜单在哪、为什么默认没有这两项 DSH 主界面左上角的「应用」按钮**不是 Web 插件渲染的**,而是 Electron 桌面壳的原生菜单: | 层 | 位置 | 说明 | | --- | --- | --- | | 按钮条(应用 / 编辑) | `app.asar:lib/preload-app.cjs` → `installWindowsMenu()` | 往页面里挂一个 `[data-windows-menu]` 影子 DOM,点击时发 IPC | | 菜单内容 | `app.asar:lib/main.js` → `applicationItems()` | 主进程用 `Menu.buildFromTemplate(...).popup()` 现场构建 | | 菜单项里的两项 | 同上 | `刷新页面` (`role: "reload"`)、`重启应用与 Host` (`app.relaunch() + quitWithoutConfirmation()`) | 这两项**代码本来就有**,但包在开发构建开关里: ```js ...development ? [ { type: "separator" }, { label: ...reloadPageMenu, role: "reload" }, { label: ...restartAppHostMenu, click: () => { app.relaunch(); quitWithoutConfirmation() } }, ] : [], ``` `development = !app.isPackaged`,正式安装版恒为 `false`,所以永远不显示。 ## 2. 补丁:11 字节等长替换 ```diff - ...development ? [ + ...!(0 === 1) ? [ ``` - `development` 与 `!(0 === 1)` **都是 11 字节** ⇒ asar 头的 `json_len`、每个文件的 `offset`、归档总大小全不变,**不需要重新打包**; - 语义不变:`!(0 === 1)` 恒为 `true`,数组照常展开; - 还需要同步一件事:asar 头里 `/lib/main.js` 的 `integrity.hash`(同文件 `blocks[0]` 共 2 处,都是 64 位十六进制)。改完内容后用新 SHA256 **等长**替换即可。 (Electron 的 `EnableEmbeddedAsarIntegrityValidation` 只在 macOS 强制;Windows 上同步哈希是保险,不影响运行。) - 幂等:脚本按内容定位 `...development ? [ `,找不到或出现多次就拒绝写入。 改完之后,`main.js` 只在**主进程启动时**加载一次 ⇒ 必须重启桌面端才生效。 ## 3. AI 在一台新电脑上的标准流程 1. 装本插件(`plugin_manager` → `install_bundle`,target 为本包绝对目录),重启一次 DSH; 2. 调 `dsh_desktop_menu`: - `{ "action": "status" }` → 看 `asar` 路径、`patched`、`hashConsistent`; - `{ "action": "apply" }` → 写入补丁(返回 `verified.patched === true`); - 让用户从托盘退出再打开桌面端 → 生效; 3. 之后「应用」菜单里就有 `刷新页面` / `重启应用与 Host`; 4. 桌面端自动更新会换掉 `app.asar`,补丁消失 ⇒ 更新后再 `apply` 一次; 5. 不想要了:`{ "action": "revert" }`。 `app.asar` 自动探测顺序:参数 `asar` → 环境变量 `DSH_DESKTOP_ASAR` → 从运行中的可执行文件逐级向上找 `/resources/app.asar`。纯浏览器版(`dsh web`)没有这个文件,`status` 会明确返回 `not-desktop`。 ## 4. 为什么这个包没有「标题栏按钮」 早期版本在窗口标题栏里注入过「刷新 / 重启」两个按钮,最后**整个删掉**,原因值得写下来: 1. **没有可用的插槽。** 标题栏是 Electron 的窗口 chrome(preload 的影子树),Harness 唯一近似的插槽 `shell.leading` 只在侧边栏完全隐藏时挂载,且被 ui-sidebar 占用。要放按钮只能往 chrome 里 DOM 注入, 属于非官方扩展点,随版本变化会飘。 2. **插件做不了真正的重启。** 页面没有 relaunch/quit 的 IPC 通道;只能起外部进程杀自己再拉起, 而这条路在本机被两道墙挡住: - 隐藏 PowerShell 被**火绒「系统加固」的 `隐藏执行PowerShell` 规则**在进程创建阶段拦截 (`restart.log` 里连一行都没有); - 改用 `taskkill /PID … /T /F` 时,整树结束会顺着父子链把助手自己也杀掉; - 再改用 node 助手按 PID 杀主进程+宿主,应用确实关了,但新实例没能起来。 3. **原生菜单本来就是正解。** `role: "reload"` 与 `app.relaunch() + quitWithoutConfirmation()` 是 Electron 自己的实现,补丁一打就能用,不需要任何外部进程 —— 所以只在「应用」菜单里加,不在标题栏加。 ## 5. 已知边界 - 原生菜单补丁只对 **Electron 桌面端**有意义;纯 Web 版没有「应用」菜单。 - 补丁改的是安装目录文件:需要当前用户对该目录有写权限(装在 Program Files 时可能要管理员)。 - 宿主进程是 `ELECTRON_RUN_AS_NODE` 的 Electron,普通 `node:fs` 打不开 `app.asar` 本身 (会报 `ENOENT, not found in `),必须用 `original-fs`;且插件模块**不能有顶层 `await`**, 否则入口会卡在 `loading` 让 web boot 失败(2026-10-21 的崩溃复盘)。 ## 6. 验证 - `node test/roundtrip.mjs`:拿真的 `app.asar` 复制一份,`revert → apply → apply(幂等)`, 断言 SHA256 回到 `6f3d1ddd…` / `26f589ee…`,归档大小不变; - `node test/status.mjs`:以 `ELECTRON_RUN_AS_NODE=1` 跑桌面端 exe,验证 `original-fs` 回退与自动探测; - `python docs/standalone/verify_patch.py`:独立脚本复核当前安装。