feat(server): 添加 session 持久化支持
- 复用 core 包的 SessionStorage 实现文件持久化 - sessions 保存到 ~/.local/share/ai-assist/sessions/ - 服务启动时自动加载已持久化的 sessions - create/addMessage/delete 操作自动同步到文件
This commit is contained in:
@@ -23,6 +23,15 @@ import {
|
|||||||
createMCPToolAdapter,
|
createMCPToolAdapter,
|
||||||
} from './mcp/index.js';
|
} 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();
|
const program = new Command();
|
||||||
|
|
||||||
// MCP 管理器实例
|
// MCP 管理器实例
|
||||||
|
|||||||
@@ -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',
|
role: 'assistant',
|
||||||
content: 'Agent core module not available. Please build @ai-assistant/core first.',
|
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',
|
role: 'assistant',
|
||||||
content: response,
|
content: response,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -165,6 +165,10 @@ export function createServer(options: ServerOptions = {}) {
|
|||||||
* 初始化服务器(加载 core 模块等)
|
* 初始化服务器(加载 core 模块等)
|
||||||
*/
|
*/
|
||||||
export async function initServer(options: ServerOptions = {}): Promise<void> {
|
export async function initServer(options: ServerOptions = {}): Promise<void> {
|
||||||
|
// 初始化 SessionManager(加载持久化的 sessions)
|
||||||
|
const sessionManager = getSessionManager();
|
||||||
|
await sessionManager.init();
|
||||||
|
|
||||||
// 尝试加载 core 模块
|
// 尝试加载 core 模块
|
||||||
const coreLoaded = await initCore();
|
const coreLoaded = await initCore();
|
||||||
if (coreLoaded) {
|
if (coreLoaded) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ sessionsRouter.post('/', async (c) => {
|
|||||||
try {
|
try {
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const input = CreateSessionInputSchema.parse(body);
|
const input = CreateSessionInputSchema.parse(body);
|
||||||
const session = sessionManager.create(input);
|
const session = await sessionManager.create(input);
|
||||||
|
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
@@ -76,7 +76,7 @@ sessionsRouter.get('/:id', (c) => {
|
|||||||
/**
|
/**
|
||||||
* DELETE /sessions/:id - 删除会话
|
* DELETE /sessions/:id - 删除会话
|
||||||
*/
|
*/
|
||||||
sessionsRouter.delete('/:id', (c) => {
|
sessionsRouter.delete('/:id', async (c) => {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
|
|
||||||
if (!sessionManager.exists(id)) {
|
if (!sessionManager.exists(id)) {
|
||||||
@@ -89,7 +89,7 @@ sessionsRouter.delete('/:id', (c) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionManager.delete(id);
|
await sessionManager.delete(id);
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -144,7 +144,7 @@ sessionsRouter.post('/:id/messages', async (c) => {
|
|||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const input = SendMessageInputSchema.parse(body);
|
const input = SendMessageInputSchema.parse(body);
|
||||||
|
|
||||||
const message = sessionManager.addMessage(id, {
|
const message = await sessionManager.addMessage(id, {
|
||||||
role: input.role,
|
role: input.role,
|
||||||
content: input.content,
|
content: input.content,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +1,160 @@
|
|||||||
/**
|
/**
|
||||||
* Session Manager
|
* Session Manager
|
||||||
*
|
*
|
||||||
* 管理所有活跃的会话
|
* 管理所有活跃的会话,支持文件持久化
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import type { Session, CreateSessionInput, Message, SessionStatus } from '../types.js';
|
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 {
|
export class SessionManager {
|
||||||
private sessions: Map<string, Session> = new Map();
|
private sessions: Map<string, Session> = new Map();
|
||||||
private messages: Map<string, Message[]> = 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 now = new Date().toISOString();
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
id: uuidv4(),
|
id: this.storage?.generateSessionId() || uuidv4(),
|
||||||
name: input.name,
|
name: input.name,
|
||||||
workdir: input.workdir || process.cwd(),
|
workdir: input.workdir || process.cwd(),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
@@ -29,6 +166,9 @@ export class SessionManager {
|
|||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
this.messages.set(session.id, []);
|
this.messages.set(session.id, []);
|
||||||
|
|
||||||
|
// 持久化
|
||||||
|
await this.persist(session.id);
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,9 +191,16 @@ export class SessionManager {
|
|||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
*/
|
*/
|
||||||
delete(id: string): boolean {
|
async delete(id: string): Promise<boolean> {
|
||||||
this.messages.delete(id);
|
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);
|
const session = this.sessions.get(sessionId);
|
||||||
if (!session) return undefined;
|
if (!session) return undefined;
|
||||||
|
|
||||||
@@ -97,6 +247,9 @@ export class SessionManager {
|
|||||||
session.messageCount = messages.length;
|
session.messageCount = messages.length;
|
||||||
session.updatedAt = new Date().toISOString();
|
session.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
// 持久化
|
||||||
|
await this.persist(sessionId);
|
||||||
|
|
||||||
return fullMessage;
|
return fullMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user