Merge branch 'master' into codex/agc-client-mcp-facade
This commit is contained in:
@@ -266,6 +266,7 @@ fn game_creator_codex_app_server_connection_error(
|
|||||||
) -> platform_llm::LlmError {
|
) -> platform_llm::LlmError {
|
||||||
match game_creator_codex_app_server_error_http_status(info, field) {
|
match game_creator_codex_app_server_error_http_status(info, field) {
|
||||||
Some(401 | 403) => game_creator_codex_app_server_error_kind("unauthorized"),
|
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 {
|
Some(status_code) => platform_llm::LlmError::Upstream {
|
||||||
status_code,
|
status_code,
|
||||||
message: "Codex app-server 连接上游失败".to_string(),
|
message: "Codex app-server 连接上游失败".to_string(),
|
||||||
@@ -303,6 +304,30 @@ fn game_creator_codex_app_server_error_detail_indicates_auth_failure(
|
|||||||
|| detail.contains("http 403")
|
|| 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(
|
fn game_creator_codex_app_server_error_detail_indicates_insufficient_mud_points(
|
||||||
error: &serde_json::Value,
|
error: &serde_json::Value,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@@ -333,6 +358,9 @@ fn game_creator_codex_app_server_failed_turn_error(
|
|||||||
message: "泥点余额不足".to_string(),
|
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) {
|
if game_creator_codex_app_server_error_detail_indicates_auth_failure(error) {
|
||||||
return game_creator_codex_app_server_error_kind("unauthorized");
|
return game_creator_codex_app_server_error_kind("unauthorized");
|
||||||
}
|
}
|
||||||
@@ -4787,6 +4815,12 @@ mod tests {
|
|||||||
"codex-app-server-error:unauthorized".to_string(),
|
"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}}),
|
serde_json::json!({"httpConnectionFailed":{"httpStatusCode":429}}),
|
||||||
platform_llm::LlmError::Upstream {
|
platform_llm::LlmError::Upstream {
|
||||||
@@ -4847,6 +4881,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]
|
#[test]
|
||||||
fn codex_app_server_failed_turn_maps_insufficient_mud_points_to_stable_upstream_error() {
|
fn codex_app_server_failed_turn_maps_insufficient_mud_points_to_stable_upstream_error() {
|
||||||
for detail in [
|
for detail in [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ackClientErrorEventsWithRetry,
|
ackClientErrorEventsWithRetry,
|
||||||
type ClientErrorEvent,
|
type ClientErrorEvent,
|
||||||
type DiagnosticLogFile,
|
type DiagnosticLogFile,
|
||||||
|
getPendingClientErrorEvents,
|
||||||
getStableErrorReportSubmissionId,
|
getStableErrorReportSubmissionId,
|
||||||
readApplicationDiagnosticLogs,
|
readApplicationDiagnosticLogs,
|
||||||
submitErrorReportBatch,
|
submitErrorReportBatch,
|
||||||
@@ -12,10 +13,12 @@ import { ThemedModal } from '../modal/ThemedModal';
|
|||||||
|
|
||||||
type ErrorReportDialogProps = {
|
type ErrorReportDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
events: ClientErrorEvent[];
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
fallbackEvents?: ClientErrorEvent[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const emptyFallbackEvents: ClientErrorEvent[] = [];
|
||||||
|
|
||||||
function diagnosticLogLabel(
|
function diagnosticLogLabel(
|
||||||
logsReady: boolean,
|
logsReady: boolean,
|
||||||
logsError: boolean,
|
logsError: boolean,
|
||||||
@@ -28,9 +31,12 @@ function diagnosticLogLabel(
|
|||||||
|
|
||||||
export function ErrorReportDialog({
|
export function ErrorReportDialog({
|
||||||
open,
|
open,
|
||||||
events,
|
|
||||||
onClose,
|
onClose,
|
||||||
|
fallbackEvents = emptyFallbackEvents,
|
||||||
}: ErrorReportDialogProps) {
|
}: ErrorReportDialogProps) {
|
||||||
|
const [events, setEvents] = useState<ClientErrorEvent[]>([]);
|
||||||
|
const [eventsReady, setEventsReady] = useState(false);
|
||||||
|
const [eventsError, setEventsError] = useState(false);
|
||||||
const [selectedFingerprints, setSelectedFingerprints] = useState<Set<string>>(
|
const [selectedFingerprints, setSelectedFingerprints] = useState<Set<string>>(
|
||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
@@ -45,29 +51,59 @@ export function ErrorReportDialog({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
setSelectedFingerprints(
|
let disposed = false;
|
||||||
new Set(events.slice(0, 1).map((event) => event.fingerprint)),
|
setEvents([]);
|
||||||
);
|
setEventsReady(false);
|
||||||
|
setEventsError(false);
|
||||||
|
setSelectedFingerprints(new Set());
|
||||||
setStatus('');
|
setStatus('');
|
||||||
setLogsReady(false);
|
setLogsReady(false);
|
||||||
setLogsError(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()
|
void readApplicationDiagnosticLogs()
|
||||||
.then((nextLogs) => {
|
.then((nextLogs) => {
|
||||||
|
if (disposed) return;
|
||||||
setLogs(nextLogs);
|
setLogs(nextLogs);
|
||||||
setLogsReady(true);
|
setLogsReady(true);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
if (disposed) return;
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
setLogsError(true);
|
setLogsError(true);
|
||||||
setLogsReady(true);
|
setLogsReady(true);
|
||||||
});
|
});
|
||||||
}, [events, open]);
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
};
|
||||||
|
}, [fallbackEvents, open]);
|
||||||
|
|
||||||
const submit = useCallback(async () => {
|
const submit = useCallback(async () => {
|
||||||
const selectedEvents = events.filter((event) =>
|
const selectedEvents = events.filter((event) =>
|
||||||
selectedFingerprints.has(event.fingerprint),
|
selectedFingerprints.has(event.fingerprint),
|
||||||
);
|
);
|
||||||
if (!selectedEvents.length || busy || !logsReady) return;
|
if (!selectedEvents.length || busy || !eventsReady || !logsReady) return;
|
||||||
if (includeLogs && logsError) return;
|
if (includeLogs && logsError) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setStatus('正在提交…');
|
setStatus('正在提交…');
|
||||||
@@ -92,6 +128,7 @@ export function ErrorReportDialog({
|
|||||||
}, [
|
}, [
|
||||||
busy,
|
busy,
|
||||||
description,
|
description,
|
||||||
|
eventsReady,
|
||||||
events,
|
events,
|
||||||
includeLogs,
|
includeLogs,
|
||||||
logs,
|
logs,
|
||||||
@@ -124,7 +161,11 @@ export function ErrorReportDialog({
|
|||||||
<div className="my-5 grid gap-4">
|
<div className="my-5 grid gap-4">
|
||||||
<section>
|
<section>
|
||||||
<strong>错误事件({events.length})</strong>
|
<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">
|
<ul className="m-0 mt-2 grid list-none gap-2 p-0">
|
||||||
{events.map((event) => (
|
{events.map((event) => (
|
||||||
<li
|
<li
|
||||||
@@ -154,7 +195,8 @@ export function ErrorReportDialog({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
)}
|
||||||
|
{eventsReady && !eventsError && !events.length && (
|
||||||
<p>当前没有待报告的错误。</p>
|
<p>当前没有待报告的错误。</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -191,11 +233,12 @@ export function ErrorReportDialog({
|
|||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<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"
|
type="button"
|
||||||
onClick={() => void submit()}
|
onClick={() => void submit()}
|
||||||
disabled={
|
disabled={
|
||||||
busy ||
|
busy ||
|
||||||
|
!eventsReady ||
|
||||||
!selectedFingerprints.size ||
|
!selectedFingerprints.size ||
|
||||||
!logsReady ||
|
!logsReady ||
|
||||||
(includeLogs && logsError)
|
(includeLogs && logsError)
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export function ErrorReportNotice() {
|
|||||||
const [notificationEvents, setNotificationEvents] = useState<
|
const [notificationEvents, setNotificationEvents] = useState<
|
||||||
ClientErrorEvent[]
|
ClientErrorEvent[]
|
||||||
>([]);
|
>([]);
|
||||||
const [reportEvents, setReportEvents] = useState<ClientErrorEvent[]>([]);
|
const [reportFallbackEvents, setReportFallbackEvents] = useState<
|
||||||
|
ClientErrorEvent[]
|
||||||
|
>([]);
|
||||||
|
const [reportOpen, setReportOpen] = useState(false);
|
||||||
|
|
||||||
const requestId = useRef(0);
|
const requestId = useRef(0);
|
||||||
const retryTimer = useRef<number | null>(null);
|
const retryTimer = useRef<number | null>(null);
|
||||||
@@ -65,31 +68,37 @@ export function ErrorReportNotice() {
|
|||||||
}, [loadEvents]);
|
}, [loadEvents]);
|
||||||
|
|
||||||
const openReport = () => {
|
const openReport = () => {
|
||||||
setReportEvents(notificationEvents);
|
setReportFallbackEvents(notificationEvents);
|
||||||
|
setReportOpen(true);
|
||||||
setNotificationEvents([]);
|
setNotificationEvents([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeReport = () => {
|
||||||
|
setReportOpen(false);
|
||||||
|
setReportFallbackEvents([]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{notificationEvents.length ? (
|
{notificationEvents.length ? (
|
||||||
<aside
|
<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"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<span className="flex-[1_1_auto]">
|
<div className="grid min-w-0 gap-[3px]">
|
||||||
发现 {notificationEvents.length} 个问题
|
<strong className="text-[13px] text-[#4a220f]">发现问题</strong>
|
||||||
</span>
|
</div>
|
||||||
<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)]"
|
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"
|
type="button"
|
||||||
onClick={openReport}
|
onClick={openReport}
|
||||||
>
|
>
|
||||||
查看并报告
|
查看并报告
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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={() => {
|
onClick={() => {
|
||||||
const dismissed = notificationEvents;
|
const dismissed = notificationEvents;
|
||||||
setNotificationEvents([]);
|
setNotificationEvents([]);
|
||||||
@@ -102,9 +111,9 @@ export function ErrorReportNotice() {
|
|||||||
</aside>
|
</aside>
|
||||||
) : null}
|
) : null}
|
||||||
<ErrorReportDialog
|
<ErrorReportDialog
|
||||||
open={reportEvents.length > 0}
|
open={reportOpen}
|
||||||
events={reportEvents}
|
fallbackEvents={reportFallbackEvents}
|
||||||
onClose={() => setReportEvents([])}
|
onClose={closeReport}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1951,6 +1951,7 @@ export function projectRuntimeVisibleError(
|
|||||||
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||||||
'usage-limit-exceeded': '智能创作用量已达上限,请检查账户额度后重试',
|
'usage-limit-exceeded': '智能创作用量已达上限,请检查账户额度后重试',
|
||||||
unauthorized: '智能服务鉴权失败,请重新登录后重试',
|
unauthorized: '智能服务鉴权失败,请重新登录后重试',
|
||||||
|
'request-too-large': '模型请求体过大,请减少参考图或上下文后重试',
|
||||||
'bad-request': '智能创作请求无效,请稍后重试',
|
'bad-request': '智能创作请求无效,请稍后重试',
|
||||||
'cyber-policy': '智能创作安全策略拒绝了本次请求,请调整任务内容',
|
'cyber-policy': '智能创作安全策略拒绝了本次请求,请调整任务内容',
|
||||||
'sandbox-error': '智能创作隔离环境启动失败,请重试或检查本机环境',
|
'sandbox-error': '智能创作隔离环境启动失败,请重试或检查本机环境',
|
||||||
@@ -1972,6 +1973,7 @@ export function projectRuntimeVisibleError(
|
|||||||
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||||||
'usage-limit-exceeded': '用量已达上限,请检查账户额度后重试',
|
'usage-limit-exceeded': '用量已达上限,请检查账户额度后重试',
|
||||||
unauthorized: '鉴权失败,请重新登录后重试',
|
unauthorized: '鉴权失败,请重新登录后重试',
|
||||||
|
'request-too-large': '模型请求体过大,请减少参考图或上下文后重试',
|
||||||
'bad-request': '请求无效,请稍后重试',
|
'bad-request': '请求无效,请稍后重试',
|
||||||
'cyber-policy': '安全策略拒绝了本次请求,请调整任务内容',
|
'cyber-policy': '安全策略拒绝了本次请求,请调整任务内容',
|
||||||
'sandbox-error': '工作区隔离启动失败,请检查项目目录后重试',
|
'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 */
|
/** @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';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
const mockState = vi.hoisted(() => ({
|
const mockState = vi.hoisted(() => ({
|
||||||
@@ -8,6 +14,10 @@ const mockState = vi.hoisted(() => ({
|
|||||||
listeners: [] as Array<() => void>,
|
listeners: [] as Array<() => void>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const dialogState = vi.hoisted(() => ({
|
||||||
|
fallbackEvents: [] as (typeof event)[],
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../src/services/errorReporting', () => ({
|
vi.mock('../src/services/errorReporting', () => ({
|
||||||
getPendingClientErrorEvents: mockState.getPending,
|
getPendingClientErrorEvents: mockState.getPending,
|
||||||
subscribeClientErrorEvents: (listener: () => void) => {
|
subscribeClientErrorEvents: (listener: () => void) => {
|
||||||
@@ -21,7 +31,16 @@ vi.mock('../src/services/errorReporting', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../src/components/error-report/ErrorReportDialog', () => ({
|
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';
|
import { ErrorReportNotice } from '../src/components/error-report/ErrorReportNotice';
|
||||||
@@ -47,7 +66,7 @@ describe('ErrorReportNotice', () => {
|
|||||||
|
|
||||||
render(<ErrorReportNotice />);
|
render(<ErrorReportNotice />);
|
||||||
|
|
||||||
expect(await screen.findByText('发现 1 个问题')).toBeTruthy();
|
expect(await screen.findByText('发现问题')).toBeTruthy();
|
||||||
expect(mockState.getPending).toHaveBeenCalledTimes(1);
|
expect(mockState.getPending).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +80,19 @@ describe('ErrorReportNotice', () => {
|
|||||||
|
|
||||||
mockState.listeners[0]?.();
|
mockState.listeners[0]?.();
|
||||||
|
|
||||||
expect(await screen.findByText('发现 1 个问题')).toBeTruthy();
|
expect(await screen.findByText('发现问题')).toBeTruthy();
|
||||||
expect(mockState.getPending).toHaveBeenCalledTimes(2);
|
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,
|
true,
|
||||||
),
|
),
|
||||||
).toBe('陶泥儿智能创作 用量已达上限,请检查账户额度后重试');
|
).toBe('陶泥儿智能创作 用量已达上限,请检查账户额度后重试');
|
||||||
|
expect(
|
||||||
|
projectRuntimeVisibleError(
|
||||||
|
'codex-app-server-error:request-too-large',
|
||||||
|
'陶泥儿智能创作',
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe('陶泥儿智能创作 模型请求体过大,请减少参考图或上下文后重试');
|
||||||
expect(
|
expect(
|
||||||
projectRuntimeVisibleError(
|
projectRuntimeVisibleError(
|
||||||
'codex-app-server-terminal-unknown: 等待 turn/completed 超时',
|
'codex-app-server-terminal-unknown: 等待 turn/completed 超时',
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事
|
|||||||
- 指纹计算可使用调用方的 page/action 及脱敏后的首个调用点作为进程内区分输入,但这些上下文不会作为事件字段上传;消息与 stack 在入池前统一脱敏,WebCrypto 失败时降级为稳定可读指纹,采集本身不得产生新的未处理拒绝。
|
- 指纹计算可使用调用方的 page/action 及脱敏后的首个调用点作为进程内区分输入,但这些上下文不会作为事件字段上传;消息与 stack 在入池前统一脱敏,WebCrypto 失败时降级为稳定可读指纹,采集本身不得产生新的未处理拒绝。
|
||||||
- 客户端 API 自动采集只覆盖网络错误、408 和 5xx;预期的 4xx 登录/鉴权失败不进入错误报告池。
|
- 客户端 API 自动采集只覆盖网络错误、408 和 5xx;预期的 4xx 登录/鉴权失败不进入错误报告池。
|
||||||
- Rust 侧通过 `app_log!` 将普通文本日志同时输出到 stderr 和 AppData `diagnostics/application.log`,超出 256 KiB 滚动到 `application.previous.log`;WebView 的 console 输出通过 `append_application_log` 镜像到同一 raw log,并在客户端桥接处再次脱敏;`read_diagnostic_logs` 只读取应用级日志。
|
- Rust 侧通过 `app_log!` 将普通文本日志同时输出到 stderr 和 AppData `diagnostics/application.log`,超出 256 KiB 滚动到 `application.previous.log`;WebView 的 console 输出通过 `append_application_log` 镜像到同一 raw log,并在客户端桥接处再次脱敏;`read_diagnostic_logs` 只读取应用级日志。
|
||||||
- 报告面板只由自动诊断通知中的“查看并报告”打开,不提供聊天命令、崩溃页按钮或其他手动入口;默认选中最新事件,其他事件可勾选。允许填写最多 2,000 字中文描述并取消日志附件;本版本不支持截图或任意文件附件。
|
- 报告面板只由自动诊断通知中的“查看并报告”打开,不提供聊天命令、崩溃页按钮或其他手动入口;默认选中当前快照中的全部事件,用户可取消不想提交的事件。允许填写最多 2,000 字中文描述并取消日志附件;本版本不支持截图或任意文件附件。
|
||||||
|
- 报告面板读取当前错误快照失败时,必须明确显示“错误事件暂不可用,请关闭后重试”,不能把失败误显示为“当前没有待报告的错误”。
|
||||||
|
- 通知中的“查看并报告”打开面板时必须保留该次通知快照;最新快照读取瞬时失败时使用这份 fallback 继续展示和提交,不能因先清空通知而丢失用户刚看到的事件。
|
||||||
- Rust 是结构化错误队列的唯一真相源:`src-tauri/src/error_report/` 负责脱敏、调用点指纹、eventId、计数、100 条上限、5 秒聚合和 ack 生命周期;WebView 仅通过 Tauri bridge 上报、读取快照并保存短暂 React 展示状态,不维护第二份事件 Map。Runtime 错误上报统一使用 `source=agent-runtime`、`action=agent-runtime` 和 Agent ID 作为 page;包含 `kind=codex-app-server-*` 或 `codex-app-server-error:*` 的消息在入池前归一为稳定类别(例如 `codex-app-server-error:other`),不把 public summary 中的动态 fingerprint/长度作为分桶输入。Rust emit 只作为无状态唤醒,携带单调递增的 generation,不携带错误正文或事件 ID。
|
- Rust 是结构化错误队列的唯一真相源:`src-tauri/src/error_report/` 负责脱敏、调用点指纹、eventId、计数、100 条上限、5 秒聚合和 ack 生命周期;WebView 仅通过 Tauri bridge 上报、读取快照并保存短暂 React 展示状态,不维护第二份事件 Map。Runtime 错误上报统一使用 `source=agent-runtime`、`action=agent-runtime` 和 Agent ID 作为 page;包含 `kind=codex-app-server-*` 或 `codex-app-server-error:*` 的消息在入池前归一为稳定类别(例如 `codex-app-server-error:other`),不把 public summary 中的动态 fingerprint/长度作为分桶输入。Rust emit 只作为无状态唤醒,携带单调递增的 generation,不携带错误正文或事件 ID。
|
||||||
- 错误事件先在当前进程内存池按 fingerprint 合并,经过 5 秒聚合后只发出一次非阻塞存在性唤醒;WebView 挂载、收到唤醒、重新获得焦点或恢复可见时都查询完整未 ack 快照,并在桥接暂时失败时做有限退避重试。通知支持“查看并报告”和“忽略”,同一 fingerprint 仅在新增时唤醒一次。通知不直接打开阻塞式报告面板。忽略只关闭当前 UI,不删除事件;提交成功后由 bridge ack/delete 选中事件。
|
- 错误事件先在当前进程内存池按 fingerprint 合并,经过 5 秒聚合后只发出一次非阻塞存在性唤醒;WebView 挂载、收到唤醒、重新获得焦点或恢复可见时都查询完整未 ack 快照,并在桥接暂时失败时做有限退避重试。通知支持“查看并报告”和“忽略”,同一 fingerprint 仅在新增时唤醒一次。通知不直接打开阻塞式报告面板;用户打开报告面板后,面板再读取一次完整未 ack 快照,确保提交使用打开时的最新事件。忽略只关闭当前 UI,不删除事件;提交成功后由 bridge ack/delete 选中事件。
|
||||||
- 上传失败只在当前进程显示失败并允许用户再次提交,不跨重启恢复事件池,不后台自动重试。
|
- 上传失败只在当前进程显示失败并允许用户再次提交,不跨重启恢复事件池,不后台自动重试。
|
||||||
|
|
||||||
## HTTP 与存储
|
## HTTP 与存储
|
||||||
|
|||||||
@@ -1289,3 +1289,8 @@ DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过
|
|||||||
- 配置文件新增 `schemaVersion: \"game-creator-config.v2\"`。新默认配置开启 `stream` 与受控联网;无版本旧配置在启动时补写 v2,旧 `codex_app_server` 路由仅在省略 `webSearchEnabled` 时按历史默认补为开启,显式 `false` 保留;Provider / Anthropic 路由未提供搜索覆盖时保持关闭,避免继承 DirectProject 默认。主配置按完整配置迁移;本地覆盖只补 schema 版本,不凭不完整 overlay 推断或写入 `agentMode` / 搜索布尔值。
|
- 配置文件新增 `schemaVersion: \"game-creator-config.v2\"`。新默认配置开启 `stream` 与受控联网;无版本旧配置在启动时补写 v2,旧 `codex_app_server` 路由仅在省略 `webSearchEnabled` 时按历史默认补为开启,显式 `false` 保留;Provider / Anthropic 路由未提供搜索覆盖时保持关闭,避免继承 DirectProject 默认。主配置按完整配置迁移;本地覆盖只补 schema 版本,不凭不完整 overlay 推断或写入 `agentMode` / 搜索布尔值。
|
||||||
- `/llm-status`、开发单 Agent 状态和项目 Agent 状态卡对 Codex 模式显示“流式开启 / 关闭”“受控联网开启 / 关闭”“Codex 原生 web_search 关闭”,不显示 API Key、URL、请求头、绝对路径或 Provider 原始错误正文。
|
- `/llm-status`、开发单 Agent 状态和项目 Agent 状态卡对 Codex 模式显示“流式开启 / 关闭”“受控联网开启 / 关闭”“Codex 原生 web_search 关闭”,不显示 API Key、URL、请求头、绝对路径或 Provider 原始错误正文。
|
||||||
- BDD 验收场景与测试映射:DirectProject 默认工具目录包含 `agc_web_search` 且原生搜索仍 disabled;未审核字段、越界数量和非公开 URL 在桥接端失败关闭;旧无版本配置迁移为 v2 且按路由得到正确默认;状态卡显示三态安全摘要。对应 Rust `configuration`、`direct_tools_mcp`、`direct_tool_bridge`、`codex_app_server` 定向测试及前端状态格式化 / AppSurface 测试。
|
- BDD 验收场景与测试映射:DirectProject 默认工具目录包含 `agc_web_search` 且原生搜索仍 disabled;未审核字段、越界数量和非公开 URL 在桥接端失败关闭;旧无版本配置迁移为 v2 且按路由得到正确默认;状态卡显示三态安全摘要。对应 Rust `configuration`、`direct_tools_mcp`、`direct_tool_bridge`、`codex_app_server` 定向测试及前端状态格式化 / AppSurface 测试。
|
||||||
|
|
||||||
|
## 2026-09-06 AGC LLM 代理请求体合同
|
||||||
|
|
||||||
|
- `/api/llm/responses` 与 `/api/llm/chat/completions` 的正式请求体上限为 `32 MiB`。两个路由必须显式配置 Axum `DefaultBodyLimit::max(LLM_REQUEST_MAX_BODY_BYTES)`;不能依赖 handler 内的 `Bytes / Json` 后置检查,否则 Axum 默认 `2 MiB` 会先拒绝 Direct Codex 携带图片工具结果的大上下文请求。超过 `32 MiB` 仍返回 `413 PAYLOAD_TOO_LARGE`。
|
||||||
|
- Codex app-server 的 failed turn 需要把上游 / 连接层 HTTP 413、`PAYLOAD_TOO_LARGE` 和 provider proxy 的 `provider request too large` 映射为稳定分类 `codex-app-server-error:request-too-large`;用户可见文案固定为“模型请求体过大,请减少参考图或上下文后重试”,不得落入 `other` 或泛化成权限 / 安全策略错误。
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ use crate::{
|
|||||||
platform_errors::map_llm_error, request_context::RequestContext, state::AppState,
|
platform_errors::map_llm_error, request_context::RequestContext, state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub(crate) const LLM_REQUEST_MAX_BODY_BYTES: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
pub(crate) mod icon_specs;
|
pub(crate) mod icon_specs;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -222,8 +224,7 @@ pub async fn proxy_llm_responses(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
body: Bytes,
|
body: Bytes,
|
||||||
) -> Result<Response, Response> {
|
) -> Result<Response, Response> {
|
||||||
const MAX_REQUEST_BYTES: usize = 32 * 1024 * 1024;
|
if body.len() > LLM_REQUEST_MAX_BODY_BYTES {
|
||||||
if body.len() > MAX_REQUEST_BYTES {
|
|
||||||
return Err(llm_error_response(
|
return Err(llm_error_response(
|
||||||
&request_context,
|
&request_context,
|
||||||
AppError::from_status(StatusCode::PAYLOAD_TOO_LARGE)
|
AppError::from_status(StatusCode::PAYLOAD_TOO_LARGE)
|
||||||
@@ -1187,6 +1188,77 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn llm_routes_accept_large_context_bodies_beyond_axum_default() {
|
||||||
|
let large_input = "x".repeat(2 * 1024 * 1024 + 1024);
|
||||||
|
let (state, user_id) = seed_authenticated_state(AppConfig::default()).await;
|
||||||
|
install_test_provisioned_router_credential(
|
||||||
|
&user_id,
|
||||||
|
"http://127.0.0.1:1".to_string(),
|
||||||
|
"fixture-key",
|
||||||
|
);
|
||||||
|
let token = issue_access_token(&state, &user_id);
|
||||||
|
let app = build_router(state);
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.method("POST")
|
||||||
|
.uri("/api/llm/responses")
|
||||||
|
.header("authorization", format!("Bearer {token}"))
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(json!({ "input": large_input }).to_string()))
|
||||||
|
.expect("request should build"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("response should not hit the default body limit");
|
||||||
|
assert_ne!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.method("POST")
|
||||||
|
.uri("/api/llm/chat/completions")
|
||||||
|
.header("authorization", format!("Bearer {token}"))
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(
|
||||||
|
json!({
|
||||||
|
"messages": [
|
||||||
|
{ "role": "user", "content": large_input }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
.expect("request should build"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("response should not hit the default body limit");
|
||||||
|
assert_ne!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn llm_responses_rejects_bodies_above_explicit_limit() {
|
||||||
|
let (state, user_id) = seed_authenticated_state(AppConfig::default()).await;
|
||||||
|
let token = issue_access_token(&state, &user_id);
|
||||||
|
let app = build_router(state);
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.method("POST")
|
||||||
|
.uri("/api/llm/responses")
|
||||||
|
.header("authorization", format!("Bearer {token}"))
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(vec![b'x'; LLM_REQUEST_MAX_BODY_BYTES + 1]))
|
||||||
|
.expect("request should build"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("oversized response should be returned");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn llm_chat_completions_streams_sse_payload() {
|
async fn llm_chat_completions_streams_sse_payload() {
|
||||||
let server_url = spawn_mock_server(vec![MockResponse {
|
let server_url = spawn_mock_server(vec![MockResponse {
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router, middleware,
|
Router,
|
||||||
|
extract::DefaultBodyLimit,
|
||||||
|
middleware,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::require_bearer_auth,
|
auth::require_bearer_auth,
|
||||||
llm::{list_llm_models, proxy_llm_chat_completions, proxy_llm_responses},
|
llm::{
|
||||||
|
LLM_REQUEST_MAX_BODY_BYTES, list_llm_models, proxy_llm_chat_completions,
|
||||||
|
proxy_llm_responses,
|
||||||
|
},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
volcengine_speech::{
|
volcengine_speech::{
|
||||||
get_volcengine_speech_config, stream_volcengine_asr, stream_volcengine_tts_bidirection,
|
get_volcengine_speech_config, stream_volcengine_asr, stream_volcengine_tts_bidirection,
|
||||||
@@ -24,17 +29,21 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/llm/chat/completions",
|
"/api/llm/chat/completions",
|
||||||
post(proxy_llm_chat_completions).route_layer(middleware::from_fn_with_state(
|
post(proxy_llm_chat_completions)
|
||||||
state.clone(),
|
.route_layer(middleware::from_fn_with_state(
|
||||||
require_bearer_auth,
|
state.clone(),
|
||||||
)),
|
require_bearer_auth,
|
||||||
|
))
|
||||||
|
.layer(DefaultBodyLimit::max(LLM_REQUEST_MAX_BODY_BYTES)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/llm/responses",
|
"/api/llm/responses",
|
||||||
post(proxy_llm_responses).route_layer(middleware::from_fn_with_state(
|
post(proxy_llm_responses)
|
||||||
state.clone(),
|
.route_layer(middleware::from_fn_with_state(
|
||||||
require_bearer_auth,
|
state.clone(),
|
||||||
)),
|
require_bearer_auth,
|
||||||
|
))
|
||||||
|
.layer(DefaultBodyLimit::max(LLM_REQUEST_MAX_BODY_BYTES)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/speech/volcengine/config",
|
"/api/speech/volcengine/config",
|
||||||
|
|||||||
Reference in New Issue
Block a user