From 97617608ba9f40cb601cf4319a48e45c764def73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Thu, 1 Oct 2026 17:54:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20Web=20=E9=A2=84=E6=A3=80?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=88=86=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/app-shell/homeWebPreflight.ts | 60 +++++++++++++--- .../app-shell/useHomeProjectCreation.ts | 14 ++-- .../tests/homeWebPreflight.test.tsx | 70 +++++++++++++------ docs/project-memory/shared-memory/pitfalls.md | 8 +++ ...¹案】AI游戏创作智能体App实施计划-2026-06-24.md | 1 + 5 files changed, 118 insertions(+), 35 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/app-shell/homeWebPreflight.ts b/apps/ai-game-creator-shell/src/features/app-shell/homeWebPreflight.ts index a166a5f8f..ff489639c 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/homeWebPreflight.ts +++ b/apps/ai-game-creator-shell/src/features/app-shell/homeWebPreflight.ts @@ -4,19 +4,63 @@ import type { HomeCreationType } from '../../view/home/useHomeDraftStore'; export const HOME_WEB_PREFLIGHT_FAILURE = 'Web 游戏环境预检未通过,请检查 Node/npm 或浏览器后重试;尚未开始生成。'; +export const HOME_WEB_PREFLIGHT_IPC_FAILURE = + 'Web 游戏环境预检调用失败,客户端连接异常;请重启客户端后重试,尚未开始生成。'; + +type HomeWebPreflightReport = { + status?: string; + code?: string; +}; + +function knownPreflightCode(value: unknown): string | undefined { + const text = value instanceof Error ? value.message : value; + if (typeof text !== 'string') return undefined; + const match = text.match( + /(?:预检未通过:|预检未通过(|code[=:])([a-z0-9-]+)/i, + ); + return match?.[1]; +} + +export function homeWebPreflightErrorMessage(value: unknown): string { + if (value instanceof Error && value.message === HOME_WEB_PREFLIGHT_FAILURE) { + return HOME_WEB_PREFLIGHT_FAILURE; + } + const code = knownPreflightCode(value); + if (code) return `Web 游戏环境预检未通过(${code});尚未开始生成。`; + return HOME_WEB_PREFLIGHT_IPC_FAILURE; +} + +function isHostPreflightError(value: unknown): boolean { + if (!(value instanceof Error)) return false; + return value.message === HOME_WEB_PREFLIGHT_FAILURE || Boolean(knownPreflightCode(value.message)); +} + export async function ensureHomeWebCreationEnvironment( invoke: TauriInvoke, creationType: HomeCreationType, startMode: ProjectStartMode, ): Promise { if (creationType !== 'game' || startMode !== 'direct-build') return; - try { - const report = await invoke<{ status: string }>( - 'preflight_web_game_creation', - ); - if (report?.status !== 'ready') throw new Error(HOME_WEB_PREFLIGHT_FAILURE); - } catch { - // 不把子进程、Provider 或宿主路径的原始错误注入首页状态栏。 - throw new Error(HOME_WEB_PREFLIGHT_FAILURE); + let lastError: unknown; + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + const report = await invoke( + 'preflight_web_game_creation', + ); + if (report?.status === 'ready') return; + throw new Error( + report?.code + ? `Web 游戏环境预检未通过:${report.code}` + : HOME_WEB_PREFLIGHT_FAILURE, + ); + } catch (error) { + lastError = error; + const code = knownPreflightCode(error); + if (isHostPreflightError(error) || code || attempt === 1) { + throw new Error(homeWebPreflightErrorMessage(error)); + } + await new Promise((resolve) => window.setTimeout(resolve, 250)); + } } + throw new Error(homeWebPreflightErrorMessage(lastError)); } diff --git a/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts b/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts index c7a572afc..bd065b4fa 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts +++ b/apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts @@ -57,7 +57,7 @@ import { importDesignFiles } from '../project-workspace/importDesignFiles'; import { clearAclElevationDenials } from './aclElevation'; import { ensureHomeWebCreationEnvironment, - HOME_WEB_PREFLIGHT_FAILURE, + homeWebPreflightErrorMessage, } from './homeWebPreflight'; import type { ShowTransientNotice } from './model'; import { resolveSessionPreviewOnProjectOpen } from './sessionPreview'; @@ -458,9 +458,10 @@ export function useHomeProjectCreation({ } try { await ensureHomeWebCreationEnvironment(invoke, creationType, startMode); - } catch { - setStatus(HOME_WEB_PREFLIGHT_FAILURE); - return HOME_WEB_PREFLIGHT_FAILURE; + } catch (error) { + const message = homeWebPreflightErrorMessage(error); + setStatus(message); + return message; } const result = await invoke( 'init_local_game_project', @@ -894,7 +895,8 @@ export function useHomeProjectCreation({ draft.creationType, startMode, ); - } catch { + } catch (error) { + const message = homeWebPreflightErrorMessage(error); setHomeCreationOperation( transitionClientOperation( operation, @@ -902,7 +904,7 @@ export function useHomeProjectCreation({ operationScope(), ), ); - return HOME_WEB_PREFLIGHT_FAILURE; + return message; } const suggestedName = options.suggestName ? await suggestAutomaticProjectName(invoke, draft) diff --git a/apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx b/apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx index ad93e007a..39e0a32c3 100644 --- a/apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx +++ b/apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx @@ -3,7 +3,10 @@ import { act, cleanup, renderHook } from '@testing-library/react'; import { afterEach, expect, it, vi } from 'vitest'; import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; -import { HOME_WEB_PREFLIGHT_FAILURE } from '../src/features/app-shell/homeWebPreflight'; +import { + HOME_WEB_PREFLIGHT_FAILURE, + HOME_WEB_PREFLIGHT_IPC_FAILURE, +} from '../src/features/app-shell/homeWebPreflight'; import { useHomeProjectCreation } from '../src/features/app-shell/useHomeProjectCreation'; afterEach(() => { @@ -67,26 +70,51 @@ it('awaits the real home preflight before naming or creating a game', async () = expect(result.current.currentProjectContext?.creationType).toBe('game'); }); -it.each([false, true])( - 'blocks naming, creation, and first generation when preflight fails (throws=%s)', - async (throws) => { - const { result, calls } = mount(async () => { - if (throws) throw new Error('private host path'); - return { status: 'blocked' }; - }); - let status = ''; - await act(async () => { - status = await result.current.createHomeDraftAutomatically( - { creationType: 'game', prompt: '做一个小游戏', attachments: [] }, - 'direct-build', - ); - }); - expect(status).toBe(HOME_WEB_PREFLIGHT_FAILURE); - expect(calls).toEqual(['preflight_web_game_creation']); - expect(result.current.currentProjectContext).toBeNull(); - expect(status).not.toContain('private'); - }, -); +it('blocks naming, creation, and first generation with the host error code', async () => { + const { result, calls } = mount(async () => { + throw new Error('Web 游戏环境预检未通过:web-preflight-browser-failed;尚未启动生成'); + }); + let status = ''; + await act(async () => { + status = await result.current.createHomeDraftAutomatically( + { creationType: 'game', prompt: '做一个小游戏', attachments: [] }, + 'direct-build', + ); + }); + expect(status).toBe('Web 游戏环境预检未通过(web-preflight-browser-failed);尚未开始生成。'); + expect(calls).toEqual(['preflight_web_game_creation']); + expect(result.current.currentProjectContext).toBeNull(); +}); + +it('distinguishes an IPC failure from a host preflight failure', async () => { + const { result, calls } = mount(async () => { + throw new Error('Failed to fetch'); + }); + let status = ''; + await act(async () => { + status = await result.current.createHomeDraftAutomatically( + { creationType: 'game', prompt: '做一个小游戏', attachments: [] }, + 'direct-build', + ); + }); + expect(status).toBe(HOME_WEB_PREFLIGHT_IPC_FAILURE); + expect(calls).toEqual(['preflight_web_game_creation', 'preflight_web_game_creation']); + expect(result.current.currentProjectContext).toBeNull(); +}); + +it('keeps the legacy fallback for a blocked report without a code', async () => { + const { result, calls } = mount(async () => ({ status: 'blocked' })); + let status = ''; + await act(async () => { + status = await result.current.createHomeDraftAutomatically( + { creationType: 'game', prompt: '做一个小游戏', attachments: [] }, + 'direct-build', + ); + }); + expect(status).toBe(HOME_WEB_PREFLIGHT_FAILURE); + expect(calls).toEqual(['preflight_web_game_creation']); + expect(result.current.currentProjectContext).toBeNull(); +}); it.each(['game', 'doc'] as const)( 'does not require the Web preflight for %s planning', diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 0602a9974..2bb5a853b 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6183,3 +6183,11 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **同时**:主站顶栏(`platform-desktop-topbar`)在窄屏必须保持单行——加 `flex-wrap` 后新增一个动作入口就会让品牌折到第二行;窄屏降级靠 `< 640px` 图标化下载入口、`< 480px` 只留品牌 IP 标识。 - **验证**:真机尺寸下用 `document.elementFromPoint(按钮中心)` 断言命中的是按钮自身而不是底部菜单(`overlay.parentElement === document.body`);顶栏断点矩阵(320–768)断言单行且无横向溢出。 - **关联**:`src/components/common/SquareImageCropModal.tsx`、`src/components/platform-entry/PlatformProfileModalShell.tsx`、`src/components/platform-entry/PlatformEntryActiveFlowShell.tsx`、`src/index.css`。 + +## 2026-10-01 AGC 首页把 Web 预检错误与 Tauri IPC 错误合并,造成无法诊断的生成阻拦 + +- **现象**:用户在首页点击「做游戏」后看到「Web 游戏环境预检未通过,请检查 Node/npm 或浏览器」,但同一安装包的 `--environment-check` 可能已经返回 `status=ready`;首页仍会阻止自动命名、建项和首次生成。 +- **根因**:`homeWebPreflight.ts` 原先对 `preflight_web_game_creation` 的所有异常统一抛出 `HOME_WEB_PREFLIGHT_FAILURE`,调用方 `useHomeProjectCreation.ts` 的两层 `catch` 又再次覆盖原始错误。于是宿主真实预检码、Tauri IPC 断连、命令未注册和 WebView `Failed to fetch` 都显示成 Node/npm/浏览器故障。 +- **处理**:保留预检失败关闭语义,但提取 Rust 返回的安全错误码;对无错误码的 Tauri 调用故障显示独立 IPC 提示,并仅对这类瞬态调用失败重试一次。首页调用方必须透传该安全提示,不能重新覆盖为通用文案。 +- **验证**:`apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx` 覆盖 ready、具体宿主错误码、IPC 失败、无错误码 blocked 和 planning 绕过;同一 Windows 安装包 CLI 实测 Node `v22.22.2`、npm `10.9.7`、Edge desktop/mobile PNG 均通过。 +- **关联**:`apps/ai-game-creator-shell/src/features/app-shell/homeWebPreflight.ts`、`apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts`、`apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx`、`apps/ai-game-creator-shell/src-tauri/src/environment_check/web_creation.rs`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 21b0ba771..860ef3572 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -116,6 +116,7 @@ UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_sema - 对客户端刚创建且内容仍匹配可信模板的 Web 脚手架,在正式生成前由宿主执行受控依赖准备和真实 Vite 构建。依赖安装禁用生命周期脚本;不自动安装或覆盖导入/用户修改过的工程。 - 准备凭证的 `ready` 只证明初次环境准备成功,不代表当前游戏已验收,后续正常修改不得因此重新安装。`preparing` 中断恢复必须证明原拥有者已结束且其执行子树已回收;身份或归属未知时保留阻断,不重复执行。 - 输出明确区分“Node/npm 构建能力通过”与“本项目 Vite 构建通过”;任何一步失败保留可行动错误,不降级为预检成功。 +- 首页必须区分宿主预检阻塞与 Tauri IPC 调用失败:Rust 返回的安全错误码可透传到状态栏;无错误码的瞬态 IPC 失败只允许一次有界重试后显示独立的客户端连接故障,不得统一伪装成 Node/npm 或浏览器故障。预检仍保持失败关闭,不得因为展示错误变得可绕过。 - 安装载荷提供相同的安全预检 CLI,验证无系统 Node 的独立运行、缺包/篡改失败关闭。NSIS 解包载荷 smoke 与真实安装器注册流程分开报告,不覆盖当前用户的既有安装。 ### 跑酷固定基线