diff --git a/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs b/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs index ad77d7f19..6b03ae39c 100644 --- a/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs +++ b/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs @@ -885,6 +885,10 @@ function readBrowserDom(url) { function resolveChromeBin() { for (const candidate of [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', + '/opt/google/chrome/chrome', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', 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 6f7f7d55f..ca61da923 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 @@ -2057,7 +2057,7 @@ mod canvas_generation_tests { #[tokio::test] async fn recovery_scan_resumes_accepted_generation_on_default_worker_stack() { - let temporary = tempfile::tempdir().expect("create accepted scan project"); + let temporary = crate::tests::canonical_test_tempdir("accepted-generation-scan-"); let root = temporary.path(); init_local_game_project_at(root, "accepted-scan", "恢复扫描测试") .expect("init accepted scan project"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/external_generation_state.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/external_generation_state.rs index 16bcd0a62..490540b08 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/external_generation_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/external_generation_state.rs @@ -736,7 +736,7 @@ mod external_generation_state_tests { #[test] fn prepared_generation_state_reuses_identity_and_only_accepted_can_resume() { - let temporary = tempfile::tempdir().expect("create generation ledger project"); + let temporary = crate::tests::canonical_test_tempdir("external-generation-ledger-"); let root = temporary.path(); init_local_game_project_at(root, "generation-ledger", "生成账本测试") .expect("init project"); @@ -846,7 +846,7 @@ mod external_generation_state_tests { #[test] fn legacy_completed_generation_persists_only_allowlisted_safe_download_fields() { - let temporary = tempfile::tempdir().expect("create legacy generation ledger project"); + let temporary = crate::tests::canonical_test_tempdir("legacy-generation-ledger-"); let root = temporary.path(); init_local_game_project_at(root, "legacy-generation-ledger", "旧同步生成账本测试") .expect("init project"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/pending_confirmation_ledger.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/pending_confirmation_ledger.rs index fa5493585..6de6a816e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/pending_confirmation_ledger.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/pending_confirmation_ledger.rs @@ -790,7 +790,7 @@ mod tests { #[test] fn generation_cleanup_failure_preserves_pending_identity_anchor() { - let temporary = tempfile::tempdir().expect("create pending cleanup project"); + let temporary = crate::tests::canonical_test_tempdir("pending-generation-cleanup-"); let root = temporary.path(); let run_id = "generation-cleanup-order-run"; init_local_game_project_at(root, "generation-cleanup-order", "生成账本清理顺序测试") diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs index 383a7c415..71370aaf8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs @@ -425,7 +425,7 @@ mod tests { const ORDINARY_NOTICE: &str = "除下方有界仓库启动上下文、当前 Session 未压缩对话尾部或历史压缩摘要外,项目记忆、资产和源码正文不会预加载"; const MEMORY_MARKER: &str = "supervisor-preloaded-context-marker"; - let directory = tempfile::tempdir().expect("temp project directory"); + let directory = crate::tests::canonical_test_tempdir("provider-request-project-"); let root = directory.path().join("project"); init_local_game_project_at(&root, "project-1", "项目总控预加载说明测试") .expect("project init"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_deadline_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_deadline_tests.rs index 843dc4c06..c7e1e211d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_deadline_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_deadline_tests.rs @@ -64,14 +64,8 @@ async fn game_chat_absolute_deadline_returns_an_in_flight_result_before_expiry() #[tokio::test] async fn game_chat_absolute_deadline_preserves_external_generation_reconciliation() { - let root = std::env::temp_dir().join(format!( - "genarrative-game-chat-deadline-reconciliation-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos() - )); + let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-reconciliation-"); + let root = temporary.path().join("project"); init_local_game_project_at(&root, "deadline-reconciliation", "硬截止收尾测试") .expect("project init"); bind_game_creator_agent_runtime_run_profile_at( @@ -271,14 +265,8 @@ async fn game_chat_absolute_deadline_preserves_external_generation_reconciliatio #[test] fn game_chat_absolute_deadline_still_cleans_local_action_recovery() { - let root = std::env::temp_dir().join(format!( - "genarrative-game-chat-deadline-local-cleanup-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system clock") - .as_nanos() - )); + let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-local-cleanup-"); + let root = temporary.path().join("project"); init_local_game_project_at(&root, "deadline-local-cleanup", "硬截止本地清理测试") .expect("project init"); let mut runtime = start_game_creator_agent_runtime_task_at( 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 6981f58f4..6339a811e 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 @@ -767,7 +767,7 @@ fn standard_specialist_empty_plan_has_no_deterministic_final_reply_fallback() { fn autonomous_manifest_waiting_context_persists_without_finishing_parent_run() { const RUN_ID: &str = "autonomous-manifest-waiting-parent"; const TASK: &str = "生成完整小游戏并完成项目任务图"; - let temporary = tempfile::tempdir().expect("create manifest waiting root"); + let temporary = crate::tests::canonical_test_tempdir("manifest-waiting-"); let root = temporary.path().join("project"); init_local_game_project_at(&root, "manifest-waiting-project", TASK) .expect("init manifest waiting project"); @@ -981,7 +981,7 @@ async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallbac const TASK: &str = "生成一个可完成静态检查和双视口试玩的塔防游戏"; const TEST_KEY: &str = "autonomous-final-reply-fallback-key"; - let temporary = tempfile::tempdir().expect("create autonomous fallback root"); + let temporary = crate::tests::canonical_test_tempdir("autonomous-fallback-"); let root = temporary.path().join("project"); init_local_game_project_at(&root, "autonomous-fallback-project", TASK) .expect("init autonomous fallback project"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs index 974216689..364eca9cc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs @@ -1221,7 +1221,7 @@ mod orphaned_external_generation_recovery_tests { #[test] fn recovery_scan_preserves_active_generation_orphan_then_cleans_terminal_legacy_orphan() { - let temporary = tempfile::tempdir().expect("create orphan generation recovery project"); + let temporary = crate::tests::canonical_test_tempdir("orphan-generation-recovery-"); let root = temporary.path(); let run_id = "orphan-generation-recovery-run"; init_local_game_project_at(root, "orphan-generation-recovery", "孤儿生成账本恢复测试") diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_bundle.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_bundle.rs index 6c162f963..17973d262 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_bundle.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_bundle.rs @@ -827,10 +827,11 @@ mod tests { .duration_since(UNIX_EPOCH) .expect("system clock should be after epoch") .as_nanos(); - let root = std::env::temp_dir().join(format!( - "genarrative-context-window-boundary-{}-{unique}", + let temporary = crate::tests::canonical_test_tempdir(&format!( + "context-window-boundary-{}-{unique}-", std::process::id() )); + let root = temporary.path().join("project"); init_local_game_project_at(&root, "project-1", "上下文窗口边界恢复项目") .expect("project init"); let mut runtime = start_game_creator_agent_runtime_task_at( diff --git a/apps/ai-game-creator-shell/src-tauri/src/mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/mcp.rs index 2c5f26c40..28cc2f1c6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/mcp.rs @@ -1931,7 +1931,10 @@ mod tests { } fn mcp_test_project(label: &str) -> PathBuf { - let root = std::env::temp_dir().join(format!( + let temp_root = std::env::temp_dir() + .canonicalize() + .expect("canonicalize MCP test temp root"); + let root = temp_root.join(format!( "game-creator-mcp-{label}-{}-{}", std::process::id(), MCP_TEST_PROJECT_COUNTER.fetch_add(1, Ordering::Relaxed) diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/recovery.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/recovery.rs index a255f8748..8365f3873 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/recovery.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/recovery.rs @@ -1606,5 +1606,12 @@ async fn project_supervisor_resume_rechecks_delegate_policy_after_delivery_reser .expect("read barrier after rejecting reserved delivery") .is_clear()); + let released = wait_for_agent_runtime_lane_release_async( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + ) + .await; + assert_eq!(released.state.run_id, parent_run_id); + fs::remove_dir_all(root).ok(); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs index 7eb2310e6..83488c15f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs @@ -120,6 +120,16 @@ fn unique_project_path() -> PathBuf { )) } +pub(crate) fn canonical_test_tempdir(prefix: &str) -> tempfile::TempDir { + let temp_root = std::env::temp_dir() + .canonicalize() + .expect("canonicalize test temp root"); + tempfile::Builder::new() + .prefix(prefix) + .tempdir_in(temp_root) + .expect("create test temp directory under canonical root") +} + fn agent_goal_sidecar_path_for_test(root: &Path, agent_id: &str, session_id: &str) -> PathBuf { let path_key = |value: &str| { format!("{:x}", Sha256::digest(value.as_bytes())) 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 75c52a682..8d7663508 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 @@ -56,7 +56,6 @@ import { type ProjectResource, type ProjectResourceCategory, projectResourcesFromReadModels, - type ProjectVersionResourceSummary, } from './resourceProjectionModel'; import { useProjectResourceCanvasLayout } from './useProjectResourceCanvasLayout'; diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts index 4f2e58f74..6cc98c8e5 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts @@ -1,6 +1,6 @@ import type { - GameIterationVersion, GameCreationAppManifest, + GameIterationVersion, ProjectResourceCanvasSection, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 0fb7a155f..18b7c4bff 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -931,7 +931,7 @@ export function registerProjectWorkbenchFoundationTests() { expect(restoredCard?.getAttribute('aria-pressed')).toBe('true'); }); - it('loads registered documents, art media, and audio inside the central focus state', async () => { + it('loads registered documents, art media, video, and audio with safe failure states inside central focus', async () => { const manifest = createGameCreationAppManifest( 'workbench-resource-media', '资源媒体测试', @@ -951,6 +951,13 @@ export function registerProjectWorkbenchFoundationTests() { localPath: 'assets/icon.svg', source: { kind: 'generated' }, }, + { + id: 'art-video', + kind: 'animation', + mediaType: 'video/mp4', + localPath: 'assets/intro.mp4', + source: { kind: 'generated' }, + }, { id: 'audio-bgm', kind: 'bgm', @@ -958,6 +965,13 @@ export function registerProjectWorkbenchFoundationTests() { localPath: 'assets/bgm.mp3', source: { kind: 'generated' }, }, + { + id: 'blocked-document', + kind: 'design-document', + mediaType: 'text/markdown', + localPath: 'game/blocked.md', + source: { kind: 'generated' }, + }, ); let layoutRevision = 0; const invoke = vi.fn( @@ -990,6 +1004,9 @@ export function registerProjectWorkbenchFoundationTests() { }; } if (command === 'read_local_project_text_preview') { + if (args?.relativePath === 'game/blocked.md') { + throw new Error('项目权限策略要求用户确认:file.read'); + } expect(args).toMatchObject({ projectPath: '/tmp/workbench-resource-media', relativePath: 'game/design.md', @@ -1004,6 +1021,14 @@ export function registerProjectWorkbenchFoundationTests() { } if (command === 'read_local_project_media_preview') { if (args?.category === 'art') { + if (args?.relativePath === 'assets/intro.mp4') { + return { + path: 'assets/intro.mp4', + mediaType: 'video/mp4', + byteLen: 128, + dataUrl: 'data:video/mp4;base64,AAAAIGZ0eXA=', + }; + } return { path: 'assets/icon.svg', mediaType: 'image/svg+xml', @@ -1065,6 +1090,22 @@ export function registerProjectWorkbenchFoundationTests() { ), ); + fireEvent.click(screen.getByRole('button', { name: /intro\.mp4/ })); + const video = (await screen.findByLabelText( + 'intro.mp4 视频预览', + )) as HTMLVideoElement; + expect(video.controls).toBe(true); + expect(video.preload).toBe('metadata'); + expect(video.getAttribute('src')).toBe( + 'data:video/mp4;base64,AAAAIGZ0eXA=', + ); + fireEvent.click( + within(screen.getByRole('region', { name: 'intro.mp4' })).getByRole( + 'button', + { name: '收起资源' }, + ), + ); + fireEvent.click(screen.getByRole('button', { name: /bgm\.mp3/ })); const audio = (await screen.findByLabelText( 'bgm.mp3 音频播放器', @@ -1076,6 +1117,19 @@ export function registerProjectWorkbenchFoundationTests() { 'read_local_project_media_preview', expect.objectContaining({ category: 'audio' }), ); + fireEvent.click( + within(screen.getByRole('region', { name: 'bgm.mp3' })).getByRole( + 'button', + { name: '收起资源' }, + ), + ); + + fireEvent.click(screen.getByRole('button', { name: /blocked\.md/ })); + expect((await screen.findByRole('alert')).textContent).toBe( + '当前项目策略要求先确认读取资源', + ); + expect(screen.getByLabelText('陶泥儿 Agent 对话')).not.toBeNull(); + expect(screen.getByLabelText('子 Agent 状态栏')).not.toBeNull(); }); it('renders, filters, highlights, and destroys the resource dependency overlay without pointer previews', async () => { diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index 2786b73e8..46a1ab8de 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -1,6 +1,6 @@ # AI 游戏创作项目开发工作台 PRD -更新时间:`2026-08-03`(阶段六) +更新时间:`2026-08-03`(阶段七验收收口) ## 1. 产品定位 @@ -445,9 +445,17 @@ type ProjectAgentMudPointAttribution = { 5. 任意现有 manifest 写入只能保留磁盘版本前缀并追加新记录;修改、删除或重排已有版本时写入失败,原 manifest 字节不被覆盖。 6. 版本选择和高亮不写 manifest、布局 sidecar 或 project revision;dependency / type 两种布局都可显示绑定高亮,既有依赖关系 SVG 语义不变。 +### 7.5 阶段七完整验收 + +1. 对照飞书需求、当前 PRD、技术方案、代码、测试与阶段提交复核阶段零至阶段六;美术编辑生成新资源继续按本 PRD 已确认的闭环条件暂缓,不作为遗漏或伪完成。 +2. AppSurface 同时覆盖文档、图片、SVG、音频和视频聚焦;视频必须使用原生 `controls` 且 `preload="metadata"`,读取策略失败时中央主视窗显示安全空态,右侧对话和底部 Agent 状态栏继续存在。 +3. `1280×800` 应用内浏览器实测 `window`、document 与 body 均无页面级横向或纵向溢出。浏览器开发页受真实登录门禁保护,不为验收绕过认证或伪造 Tauri;工作台内部结构由 AppSurface 集成测试与资源布局 CSS 合同测试复核。 +4. 根目录全量 Vitest、前后端 typecheck / lint / build、Rust workspace test / check、SpacetimeDB schema、原生壳、内容 / 编码、生产运维与部署门禁全部通过后,阶段七才允许提交。 +5. 本地 `.env`、`.env.local`、密钥、缓存、日志和构建产物不进入阶段七提交;提交前再次执行编码检查和 `git diff --check`。 + ## 8. 非目标 -- 阶段六不实现资源卡手动拖动,也不实现资源聚焦工具栏、资源聚焦工具侧边栏、美术编辑、音频编辑 / 替换、资源重新生成、资源替换、下一迭代版本创建入口、运行版本切换、版本回滚、运行模块扩展、测试切片、运行态消费版本、数值参数或泥点归因。正式版本记录已经成为 manifest 业务真相,但本阶段只读取、校验和展示已有记录。 +- 当前收口不实现资源卡手动拖动,也不实现资源聚焦工具栏、资源聚焦工具侧边栏、美术编辑、音频编辑 / 替换、资源重新生成、资源替换、下一迭代版本创建入口、运行版本切换、版本回滚、运行模块扩展、测试切片、运行态消费版本、数值参数或泥点归因。正式版本记录已经成为 manifest 业务真相,但当前只读取、校验和展示已有记录。 - 本切片不持久化资源聚焦状态、画布缩放 / 平移、搜索条件、筛选条件或当前 mode;聚焦退出时的列表上下文恢复只限当前前端会话,这些状态如需跨重启保存必须另行扩展合同,不能塞入 `game-creator-resource-layout.v1`。 - 不修改 SpacetimeDB schema。 - 不开放普通用户 Agent.md/Skill。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0f542c3f4..8bc135b27 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,13 @@ # 决策记录 +## 2026-08-03 资源管理阶段七以完整 CI 与可重复界面合同收口 + +- 背景:飞书资源管理需求的阶段零至阶段六已经分别完成资源卡禁拖、固定资源投影、中央聚焦、安全文档 / 媒体预览、依赖深度与正式版本只读模型;最后需要统一复核需求边界并用当前主分支完整门禁排除集成回归。 +- 决策:阶段七不新增平行功能,只补齐视频原生控件和资源读取策略失败空态的 AppSurface 证据,运行完整前端 / Rust / 运维 CI,并以真实 `1280×800` 浏览器测量证明页面级无溢出。开发页的真实登录门禁不得为验收绕过,工作台内部以 AppSurface 与 CSS 合同测试复核;美术编辑继续等待画板回写、血缘登记、新资源自动选中与邻近布局闭环。 +- 影响范围:`apps/ai-game-creator-shell` 资源管理测试、跨平台 CI / 运维测试脚本、工作台 PRD、AI 游戏创作实施计划和共享项目记忆;不修改 SpacetimeDB schema、manifest 业务合同或资源布局 sidecar。 +- 验证方式:根目录全量 Vitest `171` 个文件、`2155 passed / 5 skipped`;追加 lint、build、Rust workspace test / check、schema、原生壳、内容 / 编码和生产运维门禁;应用内浏览器在 `1280×800` 下 document/body client 与 scroll 尺寸相等。 +- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + ## 2026-08-03 正式项目版本阶段六落在 manifest 追加不可变记录 - 背景:阶段一至五已经完成资源卡禁拖、固定分类投影、中央聚焦、依赖关系图和引用深度排列,但“项目版本”仍只能接受未接线的前端 read model;checkpoint、布局 sidecar 和项目 mutation revision 都不能代表正式可追溯版本。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 30f5b50dc..35d24bd12 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -14,6 +14,14 @@ - 关联:相关文件、文档、提交或 Issue ``` +## Linux 生产脚本门禁不能假设本地也是 GNU userland + +- 现象:macOS 本地运行维护页、生产 API 部署和 Rust 产物门禁时,依次出现 `mv: illegal option -- T`、`mapfile: command not found`、`/usr/bin/cp` / `/usr/bin/chmod` 不存在,以及 `.rlib` 明明含有 `.o` 却报告“没有可扫描成员”;安全修复计划还会把 `/var/folders` 到 `/private/var/folders` 的系统别名误判为用户符号链接。 +- 原因:生产机是 Linux/GNU,而本地门禁运行在 BSD userland、Bash 3.2 和 BSD ar;测试桩硬编码 Linux 二进制路径与参数,归档解析器没有去掉 BSD 扩展成员名的尾随 NUL,路径校验也直接比较了未规范化字符串。 +- 处理:维护 marker 使用同目录临时文件加 POSIX `mv -f`;生产部署测试桩在 macOS 忠实模拟 GNU `mv/ln -T` 的“目标不是目录”语义,并按平台选择系统工具;脚本收集服务使用 Bash 3.2 可用的 `while read`;rlib 解析清理 BSD 成员名 NUL;计划文件只规范化系统临时目录别名,仍拒绝其下用户创建的符号链接组件。 +- 验证:运行 `npm run check:maintenance-page`、`npm run check:production-api-deploy`、`npm run check:server-rs-ddd`、`npm run test -- scripts/spacetime-repair-editor-canvas-resources.test.ts`,并在 Linux CI 保留同一生产脚本语义。 +- 关联:`scripts/deploy/maintenance-on.sh`、`scripts/check-maintenance-page.mjs`、`scripts/check-production-api-deploy.mjs`、`scripts/deploy/production-api-deploy.sh`、`scripts/check-module-runtime-artifact.mjs`、`scripts/spacetime-repair-editor-canvas-resources.mjs`。 + ## External Editor taskId 不能当作本地 manifest taskId - 现象:画布资产之间已有橙色精确引用线,但依赖任务之间没有灰色 task flow;测试用 `design-foundation` 之类字符串时正常,真实生成返回 `task-1` 后失败。 @@ -3707,6 +3715,7 @@ - 原因:`libc::stat.st_dev` 跟随平台 `dev_t`,macOS 为有符号整数,而 `std::os::unix::fs::MetadataExt::dev()` 统一返回 `u64`;直接比较会把 Linux 的类型偶合误当成 Unix 通用契约。 - 处理:与 Rust 标准库的 Unix `MetadataExt` 实现保持一致,先把 `st_dev / st_ino` 规范为 `u64`,再与 `metadata.dev() / metadata.ino()` 比较;设备号、inode 和文件类型三重检查均必须保留。 - macOS 测试夹具:`std::env::temp_dir()` 可能返回 `/var/folders/...`,而 `/var` 是系统兼容符号链接。需要真实项目根的 Runtime 测试应先 canonicalize 已存在的临时根目录,再创建唯一子目录;不得为了让夹具通过而放宽生产 Runtime 的项目根及祖先符号链接拒绝规则。 +- 异步测试隔离:测试触发后台 continuation 后,必须等待对应 Agent lane 完整释放,再删除项目夹具或安装下一项全局 mock 配置;否则前一项后台任务可能抢占后一项的唯一 mock 响应,形成只在全量顺序执行时出现的跨测试污染。 - 验证:macOS 本机运行 `cargo check --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`,并复跑 Agent DB、project owner 和 tool-plan handoff 的 Unix 相对句柄替换检测;Linux CI 继续覆盖原有安全回归。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`runner.rs`、`tool_plan_handoff.rs`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index a5037f384..580ed42db 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -499,6 +499,7 @@ game-project/ ## v1 验收证据矩阵 +- 2026-08-03 资源管理阶段七收口:阶段零至阶段六已逐项对照飞书需求、当前 PRD、实现、测试与提交证据,剩余工作仅为完整验收。AppSurface 新增原生视频 `controls + preload="metadata"` 与资源读取策略失败安全空态回归,并确认失败时右侧 Project Supervisor 对话和底部 Agent Dock 不被中央主视窗替换;全量 Vitest 为 `171` 个文件、`2155 passed / 5 skipped`。应用内浏览器固定 `1280×800` 后,`window`、document 与 body 的 client / scroll 宽高均为 `1280×800`,未出现页面级溢出;开发页真实登录门禁未被绕过,工作台内部结构继续由 AppSurface 和 CSS 合同测试作为可重复证据。最终门禁还包括 lint、build、Rust workspace test / check、SpacetimeDB schema、原生壳、内容 / 编码和生产运维检查;本地 `.env*` 不进入提交。 - `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、主窗口命令按钮复用 `/help` 命令列表、主窗口项目摘要从 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令且未选项目时不显示、灵感草稿只填充输入框不提交、能力按钮复用 `/capabilities`、LLM状态按钮复用 `/llm-status` 且结果回填 Agent 状态列表、聊天侧 `/agents` 汇总和单 Agent 对话的 provider / 模型 / 流式 / API Key 读取状态、开发日志面板只读读取 `.agent/logs/command.log` / `preview.log` / `agent.log`、项目状态按钮复用 `/status`、权限按钮复用 `/policy` 且策略草稿按钮只填入 `/policy-confirm project.index` / `/policy-confirm asset.register` / `/policy-confirm memory.write` / `/policy-confirm preview.start` / `/policy-confirm preview.open` / `/policy-confirm preview.stop` / `/policy-confirm agent.run_status` / `/policy-confirm conversation.read` / `/policy-confirm conversation.write`、审计按钮复用 `/audit`、资产按钮复用 `/assets` 且资产结果可一键复用 `/read`、任务按钮复用 `/tasks`、聊天侧 `/agents` 汇总每个 Agent 的当前状态、聊天侧 `/agent-conversations` 列出 Agent 对话读取命令、聊天侧 `/agent-memories` 列出 Agent 私有记忆读取命令、Trace 按钮复用 `/trace`、文件按钮复用 `/files` 且文件结果可一键复用 `/read`、索引按钮复用 `/index`、记忆 / 短期记忆 / 黑板按钮复用 `/memory long|short|blackboard`、快照按钮复用 `/checkpoint`、快照列表按钮复用 `/checkpoints` 且 checkpoint 结果可一键复用 `/diff` / `/restore`、历史按钮复用 `/history`、受限命令白名单按钮复用 `/commands` 且无需项目初始化、静态自检快捷按钮复用 `/smoke`、预览状态快捷按钮复用 `/preview-status`、主窗口运行时配置面板读写 Tauri 配置目录中的 `game-creator.config.json`、支持全局与每个 agent 单独选择 LLM Provider 且不把 API Key 写入聊天、单 Agent 对话面板可手动追加私有记忆且走 `memory.write` 策略、主窗口提供音效登记、画板音频导入和常用生成产物读取草稿入口,聊天侧 `/art` 可盘点美术素材且不直接触发平台生成或画板同步,聊天侧 `/context` 可盘点生成上下文来源且不直接读取上下文文件,聊天侧 `/timeline` 可汇总项目活动时间线且不直接读取日志或 trace 文件,聊天侧 `/artifacts` 可列出常用生成产物读取命令,聊天侧 `/run-artifacts` 可列出最近 run 产物读取命令,聊天侧 `/run-files` 可列出 Agent 运行辅助文件读取命令,聊天侧 `/logs` 可列出固定日志读取命令且不直接读取日志,聊天侧 `/brief` 只基于当前已加载的 manifest / 最近 run trace / 预览状态 / 资产数量 / 最近命令生成项目简报,聊天侧 `/goal` 只基于当前 manifest.goal / 最近 run goal / taskGraph.goal 汇总创作目标来源,提供 `/next` 或 `/agent-resume 细化目标:` 后续草稿且不直接触发 Tauri 读写、文件读取、预览启动或新增面板,聊天侧 `/mvp` 只基于当前 manifest / 最近 run trace / preview / 任务 / 资产 / 最近命令汇总本轮最小可玩范围,提供 `/run` 等后续草稿且不直接触发 Tauri 读写、文件读取、预览启动、导出或新增面板,聊天侧 `/audience` 只基于当前 manifest / 最近 run trace / preview 准备首批试玩对象和观察重点且不直接触发 Tauri 读写、预览启动、导出或继续 run,聊天侧 `/feedback` 只基于当前 manifest / 最近 run trace / preview 准备试玩反馈模板和修改说明草稿且不直接触发 Tauri 读写、预览启动或继续 run,聊天侧 `/next` 基于当前已加载的 manifest / 最近 run trace 输出下一步建议和 `/goal` / `/mvp` / `/accessibility` / `/performance` / `/tasks` / `/criteria` / `/groups` / `/budget` / `/qa` / `/changes` / `/trace` / `/run` / `/open-preview` / `/test-plan` / `/audience` / `/feedback` / `/assets` / `/art` / `/context` / `/timeline` / `/artifacts` / `/run-artifacts` / `/run-files` / `/logs` / `/agent-resume ` 等安全命令草稿方向,提供一个首选草稿且不直接触发 Tauri 读写、预览启动或文件读取,聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed`、`needs-revision`、`running`、`max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke`、`/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并切换客户端运行视图、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并切换客户端运行视图、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地项目文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后实时展示 Planner / Orchestrator / 角色 brief / Generator / Evaluator / 写盘 / 自检进度,并自动读取 `.agent/run.latest.json` 在普通聊天消息里展示 Run、LLM 对话、loop 轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。 - V1.17 单 Agent 持久计划验收:Rust 定向用例覆盖 native function 显式 `planUpdate`、文本 JSON omission 兼容、输入上限、单调 revision、外层 failed / budget-exhausted 保留最后可信进度、终态保留、工具 action 下标零推进、未完成步骤阻止 final、损坏状态失败关闭、context bundle v3/v2 恢复、finalization v2 计划快照与 assistant 已落盘后的 state 丢失恢复,以及 thinking / legacy plan / repair 公共审计零正文;`appSurface.test.ts` 覆盖开发 UI 刷新后完整 8 步仍在,以及普通用户 Supervisor 只显示完成数、当前步骤、等待、下一步和协作数量。2026-07-16 正式 `openai_chat / gpt-5.5` 的 `steer-runner-kill` suite 已证明 Runner boot 切换和 same-run steer 后 Agent/Session/run 不变、终态步骤不丢、revision 不回退、旧动作零执行、副作用零重放、唯一 assistant/completed 与零正文/密钥/路径公共泄漏,当前门禁状态为 PASS。 - V1.18 单 Agent 持久 Goal mode 验收:Rust/Runner 定向用例覆盖 Goal CAS 生命周期、当前 Session/run 隔离、Provider 中断安全边界、paused 重启不自启、同 run resume、旧 cancel tombstone 清理、当前 v5 context、v5 pending action 的 Goal 快照门禁、旧 schema 失败关闭、Goal edit 后自动/确认动作转 `blocked` 并重规划、finalization v3 以及 assistant 后 Runtime/Goal completed 顺序;`appSurface.test.ts` 覆盖 `执行 / 聊天 / 目标`、独立 Goal 弹层、完整控制动作和正式用户界面隔离。2026-07-16 真实 `openai_chat / gpt-5.5` 已完成 revision 1 -> 2、旧动作 blocked 且零执行、真实失败后 patchset 修复、pause、Runner pidfd 强杀换 boot、重启零推进、显式同 run resume;最终代码快照复跑有 11 组 Provider lifecycle 闭合、计划 revision 11 八步完成、唯一 assistant、零副作用重放和零 Goal/密钥/路径公共泄漏,门禁状态为 PASS。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index cf62e1db0..fae019c5f 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -607,7 +607,7 @@ Nginx 与 Pingora 在维护 marker 存在时对内网来源绕过整站维护闸 该规则只绕过网关维护响应,不会自动拉起 api-server、SpacetimeDB 或其它已停止的服务。人工执行 `maintenance-on.sh` 且后端仍运行时,可以从内网继续访问整站和修改后台数据;`pause-after-stdb` 会停止旧 API/controller/worker,在 API 被停期间静态页面可能仍可加载,但普通 API 与 `/admin/api/**` 仍不可用。验证使用 `npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`、`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml`、`npm run check:pingora-gateway-smoke` 和 `npm run check:production-ops`,不要在 live 机器上为测试临时创建维护 marker。 -版本化默认维护页固定为 `public/maintenance.html`,使用 `public/branding/taonier-maintenance-page.png` 作为品牌视觉,只允许保存无日期、无具体时段的通用文案;正常 Web 构建由 Vite 复制到发布包根目录的 `web/maintenance.html`,并由 `check-maintenance-page.mjs` 在打包前拒绝“今天 / 今晚”、具体日期或 `HH:MM` 等临时公告。计划内停服的临时公告必须放在 release 外文件中,通过 `/opt/genarrative/current/scripts/deploy/maintenance-on.sh --page-file <公告HTML> <维护原因>` 原子安装到 `/var/lib/genarrative/maintenance/page.html`。Nginx 与 Pingora 在该文件存在时优先返回它,缺失时回退当前 Web 制品的默认维护页;同一维护窗口内 Stdb / API 的后续 `maintenance-on.sh` 调用保留已安装公告,`maintenance-off.sh` 同时删除 marker 和运行态公告,避免下次维护复活旧内容。公告启用后同时用 `genarrative.world` 与 `www.genarrative.world` 的真实 HTTPS 响应校验 `503` 和公告正文。 +版本化默认维护页固定为 `public/maintenance.html`,使用 `public/branding/taonier-maintenance-page.png` 作为品牌视觉,只允许保存无日期、无具体时段的通用文案;正常 Web 构建由 Vite 复制到发布包根目录的 `web/maintenance.html`,并由 `check-maintenance-page.mjs` 在打包前拒绝“今天 / 今晚”、具体日期或 `HH:MM` 等临时公告。计划内停服的临时公告必须放在 release 外文件中,通过 `/opt/genarrative/current/scripts/deploy/maintenance-on.sh --page-file <公告HTML> <维护原因>` 原子安装到 `/var/lib/genarrative/maintenance/page.html`;公告页和 marker 都使用同目录临时文件加 POSIX 兼容 `mv -f` 的原子替换,不得依赖 GNU `mv -T`,确保 Linux 生产机与 macOS/BSD 本地门禁语义一致。Nginx 与 Pingora 在该文件存在时优先返回它,缺失时回退当前 Web 制品的默认维护页;同一维护窗口内 Stdb / API 的后续 `maintenance-on.sh` 调用保留已安装公告,`maintenance-off.sh` 同时删除 marker 和运行态公告,避免下次维护复活旧内容。公告启用后同时用 `genarrative.world` 与 `www.genarrative.world` 的真实 HTTPS 响应校验 `503` 和公告正文。 生产 Jenkins 的 `Pipeline script from SCM` 由 Jenkins controller 读取 Jenkinsfile。所有生产 Job 的 SCM URL,以及 Jenkinsfile 内部在 Jenkins Built-In Node 执行的源码准备,统一使用 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`,并显式传入 Jenkins SSH 凭据 `genarrative-local-gitea-ssh`;不再配置局域网 IP、`https://git.genarrative.world/...` 公网 fallback 或 `http://genarrative-station/git/GenarrativeAI/Genarrative.git`。所有 `GitSCM checkout` 都必须保留单分支 refspec、`shallow=true`、`depth=1`、`noTags=true` 与 `honorRefspec=true`。API / Web / Stdb 发布类流水线不在目标机器 checkout Git,统一执行上游构建归档里的部署脚本;Server-Provision 和数据库导入导出也由带 `linux && genarrative-build` 标签的 Jenkins Built-In Node 先 checkout 并 stash 所需脚本,再交给目标 dev / release agent,避免目标机把 `127.0.0.1` 误解为远端 Gitea 或让产物 commit 与执行脚本漂移。 diff --git a/scripts/check-maintenance-page.mjs b/scripts/check-maintenance-page.mjs index d82e1f2e4..2c199cf4f 100644 --- a/scripts/check-maintenance-page.mjs +++ b/scripts/check-maintenance-page.mjs @@ -68,11 +68,19 @@ function validateRuntimePageLifecycle() { const sourcePageFile = path.join(tempRoot, 'announcement.html'); const onScript = path.join(repoRoot, 'scripts/deploy/maintenance-on.sh'); const offScript = path.join(repoRoot, 'scripts/deploy/maintenance-off.sh'); + const onScriptSource = readFileSync(onScript, 'utf8'); const env = { GENARRATIVE_MAINTENANCE_FILE: markerFile, GENARRATIVE_MAINTENANCE_PAGE_FILE: runtimePageFile, }; + if (!onScriptSource.includes('replace_file_atomically')) { + fail('maintenance-on 必须通过统一 helper 原子替换公告页和 marker。'); + } + if (/\bmv\s+-[^\s]*T\b/u.test(onScriptSource)) { + fail('maintenance-on 不得使用 GNU mv 专属的 -T 参数。'); + } + try { const announcement = '