diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts index 46705025a..d97a6945a 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts @@ -35,6 +35,10 @@ import { enqueueSystemFailureNoticeText, invokeTurnEnqueue, } from '../conversation/directTurnEnqueueFailure'; +import { + visibleDirectControllerError, + visibleDirectFailureDetail, +} from '../conversation/directTurnFailure'; import type { HistorySlice } from '../generated/HistorySlice'; import type { QueueRemovalOutcome } from '../generated/QueueRemovalOutcome'; import type { ThreadItem } from '../generated/ThreadItem'; @@ -322,9 +326,7 @@ export function useDirectProjectChatController({ } catch (error) { if (projectPathRef.current !== nextProjectPath) return; setComposerNotice( - `取消待发消息失败:${ - error instanceof Error ? error.message : String(error) - }`, + visibleDirectControllerError(error, '取消待发消息失败'), ); } } @@ -394,7 +396,7 @@ export function useDirectProjectChatController({ } catch (error) { if (projectPathRef.current === projectPath) { setAttachmentNotice( - error instanceof Error ? error.message : String(error), + visibleDirectControllerError(error, '文件上传失败'), ); } return []; @@ -491,7 +493,13 @@ export function useDirectProjectChatController({ // 写权限门等前置步骤抛出时不能只留一个未处理的 rejection:回合会静默失败, // 这一轮的任何说明都没有出口。 if (projectPathRef.current === nextProjectPath) { - onRuntimeError(error instanceof Error ? error.message : String(error)); + onRuntimeError( + projectRuntimeVisibleError( + error instanceof Error ? error.message : String(error), + '陶泥儿智能创作', + true, + ), + ); } draftSafe = true; } finally { @@ -625,9 +633,7 @@ export function useDirectProjectChatController({ setComposerNotice(message); } } catch (error) { - onRuntimeError( - `终止失败:${error instanceof Error ? error.message : String(error)}`, - ); + onRuntimeError(visibleDirectControllerError(error, '终止失败')); setComposerNotice(''); } finally { setTurnCancelling(false); @@ -680,10 +686,10 @@ export function useDirectProjectChatController({ if (pages.error) throw pages.error; setStatusNotice(''); } catch (error) { - const message = error instanceof Error ? error.message : String(error); if (projectPathRef.current !== nextProjectPath) return; + const message = visibleDirectControllerError(error, '项目对话读取失败'); onRuntimeError(message); - setStatusNotice(`项目对话读取失败:${message}`); + setStatusNotice(message); } } @@ -734,9 +740,12 @@ export function useDirectProjectChatController({ } catch (error) { // 与加载成功路径同样按世代守卫:切项目后在飞的读取失败不能算到新项目头上。 if (isStaleLoad()) return; - const message = - (error instanceof Error ? error.message : String(error)) || - DIRECT_HISTORY_LOAD_ERROR_FALLBACK; + const detail = visibleDirectFailureDetail( + error instanceof Error ? error.message : String(error), + ); + const message = detail + ? `读取更早的对话历史失败:${detail}` + : DIRECT_HISTORY_LOAD_ERROR_FALLBACK; historyErrorRef.current = message; setHistoryError(message); } finally { diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnFailure.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnFailure.ts index 365209038..261cbda9d 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnFailure.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnFailure.ts @@ -65,6 +65,16 @@ export function visibleDirectFailureDetail( : safe; } +/** 控制器非回合错误的统一可见正文:只替换敏感值,空错误也保留专用未分类提示。 */ +export function visibleDirectControllerError( + error: unknown, + subject: string, +): string { + const raw = error instanceof Error ? error.message : String(error ?? ''); + const detail = visibleDirectFailureDetail(raw); + return detail ? `${subject}:${detail}` : `${subject}:未提供具体错误正文`; +} + function withVisibleFailureDetail( message: string, detail: string | null | undefined, diff --git a/apps/ai-game-creator-shell/tests/directTurnFailure.test.ts b/apps/ai-game-creator-shell/tests/directTurnFailure.test.ts index e50cf9335..1533f6999 100644 --- a/apps/ai-game-creator-shell/tests/directTurnFailure.test.ts +++ b/apps/ai-game-creator-shell/tests/directTurnFailure.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { directTurnFailureNoticeText } from '../src/view/project-development/chat/conversation/directTurnFailure'; +import { + directTurnFailureNoticeText, + visibleDirectControllerError, +} from '../src/view/project-development/chat/conversation/directTurnFailure'; describe('DirectProject 上游失败文案', () => { it.each([401, 403, 408, 429, 500, 502, 503, 504])( @@ -121,4 +124,23 @@ describe('DirectProject 上游失败文案', () => { expect(text).toContain('旧错误事件未记录具体原因'); expect(text).not.toContain('服务连接'); }); + + it('控制器错误也保留正文并只替换敏感值', () => { + const text = visibleDirectControllerError( + new Error( + 'IPC invoke failed: EPIPE;Authorization: Bearer fixture-secret;C:\\Users\\demo\\game', + ), + '终止失败', + ); + expect(text).toContain('EPIPE'); + expect(text).toContain('[已隐藏凭据]'); + expect(text).not.toContain('fixture-secret'); + expect(text).not.toContain('C:\\Users\\demo'); + }); + + it('空控制器错误使用专用未分类正文提示', () => { + expect(visibleDirectControllerError(new Error(''), '读取失败')).toBe( + '读取失败:未提供具体错误正文', + ); + }); }); diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index 5081c7f22..b2028681d 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -109,6 +109,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - Claude Code(cc)侧车的非零退出、RPC `error`、stdout JSON/UTF-8 解析失败、stderr 和静默超时也走同一映射;侧车 stderr 只在有界收口窗口内读取并进入同一脱敏 detail,不再只写裸 `eprintln!`。 - HTTP 409 只有明确包含泥点不足事实时才映射为 `paidCreditsInsufficient`;Claude Code 的普通 409 冲突保留为 `upstreamFailed`。 - `hostDropped` 携带可选的脱敏 `detail`:panic hook 能取得的负载和位置随原回合占用进入失败事件;普通 Drop 仅记录“退出时未写终态、未观察到 panic”,不推断为网络中断。旧版本无 `detail` 的事件继续可读,界面明确标注旧事件未记录原因。 +- DirectProject 控制器的取消待发、终止、附件上传、项目历史读取和发送前置异常也复用同一份前端精确脱敏出口;这些非回合错误不得直接把 `Error.message` 原文写入状态栏、composer 或历史错误行。 ## 后果与边界 diff --git a/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md index 9498c5bde..a59da4db2 100644 --- a/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【实施计划】AGC错误具体文本展示-2026-10-04.md @@ -13,6 +13,7 @@ Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md` 4. 补充 upstream、transport、stream、IPC、内存不足与 HostDropped 边界测试。 5. 更新 ADR 的当前行为口径。 6. HostDropped 的可选 detail 由 Rust 单点脱敏;panic hook 把可读负载和位置关联到原回合占用,Drop 与旧无字段事件分别验证,不更改回合排队、重试或计费行为。 +7. DirectProject 控制器的取消、上传、历史读取和前置异常统一复用前端精确脱敏出口,保留 IPC/OS 正文并替换凭据值、URL 与路径。 ## 实现顺序 @@ -33,5 +34,6 @@ Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md` - 真实 AGC dev smoke 复现并定位 `Reached maximum number of turns (8)` → `transport-closed`;修复后客户端已热重编译重启,未再次触发付费 Provider 请求。 - Rust 应用日志/启动诊断精确脱敏回归:通过。 - Rust HostDropped panic detail + 旧无字段载荷兼容回归:定向通过。 +- DirectProject 控制器错误正文:新增脱敏与空错误专用提示回归,前端定向测试通过。 - 真实 Provider、IPC 断链和内存压力尚未执行。 - 2026-10-05 当前代码真实 `npm run agc` 构建启动通过;客户端与 Runner 已启动,前端/后台/API/worker/数据库健康检查通过,停止本次自有客户端后保留原有后端。