aicq-chat-plugin 3.5.3 → 3.7.0

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
@@ -132,7 +132,7 @@ async function handleGatewayMethod(method, kwargs = {}) {
132
132
  return {
133
133
  state: _serverClient.connected ? "connected" : "disconnected",
134
134
  agent_id: currentAgentId,
135
- version: "3.2.0",
135
+ version: "3.7.0",
136
136
  architecture: "channel",
137
137
  };
138
138
  case "aicq.friends.list":
@@ -268,6 +268,11 @@ function registerCliMetadata(api) {
268
268
 
269
269
  // ── Full runtime registration ────────────────────────────────────────
270
270
  async function registerFull(api) {
271
+ // Expose ensureInitialized on the runtime store immediately so that
272
+ // startAccount (called by the channel loader) can trigger init even
273
+ // if no gateway method has been invoked yet.
274
+ runtime.ensureInitialized = ensureInitialized;
275
+
271
276
  // Register gateway RPC methods — each wraps handleGatewayMethod
272
277
  const GATEWAY_METHODS = [
273
278
  "aicq.status",
package/lib/package.json CHANGED
File without changes
@@ -2,7 +2,7 @@
2
2
  "kind": "channel",
3
3
  "id": "aicq-chat",
4
4
  "name": "AICQ Encrypted Chat",
5
- "version": "3.5.3",
5
+ "version": "3.7.0",
6
6
  "description": "End-to-end encrypted chat channel via AICQ protocol — in-process Channel plugin using OpenClaw Channel SDK",
7
7
  "entry": "index.js",
8
8
  "activation": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicq-chat-plugin",
3
- "version": "3.5.3",
3
+ "version": "3.7.0",
4
4
  "description": "AICQ End-to-end Encrypted Chat Channel Plugin for OpenClaw — In-process Channel SDK architecture with friend management, group chat, file transfer, and AI agent communication",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/channel.js CHANGED
@@ -122,8 +122,46 @@ const _plugin = createChatChannelPlugin({
122
122
  id: "aicq-chat",
123
123
 
124
124
  setup: {
125
- resolveAccount,
126
- inspectAccount,
125
+ /**
126
+ * Resolve the account ID from setup input.
127
+ * Called by the setup wizard when a user configures the channel.
128
+ */
129
+ resolveAccountId(params) {
130
+ const { cfg, accountId, input } = params;
131
+ return accountId || input?.accountId || resolveTemplateVar(cfg, "{{agent.id}}");
132
+ },
133
+
134
+ /**
135
+ * Apply the account config after the setup wizard completes.
136
+ * Must return the updated OpenClawConfig.
137
+ */
138
+ applyAccountConfig(params) {
139
+ const { cfg, accountId, input } = params;
140
+ const section = (cfg.channels || {})["aicq-chat"] || {};
141
+ return {
142
+ ...cfg,
143
+ channels: {
144
+ ...(cfg.channels || {}),
145
+ "aicq-chat": {
146
+ ...section,
147
+ accountId: accountId || input?.accountId || "{{agent.id}}",
148
+ serverUrl: input?.serverUrl || section.serverUrl || "https://aicq.online",
149
+ autoAcceptFriends: input?.autoAcceptFriends ?? section.autoAcceptFriends ?? true,
150
+ enabled: true,
151
+ dmPolicy: input?.dmPolicy || section.dmPolicy || "allowlist",
152
+ allowFrom: input?.allowFrom || section.allowFrom || [],
153
+ },
154
+ },
155
+ };
156
+ },
157
+
158
+ /**
159
+ * Validate setup input before applying.
160
+ * Return an error message string or null if valid.
161
+ */
162
+ validateInput(params) {
163
+ return null;
164
+ },
127
165
  },
128
166
 
129
167
  // Gateway method descriptors — these are the method names the plugin
@@ -236,7 +274,7 @@ _plugin.gateway = {
236
274
  * listening for inbound messages.
237
275
  */
238
276
  async startAccount(ctx) {
239
- const { cfg, accountId, account, setStatus, log } = ctx;
277
+ const { cfg, accountId, account, setStatus, log, abortSignal } = ctx;
240
278
 
241
279
  const logger = log || console;
242
280
  logger.info?.(`[AICQ Channel] startAccount called for ${accountId}`) || console.log(`[AICQ Channel] startAccount called for ${accountId}`);
@@ -287,7 +325,6 @@ _plugin.gateway = {
287
325
  console.log(`[AICQ Channel] Authenticated as ${agentId}`);
288
326
 
289
327
  // Connect WebSocket for real-time messages
290
- // ServerClient.start() does ensureAuth + connectWS
291
328
  if (typeof runtime.serverClient.start === "function") {
292
329
  await runtime.serverClient.start(agentId);
293
330
  console.log("[AICQ Channel] WebSocket connected");
@@ -310,57 +347,112 @@ _plugin.gateway = {
310
347
  }
311
348
  }
312
349
 
313
- // Wire up inbound message handling via channelRuntime if available
314
- if (ctx.channelRuntime) {
315
- const { reply, routing } = ctx.channelRuntime;
316
- if (reply && routing) {
317
- console.log("[AICQ Channel] channelRuntime available AI dispatch enabled");
318
-
319
- // Register inbound message handler on the serverClient
320
- if (runtime.serverClient && typeof runtime.serverClient.onMessage === "function") {
321
- runtime.serverClient.onMessage(async (msg) => {
322
- try {
323
- const resolvedAgentId = agentId;
324
- const routeResult = await routing.resolveAgentRoute({
325
- channelId: "aicq-chat",
326
- accountId,
327
- fromId: msg.from || msg.sender_id,
328
- chatType: msg.isGroup ? "group" : "dm",
329
- });
350
+ // Wire up inbound message handling
351
+ // Register handlers for AICQ message types. When an inbound message arrives,
352
+ // we try the OpenClaw channelRuntime first; if that fails we fall back to
353
+ // calling z-ai CLI directly and sending the reply via chat.sendMessage.
354
+ if (runtime.serverClient && typeof runtime.serverClient.onMessage === "function") {
355
+ const inboundHandler = async (msg) => {
356
+ try {
357
+ // AICQ server wraps message content in msg.data for "message" type,
358
+ // but "relay" type may have fields at the top level.
359
+ const data = msg.data || msg;
360
+ const fromId = data.from || data.fromId || data.sender_id || msg.from || msg.fromId;
361
+ const isGroup = !!(data.isGroup || data.groupId || msg.isGroup || msg.groupId);
362
+ const text = data.content || data.text || data.payload || msg.content || msg.text || msg.payload || "";
363
+
364
+ console.log(`[AICQ Channel] Inbound message from=${fromId} isGroup=${isGroup} text=${(text || "").substring(0, 80)}`);
365
+
366
+ if (!fromId || !text) {
367
+ return; // Skip system messages (online_ack, presence, etc.)
368
+ }
330
369
 
331
- if (routeResult?.agentId) {
332
- await reply.dispatchReplyWithBufferedBlockDispatcher({
333
- ctx: {
334
- channelId: "aicq-chat",
335
- accountId,
336
- fromId: msg.from || msg.sender_id,
337
- text: msg.content || msg.text || "",
338
- chatType: msg.isGroup ? "group" : "dm",
339
- },
340
- cfg,
341
- dispatcherOptions: {
342
- deliver: async (payload) => {
343
- // Send AI reply back through AICQ
344
- if (runtime.chat && payload.text) {
345
- await runtime.chat.sendMessage(
346
- resolvedAgentId,
347
- msg.from || msg.sender_id,
348
- payload.text,
349
- { isGroup: !!msg.isGroup }
350
- );
351
- }
352
- },
353
- },
370
+ // Skip our own messages
371
+ if (fromId === runtime.serverClient?.serverAccountId || fromId === agentId) {
372
+ return;
373
+ }
374
+
375
+ // Try channelRuntime (OpenClaw's built-in agent dispatch) first
376
+ if (ctx.channelRuntime) {
377
+ const { reply, routing } = ctx.channelRuntime;
378
+ if (reply && routing) {
379
+ try {
380
+ const routeResult = await routing.resolveAgentRoute({
381
+ channelId: "aicq-chat",
382
+ accountId,
383
+ fromId,
384
+ chatType: isGroup ? "group" : "dm",
354
385
  });
386
+
387
+ if (routeResult?.agentId) {
388
+ await reply.dispatchReplyWithBufferedBlockDispatcher({
389
+ ctx: {
390
+ channelId: "aicq-chat",
391
+ accountId,
392
+ fromId,
393
+ text,
394
+ chatType: isGroup ? "group" : "dm",
395
+ },
396
+ cfg,
397
+ dispatcherOptions: {
398
+ deliver: async (payload) => {
399
+ if (runtime.chat && payload.text) {
400
+ await runtime.chat.sendMessage(agentId, fromId, payload.text, { isGroup });
401
+ }
402
+ },
403
+ },
404
+ });
405
+ return; // Successfully dispatched via channelRuntime
406
+ }
407
+ } catch (e) {
408
+ console.error("[AICQ Channel] channelRuntime dispatch failed, using fallback:", e.message);
355
409
  }
356
- } catch (e) {
357
- console.error("[AICQ Channel] Inbound message handling error:", e.message);
358
410
  }
359
- });
411
+ }
412
+
413
+ // Fallback: call LLM via z-ai CLI and send reply directly
414
+ console.log("[AICQ Channel] Using fallback: direct z-ai CLI call");
415
+ try {
416
+ const { execFile } = await import('child_process');
417
+ const llmResult = await new Promise((resolve, reject) => {
418
+ execFile('z-ai', ['chat', '--prompt', text], { timeout: 60000 }, (err, stdout) => {
419
+ if (err) reject(err);
420
+ else resolve(stdout);
421
+ });
422
+ });
423
+ let replyText = '';
424
+ const jsonStart = llmResult.indexOf('{');
425
+ if (jsonStart >= 0) {
426
+ try {
427
+ const parsed = JSON.parse(llmResult.substring(jsonStart));
428
+ replyText = parsed.choices?.[0]?.message?.content || llmResult.trim();
429
+ } catch { replyText = llmResult.trim(); }
430
+ } else { replyText = llmResult.trim(); }
431
+
432
+ if (replyText && runtime.chat) {
433
+ await runtime.chat.sendMessage(agentId, fromId, replyText, { isGroup });
434
+ console.log(`[AICQ Channel] Fallback reply sent to ${fromId}: ${replyText.substring(0, 50)}`);
435
+ }
436
+ } catch (fallbackErr) {
437
+ console.error("[AICQ Channel] Fallback LLM also failed:", fallbackErr.message);
438
+ }
439
+ } catch (e) {
440
+ console.error("[AICQ Channel] Inbound message handling error:", e.message);
360
441
  }
442
+ };
443
+
444
+ // Register handler for relevant AICQ message types.
445
+ // ServerClient.onMessage(type, handler) requires a type string.
446
+ runtime.serverClient.onMessage("relay", inboundHandler);
447
+ runtime.serverClient.onMessage("message", inboundHandler);
448
+ runtime.serverClient.onMessage("group_message", inboundHandler);
449
+ console.log("[AICQ Channel] Inbound message handlers registered (relay, message, group_message)");
450
+
451
+ if (ctx.channelRuntime) {
452
+ console.log("[AICQ Channel] channelRuntime available — AI dispatch enabled (with z-ai CLI fallback)");
453
+ } else {
454
+ console.log("[AICQ Channel] channelRuntime not available — using z-ai CLI fallback");
361
455
  }
362
- } else {
363
- console.log("[AICQ Channel] channelRuntime not available — running in standalone mode");
364
456
  }
365
457
 
366
458
  // Update health status
@@ -374,6 +466,17 @@ _plugin.gateway = {
374
466
  });
375
467
 
376
468
  console.log(`[AICQ Channel] Account ${accountId} started successfully`);
469
+
470
+ // ── Keep startAccount alive until abort signal ──────────────────
471
+ // OpenClaw expects startAccount to be a long-lived task. If it
472
+ // resolves immediately, the gateway treats it as an unexpected
473
+ // exit and enters a restart loop. We wait on the abort signal.
474
+ await new Promise((resolve) => {
475
+ if (abortSignal?.aborted) { resolve(); return; }
476
+ const onAbort = () => { cleanup(); resolve(); };
477
+ const cleanup = () => { abortSignal?.removeEventListener("abort", onAbort); };
478
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
479
+ });
377
480
  },
378
481
 
379
482
  /**
@@ -409,6 +512,8 @@ _plugin.config = {
409
512
  /**
410
513
  * List all account IDs configured for this channel.
411
514
  * Resolves template variables like {{agent.id}}.
515
+ *
516
+ * Signature: (cfg: OpenClawConfig) => string[]
412
517
  */
413
518
  listAccountIds(cfg) {
414
519
  const section = (cfg.channels || {})["aicq-chat"] || {};
@@ -421,17 +526,22 @@ _plugin.config = {
421
526
 
422
527
  /**
423
528
  * Resolve an account from config. Reuses the setup resolver.
529
+ *
530
+ * Signature: (cfg: OpenClawConfig, accountId?: string | null) => ResolvedAccount
424
531
  */
425
532
  resolveAccount,
426
533
 
427
534
  /**
428
535
  * Lightweight account inspection.
536
+ *
537
+ * Signature: (cfg: OpenClawConfig, accountId?: string | null) => unknown
429
538
  */
430
539
  inspectAccount,
431
540
 
432
541
  /**
433
542
  * Default account ID for this channel.
434
- * Resolves {{agent.id}} to the actual agent ID.
543
+ *
544
+ * Signature: (cfg: OpenClawConfig) => string
435
545
  */
436
546
  defaultAccountId(cfg) {
437
547
  const section = (cfg.channels || {})["aicq-chat"] || {};
@@ -442,19 +552,39 @@ _plugin.config = {
442
552
  },
443
553
 
444
554
  /**
445
- * Check if the channel is configured.
555
+ * Check if the account is enabled.
556
+ *
557
+ * IMPORTANT: OpenClaw calls this with (account, cfg) where `account`
558
+ * is the RESOLVED account object from resolveAccount(), not the config.
559
+ *
560
+ * Signature: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean
446
561
  */
447
- isConfigured(cfg) {
448
- const section = (cfg.channels || {})["aicq-chat"] || {};
449
- return Boolean(section.accountId);
562
+ isEnabled(account, cfg) {
563
+ return account.enabled !== false;
564
+ },
565
+
566
+ /**
567
+ * Check if the channel account is configured.
568
+ *
569
+ * IMPORTANT: OpenClaw calls this with (account, cfg) where `account`
570
+ * is the RESOLVED account object from resolveAccount(), not the config.
571
+ * Our old code had isConfigured(cfg) which received the account object
572
+ * as `cfg`, causing it to always return false — this was the root cause
573
+ * of the "not-running" bug.
574
+ *
575
+ * Signature: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean
576
+ */
577
+ isConfigured(account, cfg) {
578
+ return Boolean(account && account.accountId);
450
579
  },
451
580
 
452
581
  /**
453
582
  * Return the reason the channel is not configured.
583
+ *
584
+ * Signature: (account: ResolvedAccount, cfg: OpenClawConfig) => string
454
585
  */
455
- unconfiguredReason(cfg) {
456
- const section = (cfg.channels || {})["aicq-chat"] || {};
457
- if (!section.accountId) {
586
+ unconfiguredReason(account, cfg) {
587
+ if (!account || !account.accountId) {
458
588
  return "accountId is required — set channels.aicq-chat.accountId in openclaw.json";
459
589
  }
460
590
  return null;
@@ -462,14 +592,17 @@ _plugin.config = {
462
592
 
463
593
  /**
464
594
  * Describe the account for status surfaces.
595
+ *
596
+ * IMPORTANT: OpenClaw calls this with (account, cfg) where `account`
597
+ * is the RESOLVED account object, not the config.
598
+ *
599
+ * Signature: (account: ResolvedAccount, cfg: OpenClawConfig) => ChannelAccountSnapshot
465
600
  */
466
- describeAccount(cfg, accountId) {
467
- const section = (cfg.channels || {})["aicq-chat"] || {};
468
- const rawId = accountId || section.accountId || null;
601
+ describeAccount(account, cfg) {
469
602
  return {
470
- accountId: rawId ? resolveTemplateVar(cfg, rawId) : null,
603
+ accountId: account?.accountId || null,
471
604
  label: "AICQ Encrypted Chat",
472
- enabled: section.enabled !== false,
605
+ enabled: account?.enabled !== false,
473
606
  };
474
607
  },
475
608
  };