aicq-chat-plugin 3.2.2 → 3.4.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/ui-routes.js CHANGED
@@ -4,466 +4,591 @@
4
4
  * Provides HTTP route handlers for the OpenClaw Gateway.
5
5
  * These routes serve the SPA UI and REST API endpoints.
6
6
  *
7
- * Routes are served via Gateway HTTP, not an independent Express server.
8
- * Prefix: /plugins/aicq-chat/
7
+ * Strategy: We create an Express sub-app with all routes (defined
8
+ * relative to the /plugins/aicq-chat mount point), then register it
9
+ * via api.registerHttpRoute(). The OpenClaw gateway runs Express
10
+ * internally and uses app.use(path, handler), so the mount prefix
11
+ * is stripped before the request reaches our sub-app.
12
+ *
13
+ * ESM module — imported by index.js registerFull() to register routes
14
+ * on the OpenClaw plugin API.
9
15
  */
10
- const path = require('path');
11
- const fs = require('fs');
12
- const QRCode = require('qrcode');
16
+
17
+ import path from "path";
18
+ import fs from "fs";
19
+ import { fileURLToPath } from "url";
20
+ import { createRequire } from "module";
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const QRCode = require("qrcode");
24
+ const multer = require("multer");
25
+ const express = require("express");
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
28
 
14
29
  /**
15
- * Create UI route handlers
16
- * @param {Object} ctx - Plugin context with managers
30
+ * Create and return the Express sub-app with all AICQ routes.
31
+ *
32
+ * Routes are defined RELATIVE to the /plugins/aicq-chat mount point.
33
+ * For example, the API route /plugins/aicq-chat/api/status is
34
+ * defined as /api/status in the sub-app.
35
+ *
36
+ * @param {object} ctx - Plugin context { ensureInitialized, runtime, DATA_DIR, SERVER_URL }
37
+ * @returns {import('express').Express} Express sub-app
17
38
  */
18
- function createUiRoutes(ctx) {
19
- const { db, identity, serverClient, handshake, chat, dataDir } = ctx;
20
- const UPLOADS_DIR = path.join(dataDir, 'uploads');
39
+ function createAicqExpressApp(ctx) {
40
+ const { ensureInitialized, runtime, DATA_DIR, SERVER_URL } = ctx;
41
+ const UPLOADS_DIR = path.join(DATA_DIR, "uploads");
21
42
 
22
43
  // Ensure uploads directory exists
23
44
  fs.mkdirSync(UPLOADS_DIR, { recursive: true });
24
45
 
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
- }
46
+ const app = express();
31
47
 
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
- });
48
+ // Parse JSON bodies
49
+ app.use(express.json());
49
50
 
50
- // ── API Routes ────────────────────────────────────────────────
51
+ // Parse URL-encoded bodies
52
+ app.use(express.urlencoded({ extended: true }));
51
53
 
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
- });
54
+ const upload = multer({
55
+ storage: multer.memoryStorage(),
56
+ limits: { fileSize: 50 * 1024 * 1024 },
57
+ });
62
58
 
63
- // Agents
64
- app.get('/plugins/aicq-chat/api/agents', (req, res) => {
65
- res.json({ agents: identity.listAgents() });
66
- });
59
+ /**
60
+ * Helper to get current agent ID from a request
61
+ */
62
+ function getAgentId(req) {
63
+ return (
64
+ req.query?.agent_id ||
65
+ req.body?.agent_id ||
66
+ (runtime.identity && runtime.identity.listAgents()[0]?.agent_id)
67
+ );
68
+ }
67
69
 
68
- app.post('/plugins/aicq-chat/api/agents', async (req, res) => {
70
+ // ── Serve SPA static files ────────────────────────────────────────
71
+ // NOTE: The mount point /plugins/aicq-chat is stripped by Express,
72
+ // so our sub-app sees /ui/... for requests to /plugins/aicq-chat/ui/...
73
+ const publicDir = path.join(__dirname, "..", "public");
74
+
75
+ app.use("/ui", (req, res, next) => {
76
+ const filePath = path.join(publicDir, req.path === "/" ? "index.html" : req.path);
77
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
78
+ res.sendFile(filePath);
79
+ } else {
80
+ // SPA fallback: serve index.html for all unknown routes
81
+ res.sendFile(path.join(publicDir, "index.html"));
82
+ }
83
+ });
84
+
85
+ // ── API Routes ────────────────────────────────────────────────────
86
+ // All paths are relative to the /plugins/aicq-chat mount point.
87
+
88
+ // Status
89
+ app.get("/api/status", (req, res) => {
90
+ res.json({
91
+ status: "running",
92
+ version: "3.2.0",
93
+ architecture: "channel",
94
+ connected: runtime.serverClient?.connected || false,
95
+ serverUrl: SERVER_URL,
96
+ });
97
+ });
98
+
99
+ // Agents
100
+ app.get("/api/agents", async (req, res) => {
101
+ await ensureInitialized();
102
+ res.json({ agents: runtime.identity.listAgents() });
103
+ });
104
+
105
+ app.post("/api/agents", async (req, res) => {
106
+ await ensureInitialized();
107
+ try {
108
+ const { agent_id, nickname } = req.body;
109
+ if (!agent_id) return res.status(400).json({ error: "agent_id is required" });
110
+ const agent = runtime.identity.createAgent(agent_id, nickname);
69
111
  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 });
112
+ await runtime.serverClient.start(agent_id);
79
113
  } catch (e) {
80
- res.status(500).json({ error: e.message });
114
+ console.error("[AICQ] Server registration failed:", e.message);
81
115
  }
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) => {
116
+ res.json({ success: true, agent });
117
+ } catch (e) {
118
+ res.status(500).json({ error: e.message });
119
+ }
120
+ });
121
+
122
+ app.delete("/api/agents/:id", async (req, res) => {
123
+ await ensureInitialized();
124
+ runtime.identity.deleteAgent(req.params.id);
125
+ res.json({ success: true });
126
+ });
127
+
128
+ // Friends
129
+ app.get("/api/friends", async (req, res) => {
130
+ await ensureInitialized();
131
+ const agentId = getAgentId(req);
132
+ res.json({ friends: runtime.db.listFriends(agentId) });
133
+ });
134
+
135
+ app.post("/api/friends/add", async (req, res) => {
136
+ await ensureInitialized();
137
+ try {
138
+ const { temp_number, friend_code, agent_id } = req.body;
139
+ const agentId = agent_id || getAgentId(req);
140
+ const code = temp_number || friend_code;
141
+ if (!code)
142
+ return res.status(400).json({ error: "temp_number or friend_code is required" });
143
+ const result = await runtime.handshake.addFriendByCode(agentId, code);
144
+ res.json({ success: true, result });
145
+ } catch (e) {
146
+ res.status(500).json({ error: e.message });
147
+ }
148
+ });
149
+
150
+ app.delete("/api/friends/:id", async (req, res) => {
151
+ await ensureInitialized();
152
+ try {
91
153
  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) => {
154
+ runtime.db.removeFriend(agentId, req.params.id);
120
155
  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) => {
156
+ await runtime.serverClient.removeFriend(req.params.id);
157
+ } catch (e) {}
158
+ res.json({ success: true });
159
+ } catch (e) {
160
+ res.status(500).json({ error: e.message });
161
+ }
162
+ });
163
+
164
+ app.get("/api/friends/requests", async (req, res) => {
165
+ await ensureInitialized();
166
+ try {
158
167
  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) => {
168
+ let serverRequests = [];
186
169
  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) => {
170
+ await runtime.serverClient.ensureAuth(agentId);
171
+ const result = await runtime.serverClient.listFriendRequests();
172
+ serverRequests = result.sent || [];
173
+ serverRequests = serverRequests.concat(result.received || []);
174
+ } catch (e) {}
175
+ const localRequests = runtime.db.getPendingRequests(agentId);
176
+ res.json({ requests: [...localRequests, ...serverRequests] });
177
+ } catch (e) {
178
+ res.status(500).json({ error: e.message });
179
+ }
180
+ });
181
+
182
+ app.post("/api/friends/requests/:id/accept", async (req, res) => {
183
+ await ensureInitialized();
184
+ try {
216
185
  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) => {
186
+ const result = await runtime.handshake.acceptRequest(agentId, req.params.id);
187
+ res.json(result);
188
+ } catch (e) {
189
+ res.status(500).json({ error: e.message });
190
+ }
191
+ });
192
+
193
+ app.post("/api/friends/requests/:id/reject", async (req, res) => {
194
+ await ensureInitialized();
195
+ try {
224
196
  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,
197
+ const result = await runtime.handshake.rejectRequest(agentId, req.params.id);
198
+ res.json(result);
199
+ } catch (e) {
200
+ res.status(500).json({ error: e.message });
201
+ }
202
+ });
203
+
204
+ // Groups
205
+ app.get("/api/groups", async (req, res) => {
206
+ await ensureInitialized();
207
+ const agentId = getAgentId(req);
208
+ res.json({ groups: runtime.db.listGroups(agentId) });
209
+ });
210
+
211
+ app.post("/api/groups", async (req, res) => {
212
+ await ensureInitialized();
213
+ try {
214
+ const agentId = getAgentId(req);
215
+ const { name, description } = req.body;
216
+ if (!name) return res.status(400).json({ error: "name is required" });
217
+ await runtime.serverClient.ensureAuth(agentId);
218
+ const result = await runtime.serverClient.createGroup(name, description);
219
+ if (result.id) {
220
+ runtime.db.addGroup({
221
+ agent_id: agentId,
222
+ id: result.id,
223
+ name,
224
+ owner_id: agentId,
225
+ members_json: result.members || "[]",
226
+ description: description || "",
242
227
  });
243
- res.json({ success: true, message: msg });
244
- } catch (e) {
245
- res.status(500).json({ error: e.message });
246
228
  }
247
- });
248
-
249
- app.delete('/plugins/aicq-chat/api/chat/:messageId', (req, res) => {
229
+ res.json({ success: true, group: result });
230
+ } catch (e) {
231
+ res.status(500).json({ error: e.message });
232
+ }
233
+ });
234
+
235
+ app.post("/api/groups/:id/join", async (req, res) => {
236
+ await ensureInitialized();
237
+ try {
250
238
  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) => {
239
+ await runtime.serverClient.ensureAuth(agentId);
240
+ const result = await runtime.serverClient.inviteGroupMember(req.params.id, agentId);
241
+ res.json({ success: true, result });
242
+ } catch (e) {
243
+ res.status(500).json({ error: e.message });
244
+ }
245
+ });
246
+
247
+ app.get("/api/groups/:id/messages", async (req, res) => {
248
+ await ensureInitialized();
249
+ try {
250
+ const agentId = getAgentId(req);
251
+ const limit = parseInt(req.query.limit || "50", 10);
252
+ const before = req.query.before || null;
257
253
  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(', ')}` });
254
+ await runtime.serverClient.ensureAuth(agentId);
255
+ const result = await runtime.serverClient.getGroupMessages(
256
+ req.params.id,
257
+ limit,
258
+ before
259
+ );
260
+ if (result.messages && result.messages.length > 0) {
261
+ return res.json({ messages: result.messages });
266
262
  }
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;
263
+ } catch (e) {}
264
+ const messages = runtime.db.getChatHistory(agentId, req.params.id, {
265
+ limit,
266
+ before,
267
+ });
268
+ res.json({ messages });
269
+ } catch (e) {
270
+ res.status(500).json({ error: e.message });
271
+ }
272
+ });
273
+
274
+ app.put("/api/groups/:id/silent", async (req, res) => {
275
+ await ensureInitialized();
276
+ const agentId = getAgentId(req);
277
+ const { silent } = req.body;
278
+ runtime.db.setGroupSilentMode(agentId, req.params.id, !!silent);
279
+ res.json({ success: true, silent: !!silent });
280
+ });
281
+
282
+ // Chat
283
+ app.get("/api/chat/:targetId", async (req, res) => {
284
+ await ensureInitialized();
285
+ const agentId = getAgentId(req);
286
+ const limit = parseInt(req.query.limit || "50", 10);
287
+ const before = req.query.before || null;
288
+ const messages = runtime.db.getChatHistory(agentId, req.params.targetId, {
289
+ limit,
290
+ before,
291
+ });
292
+ res.json({ messages });
293
+ });
294
+
295
+ app.post("/api/chat/send", async (req, res) => {
296
+ await ensureInitialized();
297
+ try {
298
+ const {
299
+ agent_id,
300
+ targetId,
301
+ content,
302
+ type,
303
+ isGroup,
304
+ mentions,
305
+ file_url,
306
+ file_name,
307
+ } = req.body;
335
308
  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 });
309
+ if (!targetId || !content)
310
+ return res.status(400).json({ error: "targetId and content are required" });
311
+ const msg = await runtime.chat.sendMessage(agentId, targetId, content, {
312
+ type: type || "text",
313
+ isGroup: !!isGroup,
314
+ mentions: mentions || [],
315
+ file_url,
316
+ file_name,
317
+ });
318
+ res.json({ success: true, message: msg });
319
+ } catch (e) {
320
+ res.status(500).json({ error: e.message });
321
+ }
322
+ });
323
+
324
+ app.delete("/api/chat/:messageId", async (req, res) => {
325
+ await ensureInitialized();
326
+ const agentId = getAgentId(req);
327
+ runtime.db.deleteMessage(agentId, req.params.messageId);
328
+ res.json({ success: true });
329
+ });
330
+
331
+ // Streaming endpoints
332
+ app.post("/api/chat/stream-chunk", async (req, res) => {
333
+ await ensureInitialized();
334
+ try {
335
+ const { targetId, friend_id, chunk_type, chunkType, data } = req.body;
336
+ const streamTarget = targetId || friend_id;
337
+ if (!streamTarget)
338
+ return res.status(400).json({ error: "targetId or friend_id is required" });
339
+ if (!data) return res.status(400).json({ error: "data is required" });
340
+ const type = chunk_type || chunkType || "text";
341
+ const ALLOWED_CHUNK_TYPES = [
342
+ "text",
343
+ "reasoning",
344
+ "thinking",
345
+ "clear_text",
346
+ "tool_call",
347
+ "tool_result",
348
+ ];
349
+ if (!ALLOWED_CHUNK_TYPES.includes(type)) {
350
+ return res
351
+ .status(400)
352
+ .json({
353
+ error: `Invalid chunk_type: ${type}. Allowed: ${ALLOWED_CHUNK_TYPES.join(", ")}`,
354
+ });
377
355
  }
378
- });
379
-
380
- app.get('/plugins/aicq-chat/api/identity/keys', (req, res) => {
356
+ const sent = runtime.serverClient.sendWS({
357
+ type: "stream_chunk",
358
+ to: streamTarget,
359
+ chunkType: type,
360
+ data: data,
361
+ });
362
+ if (!sent)
363
+ return res.status(503).json({ error: "Not connected to server", success: false });
364
+ res.json({ success: true });
365
+ } catch (e) {
366
+ res.status(500).json({ error: e.message });
367
+ }
368
+ });
369
+
370
+ app.post("/api/chat/stream-end", async (req, res) => {
371
+ await ensureInitialized();
372
+ try {
373
+ const { targetId, friend_id, message_id, messageId } = req.body;
374
+ const streamTarget = targetId || friend_id;
375
+ if (!streamTarget)
376
+ return res.status(400).json({ error: "targetId or friend_id is required" });
377
+ const msgId =
378
+ message_id ||
379
+ messageId ||
380
+ "msg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6);
381
+ const sent = runtime.serverClient.sendWS({
382
+ type: "stream_end",
383
+ to: streamTarget,
384
+ messageId: msgId,
385
+ });
386
+ if (!sent)
387
+ return res.status(503).json({ error: "Not connected to server", success: false });
388
+ res.json({ success: true, messageId: msgId });
389
+ } catch (e) {
390
+ res.status(500).json({ error: e.message });
391
+ }
392
+ });
393
+
394
+ // File upload
395
+ app.post("/api/upload", upload.single("file"), async (req, res) => {
396
+ await ensureInitialized();
397
+ try {
398
+ if (!req.file) return res.status(400).json({ error: "No file uploaded" });
381
399
  const agentId = getAgentId(req);
382
- const info = identity.loadAgent(agentId);
383
- if (!info) return res.status(404).json({ error: 'Agent not found' });
400
+ const targetId = req.body.targetId;
401
+ const isGroup = req.body.isGroup === "true" || req.body.isGroup === "1";
402
+ const msg = await runtime.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
+ });
408
+
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
+ });
417
+
418
+ // Identity
419
+ app.get("/api/identity", async (req, res) => {
420
+ await ensureInitialized();
421
+ const agentId = getAgentId(req);
422
+ res.json(runtime.identity.getInfo(agentId) || {});
423
+ });
424
+
425
+ app.post("/api/identity/nickname", async (req, res) => {
426
+ await ensureInitialized();
427
+ const { agent_id, nickname } = req.body;
428
+ const agentId = agent_id || getAgentId(req);
429
+ runtime.identity.updateNickname(agentId, nickname);
430
+ res.json({ success: true });
431
+ });
432
+
433
+ app.post("/api/identity/friend-code", async (req, res) => {
434
+ await ensureInitialized();
435
+ try {
436
+ const agentId = req.body.agent_id || getAgentId(req);
437
+ await runtime.serverClient.ensureAuth(agentId);
438
+ const result = await runtime.handshake.generateFriendCode(agentId);
384
439
  res.json({
440
+ success: true,
441
+ code: result.number,
442
+ expires_at: result.expiresAt || result.expires_at,
443
+ });
444
+ } catch (e) {
445
+ res.status(500).json({ error: e.message });
446
+ }
447
+ });
448
+
449
+ app.get("/api/identity/qr", async (req, res) => {
450
+ await ensureInitialized();
451
+ try {
452
+ const agentId = getAgentId(req);
453
+ const info = runtime.identity.getInfo(agentId);
454
+ if (!info) return res.status(404).json({ error: "Agent not found" });
455
+ const qrData = JSON.stringify({
456
+ type: "aicq-friend",
385
457
  agent_id: info.agent_id,
386
- nickname: info.nickname,
387
- signing_public_key: info.signing_public_key,
458
+ public_key: info.signing_public_key,
388
459
  exchange_public_key: info.exchange_public_key,
389
- signing_secret_key: info.signing_secret_key,
390
- exchange_secret_key: info.exchange_secret_key,
391
460
  fingerprint: info.fingerprint,
392
461
  });
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({
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", async (req, res) => {
470
+ await ensureInitialized();
471
+ try {
472
+ const agentId = req.body.agent_id || getAgentId(req);
473
+ runtime.identity.rotateKeys(agentId);
474
+ res.json({ success: true, info: runtime.identity.getInfo(agentId) });
475
+ } catch (e) {
476
+ res.status(500).json({ error: e.message });
477
+ }
478
+ });
479
+
480
+ app.get("/api/identity/keys", async (req, res) => {
481
+ await ensureInitialized();
482
+ const agentId = getAgentId(req);
483
+ const info = runtime.identity.loadAgent(agentId);
484
+ if (!info) return res.status(404).json({ error: "Agent not found" });
485
+ res.json({
486
+ agent_id: info.agent_id,
487
+ nickname: info.nickname,
488
+ signing_public_key: info.signing_public_key,
489
+ exchange_public_key: info.exchange_public_key,
490
+ signing_secret_key: info.signing_secret_key,
491
+ exchange_secret_key: info.exchange_secret_key,
492
+ fingerprint: info.fingerprint,
493
+ });
494
+ });
495
+
496
+ // Sync endpoint
497
+ app.post("/api/sync", async (req, res) => {
498
+ await ensureInitialized();
499
+ try {
500
+ const agentId = req.body.agent_id || getAgentId(req);
501
+ await runtime.serverClient.ensureAuth(agentId);
502
+ // Sync friends
503
+ const friendResult = await runtime.serverClient.listFriends();
504
+ if (friendResult.friends) {
505
+ for (const f of friendResult.friends) {
506
+ const existing = runtime.db.getFriend(agentId, f.id);
507
+ if (!existing) {
508
+ runtime.db.addFriend({
424
509
  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 || '',
510
+ id: f.id,
511
+ public_key: f.public_key || f.publicKey || "",
512
+ fingerprint: f.fingerprint || "",
513
+ friend_type: f.type || f.friend_type || "ai",
514
+ ai_name: f.agent_name || f.ai_name || f.displayName || "",
430
515
  });
516
+ } else {
517
+ runtime.db.updateFriendOnline(
518
+ agentId,
519
+ f.id,
520
+ f.is_online || f.isOnline || false
521
+ );
431
522
  }
432
523
  }
433
- res.json({ success: true });
434
- } catch (e) {
435
- res.status(500).json({ error: e.message });
436
524
  }
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 });
525
+ // Sync groups
526
+ const groupResult = await runtime.serverClient.listGroups();
527
+ if (groupResult.groups) {
528
+ for (const g of groupResult.groups) {
529
+ runtime.db.addGroup({
530
+ agent_id: agentId,
531
+ id: g.id,
532
+ name: g.name,
533
+ owner_id: g.owner_id || g.ownerId || "",
534
+ members_json: g.members || g.members_json || "[]",
535
+ description: g.description || "",
536
+ });
537
+ }
450
538
  }
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
- };
539
+ res.json({ success: true });
540
+ } catch (e) {
541
+ res.status(500).json({ error: e.message });
542
+ }
543
+ });
544
+
545
+ // Gateway proxy endpoint (for backward compatibility)
546
+ app.post("/api/gateway", async (req, res) => {
547
+ await ensureInitialized();
548
+ try {
549
+ // This endpoint provides backward compatibility for SPA calls
550
+ // that use the gateway RPC protocol. New code should prefer
551
+ // the REST API endpoints above.
552
+ const { method, kwargs } = req.body;
553
+ if (!method) return res.status(400).json({ error: "method is required" });
554
+
555
+ // Delegate to the gateway method handler in index.js
556
+ // (imported via the ensureInitialized runtime store)
557
+ const result = await runtime.handleGateway(method, kwargs || {});
558
+ res.json(result);
559
+ } catch (e) {
560
+ res.status(500).json({ error: e.message });
561
+ }
562
+ });
563
+
564
+ return app;
467
565
  }
468
566
 
469
- module.exports = { createUiRoutes };
567
+ /**
568
+ * Register all HTTP routes on the OpenClaw plugin API.
569
+ *
570
+ * Creates an Express sub-app with all AICQ routes and registers it
571
+ * as a handler for the /plugins/aicq-chat path prefix.
572
+ *
573
+ * @param {object} api - OpenClawPluginApi instance
574
+ * @param {object} ctx - Plugin context { ensureInitialized, runtime, DATA_DIR, SERVER_URL }
575
+ */
576
+ export function registerHttpRoutes(api, ctx) {
577
+ const app = createAicqExpressApp(ctx);
578
+
579
+ // Register the Express sub-app for all /plugins/aicq-chat requests.
580
+ // The gateway uses app.use(path, handler), so the mount prefix is
581
+ // stripped before the request reaches our sub-app.
582
+ if (typeof api.registerHttpRoute === "function") {
583
+ api.registerHttpRoute({
584
+ path: "/plugins/aicq-chat",
585
+ auth: "plugin",
586
+ handler: app,
587
+ });
588
+ } else {
589
+ console.warn(
590
+ "[AICQ Channel] api.registerHttpRoute not available — HTTP UI routes not registered. " +
591
+ "The SPA UI will not be accessible until the gateway supports plugin HTTP routes."
592
+ );
593
+ }
594
+ }