From b9f7ec5118ef045a9a9bfc54384110a4b6b57147 Mon Sep 17 00:00:00 2001
From: thzxx <1440196015@qq.com>
Date: Sat, 25 Jul 2026 19:35:33 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=A7=E6=95=B0?=
=?UTF-8?q?=E6=8D=AE=E5=AF=BC=E8=87=B4=E5=BA=94=E7=94=A8=E5=B4=A9=E6=BA=83?=
=?UTF-8?q?=20+=20=E5=9B=BE=E7=89=87=E4=B8=8A=E4=BC=A0=E5=86=97=E4=BD=99?=
=?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=20+=20=E9=A2=84=E5=AD=98?=
=?UTF-8?q?=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 崩溃修复 (renderer OOM)
view_image 返回的 base64 dataUrl(~6.7MB/张)经 dataUrl 白名单跳过截断后,
在 IPC/store/DOM 链路被反复放大,累积撑爆渲染进程堆导致 OOM 崩溃退出。
- 新增 src/lib/tool-result-display.ts: toDisplayResult() 在显示边界剥离 dataUrl
- ToolResultBlock/TraceStep 渲染前套用,store 原始 result 不变(LLM 看图链路完整)
- data:export 剥离 dataUrl/preview + 每会话 2000 条消息上限 + truncated 标记
## 图片上传修复
用户上传图片并告知 AI 后,AI 仍调 view_image 去磁盘读取。
根因: 附件提示 "do NOT search in workspace" 与 view_image "Read an image file"
语义冲突,AI 认为"读取"≠"搜索"。强化提示明确禁止调用 view_image/read_file 读取已上传图片。
## 预存类型修复
- ConfirmationDialog: MUI v9 移除 disableEscapeKeyDown 顶层 prop,迁移到 onClose reason 拦截
- ContextMenu/Sidebar: TokenUsage 对象字面量补齐 lastInputTokens/lastCompressedSaved
## 依赖升级
- @metona-team/metona-toast 0.1.2 → 0.2.1 (TypeScript 重构,修复 0.1.2 版本号打包 bug)
## 其他
- 删除仓库根目录误生成的 nul 垃圾文件
- bump version to 0.3.22
---
README.md | 2 +-
electron/ipc/handlers.ts | 71 ++++++++++++++++++++---
package-lock.json | 24 ++++----
package.json | 4 +-
src/components/ConfirmationDialog.tsx | 11 ++--
src/components/ContextMenu.tsx | 2 +-
src/components/chat/ToolResultBlock.tsx | 6 +-
src/components/layout/Sidebar.tsx | 2 +-
src/components/trace/TraceStep.tsx | 6 +-
src/lib/tool-result-display.ts | 75 +++++++++++++++++++++++++
10 files changed, 171 insertions(+), 32 deletions(-)
create mode 100644 src/lib/tool-result-display.ts
diff --git a/README.md b/README.md
index 9307e96..40a405b 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts
index 047c545..28dda27 100644
--- a/electron/ipc/handlers.ts
+++ b/electron/ipc/handlers.ts
@@ -173,20 +173,22 @@ export function registerAllIPCHandlers(
}
// 附件提示注入:用户直接上传的文件/图片,避免 LLM 误以为需要在工作空间查找
- // 解决场景:用户上传图片后,AI 先在工作空间找图片,找不到才意识到是用户上传的
+ // 解决场景:用户上传图片后,AI 仍调 view_image 去磁盘读取——根因是提示措辞
+ // "do NOT search in workspace" 与 view_image "Read an image file" 语义冲突,
+ // AI 认为"读取"不等于"搜索"。强化为明确禁止调用 view_image 读取已上传的图片。
const attachments = (userMessage as MetonaMessage & { attachments?: Array<{ name: string; type: string }> }).attachments;
if (Array.isArray(attachments) && attachments.length > 0) {
const attachmentList = attachments.map((att, i) => {
const typeLabel = att.type === 'image' ? 'image' : att.type === 'text' ? 'text file' : 'file';
const note = att.type === 'image'
- ? 'visible via vision capability, do NOT search in workspace'
+ ? 'already provided to you via vision capability — you can SEE it directly, do NOT call view_image or any tool to read it again'
: att.type === 'text'
- ? 'content already inlined in the user message, do NOT search in workspace'
+ ? 'content already inlined in the user message, do NOT search in workspace or read it again'
: 'uploaded directly by user, do NOT search in workspace';
return `${i + 1}. [${typeLabel}] ${att.name} — ${note}`;
}).join('\n');
- const attachmentBlock = `## User Attachments (Direct Upload)\nThe following files were uploaded directly by the user to this conversation. They are inline attachments, NOT workspace files:\n${attachmentList}`;
+ const attachmentBlock = `## User Attachments (Direct Upload)\nThe following files were uploaded directly by the user to this conversation. They are inline attachments, NOT workspace files:\n${attachmentList}\n\n**IMPORTANT**: Images listed above are already visible to you in this conversation. Do NOT call \`view_image\`, \`read_file\`, or any file tool to read them — doing so wastes a tool call and may fail (they are not workspace files).`;
systemPrompt.dynamicReminders = systemPrompt.dynamicReminders
? `${systemPrompt.dynamicReminders}\n\n---\n\n${attachmentBlock}`
@@ -1403,20 +1405,73 @@ export function registerAllIPCHandlers(
// ===== 数据管理 =====
+ /**
+ * 导出限流常量:单会话导出的最大消息条数。
+ *
+ * 背景:导出会把所有会话×所有消息×完整 toolResult 一次性 JSON.stringify 成单个 Blob
+ * 传到渲染进程(SettingsModal handleExport → new Blob([JSON.stringify(...)]))。
+ * 含 view_image 的会话单条 tool_result 就有 ~6.7MB base64 dataUrl,全量导出会直接
+ * 撑爆渲染进程堆导致 OOM 崩溃。
+ *
+ * 双重防护:
+ * 1. 剥离每条 tool 消息中的 dataUrl 等超大 base64(sanitizeExportMessage)
+ * 2. 全量导出时按会话限制消息条数(MAX_EXPORT_MESSAGES_PER_SESSION),避免超长会话
+ * 的纯文本累积也拖垮序列化
+ */
+ const MAX_EXPORT_MESSAGES_PER_SESSION = 2000;
+
+ /**
+ * 剥离导出消息中的超大 base64 字段(如 view_image 的 dataUrl)。
+ * 仅用于导出快照——原数据库数据不受影响。
+ */
+ const sanitizeExportMessage = (msg: Record): Record => {
+ const toolResult = msg.toolResult as Record | undefined;
+ if (toolResult && typeof toolResult === 'object' && 'result' in toolResult) {
+ const result = toolResult.result;
+ // result 含 dataUrl(view_image 等图片结果):剥离 dataUrl,保留 path/size/mimeType 等小字段
+ if (result && typeof result === 'object' && !Array.isArray(result) && 'dataUrl' in result) {
+ const { dataUrl: _omit, ...rest } = result as Record;
+ void _omit;
+ return {
+ ...msg,
+ toolResult: { ...toolResult, result: { ...rest, _displayNote: '[image base64 omitted in export]' } },
+ };
+ }
+ }
+ // attachments[].preview(用户上传图片的 base64 缩略图)也一并剥离,避免导出文件膨胀
+ const attachments = msg.attachments;
+ if (Array.isArray(attachments)) {
+ const sanitized = attachments.map((att) => {
+ if (att && typeof att === 'object' && 'preview' in att) {
+ const { preview: _omit, ...rest } = att as Record;
+ void _omit;
+ return { ...rest, _displayNote: '[preview omitted in export]' };
+ }
+ return att;
+ });
+ return { ...msg, attachments: sanitized };
+ }
+ return msg;
+ };
+
ipcMain.handle('data:export', async (_event, sessionId?: string) => {
try {
if (sessionId) {
- // 导出单个会话
+ // 导出单个会话:剥离超大 base64 后返回
const messages = sessionService.getMessages(sessionId);
- return { success: true, data: messages };
+ const sanitized = messages.map((m) => sanitizeExportMessage(m as unknown as Record));
+ return { success: true, data: sanitized };
}
- // 导出所有会话
+ // 导出所有会话:剥离 dataUrl + 每会话限条数,防渲染进程 Blob 序列化 OOM
const sessions = sessionService.list();
const allData: Record = { sessions: [], config: configService.getAll() };
for (const session of sessions) {
+ const rawMessages = sessionService.getMessages(session.id, { limit: MAX_EXPORT_MESSAGES_PER_SESSION });
+ const sanitizedMessages = rawMessages.map((m) => sanitizeExportMessage(m as unknown as Record));
(allData.sessions as Array>).push({
...session,
- messages: sessionService.getMessages(session.id),
+ messages: sanitizedMessages,
+ truncated: rawMessages.length >= MAX_EXPORT_MESSAGES_PER_SESSION,
});
}
return { success: true, data: allData };
diff --git a/package-lock.json b/package-lock.json
index 25c4e29..7aa7ae5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
{
"name": "metona-ai-desktop",
- "version": "0.3.21",
+ "version": "0.3.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metona-ai-desktop",
- "version": "0.3.21",
+ "version": "0.3.22",
"license": "MIT",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
- "@metona-team/metona-toast": "^0.1.2",
+ "@metona-team/metona-toast": "^0.2.1",
"@modelcontextprotocol/sdk": "^1.12.1",
"@mui/icons-material": "^9.1.1",
"@mui/material": "^9.1.2",
@@ -703,9 +703,9 @@
}
},
"node_modules/@electron/universal/node_modules/fs-extra": {
- "version": "11.3.6",
- "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.6.tgz",
- "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==",
+ "version": "11.4.0",
+ "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.4.0.tgz",
+ "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -779,9 +779,9 @@
}
},
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
- "version": "11.3.6",
- "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.6.tgz",
- "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==",
+ "version": "11.4.0",
+ "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.4.0.tgz",
+ "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1871,9 +1871,9 @@
}
},
"node_modules/@metona-team/metona-toast": {
- "version": "0.1.2",
- "resolved": "https://git.metona.cn/api/packages/MetonaTeam/npm/%40metona-team%2Fmetona-toast/-/0.1.2/metona-toast-0.1.2.tgz",
- "integrity": "sha512-r8q9CYhtYbsz2hH3ffpqNeo7hV0f5qXWiL65boisc0VXFNht0ZDNn/ZbWneYM2/bOqC7Vh7dJlHe0GSVbPJ3Lg==",
+ "version": "0.2.1",
+ "resolved": "https://git.metona.cn/api/packages/MetonaTeam/npm/%40metona-team%2Fmetona-toast/-/0.2.1/metona-toast-0.2.1.tgz",
+ "integrity": "sha512-JPK+83T6pULRbMxZ67Baxsdnggl0aGCteY8A6mLg1ePK6dLfl2WBtxH0x4Q9CL/+Llbh5ozCVKPKcUurTRjAbw==",
"license": "MIT"
},
"node_modules/@modelcontextprotocol/sdk": {
diff --git a/package.json b/package.json
index 8099f7a..86ebd94 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "metona-ai-desktop",
- "version": "0.3.21",
+ "version": "0.3.22",
"description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用",
"main": "dist-electron/main/main.js",
"author": "Metona Team",
@@ -25,6 +25,7 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
+ "@metona-team/metona-toast": "^0.2.1",
"@modelcontextprotocol/sdk": "^1.12.1",
"@mui/icons-material": "^9.1.1",
"@mui/material": "^9.1.2",
@@ -35,7 +36,6 @@
"electron-store": "^10.0.1",
"fuse.js": "^7.1.0",
"lru-cache": "^11.1.0",
- "@metona-team/metona-toast": "^0.1.2",
"nanoid": "^5.1.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
diff --git a/src/components/ConfirmationDialog.tsx b/src/components/ConfirmationDialog.tsx
index 52ed815..05ff17b 100644
--- a/src/components/ConfirmationDialog.tsx
+++ b/src/components/ConfirmationDialog.tsx
@@ -311,7 +311,6 @@ export function ConfirmationDialog(): React.JSX.Element | null {
}}
maxWidth="md"
fullWidth
- disableEscapeKeyDown={confirmAutoExecute}
slotProps={{
paper: {
sx: { bgcolor: 'background.paper' },
@@ -588,13 +587,17 @@ export function ConfirmationDialog(): React.JSX.Element | null {
{/* #40 修复: autoExecute 二次确认 Dialog — 避免误点击导致永久自动执行 */}
- {/* 审查修复: disableEscapeKeyDown + onKeyDown stopPropagation 防止 ESC 事件穿透到外层 Dialog */}
+ {/* 审查修复: MUI v9 移除了 disableEscapeKeyDown 顶层 prop,改为在 onClose 中按 reason 拦截 escapeKeyDown;
+ onKeyDown stopPropagation 阻止 ESC 事件穿透到外层 Dialog */}