Files
2026-07-13 13:20:22 +08:00

523 lines
31 KiB
Markdown
Raw Permalink 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.
# 设置页 → 远程连接 — WebUI 服务 (F-WEBUI)
> 本文档覆盖「设置 → 远程连接」页面 **WebUI Tab** 的全部功能,包括服务启停、远程访问控制、认证管理、QR 码登录、配置持久化、页面结构、状态同步、扩展系统 WebUI 贡献。
> 基于静态代码分析和动态 UI 验证综合整理。
>
> Channels Tab 相关内容见 [channels.md](../channels/channels.md)。
---
## (F-WEBUI-01) WebUI 服务启停 [已实现]
**用户故事**:作为桌面端用户,我希望通过一个开关快速启用或禁用 WebUI 服务,以便在需要时通过浏览器远程访问 AionUi。
**前置条件**:运行在 Electron 桌面端(`isElectronDesktop() === true`
**正常流程**(用户视角):
1. 用户进入「设置 → 远程连接」页面,默认选中 "WebUI" Tab
2. 页面显示 WebUI 服务卡片,卡片内第一行为"启用 WebUI",右侧有 Switch 开关
3. 用户打开开关(OFF → ON
4. 开关显示 loading 旋转,旁边出现橙色"启动中..."文字
5. 启动成功后:开关变为蓝色 checked 状态,旁边显示绿色"✓ 运行中"toast 提示"WebUI 启动成功"
6. 下方出现"访问地址"行(见 F-WEBUI-03
7. 如为首次启动且有初始密码,密码行显示明文(见 F-WEBUI-06
8. 用户关闭开关(ON → OFF
9. 访问地址行和 QR 码区域消失,toast 提示"WebUI 已停止"
**异常情况**
- 启动 IPC 调用失败(网络错误/主进程异常):Switch 回滚到关闭状态,toast 提示"操作失败"`settings.webui.operationFailed`
- 启动 IPC 超时(3 秒未返回):UI 乐观地认为服务已启动,显示为运行中,**同时持久化 enabled=true**(已知局限:可能导致 UI 与实际状态不一致,且下次启动时循环恢复失败服务)
- 端口被占用:服务器自动尝试下一个端口(`port + 1`),递增上限固定为 `DEFAULT_PORT + 10`(默认端口:生产 25808,开发 25809,上限 25818/25819);当用户通过 CLI/环境变量指定端口超出此范围时,不触发递增,直接报错
- 停止操作为 fire-and-forget 模式:UI 和 ConfigStorage 先行更新为停止状态 → toast 提示"WebUI 已停止"`settings.webui.stopSuccess`)→ 然后才异步调用 `webui.stop.invoke()`。**Toast 出现时服务器可能仍在运行**
- 服务器已在运行时再次收到启动请求:先停止旧实例(关闭所有 WebSocket 连接、释放端口),再启动新实例
**验收标准**
- [ ] Switch 开关可切换 WebUI 服务启停
- [ ] 启动过程显示 loading 状态 + "启动中…" 文字(以 i18n `settings.webui.starting` 实际值为准)
- [ ] 启动成功后显示绿色"✓ 运行中"标记
- [ ] 启动/停止成功各有对应 toast 提示(`settings.webui.startSuccess` / `settings.webui.stopSuccess`
- [ ] 启动失败时 Switch 回滚到关闭状态 + toast 提示(`settings.webui.operationFailed`
- [ ] 端口被占用时自动递增,上限固定为 `DEFAULT_PORT + 10`
---
## (F-WEBUI-02) 启动恢复与配置持久化 [已实现]
**用户故事**:作为用户,我希望启用 WebUI 后下次打开应用时能自动恢复服务,无需每次手动开启。
**正常流程**(用户视角):
1. 用户启用 WebUI 服务(F-WEBUI-01
2. 系统在启动成功后将 `webui.desktop.enabled = true` 写入 ConfigStorage
3. 用户关闭并重新打开应用
4. 应用在后台自动启动 WebUI 服务(非阻塞,不影响应用启动速度)
5. 进入设置页面时显示为已启动状态
**配置来源**
端口解析(覆盖式优先级):CLI 参数(`--port` / `--webui-port`> 环境变量(`AIONUI_PORT` / `PORT`> 配置文件(`userData/webui.config.json`> 默认值(25808/25809
远程访问(多源 OR 聚合 — 只要任一来源为 true 即启用):`isRemoteMode` || 环境变量(`AIONUI_ALLOW_REMOTE` / `AIONUI_REMOTE` / `AIONUI_HOST=0.0.0.0`|| 配置文件(`allowRemote: true`|| ConfigStorage 偏好
**两种启动路径的配置差异**
| 启动路径 | 端口解析 | 远程访问解析 |
| -------------- | ------------------------------------------------------- | ------------------------------------------ |
| CLI/服务器模式 | 经过 `resolveWebUIPort`CLI > env > config > default | 经过 `resolveRemoteAccess`(多源 OR 聚合) |
| 桌面自动恢复 | 仅从 ConfigStorage 读取(不经过 resolve | 仅从 ConfigStorage 读取(不经过 resolve |
**异常情况**
- 自动恢复失败(端口全部占用/其他错误):仅记录日志,不影响应用正常启动
- 配置文件 JSON 解析失败:使用空配置 `{}`,不报错
- ConfigStorage 读取失败:默认为未启用
**验收标准**
- [ ] 启用 WebUI 后,重启应用时自动恢复 WebUI 服务 [验证策略:集成测试]
- [ ] 禁用 WebUI 后,重启应用不会自动启动服务 [验证策略:集成测试]
- [ ] 启动失败不影响应用正常使用 [验证策略:集成测试]
- [ ] 持久化在启动成功后才写入(失败不写入,避免循环恢复失败)[验证策略:evaluate_script 读 ConfigStorage]
---
## (F-WEBUI-03) 访问地址展示 [已实现]
**用户故事**:作为用户,我希望 WebUI 启动后能看到访问地址,方便在浏览器中打开或分享给他人。
**前置条件**WebUI 服务正在运行(`status.running === true`
**正常流程**(用户视角):
1. WebUI 启动后,服务卡片内出现"访问地址"行
2. 根据"允许远程访问"开关状态显示不同地址:
- 远程访问关闭:显示 `http://localhost:{port}`
- 远程访问开启:显示 `http://{局域网IP}:{port}`(如 `http://192.168.3.15:25809`
3. 地址以蓝色链接样式显示,点击在系统默认浏览器中打开
4. 地址旁有复制按钮,点击复制到剪贴板,toast 提示"复制成功"
**异常情况**
- 点击链接打开浏览器失败:仅记录日志,无用户可见提示
- 局域网 IP 获取失败(无网卡/无 IPv4 地址):退回显示 `localhost`
**验收标准**
- [ ] 仅在 WebUI 运行时显示访问地址行
- [ ] 远程访问开启时显示局域网 IP 地址
- [ ] 远程访问关闭时显示 localhost
- [ ] 点击地址在系统浏览器中打开
- [ ] 复制按钮可复制地址 + toast 提示
---
## (F-WEBUI-04) 远程访问控制 [已实现]
**用户故事**:作为用户,我希望控制是否允许局域网内其他设备访问 WebUI,以便用手机或其他电脑访问 AionUi。
**正常流程**(用户视角):
1. 服务卡片内"允许远程访问"行,右侧有 Switch 开关
2. 开关下方显示说明文字("使用远程软件/服务器可安全远程访问")和蓝色"查看方式"链接
3. 用户打开开关(OFF → ON
4. **如果 WebUI 正在运行**:系统执行停止→重启流程(因为需要切换监听地址从 `127.0.0.1``0.0.0.0`),显示 loadingtoast 提示"WebUI 已重启"
5. 访问地址从 `localhost` 变为局域网 IP,QR 码区域出现(见 F-WEBUI-08
6. **如果 WebUI 未运行**:仅保存偏好,不触发重启
**异常情况**
- 重启失败(停止后无法重新启动):系统二次确认服务状态(调用 getStatus),如确认未运行则回滚开关 + toast(`settings.webui.operationFailed`
- 重启超时:最大等待 6s(停止 1.5s + 启动 3s + 状态确认 1.5s),超时后同上处理
- 持久化失败:回滚开关状态 + toast(`settings.webui.operationFailed`
- "查看方式"链接:点击打开 GitHub Wiki 远程访问指南页面(`shell.openExternal`
**验收标准**
- [ ] 切换开关可启用/禁用远程访问
- [ ] WebUI 运行中切换时,执行自动重启流程
- [ ] 重启成功有 toast 提示(`settings.webui.restartSuccess`
- [ ] 重启失败回滚开关状态 + toast 提示(`settings.webui.operationFailed`
- [ ] 设置在 WebUI 未运行时也可修改并持久化,下次启动生效
- [ ] "查看方式"链接可打开指南页面
---
## (F-WEBUI-05) 用户名管理 [已实现]
**用户故事**:作为用户,我希望能查看和修改 WebUI 的登录用户名,以便个性化我的登录凭据。
**正常流程**(用户视角):
1. 登录信息卡片(始终可见,无论 WebUI 是否启用)显示"用户名:"行
2. 当前用户名明文显示(默认 "admin"
3. 旁有复制按钮(复制用户名)和编辑按钮(铅笔图标)
4. 点击编辑按钮弹出"设置新用户名"弹窗
5. 输入框预填当前用户名
6. 用户输入新用户名,点击"确定"
7. 提交成功:toast"用户名修改成功",弹窗关闭,显示更新为新用户名
**表单校验规则**
| 规则 | 前端校验 | 后端校验 | 错误提示 |
| --------------------------- | -------- | -------- | -------------------------------------- |
| 必填 | 是 | - | 前端 i18n 提示 |
| 最少 3 字符 | 是 | 是 | 前端 i18n / 后端英文原文(未 i18n 化) |
| 最多 32 字符 | 是 | 是 | 前端 i18n / 后端英文原文(未 i18n 化) |
| 仅允许 `[a-zA-Z0-9_-]` | 是 | 是 | 前端 i18n / 后端英文原文(未 i18n 化) |
| 不能以 `_``-` 开头/结尾 | 是 | 是 | 前端 i18n / 后端英文原文(未 i18n 化) |
| 用户名已存在(不同用户) | - | 是 | "Username already exists"(英文原文) |
**异常情况**
- 前端校验失败:表单内行内提示,不提交
- 后端校验失败:toast 显示后端返回的**英文原文**错误信息(注:与密码修改不同,用户名后端错误码未做前端 i18n 翻译)
- IPC 调用异常:toast`settings.webui.usernameChangeFailed`
- 用户名与当前相同:后端直接返回成功,不执行更新
- 修改成功后:所有已存在的登录 token 失效(通过 JWT secret 轮转实现,为被动失效 — 已建立的 WebSocket 连接不会立即断开,需等到下次请求或心跳时才被拒绝)
**验收标准**
- [ ] 用户名默认显示"admin"
- [ ] 复制按钮可复制用户名
- [ ] 编辑按钮打开"设置新用户名"弹窗,预填当前值
- [ ] 前端校验规则全部生效
- [ ] 提交成功后 toast 提示 + 弹窗关闭 + UI 更新
- [ ] 提交失败有明确错误提示
- [ ] 修改后已有 WebUI 登录会话被踢出
---
## (F-WEBUI-06) 密码显示与初始密码 [已实现]
**用户故事**:作为用户,我希望在首次启动 WebUI 时能看到系统生成的初始密码,之后密码显示为遮罩状态,以保护安全。
**正常流程**(用户视角):
1. 登录信息卡片中"初始密码:"行(始终可见)
2. **首次启动(有初始密码)**:显示明文密码(12 至 16 字符,含两端;含小写 + 大写字母 + 数字 + 特殊字符 `!@#$%^&*`
3. **非首次启动(无初始密码/已修改密码)**:显示"**\*\***"(密码遮罩)
4. 旁有编辑按钮(铅笔图标),hover 时 tooltip 提示"忘记密码?点击设置新密码(不需要当前密码)"
5. **注意:密码行仅有编辑按钮,无复制功能**(与用户名行不同,用户名行有复制 + 编辑两个按钮)
**异常情况**
- 服务运行中但无初始密码且无缓存密码:显示"**\*\***",用户需通过编辑按钮设置新密码
- 密码可见状态(`canShowPlainPassword`)是组件内存状态,页面刷新后重置为遮罩
**验收标准**
- [ ] 首次启动时密码明文可见
- [ ] 用户修改密码后切换为遮罩显示
- [ ] 编辑按钮 tooltip 提示修改密码不需要当前密码
- [ ] 页面刷新后密码回到遮罩状态
- [ ] 密码行仅有编辑按钮,无复制按钮
---
## (F-WEBUI-07) 密码修改 [已实现]
**用户故事**:作为用户,我希望能设置自定义密码替换系统生成的初始密码,或在忘记密码时直接设置新密码。
**正常流程**(用户视角):
1. 点击密码行的编辑按钮
2. 弹出"设置新密码"弹窗
3. 输入新密码(Password 输入框,带可见性切换图标,placeholder"请输入新密码(至少8位)")
4. 输入确认密码(placeholder"请再次输入新密码"
5. 点击"确定"
6. 提交成功:toast"密码修改成功",弹窗关闭,密码行切换为"**\*\***"
**表单校验规则**
| 规则 | 校验端 | 错误提示 |
| ------------- | --------- | -------------------------------- |
| 新密码必填 | 前端 | "请输入新密码" |
| 最少 8 字符 | 前端+后端 | i18n 提示 / `PASSWORD_TOO_SHORT` |
| 最多 128 字符 | 后端 | `PASSWORD_TOO_LONG` |
| 弱密码黑名单 | 后端 | `PASSWORD_TOO_COMMON` |
| 确认密码必填 | 前端 | "请再次输入新密码" |
| 两次密码一致 | 前端 | "两次密码不一致" |
**弱密码黑名单**`password`, `12345678`, `123456789`, `qwertyui`, `abcdefgh`
**异常情况**
- 前端校验失败:表单内行内提示,不提交
- 后端返回多个错误码(`'; '` 分隔):前端翻译后合并显示(如 `PASSWORD_TOO_SHORT` → i18n 短密码提示)
- 后端返回未知错误码:显示原始错误信息,或兜底"密码修改失败"
- IPC 调用异常:toast"密码修改失败"
**技术说明**
- **不需要当前密码验证**changePassword API 直接设置新密码(安全性依赖 Electron 本地环境)
- **密码修改后全部 token 失效**:通过 JWT secret 轮转(`invalidateAllTokens` 生成新 secret)实现。这是**被动失效**机制 — 旧 token 不会被主动推送下线,而是在下次请求时验证失败被拒绝。已建立的 WebSocket 连接不会立即断开,需等到心跳或重连时才会被拒绝
- **后端清除初始密码**`clearInitialAdminPassword()` 清除内存中的初始密码,后续 getStatus 不再返回 initialPassword
**验收标准**
- [ ] 点击编辑按钮打开"设置新密码"弹窗
- [ ] 不需要输入当前密码
- [ ] 新密码和确认密码均有 Password 可见性切换
- [ ] 前端校验(必填、最短 8 位、两次一致)生效
- [ ] 后端校验(最长 128 位、弱密码黑名单)生效
- [ ] 提交成功后 toast 提示(`settings.webui.passwordChanged`+ 弹窗关闭 + 密码切换为遮罩
- [ ] 修改后已有 WebUI 登录 token 被动失效
---
## (F-WEBUI-08) QR 码登录 [已实现]
**用户故事**:作为用户,我希望在允许远程访问时能通过手机扫描二维码快速登录 WebUI,无需手动输入地址和密码。
**前置条件**WebUI 服务运行中(`status.running === true`)且允许远程访问(`status.allowRemote === true`
**正常流程**(用户视角):
1. 登录信息卡片下方出现分隔线 + "二维码登录"区块
2. 说明文字:"使用手机扫描二维码,即可在手机浏览器中自动登录"
3. 系统自动生成二维码(140x140 SVG,纠错级别 M
4. 二维码下方显示有效期(本地化时间格式,24h 或 12h 取决于系统 locale;使用 `toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })`
5. 旁有复制按钮(复制二维码对应的 URL)和刷新按钮
6. 用户用手机扫描二维码,自动在手机浏览器打开并登录 WebUI
7. 二维码在过期前自动刷新(token 5 分钟过期,定时器 4 分钟触发刷新)
**QR 码 URL 格式**`http://{lanIP}:{port}/qr-login?token={64位hex}`
**Token 安全机制**
| 安全特性 | 说明 |
| ---------- | ------------------------------------------- |
| 随机生成 | `crypto.randomBytes(32)` → 64 位 hex |
| 有效期 | 5 分钟(`QR_TOKEN_EXPIRY = 5 * 60 * 1000` |
| 一次性使用 | 验证后标记 `used: true` 并立即从内存中删除 |
| IP 限制 | 非远程模式下仅允许本地/局域网 IP 使用 |
| 内存存储 | 存储在进程内 `Map`,进程重启后全部失效 |
**自动生成/清除逻辑**
- 服务运行 + 允许远程 + 无 QR URL → 自动生成
- 服务停止或关闭远程访问 → 自动清除二维码 + 取消定时器
**异常情况**
- 生成失败(WebUI 未运行/IPC 异常):QR 区域显示占位文案(`settings.webui.qrGenerateFailed`+ toast(同一 i18n key)。注意:初始状态(尚未生成)和生成失败共用同一占位文案,组件无法区分两种状态
- Token 已过期:扫码后提示"QR token has expired"
- Token 已使用(重复扫码):提示"QR token has already been used"
- 非本地 IP 使用 local-only token:拒绝,提示"QR login is only allowed from local network"
- 加载中:QR 区域显示 `common.loading` 对应文案(以 i18n 实际值为准),刷新按钮显示旋转动画
**验收标准**
- [ ] 仅在 WebUI 运行 + 允许远程访问时显示 QR 区域
- [ ] 二维码自动生成,无需用户操作
- [ ] 显示有效期(本地化时间格式)
- [ ] QR 码在过期前自动刷新 [验证策略:代码审查 + 单元测试]
- [ ] 可手动点击刷新按钮
- [ ] 可复制二维码链接
- [ ] 扫码后可在手机浏览器中自动登录
- [ ] Token 为一次性使用,重复扫码失败
- [ ] 服务停止或关闭远程时 QR 区域消失
---
## (F-WEBUI-09) 页面结构 — WebUI / Channels 双 Tab [已实现]
**用户故事**:作为用户,我希望在"远程连接"设置页面中方便地切换 WebUI 服务配置和 Channel 渠道配置。
**正常流程**(用户视角):
1. **桌面端**:页面顶部显示两个 Tab
- "WebUI"(图标:地球)— 默认选中
- "Channels"(图标:通讯 + 7 个渠道 logo 缩略图:Telegram/Lark/DingTalk/WeChat/WeCom/Slack/Discord
2. 点击 Tab 切换内容区域
3. Channels Tab 内容通过懒加载方式加载,加载中显示"加载中..."
4. **非桌面端(WebUI 浏览器访问)**:无 Tab 切换,直接显示 Channels 配置内容(无 WebUI 服务管理功能)
**页面布局**
- 导航路径:左侧栏"远程连接"(位于"应用"分组下,在"显示"和"桌面宠物"之间)
- URL hash`#/settings/webui`
- WebUI Tab 内部结构:
1. 标题"WebUI"h2+ 功能描述 + 3 步引导提示条
2. WebUI 服务卡片(含蓝色提示横幅 + 启用开关 + 访问地址 + 远程访问开关)
3. 登录信息卡片(含用户名 + 密码 + QR 码登录)
**验收标准**
- [ ] 桌面端显示 WebUI / Channels 双 Tab
- [ ] 默认选中 WebUI Tab
- [ ] Tab 切换后内容区域渲染完成,Channels 懒加载期间显示 loading 占位
- [ ] 仅在 Electron 桌面端渲染 WebUI 服务配置区域(含 Tab 切换)
- [ ] 非桌面端仅显示 Channels,无 Tab 切换
---
## (F-WEBUI-10) 状态实时同步 [已实现]
**用户故事**:作为用户,我希望设置页面能实时反映 WebUI 服务的最新状态,即使状态由其他来源(如应用菜单、启动恢复)触发变更。
**正常流程**(用户视角):
1. 组件挂载时自动加载 WebUI 当前状态(偏好设置 + 服务运行状态)
2. 主进程触发的状态变更(启动/停止/端口变化)通过事件实时推送到设置页面
3. UI 自动更新所有相关元素(Switch 状态、运行标记、访问地址、QR 码区域)
**IPC 双通道机制**
- **Electron 环境**:优先使用 `electronAPI.webuiGetStatus()`(直接 IPC,无超时问题)
- **非 Electron 后备**:使用 `webui.getStatus.invoke()`bridge 模式,1.5s 超时)
- **实时事件**`webui.statusChanged.on()` 监听主进程推送的状态变更
**异常情况**
- 偏好设置读取失败:默认为未启用/不允许远程
- getStatus 调用失败或超时:使用兜底状态(`running: false, port: DEFAULT_PORT, adminUsername: 'admin'`
- 主进程 emit 的状态事件丢失:不影响 UI 已有状态,仅下次操作时刷新
**验收标准**
- [ ] 页面进入时自动加载最新状态
- [ ] 状态变更实时反映到 UI(无需手动刷新)
- [ ] 状态加载失败时使用合理的兜底默认值
---
## (F-WEBUI-11) 扩展系统 WebUI 贡献 [已实现] [验证策略:单元测试]
**用户故事**:作为扩展开发者,我希望能通过扩展 manifest 声明 API 路由和静态资源,自动注册到 WebUI 服务器。
**正常流程**(开发者视角):
1. 在扩展的 `manifest.contributes.webui` 中声明 `apiRoutes` 和/或 `staticAssets`
2. 应用加载扩展时,`resolveWebuiContributions` 校验并注册贡献
3. API 路由和静态资源在 WebUI 服务器上可用
**安全校验规则**
| 规则 | 说明 |
| ------------------ | ------------------------------------------------------------------------------ |
| 命名空间化 | 路径必须以 `/{extensionName}/` 开头 |
| 保留路径保护 | 不允许使用 `/`, `/api`, `/login`, `/logout`, `/qr-login`, `/static`, `/assets` |
| 路径冲突检测 | 跨扩展重复路径,后者被跳过 |
| 路径遍历防护 | `isPathWithinDirectory` 确保所有文件在扩展目录内 |
| 入口存在性检查 | 同时查找 dist 和 source 路径 |
| 静态资源目录存在性 | `existsSync` 检查目录 |
**已知局限**
- `wsHandlers``middleware` 已声明但运行时不支持(仅 `console.warn` 提示)
**验收标准**
- [ ] 合法的扩展 API 路由和静态资源可被注册
- [ ] 命名空间化和保留路径校验生效
- [ ] 路径遍历尝试被阻止
- [ ] 路径冲突时后者被跳过(不覆盖先注册的)
---
## 附录 AWebUI 状态矩阵
| WebUI 开关 | 允许远程 | 访问地址 | QR 码区域 | 登录信息 |
| ---------- | -------- | ------------------------- | --------- | -------- |
| OFF | (any) | 不显示 | 不显示 | 显示 |
| ON | OFF | `http://localhost:{port}` | 不显示 | 显示 |
| ON | ON | `http://{lanIP}:{port}` | 显示 | 显示 |
---
## 附录 BIPC 通信链路
```
┌──────────────────────────────────────────────────────────────────────┐
│ 渲染进程 (Renderer) - WebuiModalContent.tsx │
│ │
│ 状态加载: │
│ ├─ ConfigStorage.get('webui.desktop.enabled') │
│ ├─ ConfigStorage.get('webui.desktop.allowRemote') │
│ ├─ electronAPI.webuiGetStatus() [优先] │
│ └─ webui.getStatus.invoke() [后备, 1.5s timeout] │
│ │
│ 服务启停: │
│ ├─ webui.start.invoke({ port, allowRemote }) [3s timeout] │
│ ├─ webui.stop.invoke() [fire-and-forget] │
│ └─ webui.statusChanged.on(callback) [实时监听] │
│ │
│ 认证管理 (双通道: electronAPI 优先, bridge 后备): │
│ ├─ webuiChangePassword / webui.changePassword.invoke │
│ ├─ webuiChangeUsername / webui.changeUsername.invoke │
│ └─ webuiGenerateQRToken / webui.generateQRToken.invoke │
│ │
│ 外部操作: │
│ ├─ shell.openExternal.invoke(url) [打开浏览器/指南] │
│ └─ navigator.clipboard.writeText(text) [复制] │
└──────────────────────────────────────────────────────────────────────┘
│ IPC Bridge + Direct IPC
┌─────────────────────▼────────────────────────────────────────────────┐
│ 主进程 (Main) - webuiBridge.ts │
│ │
│ Bridge providers: │
│ ├─ webui.getStatus → WebuiService.getStatus() │
│ ├─ webui.start → startWebServerWithInstance() │
│ ├─ webui.stop → server.close() + cleanupWebAdapter() │
│ ├─ webui.changePassword → WebuiService.changePassword() │
│ ├─ webui.changeUsername → WebuiService.changeUsername() │
│ ├─ webui.generateQRToken → generateQRLoginUrlDirect() │
│ └─ webui.verifyQRToken → verifyQRTokenDirect() │
│ │
│ Emitters (主→渲染): │
│ ├─ webui.statusChanged.emit({ running, port, localUrl }) │
│ └─ webui.resetPasswordResult.emit({ success, newPassword }) │
└─────────────────────┬────────────────────────────────────────────────┘
┌─────────────────────▼────────────────────────────────────────────────┐
│ 服务层 │
│ WebuiService: getStatus / changePassword / changeUsername / │
│ resetPassword / getLanIP │
│ webuiQR: generateQRLoginUrlDirect / verifyQRTokenDirect │
│ AuthService: validatePasswordStrength / validateUsername / │
│ generateRandomPassword / hashPassword / invalidateAllTokens │
└──────────────────────────────────────────────────────────────────────┘
```
---
## 附录 CToast 通知汇总
| 触发操作 | Toast 类型 | i18n Key |
| -------------------- | ---------- | ------------------------------------- |
| WebUI 启动成功 | success | `settings.webui.startSuccess` |
| WebUI 停止成功 | success | `settings.webui.stopSuccess` |
| 远程访问切换重启成功 | success | `settings.webui.restartSuccess` |
| 启动/停止/重启失败 | error | `settings.webui.operationFailed` |
| 复制成功 | success | `common.copySuccess` |
| 用户名修改成功 | success | `settings.webui.usernameChanged` |
| 用户名修改失败 | error | `settings.webui.usernameChangeFailed` |
| 密码修改成功 | success | `settings.webui.passwordChanged` |
| 密码修改失败 | error | `settings.webui.passwordChangeFailed` |
| QR 码生成失败 | error | `settings.webui.qrGenerateFailed` |
---
## 附录 D:已知局限汇总
| # | 功能点 | 局限描述 | 来源 |
| --- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| 1 | F-WEBUI-01 | 启动 IPC 超时 3s 后乐观设置 `running: true`,同时持久化 `enabled=true`,可能导致 UI 与实际状态不一致且下次启动循环恢复失败服务 | R2 初稿 + C-04 |
| 2 | F-WEBUI-01 | 停止操作 fire-and-forgetToast 出现时服务器可能仍在运行,停止失败用户无感知 | R2 初稿 + C-05 |
| 3 | F-WEBUI-04 | 远程访问切换需重启服务器,期间短暂不可用(最长 6s:停止 1.5s + 启动 3s + 状态确认 1.5s | R2 初稿 + C-06 |
| 4 | F-WEBUI-06 | 密码首次可见状态是组件内存状态,页面刷新后丢失 | R2 初稿 |
| 5 | F-WEBUI-06 | 密码行仅有编辑按钮,无复制功能(与用户名行功能不对称) | C-07 |
| 6 | F-WEBUI-07 | 修改密码不需要输入当前密码,安全性依赖 Electron 本地环境信任 | R2 初稿 |
| 7 | F-WEBUI-05 | 用户名后端校验错误信息为硬编码英文,未做前端 i18n 翻译(密码修改有 errorCodeMap 翻译) | C-10 |
| 8 | F-WEBUI-05/07 | Token 失效通过 JWT secret 轮转实现(被动失效),已建立的 WebSocket 连接不会立即断开 | C-13 |
| 9 | F-WEBUI-08 | QR token 存储在进程内存 Map,主进程重启后全部失效 | R2 初稿 |
| 10 | F-WEBUI-08 | `isLocalIP` 不覆盖 IPv6 ULA 地址(`fd00::/8`) | R2 初稿 |
| 11 | F-WEBUI-08 | QR 码初始状态(尚未生成)和生成失败共用同一占位文案,组件无法区分两种状态 | C-11 |
| 12 | F-WEBUI-02 | UI 未暴露端口修改入口,仅可通过配置文件/CLI/环境变量修改 | R2 初稿 |
| 13 | F-WEBUI-02 | 桌面自动恢复(`restoreDesktopWebUIFromPreferences`)不经过 `resolveWebUIPort`/`resolveRemoteAccess`,CLI 和环境变量在此路径下不生效 | C-08 |
| 14 | F-WEBUI-01 | 端口递增上限固定为 `DEFAULT_PORT + 10`,用户指定端口超出此范围时递增不生效 | C-01 |