evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -4,8 +4,28 @@ import imageType from 'image-type';
4
4
  import { sanitizeFileName, saveToUploads, bufferToInboundImage } from '../utils/media-cache.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { formatItemsAsText } from '../core/message/items-formatter.js';
7
+ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
7
8
  import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
8
9
  import { chatDirPath, decodeDirSegment } from '../core/session/session-fs-store.js';
10
+ import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
11
+ import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
12
+ const feishuContactBinds = new ContactBindCodeRegistry({
13
+ channelType: 'feishu',
14
+ displayName: '飞书',
15
+ initCommand: 'ec init feishu',
16
+ });
17
+ export function registerPendingFeishuContactBind(req) {
18
+ return feishuContactBinds.register(req);
19
+ }
20
+ export function handlePendingFeishuContactBindMessage(ctx) {
21
+ return handleExplicitContactBindCommand(feishuContactBinds, ctx);
22
+ }
23
+ export function getPendingFeishuContactBind(selfAid, channelName, primaryId) {
24
+ return feishuContactBinds.get(selfAid, channelName, primaryId);
25
+ }
26
+ export function clearPendingFeishuContactBinds() {
27
+ feishuContactBinds.clear();
28
+ }
9
29
  export class FeishuChannel {
10
30
  config;
11
31
  agentAid;
@@ -125,7 +145,7 @@ export class FeishuChannel {
125
145
  try {
126
146
  // 首次交互欢迎消息(使用共享帮助函数)
127
147
  if (peerId) {
128
- await sendWelcomeIfNeeded(this.welcomeManager, peerId, msg.chat_id, (id, text) => this.sendMessage(id, text), 'Feishu');
148
+ await sendWelcomeIfNeeded(this.welcomeManager, peerId, msg.chat_id, async (id, text) => { await this.sendMessage(id, text); }, 'Feishu');
129
149
  }
130
150
  // 提取话题信息
131
151
  const threadId = msg.thread_id || undefined;
@@ -471,11 +491,11 @@ export class FeishuChannel {
471
491
  }
472
492
  async sendMessage(chatId, content, options) {
473
493
  if (!this.client)
474
- return;
494
+ throw new Error('Feishu client is not connected');
475
495
  content = exposeLocalMarkdownLinkTargetsForFeishu(content);
476
496
  if (!content || content.trim() === '') {
477
497
  logger.warn('[Feishu] Attempted to send empty message, skipping');
478
- return;
498
+ return [];
479
499
  }
480
500
  // 飞书消息内容限制约 30KB(text)/ 150KB(post),安全阈值 28000 字符
481
501
  // 超长消息自动拆分,按段落边界分割
@@ -483,12 +503,13 @@ export class FeishuChannel {
483
503
  if (content.length > MAX_CONTENT_LENGTH) {
484
504
  logger.info(`[Feishu] Message too long (${content.length} chars), splitting into parts`);
485
505
  const parts = splitLongMessage(content, MAX_CONTENT_LENGTH);
506
+ const messageIds = [];
486
507
  for (let i = 0; i < parts.length; i++) {
487
508
  // 首条消息保留 reply 选项,后续消息不再 reply
488
509
  const partOptions = i === 0 ? options : { ...options, replyToMessageId: undefined };
489
- await this.sendMessage(chatId, parts[i], partOptions);
510
+ messageIds.push(...await this.sendMessage(chatId, parts[i], partOptions));
490
511
  }
491
- return;
512
+ return messageIds;
492
513
  }
493
514
  logger.debug(`[Feishu] sendMessage called, chatId: ${chatId}, content length: ${content.length}`);
494
515
  try {
@@ -511,6 +532,7 @@ export class FeishuChannel {
511
532
  else {
512
533
  msgContent = JSON.stringify({ text: content });
513
534
  }
535
+ let messageId;
514
536
  if (options?.replyToMessageId) {
515
537
  const replyData = { msg_type: msgType, content: msgContent };
516
538
  if (options.replyInThread) {
@@ -520,6 +542,7 @@ export class FeishuChannel {
520
542
  path: { message_id: options.replyToMessageId },
521
543
  data: replyData
522
544
  });
545
+ messageId = replyRes?.data?.message_id;
523
546
  if (options.replyInThread && options.onThreadCreated) {
524
547
  const newThreadId = replyRes?.data?.thread_id;
525
548
  if (newThreadId)
@@ -527,12 +550,16 @@ export class FeishuChannel {
527
550
  }
528
551
  }
529
552
  else {
530
- await this.client.im.message.create({
553
+ const createRes = await this.client.im.message.create({
531
554
  params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
532
555
  data: { receive_id: chatId, msg_type: msgType, content: msgContent }
533
556
  });
557
+ messageId = createRes?.data?.message_id;
534
558
  }
559
+ if (!messageId)
560
+ throw new Error('Feishu send succeeded without message_id');
535
561
  logger.debug(`[Feishu] Sent message as ${useMarkdown ? 'post (Markdown)' : 'text'}`);
562
+ return [messageId];
536
563
  }
537
564
  catch (error) {
538
565
  // 230011: 消息已被撤回,降级为普通消息重试
@@ -560,7 +587,7 @@ export class FeishuChannel {
560
587
  }
561
588
  async sendFile(chatId, filePath, options) {
562
589
  if (!this.client)
563
- return;
590
+ throw new Error('Feishu client is not connected');
564
591
  try {
565
592
  // 检测是否为图片,是则走 sendImage(内联预览)而非文件卡片
566
593
  // 读取足够字节供 file-type 解析(ZIP-based 格式如 PPTX 需要更多字节)
@@ -585,24 +612,25 @@ export class FeishuChannel {
585
612
  }
586
613
  });
587
614
  if (!uploadResponse || !uploadResponse.file_key) {
588
- logger.error('[Feishu] File upload failed: no file_key returned');
589
- return;
615
+ throw new Error('Feishu file upload failed: no file_key returned');
590
616
  }
591
617
  const fileKey = uploadResponse.file_key;
592
618
  const msgContent = JSON.stringify({ file_key: fileKey });
593
619
  logger.info('[Feishu] File uploaded, file_key:', fileKey);
620
+ let messageId;
594
621
  if (options?.replyToMessageId) {
595
622
  const replyData = { msg_type: 'file', content: msgContent };
596
623
  if (options.replyInThread) {
597
624
  replyData.reply_in_thread = true;
598
625
  }
599
- await this.client.im.message.reply({
626
+ const replyRes = await this.client.im.message.reply({
600
627
  path: { message_id: options.replyToMessageId },
601
628
  data: replyData
602
629
  });
630
+ messageId = replyRes?.data?.message_id;
603
631
  }
604
632
  else {
605
- await this.client.im.message.create({
633
+ const createRes = await this.client.im.message.create({
606
634
  params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
607
635
  data: {
608
636
  receive_id: chatId,
@@ -610,8 +638,12 @@ export class FeishuChannel {
610
638
  content: msgContent
611
639
  }
612
640
  });
641
+ messageId = createRes?.data?.message_id;
613
642
  }
643
+ if (!messageId)
644
+ throw new Error('Feishu file send succeeded without message_id');
614
645
  logger.info('[Feishu] File message sent successfully');
646
+ return [messageId];
615
647
  }
616
648
  catch (error) {
617
649
  // 230011/99992354: reply target invalid, retry without reply
@@ -626,7 +658,7 @@ export class FeishuChannel {
626
658
  }
627
659
  async sendImage(chatId, png, options) {
628
660
  if (!this.client)
629
- return;
661
+ throw new Error('Feishu client is not connected');
630
662
  try {
631
663
  const uploadResponse = await this.client.im.image.create({
632
664
  data: {
@@ -636,27 +668,32 @@ export class FeishuChannel {
636
668
  });
637
669
  const imageKey = uploadResponse?.image_key;
638
670
  if (!imageKey) {
639
- logger.error('[Feishu] Image upload failed: no image_key returned');
640
- return;
671
+ throw new Error('Feishu image upload failed: no image_key returned');
641
672
  }
642
673
  logger.debug('[Feishu] Image uploaded, image_key:', imageKey);
643
674
  const msgContent = JSON.stringify({ image_key: imageKey });
675
+ let messageId;
644
676
  if (options?.replyToMessageId) {
645
677
  const replyData = { msg_type: 'image', content: msgContent };
646
678
  if (options.replyInThread)
647
679
  replyData.reply_in_thread = true;
648
- await this.client.im.message.reply({
680
+ const replyRes = await this.client.im.message.reply({
649
681
  path: { message_id: options.replyToMessageId },
650
682
  data: replyData
651
683
  });
684
+ messageId = replyRes?.data?.message_id;
652
685
  }
653
686
  else {
654
- await this.client.im.message.create({
687
+ const createRes = await this.client.im.message.create({
655
688
  params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
656
689
  data: { receive_id: chatId, msg_type: 'image', content: msgContent }
657
690
  });
691
+ messageId = createRes?.data?.message_id;
658
692
  }
693
+ if (!messageId)
694
+ throw new Error('Feishu image send succeeded without message_id');
659
695
  logger.debug('[Feishu] Image message sent successfully');
696
+ return [messageId];
660
697
  }
661
698
  catch (error) {
662
699
  // 99992354: reply target invalid — image cannot easily retry, just log
@@ -1273,6 +1310,10 @@ export class FeishuChannel {
1273
1310
  }
1274
1311
  // messageId → Pin reaction create promise(等待 reaction_id 落地,供 promoteAck 删除)
1275
1312
  pinReactions = new Map();
1313
+ // messageId → StatusInFlight reaction create promise(终态时替换)
1314
+ inFlightReactions = new Map();
1315
+ // taskId → 触发该任务的全部原始入站 messageId(批量合并时可有多条)
1316
+ taskAckMessages = new Map();
1276
1317
  /** 收到消息时添加 Pin 表情(表示"已收到,排队中") */
1277
1318
  addPinReaction(messageId) {
1278
1319
  if (!this.client)
@@ -1284,22 +1325,94 @@ export class FeishuChannel {
1284
1325
  .catch(() => undefined);
1285
1326
  this.pinReactions.set(messageId, p);
1286
1327
  }
1287
- /** Runner 开始执行时:添加 CheckMark,然后异步移除 Pin(视觉无闪烁) */
1288
- async promoteAckReaction(messageId) {
1328
+ /** Runner 开始执行时:添加 StatusInFlight,然后移除 Pin(视觉无闪烁) */
1329
+ async promoteAckReaction(messageId, taskId, sourceMessageIds, restartResume = false) {
1330
+ const messageIds = [...new Set([...(sourceMessageIds ?? []), messageId].filter(Boolean))];
1331
+ if (taskId)
1332
+ this.taskAckMessages.set(taskId, messageIds);
1289
1333
  if (!this.client)
1290
1334
  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(() => { });
1335
+ const promotions = messageIds.map(id => {
1336
+ // 先加 StatusInFlight,再删 Pin——用户看到的是 Pin→Pin+InFlight→InFlight。
1337
+ const inFlightPending = this.client.im.messageReaction.create({
1338
+ path: { message_id: id },
1339
+ data: { reaction_type: { emoji_type: 'StatusInFlight' } },
1340
+ }).then((res) => res?.data?.reaction_id)
1341
+ .catch(() => undefined);
1342
+ this.inFlightReactions.set(id, inFlightPending);
1343
+ const pending = this.pinReactions.get(id);
1344
+ this.pinReactions.delete(id);
1345
+ return { id, pending };
1346
+ });
1347
+ // Every in-flight reaction is registered before this first await, so a
1348
+ // fast terminal event cannot race ahead and strand a merged-batch status.
1349
+ for (const { id, pending } of promotions) {
1350
+ if (restartResume)
1351
+ await this.clearRestartCrossMark(id);
1352
+ const reactionId = await pending;
1353
+ if (reactionId) {
1354
+ await this.client.im.messageReaction.delete({
1355
+ path: { message_id: id, reaction_id: reactionId },
1356
+ }).catch(() => { });
1357
+ }
1358
+ }
1359
+ }
1360
+ /**
1361
+ * A daemon restart terminates the old task with CrossMark. The reaction IDs
1362
+ * are process-local, so a resumed process must discover and remove its own
1363
+ * stale terminal reaction before it can show the new task state.
1364
+ */
1365
+ async clearRestartCrossMark(messageId) {
1366
+ const reactions = this.client?.im?.messageReaction;
1367
+ if (!reactions?.list || !reactions?.delete)
1368
+ return;
1369
+ try {
1370
+ const response = await reactions.list({
1371
+ path: { message_id: messageId },
1372
+ params: { reaction_type: 'CrossMark', page_size: 50 },
1373
+ });
1374
+ const staleReactionIds = (response?.data?.items ?? [])
1375
+ .filter((reaction) => (reaction?.reaction_type?.emoji_type === 'CrossMark'
1376
+ && reaction?.operator?.operator_type === 'app'
1377
+ && typeof reaction?.reaction_id === 'string'))
1378
+ .map((reaction) => reaction.reaction_id);
1379
+ await Promise.all(staleReactionIds.map(reactionId => (reactions.delete({ path: { message_id: messageId, reaction_id: reactionId } }).catch(() => { }))));
1380
+ }
1381
+ catch {
1382
+ // A missing reaction-list permission must not block the resumed task.
1383
+ }
1384
+ }
1385
+ /** 任务终态:用 CheckMark/CrossMark 替换 StatusInFlight。 */
1386
+ async finalizeAckReaction(taskId, terminalEmoji) {
1387
+ const messageIds = this.taskAckMessages.get(taskId);
1388
+ if (!messageIds)
1389
+ return;
1390
+ if (!this.client) {
1391
+ this.taskAckMessages.delete(taskId);
1392
+ return;
1393
+ }
1394
+ this.taskAckMessages.delete(taskId);
1395
+ for (const messageId of messageIds) {
1396
+ const inFlightPending = this.inFlightReactions.get(messageId);
1397
+ // 先加终态表情,再删处理中表情,避免视觉空窗。
1398
+ const addTerminal = () => this.client.im.messageReaction.create({
1399
+ path: { message_id: messageId },
1400
+ data: { reaction_type: { emoji_type: terminalEmoji } },
1401
+ }).then((res) => !!res?.data?.reaction_id).catch(() => false);
1402
+ let terminalAdded = await addTerminal();
1403
+ if (!terminalAdded)
1404
+ terminalAdded = await addTerminal();
1405
+ if (!terminalAdded) {
1406
+ logger.warn(`[Feishu] Failed to add terminal reaction for ${messageId}; preserving processing reaction`);
1407
+ continue;
1408
+ }
1409
+ this.inFlightReactions.delete(messageId);
1410
+ const inFlightReactionId = await inFlightPending;
1411
+ if (inFlightReactionId) {
1412
+ await this.client.im.messageReaction.delete({
1413
+ path: { message_id: messageId, reaction_id: inFlightReactionId },
1414
+ }).catch(() => { });
1415
+ }
1303
1416
  }
1304
1417
  }
1305
1418
  }
@@ -1707,46 +1820,53 @@ export class FeishuChannelPlugin {
1707
1820
  sendCtx.title = '✅ 最终回复:';
1708
1821
  if (replyCtx?.metadata?.onThreadCreated)
1709
1822
  sendCtx.onThreadCreated = replyCtx.metadata.onThreadCreated;
1710
- await channel.sendMessage(channelId, payload.text, sendCtx);
1711
- return;
1823
+ const ids = await channel.sendMessage(channelId, payload.text, sendCtx);
1824
+ return ids.length > 0
1825
+ ? sentReceipt(envelope, ids, replyCtx?.threadId)
1826
+ : suppressedReceipt(envelope, 'empty_message');
1712
1827
  }
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;
1828
+ case 'result.file': return sentReceipt(envelope, await channel.sendFile(channelId, payload.filePath, replyCtx), replyCtx?.threadId);
1829
+ case 'result.image': return sentReceipt(envelope, await channel.sendImage(channelId, payload.data, replyCtx), replyCtx?.threadId);
1719
1830
  case 'activity.batch': {
1720
1831
  const filtered = payload.items.filter((i) => !(i.kind === 'tool_result' && i.ok));
1721
1832
  const text = formatItemsAsText(filtered);
1722
1833
  if (text)
1723
- await channel.sendMessage(channelId, text, replyCtx);
1724
- return;
1834
+ return sentReceipt(envelope, await channel.sendMessage(channelId, text, replyCtx), replyCtx?.threadId);
1835
+ return suppressedReceipt(envelope, 'empty_activity');
1725
1836
  }
1726
1837
  case 'status.started':
1838
+ case 'status.queued':
1839
+ case 'status.progress':
1840
+ return suppressedReceipt(envelope, 'reaction_lifecycle');
1727
1841
  case 'status.completed':
1842
+ await channel.finalizeAckReaction(envelope.taskId, 'CheckMark');
1843
+ return suppressedReceipt(envelope, 'reaction_lifecycle');
1728
1844
  case 'status.interrupted':
1729
1845
  case 'status.timeout':
1730
- case 'status.progress': return;
1846
+ await channel.finalizeAckReaction(envelope.taskId, 'CrossMark');
1847
+ return suppressedReceipt(envelope, 'reaction_lifecycle');
1731
1848
  case 'status.requires_action':
1732
- await channel.sendMessage(channelId, '等待 owner 审批', replyCtx);
1733
- return;
1849
+ return sentReceipt(envelope, await channel.sendMessage(channelId, '等待 owner 审批', replyCtx), replyCtx?.threadId);
1734
1850
  case 'status.error':
1735
- if (payload.metadata?.message)
1736
- await channel.sendMessage(channelId, payload.metadata.message, replyCtx);
1737
- return;
1851
+ await channel.finalizeAckReaction(envelope.taskId, 'CrossMark');
1852
+ if (payload.metadata?.message) {
1853
+ return sentReceipt(envelope, await channel.sendMessage(channelId, payload.metadata.message, replyCtx), replyCtx?.threadId);
1854
+ }
1855
+ return suppressedReceipt(envelope, 'reaction_lifecycle');
1738
1856
  case 'interaction': {
1739
1857
  const sent = await channel.sendInteraction(channelId, payload.interaction, replyCtx);
1740
1858
  if (!sent)
1741
1859
  throw new Error('sendInteraction returned false');
1742
- return;
1860
+ return sentReceipt(envelope, [sent], replyCtx?.threadId);
1743
1861
  }
1744
- case 'custom': return;
1745
- default: logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
1862
+ case 'custom': return suppressedReceipt(envelope, 'unsupported_custom_payload');
1863
+ default:
1864
+ logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
1865
+ return suppressedReceipt(envelope, 'unhandled_payload');
1746
1866
  }
1747
1867
  },
1748
1868
  acknowledge: (messageId) => { channel.addPinReaction(messageId); return Promise.resolve(); },
1749
- promoteAck: (messageId) => channel.promoteAckReaction(messageId),
1869
+ promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds, context?.restartResume)),
1750
1870
  onInteraction: (callback) => channel.onInteraction(callback),
1751
1871
  onInteractionInvalidated: (callback) => channel.onInteractionInvalidated(callback),
1752
1872
  invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
@@ -1766,23 +1886,31 @@ export class FeishuChannelPlugin {
1766
1886
  return {
1767
1887
  channelType: 'feishu', adapter, channel,
1768
1888
  policy,
1769
- options: { fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g, supportsImages: true, flushDelay: inst.flushDelay },
1889
+ options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
1770
1890
  connect: () => channel.connect(),
1771
1891
  disconnect: () => channel.disconnect(),
1772
1892
  onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
1773
1893
  registerBridge(bridge, channelType) {
1774
1894
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, mentionAids, threadId, rootId, chatType, source }) => {
1895
+ const replyContext = peerId || threadId
1896
+ ? {
1897
+ ...(peerId ? { peerId } : {}),
1898
+ ...(threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : {}),
1899
+ }
1900
+ : undefined;
1775
1901
  await handler({
1776
1902
  channel: adapter.channelName, channelType, channelId: chatId, content, images,
1777
1903
  selfAID: ctx.agentName, chatType: chatType || 'private',
1778
1904
  peerId: peerId || '', peerName, messageId, mentions, mentionAids, threadId,
1779
- replyContext: threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
1905
+ replyContext,
1780
1906
  source,
1781
1907
  });
1782
- }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, {
1783
- replyToMessageId: replyContext?.replyToMessageId,
1784
- replyInThread: replyContext?.replyInThread,
1785
- }), adapter, channelType);
1908
+ }), async (channelId, text, replyContext) => {
1909
+ await channel.sendMessage(channelId, text, {
1910
+ replyToMessageId: replyContext?.replyToMessageId,
1911
+ replyInThread: replyContext?.replyInThread,
1912
+ });
1913
+ }, adapter, channelType);
1786
1914
  },
1787
1915
  };
1788
1916
  }
@@ -3,7 +3,26 @@ import { markdownToPlainText } from '../utils/markdown-to-plain-text.js';
3
3
  import { requireOptional } from '../utils/npm-ops.js';
4
4
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
5
5
  import { formatItemsAsText } from '../core/message/items-formatter.js';
6
+ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
6
7
  import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
8
+ import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
9
+ const qqbotContactBinds = new ContactBindCodeRegistry({
10
+ channelType: 'qqbot',
11
+ displayName: 'QQ',
12
+ initCommand: 'ec init qqbot',
13
+ });
14
+ export function registerPendingQQBotContactBind(req) {
15
+ return qqbotContactBinds.register(req);
16
+ }
17
+ export function handlePendingQQBotContactBindMessage(ctx) {
18
+ return handleExplicitContactBindCommand(qqbotContactBinds, ctx);
19
+ }
20
+ export function getPendingQQBotContactBind(selfAid, channelName, primaryId) {
21
+ return qqbotContactBinds.get(selfAid, channelName, primaryId);
22
+ }
23
+ export function clearPendingQQBotContactBinds() {
24
+ qqbotContactBinds.clear();
25
+ }
7
26
  // ── QQBotChannel ────────────────────────────────────────────────────────────
8
27
  export class QQBotChannel {
9
28
  agentAid;
@@ -93,6 +112,9 @@ export class QQBotChannel {
93
112
  }
94
113
  logger.info('[QQBot] Disconnected');
95
114
  }
115
+ getStatus() {
116
+ return { connected: this.connected };
117
+ }
96
118
  onMessage(handler) {
97
119
  this.messageHandler = handler;
98
120
  }
@@ -390,7 +412,7 @@ export class QQBotChannelPlugin {
390
412
  return {
391
413
  channelType: 'qqbot', adapter, channel,
392
414
  policy,
393
- options: { fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g, supportsImages: true, flushDelay: inst.flushDelay },
415
+ options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
394
416
  connect: () => channel.connect(),
395
417
  disconnect: () => channel.disconnect(),
396
418
  onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),