fix: 修复大数据导致应用崩溃 + 图片上传冗余工具调用 + 预存类型错误
## 崩溃修复 (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
This commit is contained in:
@@ -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<string, unknown>): Record<string, unknown> => {
|
||||
const toolResult = msg.toolResult as Record<string, unknown> | 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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>));
|
||||
return { success: true, data: sanitized };
|
||||
}
|
||||
// 导出所有会话
|
||||
// 导出所有会话:剥离 dataUrl + 每会话限条数,防渲染进程 Blob 序列化 OOM
|
||||
const sessions = sessionService.list();
|
||||
const allData: Record<string, unknown> = { 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<string, unknown>));
|
||||
(allData.sessions as Array<Record<string, unknown>>).push({
|
||||
...session,
|
||||
messages: sessionService.getMessages(session.id),
|
||||
messages: sanitizedMessages,
|
||||
truncated: rawMessages.length >= MAX_EXPORT_MESSAGES_PER_SESSION,
|
||||
});
|
||||
}
|
||||
return { success: true, data: allData };
|
||||
|
||||
Reference in New Issue
Block a user