merge: candidate error-code-observability-2026-09-28 into dev

This commit is contained in:
weichen
2026-09-28 02:37:48 +00:00
79 changed files with 2442 additions and 176 deletions
+2
View File
@@ -44,6 +44,7 @@ export type StreamEvent = {
session_id?: string;
workspace_id?: string;
actions?: string[];
guide?: string;
};
turn?: {
client_turn_id?: string;
@@ -280,6 +281,7 @@ export function mapErrorInfo(ev: StreamEvent): ChatErrorInfo {
requestId: ei.request_id,
sessionId: ei.session_id,
workspaceId: ei.workspace_id,
guide: ei.guide,
actions,
};
}
+26
View File
@@ -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
+20
View File
@@ -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;
}
+1
View File
@@ -160,6 +160,7 @@ export interface ChatErrorInfo {
requestId?: string;
sessionId?: string;
workspaceId?: string;
guide?: string;
actions: Array<'retry' | 'continue' | 'restart'>;
}