refactor(core): 拆分大型单体文件为模块化子组件

将三个超过 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
This commit is contained in:
2025-12-16 22:07:13 +08:00
parent e53035ffc0
commit 66ad1a1ec9
17 changed files with 3154 additions and 1959 deletions
+161 -573
View File
@@ -1,48 +1,19 @@
/**
* 会话管理器
* 作为编排器,委托具体工作给各个子模块
*/
import type { ModelMessage } from 'ai';
import * as storage from './storage/index.js';
import { SessionStorage, MessageStorage, PartStorage, TodoStorage } from './storage/index.js';
import type { SessionInfo, Part, TodoItem } from './storage/index.js';
import { generateSessionId } from './id.js';
import { getProjectId, isGitRepository } from './project.js';
import type { TodoItem } from './storage/index.js';
/**
* 会话摘要(用于列表展示)
*/
export interface SessionSummary {
id: string;
title: string;
workdir: string;
messageCount: number;
createdAt: string;
updatedAt: string;
}
// 子模块
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 interface SessionData {
id: string;
projectId: string;
parentId?: string;
agentName?: string;
createdAt: string;
updatedAt: string;
workdir: string;
title?: string;
messages: ModelMessage[];
discoveredTools: string[];
todos: TodoItem[];
}
/**
* 项目元数据
*/
export interface ProjectMetadata {
id: string;
workdir: string;
createdAt: string;
isGitRepo: boolean;
}
// 重新导出类型
export type { SessionData, SessionSummary, ProjectMetadata };
/**
* 会话管理器
@@ -50,14 +21,24 @@ export interface ProjectMetadata {
*/
export class SessionManager {
private currentSession: SessionData | null = null;
private currentProject: ProjectMetadata | null = null;
private autoSaveInterval: ReturnType<typeof setInterval> | 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();
}
// ============================================================================
// 初始化
// ============================================================================
/**
* 初始化 - 尝试恢复或创建新会话
*/
@@ -66,13 +47,14 @@ export class SessionManager {
await storage.initStorage(this.storageDir);
// 获取或创建项目
this.currentProject = await this.getOrCreateProject(workdir);
await this.projectManager.getOrCreate(workdir);
// 尝试加载当前会话
const currentSessionId = await this.getCurrentSessionId();
if (currentSessionId) {
const existing = await this.loadSession(this.currentProject.id, currentSessionId);
const projectId = this.projectManager.getProjectId()!;
const existing = await this.store.load(projectId, currentSessionId);
if (existing && existing.workdir === workdir) {
this.currentSession = existing;
@@ -82,218 +64,18 @@ export class SessionManager {
}
// 创建新会话
this.currentSession = await this.createNewSession(workdir);
await this.saveSessionInfo();
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;
}
/**
* 获取或创建项目
*/
private async getOrCreateProject(workdir: string): Promise<ProjectMetadata> {
const projectId = await getProjectId(workdir);
try {
const existing = await storage.read<ProjectMetadata>(['project', projectId]);
return existing;
} catch (e) {
if (e instanceof storage.StorageNotFoundError) {
const isGitRepo = await isGitRepository(workdir);
const project: ProjectMetadata = {
id: projectId,
workdir,
createdAt: new Date().toISOString(),
isGitRepo,
};
await storage.write(['project', projectId], project);
return project;
}
throw e;
}
}
/**
* 创建新会话
*/
private async createNewSession(workdir: string): Promise<SessionData> {
if (!this.currentProject) {
throw new Error('Project not initialized. Call init() first.');
}
const sessionInfo = await SessionStorage.create(this.currentProject.id, workdir);
return {
id: sessionInfo.id,
projectId: sessionInfo.projectId,
createdAt: new Date(sessionInfo.createdAt).toISOString(),
updatedAt: new Date(sessionInfo.updatedAt).toISOString(),
workdir: sessionInfo.workdir,
title: sessionInfo.title,
messages: [],
discoveredTools: sessionInfo.discoveredTools,
todos: [],
};
}
/**
* 加载会话(从存储重建)
*/
private async loadSession(projectId: string, sessionId: string): Promise<SessionData | null> {
const sessionInfo = await SessionStorage.get(projectId, sessionId);
if (!sessionInfo) return null;
// 加载消息
const messages = await this.loadMessagesFromStorage(sessionId);
// 加载 todos
const todoList = await TodoStorage.get(sessionId);
return {
id: sessionInfo.id,
projectId: sessionInfo.projectId,
parentId: sessionInfo.parentId,
agentName: sessionInfo.agentName,
createdAt: new Date(sessionInfo.createdAt).toISOString(),
updatedAt: new Date(sessionInfo.updatedAt).toISOString(),
workdir: sessionInfo.workdir,
title: sessionInfo.title,
messages,
discoveredTools: sessionInfo.discoveredTools,
todos: todoList?.items || [],
};
}
/**
* 从存储加载消息并转换为 AI SDK 格式
*/
private async loadMessagesFromStorage(sessionId: string): Promise<ModelMessage[]> {
const messageInfos = await MessageStorage.listBySession(sessionId);
const messages: ModelMessage[] = [];
for (const messageInfo of messageInfos) {
const parts = await PartStorage.getByIds(messageInfo.id, messageInfo.partIds);
const modelMessages = this.partsToModelMessages(messageInfo.role, parts);
messages.push(...modelMessages);
}
return messages;
}
/**
* 将 Parts 转换为 AI SDK ModelMessage(用于加载历史消息)
*
* 新逻辑:
* - user 消息:直接转换
* - assistant 消息:转换文本和工具调用,然后为已完成的工具生成 tool 消息
*/
private partsToModelMessages(role: string, parts: Part[]): ModelMessage[] {
if (parts.length === 0) return [];
const result: ModelMessage[] = [];
if (role === 'user') {
// User 消息:只有文本和文件
const content: unknown[] = [];
for (const part of parts) {
if (part.type === 'text') {
content.push({ type: 'text', text: part.text });
} else if (part.type === 'file') {
content.push({
type: 'image',
image: part.data,
mimeType: part.mimeType,
});
}
}
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
result.push({
role: 'user',
content: (content[0] as { text: string }).text,
});
} else if (content.length > 0) {
result.push({
role: 'user',
content,
} as ModelMessage);
}
} else if (role === 'assistant') {
// Assistant 消息:文本 + 工具调用
const content: unknown[] = [];
// input 使用 unknown 类型以兼容 AI SDK(可能是对象、字符串等)
const completedTools: Array<{ toolCallId: string; toolName: string; input: unknown; output: unknown }> = [];
for (const part of parts) {
if (part.type === 'text') {
content.push({ type: 'text', text: part.text });
} else if (part.type === 'tool') {
// 只有非 pending 状态的工具调用才添加到 AI SDK 消息
if (part.state.status !== 'pending') {
// AI SDK v5 使用 input 字段(不是 args
content.push({
type: 'tool-call',
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
});
// 收集已完成的工具结果
if (part.state.status === 'completed') {
completedTools.push({
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
output: part.state.output,
});
} else if (part.state.status === 'error') {
completedTools.push({
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
output: part.state.error,
});
}
}
} else if (part.type === 'reasoning') {
content.push({ type: 'text', text: `[Reasoning] ${part.text}` });
}
}
// 添加 assistant 消息
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
result.push({
role: 'assistant',
content: (content[0] as { text: string }).text,
});
} else if (content.length > 0) {
result.push({
role: 'assistant',
content,
} as ModelMessage);
}
// 添加 tool 消息(如果有已完成的工具)
// AI SDK v5 要求 tool-result 必须包含 input 和 output 字段
if (completedTools.length > 0) {
result.push({
role: 'tool',
content: completedTools.map((t) => ({
type: 'tool-result',
toolCallId: t.toolCallId,
toolName: t.toolName,
input: t.input,
output: t.output,
})),
} as unknown as ModelMessage);
}
}
return result;
}
// ============================================================================
// 会话获取
// ============================================================================
/**
* 获取当前会话
@@ -306,189 +88,108 @@ export class SessionManager {
* 获取当前项目
*/
getProject(): ProjectMetadata | null {
return this.currentProject;
return this.projectManager.getProject();
}
/**
* 保存会话信息
* 获取当前会话 ID
*/
private async saveSessionInfo(): Promise<void> {
if (!this.currentSession) return;
const sessionInfo: SessionInfo = {
id: this.currentSession.id,
projectId: this.currentSession.projectId,
parentId: this.currentSession.parentId,
agentName: this.currentSession.agentName,
createdAt: new Date(this.currentSession.createdAt).getTime(),
updatedAt: Date.now(),
workdir: this.currentSession.workdir,
title: this.currentSession.title,
discoveredTools: this.currentSession.discoveredTools,
stats: {
messageCount: this.currentSession.messages.length,
inputTokens: 0,
outputTokens: 0,
},
};
await SessionStorage.save(sessionInfo);
getSessionId(): string | undefined {
return this.currentSession?.id;
}
// ============================================================================
// 会话操作
// ============================================================================
/**
* 保存当前会话
*/
async save(): Promise<void> {
if (!this.currentSession) return;
await this.saveSessionInfo();
await this.store.save(this.currentSession);
}
/**
* 同步消息到存储(将 AI SDK 消息转换为 Message + Parts
*
* 新逻辑:只存储 user 和 assistant 消息
* - user 消息:直接存储
* - assistant 消息:合并后续的 tool 消息中的工具结果
* - tool 消息:跳过(结果合并到 assistant)
* 清空当前会话并创建新会话
*/
async syncMessages(messages: ModelMessage[]): Promise<void> {
if (!this.currentSession) return;
const sessionId = this.currentSession.id;
// 删除旧消息
await MessageStorage.removeBySession(sessionId);
// 用于跟踪当前 assistant 消息的工具调用
let currentAssistantMsgId: string | null = null;
let currentUserMsgId: string | null = null;
const toolCallPartIds = new Map<string, string>(); // toolCallId -> partId
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
if (message.role === 'user') {
// User 消息
const messageInfo = await MessageStorage.create(sessionId, 'user');
currentUserMsgId = messageInfo.id;
const partIds: string[] = [];
if (typeof message.content === 'string') {
const part = await PartStorage.createText(messageInfo.id, message.content);
partIds.push(part.id);
} else if (Array.isArray(message.content)) {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'text') {
const part = await PartStorage.createText(messageInfo.id, (item as { text: string }).text);
partIds.push(part.id);
} else if (itemType === 'image') {
const img = item as unknown as { image: string; mimeType: string };
const part = await PartStorage.create(messageInfo.id, 'file', {
filename: 'image',
mimeType: img.mimeType,
data: typeof img.image === 'string' ? img.image : '',
});
partIds.push(part.id);
}
}
}
if (partIds.length > 0) {
await MessageStorage.update(sessionId, messageInfo.id, { partIds });
}
// 重置工具调用追踪
currentAssistantMsgId = null;
toolCallPartIds.clear();
} else if (message.role === 'assistant') {
// Assistant 消息:如果当前轮次已有 assistant 消息,则追加 Parts
let messageId: string;
let existingPartIds: string[] = [];
if (currentAssistantMsgId) {
// 同一轮对话的后续 assistant 消息,追加到现有消息
messageId = currentAssistantMsgId;
const existingMsg = await MessageStorage.get(sessionId, messageId);
existingPartIds = existingMsg?.partIds ?? [];
} else {
// 新的 assistant 消息
const messageInfo = await MessageStorage.create(sessionId, 'assistant', {
parentId: currentUserMsgId ?? undefined,
});
messageId = messageInfo.id;
currentAssistantMsgId = messageId;
}
const newPartIds: string[] = [];
if (typeof message.content === 'string') {
const part = await PartStorage.createText(messageId, message.content);
newPartIds.push(part.id);
} else if (Array.isArray(message.content)) {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'text') {
const part = await PartStorage.createText(messageId, (item as { text: string }).text);
newPartIds.push(part.id);
} else if (itemType === 'tool-call') {
// AI SDK 的 tool-call 使用 input 字段存储参数(不是 args)
const toolCall = item as unknown as { toolCallId: string; toolName: string; input: Record<string, unknown> };
// 创建 running 状态的工具 Part
const part = await PartStorage.createToolRunning(
messageId,
toolCall.toolCallId,
toolCall.toolName,
(toolCall.input as Record<string, unknown>) ?? {}
);
newPartIds.push(part.id);
toolCallPartIds.set(toolCall.toolCallId, part.id);
}
}
}
if (newPartIds.length > 0) {
// 合并已有的和新的 partIds
const allPartIds = [...existingPartIds, ...newPartIds];
await MessageStorage.update(sessionId, messageId, { partIds: allPartIds });
}
} else if (message.role === 'tool' && currentAssistantMsgId) {
// Tool 消息:更新对应 assistant 消息中的工具 Part 状态
if (Array.isArray(message.content)) {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'tool-result') {
// AI SDK v5 使用 output 字段存储结果(不是 result)
const toolResult = item as unknown as { toolCallId: string; toolName: string; output: unknown };
const partId = toolCallPartIds.get(toolResult.toolCallId);
if (partId) {
// 更新工具状态为 completed
// 获取原始 start time
const part = await PartStorage.get(currentAssistantMsgId, partId);
const startTime = part?.type === 'tool' && part.state.status === 'running'
? part.state.time.start
: Date.now();
await PartStorage.setToolCompleted(currentAssistantMsgId, partId, toolResult.output, startTime);
}
}
}
}
// 不创建新消息,跳过 tool role
}
// 忽略 system 消息(system prompt 通过其他方式注入)
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.syncMessages(messages);
await this.saveSessionInfo();
await this.store.syncMessages(this.currentSession.id, messages);
await this.store.save(this.currentSession);
}
/**
@@ -507,13 +208,17 @@ export class SessionManager {
return this.currentSession?.messages || [];
}
// ============================================================================
// 工具和待办操作
// ============================================================================
/**
* 设置已发现的工具
*/
async setDiscoveredTools(tools: string[]): Promise<void> {
if (!this.currentSession) return;
this.currentSession.discoveredTools = tools;
await this.saveSessionInfo();
await this.store.save(this.currentSession);
}
/**
@@ -526,10 +231,12 @@ export class SessionManager {
/**
* 更新待办事项
*/
async setTodos(todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>): Promise<void> {
async setTodos(
todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>
): Promise<void> {
if (!this.currentSession) return;
const todoList = await TodoStorage.replace(this.currentSession.id, todos);
this.currentSession.todos = todoList.items;
const items = await this.store.setTodos(this.currentSession.id, todos);
this.currentSession.todos = items;
}
/**
@@ -539,192 +246,46 @@ export class SessionManager {
return this.currentSession?.todos || [];
}
/**
* 清空当前会话并创建新会话
*/
async newSession(workdir?: string): Promise<SessionData> {
if (!this.currentProject) {
throw new Error('Project not initialized. Call init() first.');
}
const newWorkdir = workdir || this.currentSession?.workdir || process.cwd();
// 如果工作目录变化,需要切换项目
if (workdir && workdir !== this.currentProject.workdir) {
this.currentProject = await this.getOrCreateProject(workdir);
}
this.currentSession = await this.createNewSession(newWorkdir);
await this.saveSessionInfo();
await this.setCurrentSessionPointer(this.currentSession.id);
return this.currentSession;
}
// ============================================================================
// 子会话操作
// ============================================================================
/**
* 创建子会话(用于 Task 工具)
*/
createChildSession(parentId: string, agentName: string, title?: string): SessionData {
if (!this.currentProject) {
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 {
id: generateSessionId(),
projectId: this.currentProject.id,
parentId,
agentName,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
workdir,
title: title || `子任务 (@${agentName})`,
messages: [],
discoveredTools: [],
todos: [],
};
return this.store.createChildSession(projectId, parentId, agentName, workdir, title);
}
/**
* 保存子会话
*/
async saveChildSession(session: SessionData): Promise<void> {
const sessionInfo: SessionInfo = {
id: session.id,
projectId: session.projectId,
parentId: session.parentId,
agentName: session.agentName,
createdAt: new Date(session.createdAt).getTime(),
updatedAt: Date.now(),
workdir: session.workdir,
title: session.title,
discoveredTools: session.discoveredTools,
};
await SessionStorage.save(sessionInfo);
await this.store.saveChildSession(session);
}
/**
* 获取当前会话 ID
*/
getSessionId(): string | undefined {
return this.currentSession?.id;
}
// ============================================================================
// 自动保存
// ============================================================================
/**
* 恢复指定会话
*/
async restoreSession(sessionId: string): Promise<SessionData | null> {
if (!this.currentProject) {
throw new Error('Project not initialized. Call init() first.');
}
const session = await this.loadSession(this.currentProject.id, sessionId);
if (!session) return null;
this.currentSession = session;
await this.setCurrentSessionPointer(sessionId);
return session;
}
/**
* 列出当前项目的历史会话
*/
async listSessions(): Promise<SessionSummary[]> {
if (!this.currentProject) {
return this.listAllSessions();
}
const sessions = await SessionStorage.listByProject(this.currentProject.id);
return sessions.map((s) => ({
id: s.id,
title: s.title || `会话 ${s.id}`,
workdir: s.workdir,
messageCount: s.stats?.messageCount || 0,
createdAt: new Date(s.createdAt).toISOString(),
updatedAt: new Date(s.updatedAt).toISOString(),
}));
}
/**
* 列出所有项目的会话
*/
async listAllSessions(): Promise<SessionSummary[]> {
const sessions = await SessionStorage.listAll();
return sessions.map((s) => ({
id: s.id,
title: s.title || `会话 ${s.id}`,
workdir: s.workdir,
messageCount: s.stats?.messageCount || 0,
createdAt: new Date(s.createdAt).toISOString(),
updatedAt: new Date(s.updatedAt).toISOString(),
}));
}
/**
* 删除历史会话
*/
async deleteSession(sessionId: string): Promise<boolean> {
if (!this.currentProject) return false;
try {
// 删除会话的消息和 Parts
const messageInfos = await MessageStorage.listBySession(sessionId);
for (const msg of messageInfos) {
await PartStorage.removeByMessage(msg.id);
}
await MessageStorage.removeBySession(sessionId);
// 删除 todos
await TodoStorage.removeBySession(sessionId);
// 删除会话信息
await SessionStorage.remove(this.currentProject.id, sessionId);
return true;
} catch {
return false;
}
}
/**
* 获取当前会话 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 });
}
/**
* 启动自动保存(每 30 秒)
* 启动自动保存
*/
private startAutoSave(): void {
if (this.autoSaveInterval) return;
this.autoSaveInterval = setInterval(async () => {
await this.save();
}, 30000);
this.autoSave.start(() => this.save());
}
/**
* 停止自动保存
*/
stopAutoSave(): void {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null;
}
this.autoSave.stop();
}
/**
@@ -735,6 +296,10 @@ export class SessionManager {
await this.save();
}
// ============================================================================
// 清理
// ============================================================================
/**
* 清理旧会话
*/
@@ -756,6 +321,29 @@ export class SessionManager {
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 });
}
/**
* 获取存储目录
*/