/** * Session Service — 会话 CRUD + 消息持久化 * * 管理会话的创建、查询、更新、删除,以及消息的存取。 * 所有操作通过 better-sqlite3 同步执行。 * * @see docs/MetonaAI-Desktop 架构与交互设计.html */ import { nanoid } from 'nanoid'; import type Database from 'better-sqlite3'; import log from 'electron-log'; // ===== 类型定义 ===== export interface SessionRow { id: string; title: string; created_at: number; updated_at: number; message_count: number; total_tokens: number; pinned: number; archived: number; metadata: string; } export interface MessageRow { id: string; session_id: string; role: string; content: string; reasoning_content: string | null; tool_calls: string | null; tool_result: string | null; attachments: string | null; iteration: number | null; created_at: number; } export interface SessionInfo { id: string; title: string; createdAt: number; updatedAt: number; messageCount: number; totalTokens: number; pinned: boolean; archived: boolean; } export interface MessageInfo { id: string; role: string; content: string; reasoningContent?: string; toolCalls?: unknown[]; toolResult?: unknown; attachments?: unknown[]; iteration?: number; timestamp: number; } // ===== 服务类 ===== export class SessionService { /** * 获取数据库实例 */ getDB(): Database.Database { return this.getDBFn(); } constructor(private getDBFn: () => Database.Database) {} /** * 列出所有会话 */ list(options?: { archived?: boolean }): SessionInfo[] { const db = this.getDBFn(); const archived = options?.archived ?? false; const rows = db.prepare(` SELECT * FROM sessions WHERE archived = ? ORDER BY pinned DESC, updated_at DESC `).all(archived ? 1 : 0) as SessionRow[]; return rows.map(this.toSessionInfo); } /** * 创建新会话 */ create(title?: string): SessionInfo { const db = this.getDBFn(); const id = `s_${nanoid(12)}`; const now = Date.now(); const sessionTitle = title ?? '新会话'; db.prepare(` INSERT INTO sessions (id, title, created_at, updated_at) VALUES (?, ?, ?, ?) `).run(id, sessionTitle, now, now); log.info(`Session created: ${id} (${sessionTitle})`); return { id, title: sessionTitle, createdAt: now, updatedAt: now, messageCount: 0, totalTokens: 0, pinned: false, archived: false, }; } /** * 重命名会话 */ rename(sessionId: string, title: string): boolean { const db = this.getDBFn(); const result = db.prepare(` UPDATE sessions SET title = ?, updated_at = ? WHERE id = ? `).run(title, Date.now(), sessionId); if (result.changes > 0) { log.info(`Session renamed: ${sessionId} → ${title}`); return true; } return false; } /** * 删除会话(级联删除消息) */ delete(sessionId: string): boolean { const db = this.getDBFn(); const result = db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId); if (result.changes > 0) { log.info(`Session deleted: ${sessionId}`); return true; } return false; } /** * 置顶/取消置顶 */ pin(sessionId: string, pinned: boolean): boolean { const db = this.getDBFn(); const result = db.prepare(` UPDATE sessions SET pinned = ?, updated_at = ? WHERE id = ? `).run(pinned ? 1 : 0, Date.now(), sessionId); return result.changes > 0; } /** * 归档/取消归档 */ archive(sessionId: string, archived: boolean): boolean { const db = this.getDBFn(); const result = db.prepare(` UPDATE sessions SET archived = ?, updated_at = ? WHERE id = ? `).run(archived ? 1 : 0, Date.now(), sessionId); return result.changes > 0; } /** * 获取会话消息列表 */ getMessages(sessionId: string): MessageInfo[] { const db = this.getDBFn(); const rows = db.prepare(` SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC `).all(sessionId) as MessageRow[]; return rows.map(this.toMessageInfo); } /** * 保存一条消息 */ saveMessage(params: { sessionId: string; role: string; content: string; reasoningContent?: string; toolCalls?: unknown[]; toolResult?: unknown; attachments?: unknown[]; iteration?: number; }): MessageInfo { const db = this.getDBFn(); const id = `msg_${nanoid(12)}`; const now = Date.now(); db.prepare(` INSERT INTO messages (id, session_id, role, content, reasoning_content, tool_calls, tool_result, attachments, iteration, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( id, params.sessionId, params.role, params.content, params.reasoningContent ?? null, params.toolCalls ? JSON.stringify(params.toolCalls) : null, params.toolResult ? JSON.stringify(params.toolResult) : null, params.attachments ? JSON.stringify(params.attachments) : null, params.iteration ?? null, now, ); // 更新会话的 updated_at 和 message_count db.prepare(` UPDATE sessions SET updated_at = ?, message_count = message_count + 1 WHERE id = ? `).run(now, params.sessionId); return { id, role: params.role, content: params.content, reasoningContent: params.reasoningContent, toolCalls: params.toolCalls, toolResult: params.toolResult, attachments: params.attachments, iteration: params.iteration, timestamp: now, }; } /** * 更新会话 Token 统计 */ updateTokenUsage(sessionId: string, tokens: number): void { const db = this.getDBFn(); db.prepare(` UPDATE sessions SET total_tokens = total_tokens + ? WHERE id = ? `).run(tokens, sessionId); } /** * 删除一条消息 */ deleteMessage(messageId: string): boolean { const db = this.getDBFn(); const result = db.prepare('DELETE FROM messages WHERE id = ?').run(messageId); return result.changes > 0; } /** * 清空会话所有消息 */ clearMessages(sessionId: string): boolean { const db = this.getDBFn(); const result = db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId); db.prepare('UPDATE sessions SET message_count = 0, updated_at = ? WHERE id = ?').run(Date.now(), sessionId); return result.changes > 0; } /** * 保存会话的 trace 步骤和 token 用量(存入 metadata JSON) */ saveTraceData(sessionId: string, data: { traceSteps: unknown[]; tokenUsage: unknown }): void { const db = this.getDBFn(); const metadata = JSON.stringify({ traceSteps: data.traceSteps, tokenUsage: data.tokenUsage }); db.prepare(` UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ? `).run(metadata, Date.now(), sessionId); } /** * 加载会话的 trace 步骤和 token 用量 */ getTraceData(sessionId: string): { traceSteps: unknown[]; tokenUsage: unknown } | null { const db = this.getDBFn(); const row = db.prepare('SELECT metadata FROM sessions WHERE id = ?').get(sessionId) as { metadata: string } | undefined; if (!row?.metadata) return null; try { const data = JSON.parse(row.metadata); if (data.traceSteps || data.tokenUsage) return data; return null; } catch { return null; } } // ===== 私有转换方法 ===== private toSessionInfo(row: SessionRow): SessionInfo { return { id: row.id, title: row.title, createdAt: row.created_at, updatedAt: row.updated_at, messageCount: row.message_count, totalTokens: row.total_tokens, pinned: row.pinned === 1, archived: row.archived === 1, }; } private toMessageInfo(row: MessageRow): MessageInfo { return { id: row.id, role: row.role, content: row.content, reasoningContent: row.reasoning_content ?? undefined, toolCalls: row.tool_calls ? JSON.parse(row.tool_calls) : undefined, toolResult: row.tool_result ? JSON.parse(row.tool_result) : undefined, attachments: row.attachments ? JSON.parse(row.attachments) : undefined, iteration: row.iteration ?? undefined, timestamp: row.created_at, }; } }