aicq-chat-plugin 3.9.0 → 3.9.1
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/README.md +80 -80
- package/SKILL.md +78 -78
- package/cli.cjs +356 -356
- package/index.js +417 -385
- package/lib/chat.js +854 -971
- package/lib/crypto.js +168 -168
- package/lib/database.js +455 -455
- package/lib/file-transfer.js +266 -266
- package/lib/handshake.js +147 -147
- package/lib/identity.js +165 -165
- package/lib/package.json +3 -3
- package/lib/server-client.js +380 -337
- package/openclaw.plugin.json +170 -168
- package/package.json +87 -87
- package/postinstall.cjs +27 -27
- package/public/favicon.ico +0 -0
- package/public/icon-16.png +0 -0
- package/public/icon-32.png +0 -0
- package/public/index.html +1468 -1468
- package/public/logo-512.png +0 -0
- package/setup-entry.js +14 -14
- package/src/channel.js +616 -637
- package/src/ui-routes.js +647 -594
package/lib/database.js
CHANGED
|
@@ -1,455 +1,455 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AICQ Plugin Database — SQLite via sql.js (pure WASM, no native compilation)
|
|
3
|
-
*
|
|
4
|
-
* This replaces better-sqlite3 with sql.js to avoid C++ native binding issues
|
|
5
|
-
* when installed via `openclaw plugins install`.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const db = new PluginDatabase(dataDir);
|
|
9
|
-
* await db.init(); // MUST call init() before using any methods
|
|
10
|
-
* ... use db methods (all synchronous after init) ...
|
|
11
|
-
* db.close();
|
|
12
|
-
*/
|
|
13
|
-
const initSqlJs = require('sql.js');
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const crypto = require('crypto');
|
|
17
|
-
|
|
18
|
-
class PluginDatabase {
|
|
19
|
-
constructor(dataDir) {
|
|
20
|
-
this.dataDir = dataDir;
|
|
21
|
-
this.db = null;
|
|
22
|
-
this.dbPath = path.join(dataDir, 'aicq-plugin.db');
|
|
23
|
-
this._dirty = false;
|
|
24
|
-
this._saveTimer = null;
|
|
25
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Async initialization ────────────────────────────────────────────
|
|
29
|
-
async init() {
|
|
30
|
-
const SQL = await initSqlJs();
|
|
31
|
-
|
|
32
|
-
// Load existing database or create new
|
|
33
|
-
if (fs.existsSync(this.dbPath)) {
|
|
34
|
-
try {
|
|
35
|
-
const buffer = fs.readFileSync(this.dbPath);
|
|
36
|
-
this.db = new SQL.Database(buffer);
|
|
37
|
-
} catch (e) {
|
|
38
|
-
console.error('[AICQ DB] Failed to load database, creating new one:', e.message);
|
|
39
|
-
this.db = new SQL.Database();
|
|
40
|
-
}
|
|
41
|
-
} else {
|
|
42
|
-
this.db = new SQL.Database();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
this.db.run('PRAGMA foreign_keys = ON');
|
|
46
|
-
this._initSchema();
|
|
47
|
-
this._save(); // Persist initial schema
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ── Persist database to disk ────────────────────────────────────────
|
|
51
|
-
_save() {
|
|
52
|
-
try {
|
|
53
|
-
const data = this.db.export();
|
|
54
|
-
const buffer = Buffer.from(data);
|
|
55
|
-
fs.writeFileSync(this.dbPath, buffer);
|
|
56
|
-
this._dirty = false;
|
|
57
|
-
} catch (e) {
|
|
58
|
-
console.error('[AICQ DB] Save failed:', e.message);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Debounced save — coalesces multiple writes within 500ms
|
|
63
|
-
_scheduleSave() {
|
|
64
|
-
this._dirty = true;
|
|
65
|
-
if (this._saveTimer) clearTimeout(this._saveTimer);
|
|
66
|
-
this._saveTimer = setTimeout(() => this._save(), 500);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Schema ──────────────────────────────────────────────────────────
|
|
70
|
-
_initSchema() {
|
|
71
|
-
this._execScript(`
|
|
72
|
-
CREATE TABLE IF NOT EXISTS identity (
|
|
73
|
-
agent_id TEXT PRIMARY KEY,
|
|
74
|
-
nickname TEXT NOT NULL DEFAULT '',
|
|
75
|
-
avatar TEXT NOT NULL DEFAULT '',
|
|
76
|
-
signing_public_key TEXT NOT NULL,
|
|
77
|
-
signing_secret_key TEXT NOT NULL,
|
|
78
|
-
exchange_public_key TEXT NOT NULL,
|
|
79
|
-
exchange_secret_key TEXT NOT NULL,
|
|
80
|
-
created_at TEXT NOT NULL,
|
|
81
|
-
updated_at TEXT
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
CREATE TABLE IF NOT EXISTS friends (
|
|
85
|
-
id TEXT PRIMARY KEY,
|
|
86
|
-
agent_id TEXT NOT NULL,
|
|
87
|
-
public_key TEXT NOT NULL,
|
|
88
|
-
fingerprint TEXT NOT NULL,
|
|
89
|
-
added_at TEXT NOT NULL,
|
|
90
|
-
last_seen TEXT,
|
|
91
|
-
is_online INTEGER NOT NULL DEFAULT 0,
|
|
92
|
-
permissions TEXT NOT NULL DEFAULT '["chat"]',
|
|
93
|
-
friend_type TEXT NOT NULL DEFAULT 'ai',
|
|
94
|
-
ai_name TEXT NOT NULL DEFAULT '',
|
|
95
|
-
ai_avatar TEXT NOT NULL DEFAULT ''
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
CREATE TABLE IF NOT EXISTS groups (
|
|
99
|
-
id TEXT PRIMARY KEY,
|
|
100
|
-
agent_id TEXT NOT NULL,
|
|
101
|
-
name TEXT NOT NULL,
|
|
102
|
-
owner_id TEXT NOT NULL,
|
|
103
|
-
members_json TEXT NOT NULL DEFAULT '[]',
|
|
104
|
-
description TEXT,
|
|
105
|
-
silent_mode INTEGER NOT NULL DEFAULT 0,
|
|
106
|
-
created_at TEXT NOT NULL,
|
|
107
|
-
updated_at TEXT
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
111
|
-
peer_id TEXT PRIMARY KEY,
|
|
112
|
-
agent_id TEXT NOT NULL,
|
|
113
|
-
session_key TEXT NOT NULL,
|
|
114
|
-
created_at TEXT NOT NULL,
|
|
115
|
-
message_count INTEGER NOT NULL DEFAULT 0,
|
|
116
|
-
last_rotation TEXT NOT NULL
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
CREATE TABLE IF NOT EXISTS chat_history (
|
|
120
|
-
id TEXT PRIMARY KEY,
|
|
121
|
-
agent_id TEXT NOT NULL,
|
|
122
|
-
target_id TEXT NOT NULL,
|
|
123
|
-
from_id TEXT NOT NULL,
|
|
124
|
-
to_id TEXT NOT NULL,
|
|
125
|
-
type TEXT NOT NULL DEFAULT 'text',
|
|
126
|
-
content TEXT NOT NULL DEFAULT '',
|
|
127
|
-
file_url TEXT,
|
|
128
|
-
file_name TEXT,
|
|
129
|
-
is_group INTEGER NOT NULL DEFAULT 0,
|
|
130
|
-
mentions TEXT NOT NULL DEFAULT '[]',
|
|
131
|
-
timestamp TEXT NOT NULL,
|
|
132
|
-
status TEXT NOT NULL DEFAULT 'pending'
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
CREATE TABLE IF NOT EXISTS pending_requests (
|
|
136
|
-
session_id TEXT PRIMARY KEY,
|
|
137
|
-
agent_id TEXT NOT NULL,
|
|
138
|
-
requester_id TEXT NOT NULL,
|
|
139
|
-
requester_public_key TEXT NOT NULL,
|
|
140
|
-
timestamp TEXT NOT NULL
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
CREATE TABLE IF NOT EXISTS temp_numbers (
|
|
144
|
-
number TEXT PRIMARY KEY,
|
|
145
|
-
agent_id TEXT NOT NULL,
|
|
146
|
-
expires_at TEXT NOT NULL,
|
|
147
|
-
created_at TEXT NOT NULL
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
CREATE TABLE IF NOT EXISTS offline_queue (
|
|
151
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
152
|
-
agent_id TEXT NOT NULL,
|
|
153
|
-
target_id TEXT NOT NULL,
|
|
154
|
-
data TEXT NOT NULL,
|
|
155
|
-
created_at TEXT NOT NULL
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
CREATE TABLE IF NOT EXISTS group_settings (
|
|
159
|
-
group_id TEXT PRIMARY KEY,
|
|
160
|
-
agent_id TEXT NOT NULL,
|
|
161
|
-
silent_mode INTEGER NOT NULL DEFAULT 0
|
|
162
|
-
);
|
|
163
|
-
`);
|
|
164
|
-
|
|
165
|
-
// Create indexes (must be separate statements)
|
|
166
|
-
this.db.run('CREATE INDEX IF NOT EXISTS idx_friends_agent ON friends(agent_id)');
|
|
167
|
-
this.db.run('CREATE INDEX IF NOT EXISTS idx_groups_agent ON groups(agent_id)');
|
|
168
|
-
this.db.run('CREATE INDEX IF NOT EXISTS idx_chat_agent_target ON chat_history(agent_id, target_id)');
|
|
169
|
-
this.db.run('CREATE INDEX IF NOT EXISTS idx_chat_timestamp ON chat_history(agent_id, target_id, timestamp)');
|
|
170
|
-
this.db.run('CREATE INDEX IF NOT EXISTS idx_offline_target ON offline_queue(agent_id, target_id)');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Execute multiple SQL statements (like db.exec in better-sqlite3)
|
|
174
|
-
_execScript(sql) {
|
|
175
|
-
// sql.js db.exec() can handle multiple statements but returns results.
|
|
176
|
-
// For DDL statements we just use run() for each.
|
|
177
|
-
const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
|
|
178
|
-
for (const stmt of statements) {
|
|
179
|
-
try {
|
|
180
|
-
this.db.run(stmt);
|
|
181
|
-
} catch (e) {
|
|
182
|
-
// Ignore "already exists" errors for CREATE TABLE/INDEX
|
|
183
|
-
if (!e.message.includes('already exists')) {
|
|
184
|
-
console.error('[AICQ DB] Schema error:', e.message, 'SQL:', stmt.substring(0, 80));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ── Query helpers ───────────────────────────────────────────────────
|
|
191
|
-
|
|
192
|
-
// Run a parameterized write query, then schedule save
|
|
193
|
-
_run(sql, params) {
|
|
194
|
-
this.db.run(sql, params || []);
|
|
195
|
-
this._scheduleSave();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Get a single row as an object
|
|
199
|
-
_get(sql, params) {
|
|
200
|
-
const stmt = this.db.prepare(sql);
|
|
201
|
-
stmt.bind(params || []);
|
|
202
|
-
let row = null;
|
|
203
|
-
if (stmt.step()) {
|
|
204
|
-
row = stmt.getAsObject();
|
|
205
|
-
}
|
|
206
|
-
stmt.free();
|
|
207
|
-
return row;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Get all rows as objects
|
|
211
|
-
_all(sql, params) {
|
|
212
|
-
const stmt = this.db.prepare(sql);
|
|
213
|
-
stmt.bind(params || []);
|
|
214
|
-
const rows = [];
|
|
215
|
-
while (stmt.step()) {
|
|
216
|
-
rows.push(stmt.getAsObject());
|
|
217
|
-
}
|
|
218
|
-
stmt.free();
|
|
219
|
-
return rows;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ─── Identity ──────────────────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
saveIdentity({ agent_id, nickname, signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key }) {
|
|
225
|
-
const now = new Date().toISOString();
|
|
226
|
-
this._run(
|
|
227
|
-
`INSERT OR REPLACE INTO identity (agent_id, nickname, signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key, created_at, updated_at)
|
|
228
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
229
|
-
[agent_id, nickname || '', signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key, now, now]
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
loadIdentity(agentId) {
|
|
234
|
-
return this._get('SELECT * FROM identity WHERE agent_id = ?', [agentId]);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
listIdentities() {
|
|
238
|
-
return this._all('SELECT agent_id, nickname, signing_public_key, exchange_public_key, created_at FROM identity');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
deleteIdentity(agentId) {
|
|
242
|
-
this._run('DELETE FROM identity WHERE agent_id = ?', [agentId]);
|
|
243
|
-
this._run('DELETE FROM friends WHERE agent_id = ?', [agentId]);
|
|
244
|
-
this._run('DELETE FROM groups WHERE agent_id = ?', [agentId]);
|
|
245
|
-
this._run('DELETE FROM chat_history WHERE agent_id = ?', [agentId]);
|
|
246
|
-
this._run('DELETE FROM sessions WHERE agent_id = ?', [agentId]);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
updateNickname(agentId, nickname) {
|
|
250
|
-
const now = new Date().toISOString();
|
|
251
|
-
this._run('UPDATE identity SET nickname = ?, updated_at = ? WHERE agent_id = ?', [nickname, now, agentId]);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
updateAvatar(agentId, avatarUrl) {
|
|
255
|
-
const now = new Date().toISOString();
|
|
256
|
-
try {
|
|
257
|
-
this._run('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?', [avatarUrl, now, agentId]);
|
|
258
|
-
} catch (e) {
|
|
259
|
-
if (e.message && e.message.includes('no column named avatar')) {
|
|
260
|
-
this.db.run('ALTER TABLE identity ADD COLUMN avatar TEXT NOT NULL DEFAULT ""');
|
|
261
|
-
this._run('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?', [avatarUrl, now, agentId]);
|
|
262
|
-
} else {
|
|
263
|
-
throw e;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ─── Friends ───────────────────────────────────────────────────────
|
|
269
|
-
|
|
270
|
-
addFriend({ agent_id, id, public_key, fingerprint, friend_type = 'ai', ai_name = '', permissions = ['chat'] }) {
|
|
271
|
-
const now = new Date().toISOString();
|
|
272
|
-
this._run(
|
|
273
|
-
`INSERT OR REPLACE INTO friends (id, agent_id, public_key, fingerprint, added_at, is_online, permissions, friend_type, ai_name, ai_avatar)
|
|
274
|
-
VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, '')`,
|
|
275
|
-
[id, agent_id, public_key, fingerprint, now, JSON.stringify(permissions), friend_type, ai_name]
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
removeFriend(agentId, friendId) {
|
|
280
|
-
this._run('DELETE FROM friends WHERE agent_id = ? AND id = ?', [agentId, friendId]);
|
|
281
|
-
this._run('DELETE FROM sessions WHERE agent_id = ? AND peer_id = ?', [agentId, friendId]);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
getFriend(agentId, friendId) {
|
|
285
|
-
return this._get('SELECT * FROM friends WHERE agent_id = ? AND id = ?', [agentId, friendId]);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
listFriends(agentId) {
|
|
289
|
-
return this._all('SELECT * FROM friends WHERE agent_id = ? ORDER BY added_at DESC', [agentId]);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
updateFriendOnline(agentId, friendId, isOnline) {
|
|
293
|
-
const now = isOnline ? new Date().toISOString() : null;
|
|
294
|
-
// COALESCE equivalent: if now is null, keep existing last_seen
|
|
295
|
-
this._run(
|
|
296
|
-
'UPDATE friends SET is_online = ?, last_seen = COALESCE(?, last_seen) WHERE agent_id = ? AND id = ?',
|
|
297
|
-
[isOnline ? 1 : 0, now, agentId, friendId]
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ─── Groups ────────────────────────────────────────────────────────
|
|
302
|
-
|
|
303
|
-
addGroup({ agent_id, id, name, owner_id, members_json = '[]', description = '' }) {
|
|
304
|
-
const now = new Date().toISOString();
|
|
305
|
-
this._run(
|
|
306
|
-
`INSERT OR REPLACE INTO groups (id, agent_id, name, owner_id, members_json, description, silent_mode, created_at, updated_at)
|
|
307
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`,
|
|
308
|
-
[id, agent_id, name, owner_id, typeof members_json === 'string' ? members_json : JSON.stringify(members_json), description, now, now]
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
listGroups(agentId) {
|
|
313
|
-
return this._all('SELECT * FROM groups WHERE agent_id = ? ORDER BY created_at DESC', [agentId]);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
getGroup(agentId, groupId) {
|
|
317
|
-
return this._get('SELECT * FROM groups WHERE agent_id = ? AND id = ?', [agentId, groupId]);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
setGroupSilentMode(agentId, groupId, silent) {
|
|
321
|
-
this._run(
|
|
322
|
-
'INSERT OR REPLACE INTO group_settings (group_id, agent_id, silent_mode) VALUES (?, ?, ?)',
|
|
323
|
-
[groupId, agentId, silent ? 1 : 0]
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
getGroupSilentMode(agentId, groupId) {
|
|
328
|
-
const row = this._get('SELECT silent_mode FROM group_settings WHERE group_id = ? AND agent_id = ?', [groupId, agentId]);
|
|
329
|
-
return row ? !!row.silent_mode : false;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// ─── Sessions ──────────────────────────────────────────────────────
|
|
333
|
-
|
|
334
|
-
saveSession({ agent_id, peer_id, session_key }) {
|
|
335
|
-
const now = new Date().toISOString();
|
|
336
|
-
this._run(
|
|
337
|
-
`INSERT OR REPLACE INTO sessions (peer_id, agent_id, session_key, created_at, message_count, last_rotation)
|
|
338
|
-
VALUES (?, ?, ?, ?, 0, ?)`,
|
|
339
|
-
[peer_id, agent_id, session_key, now, now]
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
loadSession(agentId, peerId) {
|
|
344
|
-
return this._get('SELECT * FROM sessions WHERE agent_id = ? AND peer_id = ?', [agentId, peerId]);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
incrementSessionMessageCount(agentId, peerId) {
|
|
348
|
-
this._run('UPDATE sessions SET message_count = message_count + 1 WHERE agent_id = ? AND peer_id = ?', [agentId, peerId]);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ─── Chat History ──────────────────────────────────────────────────
|
|
352
|
-
|
|
353
|
-
saveMessage({ agent_id, target_id, from_id, to_id, type = 'text', content = '', file_url = null, file_name = null, is_group = 0, mentions = [], status = 'pending' }) {
|
|
354
|
-
const id = crypto.randomUUID();
|
|
355
|
-
const now = new Date().toISOString();
|
|
356
|
-
this._run(
|
|
357
|
-
`INSERT INTO chat_history (id, agent_id, target_id, from_id, to_id, type, content, file_url, file_name, is_group, mentions, timestamp, status)
|
|
358
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
359
|
-
[id, agent_id, target_id, from_id, to_id, type, content, file_url, file_name, is_group, JSON.stringify(mentions), now, status]
|
|
360
|
-
);
|
|
361
|
-
return { id, timestamp: now };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
getChatHistory(agentId, targetId, { limit = 50, before = null } = {}) {
|
|
365
|
-
if (before) {
|
|
366
|
-
return this._all(
|
|
367
|
-
'SELECT * FROM chat_history WHERE agent_id = ? AND target_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?',
|
|
368
|
-
[agentId, targetId, before, limit]
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
return this._all(
|
|
372
|
-
'SELECT * FROM chat_history WHERE agent_id = ? AND target_id = ? ORDER BY timestamp DESC LIMIT ?',
|
|
373
|
-
[agentId, targetId, limit]
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
deleteMessage(agentId, messageId) {
|
|
378
|
-
this._run('DELETE FROM chat_history WHERE agent_id = ? AND id = ?', [agentId, messageId]);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
updateMessageStatus(agentId, messageId, status) {
|
|
382
|
-
this._run('UPDATE chat_history SET status = ? WHERE agent_id = ? AND id = ?', [status, agentId, messageId]);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ─── Pending Requests ──────────────────────────────────────────────
|
|
386
|
-
|
|
387
|
-
savePendingRequest({ agent_id, session_id, requester_id, requester_public_key }) {
|
|
388
|
-
const now = new Date().toISOString();
|
|
389
|
-
this._run(
|
|
390
|
-
`INSERT OR REPLACE INTO pending_requests (session_id, agent_id, requester_id, requester_public_key, timestamp)
|
|
391
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
392
|
-
[session_id, agent_id, requester_id, requester_public_key, now]
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
getPendingRequests(agentId) {
|
|
397
|
-
return this._all('SELECT * FROM pending_requests WHERE agent_id = ? ORDER BY timestamp DESC', [agentId]);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
removePendingRequest(agentId, sessionId) {
|
|
401
|
-
this._run('DELETE FROM pending_requests WHERE agent_id = ? AND session_id = ?', [agentId, sessionId]);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// ─── Temp Numbers ──────────────────────────────────────────────────
|
|
405
|
-
|
|
406
|
-
saveTempNumber({ agent_id, number, expires_at }) {
|
|
407
|
-
const now = new Date().toISOString();
|
|
408
|
-
this._run(
|
|
409
|
-
'INSERT OR REPLACE INTO temp_numbers (number, agent_id, expires_at, created_at) VALUES (?, ?, ?, ?)',
|
|
410
|
-
[number, agent_id, expires_at, now]
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// ─── Offline Queue ─────────────────────────────────────────────────
|
|
415
|
-
|
|
416
|
-
enqueueOffline({ agent_id, target_id, data }) {
|
|
417
|
-
const now = new Date().toISOString();
|
|
418
|
-
this._run(
|
|
419
|
-
'INSERT INTO offline_queue (agent_id, target_id, data, created_at) VALUES (?, ?, ?, ?)',
|
|
420
|
-
[agent_id, target_id, data, now]
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
dequeueOffline(agentId, targetId, limit = 100) {
|
|
425
|
-
const rows = this._all(
|
|
426
|
-
'SELECT * FROM offline_queue WHERE agent_id = ? AND target_id = ? ORDER BY created_at ASC LIMIT ?',
|
|
427
|
-
[agentId, targetId, limit]
|
|
428
|
-
);
|
|
429
|
-
if (rows.length > 0) {
|
|
430
|
-
const ids = rows.map(r => r.id);
|
|
431
|
-
const placeholders = ids.map(() => '?').join(',');
|
|
432
|
-
this._run(`DELETE FROM offline_queue WHERE id IN (${placeholders})`, ids);
|
|
433
|
-
}
|
|
434
|
-
return rows;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// ─── Cleanup ───────────────────────────────────────────────────────
|
|
438
|
-
|
|
439
|
-
cleanup() {
|
|
440
|
-
const now = new Date().toISOString();
|
|
441
|
-
this._run("DELETE FROM temp_numbers WHERE expires_at < ?", [now]);
|
|
442
|
-
this._run("DELETE FROM pending_requests WHERE timestamp < datetime(?, '-48 hours')", [now]);
|
|
443
|
-
this._run("DELETE FROM offline_queue WHERE created_at < datetime(?, '-7 days')", [now]);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
close() {
|
|
447
|
-
if (this._saveTimer) {
|
|
448
|
-
clearTimeout(this._saveTimer);
|
|
449
|
-
}
|
|
450
|
-
this._save(); // Final save before closing
|
|
451
|
-
this.db.close();
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
module.exports = PluginDatabase;
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Plugin Database — SQLite via sql.js (pure WASM, no native compilation)
|
|
3
|
+
*
|
|
4
|
+
* This replaces better-sqlite3 with sql.js to avoid C++ native binding issues
|
|
5
|
+
* when installed via `openclaw plugins install`.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const db = new PluginDatabase(dataDir);
|
|
9
|
+
* await db.init(); // MUST call init() before using any methods
|
|
10
|
+
* ... use db methods (all synchronous after init) ...
|
|
11
|
+
* db.close();
|
|
12
|
+
*/
|
|
13
|
+
const initSqlJs = require('sql.js');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
class PluginDatabase {
|
|
19
|
+
constructor(dataDir) {
|
|
20
|
+
this.dataDir = dataDir;
|
|
21
|
+
this.db = null;
|
|
22
|
+
this.dbPath = path.join(dataDir, 'aicq-plugin.db');
|
|
23
|
+
this._dirty = false;
|
|
24
|
+
this._saveTimer = null;
|
|
25
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Async initialization ────────────────────────────────────────────
|
|
29
|
+
async init() {
|
|
30
|
+
const SQL = await initSqlJs();
|
|
31
|
+
|
|
32
|
+
// Load existing database or create new
|
|
33
|
+
if (fs.existsSync(this.dbPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const buffer = fs.readFileSync(this.dbPath);
|
|
36
|
+
this.db = new SQL.Database(buffer);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error('[AICQ DB] Failed to load database, creating new one:', e.message);
|
|
39
|
+
this.db = new SQL.Database();
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
this.db = new SQL.Database();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
46
|
+
this._initSchema();
|
|
47
|
+
this._save(); // Persist initial schema
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Persist database to disk ────────────────────────────────────────
|
|
51
|
+
_save() {
|
|
52
|
+
try {
|
|
53
|
+
const data = this.db.export();
|
|
54
|
+
const buffer = Buffer.from(data);
|
|
55
|
+
fs.writeFileSync(this.dbPath, buffer);
|
|
56
|
+
this._dirty = false;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error('[AICQ DB] Save failed:', e.message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Debounced save — coalesces multiple writes within 500ms
|
|
63
|
+
_scheduleSave() {
|
|
64
|
+
this._dirty = true;
|
|
65
|
+
if (this._saveTimer) clearTimeout(this._saveTimer);
|
|
66
|
+
this._saveTimer = setTimeout(() => this._save(), 500);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Schema ──────────────────────────────────────────────────────────
|
|
70
|
+
_initSchema() {
|
|
71
|
+
this._execScript(`
|
|
72
|
+
CREATE TABLE IF NOT EXISTS identity (
|
|
73
|
+
agent_id TEXT PRIMARY KEY,
|
|
74
|
+
nickname TEXT NOT NULL DEFAULT '',
|
|
75
|
+
avatar TEXT NOT NULL DEFAULT '',
|
|
76
|
+
signing_public_key TEXT NOT NULL,
|
|
77
|
+
signing_secret_key TEXT NOT NULL,
|
|
78
|
+
exchange_public_key TEXT NOT NULL,
|
|
79
|
+
exchange_secret_key TEXT NOT NULL,
|
|
80
|
+
created_at TEXT NOT NULL,
|
|
81
|
+
updated_at TEXT
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS friends (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
agent_id TEXT NOT NULL,
|
|
87
|
+
public_key TEXT NOT NULL,
|
|
88
|
+
fingerprint TEXT NOT NULL,
|
|
89
|
+
added_at TEXT NOT NULL,
|
|
90
|
+
last_seen TEXT,
|
|
91
|
+
is_online INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
permissions TEXT NOT NULL DEFAULT '["chat"]',
|
|
93
|
+
friend_type TEXT NOT NULL DEFAULT 'ai',
|
|
94
|
+
ai_name TEXT NOT NULL DEFAULT '',
|
|
95
|
+
ai_avatar TEXT NOT NULL DEFAULT ''
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE IF NOT EXISTS groups (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
agent_id TEXT NOT NULL,
|
|
101
|
+
name TEXT NOT NULL,
|
|
102
|
+
owner_id TEXT NOT NULL,
|
|
103
|
+
members_json TEXT NOT NULL DEFAULT '[]',
|
|
104
|
+
description TEXT,
|
|
105
|
+
silent_mode INTEGER NOT NULL DEFAULT 0,
|
|
106
|
+
created_at TEXT NOT NULL,
|
|
107
|
+
updated_at TEXT
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
111
|
+
peer_id TEXT PRIMARY KEY,
|
|
112
|
+
agent_id TEXT NOT NULL,
|
|
113
|
+
session_key TEXT NOT NULL,
|
|
114
|
+
created_at TEXT NOT NULL,
|
|
115
|
+
message_count INTEGER NOT NULL DEFAULT 0,
|
|
116
|
+
last_rotation TEXT NOT NULL
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
CREATE TABLE IF NOT EXISTS chat_history (
|
|
120
|
+
id TEXT PRIMARY KEY,
|
|
121
|
+
agent_id TEXT NOT NULL,
|
|
122
|
+
target_id TEXT NOT NULL,
|
|
123
|
+
from_id TEXT NOT NULL,
|
|
124
|
+
to_id TEXT NOT NULL,
|
|
125
|
+
type TEXT NOT NULL DEFAULT 'text',
|
|
126
|
+
content TEXT NOT NULL DEFAULT '',
|
|
127
|
+
file_url TEXT,
|
|
128
|
+
file_name TEXT,
|
|
129
|
+
is_group INTEGER NOT NULL DEFAULT 0,
|
|
130
|
+
mentions TEXT NOT NULL DEFAULT '[]',
|
|
131
|
+
timestamp TEXT NOT NULL,
|
|
132
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
CREATE TABLE IF NOT EXISTS pending_requests (
|
|
136
|
+
session_id TEXT PRIMARY KEY,
|
|
137
|
+
agent_id TEXT NOT NULL,
|
|
138
|
+
requester_id TEXT NOT NULL,
|
|
139
|
+
requester_public_key TEXT NOT NULL,
|
|
140
|
+
timestamp TEXT NOT NULL
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS temp_numbers (
|
|
144
|
+
number TEXT PRIMARY KEY,
|
|
145
|
+
agent_id TEXT NOT NULL,
|
|
146
|
+
expires_at TEXT NOT NULL,
|
|
147
|
+
created_at TEXT NOT NULL
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
CREATE TABLE IF NOT EXISTS offline_queue (
|
|
151
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
152
|
+
agent_id TEXT NOT NULL,
|
|
153
|
+
target_id TEXT NOT NULL,
|
|
154
|
+
data TEXT NOT NULL,
|
|
155
|
+
created_at TEXT NOT NULL
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
CREATE TABLE IF NOT EXISTS group_settings (
|
|
159
|
+
group_id TEXT PRIMARY KEY,
|
|
160
|
+
agent_id TEXT NOT NULL,
|
|
161
|
+
silent_mode INTEGER NOT NULL DEFAULT 0
|
|
162
|
+
);
|
|
163
|
+
`);
|
|
164
|
+
|
|
165
|
+
// Create indexes (must be separate statements)
|
|
166
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_friends_agent ON friends(agent_id)');
|
|
167
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_groups_agent ON groups(agent_id)');
|
|
168
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_chat_agent_target ON chat_history(agent_id, target_id)');
|
|
169
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_chat_timestamp ON chat_history(agent_id, target_id, timestamp)');
|
|
170
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_offline_target ON offline_queue(agent_id, target_id)');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Execute multiple SQL statements (like db.exec in better-sqlite3)
|
|
174
|
+
_execScript(sql) {
|
|
175
|
+
// sql.js db.exec() can handle multiple statements but returns results.
|
|
176
|
+
// For DDL statements we just use run() for each.
|
|
177
|
+
const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
|
|
178
|
+
for (const stmt of statements) {
|
|
179
|
+
try {
|
|
180
|
+
this.db.run(stmt);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Ignore "already exists" errors for CREATE TABLE/INDEX
|
|
183
|
+
if (!e.message.includes('already exists')) {
|
|
184
|
+
console.error('[AICQ DB] Schema error:', e.message, 'SQL:', stmt.substring(0, 80));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Query helpers ───────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
// Run a parameterized write query, then schedule save
|
|
193
|
+
_run(sql, params) {
|
|
194
|
+
this.db.run(sql, params || []);
|
|
195
|
+
this._scheduleSave();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Get a single row as an object
|
|
199
|
+
_get(sql, params) {
|
|
200
|
+
const stmt = this.db.prepare(sql);
|
|
201
|
+
stmt.bind(params || []);
|
|
202
|
+
let row = null;
|
|
203
|
+
if (stmt.step()) {
|
|
204
|
+
row = stmt.getAsObject();
|
|
205
|
+
}
|
|
206
|
+
stmt.free();
|
|
207
|
+
return row;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get all rows as objects
|
|
211
|
+
_all(sql, params) {
|
|
212
|
+
const stmt = this.db.prepare(sql);
|
|
213
|
+
stmt.bind(params || []);
|
|
214
|
+
const rows = [];
|
|
215
|
+
while (stmt.step()) {
|
|
216
|
+
rows.push(stmt.getAsObject());
|
|
217
|
+
}
|
|
218
|
+
stmt.free();
|
|
219
|
+
return rows;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Identity ──────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
saveIdentity({ agent_id, nickname, signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key }) {
|
|
225
|
+
const now = new Date().toISOString();
|
|
226
|
+
this._run(
|
|
227
|
+
`INSERT OR REPLACE INTO identity (agent_id, nickname, signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key, created_at, updated_at)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
229
|
+
[agent_id, nickname || '', signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key, now, now]
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
loadIdentity(agentId) {
|
|
234
|
+
return this._get('SELECT * FROM identity WHERE agent_id = ?', [agentId]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
listIdentities() {
|
|
238
|
+
return this._all('SELECT agent_id, nickname, signing_public_key, exchange_public_key, created_at FROM identity');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
deleteIdentity(agentId) {
|
|
242
|
+
this._run('DELETE FROM identity WHERE agent_id = ?', [agentId]);
|
|
243
|
+
this._run('DELETE FROM friends WHERE agent_id = ?', [agentId]);
|
|
244
|
+
this._run('DELETE FROM groups WHERE agent_id = ?', [agentId]);
|
|
245
|
+
this._run('DELETE FROM chat_history WHERE agent_id = ?', [agentId]);
|
|
246
|
+
this._run('DELETE FROM sessions WHERE agent_id = ?', [agentId]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
updateNickname(agentId, nickname) {
|
|
250
|
+
const now = new Date().toISOString();
|
|
251
|
+
this._run('UPDATE identity SET nickname = ?, updated_at = ? WHERE agent_id = ?', [nickname, now, agentId]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
updateAvatar(agentId, avatarUrl) {
|
|
255
|
+
const now = new Date().toISOString();
|
|
256
|
+
try {
|
|
257
|
+
this._run('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?', [avatarUrl, now, agentId]);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
if (e.message && e.message.includes('no column named avatar')) {
|
|
260
|
+
this.db.run('ALTER TABLE identity ADD COLUMN avatar TEXT NOT NULL DEFAULT ""');
|
|
261
|
+
this._run('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?', [avatarUrl, now, agentId]);
|
|
262
|
+
} else {
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Friends ───────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
addFriend({ agent_id, id, public_key, fingerprint, friend_type = 'ai', ai_name = '', permissions = ['chat'] }) {
|
|
271
|
+
const now = new Date().toISOString();
|
|
272
|
+
this._run(
|
|
273
|
+
`INSERT OR REPLACE INTO friends (id, agent_id, public_key, fingerprint, added_at, is_online, permissions, friend_type, ai_name, ai_avatar)
|
|
274
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, '')`,
|
|
275
|
+
[id, agent_id, public_key, fingerprint, now, JSON.stringify(permissions), friend_type, ai_name]
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
removeFriend(agentId, friendId) {
|
|
280
|
+
this._run('DELETE FROM friends WHERE agent_id = ? AND id = ?', [agentId, friendId]);
|
|
281
|
+
this._run('DELETE FROM sessions WHERE agent_id = ? AND peer_id = ?', [agentId, friendId]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getFriend(agentId, friendId) {
|
|
285
|
+
return this._get('SELECT * FROM friends WHERE agent_id = ? AND id = ?', [agentId, friendId]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
listFriends(agentId) {
|
|
289
|
+
return this._all('SELECT * FROM friends WHERE agent_id = ? ORDER BY added_at DESC', [agentId]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
updateFriendOnline(agentId, friendId, isOnline) {
|
|
293
|
+
const now = isOnline ? new Date().toISOString() : null;
|
|
294
|
+
// COALESCE equivalent: if now is null, keep existing last_seen
|
|
295
|
+
this._run(
|
|
296
|
+
'UPDATE friends SET is_online = ?, last_seen = COALESCE(?, last_seen) WHERE agent_id = ? AND id = ?',
|
|
297
|
+
[isOnline ? 1 : 0, now, agentId, friendId]
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ─── Groups ────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
addGroup({ agent_id, id, name, owner_id, members_json = '[]', description = '' }) {
|
|
304
|
+
const now = new Date().toISOString();
|
|
305
|
+
this._run(
|
|
306
|
+
`INSERT OR REPLACE INTO groups (id, agent_id, name, owner_id, members_json, description, silent_mode, created_at, updated_at)
|
|
307
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`,
|
|
308
|
+
[id, agent_id, name, owner_id, typeof members_json === 'string' ? members_json : JSON.stringify(members_json), description, now, now]
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
listGroups(agentId) {
|
|
313
|
+
return this._all('SELECT * FROM groups WHERE agent_id = ? ORDER BY created_at DESC', [agentId]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
getGroup(agentId, groupId) {
|
|
317
|
+
return this._get('SELECT * FROM groups WHERE agent_id = ? AND id = ?', [agentId, groupId]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
setGroupSilentMode(agentId, groupId, silent) {
|
|
321
|
+
this._run(
|
|
322
|
+
'INSERT OR REPLACE INTO group_settings (group_id, agent_id, silent_mode) VALUES (?, ?, ?)',
|
|
323
|
+
[groupId, agentId, silent ? 1 : 0]
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getGroupSilentMode(agentId, groupId) {
|
|
328
|
+
const row = this._get('SELECT silent_mode FROM group_settings WHERE group_id = ? AND agent_id = ?', [groupId, agentId]);
|
|
329
|
+
return row ? !!row.silent_mode : false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Sessions ──────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
saveSession({ agent_id, peer_id, session_key }) {
|
|
335
|
+
const now = new Date().toISOString();
|
|
336
|
+
this._run(
|
|
337
|
+
`INSERT OR REPLACE INTO sessions (peer_id, agent_id, session_key, created_at, message_count, last_rotation)
|
|
338
|
+
VALUES (?, ?, ?, ?, 0, ?)`,
|
|
339
|
+
[peer_id, agent_id, session_key, now, now]
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
loadSession(agentId, peerId) {
|
|
344
|
+
return this._get('SELECT * FROM sessions WHERE agent_id = ? AND peer_id = ?', [agentId, peerId]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
incrementSessionMessageCount(agentId, peerId) {
|
|
348
|
+
this._run('UPDATE sessions SET message_count = message_count + 1 WHERE agent_id = ? AND peer_id = ?', [agentId, peerId]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ─── Chat History ──────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
saveMessage({ agent_id, target_id, from_id, to_id, type = 'text', content = '', file_url = null, file_name = null, is_group = 0, mentions = [], status = 'pending' }) {
|
|
354
|
+
const id = crypto.randomUUID();
|
|
355
|
+
const now = new Date().toISOString();
|
|
356
|
+
this._run(
|
|
357
|
+
`INSERT INTO chat_history (id, agent_id, target_id, from_id, to_id, type, content, file_url, file_name, is_group, mentions, timestamp, status)
|
|
358
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
359
|
+
[id, agent_id, target_id, from_id, to_id, type, content, file_url, file_name, is_group, JSON.stringify(mentions), now, status]
|
|
360
|
+
);
|
|
361
|
+
return { id, timestamp: now };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getChatHistory(agentId, targetId, { limit = 50, before = null } = {}) {
|
|
365
|
+
if (before) {
|
|
366
|
+
return this._all(
|
|
367
|
+
'SELECT * FROM chat_history WHERE agent_id = ? AND target_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?',
|
|
368
|
+
[agentId, targetId, before, limit]
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return this._all(
|
|
372
|
+
'SELECT * FROM chat_history WHERE agent_id = ? AND target_id = ? ORDER BY timestamp DESC LIMIT ?',
|
|
373
|
+
[agentId, targetId, limit]
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
deleteMessage(agentId, messageId) {
|
|
378
|
+
this._run('DELETE FROM chat_history WHERE agent_id = ? AND id = ?', [agentId, messageId]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
updateMessageStatus(agentId, messageId, status) {
|
|
382
|
+
this._run('UPDATE chat_history SET status = ? WHERE agent_id = ? AND id = ?', [status, agentId, messageId]);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Pending Requests ──────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
savePendingRequest({ agent_id, session_id, requester_id, requester_public_key }) {
|
|
388
|
+
const now = new Date().toISOString();
|
|
389
|
+
this._run(
|
|
390
|
+
`INSERT OR REPLACE INTO pending_requests (session_id, agent_id, requester_id, requester_public_key, timestamp)
|
|
391
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
392
|
+
[session_id, agent_id, requester_id, requester_public_key, now]
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getPendingRequests(agentId) {
|
|
397
|
+
return this._all('SELECT * FROM pending_requests WHERE agent_id = ? ORDER BY timestamp DESC', [agentId]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
removePendingRequest(agentId, sessionId) {
|
|
401
|
+
this._run('DELETE FROM pending_requests WHERE agent_id = ? AND session_id = ?', [agentId, sessionId]);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ─── Temp Numbers ──────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
saveTempNumber({ agent_id, number, expires_at }) {
|
|
407
|
+
const now = new Date().toISOString();
|
|
408
|
+
this._run(
|
|
409
|
+
'INSERT OR REPLACE INTO temp_numbers (number, agent_id, expires_at, created_at) VALUES (?, ?, ?, ?)',
|
|
410
|
+
[number, agent_id, expires_at, now]
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ─── Offline Queue ─────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
enqueueOffline({ agent_id, target_id, data }) {
|
|
417
|
+
const now = new Date().toISOString();
|
|
418
|
+
this._run(
|
|
419
|
+
'INSERT INTO offline_queue (agent_id, target_id, data, created_at) VALUES (?, ?, ?, ?)',
|
|
420
|
+
[agent_id, target_id, data, now]
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
dequeueOffline(agentId, targetId, limit = 100) {
|
|
425
|
+
const rows = this._all(
|
|
426
|
+
'SELECT * FROM offline_queue WHERE agent_id = ? AND target_id = ? ORDER BY created_at ASC LIMIT ?',
|
|
427
|
+
[agentId, targetId, limit]
|
|
428
|
+
);
|
|
429
|
+
if (rows.length > 0) {
|
|
430
|
+
const ids = rows.map(r => r.id);
|
|
431
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
432
|
+
this._run(`DELETE FROM offline_queue WHERE id IN (${placeholders})`, ids);
|
|
433
|
+
}
|
|
434
|
+
return rows;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ─── Cleanup ───────────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
cleanup() {
|
|
440
|
+
const now = new Date().toISOString();
|
|
441
|
+
this._run("DELETE FROM temp_numbers WHERE expires_at < ?", [now]);
|
|
442
|
+
this._run("DELETE FROM pending_requests WHERE timestamp < datetime(?, '-48 hours')", [now]);
|
|
443
|
+
this._run("DELETE FROM offline_queue WHERE created_at < datetime(?, '-7 days')", [now]);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
close() {
|
|
447
|
+
if (this._saveTimer) {
|
|
448
|
+
clearTimeout(this._saveTimer);
|
|
449
|
+
}
|
|
450
|
+
this._save(); // Final save before closing
|
|
451
|
+
this.db.close();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = PluginDatabase;
|