1 Commits
13 changed files with 114 additions and 280 deletions
+2 -3
View File
@@ -2,7 +2,7 @@
> 生产级通用 AI Agent 智能体桌面应用 > 生产级通用 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) [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
[![Electron](https://img.shields.io/badge/Electron-35-47848F)](https://www.electronjs.org/) [![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/) [![React](https://img.shields.io/badge/React-19-61DAFB)](https://react.dev/)
@@ -275,8 +275,7 @@ Metona 内置 27 个工具,按功能分类如下:
| 工具 | 风险 | 需确认 | 功能 | | 工具 | 风险 | 需确认 | 功能 |
|------|------|--------|------| |------|------|--------|------|
| `task_manager` | LOW | 否 | 持久化任务 CRUD,支持父子关系 | | `task_manager` | LOW | 否 | 持久化任务 CRUD,支持父子关系,写入后通知 UI 刷新 |
| `todo_write` | SAFE | 否 | 会话级 TODOLRU 淘汰(max 50 sessions |
| `think` | SAFE | 否 | 结构化思考空间,无副作用 | | `think` | SAFE | 否 | 结构化思考空间,无副作用 |
| `view_image` | SAFE | 否 | 读取图片返回 base64,5MB 限制,7 种格式 | | `view_image` | SAFE | 否 | 读取图片返回 base64,5MB 限制,7 种格式 |
+2 -1
View File
@@ -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 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 3. ALWAYS use tools when they can help accomplish the task
4. NEVER bypass safety checks or ignore guardrails 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`;
} }
/** /**
-4
View File
@@ -74,10 +74,6 @@ export const DEFAULT_POLICIES: PermissionPolicy[] = [
// v0.3.1: HTTP 请求工具(1 个) // v0.3.1: HTTP 请求工具(1 个)
{ toolName: 'http_request', requiredLevel: PermissionLevel.READ, maxFrequency: 20 }, { 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 个)— 无副作用 // v0.3.1: 结构化思考工具(1 个)— 无副作用
{ toolName: 'think', requiredLevel: PermissionLevel.READ }, { toolName: 'think', requiredLevel: PermissionLevel.READ },
+4 -4
View File
@@ -1,6 +1,9 @@
/** /**
* 内置工具导出 * 内置工具导出
* *
* v0.3.13: 删除 todo_write 工具(零价值 + 死代码 clearSession + 与 task_manager 功能重叠)
* 工具总数:30 个(29 + DelegateTaskTool
*
* v0.3.3: 从 27 个工具扩展到 29 个工具 * v0.3.3: 从 27 个工具扩展到 29 个工具
* 新增:file_move, file_info * 新增:file_move, file_info
* *
@@ -9,7 +12,7 @@
* *
* v0.3.1: 从 15 个工具扩展到 26 个工具 * v0.3.1: 从 15 个工具扩展到 26 个工具
* 新增:git_status, git_diff, git_log, git_commit, lint_code, run_tests, * 新增: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 个工具 * v0.2.0: 从 11 个工具扩展到 15 个工具
* 新增:file_editor, code_search, task_manager, diff_viewer * 新增:file_editor, code_search, task_manager, diff_viewer
@@ -53,9 +56,6 @@ export { LintCodeTool, RunTestsTool, ProjectInfoTool } from './dev-tools';
// v0.3.1: HTTP 请求工具(1 个) // v0.3.1: HTTP 请求工具(1 个)
export { HttpRequestTool } from './http-request'; export { HttpRequestTool } from './http-request';
// v0.3.1: TODO 管理工具(1 个)
export { TodoWriteTool } from './todo';
// v0.3.1: 结构化思考工具(1 个) // v0.3.1: 结构化思考工具(1 个)
export { ThinkTool } from './think'; export { ThinkTool } from './think';
@@ -82,7 +82,22 @@ export class TaskManagerTool implements IMetonaTool {
timeoutMs: 10_000, timeoutMs: 10_000,
}; };
constructor(private getDB: () => Database.Database) {} constructor(
private getDB: () => Database.Database,
// P2v0.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<string, unknown>, context: ToolExecutionContext): Promise<unknown> { async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
try { try {
@@ -139,6 +154,9 @@ export class TaskManagerTool implements IMetonaTool {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, sessionId, title, description, 'pending', priority, parentId, null, order, now, now); `).run(id, sessionId, title, description, 'pending', priority, parentId, null, order, now, now);
// P2: 通知 UI 刷新
this.notifyChanged(sessionId);
return { return {
success: true, success: true,
task: { task: {
@@ -207,6 +225,9 @@ export class TaskManagerTool implements IMetonaTool {
// T3.1: UPDATE 语句加 AND session_id = ? 防止越权 // T3.1: UPDATE 语句加 AND session_id = ? 防止越权
db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ? AND session_id = ?`).run(...values); 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<string, unknown>; const updated = db.prepare('SELECT * FROM tasks WHERE id = ? AND session_id = ?').get(taskId, context.sessionId) as Record<string, unknown>;
return { success: true, task: mapRow(updated) }; return { success: true, task: mapRow(updated) };
} }
@@ -266,6 +287,9 @@ export class TaskManagerTool implements IMetonaTool {
return { success: false, error: `Task not found: ${taskId}` }; return { success: false, error: `Task not found: ${taskId}` };
} }
// P2: 通知 UI 刷新
this.notifyChanged(context.sessionId);
return { success: true, task_id: taskId, completed_at: now }; return { success: true, task_id: taskId, completed_at: now };
} }
@@ -301,6 +325,9 @@ export class TaskManagerTool implements IMetonaTool {
}); });
deleteMany(); deleteMany();
// P2: 通知 UI 刷新
this.notifyChanged(context.sessionId);
return { success: true, task_id: taskId, deleted: allIds.length }; return { success: true, task_id: taskId, deleted: allIds.length };
} }
-244
View File
@@ -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<string, Map<string, TodoItem>>();
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<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
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<string, TodoItem> {
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<string, TodoItem>();
TodoWriteTool.todosBySession.set(context.sessionId, map);
return map;
}
private create(args: Record<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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 };
}
}
+37 -9
View File
@@ -11,6 +11,7 @@
import { ipcMain, BrowserWindow, shell, app, dialog } from 'electron'; import { ipcMain, BrowserWindow, shell, app, dialog } from 'electron';
import { join } from 'path'; import { join } from 'path';
import { nanoid } from 'nanoid';
import type { SessionService } from '../services/session.service'; import type { SessionService } from '../services/session.service';
import type { ConfigService } from '../services/config.service'; import type { ConfigService } from '../services/config.service';
import type { WorkspaceService } from '../services/workspace.service'; import type { WorkspaceService } from '../services/workspace.service';
@@ -1567,18 +1568,35 @@ export function registerAllIPCHandlers(
return { success: false, error: 'Invalid parentId' }; return { success: false, error: 'Invalid parentId' };
} }
const db = sessionService.getDB(); 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 { 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(` db.prepare(`
INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, created_at, updated_at) INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, order_idx, created_at, updated_at)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)
`).run( `).run(
id, id,
req.sessionId, req.sessionId,
req.title, req.title,
typeof req.description === 'string' ? req.description : '', typeof req.description === 'string' ? req.description : '',
(req.priority as string) ?? 'medium', (req.priority as string) ?? 'medium',
(req.parentId as string | null) ?? null, parentId,
orderIdx,
Date.now(), Date.now(),
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 结构/枚举 // M-47 修复: 校验 id 和 updates 结构/枚举
if (typeof id !== 'string' || !id) { if (typeof id !== 'string' || !id) {
return { success: false, error: 'Invalid task 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') { if (!updates || typeof updates !== 'object') {
return { success: false, error: 'Invalid updates' }; return { success: false, error: 'Invalid updates' };
} }
@@ -1625,22 +1647,28 @@ export function registerAllIPCHandlers(
if (fields.length === 0) return { success: true }; if (fields.length === 0) return { success: true };
fields.push('updated_at = ?'); values.push(Date.now()); fields.push('updated_at = ?'); values.push(Date.now());
if (u.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); } if (u.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); }
values.push(id); values.push(id, sessionId);
db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values); // P1 修复(v0.3.13: WHERE 补 session_id 校验,防越权修改其他会话任务
db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ? AND session_id = ?`).run(...values);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
return { success: false, error: (error as Error).message }; 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 类型 // M-48 修复: 校验 id 类型
if (typeof id !== 'string' || !id) { if (typeof id !== 'string' || !id) {
return { success: false, error: 'Invalid task 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(); const db = sessionService.getDB();
try { 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 }; return { success: true };
} catch (error) { } catch (error) {
return { success: false, error: (error as Error).message }; return { success: false, error: (error as Error).message };
+9 -5
View File
@@ -14,7 +14,7 @@
* @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理 * @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 { join } from 'path';
import { existsSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, readFileSync, writeFileSync } from 'fs';
import { electronApp, optimizer } from '@electron-toolkit/utils'; import { electronApp, optimizer } from '@electron-toolkit/utils';
@@ -55,7 +55,6 @@ import {
GitStatusTool, GitDiffTool, GitLogTool, GitCommitTool, GitStatusTool, GitDiffTool, GitLogTool, GitCommitTool,
LintCodeTool, RunTestsTool, ProjectInfoTool, LintCodeTool, RunTestsTool, ProjectInfoTool,
HttpRequestTool, HttpRequestTool,
TodoWriteTool,
ThinkTool, ThinkTool,
ViewImageTool, ViewImageTool,
// v0.3.2 新增工具(1 个) // v0.3.2 新增工具(1 个)
@@ -232,7 +231,13 @@ async function initialize(): Promise<void> {
toolRegistry.registerBuiltin(runCommandTool); toolRegistry.registerBuiltin(runCommandTool);
// v0.2.0: 任务管理工具 // v0.2.0: 任务管理工具
toolRegistry.registerBuiltin(new TaskManagerTool(() => db)); // P2v0.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 统一浏览器工具 // 注册 Web Browser 统一浏览器工具
toolRegistry.registerBuiltin(new WebBrowserTool()); toolRegistry.registerBuiltin(new WebBrowserTool());
@@ -248,9 +253,8 @@ async function initialize(): Promise<void> {
toolRegistry.registerBuiltin(new RunTestsTool()); toolRegistry.registerBuiltin(new RunTestsTool());
toolRegistry.registerBuiltin(new ProjectInfoTool()); toolRegistry.registerBuiltin(new ProjectInfoTool());
// v0.3.1: 独立工具(4 个) // v0.3.1: 独立工具(3v0.3.13 删除 todo_write 后
toolRegistry.registerBuiltin(new HttpRequestTool()); toolRegistry.registerBuiltin(new HttpRequestTool());
toolRegistry.registerBuiltin(new TodoWriteTool());
toolRegistry.registerBuiltin(new ThinkTool()); toolRegistry.registerBuiltin(new ThinkTool());
toolRegistry.registerBuiltin(new ViewImageTool()); toolRegistry.registerBuiltin(new ViewImageTool());
+9 -3
View File
@@ -74,9 +74,15 @@ const metonaAPI = {
list: (sessionId?: string) => ipcRenderer.invoke('tasks:list', sessionId), list: (sessionId?: string) => ipcRenderer.invoke('tasks:list', sessionId),
create: (data: { sessionId: string; title: string; description?: string; priority?: string; parentId?: string }) => create: (data: { sessionId: string; title: string; description?: string; priority?: string; parentId?: string }) =>
ipcRenderer.invoke('tasks:create', data), ipcRenderer.invoke('tasks:create', data),
update: (id: string, updates: { title?: string; description?: string; status?: string; priority?: string; assignedTo?: string }) => update: (id: string, updates: { title?: string; description?: string; status?: string; priority?: string; assignedTo?: string }, sessionId: string) =>
ipcRenderer.invoke('tasks:update', id, updates), ipcRenderer.invoke('tasks:update', id, updates, sessionId),
delete: (id: string) => ipcRenderer.invoke('tasks:delete', id), delete: (id: string, sessionId: string) => ipcRenderer.invoke('tasks:delete', id, sessionId),
// P2v0.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: 审计日志 ===== // ===== v0.2.0: 审计日志 =====
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.12", "version": "0.3.13",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.12", "version": "0.3.13",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "metona-ai-desktop", "name": "metona-ai-desktop",
"version": "0.3.12", "version": "0.3.13",
"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",
+16 -2
View File
@@ -95,6 +95,18 @@ export function TaskList(): React.JSX.Element {
return () => { loadReqIdRef.current++; }; return () => { loadReqIdRef.current++; };
}, [sessionId, loadTasks]); }, [sessionId, loadTasks]);
// P2v0.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 () => { const handleCreate = async () => {
if (!sessionId) { if (!sessionId) {
// 用户主动操作失败应用 toast(与项目惯例一致) // 用户主动操作失败应用 toast(与项目惯例一致)
@@ -132,9 +144,10 @@ export function TaskList(): React.JSX.Element {
const handleToggleComplete = async (task: MetonaTask) => { const handleToggleComplete = async (task: MetonaTask) => {
if (!window.metona?.tasks?.update) return; if (!window.metona?.tasks?.update) return;
if (!sessionId) return;
const nextStatus: TaskStatus = task.status === 'completed' ? 'pending' : 'completed'; const nextStatus: TaskStatus = task.status === 'completed' ? 'pending' : 'completed';
try { 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) { if (!res.success) {
import('metona-toast').then((mod) => mod.default.error(res.error ?? '更新任务失败')).catch(() => {}); import('metona-toast').then((mod) => mod.default.error(res.error ?? '更新任务失败')).catch(() => {});
return; return;
@@ -147,8 +160,9 @@ export function TaskList(): React.JSX.Element {
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
if (!window.metona?.tasks?.delete) return; if (!window.metona?.tasks?.delete) return;
if (!sessionId) return;
try { try {
const res = await window.metona.tasks.delete(id); 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-toast').then((mod) => mod.default.error(res.error ?? '删除任务失败')).catch(() => {});
return; return;
+4 -1
View File
@@ -308,8 +308,11 @@ interface MetonaTasksAPI {
priority?: string; priority?: string;
assignedTo?: string; assignedTo?: string;
}, },
sessionId: string,
) => Promise<{ success: boolean; error?: string }>; ) => Promise<{ success: boolean; error?: string }>;
delete: (id: string) => Promise<{ success: boolean; error?: string }>; delete: (id: string, sessionId: string) => Promise<{ success: boolean; error?: string }>;
// P2v0.3.13: 订阅任务变更事件
onTaskChanged: (callback: (sessionId: string) => void) => () => void;
} }
// ===== v0.2.0: Audit API ===== // ===== v0.2.0: Audit API =====