From 4671c5fb5202bc099650a3768d1bf95514636e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 1 Sep 2026 10:11:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=8E=E5=8F=B0=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8A=A5=E5=91=8A=E6=9F=A5=E7=9C=8B=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加错误报告列表、详情和状态筛选页面。 支持管理员受控下载诊断包并接入错误报告 Tab 权限。 补充前端 API 类型、客户端请求和详情弹窗样式。 --- apps/admin-web/src/api/adminApiClient.ts | 70 +++++++ apps/admin-web/src/api/adminApiTypes.ts | 29 +++ apps/admin-web/src/app/AdminApp.tsx | 7 + apps/admin-web/src/app/AdminShell.tsx | 1 + apps/admin-web/src/app/adminRoutes.ts | 2 + .../src/pages/AdminErrorReportsPage.tsx | 179 ++++++++++++++++++ apps/admin-web/src/styles/admin.css | 20 ++ 7 files changed, 308 insertions(+) create mode 100644 apps/admin-web/src/pages/AdminErrorReportsPage.tsx diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index 9912b9cf7..52491b61c 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -271,6 +271,59 @@ export function listAdminTrackingEventKeys(token: string) { ); } +export function listAdminErrorReports( + token: string, + query: { + status?: string; + fingerprint?: string; + source?: string; + limit?: number; + } = {}, +) { + return request( + `/admin/api/error-reports${buildErrorReportQueryString(query)}`, + { token }, + ); +} + +export function getAdminErrorReport(token: string, batchId: string) { + return request( + `/admin/api/error-reports/${encodeURIComponent(batchId)}`, + { token }, + ); +} + +export function updateAdminErrorReport( + token: string, + batchId: string, + payload: { status: string; note?: string }, +) { + return request( + `/admin/api/error-reports/${encodeURIComponent(batchId)}`, + { method: 'PATCH', token, body: payload }, + ); +} + +export function getAdminErrorReportDownloadUrl(batchId: string) { + return `/admin/api/error-reports/${encodeURIComponent(batchId)}/download`; +} + +export async function downloadAdminErrorReport(token: string, batchId: string) { + const response = await fetch( + buildRequestUrl(getAdminErrorReportDownloadUrl(batchId)), + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); + if (!response.ok) { + throw new AdminApiError({ + message: `下载失败(${response.status})`, + status: response.status, + }); + } + return response.blob(); +} + export function getAdminFeatureGateConfig(token: string) { return request('/admin/api/feature-gates', { token, @@ -806,6 +859,23 @@ function buildQueryString(query: AdminTrackingEventListQuery) { return queryString ? `?${queryString}` : ''; } +function buildErrorReportQueryString(query: { + status?: string; + fingerprint?: string; + source?: string; + limit?: number; +}) { + const params = new URLSearchParams(); + appendQueryParam(params, 'status', query.status); + appendQueryParam(params, 'fingerprint', query.fingerprint); + appendQueryParam(params, 'source', query.source); + if (typeof query.limit === 'number' && Number.isFinite(query.limit)) { + params.set('limit', String(query.limit)); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ''; +} + function buildAdminRechargeOrderListQuery(query: AdminRechargeOrderListQuery) { const params = new URLSearchParams(); appendQueryParam(params, 'orderId', query.orderId); diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index bcd2ee1db..59efb2b56 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -96,6 +96,35 @@ export interface AdminMeResponse { admin: AdminSessionPayload; } +export interface AdminErrorReportEntry { + batchId: string; + eventCount: number; + logCount: number; + fingerprint?: string; + source?: string; + status: 'new' | 'in-progress' | 'resolved'; + userId: string; + createdAt: string; + updatedAt: string; + userDescription?: string; + attachmentSizeBytes: number; + submissionId?: string; + ossObjectKey?: string; + archiveSha256?: string; + uploadStatus?: 'uploading' | 'ready' | 'failed'; + reviewStatus?: 'new' | 'in-progress' | 'resolved'; +} + +export interface AdminErrorReportListResponse { + reports: AdminErrorReportEntry[]; +} + +export interface AdminErrorReportDetail extends AdminErrorReportEntry { + note?: string; + events: Array>; + logNames: string[]; +} + export interface AdminOverviewResponse { service: AdminServiceOverviewPayload; database: AdminDatabaseOverviewPayload; diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index de33c8761..a717f146b 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -24,6 +24,7 @@ import { AdminDebugHttpPage } from '../pages/AdminDebugHttpPage'; import { AdminEditorAssetQueryPage } from '../pages/AdminEditorAssetQueryPage'; import { AdminEditorGenerationPricingPage } from '../pages/AdminEditorGenerationPricingPage'; import { AdminEditorShowcaseReviewPage } from '../pages/AdminEditorShowcaseReviewPage'; +import { AdminErrorReportsPage } from '../pages/AdminErrorReportsPage'; import { AdminGrayReleaseConfigPage } from '../pages/AdminGrayReleaseConfigPage'; import { AdminInviteCodePage } from '../pages/AdminInviteCodePage'; import { AdminLoginPage } from '../pages/AdminLoginPage'; @@ -228,6 +229,12 @@ export function AdminApp() { onUnauthorized={handleUnauthorized} /> ) : null} + {activeRouteId === 'error-reports' ? ( + + ) : null} {activeRouteId === 'gray-release' ? ( void }; + +export function AdminErrorReportsPage({ token, onUnauthorized }: Props) { + const [reports, setReports] = useState([]); + const [selected, setSelected] = useState(null); + const [status, setStatus] = useState(''); + const [busy, setBusy] = useState(false); + const [filterStatus, setFilterStatus] = useState(''); + + const load = useCallback(async () => { + try { + const response = await listAdminErrorReports(token, { + status: filterStatus || undefined, + }); + setReports(response.reports); + } catch (error) { + if (isAdminApiError(error) && error.status === 401) + return onUnauthorized(); + setStatus(formatAdminApiError(error)); + } + }, [filterStatus, onUnauthorized, token]); + + useEffect(() => { + void load(); + }, [load]); + + async function openReport(batchId: string) { + try { + setSelected(await getAdminErrorReport(token, batchId)); + } catch (error) { + if (isAdminApiError(error) && error.status === 401) + return onUnauthorized(); + setStatus(formatAdminApiError(error)); + } + } + + async function saveStatus(nextStatus: string) { + if (!selected || busy) return; + setBusy(true); + try { + setSelected( + await updateAdminErrorReport(token, selected.batchId, { + status: nextStatus, + note: selected.note, + }), + ); + await load(); + } catch (error) { + if (isAdminApiError(error) && error.status === 401) onUnauthorized(); + else setStatus(formatAdminApiError(error)); + } finally { + setBusy(false); + } + } + + async function download(batchId: string) { + try { + const blob = await downloadAdminErrorReport(token, batchId); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = `${batchId}.zip`; + anchor.click(); + URL.revokeObjectURL(url); + } catch (error) { + if (isAdminApiError(error) && error.status === 401) onUnauthorized(); + else setStatus(formatAdminApiError(error)); + } + } + + return ( +
+
+
+

错误报告

+

查看用户提交的脱敏诊断包。

+
+ +
+ {status ?

{status}

: null} +
+ + + + + + + + + + + + + {reports.map((report) => ( + void openReport(report.batchId)} + > + + + + + + + + ))} + +
报告事件来源状态用户时间
{report.batchId}{report.eventCount}{report.source ?? '-'}{report.status}{report.userId}{new Date(report.createdAt).toLocaleString()}
+
+ {selected ? ( +
+
+
+

{selected.batchId}

+ +
+

+ 用户:{selected.userId} · 事件:{selected.eventCount} · 日志: + {selected.logCount} +

+
{JSON.stringify(selected.events, null, 2)}
+ {selected.userDescription ? ( +

用户描述:{selected.userDescription}

+ ) : null} +
+ + +
+
+
+ ) : null} +
+ ); +} diff --git a/apps/admin-web/src/styles/admin.css b/apps/admin-web/src/styles/admin.css index 719ecf714..8fd8c297d 100644 --- a/apps/admin-web/src/styles/admin.css +++ b/apps/admin-web/src/styles/admin.css @@ -2449,3 +2449,23 @@ button:disabled { white-space: nowrap; } } +.admin-detail-modal { + position: fixed; + inset: 0; + z-index: 20; + display: grid; + place-items: center; + padding: 20px; + background: rgb(15 23 42 / 35%); +} +.admin-detail-modal__panel { + width: min(860px, 100%); + max-height: 90vh; + overflow: auto; + border-radius: 14px; + padding: 20px; + background: var(--admin-surface, #fff); +} +.admin-detail-modal__panel header, +.admin-detail-modal__actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.admin-detail-modal__panel pre { max-height: 360px; overflow: auto; white-space: pre-wrap; background: #f8fafc; padding: 12px; border-radius: 8px; }