523 lines
31 KiB
Markdown
523 lines
31 KiB
Markdown
# 设置页 → 远程连接 — 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`),显示 loading,toast 提示"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 路由和静态资源可被注册
|
||
- [ ] 命名空间化和保留路径校验生效
|
||
- [ ] 路径遍历尝试被阻止
|
||
- [ ] 路径冲突时后者被跳过(不覆盖先注册的)
|
||
|
||
---
|
||
|
||
## 附录 A:WebUI 状态矩阵
|
||
|
||
| WebUI 开关 | 允许远程 | 访问地址 | QR 码区域 | 登录信息 |
|
||
| ---------- | -------- | ------------------------- | --------- | -------- |
|
||
| OFF | (any) | 不显示 | 不显示 | 显示 |
|
||
| ON | OFF | `http://localhost:{port}` | 不显示 | 显示 |
|
||
| ON | ON | `http://{lanIP}:{port}` | 显示 | 显示 |
|
||
|
||
---
|
||
|
||
## 附录 B:IPC 通信链路
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────┐
|
||
│ 渲染进程 (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 │
|
||
└──────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 附录 C:Toast 通知汇总
|
||
|
||
| 触发操作 | 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-forget,Toast 出现时服务器可能仍在运行,停止失败用户无感知 | 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 |
|