evolclaw 3.1.4 → 3.1.6

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 (99) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/agents/claude-runner.js +398 -161
  3. package/dist/agents/kit-renderer.js +191 -25
  4. package/dist/aun/aid/agentmd.js +75 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/group.js +2 -2
  10. package/dist/aun/msg/p2p.js +26 -2
  11. package/dist/aun/rpc/connection.js +23 -30
  12. package/dist/channels/aun.js +174 -99
  13. package/dist/channels/dingtalk.js +2 -1
  14. package/dist/channels/feishu.js +301 -199
  15. package/dist/channels/qqbot.js +2 -1
  16. package/dist/channels/wechat.js +2 -1
  17. package/dist/channels/wecom.js +2 -1
  18. package/dist/cli/agent.js +21 -16
  19. package/dist/cli/bench.js +41 -28
  20. package/dist/cli/help.js +8 -0
  21. package/dist/cli/index.js +176 -87
  22. package/dist/cli/init-channel.js +5 -1
  23. package/dist/cli/init.js +37 -21
  24. package/dist/cli/link-rules.js +1 -7
  25. package/dist/cli/model.js +549 -0
  26. package/dist/cli/net-check.js +133 -50
  27. package/dist/cli/watch-msg.js +7 -7
  28. package/dist/cli/watch-web/debug-log.js +18 -0
  29. package/dist/cli/watch-web/server.js +306 -0
  30. package/dist/cli/watch-web/sources/aid.js +63 -0
  31. package/dist/cli/watch-web/sources/msg.js +70 -0
  32. package/dist/cli/watch-web/sources/session.js +638 -0
  33. package/dist/cli/watch-web/sources/types.js +10 -0
  34. package/dist/cli/watch-web/static/app.js +546 -0
  35. package/dist/cli/watch-web/static/index.html +54 -0
  36. package/dist/cli/watch-web/static/style.css +247 -0
  37. package/dist/config-store.js +1 -22
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +261 -133
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -22
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/im-renderer.js +9 -20
  44. package/dist/core/message/message-bridge.js +13 -9
  45. package/dist/core/message/message-log.js +2 -2
  46. package/dist/core/message/message-processor.js +211 -123
  47. package/dist/core/message/stream-idle-monitor.js +21 -0
  48. package/dist/core/model/model-catalog.js +215 -0
  49. package/dist/core/model/model-scope.js +250 -0
  50. package/dist/core/relation/peer-identity.js +58 -55
  51. package/dist/core/relation/peer-key.js +16 -0
  52. package/dist/core/session/session-fs-store.js +34 -55
  53. package/dist/core/session/session-key.js +24 -0
  54. package/dist/core/session/session-manager.js +308 -251
  55. package/dist/core/session/session-mapper.js +9 -4
  56. package/dist/core/trigger/manager.js +3 -3
  57. package/dist/core/trigger/parser.js +4 -4
  58. package/dist/core/trigger/scheduler.js +22 -7
  59. package/dist/index.js +61 -7
  60. package/dist/ipc.js +23 -1
  61. package/dist/utils/error-utils.js +6 -0
  62. package/dist/utils/process-introspect.js +7 -5
  63. package/kits/docs/GUIDE.md +2 -2
  64. package/kits/docs/INDEX.md +8 -8
  65. package/kits/docs/channels/aun.md +56 -17
  66. package/kits/docs/channels/feishu.md +41 -12
  67. package/kits/docs/context-assembly.md +182 -0
  68. package/kits/docs/evolclaw/INDEX.md +43 -0
  69. package/kits/docs/evolclaw/agent.md +49 -0
  70. package/kits/docs/evolclaw/aid.md +49 -0
  71. package/kits/docs/evolclaw/ctl.md +46 -0
  72. package/kits/docs/evolclaw/group.md +89 -0
  73. package/kits/docs/evolclaw/model.md +51 -0
  74. package/kits/docs/evolclaw/msg.md +91 -0
  75. package/kits/docs/evolclaw/rpc.md +35 -0
  76. package/kits/docs/evolclaw/storage.md +49 -0
  77. package/kits/docs/venues/aun-group.md +10 -0
  78. package/kits/docs/venues/aun-private.md +10 -0
  79. package/kits/docs/venues/client-desktop.md +10 -0
  80. package/kits/docs/venues/client-mobile.md +10 -0
  81. package/kits/docs/venues/feishu-group.md +13 -0
  82. package/kits/docs/venues/feishu-private.md +9 -0
  83. package/kits/docs/venues/group.md +23 -0
  84. package/kits/docs/venues/private.md +10 -0
  85. package/kits/eck_manifest.json +81 -36
  86. package/kits/rules/01-overview.md +20 -10
  87. package/kits/rules/06-channel.md +34 -27
  88. package/kits/templates/system-fragments/baseagent.md +7 -1
  89. package/kits/templates/system-fragments/channel.md +7 -5
  90. package/kits/templates/system-fragments/commands.md +19 -0
  91. package/kits/templates/system-fragments/session.md +19 -3
  92. package/kits/templates/system-fragments/venue.md +24 -0
  93. package/package.json +10 -5
  94. package/dist/aun/aid/lifecycle-log.js +0 -33
  95. package/dist/utils/aid-lifecycle-log.js +0 -33
  96. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  97. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  98. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  99. package/kits/docs/evolclaw/tools.md +0 -25
@@ -21,6 +21,7 @@ export class FeishuChannel {
21
21
  enableRichContent;
22
22
  // chatId → 该会话内仍 pending 的交互卡片 messageId 集合,用于作废
23
23
  pendingCardsByChat = new Map();
24
+ cardMetaStore = new CardMetaStore();
24
25
  constructor(config) {
25
26
  this.config = config;
26
27
  this.enableRichContent = config.enableRichContent ?? false; // 默认关闭
@@ -282,7 +283,8 @@ export class FeishuChannel {
282
283
  else if (msg.message_type === 'merge_forward') {
283
284
  const { text: mergedText, images: mergedImages } = await this.extractMergeForwardContent(msg.message_id, msg.chat_id);
284
285
  if (mergedText) {
285
- const finalContent = quotedText + mergedText;
286
+ // 直接发送合并转发时,parent_id 指向自己,引用解析会把相同内容填入 quotedText 导致重复,丢弃
287
+ const finalContent = mergedText;
286
288
  const allImages = [...quotedImages, ...mergedImages];
287
289
  await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: allImages.length > 0 ? allImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
288
290
  }
@@ -320,76 +322,46 @@ export class FeishuChannel {
320
322
  const operatorId = data.operator?.open_id;
321
323
  const chatId = data.context?.open_chat_id || data.open_chat_id;
322
324
  const cardMessageId = data.open_message_id || data.context?.open_message_id;
323
- // ── CommandCard 分支:按钮直接触发命令 ──
324
- if (value._command) {
325
- if (value._initiator && operatorId && operatorId !== value._initiator) {
326
- return {
327
- toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
328
- };
325
+ const formValues = action.form_value || {};
326
+ const decision = routeCardAction({ value, formValues, operatorId }, this.cardMetaStore);
327
+ switch (decision.kind) {
328
+ case 'ignore':
329
+ return;
330
+ case 'reject':
331
+ return { toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' } };
332
+ case 'expired':
333
+ return { toast: { type: 'warning', content: '⚠️ 卡片已失效,请重新发起' } };
334
+ case 'show-input':
335
+ return decision.card;
336
+ case 'command': {
337
+ logger.info(`[Feishu] CommandCard trigger: command=${decision.command}, operator=${operatorId}`);
338
+ if (this.messageHandler) {
339
+ // 卡片回调不传 chatType——oc_ 前缀不区分群聊/单聊,
340
+ // 由 ensureSession 从已有 session 中继承正确的 chatType
341
+ await this.messageHandler({
342
+ channelId: chatId,
343
+ content: decision.command,
344
+ peerId: operatorId,
345
+ messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
346
+ source: 'card-trigger',
347
+ });
348
+ }
349
+ if (chatId && cardMessageId)
350
+ this.untrackPendingCard(chatId, cardMessageId);
351
+ this.cardMetaStore.markResolved(value._id);
352
+ this.cardMetaStore.cleanup(value._id);
353
+ return decision.card;
329
354
  }
330
- logger.info(`[Feishu] CommandCard trigger: command=${value._command}, operator=${operatorId}`);
331
- if (this.messageHandler) {
332
- // 卡片回调不传 chatType——oc_ 前缀不区分群聊/单聊,
333
- // ensureSession 从已有 session 中继承正确的 chatType
334
- await this.messageHandler({
335
- channelId: chatId,
336
- content: value._command,
337
- peerId: operatorId,
338
- messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
339
- source: 'card-trigger',
340
- });
355
+ case 'respond': {
356
+ logger.info(`[Feishu] Card action: id=${value._id}, action=${decision.response.action}`);
357
+ this.interactionCallback?.(decision.response);
358
+ if (chatId && cardMessageId)
359
+ this.untrackPendingCard(chatId, cardMessageId);
360
+ this.cardMetaStore.markResolved(value._id);
361
+ this.cardMetaStore.cleanup(value._id);
362
+ return decision.card;
341
363
  }
342
- // 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
343
- if (chatId && cardMessageId)
344
- this.untrackPendingCard(chatId, cardMessageId);
345
- const cardTitle = value._card_title || '操作';
346
- const btnLabel = value._btn_label || value._command;
347
- const cardBody = value._card_body || '';
348
- return this.buildResolvedCard(cardTitle, { type: 'interaction.response', id: '', action: value._command, operatorId }, cardBody, btnLabel);
349
- }
350
- // ── ActionInteraction 分支 ──
351
- const requestId = value._request_id;
352
- if (!requestId) {
353
- logger.debug('[Feishu] Card action without _request_id or _command, ignoring');
354
- return;
355
- }
356
- // initiator 校验
357
- if (value._initiator && operatorId && operatorId !== value._initiator) {
358
- return {
359
- toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
360
- };
361
364
  }
362
- // Legacy field change (non-form select_static with _field_key): ignore silently
363
- if (value._field_key) {
364
- logger.debug(`[Feishu] Legacy field change: requestId=${requestId}, field=${value._field_key}`);
365
- return;
366
- }
367
- // Form submit: `action.form_value` contains all field values from form container
368
- const formValues = action.form_value || {};
369
- const response = {
370
- type: 'interaction.response',
371
- id: requestId,
372
- action: value._action || 'submit',
373
- values: { ...formValues, ...value },
374
- operatorId,
375
- };
376
- // Remove internal fields from values
377
- delete response.values._request_id;
378
- delete response.values._action;
379
- delete response.values._initiator;
380
- delete response.values._card_title;
381
- const cardBody = value._card_body || '';
382
- delete response.values._card_body;
383
- const btnLabel = value._btn_label || '';
384
- delete response.values._btn_label;
385
- logger.info(`[Feishu] Card action: requestId=${requestId}, action=${response.action}, values=${JSON.stringify(response.values)}`);
386
- this.interactionCallback?.(response);
387
- // 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
388
- if (chatId && cardMessageId)
389
- this.untrackPendingCard(chatId, cardMessageId);
390
- // Return updated card (buttons disabled + result shown)
391
- const cardTitle = value._card_title || '操作';
392
- return this.buildResolvedCard(cardTitle, response, cardBody, btnLabel);
393
365
  }
394
366
  catch (err) {
395
367
  logger.error('[Feishu] Failed to handle card action:', err);
@@ -542,10 +514,15 @@ export class FeishuChannel {
542
514
  if (options.replyInThread) {
543
515
  replyData.reply_in_thread = true;
544
516
  }
545
- await this.client.im.message.reply({
517
+ const replyRes = await this.client.im.message.reply({
546
518
  path: { message_id: options.replyToMessageId },
547
519
  data: replyData
548
520
  });
521
+ if (options.replyInThread && options.onThreadCreated) {
522
+ const newThreadId = replyRes?.data?.thread_id;
523
+ if (newThreadId)
524
+ options.onThreadCreated(newThreadId);
525
+ }
549
526
  }
550
527
  else {
551
528
  await this.client.im.message.create({
@@ -562,21 +539,22 @@ export class FeishuChannel {
562
539
  }
563
540
  catch (error) {
564
541
  // 230011: 消息已被撤回,降级为普通消息重试
565
- if (error.response?.data?.code === 230011 && options?.replyToMessageId) {
566
- logger.warn('[Feishu] Message withdrawn (230011), retrying without reply');
542
+ // 99992354: message_id 不存在(合成 ID 或已过期),降级为普通消息
543
+ const errCode = error.response?.data?.code;
544
+ if ((errCode === 230011 || errCode === 99992354) && options?.replyToMessageId) {
545
+ logger.warn(`[Feishu] Reply target invalid (${errCode}), retrying without reply`);
567
546
  return this.sendMessage(chatId, content, { ...options, replyToMessageId: undefined });
568
547
  }
569
548
  // 230025: 消息内容超长,截断后重试
570
- if (error.response?.data?.code === 230025) {
549
+ if (errCode === 230025) {
571
550
  logger.warn(`[Feishu] Message too long (230025, ${content.length} chars), truncating`);
572
551
  const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
573
552
  return this.sendMessage(chatId, truncated, options);
574
553
  }
575
554
  const respData = error?.response?.data;
576
- const code = respData?.code;
577
555
  logger.error('[Feishu] Failed to send message:', respData ? JSON.stringify(respData) : error?.message ?? error);
578
556
  // post 格式相关错误(400/230001):降级为纯文本重试
579
- if (!options?.forceText && (error?.response?.status === 400 || code === 230001)) {
557
+ if (!options?.forceText && (error?.response?.status === 400 || errCode === 230001)) {
580
558
  logger.warn('[Feishu] Retrying as plain text (forceText)');
581
559
  return this.sendMessage(chatId, content, { ...options, forceText: true });
582
560
  }
@@ -639,9 +617,10 @@ export class FeishuChannel {
639
617
  logger.info('[Feishu] File message sent successfully');
640
618
  }
641
619
  catch (error) {
642
- // 230011: 消息已被撤回,降级为普通消息重试
643
- if (error.response?.data?.code === 230011 && options?.replyToMessageId) {
644
- logger.warn('[Feishu] Message withdrawn (230011), retrying file send without reply');
620
+ // 230011/99992354: reply target invalid, retry without reply
621
+ const errCode = error.response?.data?.code;
622
+ if ((errCode === 230011 || errCode === 99992354) && options?.replyToMessageId) {
623
+ logger.warn(`[Feishu] Reply target invalid (${errCode}), retrying file send without reply`);
645
624
  return this.sendFile(chatId, filePath);
646
625
  }
647
626
  logger.error('[Feishu] Failed to send file:', error);
@@ -683,6 +662,7 @@ export class FeishuChannel {
683
662
  logger.debug('[Feishu] Image message sent successfully');
684
663
  }
685
664
  catch (error) {
665
+ // 99992354: reply target invalid — image cannot easily retry, just log
686
666
  logger.error('[Feishu] Failed to send image:', error);
687
667
  throw error;
688
668
  }
@@ -736,13 +716,18 @@ export class FeishuChannel {
736
716
  // seenThreads 无时间戳,仅限容量(话题持久存在,不按时间清理)
737
717
  if (this.seenThreads.size > 1000)
738
718
  this.seenThreads.clear();
739
- // 重写文件,去掉过期条目
740
- if (this.config.seenMsgFile && this.seenMessages.size > 0) {
719
+ // 重写文件,去掉过期条目(仅在有记录被清理时才写)
720
+ if (cleaned > 0 && this.config.seenMsgFile) {
741
721
  try {
742
- const lines = [...this.seenMessages.entries()]
743
- .map(([id, ts]) => JSON.stringify({ id, ts }))
744
- .join('\n') + '\n';
745
- fs.writeFileSync(this.config.seenMsgFile, lines);
722
+ if (this.seenMessages.size === 0) {
723
+ fs.unlinkSync(this.config.seenMsgFile);
724
+ }
725
+ else {
726
+ const lines = [...this.seenMessages.entries()]
727
+ .map(([id, ts]) => JSON.stringify({ id, ts }))
728
+ .join('\n') + '\n';
729
+ fs.writeFileSync(this.config.seenMsgFile, lines);
730
+ }
746
731
  }
747
732
  catch { }
748
733
  }
@@ -981,12 +966,13 @@ export class FeishuChannel {
981
966
  if (!set || set.size === 0)
982
967
  return;
983
968
  const expiredCard = {
984
- config: { wide_screen_mode: true, update_multi: true },
969
+ schema: '2.0',
970
+ config: { update_multi: true },
985
971
  header: {
986
972
  template: 'grey',
987
973
  title: { tag: 'plain_text', content: '已过期' },
988
974
  },
989
- elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
975
+ body: { elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }] },
990
976
  };
991
977
  const ids = Array.from(set);
992
978
  this.pendingCardsByChat.delete(chatId);
@@ -1006,10 +992,8 @@ export class FeishuChannel {
1006
992
  async sendInteraction(chatId, interaction, options) {
1007
993
  if (!this.client)
1008
994
  return false;
1009
- const card = buildInteractionCard(interaction);
1010
- if (!card)
1011
- return false;
1012
- // 在新卡发送前作废旧卡(PATCH 为"已过期"),避免历史卡片仍可点击
995
+ // 统一路径:schema 2.0 内联发送(im.message),不走 cardkit 实体
996
+ const card = buildCardV2(interaction);
1013
997
  await this.invalidatePendingCards(chatId);
1014
998
  try {
1015
999
  let messageId;
@@ -1038,12 +1022,15 @@ export class FeishuChannel {
1038
1022
  messageId = res?.data?.message_id;
1039
1023
  }
1040
1024
  logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
1041
- if (messageId)
1025
+ if (messageId) {
1042
1026
  this.trackPendingCard(chatId, messageId);
1027
+ this.cardMetaStore.set(interaction.id, {
1028
+ interaction, chatId, messageId, resolved: false,
1029
+ });
1030
+ }
1043
1031
  return messageId || false;
1044
1032
  }
1045
1033
  catch (error) {
1046
- // 飞书 SDK 错误可能在 response.data、message 或 error 本身
1047
1034
  const respData = error?.response?.data;
1048
1035
  const detail = respData
1049
1036
  ? JSON.stringify(respData)
@@ -1051,42 +1038,10 @@ export class FeishuChannel {
1051
1038
  ? error.message
1052
1039
  : JSON.stringify(error, Object.getOwnPropertyNames(error));
1053
1040
  logger.error(`[Feishu] Failed to send interaction card (id=${interaction.id}, replyTo=${options?.replyToMessageId || 'none'}): ${detail}`);
1054
- // 同时记录卡片内容以便调试
1055
- logger.debug(`[Feishu] Card payload for ${interaction.id}: ${JSON.stringify(buildInteractionCard(interaction))}`);
1041
+ logger.debug(`[Feishu] Card payload for ${interaction.id}: ${JSON.stringify(card)}`);
1056
1042
  return false;
1057
1043
  }
1058
1044
  }
1059
- buildResolvedCard(cardTitle, response, cardBody, btnLabel) {
1060
- const action = response.action;
1061
- const labelMap = {
1062
- 'allow': '✅ 已允许',
1063
- 'always': '🔓 已设为始终允许',
1064
- 'deny': '❌ 已拒绝',
1065
- 'cancel': '取消',
1066
- };
1067
- const statusText = labelMap[action] || (btnLabel ? `✅ ${btnLabel}` : `✅ ${action}`);
1068
- const elements = [];
1069
- if (cardBody) {
1070
- elements.push({ tag: 'markdown', content: cardBody });
1071
- }
1072
- return {
1073
- toast: {
1074
- type: 'success',
1075
- content: statusText,
1076
- },
1077
- card: {
1078
- type: 'raw',
1079
- data: {
1080
- config: { wide_screen_mode: true, update_multi: true },
1081
- header: {
1082
- template: action === 'deny' ? 'red' : 'green',
1083
- title: { tag: 'plain_text', content: `${cardTitle} — ${statusText}` },
1084
- },
1085
- elements,
1086
- },
1087
- },
1088
- };
1089
- }
1090
1045
  addAckReaction(messageId) {
1091
1046
  if (!this.client)
1092
1047
  return;
@@ -1098,104 +1053,245 @@ export class FeishuChannel {
1098
1053
  }).catch(() => { });
1099
1054
  }
1100
1055
  }
1101
- // ── 交互卡片构建工具 ──
1102
- export function buildInteractionCard(interaction) {
1103
- const { kind } = interaction;
1104
- if (kind.kind === 'command-card') {
1105
- return buildCommandCardFeishu(kind, interaction.initiatorId);
1056
+ export class CardMetaStore {
1057
+ map = new Map();
1058
+ set(id, entry) {
1059
+ this.map.set(id, entry);
1060
+ }
1061
+ get(id) {
1062
+ return this.map.get(id);
1063
+ }
1064
+ markResolved(id) {
1065
+ const entry = this.map.get(id);
1066
+ if (entry)
1067
+ entry.resolved = true;
1068
+ }
1069
+ markInputShown(id) {
1070
+ const entry = this.map.get(id);
1071
+ if (entry)
1072
+ entry.inputShown = true;
1106
1073
  }
1107
- if (kind.kind === 'action') {
1108
- return buildActionCard(interaction.id, kind, interaction.initiatorId);
1074
+ cleanup(id) {
1075
+ this.map.delete(id);
1109
1076
  }
1110
- return null;
1111
1077
  }
1112
- function buildCommandCardFeishu(card, initiatorId) {
1113
- const elements = [];
1114
- if (card.body) {
1115
- elements.push({ tag: 'markdown', content: card.body });
1078
+ // ── 统一卡片构建器(schema 2.0 内联)──
1079
+ /**
1080
+ * 唯一卡片构建器。输入协议层 InteractionRequest,输出 schema 2.0 内联卡片 JSON。
1081
+ * - command-card: 按钮 value 带 { _id, _command, _initiator }
1082
+ * - action: 按钮 value 带 { _id, _action, _initiator };checkers → form+checker;
1083
+ * allowCustomInput → 「手动输入」按钮(form 容器外)
1084
+ * @param opts.showInput 展开自定义输入框(用于 _show_input 回调返回整卡)
1085
+ */
1086
+ export function buildCardV2(interaction, opts) {
1087
+ const { kind } = interaction;
1088
+ const id = interaction.id;
1089
+ const initiatorId = interaction.initiatorId;
1090
+ const title = kind.title;
1091
+ const body = kind.body;
1092
+ const formElements = [];
1093
+ // Body markdown
1094
+ if (body) {
1095
+ formElements.push({ tag: 'markdown', content: body, element_id: 'body_md' });
1096
+ }
1097
+ // Checkers (action only)
1098
+ if (kind.kind === 'action' && kind.checkers?.length) {
1099
+ kind.checkers.forEach((chk, idx) => {
1100
+ const text = chk.description ? `${chk.label} — ${chk.description}` : chk.label;
1101
+ formElements.push({
1102
+ tag: 'checker',
1103
+ name: `opt_${idx}`,
1104
+ checked: false,
1105
+ text: { tag: 'plain_text', content: text },
1106
+ element_id: `chk_${idx}`,
1107
+ });
1108
+ });
1109
+ formElements.push({ tag: 'hr', element_id: 'hr_btns' });
1116
1110
  }
1117
- // Build full card body for resolved state: original body + button labels
1118
- const btnLabels = card.buttons.map(btn => btn.label).join(' · ');
1119
- const fullCardBody = [card.body, btnLabels].filter(Boolean).join('\n\n');
1120
- const buttons = card.buttons.map(btn => {
1111
+ // Buttons
1112
+ const buttons = kind.buttons;
1113
+ buttons.forEach((btn, idx) => {
1121
1114
  const buttonEl = {
1122
1115
  tag: 'button',
1123
1116
  text: { tag: 'plain_text', content: btn.label },
1124
1117
  type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
1125
- value: {
1126
- _command: btn.command,
1127
- _initiator: initiatorId,
1128
- _card_title: card.title,
1129
- _card_body: fullCardBody,
1130
- _btn_label: btn.label,
1131
- },
1118
+ action_type: 'form_submit',
1119
+ name: `btn_${idx}`,
1120
+ element_id: `btn_${idx}`,
1121
+ value: kind.kind === 'command-card'
1122
+ ? { _id: id, _command: btn.command, _initiator: initiatorId }
1123
+ : { _id: id, _action: btn.key, _initiator: initiatorId },
1132
1124
  };
1133
- if (btn.disabled) {
1125
+ if (btn.disabled)
1134
1126
  buttonEl.disabled = true;
1135
- }
1136
1127
  if (btn.confirm) {
1137
1128
  buttonEl.confirm = {
1138
1129
  title: { tag: 'plain_text', content: btn.confirm.title },
1139
1130
  text: { tag: 'plain_text', content: btn.confirm.body },
1140
1131
  };
1141
1132
  }
1142
- return buttonEl;
1133
+ formElements.push(buttonEl);
1143
1134
  });
1144
- elements.push({ tag: 'action', actions: buttons });
1135
+ // Custom input (action only)
1136
+ const allowCustomInput = kind.kind === 'action' && kind.allowCustomInput;
1137
+ const outerElements = [];
1138
+ if (allowCustomInput && opts?.showInput) {
1139
+ // 展开态:输入框 + 提交按钮内联进 form(整卡作为回调返回值替换,规避 200810)
1140
+ formElements.push({ tag: 'hr', element_id: 'hr_input' }, {
1141
+ tag: 'input',
1142
+ name: 'custom_text',
1143
+ element_id: 'input_custom',
1144
+ placeholder: { tag: 'plain_text', content: '输入自定义回复...' },
1145
+ }, {
1146
+ tag: 'button',
1147
+ text: { tag: 'plain_text', content: '✅ 提交输入' },
1148
+ type: 'primary',
1149
+ action_type: 'form_submit',
1150
+ name: 'btn_submit_custom',
1151
+ element_id: 'btn_submit_custom',
1152
+ value: { _id: id, _action: '_custom_input', _initiator: initiatorId },
1153
+ });
1154
+ }
1155
+ else if (allowCustomInput) {
1156
+ // 初始态:「手动输入」按钮放在 form 容器外(form 内按钮须 form_submit,11310)
1157
+ outerElements.push({
1158
+ tag: 'button',
1159
+ text: { tag: 'plain_text', content: '✏️ 手动输入' },
1160
+ type: 'default',
1161
+ element_id: 'btn_show_input',
1162
+ value: { _id: id, _action: '_show_input', _initiator: initiatorId },
1163
+ });
1164
+ }
1145
1165
  return {
1146
- config: { wide_screen_mode: true, update_multi: true },
1147
- header: {
1148
- template: 'blue',
1149
- title: { tag: 'plain_text', content: card.title },
1166
+ schema: '2.0',
1167
+ config: { update_multi: true, streaming_mode: false },
1168
+ header: { title: { tag: 'plain_text', content: title }, template: 'blue' },
1169
+ body: {
1170
+ elements: [
1171
+ {
1172
+ tag: 'form',
1173
+ name: 'action_form',
1174
+ element_id: 'action_form',
1175
+ elements: formElements,
1176
+ },
1177
+ ...outerElements,
1178
+ ],
1150
1179
  },
1151
- elements,
1152
1180
  };
1153
1181
  }
1154
- export function buildActionCard(requestId, action, initiatorId) {
1155
- const elements = [];
1156
- // Body text
1157
- if (action.body) {
1158
- elements.push({ tag: 'markdown', content: action.body });
1182
+ /**
1183
+ * 唯一 resolved 终态构建器(按钮禁用 + 结果展示 + checker 勾选汇总)。
1184
+ * 作为飞书卡片回调的返回值下发,替换原卡片内容。
1185
+ */
1186
+ export function buildResolvedV2(interaction, response) {
1187
+ const action = response.action;
1188
+ const kind = interaction.kind;
1189
+ const labelMap = {
1190
+ 'allow': '✅ 已允许',
1191
+ 'always': '🔓 已设为始终允许',
1192
+ 'deny': '❌ 已拒绝',
1193
+ 'cancel': '取消',
1194
+ };
1195
+ const statusText = labelMap[action] || (/^\p{Emoji}/u.test(action) ? action : `✅ ${action}`);
1196
+ const headerTemplate = action === 'deny' ? 'red' : 'green';
1197
+ const headerTitle = `${kind.title} — ${statusText}`;
1198
+ const bodyElements = [];
1199
+ if (kind.body) {
1200
+ bodyElements.push({ tag: 'markdown', content: kind.body });
1201
+ }
1202
+ // Checker summary from interaction.kind.checkers + response.values
1203
+ if (kind.kind === 'action' && kind.checkers?.length && response.values) {
1204
+ const lines = kind.checkers.map((chk, idx) => {
1205
+ const checked = !!response.values[`opt_${idx}`];
1206
+ return `${checked ? '☑' : '☐'} ${chk.label}`;
1207
+ });
1208
+ bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
1209
+ }
1210
+ // CommandCard: 显示原有按钮列表(保留上下文)
1211
+ if (kind.kind === 'command-card' && kind.buttons?.length) {
1212
+ const lines = kind.buttons.map(btn => {
1213
+ const prefix = btn.command === action ? '✓' : '•';
1214
+ const cleanLabel = btn.label.replace(/^✓\s*/, '');
1215
+ return `${prefix} ${cleanLabel}`;
1216
+ });
1217
+ bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
1159
1218
  }
1160
- // Build full card body for resolved state: original body + button labels
1161
- const btnLabels = action.buttons.map(btn => btn.label).join(' · ');
1162
- const fullCardBody = [action.body, btnLabels].filter(Boolean).join('\n\n');
1163
- // Buttons row
1164
- const buttons = action.buttons.map(btn => {
1165
- const buttonEl = {
1166
- tag: 'button',
1167
- text: { tag: 'plain_text', content: btn.label },
1168
- type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
1169
- value: {
1170
- _request_id: requestId,
1171
- _action: btn.key,
1172
- _initiator: initiatorId,
1173
- _card_title: action.title,
1174
- _card_body: fullCardBody,
1175
- _btn_label: btn.label,
1176
- },
1177
- };
1178
- if (btn.confirm) {
1179
- buttonEl.confirm = {
1180
- title: { tag: 'plain_text', content: btn.confirm.title },
1181
- text: { tag: 'plain_text', content: btn.confirm.body },
1182
- };
1183
- }
1184
- return buttonEl;
1185
- });
1186
- elements.push({
1187
- tag: 'action',
1188
- actions: buttons,
1189
- });
1190
1219
  return {
1191
- config: { wide_screen_mode: true, update_multi: true },
1192
- header: {
1193
- template: 'blue',
1194
- title: { tag: 'plain_text', content: action.title },
1220
+ toast: { type: 'success', content: statusText },
1221
+ card: {
1222
+ type: 'raw',
1223
+ data: {
1224
+ schema: '2.0',
1225
+ config: { update_multi: true, streaming_mode: false },
1226
+ header: {
1227
+ template: headerTemplate,
1228
+ title: { tag: 'plain_text', content: headerTitle },
1229
+ },
1230
+ body: { elements: bodyElements },
1231
+ },
1195
1232
  },
1196
- elements,
1197
1233
  };
1198
1234
  }
1235
+ /**
1236
+ * 卡片回调的纯路由决策。不产生副作用(除 store.markInputShown),返回决策对象供
1237
+ * WS 回调执行器消费。元数据从 CardMetaStore 反查,value 只读 _id / _action / _command。
1238
+ */
1239
+ export function routeCardAction(input, store) {
1240
+ const { value, formValues, operatorId } = input;
1241
+ const id = value._id;
1242
+ if (!id)
1243
+ return { kind: 'ignore' };
1244
+ const entry = store.get(id);
1245
+ const interaction = entry?.interaction;
1246
+ const initiatorId = interaction?.initiatorId ?? value._initiator;
1247
+ // initiator 校验
1248
+ if (initiatorId && operatorId && operatorId !== initiatorId) {
1249
+ return { kind: 'reject' };
1250
+ }
1251
+ // command-card:按钮直接触发命令
1252
+ if (value._command) {
1253
+ const synthetic = {
1254
+ type: 'interaction.response', id, action: value._command, operatorId,
1255
+ };
1256
+ const card = interaction
1257
+ ? buildResolvedV2(interaction, synthetic)
1258
+ : buildResolvedV2({ type: 'interaction', id, channelId: '', sessionId: '',
1259
+ kind: { kind: 'command-card', title: '操作', buttons: [] } }, synthetic);
1260
+ return { kind: 'command', command: value._command, card };
1261
+ }
1262
+ // _show_input:点击「手动输入」→ 整卡替换为带输入框的版本(规避 200810)
1263
+ if (value._action === '_show_input') {
1264
+ if (!interaction)
1265
+ return { kind: 'expired' };
1266
+ store.markInputShown(id);
1267
+ return {
1268
+ kind: 'show-input',
1269
+ card: { toast: { type: 'info', content: '请在下方输入' },
1270
+ card: { type: 'raw', data: buildCardV2(interaction, { showInput: true }) } },
1271
+ };
1272
+ }
1273
+ // 普通提交 / 自定义输入提交
1274
+ const values = { ...formValues, ...value };
1275
+ for (const k of Object.keys(values)) {
1276
+ if (k.startsWith('_'))
1277
+ delete values[k];
1278
+ }
1279
+ const response = {
1280
+ type: 'interaction.response', id, action: value._action || 'submit', values, operatorId,
1281
+ };
1282
+ // _custom_input:把用户输入追加到 resolved 卡片正文
1283
+ let resolvedInteraction = interaction;
1284
+ if (interaction && response.action === '_custom_input' && formValues.custom_text) {
1285
+ const newBody = [interaction.kind.body, `**输入内容:** ${formValues.custom_text}`]
1286
+ .filter(Boolean).join('\n\n');
1287
+ resolvedInteraction = { ...interaction, kind: { ...interaction.kind, body: newBody } };
1288
+ }
1289
+ const card = resolvedInteraction
1290
+ ? buildResolvedV2(resolvedInteraction, response)
1291
+ : buildResolvedV2({ type: 'interaction', id, channelId: '', sessionId: '',
1292
+ kind: { kind: 'action', title: '操作', buttons: [] } }, response);
1293
+ return { kind: 'respond', response, card };
1294
+ }
1199
1295
  function displayWidth(str) {
1200
1296
  let width = 0;
1201
1297
  for (const ch of str) {
@@ -1327,7 +1423,7 @@ export class FeishuChannelPlugin {
1327
1423
  const adapter = {
1328
1424
  channelName: inst.name,
1329
1425
  channelKey: inst.name,
1330
- capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
1426
+ capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true, thread: true },
1331
1427
  send: async (envelope, payload) => {
1332
1428
  const ctx = envelope.replyContext;
1333
1429
  const channelId = envelope.channelId;
@@ -1341,6 +1437,8 @@ export class FeishuChannelPlugin {
1341
1437
  const sendCtx = { ...(ctx ?? {}) };
1342
1438
  if (payload.kind === 'result.text' && payload.isFinal)
1343
1439
  sendCtx.title = '✅ 最终回复:';
1440
+ if (ctx?.metadata?.onThreadCreated)
1441
+ sendCtx.onThreadCreated = ctx.metadata.onThreadCreated;
1344
1442
  await channel.sendMessage(channelId, payload.text, sendCtx);
1345
1443
  return;
1346
1444
  }
@@ -1367,9 +1465,12 @@ export class FeishuChannelPlugin {
1367
1465
  case 'status.progress':
1368
1466
  // Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
1369
1467
  return;
1370
- case 'interaction':
1371
- await channel.sendInteraction(channelId, payload.interaction, ctx);
1468
+ case 'interaction': {
1469
+ const sent = await channel.sendInteraction(channelId, payload.interaction, ctx);
1470
+ if (!sent)
1471
+ throw new Error('sendInteraction returned false');
1372
1472
  return;
1473
+ }
1373
1474
  case 'custom':
1374
1475
  // Feishu 不支持自定义 payload
1375
1476
  return;
@@ -1425,6 +1526,7 @@ export class FeishuChannelPlugin {
1425
1526
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
1426
1527
  await handler({
1427
1528
  channel: adapter.channelName, channelType, channelId: chatId, content, images,
1529
+ selfAID: inst.agentName,
1428
1530
  chatType: chatType || 'private',
1429
1531
  peerId: peerId || '', peerName, messageId, mentions, threadId,
1430
1532
  replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,