From 422c8931d63f253941e110ce51881ecd9e1ff276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 3 Sep 2026 10:02:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?Feat/AGC=E9=94=99=E8=AF=AF=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=20(#240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现: 在rust内存里维护错误事件队列, webview调用tauri command传入, 后台任务agent工具等直接插入 把rust , webview console的日志统一写到AppData文件夹下的(滚动保存的)日志文件. rust对传入的错误进行筛选,脱敏, 防抖,后通知前端提醒用户. 用户提醒是一个不阻塞的小UI, 展开后可以选择错误上报, 可以附加文字描述 上传时附带最近日志, 错误堆栈等信息 元数据存在数据库, 考虑到字符串信息很难查询, 所以在api server打包成zip存在OSS. 管理页面新增错误报告的查看页面 ![shotmd-1788328504.jpg](/attachments/641b43c8-deed-47d8-8c78-36adb9a2548f) ![shotmd-1788328495.jpg](/attachments/33c6ada3-dd4d-44b6-9859-f18f944eb653) ![shotmd-1788328222.jpg](/attachments/59267df9-f624-4e47-bb9b-b69d77f595c4) ![shotmd-1788328230.jpg](/attachments/63ea6a86-9a51-412b-a98c-dce454bcc968) --------- Co-authored-by: 段舒康 Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/240 Co-authored-by: 王德宇 Co-committed-by: 王德宇 --- apps/admin-web/src/api/adminApiClient.ts | 150 +++- apps/admin-web/src/api/adminApiTypes.ts | 32 + 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 | 288 +++++++ apps/admin-web/src/styles/admin.css | 20 + .../scripts/check-config.mjs | 6 +- .../src-tauri/Cargo.lock | 1 + .../src-tauri/Cargo.toml | 1 + .../src-tauri/src/agent/codex_app_server.rs | 6 +- .../src/agent/codex_provider_proxy.rs | 4 +- .../agent/generation/loop_orchestration.rs | 2 +- .../agent/runtime_driver/provider_recovery.rs | 2 +- .../src-tauri/src/agent/runtime_state.rs | 3 + .../src-tauri/src/commands.rs | 20 +- .../src-tauri/src/debug/debug_drafts.rs | 16 +- .../src-tauri/src/error_report/commands.rs | 32 + .../src-tauri/src/error_report/mod.rs | 8 + .../src/error_report/notifications.rs | 55 ++ .../src-tauri/src/error_report/queue.rs | 257 ++++++ .../src-tauri/src/error_report/sanitize.rs | 90 ++ .../src-tauri/src/main.rs | 106 ++- .../src/ui_editor/commands/binding.rs | 2 +- .../src-tauri/src/ui_editor/commands/merge.rs | 28 +- .../src/ui_editor/commands/recognition.rs | 28 +- .../commands/ui_design_suggestion.rs | 26 +- apps/ai-game-creator-shell/src/App.tsx | 12 +- .../src/app/AuthenticatedClient.tsx | 45 +- .../error-report/ErrorReportDialog.tsx | 209 +++++ .../error-report/ErrorReportNotice.tsx | 111 +++ .../projectDeliverySummaries.ts | 2 +- .../projectGuidanceSummaries.ts | 1 - .../projectPlaytestSummaries.ts | 8 +- .../projectQualitySummaries.ts | 2 +- .../projectSummaryConstants.ts | 1 - .../projectSummaryCommands.ts | 18 - .../src/services/clientApi.ts | 38 +- .../src/services/errorReporting.ts | 262 ++++++ .../src/services/errorReportingBridge.ts | 63 ++ .../tests/ErrorReportNotice.test.tsx | 67 ++ .../developer-agent-window.suite.ts | 1 - .../tests/appSurface/plan-gdd.suite.ts | 16 +- .../assert-playtest-and-release-shortcuts.ts | 45 +- .../assert-project-and-design-shortcuts.ts | 2 +- .../tests/errorReporting.test.ts | 232 ++++++ docs/README.md | 1 + .../shared-memory/decision-log.md | 17 + docs/project-memory/shared-memory/pitfalls.md | 7 + ...管理】多账号与Tab访问权限方案-2026-07-14.md | 21 +- ...术方案】AGC错误报告与诊断上传-2026-08-31.md | 45 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 4 +- ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 7 + server-rs/Cargo.lock | 1 + server-rs/crates/api-server/Cargo.toml | 1 + server-rs/crates/api-server/src/admin.rs | 3 + server-rs/crates/api-server/src/app.rs | 1 + server-rs/crates/api-server/src/assets.rs | 5 +- .../crates/api-server/src/error_reports.rs | 787 ++++++++++++++++++ server-rs/crates/api-server/src/main.rs | 2 + server-rs/crates/platform-oss/src/lib.rs | 110 ++- .../crates/shared-contracts/src/admin.rs | 3 +- .../shared-contracts/src/error_reports.rs | 88 ++ server-rs/crates/shared-contracts/src/lib.rs | 1 + .../crates/spacetime-client/src/active.rs | 2 + .../src/active/error_reports.rs | 89 ++ .../spacetime-client/src/active/mapper.rs | 7 + .../src/active/mapper/error_reports.rs | 124 +++ .../spacetime-client/src/module_bindings.rs | 61 +- ...reate_error_report_and_return_procedure.rs | 59 ++ ...elete_error_report_and_return_procedure.rs | 59 ++ .../error_report_create_input_type.rs | 25 + .../error_report_delete_input_type.rs | 15 + .../error_report_get_input_type.rs | 15 + .../error_report_list_input_type.rs | 19 + .../error_report_procedure_result_type.rs | 21 + .../error_report_snapshot_type.rs | 29 + .../src/module_bindings/error_report_table.rs | 259 ++++++ .../src/module_bindings/error_report_type.rs | 97 +++ .../error_report_update_input_type.rs | 17 + .../get_error_report_and_return_procedure.rs | 59 ++ ...list_error_reports_and_return_procedure.rs | 59 ++ ...pdate_error_report_and_return_procedure.rs | 59 ++ .../crates/spacetime-module/src/active.rs | 3 + .../spacetime-module/src/error_report.rs | 444 ++++++++++ .../crates/spacetime-module/src/migration.rs | 1 + 86 files changed, 4729 insertions(+), 226 deletions(-) create mode 100644 apps/admin-web/src/pages/AdminErrorReportsPage.tsx create mode 100644 apps/ai-game-creator-shell/src-tauri/src/error_report/commands.rs create mode 100644 apps/ai-game-creator-shell/src-tauri/src/error_report/mod.rs create mode 100644 apps/ai-game-creator-shell/src-tauri/src/error_report/notifications.rs create mode 100644 apps/ai-game-creator-shell/src-tauri/src/error_report/queue.rs create mode 100644 apps/ai-game-creator-shell/src-tauri/src/error_report/sanitize.rs create mode 100644 apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx create mode 100644 apps/ai-game-creator-shell/src/components/error-report/ErrorReportNotice.tsx create mode 100644 apps/ai-game-creator-shell/src/services/errorReporting.ts create mode 100644 apps/ai-game-creator-shell/src/services/errorReportingBridge.ts create mode 100644 apps/ai-game-creator-shell/tests/ErrorReportNotice.test.tsx create mode 100644 apps/ai-game-creator-shell/tests/errorReporting.test.ts create mode 100644 docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md create mode 100644 server-rs/crates/api-server/src/error_reports.rs create mode 100644 server-rs/crates/shared-contracts/src/error_reports.rs create mode 100644 server-rs/crates/spacetime-client/src/active/error_reports.rs create mode 100644 server-rs/crates/spacetime-client/src/active/mapper/error_reports.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/create_error_report_and_return_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/delete_error_report_and_return_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_create_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_delete_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_list_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_snapshot_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/error_report_update_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_error_report_and_return_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/list_error_reports_and_return_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/update_error_report_and_return_procedure.rs create mode 100644 server-rs/crates/spacetime-module/src/error_report.rs diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index 9912b9cf7..65bb5d4e5 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -23,6 +23,9 @@ import type { AdminEditorShowcaseListQuery, AdminEditorShowcaseListResponse, AdminEditorShowcaseReviewRequest, + AdminErrorReportDetail, + AdminErrorReportEntry, + AdminErrorReportListResponse, AdminFeatureGateConfigResponse, AdminLoginResponse, AdminMeResponse, @@ -259,7 +262,16 @@ export function listAdminTrackingEvents( query: AdminTrackingEventListQuery = {}, ) { return request( - `/admin/api/tracking/events${buildQueryString(query)}`, + `/admin/api/tracking/events${buildQueryString((params) => { + appendQueryParam(params, 'eventKey', query.eventKey); + appendQueryParam(params, 'userId', query.userId); + appendQueryParam(params, 'scopeKind', query.scopeKind); + appendQueryParam(params, 'scopeId', query.scopeId); + appendQueryParam(params, 'startDate', query.startDate); + appendQueryParam(params, 'endDate', query.endDate); + appendNumericQueryParam(params, 'limit', query.limit); + if (query.exportAll) params.set('exportAll', 'true'); + })}`, { token }, ); } @@ -271,6 +283,60 @@ export function listAdminTrackingEventKeys(token: string) { ); } +export function listAdminErrorReports( + token: string, + query: { + status?: string; + fingerprint?: string; + source?: string; + limit?: number; + offset?: 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, @@ -788,43 +854,44 @@ async function postAdminDirectUploadFile( } } -function buildQueryString(query: AdminTrackingEventListQuery) { +function buildQueryString(append: (params: URLSearchParams) => void) { const params = new URLSearchParams(); - appendQueryParam(params, 'eventKey', query.eventKey); - appendQueryParam(params, 'userId', query.userId); - appendQueryParam(params, 'scopeKind', query.scopeKind); - appendQueryParam(params, 'scopeId', query.scopeId); - appendQueryParam(params, 'startDate', query.startDate); - appendQueryParam(params, 'endDate', query.endDate); - if (typeof query.limit === 'number' && Number.isFinite(query.limit)) { - params.set('limit', String(query.limit)); - } - if (query.exportAll) { - params.set('exportAll', 'true'); - } - const queryString = params.toString(); - return queryString ? `?${queryString}` : ''; + append(params); + return formatQueryString(params); +} + +function buildErrorReportQueryString(query: { + status?: string; + fingerprint?: string; + source?: string; + limit?: number; + offset?: number; +}) { + return buildQueryString((params) => { + appendQueryParam(params, 'status', query.status); + appendQueryParam(params, 'fingerprint', query.fingerprint); + appendQueryParam(params, 'source', query.source); + appendNumericQueryParam(params, 'limit', query.limit); + appendNumericQueryParam(params, 'offset', query.offset); + }); } function buildAdminRechargeOrderListQuery(query: AdminRechargeOrderListQuery) { - const params = new URLSearchParams(); - appendQueryParam(params, 'orderId', query.orderId); - appendQueryParam( - params, - 'providerTransactionId', - query.providerTransactionId, - ); - appendQueryParam(params, 'userId', query.userId); - appendQueryParam(params, 'publicUserCode', query.publicUserCode); - appendQueryParam(params, 'paymentChannel', query.paymentChannel); - appendQueryParam(params, 'status', query.status); - appendQueryParam(params, 'createdAfter', query.createdAfter); - appendQueryParam(params, 'createdBefore', query.createdBefore); - if (typeof query.limit === 'number' && Number.isFinite(query.limit)) { - params.set('limit', String(query.limit)); - } - const queryString = params.toString(); - return queryString ? `?${queryString}` : ''; + return buildQueryString((params) => { + appendQueryParam(params, 'orderId', query.orderId); + appendQueryParam( + params, + 'providerTransactionId', + query.providerTransactionId, + ); + appendQueryParam(params, 'userId', query.userId); + appendQueryParam(params, 'publicUserCode', query.publicUserCode); + appendQueryParam(params, 'paymentChannel', query.paymentChannel); + appendQueryParam(params, 'status', query.status); + appendQueryParam(params, 'createdAfter', query.createdAfter); + appendQueryParam(params, 'createdBefore', query.createdBefore); + appendNumericQueryParam(params, 'limit', query.limit); + }); } function buildAdminUserDetailQuery(query: AdminUserDetailQuery) { @@ -900,6 +967,21 @@ function appendQueryParam( } } +function appendNumericQueryParam( + params: URLSearchParams, + key: string, + value: number | null | undefined, +) { + if (typeof value === 'number' && Number.isFinite(value)) { + params.set(key, String(value)); + } +} + +function formatQueryString(params: URLSearchParams) { + const queryString = params.toString(); + return queryString ? `?${queryString}` : ''; +} + function parseJsonResponse(responseText: string): unknown { if (!responseText.trim()) { return null; diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index bcd2ee1db..b911a6e8b 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -96,6 +96,38 @@ 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'; +} + +export interface AdminErrorReportListResponse { + reports: AdminErrorReportEntry[]; + total: number; + offset: number; + limit: number; + hasMore: boolean; +} + +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 }; + +const ADMIN_ERROR_REPORT_STATUSES = ['new', 'in-progress', 'resolved'] as const; +const ERROR_REPORT_PAGE_SIZE = 50; + +export function AdminErrorReportsPage({ token, onUnauthorized }: Props) { + const [reports, setReports] = useState([]); + const [selected, setSelected] = useState(null); + const [status, setStatus] = useState(''); + const [downloading, setDownloading] = useState(false); + const [busy, setBusy] = useState(false); + const [filterStatus, setFilterStatus] = useState(''); + const [pageOffset, setPageOffset] = useState(0); + const [pageInfo, setPageInfo] = useState({ total: 0, hasMore: false }); + const openReportRequestId = useRef(0); + const loadRequestId = useRef(0); + + const load = useCallback(async () => { + const requestId = ++loadRequestId.current; + setStatus(''); + try { + const response = await listAdminErrorReports(token, { + status: filterStatus || undefined, + limit: ERROR_REPORT_PAGE_SIZE, + offset: pageOffset, + }); + if (requestId === loadRequestId.current) { + const lastValidOffset = response.total + ? Math.floor((response.total - 1) / ERROR_REPORT_PAGE_SIZE) * + ERROR_REPORT_PAGE_SIZE + : 0; + if (pageOffset > lastValidOffset) { + setPageOffset(lastValidOffset); + return; + } + setReports(response.reports); + setPageInfo({ total: response.total, hasMore: response.hasMore }); + } + } catch (error) { + if (requestId !== loadRequestId.current) return; + if (isAdminApiError(error) && error.status === 401) + return onUnauthorized(); + setStatus(formatAdminApiError(error)); + } + }, [filterStatus, onUnauthorized, pageOffset, token]); + + const loadRef = useRef(load); + useEffect(() => { + loadRef.current = load; + }, [load]); + + useEffect(() => { + void load(); + }, [load]); + + async function openReport(batchId: string) { + const requestId = ++openReportRequestId.current; + setStatus(''); + try { + const detail = await getAdminErrorReport(token, batchId); + if (requestId === openReportRequestId.current) setSelected(detail); + } catch (error) { + if (requestId !== openReportRequestId.current) return; + if (isAdminApiError(error) && error.status === 401) + return onUnauthorized(); + setStatus(formatAdminApiError(error)); + } + } + + async function saveStatus(nextStatus: string) { + if (!selected || busy) return; + setBusy(true); + setStatus(''); + try { + const updated = await updateAdminErrorReport(token, selected.batchId, { + status: nextStatus, + note: selected.note, + }); + setSelected((current) => (current ? { ...current, ...updated } : current)); + await loadRef.current(); + } catch (error) { + if (isAdminApiError(error) && error.status === 401) onUnauthorized(); + else setStatus(formatAdminApiError(error)); + } finally { + setBusy(false); + } + } + + async function download(batchId: string) { + if (downloading) return; + setDownloading(true); + setStatus(''); + try { + const blob = await downloadAdminErrorReport(token, batchId); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = `${batchId}.zip`; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + setTimeout(() => URL.revokeObjectURL(url), 10_000); + } catch (error) { + if (isAdminApiError(error) && error.status === 401) onUnauthorized(); + else setStatus(formatAdminApiError(error)); + } finally { + setDownloading(false); + } + } + + return ( +
+
+
+

错误报告

+

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

+
+ +
+ {status ? ( +

+ {status} +

+ ) : null} +
+ + + + + + + + + + + + + {reports.map((report) => ( + void openReport(report.batchId)} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + void openReport(report.batchId); + } + }} + > + + + + + + + + ))} + +
报告事件来源状态用户时间
{report.batchId}{report.eventCount}{report.source ?? '-'}{report.status}{report.userId}{new Date(report.createdAt).toLocaleString()}
+
+
+ + {pageInfo.total === 0 + ? '暂无报告' + : `第 ${pageOffset + 1}-${Math.min(pageOffset + reports.length, pageInfo.total)} 条,共 ${pageInfo.total} 条`} + + + +
+ {selected ? ( +
+
+
+

{selected.batchId}

+ +
+

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

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

用户描述:{selected.userDescription}

+ ) : null} +