diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 4e159ed..940caeb 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -135,11 +135,6 @@ export default function App(): JSX.Element { // 主进程侧 openView(插件 main 工厂 / 深链按钮)→ 渲染层切视图 useEffect(() => window.api.onPluginsView((change) => handleOpenView(change.view, change.params)), [handleOpenView]) - // TEMP-DEBUG: 移除(改为 App.tsx 模块级触发) - - // 主进程侧 openView(插件 main 工厂 / 深链按钮)→ 渲染层切视图 - useEffect(() => window.api.onPluginsView((change) => handleOpenView(change.view, change.params)), [handleOpenView]) - // 插件列表变化(扫描/重载/启停)→ 刷新外置插件页(iframe 换 src 整页重载) useEffect( () => diff --git a/apps/desktop/src/renderer/src/components/MessageItem.tsx b/apps/desktop/src/renderer/src/components/MessageItem.tsx index 70af401..d1321c1 100644 --- a/apps/desktop/src/renderer/src/components/MessageItem.tsx +++ b/apps/desktop/src/renderer/src/components/MessageItem.tsx @@ -7,7 +7,7 @@ import { StreamingText } from './StreamingText' import { Thinking, thinkingRows } from './Thinking' import { ToolChips, type ToolCallView } from './ToolChips' import { useEntrance } from '../hooks/useEntrance' -import { LuFoldVertical, LuGitFork, LuHistory, LuLoaderCircle, LuUndo2 } from 'react-icons/lu' +import { LuFoldVertical, LuGitFork, LuHistory, LuLoaderCircle, LuTriangleAlert, LuUndo2 } from 'react-icons/lu' import { formatTokens } from '../lib/utils' interface ContentBlock { @@ -41,6 +41,29 @@ function lastThinkingLine(text: string): string { return '' } +/** 消息级错误(stopReason:'error',如 API 402 余额耗尽/限流):无此错误返回空串 */ +function readMessageError(raw: unknown): string { + if (!raw || typeof raw !== 'object') return '' + const m = raw as { stopReason?: unknown; errorMessage?: unknown } + if (m.stopReason !== 'error' || typeof m.errorMessage !== 'string' || !m.errorMessage.trim()) return '' + return m.errorMessage +} + +/** 提炼错误文案的可读部分:'402: {"message":"Insufficient Balance",…}' → '402: Insufficient Balance' */ +function readableMessageError(err: string): string { + const match = err.match(/^(\d{3}):\s*([\s\S]*)$/) + const body = match ? match[2] : err + try { + const parsed = JSON.parse(body) as { message?: unknown } + if (parsed && typeof parsed.message === 'string' && parsed.message) { + return match ? `${match[1]}: ${parsed.message}` : parsed.message + } + } catch { + // 非 JSON 体,保持原文 + } + return err +} + /** 工作计时:working=true 起表,结束后冻结读数。 * working 中途抖动(pi 轮次间隙 streaming/turnActive 抖动)不锁存: * 恢复 true 时解除冻结,继续从起点计时。 */ @@ -166,6 +189,8 @@ function AssistantTurnBase({ let thinking = 0 let stream: UiMessage['stream'] | null = null let answerText = '' + // 消息级错误(API 失败等):回合内最后一条错误保留展示,否则错误轮会渲染成空白 + let messageError = '' // 流式中的消息 = 答案雏形(最后一条);其余消息全部是过程。 // 只有回合结束(settled)且没有流式占位时,最后一条已完成消息的正文才是答案。 @@ -177,6 +202,8 @@ function AssistantTurnBase({ stream = message.stream continue } + const msgErr = readMessageError(message.raw) + if (msgErr) messageError = msgErr const asAnswer = settled && streamPos === -1 && idx === messages.length - 1 for (const block of readBlocks(message.raw)) { const thinkingText = block.type === 'thinking' ? (block.text ?? block.thinking) : undefined @@ -436,6 +463,19 @@ function AssistantTurnBase({ {/* 回合进行中但没有流式占位:工具执行或自动压缩的静默间隙,给一个存活指示 */} {turnActive && !stream && } + {/* 消息级错误(stopReason:'error',如 API 402 余额耗尽 / 限流):常驻可见, + 不随过程区折叠——否则失败轮在界面上就是一片空白,无从排查 */} + {messageError && ( +
+ +
+
模型请求失败
+
{readableMessageError(messageError)}
+
+
+ )} + + {/* 结论底部的操作行:复制 + 会话分叉。末回合走会话末尾分叉(onFork), 历史回合以「本回合之后第一条用户消息」为锚点(forkEntryId + onForkAt), 新会话都包含到本回合答案为止。常驻不闪现。 */}