From ed44e7f925076df87abd46b1d8c2596df6f65c1d Mon Sep 17 00:00:00 2001 From: Cita Date: Sun, 29 Mar 2026 11:50:16 +0800 Subject: [PATCH] Refine proxy logs debug trace UI (#309) --- .../pages/ProxyLogs.server-driven.test.tsx | 73 +- src/web/pages/ProxyLogs.tsx | 918 ++++++++++++------ 2 files changed, 707 insertions(+), 284 deletions(-) diff --git a/src/web/pages/ProxyLogs.server-driven.test.tsx b/src/web/pages/ProxyLogs.server-driven.test.tsx index 25ec2a4..5de90ee 100644 --- a/src/web/pages/ProxyLogs.server-driven.test.tsx +++ b/src/web/pages/ProxyLogs.server-driven.test.tsx @@ -238,11 +238,13 @@ describe('ProxyLogs server-driven page', () => { expect(text).toContain('推测'); expect(text).toContain('下游 Key: 移动端灰度'); } finally { - root?.unmount(); + await act(async () => { + root?.unmount(); + }); } }); - it('allows toggling proxy debug settings directly from the proxy logs page', async () => { + it('shows proxy debug traces inline and edits settings through the modal', async () => { let root!: WebTestRenderer; try { @@ -257,22 +259,22 @@ describe('ProxyLogs server-driven page', () => { }); await flushMicrotasks(); - const debugButton = root.root.find((node) => ( - node.type === 'button' - && typeof node.props.onClick === 'function' - && collectText(node).includes('调试') - )); - - await act(async () => { - debugButton.props.onClick(); - }); - await flushMicrotasks(); - expect(apiMock.getRuntimeSettings).toHaveBeenCalled(); expect(apiMock.getProxyDebugTraces).toHaveBeenCalled(); expect(collectText(root.root)).toContain('最近调试追踪'); expect(collectText(root.root)).toContain('sess-debug-1'); + const debugSettingsButton = root.root.find((node) => ( + node.type === 'button' + && typeof node.props.onClick === 'function' + && collectText(node).trim() === '调试设置' + )); + + await act(async () => { + debugSettingsButton.props.onClick(); + }); + await flushMicrotasks(); + const traceEnabledToggle = root.root.find((node) => ( node.type === 'input' && node.props.type === 'checkbox' @@ -321,6 +323,51 @@ describe('ProxyLogs server-driven page', () => { } }); + it('polls debug traces after tracing is enabled so new results are not hidden behind the settings modal', async () => { + vi.useFakeTimers(); + apiMock.getRuntimeSettings.mockResolvedValue({ + proxyDebugTraceEnabled: true, + proxyDebugCaptureHeaders: true, + proxyDebugCaptureBodies: false, + proxyDebugCaptureStreamChunks: false, + proxyDebugTargetSessionId: '', + proxyDebugTargetClientKind: '', + proxyDebugTargetModel: '', + proxyDebugRetentionHours: 24, + proxyDebugMaxBodyBytes: 262144, + }); + + let root!: WebTestRenderer; + + try { + await act(async () => { + root = create( + + + + + , + ); + }); + await flushMicrotasks(); + + const initialCalls = apiMock.getProxyDebugTraces.mock.calls.length; + + await act(async () => { + vi.advanceTimersByTime(2100); + }); + await flushMicrotasks(); + + expect(apiMock.getProxyDebugTraces.mock.calls.length).toBeGreaterThan(initialCalls); + } finally { + await act(async () => { + root?.unmount(); + }); + vi.runOnlyPendingTimers(); + vi.useRealTimers(); + } + }); + it('keeps the model badge sized to the model name in desktop rows', async () => { let root!: WebTestRenderer; diff --git a/src/web/pages/ProxyLogs.tsx b/src/web/pages/ProxyLogs.tsx index 3247cc2..73251c5 100644 --- a/src/web/pages/ProxyLogs.tsx +++ b/src/web/pages/ProxyLogs.tsx @@ -13,6 +13,9 @@ import { } from '../api.js'; import { useToast } from '../components/Toast.js'; import { ModelBadge } from '../components/BrandIcon.js'; +import CenteredModal from '../components/CenteredModal.js'; +import MobileDrawer from '../components/MobileDrawer.js'; +import ResponsiveFormGrid from '../components/ResponsiveFormGrid.js'; import SiteBadgeLink from '../components/SiteBadgeLink.js'; import { MobileCard, MobileField } from '../components/MobileCard.js'; import ResponsiveFilterPanel from '../components/ResponsiveFilterPanel.js'; @@ -86,6 +89,62 @@ const DEFAULT_PROXY_DEBUG_SETTINGS: ProxyDebugSettingsState = { proxyDebugRetentionHours: 24, proxyDebugMaxBodyBytes: 262144, }; +const DEBUG_REFRESH_INTERVAL_MS = 2000; +const debugInputStyle: React.CSSProperties = { + width: '100%', + padding: '10px 14px', + border: '1px solid var(--color-border)', + borderRadius: 'var(--radius-sm)', + fontSize: 13, + outline: 'none', + background: 'var(--color-bg)', + color: 'var(--color-text-primary)', +}; +const debugSectionCardStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 12, + padding: 14, + border: '1px solid var(--color-border-light)', + borderRadius: 'var(--radius-md)', + background: 'var(--color-bg-card)', +}; +const debugSectionLabelStyle: React.CSSProperties = { + fontSize: 12, + fontWeight: 600, + color: 'var(--color-text-secondary)', + letterSpacing: '0.02em', +}; +const debugToggleCardStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'flex-start', + gap: 10, + padding: '12px 14px', + border: '1px solid var(--color-border)', + borderRadius: 'var(--radius-sm)', + background: 'var(--color-bg)', + color: 'var(--color-text-primary)', +}; +const debugSummaryItemStyle: React.CSSProperties = { + display: 'grid', + gap: 6, + padding: 14, + border: '1px solid var(--color-border-light)', + borderRadius: 'var(--radius-md)', + background: 'var(--color-bg-card)', +}; +const debugCodeBlockStyle: React.CSSProperties = { + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + margin: 0, + padding: 12, + borderRadius: 'var(--radius-sm)', + border: '1px solid var(--color-border-light)', + background: 'var(--color-bg)', + fontSize: 12, + lineHeight: 1.5, + overflowX: 'auto', +}; function formatLatency(ms: number) { if (ms >= 1000) { @@ -322,6 +381,51 @@ function toApiTimeBoundary(value: string): string | undefined { return parsed.toISOString(); } +function normalizeProxyDebugSettings(value: any): ProxyDebugSettingsState { + return { + proxyDebugTraceEnabled: !!value?.proxyDebugTraceEnabled, + proxyDebugCaptureHeaders: value?.proxyDebugCaptureHeaders !== false, + proxyDebugCaptureBodies: !!value?.proxyDebugCaptureBodies, + proxyDebugCaptureStreamChunks: !!value?.proxyDebugCaptureStreamChunks, + proxyDebugTargetSessionId: String(value?.proxyDebugTargetSessionId || ''), + proxyDebugTargetClientKind: String(value?.proxyDebugTargetClientKind || ''), + proxyDebugTargetModel: String(value?.proxyDebugTargetModel || ''), + proxyDebugRetentionHours: Number(value?.proxyDebugRetentionHours || 24), + proxyDebugMaxBodyBytes: Number(value?.proxyDebugMaxBodyBytes || 262144), + }; +} + +function buildProxyDebugSettingsPayload(settings: ProxyDebugSettingsState) { + return { + proxyDebugTraceEnabled: settings.proxyDebugTraceEnabled, + proxyDebugCaptureHeaders: settings.proxyDebugCaptureHeaders, + proxyDebugCaptureBodies: settings.proxyDebugCaptureBodies, + proxyDebugCaptureStreamChunks: settings.proxyDebugCaptureStreamChunks, + proxyDebugTargetSessionId: settings.proxyDebugTargetSessionId.trim(), + proxyDebugTargetClientKind: settings.proxyDebugTargetClientKind.trim(), + proxyDebugTargetModel: settings.proxyDebugTargetModel.trim(), + proxyDebugRetentionHours: Math.max(1, Math.trunc(Number(settings.proxyDebugRetentionHours || 24))), + proxyDebugMaxBodyBytes: Math.max(1024, Math.trunc(Number(settings.proxyDebugMaxBodyBytes || 262144))), + }; +} + +function formatProxyDebugCaptureSummary(settings: ProxyDebugSettingsState) { + const parts = ['路由决策']; + if (settings.proxyDebugCaptureHeaders) parts.push('请求/响应头'); + if (settings.proxyDebugCaptureBodies) parts.push('请求/响应体'); + if (settings.proxyDebugCaptureStreamChunks) parts.push('流式分片'); + return parts.join('、'); +} + +function formatProxyDebugTargetSummary(settings: ProxyDebugSettingsState) { + const parts = [ + settings.proxyDebugTargetSessionId ? `Session ${settings.proxyDebugTargetSessionId}` : null, + settings.proxyDebugTargetClientKind ? `客户端 ${settings.proxyDebugTargetClientKind}` : null, + settings.proxyDebugTargetModel ? `模型 ${settings.proxyDebugTargetModel}` : null, + ].filter(Boolean); + return parts.length > 0 ? parts.join(',') : '不过滤,记录所有命中的新请求'; +} + export default function ProxyLogs() { const location = useLocation(); const navigate = useNavigate(); @@ -345,16 +449,19 @@ export default function ProxyLogs() { const [sites, setSites] = useState>([]); const [clientOptions, setClientOptions] = useState([]); const [autoRefresh, setAutoRefresh] = useState(false); - const [showDebugPanel, setShowDebugPanel] = useState(false); + const [showDebugSettingsModal, setShowDebugSettingsModal] = useState(false); const [debugPanelLoading, setDebugPanelLoading] = useState(false); const [debugPanelSaving, setDebugPanelSaving] = useState(false); const [debugSettings, setDebugSettings] = useState(DEFAULT_PROXY_DEBUG_SETTINGS); + const [debugDraftSettings, setDebugDraftSettings] = useState(DEFAULT_PROXY_DEBUG_SETTINGS); const [debugTraces, setDebugTraces] = useState([]); const [selectedDebugTraceId, setSelectedDebugTraceId] = useState(null); const [debugDetailById, setDebugDetailById] = useState>({}); const isMobile = useIsMobile(768); const toast = useToast(); const loadSeq = useRef(0); + const selectedDebugTraceIdRef = useRef(null); + const debugDetailByIdRef = useRef>({}); const fromApiBoundary = toApiTimeBoundary(fromInput); const toApiBoundaryValue = toApiTimeBoundary(toInput); const hasInvalidTimeRange = Boolean( @@ -540,6 +647,14 @@ export default function ProxyLogs() { )); }, [logs]); + useEffect(() => { + selectedDebugTraceIdRef.current = selectedDebugTraceId; + }, [selectedDebugTraceId]); + + useEffect(() => { + debugDetailByIdRef.current = debugDetailById; + }, [debugDetailById]); + const loadDetail = useCallback(async (id: number) => { const existing = detailById[id]; if (existing?.loading || existing?.data) return; @@ -565,45 +680,19 @@ export default function ProxyLogs() { } }, [detailById, toast]); - const loadDebugPanel = useCallback(async (silent = false) => { - if (!silent) setDebugPanelLoading(true); - try { - const [runtimeSettings, traceResponse] = await Promise.all([ - api.getRuntimeSettings(), - api.getProxyDebugTraces({ limit: 20 }), - ]); - setDebugSettings({ - proxyDebugTraceEnabled: !!runtimeSettings?.proxyDebugTraceEnabled, - proxyDebugCaptureHeaders: runtimeSettings?.proxyDebugCaptureHeaders !== false, - proxyDebugCaptureBodies: !!runtimeSettings?.proxyDebugCaptureBodies, - proxyDebugCaptureStreamChunks: !!runtimeSettings?.proxyDebugCaptureStreamChunks, - proxyDebugTargetSessionId: String(runtimeSettings?.proxyDebugTargetSessionId || ''), - proxyDebugTargetClientKind: String(runtimeSettings?.proxyDebugTargetClientKind || ''), - proxyDebugTargetModel: String(runtimeSettings?.proxyDebugTargetModel || ''), - proxyDebugRetentionHours: Number(runtimeSettings?.proxyDebugRetentionHours || 24), - proxyDebugMaxBodyBytes: Number(runtimeSettings?.proxyDebugMaxBodyBytes || 262144), - }); - const items = Array.isArray(traceResponse?.items) ? traceResponse.items : []; - setDebugTraces(items); - setSelectedDebugTraceId((current) => { - if (current && items.some((item) => item.id === current)) return current; - return items[0]?.id ?? null; - }); - } catch (error: any) { - toast.error(error?.message || '加载代理调试面板失败'); - } finally { - if (!silent) setDebugPanelLoading(false); + const applyLoadedDebugSettings = useCallback((nextSettings: ProxyDebugSettingsState, options?: { syncDraft?: boolean }) => { + setDebugSettings(nextSettings); + if (options?.syncDraft || !showDebugSettingsModal) { + setDebugDraftSettings(nextSettings); } - }, [toast]); + }, [showDebugSettingsModal]); - useEffect(() => { - if (!showDebugPanel) return; - void loadDebugPanel(); - }, [loadDebugPanel, showDebugPanel]); - - const loadDebugTraceDetail = useCallback(async (id: number) => { - const existing = debugDetailById[id]; - if (existing?.loading || existing?.data) return; + const loadDebugTraceDetail = useCallback(async ( + id: number, + options?: { force?: boolean; suppressToast?: boolean }, + ) => { + const existing = debugDetailByIdRef.current[id]; + if (!options?.force && (existing?.loading || existing?.data)) return; setDebugDetailById((current) => ({ ...current, @@ -622,48 +711,132 @@ export default function ProxyLogs() { ...current, [id]: { loading: false, error: message }, })); - toast.error(message); + if (!options?.suppressToast) { + toast.error(message); + } } - }, [debugDetailById, toast]); + }, [toast]); + + const syncDebugTraceItems = useCallback(async ( + items: ProxyDebugTraceListItem[], + options?: { refreshSelectedDetail?: boolean }, + ) => { + setDebugTraces(items); + const currentSelectedDebugTraceId = selectedDebugTraceIdRef.current; + const nextSelectedDebugTraceId = ( + currentSelectedDebugTraceId && items.some((item) => item.id === currentSelectedDebugTraceId) + ? currentSelectedDebugTraceId + : items[0]?.id ?? null + ); + selectedDebugTraceIdRef.current = nextSelectedDebugTraceId; + setSelectedDebugTraceId(nextSelectedDebugTraceId); + if (nextSelectedDebugTraceId && options?.refreshSelectedDetail) { + await loadDebugTraceDetail(nextSelectedDebugTraceId, { + force: true, + suppressToast: true, + }); + } + }, [loadDebugTraceDetail]); + + const loadDebugTraceList = useCallback(async ( + options?: { silent?: boolean; refreshSelectedDetail?: boolean; suppressToast?: boolean }, + ) => { + if (!options?.silent) setDebugPanelLoading(true); + try { + const traceResponse = await api.getProxyDebugTraces({ limit: 20 }); + const items = Array.isArray(traceResponse?.items) ? traceResponse.items : []; + await syncDebugTraceItems(items, { refreshSelectedDetail: options?.refreshSelectedDetail }); + } catch (error: any) { + if (!options?.suppressToast) { + toast.error(error?.message || '加载代理调试追踪失败'); + } + } finally { + if (!options?.silent) setDebugPanelLoading(false); + } + }, [syncDebugTraceItems, toast]); + + const loadDebugState = useCallback(async (silent = false) => { + if (!silent) setDebugPanelLoading(true); + try { + const [runtimeSettings, traceResponse] = await Promise.all([ + api.getRuntimeSettings(), + api.getProxyDebugTraces({ limit: 20 }), + ]); + applyLoadedDebugSettings(normalizeProxyDebugSettings(runtimeSettings), { syncDraft: true }); + const items = Array.isArray(traceResponse?.items) ? traceResponse.items : []; + await syncDebugTraceItems(items, { refreshSelectedDetail: true }); + } catch (error: any) { + toast.error(error?.message || '加载代理调试面板失败'); + } finally { + if (!silent) setDebugPanelLoading(false); + } + }, [applyLoadedDebugSettings, syncDebugTraceItems, toast]); useEffect(() => { - if (!showDebugPanel || !selectedDebugTraceId) return; - void loadDebugTraceDetail(selectedDebugTraceId); - }, [loadDebugTraceDetail, selectedDebugTraceId, showDebugPanel]); + void loadDebugState(); + }, [loadDebugState]); - const handleSaveDebugSettings = useCallback(async () => { + useEffect(() => { + if (!selectedDebugTraceId) return; + void loadDebugTraceDetail(selectedDebugTraceId); + }, [loadDebugTraceDetail, selectedDebugTraceId]); + + useEffect(() => { + if (!debugSettings.proxyDebugTraceEnabled) return; + const timer = setInterval(() => { + void loadDebugTraceList({ + silent: true, + refreshSelectedDetail: true, + suppressToast: true, + }); + }, DEBUG_REFRESH_INTERVAL_MS); + return () => clearInterval(timer); + }, [debugSettings.proxyDebugTraceEnabled, loadDebugTraceList]); + + const persistDebugSettings = useCallback(async ( + nextSettings: ProxyDebugSettingsState, + options?: { successMessage?: string; closeAfterSave?: boolean }, + ) => { setDebugPanelSaving(true); try { - const updated = await api.updateRuntimeSettings({ - proxyDebugTraceEnabled: debugSettings.proxyDebugTraceEnabled, - proxyDebugCaptureHeaders: debugSettings.proxyDebugCaptureHeaders, - proxyDebugCaptureBodies: debugSettings.proxyDebugCaptureBodies, - proxyDebugCaptureStreamChunks: debugSettings.proxyDebugCaptureStreamChunks, - proxyDebugTargetSessionId: debugSettings.proxyDebugTargetSessionId.trim(), - proxyDebugTargetClientKind: debugSettings.proxyDebugTargetClientKind.trim(), - proxyDebugTargetModel: debugSettings.proxyDebugTargetModel.trim(), - proxyDebugRetentionHours: Math.max(1, Math.trunc(Number(debugSettings.proxyDebugRetentionHours || 24))), - proxyDebugMaxBodyBytes: Math.max(1024, Math.trunc(Number(debugSettings.proxyDebugMaxBodyBytes || 262144))), + const updated = await api.updateRuntimeSettings(buildProxyDebugSettingsPayload(nextSettings)); + const normalized = normalizeProxyDebugSettings(updated); + applyLoadedDebugSettings(normalized, { syncDraft: true }); + if (options?.closeAfterSave) { + setShowDebugSettingsModal(false); + } + if (options?.successMessage) { + toast.success(options.successMessage); + } + await loadDebugTraceList({ + silent: true, + refreshSelectedDetail: true, + suppressToast: true, }); - setDebugSettings({ - proxyDebugTraceEnabled: !!updated?.proxyDebugTraceEnabled, - proxyDebugCaptureHeaders: updated?.proxyDebugCaptureHeaders !== false, - proxyDebugCaptureBodies: !!updated?.proxyDebugCaptureBodies, - proxyDebugCaptureStreamChunks: !!updated?.proxyDebugCaptureStreamChunks, - proxyDebugTargetSessionId: String(updated?.proxyDebugTargetSessionId || ''), - proxyDebugTargetClientKind: String(updated?.proxyDebugTargetClientKind || ''), - proxyDebugTargetModel: String(updated?.proxyDebugTargetModel || ''), - proxyDebugRetentionHours: Number(updated?.proxyDebugRetentionHours || 24), - proxyDebugMaxBodyBytes: Number(updated?.proxyDebugMaxBodyBytes || 262144), - }); - toast.success('代理调试设置已保存'); - await loadDebugPanel(true); + return normalized; } catch (error: any) { toast.error(error?.message || '保存代理调试设置失败'); + return null; } finally { setDebugPanelSaving(false); } - }, [debugSettings, loadDebugPanel, toast]); + }, [applyLoadedDebugSettings, loadDebugTraceList, toast]); + + const handleSaveDebugSettings = useCallback(async () => { + await persistDebugSettings(debugDraftSettings, { + successMessage: '代理调试设置已保存', + closeAfterSave: true, + }); + }, [debugDraftSettings, persistDebugSettings]); + + const handleQuickToggleDebugTrace = useCallback(async () => { + await persistDebugSettings({ + ...debugSettings, + proxyDebugTraceEnabled: !debugSettings.proxyDebugTraceEnabled, + }, { + successMessage: debugSettings.proxyDebugTraceEnabled ? '代理调试追踪已关闭' : '代理调试追踪已开启', + }); + }, [debugSettings, persistDebugSettings]); const handleToggleExpand = useCallback((id: number) => { const shouldExpand = expanded !== id; @@ -773,6 +946,187 @@ export default function ProxyLogs() { ); + const latestDebugTrace = debugTraces[0] || null; + const debugStatusBadgeStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + gap: 6, + padding: '4px 10px', + borderRadius: 999, + fontSize: 12, + fontWeight: 600, + border: '1px solid var(--color-border-light)', + color: debugSettings.proxyDebugTraceEnabled ? 'var(--color-success)' : 'var(--color-text-secondary)', + background: debugSettings.proxyDebugTraceEnabled + ? 'color-mix(in srgb, var(--color-success) 10%, transparent)' + : 'var(--color-bg-card)', + }; + const debugSettingsFooter = ( +
+ + +
+ ); + const debugSettingsForm = ( +
+
+ 这里只会显示开启后产生的新请求。开启后可记录路由选择、上游 endpoint 尝试、原始请求头/响应头,以及你勾选的请求体和响应体。 +
+ + +
+
记录内容
+ + + + +
+ +
+
定向过滤
+ + + +
+ +
+
保留策略
+ + +
+ +
+
当前摘要
+
+
状态:{debugSettings.proxyDebugTraceEnabled ? '已开启' : '未开启'}
+
记录内容:{formatProxyDebugCaptureSummary(debugDraftSettings)}
+
过滤范围:{formatProxyDebugTargetSummary(debugDraftSettings)}
+
最近追踪:{latestDebugTrace ? formatDateTimeLocal(latestDebugTrace.createdAt) : '暂无'}
+
+
+
+ + {isMobile ? debugSettingsFooter : null} +
+ ); + return (
@@ -789,14 +1143,6 @@ export default function ProxyLogs() {
-
- -
- - - - - - - - - -
-
+
+
-
+
+
+
记录内容
+
+ {formatProxyDebugCaptureSummary(debugSettings)} +
+
+
+
过滤范围
+
+ {formatProxyDebugTargetSummary(debugSettings)} +
+
+
+
最近追踪数
+
+ {debugTraces.length} +
+
+
+
最新一条
+
+ {latestDebugTrace ? formatDateTimeLocal(latestDebugTrace.createdAt) : '暂无追踪'} +
+
+
+
+ +
+
+
+
最近调试追踪
+
+ {debugSettings.proxyDebugTraceEnabled ? '开启中,结果会自动刷新' : '尚未开启'} +
+
+ + {debugPanelLoading && debugTraces.length === 0 ? ( +
加载调试追踪中...
+ ) : debugTraces.length === 0 ? ( +
+ {debugSettings.proxyDebugTraceEnabled + ? '暂时还没有新追踪。这里只显示开启后产生的新请求,等下一次代理请求进入就会出现在这里。' + : '调试追踪尚未开启。点击上方“开启调试”或“调试设置”后,新的代理请求会出现在这里。'} +
+ ) : (
-
最近调试追踪
- {debugTraces.length === 0 ? ( -
- 暂无调试追踪,开启后等待新请求进入即可。 -
- ) : ( - debugTraces.map((trace) => ( - - )) - )} -
- -
-
追踪详情
- {!selectedDebugTraceId ? ( -
- 选择左侧一条追踪后查看完整的路由选择、请求头和响应体。 -
- ) : selectedDebugTraceDetail?.loading ? ( -
加载追踪详情中...
- ) : selectedDebugTraceDetail?.error ? ( -
{selectedDebugTraceDetail.error}
- ) : selectedDebugTraceDetail?.data ? ( -
-
-
下游路径:{selectedDebugTraceDetail.data.trace.downstreamPath || '-'}
-
Session:{selectedDebugTraceDetail.data.trace.sessionId || '-'}
-
模型:{selectedDebugTraceDetail.data.trace.requestedModel || '-'}
-
候选 endpoint:{selectedDebugTraceDetail.data.trace.endpointCandidatesJson || '-'}
-
最终上游路径:{selectedDebugTraceDetail.data.trace.finalUpstreamPath || '-'}
+
+ {trace.requestedModel || '-'} · {trace.downstreamPath} +
+
+ {trace.finalStatus || 'pending'} · {trace.finalUpstreamPath || '-'} +
-
- 原始下游请求头 -
{selectedDebugTraceDetail.data.trace.requestHeadersJson || '-'}
-
-
- 原始下游请求体 -
{selectedDebugTraceDetail.data.trace.requestBodyJson || '-'}
-
-
- 最终响应 -
{selectedDebugTraceDetail.data.trace.finalResponseBodyJson || '-'}
-
-
-
Attempt 记录
- {selectedDebugTraceDetail.data.attempts.length === 0 ? ( -
暂无 attempt 记录
- ) : ( - selectedDebugTraceDetail.data.attempts.map((attempt) => ( -
- - #{attempt.attemptIndex + 1} · {attempt.endpoint} · {attempt.responseStatus ?? '-'} · {attempt.requestPath} - -
+                
+              ))}
+            
+ )} +
+ +
+
+
追踪详情
+
+ 选择左侧一条追踪后查看完整请求与 attempt 链路 +
+
+ + {!selectedDebugTraceId ? ( +
+ 暂无追踪详情。开启调试后等待新请求,或从左侧选择一条最近追踪。 +
+ ) : selectedDebugTraceDetail?.loading ? ( +
加载追踪详情中...
+ ) : selectedDebugTraceDetail?.error ? ( +
{selectedDebugTraceDetail.error}
+ ) : selectedDebugTraceDetail?.data ? ( +
+
+
基础信息
+
+
下游路径:{selectedDebugTraceDetail.data.trace.downstreamPath || '-'}
+
Session:{selectedDebugTraceDetail.data.trace.sessionId || '-'}
+
模型:{selectedDebugTraceDetail.data.trace.requestedModel || '-'}
+
候选 endpoint:{selectedDebugTraceDetail.data.trace.endpointCandidatesJson || '-'}
+
最终上游路径:{selectedDebugTraceDetail.data.trace.finalUpstreamPath || '-'}
+
+
+ +
+
+ 原始下游请求头 +
{selectedDebugTraceDetail.data.trace.requestHeadersJson || '-'}
+
+
+ 原始下游请求体 +
{selectedDebugTraceDetail.data.trace.requestBodyJson || '-'}
+
+
+ 最终响应 +
{selectedDebugTraceDetail.data.trace.finalResponseBodyJson || '-'}
+
+
+ +
+
Attempt 记录
+ {selectedDebugTraceDetail.data.attempts.length === 0 ? ( +
暂无 attempt 记录
+ ) : ( + selectedDebugTraceDetail.data.attempts.map((attempt) => ( +
+ + #{attempt.attemptIndex + 1} · {attempt.endpoint} · {attempt.responseStatus ?? '-'} · {attempt.requestPath} + +
 {`targetUrl: ${attempt.targetUrl}
 runtimeExecutor: ${attempt.runtimeExecutor || '-'}
 recoverApplied: ${attempt.recoverApplied ? 'true' : 'false'}
@@ -1060,21 +1406,51 @@ ${attempt.rawErrorText || '-'}
 
 memoryWrite:
 ${attempt.memoryWriteJson || '-'}`}
-                          
-
- )) - )} -
-
- ) : ( -
- 暂无追踪详情。 -
- )} + + + )) + )} +
-
+ ) : ( +
+ 暂无追踪详情。 +
+ )}
- ) : null} + + + {isMobile ? ( + { + setShowDebugSettingsModal(false); + setDebugDraftSettings(debugSettings); + }} + title="调试设置" + closeLabel="关闭调试设置" + side="right" + > +
+ {debugSettingsForm} +
+
+ ) : ( + { + setShowDebugSettingsModal(false); + setDebugDraftSettings(debugSettings); + }} + title="调试设置" + footer={debugSettingsFooter} + maxWidth={880} + closeOnBackdrop + closeOnEscape + > + {debugSettingsForm} + + )} {hasInvalidTimeRange && (