docs/backend-migration/plans/2026-05-07-m6-three-paths-cutover-requirements.md
feat/m5-static-server-auth-migration 已 merge)把所有 WebUI 启用路径从"老 webserver"切换到"@aionui/web-host",并删除老
webserver 代码。三条路径共用同一份 startWebHost(),只是 backend 参数
不同。
具体动作:
packages/web-host/src/index.ts 的 startWebHost() 完整组装:
组合 backend-launcher(M4) + static-server(M5) + auth(M5) +
端口管理 + WebHostHandle 生命周期packages/desktop/ 的 --webui 启动分支
(原 src/index.ts:172 对应的新位置)改为调
startWebHost({ backend: { kind: 'ownBackend', ... } })webui.start / webui.stop IPC handler(这俩 IPC
调用 WebuiModalContent.tsx 用)底层改为调
startWebHost({ backend: { kind: 'useExistingBackend', port: currentBackendPort } }),
handle.stop() 关 host,不关 backendrestoreDesktopWebUIFromPreferences() 自动恢复逻辑
保留在 packages/desktop/(位置沿用 M1 后的实际路径,plan-writer 读
M5 handoff 确认),但内部改为 await startWebHost({ kind: 'useExistingBackend', ... })。
恢复策略(读 webui.desktop.enabled 并决定是否调 startWebHost)是桌面壳
的编排,不迁到 web-hostpackages/desktop/src/process/webserver/ 整个目录tests/e2e/cases/webui/ 下新增三个用例:
desktop-ipc.e2e.ts:桌面 IPC 模式,对话全链路通desktop-gui-switch.e2e.ts:GUI Switch 开关控制,桌面 + 浏览器并用webui-headless.e2e.ts:--webui 无头模式,浏览器登录对话packages/desktop/src/common/platform/register-node.ts
(设计文档里说的 standalone 遗留空文件,顺手删)webui.start / webui.stop IPC 接口保留,
前端 WebuiModalContent.tsx 零感知)webuiResetPassword / webuiChangePassword
对外接口(底层改调 web-host auth 模块,接口不变)b157719a 清理的 standalone bun serverwebui.config.json 路径和 schema 保持一致,M5
已保证)resetPassword / changePassword /
verifyPassword / loadConfig / saveConfig 的实现和单元测试必须已由
M5 全部完成。M6 只负责把桌面 preload / IPC handler 接线到这些已存在
的实现上,不得在 M6 新增 auth 函数或补 auth 单元测试。若接线时发现 M5
有缺口,escalate 给 team-lead 决定是否回到 M5 修补,不自主扩 M6 范围M6 是整条链最高风险的一步,且 e2e 同时涉及 Electron、浏览器、backend、 WebSocket 反代四条链路,对端口时序、日志位置、GUI 启动速度非常敏感。 plan-writer 在写 M6 detailed plan 时,除了 executor 执行步骤之外,必须 额外产出一节"失败诊断清单",作为 M6 detailed plan 的必备章节,内容包括:
~/Library/Logs/AionUi/main.log)handle.stop() 误杀了
backend,查 M6 接线处--webui 会端口冲突)这一节不是 executor 的"执行步骤",而是 executor 在 e2e 失败时的"自助 诊断手册",plan-writer 不写这一节,plan 就不算完成。
| 决策点 | 结论 | 理由 |
|---|---|---|
| 切换方式 | 一次性切换,不保留过渡开关 | 设计文档"一次性替换"原则,过渡态成本更高 |
| 老 webserver 目录处理 | 直接删除,不保留注释/tag | 仓库干净;历史在 git log |
| GUI 开关关闭后 backend 处理 | backend 不停,继续服务桌面 IPC | 设计文档 E2 节 |
| 自动恢复逻辑归属 | 留在 packages/desktop/,只是改为调 @aionui/web-host.startWebHost(...)。web-host 提供能力,桌面壳编排"是否恢复 / 何时恢复"的策略 | webui.desktop.enabled 是桌面壳专属偏好,不是 web-host 通用能力;避免 web-host 被污染桌面语义 |
webui.start IPC 返回值 | 保持和老实现一致 | 前端 WebuiModalContent 代码零改动 |
三条路径的 AppMetadata 注入 | 桌面壳的入口统一构造一次,三条路径都用同一个 | 避免 drift |
| e2e 覆盖 | 三条路径各一个主流程 e2e,不做穷尽组合 | 核心场景覆盖,不过度 |
| 本里程碑是否包含回滚策略 | 包含,failed e2e 时 revert 本 PR | 高风险必须可回滚 |
packages/desktop/src/common/platform/register-node.ts 处理 | 同步删除 | 设计文档关键文件清单已列(M1 后路径已变) |
| 失败时的 backend 端口参数从哪获取(useExistingBackend) | 从 BackendLifecycleManager 的 port 属性(M4 保证可访问) | M4 已定 |
所有 e2e 全绿:
bun run test:e2e tests/e2e/cases/webui/
# 预期:
# - desktop-ipc.e2e.ts PASS
# - desktop-gui-switch.e2e.ts PASS
# - webui-headless.e2e.ts PASS
# 已有 e2e 不回归
bun run test:e2e
# 预期:全绿
老 webserver 彻底清理:
# 目录应已删除
find packages/desktop/src/process/webserver -type f 2>&1 | wc -l
# 预期:0
# 无任何残留 import
grep -rn "from.*process/webserver" packages/desktop/src/
# 预期:无输出
桌面 + 浏览器并用场景(e2e 重点验证):
# 1. 启动桌面 IPC 模式(bun run dev),打开桌面 UI
# 2. 在设置页点开 WebUI Switch
# 3. 浏览器访问 localhost:<port>,登录
# 4. 在桌面发消息 → 浏览器能看到对话更新
# 5. 在浏览器发消息 → 桌面能看到对话更新
# 6. 关闭 Switch → lsof -i :<backend-port> 仍有进程(backend 没停)
# 7. 退出 app 后再启动 → WebUI 自动恢复 on 状态
以上全部由 desktop-gui-switch.e2e.ts 自动化覆盖,用 playwright 同时
控制 Electron 和 Chromium。
前端接口不变:
# 前端 webui.start 调用应无改动
git diff origin/feat/m5-static-server-auth-migration \
packages/desktop/src/renderer/ \
packages/desktop/src/common/adapter/
# 预期:无改动(或只有无关的改动)
backend 端口透传正确(反代验证):
# 启动 GUI 开关后的 web-host port
BACKEND_PORT=<从日志读>
HOST_PORT=<从日志读>
curl -fsS http://127.0.0.1:$HOST_PORT/api/health
# 预期:200,说明 /api/* 成功反代到 backend
# WebSocket 反代
curl -fsS --include -H "Connection: Upgrade" -H "Upgrade: websocket" \
http://127.0.0.1:$HOST_PORT/ws 2>&1 | head -5
# 预期:101 Switching Protocols 或类似响应
| 风险 | 缓解 |
|---|---|
| GUI 开关切换时序和老实现不一致,导致重启后状态丢失 | e2e 覆盖"开 → 重启 → 验证仍 on";手动跑多次验证稳定性 |
useExistingBackend 传入 port 为旧值(backend 已重启换端口) | M4 保证 BackendLifecycleManager.port 是 live 引用,不是缓存值;plan-writer 确认 getter 行为 |
删老 webserver 后某些 import 未被迁移(如 extraResources 路径) | 全仓 grep webserver 找残留;bun run dev 能启动且 e2e 全绿才算合格 |
| preload IPC 暴露的方法底层切换后行为微变(比如错误码) | 保持 IPC 响应格式和老实现一致,尤其 error 字段格式 |
反代 /ws 时 session cookie 透传不正确,浏览器登录状态丢失 | e2e 覆盖"登录后 WebSocket 连接有效",检查 cookie 透传 |
| 切换时 port 分配规则变化导致用户收藏的 URL 失效 | 默认 port 25808 保持不变 |
| e2e playwright 并行控制 Electron + Chromium 复杂,测试不稳定 | plan-writer 研究仓库现有 e2e(tests/e2e/cases/teams/)找类似双实例用例参考 |
| 本里程碑改动面最大,一次失败全链路 revert 代价高 | feature 分支上跑完所有 e2e 才 push;失败时 agent 不 push,escalate 给人类 |
restoreDesktopWebUIFromPreferences 内部从老 webserver 改调 startWebHost 后,Electron 启动时机可能和原来不同(导致 WebUI 提前或延后) | restoreDesktopWebUIFromPreferences 保留在桌面壳(不迁入 web-host),plan-writer 在桌面入口的原调用点就地改内部实现,保持调用时序不变 |
getPort() 可用BackendLifecycleManager 构造签名和 port 访问方式origin/feat/m5-static-server-auth-migrationfeat/m6-three-paths-cutoverdocs/backend-migration/handoffs/M6-outcome.mdgit merge origin/feat/backend-migration12-20 小时(三条路径切换 + 写 3 个 e2e + 删老代码 + 验证。本里程碑是最大 风险点,预留充分 buffer)