evolclaw 2.8.3 → 3.0.0

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 (105) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +102 -38
  3. package/dist/agents/codex-runner.js +11 -14
  4. package/dist/agents/gemini-runner.js +10 -12
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1051 -288
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +858 -847
  40. package/dist/core/evolagent-registry.js +191 -371
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +309 -142
  48. package/dist/core/message/message-queue.js +3 -3
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +431 -275
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -591
  92. package/dist/core/agent-registry.js +0 -450
  93. package/dist/core/evolagent-schema.js +0 -72
  94. package/dist/core/message/stream-flusher.js +0 -238
  95. package/dist/core/message/thought-emitter.js +0 -162
  96. package/dist/core/reload-hooks.js +0 -87
  97. package/dist/prompts/templates.js +0 -122
  98. package/dist/templates/skills.md +0 -66
  99. package/dist/utils/channel-fingerprint.js +0 -59
  100. package/dist/utils/error-dict.js +0 -63
  101. package/dist/utils/format.js +0 -32
  102. package/dist/utils/init.js +0 -645
  103. package/dist/utils/migrate-project.js +0 -122
  104. package/dist/utils/reload-hooks.js +0 -87
  105. package/dist/utils/stats-collector.js +0 -99
@@ -1,6 +1,7 @@
1
1
  import { logger } from '../utils/logger.js';
2
- import { requireOptional } from '../utils/init-channel.js';
3
- import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
2
+ import { requireOptional } from '../utils/npm-ops.js';
3
+ import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
4
+ import { formatItemsAsText } from '../core/message/items-formatter.js';
4
5
  // ── Webhook SSRF validation ────────────────────────────────────────────────────
5
6
  const WEBHOOK_RE = /^https:\/\/(api|oapi)\.dingtalk\.com\//;
6
7
  // ── DingtalkChannel ────────────────────────────────────────────────────────────
@@ -446,9 +447,46 @@ export class DingtalkChannelPlugin {
446
447
  });
447
448
  const adapter = {
448
449
  channelName: inst.name,
449
- sendText: (id, text) => channel.sendMessage(id, text),
450
- sendFile: (id, filePath) => channel.sendFile(id, filePath),
451
- sendImage: (id, png) => channel.sendImage(id, png),
450
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
451
+ send: async (envelope, payload) => {
452
+ const ctx = envelope.replyContext;
453
+ const channelId = envelope.channelId;
454
+ switch (payload.kind) {
455
+ case 'result.text':
456
+ case 'command.result':
457
+ case 'command.error':
458
+ case 'system.notice':
459
+ case 'system.error':
460
+ case 'result.error':
461
+ await channel.sendMessage(channelId, payload.text);
462
+ return;
463
+ case 'result.file':
464
+ await channel.sendFile(channelId, payload.filePath);
465
+ return;
466
+ case 'result.image':
467
+ await channel.sendImage(channelId, payload.data);
468
+ return;
469
+ case 'activity.batch': {
470
+ const text = formatItemsAsText(payload.items);
471
+ if (text)
472
+ await channel.sendMessage(channelId, text);
473
+ return;
474
+ }
475
+ case 'interaction':
476
+ if (payload.fallbackText)
477
+ await channel.sendMessage(channelId, payload.fallbackText);
478
+ return;
479
+ case 'status.started':
480
+ case 'status.completed':
481
+ case 'status.interrupted':
482
+ case 'status.error':
483
+ case 'status.timeout':
484
+ case 'custom':
485
+ return;
486
+ default:
487
+ logger.warn(`[DingTalk] Unhandled payload kind: ${payload.kind}`);
488
+ }
489
+ },
452
490
  };
453
491
  const policy = {
454
492
  canSwitchProject: (_chatType, identity) => identity === 'owner' || identity === 'admin',
@@ -493,6 +531,21 @@ export class DingtalkChannelPlugin {
493
531
  connect: () => channel.connect(),
494
532
  disconnect: () => channel.disconnect(),
495
533
  onProjectPathRequest: () => Promise.resolve(config.projects?.defaultPath || process.cwd()),
534
+ registerBridge(bridge, channelType) {
535
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async (event) => {
536
+ await handler({
537
+ channel: adapter.channelName,
538
+ channelType,
539
+ channelId: event.channelId,
540
+ content: event.content,
541
+ images: event.images,
542
+ chatType: event.chatType || 'private',
543
+ peerId: event.peerId || '',
544
+ peerName: event.peerName,
545
+ messageId: event.messageId,
546
+ });
547
+ }), (channelId, text) => channel.sendMessage(channelId, text), adapter, channelType);
548
+ },
496
549
  });
497
550
  }
498
551
  return result;
@@ -4,6 +4,7 @@ import imageType from 'image-type';
4
4
  import { sanitizeFileName, saveToUploads, validateImage } from '../utils/media-cache.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { hasRichContent, renderAllRichContent, checkDependencies } from '../utils/rich-content-renderer.js';
7
+ import { formatItemsAsText } from '../core/message/items-formatter.js';
7
8
  export class FeishuChannel {
8
9
  config;
9
10
  client = null;
@@ -18,6 +19,8 @@ export class FeishuChannel {
18
19
  interactionCallback;
19
20
  connected = false;
20
21
  enableRichContent;
22
+ // chatId → 该会话内仍 pending 的交互卡片 messageId 集合,用于作废
23
+ pendingCardsByChat = new Map();
21
24
  constructor(config) {
22
25
  this.config = config;
23
26
  this.enableRichContent = config.enableRichContent ?? false; // 默认关闭
@@ -40,7 +43,9 @@ export class FeishuChannel {
40
43
  if (this.config.appId.startsWith('YOUR_') || this.config.appSecret.startsWith('YOUR_')) {
41
44
  throw new Error('Feishu credentials not configured (placeholder values detected)');
42
45
  }
43
- const { requireOptional } = await import('../utils/init-channel.js');
46
+ // 加载持久化的已处理消息 ID,防止重启后 Feishu 重推同一条消息
47
+ this.loadSeenMessages();
48
+ const { requireOptional } = await import('../utils/npm-ops.js');
44
49
  const lark = await requireOptional('@larksuiteoapi/node-sdk');
45
50
  try {
46
51
  this.client = new lark.Client({
@@ -57,6 +62,18 @@ export class FeishuChannel {
57
62
  return;
58
63
  }
59
64
  this.markSeen(msg.message_id);
65
+ // 丢弃飞书服务端积压超过 5 分钟才下发的消息:上游观察到 65 分钟级延迟下发的
66
+ // 历史消息(含 /restart 这类破坏性命令),无差别接收会导致非预期重启。
67
+ // create_time 是 ms 字符串。
68
+ {
69
+ const createTimeMs = Number(msg.create_time ?? 0);
70
+ const ageMs = Date.now() - createTimeMs;
71
+ const STALE_THRESHOLD_MS = 5 * 60 * 1000;
72
+ if (createTimeMs > 0 && ageMs > STALE_THRESHOLD_MS) {
73
+ logger.warn(`[Feishu] Dropping stale message: id=${msg.message_id} type=${msg.message_type} age=${Math.round(ageMs / 1000)}s create_time=${createTimeMs}`);
74
+ return;
75
+ }
76
+ }
60
77
  if (!this.messageHandler)
61
78
  return;
62
79
  // 提取 chatType(从 SDK 事件直接获取)
@@ -278,11 +295,49 @@ export class FeishuChannel {
278
295
  if (!action?.value)
279
296
  return;
280
297
  const value = action.value;
298
+ const operatorId = data.operator?.open_id;
299
+ const chatId = data.context?.open_chat_id || data.open_chat_id;
300
+ const cardMessageId = data.open_message_id || data.context?.open_message_id;
301
+ // ── CommandCard 分支:按钮直接触发命令 ──
302
+ if (value._command) {
303
+ if (value._initiator && operatorId && operatorId !== value._initiator) {
304
+ return {
305
+ toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
306
+ };
307
+ }
308
+ logger.info(`[Feishu] CommandCard trigger: command=${value._command}, operator=${operatorId}`);
309
+ if (this.messageHandler) {
310
+ // Feishu chatId 前缀:oc_ = group chat,ou_ = private user open_id
311
+ const chatType = typeof chatId === 'string' && chatId.startsWith('oc_') ? 'group' : 'private';
312
+ await this.messageHandler({
313
+ channelId: chatId,
314
+ content: value._command,
315
+ chatType,
316
+ peerId: operatorId,
317
+ messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
318
+ source: 'card-trigger',
319
+ });
320
+ }
321
+ // 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
322
+ if (chatId && cardMessageId)
323
+ this.untrackPendingCard(chatId, cardMessageId);
324
+ const cardTitle = value._card_title || '操作';
325
+ const btnLabel = value._btn_label || value._command;
326
+ const cardBody = value._card_body || '';
327
+ return this.buildResolvedCard(cardTitle, { type: 'interaction.response', id: '', action: value._command, operatorId }, cardBody, btnLabel);
328
+ }
329
+ // ── ActionInteraction 分支 ──
281
330
  const requestId = value._request_id;
282
331
  if (!requestId) {
283
- logger.debug('[Feishu] Card action without _request_id, ignoring');
332
+ logger.debug('[Feishu] Card action without _request_id or _command, ignoring');
284
333
  return;
285
334
  }
335
+ // initiator 校验
336
+ if (value._initiator && operatorId && operatorId !== value._initiator) {
337
+ return {
338
+ toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
339
+ };
340
+ }
286
341
  // Legacy field change (non-form select_static with _field_key): ignore silently
287
342
  if (value._field_key) {
288
343
  logger.debug(`[Feishu] Legacy field change: requestId=${requestId}, field=${value._field_key}`);
@@ -295,11 +350,12 @@ export class FeishuChannel {
295
350
  id: requestId,
296
351
  action: value._action || 'submit',
297
352
  values: { ...formValues, ...value },
298
- operatorId: data.operator?.open_id,
353
+ operatorId,
299
354
  };
300
355
  // Remove internal fields from values
301
356
  delete response.values._request_id;
302
357
  delete response.values._action;
358
+ delete response.values._initiator;
303
359
  delete response.values._card_title;
304
360
  const cardBody = value._card_body || '';
305
361
  delete response.values._card_body;
@@ -307,6 +363,9 @@ export class FeishuChannel {
307
363
  delete response.values._btn_label;
308
364
  logger.info(`[Feishu] Card action: requestId=${requestId}, action=${response.action}, values=${JSON.stringify(response.values)}`);
309
365
  this.interactionCallback?.(response);
366
+ // 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
367
+ if (chatId && cardMessageId)
368
+ this.untrackPendingCard(chatId, cardMessageId);
310
369
  // Return updated card (buttons disabled + result shown)
311
370
  const cardTitle = value._card_title || '操作';
312
371
  return this.buildResolvedCard(cardTitle, response, cardBody, btnLabel);
@@ -604,7 +663,35 @@ export class FeishuChannel {
604
663
  return this.seenMessages.has(msgId);
605
664
  }
606
665
  markSeen(msgId) {
607
- this.seenMessages.set(msgId, Date.now());
666
+ const now = Date.now();
667
+ this.seenMessages.set(msgId, now);
668
+ // 持久化到文件,供重启后去重
669
+ if (this.config.seenMsgFile) {
670
+ try {
671
+ fs.appendFileSync(this.config.seenMsgFile, JSON.stringify({ id: msgId, ts: now }) + '\n');
672
+ }
673
+ catch { }
674
+ }
675
+ }
676
+ loadSeenMessages() {
677
+ if (!this.config.seenMsgFile)
678
+ return;
679
+ try {
680
+ if (!fs.existsSync(this.config.seenMsgFile))
681
+ return;
682
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
683
+ const lines = fs.readFileSync(this.config.seenMsgFile, 'utf-8').split('\n').filter(Boolean);
684
+ for (const line of lines) {
685
+ try {
686
+ const { id, ts } = JSON.parse(line);
687
+ if (ts > cutoff)
688
+ this.seenMessages.set(id, ts);
689
+ }
690
+ catch { }
691
+ }
692
+ logger.info(`[Feishu] Loaded ${this.seenMessages.size} seen message ID(s) from disk`);
693
+ }
694
+ catch { }
608
695
  }
609
696
  startCleanupTask() {
610
697
  this.cleanupInterval = setInterval(() => {
@@ -621,6 +708,16 @@ export class FeishuChannel {
621
708
  // seenThreads 无时间戳,仅限容量(话题持久存在,不按时间清理)
622
709
  if (this.seenThreads.size > 1000)
623
710
  this.seenThreads.clear();
711
+ // 重写文件,去掉过期条目
712
+ if (this.config.seenMsgFile && this.seenMessages.size > 0) {
713
+ try {
714
+ const lines = [...this.seenMessages.entries()]
715
+ .map(([id, ts]) => JSON.stringify({ id, ts }))
716
+ .join('\n') + '\n';
717
+ fs.writeFileSync(this.config.seenMsgFile, lines);
718
+ }
719
+ catch { }
720
+ }
624
721
  }, 60 * 60 * 1000);
625
722
  }
626
723
  async disconnect() {
@@ -741,12 +838,63 @@ export class FeishuChannel {
741
838
  return null;
742
839
  }
743
840
  }
841
+ /** 跟踪 pending 交互卡片,等待后续作废 */
842
+ trackPendingCard(chatId, messageId) {
843
+ let set = this.pendingCardsByChat.get(chatId);
844
+ if (!set) {
845
+ set = new Set();
846
+ this.pendingCardsByChat.set(chatId, set);
847
+ }
848
+ set.add(messageId);
849
+ }
850
+ /** 卡片已 resolved(用户点击了按钮,飞书已用回调返回值替换卡片),从作废集合移除 */
851
+ untrackPendingCard(chatId, messageId) {
852
+ const set = this.pendingCardsByChat.get(chatId);
853
+ if (!set)
854
+ return;
855
+ set.delete(messageId);
856
+ if (set.size === 0)
857
+ this.pendingCardsByChat.delete(chatId);
858
+ }
859
+ /**
860
+ * 作废 chatId 下所有未被点击的旧卡片:PATCH 为"已过期"灰色卡片。
861
+ * 卡片需在 config 中声明 update_multi: true 才能被 PATCH。
862
+ */
863
+ async invalidatePendingCards(chatId) {
864
+ const set = this.pendingCardsByChat.get(chatId);
865
+ if (!set || set.size === 0)
866
+ return;
867
+ const expiredCard = {
868
+ config: { wide_screen_mode: true, update_multi: true },
869
+ header: {
870
+ template: 'grey',
871
+ title: { tag: 'plain_text', content: '已过期' },
872
+ },
873
+ elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
874
+ };
875
+ const ids = Array.from(set);
876
+ this.pendingCardsByChat.delete(chatId);
877
+ await Promise.all(ids.map(async (msgId) => {
878
+ try {
879
+ await this.client.im.message.patch({
880
+ path: { message_id: msgId },
881
+ data: { content: JSON.stringify(expiredCard) },
882
+ });
883
+ }
884
+ catch (err) {
885
+ const detail = err?.response?.data ?? err?.message ?? err;
886
+ logger.debug(`[Feishu] Patch expired card failed (msgId=${msgId}): ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`);
887
+ }
888
+ }));
889
+ }
744
890
  async sendInteraction(chatId, interaction, options) {
745
891
  if (!this.client)
746
892
  return false;
747
893
  const card = buildInteractionCard(interaction);
748
894
  if (!card)
749
895
  return false;
896
+ // 在新卡发送前作废旧卡(PATCH 为"已过期"),避免历史卡片仍可点击
897
+ await this.invalidatePendingCards(chatId);
750
898
  try {
751
899
  let messageId;
752
900
  if (options?.replyToMessageId) {
@@ -774,6 +922,8 @@ export class FeishuChannel {
774
922
  messageId = res?.data?.message_id;
775
923
  }
776
924
  logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
925
+ if (messageId)
926
+ this.trackPendingCard(chatId, messageId);
777
927
  return messageId || false;
778
928
  }
779
929
  catch (error) {
@@ -790,19 +940,6 @@ export class FeishuChannel {
790
940
  return false;
791
941
  }
792
942
  }
793
- async patchInteractionCard(messageId, card) {
794
- if (!this.client)
795
- return;
796
- try {
797
- await this.client.im.message.patch({
798
- path: { message_id: messageId },
799
- data: { content: JSON.stringify(card) },
800
- });
801
- }
802
- catch (error) {
803
- logger.warn(`[Feishu] Failed to patch card ${messageId}:`, error?.response?.data || error?.message);
804
- }
805
- }
806
943
  buildResolvedCard(cardTitle, response, cardBody, btnLabel) {
807
944
  const action = response.action;
808
945
  const labelMap = {
@@ -812,7 +949,6 @@ export class FeishuChannel {
812
949
  'cancel': '取消',
813
950
  };
814
951
  const statusText = labelMap[action] || (btnLabel ? `✅ ${btnLabel}` : `✅ ${action}`);
815
- // Build elements: original body only
816
952
  const elements = [];
817
953
  if (cardBody) {
818
954
  elements.push({ tag: 'markdown', content: cardBody });
@@ -825,7 +961,7 @@ export class FeishuChannel {
825
961
  card: {
826
962
  type: 'raw',
827
963
  data: {
828
- config: { wide_screen_mode: true },
964
+ config: { wide_screen_mode: true, update_multi: true },
829
965
  header: {
830
966
  template: action === 'deny' ? 'red' : 'green',
831
967
  title: { tag: 'plain_text', content: `${cardTitle} — ${statusText}` },
@@ -849,12 +985,57 @@ export class FeishuChannel {
849
985
  // ── 交互卡片构建工具 ──
850
986
  export function buildInteractionCard(interaction) {
851
987
  const { kind } = interaction;
988
+ if (kind.kind === 'command-card') {
989
+ return buildCommandCardFeishu(kind, interaction.initiatorId);
990
+ }
852
991
  if (kind.kind === 'action') {
853
- return buildActionCard(interaction.id, kind);
992
+ return buildActionCard(interaction.id, kind, interaction.initiatorId);
854
993
  }
855
994
  return null;
856
995
  }
857
- export function buildActionCard(requestId, action) {
996
+ function buildCommandCardFeishu(card, initiatorId) {
997
+ const elements = [];
998
+ if (card.body) {
999
+ elements.push({ tag: 'markdown', content: card.body });
1000
+ }
1001
+ // Build full card body for resolved state: original body + button labels
1002
+ const btnLabels = card.buttons.map(btn => btn.label).join(' · ');
1003
+ const fullCardBody = [card.body, btnLabels].filter(Boolean).join('\n\n');
1004
+ const buttons = card.buttons.map(btn => {
1005
+ const buttonEl = {
1006
+ tag: 'button',
1007
+ text: { tag: 'plain_text', content: btn.label },
1008
+ type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
1009
+ value: {
1010
+ _command: btn.command,
1011
+ _initiator: initiatorId,
1012
+ _card_title: card.title,
1013
+ _card_body: fullCardBody,
1014
+ _btn_label: btn.label,
1015
+ },
1016
+ };
1017
+ if (btn.disabled) {
1018
+ buttonEl.disabled = true;
1019
+ }
1020
+ if (btn.confirm) {
1021
+ buttonEl.confirm = {
1022
+ title: { tag: 'plain_text', content: btn.confirm.title },
1023
+ text: { tag: 'plain_text', content: btn.confirm.body },
1024
+ };
1025
+ }
1026
+ return buttonEl;
1027
+ });
1028
+ elements.push({ tag: 'action', actions: buttons });
1029
+ return {
1030
+ config: { wide_screen_mode: true, update_multi: true },
1031
+ header: {
1032
+ template: 'blue',
1033
+ title: { tag: 'plain_text', content: card.title },
1034
+ },
1035
+ elements,
1036
+ };
1037
+ }
1038
+ export function buildActionCard(requestId, action, initiatorId) {
858
1039
  const elements = [];
859
1040
  // Body text
860
1041
  if (action.body) {
@@ -872,6 +1053,7 @@ export function buildActionCard(requestId, action) {
872
1053
  value: {
873
1054
  _request_id: requestId,
874
1055
  _action: btn.key,
1056
+ _initiator: initiatorId,
875
1057
  _card_title: action.title,
876
1058
  _card_body: fullCardBody,
877
1059
  _btn_label: btn.label,
@@ -890,7 +1072,7 @@ export function buildActionCard(requestId, action) {
890
1072
  actions: buttons,
891
1073
  });
892
1074
  return {
893
- config: { wide_screen_mode: true },
1075
+ config: { wide_screen_mode: true, update_multi: true },
894
1076
  header: {
895
1077
  template: 'blue',
896
1078
  title: { tag: 'plain_text', content: action.title },
@@ -999,7 +1181,8 @@ export function hasMarkdownSyntax(text) {
999
1181
  ];
1000
1182
  return markdownPatterns.some(pattern => pattern.test(text));
1001
1183
  }
1002
- import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
1184
+ import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
1185
+ import { resolvePaths } from '../paths.js';
1003
1186
  export class FeishuChannelPlugin {
1004
1187
  name = 'feishu';
1005
1188
  isEnabled(config) {
@@ -1023,16 +1206,56 @@ export class FeishuChannelPlugin {
1023
1206
  appId: inst.appId,
1024
1207
  appSecret: inst.appSecret,
1025
1208
  enableRichContent: config.enableRichContent,
1209
+ seenMsgFile: path.join(resolvePaths().dataDir, `feishu-seen-${inst.name}.jsonl`),
1026
1210
  });
1027
1211
  const adapter = {
1028
1212
  channelName: inst.name,
1029
- sendText: (id, text, context) => channel.sendMessage(id, text, context),
1030
- sendFile: (id, filePath, context) => channel.sendFile(id, filePath, context),
1031
- sendImage: (id, png, context) => channel.sendImage(id, png, context),
1032
- acknowledge: (messageId) => { channel.addAckReaction(messageId); return Promise.resolve(); },
1033
- sendInteraction: (id, interaction, context) => channel.sendInteraction(id, interaction, context),
1034
- patchInteractionCard: (messageId, card) => channel.patchInteractionCard(messageId, card),
1035
- onInteraction: (callback) => channel.onInteraction(callback),
1213
+ capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
1214
+ send: async (envelope, payload) => {
1215
+ const ctx = envelope.replyContext;
1216
+ const channelId = envelope.channelId;
1217
+ switch (payload.kind) {
1218
+ case 'result.text':
1219
+ case 'command.result':
1220
+ case 'command.error':
1221
+ case 'system.notice':
1222
+ case 'system.error':
1223
+ case 'result.error': {
1224
+ const sendCtx = { ...(ctx ?? {}) };
1225
+ if (payload.kind === 'result.text' && payload.isFinal)
1226
+ sendCtx.title = '✓ 最终回复:';
1227
+ await channel.sendMessage(channelId, payload.text, sendCtx);
1228
+ return;
1229
+ }
1230
+ case 'result.file':
1231
+ await channel.sendFile(channelId, payload.filePath, ctx);
1232
+ return;
1233
+ case 'result.image':
1234
+ await channel.sendImage(channelId, payload.data, ctx);
1235
+ return;
1236
+ case 'activity.batch': {
1237
+ const text = formatItemsAsText(payload.items);
1238
+ if (text)
1239
+ await channel.sendMessage(channelId, text, ctx);
1240
+ return;
1241
+ }
1242
+ case 'status.started':
1243
+ case 'status.completed':
1244
+ case 'status.interrupted':
1245
+ case 'status.error':
1246
+ case 'status.timeout':
1247
+ // Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
1248
+ return;
1249
+ case 'interaction':
1250
+ await channel.sendInteraction(channelId, payload.interaction, ctx);
1251
+ return;
1252
+ case 'custom':
1253
+ // Feishu 不支持自定义 payload
1254
+ return;
1255
+ default:
1256
+ logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
1257
+ }
1258
+ }, acknowledge: (messageId) => { channel.addAckReaction(messageId); return Promise.resolve(); }, onInteraction: (callback) => channel.onInteraction(callback),
1036
1259
  };
1037
1260
  const policy = {
1038
1261
  canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
@@ -1077,6 +1300,19 @@ export class FeishuChannelPlugin {
1077
1300
  connect: () => channel.connect(),
1078
1301
  disconnect: () => channel.disconnect(),
1079
1302
  onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
1303
+ registerBridge(bridge, channelType) {
1304
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
1305
+ await handler({
1306
+ channel: adapter.channelName, channelType, channelId: chatId, content, images, chatType,
1307
+ peerId: peerId || '', peerName, messageId, mentions, threadId,
1308
+ replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
1309
+ source,
1310
+ });
1311
+ }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, {
1312
+ replyToMessageId: replyContext?.replyToMessageId,
1313
+ replyInThread: replyContext?.replyInThread,
1314
+ }), adapter, channelType);
1315
+ },
1080
1316
  });
1081
1317
  }
1082
1318
  return result;
@@ -1,7 +1,8 @@
1
1
  import { logger } from '../utils/logger.js';
2
2
  import { markdownToPlainText } from '../utils/rich-content-renderer.js';
3
- import { requireOptional } from '../utils/init-channel.js';
4
- import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
3
+ import { requireOptional } from '../utils/npm-ops.js';
4
+ import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
5
+ import { formatItemsAsText } from '../core/message/items-formatter.js';
5
6
  // ── QQBotChannel ────────────────────────────────────────────────────────────
6
7
  export class QQBotChannel {
7
8
  config;
@@ -245,11 +246,11 @@ export class QQBotChannel {
245
246
  async sendImage(chatId, png) {
246
247
  if (!this.client)
247
248
  return;
249
+ const fs = await import('fs');
250
+ const path = await import('path');
251
+ const os = await import('os');
252
+ const tmpPath = path.join(os.tmpdir(), `evolclaw-qqbot-${Date.now()}.png`);
248
253
  try {
249
- const fs = await import('fs');
250
- const path = await import('path');
251
- const os = await import('os');
252
- const tmpPath = path.join(os.tmpdir(), `evolclaw-qqbot-${Date.now()}.png`);
253
254
  fs.writeFileSync(tmpPath, png);
254
255
  const chatType = this.chatTypeCache.get(chatId);
255
256
  const msgId = this.msgIdCache.get(chatId);
@@ -260,14 +261,16 @@ export class QQBotChannel {
260
261
  else {
261
262
  await this.client.sendPrivateImage(chatId, `file://${tmpPath}`, msgId);
262
263
  }
264
+ }
265
+ catch (error) {
266
+ logger.error(`[QQBot] sendImage failed for ${chatId}:`, error?.message || error);
267
+ }
268
+ finally {
263
269
  try {
264
270
  fs.unlinkSync(tmpPath);
265
271
  }
266
272
  catch { /* ignore */ }
267
273
  }
268
- catch (error) {
269
- logger.error(`[QQBot] sendImage failed for ${chatId}:`, error?.message || error);
270
- }
271
274
  }
272
275
  // ── Outbound: file ─────────────────────────────────────────────────────────
273
276
  async sendFile(chatId, filePath) {
@@ -331,9 +334,46 @@ export class QQBotChannelPlugin {
331
334
  });
332
335
  const adapter = {
333
336
  channelName: inst.name,
334
- sendText: (id, text) => channel.sendMessage(id, text),
335
- sendFile: (id, filePath) => channel.sendFile(id, filePath),
336
- sendImage: (id, png) => channel.sendImage(id, png),
337
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
338
+ send: async (envelope, payload) => {
339
+ const ctx = envelope.replyContext;
340
+ const channelId = envelope.channelId;
341
+ switch (payload.kind) {
342
+ case 'result.text':
343
+ case 'command.result':
344
+ case 'command.error':
345
+ case 'system.notice':
346
+ case 'system.error':
347
+ case 'result.error':
348
+ await channel.sendMessage(channelId, payload.text);
349
+ return;
350
+ case 'result.file':
351
+ await channel.sendFile(channelId, payload.filePath);
352
+ return;
353
+ case 'result.image':
354
+ await channel.sendImage(channelId, payload.data);
355
+ return;
356
+ case 'activity.batch': {
357
+ const text = formatItemsAsText(payload.items);
358
+ if (text)
359
+ await channel.sendMessage(channelId, text);
360
+ return;
361
+ }
362
+ case 'interaction':
363
+ if (payload.fallbackText)
364
+ await channel.sendMessage(channelId, payload.fallbackText);
365
+ return;
366
+ case 'status.started':
367
+ case 'status.completed':
368
+ case 'status.interrupted':
369
+ case 'status.error':
370
+ case 'status.timeout':
371
+ case 'custom':
372
+ return;
373
+ default:
374
+ logger.warn(`[QQBot] Unhandled payload kind: ${payload.kind}`);
375
+ }
376
+ },
337
377
  };
338
378
  const policy = {
339
379
  canSwitchProject: (_chatType, identity) => identity === 'owner' || identity === 'admin',
@@ -378,6 +418,21 @@ export class QQBotChannelPlugin {
378
418
  connect: () => channel.connect(),
379
419
  disconnect: () => channel.disconnect(),
380
420
  onProjectPathRequest: () => Promise.resolve(config.projects?.defaultPath || process.cwd()),
421
+ registerBridge(bridge, channelType) {
422
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async (event) => {
423
+ handler({
424
+ channel: adapter.channelName,
425
+ channelType,
426
+ channelId: event.channelId,
427
+ content: event.content,
428
+ images: event.images,
429
+ chatType: event.chatType || 'private',
430
+ peerId: event.peerId || '',
431
+ peerName: event.peerName,
432
+ messageId: event.messageId,
433
+ });
434
+ }), (channelId, text) => channel.sendMessage(channelId, text), adapter, channelType);
435
+ },
381
436
  });
382
437
  }
383
438
  return result;