diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx index 149e5f7f7..60d5483dd 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx @@ -41,7 +41,10 @@ import { import { createPortal } from 'react-dom'; import RichTextInput from '../../components/RichTextInput'; -import { captureClientError } from '../../services/errorReporting'; +import { + captureClientError, + visibleClientErrorMessage, +} from '../../services/errorReporting'; import type { UserContentPart } from '../../view/project-development/chat/generated/UserContentPart'; import { chatPromptDraftKey, @@ -655,6 +658,9 @@ function ResourceReferenceEditor({ // 聊天特有的「发送前提醒」:提醒偏好、本轮已确认草稿指纹与表单拦截。 const [reminderOpen, setReminderOpen] = useState(false); const [reminderPolishing, setReminderPolishing] = useState(false); + const [reminderPreferenceError, setReminderPreferenceError] = useState< + string | null + >(null); // 偏好正式数据在 Rust:初始值只是「未关闭」的占位,挂载后由快照与事件补齐。 const [reminderDisabled, setReminderDisabled] = useState(false); const acknowledgedDraftKeyRef = useRef(null); @@ -681,6 +687,7 @@ function ResourceReferenceEditor({ let unsubscribe: (() => void) | null = null; void subscribeChatPromptPolishReminderDisabled((disabled) => { if (disposed) return; + setReminderPreferenceError(null); reminderPreferenceGenerationRef.current += 1; setReminderDisabled(disabled); }) @@ -688,18 +695,41 @@ function ResourceReferenceEditor({ if (disposed) release(); else unsubscribe = release; }) - .catch(() => undefined); + .catch((error) => { + if (!disposed) { + setReminderPreferenceError( + `润色提醒偏好读取失败:${visibleClientErrorMessage(error)}`, + ); + void captureClientError(error, { + source: 'chat-prompt-polish', + action: 'read-reminder-disabled', + }); + } + }); // 快照读取与订阅并发,但只有「读取期间没有任何更新」时才允许写回。 const generationAtRead = reminderPreferenceGenerationRef.current; - void readChatPromptPolishReminderDisabled().then((disabled) => { - if ( - disposed || - reminderPreferenceGenerationRef.current !== generationAtRead - ) { - return; - } - setReminderDisabled(disabled); - }); + void readChatPromptPolishReminderDisabled() + .then((disabled) => { + if ( + disposed || + reminderPreferenceGenerationRef.current !== generationAtRead + ) { + return; + } + setReminderPreferenceError(null); + setReminderDisabled(disabled); + }) + .catch((error) => { + if (!disposed) { + setReminderPreferenceError( + `润色提醒偏好读取失败:${visibleClientErrorMessage(error)}`, + ); + void captureClientError(error, { + source: 'chat-prompt-polish', + action: 'read-reminder-disabled', + }); + } + }); return () => { disposed = true; unsubscribe?.(); @@ -805,9 +835,15 @@ function ResourceReferenceEditor({ reminderPreferenceGenerationRef.current += 1; setReminderDisabled(disabled); void writeChatPromptPolishReminderDisabled(disabled) - .then((persisted) => setReminderDisabled(persisted)) + .then((persisted) => { + setReminderPreferenceError(null); + setReminderDisabled(persisted); + }) .catch((error) => { setReminderDisabled(!disabled); + setReminderPreferenceError( + `润色提醒偏好保存失败:${visibleClientErrorMessage(error)}`, + ); captureClientError(error, { source: 'chat-prompt-polish', action: 'set-reminder-disabled', @@ -888,13 +924,15 @@ function ResourceReferenceEditor({ {/* 「与原文相同 / 已截断」这类提示也要可见:只报失败会让「润色没变化」看起来像按钮坏了。 */} {showPolishAction && !reminderOpen && - (polishing || polishError || polishNotice) ? ( + (polishing || polishError || polishNotice || reminderPreferenceError) ? ( - {polishing ? '润色中…' : (polishError ?? polishNotice)} + {polishing + ? '润色中…' + : (polishError ?? polishNotice ?? reminderPreferenceError)} ) : null} {providers.map((provider) => diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts b/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts index 27b3af0a8..8cb4529d0 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts @@ -22,12 +22,8 @@ export const CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH = 40; /** 读取 Rust 偏好投影;原生桥不可用时按未关闭处理。 */ export async function readChatPromptPolishReminderDisabled(): Promise { if (!resolveTauriInvoke()) return false; - try { - const snapshot = await readWorkspacePreferences(); - return snapshot.chatPromptPolishReminderDisabled; - } catch { - return false; - } + const snapshot = await readWorkspacePreferences(); + return snapshot.chatPromptPolishReminderDisabled; } /** 写入 Rust 偏好并返回落盘后的投影;原生桥不可用时只当成次生效。 */ diff --git a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md index e355ee12b..d04e1b4a6 100644 --- a/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md +++ b/docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md @@ -141,6 +141,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接 - 共享异步操作 hook(包括 Prompt Polish)捕获异常时,操作指引可以保留,但固定失败文案后必须附精确脱敏正文。 - 资源画布历史任务账本读取属于用户可见的恢复路径:异常正文必须进入提示条;只有响应形状错误这类没有底层异常的情况才使用明确的契约提示。 - 异步业务 facade 不得把底层异常折成“空结果”来逃避错误处理;只有真实的空回包、未配置或浏览器桥不可用等无正文事实允许返回空值,Tauri/网络/IPC 异常必须上抛到统一脱敏出口。 +- 可选偏好可以安全回退默认值,但回退不能隐藏错误:偏好读写 IPC 失败必须同时保留安全正文、记录诊断并在当前输入上下文给出提示。 - Direct 活动回合事件桥注册失败即使仍有一次性快照,也必须标明事件更新通道已失联并展示安全正文,不能只显示旧快照而让用户误以为活动状态持续可靠。 - 首页富文本剪贴板读取的“无内容”与“原生 IPC 失败”必须区分;后者展示安全正文,不能静默当作空剪贴板。 - 账号邀请码复制与充值状态确认的失败提示也必须附安全正文;操作指引不能替换底层剪贴板、网络或支付查询原因。 diff --git a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md index e93c90ac1..89bf188a7 100644 --- a/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md +++ b/docs/project-memory/plans/【里程碑】AGC错误具体文本展示-2026-10-04.md @@ -62,6 +62,7 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时, - 共享 Prompt Polish hook 的注入请求/规范化/回填异常原来只显示固定失败提示;现保留精确脱敏后的异常正文,聊天输入和资源润色共用同一口径。 - 资源画布历史生成任务账本读取失败原来统一显示“暂时无法恢复历史任务”;现在保留 Tauri/文件读取正文,非数组响应仍单独保留契约错误提示。 - 发现默认 `requestChatPromptPolish` 还会把 Tauri 润色命令异常吞成 `null`,导致共享 hook 无法看到平台正文;现仅对空回包/桥不可用保留 `null`,原生异常继续上抛并统一脱敏。 +- 润色提醒偏好读取失败原来被当成“未关闭”默认值,写入失败只进错误上报池;现把读取/保存的 IPC 正文显示在输入区状态提示中,同时仍维持安全默认和回退行为。 - Direct 活动回合事件桥注册失败原来只退回启动快照,面板没有说明后续更新已失联;现保留事件桥 IPC 正文并在标题栏/活动项目面板显示。 - 首页富文本输入的原生剪贴板图片/文本读取失败原来直接按空剪贴板处理;现通过输入区错误提示保留脱敏后的剪贴板 IPC 正文。 - 账号邀请码复制和微信充值状态确认仍有固定“复制失败/暂时没能确认到账”路径;现保留剪贴板/支付查询错误正文并继续给操作指引。