docs/prds/remote/webui/webui.md
本文档覆盖「设置 → 远程连接」页面 WebUI Tab 的全部功能,包括服务启停、远程访问控制、认证管理、QR 码登录、配置持久化、页面结构、状态同步、扩展系统 WebUI 贡献。 基于静态代码分析和动态 UI 验证综合整理。
Channels Tab 相关内容见 channels.md。
用户故事:作为桌面端用户,我希望通过一个开关快速启用或禁用 WebUI 服务,以便在需要时通过浏览器远程访问 AionUi。
前置条件:运行在 Electron 桌面端(isElectronDesktop() === true)
正常流程(用户视角):
异常情况:
settings.webui.operationFailed)port + 1),递增上限固定为 DEFAULT_PORT + 10(默认端口:生产 25808,开发 25809,上限 25818/25819);当用户通过 CLI/环境变量指定端口超出此范围时,不触发递增,直接报错settings.webui.stopSuccess)→ 然后才异步调用 webui.stop.invoke()。Toast 出现时服务器可能仍在运行验收标准:
settings.webui.starting 实际值为准)settings.webui.startSuccess / settings.webui.stopSuccess)settings.webui.operationFailed)DEFAULT_PORT + 10用户故事:作为用户,我希望启用 WebUI 后下次打开应用时能自动恢复服务,无需每次手动开启。
正常流程(用户视角):
webui.desktop.enabled = true 写入 ConfigStorage配置来源:
端口解析(覆盖式优先级):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) |
异常情况:
{},不报错验收标准:
用户故事:作为用户,我希望 WebUI 启动后能看到访问地址,方便在浏览器中打开或分享给他人。
前置条件:WebUI 服务正在运行(status.running === true)
正常流程(用户视角):
http://localhost:{port}http://{局域网IP}:{port}(如 http://192.168.3.15:25809)异常情况:
localhost验收标准:
用户故事:作为用户,我希望控制是否允许局域网内其他设备访问 WebUI,以便用手机或其他电脑访问 AionUi。
正常流程(用户视角):
127.0.0.1 到 0.0.0.0),显示 loading,toast 提示"WebUI 已重启"localhost 变为局域网 IP,QR 码区域出现(见 F-WEBUI-08)异常情况:
settings.webui.operationFailed)settings.webui.operationFailed)shell.openExternal)验收标准:
settings.webui.restartSuccess)settings.webui.operationFailed)用户故事:作为用户,我希望能查看和修改 WebUI 的登录用户名,以便个性化我的登录凭据。
正常流程(用户视角):
表单校验规则:
| 规则 | 前端校验 | 后端校验 | 错误提示 |
|---|---|---|---|
| 必填 | 是 | - | 前端 i18n 提示 |
| 最少 3 字符 | 是 | 是 | 前端 i18n / 后端英文原文(未 i18n 化) |
| 最多 32 字符 | 是 | 是 | 前端 i18n / 后端英文原文(未 i18n 化) |
仅允许 [a-zA-Z0-9_-] | 是 | 是 | 前端 i18n / 后端英文原文(未 i18n 化) |
不能以 _ 或 - 开头/结尾 | 是 | 是 | 前端 i18n / 后端英文原文(未 i18n 化) |
| 用户名已存在(不同用户) | - | 是 | "Username already exists"(英文原文) |
异常情况:
settings.webui.usernameChangeFailed)验收标准:
用户故事:作为用户,我希望在首次启动 WebUI 时能看到系统生成的初始密码,之后密码显示为遮罩状态,以保护安全。
正常流程(用户视角):
!@#$%^&*)异常情况:
canShowPlainPassword)是组件内存状态,页面刷新后重置为遮罩验收标准:
用户故事:作为用户,我希望能设置自定义密码替换系统生成的初始密码,或在忘记密码时直接设置新密码。
正常流程(用户视角):
表单校验规则:
| 规则 | 校验端 | 错误提示 |
|---|---|---|
| 新密码必填 | 前端 | "请输入新密码" |
| 最少 8 字符 | 前端+后端 | i18n 提示 / PASSWORD_TOO_SHORT |
| 最多 128 字符 | 后端 | PASSWORD_TOO_LONG |
| 弱密码黑名单 | 后端 | PASSWORD_TOO_COMMON |
| 确认密码必填 | 前端 | "请再次输入新密码" |
| 两次密码一致 | 前端 | "两次密码不一致" |
弱密码黑名单:password, 12345678, 123456789, qwertyui, abcdefgh
异常情况:
'; ' 分隔):前端翻译后合并显示(如 PASSWORD_TOO_SHORT → i18n 短密码提示)技术说明:
invalidateAllTokens 生成新 secret)实现。这是被动失效机制 — 旧 token 不会被主动推送下线,而是在下次请求时验证失败被拒绝。已建立的 WebSocket 连接不会立即断开,需等到心跳或重连时才会被拒绝clearInitialAdminPassword() 清除内存中的初始密码,后续 getStatus 不再返回 initialPassword验收标准:
settings.webui.passwordChanged)+ 弹窗关闭 + 密码切换为遮罩用户故事:作为用户,我希望在允许远程访问时能通过手机扫描二维码快速登录 WebUI,无需手动输入地址和密码。
前置条件:WebUI 服务运行中(status.running === true)且允许远程访问(status.allowRemote === true)
正常流程(用户视角):
toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }))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,进程重启后全部失效 |
自动生成/清除逻辑:
异常情况:
settings.webui.qrGenerateFailed)+ toast(同一 i18n key)。注意:初始状态(尚未生成)和生成失败共用同一占位文案,组件无法区分两种状态common.loading 对应文案(以 i18n 实际值为准),刷新按钮显示旋转动画验收标准:
用户故事:作为用户,我希望在"远程连接"设置页面中方便地切换 WebUI 服务配置和 Channel 渠道配置。
正常流程(用户视角):
页面布局:
#/settings/webui验收标准:
用户故事:作为用户,我希望设置页面能实时反映 WebUI 服务的最新状态,即使状态由其他来源(如应用菜单、启动恢复)触发变更。
正常流程(用户视角):
IPC 双通道机制:
electronAPI.webuiGetStatus()(直接 IPC,无超时问题)webui.getStatus.invoke()(bridge 模式,1.5s 超时)webui.statusChanged.on() 监听主进程推送的状态变更异常情况:
running: false, port: DEFAULT_PORT, adminUsername: 'admin')验收标准:
用户故事:作为扩展开发者,我希望能通过扩展 manifest 声明 API 路由和静态资源,自动注册到 WebUI 服务器。
正常流程(开发者视角):
manifest.contributes.webui 中声明 apiRoutes 和/或 staticAssetsresolveWebuiContributions 校验并注册贡献安全校验规则:
| 规则 | 说明 |
|---|---|
| 命名空间化 | 路径必须以 /{extensionName}/ 开头 |
| 保留路径保护 | 不允许使用 /, /api, /login, /logout, /qr-login, /static, /assets |
| 路径冲突检测 | 跨扩展重复路径,后者被跳过 |
| 路径遍历防护 | isPathWithinDirectory 确保所有文件在扩展目录内 |
| 入口存在性检查 | 同时查找 dist 和 source 路径 |
| 静态资源目录存在性 | existsSync 检查目录 |
已知局限:
wsHandlers 和 middleware 已声明但运行时不支持(仅 console.warn 提示)验收标准:
| WebUI 开关 | 允许远程 | 访问地址 | QR 码区域 | 登录信息 |
|---|---|---|---|---|
| OFF | (any) | 不显示 | 不显示 | 显示 |
| ON | OFF | http://localhost:{port} | 不显示 | 显示 |
| ON | ON | http://{lanIP}:{port} | 显示 | 显示 |
┌──────────────────────────────────────────────────────────────────────┐
│ 渲染进程 (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 │
└──────────────────────────────────────────────────────────────────────┘
| 触发操作 | 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 |
| # | 功能点 | 局限描述 | 来源 |
|---|---|---|---|
| 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 |