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/README.md +14 -9
- package/SKILL.md +17 -13
- package/{cli.js → cli.cjs} +19 -34
- package/index.js +191 -257
- package/lib/package.json +3 -0
- package/openclaw.plugin.json +2 -3
- package/package.json +16 -6
- package/{postinstall.js → postinstall.cjs} +5 -4
- package/setup-entry.js +8 -55
- package/src/channel.js +218 -136
- package/src/ui-routes.js +545 -420
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
16
|
-
*
|
|
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
|
|
19
|
-
const {
|
|
20
|
-
const UPLOADS_DIR = path.join(
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
// Parse URL-encoded bodies
|
|
52
|
+
app.use(express.urlencoded({ extended: true }));
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
console.error("[AICQ] Server registration failed:", e.message);
|
|
81
115
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
await serverClient.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
} catch (e) {
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
res.json({
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
return res.
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
+
}
|