Back to Aionui

设置页 → 远程连接 — WebUI 服务 (F-WEBUI)

docs/prds/remote/webui/webui.md

2.1.1631.2 KB
Original Source

设置页 → 远程连接 — WebUI 服务 (F-WEBUI)

本文档覆盖「设置 → 远程连接」页面 WebUI Tab 的全部功能,包括服务启停、远程访问控制、认证管理、QR 码登录、配置持久化、页面结构、状态同步、扩展系统 WebUI 贡献。 基于静态代码分析和动态 UI 验证综合整理。

Channels Tab 相关内容见 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.10.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 检查目录

已知局限

  • wsHandlersmiddleware 已声明但运行时不支持(仅 console.warn 提示)

验收标准

  • 合法的扩展 API 路由和静态资源可被注册
  • 命名空间化和保留路径校验生效
  • 路径遍历尝试被阻止
  • 路径冲突时后者被跳过(不覆盖先注册的)

附录 A:WebUI 状态矩阵

WebUI 开关允许远程访问地址QR 码区域登录信息
OFF(any)不显示不显示显示
ONOFFhttp://localhost:{port}不显示显示
ONONhttp://{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 启动成功successsettings.webui.startSuccess
WebUI 停止成功successsettings.webui.stopSuccess
远程访问切换重启成功successsettings.webui.restartSuccess
启动/停止/重启失败errorsettings.webui.operationFailed
复制成功successcommon.copySuccess
用户名修改成功successsettings.webui.usernameChanged
用户名修改失败errorsettings.webui.usernameChangeFailed
密码修改成功successsettings.webui.passwordChanged
密码修改失败errorsettings.webui.passwordChangeFailed
QR 码生成失败errorsettings.webui.qrGenerateFailed

附录 D:已知局限汇总

#功能点局限描述来源
1F-WEBUI-01启动 IPC 超时 3s 后乐观设置 running: true,同时持久化 enabled=true,可能导致 UI 与实际状态不一致且下次启动循环恢复失败服务R2 初稿 + C-04
2F-WEBUI-01停止操作 fire-and-forget,Toast 出现时服务器可能仍在运行,停止失败用户无感知R2 初稿 + C-05
3F-WEBUI-04远程访问切换需重启服务器,期间短暂不可用(最长 6s:停止 1.5s + 启动 3s + 状态确认 1.5s)R2 初稿 + C-06
4F-WEBUI-06密码首次可见状态是组件内存状态,页面刷新后丢失R2 初稿
5F-WEBUI-06密码行仅有编辑按钮,无复制功能(与用户名行功能不对称)C-07
6F-WEBUI-07修改密码不需要输入当前密码,安全性依赖 Electron 本地环境信任R2 初稿
7F-WEBUI-05用户名后端校验错误信息为硬编码英文,未做前端 i18n 翻译(密码修改有 errorCodeMap 翻译)C-10
8F-WEBUI-05/07Token 失效通过 JWT secret 轮转实现(被动失效),已建立的 WebSocket 连接不会立即断开C-13
9F-WEBUI-08QR token 存储在进程内存 Map,主进程重启后全部失效R2 初稿
10F-WEBUI-08isLocalIP 不覆盖 IPv6 ULA 地址(fd00::/8R2 初稿
11F-WEBUI-08QR 码初始状态(尚未生成)和生成失败共用同一占位文案,组件无法区分两种状态C-11
12F-WEBUI-02UI 未暴露端口修改入口,仅可通过配置文件/CLI/环境变量修改R2 初稿
13F-WEBUI-02桌面自动恢复(restoreDesktopWebUIFromPreferences)不经过 resolveWebUIPort/resolveRemoteAccess,CLI 和环境变量在此路径下不生效C-08
14F-WEBUI-01端口递增上限固定为 DEFAULT_PORT + 10,用户指定端口超出此范围时递增不生效C-01