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:
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储目录
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user