evolclaw 3.1.4 → 3.1.5

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 (85) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agents/claude-runner.js +348 -156
  3. package/dist/agents/kit-renderer.js +176 -21
  4. package/dist/aun/aid/agentmd.js +68 -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/p2p.js +26 -2
  10. package/dist/aun/rpc/connection.js +23 -30
  11. package/dist/channels/aun.js +77 -88
  12. package/dist/channels/dingtalk.js +1 -0
  13. package/dist/channels/feishu.js +270 -190
  14. package/dist/channels/qqbot.js +1 -0
  15. package/dist/channels/wechat.js +1 -0
  16. package/dist/channels/wecom.js +1 -0
  17. package/dist/cli/agent.js +11 -5
  18. package/dist/cli/bench.js +40 -23
  19. package/dist/cli/index.js +170 -44
  20. package/dist/cli/init-channel.js +5 -1
  21. package/dist/cli/model.js +324 -0
  22. package/dist/cli/net-check.js +133 -50
  23. package/dist/cli/watch-msg.js +7 -7
  24. package/dist/cli/watch-web/debug-log.js +18 -0
  25. package/dist/cli/watch-web/server.js +306 -0
  26. package/dist/cli/watch-web/sources/aid.js +63 -0
  27. package/dist/cli/watch-web/sources/msg.js +70 -0
  28. package/dist/cli/watch-web/sources/session.js +638 -0
  29. package/dist/cli/watch-web/sources/types.js +10 -0
  30. package/dist/cli/watch-web/static/app.js +546 -0
  31. package/dist/cli/watch-web/static/index.html +54 -0
  32. package/dist/cli/watch-web/static/style.css +247 -0
  33. package/dist/core/channel-loader.js +7 -4
  34. package/dist/core/command-handler.js +81 -86
  35. package/dist/core/evolagent-registry.js +1 -1
  36. package/dist/core/evolagent.js +4 -4
  37. package/dist/core/interaction-router.js +59 -0
  38. package/dist/core/message/message-bridge.js +6 -6
  39. package/dist/core/message/message-log.js +2 -2
  40. package/dist/core/message/message-processor.js +86 -101
  41. package/dist/core/message/stream-idle-monitor.js +21 -0
  42. package/dist/core/model/model-catalog.js +215 -0
  43. package/dist/core/model/model-scope.js +250 -0
  44. package/dist/core/relation/peer-identity.js +40 -49
  45. package/dist/core/relation/peer-key.js +16 -0
  46. package/dist/core/session/session-fs-store.js +34 -55
  47. package/dist/core/session/session-key.js +24 -0
  48. package/dist/core/session/session-manager.js +308 -251
  49. package/dist/core/session/session-mapper.js +9 -4
  50. package/dist/core/trigger/manager.js +3 -3
  51. package/dist/core/trigger/scheduler.js +2 -1
  52. package/dist/index.js +6 -2
  53. package/dist/ipc.js +22 -0
  54. package/kits/docs/GUIDE.md +2 -2
  55. package/kits/docs/INDEX.md +11 -7
  56. package/kits/docs/channels/aun.md +56 -17
  57. package/kits/docs/channels/feishu.md +41 -12
  58. package/kits/docs/context-assembly.md +181 -0
  59. package/kits/docs/evolclaw/agent.md +49 -0
  60. package/kits/docs/evolclaw/aid.md +49 -0
  61. package/kits/docs/evolclaw/ctl.md +46 -0
  62. package/kits/docs/evolclaw/group.md +82 -0
  63. package/kits/docs/evolclaw/msg.md +86 -0
  64. package/kits/docs/evolclaw/rpc.md +35 -0
  65. package/kits/docs/evolclaw/storage.md +49 -0
  66. package/kits/docs/venues/aun-group.md +10 -0
  67. package/kits/docs/venues/aun-private.md +10 -0
  68. package/kits/docs/venues/client-desktop.md +10 -0
  69. package/kits/docs/venues/client-mobile.md +10 -0
  70. package/kits/docs/venues/feishu-group.md +13 -0
  71. package/kits/docs/venues/feishu-private.md +9 -0
  72. package/kits/docs/venues/group.md +11 -0
  73. package/kits/docs/venues/private.md +10 -0
  74. package/kits/eck_manifest.json +72 -36
  75. package/kits/rules/01-overview.md +20 -10
  76. package/kits/rules/06-channel.md +30 -27
  77. package/kits/templates/system-fragments/session.md +10 -3
  78. package/kits/templates/system-fragments/venue.md +9 -0
  79. package/package.json +11 -6
  80. package/dist/aun/aid/lifecycle-log.js +0 -33
  81. package/dist/utils/aid-lifecycle-log.js +0 -33
  82. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  83. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  84. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  85. 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; // 默认关闭
@@ -320,76 +321,46 @@ export class FeishuChannel {
320
321
  const operatorId = data.operator?.open_id;
321
322
  const chatId = data.context?.open_chat_id || data.open_chat_id;
322
323
  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
- };
324
+ const formValues = action.form_value || {};
325
+ const decision = routeCardAction({ value, formValues, operatorId }, this.cardMetaStore);
326
+ switch (decision.kind) {
327
+ case 'ignore':
328
+ return;
329
+ case 'reject':
330
+ return { toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' } };
331
+ case 'expired':
332
+ return { toast: { type: 'warning', content: '⚠️ 卡片已失效,请重新发起' } };
333
+ case 'show-input':
334
+ return decision.card;
335
+ case 'command': {
336
+ logger.info(`[Feishu] CommandCard trigger: command=${decision.command}, operator=${operatorId}`);
337
+ if (this.messageHandler) {
338
+ // 卡片回调不传 chatType——oc_ 前缀不区分群聊/单聊,
339
+ // 由 ensureSession 从已有 session 中继承正确的 chatType
340
+ await this.messageHandler({
341
+ channelId: chatId,
342
+ content: decision.command,
343
+ peerId: operatorId,
344
+ messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
345
+ source: 'card-trigger',
346
+ });
347
+ }
348
+ if (chatId && cardMessageId)
349
+ this.untrackPendingCard(chatId, cardMessageId);
350
+ this.cardMetaStore.markResolved(value._id);
351
+ this.cardMetaStore.cleanup(value._id);
352
+ return decision.card;
329
353
  }
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
- });
354
+ case 'respond': {
355
+ logger.info(`[Feishu] Card action: id=${value._id}, action=${decision.response.action}`);
356
+ this.interactionCallback?.(decision.response);
357
+ if (chatId && cardMessageId)
358
+ this.untrackPendingCard(chatId, cardMessageId);
359
+ this.cardMetaStore.markResolved(value._id);
360
+ this.cardMetaStore.cleanup(value._id);
361
+ return decision.card;
341
362
  }
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
363
  }
356
- // initiator 校验
357
- if (value._initiator && operatorId && operatorId !== value._initiator) {
358
- return {
359
- toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
360
- };
361
- }
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
364
  }
394
365
  catch (err) {
395
366
  logger.error('[Feishu] Failed to handle card action:', err);
@@ -562,21 +533,22 @@ export class FeishuChannel {
562
533
  }
563
534
  catch (error) {
564
535
  // 230011: 消息已被撤回,降级为普通消息重试
565
- if (error.response?.data?.code === 230011 && options?.replyToMessageId) {
566
- logger.warn('[Feishu] Message withdrawn (230011), retrying without reply');
536
+ // 99992354: message_id 不存在(合成 ID 或已过期),降级为普通消息
537
+ const errCode = error.response?.data?.code;
538
+ if ((errCode === 230011 || errCode === 99992354) && options?.replyToMessageId) {
539
+ logger.warn(`[Feishu] Reply target invalid (${errCode}), retrying without reply`);
567
540
  return this.sendMessage(chatId, content, { ...options, replyToMessageId: undefined });
568
541
  }
569
542
  // 230025: 消息内容超长,截断后重试
570
- if (error.response?.data?.code === 230025) {
543
+ if (errCode === 230025) {
571
544
  logger.warn(`[Feishu] Message too long (230025, ${content.length} chars), truncating`);
572
545
  const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
573
546
  return this.sendMessage(chatId, truncated, options);
574
547
  }
575
548
  const respData = error?.response?.data;
576
- const code = respData?.code;
577
549
  logger.error('[Feishu] Failed to send message:', respData ? JSON.stringify(respData) : error?.message ?? error);
578
550
  // post 格式相关错误(400/230001):降级为纯文本重试
579
- if (!options?.forceText && (error?.response?.status === 400 || code === 230001)) {
551
+ if (!options?.forceText && (error?.response?.status === 400 || errCode === 230001)) {
580
552
  logger.warn('[Feishu] Retrying as plain text (forceText)');
581
553
  return this.sendMessage(chatId, content, { ...options, forceText: true });
582
554
  }
@@ -639,9 +611,10 @@ export class FeishuChannel {
639
611
  logger.info('[Feishu] File message sent successfully');
640
612
  }
641
613
  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');
614
+ // 230011/99992354: reply target invalid, retry without reply
615
+ const errCode = error.response?.data?.code;
616
+ if ((errCode === 230011 || errCode === 99992354) && options?.replyToMessageId) {
617
+ logger.warn(`[Feishu] Reply target invalid (${errCode}), retrying file send without reply`);
645
618
  return this.sendFile(chatId, filePath);
646
619
  }
647
620
  logger.error('[Feishu] Failed to send file:', error);
@@ -683,6 +656,7 @@ export class FeishuChannel {
683
656
  logger.debug('[Feishu] Image message sent successfully');
684
657
  }
685
658
  catch (error) {
659
+ // 99992354: reply target invalid — image cannot easily retry, just log
686
660
  logger.error('[Feishu] Failed to send image:', error);
687
661
  throw error;
688
662
  }
@@ -981,12 +955,13 @@ export class FeishuChannel {
981
955
  if (!set || set.size === 0)
982
956
  return;
983
957
  const expiredCard = {
984
- config: { wide_screen_mode: true, update_multi: true },
958
+ schema: '2.0',
959
+ config: { update_multi: true },
985
960
  header: {
986
961
  template: 'grey',
987
962
  title: { tag: 'plain_text', content: '已过期' },
988
963
  },
989
- elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
964
+ body: { elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }] },
990
965
  };
991
966
  const ids = Array.from(set);
992
967
  this.pendingCardsByChat.delete(chatId);
@@ -1006,10 +981,8 @@ export class FeishuChannel {
1006
981
  async sendInteraction(chatId, interaction, options) {
1007
982
  if (!this.client)
1008
983
  return false;
1009
- const card = buildInteractionCard(interaction);
1010
- if (!card)
1011
- return false;
1012
- // 在新卡发送前作废旧卡(PATCH 为"已过期"),避免历史卡片仍可点击
984
+ // 统一路径:schema 2.0 内联发送(im.message),不走 cardkit 实体
985
+ const card = buildCardV2(interaction);
1013
986
  await this.invalidatePendingCards(chatId);
1014
987
  try {
1015
988
  let messageId;
@@ -1038,12 +1011,15 @@ export class FeishuChannel {
1038
1011
  messageId = res?.data?.message_id;
1039
1012
  }
1040
1013
  logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
1041
- if (messageId)
1014
+ if (messageId) {
1042
1015
  this.trackPendingCard(chatId, messageId);
1016
+ this.cardMetaStore.set(interaction.id, {
1017
+ interaction, chatId, messageId, resolved: false,
1018
+ });
1019
+ }
1043
1020
  return messageId || false;
1044
1021
  }
1045
1022
  catch (error) {
1046
- // 飞书 SDK 错误可能在 response.data、message 或 error 本身
1047
1023
  const respData = error?.response?.data;
1048
1024
  const detail = respData
1049
1025
  ? JSON.stringify(respData)
@@ -1051,42 +1027,10 @@ export class FeishuChannel {
1051
1027
  ? error.message
1052
1028
  : JSON.stringify(error, Object.getOwnPropertyNames(error));
1053
1029
  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))}`);
1030
+ logger.debug(`[Feishu] Card payload for ${interaction.id}: ${JSON.stringify(card)}`);
1056
1031
  return false;
1057
1032
  }
1058
1033
  }
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
1034
  addAckReaction(messageId) {
1091
1035
  if (!this.client)
1092
1036
  return;
@@ -1098,104 +1042,236 @@ export class FeishuChannel {
1098
1042
  }).catch(() => { });
1099
1043
  }
1100
1044
  }
1101
- // ── 交互卡片构建工具 ──
1102
- export function buildInteractionCard(interaction) {
1103
- const { kind } = interaction;
1104
- if (kind.kind === 'command-card') {
1105
- return buildCommandCardFeishu(kind, interaction.initiatorId);
1045
+ export class CardMetaStore {
1046
+ map = new Map();
1047
+ set(id, entry) {
1048
+ this.map.set(id, entry);
1049
+ }
1050
+ get(id) {
1051
+ return this.map.get(id);
1106
1052
  }
1107
- if (kind.kind === 'action') {
1108
- return buildActionCard(interaction.id, kind, interaction.initiatorId);
1053
+ markResolved(id) {
1054
+ const entry = this.map.get(id);
1055
+ if (entry)
1056
+ entry.resolved = true;
1057
+ }
1058
+ markInputShown(id) {
1059
+ const entry = this.map.get(id);
1060
+ if (entry)
1061
+ entry.inputShown = true;
1062
+ }
1063
+ cleanup(id) {
1064
+ this.map.delete(id);
1109
1065
  }
1110
- return null;
1111
1066
  }
1112
- function buildCommandCardFeishu(card, initiatorId) {
1113
- const elements = [];
1114
- if (card.body) {
1115
- elements.push({ tag: 'markdown', content: card.body });
1067
+ // ── 统一卡片构建器(schema 2.0 内联)──
1068
+ /**
1069
+ * 唯一卡片构建器。输入协议层 InteractionRequest,输出 schema 2.0 内联卡片 JSON。
1070
+ * - command-card: 按钮 value 带 { _id, _command, _initiator }
1071
+ * - action: 按钮 value 带 { _id, _action, _initiator };checkers → form+checker;
1072
+ * allowCustomInput → 「手动输入」按钮(form 容器外)
1073
+ * @param opts.showInput 展开自定义输入框(用于 _show_input 回调返回整卡)
1074
+ */
1075
+ export function buildCardV2(interaction, opts) {
1076
+ const { kind } = interaction;
1077
+ const id = interaction.id;
1078
+ const initiatorId = interaction.initiatorId;
1079
+ const title = kind.title;
1080
+ const body = kind.body;
1081
+ const formElements = [];
1082
+ // Body markdown
1083
+ if (body) {
1084
+ formElements.push({ tag: 'markdown', content: body, element_id: 'body_md' });
1116
1085
  }
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 => {
1086
+ // Checkers (action only)
1087
+ if (kind.kind === 'action' && kind.checkers?.length) {
1088
+ kind.checkers.forEach((chk, idx) => {
1089
+ const text = chk.description ? `${chk.label} ${chk.description}` : chk.label;
1090
+ formElements.push({
1091
+ tag: 'checker',
1092
+ name: `opt_${idx}`,
1093
+ checked: false,
1094
+ text: { tag: 'plain_text', content: text },
1095
+ element_id: `chk_${idx}`,
1096
+ });
1097
+ });
1098
+ formElements.push({ tag: 'hr', element_id: 'hr_btns' });
1099
+ }
1100
+ // Buttons
1101
+ const buttons = kind.buttons;
1102
+ buttons.forEach((btn, idx) => {
1121
1103
  const buttonEl = {
1122
1104
  tag: 'button',
1123
1105
  text: { tag: 'plain_text', content: btn.label },
1124
1106
  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
- },
1107
+ action_type: 'form_submit',
1108
+ name: `btn_${idx}`,
1109
+ element_id: `btn_${idx}`,
1110
+ value: kind.kind === 'command-card'
1111
+ ? { _id: id, _command: btn.command, _initiator: initiatorId }
1112
+ : { _id: id, _action: btn.key, _initiator: initiatorId },
1132
1113
  };
1133
- if (btn.disabled) {
1114
+ if (btn.disabled)
1134
1115
  buttonEl.disabled = true;
1135
- }
1136
1116
  if (btn.confirm) {
1137
1117
  buttonEl.confirm = {
1138
1118
  title: { tag: 'plain_text', content: btn.confirm.title },
1139
1119
  text: { tag: 'plain_text', content: btn.confirm.body },
1140
1120
  };
1141
1121
  }
1142
- return buttonEl;
1122
+ formElements.push(buttonEl);
1143
1123
  });
1144
- elements.push({ tag: 'action', actions: buttons });
1124
+ // Custom input (action only)
1125
+ const allowCustomInput = kind.kind === 'action' && kind.allowCustomInput;
1126
+ const outerElements = [];
1127
+ if (allowCustomInput && opts?.showInput) {
1128
+ // 展开态:输入框 + 提交按钮内联进 form(整卡作为回调返回值替换,规避 200810)
1129
+ formElements.push({ tag: 'hr', element_id: 'hr_input' }, {
1130
+ tag: 'input',
1131
+ name: 'custom_text',
1132
+ element_id: 'input_custom',
1133
+ placeholder: { tag: 'plain_text', content: '输入自定义回复...' },
1134
+ }, {
1135
+ tag: 'button',
1136
+ text: { tag: 'plain_text', content: '✅ 提交输入' },
1137
+ type: 'primary',
1138
+ action_type: 'form_submit',
1139
+ name: 'btn_submit_custom',
1140
+ element_id: 'btn_submit_custom',
1141
+ value: { _id: id, _action: '_custom_input', _initiator: initiatorId },
1142
+ });
1143
+ }
1144
+ else if (allowCustomInput) {
1145
+ // 初始态:「手动输入」按钮放在 form 容器外(form 内按钮须 form_submit,11310)
1146
+ outerElements.push({
1147
+ tag: 'button',
1148
+ text: { tag: 'plain_text', content: '✏️ 手动输入' },
1149
+ type: 'default',
1150
+ element_id: 'btn_show_input',
1151
+ value: { _id: id, _action: '_show_input', _initiator: initiatorId },
1152
+ });
1153
+ }
1145
1154
  return {
1146
- config: { wide_screen_mode: true, update_multi: true },
1147
- header: {
1148
- template: 'blue',
1149
- title: { tag: 'plain_text', content: card.title },
1155
+ schema: '2.0',
1156
+ config: { update_multi: true, streaming_mode: false },
1157
+ header: { title: { tag: 'plain_text', content: title }, template: 'blue' },
1158
+ body: {
1159
+ elements: [
1160
+ {
1161
+ tag: 'form',
1162
+ name: 'action_form',
1163
+ element_id: 'action_form',
1164
+ elements: formElements,
1165
+ },
1166
+ ...outerElements,
1167
+ ],
1150
1168
  },
1151
- elements,
1152
1169
  };
1153
1170
  }
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 });
1171
+ /**
1172
+ * 唯一 resolved 终态构建器(按钮禁用 + 结果展示 + checker 勾选汇总)。
1173
+ * 作为飞书卡片回调的返回值下发,替换原卡片内容。
1174
+ */
1175
+ export function buildResolvedV2(interaction, response) {
1176
+ const action = response.action;
1177
+ const kind = interaction.kind;
1178
+ const labelMap = {
1179
+ 'allow': '✅ 已允许',
1180
+ 'always': '🔓 已设为始终允许',
1181
+ 'deny': '❌ 已拒绝',
1182
+ 'cancel': '取消',
1183
+ };
1184
+ const statusText = labelMap[action] || (/^\p{Emoji}/u.test(action) ? action : `✅ ${action}`);
1185
+ const headerTemplate = action === 'deny' ? 'red' : 'green';
1186
+ const headerTitle = `${kind.title} — ${statusText}`;
1187
+ const bodyElements = [];
1188
+ if (kind.body) {
1189
+ bodyElements.push({ tag: 'markdown', content: kind.body });
1190
+ }
1191
+ // Checker summary from interaction.kind.checkers + response.values
1192
+ if (kind.kind === 'action' && kind.checkers?.length && response.values) {
1193
+ const lines = kind.checkers.map((chk, idx) => {
1194
+ const checked = !!response.values[`opt_${idx}`];
1195
+ return `${checked ? '☑' : '☐'} ${chk.label}`;
1196
+ });
1197
+ bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
1159
1198
  }
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
1199
  return {
1191
- config: { wide_screen_mode: true, update_multi: true },
1192
- header: {
1193
- template: 'blue',
1194
- title: { tag: 'plain_text', content: action.title },
1200
+ toast: { type: 'success', content: statusText },
1201
+ card: {
1202
+ type: 'raw',
1203
+ data: {
1204
+ schema: '2.0',
1205
+ config: { update_multi: true, streaming_mode: false },
1206
+ header: {
1207
+ template: headerTemplate,
1208
+ title: { tag: 'plain_text', content: headerTitle },
1209
+ },
1210
+ body: { elements: bodyElements },
1211
+ },
1195
1212
  },
1196
- elements,
1197
1213
  };
1198
1214
  }
1215
+ /**
1216
+ * 卡片回调的纯路由决策。不产生副作用(除 store.markInputShown),返回决策对象供
1217
+ * WS 回调执行器消费。元数据从 CardMetaStore 反查,value 只读 _id / _action / _command。
1218
+ */
1219
+ export function routeCardAction(input, store) {
1220
+ const { value, formValues, operatorId } = input;
1221
+ const id = value._id;
1222
+ if (!id)
1223
+ return { kind: 'ignore' };
1224
+ const entry = store.get(id);
1225
+ const interaction = entry?.interaction;
1226
+ const initiatorId = interaction?.initiatorId ?? value._initiator;
1227
+ // initiator 校验
1228
+ if (initiatorId && operatorId && operatorId !== initiatorId) {
1229
+ return { kind: 'reject' };
1230
+ }
1231
+ // command-card:按钮直接触发命令
1232
+ if (value._command) {
1233
+ const synthetic = {
1234
+ type: 'interaction.response', id, action: value._command, operatorId,
1235
+ };
1236
+ const card = interaction
1237
+ ? buildResolvedV2(interaction, synthetic)
1238
+ : buildResolvedV2({ type: 'interaction', id, channelId: '', sessionId: '',
1239
+ kind: { kind: 'command-card', title: '操作', buttons: [] } }, synthetic);
1240
+ return { kind: 'command', command: value._command, card };
1241
+ }
1242
+ // _show_input:点击「手动输入」→ 整卡替换为带输入框的版本(规避 200810)
1243
+ if (value._action === '_show_input') {
1244
+ if (!interaction)
1245
+ return { kind: 'expired' };
1246
+ store.markInputShown(id);
1247
+ return {
1248
+ kind: 'show-input',
1249
+ card: { toast: { type: 'info', content: '请在下方输入' },
1250
+ card: { type: 'raw', data: buildCardV2(interaction, { showInput: true }) } },
1251
+ };
1252
+ }
1253
+ // 普通提交 / 自定义输入提交
1254
+ const values = { ...formValues, ...value };
1255
+ for (const k of Object.keys(values)) {
1256
+ if (k.startsWith('_'))
1257
+ delete values[k];
1258
+ }
1259
+ const response = {
1260
+ type: 'interaction.response', id, action: value._action || 'submit', values, operatorId,
1261
+ };
1262
+ // _custom_input:把用户输入追加到 resolved 卡片正文
1263
+ let resolvedInteraction = interaction;
1264
+ if (interaction && response.action === '_custom_input' && formValues.custom_text) {
1265
+ const newBody = [interaction.kind.body, `**输入内容:** ${formValues.custom_text}`]
1266
+ .filter(Boolean).join('\n\n');
1267
+ resolvedInteraction = { ...interaction, kind: { ...interaction.kind, body: newBody } };
1268
+ }
1269
+ const card = resolvedInteraction
1270
+ ? buildResolvedV2(resolvedInteraction, response)
1271
+ : buildResolvedV2({ type: 'interaction', id, channelId: '', sessionId: '',
1272
+ kind: { kind: 'action', title: '操作', buttons: [] } }, response);
1273
+ return { kind: 'respond', response, card };
1274
+ }
1199
1275
  function displayWidth(str) {
1200
1276
  let width = 0;
1201
1277
  for (const ch of str) {
@@ -1367,9 +1443,12 @@ export class FeishuChannelPlugin {
1367
1443
  case 'status.progress':
1368
1444
  // Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
1369
1445
  return;
1370
- case 'interaction':
1371
- await channel.sendInteraction(channelId, payload.interaction, ctx);
1446
+ case 'interaction': {
1447
+ const sent = await channel.sendInteraction(channelId, payload.interaction, ctx);
1448
+ if (!sent)
1449
+ throw new Error('sendInteraction returned false');
1372
1450
  return;
1451
+ }
1373
1452
  case 'custom':
1374
1453
  // Feishu 不支持自定义 payload
1375
1454
  return;
@@ -1425,6 +1504,7 @@ export class FeishuChannelPlugin {
1425
1504
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
1426
1505
  await handler({
1427
1506
  channel: adapter.channelName, channelType, channelId: chatId, content, images,
1507
+ selfAID: inst.agentName,
1428
1508
  chatType: chatType || 'private',
1429
1509
  peerId: peerId || '', peerName, messageId, mentions, threadId,
1430
1510
  replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
@@ -427,6 +427,7 @@ export class QQBotChannelPlugin {
427
427
  channel: adapter.channelName,
428
428
  channelType,
429
429
  channelId: event.channelId,
430
+ selfAID: inst.agentName,
430
431
  content: event.content,
431
432
  images: event.images,
432
433
  chatType: event.chatType || 'private',
@@ -823,6 +823,7 @@ export class WechatChannelPlugin {
823
823
  channel: adapter.channelName,
824
824
  channelType,
825
825
  channelId,
826
+ selfAID: inst.agentName,
826
827
  content,
827
828
  images,
828
829
  chatType: chatType || 'private',
@@ -583,6 +583,7 @@ export class WecomChannelPlugin {
583
583
  channel: adapter.channelName,
584
584
  channelType,
585
585
  channelId: event.channelId,
586
+ selfAID: inst.agentName,
586
587
  content: event.content,
587
588
  images: event.images,
588
589
  chatType: event.chatType || 'private',