diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e1e8f5b..1718cc2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,9 +14,30 @@ export type { CompressionConfig, DetailedCompressionResult, } from './context/index.js'; -export { SessionStorage } from './session/storage.js'; +// Session - 新的三层存储结构 export { SessionManager } from './session/index.js'; -export type { SessionData, SessionSummary } from './session/types.js'; +export type { SessionData, SessionSummary, ProjectMetadata } from './session/index.js'; + +// Session Storage API +export { + SessionStorage, + MessageStorage, + PartStorage, + TodoStorage, + initStorage, + getStorageDir, + StorageNotFoundError, +} from './session/index.js'; + +export type { + SessionInfo, + MessageInfo, + Part, + PartType, + ToolStatus, + TodoItem, + TodoList, +} from './session/index.js'; // Types export type { UserInput, ChatResult } from './types/index.js'; diff --git a/packages/core/src/session/id.ts b/packages/core/src/session/id.ts new file mode 100644 index 0000000..7d052be --- /dev/null +++ b/packages/core/src/session/id.ts @@ -0,0 +1,95 @@ +/** + * ID 生成器模块 + * 提供各种实体的 ID 生成功能 + */ + +/** + * 生成会话 ID + * 格式: {date}_{random} (如 2025-12-15_abc123) + */ +export function generateSessionId(): string { + const now = new Date(); + const date = now.toISOString().slice(0, 10); // YYYY-MM-DD + const random = Math.random().toString(36).substring(2, 8); + return `${date}_${random}`; +} + +/** + * 生成消息 ID(降序,最新的在前) + * 格式: msg_{invertedTimestamp}_{random} + */ +export function generateMessageId(): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 8); + // 使用最大时间戳减去当前时间戳,实现降序 + const invertedTimestamp = 9999999999999 - timestamp; + return `msg_${invertedTimestamp}_${random}`; +} + +/** + * 生成 Part ID(升序,按创建顺序) + * 格式: part_{timestamp}_{random} + */ +export function generatePartId(): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 8); + return `part_${timestamp}_${random}`; +} + +/** + * 生成 Todo ID + * 格式: todo_{timestamp}_{random} + */ +export function generateTodoId(): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 8); + return `todo_${timestamp}_${random}`; +} + +/** + * 从消息 ID 提取时间戳 + */ +export function getTimestampFromMessageId(messageId: string): number { + const match = messageId.match(/^msg_(\d+)_/); + if (!match) return 0; + const invertedTimestamp = parseInt(match[1], 10); + return 9999999999999 - invertedTimestamp; +} + +/** + * 从 Part ID 提取时间戳 + */ +export function getTimestampFromPartId(partId: string): number { + const match = partId.match(/^part_(\d+)_/); + if (!match) return 0; + return parseInt(match[1], 10); +} + +/** + * 从会话 ID 提取日期 + */ +export function getDateFromSessionId(sessionId: string): string | null { + const match = sessionId.match(/^(\d{4}-\d{2}-\d{2})_/); + return match ? match[1] : null; +} + +/** + * 验证会话 ID 格式 + */ +export function isValidSessionId(id: string): boolean { + return /^\d{4}-\d{2}-\d{2}_[a-z0-9]+$/.test(id); +} + +/** + * 验证消息 ID 格式 + */ +export function isValidMessageId(id: string): boolean { + return /^msg_\d+_[a-z0-9]+$/.test(id); +} + +/** + * 验证 Part ID 格式 + */ +export function isValidPartId(id: string): boolean { + return /^part_\d+_[a-z0-9]+$/.test(id); +} diff --git a/packages/core/src/session/index.ts b/packages/core/src/session/index.ts index 472e702..f6c1cd7 100644 --- a/packages/core/src/session/index.ts +++ b/packages/core/src/session/index.ts @@ -1,16 +1,52 @@ -export type { - SessionData, - SessionMetadata, - SessionSummary, - SessionManagerConfig, - Todo, - TodoStatus, - StoredMessage, - CurrentSessionPointer, - ProjectMetadata, -} from './types.js'; +// 新的存储 API +export * from './storage/index.js'; -export { SessionStorage, sessionStorage } from './storage.js'; +// 类型定义 +export type { MessageInfo, Message, MessageRole } from './message.js'; +export { + MessageInfoSchema, + MessageRoleSchema, + generateMessageId, + getTimestampFromMessageId, + createMessageInfo, +} from './message.js'; + +export type { Part, PartType, ToolStatus } from './parts.js'; +export { + PartSchema, + TextPartSchema, + ToolPartSchema, + ReasoningPartSchema, + FilePartSchema, + StepStartPartSchema, + StepFinishPartSchema, + SnapshotPartSchema, + PatchPartSchema, + AgentPartSchema, + SubtaskPartSchema, + CompactionPartSchema, + RetryPartSchema, + ToolStatusSchema, + createPart, +} from './parts.js'; + +// ID 生成器 +export { + generateSessionId, + generateMessageId as generateMsgId, + generatePartId, + generateTodoId, + getTimestampFromMessageId as getMsgTimestamp, + getTimestampFromPartId, + getDateFromSessionId, + isValidSessionId, + isValidMessageId, + isValidPartId, +} from './id.js'; + +// 会话管理器 export { SessionManager, sessionManager } from './manager.js'; +export type { SessionData, SessionSummary, ProjectMetadata } from './manager.js'; + +// 项目工具 export { getProjectId, isGitRepository } from './project.js'; -export { runMigrations, getMigrationStatus } from './migration.js'; diff --git a/packages/core/src/session/manager.ts b/packages/core/src/session/manager.ts index c75442c..98707dc 100644 --- a/packages/core/src/session/manager.ts +++ b/packages/core/src/session/manager.ts @@ -1,76 +1,252 @@ import type { ModelMessage } from 'ai'; -import type { SessionData, Todo, SessionSummary, ProjectMetadata } from './types.js'; -import { SessionStorage, sessionStorage } from './storage.js'; +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'; + +/** + * 会话摘要(用于列表展示) + */ +export interface SessionSummary { + id: string; + title: string; + workdir: string; + messageCount: number; + createdAt: string; + updatedAt: string; +} + +/** + * 运行时会话数据(兼容旧接口) + */ +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 class SessionManager { - private storage: SessionStorage; private currentSession: SessionData | null = null; private currentProject: ProjectMetadata | null = null; private autoSaveInterval: ReturnType | null = null; - private lastSyncedCount: number = 0; + private storageDir?: string; - constructor(storage?: SessionStorage) { - this.storage = storage || sessionStorage; + constructor(storageDir?: string) { + this.storageDir = storageDir; } /** * 初始化 - 尝试恢复或创建新会话 */ async init(workdir: string): Promise { + // 初始化存储 + await storage.initStorage(this.storageDir); + // 获取或创建项目 - this.currentProject = await this.storage.getOrCreateProject(workdir); + this.currentProject = await this.getOrCreateProject(workdir); // 尝试加载当前会话 - const currentSessionId = await this.storage.getCurrentSessionId(); + const currentSessionId = await this.getCurrentSessionId(); if (currentSessionId) { - // 尝试加载会话 - const existing = await this.storage.loadSession(this.currentProject.id, currentSessionId); + const existing = await this.loadSession(this.currentProject.id, currentSessionId); if (existing && existing.workdir === workdir) { - // 同一工作目录,恢复会话 this.currentSession = existing; - this.lastSyncedCount = existing.messages.length; this.startAutoSave(); return this.currentSession; } } // 创建新会话 - this.currentSession = this.createNewSession(workdir); - this.lastSyncedCount = 0; - await this.save(); + this.currentSession = await this.createNewSession(workdir); + await this.saveSessionInfo(); + await this.setCurrentSessionPointer(this.currentSession.id); - // 启动自动保存 this.startAutoSave(); - return this.currentSession; } + /** + * 获取或创建项目 + */ + private async getOrCreateProject(workdir: string): Promise { + const projectId = await getProjectId(workdir); + + try { + const existing = await storage.read(['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 createNewSession(workdir: string): SessionData { + private async createNewSession(workdir: string): Promise { if (!this.currentProject) { throw new Error('Project not initialized. Call init() first.'); } + const sessionInfo = await SessionStorage.create(this.currentProject.id, workdir); + return { - id: this.storage.generateSessionId(), - projectId: this.currentProject.id, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - workdir, + 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: [], + discoveredTools: sessionInfo.discoveredTools, todos: [], }; } + /** + * 加载会话(从存储重建) + */ + private async loadSession(projectId: string, sessionId: string): Promise { + 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 { + const messageInfos = await MessageStorage.listBySession(sessionId); + const messages: ModelMessage[] = []; + + for (const messageInfo of messageInfos) { + const parts = await PartStorage.getByIds(messageInfo.id, messageInfo.partIds); + const modelMessage = this.partsToModelMessage(messageInfo.role, parts); + if (modelMessage) { + messages.push(modelMessage); + } + } + + return messages; + } + + /** + * 将 Parts 转换为 AI SDK ModelMessage + */ + private partsToModelMessage(role: string, parts: Part[]): ModelMessage | null { + if (parts.length === 0) return null; + + // 构建消息内容 + const content: unknown[] = []; + + for (const part of parts) { + switch (part.type) { + case 'text': + content.push({ type: 'text', text: part.text }); + break; + case 'tool': + if (role === 'assistant') { + content.push({ + type: 'tool-call', + toolCallId: part.toolCallId, + toolName: part.toolName, + args: part.args, + }); + } else if (role === 'tool') { + // Tool result message - AI SDK 的 tool message 格式 + return { + role: 'tool', + content: [{ + type: 'tool-result', + toolCallId: part.toolCallId, + toolName: part.toolName, + result: part.result, + }], + } as unknown as ModelMessage; + } + break; + case 'file': + content.push({ + type: 'image', + image: part.data, + mimeType: part.mimeType, + }); + break; + case 'reasoning': + // Reasoning 通常作为文本的一部分 + content.push({ type: 'text', text: `[Reasoning] ${part.text}` }); + break; + } + } + + // 简化:如果只有一个文本内容,直接使用字符串 + if (content.length === 1 && (content[0] as { type: string }).type === 'text') { + return { + role: role as 'user' | 'assistant' | 'system', + content: (content[0] as { text: string }).text, + } as ModelMessage; + } + + return { + role: role as 'user' | 'assistant' | 'system' | 'tool', + content, + } as ModelMessage; + } + /** * 获取当前会话 */ @@ -86,28 +262,104 @@ export class SessionManager { } /** - * 保存当前会话(增量保存消息) + * 保存会话信息 */ - async save(): Promise { + private async saveSessionInfo(): Promise { if (!this.currentSession) return; - // 增量保存消息 - await this.storage.saveSession(this.currentSession, this.lastSyncedCount); + 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 this.storage.setCurrentSession(this.currentSession.id); - - // 更新同步计数 - this.lastSyncedCount = this.currentSession.messages.length; + await SessionStorage.save(sessionInfo); } /** - * 添加消息 + * 保存当前会话 */ - async addMessage(message: ModelMessage): Promise { + async save(): Promise { if (!this.currentSession) return; - this.currentSession.messages.push(message); - await this.save(); + await this.saveSessionInfo(); + } + + /** + * 同步消息到存储(将 AI SDK 消息转换为 Message + Parts) + */ + async syncMessages(messages: ModelMessage[]): Promise { + if (!this.currentSession) return; + + const sessionId = this.currentSession.id; + + // 删除旧消息 + await MessageStorage.removeBySession(sessionId); + + // 保存新消息 + for (const message of messages) { + const messageInfo = await MessageStorage.create(sessionId, message.role as 'user' | 'assistant' | 'system'); + + // 将消息内容转换为 Parts + 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)) { + // 复杂内容(多个 parts) + 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 === 'tool-call') { + const toolCall = item as unknown as { toolCallId: string; toolName: string; args: Record }; + const part = await PartStorage.createTool( + messageInfo.id, + toolCall.toolCallId, + toolCall.toolName, + toolCall.args + ); + partIds.push(part.id); + } else if (itemType === 'tool-result') { + const toolResult = item as unknown as { toolCallId: string; toolName: string; result: unknown }; + const part = await PartStorage.create(messageInfo.id, 'tool', { + toolCallId: toolResult.toolCallId, + toolName: toolResult.toolName, + args: {}, + status: 'completed', + result: toolResult.result, + }); + 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); + } + } + } + + // 更新消息的 partIds + if (partIds.length > 0) { + await MessageStorage.update(sessionId, messageInfo.id, { partIds }); + } + } } /** @@ -116,7 +368,17 @@ export class SessionManager { async setMessages(messages: ModelMessage[]): Promise { if (!this.currentSession) return; this.currentSession.messages = messages; - await this.save(); + await this.syncMessages(messages); + await this.saveSessionInfo(); + } + + /** + * 添加消息 + */ + async addMessage(message: ModelMessage): Promise { + if (!this.currentSession) return; + this.currentSession.messages.push(message); + await this.setMessages(this.currentSession.messages); } /** @@ -132,7 +394,7 @@ export class SessionManager { async setDiscoveredTools(tools: string[]): Promise { if (!this.currentSession) return; this.currentSession.discoveredTools = tools; - await this.save(); + await this.saveSessionInfo(); } /** @@ -145,16 +407,16 @@ export class SessionManager { /** * 更新待办事项 */ - async setTodos(todos: Todo[]): Promise { + async setTodos(todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>): Promise { if (!this.currentSession) return; - this.currentSession.todos = todos; - await this.save(); + const todoList = await TodoStorage.replace(this.currentSession.id, todos); + this.currentSession.todos = todoList.items; } /** * 获取待办事项 */ - getTodos(): Todo[] { + getTodos(): TodoItem[] { return this.currentSession?.todos || []; } @@ -166,26 +428,22 @@ export class SessionManager { 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.storage.getOrCreateProject(workdir); + this.currentProject = await this.getOrCreateProject(workdir); } - this.currentSession = this.createNewSession(newWorkdir); - this.lastSyncedCount = 0; - await this.save(); + this.currentSession = await this.createNewSession(newWorkdir); + await this.saveSessionInfo(); + await this.setCurrentSessionPointer(this.currentSession.id); return this.currentSession; } /** * 创建子会话(用于 Task 工具) - * @param parentId 父会话 ID - * @param agentName 关联的 Agent 名称 - * @param title 会话标题 */ createChildSession(parentId: string, agentName: string, title?: string): SessionData { if (!this.currentProject) { @@ -193,8 +451,8 @@ export class SessionManager { } const workdir = this.currentSession?.workdir || process.cwd(); - const childSession: SessionData = { - id: this.storage.generateSessionId(), + return { + id: generateSessionId(), projectId: this.currentProject.id, parentId, agentName, @@ -206,14 +464,24 @@ export class SessionManager { discoveredTools: [], todos: [], }; - return childSession; } /** * 保存子会话 */ async saveChildSession(session: SessionData): Promise { - await this.storage.saveSession(session, 0); + 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); } /** @@ -231,12 +499,11 @@ export class SessionManager { throw new Error('Project not initialized. Call init() first.'); } - const session = await this.storage.loadSession(this.currentProject.id, sessionId); + const session = await this.loadSession(this.currentProject.id, sessionId); if (!session) return null; this.currentSession = session; - this.lastSyncedCount = session.messages.length; - await this.storage.setCurrentSession(sessionId); + await this.setCurrentSessionPointer(sessionId); return session; } @@ -246,26 +513,78 @@ export class SessionManager { */ async listSessions(): Promise { if (!this.currentProject) { - return this.storage.listAllSessions(); + return this.listAllSessions(); } - return this.storage.listSessionsByProject(this.currentProject.id); + + 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 { - return this.storage.listAllSessions(); + 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 { - if (!this.currentProject) { + 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; } - return this.storage.deleteSession(this.currentProject.id, sessionId); + } + + /** + * 获取当前会话 ID(从存储) + */ + private async getCurrentSessionId(): Promise { + try { + const pointer = await storage.read<{ sessionId: string }>(['current-session']); + return pointer.sessionId; + } catch { + return null; + } + } + + /** + * 设置当前会话指针 + */ + private async setCurrentSessionPointer(sessionId: string): Promise { + await storage.write(['current-session'], { sessionId }); } /** @@ -300,8 +619,29 @@ export class SessionManager { /** * 清理旧会话 */ - async cleanup(keepCount?: number): Promise { - return this.storage.cleanupOldSessions(keepCount); + async cleanup(keepCount: number = 50): Promise { + 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; + } + + /** + * 获取存储目录 + */ + getStorageDir(): string { + return this.storageDir || storage.getDefaultStorageDir(); } } diff --git a/packages/core/src/session/message.ts b/packages/core/src/session/message.ts new file mode 100644 index 0000000..7286822 --- /dev/null +++ b/packages/core/src/session/message.ts @@ -0,0 +1,79 @@ +import { z } from 'zod'; + +/** + * 消息角色 + */ +export const MessageRoleSchema = z.enum(['user', 'assistant', 'system']); +export type MessageRole = z.infer; + +/** + * 消息 Info Schema(不含内容,内容在 Parts 中) + */ +export const MessageInfoSchema = z.object({ + id: z.string(), + sessionId: z.string(), + role: MessageRoleSchema, + createdAt: z.number(), // Unix timestamp in milliseconds + // Part IDs 列表(按顺序) + partIds: z.array(z.string()), + // 元数据 + metadata: z + .object({ + // 模型信息 + model: z.string().optional(), + // 步骤索引(如果是多步骤响应) + stepIndex: z.number().optional(), + // 是否被压缩 + compacted: z.boolean().optional(), + }) + .optional(), +}); +export type MessageInfo = z.infer; + +/** + * 消息(含 Parts 内容,用于运行时) + */ +export interface Message extends Omit { + parts: import('./parts.js').Part[]; +} + +/** + * 生成消息 ID + * 使用降序 ID,最新的消息在前面(方便列表查询) + */ +export function generateMessageId(): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 8); + // 使用最大时间戳减去当前时间戳,实现降序 + const invertedTimestamp = 9999999999999 - timestamp; + return `msg_${invertedTimestamp}_${random}`; +} + +/** + * 从消息 ID 提取时间戳 + */ +export function getTimestampFromMessageId(messageId: string): number { + const match = messageId.match(/^msg_(\d+)_/); + if (!match) return 0; + const invertedTimestamp = parseInt(match[1], 10); + return 9999999999999 - invertedTimestamp; +} + +/** + * 创建消息 Info + */ +export function createMessageInfo( + sessionId: string, + role: MessageRole, + partIds: string[] = [], + metadata?: MessageInfo['metadata'] +): MessageInfo { + return { + id: generateMessageId(), + sessionId, + role, + createdAt: Date.now(), + partIds, + metadata, + }; +} diff --git a/packages/core/src/session/migration.ts b/packages/core/src/session/migration.ts deleted file mode 100644 index c177afa..0000000 --- a/packages/core/src/session/migration.ts +++ /dev/null @@ -1,197 +0,0 @@ -/** - * 数据迁移框架 - * 支持版本化的数据结构升级 - */ - -import * as fs from 'fs/promises'; -import * as path from 'path'; -import { Lock } from '../utils/lock.js'; - -type Migration = (storageDir: string) => Promise; - -/** - * 迁移函数列表 - * 新迁移添加到数组末尾,索引即为版本号 - */ -const MIGRATIONS: Migration[] = [ - // 迁移 0: 初始化新目录结构 - // 将旧的 sessions/ 目录下的会话文件迁移到新的分层结构 - async (storageDir: string) => { - const oldSessionsDir = path.join(storageDir, 'sessions'); - const projectDir = path.join(storageDir, 'project'); - const sessionDir = path.join(storageDir, 'session'); - const messageDir = path.join(storageDir, 'message'); - - // 确保新目录存在 - await fs.mkdir(projectDir, { recursive: true }); - await fs.mkdir(sessionDir, { recursive: true }); - await fs.mkdir(messageDir, { recursive: true }); - - // 检查是否有旧数据需要迁移 - try { - const files = await fs.readdir(oldSessionsDir); - for (const file of files) { - if (!file.endsWith('.json')) continue; - - const filePath = path.join(oldSessionsDir, file); - const content = await fs.readFile(filePath, 'utf-8'); - const oldSession = JSON.parse(content); - - // 提取消息 - const messages = oldSession.messages || []; - const sessionId = oldSession.id; - - // 创建消息目录 - const sessionMessageDir = path.join(messageDir, sessionId); - await fs.mkdir(sessionMessageDir, { recursive: true }); - - // 写入每条消息 - for (let i = 0; i < messages.length; i++) { - const messageFile = path.join(sessionMessageDir, `${String(i + 1).padStart(4, '0')}.json`); - await fs.writeFile( - messageFile, - JSON.stringify( - { - index: i + 1, - ...messages[i], - createdAt: oldSession.createdAt, - }, - null, - 2 - ), - 'utf-8' - ); - } - - // 创建新的会话元数据 (使用默认 projectId,后续会在 storage 中更新) - const metadata = { - id: sessionId, - projectId: 'default', - createdAt: oldSession.createdAt, - updatedAt: oldSession.updatedAt, - workdir: oldSession.workdir, - title: oldSession.title, - messageCount: messages.length, - discoveredTools: oldSession.discoveredTools || [], - todos: oldSession.todos || [], - parentId: oldSession.parentId, - agentName: oldSession.agentName, - }; - - // 写入会话元数据到默认项目目录 - const defaultProjectDir = path.join(sessionDir, 'default'); - await fs.mkdir(defaultProjectDir, { recursive: true }); - await fs.writeFile( - path.join(defaultProjectDir, `${sessionId}.json`), - JSON.stringify(metadata, null, 2), - 'utf-8' - ); - } - - // 迁移 current-session.json - const currentSessionFile = path.join(storageDir, 'current-session.json'); - try { - const currentContent = await fs.readFile(currentSessionFile, 'utf-8'); - const currentSession = JSON.parse(currentContent); - - if (currentSession.messages) { - const sessionId = currentSession.id; - const messages = currentSession.messages; - - // 创建消息目录 - const sessionMessageDir = path.join(messageDir, sessionId); - await fs.mkdir(sessionMessageDir, { recursive: true }); - - // 写入每条消息 - for (let i = 0; i < messages.length; i++) { - const messageFile = path.join(sessionMessageDir, `${String(i + 1).padStart(4, '0')}.json`); - await fs.writeFile( - messageFile, - JSON.stringify( - { - index: i + 1, - ...messages[i], - createdAt: currentSession.createdAt, - }, - null, - 2 - ), - 'utf-8' - ); - } - - // 更新 current-session.json 为只包含 sessionId - await fs.writeFile(currentSessionFile, JSON.stringify({ sessionId }, null, 2), 'utf-8'); - } - } catch { - // 没有 current-session.json,忽略 - } - } catch { - // sessions 目录不存在,跳过迁移 - } - }, -]; - -/** - * 获取当前迁移版本 - */ -async function getMigrationVersion(storageDir: string): Promise { - const versionFile = path.join(storageDir, 'migration'); - try { - const content = await fs.readFile(versionFile, 'utf-8'); - return parseInt(content.trim(), 10) || 0; - } catch { - return 0; - } -} - -/** - * 保存迁移版本 - */ -async function setMigrationVersion(storageDir: string, version: number): Promise { - const versionFile = path.join(storageDir, 'migration'); - await fs.writeFile(versionFile, version.toString(), 'utf-8'); -} - -/** - * 执行迁移 - * 在存储初始化时调用 - */ -export async function runMigrations(storageDir: string): Promise { - // 使用写锁防止并发迁移 - using _ = await Lock.write(`migration:${storageDir}`); - - const currentVersion = await getMigrationVersion(storageDir); - - for (let i = currentVersion; i < MIGRATIONS.length; i++) { - const migration = MIGRATIONS[i]; - - try { - console.log(`[migration] Running migration ${i}...`); - await migration(storageDir); - await setMigrationVersion(storageDir, i + 1); - console.log(`[migration] Migration ${i} completed`); - } catch (error) { - console.error(`[migration] Migration ${i} failed:`, error); - // 迁移失败不阻塞启动,但记录错误 - // 下次启动会重试失败的迁移 - break; - } - } -} - -/** - * 获取迁移状态 - */ -export async function getMigrationStatus(storageDir: string): Promise<{ - currentVersion: number; - latestVersion: number; - pendingMigrations: number; -}> { - const currentVersion = await getMigrationVersion(storageDir); - return { - currentVersion, - latestVersion: MIGRATIONS.length, - pendingMigrations: Math.max(0, MIGRATIONS.length - currentVersion), - }; -} diff --git a/packages/core/src/session/parts.ts b/packages/core/src/session/parts.ts new file mode 100644 index 0000000..db11811 --- /dev/null +++ b/packages/core/src/session/parts.ts @@ -0,0 +1,202 @@ +import { z } from 'zod'; + +/** + * Part 基础 Schema + */ +const PartBaseSchema = z.object({ + id: z.string(), + createdAt: z.number(), // Unix timestamp in milliseconds +}); + +/** + * 文本内容 Part + */ +export const TextPartSchema = PartBaseSchema.extend({ + type: z.literal('text'), + text: z.string(), +}); +export type TextPart = z.infer; + +/** + * 工具调用状态 + */ +export const ToolStatusSchema = z.enum(['pending', 'running', 'completed', 'error']); +export type ToolStatus = z.infer; + +/** + * 工具调用 Part + */ +export const ToolPartSchema = PartBaseSchema.extend({ + type: z.literal('tool'), + toolCallId: z.string(), + toolName: z.string(), + args: z.record(z.string(), z.unknown()), + status: ToolStatusSchema, + result: z.unknown().optional(), + error: z.string().optional(), + startedAt: z.number().optional(), + completedAt: z.number().optional(), +}); +export type ToolPart = z.infer; + +/** + * 推理/思考 Part + */ +export const ReasoningPartSchema = PartBaseSchema.extend({ + type: z.literal('reasoning'), + text: z.string(), +}); +export type ReasoningPart = z.infer; + +/** + * 文件/图片附件 Part + */ +export const FilePartSchema = PartBaseSchema.extend({ + type: z.literal('file'), + filename: z.string(), + mimeType: z.string(), + data: z.string(), // base64 encoded + size: z.number().optional(), +}); +export type FilePart = z.infer; + +/** + * 步骤开始标记 Part + */ +export const StepStartPartSchema = PartBaseSchema.extend({ + type: z.literal('step-start'), + stepIndex: z.number(), +}); +export type StepStartPart = z.infer; + +/** + * 步骤结束 Part(含 token 统计) + */ +export const StepFinishPartSchema = PartBaseSchema.extend({ + type: z.literal('step-finish'), + stepIndex: z.number(), + finishReason: z.string().optional(), + usage: z + .object({ + promptTokens: z.number(), + completionTokens: z.number(), + totalTokens: z.number(), + }) + .optional(), +}); +export type StepFinishPart = z.infer; + +/** + * Git 快照引用 Part + */ +export const SnapshotPartSchema = PartBaseSchema.extend({ + type: z.literal('snapshot'), + commitHash: z.string(), + message: z.string().optional(), +}); +export type SnapshotPart = z.infer; + +/** + * Git 补丁引用 Part + */ +export const PatchPartSchema = PartBaseSchema.extend({ + type: z.literal('patch'), + diff: z.string(), + files: z.array(z.string()), +}); +export type PatchPart = z.infer; + +/** + * 子 Agent 调用 Part + */ +export const AgentPartSchema = PartBaseSchema.extend({ + type: z.literal('agent'), + agentName: z.string(), + sessionId: z.string().optional(), // 子会话 ID + status: z.enum(['pending', 'running', 'completed', 'error']), + result: z.string().optional(), + error: z.string().optional(), +}); +export type AgentPart = z.infer; + +/** + * Task 工具委托 Part + */ +export const SubtaskPartSchema = PartBaseSchema.extend({ + type: z.literal('subtask'), + taskId: z.string(), + description: z.string(), + status: z.enum(['pending', 'running', 'completed', 'error']), + result: z.string().optional(), + error: z.string().optional(), +}); +export type SubtaskPart = z.infer; + +/** + * 上下文压缩标记 Part + */ +export const CompactionPartSchema = PartBaseSchema.extend({ + type: z.literal('compaction'), + summary: z.string(), + originalTokens: z.number(), + compressedTokens: z.number(), +}); +export type CompactionPart = z.infer; + +/** + * 重试信息 Part + */ +export const RetryPartSchema = PartBaseSchema.extend({ + type: z.literal('retry'), + reason: z.string(), + attemptNumber: z.number(), +}); +export type RetryPart = z.infer; + +/** + * 所有 Part 类型的联合 + */ +export const PartSchema = z.discriminatedUnion('type', [ + TextPartSchema, + ToolPartSchema, + ReasoningPartSchema, + FilePartSchema, + StepStartPartSchema, + StepFinishPartSchema, + SnapshotPartSchema, + PatchPartSchema, + AgentPartSchema, + SubtaskPartSchema, + CompactionPartSchema, + RetryPartSchema, +]); +export type Part = z.infer; + +/** + * Part 类型字面量 + */ +export type PartType = Part['type']; + +/** + * 创建 Part 的工厂函数 + */ +export function createPart( + type: T, + data: Omit, 'id' | 'createdAt' | 'type'> +): Extract { + return { + id: generatePartId(), + createdAt: Date.now(), + type, + ...data, + } as Extract; +} + +/** + * 生成 Part ID + */ +function generatePartId(): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 8); + return `part_${timestamp}_${random}`; +} diff --git a/packages/core/src/session/storage.ts b/packages/core/src/session/storage.ts deleted file mode 100644 index 4d5ada9..0000000 --- a/packages/core/src/session/storage.ts +++ /dev/null @@ -1,465 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import * as os from 'os'; -import type { ModelMessage } from 'ai'; -import type { - SessionData, - SessionMetadata, - SessionSummary, - StoredMessage, - CurrentSessionPointer, - ProjectMetadata, -} from './types.js'; -import { Lock } from '../utils/lock.js'; -import { getProjectId, isGitRepository } from './project.js'; -import { runMigrations } from './migration.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 projectDir: string; - private sessionDir: string; - private messageDir: string; - private currentSessionFile: string; - private initialized = false; - - constructor(storageDir?: string) { - this.storageDir = storageDir || getDefaultStorageDir(); - this.projectDir = path.join(this.storageDir, 'project'); - this.sessionDir = path.join(this.storageDir, 'session'); - this.messageDir = path.join(this.storageDir, 'message'); - this.currentSessionFile = path.join(this.storageDir, 'current-session.json'); - } - - /** - * 确保存储目录存在并执行迁移 - */ - async ensureDir(): Promise { - if (this.initialized) return; - - await fs.mkdir(this.projectDir, { recursive: true }); - await fs.mkdir(this.sessionDir, { recursive: true }); - await fs.mkdir(this.messageDir, { recursive: true }); - - // 执行数据迁移 - await runMigrations(this.storageDir); - - this.initialized = 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 getOrCreateProject(workdir: string): Promise { - await this.ensureDir(); - - const projectId = await getProjectId(workdir); - const projectFile = path.join(this.projectDir, `${projectId}.json`); - - // 先尝试读取(只需读锁) - { - using _ = await Lock.read(projectFile); - try { - const content = await fs.readFile(projectFile, 'utf-8'); - return JSON.parse(content) as ProjectMetadata; - } catch { - // 项目不存在,需要创建 - } - } - - // 读锁已释放,获取写锁来创建项目 - using _ = await Lock.write(projectFile); - - // 双重检查:可能其他进程已经创建了 - try { - const content = await fs.readFile(projectFile, 'utf-8'); - return JSON.parse(content) as ProjectMetadata; - } catch { - // 确实不存在,创建新项目 - } - - const isGitRepo = await isGitRepository(workdir); - const project: ProjectMetadata = { - id: projectId, - workdir, - createdAt: new Date().toISOString(), - isGitRepo, - }; - - await fs.writeFile(projectFile, JSON.stringify(project, null, 2), 'utf-8'); - - // 确保项目的会话目录存在 - await fs.mkdir(path.join(this.sessionDir, projectId), { recursive: true }); - - return project; - } - - // ========== 会话元数据管理 ========== - - /** - * 保存会话元数据 - */ - async saveSessionMetadata(session: SessionData): Promise { - await this.ensureDir(); - - const metadata: SessionMetadata = { - id: session.id, - projectId: session.projectId, - parentId: session.parentId, - agentName: session.agentName, - createdAt: session.createdAt, - updatedAt: new Date().toISOString(), - workdir: session.workdir, - title: session.title, - messageCount: session.messages.length, - discoveredTools: session.discoveredTools, - todos: session.todos, - }; - - const sessionFile = path.join(this.sessionDir, session.projectId, `${session.id}.json`); - - // 确保项目目录存在 - await fs.mkdir(path.join(this.sessionDir, session.projectId), { recursive: true }); - - using _ = await Lock.write(sessionFile); - await fs.writeFile(sessionFile, JSON.stringify(metadata, null, 2), 'utf-8'); - } - - /** - * 加载会话元数据 - */ - async loadSessionMetadata(projectId: string, sessionId: string): Promise { - await this.ensureDir(); - - const sessionFile = path.join(this.sessionDir, projectId, `${sessionId}.json`); - - using _ = await Lock.read(sessionFile); - - try { - const content = await fs.readFile(sessionFile, 'utf-8'); - return JSON.parse(content) as SessionMetadata; - } catch { - return null; - } - } - - // ========== 消息管理 ========== - - /** - * 追加消息 - */ - async appendMessage(sessionId: string, message: ModelMessage, index: number): Promise { - await this.ensureDir(); - - const sessionMessageDir = path.join(this.messageDir, sessionId); - await fs.mkdir(sessionMessageDir, { recursive: true }); - - const messageFile = path.join(sessionMessageDir, `${String(index).padStart(4, '0')}.json`); - - const storedMessage: StoredMessage = { - index, - role: message.role as StoredMessage['role'], - content: message.content, - createdAt: new Date().toISOString(), - }; - - using _ = await Lock.write(messageFile); - await fs.writeFile(messageFile, JSON.stringify(storedMessage, null, 2), 'utf-8'); - } - - /** - * 批量追加消息(用于增量同步) - */ - async syncMessages(sessionId: string, messages: ModelMessage[], startIndex: number): Promise { - for (let i = startIndex; i < messages.length; i++) { - await this.appendMessage(sessionId, messages[i], i + 1); - } - } - - /** - * 加载会话的所有消息 - */ - async loadMessages(sessionId: string): Promise { - await this.ensureDir(); - - const sessionMessageDir = path.join(this.messageDir, sessionId); - - try { - const files = await fs.readdir(sessionMessageDir); - const messageFiles = files.filter((f) => f.endsWith('.json')).sort(); - - const messages: ModelMessage[] = []; - - for (const file of messageFiles) { - const filePath = path.join(sessionMessageDir, file); - using _ = await Lock.read(filePath); - const content = await fs.readFile(filePath, 'utf-8'); - const stored = JSON.parse(content) as StoredMessage; - messages.push({ - role: stored.role, - content: stored.content, - } as ModelMessage); - } - - return messages; - } catch { - return []; - } - } - - /** - * 删除会话的所有消息 - */ - async deleteMessages(sessionId: string): Promise { - const sessionMessageDir = path.join(this.messageDir, sessionId); - - try { - await fs.rm(sessionMessageDir, { recursive: true, force: true }); - } catch { - // 忽略错误 - } - } - - // ========== 完整会话操作 ========== - - /** - * 保存完整会话(元数据 + 消息) - */ - async saveSession(session: SessionData, lastSyncedCount: number = 0): Promise { - // 增量同步消息 - await this.syncMessages(session.id, session.messages, lastSyncedCount); - // 保存元数据 - await this.saveSessionMetadata(session); - } - - /** - * 加载完整会话 - */ - async loadSession(projectId: string, sessionId: string): Promise { - const metadata = await this.loadSessionMetadata(projectId, sessionId); - if (!metadata) return null; - - const messages = await this.loadMessages(sessionId); - - return { - id: metadata.id, - projectId: metadata.projectId, - parentId: metadata.parentId, - agentName: metadata.agentName, - createdAt: metadata.createdAt, - updatedAt: metadata.updatedAt, - workdir: metadata.workdir, - title: metadata.title, - discoveredTools: metadata.discoveredTools, - todos: metadata.todos, - messages, - }; - } - - /** - * 删除会话 - */ - async deleteSession(projectId: string, sessionId: string): Promise { - try { - // 删除消息 - await this.deleteMessages(sessionId); - - // 删除元数据 - const sessionFile = path.join(this.sessionDir, projectId, `${sessionId}.json`); - await fs.unlink(sessionFile); - - return true; - } catch { - return false; - } - } - - // ========== 当前会话管理 ========== - - /** - * 设置当前会话 - */ - async setCurrentSession(sessionId: string): Promise { - await this.ensureDir(); - - const pointer: CurrentSessionPointer = { sessionId }; - - using _ = await Lock.write(this.currentSessionFile); - await fs.writeFile(this.currentSessionFile, JSON.stringify(pointer, null, 2), 'utf-8'); - } - - /** - * 获取当前会话 ID - */ - async getCurrentSessionId(): Promise { - await this.ensureDir(); - - using _ = await Lock.read(this.currentSessionFile); - - try { - const content = await fs.readFile(this.currentSessionFile, 'utf-8'); - const pointer = JSON.parse(content) as CurrentSessionPointer; - return pointer.sessionId; - } catch { - return null; - } - } - - /** - * 清除当前会话指针 - */ - async clearCurrentSession(): Promise { - try { - await fs.unlink(this.currentSessionFile); - } catch { - // 文件不存在,忽略 - } - } - - // ========== 会话列表 ========== - - /** - * 列出项目的所有会话 - */ - async listSessionsByProject(projectId: string): Promise { - await this.ensureDir(); - - const projectSessionDir = path.join(this.sessionDir, projectId); - - try { - const files = await fs.readdir(projectSessionDir); - const summaries: SessionSummary[] = []; - - for (const file of files) { - if (!file.endsWith('.json')) continue; - - try { - const filePath = path.join(projectSessionDir, file); - using _ = await Lock.read(filePath); - const content = await fs.readFile(filePath, 'utf-8'); - const metadata = JSON.parse(content) as SessionMetadata; - - summaries.push({ - id: metadata.id, - title: metadata.title || this.generateTitleFromId(metadata.id), - workdir: metadata.workdir, - messageCount: metadata.messageCount, - createdAt: metadata.createdAt, - updatedAt: metadata.updatedAt, - }); - } catch { - // 跳过无法解析的文件 - } - } - - // 按更新时间降序排列 - return summaries.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); - } catch { - return []; - } - } - - /** - * 列出所有会话(跨项目) - */ - async listAllSessions(): Promise { - await this.ensureDir(); - - const allSummaries: SessionSummary[] = []; - - try { - const projectDirs = await fs.readdir(this.sessionDir); - - for (const projectId of projectDirs) { - const summaries = await this.listSessionsByProject(projectId); - allSummaries.push(...summaries); - } - - // 按更新时间降序排列 - return allSummaries.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); - } catch { - return []; - } - } - - /** - * 清理旧会话(保留最近 N 个) - */ - async cleanupOldSessions(keepCount: number = 50): Promise { - const sessions = await this.listAllSessions(); - if (sessions.length <= keepCount) { - return 0; - } - - const toDelete = sessions.slice(keepCount); - let deletedCount = 0; - - for (const session of toDelete) { - // 需要找到对应的 projectId - // 这里通过遍历项目目录来查找 - try { - const projectDirs = await fs.readdir(this.sessionDir); - for (const projectId of projectDirs) { - const sessionFile = path.join(this.sessionDir, projectId, `${session.id}.json`); - try { - await fs.access(sessionFile); - if (await this.deleteSession(projectId, session.id)) { - deletedCount++; - } - break; - } catch { - // 不在这个项目目录,继续查找 - } - } - } catch { - // 忽略错误 - } - } - - return deletedCount; - } - - /** - * 从会话 ID 生成标题 - */ - private generateTitleFromId(sessionId: string): string { - return `会话 ${sessionId}`; - } - - /** - * 获取存储目录路径 - */ - getStorageDir(): string { - return this.storageDir; - } -} - -// 导出默认实例 -export const sessionStorage = new SessionStorage(); diff --git a/packages/core/src/session/storage/base.ts b/packages/core/src/session/storage/base.ts new file mode 100644 index 0000000..dcc154f --- /dev/null +++ b/packages/core/src/session/storage/base.ts @@ -0,0 +1,248 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; +import { Lock } from '../../utils/lock.js'; +import { z } from 'zod'; + +/** + * 存储错误类型 + */ +export class StorageNotFoundError extends Error { + constructor( + public readonly key: string[], + message?: string + ) { + super(message || `Resource not found: ${key.join('/')}`); + this.name = 'StorageNotFoundError'; + } +} + +/** + * 获取默认存储目录 + * 遵循 XDG 规范:~/.local/share/ai-assist/ + */ +export 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'); +} + +/** + * 存储状态 + */ +interface StorageState { + dir: string; + initialized: boolean; +} + +let storageState: StorageState | null = null; + +/** + * 初始化存储 + */ +export async function initStorage(storageDir?: string): Promise { + if (storageState?.initialized) { + return storageState.dir; + } + + const dir = storageDir || getDefaultStorageDir(); + + // 创建必要的目录 + await fs.mkdir(path.join(dir, 'project'), { recursive: true }); + await fs.mkdir(path.join(dir, 'session'), { recursive: true }); + await fs.mkdir(path.join(dir, 'message'), { recursive: true }); + await fs.mkdir(path.join(dir, 'part'), { recursive: true }); + await fs.mkdir(path.join(dir, 'todo'), { recursive: true }); + + storageState = { dir, initialized: true }; + return dir; +} + +/** + * 获取存储目录 + */ +export async function getStorageDir(): Promise { + if (!storageState?.initialized) { + return initStorage(); + } + return storageState.dir; +} + +/** + * 重置存储状态(用于测试) + */ +export function resetStorageState(): void { + storageState = null; +} + +/** + * 读取 JSON 文件 + * @param key 路径键数组,如 ['session', 'projectId', 'sessionId'] + */ +export async function read(key: string[], schema?: z.ZodType): Promise { + const dir = await getStorageDir(); + const target = path.join(dir, ...key) + '.json'; + + using _ = await Lock.read(target); + + try { + const content = await fs.readFile(target, 'utf-8'); + const data = JSON.parse(content); + + if (schema) { + return schema.parse(data); + } + return data as T; + } catch (e) { + if (e instanceof Error && 'code' in e && (e as NodeJS.ErrnoException).code === 'ENOENT') { + throw new StorageNotFoundError(key); + } + throw e; + } +} + +/** + * 写入 JSON 文件 + * @param key 路径键数组 + * @param content 要写入的内容 + */ +export async function write(key: string[], content: T): Promise { + const dir = await getStorageDir(); + const target = path.join(dir, ...key) + '.json'; + + // 确保父目录存在 + await fs.mkdir(path.dirname(target), { recursive: true }); + + using _ = await Lock.write(target); + await fs.writeFile(target, JSON.stringify(content, null, 2), 'utf-8'); +} + +/** + * 更新 JSON 文件 + * @param key 路径键数组 + * @param fn 更新函数 + */ +export async function update(key: string[], fn: (draft: T) => void): Promise { + const dir = await getStorageDir(); + const target = path.join(dir, ...key) + '.json'; + + using _ = await Lock.write(target); + + try { + const content = await fs.readFile(target, 'utf-8'); + const data = JSON.parse(content) as T; + fn(data); + await fs.writeFile(target, JSON.stringify(data, null, 2), 'utf-8'); + return data; + } catch (e) { + if (e instanceof Error && 'code' in e && (e as NodeJS.ErrnoException).code === 'ENOENT') { + throw new StorageNotFoundError(key); + } + throw e; + } +} + +/** + * 删除 JSON 文件 + * @param key 路径键数组 + */ +export async function remove(key: string[]): Promise { + const dir = await getStorageDir(); + const target = path.join(dir, ...key) + '.json'; + + try { + await fs.unlink(target); + } catch { + // 忽略不存在的文件 + } +} + +/** + * 删除目录 + * @param key 路径键数组 + */ +export async function removeDir(key: string[]): Promise { + const dir = await getStorageDir(); + const target = path.join(dir, ...key); + + try { + await fs.rm(target, { recursive: true, force: true }); + } catch { + // 忽略错误 + } +} + +/** + * 列出目录下的所有 key + * @param prefix 路径前缀 + * @returns 完整的 key 路径数组 + */ +export async function list(prefix: string[]): Promise { + const dir = await getStorageDir(); + const target = path.join(dir, ...prefix); + + try { + const entries = await fs.readdir(target, { withFileTypes: true }); + const results: string[][] = []; + + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.json')) { + // 移除 .json 扩展名 + const name = entry.name.slice(0, -5); + results.push([...prefix, name]); + } else if (entry.isDirectory()) { + // 递归列出子目录 + const subResults = await list([...prefix, entry.name]); + results.push(...subResults); + } + } + + return results.sort(); + } catch { + return []; + } +} + +/** + * 列出目录下的直接子项(不递归) + * @param prefix 路径前缀 + * @returns 子项名称数组 + */ +export async function listDirect(prefix: string[]): Promise { + const dir = await getStorageDir(); + const target = path.join(dir, ...prefix); + + try { + const entries = await fs.readdir(target, { withFileTypes: true }); + const results: string[] = []; + + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.json')) { + results.push(entry.name.slice(0, -5)); + } else if (entry.isDirectory()) { + results.push(entry.name); + } + } + + return results.sort(); + } catch { + return []; + } +} + +/** + * 检查文件是否存在 + */ +export async function exists(key: string[]): Promise { + const dir = await getStorageDir(); + const target = path.join(dir, ...key) + '.json'; + + try { + await fs.access(target); + return true; + } catch { + return false; + } +} diff --git a/packages/core/src/session/storage/index.ts b/packages/core/src/session/storage/index.ts new file mode 100644 index 0000000..55e0c17 --- /dev/null +++ b/packages/core/src/session/storage/index.ts @@ -0,0 +1,34 @@ +// Base storage utilities +export { + StorageNotFoundError, + getDefaultStorageDir, + initStorage, + getStorageDir, + resetStorageState, + read, + write, + update, + remove, + removeDir, + list, + listDirect, + exists, +} from './base.js'; + +// Session storage +export * as SessionStorage from './session.js'; +export type { SessionInfo } from './session.js'; +export { SessionInfoSchema } from './session.js'; + +// Message storage +export * as MessageStorage from './message.js'; +export type { MessageInfo } from './message.js'; + +// Part storage +export * as PartStorage from './part.js'; +export type { Part, PartType, ToolPart, ToolStatus } from './part.js'; + +// Todo storage +export * as TodoStorage from './todo.js'; +export type { TodoItem, TodoList, TodoStatus } from './todo.js'; +export { TodoItemSchema, TodoListSchema, TodoStatusSchema } from './todo.js'; diff --git a/packages/core/src/session/storage/message.ts b/packages/core/src/session/storage/message.ts new file mode 100644 index 0000000..1972f71 --- /dev/null +++ b/packages/core/src/session/storage/message.ts @@ -0,0 +1,118 @@ +import { z } from 'zod'; +import * as base from './base.js'; +import { generateMessageId } from '../id.js'; +import type { MessageInfo } from '../message.js'; +import { MessageInfoSchema } from '../message.js'; + +// Re-export types +export type { MessageInfo } from '../message.js'; + +/** + * 创建消息 + */ +export async function create( + sessionId: string, + role: MessageInfo['role'], + options?: { + partIds?: string[]; + metadata?: MessageInfo['metadata']; + } +): Promise { + const message: MessageInfo = { + id: generateMessageId(), + sessionId, + role, + createdAt: Date.now(), + partIds: options?.partIds || [], + metadata: options?.metadata, + }; + + await base.write(['message', sessionId, message.id], message); + return message; +} + +/** + * 读取消息 + */ +export async function get(sessionId: string, messageId: string): Promise { + try { + return await base.read(['message', sessionId, messageId], MessageInfoSchema); + } catch (e) { + if (e instanceof base.StorageNotFoundError) { + return null; + } + throw e; + } +} + +/** + * 保存消息 + */ +export async function save(message: MessageInfo): Promise { + await base.write(['message', message.sessionId, message.id], message); +} + +/** + * 更新消息 + */ +export async function update( + sessionId: string, + messageId: string, + updates: Partial> +): Promise { + return base.update(['message', sessionId, messageId], (message: MessageInfo) => { + Object.assign(message, updates); + }); +} + +/** + * 添加 Part 到消息 + */ +export async function addPart(sessionId: string, messageId: string, partId: string): Promise { + await base.update(['message', sessionId, messageId], (message: MessageInfo) => { + message.partIds.push(partId); + }); +} + +/** + * 删除消息 + */ +export async function remove(sessionId: string, messageId: string): Promise { + await base.remove(['message', sessionId, messageId]); +} + +/** + * 列出会话的所有消息 + */ +export async function listBySession(sessionId: string): Promise { + const keys = await base.list(['message', sessionId]); + const messages: MessageInfo[] = []; + + for (const key of keys) { + try { + const message = await base.read(key, MessageInfoSchema); + messages.push(message); + } catch { + // 跳过无法解析的文件 + } + } + + // 消息 ID 是降序的,所以排序后最新的在后面(时间升序) + // 实际上按 ID 字符串排序即可,因为降序 ID 自然会让旧消息在前 + return messages.sort((a, b) => b.id.localeCompare(a.id)).reverse(); +} + +/** + * 删除会话的所有消息 + */ +export async function removeBySession(sessionId: string): Promise { + await base.removeDir(['message', sessionId]); +} + +/** + * 获取会话的消息数量 + */ +export async function countBySession(sessionId: string): Promise { + const keys = await base.list(['message', sessionId]); + return keys.length; +} diff --git a/packages/core/src/session/storage/part.ts b/packages/core/src/session/storage/part.ts new file mode 100644 index 0000000..b14f641 --- /dev/null +++ b/packages/core/src/session/storage/part.ts @@ -0,0 +1,168 @@ +import * as base from './base.js'; +import { generatePartId } from '../id.js'; +import type { Part, PartType, ToolPart, ToolStatus } from '../parts.js'; +import { PartSchema } from '../parts.js'; + +// Re-export types +export type { Part, PartType, ToolPart, ToolStatus } from '../parts.js'; + +/** + * 创建 Part + */ +export async function create( + messageId: string, + type: T['type'], + data: Omit +): Promise { + const part = { + id: generatePartId(), + createdAt: Date.now(), + type, + ...data, + } as T; + + await base.write(['part', messageId, part.id], part); + return part; +} + +/** + * 读取 Part + */ +export async function get(messageId: string, partId: string): Promise { + try { + return await base.read(['part', messageId, partId], PartSchema); + } catch (e) { + if (e instanceof base.StorageNotFoundError) { + return null; + } + throw e; + } +} + +/** + * 保存 Part + */ +export async function save(messageId: string, part: Part): Promise { + await base.write(['part', messageId, part.id], part); +} + +/** + * 更新 Part + */ +export async function update( + messageId: string, + partId: string, + updates: Partial> +): Promise { + return base.update(['part', messageId, partId], (part: Part) => { + Object.assign(part, updates); + }); +} + +/** + * 更新 ToolPart 状态 + */ +export async function updateToolStatus( + messageId: string, + partId: string, + status: ToolStatus, + result?: unknown, + error?: string +): Promise { + return base.update(['part', messageId, partId], (part: ToolPart) => { + part.status = status; + if (status === 'running') { + part.startedAt = Date.now(); + } + if (status === 'completed' || status === 'error') { + part.completedAt = Date.now(); + if (result !== undefined) { + part.result = result; + } + if (error !== undefined) { + part.error = error; + } + } + }) as Promise; +} + +/** + * 删除 Part + */ +export async function remove(messageId: string, partId: string): Promise { + await base.remove(['part', messageId, partId]); +} + +/** + * 列出消息的所有 Parts + */ +export async function listByMessage(messageId: string): Promise { + const keys = await base.list(['part', messageId]); + const parts: Part[] = []; + + for (const key of keys) { + try { + const part = await base.read(key, PartSchema); + parts.push(part); + } catch { + // 跳过无法解析的文件 + } + } + + // 按创建时间升序排列(Part ID 是升序的) + return parts.sort((a, b) => a.createdAt - b.createdAt); +} + +/** + * 根据 Part IDs 获取 Parts(保持顺序) + */ +export async function getByIds(messageId: string, partIds: string[]): Promise { + const parts: Part[] = []; + + for (const partId of partIds) { + const part = await get(messageId, partId); + if (part) { + parts.push(part); + } + } + + return parts; +} + +/** + * 删除消息的所有 Parts + */ +export async function removeByMessage(messageId: string): Promise { + await base.removeDir(['part', messageId]); +} + +/** + * 创建文本 Part + */ +export async function createText(messageId: string, text: string): Promise { + return create(messageId, 'text', { text }); +} + +/** + * 创建工具调用 Part + */ +export async function createTool( + messageId: string, + toolCallId: string, + toolName: string, + args: Record +): Promise { + return create(messageId, 'tool', { + toolCallId, + toolName, + args, + status: 'pending', + }); +} + +/** + * 创建推理 Part + */ +export async function createReasoning(messageId: string, text: string): Promise { + return create(messageId, 'reasoning', { text }); +} diff --git a/packages/core/src/session/storage/session.ts b/packages/core/src/session/storage/session.ts new file mode 100644 index 0000000..faa9181 --- /dev/null +++ b/packages/core/src/session/storage/session.ts @@ -0,0 +1,167 @@ +import { z } from 'zod'; +import * as base from './base.js'; +import { generateSessionId } from '../id.js'; + +/** + * Session Info Schema + */ +export const SessionInfoSchema = z.object({ + id: z.string(), + projectId: z.string(), + parentId: z.string().optional(), + agentName: z.string().optional(), + createdAt: z.number(), + updatedAt: z.number(), + workdir: z.string(), + title: z.string().optional(), + discoveredTools: z.array(z.string()).default([]), + // 统计信息 + stats: z + .object({ + messageCount: z.number(), + inputTokens: z.number(), + outputTokens: z.number(), + }) + .optional(), +}); +export type SessionInfo = z.infer; + +/** + * 创建会话 + */ +export async function create( + projectId: string, + workdir: string, + options?: { + parentId?: string; + agentName?: string; + title?: string; + } +): Promise { + const session: SessionInfo = { + id: generateSessionId(), + projectId, + workdir, + createdAt: Date.now(), + updatedAt: Date.now(), + parentId: options?.parentId, + agentName: options?.agentName, + title: options?.title, + discoveredTools: [], + }; + + await base.write(['session', projectId, session.id], session); + return session; +} + +/** + * 读取会话 + */ +export async function get(projectId: string, sessionId: string): Promise { + try { + return await base.read(['session', projectId, sessionId], SessionInfoSchema); + } catch (e) { + if (e instanceof base.StorageNotFoundError) { + return null; + } + throw e; + } +} + +/** + * 更新会话 + */ +export async function update( + projectId: string, + sessionId: string, + updates: Partial> +): Promise { + return base.update(['session', projectId, sessionId], (session: SessionInfo) => { + Object.assign(session, updates, { updatedAt: Date.now() }); + }); +} + +/** + * 保存会话 + */ +export async function save(session: SessionInfo): Promise { + session.updatedAt = Date.now(); + await base.write(['session', session.projectId, session.id], session); +} + +/** + * 删除会话 + */ +export async function remove(projectId: string, sessionId: string): Promise { + await base.remove(['session', projectId, sessionId]); +} + +/** + * 列出项目的所有会话 + */ +export async function listByProject(projectId: string): Promise { + const keys = await base.list(['session', projectId]); + const sessions: SessionInfo[] = []; + + for (const key of keys) { + try { + const session = await base.read(key, SessionInfoSchema); + sessions.push(session); + } catch { + // 跳过无法解析的文件 + } + } + + // 按更新时间降序排列 + return sessions.sort((a, b) => b.updatedAt - a.updatedAt); +} + +/** + * 列出所有会话(跨项目) + */ +export async function listAll(): Promise { + const projectIds = await base.listDirect(['session']); + const allSessions: SessionInfo[] = []; + + for (const projectId of projectIds) { + const sessions = await listByProject(projectId); + allSessions.push(...sessions); + } + + // 按更新时间降序排列 + return allSessions.sort((a, b) => b.updatedAt - a.updatedAt); +} + +/** + * 更新会话标题 + */ +export async function updateTitle(projectId: string, sessionId: string, title: string): Promise { + await update(projectId, sessionId, { title }); +} + +/** + * 更新会话统计 + */ +export async function updateStats( + projectId: string, + sessionId: string, + stats: SessionInfo['stats'] +): Promise { + await update(projectId, sessionId, { stats }); +} + +/** + * 添加已发现的工具 + */ +export async function addDiscoveredTool( + projectId: string, + sessionId: string, + toolName: string +): Promise { + await base.update(['session', projectId, sessionId], (session: SessionInfo) => { + if (!session.discoveredTools.includes(toolName)) { + session.discoveredTools.push(toolName); + } + session.updatedAt = Date.now(); + }); +} diff --git a/packages/core/src/session/storage/todo.ts b/packages/core/src/session/storage/todo.ts new file mode 100644 index 0000000..0530808 --- /dev/null +++ b/packages/core/src/session/storage/todo.ts @@ -0,0 +1,199 @@ +import { z } from 'zod'; +import * as base from './base.js'; +import { generateTodoId } from '../id.js'; + +/** + * Todo 状态 + */ +export const TodoStatusSchema = z.enum(['pending', 'in_progress', 'completed']); +export type TodoStatus = z.infer; + +/** + * Todo Item Schema + */ +export const TodoItemSchema = z.object({ + id: z.string(), + content: z.string(), + status: TodoStatusSchema, + createdAt: z.number(), + updatedAt: z.number(), +}); +export type TodoItem = z.infer; + +/** + * Todo List Schema (存储在单个文件中) + */ +export const TodoListSchema = z.object({ + sessionId: z.string(), + items: z.array(TodoItemSchema), + updatedAt: z.number(), +}); +export type TodoList = z.infer; + +/** + * 获取会话的 Todo 列表 + */ +export async function get(sessionId: string): Promise { + try { + return await base.read(['todo', sessionId], TodoListSchema); + } catch (e) { + if (e instanceof base.StorageNotFoundError) { + return null; + } + throw e; + } +} + +/** + * 获取或创建 Todo 列表 + */ +export async function getOrCreate(sessionId: string): Promise { + const existing = await get(sessionId); + if (existing) return existing; + + const todoList: TodoList = { + sessionId, + items: [], + updatedAt: Date.now(), + }; + await base.write(['todo', sessionId], todoList); + return todoList; +} + +/** + * 保存 Todo 列表 + */ +export async function save(todoList: TodoList): Promise { + todoList.updatedAt = Date.now(); + await base.write(['todo', todoList.sessionId], todoList); +} + +/** + * 添加 Todo 项 + */ +export async function add(sessionId: string, content: string): Promise { + const todoList = await getOrCreate(sessionId); + + const item: TodoItem = { + id: generateTodoId(), + content, + status: 'pending', + createdAt: Date.now(), + updatedAt: Date.now(), + }; + + todoList.items.push(item); + await save(todoList); + + return item; +} + +/** + * 批量添加 Todo 项 + */ +export async function addMany(sessionId: string, contents: string[]): Promise { + const todoList = await getOrCreate(sessionId); + const now = Date.now(); + + const items: TodoItem[] = contents.map((content) => ({ + id: generateTodoId(), + content, + status: 'pending' as TodoStatus, + createdAt: now, + updatedAt: now, + })); + + todoList.items.push(...items); + await save(todoList); + + return items; +} + +/** + * 更新 Todo 项状态 + */ +export async function updateStatus(sessionId: string, todoId: string, status: TodoStatus): Promise { + const todoList = await get(sessionId); + if (!todoList) return null; + + const item = todoList.items.find((t) => t.id === todoId); + if (!item) return null; + + item.status = status; + item.updatedAt = Date.now(); + + await save(todoList); + return item; +} + +/** + * 更新 Todo 项内容 + */ +export async function updateContent(sessionId: string, todoId: string, content: string): Promise { + const todoList = await get(sessionId); + if (!todoList) return null; + + const item = todoList.items.find((t) => t.id === todoId); + if (!item) return null; + + item.content = content; + item.updatedAt = Date.now(); + + await save(todoList); + return item; +} + +/** + * 删除 Todo 项 + */ +export async function remove(sessionId: string, todoId: string): Promise { + const todoList = await get(sessionId); + if (!todoList) return false; + + const index = todoList.items.findIndex((t) => t.id === todoId); + if (index === -1) return false; + + todoList.items.splice(index, 1); + await save(todoList); + + return true; +} + +/** + * 清空 Todo 列表 + */ +export async function clear(sessionId: string): Promise { + const todoList = await get(sessionId); + if (!todoList) return; + + todoList.items = []; + await save(todoList); +} + +/** + * 删除会话的 Todo 列表 + */ +export async function removeBySession(sessionId: string): Promise { + await base.remove(['todo', sessionId]); +} + +/** + * 替换整个 Todo 列表 + */ +export async function replace(sessionId: string, items: Omit[]): Promise { + const now = Date.now(); + const todoList: TodoList = { + sessionId, + items: items.map((item) => ({ + id: generateTodoId(), + content: item.content, + status: item.status, + createdAt: now, + updatedAt: now, + })), + updatedAt: now, + }; + + await base.write(['todo', sessionId], todoList); + return todoList; +} diff --git a/packages/core/src/session/types.ts b/packages/core/src/session/types.ts deleted file mode 100644 index aeafc6f..0000000 --- a/packages/core/src/session/types.ts +++ /dev/null @@ -1,110 +0,0 @@ -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 SessionMetadata { - /** 会话 ID */ - id: string; - /** 项目 ID */ - projectId: string; - /** 父会话 ID(子会话时存在) */ - parentId?: string; - /** 关联的 Agent 名称(子会话时存在) */ - agentName?: string; - /** 创建时间 */ - createdAt: string; - /** 最后更新时间 */ - updatedAt: string; - /** 工作目录 */ - workdir: string; - /** 会话标题(可选,从第一条消息生成) */ - title?: string; - /** 消息数量 */ - messageCount: number; - /** 已发现的工具 */ - discoveredTools: string[]; - /** 待办事项 */ - todos: Todo[]; -} - -/** - * 存储的消息格式 - */ -export interface StoredMessage { - /** 消息序号 (1-based) */ - index: number; - /** 消息角色 */ - role: 'user' | 'assistant' | 'system' | 'tool'; - /** 消息内容 */ - content: ModelMessage['content']; - /** 创建时间 */ - createdAt: string; -} - -/** - * 会话数据(运行时格式,包含消息内容) - */ -export interface SessionData extends Omit { - /** 对话历史 */ - messages: ModelMessage[]; -} - -/** - * 会话摘要(用于列表展示) - */ -export interface SessionSummary { - id: string; - title: string; - workdir: string; - messageCount: number; - createdAt: string; - updatedAt: string; -} - -/** - * 当前会话指针(存储在 current-session.json) - */ -export interface CurrentSessionPointer { - sessionId: string; -} - -/** - * 项目元数据 - */ -export interface ProjectMetadata { - /** 项目 ID (Git root commit hash 或路径 hash) */ - id: string; - /** 工作目录 */ - workdir: string; - /** 创建时间 */ - createdAt: string; - /** 是否为 Git 仓库 */ - isGitRepo: boolean; -} - -/** - * 会话管理器配置 - */ -export interface SessionManagerConfig { - /** 存储目录 */ - storageDir: string; - /** 最大历史会话数量 */ - maxHistorySessions?: number; -} diff --git a/packages/core/src/tools/todo/todo-manager.ts b/packages/core/src/tools/todo/todo-manager.ts index 76ac555..47735a5 100644 --- a/packages/core/src/tools/todo/todo-manager.ts +++ b/packages/core/src/tools/todo/todo-manager.ts @@ -1,6 +1,28 @@ -import type { Todo, TodoStatus } from '../../session/types.js'; +import type { TodoItem } from '../../session/storage/todo.js'; import type { SessionManager } from '../../session/index.js'; +// 兼容旧接口的 Todo 类型 +export type Todo = { + id: string; + content: string; + status: 'pending' | 'in_progress' | 'completed'; + createdAt: string; + updatedAt: string; +}; + +export type TodoStatus = 'pending' | 'in_progress' | 'completed'; + +// 将 TodoItem 转换为 Todo +function toTodo(item: TodoItem): Todo { + return { + id: item.id, + content: item.content, + status: item.status, + createdAt: new Date(item.createdAt).toISOString(), + updatedAt: new Date(item.updatedAt).toISOString(), + }; +} + /** * Todo 管理器 * 提供对当前会话 todo 列表的操作接口 @@ -22,7 +44,7 @@ class TodoManager { if (!this.sessionManager) { return []; } - return this.sessionManager.getTodos(); + return this.sessionManager.getTodos().map(toTodo); } /** diff --git a/packages/core/src/tools/todo/todowrite.ts b/packages/core/src/tools/todo/todowrite.ts index 66818aa..2e1cca9 100644 --- a/packages/core/src/tools/todo/todowrite.ts +++ b/packages/core/src/tools/todo/todowrite.ts @@ -1,6 +1,6 @@ import type { ToolResult } from '../../types/index.js'; import type { ToolWithMetadata } from '../types.js'; -import type { Todo, TodoStatus } from '../../session/types.js'; +import type { Todo, TodoStatus } from './todo-manager.js'; import { todoManager } from './todo-manager.js'; /** diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index b74cb37..cd1fbd4 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -115,17 +115,8 @@ sessionsRouter.get('/:id/messages', async (c) => { ); } - // 从 Core Storage 读取消息 - const storage = sessionManager.getStorage(); - if (!storage) { - return c.json({ - success: true, - data: [], - }); - } - - const projectId = sessionManager.getProjectId(id); - const sessionData = await storage.loadSession(projectId, id); + // 从 Core 存储读取消息 + const sessionData = await sessionManager.loadSessionData(id); if (!sessionData) { return c.json({ diff --git a/packages/server/src/session/manager.ts b/packages/server/src/session/manager.ts index c2e8f0d..9e35bc7 100644 --- a/packages/server/src/session/manager.ts +++ b/packages/server/src/session/manager.ts @@ -41,15 +41,16 @@ interface SessionData { todos: unknown[]; } -interface SessionStorageInterface { - ensureDir(): Promise; - generateSessionId(): string; - getOrCreateProject(workdir: string): Promise; +interface SessionManagerInterface { + init(workdir: string): Promise; + getSession(): SessionData | null; + getProject(): ProjectMetadata | null; + listSessions(): Promise; listAllSessions(): Promise; - listSessionsByProject(projectId: string): Promise; - loadSession(projectId: string, sessionId: string): Promise; - saveSession(session: SessionData, lastSyncedCount?: number): Promise; - deleteSession(projectId: string, sessionId: string): Promise; + deleteSession(sessionId: string): Promise; + newSession(workdir?: string): Promise; + restoreSession(sessionId: string): Promise; + getSessionId(): string | undefined; } // ============================================================================ @@ -59,15 +60,15 @@ interface SessionStorageInterface { export class SessionManager { private sessions: Map = new Map(); private sessionProjects: Map = new Map(); // sessionId -> projectId - private storage: SessionStorageInterface | null = null; + private coreManager: SessionManagerInterface | null = null; private currentProject: ProjectMetadata | null = null; private initialized = false; /** - * 获取 storage 实例(供外部使用) + * 获取 Core SessionManager 实例(供外部使用) */ - getStorage(): SessionStorageInterface | null { - return this.storage; + getCoreManager(): SessionManagerInterface | null { + return this.coreManager; } /** @@ -78,7 +79,7 @@ export class SessionManager { } /** - * 初始化:加载 Core 模块的 SessionStorage 并恢复已有 sessions + * 初始化:加载 Core 模块的 SessionManager 并恢复已有 sessions */ async init(): Promise { if (this.initialized) return; @@ -87,39 +88,35 @@ export class SessionManager { // 动态导入 Core 模块,避免构建时依赖 const corePath = '@ai-assistant/core'; const core = (await import(/* webpackIgnore: true */ corePath)) as { - SessionStorage: new () => SessionStorageInterface; + SessionManager: new (storageDir?: string) => SessionManagerInterface; }; - this.storage = new core.SessionStorage(); - await this.storage.ensureDir(); + this.coreManager = new core.SessionManager(); + await this.coreManager.init(process.cwd()); - // 获取当前工作目录的项目 - this.currentProject = await this.storage.getOrCreateProject(process.cwd()); + this.currentProject = this.coreManager.getProject(); // 加载已持久化的 sessions(所有项目) - const summaries = await this.storage.listAllSessions(); + const summaries = await this.coreManager.listAllSessions(); console.log(`[SessionManager] Found ${summaries.length} persisted sessions`); for (const summary of summaries) { - // 需要找到 session 所属的项目,这里简化处理:加载当前项目的会话 - const sessionData = await this.storage.loadSession(this.currentProject.id, summary.id); - if (!sessionData) continue; - - // 记录 session -> project 映射 - this.sessionProjects.set(sessionData.id, sessionData.projectId); - // 转换为 Server Session 格式(只保存元数据,不存储消息) const session: Session = { - id: sessionData.id, - name: sessionData.title, - workdir: sessionData.workdir, - createdAt: sessionData.createdAt, - updatedAt: sessionData.updatedAt, + id: summary.id, + name: summary.title, + workdir: summary.workdir, + createdAt: summary.createdAt, + updatedAt: summary.updatedAt, status: 'idle', - messageCount: sessionData.messages.length, + messageCount: summary.messageCount, }; this.sessions.set(session.id, session); + // 假设所有会话都属于当前项目(简化处理) + if (this.currentProject) { + this.sessionProjects.set(session.id, this.currentProject.id); + } } console.log(`[SessionManager] Loaded ${this.sessions.size} sessions from storage`); @@ -130,36 +127,6 @@ export class SessionManager { this.initialized = true; } - /** - * 持久化单个 session 的元数据 - * 注意:消息存储由 Core Agent 负责,这里只更新会话元数据 - */ - private async persistMetadata(sessionId: string): Promise { - if (!this.storage) return; - - const session = this.sessions.get(sessionId); - if (!session) return; - - const projectId = this.sessionProjects.get(sessionId) || this.currentProject?.id || 'default'; - - // 先加载现有的 session 数据(保留消息) - const existingData = await this.storage.loadSession(projectId, sessionId); - - const sessionData: SessionData = { - id: session.id, - projectId, - createdAt: session.createdAt, - updatedAt: session.updatedAt, - workdir: session.workdir, - title: session.name, - messages: existingData?.messages || [], - discoveredTools: existingData?.discoveredTools || [], - todos: existingData?.todos || [], - }; - - await this.storage.saveSession(sessionData); - } - /** * 创建新会话 */ @@ -167,15 +134,20 @@ export class SessionManager { const now = new Date().toISOString(); const workdir = input.workdir || process.cwd(); - // 如果指定了不同的工作目录,获取对应项目 + let sessionId: string; let projectId = this.currentProject?.id || 'default'; - if (this.storage && workdir !== this.currentProject?.workdir) { - const project = await this.storage.getOrCreateProject(workdir); - projectId = project.id; + + if (this.coreManager) { + // 使用 Core SessionManager 创建会话 + const sessionData = await this.coreManager.newSession(workdir); + sessionId = sessionData.id; + projectId = sessionData.projectId; + } else { + sessionId = uuidv4(); } const session: Session = { - id: this.storage?.generateSessionId() || uuidv4(), + id: sessionId, name: input.name, workdir, createdAt: now, @@ -187,9 +159,6 @@ export class SessionManager { this.sessions.set(session.id, session); this.sessionProjects.set(session.id, projectId); - // 持久化空会话 - await this.persistMetadata(session.id); - return session; } @@ -216,9 +185,8 @@ export class SessionManager { const deleted = this.sessions.delete(id); // 从存储中删除 - if (deleted && this.storage) { - const projectId = this.sessionProjects.get(id) || this.currentProject?.id || 'default'; - await this.storage.deleteSession(projectId, id); + if (deleted && this.coreManager) { + await this.coreManager.deleteSession(id); this.sessionProjects.delete(id); } @@ -259,9 +227,6 @@ export class SessionManager { session.name = name; session.updatedAt = new Date().toISOString(); - // 持久化元数据 - await this.persistMetadata(sessionId); - return session; } @@ -278,6 +243,17 @@ export class SessionManager { exists(id: string): boolean { return this.sessions.has(id); } + + /** + * 加载会话数据(从 Core 存储) + */ + async loadSessionData(sessionId: string): Promise { + if (!this.coreManager) return null; + + // 先恢复会话 + const data = await this.coreManager.restoreSession(sessionId); + return data; + } } // 单例