aicq-chat-plugin 3.5.1 → 3.6.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
@@ -65,6 +65,7 @@ async function ensureInitialized() {
65
65
  runtime.dataDir = DATA_DIR;
66
66
  runtime.serverUrl = SERVER_URL;
67
67
  runtime.handleGateway = handleGatewayMethod;
68
+ runtime.ensureInitialized = ensureInitialized;
68
69
 
69
70
  // Periodic cleanup
70
71
  setInterval(() => _db.cleanup(), 3600000);
@@ -131,7 +132,7 @@ async function handleGatewayMethod(method, kwargs = {}) {
131
132
  return {
132
133
  state: _serverClient.connected ? "connected" : "disconnected",
133
134
  agent_id: currentAgentId,
134
- version: "3.2.0",
135
+ version: "3.6.0",
135
136
  architecture: "channel",
136
137
  };
137
138
  case "aicq.friends.list":
@@ -267,6 +268,11 @@ function registerCliMetadata(api) {
267
268
 
268
269
  // ── Full runtime registration ────────────────────────────────────────
269
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
+
270
276
  // Register gateway RPC methods — each wraps handleGatewayMethod
271
277
  const GATEWAY_METHODS = [
272
278
  "aicq.status",
@@ -2,7 +2,7 @@
2
2
  "kind": "channel",
3
3
  "id": "aicq-chat",
4
4
  "name": "AICQ Encrypted Chat",
5
- "version": "3.5.1",
5
+ "version": "3.6.1",
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": {
@@ -49,6 +49,14 @@
49
49
  "open",
50
50
  "disabled"
51
51
  ]
52
+ },
53
+ "allowFrom": {
54
+ "type": "array",
55
+ "items": {
56
+ "type": "string"
57
+ },
58
+ "default": [],
59
+ "description": "允许发消息的好友 ID 列表(dmPolicy 为 allowlist 时生效)"
52
60
  }
53
61
  },
54
62
  "required": [
@@ -71,6 +79,10 @@
71
79
  "dmPolicy": {
72
80
  "label": "DM Policy",
73
81
  "help": "Who can send direct messages: allowlist (friends only), open, or disabled"
82
+ },
83
+ "allowFrom": {
84
+ "label": "Allow From",
85
+ "help": "Friend IDs allowed to send DMs (used when dmPolicy is allowlist)"
74
86
  }
75
87
  }
76
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicq-chat-plugin",
3
- "version": "3.5.1",
3
+ "version": "3.6.1",
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/setup-entry.js CHANGED
File without changes
package/src/channel.js CHANGED
@@ -35,18 +35,29 @@ export const runtime = {
35
35
  // ── Template variable resolver ───────────────────────────────────────
36
36
  // OpenClaw stores accountId as-is (e.g. "{{agent.id}}") in config.
37
37
  // Plugins must resolve template variables at runtime.
38
+ //
39
+ // The default agent ID in OpenClaw is "main" (DEFAULT_AGENT_ID).
40
+ // When cfg.agents.list is empty/undefined (no explicit agent config),
41
+ // the implicit default agent "main" is used.
42
+
43
+ const OPENCLAW_DEFAULT_AGENT_ID = "main";
38
44
 
39
45
  function resolveTemplateVar(cfg, value) {
40
46
  if (typeof value !== "string") return value;
41
47
  const match = value.match(/^\{\{(\w[\w.]*)\}\}$/);
42
48
  if (!match) return value;
43
49
 
44
- const path = match[1]; // e.g. "agent.id"
45
- if (path === "agent.id") {
46
- const agents = cfg.agents?.list || [];
47
- if (agents.length > 0) return agents[0].id;
48
- // Fallback: use the default agent ID from config
49
- return cfg.agents?.defaultId || "default";
50
+ const tmplPath = match[1]; // e.g. "agent.id"
51
+ if (tmplPath === "agent.id") {
52
+ // Strategy: look for explicit agents in config first
53
+ const agents = cfg.agents?.list;
54
+ if (Array.isArray(agents) && agents.length > 0) {
55
+ // Use the default=true agent, or the first one
56
+ const defaultAgent = agents.find((a) => a.default) || agents[0];
57
+ if (defaultAgent?.id) return defaultAgent.id;
58
+ }
59
+ // Fallback: OpenClaw's implicit default agent ID
60
+ return OPENCLAW_DEFAULT_AGENT_ID;
50
61
  }
51
62
 
52
63
  return value; // unknown template — return as-is
@@ -74,13 +85,19 @@ function resolveAccount(cfg, accountId) {
74
85
  // Resolve template variables like {{agent.id}}
75
86
  const resolvedAccountId = resolveTemplateVar(cfg, rawAccountId);
76
87
 
88
+ // Resolve allowFrom entries (may contain {{agent.id}} or friend IDs)
89
+ const rawAllowFrom = section.allowFrom || [];
90
+ const resolvedAllowFrom = Array.isArray(rawAllowFrom)
91
+ ? rawAllowFrom.map((entry) => resolveTemplateVar(cfg, entry))
92
+ : rawAllowFrom;
93
+
77
94
  return {
78
95
  accountId: resolvedAccountId,
79
96
  serverUrl: section.serverUrl || "https://aicq.online",
80
97
  autoAcceptFriends: section.autoAcceptFriends ?? true,
81
98
  enabled: section.enabled ?? true,
82
99
  dmPolicy: section.dmPolicy || "allowlist",
83
- allowFrom: section.allowFrom ?? [],
100
+ allowFrom: resolvedAllowFrom,
84
101
  };
85
102
  }
86
103
 
@@ -105,8 +122,46 @@ const _plugin = createChatChannelPlugin({
105
122
  id: "aicq-chat",
106
123
 
107
124
  setup: {
108
- resolveAccount,
109
- 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
+ },
110
165
  },
111
166
 
112
167
  // Gateway method descriptors — these are the method names the plugin
@@ -219,24 +274,46 @@ _plugin.gateway = {
219
274
  * listening for inbound messages.
220
275
  */
221
276
  async startAccount(ctx) {
222
- const { cfg, accountId, account, setStatus } = ctx;
277
+ const { cfg, accountId, account, setStatus, log, abortSignal } = ctx;
223
278
 
224
- console.log(`[AICQ Channel] startAccount called for ${accountId}`);
279
+ const logger = log || console;
280
+ logger.info?.(`[AICQ Channel] startAccount called for ${accountId}`) || console.log(`[AICQ Channel] startAccount called for ${accountId}`);
225
281
 
226
- // Ensure the runtime (DB, identity, transport) is initialised
227
- if (runtime.handleGateway) {
228
- // Runtime already initialised via registerFull just verify
282
+ // Ensure the runtime (DB, identity, transport) is initialised.
283
+ // The runtime is populated by registerFull() in index.js, but startAccount
284
+ // may be called before any gateway method is invoked, so we must ensure
285
+ // initialization here too.
286
+ if (!runtime._initialized && typeof runtime.ensureInitialized === "function") {
287
+ try {
288
+ await runtime.ensureInitialized();
289
+ logger.info?.("[AICQ Channel] Runtime initialized via startAccount") || console.log("[AICQ Channel] Runtime initialized via startAccount");
290
+ } catch (e) {
291
+ console.error("[AICQ Channel] Runtime initialization failed:", e.message);
292
+ setStatus({
293
+ accountId,
294
+ enabled: true,
295
+ configured: true,
296
+ running: false,
297
+ lastError: `Initialization failed: ${e.message}`,
298
+ });
299
+ return;
300
+ }
229
301
  }
230
302
 
231
- // Resolve the agent ID from OpenClaw config
232
- const agents = cfg.agents?.list || [];
233
- const agentId = agents.length > 0 ? agents[0].id : accountId;
303
+ // Resolve the agent ID: prefer the resolved accountId from
304
+ // resolveAccount (which already handles {{agent.id}}), then
305
+ // fall back to the OpenClaw default agent ID.
306
+ const agents = cfg.agents?.list;
307
+ const agentId = account?.accountId || accountId || OPENCLAW_DEFAULT_AGENT_ID;
234
308
 
235
309
  // Ensure we have an identity in the plugin DB
236
310
  if (runtime.identity) {
237
311
  const existing = runtime.identity.listAgents();
238
312
  if (existing.length === 0) {
239
- runtime.identity.createAgent(agentId, agents[0]?.name || "AICQ Agent");
313
+ const agentName = (Array.isArray(agents) && agents.length > 0 && agents[0]?.name)
314
+ ? agents[0].name
315
+ : "AICQ Agent";
316
+ runtime.identity.createAgent(agentId, agentName);
240
317
  console.log(`[AICQ Channel] Created agent identity: ${agentId}`);
241
318
  }
242
319
  }
@@ -248,9 +325,12 @@ _plugin.gateway = {
248
325
  console.log(`[AICQ Channel] Authenticated as ${agentId}`);
249
326
 
250
327
  // Connect WebSocket for real-time messages
251
- if (typeof runtime.serverClient.connect === "function") {
252
- await runtime.serverClient.connect(agentId);
328
+ if (typeof runtime.serverClient.start === "function") {
329
+ await runtime.serverClient.start(agentId);
253
330
  console.log("[AICQ Channel] WebSocket connected");
331
+ } else if (typeof runtime.serverClient.connectWS === "function") {
332
+ runtime.serverClient.connectWS();
333
+ console.log("[AICQ Channel] WebSocket connecting");
254
334
  }
255
335
 
256
336
  // Sync friends and groups from server
@@ -273,11 +353,10 @@ _plugin.gateway = {
273
353
  if (reply && routing) {
274
354
  console.log("[AICQ Channel] channelRuntime available — AI dispatch enabled");
275
355
 
276
- // Register inbound message handler on the serverClient
277
356
  if (runtime.serverClient && typeof runtime.serverClient.onMessage === "function") {
278
357
  runtime.serverClient.onMessage(async (msg) => {
279
358
  try {
280
- const resolvedAgentId = agents.length > 0 ? agents[0].id : accountId;
359
+ const resolvedAgentId = agentId;
281
360
  const routeResult = await routing.resolveAgentRoute({
282
361
  channelId: "aicq-chat",
283
362
  accountId,
@@ -297,7 +376,6 @@ _plugin.gateway = {
297
376
  cfg,
298
377
  dispatcherOptions: {
299
378
  deliver: async (payload) => {
300
- // Send AI reply back through AICQ
301
379
  if (runtime.chat && payload.text) {
302
380
  await runtime.chat.sendMessage(
303
381
  resolvedAgentId,
@@ -331,6 +409,17 @@ _plugin.gateway = {
331
409
  });
332
410
 
333
411
  console.log(`[AICQ Channel] Account ${accountId} started successfully`);
412
+
413
+ // ── Keep startAccount alive until abort signal ──────────────────
414
+ // OpenClaw expects startAccount to be a long-lived task. If it
415
+ // resolves immediately, the gateway treats it as an unexpected
416
+ // exit and enters a restart loop. We wait on the abort signal.
417
+ await new Promise((resolve) => {
418
+ if (abortSignal?.aborted) { resolve(); return; }
419
+ const onAbort = () => { cleanup(); resolve(); };
420
+ const cleanup = () => { abortSignal?.removeEventListener("abort", onAbort); };
421
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
422
+ });
334
423
  },
335
424
 
336
425
  /**
@@ -340,9 +429,13 @@ _plugin.gateway = {
340
429
  const { accountId } = ctx;
341
430
  console.log(`[AICQ Channel] stopAccount called for ${accountId}`);
342
431
 
343
- if (runtime.serverClient && typeof runtime.serverClient.disconnect === "function") {
432
+ if (runtime.serverClient) {
344
433
  try {
345
- runtime.serverClient.disconnect();
434
+ if (typeof runtime.serverClient.stop === "function") {
435
+ runtime.serverClient.stop();
436
+ } else if (typeof runtime.serverClient.disconnect === "function") {
437
+ runtime.serverClient.disconnect();
438
+ }
346
439
  console.log("[AICQ Channel] WebSocket disconnected");
347
440
  } catch (e) {
348
441
  console.warn("[AICQ Channel] Disconnect error:", e.message);
@@ -362,6 +455,8 @@ _plugin.config = {
362
455
  /**
363
456
  * List all account IDs configured for this channel.
364
457
  * Resolves template variables like {{agent.id}}.
458
+ *
459
+ * Signature: (cfg: OpenClawConfig) => string[]
365
460
  */
366
461
  listAccountIds(cfg) {
367
462
  const section = (cfg.channels || {})["aicq-chat"] || {};
@@ -374,40 +469,65 @@ _plugin.config = {
374
469
 
375
470
  /**
376
471
  * Resolve an account from config. Reuses the setup resolver.
472
+ *
473
+ * Signature: (cfg: OpenClawConfig, accountId?: string | null) => ResolvedAccount
377
474
  */
378
475
  resolveAccount,
379
476
 
380
477
  /**
381
478
  * Lightweight account inspection.
479
+ *
480
+ * Signature: (cfg: OpenClawConfig, accountId?: string | null) => unknown
382
481
  */
383
482
  inspectAccount,
384
483
 
385
484
  /**
386
485
  * Default account ID for this channel.
387
- * Resolves {{agent.id}} to the actual agent ID.
486
+ *
487
+ * Signature: (cfg: OpenClawConfig) => string
388
488
  */
389
489
  defaultAccountId(cfg) {
390
490
  const section = (cfg.channels || {})["aicq-chat"] || {};
391
491
  if (section.accountId) {
392
492
  return resolveTemplateVar(cfg, section.accountId);
393
493
  }
394
- return "default";
494
+ return OPENCLAW_DEFAULT_AGENT_ID;
395
495
  },
396
496
 
397
497
  /**
398
- * Check if the channel is configured.
498
+ * Check if the account is enabled.
499
+ *
500
+ * IMPORTANT: OpenClaw calls this with (account, cfg) where `account`
501
+ * is the RESOLVED account object from resolveAccount(), not the config.
502
+ *
503
+ * Signature: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean
399
504
  */
400
- isConfigured(cfg) {
401
- const section = (cfg.channels || {})["aicq-chat"] || {};
402
- return Boolean(section.accountId);
505
+ isEnabled(account, cfg) {
506
+ return account.enabled !== false;
507
+ },
508
+
509
+ /**
510
+ * Check if the channel account is configured.
511
+ *
512
+ * IMPORTANT: OpenClaw calls this with (account, cfg) where `account`
513
+ * is the RESOLVED account object from resolveAccount(), not the config.
514
+ * Our old code had isConfigured(cfg) which received the account object
515
+ * as `cfg`, causing it to always return false — this was the root cause
516
+ * of the "not-running" bug.
517
+ *
518
+ * Signature: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean
519
+ */
520
+ isConfigured(account, cfg) {
521
+ return Boolean(account && account.accountId);
403
522
  },
404
523
 
405
524
  /**
406
525
  * Return the reason the channel is not configured.
526
+ *
527
+ * Signature: (account: ResolvedAccount, cfg: OpenClawConfig) => string
407
528
  */
408
- unconfiguredReason(cfg) {
409
- const section = (cfg.channels || {})["aicq-chat"] || {};
410
- if (!section.accountId) {
529
+ unconfiguredReason(account, cfg) {
530
+ if (!account || !account.accountId) {
411
531
  return "accountId is required — set channels.aicq-chat.accountId in openclaw.json";
412
532
  }
413
533
  return null;
@@ -415,14 +535,17 @@ _plugin.config = {
415
535
 
416
536
  /**
417
537
  * Describe the account for status surfaces.
538
+ *
539
+ * IMPORTANT: OpenClaw calls this with (account, cfg) where `account`
540
+ * is the RESOLVED account object, not the config.
541
+ *
542
+ * Signature: (account: ResolvedAccount, cfg: OpenClawConfig) => ChannelAccountSnapshot
418
543
  */
419
- describeAccount(cfg, accountId) {
420
- const section = (cfg.channels || {})["aicq-chat"] || {};
421
- const rawId = accountId || section.accountId || null;
544
+ describeAccount(account, cfg) {
422
545
  return {
423
- accountId: rawId ? resolveTemplateVar(cfg, rawId) : null,
546
+ accountId: account?.accountId || null,
424
547
  label: "AICQ Encrypted Chat",
425
- enabled: section.enabled !== false,
548
+ enabled: account?.enabled !== false,
426
549
  };
427
550
  },
428
551
  };
package/src/ui-routes.js CHANGED
File without changes