From c2ef8631f8c745e571609db817c8109cd3d585cf Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 3 Aug 2026 22:16:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=98=8E=E7=A1=AE=E6=B3=A5=E7=82=B9=E4=B8=8D?= =?UTF-8?q?=E8=B6=B3=E7=9A=84=E6=B8=B8=E6=88=8F=E7=94=9F=E6=88=90=E4=B8=AD?= =?UTF-8?q?=E6=96=AD=E5=8E=9F=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一余额不足分类与稳定原因码 阻止泥点不足进入重试和对账状态 在游戏聊天与阶段记录展示固定安全说明 补齐 API、Runtime 与前端回归测试 同步技术方案与项目决策记录 --- .../src-tauri/src/agent/generation.rs | 9 ++- .../agent/generation/loop_orchestration.rs | 25 +++++++ .../runtime_driver/game_chat_fast_path.rs | 74 +++++++++++++++---- .../agent/runtime_protocol/provider_retry.rs | 61 +++++++++++++++ .../src/agent/runtime_tools/media.rs | 31 +++++++- .../src/features/agent-runtime/model.ts | 15 ++++ .../SupervisorChatOnlyView.tsx | 57 ++++++++++++-- .../tests/agentRuntimeModel.test.ts | 31 ++++++++ .../appSurface/project-development.suite.ts | 52 ++++++++++++- .../shared-memory/decision-log.md | 6 ++ ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 1 + .../crates/api-server/src/asset_billing.rs | 39 ++++++---- 12 files changed, 358 insertions(+), 43 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs index 68665ace3..e896921fb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs @@ -55,9 +55,12 @@ pub(crate) use draft_validation::{ pub(crate) use draft_writer::write_local_game_draft_at; #[allow(unused_imports)] pub(crate) use loop_orchestration::{ - emit_agent_progress, game_creator_agent_llm_error_public_summary, - request_game_creator_llm_text, request_generator_game_draft_with_client, - request_planner_spec_with_client, run_game_creator_agent_loop_at, AgentProgressEmitter, + emit_agent_progress, game_creator_agent_llm_error_is_mud_points_insufficient, + game_creator_agent_llm_error_public_summary, game_creator_mud_points_insufficient_message, + game_creator_runtime_error_is_mud_points_insufficient, request_game_creator_llm_text, + request_generator_game_draft_with_client, request_planner_spec_with_client, + run_game_creator_agent_loop_at, AgentProgressEmitter, + GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND, }; #[allow(unused_imports)] pub(crate) use pass_artifacts::{ diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs index bcd4cc566..dd84942a3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs @@ -511,9 +511,34 @@ pub(in crate::agent) fn build_game_creator_agent_runtime_llm_client( build_game_creator_llm_client_without_redirects_from_llm_config(&single_attempt, config_path) } +pub(crate) const GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND: &str = + "kind=mud-points-insufficient"; + +pub(crate) fn game_creator_mud_points_insufficient_message(message: &str) -> bool { + let message = message.trim(); + message == "泥点余额不足" || message.starts_with("可消费泥点不足:") +} + +pub(crate) fn game_creator_runtime_error_is_mud_points_insufficient(error: &str) -> bool { + error.contains("泥点余额不足") || error.contains("可消费泥点不足:") +} + +pub(crate) fn game_creator_agent_llm_error_is_mud_points_insufficient( + error: &platform_llm::LlmError, +) -> bool { + matches!( + error, + platform_llm::LlmError::Upstream { message, .. } + if game_creator_mud_points_insufficient_message(message) + ) +} + pub(crate) fn game_creator_agent_llm_error_public_summary( error: &platform_llm::LlmError, ) -> String { + if game_creator_agent_llm_error_is_mud_points_insufficient(error) { + return GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND.to_string(); + } let (kind, http_status) = match error { platform_llm::LlmError::Timeout { .. } => ("timeout".to_string(), None), platform_llm::LlmError::Connectivity { .. } => ("connectivity".to_string(), None), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs index 00aa1f684..0625571fe 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs @@ -181,16 +181,32 @@ fn game_chat_fast_path_has_visual_asset(root: &Path, task_id: &str) -> bool { }) } -fn game_chat_fast_path_canvas_generation_failed(runtime: &AgentRuntimeState) -> bool { - runtime.observations.iter().rev().any(|observation| { - [ +fn game_chat_fast_path_canvas_generation_error( + runtime: &AgentRuntimeState, + default_error: &str, +) -> Option { + runtime.observations.iter().rev().find_map(|observation| { + let failed = [ "canvas.asset_generate:failed", "canvas.asset_generate:blocked", "canvas.asset_generate:rejected", "canvas.asset_generate:needs-reconciliation", ] .iter() - .any(|prefix| observation.starts_with(prefix)) + .any(|prefix| observation.starts_with(prefix)); + if !failed { + return None; + } + Some( + if observation.contains(GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND) + || game_creator_runtime_error_is_mud_points_insufficient(observation) + { + GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND + } else { + default_error + } + .to_string(), + ) }) } @@ -479,11 +495,11 @@ pub(crate) fn game_chat_fast_path_plan_at( .to_string(), ); } - if game_chat_fast_path_canvas_generation_failed(runtime) { - return Err( - "game-chat 统一视觉规范图生成失败,拒绝跳过美术阶段或退回纯几何首版" - .to_string(), - ); + if let Some(error) = game_chat_fast_path_canvas_generation_error( + runtime, + "game-chat 统一视觉规范图生成失败,拒绝跳过美术阶段或退回纯几何首版", + ) { + return Err(error); } let root_task = game_chat_fast_path_root_task(root, &budget)?; Ok(Some(game_chat_fast_path_canvas_asset_plan( @@ -516,10 +532,11 @@ pub(crate) fn game_chat_fast_path_plan_at( .to_string(), ); } - if game_chat_fast_path_canvas_generation_failed(runtime) { - return Err( - "game-chat 透明核心美术图集生成失败,拒绝退回纯代码核心画面".to_string() - ); + if let Some(error) = game_chat_fast_path_canvas_generation_error( + runtime, + "game-chat 透明核心美术图集生成失败,拒绝退回纯代码核心画面", + ) { + return Err(error); } let root_task = game_chat_fast_path_root_task(root, &budget)?; Ok(Some(game_chat_fast_path_canvas_asset_plan( @@ -1373,6 +1390,37 @@ mod tests { assert!(!prompt.contains("可直接作为首版主要背景")); } + #[test] + fn canvas_generation_mud_point_failure_keeps_the_stable_reason() { + let temporary = tempfile::tempdir().expect("temporary project"); + let root = temporary.path().join("project"); + init_local_game_project_at(&root, "game-chat-mud-points", "mud points") + .expect("initialize project"); + let mut runtime = start_game_creator_agent_runtime_task_at( + &root, + "art-asset-plan", + "生成透明核心美术图集", + "mud-points-art-run", + "agent-ready-task-scheduler", + "生成透明核心美术图集", + Vec::new(), + ) + .expect("start art runtime"); + runtime.observations.push( + "canvas.asset_generate:failed · 平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2;operationId=private-operation-id" + .to_string(), + ); + + let error = game_chat_fast_path_canvas_generation_error( + &runtime, + "game-chat 透明核心美术图集生成失败,拒绝退回纯代码核心画面", + ) + .expect("mud point failure must stop the fast path"); + assert_eq!(error, GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND); + assert!(!error.contains("operationId")); + assert!(!error.contains("private-operation-id")); + } + #[test] fn fallback_html_uses_spritesheet_but_never_art_spec() { let temporary = tempfile::tempdir().expect("temporary project"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs index a5290c142..74b0ce6f6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs @@ -27,6 +27,9 @@ pub(in crate::agent) fn game_creator_agent_runtime_transient_provider_error_kind error: &platform_llm::LlmError, retry_autonomous_upstream_400: bool, ) -> Option<&'static str> { + if game_creator_agent_llm_error_is_mud_points_insufficient(error) { + return None; + } match error { platform_llm::LlmError::Timeout { .. } => Some("timeout"), platform_llm::LlmError::Connectivity { .. } => Some("connectivity"), @@ -181,6 +184,12 @@ pub(in crate::agent) fn game_creator_agent_runtime_failure_conversation_message( agent_id: &str, error: &str, ) -> String { + if error.contains(GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND) + || game_creator_runtime_error_is_mud_points_insufficient(error) + { + return "泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。" + .to_string(); + } let subject = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { "项目总控 Agent" } else { @@ -1559,6 +1568,58 @@ mod tests { } } + #[test] + fn mud_point_insufficiency_interrupts_without_retry_and_uses_a_fixed_public_message() { + for status_code in [409, 502] { + let error = platform_llm::LlmError::Upstream { + status_code, + message: "泥点余额不足".to_string(), + }; + assert_eq!( + game_creator_agent_runtime_transient_provider_error_kind(&error, false), + None, + "泥点余额不足不是可通过自动重试恢复的上游瞬态错误" + ); + assert_eq!( + game_creator_agent_llm_error_public_summary(&error), + GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND + ); + let encoded = game_creator_agent_runtime_provider_error_with_transient_kind( + &error, + "agentLlm.project-supervisor", + "规划", + false, + ); + assert!(!encoded.starts_with(AGENT_RUNTIME_PROVIDER_TRANSIENT_ERROR_PREFIX)); + assert!(encoded.ends_with(GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND)); + assert_eq!( + game_creator_agent_runtime_failure_conversation_message( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &encoded, + ), + "泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。" + ); + } + + let provider_quota = platform_llm::LlmError::Upstream { + status_code: 429, + message: "insufficient_quota".to_string(), + }; + assert_eq!( + game_creator_agent_runtime_transient_provider_error_kind(&provider_quota, false), + Some("upstream-429"), + "第三方 Provider quota 不能误分类为平台泥点不足" + ); + + assert_eq!( + game_creator_agent_runtime_failure_conversation_message( + "art-asset-plan", + "canvas.asset_generate:failed · 平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2;operationId=private-operation-id", + ), + "泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。" + ); + } + #[test] fn retryable_upstream_503_keeps_generic_classification_and_specific_safe_code() { let secret = ["sk", "provider-body-secret"].join("-"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/media.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/media.rs index 1f93515df..740d1c309 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/media.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/media.rs @@ -926,7 +926,9 @@ fn platform_art_generation_observation_status( run_id: &str, error: &str, ) -> &'static str { - if platform_art_generation_error_needs_reconciliation(error) + if game_creator_runtime_error_is_mud_points_insufficient(error) { + "failed" + } else if platform_art_generation_error_needs_reconciliation(error) || game_creator_agent_runtime_external_generation_exists(root, agent_id, run_id) { AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION @@ -990,4 +992,31 @@ mod platform_art_generation_observation_tests { "failed" ); } + + #[test] + fn mud_point_insufficiency_is_a_definitive_failure_with_a_durable_generation_ledger() { + let root = tempfile::tempdir().expect("create mud point observation status root"); + let ledger_directory = root + .path() + .join(".agent/runtime/canvas-generation-requests/art-director"); + fs::create_dir_all(&ledger_directory).expect("create durable generation ledger directory"); + fs::write(ledger_directory.join("run-accepted.json"), b"accepted") + .expect("write durable generation ledger"); + + for error in [ + "平台图片生成任务失败:泥点余额不足;operationId=private-operation-id", + "平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2", + ] { + assert_eq!( + platform_art_generation_observation_status( + root.path(), + "art-director", + "run-accepted", + error, + ), + "failed", + "明确的泥点不足不能因 durable ledger 存在而误入 reconciliation" + ); + } + } } diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts index b0ef9627a..5936c6b22 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts @@ -1496,6 +1496,18 @@ export function projectRuntimeVisibleCurrentWork(runtime: AgentRuntimeState) { return '正在执行当前任务'; } +export const MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE = + '泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。'; + +export function isMudPointInsufficientRuntimeError(message: string) { + const normalized = message.trim().toLowerCase(); + return ( + message.includes('泥点余额不足') || + message.includes('可消费泥点不足:') || + normalized.includes('kind=mud-points-insufficient') + ); +} + export function projectRuntimeVisibleError( message: string, subject: string, @@ -1506,6 +1518,9 @@ export function projectRuntimeVisibleError( if (isRuntimeConfigMissingError(message)) { return '运行时配置未完成,请先打开配置'; } + if (isMudPointInsufficientRuntimeError(message)) { + return MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE; + } const exhaustedUpstreamRetry = visibleMessage.match( /(?:^|[\s::])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/, ); diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx index 45596a497..9bf9d541d 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx @@ -23,7 +23,9 @@ import type { } from '../../app/types'; import { formatAgentRuntimeEvent, + isMudPointInsufficientRuntimeError, isAgentRuntimeTerminalState, + MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE, projectNameFromPath, projectProfessionalAgentLabel, projectRuntimePlanProgress, @@ -89,6 +91,7 @@ export type GameChatSupervisorProgress = { currentWork: string; activeAgents: string[]; evidence: GameChatProgressEvidence[]; + interruptionText: string | null; }; export type GameChatResultImage = { @@ -185,7 +188,7 @@ export function formatGameChatStageRecord( ) { const terminalStatus = runtime.status === 'failed' || runtime.phase === 'failed' - ? '本轮失败' + ? progress.interruptionText || '本轮失败' : runtime.status === 'cancelled' || runtime.phase === 'cancelled' ? '本轮已取消' : runtime.status === 'completed' || runtime.phase === 'completed' @@ -213,6 +216,34 @@ export function formatGameChatStageRecord( return lines.join('\n'); } +export function gameChatMudPointInterruptionText( + runtime: AgentRuntimeState, + runtimeByAgentId: Record, +) { + const childRuntimes = Object.values(runtimeByAgentId).filter( + (childRuntime): childRuntime is AgentRuntimeState => + childRuntime !== undefined && + childRuntime.parentAgentId === runtime.agentId && + childRuntime.parentRunId === runtime.runId && + [ + 'agent-delegate', + 'agent-delegate-retry', + 'agent-ready-task-scheduler', + ].includes(childRuntime.source), + ); + const relatedRuntimes = [ + runtime, + ...childRuntimes, + ]; + return relatedRuntimes.some( + (relatedRuntime) => + relatedRuntime.error && + isMudPointInsufficientRuntimeError(relatedRuntime.error), + ) + ? MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE + : null; +} + export function isGameChatStageRecordMessage(text: string) { return text.startsWith(GAME_CHAT_STAGE_RECORD_PREFIX); } @@ -504,6 +535,10 @@ export function buildGameChatProgressEvidence( ), activeAgents, evidence, + interruptionText: gameChatMudPointInterruptionText( + runtime, + runtimeByAgentId, + ), }; } @@ -817,13 +852,19 @@ export function SupervisorChatOnlyView({ synchronizingAcceptedRun || (runtime && !isAgentRuntimeTerminalState(runtime)), ); - const status = runtimeError - ? projectRuntimeVisibleError(runtimeError, '项目总控 Agent', true) - : synchronizingAcceptedRun - ? '已投递,正在同步 Agent Runner' - : runtime - ? projectSupervisorChatRuntimeStatus(runtime) - : workspaceStatus; + const gameChatInterruptionText = + gameChatMode && runtime + ? gameChatMudPointInterruptionText(runtime, runtimeByAgentId) + : null; + const status = + gameChatInterruptionText || + (runtimeError + ? projectRuntimeVisibleError(runtimeError, '项目总控 Agent', true) + : synchronizingAcceptedRun + ? '已投递,正在同步 Agent Runner' + : runtime + ? projectSupervisorChatRuntimeStatus(runtime) + : workspaceStatus); const headerStatus = gameChatMode && previewStatus === '未启动' ? '预览未启动' diff --git a/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts b/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts index 746c6c2a5..89865c8e5 100644 --- a/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts +++ b/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts @@ -8,6 +8,7 @@ import type { } from '../src/app/types'; import { formatAgentRuntimeEvent, + MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE, mergeGameChatRuntimeResponseMessagesIntoHistory, projectRuntimeVisibleCurrentWork, projectRuntimeVisibleError, @@ -292,6 +293,36 @@ describe('Agent Runtime Provider 状态投影', () => { ); }); + test('明确说明泥点余额不足导致的中断并隐藏附加诊断', () => { + for (const message of [ + 'agentLlm.project-supervisor 规划调用 LLM 失败:kind=mud-points-insufficient', + '平台图片生成任务失败:泥点余额不足;operationId=private-operation-id', + ]) { + expect(projectRuntimeVisibleError(message, '项目总控 Agent', true)).toBe( + MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE, + ); + expect( + projectSupervisorVisibleConversationText(`后台任务失败:${message}`), + ).toBe(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE); + } + + const failedRuntime = { + ...providerRetryRuntime(), + status: 'failed', + phase: 'failed', + error: + '平台图片生成任务失败:泥点余额不足;operationId=private-operation-id', + }; + const status = projectSupervisorChatRuntimeStatus(failedRuntime); + expect(status).toBe(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE); + expect(status).not.toContain('operationId'); + expect(status).not.toContain('private-operation-id'); + + expect( + projectRuntimeVisibleError('上游余额不足', '项目总控 Agent', true), + ).toBe('项目总控 Agent 执行失败,请稍后重试'); + }); + test('即使前缀含恶意上游正文也只展示严格字段派生的摘要', () => { const fingerprint = 'b'.repeat(64); const malicious = 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 059d4472c..5d610146f 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 @@ -10,6 +10,7 @@ import type { AgentRuntimeEventRecord, AgentRuntimeState, } from '../../src/app/types'; +import { MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE } from '../../src/features/agent-runtime/model'; import { buildGameChatProgressEvidence, collectGameChatResultImages, @@ -2684,7 +2685,7 @@ export function registerProjectSupervisorSurfaceTests() { taskId: 'design-foundation', sessionId: 'design-session', runId: 'design-run', - source: 'agent-delegate', + source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: 'active-parent-run', updatedAt: 30, @@ -3574,6 +3575,52 @@ export function registerProjectSupervisorSurfaceTests() { expect(statusCard.textContent).toContain('生成 Agent 工具计划(本轮)'); }); + it('shows and archives the explicit mud point interruption from a failed art child runtime', () => { + const rootRunId = 'game-chat-mud-point-root-run'; + const runtime = gameChatRuntimeState({ + runId: rootRunId, + status: 'failed', + phase: 'failed', + error: '专业 Agent 执行失败', + updatedAt: 9100, + }); + const artRuntime = gameChatRuntimeState({ + agentId: 'art-asset-plan', + taskId: 'art-asset-plan', + sessionId: 'game-chat-mud-point-art-session', + runId: 'game-chat-mud-point-art-run', + source: 'agent-delegate', + parentAgentId: 'project-supervisor', + parentRunId: rootRunId, + status: 'failed', + phase: 'failed', + error: + '平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2;operationId=private-operation-id', + updatedAt: 9000, + }); + const runtimeByAgentId = { 'art-asset-plan': artRuntime }; + + renderGameChatStatus({ runtime, runtimeByAgentId }); + + const status = screen.getByLabelText('最新状态').textContent ?? ''; + expect(status).toContain(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE); + expect(status).not.toContain('operationId'); + expect(status).not.toContain('private-operation-id'); + + const progress = buildGameChatProgressEvidence( + runtime, + runtimeByAgentId, + null, + ); + if (!progress) { + throw new Error('missing failed game-chat progress fixture'); + } + const stageRecord = formatGameChatStageRecord(runtime, progress, []); + expect(stageRecord).toContain(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE); + expect(stageRecord).not.toContain('operationId'); + expect(stageRecord).not.toContain('private-operation-id'); + }); + it('counts only the seven first-playable tasks in game-chat progress', () => { const manifest = createGameCreationAppManifest( 'game-chat-progress-total', @@ -3622,6 +3669,7 @@ export function registerProjectSupervisorSurfaceTests() { }); const initialSupervisorOverrides = { runId: supervisorRunId, + source: 'project-supervisor-game-chat', runProfile: 'autonomous-game-build' as const, status: 'running', phase: 'execution', @@ -3652,7 +3700,7 @@ export function registerProjectSupervisorSurfaceTests() { taskId: 'code-prototype', sessionId: 'code-prototype-progress-session', runId: 'code-prototype-progress-run', - source: 'agent-delegate', + source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: supervisorRunId, delegationId: 'code-prototype-progress-delegation', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c185afd03..3cf731fa0 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5939,6 +5939,12 @@ - 边界:Supervisor 的角色选择、并行委派、all-join、视觉返工、claim gate 和 repair 自然语言合同进入 Bundle;Bundle 不是可执行 graph,也不是生产 Skill,正式 DAG、权限、安全门和完成合同继续由 Rust 与校验后的项目协作策略掌控。 - 一致性:原生工具目录从 `agent_runtime_native_executable_tools()` 生成,`mcp.call` 不混入静态原生目录;MCP 工具只从当前请求的动态 catalog 暴露。manifest 版本、section 覆盖、组合顺序和既有 Prompt 合同由测试锁定。 +## 2026-08-03 AI 游戏生成泥点不足使用确定性中断说明 + +- 决策:钱包返回 `泥点余额不足` 或 `可消费泥点不足:...` 时,API 统一按 HTTP 409 业务冲突处理并只公开固定的“泥点余额不足”;Agent Runtime 转换为稳定原因 `mud-points-insufficient`,禁止进入瞬态 Provider 自动重试。 +- 恢复边界:泥点不足表示平台已经明确拒绝计费与生成,即使本地保留 accepted External Generation ledger,也必须标为 `failed`,不能因账本存在而进入 `needs-reconciliation`。充值后的新输入“继续”仍走现有失败 successor 合同,从原任务和当前已提交项目事实接着完成,不重放旧生成请求。 +- 展示与安全:game-chat 顶部状态、持久失败对话和 Supervisor 阶段记录统一显示“泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。”;公开层不得附带 operationId、URL、项目路径、密钥或上游响应正文。第三方 `insufficient_quota`、普通 409、402 与 429 不得误分类为平台泥点不足。 + ## 2026-07-31 External v1 生成统一异步并提供托管 MCP 与完整 Skill 包 - 异步契约:External v1 的图片生成、图片编辑、图标图集、UI 素材提取、角色动画、视频、音效和背景音乐八类 POST 固定持久化入 `external_generation_job` 并返回 HTTP `202 + operationId/statusUrl/pollAfterMs`;不受站内 `GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 影响。每次逻辑生成必须携带稳定 `Idempotency-Key`,网络结果未知或调用方轮询超时时复用原键和原 operationId,不得换键重提。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 856dccd03..5d0f74c55 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -827,6 +827,7 @@ game-project/ - 2026-08-03 失败续跑收口:同一 `project-supervisor` Session、同一持久 source 的最近可信根 run 已失败、取消或预算耗尽,且新输入只是严格受限的继续意图(例如“继续”“接着做”“继续完成”“continue”“go on”)时,宿主仍创建新的 root run 身份,但必须把上一根 run 的原始任务作为继承目标和完成合同基线;首次和连续 successor 的 effective task、合同 SHA、Runtime hydration 与 scheduler 必须一致。不得把继续短语本身当游戏主题,也不得按真正新需求重置 seed manifest。跨 Session、跨 GUI / CLI / game-chat source、上一根 run 已正常完成、输入包含新的具体玩法要求或无法唯一识别前序根 run 时都不继承,继续按新任务执行。继承只复用目标与已有产物基线,不复用旧 Provider request、pending action 或副作用身份。 - game-chat 快车道只能在 `game/index.html` 缺失或仍是初始化占位,且当前 child run 尚未写入正式入口时使用首次 fallback `file.write`。项目已存在非占位入口时,后续 `code-prototype` 必须先保留并读取既有玩法,做真实局部修改并取得本人 `mutationRevision`,之后才能运行 `game.static_smoke` 与交付;禁止为了满足首版时限重新生成整份默认小游戏,也禁止连续只读 smoke。占位 fallback 仅允许俄罗斯方块和明确收集类等已有真实语义模板,未知玩法失败关闭。纯继续意图未能恢复唯一原始目标时同样失败关闭,不输出以“继续”为标题的兜底产物。 - `assets/art-spec.png` 的唯一语义是视觉规范与派生参考,不是运行时背景、角色、目标或图集。game-chat 的核心玩家、方块/目标、障碍/场景和反馈必须来自独立派生的透明 `assets/art-spritesheet.png` 及其服务端 `iconImageSrcs` 本地切片;Runtime 以 `sourceResourceId` 把切片清单绑定到当前图集,并要求活动 Canvas 分别绘制四类不同切片。纯代码核心实体、猜测图集等分坐标、单个裁切冒充全部类别、整图展示、隐藏引用、微小水印和诱饵路径均不构成真实美术使用。`playable-web-game-state.v1.sequence` 只在真实输入、状态迁移或模拟状态变化时递增,不得由纯渲染帧推进。 +- 泥点不足是确定性业务中断,不是瞬态 Provider 故障或未知副作用。钱包的 `泥点余额不足` 与 `可消费泥点不足:...` 两种领域文案统一映射为稳定原因 `mud-points-insufficient`,不得自动重试;即使 External Generation durable ledger 已存在,也必须落为 `failed`,不能误入 `needs-reconciliation`。game-chat 顶部状态、持久失败对话与 `【Supervisor 阶段记录】` 统一显示“泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。”,并禁止透传 operationId、URL、路径、密钥或任意上游正文。 - tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization:`file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。 ## 2026-07-31 长耗时与恢复收口 diff --git a/server-rs/crates/api-server/src/asset_billing.rs b/server-rs/crates/api-server/src/asset_billing.rs index 73c00e8aa..d74034a01 100644 --- a/server-rs/crates/api-server/src/asset_billing.rs +++ b/server-rs/crates/api-server/src/asset_billing.rs @@ -543,18 +543,20 @@ pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> A ); let is_insufficient_balance = matches!( &error, - SpacetimeClientError::Procedure(message) if message.contains("泥点余额不足") + SpacetimeClientError::Procedure(message) + if message.contains("泥点余额不足") || message.contains("可消费泥点不足:") ); let status = if is_insufficient_balance { StatusCode::CONFLICT } else { StatusCode::BAD_GATEWAY }; - let public_message = is_insufficient_balance.then(|| message.clone()); + let public_message = is_insufficient_balance.then_some("泥点余额不足"); + let public_detail_message = public_message.unwrap_or(message.as_str()); let app_error = AppError::from_status(status).with_details(json!({ "provider": "profile-wallet", - "message": message, + "message": public_detail_message, })); if let Some(public_message) = public_message { @@ -740,20 +742,25 @@ mod tests { #[test] fn asset_operation_wallet_insufficient_balance_is_public_message() { - let error = map_asset_operation_wallet_error(SpacetimeClientError::Procedure( - "泥点余额不足".to_string(), - )); + for domain_message in [ + "泥点余额不足", + "可消费泥点不足:需要 10,扣除退款占用后可用 2", + ] { + let error = map_asset_operation_wallet_error(SpacetimeClientError::Procedure( + domain_message.to_string(), + )); - assert_eq!(error.status_code(), StatusCode::CONFLICT); - assert_eq!(error.code(), "CONFLICT"); - assert_eq!(error.message(), "泥点余额不足"); - assert_eq!( - error - .details() - .and_then(|details| details.get("message")) - .and_then(serde_json::Value::as_str), - Some("泥点余额不足"), - ); + assert_eq!(error.status_code(), StatusCode::CONFLICT); + assert_eq!(error.code(), "CONFLICT"); + assert_eq!(error.message(), "泥点余额不足"); + assert_eq!( + error + .details() + .and_then(|details| details.get("message")) + .and_then(serde_json::Value::as_str), + Some("泥点余额不足"), + ); + } } #[test]