Merge branch 'master' into feat/agc-codex-native-tool-call-info
This commit is contained in:
@@ -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::<Vec<_>>()
|
||||
.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 [
|
||||
|
||||
@@ -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<ClientErrorEvent[]>([]);
|
||||
const [eventsReady, setEventsReady] = useState(false);
|
||||
const [eventsError, setEventsError] = useState(false);
|
||||
const [selectedFingerprints, setSelectedFingerprints] = useState<Set<string>>(
|
||||
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({
|
||||
<div className="my-5 grid gap-4">
|
||||
<section>
|
||||
<strong>错误事件({events.length})</strong>
|
||||
{events.length ? (
|
||||
{!eventsReady && <p>正在读取当前错误…</p>}
|
||||
{eventsReady && eventsError && (
|
||||
<p role="alert">错误事件暂不可用,请关闭后重试。</p>
|
||||
)}
|
||||
{eventsReady && !eventsError && events.length > 0 && (
|
||||
<ul className="m-0 mt-2 grid list-none gap-2 p-0">
|
||||
{events.map((event) => (
|
||||
<li
|
||||
@@ -154,7 +195,8 @@ export function ErrorReportDialog({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
)}
|
||||
{eventsReady && !eventsError && !events.length && (
|
||||
<p>当前没有待报告的错误。</p>
|
||||
)}
|
||||
</section>
|
||||
@@ -191,11 +233,12 @@ export function ErrorReportDialog({
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="cursor-pointer rounded-[9px] border-0 bg-[var(--platform-button-primary-fill)] px-3.5 py-[9px] text-[var(--platform-button-primary-text)]"
|
||||
className="cursor-pointer rounded-[9px] border border-(--platform-button-primary-border) bg-(image:--platform-button-primary-fill) px-3.5 py-[9px] text-(--platform-button-primary-text)"
|
||||
type="button"
|
||||
onClick={() => void submit()}
|
||||
disabled={
|
||||
busy ||
|
||||
!eventsReady ||
|
||||
!selectedFingerprints.size ||
|
||||
!logsReady ||
|
||||
(includeLogs && logsError)
|
||||
|
||||
@@ -12,7 +12,10 @@ export function ErrorReportNotice() {
|
||||
const [notificationEvents, setNotificationEvents] = useState<
|
||||
ClientErrorEvent[]
|
||||
>([]);
|
||||
const [reportEvents, setReportEvents] = useState<ClientErrorEvent[]>([]);
|
||||
const [reportFallbackEvents, setReportFallbackEvents] = useState<
|
||||
ClientErrorEvent[]
|
||||
>([]);
|
||||
const [reportOpen, setReportOpen] = useState(false);
|
||||
|
||||
const requestId = useRef(0);
|
||||
const retryTimer = useRef<number | null>(null);
|
||||
@@ -65,31 +68,37 @@ export function ErrorReportNotice() {
|
||||
}, [loadEvents]);
|
||||
|
||||
const openReport = () => {
|
||||
setReportEvents(notificationEvents);
|
||||
setReportFallbackEvents(notificationEvents);
|
||||
setReportOpen(true);
|
||||
setNotificationEvents([]);
|
||||
};
|
||||
|
||||
const closeReport = () => {
|
||||
setReportOpen(false);
|
||||
setReportFallbackEvents([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{notificationEvents.length ? (
|
||||
<aside
|
||||
className="fixed top-[calc(var(--window-chrome-height)+14px)] right-4 z-40 flex max-w-[min(420px,calc(100vw-2rem))] items-center gap-2.5 rounded-xl border border-[var(--platform-surface-border)] bg-[var(--platform-subpanel-fill)] px-3.5 py-3 text-[var(--platform-text-strong)] shadow-[0_12px_30px_rgb(36_20_12_/_18%)] max-[560px]:top-[calc(var(--window-chrome-height)+8px)] max-[560px]:right-2 max-[560px]:left-2 max-[560px]:max-w-none"
|
||||
className="fixed top-[calc(var(--window-chrome-height)+12px)] right-4 z-40 flex max-w-[min(520px,calc(100vw-2rem))] items-center gap-4 rounded-xl border border-[#efc9ae] bg-[#fffaf5] px-3.5 py-3 shadow-[0_8px_24px_rgb(100_49_26_/_16%)]"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span className="flex-[1_1_auto]">
|
||||
发现 {notificationEvents.length} 个问题
|
||||
</span>
|
||||
<div className="grid min-w-0 gap-[3px]">
|
||||
<strong className="text-[13px] text-[#4a220f]">发现问题</strong>
|
||||
</div>
|
||||
<button
|
||||
className="min-h-9 cursor-pointer rounded-lg border-0 bg-[var(--platform-button-primary-fill)] px-2.5 text-[var(--platform-button-primary-text)] last:w-9 last:bg-transparent last:px-0 last:text-xl last:text-[var(--platform-text-soft)]"
|
||||
className="flex-[0_0_auto] cursor-pointer rounded-lg border-0 bg-[#c7653d] px-3 py-[7px] text-[12px] font-bold text-white disabled:cursor-wait disabled:opacity-[0.65]"
|
||||
type="button"
|
||||
onClick={openReport}
|
||||
>
|
||||
查看并报告
|
||||
</button>
|
||||
<button
|
||||
className="min-h-9 cursor-pointer rounded-lg border-0 bg-[var(--platform-button-primary-fill)] px-2.5 text-[var(--platform-button-primary-text)] last:w-9 last:bg-transparent last:px-0 last:text-xl last:text-[var(--platform-text-soft)]"
|
||||
type="button"
|
||||
className="inline-flex h-7 w-7 flex-[0_0_auto] cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 text-[#8d6a58] hover:bg-[rgb(199_101_61_/_12%)] hover:text-[#4a220f] disabled:cursor-wait disabled:opacity-50"
|
||||
onClick={() => {
|
||||
const dismissed = notificationEvents;
|
||||
setNotificationEvents([]);
|
||||
@@ -102,9 +111,9 @@ export function ErrorReportNotice() {
|
||||
</aside>
|
||||
) : null}
|
||||
<ErrorReportDialog
|
||||
open={reportEvents.length > 0}
|
||||
events={reportEvents}
|
||||
onClose={() => setReportEvents([])}
|
||||
open={reportOpen}
|
||||
fallbackEvents={reportFallbackEvents}
|
||||
onClose={closeReport}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1951,6 +1951,7 @@ export function projectRuntimeVisibleError(
|
||||
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||||
'usage-limit-exceeded': '智能创作用量已达上限,请检查账户额度后重试',
|
||||
unauthorized: '智能服务鉴权失败,请重新登录后重试',
|
||||
'request-too-large': '模型请求体过大,请减少参考图或上下文后重试',
|
||||
'bad-request': '智能创作请求无效,请稍后重试',
|
||||
'cyber-policy': '智能创作安全策略拒绝了本次请求,请调整任务内容',
|
||||
'sandbox-error': '智能创作隔离环境启动失败,请重试或检查本机环境',
|
||||
@@ -1972,6 +1973,7 @@ export function projectRuntimeVisibleError(
|
||||
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||||
'usage-limit-exceeded': '用量已达上限,请检查账户额度后重试',
|
||||
unauthorized: '鉴权失败,请重新登录后重试',
|
||||
'request-too-large': '模型请求体过大,请减少参考图或上下文后重试',
|
||||
'bad-request': '请求无效,请稍后重试',
|
||||
'cyber-policy': '安全策略拒绝了本次请求,请调整任务内容',
|
||||
'sandbox-error': '工作区隔离启动失败,请检查项目目录后重试',
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
getPending: vi.fn(),
|
||||
readLogs: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../src/services/errorReporting', () => ({
|
||||
ackClientErrorEventsWithRetry: vi.fn(),
|
||||
getPendingClientErrorEvents: mockState.getPending,
|
||||
getStableErrorReportSubmissionId: vi.fn(),
|
||||
readApplicationDiagnosticLogs: mockState.readLogs,
|
||||
submitErrorReportBatch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../src/components/modal/ThemedModal', () => ({
|
||||
ThemedModal: ({ open, children }: { open: boolean; children: ReactNode }) =>
|
||||
open ? <div role="dialog">{children}</div> : null,
|
||||
}));
|
||||
|
||||
import { ErrorReportDialog } from '../src/components/error-report/ErrorReportDialog';
|
||||
|
||||
const event = {
|
||||
eventId: 'rust-error-1',
|
||||
fingerprint: 'fingerprint-1',
|
||||
source: 'test',
|
||||
message: '打开面板时读取的错误',
|
||||
occurredAt: '1',
|
||||
count: 1,
|
||||
};
|
||||
|
||||
describe('ErrorReportDialog', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockState.getPending.mockReset();
|
||||
mockState.readLogs.mockReset();
|
||||
});
|
||||
|
||||
it('只在面板打开时读取最新错误快照', async () => {
|
||||
mockState.getPending.mockResolvedValue([event]);
|
||||
mockState.readLogs.mockResolvedValue([]);
|
||||
|
||||
const view = render(<ErrorReportDialog open={false} onClose={vi.fn()} />);
|
||||
expect(mockState.getPending).not.toHaveBeenCalled();
|
||||
|
||||
view.rerender(<ErrorReportDialog open onClose={vi.fn()} />);
|
||||
|
||||
expect(await screen.findByText(event.message)).toBeTruthy();
|
||||
expect(mockState.getPending).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('读取错误事件失败时显示不可用状态而不是空列表', async () => {
|
||||
mockState.getPending.mockRejectedValue(new Error('读取失败'));
|
||||
mockState.readLogs.mockResolvedValue([]);
|
||||
|
||||
render(<ErrorReportDialog open onClose={vi.fn()} />);
|
||||
|
||||
expect(
|
||||
await screen.findByText('错误事件暂不可用,请关闭后重试。'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('当前没有待报告的错误。')).toBeNull();
|
||||
});
|
||||
|
||||
it('最新快照读取失败时使用打开通知时的事件快照', async () => {
|
||||
mockState.getPending.mockRejectedValue(new Error('瞬时读取失败'));
|
||||
mockState.readLogs.mockResolvedValue([]);
|
||||
|
||||
render(
|
||||
<ErrorReportDialog open fallbackEvents={[event]} onClose={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(await screen.findByText(event.message)).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('最新错误读取失败,已使用打开通知时的快照'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,12 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
@@ -8,6 +14,10 @@ const mockState = vi.hoisted(() => ({
|
||||
listeners: [] as Array<() => void>,
|
||||
}));
|
||||
|
||||
const dialogState = vi.hoisted(() => ({
|
||||
fallbackEvents: [] as (typeof event)[],
|
||||
}));
|
||||
|
||||
vi.mock('../src/services/errorReporting', () => ({
|
||||
getPendingClientErrorEvents: mockState.getPending,
|
||||
subscribeClientErrorEvents: (listener: () => void) => {
|
||||
@@ -21,7 +31,16 @@ vi.mock('../src/services/errorReporting', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../src/components/error-report/ErrorReportDialog', () => ({
|
||||
ErrorReportDialog: () => null,
|
||||
ErrorReportDialog: ({
|
||||
fallbackEvents,
|
||||
open,
|
||||
}: {
|
||||
fallbackEvents?: (typeof event)[];
|
||||
open: boolean;
|
||||
}) => {
|
||||
dialogState.fallbackEvents = fallbackEvents ?? [];
|
||||
return open ? <div data-testid="error-report-dialog" /> : null;
|
||||
},
|
||||
}));
|
||||
|
||||
import { ErrorReportNotice } from '../src/components/error-report/ErrorReportNotice';
|
||||
@@ -47,7 +66,7 @@ describe('ErrorReportNotice', () => {
|
||||
|
||||
render(<ErrorReportNotice />);
|
||||
|
||||
expect(await screen.findByText('发现 1 个问题')).toBeTruthy();
|
||||
expect(await screen.findByText('发现问题')).toBeTruthy();
|
||||
expect(mockState.getPending).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -61,7 +80,19 @@ describe('ErrorReportNotice', () => {
|
||||
|
||||
mockState.listeners[0]?.();
|
||||
|
||||
expect(await screen.findByText('发现 1 个问题')).toBeTruthy();
|
||||
expect(await screen.findByText('发现问题')).toBeTruthy();
|
||||
expect(mockState.getPending).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('打开报告时保留通知快照作为对话框 fallback', async () => {
|
||||
mockState.getPending.mockResolvedValue([event]);
|
||||
|
||||
render(<ErrorReportNotice />);
|
||||
await screen.findByText('发现问题');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看并报告' }));
|
||||
|
||||
expect(await screen.findByTestId('error-report-dialog')).toBeTruthy();
|
||||
expect(dialogState.fallbackEvents).toEqual([event]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -695,6 +695,13 @@ describe('Agent Runtime Provider 状态投影', () => {
|
||||
true,
|
||||
),
|
||||
).toBe('陶泥儿智能创作 用量已达上限,请检查账户额度后重试');
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'codex-app-server-error:request-too-large',
|
||||
'陶泥儿智能创作',
|
||||
true,
|
||||
),
|
||||
).toBe('陶泥儿智能创作 模型请求体过大,请减少参考图或上下文后重试');
|
||||
expect(
|
||||
projectRuntimeVisibleError(
|
||||
'codex-app-server-terminal-unknown: 等待 turn/completed 超时',
|
||||
|
||||
Reference in New Issue
Block a user