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:
+43
-1
@@ -11,6 +11,7 @@ import {
|
||||
import type { Tool, ToolResult, Message, AgentConfig, ProviderType } from '../types/index.js';
|
||||
import { buildZodSchema } from '../types/index.js';
|
||||
import { ToolRegistry } from '../tools/registry.js';
|
||||
import { SessionManager } from '../session/index.js';
|
||||
|
||||
// Provider 工厂函数类型
|
||||
type ProviderFactory = (apiKey: string) => (model: string) => LanguageModel;
|
||||
@@ -41,6 +42,9 @@ export class Agent {
|
||||
// 兼容旧模式:直接注册的工具
|
||||
private legacyTools: Map<string, Tool> = new Map();
|
||||
|
||||
// 会话管理器(可选)
|
||||
private sessionManager: SessionManager | null = null;
|
||||
|
||||
constructor(config: AgentConfig) {
|
||||
this.config = config;
|
||||
|
||||
@@ -58,6 +62,26 @@ export class Agent {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话管理器(启用会话持久化)
|
||||
*/
|
||||
setSessionManager(manager: SessionManager): void {
|
||||
this.sessionManager = manager;
|
||||
// 从会话恢复状态
|
||||
const session = manager.getSession();
|
||||
if (session) {
|
||||
this.conversationHistory = [...session.messages];
|
||||
this.discoveredTools = new Set(session.discoveredTools);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话管理器
|
||||
*/
|
||||
getSessionManager(): SessionManager | null {
|
||||
return this.sessionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册单个工具(兼容旧代码)
|
||||
*/
|
||||
@@ -206,15 +230,33 @@ export class Agent {
|
||||
content: fullResponse,
|
||||
});
|
||||
|
||||
// 持久化会话
|
||||
await this.persistSession();
|
||||
|
||||
return fullResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化当前会话状态
|
||||
*/
|
||||
private async persistSession(): Promise<void> {
|
||||
if (!this.sessionManager) return;
|
||||
|
||||
await this.sessionManager.setMessages(this.conversationHistory);
|
||||
await this.sessionManager.setDiscoveredTools([...this.discoveredTools]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空对话历史和发现的工具
|
||||
*/
|
||||
clearHistory(): void {
|
||||
async clearHistory(): Promise<void> {
|
||||
this.conversationHistory = [];
|
||||
this.discoveredTools.clear();
|
||||
|
||||
// 如果有会话管理器,创建新会话
|
||||
if (this.sessionManager) {
|
||||
await this.sessionManager.newSession();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+18
-2
@@ -4,8 +4,9 @@ import { Command } from 'commander';
|
||||
import { Agent } from './core/agent.js';
|
||||
import { TerminalUI } from './ui/terminal.js';
|
||||
import { loadConfig, initConfig } from './utils/config.js';
|
||||
import { toolRegistry } from './tools/index.js';
|
||||
import { toolRegistry, todoManager } from './tools/index.js';
|
||||
import { getPermissionManager, promptPermission } from './permission/index.js';
|
||||
import { SessionManager } from './session/index.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -63,12 +64,27 @@ program.action(async () => {
|
||||
// 设置工具注册表(支持动态工具发现)
|
||||
agent.setRegistry(toolRegistry);
|
||||
|
||||
// 初始化会话管理器(支持会话持久化)
|
||||
const sessionManager = new SessionManager();
|
||||
await sessionManager.init(process.cwd());
|
||||
agent.setSessionManager(sessionManager);
|
||||
|
||||
// 初始化 todoManager(让 todo 工具可以访问会话)
|
||||
todoManager.setSessionManager(sessionManager);
|
||||
|
||||
// 显示会话恢复信息
|
||||
const session = sessionManager.getSession();
|
||||
if (session && session.messages.length > 0) {
|
||||
console.log(`\n📂 已恢复会话 (${session.messages.length} 条消息)`);
|
||||
}
|
||||
|
||||
// 启动终端 UI
|
||||
const ui = new TerminalUI(agent);
|
||||
|
||||
// 优雅退出
|
||||
process.on('SIGINT', () => {
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n\n👋 再见!');
|
||||
await sessionManager.close();
|
||||
ui.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export type {
|
||||
SessionData,
|
||||
SessionSummary,
|
||||
SessionManagerConfig,
|
||||
Todo,
|
||||
TodoStatus,
|
||||
} from './types.js';
|
||||
|
||||
export { SessionStorage, sessionStorage } from './storage.js';
|
||||
export { SessionManager, sessionManager } from './manager.js';
|
||||
@@ -0,0 +1,218 @@
|
||||
import type { ModelMessage } from 'ai';
|
||||
import type { SessionData, Todo, SessionSummary } from './types.js';
|
||||
import { SessionStorage, sessionStorage } from './storage.js';
|
||||
|
||||
/**
|
||||
* 会话管理器
|
||||
* 提供高级会话操作接口
|
||||
*/
|
||||
export class SessionManager {
|
||||
private storage: SessionStorage;
|
||||
private currentSession: SessionData | null = null;
|
||||
private autoSaveInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor(storage?: SessionStorage) {
|
||||
this.storage = storage || sessionStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 - 尝试恢复或创建新会话
|
||||
*/
|
||||
async init(workdir: string): Promise<SessionData> {
|
||||
// 尝试加载当前会话
|
||||
const existing = await this.storage.loadCurrentSession();
|
||||
|
||||
if (existing && existing.workdir === workdir) {
|
||||
// 同一工作目录,恢复会话
|
||||
this.currentSession = existing;
|
||||
} else {
|
||||
// 不同目录或无会话,归档旧会话并创建新的
|
||||
if (existing) {
|
||||
await this.storage.archiveCurrentSession();
|
||||
}
|
||||
this.currentSession = this.createNewSession(workdir);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
// 启动自动保存
|
||||
this.startAutoSave();
|
||||
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
private createNewSession(workdir: string): SessionData {
|
||||
return {
|
||||
id: this.storage.generateSessionId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
workdir,
|
||||
messages: [],
|
||||
discoveredTools: [],
|
||||
todos: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话
|
||||
*/
|
||||
getSession(): SessionData | null {
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前会话
|
||||
*/
|
||||
async save(): Promise<void> {
|
||||
if (this.currentSession) {
|
||||
await this.storage.saveCurrentSession(this.currentSession);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息
|
||||
*/
|
||||
async addMessage(message: ModelMessage): Promise<void> {
|
||||
if (!this.currentSession) return;
|
||||
this.currentSession.messages.push(message);
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置消息(用于同步整个对话历史)
|
||||
*/
|
||||
async setMessages(messages: ModelMessage[]): Promise<void> {
|
||||
if (!this.currentSession) return;
|
||||
this.currentSession.messages = messages;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话历史
|
||||
*/
|
||||
getMessages(): ModelMessage[] {
|
||||
return this.currentSession?.messages || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置已发现的工具
|
||||
*/
|
||||
async setDiscoveredTools(tools: string[]): Promise<void> {
|
||||
if (!this.currentSession) return;
|
||||
this.currentSession.discoveredTools = tools;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已发现的工具
|
||||
*/
|
||||
getDiscoveredTools(): string[] {
|
||||
return this.currentSession?.discoveredTools || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新待办事项
|
||||
*/
|
||||
async setTodos(todos: Todo[]): Promise<void> {
|
||||
if (!this.currentSession) return;
|
||||
this.currentSession.todos = todos;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待办事项
|
||||
*/
|
||||
getTodos(): Todo[] {
|
||||
return this.currentSession?.todos || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前会话并创建新会话
|
||||
*/
|
||||
async newSession(workdir?: string): Promise<SessionData> {
|
||||
// 归档当前会话
|
||||
if (this.currentSession && this.currentSession.messages.length > 0) {
|
||||
await this.storage.archiveCurrentSession();
|
||||
}
|
||||
|
||||
// 创建新会话
|
||||
const newWorkdir = workdir || this.currentSession?.workdir || process.cwd();
|
||||
this.currentSession = this.createNewSession(newWorkdir);
|
||||
await this.save();
|
||||
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复指定会话
|
||||
*/
|
||||
async restoreSession(sessionId: string): Promise<SessionData | null> {
|
||||
const session = await this.storage.loadSession(sessionId);
|
||||
if (!session) return null;
|
||||
|
||||
// 归档当前会话
|
||||
if (this.currentSession && this.currentSession.messages.length > 0) {
|
||||
await this.storage.archiveCurrentSession();
|
||||
}
|
||||
|
||||
this.currentSession = session;
|
||||
await this.save();
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出历史会话
|
||||
*/
|
||||
async listSessions(): Promise<SessionSummary[]> {
|
||||
return this.storage.listSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除历史会话
|
||||
*/
|
||||
async deleteSession(sessionId: string): Promise<boolean> {
|
||||
return this.storage.deleteSession(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动自动保存(每 30 秒)
|
||||
*/
|
||||
private startAutoSave(): void {
|
||||
if (this.autoSaveInterval) return;
|
||||
|
||||
this.autoSaveInterval = setInterval(async () => {
|
||||
await this.save();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止自动保存
|
||||
*/
|
||||
stopAutoSave(): void {
|
||||
if (this.autoSaveInterval) {
|
||||
clearInterval(this.autoSaveInterval);
|
||||
this.autoSaveInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭管理器(保存并停止自动保存)
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
this.stopAutoSave();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧会话
|
||||
*/
|
||||
async cleanup(keepCount?: number): Promise<number> {
|
||||
return this.storage.cleanupOldSessions(keepCount);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
export const sessionManager = new SessionManager();
|
||||
@@ -0,0 +1,205 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import type { SessionData, SessionSummary } from './types.js';
|
||||
|
||||
/**
|
||||
* 获取默认存储目录
|
||||
* 遵循 XDG 规范:~/.local/share/ai-assist/
|
||||
*/
|
||||
function getDefaultStorageDir(): string {
|
||||
const xdgDataHome = process.env.XDG_DATA_HOME;
|
||||
if (xdgDataHome) {
|
||||
return path.join(xdgDataHome, 'ai-assist');
|
||||
}
|
||||
return path.join(os.homedir(), '.local', 'share', 'ai-assist');
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话存储类
|
||||
* 负责会话数据的读写操作
|
||||
*/
|
||||
export class SessionStorage {
|
||||
private storageDir: string;
|
||||
private sessionsDir: string;
|
||||
private currentSessionFile: string;
|
||||
|
||||
constructor(storageDir?: string) {
|
||||
this.storageDir = storageDir || getDefaultStorageDir();
|
||||
this.sessionsDir = path.join(this.storageDir, 'sessions');
|
||||
this.currentSessionFile = path.join(this.storageDir, 'current-session.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保存储目录存在
|
||||
*/
|
||||
async ensureDir(): Promise<void> {
|
||||
await fs.mkdir(this.sessionsDir, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话 ID
|
||||
*/
|
||||
generateSessionId(): string {
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().slice(0, 10); // YYYY-MM-DD
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
return `${timestamp}_${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前会话
|
||||
*/
|
||||
async saveCurrentSession(session: SessionData): Promise<void> {
|
||||
await this.ensureDir();
|
||||
session.updatedAt = new Date().toISOString();
|
||||
await fs.writeFile(
|
||||
this.currentSessionFile,
|
||||
JSON.stringify(session, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载当前会话
|
||||
*/
|
||||
async loadCurrentSession(): Promise<SessionData | null> {
|
||||
try {
|
||||
const content = await fs.readFile(this.currentSessionFile, 'utf-8');
|
||||
return JSON.parse(content) as SessionData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档当前会话到历史
|
||||
*/
|
||||
async archiveCurrentSession(): Promise<void> {
|
||||
const current = await this.loadCurrentSession();
|
||||
if (!current || current.messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ensureDir();
|
||||
const archivePath = path.join(this.sessionsDir, `${current.id}.json`);
|
||||
await fs.writeFile(archivePath, JSON.stringify(current, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前会话文件
|
||||
*/
|
||||
async clearCurrentSession(): Promise<void> {
|
||||
try {
|
||||
await fs.unlink(this.currentSessionFile);
|
||||
} catch {
|
||||
// 文件不存在,忽略
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出历史会话
|
||||
*/
|
||||
async listSessions(): Promise<SessionSummary[]> {
|
||||
await this.ensureDir();
|
||||
const files = await fs.readdir(this.sessionsDir);
|
||||
const summaries: SessionSummary[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.json')) continue;
|
||||
|
||||
try {
|
||||
const filePath = path.join(this.sessionsDir, file);
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const session = JSON.parse(content) as SessionData;
|
||||
|
||||
summaries.push({
|
||||
id: session.id,
|
||||
title: session.title || this.generateTitle(session),
|
||||
workdir: session.workdir,
|
||||
messageCount: session.messages.length,
|
||||
createdAt: session.createdAt,
|
||||
updatedAt: session.updatedAt,
|
||||
});
|
||||
} catch {
|
||||
// 跳过无法解析的文件
|
||||
}
|
||||
}
|
||||
|
||||
// 按更新时间降序排列
|
||||
return summaries.sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定会话
|
||||
*/
|
||||
async loadSession(sessionId: string): Promise<SessionData | null> {
|
||||
try {
|
||||
const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(content) as SessionData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定会话
|
||||
*/
|
||||
async deleteSession(sessionId: string): Promise<boolean> {
|
||||
try {
|
||||
const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
|
||||
await fs.unlink(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧会话(保留最近 N 个)
|
||||
*/
|
||||
async cleanupOldSessions(keepCount: number = 50): Promise<number> {
|
||||
const sessions = await this.listSessions();
|
||||
if (sessions.length <= keepCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const toDelete = sessions.slice(keepCount);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const session of toDelete) {
|
||||
if (await this.deleteSession(session.id)) {
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从会话生成标题
|
||||
*/
|
||||
private generateTitle(session: SessionData): string {
|
||||
// 从第一条用户消息生成标题
|
||||
const firstUserMessage = session.messages.find((m) => m.role === 'user');
|
||||
if (firstUserMessage && typeof firstUserMessage.content === 'string') {
|
||||
const content = firstUserMessage.content;
|
||||
// 取前 50 个字符
|
||||
return content.length > 50 ? content.substring(0, 50) + '...' : content;
|
||||
}
|
||||
return `会话 ${session.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储目录路径
|
||||
*/
|
||||
getStorageDir(): string {
|
||||
return this.storageDir;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
export const sessionStorage = new SessionStorage();
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { ModelMessage } from 'ai';
|
||||
|
||||
/**
|
||||
* 待办项状态
|
||||
*/
|
||||
export type TodoStatus = 'pending' | 'in_progress' | 'completed';
|
||||
|
||||
/**
|
||||
* 待办项
|
||||
*/
|
||||
export interface Todo {
|
||||
id: string;
|
||||
content: string;
|
||||
status: TodoStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话数据(持久化存储格式)
|
||||
*/
|
||||
export interface SessionData {
|
||||
/** 会话 ID */
|
||||
id: string;
|
||||
/** 创建时间 */
|
||||
createdAt: string;
|
||||
/** 最后更新时间 */
|
||||
updatedAt: string;
|
||||
/** 工作目录 */
|
||||
workdir: string;
|
||||
/** 会话标题(可选,从第一条消息生成) */
|
||||
title?: string;
|
||||
/** 对话历史 */
|
||||
messages: ModelMessage[];
|
||||
/** 已发现的工具 */
|
||||
discoveredTools: string[];
|
||||
/** 待办事项 */
|
||||
todos: Todo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话摘要(用于列表展示)
|
||||
*/
|
||||
export interface SessionSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
workdir: string;
|
||||
messageCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话管理器配置
|
||||
*/
|
||||
export interface SessionManagerConfig {
|
||||
/** 存储目录 */
|
||||
storageDir: string;
|
||||
/** 最大历史会话数量 */
|
||||
maxHistorySessions?: number;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -18,6 +18,8 @@ export interface ToolResult {
|
||||
success: boolean;
|
||||
output: string;
|
||||
error?: string;
|
||||
/** 额外的元数据(可选) */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 工具定义(兼容 Vercel AI SDK 的 tool 格式)
|
||||
|
||||
+2
-1
@@ -50,7 +50,8 @@ export class TerminalUI {
|
||||
return true;
|
||||
|
||||
case '/clear':
|
||||
this.agent.clearHistory();
|
||||
// clearHistory 现在是异步的
|
||||
void this.agent.clearHistory();
|
||||
console.log(chalk.green('✓ 对话历史已清空\n'));
|
||||
return true;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user