feat(server): 添加 session 持久化支持
- 复用 core 包的 SessionStorage 实现文件持久化 - sessions 保存到 ~/.local/share/ai-assist/sessions/ - 服务启动时自动加载已持久化的 sessions - create/addMessage/delete 操作自动同步到文件
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user