evolcore 0.0.2 → 0.0.3

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 (124) hide show
  1. package/CHANGELOG.md +44 -793
  2. package/dist/agents/claude-runner.js +197 -17
  3. package/dist/agents/codex-runner.js +46 -3
  4. package/dist/aun/outbox.js +8 -0
  5. package/dist/channels/aun.js +21 -4
  6. package/dist/channels/contact-bind-code.js +134 -0
  7. package/dist/channels/dingtalk.js +979 -149
  8. package/dist/channels/feishu.js +130 -54
  9. package/dist/channels/wecom-card.js +101 -0
  10. package/dist/channels/wecom-onboarding.js +82 -0
  11. package/dist/channels/wecom-state.js +191 -0
  12. package/dist/channels/wecom.js +755 -163
  13. package/dist/cli/agent-command.js +2 -1
  14. package/dist/cli/aun-commands.js +88 -33
  15. package/dist/cli/bench.js +2 -2
  16. package/dist/cli/contact.js +71 -0
  17. package/dist/cli/ctl-command.js +2 -2
  18. package/dist/cli/daemon-commands.js +77 -214
  19. package/dist/cli/handoff-command.js +2 -2
  20. package/dist/cli/help.js +9 -5
  21. package/dist/cli/index.js +132 -116
  22. package/dist/cli/init-channel.js +92 -97
  23. package/dist/cli/init.js +63 -26
  24. package/dist/cli/model.js +2 -1
  25. package/dist/cli/net-check.js +2 -2
  26. package/dist/cli/queue-command.js +30 -6
  27. package/dist/cli/raw-key-input.js +25 -0
  28. package/dist/cli/response.js +5 -6
  29. package/dist/cli/restart-monitor.js +25 -1
  30. package/dist/cli/stats.js +6 -4
  31. package/dist/cli/trigger-command.js +55 -15
  32. package/dist/cli/version.js +6 -1
  33. package/dist/config/builtin-role-templates.js +22 -10
  34. package/dist/config/builtin-roles.js +7 -1
  35. package/dist/config/config-manager.js +221 -17
  36. package/dist/config/contact-alias.js +68 -0
  37. package/dist/config/contact-book-store.js +454 -0
  38. package/dist/config/contact-book-v2-startup.js +35 -0
  39. package/dist/config/contact-book.js +156 -303
  40. package/dist/config/contact-operation-service.js +110 -0
  41. package/dist/config/peer-role-resolver.js +133 -54
  42. package/dist/config/role-ranks.js +18 -0
  43. package/dist/config/role-service.js +16 -19
  44. package/dist/config/role-store.js +16 -5
  45. package/dist/config/roles.js +10 -1
  46. package/dist/config-store.js +0 -2
  47. package/dist/core/auth/authorization-audit.js +10 -1
  48. package/dist/core/auth/operation-authorizer.js +2 -0
  49. package/dist/core/auth/operation-catalog.js +56 -0
  50. package/dist/core/command/command-handler.js +105 -10
  51. package/dist/core/command/connect-menu.js +374 -0
  52. package/dist/core/command/menu-handler.js +114 -16
  53. package/dist/core/command/role-menu.js +128 -29
  54. package/dist/core/command/slash-handler.js +28 -18
  55. package/dist/core/daemon-file-cache.js +12 -6
  56. package/dist/core/event-catalog.js +70 -0
  57. package/dist/core/evolagent-registry.js +0 -1
  58. package/dist/core/evolagent.js +20 -9
  59. package/dist/core/message/im-renderer.js +2 -0
  60. package/dist/core/message/message-bridge.js +79 -8
  61. package/dist/core/message/message-queue.js +10 -0
  62. package/dist/core/message/message-utils.js +8 -2
  63. package/dist/core/message/response-engine.js +269 -25
  64. package/dist/core/message/send-receipt.js +24 -0
  65. package/dist/core/message/stream-debouncer.js +11 -2
  66. package/dist/core/permission/tool-policy.js +47 -15
  67. package/dist/core/protected-paths.js +2 -0
  68. package/dist/core/session/session-fs-store.js +44 -3
  69. package/dist/core/session/session-manager.js +61 -5
  70. package/dist/index.js +62 -6
  71. package/dist/ipc.js +34 -5
  72. package/dist/stats/billing.js +20 -8
  73. package/dist/trigger/manager.js +3 -0
  74. package/dist/trigger/parser.js +77 -2
  75. package/dist/trigger/patch.js +8 -1
  76. package/dist/trigger/scheduler.js +3 -1
  77. package/dist/trigger/validation.js +13 -0
  78. package/dist/utils/aid-bind.js +43 -29
  79. package/dist/utils/instance-registry.js +14 -7
  80. package/dist/utils/log-writer.js +46 -0
  81. package/dist/utils/media-cache.js +4 -1
  82. package/dist/utils/model-prices.jsonl +6 -3
  83. package/dist/utils/restart-safety.js +31 -0
  84. package/dist/utils/system-memory.js +62 -0
  85. package/kits/docs/INDEX.md +2 -1
  86. package/kits/docs/evolcore/INDEX.md +5 -3
  87. package/kits/docs/evolcore/agent.md +9 -1
  88. package/kits/docs/evolcore/aid.md +5 -2
  89. package/kits/docs/evolcore/contact.md +57 -0
  90. package/kits/docs/evolcore/fs.md +9 -0
  91. package/kits/docs/evolcore/group.md +12 -3
  92. package/kits/docs/evolcore/model.md +4 -1
  93. package/kits/docs/evolcore/msg.md +9 -3
  94. package/kits/docs/evolcore/response.md +16 -21
  95. package/kits/docs/evolcore/rpc.md +2 -0
  96. package/kits/docs/evolcore/stats.md +15 -2
  97. package/kits/docs/evolcore/storage.md +1 -0
  98. package/kits/docs/evolcore/trigger.md +17 -2
  99. package/kits/eck_manifest.json +12 -0
  100. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  101. package/kits/schemas/_meta.json +7 -4
  102. package/kits/schemas/agent-config.schema.6.json +322 -0
  103. package/kits/schemas/contact-book.schema.2.json +43 -0
  104. package/kits/schemas/relation-config.schema.5.json +47 -0
  105. package/kits/schemas/role-config.schema.1.json +1 -0
  106. package/kits/schemas/role-registry.schema.1.json +2 -2
  107. package/kits/templates/roles/admin.json +1 -0
  108. package/kits/templates/roles/member.json +1 -0
  109. package/kits/templates/roles/owner.json +1 -0
  110. package/kits/templates/roles/visitor.json +1 -0
  111. package/kits/templates/system-fragments/commands.md +3 -1
  112. package/package.json +4 -4
  113. package/assets/brand/evolcore/README.md +0 -19
  114. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  115. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  116. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  117. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  118. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  119. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  120. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  121. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  122. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  123. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  124. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -6,6 +6,7 @@ import { logger } from '../utils/logger.js';
6
6
  import { formatItemsAsText } from '../core/message/items-formatter.js';
7
7
  import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
8
8
  import { chatDirPath, decodeDirSegment } from '../core/session/session-fs-store.js';
9
+ import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
9
10
  export class FeishuChannel {
10
11
  config;
11
12
  agentAid;
@@ -125,7 +126,7 @@ export class FeishuChannel {
125
126
  try {
126
127
  // 首次交互欢迎消息(使用共享帮助函数)
127
128
  if (peerId) {
128
- await sendWelcomeIfNeeded(this.welcomeManager, peerId, msg.chat_id, (id, text) => this.sendMessage(id, text), 'Feishu');
129
+ await sendWelcomeIfNeeded(this.welcomeManager, peerId, msg.chat_id, async (id, text) => { await this.sendMessage(id, text); }, 'Feishu');
129
130
  }
130
131
  // 提取话题信息
131
132
  const threadId = msg.thread_id || undefined;
@@ -471,11 +472,11 @@ export class FeishuChannel {
471
472
  }
472
473
  async sendMessage(chatId, content, options) {
473
474
  if (!this.client)
474
- return;
475
+ throw new Error('Feishu client is not connected');
475
476
  content = exposeLocalMarkdownLinkTargetsForFeishu(content);
476
477
  if (!content || content.trim() === '') {
477
478
  logger.warn('[Feishu] Attempted to send empty message, skipping');
478
- return;
479
+ return [];
479
480
  }
480
481
  // 飞书消息内容限制约 30KB(text)/ 150KB(post),安全阈值 28000 字符
481
482
  // 超长消息自动拆分,按段落边界分割
@@ -483,12 +484,13 @@ export class FeishuChannel {
483
484
  if (content.length > MAX_CONTENT_LENGTH) {
484
485
  logger.info(`[Feishu] Message too long (${content.length} chars), splitting into parts`);
485
486
  const parts = splitLongMessage(content, MAX_CONTENT_LENGTH);
487
+ const messageIds = [];
486
488
  for (let i = 0; i < parts.length; i++) {
487
489
  // 首条消息保留 reply 选项,后续消息不再 reply
488
490
  const partOptions = i === 0 ? options : { ...options, replyToMessageId: undefined };
489
- await this.sendMessage(chatId, parts[i], partOptions);
491
+ messageIds.push(...await this.sendMessage(chatId, parts[i], partOptions));
490
492
  }
491
- return;
493
+ return messageIds;
492
494
  }
493
495
  logger.debug(`[Feishu] sendMessage called, chatId: ${chatId}, content length: ${content.length}`);
494
496
  try {
@@ -511,6 +513,7 @@ export class FeishuChannel {
511
513
  else {
512
514
  msgContent = JSON.stringify({ text: content });
513
515
  }
516
+ let messageId;
514
517
  if (options?.replyToMessageId) {
515
518
  const replyData = { msg_type: msgType, content: msgContent };
516
519
  if (options.replyInThread) {
@@ -520,6 +523,7 @@ export class FeishuChannel {
520
523
  path: { message_id: options.replyToMessageId },
521
524
  data: replyData
522
525
  });
526
+ messageId = replyRes?.data?.message_id;
523
527
  if (options.replyInThread && options.onThreadCreated) {
524
528
  const newThreadId = replyRes?.data?.thread_id;
525
529
  if (newThreadId)
@@ -527,12 +531,16 @@ export class FeishuChannel {
527
531
  }
528
532
  }
529
533
  else {
530
- await this.client.im.message.create({
534
+ const createRes = await this.client.im.message.create({
531
535
  params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
532
536
  data: { receive_id: chatId, msg_type: msgType, content: msgContent }
533
537
  });
538
+ messageId = createRes?.data?.message_id;
534
539
  }
540
+ if (!messageId)
541
+ throw new Error('Feishu send succeeded without message_id');
535
542
  logger.debug(`[Feishu] Sent message as ${useMarkdown ? 'post (Markdown)' : 'text'}`);
543
+ return [messageId];
536
544
  }
537
545
  catch (error) {
538
546
  // 230011: 消息已被撤回,降级为普通消息重试
@@ -560,7 +568,7 @@ export class FeishuChannel {
560
568
  }
561
569
  async sendFile(chatId, filePath, options) {
562
570
  if (!this.client)
563
- return;
571
+ throw new Error('Feishu client is not connected');
564
572
  try {
565
573
  // 检测是否为图片,是则走 sendImage(内联预览)而非文件卡片
566
574
  // 读取足够字节供 file-type 解析(ZIP-based 格式如 PPTX 需要更多字节)
@@ -585,24 +593,25 @@ export class FeishuChannel {
585
593
  }
586
594
  });
587
595
  if (!uploadResponse || !uploadResponse.file_key) {
588
- logger.error('[Feishu] File upload failed: no file_key returned');
589
- return;
596
+ throw new Error('Feishu file upload failed: no file_key returned');
590
597
  }
591
598
  const fileKey = uploadResponse.file_key;
592
599
  const msgContent = JSON.stringify({ file_key: fileKey });
593
600
  logger.info('[Feishu] File uploaded, file_key:', fileKey);
601
+ let messageId;
594
602
  if (options?.replyToMessageId) {
595
603
  const replyData = { msg_type: 'file', content: msgContent };
596
604
  if (options.replyInThread) {
597
605
  replyData.reply_in_thread = true;
598
606
  }
599
- await this.client.im.message.reply({
607
+ const replyRes = await this.client.im.message.reply({
600
608
  path: { message_id: options.replyToMessageId },
601
609
  data: replyData
602
610
  });
611
+ messageId = replyRes?.data?.message_id;
603
612
  }
604
613
  else {
605
- await this.client.im.message.create({
614
+ const createRes = await this.client.im.message.create({
606
615
  params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
607
616
  data: {
608
617
  receive_id: chatId,
@@ -610,8 +619,12 @@ export class FeishuChannel {
610
619
  content: msgContent
611
620
  }
612
621
  });
622
+ messageId = createRes?.data?.message_id;
613
623
  }
624
+ if (!messageId)
625
+ throw new Error('Feishu file send succeeded without message_id');
614
626
  logger.info('[Feishu] File message sent successfully');
627
+ return [messageId];
615
628
  }
616
629
  catch (error) {
617
630
  // 230011/99992354: reply target invalid, retry without reply
@@ -626,7 +639,7 @@ export class FeishuChannel {
626
639
  }
627
640
  async sendImage(chatId, png, options) {
628
641
  if (!this.client)
629
- return;
642
+ throw new Error('Feishu client is not connected');
630
643
  try {
631
644
  const uploadResponse = await this.client.im.image.create({
632
645
  data: {
@@ -636,27 +649,32 @@ export class FeishuChannel {
636
649
  });
637
650
  const imageKey = uploadResponse?.image_key;
638
651
  if (!imageKey) {
639
- logger.error('[Feishu] Image upload failed: no image_key returned');
640
- return;
652
+ throw new Error('Feishu image upload failed: no image_key returned');
641
653
  }
642
654
  logger.debug('[Feishu] Image uploaded, image_key:', imageKey);
643
655
  const msgContent = JSON.stringify({ image_key: imageKey });
656
+ let messageId;
644
657
  if (options?.replyToMessageId) {
645
658
  const replyData = { msg_type: 'image', content: msgContent };
646
659
  if (options.replyInThread)
647
660
  replyData.reply_in_thread = true;
648
- await this.client.im.message.reply({
661
+ const replyRes = await this.client.im.message.reply({
649
662
  path: { message_id: options.replyToMessageId },
650
663
  data: replyData
651
664
  });
665
+ messageId = replyRes?.data?.message_id;
652
666
  }
653
667
  else {
654
- await this.client.im.message.create({
668
+ const createRes = await this.client.im.message.create({
655
669
  params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
656
670
  data: { receive_id: chatId, msg_type: 'image', content: msgContent }
657
671
  });
672
+ messageId = createRes?.data?.message_id;
658
673
  }
674
+ if (!messageId)
675
+ throw new Error('Feishu image send succeeded without message_id');
659
676
  logger.debug('[Feishu] Image message sent successfully');
677
+ return [messageId];
660
678
  }
661
679
  catch (error) {
662
680
  // 99992354: reply target invalid — image cannot easily retry, just log
@@ -1273,6 +1291,10 @@ export class FeishuChannel {
1273
1291
  }
1274
1292
  // messageId → Pin reaction create promise(等待 reaction_id 落地,供 promoteAck 删除)
1275
1293
  pinReactions = new Map();
1294
+ // messageId → StatusInFlight reaction create promise(终态时替换)
1295
+ inFlightReactions = new Map();
1296
+ // taskId → 触发该任务的全部原始入站 messageId(批量合并时可有多条)
1297
+ taskAckMessages = new Map();
1276
1298
  /** 收到消息时添加 Pin 表情(表示"已收到,排队中") */
1277
1299
  addPinReaction(messageId) {
1278
1300
  if (!this.client)
@@ -1284,22 +1306,67 @@ export class FeishuChannel {
1284
1306
  .catch(() => undefined);
1285
1307
  this.pinReactions.set(messageId, p);
1286
1308
  }
1287
- /** Runner 开始执行时:添加 CheckMark,然后异步移除 Pin(视觉无闪烁) */
1288
- async promoteAckReaction(messageId) {
1309
+ /** Runner 开始执行时:添加 StatusInFlight,然后移除 Pin(视觉无闪烁) */
1310
+ async promoteAckReaction(messageId, taskId, sourceMessageIds) {
1311
+ const messageIds = [...new Set([...(sourceMessageIds ?? []), messageId].filter(Boolean))];
1312
+ if (taskId)
1313
+ this.taskAckMessages.set(taskId, messageIds);
1289
1314
  if (!this.client)
1290
1315
  return;
1291
- // 先加 CheckMark,再删 Pin——用户看到的是 Pin→Pin+CheckMark→CheckMark,无空窗
1292
- this.client.im.messageReaction.create({
1293
- path: { message_id: messageId },
1294
- data: { reaction_type: { emoji_type: 'CheckMark' } },
1295
- }).catch(() => { });
1296
- const pending = this.pinReactions.get(messageId);
1297
- this.pinReactions.delete(messageId);
1298
- const reactionId = await pending;
1299
- if (reactionId) {
1300
- this.client.im.messageReaction.delete({
1301
- path: { message_id: messageId, reaction_id: reactionId },
1302
- }).catch(() => { });
1316
+ const promotions = messageIds.map(id => {
1317
+ // 先加 StatusInFlight,再删 Pin——用户看到的是 Pin→Pin+InFlight→InFlight。
1318
+ const inFlightPending = this.client.im.messageReaction.create({
1319
+ path: { message_id: id },
1320
+ data: { reaction_type: { emoji_type: 'StatusInFlight' } },
1321
+ }).then((res) => res?.data?.reaction_id)
1322
+ .catch(() => undefined);
1323
+ this.inFlightReactions.set(id, inFlightPending);
1324
+ const pending = this.pinReactions.get(id);
1325
+ this.pinReactions.delete(id);
1326
+ return { id, pending };
1327
+ });
1328
+ // Every in-flight reaction is registered before this first await, so a
1329
+ // fast terminal event cannot race ahead and strand a merged-batch status.
1330
+ for (const { id, pending } of promotions) {
1331
+ const reactionId = await pending;
1332
+ if (reactionId) {
1333
+ await this.client.im.messageReaction.delete({
1334
+ path: { message_id: id, reaction_id: reactionId },
1335
+ }).catch(() => { });
1336
+ }
1337
+ }
1338
+ }
1339
+ /** 任务终态:用 CheckMark/CrossMark 替换 StatusInFlight。 */
1340
+ async finalizeAckReaction(taskId, terminalEmoji) {
1341
+ const messageIds = this.taskAckMessages.get(taskId);
1342
+ if (!messageIds)
1343
+ return;
1344
+ if (!this.client) {
1345
+ this.taskAckMessages.delete(taskId);
1346
+ return;
1347
+ }
1348
+ this.taskAckMessages.delete(taskId);
1349
+ for (const messageId of messageIds) {
1350
+ const inFlightPending = this.inFlightReactions.get(messageId);
1351
+ // 先加终态表情,再删处理中表情,避免视觉空窗。
1352
+ const addTerminal = () => this.client.im.messageReaction.create({
1353
+ path: { message_id: messageId },
1354
+ data: { reaction_type: { emoji_type: terminalEmoji } },
1355
+ }).then((res) => !!res?.data?.reaction_id).catch(() => false);
1356
+ let terminalAdded = await addTerminal();
1357
+ if (!terminalAdded)
1358
+ terminalAdded = await addTerminal();
1359
+ if (!terminalAdded) {
1360
+ logger.warn(`[Feishu] Failed to add terminal reaction for ${messageId}; preserving processing reaction`);
1361
+ continue;
1362
+ }
1363
+ this.inFlightReactions.delete(messageId);
1364
+ const inFlightReactionId = await inFlightPending;
1365
+ if (inFlightReactionId) {
1366
+ await this.client.im.messageReaction.delete({
1367
+ path: { message_id: messageId, reaction_id: inFlightReactionId },
1368
+ }).catch(() => { });
1369
+ }
1303
1370
  }
1304
1371
  }
1305
1372
  }
@@ -1707,46 +1774,53 @@ export class FeishuChannelPlugin {
1707
1774
  sendCtx.title = '✅ 最终回复:';
1708
1775
  if (replyCtx?.metadata?.onThreadCreated)
1709
1776
  sendCtx.onThreadCreated = replyCtx.metadata.onThreadCreated;
1710
- await channel.sendMessage(channelId, payload.text, sendCtx);
1711
- return;
1777
+ const ids = await channel.sendMessage(channelId, payload.text, sendCtx);
1778
+ return ids.length > 0
1779
+ ? sentReceipt(envelope, ids, replyCtx?.threadId)
1780
+ : suppressedReceipt(envelope, 'empty_message');
1712
1781
  }
1713
- case 'result.file':
1714
- await channel.sendFile(channelId, payload.filePath, replyCtx);
1715
- return;
1716
- case 'result.image':
1717
- await channel.sendImage(channelId, payload.data, replyCtx);
1718
- return;
1782
+ case 'result.file': return sentReceipt(envelope, await channel.sendFile(channelId, payload.filePath, replyCtx), replyCtx?.threadId);
1783
+ case 'result.image': return sentReceipt(envelope, await channel.sendImage(channelId, payload.data, replyCtx), replyCtx?.threadId);
1719
1784
  case 'activity.batch': {
1720
1785
  const filtered = payload.items.filter((i) => !(i.kind === 'tool_result' && i.ok));
1721
1786
  const text = formatItemsAsText(filtered);
1722
1787
  if (text)
1723
- await channel.sendMessage(channelId, text, replyCtx);
1724
- return;
1788
+ return sentReceipt(envelope, await channel.sendMessage(channelId, text, replyCtx), replyCtx?.threadId);
1789
+ return suppressedReceipt(envelope, 'empty_activity');
1725
1790
  }
1726
1791
  case 'status.started':
1792
+ case 'status.queued':
1793
+ case 'status.progress':
1794
+ return suppressedReceipt(envelope, 'reaction_lifecycle');
1727
1795
  case 'status.completed':
1796
+ await channel.finalizeAckReaction(envelope.taskId, 'CheckMark');
1797
+ return suppressedReceipt(envelope, 'reaction_lifecycle');
1728
1798
  case 'status.interrupted':
1729
1799
  case 'status.timeout':
1730
- case 'status.progress': return;
1800
+ await channel.finalizeAckReaction(envelope.taskId, 'CrossMark');
1801
+ return suppressedReceipt(envelope, 'reaction_lifecycle');
1731
1802
  case 'status.requires_action':
1732
- await channel.sendMessage(channelId, '等待 owner 审批', replyCtx);
1733
- return;
1803
+ return sentReceipt(envelope, await channel.sendMessage(channelId, '等待 owner 审批', replyCtx), replyCtx?.threadId);
1734
1804
  case 'status.error':
1735
- if (payload.metadata?.message)
1736
- await channel.sendMessage(channelId, payload.metadata.message, replyCtx);
1737
- return;
1805
+ await channel.finalizeAckReaction(envelope.taskId, 'CrossMark');
1806
+ if (payload.metadata?.message) {
1807
+ return sentReceipt(envelope, await channel.sendMessage(channelId, payload.metadata.message, replyCtx), replyCtx?.threadId);
1808
+ }
1809
+ return suppressedReceipt(envelope, 'reaction_lifecycle');
1738
1810
  case 'interaction': {
1739
1811
  const sent = await channel.sendInteraction(channelId, payload.interaction, replyCtx);
1740
1812
  if (!sent)
1741
1813
  throw new Error('sendInteraction returned false');
1742
- return;
1814
+ return sentReceipt(envelope, [sent], replyCtx?.threadId);
1743
1815
  }
1744
- case 'custom': return;
1745
- default: logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
1816
+ case 'custom': return suppressedReceipt(envelope, 'unsupported_custom_payload');
1817
+ default:
1818
+ logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
1819
+ return suppressedReceipt(envelope, 'unhandled_payload');
1746
1820
  }
1747
1821
  },
1748
1822
  acknowledge: (messageId) => { channel.addPinReaction(messageId); return Promise.resolve(); },
1749
- promoteAck: (messageId) => channel.promoteAckReaction(messageId),
1823
+ promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds)),
1750
1824
  onInteraction: (callback) => channel.onInteraction(callback),
1751
1825
  onInteractionInvalidated: (callback) => channel.onInteractionInvalidated(callback),
1752
1826
  invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
@@ -1779,10 +1853,12 @@ export class FeishuChannelPlugin {
1779
1853
  replyContext: threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
1780
1854
  source,
1781
1855
  });
1782
- }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, {
1783
- replyToMessageId: replyContext?.replyToMessageId,
1784
- replyInThread: replyContext?.replyInThread,
1785
- }), adapter, channelType);
1856
+ }), async (channelId, text, replyContext) => {
1857
+ await channel.sendMessage(channelId, text, {
1858
+ replyToMessageId: replyContext?.replyToMessageId,
1859
+ replyInThread: replyContext?.replyInThread,
1860
+ });
1861
+ }, adapter, channelType);
1786
1862
  },
1787
1863
  };
1788
1864
  }
@@ -0,0 +1,101 @@
1
+ function truncate(value, max) {
2
+ if (!value)
3
+ return undefined;
4
+ const chars = [...value.trim()];
5
+ return chars.length <= max ? value.trim() : `${chars.slice(0, Math.max(1, max - 1)).join('')}...`;
6
+ }
7
+ function buttonStyle(style) {
8
+ if (style === 'primary')
9
+ return 2;
10
+ if (style === 'danger')
11
+ return 3;
12
+ return 1;
13
+ }
14
+ export function buildWecomInteractionCard(interaction, taskId) {
15
+ const bodyParts = [];
16
+ if (interaction.kind.body)
17
+ bodyParts.push(interaction.kind.body);
18
+ if (interaction.kind.kind === 'action' && interaction.kind.checkers?.length) {
19
+ bodyParts.push(interaction.kind.checkers.map((item, index) => `${index + 1}. ${item.label}`).join('\n'));
20
+ }
21
+ return {
22
+ card_type: 'button_interaction',
23
+ task_id: taskId,
24
+ main_title: {
25
+ title: truncate(interaction.kind.title, 26),
26
+ desc: truncate(bodyParts.join('\n\n'), 120),
27
+ },
28
+ button_list: interaction.kind.buttons
29
+ .map((button, index) => ({ button, index }))
30
+ .filter(({ button }) => !('disabled' in button) || !button.disabled)
31
+ .slice(0, 6)
32
+ .map(({ button, index }) => ({
33
+ text: truncate(button.label, 10) || `选项 ${index + 1}`,
34
+ style: buttonStyle(button.style),
35
+ key: interaction.kind.kind === 'command-card' ? `cmd_${index}` : `act_${index}`,
36
+ })),
37
+ };
38
+ }
39
+ export function parseWecomCardResponse(record, event, operatorId) {
40
+ const eventKey = String(event.event_key ?? '').trim();
41
+ const match = /^(cmd|act)_(\d+)$/.exec(eventKey);
42
+ if (!match)
43
+ return null;
44
+ const index = Number(match[2]);
45
+ if (record.interaction.kind.kind === 'command-card') {
46
+ const button = record.interaction.kind.buttons[index];
47
+ if (!button || button.disabled)
48
+ return null;
49
+ return { command: button.command };
50
+ }
51
+ const button = record.interaction.kind.buttons[index];
52
+ if (!button)
53
+ return null;
54
+ const values = {};
55
+ const selectedItems = event.selected_items?.selected_item;
56
+ if (Array.isArray(selectedItems)) {
57
+ for (const selected of selectedItems) {
58
+ const questionKey = String(selected?.question_key ?? '').trim();
59
+ const optionIds = selected?.option_ids?.option_id;
60
+ if (questionKey && Array.isArray(optionIds))
61
+ values[questionKey] = optionIds.filter(Boolean);
62
+ }
63
+ }
64
+ return {
65
+ response: {
66
+ type: 'interaction.response',
67
+ id: record.interaction.id,
68
+ action: button.key,
69
+ values: Object.keys(values).length > 0 ? values : undefined,
70
+ operatorId,
71
+ },
72
+ };
73
+ }
74
+ export function buildWecomTerminalCard(record, status, detail) {
75
+ const statusText = status === 'resolved'
76
+ ? `已处理${detail ? `:${detail}` : ''}`
77
+ : status === 'rejected'
78
+ ? '仅卡片发起者可操作'
79
+ : status === 'unavailable'
80
+ ? '处理服务暂不可用,请重新发起'
81
+ : `卡片已失效${detail ? `:${detail}` : ''}`;
82
+ return {
83
+ card_type: 'text_notice',
84
+ task_id: record.taskId,
85
+ main_title: {
86
+ title: truncate(record.interaction.kind.title, 26),
87
+ desc: truncate(statusText, 120),
88
+ },
89
+ sub_title_text: truncate(record.interaction.kind.body, 120),
90
+ };
91
+ }
92
+ export function buildWecomUnknownCard(taskId) {
93
+ return {
94
+ card_type: 'text_notice',
95
+ task_id: taskId,
96
+ main_title: {
97
+ title: '卡片已失效',
98
+ desc: '本地状态不存在,请重新发起操作',
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,82 @@
1
+ import os from 'node:os';
2
+ const WECOM_QR_GENERATE_URL = 'https://work.weixin.qq.com/ai/qc/generate';
3
+ const WECOM_QR_QUERY_URL = 'https://work.weixin.qq.com/ai/qc/query_result';
4
+ const WECOM_QR_PAGE_URL = 'https://work.weixin.qq.com/ai/qc/gen';
5
+ function platformCode() {
6
+ if (os.platform() === 'darwin')
7
+ return 1;
8
+ if (os.platform() === 'win32')
9
+ return 2;
10
+ if (os.platform() === 'linux')
11
+ return 3;
12
+ return 0;
13
+ }
14
+ async function fetchJson(url, fetchImpl, signal) {
15
+ const timeoutSignal = AbortSignal.timeout(15_000);
16
+ const response = await fetchImpl(url, {
17
+ headers: { Accept: 'application/json' },
18
+ signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
19
+ });
20
+ if (!response.ok)
21
+ throw new Error(`企微扫码服务请求失败: HTTP ${response.status}`);
22
+ return response.json();
23
+ }
24
+ export async function createWecomQrSession(fetchImpl = fetch) {
25
+ const url = new URL(WECOM_QR_GENERATE_URL);
26
+ url.searchParams.set('source', 'wecom-cli');
27
+ url.searchParams.set('plat', String(platformCode()));
28
+ const payload = await fetchJson(url, fetchImpl);
29
+ const scode = String(payload?.data?.scode ?? '').trim();
30
+ const authUrl = String(payload?.data?.auth_url ?? '').trim();
31
+ if (!scode || !authUrl)
32
+ throw new Error('企微扫码服务未返回 scode 或授权地址');
33
+ const browserUrl = new URL(WECOM_QR_PAGE_URL);
34
+ browserUrl.searchParams.set('source', 'wecom-cli');
35
+ browserUrl.searchParams.set('scode', scode);
36
+ return { scode, authUrl, browserUrl: browserUrl.toString() };
37
+ }
38
+ export async function pollWecomQrCredentials(scode, opts) {
39
+ const fetchImpl = opts?.fetchImpl ?? fetch;
40
+ const timeoutMs = opts?.timeoutMs ?? 5 * 60 * 1000;
41
+ const intervalMs = opts?.intervalMs ?? 3_000;
42
+ const deadline = Date.now() + timeoutMs;
43
+ const url = new URL(WECOM_QR_QUERY_URL);
44
+ url.searchParams.set('scode', scode);
45
+ let previousStatus = '';
46
+ while (Date.now() < deadline) {
47
+ if (opts?.signal?.aborted)
48
+ throw new Error('企微扫码已取消');
49
+ const payload = await fetchJson(url, fetchImpl, opts?.signal);
50
+ const status = String(payload?.data?.status ?? 'waiting');
51
+ if (status !== previousStatus) {
52
+ opts?.onStatus?.(status);
53
+ previousStatus = status;
54
+ }
55
+ if (status === 'success') {
56
+ const botId = String(payload?.data?.bot_info?.botid ?? '').trim();
57
+ const secret = String(payload?.data?.bot_info?.secret ?? '').trim();
58
+ if (!botId || !secret)
59
+ throw new Error('扫码成功,但企微未返回 Bot ID 或 Secret');
60
+ return { botId, secret };
61
+ }
62
+ if (status === 'expired' || status === 'cancelled' || status === 'rejected') {
63
+ throw new Error(`企微扫码未完成: ${status}`);
64
+ }
65
+ await new Promise((resolve, reject) => {
66
+ const finish = () => {
67
+ opts?.signal?.removeEventListener('abort', onAbort);
68
+ resolve();
69
+ };
70
+ const timer = setTimeout(finish, intervalMs);
71
+ const onAbort = () => {
72
+ clearTimeout(timer);
73
+ opts?.signal?.removeEventListener('abort', onAbort);
74
+ reject(new Error('企微扫码已取消'));
75
+ };
76
+ opts?.signal?.addEventListener('abort', onAbort, { once: true });
77
+ if (opts?.signal?.aborted)
78
+ onAbort();
79
+ });
80
+ }
81
+ throw new Error('企微扫码超时,请重新生成二维码');
82
+ }