feat: 升级至 v0.2.1 — 流式渲染修复、安全增强、工具自动执行

流式渲染修复:
- runId 机制防止 abort 后旧流事件污染新 run
- run lock 防止并发 run 污染引擎状态
- abort race 提前退出工具执行等待
- TERMINATED 状态通过 stateChange 发射
- tool_call_delta 流式参数拼接 + pending 占位替换
- 首轮卡片创建路径统一,traceStep 按 ID 精确匹配
- compressed 事件转发为 toast 通知

安全增强:
- ConfirmationHook 支持持久化自动执行(跨会话)
- 设置面板新增自动执行工具管理 UI
- SandboxManager 双重安全校验 fail-closed
- 审计日志链式哈希防篡改
- PromptInjectionDefender 中文注入标记清理
- scanCode 28 模式 + base64/$() 检测
- validatePath realpathSync 防符号链接逃逸
- code-search 使用 execFile 防命令注入

新增工具:
- file_editor、code_search、task_manager、diff_viewer

其他:
- Agent Loop 加 PARSING/REFLECTING 状态 + 指数退避重试
- MemoryManager TF-IDF 语义检索
- run_command Windows 中文编码修复(chcp 65001)
- 版本号 0.2.0 → 0.2.1
This commit is contained in:
thzxx
2026-07-12 12:54:52 +08:00
parent 469f53b623
commit 3c5aea8fb7
41 changed files with 4972 additions and 423 deletions
@@ -0,0 +1,329 @@
/**
* 任务管理工具(1 个)
*
* task_manager — 创建/更新/列出/完成任务
*
* 支持 Agent 在执行复杂任务时将任务分解为子任务并跟踪状态。
* 任务持久化到 SQLite tasks 表。
*
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html
*/
import { nanoid } from 'nanoid';
import type Database from 'better-sqlite3';
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'blocked' | 'cancelled';
export type TaskPriority = 'low' | 'medium' | 'high' | 'critical';
export interface Task {
id: string;
sessionId: string;
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
parentId: string | null;
assignedTo: string | null;
order: number;
createdAt: number;
updatedAt: number;
completedAt: number | null;
}
/** 从数据库行映射到 Task 对象 */
function mapRow(row: Record<string, unknown>): Task {
return {
id: row.id as string,
sessionId: row.session_id as string,
title: row.title as string,
description: row.description as string,
status: row.status as TaskStatus,
priority: row.priority as TaskPriority,
parentId: (row.parent_id as string) ?? null,
assignedTo: (row.assigned_to as string) ?? null,
order: row.order_idx as number,
createdAt: row.created_at as number,
updatedAt: row.updated_at as number,
completedAt: (row.completed_at as number) ?? null,
};
}
// T3.5: 合法枚举值
const VALID_STATUSES: TaskStatus[] = ['pending', 'in_progress', 'completed', 'blocked', 'cancelled'];
const VALID_PRIORITIES: TaskPriority[] = ['low', 'medium', 'high', 'critical'];
export class TaskManagerTool implements IMetonaTool {
readonly definition: MetonaToolDef = {
name: 'task_manager',
description: 'Manage tasks for complex multi-step work. Supports operations: create, update, list, complete, delete. Tasks are persisted per session and can have parent-child relationships.',
parameters: {
type: 'object',
properties: {
operation: {
type: 'string',
description: 'Task operation',
enum: ['create', 'update', 'list', 'complete', 'delete', 'get'],
},
title: { type: 'string', description: 'Task title (for create)' },
description: { type: 'string', description: 'Task description (for create/update)' },
task_id: { type: 'string', description: 'Task ID (for update/complete/delete/get)' },
status: { type: 'string', description: 'New status (for update)', enum: ['pending', 'in_progress', 'completed', 'blocked', 'cancelled'] },
priority: { type: 'string', description: 'Priority (for create/update)', enum: ['low', 'medium', 'high', 'critical'] },
parent_id: { type: 'string', description: 'Parent task ID (for create, establishes subtask relationship)' },
},
required: ['operation'],
},
category: MetonaToolCategory.DATABASE,
riskLevel: MetonaRiskLevel.LOW,
requiresPermission: false,
timeoutMs: 10_000,
};
constructor(private getDB: () => Database.Database) {}
async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
try {
const operation = args.operation as string;
switch (operation) {
case 'create':
return this.createTask(args, context);
case 'update':
return this.updateTask(args, context);
case 'list':
return this.listTasks(args, context);
case 'complete':
return this.completeTask(args, context);
case 'delete':
return this.deleteTask(args, context);
case 'get':
return this.getTask(args, context);
default:
return { success: false, error: `Unknown operation: ${operation}` };
}
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
private createTask(args: Record<string, unknown>, context: ToolExecutionContext): unknown {
const db = this.getDB();
const id = `task_${nanoid(12)}`;
const now = Date.now();
const sessionId = context.sessionId;
const title = args.title as string;
const description = (args.description as string) ?? '';
const priority = (args.priority as TaskPriority) ?? 'medium';
const parentId = (args.parent_id as string) ?? null;
if (!title) {
return { success: false, error: 'title is required for create operation' };
}
// T3.5: 校验 priority 枚举值
if (!VALID_PRIORITIES.includes(priority)) {
return { success: false, error: `Invalid priority: ${priority}. Must be one of: ${VALID_PRIORITIES.join(', ')}` };
}
// 获取同 session 同 parent 下的最大 order
const maxOrderRow = db.prepare(
'SELECT MAX(order_idx) as max_order FROM tasks WHERE session_id = ? AND parent_id IS ?',
).get(sessionId, parentId) as { max_order: number | null } | undefined;
const order = (maxOrderRow?.max_order ?? -1) + 1;
db.prepare(`
INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, assigned_to, order_idx, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, sessionId, title, description, 'pending', priority, parentId, null, order, now, now);
return {
success: true,
task: {
id, sessionId, title, description,
status: 'pending', priority, parentId,
assignedTo: null, order,
createdAt: now, updatedAt: now, completedAt: null,
},
};
}
private updateTask(args: Record<string, unknown>, context: ToolExecutionContext): unknown {
const db = this.getDB();
const taskId = args.task_id as string;
if (!taskId) {
return { success: false, error: 'task_id is required for update operation' };
}
// T3.1: 校验任务属于当前会话
const existing = db.prepare('SELECT * FROM tasks WHERE id = ? AND session_id = ?').get(taskId, context.sessionId);
if (!existing) {
return { success: false, error: `Task not found: ${taskId}` };
}
// T3.5: 校验枚举值
if (args.status !== undefined && !VALID_STATUSES.includes(args.status as TaskStatus)) {
return { success: false, error: `Invalid status: ${args.status}. Must be one of: ${VALID_STATUSES.join(', ')}` };
}
if (args.priority !== undefined && !VALID_PRIORITIES.includes(args.priority as TaskPriority)) {
return { success: false, error: `Invalid priority: ${args.priority}. Must be one of: ${VALID_PRIORITIES.join(', ')}` };
}
const updates: string[] = [];
const values: unknown[] = [];
if (args.title !== undefined) {
updates.push('title = ?');
values.push(args.title);
}
if (args.description !== undefined) {
updates.push('description = ?');
values.push(args.description);
}
if (args.status !== undefined) {
updates.push('status = ?');
values.push(args.status);
if (args.status === 'completed') {
updates.push('completed_at = ?');
values.push(Date.now());
}
}
if (args.priority !== undefined) {
updates.push('priority = ?');
values.push(args.priority);
}
if (updates.length === 0) {
return { success: false, error: 'No fields to update' };
}
updates.push('updated_at = ?');
values.push(Date.now());
values.push(taskId);
values.push(context.sessionId);
// T3.1: UPDATE 语句加 AND session_id = ? 防止越权
db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ? AND session_id = ?`).run(...values);
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) };
}
private listTasks(args: Record<string, unknown>, context: ToolExecutionContext): unknown {
const db = this.getDB();
const sessionId = context.sessionId;
const status = args.status as string | undefined;
let sql = 'SELECT * FROM tasks WHERE session_id = ?';
const values: unknown[] = [sessionId];
if (status) {
sql += ' AND status = ?';
values.push(status);
}
sql += ' ORDER BY order_idx ASC, created_at ASC';
const rows = db.prepare(sql).all(...values) as Array<Record<string, unknown>>;
const tasks = rows.map(mapRow);
return {
success: true,
tasks,
count: tasks.length,
by_status: {
pending: tasks.filter((t) => t.status === 'pending').length,
in_progress: tasks.filter((t) => t.status === 'in_progress').length,
completed: tasks.filter((t) => t.status === 'completed').length,
blocked: tasks.filter((t) => t.status === 'blocked').length,
cancelled: tasks.filter((t) => t.status === 'cancelled').length,
},
};
}
private completeTask(args: Record<string, unknown>, context: ToolExecutionContext): unknown {
const db = this.getDB();
const taskId = args.task_id as string;
if (!taskId) {
return { success: false, error: 'task_id is required for complete operation' };
}
// T3.1: 校验任务属于当前会话
const existing = db.prepare('SELECT id FROM tasks WHERE id = ? AND session_id = ?').get(taskId, context.sessionId);
if (!existing) {
return { success: false, error: `Task not found: ${taskId}` };
}
const now = Date.now();
// T3.1: UPDATE 语句加 AND session_id = ? 防止越权
const result = db.prepare(
'UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ? AND session_id = ?',
).run('completed', now, now, taskId, context.sessionId);
if (result.changes === 0) {
return { success: false, error: `Task not found: ${taskId}` };
}
return { success: true, task_id: taskId, completed_at: now };
}
private deleteTask(args: Record<string, unknown>, context: ToolExecutionContext): unknown {
const db = this.getDB();
const taskId = args.task_id as string;
if (!taskId) {
return { success: false, error: 'task_id is required for delete operation' };
}
// T3.1: 校验任务属于当前会话
const existing = db.prepare('SELECT id FROM tasks WHERE id = ? AND session_id = ?').get(taskId, context.sessionId);
if (!existing) {
return { success: false, error: `Task not found: ${taskId}` };
}
// T3.2/T3.3: 递归 CTE 查出所有后代任务 ID
const descendantIds = db.prepare(`
WITH RECURSIVE descendants AS (
SELECT id FROM tasks WHERE id = ?
UNION ALL
SELECT t.id FROM tasks t JOIN descendants d ON t.parent_id = d.id
)
SELECT id FROM descendants
`).all(taskId) as Array<{ id: string }>;
const allIds = descendantIds.map((r) => r.id);
// 事务包裹批量删除
const deleteMany = db.transaction(() => {
const placeholders = allIds.map(() => '?').join(',');
db.prepare(`DELETE FROM tasks WHERE id IN (${placeholders})`).run(...allIds);
});
deleteMany();
return { success: true, task_id: taskId, deleted: allIds.length };
}
private getTask(args: Record<string, unknown>, context: ToolExecutionContext): unknown {
const db = this.getDB();
const taskId = args.task_id as string;
if (!taskId) {
return { success: false, error: 'task_id is required for get operation' };
}
// T3.1: 校验任务属于当前会话
const row = db.prepare('SELECT * FROM tasks WHERE id = ? AND session_id = ?').get(taskId, context.sessionId) as Record<string, unknown> | undefined;
if (!row) {
return { success: false, error: `Task not found: ${taskId}` };
}
// 获取子任务(同样限制在当前会话内)
const subtasks = db.prepare('SELECT * FROM tasks WHERE parent_id = ? AND session_id = ? ORDER BY order_idx ASC').all(taskId, context.sessionId) as Array<Record<string, unknown>>;
return {
success: true,
task: mapRow(row),
subtasks: subtasks.map(mapRow),
};
}
}