diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2c35003 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Test & Build + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> target + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + patchelf \ + libssl-dev \ + libgtk-3-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev + + - name: Rust format check + working-directory: src-tauri + run: cargo fmt --check + + - name: Rust clippy + working-directory: src-tauri + run: cargo clippy --all-targets -- -D warnings + + - name: Rust tests + working-directory: src-tauri + run: cargo test + + - name: Install frontend dependencies + run: npm ci + + - name: Frontend tests + run: npm run test:run + + # 语言包重复 key 会被 JSON.parse 静默折叠,界面上直接显示裸 key, + # 肉眼看不出问题,只能靠脚本兜底 + - name: i18n key integrity check + run: npm run check:i18n + + - name: Frontend type-check & build + run: npm run build diff --git a/AGENTS.md b/AGENTS.md index 4eb1e2c..c7bab8e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,24 +70,24 @@ ### 4. 环境恢复(RestorePage) - **需求覆盖**: 需求 4.1-4.10 -- **实现状态**: ✅ 完成 +- **实现状态**: ✅ 完成(端口冲突检测除外,见「待完善功能」) - **核心功能**: - 备份包预览(manifest 解析、文件统计) - SHA256 完整性验证 - - 端口冲突检测与自动分配 + - 路径遍历防护(zip-slip,恶意备份包整体拒绝) - 配置文件、项目文件、SQL 文件还原(SQL 导入执行待完善) - 进度通知与错误汇总 ### 5. 基础设施模块 - **env_parser.rs**: .env 文件可靠读写,保留注释和空行(Property 9, 10) - **backup_manifest.rs**: Manifest 序列化/反序列化(Property 11, 12) -- **测试覆盖**: 后端单元测试 + 3 个集成测试(backup_restore、config_generation、workspace_commands)+ 前端 8 个组件测试(Vitest),含属性测试(proptest) +- **测试覆盖**: 后端单元测试 + 4 个集成测试(backup_restore、config_generation、workspace_commands、docker_manager)+ 前端组件测试(Vitest) ## ✅ v0.2.0 / v0.3.0 新增功能 ### 1. 前端国际化(i18n) - 中/英双语支持,运行时动态切换(vue-i18n) -- 后端日志英文化 +- ⚠️ 后端用户可见日志(ui_log!)当前为中文,i18n 化待完善 ### 2. 主题预设系统 - 自动 / 明亮 / 暗黑三种模式,全组件适配 @@ -115,9 +115,9 @@ 3. **测试**: - **单元测试**:放在源文件内的 `#[cfg(test)] mod tests { ... }` 模块中 - **集成测试**:放在 `src-tauri/tests/integration/` 目录下 - - 纯函数模块(env_parser、backup_manifest、config_generator)使用 `proptest` 进行属性测试 - - 标签格式:`// Feature: env-config-and-backup, Property N: {property_text}` - - 运行测试:`cargo test` + - 解析/生成类纯函数应抽为可直接测试的函数,测试必须包含真断言(禁止 `assert!(true)` 占位) + - 安全或回归类测试建议标注:`// Feature: {feature}, Property: {描述}` + - 运行测试:`cargo test`;CI 同时执行 `cargo fmt --check` 与 `cargo clippy --all-targets -- -D warnings` 4. **模块注册**: - 新增引擎模块需在 `engine/mod.rs` 中声明 `pub mod xxx;` - 新增命令子模块需在 `commands/mod.rs` 中声明并 re-export @@ -183,8 +183,8 @@ - 特性: - 备份预览(manifest 解析) - SHA256 完整性验证 - - 端口冲突检测与自动分配 - - 配置文件、项目文件还原(数据库 SQL 导入待完善) + - 路径遍历防护(zip-slip) + - 配置文件、项目文件还原(端口冲突检测、数据库 SQL 导入待完善) ### 7. 备份清单(v0.1.0 新增) - 位置:`src-tauri/src/engine/backup_manifest.rs` @@ -302,7 +302,7 @@ 3. **权限校验**: 若新增了 Tauri 插件调用,请务必更新 `src-tauri/capabilities/default.json`。 4. **TDD 流程**: - 优先编写单元测试 - - 纯函数模块使用 proptest 进行属性测试 + - 解析/生成类逻辑先抽纯函数再测 - 运行 `cargo test` 确保所有测试通过 5. **类型同步**: 修改 Rust 数据结构后,同步更新 `src/types/` 中的 TypeScript 类型定义。 @@ -339,6 +339,10 @@ ### 待完善功能 +#### 恢复端口冲突检测(中优先级) +- **当前状态**:恢复直接使用备份包中的端口,不检测宿主机端口冲突(早期文档宣称已实现,实际从未落地) +- **待完善**:恢复预览时读取 manifest.services 的端口映射,与当前运行容器/本机占用比对,冲突时提示改端口 + #### 数据库备份/恢复(低优先级) - **目标**:完善备份/恢复引擎中的数据库导出与导入 - **当前状态**:备份不导出数据库(`BackupOptions` 无 `include_database`),恢复仅提取 SQL 文件到本地、不执行导入 @@ -348,6 +352,10 @@ - 事务性恢复(失败时回滚) - **优先级**: 低(当前版本可使用手动方式备份/恢复数据库) +#### 后端日志 i18n(低优先级) +- **当前状态**:`ui_log!` 输出的用户可见日志为中文 + emoji,英文界面下日志面板仍显示中文 +- **建议策略**:结构化字段(模块、级别)保持英文,面向用户的固定短语如需双语,由前端按 key 翻译;不做完整后端 i18n + ### 开发建议 1. **稳定优先**: v0.3.1 重点是稳定性和用户体验优化 2. **Bug 修复**: 优先处理用户反馈的问题和边界情况 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0156438..4404d78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,46 @@ ### 待发布功能 - 虚拟主机管理(Nginx 站点配置) +### 🐛 修复 +- 一键停止改用 `docker compose stop`,保留容器并展示已停用状态 +- 统一服务模板解析路径,释放后不覆盖用户修改 +- 容器状态 running 文案中文化 +- 恢复引擎拒绝含路径遍历条目的备份包,堵住 zip-slip 漏洞 +- 修复 Nginx 多版本配置回读时非默认端口被错误重置为 80 的问题 +- 修复环境配置页多 PHP 服务时自定义扩展输入互相串扰的问题 +- 修复 `scripts/` 下两个同步脚本的单元测试因 shebang 被 vitest 报 SyntaxError、整个 suite 无法加载的问题 +- 修复日志文件每次启动被清空、现场日志一重启即丢失的问题(改为轮转保留最近 3 份) +- 修复日志锁被毒化后每次写日志连锁 panic 的问题 +- 修复备份打包大文件时整体读入内存的问题(改为 64KB 分块流式写入) +- 修复 `path.file_name().unwrap()` 对无名路径 panic 的问题 +- 修复 Docker 不可用时仍每 5 秒全量轮询、且每轮都刷一条失败日志的问题 +- 修复日志面板「复制」按钮复制的是后端文件日志全文、与界面显示内容对不上的问题 +- 修复配置的工作区目录不存在时数据被静默写到默认目录的问题(改为先创建目录;创建失败才回退并在界面告警) +- 修复镜像拉取命令为同步执行、下载大镜像时整个窗口「未响应」的问题(改为 async + spawn_blocking) +- 修复 `envConfig` 语言节点下存在两个同名 `toast`、JSON 解析时后者静默覆盖前者,导致应用配置后的成功提示直接显示为裸 key(`envConfig.toast.applySuccess` / `backedUp`)的问题(合并节点,恢复被覆盖的 8 条文案) +- 修复 Nginx 容器启动即崩溃、无限重启的问题:Dockerfile 末尾 `USER nginx` 使 master 进程无权限创建 `/var/cache/nginx/client_temp`、也无法 bind 80 端口(改为 root 运行 master,worker 仍为 `nginx`,PUID/PGID 映射不变) +- 修复 PUID/PGID 用户映射在默认配置下从不生效的问题:判断条件写成「PUID/PGID 不等于 1000 才调整」,而默认值本身就是 1000,等于永远跳过(改为「与镜像内当前 UID/GID 不一致才调整」) +- 修复 Nginx 镜像模板只支持 alpine 基础镜像的问题:原实现用 `deluser/adduser`,若把 `NGINX*_VERSION` 换成 debian 版(如 `nginx:1.28`)则构建直接失败;现在按基础镜像分发命令(alpine 用 `deluser/adduser`,debian 用 `groupmod/usermod`)。PHP 模板同样补齐 alpine 版兼容性 + +### 🔧 改进 +- 环境配置页 .env 解析主体抽为纯函数并以真断言测试覆盖 +- 配置生成集成测试替换为端到端断言(validate / .env / compose / 自定义变量保留) +- 清理死代码:未注册使用的占位命令、无效占位测试、未使用的 proptest 依赖 +- 新增 CI 测试流水线(fmt / clippy / cargo test / 前端测试 / 构建) +- 补充 MIT LICENSE 文件,声明 Cargo `license` 字段 +- 恢复前自动生成回滚包(`.restore_rollback_<时间戳>.zip`),恢复出错可据此回退 +- 恢复结果由「成功/失败一句话」改为返回完整明细,前端展示已恢复文件列表、逐条错误与回滚包路径 +- 环境启动流程的 Docker 同步调用(`compose down`、`up` 等待、日志轮询)统一走 `spawn_blocking`,不再阻塞 async 执行线程 +- **容器状态改为前后端共享的枚举契约**(`running`/`exited`/...),替换此前 `format!("{:?}")` 产出 `"Some(RUNNING)"` 再由前端 `includes('running')` 猜测的脆弱做法 +- **路径解析收口为 `commands/paths.rs` 单一模块**;用户级配置 `workspace.json` 与日志迁至 Tauri 官方 `app_data_dir`,解决装进 `Program Files` 后无写权限的问题,首次启动自动从旧位置迁移 +- 日志面板新增「清空」与「导出」动作(导出走后端文件日志,落盘到用户指定位置) +- 轮询间隔随 Docker 连续失败次数退避(5s → 15s → 30s),恢复后立即回到 5s +- 容器状态灯改为仅运行中时脉冲,停止状态不再一直闪烁 +- 新增 `pnpm check:i18n` 检查脚本(`scripts/check-i18n-keys.mjs`):扫描语言包同层级重复 key、中英 key 集合一致性、源码 `t()` 引用的 key 是否存在,并配 10 条单元测试防回归 + ### 📁 文档 - 合并 `doc/` 与 `docs/` 为统一的 `docs/` 目录,更新 README、AGENTS、文档索引中的路径引用 +- 修订 AGENTS.md 与实际实现不符的描述(proptest 属性测试、恢复端口冲突检测、后端日志英文化),未实现项移入「待完善功能」 --- diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..093a9b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 jeeinn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/IMPROVEMENT_REPORT.md b/docs/IMPROVEMENT_REPORT.md index 51d1eab..58cdd23 100644 --- a/docs/IMPROVEMENT_REPORT.md +++ b/docs/IMPROVEMENT_REPORT.md @@ -3,6 +3,8 @@ > **评审版本**: v0.3.1(2026-09-18) > **评审方式**: 全量阅读前端(src/,约 5600 行)与后端(src-tauri/src/,约 6600 行)核心代码,结合 CI、文档、测试的量化检查 > **评审原则**: 一切建议服从四个指标——**简单、简洁、实用、可靠**。凡与"轻量级配置管理工具"定位冲突的"高级架构",一律不推荐。 +> +> **进度标记说明**(2026-09-21 更新):每条建议标题后以 `[已完成]` / `[部分完成]` / `[未开始]` 标注当前状态;有实现偏差的条目在正文末尾附"实现偏差"说明。第二批 8 项已全部完成,详见第 6 节路线图。 --- @@ -34,19 +36,19 @@ > 「可靠」是四个指标中当前欠账最多的一项。以下按严重程度排序。 -### R1.【P0·安全】恢复引擎存在 Zip-Slip 路径遍历风险 +### R1.【P0·安全】恢复引擎存在 Zip-Slip 路径遍历风险 `[已完成]` `src-tauri/src/engine/restore_engine.rs:268-287`:`extract_prefix` 将 ZIP 条目名直接 `target_dir.join(relative)` 写盘,**未校验路径是否逃逸目标目录**。恶意构造的备份包(条目名如 `../../../x` 或 Windows 绝对路径 `C:\...`)可把文件写到工作区之外的任意位置。`projects/` 前缀恢复(restore_engine.rs:174)直接解到 `project_root`,同样暴露。 **修复**(约 20 行):解压前对每个条目做 canonicalize 前缀校验——拼接后确认 `target_path` 仍在 `target_dir` 之内,并拒绝含 `..`、绝对路径、盘符的条目名。同时为 `extract_file_to_path` 补同样的防护。**配套测试**:构造恶意 ZIP 的回归测试(`test_restore_rejects_path_traversal`)。 -### R2.【P0·可靠】恢复会直接覆盖现有文件,且无恢复前自动备份 +### R2.【P0·可靠】恢复会直接覆盖现有文件,且无恢复前自动备份 `[已完成]` 恢复流程(.env、docker-compose.yml、services/)逐文件 `fs::write` 覆盖(restore_engine.rs:111-189),中途失败则留下**半恢复状态**,用户当前可用环境被破坏且无法一键回退。对一个"迁移工具"而言这是最伤信任的故障模式。 **修复**(低成本方案,不需要事务框架):`execute_restore` 开始前,把即将被覆盖的文件打包成一份自动备份 zip(复用 BackupEngine),放在工作区 `.restore_rollback_.zip`;恢复报错时在结果中提示回滚包路径。约 30-50 行。 -### R3.【P1·可靠】Docker 启动流程在 async 命令中执行阻塞 IO +### R3.【P1·可靠】Docker 启动流程在 async 命令中执行阻塞 IO `[已完成]` `src-tauri/src/commands/env_config.rs` 的 `start_environment`: - `down_cmd.output()`(:406)、`child.wait()`(:587)为同步阻塞调用; @@ -56,25 +58,25 @@ **修复**:把这 3 类同步 `Command` 调用包进 `tokio::task::spawn_blocking`(项目在 `commands/backup.rs:23` 已有此模式的先例,保持一致即可)。`create_backup` 里的 `spawn_blocking` + `block_on` 双层嵌套(backup.rs:23-34)可顺手简化为单层。 -### R4.【P1·可靠】容器状态用 `format!("{:?}")` 字符串作为前后端契约 +### R4.【P1·可靠】容器状态用 `format!("{:?}")` 字符串作为前后端契约 `[已完成]` `src-tauri/src/docker/manager.rs:57` 把 bollard 的枚举序列化成 `"Some(RUNNING)"` 这类 Debug 字符串,前端再用 `includes('running')` 猜测解析(`src/App.vue:79-83`),后端自己也写注释承认这个格式问题(manager.rs:122)。契约脆弱,Docker 升级或 bollard 版本变化即碎。 **修复**:`PsContainer.state` 改为 `#[serde(rename_all="lowercase")]` 的枚举(`running/exited/paused/...`),前后端类型同步更新(`src/types/` 新增对应类型)。约 40 行,一劳永逸。 -### R5.【P1·可靠】备份将整个文件读入内存 +### R5.【P1·可靠】备份将整个文件读入内存 `[已完成]` `src-tauri/src/engine/backup_engine.rs:123,256`:所有文件(含可选的项目文件)先 `fs::read` 全量进内存再写 ZIP。用户勾选"包含项目文件"后,单个大文件(如数据库 dump、视频素材)即可导致内存飙升。 **修复**:`add_file_to_zip` 改为 `io::copy` 流式写入(zip crate 支持 `std::io::Read` 源),SHA256 用分块读取计算。API 签名从 `&[u8]` 改为 `&Path`,约 40 行。 -### R6.【P2·可靠】日志文件每次启动清空,无轮转 +### R6.【P2·可靠】日志文件每次启动清空,无轮转 `[已完成]` `src-tauri/src/logging.rs:19-24` 使用 `truncate(true)`,每次启动覆盖 `php-stack.log`。用户遇到问题后重启应用,现场日志即丢失,"导出日志"命令(`commands/workspace.rs:195`)也随之失去排查价值。 **修复**:保留最近 3 份按启动时间命名的日志(`php-stack.1.log` 轮转),或至少改为 append + 启动时写入分隔线。约 20 行。 -### R7.【P2·可靠】零散 panic 风险点 +### R7.【P2·可靠】零散 panic 风险点 `[部分完成]` - `backup_engine.rs:250`:`path.file_name().unwrap()` 对无名路径(如盘符根)会 panic; - `logging.rs:27,62`:`Mutex::lock().unwrap()`,另一处持锁 panic 会毒化全局日志锁; @@ -82,13 +84,15 @@ **修复**:均改为 `map_err` 降级处理,合计 15 行。 +**实现偏差(2026-09-21)**:三处里已修两处(`backup_engine.rs` 的 `file_name().unwrap()`、`logging.rs` 的两处 `Mutex::lock().unwrap()`),都是在 R5 / R6 的改动中顺带完成的。`version_manifest.rs` 的 `include_str!` + `expect` 未动——它是编译期嵌入的固定资源,解析失败等于打包损坏,属于"启动即崩"的正确行为,改降级反而会掩盖问题;报告本身也标注"可接受,但建议错误信息带上实际解析错误"。 + --- ## 2. 架构设计 > 总体分层(commands → engine → docker)是清晰的,不需要推倒重来。以下是"简洁"维度的具体欠账。 -### A1.【P0】路径解析逻辑重复三份,且生产模式写到 exe 同级目录 +### A1.【P0】路径解析逻辑重复三份,且生产模式写到 exe 同级目录 `[已完成]` 同一段"开发模式向上爬 4 层 / 生产模式取 exe 父目录"的逻辑出现在: - `src-tauri/src/commands/mod.rs:17-45`(`get_project_root`) @@ -102,78 +106,86 @@ 2. 用户级配置(workspace.json、.user_*.json、日志)迁移到 Tauri 官方 `app.path().app_data_dir()`(通过 `AppHandle` 传入,不再自己爬目录);工作区内的文件(.env、services/、备份)保持原地不动——它们本来就是"工作区"的一部分。 3. 首次启动时检测旧位置文件并自动迁移 + 提示。 -### A2.【P1】.env 键名解析依赖字符串前缀切片 +**实现偏差(2026-09-21)**:本轮只迁移了 `workspace.json` 与日志文件,`.user_mirror_config.json` / `.user_version_overrides.json` **保留在项目根**。原因:这两个文件被 `backup_engine` 按项目根打包、`restore_engine` 按项目根还原,且语义上随工作区走(换工作区就该换一套覆盖)——迁到 app_data_dir 会直接打断备份/恢复闭环,收益不抵风险。若后续要做,需连同备份/恢复管道一起改,届时应作为一个独立改动统一处理。 + +**实现方式**:`app_data_dir` 由 setup 注入一次后存入 `OnceLock`,而不是把 `AppHandle` 透传进 20 余处命令——后者会让所有命令签名膨胀,与"简单"原则冲突。未注入时回退旧逻辑,保证单元测试可用。 + +### A2.【P1】.env 键名解析依赖字符串前缀切片 `[未开始]` `commands/env_config.rs:139-263` 的 `load_existing_config` 用 `&key[3..key.len()-8]`、`&key[6..key.len()-8]` 这类魔数切片反解 `PHP82_VERSION` / `NGINX127_HTTP_HOST_PORT`,且四个服务的解析代码高度雷同。新增服务类型或前缀规则变化时极易出错(NGINX 的"5 个字母 + 1 = 6"注释就是危险信号)。 **修复**:已有 `VersionManifest::find_entry_by_env_prefix`(version_manifest.rs:76)能做前缀反查,应反向利用——遍历 manifest 中所有 `service_dir` 生成前缀去匹配 env 键,替代手写切片;四个服务的重复循环抽成一个泛型函数。约 60 行替换 120 行。 -### A3.【P1】前端缺少统一的 API 层 +### A3.【P1】前端缺少统一的 API 层 `[未开始]` `invoke` 直接散落在 8 个组件里,错误处理模式各不相同:EnvConfigPage 有 `formatErrorMessage`(EnvConfigPage.vue:208-236),其他页面直接 `showToast(e as string)`,Dashboard 只写日志不弹提示(App.vue:121)。`e as string` 假设后端总是返回字符串,遇到非字符串错误时会显示 `[object Object]`。 **修复**:新建 `src/api/` 目录,按域封装(`docker.ts` / `envConfig.ts` / `mirror.ts` / `backup.ts`),统一做三件事:错误规范化(`String(e)` + 剥离 `PORT_CONFLICT:` 等协议前缀)、loading 注入、类型标注。组件只调用 api 函数。这是后续所有前端改进的地基,约 1 天。 -### A4.【P2】镜像源三个模块职责重叠 +### A4.【P2】镜像源三个模块职责重叠 `[未开始]` `mirror_manager.rs`(379 行,预设+测试)、`mirror_config.rs`(342 行,标注"向后兼容")、`mirror_config_manager.rs`(325 行,用户配置)三者共存,"向后兼容"模块中仍有活跃逻辑。建议做一次收束:明确一个对外门面(`mirror_config_manager`),把 `mirror_config.rs` 的兼容层真正冻结或删除。非紧急,但拖越久越难拆。 -### A5.【P2】应用数据文件清单没有单一事实来源 +### A5.【P2】应用数据文件清单没有单一事实来源 `[未开始]` 当前磁盘写入物散布:项目根(.env、docker-compose.yml、services/、data/、logs/、.user_mirror_config.json、.user_version_overrides.json)、exe 目录(workspace.json、php-stack.log)。建议在 `doc/architecture/ARCHITECTURE.md` 增加一张"应用写入了什么、在哪、谁负责"的表,并作为 A1 重构的验收依据。 -### A6.【P2】死代码与占位实现 +### A6.【P2】死代码与占位实现 `[已完成]` - `commands/backup.rs:79-83`:`select_project_folder` 是空占位(返回 `Ok(None)`),却注册在 invoke_handler(lib.rs:73),前端从未调用 → 删除; - `App.vue:53-61`:两个 `@ts-ignore` 的 computed(hasAnyContainers/hasStoppedContainers)→ 删除; - `App.vue:152-181`:`openServiceConfig` 四个分支执行完全相同的 `replace('ps-','')` → 合并为一行; - `App.vue:21-28`:`Container` 接口字段用了包装对象类型 `String` 而非 `string` → 修正。 +**实现偏差(2026-09-21)**:容器相关部分已在 R4 提交中一并完成——`Container` 接口改为 `src/types/docker.ts` 的强类型定义(state 为 `ContainerState` 枚举),`@ts-ignore` 的两个 computed 已删除。`openServiceConfig` 四个分支合并、`App.vue` 的 `String` 残留未动,属纯样式清理。 + --- ## 3. 用户体验 > 界面风格统一、步骤式恢复向导、启动确认弹窗等都做得不错。以下是实际使用中会碰到的摩擦点。 -### U1.【P0·功能缺陷】多个 PHP 服务时自定义扩展输入互相污染 +### U1.【P0·功能缺陷】多个 PHP 服务时自定义扩展输入互相污染 `[已完成]` `src/components/EnvConfigPage.vue:32` 只有一个 `customExtInput`,而 `syncCustomExtensions(phpIndex)`(:503-521)把这个共享输入合并进任意一个 PHP 服务。用户给 php82 填了 `xdebug`,切到 php85 的面板时同一个输入框内容还在,保存时会错误地合并进去。 **修复**:`customExtInput` 改为 `Record`(项目里 `phpExtensionsPanelState` 已是同款模式,照抄即可)。约 10 行。**这是真实功能 bug,建议单独出一个 fix 提交。** -### U2.【P1】恢复结果只有"成功/失败"一句话,错误明细不可读 +### U2.【P1】恢复结果只有"成功/失败"一句话,错误明细不可读 `[已完成]` 后端 `execute_restore` 把多行 errors 用 `\n` 拼进 Err(commands/backup.rs:70-74),前端 `showToast(e as string)` 弹一个 3 秒的 toast(RestorePage.vue:157)——用户既看不全也来不及看。而 `RestoreResult.restored_files` 明细后端算好了却完全没展示。 **修复**:`execute_restore` 改为始终返回 `RestoreResult`,前端在恢复步骤(RestorePage 第 4 步)渲染结果卡片:恢复文件列表 + 错误列表 + (配合 R2 的)回滚包路径提示。 -### U3.【P1】Docker 不可用时仍每 5 秒全量轮询 +### U3.【P1】Docker 不可用时仍每 5 秒全量轮询 `[已完成]` `App.vue:339` 的 `setInterval` 永不清除(且组件卸载语义上也不需要,但 Docker 不可用时依旧每 5 秒 ping + 拉列表 + 写日志),Docker Desktop 未启动时日志面板会被 `dockerCheckFailed` 每隔 5 秒刷一条。 **修复**:连续失败 N 次后退避到 15-30 秒;Docker 恢复后恢复 5 秒。同时顺手把轮询间隔与"运行中"状态灯的 animate-pulse 区分开。 -### U4.【P1】UI 日志与文件日志是两套东西,复制按钮语义混乱 +### U4.【P1】UI 日志与文件日志是两套东西,复制按钮语义混乱 `[已完成]` 日志面板显示的是内存中最近 50 条(`useToast.ts:20`,超过即丢),"复制"按钮复制的却是后端文件日志全文(`App.vue:391-398` → `export_logs`)。用户在界面上看到的错误未必复制得出来(50 条以前),复制出来的内容又和看到的对不上。另外面板没有"清空"按钮,长会话下满屏历史。 **修复**:复制按钮复制 UI 日志原文;面板加"清空"与"导出文件"两个动作(导出走后端文件日志);文件日志本身配合 R6 做轮转。 -### U5.【P2】i18n 收尾 +### U5.【P2】i18n 收尾 `[部分完成]` - 残留硬编码中文运行时文案:`useConfirmDialog.ts:25-27`(默认"确认操作/确认/取消",英文界面下弹中文按钮)、`App.vue:465`(侧边栏折叠按钮 title)、`App.vue:639`(复制日志 title); - 后端用户可见日志(ui_log!)全部是中文 + emoji(如 env_config.rs 全文),英文界面下日志面板是中文。AGENTS.md 宣称"后端日志英文化"并未达成。 - **建议**:前端三处立即修;后端日志选择一个现实策略——要么承认"UI 日志面向中文用户、仅结构化字段英文",写进文档;要么把 ui_log 的固定短语做成 key 传给前端翻译。不建议做完整后端 i18n(违背"简单"原则)。 -### U6.【P2】WorkspaceInitDialog 不适配亮色主题 +**实现偏差(2026-09-21)**:只完成了 `App.vue` 复制日志按钮的硬编码 title(U4 提交中顺带改掉,新增 `dashboard.log.copyTip`)。`useConfirmDialog.ts` 的默认按钮文案与侧边栏折叠 title 未动,后端日志 i18n 按报告建议**明确不做**(违背"简单"原则)——这一策略尚未写进 AGENTS.md,建议后续补。 + +### U6.【P2】WorkspaceInitDialog 不适配亮色主题 `[未开始]` 对话框写死暗色(`bg-slate-900 border-slate-700`,无 `dark:` 前缀),亮色模式下是全应用唯一的突兀深色弹窗;确认后 `window.location.reload()` 整页刷新(WorkspaceInitDialog.vue:44),丢失所有未保存状态。建议:补 `dark:` 适配;reload 改为事件通知各页面重新加载工作区信息。 -### U7.【P2】EnvConfigPage 内置的前端版本 fallback 列表会烂掉 +### U7.【P2】EnvConfigPage 内置的前端版本 fallback 列表会烂掉 `[未开始]` `EnvConfigPage.vue:179-203` 在后端加载失败时使用一份硬编码版本清单,与 `services/version_manifest.json` 双源维护。建议失败时显示明确错误 + 重试按钮,而不是降级到一份必然过期的静态数据(备份一段"应急可用"的体验,代价是永久的双维护)。 -### U8.【P2】清理调试输出与提高键盘可达性 +### U8.【P2】清理调试输出与提高键盘可达性 `[未开始]` - 22 处 `console.log`(主要集中在 EnvConfigPage 的加载流程)清理或降级为统一 debug 开关; - 侧边栏导航与日志按钮是 `div @click`(App.vue:417-453),无 Tab 焦点、无 Enter 触发。桌面应用要求不高,但改成 `