feat: 升级至 v0.3.13 — 任务管理系统重构(删除 todo_write + IPC 越权防护 + UI 自动刷新)
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
|
||||
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<string, unknown>;
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user