feat: 添加会话持久化和 Todo 工具
会话持久化: - 新增 SessionManager 和 SessionStorage,支持会话自动保存和恢复 - 会话数据存储在 ~/.local/share/ai-assist/,遵循 XDG 规范 - 支持对话历史、已发现工具、待办事项的持久化 - 启动时自动恢复同一工作目录的上次会话 - 支持会话归档和历史会话管理 Todo 工具: - 新增 todoread 工具:读取当前会话的待办事项列表 - 新增 todowrite 工具:创建和更新待办事项 - 支持 pending/in_progress/completed 三种状态 - 待办事项随会话自动持久化 其他改进: - ToolResult 类型新增可选的 metadata 字段 - Agent 支持会话管理器集成 - clearHistory 改为异步方法
This commit is contained in:
@@ -6,6 +6,7 @@ import { bashTool } from './shell/index.js';
|
||||
|
||||
// 核心工具
|
||||
import { toolSearchTool } from './tool-search.js';
|
||||
import { todoReadTool, todoWriteTool } from './todo/index.js';
|
||||
|
||||
// 文件系统工具
|
||||
import {
|
||||
@@ -27,6 +28,8 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
// 核心工具 (deferLoading: false)
|
||||
toolSearchTool,
|
||||
bashTool,
|
||||
todoReadTool,
|
||||
todoWriteTool,
|
||||
|
||||
// 文件系统工具 (deferLoading: true)
|
||||
readFileTool,
|
||||
@@ -48,6 +51,7 @@ toolRegistry.registerAll(allToolsWithMetadata);
|
||||
// 导出
|
||||
export { toolRegistry } from './registry.js';
|
||||
export { toolSearchTool } from './tool-search.js';
|
||||
export { todoManager } from './todo/index.js';
|
||||
export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js';
|
||||
|
||||
// 兼容旧代码:导出所有工具数组(基础 Tool 类型)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { todoReadTool } from './todoread.js';
|
||||
export { todoWriteTool } from './todowrite.js';
|
||||
export { todoManager } from './todo-manager.js';
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { Todo, TodoStatus } from '../../session/types.js';
|
||||
import type { SessionManager } from '../../session/index.js';
|
||||
|
||||
/**
|
||||
* Todo 管理器
|
||||
* 提供对当前会话 todo 列表的操作接口
|
||||
*/
|
||||
class TodoManager {
|
||||
private sessionManager: SessionManager | null = null;
|
||||
|
||||
/**
|
||||
* 设置会话管理器
|
||||
*/
|
||||
setSessionManager(manager: SessionManager): void {
|
||||
this.sessionManager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 todo 列表
|
||||
*/
|
||||
getTodos(): Todo[] {
|
||||
if (!this.sessionManager) {
|
||||
return [];
|
||||
}
|
||||
return this.sessionManager.getTodos();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 todo 列表
|
||||
*/
|
||||
async setTodos(todos: Todo[]): Promise<void> {
|
||||
if (!this.sessionManager) {
|
||||
return;
|
||||
}
|
||||
await this.sessionManager.setTodos(todos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单个 todo
|
||||
*/
|
||||
async addTodo(content: string, status: TodoStatus = 'pending'): Promise<Todo> {
|
||||
const todos = this.getTodos();
|
||||
const now = new Date().toISOString();
|
||||
const newTodo: Todo = {
|
||||
id: this.generateId(),
|
||||
content,
|
||||
status,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
todos.push(newTodo);
|
||||
await this.setTodos(todos);
|
||||
return newTodo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 todo 状态
|
||||
*/
|
||||
async updateTodoStatus(id: string, status: TodoStatus): Promise<boolean> {
|
||||
const todos = this.getTodos();
|
||||
const todo = todos.find((t) => t.id === id);
|
||||
if (!todo) return false;
|
||||
|
||||
todo.status = status;
|
||||
todo.updatedAt = new Date().toISOString();
|
||||
await this.setTodos(todos);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 todo
|
||||
*/
|
||||
async deleteTodo(id: string): Promise<boolean> {
|
||||
const todos = this.getTodos();
|
||||
const index = todos.findIndex((t) => t.id === id);
|
||||
if (index === -1) return false;
|
||||
|
||||
todos.splice(index, 1);
|
||||
await this.setTodos(todos);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有 todo
|
||||
*/
|
||||
async clearTodos(): Promise<void> {
|
||||
await this.setTodos([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一 ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已初始化
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.sessionManager !== null;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const todoManager = new TodoManager();
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { todoManager } from './todo-manager.js';
|
||||
|
||||
export const todoReadTool: ToolWithMetadata = {
|
||||
name: 'todoread',
|
||||
description: `读取当前会话的待办事项列表。
|
||||
|
||||
使用场景:
|
||||
- 在对话开始时查看待处理的任务
|
||||
- 开始新任务前了解当前进度
|
||||
- 用户询问之前的任务或计划时
|
||||
- 不确定下一步做什么时
|
||||
- 完成任务后更新对剩余工作的理解
|
||||
- 每隔几条消息检查一次以确保进度正常
|
||||
|
||||
返回格式:
|
||||
- 返回 JSON 格式的待办事项列表
|
||||
- 每个事项包含 id、content(内容)、status(状态)
|
||||
- 状态:pending(待处理)、in_progress(进行中)、completed(已完成)`,
|
||||
metadata: {
|
||||
name: 'todoread',
|
||||
category: 'core',
|
||||
description: '读取待办事项列表',
|
||||
keywords: ['todo', 'task', 'list', 'read', '待办', '任务', '列表', '进度'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
parameters: {},
|
||||
execute: async (): Promise<ToolResult> => {
|
||||
if (!todoManager.isInitialized()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '会话管理器未初始化,无法读取待办事项',
|
||||
};
|
||||
}
|
||||
|
||||
const todos = todoManager.getTodos();
|
||||
const pendingCount = todos.filter((t) => t.status !== 'completed').length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: JSON.stringify(todos, null, 2),
|
||||
metadata: {
|
||||
todos,
|
||||
pendingCount,
|
||||
totalCount: todos.length,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import type { Todo, TodoStatus } from '../../session/types.js';
|
||||
import { todoManager } from './todo-manager.js';
|
||||
|
||||
/**
|
||||
* 验证 todo 项
|
||||
*/
|
||||
function validateTodo(item: unknown): item is { content: string; status: TodoStatus } {
|
||||
if (typeof item !== 'object' || item === null) return false;
|
||||
const obj = item as Record<string, unknown>;
|
||||
if (typeof obj.content !== 'string' || obj.content.trim() === '') return false;
|
||||
if (!['pending', 'in_progress', 'completed'].includes(obj.status as string)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export const todoWriteTool: ToolWithMetadata = {
|
||||
name: 'todowrite',
|
||||
description: `创建和管理当前会话的待办事项列表。用于跟踪进度、组织复杂任务,并向用户展示工作进展。
|
||||
|
||||
## 使用场景
|
||||
|
||||
主动使用此工具的情况:
|
||||
1. 复杂多步骤任务 - 任务需要 3 个或更多步骤
|
||||
2. 非平凡的复杂任务 - 需要仔细规划或多个操作的任务
|
||||
3. 用户明确要求使用待办列表
|
||||
4. 用户提供多个任务 - 用户给出编号列表或逗号分隔的任务
|
||||
5. 收到新指令后 - 立即将用户需求记录为待办
|
||||
6. 开始处理任务时 - 将其标记为 in_progress
|
||||
7. 完成任务后 - 标记为 completed 并添加发现的后续任务
|
||||
|
||||
## 不使用此工具的情况
|
||||
|
||||
跳过使用的情况:
|
||||
1. 只有单个简单任务
|
||||
2. 任务太简单,跟踪没有意义
|
||||
3. 任务可以在 3 个简单步骤内完成
|
||||
4. 纯粹的对话或信息性请求
|
||||
|
||||
## 任务状态
|
||||
|
||||
- pending: 待处理,尚未开始
|
||||
- in_progress: 进行中(同一时间只能有一个)
|
||||
- completed: 已完成
|
||||
|
||||
## 任务管理规则
|
||||
|
||||
- 实时更新任务状态
|
||||
- 完成后立即标记(不要批量标记)
|
||||
- 同一时间只有一个任务处于 in_progress
|
||||
- 完成当前任务后再开始新任务`,
|
||||
metadata: {
|
||||
name: 'todowrite',
|
||||
category: 'core',
|
||||
description: '创建和更新待办事项列表',
|
||||
keywords: ['todo', 'task', 'write', 'update', 'create', '待办', '任务', '创建', '更新'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
parameters: {
|
||||
todos: {
|
||||
type: 'array',
|
||||
description:
|
||||
'更新后的待办事项列表。每个事项包含 content(任务内容)和 status(pending/in_progress/completed)',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
if (!todoManager.isInitialized()) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '会话管理器未初始化,无法更新待办事项',
|
||||
};
|
||||
}
|
||||
|
||||
const todosInput = params.todos;
|
||||
if (!Array.isArray(todosInput)) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'todos 参数必须是数组',
|
||||
};
|
||||
}
|
||||
|
||||
// 验证并转换输入
|
||||
const now = new Date().toISOString();
|
||||
const existingTodos = todoManager.getTodos();
|
||||
const existingMap = new Map(existingTodos.map((t) => [t.content, t]));
|
||||
|
||||
const newTodos: Todo[] = [];
|
||||
for (let i = 0; i < todosInput.length; i++) {
|
||||
const item = todosInput[i];
|
||||
if (!validateTodo(item)) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `第 ${i + 1} 个待办事项格式无效。需要 { content: string, status: 'pending' | 'in_progress' | 'completed' }`,
|
||||
};
|
||||
}
|
||||
|
||||
// 查找是否已存在(通过内容匹配)
|
||||
const existing = existingMap.get(item.content);
|
||||
if (existing) {
|
||||
// 更新现有项
|
||||
newTodos.push({
|
||||
...existing,
|
||||
status: item.status,
|
||||
updatedAt: now,
|
||||
});
|
||||
} else {
|
||||
// 创建新项
|
||||
newTodos.push({
|
||||
id: Math.random().toString(36).substring(2, 10),
|
||||
content: item.content,
|
||||
status: item.status,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await todoManager.setTodos(newTodos);
|
||||
|
||||
const pendingCount = newTodos.filter((t) => t.status !== 'completed').length;
|
||||
const completedCount = newTodos.filter((t) => t.status === 'completed').length;
|
||||
const inProgressCount = newTodos.filter((t) => t.status === 'in_progress').length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `待办事项已更新: ${pendingCount} 待处理, ${inProgressCount} 进行中, ${completedCount} 已完成`,
|
||||
metadata: {
|
||||
todos: newTodos,
|
||||
pendingCount,
|
||||
inProgressCount,
|
||||
completedCount,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user