aicq-chat-plugin 2.6.5 → 3.0.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/src/channel.js ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * AICQ Channel Plugin — Core Channel Logic
3
+ *
4
+ * Wraps existing lib/ modules (identity, server-client, handshake, chat, database)
5
+ * into the OpenClaw Channel plugin interface via createChatChannelPlugin.
6
+ *
7
+ * Architecture: In-process Channel (no sidecar, no independent port)
8
+ */
9
+ const { encryptMessage, decryptMessage, deriveSessionKey, computeFingerprint } = require('../lib/crypto');
10
+
11
+ /**
12
+ * Create the AICQ channel plugin
13
+ * @param {Object} ctx - Plugin context with managers and config
14
+ */
15
+ function createAicqChannel(ctx) {
16
+ const { db, identity, serverClient, handshake, chat, dataDir, serverUrl } = ctx;
17
+
18
+ return {
19
+ // ── Account Resolution ──
20
+ resolveAccount: async (agentId) => {
21
+ // Use OpenClaw agent ID directly as AICQ account ID
22
+ let agentIdentity = identity.loadAgent(agentId);
23
+ if (!agentIdentity) {
24
+ agentIdentity = identity.createAgent(agentId, `agent-${agentId.slice(0, 8)}`);
25
+ }
26
+ return {
27
+ accountId: agentId,
28
+ displayName: agentIdentity.nickname || `agent-${agentId.slice(0, 8)}`,
29
+ metadata: {
30
+ publicKey: agentIdentity.signing_public_key,
31
+ exchangePublicKey: agentIdentity.exchange_public_key,
32
+ fingerprint: agentIdentity.fingerprint,
33
+ },
34
+ };
35
+ },
36
+
37
+ // ── DM Security Policy ──
38
+ security: {
39
+ dm: {
40
+ allowFrom: async (accountId, peerId) => {
41
+ // Only friends in the contact list can send DMs
42
+ return db.isFriend ? db.isFriend(accountId, peerId) : !!db.getFriend(accountId, peerId);
43
+ },
44
+ },
45
+ },
46
+
47
+ // ── Friend Pairing ──
48
+ pairing: {
49
+ text: async (accountId) => {
50
+ try {
51
+ await serverClient.ensureAuth(accountId);
52
+ const result = await handshake.generateFriendCode(accountId);
53
+ const code = result.number;
54
+ return {
55
+ code,
56
+ instructions: `Share this pairing code with the other party: ${code}. They can add you using the chat-friend tool's add action.`,
57
+ };
58
+ } catch (e) {
59
+ // Fallback: use public key prefix as pairing code
60
+ const info = identity.getInfo(accountId);
61
+ const code = info ? info.exchange_public_key.slice(0, 16) : 'error';
62
+ return {
63
+ code,
64
+ instructions: `Share this pairing code with the other party: ${code}`,
65
+ };
66
+ }
67
+ },
68
+ verify: async (accountId, peerCode) => {
69
+ try {
70
+ const result = await handshake.addFriendByCode(accountId, peerCode);
71
+ return { success: true, peerId: result.peer_id || result.friend_id || peerCode };
72
+ } catch (e) {
73
+ return { success: false, error: e.message };
74
+ }
75
+ },
76
+ },
77
+
78
+ // ── Inbound Message Processing ──
79
+ inbound: {
80
+ onText: async (message) => {
81
+ const { toAccountId, fromPeerId, encryptedContent } = message;
82
+
83
+ // Try to decrypt if we have a session key
84
+ let content = encryptedContent || message.content || message.payload || '';
85
+ const session = db.loadSession(toAccountId, fromPeerId);
86
+ if (session && session.session_key && typeof content === 'string') {
87
+ try {
88
+ content = decryptMessage(content, session.session_key);
89
+ } catch (e) {
90
+ // Might be plaintext, keep as is
91
+ }
92
+ }
93
+
94
+ return {
95
+ text: typeof content === 'string' ? content : JSON.stringify(content),
96
+ metadata: {
97
+ fromPeerId,
98
+ timestamp: message.timestamp,
99
+ },
100
+ };
101
+ },
102
+ onMedia: async (message) => {
103
+ const keys = identity.loadAgent(message.toAccountId);
104
+ let content = message.encryptedContent || message.content || '';
105
+ const session = db.loadSession(message.toAccountId, message.fromPeerId);
106
+ if (session && session.session_key && typeof content === 'string') {
107
+ try {
108
+ content = decryptMessage(content, session.session_key);
109
+ } catch (e) {}
110
+ }
111
+ return {
112
+ mediaUrl: content,
113
+ mediaType: message.mediaType || 'file',
114
+ metadata: { fromPeerId: message.fromPeerId },
115
+ };
116
+ },
117
+ },
118
+
119
+ // ── Outbound Message Processing ──
120
+ outbound: {
121
+ sendText: async (fromAccountId, toPeerId, text) => {
122
+ const result = await chat.sendMessage(fromAccountId, toPeerId, text, { isGroup: false });
123
+ return result;
124
+ },
125
+ sendMedia: async (fromAccountId, toPeerId, mediaUrl, mediaType) => {
126
+ const result = await chat.sendMessage(fromAccountId, toPeerId, mediaUrl, {
127
+ type: mediaType || 'file',
128
+ isGroup: false,
129
+ });
130
+ return result;
131
+ },
132
+ },
133
+
134
+ // ── Lifecycle ──
135
+ lifecycle: {
136
+ onAccountCreate: async (accountId) => {
137
+ let agentIdentity = identity.loadAgent(accountId);
138
+ if (!agentIdentity) {
139
+ agentIdentity = identity.createAgent(accountId, `agent-${accountId.slice(0, 8)}`);
140
+ }
141
+ try {
142
+ await serverClient.start(accountId);
143
+ } catch (e) {
144
+ console.error('[AICQ Channel] Server connection failed for account:', accountId, e.message);
145
+ }
146
+ },
147
+ onAccountDelete: async (accountId) => {
148
+ try {
149
+ serverClient.disconnect();
150
+ } catch (e) {}
151
+ identity.deleteAgent(accountId);
152
+ },
153
+ onShutdown: async () => {
154
+ try {
155
+ serverClient.stop();
156
+ } catch (e) {}
157
+ console.log('[AICQ Channel] Shutdown complete');
158
+ },
159
+ },
160
+ };
161
+ }
162
+
163
+ module.exports = { createAicqChannel };
@@ -0,0 +1,469 @@
1
+ /**
2
+ * AICQ Channel Plugin — Gateway HTTP Routes
3
+ *
4
+ * Provides HTTP route handlers for the OpenClaw Gateway.
5
+ * These routes serve the SPA UI and REST API endpoints.
6
+ *
7
+ * Routes are served via Gateway HTTP, not an independent Express server.
8
+ * Prefix: /plugins/aicq-chat/
9
+ */
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const QRCode = require('qrcode');
13
+
14
+ /**
15
+ * Create UI route handlers
16
+ * @param {Object} ctx - Plugin context with managers
17
+ */
18
+ function createUiRoutes(ctx) {
19
+ const { db, identity, serverClient, handshake, chat, dataDir } = ctx;
20
+ const UPLOADS_DIR = path.join(dataDir, 'uploads');
21
+
22
+ // Ensure uploads directory exists
23
+ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
24
+
25
+ /**
26
+ * Helper to get current agent ID
27
+ */
28
+ function getAgentId(req) {
29
+ return req.query?.agent_id || req.body?.agent_id || (identity.listAgents()[0]?.agent_id);
30
+ }
31
+
32
+ /**
33
+ * Register all routes on an Express app or router
34
+ * This is called by the Gateway to mount the routes
35
+ */
36
+ function registerRoutes(app) {
37
+ // ── Serve SPA static files ────────────────────────────────────
38
+ const publicDir = path.join(__dirname, '..', 'public');
39
+ app.use('/plugins/aicq-chat/ui', (req, res, next) => {
40
+ // Serve static files from public/
41
+ const filePath = path.join(publicDir, req.path === '/' ? 'index.html' : req.path);
42
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
43
+ res.sendFile(filePath);
44
+ } else {
45
+ // SPA fallback: serve index.html for all unknown routes
46
+ res.sendFile(path.join(publicDir, 'index.html'));
47
+ }
48
+ });
49
+
50
+ // ── API Routes ────────────────────────────────────────────────
51
+
52
+ // Status
53
+ app.get('/plugins/aicq-chat/api/status', (req, res) => {
54
+ res.json({
55
+ status: 'running',
56
+ version: '3.0.0',
57
+ architecture: 'channel',
58
+ connected: serverClient.connected,
59
+ serverUrl: ctx.serverUrl,
60
+ });
61
+ });
62
+
63
+ // Agents
64
+ app.get('/plugins/aicq-chat/api/agents', (req, res) => {
65
+ res.json({ agents: identity.listAgents() });
66
+ });
67
+
68
+ app.post('/plugins/aicq-chat/api/agents', async (req, res) => {
69
+ try {
70
+ const { agent_id, nickname } = req.body;
71
+ if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
72
+ const agent = identity.createAgent(agent_id, nickname);
73
+ try {
74
+ await serverClient.start(agent_id);
75
+ } catch (e) {
76
+ console.error('[AICQ] Server registration failed:', e.message);
77
+ }
78
+ res.json({ success: true, agent });
79
+ } catch (e) {
80
+ res.status(500).json({ error: e.message });
81
+ }
82
+ });
83
+
84
+ app.delete('/plugins/aicq-chat/api/agents/:id', (req, res) => {
85
+ identity.deleteAgent(req.params.id);
86
+ res.json({ success: true });
87
+ });
88
+
89
+ // Friends
90
+ app.get('/plugins/aicq-chat/api/friends', (req, res) => {
91
+ const agentId = getAgentId(req);
92
+ res.json({ friends: db.listFriends(agentId) });
93
+ });
94
+
95
+ app.post('/plugins/aicq-chat/api/friends/add', async (req, res) => {
96
+ try {
97
+ const { temp_number, friend_code, agent_id } = req.body;
98
+ const agentId = agent_id || getAgentId(req);
99
+ const code = temp_number || friend_code;
100
+ if (!code) return res.status(400).json({ error: 'temp_number or friend_code is required' });
101
+ const result = await handshake.addFriendByCode(agentId, code);
102
+ res.json({ success: true, result });
103
+ } catch (e) {
104
+ res.status(500).json({ error: e.message });
105
+ }
106
+ });
107
+
108
+ app.delete('/plugins/aicq-chat/api/friends/:id', async (req, res) => {
109
+ try {
110
+ const agentId = getAgentId(req);
111
+ db.removeFriend(agentId, req.params.id);
112
+ try { await serverClient.removeFriend(req.params.id); } catch (e) {}
113
+ res.json({ success: true });
114
+ } catch (e) {
115
+ res.status(500).json({ error: e.message });
116
+ }
117
+ });
118
+
119
+ app.get('/plugins/aicq-chat/api/friends/requests', async (req, res) => {
120
+ try {
121
+ const agentId = getAgentId(req);
122
+ let serverRequests = [];
123
+ try {
124
+ await serverClient.ensureAuth(agentId);
125
+ const result = await serverClient.listFriendRequests();
126
+ serverRequests = result.sent || [];
127
+ serverRequests = serverRequests.concat(result.received || []);
128
+ } catch (e) {}
129
+ const localRequests = db.getPendingRequests(agentId);
130
+ res.json({ requests: [...localRequests, ...serverRequests] });
131
+ } catch (e) {
132
+ res.status(500).json({ error: e.message });
133
+ }
134
+ });
135
+
136
+ app.post('/plugins/aicq-chat/api/friends/requests/:id/accept', async (req, res) => {
137
+ try {
138
+ const agentId = getAgentId(req);
139
+ const result = await handshake.acceptRequest(agentId, req.params.id);
140
+ res.json(result);
141
+ } catch (e) {
142
+ res.status(500).json({ error: e.message });
143
+ }
144
+ });
145
+
146
+ app.post('/plugins/aicq-chat/api/friends/requests/:id/reject', async (req, res) => {
147
+ try {
148
+ const agentId = getAgentId(req);
149
+ const result = await handshake.rejectRequest(agentId, req.params.id);
150
+ res.json(result);
151
+ } catch (e) {
152
+ res.status(500).json({ error: e.message });
153
+ }
154
+ });
155
+
156
+ // Groups
157
+ app.get('/plugins/aicq-chat/api/groups', (req, res) => {
158
+ const agentId = getAgentId(req);
159
+ res.json({ groups: db.listGroups(agentId) });
160
+ });
161
+
162
+ app.post('/plugins/aicq-chat/api/groups', async (req, res) => {
163
+ try {
164
+ const agentId = getAgentId(req);
165
+ const { name, description } = req.body;
166
+ if (!name) return res.status(400).json({ error: 'name is required' });
167
+ await serverClient.ensureAuth(agentId);
168
+ const result = await serverClient.createGroup(name, description);
169
+ if (result.id) {
170
+ db.addGroup({
171
+ agent_id: agentId,
172
+ id: result.id,
173
+ name,
174
+ owner_id: agentId,
175
+ members_json: result.members || '[]',
176
+ description: description || '',
177
+ });
178
+ }
179
+ res.json({ success: true, group: result });
180
+ } catch (e) {
181
+ res.status(500).json({ error: e.message });
182
+ }
183
+ });
184
+
185
+ app.post('/plugins/aicq-chat/api/groups/:id/join', async (req, res) => {
186
+ try {
187
+ const agentId = getAgentId(req);
188
+ await serverClient.ensureAuth(agentId);
189
+ const result = await serverClient.inviteGroupMember(req.params.id, agentId);
190
+ res.json({ success: true, result });
191
+ } catch (e) {
192
+ res.status(500).json({ error: e.message });
193
+ }
194
+ });
195
+
196
+ app.get('/plugins/aicq-chat/api/groups/:id/messages', async (req, res) => {
197
+ try {
198
+ const agentId = getAgentId(req);
199
+ const limit = parseInt(req.query.limit || '50', 10);
200
+ const before = req.query.before || null;
201
+ try {
202
+ await serverClient.ensureAuth(agentId);
203
+ const result = await serverClient.getGroupMessages(req.params.id, limit, before);
204
+ if (result.messages && result.messages.length > 0) {
205
+ return res.json({ messages: result.messages });
206
+ }
207
+ } catch (e) {}
208
+ const messages = db.getChatHistory(agentId, req.params.id, { limit, before });
209
+ res.json({ messages });
210
+ } catch (e) {
211
+ res.status(500).json({ error: e.message });
212
+ }
213
+ });
214
+
215
+ app.put('/plugins/aicq-chat/api/groups/:id/silent', (req, res) => {
216
+ const agentId = getAgentId(req);
217
+ const { silent } = req.body;
218
+ db.setGroupSilentMode(agentId, req.params.id, !!silent);
219
+ res.json({ success: true, silent: !!silent });
220
+ });
221
+
222
+ // Chat
223
+ app.get('/plugins/aicq-chat/api/chat/:targetId', (req, res) => {
224
+ const agentId = getAgentId(req);
225
+ const limit = parseInt(req.query.limit || '50', 10);
226
+ const before = req.query.before || null;
227
+ const messages = db.getChatHistory(agentId, req.params.targetId, { limit, before });
228
+ res.json({ messages });
229
+ });
230
+
231
+ app.post('/plugins/aicq-chat/api/chat/send', async (req, res) => {
232
+ try {
233
+ const { agent_id, targetId, content, type, isGroup, mentions, file_url, file_name } = req.body;
234
+ const agentId = agent_id || getAgentId(req);
235
+ if (!targetId || !content) return res.status(400).json({ error: 'targetId and content are required' });
236
+ const msg = await chat.sendMessage(agentId, targetId, content, {
237
+ type: type || 'text',
238
+ isGroup: !!isGroup,
239
+ mentions: mentions || [],
240
+ file_url,
241
+ file_name,
242
+ });
243
+ res.json({ success: true, message: msg });
244
+ } catch (e) {
245
+ res.status(500).json({ error: e.message });
246
+ }
247
+ });
248
+
249
+ app.delete('/plugins/aicq-chat/api/chat/:messageId', (req, res) => {
250
+ const agentId = getAgentId(req);
251
+ db.deleteMessage(agentId, req.params.messageId);
252
+ res.json({ success: true });
253
+ });
254
+
255
+ // Streaming endpoints
256
+ app.post('/plugins/aicq-chat/api/chat/stream-chunk', (req, res) => {
257
+ try {
258
+ const { targetId, friend_id, chunk_type, chunkType, data } = req.body;
259
+ const streamTarget = targetId || friend_id;
260
+ if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
261
+ if (!data) return res.status(400).json({ error: 'data is required' });
262
+ const type = chunk_type || chunkType || 'text';
263
+ const ALLOWED_CHUNK_TYPES = ['text', 'reasoning', 'thinking', 'clear_text', 'tool_call', 'tool_result'];
264
+ if (!ALLOWED_CHUNK_TYPES.includes(type)) {
265
+ return res.status(400).json({ error: `Invalid chunk_type: ${type}. Allowed: ${ALLOWED_CHUNK_TYPES.join(', ')}` });
266
+ }
267
+ const sent = serverClient.sendWS({
268
+ type: 'stream_chunk',
269
+ to: streamTarget,
270
+ chunkType: type,
271
+ data: data,
272
+ });
273
+ if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
274
+ res.json({ success: true });
275
+ } catch (e) {
276
+ res.status(500).json({ error: e.message });
277
+ }
278
+ });
279
+
280
+ app.post('/plugins/aicq-chat/api/chat/stream-end', (req, res) => {
281
+ try {
282
+ const { targetId, friend_id, message_id, messageId } = req.body;
283
+ const streamTarget = targetId || friend_id;
284
+ if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
285
+ const msgId = message_id || messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
286
+ const sent = serverClient.sendWS({
287
+ type: 'stream_end',
288
+ to: streamTarget,
289
+ messageId: msgId,
290
+ });
291
+ if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
292
+ res.json({ success: true, messageId: msgId });
293
+ } catch (e) {
294
+ res.status(500).json({ error: e.message });
295
+ }
296
+ });
297
+
298
+ // File upload
299
+ const multer = require('multer');
300
+ const upload = multer({
301
+ storage: multer.memoryStorage(),
302
+ limits: { fileSize: 50 * 1024 * 1024 },
303
+ });
304
+
305
+ app.post('/plugins/aicq-chat/api/upload', upload.single('file'), async (req, res) => {
306
+ try {
307
+ if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
308
+ const agentId = getAgentId(req);
309
+ const targetId = req.body.targetId;
310
+ const isGroup = req.body.isGroup === 'true' || req.body.isGroup === '1';
311
+ const msg = await chat.handleFileUpload(agentId, targetId, req.file, isGroup);
312
+ res.json({ success: true, message: msg });
313
+ } catch (e) {
314
+ res.status(500).json({ error: e.message });
315
+ }
316
+ });
317
+
318
+ app.get('/plugins/aicq-chat/api/files/:fileId', (req, res) => {
319
+ const filePath = path.join(UPLOADS_DIR, req.params.fileId);
320
+ if (fs.existsSync(filePath)) {
321
+ res.sendFile(filePath);
322
+ } else {
323
+ res.status(404).json({ error: 'File not found' });
324
+ }
325
+ });
326
+
327
+ // Identity
328
+ app.get('/plugins/aicq-chat/api/identity', (req, res) => {
329
+ const agentId = getAgentId(req);
330
+ res.json(identity.getInfo(agentId) || {});
331
+ });
332
+
333
+ app.post('/plugins/aicq-chat/api/identity/nickname', (req, res) => {
334
+ const { agent_id, nickname } = req.body;
335
+ const agentId = agent_id || getAgentId(req);
336
+ identity.updateNickname(agentId, nickname);
337
+ res.json({ success: true });
338
+ });
339
+
340
+ app.post('/plugins/aicq-chat/api/identity/friend-code', async (req, res) => {
341
+ try {
342
+ const agentId = req.body.agent_id || getAgentId(req);
343
+ await serverClient.ensureAuth(agentId);
344
+ const result = await handshake.generateFriendCode(agentId);
345
+ res.json({ success: true, code: result.number, expires_at: result.expiresAt || result.expires_at });
346
+ } catch (e) {
347
+ res.status(500).json({ error: e.message });
348
+ }
349
+ });
350
+
351
+ app.get('/plugins/aicq-chat/api/identity/qr', async (req, res) => {
352
+ try {
353
+ const agentId = getAgentId(req);
354
+ const info = identity.getInfo(agentId);
355
+ if (!info) return res.status(404).json({ error: 'Agent not found' });
356
+ const qrData = JSON.stringify({
357
+ type: 'aicq-friend',
358
+ agent_id: info.agent_id,
359
+ public_key: info.signing_public_key,
360
+ exchange_public_key: info.exchange_public_key,
361
+ fingerprint: info.fingerprint,
362
+ });
363
+ const qrImage = await QRCode.toDataURL(qrData);
364
+ res.json({ qr: qrImage, data: qrData, info });
365
+ } catch (e) {
366
+ res.status(500).json({ error: e.message });
367
+ }
368
+ });
369
+
370
+ app.post('/plugins/aicq-chat/api/identity/rotate-keys', (req, res) => {
371
+ try {
372
+ const agentId = req.body.agent_id || getAgentId(req);
373
+ identity.rotateKeys(agentId);
374
+ res.json({ success: true, info: identity.getInfo(agentId) });
375
+ } catch (e) {
376
+ res.status(500).json({ error: e.message });
377
+ }
378
+ });
379
+
380
+ app.get('/plugins/aicq-chat/api/identity/keys', (req, res) => {
381
+ const agentId = getAgentId(req);
382
+ const info = identity.loadAgent(agentId);
383
+ if (!info) return res.status(404).json({ error: 'Agent not found' });
384
+ res.json({
385
+ agent_id: info.agent_id,
386
+ nickname: info.nickname,
387
+ signing_public_key: info.signing_public_key,
388
+ exchange_public_key: info.exchange_public_key,
389
+ signing_secret_key: info.signing_secret_key,
390
+ exchange_secret_key: info.exchange_secret_key,
391
+ fingerprint: info.fingerprint,
392
+ });
393
+ });
394
+
395
+ // Sync endpoint
396
+ app.post('/plugins/aicq-chat/api/sync', async (req, res) => {
397
+ try {
398
+ const agentId = req.body.agent_id || getAgentId(req);
399
+ await serverClient.ensureAuth(agentId);
400
+ // Sync friends
401
+ const friendResult = await serverClient.listFriends();
402
+ if (friendResult.friends) {
403
+ for (const f of friendResult.friends) {
404
+ const existing = db.getFriend(agentId, f.id);
405
+ if (!existing) {
406
+ db.addFriend({
407
+ agent_id: agentId,
408
+ id: f.id,
409
+ public_key: f.public_key || f.publicKey || '',
410
+ fingerprint: f.fingerprint || '',
411
+ friend_type: f.type || f.friend_type || 'ai',
412
+ ai_name: f.agent_name || f.ai_name || f.displayName || '',
413
+ });
414
+ } else {
415
+ db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
416
+ }
417
+ }
418
+ }
419
+ // Sync groups
420
+ const groupResult = await serverClient.listGroups();
421
+ if (groupResult.groups) {
422
+ for (const g of groupResult.groups) {
423
+ db.addGroup({
424
+ agent_id: agentId,
425
+ id: g.id,
426
+ name: g.name,
427
+ owner_id: g.owner_id || g.ownerId || '',
428
+ members_json: g.members || g.members_json || '[]',
429
+ description: g.description || '',
430
+ });
431
+ }
432
+ }
433
+ res.json({ success: true });
434
+ } catch (e) {
435
+ res.status(500).json({ error: e.message });
436
+ }
437
+ });
438
+
439
+ // Gateway proxy endpoint (for backward compatibility)
440
+ app.post('/plugins/aicq-chat/api/gateway', async (req, res) => {
441
+ try {
442
+ const { method, kwargs } = req.body;
443
+ if (!method) return res.status(400).json({ error: 'method is required' });
444
+ // Import handleGateway from index.js
445
+ const { handleGateway } = require('../index');
446
+ const result = await handleGateway(method, kwargs);
447
+ res.json(result);
448
+ } catch (e) {
449
+ res.status(500).json({ error: e.message });
450
+ }
451
+ });
452
+ }
453
+
454
+ return {
455
+ registerRoutes,
456
+ // Also export as a map of route handlers for Gateway HTTP registration
457
+ routes: {
458
+ 'GET /plugins/aicq-chat/ui/*': 'static:public',
459
+ 'GET /plugins/aicq-chat/api/status': 'api:status',
460
+ 'GET /plugins/aicq-chat/api/friends': 'api:friends.list',
461
+ 'POST /plugins/aicq-chat/api/friends/add': 'api:friends.add',
462
+ 'DELETE /plugins/aicq-chat/api/friends/:id': 'api:friends.remove',
463
+ 'GET /plugins/aicq-chat/api/messages': 'api:messages',
464
+ 'POST /plugins/aicq-chat/api/chat/send': 'api:chat.send',
465
+ },
466
+ };
467
+ }
468
+
469
+ module.exports = { createUiRoutes };