66ad1a1ec9
将三个超过 700 行的大型文件重构为模块化架构: Agent (1033 → ~400 行): - agent-tool-executor: 工具获取、过滤和执行 - agent-message-handler: 消息构建、流式处理 - agent-mode-manager: 模式切换和权限检查 - agent-vision-handler: 视觉处理委托 CheckpointManager (1015 → ~620 行): - checkpoint-store: 检查点 CRUD 操作 - checkpoint-rollback: 回滚和撤销操作 - checkpoint-session: 会话跟踪 - checkpoint-events: 事件发射系统 SessionManager (768 → 356 行): - message-converter: Part ↔ ModelMessage 转换 - session-store: 会话 CRUD 操作 - project-manager: 项目管理 - session-auto-save: 自动保存功能 重构原则: 单一职责、编排器模式、向后兼容 API
357 lines
9.7 KiB
TypeScript
357 lines
9.7 KiB
TypeScript
/**
|
|
* 会话管理器
|
|
* 作为编排器,委托具体工作给各个子模块
|
|
*/
|
|
|
|
import type { ModelMessage } from 'ai';
|
|
import * as storage from './storage/index.js';
|
|
import type { TodoItem } from './storage/index.js';
|
|
|
|
// 子模块
|
|
import { SessionStore, type SessionData, type SessionSummary } from './session-store.js';
|
|
import { ProjectManager, type ProjectMetadata } from './project-manager.js';
|
|
import { SessionAutoSave } from './session-auto-save.js';
|
|
|
|
// 重新导出类型
|
|
export type { SessionData, SessionSummary, ProjectMetadata };
|
|
|
|
/**
|
|
* 会话管理器
|
|
* 提供高级会话操作接口,使用新的三层存储结构
|
|
*/
|
|
export class SessionManager {
|
|
private currentSession: SessionData | null = null;
|
|
private storageDir?: string;
|
|
|
|
// 子模块
|
|
private store: SessionStore;
|
|
private projectManager: ProjectManager;
|
|
private autoSave: SessionAutoSave;
|
|
|
|
constructor(storageDir?: string) {
|
|
this.storageDir = storageDir;
|
|
this.store = new SessionStore();
|
|
this.projectManager = new ProjectManager();
|
|
this.autoSave = new SessionAutoSave();
|
|
}
|
|
|
|
// ============================================================================
|
|
// 初始化
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 初始化 - 尝试恢复或创建新会话
|
|
*/
|
|
async init(workdir: string): Promise<SessionData> {
|
|
// 初始化存储
|
|
await storage.initStorage(this.storageDir);
|
|
|
|
// 获取或创建项目
|
|
await this.projectManager.getOrCreate(workdir);
|
|
|
|
// 尝试加载当前会话
|
|
const currentSessionId = await this.getCurrentSessionId();
|
|
|
|
if (currentSessionId) {
|
|
const projectId = this.projectManager.getProjectId()!;
|
|
const existing = await this.store.load(projectId, currentSessionId);
|
|
|
|
if (existing && existing.workdir === workdir) {
|
|
this.currentSession = existing;
|
|
this.startAutoSave();
|
|
return this.currentSession;
|
|
}
|
|
}
|
|
|
|
// 创建新会话
|
|
const projectId = this.projectManager.getProjectId()!;
|
|
this.currentSession = await this.store.create(projectId, workdir);
|
|
await this.store.save(this.currentSession);
|
|
await this.setCurrentSessionPointer(this.currentSession.id);
|
|
|
|
this.startAutoSave();
|
|
return this.currentSession;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 会话获取
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 获取当前会话
|
|
*/
|
|
getSession(): SessionData | null {
|
|
return this.currentSession;
|
|
}
|
|
|
|
/**
|
|
* 获取当前项目
|
|
*/
|
|
getProject(): ProjectMetadata | null {
|
|
return this.projectManager.getProject();
|
|
}
|
|
|
|
/**
|
|
* 获取当前会话 ID
|
|
*/
|
|
getSessionId(): string | undefined {
|
|
return this.currentSession?.id;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 会话操作
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 保存当前会话
|
|
*/
|
|
async save(): Promise<void> {
|
|
if (!this.currentSession) return;
|
|
await this.store.save(this.currentSession);
|
|
}
|
|
|
|
/**
|
|
* 清空当前会话并创建新会话
|
|
*/
|
|
async newSession(workdir?: string): Promise<SessionData> {
|
|
if (!this.projectManager.isInitialized()) {
|
|
throw new Error('Project not initialized. Call init() first.');
|
|
}
|
|
|
|
const newWorkdir = workdir || this.currentSession?.workdir || process.cwd();
|
|
|
|
// 如果工作目录变化,需要切换项目
|
|
if (workdir && workdir !== this.projectManager.getProject()?.workdir) {
|
|
await this.projectManager.switchProject(workdir);
|
|
}
|
|
|
|
const projectId = this.projectManager.getProjectId()!;
|
|
this.currentSession = await this.store.create(projectId, newWorkdir);
|
|
await this.store.save(this.currentSession);
|
|
await this.setCurrentSessionPointer(this.currentSession.id);
|
|
|
|
return this.currentSession;
|
|
}
|
|
|
|
/**
|
|
* 恢复指定会话
|
|
*/
|
|
async restoreSession(sessionId: string): Promise<SessionData | null> {
|
|
if (!this.projectManager.isInitialized()) {
|
|
throw new Error('Project not initialized. Call init() first.');
|
|
}
|
|
|
|
const projectId = this.projectManager.getProjectId()!;
|
|
const session = await this.store.load(projectId, sessionId);
|
|
if (!session) return null;
|
|
|
|
this.currentSession = session;
|
|
await this.setCurrentSessionPointer(sessionId);
|
|
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* 列出当前项目的历史会话
|
|
*/
|
|
async listSessions(): Promise<SessionSummary[]> {
|
|
const projectId = this.projectManager.getProjectId();
|
|
if (!projectId) {
|
|
return this.listAllSessions();
|
|
}
|
|
return this.store.listByProject(projectId);
|
|
}
|
|
|
|
/**
|
|
* 列出所有项目的会话
|
|
*/
|
|
async listAllSessions(): Promise<SessionSummary[]> {
|
|
return this.store.listAll();
|
|
}
|
|
|
|
/**
|
|
* 删除历史会话
|
|
*/
|
|
async deleteSession(sessionId: string): Promise<boolean> {
|
|
const projectId = this.projectManager.getProjectId();
|
|
if (!projectId) return false;
|
|
return this.store.delete(projectId, sessionId);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 消息操作
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 批量设置消息(用于同步整个对话历史)
|
|
*/
|
|
async setMessages(messages: ModelMessage[]): Promise<void> {
|
|
if (!this.currentSession) return;
|
|
this.currentSession.messages = messages;
|
|
await this.store.syncMessages(this.currentSession.id, messages);
|
|
await this.store.save(this.currentSession);
|
|
}
|
|
|
|
/**
|
|
* 添加消息
|
|
*/
|
|
async addMessage(message: ModelMessage): Promise<void> {
|
|
if (!this.currentSession) return;
|
|
this.currentSession.messages.push(message);
|
|
await this.setMessages(this.currentSession.messages);
|
|
}
|
|
|
|
/**
|
|
* 获取对话历史
|
|
*/
|
|
getMessages(): ModelMessage[] {
|
|
return this.currentSession?.messages || [];
|
|
}
|
|
|
|
// ============================================================================
|
|
// 工具和待办操作
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 设置已发现的工具
|
|
*/
|
|
async setDiscoveredTools(tools: string[]): Promise<void> {
|
|
if (!this.currentSession) return;
|
|
this.currentSession.discoveredTools = tools;
|
|
await this.store.save(this.currentSession);
|
|
}
|
|
|
|
/**
|
|
* 获取已发现的工具
|
|
*/
|
|
getDiscoveredTools(): string[] {
|
|
return this.currentSession?.discoveredTools || [];
|
|
}
|
|
|
|
/**
|
|
* 更新待办事项
|
|
*/
|
|
async setTodos(
|
|
todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>
|
|
): Promise<void> {
|
|
if (!this.currentSession) return;
|
|
const items = await this.store.setTodos(this.currentSession.id, todos);
|
|
this.currentSession.todos = items;
|
|
}
|
|
|
|
/**
|
|
* 获取待办事项
|
|
*/
|
|
getTodos(): TodoItem[] {
|
|
return this.currentSession?.todos || [];
|
|
}
|
|
|
|
// ============================================================================
|
|
// 子会话操作
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 创建子会话(用于 Task 工具)
|
|
*/
|
|
createChildSession(parentId: string, agentName: string, title?: string): SessionData {
|
|
if (!this.projectManager.isInitialized()) {
|
|
throw new Error('Project not initialized. Call init() first.');
|
|
}
|
|
|
|
const projectId = this.projectManager.getProjectId()!;
|
|
const workdir = this.currentSession?.workdir || process.cwd();
|
|
return this.store.createChildSession(projectId, parentId, agentName, workdir, title);
|
|
}
|
|
|
|
/**
|
|
* 保存子会话
|
|
*/
|
|
async saveChildSession(session: SessionData): Promise<void> {
|
|
await this.store.saveChildSession(session);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 自动保存
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 启动自动保存
|
|
*/
|
|
private startAutoSave(): void {
|
|
this.autoSave.start(() => this.save());
|
|
}
|
|
|
|
/**
|
|
* 停止自动保存
|
|
*/
|
|
stopAutoSave(): void {
|
|
this.autoSave.stop();
|
|
}
|
|
|
|
/**
|
|
* 关闭管理器(保存并停止自动保存)
|
|
*/
|
|
async close(): Promise<void> {
|
|
this.stopAutoSave();
|
|
await this.save();
|
|
}
|
|
|
|
// ============================================================================
|
|
// 清理
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 清理旧会话
|
|
*/
|
|
async cleanup(keepCount: number = 50): Promise<number> {
|
|
const sessions = await this.listAllSessions();
|
|
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;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 辅助方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 获取当前会话 ID(从存储)
|
|
*/
|
|
private async getCurrentSessionId(): Promise<string | null> {
|
|
try {
|
|
const pointer = await storage.read<{ sessionId: string }>(['current-session']);
|
|
return pointer.sessionId;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置当前会话指针
|
|
*/
|
|
private async setCurrentSessionPointer(sessionId: string): Promise<void> {
|
|
await storage.write(['current-session'], { sessionId });
|
|
}
|
|
|
|
/**
|
|
* 获取存储目录
|
|
*/
|
|
getStorageDir(): string {
|
|
return this.storageDir || storage.getDefaultStorageDir();
|
|
}
|
|
}
|
|
|
|
// 导出默认实例
|
|
export const sessionManager = new SessionManager();
|