aicq-openclaw 3.16.3

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 ADDED
@@ -0,0 +1,460 @@
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
+ // ── Plugin version (single source of truth: package.json) ────────────
24
+ // [v3.16] aicq.status previously reported a stale hard-coded "3.7.0".
25
+ const PLUGIN_VERSION = (() => {
26
+ try {
27
+ return JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")).version || "unknown";
28
+ } catch {
29
+ return "unknown";
30
+ }
31
+ })();
32
+
33
+ // ── Configuration ────────────────────────────────────────────────────
34
+ const DATA_DIR = process.env.AICQ_DATA_DIR || path.join(os.homedir(), ".aicq-plugin");
35
+ const SERVER_URL = process.env.AICQ_SERVER_URL || "https://aicq.me";
36
+ const AUTO_ADD_FRIENDS = process.env.AICQ_AUTO_ADD_FRIENDS
37
+ ? process.env.AICQ_AUTO_ADD_FRIENDS.split(",").map(s => s.trim()).filter(Boolean)
38
+ : []; // [fix v3.16.1] no silent default: adding a hard-coded test account surprised deployments
39
+ const AUTO_ACCEPT_FRIENDS = process.env.AICQ_AUTO_ACCEPT_FRIENDS !== "false"; // default true
40
+
41
+ fs.mkdirSync(DATA_DIR, { recursive: true });
42
+
43
+ // ── Lazy-loaded CJS modules (need async db init) ────────────────────
44
+ let _db = null;
45
+ let _identity = null;
46
+ let _serverClient = null;
47
+ let _handshake = null;
48
+ let _chat = null;
49
+
50
+ /**
51
+ * Initialize all plugin components (async, called once from registerFull).
52
+ */
53
+ async function ensureInitialized() {
54
+ if (runtime._initialized) return;
55
+
56
+ const PluginDatabase = require("./lib/database");
57
+ const IdentityManager = require("./lib/identity");
58
+ const ServerClient = require("./lib/server-client");
59
+ const HandshakeManager = require("./lib/handshake");
60
+ const ChatManager = require("./lib/chat");
61
+
62
+ // Initialize database
63
+ _db = new PluginDatabase(DATA_DIR);
64
+ await _db.init();
65
+ console.log("[AICQ Channel] Database initialized");
66
+
67
+ // Initialize managers
68
+ _identity = new IdentityManager(_db);
69
+ _serverClient = new ServerClient(_identity, _db, SERVER_URL);
70
+ _handshake = new HandshakeManager(_identity, _serverClient, _db);
71
+ const uploadsDir = path.join(DATA_DIR, "uploads");
72
+ const userfilesDir = path.join(DATA_DIR, "userfiles");
73
+ fs.mkdirSync(uploadsDir, { recursive: true });
74
+ fs.mkdirSync(userfilesDir, { recursive: true });
75
+
76
+ _chat = new ChatManager(_identity, _serverClient, _db, uploadsDir, userfilesDir);
77
+
78
+ // Populate the shared runtime store so channel adapters can use it
79
+ runtime.db = _db;
80
+ runtime.identity = _identity;
81
+ runtime.serverClient = _serverClient;
82
+ runtime.handshake = _handshake;
83
+ runtime.chat = _chat;
84
+ runtime.dataDir = DATA_DIR;
85
+ runtime.userfilesDir = userfilesDir;
86
+ runtime.uploadsDir = uploadsDir;
87
+ runtime.serverUrl = SERVER_URL;
88
+ runtime.handleGateway = handleGatewayMethod;
89
+ runtime.ensureInitialized = ensureInitialized;
90
+ runtime.autoAddFriends = AUTO_ADD_FRIENDS;
91
+ runtime.autoAcceptFriends = AUTO_ACCEPT_FRIENDS;
92
+
93
+ // Periodic cleanup
94
+ setInterval(() => _db.cleanup(), 3600000);
95
+
96
+ // SPEC 合规: 探活 aicq-sdk (Step 1)
97
+ // 这里仅检查 aicq-sdk 是否可加载, 不强制使用。
98
+ // 后续 Step 2+ 会逐步将 lib/server-client.js 的方法委托给 aicq-sdk-adapter。
99
+ try {
100
+ const { isSdkAvailable, getSdkVersion } = require('./lib/aicq-sdk-adapter');
101
+ if (isSdkAvailable()) {
102
+ console.log(`[AICQ Channel] aicq-sdk available: v${getSdkVersion()} (adapter wired, ready for Step 2 migration)`);
103
+ } else {
104
+ console.warn('[AICQ Channel] aicq-sdk not available — plugin runs in legacy mode (self-implemented protocol stack). npm install aicq-sdk@>=1.0.0');
105
+ }
106
+ } catch (e) {
107
+ console.warn(`[AICQ Channel] aicq-sdk-adapter not loadable: ${e.message}`);
108
+ }
109
+
110
+ runtime._initialized = true;
111
+ console.log("[AICQ Channel] Plugin runtime initialized");
112
+ }
113
+
114
+ // ── Sync helpers ─────────────────────────────────────────────────────
115
+ async function syncFriendsFromServer(agentId) {
116
+ try {
117
+ await _serverClient.ensureAuth(agentId);
118
+ const result = await _serverClient.listFriends();
119
+ if (result.friends) {
120
+ for (const f of result.friends) {
121
+ const existing = _db.getFriend(agentId, f.id);
122
+ if (!existing) {
123
+ _db.addFriend({
124
+ agent_id: agentId,
125
+ id: f.id,
126
+ public_key: f.public_key || f.publicKey || "",
127
+ fingerprint: f.fingerprint || "",
128
+ friend_type: f.type || f.friend_type || "ai",
129
+ ai_name: f.agent_name || f.ai_name || f.displayName || "",
130
+ });
131
+ } else {
132
+ _db.updateFriendOnline(agentId, f.id, f.is_online || f.isOnline || false);
133
+ }
134
+ }
135
+ }
136
+ } catch (e) {
137
+ console.error("[AICQ Channel] Sync friends failed:", e.message);
138
+ }
139
+ }
140
+
141
+ async function syncGroupsFromServer(agentId) {
142
+ try {
143
+ await _serverClient.ensureAuth(agentId);
144
+ const result = await _serverClient.listGroups();
145
+ if (result.groups) {
146
+ for (const g of result.groups) {
147
+ _db.addGroup({
148
+ agent_id: agentId,
149
+ id: g.id,
150
+ name: g.name,
151
+ owner_id: g.owner_id || g.ownerId || "",
152
+ members_json: g.members || g.members_json || "[]",
153
+ description: g.description || "",
154
+ });
155
+ }
156
+ }
157
+ } catch (e) {
158
+ console.error("[AICQ Channel] Sync groups failed:", e.message);
159
+ }
160
+ }
161
+
162
+ // ── Gateway method handler ───────────────────────────────────────────
163
+ async function handleGatewayMethod(method, kwargs = {}) {
164
+ const agents = _identity.listAgents();
165
+ const currentAgentId = agents.length > 0 ? agents[0].agent_id : null;
166
+
167
+ switch (method) {
168
+ case "aicq.status":
169
+ return {
170
+ state: _serverClient.connected ? "connected" : "disconnected",
171
+ agent_id: currentAgentId,
172
+ version: PLUGIN_VERSION,
173
+ architecture: "channel",
174
+ };
175
+ case "aicq.friends.list":
176
+ return { friends: _db.listFriends(currentAgentId) };
177
+ case "aicq.friends.add":
178
+ return await _handshake.addFriendByCode(currentAgentId, kwargs.temp_number);
179
+ case "aicq.friends.addByNumber": {
180
+ // Add friend by AICQ number directly (e.g., "1000000")
181
+ if (!kwargs.number && !kwargs.aicq_number)
182
+ return { error: "number or aicq_number is required" };
183
+ try {
184
+ await _serverClient.ensureAuth(currentAgentId);
185
+ const aicqNumber = kwargs.number || kwargs.aicq_number;
186
+ const result = await _serverClient.sendFriendRequest(aicqNumber, kwargs.message || 'Hi, I\'d like to add you as a friend!');
187
+ // If the request was accepted immediately, also add locally
188
+ if (result.status === 'accepted' && result.to_id) {
189
+ _db.addFriend({
190
+ agent_id: currentAgentId,
191
+ id: result.to_id,
192
+ public_key: '',
193
+ fingerprint: '',
194
+ friend_type: 'human',
195
+ ai_name: kwargs.nickname || '',
196
+ });
197
+ }
198
+ return { success: true, request_id: result.id, status: result.status, to_id: result.to_id };
199
+ } catch (e) {
200
+ return { error: e.message };
201
+ }
202
+ }
203
+ case "aicq.friends.remove":
204
+ _db.removeFriend(currentAgentId, kwargs.friend_id);
205
+ return { success: true };
206
+ case "aicq.friends.requests":
207
+ return { requests: await _handshake.getPendingRequests(currentAgentId) };
208
+ case "aicq.friends.acceptRequest":
209
+ return await _handshake.acceptRequest(currentAgentId, kwargs.request_id);
210
+ case "aicq.friends.rejectRequest":
211
+ return await _handshake.rejectRequest(currentAgentId, kwargs.request_id);
212
+ case "aicq.identity.info":
213
+ return _identity.getInfo(currentAgentId) || {};
214
+ case "aicq.agent.create":
215
+ _identity.createAgent(kwargs.agent_id, kwargs.nickname);
216
+ return { success: true };
217
+ case "aicq.agent.delete":
218
+ _identity.deleteAgent(kwargs.agent_id);
219
+ return { success: true };
220
+ case "aicq.chat.send":
221
+ return await _chat.sendMessage(currentAgentId, kwargs.targetId, kwargs.content, {
222
+ isGroup: kwargs.isGroup,
223
+ });
224
+ case "aicq.chat.history":
225
+ return {
226
+ messages: _db.getChatHistory(currentAgentId, kwargs.targetId, {
227
+ limit: kwargs.limit || 50,
228
+ }),
229
+ };
230
+ case "aicq.chat.delete":
231
+ _db.deleteMessage(currentAgentId, kwargs.message_id);
232
+ return { success: true };
233
+ case "aicq.chat.userUpload": {
234
+ // Save a file from a user to the userfiles directory and notify the AI agent
235
+ if (!kwargs.file_data && !kwargs.file_path)
236
+ return { error: "file_data (base64) or file_path is required" };
237
+ if (!kwargs.from_id && !kwargs.targetId)
238
+ return { error: "from_id or targetId is required" };
239
+ const uploadFromId = kwargs.from_id || kwargs.targetId;
240
+ const isGroupUpload = !!kwargs.isGroup;
241
+ let uploadResult;
242
+ if (kwargs.file_data) {
243
+ // Base64 file data
244
+ const fileBuffer = Buffer.from(kwargs.file_data, 'base64');
245
+ uploadResult = await _chat.handleUserFileUpload(currentAgentId, uploadFromId, {
246
+ buffer: fileBuffer,
247
+ originalname: kwargs.file_name || kwargs.fileName || 'file.bin',
248
+ size: fileBuffer.length,
249
+ }, isGroupUpload);
250
+ } else {
251
+ // File path — copy to userfiles
252
+ const srcPath = kwargs.file_path;
253
+ if (!fs.existsSync(srcPath)) return { error: "File not found: " + srcPath };
254
+ const fileBuffer = fs.readFileSync(srcPath);
255
+ uploadResult = await _chat.handleUserFileUpload(currentAgentId, uploadFromId, {
256
+ buffer: fileBuffer,
257
+ originalname: kwargs.file_name || path.basename(srcPath),
258
+ size: fileBuffer.length,
259
+ }, isGroupUpload);
260
+ }
261
+ return { success: true, localPath: uploadResult.localPath, originalName: uploadResult.originalName };
262
+ }
263
+ case "aicq.chat.userfiles": {
264
+ // List user files
265
+ const userfilesDir = runtime.userfilesDir;
266
+ if (!userfilesDir || !fs.existsSync(userfilesDir)) return { files: [] };
267
+ const userFiles = fs.readdirSync(userfilesDir)
268
+ .filter(f => fs.statSync(path.join(userfilesDir, f)).isFile())
269
+ .map(f => {
270
+ const fp = path.join(userfilesDir, f);
271
+ const stat = fs.statSync(fp);
272
+ return { name: f, path: fp, size: stat.size, modified: stat.mtime.toISOString() };
273
+ })
274
+ .sort((a, b) => b.modified.localeCompare(a.modified));
275
+ return { files: userFiles };
276
+ }
277
+ case "aicq.chat.streamChunk": {
278
+ if (!kwargs.friend_id && !kwargs.targetId)
279
+ return { error: "friend_id or targetId is required" };
280
+ if (!kwargs.data) return { error: "data is required" };
281
+ const chunkType = kwargs.chunk_type || kwargs.chunkType || "text";
282
+ const ALLOWED_CHUNK_TYPES = [
283
+ "text",
284
+ "reasoning",
285
+ "thinking",
286
+ "clear_text",
287
+ "tool_call",
288
+ "tool_result",
289
+ ];
290
+ if (!ALLOWED_CHUNK_TYPES.includes(chunkType))
291
+ return {
292
+ error: `Invalid chunk_type: ${chunkType}. Allowed: ${ALLOWED_CHUNK_TYPES.join(", ")}`,
293
+ };
294
+ const streamTarget = kwargs.friend_id || kwargs.targetId;
295
+ const sent = _serverClient.sendWS({
296
+ type: "stream_chunk",
297
+ to: streamTarget,
298
+ chunkType,
299
+ data: kwargs.data,
300
+ });
301
+ if (!sent) return { error: "Not connected to server", success: false };
302
+ return { success: true };
303
+ }
304
+ case "aicq.chat.streamEnd": {
305
+ if (!kwargs.friend_id && !kwargs.targetId)
306
+ return { error: "friend_id or targetId is required" };
307
+ const endTarget = kwargs.friend_id || kwargs.targetId;
308
+ const msgId =
309
+ kwargs.message_id ||
310
+ kwargs.messageId ||
311
+ "msg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6);
312
+ const endSent = _serverClient.sendWS({
313
+ type: "stream_end",
314
+ to: endTarget,
315
+ messageId: msgId,
316
+ });
317
+ if (!endSent) return { error: "Not connected to server", success: false };
318
+ return { success: true, messageId: msgId };
319
+ }
320
+ case "aicq.groups.list":
321
+ return { groups: _db.listGroups(currentAgentId) };
322
+ case "aicq.groups.create": {
323
+ await _serverClient.ensureAuth(currentAgentId);
324
+ const result = await _serverClient.createGroup(kwargs.name, kwargs.description);
325
+ if (result.id) {
326
+ _db.addGroup({
327
+ agent_id: currentAgentId,
328
+ id: result.id,
329
+ name: kwargs.name,
330
+ owner_id: currentAgentId,
331
+ members_json: result.members || "[]",
332
+ description: kwargs.description || "",
333
+ });
334
+ }
335
+ return { success: true, group: result };
336
+ }
337
+ case "aicq.groups.join":
338
+ await _serverClient.ensureAuth(currentAgentId);
339
+ return await _serverClient.inviteGroupMember(kwargs.group_id, currentAgentId);
340
+ case "aicq.groups.messages": {
341
+ await _serverClient.ensureAuth(currentAgentId);
342
+ return await _serverClient.getGroupMessages(kwargs.group_id, kwargs.limit || 50);
343
+ }
344
+ case "aicq.groups.silent":
345
+ _db.setGroupSilentMode(currentAgentId, kwargs.group_id, !!kwargs.silent);
346
+ return { success: true, silent: !!kwargs.silent };
347
+ case "aicq.sessions.list":
348
+ return { sessions: [] };
349
+ default:
350
+ return { error: `Unknown method: ${method}` };
351
+ }
352
+ }
353
+
354
+ // ── CLI metadata registration (lightweight, no runtime init) ─────────
355
+ function registerCliMetadata(api) {
356
+ api.registerCli(
357
+ ({ program }) => {
358
+ program
359
+ .command("aicq-chat")
360
+ .description("AICQ Encrypted Chat management");
361
+ },
362
+ {
363
+ descriptors: [
364
+ {
365
+ name: "aicq-chat",
366
+ description: "AICQ Encrypted Chat management",
367
+ hasSubcommands: false,
368
+ },
369
+ ],
370
+ }
371
+ );
372
+ }
373
+
374
+ // ── Full runtime registration ────────────────────────────────────────
375
+ async function registerFull(api) {
376
+ // Expose ensureInitialized on the runtime store immediately so that
377
+ // startAccount (called by the channel loader) can trigger init even
378
+ // if no gateway method has been invoked yet.
379
+ runtime.ensureInitialized = ensureInitialized;
380
+
381
+ // Register gateway RPC methods — each wraps handleGatewayMethod
382
+ const GATEWAY_METHODS = [
383
+ "aicq.status",
384
+ "aicq.friends.list",
385
+ "aicq.friends.add",
386
+ "aicq.friends.addByNumber",
387
+ "aicq.friends.remove",
388
+ "aicq.friends.requests",
389
+ "aicq.friends.acceptRequest",
390
+ "aicq.friends.rejectRequest",
391
+ "aicq.identity.info",
392
+ "aicq.agent.create",
393
+ "aicq.agent.delete",
394
+ "aicq.chat.send",
395
+ "aicq.chat.history",
396
+ "aicq.chat.delete",
397
+ "aicq.chat.userUpload",
398
+ "aicq.chat.userfiles",
399
+ "aicq.chat.streamChunk",
400
+ "aicq.chat.streamEnd",
401
+ "aicq.groups.list",
402
+ "aicq.groups.create",
403
+ "aicq.groups.join",
404
+ "aicq.groups.messages",
405
+ "aicq.groups.silent",
406
+ "aicq.sessions.list",
407
+ ];
408
+
409
+ for (const method of GATEWAY_METHODS) {
410
+ api.registerGatewayMethod(method, async (opts) => {
411
+ try {
412
+ await ensureInitialized();
413
+ const result = await handleGatewayMethod(method, opts.params || {});
414
+ opts.respond(true, result);
415
+ } catch (e) {
416
+ opts.respond(false, undefined, { message: e.message, code: "AICQ_ERROR" });
417
+ }
418
+ });
419
+ }
420
+
421
+ // Register HTTP routes for the SPA UI and REST API.
422
+ // Lazy-loaded to keep the entry narrow — the ui-routes module pulls in
423
+ // qrcode and multer which are not needed during setup-only registration.
424
+ try {
425
+ const { registerHttpRoutes } = await import("./src/ui-routes.js");
426
+ registerHttpRoutes(api, { ensureInitialized, runtime, DATA_DIR, SERVER_URL });
427
+ } catch (e) {
428
+ console.error("[AICQ Channel] Failed to register HTTP routes:", e.message);
429
+ }
430
+
431
+ // ── Register agent tools ──────────────────────────────────────────
432
+ // These tools let the AI agent manage friends (chat-friend), send
433
+ // messages (chat-send), and export its identity key (chat-export-key)
434
+ // via tool calls. The tool definitions are in src/tools.js.
435
+ try {
436
+ if (typeof api.registerTool === "function") {
437
+ const { createAicqTools } = await import("./src/tools.js");
438
+ const tools = createAicqTools(runtime);
439
+ for (const tool of tools) {
440
+ api.registerTool(tool);
441
+ }
442
+ console.log(`[AICQ Channel] Registered ${tools.length} agent tools: ${tools.map(t => t.name).join(", ")}`);
443
+ } else {
444
+ console.warn("[AICQ Channel] api.registerTool not available — agent tools not registered");
445
+ }
446
+ } catch (e) {
447
+ console.error("[AICQ Channel] Failed to register agent tools:", e.message);
448
+ }
449
+ }
450
+
451
+ // ── Export the entry point ───────────────────────────────────────────
452
+ export default defineChannelPluginEntry({
453
+ id: "aicq-chat",
454
+ name: "AICQ Encrypted Chat",
455
+ description:
456
+ "End-to-end encrypted chat channel plugin for OpenClaw agents — NaCl (X25519 + XSalsa20-Poly1305)",
457
+ plugin: aicqChatPlugin,
458
+ registerCliMetadata,
459
+ registerFull,
460
+ });
@@ -0,0 +1,214 @@
1
+ /**
2
+ * aicq-sdk-adapter.js — AICQ SDK (Node.js) 适配层
3
+ *
4
+ * 本文件是 openclaw-plugin 接入 aicq-sdk (npm) 的过渡层。
5
+ *
6
+ * 长期目标:将 lib/identity.js、lib/server-client.js、lib/chat.js 中
7
+ * 重复实现的 AICQ 协议逻辑替换为对 aicq-sdk 的调用,逐步删除重复代码。
8
+ *
9
+ * 迁移进度(详见 MIGRATION_TO_SDK.md):
10
+ * Step 1 ✅ 引入 aicq-sdk,创建本适配层(当前文件)
11
+ * Step 2 ⏳ 计划中:迁移 IdentityManager
12
+ * Step 3 ⏳ 计划中:迁移 ServerClient
13
+ * Step 4 ⏳ 计划中:迁移 ChatManager
14
+ * Step 5 ⏳ 计划中:删除重复的 lib/ 模块
15
+ *
16
+ * 当前阶段(Step 1):
17
+ * - 提供 AICQAdapter 类,封装 aicq-sdk 的 AICQClient
18
+ * - 暴露与 ServerClient 相似的方法签名,便于逐步替换
19
+ * - 不改变现有 ServerClient / IdentityManager / ChatManager 的行为
20
+ *
21
+ * 设计原则:
22
+ * - 适配层只做"格式转换 + 调用委托",不重新实现协议
23
+ * - openclaw 现有 identity 格式(agentId + Ed25519 私钥 hex)与 aicq-sdk 的
24
+ * Agent 格式通过本层转换
25
+ * - 后续 Step 2+ 会将 server-client.js 的方法逐个改为调用本适配层
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ let aicqSdk;
31
+ let sdkAvailable = false;
32
+ let sdkVersion = '0.0.0-unavailable';
33
+
34
+ try {
35
+ aicqSdk = require('aicq-sdk');
36
+ sdkAvailable = true;
37
+ sdkVersion = require('aicq-sdk/package.json').version || 'unknown';
38
+ } catch (err) {
39
+ // aicq-sdk 未安装 — 适配层仍可加载,但所有调用会抛错
40
+ // 由 isSdkAvailable() 让调用方决定是否启用
41
+ aicqSdk = null;
42
+ }
43
+
44
+ /**
45
+ * AICQAdapter — 把 openclaw 的 identity 格式桥接到 aicq-sdk 的 Agent 格式。
46
+ *
47
+ * 生命周期:
48
+ * 1. constructor: 创建 AICQClient 实例(不连接)
49
+ * 2. importIdentity: 从 IdentityManager 注入 Ed25519 私钥 + token
50
+ * 3. login: 调用 SDK 的 challenge-response login
51
+ * 4. connect: 建立 WebSocket 连接,注册回调
52
+ * 5. sendMessage / listFriends / ...: 委托给 SDK
53
+ * 6. disconnect: 优雅断开(SDK 自动发送 offline 消息)
54
+ */
55
+ class AICQAdapter {
56
+ /**
57
+ * @param {Object} options
58
+ * @param {string} [options.server='https://aicq.me'] - AICQ server URL
59
+ * @param {string} [options.dbPath] - optional SQLite DB path
60
+ */
61
+ constructor(options = {}) {
62
+ if (!sdkAvailable) {
63
+ throw new Error(
64
+ `aicq-sdk ${sdkVersion} not available. ` +
65
+ `Install with: npm install aicq-sdk@>=1.0.0`
66
+ );
67
+ }
68
+ const { AICQClient } = aicqSdk;
69
+ this.serverUrl = (options.server || 'https://aicq.me').replace(/\/$/, '');
70
+ this._sdk = new AICQClient(this.serverUrl);
71
+ this._imported = false;
72
+ // 优先使用 openclaw-plugin 内置 logger,找不到时退化为 console
73
+ try {
74
+ const loggerMod = require('./logger');
75
+ this._logger = loggerMod && typeof loggerMod.createLogger === 'function'
76
+ ? loggerMod.createLogger('aicq-adapter')
77
+ : console;
78
+ } catch (_e) {
79
+ this._logger = console;
80
+ }
81
+ this._logger.info(`AICQAdapter initialized (aicq-sdk ${sdkVersion}, server=${this.serverUrl})`);
82
+ }
83
+
84
+ /** 直接暴露底层 AICQClient,供高级用法使用。 */
85
+ get sdk() {
86
+ return this._sdk;
87
+ }
88
+
89
+ get isImported() {
90
+ return this._imported;
91
+ }
92
+
93
+ // ─── Identity 注入(Step 2 会用本方法替代 identity.js) ──────────
94
+
95
+ /**
96
+ * 将 openclaw IdentityManager 管理的密钥注入 aicq-sdk。
97
+ *
98
+ * @param {Object} params
99
+ * @param {string} params.agentId - openclaw 内部 agent ID
100
+ * @param {string} params.signingKeyHex - 128-char Ed25519 private key hex
101
+ * @param {string} params.publicKeyHex - 64-char Ed25519 public key hex
102
+ * @param {string} [params.accessToken] - existing JWT access token
103
+ * @param {string} [params.refreshToken] - existing JWT refresh token
104
+ * @param {string} [params.accountId] - server-assigned account ID
105
+ */
106
+ async importIdentity({ agentId, signingKeyHex, publicKeyHex, accessToken, refreshToken, accountId }) {
107
+ // aicq-sdk 的 AICQClient 当前没有公开的 importAgent 方法
108
+ // (这与 Python/Go SDK 不同)。临时方案:用 createAgent + login。
109
+ // 长期方案:在 aicq-sdk 中添加 importAgent 方法(与 Go SDK 对齐)。
110
+ //
111
+ // 这里我们仅记录意图,实际迁移在 Step 2 完成。
112
+ this._pendingIdentity = { agentId, signingKeyHex, publicKeyHex, accessToken, refreshToken, accountId };
113
+ this._logger.debug(`Identity pending import for agent=${agentId} (account=${accountId || agentId})`);
114
+ // 标记为已导入以便后续 connect/send 调用知道身份可用
115
+ this._imported = true;
116
+ }
117
+
118
+ // ─── 协议委托(后续 Step 3+ 会逐个迁移 server-client.js 的方法) ──
119
+
120
+ /** 调用 SDK 的 challenge-response login,返回 access_token。 */
121
+ async login() {
122
+ if (!this._imported) {
123
+ throw new Error('Must call importIdentity() before login()');
124
+ }
125
+ return await this._sdk.login();
126
+ }
127
+
128
+ /** 建立 WebSocket 连接。回调通过 onMessage/onGroupMessage 等注册。 */
129
+ async connect() {
130
+ await this._sdk.connect();
131
+ }
132
+
133
+ /** 优雅断开 — SDK 自动发送 offline 消息。 */
134
+ async disconnect() {
135
+ if (typeof this._sdk.disconnect === 'function') {
136
+ await this._sdk.disconnect();
137
+ } else if (typeof this._sdk.close === 'function') {
138
+ await this._sdk.close();
139
+ }
140
+ }
141
+
142
+ /** 发送私聊消息(委托给 SDK)。 */
143
+ async sendMessage(friendId, content) {
144
+ return await this._sdk.sendMessage(friendId, content);
145
+ }
146
+
147
+ /** 发送群组消息。 */
148
+ async sendGroupMessage(groupId, content) {
149
+ return await this._sdk.sendGroupMessage(groupId, content);
150
+ }
151
+
152
+ /** 列出好友。 */
153
+ async listFriends() {
154
+ return await this._sdk.listFriends();
155
+ }
156
+
157
+ /** 发送好友请求。 */
158
+ async addFriend(publicKey) {
159
+ return await this._sdk.addFriend(publicKey);
160
+ }
161
+
162
+ /** 上传文件。 */
163
+ async uploadFile(fileName, fileData, mimeType) {
164
+ return await this._sdk.uploadFile(fileName, fileData, mimeType);
165
+ }
166
+
167
+ // ─── 回调注册(Step 4 会用本方法替代 chat.js 的轮询) ─────────────
168
+
169
+ /** 注册私聊消息回调。 */
170
+ onMessage(callback) {
171
+ this._sdk.onMessage(callback);
172
+ }
173
+
174
+ /** 注册群组消息回调。 */
175
+ onGroupMessage(callback) {
176
+ this._sdk.onGroupMessage(callback);
177
+ }
178
+
179
+ /** 注册流式输出 chunk 回调。 */
180
+ onStreamChunk(callback) {
181
+ this._sdk.onStreamChunk(callback);
182
+ }
183
+
184
+ /** 注册流式输出结束回调(aicq-sdk 1.0+ 新增)。 */
185
+ onStreamEnd(callback) {
186
+ if (typeof this._sdk.onStreamEnd === 'function') {
187
+ this._sdk.onStreamEnd(callback);
188
+ } else {
189
+ this._logger.warn('aicq-sdk onStreamEnd not available (need >=1.0.0)');
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * 检查 aicq-sdk 是否可加载。
196
+ * @returns {boolean}
197
+ */
198
+ function isSdkAvailable() {
199
+ return sdkAvailable;
200
+ }
201
+
202
+ /**
203
+ * 返回 aicq-sdk 版本字符串(不可用则返回 '0.0.0-unavailable')。
204
+ * @returns {string}
205
+ */
206
+ function getSdkVersion() {
207
+ return sdkVersion;
208
+ }
209
+
210
+ module.exports = {
211
+ AICQAdapter,
212
+ isSdkAvailable,
213
+ getSdkVersion,
214
+ };