From df39d5a16f777d7e54756e617419f43eaf55c4c3 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:46:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E4=B8=8E=E9=94=99=E8=AF=AF=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 客户端 debug 保留服务器选择,正式包按发布渠道连接服务 修复错误报告时间解析与超时路由脱敏 重做后台错误详情面板并补齐定向测试 同步渠道、诊断与后台展示文档 --- apps/admin-web/src/api/adminApiTypes.ts | 2 + .../src/pages/AdminErrorReportsPage.test.tsx | 18 + .../src/pages/AdminErrorReportsPage.tsx | 298 +++++++++++--- apps/admin-web/src/styles/admin.css | 371 ++++++++++++++++++ .../scripts/build-release.mjs | 12 +- .../scripts/build-release.test.mjs | 15 + .../src-tauri/src/error_report/sanitize.rs | 33 +- .../src/app/AuthenticatedClient.tsx | 95 ++++- .../src/services/clientAuth.ts | 14 +- .../src/services/clientHttp.ts | 148 ++++++- .../src/services/errorReporting.ts | 23 +- .../tests/appSurface/auth.suite.ts | 13 +- .../tests/clientAuthStorage.test.ts | 16 +- .../tests/clientHttp.test.ts | 60 +-- .../tests/errorReporting.test.ts | 10 + .../shared-memory/team-conventions.md | 2 +- ...方案】AGC客户端更新检查与下载-2026-08-31.md | 10 +- ...术方案】AGC错误报告与诊断上传-2026-08-31.md | 4 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 19 files changed, 1022 insertions(+), 124 deletions(-) create mode 100644 apps/admin-web/src/pages/AdminErrorReportsPage.test.tsx diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 452ad30f3..1a1c6f668 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -144,6 +144,8 @@ export interface AdminErrorReportListResponse { } export interface AdminErrorReportDetail extends AdminErrorReportEntry { + firstFingerprint?: string; + firstSource?: string; note?: string; events: Array>; logNames: string[]; diff --git a/apps/admin-web/src/pages/AdminErrorReportsPage.test.tsx b/apps/admin-web/src/pages/AdminErrorReportsPage.test.tsx new file mode 100644 index 000000000..2c945abbe --- /dev/null +++ b/apps/admin-web/src/pages/AdminErrorReportsPage.test.tsx @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; + +import { + formatAdminTimestamp, + parseAdminTimestamp, +} from './AdminErrorReportsPage'; + +describe('错误报告时间格式化', () => { + it('解析 SpacetimeDB seconds.microsZ 时间', () => { + expect(parseAdminTimestamp('1778207451.731746Z')).toBe(1778207451731); + }); + + it('解析微秒字符串并拒绝无效时间', () => { + expect(parseAdminTimestamp('1778207451731746')).toBe(1778207451731); + expect(formatAdminTimestamp('not-a-date')).toBe('-'); + expect(formatAdminTimestamp('999999999999999999999999')).toBe('-'); + }); +}); diff --git a/apps/admin-web/src/pages/AdminErrorReportsPage.tsx b/apps/admin-web/src/pages/AdminErrorReportsPage.tsx index fbb5f8650..da2f7ebba 100644 --- a/apps/admin-web/src/pages/AdminErrorReportsPage.tsx +++ b/apps/admin-web/src/pages/AdminErrorReportsPage.tsx @@ -17,6 +17,57 @@ type Props = { token: string; onUnauthorized: (message?: string) => void }; const ADMIN_ERROR_REPORT_STATUSES = ['new', 'in-progress', 'resolved'] as const; const ERROR_REPORT_PAGE_SIZE = 50; +const ERROR_REPORT_STATUS_LABELS: Record = { + new: '待处理', + 'in-progress': '处理中', + resolved: '已解决', +}; + +export function parseAdminTimestamp(value: string | null | undefined) { + const normalized = value?.trim() ?? ''; + if (/^-?\d+\.\d{6}Z$/u.test(normalized)) { + const [secondsText, microsText] = normalized.slice(0, -1).split('.'); + const seconds = Number(secondsText); + const micros = Number(microsText); + return Number.isFinite(seconds) && Number.isFinite(micros) + ? seconds * 1000 + Math.floor(micros / 1000) + : Number.NaN; + } + if (/^-?\d+$/u.test(normalized)) { + const numeric = Number(normalized); + if (!Number.isFinite(numeric)) return Number.NaN; + if (Math.abs(numeric) >= 1e14) return Math.floor(numeric / 1000); + if (Math.abs(numeric) >= 1e11) return numeric; + return numeric * 1000; + } + return Date.parse(normalized); +} + +export function formatAdminTimestamp(value: string | null | undefined) { + const timestamp = parseAdminTimestamp(value); + if (!Number.isFinite(timestamp)) return '-'; + const date = new Date(timestamp); + if (!Number.isFinite(date.getTime())) return '-'; + try { + return new Intl.DateTimeFormat('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).format(date); + } catch { + return '-'; + } +} + +function eventText(event: Record, key: string) { + const value = event[key]; + return typeof value === 'string' || typeof value === 'number' + ? String(value) + : ''; +} export function AdminErrorReportsPage({ token, onUnauthorized }: Props) { const [reports, setReports] = useState([]); @@ -29,6 +80,24 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) { const [pageInfo, setPageInfo] = useState({ total: 0, hasMore: false }); const openReportRequestId = useRef(0); const loadRequestId = useRef(0); + const detailCloseButtonRef = useRef(null); + + useEffect(() => { + if (!selected) return; + const previousFocus = document.activeElement as HTMLElement | null; + const focusFrame = window.requestAnimationFrame(() => { + detailCloseButtonRef.current?.focus(); + }); + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setSelected(null); + }; + window.addEventListener('keydown', handleEscape); + return () => { + window.cancelAnimationFrame(focusFrame); + window.removeEventListener('keydown', handleEscape); + previousFocus?.focus?.(); + }; + }, [selected]); const load = useCallback(async () => { const requestId = ++loadRequestId.current; @@ -184,9 +253,15 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) { {report.batchId} {report.eventCount} {report.source ?? '-'} - {report.status} + + + {ERROR_REPORT_STATUS_LABELS[report.status] ?? report.status} + + {report.userId} - {new Date(report.createdAt).toLocaleString()} + {formatAdminTimestamp(report.createdAt)} ))} @@ -224,66 +299,173 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) { className="admin-detail-modal" role="dialog" aria-label="错误报告详情" + aria-modal="true" + onMouseDown={(event) => { + if (event.target === event.currentTarget) setSelected(null); + }} > -
-
-

{selected.batchId}

-
-

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

-
{JSON.stringify(selected.events.slice(0, 20), null, 2)}
- {selected.userDescription ? ( -

用户描述:{selected.userDescription}

- ) : null} -