diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs index a6ad51e1f..1b56483b5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs @@ -385,12 +385,44 @@ fn configure_claude_code_environment( { command.env("ANTHROPIC_AUTH_TOKEN", llm.api_key.trim()); } + // Official AGC model entries use the account session rather than a + // user-supplied API key. The Claude SDK only reads Anthropic auth + // from its child-process environment, so bridge the already-held + // platform token without persisting or exposing it in argv/logs. + if llm.api_key.trim().is_empty() + && !llm.custom_enabled + && std::env::var_os("ANTHROPIC_API_KEY").is_none() + && std::env::var_os("ANTHROPIC_AUTH_TOKEN").is_none() + { + if let Some(session) = crate::platform_session::current_platform_session() { + command.env("ANTHROPIC_AUTH_TOKEN", session.access_token); + } + } if !llm.model.trim().is_empty() { command.env("ANTHROPIC_MODEL", llm.model.trim()); } } } +fn claude_result_error_detail(value: &serde_json::Value) -> Option { + let candidates = [ + value.get("result").and_then(serde_json::Value::as_str), + value.get("error").and_then(serde_json::Value::as_str), + value.get("message").and_then(serde_json::Value::as_str), + value + .get("errors") + .and_then(serde_json::Value::as_array) + .and_then(|errors| errors.first()) + .and_then(serde_json::Value::as_str), + ]; + candidates + .into_iter() + .flatten() + .map(str::trim) + .find(|detail| !detail.is_empty()) + .map(|detail| detail.chars().take(512).collect()) +} + fn configure_claude_code_process(command: &mut tokio::process::Command) { super::codex_cli::configure_game_creator_codex_cli_process(command); } @@ -495,9 +527,12 @@ fn parse_claude_code_result( platform_llm::LlmError::Deserialize("Claude Code JSON 输出无效".to_string()) })?; if value.get("is_error").and_then(serde_json::Value::as_bool) == Some(true) { - return Err(platform_llm::LlmError::Transport( - "Claude Code 返回失败终态".to_string(), - )); + let detail = claude_result_error_detail(&value) + .map(|detail| format!(":{detail}")) + .unwrap_or_default(); + return Err(platform_llm::LlmError::Transport(format!( + "Claude Code 返回失败终态{detail}" + ))); } let response_id = value .get("session_id") @@ -829,7 +864,10 @@ fn parse_direct_stream_result( } } else if event_type == Some("result") { if event.get("is_error").and_then(serde_json::Value::as_bool) == Some(true) { - return Err("Claude Code 返回失败终态".to_string()); + let detail = claude_result_error_detail(&event) + .map(|detail| format!(":{detail}")) + .unwrap_or_default(); + return Err(format!("Claude Code 返回失败终态{detail}")); } result = event .get("result") @@ -884,6 +922,16 @@ mod tests { assert_eq!(response.tool_calls[0].name, "runtime_tool_file_read"); } + #[test] + fn preserves_claude_terminal_error_detail() { + let error = parse_claude_code_result( + br#"{"is_error":true,"result":"Authentication failed"}"#, + &request(false), + ) + .expect_err("failed terminal must be rejected"); + assert!(error.to_string().contains("Authentication failed")); + } + #[test] fn parses_direct_stream_result_and_final_text() { let mut observed = Vec::new(); @@ -903,4 +951,16 @@ mod tests { [DirectCodexTurnObservation::AgentMessageSegment { .. }] )); } + + #[test] + fn preserves_direct_stream_terminal_error_detail() { + let error = parse_direct_stream_result( + br#"{"type":"result","is_error":true,"result":"Authentication failed"} +"#, + "turn-1", + None, + ) + .expect_err("failed terminal must be rejected"); + assert!(error.contains("Authentication failed")); + } } diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 5c093965c..3528c71a9 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6158,3 +6158,9 @@ 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 官方 Claude 模型缺少账号令牌会被误报为通道断开 + +- **现象**:模型目录把回合路由到 `cc`,本地 `game-creator.config.json` 的 `llm.apiKey` 为空时,Claude Agent SDK 返回失败终态;界面只显示“执行通道未能建立或已断开”。 +- **根因**:Claude sidecar 只从 `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` 或本地 `llm.apiKey` 读取认证,没有复用已登录的 AGC 平台会话;同时失败终态解析丢弃了上游错误摘要。 +- **处理**:官方模型且未启用自定义目录时,将当前平台会话令牌仅注入 sidecar 子进程环境;保留最多 512 字符的 Claude 终态错误摘要,继续由统一诊断层脱敏,避免凭据落盘。