From 40afa10ed9280df0180dcf3bb5c2da9c1f2a61cc Mon Sep 17 00:00:00 2001 From: kurihada Date: Fri, 12 Dec 2025 15:27:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20=E6=B7=BB=E5=8A=A0=20session=20?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 复用 core 包的 SessionStorage 实现文件持久化 - sessions 保存到 ~/.local/share/ai-assist/sessions/ - 服务启动时自动加载已持久化的 sessions - create/addMessage/delete 操作自动同步到文件 --- packages/core/src/index.ts | 9 ++ packages/server/src/agent/adapter.ts | 4 +- packages/server/src/index.ts | 4 + packages/server/src/routes/sessions.ts | 8 +- packages/server/src/session/manager.ts | 165 ++++++++++++++++++++++++- 5 files changed, 178 insertions(+), 12 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 11fae24..1539541 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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 管理器实例 diff --git a/packages/server/src/agent/adapter.ts b/packages/server/src/agent/adapter.ts index dd986dc..f5635bf 100644 --- a/packages/server/src/agent/adapter.ts +++ b/packages/server/src/agent/adapter.ts @@ -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, }); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 597eb31..5e9cdaf 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -165,6 +165,10 @@ export function createServer(options: ServerOptions = {}) { * 初始化服务器(加载 core 模块等) */ export async function initServer(options: ServerOptions = {}): Promise { + // 初始化 SessionManager(加载持久化的 sessions) + const sessionManager = getSessionManager(); + await sessionManager.init(); + // 尝试加载 core 模块 const coreLoaded = await initCore(); if (coreLoaded) { diff --git a/packages/server/src/routes/sessions.ts b/packages/server/src/routes/sessions.ts index 0932a6b..8619882 100644 --- a/packages/server/src/routes/sessions.ts +++ b/packages/server/src/routes/sessions.ts @@ -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, }); diff --git a/packages/server/src/session/manager.ts b/packages/server/src/session/manager.ts index 73492bf..e357bc6 100644 --- a/packages/server/src/session/manager.ts +++ b/packages/server/src/session/manager.ts @@ -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; + generateSessionId(): string; + listSessions(): Promise>; + loadSession(sessionId: string): Promise; + saveSession(session: SessionData): Promise; + deleteSession(sessionId: string): Promise; +} + +// ============================================================================ +// 消息格式转换 +// ============================================================================ + +/** + * 将 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 = new Map(); private messages: Map = new Map(); + private storage: SessionStorageInterface | null = null; + private initialized = false; + + /** + * 初始化:加载 Core 模块的 SessionStorage 并恢复已有 sessions + */ + async init(): Promise { + 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 { + 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 { 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 { 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 | undefined { + async addMessage( + sessionId: string, + message: Omit + ): Promise { 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; }