feat(web): 增加错误审计查询和失败排查指引
管理台可按错误码或 request id 检索审计记录,聊天失败气泡展示对应排查短句。
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<ErrorEventRecord[]> {
|
||||
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 ?? [];
|
||||
}
|
||||
|
||||
@@ -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: <Database size={16} /> },
|
||||
{ key: 'features', label: '特性功能开关', icon: <ToggleLeft size={16} /> },
|
||||
{ key: 'settings', label: '系统设置白名单', icon: <Sliders size={16} /> },
|
||||
{ key: 'errors', label: '错误审计', icon: <ScrollText size={16} /> },
|
||||
{ key: 'env', label: '模型与密钥', icon: <KeyRound size={16} /> },
|
||||
{ key: 'maintenance', label: '维护计划', icon: <Wrench size={16} /> },
|
||||
{ key: 'workflows', label: '工作流定义', icon: <GitBranch size={16} /> },
|
||||
@@ -103,6 +107,8 @@ export const AdminConsoleView: React.FC<AdminConsoleViewProps> = ({
|
||||
return <FeaturesSection />;
|
||||
case 'settings':
|
||||
return <SettingsSection />;
|
||||
case 'errors':
|
||||
return <ErrorEventsSection />;
|
||||
case 'env':
|
||||
return <EnvSettingsSection />;
|
||||
case 'maintenance':
|
||||
@@ -115,6 +121,10 @@ export const AdminConsoleView: React.FC<AdminConsoleViewProps> = ({
|
||||
return <UsersSection currentUser={currentUser} onRefreshUsers={onUsersChanged} />;
|
||||
case 'docs':
|
||||
return <DocsSection />;
|
||||
default: {
|
||||
const _exhaustive: never = activeSection;
|
||||
return _exhaustive;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<ErrorEventQuery>({ limit: 50 });
|
||||
const [draft, setDraft] = useState({ code: '', requestId: '', className: '' });
|
||||
const [rows, setRows] = useState<ErrorEventRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<SectionHeader
|
||||
title="错误审计"
|
||||
desc="按错误码或 request id 检索 HTTP 与异步失败。排查步骤见错误码目录,不在这里展示密钥或 SQL。"
|
||||
/>
|
||||
<div style={{ ...sectionCardStyle, display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<input
|
||||
aria-label="错误码"
|
||||
placeholder="错误码,如 DOC_BUSY"
|
||||
value={draft.code}
|
||||
onChange={(e) => 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)' }}
|
||||
/>
|
||||
<input
|
||||
aria-label="request id"
|
||||
placeholder="request id"
|
||||
value={draft.requestId}
|
||||
onChange={(e) => 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' }}
|
||||
/>
|
||||
<input
|
||||
aria-label="错误类别"
|
||||
placeholder="class:transient / fatal"
|
||||
value={draft.className}
|
||||
onChange={(e) => 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)' }}
|
||||
/>
|
||||
<button type="button" onClick={onSearch} style={{ padding: '8px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--brand-500, #4f6bff)', color: '#fff', cursor: 'pointer' }}>
|
||||
查询
|
||||
</button>
|
||||
</div>
|
||||
{loading && <SectionLoading text="正在查询错误审计…" />}
|
||||
{error && <SectionError message={error} onRetry={() => void load(query)} />}
|
||||
{!loading && !error && (
|
||||
<div style={{ ...sectionCardStyle, padding: 0, overflow: 'auto' }}>
|
||||
<table style={sectionTableStyle}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={sectionThStyle}>时间</th>
|
||||
<th style={sectionThStyle}>错误码</th>
|
||||
<th style={sectionThStyle}>类别</th>
|
||||
<th style={sectionThStyle}>来源</th>
|
||||
<th style={sectionThStyle}>路径</th>
|
||||
<th style={sectionThStyle}>request id</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td style={sectionTdStyle} colSpan={6}>
|
||||
还没有查询结果。输入错误码或 request id 后查询。
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{rows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td style={sectionTdStyle}>{row.createdAt ? new Date(row.createdAt).toLocaleString() : '-'}</td>
|
||||
<td style={{ ...sectionTdStyle, fontFamily: 'var(--font-mono)' }}>{row.code}</td>
|
||||
<td style={sectionTdStyle}>{row.class || '-'}</td>
|
||||
<td style={sectionTdStyle}>{row.source || row.module || '-'}</td>
|
||||
<td style={sectionTdStyle}>{row.method ? `${row.method} ` : ''}{row.path || '-'}</td>
|
||||
<td style={sectionTdStyle}>
|
||||
{row.requestId ? (
|
||||
<button type="button" onClick={() => void copy(row.requestId || '')} style={{ fontFamily: 'var(--font-mono)', background: 'transparent', border: 'none', color: 'var(--brand-400, #6b84ff)', cursor: 'pointer', padding: 0 }}>
|
||||
{copied === row.requestId ? '已复制' : row.requestId}
|
||||
</button>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -382,6 +382,13 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
||||
{buildDiagLine(message.errorInfo)}
|
||||
</div>
|
||||
)}
|
||||
<details style={{ fontSize: '11.5px', color: 'var(--text-muted)' }}>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 600 }}>查看排查指引</summary>
|
||||
<div style={{ marginTop: '6px', lineHeight: 1.5 }}>
|
||||
{message.errorInfo.guide ||
|
||||
`错误码 ${message.errorInfo.code || 'UNKNOWN'}。请把错误码和 request id 交给管理员,在系统管理台「错误审计」中检索。`}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '2px' }}>
|
||||
<button
|
||||
|
||||
@@ -69,3 +69,23 @@ export interface SystemSetting {
|
||||
applyStatus?: string;
|
||||
applyError?: string;
|
||||
}
|
||||
|
||||
export interface ErrorEventRecord {
|
||||
id: number;
|
||||
code: string;
|
||||
class?: string;
|
||||
httpStatus?: number;
|
||||
requestId?: string;
|
||||
sessionId?: string;
|
||||
workspaceId?: string;
|
||||
module?: string;
|
||||
phaseId?: string;
|
||||
path?: string;
|
||||
method?: string;
|
||||
source?: string;
|
||||
upstream?: string;
|
||||
actorUserId?: number;
|
||||
retryable?: boolean;
|
||||
userMessage?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ export interface ChatErrorInfo {
|
||||
requestId?: string;
|
||||
sessionId?: string;
|
||||
workspaceId?: string;
|
||||
guide?: string;
|
||||
actions: Array<'retry' | 'continue' | 'restart'>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user