From 1fedd62c901e4496021914b119c6f009c44f2bb5 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Tue, 30 Jun 2026 10:19:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=88=9B=E4=BD=9C=E5=A3=B3?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=8E=A7=E5=88=B6=E5=AE=A1=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记录自动状态命令到本地命令日志 限制 command.auto 只能记录 auto 权限命令 补齐 agent retry 和 resume 的界面与生命周期测试 --- .../scripts/check-config.mjs | 5 ++ .../src-tauri/src/main.rs | 40 +++++++-- apps/ai-game-creator-shell/src/App.tsx | 29 ++++++- .../tests/appSurface.test.ts | 84 +++++++++++++++++-- 4 files changed, 138 insertions(+), 20 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index 72bb741e2..c07c7688b 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -243,6 +243,9 @@ for (const snippet of [ '#[cfg(debug_assertions)]\nfn open_developer_window(app: &tauri::App)', 'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())', 'open_developer_window(app)?;', + 'fn append_local_permission_log_at(', + '"command.auto"', + 'GameCreationAppPermission::Auto', ]) { if (!tauriMainSource.includes(snippet)) { throw new Error( @@ -275,6 +278,8 @@ for (const snippet of [ "'permission.pending'", "'permission.confirm'", "'permission.cancel'", + "'command.auto'", + "'agent.run_status'", 'function summarizeAgentRunTrace', '工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}', 'agentRunTrace.error ?', diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index ffbeb5612..060b1ac9d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -27,7 +27,7 @@ use shared_contracts::game_creation_app::{ GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource, GameCreationAppAssetSourceKind, GameCreationAppCommandRunState, GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor, - GameCreationAppManifest, GameCreationAppPreviewState, GameCreationAppPreviewStatus, + GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState, GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus, GAME_CREATION_AGENT_CAPABILITIES, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX, GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS, @@ -5401,15 +5401,18 @@ fn append_local_permission_log_at( validate_project_root(root)?; if !matches!( event, - "permission.pending" | "permission.confirm" | "permission.cancel" + "permission.pending" | "permission.confirm" | "permission.cancel" | "command.auto" ) { - return Err("不支持的权限日志事件".to_string()); + return Err("不支持的命令日志事件".to_string()); } - if !GAME_CREATION_APP_COMMANDS + let Some(command) = GAME_CREATION_APP_COMMANDS .iter() - .any(|command| command.id == command_id) - { + .find(|command| command.id == command_id) + else { return Err("不支持的内置命令".to_string()); + }; + if event == "command.auto" && command.permission != GameCreationAppPermission::Auto { + return Err("自动命令日志只能记录 auto 权限命令".to_string()); } let log_path = root.join(".agent/logs/command.log"); @@ -8316,11 +8319,13 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true .expect("confirm log"); append_local_permission_log_at(&root, "permission.cancel", "memory.write") .expect("cancel log"); + append_local_permission_log_at(&root, "command.auto", "preview.status").expect("auto log"); let log = fs::read_to_string(root.join(".agent/logs/command.log")).expect("command log"); assert!(log.contains("permission.pending preview.start")); assert!(log.contains("permission.confirm preview.start")); assert!(log.contains("permission.cancel memory.write")); + assert!(log.contains("command.auto preview.status")); fs::remove_dir_all(root).ok(); } @@ -8330,14 +8335,19 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true let root = unique_project_path(); init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); - let event_error = append_local_permission_log_at(&root, "permission.grant", "preview.start") - .expect_err("unknown event should fail"); + let event_error = + append_local_permission_log_at(&root, "permission.grant", "preview.start") + .expect_err("unknown event should fail"); let command_error = append_local_permission_log_at(&root, "permission.pending", "shell.exec") .expect_err("unknown command should fail"); + let auto_permission_error = + append_local_permission_log_at(&root, "command.auto", "agent.retry") + .expect_err("confirm command should not be auto-logged"); - assert!(event_error.contains("不支持的权限日志事件")); + assert!(event_error.contains("不支持的命令日志事件")); assert!(command_error.contains("不支持的内置命令")); + assert!(auto_permission_error.contains("auto 权限命令")); fs::remove_dir_all(root).ok(); } @@ -8431,6 +8441,16 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true assert_eq!(retried.status, "pending"); assert_eq!(retried.lifecycle_status, "pending"); assert_eq!(retried.next_step, "runner-claim"); + let resumed = update_agent_run_lifecycle(&root, "resume", Some("继续修复输入监听")) + .expect("resume should mark pending"); + assert_eq!(resumed.status, "pending"); + assert_eq!(resumed.lifecycle_status, "pending"); + assert_eq!(resumed.next_step, "runner-claim"); + let trace: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap()) + .expect("run trace json after resume"); + assert_eq!(trace["stopReason"], "human-resume"); + assert_eq!(trace["error"], Value::Null); let status = update_agent_run_lifecycle(&root, "status", None).expect("status should read trace"); assert_eq!(status.status, "pending"); @@ -8438,9 +8458,11 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true let activity = fs::read_to_string(root.join(".agent/activity.jsonl")).expect("activity"); assert!(activity.contains("agent.kill")); assert!(activity.contains("agent.retry")); + assert!(activity.contains("agent.resume")); assert!(activity.contains("agent.run_status")); let output = fs::read_to_string(root.join(".agent/output.jsonl")).expect("output"); assert!(output.contains("agent.kill")); + assert!(output.contains("agent.resume")); assert!(root.join(".agent/context.bundle.json").exists()); fs::remove_dir_all(root).ok(); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index cbd1ca587..c28324019 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -10,6 +10,7 @@ import { GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS, type GameCreationAppAgentGroup, + type GameCreationAppCommandDescriptor, type GameCreationAgentRunTrace, type GameCreationAppManifest, type GameCreationAppPermission, @@ -1099,8 +1100,12 @@ export function App() { function appendLocalPermissionLog( projectPath: string | null, - event: 'permission.pending' | 'permission.confirm' | 'permission.cancel', - commandId: PendingCommand['id'], + event: + | 'permission.pending' + | 'permission.confirm' + | 'permission.cancel' + | 'command.auto', + commandId: GameCreationAppCommandDescriptor['id'], ) { const invoke = resolveTauriInvoke(); if (!invoke || !projectPath) { @@ -2211,13 +2216,24 @@ export function App() { setAgentRunStatus( `${result.status} · ${result.lifecycleStatus} · ${result.nextStep}`, ); + const commandId = + action === 'status' + ? 'agent.run_status' + : (`agent.${action}` as GameCreationAppCommandDescriptor['id']); setCommandLog((current) => [ ...current, - `agent.${action}`, + commandId, 'file.write .agent/activity.jsonl', 'file.write .agent/output.jsonl', 'file.write .agent/context.bundle.json', ]); + if (announceToChat && action === 'status') { + appendLocalPermissionLog( + nextProjectPath, + 'command.auto', + 'agent.run_status', + ); + } await refreshAgentRunTrace(nextProjectPath); if (announceToChat) { setMessages((current) => [ @@ -2501,6 +2517,13 @@ export function App() { setPreviewStatus('未启动'); } setCommandLog((current) => [...current, 'preview.status']); + if (announceToChat && nextProjectPath) { + appendLocalPermissionLog( + nextProjectPath, + 'command.auto', + 'preview.status', + ); + } if (announceToChat) { setMessages((current) => [ ...current, diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 69080b19a..1798d723d 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -1092,6 +1092,11 @@ describe('AI 游戏创作 App 界面边界', () => { expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', { projectPath: '/tmp/authorized-game', }); + expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { + projectPath: '/tmp/authorized-game', + event: 'command.auto', + commandId: 'preview.status', + }); }); it('runs static smoke and starts preview from chat through the authorized project path', async () => { @@ -2193,16 +2198,40 @@ describe('AI 游戏创作 App 界面边界', () => { }; } if (command === 'control_agent_run') { + const action = String(args?.action ?? ''); + const detail = String(args?.detail ?? ''); + const resultByAction = { + status: { + status: 'pending', + lifecycleStatus: 'pending', + nextStep: 'runner-claim', + message: 'run run-control-chat 当前状态:pending / pending', + }, + kill: { + status: 'killed', + lifecycleStatus: 'killed', + nextStep: 'resume-or-retry', + message: 'run run-control-chat 已标记为 killed', + }, + retry: { + status: 'pending', + lifecycleStatus: 'pending', + nextStep: 'runner-claim', + message: 'run run-control-chat 已重试,等待下一次 claim', + }, + resume: { + status: 'pending', + lifecycleStatus: 'pending', + nextStep: 'runner-claim', + message: `run run-control-chat 已恢复:${detail}`, + }, + }[action]; + if (!resultByAction) { + throw new Error(`unexpected agent run action ${action}`); + } return { runId: 'run-control-chat', - status: args?.action === 'kill' ? 'killed' : 'pending', - lifecycleStatus: args?.action === 'kill' ? 'killed' : 'pending', - nextStep: - args?.action === 'kill' ? 'resume-or-retry' : 'runner-claim', - message: - args?.action === 'kill' - ? 'run run-control-chat 已标记为 killed' - : 'run run-control-chat 当前状态:pending / pending', + ...resultByAction, activityPath: '/tmp/authorized-game/.agent/activity.jsonl', outputPath: '/tmp/authorized-game/.agent/output.jsonl', contextBundlePath: '/tmp/authorized-game/.agent/context.bundle.json', @@ -2236,6 +2265,11 @@ describe('AI 游戏创作 App 界面边界', () => { action: 'status', detail: undefined, }); + expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { + projectPath: '/tmp/authorized-game', + event: 'command.auto', + commandId: 'agent.run_status', + }); submitChat('/agent-kill'); expect(screen.getByText('agent.kill')).not.toBeNull(); @@ -2253,6 +2287,40 @@ describe('AI 游戏创作 App 界面边界', () => { action: 'kill', detail: undefined, }); + + submitChat('/agent-retry'); + expect(screen.getByText('agent.retry')).not.toBeNull(); + expect( + screen.getByText( + '标记 /tmp/authorized-game/.agent/run.latest.json 为 pending,等待 runner claim', + ), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText(/run run-control-chat 已重试,等待下一次 claim/), + ).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('control_agent_run', { + projectPath: '/tmp/authorized-game', + action: 'retry', + detail: undefined, + }); + + submitChat('/agent-resume 继续修复输入监听'); + expect(screen.getByText('agent.resume')).not.toBeNull(); + expect( + screen.getByText( + '附加用户说明并标记 /tmp/authorized-game/.agent/run.latest.json 为 pending', + ), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText(/run run-control-chat 已恢复:继续修复输入监听/), + ).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('control_agent_run', { + projectPath: '/tmp/authorized-game', + action: 'resume', + detail: '继续修复输入监听', + }); }); it('manages long memory from chat through the authorized local project path', async () => {