aicq-chat-plugin 2.1.0

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/index.js ADDED
@@ -0,0 +1,499 @@
1
+ /**
2
+ * AICQ Chat Plugin — Main Entry Point
3
+ * OpenClaw sidecar plugin providing E2EE chat UI
4
+ */
5
+ const express = require('express');
6
+ const multer = require('multer');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const QRCode = require('qrcode');
11
+ const PluginDatabase = require('./lib/database');
12
+ const IdentityManager = require('./lib/identity');
13
+ const ServerClient = require('./lib/server-client');
14
+ const HandshakeManager = require('./lib/handshake');
15
+ const ChatManager = require('./lib/chat');
16
+
17
+ // ─── Configuration ──────────────────────────────────────────────────
18
+ const PORT = parseInt(process.env.AICQ_PORT || '6109', 10);
19
+ const SERVER_URL = process.env.AICQ_SERVER_URL || 'http://aicq.online:61018';
20
+ const DATA_DIR = process.env.AICQ_DATA_DIR || path.join(os.homedir(), '.aicq-plugin');
21
+ const UPLOADS_DIR = path.join(DATA_DIR, 'uploads');
22
+
23
+ fs.mkdirSync(DATA_DIR, { recursive: true });
24
+ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
25
+
26
+ // ─── Initialize Components ──────────────────────────────────────────
27
+ const db = new PluginDatabase(DATA_DIR);
28
+ const identity = new IdentityManager(db);
29
+ const serverClient = new ServerClient(identity, db, SERVER_URL);
30
+ const handshake = new HandshakeManager(identity, serverClient, db);
31
+ const chat = new ChatManager(identity, serverClient, db, UPLOADS_DIR);
32
+
33
+ // Auto-create a default agent if none exists
34
+ const agents = identity.listAgents();
35
+ let currentAgentId = null;
36
+ if (agents.length === 0) {
37
+ const defaultAgent = identity.createAgent('agent-' + Date.now(), '默认Agent');
38
+ currentAgentId = defaultAgent.agent_id;
39
+ console.log('[AICQ] Created default agent:', currentAgentId);
40
+ } else {
41
+ currentAgentId = agents[0].agent_id;
42
+ }
43
+
44
+ // Connect to server in background
45
+ (async () => {
46
+ try {
47
+ await serverClient.start(currentAgentId);
48
+ // Sync friends from server
49
+ await syncFriendsFromServer(currentAgentId);
50
+ await syncGroupsFromServer(currentAgentId);
51
+ } catch (e) {
52
+ console.error('[AICQ] Initial server connection failed:', e.message);
53
+ }
54
+ })();
55
+
56
+ // Periodic cleanup
57
+ setInterval(() => db.cleanup(), 3600000);
58
+
59
+ // ─── Helper: get current agent ID ──────────────────────────────────
60
+ function getAgentId(req) {
61
+ return req.query.agent_id || req.body?.agent_id || currentAgentId;
62
+ }
63
+
64
+ // ─── Sync friends/groups from server ────────────────────────────────
65
+ async function syncFriendsFromServer(agentId) {
66
+ try {
67
+ await serverClient.ensureAuth(agentId);
68
+ const result = await serverClient.listFriends();
69
+ if (result.friends) {
70
+ for (const f of result.friends) {
71
+ const existing = db.getFriend(agentId, f.id);
72
+ if (!existing) {
73
+ db.addFriend({
74
+ agent_id: agentId,
75
+ id: f.id,
76
+ public_key: f.public_key || f.publicKey || '',
77
+ fingerprint: f.fingerprint || '',
78
+ friend_type: f.type || f.friend_type || 'ai',
79
+ ai_name: f.agent_name || f.ai_name || f.displayName || '',
80
+ });
81
+ } else {
82
+ db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
83
+ }
84
+ }
85
+ }
86
+ } catch (e) {
87
+ console.error('[AICQ] Sync friends failed:', e.message);
88
+ }
89
+ }
90
+
91
+ async function syncGroupsFromServer(agentId) {
92
+ try {
93
+ await serverClient.ensureAuth(agentId);
94
+ const result = await serverClient.listGroups();
95
+ if (result.groups) {
96
+ for (const g of result.groups) {
97
+ db.addGroup({
98
+ agent_id: agentId,
99
+ id: g.id,
100
+ name: g.name,
101
+ owner_id: g.owner_id || g.ownerId || '',
102
+ members_json: g.members || g.members_json || '[]',
103
+ description: g.description || '',
104
+ });
105
+ }
106
+ }
107
+ } catch (e) {
108
+ console.error('[AICQ] Sync groups failed:', e.message);
109
+ }
110
+ }
111
+
112
+ // ─── Express App ────────────────────────────────────────────────────
113
+ const app = express();
114
+ app.use(express.json());
115
+ app.use(express.urlencoded({ extended: true }));
116
+
117
+ const upload = multer({
118
+ storage: multer.memoryStorage(),
119
+ limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
120
+ });
121
+
122
+ // ─── Serve SPA ──────────────────────────────────────────────────────
123
+ app.use(express.static(path.join(__dirname, 'public')));
124
+
125
+ // ─── API Routes ─────────────────────────────────────────────────────
126
+
127
+ // Status
128
+ app.get('/api/status', (req, res) => {
129
+ res.json({
130
+ status: 'running',
131
+ version: '2.1.0',
132
+ connected: serverClient.connected,
133
+ currentAgent: currentAgentId,
134
+ serverUrl: SERVER_URL,
135
+ });
136
+ });
137
+
138
+ // Agents
139
+ app.get('/api/agents', (req, res) => {
140
+ res.json({ agents: identity.listAgents() });
141
+ });
142
+
143
+ app.post('/api/agents', async (req, res) => {
144
+ try {
145
+ const { agent_id, nickname } = req.body;
146
+ if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
147
+ const agent = identity.createAgent(agent_id, nickname);
148
+ currentAgentId = agent_id;
149
+ // Register on server
150
+ try {
151
+ await serverClient.start(agent_id);
152
+ } catch (e) {
153
+ console.error('Server registration failed:', e.message);
154
+ }
155
+ res.json({ success: true, agent });
156
+ } catch (e) {
157
+ res.status(500).json({ error: e.message });
158
+ }
159
+ });
160
+
161
+ app.delete('/api/agents/:id', (req, res) => {
162
+ identity.deleteAgent(req.params.id);
163
+ if (currentAgentId === req.params.id) {
164
+ const remaining = identity.listAgents();
165
+ currentAgentId = remaining.length > 0 ? remaining[0].agent_id : null;
166
+ }
167
+ res.json({ success: true });
168
+ });
169
+
170
+ app.post('/api/agents/switch', async (req, res) => {
171
+ try {
172
+ const { agent_id } = req.body;
173
+ if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
174
+ currentAgentId = agent_id;
175
+ await serverClient.switchAgent(agent_id);
176
+ await syncFriendsFromServer(agent_id);
177
+ await syncGroupsFromServer(agent_id);
178
+ res.json({ success: true, agent_id });
179
+ } catch (e) {
180
+ res.status(500).json({ error: e.message });
181
+ }
182
+ });
183
+
184
+ // Friends
185
+ app.get('/api/friends', (req, res) => {
186
+ const agentId = getAgentId(req);
187
+ res.json({ friends: db.listFriends(agentId) });
188
+ });
189
+
190
+ app.post('/api/friends/add', async (req, res) => {
191
+ try {
192
+ const { temp_number, friend_code, agent_id } = req.body;
193
+ const agentId = agent_id || currentAgentId;
194
+ const code = temp_number || friend_code;
195
+ if (!code) return res.status(400).json({ error: 'temp_number or friend_code is required' });
196
+ const result = await handshake.addFriendByCode(agentId, code);
197
+ res.json({ success: true, result });
198
+ } catch (e) {
199
+ res.status(500).json({ error: e.message });
200
+ }
201
+ });
202
+
203
+ app.delete('/api/friends/:id', async (req, res) => {
204
+ try {
205
+ const agentId = getAgentId(req);
206
+ db.removeFriend(agentId, req.params.id);
207
+ try { await serverClient.removeFriend(req.params.id); } catch (e) {}
208
+ res.json({ success: true });
209
+ } catch (e) {
210
+ res.status(500).json({ error: e.message });
211
+ }
212
+ });
213
+
214
+ app.get('/api/friends/requests', async (req, res) => {
215
+ try {
216
+ const agentId = getAgentId(req);
217
+ // Get from server
218
+ let serverRequests = [];
219
+ try {
220
+ await serverClient.ensureAuth(agentId);
221
+ const result = await serverClient.listFriendRequests();
222
+ serverRequests = result.sent || [];
223
+ serverRequests = serverRequests.concat(result.received || []);
224
+ } catch (e) {}
225
+ const localRequests = db.getPendingRequests(agentId);
226
+ res.json({ requests: [...localRequests, ...serverRequests] });
227
+ } catch (e) {
228
+ res.status(500).json({ error: e.message });
229
+ }
230
+ });
231
+
232
+ app.post('/api/friends/requests/:id/accept', async (req, res) => {
233
+ try {
234
+ const agentId = getAgentId(req);
235
+ const result = await handshake.acceptRequest(agentId, req.params.id);
236
+ res.json(result);
237
+ } catch (e) {
238
+ res.status(500).json({ error: e.message });
239
+ }
240
+ });
241
+
242
+ app.post('/api/friends/requests/:id/reject', async (req, res) => {
243
+ try {
244
+ const agentId = getAgentId(req);
245
+ const result = await handshake.rejectRequest(agentId, req.params.id);
246
+ res.json(result);
247
+ } catch (e) {
248
+ res.status(500).json({ error: e.message });
249
+ }
250
+ });
251
+
252
+ // Groups
253
+ app.get('/api/groups', (req, res) => {
254
+ const agentId = getAgentId(req);
255
+ res.json({ groups: db.listGroups(agentId) });
256
+ });
257
+
258
+ app.post('/api/groups', async (req, res) => {
259
+ try {
260
+ const agentId = getAgentId(req);
261
+ const { name, description } = req.body;
262
+ if (!name) return res.status(400).json({ error: 'name is required' });
263
+ await serverClient.ensureAuth(agentId);
264
+ const result = await serverClient.createGroup(name, description);
265
+ if (result.id) {
266
+ db.addGroup({
267
+ agent_id: agentId,
268
+ id: result.id,
269
+ name,
270
+ owner_id: agentId,
271
+ members_json: result.members || '[]',
272
+ description: description || '',
273
+ });
274
+ }
275
+ res.json({ success: true, group: result });
276
+ } catch (e) {
277
+ res.status(500).json({ error: e.message });
278
+ }
279
+ });
280
+
281
+ app.post('/api/groups/:id/join', async (req, res) => {
282
+ try {
283
+ const agentId = getAgentId(req);
284
+ await serverClient.ensureAuth(agentId);
285
+ const result = await serverClient.inviteGroupMember(req.params.id, agentId);
286
+ res.json({ success: true, result });
287
+ } catch (e) {
288
+ res.status(500).json({ error: e.message });
289
+ }
290
+ });
291
+
292
+ app.get('/api/groups/:id/messages', async (req, res) => {
293
+ try {
294
+ const agentId = getAgentId(req);
295
+ const limit = parseInt(req.query.limit || '50', 10);
296
+ const before = req.query.before || null;
297
+ // Try server first
298
+ try {
299
+ await serverClient.ensureAuth(agentId);
300
+ const result = await serverClient.getGroupMessages(req.params.id, limit, before);
301
+ if (result.messages && result.messages.length > 0) {
302
+ return res.json({ messages: result.messages });
303
+ }
304
+ } catch (e) {}
305
+ // Fallback to local
306
+ const messages = db.getChatHistory(agentId, req.params.id, { limit, before });
307
+ res.json({ messages });
308
+ } catch (e) {
309
+ res.status(500).json({ error: e.message });
310
+ }
311
+ });
312
+
313
+ app.put('/api/groups/:id/silent', (req, res) => {
314
+ const agentId = getAgentId(req);
315
+ const { silent } = req.body;
316
+ db.setGroupSilentMode(agentId, req.params.id, !!silent);
317
+ res.json({ success: true, silent: !!silent });
318
+ });
319
+
320
+ // Chat
321
+ app.get('/api/chat/:targetId', (req, res) => {
322
+ const agentId = getAgentId(req);
323
+ const limit = parseInt(req.query.limit || '50', 10);
324
+ const before = req.query.before || null;
325
+ const messages = db.getChatHistory(agentId, req.params.targetId, { limit, before });
326
+ res.json({ messages });
327
+ });
328
+
329
+ app.post('/api/chat/send', async (req, res) => {
330
+ try {
331
+ const { agent_id, targetId, content, type, isGroup, mentions, file_url, file_name } = req.body;
332
+ const agentId = agent_id || currentAgentId;
333
+ if (!targetId || !content) return res.status(400).json({ error: 'targetId and content are required' });
334
+ const msg = await chat.sendMessage(agentId, targetId, content, {
335
+ type: type || 'text',
336
+ isGroup: !!isGroup,
337
+ mentions: mentions || [],
338
+ file_url,
339
+ file_name,
340
+ });
341
+ res.json({ success: true, message: msg });
342
+ } catch (e) {
343
+ res.status(500).json({ error: e.message });
344
+ }
345
+ });
346
+
347
+ app.delete('/api/chat/:messageId', (req, res) => {
348
+ const agentId = getAgentId(req);
349
+ db.deleteMessage(agentId, req.params.messageId);
350
+ res.json({ success: true });
351
+ });
352
+
353
+ // File upload
354
+ app.post('/api/upload', upload.single('file'), async (req, res) => {
355
+ try {
356
+ if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
357
+ const agentId = getAgentId(req);
358
+ const targetId = req.body.targetId;
359
+ const isGroup = req.body.isGroup === 'true' || req.body.isGroup === '1';
360
+ const msg = await chat.handleFileUpload(agentId, targetId, req.file, isGroup);
361
+ res.json({ success: true, message: msg });
362
+ } catch (e) {
363
+ res.status(500).json({ error: e.message });
364
+ }
365
+ });
366
+
367
+ app.get('/api/files/:fileId', (req, res) => {
368
+ const filePath = path.join(UPLOADS_DIR, req.params.fileId);
369
+ if (fs.existsSync(filePath)) {
370
+ res.sendFile(filePath);
371
+ } else {
372
+ res.status(404).json({ error: 'File not found' });
373
+ }
374
+ });
375
+
376
+ // Identity
377
+ app.get('/api/identity', (req, res) => {
378
+ const agentId = getAgentId(req);
379
+ res.json(identity.getInfo(agentId) || {});
380
+ });
381
+
382
+ app.post('/api/identity/nickname', (req, res) => {
383
+ const { agent_id, nickname } = req.body;
384
+ const agentId = agent_id || currentAgentId;
385
+ identity.updateNickname(agentId, nickname);
386
+ res.json({ success: true });
387
+ });
388
+
389
+ app.post('/api/identity/friend-code', async (req, res) => {
390
+ try {
391
+ const agentId = req.body.agent_id || currentAgentId;
392
+ await serverClient.ensureAuth(agentId);
393
+ const result = await handshake.generateFriendCode(agentId);
394
+ res.json({ success: true, code: result.number, expires_at: result.expiresAt || result.expires_at });
395
+ } catch (e) {
396
+ res.status(500).json({ error: e.message });
397
+ }
398
+ });
399
+
400
+ app.get('/api/identity/qr', async (req, res) => {
401
+ try {
402
+ const agentId = getAgentId(req);
403
+ const info = identity.getInfo(agentId);
404
+ if (!info) return res.status(404).json({ error: 'Agent not found' });
405
+ const qrData = JSON.stringify({
406
+ type: 'aicq-friend',
407
+ agent_id: info.agent_id,
408
+ public_key: info.signing_public_key,
409
+ exchange_public_key: info.exchange_public_key,
410
+ fingerprint: info.fingerprint,
411
+ });
412
+ const qrImage = await QRCode.toDataURL(qrData);
413
+ res.json({ qr: qrImage, data: qrData, info });
414
+ } catch (e) {
415
+ res.status(500).json({ error: e.message });
416
+ }
417
+ });
418
+
419
+ app.post('/api/identity/rotate-keys', (req, res) => {
420
+ try {
421
+ const agentId = req.body.agent_id || currentAgentId;
422
+ const newInfo = identity.rotateKeys(agentId);
423
+ res.json({ success: true, info: identity.getInfo(agentId) });
424
+ } catch (e) {
425
+ res.status(500).json({ error: e.message });
426
+ }
427
+ });
428
+
429
+ app.get('/api/identity/keys', (req, res) => {
430
+ const agentId = getAgentId(req);
431
+ const info = identity.loadAgent(agentId);
432
+ if (!info) return res.status(404).json({ error: 'Agent not found' });
433
+ res.json({
434
+ agent_id: info.agent_id,
435
+ nickname: info.nickname,
436
+ signing_public_key: info.signing_public_key,
437
+ exchange_public_key: info.exchange_public_key,
438
+ signing_secret_key: info.signing_secret_key,
439
+ exchange_secret_key: info.exchange_secret_key,
440
+ fingerprint: info.fingerprint,
441
+ });
442
+ });
443
+
444
+ // Sync endpoint
445
+ app.post('/api/sync', async (req, res) => {
446
+ try {
447
+ const agentId = req.body.agent_id || currentAgentId;
448
+ await syncFriendsFromServer(agentId);
449
+ await syncGroupsFromServer(agentId);
450
+ res.json({ success: true });
451
+ } catch (e) {
452
+ res.status(500).json({ error: e.message });
453
+ }
454
+ });
455
+
456
+ // ─── Start Server ───────────────────────────────────────────────────
457
+ app.listen(PORT, '0.0.0.0', () => {
458
+ console.log(`[AICQ Plugin] Running on http://0.0.0.0:${PORT}`);
459
+ console.log(`[AICQ Plugin] Server: ${SERVER_URL}`);
460
+ console.log(`[AICQ Plugin] Data dir: ${DATA_DIR}`);
461
+ });
462
+
463
+ // ─── OpenClaw Gateway Integration ───────────────────────────────────
464
+ process.on('message', (msg) => {
465
+ if (msg.type === 'gateway_call') {
466
+ handleGatewayCall(msg.method, msg.kwargs).then(result => {
467
+ process.send({ type: 'gateway_response', id: msg.id, result });
468
+ }).catch(err => {
469
+ process.send({ type: 'gateway_response', id: msg.id, error: err.message });
470
+ });
471
+ }
472
+ });
473
+
474
+ async function handleGatewayCall(method, kwargs = {}) {
475
+ switch (method) {
476
+ case 'aicq.status':
477
+ return { state: serverClient.connected ? 'connected' : 'disconnected', agent_id: currentAgentId, version: '2.1.0' };
478
+ case 'aicq.friends.list':
479
+ return { friends: db.listFriends(currentAgentId) };
480
+ case 'aicq.friends.add':
481
+ return await handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
482
+ case 'aicq.friends.remove':
483
+ db.removeFriend(currentAgentId, kwargs.friend_id);
484
+ return { success: true };
485
+ case 'aicq.friends.requests':
486
+ return { requests: db.getPendingRequests(currentAgentId) };
487
+ case 'aicq.identity.info':
488
+ return identity.getInfo(currentAgentId) || {};
489
+ case 'aicq.agent.create':
490
+ identity.createAgent(kwargs.agent_id, kwargs.nickname);
491
+ return { success: true };
492
+ case 'aicq.chat.send':
493
+ return await chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
494
+ case 'aicq.chat.history':
495
+ return { messages: db.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) };
496
+ default:
497
+ return { error: `Unknown method: ${method}` };
498
+ }
499
+ }