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/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * AICQ Chat Plugin — Main Entry Point
3
3
  * OpenClaw sidecar plugin providing E2EE chat UI
4
+ *
5
+ * Uses sql.js (pure WASM SQLite) instead of better-sqlite3
6
+ * to avoid native C++ compilation issues.
4
7
  */
5
8
  const express = require('express');
6
9
  const multer = require('multer');
@@ -9,654 +12,660 @@ const fs = require('fs');
9
12
  const os = require('os');
10
13
  const QRCode = require('qrcode');
11
14
  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
15
 
17
16
  // ─── Configuration ──────────────────────────────────────────────────
18
17
  const PORT = parseInt(process.env.AICQ_PORT || '6109', 10);
19
- const SERVER_URL = process.env.AICQ_SERVER_URL || 'http://aicq.online:61018';
18
+ const SERVER_URL = process.env.AICQ_SERVER_URL || 'https://aicq.online';
20
19
  const DATA_DIR = process.env.AICQ_DATA_DIR || path.join(os.homedir(), '.aicq-plugin');
21
20
  const UPLOADS_DIR = path.join(DATA_DIR, 'uploads');
22
21
 
23
22
  fs.mkdirSync(DATA_DIR, { recursive: true });
24
23
  fs.mkdirSync(UPLOADS_DIR, { recursive: true });
25
24
 
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
25
+ // ─── Async bootstrap ────────────────────────────────────────────────
26
+ // sql.js requires async init, so we wrap the entire app setup
45
27
  (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);
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');
32
+
33
+ // Lazy-load modules that depend on db
34
+ const IdentityManager = require('./lib/identity');
35
+ const ServerClient = require('./lib/server-client');
36
+ const HandshakeManager = require('./lib/handshake');
37
+ const ChatManager = require('./lib/chat');
38
+
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);
43
+
44
+ // Auto-create a default agent if none exists
45
+ const agents = identity.listAgents();
46
+ let currentAgentId = null;
47
+ if (agents.length === 0) {
48
+ const defaultAgent = identity.createAgent('agent-' + Date.now(), '默认Agent');
49
+ currentAgentId = defaultAgent.agent_id;
50
+ console.log('[AICQ] Created default agent:', currentAgentId);
51
+ } else {
52
+ currentAgentId = agents[0].agent_id;
53
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);
54
+
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;
72
+ }
73
+
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
+ }
83
94
  }
84
95
  }
96
+ } catch (e) {
97
+ console.error('[AICQ] Sync friends failed:', e.message);
85
98
  }
86
- } catch (e) {
87
- console.error('[AICQ] Sync friends failed:', e.message);
88
99
  }
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
- });
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({
108
+ 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 || '',
114
+ });
115
+ }
105
116
  }
117
+ } catch (e) {
118
+ console.error('[AICQ] Sync groups failed:', e.message);
106
119
  }
107
- } catch (e) {
108
- console.error('[AICQ] Sync groups failed:', e.message);
109
120
  }
110
- }
111
121
 
112
- // ─── Express App ────────────────────────────────────────────────────
113
- const app = express();
114
- app.use(express.json());
115
- app.use(express.urlencoded({ extended: true }));
122
+ // ─── Express App ────────────────────────────────────────────────────
123
+ const app = express();
124
+ app.use(express.json());
125
+ app.use(express.urlencoded({ extended: true }));
116
126
 
117
- const upload = multer({
118
- storage: multer.memoryStorage(),
119
- limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
120
- });
127
+ const upload = multer({
128
+ storage: multer.memoryStorage(),
129
+ limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
130
+ });
121
131
 
122
- // ─── Serve SPA ──────────────────────────────────────────────────────
123
- app.use(express.static(path.join(__dirname, 'public')));
132
+ // ─── Serve SPA ──────────────────────────────────────────────────────
133
+ app.use(express.static(path.join(__dirname, 'public')));
124
134
 
125
- // ─── API Routes ─────────────────────────────────────────────────────
135
+ // ─── API Routes ─────────────────────────────────────────────────────
126
136
 
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,
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
+ });
135
146
  });
136
- });
137
147
 
138
- // Agents
139
- app.get('/api/agents', (req, res) => {
140
- res.json({ agents: identity.listAgents() });
141
- });
148
+ // Agents
149
+ app.get('/api/agents', (req, res) => {
150
+ res.json({ agents: identity.listAgents() });
151
+ });
142
152
 
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
153
+ app.post('/api/agents', async (req, res) => {
150
154
  try {
151
- await serverClient.start(agent_id);
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 });
152
165
  } catch (e) {
153
- console.error('Server registration failed:', e.message);
166
+ res.status(500).json({ error: e.message });
154
167
  }
155
- res.json({ success: true, agent });
156
- } catch (e) {
157
- res.status(500).json({ error: e.message });
158
- }
159
- });
168
+ });
160
169
 
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
- });
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
+ });
169
178
 
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
- });
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
+ });
183
192
 
184
- // Friends
185
- app.get('/api/friends', (req, res) => {
186
- const agentId = getAgentId(req);
187
- res.json({ friends: db.listFriends(agentId) });
188
- });
193
+ // Friends
194
+ app.get('/api/friends', (req, res) => {
195
+ const agentId = getAgentId(req);
196
+ res.json({ friends: db.listFriends(agentId) });
197
+ });
189
198
 
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
- });
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
+ });
202
211
 
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
- });
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
+ });
213
222
 
214
- app.get('/api/friends/requests', async (req, res) => {
215
- try {
216
- const agentId = getAgentId(req);
217
- // Get from server
218
- let serverRequests = [];
223
+ app.get('/api/friends/requests', async (req, res) => {
219
224
  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
- });
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
+ });
231
239
 
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
- });
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
+ });
241
259
 
242
- app.post('/api/friends/requests/:id/reject', async (req, res) => {
243
- try {
260
+ // Groups
261
+ app.get('/api/groups', (req, res) => {
244
262
  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
- });
263
+ res.json({ groups: db.listGroups(agentId) });
264
+ });
251
265
 
252
- // Groups
253
- app.get('/api/groups', (req, res) => {
254
- const agentId = getAgentId(req);
255
- res.json({ groups: db.listGroups(agentId) });
256
- });
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({
275
+ agent_id: agentId,
276
+ id: result.id,
277
+ name,
278
+ owner_id: agentId,
279
+ members_json: result.members || '[]',
280
+ description: description || '',
281
+ });
282
+ }
283
+ res.json({ success: true, group: result });
284
+ } catch (e) {
285
+ res.status(500).json({ error: e.message });
286
+ }
287
+ });
257
288
 
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
- });
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 });
274
297
  }
275
- res.json({ success: true, group: result });
276
- } catch (e) {
277
- res.status(500).json({ error: e.message });
278
- }
279
- });
298
+ });
280
299
 
281
- app.post('/api/groups/:id/join', async (req, res) => {
282
- try {
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) => {
283
320
  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
- });
321
+ const { silent } = req.body;
322
+ db.setGroupSilentMode(agentId, req.params.id, !!silent);
323
+ res.json({ success: true, silent: !!silent });
324
+ });
291
325
 
292
- app.get('/api/groups/:id/messages', async (req, res) => {
293
- try {
326
+ // Chat
327
+ app.get('/api/chat/:targetId', (req, res) => {
294
328
  const agentId = getAgentId(req);
295
329
  const limit = parseInt(req.query.limit || '50', 10);
296
330
  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 });
331
+ const messages = db.getChatHistory(agentId, req.params.targetId, { limit, before });
307
332
  res.json({ messages });
308
- } catch (e) {
309
- res.status(500).json({ error: e.message });
310
- }
311
- });
333
+ });
312
334
 
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
- });
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 });
350
+ }
351
+ });
319
352
 
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
- });
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
+ });
328
358
 
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
- });
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
368
+ 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({
373
+ type: 'stream_chunk',
374
+ to: streamTarget,
375
+ chunkType: type,
376
+ data: data,
377
+ });
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 });
382
+ }
383
+ });
346
384
 
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
- });
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({
392
+ type: 'stream_end',
393
+ to: streamTarget,
394
+ messageId: msgId,
395
+ });
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
+ });
352
402
 
353
- // Streaming endpoints — for external systems / OpenClaw agent output
354
- app.post('/api/chat/stream-chunk', (req, res) => {
355
- try {
356
- const { targetId, friend_id, chunk_type, chunkType, data } = req.body;
357
- const streamTarget = targetId || friend_id;
358
- if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
359
- if (!data) return res.status(400).json({ error: 'data is required' });
360
- const type = chunk_type || chunkType || 'text';
361
- if (!['text', 'reasoning', 'tool_call', 'tool_result'].includes(type)) {
362
- return res.status(400).json({ error: `Invalid chunk_type: ${type}` });
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 });
363
414
  }
364
- const sent = serverClient.sendWS({
365
- type: 'stream_chunk',
366
- to: streamTarget,
367
- chunkType: type,
368
- data: data,
369
- });
370
- if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
371
- res.json({ success: true });
372
- } catch (e) {
373
- res.status(500).json({ error: e.message });
374
- }
375
- });
415
+ });
376
416
 
377
- app.post('/api/chat/stream-end', (req, res) => {
378
- try {
379
- const { targetId, friend_id, message_id, messageId } = req.body;
380
- const streamTarget = targetId || friend_id;
381
- if (!streamTarget) return res.status(400).json({ error: 'targetId or friend_id is required' });
382
- const msgId = message_id || messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
383
- const sent = serverClient.sendWS({
384
- type: 'stream_end',
385
- to: streamTarget,
386
- messageId: msgId,
387
- });
388
- if (!sent) return res.status(503).json({ error: 'Not connected to server', success: false });
389
- res.json({ success: true, messageId: msgId });
390
- } catch (e) {
391
- res.status(500).json({ error: e.message });
392
- }
393
- });
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' });
423
+ }
424
+ });
394
425
 
395
- // File upload
396
- app.post('/api/upload', upload.single('file'), async (req, res) => {
397
- try {
398
- if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
426
+ // Identity
427
+ app.get('/api/identity', (req, res) => {
399
428
  const agentId = getAgentId(req);
400
- const targetId = req.body.targetId;
401
- const isGroup = req.body.isGroup === 'true' || req.body.isGroup === '1';
402
- const msg = await chat.handleFileUpload(agentId, targetId, req.file, isGroup);
403
- res.json({ success: true, message: msg });
404
- } catch (e) {
405
- res.status(500).json({ error: e.message });
406
- }
407
- });
429
+ res.json(identity.getInfo(agentId) || {});
430
+ });
408
431
 
409
- app.get('/api/files/:fileId', (req, res) => {
410
- const filePath = path.join(UPLOADS_DIR, req.params.fileId);
411
- if (fs.existsSync(filePath)) {
412
- res.sendFile(filePath);
413
- } else {
414
- res.status(404).json({ error: 'File not found' });
415
- }
416
- });
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
+ });
417
438
 
418
- // Identity
419
- app.get('/api/identity', (req, res) => {
420
- const agentId = getAgentId(req);
421
- res.json(identity.getInfo(agentId) || {});
422
- });
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
+ });
423
449
 
424
- app.post('/api/identity/nickname', (req, res) => {
425
- const { agent_id, nickname } = req.body;
426
- const agentId = agent_id || currentAgentId;
427
- identity.updateNickname(agentId, nickname);
428
- res.json({ success: true });
429
- });
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
+ });
430
468
 
431
- app.post('/api/identity/friend-code', async (req, res) => {
432
- try {
433
- const agentId = req.body.agent_id || currentAgentId;
434
- await serverClient.ensureAuth(agentId);
435
- const result = await handshake.generateFriendCode(agentId);
436
- res.json({ success: true, code: result.number, expires_at: result.expiresAt || result.expires_at });
437
- } catch (e) {
438
- res.status(500).json({ error: e.message });
439
- }
440
- });
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
+ });
441
491
 
442
- app.get('/api/identity/qr', async (req, res) => {
443
- try {
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
+ },
525
+ });
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
+ }
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) => {
444
557
  const agentId = getAgentId(req);
445
- const info = identity.getInfo(agentId);
558
+ const info = identity.loadAgent(agentId);
446
559
  if (!info) return res.status(404).json({ error: 'Agent not found' });
447
- const qrData = JSON.stringify({
448
- type: 'aicq-friend',
560
+ res.json({
449
561
  agent_id: info.agent_id,
450
- public_key: info.signing_public_key,
562
+ nickname: info.nickname,
563
+ signing_public_key: info.signing_public_key,
451
564
  exchange_public_key: info.exchange_public_key,
565
+ signing_secret_key: info.signing_secret_key,
566
+ exchange_secret_key: info.exchange_secret_key,
452
567
  fingerprint: info.fingerprint,
453
568
  });
454
- const qrImage = await QRCode.toDataURL(qrData);
455
- res.json({ qr: qrImage, data: qrData, info });
456
- } catch (e) {
457
- res.status(500).json({ error: e.message });
458
- }
459
- });
460
-
461
- app.post('/api/identity/rotate-keys', (req, res) => {
462
- try {
463
- const agentId = req.body.agent_id || currentAgentId;
464
- const newInfo = identity.rotateKeys(agentId);
465
- res.json({ success: true, info: identity.getInfo(agentId) });
466
- } catch (e) {
467
- res.status(500).json({ error: e.message });
468
- }
469
- });
569
+ });
470
570
 
471
- // Avatar upload
472
- const avatarUpload = multer({
473
- storage: multer.memoryStorage(),
474
- limits: { fileSize: 2 * 1024 * 1024 }, // 2MB max
475
- fileFilter: (req, file, cb) => {
476
- if (file.mimetype && file.mimetype.startsWith('image/')) {
477
- cb(null, true);
478
- } else {
479
- cb(new Error('Only image files are allowed'));
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 });
480
580
  }
481
- },
482
- });
581
+ });
483
582
 
484
- app.post('/api/identity/avatar', avatarUpload.single('avatar'), async (req, res) => {
485
- try {
486
- if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
487
- const agentId = req.body.agent_id || currentAgentId;
488
-
489
- // Save avatar locally
490
- const avatarsDir = path.join(DATA_DIR, 'avatars');
491
- fs.mkdirSync(avatarsDir, { recursive: true });
492
- const ext = req.file.mimetype.split('/')[1] || 'png';
493
- const avatarId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
494
- const filename = `${avatarId}.${ext}`;
495
- const filePath = path.join(avatarsDir, filename);
496
- fs.writeFileSync(filePath, req.file.buffer);
497
-
498
- // Update local identity
499
- const avatarUrl = `/api/identity/avatars/${filename}`;
500
- identity.updateAvatar(agentId, avatarUrl);
501
-
502
- // Try to upload to server too
583
+ // ─── Gateway Proxy Endpoint (for extension.js) ──────────────────────
584
+ app.post('/api/gateway', async (req, res) => {
503
585
  try {
504
- await serverClient.ensureAuth(agentId);
505
- const FormData = (await import('form-data')).default;
506
- const form = new FormData();
507
- form.append('avatar', req.file.buffer, {
508
- filename: req.file.originalname || 'avatar.' + ext,
509
- contentType: req.file.mimetype,
510
- });
511
- // Use node-fetch to upload to server
512
- const fetch = (await import('node-fetch')).default;
513
- const serverUrl = SERVER_URL + '/api/v1/accounts/avatar';
514
- const serverResp = await fetch(serverUrl, {
515
- method: 'POST',
516
- body: form,
517
- headers: {
518
- ...form.getHeaders(),
519
- 'Authorization': 'Bearer ' + serverClient.getAccessToken(agentId),
520
- },
521
- });
522
- if (serverResp.ok) {
523
- const serverData = await serverResp.json();
524
- if (serverData.avatar) {
525
- identity.updateAvatar(agentId, serverData.avatar);
526
- return res.json({ success: true, avatar: serverData.avatar });
527
- }
528
- }
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);
529
590
  } catch (e) {
530
- console.error('[AICQ] Server avatar upload failed:', e.message);
591
+ res.status(500).json({ error: e.message });
531
592
  }
532
-
533
- res.json({ success: true, avatar: avatarUrl });
534
- } catch (e) {
535
- res.status(500).json({ error: e.message });
536
- }
537
- });
538
-
539
- app.get('/api/identity/avatars/:filename', (req, res) => {
540
- const filename = req.params.filename;
541
- // Security: prevent directory traversal
542
- if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
543
- return res.status(400).json({ error: 'Invalid filename' });
544
- }
545
- const filePath = path.join(DATA_DIR, 'avatars', filename);
546
- if (fs.existsSync(filePath)) {
547
- res.sendFile(filePath);
548
- } else {
549
- res.status(404).json({ error: 'Avatar not found' });
550
- }
551
- });
552
-
553
- app.get('/api/identity/keys', (req, res) => {
554
- const agentId = getAgentId(req);
555
- const info = identity.loadAgent(agentId);
556
- if (!info) return res.status(404).json({ error: 'Agent not found' });
557
- res.json({
558
- agent_id: info.agent_id,
559
- nickname: info.nickname,
560
- signing_public_key: info.signing_public_key,
561
- exchange_public_key: info.exchange_public_key,
562
- signing_secret_key: info.signing_secret_key,
563
- exchange_secret_key: info.exchange_secret_key,
564
- fingerprint: info.fingerprint,
565
593
  });
566
- });
567
-
568
- // Sync endpoint
569
- app.post('/api/sync', async (req, res) => {
570
- try {
571
- const agentId = req.body.agent_id || currentAgentId;
572
- await syncFriendsFromServer(agentId);
573
- await syncGroupsFromServer(agentId);
574
- res.json({ success: true });
575
- } catch (e) {
576
- res.status(500).json({ error: e.message });
577
- }
578
- });
579
-
580
- // ─── Gateway Proxy Endpoint (for extension.js) ──────────────────────
581
- app.post('/api/gateway', async (req, res) => {
582
- try {
583
- const { method, kwargs } = req.body;
584
- if (!method) return res.status(400).json({ error: 'method is required' });
585
- const result = await handleGatewayCall(method, kwargs);
586
- res.json(result);
587
- } catch (e) {
588
- res.status(500).json({ error: e.message });
589
- }
590
- });
591
-
592
- // ─── Start Server ───────────────────────────────────────────────────
593
- app.listen(PORT, '0.0.0.0', () => {
594
- console.log(`[AICQ Plugin] Running on http://0.0.0.0:${PORT}`);
595
- console.log(`[AICQ Plugin] Server: ${SERVER_URL}`);
596
- console.log(`[AICQ Plugin] Data dir: ${DATA_DIR}`);
597
- });
598
594
 
599
- // ─── OpenClaw Gateway Integration ───────────────────────────────────
600
- process.on('message', (msg) => {
601
- if (msg.type === 'gateway_call') {
602
- handleGatewayCall(msg.method, msg.kwargs).then(result => {
603
- process.send({ type: 'gateway_response', id: msg.id, result });
604
- }).catch(err => {
605
- process.send({ type: 'gateway_response', id: msg.id, error: err.message });
606
- });
607
- }
608
- });
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
+ });
609
601
 
610
- async function handleGatewayCall(method, kwargs = {}) {
611
- switch (method) {
612
- case 'aicq.status':
613
- return { state: serverClient.connected ? 'connected' : 'disconnected', agent_id: currentAgentId, version: '2.1.0' };
614
- case 'aicq.friends.list':
615
- return { friends: db.listFriends(currentAgentId) };
616
- case 'aicq.friends.add':
617
- return await handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
618
- case 'aicq.friends.remove':
619
- db.removeFriend(currentAgentId, kwargs.friend_id);
620
- return { success: true };
621
- case 'aicq.friends.requests':
622
- return { requests: db.getPendingRequests(currentAgentId) };
623
- case 'aicq.identity.info':
624
- return identity.getInfo(currentAgentId) || {};
625
- case 'aicq.agent.create':
626
- identity.createAgent(kwargs.agent_id, kwargs.nickname);
627
- return { success: true };
628
- case 'aicq.chat.send':
629
- return await chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, { isGroup: kwargs.isGroup });
630
- case 'aicq.chat.history':
631
- return { messages: db.getChatHistory(currentAgentId, kwargs.targetId, { limit: kwargs.limit || 50 }) };
632
- case 'aicq.chat.streamChunk': {
633
- if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
634
- if (!kwargs.data) return { error: 'data is required' };
635
- const chunkType = kwargs.chunk_type || kwargs.chunkType || 'text';
636
- if (!['text', 'reasoning', 'tool_call', 'tool_result'].includes(chunkType)) return { error: `Invalid chunk_type: ${chunkType}` };
637
- const streamTarget = kwargs.friend_id || kwargs.targetId;
638
- const sent = serverClient.sendWS({
639
- type: 'stream_chunk',
640
- to: streamTarget,
641
- chunkType: chunkType,
642
- data: kwargs.data,
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 });
643
609
  });
644
- if (!sent) return { error: 'Not connected to server', success: false };
645
- return { success: true };
646
610
  }
647
- case 'aicq.chat.streamEnd': {
648
- if (!kwargs.friend_id && !kwargs.targetId) return { error: 'friend_id or targetId is required' };
649
- const endTarget = kwargs.friend_id || kwargs.targetId;
650
- const msgId = kwargs.message_id || kwargs.messageId || ('msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6));
651
- const endSent = serverClient.sendWS({
652
- type: 'stream_end',
653
- to: endTarget,
654
- messageId: msgId,
655
- });
656
- if (!endSent) return { error: 'Not connected to server', success: false };
657
- return { success: true, messageId: msgId };
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}` };
658
666
  }
659
- default:
660
- return { error: `Unknown method: ${method}` };
661
667
  }
662
- }
668
+ })().catch(err => {
669
+ console.error('[AICQ] Fatal startup error:', err);
670
+ process.exit(1);
671
+ });