@ai-setting/roy-agent-core 1.7.77 → 1.7.85
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 +16 -0
- package/dist/env/agent/agent-component.d.ts +42 -21
- package/dist/env/agent/agent-component.js +2 -2
- package/dist/env/agent/index.js +2 -2
- package/dist/env/agent/types.d.ts +32 -12
- 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 +68 -0
- package/dist/env/event-source/event-source-handlers.js +20 -8
- package/dist/env/event-source/index.d.ts +5 -1
- package/dist/env/event-source/index.js +57 -8
- package/dist/env/event-source/types.d.ts +101 -1
- package/dist/env/event-source/wecom-card-action-handler.d.ts +141 -0
- package/dist/env/event-source/wecom-card-builder.d.ts +220 -0
- package/dist/env/event-source/wecom-cli-handler.d.ts +266 -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-ws-bridge.d.ts +213 -0
- package/dist/env/index.js +16 -14
- package/dist/env/task/delegate/delegate-tool.d.ts +62 -2
- package/dist/env/task/delegate/delegate-tool.js +3 -1
- package/dist/env/task/delegate/index.js +1 -1
- package/dist/env/task/index.js +2 -2
- package/dist/env/task/plugins/index.d.ts +2 -0
- package/dist/env/task/plugins/index.js +23 -2
- package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +41 -2
- package/dist/env/task/plugins/lark-card-patch-notifier.js +1 -1
- package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +16 -0
- package/dist/env/task/plugins/wecom-card-state-store.d.ts +121 -0
- package/dist/env/task/plugins/wecom-task-notify-plugin.d.ts +224 -0
- package/dist/env/tool/built-in/index.d.ts +2 -0
- package/dist/env/tool/built-in/index.js +6 -1
- package/dist/env/tool/index.js +3 -2
- package/dist/env/workflow/index.js +3 -3
- package/dist/env/workflow/plugins/index.js +2 -2
- package/dist/env/workflow/tools/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +68 -17
- package/dist/shared/@ai-setting/{roy-agent-core-v87njmq7.js → roy-agent-core-0dh8npnz.js} +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-2stt4rp5.js +134 -0
- package/dist/shared/@ai-setting/{roy-agent-core-mww0mnd9.js → roy-agent-core-7bpj859q.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-w0qjeq2v.js → roy-agent-core-7dd0s3xb.js} +14 -4
- package/dist/shared/@ai-setting/{roy-agent-core-rsn44n9y.js → roy-agent-core-b31v3qpv.js} +851 -46
- package/dist/shared/@ai-setting/{roy-agent-core-0v27y99n.js → roy-agent-core-begm7ytz.js} +63 -2
- package/dist/shared/@ai-setting/{roy-agent-core-aa2j7bk9.js → roy-agent-core-cha0jc7n.js} +40 -26
- package/dist/shared/@ai-setting/{roy-agent-core-hd3n57zs.js → roy-agent-core-d1n4jbza.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-9vhq7fvv.js → roy-agent-core-dkrvmjsh.js} +12 -74
- 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-m3vjepzz.js +544 -0
- package/dist/shared/@ai-setting/{roy-agent-core-ew6qb5yx.js → roy-agent-core-p6ks90tb.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-rhtdspx6.js → roy-agent-core-qk9hd89p.js} +1 -1
- package/dist/shared/@ai-setting/{roy-agent-core-6cy43nss.js → roy-agent-core-rykf7c04.js} +49 -9
- package/dist/shared/@ai-setting/{roy-agent-core-8ez6mccb.js → roy-agent-core-tk98f92b.js} +4 -0
- 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-vyr6svt6.js +31 -0
- package/dist/shared/@ai-setting/{roy-agent-core-3gpygqcx.js → roy-agent-core-xf6z96qy.js} +2086 -163
- package/package.json +10 -1
- /package/dist/shared/@ai-setting/{roy-agent-core-b4x6x4k2.js → roy-agent-core-m1fgtnrc.js} +0 -0
- /package/dist/shared/@ai-setting/{roy-agent-core-v85n42ve.js → roy-agent-core-ze5cqpf2.js} +0 -0
|
@@ -1,29 +1,40 @@
|
|
|
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
|
+
guardConfigWrite
|
|
9
|
+
} from "./roy-agent-core-2stt4rp5.js";
|
|
7
10
|
import {
|
|
8
11
|
TaskTagPlugin,
|
|
9
12
|
createLarkCliTaskNotifyHook,
|
|
10
13
|
createPerTaskGroupChatPlugin,
|
|
14
|
+
createWecomCardStateStore,
|
|
15
|
+
createWecomTaskNotifyHook,
|
|
11
16
|
extractPerTaskChatId,
|
|
12
17
|
extractPerTaskChatName
|
|
13
|
-
} from "./roy-agent-core-
|
|
18
|
+
} from "./roy-agent-core-b31v3qpv.js";
|
|
19
|
+
import {
|
|
20
|
+
WecomCardActionKey,
|
|
21
|
+
buildWecomCardUpdateFrame,
|
|
22
|
+
registerWecomRespondTarget,
|
|
23
|
+
unregisterWecomRespondTarget
|
|
24
|
+
} from "./roy-agent-core-m3vjepzz.js";
|
|
14
25
|
import {
|
|
15
26
|
getSharedEventStore
|
|
16
27
|
} from "./roy-agent-core-na18zwj0.js";
|
|
17
28
|
import {
|
|
18
29
|
createLarkCliWorkflowNotifyHook
|
|
19
|
-
} from "./roy-agent-core-
|
|
30
|
+
} from "./roy-agent-core-d1n4jbza.js";
|
|
20
31
|
import {
|
|
21
32
|
LarkCardPatchNotifier,
|
|
22
33
|
buildTaskCard
|
|
23
|
-
} from "./roy-agent-core-
|
|
34
|
+
} from "./roy-agent-core-7dd0s3xb.js";
|
|
24
35
|
import {
|
|
25
36
|
notifyReactFailure
|
|
26
|
-
} from "./roy-agent-core-
|
|
37
|
+
} from "./roy-agent-core-rykf7c04.js";
|
|
27
38
|
import {
|
|
28
39
|
normalizeLarkAtMentions
|
|
29
40
|
} from "./roy-agent-core-m880qz3y.js";
|
|
@@ -55,11 +66,133 @@ import {
|
|
|
55
66
|
} from "./roy-agent-core-fs0mn2jk.js";
|
|
56
67
|
|
|
57
68
|
// src/env/event-source/event-source-handlers.ts
|
|
58
|
-
import { spawn as
|
|
69
|
+
import { spawn as spawn3, exec as execSync } from "child_process";
|
|
59
70
|
import { promisify as promisify2 } from "node:util";
|
|
60
71
|
init_global_hook_manager();
|
|
61
72
|
init_context();
|
|
62
73
|
|
|
74
|
+
// src/env/event-source/es-plugins.ts
|
|
75
|
+
init_global_hook_manager();
|
|
76
|
+
var BUILT_IN_PLUGIN_FACTORIES = {
|
|
77
|
+
LarkCliTaskNotifyPlugin: (cfg) => createLarkCliTaskNotifyHook(cfg),
|
|
78
|
+
WecomTaskNotifyPlugin: (cfg) => createWecomTaskNotifyHook(cfg),
|
|
79
|
+
LarkCliWorkflowNotifyPlugin: (cfg) => createLarkCliWorkflowNotifyHook(cfg),
|
|
80
|
+
PerTaskGroupChatPlugin: (cfg) => createPerTaskGroupChatPlugin(cfg),
|
|
81
|
+
"task-tag": () => new TaskTagPlugin
|
|
82
|
+
};
|
|
83
|
+
function getWecomPluginFactories() {
|
|
84
|
+
return BUILT_IN_PLUGIN_FACTORIES;
|
|
85
|
+
}
|
|
86
|
+
function getPluginHookPointsShared(plugin) {
|
|
87
|
+
if (Array.isArray(plugin.hooks)) {
|
|
88
|
+
return plugin.hooks.map((h) => h.point || h);
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(plugin.hookPoints)) {
|
|
91
|
+
return plugin.hookPoints.map((h) => h.point || h);
|
|
92
|
+
}
|
|
93
|
+
if (typeof plugin.execute === "function" && plugin.hookPoint) {
|
|
94
|
+
return [plugin.hookPoint];
|
|
95
|
+
}
|
|
96
|
+
if (plugin.name && typeof plugin.execute === "function") {
|
|
97
|
+
return [
|
|
98
|
+
"task:after.create",
|
|
99
|
+
"task:after.update",
|
|
100
|
+
"task:operation:after.create",
|
|
101
|
+
"task:after.complete",
|
|
102
|
+
"task:after.delete"
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
function registerPluginHooksShared(plugin, pluginName, sourceId) {
|
|
108
|
+
const hookPoints = getPluginHookPointsShared(plugin);
|
|
109
|
+
if (typeof plugin.init === "function") {
|
|
110
|
+
const pluginEnv = {
|
|
111
|
+
getComponent: () => null,
|
|
112
|
+
registerAgent: () => {},
|
|
113
|
+
registerTool: () => {},
|
|
114
|
+
registerHook: (hook) => {
|
|
115
|
+
globalHookManager.register(hook.point, {
|
|
116
|
+
name: `${pluginName}:${hook.point}`,
|
|
117
|
+
priority: hook.priority ?? 0,
|
|
118
|
+
pluginName,
|
|
119
|
+
sourceId,
|
|
120
|
+
execute: hook.handler
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
logger: {
|
|
124
|
+
debug: (...args) => console.debug(`[plugin:${pluginName}]`, ...args),
|
|
125
|
+
info: (...args) => console.info(`[plugin:${pluginName}]`, ...args),
|
|
126
|
+
warn: (...args) => console.warn(`[plugin:${pluginName}]`, ...args),
|
|
127
|
+
error: (...args) => console.error(`[plugin:${pluginName}]`, ...args)
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
plugin.init(pluginEnv);
|
|
131
|
+
return hookPoints;
|
|
132
|
+
}
|
|
133
|
+
if (typeof plugin.execute === "function") {
|
|
134
|
+
if (plugin.hookPoint) {
|
|
135
|
+
globalHookManager.register(plugin.hookPoint, {
|
|
136
|
+
name: `${pluginName}:${plugin.hookPoint}`,
|
|
137
|
+
priority: plugin.priority ?? 0,
|
|
138
|
+
pluginName,
|
|
139
|
+
sourceId,
|
|
140
|
+
execute: plugin.execute.bind(plugin)
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
for (const hookPoint of hookPoints) {
|
|
144
|
+
globalHookManager.register(hookPoint, {
|
|
145
|
+
name: `${pluginName}:${hookPoint}`,
|
|
146
|
+
priority: plugin.priority ?? 0,
|
|
147
|
+
pluginName,
|
|
148
|
+
sourceId,
|
|
149
|
+
execute: plugin.execute.bind(plugin)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return hookPoints;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(plugin.hooks)) {
|
|
156
|
+
for (const hook of plugin.hooks) {
|
|
157
|
+
const hookPoint = hook.point || hook;
|
|
158
|
+
const priority = hook.priority ?? 0;
|
|
159
|
+
globalHookManager.register(hookPoint, {
|
|
160
|
+
name: `${pluginName}:${hookPoint}`,
|
|
161
|
+
priority,
|
|
162
|
+
pluginName,
|
|
163
|
+
sourceId,
|
|
164
|
+
execute: async (ctx) => {
|
|
165
|
+
await plugin.execute(ctx);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return hookPoints;
|
|
170
|
+
}
|
|
171
|
+
return hookPoints;
|
|
172
|
+
}
|
|
173
|
+
function importPluginShared(name, factories, pluginConfig = {}) {
|
|
174
|
+
const factory = factories[name];
|
|
175
|
+
if (!factory) {
|
|
176
|
+
console.error(`[es-plugins] Unknown plugin: ${name}`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
return factory({
|
|
181
|
+
sourceId: "",
|
|
182
|
+
pluginName: name,
|
|
183
|
+
...pluginConfig
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(`[es-plugins] Failed to instantiate plugin ${name}:`, error);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function unregisterPluginHooksShared(pluginName, hookPoints) {
|
|
191
|
+
for (const hookPoint of hookPoints) {
|
|
192
|
+
globalHookManager.unregister(hookPoint, `${pluginName}:${hookPoint}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
63
196
|
// src/env/task/plugins/lark-card-action-handler.ts
|
|
64
197
|
init_logger();
|
|
65
198
|
import { exec } from "node:child_process";
|
|
@@ -788,13 +921,19 @@ class BountyIMInstance {
|
|
|
788
921
|
const { email } = readBountyIMConfig(this.config);
|
|
789
922
|
return email || "unknown";
|
|
790
923
|
}
|
|
924
|
+
cachedSystemPrompt;
|
|
925
|
+
systemPromptResolved = false;
|
|
791
926
|
getCurrentSystemPrompt() {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
927
|
+
if (!this.systemPromptResolved) {
|
|
928
|
+
const { email, imServerUrl } = readBountyIMConfig(this.config);
|
|
929
|
+
this.cachedSystemPrompt = resolveBountyIMSystemPrompt({
|
|
930
|
+
configSystemPrompt: this.config.systemPrompt,
|
|
931
|
+
currentEmail: email,
|
|
932
|
+
imServerUrl
|
|
933
|
+
});
|
|
934
|
+
this.systemPromptResolved = true;
|
|
935
|
+
}
|
|
936
|
+
return this.cachedSystemPrompt;
|
|
798
937
|
}
|
|
799
938
|
getStatus() {
|
|
800
939
|
return this.status;
|
|
@@ -1126,145 +1265,1585 @@ class BountyCliInstance {
|
|
|
1126
1265
|
setStatus(status) {
|
|
1127
1266
|
this.status = status;
|
|
1128
1267
|
}
|
|
1129
|
-
async start() {
|
|
1130
|
-
if (this.status === "running")
|
|
1268
|
+
async start() {
|
|
1269
|
+
if (this.status === "running")
|
|
1270
|
+
return;
|
|
1271
|
+
this.setStatus("starting");
|
|
1272
|
+
const { channel, profile, customCommand } = readBountyCliConfig(this.config);
|
|
1273
|
+
if (!channel && !customCommand) {
|
|
1274
|
+
throw new Error("bounty-cli event source requires 'channel' (or 'command') in config");
|
|
1275
|
+
}
|
|
1276
|
+
const command = customCommand || buildDefaultCommand(profile, channel);
|
|
1277
|
+
this.child = spawn(`exec ${command}`, [], {
|
|
1278
|
+
shell: true,
|
|
1279
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1280
|
+
windowsHide: true
|
|
1281
|
+
});
|
|
1282
|
+
let stdoutBuffer = "";
|
|
1283
|
+
let stderrBuffer = "";
|
|
1284
|
+
this.child.stdout?.on("data", (data) => {
|
|
1285
|
+
const str = data.toString();
|
|
1286
|
+
stdoutBuffer += str;
|
|
1287
|
+
this.processStdout(stdoutBuffer, channel ?? "");
|
|
1288
|
+
stdoutBuffer = "";
|
|
1289
|
+
});
|
|
1290
|
+
this.child.stderr?.on("data", (data) => {
|
|
1291
|
+
const output = data.toString();
|
|
1292
|
+
stderrBuffer += output;
|
|
1293
|
+
this.processStderr(stderrBuffer);
|
|
1294
|
+
stderrBuffer = "";
|
|
1295
|
+
});
|
|
1296
|
+
this.child.on("exit", (code, signal) => {
|
|
1297
|
+
if (code !== 0 && signal !== "SIGINT" && signal !== "SIGTERM") {
|
|
1298
|
+
this.setStatus("error");
|
|
1299
|
+
} else {
|
|
1300
|
+
this.setStatus("stopped");
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
this.child.on("error", (err) => {
|
|
1304
|
+
console.error(`[bounty-cli] spawn error: ${err.message}`);
|
|
1305
|
+
this.setStatus("error");
|
|
1306
|
+
});
|
|
1307
|
+
this.setStatus("running");
|
|
1308
|
+
}
|
|
1309
|
+
processStdout(buffer, fallbackChannel) {
|
|
1310
|
+
const lines = buffer.split(`
|
|
1311
|
+
`);
|
|
1312
|
+
const tail = lines.pop() ?? "";
|
|
1313
|
+
this.buffer = tail;
|
|
1314
|
+
for (const line of lines) {
|
|
1315
|
+
const trimmed = line.trim();
|
|
1316
|
+
if (!trimmed)
|
|
1317
|
+
continue;
|
|
1318
|
+
this.handleEventLine(trimmed, fallbackChannel);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
processStderr(buffer) {
|
|
1322
|
+
const lines = buffer.split(`
|
|
1323
|
+
`);
|
|
1324
|
+
const tail = lines.pop() ?? "";
|
|
1325
|
+
for (const line of lines) {
|
|
1326
|
+
const trimmed = line.trim();
|
|
1327
|
+
if (!trimmed)
|
|
1328
|
+
continue;
|
|
1329
|
+
if (trimmed.startsWith("{") && trimmed.includes("event_id")) {
|
|
1330
|
+
this.handleEventLine(trimmed, readBountyCliConfig(this.config).channel ?? "");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (tail.trim().length > 0) {
|
|
1334
|
+
this.buffer = tail;
|
|
1335
|
+
} else {
|
|
1336
|
+
this.buffer = "";
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
handleEventLine(line, fallbackChannel) {
|
|
1340
|
+
let raw;
|
|
1341
|
+
try {
|
|
1342
|
+
raw = JSON.parse(line);
|
|
1343
|
+
} catch {
|
|
1344
|
+
console.error(`[bounty-cli] failed to parse event line: ${line.slice(0, 200)}`);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const { metadata, replyChannel } = translateEvent(raw, fallbackChannel);
|
|
1348
|
+
const event = {
|
|
1349
|
+
metadata,
|
|
1350
|
+
rawEvent: raw,
|
|
1351
|
+
replyChannel,
|
|
1352
|
+
recommendedAction: buildBountyCliRecommendedAction({
|
|
1353
|
+
profile: readBountyCliConfig(this.config).profile,
|
|
1354
|
+
channelName: String(metadata.channelName ?? fallbackChannel)
|
|
1355
|
+
})
|
|
1356
|
+
};
|
|
1357
|
+
if (this.eventHandler) {
|
|
1358
|
+
try {
|
|
1359
|
+
this.eventHandler(event);
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
console.error(`[bounty-cli] event handler threw: ${err}`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async stop() {
|
|
1366
|
+
if (this.status === "stopped" || this.status === "created")
|
|
1367
|
+
return;
|
|
1368
|
+
if (this.child && !this.child.killed) {
|
|
1369
|
+
this.child.kill("SIGINT");
|
|
1370
|
+
await new Promise((resolve) => {
|
|
1371
|
+
const timer = setTimeout(() => {
|
|
1372
|
+
if (this.child && !this.child.killed) {
|
|
1373
|
+
this.child.kill("SIGKILL");
|
|
1374
|
+
}
|
|
1375
|
+
resolve();
|
|
1376
|
+
}, 3000);
|
|
1377
|
+
if (this.child) {
|
|
1378
|
+
this.child.on("exit", () => {
|
|
1379
|
+
clearTimeout(timer);
|
|
1380
|
+
resolve();
|
|
1381
|
+
});
|
|
1382
|
+
} else {
|
|
1383
|
+
clearTimeout(timer);
|
|
1384
|
+
resolve();
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
this.setStatus("stopped");
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
var bountyCliHandler = {
|
|
1392
|
+
type: "bounty-cli",
|
|
1393
|
+
validateConfig(config) {
|
|
1394
|
+
const errors = [];
|
|
1395
|
+
if (!config.id)
|
|
1396
|
+
errors.push("EventSource ID is required");
|
|
1397
|
+
if (!config.name)
|
|
1398
|
+
errors.push("EventSource name is required");
|
|
1399
|
+
const { channel, customCommand } = readBountyCliConfig(config);
|
|
1400
|
+
if (!channel && !customCommand) {
|
|
1401
|
+
errors.push("Bounty CLI 'channel' (top-level channel or options.channel) is required");
|
|
1402
|
+
}
|
|
1403
|
+
return errors;
|
|
1404
|
+
},
|
|
1405
|
+
createInstance(config) {
|
|
1406
|
+
return new BountyCliInstance(config);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
// src/env/event-source/wecom-cli-handler.ts
|
|
1411
|
+
import { spawn as spawn2 } from "child_process";
|
|
1412
|
+
import { existsSync, readFileSync } from "fs";
|
|
1413
|
+
import { delimiter, isAbsolute, join as join2 } from "path";
|
|
1414
|
+
|
|
1415
|
+
// src/env/event-source/wecom-ws-bridge.ts
|
|
1416
|
+
var WECOM_WS_URL = "wss://openws.work.weixin.qq.com";
|
|
1417
|
+
var WECOM_HEARTBEAT_INTERVAL_MS = 30000;
|
|
1418
|
+
var WECOM_RECONNECT_BASE_DELAY_MS = 1000;
|
|
1419
|
+
var WECOM_RECONNECT_MAX_DELAY_MS = 60000;
|
|
1420
|
+
var WECOM_MAX_RECONNECT_ATTEMPTS = 10;
|
|
1421
|
+
var WECOM_MAX_AUTH_FAILURE_ATTEMPTS = 5;
|
|
1422
|
+
function randomString() {
|
|
1423
|
+
return Math.random().toString(36).slice(2, 10);
|
|
1424
|
+
}
|
|
1425
|
+
function generateWecomReqId(prefix) {
|
|
1426
|
+
return `${prefix}_${Date.now()}_${randomString()}`;
|
|
1427
|
+
}
|
|
1428
|
+
function buildWecomAuthFrame(botId, secret) {
|
|
1429
|
+
return {
|
|
1430
|
+
cmd: "aibot_subscribe",
|
|
1431
|
+
headers: { req_id: generateWecomReqId("aibot_subscribe") },
|
|
1432
|
+
body: { bot_id: botId, secret }
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function buildWecomHeartbeatFrame() {
|
|
1436
|
+
return {
|
|
1437
|
+
cmd: "ping",
|
|
1438
|
+
headers: { req_id: generateWecomReqId("ping") }
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
function buildWecomRespondFrame(reqId, body) {
|
|
1442
|
+
const id = reqId.trim();
|
|
1443
|
+
if (!id) {
|
|
1444
|
+
throw new Error("buildWecomRespondFrame: req_id is required (reply must reference the original message frame)");
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
cmd: "aibot_respond_msg",
|
|
1448
|
+
headers: { req_id: id },
|
|
1449
|
+
body
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
function isWecomAuthResponse(frame) {
|
|
1453
|
+
if (frame.cmd)
|
|
1454
|
+
return false;
|
|
1455
|
+
const reqId = frame.headers?.req_id ?? "";
|
|
1456
|
+
return reqId.startsWith("aibot_subscribe_");
|
|
1457
|
+
}
|
|
1458
|
+
function isWecomHeartbeatResponse(frame) {
|
|
1459
|
+
if (frame.cmd)
|
|
1460
|
+
return false;
|
|
1461
|
+
const reqId = frame.headers?.req_id ?? "";
|
|
1462
|
+
return reqId.startsWith("ping_");
|
|
1463
|
+
}
|
|
1464
|
+
function isWecomDisconnectedEvent(frame) {
|
|
1465
|
+
if (!frame || typeof frame !== "object")
|
|
1466
|
+
return false;
|
|
1467
|
+
const f = frame;
|
|
1468
|
+
if (f.cmd !== "aibot_event_callback")
|
|
1469
|
+
return false;
|
|
1470
|
+
return f.body?.event?.eventtype === "disconnected_event";
|
|
1471
|
+
}
|
|
1472
|
+
function sanitizeWecomFrame(frame) {
|
|
1473
|
+
const clean = { ...frame };
|
|
1474
|
+
if ("secret" in clean)
|
|
1475
|
+
delete clean.secret;
|
|
1476
|
+
if (clean.body && typeof clean.body === "object") {
|
|
1477
|
+
clean.body = { ...clean.body };
|
|
1478
|
+
if ("secret" in clean.body)
|
|
1479
|
+
delete clean.body.secret;
|
|
1480
|
+
}
|
|
1481
|
+
return clean;
|
|
1482
|
+
}
|
|
1483
|
+
function backoffDelay(attempt, base, max) {
|
|
1484
|
+
return Math.min(base * Math.pow(2, attempt - 1), max);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
class WecomWsBridge {
|
|
1488
|
+
opts;
|
|
1489
|
+
botId;
|
|
1490
|
+
secret;
|
|
1491
|
+
wsFactory;
|
|
1492
|
+
wsUrl;
|
|
1493
|
+
ws;
|
|
1494
|
+
heartbeatTimer;
|
|
1495
|
+
reconnectTimer;
|
|
1496
|
+
reconnectAttempts = 0;
|
|
1497
|
+
authFailureAttempts = 0;
|
|
1498
|
+
manualClose = false;
|
|
1499
|
+
lastCloseWasAuthFailure = false;
|
|
1500
|
+
stopped = false;
|
|
1501
|
+
onMessageCb;
|
|
1502
|
+
onAuthenticatedCb;
|
|
1503
|
+
onReconnectingCb;
|
|
1504
|
+
onErrorCb;
|
|
1505
|
+
constructor(opts) {
|
|
1506
|
+
this.opts = {
|
|
1507
|
+
heartbeatIntervalMs: opts.heartbeatIntervalMs ?? WECOM_HEARTBEAT_INTERVAL_MS,
|
|
1508
|
+
reconnectBaseDelayMs: opts.reconnectBaseDelayMs ?? WECOM_RECONNECT_BASE_DELAY_MS,
|
|
1509
|
+
reconnectMaxDelayMs: opts.reconnectMaxDelayMs ?? WECOM_RECONNECT_MAX_DELAY_MS,
|
|
1510
|
+
maxReconnectAttempts: opts.maxReconnectAttempts ?? WECOM_MAX_RECONNECT_ATTEMPTS,
|
|
1511
|
+
maxAuthFailureAttempts: opts.maxAuthFailureAttempts ?? WECOM_MAX_AUTH_FAILURE_ATTEMPTS
|
|
1512
|
+
};
|
|
1513
|
+
this.botId = opts.botId;
|
|
1514
|
+
this.secret = opts.secret;
|
|
1515
|
+
this.wsUrl = opts.wsUrl ?? WECOM_WS_URL;
|
|
1516
|
+
this.wsFactory = opts.wsFactory ?? ((url) => {
|
|
1517
|
+
throw new Error("wsFactory is required in production (inject WebSocket from 'ws')");
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
onMessage(cb) {
|
|
1521
|
+
this.onMessageCb = cb;
|
|
1522
|
+
}
|
|
1523
|
+
onAuthenticated(cb) {
|
|
1524
|
+
this.onAuthenticatedCb = cb;
|
|
1525
|
+
}
|
|
1526
|
+
onReconnecting(cb) {
|
|
1527
|
+
this.onReconnectingCb = cb;
|
|
1528
|
+
}
|
|
1529
|
+
onError(cb) {
|
|
1530
|
+
this.onErrorCb = cb;
|
|
1531
|
+
}
|
|
1532
|
+
connect() {
|
|
1533
|
+
if (this.stopped)
|
|
1534
|
+
return;
|
|
1535
|
+
const ws = this.wsFactory(this.wsUrl);
|
|
1536
|
+
this.ws = ws;
|
|
1537
|
+
ws.on("open", () => {
|
|
1538
|
+
this.lastCloseWasAuthFailure = false;
|
|
1539
|
+
this.sendFrame(buildWecomAuthFrame(this.optsBotId(), this.optsSecret()));
|
|
1540
|
+
});
|
|
1541
|
+
ws.on("message", (data) => {
|
|
1542
|
+
try {
|
|
1543
|
+
const raw = String(data).replace(/[\x00-\x08\x0B-\x0D\x0E-\x1F]/g, "");
|
|
1544
|
+
const frame = JSON.parse(raw);
|
|
1545
|
+
this.handleFrame(frame);
|
|
1546
|
+
} catch {}
|
|
1547
|
+
});
|
|
1548
|
+
ws.on("close", () => {
|
|
1549
|
+
this.stopHeartbeat();
|
|
1550
|
+
if (this.manualClose || this.stopped)
|
|
1551
|
+
return;
|
|
1552
|
+
this.scheduleReconnect();
|
|
1553
|
+
});
|
|
1554
|
+
ws.on("error", (err) => {
|
|
1555
|
+
this.onErrorCb?.(err);
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
optsBotId() {
|
|
1559
|
+
return this.botId;
|
|
1560
|
+
}
|
|
1561
|
+
optsSecret() {
|
|
1562
|
+
return this.secret;
|
|
1563
|
+
}
|
|
1564
|
+
handleFrame(frame) {
|
|
1565
|
+
if (isWecomAuthResponse(frame)) {
|
|
1566
|
+
if ((frame.errcode ?? 0) !== 0) {
|
|
1567
|
+
this.lastCloseWasAuthFailure = true;
|
|
1568
|
+
this.onErrorCb?.(new Error(`wecom auth failed: ${frame.errmsg ?? "?"} (code=${frame.errcode})`));
|
|
1569
|
+
this.ws?.terminate();
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
this.reconnectAttempts = 0;
|
|
1573
|
+
this.authFailureAttempts = 0;
|
|
1574
|
+
this.startHeartbeat();
|
|
1575
|
+
this.onAuthenticatedCb?.();
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (isWecomHeartbeatResponse(frame))
|
|
1579
|
+
return;
|
|
1580
|
+
if (isWecomDisconnectedEvent(frame)) {
|
|
1581
|
+
this.lastCloseWasAuthFailure = false;
|
|
1582
|
+
this.onErrorCb?.(new Error("wecom disconnected_event: server kicked this connection " + "(bot allows only 1 long connection) — reconnecting"));
|
|
1583
|
+
this.stopHeartbeat();
|
|
1584
|
+
const stale = this.ws;
|
|
1585
|
+
this.ws = undefined;
|
|
1586
|
+
try {
|
|
1587
|
+
stale?.close?.();
|
|
1588
|
+
} catch {}
|
|
1589
|
+
this.scheduleReconnect();
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
this.onMessageCb?.(sanitizeWecomFrame(frame));
|
|
1593
|
+
}
|
|
1594
|
+
sendFrame(frame) {
|
|
1595
|
+
try {
|
|
1596
|
+
this.ws?.send(JSON.stringify(frame));
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
this.onErrorCb?.(err);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
respond(reqId, body) {
|
|
1602
|
+
if (this.stopped || this.manualClose) {
|
|
1603
|
+
return new Error("wecom bridge stopped, cannot respond");
|
|
1604
|
+
}
|
|
1605
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
1606
|
+
return new Error("wecom bridge not connected, cannot respond");
|
|
1607
|
+
}
|
|
1608
|
+
try {
|
|
1609
|
+
let frame;
|
|
1610
|
+
try {
|
|
1611
|
+
frame = buildWecomRespondFrame(reqId, body);
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
return err;
|
|
1614
|
+
}
|
|
1615
|
+
this.ws.send(JSON.stringify(frame));
|
|
1616
|
+
return;
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
sendCmd(cmd, body) {
|
|
1622
|
+
if (this.stopped || this.manualClose) {
|
|
1623
|
+
return new Error("wecom bridge stopped, cannot send");
|
|
1624
|
+
}
|
|
1625
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
1626
|
+
return new Error("wecom bridge not connected, cannot send");
|
|
1627
|
+
}
|
|
1628
|
+
try {
|
|
1629
|
+
this.ws.send(JSON.stringify({
|
|
1630
|
+
cmd,
|
|
1631
|
+
headers: { req_id: generateWecomReqId(cmd) },
|
|
1632
|
+
body
|
|
1633
|
+
}));
|
|
1634
|
+
return;
|
|
1635
|
+
} catch (err) {
|
|
1636
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
sendFrameWithReqId(frame) {
|
|
1640
|
+
if (this.stopped || this.manualClose) {
|
|
1641
|
+
return new Error("wecom bridge stopped, cannot send");
|
|
1642
|
+
}
|
|
1643
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
1644
|
+
return new Error("wecom bridge not connected, cannot send");
|
|
1645
|
+
}
|
|
1646
|
+
if (!frame.headers?.req_id?.trim()) {
|
|
1647
|
+
return new Error("sendFrameWithReqId: req_id is required (must reference the original frame)");
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
this.ws.send(JSON.stringify(frame));
|
|
1651
|
+
return;
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
startHeartbeat() {
|
|
1657
|
+
this.stopHeartbeat();
|
|
1658
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1659
|
+
this.sendFrame(buildWecomHeartbeatFrame());
|
|
1660
|
+
}, this.opts.heartbeatIntervalMs);
|
|
1661
|
+
}
|
|
1662
|
+
stopHeartbeat() {
|
|
1663
|
+
if (this.heartbeatTimer) {
|
|
1664
|
+
clearInterval(this.heartbeatTimer);
|
|
1665
|
+
this.heartbeatTimer = undefined;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
scheduleReconnect() {
|
|
1669
|
+
if (this.manualClose || this.stopped)
|
|
1670
|
+
return;
|
|
1671
|
+
if (this.reconnectTimer)
|
|
1672
|
+
return;
|
|
1673
|
+
const isAuthFailure = this.lastCloseWasAuthFailure;
|
|
1674
|
+
if (isAuthFailure) {
|
|
1675
|
+
if (this.authFailureAttempts >= this.opts.maxAuthFailureAttempts) {
|
|
1676
|
+
this.onErrorCb?.(new Error(`wecom auth failed ${this.authFailureAttempts} times, giving up`));
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
this.authFailureAttempts++;
|
|
1680
|
+
} else {
|
|
1681
|
+
if (this.reconnectAttempts >= this.opts.maxReconnectAttempts) {
|
|
1682
|
+
this.onErrorCb?.(new Error(`wecom reconnect exhausted (${this.reconnectAttempts} attempts)`));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
this.reconnectAttempts++;
|
|
1686
|
+
}
|
|
1687
|
+
const attempt = isAuthFailure ? this.authFailureAttempts : this.reconnectAttempts;
|
|
1688
|
+
const delay = backoffDelay(attempt, this.opts.reconnectBaseDelayMs, this.opts.reconnectMaxDelayMs);
|
|
1689
|
+
this.onReconnectingCb?.(attempt);
|
|
1690
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1691
|
+
this.reconnectTimer = undefined;
|
|
1692
|
+
if (this.manualClose || this.stopped)
|
|
1693
|
+
return;
|
|
1694
|
+
this.connect();
|
|
1695
|
+
}, delay);
|
|
1696
|
+
}
|
|
1697
|
+
stop() {
|
|
1698
|
+
this.stopped = true;
|
|
1699
|
+
this.manualClose = true;
|
|
1700
|
+
this.stopHeartbeat();
|
|
1701
|
+
if (this.reconnectTimer) {
|
|
1702
|
+
clearTimeout(this.reconnectTimer);
|
|
1703
|
+
this.reconnectTimer = undefined;
|
|
1704
|
+
}
|
|
1705
|
+
try {
|
|
1706
|
+
this.ws?.close();
|
|
1707
|
+
} catch {}
|
|
1708
|
+
this.ws = undefined;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/env/event-source/wecom-card-action-handler.ts
|
|
1713
|
+
init_logger();
|
|
1714
|
+
var logger2 = createLogger("wecom:card-action");
|
|
1715
|
+
var cardActionInflight2 = new Map;
|
|
1716
|
+
var ROY_TASK_PREFIX = "roy-task-";
|
|
1717
|
+
function parseRoyTaskId(cardTaskId) {
|
|
1718
|
+
if (!cardTaskId.startsWith(ROY_TASK_PREFIX))
|
|
1719
|
+
return null;
|
|
1720
|
+
const rest = cardTaskId.slice(ROY_TASK_PREFIX.length);
|
|
1721
|
+
const m = rest.match(/^(\d+)/);
|
|
1722
|
+
if (!m)
|
|
1723
|
+
return null;
|
|
1724
|
+
const n = Number(m[1]);
|
|
1725
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1726
|
+
}
|
|
1727
|
+
function extractWecomCardAction(frame) {
|
|
1728
|
+
if (!frame || typeof frame !== "object")
|
|
1729
|
+
return null;
|
|
1730
|
+
const f = frame;
|
|
1731
|
+
if (f.cmd !== "aibot_event_callback")
|
|
1732
|
+
return null;
|
|
1733
|
+
const body = f.body;
|
|
1734
|
+
if (!body || typeof body !== "object")
|
|
1735
|
+
return null;
|
|
1736
|
+
const event = body.event;
|
|
1737
|
+
if (event?.eventtype !== "template_card_event")
|
|
1738
|
+
return null;
|
|
1739
|
+
const tce = event.template_card_event;
|
|
1740
|
+
if (!tce)
|
|
1741
|
+
return null;
|
|
1742
|
+
const cardTaskId = String(tce.task_id ?? "");
|
|
1743
|
+
const from = body.from ?? {};
|
|
1744
|
+
return {
|
|
1745
|
+
eventKey: String(tce.event_key ?? ""),
|
|
1746
|
+
cardTaskId,
|
|
1747
|
+
taskId: parseRoyTaskId(cardTaskId),
|
|
1748
|
+
userId: from.userid ?? "",
|
|
1749
|
+
reqId: f.headers?.req_id ?? ""
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
var ACTION_CARD_LABEL = {
|
|
1753
|
+
view: {
|
|
1754
|
+
statusLabel: "\uD83D\uDC40 已查看",
|
|
1755
|
+
note: "用户已查看任务"
|
|
1756
|
+
},
|
|
1757
|
+
mark_done: {
|
|
1758
|
+
statusLabel: "\uD83C\uDF89 已完成",
|
|
1759
|
+
note: "用户已确认任务完成"
|
|
1760
|
+
},
|
|
1761
|
+
reopen: {
|
|
1762
|
+
statusLabel: "\uD83D\uDD04 进行中",
|
|
1763
|
+
note: "用户已重新打开任务"
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
async function handleWecomCardAction(frame, ports) {
|
|
1767
|
+
const action = extractWecomCardAction(frame);
|
|
1768
|
+
if (!action) {
|
|
1769
|
+
return { ok: false, error: "not a template_card_event frame" };
|
|
1770
|
+
}
|
|
1771
|
+
const inflightKey = `${action.cardTaskId}:${action.eventKey}`;
|
|
1772
|
+
const inflight = cardActionInflight2.get(inflightKey);
|
|
1773
|
+
if (inflight) {
|
|
1774
|
+
logger2.warn(`handleWecomCardAction: ${inflightKey} 已有同操作在处理中` + `(疑似用户重复点击),本次忽略。`);
|
|
1775
|
+
return { ok: false, error: "同操作处理中,已忽略重复点击" };
|
|
1776
|
+
}
|
|
1777
|
+
const job = doHandle(action, ports).finally(() => {
|
|
1778
|
+
cardActionInflight2.delete(inflightKey);
|
|
1779
|
+
});
|
|
1780
|
+
cardActionInflight2.set(inflightKey, job);
|
|
1781
|
+
return job;
|
|
1782
|
+
}
|
|
1783
|
+
async function doHandle(action, ports) {
|
|
1784
|
+
let actionType;
|
|
1785
|
+
if (action.eventKey === WecomCardActionKey.MARK_DONE) {
|
|
1786
|
+
actionType = "mark_done";
|
|
1787
|
+
} else if (action.eventKey === WecomCardActionKey.REOPEN) {
|
|
1788
|
+
actionType = "reopen";
|
|
1789
|
+
} else if (action.eventKey === WecomCardActionKey.VIEW) {
|
|
1790
|
+
actionType = "view";
|
|
1791
|
+
} else {
|
|
1792
|
+
return { ok: false, error: `unknown event_key: ${action.eventKey}` };
|
|
1793
|
+
}
|
|
1794
|
+
if (action.taskId === null && actionType !== "view") {
|
|
1795
|
+
return {
|
|
1796
|
+
ok: false,
|
|
1797
|
+
error: `cannot parse roy task id from card task_id: ${action.cardTaskId}`
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
let opError;
|
|
1801
|
+
try {
|
|
1802
|
+
if (actionType === "mark_done" && action.taskId !== null) {
|
|
1803
|
+
await ports.markTaskDone(action.taskId);
|
|
1804
|
+
} else if (actionType === "reopen" && action.taskId !== null) {
|
|
1805
|
+
await ports.reopenTask(action.taskId);
|
|
1806
|
+
}
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
opError = err instanceof Error ? err.message : String(err);
|
|
1809
|
+
logger2.error(`handleWecomCardAction: task op failed for #${action.taskId}: ${opError}`);
|
|
1810
|
+
}
|
|
1811
|
+
const label = ACTION_CARD_LABEL[actionType];
|
|
1812
|
+
const note = actionType === "view" ? label.note : opError ? `${label.note}(操作失败:${opError})` : label.note;
|
|
1813
|
+
let timeline;
|
|
1814
|
+
if ((actionType === "view" || actionType === "reopen") && action.taskId !== null) {
|
|
1815
|
+
try {
|
|
1816
|
+
timeline = ports.loadTimeline?.(action.taskId) ?? [];
|
|
1817
|
+
if (timeline.length === 0) {
|
|
1818
|
+
logger2.warn(`handleWecomCardAction: task #${action.taskId} 无时间线记录,` + `降级为简单按钮态更新`);
|
|
1819
|
+
}
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1822
|
+
logger2.warn(`handleWecomCardAction: loadTimeline failed for ` + `task #${action.taskId}: ${msg}(降级为简单按钮态更新)`);
|
|
1823
|
+
timeline = [];
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
let taskExtra;
|
|
1827
|
+
try {
|
|
1828
|
+
const t = action.taskId !== null ? await ports.loadTaskTitle?.(action.taskId) : undefined;
|
|
1829
|
+
if (t && (t.title || t.number !== undefined)) {
|
|
1830
|
+
taskExtra = { taskNumber: t.number, taskTitle: t.title };
|
|
1831
|
+
}
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1834
|
+
logger2.warn(`handleWecomCardAction: loadTaskTitle failed for ` + `task #${action.taskId}: ${msg}(降级为无标题更新)`);
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
await ports.updateCard(action.reqId, action.cardTaskId, label.statusLabel, note, timeline, actionType, taskExtra);
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1840
|
+
logger2.error(`handleWecomCardAction: updateCard failed: ${msg}`);
|
|
1841
|
+
return {
|
|
1842
|
+
ok: false,
|
|
1843
|
+
action: actionType,
|
|
1844
|
+
error: `card update failed: ${msg}`
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
if (opError) {
|
|
1848
|
+
return { ok: false, action: actionType, error: opError };
|
|
1849
|
+
}
|
|
1850
|
+
logger2.info(`handleWecomCardAction: task #${action.taskId} → ${actionType} ` + `(by ${action.userId})`);
|
|
1851
|
+
return { ok: true, action: actionType };
|
|
1852
|
+
}
|
|
1853
|
+
function buildWecomCardUpdateBody(input) {
|
|
1854
|
+
const refreshable = input.action === "view" || input.action === "reopen";
|
|
1855
|
+
const frame = buildWecomCardUpdateFrame({
|
|
1856
|
+
reqId: "placeholder",
|
|
1857
|
+
taskId: input.cardTaskId,
|
|
1858
|
+
statusLabel: input.statusLabel,
|
|
1859
|
+
note: input.note,
|
|
1860
|
+
timeline: input.timeline,
|
|
1861
|
+
taskTitle: input.taskTitle,
|
|
1862
|
+
taskNumber: input.taskNumber,
|
|
1863
|
+
buttonOverrides: refreshable ? [{ text: "刷新详情", key: WecomCardActionKey.VIEW, style: 1 }] : [{ text: "已处理", key: "task_done_ack", style: 4 }]
|
|
1864
|
+
});
|
|
1865
|
+
return { cmd: frame.cmd, body: frame.body };
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/env/event-source/wecom-media.ts
|
|
1869
|
+
import { createDecipheriv } from "crypto";
|
|
1870
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
1871
|
+
import { basename, join, resolve } from "path";
|
|
1872
|
+
var WECOM_MEDIA_DIR = "/tmp/wecom-media";
|
|
1873
|
+
var WECOM_MEDIA_BLOCK_SIZE = 32;
|
|
1874
|
+
var mediaDirOverride;
|
|
1875
|
+
function getWecomMediaDir() {
|
|
1876
|
+
return mediaDirOverride ?? WECOM_MEDIA_DIR;
|
|
1877
|
+
}
|
|
1878
|
+
function decryptWecomMedia(encrypted, aesKeyBase64) {
|
|
1879
|
+
if (!encrypted || encrypted.length === 0) {
|
|
1880
|
+
throw new Error("decryptWecomMedia: encrypted buffer is empty");
|
|
1881
|
+
}
|
|
1882
|
+
if (!aesKeyBase64 || typeof aesKeyBase64 !== "string") {
|
|
1883
|
+
throw new Error("decryptWecomMedia: aesKey must be a non-empty string");
|
|
1884
|
+
}
|
|
1885
|
+
const key = Buffer.from(aesKeyBase64, "base64");
|
|
1886
|
+
if (key.length !== 32) {
|
|
1887
|
+
throw new Error(`decryptWecomMedia: aesKey must decode to 32 bytes (got ${key.length})`);
|
|
1888
|
+
}
|
|
1889
|
+
const iv = key.subarray(0, 16);
|
|
1890
|
+
const decipher = createDecipheriv("aes-256-cbc", key, iv);
|
|
1891
|
+
decipher.setAutoPadding(false);
|
|
1892
|
+
const decrypted = Buffer.concat([
|
|
1893
|
+
decipher.update(encrypted),
|
|
1894
|
+
decipher.final()
|
|
1895
|
+
]);
|
|
1896
|
+
const padLen = decrypted[decrypted.length - 1];
|
|
1897
|
+
if (padLen < 1 || padLen > WECOM_MEDIA_BLOCK_SIZE || padLen > decrypted.length) {
|
|
1898
|
+
throw new Error(`decryptWecomMedia: invalid PKCS#7 padding value: ${padLen}`);
|
|
1899
|
+
}
|
|
1900
|
+
for (let i = decrypted.length - padLen;i < decrypted.length; i++) {
|
|
1901
|
+
if (decrypted[i] !== padLen) {
|
|
1902
|
+
throw new Error("decryptWecomMedia: invalid PKCS#7 padding (bytes mismatch)");
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return decrypted.subarray(0, decrypted.length - padLen);
|
|
1906
|
+
}
|
|
1907
|
+
function refFromField(kind, field) {
|
|
1908
|
+
if (!field || typeof field !== "object")
|
|
1909
|
+
return null;
|
|
1910
|
+
const url = typeof field.url === "string" ? field.url : undefined;
|
|
1911
|
+
const aeskey = typeof field.aeskey === "string" ? field.aeskey : undefined;
|
|
1912
|
+
if (!url || !aeskey)
|
|
1913
|
+
return null;
|
|
1914
|
+
return {
|
|
1915
|
+
kind,
|
|
1916
|
+
url,
|
|
1917
|
+
aeskey,
|
|
1918
|
+
filename: typeof field.filename === "string" ? field.filename : undefined
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
function extractWecomMediaRefs(body) {
|
|
1922
|
+
const refs = [];
|
|
1923
|
+
const msgType = String(body.msgtype ?? "");
|
|
1924
|
+
const single = refFromField(msgType, body[msgType]);
|
|
1925
|
+
if (single)
|
|
1926
|
+
refs.push(single);
|
|
1927
|
+
if (msgType === "mixed") {
|
|
1928
|
+
const mixed = body.mixed;
|
|
1929
|
+
for (const item of mixed?.msg_item ?? []) {
|
|
1930
|
+
if (!item || typeof item !== "object")
|
|
1931
|
+
continue;
|
|
1932
|
+
const it = item;
|
|
1933
|
+
const itemType = String(it.msgtype ?? "");
|
|
1934
|
+
const ref = refFromField(itemType, it[itemType]);
|
|
1935
|
+
if (ref)
|
|
1936
|
+
refs.push(ref);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return refs;
|
|
1940
|
+
}
|
|
1941
|
+
async function defaultWecomMediaFetcher(url) {
|
|
1942
|
+
const resp = await fetch(url);
|
|
1943
|
+
const status = resp.status;
|
|
1944
|
+
if (!resp.ok) {
|
|
1945
|
+
return { ok: false, status, buffer: Buffer.alloc(0) };
|
|
1946
|
+
}
|
|
1947
|
+
const ab = await resp.arrayBuffer();
|
|
1948
|
+
const disposition = resp.headers.get("content-disposition") ?? "";
|
|
1949
|
+
let filename;
|
|
1950
|
+
const utf8Match = disposition.match(/filename\*=UTF-8''([^;\s]+)/i);
|
|
1951
|
+
if (utf8Match) {
|
|
1952
|
+
filename = decodeURIComponent(utf8Match[1]);
|
|
1953
|
+
} else {
|
|
1954
|
+
const m = disposition.match(/filename="?([^";\s]+)"?/i);
|
|
1955
|
+
if (m)
|
|
1956
|
+
filename = decodeURIComponent(m[1]);
|
|
1957
|
+
}
|
|
1958
|
+
return {
|
|
1959
|
+
ok: true,
|
|
1960
|
+
status,
|
|
1961
|
+
buffer: Buffer.from(ab),
|
|
1962
|
+
filename
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
async function fetchAndSaveWecomMedia(input) {
|
|
1966
|
+
const fetcher = input.fetcher ?? defaultWecomMediaFetcher;
|
|
1967
|
+
const result = await fetcher(input.url);
|
|
1968
|
+
if (!result.ok) {
|
|
1969
|
+
throw new Error("fetchAndSaveWecomMedia: download failed (HTTP " + result.status + ")" + " for msgid=" + input.msgid + " url=" + input.url.slice(0, 120));
|
|
1970
|
+
}
|
|
1971
|
+
const decrypted = decryptWecomMedia(result.buffer, input.aeskey);
|
|
1972
|
+
return saveWecomMediaFile({
|
|
1973
|
+
msgid: input.msgid,
|
|
1974
|
+
filename: input.filename ?? result.filename,
|
|
1975
|
+
buffer: decrypted,
|
|
1976
|
+
chatId: input.chatId
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
async function saveWecomMediaFile(input) {
|
|
1980
|
+
const root = getWecomMediaDir();
|
|
1981
|
+
mkdirSync(root, { recursive: true, mode: 448 });
|
|
1982
|
+
const safeMsgid = input.msgid.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
|
|
1983
|
+
const safeName = input.filename ? basename(input.filename) : `${safeMsgid}.bin`;
|
|
1984
|
+
const safeChatId = input.chatId ? input.chatId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 120) : undefined;
|
|
1985
|
+
const dir = safeChatId ? join(resolve(root), safeChatId) : resolve(root);
|
|
1986
|
+
if (safeChatId)
|
|
1987
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1988
|
+
const filePath = join(dir, `${safeMsgid}_${safeName}`);
|
|
1989
|
+
writeFileSync(filePath, input.buffer);
|
|
1990
|
+
return filePath;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/env/event-source/wecom-cli-handler.ts
|
|
1994
|
+
var WECOM_CLI_ES_TYPE = "wecom-cli";
|
|
1995
|
+
var WECOM_CHAT_ID_PREFIX = "wecom:";
|
|
1996
|
+
var WECOM_WS_DEFAULT_URL = "wss://openws.work.weixin.qq.com";
|
|
1997
|
+
function readWecomCliConfig(config) {
|
|
1998
|
+
const top = config;
|
|
1999
|
+
const opts = config.options ?? {};
|
|
2000
|
+
return {
|
|
2001
|
+
botId: top.botId || opts.botId || undefined,
|
|
2002
|
+
secret: top.secret || opts.secret || undefined,
|
|
2003
|
+
command: top.command || opts.command || undefined,
|
|
2004
|
+
webhook: top.webhook || opts.webhook || undefined,
|
|
2005
|
+
credentialsPath: top.credentialsPath || opts.credentialsPath || undefined
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
var WECOM_CREDENTIALS_DEFAULT_PATH = join2(process.env.HOME || "/tmp", ".roy-agent", "wecom-credentials.json");
|
|
2009
|
+
function readWecomCredentialsFile(path) {
|
|
2010
|
+
try {
|
|
2011
|
+
const raw = readFileSync(path, "utf-8");
|
|
2012
|
+
const parsed = JSON.parse(raw);
|
|
2013
|
+
const agentId = typeof parsed.agent_id === "string" ? parsed.agent_id : undefined;
|
|
2014
|
+
const botId = typeof parsed.bot_id === "string" ? parsed.bot_id : undefined;
|
|
2015
|
+
const secret = typeof parsed.secret === "string" ? parsed.secret : undefined;
|
|
2016
|
+
const id = botId || agentId;
|
|
2017
|
+
if (!id || !secret)
|
|
2018
|
+
return null;
|
|
2019
|
+
return { botId: id, secret };
|
|
2020
|
+
} catch {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
function resolveWecomCredentials(config, credentialsPath = WECOM_CREDENTIALS_DEFAULT_PATH) {
|
|
2025
|
+
const cliConfig = readWecomCliConfig(config);
|
|
2026
|
+
if (cliConfig.botId && cliConfig.secret) {
|
|
2027
|
+
return { botId: cliConfig.botId, secret: cliConfig.secret };
|
|
2028
|
+
}
|
|
2029
|
+
const fromFile = readWecomCredentialsFile(cliConfig.credentialsPath ?? credentialsPath);
|
|
2030
|
+
if (fromFile)
|
|
2031
|
+
return fromFile;
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
function buildWecomReplyChannel(input) {
|
|
2035
|
+
const chatType = input.chatType === "group" ? "group" : "single";
|
|
2036
|
+
const userId = input.userId?.trim() || undefined;
|
|
2037
|
+
const rawChatId = input.chatId?.trim() || undefined;
|
|
2038
|
+
const chatId = rawChatId ?? userId ?? `${WECOM_CHAT_ID_PREFIX}unknown`;
|
|
2039
|
+
return {
|
|
2040
|
+
type: WECOM_CLI_ES_TYPE,
|
|
2041
|
+
chatId,
|
|
2042
|
+
messageId: input.messageId?.trim() || undefined,
|
|
2043
|
+
params: {
|
|
2044
|
+
chatType,
|
|
2045
|
+
userId,
|
|
2046
|
+
wecomChatId: rawChatId,
|
|
2047
|
+
wecomUserId: userId,
|
|
2048
|
+
...input.sourceId ? { sourceId: input.sourceId } : {},
|
|
2049
|
+
...input.reqId ? { reqId: input.reqId } : {}
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
function hasWecomMediaRef(body, kind) {
|
|
2054
|
+
const field = body[kind];
|
|
2055
|
+
return !!field && typeof field === "object" && typeof field.url === "string" && typeof field.aeskey === "string";
|
|
2056
|
+
}
|
|
2057
|
+
function extractText(body) {
|
|
2058
|
+
const msgType = String(body.msgtype ?? "");
|
|
2059
|
+
switch (msgType) {
|
|
2060
|
+
case "text": {
|
|
2061
|
+
const t = body.text;
|
|
2062
|
+
return t?.content ?? "";
|
|
2063
|
+
}
|
|
2064
|
+
case "mixed": {
|
|
2065
|
+
const mixed = body.mixed;
|
|
2066
|
+
const items = mixed?.msg_item ?? [];
|
|
2067
|
+
const parts = [];
|
|
2068
|
+
for (const it of items) {
|
|
2069
|
+
const item = it;
|
|
2070
|
+
if (item?.msgtype === "text" && item.text?.content) {
|
|
2071
|
+
parts.push(item.text.content);
|
|
2072
|
+
} else if (item?.msgtype) {
|
|
2073
|
+
parts.push(`[${item.msgtype}]`);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
return parts.join(`
|
|
2077
|
+
`);
|
|
2078
|
+
}
|
|
2079
|
+
case "image":
|
|
2080
|
+
return hasWecomMediaRef(body, "image") ? "[image](media:downloading)" : "[image]";
|
|
2081
|
+
case "voice":
|
|
2082
|
+
return hasWecomMediaRef(body, "voice") ? "[voice](media:downloading)" : "[voice]";
|
|
2083
|
+
case "file":
|
|
2084
|
+
return hasWecomMediaRef(body, "file") ? "[file](media:downloading)" : "[file]";
|
|
2085
|
+
case "video":
|
|
2086
|
+
return hasWecomMediaRef(body, "video") ? "[video](media:downloading)" : "[video]";
|
|
2087
|
+
default:
|
|
2088
|
+
return msgType ? `[${msgType}]` : "";
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
function extractWecomQuotedText(body) {
|
|
2092
|
+
const quote = body.quote;
|
|
2093
|
+
if (!quote || typeof quote !== "object")
|
|
2094
|
+
return;
|
|
2095
|
+
const msgType = String(quote.msgtype ?? "");
|
|
2096
|
+
if (msgType === "text") {
|
|
2097
|
+
const content = quote.text?.content;
|
|
2098
|
+
return typeof content === "string" && content ? content : undefined;
|
|
2099
|
+
}
|
|
2100
|
+
return msgType ? `[${msgType}]` : undefined;
|
|
2101
|
+
}
|
|
2102
|
+
function buildWecomUserMessage(text, replyContext) {
|
|
2103
|
+
const quotedText = replyContext?.quotedText;
|
|
2104
|
+
if (!quotedText)
|
|
2105
|
+
return text;
|
|
2106
|
+
return `${text}
|
|
2107
|
+
|
|
2108
|
+
[引用消息] ${quotedText}`;
|
|
2109
|
+
}
|
|
2110
|
+
function parseWecomFrame(raw) {
|
|
2111
|
+
if (!raw || typeof raw !== "object")
|
|
2112
|
+
return null;
|
|
2113
|
+
const frame = raw;
|
|
2114
|
+
const cmd = String(frame.cmd ?? "");
|
|
2115
|
+
const body = frame.body;
|
|
2116
|
+
if (!body || typeof body !== "object")
|
|
2117
|
+
return null;
|
|
2118
|
+
const msgid = typeof body.msgid === "string" ? body.msgid : undefined;
|
|
2119
|
+
const isMsg = cmd === "aibot_msg_callback";
|
|
2120
|
+
const isEvent = cmd === "aibot_event_callback";
|
|
2121
|
+
if (!isMsg && !isEvent)
|
|
2122
|
+
return null;
|
|
2123
|
+
if (isMsg && !msgid)
|
|
2124
|
+
return null;
|
|
2125
|
+
const msgType = String(body.msgtype ?? (isEvent ? "event" : ""));
|
|
2126
|
+
const from = body.from ?? {};
|
|
2127
|
+
const senderId = from.userid ?? "";
|
|
2128
|
+
const chattype = typeof body.chattype === "string" ? body.chattype : undefined;
|
|
2129
|
+
const rawChatId = typeof body.chatid === "string" ? body.chatid : undefined;
|
|
2130
|
+
let eventType;
|
|
2131
|
+
let text;
|
|
2132
|
+
if (isEvent) {
|
|
2133
|
+
const event = body.event ?? {};
|
|
2134
|
+
const eventTypeName = event.eventtype ?? "unknown";
|
|
2135
|
+
eventType = `wecom.event.${eventTypeName}`;
|
|
2136
|
+
text = `[wecom-event:${eventTypeName}]`;
|
|
2137
|
+
} else {
|
|
2138
|
+
eventType = "wecom.message.receive";
|
|
2139
|
+
text = extractText(body);
|
|
2140
|
+
}
|
|
2141
|
+
const metadata = {
|
|
2142
|
+
sourceType: WECOM_CLI_ES_TYPE,
|
|
2143
|
+
eventType,
|
|
2144
|
+
eventId: msgid,
|
|
2145
|
+
appId: typeof body.aibotid === "string" ? body.aibotid : undefined,
|
|
2146
|
+
chatId: rawChatId ?? senderId ?? undefined,
|
|
2147
|
+
chatType: chattype,
|
|
2148
|
+
messageId: msgid,
|
|
2149
|
+
messageType: msgType,
|
|
2150
|
+
senderId,
|
|
2151
|
+
sourceTime: typeof body.create_time === "number" ? new Date(body.create_time * 1000).toISOString() : undefined,
|
|
2152
|
+
payload: body
|
|
2153
|
+
};
|
|
2154
|
+
const quotedText = extractWecomQuotedText(body);
|
|
2155
|
+
if (quotedText) {
|
|
2156
|
+
metadata.replyContext = { quotedText };
|
|
2157
|
+
}
|
|
2158
|
+
return {
|
|
2159
|
+
metadata,
|
|
2160
|
+
text,
|
|
2161
|
+
reqId: frame.headers?.req_id
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
var WECOM_CHANNEL_PROMPT_SECTION_TITLE = "⭐ 企业微信回复规范(system prompt 注入版)";
|
|
2165
|
+
function buildWecomChannelSystemPrompt(opts) {
|
|
2166
|
+
const botLine = opts.botId ? `本渠道智能机器人 BotID:${opts.botId}。` : `本渠道未配置 BotID(使用事件回调里的 aibotid)。`;
|
|
2167
|
+
return [
|
|
2168
|
+
`# ${WECOM_CHANNEL_PROMPT_SECTION_TITLE}`,
|
|
2169
|
+
``,
|
|
2170
|
+
`本节是企业微信渠道的**回复规范**(静态规则,随 system prompt 注入)。`,
|
|
2171
|
+
`每条企微事件的 \`recommendedAction\` 只带动态定位(chatId / messageId),`,
|
|
2172
|
+
`所有回复规则都在本节 —— replyChannel 给出的 chatId 是本轮唯一权威定位。`,
|
|
2173
|
+
botLine,
|
|
2174
|
+
``,
|
|
2175
|
+
`## 回复命令(唯一出站通道)`,
|
|
2176
|
+
`用 \`wecom_respond\` 工具回复。工具内部**主路径是官方 CLI**`,
|
|
2177
|
+
`(\`wecom-cli message aibot send --msg-type markdown\`,已扫码授权),`,
|
|
2178
|
+
`CLI 失败时自动降级为 WS 长连接回帧(aibot_respond_msg)。`,
|
|
2179
|
+
`- \`content\`:**必填**,回复正文(markdown 格式)。`,
|
|
2180
|
+
`- \`chat_id\`:可选;缺省自动取本事件的 chatId/senderId(单聊=对方 userid,群聊=chatid)。`,
|
|
2181
|
+
`- \`req_id\`:可选;缺省取 metadata.reqId(仅 WS fallback 用)。`,
|
|
2182
|
+
`- ⚠️ 不要自己拼 \`wecom-cli\` 命令 —— 统一走 \`wecom_respond\`(chat_id 需要内部解析成真实 userid,手工拼会报 40073)。`,
|
|
2183
|
+
`- ⚠️ CLI 没有 text 类型;markdown 上限 20480 UTF-8 字节,超长分段压缩,不要静默截断。`,
|
|
2184
|
+
`- 工具返回 success=false 时如实告知用户,不要假装已发送。`,
|
|
2185
|
+
``,
|
|
2186
|
+
`## 出站纪律`,
|
|
2187
|
+
`- ❌ 不要用 \`curl\` 直连 \`qyapi.weixin.qq.com\`(绕过 CLI 丢鉴权与错误规范)。`,
|
|
2188
|
+
`- ❌ 不要用 \`lark-cli\` 的任何 flag(--profile / --msg-type / --as bot 都不适用于企微)。`,
|
|
2189
|
+
`- ❌ 整次回复只发**一条**消息:先想清楚再发,不要拆段循环 send 刷屏。`,
|
|
2190
|
+
``,
|
|
2191
|
+
`## 文档交付规范`,
|
|
2192
|
+
`- 文档类生成(报告 / 代码 / 长文):企微消息里给**结论 + 关键内容摘要**,`,
|
|
2193
|
+
` 完整文档内容直接放进 markdown 正文(企微支持 markdown 渲染),`,
|
|
2194
|
+
` **不要只给本地文件路径** —— 企微用户无法访问 roy-agent 所在机器的文件系统。`,
|
|
2195
|
+
`- **调研文档 / docs / ppt 等文档类产出必须上传到企微文档,链接作为交付物**:`,
|
|
2196
|
+
` - 在线文档:\`wecom-cli doc create --doc-name <标题> --doc-type doc --content-type text --file-path <本地md>\``,
|
|
2197
|
+
` (或 --content 传纯文本;ppt 用 --doc-type ppt),取返回里的文档链接`,
|
|
2198
|
+
` - 微盘文件:\`wecom-cli disk files upload --file-path <本地文件>\`(默认传到我的空间),取返回里的文件链接`,
|
|
2199
|
+
` - 本地路径只作中转(生成 → 上传企微文档 → 回复里贴链接),**不落本地路径**交付`,
|
|
2200
|
+
``,
|
|
2201
|
+
`## 图片/文件消息处理(Task #5199)`,
|
|
2202
|
+
`- 用户发图片时,消息正文是 \`[image](media:downloading)\` 占位;`,
|
|
2203
|
+
` 媒体文件已自动下载解密落盘,本地路径在事件 metadata.mediaFiles`,
|
|
2204
|
+
` (数组,如 \`/tmp/wecom-media/<chatId>/<msgid>_photo.png\`)。`,
|
|
2205
|
+
`- ⚠️ **只用 metadata.mediaFiles 里列出的路径**(Task #5313)——`,
|
|
2206
|
+
` 禁止 \`ls\`/\`find\` 扫媒体目录:同目录下的历史文件属于别的消息,`,
|
|
2207
|
+
` 扫目录会把它们误当本条消息的附件。`,
|
|
2208
|
+
`- **看图**:用 \`understand_image\` 工具,\`image_source\` 传`,
|
|
2209
|
+
` metadata.mediaFiles 里的本地路径,\`prompt\` 写想了解什么。`,
|
|
2210
|
+
`- mediaFiles 缺失(下载失败/URL 过期)时如实告知用户,不要编造图片内容。`,
|
|
2211
|
+
`- ⚠️ 截图内容边界(Task #5317):截图/图片里可能内嵌`,
|
|
2212
|
+
` **其他群聊或其他会话的聊天记录**(如聊天记录截图、转发图)。`,
|
|
2213
|
+
` 截图里的聊天记录**不是本群的对话上下文**——分析图片时如需`,
|
|
2214
|
+
` 提及其中内容,必须明确标注「截图里包含的内容」,`,
|
|
2215
|
+
` 禁止把截图内的聊天记录当作本群刚发生的消息来转述或回应。`,
|
|
2216
|
+
``,
|
|
2217
|
+
`## 引用消息上下文 replyContext`,
|
|
2218
|
+
`本条消息引用了其他消息时,user message 末尾会带 \`[引用消息] <原文>\` 行;`,
|
|
2219
|
+
`同时 recommendedAction.replyContext.quotedText 也有同一份内容(非文本引用为 [image] 等占位)。`,
|
|
2220
|
+
`- 回复时优先围绕被引用内容作答,不要忽略引用语境。`
|
|
2221
|
+
].join(`
|
|
2222
|
+
`);
|
|
2223
|
+
}
|
|
2224
|
+
function buildWecomNotifyPrompt(input) {
|
|
2225
|
+
const isGroup = input.chatType === "group";
|
|
2226
|
+
const target = isGroup ? `群聊 chat_id = "${input.chatId ?? "<chatid>"}"` : `单聊 chat_id = "${input.userId ?? "<userid>"}"(企微单聊用对方 userid 作 chat_id)`;
|
|
2227
|
+
return [
|
|
2228
|
+
`### ① 用 wecom_respond 工具回消息
|
|
2229
|
+
` + `本事件来自企业微信(WeCom)。回复用户请用 \`wecom_respond\` 工具(主路径 = 官方 wecom-cli 已授权,CLI 失败自动降级 WS 回帧),目标:${target}。
|
|
2230
|
+
` + `不要使用 lark-cli —— 那是飞书的通道,参数完全不同。`,
|
|
2231
|
+
`### ② 具体参数
|
|
2232
|
+
` + `调用 \`wecom_respond\` 工具:
|
|
2233
|
+
` + `- \`content\`:**必填**,回复正文(markdown)
|
|
2234
|
+
` + `- \`chat_id\`:可选;缺省自动取本事件 chatId / senderId(单聊 = 对方 userid)
|
|
2235
|
+
` + `- \`req_id\`:可选;缺省取 metadata.reqId(仅 WS fallback 用)
|
|
2236
|
+
` + `- ⚠️ 不要自己拼 \`wecom-cli\` 命令 —— chat_id 需内部解析成真实 userid,手拼会报 40073`,
|
|
2237
|
+
`### ③ 禁止的做法
|
|
2238
|
+
` + `- ❌ 不要用 \`curl\` 直连 \`qyapi.weixin.qq.com\` —— 绕过官方通道会丢鉴权与错误规范
|
|
2239
|
+
` + `- ❌ 不要用 \`lark-cli\` 的任何 flag(--profile / --msg-type post / --content)
|
|
2240
|
+
` + `- ❌ 不要自己编 chat_id / req_id —— 用工具默认值即可
|
|
2241
|
+
` + `- ❌ 不要在失败时假装已发送`,
|
|
2242
|
+
`### ④ 怎么判断成功
|
|
2243
|
+
` + `- 工具返回 \`success: true\` = 回帧已发出
|
|
2244
|
+
` + `- 工具返回 \`success: false\` = 发送失败(读 \`error\` 字段),如实告知用户失败原因`
|
|
2245
|
+
].join(`
|
|
2246
|
+
|
|
2247
|
+
`);
|
|
2248
|
+
}
|
|
2249
|
+
function resolveWecomBinary(binName = "wecom-cli") {
|
|
2250
|
+
if (isAbsolute(binName)) {
|
|
2251
|
+
return existsSync(binName) ? { found: true, path: binName } : { found: false };
|
|
2252
|
+
}
|
|
2253
|
+
if (binName.includes("/")) {
|
|
2254
|
+
return existsSync(binName) ? { found: true, path: binName } : { found: false };
|
|
2255
|
+
}
|
|
2256
|
+
const pathEnv = process.env.PATH ?? "";
|
|
2257
|
+
const candidates = [binName, `${binName}.exe`, `${binName}.cmd`, `${binName}.bat`];
|
|
2258
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
2259
|
+
if (!dir)
|
|
2260
|
+
continue;
|
|
2261
|
+
for (const candidate of candidates) {
|
|
2262
|
+
const full = join2(dir, candidate);
|
|
2263
|
+
if (existsSync(full))
|
|
2264
|
+
return { found: true, path: full };
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
return { found: false };
|
|
2268
|
+
}
|
|
2269
|
+
function probeCommandBinary(command) {
|
|
2270
|
+
const stripped = command.trim().replace(/^exec\s+/, "");
|
|
2271
|
+
const firstToken = (stripped.match(/^"[^"]+"|^'[^']+'|^\S+/) ?? [""])[0].replace(/^["']|["']$/g, "");
|
|
2272
|
+
if (!firstToken)
|
|
2273
|
+
return { found: true };
|
|
2274
|
+
if (!firstToken.includes("/")) {
|
|
2275
|
+
return resolveWecomBinary(firstToken);
|
|
2276
|
+
}
|
|
2277
|
+
return resolveWecomBinary(firstToken);
|
|
2278
|
+
}
|
|
2279
|
+
async function defaultWecomWsFactory() {
|
|
2280
|
+
const { WebSocket: Ws } = await import("ws");
|
|
2281
|
+
return (url) => new Ws(url);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
class WecomCliInstance {
|
|
2285
|
+
config;
|
|
2286
|
+
status = "created";
|
|
2287
|
+
child;
|
|
2288
|
+
buffer = "";
|
|
2289
|
+
eventHandler;
|
|
2290
|
+
lastError;
|
|
2291
|
+
wsBridge;
|
|
2292
|
+
pluginInstances = new Map;
|
|
2293
|
+
loadedPlugins = [];
|
|
2294
|
+
cardActionPorts;
|
|
2295
|
+
constructor(config) {
|
|
2296
|
+
this.config = config;
|
|
2297
|
+
}
|
|
2298
|
+
setCardActionPorts(ports) {
|
|
2299
|
+
this.cardActionPorts = ports;
|
|
2300
|
+
}
|
|
2301
|
+
hasCardActionPorts() {
|
|
2302
|
+
return this.cardActionPorts !== undefined;
|
|
2303
|
+
}
|
|
2304
|
+
environment;
|
|
2305
|
+
setEnvironment(env) {
|
|
2306
|
+
this.environment = env;
|
|
2307
|
+
}
|
|
2308
|
+
getBridge() {
|
|
2309
|
+
return this.wsBridge;
|
|
2310
|
+
}
|
|
2311
|
+
async handleWsFrame(frame) {
|
|
2312
|
+
const cardAction = extractWecomCardAction(frame);
|
|
2313
|
+
if (cardAction) {
|
|
2314
|
+
await this.handleCardActionEvent(frame, cardAction);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
const parsed = parseWecomFrame(frame);
|
|
2318
|
+
if (!parsed)
|
|
2319
|
+
return;
|
|
2320
|
+
const { botId } = readWecomCliConfig(this.config);
|
|
2321
|
+
const channelPrompt = buildWecomChannelSystemPrompt(botId ? { botId } : {});
|
|
2322
|
+
const metadata = {
|
|
2323
|
+
...parsed.metadata,
|
|
2324
|
+
systemPrompt: channelPrompt,
|
|
2325
|
+
reqId: parsed.reqId,
|
|
2326
|
+
plugins: this.config.handleRule?.plugins || [],
|
|
2327
|
+
pluginEnabled: Object.fromEntries((this.config.handleRule?.plugins || []).map((p) => [
|
|
2328
|
+
`${this.config.id}:${p}`,
|
|
2329
|
+
true
|
|
2330
|
+
]))
|
|
2331
|
+
};
|
|
2332
|
+
const replyChannel = buildWecomReplyChannel({
|
|
2333
|
+
chatId: metadata.chatId,
|
|
2334
|
+
messageId: metadata.messageId,
|
|
2335
|
+
chatType: metadata.chatType,
|
|
2336
|
+
userId: metadata.senderId,
|
|
2337
|
+
sourceId: this.config.id,
|
|
2338
|
+
reqId: parsed.reqId
|
|
2339
|
+
});
|
|
2340
|
+
const event = {
|
|
2341
|
+
id: `es-${this.config.id}-${Date.now()}`,
|
|
2342
|
+
type: "event-source.event.wecom-cli",
|
|
2343
|
+
timestamp: Date.now(),
|
|
2344
|
+
metadata: {
|
|
2345
|
+
source: "event-source",
|
|
2346
|
+
sourceId: this.config.id,
|
|
2347
|
+
sourceMessageType: metadata.eventType,
|
|
2348
|
+
systemPrompt: channelPrompt,
|
|
2349
|
+
replyChannel,
|
|
2350
|
+
...metadata
|
|
2351
|
+
},
|
|
2352
|
+
payload: {
|
|
2353
|
+
sourceId: this.config.id,
|
|
2354
|
+
sourceType: WECOM_CLI_ES_TYPE,
|
|
2355
|
+
rawEvent: frame,
|
|
2356
|
+
message: buildWecomUserMessage(parsed.text, metadata.replyContext),
|
|
2357
|
+
metadata,
|
|
2358
|
+
replyChannel,
|
|
2359
|
+
timestamp: Date.now()
|
|
2360
|
+
},
|
|
2361
|
+
rawEvent: frame,
|
|
2362
|
+
replyChannel,
|
|
2363
|
+
recommendedAction: {
|
|
2364
|
+
action: `处理企业微信消息并回复。回复规范见 system prompt「${WECOM_CHANNEL_PROMPT_SECTION_TITLE}」章节。` + ` 用 wecom_respond 工具回复(content=回复正文);` + `chat_id 缺省取本事件 chatId。` + (parsed.reqId ? `(WS fallback req_id=${parsed.reqId})` : ""),
|
|
2365
|
+
replyTo: {
|
|
2366
|
+
chatId: replyChannel.chatId,
|
|
2367
|
+
messageId: replyChannel.messageId
|
|
2368
|
+
},
|
|
2369
|
+
...metadata.replyContext ? { replyContext: metadata.replyContext } : {},
|
|
2370
|
+
staticRulesInSystemPrompt: true
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
const mediaRefs = extractWecomMediaRefs(frame.body ?? {});
|
|
2374
|
+
if (mediaRefs.length > 0 && parsed.metadata.messageId) {
|
|
2375
|
+
const msgid = parsed.metadata.messageId;
|
|
2376
|
+
const saved = await this.downloadMediaRefs(msgid, mediaRefs, parsed.metadata.chatId);
|
|
2377
|
+
if (saved.length > 0) {
|
|
2378
|
+
event.payload.mediaFiles = saved;
|
|
2379
|
+
event.metadata.mediaFiles = saved;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
try {
|
|
2383
|
+
this.eventHandler?.(event);
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
console.error(`[wecom-cli] event handler threw: ${err}`);
|
|
2386
|
+
notifyReactFailure({
|
|
2387
|
+
error: err,
|
|
2388
|
+
envEvent: event,
|
|
2389
|
+
replyChannel
|
|
2390
|
+
}).catch(() => {});
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
async handleCardActionEvent(frame, action) {
|
|
2394
|
+
if (!this.cardActionPorts) {
|
|
2395
|
+
console.warn(`[wecom-cli] handleCardActionEvent: cardActionPorts 未注入,` + `template_card_event 被跳过(event_key=${action?.eventKey})。` + `调 instance.setCardActionPorts({markTaskDone, reopenTask, updateCard}) 注入。`);
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
try {
|
|
2399
|
+
const result = await handleWecomCardAction(frame, this.cardActionPorts);
|
|
2400
|
+
if (!result.ok) {
|
|
2401
|
+
console.warn(`[wecom-cli] handleCardActionEvent failed: ${result.error ?? "unknown"}`);
|
|
2402
|
+
}
|
|
2403
|
+
} catch (err) {
|
|
2404
|
+
console.error(`[wecom-cli] handleCardActionEvent threw: ${err.message}`);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
wireCardActionPorts() {
|
|
2408
|
+
if (this.cardActionPorts)
|
|
2409
|
+
return;
|
|
2410
|
+
const taskComponent = this.environment?.getComponent?.("task");
|
|
2411
|
+
if (!taskComponent?.updateTask || !this.wsBridge) {
|
|
2412
|
+
console.warn(`[wecom-cli] cardActionPorts 未注入:taskComponent.updateTask=` + `${typeof taskComponent?.updateTask}, wsBridge=${!!this.wsBridge}。` + `卡片按钮点击会被跳过。`);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
this.cardActionPorts = {
|
|
2416
|
+
markTaskDone: async (taskId) => {
|
|
2417
|
+
await taskComponent.updateTask(taskId, {
|
|
2418
|
+
status: "completed",
|
|
2419
|
+
current_status: "用户在企微卡片点「标记完成」确认完成"
|
|
2420
|
+
});
|
|
2421
|
+
},
|
|
2422
|
+
reopenTask: async (taskId) => {
|
|
2423
|
+
await taskComponent.updateTask(taskId, {
|
|
2424
|
+
status: "active",
|
|
2425
|
+
current_status: "用户在企微卡片点「重新打开」"
|
|
2426
|
+
});
|
|
2427
|
+
},
|
|
2428
|
+
updateCard: async (reqId, cardTaskId, statusLabel, note, timeline, action, extra) => {
|
|
2429
|
+
const { cmd, body } = buildWecomCardUpdateBody({
|
|
2430
|
+
cardTaskId,
|
|
2431
|
+
statusLabel,
|
|
2432
|
+
note,
|
|
2433
|
+
timeline,
|
|
2434
|
+
action,
|
|
2435
|
+
taskTitle: extra?.taskTitle,
|
|
2436
|
+
taskNumber: extra?.taskNumber
|
|
2437
|
+
});
|
|
2438
|
+
const frame = {
|
|
2439
|
+
cmd,
|
|
2440
|
+
headers: { req_id: reqId },
|
|
2441
|
+
body
|
|
2442
|
+
};
|
|
2443
|
+
const err = this.wsBridge.sendFrameWithReqId(frame);
|
|
2444
|
+
if (err)
|
|
2445
|
+
throw err;
|
|
2446
|
+
},
|
|
2447
|
+
loadTimeline: (taskId) => {
|
|
2448
|
+
const store = this.getWecomCardStateStore();
|
|
2449
|
+
if (!store)
|
|
2450
|
+
return [];
|
|
2451
|
+
const rec = store.get(taskId);
|
|
2452
|
+
if (!rec?.timeline)
|
|
2453
|
+
return [];
|
|
2454
|
+
try {
|
|
2455
|
+
const arr = JSON.parse(rec.timeline);
|
|
2456
|
+
return Array.isArray(arr) ? arr : [];
|
|
2457
|
+
} catch {
|
|
2458
|
+
return [];
|
|
2459
|
+
}
|
|
2460
|
+
},
|
|
2461
|
+
loadTaskTitle: (taskId) => {
|
|
2462
|
+
const taskComponent2 = this.environment?.getComponent?.("task");
|
|
2463
|
+
const getter = taskComponent2?.getTask;
|
|
2464
|
+
if (!getter)
|
|
2465
|
+
return;
|
|
2466
|
+
try {
|
|
2467
|
+
const raw = getter.call(taskComponent2, taskId);
|
|
2468
|
+
const extract = (t) => {
|
|
2469
|
+
if (!t || typeof t !== "object")
|
|
2470
|
+
return;
|
|
2471
|
+
const rec = t;
|
|
2472
|
+
if (typeof rec.title !== "string" || !rec.title)
|
|
2473
|
+
return;
|
|
2474
|
+
return {
|
|
2475
|
+
title: rec.title,
|
|
2476
|
+
number: typeof rec.id === "number" ? rec.id : taskId
|
|
2477
|
+
};
|
|
2478
|
+
};
|
|
2479
|
+
if (raw && typeof raw.then === "function") {
|
|
2480
|
+
return raw.then(extract).catch((err) => {
|
|
2481
|
+
console.warn(`[wecom-cli] loadTaskTitle failed for task #${taskId}: ${err}`);
|
|
2482
|
+
return;
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
return extract(raw);
|
|
2486
|
+
} catch (err) {
|
|
2487
|
+
console.warn(`[wecom-cli] loadTaskTitle failed for task #${taskId}: ${err}`);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
console.info(`[wecom-cli] cardActionPorts 已注入(task ops + bridge.respond 更新 + timeline + taskTitle)`);
|
|
2493
|
+
}
|
|
2494
|
+
getWecomCardStateStore() {
|
|
2495
|
+
const plugin = this.pluginInstances.get("WecomTaskNotifyPlugin");
|
|
2496
|
+
const store = plugin?.cardStateStore;
|
|
2497
|
+
if (store && typeof store.get === "function") {
|
|
2498
|
+
return store;
|
|
2499
|
+
}
|
|
2500
|
+
if (!this.fallbackCardStateStore) {
|
|
2501
|
+
try {
|
|
2502
|
+
this.fallbackCardStateStore = createWecomCardStateStore();
|
|
2503
|
+
} catch (err) {
|
|
2504
|
+
console.warn(`[wecom-cli] fallback card state store 创建失败: ${err}`);
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
return this.fallbackCardStateStore;
|
|
2509
|
+
}
|
|
2510
|
+
fallbackCardStateStore;
|
|
2511
|
+
async downloadMediaRefs(msgid, refs, chatId) {
|
|
2512
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("media download timeout (10s)")), 1e4));
|
|
2513
|
+
try {
|
|
2514
|
+
const paths = await Promise.race([
|
|
2515
|
+
timeout,
|
|
2516
|
+
Promise.all(refs.map(async (ref) => {
|
|
2517
|
+
try {
|
|
2518
|
+
const path = await fetchAndSaveWecomMedia({
|
|
2519
|
+
msgid,
|
|
2520
|
+
url: ref.url,
|
|
2521
|
+
aeskey: ref.aeskey,
|
|
2522
|
+
kind: ref.kind,
|
|
2523
|
+
filename: ref.filename,
|
|
2524
|
+
chatId
|
|
2525
|
+
});
|
|
2526
|
+
console.log(`[wecom-cli] media saved: ${ref.kind} chat=${chatId ?? "(none)"} ${path}`);
|
|
2527
|
+
return path;
|
|
2528
|
+
} catch (err) {
|
|
2529
|
+
console.error(`[wecom-cli] media download failed for ${ref.kind}: ${err.message}`);
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
}))
|
|
2533
|
+
]);
|
|
2534
|
+
return paths.filter((p) => !!p);
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
console.error(`[wecom-cli] media download aborted: ${err.message}`);
|
|
2537
|
+
return [];
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
getStatus() {
|
|
2541
|
+
return this.status;
|
|
2542
|
+
}
|
|
2543
|
+
setStatus(status) {
|
|
2544
|
+
this.status = status;
|
|
2545
|
+
}
|
|
2546
|
+
onEvent(handler) {
|
|
2547
|
+
this.eventHandler = handler;
|
|
2548
|
+
}
|
|
2549
|
+
offEvent() {
|
|
2550
|
+
this.eventHandler = undefined;
|
|
2551
|
+
}
|
|
2552
|
+
getLastError() {
|
|
2553
|
+
return this.lastError;
|
|
2554
|
+
}
|
|
2555
|
+
buildCommand() {
|
|
2556
|
+
const { command } = readWecomCliConfig(this.config);
|
|
2557
|
+
if (command)
|
|
2558
|
+
return command;
|
|
2559
|
+
return "wecom-cli chat messages list --help";
|
|
2560
|
+
}
|
|
2561
|
+
loadPlugins() {
|
|
2562
|
+
const plugins = this.config.handleRule?.plugins || [];
|
|
2563
|
+
if (plugins.length === 0)
|
|
2564
|
+
return;
|
|
2565
|
+
for (const pluginName of plugins) {
|
|
2566
|
+
const pluginConfig = this.config.handleRule?.pluginConfig?.[pluginName] ?? {};
|
|
2567
|
+
const plugin = importPluginShared(pluginName, getWecomPluginFactories(), {
|
|
2568
|
+
sourceId: this.config.id,
|
|
2569
|
+
pluginName,
|
|
2570
|
+
...pluginConfig
|
|
2571
|
+
});
|
|
2572
|
+
if (!plugin)
|
|
2573
|
+
continue;
|
|
2574
|
+
this.pluginInstances.set(pluginName, plugin);
|
|
2575
|
+
const hookPoints = registerPluginHooksShared(plugin, pluginName, this.config.id);
|
|
2576
|
+
this.loadedPlugins.push({ name: pluginName, hooks: hookPoints });
|
|
2577
|
+
console.debug(`[wecom-cli] Loaded plugin "${pluginName}" with ${hookPoints.length} hook(s): [${hookPoints.join(", ")}]`);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
unloadPlugins() {
|
|
2581
|
+
for (const p of this.loadedPlugins) {
|
|
2582
|
+
unregisterPluginHooksShared(p.name, p.hooks);
|
|
2583
|
+
}
|
|
2584
|
+
this.loadedPlugins = [];
|
|
2585
|
+
this.pluginInstances.clear();
|
|
2586
|
+
}
|
|
2587
|
+
async start() {
|
|
2588
|
+
this.loadPlugins();
|
|
2589
|
+
await this.startWithBridge(await defaultWecomWsFactory());
|
|
2590
|
+
}
|
|
2591
|
+
async startWithBridge(wsFactory, credentialsPath) {
|
|
2592
|
+
if (this.status === "running")
|
|
2593
|
+
return;
|
|
2594
|
+
this.setStatus("starting");
|
|
2595
|
+
const credentials = resolveWecomCredentials(this.config, credentialsPath);
|
|
2596
|
+
if (credentials) {
|
|
2597
|
+
this.startWsBridge(credentials, wsFactory);
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
this.startSkeletonMode();
|
|
2601
|
+
}
|
|
2602
|
+
startWsBridge(credentials, wsFactory) {
|
|
2603
|
+
const bridge = new WecomWsBridge({
|
|
2604
|
+
botId: credentials.botId,
|
|
2605
|
+
secret: credentials.secret,
|
|
2606
|
+
wsFactory
|
|
2607
|
+
});
|
|
2608
|
+
this.wsBridge = bridge;
|
|
2609
|
+
registerWecomRespondTarget(this.config.id, {
|
|
2610
|
+
respond: (reqId, body) => bridge.respond(reqId, body),
|
|
2611
|
+
sendCmd: (cmd, body) => bridge.sendCmd(cmd, body)
|
|
2612
|
+
});
|
|
2613
|
+
this.wireCardActionPorts();
|
|
2614
|
+
bridge.onMessage((frame) => {
|
|
2615
|
+
this.handleWsFrame(frame).catch((err) => {
|
|
2616
|
+
console.error(`[wecom-cli] ws frame handling threw: ${err}`);
|
|
2617
|
+
try {
|
|
2618
|
+
const parsed = parseWecomFrame(frame);
|
|
2619
|
+
if (!parsed) {
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
const baseChannel = buildWecomReplyChannel({
|
|
2623
|
+
chatId: parsed.metadata.chatId,
|
|
2624
|
+
messageId: parsed.metadata.messageId,
|
|
2625
|
+
chatType: parsed.metadata.chatType,
|
|
2626
|
+
userId: parsed.metadata.senderId,
|
|
2627
|
+
sourceId: this.config.id,
|
|
2628
|
+
reqId: parsed.reqId
|
|
2629
|
+
});
|
|
2630
|
+
notifyReactFailure({
|
|
2631
|
+
error: err,
|
|
2632
|
+
envEvent: {
|
|
2633
|
+
type: "event-source.event.wecom-cli",
|
|
2634
|
+
metadata: {
|
|
2635
|
+
sourceId: this.config.id,
|
|
2636
|
+
replyChannel: baseChannel
|
|
2637
|
+
},
|
|
2638
|
+
payload: {}
|
|
2639
|
+
}
|
|
2640
|
+
}).catch(() => {});
|
|
2641
|
+
} catch {}
|
|
2642
|
+
});
|
|
2643
|
+
});
|
|
2644
|
+
bridge.onError((err) => {
|
|
2645
|
+
this.lastError = err.message;
|
|
2646
|
+
console.error(`[wecom-cli] ws error: ${err.message}`);
|
|
2647
|
+
});
|
|
2648
|
+
bridge.connect();
|
|
2649
|
+
this.setStatus("running");
|
|
2650
|
+
}
|
|
2651
|
+
startSkeletonMode() {
|
|
2652
|
+
console.warn(`[wecom-cli] ⚠️ 骨架模式:未找到企微凭据(config botId/secret 或 ` + `${WECOM_CREDENTIALS_DEFAULT_PATH}),**不会收到任何入站消息**。
|
|
2653
|
+
` + `[wecom-cli] 请在 event source 配置里提供 botId + secret,或创建凭据文件(chmod 600)。
|
|
2654
|
+
` + `[wecom-cli] 如需临时自定义,可在事件源配置里设置 'command' 字段。`);
|
|
2655
|
+
this.startSkeletonSpawn();
|
|
2656
|
+
}
|
|
2657
|
+
startSkeletonSpawn() {
|
|
2658
|
+
const { command: customCommand } = readWecomCliConfig(this.config);
|
|
2659
|
+
if (!customCommand) {
|
|
2660
|
+
console.warn(`[wecom-cli] ⚠️ 骨架模式:当前启动的是占位命令 ` + `"wecom-cli chat messages list --help",它**不会产出任何入站事件**。
|
|
2661
|
+
` + `[wecom-cli] Phase 2 长连接需要凭据(config botId/secret 或凭据文件)。
|
|
2662
|
+
` + `[wecom-cli] 如需临时自定义,可在事件源配置里设置 'command' 字段。`);
|
|
2663
|
+
}
|
|
2664
|
+
if (customCommand) {
|
|
2665
|
+
const probe = probeCommandBinary(customCommand);
|
|
2666
|
+
if (!probe.found) {
|
|
2667
|
+
this.lastError = `wecom-cli command not found: ${customCommand}`;
|
|
2668
|
+
this.setStatus("error");
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
} else {
|
|
2672
|
+
const bin = resolveWecomBinary();
|
|
2673
|
+
if (!bin.found) {
|
|
2674
|
+
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).";
|
|
2675
|
+
this.setStatus("error");
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
const command = this.buildCommand();
|
|
2680
|
+
let child;
|
|
2681
|
+
try {
|
|
2682
|
+
child = spawn2(`exec ${command}`, [], {
|
|
2683
|
+
shell: true,
|
|
2684
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2685
|
+
windowsHide: true
|
|
2686
|
+
});
|
|
2687
|
+
} catch (err) {
|
|
2688
|
+
this.lastError = `failed to spawn wecom-cli: ${err.message}`;
|
|
2689
|
+
this.setStatus("error");
|
|
1131
2690
|
return;
|
|
1132
|
-
this.setStatus("starting");
|
|
1133
|
-
const { channel, profile, customCommand } = readBountyCliConfig(this.config);
|
|
1134
|
-
if (!channel && !customCommand) {
|
|
1135
|
-
throw new Error("bounty-cli event source requires 'channel' (or 'command') in config");
|
|
1136
2691
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1141
|
-
windowsHide: true
|
|
2692
|
+
this.child = child;
|
|
2693
|
+
child.stdout?.on("data", (data) => {
|
|
2694
|
+
this.processStdout(data.toString());
|
|
1142
2695
|
});
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
stdoutBuffer = "";
|
|
2696
|
+
child.stderr?.on("data", (data) => {
|
|
2697
|
+
const text = data.toString().trim();
|
|
2698
|
+
if (text) {
|
|
2699
|
+
this.lastError = text.slice(0, 500);
|
|
2700
|
+
console.error(`[wecom-cli] ${text}`);
|
|
2701
|
+
}
|
|
1150
2702
|
});
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
this.processStderr(stderrBuffer);
|
|
1155
|
-
stderrBuffer = "";
|
|
2703
|
+
child.on("error", (err) => {
|
|
2704
|
+
this.lastError = `wecom-cli process error: ${err.message}`;
|
|
2705
|
+
this.setStatus("error");
|
|
1156
2706
|
});
|
|
1157
|
-
|
|
2707
|
+
child.on("exit", (code, signal) => {
|
|
1158
2708
|
if (code !== 0 && signal !== "SIGINT" && signal !== "SIGTERM") {
|
|
2709
|
+
this.lastError = `wecom-cli exited with code ${code}`;
|
|
1159
2710
|
this.setStatus("error");
|
|
1160
2711
|
} else {
|
|
1161
2712
|
this.setStatus("stopped");
|
|
1162
2713
|
}
|
|
1163
2714
|
});
|
|
1164
|
-
this.child.on("error", (err) => {
|
|
1165
|
-
console.error(`[bounty-cli] spawn error: ${err.message}`);
|
|
1166
|
-
this.setStatus("error");
|
|
1167
|
-
});
|
|
1168
2715
|
this.setStatus("running");
|
|
1169
2716
|
}
|
|
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(`
|
|
2717
|
+
processStdout(chunk) {
|
|
2718
|
+
this.buffer += chunk;
|
|
2719
|
+
const lines = this.buffer.split(`
|
|
1184
2720
|
`);
|
|
1185
|
-
|
|
2721
|
+
this.buffer = lines.pop() ?? "";
|
|
1186
2722
|
for (const line of lines) {
|
|
1187
2723
|
const trimmed = line.trim();
|
|
1188
2724
|
if (!trimmed)
|
|
1189
2725
|
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 = "";
|
|
2726
|
+
this.handleEventLine(trimmed);
|
|
1198
2727
|
}
|
|
1199
2728
|
}
|
|
1200
|
-
handleEventLine(line
|
|
2729
|
+
handleEventLine(line) {
|
|
1201
2730
|
let raw;
|
|
1202
2731
|
try {
|
|
1203
2732
|
raw = JSON.parse(line);
|
|
1204
2733
|
} catch {
|
|
1205
|
-
console.error(`[
|
|
2734
|
+
console.error(`[wecom-cli] skipping non-JSON line: ${line.slice(0, 200)}`);
|
|
1206
2735
|
return;
|
|
1207
2736
|
}
|
|
1208
|
-
const
|
|
2737
|
+
const parsed = parseWecomFrame(raw);
|
|
2738
|
+
if (!parsed) {
|
|
2739
|
+
console.error(`[wecom-cli] skipping non-event frame: ${JSON.stringify(raw).slice(0, 200)}`);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
const replyChannel = buildWecomReplyChannel({
|
|
2743
|
+
chatId: parsed.metadata.chatId,
|
|
2744
|
+
messageId: parsed.metadata.messageId,
|
|
2745
|
+
chatType: parsed.metadata.chatType,
|
|
2746
|
+
userId: parsed.metadata.senderId,
|
|
2747
|
+
sourceId: this.config.id,
|
|
2748
|
+
reqId: parsed.reqId
|
|
2749
|
+
});
|
|
1209
2750
|
const event = {
|
|
1210
|
-
metadata,
|
|
2751
|
+
metadata: parsed.metadata,
|
|
2752
|
+
payload: {
|
|
2753
|
+
sourceId: this.config.id,
|
|
2754
|
+
sourceType: WECOM_CLI_ES_TYPE,
|
|
2755
|
+
rawEvent: raw,
|
|
2756
|
+
message: buildWecomUserMessage(parsed.text, parsed.metadata.replyContext),
|
|
2757
|
+
metadata: parsed.metadata,
|
|
2758
|
+
replyChannel,
|
|
2759
|
+
timestamp: Date.now()
|
|
2760
|
+
},
|
|
1211
2761
|
rawEvent: raw,
|
|
1212
2762
|
replyChannel,
|
|
1213
|
-
recommendedAction:
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
2763
|
+
recommendedAction: {
|
|
2764
|
+
description: "Reply to the WeCom user via `wecom-cli message aibot send --json ...`",
|
|
2765
|
+
command: `wecom-cli message aibot send --json '{"chat_id":"${parsed.metadata.chatId ?? parsed.metadata.senderId ?? ""}","msg_type":"markdown","markdown":{"content":"..."}}'`,
|
|
2766
|
+
notes: [
|
|
2767
|
+
"企微单聊的 chat_id 是对方 userid;群聊的 chat_id 是回调里的 chatid。",
|
|
2768
|
+
"发送成功 = 退出码 0 且 stdout 无 error 字段。"
|
|
2769
|
+
]
|
|
2770
|
+
}
|
|
1217
2771
|
};
|
|
1218
2772
|
if (this.eventHandler) {
|
|
1219
2773
|
try {
|
|
1220
2774
|
this.eventHandler(event);
|
|
1221
2775
|
} catch (err) {
|
|
1222
|
-
console.error(`[
|
|
2776
|
+
console.error(`[wecom-cli] event handler threw: ${err}`);
|
|
2777
|
+
notifyReactFailure({
|
|
2778
|
+
error: err,
|
|
2779
|
+
envEvent: event,
|
|
2780
|
+
replyChannel
|
|
2781
|
+
}).catch(() => {});
|
|
1223
2782
|
}
|
|
1224
2783
|
}
|
|
1225
2784
|
}
|
|
1226
2785
|
async stop() {
|
|
1227
2786
|
if (this.status === "stopped" || this.status === "created")
|
|
1228
2787
|
return;
|
|
2788
|
+
this.unloadPlugins();
|
|
2789
|
+
if (this.wsBridge) {
|
|
2790
|
+
unregisterWecomRespondTarget(this.config.id);
|
|
2791
|
+
this.wsBridge.stop();
|
|
2792
|
+
this.wsBridge = undefined;
|
|
2793
|
+
}
|
|
2794
|
+
try {
|
|
2795
|
+
this.fallbackCardStateStore?.close?.();
|
|
2796
|
+
} catch {}
|
|
2797
|
+
this.fallbackCardStateStore = undefined;
|
|
1229
2798
|
if (this.child && !this.child.killed) {
|
|
1230
2799
|
this.child.kill("SIGINT");
|
|
1231
|
-
await new Promise((
|
|
2800
|
+
await new Promise((resolve2) => {
|
|
1232
2801
|
const timer = setTimeout(() => {
|
|
1233
|
-
if (this.child && !this.child.killed)
|
|
2802
|
+
if (this.child && !this.child.killed)
|
|
1234
2803
|
this.child.kill("SIGKILL");
|
|
1235
|
-
|
|
1236
|
-
resolve();
|
|
2804
|
+
resolve2();
|
|
1237
2805
|
}, 3000);
|
|
1238
2806
|
if (this.child) {
|
|
1239
2807
|
this.child.on("exit", () => {
|
|
1240
2808
|
clearTimeout(timer);
|
|
1241
|
-
|
|
2809
|
+
resolve2();
|
|
1242
2810
|
});
|
|
1243
2811
|
} else {
|
|
1244
2812
|
clearTimeout(timer);
|
|
1245
|
-
|
|
2813
|
+
resolve2();
|
|
1246
2814
|
}
|
|
1247
2815
|
});
|
|
1248
2816
|
}
|
|
1249
2817
|
this.setStatus("stopped");
|
|
1250
2818
|
}
|
|
1251
2819
|
}
|
|
1252
|
-
var
|
|
1253
|
-
type:
|
|
2820
|
+
var wecomCliHandler = {
|
|
2821
|
+
type: WECOM_CLI_ES_TYPE,
|
|
1254
2822
|
validateConfig(config) {
|
|
1255
2823
|
const errors = [];
|
|
1256
2824
|
if (!config.id)
|
|
1257
2825
|
errors.push("EventSource ID is required");
|
|
1258
2826
|
if (!config.name)
|
|
1259
2827
|
errors.push("EventSource name is required");
|
|
1260
|
-
const {
|
|
1261
|
-
if (!
|
|
1262
|
-
errors.push("
|
|
2828
|
+
const { botId, secret } = readWecomCliConfig(config);
|
|
2829
|
+
if (!botId) {
|
|
2830
|
+
errors.push("WeCom 'botId' is required (企业微信智能机器人的 BotID;CLI 用 --bot-id 传入)");
|
|
2831
|
+
}
|
|
2832
|
+
if (!secret) {
|
|
2833
|
+
errors.push("WeCom 'secret' is required (智能机器人 Secret;CLI 用 --secret 传入)");
|
|
1263
2834
|
}
|
|
1264
2835
|
return errors;
|
|
1265
2836
|
},
|
|
1266
2837
|
createInstance(config) {
|
|
1267
|
-
return new
|
|
2838
|
+
return new WecomCliInstance(config);
|
|
2839
|
+
},
|
|
2840
|
+
getOptions() {
|
|
2841
|
+
return [
|
|
2842
|
+
{ name: "botId", description: "企业微信智能机器人 BotID", required: true, type: "string" },
|
|
2843
|
+
{ name: "secret", description: "智能机器人 Secret(长连接专用密钥)", required: true, type: "string" },
|
|
2844
|
+
{ name: "webhook", description: "群机器人 webhook URL(可选)", required: false, type: "string" },
|
|
2845
|
+
{ name: "command", description: "覆盖默认启动命令(高级)", required: false, type: "string" }
|
|
2846
|
+
];
|
|
1268
2847
|
}
|
|
1269
2848
|
};
|
|
1270
2849
|
|
|
@@ -1488,7 +3067,13 @@ function writeConfigPath(configPath, dotPath, value) {
|
|
|
1488
3067
|
}
|
|
1489
3068
|
});
|
|
1490
3069
|
const result = applyEdits(text, edits);
|
|
1491
|
-
|
|
3070
|
+
const configDir = path.dirname(configPath);
|
|
3071
|
+
const ok = guardConfigWrite(configDir, path.basename(configPath), () => {
|
|
3072
|
+
fs.writeFileSync(configPath, result, "utf-8");
|
|
3073
|
+
});
|
|
3074
|
+
if (!ok) {
|
|
3075
|
+
throw new Error(`config.jsonc 写入失败(护栏记录见 audit.log):${configPath}`);
|
|
3076
|
+
}
|
|
1492
3077
|
}
|
|
1493
3078
|
function readFallbackModelList(configPath) {
|
|
1494
3079
|
const config = readConfigFile(configPath);
|
|
@@ -1728,12 +3313,12 @@ function resolveSocketPath() {
|
|
|
1728
3313
|
return process.env.ROY_DAEMON_SOCKET || DEFAULT_SOCKET_PATH;
|
|
1729
3314
|
}
|
|
1730
3315
|
async function requestReset(socketPath, reason) {
|
|
1731
|
-
return new Promise((
|
|
3316
|
+
return new Promise((resolve2) => {
|
|
1732
3317
|
let sock;
|
|
1733
3318
|
try {
|
|
1734
3319
|
sock = connect(socketPath);
|
|
1735
3320
|
} catch (e) {
|
|
1736
|
-
|
|
3321
|
+
resolve2({ confirmed: false, reason: e.message });
|
|
1737
3322
|
return;
|
|
1738
3323
|
}
|
|
1739
3324
|
let settled = false;
|
|
@@ -1743,7 +3328,7 @@ async function requestReset(socketPath, reason) {
|
|
|
1743
3328
|
settled = true;
|
|
1744
3329
|
clearTimeout(timer);
|
|
1745
3330
|
sock.destroy();
|
|
1746
|
-
|
|
3331
|
+
resolve2({ confirmed, reason: why });
|
|
1747
3332
|
};
|
|
1748
3333
|
const timer = setTimeout(() => finish(false, `daemon 未在 ${RESET_ACK_TIMEOUT_MS}ms 内确认`), RESET_ACK_TIMEOUT_MS);
|
|
1749
3334
|
sock.once("connect", () => {
|
|
@@ -1799,8 +3384,229 @@ var resetCommand = {
|
|
|
1799
3384
|
}
|
|
1800
3385
|
};
|
|
1801
3386
|
|
|
3387
|
+
// src/env/event-source/es-command.ts
|
|
3388
|
+
import * as fs2 from "fs";
|
|
3389
|
+
import * as path2 from "path";
|
|
3390
|
+
var MOD_ALLOWED_FIELDS = [
|
|
3391
|
+
"enabled",
|
|
3392
|
+
"name",
|
|
3393
|
+
"interval",
|
|
3394
|
+
"cron",
|
|
3395
|
+
"profile"
|
|
3396
|
+
];
|
|
3397
|
+
function resolveEsFilePath(configPath) {
|
|
3398
|
+
const dir = configPath ? path2.dirname(configPath) : defaultRoyDir();
|
|
3399
|
+
return path2.join(dir, "event-sources.json");
|
|
3400
|
+
}
|
|
3401
|
+
function defaultRoyDir() {
|
|
3402
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
|
3403
|
+
return path2.join(home, ".roy-agent");
|
|
3404
|
+
}
|
|
3405
|
+
function readEsFile(esFile) {
|
|
3406
|
+
if (!fs2.existsSync(esFile)) {
|
|
3407
|
+
return { version: "1.0.0", sources: [] };
|
|
3408
|
+
}
|
|
3409
|
+
try {
|
|
3410
|
+
const parsed = JSON.parse(fs2.readFileSync(esFile, "utf-8"));
|
|
3411
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.sources)) {
|
|
3412
|
+
return parsed;
|
|
3413
|
+
}
|
|
3414
|
+
} catch {}
|
|
3415
|
+
throw new Error(`event-sources.json 解析失败:${esFile}`);
|
|
3416
|
+
}
|
|
3417
|
+
function writeEsFile(esFile, data) {
|
|
3418
|
+
fs2.mkdirSync(path2.dirname(esFile), { recursive: true });
|
|
3419
|
+
const tmp = `${esFile}.tmp-${process.pid}-${Date.now()}`;
|
|
3420
|
+
try {
|
|
3421
|
+
fs2.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
3422
|
+
fs2.renameSync(tmp, esFile);
|
|
3423
|
+
} catch (err) {
|
|
3424
|
+
try {
|
|
3425
|
+
fs2.rmSync(tmp, { force: true });
|
|
3426
|
+
} catch {}
|
|
3427
|
+
throw new Error(`写入 ${esFile} 失败:${err.message}`);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
function dualResult2(text) {
|
|
3431
|
+
return {
|
|
3432
|
+
ok: true,
|
|
3433
|
+
output: {
|
|
3434
|
+
text,
|
|
3435
|
+
card: renderTextCard("⚙️ EventSource 管理", text),
|
|
3436
|
+
kind: "card"
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
var USAGE2 = [
|
|
3441
|
+
"用法:##command##: es <子命令> [参数]",
|
|
3442
|
+
"",
|
|
3443
|
+
"子命令:",
|
|
3444
|
+
" list 列出所有事件源",
|
|
3445
|
+
" add <name> <type> 新增事件源(enabled=true)",
|
|
3446
|
+
" del <name|id> 删除事件源(别名 remove)",
|
|
3447
|
+
" mod <name|id> k=v 修改字段(enabled/name/interval/cron/profile;别名 update)",
|
|
3448
|
+
"",
|
|
3449
|
+
" ⚠️ command/url 不可经 mod 修改(避免命令注入面);如需改请 del 后 add。",
|
|
3450
|
+
"",
|
|
3451
|
+
"注意:修改的是磁盘配置,运行中的进程需「##command##: reset」重启后生效。"
|
|
3452
|
+
].join(`
|
|
3453
|
+
`);
|
|
3454
|
+
function findSource(sources, nameOrId) {
|
|
3455
|
+
return sources.find((s) => s.name === nameOrId || s.id === nameOrId);
|
|
3456
|
+
}
|
|
3457
|
+
function parseKv(pair) {
|
|
3458
|
+
const idx = pair.indexOf("=");
|
|
3459
|
+
if (idx <= 0)
|
|
3460
|
+
return null;
|
|
3461
|
+
const key = pair.slice(0, idx).trim();
|
|
3462
|
+
const rawValue = pair.slice(idx + 1).trim();
|
|
3463
|
+
let value = rawValue;
|
|
3464
|
+
if (rawValue === "true")
|
|
3465
|
+
value = true;
|
|
3466
|
+
else if (rawValue === "false")
|
|
3467
|
+
value = false;
|
|
3468
|
+
else if (rawValue !== "" && !Number.isNaN(Number(rawValue))) {
|
|
3469
|
+
value = Number(rawValue);
|
|
3470
|
+
}
|
|
3471
|
+
return { key, value };
|
|
3472
|
+
}
|
|
3473
|
+
function renderList(sources) {
|
|
3474
|
+
if (sources.length === 0) {
|
|
3475
|
+
return `当前没有已配置的事件源。
|
|
3476
|
+
(可用「##command##: es add <name> <type>」新增)`;
|
|
3477
|
+
}
|
|
3478
|
+
const lines = sources.map((s) => {
|
|
3479
|
+
const name = String(s.name ?? "?");
|
|
3480
|
+
const type = String(s.type ?? "?");
|
|
3481
|
+
const enabled = s.enabled === false ? "disabled" : "enabled";
|
|
3482
|
+
const id = String(s.id ?? "");
|
|
3483
|
+
return `- ${name} [${type}] ${enabled} (${id})`;
|
|
3484
|
+
});
|
|
3485
|
+
return `事件源列表(${sources.length} 个):
|
|
3486
|
+
${lines.join(`
|
|
3487
|
+
`)}`;
|
|
3488
|
+
}
|
|
3489
|
+
var esCommand = {
|
|
3490
|
+
name: "es",
|
|
3491
|
+
description: "管理事件源(list/add/del/mod),改磁盘配置",
|
|
3492
|
+
usage: "##command##: es <list|add|del|mod> [参数]",
|
|
3493
|
+
async execute(ctx) {
|
|
3494
|
+
try {
|
|
3495
|
+
return await executeEsCommand(ctx);
|
|
3496
|
+
} catch (err) {
|
|
3497
|
+
return {
|
|
3498
|
+
ok: false,
|
|
3499
|
+
error: `event-sources.json 读写失败:${err.message}`
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
};
|
|
3504
|
+
async function executeEsCommand(ctx) {
|
|
3505
|
+
{
|
|
3506
|
+
const esFile = resolveEsFilePath(ctx.configPath);
|
|
3507
|
+
const [sub, ...rest] = ctx.args;
|
|
3508
|
+
switch (sub) {
|
|
3509
|
+
case "list":
|
|
3510
|
+
case undefined: {
|
|
3511
|
+
const data = readEsFile(esFile);
|
|
3512
|
+
return dualResult2(renderList(data.sources));
|
|
3513
|
+
}
|
|
3514
|
+
case "add": {
|
|
3515
|
+
const [name, type] = rest;
|
|
3516
|
+
if (!name || !type) {
|
|
3517
|
+
return { ok: false, error: `用法:##command##: es add <name> <type>
|
|
3518
|
+
${USAGE2}` };
|
|
3519
|
+
}
|
|
3520
|
+
const data = readEsFile(esFile);
|
|
3521
|
+
if (findSource(data.sources, name)) {
|
|
3522
|
+
return {
|
|
3523
|
+
ok: false,
|
|
3524
|
+
error: `事件源「${name}」已存在(add 拒绝重名)。可用 es mod 修改或 es del 删除后重建。`
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
const id = `es-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3528
|
+
const source = {
|
|
3529
|
+
id,
|
|
3530
|
+
name,
|
|
3531
|
+
type,
|
|
3532
|
+
enabled: true
|
|
3533
|
+
};
|
|
3534
|
+
data.sources.push(source);
|
|
3535
|
+
writeEsFile(esFile, data);
|
|
3536
|
+
return dualResult2(`✅ 已新增事件源「${name}」[${type}](id=${id},enabled)。` + `重启进程后生效(##command##: reset)。`);
|
|
3537
|
+
}
|
|
3538
|
+
case "del":
|
|
3539
|
+
case "remove": {
|
|
3540
|
+
const target = rest[0];
|
|
3541
|
+
if (!target) {
|
|
3542
|
+
return { ok: false, error: `用法:##command##: es del <name|id>
|
|
3543
|
+
${USAGE2}` };
|
|
3544
|
+
}
|
|
3545
|
+
const data = readEsFile(esFile);
|
|
3546
|
+
const src = findSource(data.sources, target);
|
|
3547
|
+
if (!src) {
|
|
3548
|
+
return { ok: false, error: `未找到事件源「${target}」。` };
|
|
3549
|
+
}
|
|
3550
|
+
data.sources = data.sources.filter((s) => s !== src);
|
|
3551
|
+
writeEsFile(esFile, data);
|
|
3552
|
+
return dualResult2(`✅ 已删除事件源「${String(src.name)}」(id=${String(src.id)})。` + `重启进程后生效(##command##: reset)。`);
|
|
3553
|
+
}
|
|
3554
|
+
case "mod":
|
|
3555
|
+
case "update": {
|
|
3556
|
+
const [target, ...pairs] = rest;
|
|
3557
|
+
if (!target || pairs.length === 0) {
|
|
3558
|
+
return {
|
|
3559
|
+
ok: false,
|
|
3560
|
+
error: `用法:##command##: es mod <name|id> k=v [k=v...]
|
|
3561
|
+
${USAGE2}`
|
|
3562
|
+
};
|
|
3563
|
+
}
|
|
3564
|
+
const data = readEsFile(esFile);
|
|
3565
|
+
const src = findSource(data.sources, target);
|
|
3566
|
+
if (!src) {
|
|
3567
|
+
return { ok: false, error: `未找到事件源「${target}」。` };
|
|
3568
|
+
}
|
|
3569
|
+
const applied = [];
|
|
3570
|
+
for (const pair of pairs) {
|
|
3571
|
+
const kv = parseKv(pair);
|
|
3572
|
+
if (!kv) {
|
|
3573
|
+
return { ok: false, error: `参数格式错误:「${pair}」(应为 k=v)
|
|
3574
|
+
${USAGE2}` };
|
|
3575
|
+
}
|
|
3576
|
+
if (!MOD_ALLOWED_FIELDS.includes(kv.key)) {
|
|
3577
|
+
return {
|
|
3578
|
+
ok: false,
|
|
3579
|
+
error: `字段「${kv.key}」不允许修改(白名单:${MOD_ALLOWED_FIELDS.join("/")};id/type 不可改)。`
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
if (kv.key === "name" && typeof kv.value === "string" && kv.value !== src.name && data.sources.some((s0) => s0 !== src && s0.name === kv.value)) {
|
|
3583
|
+
return {
|
|
3584
|
+
ok: false,
|
|
3585
|
+
error: `名称「${String(kv.value)}」已被其它事件源占用,改 name 拒绝。`
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
let value = kv.value;
|
|
3589
|
+
if (kv.key === "enabled") {
|
|
3590
|
+
value = kv.value === true || kv.value === 1 || kv.value === "1" || kv.value === "true" || kv.value === "yes" || kv.value === "on";
|
|
3591
|
+
}
|
|
3592
|
+
src[kv.key] = value;
|
|
3593
|
+
applied.push(`${kv.key}=${String(value)}`);
|
|
3594
|
+
}
|
|
3595
|
+
writeEsFile(esFile, data);
|
|
3596
|
+
return dualResult2(`✅ 已修改事件源「${String(src.name)}」:${applied.join(",")}。` + `重启进程后生效(##command##: reset)。`);
|
|
3597
|
+
}
|
|
3598
|
+
default:
|
|
3599
|
+
return {
|
|
3600
|
+
ok: false,
|
|
3601
|
+
error: `未知子命令「${sub}」。
|
|
3602
|
+
${USAGE2}`
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
|
|
1802
3608
|
// src/env/event-source/command-registry.ts
|
|
1803
|
-
var
|
|
3609
|
+
var logger3 = createLogger("command-registry");
|
|
1804
3610
|
var COMMAND_PREFIX = "##command##:";
|
|
1805
3611
|
var COMMAND_PREFIX_LOWER = COMMAND_PREFIX.toLowerCase();
|
|
1806
3612
|
function stripLeadingMentions(text) {
|
|
@@ -1873,7 +3679,7 @@ ${this.renderHelp()}`
|
|
|
1873
3679
|
});
|
|
1874
3680
|
} catch (err) {
|
|
1875
3681
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1876
|
-
|
|
3682
|
+
logger3.error(`[CommandRegistry] command "${subcommand}" failed: ${msg}`);
|
|
1877
3683
|
return { ok: false, error: `命令执行失败:${msg}` };
|
|
1878
3684
|
}
|
|
1879
3685
|
}
|
|
@@ -1892,6 +3698,7 @@ function getGlobalCommandRegistry() {
|
|
|
1892
3698
|
registry.register(fallbackModelCommand);
|
|
1893
3699
|
registry.register(listCommand);
|
|
1894
3700
|
registry.register(resetCommand);
|
|
3701
|
+
registry.register(esCommand);
|
|
1895
3702
|
g.__royCommandRegistry = registry;
|
|
1896
3703
|
}
|
|
1897
3704
|
return g.__royCommandRegistry;
|
|
@@ -1903,7 +3710,7 @@ async function handleCommandMessage(text, opts = {}) {
|
|
|
1903
3710
|
if (!invocation)
|
|
1904
3711
|
return null;
|
|
1905
3712
|
const registry = opts.registry ?? getGlobalCommandRegistry();
|
|
1906
|
-
|
|
3713
|
+
logger3.info(`[handleCommandMessage] dispatching "${invocation.subcommand}" args=${JSON.stringify(invocation.args)}`);
|
|
1907
3714
|
return registry.dispatch(invocation.subcommand, invocation.args, {
|
|
1908
3715
|
rawText: text,
|
|
1909
3716
|
configPath: opts.configPath
|
|
@@ -1912,7 +3719,7 @@ async function handleCommandMessage(text, opts = {}) {
|
|
|
1912
3719
|
|
|
1913
3720
|
// src/env/event-source/command-card-action.ts
|
|
1914
3721
|
init_logger();
|
|
1915
|
-
var
|
|
3722
|
+
var logger4 = createLogger("command-card-action");
|
|
1916
3723
|
function isCommandCardAction(event) {
|
|
1917
3724
|
const av = event.actionValue;
|
|
1918
3725
|
if (av && typeof av === "object") {
|
|
@@ -1969,7 +3776,7 @@ async function handleCommandCardAction(event, ports) {
|
|
|
1969
3776
|
return { ok: false, error: "无法识别的 command 卡片动作" };
|
|
1970
3777
|
}
|
|
1971
3778
|
const executed = `${call.subcommand} ${call.args.join(" ")}`;
|
|
1972
|
-
|
|
3779
|
+
logger4.info(`[handleCommandCardAction] 执行 ${executed}`);
|
|
1973
3780
|
let result;
|
|
1974
3781
|
try {
|
|
1975
3782
|
result = await ports.executeCommand(call.subcommand, call.args, {
|
|
@@ -1977,7 +3784,7 @@ async function handleCommandCardAction(event, ports) {
|
|
|
1977
3784
|
});
|
|
1978
3785
|
} catch (err) {
|
|
1979
3786
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1980
|
-
|
|
3787
|
+
logger4.error(`[handleCommandCardAction] ${executed} 执行异常:${msg}`);
|
|
1981
3788
|
return { ok: false, executed, error: `命令执行异常:${msg}` };
|
|
1982
3789
|
}
|
|
1983
3790
|
if (!result.ok) {
|
|
@@ -1990,7 +3797,7 @@ async function handleCommandCardAction(event, ports) {
|
|
|
1990
3797
|
await ports.patchCard(event.token, card, event.profile);
|
|
1991
3798
|
} catch (err) {
|
|
1992
3799
|
patchError = err.message;
|
|
1993
|
-
|
|
3800
|
+
logger4.error(`[handleCommandCardAction] patch 卡片失败:${patchError}`);
|
|
1994
3801
|
}
|
|
1995
3802
|
}
|
|
1996
3803
|
return { ok: true, executed, card, patchError };
|
|
@@ -1999,7 +3806,7 @@ async function handleCommandCardAction(event, ports) {
|
|
|
1999
3806
|
// src/env/event-source/event-source-handlers.ts
|
|
2000
3807
|
init_logger();
|
|
2001
3808
|
var execAsync2 = promisify2(execSync);
|
|
2002
|
-
var
|
|
3809
|
+
var logger5 = createLogger("lark-cli:event-source");
|
|
2003
3810
|
var CHILD_READY_TIMEOUT_MS = 5000;
|
|
2004
3811
|
var GRACEFUL_STOP_TIMEOUT_MS = 5000;
|
|
2005
3812
|
var CARD_ACTION_EVENT_TYPE = "card.action.trigger";
|
|
@@ -2009,12 +3816,6 @@ function isChildAlive(child) {
|
|
|
2009
3816
|
function isPluginDebugEnabled() {
|
|
2010
3817
|
return process.env.ROY_TASK_NOTIFY_DEBUG === "1" || process.env.ROY_TRACE_DEBUG === "1";
|
|
2011
3818
|
}
|
|
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
3819
|
function matchEventType(eventType, patterns) {
|
|
2019
3820
|
for (const pattern of patterns) {
|
|
2020
3821
|
if (pattern === "*")
|
|
@@ -2040,6 +3841,40 @@ function buildLarkRecommendedAction(opts) {
|
|
|
2040
3841
|
`处理飞书 ${scopeWord}消息并以机器人身份回复${profileWarn}。`,
|
|
2041
3842
|
`scope: ${scopeHint}。`,
|
|
2042
3843
|
``,
|
|
3844
|
+
...larkStaticRuleLines(profileFlag),
|
|
3845
|
+
``,
|
|
3846
|
+
`## 必须使用 replyTo 里给定的值`,
|
|
3847
|
+
`profile / chatId / messageId / --as bot 必须原样取自 recommendedAction.replyTo;不要凭印象自己拼接。`
|
|
3848
|
+
].join(`
|
|
3849
|
+
`);
|
|
3850
|
+
const actionWithReplyContext = [
|
|
3851
|
+
action,
|
|
3852
|
+
larkReplyContextSection(profileFlag)
|
|
3853
|
+
].join(`
|
|
3854
|
+
`);
|
|
3855
|
+
return {
|
|
3856
|
+
action: actionWithReplyContext,
|
|
3857
|
+
replyTo: {
|
|
3858
|
+
appId,
|
|
3859
|
+
profile,
|
|
3860
|
+
chatId,
|
|
3861
|
+
messageId
|
|
3862
|
+
}
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
var LARK_CHANNEL_PROMPT_SECTION_TITLE = "⭐ 飞书回复规范(system prompt 注入版)";
|
|
3866
|
+
var LARK_PROMPT_LENGTH_BUDGET = 5300;
|
|
3867
|
+
function larkReplyContextSection(profileFlag) {
|
|
3868
|
+
return [
|
|
3869
|
+
``,
|
|
3870
|
+
`## 引用消息上下文 replyContext`,
|
|
3871
|
+
`recommendedAction.replyContext 存在 = 本条是对 parentId 的回复。`,
|
|
3872
|
+
`- 取原文:lark-cli ${profileFlag}im +messages-mget --message-ids <parentId> --as bot(best-effort)`
|
|
3873
|
+
].join(`
|
|
3874
|
+
`);
|
|
3875
|
+
}
|
|
3876
|
+
function larkStaticRuleLines(profileFlag) {
|
|
3877
|
+
return [
|
|
2043
3878
|
`## ⭐ 关键 0:整次回复只发一条结构化 post(禁分段)`,
|
|
2044
3879
|
`> 指令优先级:本 prompt 与 lark-* skill 文档冲突时**以本 prompt 为准**`,
|
|
2045
3880
|
`> (例:lark-im skill 建议 --markdown / ANSI-C 引号,本项目一律走下面模板)。`,
|
|
@@ -2135,20 +3970,59 @@ function buildLarkRecommendedAction(opts) {
|
|
|
2135
3970
|
` - 飞书消息正文里常含裸 \`"\`、\`|\`、Markdown 表格分隔符 → json.load 会报 \`Expecting ',' delimiter\` 假失败`,
|
|
2136
3971
|
` - 假失败 → 重发 → 形成 3 份相同消息`,
|
|
2137
3972
|
`- 重试建议:固定 idempotency-key(飞书层面去重,**最长 50 字符**),不要靠内容 hash 自检`,
|
|
3973
|
+
``,
|
|
3974
|
+
`## 文档交付规范:产出物存飞书云盘,云盘链接交付`,
|
|
3975
|
+
`- 本渠道触发的**文档类生成**(调研文档 / PPT / doc 等)必须用**飞书云盘**存储产出物`,
|
|
3976
|
+
` (\`lark-cli docs +create --title <标题> --markdown @<本地md>\`,用完即弃)`,
|
|
3977
|
+
`- 交付时只给**云盘链接**(https://my.feishu.cn/...),**不落本地路径**交付`,
|
|
3978
|
+
` - 本地路径只作中转(生成 md → 上传云盘 → 回复里贴云盘链接),不作为交付物`
|
|
3979
|
+
];
|
|
3980
|
+
}
|
|
3981
|
+
function buildLarkChannelSystemPrompt(opts) {
|
|
3982
|
+
const { profile } = opts;
|
|
3983
|
+
const profileFlag = profile ? `--profile ${profile} ` : "";
|
|
3984
|
+
const profileWarn = profile ? `(⚠️ 必须用 profile="${profile}" 调 lark-cli;openId per-app 隔离,换 profile 会失败)` : "";
|
|
3985
|
+
const header = [
|
|
3986
|
+
`# ${LARK_CHANNEL_PROMPT_SECTION_TITLE}`,
|
|
3987
|
+
``,
|
|
3988
|
+
`本节是飞书渠道的**回复规范**(静态规则,随 system prompt 注入)。`,
|
|
3989
|
+
`每条飞书事件的 \`recommendedAction\` 只带动态定位(replyTo 四元组),`,
|
|
3990
|
+
`profile / chatId / messageId 是本轮唯一权威定位,其余回复规则全部在此。`,
|
|
3991
|
+
profile ? `本渠道 profile:${profile}${profileWarn}` : `本渠道未配置 profile(使用 lark-cli active profile)。`,
|
|
3992
|
+
``
|
|
3993
|
+
].join(`
|
|
3994
|
+
`);
|
|
3995
|
+
const footer = [
|
|
2138
3996
|
``,
|
|
2139
3997
|
`## 必须使用 replyTo 里给定的值`,
|
|
2140
|
-
`profile / chatId / messageId / --as bot 必须原样取自 recommendedAction.replyTo
|
|
3998
|
+
`profile / chatId / messageId / --as bot 必须原样取自 recommendedAction.replyTo;不要凭印象自己拼接。`,
|
|
3999
|
+
larkReplyContextSection(profileFlag)
|
|
2141
4000
|
].join(`
|
|
2142
4001
|
`);
|
|
2143
|
-
return
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
|
|
4002
|
+
return header + larkStaticRuleLines(profileFlag).join(`
|
|
4003
|
+
`) + footer;
|
|
4004
|
+
}
|
|
4005
|
+
function buildLarkLeanAction(chatType) {
|
|
4006
|
+
const isGroup = chatType === "group";
|
|
4007
|
+
const scopeHint = isGroup ? `群聊消息;回复到 replyTo.chatId。如需 @发件人,用结构化 at 节点 {"tag":"at","user_id":"<senderId>"}(不要写 <at> 文本标签)` : "p2p 私聊消息;回复到 replyTo.chatId";
|
|
4008
|
+
return [
|
|
4009
|
+
`处理飞书${isGroup ? "群聊" : "p2p 私聊"}消息并回复。回复规范见 system prompt「${LARK_CHANNEL_PROMPT_SECTION_TITLE}」章节。`,
|
|
4010
|
+
`scope: ${scopeHint}。`,
|
|
4011
|
+
`replyTo 的 profile/chatId/messageId 是本轮唯一权威定位;其余回复规则(发送命令模板、长度上限、发送纪律等)全部见 system prompt 上述章节。`
|
|
4012
|
+
].join(`
|
|
4013
|
+
`);
|
|
4014
|
+
}
|
|
4015
|
+
function resolveLarkChannelSystemPrompt(config) {
|
|
4016
|
+
const mode = config.channelPromptMode ?? "lean";
|
|
4017
|
+
if (mode === "legacy") {
|
|
4018
|
+
return config.systemPrompt;
|
|
4019
|
+
}
|
|
4020
|
+
const channelPrompt = buildLarkChannelSystemPrompt({
|
|
4021
|
+
profile: config.profile
|
|
4022
|
+
});
|
|
4023
|
+
return config.systemPrompt ? config.systemPrompt + `
|
|
4024
|
+
|
|
4025
|
+
` + channelPrompt : channelPrompt;
|
|
2152
4026
|
}
|
|
2153
4027
|
function extractMetadata(rawEvent, eventType) {
|
|
2154
4028
|
const event = rawEvent;
|
|
@@ -2184,6 +4058,19 @@ function extractMetadata(rawEvent, eventType) {
|
|
|
2184
4058
|
metadata.chatType = messageData.chat_type;
|
|
2185
4059
|
metadata.messageId = messageData.message_id;
|
|
2186
4060
|
metadata.messageType = messageData.message_type;
|
|
4061
|
+
const rootId = messageData.root_id ?? event.root_id;
|
|
4062
|
+
const parentId = messageData.parent_id ?? event.reply_to ?? event.parent_id;
|
|
4063
|
+
const threadId = messageData.thread_id ?? event.thread_id;
|
|
4064
|
+
if (rootId || parentId || threadId) {
|
|
4065
|
+
const replyContext = {};
|
|
4066
|
+
if (rootId)
|
|
4067
|
+
replyContext.rootId = rootId;
|
|
4068
|
+
if (parentId)
|
|
4069
|
+
replyContext.parentId = parentId;
|
|
4070
|
+
if (threadId)
|
|
4071
|
+
replyContext.threadId = threadId;
|
|
4072
|
+
metadata.replyContext = replyContext;
|
|
4073
|
+
}
|
|
2187
4074
|
}
|
|
2188
4075
|
if (typeof senderId === "string") {
|
|
2189
4076
|
metadata.senderId = senderId;
|
|
@@ -2193,13 +4080,33 @@ function extractMetadata(rawEvent, eventType) {
|
|
|
2193
4080
|
let recommendedAction;
|
|
2194
4081
|
if (eventType === "im.message.receive_v1" || event.type === "im.message.receive_v1") {
|
|
2195
4082
|
const profile2 = event.profile;
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
4083
|
+
const promptMode = event.channelPromptMode ?? "lean";
|
|
4084
|
+
if (promptMode === "legacy") {
|
|
4085
|
+
recommendedAction = buildLarkRecommendedAction({
|
|
4086
|
+
profile: profile2,
|
|
4087
|
+
chatType: metadata.chatType,
|
|
4088
|
+
appId: metadata.appId,
|
|
4089
|
+
chatId: metadata.chatId,
|
|
4090
|
+
messageId: metadata.messageId
|
|
4091
|
+
});
|
|
4092
|
+
} else {
|
|
4093
|
+
recommendedAction = {
|
|
4094
|
+
action: buildLarkLeanAction(metadata.chatType),
|
|
4095
|
+
replyTo: {
|
|
4096
|
+
appId: metadata.appId,
|
|
4097
|
+
profile: profile2,
|
|
4098
|
+
chatId: metadata.chatId,
|
|
4099
|
+
messageId: metadata.messageId
|
|
4100
|
+
},
|
|
4101
|
+
staticRulesInSystemPrompt: true
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
if (metadata.replyContext) {
|
|
4105
|
+
recommendedAction = {
|
|
4106
|
+
...recommendedAction,
|
|
4107
|
+
replyContext: metadata.replyContext
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
2203
4110
|
}
|
|
2204
4111
|
const profile = event.profile;
|
|
2205
4112
|
const senderOpenId = typeof senderId === "string" ? senderId : senderId?.open_id;
|
|
@@ -2228,8 +4135,8 @@ class LarkCliInstance {
|
|
|
2228
4135
|
cardActionPorts;
|
|
2229
4136
|
setCardActionPorts(ports) {
|
|
2230
4137
|
this.cardActionPorts = ports;
|
|
2231
|
-
if (typeof
|
|
2232
|
-
|
|
4138
|
+
if (typeof logger5 !== "undefined" && logger5?.debug) {
|
|
4139
|
+
logger5.debug(`[LarkCliInstance] CardActionPorts injected: ` + `getTask=${typeof ports.getTask}, updateTaskContext=${typeof ports.updateTaskContext}, ` + `createGroupChat=${typeof ports.createGroupChat}, deleteGroupChat=${typeof ports.deleteGroupChat}, ` + `notifier=${typeof ports.notifier}`);
|
|
2233
4140
|
}
|
|
2234
4141
|
}
|
|
2235
4142
|
hasCardActionPorts() {
|
|
@@ -2244,6 +4151,15 @@ class LarkCliInstance {
|
|
|
2244
4151
|
recentEventIds = new Map;
|
|
2245
4152
|
DEDUP_WINDOW_MS = 60000;
|
|
2246
4153
|
DEDUP_MAX_SIZE = 100;
|
|
4154
|
+
cachedChannelSystemPrompt;
|
|
4155
|
+
channelSystemPromptResolved = false;
|
|
4156
|
+
getChannelSystemPrompt() {
|
|
4157
|
+
if (!this.channelSystemPromptResolved) {
|
|
4158
|
+
this.cachedChannelSystemPrompt = resolveLarkChannelSystemPrompt(this.config);
|
|
4159
|
+
this.channelSystemPromptResolved = true;
|
|
4160
|
+
}
|
|
4161
|
+
return this.cachedChannelSystemPrompt;
|
|
4162
|
+
}
|
|
2247
4163
|
environment;
|
|
2248
4164
|
loadedPlugins = [];
|
|
2249
4165
|
constructor(config) {
|
|
@@ -2256,7 +4172,7 @@ class LarkCliInstance {
|
|
|
2256
4172
|
if (this.status === "running")
|
|
2257
4173
|
return;
|
|
2258
4174
|
if (this.starting) {
|
|
2259
|
-
|
|
4175
|
+
logger5.warn(`[LarkCliInstance] start() 已在执行中,忽略重复调用`);
|
|
2260
4176
|
return;
|
|
2261
4177
|
}
|
|
2262
4178
|
this.starting = true;
|
|
@@ -2286,12 +4202,12 @@ class LarkCliInstance {
|
|
|
2286
4202
|
if (!consumesCardAction)
|
|
2287
4203
|
return;
|
|
2288
4204
|
if (this.config.eventTypes?.length && !matchEventType(CARD_ACTION_EVENT_TYPE, this.config.eventTypes)) {
|
|
2289
|
-
|
|
4205
|
+
logger5.warn(`[LarkCliInstance] eventKeys 含 ${CARD_ACTION_EVENT_TYPE},但 eventTypes=` + `${JSON.stringify(this.config.eventTypes)} 不含它 → 卡片点击会被**静默丢弃**。` + `请把 ${CARD_ACTION_EVENT_TYPE} 加进 eventTypes,或清空 eventTypes。`);
|
|
2290
4206
|
}
|
|
2291
|
-
|
|
4207
|
+
logger5.info(`[LarkCliInstance] 已监听 ${CARD_ACTION_EVENT_TYPE}。` + `⚠️ 若点击卡片无反应,先确认飞书开发者后台「事件与回调 → 回调配置」已开启该事件。`);
|
|
2292
4208
|
}
|
|
2293
4209
|
spawnConsumeChild(command) {
|
|
2294
|
-
return
|
|
4210
|
+
return spawn3("exec " + command, [], {
|
|
2295
4211
|
shell: true,
|
|
2296
4212
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2297
4213
|
windowsHide: true
|
|
@@ -2328,7 +4244,7 @@ class LarkCliInstance {
|
|
|
2328
4244
|
const conflicts = keys.filter((k) => busy.has(k));
|
|
2329
4245
|
if (conflicts.length > 0) {
|
|
2330
4246
|
const profile = this.config.profile;
|
|
2331
|
-
|
|
4247
|
+
logger5.warn(`[LarkCliInstance] eventKey 已被其它 consumer 占用(lark-cli 一个 key 只允许一个 consumer):` + `${conflicts.join(", ")}。本 event-source 启动后可能 exit 2。` + `用 \`lark-cli${profile ? ` --profile ${profile}` : ""} event status\` 查看,必要时 \`event stop\`。`);
|
|
2332
4248
|
}
|
|
2333
4249
|
}
|
|
2334
4250
|
async probeBusyEventKeys() {
|
|
@@ -2379,16 +4295,16 @@ class LarkCliInstance {
|
|
|
2379
4295
|
});
|
|
2380
4296
|
const handleExit = (code, source) => {
|
|
2381
4297
|
if (code !== 0 && code !== null) {
|
|
2382
|
-
|
|
4298
|
+
logger5.warn(`[LarkCliInstance] consume 子进程异常退出 code=${code} (${source}) cmd="${command}"`);
|
|
2383
4299
|
this.status = "error";
|
|
2384
4300
|
} else {
|
|
2385
|
-
|
|
4301
|
+
logger5.info(`[LarkCliInstance] consume 子进程已退出 code=${code} (${source}) cmd="${command}"`);
|
|
2386
4302
|
}
|
|
2387
4303
|
settle();
|
|
2388
4304
|
queueMicrotask(() => {
|
|
2389
4305
|
if (this.children.length > 0 && this.children.every((c) => !isChildAlive(c))) {
|
|
2390
4306
|
if (this.status !== "error") {
|
|
2391
|
-
|
|
4307
|
+
logger5.warn(`[LarkCliInstance] 所有 consume 子进程都已退出,状态从 running 改为 stopped`);
|
|
2392
4308
|
this.status = "stopped";
|
|
2393
4309
|
}
|
|
2394
4310
|
}
|
|
@@ -2397,12 +4313,12 @@ class LarkCliInstance {
|
|
|
2397
4313
|
child.on("exit", (code) => handleExit(code, "exit"));
|
|
2398
4314
|
child.on("close", (code) => handleExit(code, "close"));
|
|
2399
4315
|
child.on("error", (error) => {
|
|
2400
|
-
|
|
4316
|
+
logger5.error(`[LarkCliInstance] consume 子进程 error: ${error.message}`);
|
|
2401
4317
|
this.status = "error";
|
|
2402
4318
|
settle();
|
|
2403
4319
|
});
|
|
2404
4320
|
const timer = setTimeout(() => {
|
|
2405
|
-
|
|
4321
|
+
logger5.warn(`[LarkCliInstance] consume 子进程 ${CHILD_READY_TIMEOUT_MS}ms 内未见 ready marker: cmd="${command}"`);
|
|
2406
4322
|
settle();
|
|
2407
4323
|
}, CHILD_READY_TIMEOUT_MS);
|
|
2408
4324
|
ready.then(() => clearTimeout(timer));
|
|
@@ -2425,16 +4341,16 @@ class LarkCliInstance {
|
|
|
2425
4341
|
try {
|
|
2426
4342
|
child.kill("SIGTERM");
|
|
2427
4343
|
} catch (error) {
|
|
2428
|
-
|
|
4344
|
+
logger5.warn(`[LarkCliInstance] Error sending SIGTERM to ${child.pid}: ${error}`);
|
|
2429
4345
|
}
|
|
2430
4346
|
}
|
|
2431
|
-
const allExited = Promise.all(children.map((child) => new Promise((
|
|
4347
|
+
const allExited = Promise.all(children.map((child) => new Promise((resolve2) => {
|
|
2432
4348
|
if (isChildAlive(child) === false)
|
|
2433
|
-
return
|
|
2434
|
-
child.once("exit", () =>
|
|
2435
|
-
child.once("error", () =>
|
|
4349
|
+
return resolve2();
|
|
4350
|
+
child.once("exit", () => resolve2());
|
|
4351
|
+
child.once("error", () => resolve2());
|
|
2436
4352
|
})));
|
|
2437
|
-
const timeout = new Promise((
|
|
4353
|
+
const timeout = new Promise((resolve2) => setTimeout(resolve2, GRACEFUL_STOP_TIMEOUT_MS));
|
|
2438
4354
|
await Promise.race([allExited, timeout]);
|
|
2439
4355
|
for (const child of children) {
|
|
2440
4356
|
if (isChildAlive(child)) {
|
|
@@ -2480,12 +4396,12 @@ class LarkCliInstance {
|
|
|
2480
4396
|
}
|
|
2481
4397
|
wireCardActionPorts() {
|
|
2482
4398
|
if (this.cardActionPorts) {
|
|
2483
|
-
|
|
4399
|
+
logger5.debug("[LarkCliInstance] cardActionPorts 已由外部注入,跳过默认 cross-wire");
|
|
2484
4400
|
return;
|
|
2485
4401
|
}
|
|
2486
4402
|
const taskComponent = this.environment?.getComponent?.("task");
|
|
2487
4403
|
if (!taskComponent?.getTask || !taskComponent?.updateTask) {
|
|
2488
|
-
|
|
4404
|
+
logger5.debug(`[LarkCliInstance] card.action.trigger 端口未注入:taskComponent 不可用` + `(getTask=${typeof taskComponent?.getTask}, updateTask=${typeof taskComponent?.updateTask})。` + `卡片按钮点击会被跳过。`);
|
|
2489
4405
|
return;
|
|
2490
4406
|
}
|
|
2491
4407
|
const sharedNotifier = this.pluginInstances.get("LarkCliTaskNotifyPlugin")?.cardNotifier ?? this.pluginInstances.get("PerTaskGroupChatPlugin")?.cardNotifier;
|
|
@@ -2514,7 +4430,7 @@ class LarkCliInstance {
|
|
|
2514
4430
|
deleteGroupChat: (chatId, profile) => deleteGroupChatViaLarkCli(chatId, profile),
|
|
2515
4431
|
notifier: sharedNotifier ?? new LarkCardPatchNotifier
|
|
2516
4432
|
});
|
|
2517
|
-
|
|
4433
|
+
logger5.info(`[LarkCliInstance] card.action.trigger 端口已注入` + `(notifier ${sharedNotifier ? "复用插件共享实例" : "新建实例"})`);
|
|
2518
4434
|
}
|
|
2519
4435
|
getPluginHookPoints(plugin) {
|
|
2520
4436
|
if (Array.isArray(plugin.hooks)) {
|
|
@@ -2712,7 +4628,7 @@ class LarkCliInstance {
|
|
|
2712
4628
|
}
|
|
2713
4629
|
return null;
|
|
2714
4630
|
} catch (err) {
|
|
2715
|
-
|
|
4631
|
+
logger5.warn(`[LarkCliInstance] resolveChatTaskBinding failed for chatId=${chatId}: ${err}. ` + `群消息将不会注入 parentTaskId。`);
|
|
2716
4632
|
return null;
|
|
2717
4633
|
}
|
|
2718
4634
|
}
|
|
@@ -2734,7 +4650,7 @@ class LarkCliInstance {
|
|
|
2734
4650
|
processStreamWith(data, buffer, opts = {}) {
|
|
2735
4651
|
buffer.value += data;
|
|
2736
4652
|
if (buffer.value.length > 2 * 1024 * 1024) {
|
|
2737
|
-
|
|
4653
|
+
logger5.warn(`[LarkCliInstance] stream 行缓冲超过 2MB 仍无换行,丢弃(疑似非 NDJSON 输出)`);
|
|
2738
4654
|
buffer.value = "";
|
|
2739
4655
|
return;
|
|
2740
4656
|
}
|
|
@@ -2770,12 +4686,12 @@ class LarkCliInstance {
|
|
|
2770
4686
|
const now = Date.now();
|
|
2771
4687
|
const lastShared = LarkCliInstance.SHARED_SEEN.get(dedupKey);
|
|
2772
4688
|
if (lastShared !== undefined && now - lastShared < LarkCliInstance.SHARED_MSG_TTL_MS) {
|
|
2773
|
-
|
|
4689
|
+
logger5.warn(`[LarkCliInstance] duplicate lark-cli event suppressed across instances (event_id=${dedupKey}, ` + `lastSeen=${now - lastShared}ms ago). ` + `Task #4137: SHARED_SEEN 跨实例去重。`);
|
|
2774
4690
|
return;
|
|
2775
4691
|
}
|
|
2776
4692
|
const last = this.recentEventIds.get(dedupKey);
|
|
2777
4693
|
if (last !== undefined && now - last < this.DEDUP_WINDOW_MS) {
|
|
2778
|
-
|
|
4694
|
+
logger5.warn(`[LarkCliInstance] duplicate lark-cli event suppressed (event_id=${dedupKey}, ` + `lastSeen=${now - last}ms ago). ` + `Task #3977/3978: 防止同一飞书事件触发两次任务创建。`);
|
|
2779
4695
|
return;
|
|
2780
4696
|
}
|
|
2781
4697
|
LarkCliInstance.SHARED_SEEN.set(dedupKey, now);
|
|
@@ -2833,9 +4749,14 @@ class LarkCliInstance {
|
|
|
2833
4749
|
const enrichedEvent = {
|
|
2834
4750
|
...rawEvent,
|
|
2835
4751
|
profile: this.config.profile,
|
|
2836
|
-
eventSourceId: this.config.id
|
|
4752
|
+
eventSourceId: this.config.id,
|
|
4753
|
+
channelPromptMode: this.config.channelPromptMode ?? "lean"
|
|
2837
4754
|
};
|
|
2838
4755
|
const { metadata, replyChannel, recommendedAction } = extractMetadata(enrichedEvent, eventType);
|
|
4756
|
+
const channelSystemPrompt = this.getChannelSystemPrompt();
|
|
4757
|
+
if (channelSystemPrompt !== undefined) {
|
|
4758
|
+
metadata.systemPrompt = channelSystemPrompt;
|
|
4759
|
+
}
|
|
2839
4760
|
failureReplyChannel = replyChannel;
|
|
2840
4761
|
const commandText = isCardAction ? undefined : extractCommandText(enrichedEvent);
|
|
2841
4762
|
const parentTaskId = isCardAction ? null : await this.resolveChatTaskBinding(metadata?.chatId);
|
|
@@ -2888,6 +4809,7 @@ class LarkCliInstance {
|
|
|
2888
4809
|
pluginEnabled,
|
|
2889
4810
|
replyChannel,
|
|
2890
4811
|
agent,
|
|
4812
|
+
...channelSystemPrompt !== undefined ? { systemPrompt: channelSystemPrompt } : {},
|
|
2891
4813
|
parentTaskId: parentTaskId ?? undefined,
|
|
2892
4814
|
...triggerEventId ? { triggerEventId } : {},
|
|
2893
4815
|
traceContext: eventTraceContext
|
|
@@ -2964,11 +4886,11 @@ class LarkCliInstance {
|
|
|
2964
4886
|
const chatId = event.chat_id || "";
|
|
2965
4887
|
const operatorId = event.operator_id?.open_id ?? event.operator_id ?? undefined;
|
|
2966
4888
|
if (!token || !messageId || !chatId) {
|
|
2967
|
-
|
|
4889
|
+
logger5.warn(`[LarkCliInstance] handleCardActionEvent: missing token/messageId/chatId, skip`);
|
|
2968
4890
|
return;
|
|
2969
4891
|
}
|
|
2970
4892
|
if (!this.cardActionPorts) {
|
|
2971
|
-
|
|
4893
|
+
logger5.warn(`[LarkCliInstance] handleCardActionEvent: cardActionPorts 未注入,` + `card.action.trigger 事件被跳过。` + `调 instance.setCardActionPorts({getTask, updateTaskContext, createGroupChat, deleteGroupChat, notifier}) 注入。`);
|
|
2972
4894
|
return;
|
|
2973
4895
|
}
|
|
2974
4896
|
const result = await handleCardAction({
|
|
@@ -2983,10 +4905,10 @@ class LarkCliInstance {
|
|
|
2983
4905
|
const patch = this.cardActionPorts.patchByToken ?? patchCardByLarkCli;
|
|
2984
4906
|
await patch(token, result.cardUpdate, profile);
|
|
2985
4907
|
} else {
|
|
2986
|
-
|
|
4908
|
+
logger5.warn(`[LarkCliInstance] handleCardActionEvent failed: ${result.error ?? "unknown"}`);
|
|
2987
4909
|
}
|
|
2988
4910
|
} catch (err) {
|
|
2989
|
-
|
|
4911
|
+
logger5.error(`[LarkCliInstance] handleCardActionEvent error: ${err.message}`);
|
|
2990
4912
|
}
|
|
2991
4913
|
}
|
|
2992
4914
|
isCommandCardActionEvent(event) {
|
|
@@ -3102,7 +5024,7 @@ class LarkCliInstance {
|
|
|
3102
5024
|
}
|
|
3103
5025
|
await this.sendLarkText(output.text, profile, messageId, chatId);
|
|
3104
5026
|
} catch (err) {
|
|
3105
|
-
|
|
5027
|
+
logger5.error(`[LarkCliInstance] handleCommandMessage error: ${err.message}`);
|
|
3106
5028
|
try {
|
|
3107
5029
|
const card = renderTextCard("⚠️ 命令执行异常", `命令处理时发生异常:${err.message}`);
|
|
3108
5030
|
await this.sendLarkCard(card, profile, messageId, chatId);
|
|
@@ -3138,8 +5060,8 @@ class LarkCliInstance {
|
|
|
3138
5060
|
if (profile) {
|
|
3139
5061
|
args.unshift("--profile", profile);
|
|
3140
5062
|
}
|
|
3141
|
-
await new Promise((
|
|
3142
|
-
const proc =
|
|
5063
|
+
await new Promise((resolve2, reject) => {
|
|
5064
|
+
const proc = spawn3("lark-cli", args, {
|
|
3143
5065
|
shell: false,
|
|
3144
5066
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3145
5067
|
});
|
|
@@ -3149,7 +5071,7 @@ class LarkCliInstance {
|
|
|
3149
5071
|
});
|
|
3150
5072
|
proc.on("close", (code) => {
|
|
3151
5073
|
if (code === 0)
|
|
3152
|
-
|
|
5074
|
+
resolve2();
|
|
3153
5075
|
else
|
|
3154
5076
|
reject(new Error(`lark-cli exited with code ${code}: ${stderr}`));
|
|
3155
5077
|
});
|
|
@@ -3223,7 +5145,7 @@ class LarkCliInstance {
|
|
|
3223
5145
|
};
|
|
3224
5146
|
}
|
|
3225
5147
|
async sendLarkMessage(options) {
|
|
3226
|
-
return new Promise((
|
|
5148
|
+
return new Promise((resolve2, reject) => {
|
|
3227
5149
|
const { chatId, userId, messageId, content, format = "text", profile } = options;
|
|
3228
5150
|
const normalizedContent = normalizeLarkAtMentions(content);
|
|
3229
5151
|
const contentFlag = format === "markdown" ? "--markdown" : "--text";
|
|
@@ -3241,7 +5163,7 @@ class LarkCliInstance {
|
|
|
3241
5163
|
if (profile) {
|
|
3242
5164
|
args.unshift("--profile", profile);
|
|
3243
5165
|
}
|
|
3244
|
-
const proc =
|
|
5166
|
+
const proc = spawn3("lark-cli", args, {
|
|
3245
5167
|
shell: false,
|
|
3246
5168
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3247
5169
|
});
|
|
@@ -3255,7 +5177,7 @@ class LarkCliInstance {
|
|
|
3255
5177
|
});
|
|
3256
5178
|
proc.on("close", (code) => {
|
|
3257
5179
|
if (code === 0) {
|
|
3258
|
-
|
|
5180
|
+
resolve2();
|
|
3259
5181
|
} else {
|
|
3260
5182
|
reject(new Error(`lark-cli exited with code ${code}: ${stderr}`));
|
|
3261
5183
|
}
|
|
@@ -3392,7 +5314,7 @@ var larkCliHandler = {
|
|
|
3392
5314
|
},
|
|
3393
5315
|
async emitTriggerEvent(payload, store) {
|
|
3394
5316
|
const eventId = await store.insertTriggerEvent(payload, "lark-cli");
|
|
3395
|
-
|
|
5317
|
+
logger5.info(`[larkCliHandler] emitTriggerEvent: id=${eventId}`, {
|
|
3396
5318
|
chatId: payload.metadata?.chatId,
|
|
3397
5319
|
senderId: payload.senderId
|
|
3398
5320
|
});
|
|
@@ -3407,7 +5329,7 @@ var larkCliHandler = {
|
|
|
3407
5329
|
try {
|
|
3408
5330
|
const result = await runner.run(command);
|
|
3409
5331
|
if (result.code !== 0) {
|
|
3410
|
-
|
|
5332
|
+
logger5.error(`[larkCliHandler] replyMessage non-zero exit`, {
|
|
3411
5333
|
code: result.code,
|
|
3412
5334
|
stderr: result.stderr
|
|
3413
5335
|
});
|
|
@@ -3421,7 +5343,7 @@ var larkCliHandler = {
|
|
|
3421
5343
|
return { success: true, messageId: replyMessageId };
|
|
3422
5344
|
} catch (err) {
|
|
3423
5345
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3424
|
-
|
|
5346
|
+
logger5.error(`[larkCliHandler] replyMessage spawn error`, { error: errMsg });
|
|
3425
5347
|
return { success: false, error: errMsg };
|
|
3426
5348
|
}
|
|
3427
5349
|
},
|
|
@@ -3526,10 +5448,10 @@ async function patchCardByLarkCli(token, card, profile) {
|
|
|
3526
5448
|
const cmd = `lark-cli ${profileFlag}api POST /open-apis/interactive/v1/card/update ` + `--data '${JSON.stringify({ token, card }).replace(/'/g, "'\\''")}' --as bot`;
|
|
3527
5449
|
try {
|
|
3528
5450
|
const { stdout } = await execAsync2(cmd, { timeout: 30000 });
|
|
3529
|
-
|
|
5451
|
+
logger5.debug(`patchCardByLarkCli ok: ${stdout?.slice(0, 200)}`);
|
|
3530
5452
|
} catch (err) {
|
|
3531
5453
|
const msg = err.message;
|
|
3532
|
-
|
|
5454
|
+
logger5.error(`patchCardByLarkCli failed: ${msg}`);
|
|
3533
5455
|
throw new Error(`卡片刷新失败:${msg}`);
|
|
3534
5456
|
}
|
|
3535
5457
|
}
|
|
@@ -3538,10 +5460,11 @@ var builtInHandlers = [
|
|
|
3538
5460
|
timerHandler,
|
|
3539
5461
|
bountyIMHandler,
|
|
3540
5462
|
bountyCliHandler,
|
|
3541
|
-
cliCommandHandler
|
|
5463
|
+
cliCommandHandler,
|
|
5464
|
+
wecomCliHandler
|
|
3542
5465
|
];
|
|
3543
5466
|
function getBuiltInHandler(type) {
|
|
3544
5467
|
return builtInHandlers.find((h) => h.type === type);
|
|
3545
5468
|
}
|
|
3546
5469
|
|
|
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 };
|
|
5470
|
+
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, buildLarkRecommendedAction, LARK_CHANNEL_PROMPT_SECTION_TITLE, LARK_PROMPT_LENGTH_BUDGET, buildLarkChannelSystemPrompt, resolveLarkChannelSystemPrompt, extractMetadata, LarkCliInstance, renderRuleNoticeSection, extractCommandText, larkCliHandler, patchCardByLarkCli, builtInHandlers, getBuiltInHandler };
|