GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-25-windows-tray-design.md
2026-05-20 21:39:12 +08:00

495 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Neta Windows 托盘模式设计
> 日期2026-04-25
> 状态:待实施
> 修订v2 — 按架构复核收紧运行时边界、控制接口与实施顺序
## 目标
在现有 Windows 离线安装链路基础上,为 Neta 增加首版托盘模式。
首版要求保留当前安装器总体体验,但新增完整的 Windows 托盘入口,使用户可以通过托盘完成打开系统、启停服务、打开目录和退出程序。
本期目标不是重做桌面端架构,而是在不改动数据库安全方案的前提下,补齐 Windows 本地使用体验中最明显的缺口。
## 当前项目现实
当前 Neta 的 Windows 运行链路已经具备以下真实状态:
- `backend.exe` 是现有唯一运行入口,启动入口为 `packages/backend/bootstrap.js`
- 启动前已会读取外部 `config.yaml` 并注入运行配置
- `backend.exe` 已负责真实端口选择、`data.dir` 路径收口、日志目录、运行锁和自动打开浏览器
- Inno Setup 当前默认安装、快捷方式、首次启动和卸载都直接围绕 `backend.exe`
- 仓库当前没有现成的桌面端托盘基础设施,也没有 `.NET` / Rust / Go 托盘项目
因此,这次托盘设计必须是对现有单进程 Windows 架构的增量改造,而不是重画一套新的桌面运行时。
## 已确认约束
- 保持当前启动方式总体体验不变,只是额外增加托盘图标和托盘菜单
- 首版托盘菜单必须包含:
- 打开系统
- 重启服务
- 停止服务
- 打开日志目录
- 打开配置目录
- 退出程序
- 安装器允许新增独立 `tray.exe`
- 桌面快捷方式和开机自启都统一启动 `tray.exe`
- `tray.exe` 负责拉起和监控 `backend.exe`
- 数据库安全相关问题本期不改,继续沿用当前 `config.yaml` 方案
- 不引入 Windows Service
- 不实现“关闭浏览器窗口后最小化到托盘”
- 不实现自动更新、首次启动配置向导
- 不调整 `config.yaml` 的当前存放位置
## 1. 方案对比
### 方案 A独立 `tray.exe` + 现有 `backend.exe`(推荐)
- `backend.exe` 继续承载 Midway.js 后端与前端静态资源
- 新增一个轻量原生 Windows 托盘程序 `tray.exe`
- `tray.exe` 负责托盘图标、菜单、开机自启、单实例、启动/停止/重启 `backend.exe`
- 安装器入口、桌面快捷方式、开始菜单和开机自启统一指向 `tray.exe`
优点:
- Windows 职责边界清晰,托盘控制和 Web 服务解耦
- 便于处理托盘生命周期、单实例和目录打开等原生能力
- 后续若继续扩展状态提示、升级控制器、最小化托盘等能力,演进路径更自然
代价:
- 安装产物从一个主 exe 变成两个 exe
- 打包链路需要增加一个托盘程序构建步骤
- 打包环境要新增桌面端工具链依赖
### 方案 B把托盘能力并入 `backend.exe`
- 继续只保留 `backend.exe`
- 同一个进程同时承担后端服务与托盘功能
优点:
- 产物更少
- 打包表面上更简单
代价:
- Node/pkg 进程承载 Windows 原生托盘能力更复杂
- 进程窗口形态、生命周期和服务控制容易互相耦合
- 后续加菜单状态和异常恢复时复杂度明显更高
### 方案 C`tray.exe` 仅做启动器,核心控制仍回到 `backend.exe`
优点:
- 比单进程更容易落地
代价:
- 职责边界仍不干净
- 后续扩菜单、状态同步和异常恢复时容易重新耦合
### 结论
采用方案 A新增独立 `tray.exe` 作为 Windows 壳层,`backend.exe` 保持当前服务职责。
## 2. 总体架构
安装产物分成两层:
- `backend.exe`:现有 pkg 产物,继续负责 Midway.js 服务、静态前端、配置加载、数据目录解析、真实端口选择、单实例、优雅退出
- `tray.exe`:新增 Windows 托盘控制器,负责本地托盘交互、菜单操作和后端进程编排
启动路径改为:
```text
桌面快捷方式 / 开机自启 / 开始菜单
-> tray.exe
-> 检查 backend.exe 是否已运行
-> 未运行则拉起 backend.exe
-> 已运行则仅挂托盘
```
核心原则:
- `backend.exe` 是唯一运行时真源
- Web 服务职责、真实端口、路径、锁、ready 状态全部保留在 `backend.exe`
- `tray.exe` 不是第二个运行时中心,只是本地壳层和控制面消费者
- 两者之间只通过最小化的本机控制协议通信,不共享业务逻辑
## 3. 托盘菜单定义
首版托盘菜单固定包含以下项目:
1. 打开系统
- 打开当前本地前端首页
- URL 必须来自 `backend.exe` 状态接口返回值,不由 `tray.exe` 自行猜测
2. 重启服务
-`tray.exe` 先请求 `backend.exe` 优雅停止
- 停止完成后,再由 `tray.exe` 重新拉起 `backend.exe`
- “重启”是托盘侧的编排动作,不是后端自重启接口
3. 停止服务
-`tray.exe` 调用本机 stop 控制接口
- 等待后端退出,并将托盘状态更新为“服务未运行”
4. 打开日志目录
- 打开 `{dataDir}/logs`
5. 打开配置目录
- 打开安装目录中 `config.yaml` 所在目录
6. 退出程序
- 默认同时退出托盘和后端服务
- 顺序为:先请求后端优雅退出,再退出 `tray.exe`
- 如后端超时未退出,可执行兜底强制结束
## 4. 本机控制协议
`tray.exe` 不承载业务逻辑,只作为本机控制层。
`backend.exe` 在现有 Web 服务之外,新增一个仅本机可用的最小控制入口供托盘调用。
### 4.1 通信方式
首版采用 loopback HTTP
- 仅监听 `127.0.0.1`
- 不对外网暴露
- 使用本地 secret 进行鉴权,避免同机其他进程随意控制
### 4.2 首次发现机制
为了解决动态端口和托盘首次附着问题,`backend.exe` 启动后必须在 `{dataDir}/runtime-info.json` 写入本机运行时引导信息。
该文件至少包含:
- `pid`
- `ready`
- `startedAt`
- `port`
- `url`
- `controlBaseUrl`
- `controlSecret`
- `dataDir`
- `logDir`
- `configDir`
用途:
- `tray.exe` 首次启动时,先通过安装目录下的 `config.yaml` 定位 `data.dir`
- 再从 `{dataDir}/runtime-info.json` 读取当前 `backend.exe` 的真实控制地址和本机 secret
- 从而避免硬编码 8003也避免在“backend 已经在运行”时无法获知 secret
`runtime-info.json` 是本地桌面版运行时的引导文件,不是对外 API也不是新的业务配置入口。
### 4.3 最小控制面
首版只提供两个接口:
- `GET /base/runtime/status`
- `POST /base/runtime/stop`
说明:
- 路由层级必须遵循当前 `modules/base/controller/app/*.ts` 的现有挂载风格
- 这里的 `/base/runtime/*` 是基于当前 `app/comm` 控制器实际访问模式确定的逻辑落点
明确不提供:
- `POST /base/runtime/restart`
- `POST /base/runtime/open`
原因:
- `restart` 应由 `tray.exe` 负责 stop + spawn 编排,不应由 `backend.exe` 自己承担自重启职责
- `open` 不需要独立控制接口,`tray.exe` 从状态接口取得真实 URL 后自行打开浏览器即可
### 4.4 状态接口返回内容
状态接口至少返回:
- 当前运行状态
- ready 状态
- 实际监听端口
- 首页 URL
- 数据目录路径
- 日志目录路径
- 配置目录路径
- 后端 PID
这样 `tray.exe` 可以始终依赖真实运行状态驱动菜单,而不是自行拼接路径或端口。
### 4.5 启动握手
`tray.exe` 启动后执行:
1. 读取安装目录下的 `config.yaml`,得到 `data.dir`
2. 读取 `{dataDir}/runtime-info.json`
3. 若引导文件存在,则先根据其中的 `pid``controlBaseUrl` 尝试附着已有 `backend.exe`
4. 若状态接口返回 ready则仅挂载托盘
5. 若引导文件缺失、陈旧或接口不可达,则由 `tray.exe` 拉起新的 `backend.exe`
6. 新进程启动后,先轮询 `runtime-info.json`,再轮询 `status` 接口直到 ready
7. ready 后刷新菜单状态
运行时存活性判断优先级保持为:
1. `status` 接口
2. `backend.exe` 进程是否仍存活
3. `neta.lock` 仅作诊断辅助
### 4.6 鉴权
首版控制接口必须具备本机鉴权机制:
- `backend.exe` 每次启动都生成新的 `controlSecret`
- `controlSecret` 仅写入 `{dataDir}/runtime-info.json`,供本机 `tray.exe` 复用
- `tray.exe` 后续请求必须携带该 secret
- 正常退出时必须清理 `runtime-info.json`
这样既保留了“每次启动轮换 secret”的边界也解决了托盘在附着已运行 backend 时的发现问题。
## 5. 运行时行为与边界条件
### 5.1 单实例
- `backend.exe` 继续保留现有单实例锁与运行检测逻辑
- `tray.exe` 也需要独立单实例保护,避免出现多个托盘图标
- 如果重复启动 `tray.exe`,不创建第二个实例,而是唤起已有实例并执行“打开系统”
### 5.2 状态判断优先级
`tray.exe` 判断 `backend.exe` 状态时,优先级固定为:
1. 先探测本机 `status` 接口
2. 接口不可达时,再检查 `backend.exe` 进程是否存在
3. `neta.lock` 仅作为诊断辅助,不作为一等控制协议
原因:
- 当前真实端口由 `backend.exe` 决定
- `backend.exe` 已有自己的锁和退出清理机制
- 直接依赖锁文件容易与真实 ready 状态漂移
### 5.3 端口处理
- `backend.exe` 继续沿用现有可用端口探测逻辑,不强制固定 8003
- `tray.exe` 不自行猜测端口
- “打开系统”始终使用状态接口返回的真实 URL
### 5.4 启动失败
- `tray.exe` 拉起后端后,在限定时间内轮询状态接口
- 若超时仍未 ready托盘图标进入“异常”状态
- 菜单保留“打开日志目录”和“重试启动”的恢复路径
- 首版不增加复杂配置向导,只提示“服务启动失败,请查看日志”
### 5.5 后端异常退出
- `tray.exe` 持续感知 `backend.exe` 状态
- 若后端异常崩溃,托盘菜单切换为“服务未运行”
- 用户可以直接从托盘执行重启服务
### 5.6 优雅退出
退出顺序固定为:
```text
tray.exe 发起 stop 请求
-> backend.exe 执行优雅收尾
-> 超时则由 tray.exe 兜底结束
-> tray.exe 自身退出
```
同一顺序也用于安装器卸载阶段。
### 5.7 手动运行 backend.exe
如果用户绕过标准入口,直接双击 `backend.exe`
- 服务仍可启动
- 但不保证拥有完整托盘体验
- 标准使用入口仍是 `tray.exe`
## 6. 技术选型
建议新增一个独立包:`packages/windows-tray/`
### 6.1 推荐实现
使用 `.NET 8` 实现原生 Windows 托盘程序,并发布为自包含单文件 `tray.exe`
推荐原因:
- 做系统托盘、单实例、打开目录、进程管理这些 Windows 原生动作更稳妥
- 与 Node/pkg 运行时彻底解耦
- 可以直接产出适合 Inno Setup 分发的单文件 exe
- 不要求最终用户额外安装 .NET Runtime
### 6.2 当前仓库前提
当前 Neta 仓库中没有现成的 `.NET` 托盘工程,因此这不是接入现有桌面端项目,而是新增一条桌面端工具链。
这意味着打包环境必须新增前置条件:
- 打包机安装 `.NET 8 SDK`
- 安装器构建脚本支持 `dotnet publish`
- Windows 打包说明同步补充 `.NET 8 SDK` 依赖
### 6.3 为什么不放进现有前后端栈里做
- 前端 Vue 不是桌面托盘技术栈
- 后端 Node/pkg 进程更适合承担服务,不适合顺带承载稳定的 Windows 壳层能力
- 用独立托盘程序更符合当前项目边界业务继续在后端UI 继续在浏览器Windows 壳层作为单独辅助包存在
## 7. 仓库改动清单
### 7.1 新增后端控制层
建议新增:
- `packages/backend/src/modules/base/controller/app/runtime.ts`
- `packages/backend/src/modules/base/service/runtime.ts`
- `packages/backend/src/comm/runtime-secret.ts`
- `packages/backend/src/comm/runtime-state.ts`
- `packages/backend/src/comm/runtime-info.ts`
- `packages/backend/src/comm/graceful-shutdown.ts`
建议修改:
- `packages/backend/bootstrap.js`
- 新增最小 CLI 参数支持,例如:
- `--tray-secret`
- `--no-browser`
- `packages/backend/src/configuration.ts`
- 注册统一 shutdown coordinator
- 暴露真实端口、目录和运行状态
- 维护并清理 `{dataDir}/runtime-info.json`
### 7.2 新增托盘包
建议新增:
- `packages/windows-tray/Neta.Tray.csproj`
- `packages/windows-tray/Program.cs`
- `packages/windows-tray/TrayApplicationContext.cs`
- `packages/windows-tray/BackendProcessManager.cs`
- `packages/windows-tray/RuntimeInfoStore.cs`
- `packages/windows-tray/StatusClient.cs`
- `packages/windows-tray/SingleInstance.cs`
- `packages/windows-tray/Assets/neta.ico`
职责包括:
- 托盘图标与菜单
- 单实例
- 读取 `config.yaml``runtime-info.json`
- 启动 `backend.exe`
- 请求停止 `backend.exe`
- 编排重启 `backend.exe`
- 打开浏览器
- 打开日志目录
- 打开配置目录
- 按“status > process > lock”顺序判断状态
- 支持安装器卸载阶段的无界面 `--shutdown` 模式
### 7.3 修改打包和安装器链路
建议修改:
- `packages/backend/scripts/build-windows-installer.js`
- 先生成 `backend.exe`
- 再执行 `dotnet publish` 生成 `tray.exe`
- 最后一起交给 Inno Setup 打包
- `packages/backend/installer/setup.iss`
- 安装 `backend.exe``tray.exe`
- 桌面快捷方式改为指向 `tray.exe`
- 开机自启注册表改为指向 `tray.exe`
- 卸载时优先调用 `tray.exe --shutdown`
- 若优雅停机失败,再兜底 `taskkill`
## 8. 实施顺序
这次改造必须严格分阶段推进,不能并行混改:
1. 先在 `backend.exe` 中补齐最小本机控制面
2. 再实现 `tray.exe` 并接入状态轮询、启停编排与单实例
3. 最后修改安装器、快捷方式、开机自启和卸载流程
原因:
- `tray.exe` 必须依赖 `backend.exe` 的真实状态面才能稳定工作
- 当前安装器和卸载逻辑是围绕 `backend.exe` 写死的
- 若未先立住控制面,托盘实现会被迫用端口猜测、进程猜测和锁文件硬拼状态
## 9. 安装器行为
### 9.1 安装后入口
安装完成后:
- 桌面快捷方式指向 `tray.exe`
- 开始菜单入口指向 `tray.exe`
- 若勾选开机自启,注册表项写入 `tray.exe`
### 9.2 卸载顺序
卸载时执行:
1. 调用 `tray.exe --shutdown`,由托盘侧先请求 `backend.exe` 优雅退出
2. 等待短超时,让 `tray.exe` 自身退出
3. 若仍有残留进程,再兜底强制结束 `tray.exe` / `backend.exe`
4. 删除程序目录
5. 根据现有安装器策略决定是否保留数据目录
### 9.3 重装
- 重装继续保留当前“程序目录覆盖、数据目录尽量不动”的原则
- `tray.exe` 只是新增程序层产物,不改变现有数据目录策略
## 10. 验证策略
### 10.1 自动验证
后端测试:
- 本机状态接口返回真实端口和目录
- 本机 secret 校验有效
- stop 行为符合预期
- 优雅退出逻辑符合预期
打包脚本 smoke
- 能生成 `packages/backend/build/pkg-output/backend.exe`
- 能生成托盘发布产物 `tray.exe`
- Inno Setup 能将两者一并打包进安装器
### 10.2 Windows 手工验收
必须验证:
- 安装后系统托盘出现图标
- “打开系统”能打开正确 URL
- “停止服务 / 重启服务”状态切换正确
- “打开日志目录 / 打开配置目录”路径正确
- “退出程序”能同时退出托盘与后端
- 卸载后程序目录清理正常
- 重装后数据目录不会被误删
## 11. 本期明确不做
本期范围明确排除以下内容:
- 数据库安全改造
- 首次启动配置向导
- 自动更新机制
- 浏览器关闭后最小化到托盘
-`backend.exe` 改造成 Windows Service
- 调整 `config.yaml` 到 ProgramData 或其他新位置
## 12. 结论
本期采用“独立 `tray.exe` + 现有 `backend.exe`”的双进程方案。
其中:
- `backend.exe` 是唯一运行时真源
- `tray.exe` 是 Windows 本地托盘控制层,也是安装器标准入口
- 两者通过仅本机可用、带 secret 鉴权的最小控制接口通信
- `tray.exe` 只消费状态并编排 stop + spawn不承担后端自重启职责
这样可以在不重做现有安装器和服务架构的前提下,为 Windows 用户补齐托盘入口、服务启停、目录打开和整体退出能力,并为后续继续扩展桌面体验保留清晰边界。