aicq-chat-plugin 3.8.1 → 3.9.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/README.md +80 -80
- package/SKILL.md +78 -78
- package/cli.cjs +356 -356
- package/index.js +417 -375
- package/lib/chat.js +854 -749
- package/lib/crypto.js +168 -168
- package/lib/database.js +455 -455
- package/lib/file-transfer.js +266 -266
- package/lib/handshake.js +147 -147
- package/lib/identity.js +165 -165
- package/lib/package.json +3 -3
- package/lib/server-client.js +380 -337
- package/openclaw.plugin.json +170 -168
- package/package.json +87 -87
- package/postinstall.cjs +27 -27
- package/public/favicon.ico +0 -0
- package/public/icon-16.png +0 -0
- package/public/icon-32.png +0 -0
- package/public/index.html +1468 -1468
- package/public/logo-512.png +0 -0
- package/setup-entry.js +14 -14
- package/src/channel.js +616 -613
- package/src/ui-routes.js +647 -594
package/src/ui-routes.js
CHANGED
|
@@ -1,594 +1,647 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AICQ Channel Plugin — Gateway HTTP Routes
|
|
3
|
-
*
|
|
4
|
-
* Provides HTTP route handlers for the OpenClaw Gateway.
|
|
5
|
-
* These routes serve the SPA UI and REST API endpoints.
|
|
6
|
-
*
|
|
7
|
-
* 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.
|
|
15
|
-
*/
|
|
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));
|
|
28
|
-
|
|
29
|
-
/**
|
|
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
|
|
38
|
-
*/
|
|
39
|
-
function createAicqExpressApp(ctx) {
|
|
40
|
-
const { ensureInitialized, runtime, DATA_DIR, SERVER_URL } = ctx;
|
|
41
|
-
const UPLOADS_DIR = path.join(DATA_DIR, "uploads");
|
|
42
|
-
|
|
43
|
-
// Ensure uploads directory exists
|
|
44
|
-
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
45
|
-
|
|
46
|
-
const app = express();
|
|
47
|
-
|
|
48
|
-
// Parse JSON bodies
|
|
49
|
-
app.use(express.json());
|
|
50
|
-
|
|
51
|
-
// Parse URL-encoded bodies
|
|
52
|
-
app.use(express.urlencoded({ extended: true }));
|
|
53
|
-
|
|
54
|
-
const upload = multer({
|
|
55
|
-
storage: multer.memoryStorage(),
|
|
56
|
-
limits: { fileSize: 50 * 1024 * 1024 },
|
|
57
|
-
});
|
|
58
|
-
|
|
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
|
-
}
|
|
69
|
-
|
|
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);
|
|
111
|
-
try {
|
|
112
|
-
await runtime.serverClient.start(agent_id);
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.error("[AICQ] Server registration failed:", e.message);
|
|
115
|
-
}
|
|
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 {
|
|
153
|
-
const agentId = getAgentId(req);
|
|
154
|
-
runtime.db.removeFriend(agentId, req.params.id);
|
|
155
|
-
try {
|
|
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 {
|
|
167
|
-
const agentId = getAgentId(req);
|
|
168
|
-
let serverRequests = [];
|
|
169
|
-
try {
|
|
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 {
|
|
185
|
-
const agentId = getAgentId(req);
|
|
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 {
|
|
196
|
-
const agentId = getAgentId(req);
|
|
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 || "",
|
|
227
|
-
});
|
|
228
|
-
}
|
|
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 {
|
|
238
|
-
const agentId = getAgentId(req);
|
|
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;
|
|
253
|
-
try {
|
|
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 });
|
|
262
|
-
}
|
|
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;
|
|
308
|
-
const agentId = agent_id || getAgentId(req);
|
|
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
|
-
});
|
|
355
|
-
}
|
|
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" });
|
|
399
|
-
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 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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
app.
|
|
426
|
-
await ensureInitialized();
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
res.
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
res.
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Channel Plugin — Gateway HTTP Routes
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP route handlers for the OpenClaw Gateway.
|
|
5
|
+
* These routes serve the SPA UI and REST API endpoints.
|
|
6
|
+
*
|
|
7
|
+
* 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.
|
|
15
|
+
*/
|
|
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));
|
|
28
|
+
|
|
29
|
+
/**
|
|
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
|
|
38
|
+
*/
|
|
39
|
+
function createAicqExpressApp(ctx) {
|
|
40
|
+
const { ensureInitialized, runtime, DATA_DIR, SERVER_URL } = ctx;
|
|
41
|
+
const UPLOADS_DIR = path.join(DATA_DIR, "uploads");
|
|
42
|
+
|
|
43
|
+
// Ensure uploads directory exists
|
|
44
|
+
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
45
|
+
|
|
46
|
+
const app = express();
|
|
47
|
+
|
|
48
|
+
// Parse JSON bodies
|
|
49
|
+
app.use(express.json());
|
|
50
|
+
|
|
51
|
+
// Parse URL-encoded bodies
|
|
52
|
+
app.use(express.urlencoded({ extended: true }));
|
|
53
|
+
|
|
54
|
+
const upload = multer({
|
|
55
|
+
storage: multer.memoryStorage(),
|
|
56
|
+
limits: { fileSize: 50 * 1024 * 1024 },
|
|
57
|
+
});
|
|
58
|
+
|
|
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
|
+
}
|
|
69
|
+
|
|
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);
|
|
111
|
+
try {
|
|
112
|
+
await runtime.serverClient.start(agent_id);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error("[AICQ] Server registration failed:", e.message);
|
|
115
|
+
}
|
|
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 {
|
|
153
|
+
const agentId = getAgentId(req);
|
|
154
|
+
runtime.db.removeFriend(agentId, req.params.id);
|
|
155
|
+
try {
|
|
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 {
|
|
167
|
+
const agentId = getAgentId(req);
|
|
168
|
+
let serverRequests = [];
|
|
169
|
+
try {
|
|
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 {
|
|
185
|
+
const agentId = getAgentId(req);
|
|
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 {
|
|
196
|
+
const agentId = getAgentId(req);
|
|
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 || "",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
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 {
|
|
238
|
+
const agentId = getAgentId(req);
|
|
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;
|
|
253
|
+
try {
|
|
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 });
|
|
262
|
+
}
|
|
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;
|
|
308
|
+
const agentId = agent_id || getAgentId(req);
|
|
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
|
+
});
|
|
355
|
+
}
|
|
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" });
|
|
399
|
+
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 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
|
+
// User file upload — saves to userfiles dir and notifies AI agent
|
|
410
|
+
app.post("/api/user-upload", upload.single("file"), async (req, res) => {
|
|
411
|
+
await ensureInitialized();
|
|
412
|
+
try {
|
|
413
|
+
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
|
|
414
|
+
const agentId = getAgentId(req);
|
|
415
|
+
const fromId = req.body.fromId || req.body.from_id || agentId;
|
|
416
|
+
const isGroup = req.body.isGroup === "true" || req.body.isGroup === "1";
|
|
417
|
+
const result = await runtime.chat.handleUserFileUpload(agentId, fromId, req.file, isGroup);
|
|
418
|
+
res.json({ success: true, message: result.msg, localPath: result.localPath, originalName: result.originalName });
|
|
419
|
+
} catch (e) {
|
|
420
|
+
res.status(500).json({ error: e.message });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// List user files
|
|
425
|
+
app.get("/api/userfiles", async (req, res) => {
|
|
426
|
+
await ensureInitialized();
|
|
427
|
+
try {
|
|
428
|
+
const userfilesDir = runtime.userfilesDir;
|
|
429
|
+
if (!userfilesDir || !fs.existsSync(userfilesDir)) {
|
|
430
|
+
return res.json({ files: [] });
|
|
431
|
+
}
|
|
432
|
+
const files = fs.readdirSync(userfilesDir)
|
|
433
|
+
.filter(f => fs.statSync(path.join(userfilesDir, f)).isFile())
|
|
434
|
+
.map(f => {
|
|
435
|
+
const filePath = path.join(userfilesDir, f);
|
|
436
|
+
const stat = fs.statSync(filePath);
|
|
437
|
+
return {
|
|
438
|
+
name: f,
|
|
439
|
+
path: filePath,
|
|
440
|
+
size: stat.size,
|
|
441
|
+
modified: stat.mtime.toISOString(),
|
|
442
|
+
};
|
|
443
|
+
})
|
|
444
|
+
.sort((a, b) => b.modified.localeCompare(a.modified));
|
|
445
|
+
res.json({ files });
|
|
446
|
+
} catch (e) {
|
|
447
|
+
res.status(500).json({ error: e.message });
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Serve user files
|
|
452
|
+
app.get("/api/userfiles/:filename", (req, res) => {
|
|
453
|
+
const USERFILES_DIR = runtime.userfilesDir || path.join(DATA_DIR, "userfiles");
|
|
454
|
+
const filePath = path.join(USERFILES_DIR, req.params.filename);
|
|
455
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
456
|
+
res.sendFile(filePath);
|
|
457
|
+
} else {
|
|
458
|
+
res.status(404).json({ error: "File not found" });
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
app.get("/api/files/:fileId", (req, res) => {
|
|
463
|
+
const filePath = path.join(UPLOADS_DIR, req.params.fileId);
|
|
464
|
+
if (fs.existsSync(filePath)) {
|
|
465
|
+
res.sendFile(filePath);
|
|
466
|
+
} else {
|
|
467
|
+
res.status(404).json({ error: "File not found" });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Identity
|
|
472
|
+
app.get("/api/identity", async (req, res) => {
|
|
473
|
+
await ensureInitialized();
|
|
474
|
+
const agentId = getAgentId(req);
|
|
475
|
+
res.json(runtime.identity.getInfo(agentId) || {});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
app.post("/api/identity/nickname", async (req, res) => {
|
|
479
|
+
await ensureInitialized();
|
|
480
|
+
const { agent_id, nickname } = req.body;
|
|
481
|
+
const agentId = agent_id || getAgentId(req);
|
|
482
|
+
runtime.identity.updateNickname(agentId, nickname);
|
|
483
|
+
res.json({ success: true });
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
app.post("/api/identity/friend-code", async (req, res) => {
|
|
487
|
+
await ensureInitialized();
|
|
488
|
+
try {
|
|
489
|
+
const agentId = req.body.agent_id || getAgentId(req);
|
|
490
|
+
await runtime.serverClient.ensureAuth(agentId);
|
|
491
|
+
const result = await runtime.handshake.generateFriendCode(agentId);
|
|
492
|
+
res.json({
|
|
493
|
+
success: true,
|
|
494
|
+
code: result.number,
|
|
495
|
+
expires_at: result.expiresAt || result.expires_at,
|
|
496
|
+
});
|
|
497
|
+
} catch (e) {
|
|
498
|
+
res.status(500).json({ error: e.message });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
app.get("/api/identity/qr", async (req, res) => {
|
|
503
|
+
await ensureInitialized();
|
|
504
|
+
try {
|
|
505
|
+
const agentId = getAgentId(req);
|
|
506
|
+
const info = runtime.identity.getInfo(agentId);
|
|
507
|
+
if (!info) return res.status(404).json({ error: "Agent not found" });
|
|
508
|
+
const qrData = JSON.stringify({
|
|
509
|
+
type: "aicq-friend",
|
|
510
|
+
agent_id: info.agent_id,
|
|
511
|
+
public_key: info.signing_public_key,
|
|
512
|
+
exchange_public_key: info.exchange_public_key,
|
|
513
|
+
fingerprint: info.fingerprint,
|
|
514
|
+
});
|
|
515
|
+
const qrImage = await QRCode.toDataURL(qrData);
|
|
516
|
+
res.json({ qr: qrImage, data: qrData, info });
|
|
517
|
+
} catch (e) {
|
|
518
|
+
res.status(500).json({ error: e.message });
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
app.post("/api/identity/rotate-keys", async (req, res) => {
|
|
523
|
+
await ensureInitialized();
|
|
524
|
+
try {
|
|
525
|
+
const agentId = req.body.agent_id || getAgentId(req);
|
|
526
|
+
runtime.identity.rotateKeys(agentId);
|
|
527
|
+
res.json({ success: true, info: runtime.identity.getInfo(agentId) });
|
|
528
|
+
} catch (e) {
|
|
529
|
+
res.status(500).json({ error: e.message });
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
app.get("/api/identity/keys", async (req, res) => {
|
|
534
|
+
await ensureInitialized();
|
|
535
|
+
const agentId = getAgentId(req);
|
|
536
|
+
const info = runtime.identity.loadAgent(agentId);
|
|
537
|
+
if (!info) return res.status(404).json({ error: "Agent not found" });
|
|
538
|
+
res.json({
|
|
539
|
+
agent_id: info.agent_id,
|
|
540
|
+
nickname: info.nickname,
|
|
541
|
+
signing_public_key: info.signing_public_key,
|
|
542
|
+
exchange_public_key: info.exchange_public_key,
|
|
543
|
+
signing_secret_key: info.signing_secret_key,
|
|
544
|
+
exchange_secret_key: info.exchange_secret_key,
|
|
545
|
+
fingerprint: info.fingerprint,
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Sync endpoint
|
|
550
|
+
app.post("/api/sync", async (req, res) => {
|
|
551
|
+
await ensureInitialized();
|
|
552
|
+
try {
|
|
553
|
+
const agentId = req.body.agent_id || getAgentId(req);
|
|
554
|
+
await runtime.serverClient.ensureAuth(agentId);
|
|
555
|
+
// Sync friends
|
|
556
|
+
const friendResult = await runtime.serverClient.listFriends();
|
|
557
|
+
if (friendResult.friends) {
|
|
558
|
+
for (const f of friendResult.friends) {
|
|
559
|
+
const existing = runtime.db.getFriend(agentId, f.id);
|
|
560
|
+
if (!existing) {
|
|
561
|
+
runtime.db.addFriend({
|
|
562
|
+
agent_id: agentId,
|
|
563
|
+
id: f.id,
|
|
564
|
+
public_key: f.public_key || f.publicKey || "",
|
|
565
|
+
fingerprint: f.fingerprint || "",
|
|
566
|
+
friend_type: f.type || f.friend_type || "ai",
|
|
567
|
+
ai_name: f.agent_name || f.ai_name || f.displayName || "",
|
|
568
|
+
});
|
|
569
|
+
} else {
|
|
570
|
+
runtime.db.updateFriendOnline(
|
|
571
|
+
agentId,
|
|
572
|
+
f.id,
|
|
573
|
+
f.is_online || f.isOnline || false
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Sync groups
|
|
579
|
+
const groupResult = await runtime.serverClient.listGroups();
|
|
580
|
+
if (groupResult.groups) {
|
|
581
|
+
for (const g of groupResult.groups) {
|
|
582
|
+
runtime.db.addGroup({
|
|
583
|
+
agent_id: agentId,
|
|
584
|
+
id: g.id,
|
|
585
|
+
name: g.name,
|
|
586
|
+
owner_id: g.owner_id || g.ownerId || "",
|
|
587
|
+
members_json: g.members || g.members_json || "[]",
|
|
588
|
+
description: g.description || "",
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
res.json({ success: true });
|
|
593
|
+
} catch (e) {
|
|
594
|
+
res.status(500).json({ error: e.message });
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Gateway proxy endpoint (for backward compatibility)
|
|
599
|
+
app.post("/api/gateway", async (req, res) => {
|
|
600
|
+
await ensureInitialized();
|
|
601
|
+
try {
|
|
602
|
+
// This endpoint provides backward compatibility for SPA calls
|
|
603
|
+
// that use the gateway RPC protocol. New code should prefer
|
|
604
|
+
// the REST API endpoints above.
|
|
605
|
+
const { method, kwargs } = req.body;
|
|
606
|
+
if (!method) return res.status(400).json({ error: "method is required" });
|
|
607
|
+
|
|
608
|
+
// Delegate to the gateway method handler in index.js
|
|
609
|
+
// (imported via the ensureInitialized runtime store)
|
|
610
|
+
const result = await runtime.handleGateway(method, kwargs || {});
|
|
611
|
+
res.json(result);
|
|
612
|
+
} catch (e) {
|
|
613
|
+
res.status(500).json({ error: e.message });
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
return app;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Register all HTTP routes on the OpenClaw plugin API.
|
|
622
|
+
*
|
|
623
|
+
* Creates an Express sub-app with all AICQ routes and registers it
|
|
624
|
+
* as a handler for the /plugins/aicq-chat path prefix.
|
|
625
|
+
*
|
|
626
|
+
* @param {object} api - OpenClawPluginApi instance
|
|
627
|
+
* @param {object} ctx - Plugin context { ensureInitialized, runtime, DATA_DIR, SERVER_URL }
|
|
628
|
+
*/
|
|
629
|
+
export function registerHttpRoutes(api, ctx) {
|
|
630
|
+
const app = createAicqExpressApp(ctx);
|
|
631
|
+
|
|
632
|
+
// Register the Express sub-app for all /plugins/aicq-chat requests.
|
|
633
|
+
// The gateway uses app.use(path, handler), so the mount prefix is
|
|
634
|
+
// stripped before the request reaches our sub-app.
|
|
635
|
+
if (typeof api.registerHttpRoute === "function") {
|
|
636
|
+
api.registerHttpRoute({
|
|
637
|
+
path: "/plugins/aicq-chat",
|
|
638
|
+
auth: "plugin",
|
|
639
|
+
handler: app,
|
|
640
|
+
});
|
|
641
|
+
} else {
|
|
642
|
+
console.warn(
|
|
643
|
+
"[AICQ Channel] api.registerHttpRoute not available — HTTP UI routes not registered. " +
|
|
644
|
+
"The SPA UI will not be accessible until the gateway supports plugin HTTP routes."
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|