统一应用壳客户端错误正文
让运行配置、认证、插件、预览和策划错误复用统一脱敏出口 保留 typed 认证分流并补充长期错误处理口径
This commit is contained in:
@@ -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`。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
|
||||
Reference in New Issue
Block a user