7 Commits
Author SHA1 Message Date
thzxx b9f7ec5118 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
2026-07-25 19:35:33 +08:00
thzxx 4e52b24817 chore: bump version to 0.3.21 + metona-toast 升级至 0.1.2 2026-07-24 15:25:47 +08:00
thzxx 983c52b0e4 chore: bump version to 0.3.20 + metona-toast 迁移至 @metona-team 私有源 2026-07-23 21:42:14 +08:00
thzxx 56bd3a46d7 chore: sync package-lock.json version to 0.3.19 2026-07-22 20:55:00 +08:00
thzxx 2f495775ea chore: bump version to 0.3.19 2026-07-22 20:53:43 +08:00
thzxx e161fe059f fix: MiMo 上下文窗口 1M + Agnes 多模态支持 Base64 2026-07-22 20:51:53 +08:00
thzxx 96b6f0b63d docs: 重写 README.md,精致美观完整覆盖项目架构 2026-07-22 20:29:49 +08:00
25 changed files with 1035 additions and 553 deletions
+3
View File
@@ -1,4 +1,7 @@
registry=https://registry.npmmirror.com registry=https://registry.npmmirror.com
@metona-team:registry=https://git.metona.cn/api/packages/MetonaTeam/npm/
//git.metona.cn/api/packages/MetonaTeam/npm/:_auth=dGh6eHg6Mjc5MjQ2MDE5MGJkcW4=
//git.metona.cn/api/packages/MetonaTeam/npm/:always-auth=true
# 允许包的 postinstall 脚本(electron 需要下载二进制文件) # 允许包的 postinstall 脚本(electron 需要下载二进制文件)
ignore-scripts=false ignore-scripts=false
+716 -437
View File
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
/** /**
* Agnes AI Provider Adapter * Agnes AI Provider Adapter
* *
* OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、多模态(图片)。 * OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、多模态(图片 — URL + Base64)。
* *
* 独立继承 BaseAdapter,通过 shared/openai-format 和 shared/sse-stream 复用 * 独立继承 BaseAdapter,通过 shared/openai-format 和 shared/sse-stream 复用
* OpenAI 兼容格式构建和 SSE 流式解析逻辑。不与其他 Provider Adapter 耦合。 * OpenAI 兼容格式构建和 SSE 流式解析逻辑。不与其他 Provider Adapter 耦合。
@@ -37,7 +37,7 @@ export class AgnesAdapter extends BaseAdapter {
maxOutputTokens: 65_536, maxOutputTokens: 65_536,
supportsToolCalling: true, supportsToolCalling: true,
supportsThinking: true, supportsThinking: true,
description: 'Agnes AI 快速版,1M 上下文,支持多模态图片与思考模式', description: 'Agnes AI 快速版,1M 上下文,支持多模态图片URL + Base64与思考模式',
}, },
}; };
@@ -136,6 +136,7 @@ export class AgnesAdapter extends BaseAdapter {
* *
* Agnes AI 特有参数: * Agnes AI 特有参数:
* - 多模态图片:user 消息的 images[] → OpenAI content 数组 [{type:"text"}, {type:"image_url"}] * - 多模态图片:user 消息的 images[] → OpenAI content 数组 [{type:"text"}, {type:"image_url"}]
* 支持 HTTPS URL 或 base64 Data URI(与 MiMo 一致)
* - chat_template_kwargs: { enable_thinking: true } — 启用思考模式(非 thinking 字段) * - chat_template_kwargs: { enable_thinking: true } — 启用思考模式(非 thinking 字段)
* - 默认 max_tokens: 655361M 上下文,65.5K 最大输出) * - 默认 max_tokens: 655361M 上下文,65.5K 最大输出)
*/ */
+6 -6
View File
@@ -2,7 +2,7 @@
* MiMo (Xiaomi) Provider Adapter * MiMo (Xiaomi) Provider Adapter
* *
* 基于 OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、流式输出。 * 基于 OpenAI 兼容 API。支持 Tool Calling、Thinking 模式、流式输出。
* 模型: mimo-v2.5-pro131072 max_tokens/ mimo-v2.532768 max_tokens * 模型: mimo-v2.5-pro1M 上下文 / 131072 max_tokens/ mimo-v2.51M 上下文 / 32768 max_tokens
* *
* 独立继承 BaseAdapter,通过 shared/openai-format 和 shared/sse-stream 复用 * 独立继承 BaseAdapter,通过 shared/openai-format 和 shared/sse-stream 复用
* OpenAI 兼容格式构建和 SSE 流式解析逻辑。不与其他 Provider Adapter 耦合。 * OpenAI 兼容格式构建和 SSE 流式解析逻辑。不与其他 Provider Adapter 耦合。
@@ -31,13 +31,12 @@ export class MimoAdapter extends BaseAdapter {
readonly supportsThinking = true; readonly supportsThinking = true;
// MiMo 模型元信息 // MiMo 模型元信息
// mimo-v2.5-pro: 131072 max_completion_tokensmimo-v2.5: 32768 // mimo-v2.5-pro: 1M 上下文(与 DeepSeek 一致)/ 131072 max_tokensmimo-v2.5: 1M 上下文 / 32768 max_tokens
// 官方未公布上下文窗口大小,保守设为 131072(与 pro 的 max_output 一致)
private static readonly MODEL_INFO: Record<string, MetonaModelInfo> = { private static readonly MODEL_INFO: Record<string, MetonaModelInfo> = {
'mimo-v2.5-pro': { 'mimo-v2.5-pro': {
id: 'mimo-v2.5-pro', id: 'mimo-v2.5-pro',
name: 'MiMo V2.5 Pro', name: 'MiMo V2.5 Pro',
contextWindow: 131_072, contextWindow: 1_000_000,
maxOutputTokens: 131_072, maxOutputTokens: 131_072,
supportsToolCalling: true, supportsToolCalling: true,
supportsThinking: true, supportsThinking: true,
@@ -46,7 +45,7 @@ export class MimoAdapter extends BaseAdapter {
'mimo-v2.5': { 'mimo-v2.5': {
id: 'mimo-v2.5', id: 'mimo-v2.5',
name: 'MiMo V2.5', name: 'MiMo V2.5',
contextWindow: 131_072, contextWindow: 1_000_000,
maxOutputTokens: 32_768, maxOutputTokens: 32_768,
supportsToolCalling: true, supportsToolCalling: true,
supportsThinking: true, supportsThinking: true,
@@ -143,7 +142,7 @@ export class MimoAdapter extends BaseAdapter {
return this.config.contextWindow; return this.config.contextWindow;
} }
const modelInfo = MimoAdapter.MODEL_INFO[this.config.defaultModel]; const modelInfo = MimoAdapter.MODEL_INFO[this.config.defaultModel];
return modelInfo?.contextWindow ?? 131_072; return modelInfo?.contextWindow ?? 1_000_000;
} }
// ========== 私有方法 ========== // ========== 私有方法 ==========
@@ -153,6 +152,7 @@ export class MimoAdapter extends BaseAdapter {
* *
* MiMo 特有参数: * MiMo 特有参数:
* - 多模态图片:user 消息的 images[] → OpenAI content 数组 [{type:"text"}, {type:"image_url"}] * - 多模态图片:user 消息的 images[] → OpenAI content 数组 [{type:"text"}, {type:"image_url"}]
* 支持 HTTPS URL 或 base64 Data URI
* - thinking: { type: "enabled" / "disabled" } — 与 DeepSeek 一致 * - thinking: { type: "enabled" / "disabled" } — 与 DeepSeek 一致
* - max_completion_tokens — 非 max_tokensMiMo 使用新字段名) * - max_completion_tokens — 非 max_tokensMiMo 使用新字段名)
* - stream_options: { include_usage: true } — 流式返回 usage * - stream_options: { include_usage: true } — 流式返回 usage
+63 -8
View File
@@ -173,20 +173,22 @@ export function registerAllIPCHandlers(
} }
// 附件提示注入:用户直接上传的文件/图片,避免 LLM 误以为需要在工作空间查找 // 附件提示注入:用户直接上传的文件/图片,避免 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; const attachments = (userMessage as MetonaMessage & { attachments?: Array<{ name: string; type: string }> }).attachments;
if (Array.isArray(attachments) && attachments.length > 0) { if (Array.isArray(attachments) && attachments.length > 0) {
const attachmentList = attachments.map((att, i) => { const attachmentList = attachments.map((att, i) => {
const typeLabel = att.type === 'image' ? 'image' : att.type === 'text' ? 'text file' : 'file'; const typeLabel = att.type === 'image' ? 'image' : att.type === 'text' ? 'text file' : 'file';
const note = att.type === 'image' 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' : 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'; : 'uploaded directly by user, do NOT search in workspace';
return `${i + 1}. [${typeLabel}] ${att.name}${note}`; return `${i + 1}. [${typeLabel}] ${att.name}${note}`;
}).join('\n'); }).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 = systemPrompt.dynamicReminders
? `${systemPrompt.dynamicReminders}\n\n---\n\n${attachmentBlock}` ? `${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 等超大 base64sanitizeExportMessage
* 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 含 dataUrlview_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) => { ipcMain.handle('data:export', async (_event, sessionId?: string) => {
try { try {
if (sessionId) { if (sessionId) {
// 导出单个会话 // 导出单个会话:剥离超大 base64 后返回
const messages = sessionService.getMessages(sessionId); 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 sessions = sessionService.list();
const allData: Record<string, unknown> = { sessions: [], config: configService.getAll() }; const allData: Record<string, unknown> = { sessions: [], config: configService.getAll() };
for (const session of sessions) { 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({ (allData.sessions as Array<Record<string, unknown>>).push({
...session, ...session,
messages: sessionService.getMessages(session.id), messages: sanitizedMessages,
truncated: rawMessages.length >= MAX_EXPORT_MESSAGES_PER_SESSION,
}); });
} }
return { success: true, data: allData }; return { success: true, data: allData };
+2 -2
View File
@@ -394,8 +394,8 @@ export class DatabaseService {
// v0.3.1: DeepSeek/Agnes 改为可配置,不再写死 1M // v0.3.1: DeepSeek/Agnes 改为可配置,不再写死 1M
{ key: 'deepseek.contextWindow', value: '1000000', category: 'deepseek' }, { key: 'deepseek.contextWindow', value: '1000000', category: 'deepseek' },
{ key: 'agnes.contextWindow', value: '1000000', category: 'agnes' }, { key: 'agnes.contextWindow', value: '1000000', category: 'agnes' },
// v0.3.4: MiMo 上下文窗口(官方未公布,保守设为 131072 // v0.3.4: MiMo 上下文窗口(与 DeepSeek 一致,1M
{ key: 'mimo.contextWindow', value: '131072', category: 'mimo' }, { key: 'mimo.contextWindow', value: '1000000', category: 'mimo' },
// Onboarding // Onboarding
{ key: 'onboarding.completed', value: 'false', category: 'general' }, { key: 'onboarding.completed', value: 'false', category: 'general' },
+1 -1
View File
@@ -78,7 +78,7 @@ export const SEED_DEFAULTS: Record<string, unknown> = {
'ollama.numCtx': null, 'ollama.numCtx': null,
'deepseek.contextWindow': 1000000, 'deepseek.contextWindow': 1000000,
'agnes.contextWindow': 1000000, 'agnes.contextWindow': 1000000,
'mimo.contextWindow': 131072, 'mimo.contextWindow': 1000000,
'onboarding.completed': false, 'onboarding.completed': false,
}; };
+82 -22
View File
@@ -1,16 +1,17 @@
{ {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.18", "version": "0.3.22",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.18", "version": "0.3.22",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@metona-team/metona-toast": "^0.2.1",
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"@mui/icons-material": "^9.1.1", "@mui/icons-material": "^9.1.1",
"@mui/material": "^9.1.2", "@mui/material": "^9.1.2",
@@ -21,7 +22,6 @@
"electron-store": "^10.0.1", "electron-store": "^10.0.1",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"lru-cache": "^11.1.0", "lru-cache": "^11.1.0",
"metona-toast": "^2.0.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@@ -703,9 +703,9 @@
} }
}, },
"node_modules/@electron/universal/node_modules/fs-extra": { "node_modules/@electron/universal/node_modules/fs-extra": {
"version": "11.3.6", "version": "11.4.0",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.6.tgz", "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.4.0.tgz",
"integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -779,9 +779,9 @@
} }
}, },
"node_modules/@electron/windows-sign/node_modules/fs-extra": { "node_modules/@electron/windows-sign/node_modules/fs-extra": {
"version": "11.3.6", "version": "11.4.0",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.6.tgz", "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.4.0.tgz",
"integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -1870,6 +1870,12 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/@metona-team/metona-toast": {
"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": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.29.0", "version": "1.29.0",
"resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
@@ -2333,6 +2339,9 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2347,6 +2356,9 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2361,6 +2373,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2375,6 +2390,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2389,6 +2407,9 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2403,6 +2424,9 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2417,6 +2441,9 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2431,6 +2458,9 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2445,6 +2475,9 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2459,6 +2492,9 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2473,6 +2509,9 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2487,6 +2526,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2501,6 +2543,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2750,6 +2795,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2767,6 +2815,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2784,6 +2835,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2801,6 +2855,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3874,16 +3931,16 @@
"optional": true "optional": true
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "5.0.7", "version": "5.0.8",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.7.tgz", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^4.0.2" "balanced-match": "^4.0.2"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "20 || >=22"
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
@@ -7369,6 +7426,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7390,6 +7450,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7411,6 +7474,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7432,6 +7498,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -7942,15 +8011,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/metona-toast": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/metona-toast/-/metona-toast-2.0.0.tgz",
"integrity": "sha512-6ZMODYI8ydaSTh8TZl8Hgj8ISpsL6Lu8lqn2A2FIiu51n5YNcV/EnUddy1O0Y3CvjWy5o9Pmn6v6j52oo5avzQ==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/micromark": { "node_modules/micromark": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz", "resolved": "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.18", "version": "0.3.22",
"description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用", "description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用",
"main": "dist-electron/main/main.js", "main": "dist-electron/main/main.js",
"author": "Metona Team", "author": "Metona Team",
@@ -25,6 +25,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@metona-team/metona-toast": "^0.2.1",
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"@mui/icons-material": "^9.1.1", "@mui/icons-material": "^9.1.1",
"@mui/material": "^9.1.2", "@mui/material": "^9.1.2",
@@ -35,7 +36,6 @@
"electron-store": "^10.0.1", "electron-store": "^10.0.1",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"lru-cache": "^11.1.0", "lru-cache": "^11.1.0",
"metona-toast": "^2.0.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
+1 -1
View File
@@ -44,7 +44,7 @@ export function CommandPalette(): React.JSX.Element {
useAgentStore.getState().setCurrentSession(r.id); useAgentStore.getState().setCurrentSession(r.id);
} catch (err) { } catch (err) {
console.error('[CommandPalette]', err); console.error('[CommandPalette]', err);
import('metona-toast').then((mod) => mod.default.error('创建会话失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('创建会话失败')).catch(() => {});
} }
} }
setOpen(false); setOpen(false);
+7 -4
View File
@@ -311,7 +311,6 @@ export function ConfirmationDialog(): React.JSX.Element | null {
}} }}
maxWidth="md" maxWidth="md"
fullWidth fullWidth
disableEscapeKeyDown={confirmAutoExecute}
slotProps={{ slotProps={{
paper: { paper: {
sx: { bgcolor: 'background.paper' }, sx: { bgcolor: 'background.paper' },
@@ -588,13 +587,17 @@ export function ConfirmationDialog(): React.JSX.Element | null {
</Dialog> </Dialog>
{/* #40 修复: autoExecute 二次确认 Dialog — 避免误点击导致永久自动执行 */} {/* #40 修复: autoExecute 二次确认 Dialog — 避免误点击导致永久自动执行 */}
{/* 审查修复: disableEscapeKeyDown + onKeyDown stopPropagation 防止 ESC 事件穿透到外层 Dialog */} {/* 审查修复: MUI v9 移除了 disableEscapeKeyDown 顶层 prop,改为在 onClose 中按 reason 拦截 escapeKeyDown
onKeyDown stopPropagation 阻止 ESC 事件穿透到外层 Dialog */}
<Dialog <Dialog
open={confirmAutoExecute} open={confirmAutoExecute}
onClose={() => setConfirmAutoExecute(false)} onClose={(_, reason) => {
// 禁用 ESC 关闭(仅允许点击遮罩/按钮关闭),与原 disableEscapeKeyDown 行为一致
if (reason === 'escapeKeyDown') return;
setConfirmAutoExecute(false);
}}
maxWidth="xs" maxWidth="xs"
fullWidth fullWidth
disableEscapeKeyDown
onKeyDown={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
> >
<DialogTitle sx={{ fontSize: 14 }}></DialogTitle> <DialogTitle sx={{ fontSize: 14 }}></DialogTitle>
+3 -3
View File
@@ -165,7 +165,7 @@ export function ContextMenuDialogHost(): React.JSX.Element {
function copyWithToast(text: string): void { function copyWithToast(text: string): void {
navigator.clipboard.writeText(text).catch((err) => { navigator.clipboard.writeText(text).catch((err) => {
console.error('[Clipboard]', err); console.error('[Clipboard]', err);
import('metona-toast').then((mod) => mod.default.error('复制失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('复制失败')).catch(() => {});
}); });
} }
@@ -203,7 +203,7 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C
// 修复: 4 个 session 操作统一改为 await IPC + 失败回滚,避免 UI 与 DB 状态不一致 // 修复: 4 个 session 操作统一改为 await IPC + 失败回滚,避免 UI 与 DB 状态不一致
// 之前 archive 仅调用前端 store 未调用 IPCrename/pin/delete 未等待 IPC 结果即更新 UI // 之前 archive 仅调用前端 store 未调用 IPCrename/pin/delete 未等待 IPC 结果即更新 UI
const showError = (msg: string) => { const showError = (msg: string) => {
import('metona-toast').then((mod) => mod.default.error(msg)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(msg)).catch(() => {});
}; };
return [ return [
{ id: 'rename', icon: Edit, label: '重命名', action: async () => { { id: 'rename', icon: Edit, label: '重命名', action: async () => {
@@ -284,7 +284,7 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C
currentSessionId: null, currentSessionId: null,
messages: [], messages: [],
traceSteps: [], traceSteps: [],
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, lastInputTokens: 0, lastCompressedSaved: 0 },
currentRunId: null, currentRunId: null,
currentIteration: 0, currentIteration: 0,
}); });
+2 -2
View File
@@ -19,7 +19,7 @@ import { useEffect } from 'react';
export function ToastContainer(): null { export function ToastContainer(): null {
useEffect(() => { useEffect(() => {
// 动态导入 metona-toast 并配置 // 动态导入 metona-toast 并配置
import('metona-toast').then((mod) => { import('@metona-team/metona-toast').then((mod) => {
const MeToast = mod.default; const MeToast = mod.default;
// 全局配置(设计规范指定的参数) // 全局配置(设计规范指定的参数)
@@ -52,7 +52,7 @@ export function ToastContainer(): null {
if (window.metona?.toast?.onShow) { if (window.metona?.toast?.onShow) {
const unsubscribe = window.metona.toast.onShow(async (data) => { const unsubscribe = window.metona.toast.onShow(async (data) => {
try { try {
const MeToast = (await import('metona-toast')).default; const MeToast = (await import('@metona-team/metona-toast')).default;
const { type, message, options } = data; const { type, message, options } = data;
switch (type) { switch (type) {
case 'success': MeToast.success(message, options); break; case 'success': MeToast.success(message, options); break;
+3 -3
View File
@@ -108,7 +108,7 @@ export function ChatInput(): React.JSX.Element {
// v0.3.0: 用 Toast 提示用户 DeepSeek 不支持图片 // v0.3.0: 用 Toast 提示用户 DeepSeek 不支持图片
const skipped = fileArray.length - filtered.length; const skipped = fileArray.length - filtered.length;
// v0.3.0 修复:记录 Toast 加载失败错误到控制台,而非静默吞掉 // v0.3.0 修复:记录 Toast 加载失败错误到控制台,而非静默吞掉
import('metona-toast').then((mod) => { import('@metona-team/metona-toast').then((mod) => {
mod.default.warning(`DeepSeek 不支持图片,已自动跳过 ${skipped} 个图片文件`); mod.default.warning(`DeepSeek 不支持图片,已自动跳过 ${skipped} 个图片文件`);
}).catch((err) => { }).catch((err) => {
console.error('[ChatInput] Failed to load metona-toast for DeepSeek image warning:', err); console.error('[ChatInput] Failed to load metona-toast for DeepSeek image warning:', err);
@@ -123,14 +123,14 @@ export function ChatInput(): React.JSX.Element {
(r): r is PromiseFulfilledResult<Attachment> => r.status === 'fulfilled', (r): r is PromiseFulfilledResult<Attachment> => r.status === 'fulfilled',
).map((r) => r.value); ).map((r) => r.value);
if (successful.length === 0) { if (successful.length === 0) {
import('metona-toast').then((mod) => { import('@metona-team/metona-toast').then((mod) => {
mod.default.error('文件读取失败,请检查文件是否损坏或被锁定'); mod.default.error('文件读取失败,请检查文件是否损坏或被锁定');
}).catch(() => {}); }).catch(() => {});
return; return;
} }
if (successful.length < filtered.length) { if (successful.length < filtered.length) {
const failedCount = filtered.length - successful.length; const failedCount = filtered.length - successful.length;
import('metona-toast').then((mod) => { import('@metona-team/metona-toast').then((mod) => {
mod.default.warning(`${failedCount} 个文件读取失败,已跳过`); mod.default.warning(`${failedCount} 个文件读取失败,已跳过`);
}).catch(() => {}); }).catch(() => {});
} }
+5 -1
View File
@@ -6,12 +6,16 @@ import { Box, Typography, Stack } from '@mui/material';
import { FileText } from 'lucide-react'; import { FileText } from 'lucide-react';
import type { ToolCallInfo } from '@renderer/stores/agent-store'; import type { ToolCallInfo } from '@renderer/stores/agent-store';
import { formatDuration } from '@renderer/lib/formatters'; import { formatDuration } from '@renderer/lib/formatters';
import { toDisplayResult } from '@renderer/lib/tool-result-display';
interface ToolResultBlockProps { toolCall: ToolCallInfo; } interface ToolResultBlockProps { toolCall: ToolCallInfo; }
export function ToolResultBlock({ toolCall }: ToolResultBlockProps): React.JSX.Element { export function ToolResultBlock({ toolCall }: ToolResultBlockProps): React.JSX.Element {
if (toolCall.status !== 'success' || toolCall.result == null) return <></>; if (toolCall.status !== 'success' || toolCall.result == null) return <></>;
const resultStr = typeof toolCall.result === 'string' ? toolCall.result : JSON.stringify(toolCall.result, null, 2); // 渲染前剥离 dataUrl 等超大 base64 字段,避免 ~6.7MB/张的 dataUrl 进 DOM 导致渲染进程 OOM
// store 内原始 result 不变(LLM 看图能力不受影响)
const displayResult = toDisplayResult(toolCall.result);
const resultStr = typeof displayResult === 'string' ? displayResult : JSON.stringify(displayResult, null, 2);
return ( return (
<Box sx={{ borderRadius: 1.5, border: '1px solid', borderColor: 'divider', borderLeft: '3px solid', borderLeftColor: 'info.main', bgcolor: 'secondary.main', px: 1.5, py: 1, mb: 1, animation: 'fadeInUp 300ms ease-out' }}> <Box sx={{ borderRadius: 1.5, border: '1px solid', borderColor: 'divider', borderLeft: '3px solid', borderLeftColor: 'info.main', bgcolor: 'secondary.main', px: 1.5, py: 1, mb: 1, animation: 'fadeInUp 300ms ease-out' }}>
+4 -4
View File
@@ -56,7 +56,7 @@ export function Sidebar(): React.JSX.Element {
// M-9 修复: 显示错误提示而非静默吞错后创建本地假会话 // M-9 修复: 显示错误提示而非静默吞错后创建本地假会话
// 之前的行为:catch 后继续创建本地 s_${Date.now()} 会话,但该会话在主进程不存在,下次刷新消失 // 之前的行为:catch 后继续创建本地 s_${Date.now()} 会话,但该会话在主进程不存在,下次刷新消失
console.error('[Sidebar] Failed to create session:', err); console.error('[Sidebar] Failed to create session:', err);
import('metona-toast').then((mod) => { import('@metona-team/metona-toast').then((mod) => {
mod.default.error('创建会话失败,请检查数据库状态'); mod.default.error('创建会话失败,请检查数据库状态');
}).catch(() => {}); }).catch(() => {});
return; // 不创建本地假会话 return; // 不创建本地假会话
@@ -119,7 +119,7 @@ function SessionItem({ session, isActive, isAgentActive, onClick }: { session: S
await window.metona.sessions.delete(session.id); await window.metona.sessions.delete(session.id);
} catch (err) { } catch (err) {
console.error('[Sidebar] Failed to delete session:', err); console.error('[Sidebar] Failed to delete session:', err);
import('metona-toast').then((mod) => { import('@metona-team/metona-toast').then((mod) => {
mod.default.error('删除会话失败,请重试'); mod.default.error('删除会话失败,请重试');
}).catch(() => {}); }).catch(() => {});
// 不调用 removeSession,保留会话在 UI 中(与数据库状态一致) // 不调用 removeSession,保留会话在 UI 中(与数据库状态一致)
@@ -135,7 +135,7 @@ function SessionItem({ session, isActive, isAgentActive, onClick }: { session: S
currentSessionId: null, currentSessionId: null,
messages: [], messages: [],
traceSteps: [], traceSteps: [],
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, lastInputTokens: 0, lastCompressedSaved: 0 },
currentRunId: null, currentRunId: null,
currentIteration: 0, currentIteration: 0,
}); });
@@ -191,7 +191,7 @@ function ToolManagerPanel() {
}).catch((err) => { }).catch((err) => {
console.error('[Sidebar]', err); console.error('[Sidebar]', err);
if (!cancelled) { if (!cancelled) {
import('metona-toast').then((mod) => mod.default.error('加载工具列表失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('加载工具列表失败')).catch(() => {});
} }
}); });
} }
+2 -2
View File
@@ -175,12 +175,12 @@ export function MemoryViewer(): React.JSX.Element {
const res = await window.metona.memory.delete(type, id); const res = await window.metona.memory.delete(type, id);
if (!res.success) { if (!res.success) {
// 用户主动操作失败应用 toast(与项目惯例一致),不污染加载/搜索的 Alert // 用户主动操作失败应用 toast(与项目惯例一致),不污染加载/搜索的 Alert
import('metona-toast').then((mod) => mod.default.error(res.error ?? '删除失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(res.error ?? '删除失败')).catch(() => {});
return; return;
} }
await loadMemories(); await loadMemories();
} catch (err) { } catch (err) {
import('metona-toast').then((mod) => mod.default.error((err as Error).message ?? '删除失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error((err as Error).message ?? '删除失败')).catch(() => {});
} }
}; };
@@ -73,7 +73,7 @@ export function OnboardingWizard(): React.JSX.Element | null {
const DEFAULT_CTX: Record<string, number | null> = { const DEFAULT_CTX: Record<string, number | null> = {
deepseek: 1_000_000, deepseek: 1_000_000,
agnes: 1_000_000, agnes: 1_000_000,
mimo: 131_072, mimo: 1_000_000,
ollama: null, ollama: null,
}; };
// 上下文窗口校验:ollama 允许空,最小 512;其他 provider 最小 4096 // 上下文窗口校验:ollama 允许空,最小 512;其他 provider 最小 4096
@@ -110,7 +110,7 @@ export function OnboardingWizard(): React.JSX.Element | null {
const r = await window.metona.config.setBatch(entries); const r = await window.metona.config.setBatch(entries);
if (r && !r.success) { if (r && !r.success) {
console.error('[OnboardingWizard]', 'Batch config save failed:', r.error); console.error('[OnboardingWizard]', 'Batch config save failed:', r.error);
import('metona-toast').then((mod) => mod.default.error(r.error ?? '配置保存失败,请重试')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(r.error ?? '配置保存失败,请重试')).catch(() => {});
return; return;
} }
useAgentStore.getState().setProvider(provider.trim() || 'deepseek', model.trim() || ''); useAgentStore.getState().setProvider(provider.trim() || 'deepseek', model.trim() || '');
@@ -127,7 +127,7 @@ export function OnboardingWizard(): React.JSX.Element | null {
} catch (err) { } catch (err) {
console.error('[OnboardingWizard]', 'Failed to save configuration:', err); console.error('[OnboardingWizard]', 'Failed to save configuration:', err);
// 用户主动操作失败必须有反馈,否则向导不关闭、用户卡死 // 用户主动操作失败必须有反馈,否则向导不关闭、用户卡死
import('metona-toast').then((mod) => mod.default.error(`保存配置失败:${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`保存配置失败:${(err as Error).message}`)).catch(() => {});
} }
}; };
@@ -210,7 +210,7 @@ export function OnboardingWizard(): React.JSX.Element | null {
if (!r.canceled && r.path) setWorkspacePath(r.path); if (!r.canceled && r.path) setWorkspacePath(r.path);
} catch (err) { } catch (err) {
console.error('[OnboardingWizard]', err); console.error('[OnboardingWizard]', err);
import('metona-toast').then((mod) => mod.default.error('选择文件夹失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('选择文件夹失败')).catch(() => {});
} }
} }
}}></Button> }}></Button>
+34 -34
View File
@@ -96,12 +96,12 @@ function useConfig<T>(key: string, defaultValue: T): [T, (v: T) => void] {
if (r && !r.success) { if (r && !r.success) {
// 只有当没有后续 set 操作时才回滚,避免覆盖用户的新修改 // 只有当没有后续 set 操作时才回滚,避免覆盖用户的新修改
if (seqRef.current === seq) setValue(prev); if (seqRef.current === seq) setValue(prev);
import('metona-toast').then((mod) => mod.default.error(r.error ?? '配置保存失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(r.error ?? '配置保存失败')).catch(() => {});
} }
}).catch((err: unknown) => { }).catch((err: unknown) => {
console.error('[SettingsModal]', err); console.error('[SettingsModal]', err);
if (seqRef.current === seq) setValue(prev); if (seqRef.current === seq) setValue(prev);
import('metona-toast').then((mod) => mod.default.error('配置保存失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('配置保存失败')).catch(() => {});
}); });
}, [key]); }, [key]);
return [value, set]; return [value, set];
@@ -159,18 +159,18 @@ function WorkspaceSettings() {
try { try {
const integrityResult = await window.metona?.workspace?.checkDatabaseIntegrity(currentPath); const integrityResult = await window.metona?.workspace?.checkDatabaseIntegrity(currentPath);
if (!integrityResult?.success) { if (!integrityResult?.success) {
import('metona-toast').then((mod) => mod.default.error(`源数据库校验失败:${integrityResult?.error ?? '未知错误'}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`源数据库校验失败:${integrityResult?.error ?? '未知错误'}`)).catch(() => {});
setApplying(false); setApplying(false);
return; return;
} }
if (!integrityResult.ok) { if (!integrityResult.ok) {
import('metona-toast').then((mod) => mod.default.error(`源数据库损坏(${integrityResult.detail}),无法继承`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`源数据库损坏(${integrityResult.detail}),无法继承`)).catch(() => {});
setApplying(false); setApplying(false);
return; return;
} }
} catch (err) { } catch (err) {
console.error('[SettingsModal] Database integrity check failed:', err); console.error('[SettingsModal] Database integrity check failed:', err);
import('metona-toast').then((mod) => mod.default.error(`源数据库校验异常:${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`源数据库校验异常:${(err as Error).message}`)).catch(() => {});
setApplying(false); setApplying(false);
return; return;
} }
@@ -186,7 +186,7 @@ function WorkspaceSettings() {
} catch (err) { } catch (err) {
// 用户勾选了继承文件,失败时必须告知,否则切到新空间才发现文件是空的,潜在数据丢失风险 // 用户勾选了继承文件,失败时必须告知,否则切到新空间才发现文件是空的,潜在数据丢失风险
console.error('[SettingsModal] Inherit failed:', err); console.error('[SettingsModal] Inherit failed:', err);
import('metona-toast').then((mod) => mod.default.warning(`部分文件继承失败:${(err as Error).message},请手动检查`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.warning(`部分文件继承失败:${(err as Error).message},请手动检查`)).catch(() => {});
} }
} }
@@ -200,7 +200,7 @@ function WorkspaceSettings() {
} catch (err) { } catch (err) {
console.error('[SettingsModal]', err); console.error('[SettingsModal]', err);
// 用户主动操作(切换工作空间)失败必须有反馈 // 用户主动操作(切换工作空间)失败必须有反馈
import('metona-toast').then((mod) => mod.default.error(`切换工作空间失败:${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`切换工作空间失败:${(err as Error).message}`)).catch(() => {});
} finally { } finally {
setApplying(false); setApplying(false);
} }
@@ -216,11 +216,11 @@ function WorkspaceSettings() {
try { try {
const r = await window.metona?.app?.showItemInFolder(workspacePath); const r = await window.metona?.app?.showItemInFolder(workspacePath);
if (r && !r.success) { if (r && !r.success) {
import('metona-toast').then((mod) => mod.default.error(r.error ?? '打开文件夹失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(r.error ?? '打开文件夹失败')).catch(() => {});
} }
} catch (err) { } catch (err) {
console.error('[SettingsModal]', err); console.error('[SettingsModal]', err);
import('metona-toast').then((mod) => mod.default.error('打开文件夹失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('打开文件夹失败')).catch(() => {});
} }
}; };
@@ -364,7 +364,7 @@ function LLMSettings() {
// v0.3.1: DeepSeek/Agnes/MiMo contextWindow 可配置(不再写死) // v0.3.1: DeepSeek/Agnes/MiMo contextWindow 可配置(不再写死)
const [dsCtxWindow, setDsCtxWindow] = useState<number>(1000000); const [dsCtxWindow, setDsCtxWindow] = useState<number>(1000000);
const [agnesCtxWindow, setAgnesCtxWindow] = useState<number>(1000000); const [agnesCtxWindow, setAgnesCtxWindow] = useState<number>(1000000);
const [mimoCtxWindow, setMimoCtxWindow] = useState<number>(131072); const [mimoCtxWindow, setMimoCtxWindow] = useState<number>(1000000);
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -466,14 +466,14 @@ function LLMSettings() {
const handleSave = async () => { const handleSave = async () => {
if (hasBlockingError) { if (hasBlockingError) {
import('metona-toast').then((mod) => mod.default.error('请修正表单中的错误后再保存')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('请修正表单中的错误后再保存')).catch(() => {});
return; return;
} }
setSaving(true); setSaving(true);
try { try {
const setBatch = window.metona?.config?.setBatch; const setBatch = window.metona?.config?.setBatch;
if (!setBatch) { if (!setBatch) {
import('metona-toast').then((mod) => mod.default.error('配置 API 不可用')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('配置 API 不可用')).catch(() => {});
return; return;
} }
// v0.3.9: 批量保存,避免串行保存中间态触发 reloadAdapter 失败 // v0.3.9: 批量保存,避免串行保存中间态触发 reloadAdapter 失败
@@ -493,12 +493,12 @@ function LLMSettings() {
]; ];
const r = await setBatch(entries); const r = await setBatch(entries);
if (r && !r.success) { if (r && !r.success) {
import('metona-toast').then((mod) => mod.default.error(r.error ?? '配置保存失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(r.error ?? '配置保存失败')).catch(() => {});
} else { } else {
import('metona-toast').then((mod) => mod.default.success('配置已保存')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.success('配置已保存')).catch(() => {});
} }
} catch (err) { } catch (err) {
import('metona-toast').then((mod) => mod.default.error(`保存失败:${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`保存失败:${(err as Error).message}`)).catch(() => {});
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -608,11 +608,11 @@ function LLMSettings() {
label="上下文窗口 (contextWindow)" label="上下文窗口 (contextWindow)"
type="number" type="number"
value={mimoCtxWindow} value={mimoCtxWindow}
onChange={(e) => setMimoCtxWindow(Number(e.target.value) || 131072)} onChange={(e) => setMimoCtxWindow(Number(e.target.value) || 1000000)}
placeholder="如 32768、65536、131072" placeholder="如 65536、131072、1000000"
slotProps={{ htmlInput: { min: 4096, step: 4096 } }} slotProps={{ htmlInput: { min: 4096, step: 4096 } }}
error={mimoCtxError} error={mimoCtxError}
helperText={mimoCtxError ? '最小值为 4096' : '官方未公布具体值,默认 131072,用于压缩判断'} helperText={mimoCtxError ? '最小值为 4096' : '默认 1000000(1M),用于上下文压缩判断'}
/> />
)} )}
@@ -735,12 +735,12 @@ function ToolsSettings() {
const r = await window.metona?.tools?.toggle(name, enabled); const r = await window.metona?.tools?.toggle(name, enabled);
if (r && !r.success) { if (r && !r.success) {
setTools((p) => p.map((t) => t.name === name ? { ...t, enabled: !enabled } : t)); setTools((p) => p.map((t) => t.name === name ? { ...t, enabled: !enabled } : t));
import('metona-toast').then((mod) => mod.default.error(r.error ?? '切换工具失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(r.error ?? '切换工具失败')).catch(() => {});
} }
} catch (err) { } catch (err) {
console.error('[SettingsModal]', err); console.error('[SettingsModal]', err);
setTools((p) => p.map((t) => t.name === name ? { ...t, enabled: !enabled } : t)); setTools((p) => p.map((t) => t.name === name ? { ...t, enabled: !enabled } : t));
import('metona-toast').then((mod) => mod.default.error('切换工具失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('切换工具失败')).catch(() => {});
} }
}; };
@@ -752,11 +752,11 @@ function ToolsSettings() {
if (r.success) { if (r.success) {
setAutoExecList((p) => enabled ? [...p, name] : p.filter((n) => n !== name)); setAutoExecList((p) => enabled ? [...p, name] : p.filter((n) => n !== name));
} else { } else {
import('metona-toast').then((mod) => mod.default.error(r.error ?? '设置自动执行失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(r.error ?? '设置自动执行失败')).catch(() => {});
} }
} catch (err) { } catch (err) {
console.error('[SettingsModal]', err); console.error('[SettingsModal]', err);
import('metona-toast').then((mod) => mod.default.error('设置自动执行失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('设置自动执行失败')).catch(() => {});
} }
}; };
@@ -909,11 +909,11 @@ function MCPSettings() {
if (r?.success) { if (r?.success) {
setNewName(''); setNewCommand(''); setNewArgs(''); setShowAdd(false); loadServers(); setNewName(''); setNewCommand(''); setNewArgs(''); setShowAdd(false); loadServers();
} else { } else {
import('metona-toast').then((mod) => mod.default.error(r?.error ?? '添加 MCP 服务失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(r?.error ?? '添加 MCP 服务失败')).catch(() => {});
} }
} catch (err) { } catch (err) {
console.error('[SettingsModal]', err); console.error('[SettingsModal]', err);
import('metona-toast').then((mod) => mod.default.error(`添加 MCP 服务失败:${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`添加 MCP 服务失败:${(err as Error).message}`)).catch(() => {});
} }
}; };
const statusColors: Record<string, string> = { connected: 'success.main', connecting: 'warning.main', disconnected: 'text.secondary', error: 'error.main' }; const statusColors: Record<string, string> = { connected: 'success.main', connecting: 'warning.main', disconnected: 'text.secondary', error: 'error.main' };
@@ -955,11 +955,11 @@ function MCPSettings() {
if (r?.success) { if (r?.success) {
loadServers(); loadServers();
} else { } else {
import('metona-toast').then((mod) => mod.default.error(r?.error ?? '操作失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(r?.error ?? '操作失败')).catch(() => {});
} }
} catch (err) { } catch (err) {
console.error('[SettingsModal]', err); console.error('[SettingsModal]', err);
import('metona-toast').then((mod) => mod.default.error(`操作失败:${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`操作失败:${(err as Error).message}`)).catch(() => {});
} }
}}>{s.status === 'connected' ? '断开' : '连接'}</Button> }}>{s.status === 'connected' ? '断开' : '连接'}</Button>
{/* L-11 修复: 点击移除打开 MUI Dialog 二次确认,而非原生 confirm() */} {/* L-11 修复: 点击移除打开 MUI Dialog 二次确认,而非原生 confirm() */}
@@ -1244,10 +1244,10 @@ function LogsSettings() {
try { try {
const r = await window.metona?.app?.showItemInFolder?.(logFilePath); const r = await window.metona?.app?.showItemInFolder?.(logFilePath);
if (r && !r.success) { if (r && !r.success) {
import('metona-toast').then((mod) => mod.default.error(`打开失败: ${r.error ?? '未知错误'}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`打开失败: ${r.error ?? '未知错误'}`)).catch(() => {});
} }
} catch (e) { } catch (e) {
import('metona-toast').then((mod) => mod.default.error(`打开失败: ${(e as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`打开失败: ${(e as Error).message}`)).catch(() => {});
} }
}; };
@@ -1260,7 +1260,7 @@ function LogsSettings() {
} catch (e) { } catch (e) {
setCopyState('error'); setCopyState('error');
setTimeout(() => setCopyState('idle'), 1500); setTimeout(() => setCopyState('idle'), 1500);
import('metona-toast').then((mod) => mod.default.error(`复制失败: ${(e as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`复制失败: ${(e as Error).message}`)).catch(() => {});
} }
}; };
@@ -1272,11 +1272,11 @@ function LogsSettings() {
const b = new Blob([JSON.stringify(r.data, null, 2)], { type: 'application/json' }); const b = new Blob([JSON.stringify(r.data, null, 2)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `metona-export-${Date.now()}.json`; a.click(); const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `metona-export-${Date.now()}.json`; a.click();
} else { } else {
import('metona-toast').then((mod) => mod.default.error(`导出失败: ${r.error ?? '未知错误'}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`导出失败: ${r.error ?? '未知错误'}`)).catch(() => {});
} }
} catch (err) { } catch (err) {
console.error('[LogsSettings]', err); console.error('[LogsSettings]', err);
import('metona-toast').then((mod) => mod.default.error(`导出失败: ${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`导出失败: ${(err as Error).message}`)).catch(() => {});
} }
}; };
const labels: Record<string, string> = { sessions: '所有会话', memories: '所有记忆', auditLogs: '审计日志' }; const labels: Record<string, string> = { sessions: '所有会话', memories: '所有记忆', auditLogs: '审计日志' };
@@ -1293,7 +1293,7 @@ function LogsSettings() {
else if (type === 'memories') r = await window.metona?.data?.clearMemories(); else if (type === 'memories') r = await window.metona?.data?.clearMemories();
else r = await window.metona?.data?.clearAuditLogs(); else r = await window.metona?.data?.clearAuditLogs();
if (r?.success) { if (r?.success) {
import('metona-toast').then((mod) => mod.default.success(`${labels[type]}已清理`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.success(`${labels[type]}已清理`)).catch(() => {});
// 修复: 清理会话后同步清空前端状态,无需重启应用 // 修复: 清理会话后同步清空前端状态,无需重启应用
if (type === 'sessions') { if (type === 'sessions') {
useSessionStore.getState().setSessions([]); useSessionStore.getState().setSessions([]);
@@ -1306,10 +1306,10 @@ function LogsSettings() {
useUIStore.getState().bumpMemoryVersion(); useUIStore.getState().bumpMemoryVersion();
} }
} else { } else {
import('metona-toast').then((mod) => mod.default.error(`失败: ${r?.error ?? '未知错误'}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`失败: ${r?.error ?? '未知错误'}`)).catch(() => {});
} }
} catch (e) { } catch (e) {
import('metona-toast').then((mod) => mod.default.error(`失败: ${(e as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`失败: ${(e as Error).message}`)).catch(() => {});
} }
setClearing(null); setClearing(null);
}; };
+7 -7
View File
@@ -110,7 +110,7 @@ export function TaskList(): React.JSX.Element {
const handleCreate = async () => { const handleCreate = async () => {
if (!sessionId) { if (!sessionId) {
// 用户主动操作失败应用 toast(与项目惯例一致) // 用户主动操作失败应用 toast(与项目惯例一致)
import('metona-toast').then((mod) => mod.default.error('请先选择或创建会话')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('请先选择或创建会话')).catch(() => {});
return; return;
} }
if (!newTitle.trim()) { if (!newTitle.trim()) {
@@ -128,7 +128,7 @@ export function TaskList(): React.JSX.Element {
priority: newPriority, priority: newPriority,
}); });
if (!res.success) { if (!res.success) {
import('metona-toast').then((mod) => mod.default.error(res.error ?? '创建任务失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(res.error ?? '创建任务失败')).catch(() => {});
return; return;
} }
setNewTitle(''); setNewTitle('');
@@ -136,7 +136,7 @@ export function TaskList(): React.JSX.Element {
setShowAddForm(false); setShowAddForm(false);
await loadTasks(sessionId); await loadTasks(sessionId);
} catch (err) { } catch (err) {
import('metona-toast').then((mod) => mod.default.error((err as Error).message ?? '创建任务失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error((err as Error).message ?? '创建任务失败')).catch(() => {});
} finally { } finally {
setCreating(false); setCreating(false);
} }
@@ -149,12 +149,12 @@ export function TaskList(): React.JSX.Element {
try { try {
const res = await window.metona.tasks.update(task.id, { status: nextStatus }, sessionId); const res = await window.metona.tasks.update(task.id, { status: nextStatus }, sessionId);
if (!res.success) { if (!res.success) {
import('metona-toast').then((mod) => mod.default.error(res.error ?? '更新任务失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(res.error ?? '更新任务失败')).catch(() => {});
return; return;
} }
await loadTasks(sessionId ?? undefined); await loadTasks(sessionId ?? undefined);
} catch (err) { } catch (err) {
import('metona-toast').then((mod) => mod.default.error((err as Error).message ?? '更新任务失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error((err as Error).message ?? '更新任务失败')).catch(() => {});
} }
}; };
@@ -164,13 +164,13 @@ export function TaskList(): React.JSX.Element {
try { try {
const res = await window.metona.tasks.delete(id, sessionId); const res = await window.metona.tasks.delete(id, sessionId);
if (!res.success) { if (!res.success) {
import('metona-toast').then((mod) => mod.default.error(res.error ?? '删除任务失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(res.error ?? '删除任务失败')).catch(() => {});
return; return;
} }
if (expandedId === id) setExpandedId(null); if (expandedId === id) setExpandedId(null);
await loadTasks(sessionId ?? undefined); await loadTasks(sessionId ?? undefined);
} catch (err) { } catch (err) {
import('metona-toast').then((mod) => mod.default.error((err as Error).message ?? '删除任务失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error((err as Error).message ?? '删除任务失败')).catch(() => {});
} }
}; };
+4 -2
View File
@@ -9,6 +9,7 @@ import { Box, Typography, IconButton, Collapse, Stack } from '@mui/material';
import { ChevronDown, ChevronRight, CircleDot, CheckCircle, Loader2, Clock, XCircle, Ban } from 'lucide-react'; import { ChevronDown, ChevronRight, CircleDot, CheckCircle, Loader2, Clock, XCircle, Ban } from 'lucide-react';
import { TRACE_STATE_COLORS, TRACE_STATE_LABELS } from '@renderer/lib/constants'; import { TRACE_STATE_COLORS, TRACE_STATE_LABELS } from '@renderer/lib/constants';
import { formatDuration, formatTokens } from '@renderer/lib/formatters'; import { formatDuration, formatTokens } from '@renderer/lib/formatters';
import { toDisplayResult } from '@renderer/lib/tool-result-display';
import type { ToolCallInfo } from '@renderer/stores/agent-store'; import type { ToolCallInfo } from '@renderer/stores/agent-store';
import type { TraceStep as TraceStepType } from '@renderer/stores/agent-store'; import type { TraceStep as TraceStepType } from '@renderer/stores/agent-store';
@@ -118,10 +119,11 @@ function ToolCallRow({ tc }: { tc: ToolCallInfo }): React.JSX.Element {
const statusColor = TOOL_STATUS_COLORS[tc.status] ?? '#8b8fa7'; const statusColor = TOOL_STATUS_COLORS[tc.status] ?? '#8b8fa7';
const statusLabel = TOOL_STATUS_LABELS[tc.status] ?? tc.status; const statusLabel = TOOL_STATUS_LABELS[tc.status] ?? tc.status;
// 结果摘要(不截断,限制高度 + 滚动) // 结果摘要(限制高度 + 滚动)。渲染前剥离 dataUrl 等超大 base64,避免 ~6.7MB/张进 DOM 导致 OOM
const hasResult = tc.result != null; const hasResult = tc.result != null;
const displayResult = toDisplayResult(tc.result);
const resultStr = hasResult const resultStr = hasResult
? (typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result, null, 2)) ? (typeof displayResult === 'string' ? displayResult : JSON.stringify(displayResult, null, 2))
: ''; : '';
return ( return (
+1 -1
View File
@@ -70,7 +70,7 @@ export function WorkspaceViewer(): React.JSX.Element {
await window.metona?.app?.showItemInFolder(path); await window.metona?.app?.showItemInFolder(path);
} catch (err) { } catch (err) {
console.error('[WorkspaceViewer]', err); console.error('[WorkspaceViewer]', err);
import('metona-toast').then((mod) => mod.default.error('打开文件夹失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('打开文件夹失败')).catch(() => {});
} }
}; };
+2 -2
View File
@@ -83,7 +83,7 @@ export function useKeyboardShortcuts(): void {
}).catch((err) => { }).catch((err) => {
console.error('[KeyboardShortcuts]', err); console.error('[KeyboardShortcuts]', err);
// 与 Sidebar/CommandPalette 入口对齐:失败时弹 toast 提示用户 // 与 Sidebar/CommandPalette 入口对齐:失败时弹 toast 提示用户
import('metona-toast').then((mod) => mod.default.error('创建会话失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('创建会话失败')).catch(() => {});
}); });
} }
e.preventDefault(); e.preventDefault();
@@ -171,7 +171,7 @@ export function useKeyboardShortcuts(): void {
navigator.clipboard.writeText(lastAssistant.content).catch((err) => { navigator.clipboard.writeText(lastAssistant.content).catch((err) => {
console.error('[KeyboardShortcuts]', err); console.error('[KeyboardShortcuts]', err);
// 复制是用户主动操作,失败时必须有反馈 // 复制是用户主动操作,失败时必须有反馈
import('metona-toast').then((mod) => mod.default.error('复制失败')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error('复制失败')).catch(() => {});
}); });
} }
e.preventDefault(); e.preventDefault();
+75
View File
@@ -0,0 +1,75 @@
/**
* 工具结果显示层裁剪 — 防止 base64 大字段撑爆渲染进程内存
*
* 背景:view_image 等工具返回的 dataUrl(最大 ~6.7MB/张 base64)会经 IPC 流式事件
* 完整进入渲染进程,并写入 store 的 message / traceStep。若直接 JSON.stringify 渲染,
* 单条 tool_result 就会把 ~6.7MB 字符串塞进 DOM,多轮多图累积导致渲染进程 OOM 崩溃。
*
* 策略:仅在最外层显示边界剥离 dataUrl 这类超大 base64 字段,返回轻量摘要。
* - 主进程 / DB / LLM 链路保留完整 dataUrl(多模态 LLM 看图依赖它,不可裁剪)
* - 渲染进程只用于"展示"的 result 先过 toDisplayResultstore 内原始 result 不变
*
* 副作用:纯函数,不修改入参(返回新对象或原值引用)。
*/
/** 视为"超大"的字符串阈值(字符数),超过即判定为需裁剪的大字段 */
const LARGE_FIELD_THRESHOLD = 8_192;
/**
* 判断一个值是否为"含大 base64 的对象"——即 view_image 等工具的返回结构。
* 仅当对象包含 dataUrl(或同等量级的 base64)字段时才裁剪,普通结构原样返回。
*/
function hasLargeBase64(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
&& 'dataUrl' in value;
}
/**
* 将工具结果转换为"适合显示"的轻量版本:剥离 dataUrl 等超大字段。
*
* @param result 原始工具结果(来自 store 的 message.toolCalls[].result
* @returns 用于 UI 展示的裁剪后结果(原值或新对象),永不修改入参
*/
export function toDisplayResult(result: unknown): unknown {
// 非对象(string / number / null 等)无需裁剪
if (typeof result !== 'object' || result === null) {
return result;
}
// 含 dataUrl 的结果(view_image 等):剥离 dataUrl,保留 path/size/mimeType 等小字段
if (hasLargeBase64(result)) {
const { dataUrl: _omit, ...rest } = result as Record<string, unknown>;
void _omit; // 显式标记丢弃
return {
...rest,
// 标注被裁剪,便于用户/开发者理解为何看不到完整 dataUrl
_displayNote: '[image base64 omitted for display — visible to LLM only]',
};
}
// 其余对象/数组:递归剔除任意超长字符串字段(防御性,覆盖未来可能的 base64 字段)
return stripLargeStrings(result);
}
/**
* 递归剔除对象/数组中超过阈值的超长字符串字段,避免任何大 base64 进入 DOM。
* 对小字符串、数字、布尔等原样保留。
*/
function stripLargeStrings(value: unknown): unknown {
if (typeof value === 'string') {
return value.length > LARGE_FIELD_THRESHOLD
? `${value.slice(0, LARGE_FIELD_THRESHOLD)}… [truncated for display]`
: value;
}
if (Array.isArray(value)) {
return value.map(stripLargeStrings);
}
if (typeof value === 'object' && value !== null) {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
out[k] = stripLargeStrings(v);
}
return out;
}
return value;
}
+3 -3
View File
@@ -268,7 +268,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
// v0.3.18 修复: 工具未就绪时阻止发送,避免 MCP 工具不可用 // v0.3.18 修复: 工具未就绪时阻止发送,避免 MCP 工具不可用
if (!get().toolsReady) { if (!get().toolsReady) {
import('metona-toast').then((mod) => mod.default.warning('工具正在加载中,请稍候...')).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.warning('工具正在加载中,请稍候...')).catch(() => {});
return; return;
} }
@@ -295,7 +295,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
timestamp: Date.now(), timestamp: Date.now(),
}); });
// 额外弹 toast 作为通知,确保用户感知(system message 仅在聊天流内可见) // 额外弹 toast 作为通知,确保用户感知(system message 仅在聊天流内可见)
import('metona-toast').then((mod) => mod.default.error(`无法创建会话:${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`无法创建会话:${(err as Error).message}`)).catch(() => {});
return; return;
} }
} }
@@ -376,7 +376,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
timestamp: Date.now(), timestamp: Date.now(),
}); });
// 额外弹 toast 作为通知,确保用户感知(system message 仅在聊天流内可见) // 额外弹 toast 作为通知,确保用户感知(system message 仅在聊天流内可见)
import('metona-toast').then((mod) => mod.default.error(`消息发送失败:${(err as Error).message}`)).catch(() => {}); import('@metona-team/metona-toast').then((mod) => mod.default.error(`消息发送失败:${(err as Error).message}`)).catch(() => {});
}); });
} }
}, },