From 3a30e8f5b4890d01f10793c7bad064c2380361e9 Mon Sep 17 00:00:00 2001
From: thzxx <1440196015@qq.com>
Date: Fri, 21 Aug 2026 22:36:52 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20v0.5.3=20=E6=A8=A1=E5=9E=8B=E8=83=BD?=
=?UTF-8?q?=E5=8A=9B=E5=AE=8C=E6=95=B4=E6=80=A7=E4=BF=AE=E5=A4=8D=20?=
=?UTF-8?q?=E2=80=94=20MCP=20=E5=B7=A5=E5=85=B7=E5=8A=A8=E6=80=81=E5=90=8C?=
=?UTF-8?q?=E6=AD=A5=20+=20maxTokens=20=E6=A8=A1=E5=9E=8B=E4=B8=8A?=
=?UTF-8?q?=E9=99=90=E9=92=B3=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
背景:v0.5.2 工具调用失效修复后,对模型全能力矩阵(工具/思考/多模态/流式/
压缩/摘要/记忆/故障转移/余额)做契约级核查,发现并修复两处同类时序/边界缺陷。
P1 MCP 工具运行中增删不同步引擎:
- 症状:运行中添加/启用 MCP server 后,已打开会话拿不到新工具;断开
server 后已有引擎仍持有失效工具定义(模型发起调用才报 Unknown tool)
- 根因:setToolsAll 仅在启动期与工具开关时调用,MCP 连接/断开路径缺失
(与 v0.5.2 修复的懒创建陷阱同类——"变更点 × 同步路径"未全覆盖)
- 修复:MCPManager 新增 setOnToolsChanged 回调,connectServer 注册完成 /
disconnectServer 注销完成后触发;main.ts 注入回调同步全部已存在引擎。
懒创建引擎由 createEngine 实时拉取(v0.5.2),三条路径(启动/懒创建/
运行中变更)全覆盖。README"无需重启动态发现"的宣称至此真实成立。
P1 maxTokens 超模型上限直接 400:
- 症状:引擎默认 maxTokens=63488,OpenAI gpt-4o(16384)/gpt-4.1(32768)、
Anthropic opus/haiku(32000)、MiMo standard(32768) 每次请求 400,等于不可用
- 修复:五个 adapter(DeepSeek/Agnes/MiMo/OpenAI/Anthropic)统一按
MODEL_INFO.maxOutputTokens 钳制;MiMo 保留 thinking 兜底 32768 语义;
Anthropic thinking budget 在钳制后的 max_tokens 内二分,自动跟随
测试(224 → 236 用例):
- 新增 maxTokens 钳制契约测试 ×9(max-tokens-clamp.test.ts):mock fetch
记录真实请求体断言——超限钳制(MiMo standard/OpenAI gpt-4o/Anthropic
opus)/ 未超限原样传递(DeepSeek/Agnes/MiMo pro/o3-mini/sonnet)/
推理模型字段名 / 未配置默认值安全性
- 新增 MCP 动态同步端到端测试 ×3(mcp-tools-sync.test.ts):mock MCP SDK
+ 真实 MCPManager/ToolRegistry/AgentEngineManager——先建引擎再连
server,断言同一会话请求的 tools 动态更新 / 断开后移除失效定义 /
回调异常不阻断 MCP 主流程
能力矩阵核查结论(无回归确认): 工具调用主链路 ✓(v0.5.2)/ SubAgent
工具 ✓(delegate 实时 resolveTools)/ thinking 热更新 ✓(baseConfig 合并
路径无懒创建陷阱)/ 多模态当轮 ✓ / 压缩与孤立 tool 消息配对 ✓ / 摘要分层 ✓ /
记忆注入 ✓ / 故障转移 ✓ / 余额 ✓(v0.5.2)。已知设计限制:历史轮图片不
回传(attachments 仅存缩略图,图片只在发送当轮注入上下文)。
验证: lint 0 / typecheck 双工程 0 / test:electron 236 全过 / build 成功
---
README.md | 4 +-
.../__tests__/max-tokens-clamp.test.ts | 196 ++++++++++++++
electron/harness/adapters/agnes-ai.adapter.ts | 55 ++--
.../harness/adapters/anthropic.adapter.ts | 91 +++++--
electron/harness/adapters/deepseek.adapter.ts | 7 +-
electron/harness/adapters/mimo.adapter.ts | 58 +++--
electron/harness/adapters/openai.adapter.ts | 67 +++--
.../tools/__tests__/mcp-tools-sync.test.ts | 246 ++++++++++++++++++
electron/main.ts | 9 +-
electron/services/mcp-manager.service.ts | 31 +++
package-lock.json | 4 +-
package.json | 2 +-
12 files changed, 669 insertions(+), 101 deletions(-)
create mode 100644 electron/harness/adapters/__tests__/max-tokens-clamp.test.ts
create mode 100644 electron/harness/tools/__tests__/mcp-tools-sync.test.ts
diff --git a/README.md b/README.md
index aec805d..a1d7cf9 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
@@ -865,7 +865,7 @@ npm run format # Prettier 格式化
# ─── 测试 ─────────────────────────────────
npm test # 运行单元测试 (Vitest, 系统 Node — audit 套件因 better-sqlite3 ABI 自动跳过)
-npm run test:electron # 运行全量单元测试 (Electron Node ABI, 224 用例全执行, 含 SQLite 审计链哈希 + 引擎工具链集成)
+npm run test:electron # 运行全量单元测试 (Electron Node ABI, 236 用例全执行, 含 SQLite 审计链哈希 + 引擎工具链集成)
npm run test:watch # 测试监听模式
# ─── 构建 ─────────────────────────────────
diff --git a/electron/harness/adapters/__tests__/max-tokens-clamp.test.ts b/electron/harness/adapters/__tests__/max-tokens-clamp.test.ts
new file mode 100644
index 0000000..b17df34
--- /dev/null
+++ b/electron/harness/adapters/__tests__/max-tokens-clamp.test.ts
@@ -0,0 +1,196 @@
+/**
+ * Provider maxTokens 上限钳制契约测试(v0.5.3)
+ *
+ * 背景:引擎默认 maxTokens=63488(engine.ts DEFAULT_CONFIG),超过部分模型
+ * 上限时 API 直接 400 —— OpenAI gpt-4o(16384)/gpt-4.1(32768)、Anthropic
+ * opus/haiku(32000)、MiMo standard(32768) 曾不可用。v0.5.3 各 adapter 按
+ * MODEL_INFO.maxOutputTokens 钳制。
+ *
+ * 测试策略(v0.5.2 教训):mock fetch 记录真实请求体并断言契约 ——
+ * 不 mock adapter 内部方法,验证"发出的 HTTP 请求体"这个最终事实。
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+
+vi.mock('electron-log', () => ({
+ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
+}));
+
+import { DeepSeekAdapter } from '../deepseek.adapter';
+import { AgnesAdapter } from '../agnes-ai.adapter';
+import { MimoAdapter } from '../mimo.adapter';
+import { OpenAIAdapter } from '../openai.adapter';
+import { AnthropicAdapter } from '../anthropic.adapter';
+import type { MetonaRequest } from '../../types';
+
+const mockFetch = vi.fn();
+vi.stubGlobal('fetch', mockFetch);
+
+beforeEach(() => {
+ mockFetch.mockReset();
+});
+afterEach(() => {
+ mockFetch.mockReset();
+});
+
+/** 引擎默认形态的请求(maxTokens=63488,与 engine DEFAULT_CONFIG 一致) */
+function makeRequest(overrides: Partial = {}): MetonaRequest {
+ return {
+ meta: {
+ sessionId: 's',
+ iteration: 1,
+ requestId: 'r',
+ timestamp: Date.now(),
+ agentVersion: '1',
+ },
+ systemPrompt: { roleDefinition: '', outputConstraints: '', safetyGuidelines: '' },
+ messages: [{ role: 'user', content: 'hi', timestamp: Date.now() }],
+ params: {
+ maxTokens: 63_488,
+ temperature: 0,
+ stream: false,
+ thinkingEnabled: false,
+ },
+ ...overrides,
+ };
+}
+
+/** OpenAI 兼容系成功响应 */
+function openAIResponse(): Response {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => ({ choices: [{ message: { content: 'ok' } }], usage: {} }),
+ } as unknown as Response;
+}
+
+/** Anthropic 成功响应 */
+function anthropicResponse(): Response {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => ({ content: [{ type: 'text', text: 'ok' }], usage: {} }),
+ } as unknown as Response;
+}
+
+/** 取 mock fetch 收到的请求体 */
+function requestBody(): Record {
+ const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
+ return JSON.parse(init.body as string) as Record;
+}
+
+describe('maxTokens 模型上限钳制(引擎默认 63488 场景)', () => {
+ it('DeepSeek v4 pro(上限 384K):63488 未超限,原样传递', async () => {
+ mockFetch.mockResolvedValue(openAIResponse());
+ const adapter = new DeepSeekAdapter({
+ provider: 'deepseek',
+ baseURL: 'https://api.deepseek.com',
+ apiKey: 'sk',
+ defaultModel: 'deepseek-v4-pro',
+ });
+ await adapter.send(makeRequest());
+ expect(requestBody().max_tokens).toBe(63_488);
+ });
+
+ it('Agnes flash(上限 65536):63488 未超限,原样传递', async () => {
+ mockFetch.mockResolvedValue(openAIResponse());
+ const adapter = new AgnesAdapter({
+ provider: 'agnes',
+ baseURL: 'https://api.agnes.com/v1',
+ apiKey: 'k',
+ defaultModel: 'agnes-2.0-flash',
+ });
+ await adapter.send(makeRequest());
+ expect(requestBody().max_tokens).toBe(63_488);
+ });
+
+ it('MiMo standard(上限 32768):钳制到 32768(原为 400 错误场景)', async () => {
+ mockFetch.mockResolvedValue(openAIResponse());
+ const adapter = new MimoAdapter({
+ provider: 'mimo',
+ baseURL: 'https://api.mimo.com/v1',
+ apiKey: 'k',
+ defaultModel: 'mimo-v2.5',
+ });
+ await adapter.send(makeRequest());
+ expect(requestBody().max_completion_tokens).toBe(32_768);
+ });
+
+ it('MiMo pro(上限 131072):63488 未超限,原样传递', async () => {
+ mockFetch.mockResolvedValue(openAIResponse());
+ const adapter = new MimoAdapter({
+ provider: 'mimo',
+ baseURL: 'https://api.mimo.com/v1',
+ apiKey: 'k',
+ defaultModel: 'mimo-v2.5-pro',
+ });
+ await adapter.send(makeRequest());
+ expect(requestBody().max_completion_tokens).toBe(63_488);
+ });
+
+ it('OpenAI gpt-4o(上限 16384):钳制到 16384(原为 400 错误场景)', async () => {
+ mockFetch.mockResolvedValue(openAIResponse());
+ const adapter = new OpenAIAdapter({
+ provider: 'openai',
+ baseURL: 'https://api.openai.com/v1',
+ apiKey: 'k',
+ defaultModel: 'gpt-4o',
+ });
+ await adapter.send(makeRequest());
+ expect(requestBody().max_tokens).toBe(16_384);
+ });
+
+ it('OpenAI o3-mini(上限 100K):63488 未超限,推理模型字段名正确', async () => {
+ mockFetch.mockResolvedValue(openAIResponse());
+ const adapter = new OpenAIAdapter({
+ provider: 'openai',
+ baseURL: 'https://api.openai.com/v1',
+ apiKey: 'k',
+ defaultModel: 'o3-mini',
+ });
+ await adapter.send(makeRequest());
+ expect(requestBody().max_completion_tokens).toBe(63_488);
+ expect(requestBody().max_tokens).toBeUndefined();
+ });
+
+ it('Anthropic opus(上限 32000):钳制到 32000(原为 400 错误场景)', async () => {
+ mockFetch.mockResolvedValue(anthropicResponse());
+ const adapter = new AnthropicAdapter({
+ provider: 'anthropic',
+ baseURL: 'https://api.anthropic.com',
+ apiKey: 'k',
+ defaultModel: 'claude-opus-4-1',
+ });
+ await adapter.send(makeRequest());
+ expect(requestBody().max_tokens).toBe(32_000);
+ });
+
+ it('Anthropic sonnet(上限 64000):63488 未超限', async () => {
+ mockFetch.mockResolvedValue(anthropicResponse());
+ const adapter = new AnthropicAdapter({
+ provider: 'anthropic',
+ baseURL: 'https://api.anthropic.com',
+ apiKey: 'k',
+ defaultModel: 'claude-sonnet-4-5',
+ });
+ await adapter.send(makeRequest());
+ expect(requestBody().max_tokens).toBe(63_488);
+ });
+
+ it('未配置 maxTokens 时各 adapter 使用安全默认值(不超过模型上限)', async () => {
+ mockFetch.mockResolvedValue(openAIResponse());
+ const request = makeRequest({
+ params: { temperature: 0, stream: false } as MetonaRequest['params'],
+ });
+
+ // MiMo standard + thinking 默认 → 兜底 32768(= 上限,安全)
+ const mimo = new MimoAdapter({
+ provider: 'mimo',
+ baseURL: 'https://api.mimo.com/v1',
+ apiKey: 'k',
+ defaultModel: 'mimo-v2.5',
+ });
+ await mimo.send(request);
+ expect(requestBody().max_completion_tokens).toBe(32_768);
+ });
+});
diff --git a/electron/harness/adapters/agnes-ai.adapter.ts b/electron/harness/adapters/agnes-ai.adapter.ts
index 597afe8..c0fca1f 100644
--- a/electron/harness/adapters/agnes-ai.adapter.ts
+++ b/electron/harness/adapters/agnes-ai.adapter.ts
@@ -48,21 +48,25 @@ export class AgnesAdapter extends BaseAdapter {
const body = this.toNativeRequest(request, false);
// #24 修复: 使用 fetchWithTimeout 替代 getFetchSignal + fetch,确保 timer 清理
- const response = await this.fetchWithTimeout(`${this.config.baseURL}/chat/completions`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.config.apiKey}`,
- ...this.config.headers,
+ const response = await this.fetchWithTimeout(
+ `${this.config.baseURL}/chat/completions`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.config.apiKey}`,
+ ...this.config.headers,
+ },
+ body: JSON.stringify(body),
},
- body: JSON.stringify(body),
- }, this.config.timeoutMs ?? 300_000);
+ this.config.timeoutMs ?? 300_000,
+ );
if (!response.ok) {
await this.throwHttpError(response, 'Agnes AI API error');
}
- const data = await response.json() as Record;
+ const data = (await response.json()) as Record;
const parsed = parseOpenAICompatibleResponse(data);
return {
@@ -88,15 +92,19 @@ export class AgnesAdapter extends BaseAdapter {
const body = this.toNativeRequest(request, true);
// #24 修复: 使用 fetchWithTimeout 替代 getFetchSignal + fetch,确保 timer 清理
- const response = await this.fetchWithTimeout(`${this.config.baseURL}/chat/completions`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.config.apiKey}`,
- ...this.config.headers,
+ const response = await this.fetchWithTimeout(
+ `${this.config.baseURL}/chat/completions`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.config.apiKey}`,
+ ...this.config.headers,
+ },
+ body: JSON.stringify(body),
},
- body: JSON.stringify(body),
- }, this.config.timeoutMs ?? 300_000);
+ this.config.timeoutMs ?? 300_000,
+ );
if (!response.ok || !response.body) {
await this.throwHttpError(response, 'Agnes AI stream error');
@@ -170,15 +178,22 @@ export class AgnesAdapter extends BaseAdapter {
}
if (imageCount > 0) {
- const firstUrl = request.messages.find(m => m.images?.length)?.images?.[0]?.url ?? '';
- log.info(`[Agnes] Processing ${imageCount} image(s), first URL prefix: ${firstUrl.slice(0, 50)}`);
+ const firstUrl = request.messages.find((m) => m.images?.length)?.images?.[0]?.url ?? '';
+ log.info(
+ `[Agnes] Processing ${imageCount} image(s), first URL prefix: ${firstUrl.slice(0, 50)}`,
+ );
}
+ // v0.5.3: max_tokens 按模型上限钳制(agnes-2.0-flash 上限 65536)
+ const modelInfo = AgnesAdapter.MODEL_INFO[this.config.defaultModel];
+ const maxOutput = modelInfo?.maxOutputTokens ?? 65_536;
+ const maxTokens = Math.min(request.params.maxTokens ?? maxOutput, maxOutput);
+
const body: Record = {
model: this.config.defaultModel,
messages,
temperature: request.params.temperature,
- max_tokens: request.params.maxTokens ?? 65536,
+ max_tokens: maxTokens,
stream,
};
diff --git a/electron/harness/adapters/anthropic.adapter.ts b/electron/harness/adapters/anthropic.adapter.ts
index 60dc813..09c15fc 100644
--- a/electron/harness/adapters/anthropic.adapter.ts
+++ b/electron/harness/adapters/anthropic.adapter.ts
@@ -72,17 +72,21 @@ export class AnthropicAdapter extends BaseAdapter {
async send(request: MetonaRequest): Promise {
const body = await this.toNativeRequest(request, false);
- const response = await this.fetchWithTimeout(`${this.config.baseURL}/v1/messages`, {
- method: 'POST',
- headers: this.buildHeaders(),
- body: JSON.stringify(body),
- }, this.config.timeoutMs ?? 120_000);
+ const response = await this.fetchWithTimeout(
+ `${this.config.baseURL}/v1/messages`,
+ {
+ method: 'POST',
+ headers: this.buildHeaders(),
+ body: JSON.stringify(body),
+ },
+ this.config.timeoutMs ?? 120_000,
+ );
if (!response.ok) {
await this.throwHttpError(response, 'Anthropic API error');
}
- const data = await response.json() as Record;
+ const data = (await response.json()) as Record;
return this.toMetonaResponse(data, request.meta.requestId);
}
@@ -90,11 +94,15 @@ export class AnthropicAdapter extends BaseAdapter {
async *sendStream(request: MetonaRequest): AsyncIterable {
const body = await this.toNativeRequest(request, true);
- const response = await this.fetchWithTimeout(`${this.config.baseURL}/v1/messages`, {
- method: 'POST',
- headers: this.buildHeaders(),
- body: JSON.stringify(body),
- }, this.config.timeoutMs ?? 300_000);
+ const response = await this.fetchWithTimeout(
+ `${this.config.baseURL}/v1/messages`,
+ {
+ method: 'POST',
+ headers: this.buildHeaders(),
+ body: JSON.stringify(body),
+ },
+ this.config.timeoutMs ?? 300_000,
+ );
if (!response.ok || !response.body) {
await this.throwHttpError(response, 'Anthropic stream error');
@@ -140,7 +148,11 @@ export class AnthropicAdapter extends BaseAdapter {
if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
events.push({ type: MetonaStreamEventType.TEXT_DELTA, ...base(), delta: delta.text });
} else if (delta?.type === 'thinking_delta' && typeof delta.thinking === 'string') {
- events.push({ type: MetonaStreamEventType.REASONING_DELTA, ...base(), delta: delta.thinking });
+ events.push({
+ type: MetonaStreamEventType.REASONING_DELTA,
+ ...base(),
+ delta: delta.thinking,
+ });
} else if (delta?.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
const block = toolBlocks.get(index);
if (block) {
@@ -189,7 +201,8 @@ export class AnthropicAdapter extends BaseAdapter {
usage: {
inputTokens: (this.lastInputTokens as number) ?? 0,
outputTokens: (usage.output_tokens as number) ?? 0,
- totalTokens: ((this.lastInputTokens as number) ?? 0) + ((usage.output_tokens as number) ?? 0),
+ totalTokens:
+ ((this.lastInputTokens as number) ?? 0) + ((usage.output_tokens as number) ?? 0),
},
});
}
@@ -250,7 +263,10 @@ export class AnthropicAdapter extends BaseAdapter {
yield ev;
}
} catch (parseErr) {
- log.warn(`[Anthropic] Failed to parse SSE line: ${(parseErr as Error).message}`, trimmed.slice(0, 200));
+ log.warn(
+ `[Anthropic] Failed to parse SSE line: ${(parseErr as Error).message}`,
+ trimmed.slice(0, 200),
+ );
}
}
}
@@ -291,7 +307,10 @@ export class AnthropicAdapter extends BaseAdapter {
* 4. 连续同角色消息合并(API 要求严格交替)
* 5. 首条消息必须为 user(历史以 assistant 开头时补占位)
*/
- private async toNativeRequest(request: MetonaRequest, stream: boolean): Promise> {
+ private async toNativeRequest(
+ request: MetonaRequest,
+ stream: boolean,
+ ): Promise> {
// System Prompt 拼接(Anthropic 使用顶层 system 字段)
const system = [
request.systemPrompt.roleDefinition,
@@ -303,7 +322,10 @@ export class AnthropicAdapter extends BaseAdapter {
.join('\n\n');
// 转换消息(非 system)
- const converted: Array<{ role: 'user' | 'assistant'; content: Array> }> = [];
+ const converted: Array<{
+ role: 'user' | 'assistant';
+ content: Array>;
+ }> = [];
for (const m of request.messages) {
if (m.role === 'system') continue;
@@ -316,7 +338,9 @@ export class AnthropicAdapter extends BaseAdapter {
: JSON.stringify(m.toolResult.result);
converted.push({
role: 'user',
- content: [{ type: 'tool_result', tool_use_id: m.toolResult.toolCallId, content: contentStr }],
+ content: [
+ { type: 'tool_result', tool_use_id: m.toolResult.toolCallId, content: contentStr },
+ ],
});
continue;
}
@@ -345,7 +369,8 @@ export class AnthropicAdapter extends BaseAdapter {
}
// 合并连续同角色消息(Anthropic 要求 user/assistant 交替)
- const merged: Array<{ role: 'user' | 'assistant'; content: Array> }> = [];
+ const merged: Array<{ role: 'user' | 'assistant'; content: Array> }> =
+ [];
for (const msg of converted) {
const last = merged[merged.length - 1];
if (last && last.role === msg.role) {
@@ -357,12 +382,20 @@ export class AnthropicAdapter extends BaseAdapter {
// 首条消息必须为 user
if (merged.length === 0 || merged[0].role !== 'user') {
- merged.unshift({ role: 'user', content: [{ type: 'text', text: '[Conversation history follows]' }] });
+ merged.unshift({
+ role: 'user',
+ content: [{ type: 'text', text: '[Conversation history follows]' }],
+ });
}
+ // v0.5.3: max_tokens 按模型上限钳制(sonnet 64000 / opus 32000 / haiku 32000)—
+ // 引擎默认 63488 超过 opus/haiku 上限时 API 直接 400;thinking budget 已在此值内二分
+ const anthropicMaxOutput =
+ AnthropicAdapter.MODEL_INFO[this.config.defaultModel]?.maxOutputTokens ?? 64_000;
+
const body: Record = {
model: this.config.defaultModel,
- max_tokens: request.params.maxTokens ?? 8192,
+ max_tokens: Math.min(request.params.maxTokens ?? 8192, anthropicMaxOutput),
system,
messages: merged,
stream,
@@ -379,7 +412,12 @@ export class AnthropicAdapter extends BaseAdapter {
// Thinking 模式:budget_tokens(必须小于 max_tokens,此处钳制到一半)
if (request.params.thinkingEnabled) {
- const budgetMap: Record = { low: 1024, medium: 4096, high: 16384, max: 32768 };
+ const budgetMap: Record = {
+ low: 1024,
+ medium: 4096,
+ high: 16384,
+ max: 32768,
+ };
const budget = Math.min(
budgetMap[request.params.thinkingEffort ?? 'high'] ?? 16384,
Math.floor((body.max_tokens as number) / 2),
@@ -435,7 +473,8 @@ export class AnthropicAdapter extends BaseAdapter {
for (const block of contentBlocks) {
if (block.type === 'text') text += (block.text as string) ?? '';
- else if (block.type === 'thinking') reasoningContent = (block.thinking as string) ?? undefined;
+ else if (block.type === 'thinking')
+ reasoningContent = (block.thinking as string) ?? undefined;
else if (block.type === 'tool_use') {
let args: Record = {};
const rawInput = block.input;
@@ -453,9 +492,11 @@ export class AnthropicAdapter extends BaseAdapter {
const usage = (data.usage as Record) ?? {};
const stopReason = (data.stop_reason as string) ?? 'end_turn';
const finishReason: MetonaFinishReason =
- stopReason === 'tool_use' ? MetonaFinishReason.TOOL_CALLS
- : stopReason === 'max_tokens' ? MetonaFinishReason.LENGTH
- : MetonaFinishReason.STOP;
+ stopReason === 'tool_use'
+ ? MetonaFinishReason.TOOL_CALLS
+ : stopReason === 'max_tokens'
+ ? MetonaFinishReason.LENGTH
+ : MetonaFinishReason.STOP;
return {
meta: {
diff --git a/electron/harness/adapters/deepseek.adapter.ts b/electron/harness/adapters/deepseek.adapter.ts
index 52961fc..dfaae77 100644
--- a/electron/harness/adapters/deepseek.adapter.ts
+++ b/electron/harness/adapters/deepseek.adapter.ts
@@ -239,11 +239,16 @@ export class DeepSeekAdapter extends BaseAdapter {
const messages = buildOpenAICompatibleMessages(request);
const tools = buildOpenAICompatibleTools(request.tools);
+ // v0.5.3: max_tokens 按模型上限钳制 — 引擎默认 63488 超过部分模型上限时 API 直接 400
+ const modelInfo = DeepSeekAdapter.MODEL_INFO[this.config.defaultModel];
+ const maxOutput = modelInfo?.maxOutputTokens ?? 384_000;
+ const maxTokens = Math.min(request.params.maxTokens ?? maxOutput, maxOutput);
+
const body: Record = {
model: this.config.defaultModel,
messages,
temperature: request.params.temperature,
- max_tokens: request.params.maxTokens,
+ max_tokens: maxTokens,
stream,
};
diff --git a/electron/harness/adapters/mimo.adapter.ts b/electron/harness/adapters/mimo.adapter.ts
index 1da034b..adce04e 100644
--- a/electron/harness/adapters/mimo.adapter.ts
+++ b/electron/harness/adapters/mimo.adapter.ts
@@ -59,21 +59,25 @@ export class MimoAdapter extends BaseAdapter {
const body = this.toNativeRequest(request, false);
// #24 修复: 使用 fetchWithTimeout 替代 getFetchSignal + fetch,确保 timer 清理
- const response = await this.fetchWithTimeout(`${this.config.baseURL}/chat/completions`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.config.apiKey}`,
- ...this.config.headers,
+ const response = await this.fetchWithTimeout(
+ `${this.config.baseURL}/chat/completions`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.config.apiKey}`,
+ ...this.config.headers,
+ },
+ body: JSON.stringify(body),
},
- body: JSON.stringify(body),
- }, this.config.timeoutMs ?? 120_000);
+ this.config.timeoutMs ?? 120_000,
+ );
if (!response.ok) {
await this.throwHttpError(response, 'MiMo API error');
}
- const data = await response.json() as Record;
+ const data = (await response.json()) as Record;
const parsed = parseOpenAICompatibleResponse(data);
return {
@@ -98,15 +102,19 @@ export class MimoAdapter extends BaseAdapter {
const body = this.toNativeRequest(request, true);
// #24 修复: 使用 fetchWithTimeout 替代 getFetchSignal + fetch,确保 timer 清理
- const response = await this.fetchWithTimeout(`${this.config.baseURL}/chat/completions`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.config.apiKey}`,
- ...this.config.headers,
+ const response = await this.fetchWithTimeout(
+ `${this.config.baseURL}/chat/completions`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.config.apiKey}`,
+ ...this.config.headers,
+ },
+ body: JSON.stringify(body),
},
- body: JSON.stringify(body),
- }, this.config.timeoutMs ?? 300_000);
+ this.config.timeoutMs ?? 300_000,
+ );
if (!response.ok || !response.body) {
await this.throwHttpError(response, 'MiMo stream error');
@@ -187,16 +195,18 @@ export class MimoAdapter extends BaseAdapter {
messages[i].content = contentParts;
}
+ // MiMo 使用 max_completion_tokens(非 max_tokens)
+ // #41 修复: thinking 模式下未配置时兜底 32768(thinking 占用 token 配额,API 默认值过小会截断输出)
+ // v0.5.3: 按模型上限钳制(pro 131072 / standard 32768)—
+ // 引擎默认 63488 超过 standard 上限时 API 直接 400
+ const mimoMaxOutput =
+ MimoAdapter.MODEL_INFO[this.config.defaultModel]?.maxOutputTokens ?? 131_072;
+ const mimoDefault = request.params.thinkingEnabled !== false ? 32_768 : mimoMaxOutput;
+
const body: Record = {
model: this.config.defaultModel,
messages,
- // MiMo 使用 max_completion_tokens(非 max_tokens)
- // #41 修复: thinking 模式下 max_completion_tokens 未配置时使用兜底默认值(32768)
- // thinking 占用 token 配额,未配置时 API 默认值可能过小导致输出被截断
- // thinkingEnabled !== false 包含 true 和 undefined(MiMo 默认 enabled)两种情况
- // 审查修复: 兜底值从 63488 改为 32768,避免超出标准版上限导致 API 400
- max_completion_tokens: request.params.maxTokens
- ?? (request.params.thinkingEnabled !== false ? 32768 : undefined),
+ max_completion_tokens: Math.min(request.params.maxTokens ?? mimoDefault, mimoMaxOutput),
stream,
};
diff --git a/electron/harness/adapters/openai.adapter.ts b/electron/harness/adapters/openai.adapter.ts
index 6f8b5a9..5ce00ab 100644
--- a/electron/harness/adapters/openai.adapter.ts
+++ b/electron/harness/adapters/openai.adapter.ts
@@ -69,21 +69,25 @@ export class OpenAIAdapter extends BaseAdapter {
async send(request: MetonaRequest): Promise {
const body = this.toNativeRequest(request, false);
- const response = await this.fetchWithTimeout(`${this.config.baseURL}/chat/completions`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.config.apiKey}`,
- ...this.config.headers,
+ const response = await this.fetchWithTimeout(
+ `${this.config.baseURL}/chat/completions`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.config.apiKey}`,
+ ...this.config.headers,
+ },
+ body: JSON.stringify(body),
},
- body: JSON.stringify(body),
- }, this.config.timeoutMs ?? 120_000);
+ this.config.timeoutMs ?? 120_000,
+ );
if (!response.ok) {
await this.throwHttpError(response, 'OpenAI API error');
}
- const data = await response.json() as Record;
+ const data = (await response.json()) as Record;
const parsed = parseOpenAICompatibleResponse(data);
return {
@@ -107,15 +111,19 @@ export class OpenAIAdapter extends BaseAdapter {
async *sendStream(request: MetonaRequest): AsyncIterable {
const body = this.toNativeRequest(request, true);
- const response = await this.fetchWithTimeout(`${this.config.baseURL}/chat/completions`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.config.apiKey}`,
- ...this.config.headers,
+ const response = await this.fetchWithTimeout(
+ `${this.config.baseURL}/chat/completions`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.config.apiKey}`,
+ ...this.config.headers,
+ },
+ body: JSON.stringify(body),
},
- body: JSON.stringify(body),
- }, this.config.timeoutMs ?? 300_000);
+ this.config.timeoutMs ?? 300_000,
+ );
if (!response.ok || !response.body) {
await this.throwHttpError(response, 'OpenAI stream error');
@@ -139,7 +147,7 @@ export class OpenAIAdapter extends BaseAdapter {
signal: AbortSignal.timeout(10_000),
});
if (response.ok) {
- const data = await response.json() as { data?: Array<{ id: string }> };
+ const data = (await response.json()) as { data?: Array<{ id: string }> };
if (data.data?.length) {
return data.data.map((m) => OpenAIAdapter.MODEL_INFO[m.id] ?? { id: m.id });
}
@@ -208,15 +216,19 @@ export class OpenAIAdapter extends BaseAdapter {
};
// Token 上限参数:o 系列/gpt-5 使用 max_completion_tokens
- if (request.params.maxTokens) {
+ // v0.5.3: 按模型上限钳制(gpt-4o 16384 / gpt-4.1 32768 / o3-mini 100000)—
+ // 引擎默认 63488 超过 gpt-4o/gpt-4.1 上限时 API 直接 400
+ const oaMaxOutput = OpenAIAdapter.MODEL_INFO[model]?.maxOutputTokens ?? 128_000;
+ const oaMaxTokens = Math.min(
+ request.params.maxTokens ?? (isReasoningModel ? 32_768 : oaMaxOutput),
+ oaMaxOutput,
+ );
+ if (oaMaxTokens) {
if (isReasoningModel) {
- body.max_completion_tokens = request.params.maxTokens;
+ body.max_completion_tokens = oaMaxTokens;
} else {
- body.max_tokens = request.params.maxTokens;
+ body.max_tokens = oaMaxTokens;
}
- } else if (isReasoningModel) {
- // 推理模型未配置时使用兜底值(thinking 占用 token 配额,默认值过小会被截断)
- body.max_completion_tokens = 32_768;
}
if (stream) {
@@ -229,7 +241,12 @@ export class OpenAIAdapter extends BaseAdapter {
// Thinking 模式:推理模型映射 reasoning_effort;非推理模型忽略
if (request.params.thinkingEnabled && isReasoningModel) {
- const effortMap: Record = { low: 'low', medium: 'medium', high: 'high', max: 'high' };
+ const effortMap: Record = {
+ low: 'low',
+ medium: 'medium',
+ high: 'high',
+ max: 'high',
+ };
body.reasoning_effort = effortMap[request.params.thinkingEffort ?? 'high'] ?? 'high';
} else if (!isReasoningModel) {
// 非推理模型使用温度控制
diff --git a/electron/harness/tools/__tests__/mcp-tools-sync.test.ts b/electron/harness/tools/__tests__/mcp-tools-sync.test.ts
new file mode 100644
index 0000000..f87fc08
--- /dev/null
+++ b/electron/harness/tools/__tests__/mcp-tools-sync.test.ts
@@ -0,0 +1,246 @@
+/**
+ * MCP 工具动态变更 × 引擎同步测试(v0.5.3)
+ *
+ * 背景:MCP 工具在运行中增删(添加/断开/启停 server)后,已存在引擎的
+ * 工具列表不会自动更新 —— setToolsAll 只在启动期与工具开关时被调用。
+ * 后果:已打开会话拿不到新 MCP 工具,或持有已断开 server 的失效工具定义。
+ * v0.5.3 修复:MCPManager.setOnToolsChanged 回调,连接/断开后同步全部引擎。
+ *
+ * 测试策略:mock MCP SDK(Client/StdioClientTransport),用真实的
+ * MCPManager + ToolRegistry + AgentEngineManager 端到端验证 ——
+ * 先创建会话引擎(复现"已打开会话"),再连接 MCP server,断言引擎请求
+ * 的 tools 集合动态更新。
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+
+vi.mock('electron-log', () => ({
+ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
+}));
+
+// ===== Mock MCP SDK =====
+const mockConnect = vi.fn(async () => undefined);
+const mockListTools = vi.fn(async () => ({
+ tools: [
+ {
+ name: 'search_docs',
+ description: 'Search documentation',
+ inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
+ },
+ ],
+}));
+const mockClose = vi.fn(async () => undefined);
+
+vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
+ Client: class {
+ connect = mockConnect;
+ listTools = mockListTools;
+ close = mockClose;
+ callTool = vi.fn(async () => ({ content: [] }));
+ },
+}));
+
+vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
+ StdioClientTransport: class {
+ // 测试不真实 spawn 子进程(真实 stdio 会拉起 npx 进程)
+ },
+}));
+
+import { MCPManager } from '../../../services/mcp-manager.service';
+import { AgentEngineManager } from '../../../services/agent-engine-manager.service';
+import { ToolRegistry } from '../registry';
+import type {
+ IMetonaProviderAdapter,
+ MetonaRequest,
+ MetonaResponse,
+ MetonaStreamEvent,
+} from '../../types';
+import { MetonaStreamEventType } from '../../types';
+import { mkdtempSync, rmSync } from 'fs';
+import { tmpdir } from 'os';
+import { join } from 'path';
+
+// connectServer 成功后会写 mcp_servers 表(last_connected)— 用真实 :memory: SQLite
+let dbAvailable = true;
+let Database: typeof import('better-sqlite3');
+try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ Database = require('better-sqlite3');
+ const probe = new Database(':memory:');
+ probe.close();
+} catch {
+ dbAvailable = false;
+}
+
+/** 记录请求的 mock adapter */
+function createRecordingAdapter(requests: MetonaRequest[]): IMetonaProviderAdapter {
+ return {
+ providerId: 'mock',
+ supportedModels: ['mock-model'],
+ supportsToolCalling: true,
+ supportsThinking: false,
+ getContextWindow: () => 1_000_000,
+ send: vi.fn(
+ async (): Promise => ({
+ meta: { requestId: 'r', provider: 'mock', model: 'm', latencyMs: 1, timestamp: Date.now() },
+ content: 'ok',
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
+ finishReason: 'stop' as never,
+ }),
+ ),
+ sendStream: vi.fn(async function* (req: MetonaRequest): AsyncIterable {
+ requests.push(req);
+ yield {
+ type: MetonaStreamEventType.TEXT_DELTA,
+ requestId: 'r',
+ sessionId: req.meta.sessionId,
+ iteration: req.meta.iteration,
+ seq: 0,
+ timestamp: Date.now(),
+ delta: 'done',
+ };
+ yield {
+ type: MetonaStreamEventType.DONE,
+ requestId: 'r',
+ sessionId: req.meta.sessionId,
+ iteration: req.meta.iteration,
+ seq: 1,
+ timestamp: Date.now(),
+ };
+ }),
+ setAbortSignal: vi.fn(),
+ healthCheck: async () => true,
+ };
+}
+
+const userMessage = { role: 'user' as const, content: 'hello', timestamp: Date.now() };
+const systemPrompt = { roleDefinition: '', outputConstraints: '', safetyGuidelines: '' };
+
+describe.skipIf(!dbAvailable)('MCP 工具动态变更 × 引擎同步(v0.5.3)', () => {
+ let db: any;
+ let dir: string;
+
+ beforeEach(() => {
+ mockConnect.mockClear();
+ mockListTools.mockClear();
+ mockClose.mockClear();
+ dir = mkdtempSync(join(tmpdir(), 'metona-mcp-sync-'));
+ db = new Database(':memory:');
+ db.exec(`
+ CREATE TABLE mcp_servers (
+ id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE,
+ transport TEXT NOT NULL, command TEXT, args TEXT, url TEXT, headers TEXT,
+ enabled INTEGER NOT NULL DEFAULT 1, last_connected INTEGER, error_message TEXT,
+ created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0
+ );
+ `);
+ });
+
+ afterEach(() => {
+ try {
+ db?.close();
+ } catch {
+ /* ignore */
+ }
+ try {
+ rmSync(dir, { recursive: true, force: true });
+ } catch {
+ /* ignore */
+ }
+ });
+
+ it('运行中连接 MCP server 后,已存在引擎的工具列表动态更新', async () => {
+ const requests: MetonaRequest[] = [];
+ const adapter = createRecordingAdapter(requests);
+ const registry = new ToolRegistry();
+ const manager = new AgentEngineManager({
+ buildAdapter: () => adapter,
+ baseConfig: {},
+ toolRegistry: registry,
+ });
+ const mcpManager = new MCPManager(() => db, registry);
+
+ // v0.5.3 的接线:MCP 工具变更 → 同步全部已存在引擎
+ mcpManager.setOnToolsChanged(() => {
+ manager.setToolsAll(registry.listTools());
+ });
+
+ // 1. 用户先打开会话(引擎已存在,此时无 MCP 工具)
+ const engine = manager.getEngine('sess-mcp');
+ await engine.runStream(userMessage, 'sess-mcp', [], systemPrompt);
+ expect(requests[0].tools ?? []).toHaveLength(0);
+
+ // 2. 运行中添加并连接 MCP server(复现 ipc/mcp.ts addServer 路径)
+ await mcpManager.connectServer({
+ id: 'mcp_test',
+ name: 'test-server',
+ transport: 'stdio',
+ command: 'npx',
+ args: ['-y', 'some-mcp-server'],
+ enabled: true,
+ });
+
+ // 3. 同一会话再发消息 —— 引擎请求携带新 MCP 工具(修复点:
+ // v0.5.3 之前此处仍为空,需新建会话才能拿到)
+ requests.length = 0;
+ await engine.runStream(userMessage, 'sess-mcp', [], systemPrompt);
+ const toolNames = (requests[0].tools ?? []).map((t) => t.name);
+ expect(toolNames).toContain('mcp_test-server_search_docs');
+ });
+
+ it('断开 MCP server 后,已存在引擎移除失效工具定义', async () => {
+ const requests: MetonaRequest[] = [];
+ const adapter = createRecordingAdapter(requests);
+ const registry = new ToolRegistry();
+ const manager = new AgentEngineManager({
+ buildAdapter: () => adapter,
+ baseConfig: {},
+ toolRegistry: registry,
+ });
+ const mcpManager = new MCPManager(() => db, registry);
+ mcpManager.setOnToolsChanged(() => {
+ manager.setToolsAll(registry.listTools());
+ });
+
+ // 连接 → 引擎拿到工具
+ await mcpManager.connectServer({
+ id: 'mcp_test2',
+ name: 'srv2',
+ transport: 'stdio',
+ command: 'npx',
+ args: [],
+ enabled: true,
+ });
+ const engine = manager.getEngine('sess-mcp2');
+ await engine.runStream(userMessage, 'sess-mcp2', [], systemPrompt);
+ expect((requests[0].tools ?? []).length).toBeGreaterThan(0);
+
+ // 断开 → 引擎的后续请求不再携带失效工具(修复点:此前引擎持有
+ // 失效定义,模型发起调用时才报 Unknown tool)
+ await mcpManager.disconnectServer('srv2');
+ requests.length = 0;
+ await engine.runStream(userMessage, 'sess-mcp2', [], systemPrompt);
+ expect(requests[0].tools ?? []).toHaveLength(0);
+ });
+
+ it('回调抛异常不影响 MCP 连接主流程(防御性)', async () => {
+ const registry = new ToolRegistry();
+ const mcpManager = new MCPManager(() => db, registry);
+ mcpManager.setOnToolsChanged(() => {
+ throw new Error('callback exploded');
+ });
+
+ // 连接仍成功(回调失败仅记录日志)
+ await expect(
+ mcpManager.connectServer({
+ id: 'mcp_test3',
+ name: 'srv3',
+ transport: 'stdio',
+ command: 'npx',
+ args: [],
+ enabled: true,
+ }),
+ ).resolves.toBeUndefined();
+ expect(registry.listTools().length).toBeGreaterThan(0);
+ });
+});
diff --git a/electron/main.ts b/electron/main.ts
index 48c2663..3405952 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -405,8 +405,15 @@ async function initialize(): Promise {
};
agentEngineManager.setFallbackAdapter(buildFallbackAdapter());
- // P1-11: MCP 初始化等待所有连接完成后再广播 tools:ready(修复工具未注册即广播的窗口)
+ // ===== P1-11: MCP 初始化等待所有连接完成后再广播 tools:ready(修复工具未注册即广播的窗口) =====
// v0.3.18: toolsReadyRef 供 tools:isReady 查询(解决事件竞态)
+ // v0.5.3: MCP 工具集合运行中变更(添加/断开/启停 server)→ 同步所有已存在引擎。
+ // 此前仅启动期 setToolsAll;运行中增删后已打开会话的引擎拿不到新工具
+ // (或持有失效定义,调用报 Unknown tool)。懒创建引擎由 createEngine
+ // 实时拉取(v0.5.2),此回调覆盖既有引擎。
+ mcpManager.setOnToolsChanged(() => {
+ agentEngineManager.setToolsAll(toolRegistry.listTools());
+ });
const toolsReadyRef: ToolsReadyRef = { ready: false, toolCount: 0 };
mcpManager
.initialize()
diff --git a/electron/services/mcp-manager.service.ts b/electron/services/mcp-manager.service.ts
index fbbed18..ec24c85 100644
--- a/electron/services/mcp-manager.service.ts
+++ b/electron/services/mcp-manager.service.ts
@@ -219,12 +219,37 @@ class MCPToolAdapter implements IMetonaTool {
export class MCPManager {
private servers = new Map();
+ /**
+ * 工具集合变更回调(v0.5.3)
+ *
+ * connectServer / disconnectServer 完成后触发,供调用方同步引擎工具列表。
+ * 背景:setToolsAll 只作用于已存在的引擎;MCP 工具在运行中增删时,
+ * 已打开会话的引擎不会自动感知 — 不回调同步则已有引擎持有失效工具定义
+ * (调用报 Unknown tool)或缺失新工具(与 README"无需重启"的宣称不符)。
+ * 懒创建引擎由 createEngine 从 registry 实时拉取(v0.5.2),无需此回调。
+ */
+ private toolsChangedCallback: (() => void) | null = null;
constructor(
private getDB: () => Database.Database,
private toolRegistry: ToolRegistry,
) {}
+ /** 注册工具集合变更回调(main.ts 在 AgentEngineManager 创建后注入) */
+ setOnToolsChanged(callback: () => void): void {
+ this.toolsChangedCallback = callback;
+ }
+
+ /** 工具集合变更后的统一通知(连接注册完成 / 断开注销完成) */
+ private notifyToolsChanged(): void {
+ try {
+ this.toolsChangedCallback?.();
+ } catch (err) {
+ // 回调失败不影响 MCP 主流程(仅引擎工具列表滞后,下轮 setToolsAll 兜底)
+ log.warn('[MCPManager] toolsChanged callback failed:', err);
+ }
+ }
+
/**
* 初始化:从数据库加载已启用的 MCP Server 并连接
*
@@ -342,6 +367,9 @@ export class MCPManager {
this.toolRegistry.registerMCP(name, adapter);
}
+ // v0.5.3: 工具集合已变化 — 通知调用方同步引擎工具列表(已存在引擎热更新)
+ this.notifyToolsChanged();
+
// 更新状态
const state = this.servers.get(name)!;
state.status = 'connected';
@@ -402,6 +430,9 @@ export class MCPManager {
state.client = null;
state.tools = [];
+ // v0.5.3: 工具集合已变化 — 通知调用方同步引擎(已有引擎需移除失效工具定义)
+ this.notifyToolsChanged();
+
log.info(`MCP server "${name}" disconnected`);
}
diff --git a/package-lock.json b/package-lock.json
index a6878c6..41c4501 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "metona-ai-desktop",
- "version": "0.5.1",
+ "version": "0.5.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metona-ai-desktop",
- "version": "0.5.1",
+ "version": "0.5.2",
"license": "MIT",
"dependencies": {
"@emotion/react": "^11.14.0",
diff --git a/package.json b/package.json
index 73e0836..42677a0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "metona-ai-desktop",
- "version": "0.5.2",
+ "version": "0.5.3",
"description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用",
"main": "dist-electron/main/main.js",
"author": "Metona Team",