feat(server): 添加 session 持久化支持

- 复用 core 包的 SessionStorage 实现文件持久化
- sessions 保存到 ~/.local/share/ai-assist/sessions/
- 服务启动时自动加载已持久化的 sessions
- create/addMessage/delete 操作自动同步到文件
This commit is contained in:
2025-12-12 15:27:16 +08:00
parent b17649930b
commit 40afa10ed9
5 changed files with 178 additions and 12 deletions
+9
View File
@@ -23,6 +23,15 @@ import {
createMCPToolAdapter,
} from './mcp/index.js';
// ============================================================================
// 库导出(供 server 等包使用)
// ============================================================================
export { Agent } from './core/agent.js';
export { toolRegistry } from './tools/index.js';
export { loadConfig } from './utils/config.js';
export { SessionStorage } from './session/storage.js';
export type { SessionData, SessionSummary } from './session/types.js';
const program = new Command();
// MCP 管理器实例
+2 -2
View File
@@ -148,7 +148,7 @@ export async function processMessage(sessionId: string, content: string): Promis
},
});
const assistantMessage = sessionManager.addMessage(sessionId, {
const assistantMessage = await sessionManager.addMessage(sessionId, {
role: 'assistant',
content: 'Agent core module not available. Please build @ai-assistant/core first.',
});
@@ -184,7 +184,7 @@ export async function processMessage(sessionId: string, content: string): Promis
});
// 保存助手消息
const assistantMessage = sessionManager.addMessage(sessionId, {
const assistantMessage = await sessionManager.addMessage(sessionId, {
role: 'assistant',
content: response,
});
+4
View File
@@ -165,6 +165,10 @@ export function createServer(options: ServerOptions = {}) {
* 初始化服务器(加载 core 模块等)
*/
export async function initServer(options: ServerOptions = {}): Promise<void> {
// 初始化 SessionManager(加载持久化的 sessions
const sessionManager = getSessionManager();
await sessionManager.init();
// 尝试加载 core 模块
const coreLoaded = await initCore();
if (coreLoaded) {
+4 -4
View File
@@ -30,7 +30,7 @@ sessionsRouter.post('/', async (c) => {
try {
const body = await c.req.json();
const input = CreateSessionInputSchema.parse(body);
const session = sessionManager.create(input);
const session = await sessionManager.create(input);
return c.json(
{
@@ -76,7 +76,7 @@ sessionsRouter.get('/:id', (c) => {
/**
* DELETE /sessions/:id - 删除会话
*/
sessionsRouter.delete('/:id', (c) => {
sessionsRouter.delete('/:id', async (c) => {
const id = c.req.param('id');
if (!sessionManager.exists(id)) {
@@ -89,7 +89,7 @@ sessionsRouter.delete('/:id', (c) => {
);
}
sessionManager.delete(id);
await sessionManager.delete(id);
return c.json({
success: true,
@@ -144,7 +144,7 @@ sessionsRouter.post('/:id/messages', async (c) => {
const body = await c.req.json();
const input = SendMessageInputSchema.parse(body);
const message = sessionManager.addMessage(id, {
const message = await sessionManager.addMessage(id, {
role: input.role,
content: input.content,
});
+159 -6
View File
@@ -1,23 +1,160 @@
/**
* Session Manager
*
* 管理所有活跃的会话
* 管理所有活跃的会话,支持文件持久化
*/
import { v4 as uuidv4 } from 'uuid';
import type { Session, CreateSessionInput, Message, SessionStatus } from '../types.js';
// ============================================================================
// Core 模块接口定义(避免构建时依赖)
// ============================================================================
interface SessionData {
id: string;
parentId?: string;
agentName?: string;
createdAt: string;
updatedAt: string;
workdir: string;
title?: string;
messages: Array<{ role: string; content: unknown }>;
discoveredTools: string[];
todos: unknown[];
}
interface SessionStorageInterface {
ensureDir(): Promise<void>;
generateSessionId(): string;
listSessions(): Promise<Array<{ id: string; title: string; workdir: string; messageCount: number; createdAt: string; updatedAt: string }>>;
loadSession(sessionId: string): Promise<SessionData | null>;
saveSession(session: SessionData): Promise<void>;
deleteSession(sessionId: string): Promise<boolean>;
}
// ============================================================================
// 消息格式转换
// ============================================================================
/**
* 将 Server Message 转换为 Core ModelMessage 格式
*/
function toModelMessage(msg: Message): { role: string; content: string } {
return { role: msg.role, content: msg.content };
}
/**
* 将 Core ModelMessage 转换为 Server Message 格式
*/
function fromModelMessage(
msg: { role: string; content: unknown },
sessionId: string,
index: number
): Message {
return {
id: uuidv4(),
sessionId,
role: msg.role as 'user' | 'assistant' | 'system' | 'tool',
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
createdAt: new Date().toISOString(),
};
}
// ============================================================================
// SessionManager 类
// ============================================================================
export class SessionManager {
private sessions: Map<string, Session> = new Map();
private messages: Map<string, Message[]> = new Map();
private storage: SessionStorageInterface | null = null;
private initialized = false;
/**
* 初始化:加载 Core 模块的 SessionStorage 并恢复已有 sessions
*/
async init(): Promise<void> {
if (this.initialized) return;
try {
// 动态导入 Core 模块,避免构建时依赖
const corePath = '@ai-assistant/core';
const core = await import(/* webpackIgnore: true */ corePath) as {
SessionStorage: new () => SessionStorageInterface;
};
this.storage = new core.SessionStorage();
await this.storage.ensureDir();
// 加载已持久化的 sessions
const summaries = await this.storage.listSessions();
console.log(`[SessionManager] Found ${summaries.length} persisted sessions`);
for (const summary of summaries) {
const sessionData = await this.storage.loadSession(summary.id);
if (!sessionData) continue;
// 转换为 Server Session 格式
const session: Session = {
id: sessionData.id,
name: sessionData.title,
workdir: sessionData.workdir,
createdAt: sessionData.createdAt,
updatedAt: sessionData.updatedAt,
status: 'idle',
messageCount: sessionData.messages.length,
};
this.sessions.set(session.id, session);
// 转换消息格式
const messages = sessionData.messages.map((msg, i) =>
fromModelMessage(msg, session.id, i)
);
this.messages.set(session.id, messages);
}
console.log(`[SessionManager] Loaded ${this.sessions.size} sessions from storage`);
} catch (error) {
console.warn('[SessionManager] Storage not available, using memory only:', error);
}
this.initialized = true;
}
/**
* 持久化单个 session
*/
private async persist(sessionId: string): Promise<void> {
if (!this.storage) return;
const session = this.sessions.get(sessionId);
const messages = this.messages.get(sessionId) || [];
if (!session) return;
const sessionData: SessionData = {
id: session.id,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
workdir: session.workdir,
title: session.name,
messages: messages.map(toModelMessage),
discoveredTools: [],
todos: [],
};
await this.storage.saveSession(sessionData);
}
/**
* 创建新会话
*/
create(input: CreateSessionInput = {}): Session {
async create(input: CreateSessionInput = {}): Promise<Session> {
const now = new Date().toISOString();
const session: Session = {
id: uuidv4(),
id: this.storage?.generateSessionId() || uuidv4(),
name: input.name,
workdir: input.workdir || process.cwd(),
createdAt: now,
@@ -29,6 +166,9 @@ export class SessionManager {
this.sessions.set(session.id, session);
this.messages.set(session.id, []);
// 持久化
await this.persist(session.id);
return session;
}
@@ -51,9 +191,16 @@ export class SessionManager {
/**
* 删除会话
*/
delete(id: string): boolean {
async delete(id: string): Promise<boolean> {
this.messages.delete(id);
return this.sessions.delete(id);
const deleted = this.sessions.delete(id);
// 从存储中删除
if (deleted && this.storage) {
await this.storage.deleteSession(id);
}
return deleted;
}
/**
@@ -78,7 +225,10 @@ export class SessionManager {
/**
* 添加消息
*/
addMessage(sessionId: string, message: Omit<Message, 'id' | 'sessionId' | 'createdAt'>): Message | undefined {
async addMessage(
sessionId: string,
message: Omit<Message, 'id' | 'sessionId' | 'createdAt'>
): Promise<Message | undefined> {
const session = this.sessions.get(sessionId);
if (!session) return undefined;
@@ -97,6 +247,9 @@ export class SessionManager {
session.messageCount = messages.length;
session.updatedAt = new Date().toISOString();
// 持久化
await this.persist(sessionId);
return fullMessage;
}