diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index 7b5281964..6b6f32d30 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -267,6 +267,7 @@ fn game_creator_codex_app_server_connection_error( ) -> platform_llm::LlmError { match game_creator_codex_app_server_error_http_status(info, field) { Some(401 | 403) => game_creator_codex_app_server_error_kind("unauthorized"), + Some(413) => game_creator_codex_app_server_error_kind("request-too-large"), Some(status_code) => platform_llm::LlmError::Upstream { status_code, message: "Codex app-server 连接上游失败".to_string(), @@ -304,6 +305,30 @@ fn game_creator_codex_app_server_error_detail_indicates_auth_failure( || detail.contains("http 403") } +fn game_creator_codex_app_server_error_detail_indicates_request_too_large( + error: &serde_json::Value, +) -> bool { + let Some(error) = error.as_object() else { + return false; + }; + let detail = ["message", "additionalDetails", "code"] + .into_iter() + .filter_map(|field| error.get(field).and_then(serde_json::Value::as_str)) + .collect::>() + .join(" ") + .to_ascii_lowercase(); + if detail.is_empty() { + return false; + } + detail.contains("413 payload too large") + || detail.contains("http 413") + || detail.contains("status 413") + || detail.contains("payload_too_large") + || detail.contains("payload too large") + || detail.contains("request too large") + || detail.contains("provider request too large") +} + fn game_creator_codex_app_server_error_detail_indicates_insufficient_mud_points( error: &serde_json::Value, ) -> bool { @@ -334,6 +359,9 @@ fn game_creator_codex_app_server_failed_turn_error( message: "泥点余额不足".to_string(), }; } + if game_creator_codex_app_server_error_detail_indicates_request_too_large(error) { + return game_creator_codex_app_server_error_kind("request-too-large"); + } if game_creator_codex_app_server_error_detail_indicates_auth_failure(error) { return game_creator_codex_app_server_error_kind("unauthorized"); } @@ -4691,6 +4719,12 @@ mod tests { "codex-app-server-error:unauthorized".to_string(), ), ), + ( + serde_json::json!({"httpConnectionFailed":{"httpStatusCode":413}}), + platform_llm::LlmError::InvalidRequest( + "codex-app-server-error:request-too-large".to_string(), + ), + ), ( serde_json::json!({"httpConnectionFailed":{"httpStatusCode":429}}), platform_llm::LlmError::Upstream { @@ -4751,6 +4785,31 @@ mod tests { } } + #[test] + fn codex_app_server_failed_turn_maps_request_too_large_details() { + for detail in [ + "HTTP 413 Payload Too Large", + "status 413", + "PAYLOAD_TOO_LARGE", + "provider request too large", + ] { + let error = game_creator_codex_app_server_failed_turn_error(&serde_json::json!({ + "status": "failed", + "error": { + "message": detail, + "additionalDetails": "private upstream diagnostics", + "codexErrorInfo": "other" + } + })); + assert_eq!( + error, + platform_llm::LlmError::InvalidRequest( + "codex-app-server-error:request-too-large".to_string(), + ) + ); + } + } + #[test] fn codex_app_server_failed_turn_maps_insufficient_mud_points_to_stable_upstream_error() { for detail in [ diff --git a/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx b/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx index d22ea9902..216f04b61 100644 --- a/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx +++ b/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx @@ -4,6 +4,7 @@ import { ackClientErrorEventsWithRetry, type ClientErrorEvent, type DiagnosticLogFile, + getPendingClientErrorEvents, getStableErrorReportSubmissionId, readApplicationDiagnosticLogs, submitErrorReportBatch, @@ -12,10 +13,12 @@ import { ThemedModal } from '../modal/ThemedModal'; type ErrorReportDialogProps = { open: boolean; - events: ClientErrorEvent[]; onClose: () => void; + fallbackEvents?: ClientErrorEvent[]; }; +const emptyFallbackEvents: ClientErrorEvent[] = []; + function diagnosticLogLabel( logsReady: boolean, logsError: boolean, @@ -28,9 +31,12 @@ function diagnosticLogLabel( export function ErrorReportDialog({ open, - events, onClose, + fallbackEvents = emptyFallbackEvents, }: ErrorReportDialogProps) { + const [events, setEvents] = useState([]); + const [eventsReady, setEventsReady] = useState(false); + const [eventsError, setEventsError] = useState(false); const [selectedFingerprints, setSelectedFingerprints] = useState>( new Set(), ); @@ -45,29 +51,59 @@ export function ErrorReportDialog({ useEffect(() => { if (!open) return; - setSelectedFingerprints( - new Set(events.slice(0, 1).map((event) => event.fingerprint)), - ); + let disposed = false; + setEvents([]); + setEventsReady(false); + setEventsError(false); + setSelectedFingerprints(new Set()); setStatus(''); setLogsReady(false); setLogsError(false); + void getPendingClientErrorEvents() + .then((nextEvents) => { + if (disposed) return; + setEvents(nextEvents); + setSelectedFingerprints( + new Set(nextEvents.map((event) => event.fingerprint)), + ); + setEventsReady(true); + }) + .catch(() => { + if (disposed) return; + if (fallbackEvents.length) { + setEvents(fallbackEvents); + setSelectedFingerprints( + new Set(fallbackEvents.map((event) => event.fingerprint)), + ); + setEventsReady(true); + setStatus('最新错误读取失败,已使用打开通知时的快照'); + return; + } + setEventsError(true); + setEventsReady(true); + }); void readApplicationDiagnosticLogs() .then((nextLogs) => { + if (disposed) return; setLogs(nextLogs); setLogsReady(true); }) .catch(() => { + if (disposed) return; setLogs([]); setLogsError(true); setLogsReady(true); }); - }, [events, open]); + return () => { + disposed = true; + }; + }, [fallbackEvents, open]); const submit = useCallback(async () => { const selectedEvents = events.filter((event) => selectedFingerprints.has(event.fingerprint), ); - if (!selectedEvents.length || busy || !logsReady) return; + if (!selectedEvents.length || busy || !eventsReady || !logsReady) return; if (includeLogs && logsError) return; setBusy(true); setStatus('正在提交…'); @@ -92,6 +128,7 @@ export function ErrorReportDialog({ }, [ busy, description, + eventsReady, events, includeLogs, logs, @@ -124,7 +161,11 @@ export function ErrorReportDialog({
错误事件({events.length}) - {events.length ? ( + {!eventsReady &&

正在读取当前错误…

} + {eventsReady && eventsError && ( +

错误事件暂不可用,请关闭后重试。

+ )} + {eventsReady && !eventsError && events.length > 0 && (
    {events.map((event) => (
  • ))}
- ) : ( + )} + {eventsReady && !eventsError && !events.length && (

当前没有待报告的错误。

)}
@@ -191,11 +233,12 @@ export function ErrorReportDialog({ 取消