统一 Claude Code 与 Codex 错误分类
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m46s
Project CI / AI game creator shell Rust crates (push) Successful in 3m18s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 5m5s
Project CI / Frontend tests (push) Successful in 4m36s
Project CI / Backend tests (push) Failing after 5m40s
Project CI / Repository checks (push) Failing after 4m32s
Project CI / AI game creator shell web tests (push) Successful in 2m54s
Project CI / Native shell tests (push) Successful in 8m37s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m46s
Project CI / AI game creator shell Rust crates (push) Successful in 3m18s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 5m5s
Project CI / Frontend tests (push) Successful in 4m36s
Project CI / Backend tests (push) Failing after 5m40s
Project CI / Repository checks (push) Failing after 4m32s
Project CI / AI game creator shell web tests (push) Successful in 2m54s
Project CI / Native shell tests (push) Successful in 8m37s
保留 Claude Code 的上游 HTTP 状态、超时、空响应和无效 JSON 语义 补充状态摘要、重试建议、定向回归测试与排障记录
This commit is contained in:
@@ -43,9 +43,9 @@ pub mod tool;
|
||||
pub(crate) use art_manifest::*;
|
||||
use claude_code_cli::*;
|
||||
pub(crate) use claude_code_cli::{
|
||||
cancel_direct_claude_code_turn_at, direct_game_creator_claude_code_chat_at,
|
||||
direct_game_creator_claude_code_home_chat, game_creator_claude_code_cli_route_error,
|
||||
game_creator_claude_code_cli_version_identity,
|
||||
cancel_direct_claude_code_turn_at, claude_code_failure_to_llm_error,
|
||||
direct_game_creator_claude_code_chat_at, direct_game_creator_claude_code_home_chat,
|
||||
game_creator_claude_code_cli_route_error, game_creator_claude_code_cli_version_identity,
|
||||
};
|
||||
pub(crate) use codex_app_server::direct_game_creator_codex_chat_at;
|
||||
pub(crate) use codex_app_server::turn_error::*;
|
||||
|
||||
@@ -747,6 +747,46 @@ fn parse_usage(value: &serde_json::Value) -> Option<platform_llm::LlmTokenUsage>
|
||||
})
|
||||
}
|
||||
|
||||
/// 把 Claude Code/sidecar 的字符串失败投影到与 Codex app-server 相同的 LlmError 分类。
|
||||
///
|
||||
/// DirectProject 不能把上游 HTTP 错误统统包装成 Transport:那会把 429、401、5xx 等可识别
|
||||
/// 的上游事实显示成“执行通道已断开”。这里只认结构化的状态位置(`Request rejected (N)` /
|
||||
/// `HTTP N`),其它文本继续保留为 Transport,避免凭关键词猜测。
|
||||
pub(crate) fn claude_code_failure_to_llm_error(detail: String) -> platform_llm::LlmError {
|
||||
if let Some(status_code) = claude_code_failure_status_code(&detail) {
|
||||
return platform_llm::LlmError::Upstream {
|
||||
status_code,
|
||||
message: detail,
|
||||
};
|
||||
}
|
||||
if detail.contains("Claude Agent SDK sidecar 回合超时") {
|
||||
return platform_llm::LlmError::Timeout { attempts: 1 };
|
||||
}
|
||||
if detail.contains("Claude Code 缺少最终回复") {
|
||||
return platform_llm::LlmError::EmptyResponse;
|
||||
}
|
||||
if detail.contains("sidecar 输出不是有效 JSON") || detail.contains("stream-json 包含无效 JSON")
|
||||
{
|
||||
return platform_llm::LlmError::Deserialize(detail);
|
||||
}
|
||||
platform_llm::LlmError::Transport(detail)
|
||||
}
|
||||
|
||||
fn claude_code_failure_status_code(detail: &str) -> Option<u16> {
|
||||
["Request rejected (", "HTTP "].iter().find_map(|marker| {
|
||||
let tail = detail.split_once(marker)?.1;
|
||||
let digits = tail
|
||||
.chars()
|
||||
.take_while(|character| character.is_ascii_digit())
|
||||
.collect::<String>();
|
||||
if digits.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
let status_code = digits.parse::<u16>().ok()?;
|
||||
(100..=599).contains(&status_code).then_some(status_code)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_claude_code_result(
|
||||
stdout: &[u8],
|
||||
request: &LlmRunRequest,
|
||||
@@ -758,7 +798,7 @@ fn parse_claude_code_result(
|
||||
let detail = claude_result_error_detail(&value)
|
||||
.map(|detail| format!(":{detail}"))
|
||||
.unwrap_or_default();
|
||||
return Err(platform_llm::LlmError::Transport(format!(
|
||||
return Err(claude_code_failure_to_llm_error(format!(
|
||||
"Claude Code 返回失败终态{detail}"
|
||||
)));
|
||||
}
|
||||
@@ -1156,6 +1196,29 @@ mod tests {
|
||||
assert!(error.to_string().contains("Authentication failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_failure_reuses_codex_error_categories() {
|
||||
assert!(matches!(
|
||||
claude_code_failure_to_llm_error(
|
||||
"Claude Code 返回失败终态:API Error: Request rejected (429)".into()
|
||||
),
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 429,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(matches!(
|
||||
claude_code_failure_to_llm_error(
|
||||
"Claude Agent SDK sidecar 回合超时:连续 180000 ms 没有任何事件".into()
|
||||
),
|
||||
platform_llm::LlmError::Timeout { attempts: 1 }
|
||||
));
|
||||
assert!(matches!(
|
||||
claude_code_failure_to_llm_error("Claude Code 缺少最终回复".into()),
|
||||
platform_llm::LlmError::EmptyResponse
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_direct_stream_result_and_final_text() {
|
||||
let mut observed = Vec::new();
|
||||
|
||||
@@ -5300,7 +5300,9 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer(
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
.map_err(|detail| TurnError::from_model_call(&platform_llm::LlmError::Transport(detail)));
|
||||
.map_err(|detail| {
|
||||
TurnError::from_model_call(&crate::agent::claude_code_failure_to_llm_error(detail))
|
||||
});
|
||||
}
|
||||
game_creator_codex_app_server_validate_llm_config(&config.llm).map_err(|error| {
|
||||
TurnError::EnvironmentNotReady(EnvironmentNotReady {
|
||||
|
||||
@@ -304,7 +304,7 @@ impl ModelCallKind {
|
||||
native,
|
||||
} => match native {
|
||||
Some(native) => native.is_retryable(),
|
||||
None => *status_code >= 500,
|
||||
None => matches!(*status_code, 401 | 408 | 429 | 500..=599),
|
||||
},
|
||||
Self::PaidCreditsInsufficient => false,
|
||||
Self::EmptyResponse => false,
|
||||
@@ -322,9 +322,16 @@ impl ModelCallKind {
|
||||
}
|
||||
Self::EmptyResponse => Some("模型未返回内容"),
|
||||
Self::PayloadInvalid { .. } => Some("模型回执无法解析"),
|
||||
Self::RequestRejected { native } | Self::UpstreamFailed { native, .. } => {
|
||||
Self::RequestRejected { native } => {
|
||||
native.as_ref().and_then(NativeKind::public_summary)
|
||||
}
|
||||
Self::UpstreamFailed {
|
||||
status_code,
|
||||
native,
|
||||
} => native
|
||||
.as_ref()
|
||||
.and_then(NativeKind::public_summary)
|
||||
.or_else(|| upstream_status_summary(*status_code)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,13 +348,40 @@ impl ModelCallKind {
|
||||
Self::EmptyResponse | Self::PayloadInvalid { .. } => {
|
||||
Some("模型未给出可用的回执,请重试;如持续失败请检查项目诊断")
|
||||
}
|
||||
Self::RequestRejected { native } | Self::UpstreamFailed { native, .. } => {
|
||||
native.as_ref().and_then(NativeKind::recovery_hint)
|
||||
}
|
||||
Self::RequestRejected { native } => native.as_ref().and_then(NativeKind::recovery_hint),
|
||||
Self::UpstreamFailed {
|
||||
status_code,
|
||||
native,
|
||||
} => native
|
||||
.as_ref()
|
||||
.and_then(NativeKind::recovery_hint)
|
||||
.or_else(|| upstream_status_recovery_hint(*status_code)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn upstream_status_summary(status_code: u16) -> Option<&'static str> {
|
||||
Some(match status_code {
|
||||
401 => "上游服务拒绝认证(HTTP 401)",
|
||||
403 => "上游服务拒绝访问(HTTP 403)",
|
||||
408 => "上游请求超时(HTTP 408)",
|
||||
429 => "上游服务返回 HTTP 429",
|
||||
500..=599 => "上游服务暂时不可用",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn upstream_status_recovery_hint(status_code: u16) -> Option<&'static str> {
|
||||
Some(match status_code {
|
||||
401 => "请重新登录陶泥儿后重试;如持续失败请检查项目诊断",
|
||||
403 => "请检查当前账号的上游访问权限后重试;如持续失败请检查项目诊断",
|
||||
408 => "上游请求超时,请稍后重试;如持续失败请检查项目诊断",
|
||||
429 => "上游服务暂时繁忙(HTTP 429),请稍后重试;如持续失败请检查项目诊断",
|
||||
500..=599 => "上游服务暂时不可用,请稍后重试;如持续失败请检查项目诊断",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════════════════
|
||||
// 入队失败:`#[tauri::command]` 的 `Err`,这一轮没有开始也没有进队列
|
||||
// ══════════════════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1327,6 +1361,31 @@ mod tests {
|
||||
assert!(!projected.is_model_repairable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upstream_http_status_keeps_codex_style_summary_and_retry_guidance() {
|
||||
let rate_limited = TurnError::from_model_call(&LlmError::Upstream {
|
||||
status_code: 429,
|
||||
message: "Claude Code 返回失败终态:API Error: Request rejected (429)".into(),
|
||||
});
|
||||
assert_eq!(rate_limited.public_summary(), Some("上游服务返回 HTTP 429"));
|
||||
assert_eq!(
|
||||
rate_limited.recovery_hint(),
|
||||
Some("上游服务暂时繁忙(HTTP 429),请稍后重试;如持续失败请检查项目诊断")
|
||||
);
|
||||
assert!(rate_limited.is_retryable());
|
||||
assert!(!rate_limited.is_model_repairable());
|
||||
|
||||
let unauthorized = TurnError::from_model_call(&LlmError::Upstream {
|
||||
status_code: 401,
|
||||
message: "Claude Code 返回失败终态:HTTP 401".into(),
|
||||
});
|
||||
assert_eq!(
|
||||
unauthorized.public_summary(),
|
||||
Some("上游服务拒绝认证(HTTP 401)")
|
||||
);
|
||||
assert!(unauthorized.is_retryable());
|
||||
}
|
||||
|
||||
/// 反馈判据:原生分类里"再跑一次也不会变"的那些不再反馈给模型。
|
||||
#[test]
|
||||
fn terminal_native_kinds_are_not_fed_back_to_the_model() {
|
||||
|
||||
Reference in New Issue
Block a user