@clawchatsai/connector 0.0.41 → 0.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.js +139 -90
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -334,14 +334,14 @@ function getActiveDb() {
|
|
|
334
334
|
return getDb(getWorkspaces().active);
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
|
|
338
|
-
function getGlobalDb() {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
337
|
+
const _globalDbCache = new Map(); // keyed by resolved dbPath
|
|
338
|
+
function getGlobalDb(dataDir = DATA_DIR) {
|
|
339
|
+
const dbPath = path.join(dataDir, 'global.db');
|
|
340
|
+
if (_globalDbCache.has(dbPath)) return _globalDbCache.get(dbPath);
|
|
341
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
342
|
+
const db = new Database(dbPath);
|
|
343
|
+
db.pragma('journal_mode = WAL');
|
|
344
|
+
db.exec(`
|
|
345
345
|
CREATE TABLE IF NOT EXISTS custom_emojis (
|
|
346
346
|
name TEXT NOT NULL,
|
|
347
347
|
pack TEXT NOT NULL DEFAULT 'slackmojis',
|
|
@@ -351,7 +351,8 @@ function getGlobalDb() {
|
|
|
351
351
|
PRIMARY KEY (name, pack)
|
|
352
352
|
)
|
|
353
353
|
`);
|
|
354
|
-
|
|
354
|
+
_globalDbCache.set(dbPath, db);
|
|
355
|
+
return db;
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
function closeDb(workspaceName) {
|
|
@@ -363,11 +364,40 @@ function closeDb(workspaceName) {
|
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
function closeAllDbs() {
|
|
366
|
-
for (const [
|
|
367
|
-
db.close();
|
|
368
|
-
}
|
|
367
|
+
for (const [, db] of dbCache) db.close();
|
|
369
368
|
dbCache.clear();
|
|
370
|
-
|
|
369
|
+
for (const [, db] of _globalDbCache) db.close();
|
|
370
|
+
_globalDbCache.clear();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function _createFtsTables(db) {
|
|
374
|
+
db.exec(`
|
|
375
|
+
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
|
376
|
+
content,
|
|
377
|
+
content=messages,
|
|
378
|
+
content_rowid=rowid,
|
|
379
|
+
tokenize='porter unicode61'
|
|
380
|
+
);
|
|
381
|
+
CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
|
|
382
|
+
INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
383
|
+
END;
|
|
384
|
+
CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
|
|
385
|
+
INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.rowid, old.content);
|
|
386
|
+
END;
|
|
387
|
+
CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
|
|
388
|
+
INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.rowid, old.content);
|
|
389
|
+
INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
390
|
+
END;
|
|
391
|
+
`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function _dropFtsTables(db) {
|
|
395
|
+
db.exec(`
|
|
396
|
+
DROP TABLE IF EXISTS messages_fts;
|
|
397
|
+
DROP TRIGGER IF EXISTS messages_ai;
|
|
398
|
+
DROP TRIGGER IF EXISTS messages_ad;
|
|
399
|
+
DROP TRIGGER IF EXISTS messages_au;
|
|
400
|
+
`);
|
|
371
401
|
}
|
|
372
402
|
|
|
373
403
|
function migrate(db) {
|
|
@@ -428,30 +458,29 @@ function migrate(db) {
|
|
|
428
458
|
db.exec('CREATE INDEX IF NOT EXISTS idx_unread_thread ON unread_messages(thread_id)');
|
|
429
459
|
|
|
430
460
|
// FTS5 table — CREATE VIRTUAL TABLE doesn't support IF NOT EXISTS in all versions,
|
|
431
|
-
// so check
|
|
461
|
+
// so check existence first, then verify integrity and auto-repair if corrupted.
|
|
432
462
|
const hasFts = db.prepare(
|
|
433
463
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'"
|
|
434
464
|
).get();
|
|
435
465
|
if (!hasFts) {
|
|
436
|
-
db
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
`);
|
|
466
|
+
_createFtsTables(db);
|
|
467
|
+
} else {
|
|
468
|
+
// Integrity check: corruption causes all message writes to 500.
|
|
469
|
+
// Attempt rebuild first; if that fails, drop entirely for graceful degradation
|
|
470
|
+
// (messages still save, search returns empty until next restart recreates the table).
|
|
471
|
+
try {
|
|
472
|
+
db.prepare("INSERT INTO messages_fts(messages_fts) VALUES('integrity-check')").run();
|
|
473
|
+
} catch (err) {
|
|
474
|
+
console.warn('[DB] messages_fts integrity check failed, attempting rebuild:', err.message);
|
|
475
|
+
try {
|
|
476
|
+
db.prepare("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')").run();
|
|
477
|
+
console.log('[DB] messages_fts rebuilt successfully — search index restored');
|
|
478
|
+
} catch (rebuildErr) {
|
|
479
|
+
console.error('[DB] messages_fts rebuild failed, dropping FTS for graceful degradation:', rebuildErr.message);
|
|
480
|
+
_dropFtsTables(db);
|
|
481
|
+
// On the next gateway restart the table will be recreated fresh via the !hasFts path
|
|
482
|
+
}
|
|
483
|
+
}
|
|
455
484
|
}
|
|
456
485
|
}
|
|
457
486
|
|
|
@@ -879,24 +908,29 @@ function handleGetThreads(req, res, params, query) {
|
|
|
879
908
|
let threads, total;
|
|
880
909
|
if (search) {
|
|
881
910
|
// FTS5 search across messages, return matching thread IDs
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
911
|
+
try {
|
|
912
|
+
const ftsQuery = `
|
|
913
|
+
SELECT DISTINCT m.thread_id
|
|
914
|
+
FROM messages m
|
|
915
|
+
JOIN messages_fts ON messages_fts.rowid = m.rowid
|
|
916
|
+
WHERE messages_fts MATCH ?
|
|
917
|
+
`;
|
|
918
|
+
const matchingIds = db.prepare(ftsQuery).all(search).map(r => r.thread_id);
|
|
919
|
+
if (matchingIds.length === 0) {
|
|
920
|
+
return send(res, 200, { threads: [], total: 0, page });
|
|
921
|
+
}
|
|
922
|
+
const placeholders = matchingIds.map(() => '?').join(',');
|
|
923
|
+
total = db.prepare(
|
|
924
|
+
`SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`
|
|
925
|
+
).get(...matchingIds).c;
|
|
926
|
+
threads = db.prepare(
|
|
927
|
+
`SELECT t.*, (SELECT MAX(m.timestamp) FROM messages m WHERE m.thread_id = t.id) as last_message_at
|
|
928
|
+
FROM threads t WHERE t.id IN (${placeholders}) ORDER BY t.pinned DESC, t.sort_order DESC, t.updated_at DESC LIMIT ? OFFSET ?`
|
|
929
|
+
).all(...matchingIds, limit, offset);
|
|
930
|
+
} catch (ftsErr) {
|
|
931
|
+
console.warn('[DB] FTS thread search failed, returning empty results:', ftsErr.message);
|
|
890
932
|
return send(res, 200, { threads: [], total: 0, page });
|
|
891
933
|
}
|
|
892
|
-
const placeholders = matchingIds.map(() => '?').join(',');
|
|
893
|
-
total = db.prepare(
|
|
894
|
-
`SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`
|
|
895
|
-
).get(...matchingIds).c;
|
|
896
|
-
threads = db.prepare(
|
|
897
|
-
`SELECT t.*, (SELECT MAX(m.timestamp) FROM messages m WHERE m.thread_id = t.id) as last_message_at
|
|
898
|
-
FROM threads t WHERE t.id IN (${placeholders}) ORDER BY t.pinned DESC, t.sort_order DESC, t.updated_at DESC LIMIT ? OFFSET ?`
|
|
899
|
-
).all(...matchingIds, limit, offset);
|
|
900
934
|
} else {
|
|
901
935
|
total = db.prepare('SELECT COUNT(*) as c FROM threads').get().c;
|
|
902
936
|
threads = db.prepare(
|
|
@@ -1183,29 +1217,34 @@ function handleSearch(req, res, params, query) {
|
|
|
1183
1217
|
const offset = (page - 1) * limit;
|
|
1184
1218
|
|
|
1185
1219
|
// FTS5 search with snippet
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1220
|
+
try {
|
|
1221
|
+
const results = db.prepare(`
|
|
1222
|
+
SELECT
|
|
1223
|
+
m.id as messageId,
|
|
1224
|
+
m.thread_id as threadId,
|
|
1225
|
+
t.title as threadTitle,
|
|
1226
|
+
m.role,
|
|
1227
|
+
snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as content,
|
|
1228
|
+
m.timestamp
|
|
1229
|
+
FROM messages_fts
|
|
1230
|
+
JOIN messages m ON messages_fts.rowid = m.rowid
|
|
1231
|
+
JOIN threads t ON m.thread_id = t.id
|
|
1232
|
+
WHERE messages_fts MATCH ?
|
|
1233
|
+
ORDER BY rank
|
|
1234
|
+
LIMIT ? OFFSET ?
|
|
1235
|
+
`).all(q, limit, offset);
|
|
1236
|
+
|
|
1237
|
+
const totalRow = db.prepare(`
|
|
1238
|
+
SELECT COUNT(*) as c
|
|
1239
|
+
FROM messages_fts
|
|
1240
|
+
WHERE messages_fts MATCH ?
|
|
1241
|
+
`).get(q);
|
|
1242
|
+
|
|
1243
|
+
send(res, 200, { results, total: totalRow.c });
|
|
1244
|
+
} catch (ftsErr) {
|
|
1245
|
+
console.warn('[DB] FTS message search failed, returning empty results:', ftsErr.message);
|
|
1246
|
+
send(res, 200, { results: [], total: 0 });
|
|
1247
|
+
}
|
|
1209
1248
|
}
|
|
1210
1249
|
|
|
1211
1250
|
// --- Export ---
|
|
@@ -3485,12 +3524,17 @@ export function createApp(config = {}) {
|
|
|
3485
3524
|
const search = query.search || '';
|
|
3486
3525
|
let threads, total;
|
|
3487
3526
|
if (search) {
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3527
|
+
try {
|
|
3528
|
+
const ftsQuery = `SELECT DISTINCT m.thread_id FROM messages m JOIN messages_fts ON messages_fts.rowid = m.rowid WHERE messages_fts MATCH ?`;
|
|
3529
|
+
const matchingIds = db.prepare(ftsQuery).all(search).map(r => r.thread_id);
|
|
3530
|
+
if (matchingIds.length === 0) return send(res, 200, { threads: [], total: 0, page });
|
|
3531
|
+
const placeholders = matchingIds.map(() => '?').join(',');
|
|
3532
|
+
total = db.prepare(`SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`).get(...matchingIds).c;
|
|
3533
|
+
threads = db.prepare(`SELECT * FROM threads WHERE id IN (${placeholders}) ORDER BY pinned DESC, sort_order DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...matchingIds, limit, offset);
|
|
3534
|
+
} catch (ftsErr) {
|
|
3535
|
+
console.warn('[DB] FTS thread search failed, returning empty results:', ftsErr.message);
|
|
3536
|
+
return send(res, 200, { threads: [], total: 0, page });
|
|
3537
|
+
}
|
|
3494
3538
|
} else {
|
|
3495
3539
|
total = db.prepare('SELECT COUNT(*) as c FROM threads').get().c;
|
|
3496
3540
|
threads = db.prepare('SELECT * FROM threads ORDER BY pinned DESC, sort_order DESC, updated_at DESC LIMIT ? OFFSET ?').all(limit, offset);
|
|
@@ -3697,14 +3741,19 @@ export function createApp(config = {}) {
|
|
|
3697
3741
|
const page = parseInt(query.page || '1', 10);
|
|
3698
3742
|
const limit = Math.min(parseInt(query.limit || '20', 10), 100);
|
|
3699
3743
|
const offset = (page - 1) * limit;
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3744
|
+
try {
|
|
3745
|
+
const results = db.prepare(`
|
|
3746
|
+
SELECT m.id as messageId, m.thread_id as threadId, t.title as threadTitle, m.role,
|
|
3747
|
+
snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as content, m.timestamp
|
|
3748
|
+
FROM messages_fts JOIN messages m ON messages_fts.rowid = m.rowid JOIN threads t ON m.thread_id = t.id
|
|
3749
|
+
WHERE messages_fts MATCH ? ORDER BY rank LIMIT ? OFFSET ?
|
|
3750
|
+
`).all(q, limit, offset);
|
|
3751
|
+
const totalRow = db.prepare(`SELECT COUNT(*) as c FROM messages_fts WHERE messages_fts MATCH ?`).get(q);
|
|
3752
|
+
send(res, 200, { results, total: totalRow.c });
|
|
3753
|
+
} catch (ftsErr) {
|
|
3754
|
+
console.warn('[DB] FTS message search failed, returning empty results:', ftsErr.message);
|
|
3755
|
+
send(res, 200, { results: [], total: 0 });
|
|
3756
|
+
}
|
|
3708
3757
|
}
|
|
3709
3758
|
|
|
3710
3759
|
function _handleExport(req, res) {
|
|
@@ -4368,7 +4417,7 @@ export function createApp(config = {}) {
|
|
|
4368
4417
|
// Custom emoji listing (no auth)
|
|
4369
4418
|
if (method === 'GET' && urlPath === '/api/emoji') {
|
|
4370
4419
|
try {
|
|
4371
|
-
const db = getGlobalDb();
|
|
4420
|
+
const db = getGlobalDb(_DATA_DIR);
|
|
4372
4421
|
const rows = db.prepare('SELECT name, pack, url, mime_type FROM custom_emojis ORDER BY created_at DESC').all();
|
|
4373
4422
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' });
|
|
4374
4423
|
return res.end(JSON.stringify(rows));
|
|
@@ -4412,7 +4461,7 @@ export function createApp(config = {}) {
|
|
|
4412
4461
|
if (urlLower.endsWith('.gif')) mimeType = 'image/gif';
|
|
4413
4462
|
else if (urlLower.endsWith('.webp')) mimeType = 'image/webp';
|
|
4414
4463
|
else if (urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg')) mimeType = 'image/jpeg';
|
|
4415
|
-
const db = getGlobalDb();
|
|
4464
|
+
const db = getGlobalDb(_DATA_DIR);
|
|
4416
4465
|
db.prepare('INSERT OR REPLACE INTO custom_emojis (name, pack, url, mime_type) VALUES (?, ?, ?, ?)').run(safeName, targetPack, url, mimeType);
|
|
4417
4466
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4418
4467
|
return res.end(JSON.stringify({ name: safeName, pack: targetPack, url, mime_type: mimeType }));
|
|
@@ -4424,7 +4473,7 @@ export function createApp(config = {}) {
|
|
|
4424
4473
|
try {
|
|
4425
4474
|
const { name, pack } = await parseBody(req);
|
|
4426
4475
|
if (!name || !pack) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing name or pack' })); }
|
|
4427
|
-
const db = getGlobalDb();
|
|
4476
|
+
const db = getGlobalDb(_DATA_DIR);
|
|
4428
4477
|
db.prepare('DELETE FROM custom_emojis WHERE name = ? AND pack = ?').run(name, pack);
|
|
4429
4478
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4430
4479
|
return res.end(JSON.stringify({ ok: true }));
|
|
@@ -4581,7 +4630,7 @@ if (isDirectRun) {
|
|
|
4581
4630
|
app.gatewayClient.connect();
|
|
4582
4631
|
|
|
4583
4632
|
// Initialize global DB (custom emojis, etc.)
|
|
4584
|
-
getGlobalDb();
|
|
4633
|
+
getGlobalDb(_DATA_DIR);
|
|
4585
4634
|
});
|
|
4586
4635
|
|
|
4587
4636
|
// Graceful shutdown
|