@ai-setting/roy-agent-core 1.7.56 → 1.7.58

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.
Files changed (108) hide show
  1. package/dist/config/index.js +8 -8
  2. package/dist/env/agent/agent-component.d.ts +11 -0
  3. package/dist/env/agent/index.js +14 -14
  4. package/dist/env/commands/index.js +8 -8
  5. package/dist/env/context/index.js +4 -4
  6. package/dist/env/debug/index.js +8 -8
  7. package/dist/env/error-reply.d.ts +4 -3
  8. package/dist/env/event-source/event-source-handlers.d.ts +28 -0
  9. package/dist/env/event-source/index.js +18 -17
  10. package/dist/env/hook/index.js +6 -6
  11. package/dist/env/index.js +44 -43
  12. package/dist/env/llm/index.js +9 -9
  13. package/dist/env/log-trace/index.js +8 -8
  14. package/dist/env/log-trace/opentelemetry/tracer-provider.d.ts +86 -1
  15. package/dist/env/mcp/index.js +9 -9
  16. package/dist/env/mcp/tool/index.js +2 -2
  17. package/dist/env/memory/index.js +9 -9
  18. package/dist/env/memory/plugin/index.js +2 -2
  19. package/dist/env/plugin/index.js +11 -9
  20. package/dist/env/plugin/lark-cli-notification-channel.d.ts +11 -2
  21. package/dist/env/prompt/index.js +8 -8
  22. package/dist/env/session/index.js +11 -11
  23. package/dist/env/session/storage/index.js +5 -5
  24. package/dist/env/skill/index.js +8 -8
  25. package/dist/env/task/delegate/delegate-tool.d.ts +26 -0
  26. package/dist/env/task/delegate/index.js +9 -9
  27. package/dist/env/task/index.js +18 -17
  28. package/dist/env/task/plugins/index.js +11 -10
  29. package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +109 -2
  30. package/dist/env/task/plugins/lark-card-state-store.d.ts +137 -0
  31. package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +154 -2
  32. package/dist/env/task/storage/index.js +4 -3
  33. package/dist/env/task/tools/index.js +4 -4
  34. package/dist/env/tool/built-in/index.js +4 -4
  35. package/dist/env/tool/index.js +11 -11
  36. package/dist/env/workflow/engine/index.js +13 -13
  37. package/dist/env/workflow/hooks/index.js +8 -8
  38. package/dist/env/workflow/index.js +23 -23
  39. package/dist/env/workflow/nodes/index.js +3 -3
  40. package/dist/env/workflow/plugins/index.js +9 -9
  41. package/dist/env/workflow/service/index.js +3 -3
  42. package/dist/env/workflow/storage/index.js +4 -4
  43. package/dist/env/workflow/tools/index.js +15 -15
  44. package/dist/env/workflow/utils/index.js +5 -5
  45. package/dist/index.js +58 -57
  46. package/dist/shared/@ai-setting/{roy-agent-core-0b3fdnn3.js → roy-agent-core-029r8kdw.js} +4 -4
  47. package/dist/shared/@ai-setting/{roy-agent-core-nra8vfrs.js → roy-agent-core-08njdcpj.js} +1 -1
  48. package/dist/shared/@ai-setting/{roy-agent-core-40xzbjtm.js → roy-agent-core-0kn3zt1t.js} +2 -2
  49. package/dist/shared/@ai-setting/{roy-agent-core-nhk0dsxr.js → roy-agent-core-0yn7a78e.js} +14 -14
  50. package/dist/shared/@ai-setting/{roy-agent-core-tbrn9wdm.js → roy-agent-core-14bq77wr.js} +1 -1
  51. package/dist/shared/@ai-setting/{roy-agent-core-qyqnmz8c.js → roy-agent-core-20m8je6e.js} +1 -1
  52. package/dist/shared/@ai-setting/{roy-agent-core-zmeka63k.js → roy-agent-core-2ctyjy62.js} +1 -1
  53. package/dist/shared/@ai-setting/{roy-agent-core-heft3j4c.js → roy-agent-core-2fk8c9ax.js} +3 -3
  54. package/dist/shared/@ai-setting/{roy-agent-core-8cgtwytv.js → roy-agent-core-4096f575.js} +4 -4
  55. package/dist/shared/@ai-setting/{roy-agent-core-yg247mk4.js → roy-agent-core-47s81t6g.js} +2 -2
  56. package/dist/shared/@ai-setting/{roy-agent-core-q7zksbw0.js → roy-agent-core-5f7myqnb.js} +5 -5
  57. package/dist/shared/@ai-setting/{roy-agent-core-vmg30n5x.js → roy-agent-core-6bradb0j.js} +15 -10
  58. package/dist/shared/@ai-setting/{roy-agent-core-zjttt2y4.js → roy-agent-core-7nb23tdd.js} +1 -1
  59. package/dist/shared/@ai-setting/roy-agent-core-7q4wdze2.js +7 -0
  60. package/dist/shared/@ai-setting/{roy-agent-core-d4jpn06f.js → roy-agent-core-84rs0c8d.js} +7 -7
  61. package/dist/shared/@ai-setting/{roy-agent-core-c6at31ra.js → roy-agent-core-84xtww7d.js} +3 -3
  62. package/dist/shared/@ai-setting/{roy-agent-core-202v00x4.js → roy-agent-core-89sxsrc7.js} +1 -1
  63. package/dist/shared/@ai-setting/{roy-agent-core-ezsyyjxw.js → roy-agent-core-8bngppw0.js} +580 -83
  64. package/dist/shared/@ai-setting/{roy-agent-core-b599jgyd.js → roy-agent-core-8p63s836.js} +1 -1
  65. package/dist/shared/@ai-setting/{roy-agent-core-e0wbq0qq.js → roy-agent-core-991pk4ss.js} +11 -11
  66. package/dist/shared/@ai-setting/{roy-agent-core-5rxs0ra3.js → roy-agent-core-9zhkdg6w.js} +2 -2
  67. package/dist/shared/@ai-setting/{roy-agent-core-gn8f4rhs.js → roy-agent-core-baaa4wtp.js} +2 -2
  68. package/dist/shared/@ai-setting/{roy-agent-core-nntphgww.js → roy-agent-core-c36335nw.js} +146 -8
  69. package/dist/shared/@ai-setting/{roy-agent-core-tc7gw38m.js → roy-agent-core-c7w2h47a.js} +3 -3
  70. package/dist/shared/@ai-setting/{roy-agent-core-j5c3chtn.js → roy-agent-core-cyefe8z5.js} +1 -1
  71. package/dist/shared/@ai-setting/{roy-agent-core-n3b7ztms.js → roy-agent-core-d75t8gea.js} +1 -1
  72. package/dist/shared/@ai-setting/{roy-agent-core-yv9x4wmg.js → roy-agent-core-e1w17tht.js} +2 -2
  73. package/dist/shared/@ai-setting/{roy-agent-core-f8sp1txg.js → roy-agent-core-e250355z.js} +3 -3
  74. package/dist/shared/@ai-setting/{roy-agent-core-9nv586rw.js → roy-agent-core-eanq305k.js} +22 -16
  75. package/dist/shared/@ai-setting/{roy-agent-core-6qgq7h1g.js → roy-agent-core-em4hks9t.js} +4 -4
  76. package/dist/shared/@ai-setting/{roy-agent-core-0bmk44ye.js → roy-agent-core-en2xqxs9.js} +5 -5
  77. package/dist/shared/@ai-setting/{roy-agent-core-3kz6934b.js → roy-agent-core-fbjd7bt6.js} +3 -7
  78. package/dist/shared/@ai-setting/{roy-agent-core-98aa9r0x.js → roy-agent-core-fw6k4w1h.js} +6 -6
  79. package/dist/shared/@ai-setting/{roy-agent-core-2044fbsd.js → roy-agent-core-fy4mcj6x.js} +40 -47
  80. package/dist/shared/@ai-setting/{roy-agent-core-ns3pbyz4.js → roy-agent-core-ge87xbas.js} +1 -1
  81. package/dist/shared/@ai-setting/{roy-agent-core-r9qe0ghe.js → roy-agent-core-geh6j7bw.js} +2 -2
  82. package/dist/shared/@ai-setting/{roy-agent-core-wafk25ff.js → roy-agent-core-gksw1gjb.js} +1 -1
  83. package/dist/shared/@ai-setting/{roy-agent-core-3dkzqqsd.js → roy-agent-core-hefarcp0.js} +1 -1
  84. package/dist/shared/@ai-setting/{roy-agent-core-5gw6w424.js → roy-agent-core-hs41gx9w.js} +5 -5
  85. package/dist/shared/@ai-setting/{roy-agent-core-d14h726p.js → roy-agent-core-jjhw9am6.js} +4 -4
  86. package/dist/shared/@ai-setting/{roy-agent-core-5psjwd08.js → roy-agent-core-jjxdmedn.js} +2 -2
  87. package/dist/shared/@ai-setting/{roy-agent-core-bwzy00t2.js → roy-agent-core-kq9pz2a7.js} +4 -4
  88. package/dist/shared/@ai-setting/{roy-agent-core-8t7wny6v.js → roy-agent-core-m68z3pw1.js} +1 -1
  89. package/dist/shared/@ai-setting/{roy-agent-core-16t0zfbk.js → roy-agent-core-m97jn2ra.js} +1 -1
  90. package/dist/shared/@ai-setting/{roy-agent-core-kzg1jwt8.js → roy-agent-core-n0p0hwgd.js} +1 -1
  91. package/dist/shared/@ai-setting/{roy-agent-core-xcq04fq0.js → roy-agent-core-psg6f1v6.js} +6 -6
  92. package/dist/shared/@ai-setting/{roy-agent-core-4zy2v3br.js → roy-agent-core-q4gdfypk.js} +3 -3
  93. package/dist/shared/@ai-setting/{roy-agent-core-m8n0dffe.js → roy-agent-core-rcgvmtms.js} +1 -1
  94. package/dist/shared/@ai-setting/{roy-agent-core-8seq65bp.js → roy-agent-core-rt115qfc.js} +44 -1
  95. package/dist/shared/@ai-setting/{roy-agent-core-jszgaevt.js → roy-agent-core-syyw58p9.js} +2 -2
  96. package/dist/shared/@ai-setting/{roy-agent-core-3mc6vmvj.js → roy-agent-core-t4x089js.js} +1 -1
  97. package/dist/shared/@ai-setting/{roy-agent-core-qjjf4tjb.js → roy-agent-core-t64nmayv.js} +2 -2
  98. package/dist/shared/@ai-setting/{roy-agent-core-833t16n1.js → roy-agent-core-txwptxk1.js} +3 -3
  99. package/dist/shared/@ai-setting/{roy-agent-core-w7hnjhvx.js → roy-agent-core-tze5596n.js} +3 -3
  100. package/dist/shared/@ai-setting/{roy-agent-core-pjxa78mp.js → roy-agent-core-vath15kt.js} +6 -6
  101. package/dist/shared/@ai-setting/{roy-agent-core-e677am8e.js → roy-agent-core-vgdhxbx3.js} +2 -2
  102. package/dist/shared/@ai-setting/{roy-agent-core-pemxr5n4.js → roy-agent-core-vh0qzb24.js} +2 -2
  103. package/dist/shared/@ai-setting/{roy-agent-core-v2188rnk.js → roy-agent-core-w93z6a42.js} +8 -8
  104. package/dist/shared/@ai-setting/{roy-agent-core-97eyghsp.js → roy-agent-core-ygn5tzt1.js} +2 -2
  105. package/dist/shared/@ai-setting/{roy-agent-core-1pnfkspm.js → roy-agent-core-yphwdm9x.js} +2 -2
  106. package/dist/shared/@ai-setting/{roy-agent-core-gngjkw3b.js → roy-agent-core-ytd5eztz.js} +1 -1
  107. package/dist/utils/temp-file-manager/index.js +2 -2
  108. package/package.json +1 -1
@@ -1,6 +1,10 @@
1
+ import {
2
+ createCompatibleDatabase,
3
+ init_sqlite_task_store
4
+ } from "./roy-agent-core-fbjd7bt6.js";
1
5
  import {
2
6
  getSharedEventStore
3
- } from "./roy-agent-core-zmeka63k.js";
7
+ } from "./roy-agent-core-2ctyjy62.js";
4
8
  import {
5
9
  TaskHookPoints
6
10
  } from "./roy-agent-core-1pg0fepg.js";
@@ -10,31 +14,31 @@ import {
10
14
  import {
11
15
  LarkCardPatchNotifier,
12
16
  humanizeValue
13
- } from "./roy-agent-core-nntphgww.js";
17
+ } from "./roy-agent-core-c36335nw.js";
14
18
  import {
15
19
  validateWorkflowDefinition
16
- } from "./roy-agent-core-202v00x4.js";
20
+ } from "./roy-agent-core-89sxsrc7.js";
17
21
  import {
18
22
  enqueueToAddFakeUserMessage
19
- } from "./roy-agent-core-wafk25ff.js";
23
+ } from "./roy-agent-core-gksw1gjb.js";
20
24
  import {
21
25
  BasePlugin
22
26
  } from "./roy-agent-core-az13yzmc.js";
23
27
  import {
24
28
  init_context
25
- } from "./roy-agent-core-16t0zfbk.js";
29
+ } from "./roy-agent-core-m97jn2ra.js";
26
30
  import {
27
31
  getEnvContextOrEmpty
28
- } from "./roy-agent-core-jszgaevt.js";
32
+ } from "./roy-agent-core-syyw58p9.js";
29
33
  import {
30
34
  TracedAs,
31
35
  init_decorator,
32
36
  wrapFunction
33
- } from "./roy-agent-core-b599jgyd.js";
37
+ } from "./roy-agent-core-8p63s836.js";
34
38
  import {
35
39
  createLogger,
36
40
  init_logger
37
- } from "./roy-agent-core-8seq65bp.js";
41
+ } from "./roy-agent-core-rt115qfc.js";
38
42
  import {
39
43
  __legacyDecorateClassTS,
40
44
  __require
@@ -299,11 +303,11 @@ ${prompt}`;
299
303
  }
300
304
  if (!this._workflowRepo) {
301
305
  try {
302
- const sqlite = await import("./roy-agent-core-8cgtwytv.js");
306
+ const sqlite = await import("./roy-agent-core-4096f575.js");
303
307
  this.ensureDataDir();
304
308
  const db = sqlite.getDatabase();
305
309
  sqlite.initializeTables();
306
- const { WorkflowRepository } = await import("./roy-agent-core-d14h726p.js");
310
+ const { WorkflowRepository } = await import("./roy-agent-core-jjhw9am6.js");
307
311
  this._workflowRepo = new WorkflowRepository(db);
308
312
  return this._workflowRepo;
309
313
  } catch {
@@ -377,6 +381,179 @@ init_decorator();
377
381
  init_logger();
378
382
  init_context();
379
383
 
384
+ // src/env/task/plugins/lark-card-state-store.ts
385
+ init_logger();
386
+ init_sqlite_task_store();
387
+ import { join } from "path";
388
+ var logger3 = createLogger("lark-cli:card-state");
389
+
390
+ class MemoryLarkCardStateStore {
391
+ records = new Map;
392
+ upsert(state) {
393
+ this.records.set(state.taskId, { ...state });
394
+ }
395
+ get(taskId) {
396
+ const r = this.records.get(taskId);
397
+ return r ? { ...r } : undefined;
398
+ }
399
+ list() {
400
+ return [...this.records.values()].map((r) => ({ ...r }));
401
+ }
402
+ markPinned(taskId, pinned) {
403
+ const r = this.records.get(taskId);
404
+ if (!r)
405
+ return;
406
+ r.pinned = pinned;
407
+ r.updatedAt = Date.now();
408
+ }
409
+ markPaused(taskId) {
410
+ const r = this.records.get(taskId);
411
+ if (!r)
412
+ return;
413
+ r.status = "paused";
414
+ r.updatedAt = Date.now();
415
+ }
416
+ markReopened(taskId) {
417
+ const r = this.records.get(taskId);
418
+ if (!r)
419
+ return;
420
+ r.status = "open";
421
+ delete r.closedAt;
422
+ r.updatedAt = Date.now();
423
+ }
424
+ markClosed(taskId) {
425
+ const r = this.records.get(taskId);
426
+ if (!r)
427
+ return;
428
+ const now = Date.now();
429
+ r.status = "closed";
430
+ r.closedAt = now;
431
+ r.updatedAt = now;
432
+ }
433
+ delete(taskId) {
434
+ this.records.delete(taskId);
435
+ }
436
+ close() {
437
+ this.records.clear();
438
+ }
439
+ }
440
+ function rowToState(row) {
441
+ const state = {
442
+ taskId: row.task_id,
443
+ messageId: row.message_id,
444
+ chatId: row.chat_id,
445
+ pinned: row.pinned === 1,
446
+ status: row.status ?? "open",
447
+ createdAt: row.created_at,
448
+ updatedAt: row.updated_at
449
+ };
450
+ if (row.profile)
451
+ state.profile = row.profile;
452
+ if (row.closed_at !== null && row.closed_at !== undefined) {
453
+ state.closedAt = row.closed_at;
454
+ }
455
+ if (row.timeline)
456
+ state.timeline = row.timeline;
457
+ return state;
458
+ }
459
+
460
+ class SqliteLarkCardStateStore {
461
+ db;
462
+ constructor(dbPath) {
463
+ if (dbPath !== ":memory:") {
464
+ const { mkdirSync } = __require("fs");
465
+ const dir = __require("path").dirname(dbPath);
466
+ try {
467
+ mkdirSync(dir, { recursive: true });
468
+ } catch {}
469
+ }
470
+ this.db = createCompatibleDatabase(dbPath);
471
+ try {
472
+ this.db.run("PRAGMA journal_mode = WAL");
473
+ this.db.run("PRAGMA busy_timeout = 5000");
474
+ this.db.run("PRAGMA synchronous = NORMAL");
475
+ } catch {}
476
+ this.db.run(`
477
+ CREATE TABLE IF NOT EXISTS lark_card (
478
+ task_id INTEGER PRIMARY KEY,
479
+ message_id TEXT NOT NULL,
480
+ chat_id TEXT NOT NULL,
481
+ profile TEXT,
482
+ pinned INTEGER NOT NULL DEFAULT 0,
483
+ status TEXT NOT NULL DEFAULT 'open',
484
+ created_at INTEGER NOT NULL,
485
+ updated_at INTEGER NOT NULL,
486
+ closed_at INTEGER,
487
+ timeline TEXT
488
+ )
489
+ `);
490
+ try {
491
+ this.db.run("ALTER TABLE lark_card ADD COLUMN timeline TEXT");
492
+ } catch {}
493
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_lark_card_status ON lark_card(status)");
494
+ }
495
+ upsert(state) {
496
+ this.db.prepare(`INSERT INTO lark_card
497
+ (task_id, message_id, chat_id, profile, pinned, status, created_at, updated_at, closed_at, timeline)
498
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
499
+ ON CONFLICT(task_id) DO UPDATE SET
500
+ message_id = excluded.message_id,
501
+ chat_id = excluded.chat_id,
502
+ profile = excluded.profile,
503
+ pinned = excluded.pinned,
504
+ status = excluded.status,
505
+ updated_at = excluded.updated_at,
506
+ closed_at = excluded.closed_at,
507
+ timeline = excluded.timeline`).run(state.taskId, state.messageId, state.chatId, state.profile ?? null, state.pinned ? 1 : 0, state.status, state.createdAt, state.updatedAt, state.closedAt ?? null, state.timeline ?? null);
508
+ }
509
+ get(taskId) {
510
+ const row = this.db.prepare("SELECT * FROM lark_card WHERE task_id = ?").get(taskId);
511
+ return row ? rowToState(row) : undefined;
512
+ }
513
+ list() {
514
+ const rows = this.db.prepare("SELECT * FROM lark_card ORDER BY task_id ASC").all();
515
+ return rows.map(rowToState);
516
+ }
517
+ markPinned(taskId, pinned) {
518
+ this.db.prepare("UPDATE lark_card SET pinned = ?, updated_at = ? WHERE task_id = ?").run(pinned ? 1 : 0, Date.now(), taskId);
519
+ }
520
+ markPaused(taskId) {
521
+ this.db.prepare("UPDATE lark_card SET status = 'paused', updated_at = ? WHERE task_id = ?").run(Date.now(), taskId);
522
+ }
523
+ markReopened(taskId) {
524
+ this.db.prepare("UPDATE lark_card SET status = 'open', closed_at = NULL, updated_at = ? WHERE task_id = ?").run(Date.now(), taskId);
525
+ }
526
+ markClosed(taskId) {
527
+ const now = Date.now();
528
+ this.db.prepare("UPDATE lark_card SET status = 'closed', closed_at = ?, updated_at = ? WHERE task_id = ?").run(now, now, taskId);
529
+ }
530
+ delete(taskId) {
531
+ this.db.prepare("DELETE FROM lark_card WHERE task_id = ?").run(taskId);
532
+ }
533
+ close() {
534
+ try {
535
+ this.db.close?.();
536
+ } catch {}
537
+ }
538
+ }
539
+ function getDefaultLarkCardDbPath() {
540
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
541
+ return join(home, ".local", "share", "roy-agent", "lark-cards.db");
542
+ }
543
+ function createLarkCardStateStore(dbPath) {
544
+ const isTestEnv = "BUN_TEST_WORKER_ID" in process.env;
545
+ if (!dbPath && !process.env.ROY_AGENT_LARK_CARD_DB && isTestEnv) {
546
+ return new MemoryLarkCardStateStore;
547
+ }
548
+ const path = dbPath ?? process.env.ROY_AGENT_LARK_CARD_DB ?? getDefaultLarkCardDbPath();
549
+ try {
550
+ return new SqliteLarkCardStateStore(path);
551
+ } catch (err) {
552
+ logger3.warn(`LarkCardStateStore: sqlite unavailable at ${path} (${err.message}) ` + `→ falling back to in-memory (卡片仍可发,但重启后无法 patch 旧卡)`);
553
+ return new MemoryLarkCardStateStore;
554
+ }
555
+ }
556
+
380
557
  // src/env/task/plugins/card-chat-routing.ts
381
558
  var CTX_FIELD_LARK_PER_TASK_CHAT_ID = "larkPerTaskChatId";
382
559
  function extractPerTaskChatId(contextStr) {
@@ -454,7 +631,7 @@ function decideCardChatTarget(input) {
454
631
  }
455
632
 
456
633
  // src/env/task/plugins/lark-cli-task-notify-plugin.ts
457
- var logger3 = createLogger("lark-cli:task-notify");
634
+ var logger4 = createLogger("lark-cli:task-notify");
458
635
  var CTX_FIELD_CHAT_ID = "larkPerTaskChatId";
459
636
  var CTX_FIELD_CHAT_PROFILE = "larkPerTaskChatProfile";
460
637
  function parseLarkPerTaskChatId(ctxStr) {
@@ -479,6 +656,23 @@ function parseLarkPerTaskChatProfile(ctxStr) {
479
656
  return;
480
657
  }
481
658
  }
659
+ function isTerminalTaskStatus(status) {
660
+ return status === "completed" || status === "cancelled" || status === "failed" || status === "timeout" || status === "archived";
661
+ }
662
+ function mapTaskStatusToCardStatus(taskStatus) {
663
+ switch (taskStatus) {
664
+ case "cancelled":
665
+ return "cancelled";
666
+ case "paused":
667
+ case "archived":
668
+ return "stopped";
669
+ case "failed":
670
+ case "timeout":
671
+ return "failed";
672
+ default:
673
+ return "completed";
674
+ }
675
+ }
482
676
  var NotifyType;
483
677
  ((NotifyType2) => {
484
678
  NotifyType2["TASK_CREATED"] = "task_created";
@@ -497,6 +691,8 @@ class LarkCliTaskNotifyHook {
497
691
  config;
498
692
  cardNotifier;
499
693
  cardMessageIds = new Map;
694
+ cardStateStore;
695
+ restoredTaskIds = new Set;
500
696
  lastNotifyTimestamps = new Map;
501
697
  cardRoutingPorts;
502
698
  static CARD_START_FAILED_SENTINEL = "__card_start_failed__";
@@ -520,11 +716,12 @@ class LarkCliTaskNotifyHook {
520
716
  };
521
717
  if (this.config.cardMode !== false) {
522
718
  this.cardNotifier = config.cardNotifier ?? new LarkCardPatchNotifier;
719
+ this.cardStateStore = config.cardStateStore ?? createLarkCardStateStore();
523
720
  }
524
721
  }
525
722
  setCardRoutingPorts(ports) {
526
723
  this.cardRoutingPorts = ports;
527
- logger3.debug(`CardRoutingPorts injected: getTask=${typeof ports.getTask}`);
724
+ logger4.debug(`CardRoutingPorts injected: getTask=${typeof ports.getTask}`);
528
725
  }
529
726
  hasCardRoutingPorts() {
530
727
  return this.cardRoutingPorts !== undefined;
@@ -537,7 +734,7 @@ class LarkCliTaskNotifyHook {
537
734
  if (this.pluginName && this.sourceId) {
538
735
  const key = `${this.sourceId}:${this.pluginName}`;
539
736
  if (env.pluginEnabled && env.pluginEnabled[key] !== true) {
540
- logger3.debug(`Plugin not enabled for source: ${key}`);
737
+ logger4.debug(`Plugin not enabled for source: ${key}`);
541
738
  return;
542
739
  }
543
740
  }
@@ -554,6 +751,9 @@ class LarkCliTaskNotifyHook {
554
751
  case TaskHookPoints.OPERATION_AFTER_CREATE:
555
752
  await this.onAfterOperationCreate(ctx);
556
753
  break;
754
+ case TaskHookPoints.AFTER_DELETE:
755
+ await this.onAfterDelete(ctx);
756
+ break;
557
757
  }
558
758
  }
559
759
  async onAfterCreate(ctx) {
@@ -569,13 +769,19 @@ class LarkCliTaskNotifyHook {
569
769
  if (!data?.task)
570
770
  return;
571
771
  const { task, changes } = data;
772
+ const terminalCardStatus = this.cardStatusOf(task, changes);
773
+ if (terminalCardStatus) {
774
+ const notifyType2 = terminalCardStatus === "completed" || terminalCardStatus === "cancelled" ? "task_completed" /* TASK_COMPLETED */ : "task_updated" /* TASK_UPDATED */;
775
+ if (!this.shouldNotify(notifyType2))
776
+ return;
777
+ if (!this.isCardModeEnabled())
778
+ return;
779
+ await this.sendCardFinish(task);
780
+ return;
781
+ }
572
782
  let notifyType;
573
- if (changes?.status === "completed" || changes?.progress === 100) {
574
- notifyType = "task_completed" /* TASK_COMPLETED */;
575
- } else if (changes?.status) {
783
+ if (changes?.status) {
576
784
  notifyType = "task_updated" /* TASK_UPDATED */;
577
- } else if (task.status === "completed" || task.progress === 100) {
578
- notifyType = "task_completed" /* TASK_COMPLETED */;
579
785
  } else if (changes && Object.keys(changes).length > 0) {
580
786
  notifyType = "task_updated" /* TASK_UPDATED */;
581
787
  } else {
@@ -585,11 +791,27 @@ class LarkCliTaskNotifyHook {
585
791
  return;
586
792
  if (!this.isCardModeEnabled())
587
793
  return;
588
- if (notifyType === "task_completed" /* TASK_COMPLETED */ || task.status === "completed") {
589
- await this.sendCardFinish(task);
590
- } else {
591
- await this.sendCardUpdate(task, changes);
794
+ await this.sendCardUpdate(task, changes);
795
+ }
796
+ cardStatusOf(task, changes) {
797
+ const changedStatus = changes?.status;
798
+ const effective = String(changedStatus ?? task.status ?? "");
799
+ switch (effective) {
800
+ case "completed":
801
+ return "completed";
802
+ case "cancelled":
803
+ return "cancelled";
804
+ case "paused":
805
+ return "stopped";
806
+ case "archived":
807
+ return "stopped";
808
+ case "failed":
809
+ case "timeout":
810
+ return "failed";
811
+ default:
812
+ break;
592
813
  }
814
+ return;
593
815
  }
594
816
  async onAfterComplete(ctx) {
595
817
  const data = ctx.data;
@@ -600,6 +822,52 @@ class LarkCliTaskNotifyHook {
600
822
  return;
601
823
  await this.sendCardFinish(task);
602
824
  }
825
+ async onAfterDelete(ctx) {
826
+ const data = ctx.data;
827
+ const taskId = data?.id;
828
+ if (data?.deleted !== true || typeof taskId !== "number")
829
+ return;
830
+ if (!this.shouldNotify("task_completed" /* TASK_COMPLETED */))
831
+ return;
832
+ if (!this.isCardModeEnabled())
833
+ return;
834
+ if (!this.cardNotifier)
835
+ return;
836
+ let record;
837
+ try {
838
+ record = this.cardStateStore?.get(taskId);
839
+ } catch (err) {
840
+ logger4.warn(`onAfterDelete: 读取卡片记录失败 task #${taskId}: ${err.message}`);
841
+ }
842
+ const messageId = record?.messageId || this.cardMessageIds.get(taskId);
843
+ if (!messageId) {
844
+ logger4.debug(`onAfterDelete: task #${taskId} 无卡片记录,skip(不发新卡)`);
845
+ return;
846
+ }
847
+ this.restoreCardState(taskId);
848
+ const profile = record?.profile;
849
+ const chatId = record?.chatId;
850
+ try {
851
+ await this.cardNotifier.finishTask({
852
+ messageId,
853
+ profile,
854
+ chatId,
855
+ status: "deleted",
856
+ finalStep: "任务已取消(删除)"
857
+ });
858
+ } catch (err) {
859
+ logger4.warn(`onAfterDelete: patch 卡片失败 task #${taskId} message=${messageId}: ${err.message}`);
860
+ }
861
+ try {
862
+ await this.cardNotifier.unpinMessage(messageId, profile);
863
+ } catch (err) {
864
+ logger4.warn(`onAfterDelete: unpin 失败 task #${taskId} message=${messageId}: ${err.message}`);
865
+ }
866
+ this.persistClosed(taskId);
867
+ this.persistPinned(taskId, false);
868
+ this.cardMessageIds.delete(taskId);
869
+ logger4.debug(`onAfterDelete: task #${taskId} 卡片已收尾 status=deleted message=${messageId}`);
870
+ }
603
871
  async onAfterOperationCreate(ctx) {
604
872
  const { operation } = ctx.data;
605
873
  if (!operation)
@@ -610,9 +878,14 @@ class LarkCliTaskNotifyHook {
610
878
  return;
611
879
  if (!this.isCardModeEnabled())
612
880
  return;
881
+ const persisted = this.restoreCardState(operation.taskId);
613
882
  const messageId = this.cardMessageIds.get(operation.taskId);
614
- if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
615
- logger3.debug(`onAfterOperationCreate: no card for task #${operation.taskId}, skip`);
883
+ if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL || messageId === LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL) {
884
+ logger4.debug(`onAfterOperationCreate: no usable card for task #${operation.taskId}, skip`);
885
+ return;
886
+ }
887
+ if (persisted && persisted.status !== "open") {
888
+ logger4.debug(`onAfterOperationCreate: task #${operation.taskId} card status=${persisted.status}, skip`);
616
889
  return;
617
890
  }
618
891
  const { milestones, processSteps } = this.deriveTimelineFromOperation(operation);
@@ -629,12 +902,186 @@ class LarkCliTaskNotifyHook {
629
902
  processSteps
630
903
  });
631
904
  } catch (err) {
632
- logger3.error(`sendCardOperation failed for task #${operation.taskId}: ${err.message}`);
905
+ logger4.error(`sendCardOperation failed for task #${operation.taskId}: ${err.message}`);
906
+ }
907
+ }
908
+ restoreCardState(taskId) {
909
+ if (!this.cardStateStore)
910
+ return;
911
+ try {
912
+ const rec = this.cardStateStore.get(taskId);
913
+ if (!rec)
914
+ return;
915
+ if (!this.restoredTaskIds.has(taskId)) {
916
+ this.restoredTaskIds.add(taskId);
917
+ if (rec.status !== "closed" && rec.messageId) {
918
+ this.cardMessageIds.set(taskId, rec.messageId);
919
+ this.hydrateNotifierState(rec);
920
+ logger4.debug(`restoreCardState: task #${taskId} ← 持久层 message ${rec.messageId} ` + `(status=${rec.status}, pinned=${rec.pinned}, timeline=${rec.timeline ? "yes" : "no"})`);
921
+ }
922
+ }
923
+ return rec;
924
+ } catch (err) {
925
+ logger4.warn(`restoreCardState failed for task #${taskId}: ${err.message}`);
926
+ return;
927
+ }
928
+ }
929
+ hydrateNotifierState(rec) {
930
+ if (!this.cardNotifier || !rec.timeline)
931
+ return;
932
+ try {
933
+ const snapshot = JSON.parse(rec.timeline);
934
+ this.cardNotifier.hydrateState?.(rec.messageId, snapshot);
935
+ } catch (err) {
936
+ logger4.warn(`hydrateNotifierState failed for task #${rec.taskId}: ${err.message}`);
937
+ }
938
+ }
939
+ snapshotTimeline(state) {
940
+ if (!this.cardNotifier || !state.messageId)
941
+ return state;
942
+ try {
943
+ const snap = this.cardNotifier.exportState?.(state.messageId);
944
+ if (!snap)
945
+ return state;
946
+ return { ...state, timeline: JSON.stringify(snap) };
947
+ } catch (err) {
948
+ logger4.warn(`snapshotTimeline failed for task #${state.taskId}: ${err.message}`);
949
+ return state;
950
+ }
951
+ }
952
+ persistCardState(state, includeTimeline = false) {
953
+ if (!this.cardStateStore)
954
+ return;
955
+ try {
956
+ const payload = includeTimeline ? this.snapshotTimeline(state) : state;
957
+ this.cardStateStore.upsert(payload);
958
+ } catch (err) {
959
+ logger4.warn(`persistCardState failed for task #${state.taskId}: ${err.message}`);
960
+ }
961
+ }
962
+ persistPinned(taskId, pinned) {
963
+ if (!this.cardStateStore)
964
+ return;
965
+ try {
966
+ this.cardStateStore.markPinned(taskId, pinned);
967
+ } catch (err) {
968
+ logger4.warn(`persistPinned failed for task #${taskId}: ${err.message}`);
969
+ }
970
+ }
971
+ persistPaused(taskId) {
972
+ if (!this.cardStateStore)
973
+ return;
974
+ try {
975
+ this.cardStateStore.markPaused(taskId);
976
+ } catch (err) {
977
+ logger4.warn(`persistPaused failed for task #${taskId}: ${err.message}`);
978
+ }
979
+ }
980
+ dropStaleMessageId(taskId) {
981
+ if (!this.cardStateStore)
982
+ return;
983
+ try {
984
+ const existing = this.cardStateStore.get(taskId);
985
+ if (!existing)
986
+ return;
987
+ this.cardStateStore.upsert({
988
+ ...existing,
989
+ messageId: "",
990
+ pinned: false,
991
+ updatedAt: Date.now()
992
+ });
993
+ } catch (err) {
994
+ logger4.warn(`dropStaleMessageId failed for task #${taskId}: ${err.message}`);
995
+ }
996
+ }
997
+ persistReopened(taskId) {
998
+ if (!this.cardStateStore)
999
+ return;
1000
+ try {
1001
+ this.cardStateStore.markReopened(taskId);
1002
+ } catch (err) {
1003
+ logger4.warn(`persistReopened failed for task #${taskId}: ${err.message}`);
1004
+ }
1005
+ }
1006
+ persistClosed(taskId) {
1007
+ if (!this.cardStateStore)
1008
+ return;
1009
+ try {
1010
+ this.cardStateStore.markClosed(taskId);
1011
+ } catch (err) {
1012
+ logger4.warn(`persistClosed failed for task #${taskId}: ${err.message}`);
1013
+ }
1014
+ }
1015
+ refreshTimeline(taskId) {
1016
+ if (!this.cardStateStore || !this.cardNotifier)
1017
+ return;
1018
+ try {
1019
+ const rec = this.cardStateStore.get(taskId);
1020
+ if (!rec || !rec.messageId)
1021
+ return;
1022
+ this.cardStateStore.upsert(this.snapshotTimeline(rec));
1023
+ } catch (err) {
1024
+ logger4.warn(`refreshTimeline failed for task #${taskId}: ${err.message}`);
1025
+ }
1026
+ }
1027
+ async repinAfterResume(task, persisted) {
1028
+ if (!this.cardNotifier || typeof this.cardNotifier.pinMessage !== "function") {
1029
+ return;
1030
+ }
1031
+ if (!persisted.messageId)
1032
+ return;
1033
+ if (persisted.pinned)
1034
+ return;
1035
+ const ctx = getEnvContextOrEmpty();
1036
+ const profile = this.resolveProfile(task, ctx);
1037
+ try {
1038
+ await this.cardNotifier.pinMessage(persisted.messageId, profile);
1039
+ this.persistPinned(task.id, true);
1040
+ logger4.debug(`repinAfterResume: task #${task.id} 卡片 ${persisted.messageId} 已重新置顶`);
1041
+ } catch (err) {
1042
+ logger4.warn(`repinAfterResume failed (non-fatal) for task #${task.id}: ${err.message}`);
1043
+ }
1044
+ }
1045
+ buildCardState(task, messageId, chatId, profile, pinned) {
1046
+ let existing;
1047
+ try {
1048
+ existing = this.cardStateStore?.get(task.id);
1049
+ } catch (err) {
1050
+ logger4.warn(`buildCardState: read existing failed for task #${task.id}: ${err.message}`);
1051
+ }
1052
+ const now = Date.now();
1053
+ return {
1054
+ taskId: task.id,
1055
+ messageId,
1056
+ chatId,
1057
+ profile,
1058
+ pinned,
1059
+ status: "open",
1060
+ createdAt: existing?.createdAt ?? now,
1061
+ updatedAt: now
1062
+ };
1063
+ }
1064
+ getCardState(taskId) {
1065
+ if (!this.cardStateStore)
1066
+ return;
1067
+ try {
1068
+ return this.cardStateStore.get(taskId);
1069
+ } catch {
1070
+ return;
1071
+ }
1072
+ }
1073
+ listCardStates() {
1074
+ if (!this.cardStateStore)
1075
+ return [];
1076
+ try {
1077
+ return this.cardStateStore.list();
1078
+ } catch {
1079
+ return [];
633
1080
  }
634
1081
  }
635
1082
  isCardModeEnabled() {
636
1083
  if (this.config.cardMode === false || !this.cardNotifier) {
637
- logger3.debug("cardMode=false (legacy opt-out), skip task notification (no text fallback)");
1084
+ logger4.debug("cardMode=false (legacy opt-out), skip task notification (no text fallback)");
638
1085
  return false;
639
1086
  }
640
1087
  return true;
@@ -696,9 +1143,15 @@ class LarkCliTaskNotifyHook {
696
1143
  async sendCardStart(task) {
697
1144
  if (!this.cardNotifier)
698
1145
  return;
1146
+ const persisted = this.restoreCardState(task.id);
1147
+ if (persisted?.status === "closed") {
1148
+ this.cardMessageIds.delete(task.id);
1149
+ } else if (persisted?.status === "paused") {
1150
+ this.persistReopened(task.id);
1151
+ }
699
1152
  const claim = this.cardMessageIds.get(task.id);
700
1153
  if (claim) {
701
- logger3.debug(`sendCardStart: task #${task.id} 已有首卡(${claim})→ 跳过重复发卡`);
1154
+ logger4.debug(`sendCardStart: task #${task.id} 已有首卡(${claim})→ 跳过重复发卡`);
702
1155
  return;
703
1156
  }
704
1157
  this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL);
@@ -713,7 +1166,7 @@ class LarkCliTaskNotifyHook {
713
1166
  const profile = target.profile;
714
1167
  if (!chatId) {
715
1168
  this.cardMessageIds.delete(task.id);
716
- logger3.debug(`sendCardStart: no chatId for task #${task.id}, skip`);
1169
+ logger4.debug(`sendCardStart: no chatId for task #${task.id}, skip`);
717
1170
  return;
718
1171
  }
719
1172
  try {
@@ -730,29 +1183,55 @@ class LarkCliTaskNotifyHook {
730
1183
  ]
731
1184
  });
732
1185
  this.cardMessageIds.set(task.id, messageId);
733
- logger3.debug(`sendCardStart: task #${task.id} → message ${messageId} (pinned=${pinned}) ` + `chat=${chatId} source=${target.source} reason=${target.reason}`);
1186
+ this.persistCardState(this.buildCardState(task, messageId, chatId, profile, pinned), true);
1187
+ logger4.debug(`sendCardStart: task #${task.id} → message ${messageId} (pinned=${pinned}) ` + `chat=${chatId} source=${target.source} reason=${target.reason}`);
734
1188
  } catch (err) {
735
1189
  this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL);
736
- logger3.error(`sendCardStart failed for task #${task.id}: ${err.message}`);
1190
+ logger4.error(`sendCardStart failed for task #${task.id}: ${err.message}`);
737
1191
  }
738
1192
  }
739
1193
  async sendCardUpdate(task, changes) {
740
1194
  if (!this.cardNotifier)
741
1195
  return;
1196
+ const persisted = this.restoreCardState(task.id);
1197
+ if (persisted?.status === "closed") {
1198
+ const stillTerminal = isTerminalTaskStatus(String(task.status));
1199
+ if (stillTerminal) {
1200
+ logger4.debug(`sendCardUpdate: task #${task.id} 卡片已终结(closed)且任务仍终态 → skip patch`);
1201
+ return;
1202
+ }
1203
+ logger4.info(`sendCardUpdate: task #${task.id} 已 reopen(closed 记录 + 当前 status=${task.status})` + `→ 重新建卡`);
1204
+ this.cardMessageIds.delete(task.id);
1205
+ this.dropStaleMessageId(task.id);
1206
+ }
1207
+ if (persisted?.status === "paused" && String(task.status) !== "paused") {
1208
+ this.persistReopened(task.id);
1209
+ logger4.debug(`sendCardUpdate: task #${task.id} resume(paused → open),复用卡片 ${persisted.messageId}`);
1210
+ await this.repinAfterResume(task, persisted);
1211
+ }
742
1212
  let messageId = this.cardMessageIds.get(task.id);
1213
+ if (!messageId && isTerminalTaskStatus(String(task.status))) {
1214
+ logger4.debug(`sendCardUpdate: task #${task.id} 状态=${task.status}(终结/归档)且无卡片 → skip`);
1215
+ return;
1216
+ }
1217
+ if (!messageId) {
1218
+ this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL);
1219
+ }
743
1220
  const target = messageId ? undefined : await this.resolveCardTarget(task);
744
1221
  const chatId = target?.chatId ?? this.resolveChatId(task, getEnvContextOrEmpty());
745
1222
  const profile = target?.profile ?? this.resolveProfile(task, getEnvContextOrEmpty());
746
1223
  if (!chatId) {
747
- logger3.debug(`sendCardUpdate: no chatId for task #${task.id}, skip`);
1224
+ if (!messageId)
1225
+ this.cardMessageIds.delete(task.id);
1226
+ logger4.debug(`sendCardUpdate: no chatId for task #${task.id}, skip`);
748
1227
  return;
749
1228
  }
750
1229
  if (messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
751
- logger3.debug(`sendCardUpdate: startTask failed previously for task #${task.id}, skip`);
1230
+ logger4.debug(`sendCardUpdate: startTask failed previously for task #${task.id}, skip`);
752
1231
  return;
753
1232
  }
754
1233
  if (messageId === LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL) {
755
- logger3.debug(`sendCardUpdate: task #${task.id} 首卡正在发送中(in-flight)→ 跳过本次 update`);
1234
+ logger4.debug(`sendCardUpdate: task #${task.id} 首卡正在发送中(in-flight)→ 跳过本次 update`);
756
1235
  return;
757
1236
  }
758
1237
  if (!messageId) {
@@ -764,9 +1243,10 @@ class LarkCliTaskNotifyHook {
764
1243
  });
765
1244
  messageId = res.messageId;
766
1245
  this.cardMessageIds.set(task.id, messageId);
1246
+ this.persistCardState(this.buildCardState(task, messageId, chatId, profile, res.pinned), true);
767
1247
  } catch (err) {
768
1248
  this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL);
769
- logger3.error(`sendCardUpdate fallback start failed: ${err.message}`);
1249
+ logger4.error(`sendCardUpdate fallback start failed: ${err.message}`);
770
1250
  return;
771
1251
  }
772
1252
  }
@@ -789,32 +1269,49 @@ class LarkCliTaskNotifyHook {
789
1269
  milestones: [],
790
1270
  processSteps
791
1271
  });
1272
+ const current = this.getCardState(task.id);
1273
+ this.persistPinned(task.id, current?.pinned ?? persisted?.pinned ?? true);
1274
+ this.refreshTimeline(task.id);
792
1275
  } catch (err) {
793
- logger3.error(`sendCardUpdate failed: ${err.message}`);
1276
+ logger4.error(`sendCardUpdate failed: ${err.message}`);
794
1277
  }
795
1278
  }
796
1279
  async sendCardFinish(task) {
797
1280
  if (!this.cardNotifier)
798
1281
  return;
1282
+ this.restoreCardState(task.id);
799
1283
  const messageId = this.cardMessageIds.get(task.id);
800
1284
  const ctx = getEnvContextOrEmpty();
801
1285
  const profile = this.resolveProfile(task, ctx);
802
1286
  if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
803
- logger3.debug(`sendCardFinish: no card messageId for task #${task.id}, skip`);
1287
+ logger4.debug(`sendCardFinish: no card messageId for task #${task.id}, skip ` + `(记录缺失 → 不发新卡,保持幂等)`);
804
1288
  return;
805
1289
  }
806
- const status = String(task.status) === "failed" || String(task.status) === "cancelled" || String(task.status) === "timeout" ? "failed" : "completed";
1290
+ const rawStatus = String(task.status);
1291
+ const status = mapTaskStatusToCardStatus(rawStatus);
1292
+ const isPaused = rawStatus === "paused" || rawStatus === "archived";
807
1293
  try {
808
- await this.cardNotifier.finishTask({
1294
+ const finish = isPaused ? this.cardNotifier.pauseTask.bind(this.cardNotifier) : this.cardNotifier.finishTask.bind(this.cardNotifier);
1295
+ await finish({
809
1296
  messageId,
810
1297
  profile,
811
1298
  status,
812
1299
  meta: this.buildTaskMeta(task),
813
- finalStep: task.current_status || undefined
1300
+ finalStep: task.current_status || undefined,
1301
+ ...rawStatus === "archived" ? { pausedMilestoneTitle: "Task 已归档" } : {}
814
1302
  });
815
- this.cardMessageIds.delete(task.id);
1303
+ if (isPaused) {
1304
+ this.persistPaused(task.id);
1305
+ this.persistPinned(task.id, false);
1306
+ this.refreshTimeline(task.id);
1307
+ } else {
1308
+ this.persistClosed(task.id);
1309
+ this.persistPinned(task.id, false);
1310
+ this.cardMessageIds.delete(task.id);
1311
+ }
1312
+ logger4.debug(`sendCardFinish: task #${task.id} → status=${status} ` + `(raw=${rawStatus}) message=${messageId}`);
816
1313
  } catch (err) {
817
- logger3.error(`sendCardFinish failed: ${err.message}`);
1314
+ logger4.error(`sendCardFinish failed: ${err.message}`);
818
1315
  }
819
1316
  }
820
1317
  resolveChatId(task, env) {
@@ -891,7 +1388,7 @@ class LarkCliTaskNotifyHook {
891
1388
  const now = Date.now();
892
1389
  const lastTime = this.lastNotifyTimestamps.get(key);
893
1390
  if (lastTime && now - lastTime < throttleMs) {
894
- logger3.debug(`[throttle] skip OPERATION_CREATED patch for task #${operation.taskId} (${now - lastTime}ms ago)`);
1391
+ logger4.debug(`[throttle] skip OPERATION_CREATED patch for task #${operation.taskId} (${now - lastTime}ms ago)`);
895
1392
  return true;
896
1393
  }
897
1394
  this.lastNotifyTimestamps.set(key, now);
@@ -934,7 +1431,7 @@ init_decorator();
934
1431
  init_logger();
935
1432
  init_context();
936
1433
  import { spawn } from "child_process";
937
- var logger4 = createLogger("lark-cli:per-task-group");
1434
+ var logger5 = createLogger("lark-cli:per-task-group");
938
1435
  function isPluginDebugEnabled() {
939
1436
  return process.env.ROY_TASK_NOTIFY_DEBUG === "1" || process.env.ROY_TRACE_DEBUG === "1";
940
1437
  }
@@ -989,7 +1486,7 @@ class PerTaskGroupChatPlugin {
989
1486
  const renderedName = this.renderTemplate(this.config.nameTemplate, task);
990
1487
  const existingChatId = this.getTaskChatId(task);
991
1488
  if (existingChatId) {
992
- logger4.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 已有专属群 ${existingChatId} → 复用(不重复建群)`);
1489
+ logger5.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 已有专属群 ${existingChatId} → 复用(不重复建群)`);
993
1490
  return {
994
1491
  chatId: existingChatId,
995
1492
  profile: this.parseProfileFromContext(task.context),
@@ -998,7 +1495,7 @@ class PerTaskGroupChatPlugin {
998
1495
  }
999
1496
  const inflight = this.groupChatInflight.get(task.id);
1000
1497
  if (inflight) {
1001
- logger4.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 建群已在进行中 → 复用同一 promise`);
1498
+ logger5.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 建群已在进行中 → 复用同一 promise`);
1002
1499
  return inflight;
1003
1500
  }
1004
1501
  const pending = this.doEnsureGroupChat(task, renderedName, replyChannelOverride);
@@ -1012,7 +1509,7 @@ class PerTaskGroupChatPlugin {
1012
1509
  async doEnsureGroupChat(task, name, replyChannelOverride) {
1013
1510
  const replyChannel = replyChannelOverride ?? getEnvContextOrEmpty().replyChannel;
1014
1511
  if (!replyChannel?.chatId && this.config.requireReplyChannel) {
1015
- logger4.warn(`ensureGroupChat[${this.pluginName}]: no replyChannel.chatId for task #${task.id} ` + `(requireReplyChannel=true) → 无法建群`);
1512
+ logger5.warn(`ensureGroupChat[${this.pluginName}]: no replyChannel.chatId for task #${task.id} ` + `(requireReplyChannel=true) → 无法建群`);
1016
1513
  return;
1017
1514
  }
1018
1515
  const description = this.renderTemplate(this.config.descriptionTemplate, task);
@@ -1030,30 +1527,30 @@ class PerTaskGroupChatPlugin {
1030
1527
  await this.writeChatBinding(task, chatId, profile, "creating");
1031
1528
  await this.attachChatIdToTask(task, chatId, profile);
1032
1529
  await this.writeChatBinding(task, chatId, profile, "ready");
1033
- logger4.info(`ensureGroupChat[${this.pluginName}]: created group ${chatId} for task #${task.id}`);
1530
+ logger5.info(`ensureGroupChat[${this.pluginName}]: created group ${chatId} for task #${task.id}`);
1034
1531
  return { chatId, profile, name };
1035
1532
  } catch (error) {
1036
- logger4.error(`ensureGroupChat[${this.pluginName}]: failed for task #${task.id}: ${error}`);
1533
+ logger5.error(`ensureGroupChat[${this.pluginName}]: failed for task #${task.id}: ${error}`);
1037
1534
  return;
1038
1535
  }
1039
1536
  }
1040
1537
  setTaskUpdater(updater) {
1041
1538
  this.taskUpdater = updater;
1042
- logger4.debug(`TaskUpdater injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1539
+ logger5.debug(`TaskUpdater injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1043
1540
  }
1044
1541
  hasTaskUpdater() {
1045
1542
  return this.taskUpdater !== undefined;
1046
1543
  }
1047
1544
  setTaskGetter(getter) {
1048
1545
  this.taskGetter = getter;
1049
- logger4.debug(`TaskGetter injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1546
+ logger5.debug(`TaskGetter injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1050
1547
  }
1051
1548
  hasTaskGetter() {
1052
1549
  return this.taskGetter !== undefined;
1053
1550
  }
1054
1551
  setSessionComponent(sessionComponent) {
1055
1552
  this.sessionComponent = sessionComponent;
1056
- logger4.debug(`SessionComponent injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1553
+ logger5.debug(`SessionComponent injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1057
1554
  }
1058
1555
  hasSessionComponent() {
1059
1556
  return this.sessionComponent !== undefined;
@@ -1066,7 +1563,7 @@ class PerTaskGroupChatPlugin {
1066
1563
  if (process.env.ROY_HOOK_DEBUG === "1") {
1067
1564
  console.error(`[DEBUG per-task-group-chat.execute] → SKIP (config.enabled=false)`);
1068
1565
  }
1069
- logger4.debug(`execute: plugin disabled (pluginName=${this.pluginName})`);
1566
+ logger5.debug(`execute: plugin disabled (pluginName=${this.pluginName})`);
1070
1567
  return;
1071
1568
  }
1072
1569
  if (this.pluginName && this.sourceId) {
@@ -1079,13 +1576,13 @@ class PerTaskGroupChatPlugin {
1079
1576
  if (process.env.ROY_HOOK_DEBUG === "1") {
1080
1577
  console.error(`[DEBUG per-task-group-chat.execute] → SKIP (plugin NOT enabled for ${key})`);
1081
1578
  }
1082
- logger4.debug(`execute[${this.pluginName}]: plugin NOT enabled (key=${key}, ` + `env.pluginEnabled=${JSON.stringify(env.pluginEnabled)}, ` + `hookPoint=${ctx.hookPoint}). ` + `→ skip. 修复:roy-agent eventsource update <id> 必须包含 PerTaskGroupChatPlugin`);
1579
+ logger5.debug(`execute[${this.pluginName}]: plugin NOT enabled (key=${key}, ` + `env.pluginEnabled=${JSON.stringify(env.pluginEnabled)}, ` + `hookPoint=${ctx.hookPoint}). ` + `→ skip. 修复:roy-agent eventsource update <id> 必须包含 PerTaskGroupChatPlugin`);
1083
1580
  return;
1084
1581
  }
1085
1582
  const enabledState = env.pluginEnabled ? env.pluginEnabled[key] === true ? "✅ enabled" : "❌ NOT enabled" : "⚠️ env.pluginEnabled 未设置 (env 没创建 / 插件未注册到 eventsource)";
1086
- logger4.debug(`execute[${this.pluginName}]: ${enabledState} (key=${key}, ` + `hookPoint=${ctx.hookPoint}, taskUpdaterInjected=${this.hasTaskUpdater()})`);
1583
+ logger5.debug(`execute[${this.pluginName}]: ${enabledState} (key=${key}, ` + `hookPoint=${ctx.hookPoint}, taskUpdaterInjected=${this.hasTaskUpdater()})`);
1087
1584
  } else {
1088
- logger4.debug(`execute[${this.pluginName}]: missing pluginName/sourceId metadata ` + `(pluginName=${this.pluginName}, sourceId=${this.sourceId}), ` + `skipping pluginEnabled check. hookPoint=${ctx.hookPoint}`);
1585
+ logger5.debug(`execute[${this.pluginName}]: missing pluginName/sourceId metadata ` + `(pluginName=${this.pluginName}, sourceId=${this.sourceId}), ` + `skipping pluginEnabled check. hookPoint=${ctx.hookPoint}`);
1089
1586
  }
1090
1587
  if (process.env.ROY_HOOK_DEBUG === "1") {
1091
1588
  console.error(`[DEBUG per-task-group-chat.execute] → PASSED pluginEnabled check, will dispatch hookPoint=${ctx.hookPoint}`);
@@ -1104,13 +1601,13 @@ class PerTaskGroupChatPlugin {
1104
1601
  await this.onAfterComplete(ctx);
1105
1602
  break;
1106
1603
  default:
1107
- logger4.debug(`execute: unhandled hookPoint=${ctx.hookPoint}, skip`);
1604
+ logger5.debug(`execute: unhandled hookPoint=${ctx.hookPoint}, skip`);
1108
1605
  }
1109
1606
  }
1110
1607
  async onAfterCreate(ctx) {
1111
1608
  const data = ctx.data;
1112
1609
  if (!data?.task) {
1113
- logger4.debug("onAfterCreate: task is missing, skip");
1610
+ logger5.debug("onAfterCreate: task is missing, skip");
1114
1611
  return;
1115
1612
  }
1116
1613
  const { task } = data;
@@ -1121,38 +1618,38 @@ class PerTaskGroupChatPlugin {
1121
1618
  console.error(`[DEBUG per-task-group-chat.onAfterCreate] env.messageId=${env.messageId}`);
1122
1619
  console.error(`[DEBUG per-task-group-chat.onAfterCreate] env.sourceId=${env.sourceId}`);
1123
1620
  }
1124
- logger4.debug(`onAfterCreate[${this.pluginName}]: enter task=${task.id} "${task.title}" ` + `parent_task_id=${task.parent_task_id ?? "none"} ` + `requireReplyChannel=${this.config.requireReplyChannel} ` + `disableSubTaskSkip=${this.config.disableSubTaskSkip} ` + `sendWelcomeMessage=${this.config.sendWelcomeMessage} ` + `autoCreateGroup=${this.config.autoCreateGroup}`);
1621
+ logger5.debug(`onAfterCreate[${this.pluginName}]: enter task=${task.id} "${task.title}" ` + `parent_task_id=${task.parent_task_id ?? "none"} ` + `requireReplyChannel=${this.config.requireReplyChannel} ` + `disableSubTaskSkip=${this.config.disableSubTaskSkip} ` + `sendWelcomeMessage=${this.config.sendWelcomeMessage} ` + `autoCreateGroup=${this.config.autoCreateGroup}`);
1125
1622
  if (!this.config.autoCreateGroup) {
1126
- logger4.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} autoCreateGroup=false ` + `→ skip group chat creation(按需建群:用户在卡片顶部按钮触发)`);
1623
+ logger5.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} autoCreateGroup=false ` + `→ skip group chat creation(按需建群:用户在卡片顶部按钮触发)`);
1127
1624
  return;
1128
1625
  }
1129
1626
  const replyChannel = this.getReplyChannelFromCtx(ctx);
1130
1627
  if (!replyChannel?.chatId && this.config.requireReplyChannel) {
1131
- logger4.debug(`onAfterCreate[${this.pluginName}]: no replyChannel.chatId for task #${task.id}, skip. ` + `修复:eventsource 收到 im.message.receive_v1 事件时 replyChannel 应有值;` + `或在 plugin config 设 requireReplyChannel=false 强制创建群聊(profile 降级到 fallbackProfile/无)`);
1628
+ logger5.debug(`onAfterCreate[${this.pluginName}]: no replyChannel.chatId for task #${task.id}, skip. ` + `修复:eventsource 收到 im.message.receive_v1 事件时 replyChannel 应有值;` + `或在 plugin config 设 requireReplyChannel=false 强制创建群聊(profile 降级到 fallbackProfile/无)`);
1132
1629
  return;
1133
1630
  }
1134
1631
  if (!replyChannel?.chatId && !this.config.requireReplyChannel) {
1135
- logger4.debug(`onAfterCreate[${this.pluginName}]: no replyChannel but requireReplyChannel=false, ` + `proceeding with profile fallback (fallbackProfile="${this.config.fallbackProfile}")`);
1632
+ logger5.debug(`onAfterCreate[${this.pluginName}]: no replyChannel but requireReplyChannel=false, ` + `proceeding with profile fallback (fallbackProfile="${this.config.fallbackProfile}")`);
1136
1633
  }
1137
1634
  if (this.isSubTask(task) && !this.config.disableSubTaskSkip) {
1138
- logger4.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task, ` + `skip group chat creation. 关闭方式:config.disableSubTaskSkip=true`);
1635
+ logger5.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task, ` + `skip group chat creation. 关闭方式:config.disableSubTaskSkip=true`);
1139
1636
  return;
1140
1637
  }
1141
1638
  if (this.isSubTask(task) && this.config.disableSubTaskSkip) {
1142
- logger4.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task but ` + `disableSubTaskSkip=true, proceeding to create group chat`);
1639
+ logger5.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task but ` + `disableSubTaskSkip=true, proceeding to create group chat`);
1143
1640
  }
1144
1641
  try {
1145
1642
  const created = await this.ensureGroupChat(task, replyChannel);
1146
1643
  if (!created?.chatId) {
1147
- logger4.error(`onAfterCreate[${this.pluginName}]: ensureGroupChat 未返回 chatId for task #${task.id}`);
1644
+ logger5.error(`onAfterCreate[${this.pluginName}]: ensureGroupChat 未返回 chatId for task #${task.id}`);
1148
1645
  if (!this.config.fallbackToMain) {
1149
1646
  throw new Error(`per-task group chat unavailable for task #${task.id}`);
1150
1647
  }
1151
1648
  return;
1152
1649
  }
1153
- logger4.info(`onAfterCreate[${this.pluginName}]: task #${task.id} → group ${created.chatId} ` + `(initial card owned by LarkCliTaskNotifyHook)`);
1650
+ logger5.info(`onAfterCreate[${this.pluginName}]: task #${task.id} → group ${created.chatId} ` + `(initial card owned by LarkCliTaskNotifyHook)`);
1154
1651
  } catch (error) {
1155
- logger4.error(`onAfterCreate[${this.pluginName}]: group chat failed for task #${task.id}: ${error}`);
1652
+ logger5.error(`onAfterCreate[${this.pluginName}]: group chat failed for task #${task.id}: ${error}`);
1156
1653
  if (!this.config.fallbackToMain)
1157
1654
  throw error;
1158
1655
  }
@@ -1160,7 +1657,7 @@ class PerTaskGroupChatPlugin {
1160
1657
  async writeChatBinding(task, chatId, profile, status, errorMessage) {
1161
1658
  if (!this.sessionComponent) {
1162
1659
  if (isPluginDebugEnabled()) {
1163
- logger4.debug(`writeChatBinding[${this.pluginName}]: no sessionComponent injected, ` + `skip chat-task binding for chat ${chatId} (status=${status})`);
1660
+ logger5.debug(`writeChatBinding[${this.pluginName}]: no sessionComponent injected, ` + `skip chat-task binding for chat ${chatId} (status=${status})`);
1164
1661
  }
1165
1662
  return;
1166
1663
  }
@@ -1173,37 +1670,37 @@ class PerTaskGroupChatPlugin {
1173
1670
  errorMessage: errorMessage ?? null
1174
1671
  });
1175
1672
  } catch (bindErr) {
1176
- logger4.warn(`writeChatBinding[${this.pluginName}]: failed to write chat-task binding for ` + `chat ${chatId} / task #${task.id} (status=${status}): ${bindErr}. ` + `群消息 handler 将无法自动注入 parentTaskId。`);
1673
+ logger5.warn(`writeChatBinding[${this.pluginName}]: failed to write chat-task binding for ` + `chat ${chatId} / task #${task.id} (status=${status}): ${bindErr}. ` + `群消息 handler 将无法自动注入 parentTaskId。`);
1177
1674
  }
1178
1675
  }
1179
1676
  async onAfterUpdate(ctx) {
1180
1677
  const data = ctx.data;
1181
1678
  if (!data?.task) {
1182
- logger4.debug("onAfterUpdate: task is missing, skip");
1679
+ logger5.debug("onAfterUpdate: task is missing, skip");
1183
1680
  return;
1184
1681
  }
1185
- logger4.debug(`onAfterUpdate[${this.pluginName}]: task #${data.task.id} 卡片由 ` + `LarkCliTaskNotifyHook 负责(本插件不再 patch),skip`);
1682
+ logger5.debug(`onAfterUpdate[${this.pluginName}]: task #${data.task.id} 卡片由 ` + `LarkCliTaskNotifyHook 负责(本插件不再 patch),skip`);
1186
1683
  }
1187
1684
  async onAfterOperationCreate(ctx) {
1188
1685
  const data = ctx.data;
1189
1686
  if (!data?.operation) {
1190
- logger4.debug("onAfterOperationCreate: operation is missing, skip");
1687
+ logger5.debug("onAfterOperationCreate: operation is missing, skip");
1191
1688
  return;
1192
1689
  }
1193
- logger4.debug(`onAfterOperationCreate[${this.pluginName}]: operation #${data.operation.id} ` + `卡片由 LarkCliTaskNotifyHook 负责,skip`);
1690
+ logger5.debug(`onAfterOperationCreate[${this.pluginName}]: operation #${data.operation.id} ` + `卡片由 LarkCliTaskNotifyHook 负责,skip`);
1194
1691
  }
1195
1692
  async onAfterComplete(ctx) {
1196
1693
  const data = ctx.data;
1197
1694
  if (!data?.task) {
1198
- logger4.debug("onAfterComplete: task is missing, skip");
1695
+ logger5.debug("onAfterComplete: task is missing, skip");
1199
1696
  return;
1200
1697
  }
1201
- logger4.debug(`onAfterComplete[${this.pluginName}]: task #${data.task.id} 终态卡片由 ` + `LarkCliTaskNotifyHook 负责,skip`);
1698
+ logger5.debug(`onAfterComplete[${this.pluginName}]: task #${data.task.id} 终态卡片由 ` + `LarkCliTaskNotifyHook 负责,skip`);
1202
1699
  }
1203
1700
  createGroupChat(opts) {
1204
1701
  const args = this.buildCreateGroupChatArgs(opts);
1205
1702
  if (isPluginDebugEnabled()) {
1206
- logger4.debug(`Creating group chat: lark-cli ${args.join(" ")}`);
1703
+ logger5.debug(`Creating group chat: lark-cli ${args.join(" ")}`);
1207
1704
  }
1208
1705
  return new Promise((resolve, reject) => {
1209
1706
  const proc = spawn("lark-cli", args, {
@@ -1278,16 +1775,16 @@ class PerTaskGroupChatPlugin {
1278
1775
  parsed[CTX_FIELD_CHAT_TASK_ID] = task.id;
1279
1776
  const newContext = JSON.stringify(parsed);
1280
1777
  if (!this.taskUpdater) {
1281
- logger4.warn(`attachChatIdToTask[${this.pluginName}]: ⚠️ no taskUpdater injected, ` + `chat_id ${chatId} NOT persisted to db for task #${task.id}. ` + `后果:下次 AFTER_UPDATE 从 DB 加载 task 时 getTaskChatId() 返回 undefined,` + `所有 update/complete/operation 通知 silently 丢失。` + `修复:event-source-handlers.ts:registerPluginHooks 应自动注入 setTaskUpdater。`);
1778
+ logger5.warn(`attachChatIdToTask[${this.pluginName}]: ⚠️ no taskUpdater injected, ` + `chat_id ${chatId} NOT persisted to db for task #${task.id}. ` + `后果:下次 AFTER_UPDATE 从 DB 加载 task 时 getTaskChatId() 返回 undefined,` + `所有 update/complete/operation 通知 silently 丢失。` + `修复:event-source-handlers.ts:registerPluginHooks 应自动注入 setTaskUpdater。`);
1282
1779
  task.context = newContext;
1283
1780
  return;
1284
1781
  }
1285
1782
  try {
1286
1783
  await this.taskUpdater(task.id, newContext);
1287
1784
  task.context = newContext;
1288
- logger4.debug(`attachChatIdToTask[${this.pluginName}]: ✅ persisted chat_id ${chatId} ` + `to task #${task.id} context`);
1785
+ logger5.debug(`attachChatIdToTask[${this.pluginName}]: ✅ persisted chat_id ${chatId} ` + `to task #${task.id} context`);
1289
1786
  } catch (error) {
1290
- logger4.error(`Failed to persist chat_id to task #${task.id}: ${error}`);
1787
+ logger5.error(`Failed to persist chat_id to task #${task.id}: ${error}`);
1291
1788
  task.context = newContext;
1292
1789
  }
1293
1790
  }
@@ -1382,15 +1879,15 @@ class PerTaskGroupChatPlugin {
1382
1879
  }
1383
1880
  resolveBotAppIds(replyChannel) {
1384
1881
  if (this.config.botAppIds.length > 0) {
1385
- logger4.debug(`resolveBotAppIds[${this.pluginName}]: using config.botAppIds=${JSON.stringify(this.config.botAppIds)}`);
1882
+ logger5.debug(`resolveBotAppIds[${this.pluginName}]: using config.botAppIds=${JSON.stringify(this.config.botAppIds)}`);
1386
1883
  return this.config.botAppIds;
1387
1884
  }
1388
1885
  const appId = replyChannel ? replyChannel.appId : undefined;
1389
1886
  if (appId) {
1390
- logger4.debug(`resolveBotAppIds[${this.pluginName}]: using replyChannel.appId=${appId} (auto-detected from event header.app_id)`);
1887
+ logger5.debug(`resolveBotAppIds[${this.pluginName}]: using replyChannel.appId=${appId} (auto-detected from event header.app_id)`);
1391
1888
  return [appId];
1392
1889
  }
1393
- logger4.warn(`resolveBotAppIds[${this.pluginName}]: ⚠️ no botAppIds found, creating group WITHOUT --bots. ` + `Bot will NOT be invited automatically. ` + `可能原因:(1) eventsource 没有收到 im.message.receive_v1 事件(replyChannel.appId 缺失);` + `(2) 配置了 requireReplyChannel=false 模式;` + `(3) buildReplyChannel 没透传 appId(应在 v1.10+ 修复)。` + `修复:手动设置 config.botAppIds: ["cli_xxx"] 或确认 eventsource 处理了 lark event。`);
1890
+ logger5.warn(`resolveBotAppIds[${this.pluginName}]: ⚠️ no botAppIds found, creating group WITHOUT --bots. ` + `Bot will NOT be invited automatically. ` + `可能原因:(1) eventsource 没有收到 im.message.receive_v1 事件(replyChannel.appId 缺失);` + `(2) 配置了 requireReplyChannel=false 模式;` + `(3) buildReplyChannel 没透传 appId(应在 v1.10+ 修复)。` + `修复:手动设置 config.botAppIds: ["cli_xxx"] 或确认 eventsource 处理了 lark event。`);
1394
1891
  return [];
1395
1892
  }
1396
1893
  resolveUserIds(replyChannel) {
@@ -1400,7 +1897,7 @@ class PerTaskGroupChatPlugin {
1400
1897
  if (id && id !== "sender" && id.startsWith("ou_")) {
1401
1898
  ids.add(id);
1402
1899
  } else if (id && id !== "sender") {
1403
- logger4.debug(`resolveUserIds[${this.pluginName}]: skip non-open_id in defaultInviteUserIds: "${id}"`);
1900
+ logger5.debug(`resolveUserIds[${this.pluginName}]: skip non-open_id in defaultInviteUserIds: "${id}"`);
1404
1901
  }
1405
1902
  }
1406
1903
  }
@@ -1423,7 +1920,7 @@ class PerTaskGroupChatPlugin {
1423
1920
  }
1424
1921
  }
1425
1922
  if (isPluginDebugEnabled()) {
1426
- logger4.debug(`resolveUserIds[${this.pluginName}]: ` + `defaultInviteUserIds=${JSON.stringify(this.config.defaultInviteUserIds)} ` + `inviteUserIds=${JSON.stringify(this.config.inviteUserIds)} ` + `replyChannel.userId=${replyChannel?.userId ?? "(none)"} ` + `→ final userIds=${JSON.stringify(Array.from(ids))}`);
1923
+ logger5.debug(`resolveUserIds[${this.pluginName}]: ` + `defaultInviteUserIds=${JSON.stringify(this.config.defaultInviteUserIds)} ` + `inviteUserIds=${JSON.stringify(this.config.inviteUserIds)} ` + `replyChannel.userId=${replyChannel?.userId ?? "(none)"} ` + `→ final userIds=${JSON.stringify(Array.from(ids))}`);
1427
1924
  }
1428
1925
  return Array.from(ids);
1429
1926
  }