diff --git a/config/feature_flags.yaml b/config/feature_flags.yaml index f57c7b3c..e6391544 100644 --- a/config/feature_flags.yaml +++ b/config/feature_flags.yaml @@ -79,6 +79,12 @@ mobile: build_modes: [debug, profile, release] description: Mobile runtime logs view ui_surface: settings_page + help: + enabled: true + release_tier: stable + build_modes: [debug, profile, release] + description: Mobile setup help and installation guide + ui_surface: settings_page account_access: enabled: true release_tier: stable @@ -186,6 +192,12 @@ desktop: build_modes: [debug, profile, release] description: Desktop runtime logs view ui_surface: settings_page + help: + enabled: true + release_tier: stable + build_modes: [debug, profile, release] + description: Desktop setup help and installation guide + ui_surface: settings_page account_access: enabled: true release_tier: stable diff --git a/docs/cases/00-review-env-and-matrix.md b/docs/cases/00-review-env-and-matrix.md new file mode 100644 index 00000000..5a9548bb --- /dev/null +++ b/docs/cases/00-review-env-and-matrix.md @@ -0,0 +1,65 @@ +# XWorkmate App Review Test Cases|环境与覆盖矩阵 + +> 适用目录:`xworkmate-app/docs/cases/` +> +> 目标:为 Apple 审核、公网 Bridge、自建 Bridge、本地 Bridge、典型 AI 工作流提供稳定、可复现、可回归的测试用例。 + +## 1. 测试账号与环境变量 + +### 1.1 云端账号 + +| 项目 | 值 | +|---|---| +| 账号类型 | 只读评审账号(Apple 审核专用) | +| 服务地址 | `https://accounts.svc.plus` | +| 邮箱 / 账号 | `review@svc.plus` | +| 密码 | 从安全变量读取:`XWORKMATE_REVIEW_PASSWORD` | + +> 不建议将真实密码提交到仓库。Apple 审核备注、CI Secret、本地 `.env.local` 可保存真实值。 + +### 1.2 公网 xworkmate-bridge|正式 Token 组合 + +```bash +BRIDGE_SERVER_URL=https://xworkmate-bridge.svc.plus +BRIDGE_AUTH_TOKEN=${BRIDGE_AUTH_TOKEN} +``` + +### 1.3 公网 xworkmate-bridge|Review Token 组合 + +```bash +BRIDGE_SERVER_URL=https://xworkmate-bridge.svc.plus +BRIDGE_REVIEW_AUTH_TOKEN=${BRIDGE_REVIEW_AUTH_TOKEN} +``` + +### 1.4 本地 xworkmate-bridge|开发组合 + +```bash +BRIDGE_SERVER_URL=http://127.0.0.1:8787 +BRIDGE_AUTH_TOKEN=${BRIDGE_AUTH_TOKEN} +``` + +## 2. 通用验收标准 + +- App 能完成账号登录、Bridge 连接、Token 校验、会话初始化。 +- 网络异常、Token 失效、Bridge 不可达时,App 不崩溃,并给出明确错误提示。 +- 长任务执行期间,状态流、日志流、任务结果、取消重试行为保持稳定。 +- 同一任务重复执行 3 次,产物结构和关键字段稳定,不出现空白页、卡死、重复提交、状态错乱。 +- App 从前台切后台再恢复,任务状态可以继续读取或明确提示已中断。 + +## 3. 五类典型任务覆盖矩阵 + +| 用例 | 核心链路 | 主要覆盖点 | 稳定性关注 | +|---|---|---|---| +| AI 资讯 Markdown | 搜索 / 摘要 / Markdown 生成 | 长文本、引用、结构化输出 | 流式输出中断、重复标题、空结果兜底 | +| 图片制作视频 | 图片输入 / 脚本 / 分镜 / 视频指令 | 多模态任务、素材引用、步骤编排 | 附件丢失、任务超时、进度状态错乱 | +| 7 张连续图片 | 连续图文 / 风格一致 / 批量生成 | 多步骤连续产物 | 序号错乱、风格漂移、单张失败重试 | +| 多平台软文矩阵 | 公众号 / 小红书 / 视频号 / 推文 | 同主题多平台改写 | 语气失控、长度失控、平台字段缺失 | +| 章节拆解生成图文 PDF | 长文拆解 / 图文页 / PDF 导出 | 长链路 Artifact | 内存占用、导出失败、页面丢失 | + +## 4. 回归优先级 + +P0:登录、Bridge 连接、Token 校验、会话创建、任务状态流。 + +P1:长任务恢复、取消、重试、错误提示、Artifact 列表刷新。 + +P2:多平台文本质量、图片风格一致性、PDF 排版一致性。 diff --git a/docs/cases/01-ai-news-markdown.md b/docs/cases/01-ai-news-markdown.md new file mode 100644 index 00000000..4960bd8f --- /dev/null +++ b/docs/cases/01-ai-news-markdown.md @@ -0,0 +1,43 @@ +# Case 01|AI 资讯 Markdown 生成 + +## 目标 + +验证 XWorkmate App 在连接公网或本地 xworkmate-bridge 后,能稳定执行“AI 资讯收集 → 摘要 → Markdown 文档生成”的典型文本任务。 + +## 前置条件 + +- 已登录只读评审账号。 +- Bridge 已连接成功。 +- 当前会话可创建任务并接收状态流。 + +## 输入提示词 + +```text +请整理最近 AI 领域值得技术人关注的 5 条资讯,输出 Markdown。 +要求: +1. 每条包含标题、发生时间、核心变化、对开发者的影响。 +2. 不要堆砌新闻,要提炼趋势。 +3. 最后给出 3 条行动建议。 +``` + +## 执行步骤 + +1. 打开 XWorkmate App。 +2. 使用 Review 账号登录。 +3. 选择公网 Bridge 或本地 Bridge。 +4. 新建任务并输入提示词。 +5. 等待任务完成,查看 Markdown 结果。 +6. 复制或导出结果,确认格式完整。 + +## 预期结果 + +- 任务能进入 Running 状态,并持续返回日志或进度。 +- Markdown 结构完整,包含标题、列表、结论。 +- 不出现空白结果、重复输出、乱码、半截 Markdown。 +- 网络短暂波动后,App 能恢复状态或明确提示失败原因。 + +## 稳定性检查 + +- 连续执行 3 次,任务状态不串线。 +- 切后台 30 秒后返回,结果仍可查看。 +- Bridge Token 错误时,能显示授权失败,而不是无限 Loading。 diff --git a/docs/cases/02-image-to-video.md b/docs/cases/02-image-to-video.md new file mode 100644 index 00000000..48256ab4 --- /dev/null +++ b/docs/cases/02-image-to-video.md @@ -0,0 +1,44 @@ +# Case 02|图片制作视频任务 + +## 目标 + +验证“图片素材 → 视频脚本 → 分镜规划 → 生成指令”的多模态长链路任务是否稳定。 + +## 前置条件 + +- App 已登录。 +- Bridge 连接成功。 +- 至少准备 1 张测试图片。 + +## 输入提示词 + +```text +基于这张图片,帮我生成一个 30 秒短视频方案。 +要求: +1. 输出视频标题。 +2. 拆成 5 个分镜。 +3. 每个分镜包含画面描述、旁白、字幕、转场建议。 +4. 风格适合技术公众号和视频号。 +5. 最后输出可交给视频生成工具的结构化指令。 +``` + +## 执行步骤 + +1. 上传或选择一张测试图片。 +2. 新建图片制作视频任务。 +3. 输入提示词并开始执行。 +4. 观察上传、任务创建、执行、结果展示全过程。 +5. 复制分镜结果,确认结构完整。 + +## 预期结果 + +- 图片能被正确绑定到任务。 +- 任务执行过程中不丢失附件引用。 +- 输出包含标题、5 个分镜、旁白、字幕、转场建议。 +- 若图片上传失败,应明确提示,不应继续生成无图任务。 + +## 稳定性检查 + +- 大图、普通图各执行 1 次。 +- 弱网环境下,上传失败可重试。 +- 用户取消任务后,不应继续追加输出。 diff --git a/docs/cases/03-seven-continuous-images.md b/docs/cases/03-seven-continuous-images.md new file mode 100644 index 00000000..3701e2e9 --- /dev/null +++ b/docs/cases/03-seven-continuous-images.md @@ -0,0 +1,37 @@ +# Case 03|7 张连续图片生成 + +## 目标 + +验证连续 7 张图文图片任务在风格一致性、顺序稳定性、失败重试方面的表现。 + +## 输入提示词 + +```text +围绕“AI-Native 技术人成长路线图”,规划 7 张连续图片。 +要求: +1. 每张图有页码、标题、核心观点、3 个关键词。 +2. 蓝白科技风,适合公众号竖版图。 +3. 7 张图之间逻辑连续,不要重复。 +4. 输出每张图的文案与设计提示词。 +``` + +## 执行步骤 + +1. 新建连续图片任务。 +2. 输入提示词。 +3. 等待系统生成 7 页图文规划或图片提示词。 +4. 检查每一页是否完整。 +5. 记录任务耗时、失败页、重试结果。 + +## 预期结果 + +- 输出严格包含 7 张,不多不少。 +- 每张图都有页码、标题、核心观点、关键词。 +- 主题连续,表达不重复。 +- 单页生成失败时,可单页重试,不影响已完成页面。 + +## 稳定性检查 + +- 执行期间切换页面,任务状态不丢失。 +- 第 4 张之后仍保持同一风格约束。 +- 重试后页码顺序不乱。 diff --git a/docs/cases/04-multi-platform-copy-matrix.md b/docs/cases/04-multi-platform-copy-matrix.md new file mode 100644 index 00000000..0e20c511 --- /dev/null +++ b/docs/cases/04-multi-platform-copy-matrix.md @@ -0,0 +1,39 @@ +# Case 04|多平台软文矩阵生成 + +## 目标 + +验证同一主题在公众号、小红书、视频号、朋友圈、推文等多平台之间的改写稳定性。 + +## 输入提示词 + +```text +主题:企业为什么最终会走向平台工程。 +请生成多平台软文矩阵: +1. 公众号 500 字观点文。 +2. 小红书 300 字种草式表达。 +3. 视频号 60 秒口播稿。 +4. 朋友圈 100 字转发文案。 +5. 推文 280 字以内。 +要求:同一观点,不同平台语气;不要标题党;适合技术人阅读。 +``` + +## 执行步骤 + +1. 新建软文矩阵任务。 +2. 输入主题和平台要求。 +3. 等待任务完成。 +4. 分平台检查文案结构、长度和语气。 +5. 导出或复制结果。 + +## 预期结果 + +- 输出包含 5 个平台版本。 +- 每个平台有清晰标题或字段标识。 +- 公众号版本偏观点深度,小红书更轻,视频号适合口播。 +- 不出现平台漏项、字段混乱、同文复制。 + +## 稳定性检查 + +- 长度限制能被大体遵守。 +- 多次生成不丢平台字段。 +- 结果页刷新后仍能看到完整产物。 diff --git a/docs/cases/05-chapter-to-graphic-pdf.md b/docs/cases/05-chapter-to-graphic-pdf.md new file mode 100644 index 00000000..36fffaf3 --- /dev/null +++ b/docs/cases/05-chapter-to-graphic-pdf.md @@ -0,0 +1,41 @@ +# Case 05|章节拆解生成图文 PDF + +## 目标 + +验证“长章节输入 → 图文拆解 → 页面规划 → PDF 产物”的长链路 Artifact 任务稳定性。 + +## 输入提示词 + +```text +请把下面这章内容拆解成一份 7 页图文 PDF。 +要求: +1. 第 1 页是封面观点。 +2. 第 2-6 页每页一个核心小节。 +3. 第 7 页是总结和行动建议。 +4. 每页包含标题、主文案、视觉建议、关键词。 +5. 最后输出 PDF 结构目录。 + +章节内容: +企业上云不是一次资源搬迁,而是业务边界、成本结构、治理能力和交付模式的重建…… +``` + +## 执行步骤 + +1. 新建章节拆解任务。 +2. 粘贴长文本章节。 +3. 执行图文 PDF 规划任务。 +4. 检查页面数量、章节顺序、封面和总结页。 +5. 若支持导出 PDF,执行导出并打开检查。 + +## 预期结果 + +- 页面规划完整,严格 7 页。 +- 每页包含标题、主文案、视觉建议、关键词。 +- PDF 导出成功时,页面不丢失、不空白、不乱序。 +- PDF 导出失败时,错误信息清晰,并保留中间图文结构。 + +## 稳定性检查 + +- 长文本输入不会导致 App 卡死。 +- 导出期间切后台,回到前台后状态可见。 +- Artifact 列表刷新后,PDF 或中间结果仍可访问。 diff --git a/docs/cases/README.md b/docs/cases/README.md index dcd32484..90714b75 100644 --- a/docs/cases/README.md +++ b/docs/cases/README.md @@ -5,6 +5,7 @@ ## 当前入口 - [核心功能集成测试手动 Case](./core-integration-manual-cases.md) +- [云端账号与 XWorkmate Bridge 连接手动 Case](./cloud-account-and-bridge-manual-cases.md) - [云原生 Service Mesh 网络科普视频调研场景测试用例](./service-mesh-evolution-video-scenario/README.md) - [OpenClaw Gateway 5 并发 E2E 回归场景](./openclaw-gateway-e2e-regression/README.md) diff --git a/docs/cases/cloud-account-and-bridge-manual-cases.md b/docs/cases/cloud-account-and-bridge-manual-cases.md new file mode 100644 index 00000000..905fcad0 --- /dev/null +++ b/docs/cases/cloud-account-and-bridge-manual-cases.md @@ -0,0 +1,489 @@ +# 云端账号与 XWorkmate Bridge 连接手动 Case + +本文档整理 Apple 审核专用只读账号、svc.plus 云端同步、以及公网 / 本地 `xworkmate-bridge` 接入的手动验证用例。 + +## 1. 测试账号与连接参数 + +### 1.1 云端账号 + +| 项目 | 内容 | +|------|------| +| 账号类型 | 只读评审账号(Apple 审核专用) | +| 服务地址 | `https://accounts.svc.plus` | +| 邮箱 / 账号 | `review@svc.plus` | +| 密码 | `***REMOVED-CREDENTIAL***` | + +### 1.2 公网 xworkmate-bridge 组合 1 + +| 环境变量 | 值 | +|----------|----| +| `BRIDGE_SERVER_URL` | `https://xworkmate-bridge.svc.plus` | +| `BRIDGE_AUTH_TOKEN` | `***REMOVED-CREDENTIAL***` | + +### 1.3 公网 xworkmate-bridge 组合 2 + +| 环境变量 | 值 | +|----------|----| +| `BRIDGE_SERVER_URL` | `https://xworkmate-bridge.svc.plus` | +| `BRIDGE_REVIEW_AUTH_TOKEN` | `***REMOVED-CREDENTIAL***` | + +### 1.4 本地 xworkmate-bridge + +| 环境变量 | 值 | +|----------|----| +| `BRIDGE_SERVER_URL` | `http://127.0.0.1:8787` | +| `BRIDGE_AUTH_TOKEN` | `***REMOVED-CREDENTIAL***` | + +--- + +## 2. 通用证据记录要求 + +每个 case 执行后建议记录: + +- 当前 App 版本 / 构建号 +- 当前平台与网络环境 +- 当前入口:`Settings -> Integrations` +- 当前页签:`svc.plus 云端同步` 或 `AI 智能体工作空间` +- 服务地址 / Bridge 地址 +- token 类型:`BRIDGE_AUTH_TOKEN` 或 `BRIDGE_REVIEW_AUTH_TOKEN` +- 连接测试结果摘要 +- 是否出现 secret 明文 +- 截图点:保存前、保存后、重新进入设置页 + +--- + +## 3. 五类典型任务 + +以下五类任务用于验证 App 登录后能否通过当前连接方式稳定创建任务、执行技能、生成文件产物并回写到当前线程。 + +| 任务编号 | 类型 | 验收产物 | +|----------|------|----------| +| CASE-001 | 采集最新 AI 资讯 | `.md` 文件 | +| CASE-002 | 附件图片制作视频 | 视频文件,附件图片被使用 | +| CASE-003 | 安全身份演进连续图片 | 7 张连续风格图片 | +| CASE-004 | 多平台软文矩阵 | 多个 `.md` 文件 | +| CASE-005 | 章节拆解 + Codex + GPT images2 + PDF | 汇总排版后的 PDF | + +### `TASK-CASE-001` 采集最新 AI 资讯并保存 Markdown + +- 输入提示词 + +```text +采集最新AI资讯,保存在md文件 +``` + +- 期望结果 + - 任务能联网采集最新 AI 资讯 + - 结果保存为 Markdown 文件 + - 线程结果区展示文件产物 + - Markdown 内包含标题、来源、摘要和时间信息 +- 建议记录项 + - 任务线程 ID + - 输出 `.md` 文件路径 + - 资讯来源数量 + - 截图点:任务完成后的 artifact 区域 + +### `TASK-CASE-002` 附件图片制作视频 + +- 前置条件 + - 当前线程上传至少 1 张图片附件 +- 输入提示词 + +```text +制作视频,附件带有图片 +``` + +- 期望结果 + - 任务识别并使用用户上传的图片附件 + - 输出视频文件 + - 失败时错误信息明确说明缺少图片、视频生成失败或依赖不可用 + - 产物归属当前线程 workspace +- 建议记录项 + - 附件图片文件名 + - 输出视频路径 + - 视频时长和分辨率 + - 截图点:附件与视频产物 + +### `TASK-CASE-003` 安全身份演进连续 7 张图片 + +- 输入提示词 + +```text +从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进,连续制作 7 张一系列图片 +``` + +- 期望结果 + - 输出 7 张图片 + - 7 张图片主题分别覆盖:单机权限、网络边界、Web 安全、云身份、Zero Trust、AI Agent 身份、AI 模型与知识保护 + - 图片风格、尺寸、命名方式保持连续一致 + - 线程结果区能看到完整图片系列 +- 建议记录项 + - 7 张图片路径 + - 图片尺寸 + - 是否存在缺图或主题错位 + - 截图点:图片系列列表 + +### `TASK-CASE-004` 安全身份演进多平台软文矩阵 + +- 输入提示词 + +```text +围绕 从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 + +1. 输出Markdown格式文件, 微信公众号短图文 400-600字 插入关键词的软文 +2. 输出Markdown格式文件, 小红书风格 600-800字 插入钩子话题的软文 +3. 输出Markdown格式文件, X文案串 小于144字的英语 鲜明的观点 +4. 输出Markdown格式文件, 微信公众号文章 800-1200字左右 +5. 输出Markdown格式文件, 头条号长文 800-1200字左右 +``` + +- 期望结果 + - 输出 Markdown 格式文件 + - 至少包含微信公众号短图文、小红书风格、X 文案串、微信公众号文章、头条号长文五类内容 + - 字数、语言和平台风格符合输入要求 + - X 文案串为英语且单条小于 144 字符 + - 内容围绕同一条安全身份演进主线,不跑题 +- 建议记录项 + - 输出文件路径 + - 每个平台内容字数 + - X 文案字符数 + - 截图点:Markdown 产物列表 + +### `TASK-CASE-005` 安全身份演进章节拆解生成图文 PDF + +- 输入提示词 + +```text +从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 +拆章节 -> 每章调用 Codex -> 每章 GPT images2 生成图 -> 汇总排版 -> 输出 PDF +``` + +- 期望结果 + - 任务先拆分章节,再逐章生成内容 + - 每章调用 Codex 生成或整理章节文本 + - 每章调用 GPT images2 生成配图 + - 最终汇总排版为 PDF + - PDF 中章节顺序与演进主线一致,图片与章节内容匹配 +- 建议记录项 + - 章节清单 + - 每章图片路径 + - 输出 PDF 路径 + - PDF 页数 + - 截图点:PDF artifact 与预览页 + +--- + +## 4. 连接云端账号 + +### `MANUAL-CLOUD-001` 只读评审账号登录 + +- 前置条件 + - App 可访问 `https://accounts.svc.plus` + - 当前未登录,或已退出其他账号 +- 操作步骤 + 1. 打开 `Settings -> Integrations` + 2. 切换到 `svc.plus 云端同步` + 3. 在 `服务地址` 输入 `https://accounts.svc.plus` + 4. 在 `邮箱或账号` 输入 `review@svc.plus` + 5. 在 `密码` 输入 `***REMOVED-CREDENTIAL***` + 6. 点击 `登录` + 7. 等待账号同步完成 +- 期望结果 + - 登录成功,账号状态稳定显示为已登录或同步完成 + - App 不展示密码明文 + - 若账号侧托管 bridge 配置可用,设置页可同步到对应连接配置 + - 只读评审账号不能触发破坏性写入或管理动作 +- 建议记录项 + - 登录账号 + - 服务地址 + - 登录结果摘要 + - 同步后的 bridge endpoint + - 截图点:登录成功后的 `svc.plus 云端同步` 页 + +### `MANUAL-CLOUD-002` 退出后重新登录保持稳定 + +- 前置条件 + - 已完成 `MANUAL-CLOUD-001` +- 操作步骤 + 1. 在设置页退出当前账号 + 2. 关闭或返回设置页 + 3. 再次进入 `Settings -> Integrations -> svc.plus 云端同步` + 4. 使用 `review@svc.plus` / `***REMOVED-CREDENTIAL***` 重新登录 + 5. 观察同步状态与本地配置状态 +- 期望结果 + - 退出后不会继续显示已登录状态 + - 重新登录成功 + - 重新登录后同步状态可恢复 + - 本地已保存的手动 bridge override 不应被异常覆盖 +- 建议记录项 + - 退出前后账号状态 + - 重新登录结果 + - 同步前后 endpoint 对比 + +### `MANUAL-CLOUD-TASKS-001` 云端账号登录后执行五类典型任务 + +- 前置条件 + - 已完成 `MANUAL-CLOUD-001` + - 云端同步状态稳定 + - 当前线程可创建新任务 +- 操作步骤 + 1. 使用 `review@svc.plus` 登录 `svc.plus 云端同步` + 2. 确认账号同步完成 + 3. 新建或进入一个测试线程 + 4. 依次执行 `TASK-CASE-001` 到 `TASK-CASE-005` + 5. 每个任务完成后记录产物路径和结果摘要 +- 期望结果 + - 五类任务均能在云端账号连接上下文下启动 + - 任务产物均归属当前 App 线程 + - 云端账号只读属性不影响正常评审用任务执行 + - 执行过程中不暴露密码、session 或 bridge token 明文 +- 建议记录项 + - 登录账号 + - 五类任务的线程 ID + - 五类任务的产物路径 + - 失败任务的错误摘要 + +--- + +## 5. 连接公网 xworkmate-bridge + +### `MANUAL-BRIDGE-REMOTE-001` 公网 bridge 使用 `BRIDGE_AUTH_TOKEN` + +- 前置条件 + - 当前网络可访问 `https://xworkmate-bridge.svc.plus` + - 准备公网组合 1 的 `BRIDGE_AUTH_TOKEN` +- 操作步骤 + 1. 打开 `Settings -> Integrations` + 2. 切换到 `AI 智能体工作空间` + 3. 在 `Bridge 地址` 输入 `https://xworkmate-bridge.svc.plus` + 4. 在 `鉴权令牌 (TOKEN)` 输入 `***REMOVED-CREDENTIAL***` + 5. 点击 `保存配置` + 6. 重新进入设置页确认配置仍然存在 + 7. 发起一次需要 AI 智能体工作空间的任务,确认可建立连接 +- 期望结果 + - 配置保存成功 + - 重新进入设置页时 endpoint 保持为公网 bridge 地址 + - token 不以明文展示 + - 任务请求走公网 bridge,而不是本地 `127.0.0.1` +- 建议记录项 + - Bridge 地址 + - token 类型:`BRIDGE_AUTH_TOKEN` + - 保存结果 + - 任务执行结果摘要 + - 截图点:`AI 智能体工作空间` 保存后的页面 + +### `MANUAL-BRIDGE-REMOTE-002` 公网 bridge 使用 `BRIDGE_REVIEW_AUTH_TOKEN` + +- 前置条件 + - 当前网络可访问 `https://xworkmate-bridge.svc.plus` + - 准备公网组合 2 的 `BRIDGE_REVIEW_AUTH_TOKEN` +- 操作步骤 + 1. 打开 `Settings -> Integrations -> AI 智能体工作空间` + 2. 在 `Bridge 地址` 输入 `https://xworkmate-bridge.svc.plus` + 3. 在 `鉴权令牌 (TOKEN)` 输入 `***REMOVED-CREDENTIAL***` + 4. 点击 `保存配置` + 5. 重新进入设置页确认配置稳定 + 6. 发起一次 AI 智能体工作空间任务 +- 期望结果 + - 使用 review token 也能保存并建立连接 + - token 不以明文展示 + - 任务侧不会把 review token 写入日志明文 + - 失败时错误信息能区分网络不可达、鉴权失败和服务异常 +- 建议记录项 + - Bridge 地址 + - token 类型:`BRIDGE_REVIEW_AUTH_TOKEN` + - 保存结果 + - 连接或任务结果摘要 + - 是否在页面或日志看到 secret 明文 + +### `MANUAL-BRIDGE-REMOTE-TASKS-001` 公网 bridge 组合 1 执行五类典型任务 + +- 前置条件 + - 已完成 `MANUAL-BRIDGE-REMOTE-001` + - 当前配置使用 `BRIDGE_AUTH_TOKEN` +- 操作步骤 + 1. 确认 `Bridge 地址` 为 `https://xworkmate-bridge.svc.plus` + 2. 确认 token 类型为 `BRIDGE_AUTH_TOKEN` + 3. 新建或进入一个测试线程 + 4. 依次执行 `TASK-CASE-001` 到 `TASK-CASE-005` + 5. 每个任务完成后重新进入设置页,确认公网 bridge 配置未丢失 +- 期望结果 + - 五类任务均通过公网 bridge 组合 1 执行 + - 生成 Markdown、视频、图片系列和 PDF 产物 + - 任务不会回退到本地 `127.0.0.1` + - bridge token 不出现在页面、任务摘要或普通日志明文中 +- 建议记录项 + - token 类型 + - 每类任务产物路径 + - bridge 连接结果 + - 是否出现 endpoint 回退 + +### `MANUAL-BRIDGE-REMOTE-TASKS-002` 公网 bridge 组合 2 执行五类典型任务 + +- 前置条件 + - 已完成 `MANUAL-BRIDGE-REMOTE-002` + - 当前配置使用 `BRIDGE_REVIEW_AUTH_TOKEN` +- 操作步骤 + 1. 确认 `Bridge 地址` 为 `https://xworkmate-bridge.svc.plus` + 2. 确认 token 类型为 `BRIDGE_REVIEW_AUTH_TOKEN` + 3. 新建或进入一个测试线程 + 4. 依次执行 `TASK-CASE-001` 到 `TASK-CASE-005` + 5. 对比五类任务的启动、执行、产物回写是否与组合 1 一致 +- 期望结果 + - review token 能支持五类典型评审任务 + - Markdown、视频、图片系列和 PDF 均能作为产物回写 + - 若某类任务受权限限制失败,错误信息应明确说明鉴权或能力限制 + - 不泄漏 `BRIDGE_REVIEW_AUTH_TOKEN` 明文 +- 建议记录项 + - token 类型 + - 五类任务结果摘要 + - 失败任务错误码或错误文案 + - 截图点:最终产物列表 + +--- + +## 6. 连接本地 xworkmate-bridge + +### `MANUAL-BRIDGE-LOCAL-001` 本地 bridge 使用 `BRIDGE_AUTH_TOKEN` + +- 前置条件 + - 本机已启动 `xworkmate-bridge` + - `http://127.0.0.1:8787` 可访问 + - 准备本地组合的 `BRIDGE_AUTH_TOKEN` +- 操作步骤 + 1. 打开 `Settings -> Integrations -> AI 智能体工作空间` + 2. 在 `Bridge 地址` 输入 `http://127.0.0.1:8787` + 3. 在 `鉴权令牌 (TOKEN)` 输入 `***REMOVED-CREDENTIAL***` + 4. 点击 `保存配置` + 5. 发起一次 AI 智能体工作空间任务 + 6. 对照本地 bridge 日志确认请求到达 +- 期望结果 + - 配置保存成功 + - 任务请求命中本地 bridge + - 页面不会把公网 bridge 与本地 bridge endpoint 混用 + - 重新进入设置页后仍显示本地 bridge 地址 +- 建议记录项 + - 本地 bridge 监听地址 + - App 保存结果 + - 本地 bridge 日志摘要 + - 任务结果摘要 + +### `MANUAL-BRIDGE-LOCAL-002` 本地 bridge 未启动时的错误提示 + +- 前置条件 + - 本地 `xworkmate-bridge` 未启动 + - 设置页使用 `http://127.0.0.1:8787` +- 操作步骤 + 1. 打开 `Settings -> Integrations -> AI 智能体工作空间` + 2. 保存本地 bridge 地址与 token + 3. 发起一次 AI 智能体工作空间任务 + 4. 观察页面错误提示 +- 期望结果 + - 保存配置可完成,或给出明确的连接失败提示 + - 任务失败信息明确指向本地 bridge 不可达 + - 不会自动回退到公网 bridge + - 不会清空用户刚输入的本地配置 +- 建议记录项 + - 错误提示文案 + - 任务失败摘要 + - 设置页配置是否保留 + +### `MANUAL-BRIDGE-LOCAL-TASKS-001` 本地 bridge 执行五类典型任务 + +- 前置条件 + - 已完成 `MANUAL-BRIDGE-LOCAL-001` + - 本地 `xworkmate-bridge` 保持运行 +- 操作步骤 + 1. 确认 `Bridge 地址` 为 `http://127.0.0.1:8787` + 2. 新建或进入一个测试线程 + 3. 依次执行 `TASK-CASE-001` 到 `TASK-CASE-005` + 4. 每个任务完成后对照本地 bridge 日志 + 5. 重新进入设置页确认本地 bridge 地址仍然保留 +- 期望结果 + - 五类任务请求均命中本地 bridge + - 任务产物正常回写到当前线程 + - 断网或公网不可用时,本地 bridge 任务仍按本地能力给出明确结果 + - 不会自动切换到公网 bridge +- 建议记录项 + - 本地 bridge 日志摘要 + - 五类任务产物路径 + - 是否发生公网 endpoint 混用 + - 截图点:设置页与任务产物 + +--- + +## 7. 云端同步与手动配置共存 + +### `MANUAL-BRIDGE-MIXED-001` 云端账号登录后切换公网 bridge 手动配置 + +- 前置条件 + - 已使用 `review@svc.plus` 登录 + - 云端同步状态稳定 +- 操作步骤 + 1. 登录 `svc.plus 云端同步` + 2. 切换到 `AI 智能体工作空间` + 3. 手动输入公网 bridge 组合 1 + 4. 点击 `保存配置` + 5. 返回主页面后重新进入设置页 +- 期望结果 + - 手动 bridge 配置保存成功 + - 云端账号登录状态不丢失 + - 本地手动配置不会被同一轮页面刷新异常覆盖 + - 页面能区分云端同步状态与 AI 智能体工作空间连接状态 +- 建议记录项 + - 登录账号 + - 同步状态 + - 手动配置 endpoint + - 重新进入设置页后的 endpoint + +### `MANUAL-BRIDGE-MIXED-002` 公网 bridge 与本地 bridge 来回切换 + +- 前置条件 + - 公网 bridge 可访问 + - 本地 bridge 可按需启动 +- 操作步骤 + 1. 保存公网 bridge 组合 1 + 2. 发起一次任务并记录结果 + 3. 切换为本地 bridge 组合 + 4. 发起一次任务并记录结果 + 5. 再次切换回公网 bridge 组合 2 + 6. 重新进入设置页确认最终配置 +- 期望结果 + - 每次切换后 endpoint 与 token 类型都按用户最后一次保存生效 + - 任务请求不会继续使用旧 endpoint + - 页面不会出现公网 / 本地配置交叉污染 + - 最终配置以最后一次保存为准 +- 建议记录项 + - 三次保存的 endpoint + - 三次任务结果摘要 + - 最终设置页截图 + +--- + +## 8. 回归覆盖矩阵 + +| 测试编号 | 云端账号 | 公网 Bridge | 本地 Bridge | Secret 隐藏 | 配置持久化 | +|----------|:--------:|:-----------:|:-----------:|:-----------:|:----------:| +| MANUAL-CLOUD-001 | ✅ | ✅ | - | ✅ | ✅ | +| MANUAL-CLOUD-002 | ✅ | ✅ | - | ✅ | ✅ | +| MANUAL-CLOUD-TASKS-001 | ✅ | ✅ | - | ✅ | ✅ | +| MANUAL-BRIDGE-REMOTE-001 | - | ✅ | - | ✅ | ✅ | +| MANUAL-BRIDGE-REMOTE-002 | - | ✅ | - | ✅ | ✅ | +| MANUAL-BRIDGE-REMOTE-TASKS-001 | - | ✅ | - | ✅ | ✅ | +| MANUAL-BRIDGE-REMOTE-TASKS-002 | - | ✅ | - | ✅ | ✅ | +| MANUAL-BRIDGE-LOCAL-001 | - | - | ✅ | ✅ | ✅ | +| MANUAL-BRIDGE-LOCAL-002 | - | - | ✅ | ✅ | ✅ | +| MANUAL-BRIDGE-LOCAL-TASKS-001 | - | - | ✅ | ✅ | ✅ | +| MANUAL-BRIDGE-MIXED-001 | ✅ | ✅ | - | ✅ | ✅ | +| MANUAL-BRIDGE-MIXED-002 | - | ✅ | ✅ | ✅ | ✅ | + +## 9. 典型任务覆盖矩阵 + +| 连接方式 | CASE-001 AI 资讯 MD | CASE-002 图片视频 | CASE-003 7 图系列 | CASE-004 软文矩阵 | CASE-005 图文 PDF | +|----------|:-------------------:|:-----------------:|:-----------------:|:-----------------:|:-----------------:| +| 云端账号 | ✅ | ✅ | ✅ | ✅ | ✅ | +| 公网 bridge 组合 1 | ✅ | ✅ | ✅ | ✅ | ✅ | +| 公网 bridge 组合 2 | ✅ | ✅ | ✅ | ✅ | ✅ | +| 本地 bridge | ✅ | ✅ | ✅ | ✅ | ✅ | + +> 注意:以上 token 为评审 / 测试用途。执行测试时不得将 token 明文贴入公开 issue、公开日志或截图备注。 diff --git a/docs/cases/codex-goal-stability.md b/docs/cases/codex-goal-stability.md new file mode 100644 index 00000000..4ed722ba --- /dev/null +++ b/docs/cases/codex-goal-stability.md @@ -0,0 +1,41 @@ +# Codex /goal|XWorkmate App 稳定性修复 + +```text +/goal +你在 /Users/shenlan/workspaces/ai-workspace-lab/xworkmate-app 仓库中持续执行稳定性修复。 + +请参考: +- /Users/shenlan/workspaces/ai-workspace-lab/xworkspace-console/docs/case/ +- /Users/shenlan/workspaces/ai-workspace-lab/xworkmate-app/docs/cases/ + +目标不是重构 UI,而是围绕 Review 账号、公网 xworkmate-bridge、本地 xworkmate-bridge 和 5 类典型任务用例,修复 App 在登录、Bridge 连接、Token 校验、任务创建、状态流、长任务恢复、取消重试、Artifact 展示中的稳定性问题。 + +环境覆盖: +1. 云端只读评审账号:accounts.svc.plus / review@svc.plus,密码从 XWORKMATE_REVIEW_PASSWORD 读取。 +2. 公网 Bridge:BRIDGE_SERVER_URL=https://xworkmate-bridge.svc.plus,支持 BRIDGE_AUTH_TOKEN。 +3. 公网 Review Bridge:BRIDGE_SERVER_URL=https://xworkmate-bridge.svc.plus,支持 BRIDGE_REVIEW_AUTH_TOKEN。 +4. 本地 Bridge:BRIDGE_SERVER_URL=http://127.0.0.1:8787,支持 BRIDGE_AUTH_TOKEN。 + +典型任务用例: +1. AI 资讯 Markdown。 +2. 图片制作视频。 +3. 7 张连续图片。 +4. 多平台软文矩阵。 +5. 章节拆解生成图文 PDF。 + +执行要求: +1. 先阅读 docs/cases 下的用例和覆盖矩阵,梳理当前 App 中对应链路。 +2. 不改变现有主视觉和核心交互,优先修复崩溃、卡死、无限 Loading、状态丢失、错误提示不清晰、Token 分支混乱、长任务恢复失败等问题。 +3. 优先补齐连接层、任务状态层、错误处理层、日志可观测性和最小回归测试。 +4. 对每一处修复,说明触发场景、根因、修改文件、验证方式。 +5. 每轮只做小步提交,避免大面积重构。 +6. 禁止把真实密码或 Token 写入仓库;统一通过环境变量、Secret 或本地 .env.local 读取。 +7. 如果发现用例与当前实现不一致,优先补充兼容层或清晰错误提示,不要直接删除功能入口。 + +验收标准: +- 三种 Bridge 连接模式均能给出明确成功或失败状态。 +- Review 账号登录失败、Token 错误、Bridge 不可达时 App 不崩溃。 +- 五类典型任务至少能完成任务创建、状态展示、失败提示和结果/Artifact 展示的主链路验证。 +- 长任务切后台再恢复后,状态不会错乱。 +- 连续执行 3 次同类任务,不出现任务串线、重复提交、空白结果页。 +``` diff --git a/lib/app/ui_feature_manifest_core.dart b/lib/app/ui_feature_manifest_core.dart index 80807213..86444088 100644 --- a/lib/app/ui_feature_manifest_core.dart +++ b/lib/app/ui_feature_manifest_core.dart @@ -49,6 +49,7 @@ abstract final class UiFeatureKeys { static const settingsArchivedTasks = 'settings.archived_tasks'; static const settingsRemoteDesktop = 'settings.remote_desktop'; static const settingsLogs = 'settings.logs'; + static const settingsHelp = 'settings.help'; static const settingsAccountAccess = 'settings.account_access'; static const settingsVaultServer = 'settings.vault_server'; static const settingsExperimentalCanvas = 'settings.experimental_canvas'; @@ -370,6 +371,7 @@ class UiFeatureAccess { UiFeatureKeys.settingsArchivedTasks: SettingsTab.archivedTasks, UiFeatureKeys.settingsRemoteDesktop: SettingsTab.remoteDesktop, UiFeatureKeys.settingsLogs: SettingsTab.logs, + UiFeatureKeys.settingsHelp: SettingsTab.help, }; bool isEnabledPath(String path) { diff --git a/lib/features/desktop/desktop_view.dart b/lib/features/desktop/desktop_view.dart index a7c8c2e2..1fac3981 100644 --- a/lib/features/desktop/desktop_view.dart +++ b/lib/features/desktop/desktop_view.dart @@ -6,11 +6,15 @@ import 'desktop_client.dart'; import 'desktop_input_handler.dart'; import '../../app/app_controller.dart'; import '../../runtime/gateway_acp_client.dart'; +import '../../runtime/runtime_models.dart'; import '../../widgets/surface_card.dart'; import '../../i18n/app_language.dart'; import '../workspace_management/workspace_management_panel.dart'; import '../workspace_management/workspace_management_i18n.dart'; +bool desktopRemoteWorkspaceConnectionEnabled(RuntimeConnectionMode mode) => + mode == RuntimeConnectionMode.remote; + class DesktopView extends StatefulWidget { const DesktopView({ super.key, @@ -239,8 +243,8 @@ class _DesktopViewState extends State { SnackBar( content: Text( appText( - '连接AI工作空间失败: $message', - 'Failed to connect AI Workspace: $message', + '连接远程 AI 工作空间失败: $message', + 'Failed to connect Remote AI Workspace: $message', ), ), backgroundColor: Colors.redAccent, @@ -252,7 +256,10 @@ class _DesktopViewState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - appText('连接AI工作空间失败: $e', 'Failed to connect AI Workspace: $e'), + appText( + '连接远程 AI 工作空间失败: $e', + 'Failed to connect Remote AI Workspace: $e', + ), ), backgroundColor: Colors.redAccent, ), @@ -284,6 +291,9 @@ class _DesktopViewState extends State { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final hasVideoFrame = _hasVideoFrame; + final canConnectRemoteWorkspace = desktopRemoteWorkspaceConnectionEnabled( + widget.controller.connection.mode, + ); return Padding( padding: const EdgeInsets.all(16.0), @@ -305,7 +315,9 @@ class _DesktopViewState extends State { children: [ // Connection Button ElevatedButton.icon( - onPressed: _toggleConnection, + onPressed: canConnectRemoteWorkspace + ? _toggleConnection + : null, style: ElevatedButton.styleFrom( backgroundColor: _connectionState == 'connected' ? Colors.redAccent @@ -332,8 +344,8 @@ class _DesktopViewState extends State { : (_connectionState == 'connecting' ? appText('正在连接...', 'Connecting...') : appText( - '连接AI工作空间', - 'Connect AI Workspace', + '连接远程 AI 工作空间', + 'Connect Remote AI Workspace', )), style: const TextStyle(fontWeight: FontWeight.bold), ), @@ -563,8 +575,8 @@ class _DesktopViewState extends State { 'Establishing WebRTC connection, please wait...', ) : appText( - '未开启 AI 工作空间流。点击“连接AI工作空间”启动视频流。', - 'AI Workspace stream not enabled. Click "Connect AI Workspace" to start the video stream.', + '未开启远程 AI 工作空间流。点击“连接远程 AI 工作空间”启动视频流。', + 'Remote AI Workspace stream not enabled. Click "Connect Remote AI Workspace" to start the video stream.', ), style: TextStyle( color: theme.colorScheme.onSurface diff --git a/lib/features/mobile/mobile_settings_page.dart b/lib/features/mobile/mobile_settings_page.dart index 7bb904c8..66afb7e8 100644 --- a/lib/features/mobile/mobile_settings_page.dart +++ b/lib/features/mobile/mobile_settings_page.dart @@ -10,6 +10,7 @@ import '../../runtime/runtime_controllers.dart'; import '../../runtime/runtime_models.dart'; import '../../theme/app_palette.dart'; import 'mobile_settings_page_widgets.dart'; +import '../settings/settings_help_panel.dart'; class MobileSettingsPage extends StatefulWidget { const MobileSettingsPage({super.key, required this.controller}); @@ -185,12 +186,14 @@ class _MobileSettingsPageState extends State { await controller.refreshSingleAgentCapabilitiesInternal( forceRefresh: true, ); - } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); + } catch (e, stackTrace) { + debugPrint('Error: $e\n$stackTrace'); // Account login should not fail only because runtime refresh is transient. } try { await controller.refreshAcpCapabilitiesInternal(forceRefresh: true); - } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); + } catch (e, stackTrace) { + debugPrint('Error: $e\n$stackTrace'); // Runtime capabilities can be refreshed again from Assistant. } } @@ -274,6 +277,8 @@ class _MobileSettingsPageState extends State { ], if (currentTab == SettingsTab.archivedTasks) _ArchivedTasksSection(controller: controller) + else if (currentTab == SettingsTab.help) + const SettingsHelpPanel() else _AccountSection( settings: settings, diff --git a/lib/features/mobile/mobile_settings_page_widgets.dart b/lib/features/mobile/mobile_settings_page_widgets.dart index 3ba78e15..fa518199 100644 --- a/lib/features/mobile/mobile_settings_page_widgets.dart +++ b/lib/features/mobile/mobile_settings_page_widgets.dart @@ -26,7 +26,9 @@ class MobileSettingsTabSelectorInternal extends StatelessWidget { icon: Icon( tab == SettingsTab.archivedTasks ? Icons.inventory_2_outlined - : Icons.hub_outlined, + : (tab == SettingsTab.help + ? Icons.help_outline_rounded + : Icons.hub_outlined), ), label: Text(tab.label), ), diff --git a/lib/features/settings/settings_help_panel.dart b/lib/features/settings/settings_help_panel.dart new file mode 100644 index 00000000..52eb6125 --- /dev/null +++ b/lib/features/settings/settings_help_panel.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../i18n/app_language.dart'; +import '../../theme/app_palette.dart'; + +class SettingsHelpPanel extends StatelessWidget { + const SettingsHelpPanel({super.key}); + + static const _sections = <_HelpSection>[ + _HelpSection( + title: '1. 快速安装(一键部署)', + codeLabel: 'bash', + code: ''' +curl -sfL https://install.svc.plus/ai-workspace | bash - +''', + ), + _HelpSection( + title: '2. 带 API Key 安装', + codeLabel: 'bash', + code: ''' +export DEEPSEEK_API_KEY="" +export NVIDIA_API_KEY="" +export OLLAMA_API_KEY="" +curl -sfL https://install.svc.plus/ai-workspace | bash - +''', + ), + _HelpSection( + title: '3. 卸载(保留数据)', + codeLabel: 'bash', + code: ''' +curl -sfL https://install.svc.plus/ai-workspace | bash -s -- uninstall +''', + ), + _HelpSection( + title: '4. 彻底卸载(清除所有数据)', + codeLabel: 'bash', + code: ''' +curl -sfL https://install.svc.plus/ai-workspace | bash -s -- uninstall --purge +''', + ), + ]; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final palette = context.palette; + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appText('帮助', 'Help'), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 10), + Text( + appText( + '测试提示词模板,本文档提供了用于测试 setup-ai-workspace-all-in-one.sh 的标准化提示词模板,可直接复制粘贴到终端执行。', + 'Test prompt templates for setup-ai-workspace-all-in-one.sh. Copy and paste directly into your terminal.', + ), + style: theme.textTheme.bodyMedium?.copyWith( + color: palette.textSecondary, + height: 1.45, + ), + ), + const SizedBox(height: 18), + for (final section in _sections) ...[ + _HelpCodeSectionCard(section: section), + const SizedBox(height: 18), + ], + _InfoBlock( + title: appText('环境变量参考', 'Environment variables'), + child: Table( + columnWidths: const { + 0: FlexColumnWidth(1.1), + 1: FlexColumnWidth(1.4), + 2: FlexColumnWidth(0.7), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + _tableRow('DEEPSEEK_API_KEY', 'DeepSeek 模型 API 密钥', '可选'), + _tableRow('NVIDIA_API_KEY', 'NVIDIA NIM API 密钥', '可选'), + _tableRow('OLLAMA_API_KEY', 'Ollama 服务 API 密钥', '可选'), + _tableRow('PLAYBOOK_DIR', '本地 Playbook 目录路径(开发调试用)', '可选'), + ], + ), + ), + const SizedBox(height: 18), + _InfoBlock( + title: appText('支持平台', 'Supported platforms'), + child: Table( + columnWidths: const { + 0: FlexColumnWidth(1.1), + 1: FlexColumnWidth(0.7), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + _tableRow('macOS (Apple Silicon / Intel)', '已测试'), + _tableRow('Debian 11/12', '已测试'), + _tableRow('Ubuntu 22.04/24.04', '已测试'), + _tableRow('其他 Linux 发行版', '未测试'), + ], + ), + ), + ], + ), + ); + } + + TableRow _tableRow(String a, String b, [String? c]) { + return TableRow( + children: [_tableCell(a), _tableCell(b), if (c != null) _tableCell(c)], + ); + } + + Widget _tableCell(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6), + child: Text(text), + ); + } +} + +class _InfoBlock extends StatelessWidget { + const _InfoBlock({required this.title, required this.child}); + + final String title; + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final palette = context.palette; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: palette.strokeSoft), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: theme.textTheme.titleMedium), + const SizedBox(height: 12), + child, + ], + ), + ); + } +} + +class _HelpSection { + const _HelpSection({ + required this.title, + required this.codeLabel, + required this.code, + }); + + final String title; + final String codeLabel; + final String code; +} + +class _HelpCodeSectionCard extends StatelessWidget { + const _HelpCodeSectionCard({required this.section}); + + final _HelpSection section; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final palette = context.palette; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + section.title, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: palette.surfacePrimary, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: palette.strokeSoft), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 18, 18, 8), + child: Row( + children: [ + Text( + section.codeLabel, + style: theme.textTheme.titleMedium?.copyWith( + color: palette.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + IconButton( + tooltip: appText('复制', 'Copy'), + onPressed: () => Clipboard.setData( + ClipboardData(text: section.code.trim()), + ), + icon: const Icon(Icons.content_copy_rounded), + ), + ], + ), + ), + const Divider(height: 1), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.all(18), + child: SelectableText( + section.code.trimRight(), + style: theme.textTheme.bodyLarge?.copyWith( + fontFamily: 'monospace', + height: 1.7, + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/features/settings/settings_page_core.dart b/lib/features/settings/settings_page_core.dart index df275191..928c32e0 100644 --- a/lib/features/settings/settings_page_core.dart +++ b/lib/features/settings/settings_page_core.dart @@ -17,6 +17,7 @@ import '../../widgets/surface_card.dart'; import 'settings_account_panel.dart'; import 'settings_about_panel.dart'; import 'settings_archived_tasks_panel.dart'; +import 'settings_help_panel.dart'; import 'settings_logs_panel.dart'; import 'settings_remote_desktop_panel.dart'; @@ -556,6 +557,11 @@ class _SettingsPageState extends State { onStop: (sessionKey) => _stopArchivedTask(sessionKey), ), ), + ] else if (currentTab == SettingsTab.help) ...[ + SurfaceCard( + key: const ValueKey('settings-help-panel-card'), + child: const SettingsHelpPanel(), + ), ] else if (currentTab == SettingsTab.remoteDesktop) ...[ SurfaceCard( key: const ValueKey('settings-remote-desktop-panel-card'), @@ -603,9 +609,11 @@ class _SettingsTabSelector extends StatelessWidget { ? Icons.desktop_windows_outlined : (tab == SettingsTab.logs ? Icons.terminal_outlined - : (tab == SettingsTab.archivedTasks - ? Icons.inventory_2_outlined - : Icons.hub_outlined)), + : (tab == SettingsTab.help + ? Icons.help_outline_rounded + : (tab == SettingsTab.archivedTasks + ? Icons.inventory_2_outlined + : Icons.hub_outlined))), ), label: Text(tab.label), ), diff --git a/lib/models/app_models.dart b/lib/models/app_models.dart index 3e05dd8c..a2b399a3 100644 --- a/lib/models/app_models.dart +++ b/lib/models/app_models.dart @@ -149,7 +149,7 @@ extension AssistantModeCopy on AssistantMode { }; } -enum SettingsTab { gateway, archivedTasks, remoteDesktop, logs } +enum SettingsTab { gateway, archivedTasks, remoteDesktop, logs, help } extension SettingsTabCopy on SettingsTab { String get label => switch (this) { @@ -157,6 +157,7 @@ extension SettingsTabCopy on SettingsTab { SettingsTab.archivedTasks => appText('归档任务', 'Archived tasks'), SettingsTab.remoteDesktop => appText('AI工作空间', 'AI Workspace'), SettingsTab.logs => appText('运行日志', 'Runtime Logs'), + SettingsTab.help => appText('帮助', 'Help'), }; } diff --git a/macos/Podfile b/macos/Podfile index 09129ab9..571f554e 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -97,7 +97,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) target.build_configurations.each do |config| - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '11.5' + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '14.0' next unless ['Pods-Runner', 'Pods-RunnerTests', 'WebRTC-SDK', 'flutter_webrtc'].include?(target.name) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 3720858f..54cd6ad2 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -61,6 +61,6 @@ SPEC CHECKSUMS: super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db -PODFILE CHECKSUM: 0ff086e365498790bfa0f77c18fab96518c27b1e +PODFILE CHECKSUM: 1eb7d5d1472c632b8f775dd34562291c20ae818a COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 70e474d5..0d884d44 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -511,7 +511,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; OTHER_CFLAGS = ( "$(inherited)", @@ -539,7 +539,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; OTHER_CFLAGS = ( "$(inherited)", @@ -567,7 +567,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; OTHER_CFLAGS = ( "$(inherited)", @@ -629,7 +629,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -665,7 +665,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.5; + MACOSX_DEPLOYMENT_TARGET = 14.0; OTHER_CFLAGS = ( "$(inherited)", "-Wno-ignored-attributes", @@ -688,7 +688,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; @@ -740,7 +740,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -791,7 +791,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -827,7 +827,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.5; + MACOSX_DEPLOYMENT_TARGET = 14.0; OTHER_CFLAGS = ( "$(inherited)", "-Wno-ignored-attributes", @@ -867,7 +867,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.5; + MACOSX_DEPLOYMENT_TARGET = 14.0; OTHER_CFLAGS = ( "$(inherited)", "-Wno-ignored-attributes", @@ -890,7 +890,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -899,7 +899,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/test/features/settings/settings_help_panel_test.dart b/test/features/settings/settings_help_panel_test.dart new file mode 100644 index 00000000..c4b4098c --- /dev/null +++ b/test/features/settings/settings_help_panel_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/features/settings/settings_help_panel.dart'; +import 'package:xworkmate/theme/app_theme.dart'; + +void main() { + testWidgets('renders setup help sections and metadata tables', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(), + home: const Scaffold(body: SettingsHelpPanel()), + ), + ); + + expect(find.text('帮助'), findsWidgets); + expect(find.text('1. 快速安装(一键部署)'), findsOneWidget); + expect(find.text('2. 带 API Key 安装'), findsOneWidget); + expect(find.text('环境变量参考'), findsOneWidget); + expect(find.text('支持平台'), findsOneWidget); + expect(find.text('DEEPSEEK_API_KEY'), findsOneWidget); + expect(find.text('macOS (Apple Silicon / Intel)'), findsOneWidget); + }); +} diff --git a/test/features/settings/settings_remote_desktop_panel_test.dart b/test/features/settings/settings_remote_desktop_panel_test.dart index 544ea1f3..5ce8eebf 100644 --- a/test/features/settings/settings_remote_desktop_panel_test.dart +++ b/test/features/settings/settings_remote_desktop_panel_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/app/app_controller.dart'; +import 'package:xworkmate/features/desktop/desktop_view.dart'; import 'package:xworkmate/features/settings/settings_remote_desktop_panel.dart'; import 'package:xworkmate/runtime/secure_config_store.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; @@ -8,12 +9,27 @@ import 'package:xworkmate/theme/app_theme.dart'; import 'package:xworkmate/widgets/surface_card.dart'; void main() { + test('remote workspace connection is enabled only in remote mode', () { + expect( + desktopRemoteWorkspaceConnectionEnabled(RuntimeConnectionMode.remote), + isTrue, + ); + expect( + desktopRemoteWorkspaceConnectionEnabled( + RuntimeConnectionMode.unconfigured, + ), + isFalse, + ); + }); + group('SettingsRemoteDesktopPanel', () { - testWidgets('renders the panel title and connection dashboard', (tester) async { + testWidgets('renders the panel title and connection dashboard', ( + tester, + ) async { // Set desktop window size tester.view.physicalSize = const Size(1280, 900); tester.view.devicePixelRatio = 1.0; - + final store = _MemorySecureConfigStore(); final controller = _NoopRefreshAppController(store: store); addTearDown(() { @@ -30,20 +46,13 @@ void main() { // Verify the panel headers and titles expect(find.text('AI工作空间'), findsOneWidget); - expect(find.text('连接AI工作空间'), findsOneWidget); + expect(find.text('连接远程 AI 工作空间'), findsOneWidget); + final connectButton = tester.widget( + find.widgetWithText(ElevatedButton, '连接远程 AI 工作空间'), + ); + expect(connectButton.onPressed, isNull); expect(find.text('工作空间管理'), findsOneWidget); - // Verify advanced options are hidden initially - expect(find.text('GPU 加速'), findsNothing); - - // Tap to expand advanced options - await tester.tap(find.text('高级选项')); - await tester.pumpAndSettle(); - - // Verify advanced options appear - expect(find.text('GPU 加速'), findsOneWidget); - expect(find.widgetWithText(TextField, 'Display'), findsOneWidget); - expect(find.text('Display'), findsOneWidget); expect(find.text('工作空间管理'), findsOneWidget); }); }); @@ -54,10 +63,7 @@ Widget _buildTestApp({required Widget child}) { theme: AppTheme.light(), home: Material( child: Center( - child: SizedBox( - width: 1100, - child: SurfaceCard(child: child), - ), + child: SizedBox(width: 1100, child: SurfaceCard(child: child)), ), ), ); @@ -65,7 +71,7 @@ Widget _buildTestApp({required Widget child}) { class _NoopRefreshAppController extends AppController { _NoopRefreshAppController({required SecureConfigStore store}) - : super(environmentOverride: const {}, store: store); + : super(environmentOverride: const {}, store: store); Future refreshAcpCapabilitiesInternal({ bool forceRefresh = false, diff --git a/test/runtime/ui_feature_manifest_desktop_surface_test.dart b/test/runtime/ui_feature_manifest_desktop_surface_test.dart index 64d1b079..768bb9e4 100644 --- a/test/runtime/ui_feature_manifest_desktop_surface_test.dart +++ b/test/runtime/ui_feature_manifest_desktop_surface_test.dart @@ -31,6 +31,7 @@ void main() { desktop.availableSettingsTabs, isNot(contains(SettingsTab.remoteDesktop)), ); + expect(desktop.availableSettingsTabs, contains(SettingsTab.help)); expect( desktop.sanitizeSettingsTab(SettingsTab.remoteDesktop), SettingsTab.gateway,