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/index.js CHANGED
@@ -1,375 +1,417 @@
1
- /**
2
- * AICQ Chat Plugin — Channel Plugin Entry Point
3
- *
4
- * Architecture: Channel (in-process, no independent port)
5
- * - Runs inside the OpenClaw process
6
- * - Uses defineChannelPluginEntry from the official Channel Plugin SDK
7
- * - Provides Gateway RPC methods for the SPA UI and agent tools
8
- * - No sidecar process needed
9
- *
10
- * ESM module — this file IS the openclaw extension entry.
11
- */
12
-
13
- import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
14
- import { aicqChatPlugin, runtime } from "./src/channel.js";
15
- import { createRequire } from "module";
16
- import path from "path";
17
- import os from "os";
18
- import fs from "fs";
19
-
20
- // ── CJS interop — lib/ modules are CommonJS ──────────────────────────
21
- const require = createRequire(import.meta.url);
22
-
23
- // ── Configuration ────────────────────────────────────────────────────
24
- const DATA_DIR = process.env.AICQ_DATA_DIR || path.join(os.homedir(), ".aicq-plugin");
25
- const SERVER_URL = process.env.AICQ_SERVER_URL || "https://aicq.online";
26
-
27
- fs.mkdirSync(DATA_DIR, { recursive: true });
28
-
29
- // ── Lazy-loaded CJS modules (need async db init) ────────────────────
30
- let _db = null;
31
- let _identity = null;
32
- let _serverClient = null;
33
- let _handshake = null;
34
- let _chat = null;
35
-
36
- /**
37
- * Initialize all plugin components (async, called once from registerFull).
38
- */
39
- async function ensureInitialized() {
40
- if (runtime._initialized) return;
41
-
42
- const PluginDatabase = require("./lib/database");
43
- const IdentityManager = require("./lib/identity");
44
- const ServerClient = require("./lib/server-client");
45
- const HandshakeManager = require("./lib/handshake");
46
- const ChatManager = require("./lib/chat");
47
-
48
- // Initialize database
49
- _db = new PluginDatabase(DATA_DIR);
50
- await _db.init();
51
- console.log("[AICQ Channel] Database initialized");
52
-
53
- // Initialize managers
54
- _identity = new IdentityManager(_db);
55
- _serverClient = new ServerClient(_identity, _db, SERVER_URL);
56
- _handshake = new HandshakeManager(_identity, _serverClient, _db);
57
- _chat = new ChatManager(_identity, _serverClient, _db, path.join(DATA_DIR, "uploads"));
58
-
59
- // Populate the shared runtime store so channel adapters can use it
60
- runtime.db = _db;
61
- runtime.identity = _identity;
62
- runtime.serverClient = _serverClient;
63
- runtime.handshake = _handshake;
64
- runtime.chat = _chat;
65
- runtime.dataDir = DATA_DIR;
66
- runtime.serverUrl = SERVER_URL;
67
- runtime.handleGateway = handleGatewayMethod;
68
- runtime.ensureInitialized = ensureInitialized;
69
-
70
- // Periodic cleanup
71
- setInterval(() => _db.cleanup(), 3600000);
72
-
73
- runtime._initialized = true;
74
- console.log("[AICQ Channel] Plugin runtime initialized");
75
- }
76
-
77
- // ── Sync helpers ─────────────────────────────────────────────────────
78
- async function syncFriendsFromServer(agentId) {
79
- try {
80
- await _serverClient.ensureAuth(agentId);
81
- const result = await _serverClient.listFriends();
82
- if (result.friends) {
83
- for (const f of result.friends) {
84
- const existing = _db.getFriend(agentId, f.id);
85
- if (!existing) {
86
- _db.addFriend({
87
- agent_id: agentId,
88
- id: f.id,
89
- public_key: f.public_key || f.publicKey || "",
90
- fingerprint: f.fingerprint || "",
91
- friend_type: f.type || f.friend_type || "ai",
92
- ai_name: f.agent_name || f.ai_name || f.displayName || "",
93
- });
94
- } else {
95
- _db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
96
- }
97
- }
98
- }
99
- } catch (e) {
100
- console.error("[AICQ Channel] Sync friends failed:", e.message);
101
- }
102
- }
103
-
104
- async function syncGroupsFromServer(agentId) {
105
- try {
106
- await _serverClient.ensureAuth(agentId);
107
- const result = await _serverClient.listGroups();
108
- if (result.groups) {
109
- for (const g of result.groups) {
110
- _db.addGroup({
111
- agent_id: agentId,
112
- id: g.id,
113
- name: g.name,
114
- owner_id: g.owner_id || g.ownerId || "",
115
- members_json: g.members || g.members_json || "[]",
116
- description: g.description || "",
117
- });
118
- }
119
- }
120
- } catch (e) {
121
- console.error("[AICQ Channel] Sync groups failed:", e.message);
122
- }
123
- }
124
-
125
- // ── Gateway method handler ───────────────────────────────────────────
126
- async function handleGatewayMethod(method, kwargs = {}) {
127
- const agents = _identity.listAgents();
128
- const currentAgentId = agents.length > 0 ? agents[0].agent_id : null;
129
-
130
- switch (method) {
131
- case "aicq.status":
132
- return {
133
- state: _serverClient.connected ? "connected" : "disconnected",
134
- agent_id: currentAgentId,
135
- version: "3.8.0",
136
- architecture: "channel",
137
- };
138
- case "aicq.friends.list":
139
- return { friends: _db.listFriends(currentAgentId) };
140
- case "aicq.friends.add":
141
- return await _handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
142
- case "aicq.friends.remove":
143
- _db.removeFriend(currentAgentId, kwargs.friend_id);
144
- return { success: true };
145
- case "aicq.friends.requests":
146
- return { requests: _db.getPendingRequests(currentAgentId) };
147
- case "aicq.friends.acceptRequest":
148
- return await _handshake.acceptRequest(currentAgentId, kwargs.request_id);
149
- case "aicq.friends.rejectRequest":
150
- return await _handshake.rejectRequest(currentAgentId, kwargs.request_id);
151
- case "aicq.identity.info":
152
- return _identity.getInfo(currentAgentId) || {};
153
- case "aicq.agent.create":
154
- _identity.createAgent(kwargs.agent_id, kwargs.nickname);
155
- return { success: true };
156
- case "aicq.agent.delete":
157
- _identity.deleteAgent(kwargs.agent_id);
158
- return { success: true };
159
- case "aicq.chat.send":
160
- return await _chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, {
161
- isGroup: kwargs.isGroup,
162
- });
163
- case "aicq.chat.history":
164
- return {
165
- messages: _db.getChatHistory(currentAgentId, kwargs.targetId, {
166
- limit: kwargs.limit || 50,
167
- }),
168
- };
169
- case "aicq.chat.delete":
170
- _db.deleteMessage(currentAgentId, kwargs.message_id);
171
- return { success: true };
172
- case "aicq.chat.streamChunk": {
173
- if (!kwargs.friend_id && !kwargs.targetId)
174
- return { error: "friend_id or targetId is required" };
175
- if (!kwargs.data) return { error: "data is required" };
176
- const chunkType = kwargs.chunk_type || kwargs.chunkType || "text";
177
- const ALLOWED_CHUNK_TYPES = [
178
- "text",
179
- "reasoning",
180
- "thinking",
181
- "clear_text",
182
- "tool_call",
183
- "tool_result",
184
- ];
185
- if (!ALLOWED_CHUNK_TYPES.includes(chunkType))
186
- return {
187
- error: `Invalid chunk_type: ${chunkType}. Allowed: ${ALLOWED_CHUNK_TYPES.join(", ")}`,
188
- };
189
- const streamTarget = kwargs.friend_id || kwargs.targetId;
190
- const sent = _serverClient.sendWS({
191
- type: "stream_chunk",
192
- to: streamTarget,
193
- chunkType,
194
- data: kwargs.data,
195
- });
196
- if (!sent) return { error: "Not connected to server", success: false };
197
- return { success: true };
198
- }
199
- case "aicq.chat.streamEnd": {
200
- if (!kwargs.friend_id && !kwargs.targetId)
201
- return { error: "friend_id or targetId is required" };
202
- const endTarget = kwargs.friend_id || kwargs.targetId;
203
- const msgId =
204
- kwargs.message_id ||
205
- kwargs.messageId ||
206
- "msg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6);
207
- const endSent = _serverClient.sendWS({
208
- type: "stream_end",
209
- to: endTarget,
210
- messageId: msgId,
211
- });
212
- if (!endSent) return { error: "Not connected to server", success: false };
213
- return { success: true, messageId: msgId };
214
- }
215
- case "aicq.groups.list":
216
- return { groups: _db.listGroups(currentAgentId) };
217
- case "aicq.groups.create": {
218
- await _serverClient.ensureAuth(currentAgentId);
219
- const result = await _serverClient.createGroup(kwargs.name, kwargs.description);
220
- if (result.id) {
221
- _db.addGroup({
222
- agent_id: currentAgentId,
223
- id: result.id,
224
- name: kwargs.name,
225
- owner_id: currentAgentId,
226
- members_json: result.members || "[]",
227
- description: kwargs.description || "",
228
- });
229
- }
230
- return { success: true, group: result };
231
- }
232
- case "aicq.groups.join":
233
- await _serverClient.ensureAuth(currentAgentId);
234
- return await _serverClient.inviteGroupMember(kwargs.group_id, currentAgentId);
235
- case "aicq.groups.messages": {
236
- await _serverClient.ensureAuth(currentAgentId);
237
- return await _serverClient.getGroupMessages(kwargs.group_id, kwargs.limit || 50);
238
- }
239
- case "aicq.groups.silent":
240
- _db.setGroupSilentMode(currentAgentId, kwargs.group_id, !!kwargs.silent);
241
- return { success: true, silent: !!kwargs.silent };
242
- case "aicq.chat.sendFile": {
243
- if (!kwargs.targetId) return { error: "targetId is required" };
244
- if (!kwargs.filePath && !kwargs.file_path) return { error: "filePath is required" };
245
- const sendFilePath = kwargs.filePath || kwargs.file_path;
246
- const sendResult = await _chat.sendFile(currentAgentId, kwargs.targetId, sendFilePath, {
247
- isGroup: !!kwargs.isGroup,
248
- caption: kwargs.caption || "",
249
- });
250
- return { success: true, file: sendResult };
251
- }
252
- case "aicq.chat.sendImage": {
253
- if (!kwargs.targetId) return { error: "targetId is required" };
254
- if (!kwargs.filePath && !kwargs.file_path && !kwargs.base64) return { error: "filePath or base64 is required" };
255
- if (kwargs.base64) {
256
- const imgResult = await _chat.sendFileFromBase64(
257
- currentAgentId, kwargs.targetId, kwargs.base64,
258
- kwargs.fileName || kwargs.file_name || "image.png",
259
- { isGroup: !!kwargs.isGroup, caption: kwargs.caption || "", mimeType: kwargs.mimeType || "" }
260
- );
261
- return { success: true, file: imgResult };
262
- }
263
- const imgFilePath = kwargs.filePath || kwargs.file_path;
264
- const imgResult = await _chat.sendFile(currentAgentId, kwargs.targetId, imgFilePath, {
265
- isGroup: !!kwargs.isGroup,
266
- caption: kwargs.caption || "",
267
- });
268
- return { success: true, file: imgResult };
269
- }
270
- case "aicq.chat.sendFileFromBase64": {
271
- if (!kwargs.targetId) return { error: "targetId is required" };
272
- if (!kwargs.base64) return { error: "base64 is required" };
273
- if (!kwargs.fileName && !kwargs.file_name) return { error: "fileName is required" };
274
- const b64Result = await _chat.sendFileFromBase64(
275
- currentAgentId, kwargs.targetId, kwargs.base64,
276
- kwargs.fileName || kwargs.file_name,
277
- { isGroup: !!kwargs.isGroup, caption: kwargs.caption || "", mimeType: kwargs.mimeType || "" }
278
- );
279
- return { success: true, file: b64Result };
280
- }
281
- case "aicq.sessions.list":
282
- return { sessions: [] };
283
- default:
284
- return { error: `Unknown method: ${method}` };
285
- }
286
- }
287
-
288
- // ── CLI metadata registration (lightweight, no runtime init) ─────────
289
- function registerCliMetadata(api) {
290
- api.registerCli(
291
- ({ program }) => {
292
- program
293
- .command("aicq-chat")
294
- .description("AICQ Encrypted Chat management");
295
- },
296
- {
297
- descriptors: [
298
- {
299
- name: "aicq-chat",
300
- description: "AICQ Encrypted Chat management",
301
- hasSubcommands: false,
302
- },
303
- ],
304
- }
305
- );
306
- }
307
-
308
- // ── Full runtime registration ────────────────────────────────────────
309
- async function registerFull(api) {
310
- // Expose ensureInitialized on the runtime store immediately so that
311
- // startAccount (called by the channel loader) can trigger init even
312
- // if no gateway method has been invoked yet.
313
- runtime.ensureInitialized = ensureInitialized;
314
-
315
- // Register gateway RPC methods — each wraps handleGatewayMethod
316
- const GATEWAY_METHODS = [
317
- "aicq.status",
318
- "aicq.friends.list",
319
- "aicq.friends.add",
320
- "aicq.friends.remove",
321
- "aicq.friends.requests",
322
- "aicq.friends.acceptRequest",
323
- "aicq.friends.rejectRequest",
324
- "aicq.identity.info",
325
- "aicq.agent.create",
326
- "aicq.agent.delete",
327
- "aicq.chat.send",
328
- "aicq.chat.history",
329
- "aicq.chat.delete",
330
- "aicq.chat.streamChunk",
331
- "aicq.chat.streamEnd",
332
- "aicq.chat.sendFile",
333
- "aicq.chat.sendImage",
334
- "aicq.chat.sendFileFromBase64",
335
- "aicq.groups.list",
336
- "aicq.groups.create",
337
- "aicq.groups.join",
338
- "aicq.groups.messages",
339
- "aicq.groups.silent",
340
- "aicq.sessions.list",
341
- ];
342
-
343
- for (const method of GATEWAY_METHODS) {
344
- api.registerGatewayMethod(method, async (opts) => {
345
- try {
346
- await ensureInitialized();
347
- const result = await handleGatewayMethod(method, opts.params || {});
348
- opts.respond(true, result);
349
- } catch (e) {
350
- opts.respond(false, undefined, { message: e.message, code: "AICQ_ERROR" });
351
- }
352
- });
353
- }
354
-
355
- // Register HTTP routes for the SPA UI and REST API.
356
- // Lazy-loaded to keep the entry narrow — the ui-routes module pulls in
357
- // qrcode and multer which are not needed during setup-only registration.
358
- try {
359
- const { registerHttpRoutes } = await import("./src/ui-routes.js");
360
- registerHttpRoutes(api, { ensureInitialized, runtime, DATA_DIR, SERVER_URL });
361
- } catch (e) {
362
- console.error("[AICQ Channel] Failed to register HTTP routes:", e.message);
363
- }
364
- }
365
-
366
- // ── Export the entry point ───────────────────────────────────────────
367
- export default defineChannelPluginEntry({
368
- id: "aicq-chat",
369
- name: "AICQ Encrypted Chat",
370
- description:
371
- "End-to-end encrypted chat channel plugin for OpenClaw agents — NaCl (X25519 + XSalsa20-Poly1305)",
372
- plugin: aicqChatPlugin,
373
- registerCliMetadata,
374
- registerFull,
375
- });
1
+ /**
2
+ * AICQ Chat Plugin — Channel Plugin Entry Point
3
+ *
4
+ * Architecture: Channel (in-process, no independent port)
5
+ * - Runs inside the OpenClaw process
6
+ * - Uses defineChannelPluginEntry from the official Channel Plugin SDK
7
+ * - Provides Gateway RPC methods for the SPA UI and agent tools
8
+ * - No sidecar process needed
9
+ *
10
+ * ESM module — this file IS the openclaw extension entry.
11
+ */
12
+
13
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
14
+ import { aicqChatPlugin, runtime } from "./src/channel.js";
15
+ import { createRequire } from "module";
16
+ import path from "path";
17
+ import os from "os";
18
+ import fs from "fs";
19
+
20
+ // ── CJS interop — lib/ modules are CommonJS ──────────────────────────
21
+ const require = createRequire(import.meta.url);
22
+
23
+ // ── Configuration ────────────────────────────────────────────────────
24
+ const DATA_DIR = process.env.AICQ_DATA_DIR || path.join(os.homedir(), ".aicq-plugin");
25
+ const SERVER_URL = process.env.AICQ_SERVER_URL || "https://aicq.online";
26
+ const AUTO_ADD_FRIENDS = process.env.AICQ_AUTO_ADD_FRIENDS
27
+ ? process.env.AICQ_AUTO_ADD_FRIENDS.split(",").map(s => s.trim()).filter(Boolean)
28
+ : ["1000000"]; // Default: auto-add user 1000000
29
+ const AUTO_ACCEPT_FRIENDS = process.env.AICQ_AUTO_ACCEPT_FRIENDS !== "false"; // default true
30
+
31
+ fs.mkdirSync(DATA_DIR, { recursive: true });
32
+
33
+ // ── Lazy-loaded CJS modules (need async db init) ────────────────────
34
+ let _db = null;
35
+ let _identity = null;
36
+ let _serverClient = null;
37
+ let _handshake = null;
38
+ let _chat = null;
39
+
40
+ /**
41
+ * Initialize all plugin components (async, called once from registerFull).
42
+ */
43
+ async function ensureInitialized() {
44
+ if (runtime._initialized) return;
45
+
46
+ const PluginDatabase = require("./lib/database");
47
+ const IdentityManager = require("./lib/identity");
48
+ const ServerClient = require("./lib/server-client");
49
+ const HandshakeManager = require("./lib/handshake");
50
+ const ChatManager = require("./lib/chat");
51
+
52
+ // Initialize database
53
+ _db = new PluginDatabase(DATA_DIR);
54
+ await _db.init();
55
+ console.log("[AICQ Channel] Database initialized");
56
+
57
+ // Initialize managers
58
+ _identity = new IdentityManager(_db);
59
+ _serverClient = new ServerClient(_identity, _db, SERVER_URL);
60
+ _handshake = new HandshakeManager(_identity, _serverClient, _db);
61
+ const uploadsDir = path.join(DATA_DIR, "uploads");
62
+ const userfilesDir = path.join(DATA_DIR, "userfiles");
63
+ fs.mkdirSync(uploadsDir, { recursive: true });
64
+ fs.mkdirSync(userfilesDir, { recursive: true });
65
+
66
+ _chat = new ChatManager(_identity, _serverClient, _db, uploadsDir, userfilesDir);
67
+
68
+ // Populate the shared runtime store so channel adapters can use it
69
+ runtime.db = _db;
70
+ runtime.identity = _identity;
71
+ runtime.serverClient = _serverClient;
72
+ runtime.handshake = _handshake;
73
+ runtime.chat = _chat;
74
+ runtime.dataDir = DATA_DIR;
75
+ runtime.userfilesDir = userfilesDir;
76
+ runtime.uploadsDir = uploadsDir;
77
+ runtime.serverUrl = SERVER_URL;
78
+ runtime.handleGateway = handleGatewayMethod;
79
+ runtime.ensureInitialized = ensureInitialized;
80
+ runtime.autoAddFriends = AUTO_ADD_FRIENDS;
81
+ runtime.autoAcceptFriends = AUTO_ACCEPT_FRIENDS;
82
+
83
+ // Periodic cleanup
84
+ setInterval(() => _db.cleanup(), 3600000);
85
+
86
+ runtime._initialized = true;
87
+ console.log("[AICQ Channel] Plugin runtime initialized");
88
+ }
89
+
90
+ // ── Sync helpers ─────────────────────────────────────────────────────
91
+ async function syncFriendsFromServer(agentId) {
92
+ try {
93
+ await _serverClient.ensureAuth(agentId);
94
+ const result = await _serverClient.listFriends();
95
+ if (result.friends) {
96
+ for (const f of result.friends) {
97
+ const existing = _db.getFriend(agentId, f.id);
98
+ if (!existing) {
99
+ _db.addFriend({
100
+ agent_id: agentId,
101
+ id: f.id,
102
+ public_key: f.public_key || f.publicKey || "",
103
+ fingerprint: f.fingerprint || "",
104
+ friend_type: f.type || f.friend_type || "ai",
105
+ ai_name: f.agent_name || f.ai_name || f.displayName || "",
106
+ });
107
+ } else {
108
+ _db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
109
+ }
110
+ }
111
+ }
112
+ } catch (e) {
113
+ console.error("[AICQ Channel] Sync friends failed:", e.message);
114
+ }
115
+ }
116
+
117
+ async function syncGroupsFromServer(agentId) {
118
+ try {
119
+ await _serverClient.ensureAuth(agentId);
120
+ const result = await _serverClient.listGroups();
121
+ if (result.groups) {
122
+ for (const g of result.groups) {
123
+ _db.addGroup({
124
+ agent_id: agentId,
125
+ id: g.id,
126
+ name: g.name,
127
+ owner_id: g.owner_id || g.ownerId || "",
128
+ members_json: g.members || g.members_json || "[]",
129
+ description: g.description || "",
130
+ });
131
+ }
132
+ }
133
+ } catch (e) {
134
+ console.error("[AICQ Channel] Sync groups failed:", e.message);
135
+ }
136
+ }
137
+
138
+ // ── Gateway method handler ───────────────────────────────────────────
139
+ async function handleGatewayMethod(method, kwargs = {}) {
140
+ const agents = _identity.listAgents();
141
+ const currentAgentId = agents.length > 0 ? agents[0].agent_id : null;
142
+
143
+ switch (method) {
144
+ case "aicq.status":
145
+ return {
146
+ state: _serverClient.connected ? "connected" : "disconnected",
147
+ agent_id: currentAgentId,
148
+ version: "3.6.0",
149
+ architecture: "channel",
150
+ };
151
+ case "aicq.friends.list":
152
+ return { friends: _db.listFriends(currentAgentId) };
153
+ case "aicq.friends.add":
154
+ return await _handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
155
+ case "aicq.friends.addByNumber": {
156
+ // Add friend by AICQ number directly (e.g., "1000000")
157
+ if (!kwargs.number && !kwargs.aicq_number)
158
+ return { error: "number or aicq_number is required" };
159
+ try {
160
+ await _serverClient.ensureAuth(currentAgentId);
161
+ const aicqNumber = kwargs.number || kwargs.aicq_number;
162
+ const result = await _serverClient.sendFriendRequest(aicqNumber, kwargs.message || 'Hi, I\'d like to add you as a friend!');
163
+ // If the request was accepted immediately, also add locally
164
+ if (result.status === 'accepted' && result.to_id) {
165
+ _db.addFriend({
166
+ agent_id: currentAgentId,
167
+ id: result.to_id,
168
+ public_key: '',
169
+ fingerprint: '',
170
+ friend_type: 'human',
171
+ ai_name: kwargs.nickname || '',
172
+ });
173
+ }
174
+ return { success: true, request_id: result.id, status: result.status, to_id: result.to_id };
175
+ } catch (e) {
176
+ return { error: e.message };
177
+ }
178
+ }
179
+ case "aicq.friends.remove":
180
+ _db.removeFriend(currentAgentId, kwargs.friend_id);
181
+ return { success: true };
182
+ case "aicq.friends.requests":
183
+ return { requests: _db.getPendingRequests(currentAgentId) };
184
+ case "aicq.friends.acceptRequest":
185
+ return await _handshake.acceptRequest(currentAgentId, kwargs.request_id);
186
+ case "aicq.friends.rejectRequest":
187
+ return await _handshake.rejectRequest(currentAgentId, kwargs.request_id);
188
+ case "aicq.identity.info":
189
+ return _identity.getInfo(currentAgentId) || {};
190
+ case "aicq.agent.create":
191
+ _identity.createAgent(kwargs.agent_id, kwargs.nickname);
192
+ return { success: true };
193
+ case "aicq.agent.delete":
194
+ _identity.deleteAgent(kwargs.agent_id);
195
+ return { success: true };
196
+ case "aicq.chat.send":
197
+ return await _chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, {
198
+ isGroup: kwargs.isGroup,
199
+ });
200
+ case "aicq.chat.history":
201
+ return {
202
+ messages: _db.getChatHistory(currentAgentId, kwargs.targetId, {
203
+ limit: kwargs.limit || 50,
204
+ }),
205
+ };
206
+ case "aicq.chat.delete":
207
+ _db.deleteMessage(currentAgentId, kwargs.message_id);
208
+ return { success: true };
209
+ case "aicq.chat.userUpload": {
210
+ // Save a file from a user to the userfiles directory and notify the AI agent
211
+ if (!kwargs.file_data && !kwargs.file_path)
212
+ return { error: "file_data (base64) or file_path is required" };
213
+ if (!kwargs.from_id && !kwargs.targetId)
214
+ return { error: "from_id or targetId is required" };
215
+ const uploadFromId = kwargs.from_id || kwargs.targetId;
216
+ const isGroupUpload = !!kwargs.isGroup;
217
+ let uploadResult;
218
+ if (kwargs.file_data) {
219
+ // Base64 file data
220
+ const fileBuffer = Buffer.from(kwargs.file_data, 'base64');
221
+ uploadResult = await _chat.handleUserFileUpload(currentAgentId, uploadFromId, {
222
+ buffer: fileBuffer,
223
+ originalname: kwargs.file_name || kwargs.fileName || 'file.bin',
224
+ size: fileBuffer.length,
225
+ }, isGroupUpload);
226
+ } else {
227
+ // File path — copy to userfiles
228
+ const srcPath = kwargs.file_path;
229
+ if (!fs.existsSync(srcPath)) return { error: "File not found: " + srcPath };
230
+ const fileBuffer = fs.readFileSync(srcPath);
231
+ uploadResult = await _chat.handleUserFileUpload(currentAgentId, uploadFromId, {
232
+ buffer: fileBuffer,
233
+ originalname: kwargs.file_name || path.basename(srcPath),
234
+ size: fileBuffer.length,
235
+ }, isGroupUpload);
236
+ }
237
+ return { success: true, localPath: uploadResult.localPath, originalName: uploadResult.originalName };
238
+ }
239
+ case "aicq.chat.userfiles": {
240
+ // List user files
241
+ const userfilesDir = runtime.userfilesDir;
242
+ if (!userfilesDir || !fs.existsSync(userfilesDir)) return { files: [] };
243
+ const userFiles = fs.readdirSync(userfilesDir)
244
+ .filter(f => fs.statSync(path.join(userfilesDir, f)).isFile())
245
+ .map(f => {
246
+ const fp = path.join(userfilesDir, f);
247
+ const stat = fs.statSync(fp);
248
+ return { name: f, path: fp, size: stat.size, modified: stat.mtime.toISOString() };
249
+ })
250
+ .sort((a, b) => b.modified.localeCompare(a.modified));
251
+ return { files: userFiles };
252
+ }
253
+ case "aicq.chat.streamChunk": {
254
+ if (!kwargs.friend_id && !kwargs.targetId)
255
+ return { error: "friend_id or targetId is required" };
256
+ if (!kwargs.data) return { error: "data is required" };
257
+ const chunkType = kwargs.chunk_type || kwargs.chunkType || "text";
258
+ const ALLOWED_CHUNK_TYPES = [
259
+ "text",
260
+ "reasoning",
261
+ "thinking",
262
+ "clear_text",
263
+ "tool_call",
264
+ "tool_result",
265
+ ];
266
+ if (!ALLOWED_CHUNK_TYPES.includes(chunkType))
267
+ return {
268
+ error: `Invalid chunk_type: ${chunkType}. Allowed: ${ALLOWED_CHUNK_TYPES.join(", ")}`,
269
+ };
270
+ const streamTarget = kwargs.friend_id || kwargs.targetId;
271
+ const sent = _serverClient.sendWS({
272
+ type: "stream_chunk",
273
+ to: streamTarget,
274
+ chunkType,
275
+ data: kwargs.data,
276
+ });
277
+ if (!sent) return { error: "Not connected to server", success: false };
278
+ return { success: true };
279
+ }
280
+ case "aicq.chat.streamEnd": {
281
+ if (!kwargs.friend_id && !kwargs.targetId)
282
+ return { error: "friend_id or targetId is required" };
283
+ const endTarget = kwargs.friend_id || kwargs.targetId;
284
+ const msgId =
285
+ kwargs.message_id ||
286
+ kwargs.messageId ||
287
+ "msg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6);
288
+ const endSent = _serverClient.sendWS({
289
+ type: "stream_end",
290
+ to: endTarget,
291
+ messageId: msgId,
292
+ });
293
+ if (!endSent) return { error: "Not connected to server", success: false };
294
+ return { success: true, messageId: msgId };
295
+ }
296
+ case "aicq.groups.list":
297
+ return { groups: _db.listGroups(currentAgentId) };
298
+ case "aicq.groups.create": {
299
+ await _serverClient.ensureAuth(currentAgentId);
300
+ const result = await _serverClient.createGroup(kwargs.name, kwargs.description);
301
+ if (result.id) {
302
+ _db.addGroup({
303
+ agent_id: currentAgentId,
304
+ id: result.id,
305
+ name: kwargs.name,
306
+ owner_id: currentAgentId,
307
+ members_json: result.members || "[]",
308
+ description: kwargs.description || "",
309
+ });
310
+ }
311
+ return { success: true, group: result };
312
+ }
313
+ case "aicq.groups.join":
314
+ await _serverClient.ensureAuth(currentAgentId);
315
+ return await _serverClient.inviteGroupMember(kwargs.group_id, currentAgentId);
316
+ case "aicq.groups.messages": {
317
+ await _serverClient.ensureAuth(currentAgentId);
318
+ return await _serverClient.getGroupMessages(kwargs.group_id, kwargs.limit || 50);
319
+ }
320
+ case "aicq.groups.silent":
321
+ _db.setGroupSilentMode(currentAgentId, kwargs.group_id, !!kwargs.silent);
322
+ return { success: true, silent: !!kwargs.silent };
323
+ case "aicq.sessions.list":
324
+ return { sessions: [] };
325
+ default:
326
+ return { error: `Unknown method: ${method}` };
327
+ }
328
+ }
329
+
330
+ // ── CLI metadata registration (lightweight, no runtime init) ─────────
331
+ function registerCliMetadata(api) {
332
+ api.registerCli(
333
+ ({ program }) => {
334
+ program
335
+ .command("aicq-chat")
336
+ .description("AICQ Encrypted Chat management");
337
+ },
338
+ {
339
+ descriptors: [
340
+ {
341
+ name: "aicq-chat",
342
+ description: "AICQ Encrypted Chat management",
343
+ hasSubcommands: false,
344
+ },
345
+ ],
346
+ }
347
+ );
348
+ }
349
+
350
+ // ── Full runtime registration ────────────────────────────────────────
351
+ async function registerFull(api) {
352
+ // Expose ensureInitialized on the runtime store immediately so that
353
+ // startAccount (called by the channel loader) can trigger init even
354
+ // if no gateway method has been invoked yet.
355
+ runtime.ensureInitialized = ensureInitialized;
356
+
357
+ // Register gateway RPC methods each wraps handleGatewayMethod
358
+ const GATEWAY_METHODS = [
359
+ "aicq.status",
360
+ "aicq.friends.list",
361
+ "aicq.friends.add",
362
+ "aicq.friends.addByNumber",
363
+ "aicq.friends.remove",
364
+ "aicq.friends.requests",
365
+ "aicq.friends.acceptRequest",
366
+ "aicq.friends.rejectRequest",
367
+ "aicq.identity.info",
368
+ "aicq.agent.create",
369
+ "aicq.agent.delete",
370
+ "aicq.chat.send",
371
+ "aicq.chat.history",
372
+ "aicq.chat.delete",
373
+ "aicq.chat.userUpload",
374
+ "aicq.chat.userfiles",
375
+ "aicq.chat.streamChunk",
376
+ "aicq.chat.streamEnd",
377
+ "aicq.groups.list",
378
+ "aicq.groups.create",
379
+ "aicq.groups.join",
380
+ "aicq.groups.messages",
381
+ "aicq.groups.silent",
382
+ "aicq.sessions.list",
383
+ ];
384
+
385
+ for (const method of GATEWAY_METHODS) {
386
+ api.registerGatewayMethod(method, async (opts) => {
387
+ try {
388
+ await ensureInitialized();
389
+ const result = await handleGatewayMethod(method, opts.params || {});
390
+ opts.respond(true, result);
391
+ } catch (e) {
392
+ opts.respond(false, undefined, { message: e.message, code: "AICQ_ERROR" });
393
+ }
394
+ });
395
+ }
396
+
397
+ // Register HTTP routes for the SPA UI and REST API.
398
+ // Lazy-loaded to keep the entry narrow — the ui-routes module pulls in
399
+ // qrcode and multer which are not needed during setup-only registration.
400
+ try {
401
+ const { registerHttpRoutes } = await import("./src/ui-routes.js");
402
+ registerHttpRoutes(api, { ensureInitialized, runtime, DATA_DIR, SERVER_URL });
403
+ } catch (e) {
404
+ console.error("[AICQ Channel] Failed to register HTTP routes:", e.message);
405
+ }
406
+ }
407
+
408
+ // ── Export the entry point ───────────────────────────────────────────
409
+ export default defineChannelPluginEntry({
410
+ id: "aicq-chat",
411
+ name: "AICQ Encrypted Chat",
412
+ description:
413
+ "End-to-end encrypted chat channel plugin for OpenClaw agents — NaCl (X25519 + XSalsa20-Poly1305)",
414
+ plugin: aicqChatPlugin,
415
+ registerCliMetadata,
416
+ registerFull,
417
+ });