diff --git a/web/src/api/chatStream.ts b/web/src/api/chatStream.ts index e51a47a7..ddbe7831 100644 --- a/web/src/api/chatStream.ts +++ b/web/src/api/chatStream.ts @@ -43,6 +43,7 @@ export type StreamEvent = { session_id?: string; workspace_id?: string; actions?: string[]; + guide?: string; }; turn?: { client_turn_id?: string; @@ -251,6 +252,7 @@ export function mapErrorInfo(ev: StreamEvent): ChatErrorInfo { requestId: ei.request_id, sessionId: ei.session_id, workspaceId: ei.workspace_id, + guide: ei.guide, actions, }; } diff --git a/web/src/api/systemAdmin.ts b/web/src/api/systemAdmin.ts index e82d54b9..8654b9d5 100644 --- a/web/src/api/systemAdmin.ts +++ b/web/src/api/systemAdmin.ts @@ -5,6 +5,7 @@ import type { SystemKbInfo, SystemOverview, SystemSetting, + ErrorEventRecord, } from '../types/systemAdmin'; const BASE = '/api/v1/admin/system'; @@ -63,3 +64,28 @@ export async function updateSystemSetting( const data = (await resp.json()) as { setting?: SystemSetting } & SystemSetting; return data.setting ?? data; } + +export interface ErrorEventQuery { + code?: string; + requestId?: string; + className?: string; + since?: string; + until?: string; + limit?: number; +} + +export async function fetchErrorEvents(query: ErrorEventQuery = {}): Promise { + const params = new URLSearchParams(); + if (query.code) params.set('code', query.code); + if (query.requestId) params.set('request_id', query.requestId); + if (query.className) params.set('class', query.className); + if (query.since) params.set('since', query.since); + if (query.until) params.set('until', query.until); + if (query.limit) params.set('limit', String(query.limit)); + const qs = params.toString(); + const data = await getJson<{ events?: ErrorEventRecord[] }>( + `${BASE}/error-events${qs ? `?${qs}` : ''}`, + '获取错误审计失败', + ); + return data.events ?? []; +} diff --git a/web/src/components/admin/console/AdminConsoleView.tsx b/web/src/components/admin/console/AdminConsoleView.tsx index 09459835..c77715ac 100644 --- a/web/src/components/admin/console/AdminConsoleView.tsx +++ b/web/src/components/admin/console/AdminConsoleView.tsx @@ -10,6 +10,7 @@ import { ShieldAlert, ShieldX, Sliders, + ScrollText, ToggleLeft, Users, Wrench, @@ -19,6 +20,7 @@ import { UserProfile } from '../../../types/docgov'; import { RoleDefinition } from '../../../types/rbac'; import { DocsSection } from './sections/DocsSection'; import { EnvSettingsSection } from './sections/EnvSettingsSection'; +import { ErrorEventsSection } from './sections/ErrorEventsSection'; import { FeaturesSection } from './sections/FeaturesSection'; import { KbSection } from './sections/KbSection'; import { MaintenanceSection } from './sections/MaintenanceSection'; @@ -43,6 +45,7 @@ type SectionKey = | 'kb' | 'features' | 'settings' + | 'errors' | 'env' | 'maintenance' | 'workflows' @@ -55,6 +58,7 @@ const NAV_ITEMS: Array<{ key: SectionKey; label: string; icon: React.ReactNode } { key: 'kb', label: '知识库连接', icon: }, { key: 'features', label: '特性功能开关', icon: }, { key: 'settings', label: '系统设置白名单', icon: }, + { key: 'errors', label: '错误审计', icon: }, { key: 'env', label: '模型与密钥', icon: }, { key: 'maintenance', label: '维护计划', icon: }, { key: 'workflows', label: '工作流定义', icon: }, @@ -103,6 +107,8 @@ export const AdminConsoleView: React.FC = ({ return ; case 'settings': return ; + case 'errors': + return ; case 'env': return ; case 'maintenance': @@ -115,6 +121,10 @@ export const AdminConsoleView: React.FC = ({ return ; case 'docs': return ; + default: { + const _exhaustive: never = activeSection; + return _exhaustive; + } } }; diff --git a/web/src/components/admin/console/sections/ErrorEventsSection.tsx b/web/src/components/admin/console/sections/ErrorEventsSection.tsx new file mode 100644 index 00000000..abf72d17 --- /dev/null +++ b/web/src/components/admin/console/sections/ErrorEventsSection.tsx @@ -0,0 +1,133 @@ +import React, { useCallback, useState } from 'react'; +import { fetchErrorEvents, type ErrorEventQuery } from '../../../../api/systemAdmin'; +import type { ErrorEventRecord } from '../../../../types/systemAdmin'; +import { + SectionError, + SectionHeader, + SectionLoading, + sectionCardStyle, + sectionTableStyle, + sectionTdStyle, + sectionThStyle, +} from './sectionCommon'; + +export const ErrorEventsSection: React.FC = () => { + const [query, setQuery] = useState({ limit: 50 }); + const [draft, setDraft] = useState({ code: '', requestId: '', className: '' }); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(''); + + const load = useCallback(async (next: ErrorEventQuery) => { + setLoading(true); + setError(null); + try { + setRows(await fetchErrorEvents(next)); + } catch (err) { + setError(err instanceof Error ? err.message : '获取错误审计失败'); + } finally { + setLoading(false); + } + }, []); + + const onSearch = () => { + const next: ErrorEventQuery = { + limit: 50, + code: draft.code.trim() || undefined, + requestId: draft.requestId.trim() || undefined, + className: draft.className.trim() || undefined, + }; + setQuery(next); + void load(next); + }; + + const copy = async (value: string) => { + try { + await navigator.clipboard.writeText(value); + setCopied(value); + } catch { + setCopied(''); + } + }; + + return ( +
+ +
+ setDraft((d) => ({ ...d, code: e.target.value }))} + style={{ padding: '8px 10px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-app)', color: 'var(--text-strong)' }} + /> + setDraft((d) => ({ ...d, requestId: e.target.value }))} + style={{ padding: '8px 10px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-app)', color: 'var(--text-strong)', minWidth: '220px' }} + /> + setDraft((d) => ({ ...d, className: e.target.value }))} + style={{ padding: '8px 10px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-app)', color: 'var(--text-strong)' }} + /> + +
+ {loading && } + {error && void load(query)} />} + {!loading && !error && ( +
+ + + + + + + + + + + + + {rows.length === 0 && ( + + + + )} + {rows.map((row) => ( + + + + + + + + + ))} + +
时间错误码类别来源路径request id
+ 还没有查询结果。输入错误码或 request id 后查询。 +
{row.createdAt ? new Date(row.createdAt).toLocaleString() : '-'}{row.code}{row.class || '-'}{row.source || row.module || '-'}{row.method ? `${row.method} ` : ''}{row.path || '-'} + {row.requestId ? ( + + ) : ( + '-' + )} +
+
+ )} +
+ ); +}; diff --git a/web/src/components/chat/MessageBubble.tsx b/web/src/components/chat/MessageBubble.tsx index aa605899..aafd355a 100644 --- a/web/src/components/chat/MessageBubble.tsx +++ b/web/src/components/chat/MessageBubble.tsx @@ -382,6 +382,13 @@ const MessageBubbleInner: React.FC = ({ {buildDiagLine(message.errorInfo)} )} +
+ 查看排查指引 +
+ {message.errorInfo.guide || + `错误码 ${message.errorInfo.code || 'UNKNOWN'}。请把错误码和 request id 交给管理员,在系统管理台「错误审计」中检索。`} +
+