统一应用壳客户端错误正文

让运行配置、认证、插件、预览和策划错误复用统一脱敏出口

保留 typed 认证分流并补充长期错误处理口径
This commit is contained in:
kdletters
2026-10-05 10:39:20 +08:00
parent c5e04cfa36
commit 9740c39fe4
6 changed files with 47 additions and 65 deletions
+11 -15
View File
@@ -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(() => {
@@ -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));
}
}}
/>
@@ -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',
@@ -76,6 +76,19 @@ export function normalizeDiagnosticText(value: string) {
.replace(/\b[0-9a-f]{8,}\b/giu, '<id>');
}
/** 用户可见的普通客户端错误正文:保留状态码/原因,替换凭据、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<ClientErrorReportContext> = {},
@@ -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 分流,非结构化错误保留安全正文后再进入状态提示或错误上报。
## 后果与边界
@@ -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`。
## 实现顺序