From 95153e91719fe2ac24fd4edb7456263c2ac9bd0e Mon Sep 17 00:00:00 2001 From: thzxx Date: Sat, 18 Apr 2026 07:42:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=93=E5=8C=85?= =?UTF-8?q?=E5=90=8E=E5=BA=94=E7=94=A8=E6=97=A0=E6=B3=95=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=EF=BC=88sql.js=20WASM=20=E4=B8=8D=E5=90=AB=20FTS5=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因分析: - sql.js 默认 WASM 构建不包含 FTS5 全文搜索扩展 - initDatabase() 中 CREATE VIRTUAL TABLE USING fts5 直接抛异常 - app.whenReady() 链没有 .catch(),错误被静默吞掉 - 结果:进程启动但窗口永远不显示(用户看到双击没反应) 修复内容: 1. main.ts: 添加 app.whenReady().catch() 错误处理,失败时弹出错误对话框 2. main.ts: 添加全局 uncaughtException/unhandledRejection 处理,写入 startup-error.log 3. sqlite.ts: FTS5 虚拟表创建改为可选,缺失时自动降级为 LIKE 搜索 4. sqlite.ts: 所有 FTS5 同步操作(save/delete/clear)加 _hasFTS5 守卫 --- src/main/db/sqlite.ts | 57 +++++++++++++++++++++++++++++++------------ src/main/main.ts | 29 +++++++++++++++++++++- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/main/db/sqlite.ts b/src/main/db/sqlite.ts index b2166ec..ca82924 100644 --- a/src/main/db/sqlite.ts +++ b/src/main/db/sqlite.ts @@ -73,6 +73,7 @@ function runTransaction(db: SQL.Database, fn: () => void): void { let db: SQL.Database | null = null; let dbPath: string | null = null; +let _hasFTS5 = false; /** 获取数据库实例 */ export function getDb(): SQL.Database { @@ -181,13 +182,6 @@ export async function initDatabase(): Promise { CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type); CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance DESC); - -- 记忆 FTS5 全文搜索 - CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( - content, tags, type, - content='memories', - content_rowid='rowid' - ); - -- 设置表 CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, @@ -211,6 +205,21 @@ export async function initDatabase(): Promise { CREATE INDEX IF NOT EXISTS idx_traces_session ON traces(session_id, created_at); `); + // 尝试创建 FTS5 全文搜索(可选,sql.js 默认 WASM 可能不包含 FTS5) + try { + db.run(` + CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( + content, tags, type, + content='memories', + content_rowid='rowid' + ); + `); + _hasFTS5 = true; + } catch { + _hasFTS5 = false; + console.warn('[sqlite] FTS5 不可用,记忆搜索降级为 LIKE 模式'); + } + // 写入一次确保文件存在 persist(); @@ -352,11 +361,13 @@ export function saveMemory(entry: MemoryRow): string { entry.source, entry.session_id, entry.use_count, entry.embedding, entry.created_at, entry.updated_at, entry.last_used_at] ); - // FTS 同步 - try { - runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [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 同步失败不影响主流程 */ } + // FTS 同步(仅在 FTS5 可用时) + if (_hasFTS5) { + try { + runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [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 同步失败不影响主流程 */ } + } persist(); return entry.id; } @@ -375,9 +386,9 @@ export function getMemoriesByType(type: string): MemoryRow[] { export function deleteMemory(id: string): void { const d = getDb(); - try { - runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [id]); - } catch { /* ignore */ } + if (_hasFTS5) { + try { runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [id]); } catch { /* ignore */ } + } runExec(d, 'DELETE FROM memories WHERE id = ?', [id]); persist(); } @@ -385,12 +396,26 @@ export function deleteMemory(id: string): void { export function clearAllMemories(): void { const d = getDb(); d.run('DELETE FROM memories'); - try { d.run('DELETE FROM memories_fts'); } catch { /* ignore */ } + if (_hasFTS5) { + try { d.run('DELETE FROM memories_fts'); } catch { /* ignore */ } + } persist(); } export function searchMemoriesFTS(query: string, limit = 10): MemoryRow[] { const d = getDb(); + + // FTS5 不可用时直接走 LIKE + if (!_hasFTS5) { + const likeQ = `%${query.replace(/[^\w\u4e00-\u9fff]/g, '%')}%`; + return queryAll(d, ` + SELECT * FROM memories + WHERE content LIKE ? OR tags LIKE ? + ORDER BY importance DESC + LIMIT ? + `, [likeQ, likeQ, limit]) as unknown as MemoryRow[]; + } + try { // FTS5 搜索 const ftsQuery = query.replace(/[^\w\u4e00-\u9fff\s]/g, ' ').split(/\s+/).filter(w => w.length > 0).map(w => `"${w}"*`).join(' OR '); diff --git a/src/main/main.ts b/src/main/main.ts index 4c8dd19..1a92432 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -2,7 +2,7 @@ * Metona Ollama Desktop - 主进程入口 */ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, dialog } from 'electron'; import * as path from 'path'; import * as fs from 'fs'; import { setupIPC } from './ipc.js'; @@ -11,6 +11,23 @@ import { createMenu } from './menu.js'; import { showNotification } from './utils.js'; import { ensureWorkspaceDir, killAllProcesses } from './workspace.js'; +// ── 全局错误处理:写入文件 + 弹窗提示 ── +const ERROR_LOG = path.join(app.getPath('userData'), 'startup-error.log'); + +function logStartupError(phase: string, err: unknown): void { + const msg = `[${new Date().toISOString()}] ${phase}: ${err instanceof Error ? err.stack || err.message : String(err)}\n`; + try { fs.appendFileSync(ERROR_LOG, msg); } catch { /* ignore */ } + console.error(msg); +} + +process.on('uncaughtException', (err) => { + logStartupError('uncaughtException', err); +}); + +process.on('unhandledRejection', (err) => { + logStartupError('unhandledRejection', err); +}); + const APP_NAME = 'Metona Ollama'; const ICON_PATH = path.join(__dirname, '..', '..', 'assets', 'icons', 'llama.png'); const ICO_PATH = path.join(__dirname, '..', '..', 'assets', 'icons', 'llama.ico'); @@ -157,6 +174,16 @@ app.whenReady().then(async () => { mainWindow.show(); } }); +}).catch((err) => { + logStartupError('app.whenReady', err); + // 弹出错误对话框,让用户知道发生了什么 + try { + dialog.showErrorBox('Metona Ollama 启动失败', + `应用初始化出错,请检查以下信息:\n\n${err instanceof Error ? err.message : String(err)}\n\n错误日志:${ERROR_LOG}`); + } catch { + // dialog 也可能失败(app 未完全初始化) + } + app.quit(); }); app.on('window-all-closed', () => {