aicq-chat-plugin 2.6.7 → 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/index.js CHANGED
@@ -1,671 +1,394 @@
1
1
  /**
2
- * AICQ Chat Plugin — Main Entry Point
3
- * OpenClaw sidecar plugin providing E2EE chat UI
2
+ * AICQ Chat Plugin — Channel Plugin Entry Point
4
3
  *
5
- * Uses sql.js (pure WASM SQLite) instead of better-sqlite3
6
- * to avoid native C++ compilation issues.
4
+ * Architecture: Channel (in-process, no independent port)
5
+ * - Runs inside the OpenClaw process
6
+ * - Uses createChatChannelPlugin for E2EE chat channel
7
+ * - Provides Gateway HTTP routes for the SPA UI
8
+ * - No sidecar process needed
7
9
  */
8
- const express = require('express');
9
- const multer = require('multer');
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
12
  const os = require('os');
13
- const QRCode = require('qrcode');
14
- const PluginDatabase = require('./lib/database');
15
13
 
16
- // ─── Configuration ──────────────────────────────────────────────────
17
- const PORT = parseInt(process.env.AICQ_PORT || '6109', 10);
18
- const SERVER_URL = process.env.AICQ_SERVER_URL || 'https://aicq.online';
14
+ // ── Configuration ──────────────────────────────────────────────────
19
15
  const DATA_DIR = process.env.AICQ_DATA_DIR || path.join(os.homedir(), '.aicq-plugin');
20
- const UPLOADS_DIR = path.join(DATA_DIR, 'uploads');
16
+ const SERVER_URL = process.env.AICQ_SERVER_URL || 'https://aicq.online';
21
17
 
22
18
  fs.mkdirSync(DATA_DIR, { recursive: true });
23
- fs.mkdirSync(UPLOADS_DIR, { recursive: true });
24
19
 
25
- // ─── Async bootstrap ────────────────────────────────────────────────
26
- // sql.js requires async init, so we wrap the entire app setup
27
- (async () => {
28
- // Initialize database (async — loads WASM + opens/creates DB file)
29
- const db = new PluginDatabase(DATA_DIR);
30
- await db.init();
31
- console.log('[AICQ] Database initialized');
20
+ // ── Lazy-loaded modules (require db init) ──────────────────────────
21
+ let _db = null;
22
+ let _identity = null;
23
+ let _serverClient = null;
24
+ let _handshake = null;
25
+ let _chat = null;
26
+ let _channel = null;
27
+ let _uiRoutes = null;
28
+ let _initialized = false;
29
+
30
+ /**
31
+ * Initialize all plugin components (async, called once)
32
+ */
33
+ async function ensureInitialized() {
34
+ if (_initialized) return;
32
35
 
33
- // Lazy-load modules that depend on db
36
+ const PluginDatabase = require('./lib/database');
34
37
  const IdentityManager = require('./lib/identity');
35
38
  const ServerClient = require('./lib/server-client');
36
39
  const HandshakeManager = require('./lib/handshake');
37
40
  const ChatManager = require('./lib/chat');
38
41
 
39
- const identity = new IdentityManager(db);
40
- const serverClient = new ServerClient(identity, db, SERVER_URL);
41
- const handshake = new HandshakeManager(identity, serverClient, db);
42
- const chat = new ChatManager(identity, serverClient, db, UPLOADS_DIR);
42
+ // Initialize database
43
+ _db = new PluginDatabase(DATA_DIR);
44
+ await _db.init();
45
+ console.log('[AICQ Channel] Database initialized');
46
+
47
+ // Initialize managers
48
+ _identity = new IdentityManager(_db);
49
+ _serverClient = new ServerClient(_identity, _db, SERVER_URL);
50
+ _handshake = new HandshakeManager(_identity, _serverClient, _db);
51
+ _chat = new ChatManager(_identity, _serverClient, _db, path.join(DATA_DIR, 'uploads'));
52
+
53
+ // Load channel and UI route creators
54
+ const { createAicqChannel } = require('./src/channel');
55
+ const { createUiRoutes } = require('./src/ui-routes');
56
+
57
+ _channel = createAicqChannel({
58
+ db: _db,
59
+ identity: _identity,
60
+ serverClient: _serverClient,
61
+ handshake: _handshake,
62
+ chat: _chat,
63
+ dataDir: DATA_DIR,
64
+ serverUrl: SERVER_URL,
65
+ });
66
+
67
+ _uiRoutes = createUiRoutes({
68
+ db: _db,
69
+ identity: _identity,
70
+ serverClient: _serverClient,
71
+ handshake: _handshake,
72
+ chat: _chat,
73
+ dataDir: DATA_DIR,
74
+ });
75
+
76
+ // Periodic cleanup
77
+ setInterval(() => _db.cleanup(), 3600000);
78
+
79
+ _initialized = true;
80
+ console.log('[AICQ Channel] Plugin components initialized');
81
+ }
82
+
83
+ // ── register() — Called by OpenClaw when the plugin is discovered ────
84
+ function register() {
85
+ return {
86
+ id: 'aicq-chat',
87
+ name: 'AICQ Encrypted Chat',
88
+ version: '3.0.0',
89
+ description: 'End-to-end encrypted chat channel plugin for OpenClaw agents',
90
+ kind: 'channel',
91
+
92
+ // Channel configuration
93
+ channel: {
94
+ id: 'aicq-chat',
95
+ label: 'AICQ Encrypted Chat',
96
+ },
43
97
 
44
- // Auto-create a default agent if none exists
45
- const agents = identity.listAgents();
46
- let currentAgentId = null;
98
+ // Tool definitions for OpenClaw agent use
99
+ tools: {
100
+ 'chat-friend': {
101
+ description: 'Manage AICQ friends — list, add by friend code, remove, view requests, accept/reject requests',
102
+ parameters: {
103
+ type: 'object',
104
+ properties: {
105
+ action: {
106
+ type: 'string',
107
+ enum: ['list', 'add', 'remove', 'requests', 'accept', 'reject'],
108
+ description: 'The friend management action to perform',
109
+ },
110
+ friend_code: {
111
+ type: 'string',
112
+ description: 'Friend code or temp number for adding a friend',
113
+ },
114
+ friend_id: {
115
+ type: 'string',
116
+ description: 'Friend ID for remove/accept/reject actions',
117
+ },
118
+ },
119
+ required: ['action'],
120
+ },
121
+ },
122
+ 'chat-send': {
123
+ description: 'Send an encrypted message to a friend or group via AICQ',
124
+ parameters: {
125
+ type: 'object',
126
+ properties: {
127
+ targetId: {
128
+ type: 'string',
129
+ description: 'The friend ID or group ID to send the message to',
130
+ },
131
+ content: {
132
+ type: 'string',
133
+ description: 'The message content to send',
134
+ },
135
+ isGroup: {
136
+ type: 'boolean',
137
+ description: 'Whether the target is a group (default: false)',
138
+ },
139
+ },
140
+ required: ['targetId', 'content'],
141
+ },
142
+ },
143
+ 'chat-export-key': {
144
+ description: 'Export your AICQ identity public key and fingerprint for sharing',
145
+ parameters: {
146
+ type: 'object',
147
+ properties: {
148
+ format: {
149
+ type: 'string',
150
+ enum: ['json', 'qr'],
151
+ description: 'Output format: json for key data, qr for QR code image (default: json)',
152
+ },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ };
158
+ }
159
+
160
+ // ── activate() — Called by OpenClaw when the plugin is enabled ───────
161
+ async function activate(config) {
162
+ await ensureInitialized();
163
+
164
+ // Auto-create default agent if none exists
165
+ const agents = _identity.listAgents();
166
+ let currentAgentId;
47
167
  if (agents.length === 0) {
48
- const defaultAgent = identity.createAgent('agent-' + Date.now(), '默认Agent');
168
+ const defaultAgent = _identity.createAgent('agent-' + Date.now(), '默认Agent');
49
169
  currentAgentId = defaultAgent.agent_id;
50
- console.log('[AICQ] Created default agent:', currentAgentId);
170
+ console.log('[AICQ Channel] Created default agent:', currentAgentId);
51
171
  } else {
52
172
  currentAgentId = agents[0].agent_id;
53
173
  }
54
174
 
55
- // Connect to server in background
56
- (async () => {
57
- try {
58
- await serverClient.start(currentAgentId);
59
- await syncFriendsFromServer(currentAgentId);
60
- await syncGroupsFromServer(currentAgentId);
61
- } catch (e) {
62
- console.error('[AICQ] Initial server connection failed:', e.message);
63
- }
64
- })();
65
-
66
- // Periodic cleanup + save
67
- setInterval(() => db.cleanup(), 3600000);
68
-
69
- // ─── Helper: get current agent ID ──────────────────────────────────
70
- function getAgentId(req) {
71
- return req.query.agent_id || req.body?.agent_id || currentAgentId;
175
+ // Connect to AICQ server
176
+ try {
177
+ await _serverClient.start(currentAgentId);
178
+ // Sync friends and groups from server
179
+ await syncFriendsFromServer(currentAgentId);
180
+ await syncGroupsFromServer(currentAgentId);
181
+ } catch (e) {
182
+ console.error('[AICQ Channel] Initial server connection failed:', e.message);
72
183
  }
73
184
 
74
- // ─── Sync friends/groups from server ────────────────────────────────
75
- async function syncFriendsFromServer(agentId) {
76
- try {
77
- await serverClient.ensureAuth(agentId);
78
- const result = await serverClient.listFriends();
79
- if (result.friends) {
80
- for (const f of result.friends) {
81
- const existing = db.getFriend(agentId, f.id);
82
- if (!existing) {
83
- db.addFriend({
84
- agent_id: agentId,
85
- id: f.id,
86
- public_key: f.public_key || f.publicKey || '',
87
- fingerprint: f.fingerprint || '',
88
- friend_type: f.type || f.friend_type || 'ai',
89
- ai_name: f.agent_name || f.ai_name || f.displayName || '',
90
- });
91
- } else {
92
- db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
93
- }
94
- }
95
- }
96
- } catch (e) {
97
- console.error('[AICQ] Sync friends failed:', e.message);
98
- }
99
- }
100
-
101
- async function syncGroupsFromServer(agentId) {
102
- try {
103
- await serverClient.ensureAuth(agentId);
104
- const result = await serverClient.listGroups();
105
- if (result.groups) {
106
- for (const g of result.groups) {
107
- db.addGroup({
185
+ return {
186
+ handleTool,
187
+ handleGateway,
188
+ channel: _channel,
189
+ gatewayRoutes: _uiRoutes,
190
+ };
191
+ }
192
+
193
+ // ── Sync helpers ────────────────────────────────────────────────────
194
+ async function syncFriendsFromServer(agentId) {
195
+ try {
196
+ await _serverClient.ensureAuth(agentId);
197
+ const result = await _serverClient.listFriends();
198
+ if (result.friends) {
199
+ for (const f of result.friends) {
200
+ const existing = _db.getFriend(agentId, f.id);
201
+ if (!existing) {
202
+ _db.addFriend({
108
203
  agent_id: agentId,
109
- id: g.id,
110
- name: g.name,
111
- owner_id: g.owner_id || g.ownerId || '',
112
- members_json: g.members || g.members_json || '[]',
113
- description: g.description || '',
204
+ id: f.id,
205
+ public_key: f.public_key || f.publicKey || '',
206
+ fingerprint: f.fingerprint || '',
207
+ friend_type: f.type || f.friend_type || 'ai',
208
+ ai_name: f.agent_name || f.ai_name || f.displayName || '',
114
209
  });
210
+ } else {
211
+ _db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
115
212
  }
116
213
  }
117
- } catch (e) {
118
- console.error('[AICQ] Sync groups failed:', e.message);
119
214
  }
215
+ } catch (e) {
216
+ console.error('[AICQ Channel] Sync friends failed:', e.message);
120
217
  }
121
-
122
- // ─── Express App ────────────────────────────────────────────────────
123
- const app = express();
124
- app.use(express.json());
125
- app.use(express.urlencoded({ extended: true }));
126
-
127
- const upload = multer({
128
- storage: multer.memoryStorage(),
129
- limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
130
- });
131
-
132
- // ─── Serve SPA ──────────────────────────────────────────────────────
133
- app.use(express.static(path.join(__dirname, 'public')));
134
-
135
- // ─── API Routes ─────────────────────────────────────────────────────
136
-
137
- // Status
138
- app.get('/api/status', (req, res) => {
139
- res.json({
140
- status: 'running',
141
- version: '2.6.0',
142
- connected: serverClient.connected,
143
- currentAgent: currentAgentId,
144
- serverUrl: SERVER_URL,
145
- });
146
- });
147
-
148
- // Agents
149
- app.get('/api/agents', (req, res) => {
150
- res.json({ agents: identity.listAgents() });
151
- });
152
-
153
- app.post('/api/agents', async (req, res) => {
154
- try {
155
- const { agent_id, nickname } = req.body;
156
- if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
157
- const agent = identity.createAgent(agent_id, nickname);
158
- currentAgentId = agent_id;
159
- try {
160
- await serverClient.start(agent_id);
161
- } catch (e) {
162
- console.error('Server registration failed:', e.message);
163
- }
164
- res.json({ success: true, agent });
165
- } catch (e) {
166
- res.status(500).json({ error: e.message });
167
- }
168
- });
169
-
170
- app.delete('/api/agents/:id', (req, res) => {
171
- identity.deleteAgent(req.params.id);
172
- if (currentAgentId === req.params.id) {
173
- const remaining = identity.listAgents();
174
- currentAgentId = remaining.length > 0 ? remaining[0].agent_id : null;
175
- }
176
- res.json({ success: true });
177
- });
178
-
179
- app.post('/api/agents/switch', async (req, res) => {
180
- try {
181
- const { agent_id } = req.body;
182
- if (!agent_id) return res.status(400).json({ error: 'agent_id is required' });
183
- currentAgentId = agent_id;
184
- await serverClient.switchAgent(agent_id);
185
- await syncFriendsFromServer(agent_id);
186
- await syncGroupsFromServer(agent_id);
187
- res.json({ success: true, agent_id });
188
- } catch (e) {
189
- res.status(500).json({ error: e.message });
190
- }
191
- });
192
-
193
- // Friends
194
- app.get('/api/friends', (req, res) => {
195
- const agentId = getAgentId(req);
196
- res.json({ friends: db.listFriends(agentId) });
197
- });
198
-
199
- app.post('/api/friends/add', async (req, res) => {
200
- try {
201
- const { temp_number, friend_code, agent_id } = req.body;
202
- const agentId = agent_id || currentAgentId;
203
- const code = temp_number || friend_code;
204
- if (!code) return res.status(400).json({ error: 'temp_number or friend_code is required' });
205
- const result = await handshake.addFriendByCode(agentId, code);
206
- res.json({ success: true, result });
207
- } catch (e) {
208
- res.status(500).json({ error: e.message });
209
- }
210
- });
211
-
212
- app.delete('/api/friends/:id', async (req, res) => {
213
- try {
214
- const agentId = getAgentId(req);
215
- db.removeFriend(agentId, req.params.id);
216
- try { await serverClient.removeFriend(req.params.id); } catch (e) {}
217
- res.json({ success: true });
218
- } catch (e) {
219
- res.status(500).json({ error: e.message });
220
- }
221
- });
222
-
223
- app.get('/api/friends/requests', async (req, res) => {
224
- try {
225
- const agentId = getAgentId(req);
226
- let serverRequests = [];
227
- try {
228
- await serverClient.ensureAuth(agentId);
229
- const result = await serverClient.listFriendRequests();
230
- serverRequests = result.sent || [];
231
- serverRequests = serverRequests.concat(result.received || []);
232
- } catch (e) {}
233
- const localRequests = db.getPendingRequests(agentId);
234
- res.json({ requests: [...localRequests, ...serverRequests] });
235
- } catch (e) {
236
- res.status(500).json({ error: e.message });
237
- }
238
- });
239
-
240
- app.post('/api/friends/requests/:id/accept', async (req, res) => {
241
- try {
242
- const agentId = getAgentId(req);
243
- const result = await handshake.acceptRequest(agentId, req.params.id);
244
- res.json(result);
245
- } catch (e) {
246
- res.status(500).json({ error: e.message });
247
- }
248
- });
249
-
250
- app.post('/api/friends/requests/:id/reject', async (req, res) => {
251
- try {
252
- const agentId = getAgentId(req);
253
- const result = await handshake.rejectRequest(agentId, req.params.id);
254
- res.json(result);
255
- } catch (e) {
256
- res.status(500).json({ error: e.message });
257
- }
258
- });
259
-
260
- // Groups
261
- app.get('/api/groups', (req, res) => {
262
- const agentId = getAgentId(req);
263
- res.json({ groups: db.listGroups(agentId) });
264
- });
265
-
266
- app.post('/api/groups', async (req, res) => {
267
- try {
268
- const agentId = getAgentId(req);
269
- const { name, description } = req.body;
270
- if (!name) return res.status(400).json({ error: 'name is required' });
271
- await serverClient.ensureAuth(agentId);
272
- const result = await serverClient.createGroup(name, description);
273
- if (result.id) {
274
- db.addGroup({
218
+ }
219
+
220
+ async function syncGroupsFromServer(agentId) {
221
+ try {
222
+ await _serverClient.ensureAuth(agentId);
223
+ const result = await _serverClient.listGroups();
224
+ if (result.groups) {
225
+ for (const g of result.groups) {
226
+ _db.addGroup({
275
227
  agent_id: agentId,
276
- id: result.id,
277
- name,
278
- owner_id: agentId,
279
- members_json: result.members || '[]',
280
- description: description || '',
228
+ id: g.id,
229
+ name: g.name,
230
+ owner_id: g.owner_id || g.ownerId || '',
231
+ members_json: g.members || g.members_json || '[]',
232
+ description: g.description || '',
281
233
  });
282
234
  }
283
- res.json({ success: true, group: result });
284
- } catch (e) {
285
- res.status(500).json({ error: e.message });
286
- }
287
- });
288
-
289
- app.post('/api/groups/:id/join', async (req, res) => {
290
- try {
291
- const agentId = getAgentId(req);
292
- await serverClient.ensureAuth(agentId);
293
- const result = await serverClient.inviteGroupMember(req.params.id, agentId);
294
- res.json({ success: true, result });
295
- } catch (e) {
296
- res.status(500).json({ error: e.message });
297
235
  }
298
- });
299
-
300
- app.get('/api/groups/:id/messages', async (req, res) => {
301
- try {
302
- const agentId = getAgentId(req);
303
- const limit = parseInt(req.query.limit || '50', 10);
304
- const before = req.query.before || null;
305
- try {
306
- await serverClient.ensureAuth(agentId);
307
- const result = await serverClient.getGroupMessages(req.params.id, limit, before);
308
- if (result.messages && result.messages.length > 0) {
309
- return res.json({ messages: result.messages });
310
- }
311
- } catch (e) {}
312
- const messages = db.getChatHistory(agentId, req.params.id, { limit, before });
313
- res.json({ messages });
314
- } catch (e) {
315
- res.status(500).json({ error: e.message });
316
- }
317
- });
318
-
319
- app.put('/api/groups/:id/silent', (req, res) => {
320
- const agentId = getAgentId(req);
321
- const { silent } = req.body;
322
- db.setGroupSilentMode(agentId, req.params.id, !!silent);
323
- res.json({ success: true, silent: !!silent });
324
- });
325
-
326
- // Chat
327
- app.get('/api/chat/:targetId', (req, res) => {
328
- const agentId = getAgentId(req);
329
- const limit = parseInt(req.query.limit || '50', 10);
330
- const before = req.query.before || null;
331
- const messages = db.getChatHistory(agentId, req.params.targetId, { limit, before });
332
- res.json({ messages });
333
- });
334
-
335
- app.post('/api/chat/send', async (req, res) => {
336
- try {
337
- const { agent_id, targetId, content, type, isGroup, mentions, file_url, file_name } = req.body;
338
- const agentId = agent_id || currentAgentId;
339
- if (!targetId || !content) return res.status(400).json({ error: 'targetId and content are required' });
340
- const msg = await chat.sendMessage(agentId, targetId, content, {
341
- type: type || 'text',
342
- isGroup: !!isGroup,
343
- mentions: mentions || [],
344
- file_url,
345
- file_name,
346
- });
347
- res.json({ success: true, message: msg });
348
- } catch (e) {
349
- res.status(500).json({ error: e.message });
236
+ } catch (e) {
237
+ console.error('[AICQ Channel] Sync groups failed:', e.message);
238
+ }
239
+ }
240
+
241
+ // ── Tool handler ────────────────────────────────────────────────────
242
+ async function handleTool(toolName, params) {
243
+ await ensureInitialized();
244
+ const agents = _identity.listAgents();
245
+ const currentAgentId = agents.length > 0 ? agents[0].agent_id : null;
246
+
247
+ switch (toolName) {
248
+ case 'chat-friend': {
249
+ const { action, friend_code, friend_id } = params || {};
250
+ switch (action) {
251
+ case 'list':
252
+ return { friends: _db.listFriends(currentAgentId) };
253
+ case 'add':
254
+ return await _handshake.addFriendByCode(currentAgentId, friend_code);
255
+ case 'remove':
256
+ _db.removeFriend(currentAgentId, friend_id);
257
+ try { await _serverClient.removeFriend(friend_id); } catch (e) {}
258
+ return { success: true };
259
+ case 'requests':
260
+ return { requests: _db.getPendingRequests(currentAgentId) };
261
+ case 'accept':
262
+ return await _handshake.acceptRequest(currentAgentId, friend_id);
263
+ case 'reject':
264
+ return await _handshake.rejectRequest(currentAgentId, friend_id);
265
+ default:
266
+ return { error: `Unknown friend action: ${action}` };
267
+ }
350
268
  }
351
- });
352
-
353
- app.delete('/api/chat/:messageId', (req, res) => {
354
- const agentId = getAgentId(req);
355
- db.deleteMessage(agentId, req.params.messageId);
356
- res.json({ success: true });
357
- });
358
-
359
- // Streaming endpoints
360
- app.post('/api/chat/stream-chunk', (req, res) => {
361
- try {
362
- const { targetId, friend_id, chunk_type, chunkType, data } = req.body;
363
- const streamTarget = targetId || friend_id;
364
- if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
365
- if (!data) return res.status(400).json({ error: 'data is required' });
366
- const type = chunk_type || chunkType || 'text';
367
- // Allowed chunk types — extended to include thinking and clear_text
269
+ case 'chat-send':
270
+ return await _chat.sendMessage(
271
+ currentAgentId,
272
+ params.targetId,
273
+ params.content,
274
+ { isGroup: params.isGroup || false }
275
+ );
276
+ case 'chat-export-key':
277
+ return _identity.getInfo(currentAgentId) || {};
278
+ default:
279
+ return { error: `Unknown tool: ${toolName}` };
280
+ }
281
+ }
282
+
283
+ // ── Gateway handler ─────────────────────────────────────────────────
284
+ async function handleGateway(method, kwargs = {}) {
285
+ await ensureInitialized();
286
+ const agents = _identity.listAgents();
287
+ const currentAgentId = agents.length > 0 ? agents[0].agent_id : null;
288
+
289
+ switch (method) {
290
+ case 'aicq.status':
291
+ return {
292
+ state: _serverClient.connected ? 'connected' : 'disconnected',
293
+ agent_id: currentAgentId,
294
+ version: '3.0.0',
295
+ architecture: 'channel',
296
+ };
297
+ case 'aicq.friends.list':
298
+ return { friends: _db.listFriends(currentAgentId) };
299
+ case 'aicq.friends.add':
300
+ return await _handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
301
+ case 'aicq.friends.remove':
302
+ _db.removeFriend(currentAgentId, kwargs.friend_id);
303
+ return { success: true };
304
+ case 'aicq.friends.requests':
305
+ return { requests: _db.getPendingRequests(currentAgentId) };
306
+ case 'aicq.friends.acceptRequest':
307
+ return await _handshake.acceptRequest(currentAgentId, kwargs.request_id);
308
+ case 'aicq.friends.rejectRequest':
309
+ return await _handshake.rejectRequest(currentAgentId, kwargs.request_id);
310
+ case 'aicq.identity.info':
311
+ return _identity.getInfo(currentAgentId) || {};
312
+ case 'aicq.agent.create':
313
+ _identity.createAgent(kwargs.agent_id, kwargs.nickname);
314
+ return { success: true };
315
+ case 'aicq.agent.delete':
316
+ _identity.deleteAgent(kwargs.agent_id);
317
+ return { success: true };
318
+ case 'aicq.chat.send':
319
+ return await _chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
320
+ case 'aicq.chat.history':
321
+ return { messages: _db.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) };
322
+ case 'aicq.chat.delete':
323
+ _db.deleteMessage(currentAgentId, kwargs.message_id);
324
+ return { success: true };
325
+ case 'aicq.chat.streamChunk': {
326
+ if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
327
+ if (!kwargs.data) return { error: 'data is required' };
328
+ const chunkType = kwargs.chunk_type || kwargs.chunkType || 'text';
368
329
  const ALLOWED_CHUNK_TYPES = ['text', 'reasoning', 'thinking', 'clear_text', 'tool_call', 'tool_result'];
369
- if (!ALLOWED_CHUNK_TYPES.includes(type)) {
370
- return res.status(400).json({ error: `Invalid chunk_type: ${type}. Allowed: ${ALLOWED_CHUNK_TYPES.join(', ')}` });
371
- }
372
- const sent = serverClient.sendWS({
330
+ if (!ALLOWED_CHUNK_TYPES.includes(chunkType)) return { error: `Invalid chunk_type: ${chunkType}. Allowed: ${ALLOWED_CHUNK_TYPES.join(', ')}` };
331
+ const streamTarget = kwargs.friend_id || kwargs.targetId;
332
+ const sent = _serverClient.sendWS({
373
333
  type: 'stream_chunk',
374
334
  to: streamTarget,
375
- chunkType: type,
376
- data: data,
335
+ chunkType: chunkType,
336
+ data: kwargs.data,
377
337
  });
378
- if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
379
- res.json({ success: true });
380
- } catch (e) {
381
- res.status(500).json({ error: e.message });
338
+ if (!sent) return { error: 'Not connected to server', success: false };
339
+ return { success: true };
382
340
  }
383
- });
384
-
385
- app.post('/api/chat/stream-end', (req, res) => {
386
- try {
387
- const { targetId, friend_id, message_id, messageId } = req.body;
388
- const streamTarget = targetId || friend_id;
389
- if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
390
- const msgId = message_id || messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
391
- const sent = serverClient.sendWS({
341
+ case 'aicq.chat.streamEnd': {
342
+ if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
343
+ const endTarget = kwargs.friend_id || kwargs.targetId;
344
+ const msgId = kwargs.message_id || kwargs.messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
345
+ const endSent = _serverClient.sendWS({
392
346
  type: 'stream_end',
393
- to: streamTarget,
347
+ to: endTarget,
394
348
  messageId: msgId,
395
349
  });
396
- if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
397
- res.json({ success: true, messageId: msgId });
398
- } catch (e) {
399
- res.status(500).json({ error: e.message });
400
- }
401
- });
402
-
403
- // File upload
404
- app.post('/api/upload', upload.single('file'), async (req, res) => {
405
- try {
406
- if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
407
- const agentId = getAgentId(req);
408
- const targetId = req.body.targetId;
409
- const isGroup = req.body.isGroup === 'true' || req.body.isGroup === '1';
410
- const msg = await chat.handleFileUpload(agentId, targetId, req.file, isGroup);
411
- res.json({ success: true, message: msg });
412
- } catch (e) {
413
- res.status(500).json({ error: e.message });
414
- }
415
- });
416
-
417
- app.get('/api/files/:fileId', (req, res) => {
418
- const filePath = path.join(UPLOADS_DIR, req.params.fileId);
419
- if (fs.existsSync(filePath)) {
420
- res.sendFile(filePath);
421
- } else {
422
- res.status(404).json({ error: 'File not found' });
350
+ if (!endSent) return { error: 'Not connected to server', success: false };
351
+ return { success: true, messageId: msgId };
423
352
  }
424
- });
425
-
426
- // Identity
427
- app.get('/api/identity', (req, res) => {
428
- const agentId = getAgentId(req);
429
- res.json(identity.getInfo(agentId) || {});
430
- });
431
-
432
- app.post('/api/identity/nickname', (req, res) => {
433
- const { agent_id, nickname } = req.body;
434
- const agentId = agent_id || currentAgentId;
435
- identity.updateNickname(agentId, nickname);
436
- res.json({ success: true });
437
- });
438
-
439
- app.post('/api/identity/friend-code', async (req, res) => {
440
- try {
441
- const agentId = req.body.agent_id || currentAgentId;
442
- await serverClient.ensureAuth(agentId);
443
- const result = await handshake.generateFriendCode(agentId);
444
- res.json({ success: true, code: result.number, expires_at: result.expiresAt || result.expires_at });
445
- } catch (e) {
446
- res.status(500).json({ error: e.message });
447
- }
448
- });
449
-
450
- app.get('/api/identity/qr', async (req, res) => {
451
- try {
452
- const agentId = getAgentId(req);
453
- const info = identity.getInfo(agentId);
454
- if (!info) return res.status(404).json({ error: 'Agent not found' });
455
- const qrData = JSON.stringify({
456
- type: 'aicq-friend',
457
- agent_id: info.agent_id,
458
- public_key: info.signing_public_key,
459
- exchange_public_key: info.exchange_public_key,
460
- fingerprint: info.fingerprint,
461
- });
462
- const qrImage = await QRCode.toDataURL(qrData);
463
- res.json({ qr: qrImage, data: qrData, info });
464
- } catch (e) {
465
- res.status(500).json({ error: e.message });
466
- }
467
- });
468
-
469
- app.post('/api/identity/rotate-keys', (req, res) => {
470
- try {
471
- const agentId = req.body.agent_id || currentAgentId;
472
- const newInfo = identity.rotateKeys(agentId);
473
- res.json({ success: true, info: identity.getInfo(agentId) });
474
- } catch (e) {
475
- res.status(500).json({ error: e.message });
476
- }
477
- });
478
-
479
- // Avatar upload
480
- const avatarUpload = multer({
481
- storage: multer.memoryStorage(),
482
- limits: { fileSize: 5 * 1024 * 1024 }, // 5MB max (client should resize before uploading)
483
- fileFilter: (req, file, cb) => {
484
- if (file.mimetype && file.mimetype.startsWith('image/')) {
485
- cb(null, true);
486
- } else {
487
- cb(new Error('Only image files are allowed'));
488
- }
489
- },
490
- });
491
-
492
- app.post('/api/identity/avatar', avatarUpload.single('avatar'), async (req, res) => {
493
- try {
494
- if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
495
- const agentId = req.body.agent_id || currentAgentId;
496
-
497
- const avatarsDir = path.join(DATA_DIR, 'avatars');
498
- fs.mkdirSync(avatarsDir, { recursive: true });
499
- const ext = req.file.mimetype.split('/')[1] || 'png';
500
- const avatarId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
501
- const filename = `${avatarId}.${ext}`;
502
- const filePath = path.join(avatarsDir, filename);
503
- fs.writeFileSync(filePath, req.file.buffer);
504
-
505
- const avatarUrl = `/api/identity/avatars/${filename}`;
506
- identity.updateAvatar(agentId, avatarUrl);
507
-
508
- try {
509
- await serverClient.ensureAuth(agentId);
510
- const FormData = (await import('form-data')).default;
511
- const form = new FormData();
512
- form.append('avatar', req.file.buffer, {
513
- filename: req.file.originalname || 'avatar.' + ext,
514
- contentType: req.file.mimetype,
515
- });
516
- const fetch = (await import('node-fetch')).default;
517
- const serverUrl = SERVER_URL + '/api/v1/accounts/avatar';
518
- const serverResp = await fetch(serverUrl, {
519
- method: 'POST',
520
- body: form,
521
- headers: {
522
- ...form.getHeaders(),
523
- 'Authorization': 'Bearer ' + serverClient.getAccessToken(agentId),
524
- },
353
+ case 'aicq.groups.list':
354
+ return { groups: _db.listGroups(currentAgentId) };
355
+ case 'aicq.groups.create': {
356
+ await _serverClient.ensureAuth(currentAgentId);
357
+ const result = await _serverClient.createGroup(kwargs.name, kwargs.description);
358
+ if (result.id) {
359
+ _db.addGroup({
360
+ agent_id: currentAgentId,
361
+ id: result.id,
362
+ name: kwargs.name,
363
+ owner_id: currentAgentId,
364
+ members_json: result.members || '[]',
365
+ description: kwargs.description || '',
525
366
  });
526
- if (serverResp.ok) {
527
- const serverData = await serverResp.json();
528
- if (serverData.avatar) {
529
- identity.updateAvatar(agentId, serverData.avatar);
530
- return res.json({ success: true, avatar: serverData.avatar });
531
- }
532
- }
533
- } catch (e) {
534
- console.error('[AICQ] Server avatar upload failed:', e.message);
535
367
  }
536
-
537
- res.json({ success: true, avatar: avatarUrl });
538
- } catch (e) {
539
- res.status(500).json({ error: e.message });
540
- }
541
- });
542
-
543
- app.get('/api/identity/avatars/:filename', (req, res) => {
544
- const filename = req.params.filename;
545
- if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
546
- return res.status(400).json({ error: 'Invalid filename' });
547
- }
548
- const filePath = path.join(DATA_DIR, 'avatars', filename);
549
- if (fs.existsSync(filePath)) {
550
- res.sendFile(filePath);
551
- } else {
552
- res.status(404).json({ error: 'Avatar not found' });
553
- }
554
- });
555
-
556
- app.get('/api/identity/keys', (req, res) => {
557
- const agentId = getAgentId(req);
558
- const info = identity.loadAgent(agentId);
559
- if (!info) return res.status(404).json({ error: 'Agent not found' });
560
- res.json({
561
- agent_id: info.agent_id,
562
- nickname: info.nickname,
563
- signing_public_key: info.signing_public_key,
564
- exchange_public_key: info.exchange_public_key,
565
- signing_secret_key: info.signing_secret_key,
566
- exchange_secret_key: info.exchange_secret_key,
567
- fingerprint: info.fingerprint,
568
- });
569
- });
570
-
571
- // Sync endpoint
572
- app.post('/api/sync', async (req, res) => {
573
- try {
574
- const agentId = req.body.agent_id || currentAgentId;
575
- await syncFriendsFromServer(agentId);
576
- await syncGroupsFromServer(agentId);
577
- res.json({ success: true });
578
- } catch (e) {
579
- res.status(500).json({ error: e.message });
580
- }
581
- });
582
-
583
- // ─── Gateway Proxy Endpoint (for extension.js) ──────────────────────
584
- app.post('/api/gateway', async (req, res) => {
585
- try {
586
- const { method, kwargs } = req.body;
587
- if (!method) return res.status(400).json({ error: 'method is required' });
588
- const result = await handleGatewayCall(method, kwargs);
589
- res.json(result);
590
- } catch (e) {
591
- res.status(500).json({ error: e.message });
592
- }
593
- });
594
-
595
- // ─── Start Server ───────────────────────────────────────────────────
596
- app.listen(PORT, '0.0.0.0', () => {
597
- console.log(`[AICQ Plugin] Running on http://0.0.0.0:${PORT}`);
598
- console.log(`[AICQ Plugin] Server: ${SERVER_URL}`);
599
- console.log(`[AICQ Plugin] Data dir: ${DATA_DIR}`);
600
- });
601
-
602
- // ─── OpenClaw Gateway Integration ───────────────────────────────────
603
- process.on('message', (msg) => {
604
- if (msg.type === 'gateway_call') {
605
- handleGatewayCall(msg.method, msg.kwargs).then(result => {
606
- process.send({ type: 'gateway_response', id: msg.id, result });
607
- }).catch(err => {
608
- process.send({ type: 'gateway_response', id: msg.id, error: err.message });
609
- });
368
+ return { success: true, group: result };
610
369
  }
611
- });
612
-
613
- async function handleGatewayCall(method, kwargs = {}) {
614
- switch (method) {
615
- case 'aicq.status':
616
- return { state: serverClient.connected ? 'connected' : 'disconnected', agent_id: currentAgentId, version: '2.6.0' };
617
- case 'aicq.friends.list':
618
- return { friends: db.listFriends(currentAgentId) };
619
- case 'aicq.friends.add':
620
- return await handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
621
- case 'aicq.friends.remove':
622
- db.removeFriend(currentAgentId, kwargs.friend_id);
623
- return { success: true };
624
- case 'aicq.friends.requests':
625
- return { requests: db.getPendingRequests(currentAgentId) };
626
- case 'aicq.identity.info':
627
- return identity.getInfo(currentAgentId) || {};
628
- case 'aicq.agent.create':
629
- identity.createAgent(kwargs.agent_id, kwargs.nickname);
630
- return { success: true };
631
- case 'aicq.chat.send':
632
- return await chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
633
- case 'aicq.chat.history':
634
- return { messages: db.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) };
635
- case 'aicq.chat.streamChunk': {
636
- if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
637
- if (!kwargs.data) return { error: 'data is required' };
638
- const chunkType = kwargs.chunk_type || kwargs.chunkType || 'text';
639
- // Allowed chunk types — extended to include thinking and clear_text
640
- const ALLOWED_CHUNK_TYPES = ['text', 'reasoning', 'thinking', 'clear_text', 'tool_call', 'tool_result'];
641
- if (!ALLOWED_CHUNK_TYPES.includes(chunkType)) return { error: `Invalid chunk_type: ${chunkType}. Allowed: ${ALLOWED_CHUNK_TYPES.join(', ')}` };
642
- const streamTarget = kwargs.friend_id || kwargs.targetId;
643
- const sent = serverClient.sendWS({
644
- type: 'stream_chunk',
645
- to: streamTarget,
646
- chunkType: chunkType,
647
- data: kwargs.data,
648
- });
649
- if (!sent) return { error: 'Not connected to server', success: false };
650
- return { success: true };
651
- }
652
- case 'aicq.chat.streamEnd': {
653
- if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
654
- const endTarget = kwargs.friend_id || kwargs.targetId;
655
- const msgId = kwargs.message_id || kwargs.messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
656
- const endSent = serverClient.sendWS({
657
- type: 'stream_end',
658
- to: endTarget,
659
- messageId: msgId,
660
- });
661
- if (!endSent) return { error: 'Not connected to server', success: false };
662
- return { success: true, messageId: msgId };
663
- }
664
- default:
665
- return { error: `Unknown method: ${method}` };
370
+ case 'aicq.groups.join':
371
+ await _serverClient.ensureAuth(currentAgentId);
372
+ return await _serverClient.inviteGroupMember(kwargs.group_id, currentAgentId);
373
+ case 'aicq.groups.messages': {
374
+ await _serverClient.ensureAuth(currentAgentId);
375
+ return await _serverClient.getGroupMessages(kwargs.group_id, kwargs.limit || 50);
666
376
  }
377
+ case 'aicq.groups.silent':
378
+ _db.setGroupSilentMode(currentAgentId, kwargs.group_id, !!kwargs.silent);
379
+ return { success: true, silent: !!kwargs.silent };
380
+ case 'aicq.sessions.list':
381
+ return { sessions: [] };
382
+ default:
383
+ return { error: `Unknown method: ${method}` };
667
384
  }
668
- })().catch(err => {
669
- console.error('[AICQ] Fatal startup error:', err);
670
- process.exit(1);
671
- });
385
+ }
386
+
387
+ // ── Exports ─────────────────────────────────────────────────────────
388
+ module.exports = {
389
+ register,
390
+ activate,
391
+ handleTool,
392
+ handleGateway,
393
+ ensureInitialized,
394
+ };