From dc7c7dc6ccdaf6ec776d822af8249ebc925f75b1 Mon Sep 17 00:00:00 2001 From: menghao Date: Wed, 5 Aug 2026 10:00:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90P7=E9=AA=8C=E6=94=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=8ECI=E7=A8=B3=E5=AE=9A=E6=80=A7=E5=8A=A0?= =?UTF-8?q?=E5=9B=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正工作台前端 lint 与正式测试切片初始化依赖 补齐可调参数完成态测试夹具并稳定生成恢复 HTTP 读取 修正 macOS Mach-O release 产物识别与原生壳门禁 同步阶段七验收文档与长期排障记忆 --- .../src/agent/generation/canvas_generation.rs | 14 +- .../agent/runtime_driver/main_loop_tests.rs | 8 ++ .../project-workspace/gameRunBridge.ts | 2 +- .../src/view/project-development/index.tsx | 11 +- apps/desktop-shell/scripts/check-config.mjs | 3 + .../scripts/stage-release-binary.mjs | 8 +- ...AI游戏创作】项目开发工作台PRD-2026-07-20.md | 2 + docs/project-memory/shared-memory/pitfalls.md | 16 +++ scripts/check-native-shells.mjs | 8 +- .../shared-contracts/src/game_creation_app.rs | 120 ++++++++++-------- 10 files changed, 128 insertions(+), 64 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index ca61da923..0e3cb2a4b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -1487,10 +1487,22 @@ mod canvas_generation_tests { stream .set_read_timeout(Some(Duration::from_secs(2))) .expect("set request read timeout"); + let deadline = std::time::Instant::now() + Duration::from_secs(10); let mut bytes = Vec::new(); let mut buffer = [0_u8; 4096]; loop { - let read = stream.read(&mut buffer).expect("read request bytes"); + let read = match stream.read(&mut buffer) { + Ok(read) => read, + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) && std::time::Instant::now() < deadline => + { + continue; + } + Err(error) => panic!("read request bytes: {error}"), + }; if read == 0 { break; } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs index 6339a811e..1470bd897 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs @@ -152,6 +152,14 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState ("memory/project.md", "# 项目记忆\n\n正式约束。\n"), ("game/game_design.md", "# 游戏设计\n\n核心循环。\n"), ("game/balance.json", r#"{"lives":3,"speed":1}"#), + ( + "game/tunable-parameters.json", + r#"{"schemaVersion":"game-creator-tunable-parameters.v1","parameters":[]}"#, + ), + ( + "game/tunable-values.json", + r#"{"schemaVersion":"game-creator-tunable-values.v1","values":{}}"#, + ), ( "assets/manifest.art.json", r#"{"assets":[],"status":"not-generated"}"#, diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/gameRunBridge.ts b/apps/ai-game-creator-shell/src/features/project-workspace/gameRunBridge.ts index 45a66d5dc..4d0080831 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/gameRunBridge.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/gameRunBridge.ts @@ -1,7 +1,7 @@ import type { GameHostMessage, - GameHostSlicePayload, GameHostMessageType, + GameHostSlicePayload, GameRunSession, GameRuntimeInspectionMetric, GameRuntimeMessage, diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 07107ab68..c45132196 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -41,12 +41,12 @@ import type { } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import { resolveTauriInvoke } from '../../app/tauri'; import { + type ActiveGameTestSlice, beginGameTestSlice, formalGameTestSlices, - type ActiveGameTestSlice, - type TrustedGameResourceInspection, projectGameTestSliceMessage, projectGameTestSliceSessionStatus, + type TrustedGameResourceInspection, trustedInspectionFromRuntimeMessage, } from '../../features/project-workspace/gameTestSliceModel'; import { @@ -580,6 +580,7 @@ export default function ProjectDevelopmentView({ () => formalGameTestSlices(currentRunnableVersion), [currentRunnableVersion], ); + const firstTestSliceId = testSlices[0]?.sliceId ?? ''; const selectedTestSliceIndex = Math.max( 0, testSlices.findIndex((slice) => slice.sliceId === selectedTestSliceId), @@ -808,8 +809,6 @@ export default function ProjectDevelopmentView({ ), [resourcePositionsByCategory], ); - const selectedResource = - resources.find((resource) => resource.id === selectedResourceId) ?? null; const focusedResource = resources.find((resource) => resource.id === focusedResourceId) ?? null; const focusedReplacementBindings = @@ -950,10 +949,10 @@ export default function ProjectDevelopmentView({ }, [currentRunnableVersion, manifest.projectId, mode, projectPath]); useEffect(() => { - setSelectedTestSliceId(testSlices[0]?.sliceId ?? ''); + setSelectedTestSliceId(firstTestSliceId); setActiveTestSlice(null); setTrustedInspection(null); - }, [currentRunnableVersion?.versionId]); + }, [currentRunnableVersion?.versionId, firstTestSliceId]); useEffect(() => { setActiveRunnablePreviewVersionId(null); diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 26ca9cc53..1bf73082c 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -733,7 +733,10 @@ for (const snippet of [ 'header[3] === 0x46', '(stat.mode & 0o111) === 0', 'header.readUInt32BE(0)', + 'machMagic === 0xcafebabf', + 'machMagic === 0xbfbafeca', 'machMagic === 0xfeedfacf', + 'machMagic === 0xcffaedfe', 'header[0] !== 0x4d', 'header[1] !== 0x5a', "console.log('[check:native-shells] desktop-release-binary-artifact')", diff --git a/apps/desktop-shell/scripts/stage-release-binary.mjs b/apps/desktop-shell/scripts/stage-release-binary.mjs index 13ce5bac8..8b50598fc 100644 --- a/apps/desktop-shell/scripts/stage-release-binary.mjs +++ b/apps/desktop-shell/scripts/stage-release-binary.mjs @@ -49,9 +49,13 @@ function assertExecutable(filePath, label) { const machMagic = header.readUInt32BE(0); const isMachO = machMagic === 0xcafebabe || - machMagic === 0xcafed00d || + machMagic === 0xcafebabf || + machMagic === 0xbebafeca || + machMagic === 0xbfbafeca || machMagic === 0xfeedface || - machMagic === 0xfeedfacf; + machMagic === 0xfeedfacf || + machMagic === 0xcefaedfe || + machMagic === 0xcffaedfe; if (!isMachO || (stat.mode & 0o111) === 0) { throw new Error(`${label} must be an executable Mach-O file`); } diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index 986c089ef..a22d58117 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -564,6 +564,8 @@ type ProjectAgentMudPointAttribution = { ### 7.5 阶段七完整验收 +实施状态(2026-08-04):已完成阶段零至阶段六的需求、合同、实现与测试映射复核,并按 `.gitea/workflows/project-ci.yml` 完成仓库门禁、前端、后端 workspace 与原生壳回归。阶段七收口修正了 Rust 格式、前端 import / 未使用变量 lint、正式测试切片初始化 effect 的依赖声明、旧完成态测试夹具缺少可调参数正式产物、生成恢复 mock HTTP 读取对短暂 `WouldBlock` 不稳定,以及 macOS 原生 little-endian Mach-O / FAT64 release 二进制魔数识别;`1280×800` 应用内浏览器验证保持真实登录门禁,`window`、document 与 body 的宽高、滚动尺寸完全一致且控制台无错误。工作台内部结构继续由 AppSurface 集成测试和资源布局 CSS 合同测试验证,不引入认证绕过或伪造 Tauri 环境。 + 1. 对照飞书需求、当前 PRD、技术方案、代码、测试与阶段提交复核阶段零至阶段六;美术编辑生成新资源继续按本 PRD 已确认的闭环条件暂缓,不作为遗漏或伪完成。 2. AppSurface 同时覆盖文档、图片、SVG、音频和视频聚焦;视频必须使用原生 `controls` 且 `preload="metadata"`,读取策略失败时中央主视窗显示安全空态,右侧对话和底部 Agent 状态栏继续存在。 3. `1280×800` 应用内浏览器实测 `window`、document 与 body 均无页面级横向或纵向溢出。浏览器开发页受真实登录门禁保护,不为验收绕过认证或伪造 Tauri;工作台内部结构由 AppSurface 集成测试与资源布局 CSS 合同测试复核。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 85a6cb48e..3887bca27 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4090,3 +4090,19 @@ - 原因:测试触发了与断言无关的账户读取,较快环境中请求会在用例结束前失败,较慢 CI 中请求延迟到组件和 jsdom 均已销毁后才收束。仅用 revision 丢弃旧请求而不在卸载时推进 revision,最后一个在途请求仍会被误认作当前请求。 - 处理:调用方在未认证时不得启动受保护的钱包刷新;可取消的读取要为每轮分配 `AbortController`,新读取先失效并中止旧读取,组件卸载时同时推进 revision、abort 当前请求并清空句柄。所有 `then / catch / finally` 在更新状态前都要检查 signal 与 revision。 - 验证:定向测试覆盖卸载后请求 signal 已中止;同时复跑触发钱包刷新回调的画布生成集成测试和完整前端测试,不能以单文件偶然快速收束代替全量验证。 + +## macOS release 二进制不能只按大端展示值识别 Mach-O(2026-08-04) + +- 现象:Tauri release build 已成功生成可执行 arm64 二进制,`file` 也识别为 Mach-O,但桌面壳 staging 与 `check:native-shells` 仍报“must be an executable Mach-O file”。 +- 原因:原生 Mach-O 文件头通常以 little-endian 字节序写入;用 `readUInt32BE(0)` 读取 arm64 的 `cf fa ed fe` 会得到 `0xcffaedfe`。只接受 `0xfeedfacf` 等大端展示值会误拒绝真实产物,原校验还把 FAT64 魔数误写成了无关值。 +- 处理:staging 与根原生壳门禁使用同一完整白名单,同时接受 Mach-O 32/64 与 FAT 32/64 的正向和字节交换魔数;继续校验普通文件、最小体积与可执行权限,不因平台差异放宽产物真实性。 +- 验证:macOS 本机执行 `npm run desktop-shell:build -- --no-bundle`、`npm run desktop-shell:stage-release-binary` 和 `npm run check:native-shells`;Linux CI 继续验证 ELF,两个 Tauri `Cargo.lock` 在门禁前后必须不变。 +- 关联:`apps/desktop-shell/scripts/stage-release-binary.mjs`、`apps/desktop-shell/scripts/check-config.mjs`、`scripts/check-native-shells.mjs`。 + +## mock HTTP 已设置读超时仍要容忍短暂 WouldBlock(2026-08-04) + +- 现象:生成恢复定向测试通常通过,但完整原生壳 suite 偶发在读取 loopback 请求时收到 `Resource temporarily unavailable`,导致 mock server 提前退出,主测试随后只看到 channel 断开。 +- 原因:macOS 上带读取超时的 `TcpStream::read` 可以用 `WouldBlock` 表示本轮超时;完整 suite 调度压力可能让请求字节晚于单次两秒窗口到达。对第一次 `WouldBlock` 直接 panic 会把调度抖动误判成协议失败。 +- 处理:测试 HTTP reader 继续保留单次 socket 超时,并在明确的总 deadline 内重试 `WouldBlock / TimedOut`;超过总 deadline 或出现其它 I/O 错误仍失败关闭。不能移除超时或无限重试,否则真正的请求缺失会把 CI 挂死。 +- 验证:定向复跑 `recovery_scan_resumes_accepted_generation_on_default_worker_stack`,并在 `--test-threads=1` 的完整 AI 游戏壳 suite 中确认生成恢复、下载和账本清理全部通过。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs`。 diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index bbfd91361..622269dfb 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -4915,9 +4915,13 @@ function assertDesktopReleaseBinaryArtifact() { const machMagic = header.readUInt32BE(0); const isMachO = machMagic === 0xcafebabe || - machMagic === 0xcafed00d || + machMagic === 0xcafebabf || + machMagic === 0xbebafeca || + machMagic === 0xbfbafeca || machMagic === 0xfeedface || - machMagic === 0xfeedfacf; + machMagic === 0xfeedfacf || + machMagic === 0xcefaedfe || + machMagic === 0xcffaedfe; if (!isMachO || (stat.mode & 0o111) === 0) { throw new Error( 'desktop macOS release binary must be an executable Mach-O file', diff --git a/server-rs/crates/shared-contracts/src/game_creation_app.rs b/server-rs/crates/shared-contracts/src/game_creation_app.rs index 57db62f95..6e95fa499 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -2396,9 +2396,11 @@ mod tests { resource_id: "asset-player-next".to_string(), }, ]; - assert!(validate_game_iteration_versions(&[root, invalid_child]) - .expect_err("reject duplicate slot") - .contains("资源槽位重复")); + assert!( + validate_game_iteration_versions(&[root, invalid_child]) + .expect_err("reject duplicate slot") + .contains("资源槽位重复") + ); } #[test] @@ -2552,33 +2554,39 @@ mod tests { ) .expect("accept valid runnable version graph"); - assert!(validate_runnable_game_versions( - "project-runnable", - &[root.clone(), child.clone()], - Some("missing") - ) - .expect_err("reject missing current version") - .contains("当前可运行版本不存在")); + assert!( + validate_runnable_game_versions( + "project-runnable", + &[root.clone(), child.clone()], + Some("missing") + ) + .expect_err("reject missing current version") + .contains("当前可运行版本不存在") + ); let mut invalid_child = child.clone(); invalid_child.validation.playtest_passed = false; - assert!(validate_runnable_game_versions( - "project-runnable", - &[root.clone(), invalid_child], - Some("runnable-r2") - ) - .expect_err("reject incomplete validation") - .contains("缺少完整验证凭证")); + assert!( + validate_runnable_game_versions( + "project-runnable", + &[root.clone(), invalid_child], + Some("runnable-r2") + ) + .expect_err("reject incomplete validation") + .contains("缺少完整验证凭证") + ); let mut invalid_child = child; invalid_child.artifact_path = ".agent/runnable-versions/other/artifact".to_string(); - assert!(validate_runnable_game_versions( - "project-runnable", - &[root, invalid_child], - Some("runnable-r2") - ) - .expect_err("reject mutable artifact identity") - .contains("不可变产物身份无效")); + assert!( + validate_runnable_game_versions( + "project-runnable", + &[root, invalid_child], + Some("runnable-r2") + ) + .expect_err("reject mutable artifact identity") + .contains("不可变产物身份无效") + ); } #[test] @@ -2592,45 +2600,53 @@ mod tests { let mut wrong_version = version.clone(); wrong_version.test_slices[0].version_id = "runnable-r2".to_string(); - assert!(validate_runnable_game_versions( - "project-runnable", - &[wrong_version], - Some("runnable-r1") - ) - .expect_err("reject a slice owned by another version") - .contains("测试切片版本身份不一致")); + assert!( + validate_runnable_game_versions( + "project-runnable", + &[wrong_version], + Some("runnable-r1") + ) + .expect_err("reject a slice owned by another version") + .contains("测试切片版本身份不一致") + ); let mut duplicate_order = version.clone(); let mut second_slice = duplicate_order.test_slices[0].clone(); second_slice.slice_id = "boss".to_string(); duplicate_order.test_slices.push(second_slice); - assert!(validate_runnable_game_versions( - "project-runnable", - &[duplicate_order], - Some("runnable-r1") - ) - .expect_err("reject duplicate slice order") - .contains("测试切片 ID 或顺序重复")); + assert!( + validate_runnable_game_versions( + "project-runnable", + &[duplicate_order], + Some("runnable-r1") + ) + .expect_err("reject duplicate slice order") + .contains("测试切片 ID 或顺序重复") + ); let mut active_at_registration = version.clone(); active_at_registration.test_slices[0].status = GameTestSliceStatus::Playing; - assert!(validate_runnable_game_versions( - "project-runnable", - &[active_at_registration], - Some("runnable-r1") - ) - .expect_err("reject a non-idle registered slice") - .contains("必须以 idle 登记")); + assert!( + validate_runnable_game_versions( + "project-runnable", + &[active_at_registration], + Some("runnable-r1") + ) + .expect_err("reject a non-idle registered slice") + .contains("必须以 idle 登记") + ); let mut unbound_descriptor = version; unbound_descriptor.resource_descriptors[0].resource_id = "unbound".to_string(); - assert!(validate_runnable_game_versions( - "project-runnable", - &[unbound_descriptor], - Some("runnable-r1") - ) - .expect_err("reject an unbound trusted descriptor") - .contains("资源描述未绑定")); + assert!( + validate_runnable_game_versions( + "project-runnable", + &[unbound_descriptor], + Some("runnable-r1") + ) + .expect_err("reject an unbound trusted descriptor") + .contains("资源描述未绑定") + ); } #[test]