统一前端结构化错误展示

避免资源和工作台错误退化为 object Object

统一 Direct UI 和资源面板的正文脱敏出口

通过错误报告和 Direct 展示回归测试
This commit is contained in:
kdletters
2026-10-05 18:58:40 +08:00
parent 8ea1f018fe
commit 7e24d9511f
12 changed files with 43 additions and 66 deletions
+1 -1
View File
@@ -567,7 +567,7 @@ export function App({
if (!isCurrentDesignTurn(nextProjectPath, clientTurnId)) {
return;
}
const rawMessage = error instanceof Error ? error.message : String(error);
const rawMessage = visibleClientErrorMessage(error);
const message = visibleClientErrorMessage(error);
if (isRuntimeConfigMissingError(rawMessage)) {
requestRuntimeConfigOpen();
@@ -78,7 +78,7 @@ async function inspectRecentWorkspace(
]);
return { path: workspace, status, retryable: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const message = visibleClientErrorMessage(error);
return {
path: workspace,
status: null,
@@ -1,4 +1,4 @@
import { normalizeDiagnosticText } from '../../services/errorReporting';
import { visibleClientErrorMessage } from '../../services/errorReporting';
import {
applyLocalProjectAssetGenerationRecords,
type LocalProjectAssetGenerationTaskRecord,
@@ -90,17 +90,7 @@ export type ResourceCanvasAssetGenerationQueue = {
};
function errorMessage(error: unknown) {
const raw =
typeof error === 'string'
? error
: error instanceof Error
? error.message
: String(error ?? '');
const safe = normalizeDiagnosticText(raw.replace(/\s+/gu, ' ').trim()).slice(
0,
400,
);
return safe || '生成素材失败';
return visibleClientErrorMessage(error, '生成素材失败').slice(0, 400);
}
function safeGenerationRecord(
@@ -1,5 +1,5 @@
import type { EditorAsset } from '../../../../../src/components/image-editor/ImageCanvasEditorTypes';
import { normalizeDiagnosticText } from '../../services/errorReporting';
import { visibleClientErrorMessage } from '../../services/errorReporting';
import type { ProjectResource } from '../../view/project-development/resourceProjectionModel';
import {
resourceCanvasAssetKind,
@@ -257,10 +257,10 @@ export function resourceReplacementPickerAssets(
* 不认识的错误原样透出:宁可显示后端原文,也不把原因吞掉换成"替换失败"这种无信息文案。
*/
export function resourceVersionReplacementErrorMessage(error: unknown): string {
const message = normalizeDiagnosticText(
error instanceof Error ? error.message : String(error ?? '').trim(),
).slice(0, 400);
if (!message) return '替换素材失败';
const message = visibleClientErrorMessage(error, '替换素材失败').slice(
0,
400,
);
if (message.includes('project-revision-conflict')) {
return '项目已被其它操作改动,请重试替换';
}
@@ -1,3 +1,4 @@
import { visibleClientErrorMessage } from './errorReporting';
import {
currentPlatformSessionGeneration,
requestPlatformSessionRefresh,
@@ -11,7 +12,7 @@ import {
* 续期后重试一次,网络抖动、5xx 和业务拒绝必须原样抛出。
*/
export function isPlatformAuthenticationRequiredError(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const message = visibleClientErrorMessage(error);
return (
message.includes('authentication-required') ||
message.includes('codex-app-server-error:unauthorized') ||
@@ -16,7 +16,10 @@ import {
hasMeaningfulDirectCodexContent,
userItemFromContent,
} from '../../../../features/project-workspace/resourceReferences';
import { captureAgentRuntimeError } from '../../../../services/errorReporting';
import {
captureAgentRuntimeError,
visibleClientErrorMessage,
} from '../../../../services/errorReporting';
import {
createDirectProjectTurnId,
DIRECT_CODEX_AGENT_ID,
@@ -495,7 +498,7 @@ export function useDirectProjectChatController({
if (projectPathRef.current === nextProjectPath) {
onRuntimeError(
projectRuntimeVisibleError(
error instanceof Error ? error.message : String(error),
visibleClientErrorMessage(error),
'陶泥儿智能创作',
true,
),
@@ -565,7 +568,7 @@ export function useDirectProjectChatController({
if (!systemNotice) {
onRuntimeError(
projectRuntimeVisibleError(
error instanceof Error ? error.message : String(error),
visibleClientErrorMessage(error),
'陶泥儿智能创作',
true,
),
@@ -741,7 +744,7 @@ export function useDirectProjectChatController({
// 与加载成功路径同样按世代守卫:切项目后在飞的读取失败不能算到新项目头上。
if (isStaleLoad()) return;
const detail = visibleDirectFailureDetail(
error instanceof Error ? error.message : String(error),
visibleClientErrorMessage(error),
);
const message = detail
? `读取更早的对话历史失败:${detail}`
@@ -7,6 +7,7 @@ import {
} from 'react';
import { resolveTauriInvoke } from '../../../../app/tauri';
import { visibleClientErrorMessage } from '../../../../services/errorReporting';
import {
canSubscribeTauriEvents,
subscribeTauriEvent,
@@ -128,7 +129,10 @@ export function useDirectThreadChatSubscription({
);
} while (consumeAgain && !disposed);
} catch (error) {
if (!disposed && String(error).includes('SUBSCRIPTION_EXPIRED')) {
if (
!disposed &&
visibleClientErrorMessage(error).includes('SUBSCRIPTION_EXPIRED')
) {
subscriptionId = null;
try {
await bootstrap();
@@ -525,7 +525,7 @@ function projectDevelopmentErrorMessage(
}
function isPlatformAuthenticationRequired(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const message = visibleClientErrorMessage(error);
return (
message.includes('authentication-required') ||
message.includes('登录已失效')
@@ -6728,8 +6728,7 @@ export default function ProjectDevelopmentView({
}
} catch (error) {
if (!isCurrentRecoveryProject(actionProject)) return;
const rawMessage =
error instanceof Error ? error.message : String(error);
const rawMessage = visibleClientErrorMessage(error);
const message = projectDevelopmentErrorMessage(error);
let displayMessage = message;
if (rawMessage.includes('service-identity-confirmation-required')) {
@@ -8,24 +8,13 @@
* 口径与 `resourceVersionReplacementErrorMessage` 里同样的两条映射逐字一致,
* 三条链路给用户的说法必须一样。
*/
import { normalizeDiagnosticText } from '../../services/errorReporting';
import { visibleClientErrorMessage } from '../../services/errorReporting';
export function projectAssetCommandErrorMessage(
error: unknown,
fallback: string,
): string {
let message = '';
if (typeof error === 'string') {
message = error.trim();
} else if (error instanceof Error) {
message = error.message.trim();
} else if (error !== null && error !== undefined) {
message = String(error).trim();
}
message = normalizeDiagnosticText(message).slice(0, 400);
if (!message) {
return fallback;
}
const message = visibleClientErrorMessage(error, fallback);
if (message.includes('project-revision-conflict')) {
return '项目已被其它操作改动,请刷新后重试';
}
@@ -10,7 +10,7 @@ import {
isSafeProjectResourceCanvasCoordinate,
isSafeProjectResourceCanvasLayoutRevision,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { normalizeDiagnosticText } from '../../services/errorReporting';
import { visibleClientErrorMessage } from '../../services/errorReporting';
import {
applyResourceCanvasPositionWrites,
createEmptyResourceCanvasLayout,
@@ -26,16 +26,12 @@ import { normalizeResourceCanvasPosition } from './resourceCanvasSectionMapping'
*
* 这两个 `.catch` 此前写成 `() => { ... }` —— 没有 message 参数,失败原因被整个吞掉:
* 用户只看到"布局保存失败",排障端也拿不到为什么(revision 越界?IPC 拒绝?权限?)。
* 这里把 `error.message` 压成一行并复用既有的 `normalizeDiagnosticText`(路径 / token /
* URL / 长 id 一律打码),再截断,避免多行堆栈糊进日志;日志由客户端已安装的
* 这里复用既有的 `visibleClientErrorMessage`(结构化对象也保留正文,路径 / token / URL /
* 长 id 一律打码),再截断,避免多行堆栈糊进日志;日志由客户端已安装的
* webview→application.log 桥带进诊断日志,遇到"某个窗口保存一直失败"能直接查。
*/
function describeResourceCanvasLayoutFailure(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return normalizeDiagnosticText(message.replace(/\s+/gu, ' ').trim()).slice(
0,
400,
);
return visibleClientErrorMessage(error).slice(0, 400);
}
type LayoutNotice =
@@ -439,9 +435,7 @@ function reconcileLayout(
* 做,磁盘上仍是旧栏目取值的坐标不会因为 `section` 字面量不同而漏出整理范围。
*/
organizeSections:
| readonly ProjectResourceCanvasCategory[]
| null
| undefined = undefined,
readonly ProjectResourceCanvasCategory[] | null | undefined = undefined,
) {
// `undefined` = 不是整理;`null` = 整理全部栏目;否则按集合判定。
const organizedSections =
@@ -8,7 +8,7 @@ import {
} from 'react';
import type { ProjectResourceCanvasLayoutMode } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { normalizeDiagnosticText } from '../../services/errorReporting';
import { visibleClientErrorMessage } from '../../services/errorReporting';
import {
cancelLocalProjectResourcePreviewScope,
createProjectResourcePreviewRequestId,
@@ -109,8 +109,7 @@ function previewReadErrorMessage(
kind: ProjectResourceCardPreviewKind,
error: unknown,
): { error: string; retryable: boolean } {
const rawMessage = error instanceof Error ? error.message : String(error);
const message = normalizeDiagnosticText(rawMessage).slice(0, 400);
const message = visibleClientErrorMessage(error).slice(0, 400);
const kindLabel = resourceReadKindLabel(kind);
if (message.includes('项目权限策略要求用户确认')) {
return {
@@ -342,8 +342,7 @@ export function useUiEditorSession(
})
.catch((cause: unknown) => {
if (!cancelled) {
const message =
cause instanceof Error ? cause.message : String(cause);
const message = visibleClientErrorMessage(cause);
setStatus(message);
setLoadError(message);
}
@@ -606,7 +605,7 @@ export function useUiEditorSession(
}));
setStatus(`已加入 ${prepared.length} 项独立素材。`);
} catch (cause) {
setStatus(cause instanceof Error ? cause.message : String(cause));
setStatus(visibleClientErrorMessage(cause));
}
}
@@ -970,7 +969,7 @@ export function useUiEditorSession(
reportWorkflowCompletion(
'structure-recognition',
'failure',
cause instanceof Error ? cause.message : String(cause),
visibleClientErrorMessage(cause),
setRecognitionStatus,
);
} finally {
@@ -1187,7 +1186,7 @@ export function useUiEditorSession(
reportWorkflowCompletion(
'asset-separation',
'failure',
cause instanceof Error ? cause.message : String(cause),
visibleClientErrorMessage(cause),
setSeparationStatus,
);
} finally {
@@ -1231,7 +1230,7 @@ export function useUiEditorSession(
reportWorkflowCompletion(
'asset-separation',
'failure',
cause instanceof Error ? cause.message : String(cause),
visibleClientErrorMessage(cause),
setSeparationStatus,
);
}
@@ -1266,7 +1265,7 @@ export function useUiEditorSession(
reportWorkflowCompletion(
'asset-separation',
'failure',
cause instanceof Error ? cause.message : String(cause),
visibleClientErrorMessage(cause),
setSeparationStatus,
);
}
@@ -1298,7 +1297,7 @@ export function useUiEditorSession(
reportWorkflowCompletion(
'asset-separation',
'failure',
cause instanceof Error ? cause.message : String(cause),
visibleClientErrorMessage(cause),
setSeparationStatus,
);
}
@@ -1424,8 +1423,7 @@ export function useUiEditorSession(
analytics?.record(saved.status === 'saved');
return { status: 'generated' as const, result: generated };
} catch (cause) {
const message =
cause instanceof Error ? cause.message : String(cause);
const message = visibleClientErrorMessage(cause);
return {
status: 'failed' as const,
phase: 'generate' as const,
@@ -1434,7 +1432,7 @@ export function useUiEditorSession(
}
});
} catch (cause) {
const message = cause instanceof Error ? cause.message : String(cause);
const message = visibleClientErrorMessage(cause);
setSaveError(message);
return { status: 'failed' as const, phase: 'save' as const, message };
} finally {