合并:同步主分支最新修改到游戏评分分支
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
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:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user