From f640c9b48a16faa1c9e9fcb2e421332b51c1195f Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Tue, 21 Jul 2026 17:01:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=87=E7=BA=A7=E8=87=B3=20v0.3.13?= =?UTF-8?q?=20=E2=80=94=20=E4=BB=BB=E5=8A=A1=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E9=87=8D=E6=9E=84=EF=BC=88=E5=88=A0=E9=99=A4=20todo?= =?UTF-8?q?=5Fwrite=20+=20IPC=20=E8=B6=8A=E6=9D=83=E9=98=B2=E6=8A=A4=20+?= =?UTF-8?q?=20UI=20=E8=87=AA=E5=8A=A8=E5=88=B7=E6=96=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- electron/harness/prompts/context-builder.ts | 3 +- electron/harness/sandbox/permissions.ts | 4 - electron/harness/tools/built-in/index.ts | 8 +- .../harness/tools/built-in/task-manager.ts | 29 ++- electron/harness/tools/built-in/todo.ts | 244 ------------------ electron/ipc/handlers.ts | 46 +++- electron/main.ts | 14 +- electron/preload.ts | 12 +- package-lock.json | 4 +- package.json | 2 +- src/components/tasks/TaskList.tsx | 18 +- src/types/global.d.ts | 5 +- 13 files changed, 114 insertions(+), 280 deletions(-) delete mode 100644 electron/harness/tools/built-in/todo.ts diff --git a/README.md b/README.md index 5a29153..a108861 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > 生产级通用 AI Agent 智能体桌面应用 -[![Version](https://img.shields.io/badge/version-0.3.12-blue)](./package.json) +[![Version](https://img.shields.io/badge/version-0.3.13-blue)](./package.json) [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE) [![Electron](https://img.shields.io/badge/Electron-35-47848F)](https://www.electronjs.org/) [![React](https://img.shields.io/badge/React-19-61DAFB)](https://react.dev/) @@ -275,8 +275,7 @@ Metona 内置 27 个工具,按功能分类如下: | 工具 | 风险 | 需确认 | 功能 | |------|------|--------|------| -| `task_manager` | LOW | 否 | 持久化任务 CRUD,支持父子关系 | -| `todo_write` | SAFE | 否 | 会话级 TODO,LRU 淘汰(max 50 sessions) | +| `task_manager` | LOW | 否 | 持久化任务 CRUD,支持父子关系,写入后通知 UI 刷新 | | `think` | SAFE | 否 | 结构化思考空间,无副作用 | | `view_image` | SAFE | 否 | 读取图片返回 base64,5MB 限制,7 种格式 | diff --git a/electron/harness/prompts/context-builder.ts b/electron/harness/prompts/context-builder.ts index c3f95d9..ee823b9 100644 --- a/electron/harness/prompts/context-builder.ts +++ b/electron/harness/prompts/context-builder.ts @@ -213,7 +213,8 @@ Use tools when needed to gather information or perform actions. Think step by st 2. NEVER fabricate information — if you don't know, call a tool or say so 3. ALWAYS use tools when they can help accomplish the task 4. NEVER bypass safety checks or ignore guardrails -5. When task is complete, provide a clear summary of what was done`; +5. When task is complete, provide a clear summary of what was done +6. For multi-step complex tasks (3+ steps), proactively use \`task_manager\` to break down and track progress — users will see task updates in the Tasks panel`; } /** diff --git a/electron/harness/sandbox/permissions.ts b/electron/harness/sandbox/permissions.ts index d28450e..3ea4051 100644 --- a/electron/harness/sandbox/permissions.ts +++ b/electron/harness/sandbox/permissions.ts @@ -74,10 +74,6 @@ export const DEFAULT_POLICIES: PermissionPolicy[] = [ // v0.3.1: HTTP 请求工具(1 个) { toolName: 'http_request', requiredLevel: PermissionLevel.READ, maxFrequency: 20 }, - // v0.3.1: TODO 管理工具(1 个)— 内存存储,无持久化副作用 - // v0.3.1 修复 WARN-9: 改为 READ(内存操作,非数据库) - { toolName: 'todo_write', requiredLevel: PermissionLevel.READ }, - // v0.3.1: 结构化思考工具(1 个)— 无副作用 { toolName: 'think', requiredLevel: PermissionLevel.READ }, diff --git a/electron/harness/tools/built-in/index.ts b/electron/harness/tools/built-in/index.ts index d9bde0a..d8991e6 100644 --- a/electron/harness/tools/built-in/index.ts +++ b/electron/harness/tools/built-in/index.ts @@ -1,6 +1,9 @@ /** * 内置工具导出 * + * v0.3.13: 删除 todo_write 工具(零价值 + 死代码 clearSession + 与 task_manager 功能重叠) + * 工具总数:30 个(29 + DelegateTaskTool) + * * v0.3.3: 从 27 个工具扩展到 29 个工具 * 新增:file_move, file_info * @@ -9,7 +12,7 @@ * * v0.3.1: 从 15 个工具扩展到 26 个工具 * 新增:git_status, git_diff, git_log, git_commit, lint_code, run_tests, - * project_info, http_request, todo_write, think, view_image + * project_info, http_request, think, view_image * * v0.2.0: 从 11 个工具扩展到 15 个工具 * 新增:file_editor, code_search, task_manager, diff_viewer @@ -53,9 +56,6 @@ export { LintCodeTool, RunTestsTool, ProjectInfoTool } from './dev-tools'; // v0.3.1: HTTP 请求工具(1 个) export { HttpRequestTool } from './http-request'; -// v0.3.1: TODO 管理工具(1 个) -export { TodoWriteTool } from './todo'; - // v0.3.1: 结构化思考工具(1 个) export { ThinkTool } from './think'; diff --git a/electron/harness/tools/built-in/task-manager.ts b/electron/harness/tools/built-in/task-manager.ts index e46c3f3..3418032 100644 --- a/electron/harness/tools/built-in/task-manager.ts +++ b/electron/harness/tools/built-in/task-manager.ts @@ -82,7 +82,22 @@ export class TaskManagerTool implements IMetonaTool { timeoutMs: 10_000, }; - constructor(private getDB: () => Database.Database) {} + constructor( + private getDB: () => Database.Database, + // P2(v0.3.13): 写入后回调,用于通知 UI 刷新(避免工具层依赖 Electron) + private onTaskChanged?: (sessionId: string) => void, + ) {} + + /** 写入后触发回调(仅对修改操作) */ + private notifyChanged(sessionId: string): void { + if (this.onTaskChanged) { + try { + this.onTaskChanged(sessionId); + } catch { + // 回调失败不影响工具主流程 + } + } + } async execute(args: Record, context: ToolExecutionContext): Promise { try { @@ -139,6 +154,9 @@ export class TaskManagerTool implements IMetonaTool { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run(id, sessionId, title, description, 'pending', priority, parentId, null, order, now, now); + // P2: 通知 UI 刷新 + this.notifyChanged(sessionId); + return { success: true, task: { @@ -207,6 +225,9 @@ export class TaskManagerTool implements IMetonaTool { // T3.1: UPDATE 语句加 AND session_id = ? 防止越权 db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ? AND session_id = ?`).run(...values); + // P2: 通知 UI 刷新 + this.notifyChanged(context.sessionId); + const updated = db.prepare('SELECT * FROM tasks WHERE id = ? AND session_id = ?').get(taskId, context.sessionId) as Record; return { success: true, task: mapRow(updated) }; } @@ -266,6 +287,9 @@ export class TaskManagerTool implements IMetonaTool { return { success: false, error: `Task not found: ${taskId}` }; } + // P2: 通知 UI 刷新 + this.notifyChanged(context.sessionId); + return { success: true, task_id: taskId, completed_at: now }; } @@ -301,6 +325,9 @@ export class TaskManagerTool implements IMetonaTool { }); deleteMany(); + // P2: 通知 UI 刷新 + this.notifyChanged(context.sessionId); + return { success: true, task_id: taskId, deleted: allIds.length }; } diff --git a/electron/harness/tools/built-in/todo.ts b/electron/harness/tools/built-in/todo.ts deleted file mode 100644 index a8238f4..0000000 --- a/electron/harness/tools/built-in/todo.ts +++ /dev/null @@ -1,244 +0,0 @@ -/** - * TODO 管理工具(1 个) - * - * todo_write — 会话级 TODO 列表管理 - * - * 使用内存 Map 存储,按 sessionId 隔离。 - * 与 task_manager 区别:todo_write 用于临时思考拆解(内存,会话级), - * task_manager 用于持久化任务跟踪(数据库,跨会话)。 - * - * v0.3.1 修复 FAIL-2: 添加 LRU 策略,限制最大会话数 50, - * 超出时淘汰最久未访问的会话。提供 clearSession 静态方法供外部清理。 - * - * v0.3.1 修复 WARN-3: 真正的 LRU 实现 — 访问命中时通过 delete + re-insert - * 刷新会话位置,确保活跃会话不被淘汰,仅淘汰最久未访问的会话。 - */ - -import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool'; -import type { MetonaToolDef } from '../../../harness/types'; -import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types'; -import log from 'electron-log'; - -type TodoStatus = 'pending' | 'in_progress' | 'completed'; -type TodoPriority = 'high' | 'medium' | 'low'; - -interface TodoItem { - id: string; - content: string; - status: TodoStatus; - priority: TodoPriority; - createdAt: number; - updatedAt: number; -} - -const VALID_STATUSES: TodoStatus[] = ['pending', 'in_progress', 'completed']; -const VALID_PRIORITIES: TodoPriority[] = ['high', 'medium', 'low']; - -// v0.3.1 修复 FAIL-2: LRU 策略限制最大会话数,防止内存泄漏 -const MAX_SESSIONS = 50; - -export class TodoWriteTool implements IMetonaTool { - readonly definition: MetonaToolDef = { - name: 'todo_write', - description: 'Manage a session-level TODO list in memory. Actions: create, update, list, complete, clear. Unlike task_manager (persistent DB), todo_write is for temporary task breakdown within a session and does not persist across sessions.', - parameters: { - type: 'object', - properties: { - action: { - type: 'string', - description: 'Operation type', - enum: ['create', 'update', 'list', 'complete', 'clear'], - }, - id: { type: 'string', description: 'TODO item ID (required for update/complete)' }, - content: { type: 'string', description: 'TODO content (required for create/update)' }, - status: { - type: 'string', - description: 'Status (for update)', - enum: ['pending', 'in_progress', 'completed'], - }, - priority: { - type: 'string', - description: 'Priority (for create, default medium)', - enum: ['high', 'medium', 'low'], - }, - }, - required: ['action'], - }, - // v0.3.1 修复 WARN-9: 改为 CUSTOM(内存操作,非数据库) - category: MetonaToolCategory.CUSTOM, - riskLevel: MetonaRiskLevel.SAFE, - requiresPermission: false, - timeoutMs: 5_000, - }; - - // v0.3.1 修复 FAIL-2: 会话级 TODO 存储 + LRU 淘汰 - private static todosBySession = new Map>(); - private static counter = 0; - - /** - * v0.3.1 修复 FAIL-2: 清理指定会话的 TODO(供外部调用,如会话结束时) - */ - static clearSession(sessionId: string): void { - const removed = TodoWriteTool.todosBySession.delete(sessionId); - if (removed) { - log.debug(`[TodoWriteTool] Cleared TODOs for session ${sessionId}`); - } - } - - /** - * v0.3.1 修复 FAIL-2: LRU 淘汰 — 超过 MAX_SESSIONS 时删除最久未访问的会话 - * Map 的迭代顺序按插入顺序,第一个即最久未访问 - * - * v0.3.1 修复 WARN-3: 配合 getSessionMap 的访问刷新位置,实现真正的 LRU。 - * 活跃会话因访问而刷新到 Map 末尾,不会被淘汰。 - */ - private static evictIfFull(): void { - while (TodoWriteTool.todosBySession.size >= MAX_SESSIONS) { - const oldest = TodoWriteTool.todosBySession.keys().next().value; - if (oldest === undefined) break; - TodoWriteTool.todosBySession.delete(oldest); - log.debug(`[TodoWriteTool] LRU evicted session ${oldest}`); - } - } - - async execute(args: Record, context: ToolExecutionContext): Promise { - try { - const action = args.action as string; - - switch (action) { - case 'create': - return this.create(args, context); - case 'update': - return this.update(args, context); - case 'list': - return this.list(context); - case 'complete': - return this.complete(args, context); - case 'clear': - return this.clear(context); - default: - return { success: false, error: `Unknown action: ${action}` }; - } - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - return { error: errMsg, success: false }; - } - } - - /** - * 获取指定会话的 TODO Map - * - * v0.3.1 修复 WARN-3: 真正的 LRU — 命中已有会话时通过 delete + re-insert - * 将会话移到 Map 末尾(最近使用位置),防止活跃会话被淘汰。 - * 新会话插入前触发 evictIfFull。 - */ - private getSessionMap(context: ToolExecutionContext): Map { - const existing = TodoWriteTool.todosBySession.get(context.sessionId); - if (existing) { - // LRU 刷新位置:delete + re-insert → 移到 Map 末尾 - TodoWriteTool.todosBySession.delete(context.sessionId); - TodoWriteTool.todosBySession.set(context.sessionId, existing); - return existing; - } - // 新会话插入前检查 LRU 淘汰 - TodoWriteTool.evictIfFull(); - const map = new Map(); - TodoWriteTool.todosBySession.set(context.sessionId, map); - return map; - } - - private create(args: Record, context: ToolExecutionContext): unknown { - const content = args.content as string; - if (!content) { - return { success: false, id: '', todo: null, error: 'content is required for create action' }; - } - - const priority = (args.priority as TodoPriority) ?? 'medium'; - if (!VALID_PRIORITIES.includes(priority)) { - return { success: false, id: '', todo: null, error: `Invalid priority: ${priority}. Must be one of: ${VALID_PRIORITIES.join(', ')}` }; - } - - const now = Date.now(); - const id = `todo_${now}_${TodoWriteTool.counter++}`; - const todo: TodoItem = { - id, - content, - status: 'pending', - priority, - createdAt: now, - updatedAt: now, - }; - this.getSessionMap(context).set(id, todo); - - return { success: true, id, todo }; - } - - private update(args: Record, context: ToolExecutionContext): unknown { - const id = args.id as string; - if (!id) { - return { success: false, id: '', todo: null, error: 'id is required for update action' }; - } - - const map = this.getSessionMap(context); - const todo = map.get(id); - if (!todo) { - return { success: false, id, todo: null, error: `TODO not found: ${id}` }; - } - - if (args.content !== undefined) { - todo.content = args.content as string; - } - if (args.status !== undefined) { - const status = args.status as TodoStatus; - if (!VALID_STATUSES.includes(status)) { - return { success: false, id, todo: null, error: `Invalid status: ${status}. Must be one of: ${VALID_STATUSES.join(', ')}` }; - } - todo.status = status; - } - if (args.priority !== undefined) { - const priority = args.priority as TodoPriority; - if (!VALID_PRIORITIES.includes(priority)) { - return { success: false, id, todo: null, error: `Invalid priority: ${priority}. Must be one of: ${VALID_PRIORITIES.join(', ')}` }; - } - todo.priority = priority; - } - todo.updatedAt = Date.now(); - - return { success: true, id, todo }; - } - - private list(context: ToolExecutionContext): unknown { - const map = this.getSessionMap(context); - const todos = Array.from(map.values()).sort((a, b) => a.createdAt - b.createdAt); - return { - todos, - count: todos.length, - pending: todos.filter((t) => t.status === 'pending').length, - completed: todos.filter((t) => t.status === 'completed').length, - }; - } - - private complete(args: Record, context: ToolExecutionContext): unknown { - const id = args.id as string; - if (!id) { - return { success: false, id: '', todo: null, error: 'id is required for complete action' }; - } - - const map = this.getSessionMap(context); - const todo = map.get(id); - if (!todo) { - return { success: false, id, todo: null, error: `TODO not found: ${id}` }; - } - - todo.status = 'completed'; - todo.updatedAt = Date.now(); - return { success: true, id, todo }; - } - - private clear(context: ToolExecutionContext): unknown { - const map = this.getSessionMap(context); - const cleared = map.size; - map.clear(); - return { success: true, cleared }; - } -} diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 5cb809d..a7216c9 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -11,6 +11,7 @@ import { ipcMain, BrowserWindow, shell, app, dialog } from 'electron'; import { join } from 'path'; +import { nanoid } from 'nanoid'; import type { SessionService } from '../services/session.service'; import type { ConfigService } from '../services/config.service'; import type { WorkspaceService } from '../services/workspace.service'; @@ -1567,18 +1568,35 @@ export function registerAllIPCHandlers( return { success: false, error: 'Invalid parentId' }; } const db = sessionService.getDB(); - const id = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + // P4 统一(v0.3.13): ID 生成方式与 task_manager 工具一致(nanoid) + const id = `task_${nanoid(12)}`; try { + // P4 统一: 计算 order_idx = MAX(同 session+parent 的 order_idx) + 1(与 task_manager 工具一致) + // 注意:parent_id IS NULL 时需要单独处理,COALESCE(NULL, NULL) 仍为 NULL + const parentId = (req.parentId as string | null) ?? null; + let orderIdx = 0; + if (parentId === null) { + const orderRow = db.prepare( + 'SELECT COALESCE(MAX(order_idx), -1) AS maxOrder FROM tasks WHERE session_id = ? AND parent_id IS NULL' + ).get(req.sessionId) as { maxOrder: number } | undefined; + orderIdx = (orderRow?.maxOrder ?? -1) + 1; + } else { + const orderRow = db.prepare( + 'SELECT COALESCE(MAX(order_idx), -1) AS maxOrder FROM tasks WHERE session_id = ? AND parent_id = ?' + ).get(req.sessionId, parentId) as { maxOrder: number } | undefined; + orderIdx = (orderRow?.maxOrder ?? -1) + 1; + } db.prepare(` - INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, created_at, updated_at) - VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?) + INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, order_idx, created_at, updated_at) + VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?) `).run( id, req.sessionId, req.title, typeof req.description === 'string' ? req.description : '', (req.priority as string) ?? 'medium', - (req.parentId as string | null) ?? null, + parentId, + orderIdx, Date.now(), Date.now(), ); @@ -1588,11 +1606,15 @@ export function registerAllIPCHandlers( } }); - ipcMain.handle('tasks:update', async (_event, id: unknown, updates: unknown) => { + ipcMain.handle('tasks:update', async (_event, id: unknown, updates: unknown, sessionId: unknown) => { // M-47 修复: 校验 id 和 updates 结构/枚举 if (typeof id !== 'string' || !id) { return { success: false, error: 'Invalid task id' }; } + // P1 修复(v0.3.13): 补 session_id 越权保护(与 task_manager 工具一致) + if (typeof sessionId !== 'string' || !sessionId) { + return { success: false, error: 'Invalid sessionId' }; + } if (!updates || typeof updates !== 'object') { return { success: false, error: 'Invalid updates' }; } @@ -1625,22 +1647,28 @@ export function registerAllIPCHandlers( if (fields.length === 0) return { success: true }; fields.push('updated_at = ?'); values.push(Date.now()); if (u.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); } - values.push(id); - db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values); + values.push(id, sessionId); + // P1 修复(v0.3.13): WHERE 补 session_id 校验,防越权修改其他会话任务 + db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ? AND session_id = ?`).run(...values); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); - ipcMain.handle('tasks:delete', async (_event, id: unknown) => { + ipcMain.handle('tasks:delete', async (_event, id: unknown, sessionId: unknown) => { // M-48 修复: 校验 id 类型 if (typeof id !== 'string' || !id) { return { success: false, error: 'Invalid task id' }; } + // P1 修复(v0.3.13): 补 session_id 越权保护 + if (typeof sessionId !== 'string' || !sessionId) { + return { success: false, error: 'Invalid sessionId' }; + } const db = sessionService.getDB(); try { - db.prepare('DELETE FROM tasks WHERE id = ?').run(id); + // P1 修复: WHERE 补 session_id 校验 + db.prepare('DELETE FROM tasks WHERE id = ? AND session_id = ?').run(id, sessionId); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; diff --git a/electron/main.ts b/electron/main.ts index 2ccfb2b..fcf3e54 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -14,7 +14,7 @@ * @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理 */ -import { app, shell, Menu } from 'electron'; +import { app, shell, Menu, BrowserWindow } from 'electron'; import { join } from 'path'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import { electronApp, optimizer } from '@electron-toolkit/utils'; @@ -55,7 +55,6 @@ import { GitStatusTool, GitDiffTool, GitLogTool, GitCommitTool, LintCodeTool, RunTestsTool, ProjectInfoTool, HttpRequestTool, - TodoWriteTool, ThinkTool, ViewImageTool, // v0.3.2 新增工具(1 个) @@ -232,7 +231,13 @@ async function initialize(): Promise { toolRegistry.registerBuiltin(runCommandTool); // v0.2.0: 任务管理工具 - toolRegistry.registerBuiltin(new TaskManagerTool(() => db)); + // P2(v0.3.13): 注入 onTaskChanged 回调,工具写入后广播 IPC 事件给所有窗口 + toolRegistry.registerBuiltin(new TaskManagerTool(() => db, (sessionId) => { + // 广播任务变更事件给所有窗口(UI 订阅后自动刷新) + for (const win of BrowserWindow.getAllWindows()) { + win.webContents.send('task:changed', sessionId); + } + })); // 注册 Web Browser 统一浏览器工具 toolRegistry.registerBuiltin(new WebBrowserTool()); @@ -248,9 +253,8 @@ async function initialize(): Promise { toolRegistry.registerBuiltin(new RunTestsTool()); toolRegistry.registerBuiltin(new ProjectInfoTool()); - // v0.3.1: 独立工具(4 个) + // v0.3.1: 独立工具(3 个,v0.3.13 删除 todo_write 后) toolRegistry.registerBuiltin(new HttpRequestTool()); - toolRegistry.registerBuiltin(new TodoWriteTool()); toolRegistry.registerBuiltin(new ThinkTool()); toolRegistry.registerBuiltin(new ViewImageTool()); diff --git a/electron/preload.ts b/electron/preload.ts index 8453741..6dc91c6 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -74,9 +74,15 @@ const metonaAPI = { list: (sessionId?: string) => ipcRenderer.invoke('tasks:list', sessionId), create: (data: { sessionId: string; title: string; description?: string; priority?: string; parentId?: string }) => ipcRenderer.invoke('tasks:create', data), - update: (id: string, updates: { title?: string; description?: string; status?: string; priority?: string; assignedTo?: string }) => - ipcRenderer.invoke('tasks:update', id, updates), - delete: (id: string) => ipcRenderer.invoke('tasks:delete', id), + update: (id: string, updates: { title?: string; description?: string; status?: string; priority?: string; assignedTo?: string }, sessionId: string) => + ipcRenderer.invoke('tasks:update', id, updates, sessionId), + delete: (id: string, sessionId: string) => ipcRenderer.invoke('tasks:delete', id, sessionId), + // P2(v0.3.13): 订阅任务变更事件(Agent 通过 task_manager 写入后触发) + onTaskChanged: (callback: (sessionId: string) => void) => { + const listener = (_event: unknown, sessionId: string): void => callback(sessionId); + ipcRenderer.on('task:changed', listener); + return () => ipcRenderer.removeListener('task:changed', listener); + }, }, // ===== v0.2.0: 审计日志 ===== diff --git a/package-lock.json b/package-lock.json index 8b5bcfc..1ae8147 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "metona-ai-desktop", - "version": "0.3.12", + "version": "0.3.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metona-ai-desktop", - "version": "0.3.12", + "version": "0.3.13", "license": "MIT", "dependencies": { "@emotion/react": "^11.14.0", diff --git a/package.json b/package.json index 382a092..914162e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metona-ai-desktop", - "version": "0.3.12", + "version": "0.3.13", "description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用", "main": "dist-electron/main/main.js", "author": "Metona Team", diff --git a/src/components/tasks/TaskList.tsx b/src/components/tasks/TaskList.tsx index 2c6bccb..5820a7a 100644 --- a/src/components/tasks/TaskList.tsx +++ b/src/components/tasks/TaskList.tsx @@ -95,6 +95,18 @@ export function TaskList(): React.JSX.Element { return () => { loadReqIdRef.current++; }; }, [sessionId, loadTasks]); + // P2(v0.3.13): 订阅任务变更事件(Agent 通过 task_manager 工具写入后自动刷新) + useEffect(() => { + if (!window.metona?.tasks?.onTaskChanged) return; + const unsubscribe = window.metona.tasks.onTaskChanged((changedSessionId) => { + // 只刷新当前会话的任务 + if (changedSessionId === sessionId) { + loadTasks(sessionId ?? undefined); + } + }); + return unsubscribe; + }, [sessionId, loadTasks]); + const handleCreate = async () => { if (!sessionId) { // 用户主动操作失败应用 toast(与项目惯例一致) @@ -132,9 +144,10 @@ export function TaskList(): React.JSX.Element { const handleToggleComplete = async (task: MetonaTask) => { if (!window.metona?.tasks?.update) return; + if (!sessionId) return; const nextStatus: TaskStatus = task.status === 'completed' ? 'pending' : 'completed'; try { - const res = await window.metona.tasks.update(task.id, { status: nextStatus }); + const res = await window.metona.tasks.update(task.id, { status: nextStatus }, sessionId); if (!res.success) { import('metona-toast').then((mod) => mod.default.error(res.error ?? '更新任务失败')).catch(() => {}); return; @@ -147,8 +160,9 @@ export function TaskList(): React.JSX.Element { const handleDelete = async (id: string) => { if (!window.metona?.tasks?.delete) return; + if (!sessionId) return; try { - const res = await window.metona.tasks.delete(id); + const res = await window.metona.tasks.delete(id, sessionId); if (!res.success) { import('metona-toast').then((mod) => mod.default.error(res.error ?? '删除任务失败')).catch(() => {}); return; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 695dc12..e44f304 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -308,8 +308,11 @@ interface MetonaTasksAPI { priority?: string; assignedTo?: string; }, + sessionId: string, ) => Promise<{ success: boolean; error?: string }>; - delete: (id: string) => Promise<{ success: boolean; error?: string }>; + delete: (id: string, sessionId: string) => Promise<{ success: boolean; error?: string }>; + // P2(v0.3.13): 订阅任务变更事件 + onTaskChanged: (callback: (sessionId: string) => void) => () => void; } // ===== v0.2.0: Audit API =====