fix: 修复打包后应用无法启动(sql.js WASM 不含 FTS5 模块)

根因分析:
- 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 守卫
This commit is contained in:
thzxx
2026-04-18 07:42:54 +08:00
parent 4518929a4c
commit 95153e9171
2 changed files with 69 additions and 17 deletions
+36 -11
View File
@@ -73,6 +73,7 @@ function runTransaction(db: SQL.Database, fn: () => void): void {
let db: SQL.Database | null = null; let db: SQL.Database | null = null;
let dbPath: string | null = null; let dbPath: string | null = null;
let _hasFTS5 = false;
/** 获取数据库实例 */ /** 获取数据库实例 */
export function getDb(): SQL.Database { export function getDb(): SQL.Database {
@@ -181,13 +182,6 @@ export async function initDatabase(): Promise<SQL.Database> {
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type); CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance DESC); 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 ( CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
@@ -211,6 +205,21 @@ export async function initDatabase(): Promise<SQL.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);
`); `);
// 尝试创建 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(); persist();
@@ -352,11 +361,13 @@ export function saveMemory(entry: MemoryRow): string {
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 同步(仅在 FTS5 可用时)
if (_hasFTS5) {
try { try {
runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [entry.id]); 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]); 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(); persist();
return entry.id; return entry.id;
} }
@@ -375,9 +386,9 @@ export function getMemoriesByType(type: string): MemoryRow[] {
export function deleteMemory(id: string): void { export function deleteMemory(id: string): void {
const d = getDb(); const d = getDb();
try { if (_hasFTS5) {
runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [id]); try { runExec(d, "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories WHERE id = ?)", [id]); } catch { /* ignore */ }
} catch { /* ignore */ } }
runExec(d, 'DELETE FROM memories WHERE id = ?', [id]); runExec(d, 'DELETE FROM memories WHERE id = ?', [id]);
persist(); persist();
} }
@@ -385,12 +396,26 @@ export function deleteMemory(id: string): void {
export function clearAllMemories(): void { export function clearAllMemories(): void {
const d = getDb(); const d = getDb();
d.run('DELETE FROM memories'); d.run('DELETE FROM memories');
if (_hasFTS5) {
try { d.run('DELETE FROM memories_fts'); } catch { /* ignore */ } try { d.run('DELETE FROM memories_fts'); } catch { /* ignore */ }
}
persist(); persist();
} }
export function searchMemoriesFTS(query: string, limit = 10): MemoryRow[] { export function searchMemoriesFTS(query: string, limit = 10): MemoryRow[] {
const d = getDb(); 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 { try {
// 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 ');
+28 -1
View File
@@ -2,7 +2,7 @@
* Metona Ollama Desktop - 主进程入口 * Metona Ollama Desktop - 主进程入口
*/ */
import { app, BrowserWindow } from 'electron'; import { app, BrowserWindow, dialog } from 'electron';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import { setupIPC } from './ipc.js'; import { setupIPC } from './ipc.js';
@@ -11,6 +11,23 @@ import { createMenu } from './menu.js';
import { showNotification } from './utils.js'; import { showNotification } from './utils.js';
import { ensureWorkspaceDir, killAllProcesses } from './workspace.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 APP_NAME = 'Metona Ollama';
const ICON_PATH = path.join(__dirname, '..', '..', 'assets', 'icons', 'llama.png'); const ICON_PATH = path.join(__dirname, '..', '..', 'assets', 'icons', 'llama.png');
const ICO_PATH = path.join(__dirname, '..', '..', 'assets', 'icons', 'llama.ico'); const ICO_PATH = path.join(__dirname, '..', '..', 'assets', 'icons', 'llama.ico');
@@ -157,6 +174,16 @@ app.whenReady().then(async () => {
mainWindow.show(); 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', () => { app.on('window-all-closed', () => {