Files
metona-ai-desktop/electron/services/session.service.ts
T
thzxx f4532a2bb2 refactor: 移除多余依赖,统一 MUI 为唯一 UI 库
- 移除 radix-ui、clsx、tailwind-merge、class-variance-authority、react-router-dom

- 前后端全面适配 MUI 组件,清理残留 Tailwind 工具类引用

- 优化 Agent Loop 引擎、IPC 处理器、Provider Adapter 等后端模块

- 新增 Agent 网络工具通用设计文档 v2
2026-07-05 19:15:48 +08:00

338 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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((row) => this.toSessionInfo(row));
}
/**
* 创建新会话
*/
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((row) => this.toMessageInfo(row));
}
/**
* 保存一条消息
*/
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 ? this.safeJsonParse(row.tool_calls) as unknown[] : undefined,
toolResult: row.tool_result ? this.safeJsonParse(row.tool_result) : undefined,
attachments: row.attachments ? this.safeJsonParse(row.attachments) as unknown[] : undefined,
iteration: row.iteration ?? undefined,
timestamp: row.created_at,
};
}
/**
* 安全 JSON 解析:解析失败时返回 undefined 而非抛出异常
*/
private safeJsonParse(json: string): unknown {
try {
return JSON.parse(json);
} catch {
return undefined;
}
}
}