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 type { Tool, ToolResult, Message, AgentConfig, ProviderType } from '../types/index.js';
|
||||||
import { buildZodSchema } from '../types/index.js';
|
import { buildZodSchema } from '../types/index.js';
|
||||||
import { ToolRegistry } from '../tools/registry.js';
|
import { ToolRegistry } from '../tools/registry.js';
|
||||||
|
import { SessionManager } from '../session/index.js';
|
||||||
|
|
||||||
// Provider 工厂函数类型
|
// Provider 工厂函数类型
|
||||||
type ProviderFactory = (apiKey: string) => (model: string) => LanguageModel;
|
type ProviderFactory = (apiKey: string) => (model: string) => LanguageModel;
|
||||||
@@ -41,6 +42,9 @@ export class Agent {
|
|||||||
// 兼容旧模式:直接注册的工具
|
// 兼容旧模式:直接注册的工具
|
||||||
private legacyTools: Map<string, Tool> = new Map();
|
private legacyTools: Map<string, Tool> = new Map();
|
||||||
|
|
||||||
|
// 会话管理器(可选)
|
||||||
|
private sessionManager: SessionManager | null = null;
|
||||||
|
|
||||||
constructor(config: AgentConfig) {
|
constructor(config: AgentConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
@@ -58,6 +62,26 @@ export class Agent {
|
|||||||
this.registry = registry;
|
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,
|
content: fullResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 持久化会话
|
||||||
|
await this.persistSession();
|
||||||
|
|
||||||
return fullResponse;
|
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.conversationHistory = [];
|
||||||
this.discoveredTools.clear();
|
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 { Agent } from './core/agent.js';
|
||||||
import { TerminalUI } from './ui/terminal.js';
|
import { TerminalUI } from './ui/terminal.js';
|
||||||
import { loadConfig, initConfig } from './utils/config.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 { getPermissionManager, promptPermission } from './permission/index.js';
|
||||||
|
import { SessionManager } from './session/index.js';
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
@@ -63,12 +64,27 @@ program.action(async () => {
|
|||||||
// 设置工具注册表(支持动态工具发现)
|
// 设置工具注册表(支持动态工具发现)
|
||||||
agent.setRegistry(toolRegistry);
|
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
|
// 启动终端 UI
|
||||||
const ui = new TerminalUI(agent);
|
const ui = new TerminalUI(agent);
|
||||||
|
|
||||||
// 优雅退出
|
// 优雅退出
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', async () => {
|
||||||
console.log('\n\n👋 再见!');
|
console.log('\n\n👋 再见!');
|
||||||
|
await sessionManager.close();
|
||||||
ui.close();
|
ui.close();
|
||||||
process.exit(0);
|
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 { toolSearchTool } from './tool-search.js';
|
||||||
|
import { todoReadTool, todoWriteTool } from './todo/index.js';
|
||||||
|
|
||||||
// 文件系统工具
|
// 文件系统工具
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +28,8 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
|
|||||||
// 核心工具 (deferLoading: false)
|
// 核心工具 (deferLoading: false)
|
||||||
toolSearchTool,
|
toolSearchTool,
|
||||||
bashTool,
|
bashTool,
|
||||||
|
todoReadTool,
|
||||||
|
todoWriteTool,
|
||||||
|
|
||||||
// 文件系统工具 (deferLoading: true)
|
// 文件系统工具 (deferLoading: true)
|
||||||
readFileTool,
|
readFileTool,
|
||||||
@@ -48,6 +51,7 @@ toolRegistry.registerAll(allToolsWithMetadata);
|
|||||||
// 导出
|
// 导出
|
||||||
export { toolRegistry } from './registry.js';
|
export { toolRegistry } from './registry.js';
|
||||||
export { toolSearchTool } from './tool-search.js';
|
export { toolSearchTool } from './tool-search.js';
|
||||||
|
export { todoManager } from './todo/index.js';
|
||||||
export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js';
|
export type { ToolWithMetadata, ToolMetadata, ToolCategory, ToolSearchResult } from './types.js';
|
||||||
|
|
||||||
// 兼容旧代码:导出所有工具数组(基础 Tool 类型)
|
// 兼容旧代码:导出所有工具数组(基础 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;
|
success: boolean;
|
||||||
output: string;
|
output: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
/** 额外的元数据(可选) */
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工具定义(兼容 Vercel AI SDK 的 tool 格式)
|
// 工具定义(兼容 Vercel AI SDK 的 tool 格式)
|
||||||
|
|||||||
+2
-1
@@ -50,7 +50,8 @@ export class TerminalUI {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
case '/clear':
|
case '/clear':
|
||||||
this.agent.clearHistory();
|
// clearHistory 现在是异步的
|
||||||
|
void this.agent.clearHistory();
|
||||||
console.log(chalk.green('✓ 对话历史已清空\n'));
|
console.log(chalk.green('✓ 对话历史已清空\n'));
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user