refactor(storage): 采用 OpenCode 风格三层存储结构

重构消息存储系统,从"每条消息一个文件"改为分层存储:
- Session → Message → Parts 三层结构
- 12 种 Part 类型(TextPart, ToolPart, ReasoningPart 等)
- ToolPart 状态机(pending → running → completed/error)
- 通用 Storage API(read/write/list/remove)

新增文件:
- parts.ts: Part 类型定义(Zod schema)
- message.ts: MessageInfo 类型定义
- id.ts: ID 生成器
- storage/: 分层存储实现

删除旧文件:
- storage.ts, types.ts, migration.ts

存储路径:
~/.local/share/ai-assist/
├── session/{projectId}/{sessionId}.json
├── message/{sessionId}/{messageId}.json
├── part/{messageId}/{partId}.json
└── todo/{sessionId}.json
This commit is contained in:
2025-12-15 11:16:10 +08:00
parent b8fcb65f73
commit 527692ec03
19 changed files with 1867 additions and 943 deletions
+23 -2
View File
@@ -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';
+95
View File
@@ -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);
}
+49 -13
View File
@@ -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';
+406 -66
View File
@@ -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<typeof setInterval> | 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<SessionData> {
// 初始化存储
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<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 createNewSession(workdir: string): SessionData {
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: 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<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 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<void> {
private async saveSessionInfo(): Promise<void> {
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<void> {
async save(): Promise<void> {
if (!this.currentSession) return;
this.currentSession.messages.push(message);
await this.save();
await this.saveSessionInfo();
}
/**
* 同步消息到存储(将 AI SDK 消息转换为 Message + Parts
*/
async syncMessages(messages: ModelMessage[]): Promise<void> {
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<string, unknown> };
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<void> {
if (!this.currentSession) return;
this.currentSession.messages = messages;
await this.save();
await this.syncMessages(messages);
await this.saveSessionInfo();
}
/**
* 添加消息
*/
async addMessage(message: ModelMessage): Promise<void> {
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<void> {
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<void> {
async setTodos(todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>): Promise<void> {
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<void> {
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<SessionSummary[]> {
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<SessionSummary[]> {
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<boolean> {
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<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 });
}
/**
@@ -300,8 +619,29 @@ export class SessionManager {
/**
* 清理旧会话
*/
async cleanup(keepCount?: number): Promise<number> {
return this.storage.cleanupOldSessions(keepCount);
async cleanup(keepCount: number = 50): Promise<number> {
const sessions = await this.listAllSessions();
if (sessions.length <= keepCount) {
return 0;
}
const toDelete = sessions.slice(keepCount);
let deletedCount = 0;
for (const session of toDelete) {
if (await this.deleteSession(session.id)) {
deletedCount++;
}
}
return deletedCount;
}
/**
* 获取存储目录
*/
getStorageDir(): string {
return this.storageDir || storage.getDefaultStorageDir();
}
}
+79
View File
@@ -0,0 +1,79 @@
import { z } from 'zod';
/**
* 消息角色
*/
export const MessageRoleSchema = z.enum(['user', 'assistant', 'system']);
export type MessageRole = z.infer<typeof MessageRoleSchema>;
/**
* 消息 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<typeof MessageInfoSchema>;
/**
* 消息(含 Parts 内容,用于运行时)
*/
export interface Message extends Omit<MessageInfo, 'partIds'> {
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,
};
}
-197
View File
@@ -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<void>;
/**
* 迁移函数列表
* 新迁移添加到数组末尾,索引即为版本号
*/
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<number> {
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<void> {
const versionFile = path.join(storageDir, 'migration');
await fs.writeFile(versionFile, version.toString(), 'utf-8');
}
/**
* 执行迁移
* 在存储初始化时调用
*/
export async function runMigrations(storageDir: string): Promise<void> {
// 使用写锁防止并发迁移
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),
};
}
+202
View File
@@ -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<typeof TextPartSchema>;
/**
* 工具调用状态
*/
export const ToolStatusSchema = z.enum(['pending', 'running', 'completed', 'error']);
export type ToolStatus = z.infer<typeof ToolStatusSchema>;
/**
* 工具调用 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<typeof ToolPartSchema>;
/**
* 推理/思考 Part
*/
export const ReasoningPartSchema = PartBaseSchema.extend({
type: z.literal('reasoning'),
text: z.string(),
});
export type ReasoningPart = z.infer<typeof ReasoningPartSchema>;
/**
* 文件/图片附件 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<typeof FilePartSchema>;
/**
* 步骤开始标记 Part
*/
export const StepStartPartSchema = PartBaseSchema.extend({
type: z.literal('step-start'),
stepIndex: z.number(),
});
export type StepStartPart = z.infer<typeof StepStartPartSchema>;
/**
* 步骤结束 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<typeof StepFinishPartSchema>;
/**
* Git 快照引用 Part
*/
export const SnapshotPartSchema = PartBaseSchema.extend({
type: z.literal('snapshot'),
commitHash: z.string(),
message: z.string().optional(),
});
export type SnapshotPart = z.infer<typeof SnapshotPartSchema>;
/**
* Git 补丁引用 Part
*/
export const PatchPartSchema = PartBaseSchema.extend({
type: z.literal('patch'),
diff: z.string(),
files: z.array(z.string()),
});
export type PatchPart = z.infer<typeof PatchPartSchema>;
/**
* 子 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<typeof AgentPartSchema>;
/**
* 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<typeof SubtaskPartSchema>;
/**
* 上下文压缩标记 Part
*/
export const CompactionPartSchema = PartBaseSchema.extend({
type: z.literal('compaction'),
summary: z.string(),
originalTokens: z.number(),
compressedTokens: z.number(),
});
export type CompactionPart = z.infer<typeof CompactionPartSchema>;
/**
* 重试信息 Part
*/
export const RetryPartSchema = PartBaseSchema.extend({
type: z.literal('retry'),
reason: z.string(),
attemptNumber: z.number(),
});
export type RetryPart = z.infer<typeof RetryPartSchema>;
/**
* 所有 Part 类型的联合
*/
export const PartSchema = z.discriminatedUnion('type', [
TextPartSchema,
ToolPartSchema,
ReasoningPartSchema,
FilePartSchema,
StepStartPartSchema,
StepFinishPartSchema,
SnapshotPartSchema,
PatchPartSchema,
AgentPartSchema,
SubtaskPartSchema,
CompactionPartSchema,
RetryPartSchema,
]);
export type Part = z.infer<typeof PartSchema>;
/**
* Part 类型字面量
*/
export type PartType = Part['type'];
/**
* 创建 Part 的工厂函数
*/
export function createPart<T extends PartType>(
type: T,
data: Omit<Extract<Part, { type: T }>, 'id' | 'createdAt' | 'type'>
): Extract<Part, { type: T }> {
return {
id: generatePartId(),
createdAt: Date.now(),
type,
...data,
} as Extract<Part, { type: T }>;
}
/**
* 生成 Part ID
*/
function generatePartId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `part_${timestamp}_${random}`;
}
-465
View File
@@ -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<void> {
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<ProjectMetadata> {
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<void> {
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<SessionMetadata | null> {
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<void> {
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<void> {
for (let i = startIndex; i < messages.length; i++) {
await this.appendMessage(sessionId, messages[i], i + 1);
}
}
/**
* 加载会话的所有消息
*/
async loadMessages(sessionId: string): Promise<ModelMessage[]> {
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<void> {
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<void> {
// 增量同步消息
await this.syncMessages(session.id, session.messages, lastSyncedCount);
// 保存元数据
await this.saveSessionMetadata(session);
}
/**
* 加载完整会话
*/
async loadSession(projectId: string, sessionId: string): Promise<SessionData | null> {
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<boolean> {
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<void> {
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<string | null> {
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<void> {
try {
await fs.unlink(this.currentSessionFile);
} catch {
// 文件不存在,忽略
}
}
// ========== 会话列表 ==========
/**
* 列出项目的所有会话
*/
async listSessionsByProject(projectId: string): Promise<SessionSummary[]> {
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<SessionSummary[]> {
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<number> {
const sessions = await this.listAllSessions();
if (sessions.length <= keepCount) {
return 0;
}
const toDelete = sessions.slice(keepCount);
let deletedCount = 0;
for (const session of toDelete) {
// 需要找到对应的 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();
+248
View File
@@ -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<string> {
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<string> {
if (!storageState?.initialized) {
return initStorage();
}
return storageState.dir;
}
/**
* 重置存储状态(用于测试)
*/
export function resetStorageState(): void {
storageState = null;
}
/**
* 读取 JSON 文件
* @param key 路径键数组,如 ['session', 'projectId', 'sessionId']
*/
export async function read<T>(key: string[], schema?: z.ZodType<T>): Promise<T> {
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<T>(key: string[], content: T): Promise<void> {
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<T>(key: string[], fn: (draft: T) => void): Promise<T> {
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<void> {
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<void> {
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<string[][]> {
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<string[]> {
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<boolean> {
const dir = await getStorageDir();
const target = path.join(dir, ...key) + '.json';
try {
await fs.access(target);
return true;
} catch {
return false;
}
}
@@ -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';
@@ -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<MessageInfo> {
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<MessageInfo | null> {
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<void> {
await base.write(['message', message.sessionId, message.id], message);
}
/**
* 更新消息
*/
export async function update(
sessionId: string,
messageId: string,
updates: Partial<Omit<MessageInfo, 'id' | 'sessionId' | 'createdAt'>>
): Promise<MessageInfo> {
return base.update(['message', sessionId, messageId], (message: MessageInfo) => {
Object.assign(message, updates);
});
}
/**
* 添加 Part 到消息
*/
export async function addPart(sessionId: string, messageId: string, partId: string): Promise<void> {
await base.update(['message', sessionId, messageId], (message: MessageInfo) => {
message.partIds.push(partId);
});
}
/**
* 删除消息
*/
export async function remove(sessionId: string, messageId: string): Promise<void> {
await base.remove(['message', sessionId, messageId]);
}
/**
* 列出会话的所有消息
*/
export async function listBySession(sessionId: string): Promise<MessageInfo[]> {
const keys = await base.list(['message', sessionId]);
const messages: MessageInfo[] = [];
for (const key of keys) {
try {
const message = await base.read<MessageInfo>(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<void> {
await base.removeDir(['message', sessionId]);
}
/**
* 获取会话的消息数量
*/
export async function countBySession(sessionId: string): Promise<number> {
const keys = await base.list(['message', sessionId]);
return keys.length;
}
+168
View File
@@ -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<T extends Part>(
messageId: string,
type: T['type'],
data: Omit<T, 'id' | 'createdAt' | 'type'>
): Promise<T> {
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<Part | null> {
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<void> {
await base.write(['part', messageId, part.id], part);
}
/**
* 更新 Part
*/
export async function update<T extends Part>(
messageId: string,
partId: string,
updates: Partial<Omit<T, 'id' | 'createdAt' | 'type'>>
): Promise<Part> {
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<ToolPart> {
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<ToolPart>;
}
/**
* 删除 Part
*/
export async function remove(messageId: string, partId: string): Promise<void> {
await base.remove(['part', messageId, partId]);
}
/**
* 列出消息的所有 Parts
*/
export async function listByMessage(messageId: string): Promise<Part[]> {
const keys = await base.list(['part', messageId]);
const parts: Part[] = [];
for (const key of keys) {
try {
const part = await base.read<Part>(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<Part[]> {
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<void> {
await base.removeDir(['part', messageId]);
}
/**
* 创建文本 Part
*/
export async function createText(messageId: string, text: string): Promise<Part> {
return create(messageId, 'text', { text });
}
/**
* 创建工具调用 Part
*/
export async function createTool(
messageId: string,
toolCallId: string,
toolName: string,
args: Record<string, unknown>
): Promise<ToolPart> {
return create<ToolPart>(messageId, 'tool', {
toolCallId,
toolName,
args,
status: 'pending',
});
}
/**
* 创建推理 Part
*/
export async function createReasoning(messageId: string, text: string): Promise<Part> {
return create(messageId, 'reasoning', { text });
}
@@ -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<typeof SessionInfoSchema>;
/**
* 创建会话
*/
export async function create(
projectId: string,
workdir: string,
options?: {
parentId?: string;
agentName?: string;
title?: string;
}
): Promise<SessionInfo> {
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<SessionInfo | null> {
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<Omit<SessionInfo, 'id' | 'projectId' | 'createdAt'>>
): Promise<SessionInfo> {
return base.update(['session', projectId, sessionId], (session: SessionInfo) => {
Object.assign(session, updates, { updatedAt: Date.now() });
});
}
/**
* 保存会话
*/
export async function save(session: SessionInfo): Promise<void> {
session.updatedAt = Date.now();
await base.write(['session', session.projectId, session.id], session);
}
/**
* 删除会话
*/
export async function remove(projectId: string, sessionId: string): Promise<void> {
await base.remove(['session', projectId, sessionId]);
}
/**
* 列出项目的所有会话
*/
export async function listByProject(projectId: string): Promise<SessionInfo[]> {
const keys = await base.list(['session', projectId]);
const sessions: SessionInfo[] = [];
for (const key of keys) {
try {
const session = await base.read<SessionInfo>(key, SessionInfoSchema);
sessions.push(session);
} catch {
// 跳过无法解析的文件
}
}
// 按更新时间降序排列
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
}
/**
* 列出所有会话(跨项目)
*/
export async function listAll(): Promise<SessionInfo[]> {
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<void> {
await update(projectId, sessionId, { title });
}
/**
* 更新会话统计
*/
export async function updateStats(
projectId: string,
sessionId: string,
stats: SessionInfo['stats']
): Promise<void> {
await update(projectId, sessionId, { stats });
}
/**
* 添加已发现的工具
*/
export async function addDiscoveredTool(
projectId: string,
sessionId: string,
toolName: string
): Promise<void> {
await base.update(['session', projectId, sessionId], (session: SessionInfo) => {
if (!session.discoveredTools.includes(toolName)) {
session.discoveredTools.push(toolName);
}
session.updatedAt = Date.now();
});
}
+199
View File
@@ -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<typeof TodoStatusSchema>;
/**
* 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<typeof TodoItemSchema>;
/**
* Todo List Schema (存储在单个文件中)
*/
export const TodoListSchema = z.object({
sessionId: z.string(),
items: z.array(TodoItemSchema),
updatedAt: z.number(),
});
export type TodoList = z.infer<typeof TodoListSchema>;
/**
* 获取会话的 Todo 列表
*/
export async function get(sessionId: string): Promise<TodoList | null> {
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<TodoList> {
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<void> {
todoList.updatedAt = Date.now();
await base.write(['todo', todoList.sessionId], todoList);
}
/**
* 添加 Todo 项
*/
export async function add(sessionId: string, content: string): Promise<TodoItem> {
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<TodoItem[]> {
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<TodoItem | null> {
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<TodoItem | null> {
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<boolean> {
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<void> {
const todoList = await get(sessionId);
if (!todoList) return;
todoList.items = [];
await save(todoList);
}
/**
* 删除会话的 Todo 列表
*/
export async function removeBySession(sessionId: string): Promise<void> {
await base.remove(['todo', sessionId]);
}
/**
* 替换整个 Todo 列表
*/
export async function replace(sessionId: string, items: Omit<TodoItem, 'id' | 'createdAt' | 'updatedAt'>[]): Promise<TodoList> {
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;
}
-110
View File
@@ -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<SessionMetadata, 'messageCount'> {
/** 对话历史 */
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;
}
+24 -2
View File
@@ -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);
}
/**
+1 -1
View File
@@ -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';
/**
+2 -11
View File
@@ -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({
+52 -76
View File
@@ -41,15 +41,16 @@ interface SessionData {
todos: unknown[];
}
interface SessionStorageInterface {
ensureDir(): Promise<void>;
generateSessionId(): string;
getOrCreateProject(workdir: string): Promise<ProjectMetadata>;
interface SessionManagerInterface {
init(workdir: string): Promise<SessionData>;
getSession(): SessionData | null;
getProject(): ProjectMetadata | null;
listSessions(): Promise<SessionSummary[]>;
listAllSessions(): Promise<SessionSummary[]>;
listSessionsByProject(projectId: string): Promise<SessionSummary[]>;
loadSession(projectId: string, sessionId: string): Promise<SessionData | null>;
saveSession(session: SessionData, lastSyncedCount?: number): Promise<void>;
deleteSession(projectId: string, sessionId: string): Promise<boolean>;
deleteSession(sessionId: string): Promise<boolean>;
newSession(workdir?: string): Promise<SessionData>;
restoreSession(sessionId: string): Promise<SessionData | null>;
getSessionId(): string | undefined;
}
// ============================================================================
@@ -59,15 +60,15 @@ interface SessionStorageInterface {
export class SessionManager {
private sessions: Map<string, Session> = new Map();
private sessionProjects: Map<string, string> = 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<void> {
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<void> {
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<SessionData | null> {
if (!this.coreManager) return null;
// 先恢复会话
const data = await this.coreManager.restoreSession(sessionId);
return data;
}
}
// 单例