@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,49 +1,79 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defaultLarkCliRunner
|
|
3
|
-
} from "./roy-agent-core-
|
|
3
|
+
} from "./roy-agent-core-vx8zc847.js";
|
|
4
4
|
import {
|
|
5
5
|
cliCommandHandler
|
|
6
6
|
} from "./roy-agent-core-v0hf14bj.js";
|
|
7
|
+
import {
|
|
8
|
+
getSharedWecomSessionCardStore
|
|
9
|
+
} from "./roy-agent-core-7kvpc0w5.js";
|
|
10
|
+
import {
|
|
11
|
+
guardConfigWrite
|
|
12
|
+
} from "./roy-agent-core-2stt4rp5.js";
|
|
7
13
|
import {
|
|
8
14
|
TaskTagPlugin,
|
|
15
|
+
createGroupChatViaLarkCli,
|
|
9
16
|
createLarkCliTaskNotifyHook,
|
|
10
17
|
createPerTaskGroupChatPlugin,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
createWecomCardStateStore,
|
|
19
|
+
createWecomTaskNotifyHook,
|
|
20
|
+
deleteGroupChatViaLarkCli,
|
|
21
|
+
handleCardAction
|
|
22
|
+
} from "./roy-agent-core-ra66sf50.js";
|
|
23
|
+
import {
|
|
24
|
+
registerWecomRespondTarget,
|
|
25
|
+
sendWecomCliMarkdown,
|
|
26
|
+
unregisterWecomRespondTarget
|
|
27
|
+
} from "./roy-agent-core-h1e1fm9b.js";
|
|
28
|
+
import {
|
|
29
|
+
ROY_SESSION_PREFIX,
|
|
30
|
+
WECOM_SESSION_TASK_SEP,
|
|
31
|
+
WecomCardActionKey,
|
|
32
|
+
buildWecomCardUpdateFrame,
|
|
33
|
+
buildWecomSessionUpdateFrame
|
|
34
|
+
} from "./roy-agent-core-c4ec7k0p.js";
|
|
14
35
|
import {
|
|
15
36
|
getSharedEventStore
|
|
16
|
-
} from "./roy-agent-core-
|
|
37
|
+
} from "./roy-agent-core-4t3yskp8.js";
|
|
17
38
|
import {
|
|
18
39
|
createLarkCliWorkflowNotifyHook
|
|
19
|
-
} from "./roy-agent-core-
|
|
40
|
+
} from "./roy-agent-core-wr1qexzq.js";
|
|
20
41
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
DEFAULT_APP_LINK_BASE,
|
|
43
|
+
getSharedLarkCardNotifier,
|
|
44
|
+
getSharedLarkCardStateStore
|
|
45
|
+
} from "./roy-agent-core-shb1qsyg.js";
|
|
46
|
+
import {
|
|
47
|
+
RECENT_MESSAGE_LIMIT,
|
|
48
|
+
createActivityProbe,
|
|
49
|
+
createSessionActivityProbe,
|
|
50
|
+
extractToolCallsFromMessages,
|
|
51
|
+
mapBgTaskStatusToSessionLifecycle,
|
|
52
|
+
toActivityMessage
|
|
53
|
+
} from "./roy-agent-core-418zc9ha.js";
|
|
24
54
|
import {
|
|
25
55
|
notifyReactFailure
|
|
26
|
-
} from "./roy-agent-core-
|
|
56
|
+
} from "./roy-agent-core-9c565frx.js";
|
|
27
57
|
import {
|
|
28
58
|
normalizeLarkAtMentions
|
|
29
59
|
} from "./roy-agent-core-m880qz3y.js";
|
|
30
60
|
import {
|
|
31
61
|
globalHookManager,
|
|
32
62
|
init_global_hook_manager
|
|
33
|
-
} from "./roy-agent-core-
|
|
63
|
+
} from "./roy-agent-core-yet36q5k.js";
|
|
34
64
|
import {
|
|
35
65
|
init_context
|
|
36
|
-
} from "./roy-agent-core-
|
|
66
|
+
} from "./roy-agent-core-tkpp1ba0.js";
|
|
37
67
|
import {
|
|
38
68
|
createEnvContext,
|
|
39
69
|
normalizeParentTaskId,
|
|
40
70
|
runWithEnvContext
|
|
41
|
-
} from "./roy-agent-core-
|
|
71
|
+
} from "./roy-agent-core-p31ftgq9.js";
|
|
42
72
|
import {
|
|
43
73
|
TracedAs,
|
|
44
74
|
init_decorator,
|
|
45
75
|
wrapFunction
|
|
46
|
-
} from "./roy-agent-core-
|
|
76
|
+
} from "./roy-agent-core-5s2t8z00.js";
|
|
47
77
|
import {
|
|
48
78
|
createLogger,
|
|
49
79
|
getTracerProvider,
|
|
@@ -55,250 +85,139 @@ import {
|
|
|
55
85
|
} from "./roy-agent-core-fs0mn2jk.js";
|
|
56
86
|
|
|
57
87
|
// src/env/event-source/event-source-handlers.ts
|
|
58
|
-
import { spawn as
|
|
59
|
-
import { promisify
|
|
88
|
+
import { spawn as spawn3, exec as execSync } from "child_process";
|
|
89
|
+
import { promisify } from "node:util";
|
|
60
90
|
init_global_hook_manager();
|
|
61
91
|
init_context();
|
|
62
92
|
|
|
63
|
-
// src/env/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!taskId || Number.isNaN(taskId)) {
|
|
79
|
-
return { ok: false, action, error: `Invalid taskId: ${taskIdRaw}` };
|
|
80
|
-
}
|
|
81
|
-
if (action !== "create_group" && action !== "disband_group") {
|
|
82
|
-
return { ok: false, action, error: `Unknown action: ${action}` };
|
|
83
|
-
}
|
|
84
|
-
const inflightKey = `${event.profile ?? "-"}:${event.messageId || String(taskId)}:${action}`;
|
|
85
|
-
const inflight = cardActionInflight.get(inflightKey);
|
|
86
|
-
if (inflight) {
|
|
87
|
-
logger.warn(`handleCardAction: ${inflightKey} 已有同操作在处理中(疑似用户重复点击),本次忽略。`);
|
|
88
|
-
return { ok: false, action, error: `重复点击:${inflightKey} 正在处理中,已忽略` };
|
|
93
|
+
// src/env/event-source/es-plugins.ts
|
|
94
|
+
init_global_hook_manager();
|
|
95
|
+
var BUILT_IN_PLUGIN_FACTORIES = {
|
|
96
|
+
LarkCliTaskNotifyPlugin: (cfg) => createLarkCliTaskNotifyHook(cfg),
|
|
97
|
+
WecomTaskNotifyPlugin: (cfg) => createWecomTaskNotifyHook(cfg),
|
|
98
|
+
LarkCliWorkflowNotifyPlugin: (cfg) => createLarkCliWorkflowNotifyHook(cfg),
|
|
99
|
+
PerTaskGroupChatPlugin: (cfg) => createPerTaskGroupChatPlugin(cfg),
|
|
100
|
+
"task-tag": () => new TaskTagPlugin
|
|
101
|
+
};
|
|
102
|
+
function getWecomPluginFactories() {
|
|
103
|
+
return BUILT_IN_PLUGIN_FACTORIES;
|
|
104
|
+
}
|
|
105
|
+
function getPluginHookPointsShared(plugin) {
|
|
106
|
+
if (Array.isArray(plugin.hooks)) {
|
|
107
|
+
return plugin.hooks.map((h) => h.point || h);
|
|
89
108
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
return await pending;
|
|
94
|
-
} finally {
|
|
95
|
-
cardActionInflight.delete(inflightKey);
|
|
109
|
+
if (Array.isArray(plugin.hookPoints)) {
|
|
110
|
+
return plugin.hookPoints.map((h) => h.point || h);
|
|
96
111
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const { action } = event.actionValue;
|
|
100
|
-
if (!ports.getTask) {
|
|
101
|
-
return { ok: false, action, error: "ports.getTask not injected" };
|
|
112
|
+
if (typeof plugin.execute === "function" && plugin.hookPoint) {
|
|
113
|
+
return [plugin.hookPoint];
|
|
102
114
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
if (plugin.name && typeof plugin.execute === "function") {
|
|
116
|
+
return [
|
|
117
|
+
"task:after.create",
|
|
118
|
+
"task:after.update",
|
|
119
|
+
"task:operation:after.create",
|
|
120
|
+
"task:after.complete",
|
|
121
|
+
"task:after.delete"
|
|
122
|
+
];
|
|
106
123
|
}
|
|
107
|
-
|
|
108
|
-
if (action === "create_group") {
|
|
109
|
-
return await handleCreateGroup(event, ports, task, existingChatId);
|
|
110
|
-
} else {
|
|
111
|
-
return await handleDisbandGroup(event, ports, task, existingChatId);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function buildCardAfterGroupChange(event, ports, task, perTaskChatId, perTaskChatName) {
|
|
115
|
-
const meta = buildTaskMetaFromTask(task, perTaskChatId, perTaskChatName);
|
|
116
|
-
const preserved = ports.notifier?.updateMetaAndBuildCard?.(event.messageId, meta, { clearPerTaskChat: !perTaskChatId });
|
|
117
|
-
if (preserved) {
|
|
118
|
-
logger.debug(`buildCardAfterGroupChange: 复用 notifier 累积状态(里程碑历史保留)messageId=${event.messageId}`);
|
|
119
|
-
return preserved;
|
|
120
|
-
}
|
|
121
|
-
logger.warn(`buildCardAfterGroupChange: notifier 无 messageId=${event.messageId} 的状态,` + `fallback 重建卡片(该卡片的历史里程碑可能丢失)`);
|
|
122
|
-
return buildTaskCard({
|
|
123
|
-
status: "running",
|
|
124
|
-
meta,
|
|
125
|
-
milestones: [
|
|
126
|
-
{ title: "Task 创建", timestamp: new Date().toISOString(), description: task.current_status }
|
|
127
|
-
]
|
|
128
|
-
});
|
|
124
|
+
return [];
|
|
129
125
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
function registerPluginHooksShared(plugin, pluginName, sourceId) {
|
|
127
|
+
const hookPoints = getPluginHookPointsShared(plugin);
|
|
128
|
+
if (typeof plugin.init === "function") {
|
|
129
|
+
const pluginEnv = {
|
|
130
|
+
getComponent: () => null,
|
|
131
|
+
registerAgent: () => {},
|
|
132
|
+
registerTool: () => {},
|
|
133
|
+
registerHook: (hook) => {
|
|
134
|
+
globalHookManager.register(hook.point, {
|
|
135
|
+
name: `${pluginName}:${hook.point}`,
|
|
136
|
+
priority: hook.priority ?? 0,
|
|
137
|
+
pluginName,
|
|
138
|
+
sourceId,
|
|
139
|
+
execute: hook.handler
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
logger: {
|
|
143
|
+
debug: (...args) => console.debug(`[plugin:${pluginName}]`, ...args),
|
|
144
|
+
info: (...args) => console.info(`[plugin:${pluginName}]`, ...args),
|
|
145
|
+
warn: (...args) => console.warn(`[plugin:${pluginName}]`, ...args),
|
|
146
|
+
error: (...args) => console.error(`[plugin:${pluginName}]`, ...args)
|
|
147
|
+
}
|
|
139
148
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
await ports.inviteUser(chatId, operatorOpenId, event.profile);
|
|
163
|
-
invitedOpenIds = [operatorOpenId];
|
|
164
|
-
logger.debug(`handleCreateGroup: 走 inviteUser 端口邀请 ${operatorOpenId} 进 ${chatId}`);
|
|
165
|
-
} catch (err) {
|
|
166
|
-
logger.warn(`handleCreateGroup: inviteUser(${chatId}, ${operatorOpenId}) 失败(不阻断主流程): ${err.message}`);
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
logger.warn(`handleCreateGroup: createGroupChat 不支持 initialUserIds 且 inviteUser 未注入,` + `${operatorOpenId} 未被拉进群 ${chatId}。` + `需升级 caller(bridge/real impl)使用 ports.inviteUser 或实现 createGroupChat.initialUserIds 参数。`);
|
|
149
|
+
plugin.init(pluginEnv);
|
|
150
|
+
return hookPoints;
|
|
151
|
+
}
|
|
152
|
+
if (typeof plugin.execute === "function") {
|
|
153
|
+
if (plugin.hookPoint) {
|
|
154
|
+
globalHookManager.register(plugin.hookPoint, {
|
|
155
|
+
name: `${pluginName}:${plugin.hookPoint}`,
|
|
156
|
+
priority: plugin.priority ?? 0,
|
|
157
|
+
pluginName,
|
|
158
|
+
sourceId,
|
|
159
|
+
execute: plugin.execute.bind(plugin)
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
for (const hookPoint of hookPoints) {
|
|
163
|
+
globalHookManager.register(hookPoint, {
|
|
164
|
+
name: `${pluginName}:${hookPoint}`,
|
|
165
|
+
priority: plugin.priority ?? 0,
|
|
166
|
+
pluginName,
|
|
167
|
+
sourceId,
|
|
168
|
+
execute: plugin.execute.bind(plugin)
|
|
169
|
+
});
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
return hookPoints;
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(plugin.hooks)) {
|
|
175
|
+
for (const hook of plugin.hooks) {
|
|
176
|
+
const hookPoint = hook.point || hook;
|
|
177
|
+
const priority = hook.priority ?? 0;
|
|
178
|
+
globalHookManager.register(hookPoint, {
|
|
179
|
+
name: `${pluginName}:${hookPoint}`,
|
|
180
|
+
priority,
|
|
181
|
+
pluginName,
|
|
182
|
+
sourceId,
|
|
183
|
+
execute: async (ctx) => {
|
|
184
|
+
await plugin.execute(ctx);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
181
187
|
}
|
|
182
|
-
|
|
183
|
-
return { ok: true, action: "create_group", newChatId: chatId, cardUpdate };
|
|
184
|
-
} catch (err) {
|
|
185
|
-
logger.error(`handleCardAction.create_group failed: ${err.message}`);
|
|
186
|
-
return { ok: false, action: "create_group", error: err.message };
|
|
188
|
+
return hookPoints;
|
|
187
189
|
}
|
|
190
|
+
return hookPoints;
|
|
188
191
|
}
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return
|
|
194
|
-
if (typeof raw === "object" && typeof raw.open_id === "string")
|
|
195
|
-
return raw.open_id;
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
async function handleDisbandGroup(event, ports, task, existingChatId) {
|
|
199
|
-
if (!existingChatId) {
|
|
200
|
-
return {
|
|
201
|
-
ok: false,
|
|
202
|
-
action: "disband_group",
|
|
203
|
-
error: `Task #${task.id} 无专属群(context.larkPerTaskChatId 空),无需解散`
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
if (!ports.deleteGroupChat) {
|
|
207
|
-
return { ok: false, action: "disband_group", error: "ports.deleteGroupChat not injected" };
|
|
192
|
+
function importPluginShared(name, factories, pluginConfig = {}) {
|
|
193
|
+
const factory = factories[name];
|
|
194
|
+
if (!factory) {
|
|
195
|
+
console.error(`[es-plugins] Unknown plugin: ${name}`);
|
|
196
|
+
return null;
|
|
208
197
|
}
|
|
209
198
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
[CTX_FIELD_CHAT_CREATED_AT]: undefined,
|
|
215
|
-
[CTX_FIELD_CHAT_TASK_ID]: undefined
|
|
199
|
+
return factory({
|
|
200
|
+
sourceId: "",
|
|
201
|
+
pluginName: name,
|
|
202
|
+
...pluginConfig
|
|
216
203
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const cardUpdate = buildCardAfterGroupChange(event, ports, task, undefined, undefined);
|
|
221
|
-
return { ok: true, action: "disband_group", cardUpdate };
|
|
222
|
-
} catch (err) {
|
|
223
|
-
logger.error(`handleCardAction.disband_group failed: ${err.message}`);
|
|
224
|
-
return { ok: false, action: "disband_group", error: err.message };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
function patchContext(contextStr, changes) {
|
|
228
|
-
let obj = {};
|
|
229
|
-
try {
|
|
230
|
-
const parsed = JSON.parse(contextStr);
|
|
231
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
232
|
-
obj = parsed;
|
|
233
|
-
}
|
|
234
|
-
} catch {
|
|
235
|
-
obj = {};
|
|
236
|
-
}
|
|
237
|
-
for (const [k, v] of Object.entries(changes)) {
|
|
238
|
-
if (v === undefined) {
|
|
239
|
-
delete obj[k];
|
|
240
|
-
} else {
|
|
241
|
-
obj[k] = v;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return JSON.stringify(obj);
|
|
245
|
-
}
|
|
246
|
-
function buildTaskMetaFromTask(task, perTaskChatId, perTaskChatName) {
|
|
247
|
-
return {
|
|
248
|
-
id: task.id,
|
|
249
|
-
title: task.title,
|
|
250
|
-
description: task.description,
|
|
251
|
-
goals: task.goals,
|
|
252
|
-
parentTaskId: typeof task.parent_task_id === "number" && task.parent_task_id > 0 ? task.parent_task_id : undefined,
|
|
253
|
-
priority: task.priority,
|
|
254
|
-
type: task.type,
|
|
255
|
-
projectPath: task.project_path,
|
|
256
|
-
context: task.context,
|
|
257
|
-
currentStatus: task.current_status,
|
|
258
|
-
perTaskChatId,
|
|
259
|
-
perTaskChatName
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
async function deleteGroupChatViaLarkCli(chatId, profile) {
|
|
263
|
-
const profileFlag = profile ? `--profile ${profile} ` : "";
|
|
264
|
-
try {
|
|
265
|
-
const { stdout: authStdout } = await execAsync(`lark-cli ${profileFlag}auth status --json`, { timeout: 15000 });
|
|
266
|
-
const appId = JSON.parse(authStdout).appId;
|
|
267
|
-
if (!appId) {
|
|
268
|
-
throw new Error(`lark-cli auth status 未返回 appId: ${authStdout.slice(0, 200)}`);
|
|
269
|
-
}
|
|
270
|
-
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`;
|
|
271
|
-
const { stdout, stderr } = await execAsync(cmd, { timeout: 30000 });
|
|
272
|
-
logger.debug(`deleteGroupChatViaLarkCli(${chatId}) bot 退群 ok: ` + `stdout=${stdout?.slice(0, 300)} stderr=${stderr?.slice(0, 300)}`);
|
|
273
|
-
} catch (err) {
|
|
274
|
-
logger.warn(`deleteGroupChatViaLarkCli(${chatId}) bot 退群失败(task.context 仍会清掉,` + `飞书侧群会保留):${err.message}`);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(`[es-plugins] Failed to instantiate plugin ${name}:`, error);
|
|
206
|
+
return null;
|
|
275
207
|
}
|
|
276
208
|
}
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
function truncateText(text, maxLen) {
|
|
281
|
-
if (!text)
|
|
282
|
-
return "";
|
|
283
|
-
if (text.length <= maxLen)
|
|
284
|
-
return text;
|
|
285
|
-
return text.substring(0, Math.max(0, maxLen - 3)) + "...";
|
|
286
|
-
}
|
|
287
|
-
async function createGroupChatViaLarkCli(opts) {
|
|
288
|
-
const profileFlag = opts.profile ? `--profile ${opts.profile} ` : "";
|
|
289
|
-
const usersFlag = opts.initialUserIds && opts.initialUserIds.length > 0 ? `--users ${shellQuote(opts.initialUserIds.join(","))} ` : "";
|
|
290
|
-
const cmd = `lark-cli ${profileFlag}im +chat-create --name ${shellQuote(truncateText(opts.name, 60))} ` + usersFlag + `--description ${shellQuote(truncateText(opts.description, 100))} --as bot`;
|
|
291
|
-
const { stdout } = await execAsync(cmd, { timeout: 60000 });
|
|
292
|
-
const m = stdout.match(/"chat_id"\s*:\s*"(oc_[a-f0-9]+)"/);
|
|
293
|
-
if (!m) {
|
|
294
|
-
throw new Error(`lark-cli +chat-create 返回无法解析: ${stdout.slice(0, 300)}`);
|
|
209
|
+
function unregisterPluginHooksShared(pluginName, hookPoints) {
|
|
210
|
+
for (const hookPoint of hookPoints) {
|
|
211
|
+
globalHookManager.unregister(hookPoint, `${pluginName}:${hookPoint}`);
|
|
295
212
|
}
|
|
296
|
-
return m[1];
|
|
297
213
|
}
|
|
298
214
|
|
|
299
215
|
// src/env/event-source/timer-handler.ts
|
|
300
216
|
init_decorator();
|
|
217
|
+
init_logger();
|
|
301
218
|
var DEFAULT_TIMER_INTERVAL_MS = 10 * 60 * 1000;
|
|
219
|
+
var SKIPPED_TICK_LOG_EVERY = 20;
|
|
220
|
+
var timerLogger = createLogger("timer:event-source");
|
|
302
221
|
var SELF_REMINDER_MESSAGE = `[Timer Self-Reminder]
|
|
303
222
|
请读取最近 1 周处理的 task 信息,分析用户当前的工作目标和重点任务。
|
|
304
223
|
基于分析结果,自主构建有价值的任务来推进用户目标。
|
|
@@ -316,6 +235,19 @@ var TIMER_OPTIONS_KEY_SCHEDULE = "schedule";
|
|
|
316
235
|
var TIMER_OPTIONS_KEY_SESSION_ID = "sessionId";
|
|
317
236
|
var TIMER_OPTIONS_KEY_SKIP_WHEN_RUNNING = "skipWhenRunning";
|
|
318
237
|
var TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
238
|
+
function buildFireSpanAttributes(ctx) {
|
|
239
|
+
const attrs = {
|
|
240
|
+
mode: ctx.mode,
|
|
241
|
+
sourceId: ctx.sourceId,
|
|
242
|
+
skippedSinceLastFire: ctx.skippedSinceLastFire
|
|
243
|
+
};
|
|
244
|
+
if (ctx.entry !== undefined) {
|
|
245
|
+
attrs.scheduleTime = ctx.entry.time;
|
|
246
|
+
if (ctx.entry.label !== undefined)
|
|
247
|
+
attrs.scheduleLabel = ctx.entry.label;
|
|
248
|
+
}
|
|
249
|
+
return attrs;
|
|
250
|
+
}
|
|
319
251
|
function isValidScheduleTime(time) {
|
|
320
252
|
return typeof time === "string" && TIME_PATTERN.test(time);
|
|
321
253
|
}
|
|
@@ -406,6 +338,9 @@ class TimerInstance {
|
|
|
406
338
|
scheduleSessionId;
|
|
407
339
|
skipWhenRunning;
|
|
408
340
|
firedKeys = new Set;
|
|
341
|
+
firedTicks = 0;
|
|
342
|
+
skippedTicks = 0;
|
|
343
|
+
skippedSinceLastFire = 0;
|
|
409
344
|
now;
|
|
410
345
|
status = "created";
|
|
411
346
|
timerId;
|
|
@@ -461,6 +396,13 @@ class TimerInstance {
|
|
|
461
396
|
getStatus() {
|
|
462
397
|
return this.status;
|
|
463
398
|
}
|
|
399
|
+
getTickStats() {
|
|
400
|
+
return {
|
|
401
|
+
firedTicks: this.firedTicks,
|
|
402
|
+
skippedTicks: this.skippedTicks,
|
|
403
|
+
skippedSinceLastFire: this.skippedSinceLastFire
|
|
404
|
+
};
|
|
405
|
+
}
|
|
464
406
|
onEvent(handler) {
|
|
465
407
|
this.eventHandler = handler;
|
|
466
408
|
}
|
|
@@ -477,13 +419,31 @@ class TimerInstance {
|
|
|
477
419
|
allTasks = this.getRunningTasks();
|
|
478
420
|
} catch (error) {
|
|
479
421
|
console.warn(`[TimerInstance] Failed to get running tasks, skipping tick: ${error}`);
|
|
422
|
+
this.recordSkip();
|
|
480
423
|
return;
|
|
481
424
|
}
|
|
482
425
|
const hasRunning = Array.isArray(allTasks) && allTasks.some((t) => t.status === "running");
|
|
483
426
|
if (hasRunning) {
|
|
427
|
+
this.recordSkip();
|
|
484
428
|
return;
|
|
485
429
|
}
|
|
486
|
-
|
|
430
|
+
this.fireTick({
|
|
431
|
+
mode: "interval",
|
|
432
|
+
sourceId: this.config.id,
|
|
433
|
+
skippedSinceLastFire: this.skippedSinceLastFire
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
recordSkip() {
|
|
437
|
+
this.skippedTicks++;
|
|
438
|
+
this.skippedSinceLastFire++;
|
|
439
|
+
if (this.skippedTicks % SKIPPED_TICK_LOG_EVERY === 0) {
|
|
440
|
+
timerLogger.debug(`[TimerInstance] ${this.config.id}: ${this.skippedTicks} ticks skipped ` + `(${this.skippedSinceLastFire} since last fire, mode=${this.isScheduleMode ? "schedule" : "interval"})`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
fireTick(ctx) {
|
|
444
|
+
this.firedTicks++;
|
|
445
|
+
this.skippedSinceLastFire = 0;
|
|
446
|
+
const event = this.buildSelfReminderEvent(ctx.entry);
|
|
487
447
|
this.eventHandler?.(event);
|
|
488
448
|
}
|
|
489
449
|
tickSchedule() {
|
|
@@ -492,6 +452,7 @@ class TimerInstance {
|
|
|
492
452
|
this.invalidScheduleWarned = true;
|
|
493
453
|
console.warn(`[TimerInstance] Timer ${this.config.id} has an invalid or empty schedule configuration; ` + `no events will be fired. Check options.schedule (expected [{ time: "HH:MM", prompt: "..." }]).`);
|
|
494
454
|
}
|
|
455
|
+
this.recordSkip();
|
|
495
456
|
return;
|
|
496
457
|
}
|
|
497
458
|
const now = new Date(this.now());
|
|
@@ -513,6 +474,7 @@ class TimerInstance {
|
|
|
513
474
|
hasRunning = Array.isArray(allTasks) && allTasks.some((t) => t.status === "running");
|
|
514
475
|
} catch (error) {
|
|
515
476
|
console.warn(`[TimerInstance] Failed to get running tasks, skipping tick: ${error}`);
|
|
477
|
+
this.recordSkip();
|
|
516
478
|
return;
|
|
517
479
|
}
|
|
518
480
|
if (hasRunning) {
|
|
@@ -521,10 +483,15 @@ class TimerInstance {
|
|
|
521
483
|
}
|
|
522
484
|
this.firedKeys.add(firedKey);
|
|
523
485
|
this.pruneFiredKeys(dateKey);
|
|
524
|
-
|
|
525
|
-
|
|
486
|
+
this.fireTick({
|
|
487
|
+
mode: "schedule",
|
|
488
|
+
sourceId: this.config.id,
|
|
489
|
+
skippedSinceLastFire: this.skippedSinceLastFire,
|
|
490
|
+
entry
|
|
491
|
+
});
|
|
526
492
|
return;
|
|
527
493
|
}
|
|
494
|
+
this.recordSkip();
|
|
528
495
|
}
|
|
529
496
|
pruneFiredKeys(currentDateKey) {
|
|
530
497
|
if (this.firedKeys.size <= 32)
|
|
@@ -581,14 +548,14 @@ class TimerInstance {
|
|
|
581
548
|
}
|
|
582
549
|
}
|
|
583
550
|
__legacyDecorateClassTS([
|
|
584
|
-
TracedAs("timer.tick", {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
], TimerInstance.prototype, "
|
|
551
|
+
TracedAs("timer.tick.fire", {
|
|
552
|
+
recordParams: true,
|
|
553
|
+
recordResult: false,
|
|
554
|
+
log: false,
|
|
555
|
+
paramFilter: (args) => buildFireSpanAttributes(args[0]),
|
|
556
|
+
flattenParamFilter: true
|
|
557
|
+
})
|
|
558
|
+
], TimerInstance.prototype, "fireTick", null);
|
|
592
559
|
var timerHandler = {
|
|
593
560
|
type: "timer",
|
|
594
561
|
validateConfig(config) {
|
|
@@ -788,13 +755,19 @@ class BountyIMInstance {
|
|
|
788
755
|
const { email } = readBountyIMConfig(this.config);
|
|
789
756
|
return email || "unknown";
|
|
790
757
|
}
|
|
758
|
+
cachedSystemPrompt;
|
|
759
|
+
systemPromptResolved = false;
|
|
791
760
|
getCurrentSystemPrompt() {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
761
|
+
if (!this.systemPromptResolved) {
|
|
762
|
+
const { email, imServerUrl } = readBountyIMConfig(this.config);
|
|
763
|
+
this.cachedSystemPrompt = resolveBountyIMSystemPrompt({
|
|
764
|
+
configSystemPrompt: this.config.systemPrompt,
|
|
765
|
+
currentEmail: email,
|
|
766
|
+
imServerUrl
|
|
767
|
+
});
|
|
768
|
+
this.systemPromptResolved = true;
|
|
769
|
+
}
|
|
770
|
+
return this.cachedSystemPrompt;
|
|
798
771
|
}
|
|
799
772
|
getStatus() {
|
|
800
773
|
return this.status;
|
|
@@ -1148,123 +1121,2030 @@ class BountyCliInstance {
|
|
|
1148
1121
|
this.processStdout(stdoutBuffer, channel ?? "");
|
|
1149
1122
|
stdoutBuffer = "";
|
|
1150
1123
|
});
|
|
1151
|
-
this.child.stderr?.on("data", (data) => {
|
|
1152
|
-
const output = data.toString();
|
|
1153
|
-
stderrBuffer += output;
|
|
1154
|
-
this.processStderr(stderrBuffer);
|
|
1155
|
-
stderrBuffer = "";
|
|
1124
|
+
this.child.stderr?.on("data", (data) => {
|
|
1125
|
+
const output = data.toString();
|
|
1126
|
+
stderrBuffer += output;
|
|
1127
|
+
this.processStderr(stderrBuffer);
|
|
1128
|
+
stderrBuffer = "";
|
|
1129
|
+
});
|
|
1130
|
+
this.child.on("exit", (code, signal) => {
|
|
1131
|
+
if (code !== 0 && signal !== "SIGINT" && signal !== "SIGTERM") {
|
|
1132
|
+
this.setStatus("error");
|
|
1133
|
+
} else {
|
|
1134
|
+
this.setStatus("stopped");
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
this.child.on("error", (err) => {
|
|
1138
|
+
console.error(`[bounty-cli] spawn error: ${err.message}`);
|
|
1139
|
+
this.setStatus("error");
|
|
1140
|
+
});
|
|
1141
|
+
this.setStatus("running");
|
|
1142
|
+
}
|
|
1143
|
+
processStdout(buffer, fallbackChannel) {
|
|
1144
|
+
const lines = buffer.split(`
|
|
1145
|
+
`);
|
|
1146
|
+
const tail = lines.pop() ?? "";
|
|
1147
|
+
this.buffer = tail;
|
|
1148
|
+
for (const line of lines) {
|
|
1149
|
+
const trimmed = line.trim();
|
|
1150
|
+
if (!trimmed)
|
|
1151
|
+
continue;
|
|
1152
|
+
this.handleEventLine(trimmed, fallbackChannel);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
processStderr(buffer) {
|
|
1156
|
+
const lines = buffer.split(`
|
|
1157
|
+
`);
|
|
1158
|
+
const tail = lines.pop() ?? "";
|
|
1159
|
+
for (const line of lines) {
|
|
1160
|
+
const trimmed = line.trim();
|
|
1161
|
+
if (!trimmed)
|
|
1162
|
+
continue;
|
|
1163
|
+
if (trimmed.startsWith("{") && trimmed.includes("event_id")) {
|
|
1164
|
+
this.handleEventLine(trimmed, readBountyCliConfig(this.config).channel ?? "");
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (tail.trim().length > 0) {
|
|
1168
|
+
this.buffer = tail;
|
|
1169
|
+
} else {
|
|
1170
|
+
this.buffer = "";
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
handleEventLine(line, fallbackChannel) {
|
|
1174
|
+
let raw;
|
|
1175
|
+
try {
|
|
1176
|
+
raw = JSON.parse(line);
|
|
1177
|
+
} catch {
|
|
1178
|
+
console.error(`[bounty-cli] failed to parse event line: ${line.slice(0, 200)}`);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
const { metadata, replyChannel } = translateEvent(raw, fallbackChannel);
|
|
1182
|
+
const event = {
|
|
1183
|
+
metadata,
|
|
1184
|
+
rawEvent: raw,
|
|
1185
|
+
replyChannel,
|
|
1186
|
+
recommendedAction: buildBountyCliRecommendedAction({
|
|
1187
|
+
profile: readBountyCliConfig(this.config).profile,
|
|
1188
|
+
channelName: String(metadata.channelName ?? fallbackChannel)
|
|
1189
|
+
})
|
|
1190
|
+
};
|
|
1191
|
+
if (this.eventHandler) {
|
|
1192
|
+
try {
|
|
1193
|
+
this.eventHandler(event);
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
console.error(`[bounty-cli] event handler threw: ${err}`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
async stop() {
|
|
1200
|
+
if (this.status === "stopped" || this.status === "created")
|
|
1201
|
+
return;
|
|
1202
|
+
if (this.child && !this.child.killed) {
|
|
1203
|
+
this.child.kill("SIGINT");
|
|
1204
|
+
await new Promise((resolve) => {
|
|
1205
|
+
const timer = setTimeout(() => {
|
|
1206
|
+
if (this.child && !this.child.killed) {
|
|
1207
|
+
this.child.kill("SIGKILL");
|
|
1208
|
+
}
|
|
1209
|
+
resolve();
|
|
1210
|
+
}, 3000);
|
|
1211
|
+
if (this.child) {
|
|
1212
|
+
this.child.on("exit", () => {
|
|
1213
|
+
clearTimeout(timer);
|
|
1214
|
+
resolve();
|
|
1215
|
+
});
|
|
1216
|
+
} else {
|
|
1217
|
+
clearTimeout(timer);
|
|
1218
|
+
resolve();
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
this.setStatus("stopped");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
var bountyCliHandler = {
|
|
1226
|
+
type: "bounty-cli",
|
|
1227
|
+
validateConfig(config) {
|
|
1228
|
+
const errors = [];
|
|
1229
|
+
if (!config.id)
|
|
1230
|
+
errors.push("EventSource ID is required");
|
|
1231
|
+
if (!config.name)
|
|
1232
|
+
errors.push("EventSource name is required");
|
|
1233
|
+
const { channel, customCommand } = readBountyCliConfig(config);
|
|
1234
|
+
if (!channel && !customCommand) {
|
|
1235
|
+
errors.push("Bounty CLI 'channel' (top-level channel or options.channel) is required");
|
|
1236
|
+
}
|
|
1237
|
+
return errors;
|
|
1238
|
+
},
|
|
1239
|
+
createInstance(config) {
|
|
1240
|
+
return new BountyCliInstance(config);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// src/env/event-source/wecom-cli-handler.ts
|
|
1245
|
+
import { spawn as spawn2 } from "child_process";
|
|
1246
|
+
import { existsSync, readFileSync } from "fs";
|
|
1247
|
+
import { delimiter, isAbsolute, join as join2 } from "path";
|
|
1248
|
+
|
|
1249
|
+
// src/env/event-source/wecom-ws-bridge.ts
|
|
1250
|
+
var WECOM_WS_URL = "wss://openws.work.weixin.qq.com";
|
|
1251
|
+
var WECOM_HEARTBEAT_INTERVAL_MS = 30000;
|
|
1252
|
+
var WECOM_RECONNECT_BASE_DELAY_MS = 1000;
|
|
1253
|
+
var WECOM_RECONNECT_MAX_DELAY_MS = 60000;
|
|
1254
|
+
var WECOM_MAX_RECONNECT_ATTEMPTS = 10;
|
|
1255
|
+
var WECOM_MAX_AUTH_FAILURE_ATTEMPTS = 5;
|
|
1256
|
+
function randomString() {
|
|
1257
|
+
return Math.random().toString(36).slice(2, 10);
|
|
1258
|
+
}
|
|
1259
|
+
function generateWecomReqId(prefix) {
|
|
1260
|
+
return `${prefix}_${Date.now()}_${randomString()}`;
|
|
1261
|
+
}
|
|
1262
|
+
function buildWecomAuthFrame(botId, secret) {
|
|
1263
|
+
return {
|
|
1264
|
+
cmd: "aibot_subscribe",
|
|
1265
|
+
headers: { req_id: generateWecomReqId("aibot_subscribe") },
|
|
1266
|
+
body: { bot_id: botId, secret }
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
function buildWecomHeartbeatFrame() {
|
|
1270
|
+
return {
|
|
1271
|
+
cmd: "ping",
|
|
1272
|
+
headers: { req_id: generateWecomReqId("ping") }
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function buildWecomRespondFrame(reqId, body) {
|
|
1276
|
+
const id = reqId.trim();
|
|
1277
|
+
if (!id) {
|
|
1278
|
+
throw new Error("buildWecomRespondFrame: req_id is required (reply must reference the original message frame)");
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
cmd: "aibot_respond_msg",
|
|
1282
|
+
headers: { req_id: id },
|
|
1283
|
+
body
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
function isWecomAuthResponse(frame) {
|
|
1287
|
+
if (frame.cmd)
|
|
1288
|
+
return false;
|
|
1289
|
+
const reqId = frame.headers?.req_id ?? "";
|
|
1290
|
+
return reqId.startsWith("aibot_subscribe_");
|
|
1291
|
+
}
|
|
1292
|
+
function isWecomHeartbeatResponse(frame) {
|
|
1293
|
+
if (frame.cmd)
|
|
1294
|
+
return false;
|
|
1295
|
+
const reqId = frame.headers?.req_id ?? "";
|
|
1296
|
+
return reqId.startsWith("ping_");
|
|
1297
|
+
}
|
|
1298
|
+
function isWecomDisconnectedEvent(frame) {
|
|
1299
|
+
if (!frame || typeof frame !== "object")
|
|
1300
|
+
return false;
|
|
1301
|
+
const f = frame;
|
|
1302
|
+
if (f.cmd !== "aibot_event_callback")
|
|
1303
|
+
return false;
|
|
1304
|
+
return f.body?.event?.eventtype === "disconnected_event";
|
|
1305
|
+
}
|
|
1306
|
+
function sanitizeWecomFrame(frame) {
|
|
1307
|
+
const clean = { ...frame };
|
|
1308
|
+
if ("secret" in clean)
|
|
1309
|
+
delete clean.secret;
|
|
1310
|
+
if (clean.body && typeof clean.body === "object") {
|
|
1311
|
+
clean.body = { ...clean.body };
|
|
1312
|
+
if ("secret" in clean.body)
|
|
1313
|
+
delete clean.body.secret;
|
|
1314
|
+
}
|
|
1315
|
+
return clean;
|
|
1316
|
+
}
|
|
1317
|
+
function backoffDelay(attempt, base, max) {
|
|
1318
|
+
return Math.min(base * Math.pow(2, attempt - 1), max);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
class WecomWsBridge {
|
|
1322
|
+
opts;
|
|
1323
|
+
botId;
|
|
1324
|
+
secret;
|
|
1325
|
+
wsFactory;
|
|
1326
|
+
wsUrl;
|
|
1327
|
+
ws;
|
|
1328
|
+
heartbeatTimer;
|
|
1329
|
+
reconnectTimer;
|
|
1330
|
+
reconnectAttempts = 0;
|
|
1331
|
+
authFailureAttempts = 0;
|
|
1332
|
+
manualClose = false;
|
|
1333
|
+
lastCloseWasAuthFailure = false;
|
|
1334
|
+
stopped = false;
|
|
1335
|
+
onMessageCb;
|
|
1336
|
+
onAuthenticatedCb;
|
|
1337
|
+
onReconnectingCb;
|
|
1338
|
+
onErrorCb;
|
|
1339
|
+
constructor(opts) {
|
|
1340
|
+
this.opts = {
|
|
1341
|
+
heartbeatIntervalMs: opts.heartbeatIntervalMs ?? WECOM_HEARTBEAT_INTERVAL_MS,
|
|
1342
|
+
reconnectBaseDelayMs: opts.reconnectBaseDelayMs ?? WECOM_RECONNECT_BASE_DELAY_MS,
|
|
1343
|
+
reconnectMaxDelayMs: opts.reconnectMaxDelayMs ?? WECOM_RECONNECT_MAX_DELAY_MS,
|
|
1344
|
+
maxReconnectAttempts: opts.maxReconnectAttempts ?? WECOM_MAX_RECONNECT_ATTEMPTS,
|
|
1345
|
+
maxAuthFailureAttempts: opts.maxAuthFailureAttempts ?? WECOM_MAX_AUTH_FAILURE_ATTEMPTS
|
|
1346
|
+
};
|
|
1347
|
+
this.botId = opts.botId;
|
|
1348
|
+
this.secret = opts.secret;
|
|
1349
|
+
this.wsUrl = opts.wsUrl ?? WECOM_WS_URL;
|
|
1350
|
+
this.wsFactory = opts.wsFactory ?? ((url) => {
|
|
1351
|
+
throw new Error("wsFactory is required in production (inject WebSocket from 'ws')");
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
onMessage(cb) {
|
|
1355
|
+
this.onMessageCb = cb;
|
|
1356
|
+
}
|
|
1357
|
+
onAuthenticated(cb) {
|
|
1358
|
+
this.onAuthenticatedCb = cb;
|
|
1359
|
+
}
|
|
1360
|
+
onReconnecting(cb) {
|
|
1361
|
+
this.onReconnectingCb = cb;
|
|
1362
|
+
}
|
|
1363
|
+
onError(cb) {
|
|
1364
|
+
this.onErrorCb = cb;
|
|
1365
|
+
}
|
|
1366
|
+
connect() {
|
|
1367
|
+
if (this.stopped)
|
|
1368
|
+
return;
|
|
1369
|
+
const ws = this.wsFactory(this.wsUrl);
|
|
1370
|
+
this.ws = ws;
|
|
1371
|
+
ws.on("open", () => {
|
|
1372
|
+
this.lastCloseWasAuthFailure = false;
|
|
1373
|
+
this.sendFrame(buildWecomAuthFrame(this.optsBotId(), this.optsSecret()));
|
|
1374
|
+
});
|
|
1375
|
+
ws.on("message", (data) => {
|
|
1376
|
+
try {
|
|
1377
|
+
const raw = String(data).replace(/[\x00-\x08\x0B-\x0D\x0E-\x1F]/g, "");
|
|
1378
|
+
const frame = JSON.parse(raw);
|
|
1379
|
+
this.handleFrame(frame);
|
|
1380
|
+
} catch {}
|
|
1381
|
+
});
|
|
1382
|
+
ws.on("close", () => {
|
|
1383
|
+
this.stopHeartbeat();
|
|
1384
|
+
if (this.manualClose || this.stopped)
|
|
1385
|
+
return;
|
|
1386
|
+
this.scheduleReconnect();
|
|
1387
|
+
});
|
|
1388
|
+
ws.on("error", (err) => {
|
|
1389
|
+
this.onErrorCb?.(err);
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
optsBotId() {
|
|
1393
|
+
return this.botId;
|
|
1394
|
+
}
|
|
1395
|
+
optsSecret() {
|
|
1396
|
+
return this.secret;
|
|
1397
|
+
}
|
|
1398
|
+
handleFrame(frame) {
|
|
1399
|
+
if (isWecomAuthResponse(frame)) {
|
|
1400
|
+
if ((frame.errcode ?? 0) !== 0) {
|
|
1401
|
+
this.lastCloseWasAuthFailure = true;
|
|
1402
|
+
this.onErrorCb?.(new Error(`wecom auth failed: ${frame.errmsg ?? "?"} (code=${frame.errcode})`));
|
|
1403
|
+
this.ws?.terminate();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
this.reconnectAttempts = 0;
|
|
1407
|
+
this.authFailureAttempts = 0;
|
|
1408
|
+
this.startHeartbeat();
|
|
1409
|
+
this.onAuthenticatedCb?.();
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (isWecomHeartbeatResponse(frame))
|
|
1413
|
+
return;
|
|
1414
|
+
if (isWecomDisconnectedEvent(frame)) {
|
|
1415
|
+
this.lastCloseWasAuthFailure = false;
|
|
1416
|
+
this.onErrorCb?.(new Error("wecom disconnected_event: server kicked this connection " + "(bot allows only 1 long connection) — reconnecting"));
|
|
1417
|
+
this.stopHeartbeat();
|
|
1418
|
+
const stale = this.ws;
|
|
1419
|
+
this.ws = undefined;
|
|
1420
|
+
try {
|
|
1421
|
+
stale?.close?.();
|
|
1422
|
+
} catch {}
|
|
1423
|
+
this.scheduleReconnect();
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
this.onMessageCb?.(sanitizeWecomFrame(frame));
|
|
1427
|
+
}
|
|
1428
|
+
sendFrame(frame) {
|
|
1429
|
+
try {
|
|
1430
|
+
this.ws?.send(JSON.stringify(frame));
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
this.onErrorCb?.(err);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
respond(reqId, body) {
|
|
1436
|
+
if (this.stopped || this.manualClose) {
|
|
1437
|
+
return new Error("wecom bridge stopped, cannot respond");
|
|
1438
|
+
}
|
|
1439
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
1440
|
+
return new Error("wecom bridge not connected, cannot respond");
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
let frame;
|
|
1444
|
+
try {
|
|
1445
|
+
frame = buildWecomRespondFrame(reqId, body);
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
return err;
|
|
1448
|
+
}
|
|
1449
|
+
this.ws.send(JSON.stringify(frame));
|
|
1450
|
+
return;
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
sendCmd(cmd, body) {
|
|
1456
|
+
if (this.stopped || this.manualClose) {
|
|
1457
|
+
return new Error("wecom bridge stopped, cannot send");
|
|
1458
|
+
}
|
|
1459
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
1460
|
+
return new Error("wecom bridge not connected, cannot send");
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
this.ws.send(JSON.stringify({
|
|
1464
|
+
cmd,
|
|
1465
|
+
headers: { req_id: generateWecomReqId(cmd) },
|
|
1466
|
+
body
|
|
1467
|
+
}));
|
|
1468
|
+
return;
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
sendFrameWithReqId(frame) {
|
|
1474
|
+
if (this.stopped || this.manualClose) {
|
|
1475
|
+
return new Error("wecom bridge stopped, cannot send");
|
|
1476
|
+
}
|
|
1477
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
1478
|
+
return new Error("wecom bridge not connected, cannot send");
|
|
1479
|
+
}
|
|
1480
|
+
if (!frame.headers?.req_id?.trim()) {
|
|
1481
|
+
return new Error("sendFrameWithReqId: req_id is required (must reference the original frame)");
|
|
1482
|
+
}
|
|
1483
|
+
try {
|
|
1484
|
+
this.ws.send(JSON.stringify(frame));
|
|
1485
|
+
return;
|
|
1486
|
+
} catch (err) {
|
|
1487
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
startHeartbeat() {
|
|
1491
|
+
this.stopHeartbeat();
|
|
1492
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1493
|
+
this.sendFrame(buildWecomHeartbeatFrame());
|
|
1494
|
+
}, this.opts.heartbeatIntervalMs);
|
|
1495
|
+
}
|
|
1496
|
+
stopHeartbeat() {
|
|
1497
|
+
if (this.heartbeatTimer) {
|
|
1498
|
+
clearInterval(this.heartbeatTimer);
|
|
1499
|
+
this.heartbeatTimer = undefined;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
scheduleReconnect() {
|
|
1503
|
+
if (this.manualClose || this.stopped)
|
|
1504
|
+
return;
|
|
1505
|
+
if (this.reconnectTimer)
|
|
1506
|
+
return;
|
|
1507
|
+
const isAuthFailure = this.lastCloseWasAuthFailure;
|
|
1508
|
+
if (isAuthFailure) {
|
|
1509
|
+
if (this.authFailureAttempts >= this.opts.maxAuthFailureAttempts) {
|
|
1510
|
+
this.onErrorCb?.(new Error(`wecom auth failed ${this.authFailureAttempts} times, giving up`));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
this.authFailureAttempts++;
|
|
1514
|
+
} else {
|
|
1515
|
+
if (this.reconnectAttempts >= this.opts.maxReconnectAttempts) {
|
|
1516
|
+
this.onErrorCb?.(new Error(`wecom reconnect exhausted (${this.reconnectAttempts} attempts)`));
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
this.reconnectAttempts++;
|
|
1520
|
+
}
|
|
1521
|
+
const attempt = isAuthFailure ? this.authFailureAttempts : this.reconnectAttempts;
|
|
1522
|
+
const delay = backoffDelay(attempt, this.opts.reconnectBaseDelayMs, this.opts.reconnectMaxDelayMs);
|
|
1523
|
+
this.onReconnectingCb?.(attempt);
|
|
1524
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1525
|
+
this.reconnectTimer = undefined;
|
|
1526
|
+
if (this.manualClose || this.stopped)
|
|
1527
|
+
return;
|
|
1528
|
+
this.connect();
|
|
1529
|
+
}, delay);
|
|
1530
|
+
}
|
|
1531
|
+
stop() {
|
|
1532
|
+
this.stopped = true;
|
|
1533
|
+
this.manualClose = true;
|
|
1534
|
+
this.stopHeartbeat();
|
|
1535
|
+
if (this.reconnectTimer) {
|
|
1536
|
+
clearTimeout(this.reconnectTimer);
|
|
1537
|
+
this.reconnectTimer = undefined;
|
|
1538
|
+
}
|
|
1539
|
+
try {
|
|
1540
|
+
this.ws?.close();
|
|
1541
|
+
} catch {}
|
|
1542
|
+
this.ws = undefined;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/env/event-source/wecom-card-action-handler.ts
|
|
1547
|
+
init_logger();
|
|
1548
|
+
var logger = createLogger("wecom:card-action");
|
|
1549
|
+
var cardActionInflight = new Map;
|
|
1550
|
+
var ROY_TASK_PREFIX = "roy-task-";
|
|
1551
|
+
function parseRoySessionId(cardTaskId) {
|
|
1552
|
+
if (!cardTaskId.startsWith(ROY_SESSION_PREFIX))
|
|
1553
|
+
return null;
|
|
1554
|
+
const rest = cardTaskId.slice(ROY_SESSION_PREFIX.length);
|
|
1555
|
+
const sepIdx = rest.indexOf(WECOM_SESSION_TASK_SEP);
|
|
1556
|
+
const id = (sepIdx >= 0 ? rest.slice(0, sepIdx) : rest).trim();
|
|
1557
|
+
return id || null;
|
|
1558
|
+
}
|
|
1559
|
+
function parseRoyTaskId(cardTaskId) {
|
|
1560
|
+
if (!cardTaskId.startsWith(ROY_TASK_PREFIX))
|
|
1561
|
+
return null;
|
|
1562
|
+
const rest = cardTaskId.slice(ROY_TASK_PREFIX.length);
|
|
1563
|
+
const m = rest.match(/^(\d+)/);
|
|
1564
|
+
if (!m)
|
|
1565
|
+
return null;
|
|
1566
|
+
const n = Number(m[1]);
|
|
1567
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1568
|
+
}
|
|
1569
|
+
function extractWecomCardAction(frame) {
|
|
1570
|
+
if (!frame || typeof frame !== "object")
|
|
1571
|
+
return null;
|
|
1572
|
+
const f = frame;
|
|
1573
|
+
if (f.cmd !== "aibot_event_callback")
|
|
1574
|
+
return null;
|
|
1575
|
+
const body = f.body;
|
|
1576
|
+
if (!body || typeof body !== "object")
|
|
1577
|
+
return null;
|
|
1578
|
+
const event = body.event;
|
|
1579
|
+
if (event?.eventtype !== "template_card_event")
|
|
1580
|
+
return null;
|
|
1581
|
+
const tce = event.template_card_event;
|
|
1582
|
+
if (!tce)
|
|
1583
|
+
return null;
|
|
1584
|
+
const cardTaskId = String(tce.task_id ?? "");
|
|
1585
|
+
const from = body.from ?? {};
|
|
1586
|
+
return {
|
|
1587
|
+
eventKey: String(tce.event_key ?? ""),
|
|
1588
|
+
cardTaskId,
|
|
1589
|
+
taskId: parseRoyTaskId(cardTaskId),
|
|
1590
|
+
sessionId: parseRoySessionId(cardTaskId),
|
|
1591
|
+
userId: from.userid ?? "",
|
|
1592
|
+
reqId: f.headers?.req_id ?? ""
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
var ACTION_CARD_LABEL = {
|
|
1596
|
+
view: {
|
|
1597
|
+
statusLabel: "\uD83D\uDC40 已查看",
|
|
1598
|
+
note: "用户已查看任务"
|
|
1599
|
+
},
|
|
1600
|
+
mark_done: {
|
|
1601
|
+
statusLabel: "\uD83C\uDF89 已完成",
|
|
1602
|
+
note: "用户已确认任务完成"
|
|
1603
|
+
},
|
|
1604
|
+
reopen: {
|
|
1605
|
+
statusLabel: "\uD83D\uDD04 进行中",
|
|
1606
|
+
note: "用户已重新打开任务"
|
|
1607
|
+
},
|
|
1608
|
+
refresh_activity: {
|
|
1609
|
+
statusLabel: "⚡ 活性已刷新",
|
|
1610
|
+
note: "已读取子会话最新活性"
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
async function handleWecomCardAction(frame, ports) {
|
|
1614
|
+
const action = extractWecomCardAction(frame);
|
|
1615
|
+
if (!action) {
|
|
1616
|
+
return { ok: false, error: "not a template_card_event frame" };
|
|
1617
|
+
}
|
|
1618
|
+
const inflightKey = `${action.cardTaskId}:${action.eventKey}`;
|
|
1619
|
+
const inflight = cardActionInflight.get(inflightKey);
|
|
1620
|
+
if (inflight) {
|
|
1621
|
+
logger.warn(`handleWecomCardAction: ${inflightKey} 已有同操作在处理中` + `(疑似用户重复点击),本次忽略。`);
|
|
1622
|
+
return { ok: false, error: "同操作处理中,已忽略重复点击" };
|
|
1623
|
+
}
|
|
1624
|
+
const job = doHandle(action, ports).finally(() => {
|
|
1625
|
+
cardActionInflight.delete(inflightKey);
|
|
1626
|
+
});
|
|
1627
|
+
cardActionInflight.set(inflightKey, job);
|
|
1628
|
+
return job;
|
|
1629
|
+
}
|
|
1630
|
+
async function probeSessionActivityByAssociatedTask(taskId, ports) {
|
|
1631
|
+
if (!ports.findSessionCardByTaskId || !ports.loadSessionActivity) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const card = await ports.findSessionCardByTaskId(taskId);
|
|
1635
|
+
if (!card?.sessionId)
|
|
1636
|
+
return;
|
|
1637
|
+
return await ports.loadSessionActivity(card.sessionId);
|
|
1638
|
+
}
|
|
1639
|
+
async function doHandle(action, ports) {
|
|
1640
|
+
let actionType;
|
|
1641
|
+
if (action.eventKey === WecomCardActionKey.MARK_DONE) {
|
|
1642
|
+
actionType = "mark_done";
|
|
1643
|
+
} else if (action.eventKey === WecomCardActionKey.REOPEN) {
|
|
1644
|
+
actionType = "reopen";
|
|
1645
|
+
} else if (action.eventKey === WecomCardActionKey.VIEW) {
|
|
1646
|
+
actionType = "view";
|
|
1647
|
+
} else if (action.eventKey === WecomCardActionKey.REFRESH_ACTIVITY) {
|
|
1648
|
+
actionType = "refresh_activity";
|
|
1649
|
+
} else if (action.eventKey === WecomCardActionKey.CREATE_LARK_GROUP) {
|
|
1650
|
+
actionType = "create_lark_group";
|
|
1651
|
+
} else {
|
|
1652
|
+
return { ok: false, error: `unknown event_key: ${action.eventKey}` };
|
|
1653
|
+
}
|
|
1654
|
+
if (action.sessionId !== null) {
|
|
1655
|
+
if (actionType === "refresh_activity") {
|
|
1656
|
+
return await doHandleSessionActivity(action, ports);
|
|
1657
|
+
}
|
|
1658
|
+
if (actionType === "create_lark_group") {
|
|
1659
|
+
return await doHandleSessionCreateLarkGroup(action, ports);
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
ok: false,
|
|
1663
|
+
error: `action ${actionType} requires a task card (got session ${action.sessionId})`
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
if (action.taskId === null && actionType !== "view") {
|
|
1667
|
+
return {
|
|
1668
|
+
ok: false,
|
|
1669
|
+
error: `cannot parse roy task id from card task_id: ${action.cardTaskId}`
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
let opError;
|
|
1673
|
+
try {
|
|
1674
|
+
if (actionType === "mark_done" && action.taskId !== null) {
|
|
1675
|
+
await ports.markTaskDone(action.taskId);
|
|
1676
|
+
} else if (actionType === "reopen" && action.taskId !== null) {
|
|
1677
|
+
await ports.reopenTask(action.taskId);
|
|
1678
|
+
}
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
opError = err instanceof Error ? err.message : String(err);
|
|
1681
|
+
logger.error(`handleWecomCardAction: task op failed for #${action.taskId}: ${opError}`);
|
|
1682
|
+
}
|
|
1683
|
+
const label = ACTION_CARD_LABEL[actionType];
|
|
1684
|
+
const note = actionType === "view" || actionType === "refresh_activity" ? label.note : opError ? `${label.note}(操作失败:${opError})` : label.note;
|
|
1685
|
+
let timeline;
|
|
1686
|
+
if ((actionType === "view" || actionType === "reopen") && action.taskId !== null) {
|
|
1687
|
+
try {
|
|
1688
|
+
timeline = ports.loadTimeline?.(action.taskId) ?? [];
|
|
1689
|
+
if (timeline.length === 0) {
|
|
1690
|
+
logger.warn(`handleWecomCardAction: task #${action.taskId} 无时间线记录,` + `降级为简单按钮态更新`);
|
|
1691
|
+
}
|
|
1692
|
+
} catch (err) {
|
|
1693
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1694
|
+
logger.warn(`handleWecomCardAction: loadTimeline failed for ` + `task #${action.taskId}: ${msg}(降级为简单按钮态更新)`);
|
|
1695
|
+
timeline = [];
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
let taskExtra;
|
|
1699
|
+
try {
|
|
1700
|
+
const t = action.taskId !== null ? await ports.loadTaskTitle?.(action.taskId) : undefined;
|
|
1701
|
+
if (t && (t.title || t.number !== undefined)) {
|
|
1702
|
+
taskExtra = { taskNumber: t.number, taskTitle: t.title };
|
|
1703
|
+
}
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1706
|
+
logger.warn(`handleWecomCardAction: loadTaskTitle failed for ` + `task #${action.taskId}: ${msg}(降级为无标题更新)`);
|
|
1707
|
+
}
|
|
1708
|
+
let activity;
|
|
1709
|
+
let activityTerminal;
|
|
1710
|
+
if (actionType === "refresh_activity" && action.taskId !== null) {
|
|
1711
|
+
try {
|
|
1712
|
+
const r = await ports.loadSubSessionActivity?.(action.taskId);
|
|
1713
|
+
activity = r?.activity ?? null;
|
|
1714
|
+
activityTerminal = r?.terminal === true;
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1717
|
+
logger.warn(`handleWecomCardAction: loadSubSessionActivity failed for ` + `task #${action.taskId}: ${msg}(降级为「暂无数据」)`);
|
|
1718
|
+
activity = null;
|
|
1719
|
+
}
|
|
1720
|
+
if (activity === null) {
|
|
1721
|
+
try {
|
|
1722
|
+
const fallback = await probeSessionActivityByAssociatedTask(action.taskId, ports);
|
|
1723
|
+
if (fallback?.activity) {
|
|
1724
|
+
activity = fallback.activity;
|
|
1725
|
+
activityTerminal = fallback.terminal === true;
|
|
1726
|
+
logger.info(`handleWecomCardAction: task #${action.taskId} bgTask 反查失败,` + `已回退 session store probe(messageCount=${fallback.activity.messageCount})`);
|
|
1727
|
+
}
|
|
1728
|
+
} catch (err) {
|
|
1729
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1730
|
+
logger.warn(`handleWecomCardAction: session probe fallback failed for ` + `task #${action.taskId}: ${msg}(保持「暂无数据」)`);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
try {
|
|
1735
|
+
await ports.updateCard(action.reqId, action.cardTaskId, label.statusLabel, note, timeline, actionType, activity === undefined ? taskExtra : {
|
|
1736
|
+
...taskExtra ?? {},
|
|
1737
|
+
activity,
|
|
1738
|
+
...activityTerminal !== undefined ? { activityTerminal } : {}
|
|
1739
|
+
});
|
|
1740
|
+
} catch (err) {
|
|
1741
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1742
|
+
logger.error(`handleWecomCardAction: updateCard failed: ${msg}`);
|
|
1743
|
+
return {
|
|
1744
|
+
ok: false,
|
|
1745
|
+
action: actionType,
|
|
1746
|
+
error: `card update failed: ${msg}`
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
if (opError) {
|
|
1750
|
+
return { ok: false, action: actionType, error: opError };
|
|
1751
|
+
}
|
|
1752
|
+
logger.info(`handleWecomCardAction: task #${action.taskId} → ${actionType} ` + `(by ${action.userId})`);
|
|
1753
|
+
return { ok: true, action: actionType };
|
|
1754
|
+
}
|
|
1755
|
+
async function doHandleSessionActivity(action, ports) {
|
|
1756
|
+
const sessionId = action.sessionId;
|
|
1757
|
+
let state;
|
|
1758
|
+
try {
|
|
1759
|
+
state = await ports.loadSessionCardState?.(sessionId);
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
logger.warn(`handleWecomCardAction: loadSessionCardState failed for ${sessionId}: ` + `${err instanceof Error ? err.message : String(err)}(降级为仅活性)`);
|
|
1762
|
+
state = undefined;
|
|
1763
|
+
}
|
|
1764
|
+
if (!state) {
|
|
1765
|
+
logger.warn(`handleWecomCardAction: 无 session 卡持久层记录(${sessionId})` + ` ⇒ 刷新卡降级为仅活性(生命周期/操作记录不可用)`);
|
|
1766
|
+
}
|
|
1767
|
+
let activity = null;
|
|
1768
|
+
let probeTerminal;
|
|
1769
|
+
try {
|
|
1770
|
+
const r = await ports.loadSessionActivity?.(sessionId);
|
|
1771
|
+
activity = r?.activity ?? null;
|
|
1772
|
+
probeTerminal = r?.terminal;
|
|
1773
|
+
} catch (err) {
|
|
1774
|
+
logger.warn(`handleWecomCardAction: loadSessionActivity failed for ${sessionId}: ` + `${err instanceof Error ? err.message : String(err)}(降级为「暂无数据」)`);
|
|
1775
|
+
activity = null;
|
|
1776
|
+
}
|
|
1777
|
+
let sessionTask;
|
|
1778
|
+
try {
|
|
1779
|
+
const t = await ports.loadSessionTaskInfo?.(sessionId);
|
|
1780
|
+
if (t && typeof t.id === "number") {
|
|
1781
|
+
sessionTask = t;
|
|
1782
|
+
}
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
logger.warn(`handleWecomCardAction: loadSessionTaskInfo failed for ${sessionId}: ` + `${err instanceof Error ? err.message : String(err)}(降级为无任务行)`);
|
|
1785
|
+
}
|
|
1786
|
+
let linkedChatId;
|
|
1787
|
+
try {
|
|
1788
|
+
const g = await ports.findGroupChatForSession?.(sessionId);
|
|
1789
|
+
if (g && typeof g.chatId === "string" && g.chatId.trim()) {
|
|
1790
|
+
linkedChatId = g.chatId.trim();
|
|
1791
|
+
}
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
logger.warn(`handleWecomCardAction: findGroupChatForSession failed for ${sessionId}: ` + `${err instanceof Error ? err.message : String(err)}(当作没有群)`);
|
|
1794
|
+
}
|
|
1795
|
+
if (ports.updateSessionCard) {
|
|
1796
|
+
try {
|
|
1797
|
+
await ports.updateSessionCard(action.reqId, {
|
|
1798
|
+
sessionId,
|
|
1799
|
+
cardTaskId: action.cardTaskId,
|
|
1800
|
+
state,
|
|
1801
|
+
activity,
|
|
1802
|
+
...sessionTask ? { task: sessionTask } : {},
|
|
1803
|
+
...linkedChatId ? { perTaskChatId: linkedChatId } : {}
|
|
1804
|
+
});
|
|
1805
|
+
logger.info(`handleWecomCardAction: session ${sessionId} → refresh ` + `(lifecycle=${state?.lifecycle ?? "unknown"}, by ${action.userId})`);
|
|
1806
|
+
return { ok: true, action: "refresh_activity" };
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1809
|
+
logger.error(`handleWecomCardAction: session card update failed for ${sessionId}: ${msg}`);
|
|
1810
|
+
return {
|
|
1811
|
+
ok: false,
|
|
1812
|
+
action: "refresh_activity",
|
|
1813
|
+
error: `card update failed: ${msg}`
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
try {
|
|
1818
|
+
await ports.updateCard(action.reqId, action.cardTaskId, ACTION_CARD_LABEL.refresh_activity.statusLabel, ACTION_CARD_LABEL.refresh_activity.note, undefined, "refresh_activity", {
|
|
1819
|
+
activity,
|
|
1820
|
+
activityTerminal: state?.lifecycle !== undefined ? state.lifecycle !== "running" : probeTerminal ?? false
|
|
1821
|
+
});
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1824
|
+
logger.error(`handleWecomCardAction: session card update failed for ${sessionId}: ${msg}`);
|
|
1825
|
+
return { ok: false, action: "refresh_activity", error: `card update failed: ${msg}` };
|
|
1826
|
+
}
|
|
1827
|
+
logger.info(`handleWecomCardAction: session ${sessionId} → refresh ` + `(lifecycle=${state?.lifecycle ?? "unknown"}, by ${action.userId})`);
|
|
1828
|
+
return { ok: true, action: "refresh_activity" };
|
|
1829
|
+
}
|
|
1830
|
+
async function doHandleSessionCreateLarkGroup(action, ports) {
|
|
1831
|
+
const sessionId = action.sessionId;
|
|
1832
|
+
let existingChatId;
|
|
1833
|
+
try {
|
|
1834
|
+
const g = await ports.findGroupChatForSession?.(sessionId);
|
|
1835
|
+
if (g && typeof g.chatId === "string" && g.chatId.trim()) {
|
|
1836
|
+
existingChatId = g.chatId.trim();
|
|
1837
|
+
}
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
logger.warn(`handleWecomCardAction: findGroupChatForSession(${sessionId}) 失败` + `(当作没有群): ${err instanceof Error ? err.message : String(err)}`);
|
|
1840
|
+
}
|
|
1841
|
+
if (existingChatId) {
|
|
1842
|
+
logger.info(`handleWecomCardAction: session ${sessionId} 已有飞书群 ` + `${existingChatId}(幂等命中,不重复建群)`);
|
|
1843
|
+
await updateSessionCardWithGroup(ports, action, sessionId, existingChatId);
|
|
1844
|
+
return { ok: true, action: "create_lark_group", newChatId: existingChatId };
|
|
1845
|
+
}
|
|
1846
|
+
if (!ports.createLarkGroupForSession) {
|
|
1847
|
+
return {
|
|
1848
|
+
ok: false,
|
|
1849
|
+
action: "create_lark_group",
|
|
1850
|
+
error: "ports.createLarkGroupForSession not injected " + "(wecom-cli-handler wiring missing)"
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
let chatId;
|
|
1854
|
+
try {
|
|
1855
|
+
chatId = await ports.createLarkGroupForSession(sessionId);
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1858
|
+
logger.error(`handleWecomCardAction: createLarkGroupForSession(${sessionId}) ` + `failed: ${msg}`);
|
|
1859
|
+
await updateSessionCardWithGroup(ports, action, sessionId, undefined, `⚠️ 建群失败:${msg.slice(0, 80)}`);
|
|
1860
|
+
return { ok: false, action: "create_lark_group", error: msg };
|
|
1861
|
+
}
|
|
1862
|
+
logger.info(`handleWecomCardAction: session ${sessionId} → 飞书群 ${chatId} ` + `创建成功(by ${action.userId})`);
|
|
1863
|
+
await updateSessionCardWithGroup(ports, action, sessionId, chatId);
|
|
1864
|
+
if (ports.notifyGroupLink) {
|
|
1865
|
+
try {
|
|
1866
|
+
await ports.notifyGroupLink({
|
|
1867
|
+
sessionId,
|
|
1868
|
+
chatId,
|
|
1869
|
+
...action.userId ? { userId: action.userId } : {}
|
|
1870
|
+
});
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
logger.warn(`handleWecomCardAction: notifyGroupLink(${sessionId} → ${chatId}) ` + `失败(不阻断建群结果): ` + `${err instanceof Error ? err.message : String(err)}`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return { ok: true, action: "create_lark_group", newChatId: chatId };
|
|
1876
|
+
}
|
|
1877
|
+
async function updateSessionCardWithGroup(ports, action, sessionId, chatId, errorHint) {
|
|
1878
|
+
if (!ports.updateSessionCard)
|
|
1879
|
+
return;
|
|
1880
|
+
try {
|
|
1881
|
+
let state;
|
|
1882
|
+
try {
|
|
1883
|
+
state = await ports.loadSessionCardState?.(sessionId);
|
|
1884
|
+
} catch {
|
|
1885
|
+
state = undefined;
|
|
1886
|
+
}
|
|
1887
|
+
let activity = null;
|
|
1888
|
+
try {
|
|
1889
|
+
const r = await ports.loadSessionActivity?.(sessionId);
|
|
1890
|
+
activity = r?.activity ?? null;
|
|
1891
|
+
} catch {
|
|
1892
|
+
activity = null;
|
|
1893
|
+
}
|
|
1894
|
+
await ports.updateSessionCard(action.reqId, {
|
|
1895
|
+
sessionId,
|
|
1896
|
+
cardTaskId: action.cardTaskId,
|
|
1897
|
+
...state ? { state } : {},
|
|
1898
|
+
activity,
|
|
1899
|
+
...chatId ? { perTaskChatId: chatId } : {},
|
|
1900
|
+
...errorHint ? { errorHint } : {}
|
|
1901
|
+
});
|
|
1902
|
+
logger.info(`handleWecomCardAction: session card updated after group change ` + `(session=${sessionId}, chatId=${chatId ?? "none"})`);
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
logger.warn(`handleWecomCardAction: session card update after group change ` + `failed for ${sessionId}(不阻断建群结果): ` + `${err instanceof Error ? err.message : String(err)}`);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function buildWecomCardUpdateBody(input) {
|
|
1908
|
+
const refreshable = input.action === "view" || input.action === "reopen";
|
|
1909
|
+
const activityRefreshable = input.action === "refresh_activity";
|
|
1910
|
+
const frame = buildWecomCardUpdateFrame({
|
|
1911
|
+
reqId: "placeholder",
|
|
1912
|
+
taskId: input.cardTaskId,
|
|
1913
|
+
statusLabel: input.statusLabel,
|
|
1914
|
+
note: input.note,
|
|
1915
|
+
timeline: input.timeline,
|
|
1916
|
+
taskTitle: input.taskTitle,
|
|
1917
|
+
taskNumber: input.taskNumber,
|
|
1918
|
+
buttonOverrides: refreshable ? [{ text: "刷新详情", key: WecomCardActionKey.VIEW, style: 1 }] : activityRefreshable ? [
|
|
1919
|
+
{ text: "刷新详情", key: WecomCardActionKey.VIEW, style: 2 },
|
|
1920
|
+
{ text: "刷新活性", key: WecomCardActionKey.REFRESH_ACTIVITY, style: 1 }
|
|
1921
|
+
] : [{ text: "已处理", key: "task_done_ack", style: 4 }],
|
|
1922
|
+
...input.activity !== undefined ? { activity: input.activity } : {},
|
|
1923
|
+
...input.activityTerminal !== undefined ? { activityTerminal: input.activityTerminal } : {}
|
|
1924
|
+
});
|
|
1925
|
+
return { cmd: frame.cmd, body: frame.body };
|
|
1926
|
+
}
|
|
1927
|
+
function buildWecomSessionCardUpdateBody(input) {
|
|
1928
|
+
const state = input.state;
|
|
1929
|
+
const lifecycle = state?.lifecycle ?? "unknown";
|
|
1930
|
+
let milestones;
|
|
1931
|
+
if (state?.milestones) {
|
|
1932
|
+
try {
|
|
1933
|
+
const arr = JSON.parse(state.milestones);
|
|
1934
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
1935
|
+
milestones = arr;
|
|
1936
|
+
}
|
|
1937
|
+
} catch {
|
|
1938
|
+
milestones = undefined;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const task = input.task ?? (state?.associatedTaskId !== undefined ? { id: state.associatedTaskId } : undefined);
|
|
1942
|
+
const frame = buildWecomSessionUpdateFrame({
|
|
1943
|
+
reqId: input.reqIdForTest ?? "placeholder",
|
|
1944
|
+
cardTaskId: input.cardTaskId,
|
|
1945
|
+
sessionId: input.sessionId,
|
|
1946
|
+
lifecycle,
|
|
1947
|
+
...state?.title ? { title: state.title } : {},
|
|
1948
|
+
...state?.agentType ? { agentType: state.agentType } : {},
|
|
1949
|
+
...state?.startedAt ? { startedAt: state.startedAt } : {},
|
|
1950
|
+
...task ? { task } : {},
|
|
1951
|
+
...milestones ? { milestones } : {},
|
|
1952
|
+
...input.perTaskChatId ? { perTaskChatId: input.perTaskChatId } : {},
|
|
1953
|
+
...input.errorHint ? { errorHint: input.errorHint } : {},
|
|
1954
|
+
...input.activity !== undefined ? { activity: input.activity } : {},
|
|
1955
|
+
...input.now !== undefined ? { now: input.now } : {}
|
|
1956
|
+
});
|
|
1957
|
+
return { cmd: frame.cmd, body: frame.body };
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/env/event-source/wecom-media.ts
|
|
1961
|
+
import { createDecipheriv } from "crypto";
|
|
1962
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
1963
|
+
import { basename, join, resolve } from "path";
|
|
1964
|
+
var WECOM_MEDIA_DIR = "/tmp/wecom-media";
|
|
1965
|
+
var WECOM_MEDIA_BLOCK_SIZE = 32;
|
|
1966
|
+
var mediaDirOverride;
|
|
1967
|
+
function getWecomMediaDir() {
|
|
1968
|
+
return mediaDirOverride ?? WECOM_MEDIA_DIR;
|
|
1969
|
+
}
|
|
1970
|
+
function decryptWecomMedia(encrypted, aesKeyBase64) {
|
|
1971
|
+
if (!encrypted || encrypted.length === 0) {
|
|
1972
|
+
throw new Error("decryptWecomMedia: encrypted buffer is empty");
|
|
1973
|
+
}
|
|
1974
|
+
if (!aesKeyBase64 || typeof aesKeyBase64 !== "string") {
|
|
1975
|
+
throw new Error("decryptWecomMedia: aesKey must be a non-empty string");
|
|
1976
|
+
}
|
|
1977
|
+
const key = Buffer.from(aesKeyBase64, "base64");
|
|
1978
|
+
if (key.length !== 32) {
|
|
1979
|
+
throw new Error(`decryptWecomMedia: aesKey must decode to 32 bytes (got ${key.length})`);
|
|
1980
|
+
}
|
|
1981
|
+
const iv = key.subarray(0, 16);
|
|
1982
|
+
const decipher = createDecipheriv("aes-256-cbc", key, iv);
|
|
1983
|
+
decipher.setAutoPadding(false);
|
|
1984
|
+
const decrypted = Buffer.concat([
|
|
1985
|
+
decipher.update(encrypted),
|
|
1986
|
+
decipher.final()
|
|
1987
|
+
]);
|
|
1988
|
+
const padLen = decrypted[decrypted.length - 1];
|
|
1989
|
+
if (padLen < 1 || padLen > WECOM_MEDIA_BLOCK_SIZE || padLen > decrypted.length) {
|
|
1990
|
+
throw new Error(`decryptWecomMedia: invalid PKCS#7 padding value: ${padLen}`);
|
|
1991
|
+
}
|
|
1992
|
+
for (let i = decrypted.length - padLen;i < decrypted.length; i++) {
|
|
1993
|
+
if (decrypted[i] !== padLen) {
|
|
1994
|
+
throw new Error("decryptWecomMedia: invalid PKCS#7 padding (bytes mismatch)");
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return decrypted.subarray(0, decrypted.length - padLen);
|
|
1998
|
+
}
|
|
1999
|
+
function refFromField(kind, field) {
|
|
2000
|
+
if (!field || typeof field !== "object")
|
|
2001
|
+
return null;
|
|
2002
|
+
const url = typeof field.url === "string" ? field.url : undefined;
|
|
2003
|
+
const aeskey = typeof field.aeskey === "string" ? field.aeskey : undefined;
|
|
2004
|
+
if (!url || !aeskey)
|
|
2005
|
+
return null;
|
|
2006
|
+
return {
|
|
2007
|
+
kind,
|
|
2008
|
+
url,
|
|
2009
|
+
aeskey,
|
|
2010
|
+
filename: typeof field.filename === "string" ? field.filename : undefined
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
function extractWecomMediaRefs(body) {
|
|
2014
|
+
const refs = [];
|
|
2015
|
+
const msgType = String(body.msgtype ?? "");
|
|
2016
|
+
const single = refFromField(msgType, body[msgType]);
|
|
2017
|
+
if (single)
|
|
2018
|
+
refs.push(single);
|
|
2019
|
+
if (msgType === "mixed") {
|
|
2020
|
+
const mixed = body.mixed;
|
|
2021
|
+
for (const item of mixed?.msg_item ?? []) {
|
|
2022
|
+
if (!item || typeof item !== "object")
|
|
2023
|
+
continue;
|
|
2024
|
+
const it = item;
|
|
2025
|
+
const itemType = String(it.msgtype ?? "");
|
|
2026
|
+
const ref = refFromField(itemType, it[itemType]);
|
|
2027
|
+
if (ref)
|
|
2028
|
+
refs.push(ref);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return refs;
|
|
2032
|
+
}
|
|
2033
|
+
async function defaultWecomMediaFetcher(url) {
|
|
2034
|
+
const resp = await fetch(url);
|
|
2035
|
+
const status = resp.status;
|
|
2036
|
+
if (!resp.ok) {
|
|
2037
|
+
return { ok: false, status, buffer: Buffer.alloc(0) };
|
|
2038
|
+
}
|
|
2039
|
+
const ab = await resp.arrayBuffer();
|
|
2040
|
+
const disposition = resp.headers.get("content-disposition") ?? "";
|
|
2041
|
+
let filename;
|
|
2042
|
+
const utf8Match = disposition.match(/filename\*=UTF-8''([^;\s]+)/i);
|
|
2043
|
+
if (utf8Match) {
|
|
2044
|
+
filename = decodeURIComponent(utf8Match[1]);
|
|
2045
|
+
} else {
|
|
2046
|
+
const m = disposition.match(/filename="?([^";\s]+)"?/i);
|
|
2047
|
+
if (m)
|
|
2048
|
+
filename = decodeURIComponent(m[1]);
|
|
2049
|
+
}
|
|
2050
|
+
return {
|
|
2051
|
+
ok: true,
|
|
2052
|
+
status,
|
|
2053
|
+
buffer: Buffer.from(ab),
|
|
2054
|
+
filename
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
async function fetchAndSaveWecomMedia(input) {
|
|
2058
|
+
const fetcher = input.fetcher ?? defaultWecomMediaFetcher;
|
|
2059
|
+
const result = await fetcher(input.url);
|
|
2060
|
+
if (!result.ok) {
|
|
2061
|
+
throw new Error("fetchAndSaveWecomMedia: download failed (HTTP " + result.status + ")" + " for msgid=" + input.msgid + " url=" + input.url.slice(0, 120));
|
|
2062
|
+
}
|
|
2063
|
+
const decrypted = decryptWecomMedia(result.buffer, input.aeskey);
|
|
2064
|
+
return saveWecomMediaFile({
|
|
2065
|
+
msgid: input.msgid,
|
|
2066
|
+
filename: input.filename ?? result.filename,
|
|
2067
|
+
buffer: decrypted,
|
|
2068
|
+
chatId: input.chatId
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
async function saveWecomMediaFile(input) {
|
|
2072
|
+
const root = getWecomMediaDir();
|
|
2073
|
+
mkdirSync(root, { recursive: true, mode: 448 });
|
|
2074
|
+
const safeMsgid = input.msgid.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
|
|
2075
|
+
const safeName = input.filename ? basename(input.filename) : `${safeMsgid}.bin`;
|
|
2076
|
+
const safeChatId = input.chatId ? input.chatId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 120) : undefined;
|
|
2077
|
+
const dir = safeChatId ? join(resolve(root), safeChatId) : resolve(root);
|
|
2078
|
+
if (safeChatId)
|
|
2079
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2080
|
+
const filePath = join(dir, `${safeMsgid}_${safeName}`);
|
|
2081
|
+
writeFileSync(filePath, input.buffer);
|
|
2082
|
+
return filePath;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/env/event-source/wecom-cli-handler.ts
|
|
2086
|
+
var WECOM_CLI_ES_TYPE = "wecom-cli";
|
|
2087
|
+
var WECOM_CHAT_ID_PREFIX = "wecom:";
|
|
2088
|
+
var WECOM_WS_DEFAULT_URL = "wss://openws.work.weixin.qq.com";
|
|
2089
|
+
function readWecomCliConfig(config) {
|
|
2090
|
+
const top = config;
|
|
2091
|
+
const opts = config.options ?? {};
|
|
2092
|
+
return {
|
|
2093
|
+
botId: top.botId || opts.botId || undefined,
|
|
2094
|
+
secret: top.secret || opts.secret || undefined,
|
|
2095
|
+
command: top.command || opts.command || undefined,
|
|
2096
|
+
webhook: top.webhook || opts.webhook || undefined,
|
|
2097
|
+
credentialsPath: top.credentialsPath || opts.credentialsPath || undefined
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
var WECOM_CREDENTIALS_DEFAULT_PATH = join2(process.env.HOME || "/tmp", ".roy-agent", "wecom-credentials.json");
|
|
2101
|
+
function readWecomCredentialsFile(path) {
|
|
2102
|
+
try {
|
|
2103
|
+
const raw = readFileSync(path, "utf-8");
|
|
2104
|
+
const parsed = JSON.parse(raw);
|
|
2105
|
+
const agentId = typeof parsed.agent_id === "string" ? parsed.agent_id : undefined;
|
|
2106
|
+
const botId = typeof parsed.bot_id === "string" ? parsed.bot_id : undefined;
|
|
2107
|
+
const secret = typeof parsed.secret === "string" ? parsed.secret : undefined;
|
|
2108
|
+
const id = botId || agentId;
|
|
2109
|
+
if (!id || !secret)
|
|
2110
|
+
return null;
|
|
2111
|
+
return { botId: id, secret };
|
|
2112
|
+
} catch {
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function resolveWecomCredentials(config, credentialsPath = WECOM_CREDENTIALS_DEFAULT_PATH) {
|
|
2117
|
+
const cliConfig = readWecomCliConfig(config);
|
|
2118
|
+
if (cliConfig.botId && cliConfig.secret) {
|
|
2119
|
+
return { botId: cliConfig.botId, secret: cliConfig.secret };
|
|
2120
|
+
}
|
|
2121
|
+
const fromFile = readWecomCredentialsFile(cliConfig.credentialsPath ?? credentialsPath);
|
|
2122
|
+
if (fromFile)
|
|
2123
|
+
return fromFile;
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
function buildWecomReplyChannel(input) {
|
|
2127
|
+
const chatType = input.chatType === "group" ? "group" : "single";
|
|
2128
|
+
const userId = input.userId?.trim() || undefined;
|
|
2129
|
+
const rawChatId = input.chatId?.trim() || undefined;
|
|
2130
|
+
const chatId = rawChatId ?? userId ?? `${WECOM_CHAT_ID_PREFIX}unknown`;
|
|
2131
|
+
return {
|
|
2132
|
+
type: WECOM_CLI_ES_TYPE,
|
|
2133
|
+
chatId,
|
|
2134
|
+
messageId: input.messageId?.trim() || undefined,
|
|
2135
|
+
params: {
|
|
2136
|
+
chatType,
|
|
2137
|
+
userId,
|
|
2138
|
+
wecomChatId: rawChatId,
|
|
2139
|
+
wecomUserId: userId,
|
|
2140
|
+
...input.sourceId ? { sourceId: input.sourceId } : {},
|
|
2141
|
+
...input.reqId ? { reqId: input.reqId } : {},
|
|
2142
|
+
statusNotifications: input.statusNotifications === true
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
function hasWecomMediaRef(body, kind) {
|
|
2147
|
+
const field = body[kind];
|
|
2148
|
+
return !!field && typeof field === "object" && typeof field.url === "string" && typeof field.aeskey === "string";
|
|
2149
|
+
}
|
|
2150
|
+
function extractText(body) {
|
|
2151
|
+
const msgType = String(body.msgtype ?? "");
|
|
2152
|
+
switch (msgType) {
|
|
2153
|
+
case "text": {
|
|
2154
|
+
const t = body.text;
|
|
2155
|
+
return t?.content ?? "";
|
|
2156
|
+
}
|
|
2157
|
+
case "mixed": {
|
|
2158
|
+
const mixed = body.mixed;
|
|
2159
|
+
const items = mixed?.msg_item ?? [];
|
|
2160
|
+
const parts = [];
|
|
2161
|
+
for (const it of items) {
|
|
2162
|
+
const item = it;
|
|
2163
|
+
if (item?.msgtype === "text" && item.text?.content) {
|
|
2164
|
+
parts.push(item.text.content);
|
|
2165
|
+
} else if (item?.msgtype) {
|
|
2166
|
+
parts.push(`[${item.msgtype}]`);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
return parts.join(`
|
|
2170
|
+
`);
|
|
2171
|
+
}
|
|
2172
|
+
case "image":
|
|
2173
|
+
return hasWecomMediaRef(body, "image") ? "[image](media:downloading)" : "[image]";
|
|
2174
|
+
case "voice":
|
|
2175
|
+
return hasWecomMediaRef(body, "voice") ? "[voice](media:downloading)" : "[voice]";
|
|
2176
|
+
case "file":
|
|
2177
|
+
return hasWecomMediaRef(body, "file") ? "[file](media:downloading)" : "[file]";
|
|
2178
|
+
case "video":
|
|
2179
|
+
return hasWecomMediaRef(body, "video") ? "[video](media:downloading)" : "[video]";
|
|
2180
|
+
default:
|
|
2181
|
+
return msgType ? `[${msgType}]` : "";
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function extractWecomQuotedText(body) {
|
|
2185
|
+
const quote = body.quote;
|
|
2186
|
+
if (!quote || typeof quote !== "object")
|
|
2187
|
+
return;
|
|
2188
|
+
const msgType = String(quote.msgtype ?? "");
|
|
2189
|
+
if (msgType === "text") {
|
|
2190
|
+
const content = quote.text?.content;
|
|
2191
|
+
return typeof content === "string" && content ? content : undefined;
|
|
2192
|
+
}
|
|
2193
|
+
return msgType ? `[${msgType}]` : undefined;
|
|
2194
|
+
}
|
|
2195
|
+
function buildWecomUserMessage(text, replyContext) {
|
|
2196
|
+
const quotedText = replyContext?.quotedText;
|
|
2197
|
+
if (!quotedText)
|
|
2198
|
+
return text;
|
|
2199
|
+
return `${text}
|
|
2200
|
+
|
|
2201
|
+
[引用消息] ${quotedText}`;
|
|
2202
|
+
}
|
|
2203
|
+
function parseWecomFrame(raw) {
|
|
2204
|
+
if (!raw || typeof raw !== "object")
|
|
2205
|
+
return null;
|
|
2206
|
+
const frame = raw;
|
|
2207
|
+
const cmd = String(frame.cmd ?? "");
|
|
2208
|
+
const body = frame.body;
|
|
2209
|
+
if (!body || typeof body !== "object")
|
|
2210
|
+
return null;
|
|
2211
|
+
const msgid = typeof body.msgid === "string" ? body.msgid : undefined;
|
|
2212
|
+
const isMsg = cmd === "aibot_msg_callback";
|
|
2213
|
+
const isEvent = cmd === "aibot_event_callback";
|
|
2214
|
+
if (!isMsg && !isEvent)
|
|
2215
|
+
return null;
|
|
2216
|
+
if (isMsg && !msgid)
|
|
2217
|
+
return null;
|
|
2218
|
+
const msgType = String(body.msgtype ?? (isEvent ? "event" : ""));
|
|
2219
|
+
const from = body.from ?? {};
|
|
2220
|
+
const senderId = from.userid ?? "";
|
|
2221
|
+
const chattype = typeof body.chattype === "string" ? body.chattype : undefined;
|
|
2222
|
+
const rawChatId = typeof body.chatid === "string" ? body.chatid : undefined;
|
|
2223
|
+
let eventType;
|
|
2224
|
+
let text;
|
|
2225
|
+
if (isEvent) {
|
|
2226
|
+
const event = body.event ?? {};
|
|
2227
|
+
const eventTypeName = event.eventtype ?? "unknown";
|
|
2228
|
+
eventType = `wecom.event.${eventTypeName}`;
|
|
2229
|
+
text = `[wecom-event:${eventTypeName}]`;
|
|
2230
|
+
} else {
|
|
2231
|
+
eventType = "wecom.message.receive";
|
|
2232
|
+
text = extractText(body);
|
|
2233
|
+
}
|
|
2234
|
+
const metadata = {
|
|
2235
|
+
sourceType: WECOM_CLI_ES_TYPE,
|
|
2236
|
+
eventType,
|
|
2237
|
+
eventId: msgid,
|
|
2238
|
+
appId: typeof body.aibotid === "string" ? body.aibotid : undefined,
|
|
2239
|
+
chatId: rawChatId ?? senderId ?? undefined,
|
|
2240
|
+
chatType: chattype,
|
|
2241
|
+
messageId: msgid,
|
|
2242
|
+
messageType: msgType,
|
|
2243
|
+
senderId,
|
|
2244
|
+
sourceTime: typeof body.create_time === "number" ? new Date(body.create_time * 1000).toISOString() : undefined,
|
|
2245
|
+
payload: body
|
|
2246
|
+
};
|
|
2247
|
+
const quotedText = extractWecomQuotedText(body);
|
|
2248
|
+
if (quotedText) {
|
|
2249
|
+
metadata.replyContext = { quotedText };
|
|
2250
|
+
}
|
|
2251
|
+
return {
|
|
2252
|
+
metadata,
|
|
2253
|
+
text,
|
|
2254
|
+
reqId: frame.headers?.req_id
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
var WECOM_DEFAULT_EXTEND_TOOLS = ["wecom_respond"];
|
|
2258
|
+
function resolveWecomExtendTools(config) {
|
|
2259
|
+
const out = [];
|
|
2260
|
+
const push = (name) => {
|
|
2261
|
+
if (typeof name !== "string")
|
|
2262
|
+
return;
|
|
2263
|
+
const trimmed = name.trim();
|
|
2264
|
+
if (trimmed.length === 0)
|
|
2265
|
+
return;
|
|
2266
|
+
if (!out.includes(trimmed))
|
|
2267
|
+
out.push(trimmed);
|
|
2268
|
+
};
|
|
2269
|
+
WECOM_DEFAULT_EXTEND_TOOLS.forEach(push);
|
|
2270
|
+
(config?.extendTools ?? []).forEach(push);
|
|
2271
|
+
return out;
|
|
2272
|
+
}
|
|
2273
|
+
var WECOM_CHANNEL_PROMPT_SECTION_TITLE = "⭐ 企业微信回复规范(system prompt 注入版)";
|
|
2274
|
+
function buildWecomChannelSystemPrompt(opts) {
|
|
2275
|
+
const botLine = opts.botId ? `本渠道智能机器人 BotID:${opts.botId}。` : `本渠道未配置 BotID(使用事件回调里的 aibotid)。`;
|
|
2276
|
+
return [
|
|
2277
|
+
`# ${WECOM_CHANNEL_PROMPT_SECTION_TITLE}`,
|
|
2278
|
+
``,
|
|
2279
|
+
`本节是企业微信渠道的**回复规范**(静态规则,随 system prompt 注入)。`,
|
|
2280
|
+
`每条企微事件的 \`recommendedAction\` 只带动态定位(chatId / messageId),`,
|
|
2281
|
+
`所有回复规则都在本节 —— replyChannel 给出的 chatId 是本轮唯一权威定位。`,
|
|
2282
|
+
botLine,
|
|
2283
|
+
``,
|
|
2284
|
+
`## 回复命令(唯一出站通道)`,
|
|
2285
|
+
`用 \`wecom_respond\` 工具回复。工具内部**主路径是官方 CLI**`,
|
|
2286
|
+
`(\`wecom-cli message aibot send --msg-type markdown\`,已扫码授权),`,
|
|
2287
|
+
`CLI 失败时自动降级为 WS 长连接回帧(aibot_respond_msg)。`,
|
|
2288
|
+
`- \`content\`:**必填**,回复正文(markdown 格式)。`,
|
|
2289
|
+
`- \`chat_id\`:可选;缺省自动取本事件的 chatId/senderId(单聊=对方 userid,群聊=chatid)。`,
|
|
2290
|
+
`- \`req_id\`:可选;缺省取 metadata.reqId(仅 WS fallback 用)。`,
|
|
2291
|
+
`- ⚠️ 不要自己拼 \`wecom-cli\` 命令 —— 统一走 \`wecom_respond\`(chat_id 需要内部解析成真实 userid,手工拼会报 40073)。`,
|
|
2292
|
+
`- ⚠️ CLI 没有 text 类型;markdown 上限 20480 UTF-8 字节,超长分段压缩,不要静默截断。`,
|
|
2293
|
+
`- 工具返回 success=false 时如实告知用户,不要假装已发送。`,
|
|
2294
|
+
``,
|
|
2295
|
+
`## 出站纪律`,
|
|
2296
|
+
`- ❌ 不要用 \`curl\` 直连 \`qyapi.weixin.qq.com\`(绕过 CLI 丢鉴权与错误规范)。`,
|
|
2297
|
+
`- ❌ 不要用 \`lark-cli\` 的任何 flag(--profile / --msg-type / --as bot 都不适用于企微)。`,
|
|
2298
|
+
`- ❌ 整次回复只发**一条**消息:先想清楚再发,不要拆段循环 send 刷屏。`,
|
|
2299
|
+
``,
|
|
2300
|
+
`## 文档交付规范`,
|
|
2301
|
+
`- 文档类生成(报告 / 代码 / 长文):企微消息里给**结论 + 关键内容摘要**,`,
|
|
2302
|
+
` 完整文档内容直接放进 markdown 正文(企微支持 markdown 渲染),`,
|
|
2303
|
+
` **不要只给本地文件路径** —— 企微用户无法访问 roy-agent 所在机器的文件系统。`,
|
|
2304
|
+
`- **调研文档 / docs / ppt 等文档类产出必须上传到企微文档,链接作为交付物**:`,
|
|
2305
|
+
` - 在线文档:\`wecom-cli doc create --doc-name <标题> --doc-type doc --content-type text --file-path <本地md>\``,
|
|
2306
|
+
` (或 --content 传纯文本;ppt 用 --doc-type ppt),取返回里的文档链接`,
|
|
2307
|
+
` - 微盘文件:\`wecom-cli disk files upload --file-path <本地文件>\`(默认传到我的空间),取返回里的文件链接`,
|
|
2308
|
+
` - 本地路径只作中转(生成 → 上传企微文档 → 回复里贴链接),**不落本地路径**交付`,
|
|
2309
|
+
``,
|
|
2310
|
+
`## 图片/文件消息处理(Task #5199)`,
|
|
2311
|
+
`- 用户发图片时,消息正文是 \`[image](media:downloading)\` 占位;`,
|
|
2312
|
+
` 媒体文件已自动下载解密落盘,本地路径在事件 metadata.mediaFiles`,
|
|
2313
|
+
` (数组,如 \`/tmp/wecom-media/<chatId>/<msgid>_photo.png\`)。`,
|
|
2314
|
+
`- ⚠️ **只用 metadata.mediaFiles 里列出的路径**(Task #5313)——`,
|
|
2315
|
+
` 禁止 \`ls\`/\`find\` 扫媒体目录:同目录下的历史文件属于别的消息,`,
|
|
2316
|
+
` 扫目录会把它们误当本条消息的附件。`,
|
|
2317
|
+
`- **看图**:用 \`understand_image\` 工具,\`image_source\` 传`,
|
|
2318
|
+
` metadata.mediaFiles 里的本地路径,\`prompt\` 写想了解什么。`,
|
|
2319
|
+
`- mediaFiles 缺失(下载失败/URL 过期)时如实告知用户,不要编造图片内容。`,
|
|
2320
|
+
`- ⚠️ 截图内容边界(Task #5317):截图/图片里可能内嵌`,
|
|
2321
|
+
` **其他群聊或其他会话的聊天记录**(如聊天记录截图、转发图)。`,
|
|
2322
|
+
` 截图里的聊天记录**不是本群的对话上下文**——分析图片时如需`,
|
|
2323
|
+
` 提及其中内容,必须明确标注「截图里包含的内容」,`,
|
|
2324
|
+
` 禁止把截图内的聊天记录当作本群刚发生的消息来转述或回应。`,
|
|
2325
|
+
``,
|
|
2326
|
+
`## 引用消息上下文 replyContext`,
|
|
2327
|
+
`本条消息引用了其他消息时,user message 末尾会带 \`[引用消息] <原文>\` 行;`,
|
|
2328
|
+
`同时 recommendedAction.replyContext.quotedText 也有同一份内容(非文本引用为 [image] 等占位)。`,
|
|
2329
|
+
`- 回复时优先围绕被引用内容作答,不要忽略引用语境。`
|
|
2330
|
+
].join(`
|
|
2331
|
+
`);
|
|
2332
|
+
}
|
|
2333
|
+
function buildWecomNotifyPrompt(input) {
|
|
2334
|
+
const isGroup = input.chatType === "group";
|
|
2335
|
+
const target = isGroup ? `群聊 chat_id = "${input.chatId ?? "<chatid>"}"` : `单聊 chat_id = "${input.userId ?? "<userid>"}"(企微单聊用对方 userid 作 chat_id)`;
|
|
2336
|
+
return [
|
|
2337
|
+
`### ① 用 wecom_respond 工具回消息
|
|
2338
|
+
` + `本事件来自企业微信(WeCom)。回复用户请用 \`wecom_respond\` 工具(主路径 = 官方 wecom-cli 已授权,CLI 失败自动降级 WS 回帧),目标:${target}。
|
|
2339
|
+
` + `不要使用 lark-cli —— 那是飞书的通道,参数完全不同。`,
|
|
2340
|
+
`### ② 具体参数
|
|
2341
|
+
` + `调用 \`wecom_respond\` 工具:
|
|
2342
|
+
` + `- \`content\`:**必填**,回复正文(markdown)
|
|
2343
|
+
` + `- \`chat_id\`:可选;缺省自动取本事件 chatId / senderId(单聊 = 对方 userid)
|
|
2344
|
+
` + `- \`req_id\`:可选;缺省取 metadata.reqId(仅 WS fallback 用)
|
|
2345
|
+
` + `- ⚠️ 不要自己拼 \`wecom-cli\` 命令 —— chat_id 需内部解析成真实 userid,手拼会报 40073`,
|
|
2346
|
+
`### ③ 禁止的做法
|
|
2347
|
+
` + `- ❌ 不要用 \`curl\` 直连 \`qyapi.weixin.qq.com\` —— 绕过官方通道会丢鉴权与错误规范
|
|
2348
|
+
` + `- ❌ 不要用 \`lark-cli\` 的任何 flag(--profile / --msg-type post / --content)
|
|
2349
|
+
` + `- ❌ 不要自己编 chat_id / req_id —— 用工具默认值即可
|
|
2350
|
+
` + `- ❌ 不要在失败时假装已发送`,
|
|
2351
|
+
`### ④ 怎么判断成功
|
|
2352
|
+
` + `- 工具返回 \`success: true\` = 回帧已发出
|
|
2353
|
+
` + `- 工具返回 \`success: false\` = 发送失败(读 \`error\` 字段),如实告知用户失败原因`
|
|
2354
|
+
].join(`
|
|
2355
|
+
|
|
2356
|
+
`);
|
|
2357
|
+
}
|
|
2358
|
+
function resolveWecomBinary(binName = "wecom-cli") {
|
|
2359
|
+
if (isAbsolute(binName)) {
|
|
2360
|
+
return existsSync(binName) ? { found: true, path: binName } : { found: false };
|
|
2361
|
+
}
|
|
2362
|
+
if (binName.includes("/")) {
|
|
2363
|
+
return existsSync(binName) ? { found: true, path: binName } : { found: false };
|
|
2364
|
+
}
|
|
2365
|
+
const pathEnv = process.env.PATH ?? "";
|
|
2366
|
+
const candidates = [binName, `${binName}.exe`, `${binName}.cmd`, `${binName}.bat`];
|
|
2367
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
2368
|
+
if (!dir)
|
|
2369
|
+
continue;
|
|
2370
|
+
for (const candidate of candidates) {
|
|
2371
|
+
const full = join2(dir, candidate);
|
|
2372
|
+
if (existsSync(full))
|
|
2373
|
+
return { found: true, path: full };
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
return { found: false };
|
|
2377
|
+
}
|
|
2378
|
+
function probeCommandBinary(command) {
|
|
2379
|
+
const stripped = command.trim().replace(/^exec\s+/, "");
|
|
2380
|
+
const firstToken = (stripped.match(/^"[^"]+"|^'[^']+'|^\S+/) ?? [""])[0].replace(/^["']|["']$/g, "");
|
|
2381
|
+
if (!firstToken)
|
|
2382
|
+
return { found: true };
|
|
2383
|
+
if (!firstToken.includes("/")) {
|
|
2384
|
+
return resolveWecomBinary(firstToken);
|
|
2385
|
+
}
|
|
2386
|
+
return resolveWecomBinary(firstToken);
|
|
2387
|
+
}
|
|
2388
|
+
async function defaultWecomWsFactory() {
|
|
2389
|
+
const { WebSocket: Ws } = await import("ws");
|
|
2390
|
+
return (url) => new Ws(url);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
class WecomCliInstance {
|
|
2394
|
+
config;
|
|
2395
|
+
status = "created";
|
|
2396
|
+
child;
|
|
2397
|
+
buffer = "";
|
|
2398
|
+
eventHandler;
|
|
2399
|
+
lastError;
|
|
2400
|
+
wsBridge;
|
|
2401
|
+
pluginInstances = new Map;
|
|
2402
|
+
loadedPlugins = [];
|
|
2403
|
+
cardActionPorts;
|
|
2404
|
+
sessionCardStore;
|
|
2405
|
+
constructor(config) {
|
|
2406
|
+
this.config = config;
|
|
2407
|
+
}
|
|
2408
|
+
setCardActionPorts(ports) {
|
|
2409
|
+
this.cardActionPorts = ports;
|
|
2410
|
+
this.applySessionCardPorts();
|
|
2411
|
+
}
|
|
2412
|
+
hasCardActionPorts() {
|
|
2413
|
+
return this.cardActionPorts !== undefined;
|
|
2414
|
+
}
|
|
2415
|
+
setSessionCardStore(store) {
|
|
2416
|
+
this.sessionCardStore = store;
|
|
2417
|
+
this.applySessionCardPorts();
|
|
2418
|
+
}
|
|
2419
|
+
hasSessionCardStore() {
|
|
2420
|
+
return this.sessionCardStore !== undefined;
|
|
2421
|
+
}
|
|
2422
|
+
applySessionCardPorts() {
|
|
2423
|
+
if (!this.cardActionPorts || !this.sessionCardStore)
|
|
2424
|
+
return;
|
|
2425
|
+
const store = this.sessionCardStore;
|
|
2426
|
+
this.cardActionPorts.loadSessionCardState ??= (sessionId) => store.get(sessionId);
|
|
2427
|
+
this.cardActionPorts.findSessionCardByTaskId ??= (taskId) => {
|
|
2428
|
+
const hits = store.list().filter((s) => s.associatedTaskId === taskId);
|
|
2429
|
+
if (hits.length === 0)
|
|
2430
|
+
return;
|
|
2431
|
+
const open = hits.filter((s) => s.closedAt === undefined);
|
|
2432
|
+
const pool = open.length > 0 ? open : hits;
|
|
2433
|
+
return pool.reduce((a, b) => b.updatedAt > a.updatedAt ? b : a);
|
|
2434
|
+
};
|
|
2435
|
+
this.cardActionPorts.updateSessionCard ??= async (reqId, input) => {
|
|
2436
|
+
const { cmd, body } = buildWecomSessionCardUpdateBody({
|
|
2437
|
+
cardTaskId: input.cardTaskId,
|
|
2438
|
+
sessionId: input.sessionId,
|
|
2439
|
+
...input.state ? { state: input.state } : {},
|
|
2440
|
+
activity: input.activity,
|
|
2441
|
+
...input.task ? { task: input.task } : {},
|
|
2442
|
+
...input.perTaskChatId ? { perTaskChatId: input.perTaskChatId } : {},
|
|
2443
|
+
...input.errorHint ? { errorHint: input.errorHint } : {},
|
|
2444
|
+
...input.now !== undefined ? { now: input.now } : {}
|
|
2445
|
+
});
|
|
2446
|
+
const err = this.wsBridge?.sendFrameWithReqId({
|
|
2447
|
+
cmd,
|
|
2448
|
+
headers: { req_id: reqId },
|
|
2449
|
+
body
|
|
2450
|
+
});
|
|
2451
|
+
if (err)
|
|
2452
|
+
throw err;
|
|
2453
|
+
};
|
|
2454
|
+
this.cardActionPorts.loadSessionTaskInfo ??= async (sessionId) => {
|
|
2455
|
+
const taskComponent = this.environment?.getComponent?.("task");
|
|
2456
|
+
const getter = taskComponent?.getTaskIdBySessionId;
|
|
2457
|
+
if (typeof getter !== "function")
|
|
2458
|
+
return;
|
|
2459
|
+
const taskId = await getter.call(taskComponent, sessionId);
|
|
2460
|
+
if (typeof taskId !== "number")
|
|
2461
|
+
return;
|
|
2462
|
+
const withOps = await taskComponent?.getTaskWithOperations?.(taskId);
|
|
2463
|
+
if (!withOps)
|
|
2464
|
+
return { id: taskId };
|
|
2465
|
+
const ops = Array.isArray(withOps.operations) ? withOps.operations : [];
|
|
2466
|
+
return {
|
|
2467
|
+
id: taskId,
|
|
2468
|
+
...typeof withOps.title === "string" ? { title: withOps.title } : {},
|
|
2469
|
+
...typeof withOps.status === "string" ? { status: withOps.status } : {},
|
|
2470
|
+
...typeof withOps.progress === "number" ? { progress: withOps.progress } : {},
|
|
2471
|
+
...ops.length > 0 ? {
|
|
2472
|
+
milestones: ops.slice(-10).map((o) => ({
|
|
2473
|
+
title: String(o.milestoneTitle ?? o.milestoneType ?? ""),
|
|
2474
|
+
timestamp: String(o.timestamp ?? "")
|
|
2475
|
+
}))
|
|
2476
|
+
} : {}
|
|
2477
|
+
};
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
environment;
|
|
2481
|
+
setEnvironment(env) {
|
|
2482
|
+
this.environment = env;
|
|
2483
|
+
}
|
|
2484
|
+
getBridge() {
|
|
2485
|
+
return this.wsBridge;
|
|
2486
|
+
}
|
|
2487
|
+
async handleWsFrame(frame) {
|
|
2488
|
+
const cardAction = extractWecomCardAction(frame);
|
|
2489
|
+
if (cardAction) {
|
|
2490
|
+
await this.handleCardActionEvent(frame, cardAction);
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
const parsed = parseWecomFrame(frame);
|
|
2494
|
+
if (!parsed)
|
|
2495
|
+
return;
|
|
2496
|
+
const { botId } = readWecomCliConfig(this.config);
|
|
2497
|
+
const channelPrompt = buildWecomChannelSystemPrompt(botId ? { botId } : {});
|
|
2498
|
+
const metadata = {
|
|
2499
|
+
...parsed.metadata,
|
|
2500
|
+
systemPrompt: channelPrompt,
|
|
2501
|
+
extendTools: resolveWecomExtendTools(this.config),
|
|
2502
|
+
reqId: parsed.reqId,
|
|
2503
|
+
plugins: this.config.handleRule?.plugins || [],
|
|
2504
|
+
pluginEnabled: Object.fromEntries((this.config.handleRule?.plugins || []).map((p) => [
|
|
2505
|
+
`${this.config.id}:${p}`,
|
|
2506
|
+
true
|
|
2507
|
+
]))
|
|
2508
|
+
};
|
|
2509
|
+
const replyChannel = buildWecomReplyChannel({
|
|
2510
|
+
chatId: metadata.chatId,
|
|
2511
|
+
messageId: metadata.messageId,
|
|
2512
|
+
chatType: metadata.chatType,
|
|
2513
|
+
userId: metadata.senderId,
|
|
2514
|
+
sourceId: this.config.id,
|
|
2515
|
+
reqId: parsed.reqId,
|
|
2516
|
+
statusNotifications: this.config.statusNotifications === true
|
|
2517
|
+
});
|
|
2518
|
+
const event = {
|
|
2519
|
+
id: `es-${this.config.id}-${Date.now()}`,
|
|
2520
|
+
type: "event-source.event.wecom-cli",
|
|
2521
|
+
timestamp: Date.now(),
|
|
2522
|
+
metadata: {
|
|
2523
|
+
source: "event-source",
|
|
2524
|
+
sourceId: this.config.id,
|
|
2525
|
+
sourceMessageType: metadata.eventType,
|
|
2526
|
+
systemPrompt: channelPrompt,
|
|
2527
|
+
replyChannel,
|
|
2528
|
+
...metadata
|
|
2529
|
+
},
|
|
2530
|
+
payload: {
|
|
2531
|
+
sourceId: this.config.id,
|
|
2532
|
+
sourceType: WECOM_CLI_ES_TYPE,
|
|
2533
|
+
rawEvent: frame,
|
|
2534
|
+
message: buildWecomUserMessage(parsed.text, metadata.replyContext),
|
|
2535
|
+
metadata,
|
|
2536
|
+
replyChannel,
|
|
2537
|
+
timestamp: Date.now()
|
|
2538
|
+
},
|
|
2539
|
+
rawEvent: frame,
|
|
2540
|
+
replyChannel,
|
|
2541
|
+
recommendedAction: {
|
|
2542
|
+
action: `处理企业微信消息并回复。回复规范见 system prompt「${WECOM_CHANNEL_PROMPT_SECTION_TITLE}」章节。` + ` 用 wecom_respond 工具回复(content=回复正文);` + `chat_id 缺省取本事件 chatId。` + (parsed.reqId ? `(WS fallback req_id=${parsed.reqId})` : ""),
|
|
2543
|
+
replyTo: {
|
|
2544
|
+
chatId: replyChannel.chatId,
|
|
2545
|
+
messageId: replyChannel.messageId
|
|
2546
|
+
},
|
|
2547
|
+
...metadata.replyContext ? { replyContext: metadata.replyContext } : {},
|
|
2548
|
+
staticRulesInSystemPrompt: true
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
const mediaRefs = extractWecomMediaRefs(frame.body ?? {});
|
|
2552
|
+
if (mediaRefs.length > 0 && parsed.metadata.messageId) {
|
|
2553
|
+
const msgid = parsed.metadata.messageId;
|
|
2554
|
+
const saved = await this.downloadMediaRefs(msgid, mediaRefs, parsed.metadata.chatId);
|
|
2555
|
+
if (saved.length > 0) {
|
|
2556
|
+
event.payload.mediaFiles = saved;
|
|
2557
|
+
event.metadata.mediaFiles = saved;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
try {
|
|
2561
|
+
this.eventHandler?.(event);
|
|
2562
|
+
} catch (err) {
|
|
2563
|
+
console.error(`[wecom-cli] event handler threw: ${err}`);
|
|
2564
|
+
notifyReactFailure({
|
|
2565
|
+
error: err,
|
|
2566
|
+
envEvent: event,
|
|
2567
|
+
replyChannel
|
|
2568
|
+
}).catch(() => {});
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
async handleCardActionEvent(frame, action) {
|
|
2572
|
+
if (!this.cardActionPorts) {
|
|
2573
|
+
console.warn(`[wecom-cli] handleCardActionEvent: cardActionPorts 未注入,` + `template_card_event 被跳过(event_key=${action?.eventKey})。` + `调 instance.setCardActionPorts({markTaskDone, reopenTask, updateCard}) 注入。`);
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
try {
|
|
2577
|
+
const result = await handleWecomCardAction(frame, this.cardActionPorts);
|
|
2578
|
+
if (!result.ok) {
|
|
2579
|
+
console.warn(`[wecom-cli] handleCardActionEvent failed: ${result.error ?? "unknown"}`);
|
|
2580
|
+
}
|
|
2581
|
+
} catch (err) {
|
|
2582
|
+
console.error(`[wecom-cli] handleCardActionEvent threw: ${err.message}`);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
wireCardActionPorts() {
|
|
2586
|
+
if (this.cardActionPorts)
|
|
2587
|
+
return;
|
|
2588
|
+
const taskComponent = this.environment?.getComponent?.("task");
|
|
2589
|
+
if (!this.wsBridge) {
|
|
2590
|
+
console.warn(`[wecom-cli] cardActionPorts 未注入:wsBridge 缺失(没有通道发更新帧)。` + `卡片按钮点击会被跳过。`);
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const taskOpsAvailable = typeof taskComponent?.updateTask === "function";
|
|
2594
|
+
if (!taskOpsAvailable) {
|
|
2595
|
+
console.warn(`[wecom-cli] taskComponent.updateTask 不可用 ⇒ 「标记完成 / 重新打开」` + `降级为报错;session 卡「刷新」不受影响(Task #5364)。`);
|
|
2596
|
+
}
|
|
2597
|
+
const requireTaskOps = () => {
|
|
2598
|
+
if (!taskOpsAvailable) {
|
|
2599
|
+
throw new Error("task ops unavailable (taskComponent.updateTask missing)");
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
this.cardActionPorts = {
|
|
2603
|
+
markTaskDone: async (taskId) => {
|
|
2604
|
+
requireTaskOps();
|
|
2605
|
+
await taskComponent.updateTask(taskId, {
|
|
2606
|
+
status: "completed",
|
|
2607
|
+
current_status: "用户在企微卡片点「标记完成」确认完成"
|
|
2608
|
+
});
|
|
2609
|
+
},
|
|
2610
|
+
reopenTask: async (taskId) => {
|
|
2611
|
+
requireTaskOps();
|
|
2612
|
+
await taskComponent.updateTask(taskId, {
|
|
2613
|
+
status: "active",
|
|
2614
|
+
current_status: "用户在企微卡片点「重新打开」"
|
|
2615
|
+
});
|
|
2616
|
+
},
|
|
2617
|
+
updateCard: async (reqId, cardTaskId, statusLabel, note, timeline, action, extra) => {
|
|
2618
|
+
const { cmd, body } = buildWecomCardUpdateBody({
|
|
2619
|
+
cardTaskId,
|
|
2620
|
+
statusLabel,
|
|
2621
|
+
note,
|
|
2622
|
+
timeline,
|
|
2623
|
+
action,
|
|
2624
|
+
taskTitle: extra?.taskTitle,
|
|
2625
|
+
taskNumber: extra?.taskNumber,
|
|
2626
|
+
...extra?.activity !== undefined ? { activity: extra.activity } : {},
|
|
2627
|
+
...extra?.activityTerminal !== undefined ? { activityTerminal: extra.activityTerminal } : {}
|
|
2628
|
+
});
|
|
2629
|
+
const frame = {
|
|
2630
|
+
cmd,
|
|
2631
|
+
headers: { req_id: reqId },
|
|
2632
|
+
body
|
|
2633
|
+
};
|
|
2634
|
+
const err = this.wsBridge.sendFrameWithReqId(frame);
|
|
2635
|
+
if (err)
|
|
2636
|
+
throw err;
|
|
2637
|
+
},
|
|
2638
|
+
loadTimeline: (taskId) => {
|
|
2639
|
+
const store = this.getWecomCardStateStore();
|
|
2640
|
+
if (!store)
|
|
2641
|
+
return [];
|
|
2642
|
+
const rec = store.get(taskId);
|
|
2643
|
+
if (!rec?.timeline)
|
|
2644
|
+
return [];
|
|
2645
|
+
try {
|
|
2646
|
+
const arr = JSON.parse(rec.timeline);
|
|
2647
|
+
return Array.isArray(arr) ? arr : [];
|
|
2648
|
+
} catch {
|
|
2649
|
+
return [];
|
|
2650
|
+
}
|
|
2651
|
+
},
|
|
2652
|
+
loadTaskTitle: (taskId) => {
|
|
2653
|
+
const taskComponent2 = this.environment?.getComponent?.("task");
|
|
2654
|
+
const getter = taskComponent2?.getTask;
|
|
2655
|
+
if (!getter)
|
|
2656
|
+
return;
|
|
2657
|
+
try {
|
|
2658
|
+
const raw = getter.call(taskComponent2, taskId);
|
|
2659
|
+
const extract = (t) => {
|
|
2660
|
+
if (!t || typeof t !== "object")
|
|
2661
|
+
return;
|
|
2662
|
+
const rec = t;
|
|
2663
|
+
if (typeof rec.title !== "string" || !rec.title)
|
|
2664
|
+
return;
|
|
2665
|
+
return {
|
|
2666
|
+
title: rec.title,
|
|
2667
|
+
number: typeof rec.id === "number" ? rec.id : taskId
|
|
2668
|
+
};
|
|
2669
|
+
};
|
|
2670
|
+
if (raw && typeof raw.then === "function") {
|
|
2671
|
+
return raw.then(extract).catch((err) => {
|
|
2672
|
+
console.warn(`[wecom-cli] loadTaskTitle failed for task #${taskId}: ${err}`);
|
|
2673
|
+
return;
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
return extract(raw);
|
|
2677
|
+
} catch (err) {
|
|
2678
|
+
console.warn(`[wecom-cli] loadTaskTitle failed for task #${taskId}: ${err}`);
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
this.cardActionPorts.loadSubSessionActivity = createActivityProbe({
|
|
2684
|
+
listBackgroundTasks: () => {
|
|
2685
|
+
const tc = this.environment?.getComponent?.("task");
|
|
2686
|
+
return tc?.getBackgroundTaskManager?.()?.listTasks?.();
|
|
2687
|
+
},
|
|
2688
|
+
getSubSessionActivity: (bgId) => {
|
|
2689
|
+
const tc = this.environment?.getComponent?.("task");
|
|
2690
|
+
return tc?.getBackgroundTaskManager?.()?.getSubSessionActivity?.(bgId);
|
|
2691
|
+
},
|
|
2692
|
+
getLastMessage: async (subSessionId) => {
|
|
2693
|
+
const sc = this.environment?.getComponent?.("session");
|
|
2694
|
+
if (!sc?.getMessages)
|
|
2695
|
+
return;
|
|
2696
|
+
const tail = await sc.getMessages(subSessionId, { offset: -1, limit: 1 });
|
|
2697
|
+
return toActivityMessage(tail?.[tail.length - 1]);
|
|
2698
|
+
},
|
|
2699
|
+
isTerminalTask: async (taskId) => {
|
|
2700
|
+
const tc = this.environment?.getComponent?.("task");
|
|
2701
|
+
const raw = tc?.getTask?.call(tc, taskId);
|
|
2702
|
+
const t = raw && typeof raw.then === "function" ? await raw : raw;
|
|
2703
|
+
return t?.status === "completed" || t?.status === "cancelled" || t?.status === "failed" || t?.status === "archived";
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
this.cardActionPorts.loadSessionActivity = createSessionActivityProbe({
|
|
2707
|
+
getSession: async (sessionId) => {
|
|
2708
|
+
const sc = this.environment?.getComponent?.("session");
|
|
2709
|
+
if (!sc?.get)
|
|
2710
|
+
return;
|
|
2711
|
+
const s = await sc.get(sessionId);
|
|
2712
|
+
if (!s)
|
|
2713
|
+
return;
|
|
2714
|
+
return {
|
|
2715
|
+
...typeof s.messageCount === "number" ? { messageCount: s.messageCount } : {},
|
|
2716
|
+
...typeof s.updatedAt === "number" ? { updatedAt: s.updatedAt } : {}
|
|
2717
|
+
};
|
|
2718
|
+
},
|
|
2719
|
+
getLastMessage: async (sessionId) => {
|
|
2720
|
+
const sc = this.environment?.getComponent?.("session");
|
|
2721
|
+
if (!sc?.getMessages)
|
|
2722
|
+
return;
|
|
2723
|
+
const tail = await sc.getMessages(sessionId, { offset: -1, limit: 1 });
|
|
2724
|
+
return toActivityMessage(tail?.[tail.length - 1]);
|
|
2725
|
+
},
|
|
2726
|
+
getLifecycle: (sessionId) => {
|
|
2727
|
+
const tc = this.environment?.getComponent?.("task");
|
|
2728
|
+
const tasks = tc?.getBackgroundTaskManager?.()?.listTasks?.() ?? [];
|
|
2729
|
+
const hit = tasks.find((t) => t?.subSessionId === sessionId);
|
|
2730
|
+
return mapBgTaskStatusToSessionLifecycle(hit?.status);
|
|
2731
|
+
}
|
|
2732
|
+
});
|
|
2733
|
+
this.sessionCardStore ??= getSharedWecomSessionCardStore();
|
|
2734
|
+
this.applySessionCardPorts();
|
|
2735
|
+
if (this.cardActionPorts) {
|
|
2736
|
+
const sessionComponent = () => this.environment?.getComponent?.("session");
|
|
2737
|
+
this.cardActionPorts.findGroupChatForSession ??= async (sessionId) => {
|
|
2738
|
+
const sc = sessionComponent();
|
|
2739
|
+
if (typeof sc?.findGroupChatForSession !== "function") {
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
return sc.findGroupChatForSession(sessionId);
|
|
2743
|
+
};
|
|
2744
|
+
this.cardActionPorts.createLarkGroupForSession ??= async (sessionId) => {
|
|
2745
|
+
const sc = sessionComponent();
|
|
2746
|
+
let title;
|
|
2747
|
+
try {
|
|
2748
|
+
const s = await sc?.get?.(sessionId);
|
|
2749
|
+
if (typeof s?.title === "string" && s.title.trim()) {
|
|
2750
|
+
title = s.title.trim();
|
|
2751
|
+
}
|
|
2752
|
+
} catch {}
|
|
2753
|
+
const name = title ?? sessionId.slice(-20);
|
|
2754
|
+
const chatId = await createGroupChatViaLarkCli({
|
|
2755
|
+
name: `会话 ${name}`.slice(0, 60),
|
|
2756
|
+
description: `roy-agent session ${sessionId}`.slice(0, 100)
|
|
2757
|
+
});
|
|
2758
|
+
try {
|
|
2759
|
+
await sc?.attachGroupSession?.({
|
|
2760
|
+
chatId,
|
|
2761
|
+
originSessionId: sessionId,
|
|
2762
|
+
title: name
|
|
2763
|
+
});
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
console.warn(`[wecom-cli] attachGroupSession(${chatId} ← ${sessionId}) ` + `失败(不阻断建群): ${err}`);
|
|
2766
|
+
}
|
|
2767
|
+
return chatId;
|
|
2768
|
+
};
|
|
2769
|
+
this.cardActionPorts.notifyGroupLink ??= async (input) => {
|
|
2770
|
+
const link = `${DEFAULT_APP_LINK_BASE}/client/chat/open?openChatId=${input.chatId}`;
|
|
2771
|
+
const text = `\uD83D\uDCE8 会话 ${input.sessionId} 的关联飞书群已建立:` + `[点击进入群](${link})`;
|
|
2772
|
+
let target = input.userId || "";
|
|
2773
|
+
if (!target) {
|
|
2774
|
+
try {
|
|
2775
|
+
target = this.sessionCardStore?.get(input.sessionId)?.chatId ?? "";
|
|
2776
|
+
} catch {
|
|
2777
|
+
target = "";
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
if (!target) {
|
|
2781
|
+
console.warn(`[wecom-cli] notifyGroupLink: 无法定位企微会话 ` + `(userId 缺失且 session store 无 chatId),跳过通知`);
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
const r = await sendWecomCliMarkdown({ chatId: target, content: text });
|
|
2785
|
+
if (!r.ok) {
|
|
2786
|
+
console.warn(`[wecom-cli] notifyGroupLink cli failed: ${r.ok ? "unknown" : r.error}`);
|
|
2787
|
+
}
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
console.info(`[wecom-cli] cardActionPorts 已注入(task ops + bridge.respond 更新 + timeline + taskTitle + activity + session task info + lark group)`);
|
|
2791
|
+
}
|
|
2792
|
+
getWecomCardStateStore() {
|
|
2793
|
+
const plugin = this.pluginInstances.get("WecomTaskNotifyPlugin");
|
|
2794
|
+
const store = plugin?.cardStateStore;
|
|
2795
|
+
if (store && typeof store.get === "function") {
|
|
2796
|
+
return store;
|
|
2797
|
+
}
|
|
2798
|
+
if (!this.fallbackCardStateStore) {
|
|
2799
|
+
try {
|
|
2800
|
+
this.fallbackCardStateStore = createWecomCardStateStore();
|
|
2801
|
+
} catch (err) {
|
|
2802
|
+
console.warn(`[wecom-cli] fallback card state store 创建失败: ${err}`);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return this.fallbackCardStateStore;
|
|
2807
|
+
}
|
|
2808
|
+
fallbackCardStateStore;
|
|
2809
|
+
async downloadMediaRefs(msgid, refs, chatId) {
|
|
2810
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("media download timeout (10s)")), 1e4));
|
|
2811
|
+
try {
|
|
2812
|
+
const paths = await Promise.race([
|
|
2813
|
+
timeout,
|
|
2814
|
+
Promise.all(refs.map(async (ref) => {
|
|
2815
|
+
try {
|
|
2816
|
+
const path = await fetchAndSaveWecomMedia({
|
|
2817
|
+
msgid,
|
|
2818
|
+
url: ref.url,
|
|
2819
|
+
aeskey: ref.aeskey,
|
|
2820
|
+
kind: ref.kind,
|
|
2821
|
+
filename: ref.filename,
|
|
2822
|
+
chatId
|
|
2823
|
+
});
|
|
2824
|
+
console.log(`[wecom-cli] media saved: ${ref.kind} chat=${chatId ?? "(none)"} ${path}`);
|
|
2825
|
+
return path;
|
|
2826
|
+
} catch (err) {
|
|
2827
|
+
console.error(`[wecom-cli] media download failed for ${ref.kind}: ${err.message}`);
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
}))
|
|
2831
|
+
]);
|
|
2832
|
+
return paths.filter((p) => !!p);
|
|
2833
|
+
} catch (err) {
|
|
2834
|
+
console.error(`[wecom-cli] media download aborted: ${err.message}`);
|
|
2835
|
+
return [];
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
getStatus() {
|
|
2839
|
+
return this.status;
|
|
2840
|
+
}
|
|
2841
|
+
setStatus(status) {
|
|
2842
|
+
this.status = status;
|
|
2843
|
+
}
|
|
2844
|
+
onEvent(handler) {
|
|
2845
|
+
this.eventHandler = handler;
|
|
2846
|
+
}
|
|
2847
|
+
offEvent() {
|
|
2848
|
+
this.eventHandler = undefined;
|
|
2849
|
+
}
|
|
2850
|
+
getLastError() {
|
|
2851
|
+
return this.lastError;
|
|
2852
|
+
}
|
|
2853
|
+
buildCommand() {
|
|
2854
|
+
const { command } = readWecomCliConfig(this.config);
|
|
2855
|
+
if (command)
|
|
2856
|
+
return command;
|
|
2857
|
+
return "wecom-cli chat messages list --help";
|
|
2858
|
+
}
|
|
2859
|
+
loadPlugins() {
|
|
2860
|
+
const plugins = this.config.handleRule?.plugins || [];
|
|
2861
|
+
if (plugins.length === 0)
|
|
2862
|
+
return;
|
|
2863
|
+
for (const pluginName of plugins) {
|
|
2864
|
+
const pluginConfig = this.config.handleRule?.pluginConfig?.[pluginName] ?? {};
|
|
2865
|
+
const plugin = importPluginShared(pluginName, getWecomPluginFactories(), {
|
|
2866
|
+
sourceId: this.config.id,
|
|
2867
|
+
pluginName,
|
|
2868
|
+
statusNotifications: this.config.statusNotifications === true,
|
|
2869
|
+
...pluginConfig
|
|
2870
|
+
});
|
|
2871
|
+
if (!plugin)
|
|
2872
|
+
continue;
|
|
2873
|
+
this.pluginInstances.set(pluginName, plugin);
|
|
2874
|
+
const hookPoints = registerPluginHooksShared(plugin, pluginName, this.config.id);
|
|
2875
|
+
this.loadedPlugins.push({ name: pluginName, hooks: hookPoints });
|
|
2876
|
+
console.debug(`[wecom-cli] Loaded plugin "${pluginName}" with ${hookPoints.length} hook(s): [${hookPoints.join(", ")}]`);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
unloadPlugins() {
|
|
2880
|
+
for (const p of this.loadedPlugins) {
|
|
2881
|
+
unregisterPluginHooksShared(p.name, p.hooks);
|
|
2882
|
+
}
|
|
2883
|
+
this.loadedPlugins = [];
|
|
2884
|
+
this.pluginInstances.clear();
|
|
2885
|
+
}
|
|
2886
|
+
async start() {
|
|
2887
|
+
this.loadPlugins();
|
|
2888
|
+
await this.startWithBridge(await defaultWecomWsFactory());
|
|
2889
|
+
}
|
|
2890
|
+
async startWithBridge(wsFactory, credentialsPath) {
|
|
2891
|
+
if (this.status === "running")
|
|
2892
|
+
return;
|
|
2893
|
+
this.setStatus("starting");
|
|
2894
|
+
const credentials = resolveWecomCredentials(this.config, credentialsPath);
|
|
2895
|
+
if (credentials) {
|
|
2896
|
+
this.startWsBridge(credentials, wsFactory);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
this.startSkeletonMode();
|
|
2900
|
+
}
|
|
2901
|
+
startWsBridge(credentials, wsFactory) {
|
|
2902
|
+
const bridge = new WecomWsBridge({
|
|
2903
|
+
botId: credentials.botId,
|
|
2904
|
+
secret: credentials.secret,
|
|
2905
|
+
wsFactory
|
|
2906
|
+
});
|
|
2907
|
+
this.wsBridge = bridge;
|
|
2908
|
+
registerWecomRespondTarget(this.config.id, {
|
|
2909
|
+
respond: (reqId, body) => bridge.respond(reqId, body),
|
|
2910
|
+
sendCmd: (cmd, body) => bridge.sendCmd(cmd, body)
|
|
2911
|
+
});
|
|
2912
|
+
this.wireCardActionPorts();
|
|
2913
|
+
bridge.onMessage((frame) => {
|
|
2914
|
+
this.handleWsFrame(frame).catch((err) => {
|
|
2915
|
+
console.error(`[wecom-cli] ws frame handling threw: ${err}`);
|
|
2916
|
+
try {
|
|
2917
|
+
const parsed = parseWecomFrame(frame);
|
|
2918
|
+
if (!parsed) {
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
const baseChannel = buildWecomReplyChannel({
|
|
2922
|
+
chatId: parsed.metadata.chatId,
|
|
2923
|
+
messageId: parsed.metadata.messageId,
|
|
2924
|
+
chatType: parsed.metadata.chatType,
|
|
2925
|
+
userId: parsed.metadata.senderId,
|
|
2926
|
+
sourceId: this.config.id,
|
|
2927
|
+
reqId: parsed.reqId,
|
|
2928
|
+
statusNotifications: this.config.statusNotifications === true
|
|
2929
|
+
});
|
|
2930
|
+
notifyReactFailure({
|
|
2931
|
+
error: err,
|
|
2932
|
+
envEvent: {
|
|
2933
|
+
type: "event-source.event.wecom-cli",
|
|
2934
|
+
metadata: {
|
|
2935
|
+
sourceId: this.config.id,
|
|
2936
|
+
replyChannel: baseChannel
|
|
2937
|
+
},
|
|
2938
|
+
payload: {}
|
|
2939
|
+
}
|
|
2940
|
+
}).catch(() => {});
|
|
2941
|
+
} catch {}
|
|
2942
|
+
});
|
|
2943
|
+
});
|
|
2944
|
+
bridge.onError((err) => {
|
|
2945
|
+
this.lastError = err.message;
|
|
2946
|
+
console.error(`[wecom-cli] ws error: ${err.message}`);
|
|
2947
|
+
});
|
|
2948
|
+
bridge.connect();
|
|
2949
|
+
this.setStatus("running");
|
|
2950
|
+
}
|
|
2951
|
+
startSkeletonMode() {
|
|
2952
|
+
console.warn(`[wecom-cli] ⚠️ 骨架模式:未找到企微凭据(config botId/secret 或 ` + `${WECOM_CREDENTIALS_DEFAULT_PATH}),**不会收到任何入站消息**。
|
|
2953
|
+
` + `[wecom-cli] 请在 event source 配置里提供 botId + secret,或创建凭据文件(chmod 600)。
|
|
2954
|
+
` + `[wecom-cli] 如需临时自定义,可在事件源配置里设置 'command' 字段。`);
|
|
2955
|
+
this.startSkeletonSpawn();
|
|
2956
|
+
}
|
|
2957
|
+
startSkeletonSpawn() {
|
|
2958
|
+
const { command: customCommand } = readWecomCliConfig(this.config);
|
|
2959
|
+
if (!customCommand) {
|
|
2960
|
+
console.warn(`[wecom-cli] ⚠️ 骨架模式:当前启动的是占位命令 ` + `"wecom-cli chat messages list --help",它**不会产出任何入站事件**。
|
|
2961
|
+
` + `[wecom-cli] Phase 2 长连接需要凭据(config botId/secret 或凭据文件)。
|
|
2962
|
+
` + `[wecom-cli] 如需临时自定义,可在事件源配置里设置 'command' 字段。`);
|
|
2963
|
+
}
|
|
2964
|
+
if (customCommand) {
|
|
2965
|
+
const probe = probeCommandBinary(customCommand);
|
|
2966
|
+
if (!probe.found) {
|
|
2967
|
+
this.lastError = `wecom-cli command not found: ${customCommand}`;
|
|
2968
|
+
this.setStatus("error");
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
} else {
|
|
2972
|
+
const bin = resolveWecomBinary();
|
|
2973
|
+
if (!bin.found) {
|
|
2974
|
+
this.lastError = "wecom-cli binary not found on PATH. Install it with: npm install -g @wecom/cli --registry=https://registry.npmmirror.com (or set 'command' in the event source config to override the launch command).";
|
|
2975
|
+
this.setStatus("error");
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
const command = this.buildCommand();
|
|
2980
|
+
let child;
|
|
2981
|
+
try {
|
|
2982
|
+
child = spawn2(`exec ${command}`, [], {
|
|
2983
|
+
shell: true,
|
|
2984
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2985
|
+
windowsHide: true
|
|
2986
|
+
});
|
|
2987
|
+
} catch (err) {
|
|
2988
|
+
this.lastError = `failed to spawn wecom-cli: ${err.message}`;
|
|
2989
|
+
this.setStatus("error");
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
this.child = child;
|
|
2993
|
+
child.stdout?.on("data", (data) => {
|
|
2994
|
+
this.processStdout(data.toString());
|
|
2995
|
+
});
|
|
2996
|
+
child.stderr?.on("data", (data) => {
|
|
2997
|
+
const text = data.toString().trim();
|
|
2998
|
+
if (text) {
|
|
2999
|
+
this.lastError = text.slice(0, 500);
|
|
3000
|
+
console.error(`[wecom-cli] ${text}`);
|
|
3001
|
+
}
|
|
1156
3002
|
});
|
|
1157
|
-
|
|
3003
|
+
child.on("error", (err) => {
|
|
3004
|
+
this.lastError = `wecom-cli process error: ${err.message}`;
|
|
3005
|
+
this.setStatus("error");
|
|
3006
|
+
});
|
|
3007
|
+
child.on("exit", (code, signal) => {
|
|
1158
3008
|
if (code !== 0 && signal !== "SIGINT" && signal !== "SIGTERM") {
|
|
3009
|
+
this.lastError = `wecom-cli exited with code ${code}`;
|
|
1159
3010
|
this.setStatus("error");
|
|
1160
3011
|
} else {
|
|
1161
3012
|
this.setStatus("stopped");
|
|
1162
3013
|
}
|
|
1163
3014
|
});
|
|
1164
|
-
this.child.on("error", (err) => {
|
|
1165
|
-
console.error(`[bounty-cli] spawn error: ${err.message}`);
|
|
1166
|
-
this.setStatus("error");
|
|
1167
|
-
});
|
|
1168
3015
|
this.setStatus("running");
|
|
1169
3016
|
}
|
|
1170
|
-
processStdout(
|
|
1171
|
-
|
|
1172
|
-
`
|
|
1173
|
-
const tail = lines.pop() ?? "";
|
|
1174
|
-
this.buffer = tail;
|
|
1175
|
-
for (const line of lines) {
|
|
1176
|
-
const trimmed = line.trim();
|
|
1177
|
-
if (!trimmed)
|
|
1178
|
-
continue;
|
|
1179
|
-
this.handleEventLine(trimmed, fallbackChannel);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
processStderr(buffer) {
|
|
1183
|
-
const lines = buffer.split(`
|
|
3017
|
+
processStdout(chunk) {
|
|
3018
|
+
this.buffer += chunk;
|
|
3019
|
+
const lines = this.buffer.split(`
|
|
1184
3020
|
`);
|
|
1185
|
-
|
|
3021
|
+
this.buffer = lines.pop() ?? "";
|
|
1186
3022
|
for (const line of lines) {
|
|
1187
3023
|
const trimmed = line.trim();
|
|
1188
3024
|
if (!trimmed)
|
|
1189
3025
|
continue;
|
|
1190
|
-
|
|
1191
|
-
this.handleEventLine(trimmed, readBountyCliConfig(this.config).channel ?? "");
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
if (tail.trim().length > 0) {
|
|
1195
|
-
this.buffer = tail;
|
|
1196
|
-
} else {
|
|
1197
|
-
this.buffer = "";
|
|
3026
|
+
this.handleEventLine(trimmed);
|
|
1198
3027
|
}
|
|
1199
3028
|
}
|
|
1200
|
-
handleEventLine(line
|
|
3029
|
+
handleEventLine(line) {
|
|
1201
3030
|
let raw;
|
|
1202
3031
|
try {
|
|
1203
3032
|
raw = JSON.parse(line);
|
|
1204
3033
|
} catch {
|
|
1205
|
-
console.error(`[
|
|
3034
|
+
console.error(`[wecom-cli] skipping non-JSON line: ${line.slice(0, 200)}`);
|
|
1206
3035
|
return;
|
|
1207
3036
|
}
|
|
1208
|
-
const
|
|
3037
|
+
const parsed = parseWecomFrame(raw);
|
|
3038
|
+
if (!parsed) {
|
|
3039
|
+
console.error(`[wecom-cli] skipping non-event frame: ${JSON.stringify(raw).slice(0, 200)}`);
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
const replyChannel = buildWecomReplyChannel({
|
|
3043
|
+
chatId: parsed.metadata.chatId,
|
|
3044
|
+
messageId: parsed.metadata.messageId,
|
|
3045
|
+
chatType: parsed.metadata.chatType,
|
|
3046
|
+
userId: parsed.metadata.senderId,
|
|
3047
|
+
sourceId: this.config.id,
|
|
3048
|
+
reqId: parsed.reqId,
|
|
3049
|
+
statusNotifications: this.config.statusNotifications === true
|
|
3050
|
+
});
|
|
1209
3051
|
const event = {
|
|
1210
|
-
metadata,
|
|
3052
|
+
metadata: parsed.metadata,
|
|
3053
|
+
payload: {
|
|
3054
|
+
sourceId: this.config.id,
|
|
3055
|
+
sourceType: WECOM_CLI_ES_TYPE,
|
|
3056
|
+
rawEvent: raw,
|
|
3057
|
+
message: buildWecomUserMessage(parsed.text, parsed.metadata.replyContext),
|
|
3058
|
+
metadata: parsed.metadata,
|
|
3059
|
+
replyChannel,
|
|
3060
|
+
timestamp: Date.now()
|
|
3061
|
+
},
|
|
1211
3062
|
rawEvent: raw,
|
|
1212
3063
|
replyChannel,
|
|
1213
|
-
recommendedAction:
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
3064
|
+
recommendedAction: {
|
|
3065
|
+
description: "Reply to the WeCom user via `wecom-cli message aibot send --json ...`",
|
|
3066
|
+
command: `wecom-cli message aibot send --json '{"chat_id":"${parsed.metadata.chatId ?? parsed.metadata.senderId ?? ""}","msg_type":"markdown","markdown":{"content":"..."}}'`,
|
|
3067
|
+
notes: [
|
|
3068
|
+
"企微单聊的 chat_id 是对方 userid;群聊的 chat_id 是回调里的 chatid。",
|
|
3069
|
+
"发送成功 = 退出码 0 且 stdout 无 error 字段。"
|
|
3070
|
+
]
|
|
3071
|
+
}
|
|
1217
3072
|
};
|
|
1218
3073
|
if (this.eventHandler) {
|
|
1219
3074
|
try {
|
|
1220
3075
|
this.eventHandler(event);
|
|
1221
3076
|
} catch (err) {
|
|
1222
|
-
console.error(`[
|
|
3077
|
+
console.error(`[wecom-cli] event handler threw: ${err}`);
|
|
3078
|
+
notifyReactFailure({
|
|
3079
|
+
error: err,
|
|
3080
|
+
envEvent: event,
|
|
3081
|
+
replyChannel
|
|
3082
|
+
}).catch(() => {});
|
|
1223
3083
|
}
|
|
1224
3084
|
}
|
|
1225
3085
|
}
|
|
1226
3086
|
async stop() {
|
|
1227
3087
|
if (this.status === "stopped" || this.status === "created")
|
|
1228
3088
|
return;
|
|
3089
|
+
this.unloadPlugins();
|
|
3090
|
+
if (this.wsBridge) {
|
|
3091
|
+
unregisterWecomRespondTarget(this.config.id);
|
|
3092
|
+
this.wsBridge.stop();
|
|
3093
|
+
this.wsBridge = undefined;
|
|
3094
|
+
}
|
|
3095
|
+
try {
|
|
3096
|
+
this.fallbackCardStateStore?.close?.();
|
|
3097
|
+
} catch {}
|
|
3098
|
+
this.fallbackCardStateStore = undefined;
|
|
1229
3099
|
if (this.child && !this.child.killed) {
|
|
1230
3100
|
this.child.kill("SIGINT");
|
|
1231
|
-
await new Promise((
|
|
3101
|
+
await new Promise((resolve2) => {
|
|
1232
3102
|
const timer = setTimeout(() => {
|
|
1233
|
-
if (this.child && !this.child.killed)
|
|
3103
|
+
if (this.child && !this.child.killed)
|
|
1234
3104
|
this.child.kill("SIGKILL");
|
|
1235
|
-
|
|
1236
|
-
resolve();
|
|
3105
|
+
resolve2();
|
|
1237
3106
|
}, 3000);
|
|
1238
3107
|
if (this.child) {
|
|
1239
3108
|
this.child.on("exit", () => {
|
|
1240
3109
|
clearTimeout(timer);
|
|
1241
|
-
|
|
3110
|
+
resolve2();
|
|
1242
3111
|
});
|
|
1243
3112
|
} else {
|
|
1244
3113
|
clearTimeout(timer);
|
|
1245
|
-
|
|
3114
|
+
resolve2();
|
|
1246
3115
|
}
|
|
1247
3116
|
});
|
|
1248
3117
|
}
|
|
1249
3118
|
this.setStatus("stopped");
|
|
1250
3119
|
}
|
|
1251
3120
|
}
|
|
1252
|
-
var
|
|
1253
|
-
type:
|
|
3121
|
+
var wecomCliHandler = {
|
|
3122
|
+
type: WECOM_CLI_ES_TYPE,
|
|
1254
3123
|
validateConfig(config) {
|
|
1255
3124
|
const errors = [];
|
|
1256
3125
|
if (!config.id)
|
|
1257
3126
|
errors.push("EventSource ID is required");
|
|
1258
3127
|
if (!config.name)
|
|
1259
3128
|
errors.push("EventSource name is required");
|
|
1260
|
-
const {
|
|
1261
|
-
if (!
|
|
1262
|
-
errors.push("
|
|
3129
|
+
const { botId, secret } = readWecomCliConfig(config);
|
|
3130
|
+
if (!botId) {
|
|
3131
|
+
errors.push("WeCom 'botId' is required (企业微信智能机器人的 BotID;CLI 用 --bot-id 传入)");
|
|
3132
|
+
}
|
|
3133
|
+
if (!secret) {
|
|
3134
|
+
errors.push("WeCom 'secret' is required (智能机器人 Secret;CLI 用 --secret 传入)");
|
|
1263
3135
|
}
|
|
1264
3136
|
return errors;
|
|
1265
3137
|
},
|
|
1266
3138
|
createInstance(config) {
|
|
1267
|
-
return new
|
|
3139
|
+
return new WecomCliInstance(config);
|
|
3140
|
+
},
|
|
3141
|
+
getOptions() {
|
|
3142
|
+
return [
|
|
3143
|
+
{ name: "botId", description: "企业微信智能机器人 BotID", required: true, type: "string" },
|
|
3144
|
+
{ name: "secret", description: "智能机器人 Secret(长连接专用密钥)", required: true, type: "string" },
|
|
3145
|
+
{ name: "webhook", description: "群机器人 webhook URL(可选)", required: false, type: "string" },
|
|
3146
|
+
{ name: "command", description: "覆盖默认启动命令(高级)", required: false, type: "string" }
|
|
3147
|
+
];
|
|
1268
3148
|
}
|
|
1269
3149
|
};
|
|
1270
3150
|
|
|
@@ -1488,7 +3368,13 @@ function writeConfigPath(configPath, dotPath, value) {
|
|
|
1488
3368
|
}
|
|
1489
3369
|
});
|
|
1490
3370
|
const result = applyEdits(text, edits);
|
|
1491
|
-
|
|
3371
|
+
const configDir = path.dirname(configPath);
|
|
3372
|
+
const ok = guardConfigWrite(configDir, path.basename(configPath), () => {
|
|
3373
|
+
fs.writeFileSync(configPath, result, "utf-8");
|
|
3374
|
+
});
|
|
3375
|
+
if (!ok) {
|
|
3376
|
+
throw new Error(`config.jsonc 写入失败(护栏记录见 audit.log):${configPath}`);
|
|
3377
|
+
}
|
|
1492
3378
|
}
|
|
1493
3379
|
function readFallbackModelList(configPath) {
|
|
1494
3380
|
const config = readConfigFile(configPath);
|
|
@@ -1728,12 +3614,12 @@ function resolveSocketPath() {
|
|
|
1728
3614
|
return process.env.ROY_DAEMON_SOCKET || DEFAULT_SOCKET_PATH;
|
|
1729
3615
|
}
|
|
1730
3616
|
async function requestReset(socketPath, reason) {
|
|
1731
|
-
return new Promise((
|
|
3617
|
+
return new Promise((resolve2) => {
|
|
1732
3618
|
let sock;
|
|
1733
3619
|
try {
|
|
1734
3620
|
sock = connect(socketPath);
|
|
1735
3621
|
} catch (e) {
|
|
1736
|
-
|
|
3622
|
+
resolve2({ confirmed: false, reason: e.message });
|
|
1737
3623
|
return;
|
|
1738
3624
|
}
|
|
1739
3625
|
let settled = false;
|
|
@@ -1743,7 +3629,7 @@ async function requestReset(socketPath, reason) {
|
|
|
1743
3629
|
settled = true;
|
|
1744
3630
|
clearTimeout(timer);
|
|
1745
3631
|
sock.destroy();
|
|
1746
|
-
|
|
3632
|
+
resolve2({ confirmed, reason: why });
|
|
1747
3633
|
};
|
|
1748
3634
|
const timer = setTimeout(() => finish(false, `daemon 未在 ${RESET_ACK_TIMEOUT_MS}ms 内确认`), RESET_ACK_TIMEOUT_MS);
|
|
1749
3635
|
sock.once("connect", () => {
|
|
@@ -1799,6 +3685,227 @@ var resetCommand = {
|
|
|
1799
3685
|
}
|
|
1800
3686
|
};
|
|
1801
3687
|
|
|
3688
|
+
// src/env/event-source/es-command.ts
|
|
3689
|
+
import * as fs2 from "fs";
|
|
3690
|
+
import * as path2 from "path";
|
|
3691
|
+
var MOD_ALLOWED_FIELDS = [
|
|
3692
|
+
"enabled",
|
|
3693
|
+
"name",
|
|
3694
|
+
"interval",
|
|
3695
|
+
"cron",
|
|
3696
|
+
"profile"
|
|
3697
|
+
];
|
|
3698
|
+
function resolveEsFilePath(configPath) {
|
|
3699
|
+
const dir = configPath ? path2.dirname(configPath) : defaultRoyDir();
|
|
3700
|
+
return path2.join(dir, "event-sources.json");
|
|
3701
|
+
}
|
|
3702
|
+
function defaultRoyDir() {
|
|
3703
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
|
3704
|
+
return path2.join(home, ".roy-agent");
|
|
3705
|
+
}
|
|
3706
|
+
function readEsFile(esFile) {
|
|
3707
|
+
if (!fs2.existsSync(esFile)) {
|
|
3708
|
+
return { version: "1.0.0", sources: [] };
|
|
3709
|
+
}
|
|
3710
|
+
try {
|
|
3711
|
+
const parsed = JSON.parse(fs2.readFileSync(esFile, "utf-8"));
|
|
3712
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.sources)) {
|
|
3713
|
+
return parsed;
|
|
3714
|
+
}
|
|
3715
|
+
} catch {}
|
|
3716
|
+
throw new Error(`event-sources.json 解析失败:${esFile}`);
|
|
3717
|
+
}
|
|
3718
|
+
function writeEsFile(esFile, data) {
|
|
3719
|
+
fs2.mkdirSync(path2.dirname(esFile), { recursive: true });
|
|
3720
|
+
const tmp = `${esFile}.tmp-${process.pid}-${Date.now()}`;
|
|
3721
|
+
try {
|
|
3722
|
+
fs2.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
3723
|
+
fs2.renameSync(tmp, esFile);
|
|
3724
|
+
} catch (err) {
|
|
3725
|
+
try {
|
|
3726
|
+
fs2.rmSync(tmp, { force: true });
|
|
3727
|
+
} catch {}
|
|
3728
|
+
throw new Error(`写入 ${esFile} 失败:${err.message}`);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
function dualResult2(text) {
|
|
3732
|
+
return {
|
|
3733
|
+
ok: true,
|
|
3734
|
+
output: {
|
|
3735
|
+
text,
|
|
3736
|
+
card: renderTextCard("⚙️ EventSource 管理", text),
|
|
3737
|
+
kind: "card"
|
|
3738
|
+
}
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
var USAGE2 = [
|
|
3742
|
+
"用法:##command##: es <子命令> [参数]",
|
|
3743
|
+
"",
|
|
3744
|
+
"子命令:",
|
|
3745
|
+
" list 列出所有事件源",
|
|
3746
|
+
" add <name> <type> 新增事件源(enabled=true)",
|
|
3747
|
+
" del <name|id> 删除事件源(别名 remove)",
|
|
3748
|
+
" mod <name|id> k=v 修改字段(enabled/name/interval/cron/profile;别名 update)",
|
|
3749
|
+
"",
|
|
3750
|
+
" ⚠️ command/url 不可经 mod 修改(避免命令注入面);如需改请 del 后 add。",
|
|
3751
|
+
"",
|
|
3752
|
+
"注意:修改的是磁盘配置,运行中的进程需「##command##: reset」重启后生效。"
|
|
3753
|
+
].join(`
|
|
3754
|
+
`);
|
|
3755
|
+
function findSource(sources, nameOrId) {
|
|
3756
|
+
return sources.find((s) => s.name === nameOrId || s.id === nameOrId);
|
|
3757
|
+
}
|
|
3758
|
+
function parseKv(pair) {
|
|
3759
|
+
const idx = pair.indexOf("=");
|
|
3760
|
+
if (idx <= 0)
|
|
3761
|
+
return null;
|
|
3762
|
+
const key = pair.slice(0, idx).trim();
|
|
3763
|
+
const rawValue = pair.slice(idx + 1).trim();
|
|
3764
|
+
let value = rawValue;
|
|
3765
|
+
if (rawValue === "true")
|
|
3766
|
+
value = true;
|
|
3767
|
+
else if (rawValue === "false")
|
|
3768
|
+
value = false;
|
|
3769
|
+
else if (rawValue !== "" && !Number.isNaN(Number(rawValue))) {
|
|
3770
|
+
value = Number(rawValue);
|
|
3771
|
+
}
|
|
3772
|
+
return { key, value };
|
|
3773
|
+
}
|
|
3774
|
+
function renderList(sources) {
|
|
3775
|
+
if (sources.length === 0) {
|
|
3776
|
+
return `当前没有已配置的事件源。
|
|
3777
|
+
(可用「##command##: es add <name> <type>」新增)`;
|
|
3778
|
+
}
|
|
3779
|
+
const lines = sources.map((s) => {
|
|
3780
|
+
const name = String(s.name ?? "?");
|
|
3781
|
+
const type = String(s.type ?? "?");
|
|
3782
|
+
const enabled = s.enabled === false ? "disabled" : "enabled";
|
|
3783
|
+
const id = String(s.id ?? "");
|
|
3784
|
+
return `- ${name} [${type}] ${enabled} (${id})`;
|
|
3785
|
+
});
|
|
3786
|
+
return `事件源列表(${sources.length} 个):
|
|
3787
|
+
${lines.join(`
|
|
3788
|
+
`)}`;
|
|
3789
|
+
}
|
|
3790
|
+
var esCommand = {
|
|
3791
|
+
name: "es",
|
|
3792
|
+
description: "管理事件源(list/add/del/mod),改磁盘配置",
|
|
3793
|
+
usage: "##command##: es <list|add|del|mod> [参数]",
|
|
3794
|
+
async execute(ctx) {
|
|
3795
|
+
try {
|
|
3796
|
+
return await executeEsCommand(ctx);
|
|
3797
|
+
} catch (err) {
|
|
3798
|
+
return {
|
|
3799
|
+
ok: false,
|
|
3800
|
+
error: `event-sources.json 读写失败:${err.message}`
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
};
|
|
3805
|
+
async function executeEsCommand(ctx) {
|
|
3806
|
+
{
|
|
3807
|
+
const esFile = resolveEsFilePath(ctx.configPath);
|
|
3808
|
+
const [sub, ...rest] = ctx.args;
|
|
3809
|
+
switch (sub) {
|
|
3810
|
+
case "list":
|
|
3811
|
+
case undefined: {
|
|
3812
|
+
const data = readEsFile(esFile);
|
|
3813
|
+
return dualResult2(renderList(data.sources));
|
|
3814
|
+
}
|
|
3815
|
+
case "add": {
|
|
3816
|
+
const [name, type] = rest;
|
|
3817
|
+
if (!name || !type) {
|
|
3818
|
+
return { ok: false, error: `用法:##command##: es add <name> <type>
|
|
3819
|
+
${USAGE2}` };
|
|
3820
|
+
}
|
|
3821
|
+
const data = readEsFile(esFile);
|
|
3822
|
+
if (findSource(data.sources, name)) {
|
|
3823
|
+
return {
|
|
3824
|
+
ok: false,
|
|
3825
|
+
error: `事件源「${name}」已存在(add 拒绝重名)。可用 es mod 修改或 es del 删除后重建。`
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
const id = `es-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3829
|
+
const source = {
|
|
3830
|
+
id,
|
|
3831
|
+
name,
|
|
3832
|
+
type,
|
|
3833
|
+
enabled: true
|
|
3834
|
+
};
|
|
3835
|
+
data.sources.push(source);
|
|
3836
|
+
writeEsFile(esFile, data);
|
|
3837
|
+
return dualResult2(`✅ 已新增事件源「${name}」[${type}](id=${id},enabled)。` + `重启进程后生效(##command##: reset)。`);
|
|
3838
|
+
}
|
|
3839
|
+
case "del":
|
|
3840
|
+
case "remove": {
|
|
3841
|
+
const target = rest[0];
|
|
3842
|
+
if (!target) {
|
|
3843
|
+
return { ok: false, error: `用法:##command##: es del <name|id>
|
|
3844
|
+
${USAGE2}` };
|
|
3845
|
+
}
|
|
3846
|
+
const data = readEsFile(esFile);
|
|
3847
|
+
const src = findSource(data.sources, target);
|
|
3848
|
+
if (!src) {
|
|
3849
|
+
return { ok: false, error: `未找到事件源「${target}」。` };
|
|
3850
|
+
}
|
|
3851
|
+
data.sources = data.sources.filter((s) => s !== src);
|
|
3852
|
+
writeEsFile(esFile, data);
|
|
3853
|
+
return dualResult2(`✅ 已删除事件源「${String(src.name)}」(id=${String(src.id)})。` + `重启进程后生效(##command##: reset)。`);
|
|
3854
|
+
}
|
|
3855
|
+
case "mod":
|
|
3856
|
+
case "update": {
|
|
3857
|
+
const [target, ...pairs] = rest;
|
|
3858
|
+
if (!target || pairs.length === 0) {
|
|
3859
|
+
return {
|
|
3860
|
+
ok: false,
|
|
3861
|
+
error: `用法:##command##: es mod <name|id> k=v [k=v...]
|
|
3862
|
+
${USAGE2}`
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
const data = readEsFile(esFile);
|
|
3866
|
+
const src = findSource(data.sources, target);
|
|
3867
|
+
if (!src) {
|
|
3868
|
+
return { ok: false, error: `未找到事件源「${target}」。` };
|
|
3869
|
+
}
|
|
3870
|
+
const applied = [];
|
|
3871
|
+
for (const pair of pairs) {
|
|
3872
|
+
const kv = parseKv(pair);
|
|
3873
|
+
if (!kv) {
|
|
3874
|
+
return { ok: false, error: `参数格式错误:「${pair}」(应为 k=v)
|
|
3875
|
+
${USAGE2}` };
|
|
3876
|
+
}
|
|
3877
|
+
if (!MOD_ALLOWED_FIELDS.includes(kv.key)) {
|
|
3878
|
+
return {
|
|
3879
|
+
ok: false,
|
|
3880
|
+
error: `字段「${kv.key}」不允许修改(白名单:${MOD_ALLOWED_FIELDS.join("/")};id/type 不可改)。`
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3883
|
+
if (kv.key === "name" && typeof kv.value === "string" && kv.value !== src.name && data.sources.some((s0) => s0 !== src && s0.name === kv.value)) {
|
|
3884
|
+
return {
|
|
3885
|
+
ok: false,
|
|
3886
|
+
error: `名称「${String(kv.value)}」已被其它事件源占用,改 name 拒绝。`
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
let value = kv.value;
|
|
3890
|
+
if (kv.key === "enabled") {
|
|
3891
|
+
value = kv.value === true || kv.value === 1 || kv.value === "1" || kv.value === "true" || kv.value === "yes" || kv.value === "on";
|
|
3892
|
+
}
|
|
3893
|
+
src[kv.key] = value;
|
|
3894
|
+
applied.push(`${kv.key}=${String(value)}`);
|
|
3895
|
+
}
|
|
3896
|
+
writeEsFile(esFile, data);
|
|
3897
|
+
return dualResult2(`✅ 已修改事件源「${String(src.name)}」:${applied.join(",")}。` + `重启进程后生效(##command##: reset)。`);
|
|
3898
|
+
}
|
|
3899
|
+
default:
|
|
3900
|
+
return {
|
|
3901
|
+
ok: false,
|
|
3902
|
+
error: `未知子命令「${sub}」。
|
|
3903
|
+
${USAGE2}`
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
|
|
1802
3909
|
// src/env/event-source/command-registry.ts
|
|
1803
3910
|
var logger2 = createLogger("command-registry");
|
|
1804
3911
|
var COMMAND_PREFIX = "##command##:";
|
|
@@ -1892,6 +3999,7 @@ function getGlobalCommandRegistry() {
|
|
|
1892
3999
|
registry.register(fallbackModelCommand);
|
|
1893
4000
|
registry.register(listCommand);
|
|
1894
4001
|
registry.register(resetCommand);
|
|
4002
|
+
registry.register(esCommand);
|
|
1895
4003
|
g.__royCommandRegistry = registry;
|
|
1896
4004
|
}
|
|
1897
4005
|
return g.__royCommandRegistry;
|
|
@@ -1998,7 +4106,7 @@ async function handleCommandCardAction(event, ports) {
|
|
|
1998
4106
|
|
|
1999
4107
|
// src/env/event-source/event-source-handlers.ts
|
|
2000
4108
|
init_logger();
|
|
2001
|
-
var
|
|
4109
|
+
var execAsync = promisify(execSync);
|
|
2002
4110
|
var logger4 = createLogger("lark-cli:event-source");
|
|
2003
4111
|
var CHILD_READY_TIMEOUT_MS = 5000;
|
|
2004
4112
|
var GRACEFUL_STOP_TIMEOUT_MS = 5000;
|
|
@@ -2009,12 +4117,6 @@ function isChildAlive(child) {
|
|
|
2009
4117
|
function isPluginDebugEnabled() {
|
|
2010
4118
|
return process.env.ROY_TASK_NOTIFY_DEBUG === "1" || process.env.ROY_TRACE_DEBUG === "1";
|
|
2011
4119
|
}
|
|
2012
|
-
var BUILT_IN_PLUGIN_FACTORIES = {
|
|
2013
|
-
LarkCliTaskNotifyPlugin: (cfg) => createLarkCliTaskNotifyHook(cfg),
|
|
2014
|
-
LarkCliWorkflowNotifyPlugin: (cfg) => createLarkCliWorkflowNotifyHook(cfg),
|
|
2015
|
-
PerTaskGroupChatPlugin: (cfg) => createPerTaskGroupChatPlugin(cfg),
|
|
2016
|
-
"task-tag": () => new TaskTagPlugin
|
|
2017
|
-
};
|
|
2018
4120
|
function matchEventType(eventType, patterns) {
|
|
2019
4121
|
for (const pattern of patterns) {
|
|
2020
4122
|
if (pattern === "*")
|
|
@@ -2029,17 +4131,85 @@ function matchEventType(eventType, patterns) {
|
|
|
2029
4131
|
}
|
|
2030
4132
|
return false;
|
|
2031
4133
|
}
|
|
4134
|
+
function buildSlimRawData(rawEvent) {
|
|
4135
|
+
const event = rawEvent ?? {};
|
|
4136
|
+
const innerEvent = event.event;
|
|
4137
|
+
const message = innerEvent?.message ?? event.message;
|
|
4138
|
+
const sender = innerEvent?.sender ?? event.sender;
|
|
4139
|
+
const slim = {};
|
|
4140
|
+
const chatId = message?.chat_id ?? event.chat_id;
|
|
4141
|
+
if (chatId !== undefined)
|
|
4142
|
+
slim.chat_id = chatId;
|
|
4143
|
+
const messageId = message?.message_id ?? event.message_id ?? event.id;
|
|
4144
|
+
if (messageId !== undefined)
|
|
4145
|
+
slim.message_id = messageId;
|
|
4146
|
+
const rawSenderId = sender?.sender_id ?? event.sender_id;
|
|
4147
|
+
let senderId;
|
|
4148
|
+
if (typeof rawSenderId === "string") {
|
|
4149
|
+
senderId = rawSenderId;
|
|
4150
|
+
} else if (rawSenderId && typeof rawSenderId === "object") {
|
|
4151
|
+
const obj = rawSenderId;
|
|
4152
|
+
senderId = obj.open_id ?? obj.user_id ?? obj.union_id;
|
|
4153
|
+
}
|
|
4154
|
+
if (senderId !== undefined)
|
|
4155
|
+
slim.sender_id = senderId;
|
|
4156
|
+
const chatType = message?.chat_type ?? event.chat_type;
|
|
4157
|
+
if (chatType !== undefined)
|
|
4158
|
+
slim.chat_type = chatType;
|
|
4159
|
+
const content = message?.content ?? event.content;
|
|
4160
|
+
if (content !== undefined)
|
|
4161
|
+
slim.content = content;
|
|
4162
|
+
const mentions = message?.mentions ?? event.mentions;
|
|
4163
|
+
if (mentions !== undefined)
|
|
4164
|
+
slim.mentions = mentions;
|
|
4165
|
+
return slim;
|
|
4166
|
+
}
|
|
2032
4167
|
function buildLarkRecommendedAction(opts) {
|
|
2033
|
-
const { profile, chatType, appId, chatId, messageId } = opts;
|
|
4168
|
+
const { profile, chatType, appId, chatId, messageId, senderId } = opts;
|
|
2034
4169
|
const profileFlag = profile ? `--profile ${profile} ` : "";
|
|
2035
4170
|
const profileWarn = profile ? `(⚠️ 必须用 profile="${profile}" 调 lark-cli;openId per-app 隔离,换 profile 会失败)` : "";
|
|
2036
4171
|
const isGroup = chatType === "group";
|
|
2037
|
-
const
|
|
4172
|
+
const senderIdForPrompt = senderId || "<senderId>";
|
|
4173
|
+
const scopeHint = isGroup ? `群聊消息;回复到 replyTo.chatId。如需 @发件人,用结构化 at 节点 {"tag":"at","user_id":"${senderIdForPrompt}"}(不要写 <at> 文本标签)` : "p2p 私聊消息;回复到 replyTo.chatId";
|
|
2038
4174
|
const scopeWord = isGroup ? "群聊" : "p2p 私聊";
|
|
2039
4175
|
const action = [
|
|
2040
4176
|
`处理飞书 ${scopeWord}消息并以机器人身份回复${profileWarn}。`,
|
|
2041
4177
|
`scope: ${scopeHint}。`,
|
|
2042
4178
|
``,
|
|
4179
|
+
...larkStaticRuleLines(profileFlag, senderIdForPrompt),
|
|
4180
|
+
``,
|
|
4181
|
+
`## 必须使用 replyTo 里给定的值`,
|
|
4182
|
+
`profile / chatId / messageId / --as bot 必须原样取自 recommendedAction.replyTo;不要凭印象自己拼接。`
|
|
4183
|
+
].join(`
|
|
4184
|
+
`);
|
|
4185
|
+
const actionWithReplyContext = [
|
|
4186
|
+
action,
|
|
4187
|
+
larkReplyContextSection(profileFlag)
|
|
4188
|
+
].join(`
|
|
4189
|
+
`);
|
|
4190
|
+
return {
|
|
4191
|
+
action: actionWithReplyContext,
|
|
4192
|
+
replyTo: {
|
|
4193
|
+
appId,
|
|
4194
|
+
profile,
|
|
4195
|
+
chatId,
|
|
4196
|
+
messageId
|
|
4197
|
+
}
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
var LARK_CHANNEL_PROMPT_SECTION_TITLE = "⭐ 飞书回复规范(system prompt 注入版)";
|
|
4201
|
+
var LARK_PROMPT_LENGTH_BUDGET = 5900;
|
|
4202
|
+
function larkReplyContextSection(profileFlag) {
|
|
4203
|
+
return [
|
|
4204
|
+
``,
|
|
4205
|
+
`## 引用消息上下文 replyContext`,
|
|
4206
|
+
`recommendedAction.replyContext 存在 = 本条是对 parentId 的回复。`,
|
|
4207
|
+
`- 取原文:lark-cli ${profileFlag}im +messages-mget --message-ids <parentId> --as bot(best-effort)`
|
|
4208
|
+
].join(`
|
|
4209
|
+
`);
|
|
4210
|
+
}
|
|
4211
|
+
function larkStaticRuleLines(profileFlag, senderIdForPrompt) {
|
|
4212
|
+
return [
|
|
2043
4213
|
`## ⭐ 关键 0:整次回复只发一条结构化 post(禁分段)`,
|
|
2044
4214
|
`> 指令优先级:本 prompt 与 lark-* skill 文档冲突时**以本 prompt 为准**`,
|
|
2045
4215
|
`> (例:lark-im skill 建议 --markdown / ANSI-C 引号,本项目一律走下面模板)。`,
|
|
@@ -2074,6 +4244,13 @@ function buildLarkRecommendedAction(opts) {
|
|
|
2074
4244
|
` ]}}`,
|
|
2075
4245
|
``,
|
|
2076
4246
|
`- @mention:用 {"tag":"at","user_id":"ou_xxx"}(user_id 填对方 open_id;@所有人填 "all")`,
|
|
4247
|
+
`- **群聊必须 @ sender**:若接收的消息是 \`chat_type: group\`,回复时 content **必须**保留结构化 at 节点 @ 发送者(user_id 填 senderId,即消息发送者的 open_id);**禁止只用引用回复**(quote/reply 不算 @,群聊里对方收不到提醒);私聊(p2p)不需要。群聊可跑示例(照抄,--arg uid 填消息发送者 open_id):`,
|
|
4248
|
+
` body_file="$(mktemp /tmp/lark-ask.XXXXXX.md)"`,
|
|
4249
|
+
` cat > "$body_file" <<'EOF2'`,
|
|
4250
|
+
` 回复正文……`,
|
|
4251
|
+
` EOF2`,
|
|
4252
|
+
` content="$(jq -nc --rawfile md "$body_file" --arg uid "${senderIdForPrompt}" '{zh_cn:{content:[[{tag:"at",user_id:$uid}],[{tag:"md",text:$md}]]}}')"`,
|
|
4253
|
+
` lark-cli ${profileFlag}im +messages-reply --message-id <replyTo.messageId> --as bot --msg-type post --content "$content"`,
|
|
2077
4254
|
`- 正文:全部放进 {"tag":"md","text":"..."} 这一个节点里`,
|
|
2078
4255
|
`- 不需要 @ 时,content 里只放 md 节点:{"zh_cn":{"content":[[{"tag":"md","text":"..."}]]}}`,
|
|
2079
4256
|
`- md 节点支持 CommonMark 0.31 + GFM:ATX 标题(#~######)/ 表格 / 任务列表 / 加粗 / 围栏代码块 / 超链接`,
|
|
@@ -2091,7 +4268,7 @@ function buildLarkRecommendedAction(opts) {
|
|
|
2091
4268
|
` cat > "$body_file" <<'LARK_REPLY_EOF'`,
|
|
2092
4269
|
` ...(回复的 Markdown 正文,可多行 / 含表格 / 含特殊字符;不要写 <at> 标签)`,
|
|
2093
4270
|
` LARK_REPLY_EOF`,
|
|
2094
|
-
` content="$(jq -nc --rawfile md "$body_file" --arg uid "
|
|
4271
|
+
` content="$(jq -nc --rawfile md "$body_file" --arg uid "${senderIdForPrompt}" '{zh_cn:{content:[[{tag:"at",user_id:$uid}],[{tag:"md",text:$md}]]}}')"`,
|
|
2095
4272
|
` lark-cli ${profileFlag}im +messages-reply --message-id <取自 replyTo.messageId> --as bot --msg-type post --content "$content"`,
|
|
2096
4273
|
``,
|
|
2097
4274
|
`不需要 @ 某人时,把 jq 表达式换成 '{zh_cn:{content:[[{tag:"md",text:$md}]]}}'。`,
|
|
@@ -2135,20 +4312,60 @@ function buildLarkRecommendedAction(opts) {
|
|
|
2135
4312
|
` - 飞书消息正文里常含裸 \`"\`、\`|\`、Markdown 表格分隔符 → json.load 会报 \`Expecting ',' delimiter\` 假失败`,
|
|
2136
4313
|
` - 假失败 → 重发 → 形成 3 份相同消息`,
|
|
2137
4314
|
`- 重试建议:固定 idempotency-key(飞书层面去重,**最长 50 字符**),不要靠内容 hash 自检`,
|
|
4315
|
+
``,
|
|
4316
|
+
`## 文档交付规范:产出物存飞书云盘,云盘链接交付`,
|
|
4317
|
+
`- 本渠道触发的**文档类生成**(调研文档 / PPT / doc 等)必须用**飞书云盘**存储产出物`,
|
|
4318
|
+
` (\`lark-cli docs +create --title <标题> --markdown @<本地md>\`,用完即弃)`,
|
|
4319
|
+
`- 交付时只给**云盘链接**(https://my.feishu.cn/...),**不落本地路径**交付`,
|
|
4320
|
+
` - 本地路径只作中转(生成 md → 上传云盘 → 回复里贴云盘链接),不作为交付物`
|
|
4321
|
+
];
|
|
4322
|
+
}
|
|
4323
|
+
function buildLarkChannelSystemPrompt(opts) {
|
|
4324
|
+
const { profile } = opts;
|
|
4325
|
+
const profileFlag = profile ? `--profile ${profile} ` : "";
|
|
4326
|
+
const profileWarn = profile ? `(⚠️ 必须用 profile="${profile}" 调 lark-cli;openId per-app 隔离,换 profile 会失败)` : "";
|
|
4327
|
+
const header = [
|
|
4328
|
+
`# ${LARK_CHANNEL_PROMPT_SECTION_TITLE}`,
|
|
4329
|
+
``,
|
|
4330
|
+
`本节是飞书渠道的**回复规范**(静态规则,随 system prompt 注入)。`,
|
|
4331
|
+
`每条飞书事件的 \`recommendedAction\` 只带动态定位(replyTo 四元组),`,
|
|
4332
|
+
`profile / chatId / messageId 是本轮唯一权威定位,其余回复规则全部在此。`,
|
|
4333
|
+
profile ? `本渠道 profile:${profile}${profileWarn}` : `本渠道未配置 profile(使用 lark-cli active profile)。`,
|
|
4334
|
+
``
|
|
4335
|
+
].join(`
|
|
4336
|
+
`);
|
|
4337
|
+
const footer = [
|
|
2138
4338
|
``,
|
|
2139
4339
|
`## 必须使用 replyTo 里给定的值`,
|
|
2140
|
-
`profile / chatId / messageId / --as bot 必须原样取自 recommendedAction.replyTo
|
|
4340
|
+
`profile / chatId / messageId / --as bot 必须原样取自 recommendedAction.replyTo;不要凭印象自己拼接。`,
|
|
4341
|
+
larkReplyContextSection(profileFlag)
|
|
2141
4342
|
].join(`
|
|
2142
4343
|
`);
|
|
2143
|
-
return
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
4344
|
+
return header + larkStaticRuleLines(profileFlag, "<senderId>").join(`
|
|
4345
|
+
`) + footer;
|
|
4346
|
+
}
|
|
4347
|
+
function buildLarkLeanAction(chatType, senderId) {
|
|
4348
|
+
const isGroup = chatType === "group";
|
|
4349
|
+
const senderIdForPrompt = senderId || "<senderId>";
|
|
4350
|
+
const scopeHint = isGroup ? `群聊消息;回复到 replyTo.chatId。如需 @发件人,用结构化 at 节点 {"tag":"at","user_id":"${senderIdForPrompt}"}(不要写 <at> 文本标签)` : "p2p 私聊消息;回复到 replyTo.chatId";
|
|
4351
|
+
return [
|
|
4352
|
+
`处理飞书${isGroup ? "群聊" : "p2p 私聊"}消息并回复。回复规范见 system prompt「${LARK_CHANNEL_PROMPT_SECTION_TITLE}」章节。`,
|
|
4353
|
+
`scope: ${scopeHint}。`,
|
|
4354
|
+
`replyTo 的 profile/chatId/messageId 是本轮唯一权威定位;其余回复规则(发送命令模板、长度上限、发送纪律等)全部见 system prompt 上述章节。`
|
|
4355
|
+
].join(`
|
|
4356
|
+
`);
|
|
4357
|
+
}
|
|
4358
|
+
function resolveLarkChannelSystemPrompt(config) {
|
|
4359
|
+
const mode = config.channelPromptMode ?? "lean";
|
|
4360
|
+
if (mode === "legacy") {
|
|
4361
|
+
return config.systemPrompt;
|
|
4362
|
+
}
|
|
4363
|
+
const channelPrompt = buildLarkChannelSystemPrompt({
|
|
4364
|
+
profile: config.profile
|
|
4365
|
+
});
|
|
4366
|
+
return config.systemPrompt ? config.systemPrompt + `
|
|
4367
|
+
|
|
4368
|
+
` + channelPrompt : channelPrompt;
|
|
2152
4369
|
}
|
|
2153
4370
|
function extractMetadata(rawEvent, eventType) {
|
|
2154
4371
|
const event = rawEvent;
|
|
@@ -2184,6 +4401,19 @@ function extractMetadata(rawEvent, eventType) {
|
|
|
2184
4401
|
metadata.chatType = messageData.chat_type;
|
|
2185
4402
|
metadata.messageId = messageData.message_id;
|
|
2186
4403
|
metadata.messageType = messageData.message_type;
|
|
4404
|
+
const rootId = messageData.root_id ?? event.root_id;
|
|
4405
|
+
const parentId = messageData.parent_id ?? event.reply_to ?? event.parent_id;
|
|
4406
|
+
const threadId = messageData.thread_id ?? event.thread_id;
|
|
4407
|
+
if (rootId || parentId || threadId) {
|
|
4408
|
+
const replyContext = {};
|
|
4409
|
+
if (rootId)
|
|
4410
|
+
replyContext.rootId = rootId;
|
|
4411
|
+
if (parentId)
|
|
4412
|
+
replyContext.parentId = parentId;
|
|
4413
|
+
if (threadId)
|
|
4414
|
+
replyContext.threadId = threadId;
|
|
4415
|
+
metadata.replyContext = replyContext;
|
|
4416
|
+
}
|
|
2187
4417
|
}
|
|
2188
4418
|
if (typeof senderId === "string") {
|
|
2189
4419
|
metadata.senderId = senderId;
|
|
@@ -2193,13 +4423,34 @@ function extractMetadata(rawEvent, eventType) {
|
|
|
2193
4423
|
let recommendedAction;
|
|
2194
4424
|
if (eventType === "im.message.receive_v1" || event.type === "im.message.receive_v1") {
|
|
2195
4425
|
const profile2 = event.profile;
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
4426
|
+
const promptMode = event.channelPromptMode ?? "lean";
|
|
4427
|
+
if (promptMode === "legacy") {
|
|
4428
|
+
recommendedAction = buildLarkRecommendedAction({
|
|
4429
|
+
profile: profile2,
|
|
4430
|
+
chatType: metadata.chatType,
|
|
4431
|
+
appId: metadata.appId,
|
|
4432
|
+
chatId: metadata.chatId,
|
|
4433
|
+
messageId: metadata.messageId,
|
|
4434
|
+
senderId: metadata.senderId
|
|
4435
|
+
});
|
|
4436
|
+
} else {
|
|
4437
|
+
recommendedAction = {
|
|
4438
|
+
action: buildLarkLeanAction(metadata.chatType, metadata.senderId),
|
|
4439
|
+
replyTo: {
|
|
4440
|
+
appId: metadata.appId,
|
|
4441
|
+
profile: profile2,
|
|
4442
|
+
chatId: metadata.chatId,
|
|
4443
|
+
messageId: metadata.messageId
|
|
4444
|
+
},
|
|
4445
|
+
staticRulesInSystemPrompt: true
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
if (metadata.replyContext) {
|
|
4449
|
+
recommendedAction = {
|
|
4450
|
+
...recommendedAction,
|
|
4451
|
+
replyContext: metadata.replyContext
|
|
4452
|
+
};
|
|
4453
|
+
}
|
|
2203
4454
|
}
|
|
2204
4455
|
const profile = event.profile;
|
|
2205
4456
|
const senderOpenId = typeof senderId === "string" ? senderId : senderId?.open_id;
|
|
@@ -2210,7 +4461,8 @@ function extractMetadata(rawEvent, eventType) {
|
|
|
2210
4461
|
chatId: messageData?.chat_id,
|
|
2211
4462
|
messageId: messageData?.message_id,
|
|
2212
4463
|
params: {
|
|
2213
|
-
userId: senderOpenId
|
|
4464
|
+
userId: senderOpenId,
|
|
4465
|
+
statusNotifications: event.statusNotifications === true
|
|
2214
4466
|
}
|
|
2215
4467
|
};
|
|
2216
4468
|
return { metadata, replyChannel, recommendedAction };
|
|
@@ -2244,6 +4496,15 @@ class LarkCliInstance {
|
|
|
2244
4496
|
recentEventIds = new Map;
|
|
2245
4497
|
DEDUP_WINDOW_MS = 60000;
|
|
2246
4498
|
DEDUP_MAX_SIZE = 100;
|
|
4499
|
+
cachedChannelSystemPrompt;
|
|
4500
|
+
channelSystemPromptResolved = false;
|
|
4501
|
+
getChannelSystemPrompt() {
|
|
4502
|
+
if (!this.channelSystemPromptResolved) {
|
|
4503
|
+
this.cachedChannelSystemPrompt = resolveLarkChannelSystemPrompt(this.config);
|
|
4504
|
+
this.channelSystemPromptResolved = true;
|
|
4505
|
+
}
|
|
4506
|
+
return this.cachedChannelSystemPrompt;
|
|
4507
|
+
}
|
|
2247
4508
|
environment;
|
|
2248
4509
|
loadedPlugins = [];
|
|
2249
4510
|
constructor(config) {
|
|
@@ -2291,7 +4552,7 @@ class LarkCliInstance {
|
|
|
2291
4552
|
logger4.info(`[LarkCliInstance] 已监听 ${CARD_ACTION_EVENT_TYPE}。` + `⚠️ 若点击卡片无反应,先确认飞书开发者后台「事件与回调 → 回调配置」已开启该事件。`);
|
|
2292
4553
|
}
|
|
2293
4554
|
spawnConsumeChild(command) {
|
|
2294
|
-
return
|
|
4555
|
+
return spawn3("exec " + command, [], {
|
|
2295
4556
|
shell: true,
|
|
2296
4557
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2297
4558
|
windowsHide: true
|
|
@@ -2335,7 +4596,7 @@ class LarkCliInstance {
|
|
|
2335
4596
|
const profile = this.config.profile;
|
|
2336
4597
|
const profileFlag = profile ? `--profile ${profile} ` : "";
|
|
2337
4598
|
try {
|
|
2338
|
-
const { stdout } = await
|
|
4599
|
+
const { stdout } = await execAsync(`lark-cli ${profileFlag}event status --json`, {
|
|
2339
4600
|
timeout: 5000
|
|
2340
4601
|
});
|
|
2341
4602
|
const parsed = JSON.parse(stdout);
|
|
@@ -2428,13 +4689,13 @@ class LarkCliInstance {
|
|
|
2428
4689
|
logger4.warn(`[LarkCliInstance] Error sending SIGTERM to ${child.pid}: ${error}`);
|
|
2429
4690
|
}
|
|
2430
4691
|
}
|
|
2431
|
-
const allExited = Promise.all(children.map((child) => new Promise((
|
|
4692
|
+
const allExited = Promise.all(children.map((child) => new Promise((resolve2) => {
|
|
2432
4693
|
if (isChildAlive(child) === false)
|
|
2433
|
-
return
|
|
2434
|
-
child.once("exit", () =>
|
|
2435
|
-
child.once("error", () =>
|
|
4694
|
+
return resolve2();
|
|
4695
|
+
child.once("exit", () => resolve2());
|
|
4696
|
+
child.once("error", () => resolve2());
|
|
2436
4697
|
})));
|
|
2437
|
-
const timeout = new Promise((
|
|
4698
|
+
const timeout = new Promise((resolve2) => setTimeout(resolve2, GRACEFUL_STOP_TIMEOUT_MS));
|
|
2438
4699
|
await Promise.race([allExited, timeout]);
|
|
2439
4700
|
for (const child of children) {
|
|
2440
4701
|
if (isChildAlive(child)) {
|
|
@@ -2488,7 +4749,7 @@ class LarkCliInstance {
|
|
|
2488
4749
|
logger4.debug(`[LarkCliInstance] card.action.trigger 端口未注入:taskComponent 不可用` + `(getTask=${typeof taskComponent?.getTask}, updateTask=${typeof taskComponent?.updateTask})。` + `卡片按钮点击会被跳过。`);
|
|
2489
4750
|
return;
|
|
2490
4751
|
}
|
|
2491
|
-
const sharedNotifier = this.pluginInstances.get("LarkCliTaskNotifyPlugin")?.cardNotifier ?? this.pluginInstances.get("PerTaskGroupChatPlugin")?.cardNotifier;
|
|
4752
|
+
const sharedNotifier = this.pluginInstances.get("LarkCliTaskNotifyPlugin")?.cardNotifier ?? this.pluginInstances.get("PerTaskGroupChatPlugin")?.cardNotifier ?? getSharedLarkCardNotifier();
|
|
2492
4753
|
this.setCardActionPorts({
|
|
2493
4754
|
getTask: async (id) => {
|
|
2494
4755
|
const t = await taskComponent.getTask(id);
|
|
@@ -2512,7 +4773,117 @@ class LarkCliInstance {
|
|
|
2512
4773
|
},
|
|
2513
4774
|
createGroupChat: (opts) => createGroupChatViaLarkCli(opts),
|
|
2514
4775
|
deleteGroupChat: (chatId, profile) => deleteGroupChatViaLarkCli(chatId, profile),
|
|
2515
|
-
notifier: sharedNotifier ??
|
|
4776
|
+
notifier: sharedNotifier ?? getSharedLarkCardNotifier(),
|
|
4777
|
+
resolveSessionIdByTask: async (taskId) => {
|
|
4778
|
+
const fromNotifier = sharedNotifier?.getSessionIdByTask?.(taskId);
|
|
4779
|
+
if (fromNotifier)
|
|
4780
|
+
return fromNotifier;
|
|
4781
|
+
const tc = taskComponent;
|
|
4782
|
+
try {
|
|
4783
|
+
const rec = getSharedLarkCardStateStore().get(taskId);
|
|
4784
|
+
if (rec?.sessionId)
|
|
4785
|
+
return rec.sessionId;
|
|
4786
|
+
} catch {}
|
|
4787
|
+
return;
|
|
4788
|
+
},
|
|
4789
|
+
loadSessionActivity: createSessionActivityProbe({
|
|
4790
|
+
getSession: async (sessionId) => {
|
|
4791
|
+
const sc = this.environment?.getComponent?.("session");
|
|
4792
|
+
if (!sc?.get)
|
|
4793
|
+
return;
|
|
4794
|
+
const s = await sc.get(sessionId);
|
|
4795
|
+
if (!s)
|
|
4796
|
+
return;
|
|
4797
|
+
return {
|
|
4798
|
+
...typeof s.messageCount === "number" ? { messageCount: s.messageCount } : {},
|
|
4799
|
+
...typeof s.updatedAt === "number" ? { updatedAt: s.updatedAt } : {}
|
|
4800
|
+
};
|
|
4801
|
+
},
|
|
4802
|
+
getLastMessage: async (sessionId) => {
|
|
4803
|
+
const sc = this.environment?.getComponent?.("session");
|
|
4804
|
+
if (!sc?.getMessages)
|
|
4805
|
+
return;
|
|
4806
|
+
const tail = await sc.getMessages(sessionId, { offset: -1, limit: 1 });
|
|
4807
|
+
return toActivityMessage(tail?.[tail.length - 1]);
|
|
4808
|
+
},
|
|
4809
|
+
getLifecycle: (sessionId) => {
|
|
4810
|
+
const bgTasks = taskComponent.getBackgroundTaskManager?.()?.listTasks?.() ?? [];
|
|
4811
|
+
const hit = bgTasks.find((t) => t?.subSessionId === sessionId);
|
|
4812
|
+
return mapBgTaskStatusToSessionLifecycle(hit?.status);
|
|
4813
|
+
}
|
|
4814
|
+
}),
|
|
4815
|
+
loadSubSessionActivity: createActivityProbe({
|
|
4816
|
+
listBackgroundTasks: () => taskComponent.getBackgroundTaskManager?.()?.listTasks?.(),
|
|
4817
|
+
getSubSessionActivity: (bgId) => taskComponent.getBackgroundTaskManager?.()?.getSubSessionActivity?.(bgId),
|
|
4818
|
+
getLastMessage: async (subSessionId) => {
|
|
4819
|
+
const sessionComponent = this.environment?.getComponent?.("session");
|
|
4820
|
+
if (!sessionComponent?.getMessages)
|
|
4821
|
+
return;
|
|
4822
|
+
const tail = await sessionComponent.getMessages(subSessionId, {
|
|
4823
|
+
offset: -1,
|
|
4824
|
+
limit: 1
|
|
4825
|
+
});
|
|
4826
|
+
return toActivityMessage(tail?.[tail.length - 1]);
|
|
4827
|
+
},
|
|
4828
|
+
isTerminalTask: async (taskId) => {
|
|
4829
|
+
const t = await taskComponent.getTask?.(taskId);
|
|
4830
|
+
const status = t?.status;
|
|
4831
|
+
return status === "completed" || status === "cancelled" || status === "failed" || status === "archived";
|
|
4832
|
+
}
|
|
4833
|
+
}),
|
|
4834
|
+
loadSessionMessages: async (sessionId, limit) => {
|
|
4835
|
+
const sc = this.environment?.getComponent?.("session");
|
|
4836
|
+
if (!sc?.getMessages)
|
|
4837
|
+
return;
|
|
4838
|
+
const n = typeof limit === "number" && limit > 0 ? limit : RECENT_MESSAGE_LIMIT;
|
|
4839
|
+
const tail = await sc.getMessages(sessionId, {
|
|
4840
|
+
offset: -1,
|
|
4841
|
+
limit: n * 3
|
|
4842
|
+
});
|
|
4843
|
+
const calls = extractToolCallsFromMessages(tail);
|
|
4844
|
+
return calls.slice(-n);
|
|
4845
|
+
},
|
|
4846
|
+
attachGroupSession: async (args) => {
|
|
4847
|
+
const sessionComponent = this.environment?.getComponent?.("session");
|
|
4848
|
+
if (typeof sessionComponent?.attachGroupSession !== "function") {
|
|
4849
|
+
logger4.warn(`[LarkCliInstance] attachGroupSession 跳过:sessionComponent.attachGroupSession 不可用`);
|
|
4850
|
+
return;
|
|
4851
|
+
}
|
|
4852
|
+
await sessionComponent.attachGroupSession(args);
|
|
4853
|
+
logger4.info(`[LarkCliInstance] 群 session 已初始化:${args.chatId} ← ${args.originSessionId}` + `(独立会话 + 1 条 user 事件背景提示)`);
|
|
4854
|
+
},
|
|
4855
|
+
findGroupChatForSession: async (sessionId) => {
|
|
4856
|
+
const sessionComponent = this.environment?.getComponent?.("session");
|
|
4857
|
+
if (typeof sessionComponent?.findGroupChatForSession !== "function") {
|
|
4858
|
+
logger4.warn(`[LarkCliInstance] findGroupChatForSession 跳过:sessionComponent 不支持`);
|
|
4859
|
+
return;
|
|
4860
|
+
}
|
|
4861
|
+
return await sessionComponent.findGroupChatForSession(sessionId);
|
|
4862
|
+
},
|
|
4863
|
+
detachGroupSession: async (chatId) => {
|
|
4864
|
+
const sessionComponent = this.environment?.getComponent?.("session");
|
|
4865
|
+
if (typeof sessionComponent?.detachGroupSession !== "function") {
|
|
4866
|
+
logger4.warn(`[LarkCliInstance] detachGroupSession 跳过:sessionComponent.detachGroupSession 不可用`);
|
|
4867
|
+
return;
|
|
4868
|
+
}
|
|
4869
|
+
const r = await sessionComponent.detachGroupSession(chatId);
|
|
4870
|
+
if (!r.ok) {
|
|
4871
|
+
logger4.warn(`[LarkCliInstance] detachGroupSession 失败: ${chatId}`);
|
|
4872
|
+
return;
|
|
4873
|
+
}
|
|
4874
|
+
logger4.info(`[LarkCliInstance] 群 session 已标记解散:${chatId}`);
|
|
4875
|
+
},
|
|
4876
|
+
getSessionTitle: async (sessionId) => {
|
|
4877
|
+
const sessionComponent = this.environment?.getComponent?.("session");
|
|
4878
|
+
if (typeof sessionComponent?.get !== "function")
|
|
4879
|
+
return;
|
|
4880
|
+
try {
|
|
4881
|
+
const s = await sessionComponent.get(sessionId);
|
|
4882
|
+
return typeof s?.title === "string" && s.title.trim() ? s.title.trim() : undefined;
|
|
4883
|
+
} catch {
|
|
4884
|
+
return;
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
2516
4887
|
});
|
|
2517
4888
|
logger4.info(`[LarkCliInstance] card.action.trigger 端口已注入` + `(notifier ${sharedNotifier ? "复用插件共享实例" : "新建实例"})`);
|
|
2518
4889
|
}
|
|
@@ -2673,6 +5044,12 @@ class LarkCliInstance {
|
|
|
2673
5044
|
getTask: taskComponent?.getTask ? (id) => taskComponent.getTask(id) : undefined
|
|
2674
5045
|
};
|
|
2675
5046
|
plugin.setCardRoutingPorts({ ...this.cardRoutingPorts });
|
|
5047
|
+
if (typeof plugin === "object" && "syncGroupCardFromOriginFn" in plugin) {
|
|
5048
|
+
const sessionComponent = this.environment?.getComponent?.("session");
|
|
5049
|
+
if (typeof sessionComponent?.findGroupChatForSession === "function") {
|
|
5050
|
+
plugin.syncGroupCardFromOriginFn = (sid) => sessionComponent.findGroupChatForSession(sid);
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
2676
5053
|
}
|
|
2677
5054
|
injectSessionComponent(plugin) {
|
|
2678
5055
|
if (typeof plugin.setSessionComponent !== "function") {
|
|
@@ -2833,9 +5210,18 @@ class LarkCliInstance {
|
|
|
2833
5210
|
const enrichedEvent = {
|
|
2834
5211
|
...rawEvent,
|
|
2835
5212
|
profile: this.config.profile,
|
|
2836
|
-
eventSourceId: this.config.id
|
|
5213
|
+
eventSourceId: this.config.id,
|
|
5214
|
+
channelPromptMode: this.config.channelPromptMode ?? "lean",
|
|
5215
|
+
statusNotifications: this.config.statusNotifications === true
|
|
2837
5216
|
};
|
|
2838
5217
|
const { metadata, replyChannel, recommendedAction } = extractMetadata(enrichedEvent, eventType);
|
|
5218
|
+
const channelSystemPrompt = this.getChannelSystemPrompt();
|
|
5219
|
+
if (channelSystemPrompt !== undefined) {
|
|
5220
|
+
metadata.systemPrompt = channelSystemPrompt;
|
|
5221
|
+
}
|
|
5222
|
+
if (this.config.extendTools?.length) {
|
|
5223
|
+
metadata.extendTools = [...this.config.extendTools];
|
|
5224
|
+
}
|
|
2839
5225
|
failureReplyChannel = replyChannel;
|
|
2840
5226
|
const commandText = isCardAction ? undefined : extractCommandText(enrichedEvent);
|
|
2841
5227
|
const parentTaskId = isCardAction ? null : await this.resolveChatTaskBinding(metadata?.chatId);
|
|
@@ -2864,8 +5250,9 @@ class LarkCliInstance {
|
|
|
2864
5250
|
console.error(`[LarkCliInstance] emitTriggerEvent failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2865
5251
|
}
|
|
2866
5252
|
}
|
|
5253
|
+
const slimRawData = buildSlimRawData(rawEvent);
|
|
2867
5254
|
const message = JSON.stringify({
|
|
2868
|
-
rawData,
|
|
5255
|
+
rawData: slimRawData,
|
|
2869
5256
|
recommendedAction,
|
|
2870
5257
|
...triggerEventId ? { triggerEventId } : {}
|
|
2871
5258
|
});
|
|
@@ -2888,6 +5275,8 @@ class LarkCliInstance {
|
|
|
2888
5275
|
pluginEnabled,
|
|
2889
5276
|
replyChannel,
|
|
2890
5277
|
agent,
|
|
5278
|
+
...channelSystemPrompt !== undefined ? { systemPrompt: channelSystemPrompt } : {},
|
|
5279
|
+
...this.config.extendTools?.length ? { extendTools: [...this.config.extendTools] } : {},
|
|
2891
5280
|
parentTaskId: parentTaskId ?? undefined,
|
|
2892
5281
|
...triggerEventId ? { triggerEventId } : {},
|
|
2893
5282
|
traceContext: eventTraceContext
|
|
@@ -2981,7 +5370,12 @@ class LarkCliInstance {
|
|
|
2981
5370
|
}, this.cardActionPorts);
|
|
2982
5371
|
if (result.ok && result.cardUpdate) {
|
|
2983
5372
|
const patch = this.cardActionPorts.patchByToken ?? patchCardByLarkCli;
|
|
2984
|
-
|
|
5373
|
+
try {
|
|
5374
|
+
await patch(token, result.cardUpdate, profile);
|
|
5375
|
+
logger4.info(`[LarkCliInstance] 卡片 patch 成功: messageId=${messageId} action=${String(result.action)} chatId=${chatId}`);
|
|
5376
|
+
} catch (patchErr) {
|
|
5377
|
+
logger4.warn(`[LarkCliInstance] 卡片 patch 失败: messageId=${messageId} ` + `action=${String(result.action)} error=${patchErr.message}` + `(token 30min 有效,过期即失败——需用新卡重试)`);
|
|
5378
|
+
}
|
|
2985
5379
|
} else {
|
|
2986
5380
|
logger4.warn(`[LarkCliInstance] handleCardActionEvent failed: ${result.error ?? "unknown"}`);
|
|
2987
5381
|
}
|
|
@@ -3138,8 +5532,8 @@ class LarkCliInstance {
|
|
|
3138
5532
|
if (profile) {
|
|
3139
5533
|
args.unshift("--profile", profile);
|
|
3140
5534
|
}
|
|
3141
|
-
await new Promise((
|
|
3142
|
-
const proc =
|
|
5535
|
+
await new Promise((resolve2, reject) => {
|
|
5536
|
+
const proc = spawn3("lark-cli", args, {
|
|
3143
5537
|
shell: false,
|
|
3144
5538
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3145
5539
|
});
|
|
@@ -3149,7 +5543,7 @@ class LarkCliInstance {
|
|
|
3149
5543
|
});
|
|
3150
5544
|
proc.on("close", (code) => {
|
|
3151
5545
|
if (code === 0)
|
|
3152
|
-
|
|
5546
|
+
resolve2();
|
|
3153
5547
|
else
|
|
3154
5548
|
reject(new Error(`lark-cli exited with code ${code}: ${stderr}`));
|
|
3155
5549
|
});
|
|
@@ -3223,7 +5617,7 @@ class LarkCliInstance {
|
|
|
3223
5617
|
};
|
|
3224
5618
|
}
|
|
3225
5619
|
async sendLarkMessage(options) {
|
|
3226
|
-
return new Promise((
|
|
5620
|
+
return new Promise((resolve2, reject) => {
|
|
3227
5621
|
const { chatId, userId, messageId, content, format = "text", profile } = options;
|
|
3228
5622
|
const normalizedContent = normalizeLarkAtMentions(content);
|
|
3229
5623
|
const contentFlag = format === "markdown" ? "--markdown" : "--text";
|
|
@@ -3241,7 +5635,7 @@ class LarkCliInstance {
|
|
|
3241
5635
|
if (profile) {
|
|
3242
5636
|
args.unshift("--profile", profile);
|
|
3243
5637
|
}
|
|
3244
|
-
const proc =
|
|
5638
|
+
const proc = spawn3("lark-cli", args, {
|
|
3245
5639
|
shell: false,
|
|
3246
5640
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3247
5641
|
});
|
|
@@ -3255,7 +5649,7 @@ class LarkCliInstance {
|
|
|
3255
5649
|
});
|
|
3256
5650
|
proc.on("close", (code) => {
|
|
3257
5651
|
if (code === 0) {
|
|
3258
|
-
|
|
5652
|
+
resolve2();
|
|
3259
5653
|
} else {
|
|
3260
5654
|
reject(new Error(`lark-cli exited with code ${code}: ${stderr}`));
|
|
3261
5655
|
}
|
|
@@ -3470,7 +5864,7 @@ ${headline}
|
|
|
3470
5864
|
- **chatId: ${chatId || "(缺失)"}** ← 关键!必须发到这里
|
|
3471
5865
|
- **messageId: ${messageId || "(缺失)"}** ← ${messageId ? "reply 这条,保持话题线索" : "无触发消息,用 +messages-send"}
|
|
3472
5866
|
- sender: ${senderId || "(未知)"}
|
|
3473
|
-
${input.notifyEvent.subSessionId ? `- 子 session: ${input.notifyEvent.subSessionId} ← 如需续跑:delegate_task(
|
|
5867
|
+
${input.notifyEvent.subSessionId ? `- 子 session: ${input.notifyEvent.subSessionId} ← 如需续跑:delegate_task(sub_session_id="${input.notifyEvent.subSessionId}", prompt=<新要求>)
|
|
3474
5868
|
` : ""}${profile ? `- profile: ${profile} ← 调用 lark-cli 必须带 --profile
|
|
3475
5869
|
` : ""}
|
|
3476
5870
|
### 系统回执(rule-based 直发)
|
|
@@ -3525,7 +5919,7 @@ async function patchCardByLarkCli(token, card, profile) {
|
|
|
3525
5919
|
const profileFlag = profile ? `--profile ${profile} ` : "";
|
|
3526
5920
|
const cmd = `lark-cli ${profileFlag}api POST /open-apis/interactive/v1/card/update ` + `--data '${JSON.stringify({ token, card }).replace(/'/g, "'\\''")}' --as bot`;
|
|
3527
5921
|
try {
|
|
3528
|
-
const { stdout } = await
|
|
5922
|
+
const { stdout } = await execAsync(cmd, { timeout: 30000 });
|
|
3529
5923
|
logger4.debug(`patchCardByLarkCli ok: ${stdout?.slice(0, 200)}`);
|
|
3530
5924
|
} catch (err) {
|
|
3531
5925
|
const msg = err.message;
|
|
@@ -3538,10 +5932,11 @@ var builtInHandlers = [
|
|
|
3538
5932
|
timerHandler,
|
|
3539
5933
|
bountyIMHandler,
|
|
3540
5934
|
bountyCliHandler,
|
|
3541
|
-
cliCommandHandler
|
|
5935
|
+
cliCommandHandler,
|
|
5936
|
+
wecomCliHandler
|
|
3542
5937
|
];
|
|
3543
5938
|
function getBuiltInHandler(type) {
|
|
3544
5939
|
return builtInHandlers.find((h) => h.type === type);
|
|
3545
5940
|
}
|
|
3546
5941
|
|
|
3547
|
-
export { TimerInstance, timerHandler, buildDefaultBountyIMSystemPrompt, resolveBountyIMSystemPrompt, BountyIMInstance, bountyIMHandler, bountyCliHandler, FALLBACK_MODEL_ACTION_NS, FALLBACK_MODEL_ACTIONS, isFallbackModelCard, renderTextCard, renderFallbackModelCard, DEFAULT_CONFIG_PATH, readFallbackModelList, readRawFallbackModelArray, writeFallbackModelList, fallbackModelCommand, listCommand, COMMAND_PREFIX, isCommandMessage, extractCommandInvocation, CommandRegistry, getGlobalCommandRegistry, handleCommandMessage, isCommandCardAction, translateCardActionToCommand, handleCommandCardAction, buildLarkRecommendedAction, extractMetadata, LarkCliInstance, renderRuleNoticeSection, extractCommandText, larkCliHandler, patchCardByLarkCli, builtInHandlers, getBuiltInHandler };
|
|
5942
|
+
export { TimerInstance, timerHandler, buildDefaultBountyIMSystemPrompt, resolveBountyIMSystemPrompt, BountyIMInstance, bountyIMHandler, bountyCliHandler, sanitizeWecomFrame, WECOM_CLI_ES_TYPE, WECOM_CHAT_ID_PREFIX, WECOM_WS_DEFAULT_URL, readWecomCliConfig, WECOM_CREDENTIALS_DEFAULT_PATH, readWecomCredentialsFile, resolveWecomCredentials, buildWecomReplyChannel, parseWecomFrame, WECOM_CHANNEL_PROMPT_SECTION_TITLE, buildWecomChannelSystemPrompt, buildWecomNotifyPrompt, resolveWecomBinary, probeCommandBinary, wecomCliHandler, FALLBACK_MODEL_ACTION_NS, FALLBACK_MODEL_ACTIONS, isFallbackModelCard, renderTextCard, renderFallbackModelCard, DEFAULT_CONFIG_PATH, readFallbackModelList, readRawFallbackModelArray, writeFallbackModelList, fallbackModelCommand, listCommand, resolveEsFilePath, esCommand, COMMAND_PREFIX, isCommandMessage, extractCommandInvocation, CommandRegistry, getGlobalCommandRegistry, handleCommandMessage, isCommandCardAction, translateCardActionToCommand, handleCommandCardAction, buildSlimRawData, buildLarkRecommendedAction, LARK_CHANNEL_PROMPT_SECTION_TITLE, LARK_PROMPT_LENGTH_BUDGET, buildLarkChannelSystemPrompt, resolveLarkChannelSystemPrompt, extractMetadata, LarkCliInstance, renderRuleNoticeSection, extractCommandText, larkCliHandler, patchCardByLarkCli, builtInHandlers, getBuiltInHandler };
|