@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.
- package/dist/config/config-protection.d.ts +89 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +23 -7
- package/dist/env/agent/agent-component.d.ts +43 -22
- package/dist/env/agent/agent-component.js +13 -12
- package/dist/env/agent/index.js +13 -12
- package/dist/env/agent/types.d.ts +46 -12
- package/dist/env/commands/index.js +7 -7
- package/dist/env/context/index.js +3 -3
- package/dist/env/debug/index.js +7 -7
- package/dist/env/error-reply.d.ts +6 -0
- package/dist/env/error-reply.js +1 -1
- package/dist/env/event-source/bounty-im-handler.d.ts +8 -0
- package/dist/env/event-source/es-command.d.ts +39 -0
- package/dist/env/event-source/es-plugins.d.ts +68 -0
- package/dist/env/event-source/event-source-component.d.ts +15 -0
- package/dist/env/event-source/event-source-handlers.d.ts +84 -0
- package/dist/env/event-source/event-source-handlers.js +40 -23
- package/dist/env/event-source/event-store.js +2 -2
- package/dist/env/event-source/index.d.ts +5 -1
- package/dist/env/event-source/index.js +75 -23
- package/dist/env/event-source/timer-handler.d.ts +98 -0
- package/dist/env/event-source/types.d.ts +154 -2
- package/dist/env/event-source/wecom-card-action-handler.d.ts +373 -0
- package/dist/env/event-source/wecom-card-builder.d.ts +426 -0
- package/dist/env/event-source/wecom-cli-handler.d.ts +291 -0
- package/dist/env/event-source/wecom-cli-sender.d.ts +88 -0
- package/dist/env/event-source/wecom-media.d.ts +130 -0
- package/dist/env/event-source/wecom-respond-registry.d.ts +151 -0
- package/dist/env/event-source/wecom-respond-registry.js +33 -0
- package/dist/env/event-source/wecom-session-card-sender.d.ts +135 -0
- package/dist/env/event-source/wecom-session-card-sender.js +153 -0
- package/dist/env/event-source/wecom-ws-bridge.d.ts +213 -0
- package/dist/env/hook/index.js +5 -5
- package/dist/env/index.js +51 -45
- package/dist/env/llm/index.js +9 -8
- package/dist/env/log-trace/index.js +7 -7
- package/dist/env/log-trace/trace-filters.d.ts +6 -1
- package/dist/env/mcp/index.js +7 -7
- package/dist/env/memory/index.js +7 -7
- package/dist/env/plugin/index.js +7 -7
- package/dist/env/prompt/index.js +8 -8
- package/dist/env/prompt/prompts-index.d.ts +1 -1
- package/dist/env/session/group-session-link.d.ts +99 -0
- package/dist/env/session/index.js +11 -10
- package/dist/env/session/session-component.d.ts +89 -0
- package/dist/env/session/storage/index.js +5 -4
- package/dist/env/session/types.d.ts +10 -0
- package/dist/env/skill/index.js +7 -7
- package/dist/env/task/delegate/delegate-channel-hint.d.ts +39 -0
- package/dist/env/task/delegate/delegate-tool.d.ts +132 -7
- package/dist/env/task/delegate/delegate-tool.js +13 -9
- package/dist/env/task/delegate/index.js +8 -8
- package/dist/env/task/index.js +18 -18
- package/dist/env/task/mcp/index.js +4 -4
- package/dist/env/task/plugins/card-subject.d.ts +117 -0
- package/dist/env/task/plugins/index.d.ts +2 -0
- package/dist/env/task/plugins/index.js +39 -16
- package/dist/env/task/plugins/lark-card-action-handler.d.ts +261 -6
- package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +542 -2
- package/dist/env/task/plugins/lark-card-patch-notifier.js +12 -2
- package/dist/env/task/plugins/lark-card-state-store.d.ts +60 -0
- package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +61 -0
- package/dist/env/task/plugins/sub-session-activity.d.ts +415 -0
- package/dist/env/task/plugins/wecom-card-state-store.d.ts +121 -0
- package/dist/env/task/plugins/wecom-session-card-store.d.ts +134 -0
- package/dist/env/task/plugins/wecom-session-card-store.js +23 -0
- package/dist/env/task/plugins/wecom-task-notify-plugin.d.ts +264 -0
- package/dist/env/task/storage/index.js +3 -3
- package/dist/env/task/tools/index.js +4 -4
- package/dist/env/tool/built-in/index.d.ts +2 -0
- package/dist/env/tool/built-in/index.js +9 -2
- package/dist/env/tool/index.js +14 -9
- package/dist/env/tool/types.d.ts +25 -0
- package/dist/env/tool/validator.d.ts +8 -0
- package/dist/env/workflow/engine/index.js +12 -12
- package/dist/env/workflow/hooks/index.js +7 -7
- package/dist/env/workflow/index.js +24 -23
- package/dist/env/workflow/nodes/index.js +2 -2
- package/dist/env/workflow/plugins/index.js +10 -8
- package/dist/env/workflow/service/index.js +2 -2
- package/dist/env/workflow/storage/index.js +3 -3
- package/dist/env/workflow/tools/index.js +16 -15
- package/dist/env/workflow/utils/index.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +118 -62
- package/dist/shared/@ai-setting/{roy-agent-core-dc9zq75z.js → roy-agent-core-0crmsc12.js} +3 -3
- package/dist/shared/@ai-setting/{roy-agent-core-aa2j7bk9.js → roy-agent-core-0sbmdz8d.js} +171 -36
- package/dist/shared/@ai-setting/{roy-agent-core-87942bw4.js → roy-agent-core-1mhyh4c5.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-v6vr6za7.js → roy-agent-core-1vv4fx15.js} +4 -4
- package/dist/shared/@ai-setting/roy-agent-core-2stt4rp5.js +134 -0
- package/dist/shared/@ai-setting/{roy-agent-core-nwbfepba.js → roy-agent-core-3e040tc5.js} +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-418zc9ha.js +288 -0
- package/dist/shared/@ai-setting/{roy-agent-core-r5wt1h3c.js → roy-agent-core-4fkxy2gv.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-na18zwj0.js → roy-agent-core-4t3yskp8.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-5pr4zp88.js → roy-agent-core-5kzhaap1.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-qq96p0tj.js → roy-agent-core-5s2t8z00.js} +22 -1
- package/dist/shared/@ai-setting/{roy-agent-core-6rh8k6ba.js → roy-agent-core-6gj1hc5z.js} +7 -7
- package/dist/shared/@ai-setting/{roy-agent-core-ezvpreqz.js → roy-agent-core-6k8mb7zx.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-g8htfwxj.js → roy-agent-core-7f10p7m2.js} +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-7kvpc0w5.js +177 -0
- package/dist/shared/@ai-setting/{roy-agent-core-h21gngxf.js → roy-agent-core-7yh5wps0.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-efkjyh69.js → roy-agent-core-9710zwwz.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-6cy43nss.js → roy-agent-core-9c565frx.js} +49 -9
- package/dist/shared/@ai-setting/{roy-agent-core-fghc2m8z.js → roy-agent-core-aa8kv18f.js} +34 -2
- package/dist/shared/@ai-setting/{roy-agent-core-ew6qb5yx.js → roy-agent-core-an35dzbf.js} +6 -6
- package/dist/shared/@ai-setting/{roy-agent-core-mww0mnd9.js → roy-agent-core-azyz78ky.js} +7 -7
- package/dist/shared/@ai-setting/{roy-agent-core-264qexac.js → roy-agent-core-b7h39vrc.js} +4 -4
- package/dist/shared/@ai-setting/{roy-agent-core-dqxec05w.js → roy-agent-core-bjtc9d35.js} +2 -2
- package/dist/shared/@ai-setting/roy-agent-core-c4ec7k0p.js +426 -0
- package/dist/shared/@ai-setting/{roy-agent-core-ddyk1rc0.js → roy-agent-core-cre2y2ct.js} +160 -4
- package/dist/shared/@ai-setting/{roy-agent-core-tr3ep81v.js → roy-agent-core-d6zdbgtc.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-0qyrk5k0.js → roy-agent-core-ds6qzmf8.js} +14 -5
- package/dist/shared/@ai-setting/{roy-agent-core-cgtq6427.js → roy-agent-core-e08bvpaq.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-12n08pg5.js → roy-agent-core-e69qxdyy.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-rhtdspx6.js → roy-agent-core-fan8g0z1.js} +10 -10
- package/dist/shared/@ai-setting/{roy-agent-core-xcp8vwax.js → roy-agent-core-fepm835e.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-6j02sbee.js → roy-agent-core-fhg03v43.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-99nwgj84.js → roy-agent-core-fzw9xzm1.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-dvtg4sn0.js → roy-agent-core-g8yr733d.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-3gpygqcx.js → roy-agent-core-gr5h1dyr.js} +2753 -358
- package/dist/shared/@ai-setting/roy-agent-core-h1e1fm9b.js +380 -0
- package/dist/shared/@ai-setting/{roy-agent-core-c2gbt68e.js → roy-agent-core-h83zdsrq.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-8ez6mccb.js → roy-agent-core-k3gcvybs.js} +5 -1
- package/dist/shared/@ai-setting/{roy-agent-core-pgvcf91v.js → roy-agent-core-m39v7qpx.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-tm1mxttk.js → roy-agent-core-mf5p38yf.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-83bewqx8.js → roy-agent-core-mvh0td7p.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-v85n42ve.js → roy-agent-core-mx855td4.js} +17 -38
- package/dist/shared/@ai-setting/{roy-agent-core-v87njmq7.js → roy-agent-core-n197medr.js} +13 -13
- package/dist/shared/@ai-setting/{roy-agent-core-cmprygrp.js → roy-agent-core-nqkfkpf4.js} +4 -4
- package/dist/shared/@ai-setting/{roy-agent-core-7cbpmk4q.js → roy-agent-core-p31ftgq9.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-vffccgf6.js → roy-agent-core-qwd2dq5q.js} +10 -3
- package/dist/shared/@ai-setting/{roy-agent-core-rsn44n9y.js → roy-agent-core-ra66sf50.js} +2526 -942
- package/dist/shared/@ai-setting/{roy-agent-core-1v4g0d4x.js → roy-agent-core-rm7seqwb.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-w0qjeq2v.js → roy-agent-core-shb1qsyg.js} +1041 -18
- package/dist/shared/@ai-setting/{roy-agent-core-ax9e9szb.js → roy-agent-core-smf94h73.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-acj8ag08.js → roy-agent-core-tjnabyvq.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-qqaak24r.js → roy-agent-core-tkpp1ba0.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-9vhq7fvv.js → roy-agent-core-tpm1dd1g.js} +34 -88
- package/dist/shared/@ai-setting/{roy-agent-core-ed233k5z.js → roy-agent-core-v1n0106v.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-zxz5fkwj.js → roy-agent-core-v6yshv0z.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-mcy5apay.js → roy-agent-core-vf9sp6s3.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-q813vafd.js → roy-agent-core-vpcxv1ty.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-bxkjk7zx.js → roy-agent-core-vx8zc847.js} +4 -1
- package/dist/shared/@ai-setting/{roy-agent-core-xvt7kf7p.js → roy-agent-core-w9ryeg2p.js} +3 -3
- package/dist/shared/@ai-setting/{roy-agent-core-es203w7p.js → roy-agent-core-wqs6yypq.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-hd3n57zs.js → roy-agent-core-wr1qexzq.js} +5 -5
- package/dist/shared/@ai-setting/roy-agent-core-wxrkcb17.js +56 -0
- package/dist/shared/@ai-setting/{roy-agent-core-0snm5hc5.js → roy-agent-core-y6shd91r.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-9vxh6f8h.js → roy-agent-core-y7fqtzk2.js} +7 -7
- package/dist/shared/@ai-setting/{roy-agent-core-cpcd24n2.js → roy-agent-core-yet36q5k.js} +2 -2
- package/dist/shared/@ai-setting/{roy-agent-core-3fh1f7s7.js → roy-agent-core-yfpnkc4z.js} +5 -5
- package/dist/shared/@ai-setting/{roy-agent-core-0v27y99n.js → roy-agent-core-yh68z1bg.js} +65 -4
- package/package.json +18 -1
- /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-
|
|
12
|
+
} from "./roy-agent-core-4t3yskp8.js";
|
|
4
13
|
import {
|
|
5
14
|
WORKFLOW_EXTRACT_PROMPT
|
|
6
|
-
} from "./roy-agent-core-
|
|
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-
|
|
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-
|
|
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-
|
|
46
|
+
} from "./roy-agent-core-rm7seqwb.js";
|
|
31
47
|
import {
|
|
32
48
|
enqueueToAddFakeUserMessage
|
|
33
|
-
} from "./roy-agent-core-
|
|
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-
|
|
55
|
+
} from "./roy-agent-core-tkpp1ba0.js";
|
|
40
56
|
import {
|
|
41
|
-
getEnvContextOrEmpty
|
|
42
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1696
|
+
// src/env/task/plugins/lark-card-action-handler.ts
|
|
1680
1697
|
init_logger();
|
|
1681
|
-
|
|
1682
|
-
import {
|
|
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/
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
|
1944
|
-
if (!
|
|
1848
|
+
function extractTargetSessionId(context) {
|
|
1849
|
+
if (!context)
|
|
1945
1850
|
return;
|
|
1946
1851
|
try {
|
|
1947
|
-
const
|
|
1948
|
-
const
|
|
1949
|
-
|
|
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
|
|
1955
|
-
|
|
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
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
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
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
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
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
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
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
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
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
await this.sendCardStart(task);
|
|
2102
|
+
if (!existing?.chatId) {
|
|
2103
|
+
return {
|
|
2104
|
+
ok: false,
|
|
2105
|
+
action,
|
|
2106
|
+
error: `会话 ${sessionId} 没有专属群,无需解散`
|
|
2107
|
+
};
|
|
2062
2108
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
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
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
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
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
2219
|
-
const
|
|
2220
|
-
|
|
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
|
-
|
|
2244
|
-
|
|
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
|
-
|
|
2251
|
-
|
|
3621
|
+
markPaused(taskId) {
|
|
3622
|
+
const r = this.records.get(taskId);
|
|
3623
|
+
if (!r)
|
|
2252
3624
|
return;
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
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
|
-
|
|
2272
|
-
|
|
3636
|
+
markClosed(taskId) {
|
|
3637
|
+
const r = this.records.get(taskId);
|
|
3638
|
+
if (!r)
|
|
2273
3639
|
return;
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
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
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
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
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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
|
-
|
|
2295
|
-
|
|
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
|
-
|
|
2298
|
-
|
|
2299
|
-
this.
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
-
|
|
2305
|
-
|
|
3927
|
+
const fields = extractWecomFields(env, this.sourceId);
|
|
3928
|
+
if (!fields.isWecom) {
|
|
3929
|
+
logger9.debug("not a wecom event, skip");
|
|
2306
3930
|
return;
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
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
|
-
|
|
2314
|
-
|
|
3950
|
+
async onAfterCreate(ctx) {
|
|
3951
|
+
const { task } = ctx.data;
|
|
3952
|
+
if (!task || !this.shouldNotify(WecomNotifyType.TASK_CREATED))
|
|
2315
3953
|
return;
|
|
2316
|
-
|
|
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
|
-
|
|
2323
|
-
|
|
3956
|
+
async onAfterUpdate(ctx) {
|
|
3957
|
+
const data = ctx.data;
|
|
3958
|
+
if (!data?.task)
|
|
2324
3959
|
return;
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2358
|
-
|
|
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
|
|
2370
|
-
|
|
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
|
-
|
|
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
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
-
|
|
2407
|
-
|
|
4018
|
+
async onAfterOperationCreate(ctx) {
|
|
4019
|
+
const { operation } = ctx.data;
|
|
4020
|
+
if (!operation)
|
|
2408
4021
|
return;
|
|
2409
|
-
|
|
2410
|
-
return
|
|
2411
|
-
|
|
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
|
-
|
|
2416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2487
|
-
|
|
2488
|
-
const
|
|
2489
|
-
if (
|
|
2490
|
-
|
|
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.
|
|
2500
|
-
|
|
2501
|
-
|
|
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
|
-
|
|
2515
|
-
|
|
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
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
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
|
-
|
|
2528
|
-
this.
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
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
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
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
|
-
|
|
2546
|
-
|
|
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
|
-
|
|
2555
|
-
|
|
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
|
-
|
|
2560
|
-
|
|
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
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
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 (
|
|
2572
|
-
|
|
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
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
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
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
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.
|
|
2605
|
-
|
|
2606
|
-
|
|
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
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
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
|
-
|
|
4191
|
+
logger9.error(`deliver threw for task #${input.task.id}: ${err.message}`);
|
|
4192
|
+
return false;
|
|
2619
4193
|
}
|
|
2620
4194
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
return;
|
|
2624
|
-
|
|
2625
|
-
|
|
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
|
-
|
|
2633
|
-
|
|
2634
|
-
const
|
|
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
|
-
|
|
2637
|
-
const
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
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
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
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
|
-
|
|
4251
|
+
return true;
|
|
2657
4252
|
} catch (err) {
|
|
2658
|
-
|
|
4253
|
+
logger9.warn(`deliverTemplateCard threw for task #${input.task.id}: ` + `${err.message}(回落 markdown)`);
|
|
4254
|
+
return false;
|
|
2659
4255
|
}
|
|
2660
4256
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
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
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
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
|
-
|
|
2686
|
-
const
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
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
|
-
|
|
2725
|
-
|
|
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
|
-
|
|
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
|
|
4326
|
+
if (!notifyTypes || notifyTypes.length === 0)
|
|
2744
4327
|
return true;
|
|
2745
|
-
|
|
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
|
-
|
|
2752
|
-
],
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
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
|
|
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
|
|
4366
|
+
var CTX_FIELD_CHAT_ID3 = "larkPerTaskChatId";
|
|
2783
4367
|
var CTX_FIELD_CHAT_PROFILE2 = "larkPerTaskChatProfile";
|
|
2784
|
-
var
|
|
2785
|
-
var
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4458
|
+
logger10.info(`ensureGroupChat[${this.pluginName}]: created group ${chatId} for task #${task.id}`);
|
|
2875
4459
|
return { chatId, profile, name };
|
|
2876
4460
|
} catch (error) {
|
|
2877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4511
|
+
logger10.debug(`execute[${this.pluginName}]: ${enabledState} (key=${key}, ` + `hookPoint=${ctx.hookPoint}, taskUpdaterInjected=${this.hasTaskUpdater()})`);
|
|
2928
4512
|
} else {
|
|
2929
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4578
|
+
logger10.info(`onAfterCreate[${this.pluginName}]: task #${task.id} → group ${created.chatId} ` + `(initial card owned by LarkCliTaskNotifyHook)`);
|
|
2995
4579
|
} catch (error) {
|
|
2996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4607
|
+
logger10.debug("onAfterUpdate: task is missing, skip");
|
|
3024
4608
|
return;
|
|
3025
4609
|
}
|
|
3026
|
-
|
|
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
|
-
|
|
4615
|
+
logger10.debug("onAfterOperationCreate: operation is missing, skip");
|
|
3032
4616
|
return;
|
|
3033
4617
|
}
|
|
3034
|
-
|
|
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
|
-
|
|
4623
|
+
logger10.debug("onAfterComplete: task is missing, skip");
|
|
3040
4624
|
return;
|
|
3041
4625
|
}
|
|
3042
|
-
|
|
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
|
-
|
|
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[
|
|
4700
|
+
parsed[CTX_FIELD_CHAT_ID3] = chatId;
|
|
3117
4701
|
parsed[CTX_FIELD_CHAT_PROFILE2] = profile;
|
|
3118
|
-
parsed[
|
|
3119
|
-
parsed[
|
|
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
|
-
|
|
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
|
-
|
|
4713
|
+
logger10.debug(`attachChatIdToTask[${this.pluginName}]: ✅ persisted chat_id ${chatId} ` + `to task #${task.id} context`);
|
|
3130
4714
|
} catch (error) {
|
|
3131
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
4815
|
+
logger10.debug(`resolveBotAppIds[${this.pluginName}]: using replyChannel.appId=${appId} (auto-detected from event header.app_id)`);
|
|
3232
4816
|
return [appId];
|
|
3233
4817
|
}
|
|
3234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 };
|