合并:同步主分支最新修改到游戏评分分支
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled

合并 origin/master 最新提交并保留评分展示和评价样式修复

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-10-01 11:10:32 +00:00
10 changed files with 332 additions and 13 deletions
@@ -212,7 +212,7 @@ async fn run_sidecar_turn(
.take()
.ok_or_else(|| "Claude Agent SDK sidecar stderr 未建立".to_string())?;
let stderr_task = tokio::spawn(read_capped(stderr, CLAUDE_CODE_OUTPUT_MAX_BYTES));
let result = tokio::time::timeout(Duration::from_millis(timeout_ms.max(1)), async {
let result = match tokio::time::timeout(Duration::from_millis(timeout_ms.max(1)), async {
let mut lines = BufReader::new(stdout).lines();
let mut events = Vec::new();
let mut terminal = None;
@@ -257,7 +257,21 @@ async fn run_sidecar_turn(
Ok(SidecarTurnResult { events, result })
})
.await
.map_err(|_| "Claude Agent SDK sidecar 回合超时".to_string())??;
{
Ok(result) => result?,
Err(_) => {
let detail = stderr_task
.await
.ok()
.and_then(Result::ok)
.and_then(|bytes| String::from_utf8(bytes).ok())
.map(|value| value.trim().chars().take(512).collect::<String>())
.filter(|value| !value.is_empty())
.map(|value| format!(":{value}"))
.unwrap_or_default();
return Err(format!("Claude Agent SDK sidecar 回合超时{detail}"));
}
};
let _ = stderr_task.await;
Ok(result)
}
@@ -318,6 +332,14 @@ pub(crate) fn game_creator_claude_code_cli_route_error(
if model.is_empty() || model.chars().count() > CLAUDE_CODE_MODEL_MAX_CHARS {
return Some("cc 模式需要有效的 Claude Code 模型标识".to_string());
}
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()
&& crate::platform_session::current_platform_session().is_none()
{
return Some("cc 模式需要先登录陶泥儿账号".to_string());
}
None
}
@@ -386,12 +408,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<String> {
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);
}
@@ -496,9 +550,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")
@@ -749,7 +806,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")
@@ -804,6 +864,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();
@@ -822,4 +892,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"));
}
}
@@ -32,7 +32,10 @@ export function homeWebPreflightErrorMessage(value: unknown): string {
function isHostPreflightError(value: unknown): boolean {
if (!(value instanceof Error)) return false;
return value.message === HOME_WEB_PREFLIGHT_FAILURE || Boolean(knownPreflightCode(value.message));
return (
value.message === HOME_WEB_PREFLIGHT_FAILURE ||
Boolean(knownPreflightCode(value.message))
);
}
export async function ensureHomeWebCreationEnvironment(
@@ -72,7 +72,9 @@ it('awaits the real home preflight before naming or creating a game', async () =
it('blocks naming, creation, and first generation with the host error code', async () => {
const { result, calls } = mount(async () => {
throw new Error('Web 游戏环境预检未通过:web-preflight-browser-failed;尚未启动生成');
throw new Error(
'Web 游戏环境预检未通过:web-preflight-browser-failed;尚未启动生成',
);
});
let status = '';
await act(async () => {
@@ -81,7 +83,9 @@ it('blocks naming, creation, and first generation with the host error code', asy
'direct-build',
);
});
expect(status).toBe('Web 游戏环境预检未通过(web-preflight-browser-failed);尚未开始生成。');
expect(status).toBe(
'Web 游戏环境预检未通过(web-preflight-browser-failed);尚未开始生成。',
);
expect(calls).toEqual(['preflight_web_game_creation']);
expect(result.current.currentProjectContext).toBeNull();
});
@@ -98,7 +102,10 @@ it('distinguishes an IPC failure from a host preflight failure', async () => {
);
});
expect(status).toBe(HOME_WEB_PREFLIGHT_IPC_FAILURE);
expect(calls).toEqual(['preflight_web_game_creation', 'preflight_web_game_creation']);
expect(calls).toEqual([
'preflight_web_game_creation',
'preflight_web_game_creation',
]);
expect(result.current.currentProjectContext).toBeNull();
});