@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.
Files changed (66) hide show
  1. package/dist/config/config-protection.d.ts +89 -0
  2. package/dist/config/index.d.ts +1 -0
  3. package/dist/config/index.js +16 -0
  4. package/dist/env/agent/agent-component.d.ts +42 -21
  5. package/dist/env/agent/agent-component.js +2 -2
  6. package/dist/env/agent/index.js +2 -2
  7. package/dist/env/agent/types.d.ts +32 -12
  8. package/dist/env/error-reply.d.ts +6 -0
  9. package/dist/env/error-reply.js +1 -1
  10. package/dist/env/event-source/bounty-im-handler.d.ts +8 -0
  11. package/dist/env/event-source/es-command.d.ts +39 -0
  12. package/dist/env/event-source/es-plugins.d.ts +68 -0
  13. package/dist/env/event-source/event-source-component.d.ts +15 -0
  14. package/dist/env/event-source/event-source-handlers.d.ts +68 -0
  15. package/dist/env/event-source/event-source-handlers.js +20 -8
  16. package/dist/env/event-source/index.d.ts +5 -1
  17. package/dist/env/event-source/index.js +57 -8
  18. package/dist/env/event-source/types.d.ts +101 -1
  19. package/dist/env/event-source/wecom-card-action-handler.d.ts +141 -0
  20. package/dist/env/event-source/wecom-card-builder.d.ts +220 -0
  21. package/dist/env/event-source/wecom-cli-handler.d.ts +266 -0
  22. package/dist/env/event-source/wecom-cli-sender.d.ts +88 -0
  23. package/dist/env/event-source/wecom-media.d.ts +130 -0
  24. package/dist/env/event-source/wecom-respond-registry.d.ts +151 -0
  25. package/dist/env/event-source/wecom-ws-bridge.d.ts +213 -0
  26. package/dist/env/index.js +16 -14
  27. package/dist/env/task/delegate/delegate-tool.d.ts +62 -2
  28. package/dist/env/task/delegate/delegate-tool.js +3 -1
  29. package/dist/env/task/delegate/index.js +1 -1
  30. package/dist/env/task/index.js +2 -2
  31. package/dist/env/task/plugins/index.d.ts +2 -0
  32. package/dist/env/task/plugins/index.js +23 -2
  33. package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +41 -2
  34. package/dist/env/task/plugins/lark-card-patch-notifier.js +1 -1
  35. package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +16 -0
  36. package/dist/env/task/plugins/wecom-card-state-store.d.ts +121 -0
  37. package/dist/env/task/plugins/wecom-task-notify-plugin.d.ts +224 -0
  38. package/dist/env/tool/built-in/index.d.ts +2 -0
  39. package/dist/env/tool/built-in/index.js +6 -1
  40. package/dist/env/tool/index.js +3 -2
  41. package/dist/env/workflow/index.js +3 -3
  42. package/dist/env/workflow/plugins/index.js +2 -2
  43. package/dist/env/workflow/tools/index.js +2 -2
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +68 -17
  46. package/dist/shared/@ai-setting/{roy-agent-core-v87njmq7.js → roy-agent-core-0dh8npnz.js} +1 -1
  47. package/dist/shared/@ai-setting/roy-agent-core-2stt4rp5.js +134 -0
  48. package/dist/shared/@ai-setting/{roy-agent-core-mww0mnd9.js → roy-agent-core-7bpj859q.js} +1 -1
  49. package/dist/shared/@ai-setting/{roy-agent-core-w0qjeq2v.js → roy-agent-core-7dd0s3xb.js} +14 -4
  50. package/dist/shared/@ai-setting/{roy-agent-core-rsn44n9y.js → roy-agent-core-b31v3qpv.js} +851 -46
  51. package/dist/shared/@ai-setting/{roy-agent-core-0v27y99n.js → roy-agent-core-begm7ytz.js} +63 -2
  52. package/dist/shared/@ai-setting/{roy-agent-core-aa2j7bk9.js → roy-agent-core-cha0jc7n.js} +40 -26
  53. package/dist/shared/@ai-setting/{roy-agent-core-hd3n57zs.js → roy-agent-core-d1n4jbza.js} +1 -1
  54. package/dist/shared/@ai-setting/{roy-agent-core-9vhq7fvv.js → roy-agent-core-dkrvmjsh.js} +12 -74
  55. package/dist/shared/@ai-setting/{roy-agent-core-99nwgj84.js → roy-agent-core-fzw9xzm1.js} +1 -1
  56. package/dist/shared/@ai-setting/roy-agent-core-m3vjepzz.js +544 -0
  57. package/dist/shared/@ai-setting/{roy-agent-core-ew6qb5yx.js → roy-agent-core-p6ks90tb.js} +1 -1
  58. package/dist/shared/@ai-setting/{roy-agent-core-rhtdspx6.js → roy-agent-core-qk9hd89p.js} +1 -1
  59. package/dist/shared/@ai-setting/{roy-agent-core-6cy43nss.js → roy-agent-core-rykf7c04.js} +49 -9
  60. package/dist/shared/@ai-setting/{roy-agent-core-8ez6mccb.js → roy-agent-core-tk98f92b.js} +4 -0
  61. package/dist/shared/@ai-setting/{roy-agent-core-bxkjk7zx.js → roy-agent-core-vx8zc847.js} +4 -1
  62. package/dist/shared/@ai-setting/roy-agent-core-vyr6svt6.js +31 -0
  63. package/dist/shared/@ai-setting/{roy-agent-core-3gpygqcx.js → roy-agent-core-xf6z96qy.js} +2086 -163
  64. package/package.json +10 -1
  65. /package/dist/shared/@ai-setting/{roy-agent-core-b4x6x4k2.js → roy-agent-core-m1fgtnrc.js} +0 -0
  66. /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-bxkjk7zx.js";
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-rsn44n9y.js";
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-hd3n57zs.js";
30
+ } from "./roy-agent-core-d1n4jbza.js";
20
31
  import {
21
32
  LarkCardPatchNotifier,
22
33
  buildTaskCard
23
- } from "./roy-agent-core-w0qjeq2v.js";
34
+ } from "./roy-agent-core-7dd0s3xb.js";
24
35
  import {
25
36
  notifyReactFailure
26
- } from "./roy-agent-core-6cy43nss.js";
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 spawn2, exec as execSync } from "child_process";
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
- const { email, imServerUrl } = readBountyIMConfig(this.config);
793
- return resolveBountyIMSystemPrompt({
794
- configSystemPrompt: this.config.systemPrompt,
795
- currentEmail: email,
796
- imServerUrl
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
- const command = customCommand || buildDefaultCommand(profile, channel);
1138
- this.child = spawn(`exec ${command}`, [], {
1139
- shell: true,
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
- let stdoutBuffer = "";
1144
- let stderrBuffer = "";
1145
- this.child.stdout?.on("data", (data) => {
1146
- const str = data.toString();
1147
- stdoutBuffer += str;
1148
- this.processStdout(stdoutBuffer, channel ?? "");
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
- this.child.stderr?.on("data", (data) => {
1152
- const output = data.toString();
1153
- stderrBuffer += output;
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
- this.child.on("exit", (code, signal) => {
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(buffer, fallbackChannel) {
1171
- const lines = buffer.split(`
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
- const tail = lines.pop() ?? "";
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
- if (trimmed.startsWith("{") && trimmed.includes("event_id")) {
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, fallbackChannel) {
2729
+ handleEventLine(line) {
1201
2730
  let raw;
1202
2731
  try {
1203
2732
  raw = JSON.parse(line);
1204
2733
  } catch {
1205
- console.error(`[bounty-cli] failed to parse event line: ${line.slice(0, 200)}`);
2734
+ console.error(`[wecom-cli] skipping non-JSON line: ${line.slice(0, 200)}`);
1206
2735
  return;
1207
2736
  }
1208
- const { metadata, replyChannel } = translateEvent(raw, fallbackChannel);
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: buildBountyCliRecommendedAction({
1214
- profile: readBountyCliConfig(this.config).profile,
1215
- channelName: String(metadata.channelName ?? fallbackChannel)
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(`[bounty-cli] event handler threw: ${err}`);
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((resolve) => {
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
- resolve();
2809
+ resolve2();
1242
2810
  });
1243
2811
  } else {
1244
2812
  clearTimeout(timer);
1245
- resolve();
2813
+ resolve2();
1246
2814
  }
1247
2815
  });
1248
2816
  }
1249
2817
  this.setStatus("stopped");
1250
2818
  }
1251
2819
  }
1252
- var bountyCliHandler = {
1253
- type: "bounty-cli",
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 { channel, customCommand } = readBountyCliConfig(config);
1261
- if (!channel && !customCommand) {
1262
- errors.push("Bounty CLI 'channel' (top-level channel or options.channel) is required");
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 BountyCliInstance(config);
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
- fs.writeFileSync(configPath, result, "utf-8");
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((resolve) => {
3316
+ return new Promise((resolve2) => {
1732
3317
  let sock;
1733
3318
  try {
1734
3319
  sock = connect(socketPath);
1735
3320
  } catch (e) {
1736
- resolve({ confirmed: false, reason: e.message });
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
- resolve({ confirmed, reason: why });
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 logger2 = createLogger("command-registry");
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
- logger2.error(`[CommandRegistry] command "${subcommand}" failed: ${msg}`);
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
- logger2.info(`[handleCommandMessage] dispatching "${invocation.subcommand}" args=${JSON.stringify(invocation.args)}`);
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 logger3 = createLogger("command-card-action");
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
- logger3.info(`[handleCommandCardAction] 执行 ${executed}`);
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
- logger3.error(`[handleCommandCardAction] ${executed} 执行异常:${msg}`);
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
- logger3.error(`[handleCommandCardAction] patch 卡片失败:${patchError}`);
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 logger4 = createLogger("lark-cli:event-source");
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
- action,
2145
- replyTo: {
2146
- appId,
2147
- profile,
2148
- chatId,
2149
- messageId
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
- recommendedAction = buildLarkRecommendedAction({
2197
- profile: profile2,
2198
- chatType: metadata.chatType,
2199
- appId: metadata.appId,
2200
- chatId: metadata.chatId,
2201
- messageId: metadata.messageId
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 logger4 !== "undefined" && logger4?.debug) {
2232
- logger4.debug(`[LarkCliInstance] CardActionPorts injected: ` + `getTask=${typeof ports.getTask}, updateTaskContext=${typeof ports.updateTaskContext}, ` + `createGroupChat=${typeof ports.createGroupChat}, deleteGroupChat=${typeof ports.deleteGroupChat}, ` + `notifier=${typeof ports.notifier}`);
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
- logger4.warn(`[LarkCliInstance] start() 已在执行中,忽略重复调用`);
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
- logger4.warn(`[LarkCliInstance] eventKeys 含 ${CARD_ACTION_EVENT_TYPE},但 eventTypes=` + `${JSON.stringify(this.config.eventTypes)} 不含它 → 卡片点击会被**静默丢弃**。` + `请把 ${CARD_ACTION_EVENT_TYPE} 加进 eventTypes,或清空 eventTypes。`);
4205
+ logger5.warn(`[LarkCliInstance] eventKeys 含 ${CARD_ACTION_EVENT_TYPE},但 eventTypes=` + `${JSON.stringify(this.config.eventTypes)} 不含它 → 卡片点击会被**静默丢弃**。` + `请把 ${CARD_ACTION_EVENT_TYPE} 加进 eventTypes,或清空 eventTypes。`);
2290
4206
  }
2291
- logger4.info(`[LarkCliInstance] 已监听 ${CARD_ACTION_EVENT_TYPE}。` + `⚠️ 若点击卡片无反应,先确认飞书开发者后台「事件与回调 → 回调配置」已开启该事件。`);
4207
+ logger5.info(`[LarkCliInstance] 已监听 ${CARD_ACTION_EVENT_TYPE}。` + `⚠️ 若点击卡片无反应,先确认飞书开发者后台「事件与回调 → 回调配置」已开启该事件。`);
2292
4208
  }
2293
4209
  spawnConsumeChild(command) {
2294
- return spawn2("exec " + command, [], {
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
- logger4.warn(`[LarkCliInstance] eventKey 已被其它 consumer 占用(lark-cli 一个 key 只允许一个 consumer):` + `${conflicts.join(", ")}。本 event-source 启动后可能 exit 2。` + `用 \`lark-cli${profile ? ` --profile ${profile}` : ""} event status\` 查看,必要时 \`event stop\`。`);
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
- logger4.warn(`[LarkCliInstance] consume 子进程异常退出 code=${code} (${source}) cmd="${command}"`);
4298
+ logger5.warn(`[LarkCliInstance] consume 子进程异常退出 code=${code} (${source}) cmd="${command}"`);
2383
4299
  this.status = "error";
2384
4300
  } else {
2385
- logger4.info(`[LarkCliInstance] consume 子进程已退出 code=${code} (${source}) cmd="${command}"`);
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
- logger4.warn(`[LarkCliInstance] 所有 consume 子进程都已退出,状态从 running 改为 stopped`);
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
- logger4.error(`[LarkCliInstance] consume 子进程 error: ${error.message}`);
4316
+ logger5.error(`[LarkCliInstance] consume 子进程 error: ${error.message}`);
2401
4317
  this.status = "error";
2402
4318
  settle();
2403
4319
  });
2404
4320
  const timer = setTimeout(() => {
2405
- logger4.warn(`[LarkCliInstance] consume 子进程 ${CHILD_READY_TIMEOUT_MS}ms 内未见 ready marker: cmd="${command}"`);
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
- logger4.warn(`[LarkCliInstance] Error sending SIGTERM to ${child.pid}: ${error}`);
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((resolve) => {
4347
+ const allExited = Promise.all(children.map((child) => new Promise((resolve2) => {
2432
4348
  if (isChildAlive(child) === false)
2433
- return resolve();
2434
- child.once("exit", () => resolve());
2435
- child.once("error", () => resolve());
4349
+ return resolve2();
4350
+ child.once("exit", () => resolve2());
4351
+ child.once("error", () => resolve2());
2436
4352
  })));
2437
- const timeout = new Promise((resolve) => setTimeout(resolve, GRACEFUL_STOP_TIMEOUT_MS));
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
- logger4.debug("[LarkCliInstance] cardActionPorts 已由外部注入,跳过默认 cross-wire");
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
- logger4.debug(`[LarkCliInstance] card.action.trigger 端口未注入:taskComponent 不可用` + `(getTask=${typeof taskComponent?.getTask}, updateTask=${typeof taskComponent?.updateTask})。` + `卡片按钮点击会被跳过。`);
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
- logger4.info(`[LarkCliInstance] card.action.trigger 端口已注入` + `(notifier ${sharedNotifier ? "复用插件共享实例" : "新建实例"})`);
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
- logger4.warn(`[LarkCliInstance] resolveChatTaskBinding failed for chatId=${chatId}: ${err}. ` + `群消息将不会注入 parentTaskId。`);
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
- logger4.warn(`[LarkCliInstance] stream 行缓冲超过 2MB 仍无换行,丢弃(疑似非 NDJSON 输出)`);
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
- logger4.warn(`[LarkCliInstance] duplicate lark-cli event suppressed across instances (event_id=${dedupKey}, ` + `lastSeen=${now - lastShared}ms ago). ` + `Task #4137: SHARED_SEEN 跨实例去重。`);
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
- logger4.warn(`[LarkCliInstance] duplicate lark-cli event suppressed (event_id=${dedupKey}, ` + `lastSeen=${now - last}ms ago). ` + `Task #3977/3978: 防止同一飞书事件触发两次任务创建。`);
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
- logger4.warn(`[LarkCliInstance] handleCardActionEvent: missing token/messageId/chatId, skip`);
4889
+ logger5.warn(`[LarkCliInstance] handleCardActionEvent: missing token/messageId/chatId, skip`);
2968
4890
  return;
2969
4891
  }
2970
4892
  if (!this.cardActionPorts) {
2971
- logger4.warn(`[LarkCliInstance] handleCardActionEvent: cardActionPorts 未注入,` + `card.action.trigger 事件被跳过。` + `调 instance.setCardActionPorts({getTask, updateTaskContext, createGroupChat, deleteGroupChat, notifier}) 注入。`);
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
- logger4.warn(`[LarkCliInstance] handleCardActionEvent failed: ${result.error ?? "unknown"}`);
4908
+ logger5.warn(`[LarkCliInstance] handleCardActionEvent failed: ${result.error ?? "unknown"}`);
2987
4909
  }
2988
4910
  } catch (err) {
2989
- logger4.error(`[LarkCliInstance] handleCardActionEvent error: ${err.message}`);
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
- logger4.error(`[LarkCliInstance] handleCommandMessage error: ${err.message}`);
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((resolve, reject) => {
3142
- const proc = spawn2("lark-cli", args, {
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
- resolve();
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((resolve, reject) => {
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 = spawn2("lark-cli", args, {
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
- resolve();
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
- logger4.info(`[larkCliHandler] emitTriggerEvent: id=${eventId}`, {
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
- logger4.error(`[larkCliHandler] replyMessage non-zero exit`, {
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
- logger4.error(`[larkCliHandler] replyMessage spawn error`, { error: errMsg });
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
- logger4.debug(`patchCardByLarkCli ok: ${stdout?.slice(0, 200)}`);
5451
+ logger5.debug(`patchCardByLarkCli ok: ${stdout?.slice(0, 200)}`);
3530
5452
  } catch (err) {
3531
5453
  const msg = err.message;
3532
- logger4.error(`patchCardByLarkCli failed: ${msg}`);
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 };