From 9740c39fe48d9b25f6049e39c001355eb3babd02 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:39:20 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=BA=94=E7=94=A8=E5=A3=B3?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E9=94=99=E8=AF=AF=E6=AD=A3=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 让运行配置、认证、插件、预览和策划错误复用统一脱敏出口 保留 typed 认证分流并补充长期错误处理口径 --- apps/ai-game-creator-shell/src/App.tsx | 26 ++++----- .../src/app/AuthenticatedClient.tsx | 17 +++--- .../runtime-config/RuntimeConfigDialog.tsx | 54 +++++-------------- .../src/services/errorReporting.ts | 13 +++++ ...GC命令错误结构化与错误报告口径-2026-10-01.md | 1 + ...实施计划】AGC错误具体文本展示-2026-10-04.md | 1 + 6 files changed, 47 insertions(+), 65 deletions(-) diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 145a3fb0a..171042b4b 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -88,6 +88,7 @@ import { type ResourceReferenceInsertManyEventDetail, } from './features/project-workspace/resourceReferences'; import { RuntimeConfigDialog } from './features/runtime-config/RuntimeConfigDialog'; +import { visibleClientErrorMessage } from './services/errorReporting'; import { setAgcPluginProjectPath, startAvailableAgcEditorPlugins, @@ -247,9 +248,7 @@ export function App({ return; } setWorkspaceStatus( - `编辑器插件未就绪:${ - error instanceof Error ? error.message : String(error) - }`, + `编辑器插件未就绪:${visibleClientErrorMessage(error)}`, ); }); return () => { @@ -564,8 +563,9 @@ export function App({ if (!isCurrentDesignTurn(nextProjectPath, clientTurnId)) { return; } - const message = error instanceof Error ? error.message : String(error); - if (isRuntimeConfigMissingError(message)) { + const rawMessage = error instanceof Error ? error.message : String(error); + const message = visibleClientErrorMessage(error); + if (isRuntimeConfigMissingError(rawMessage)) { requestRuntimeConfigOpen(); } setProjectChatError(message); @@ -916,7 +916,7 @@ export function App({ setPublishProgress({ phase: 'failed', message: '发布失败', - detail: error instanceof Error ? error.message : String(error), + detail: visibleClientErrorMessage(error), }); } } @@ -1111,7 +1111,7 @@ export function App({ } catch (error) { if (planningStartMode) { setProjectChatError( - `策划会话无法恢复:${error instanceof Error ? error.message : String(error)}`, + `策划会话无法恢复:${visibleClientErrorMessage(error)}`, ); } } @@ -1249,7 +1249,7 @@ export function App({ if (projectScopeVersionRef.current !== projectScopeVersion) { return; } - const message = error instanceof Error ? error.message : String(error); + const message = visibleClientErrorMessage(error); setWorkspaceStatus(message); if (announceToChat) { setMessages((current) => [ @@ -1413,9 +1413,7 @@ export function App({ } catch (error) { if (localProjectPathRef.current === input.projectPath) { setProjectChatError( - `DirectProject 对话权限检查失败:${ - error instanceof Error ? error.message : String(error) - }`, + `DirectProject 对话权限检查失败:${visibleClientErrorMessage(error)}`, ); } return false; @@ -1568,9 +1566,7 @@ export function App({ } catch (error) { onRunNotice?.({ tone: 'error', - message: `运行游戏失败:${ - error instanceof Error ? error.message : String(error) - }`, + message: `运行游戏失败:${visibleClientErrorMessage(error)}`, }); } } @@ -1922,7 +1918,7 @@ export function App({ .catch((error) => { if (!isCurrentDesignTurn(nextProjectPath, clientTurnId)) return; - setProjectChatError(String(error)); + setProjectChatError(visibleClientErrorMessage(error)); designReplyAnimation.discardUnpersisted(); }) .finally(() => { diff --git a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx index 96d8b5c54..0d6220044 100644 --- a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx +++ b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx @@ -34,6 +34,7 @@ import { import { captureClientError, installWebviewLogBridge, + visibleClientErrorMessage, } from '../services/errorReporting'; import type { AuthNetworkFailure } from '../services/generated/AuthNetworkFailure'; import type { AuthResponseInvalid } from '../services/generated/AuthResponseInvalid'; @@ -101,7 +102,7 @@ export class ClientRuntimeErrorBoundary extends Component< return { errorMessage: error instanceof Error && error.message.trim() - ? error.message + ? visibleClientErrorMessage(error) : '登录后页面发生未知错误', }; } @@ -179,7 +180,7 @@ export function AuthenticatedClient({ setServerSelection(next); return true; } catch (error) { - setLoginStatus(error instanceof Error ? error.message : String(error)); + setLoginStatus(visibleClientErrorMessage(error)); return false; } } @@ -268,7 +269,7 @@ export function AuthenticatedClient({ setAuthStatus('unauthenticated'); // 启动检查超时等我们自己的失败:改动前会显示原因 + 重试按钮,这里恢复同样的可见反馈。 if (!(error instanceof ClientAuthErrorWrapper)) { - const text = error instanceof Error ? error.message : String(error); + const text = visibleClientErrorMessage(error); setAuthCheckError(text); setLoginStatus(text); throw error; @@ -543,7 +544,7 @@ export function AuthenticatedClient({ } catch (error) { // 本地运行时超时等我们自己的失败也要离开「正在发送验证码」并给出原因,改动前就是如此。 if (!(error instanceof ClientAuthErrorWrapper)) { - setLoginStatus(error instanceof Error ? error.message : String(error)); + setLoginStatus(visibleClientErrorMessage(error)); throw error; } const failure = error.error; @@ -770,7 +771,7 @@ export function AuthenticatedClient({ } catch (error) { // 本地运行时超时等我们自己的失败也要离开「正在登录」并给出原因,改动前就是如此。 if (!(error instanceof ClientAuthErrorWrapper)) { - setLoginStatus(error instanceof Error ? error.message : String(error)); + setLoginStatus(visibleClientErrorMessage(error)); throw error; } const failure = error.error; @@ -1029,11 +1030,7 @@ export function AuthenticatedClient({ normalizeClientServerBaseUrl(customServerUrl); persistServerSelection(); } catch (error) { - setLoginStatus( - error instanceof Error - ? error.message - : String(error), - ); + setLoginStatus(visibleClientErrorMessage(error)); } }} /> diff --git a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx index c7488f27b..7861eae67 100644 --- a/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx +++ b/apps/ai-game-creator-shell/src/features/runtime-config/RuntimeConfigDialog.tsx @@ -37,6 +37,7 @@ import { gameCreatorLlmReasoningEfforts, } from '../../app/types'; import { checkForAppUpdateResult } from '../../services/appUpdate'; +import { visibleClientErrorMessage } from '../../services/errorReporting'; import { notifyLlmConfigChanged } from '../../services/llmModelCatalog'; import { AGC_PLUGIN_STATE_CHANGED_EVENT, @@ -83,12 +84,7 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = { }; type RuntimeSettingsSection = - | 'general' - | 'workspace' - | 'agents' - | 'extensions' - | 'advanced' - | 'about'; + 'general' | 'workspace' | 'agents' | 'extensions' | 'advanced' | 'about'; type RuntimeConfigToast = { tone: 'success' | 'error'; @@ -301,9 +297,7 @@ export function RuntimeConfigDialog({ }) .catch((error) => { if (!disposed) { - setAgcPluginsStatus( - error instanceof Error ? error.message : String(error), - ); + setAgcPluginsStatus(visibleClientErrorMessage(error)); } }); return () => { @@ -358,9 +352,7 @@ export function RuntimeConfigDialog({ ); setAgcPluginsStatus(''); } catch (error) { - setAgcPluginsStatus( - error instanceof Error ? error.message : String(error), - ); + setAgcPluginsStatus(visibleClientErrorMessage(error)); } } @@ -377,9 +369,7 @@ export function RuntimeConfigDialog({ ); setAgcPluginsStatus(''); } catch (error) { - setAgcPluginsStatus( - error instanceof Error ? error.message : String(error), - ); + setAgcPluginsStatus(visibleClientErrorMessage(error)); } finally { setAgcPluginsBusy(false); } @@ -395,9 +385,7 @@ export function RuntimeConfigDialog({ ); setAgcPluginsStatus('插件已重新加载'); } catch (error) { - setAgcPluginsStatus( - error instanceof Error ? error.message : String(error), - ); + setAgcPluginsStatus(visibleClientErrorMessage(error)); } finally { setAgcPluginsBusy(false); } @@ -416,9 +404,7 @@ export function RuntimeConfigDialog({ await readClientExtensions(); setAgcPluginsStatus(enabled ? '内置插件已启用' : '内置插件已禁用'); } catch (error) { - setAgcPluginsStatus( - error instanceof Error ? error.message : String(error), - ); + setAgcPluginsStatus(visibleClientErrorMessage(error)); } finally { setAgcPluginsBusy(false); } @@ -442,9 +428,7 @@ export function RuntimeConfigDialog({ setClientExtensionsStatus(''); } catch (error) { setClientExtensionsLoadState('error'); - setClientExtensionsStatus( - error instanceof Error ? error.message : String(error), - ); + setClientExtensionsStatus(visibleClientErrorMessage(error)); } finally { setClientExtensionsBusy(false); } @@ -487,9 +471,7 @@ export function RuntimeConfigDialog({ : '未发现可管理的扩展', ); } catch (error) { - setClientExtensionsStatus( - error instanceof Error ? error.message : String(error), - ); + setClientExtensionsStatus(visibleClientErrorMessage(error)); } finally { setClientExtensionsBusy(false); } @@ -531,9 +513,7 @@ export function RuntimeConfigDialog({ ); cancelRenameClientExtension(); } catch (error) { - setClientExtensionsStatus( - error instanceof Error ? error.message : String(error), - ); + setClientExtensionsStatus(visibleClientErrorMessage(error)); } finally { setClientExtensionsBusy(false); } @@ -561,9 +541,7 @@ export function RuntimeConfigDialog({ await readClientExtensions(); await readAgcPlugins(); } catch (error) { - setClientExtensionsStatus( - error instanceof Error ? error.message : String(error), - ); + setClientExtensionsStatus(visibleClientErrorMessage(error)); } finally { setClientExtensionsBusy(false); } @@ -584,9 +562,7 @@ export function RuntimeConfigDialog({ } setClientExtensionsStatus('扩展已移除'); } catch (error) { - setClientExtensionsStatus( - error instanceof Error ? error.message : String(error), - ); + setClientExtensionsStatus(visibleClientErrorMessage(error)); } finally { setClientExtensionsBusy(false); } @@ -632,9 +608,7 @@ export function RuntimeConfigDialog({ setRuntimeConfigStatus(`已读取:${result.path}`); onLog?.('runtime_config.read'); } catch (error) { - setRuntimeConfigStatus( - error instanceof Error ? error.message : String(error), - ); + setRuntimeConfigStatus(visibleClientErrorMessage(error)); } finally { runtimeConfigBusyRef.current = false; setRuntimeConfigBusy(false); @@ -689,7 +663,7 @@ export function RuntimeConfigDialog({ }); onLog?.('runtime_config.save'); } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const message = visibleClientErrorMessage(error); setRuntimeConfigStatus(message); setRuntimeConfigToast({ tone: 'error', diff --git a/apps/ai-game-creator-shell/src/services/errorReporting.ts b/apps/ai-game-creator-shell/src/services/errorReporting.ts index c22a703ab..08d8d53da 100644 --- a/apps/ai-game-creator-shell/src/services/errorReporting.ts +++ b/apps/ai-game-creator-shell/src/services/errorReporting.ts @@ -76,6 +76,19 @@ export function normalizeDiagnosticText(value: string) { .replace(/\b[0-9a-f]{8,}\b/giu, ''); } +/** 用户可见的普通客户端错误正文:保留状态码/原因,替换凭据、URL、路径和长标识。 */ +export function visibleClientErrorMessage( + error: unknown, + fallback = '未提供具体错误正文', +) { + const raw = error instanceof Error ? error.message : String(error ?? ''); + const safe = normalizeDiagnosticText(raw.replace(/\s+/gu, ' ').trim()).slice( + 0, + 400, + ); + return safe || fallback; +} + export async function captureClientError( error: unknown, context: Partial = {}, diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index e2f3e88c4..2db046ff5 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -111,6 +111,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - `hostDropped` 携带可选的脱敏 `detail`:panic hook 能取得的负载和位置随原回合占用进入失败事件;普通 Drop 仅记录“退出时未写终态、未观察到 panic”,不推断为网络中断。旧版本无 `detail` 的事件继续可读,界面明确标注旧事件未记录原因。 - DirectProject 控制器的取消待发、终止、附件上传、项目历史读取和发送前置异常也复用同一份前端精确脱敏出口;这些非回合错误不得直接把 `Error.message` 原文写入状态栏、composer 或历史错误行。 - AGC 项目资源编辑、上传、预览、恢复队列和生成结果回读的面板错误统一经过 `normalizeDiagnosticText`,保留状态码与安全正文,替换凭据、URL、路径和长标识;资源业务提示不再直接展示 `Error.message`。 +- 应用壳、运行配置、认证状态、插件启动、运行预览和策划会话的普通客户端错误统一经过 `visibleClientErrorMessage`;结构化认证拒绝仍按 typed reason 分流,非结构化错误保留安全正文后再进入状态提示或错误上报。 ## 后果与边界 diff --git a/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md index 703c85068..5f4d8f4dd 100644 --- a/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md @@ -15,6 +15,7 @@ Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md` 6. HostDropped 的可选 detail 由 Rust 单点脱敏;panic hook 把可读负载和位置关联到原回合占用,Drop 与旧无字段事件分别验证,不更改回合排队、重试或计费行为。 7. DirectProject 控制器的取消、上传、历史读取和前置异常统一复用前端精确脱敏出口,保留 IPC/OS 正文并替换凭据值、URL 与路径。 8. 项目资源编辑/上传/预览/恢复队列/生成结果回读的 UI 错误统一使用 `normalizeDiagnosticText`,不再直出 `Error.message`。 +9. 应用壳、运行配置、认证状态、插件启动、运行预览和策划会话的非结构化错误统一使用 `visibleClientErrorMessage`。 ## 实现顺序