refactor: 数据库层从 better-sqlite3 迁移到 sql.js (WASM)
- 替换原生依赖 better-sqlite3 为纯 WASM 的 sql.js,消除 Electron ABI 版本不匹配问题 - 新增 sql.js 兼容层:runExec/queryOne/queryAll/runPragma/runTransaction - initDatabase() 改为异步(WASM 加载需要) - 每次写操作后自动 persist() 到磁盘 - electron-builder 配置:extraResources 打包 sql-wasm.wasm - 移除 @electron/rebuild 依赖和 postinstall 脚本 - 设置 npmRebuild: false(不再需要原生模块重编译)
This commit is contained in:
+9
-7
@@ -14,8 +14,7 @@
|
|||||||
"start": "npm run build && electron . --no-sandbox",
|
"start": "npm run build && electron . --no-sandbox",
|
||||||
"pack": "npm run build && electron-builder --dir",
|
"pack": "npm run build && electron-builder --dir",
|
||||||
"dist": "npm run build && electron-builder --win",
|
"dist": "npm run build && electron-builder --win",
|
||||||
"dist:nsis": "npm run build && electron-builder --win nsis",
|
"dist:nsis": "npm run build && electron-builder --win nsis"
|
||||||
"postinstall": "electron-rebuild --version 33.4.11 || true"
|
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.metona.ollama-desktop",
|
"appId": "com.metona.ollama-desktop",
|
||||||
@@ -23,13 +22,18 @@
|
|||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
"npmRebuild": true,
|
"npmRebuild": false,
|
||||||
"buildDependenciesFromSource": true,
|
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"!node_modules/**/*"
|
"!node_modules/**/*"
|
||||||
],
|
],
|
||||||
|
"extraResources": [
|
||||||
|
{
|
||||||
|
"from": "node_modules/sql.js/dist/sql-wasm.wasm",
|
||||||
|
"to": "sql-wasm.wasm"
|
||||||
|
}
|
||||||
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
@@ -54,7 +58,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/rebuild": "^3.7.1",
|
|
||||||
"@types/node": "^20.17.0",
|
"@types/node": "^20.17.0",
|
||||||
"electron": "^33.4.11",
|
"electron": "^33.4.11",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
@@ -62,7 +65,6 @@
|
|||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"sql.js": "^1.11.0"
|
||||||
"better-sqlite3": "^12.9.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+170
-79
@@ -1,34 +1,123 @@
|
|||||||
/**
|
/**
|
||||||
* Metona Ollama Desktop - SQLite 数据库层
|
* Metona Ollama Desktop - SQLite 数据库层
|
||||||
* v4.0: 替代 IndexedDB,使用 better-sqlite3
|
* v4.1: 替代 better-sqlite3,使用 sql.js (WASM) 无需原生编译
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Database from 'better-sqlite3';
|
import * as SQL from 'sql.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
|
||||||
let db: Database.Database | null = null;
|
// ─── sql.js 兼容层 ───
|
||||||
|
// 封装 sql.js 的 API,提供与 better-sqlite3 相近的接口
|
||||||
|
|
||||||
|
interface Row { [key: string]: unknown }
|
||||||
|
|
||||||
|
function runExec(db: SQL.Database, sql: string, params?: unknown[]): void {
|
||||||
|
if (params && params.length) {
|
||||||
|
const stmt = db.prepare(sql);
|
||||||
|
stmt.bind(params as SQL.BindParams);
|
||||||
|
stmt.step();
|
||||||
|
stmt.free();
|
||||||
|
} else {
|
||||||
|
db.run(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryOne(db: SQL.Database, sql: string, params?: unknown[]): Row | null {
|
||||||
|
const stmt = db.prepare(sql);
|
||||||
|
if (params && params.length) stmt.bind(params as SQL.BindParams);
|
||||||
|
if (stmt.step()) {
|
||||||
|
const cols = stmt.getColumnNames();
|
||||||
|
const vals = stmt.get();
|
||||||
|
const row: Row = {};
|
||||||
|
cols.forEach((c, i) => { row[c] = vals[i]; });
|
||||||
|
stmt.free();
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
stmt.free();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryAll(db: SQL.Database, sql: string, params?: unknown[]): Row[] {
|
||||||
|
const stmt = db.prepare(sql);
|
||||||
|
if (params && params.length) stmt.bind(params as SQL.BindParams);
|
||||||
|
const cols = stmt.getColumnNames();
|
||||||
|
const rows: Row[] = [];
|
||||||
|
while (stmt.step()) {
|
||||||
|
const vals = stmt.get();
|
||||||
|
const row: Row = {};
|
||||||
|
cols.forEach((c, i) => { row[c] = vals[i]; });
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
stmt.free();
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPragma(db: SQL.Database, expr: string): void {
|
||||||
|
db.run(`PRAGMA ${expr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTransaction(db: SQL.Database, fn: () => void): void {
|
||||||
|
db.run('BEGIN TRANSACTION');
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
db.run('COMMIT');
|
||||||
|
} catch (err) {
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 数据库实例 ───
|
||||||
|
|
||||||
|
let db: SQL.Database | null = null;
|
||||||
|
let dbPath: string | null = null;
|
||||||
|
|
||||||
/** 获取数据库实例 */
|
/** 获取数据库实例 */
|
||||||
export function getDb(): Database.Database {
|
export function getDb(): SQL.Database {
|
||||||
if (!db) throw new Error('数据库未初始化,请先调用 initDatabase()');
|
if (!db) throw new Error('数据库未初始化,请先调用 initDatabase()');
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化数据库 */
|
/** 持久化数据库到磁盘 */
|
||||||
export function initDatabase(): Database.Database {
|
function persist(): void {
|
||||||
|
if (!db || !dbPath) return;
|
||||||
|
try {
|
||||||
|
const data = db.export();
|
||||||
|
const buf = Buffer.from(data);
|
||||||
|
fs.writeFileSync(dbPath, buf);
|
||||||
|
} catch { /* 静默失败,不阻断主流程 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化数据库(异步,需加载 WASM) */
|
||||||
|
export async function initDatabase(): Promise<SQL.Database> {
|
||||||
if (db) return db;
|
if (db) return db;
|
||||||
|
|
||||||
const dbPath = path.join(app.getPath('userData'), 'metona.db');
|
// 定位 sql-wasm.wasm 文件(开发时在 node_modules,打包后在 resources)
|
||||||
db = new Database(dbPath);
|
const wasmPath = app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, 'sql-wasm.wasm')
|
||||||
|
: path.join(app.getAppPath(), 'node_modules/sql.js/dist/sql-wasm.wasm');
|
||||||
|
|
||||||
|
const SQLJS = await SQL.default({
|
||||||
|
locateFile: () => wasmPath,
|
||||||
|
});
|
||||||
|
dbPath = path.join(app.getPath('userData'), 'metona.db');
|
||||||
|
|
||||||
|
// 从磁盘加载已有数据库,或创建新的
|
||||||
|
let data: Uint8Array | undefined;
|
||||||
|
if (fs.existsSync(dbPath)) {
|
||||||
|
data = fs.readFileSync(dbPath);
|
||||||
|
}
|
||||||
|
db = new SQLJS.Database(data);
|
||||||
|
|
||||||
// 性能优化
|
// 性能优化
|
||||||
db.pragma('journal_mode = WAL');
|
runPragma(db, 'journal_mode = WAL');
|
||||||
db.pragma('synchronous = NORMAL');
|
runPragma(db, 'synchronous = NORMAL');
|
||||||
db.pragma('foreign_keys = ON');
|
runPragma(db, 'foreign_keys = ON');
|
||||||
|
|
||||||
// 创建表
|
// 创建表
|
||||||
db.exec(`
|
db.run(`
|
||||||
-- 会话表
|
-- 会话表
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -122,10 +211,13 @@ export function initDatabase(): Database.Database {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_traces_session ON traces(session_id, created_at);
|
CREATE INDEX IF NOT EXISTS idx_traces_session ON traces(session_id, created_at);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// 写入一次确保文件存在
|
||||||
|
persist();
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Sessions CRUD ───
|
// ─── 类型定义 ───
|
||||||
|
|
||||||
export interface SessionRow {
|
export interface SessionRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -197,103 +289,104 @@ export interface TraceRow {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Sessions CRUD ───
|
||||||
|
|
||||||
export function saveSession(session: SessionRow): string {
|
export function saveSession(session: SessionRow): string {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
d.prepare(`INSERT OR REPLACE INTO sessions (id, title, model, system_prompt, parent_id, status, created_at, updated_at)
|
runExec(d, `INSERT OR REPLACE INTO sessions (id, title, model, system_prompt, parent_id, status, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
session.id, session.title, session.model, session.system_prompt, session.parent_id,
|
[session.id, session.title, session.model, session.system_prompt, session.parent_id,
|
||||||
session.status || 'active', session.created_at, session.updated_at
|
session.status || 'active', session.created_at, session.updated_at]
|
||||||
);
|
);
|
||||||
|
persist();
|
||||||
return session.id;
|
return session.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSession(id: string): SessionRow | null {
|
export function getSession(id: string): SessionRow | null {
|
||||||
const d = getDb();
|
return queryOne(getDb(), 'SELECT * FROM sessions WHERE id = ?', [id]) as SessionRow | null;
|
||||||
return d.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as SessionRow | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllSessions(): SessionRow[] {
|
export function getAllSessions(): SessionRow[] {
|
||||||
const d = getDb();
|
return queryAll(getDb(), 'SELECT * FROM sessions ORDER BY updated_at DESC') as SessionRow[];
|
||||||
return d.prepare('SELECT * FROM sessions ORDER BY updated_at DESC').all() as SessionRow[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteSession(id: string): void {
|
export function deleteSession(id: string): void {
|
||||||
const d = getDb();
|
runExec(getDb(), 'DELETE FROM sessions WHERE id = ?', [id]);
|
||||||
d.prepare('DELETE FROM sessions WHERE id = ?').run(id);
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAllSessions(): void {
|
export function clearAllSessions(): void {
|
||||||
const d = getDb();
|
getDb().run('DELETE FROM sessions');
|
||||||
d.exec('DELETE FROM sessions');
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Messages CRUD ───
|
// ─── Messages CRUD ───
|
||||||
|
|
||||||
export function saveMessage(msg: MessageRow): string {
|
export function saveMessage(msg: MessageRow): string {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
d.prepare(`INSERT OR REPLACE INTO messages (id, session_id, role, content, thinking, images, tool_calls, tool_name, eval_count, total_duration, created_at)
|
runExec(d, `INSERT OR REPLACE INTO messages (id, session_id, role, content, thinking, images, tool_calls, tool_name, eval_count, total_duration, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
msg.id, msg.session_id, msg.role, msg.content, msg.thinking, msg.images,
|
[msg.id, msg.session_id, msg.role, msg.content, msg.thinking, msg.images,
|
||||||
msg.tool_calls, msg.tool_name, msg.eval_count, msg.total_duration, msg.created_at
|
msg.tool_calls, msg.tool_name, msg.eval_count, msg.total_duration, msg.created_at]
|
||||||
);
|
);
|
||||||
|
persist();
|
||||||
return msg.id;
|
return msg.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMessagesBySession(sessionId: string): MessageRow[] {
|
export function getMessagesBySession(sessionId: string): MessageRow[] {
|
||||||
const d = getDb();
|
return queryAll(getDb(), 'SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC', [sessionId]) as MessageRow[];
|
||||||
return d.prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC').all(sessionId) as MessageRow[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteMessagesBySession(sessionId: string): void {
|
export function deleteMessagesBySession(sessionId: string): void {
|
||||||
const d = getDb();
|
runExec(getDb(), 'DELETE FROM messages WHERE session_id = ?', [sessionId]);
|
||||||
d.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Memories CRUD ───
|
// ─── Memories CRUD ───
|
||||||
|
|
||||||
export function saveMemory(entry: MemoryRow): string {
|
export function saveMemory(entry: MemoryRow): string {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
d.prepare(`INSERT OR REPLACE INTO memories (id, type, content, importance, tags, source, session_id, use_count, embedding, created_at, updated_at, last_used_at)
|
runExec(d, `INSERT OR REPLACE INTO memories (id, type, content, importance, tags, source, session_id, use_count, embedding, created_at, updated_at, last_used_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
entry.id, entry.type, entry.content, entry.importance, entry.tags,
|
[entry.id, entry.type, entry.content, entry.importance, entry.tags,
|
||||||
entry.source, entry.session_id, entry.use_count, entry.embedding,
|
entry.source, entry.session_id, entry.use_count, entry.embedding,
|
||||||
entry.created_at, entry.updated_at, entry.last_used_at
|
entry.created_at, entry.updated_at, entry.last_used_at]
|
||||||
);
|
);
|
||||||
// FTS 同步
|
// FTS 同步
|
||||||
try {
|
try {
|
||||||
d.prepare("DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)").run(entry.id);
|
runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [entry.id]);
|
||||||
d.prepare("INSERT INTO memories_fts(rowid, content, tags, type) SELECT rowid, content, tags, type FROM memories WHERE id = ?").run(entry.id);
|
runExec(d, "INSERT INTO memories_fts(rowid, content, tags, type) SELECT rowid, content, tags, type FROM memories WHERE id = ?", [entry.id]);
|
||||||
} catch { /* FTS 同步失败不影响主流程 */ }
|
} catch { /* FTS 同步失败不影响主流程 */ }
|
||||||
|
persist();
|
||||||
return entry.id;
|
return entry.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMemory(id: string): MemoryRow | null {
|
export function getMemory(id: string): MemoryRow | null {
|
||||||
const d = getDb();
|
return queryOne(getDb(), 'SELECT * FROM memories WHERE id = ?', [id]) as MemoryRow | null;
|
||||||
return d.prepare('SELECT * FROM memories WHERE id = ?').get(id) as MemoryRow | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllMemories(): MemoryRow[] {
|
export function getAllMemories(): MemoryRow[] {
|
||||||
const d = getDb();
|
return queryAll(getDb(), 'SELECT * FROM memories ORDER BY importance DESC, updated_at DESC') as MemoryRow[];
|
||||||
return d.prepare('SELECT * FROM memories ORDER BY importance DESC, updated_at DESC').all() as MemoryRow[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMemoriesByType(type: string): MemoryRow[] {
|
export function getMemoriesByType(type: string): MemoryRow[] {
|
||||||
const d = getDb();
|
return queryAll(getDb(), 'SELECT * FROM memories WHERE type = ? ORDER BY importance DESC', [type]) as MemoryRow[];
|
||||||
return d.prepare('SELECT * FROM memories WHERE type = ? ORDER BY importance DESC').all(type) as MemoryRow[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteMemory(id: string): void {
|
export function deleteMemory(id: string): void {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
try {
|
try {
|
||||||
d.prepare("DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)").run(id);
|
runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [id]);
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
d.prepare('DELETE FROM memories WHERE id = ?').run(id);
|
runExec(d, 'DELETE FROM memories WHERE id = ?', [id]);
|
||||||
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAllMemories(): void {
|
export function clearAllMemories(): void {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
d.exec('DELETE FROM memories');
|
d.run('DELETE FROM memories');
|
||||||
try { d.exec('DELETE FROM memories_fts'); } catch { /* ignore */ }
|
try { d.run('DELETE FROM memories_fts'); } catch { /* ignore */ }
|
||||||
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchMemoriesFTS(query: string, limit = 10): MemoryRow[] {
|
export function searchMemoriesFTS(query: string, limit = 10): MemoryRow[] {
|
||||||
@@ -302,36 +395,35 @@ export function searchMemoriesFTS(query: string, limit = 10): MemoryRow[] {
|
|||||||
// FTS5 搜索
|
// FTS5 搜索
|
||||||
const ftsQuery = query.replace(/[^\w\u4e00-\u9fff\s]/g, ' ').split(/\s+/).filter(w => w.length > 0).map(w => `"${w}"*`).join(' OR ');
|
const ftsQuery = query.replace(/[^\w\u4e00-\u9fff\s]/g, ' ').split(/\s+/).filter(w => w.length > 0).map(w => `"${w}"*`).join(' OR ');
|
||||||
if (!ftsQuery) return [];
|
if (!ftsQuery) return [];
|
||||||
return d.prepare(`
|
return queryAll(d, `
|
||||||
SELECT m.* FROM memories m
|
SELECT m.* FROM memories m
|
||||||
JOIN memories_fts f ON m.rowid = f.rowid
|
JOIN memories_fts f ON m.rowid = f.rowid
|
||||||
WHERE memories_fts MATCH ?
|
WHERE memories_fts MATCH ?
|
||||||
ORDER BY rank
|
ORDER BY rank
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(ftsQuery, limit) as MemoryRow[];
|
`, [ftsQuery, limit]) as MemoryRow[];
|
||||||
} catch {
|
} catch {
|
||||||
// FTS 失败回退到 LIKE 搜索
|
// FTS 失败回退到 LIKE 搜索
|
||||||
return d.prepare(`
|
return queryAll(d, `
|
||||||
SELECT * FROM memories
|
SELECT * FROM memories
|
||||||
WHERE content LIKE ? OR tags LIKE ?
|
WHERE content LIKE ? OR tags LIKE ?
|
||||||
ORDER BY importance DESC
|
ORDER BY importance DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(`%${query}%`, `%${query}%`, limit) as MemoryRow[];
|
`, [`%${query}%`, `%${query}%`, limit]) as MemoryRow[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Settings CRUD ───
|
// ─── Settings CRUD ───
|
||||||
|
|
||||||
export function saveSetting(key: string, value: unknown): void {
|
export function saveSetting(key: string, value: unknown): void {
|
||||||
const d = getDb();
|
runExec(getDb(), 'INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)',
|
||||||
d.prepare('INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)').run(
|
[key, JSON.stringify(value), Date.now()]
|
||||||
key, JSON.stringify(value), Date.now()
|
|
||||||
);
|
);
|
||||||
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSetting<T = unknown>(key: string, defaultValue: T | null = null): T {
|
export function getSetting<T = unknown>(key: string, defaultValue: T | null = null): T {
|
||||||
const d = getDb();
|
const row = queryOne(getDb(), 'SELECT value FROM settings WHERE key = ?', [key]) as { value: string } | null;
|
||||||
const row = d.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
|
|
||||||
if (!row) return defaultValue as T;
|
if (!row) return defaultValue as T;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(row.value) as T;
|
return JSON.parse(row.value) as T;
|
||||||
@@ -343,33 +435,31 @@ export function getSetting<T = unknown>(key: string, defaultValue: T | null = nu
|
|||||||
// ─── Tool Calls CRUD ───
|
// ─── Tool Calls CRUD ───
|
||||||
|
|
||||||
export function saveToolCall(tc: ToolCallRow): string {
|
export function saveToolCall(tc: ToolCallRow): string {
|
||||||
const d = getDb();
|
runExec(getDb(), `INSERT OR REPLACE INTO tool_calls (id, message_id, session_id, tool_name, arguments, result, status, duration_ms, created_at)
|
||||||
d.prepare(`INSERT OR REPLACE INTO tool_calls (id, message_id, session_id, tool_name, arguments, result, status, duration_ms, created_at)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
[tc.id, tc.message_id, tc.session_id, tc.tool_name, tc.arguments, tc.result, tc.status, tc.duration_ms, tc.created_at]
|
||||||
tc.id, tc.message_id, tc.session_id, tc.tool_name, tc.arguments, tc.result, tc.status, tc.duration_ms, tc.created_at
|
|
||||||
);
|
);
|
||||||
|
persist();
|
||||||
return tc.id;
|
return tc.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getToolCallsBySession(sessionId: string): ToolCallRow[] {
|
export function getToolCallsBySession(sessionId: string): ToolCallRow[] {
|
||||||
const d = getDb();
|
return queryAll(getDb(), 'SELECT * FROM tool_calls WHERE session_id = ? ORDER BY created_at ASC', [sessionId]) as ToolCallRow[];
|
||||||
return d.prepare('SELECT * FROM tool_calls WHERE session_id = ? ORDER BY created_at ASC').all(sessionId) as ToolCallRow[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Traces CRUD ───
|
// ─── Traces CRUD ───
|
||||||
|
|
||||||
export function saveTrace(trace: TraceRow): string {
|
export function saveTrace(trace: TraceRow): string {
|
||||||
const d = getDb();
|
runExec(getDb(), `INSERT OR REPLACE INTO traces (id, session_id, step_index, thought, action, action_input, observation, loop_count, created_at)
|
||||||
d.prepare(`INSERT OR REPLACE INTO traces (id, session_id, step_index, thought, action, action_input, observation, loop_count, created_at)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
[trace.id, trace.session_id, trace.step_index, trace.thought, trace.action, trace.action_input, trace.observation, trace.loop_count, trace.created_at]
|
||||||
trace.id, trace.session_id, trace.step_index, trace.thought, trace.action, trace.action_input, trace.observation, trace.loop_count, trace.created_at
|
|
||||||
);
|
);
|
||||||
|
persist();
|
||||||
return trace.id;
|
return trace.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTracesBySession(sessionId: string): TraceRow[] {
|
export function getTracesBySession(sessionId: string): TraceRow[] {
|
||||||
const d = getDb();
|
return queryAll(getDb(), 'SELECT * FROM traces WHERE session_id = ? ORDER BY step_index ASC', [sessionId]) as TraceRow[];
|
||||||
return d.prepare('SELECT * FROM traces WHERE session_id = ? ORDER BY step_index ASC').all(sessionId) as TraceRow[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Export/Import ───
|
// ─── Export/Import ───
|
||||||
@@ -384,10 +474,10 @@ export interface ExportData {
|
|||||||
|
|
||||||
export function exportAllSessions(): ExportData {
|
export function exportAllSessions(): ExportData {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
const sessions = d.prepare('SELECT * FROM sessions').all() as SessionRow[];
|
const sessions = queryAll(d, 'SELECT * FROM sessions') as SessionRow[];
|
||||||
const messages = d.prepare('SELECT * FROM messages').all() as MessageRow[];
|
const messages = queryAll(d, 'SELECT * FROM messages') as MessageRow[];
|
||||||
const memories = d.prepare('SELECT * FROM memories').all() as MemoryRow[];
|
const memories = queryAll(d, 'SELECT * FROM memories') as MemoryRow[];
|
||||||
const settingsRows = d.prepare('SELECT * FROM settings').all() as SettingRow[];
|
const settingsRows = queryAll(d, 'SELECT * FROM settings') as SettingRow[];
|
||||||
const settings = settingsRows.map(r => ({ key: r.key, value: (() => { try { return JSON.parse(r.value); } catch { return r.value; } })() }));
|
const settings = settingsRows.map(r => ({ key: r.key, value: (() => { try { return JSON.parse(r.value); } catch { return r.value; } })() }));
|
||||||
return { sessions, messages, memories, settings, exportedAt: Date.now() };
|
return { sessions, messages, memories, settings, exportedAt: Date.now() };
|
||||||
}
|
}
|
||||||
@@ -397,27 +487,27 @@ export function importSessions(data: ExportData): { imported: number; skipped: n
|
|||||||
let imported = 0;
|
let imported = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
|
|
||||||
const txn = d.transaction(() => {
|
runTransaction(d, () => {
|
||||||
for (const session of data.sessions) {
|
for (const session of data.sessions) {
|
||||||
const existing = d.prepare('SELECT id FROM sessions WHERE id = ?').get(session.id);
|
const existing = queryOne(d, 'SELECT id FROM sessions WHERE id = ?', [session.id]);
|
||||||
if (existing) { skipped++; continue; }
|
if (existing) { skipped++; continue; }
|
||||||
saveSession(session);
|
saveSession(session);
|
||||||
imported++;
|
imported++;
|
||||||
}
|
}
|
||||||
for (const msg of data.messages) {
|
for (const msg of data.messages) {
|
||||||
const sessionExists = d.prepare('SELECT id FROM sessions WHERE id = ?').get(msg.session_id);
|
const sessionExists = queryOne(d, 'SELECT id FROM sessions WHERE id = ?', [msg.session_id]);
|
||||||
if (sessionExists) saveMessage(msg);
|
if (sessionExists) saveMessage(msg);
|
||||||
}
|
}
|
||||||
for (const mem of data.memories) {
|
for (const mem of data.memories) {
|
||||||
const existing = d.prepare('SELECT id FROM memories WHERE id = ?').get(mem.id);
|
const existing = queryOne(d, 'SELECT id FROM memories WHERE id = ?', [mem.id]);
|
||||||
if (!existing) saveMemory(mem);
|
if (!existing) saveMemory(mem);
|
||||||
}
|
}
|
||||||
for (const s of data.settings) {
|
for (const s of data.settings) {
|
||||||
saveSetting(s.key, s.value);
|
saveSetting(s.key, s.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
txn();
|
|
||||||
|
|
||||||
|
persist();
|
||||||
return { imported, skipped };
|
return { imported, skipped };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +515,7 @@ export function importSessions(data: ExportData): { imported: number; skipped: n
|
|||||||
|
|
||||||
export function closeDatabase(): void {
|
export function closeDatabase(): void {
|
||||||
if (db) {
|
if (db) {
|
||||||
|
persist();
|
||||||
db.close();
|
db.close();
|
||||||
db = null;
|
db = null;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -76,7 +76,7 @@ function summarizeResult(toolName: string, result: Record<string, unknown>): str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupIPC(): void {
|
export async function setupIPC(): Promise<void> {
|
||||||
ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => {
|
ipcMain.handle('dialog:openFile', async (_, options?: { filters?: Array<{ name: string; extensions: string[] }> }) => {
|
||||||
const result = await dialog.showOpenDialog(mainWindow!, {
|
const result = await dialog.showOpenDialog(mainWindow!, {
|
||||||
properties: ['openFile', 'multiSelections'],
|
properties: ['openFile', 'multiSelections'],
|
||||||
@@ -267,7 +267,7 @@ export function setupIPC(): void {
|
|||||||
// ══════════════════════════════════════════════
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
// 初始化数据库
|
// 初始化数据库
|
||||||
initDatabase();
|
await initDatabase();
|
||||||
sendLog('success', '📦 SQLite 数据库已初始化');
|
sendLog('success', '📦 SQLite 数据库已初始化');
|
||||||
|
|
||||||
// Sessions
|
// Sessions
|
||||||
|
|||||||
+2
-2
@@ -143,9 +143,9 @@ if (!gotTheLock) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(async () => {
|
||||||
ensureWorkspaceDir();
|
ensureWorkspaceDir();
|
||||||
setupIPC();
|
await setupIPC();
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
createTray();
|
createTray();
|
||||||
createMenu();
|
createMenu();
|
||||||
|
|||||||
Reference in New Issue
Block a user