aicq-chat-plugin 2.5.9 → 2.6.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/lib/chat.js CHANGED
File without changes
package/lib/crypto.js CHANGED
File without changes
package/lib/database.js CHANGED
@@ -1,7 +1,16 @@
1
1
  /**
2
- * AICQ Plugin Database — SQLite via better-sqlite3
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();
3
12
  */
4
- const Database = require('better-sqlite3');
13
+ const initSqlJs = require('sql.js');
5
14
  const path = require('path');
6
15
  const fs = require('fs');
7
16
  const crypto = require('crypto');
@@ -9,18 +18,57 @@ const crypto = require('crypto');
9
18
  class PluginDatabase {
10
19
  constructor(dataDir) {
11
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;
12
25
  fs.mkdirSync(dataDir, { recursive: true });
13
- const dbPath = path.join(dataDir, 'aicq-plugin.db');
14
- this.db = new Database(dbPath);
15
- this.db.pragma('journal_mode = WAL');
16
- this.db.pragma('synchronous = NORMAL');
17
- this.db.pragma('foreign_keys = ON');
18
- this.db.pragma('busy_timeout = 5000');
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');
19
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
+ }
20
60
  }
21
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 ──────────────────────────────────────────────────────────
22
70
  _initSchema() {
23
- this.db.exec(`
71
+ this._execScript(`
24
72
  CREATE TABLE IF NOT EXISTS identity (
25
73
  agent_id TEXT PRIMARY KEY,
26
74
  nickname TEXT NOT NULL DEFAULT '',
@@ -46,7 +94,6 @@ class PluginDatabase {
46
94
  ai_name TEXT NOT NULL DEFAULT '',
47
95
  ai_avatar TEXT NOT NULL DEFAULT ''
48
96
  );
49
- CREATE INDEX IF NOT EXISTS idx_friends_agent ON friends(agent_id);
50
97
 
51
98
  CREATE TABLE IF NOT EXISTS groups (
52
99
  id TEXT PRIMARY KEY,
@@ -59,7 +106,6 @@ class PluginDatabase {
59
106
  created_at TEXT NOT NULL,
60
107
  updated_at TEXT
61
108
  );
62
- CREATE INDEX IF NOT EXISTS idx_groups_agent ON groups(agent_id);
63
109
 
64
110
  CREATE TABLE IF NOT EXISTS sessions (
65
111
  peer_id TEXT PRIMARY KEY,
@@ -85,8 +131,6 @@ class PluginDatabase {
85
131
  timestamp TEXT NOT NULL,
86
132
  status TEXT NOT NULL DEFAULT 'pending'
87
133
  );
88
- CREATE INDEX IF NOT EXISTS idx_chat_agent_target ON chat_history(agent_id, target_id);
89
- CREATE INDEX IF NOT EXISTS idx_chat_timestamp ON chat_history(agent_id, target_id, timestamp);
90
134
 
91
135
  CREATE TABLE IF NOT EXISTS pending_requests (
92
136
  session_id TEXT PRIMARY KEY,
@@ -110,7 +154,6 @@ class PluginDatabase {
110
154
  data TEXT NOT NULL,
111
155
  created_at TEXT NOT NULL
112
156
  );
113
- CREATE INDEX IF NOT EXISTS idx_offline_target ON offline_queue(agent_id, target_id);
114
157
 
115
158
  CREATE TABLE IF NOT EXISTS group_settings (
116
159
  group_id TEXT PRIMARY KEY,
@@ -118,48 +161,104 @@ class PluginDatabase {
118
161
  silent_mode INTEGER NOT NULL DEFAULT 0
119
162
  );
120
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;
121
220
  }
122
221
 
123
222
  // ─── Identity ──────────────────────────────────────────────────────
124
223
 
125
224
  saveIdentity({ agent_id, nickname, signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key }) {
126
225
  const now = new Date().toISOString();
127
- this.db.prepare(`
128
- INSERT OR REPLACE INTO identity (agent_id, nickname, signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key, created_at, updated_at)
129
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
130
- `).run(agent_id, nickname || '', signing_public_key, signing_secret_key, exchange_public_key, exchange_secret_key, now, now);
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
+ );
131
231
  }
132
232
 
133
233
  loadIdentity(agentId) {
134
- return this.db.prepare('SELECT * FROM identity WHERE agent_id = ?').get(agentId);
234
+ return this._get('SELECT * FROM identity WHERE agent_id = ?', [agentId]);
135
235
  }
136
236
 
137
237
  listIdentities() {
138
- return this.db.prepare('SELECT agent_id, nickname, signing_public_key, exchange_public_key, created_at FROM identity').all();
238
+ return this._all('SELECT agent_id, nickname, signing_public_key, exchange_public_key, created_at FROM identity');
139
239
  }
140
240
 
141
241
  deleteIdentity(agentId) {
142
- this.db.prepare('DELETE FROM identity WHERE agent_id = ?').run(agentId);
143
- this.db.prepare('DELETE FROM friends WHERE agent_id = ?').run(agentId);
144
- this.db.prepare('DELETE FROM groups WHERE agent_id = ?').run(agentId);
145
- this.db.prepare('DELETE FROM chat_history WHERE agent_id = ?').run(agentId);
146
- this.db.prepare('DELETE FROM sessions WHERE agent_id = ?').run(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]);
147
247
  }
148
248
 
149
249
  updateNickname(agentId, nickname) {
150
250
  const now = new Date().toISOString();
151
- this.db.prepare('UPDATE identity SET nickname = ?, updated_at = ? WHERE agent_id = ?').run(nickname, now, agentId);
251
+ this._run('UPDATE identity SET nickname = ?, updated_at = ? WHERE agent_id = ?', [nickname, now, agentId]);
152
252
  }
153
253
 
154
254
  updateAvatar(agentId, avatarUrl) {
155
255
  const now = new Date().toISOString();
156
- // Ensure column exists (migration for existing databases)
157
256
  try {
158
- this.db.prepare('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?').run(avatarUrl, now, agentId);
257
+ this._run('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?', [avatarUrl, now, agentId]);
159
258
  } catch (e) {
160
- if (e.message.includes('no column named avatar')) {
161
- this.db.exec('ALTER TABLE identity ADD COLUMN avatar TEXT NOT NULL DEFAULT ""');
162
- this.db.prepare('UPDATE identity SET avatar = ?, updated_at = ? WHERE agent_id = ?').run(avatarUrl, now, agentId);
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]);
163
262
  } else {
164
263
  throw e;
165
264
  }
@@ -170,57 +269,63 @@ class PluginDatabase {
170
269
 
171
270
  addFriend({ agent_id, id, public_key, fingerprint, friend_type = 'ai', ai_name = '', permissions = ['chat'] }) {
172
271
  const now = new Date().toISOString();
173
- this.db.prepare(`
174
- INSERT OR REPLACE INTO friends (id, agent_id, public_key, fingerprint, added_at, is_online, permissions, friend_type, ai_name, ai_avatar)
175
- VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, '')
176
- `).run(id, agent_id, public_key, fingerprint, now, JSON.stringify(permissions), friend_type, ai_name);
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
+ );
177
277
  }
178
278
 
179
279
  removeFriend(agentId, friendId) {
180
- this.db.prepare('DELETE FROM friends WHERE agent_id = ? AND id = ?').run(agentId, friendId);
181
- this.db.prepare('DELETE FROM sessions WHERE agent_id = ? AND peer_id = ?').run(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]);
182
282
  }
183
283
 
184
284
  getFriend(agentId, friendId) {
185
- return this.db.prepare('SELECT * FROM friends WHERE agent_id = ? AND id = ?').get(agentId, friendId);
285
+ return this._get('SELECT * FROM friends WHERE agent_id = ? AND id = ?', [agentId, friendId]);
186
286
  }
187
287
 
188
288
  listFriends(agentId) {
189
- return this.db.prepare('SELECT * FROM friends WHERE agent_id = ? ORDER BY added_at DESC').all(agentId);
289
+ return this._all('SELECT * FROM friends WHERE agent_id = ? ORDER BY added_at DESC', [agentId]);
190
290
  }
191
291
 
192
292
  updateFriendOnline(agentId, friendId, isOnline) {
193
293
  const now = isOnline ? new Date().toISOString() : null;
194
- this.db.prepare('UPDATE friends SET is_online = ?, last_seen = COALESCE(?, last_seen) WHERE agent_id = ? AND id = ?')
195
- .run(isOnline ? 1 : 0, now, agentId, friendId);
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
+ );
196
299
  }
197
300
 
198
301
  // ─── Groups ────────────────────────────────────────────────────────
199
302
 
200
303
  addGroup({ agent_id, id, name, owner_id, members_json = '[]', description = '' }) {
201
304
  const now = new Date().toISOString();
202
- this.db.prepare(`
203
- INSERT OR REPLACE INTO groups (id, agent_id, name, owner_id, members_json, description, silent_mode, created_at, updated_at)
204
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
205
- `).run(id, agent_id, name, owner_id, typeof members_json === 'string' ? members_json : JSON.stringify(members_json), description, now, now);
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
+ );
206
310
  }
207
311
 
208
312
  listGroups(agentId) {
209
- return this.db.prepare('SELECT * FROM groups WHERE agent_id = ? ORDER BY created_at DESC').all(agentId);
313
+ return this._all('SELECT * FROM groups WHERE agent_id = ? ORDER BY created_at DESC', [agentId]);
210
314
  }
211
315
 
212
316
  getGroup(agentId, groupId) {
213
- return this.db.prepare('SELECT * FROM groups WHERE agent_id = ? AND id = ?').get(agentId, groupId);
317
+ return this._get('SELECT * FROM groups WHERE agent_id = ? AND id = ?', [agentId, groupId]);
214
318
  }
215
319
 
216
320
  setGroupSilentMode(agentId, groupId, silent) {
217
- this.db.prepare(`
218
- INSERT OR REPLACE INTO group_settings (group_id, agent_id, silent_mode) VALUES (?, ?, ?)
219
- `).run(groupId, agentId, silent ? 1 : 0);
321
+ this._run(
322
+ 'INSERT OR REPLACE INTO group_settings (group_id, agent_id, silent_mode) VALUES (?, ?, ?)',
323
+ [groupId, agentId, silent ? 1 : 0]
324
+ );
220
325
  }
221
326
 
222
327
  getGroupSilentMode(agentId, groupId) {
223
- const row = this.db.prepare('SELECT silent_mode FROM group_settings WHERE group_id = ? AND agent_id = ?').get(groupId, agentId);
328
+ const row = this._get('SELECT silent_mode FROM group_settings WHERE group_id = ? AND agent_id = ?', [groupId, agentId]);
224
329
  return row ? !!row.silent_mode : false;
225
330
  }
226
331
 
@@ -228,18 +333,19 @@ class PluginDatabase {
228
333
 
229
334
  saveSession({ agent_id, peer_id, session_key }) {
230
335
  const now = new Date().toISOString();
231
- this.db.prepare(`
232
- INSERT OR REPLACE INTO sessions (peer_id, agent_id, session_key, created_at, message_count, last_rotation)
233
- VALUES (?, ?, ?, ?, 0, ?)
234
- `).run(peer_id, agent_id, session_key, now, now);
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
+ );
235
341
  }
236
342
 
237
343
  loadSession(agentId, peerId) {
238
- return this.db.prepare('SELECT * FROM sessions WHERE agent_id = ? AND peer_id = ?').get(agentId, peerId);
344
+ return this._get('SELECT * FROM sessions WHERE agent_id = ? AND peer_id = ?', [agentId, peerId]);
239
345
  }
240
346
 
241
347
  incrementSessionMessageCount(agentId, peerId) {
242
- this.db.prepare('UPDATE sessions SET message_count = message_count + 1 WHERE agent_id = ? AND peer_id = ?').run(agentId, peerId);
348
+ this._run('UPDATE sessions SET message_count = message_count + 1 WHERE agent_id = ? AND peer_id = ?', [agentId, peerId]);
243
349
  }
244
350
 
245
351
  // ─── Chat History ──────────────────────────────────────────────────
@@ -247,73 +353,83 @@ class PluginDatabase {
247
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' }) {
248
354
  const id = crypto.randomUUID();
249
355
  const now = new Date().toISOString();
250
- this.db.prepare(`
251
- INSERT INTO chat_history (id, agent_id, target_id, from_id, to_id, type, content, file_url, file_name, is_group, mentions, timestamp, status)
252
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
- `).run(id, agent_id, target_id, from_id, to_id, type, content, file_url, file_name, is_group, JSON.stringify(mentions), now, status);
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
+ );
254
361
  return { id, timestamp: now };
255
362
  }
256
363
 
257
364
  getChatHistory(agentId, targetId, { limit = 50, before = null } = {}) {
258
365
  if (before) {
259
- return this.db.prepare(
260
- 'SELECT * FROM chat_history WHERE agent_id = ? AND target_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?'
261
- ).all(agentId, targetId, before, limit);
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
+ );
262
370
  }
263
- return this.db.prepare(
264
- 'SELECT * FROM chat_history WHERE agent_id = ? AND target_id = ? ORDER BY timestamp DESC LIMIT ?'
265
- ).all(agentId, targetId, limit);
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
+ );
266
375
  }
267
376
 
268
377
  deleteMessage(agentId, messageId) {
269
- this.db.prepare('DELETE FROM chat_history WHERE agent_id = ? AND id = ?').run(agentId, messageId);
378
+ this._run('DELETE FROM chat_history WHERE agent_id = ? AND id = ?', [agentId, messageId]);
270
379
  }
271
380
 
272
381
  updateMessageStatus(agentId, messageId, status) {
273
- this.db.prepare('UPDATE chat_history SET status = ? WHERE agent_id = ? AND id = ?').run(status, agentId, messageId);
382
+ this._run('UPDATE chat_history SET status = ? WHERE agent_id = ? AND id = ?', [status, agentId, messageId]);
274
383
  }
275
384
 
276
385
  // ─── Pending Requests ──────────────────────────────────────────────
277
386
 
278
387
  savePendingRequest({ agent_id, session_id, requester_id, requester_public_key }) {
279
388
  const now = new Date().toISOString();
280
- this.db.prepare(`
281
- INSERT OR REPLACE INTO pending_requests (session_id, agent_id, requester_id, requester_public_key, timestamp)
282
- VALUES (?, ?, ?, ?, ?)
283
- `).run(session_id, agent_id, requester_id, requester_public_key, now);
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
+ );
284
394
  }
285
395
 
286
396
  getPendingRequests(agentId) {
287
- return this.db.prepare('SELECT * FROM pending_requests WHERE agent_id = ? ORDER BY timestamp DESC').all(agentId);
397
+ return this._all('SELECT * FROM pending_requests WHERE agent_id = ? ORDER BY timestamp DESC', [agentId]);
288
398
  }
289
399
 
290
400
  removePendingRequest(agentId, sessionId) {
291
- this.db.prepare('DELETE FROM pending_requests WHERE agent_id = ? AND session_id = ?').run(agentId, sessionId);
401
+ this._run('DELETE FROM pending_requests WHERE agent_id = ? AND session_id = ?', [agentId, sessionId]);
292
402
  }
293
403
 
294
404
  // ─── Temp Numbers ──────────────────────────────────────────────────
295
405
 
296
406
  saveTempNumber({ agent_id, number, expires_at }) {
297
407
  const now = new Date().toISOString();
298
- this.db.prepare('INSERT OR REPLACE INTO temp_numbers (number, agent_id, expires_at, created_at) VALUES (?, ?, ?, ?)')
299
- .run(number, agent_id, expires_at, now);
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
+ );
300
412
  }
301
413
 
302
414
  // ─── Offline Queue ─────────────────────────────────────────────────
303
415
 
304
416
  enqueueOffline({ agent_id, target_id, data }) {
305
417
  const now = new Date().toISOString();
306
- this.db.prepare('INSERT INTO offline_queue (agent_id, target_id, data, created_at) VALUES (?, ?, ?, ?)')
307
- .run(agent_id, target_id, data, now);
418
+ this._run(
419
+ 'INSERT INTO offline_queue (agent_id, target_id, data, created_at) VALUES (?, ?, ?, ?)',
420
+ [agent_id, target_id, data, now]
421
+ );
308
422
  }
309
423
 
310
424
  dequeueOffline(agentId, targetId, limit = 100) {
311
- const rows = this.db.prepare('SELECT * FROM offline_queue WHERE agent_id = ? AND target_id = ? ORDER BY created_at ASC LIMIT ?')
312
- .all(agentId, targetId, limit);
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
+ );
313
429
  if (rows.length > 0) {
314
430
  const ids = rows.map(r => r.id);
315
431
  const placeholders = ids.map(() => '?').join(',');
316
- this.db.prepare(`DELETE FROM offline_queue WHERE id IN (${placeholders})`).run(...ids);
432
+ this._run(`DELETE FROM offline_queue WHERE id IN (${placeholders})`, ids);
317
433
  }
318
434
  return rows;
319
435
  }
@@ -322,12 +438,16 @@ class PluginDatabase {
322
438
 
323
439
  cleanup() {
324
440
  const now = new Date().toISOString();
325
- this.db.prepare("DELETE FROM temp_numbers WHERE expires_at < ?").run(now);
326
- this.db.prepare("DELETE FROM pending_requests WHERE timestamp < datetime(?, '-48 hours')").run(now);
327
- this.db.prepare("DELETE FROM offline_queue WHERE created_at < datetime(?, '-7 days')").run(now);
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]);
328
444
  }
329
445
 
330
446
  close() {
447
+ if (this._saveTimer) {
448
+ clearTimeout(this._saveTimer);
449
+ }
450
+ this._save(); // Final save before closing
331
451
  this.db.close();
332
452
  }
333
453
  }
File without changes
package/lib/handshake.js CHANGED
File without changes
package/lib/identity.js CHANGED
File without changes
@@ -6,7 +6,7 @@ const fetch = require('node-fetch');
6
6
  const { signMessage, computeFingerprint } = require('./crypto');
7
7
 
8
8
  class ServerClient {
9
- constructor(identityManager, db, serverUrl = 'http://aicq.online:61018') {
9
+ constructor(identityManager, db, serverUrl = 'https://aicq.online') {
10
10
  this.identity = identityManager;
11
11
  this.db = db;
12
12
  this.serverUrl = serverUrl;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "aicq-chat",
3
3
  "name": "AICQ Encrypted Chat",
4
- "version": "2.5.9",
4
+ "version": "2.6.1",
5
5
  "description": "End-to-end encrypted chat plugin for OpenClaw agents — Node.js implementation with full UI",
6
6
  "entry": "index.js",
7
7
  "activation": {
@@ -96,7 +96,7 @@
96
96
  "requires": {
97
97
  "node": ">=18.0.0",
98
98
  "packages": [
99
- "better-sqlite3",
99
+ "sql.js",
100
100
  "tweetnacl",
101
101
  "ws",
102
102
  "qrcode",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aicq-chat-plugin",
3
- "version": "2.5.9",
4
- "description": "AICQ End-to-end Encrypted Chat Plugin for OpenClaw \u2014 Full UI with friend management, group chat, file transfer, and AI agent communication",
3
+ "version": "2.6.1",
4
+ "description": "AICQ End-to-end Encrypted Chat Plugin for OpenClaw Full UI with friend management, group chat, file transfer, and AI agent communication",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "aicq-plugin": "cli.js"
@@ -37,23 +37,23 @@
37
37
  "license": "MIT",
38
38
  "repository": {
39
39
  "type": "git",
40
- "url": "git+https://github.com/ctz168/aicq.git",
41
- "directory": "plugin-js"
40
+ "url": "git+https://github.com/ctz168/pluginAICQ.git",
41
+ "directory": "openclaw-plugin"
42
42
  },
43
43
  "bugs": {
44
- "url": "https://github.com/ctz168/aicq/issues"
44
+ "url": "https://github.com/ctz168/pluginAICQ/issues"
45
45
  },
46
46
  "homepage": "https://aicq.online",
47
47
  "dependencies": {
48
- "better-sqlite3": "^11.0.0",
49
- "tweetnacl": "^1.0.3",
50
- "tweetnacl-util": "^0.15.1",
51
- "ws": "^8.16.0",
52
- "qrcode": "^1.5.3",
53
48
  "express": "^4.18.2",
54
- "multer": "^1.4.5-lts.1",
55
49
  "marked": "^12.0.0",
56
- "node-fetch": "^2.7.0"
50
+ "multer": "^1.4.5-lts.1",
51
+ "node-fetch": "^2.7.0",
52
+ "qrcode": "^1.5.3",
53
+ "sql.js": "^1.14.1",
54
+ "tweetnacl": "^1.0.3",
55
+ "tweetnacl-util": "^0.15.1",
56
+ "ws": "^8.16.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=18.0.0"
package/postinstall.js CHANGED
@@ -42,7 +42,17 @@ AICQ 是一个端到端加密聊天插件,适用于 OpenClaw 的完整聊天 U
42
42
  ## 一键启动
43
43
 
44
44
  \`\`\`bash
45
- npx aicq-chat-plugin
45
+ # 1. 卸载旧版
46
+ openclaw plugins uninstall aicq-chat
47
+
48
+ # 2. 安装新版
49
+ openclaw plugins install npm:aicq-chat-plugin
50
+
51
+ # 3. 重启 gateway
52
+ openclaw gateway restart
53
+
54
+ # 4. 浏览器访问聊天界面
55
+ open http://localhost:6109
46
56
  \`\`\`
47
57
 
48
58
  ## OpenClaw 集成
@@ -66,7 +76,7 @@ npx aicq-chat-plugin
66
76
  | 变量 | 默认值 | 说明 |
67
77
  |------|--------|------|
68
78
  | \`AICQ_PORT\` | 6109 | 插件服务端口 |
69
- | \`AICQ_SERVER_URL\` | http://aicq.online:61018 | AICQ 服务器地址 |
79
+ | \`AICQ_SERVER_URL\` | https://aicq.online | AICQ 服务器地址 |
70
80
  | \`AICQ_DATA_DIR\` | ~/.aicq-plugin | 数据存储目录 |
71
81
 
72
82
  ## Chat UI
@@ -270,7 +280,7 @@ console.log(' ╚════════════════════
270
280
  console.log('');
271
281
 
272
282
  // Read version from package.json
273
- let version = '2.5.9';
283
+ let version = '2.6.0';
274
284
  try {
275
285
  const pkg = JSON.parse(fs.readFileSync(path.join(PLUGIN_DIR, 'package.json'), 'utf8'));
276
286
  version = pkg.version;
@@ -335,8 +345,8 @@ if (skillInstalled || pluginInstalled) {
335
345
  console.log(' ║ OPENCLAW_HOME=<openclaw-root> ║');
336
346
  console.log(' ║ OPENCLAW_WORKSPACE=<workspace-dir> ║');
337
347
  console.log(' ║ ║');
338
- console.log(' ║ Or start standalone: ║');
339
- console.log(' ║ npx aicq-chat-plugin ║');
348
+ console.log(' ║ Or install via openclaw CLI: ║');
349
+ console.log(' ║ openclaw plugins install npm:aicq-chat-plugin ║');
340
350
  console.log(' ║ ║');
341
351
  console.log(' ║ Chat UI: http://localhost:6109 ║');
342
352
  console.log(' ║ Docs: https://aicq.online ║');
Binary file
Binary file
Binary file