@ai-setting/roy-agent-core 1.7.77 → 1.7.88

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 (155) hide show
  1. package/dist/config/config-protection.d.ts +89 -0
  2. package/dist/config/index.d.ts +1 -0
  3. package/dist/config/index.js +23 -7
  4. package/dist/env/agent/agent-component.d.ts +43 -22
  5. package/dist/env/agent/agent-component.js +13 -12
  6. package/dist/env/agent/index.js +13 -12
  7. package/dist/env/agent/types.d.ts +46 -12
  8. package/dist/env/commands/index.js +7 -7
  9. package/dist/env/context/index.js +3 -3
  10. package/dist/env/debug/index.js +7 -7
  11. package/dist/env/error-reply.d.ts +6 -0
  12. package/dist/env/error-reply.js +1 -1
  13. package/dist/env/event-source/bounty-im-handler.d.ts +8 -0
  14. package/dist/env/event-source/es-command.d.ts +39 -0
  15. package/dist/env/event-source/es-plugins.d.ts +68 -0
  16. package/dist/env/event-source/event-source-component.d.ts +15 -0
  17. package/dist/env/event-source/event-source-handlers.d.ts +84 -0
  18. package/dist/env/event-source/event-source-handlers.js +40 -23
  19. package/dist/env/event-source/event-store.js +2 -2
  20. package/dist/env/event-source/index.d.ts +5 -1
  21. package/dist/env/event-source/index.js +75 -23
  22. package/dist/env/event-source/timer-handler.d.ts +98 -0
  23. package/dist/env/event-source/types.d.ts +154 -2
  24. package/dist/env/event-source/wecom-card-action-handler.d.ts +373 -0
  25. package/dist/env/event-source/wecom-card-builder.d.ts +426 -0
  26. package/dist/env/event-source/wecom-cli-handler.d.ts +291 -0
  27. package/dist/env/event-source/wecom-cli-sender.d.ts +88 -0
  28. package/dist/env/event-source/wecom-media.d.ts +130 -0
  29. package/dist/env/event-source/wecom-respond-registry.d.ts +151 -0
  30. package/dist/env/event-source/wecom-respond-registry.js +33 -0
  31. package/dist/env/event-source/wecom-session-card-sender.d.ts +135 -0
  32. package/dist/env/event-source/wecom-session-card-sender.js +153 -0
  33. package/dist/env/event-source/wecom-ws-bridge.d.ts +213 -0
  34. package/dist/env/hook/index.js +5 -5
  35. package/dist/env/index.js +51 -45
  36. package/dist/env/llm/index.js +9 -8
  37. package/dist/env/log-trace/index.js +7 -7
  38. package/dist/env/log-trace/trace-filters.d.ts +6 -1
  39. package/dist/env/mcp/index.js +7 -7
  40. package/dist/env/memory/index.js +7 -7
  41. package/dist/env/plugin/index.js +7 -7
  42. package/dist/env/prompt/index.js +8 -8
  43. package/dist/env/prompt/prompts-index.d.ts +1 -1
  44. package/dist/env/session/group-session-link.d.ts +99 -0
  45. package/dist/env/session/index.js +11 -10
  46. package/dist/env/session/session-component.d.ts +89 -0
  47. package/dist/env/session/storage/index.js +5 -4
  48. package/dist/env/session/types.d.ts +10 -0
  49. package/dist/env/skill/index.js +7 -7
  50. package/dist/env/task/delegate/delegate-channel-hint.d.ts +39 -0
  51. package/dist/env/task/delegate/delegate-tool.d.ts +132 -7
  52. package/dist/env/task/delegate/delegate-tool.js +13 -9
  53. package/dist/env/task/delegate/index.js +8 -8
  54. package/dist/env/task/index.js +18 -18
  55. package/dist/env/task/mcp/index.js +4 -4
  56. package/dist/env/task/plugins/card-subject.d.ts +117 -0
  57. package/dist/env/task/plugins/index.d.ts +2 -0
  58. package/dist/env/task/plugins/index.js +39 -16
  59. package/dist/env/task/plugins/lark-card-action-handler.d.ts +261 -6
  60. package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +542 -2
  61. package/dist/env/task/plugins/lark-card-patch-notifier.js +12 -2
  62. package/dist/env/task/plugins/lark-card-state-store.d.ts +60 -0
  63. package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +61 -0
  64. package/dist/env/task/plugins/sub-session-activity.d.ts +415 -0
  65. package/dist/env/task/plugins/wecom-card-state-store.d.ts +121 -0
  66. package/dist/env/task/plugins/wecom-session-card-store.d.ts +134 -0
  67. package/dist/env/task/plugins/wecom-session-card-store.js +23 -0
  68. package/dist/env/task/plugins/wecom-task-notify-plugin.d.ts +264 -0
  69. package/dist/env/task/storage/index.js +3 -3
  70. package/dist/env/task/tools/index.js +4 -4
  71. package/dist/env/tool/built-in/index.d.ts +2 -0
  72. package/dist/env/tool/built-in/index.js +9 -2
  73. package/dist/env/tool/index.js +14 -9
  74. package/dist/env/tool/types.d.ts +25 -0
  75. package/dist/env/tool/validator.d.ts +8 -0
  76. package/dist/env/workflow/engine/index.js +12 -12
  77. package/dist/env/workflow/hooks/index.js +7 -7
  78. package/dist/env/workflow/index.js +24 -23
  79. package/dist/env/workflow/nodes/index.js +2 -2
  80. package/dist/env/workflow/plugins/index.js +10 -8
  81. package/dist/env/workflow/service/index.js +2 -2
  82. package/dist/env/workflow/storage/index.js +3 -3
  83. package/dist/env/workflow/tools/index.js +16 -15
  84. package/dist/env/workflow/utils/index.js +4 -4
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.js +118 -62
  87. package/dist/shared/@ai-setting/{roy-agent-core-dc9zq75z.js → roy-agent-core-0crmsc12.js} +3 -3
  88. package/dist/shared/@ai-setting/{roy-agent-core-aa2j7bk9.js → roy-agent-core-0sbmdz8d.js} +171 -36
  89. package/dist/shared/@ai-setting/{roy-agent-core-87942bw4.js → roy-agent-core-1mhyh4c5.js} +1 -1
  90. package/dist/shared/@ai-setting/{roy-agent-core-v6vr6za7.js → roy-agent-core-1vv4fx15.js} +4 -4
  91. package/dist/shared/@ai-setting/roy-agent-core-2stt4rp5.js +134 -0
  92. package/dist/shared/@ai-setting/{roy-agent-core-nwbfepba.js → roy-agent-core-3e040tc5.js} +1 -1
  93. package/dist/shared/@ai-setting/roy-agent-core-418zc9ha.js +288 -0
  94. package/dist/shared/@ai-setting/{roy-agent-core-r5wt1h3c.js → roy-agent-core-4fkxy2gv.js} +1 -1
  95. package/dist/shared/@ai-setting/{roy-agent-core-na18zwj0.js → roy-agent-core-4t3yskp8.js} +1 -1
  96. package/dist/shared/@ai-setting/{roy-agent-core-5pr4zp88.js → roy-agent-core-5kzhaap1.js} +1 -1
  97. package/dist/shared/@ai-setting/{roy-agent-core-qq96p0tj.js → roy-agent-core-5s2t8z00.js} +22 -1
  98. package/dist/shared/@ai-setting/{roy-agent-core-6rh8k6ba.js → roy-agent-core-6gj1hc5z.js} +7 -7
  99. package/dist/shared/@ai-setting/{roy-agent-core-ezvpreqz.js → roy-agent-core-6k8mb7zx.js} +1 -1
  100. package/dist/shared/@ai-setting/{roy-agent-core-g8htfwxj.js → roy-agent-core-7f10p7m2.js} +1 -1
  101. package/dist/shared/@ai-setting/roy-agent-core-7kvpc0w5.js +177 -0
  102. package/dist/shared/@ai-setting/{roy-agent-core-h21gngxf.js → roy-agent-core-7yh5wps0.js} +1 -1
  103. package/dist/shared/@ai-setting/{roy-agent-core-efkjyh69.js → roy-agent-core-9710zwwz.js} +1 -1
  104. package/dist/shared/@ai-setting/{roy-agent-core-6cy43nss.js → roy-agent-core-9c565frx.js} +49 -9
  105. package/dist/shared/@ai-setting/{roy-agent-core-fghc2m8z.js → roy-agent-core-aa8kv18f.js} +34 -2
  106. package/dist/shared/@ai-setting/{roy-agent-core-ew6qb5yx.js → roy-agent-core-an35dzbf.js} +6 -6
  107. package/dist/shared/@ai-setting/{roy-agent-core-mww0mnd9.js → roy-agent-core-azyz78ky.js} +7 -7
  108. package/dist/shared/@ai-setting/{roy-agent-core-264qexac.js → roy-agent-core-b7h39vrc.js} +4 -4
  109. package/dist/shared/@ai-setting/{roy-agent-core-dqxec05w.js → roy-agent-core-bjtc9d35.js} +2 -2
  110. package/dist/shared/@ai-setting/roy-agent-core-c4ec7k0p.js +426 -0
  111. package/dist/shared/@ai-setting/{roy-agent-core-ddyk1rc0.js → roy-agent-core-cre2y2ct.js} +160 -4
  112. package/dist/shared/@ai-setting/{roy-agent-core-tr3ep81v.js → roy-agent-core-d6zdbgtc.js} +1 -1
  113. package/dist/shared/@ai-setting/{roy-agent-core-0qyrk5k0.js → roy-agent-core-ds6qzmf8.js} +14 -5
  114. package/dist/shared/@ai-setting/{roy-agent-core-cgtq6427.js → roy-agent-core-e08bvpaq.js} +1 -1
  115. package/dist/shared/@ai-setting/{roy-agent-core-12n08pg5.js → roy-agent-core-e69qxdyy.js} +1 -1
  116. package/dist/shared/@ai-setting/{roy-agent-core-rhtdspx6.js → roy-agent-core-fan8g0z1.js} +10 -10
  117. package/dist/shared/@ai-setting/{roy-agent-core-xcp8vwax.js → roy-agent-core-fepm835e.js} +2 -2
  118. package/dist/shared/@ai-setting/{roy-agent-core-6j02sbee.js → roy-agent-core-fhg03v43.js} +1 -1
  119. package/dist/shared/@ai-setting/{roy-agent-core-99nwgj84.js → roy-agent-core-fzw9xzm1.js} +1 -1
  120. package/dist/shared/@ai-setting/{roy-agent-core-dvtg4sn0.js → roy-agent-core-g8yr733d.js} +1 -1
  121. package/dist/shared/@ai-setting/{roy-agent-core-3gpygqcx.js → roy-agent-core-gr5h1dyr.js} +2753 -358
  122. package/dist/shared/@ai-setting/roy-agent-core-h1e1fm9b.js +380 -0
  123. package/dist/shared/@ai-setting/{roy-agent-core-c2gbt68e.js → roy-agent-core-h83zdsrq.js} +2 -2
  124. package/dist/shared/@ai-setting/{roy-agent-core-8ez6mccb.js → roy-agent-core-k3gcvybs.js} +5 -1
  125. package/dist/shared/@ai-setting/{roy-agent-core-pgvcf91v.js → roy-agent-core-m39v7qpx.js} +2 -2
  126. package/dist/shared/@ai-setting/{roy-agent-core-tm1mxttk.js → roy-agent-core-mf5p38yf.js} +2 -2
  127. package/dist/shared/@ai-setting/{roy-agent-core-83bewqx8.js → roy-agent-core-mvh0td7p.js} +2 -2
  128. package/dist/shared/@ai-setting/{roy-agent-core-v85n42ve.js → roy-agent-core-mx855td4.js} +17 -38
  129. package/dist/shared/@ai-setting/{roy-agent-core-v87njmq7.js → roy-agent-core-n197medr.js} +13 -13
  130. package/dist/shared/@ai-setting/{roy-agent-core-cmprygrp.js → roy-agent-core-nqkfkpf4.js} +4 -4
  131. package/dist/shared/@ai-setting/{roy-agent-core-7cbpmk4q.js → roy-agent-core-p31ftgq9.js} +1 -1
  132. package/dist/shared/@ai-setting/{roy-agent-core-vffccgf6.js → roy-agent-core-qwd2dq5q.js} +10 -3
  133. package/dist/shared/@ai-setting/{roy-agent-core-rsn44n9y.js → roy-agent-core-ra66sf50.js} +2526 -942
  134. package/dist/shared/@ai-setting/{roy-agent-core-1v4g0d4x.js → roy-agent-core-rm7seqwb.js} +1 -1
  135. package/dist/shared/@ai-setting/{roy-agent-core-w0qjeq2v.js → roy-agent-core-shb1qsyg.js} +1041 -18
  136. package/dist/shared/@ai-setting/{roy-agent-core-ax9e9szb.js → roy-agent-core-smf94h73.js} +1 -1
  137. package/dist/shared/@ai-setting/{roy-agent-core-acj8ag08.js → roy-agent-core-tjnabyvq.js} +1 -1
  138. package/dist/shared/@ai-setting/{roy-agent-core-qqaak24r.js → roy-agent-core-tkpp1ba0.js} +1 -1
  139. package/dist/shared/@ai-setting/{roy-agent-core-9vhq7fvv.js → roy-agent-core-tpm1dd1g.js} +34 -88
  140. package/dist/shared/@ai-setting/{roy-agent-core-ed233k5z.js → roy-agent-core-v1n0106v.js} +1 -1
  141. package/dist/shared/@ai-setting/{roy-agent-core-zxz5fkwj.js → roy-agent-core-v6yshv0z.js} +1 -1
  142. package/dist/shared/@ai-setting/{roy-agent-core-mcy5apay.js → roy-agent-core-vf9sp6s3.js} +1 -1
  143. package/dist/shared/@ai-setting/{roy-agent-core-q813vafd.js → roy-agent-core-vpcxv1ty.js} +1 -1
  144. package/dist/shared/@ai-setting/{roy-agent-core-bxkjk7zx.js → roy-agent-core-vx8zc847.js} +4 -1
  145. package/dist/shared/@ai-setting/{roy-agent-core-xvt7kf7p.js → roy-agent-core-w9ryeg2p.js} +3 -3
  146. package/dist/shared/@ai-setting/{roy-agent-core-es203w7p.js → roy-agent-core-wqs6yypq.js} +1 -1
  147. package/dist/shared/@ai-setting/{roy-agent-core-hd3n57zs.js → roy-agent-core-wr1qexzq.js} +5 -5
  148. package/dist/shared/@ai-setting/roy-agent-core-wxrkcb17.js +56 -0
  149. package/dist/shared/@ai-setting/{roy-agent-core-0snm5hc5.js → roy-agent-core-y6shd91r.js} +1 -1
  150. package/dist/shared/@ai-setting/{roy-agent-core-9vxh6f8h.js → roy-agent-core-y7fqtzk2.js} +7 -7
  151. package/dist/shared/@ai-setting/{roy-agent-core-cpcd24n2.js → roy-agent-core-yet36q5k.js} +2 -2
  152. package/dist/shared/@ai-setting/{roy-agent-core-3fh1f7s7.js → roy-agent-core-yfpnkc4z.js} +5 -5
  153. package/dist/shared/@ai-setting/{roy-agent-core-0v27y99n.js → roy-agent-core-yh68z1bg.js} +65 -4
  154. package/package.json +18 -1
  155. /package/dist/shared/@ai-setting/{roy-agent-core-b4x6x4k2.js → roy-agent-core-m1fgtnrc.js} +0 -0
@@ -1,13 +1,18 @@
1
+ import {
2
+ sendWecomReply,
3
+ sendWecomTemplateCard
4
+ } from "./roy-agent-core-h1e1fm9b.js";
5
+ import {
6
+ WecomCardActionKey,
7
+ buildWecomButtonInteractionCard,
8
+ buildWecomTextNoticeCard
9
+ } from "./roy-agent-core-c4ec7k0p.js";
1
10
  import {
2
11
  getSharedEventStore
3
- } from "./roy-agent-core-na18zwj0.js";
12
+ } from "./roy-agent-core-4t3yskp8.js";
4
13
  import {
5
14
  WORKFLOW_EXTRACT_PROMPT
6
- } from "./roy-agent-core-9vxh6f8h.js";
7
- import {
8
- createCompatibleDatabase,
9
- init_sqlite_task_store
10
- } from "./roy-agent-core-ezvpreqz.js";
15
+ } from "./roy-agent-core-y7fqtzk2.js";
11
16
  import {
12
17
  TaskHookPoints
13
18
  } from "./roy-agent-core-1pg0fepg.js";
@@ -20,31 +25,43 @@ import {
20
25
  renderCandidateKey,
21
26
  renderCandidateValue,
22
27
  writeDecisionHint
23
- } from "./roy-agent-core-tr3ep81v.js";
28
+ } from "./roy-agent-core-d6zdbgtc.js";
24
29
  import {
25
30
  LarkCardPatchNotifier,
31
+ buildSessionCard,
32
+ buildTaskCard,
33
+ createLarkCardStateStore,
34
+ getSharedLarkCardNotifier,
26
35
  humanizeValue
27
- } from "./roy-agent-core-w0qjeq2v.js";
36
+ } from "./roy-agent-core-shb1qsyg.js";
37
+ import {
38
+ RECENT_MESSAGE_LIMIT
39
+ } from "./roy-agent-core-418zc9ha.js";
40
+ import {
41
+ createCompatibleDatabase,
42
+ init_sqlite_task_store
43
+ } from "./roy-agent-core-6k8mb7zx.js";
28
44
  import {
29
45
  validateWorkflowDefinition
30
- } from "./roy-agent-core-1v4g0d4x.js";
46
+ } from "./roy-agent-core-rm7seqwb.js";
31
47
  import {
32
48
  enqueueToAddFakeUserMessage
33
- } from "./roy-agent-core-h21gngxf.js";
49
+ } from "./roy-agent-core-7yh5wps0.js";
34
50
  import {
35
51
  BasePlugin
36
52
  } from "./roy-agent-core-az13yzmc.js";
37
53
  import {
38
54
  init_context
39
- } from "./roy-agent-core-qqaak24r.js";
55
+ } from "./roy-agent-core-tkpp1ba0.js";
40
56
  import {
41
- getEnvContextOrEmpty
42
- } from "./roy-agent-core-7cbpmk4q.js";
57
+ getEnvContextOrEmpty,
58
+ init_env_context
59
+ } from "./roy-agent-core-p31ftgq9.js";
43
60
  import {
44
61
  TracedAs,
45
62
  init_decorator,
46
63
  wrapFunction
47
- } from "./roy-agent-core-qq96p0tj.js";
64
+ } from "./roy-agent-core-5s2t8z00.js";
48
65
  import {
49
66
  createLogger,
50
67
  init_logger
@@ -313,11 +330,11 @@ ${prompt}`;
313
330
  }
314
331
  if (!this._workflowRepo) {
315
332
  try {
316
- const sqlite = await import("./roy-agent-core-dc9zq75z.js");
333
+ const sqlite = await import("./roy-agent-core-0crmsc12.js");
317
334
  this.ensureDataDir();
318
335
  const db = sqlite.getDatabase();
319
336
  sqlite.initializeTables();
320
- const { WorkflowRepository } = await import("./roy-agent-core-xvt7kf7p.js");
337
+ const { WorkflowRepository } = await import("./roy-agent-core-w9ryeg2p.js");
321
338
  this._workflowRepo = new WorkflowRepository(db);
322
339
  return this._workflowRepo;
323
340
  } catch {
@@ -1676,178 +1693,10 @@ init_decorator();
1676
1693
  init_logger();
1677
1694
  init_context();
1678
1695
 
1679
- // src/env/task/plugins/lark-card-state-store.ts
1696
+ // src/env/task/plugins/lark-card-action-handler.ts
1680
1697
  init_logger();
1681
- init_sqlite_task_store();
1682
- import { join } from "path";
1683
- var logger6 = createLogger("lark-cli:card-state");
1684
-
1685
- class MemoryLarkCardStateStore {
1686
- records = new Map;
1687
- upsert(state) {
1688
- this.records.set(state.taskId, { ...state });
1689
- }
1690
- get(taskId) {
1691
- const r = this.records.get(taskId);
1692
- return r ? { ...r } : undefined;
1693
- }
1694
- list() {
1695
- return [...this.records.values()].map((r) => ({ ...r }));
1696
- }
1697
- markPinned(taskId, pinned) {
1698
- const r = this.records.get(taskId);
1699
- if (!r)
1700
- return;
1701
- r.pinned = pinned;
1702
- r.updatedAt = Date.now();
1703
- }
1704
- markPaused(taskId) {
1705
- const r = this.records.get(taskId);
1706
- if (!r)
1707
- return;
1708
- r.status = "paused";
1709
- r.updatedAt = Date.now();
1710
- }
1711
- markReopened(taskId) {
1712
- const r = this.records.get(taskId);
1713
- if (!r)
1714
- return;
1715
- r.status = "open";
1716
- delete r.closedAt;
1717
- r.updatedAt = Date.now();
1718
- }
1719
- markClosed(taskId) {
1720
- const r = this.records.get(taskId);
1721
- if (!r)
1722
- return;
1723
- const now = Date.now();
1724
- r.status = "closed";
1725
- r.closedAt = now;
1726
- r.updatedAt = now;
1727
- }
1728
- delete(taskId) {
1729
- this.records.delete(taskId);
1730
- }
1731
- close() {
1732
- this.records.clear();
1733
- }
1734
- }
1735
- function rowToState(row) {
1736
- const state = {
1737
- taskId: row.task_id,
1738
- messageId: row.message_id,
1739
- chatId: row.chat_id,
1740
- pinned: row.pinned === 1,
1741
- status: row.status ?? "open",
1742
- createdAt: row.created_at,
1743
- updatedAt: row.updated_at
1744
- };
1745
- if (row.profile)
1746
- state.profile = row.profile;
1747
- if (row.closed_at !== null && row.closed_at !== undefined) {
1748
- state.closedAt = row.closed_at;
1749
- }
1750
- if (row.timeline)
1751
- state.timeline = row.timeline;
1752
- return state;
1753
- }
1754
-
1755
- class SqliteLarkCardStateStore {
1756
- db;
1757
- constructor(dbPath) {
1758
- if (dbPath !== ":memory:") {
1759
- const { mkdirSync } = __require("fs");
1760
- const dir = __require("path").dirname(dbPath);
1761
- try {
1762
- mkdirSync(dir, { recursive: true });
1763
- } catch {}
1764
- }
1765
- this.db = createCompatibleDatabase(dbPath);
1766
- try {
1767
- this.db.run("PRAGMA journal_mode = WAL");
1768
- this.db.run("PRAGMA busy_timeout = 5000");
1769
- this.db.run("PRAGMA synchronous = NORMAL");
1770
- } catch {}
1771
- this.db.run(`
1772
- CREATE TABLE IF NOT EXISTS lark_card (
1773
- task_id INTEGER PRIMARY KEY,
1774
- message_id TEXT NOT NULL,
1775
- chat_id TEXT NOT NULL,
1776
- profile TEXT,
1777
- pinned INTEGER NOT NULL DEFAULT 0,
1778
- status TEXT NOT NULL DEFAULT 'open',
1779
- created_at INTEGER NOT NULL,
1780
- updated_at INTEGER NOT NULL,
1781
- closed_at INTEGER,
1782
- timeline TEXT
1783
- )
1784
- `);
1785
- try {
1786
- this.db.run("ALTER TABLE lark_card ADD COLUMN timeline TEXT");
1787
- } catch {}
1788
- this.db.run("CREATE INDEX IF NOT EXISTS idx_lark_card_status ON lark_card(status)");
1789
- }
1790
- upsert(state) {
1791
- this.db.prepare(`INSERT INTO lark_card
1792
- (task_id, message_id, chat_id, profile, pinned, status, created_at, updated_at, closed_at, timeline)
1793
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1794
- ON CONFLICT(task_id) DO UPDATE SET
1795
- message_id = excluded.message_id,
1796
- chat_id = excluded.chat_id,
1797
- profile = excluded.profile,
1798
- pinned = excluded.pinned,
1799
- status = excluded.status,
1800
- updated_at = excluded.updated_at,
1801
- closed_at = excluded.closed_at,
1802
- 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);
1803
- }
1804
- get(taskId) {
1805
- const row = this.db.prepare("SELECT * FROM lark_card WHERE task_id = ?").get(taskId);
1806
- return row ? rowToState(row) : undefined;
1807
- }
1808
- list() {
1809
- const rows = this.db.prepare("SELECT * FROM lark_card ORDER BY task_id ASC").all();
1810
- return rows.map(rowToState);
1811
- }
1812
- markPinned(taskId, pinned) {
1813
- this.db.prepare("UPDATE lark_card SET pinned = ?, updated_at = ? WHERE task_id = ?").run(pinned ? 1 : 0, Date.now(), taskId);
1814
- }
1815
- markPaused(taskId) {
1816
- this.db.prepare("UPDATE lark_card SET status = 'paused', updated_at = ? WHERE task_id = ?").run(Date.now(), taskId);
1817
- }
1818
- markReopened(taskId) {
1819
- this.db.prepare("UPDATE lark_card SET status = 'open', closed_at = NULL, updated_at = ? WHERE task_id = ?").run(Date.now(), taskId);
1820
- }
1821
- markClosed(taskId) {
1822
- const now = Date.now();
1823
- this.db.prepare("UPDATE lark_card SET status = 'closed', closed_at = ?, updated_at = ? WHERE task_id = ?").run(now, now, taskId);
1824
- }
1825
- delete(taskId) {
1826
- this.db.prepare("DELETE FROM lark_card WHERE task_id = ?").run(taskId);
1827
- }
1828
- close() {
1829
- try {
1830
- this.db.close?.();
1831
- } catch {}
1832
- }
1833
- }
1834
- function getDefaultLarkCardDbPath() {
1835
- const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
1836
- return join(home, ".local", "share", "roy-agent", "lark-cards.db");
1837
- }
1838
- function createLarkCardStateStore(dbPath) {
1839
- const isTestEnv = "BUN_TEST_WORKER_ID" in process.env;
1840
- if (!dbPath && !process.env.ROY_AGENT_LARK_CARD_DB && isTestEnv) {
1841
- return new MemoryLarkCardStateStore;
1842
- }
1843
- const path = dbPath ?? process.env.ROY_AGENT_LARK_CARD_DB ?? getDefaultLarkCardDbPath();
1844
- try {
1845
- return new SqliteLarkCardStateStore(path);
1846
- } catch (err) {
1847
- logger6.warn(`LarkCardStateStore: sqlite unavailable at ${path} (${err.message}) ` + `→ falling back to in-memory (卡片仍可发,但重启后无法 patch 旧卡)`);
1848
- return new MemoryLarkCardStateStore;
1849
- }
1850
- }
1698
+ import { exec } from "node:child_process";
1699
+ import { promisify } from "node:util";
1851
1700
 
1852
1701
  // src/env/task/plugins/card-chat-routing.ts
1853
1702
  var CTX_FIELD_LARK_PER_TASK_CHAT_ID = "larkPerTaskChatId";
@@ -1925,804 +1774,2538 @@ function decideCardChatTarget(input) {
1925
1774
  };
1926
1775
  }
1927
1776
 
1928
- // src/env/task/plugins/lark-cli-task-notify-plugin.ts
1929
- var logger7 = createLogger("lark-cli:task-notify");
1930
- var CTX_FIELD_CHAT_ID = "larkPerTaskChatId";
1931
- var CTX_FIELD_CHAT_PROFILE = "larkPerTaskChatProfile";
1932
- function parseLarkPerTaskChatId(ctxStr) {
1933
- if (!ctxStr)
1934
- return;
1935
- try {
1936
- const obj = JSON.parse(ctxStr);
1937
- const id = obj[CTX_FIELD_CHAT_ID];
1938
- return typeof id === "string" ? id : undefined;
1939
- } catch {
1777
+ // src/env/task/plugins/card-subject.ts
1778
+ function pickCardSubject(value) {
1779
+ const rawSession = value?.sessionId;
1780
+ const sessionId = typeof rawSession === "string" && rawSession.trim() ? rawSession : undefined;
1781
+ if (!sessionId)
1940
1782
  return;
1783
+ const rawTask = value?.taskId;
1784
+ const taskId = typeof rawTask === "number" ? rawTask : typeof rawTask === "string" && rawTask.trim() ? Number(rawTask.trim()) : undefined;
1785
+ return {
1786
+ sessionId,
1787
+ ...taskId !== undefined && Number.isSafeInteger(taskId) && taskId > 0 ? { taskId } : {}
1788
+ };
1789
+ }
1790
+
1791
+ // src/env/task/plugins/lark-card-action-handler.ts
1792
+ var logger6 = createLogger("lark-card-action-handler");
1793
+ var execAsync = promisify(exec);
1794
+ var KNOWN_CARD_ACTIONS = new Set([
1795
+ "create_group",
1796
+ "disband_group",
1797
+ "refresh_activity"
1798
+ ]);
1799
+ function isCardActionName(v) {
1800
+ return typeof v === "string" && KNOWN_CARD_ACTIONS.has(v);
1801
+ }
1802
+ var ACTION_SUBJECT_FIELD = {
1803
+ create_group: "taskId or sessionId",
1804
+ disband_group: "taskId or sessionId",
1805
+ refresh_activity: "sessionId"
1806
+ };
1807
+ function validateCardActionValue(raw) {
1808
+ const action = raw.action;
1809
+ if (!isCardActionName(action)) {
1810
+ return { ok: false, action: String(action), error: `Unknown action: ${String(action)}` };
1811
+ }
1812
+ const requiredField = ACTION_SUBJECT_FIELD[action];
1813
+ if (action === "create_group" || action === "disband_group") {
1814
+ const rawTaskId = raw.taskId;
1815
+ const rawSessionId2 = typeof raw.sessionId === "string" ? raw.sessionId.trim() : "";
1816
+ if (rawTaskId === undefined || rawTaskId === null || rawTaskId === "") {
1817
+ if (rawSessionId2) {
1818
+ return { ok: true, action, sessionId: rawSessionId2 };
1819
+ }
1820
+ return {
1821
+ ok: false,
1822
+ action,
1823
+ error: `Missing required field: ${requiredField} (action=${action})`
1824
+ };
1825
+ }
1826
+ const taskId = typeof rawTaskId === "number" ? rawTaskId : typeof rawTaskId === "string" ? parseInt(rawTaskId, 10) : Number.NaN;
1827
+ if (!Number.isFinite(taskId) || Number.isNaN(taskId) || taskId <= 0) {
1828
+ return { ok: false, action, error: `Invalid taskId: ${String(rawTaskId)}` };
1829
+ }
1830
+ return {
1831
+ ok: true,
1832
+ action,
1833
+ taskId,
1834
+ ...rawSessionId2 ? { sessionId: rawSessionId2 } : {}
1835
+ };
1836
+ }
1837
+ const rawSessionId = raw.sessionId;
1838
+ const sessionId = typeof rawSessionId === "string" ? rawSessionId.trim() : "";
1839
+ if (!sessionId) {
1840
+ return {
1841
+ ok: false,
1842
+ action,
1843
+ error: `Missing required field: ${requiredField} (action=${action})`
1844
+ };
1941
1845
  }
1846
+ return { ok: true, action, sessionId };
1942
1847
  }
1943
- function parseLarkPerTaskChatProfile(ctxStr) {
1944
- if (!ctxStr)
1848
+ function extractTargetSessionId(context) {
1849
+ if (!context)
1945
1850
  return;
1946
1851
  try {
1947
- const obj = JSON.parse(ctxStr);
1948
- const profile = obj[CTX_FIELD_CHAT_PROFILE];
1949
- return typeof profile === "string" ? profile : undefined;
1852
+ const parsed = JSON.parse(context);
1853
+ for (const key of ["subSessionId", "sessionId"]) {
1854
+ const v = parsed[key];
1855
+ if (typeof v === "string" && v.trim().length > 0)
1856
+ return v.trim();
1857
+ }
1858
+ return;
1950
1859
  } catch {
1951
1860
  return;
1952
1861
  }
1953
1862
  }
1954
- function isTerminalTaskStatus(status) {
1955
- return status === "completed" || status === "cancelled" || status === "failed" || status === "timeout" || status === "archived";
1863
+ async function resolveGroupLinkTargetSessionId(event, ports, taskId, taskContext) {
1864
+ const fromCard = event.actionValue?.sessionId;
1865
+ if (typeof fromCard === "string" && fromCard.trim().length > 0) {
1866
+ return { sessionId: fromCard.trim(), source: "card" };
1867
+ }
1868
+ const fromContext = extractTargetSessionId(taskContext);
1869
+ if (fromContext) {
1870
+ return { sessionId: fromContext, source: "task.context" };
1871
+ }
1872
+ if (ports.resolveSessionIdByTask) {
1873
+ try {
1874
+ const resolved = await ports.resolveSessionIdByTask(taskId);
1875
+ if (typeof resolved === "string" && resolved.trim().length > 0) {
1876
+ return { sessionId: resolved.trim(), source: "resolveSessionIdByTask" };
1877
+ }
1878
+ } catch (err) {
1879
+ logger6.warn(`resolveGroupLinkTargetSessionId: resolveSessionIdByTask(${taskId}) 失败(继续跳过 link): ` + `${err.message}`);
1880
+ }
1881
+ }
1882
+ return { sessionId: undefined, source: "none" };
1956
1883
  }
1957
- function mapTaskStatusToCardStatus(taskStatus) {
1958
- switch (taskStatus) {
1959
- case "cancelled":
1960
- return "cancelled";
1961
- case "paused":
1962
- case "archived":
1963
- return "stopped";
1964
- case "failed":
1965
- case "timeout":
1966
- return "failed";
1967
- default:
1968
- return "completed";
1884
+ async function attachGroupSessionForTask(event, ports, taskId, chatId, taskContext) {
1885
+ if (!ports.attachGroupSession)
1886
+ return;
1887
+ const { sessionId: targetSessionId, source } = await resolveGroupLinkTargetSessionId(event, ports, taskId, taskContext);
1888
+ if (!targetSessionId) {
1889
+ logger6.warn(`attachGroupSessionForTask: 取不到目标 session(card/context/resolveSessionIdByTask 全空),` + `跳过关联 —— 群 ${chatId} 将是独立空会话(无背景提示)`);
1890
+ return;
1891
+ }
1892
+ try {
1893
+ await ports.attachGroupSession({
1894
+ chatId,
1895
+ originSessionId: targetSessionId,
1896
+ title: `Lark Chat ${chatId}`
1897
+ });
1898
+ logger6.info(`attachGroupSessionForTask: ${chatId} ← ${targetSessionId}` + `(来源=${source},已注入 1 条 user 事件作为背景提示)`);
1899
+ } catch (err) {
1900
+ logger6.warn(`attachGroupSessionForTask: attach(${chatId} ← ${targetSessionId}) 失败(不阻断建群): ` + `${err.message}`);
1969
1901
  }
1970
1902
  }
1971
- var NotifyType;
1972
- ((NotifyType2) => {
1973
- NotifyType2["TASK_CREATED"] = "task_created";
1974
- NotifyType2["TASK_COMPLETED"] = "task_completed";
1975
- NotifyType2["TASK_UPDATED"] = "task_updated";
1976
- NotifyType2["TASK_FAILED"] = "task_failed";
1977
- NotifyType2["OPERATION_CREATED"] = "operation_created";
1978
- })(NotifyType ||= {});
1979
-
1980
- class LarkCliTaskNotifyHook {
1981
- name;
1982
- priority;
1983
- description;
1984
- pluginName;
1985
- sourceId;
1986
- config;
1987
- cardNotifier;
1988
- cardMessageIds = new Map;
1989
- cardStateStore;
1990
- restoredTaskIds = new Set;
1991
- lastNotifyTimestamps = new Map;
1992
- trailingPatchTimers = new Map;
1993
- cardRoutingPorts;
1994
- static CARD_START_FAILED_SENTINEL = "__card_start_failed__";
1995
- static CARD_START_INFLIGHT_SENTINEL = "__card_start_inflight__";
1996
- constructor(config = {}) {
1997
- this.name = config.pluginName || "LarkCliTaskNotifyHook";
1998
- this.priority = config.priority ?? 5;
1999
- this.description = "LarkCli 任务通知 Hook(卡片模式)";
2000
- this.pluginName = config.pluginName;
2001
- this.sourceId = config.sourceId;
2002
- this.config = {
2003
- enabled: true,
2004
- priority: 5,
2005
- notifyTypes: [
2006
- "task_created" /* TASK_CREATED */,
2007
- "task_updated" /* TASK_UPDATED */,
2008
- "task_completed" /* TASK_COMPLETED */,
2009
- "operation_created" /* OPERATION_CREATED */
2010
- ],
2011
- ...config
1903
+ var CTX_FIELD_CHAT_ID = "larkPerTaskChatId";
1904
+ var CTX_FIELD_CHAT_NAME = "larkPerTaskChatName";
1905
+ var CTX_FIELD_CHAT_CREATED_AT = "larkPerTaskChatCreatedAt";
1906
+ var CTX_FIELD_CHAT_TASK_ID = "larkPerTaskChatTaskId";
1907
+ var CTX_FIELD_CHAT_INVITEES = "larkPerTaskChatInvitees";
1908
+ var cardActionInflight = new Map;
1909
+ async function handleCardAction(event, ports) {
1910
+ const rawAction = event.actionValue.action;
1911
+ if (!isCardActionName(rawAction)) {
1912
+ return { ok: false, action: String(rawAction), error: `Unknown action: ${String(rawAction)}` };
1913
+ }
1914
+ if (rawAction === "refresh_activity") {
1915
+ return await handleRefreshSessionCard(event, ports);
1916
+ }
1917
+ const validation = validateCardActionValue(event.actionValue);
1918
+ if (!validation.ok) {
1919
+ const failed = validation;
1920
+ return { ok: false, action: rawAction, error: failed.error };
1921
+ }
1922
+ if (validation.action === "refresh_activity") {
1923
+ return await handleRefreshSessionCard(event, ports);
1924
+ }
1925
+ const groupValidation = validation;
1926
+ const action = groupValidation.action;
1927
+ const taskId = groupValidation.taskId;
1928
+ const subjectSessionId = groupValidation.sessionId;
1929
+ const inflightSubject = event.messageId || (taskId !== undefined ? String(taskId) : subjectSessionId) || "-";
1930
+ const inflightKey = `${event.profile ?? "-"}:${inflightSubject}:${action}`;
1931
+ const inflight = cardActionInflight.get(inflightKey);
1932
+ if (inflight) {
1933
+ logger6.warn(`handleCardAction: ${inflightKey} 已有同操作在处理中(疑似用户重复点击),本次忽略。`);
1934
+ return { ok: false, action, error: `重复点击:${inflightKey} 正在处理中,已忽略` };
1935
+ }
1936
+ const pending = taskId !== undefined ? handleCardActionInner(event, ports, action, taskId) : handleGroupActionSessionOnly(event, ports, action, subjectSessionId);
1937
+ cardActionInflight.set(inflightKey, pending);
1938
+ try {
1939
+ return await pending;
1940
+ } finally {
1941
+ cardActionInflight.delete(inflightKey);
1942
+ }
1943
+ }
1944
+ async function handleRefreshSessionCard(event, ports) {
1945
+ const action = "refresh_activity";
1946
+ let subject = pickCardSubject(event.actionValue);
1947
+ if (!subject) {
1948
+ const legacyTaskId = parseTaskIdLoose(event.actionValue.taskId);
1949
+ if (legacyTaskId !== undefined) {
1950
+ let resolved;
1951
+ try {
1952
+ resolved = await ports.resolveSessionIdByTask?.(legacyTaskId);
1953
+ } catch {
1954
+ resolved = undefined;
1955
+ }
1956
+ if (resolved) {
1957
+ subject = { sessionId: resolved, taskId: legacyTaskId };
1958
+ logger6.info(`handleCardAction: 旧 task 卡 #${legacyTaskId} 无 sessionId,` + `反查得到 ${resolved}(按 session 主键刷新)`);
1959
+ }
1960
+ }
1961
+ }
1962
+ if (!subject) {
1963
+ return {
1964
+ ok: false,
1965
+ action,
1966
+ error: `Missing required field: sessionId (action=${action}; sessionId is required ` + `as the card primary key, got taskId=${String(event.actionValue.taskId)})`
2012
1967
  };
2013
- if (this.config.cardMode !== false) {
2014
- this.cardNotifier = config.cardNotifier ?? new LarkCardPatchNotifier;
2015
- this.cardStateStore = config.cardStateStore ?? createLarkCardStateStore();
1968
+ }
1969
+ const canRenderSessionCard = ports.loadSessionActivity !== undefined || typeof ports.notifier?.buildSessionCardWithActivity === "function";
1970
+ if (!canRenderSessionCard && ports.loadSubSessionActivity && subject.taskId !== undefined) {
1971
+ const legacyTask = await ports.getTask?.(subject.taskId);
1972
+ if (legacyTask) {
1973
+ logger6.info(`handleCardAction: 旧装配点(无 loadSessionActivity)—— task #${subject.taskId} ` + `刷新回落到 task 主键路径(保留 #5360 之前的可达性)`);
1974
+ return await handleRefreshActivity(event, ports, legacyTask, subject.taskId);
2016
1975
  }
2017
1976
  }
2018
- setCardRoutingPorts(ports) {
2019
- this.cardRoutingPorts = ports;
2020
- logger7.debug(`CardRoutingPorts injected: getTask=${typeof ports.getTask}`);
2021
- }
2022
- hasCardRoutingPorts() {
2023
- return this.cardRoutingPorts !== undefined;
1977
+ if (!canRenderSessionCard && !ports.refreshSessionCard) {
1978
+ logger6.warn(`handleCardAction: refresh_activity 已通过 sessionId 校验(sessionId=${subject.sessionId}),` + `但既没有 ports.refreshSessionCard,也没有 loadSessionActivity / ` + `buildSessionCardWithActivity —— 无 session 卡渲染实现。`);
1979
+ return {
1980
+ ok: false,
1981
+ action,
1982
+ error: `refresh_activity not available: no session card renderer injected ` + `(sessionId=${subject.sessionId}; ports.refreshSessionCard / loadSessionActivity both missing)`
1983
+ };
2024
1984
  }
2025
- async execute(ctx) {
2026
- if (!this.config.enabled) {
2027
- return;
1985
+ if (ports.refreshSessionCard) {
1986
+ const inflightKey = `${event.profile ?? "-"}:${event.messageId || subject.sessionId}:${action}`;
1987
+ const inflight = cardActionInflight.get(inflightKey);
1988
+ if (inflight) {
1989
+ logger6.warn(`handleCardAction: ${inflightKey} 已有同操作在处理中(疑似用户重复点击),本次忽略。`);
1990
+ return { ok: false, action, error: `重复点击:${inflightKey} 正在处理中,已忽略` };
2028
1991
  }
2029
- const env = getEnvContextOrEmpty();
2030
- if (this.pluginName && this.sourceId) {
2031
- const key = `${this.sourceId}:${this.pluginName}`;
2032
- if (env.pluginEnabled && env.pluginEnabled[key] !== true) {
2033
- logger7.debug(`Plugin not enabled for source: ${key}`);
2034
- return;
1992
+ const pending = (async () => {
1993
+ try {
1994
+ const result = await ports.refreshSessionCard({
1995
+ sessionId: subject.sessionId,
1996
+ chatId: event.chatId,
1997
+ messageId: event.messageId,
1998
+ token: event.token,
1999
+ profile: event.profile
2000
+ });
2001
+ if (result.error) {
2002
+ return { ok: false, action, error: result.error };
2003
+ }
2004
+ return { ok: true, action, cardUpdate: result.cardUpdate };
2005
+ } catch (err) {
2006
+ logger6.error(`handleCardAction.refresh_activity failed: ${err.message}`);
2007
+ return { ok: false, action, error: err.message };
2035
2008
  }
2009
+ })();
2010
+ cardActionInflight.set(inflightKey, pending);
2011
+ try {
2012
+ return await pending;
2013
+ } finally {
2014
+ cardActionInflight.delete(inflightKey);
2036
2015
  }
2037
- switch (ctx.hookPoint) {
2038
- case TaskHookPoints.AFTER_CREATE:
2039
- await this.onAfterCreate(ctx);
2040
- break;
2041
- case TaskHookPoints.AFTER_UPDATE:
2042
- await this.onAfterUpdate(ctx);
2043
- break;
2044
- case TaskHookPoints.AFTER_COMPLETE:
2045
- await this.onAfterComplete(ctx);
2046
- break;
2047
- case TaskHookPoints.OPERATION_AFTER_CREATE:
2048
- await this.onAfterOperationCreate(ctx);
2049
- break;
2050
- case TaskHookPoints.AFTER_DELETE:
2051
- await this.onAfterDelete(ctx);
2052
- break;
2016
+ }
2017
+ return await handleRefreshSessionActivity(event, ports, subject);
2018
+ }
2019
+ async function handleGroupActionSessionOnly(event, ports, action, sessionId) {
2020
+ const existing = await lookupSessionGroup(ports, sessionId);
2021
+ if (action === "create_group") {
2022
+ if (existing?.chatId) {
2023
+ logger6.info(`handleGroupActionSessionOnly: 会话 ${sessionId} 已有群 ${existing.chatId}` + `(幂等命中,不重复建群)`);
2024
+ return {
2025
+ ok: true,
2026
+ action,
2027
+ newChatId: existing.chatId,
2028
+ cardUpdate: buildCardAfterSessionGroupChange(event, ports, {
2029
+ sessionId,
2030
+ chatId: existing.chatId
2031
+ })
2032
+ };
2033
+ }
2034
+ if (!ports.createGroupChat) {
2035
+ return { ok: false, action, error: "ports.createGroupChat not injected" };
2036
+ }
2037
+ try {
2038
+ let title;
2039
+ try {
2040
+ const t = await ports.getSessionTitle?.(sessionId);
2041
+ if (typeof t === "string" && t.trim())
2042
+ title = t.trim();
2043
+ } catch (err) {
2044
+ logger6.warn(`handleGroupActionSessionOnly: getSessionTitle(${sessionId}) 失败(用 sessionId 兜底): ` + `${err.message}`);
2045
+ }
2046
+ const renderedName = truncateText(`会话 ${title ?? sessionId}`, 60);
2047
+ const description = truncateText(`roy-agent session ${sessionId}`, 100);
2048
+ const operatorOpenId = extractOperatorOpenId(event.operatorId);
2049
+ const initialUserIds = operatorOpenId ? [operatorOpenId] : undefined;
2050
+ let initialBotIds;
2051
+ try {
2052
+ const botAppId = await resolveBotAppId(event.profile);
2053
+ if (botAppId)
2054
+ initialBotIds = [botAppId];
2055
+ } catch (err) {
2056
+ logger6.warn(`handleGroupActionSessionOnly: resolveBotAppId 失败` + `(bot 可能不在群里,发卡会 230002): ${err.message}`);
2057
+ }
2058
+ const chatId = await ports.createGroupChat({
2059
+ name: renderedName,
2060
+ description,
2061
+ profile: event.profile,
2062
+ initialUserIds,
2063
+ initialBotIds
2064
+ });
2065
+ if (operatorOpenId && !initialUserIds && ports.inviteUser) {
2066
+ try {
2067
+ await ports.inviteUser(chatId, operatorOpenId, event.profile);
2068
+ } catch (err) {
2069
+ logger6.warn(`handleGroupActionSessionOnly: inviteUser(${chatId}, ${operatorOpenId}) 失败` + `(不阻断主流程): ${err.message}`);
2070
+ }
2071
+ }
2072
+ await attachGroupSessionForSession(ports, chatId, sessionId, renderedName);
2073
+ if (ports.notifier?.upsertSessionCard) {
2074
+ try {
2075
+ const cardRes = await ports.notifier.upsertSessionCard({
2076
+ chatId,
2077
+ sessionId: chatId,
2078
+ title: renderedName,
2079
+ lifecycle: "running",
2080
+ perTaskChatId: chatId
2081
+ });
2082
+ logger6.info(`handleGroupActionSessionOnly: 新群卡已发 chatId=${chatId} ` + `messageId=${cardRes.messageId}(session_id=群 session,#5417)`);
2083
+ } catch (err) {
2084
+ logger6.warn(`handleGroupActionSessionOnly: 新群卡发送失败(不阻断建群): ` + `${err.message}`);
2085
+ }
2086
+ }
2087
+ return {
2088
+ ok: true,
2089
+ action,
2090
+ newChatId: chatId,
2091
+ cardUpdate: buildCardAfterSessionGroupChange(event, ports, {
2092
+ sessionId,
2093
+ chatId,
2094
+ chatName: renderedName
2095
+ })
2096
+ };
2097
+ } catch (err) {
2098
+ logger6.error(`handleCardAction.create_group(session-only) failed: ${err.message}`);
2099
+ return { ok: false, action, error: err.message };
2053
2100
  }
2054
2101
  }
2055
- async onAfterCreate(ctx) {
2056
- const { task } = ctx.data;
2057
- if (!task || !this.shouldNotify("task_created" /* TASK_CREATED */))
2058
- return;
2059
- if (!this.isCardModeEnabled())
2060
- return;
2061
- await this.sendCardStart(task);
2102
+ if (!existing?.chatId) {
2103
+ return {
2104
+ ok: false,
2105
+ action,
2106
+ error: `会话 ${sessionId} 没有专属群,无需解散`
2107
+ };
2062
2108
  }
2063
- async onAfterUpdate(ctx) {
2064
- const data = ctx.data;
2065
- if (!data?.task)
2066
- return;
2067
- const { task, changes } = data;
2068
- const terminalCardStatus = this.cardStatusOf(task, changes);
2069
- if (terminalCardStatus) {
2070
- const notifyType2 = terminalCardStatus === "completed" || terminalCardStatus === "cancelled" ? "task_completed" /* TASK_COMPLETED */ : "task_updated" /* TASK_UPDATED */;
2071
- if (!this.shouldNotify(notifyType2))
2072
- return;
2073
- if (!this.isCardModeEnabled())
2074
- return;
2075
- await this.sendCardFinish(task);
2076
- return;
2077
- }
2078
- let notifyType;
2079
- if (changes?.status) {
2080
- notifyType = "task_updated" /* TASK_UPDATED */;
2081
- } else if (changes && Object.keys(changes).length > 0) {
2082
- notifyType = "task_updated" /* TASK_UPDATED */;
2109
+ if (!ports.deleteGroupChat) {
2110
+ return { ok: false, action, error: "ports.deleteGroupChat not injected" };
2111
+ }
2112
+ try {
2113
+ await ports.deleteGroupChat(existing.chatId, event.profile);
2114
+ if (!ports.detachGroupSession) {
2115
+ logger6.warn(`handleGroupActionSessionOnly: detachGroupSession 端口未注入 ⇒ 群 ${existing.chatId} ` + `已解散但关联标记残留(反查仍会命中这个群)`);
2083
2116
  } else {
2084
- return;
2117
+ const unlinkTarget = existing.aliasSessionId ?? existing.chatId;
2118
+ try {
2119
+ await ports.detachGroupSession(unlinkTarget);
2120
+ } catch (err) {
2121
+ logger6.warn(`handleGroupActionSessionOnly: detachGroupSession(${unlinkTarget}) 失败` + `(不阻断解散): ${err.message}`);
2122
+ }
2085
2123
  }
2086
- if (!this.shouldNotify(notifyType))
2087
- return;
2088
- if (!this.isCardModeEnabled())
2089
- return;
2090
- await this.sendCardUpdate(task, changes);
2124
+ return {
2125
+ ok: true,
2126
+ action,
2127
+ cardUpdate: buildCardAfterSessionGroupChange(event, ports, { sessionId })
2128
+ };
2129
+ } catch (err) {
2130
+ logger6.error(`handleCardAction.disband_group(session-only) failed: ${err.message}`);
2131
+ return { ok: false, action, error: err.message };
2091
2132
  }
2092
- cardStatusOf(task, changes) {
2093
- const changedStatus = changes?.status;
2094
- const effective = String(changedStatus ?? task.status ?? "");
2095
- switch (effective) {
2096
- case "completed":
2097
- return "completed";
2098
- case "cancelled":
2099
- return "cancelled";
2100
- case "paused":
2101
- return "stopped";
2102
- case "archived":
2103
- return "stopped";
2104
- case "failed":
2105
- case "timeout":
2106
- return "failed";
2107
- default:
2108
- break;
2133
+ }
2134
+ async function lookupSessionGroup(ports, sessionId) {
2135
+ if (!ports.findGroupChatForSession)
2136
+ return;
2137
+ try {
2138
+ const r = await ports.findGroupChatForSession(sessionId);
2139
+ if (r && typeof r.chatId === "string" && r.chatId.trim()) {
2140
+ return { chatId: r.chatId.trim(), ...r.aliasSessionId ? { aliasSessionId: r.aliasSessionId } : {} };
2109
2141
  }
2110
2142
  return;
2143
+ } catch (err) {
2144
+ logger6.warn(`lookupSessionGroup(${sessionId}) 失败(当作没有群): ${err.message}`);
2145
+ return;
2111
2146
  }
2112
- async onAfterComplete(ctx) {
2113
- const data = ctx.data;
2114
- const task = data?.task ?? data;
2115
- if (!task || !this.shouldNotify("task_completed" /* TASK_COMPLETED */))
2116
- return;
2117
- if (!this.isCardModeEnabled())
2118
- return;
2119
- const terminalStatus = data.terminalStatus;
2120
- if (terminalStatus === "to_validate" || task.status === "to_validate") {
2121
- return;
2122
- }
2123
- await this.sendCardFinish(task);
2147
+ }
2148
+ async function attachGroupSessionForSession(ports, chatId, sessionId, title) {
2149
+ if (!ports.attachGroupSession) {
2150
+ logger6.warn(`attachGroupSessionForSession: 端口未注入 ⇒ 群 ${chatId} 将没有背景提示事件` + `(连不到 ${sessionId} 的溯源信息)`);
2151
+ return;
2124
2152
  }
2125
- async onAfterDelete(ctx) {
2126
- const data = ctx.data;
2127
- const taskId = data?.id;
2128
- if (data?.deleted !== true || typeof taskId !== "number")
2129
- return;
2130
- if (!this.shouldNotify("task_completed" /* TASK_COMPLETED */))
2131
- return;
2132
- if (!this.isCardModeEnabled())
2133
- return;
2134
- if (!this.cardNotifier)
2135
- return;
2136
- let record;
2137
- try {
2138
- record = this.cardStateStore?.get(taskId);
2139
- } catch (err) {
2140
- logger7.warn(`onAfterDelete: 读取卡片记录失败 task #${taskId}: ${err.message}`);
2141
- }
2142
- const messageId = record?.messageId || this.cardMessageIds.get(taskId);
2143
- if (!messageId) {
2144
- logger7.debug(`onAfterDelete: task #${taskId} 无卡片记录,skip(不发新卡)`);
2145
- return;
2146
- }
2147
- this.restoreCardState(taskId);
2148
- const profile = record?.profile;
2149
- const chatId = record?.chatId;
2150
- try {
2151
- await this.cardNotifier.finishTask({
2152
- messageId,
2153
- profile,
2154
- chatId,
2155
- status: "deleted",
2156
- finalStep: "任务已取消(删除)"
2157
- });
2158
- } catch (err) {
2159
- logger7.warn(`onAfterDelete: patch 卡片失败 task #${taskId} message=${messageId}: ${err.message}`);
2160
- }
2161
- try {
2162
- await this.cardNotifier.unpinMessage(messageId, profile);
2163
- } catch (err) {
2164
- logger7.warn(`onAfterDelete: unpin 失败 task #${taskId} message=${messageId}: ${err.message}`);
2165
- }
2166
- this.cancelTrailingPatch(taskId);
2167
- this.persistClosed(taskId);
2168
- this.persistPinned(taskId, false);
2169
- this.cardMessageIds.delete(taskId);
2170
- logger7.debug(`onAfterDelete: task #${taskId} 卡片已收尾 status=deleted message=${messageId}`);
2153
+ try {
2154
+ await ports.attachGroupSession({
2155
+ chatId,
2156
+ originSessionId: sessionId,
2157
+ ...title ? { title } : {}
2158
+ });
2159
+ logger6.info(`attachGroupSessionForSession: ${chatId} ← ${sessionId}` + `(已注入 1 条 user 事件作为背景提示)`);
2160
+ } catch (err) {
2161
+ logger6.warn(`attachGroupSessionForSession: attach(${chatId} ← ${sessionId}) 失败(不阻断建群): ` + `${err.message}`);
2171
2162
  }
2172
- async onAfterOperationCreate(ctx) {
2173
- const { operation } = ctx.data;
2174
- if (!operation)
2175
- return;
2176
- if (!this.shouldNotify("operation_created" /* OPERATION_CREATED */))
2177
- return;
2178
- if (!this.isCardModeEnabled())
2179
- return;
2180
- const persisted = this.restoreCardState(operation.taskId);
2181
- const messageId = this.cardMessageIds.get(operation.taskId);
2182
- if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL || messageId === LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL) {
2183
- logger7.debug(`onAfterOperationCreate: no usable card for task #${operation.taskId}, skip`);
2184
- return;
2185
- }
2186
- if (persisted && persisted.status !== "open") {
2187
- logger7.debug(`onAfterOperationCreate: task #${operation.taskId} card status=${persisted.status}, skip`);
2188
- return;
2163
+ }
2164
+ function buildCardAfterSessionGroupChange(event, ports, opts) {
2165
+ const preserved = ports.notifier?.applySessionGroupChange?.(event.messageId, {
2166
+ sessionId: opts.sessionId,
2167
+ ...opts.chatId ? { perTaskChatId: opts.chatId } : {},
2168
+ ...opts.chatName ? { perTaskChatName: opts.chatName } : {}
2169
+ });
2170
+ if (preserved)
2171
+ return preserved;
2172
+ logger6.warn(`buildCardAfterSessionGroupChange: notifier 无 messageId=${event.messageId} 的状态,` + `fallback 重建 session 卡(历史里程碑可能丢失)`);
2173
+ return buildSessionCard({
2174
+ sessionId: opts.sessionId,
2175
+ ...opts.chatId ? { perTaskChatId: opts.chatId } : {},
2176
+ ...opts.chatName ? { perTaskChatName: opts.chatName } : {}
2177
+ });
2178
+ }
2179
+ async function handleCardActionInner(event, ports, action, taskId) {
2180
+ if (!ports.getTask) {
2181
+ return { ok: false, action, error: "ports.getTask not injected" };
2182
+ }
2183
+ const task = await ports.getTask(taskId);
2184
+ if (!task) {
2185
+ return { ok: false, action, error: `Task #${taskId} not found` };
2186
+ }
2187
+ const existingChatId = extractPerTaskChatId(task.context);
2188
+ switch (action) {
2189
+ case "create_group":
2190
+ return await handleCreateGroup(event, ports, task, existingChatId);
2191
+ case "disband_group":
2192
+ return await handleDisbandGroup(event, ports, task, existingChatId);
2193
+ }
2194
+ }
2195
+ function buildCardAfterGroupChange(event, ports, task, perTaskChatId, perTaskChatName) {
2196
+ const meta = buildTaskMetaFromTask(task, perTaskChatId, perTaskChatName);
2197
+ const preserved = ports.notifier?.updateMetaAndBuildCard?.(event.messageId, meta, { clearPerTaskChat: !perTaskChatId });
2198
+ if (preserved) {
2199
+ logger6.debug(`buildCardAfterGroupChange: 复用 notifier 累积状态(里程碑历史保留)messageId=${event.messageId}`);
2200
+ return preserved;
2201
+ }
2202
+ logger6.warn(`buildCardAfterGroupChange: notifier 无 messageId=${event.messageId} 的状态,` + `fallback 重建卡片(该卡片的历史里程碑可能丢失)`);
2203
+ return buildTaskCard({
2204
+ status: "running",
2205
+ meta,
2206
+ milestones: [
2207
+ { title: "Task 创建", timestamp: new Date().toISOString(), description: task.current_status }
2208
+ ]
2209
+ });
2210
+ }
2211
+ async function handleCreateGroup(event, ports, task, existingChatId) {
2212
+ if (existingChatId) {
2213
+ const existingName = extractPerTaskChatName(task.context);
2214
+ logger6.info(`handleCreateGroup: Task #${task.id} 已有专属群 ${existingChatId}(幂等命中,不重复建群)`);
2215
+ await attachGroupSessionForTask(event, ports, task.id, existingChatId, task.context);
2216
+ return {
2217
+ ok: true,
2218
+ action: "create_group",
2219
+ newChatId: existingChatId,
2220
+ cardUpdate: buildCardAfterGroupChange(event, ports, task, existingChatId, existingName)
2221
+ };
2222
+ }
2223
+ if (!ports.createGroupChat) {
2224
+ return { ok: false, action: "create_group", error: "ports.createGroupChat not injected" };
2225
+ }
2226
+ try {
2227
+ const renderedName = truncateText(`Task #${task.id} - ${task.title ?? "(无标题)"}`, 60);
2228
+ const description = truncateText(`roy-agent task #${task.id} - ${task.project_path ?? ""}`, 100);
2229
+ const operatorOpenId = extractOperatorOpenId(event.operatorId);
2230
+ const initialUserIds = operatorOpenId ? [operatorOpenId] : undefined;
2231
+ const chatId = await ports.createGroupChat({
2232
+ name: renderedName,
2233
+ description,
2234
+ profile: event.profile,
2235
+ initialUserIds
2236
+ });
2237
+ let invitedOpenIds = [];
2238
+ if (operatorOpenId) {
2239
+ if (initialUserIds) {
2240
+ invitedOpenIds = [operatorOpenId];
2241
+ logger6.debug(`handleCreateGroup: ${operatorOpenId} 已在建群时邀请(initialUserIds 路径)`);
2242
+ } else if (ports.inviteUser) {
2243
+ try {
2244
+ await ports.inviteUser(chatId, operatorOpenId, event.profile);
2245
+ invitedOpenIds = [operatorOpenId];
2246
+ logger6.debug(`handleCreateGroup: 走 inviteUser 端口邀请 ${operatorOpenId} 进 ${chatId}`);
2247
+ } catch (err) {
2248
+ logger6.warn(`handleCreateGroup: inviteUser(${chatId}, ${operatorOpenId}) 失败(不阻断主流程): ${err.message}`);
2249
+ }
2250
+ } else {
2251
+ logger6.warn(`handleCreateGroup: createGroupChat 不支持 initialUserIds 且 inviteUser 未注入,` + `${operatorOpenId} 未被拉进群 ${chatId}。` + `需升级 caller(bridge/real impl)使用 ports.inviteUser 或实现 createGroupChat.initialUserIds 参数。`);
2252
+ }
2189
2253
  }
2190
- const { milestones, processSteps } = this.deriveTimelineFromOperation(operation);
2191
- if (milestones.length === 0 && processSteps.length === 0)
2192
- return;
2193
- const env = getEnvContextOrEmpty();
2194
- const profile = env.replyChannel?.profile;
2195
- this.cardNotifier.mergeTimelineState?.(messageId, milestones, processSteps);
2196
- this.refreshTimeline(operation.taskId);
2197
- const throttled = this.shouldThrottleOperation(operation);
2198
- if (throttled) {
2199
- this.scheduleTrailingPatch(operation.taskId, messageId, profile);
2200
- logger7.debug(`[throttle] OPERATION_CREATED for task #${operation.taskId} ` + `merged into state + ledger; card patch deferred to window end`);
2201
- return;
2254
+ const newContext = patchContext(task.context ?? "{}", {
2255
+ [CTX_FIELD_CHAT_ID]: chatId,
2256
+ [CTX_FIELD_CHAT_NAME]: renderedName,
2257
+ [CTX_FIELD_CHAT_CREATED_AT]: new Date().toISOString(),
2258
+ [CTX_FIELD_CHAT_TASK_ID]: task.id,
2259
+ [CTX_FIELD_CHAT_INVITEES]: invitedOpenIds.length > 0 ? invitedOpenIds : undefined
2260
+ });
2261
+ if (ports.updateTaskContext) {
2262
+ await ports.updateTaskContext(task.id, newContext);
2202
2263
  }
2203
- await this.patchOperationTimeline(operation.taskId, messageId, profile, milestones, processSteps);
2264
+ const cardUpdate = buildCardAfterGroupChange(event, ports, task, chatId, renderedName);
2265
+ await attachGroupSessionForTask(event, ports, task.id, chatId, task.context);
2266
+ return { ok: true, action: "create_group", newChatId: chatId, cardUpdate };
2267
+ } catch (err) {
2268
+ logger6.error(`handleCardAction.create_group failed: ${err.message}`);
2269
+ return { ok: false, action: "create_group", error: err.message };
2204
2270
  }
2205
- async patchOperationTimeline(taskId, messageId, profile, milestones, processSteps) {
2206
- try {
2207
- await this.cardNotifier.updateTask({
2208
- messageId,
2271
+ }
2272
+ function extractOperatorOpenId(raw) {
2273
+ if (!raw)
2274
+ return;
2275
+ if (typeof raw === "string")
2276
+ return raw;
2277
+ if (typeof raw === "object" && typeof raw.open_id === "string")
2278
+ return raw.open_id;
2279
+ return;
2280
+ }
2281
+ function parseTaskIdLoose(raw) {
2282
+ if (typeof raw === "number") {
2283
+ return Number.isSafeInteger(raw) && raw > 0 ? raw : undefined;
2284
+ }
2285
+ if (typeof raw === "string" && /^\d+$/.test(raw.trim())) {
2286
+ const n = Number(raw.trim());
2287
+ return Number.isSafeInteger(n) && n > 0 ? n : undefined;
2288
+ }
2289
+ return;
2290
+ }
2291
+ async function handleRefreshSessionActivity(event, ports, subject) {
2292
+ const action = "refresh_activity";
2293
+ const sessionId = subject.sessionId;
2294
+ let probe;
2295
+ try {
2296
+ probe = await ports.loadSessionActivity?.(sessionId);
2297
+ } catch (err) {
2298
+ logger6.warn(`handleRefreshSessionActivity: loadSessionActivity failed for ${sessionId}: ` + `${err.message}(降级为「暂无数据」)`);
2299
+ probe = undefined;
2300
+ }
2301
+ let taskEnrichment;
2302
+ if (subject.taskId !== undefined) {
2303
+ taskEnrichment = await loadTaskEnrichment(subject.taskId, ports);
2304
+ }
2305
+ let recentMessages;
2306
+ try {
2307
+ recentMessages = await ports.loadSessionMessages?.(sessionId, RECENT_MESSAGE_LIMIT);
2308
+ } catch (err) {
2309
+ logger6.warn(`handleRefreshSessionActivity: loadSessionMessages failed for ${sessionId}: ` + `${err.message}(降级为不渲染最近消息面板)`);
2310
+ recentMessages = undefined;
2311
+ }
2312
+ const snapshot = probe?.activity ?? null;
2313
+ const sessionAlive = probe?.activity !== undefined && !probe?.terminal;
2314
+ const lifecycle = probe?.lifecycle ? probe.lifecycle : probe?.terminal ? "completed" : sessionAlive ? "running" : taskEnrichment?.lifecycle ?? "running";
2315
+ const preserved = ports.notifier?.buildSessionCardWithActivity?.(event.messageId, sessionId, snapshot, {
2316
+ lifecycle,
2317
+ task: taskEnrichment?.info,
2318
+ ...recentMessages !== undefined ? { recentMessages } : {}
2319
+ });
2320
+ if (preserved) {
2321
+ logger6.debug(`handleRefreshSessionActivity: ${sessionId} 活性已刷新 ` + `(messageCount=${snapshot?.messageCount ?? "n/a"}, task=${subject.taskId ?? "none"})`);
2322
+ return { ok: true, action, cardUpdate: preserved };
2323
+ }
2324
+ logger6.warn(`handleRefreshSessionActivity: notifier 无 messageId=${event.messageId} 的状态,` + `fallback 重建 session 卡`);
2325
+ const groupChat = taskEnrichment?.info ? undefined : await lookupSessionGroup(ports, sessionId);
2326
+ return {
2327
+ ok: true,
2328
+ action,
2329
+ cardUpdate: buildSessionCard({
2330
+ sessionId,
2331
+ activity: snapshot,
2332
+ lifecycle,
2333
+ ...taskEnrichment?.info ? { task: taskEnrichment.info } : {},
2334
+ ...groupChat?.chatId ? { perTaskChatId: groupChat.chatId } : {},
2335
+ ...recentMessages !== undefined ? { recentMessages } : {}
2336
+ })
2337
+ };
2338
+ }
2339
+ async function loadTaskEnrichment(taskId, ports) {
2340
+ try {
2341
+ const task = await ports.getTask?.(taskId);
2342
+ if (!task)
2343
+ return;
2344
+ const raw = task;
2345
+ return {
2346
+ lifecycle: lifecycleOfTaskStatus(raw.status),
2347
+ info: {
2348
+ id: typeof raw.id === "number" ? raw.id : taskId,
2349
+ ...raw.title ? { title: raw.title } : {},
2350
+ ...raw.status ? { status: raw.status } : {},
2351
+ ...typeof raw.progress === "number" ? { progress: raw.progress } : {},
2352
+ ...raw.current_status ? { currentStatus: raw.current_status } : {},
2353
+ ...Array.isArray(raw.milestones) && raw.milestones.length > 0 ? { milestones: raw.milestones } : {},
2354
+ ...raw.priority ? { priority: raw.priority } : {},
2355
+ ...raw.goals_and_expected_deliverables || raw.goals ? { goals: raw.goals_and_expected_deliverables ?? raw.goals } : {},
2356
+ ...typeof raw.parent_task_id === "number" && raw.parent_task_id > 0 ? { parentTaskId: raw.parent_task_id } : {},
2357
+ ...raw.created_at || raw.createdAt ? { createdAt: raw.created_at ?? raw.createdAt } : {},
2358
+ ...raw.updated_at || raw.updatedAt ? { updatedAt: raw.updated_at ?? raw.updatedAt } : {}
2359
+ }
2360
+ };
2361
+ } catch (err) {
2362
+ logger6.warn(`loadTaskEnrichment: task #${taskId} 读取失败(降级为无 task 信息): ` + `${err.message}`);
2363
+ return;
2364
+ }
2365
+ }
2366
+ function lifecycleOfTaskStatus(status) {
2367
+ switch (status) {
2368
+ case "completed":
2369
+ return "completed";
2370
+ case "failed":
2371
+ return "failed";
2372
+ case "cancelled":
2373
+ case "archived":
2374
+ case "paused":
2375
+ return "stopped";
2376
+ case "active":
2377
+ case "todo":
2378
+ case "to_validate":
2379
+ return "running";
2380
+ default:
2381
+ return;
2382
+ }
2383
+ }
2384
+ async function handleRefreshActivity(event, ports, task, taskId) {
2385
+ const action = "refresh_activity";
2386
+ let activity;
2387
+ try {
2388
+ activity = await ports.loadSubSessionActivity?.(taskId);
2389
+ } catch (err) {
2390
+ logger6.warn(`handleRefreshActivity: loadSubSessionActivity failed for task #${taskId}: ` + `${err.message}(降级为「暂无数据」)`);
2391
+ activity = undefined;
2392
+ }
2393
+ const snapshot = activity?.activity ?? null;
2394
+ const terminal = activity?.terminal ?? false;
2395
+ const preserved = ports.notifier?.buildCardWithActivity?.(event.messageId, snapshot, {
2396
+ terminal
2397
+ });
2398
+ if (preserved) {
2399
+ logger6.debug(`handleRefreshActivity: task #${taskId} 活性已刷新 ` + `(messageCount=${snapshot?.messageCount ?? "n/a"}, terminal=${terminal})`);
2400
+ return { ok: true, action, cardUpdate: preserved };
2401
+ }
2402
+ logger6.warn(`handleRefreshActivity: notifier 无 messageId=${event.messageId} 的状态,` + `fallback 重建卡片(历史里程碑可能丢失)`);
2403
+ const meta = buildTaskMetaFromTask(task, extractPerTaskChatId(task.context), undefined);
2404
+ return {
2405
+ ok: true,
2406
+ action,
2407
+ cardUpdate: buildTaskCard({
2408
+ status: cardStatusOfTaskStatus(task.status),
2409
+ meta,
2410
+ activity: snapshot,
2411
+ activityTerminal: terminal
2412
+ })
2413
+ };
2414
+ }
2415
+ function cardStatusOfTaskStatus(status) {
2416
+ switch (status) {
2417
+ case "completed":
2418
+ return "completed";
2419
+ case "cancelled":
2420
+ return "cancelled";
2421
+ case "failed":
2422
+ return "failed";
2423
+ case "paused":
2424
+ return "stopped";
2425
+ case "archived":
2426
+ return "stopped";
2427
+ default:
2428
+ return "running";
2429
+ }
2430
+ }
2431
+ async function handleDisbandGroup(event, ports, task, existingChatId) {
2432
+ if (!existingChatId) {
2433
+ return {
2434
+ ok: false,
2435
+ action: "disband_group",
2436
+ error: `Task #${task.id} 无专属群(context.larkPerTaskChatId 空),无需解散`
2437
+ };
2438
+ }
2439
+ if (!ports.deleteGroupChat) {
2440
+ return { ok: false, action: "disband_group", error: "ports.deleteGroupChat not injected" };
2441
+ }
2442
+ try {
2443
+ await ports.deleteGroupChat(existingChatId, event.profile);
2444
+ const newContext = patchContext(task.context ?? "{}", {
2445
+ [CTX_FIELD_CHAT_ID]: undefined,
2446
+ [CTX_FIELD_CHAT_NAME]: undefined,
2447
+ [CTX_FIELD_CHAT_CREATED_AT]: undefined,
2448
+ [CTX_FIELD_CHAT_TASK_ID]: undefined
2449
+ });
2450
+ if (ports.updateTaskContext) {
2451
+ await ports.updateTaskContext(task.id, newContext);
2452
+ }
2453
+ const cardUpdate = buildCardAfterGroupChange(event, ports, task, undefined, undefined);
2454
+ return { ok: true, action: "disband_group", cardUpdate };
2455
+ } catch (err) {
2456
+ logger6.error(`handleCardAction.disband_group failed: ${err.message}`);
2457
+ return { ok: false, action: "disband_group", error: err.message };
2458
+ }
2459
+ }
2460
+ function patchContext(contextStr, changes) {
2461
+ let obj = {};
2462
+ try {
2463
+ const parsed = JSON.parse(contextStr);
2464
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2465
+ obj = parsed;
2466
+ }
2467
+ } catch {
2468
+ obj = {};
2469
+ }
2470
+ for (const [k, v] of Object.entries(changes)) {
2471
+ if (v === undefined) {
2472
+ delete obj[k];
2473
+ } else {
2474
+ obj[k] = v;
2475
+ }
2476
+ }
2477
+ return JSON.stringify(obj);
2478
+ }
2479
+ function buildTaskMetaFromTask(task, perTaskChatId, perTaskChatName) {
2480
+ return {
2481
+ id: task.id,
2482
+ title: task.title,
2483
+ description: task.description,
2484
+ goals: task.goals,
2485
+ parentTaskId: typeof task.parent_task_id === "number" && task.parent_task_id > 0 ? task.parent_task_id : undefined,
2486
+ priority: task.priority,
2487
+ type: task.type,
2488
+ projectPath: task.project_path,
2489
+ context: task.context,
2490
+ currentStatus: task.current_status,
2491
+ perTaskChatId,
2492
+ perTaskChatName
2493
+ };
2494
+ }
2495
+ async function deleteGroupChatViaLarkCli(chatId, profile) {
2496
+ const profileFlag = profile ? `--profile ${profile} ` : "";
2497
+ try {
2498
+ const { stdout: authStdout } = await execAsync(`lark-cli ${profileFlag}auth status --json`, { timeout: 15000 });
2499
+ const appId = JSON.parse(authStdout).appId;
2500
+ if (!appId) {
2501
+ throw new Error(`lark-cli auth status 未返回 appId: ${authStdout.slice(0, 200)}`);
2502
+ }
2503
+ const cmd = `lark-cli ${profileFlag}api DELETE /open-apis/im/v1/chats/${chatId}/members ` + `--params '{"member_id_type":"app_id"}' ` + `--data ${shellQuote(JSON.stringify({ id_list: [appId] }))} ` + `--as bot`;
2504
+ const { stdout, stderr } = await execAsync(cmd, { timeout: 30000 });
2505
+ logger6.debug(`deleteGroupChatViaLarkCli(${chatId}) bot 退群 ok: ` + `stdout=${stdout?.slice(0, 300)} stderr=${stderr?.slice(0, 300)}`);
2506
+ } catch (err) {
2507
+ logger6.warn(`deleteGroupChatViaLarkCli(${chatId}) bot 退群失败(task.context 仍会清掉,` + `飞书侧群会保留):${err.message}`);
2508
+ }
2509
+ }
2510
+ function shellQuote(s) {
2511
+ return `'${s.replace(/'/g, `'\\''`)}'`;
2512
+ }
2513
+ async function resolveBotAppId(profile) {
2514
+ const profileFlag = profile ? `--profile ${profile} ` : "";
2515
+ const { stdout } = await execAsync(`lark-cli ${profileFlag}auth status --json`, { timeout: 15000 });
2516
+ const appId = JSON.parse(stdout).appId;
2517
+ if (!appId) {
2518
+ throw new Error(`lark-cli auth status 未返回 appId: ${stdout.slice(0, 200)}`);
2519
+ }
2520
+ return appId;
2521
+ }
2522
+ function truncateText(text, maxLen) {
2523
+ if (!text)
2524
+ return "";
2525
+ if (text.length <= maxLen)
2526
+ return text;
2527
+ return text.substring(0, Math.max(0, maxLen - 3)) + "...";
2528
+ }
2529
+ async function createGroupChatViaLarkCli(opts) {
2530
+ const profileFlag = opts.profile ? `--profile ${opts.profile} ` : "";
2531
+ const usersFlag = opts.initialUserIds && opts.initialUserIds.length > 0 ? `--users ${shellQuote(opts.initialUserIds.join(","))} ` : "";
2532
+ const botsFlag = opts.initialBotIds && opts.initialBotIds.length > 0 ? `--bots ${shellQuote(opts.initialBotIds.join(","))} ` : "";
2533
+ const cmd = `lark-cli ${profileFlag}im +chat-create --name ${shellQuote(truncateText(opts.name, 60))} ` + usersFlag + botsFlag + `--description ${shellQuote(truncateText(opts.description, 100))} --as bot`;
2534
+ const { stdout } = await execAsync(cmd, { timeout: 60000 });
2535
+ const m = stdout.match(/"chat_id"\s*:\s*"(oc_[a-f0-9]+)"/);
2536
+ if (!m) {
2537
+ throw new Error(`lark-cli +chat-create 返回无法解析: ${stdout.slice(0, 300)}`);
2538
+ }
2539
+ return m[1];
2540
+ }
2541
+
2542
+ // src/env/task/plugins/lark-cli-task-notify-plugin.ts
2543
+ var logger7 = createLogger("lark-cli:task-notify");
2544
+ var CTX_FIELD_CHAT_ID2 = "larkPerTaskChatId";
2545
+ var CTX_FIELD_CHAT_PROFILE = "larkPerTaskChatProfile";
2546
+ function parseLarkPerTaskChatId(ctxStr) {
2547
+ if (!ctxStr)
2548
+ return;
2549
+ try {
2550
+ const obj = JSON.parse(ctxStr);
2551
+ const id = obj[CTX_FIELD_CHAT_ID2];
2552
+ return typeof id === "string" ? id : undefined;
2553
+ } catch {
2554
+ return;
2555
+ }
2556
+ }
2557
+ function parseLarkPerTaskChatProfile(ctxStr) {
2558
+ if (!ctxStr)
2559
+ return;
2560
+ try {
2561
+ const obj = JSON.parse(ctxStr);
2562
+ const profile = obj[CTX_FIELD_CHAT_PROFILE];
2563
+ return typeof profile === "string" ? profile : undefined;
2564
+ } catch {
2565
+ return;
2566
+ }
2567
+ }
2568
+ function isTerminalTaskStatus(status) {
2569
+ return status === "completed" || status === "cancelled" || status === "failed" || status === "timeout" || status === "archived";
2570
+ }
2571
+ function mapTaskStatusToCardStatus(taskStatus) {
2572
+ switch (taskStatus) {
2573
+ case "cancelled":
2574
+ return "cancelled";
2575
+ case "paused":
2576
+ case "archived":
2577
+ return "stopped";
2578
+ case "failed":
2579
+ case "timeout":
2580
+ return "failed";
2581
+ default:
2582
+ return "completed";
2583
+ }
2584
+ }
2585
+ var NotifyType;
2586
+ ((NotifyType2) => {
2587
+ NotifyType2["TASK_CREATED"] = "task_created";
2588
+ NotifyType2["TASK_COMPLETED"] = "task_completed";
2589
+ NotifyType2["TASK_UPDATED"] = "task_updated";
2590
+ NotifyType2["TASK_FAILED"] = "task_failed";
2591
+ NotifyType2["OPERATION_CREATED"] = "operation_created";
2592
+ })(NotifyType ||= {});
2593
+
2594
+ class LarkCliTaskNotifyHook {
2595
+ name;
2596
+ priority;
2597
+ description;
2598
+ pluginName;
2599
+ sourceId;
2600
+ config;
2601
+ cardNotifier;
2602
+ cardMessageIds = new Map;
2603
+ cardStateStore;
2604
+ restoredTaskIds = new Set;
2605
+ lastNotifyTimestamps = new Map;
2606
+ trailingPatchTimers = new Map;
2607
+ cardRoutingPorts;
2608
+ static CARD_START_FAILED_SENTINEL = "__card_start_failed__";
2609
+ static CARD_START_INFLIGHT_SENTINEL = "__card_start_inflight__";
2610
+ constructor(config = {}) {
2611
+ this.name = config.pluginName || "LarkCliTaskNotifyHook";
2612
+ this.priority = config.priority ?? 5;
2613
+ this.description = "LarkCli 任务通知 Hook(卡片模式)";
2614
+ this.pluginName = config.pluginName;
2615
+ this.sourceId = config.sourceId;
2616
+ this.config = {
2617
+ enabled: true,
2618
+ priority: 5,
2619
+ notifyTypes: [
2620
+ "task_created" /* TASK_CREATED */,
2621
+ "task_updated" /* TASK_UPDATED */,
2622
+ "task_completed" /* TASK_COMPLETED */,
2623
+ "operation_created" /* OPERATION_CREATED */
2624
+ ],
2625
+ ...config
2626
+ };
2627
+ if (this.config.cardMode !== false) {
2628
+ const isTestEnv = "BUN_TEST_WORKER_ID" in process.env;
2629
+ this.cardNotifier = config.cardNotifier ?? (isTestEnv ? new LarkCardPatchNotifier : getSharedLarkCardNotifier());
2630
+ this.cardStateStore = config.cardStateStore ?? createLarkCardStateStore();
2631
+ }
2632
+ }
2633
+ setCardRoutingPorts(ports) {
2634
+ this.cardRoutingPorts = ports;
2635
+ logger7.debug(`CardRoutingPorts injected: getTask=${typeof ports.getTask}`);
2636
+ }
2637
+ hasCardRoutingPorts() {
2638
+ return this.cardRoutingPorts !== undefined;
2639
+ }
2640
+ async execute(ctx) {
2641
+ if (!this.config.enabled) {
2642
+ return;
2643
+ }
2644
+ const env = getEnvContextOrEmpty();
2645
+ if (this.pluginName && this.sourceId) {
2646
+ const key = `${this.sourceId}:${this.pluginName}`;
2647
+ if (env.pluginEnabled && env.pluginEnabled[key] !== true) {
2648
+ logger7.debug(`Plugin not enabled for source: ${key}`);
2649
+ return;
2650
+ }
2651
+ }
2652
+ switch (ctx.hookPoint) {
2653
+ case TaskHookPoints.AFTER_CREATE:
2654
+ await this.onAfterCreate(ctx);
2655
+ break;
2656
+ case TaskHookPoints.AFTER_UPDATE:
2657
+ await this.onAfterUpdate(ctx);
2658
+ break;
2659
+ case TaskHookPoints.AFTER_COMPLETE:
2660
+ await this.onAfterComplete(ctx);
2661
+ break;
2662
+ case TaskHookPoints.OPERATION_AFTER_CREATE:
2663
+ await this.onAfterOperationCreate(ctx);
2664
+ break;
2665
+ case TaskHookPoints.AFTER_DELETE:
2666
+ await this.onAfterDelete(ctx);
2667
+ break;
2668
+ }
2669
+ }
2670
+ async onAfterCreate(ctx) {
2671
+ const { task, sessionId: ctxSessionId } = ctx.data;
2672
+ if (!task || !this.shouldNotify("task_created" /* TASK_CREATED */))
2673
+ return;
2674
+ if (!this.isCardModeEnabled())
2675
+ return;
2676
+ const sessionId = typeof ctxSessionId === "string" && ctxSessionId.trim() ? ctxSessionId.trim() : this.resolveSessionIdForTask(task.id);
2677
+ if (sessionId) {
2678
+ const merged = await this.patchSessionCardForTask(task, {
2679
+ sessionId,
2680
+ lifecycle: "running",
2681
+ milestones: [
2682
+ {
2683
+ title: "Task 创建",
2684
+ timestamp: new Date().toISOString(),
2685
+ description: task.current_status || undefined
2686
+ }
2687
+ ]
2688
+ });
2689
+ if (merged) {
2690
+ logger7.debug(`onAfterCreate: task #${task.id} 已渲染进 session 卡 ${sessionId}(单卡语义)`);
2691
+ return;
2692
+ }
2693
+ }
2694
+ await this.sendCardStart(task);
2695
+ }
2696
+ async onAfterUpdate(ctx) {
2697
+ const data = ctx.data;
2698
+ if (!data?.task)
2699
+ return;
2700
+ const { task, changes } = data;
2701
+ const terminalCardStatus = this.cardStatusOf(task, changes);
2702
+ if (terminalCardStatus) {
2703
+ const notifyType2 = terminalCardStatus === "completed" || terminalCardStatus === "cancelled" ? "task_completed" /* TASK_COMPLETED */ : "task_updated" /* TASK_UPDATED */;
2704
+ if (!this.shouldNotify(notifyType2))
2705
+ return;
2706
+ if (!this.isCardModeEnabled())
2707
+ return;
2708
+ const sessionIdTerminal = this.resolveSessionIdForTask(task.id);
2709
+ if (sessionIdTerminal) {
2710
+ const mergedTerminal = await this.patchSessionCardForTask(task, {
2711
+ sessionId: sessionIdTerminal,
2712
+ lifecycle: lifecycleOfTaskStatus(String(task.status)) ?? "running",
2713
+ milestones: [
2714
+ {
2715
+ title: terminalCardStatus === "completed" ? "任务已完成" : "任务状态更新",
2716
+ timestamp: new Date().toISOString(),
2717
+ description: task.current_status || undefined
2718
+ }
2719
+ ]
2720
+ });
2721
+ if (mergedTerminal)
2722
+ return;
2723
+ }
2724
+ await this.sendCardFinish(task);
2725
+ return;
2726
+ }
2727
+ let notifyType;
2728
+ if (changes?.status) {
2729
+ notifyType = "task_updated" /* TASK_UPDATED */;
2730
+ } else if (changes && Object.keys(changes).length > 0) {
2731
+ notifyType = "task_updated" /* TASK_UPDATED */;
2732
+ } else {
2733
+ return;
2734
+ }
2735
+ if (!this.shouldNotify(notifyType))
2736
+ return;
2737
+ if (!this.isCardModeEnabled())
2738
+ return;
2739
+ const sessionIdUpdate = this.resolveSessionIdForTask(task.id);
2740
+ if (sessionIdUpdate) {
2741
+ const mergedUpdate = await this.patchSessionCardForTask(task, {
2742
+ sessionId: sessionIdUpdate,
2743
+ lifecycle: lifecycleOfTaskStatus(String(task.status)) ?? "running"
2744
+ });
2745
+ if (mergedUpdate)
2746
+ return;
2747
+ }
2748
+ await this.sendCardUpdate(task, changes);
2749
+ }
2750
+ cardStatusOf(task, changes) {
2751
+ const changedStatus = changes?.status;
2752
+ const effective = String(changedStatus ?? task.status ?? "");
2753
+ switch (effective) {
2754
+ case "completed":
2755
+ return "completed";
2756
+ case "cancelled":
2757
+ return "cancelled";
2758
+ case "paused":
2759
+ return "stopped";
2760
+ case "archived":
2761
+ return "stopped";
2762
+ case "failed":
2763
+ case "timeout":
2764
+ return "failed";
2765
+ default:
2766
+ break;
2767
+ }
2768
+ return;
2769
+ }
2770
+ async onAfterComplete(ctx) {
2771
+ const data = ctx.data;
2772
+ const task = data?.task ?? data;
2773
+ if (!task || !this.shouldNotify("task_completed" /* TASK_COMPLETED */))
2774
+ return;
2775
+ if (!this.isCardModeEnabled())
2776
+ return;
2777
+ const terminalStatus = data.terminalStatus;
2778
+ if (terminalStatus === "to_validate" || task.status === "to_validate") {
2779
+ return;
2780
+ }
2781
+ await this.sendCardFinish(task);
2782
+ }
2783
+ async onAfterDelete(ctx) {
2784
+ const data = ctx.data;
2785
+ const taskId = data?.id;
2786
+ if (data?.deleted !== true || typeof taskId !== "number")
2787
+ return;
2788
+ if (!this.shouldNotify("task_completed" /* TASK_COMPLETED */))
2789
+ return;
2790
+ if (!this.isCardModeEnabled())
2791
+ return;
2792
+ if (!this.cardNotifier)
2793
+ return;
2794
+ let record;
2795
+ try {
2796
+ record = this.cardStateStore?.get(taskId);
2797
+ } catch (err) {
2798
+ logger7.warn(`onAfterDelete: 读取卡片记录失败 task #${taskId}: ${err.message}`);
2799
+ }
2800
+ const messageId = record?.messageId || this.cardMessageIds.get(taskId);
2801
+ if (!messageId) {
2802
+ logger7.debug(`onAfterDelete: task #${taskId} 无卡片记录,skip(不发新卡)`);
2803
+ return;
2804
+ }
2805
+ this.restoreCardState(taskId);
2806
+ const profile = record?.profile;
2807
+ const chatId = record?.chatId;
2808
+ try {
2809
+ await this.cardNotifier.finishTask({
2810
+ messageId,
2811
+ profile,
2812
+ chatId,
2813
+ status: "deleted",
2814
+ finalStep: "任务已取消(删除)"
2815
+ });
2816
+ } catch (err) {
2817
+ logger7.warn(`onAfterDelete: patch 卡片失败 task #${taskId} message=${messageId}: ${err.message}`);
2818
+ }
2819
+ try {
2820
+ await this.cardNotifier.unpinMessage(messageId, profile);
2821
+ } catch (err) {
2822
+ logger7.warn(`onAfterDelete: unpin 失败 task #${taskId} message=${messageId}: ${err.message}`);
2823
+ }
2824
+ this.cancelTrailingPatch(taskId);
2825
+ this.persistClosed(taskId);
2826
+ this.persistPinned(taskId, false);
2827
+ this.cardMessageIds.delete(taskId);
2828
+ logger7.debug(`onAfterDelete: task #${taskId} 卡片已收尾 status=deleted message=${messageId}`);
2829
+ }
2830
+ async onAfterOperationCreate(ctx) {
2831
+ const { operation } = ctx.data;
2832
+ if (!operation)
2833
+ return;
2834
+ if (!this.shouldNotify("operation_created" /* OPERATION_CREATED */))
2835
+ return;
2836
+ if (!this.isCardModeEnabled())
2837
+ return;
2838
+ const persisted = this.restoreCardState(operation.taskId);
2839
+ const messageId = this.cardMessageIds.get(operation.taskId);
2840
+ if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL || messageId === LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL) {
2841
+ logger7.debug(`onAfterOperationCreate: no usable card for task #${operation.taskId}, skip`);
2842
+ return;
2843
+ }
2844
+ if (persisted && persisted.status !== "open") {
2845
+ logger7.debug(`onAfterOperationCreate: task #${operation.taskId} card status=${persisted.status}, skip`);
2846
+ return;
2847
+ }
2848
+ const { milestones, processSteps } = this.deriveTimelineFromOperation(operation);
2849
+ if (milestones.length === 0 && processSteps.length === 0)
2850
+ return;
2851
+ const env = getEnvContextOrEmpty();
2852
+ const profile = env.replyChannel?.profile;
2853
+ this.cardNotifier.mergeTimelineState?.(messageId, milestones, processSteps);
2854
+ this.refreshTimeline(operation.taskId);
2855
+ const throttled = this.shouldThrottleOperation(operation);
2856
+ if (throttled) {
2857
+ this.scheduleTrailingPatch(operation.taskId, messageId, profile);
2858
+ logger7.debug(`[throttle] OPERATION_CREATED for task #${operation.taskId} ` + `merged into state + ledger; card patch deferred to window end`);
2859
+ return;
2860
+ }
2861
+ await this.patchOperationTimeline(operation.taskId, messageId, profile, milestones, processSteps);
2862
+ }
2863
+ async patchOperationTimeline(taskId, messageId, profile, milestones, processSteps) {
2864
+ try {
2865
+ await this.cardNotifier.updateTask({
2866
+ messageId,
2867
+ profile,
2868
+ status: "running",
2869
+ milestones,
2870
+ processSteps
2871
+ });
2872
+ } catch (err) {
2873
+ logger7.error(`sendCardOperation failed for task #${taskId}: ${err.message}`);
2874
+ }
2875
+ }
2876
+ scheduleTrailingPatch(taskId, messageId, profile) {
2877
+ const existing = this.trailingPatchTimers.get(taskId);
2878
+ if (existing) {
2879
+ return;
2880
+ }
2881
+ const throttleMs = this.config.operationThrottleMs ?? 5000;
2882
+ const delay = throttleMs > 0 ? throttleMs : 0;
2883
+ const timer = setTimeout(() => {
2884
+ this.trailingPatchTimers.delete(taskId);
2885
+ const liveMessageId = this.cardMessageIds.get(taskId);
2886
+ if (liveMessageId !== messageId) {
2887
+ logger7.debug(`[throttle] trailing patch skipped for task #${taskId}: ` + `card no longer tracked (finished/closed)`);
2888
+ return;
2889
+ }
2890
+ const rec = this.cardStateStore?.get(taskId);
2891
+ if (rec && rec.status !== "open") {
2892
+ logger7.debug(`[throttle] trailing patch skipped for task #${taskId}: ` + `card status=${rec.status}`);
2893
+ return;
2894
+ }
2895
+ this.patchOperationTimeline(taskId, messageId, profile, [], []);
2896
+ }, delay);
2897
+ if (typeof timer.unref === "function")
2898
+ timer.unref();
2899
+ this.trailingPatchTimers.set(taskId, timer);
2900
+ }
2901
+ cancelTrailingPatch(taskId) {
2902
+ const timer = this.trailingPatchTimers.get(taskId);
2903
+ if (!timer)
2904
+ return;
2905
+ clearTimeout(timer);
2906
+ this.trailingPatchTimers.delete(taskId);
2907
+ }
2908
+ restoreCardState(taskId) {
2909
+ if (!this.cardStateStore)
2910
+ return;
2911
+ try {
2912
+ const rec = this.cardStateStore.get(taskId);
2913
+ if (!rec)
2914
+ return;
2915
+ if (rec.sessionId) {
2916
+ this.cardNotifier?.bindTaskToSession?.(taskId, rec.sessionId);
2917
+ if (rec.messageId) {
2918
+ this.cardNotifier?.adoptSessionCard?.(rec.sessionId, rec.messageId);
2919
+ }
2920
+ }
2921
+ if (!this.restoredTaskIds.has(taskId)) {
2922
+ this.restoredTaskIds.add(taskId);
2923
+ if (rec.status !== "closed" && rec.messageId) {
2924
+ this.cardMessageIds.set(taskId, rec.messageId);
2925
+ this.hydrateNotifierState(rec);
2926
+ logger7.debug(`restoreCardState: task #${taskId} ← 持久层 message ${rec.messageId} ` + `(status=${rec.status}, pinned=${rec.pinned}, timeline=${rec.timeline ? "yes" : "no"})`);
2927
+ }
2928
+ }
2929
+ return rec;
2930
+ } catch (err) {
2931
+ logger7.warn(`restoreCardState failed for task #${taskId}: ${err.message}`);
2932
+ return;
2933
+ }
2934
+ }
2935
+ hydrateNotifierState(rec) {
2936
+ if (!this.cardNotifier || !rec.timeline)
2937
+ return;
2938
+ try {
2939
+ const snapshot = JSON.parse(rec.timeline);
2940
+ this.cardNotifier.hydrateState?.(rec.messageId, snapshot);
2941
+ } catch (err) {
2942
+ logger7.warn(`hydrateNotifierState failed for task #${rec.taskId}: ${err.message}`);
2943
+ }
2944
+ }
2945
+ snapshotTimeline(state) {
2946
+ if (!this.cardNotifier || !state.messageId)
2947
+ return state;
2948
+ try {
2949
+ const snap = this.cardNotifier.exportState?.(state.messageId);
2950
+ if (!snap)
2951
+ return state;
2952
+ return { ...state, timeline: JSON.stringify(snap) };
2953
+ } catch (err) {
2954
+ logger7.warn(`snapshotTimeline failed for task #${state.taskId}: ${err.message}`);
2955
+ return state;
2956
+ }
2957
+ }
2958
+ persistCardState(state, includeTimeline = false) {
2959
+ if (!this.cardStateStore)
2960
+ return;
2961
+ try {
2962
+ const payload = includeTimeline ? this.snapshotTimeline(state) : state;
2963
+ this.cardStateStore.upsert(payload);
2964
+ } catch (err) {
2965
+ logger7.warn(`persistCardState failed for task #${state.taskId}: ${err.message}`);
2966
+ }
2967
+ }
2968
+ persistPinned(taskId, pinned) {
2969
+ if (!this.cardStateStore)
2970
+ return;
2971
+ try {
2972
+ this.cardStateStore.markPinned(taskId, pinned);
2973
+ } catch (err) {
2974
+ logger7.warn(`persistPinned failed for task #${taskId}: ${err.message}`);
2975
+ }
2976
+ }
2977
+ persistPaused(taskId) {
2978
+ if (!this.cardStateStore)
2979
+ return;
2980
+ try {
2981
+ this.cardStateStore.markPaused(taskId);
2982
+ } catch (err) {
2983
+ logger7.warn(`persistPaused failed for task #${taskId}: ${err.message}`);
2984
+ }
2985
+ }
2986
+ dropStaleMessageId(taskId) {
2987
+ if (!this.cardStateStore)
2988
+ return;
2989
+ try {
2990
+ const existing = this.cardStateStore.get(taskId);
2991
+ if (!existing)
2992
+ return;
2993
+ this.cardStateStore.upsert({
2994
+ ...existing,
2995
+ messageId: "",
2996
+ pinned: false,
2997
+ updatedAt: Date.now()
2998
+ });
2999
+ } catch (err) {
3000
+ logger7.warn(`dropStaleMessageId failed for task #${taskId}: ${err.message}`);
3001
+ }
3002
+ }
3003
+ persistReopened(taskId) {
3004
+ if (!this.cardStateStore)
3005
+ return;
3006
+ try {
3007
+ this.cardStateStore.markReopened(taskId);
3008
+ } catch (err) {
3009
+ logger7.warn(`persistReopened failed for task #${taskId}: ${err.message}`);
3010
+ }
3011
+ }
3012
+ persistClosed(taskId) {
3013
+ if (!this.cardStateStore)
3014
+ return;
3015
+ try {
3016
+ this.cardStateStore.markClosed(taskId);
3017
+ } catch (err) {
3018
+ logger7.warn(`persistClosed failed for task #${taskId}: ${err.message}`);
3019
+ }
3020
+ }
3021
+ refreshTimeline(taskId) {
3022
+ if (!this.cardStateStore || !this.cardNotifier)
3023
+ return;
3024
+ try {
3025
+ const rec = this.cardStateStore.get(taskId);
3026
+ if (!rec || !rec.messageId)
3027
+ return;
3028
+ this.cardStateStore.upsert(this.snapshotTimeline(rec));
3029
+ } catch (err) {
3030
+ logger7.warn(`refreshTimeline failed for task #${taskId}: ${err.message}`);
3031
+ }
3032
+ }
3033
+ async repinAfterResume(task, persisted) {
3034
+ if (!this.cardNotifier || typeof this.cardNotifier.pinMessage !== "function") {
3035
+ return;
3036
+ }
3037
+ if (!persisted.messageId)
3038
+ return;
3039
+ if (persisted.pinned)
3040
+ return;
3041
+ const ctx = getEnvContextOrEmpty();
3042
+ const profile = this.resolveProfile(task, ctx);
3043
+ try {
3044
+ await this.cardNotifier.pinMessage(persisted.messageId, profile);
3045
+ this.persistPinned(task.id, true);
3046
+ logger7.debug(`repinAfterResume: task #${task.id} 卡片 ${persisted.messageId} 已重新置顶`);
3047
+ } catch (err) {
3048
+ logger7.warn(`repinAfterResume failed (non-fatal) for task #${task.id}: ${err.message}`);
3049
+ }
3050
+ }
3051
+ async reopenReuseCard(task, persisted, changes) {
3052
+ if (!this.cardNotifier)
3053
+ return false;
3054
+ const messageId = persisted.messageId;
3055
+ if (!messageId) {
3056
+ logger7.debug(`reopenReuseCard: task #${task.id} 记录无 messageId → 走建卡路径`);
3057
+ return false;
3058
+ }
3059
+ const ctx = getEnvContextOrEmpty();
3060
+ const profile = this.resolveProfile(task, ctx);
3061
+ this.cardNotifier.unfinaliseCard?.(messageId);
3062
+ const reopenMilestone = {
3063
+ title: "\uD83D\uDD04 任务重新打开",
3064
+ timestamp: new Date().toISOString(),
3065
+ description: task.current_status || undefined
3066
+ };
3067
+ const changeKeys = Object.keys(changes ?? {});
3068
+ const reopenSteps = changeKeys.length > 0 ? [
3069
+ {
3070
+ title: "任务重新打开",
3071
+ timestamp: new Date().toISOString(),
3072
+ description: changeKeys.map((k) => `${k}=${humanizeValue(changes[k], 80)}`).join(" · ")
3073
+ }
3074
+ ] : [];
3075
+ const hasState = this.cardNotifier.exportState?.(messageId) !== undefined;
3076
+ if (!hasState) {
3077
+ const persistedRec = this.cardStateStore?.get(task.id);
3078
+ if (persistedRec?.timeline) {
3079
+ this.hydrateNotifierState(persistedRec);
3080
+ logger7.debug(`reopenReuseCard: task #${task.id} 内存态缺失 → ` + `从持久层快照恢复卡片内容(历史时间线保留)`);
3081
+ }
3082
+ }
3083
+ const merged = this.cardNotifier.mergeTimelineState?.(messageId, [reopenMilestone], reopenSteps) ?? false;
3084
+ try {
3085
+ await this.cardNotifier.updateTask({
3086
+ messageId,
3087
+ profile,
3088
+ status: "running",
3089
+ meta: this.buildTaskMeta(task),
3090
+ progress: typeof task.progress === "number" && task.progress > 0 ? task.progress : undefined,
3091
+ milestones: merged ? [] : [reopenMilestone],
3092
+ processSteps: merged ? [] : reopenSteps
3093
+ });
3094
+ } catch (err) {
3095
+ logger7.warn(`reopenReuseCard: task #${task.id} 原卡 patch 失败: ` + `${err.message}`);
3096
+ return false;
3097
+ }
3098
+ this.persistReopened(task.id);
3099
+ this.refreshTimeline(task.id);
3100
+ try {
3101
+ await this.repinAfterResume(task, {
3102
+ ...persisted,
3103
+ status: "open"
3104
+ });
3105
+ } catch (pinErr) {
3106
+ logger7.warn(`reopenReuseCard: task #${task.id} 重新 pin 失败(非致命,卡片内容已更新): ` + `${pinErr.message}`);
3107
+ }
3108
+ logger7.info(`reopenReuseCard: task #${task.id} 已复用原卡 ${messageId} ` + `(patch + reopen 里程碑 + 重新 pin)`);
3109
+ return true;
3110
+ }
3111
+ buildCardState(task, messageId, chatId, profile, pinned) {
3112
+ let existing;
3113
+ try {
3114
+ existing = this.cardStateStore?.get(task.id);
3115
+ } catch (err) {
3116
+ logger7.warn(`buildCardState: read existing failed for task #${task.id}: ${err.message}`);
3117
+ }
3118
+ const now = Date.now();
3119
+ const sessionId = this.resolveSessionIdForTask(task.id);
3120
+ return {
3121
+ taskId: task.id,
3122
+ messageId,
3123
+ chatId,
3124
+ profile,
3125
+ pinned,
3126
+ status: "open",
3127
+ ...sessionId ? { sessionId } : {},
3128
+ createdAt: existing?.createdAt ?? now,
3129
+ updatedAt: now
3130
+ };
3131
+ }
3132
+ getCardState(taskId) {
3133
+ if (!this.cardStateStore)
3134
+ return;
3135
+ try {
3136
+ return this.cardStateStore.get(taskId);
3137
+ } catch {
3138
+ return;
3139
+ }
3140
+ }
3141
+ listCardStates() {
3142
+ if (!this.cardStateStore)
3143
+ return [];
3144
+ try {
3145
+ return this.cardStateStore.list();
3146
+ } catch {
3147
+ return [];
3148
+ }
3149
+ }
3150
+ isCardModeEnabled() {
3151
+ if (this.config.cardMode === false || !this.cardNotifier) {
3152
+ logger7.debug("cardMode=false (legacy opt-out), skip task notification (no text fallback)");
3153
+ return false;
3154
+ }
3155
+ return true;
3156
+ }
3157
+ async resolveCardTarget(task) {
3158
+ const env = getEnvContextOrEmpty();
3159
+ const triggerChatId = env.replyChannel?.chatId;
3160
+ const triggerProfile = env.replyChannel?.profile;
3161
+ const lineage = await this.collectLineage(task);
3162
+ const lineageIds = lineage.map((t) => t.id);
3163
+ const selfChatId = extractPerTaskChatId(task.context);
3164
+ let lineageChat;
3165
+ for (const node of lineage) {
3166
+ const chatId = extractPerTaskChatId(node.context);
3167
+ if (chatId) {
3168
+ lineageChat = {
3169
+ chatId,
3170
+ profile: extractPerTaskChatProfile(node.context),
3171
+ isSelf: node.id === task.id
3172
+ };
3173
+ break;
3174
+ }
3175
+ }
3176
+ if (!lineageChat && selfChatId) {
3177
+ lineageChat = {
3178
+ chatId: selfChatId,
3179
+ profile: extractPerTaskChatProfile(task.context),
3180
+ isSelf: true
3181
+ };
3182
+ }
3183
+ return decideCardChatTarget({
3184
+ taskId: task.id,
3185
+ lineageIds,
3186
+ lineageChat,
3187
+ triggerChatId,
3188
+ triggerProfile
3189
+ });
3190
+ }
3191
+ async collectLineage(task) {
3192
+ const out = [
3193
+ { id: task.id, context: task.context }
3194
+ ];
3195
+ let parentId = typeof task.parent_task_id === "number" && task.parent_task_id > 0 ? task.parent_task_id : undefined;
3196
+ let depth = 0;
3197
+ while (parentId && depth < 10 && this.cardRoutingPorts?.getTask) {
3198
+ try {
3199
+ const parent = await this.cardRoutingPorts.getTask(parentId);
3200
+ if (!parent)
3201
+ break;
3202
+ out.push({ id: parentId, context: parent.context });
3203
+ parentId = typeof parent.parent_task_id === "number" && parent.parent_task_id > 0 ? parent.parent_task_id : undefined;
3204
+ } catch {
3205
+ break;
3206
+ }
3207
+ depth++;
3208
+ }
3209
+ return out;
3210
+ }
3211
+ async sendCardStart(task) {
3212
+ if (!this.cardNotifier)
3213
+ return;
3214
+ const persisted = this.restoreCardState(task.id);
3215
+ if (persisted?.status === "closed") {
3216
+ this.cardMessageIds.delete(task.id);
3217
+ } else if (persisted?.status === "paused") {
3218
+ this.persistReopened(task.id);
3219
+ }
3220
+ const claim = this.cardMessageIds.get(task.id);
3221
+ if (claim) {
3222
+ logger7.debug(`sendCardStart: task #${task.id} 已有首卡(${claim})→ 跳过重复发卡`);
3223
+ return;
3224
+ }
3225
+ this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL);
3226
+ let target;
3227
+ try {
3228
+ target = await this.resolveCardTarget(task);
3229
+ } catch (err) {
3230
+ this.cardMessageIds.delete(task.id);
3231
+ throw err;
3232
+ }
3233
+ const chatId = target.chatId;
3234
+ const profile = target.profile;
3235
+ if (!chatId) {
3236
+ this.cardMessageIds.delete(task.id);
3237
+ logger7.debug(`sendCardStart: no chatId for task #${task.id}, skip`);
3238
+ return;
3239
+ }
3240
+ try {
3241
+ const { messageId, pinned } = await this.cardNotifier.startTask({
3242
+ chatId,
3243
+ profile,
3244
+ meta: this.buildTaskMeta(task),
3245
+ milestones: [
3246
+ {
3247
+ title: "Task 创建",
3248
+ timestamp: new Date().toISOString(),
3249
+ description: task.current_status || undefined
3250
+ }
3251
+ ]
3252
+ });
3253
+ this.cardMessageIds.set(task.id, messageId);
3254
+ this.persistCardState(this.buildCardState(task, messageId, chatId, profile, pinned), true);
3255
+ logger7.debug(`sendCardStart: task #${task.id} → message ${messageId} (pinned=${pinned}) ` + `chat=${chatId} source=${target.source} reason=${target.reason}`);
3256
+ } catch (err) {
3257
+ this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL);
3258
+ logger7.error(`sendCardStart failed for task #${task.id}: ${err.message}`);
3259
+ }
3260
+ }
3261
+ async sendCardUpdate(task, changes) {
3262
+ if (!this.cardNotifier)
3263
+ return;
3264
+ const persisted = this.restoreCardState(task.id);
3265
+ if (persisted?.status === "closed") {
3266
+ const stillTerminal = isTerminalTaskStatus(String(task.status));
3267
+ if (stillTerminal) {
3268
+ logger7.debug(`sendCardUpdate: task #${task.id} 卡片已终结(closed)且任务仍终态 → skip patch`);
3269
+ return;
3270
+ }
3271
+ const reused = await this.reopenReuseCard(task, persisted, changes);
3272
+ if (reused) {
3273
+ this.cardMessageIds.set(task.id, persisted.messageId);
3274
+ return;
3275
+ }
3276
+ logger7.info(`sendCardUpdate: task #${task.id} 已 reopen 但原卡 patch 失败 → 降级发新卡`);
3277
+ this.cardMessageIds.delete(task.id);
3278
+ this.dropStaleMessageId(task.id);
3279
+ }
3280
+ if (persisted?.status === "paused" && String(task.status) !== "paused") {
3281
+ this.persistReopened(task.id);
3282
+ logger7.debug(`sendCardUpdate: task #${task.id} resume(paused → open),复用卡片 ${persisted.messageId}`);
3283
+ await this.repinAfterResume(task, persisted);
3284
+ }
3285
+ let messageId = this.cardMessageIds.get(task.id);
3286
+ if (!messageId && isTerminalTaskStatus(String(task.status))) {
3287
+ logger7.debug(`sendCardUpdate: task #${task.id} 状态=${task.status}(终结/归档)且无卡片 → skip`);
3288
+ return;
3289
+ }
3290
+ if (!messageId) {
3291
+ this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL);
3292
+ }
3293
+ const target = messageId ? undefined : await this.resolveCardTarget(task);
3294
+ const chatId = target?.chatId ?? this.resolveChatId(task, getEnvContextOrEmpty());
3295
+ const profile = target?.profile ?? this.resolveProfile(task, getEnvContextOrEmpty());
3296
+ if (!chatId) {
3297
+ if (!messageId)
3298
+ this.cardMessageIds.delete(task.id);
3299
+ logger7.debug(`sendCardUpdate: no chatId for task #${task.id}, skip`);
3300
+ return;
3301
+ }
3302
+ if (messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
3303
+ logger7.debug(`sendCardUpdate: startTask failed previously for task #${task.id}, skip`);
3304
+ return;
3305
+ }
3306
+ if (messageId === LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL) {
3307
+ logger7.debug(`sendCardUpdate: task #${task.id} 首卡正在发送中(in-flight)→ 跳过本次 update`);
3308
+ return;
3309
+ }
3310
+ if (!messageId) {
3311
+ try {
3312
+ const res = await this.cardNotifier.startTask({
3313
+ chatId,
3314
+ profile,
3315
+ meta: this.buildTaskMeta(task)
3316
+ });
3317
+ messageId = res.messageId;
3318
+ this.cardMessageIds.set(task.id, messageId);
3319
+ this.persistCardState(this.buildCardState(task, messageId, chatId, profile, res.pinned), true);
3320
+ } catch (err) {
3321
+ this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL);
3322
+ logger7.error(`sendCardUpdate fallback start failed: ${err.message}`);
3323
+ return;
3324
+ }
3325
+ }
3326
+ const changeKeys = Object.keys(changes ?? {});
3327
+ const processSteps = changeKeys.length > 0 ? [
3328
+ {
3329
+ title: "状态更新",
3330
+ timestamp: new Date().toISOString(),
3331
+ description: changeKeys.map((k) => `${k}=${humanizeValue(changes[k], 80)}`).join(" · ")
3332
+ }
3333
+ ] : [];
3334
+ try {
3335
+ await this.cardNotifier.updateTask({
3336
+ messageId,
3337
+ chatId,
2209
3338
  profile,
2210
- status: "running",
2211
- milestones,
3339
+ status: String(task.status) === "to_validate" ? "to_validate" : "running",
3340
+ meta: this.buildTaskMeta(task),
3341
+ progress: typeof task.progress === "number" && task.progress > 0 ? task.progress : undefined,
3342
+ milestones: [],
2212
3343
  processSteps
2213
3344
  });
3345
+ const current = this.getCardState(task.id);
3346
+ this.persistPinned(task.id, current?.pinned ?? persisted?.pinned ?? true);
3347
+ this.refreshTimeline(task.id);
2214
3348
  } catch (err) {
2215
- logger7.error(`sendCardOperation failed for task #${taskId}: ${err.message}`);
3349
+ logger7.error(`sendCardUpdate failed: ${err.message}`);
3350
+ }
3351
+ }
3352
+ async sendCardFinish(task) {
3353
+ if (!this.cardNotifier)
3354
+ return;
3355
+ this.restoreCardState(task.id);
3356
+ const messageId = this.cardMessageIds.get(task.id);
3357
+ const ctx = getEnvContextOrEmpty();
3358
+ const profile = this.resolveProfile(task, ctx);
3359
+ if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
3360
+ logger7.debug(`sendCardFinish: no card messageId for task #${task.id}, skip ` + `(记录缺失 → 不发新卡,保持幂等)`);
3361
+ return;
3362
+ }
3363
+ const rawStatus = String(task.status);
3364
+ const status = mapTaskStatusToCardStatus(rawStatus);
3365
+ const isPaused = rawStatus === "paused" || rawStatus === "archived";
3366
+ try {
3367
+ this.cancelTrailingPatch(task.id);
3368
+ const finish = isPaused ? this.cardNotifier.pauseTask.bind(this.cardNotifier) : this.cardNotifier.finishTask.bind(this.cardNotifier);
3369
+ await finish({
3370
+ messageId,
3371
+ profile,
3372
+ status,
3373
+ meta: this.buildTaskMeta(task),
3374
+ finalStep: task.current_status || undefined,
3375
+ expandTimeline: rawStatus === "archived" ? true : undefined,
3376
+ ...rawStatus === "archived" ? { pausedMilestoneTitle: "Task 已归档" } : {}
3377
+ });
3378
+ if (isPaused) {
3379
+ this.persistPaused(task.id);
3380
+ this.persistPinned(task.id, false);
3381
+ this.refreshTimeline(task.id);
3382
+ } else {
3383
+ this.persistClosed(task.id);
3384
+ this.persistPinned(task.id, false);
3385
+ this.cardMessageIds.delete(task.id);
3386
+ }
3387
+ logger7.debug(`sendCardFinish: task #${task.id} → status=${status} ` + `(raw=${rawStatus}) message=${messageId}`);
3388
+ } catch (err) {
3389
+ logger7.error(`sendCardFinish failed: ${err.message}`);
3390
+ }
3391
+ }
3392
+ resolveChatId(task, env) {
3393
+ const envChat = env.replyChannel?.chatId;
3394
+ if (envChat)
3395
+ return envChat;
3396
+ const ctxStr = task.context;
3397
+ if (typeof ctxStr === "string") {
3398
+ const ctxChat = parseLarkPerTaskChatId(ctxStr);
3399
+ if (ctxChat)
3400
+ return ctxChat;
3401
+ }
3402
+ return;
3403
+ }
3404
+ resolveProfile(task, env) {
3405
+ const envProfile = env.replyChannel?.profile;
3406
+ if (envProfile)
3407
+ return envProfile;
3408
+ const ctxStr = task.context;
3409
+ if (typeof ctxStr === "string") {
3410
+ const ctxProfile = parseLarkPerTaskChatProfile(ctxStr);
3411
+ if (ctxProfile)
3412
+ return ctxProfile;
3413
+ }
3414
+ return;
3415
+ }
3416
+ buildTaskMeta(task) {
3417
+ const sessionId = this.resolveSessionIdForTask(task.id);
3418
+ const recordedNames = extractRecordedChatNames(task.context);
3419
+ const matchedName = recordedNames.find((n) => new RegExp(`(?:Task\\s*#?\\s*|#)(?<![\\d])(?<!\\d)${task.id}(?!\\d)(?![\\d])`, "i").test(n));
3420
+ return {
3421
+ id: task.id,
3422
+ title: task.title,
3423
+ description: task.description,
3424
+ goals: task.goals_and_expected_deliverables,
3425
+ parentTaskId: typeof task.parent_task_id === "number" && task.parent_task_id > 0 ? task.parent_task_id : undefined,
3426
+ priority: task.priority,
3427
+ type: task.type,
3428
+ projectPath: task.project_path,
3429
+ context: task.context,
3430
+ currentStatus: task.current_status,
3431
+ perTaskChatId: extractPerTaskChatId(task.context),
3432
+ perTaskChatName: matchedName,
3433
+ ...sessionId ? { sessionId } : {}
3434
+ };
3435
+ }
3436
+ resolveSessionIdForTask(taskId) {
3437
+ const fromIndex = this.cardNotifier?.getSessionIdByTask?.(taskId);
3438
+ if (fromIndex)
3439
+ return fromIndex;
3440
+ try {
3441
+ const rec = this.cardStateStore?.get(taskId);
3442
+ if (rec?.sessionId)
3443
+ return rec.sessionId;
3444
+ } catch {}
3445
+ const ctx = getEnvContextOrEmpty();
3446
+ const fromCtx = ctx?.sessionId ?? ctx?.replyChannel?.sessionId;
3447
+ return typeof fromCtx === "string" && fromCtx ? fromCtx : undefined;
3448
+ }
3449
+ async patchSessionCardForTask(task, opts) {
3450
+ if (!this.cardNotifier)
3451
+ return false;
3452
+ const meta = this.buildTaskMeta(task);
3453
+ const sessionId = opts.sessionId;
3454
+ const chatId = meta.perTaskChatId ?? this.resolveChatId(task, getEnvContextOrEmpty());
3455
+ if (!chatId)
3456
+ return false;
3457
+ const profile = this.resolveProfile(task, getEnvContextOrEmpty());
3458
+ const agentType = this.resolveAgentType();
3459
+ const res = await this.cardNotifier.upsertSessionCard({
3460
+ chatId,
3461
+ ...profile ? { profile } : {},
3462
+ sessionId,
3463
+ ...task.title ? { title: task.title } : {},
3464
+ ...opts.statusPhrase ? { statusPhrase: opts.statusPhrase } : {},
3465
+ ...agentType ? { agentType } : {},
3466
+ startedAt: task.createdAt ? String(task.createdAt) : new Date().toISOString(),
3467
+ ...opts.milestones ? { milestones: opts.milestones } : {},
3468
+ lifecycle: opts.lifecycle,
3469
+ task: this.buildSessionCardTask(task, meta),
3470
+ ...meta.perTaskChatId ? { perTaskChatId: meta.perTaskChatId } : {},
3471
+ ...meta.perTaskChatName ? { perTaskChatName: meta.perTaskChatName } : {}
3472
+ });
3473
+ this.recordSessionCardForTask(task, sessionId, chatId, profile, res.messageId);
3474
+ if (this.syncGroupCardFromOriginFn) {
3475
+ await this.cardNotifier.syncGroupCardFromOrigin(sessionId, {
3476
+ lifecycle: opts.lifecycle,
3477
+ ...task.title ? { title: task.title } : {},
3478
+ ...opts.milestones ? { milestones: opts.milestones } : {}
3479
+ }, this.syncGroupCardFromOriginFn);
3480
+ }
3481
+ return true;
3482
+ }
3483
+ syncGroupCardFromOriginFn;
3484
+ recordSessionCardForTask(task, sessionId, chatId, profile, messageIdIn) {
3485
+ this.cardNotifier?.bindTaskToSession?.(task.id, sessionId);
3486
+ if (!this.cardStateStore)
3487
+ return;
3488
+ const messageId = messageIdIn ?? this.cardNotifier?.getSessionCardMessageId?.(sessionId);
3489
+ if (!messageId)
3490
+ return;
3491
+ this.cardMessageIds.set(task.id, messageId);
3492
+ try {
3493
+ const existing = this.cardStateStore.get(task.id);
3494
+ const now = Date.now();
3495
+ this.cardStateStore.upsert({
3496
+ taskId: task.id,
3497
+ messageId,
3498
+ chatId: chatId ?? existing?.chatId ?? "",
3499
+ ...profile ?? existing?.profile ? { profile: profile ?? existing?.profile } : {},
3500
+ pinned: existing?.pinned ?? true,
3501
+ status: existing?.status ?? "open",
3502
+ createdAt: existing?.createdAt ?? now,
3503
+ updatedAt: now,
3504
+ sessionId,
3505
+ ...existing?.timeline ? { timeline: existing.timeline } : {}
3506
+ });
3507
+ } catch (err) {
3508
+ logger7.warn(`recordSessionCardForTask failed for task #${task.id}: ${err.message}`);
3509
+ }
3510
+ }
3511
+ resolveAgentType() {
3512
+ const ctx = getEnvContextOrEmpty();
3513
+ return typeof ctx?.subagentType === "string" && ctx.subagentType ? ctx.subagentType : undefined;
3514
+ }
3515
+ buildSessionCardTask(task, meta) {
3516
+ return {
3517
+ id: task.id,
3518
+ ...task.title ? { title: task.title } : {},
3519
+ ...task.status ? { status: String(task.status) } : {},
3520
+ ...typeof task.progress === "number" ? { progress: task.progress } : {},
3521
+ ...task.current_status ? { currentStatus: task.current_status } : {},
3522
+ ...meta.priority ? { priority: meta.priority } : {},
3523
+ ...typeof meta.parentTaskId === "number" ? { parentTaskId: meta.parentTaskId } : {},
3524
+ ...meta.goals ? { goals: meta.goals } : {},
3525
+ ...task.createdAt ? { createdAt: String(task.createdAt) } : {},
3526
+ ...task.updatedAt ? { updatedAt: String(task.updatedAt) } : {}
3527
+ };
3528
+ }
3529
+ deriveTimelineFromOperation(operation) {
3530
+ const raw = operation;
3531
+ const title = operation.milestoneTitle ?? operation.actionTitle ?? raw.action_title ?? raw.description ?? raw.type ?? undefined;
3532
+ const detail = operation.milestoneDescription ?? operation.actionDescription ?? raw.action_description ?? undefined;
3533
+ const timestamp = operation.timestamp ?? new Date().toISOString();
3534
+ const milestones = [];
3535
+ const processSteps = [];
3536
+ if (operation.milestoneTitle || operation.milestoneType || title) {
3537
+ milestones.push({
3538
+ title: title ?? String(operation.milestoneType ?? "里程碑"),
3539
+ timestamp,
3540
+ description: detail
3541
+ });
3542
+ }
3543
+ if (operation.processDescription || detail) {
3544
+ processSteps.push({
3545
+ title: title ?? "过程",
3546
+ timestamp,
3547
+ description: operation.processDescription ?? detail
3548
+ });
3549
+ }
3550
+ const currentStep = operation.processDescription ?? detail ?? title ?? undefined;
3551
+ return { milestones, processSteps, currentStep };
3552
+ }
3553
+ shouldThrottleOperation(operation) {
3554
+ const throttleMs = this.config.operationThrottleMs ?? 5000;
3555
+ if (throttleMs <= 0)
3556
+ return false;
3557
+ const key = `op:${operation.taskId}`;
3558
+ const now = Date.now();
3559
+ const lastTime = this.lastNotifyTimestamps.get(key);
3560
+ if (lastTime && now - lastTime < throttleMs) {
3561
+ logger7.debug(`[throttle] skip OPERATION_CREATED patch for task #${operation.taskId} (${now - lastTime}ms ago)`);
3562
+ return true;
2216
3563
  }
3564
+ this.lastNotifyTimestamps.set(key, now);
3565
+ return false;
3566
+ }
3567
+ shouldNotify(type) {
3568
+ const { notifyTypes } = this.config;
3569
+ if (notifyTypes && notifyTypes.length === 0)
3570
+ return true;
3571
+ if (notifyTypes)
3572
+ return notifyTypes.includes(type);
3573
+ return type === "task_completed" /* TASK_COMPLETED */;
3574
+ }
3575
+ }
3576
+ __legacyDecorateClassTS([
3577
+ TracedAs("lark-cli-task-notify.execute")
3578
+ ], LarkCliTaskNotifyHook.prototype, "execute", null);
3579
+ __legacyDecorateClassTS([
3580
+ TracedAs("lark-cli-task-notify.resolveCardTarget", {
3581
+ recordParams: true,
3582
+ recordResult: true,
3583
+ flattenParamFilter: true,
3584
+ paramFilter: (args) => ({
3585
+ taskId: args[0]?.id,
3586
+ triggerChatId: undefined
3587
+ }),
3588
+ resultFilter: (r) => ({
3589
+ source: r?.source,
3590
+ chatId: r?.chatId,
3591
+ chatName: r?.chatName,
3592
+ reason: r?.reason?.slice(0, 200)
3593
+ })
3594
+ })
3595
+ ], LarkCliTaskNotifyHook.prototype, "resolveCardTarget", null);
3596
+ function createLarkCliTaskNotifyHook(config = {}) {
3597
+ return new LarkCliTaskNotifyHook(config);
3598
+ }
3599
+ // src/env/task/plugins/wecom-task-notify-plugin.ts
3600
+ init_logger();
3601
+ init_env_context();
3602
+
3603
+ // src/env/task/plugins/wecom-card-state-store.ts
3604
+ init_logger();
3605
+ init_sqlite_task_store();
3606
+ import { join } from "path";
3607
+ var logger8 = createLogger("wecom:card-state");
3608
+
3609
+ class MemoryWecomCardStateStore {
3610
+ records = new Map;
3611
+ upsert(state) {
3612
+ this.records.set(state.taskId, { ...state });
2217
3613
  }
2218
- scheduleTrailingPatch(taskId, messageId, profile) {
2219
- const existing = this.trailingPatchTimers.get(taskId);
2220
- if (existing) {
2221
- return;
2222
- }
2223
- const throttleMs = this.config.operationThrottleMs ?? 5000;
2224
- const delay = throttleMs > 0 ? throttleMs : 0;
2225
- const timer = setTimeout(() => {
2226
- this.trailingPatchTimers.delete(taskId);
2227
- const liveMessageId = this.cardMessageIds.get(taskId);
2228
- if (liveMessageId !== messageId) {
2229
- logger7.debug(`[throttle] trailing patch skipped for task #${taskId}: ` + `card no longer tracked (finished/closed)`);
2230
- return;
2231
- }
2232
- const rec = this.cardStateStore?.get(taskId);
2233
- if (rec && rec.status !== "open") {
2234
- logger7.debug(`[throttle] trailing patch skipped for task #${taskId}: ` + `card status=${rec.status}`);
2235
- return;
2236
- }
2237
- this.patchOperationTimeline(taskId, messageId, profile, [], []);
2238
- }, delay);
2239
- if (typeof timer.unref === "function")
2240
- timer.unref();
2241
- this.trailingPatchTimers.set(taskId, timer);
3614
+ get(taskId) {
3615
+ const r = this.records.get(taskId);
3616
+ return r ? { ...r } : undefined;
2242
3617
  }
2243
- cancelTrailingPatch(taskId) {
2244
- const timer = this.trailingPatchTimers.get(taskId);
2245
- if (!timer)
2246
- return;
2247
- clearTimeout(timer);
2248
- this.trailingPatchTimers.delete(taskId);
3618
+ list() {
3619
+ return [...this.records.values()].map((r) => ({ ...r }));
2249
3620
  }
2250
- restoreCardState(taskId) {
2251
- if (!this.cardStateStore)
3621
+ markPaused(taskId) {
3622
+ const r = this.records.get(taskId);
3623
+ if (!r)
2252
3624
  return;
2253
- try {
2254
- const rec = this.cardStateStore.get(taskId);
2255
- if (!rec)
2256
- return;
2257
- if (!this.restoredTaskIds.has(taskId)) {
2258
- this.restoredTaskIds.add(taskId);
2259
- if (rec.status !== "closed" && rec.messageId) {
2260
- this.cardMessageIds.set(taskId, rec.messageId);
2261
- this.hydrateNotifierState(rec);
2262
- logger7.debug(`restoreCardState: task #${taskId} ← 持久层 message ${rec.messageId} ` + `(status=${rec.status}, pinned=${rec.pinned}, timeline=${rec.timeline ? "yes" : "no"})`);
2263
- }
2264
- }
2265
- return rec;
2266
- } catch (err) {
2267
- logger7.warn(`restoreCardState failed for task #${taskId}: ${err.message}`);
3625
+ r.status = "paused";
3626
+ r.updatedAt = Date.now();
3627
+ }
3628
+ markReopened(taskId) {
3629
+ const r = this.records.get(taskId);
3630
+ if (!r)
2268
3631
  return;
2269
- }
3632
+ r.status = "open";
3633
+ delete r.closedAt;
3634
+ r.updatedAt = Date.now();
2270
3635
  }
2271
- hydrateNotifierState(rec) {
2272
- if (!this.cardNotifier || !rec.timeline)
3636
+ markClosed(taskId) {
3637
+ const r = this.records.get(taskId);
3638
+ if (!r)
2273
3639
  return;
2274
- try {
2275
- const snapshot = JSON.parse(rec.timeline);
2276
- this.cardNotifier.hydrateState?.(rec.messageId, snapshot);
2277
- } catch (err) {
2278
- logger7.warn(`hydrateNotifierState failed for task #${rec.taskId}: ${err.message}`);
3640
+ const now = Date.now();
3641
+ r.status = "closed";
3642
+ r.closedAt = now;
3643
+ r.updatedAt = now;
3644
+ }
3645
+ delete(taskId) {
3646
+ this.records.delete(taskId);
3647
+ }
3648
+ close() {
3649
+ this.records.clear();
3650
+ }
3651
+ }
3652
+ function createMemoryWecomCardStore() {
3653
+ return new MemoryWecomCardStateStore;
3654
+ }
3655
+ function rowToState(row) {
3656
+ const state = {
3657
+ taskId: row.task_id,
3658
+ chatId: row.chat_id,
3659
+ pinned: row.pinned === 1,
3660
+ status: row.status ?? "open",
3661
+ createdAt: row.created_at,
3662
+ updatedAt: row.updated_at,
3663
+ updateCount: row.update_count ?? 0
3664
+ };
3665
+ if (row.closed_at !== null && row.closed_at !== undefined) {
3666
+ state.closedAt = row.closed_at;
3667
+ }
3668
+ if (row.timeline)
3669
+ state.timeline = row.timeline;
3670
+ return state;
3671
+ }
3672
+
3673
+ class SqliteWecomCardStateStore {
3674
+ db;
3675
+ constructor(dbPath) {
3676
+ if (dbPath !== ":memory:") {
3677
+ const { mkdirSync } = __require("fs");
3678
+ const dir = __require("path").dirname(dbPath);
3679
+ try {
3680
+ mkdirSync(dir, { recursive: true });
3681
+ } catch {}
2279
3682
  }
3683
+ this.db = createCompatibleDatabase(dbPath);
3684
+ try {
3685
+ this.db.run("PRAGMA journal_mode = WAL");
3686
+ this.db.run("PRAGMA busy_timeout = 5000");
3687
+ this.db.run("PRAGMA synchronous = NORMAL");
3688
+ } catch {}
3689
+ this.db.run(`
3690
+ CREATE TABLE IF NOT EXISTS wecom_card (
3691
+ task_id INTEGER PRIMARY KEY,
3692
+ chat_id TEXT NOT NULL,
3693
+ pinned INTEGER NOT NULL DEFAULT 0,
3694
+ status TEXT NOT NULL DEFAULT 'open',
3695
+ created_at INTEGER NOT NULL,
3696
+ updated_at INTEGER NOT NULL,
3697
+ closed_at INTEGER,
3698
+ update_count INTEGER NOT NULL DEFAULT 0,
3699
+ timeline TEXT
3700
+ )
3701
+ `);
3702
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_wecom_card_status ON wecom_card(status)");
2280
3703
  }
2281
- snapshotTimeline(state) {
2282
- if (!this.cardNotifier || !state.messageId)
2283
- return state;
3704
+ upsert(state) {
3705
+ this.db.prepare(`INSERT INTO wecom_card
3706
+ (task_id, chat_id, pinned, status, created_at, updated_at, closed_at, update_count, timeline)
3707
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3708
+ ON CONFLICT(task_id) DO UPDATE SET
3709
+ chat_id = excluded.chat_id,
3710
+ pinned = excluded.pinned,
3711
+ status = excluded.status,
3712
+ updated_at = excluded.updated_at,
3713
+ closed_at = excluded.closed_at,
3714
+ update_count = excluded.update_count,
3715
+ timeline = excluded.timeline`).run(state.taskId, state.chatId, state.pinned ? 1 : 0, state.status, state.createdAt, state.updatedAt, state.closedAt ?? null, state.updateCount, state.timeline ?? null);
3716
+ }
3717
+ get(taskId) {
3718
+ const row = this.db.prepare("SELECT * FROM wecom_card WHERE task_id = ?").get(taskId);
3719
+ return row ? rowToState(row) : undefined;
3720
+ }
3721
+ list() {
3722
+ const rows = this.db.prepare("SELECT * FROM wecom_card ORDER BY task_id ASC").all();
3723
+ return rows.map(rowToState);
3724
+ }
3725
+ markPaused(taskId) {
3726
+ this.db.prepare("UPDATE wecom_card SET status = 'paused', updated_at = ? WHERE task_id = ?").run(Date.now(), taskId);
3727
+ }
3728
+ markReopened(taskId) {
3729
+ this.db.prepare("UPDATE wecom_card SET status = 'open', closed_at = NULL, updated_at = ? WHERE task_id = ?").run(Date.now(), taskId);
3730
+ }
3731
+ markClosed(taskId) {
3732
+ const now = Date.now();
3733
+ this.db.prepare("UPDATE wecom_card SET status = 'closed', closed_at = ?, updated_at = ? WHERE task_id = ?").run(now, now, taskId);
3734
+ }
3735
+ delete(taskId) {
3736
+ this.db.prepare("DELETE FROM wecom_card WHERE task_id = ?").run(taskId);
3737
+ }
3738
+ close() {
2284
3739
  try {
2285
- const snap = this.cardNotifier.exportState?.(state.messageId);
2286
- if (!snap)
2287
- return state;
2288
- return { ...state, timeline: JSON.stringify(snap) };
2289
- } catch (err) {
2290
- logger7.warn(`snapshotTimeline failed for task #${state.taskId}: ${err.message}`);
2291
- return state;
3740
+ this.db.close?.();
3741
+ } catch {}
3742
+ }
3743
+ }
3744
+ function getDefaultWecomCardDbPath() {
3745
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
3746
+ return join(home, ".local", "share", "roy-agent", "wecom-cards.db");
3747
+ }
3748
+ function createWecomCardStateStore(dbPath) {
3749
+ const isTestEnv = "BUN_TEST_WORKER_ID" in process.env;
3750
+ if (!dbPath && !process.env.ROY_AGENT_WECOM_CARD_DB && isTestEnv) {
3751
+ return new MemoryWecomCardStateStore;
3752
+ }
3753
+ const path = dbPath ?? process.env.ROY_AGENT_WECOM_CARD_DB ?? getDefaultWecomCardDbPath();
3754
+ try {
3755
+ return new SqliteWecomCardStateStore(path);
3756
+ } catch (err) {
3757
+ logger8.warn(`WecomCardStateStore: sqlite unavailable at ${path} (${err.message}) ` + `→ falling back to in-memory (卡片仍可发,但重启后时间线丢失)`);
3758
+ return new MemoryWecomCardStateStore;
3759
+ }
3760
+ }
3761
+
3762
+ // src/env/task/plugins/wecom-task-notify-plugin.ts
3763
+ var logger9 = createLogger("wecom:task-notify");
3764
+ var WecomNotifyType = {
3765
+ TASK_CREATED: "task_created",
3766
+ TASK_UPDATED: "task_updated",
3767
+ TASK_COMPLETED: "task_completed",
3768
+ OPERATION_CREATED: "operation_created"
3769
+ };
3770
+ var WECOM_EVENT_TYPE = "event-source.event.wecom-cli";
3771
+ var WECOM_CHANNEL_TYPE = "wecom-cli";
3772
+ function extractWecomFields(env, fallbackSourceId) {
3773
+ const evt = env.envEvent;
3774
+ const rcType = env.replyChannel?.type;
3775
+ const isWecom = evt?.type === WECOM_EVENT_TYPE || rcType === WECOM_CHANNEL_TYPE;
3776
+ if (!isWecom)
3777
+ return { isWecom: false };
3778
+ const chatId = env.replyChannel?.chatId || (typeof evt?.metadata?.chatId === "string" ? evt.metadata.chatId : undefined);
3779
+ const sourceId = (typeof evt?.metadata?.sourceId === "string" ? evt.metadata.sourceId : undefined) ?? fallbackSourceId ?? undefined;
3780
+ return { isWecom: true, chatId, sourceId };
3781
+ }
3782
+ function statusLabelOf(task, terminalStatus) {
3783
+ const s = String(terminalStatus ?? task.status ?? "");
3784
+ switch (s) {
3785
+ case "todo":
3786
+ return "\uD83D\uDCDD 待办";
3787
+ case "active":
3788
+ return "\uD83D\uDD04 进行中";
3789
+ case "to_validate":
3790
+ return "\uD83D\uDD0D 待验证";
3791
+ case "completed":
3792
+ return "\uD83C\uDF89 已完成";
3793
+ case "paused":
3794
+ return "⏸️ 已暂停";
3795
+ case "cancelled":
3796
+ return "\uD83D\uDEAB 已取消";
3797
+ case "archived":
3798
+ return "\uD83D\uDCE6 已归档";
3799
+ case "failed":
3800
+ case "timeout":
3801
+ return "\uD83D\uDEA8 已失败";
3802
+ default:
3803
+ return "\uD83D\uDD04 进行中";
3804
+ }
3805
+ }
3806
+ function isTerminalUpdate(task, changes) {
3807
+ const effective = String(changes?.status ?? task.status ?? "");
3808
+ return effective === "completed" || effective === "cancelled" || effective === "failed" || effective === "timeout" || effective === "archived" || effective === "paused";
3809
+ }
3810
+ function isHardTerminal(status) {
3811
+ return status === "completed" || status === "cancelled" || status === "failed" || status === "timeout" || status === "archived";
3812
+ }
3813
+ function clip(text, max = 200) {
3814
+ if (!text)
3815
+ return "";
3816
+ const t = text.trim();
3817
+ return t.length > max ? `${t.slice(0, max)}…` : t;
3818
+ }
3819
+ function buildWecomTaskCard(input) {
3820
+ const { task, statusLabel, milestones } = input;
3821
+ const lines = [];
3822
+ lines.push(`### \uD83D\uDCCB Task #${task.id} · ${task.title}`);
3823
+ lines.push("");
3824
+ const progress = typeof task.progress === "number" && task.progress > 0 ? `(${task.progress}%)` : "";
3825
+ lines.push(`**状态**:${statusLabel}${progress}`);
3826
+ if (input.terminal) {
3827
+ lines.push(`---`);
3828
+ }
3829
+ lines.push("");
3830
+ const desc = clip(task.description, 120);
3831
+ if (desc) {
3832
+ lines.push(`> ${desc}`);
3833
+ lines.push("");
3834
+ }
3835
+ if (input.changeSummary) {
3836
+ lines.push(`**本次更新**:${clip(input.changeSummary, 120)}`);
3837
+ lines.push("");
3838
+ }
3839
+ if (milestones.length > 0) {
3840
+ lines.push(`**时间线**`);
3841
+ for (const m of milestones) {
3842
+ const ts = formatTs(m.timestamp);
3843
+ const d = m.description ? `:${clip(m.description, 100)}` : "";
3844
+ lines.push(`- \uD83D\uDD50 ${ts} — ${m.title}${d}`);
2292
3845
  }
3846
+ lines.push("");
3847
+ }
3848
+ return lines.join(`
3849
+ `).trim();
3850
+ }
3851
+ function formatTs(iso) {
3852
+ try {
3853
+ const d = new Date(iso);
3854
+ if (isNaN(d.getTime()))
3855
+ return iso;
3856
+ const pad = (n) => String(n).padStart(2, "0");
3857
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` + `${pad(d.getHours())}:${pad(d.getMinutes())}`;
3858
+ } catch {
3859
+ return iso;
3860
+ }
3861
+ }
3862
+ function milestoneFromOperation(op) {
3863
+ const raw = op;
3864
+ const title = op.milestoneTitle ?? raw.action_title ?? String(op.milestoneType ?? "里程碑");
3865
+ const detail = op.milestoneDescription ?? raw.action_description ?? undefined;
3866
+ return {
3867
+ title,
3868
+ timestamp: op.timestamp ?? new Date().toISOString(),
3869
+ description: detail ?? op.processDescription ?? undefined
3870
+ };
3871
+ }
3872
+
3873
+ class WecomTaskNotifyHook {
3874
+ name;
3875
+ priority;
3876
+ description;
3877
+ pluginName;
3878
+ sourceId;
3879
+ config;
3880
+ sender;
3881
+ cardStateStore;
3882
+ esStatusNotifications;
3883
+ lastNotifyTimestamps = new Map;
3884
+ constructor(config = {}) {
3885
+ this.name = config.pluginName || "WecomTaskNotifyHook";
3886
+ this.priority = config.priority ?? 5;
3887
+ this.description = "Wecom 任务进展通知 Hook(markdown 卡片)";
3888
+ this.pluginName = config.pluginName;
3889
+ this.sourceId = config.sourceId;
3890
+ this.config = {
3891
+ enabled: true,
3892
+ priority: 5,
3893
+ notifyTypes: [
3894
+ WecomNotifyType.TASK_CREATED,
3895
+ WecomNotifyType.TASK_UPDATED,
3896
+ WecomNotifyType.TASK_COMPLETED,
3897
+ WecomNotifyType.OPERATION_CREATED
3898
+ ],
3899
+ ...config
3900
+ };
3901
+ this.esStatusNotifications = config.statusNotifications === true;
3902
+ this.sender = config.sender ?? {
3903
+ send: (input) => this.defaultSend(input)
3904
+ };
3905
+ this.cardStateStore = config.cardStateStore ?? createWecomCardStateStore();
2293
3906
  }
2294
- persistCardState(state, includeTimeline = false) {
2295
- if (!this.cardStateStore)
3907
+ async defaultSend(input) {
3908
+ const env = getEnvContextOrEmpty();
3909
+ const { sourceId } = extractWecomFields(env, this.sourceId);
3910
+ return sendWecomReply({
3911
+ sourceId: sourceId ?? "unknown",
3912
+ rawChatId: input.chatId,
3913
+ content: input.content
3914
+ });
3915
+ }
3916
+ async execute(ctx) {
3917
+ if (!this.config.enabled)
2296
3918
  return;
2297
- try {
2298
- const payload = includeTimeline ? this.snapshotTimeline(state) : state;
2299
- this.cardStateStore.upsert(payload);
2300
- } catch (err) {
2301
- logger7.warn(`persistCardState failed for task #${state.taskId}: ${err.message}`);
3919
+ const env = getEnvContextOrEmpty();
3920
+ if (this.pluginName && this.sourceId) {
3921
+ const key = `${this.sourceId}:${this.pluginName}`;
3922
+ if (env.pluginEnabled && env.pluginEnabled[key] !== true) {
3923
+ logger9.debug(`Plugin not enabled for source: ${key}`);
3924
+ return;
3925
+ }
2302
3926
  }
2303
- }
2304
- persistPinned(taskId, pinned) {
2305
- if (!this.cardStateStore)
3927
+ const fields = extractWecomFields(env, this.sourceId);
3928
+ if (!fields.isWecom) {
3929
+ logger9.debug("not a wecom event, skip");
2306
3930
  return;
2307
- try {
2308
- this.cardStateStore.markPinned(taskId, pinned);
2309
- } catch (err) {
2310
- logger7.warn(`persistPinned failed for task #${taskId}: ${err.message}`);
3931
+ }
3932
+ switch (ctx.hookPoint) {
3933
+ case TaskHookPoints.AFTER_CREATE:
3934
+ await this.onAfterCreate(ctx);
3935
+ break;
3936
+ case TaskHookPoints.AFTER_UPDATE:
3937
+ await this.onAfterUpdate(ctx);
3938
+ break;
3939
+ case TaskHookPoints.AFTER_COMPLETE:
3940
+ await this.onAfterComplete(ctx);
3941
+ break;
3942
+ case TaskHookPoints.OPERATION_AFTER_CREATE:
3943
+ await this.onAfterOperationCreate(ctx);
3944
+ break;
3945
+ case TaskHookPoints.AFTER_DELETE:
3946
+ await this.onAfterDelete(ctx);
3947
+ break;
2311
3948
  }
2312
3949
  }
2313
- persistPaused(taskId) {
2314
- if (!this.cardStateStore)
3950
+ async onAfterCreate(ctx) {
3951
+ const { task } = ctx.data;
3952
+ if (!task || !this.shouldNotify(WecomNotifyType.TASK_CREATED))
2315
3953
  return;
2316
- try {
2317
- this.cardStateStore.markPaused(taskId);
2318
- } catch (err) {
2319
- logger7.warn(`persistPaused failed for task #${taskId}: ${err.message}`);
2320
- }
3954
+ await this.sendCardStart(task);
2321
3955
  }
2322
- dropStaleMessageId(taskId) {
2323
- if (!this.cardStateStore)
3956
+ async onAfterUpdate(ctx) {
3957
+ const data = ctx.data;
3958
+ if (!data?.task)
2324
3959
  return;
2325
- try {
2326
- const existing = this.cardStateStore.get(taskId);
2327
- if (!existing)
3960
+ const { task, changes } = data;
3961
+ const changeKeys = Object.keys(changes ?? {});
3962
+ if (changeKeys.length === 0)
3963
+ return;
3964
+ if (isTerminalUpdate(task, changes)) {
3965
+ if (!this.shouldNotify(WecomNotifyType.TASK_COMPLETED))
2328
3966
  return;
2329
- this.cardStateStore.upsert({
2330
- ...existing,
2331
- messageId: "",
2332
- pinned: false,
2333
- updatedAt: Date.now()
2334
- });
2335
- } catch (err) {
2336
- logger7.warn(`dropStaleMessageId failed for task #${taskId}: ${err.message}`);
2337
- }
2338
- }
2339
- persistReopened(taskId) {
2340
- if (!this.cardStateStore)
3967
+ await this.sendCardFinish(task, String(changes?.status ?? task.status));
2341
3968
  return;
2342
- try {
2343
- this.cardStateStore.markReopened(taskId);
2344
- } catch (err) {
2345
- logger7.warn(`persistReopened failed for task #${taskId}: ${err.message}`);
2346
3969
  }
2347
- }
2348
- persistClosed(taskId) {
2349
- if (!this.cardStateStore)
3970
+ if (!this.shouldNotify(WecomNotifyType.TASK_UPDATED))
2350
3971
  return;
2351
- try {
2352
- this.cardStateStore.markClosed(taskId);
2353
- } catch (err) {
2354
- logger7.warn(`persistClosed failed for task #${taskId}: ${err.message}`);
2355
- }
3972
+ await this.sendCardUpdate(task, changes);
2356
3973
  }
2357
- refreshTimeline(taskId) {
2358
- if (!this.cardStateStore || !this.cardNotifier)
3974
+ async onAfterComplete(ctx) {
3975
+ const data = ctx.data;
3976
+ const task = data?.task ?? data;
3977
+ if (!task || !this.shouldNotify(WecomNotifyType.TASK_COMPLETED))
3978
+ return;
3979
+ const terminalStatus = data.terminalStatus;
3980
+ if (terminalStatus === "to_validate" || task.status === "to_validate") {
2359
3981
  return;
2360
- try {
2361
- const rec = this.cardStateStore.get(taskId);
2362
- if (!rec || !rec.messageId)
2363
- return;
2364
- this.cardStateStore.upsert(this.snapshotTimeline(rec));
2365
- } catch (err) {
2366
- logger7.warn(`refreshTimeline failed for task #${taskId}: ${err.message}`);
2367
3982
  }
3983
+ await this.sendCardFinish(task, String(task.status));
2368
3984
  }
2369
- async repinAfterResume(task, persisted) {
2370
- if (!this.cardNotifier || typeof this.cardNotifier.pinMessage !== "function") {
3985
+ async onAfterDelete(ctx) {
3986
+ const data = ctx.data;
3987
+ const taskId = data?.id;
3988
+ if (data?.deleted !== true || typeof taskId !== "number")
2371
3989
  return;
2372
- }
2373
- if (!persisted.messageId)
3990
+ if (!this.shouldNotify(WecomNotifyType.TASK_COMPLETED))
2374
3991
  return;
2375
- if (persisted.pinned)
3992
+ const rec = this.getRecord(taskId);
3993
+ if (!rec) {
3994
+ logger9.debug(`onAfterDelete: task #${taskId} 无卡片记录,skip`);
2376
3995
  return;
2377
- const ctx = getEnvContextOrEmpty();
2378
- const profile = this.resolveProfile(task, ctx);
2379
- try {
2380
- await this.cardNotifier.pinMessage(persisted.messageId, profile);
2381
- this.persistPinned(task.id, true);
2382
- logger7.debug(`repinAfterResume: task #${task.id} 卡片 ${persisted.messageId} 已重新置顶`);
2383
- } catch (err) {
2384
- logger7.warn(`repinAfterResume failed (non-fatal) for task #${task.id}: ${err.message}`);
2385
- }
2386
- }
2387
- buildCardState(task, messageId, chatId, profile, pinned) {
2388
- let existing;
2389
- try {
2390
- existing = this.cardStateStore?.get(task.id);
2391
- } catch (err) {
2392
- logger7.warn(`buildCardState: read existing failed for task #${task.id}: ${err.message}`);
2393
3996
  }
2394
- const now = Date.now();
2395
- return {
2396
- taskId: task.id,
2397
- messageId,
2398
- chatId,
2399
- profile,
2400
- pinned,
2401
- status: "open",
2402
- createdAt: existing?.createdAt ?? now,
2403
- updatedAt: now
2404
- };
3997
+ const milestones = this.parseTimeline(rec.timeline);
3998
+ milestones.push({
3999
+ title: "任务已取消(删除)",
4000
+ timestamp: new Date().toISOString()
4001
+ });
4002
+ await this.deliver({
4003
+ chatId: rec.chatId,
4004
+ task: {
4005
+ id: taskId,
4006
+ title: `(已删除)`,
4007
+ description: "",
4008
+ status: "cancelled",
4009
+ progress: 0
4010
+ },
4011
+ statusLabel: statusLabelOf({ status: "cancelled" }),
4012
+ milestones,
4013
+ terminal: true
4014
+ });
4015
+ this.cardStateStore.markClosed(taskId);
4016
+ logger9.debug(`onAfterDelete: task #${taskId} 卡片已收尾`);
2405
4017
  }
2406
- getCardState(taskId) {
2407
- if (!this.cardStateStore)
4018
+ async onAfterOperationCreate(ctx) {
4019
+ const { operation } = ctx.data;
4020
+ if (!operation)
2408
4021
  return;
2409
- try {
2410
- return this.cardStateStore.get(taskId);
2411
- } catch {
4022
+ if (!this.shouldNotify(WecomNotifyType.OPERATION_CREATED))
4023
+ return;
4024
+ const rec = this.getRecord(operation.taskId);
4025
+ if (!rec) {
4026
+ logger9.debug(`onAfterOperationCreate: task #${operation.taskId} 无卡片记录,skip`);
2412
4027
  return;
2413
4028
  }
2414
- }
2415
- listCardStates() {
2416
- if (!this.cardStateStore)
2417
- return [];
2418
- try {
2419
- return this.cardStateStore.list();
2420
- } catch {
2421
- return [];
2422
- }
2423
- }
2424
- isCardModeEnabled() {
2425
- if (this.config.cardMode === false || !this.cardNotifier) {
2426
- logger7.debug("cardMode=false (legacy opt-out), skip task notification (no text fallback)");
2427
- return false;
2428
- }
2429
- return true;
2430
- }
2431
- async resolveCardTarget(task) {
2432
- const env = getEnvContextOrEmpty();
2433
- const triggerChatId = env.replyChannel?.chatId;
2434
- const triggerProfile = env.replyChannel?.profile;
2435
- const lineage = await this.collectLineage(task);
2436
- const lineageIds = lineage.map((t) => t.id);
2437
- const selfChatId = extractPerTaskChatId(task.context);
2438
- let lineageChat;
2439
- for (const node of lineage) {
2440
- const chatId = extractPerTaskChatId(node.context);
2441
- if (chatId) {
2442
- lineageChat = {
2443
- chatId,
2444
- profile: extractPerTaskChatProfile(node.context),
2445
- isSelf: node.id === task.id
2446
- };
2447
- break;
2448
- }
2449
- }
2450
- if (!lineageChat && selfChatId) {
2451
- lineageChat = {
2452
- chatId: selfChatId,
2453
- profile: extractPerTaskChatProfile(task.context),
2454
- isSelf: true
2455
- };
2456
- }
2457
- return decideCardChatTarget({
2458
- taskId: task.id,
2459
- lineageIds,
2460
- lineageChat,
2461
- triggerChatId,
2462
- triggerProfile
2463
- });
2464
- }
2465
- async collectLineage(task) {
2466
- const out = [
2467
- { id: task.id, context: task.context }
2468
- ];
2469
- let parentId = typeof task.parent_task_id === "number" && task.parent_task_id > 0 ? task.parent_task_id : undefined;
2470
- let depth = 0;
2471
- while (parentId && depth < 10 && this.cardRoutingPorts?.getTask) {
2472
- try {
2473
- const parent = await this.cardRoutingPorts.getTask(parentId);
2474
- if (!parent)
2475
- break;
2476
- out.push({ id: parentId, context: parent.context });
2477
- parentId = typeof parent.parent_task_id === "number" && parent.parent_task_id > 0 ? parent.parent_task_id : undefined;
2478
- } catch {
2479
- break;
2480
- }
2481
- depth++;
4029
+ if (rec.status !== "open") {
4030
+ logger9.debug(`onAfterOperationCreate: task #${operation.taskId} status=${rec.status}, skip`);
4031
+ return;
2482
4032
  }
2483
- return out;
4033
+ const milestone = milestoneFromOperation(operation);
4034
+ const milestones = this.parseTimeline(rec.timeline);
4035
+ milestones.push(milestone);
4036
+ const trimmed = this.trimTimeline(milestones);
4037
+ this.persistTimeline(operation.taskId, rec, trimmed, rec.updateCount);
4038
+ logger9.debug(`[single-card] OPERATION_CREATED for task ` + `#${operation.taskId} merged into timeline`);
2484
4039
  }
2485
4040
  async sendCardStart(task) {
2486
- if (!this.cardNotifier)
2487
- return;
2488
- const persisted = this.restoreCardState(task.id);
2489
- if (persisted?.status === "closed") {
2490
- this.cardMessageIds.delete(task.id);
2491
- } else if (persisted?.status === "paused") {
2492
- this.persistReopened(task.id);
2493
- }
2494
- const claim = this.cardMessageIds.get(task.id);
2495
- if (claim) {
2496
- logger7.debug(`sendCardStart: task #${task.id} 已有首卡(${claim})→ 跳过重复发卡`);
4041
+ const env = getEnvContextOrEmpty();
4042
+ const fields = extractWecomFields(env, this.sourceId);
4043
+ const chatId = fields.chatId;
4044
+ if (!chatId) {
4045
+ logger9.debug(`sendCardStart: no chatId for task #${task.id}, skip`);
2497
4046
  return;
2498
4047
  }
2499
- this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL);
2500
- let target;
2501
- try {
2502
- target = await this.resolveCardTarget(task);
2503
- } catch (err) {
2504
- this.cardMessageIds.delete(task.id);
2505
- throw err;
2506
- }
2507
- const chatId = target.chatId;
2508
- const profile = target.profile;
2509
- if (!chatId) {
2510
- this.cardMessageIds.delete(task.id);
2511
- logger7.debug(`sendCardStart: no chatId for task #${task.id}, skip`);
4048
+ const existing = this.getRecord(task.id);
4049
+ if (existing?.status === "open") {
4050
+ logger9.debug(`sendCardStart: task #${task.id} 已有 open 记录 → 跳过重复发卡`);
2512
4051
  return;
2513
4052
  }
2514
- try {
2515
- const { messageId, pinned } = await this.cardNotifier.startTask({
4053
+ const milestones = [
4054
+ {
4055
+ title: "Task 创建",
4056
+ timestamp: new Date().toISOString(),
4057
+ description: task.current_status || undefined
4058
+ }
4059
+ ];
4060
+ const ok = await this.deliver({
4061
+ chatId,
4062
+ task,
4063
+ statusLabel: statusLabelOf(task),
4064
+ milestones
4065
+ });
4066
+ if (ok) {
4067
+ this.cardStateStore.upsert({
4068
+ taskId: task.id,
2516
4069
  chatId,
2517
- profile,
2518
- meta: this.buildTaskMeta(task),
2519
- milestones: [
2520
- {
2521
- title: "Task 创建",
2522
- timestamp: new Date().toISOString(),
2523
- description: task.current_status || undefined
2524
- }
2525
- ]
4070
+ pinned: false,
4071
+ status: "open",
4072
+ createdAt: Date.now(),
4073
+ updatedAt: Date.now(),
4074
+ updateCount: 1,
4075
+ timeline: JSON.stringify(milestones)
2526
4076
  });
2527
- this.cardMessageIds.set(task.id, messageId);
2528
- this.persistCardState(this.buildCardState(task, messageId, chatId, profile, pinned), true);
2529
- logger7.debug(`sendCardStart: task #${task.id} → message ${messageId} (pinned=${pinned}) ` + `chat=${chatId} source=${target.source} reason=${target.reason}`);
2530
- } catch (err) {
2531
- this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL);
2532
- logger7.error(`sendCardStart failed for task #${task.id}: ${err.message}`);
4077
+ } else {
4078
+ this.cardStateStore.upsert({
4079
+ taskId: task.id,
4080
+ chatId,
4081
+ pinned: false,
4082
+ status: "open",
4083
+ createdAt: Date.now(),
4084
+ updatedAt: Date.now(),
4085
+ updateCount: 0,
4086
+ timeline: JSON.stringify(milestones)
4087
+ });
4088
+ logger9.warn(`sendCardStart: task #${task.id} 首卡发送失败(已落库待重试)`);
2533
4089
  }
2534
4090
  }
2535
4091
  async sendCardUpdate(task, changes) {
2536
- if (!this.cardNotifier)
2537
- return;
2538
- const persisted = this.restoreCardState(task.id);
2539
- if (persisted?.status === "closed") {
2540
- const stillTerminal = isTerminalTaskStatus(String(task.status));
2541
- if (stillTerminal) {
2542
- logger7.debug(`sendCardUpdate: task #${task.id} 卡片已终结(closed)且任务仍终态 → skip patch`);
4092
+ const rec = this.getRecord(task.id);
4093
+ if (!rec) {
4094
+ if (isHardTerminal(String(task.status))) {
4095
+ logger9.debug(`sendCardUpdate: task #${task.id} 终态且无卡片 → skip`);
2543
4096
  return;
2544
4097
  }
2545
- logger7.info(`sendCardUpdate: task #${task.id} 已 reopen(closed 记录 + 当前 status=${task.status})` + `→ 重新建卡`);
2546
- this.cardMessageIds.delete(task.id);
2547
- this.dropStaleMessageId(task.id);
2548
- }
2549
- if (persisted?.status === "paused" && String(task.status) !== "paused") {
2550
- this.persistReopened(task.id);
2551
- logger7.debug(`sendCardUpdate: task #${task.id} resume(paused → open),复用卡片 ${persisted.messageId}`);
2552
- await this.repinAfterResume(task, persisted);
4098
+ logger9.debug(`sendCardUpdate: task #${task.id} 无卡片记录 → ` + `单卡语义不补发(进展落时间线)`);
4099
+ return;
2553
4100
  }
2554
- let messageId = this.cardMessageIds.get(task.id);
2555
- if (!messageId && isTerminalTaskStatus(String(task.status))) {
2556
- logger7.debug(`sendCardUpdate: task #${task.id} 状态=${task.status}(终结/归档)且无卡片 → skip`);
4101
+ if (rec.status === "closed" && isHardTerminal(String(task.status))) {
4102
+ logger9.debug(`sendCardUpdate: task #${task.id} closed + 终态 → skip`);
2557
4103
  return;
2558
4104
  }
2559
- if (!messageId) {
2560
- this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL);
4105
+ const reopened = rec.status === "closed" && !isHardTerminal(String(task.status));
4106
+ if (reopened) {
4107
+ this.cardStateStore.markReopened(task.id);
4108
+ logger9.debug(`sendCardUpdate: task #${task.id} reopen(复用记录)`);
4109
+ }
4110
+ const fresh = reopened ? this.getRecord(task.id) : rec;
4111
+ const milestones = this.parseTimeline(fresh.timeline);
4112
+ if (reopened) {
4113
+ const last = milestones[milestones.length - 1];
4114
+ if (last?.title !== REOPEN_TITLE) {
4115
+ milestones.push({
4116
+ title: REOPEN_TITLE,
4117
+ timestamp: new Date().toISOString(),
4118
+ description: task.current_status ? task.current_status.slice(0, 80) : undefined
4119
+ });
4120
+ }
2561
4121
  }
2562
- const target = messageId ? undefined : await this.resolveCardTarget(task);
2563
- const chatId = target?.chatId ?? this.resolveChatId(task, getEnvContextOrEmpty());
2564
- const profile = target?.profile ?? this.resolveProfile(task, getEnvContextOrEmpty());
2565
- if (!chatId) {
2566
- if (!messageId)
2567
- this.cardMessageIds.delete(task.id);
2568
- logger7.debug(`sendCardUpdate: no chatId for task #${task.id}, skip`);
4122
+ this.persistTimeline(task.id, fresh, milestones, fresh.updateCount);
4123
+ logger9.debug(`sendCardUpdate: task #${task.id} 单卡语义(不发新卡,` + `进展走时间线;点「刷新详情」可见)`);
4124
+ }
4125
+ async sendCardFinish(task, rawStatus) {
4126
+ const rec = this.getRecord(task.id);
4127
+ if (!rec) {
4128
+ logger9.debug(`sendCardFinish: no card record for task #${task.id}, skip(不发新卡)`);
2569
4129
  return;
2570
4130
  }
2571
- if (messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
2572
- logger7.debug(`sendCardUpdate: startTask failed previously for task #${task.id}, skip`);
4131
+ if (rec.status === "closed" && rec.closedAt !== undefined && Date.now() - rec.closedAt < 60000) {
4132
+ const milestones2 = this.parseTimeline(rec.timeline);
4133
+ milestones2.push({
4134
+ title: FINISH_TITLE[rawStatus] ?? "任务收尾",
4135
+ timestamp: new Date().toISOString(),
4136
+ description: task.current_status || undefined
4137
+ });
4138
+ this.persistTimeline(task.id, rec, milestones2, rec.updateCount);
4139
+ logger9.debug(`sendCardFinish: task #${task.id} 已收尾(60s 内重复终态)→ ` + `合并不发新卡`);
2573
4140
  return;
2574
4141
  }
2575
- if (messageId === LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL) {
2576
- logger7.debug(`sendCardUpdate: task #${task.id} 首卡正在发送中(in-flight)→ 跳过本次 update`);
2577
- return;
4142
+ const milestones = this.parseTimeline(rec.timeline);
4143
+ milestones.push({
4144
+ title: FINISH_TITLE[rawStatus] ?? "任务收尾",
4145
+ timestamp: new Date().toISOString(),
4146
+ description: task.current_status || undefined
4147
+ });
4148
+ await this.deliver({
4149
+ chatId: rec.chatId,
4150
+ task,
4151
+ statusLabel: statusLabelOf(task, rawStatus),
4152
+ milestones,
4153
+ terminal: true
4154
+ });
4155
+ this.persistTimeline(task.id, rec, milestones, rec.updateCount + 1);
4156
+ if (rawStatus === "paused") {
4157
+ this.cardStateStore.markPaused(task.id);
4158
+ } else {
4159
+ this.cardStateStore.markClosed(task.id);
2578
4160
  }
2579
- if (!messageId) {
2580
- try {
2581
- const res = await this.cardNotifier.startTask({
2582
- chatId,
2583
- profile,
2584
- meta: this.buildTaskMeta(task)
4161
+ logger9.debug(`sendCardFinish: task #${task.id} → status=${rawStatus}`);
4162
+ }
4163
+ async deliver(input) {
4164
+ const content = buildWecomTaskCard({
4165
+ task: input.task,
4166
+ statusLabel: input.statusLabel,
4167
+ milestones: input.milestones,
4168
+ changeSummary: input.changeSummary,
4169
+ terminal: input.terminal
4170
+ });
4171
+ const cardResult = await this.deliverTemplateCard(input);
4172
+ if (cardResult) {
4173
+ if (this.statusNotificationsEnabled()) {
4174
+ await this.sendStatusMarkdown(input, content).catch((err) => {
4175
+ logger9.warn(`sendStatusMarkdown failed for task #${input.task.id}: ` + `${err.message}(不影响卡片送达)`);
2585
4176
  });
2586
- messageId = res.messageId;
2587
- this.cardMessageIds.set(task.id, messageId);
2588
- this.persistCardState(this.buildCardState(task, messageId, chatId, profile, res.pinned), true);
2589
- } catch (err) {
2590
- this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL);
2591
- logger7.error(`sendCardUpdate fallback start failed: ${err.message}`);
2592
- return;
2593
4177
  }
4178
+ return true;
2594
4179
  }
2595
- const changeKeys = Object.keys(changes ?? {});
2596
- const processSteps = changeKeys.length > 0 ? [
2597
- {
2598
- title: "状态更新",
2599
- timestamp: new Date().toISOString(),
2600
- description: changeKeys.map((k) => `${k}=${humanizeValue(changes[k], 80)}`).join(" · ")
2601
- }
2602
- ] : [];
2603
4180
  try {
2604
- await this.cardNotifier.updateTask({
2605
- messageId,
2606
- chatId,
2607
- profile,
2608
- status: "running",
2609
- meta: this.buildTaskMeta(task),
2610
- progress: typeof task.progress === "number" && task.progress > 0 ? task.progress : undefined,
2611
- milestones: [],
2612
- processSteps
4181
+ const r = await this.sender.send({
4182
+ chatId: input.chatId,
4183
+ content
2613
4184
  });
2614
- const current = this.getCardState(task.id);
2615
- this.persistPinned(task.id, current?.pinned ?? persisted?.pinned ?? true);
2616
- this.refreshTimeline(task.id);
4185
+ if (!r.ok) {
4186
+ logger9.warn(`deliver failed for task #${input.task.id}: ${r.error ?? "unknown"}`);
4187
+ return false;
4188
+ }
4189
+ return true;
2617
4190
  } catch (err) {
2618
- logger7.error(`sendCardUpdate failed: ${err.message}`);
4191
+ logger9.error(`deliver threw for task #${input.task.id}: ${err.message}`);
4192
+ return false;
2619
4193
  }
2620
4194
  }
2621
- async sendCardFinish(task) {
2622
- if (!this.cardNotifier)
2623
- return;
2624
- this.restoreCardState(task.id);
2625
- const messageId = this.cardMessageIds.get(task.id);
2626
- const ctx = getEnvContextOrEmpty();
2627
- const profile = this.resolveProfile(task, ctx);
2628
- if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
2629
- logger7.debug(`sendCardFinish: no card messageId for task #${task.id}, skip ` + `(记录缺失 → 不发新卡,保持幂等)`);
2630
- return;
4195
+ statusNotificationsEnabled() {
4196
+ try {
4197
+ return this.esStatusNotifications === true;
4198
+ } catch {
4199
+ return false;
2631
4200
  }
2632
- const rawStatus = String(task.status);
2633
- const status = mapTaskStatusToCardStatus(rawStatus);
2634
- const isPaused = rawStatus === "paused" || rawStatus === "archived";
4201
+ }
4202
+ async sendStatusMarkdown(input, content) {
4203
+ const r = await this.sender.send({ chatId: input.chatId, content });
4204
+ if (!r.ok) {
4205
+ logger9.warn(`sendStatusMarkdown failed for task #${input.task.id}: ` + `${r.error ?? "unknown"}(不影响卡片送达)`);
4206
+ }
4207
+ }
4208
+ async deliverTemplateCard(input) {
2635
4209
  try {
2636
- this.cancelTrailingPatch(task.id);
2637
- const finish = isPaused ? this.cardNotifier.pauseTask.bind(this.cardNotifier) : this.cardNotifier.finishTask.bind(this.cardNotifier);
2638
- await finish({
2639
- messageId,
2640
- profile,
2641
- status,
2642
- meta: this.buildTaskMeta(task),
2643
- finalStep: task.current_status || undefined,
2644
- expandTimeline: rawStatus === "archived" ? true : undefined,
2645
- ...rawStatus === "archived" ? { pausedMilestoneTitle: "Task 已归档" } : {}
4210
+ const env = getEnvContextOrEmpty();
4211
+ const fields = extractWecomFields(env, this.sourceId);
4212
+ const sourceId = fields.sourceId ?? "unknown";
4213
+ const chatType = env.replyChannel?.params?.chatType === "group" || fields.chatId !== input.chatId ? "group" : "single";
4214
+ const notice = buildWecomTextNoticeCard({
4215
+ taskId: input.task.id,
4216
+ title: input.task.title,
4217
+ statusLabel: input.statusLabel,
4218
+ progress: Number(input.task.progress ?? 0),
4219
+ description: input.task.description,
4220
+ changeSummary: input.changeSummary,
4221
+ milestones: input.milestones,
4222
+ terminal: input.terminal
2646
4223
  });
2647
- if (isPaused) {
2648
- this.persistPaused(task.id);
2649
- this.persistPinned(task.id, false);
2650
- this.refreshTimeline(task.id);
2651
- } else {
2652
- this.persistClosed(task.id);
2653
- this.persistPinned(task.id, false);
2654
- this.cardMessageIds.delete(task.id);
4224
+ const card = input.terminal ? notice : buildWecomButtonInteractionCard({
4225
+ taskId: input.task.id,
4226
+ title: input.task.title,
4227
+ statusLabel: input.statusLabel,
4228
+ progress: Number(input.task.progress ?? 0),
4229
+ description: input.task.description,
4230
+ changeSummary: input.changeSummary,
4231
+ milestones: input.milestones,
4232
+ buttons: [
4233
+ { text: "刷新详情", key: WecomCardActionKey.VIEW, style: 1 },
4234
+ {
4235
+ text: "刷新活性",
4236
+ key: WecomCardActionKey.REFRESH_ACTIVITY,
4237
+ style: 2
4238
+ }
4239
+ ]
4240
+ });
4241
+ const r = await sendWecomTemplateCard({
4242
+ sourceId,
4243
+ chatId: input.chatId,
4244
+ chatType,
4245
+ card
4246
+ });
4247
+ if (!r.ok) {
4248
+ logger9.warn(`deliverTemplateCard failed for task #${input.task.id}: ` + `${r.error ?? "unknown"}(回落 markdown)`);
4249
+ return false;
2655
4250
  }
2656
- logger7.debug(`sendCardFinish: task #${task.id} → status=${status} ` + `(raw=${rawStatus}) message=${messageId}`);
4251
+ return true;
2657
4252
  } catch (err) {
2658
- logger7.error(`sendCardFinish failed: ${err.message}`);
4253
+ logger9.warn(`deliverTemplateCard threw for task #${input.task.id}: ` + `${err.message}(回落 markdown)`);
4254
+ return false;
2659
4255
  }
2660
4256
  }
2661
- resolveChatId(task, env) {
2662
- const envChat = env.replyChannel?.chatId;
2663
- if (envChat)
2664
- return envChat;
2665
- const ctxStr = task.context;
2666
- if (typeof ctxStr === "string") {
2667
- const ctxChat = parseLarkPerTaskChatId(ctxStr);
2668
- if (ctxChat)
2669
- return ctxChat;
4257
+ getRecord(taskId) {
4258
+ try {
4259
+ return this.cardStateStore.get(taskId);
4260
+ } catch (err) {
4261
+ logger9.warn(`getRecord failed for task #${taskId}: ${err.message}`);
4262
+ return;
2670
4263
  }
2671
- return;
2672
4264
  }
2673
- resolveProfile(task, env) {
2674
- const envProfile = env.replyChannel?.profile;
2675
- if (envProfile)
2676
- return envProfile;
2677
- const ctxStr = task.context;
2678
- if (typeof ctxStr === "string") {
2679
- const ctxProfile = parseLarkPerTaskChatProfile(ctxStr);
2680
- if (ctxProfile)
2681
- return ctxProfile;
4265
+ parseTimeline(timeline) {
4266
+ if (!timeline)
4267
+ return [];
4268
+ try {
4269
+ const arr = JSON.parse(timeline);
4270
+ return Array.isArray(arr) ? arr : [];
4271
+ } catch {
4272
+ return [];
2682
4273
  }
2683
- return;
2684
4274
  }
2685
- buildTaskMeta(task) {
2686
- const recordedNames = extractRecordedChatNames(task.context);
2687
- const matchedName = recordedNames.find((n) => new RegExp(`(?:Task\\s*#?\\s*|#)(?<![\\d])(?<!\\d)${task.id}(?!\\d)(?![\\d])`, "i").test(n));
2688
- return {
2689
- id: task.id,
2690
- title: task.title,
2691
- description: task.description,
2692
- goals: task.goals_and_expected_deliverables,
2693
- parentTaskId: typeof task.parent_task_id === "number" && task.parent_task_id > 0 ? task.parent_task_id : undefined,
2694
- priority: task.priority,
2695
- type: task.type,
2696
- projectPath: task.project_path,
2697
- context: task.context,
2698
- currentStatus: task.current_status,
2699
- perTaskChatId: extractPerTaskChatId(task.context),
2700
- perTaskChatName: matchedName
2701
- };
4275
+ trimTimeline(milestones) {
4276
+ const max = this.config.maxTimelineEntries ?? 20;
4277
+ if (milestones.length <= max)
4278
+ return milestones;
4279
+ return milestones.slice(milestones.length - max);
2702
4280
  }
2703
- deriveTimelineFromOperation(operation) {
2704
- const raw = operation;
2705
- const title = operation.milestoneTitle ?? operation.actionTitle ?? raw.action_title ?? raw.description ?? raw.type ?? undefined;
2706
- const detail = operation.milestoneDescription ?? operation.actionDescription ?? raw.action_description ?? undefined;
2707
- const timestamp = operation.timestamp ?? new Date().toISOString();
2708
- const milestones = [];
2709
- const processSteps = [];
2710
- if (operation.milestoneTitle || operation.milestoneType || title) {
2711
- milestones.push({
2712
- title: title ?? String(operation.milestoneType ?? "里程碑"),
2713
- timestamp,
2714
- description: detail
2715
- });
2716
- }
2717
- if (operation.processDescription || detail) {
2718
- processSteps.push({
2719
- title: title ?? "过程",
2720
- timestamp,
2721
- description: operation.processDescription ?? detail
4281
+ persistTimeline(taskId, rec, milestones, updateCount) {
4282
+ try {
4283
+ this.cardStateStore.upsert({
4284
+ ...rec,
4285
+ updatedAt: Date.now(),
4286
+ updateCount,
4287
+ timeline: JSON.stringify(milestones)
2722
4288
  });
4289
+ } catch (err) {
4290
+ logger9.warn(`persistTimeline failed for task #${taskId}: ${err.message}`);
2723
4291
  }
2724
- const currentStep = operation.processDescription ?? detail ?? title ?? undefined;
2725
- return { milestones, processSteps, currentStep };
4292
+ }
4293
+ hydrateTask(taskId, rec) {
4294
+ return {
4295
+ id: taskId,
4296
+ title: `Task #${taskId}`,
4297
+ description: "",
4298
+ status: "active",
4299
+ priority: "medium",
4300
+ type: "normal",
4301
+ progress: 0,
4302
+ current_status: "",
4303
+ goals_and_expected_deliverables: "",
4304
+ createdAt: new Date(rec.createdAt).toISOString(),
4305
+ updatedAt: new Date(rec.updatedAt).toISOString(),
4306
+ project_path: "",
4307
+ context: "{}"
4308
+ };
2726
4309
  }
2727
4310
  shouldThrottleOperation(operation) {
2728
4311
  const throttleMs = this.config.operationThrottleMs ?? 5000;
@@ -2732,7 +4315,7 @@ class LarkCliTaskNotifyHook {
2732
4315
  const now = Date.now();
2733
4316
  const lastTime = this.lastNotifyTimestamps.get(key);
2734
4317
  if (lastTime && now - lastTime < throttleMs) {
2735
- logger7.debug(`[throttle] skip OPERATION_CREATED patch for task #${operation.taskId} (${now - lastTime}ms ago)`);
4318
+ logger9.debug(`[throttle] skip OPERATION_CREATED send for task #${operation.taskId} ` + `(${now - lastTime}ms ago)`);
2736
4319
  return true;
2737
4320
  }
2738
4321
  this.lastNotifyTimestamps.set(key, now);
@@ -2740,49 +4323,50 @@ class LarkCliTaskNotifyHook {
2740
4323
  }
2741
4324
  shouldNotify(type) {
2742
4325
  const { notifyTypes } = this.config;
2743
- if (notifyTypes && notifyTypes.length === 0)
4326
+ if (!notifyTypes || notifyTypes.length === 0)
2744
4327
  return true;
2745
- if (notifyTypes)
2746
- return notifyTypes.includes(type);
2747
- return type === "task_completed" /* TASK_COMPLETED */;
4328
+ return notifyTypes.includes(type);
2748
4329
  }
2749
4330
  }
2750
4331
  __legacyDecorateClassTS([
2751
- TracedAs("lark-cli-task-notify.execute")
2752
- ], LarkCliTaskNotifyHook.prototype, "execute", null);
2753
- __legacyDecorateClassTS([
2754
- TracedAs("lark-cli-task-notify.resolveCardTarget", {
2755
- recordParams: true,
2756
- recordResult: true,
2757
- flattenParamFilter: true,
2758
- paramFilter: (args) => ({
2759
- taskId: args[0]?.id,
2760
- triggerChatId: undefined
2761
- }),
2762
- resultFilter: (r) => ({
2763
- source: r?.source,
2764
- chatId: r?.chatId,
2765
- chatName: r?.chatName,
2766
- reason: r?.reason?.slice(0, 200)
2767
- })
2768
- })
2769
- ], LarkCliTaskNotifyHook.prototype, "resolveCardTarget", null);
2770
- function createLarkCliTaskNotifyHook(config = {}) {
2771
- return new LarkCliTaskNotifyHook(config);
4332
+ TracedExecute
4333
+ ], WecomTaskNotifyHook.prototype, "execute", null);
4334
+ var FINISH_TITLE = {
4335
+ completed: "\uD83C\uDF89 任务完成",
4336
+ cancelled: "\uD83D\uDEAB 任务取消",
4337
+ failed: "\uD83D\uDEA8 任务失败",
4338
+ timeout: "\uD83D\uDEA8 任务超时",
4339
+ archived: "\uD83D\uDCE6 任务归档",
4340
+ paused: "⏸️ 任务暂停"
4341
+ };
4342
+ var REOPEN_TITLE = "\uD83D\uDD04 已重开";
4343
+ function createWecomTaskNotifyHook(config = {}) {
4344
+ return new WecomTaskNotifyHook(config);
4345
+ }
4346
+ function TracedExecute(_target, _propertyKey, descriptor) {
4347
+ const original = descriptor.value;
4348
+ descriptor.value = async function(...args) {
4349
+ try {
4350
+ return await original.apply(this, args);
4351
+ } catch (err) {
4352
+ logger9.error(`wecom-task-notify execute threw: ${err.message}`);
4353
+ }
4354
+ };
4355
+ return descriptor;
2772
4356
  }
2773
4357
  // src/env/task/plugins/per-task-group-chat-plugin.ts
2774
4358
  init_decorator();
2775
4359
  init_logger();
2776
4360
  init_context();
2777
4361
  import { spawn } from "child_process";
2778
- var logger8 = createLogger("lark-cli:per-task-group");
4362
+ var logger10 = createLogger("lark-cli:per-task-group");
2779
4363
  function isPluginDebugEnabled() {
2780
4364
  return process.env.ROY_TASK_NOTIFY_DEBUG === "1" || process.env.ROY_TRACE_DEBUG === "1";
2781
4365
  }
2782
- var CTX_FIELD_CHAT_ID2 = "larkPerTaskChatId";
4366
+ var CTX_FIELD_CHAT_ID3 = "larkPerTaskChatId";
2783
4367
  var CTX_FIELD_CHAT_PROFILE2 = "larkPerTaskChatProfile";
2784
- var CTX_FIELD_CHAT_CREATED_AT = "larkPerTaskChatCreatedAt";
2785
- var CTX_FIELD_CHAT_TASK_ID = "larkPerTaskChatTaskId";
4368
+ var CTX_FIELD_CHAT_CREATED_AT2 = "larkPerTaskChatCreatedAt";
4369
+ var CTX_FIELD_CHAT_TASK_ID2 = "larkPerTaskChatTaskId";
2786
4370
 
2787
4371
  class PerTaskGroupChatPlugin {
2788
4372
  name;
@@ -2818,7 +4402,7 @@ class PerTaskGroupChatPlugin {
2818
4402
  resolveToTriggerChatFallback: config.resolveToTriggerChatFallback ?? false,
2819
4403
  cardInitialMessage: config.cardInitialMessage !== false,
2820
4404
  autoCreateGroup: config.autoCreateGroup === true,
2821
- cardNotifier: config.cardNotifier ?? new LarkCardPatchNotifier
4405
+ cardNotifier: config.cardNotifier ?? getSharedLarkCardNotifier()
2822
4406
  };
2823
4407
  this.cardNotifier = this.config.cardNotifier;
2824
4408
  this.name = this.config.pluginName;
@@ -2830,7 +4414,7 @@ class PerTaskGroupChatPlugin {
2830
4414
  const renderedName = this.renderTemplate(this.config.nameTemplate, task);
2831
4415
  const existingChatId = this.getTaskChatId(task);
2832
4416
  if (existingChatId) {
2833
- logger8.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 已有专属群 ${existingChatId} → 复用(不重复建群)`);
4417
+ logger10.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 已有专属群 ${existingChatId} → 复用(不重复建群)`);
2834
4418
  return {
2835
4419
  chatId: existingChatId,
2836
4420
  profile: this.parseProfileFromContext(task.context),
@@ -2839,7 +4423,7 @@ class PerTaskGroupChatPlugin {
2839
4423
  }
2840
4424
  const inflight = this.groupChatInflight.get(task.id);
2841
4425
  if (inflight) {
2842
- logger8.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 建群已在进行中 → 复用同一 promise`);
4426
+ logger10.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 建群已在进行中 → 复用同一 promise`);
2843
4427
  return inflight;
2844
4428
  }
2845
4429
  const pending = this.doEnsureGroupChat(task, renderedName, replyChannelOverride);
@@ -2853,7 +4437,7 @@ class PerTaskGroupChatPlugin {
2853
4437
  async doEnsureGroupChat(task, name, replyChannelOverride) {
2854
4438
  const replyChannel = replyChannelOverride ?? getEnvContextOrEmpty().replyChannel;
2855
4439
  if (!replyChannel?.chatId && this.config.requireReplyChannel) {
2856
- logger8.warn(`ensureGroupChat[${this.pluginName}]: no replyChannel.chatId for task #${task.id} ` + `(requireReplyChannel=true) → 无法建群`);
4440
+ logger10.warn(`ensureGroupChat[${this.pluginName}]: no replyChannel.chatId for task #${task.id} ` + `(requireReplyChannel=true) → 无法建群`);
2857
4441
  return;
2858
4442
  }
2859
4443
  const description = this.renderTemplate(this.config.descriptionTemplate, task);
@@ -2871,30 +4455,30 @@ class PerTaskGroupChatPlugin {
2871
4455
  await this.writeChatBinding(task, chatId, profile, "creating");
2872
4456
  await this.attachChatIdToTask(task, chatId, profile);
2873
4457
  await this.writeChatBinding(task, chatId, profile, "ready");
2874
- logger8.info(`ensureGroupChat[${this.pluginName}]: created group ${chatId} for task #${task.id}`);
4458
+ logger10.info(`ensureGroupChat[${this.pluginName}]: created group ${chatId} for task #${task.id}`);
2875
4459
  return { chatId, profile, name };
2876
4460
  } catch (error) {
2877
- logger8.error(`ensureGroupChat[${this.pluginName}]: failed for task #${task.id}: ${error}`);
4461
+ logger10.error(`ensureGroupChat[${this.pluginName}]: failed for task #${task.id}: ${error}`);
2878
4462
  return;
2879
4463
  }
2880
4464
  }
2881
4465
  setTaskUpdater(updater) {
2882
4466
  this.taskUpdater = updater;
2883
- logger8.debug(`TaskUpdater injected for plugin ${this.name} (sourceId=${this.sourceId})`);
4467
+ logger10.debug(`TaskUpdater injected for plugin ${this.name} (sourceId=${this.sourceId})`);
2884
4468
  }
2885
4469
  hasTaskUpdater() {
2886
4470
  return this.taskUpdater !== undefined;
2887
4471
  }
2888
4472
  setTaskGetter(getter) {
2889
4473
  this.taskGetter = getter;
2890
- logger8.debug(`TaskGetter injected for plugin ${this.name} (sourceId=${this.sourceId})`);
4474
+ logger10.debug(`TaskGetter injected for plugin ${this.name} (sourceId=${this.sourceId})`);
2891
4475
  }
2892
4476
  hasTaskGetter() {
2893
4477
  return this.taskGetter !== undefined;
2894
4478
  }
2895
4479
  setSessionComponent(sessionComponent) {
2896
4480
  this.sessionComponent = sessionComponent;
2897
- logger8.debug(`SessionComponent injected for plugin ${this.name} (sourceId=${this.sourceId})`);
4481
+ logger10.debug(`SessionComponent injected for plugin ${this.name} (sourceId=${this.sourceId})`);
2898
4482
  }
2899
4483
  hasSessionComponent() {
2900
4484
  return this.sessionComponent !== undefined;
@@ -2907,7 +4491,7 @@ class PerTaskGroupChatPlugin {
2907
4491
  if (process.env.ROY_HOOK_DEBUG === "1") {
2908
4492
  console.error(`[DEBUG per-task-group-chat.execute] → SKIP (config.enabled=false)`);
2909
4493
  }
2910
- logger8.debug(`execute: plugin disabled (pluginName=${this.pluginName})`);
4494
+ logger10.debug(`execute: plugin disabled (pluginName=${this.pluginName})`);
2911
4495
  return;
2912
4496
  }
2913
4497
  if (this.pluginName && this.sourceId) {
@@ -2920,13 +4504,13 @@ class PerTaskGroupChatPlugin {
2920
4504
  if (process.env.ROY_HOOK_DEBUG === "1") {
2921
4505
  console.error(`[DEBUG per-task-group-chat.execute] → SKIP (plugin NOT enabled for ${key})`);
2922
4506
  }
2923
- logger8.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`);
4507
+ logger10.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`);
2924
4508
  return;
2925
4509
  }
2926
4510
  const enabledState = env.pluginEnabled ? env.pluginEnabled[key] === true ? "✅ enabled" : "❌ NOT enabled" : "⚠️ env.pluginEnabled 未设置 (env 没创建 / 插件未注册到 eventsource)";
2927
- logger8.debug(`execute[${this.pluginName}]: ${enabledState} (key=${key}, ` + `hookPoint=${ctx.hookPoint}, taskUpdaterInjected=${this.hasTaskUpdater()})`);
4511
+ logger10.debug(`execute[${this.pluginName}]: ${enabledState} (key=${key}, ` + `hookPoint=${ctx.hookPoint}, taskUpdaterInjected=${this.hasTaskUpdater()})`);
2928
4512
  } else {
2929
- logger8.debug(`execute[${this.pluginName}]: missing pluginName/sourceId metadata ` + `(pluginName=${this.pluginName}, sourceId=${this.sourceId}), ` + `skipping pluginEnabled check. hookPoint=${ctx.hookPoint}`);
4513
+ logger10.debug(`execute[${this.pluginName}]: missing pluginName/sourceId metadata ` + `(pluginName=${this.pluginName}, sourceId=${this.sourceId}), ` + `skipping pluginEnabled check. hookPoint=${ctx.hookPoint}`);
2930
4514
  }
2931
4515
  if (process.env.ROY_HOOK_DEBUG === "1") {
2932
4516
  console.error(`[DEBUG per-task-group-chat.execute] → PASSED pluginEnabled check, will dispatch hookPoint=${ctx.hookPoint}`);
@@ -2945,13 +4529,13 @@ class PerTaskGroupChatPlugin {
2945
4529
  await this.onAfterComplete(ctx);
2946
4530
  break;
2947
4531
  default:
2948
- logger8.debug(`execute: unhandled hookPoint=${ctx.hookPoint}, skip`);
4532
+ logger10.debug(`execute: unhandled hookPoint=${ctx.hookPoint}, skip`);
2949
4533
  }
2950
4534
  }
2951
4535
  async onAfterCreate(ctx) {
2952
4536
  const data = ctx.data;
2953
4537
  if (!data?.task) {
2954
- logger8.debug("onAfterCreate: task is missing, skip");
4538
+ logger10.debug("onAfterCreate: task is missing, skip");
2955
4539
  return;
2956
4540
  }
2957
4541
  const { task } = data;
@@ -2962,38 +4546,38 @@ class PerTaskGroupChatPlugin {
2962
4546
  console.error(`[DEBUG per-task-group-chat.onAfterCreate] env.messageId=${env.messageId}`);
2963
4547
  console.error(`[DEBUG per-task-group-chat.onAfterCreate] env.sourceId=${env.sourceId}`);
2964
4548
  }
2965
- logger8.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}`);
4549
+ logger10.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}`);
2966
4550
  if (!this.config.autoCreateGroup) {
2967
- logger8.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} autoCreateGroup=false ` + `→ skip group chat creation(按需建群:用户在卡片顶部按钮触发)`);
4551
+ logger10.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} autoCreateGroup=false ` + `→ skip group chat creation(按需建群:用户在卡片顶部按钮触发)`);
2968
4552
  return;
2969
4553
  }
2970
4554
  const replyChannel = this.getReplyChannelFromCtx(ctx);
2971
4555
  if (!replyChannel?.chatId && this.config.requireReplyChannel) {
2972
- logger8.debug(`onAfterCreate[${this.pluginName}]: no replyChannel.chatId for task #${task.id}, skip. ` + `修复:eventsource 收到 im.message.receive_v1 事件时 replyChannel 应有值;` + `或在 plugin config 设 requireReplyChannel=false 强制创建群聊(profile 降级到 fallbackProfile/无)`);
4556
+ logger10.debug(`onAfterCreate[${this.pluginName}]: no replyChannel.chatId for task #${task.id}, skip. ` + `修复:eventsource 收到 im.message.receive_v1 事件时 replyChannel 应有值;` + `或在 plugin config 设 requireReplyChannel=false 强制创建群聊(profile 降级到 fallbackProfile/无)`);
2973
4557
  return;
2974
4558
  }
2975
4559
  if (!replyChannel?.chatId && !this.config.requireReplyChannel) {
2976
- logger8.debug(`onAfterCreate[${this.pluginName}]: no replyChannel but requireReplyChannel=false, ` + `proceeding with profile fallback (fallbackProfile="${this.config.fallbackProfile}")`);
4560
+ logger10.debug(`onAfterCreate[${this.pluginName}]: no replyChannel but requireReplyChannel=false, ` + `proceeding with profile fallback (fallbackProfile="${this.config.fallbackProfile}")`);
2977
4561
  }
2978
4562
  if (this.isSubTask(task) && !this.config.disableSubTaskSkip) {
2979
- logger8.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task, ` + `skip group chat creation. 关闭方式:config.disableSubTaskSkip=true`);
4563
+ logger10.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task, ` + `skip group chat creation. 关闭方式:config.disableSubTaskSkip=true`);
2980
4564
  return;
2981
4565
  }
2982
4566
  if (this.isSubTask(task) && this.config.disableSubTaskSkip) {
2983
- logger8.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task but ` + `disableSubTaskSkip=true, proceeding to create group chat`);
4567
+ logger10.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task but ` + `disableSubTaskSkip=true, proceeding to create group chat`);
2984
4568
  }
2985
4569
  try {
2986
4570
  const created = await this.ensureGroupChat(task, replyChannel);
2987
4571
  if (!created?.chatId) {
2988
- logger8.error(`onAfterCreate[${this.pluginName}]: ensureGroupChat 未返回 chatId for task #${task.id}`);
4572
+ logger10.error(`onAfterCreate[${this.pluginName}]: ensureGroupChat 未返回 chatId for task #${task.id}`);
2989
4573
  if (!this.config.fallbackToMain) {
2990
4574
  throw new Error(`per-task group chat unavailable for task #${task.id}`);
2991
4575
  }
2992
4576
  return;
2993
4577
  }
2994
- logger8.info(`onAfterCreate[${this.pluginName}]: task #${task.id} → group ${created.chatId} ` + `(initial card owned by LarkCliTaskNotifyHook)`);
4578
+ logger10.info(`onAfterCreate[${this.pluginName}]: task #${task.id} → group ${created.chatId} ` + `(initial card owned by LarkCliTaskNotifyHook)`);
2995
4579
  } catch (error) {
2996
- logger8.error(`onAfterCreate[${this.pluginName}]: group chat failed for task #${task.id}: ${error}`);
4580
+ logger10.error(`onAfterCreate[${this.pluginName}]: group chat failed for task #${task.id}: ${error}`);
2997
4581
  if (!this.config.fallbackToMain)
2998
4582
  throw error;
2999
4583
  }
@@ -3001,7 +4585,7 @@ class PerTaskGroupChatPlugin {
3001
4585
  async writeChatBinding(task, chatId, profile, status, errorMessage) {
3002
4586
  if (!this.sessionComponent) {
3003
4587
  if (isPluginDebugEnabled()) {
3004
- logger8.debug(`writeChatBinding[${this.pluginName}]: no sessionComponent injected, ` + `skip chat-task binding for chat ${chatId} (status=${status})`);
4588
+ logger10.debug(`writeChatBinding[${this.pluginName}]: no sessionComponent injected, ` + `skip chat-task binding for chat ${chatId} (status=${status})`);
3005
4589
  }
3006
4590
  return;
3007
4591
  }
@@ -3014,37 +4598,37 @@ class PerTaskGroupChatPlugin {
3014
4598
  errorMessage: errorMessage ?? null
3015
4599
  });
3016
4600
  } catch (bindErr) {
3017
- logger8.warn(`writeChatBinding[${this.pluginName}]: failed to write chat-task binding for ` + `chat ${chatId} / task #${task.id} (status=${status}): ${bindErr}. ` + `群消息 handler 将无法自动注入 parentTaskId。`);
4601
+ logger10.warn(`writeChatBinding[${this.pluginName}]: failed to write chat-task binding for ` + `chat ${chatId} / task #${task.id} (status=${status}): ${bindErr}. ` + `群消息 handler 将无法自动注入 parentTaskId。`);
3018
4602
  }
3019
4603
  }
3020
4604
  async onAfterUpdate(ctx) {
3021
4605
  const data = ctx.data;
3022
4606
  if (!data?.task) {
3023
- logger8.debug("onAfterUpdate: task is missing, skip");
4607
+ logger10.debug("onAfterUpdate: task is missing, skip");
3024
4608
  return;
3025
4609
  }
3026
- logger8.debug(`onAfterUpdate[${this.pluginName}]: task #${data.task.id} 卡片由 ` + `LarkCliTaskNotifyHook 负责(本插件不再 patch),skip`);
4610
+ logger10.debug(`onAfterUpdate[${this.pluginName}]: task #${data.task.id} 卡片由 ` + `LarkCliTaskNotifyHook 负责(本插件不再 patch),skip`);
3027
4611
  }
3028
4612
  async onAfterOperationCreate(ctx) {
3029
4613
  const data = ctx.data;
3030
4614
  if (!data?.operation) {
3031
- logger8.debug("onAfterOperationCreate: operation is missing, skip");
4615
+ logger10.debug("onAfterOperationCreate: operation is missing, skip");
3032
4616
  return;
3033
4617
  }
3034
- logger8.debug(`onAfterOperationCreate[${this.pluginName}]: operation #${data.operation.id} ` + `卡片由 LarkCliTaskNotifyHook 负责,skip`);
4618
+ logger10.debug(`onAfterOperationCreate[${this.pluginName}]: operation #${data.operation.id} ` + `卡片由 LarkCliTaskNotifyHook 负责,skip`);
3035
4619
  }
3036
4620
  async onAfterComplete(ctx) {
3037
4621
  const data = ctx.data;
3038
4622
  if (!data?.task) {
3039
- logger8.debug("onAfterComplete: task is missing, skip");
4623
+ logger10.debug("onAfterComplete: task is missing, skip");
3040
4624
  return;
3041
4625
  }
3042
- logger8.debug(`onAfterComplete[${this.pluginName}]: task #${data.task.id} 终态卡片由 ` + `LarkCliTaskNotifyHook 负责,skip`);
4626
+ logger10.debug(`onAfterComplete[${this.pluginName}]: task #${data.task.id} 终态卡片由 ` + `LarkCliTaskNotifyHook 负责,skip`);
3043
4627
  }
3044
4628
  createGroupChat(opts) {
3045
4629
  const args = this.buildCreateGroupChatArgs(opts);
3046
4630
  if (isPluginDebugEnabled()) {
3047
- logger8.debug(`Creating group chat: lark-cli ${args.join(" ")}`);
4631
+ logger10.debug(`Creating group chat: lark-cli ${args.join(" ")}`);
3048
4632
  }
3049
4633
  return new Promise((resolve, reject) => {
3050
4634
  const proc = spawn("lark-cli", args, {
@@ -3113,22 +4697,22 @@ class PerTaskGroupChatPlugin {
3113
4697
  } catch {
3114
4698
  parsed = {};
3115
4699
  }
3116
- parsed[CTX_FIELD_CHAT_ID2] = chatId;
4700
+ parsed[CTX_FIELD_CHAT_ID3] = chatId;
3117
4701
  parsed[CTX_FIELD_CHAT_PROFILE2] = profile;
3118
- parsed[CTX_FIELD_CHAT_CREATED_AT] = new Date().toISOString();
3119
- parsed[CTX_FIELD_CHAT_TASK_ID] = task.id;
4702
+ parsed[CTX_FIELD_CHAT_CREATED_AT2] = new Date().toISOString();
4703
+ parsed[CTX_FIELD_CHAT_TASK_ID2] = task.id;
3120
4704
  const newContext = JSON.stringify(parsed);
3121
4705
  if (!this.taskUpdater) {
3122
- logger8.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。`);
4706
+ logger10.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。`);
3123
4707
  task.context = newContext;
3124
4708
  return;
3125
4709
  }
3126
4710
  try {
3127
4711
  await this.taskUpdater(task.id, newContext);
3128
4712
  task.context = newContext;
3129
- logger8.debug(`attachChatIdToTask[${this.pluginName}]: ✅ persisted chat_id ${chatId} ` + `to task #${task.id} context`);
4713
+ logger10.debug(`attachChatIdToTask[${this.pluginName}]: ✅ persisted chat_id ${chatId} ` + `to task #${task.id} context`);
3130
4714
  } catch (error) {
3131
- logger8.error(`Failed to persist chat_id to task #${task.id}: ${error}`);
4715
+ logger10.error(`Failed to persist chat_id to task #${task.id}: ${error}`);
3132
4716
  task.context = newContext;
3133
4717
  }
3134
4718
  }
@@ -3155,7 +4739,7 @@ class PerTaskGroupChatPlugin {
3155
4739
  return;
3156
4740
  try {
3157
4741
  const obj = JSON.parse(ctxStr);
3158
- const id = obj[CTX_FIELD_CHAT_ID2];
4742
+ const id = obj[CTX_FIELD_CHAT_ID3];
3159
4743
  return typeof id === "string" ? id : undefined;
3160
4744
  } catch {
3161
4745
  return;
@@ -3223,15 +4807,15 @@ class PerTaskGroupChatPlugin {
3223
4807
  }
3224
4808
  resolveBotAppIds(replyChannel) {
3225
4809
  if (this.config.botAppIds.length > 0) {
3226
- logger8.debug(`resolveBotAppIds[${this.pluginName}]: using config.botAppIds=${JSON.stringify(this.config.botAppIds)}`);
4810
+ logger10.debug(`resolveBotAppIds[${this.pluginName}]: using config.botAppIds=${JSON.stringify(this.config.botAppIds)}`);
3227
4811
  return this.config.botAppIds;
3228
4812
  }
3229
4813
  const appId = replyChannel ? replyChannel.appId : undefined;
3230
4814
  if (appId) {
3231
- logger8.debug(`resolveBotAppIds[${this.pluginName}]: using replyChannel.appId=${appId} (auto-detected from event header.app_id)`);
4815
+ logger10.debug(`resolveBotAppIds[${this.pluginName}]: using replyChannel.appId=${appId} (auto-detected from event header.app_id)`);
3232
4816
  return [appId];
3233
4817
  }
3234
- logger8.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。`);
4818
+ logger10.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。`);
3235
4819
  return [];
3236
4820
  }
3237
4821
  resolveUserIds(replyChannel) {
@@ -3241,7 +4825,7 @@ class PerTaskGroupChatPlugin {
3241
4825
  if (id && id !== "sender" && id.startsWith("ou_")) {
3242
4826
  ids.add(id);
3243
4827
  } else if (id && id !== "sender") {
3244
- logger8.debug(`resolveUserIds[${this.pluginName}]: skip non-open_id in defaultInviteUserIds: "${id}"`);
4828
+ logger10.debug(`resolveUserIds[${this.pluginName}]: skip non-open_id in defaultInviteUserIds: "${id}"`);
3245
4829
  }
3246
4830
  }
3247
4831
  }
@@ -3264,7 +4848,7 @@ class PerTaskGroupChatPlugin {
3264
4848
  }
3265
4849
  }
3266
4850
  if (isPluginDebugEnabled()) {
3267
- logger8.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))}`);
4851
+ logger10.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))}`);
3268
4852
  }
3269
4853
  return Array.from(ids);
3270
4854
  }
@@ -3417,4 +5001,4 @@ __legacyDecorateClassTS([
3417
5001
  function createPerTaskGroupChatPlugin(config = {}) {
3418
5002
  return new PerTaskGroupChatPlugin(config);
3419
5003
  }
3420
- export { createAutoTaskPlugin, LAYA_REUSE_THRESHOLD, LayaDecisionPlugin, createLayaDecisionPlugin, LAYA_COMPLETION_THRESHOLD, buildCompletionHint, renderCompletionState, buildCompletionQuestion, LayaCompletionPlugin, createLayaCompletionPlugin, registerLayaCompletionTaskHook, WORKFLOW_EXTRACTOR_PROMPT, formatExtractorInput, formatExtractorInputFromDescription, parseExtractorOutput, TaskTagPlugin, MemoryLarkCardStateStore, SqliteLarkCardStateStore, getDefaultLarkCardDbPath, createLarkCardStateStore, extractPerTaskChatId, extractPerTaskChatName, NotifyType, LarkCliTaskNotifyHook, createLarkCliTaskNotifyHook, PerTaskGroupChatPlugin, createPerTaskGroupChatPlugin };
5004
+ export { createAutoTaskPlugin, LAYA_REUSE_THRESHOLD, LayaDecisionPlugin, createLayaDecisionPlugin, LAYA_COMPLETION_THRESHOLD, buildCompletionHint, renderCompletionState, buildCompletionQuestion, LayaCompletionPlugin, createLayaCompletionPlugin, registerLayaCompletionTaskHook, WORKFLOW_EXTRACTOR_PROMPT, formatExtractorInput, formatExtractorInputFromDescription, parseExtractorOutput, TaskTagPlugin, handleCardAction, deleteGroupChatViaLarkCli, createGroupChatViaLarkCli, NotifyType, LarkCliTaskNotifyHook, createLarkCliTaskNotifyHook, MemoryWecomCardStateStore, createMemoryWecomCardStore, SqliteWecomCardStateStore, getDefaultWecomCardDbPath, createWecomCardStateStore, WecomNotifyType, extractWecomFields, buildWecomTaskCard, WecomTaskNotifyHook, createWecomTaskNotifyHook, PerTaskGroupChatPlugin, createPerTaskGroupChatPlugin };