@openclaw/feishu 2026.7.2-beta.1 → 2026.7.2-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,16 @@
1
1
  import { _ as normalizeCommentFileType, c as encodeQuery, d as isRecord$1, f as normalizeString, g as buildFeishuCommentTarget, h as requestFeishuApi, l as extractReplyText, m as readString, o as resolveFeishuAccount, p as parseCommentContentElements, s as resolveFeishuRuntimeAccount } from "./accounts-BDoGHJDk.js";
2
2
  import { i as resolveReceiveIdType } from "./targets-BUjQ1TcA.js";
3
- import { C as resolveFeishuGroupConfig, D as resolveFeishuReplyPolicy, M as createFeishuCardInteractionEnvelope, N as decodeFeishuCardAction, O as normalizeFeishuChatType$2, S as resolveFeishuDmIngressAccess, T as resolveFeishuGroupSenderActivationIngressAccess, b as hasExplicitFeishuGroupConfig, d as chunkFeishuPostMarkdown, f as materializeFeishuPostMarkdownSoftBreaks, j as buildFeishuCardActionTextFallback, k as resolveFeishuChatType, o as resolveFeishuCardTemplate, w as resolveFeishuGroupConversationIngressAccess, x as normalizeFeishuAllowEntry } from "./send-result-CPS7AozW.js";
3
+ import { A as resolveFeishuChatType, C as resolveFeishuDmIngressAccess, E as resolveFeishuGroupSenderActivationIngressAccess, M as buildFeishuCardActionTextFallback, N as createFeishuCardInteractionEnvelope, O as resolveFeishuReplyPolicy, P as decodeFeishuCardAction, S as normalizeFeishuAllowEntry, T as resolveFeishuGroupConversationIngressAccess, d as chunkFeishuPostMarkdown, f as materializeFeishuPostMarkdownSoftBreaks, k as normalizeFeishuChatType$2, o as resolveFeishuCardTemplate, w as resolveFeishuGroupConfig, x as hasExplicitFeishuGroupConfig } from "./send-result-DfE5Fhz6.js";
4
4
  import { o as resolveConfiguredFeishuGroupSessionScope, t as buildFeishuConversationId } from "./conversation-id-BeJL-wq7.js";
5
+ import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-87BmeMpj.js";
5
6
  import { t as getFeishuRuntime } from "./runtime-C5JxBWZp.js";
6
- import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-BUC-R2Wi.js";
7
- import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-Bpe8ONK_.js";
7
+ import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-BHv8WW9i.js";
8
8
  import { t as createFeishuThreadBindingManager } from "./thread-bindings-V0bwk0A1.js";
9
9
  import { createReplyPrefixContext, evaluateSupplementalContextVisibility, getSessionEntry, normalizeAgentId as normalizeAgentId$2, resolveChannelContextVisibilityMode } from "./runtime-api.js";
10
10
  import { t as readFeishuJsonResponse } from "./json-response-CheljwGr.js";
11
- import { _ as resolveFeishuIdentityEmoji, c as parsePostContent, d as isMentionForwardRequest, f as isFeishuGroupChatType, g as normalizeFeishuExternalKey, h as shouldSuppressFeishuTextForVoiceMedia, i as sendCardFeishu, l as extractMentionTargets, m as sendMediaFeishu, n as getMessageFeishu, o as sendMessageFeishu, p as saveMessageResourceFeishu, r as listFeishuThreadMessages, s as sendStructuredCardFeishu, u as isFeishuBroadcastMention } from "./send-CpnLYueT.js";
12
- import { i as waitForAbortableDelay, r as raceWithTimeoutAndAbort } from "./probe-CAloGqgG.js";
13
- import { t as fetchBotIdentityForMonitor } from "./monitor.startup-DpZzunQn.js";
11
+ import { _ as buildFeishuMediaFallbackText, c as parsePostContent, d as isMentionForwardRequest, f as isFeishuGroupChatType, g as normalizeFeishuExternalKey, h as shouldSuppressFeishuTextForVoiceMedia, i as sendCardFeishu, l as extractMentionTargets, m as sendMediaFeishu, n as getMessageFeishu, o as sendMessageFeishu, p as saveMessageResourceFeishu, r as listFeishuThreadMessages, s as sendStructuredCardFeishu, u as isFeishuBroadcastMention, v as resolveFeishuIdentityEmoji } from "./send-DcrXbqqA.js";
12
+ import { i as waitForAbortableDelay, r as raceWithTimeoutAndAbort } from "./probe-BgboTHIn.js";
13
+ import { t as fetchBotIdentityForMonitor } from "./monitor.startup-Cy8NPGAX.js";
14
14
  import { createChannelMessageReplyPipeline, formatChannelProgressDraftLineForEntry, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelPreviewStreamMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
15
15
  import { createChannelPairingController, createChannelPairingController as createChannelPairingController$1 } from "openclaw/plugin-sdk/channel-pairing";
16
16
  import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
@@ -22,20 +22,22 @@ import fs from "node:fs";
22
22
  import os from "node:os";
23
23
  import path from "node:path";
24
24
  import { normalizeAccountId, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
25
+ import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
25
26
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
26
27
  import * as Lark from "@larksuiteoapi/node-sdk";
27
28
  import { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, applyBasicWebhookRequestGuards, createFixedWindowRateLimiter, createWebhookAnomalyTracker, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
28
29
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
29
- import { resolveSendableOutboundReplyParts, resolveTextChunksWithFallback, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
30
+ import { getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts, resolveTextChunksWithFallback, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
30
31
  import { resolvePinnedMainDmOwnerFromAllowlist, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
31
32
  import * as crypto$1 from "node:crypto";
32
33
  import crypto, { createHash } from "node:crypto";
33
- import { buildChannelInboundEventContext, formatInboundMediaUnavailableText, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
34
+ import { buildChannelInboundEventContext, formatAgentEnvelope, formatInboundMediaUnavailableText, recordChannelBotPairLoopAndCheckSuppression, resolveEnvelopeFormatOptions, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
34
35
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
35
36
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
36
- import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
37
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
38
+ import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
37
39
  import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
38
- import { formatReasoningMessage } from "openclaw/plugin-sdk/agent-runtime";
40
+ import { formatReasoningMessage, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
39
41
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
40
42
  import * as http from "node:http";
41
43
  import { installRequestBodyLimitGuard, readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
@@ -478,10 +480,150 @@ async function resolveGroupName(params) {
478
480
  return resolvedName;
479
481
  }
480
482
  //#endregion
483
+ //#region extensions/feishu/src/bot-name.ts
484
+ const POSITIVE_TTL_MS = 10 * 6e4;
485
+ const NEGATIVE_TTL_MS = 6e4;
486
+ const MAX_CACHE_ENTRIES = 5e3;
487
+ const BREAKER_FAILURE_THRESHOLD = 10;
488
+ const BREAKER_OPEN_MS = 60 * 6e4;
489
+ const PERMISSION_BACKOFF_MS = 6e4;
490
+ const REQUEST_TIMEOUT_MS = 1500;
491
+ const cache = /* @__PURE__ */ new Map();
492
+ const breakerByAccount = /* @__PURE__ */ new Map();
493
+ const permissionBackoffUntilByAccount = /* @__PURE__ */ new Map();
494
+ const inflight = /* @__PURE__ */ new Map();
495
+ function resolveCacheKey(accountId, openId) {
496
+ return `${accountId}::${openId}`;
497
+ }
498
+ function readCache(key) {
499
+ const entry = cache.get(key);
500
+ if (!entry) return;
501
+ if (entry.expiresAt <= Date.now()) {
502
+ cache.delete(key);
503
+ return;
504
+ }
505
+ cache.delete(key);
506
+ cache.set(key, entry);
507
+ return entry;
508
+ }
509
+ function writeCache(key, entry) {
510
+ cache.delete(key);
511
+ cache.set(key, entry);
512
+ pruneMapToMaxSize(cache, MAX_CACHE_ENTRIES);
513
+ }
514
+ function isBreakerOpen(accountId) {
515
+ const state = breakerByAccount.get(accountId);
516
+ if (!state) return false;
517
+ if (state.openUntil > Date.now()) return true;
518
+ if (state.openUntil > 0) breakerByAccount.delete(accountId);
519
+ return false;
520
+ }
521
+ function recordFailure(accountId) {
522
+ const state = breakerByAccount.get(accountId) ?? {
523
+ failures: 0,
524
+ openUntil: 0
525
+ };
526
+ state.failures += 1;
527
+ if (state.failures >= BREAKER_FAILURE_THRESHOLD) {
528
+ state.failures = 0;
529
+ state.openUntil = Date.now() + BREAKER_OPEN_MS;
530
+ }
531
+ breakerByAccount.set(accountId, state);
532
+ }
533
+ function recordSuccess(accountId) {
534
+ breakerByAccount.delete(accountId);
535
+ }
536
+ function readFeishuErrorCode(error) {
537
+ if (!error || typeof error !== "object") return;
538
+ const data = error.response?.data;
539
+ if (!data || typeof data !== "object") return;
540
+ const code = data.code;
541
+ return typeof code === "number" ? code : void 0;
542
+ }
543
+ async function requestBotName(params) {
544
+ const { account, openId, log } = params;
545
+ const query = new URLSearchParams({ bot_ids: openId });
546
+ try {
547
+ const response = await createFeishuClient(account).request({
548
+ method: "GET",
549
+ url: `/open-apis/bot/v3/bots/basic_batch?${query.toString()}`,
550
+ timeout: REQUEST_TIMEOUT_MS
551
+ });
552
+ const code = response.code ?? 0;
553
+ if (code === 0) return response;
554
+ if (code === 99991672) {
555
+ log(`feishu[${account.accountId}]: bot.basic_info scope not granted`);
556
+ return "permission";
557
+ }
558
+ log(`feishu[${account.accountId}]: bot name lookup failed (code=${code})`);
559
+ return "failure";
560
+ } catch (error) {
561
+ if (readFeishuErrorCode(error) === 99991672) {
562
+ log(`feishu[${account.accountId}]: bot.basic_info scope not granted`);
563
+ return "permission";
564
+ }
565
+ log(`feishu[${account.accountId}]: bot name lookup failed: ${String(error)}`);
566
+ return "failure";
567
+ }
568
+ }
569
+ async function resolveUncachedBotName(params) {
570
+ const { account, openId, cacheKey, log } = params;
571
+ if (isBreakerOpen(account.accountId)) {
572
+ log(`feishu[${account.accountId}]: bot name lookup skipped (breaker open)`);
573
+ return;
574
+ }
575
+ const result = await requestBotName({
576
+ account,
577
+ openId,
578
+ log
579
+ });
580
+ if (result === "permission") {
581
+ permissionBackoffUntilByAccount.set(account.accountId, Date.now() + PERMISSION_BACKOFF_MS);
582
+ return;
583
+ }
584
+ if (result === "failure") {
585
+ recordFailure(account.accountId);
586
+ return;
587
+ }
588
+ recordSuccess(account.accountId);
589
+ const name = result.data?.bots?.[openId]?.name?.trim();
590
+ writeCache(cacheKey, {
591
+ ...name ? { name } : {},
592
+ expiresAt: Date.now() + (name ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS)
593
+ });
594
+ return name;
595
+ }
596
+ async function resolveFeishuBotName(params) {
597
+ const openId = params.openId.trim();
598
+ if (!params.account.configured || !openId) return;
599
+ const permissionBackoffUntil = permissionBackoffUntilByAccount.get(params.account.accountId);
600
+ if (permissionBackoffUntil !== void 0) {
601
+ if (permissionBackoffUntil > Date.now()) return;
602
+ permissionBackoffUntilByAccount.delete(params.account.accountId);
603
+ }
604
+ const key = resolveCacheKey(params.account.accountId, openId);
605
+ const cached = readCache(key);
606
+ if (cached) return cached.name;
607
+ const pending = inflight.get(key);
608
+ if (pending) return pending;
609
+ const lookup = resolveUncachedBotName({
610
+ ...params,
611
+ openId,
612
+ cacheKey: key
613
+ });
614
+ inflight.set(key, lookup);
615
+ try {
616
+ return await lookup;
617
+ } finally {
618
+ inflight.delete(key);
619
+ }
620
+ }
621
+ //#endregion
481
622
  //#region extensions/feishu/src/bot-sender-name.ts
482
623
  const IGNORED_PERMISSION_SCOPE_TOKENS = ["contact:contact.base:readonly"];
483
624
  const FEISHU_SCOPE_CORRECTIONS = { "contact:contact.base:readonly": "contact:user.base:readonly" };
484
625
  const SENDER_NAME_TTL_MS = 600 * 1e3;
626
+ const SENDER_NAME_CACHE_MAX_SIZE = 500;
485
627
  const senderNameCache = /* @__PURE__ */ new Map();
486
628
  function correctFeishuScopeInUrl(url) {
487
629
  let corrected = url;
@@ -535,10 +677,13 @@ async function resolveFeishuSenderName(params) {
535
677
  const name = user?.name ?? user?.nickname ?? user?.en_name;
536
678
  if (name) {
537
679
  const expireAt = resolveExpiresAtMsFromDurationMs(SENDER_NAME_TTL_MS);
538
- if (expireAt !== void 0) senderNameCache.set(normalizedSenderId, {
539
- name,
540
- expireAt
541
- });
680
+ if (expireAt !== void 0) {
681
+ senderNameCache.set(normalizedSenderId, {
682
+ name,
683
+ expireAt
684
+ });
685
+ pruneMapToMaxSize(senderNameCache, SENDER_NAME_CACHE_MAX_SIZE);
686
+ }
542
687
  return { name };
543
688
  }
544
689
  return {};
@@ -563,12 +708,15 @@ const DEDUP_TTL_MS = 1440 * 60 * 1e3;
563
708
  const MEMORY_MAX_SIZE = 1e3;
564
709
  const STORE_MAX_ENTRIES = 1e4;
565
710
  function createFeishuDedupeGuard() {
566
- return createClaimableDedupe({
567
- pluginId: "feishu",
568
- namespacePrefix: DEDUPE_NAMESPACE_PREFIX,
569
- ttlMs: DEDUP_TTL_MS,
570
- memoryMaxSize: MEMORY_MAX_SIZE,
571
- stateMaxEntries: STORE_MAX_ENTRIES
711
+ return createChannelReplayGuard({
712
+ dedupe: {
713
+ pluginId: "feishu",
714
+ namespacePrefix: DEDUPE_NAMESPACE_PREFIX,
715
+ ttlMs: DEDUP_TTL_MS,
716
+ memoryMaxSize: MEMORY_MAX_SIZE,
717
+ stateMaxEntries: STORE_MAX_ENTRIES
718
+ },
719
+ buildReplayKey: (messageId) => messageId
572
720
  });
573
721
  }
574
722
  const feishuDedupeState = {
@@ -590,18 +738,13 @@ function dedupeOptions(namespace, log) {
590
738
  }
591
739
  /**
592
740
  * Claims a dedupe key for exclusive handling. Duplicate (already committed)
593
- * and in-flight keys are reported; blank keys fail open as claimed so an
741
+ * and in-flight keys are reported; blank keys fail open as invalid so an
594
742
  * unidentifiable event is never suppressed.
595
743
  */
596
744
  async function claimUnprocessedFeishuMessage(params) {
597
- const key = dedupeKey(params.messageId);
598
- if (!key) return "claimed";
599
- return (await feishuDedupeState.guard.claim(key, dedupeOptions(params.namespace, params.log))).kind;
600
- }
601
- /** Drops an uncommitted claim so a failed handler can retry the message. */
602
- function releaseFeishuMessageProcessing(messageId, namespace = "global") {
603
- const key = dedupeKey(messageId);
604
- if (key) feishuDedupeState.guard.release(key, { namespace });
745
+ const claim = await feishuDedupeState.guard.claim(params.messageId, dedupeOptions(params.namespace, params.log));
746
+ if (claim.kind === "inflight") return { kind: "inflight" };
747
+ return claim;
605
748
  }
606
749
  /**
607
750
  * Claims (unless the caller already holds the claim) and commits a message.
@@ -612,26 +755,22 @@ async function finalizeFeishuMessageProcessing(params) {
612
755
  const key = dedupeKey(params.messageId);
613
756
  if (!key) return false;
614
757
  const options = dedupeOptions(params.namespace, params.log);
615
- if (!params.claimHeld && (await feishuDedupeState.guard.claim(key, options)).kind !== "claimed") return false;
616
- return await feishuDedupeState.guard.commit(key, options);
758
+ const claim = params.processingClaim ?? await feishuDedupeState.guard.claim(key, options);
759
+ if ("kind" in claim && claim.kind !== "claimed") return false;
760
+ return await ("kind" in claim ? claim.handle : claim).commit();
617
761
  }
618
762
  /** Records a handled message so restart/replay cannot dispatch it again; false when already recorded. */
619
763
  async function recordProcessedFeishuMessage(messageId, namespace = "global", log) {
620
- const key = dedupeKey(messageId);
621
- if (!key) return false;
622
- return await feishuDedupeState.guard.commit(key, dedupeOptions(namespace, log));
764
+ const claim = await feishuDedupeState.guard.claim(messageId, dedupeOptions(namespace, log));
765
+ return claim.kind === "claimed" ? await claim.handle.commit() : false;
623
766
  }
624
767
  /** Forgets a recorded message so a retryable synthetic event can be handled on redelivery. */
625
768
  async function forgetProcessedFeishuMessage(messageId, namespace = "global", log) {
626
- const key = dedupeKey(messageId);
627
- if (!key) return false;
628
- return await feishuDedupeState.guard.forget(key, dedupeOptions(namespace, log));
769
+ return await feishuDedupeState.guard.forget(messageId, dedupeOptions(namespace, log));
629
770
  }
630
771
  /** Checks recency without claiming or recording. */
631
772
  async function hasProcessedFeishuMessage(messageId, namespace = "global", log) {
632
- const key = dedupeKey(messageId);
633
- if (!key) return false;
634
- return await feishuDedupeState.guard.hasRecent(key, dedupeOptions(namespace, log));
773
+ return await feishuDedupeState.guard.hasRecent(messageId, dedupeOptions(namespace, log));
635
774
  }
636
775
  /** Loads recent persisted entries into memory at account start; returns the loaded count. */
637
776
  async function warmupDedupFromPluginState(namespace, log) {
@@ -1495,11 +1634,6 @@ function rememberStreamingStartFailure(accountId, now = Date.now()) {
1495
1634
  streamingStartBackoffUntilByAccount.set(accountId, backoffUntil);
1496
1635
  return backoffUntil;
1497
1636
  }
1498
- function formatMediaFallbackText(text, mediaUrl) {
1499
- const trimmedText = text?.trim() ?? "";
1500
- const attachmentText = `📎 ${mediaUrl}`;
1501
- return trimmedText ? `${trimmedText}\n\n${attachmentText}` : attachmentText;
1502
- }
1503
1637
  function normalizeEpochMs(timestamp) {
1504
1638
  if (!Number.isFinite(timestamp) || timestamp === void 0 || timestamp <= 0) return;
1505
1639
  return timestamp < MS_EPOCH_MIN ? timestamp * 1e3 : timestamp;
@@ -1524,7 +1658,7 @@ function resolveCardNote(agentId, identity, prefixCtx) {
1524
1658
  }
1525
1659
  function createFeishuReplyDispatcher(params) {
1526
1660
  const core = getFeishuRuntime();
1527
- const { cfg, agentId, chatId, sendTarget, replyToMessageId, typingTargetMessageId: explicitTypingTargetMessageId, skipReplyToInMessages, replyInThread, threadReply, rootId, accountId, identity, mentionTargets } = params;
1661
+ const { cfg, agentId, chatId, sendTarget, replyToMessageId, typingTargetMessageId: explicitTypingTargetMessageId, skipReplyToInMessages, replyInThread, threadReply, rootId, accountId, identity, mentionTargets, requiredMentionTargets } = params;
1528
1662
  const sendReplyToMessageId = skipReplyToInMessages ? void 0 : replyToMessageId;
1529
1663
  const typingTargetMessageId = explicitTypingTargetMessageId?.trim() || replyToMessageId;
1530
1664
  const threadReplyMode = threadReply === true;
@@ -1589,7 +1723,7 @@ function createFeishuReplyDispatcher(params) {
1589
1723
  channel: "feishu"
1590
1724
  });
1591
1725
  const renderMode = account.config?.renderMode ?? "auto";
1592
- const streamingEnabled = resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
1726
+ const streamingEnabled = !requiredMentionTargets?.length && resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
1593
1727
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1594
1728
  const coreBlockStreamingEnabled = blockStreamingEnabled === true;
1595
1729
  const reasoningPreviewEnabled = streamingEnabled && params.allowReasoningPreview === true;
@@ -1737,12 +1871,15 @@ function createFeishuReplyDispatcher(params) {
1737
1871
  limit: textChunkLimit,
1738
1872
  mode: chunkMode,
1739
1873
  firstChunkMentions: paramsLocal.firstChunkMentions,
1874
+ chunkMentions: paramsLocal.chunkMentions,
1740
1875
  initialChunks
1741
1876
  }));
1742
1877
  for (const [index, chunk] of chunks.entries()) {
1878
+ const mentions = [...paramsLocal.chunkMentions ?? [], ...index === 0 ? paramsLocal.firstChunkMentions ?? [] : []];
1743
1879
  await paramsLocal.sendChunk({
1744
1880
  chunk,
1745
- isFirst: index === 0
1881
+ isFirst: index === 0,
1882
+ mentions: mentions.length > 0 ? mentions : void 0
1746
1883
  });
1747
1884
  markVisibleReplySent();
1748
1885
  }
@@ -1771,7 +1908,8 @@ function createFeishuReplyDispatcher(params) {
1771
1908
  text: options.fallbackText,
1772
1909
  useCard: false,
1773
1910
  infoKind: "final",
1774
- sendChunk: async ({ chunk }) => {
1911
+ chunkMentions: requiredMentionTargets,
1912
+ sendChunk: async ({ chunk, mentions }) => {
1775
1913
  await sendMessageFeishu({
1776
1914
  cfg,
1777
1915
  to: sendTarget,
@@ -1779,20 +1917,25 @@ function createFeishuReplyDispatcher(params) {
1779
1917
  replyToMessageId: sendReplyToMessageId,
1780
1918
  replyInThread: effectiveReplyInThread,
1781
1919
  allowTopLevelReplyFallback,
1782
- accountId
1920
+ accountId,
1921
+ ...mentions ? { mentions } : {}
1783
1922
  });
1784
1923
  }
1785
1924
  });
1786
1925
  }
1787
1926
  },
1788
1927
  onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
1789
- const fallbackText = formatMediaFallbackText(sentFallbackText ? void 0 : options.fallbackText, mediaUrl);
1928
+ const fallbackText = await buildFeishuMediaFallbackText({
1929
+ text: sentFallbackText ? void 0 : options.fallbackText,
1930
+ mediaUrl
1931
+ });
1790
1932
  sentFallbackText = true;
1791
1933
  await sendChunkedTextReply({
1792
1934
  text: fallbackText,
1793
1935
  useCard: false,
1794
1936
  infoKind: "final",
1795
- sendChunk: async ({ chunk }) => {
1937
+ chunkMentions: requiredMentionTargets,
1938
+ sendChunk: async ({ chunk, mentions }) => {
1796
1939
  await sendMessageFeishu({
1797
1940
  cfg,
1798
1941
  to: sendTarget,
@@ -1800,7 +1943,8 @@ function createFeishuReplyDispatcher(params) {
1800
1943
  replyToMessageId: sendReplyToMessageId,
1801
1944
  replyInThread: effectiveReplyInThread,
1802
1945
  allowTopLevelReplyFallback,
1803
- accountId
1946
+ accountId,
1947
+ ...mentions ? { mentions } : {}
1804
1948
  });
1805
1949
  }
1806
1950
  });
@@ -1821,7 +1965,8 @@ function createFeishuReplyDispatcher(params) {
1821
1965
  replyToMessageId: sendReplyToMessageId,
1822
1966
  replyInThread: effectiveReplyInThread,
1823
1967
  allowTopLevelReplyFallback,
1824
- accountId
1968
+ accountId,
1969
+ ...requiredMentionTargets?.length ? { mentions: requiredMentionTargets } : {}
1825
1970
  });
1826
1971
  markVisibleReplySent();
1827
1972
  params.runtime.error?.(`feishu[${account.accountId}]: sent no-visible-reply fallback (${reason})`);
@@ -1835,10 +1980,10 @@ function createFeishuReplyDispatcher(params) {
1835
1980
  idleSideEffectsPromise = nextIdleSideEffects.catch(() => {});
1836
1981
  return nextIdleSideEffects;
1837
1982
  };
1838
- const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
1983
+ const dispatcherOptions = {
1839
1984
  responsePrefix: prefixContext.responsePrefix,
1840
1985
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
1841
- humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
1986
+ humanDelay: resolveHumanDelayConfig(cfg, agentId),
1842
1987
  silentReplyContext: {
1843
1988
  cfg,
1844
1989
  sessionKey: params.sessionKey,
@@ -1861,142 +2006,151 @@ function createFeishuReplyDispatcher(params) {
1861
2006
  if (streamingEnabled && renderMode === "card") startStreaming();
1862
2007
  await Promise.resolve(typingCallbacks?.onReplyStart?.());
1863
2008
  },
1864
- deliver: async (payload, info) => {
1865
- if (info?.kind === "final") skippedFinalReason = null;
1866
- const payloadText = payload.isReasoning && payload.text ? formatReasoningMessage(payload.text) : payload.text;
1867
- const reply = resolveSendableOutboundReplyParts({
1868
- ...payload,
1869
- text: payloadText
1870
- });
1871
- const text = info?.kind === "final" ? mergeStreamingFinalText(streamText, reply.text, payload.isError === true && hasStreamingFinalText) : reply.text;
1872
- const hasText = reply.hasText;
1873
- const hasMedia = reply.hasMedia;
1874
- const hasVoiceMedia = hasMedia && reply.mediaUrls.some((mediaUrl) => shouldSuppressFeishuTextForVoiceMedia({
1875
- mediaUrl,
1876
- ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
1877
- }));
1878
- const finalTextExceedsStreamingLimit = info?.kind === "final" && hasText && text.length > textChunkLimit;
1879
- const useStaticCard = hasText && (renderMode === "card" || info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw" || renderMode === "auto" && shouldUseCard(text));
1880
- const useStreamingCard = hasText && streamingEnabled && !finalTextExceedsStreamingLimit && (info?.kind === "final" || useStaticCard);
1881
- const finalTextWouldUseStreamingCard = info?.kind === "final" && hasText && streamingEnabled;
1882
- const useCard = useStaticCard || useStreamingCard;
1883
- const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
1884
- const skipTextForClosedStreamingFinal = info?.kind === "final" && hasText && streamingClosedForReply && !streamingCloseErroredForReply && finalTextWouldUseStreamingCard;
1885
- const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal && !skipTextForClosedStreamingFinal;
1886
- const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText || skipTextForDuplicateFinal));
1887
- if (!shouldDeliverText && !hasMedia) return;
1888
- if (shouldDiscardStreamingPreview) await discardStreamingPreview();
1889
- if (shouldDeliverText) {
1890
- if (info?.kind === "block") {
1891
- if (!useStreamingCard) {
1892
- if (coreBlockStreamingEnabled) {
1893
- const firstChunkMentions = !sentIndependentBlockText && mentionTargets?.length ? mentionTargets : void 0;
1894
- await sendChunkedTextReply({
1895
- text,
1896
- useCard: false,
1897
- infoKind: "block",
1898
- firstChunkMentions,
1899
- sendChunk: async ({ chunk, isFirst }) => {
1900
- await sendMessageFeishu({
1901
- cfg,
1902
- to: sendTarget,
1903
- text: chunk,
1904
- replyToMessageId: sendReplyToMessageId,
1905
- replyInThread: effectiveReplyInThread,
1906
- allowTopLevelReplyFallback,
1907
- accountId,
1908
- ...isFirst && firstChunkMentions ? { mentions: firstChunkMentions } : {}
1909
- });
1910
- }
1911
- });
1912
- sentIndependentBlockText = true;
1913
- if (hasMedia) await sendMediaReplies(payload);
2009
+ onIdle: () => queueIdleSideEffects(),
2010
+ onCleanup: () => {
2011
+ typingCallbacks?.onCleanup?.();
2012
+ }
2013
+ };
2014
+ const handleDeliveryError = async (error, info) => {
2015
+ streamingCloseErroredForReply = true;
2016
+ streamingClosedForReply = false;
2017
+ params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
2018
+ await queueIdleSideEffects({ markClosedForReply: false }).catch((cleanupError) => params.runtime.error?.(`feishu[${account.accountId}] reply error cleanup failed: ${String(cleanupError)}`));
2019
+ };
2020
+ return {
2021
+ dispatcherOptions,
2022
+ delivery: {
2023
+ deliver: async (payload, info) => {
2024
+ if (info?.kind === "final") skippedFinalReason = null;
2025
+ const payloadText = payload.isReasoning && payload.text ? formatReasoningMessage(payload.text) : payload.text;
2026
+ const reply = resolveSendableOutboundReplyParts({
2027
+ ...payload,
2028
+ text: payloadText
2029
+ });
2030
+ const text = info?.kind === "final" ? mergeStreamingFinalText(streamText, reply.text, payload.isError === true && hasStreamingFinalText) : reply.text;
2031
+ const hasText = reply.hasText;
2032
+ const hasMedia = reply.hasMedia;
2033
+ const ttsSupplement = getReplyPayloadTtsSupplement(payload);
2034
+ const ttsTextAlreadyVisible = ttsSupplement?.visibleTextAlreadyDelivered === true;
2035
+ const hasVoiceMedia = hasMedia && reply.mediaUrls.some((mediaUrl) => shouldSuppressFeishuTextForVoiceMedia({
2036
+ mediaUrl,
2037
+ ...payload.audioAsVoice === true ? { audioAsVoice: true } : {},
2038
+ ttsSupplement
2039
+ }));
2040
+ const finalTextExceedsStreamingLimit = info?.kind === "final" && hasText && text.length > textChunkLimit;
2041
+ const useStaticCard = hasText && (renderMode === "card" || info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw" || renderMode === "auto" && shouldUseCard(text));
2042
+ const useStreamingCard = hasText && streamingEnabled && !finalTextExceedsStreamingLimit && (info?.kind === "final" || useStaticCard);
2043
+ const finalTextWouldUseStreamingCard = info?.kind === "final" && hasText && streamingEnabled;
2044
+ const useCard = useStaticCard || useStreamingCard;
2045
+ const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
2046
+ const skipTextForClosedStreamingFinal = info?.kind === "final" && hasText && streamingClosedForReply && !streamingCloseErroredForReply && finalTextWouldUseStreamingCard;
2047
+ const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal && !skipTextForClosedStreamingFinal;
2048
+ const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText && !ttsTextAlreadyVisible || skipTextForDuplicateFinal));
2049
+ if (!shouldDeliverText && !hasMedia) return;
2050
+ if (shouldDiscardStreamingPreview) await discardStreamingPreview();
2051
+ if (shouldDeliverText) {
2052
+ if (info?.kind === "block") {
2053
+ if (!useStreamingCard) {
2054
+ if (coreBlockStreamingEnabled) {
2055
+ const firstChunkMentions = !sentIndependentBlockText && mentionTargets?.length ? mentionTargets : void 0;
2056
+ await sendChunkedTextReply({
2057
+ text,
2058
+ useCard: false,
2059
+ infoKind: "block",
2060
+ firstChunkMentions,
2061
+ chunkMentions: requiredMentionTargets,
2062
+ sendChunk: async ({ chunk, mentions }) => {
2063
+ await sendMessageFeishu({
2064
+ cfg,
2065
+ to: sendTarget,
2066
+ text: chunk,
2067
+ replyToMessageId: sendReplyToMessageId,
2068
+ replyInThread: effectiveReplyInThread,
2069
+ allowTopLevelReplyFallback,
2070
+ accountId,
2071
+ ...mentions ? { mentions } : {}
2072
+ });
2073
+ }
2074
+ });
2075
+ sentIndependentBlockText = true;
2076
+ if (hasMedia) await sendMediaReplies(payload);
2077
+ }
2078
+ return;
1914
2079
  }
1915
- return;
2080
+ startStreaming();
2081
+ if (streamingStartPromise) await streamingStartPromise;
1916
2082
  }
1917
- startStreaming();
1918
- if (streamingStartPromise) await streamingStartPromise;
1919
- }
1920
- if (info?.kind === "final" && useStreamingCard) {
1921
- startStreaming();
1922
- if (streamingStartPromise) await streamingStartPromise;
1923
- }
1924
- const shouldStreamText = info?.kind === "block" || info?.kind === "final";
1925
- if (streaming?.isActive() && shouldStreamText) {
1926
- if (info?.kind === "block") queueStreamingUpdate(text, {
1927
- mode: "delta",
1928
- dedupeWithLastPartial: true
1929
- });
1930
- if (info?.kind === "final") {
1931
- streamText = text;
1932
- hasStreamingFinalText = true;
1933
- snapshotBaseText = "";
1934
- lastSnapshotTextLength = text.length;
1935
- flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2083
+ if (info?.kind === "final" && useStreamingCard) {
2084
+ startStreaming();
2085
+ if (streamingStartPromise) await streamingStartPromise;
1936
2086
  }
1937
- if (hasMedia) await sendMediaReplies(payload);
1938
- return;
1939
- }
1940
- if (useCard) {
1941
- const cardHeader = resolveCardHeader(agentId, identity);
1942
- const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1943
- await sendChunkedTextReply({
1944
- text,
1945
- useCard: true,
1946
- infoKind: info?.kind,
1947
- sendChunk: async ({ chunk }) => {
1948
- await sendStructuredCardFeishu({
1949
- cfg,
1950
- to: sendTarget,
1951
- text: chunk,
1952
- replyToMessageId: sendReplyToMessageId,
1953
- replyInThread: effectiveReplyInThread,
1954
- allowTopLevelReplyFallback,
1955
- accountId,
1956
- header: cardHeader,
1957
- note: cardNote
1958
- });
1959
- }
1960
- });
1961
- } else {
1962
- const firstChunkMentions = info?.kind === "final" && mentionTargets?.length ? mentionTargets : void 0;
1963
- await sendChunkedTextReply({
1964
- text,
1965
- useCard: false,
1966
- infoKind: info?.kind,
1967
- firstChunkMentions,
1968
- sendChunk: async ({ chunk, isFirst }) => {
1969
- await sendMessageFeishu({
1970
- cfg,
1971
- to: sendTarget,
1972
- text: chunk,
1973
- replyToMessageId: sendReplyToMessageId,
1974
- replyInThread: effectiveReplyInThread,
1975
- allowTopLevelReplyFallback,
1976
- accountId,
1977
- ...isFirst && firstChunkMentions ? { mentions: firstChunkMentions } : {}
1978
- });
2087
+ const shouldStreamText = info?.kind === "block" || info?.kind === "final";
2088
+ if (streaming?.isActive() && shouldStreamText) {
2089
+ if (info?.kind === "block") queueStreamingUpdate(text, {
2090
+ mode: "delta",
2091
+ dedupeWithLastPartial: true
2092
+ });
2093
+ if (info?.kind === "final") {
2094
+ streamText = text;
2095
+ hasStreamingFinalText = true;
2096
+ snapshotBaseText = "";
2097
+ lastSnapshotTextLength = text.length;
2098
+ flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
1979
2099
  }
1980
- });
2100
+ if (hasMedia) await sendMediaReplies(payload);
2101
+ return;
2102
+ }
2103
+ if (useCard) {
2104
+ const cardHeader = resolveCardHeader(agentId, identity);
2105
+ const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
2106
+ await sendChunkedTextReply({
2107
+ text,
2108
+ useCard: true,
2109
+ infoKind: info?.kind,
2110
+ chunkMentions: requiredMentionTargets,
2111
+ sendChunk: async ({ chunk, mentions }) => {
2112
+ await sendStructuredCardFeishu({
2113
+ cfg,
2114
+ to: sendTarget,
2115
+ text: chunk,
2116
+ replyToMessageId: sendReplyToMessageId,
2117
+ replyInThread: effectiveReplyInThread,
2118
+ allowTopLevelReplyFallback,
2119
+ accountId,
2120
+ header: cardHeader,
2121
+ note: cardNote,
2122
+ ...mentions ? { mentions } : {}
2123
+ });
2124
+ }
2125
+ });
2126
+ } else {
2127
+ const firstChunkMentions = info?.kind === "final" && mentionTargets?.length ? mentionTargets : void 0;
2128
+ await sendChunkedTextReply({
2129
+ text,
2130
+ useCard: false,
2131
+ infoKind: info?.kind,
2132
+ firstChunkMentions,
2133
+ chunkMentions: requiredMentionTargets,
2134
+ sendChunk: async ({ chunk, mentions }) => {
2135
+ await sendMessageFeishu({
2136
+ cfg,
2137
+ to: sendTarget,
2138
+ text: chunk,
2139
+ replyToMessageId: sendReplyToMessageId,
2140
+ replyInThread: effectiveReplyInThread,
2141
+ allowTopLevelReplyFallback,
2142
+ accountId,
2143
+ ...mentions ? { mentions } : {}
2144
+ });
2145
+ }
2146
+ });
2147
+ }
1981
2148
  }
1982
- }
1983
- if (hasMedia) await sendMediaReplies(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
1984
- },
1985
- onError: async (error, info) => {
1986
- streamingCloseErroredForReply = true;
1987
- streamingClosedForReply = false;
1988
- params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
1989
- await queueIdleSideEffects({ markClosedForReply: false });
2149
+ if (hasMedia) await sendMediaReplies(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
2150
+ },
2151
+ onError: handleDeliveryError
1990
2152
  },
1991
- onIdle: () => queueIdleSideEffects(),
1992
- onCleanup: () => {
1993
- typingCallbacks?.onCleanup?.();
1994
- }
1995
- });
1996
- return {
1997
- dispatcher,
1998
2153
  replyOptions: {
1999
- ...replyOptions,
2000
2154
  onModelSelected: prefixContext.onModelSelected,
2001
2155
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : true,
2002
2156
  onPartialReply: streamingEnabled ? (payload) => {
@@ -2038,7 +2192,6 @@ function createFeishuReplyDispatcher(params) {
2038
2192
  updateStreamingStatusLine("");
2039
2193
  } : void 0
2040
2194
  },
2041
- markDispatchIdle,
2042
2195
  ensureNoVisibleReplyFallback,
2043
2196
  getVisibleReplyState: () => ({
2044
2197
  visibleReplySent,
@@ -2047,6 +2200,17 @@ function createFeishuReplyDispatcher(params) {
2047
2200
  };
2048
2201
  }
2049
2202
  //#endregion
2203
+ //#region extensions/feishu/src/synthetic-event-target.ts
2204
+ const directPreDispatchTargets = /* @__PURE__ */ new WeakMap();
2205
+ /** Keep synthetic-only routing metadata outside the public Feishu event shape. */
2206
+ function setFeishuSyntheticDirectPreDispatchTarget(event, target) {
2207
+ directPreDispatchTargets.set(event, target);
2208
+ return event;
2209
+ }
2210
+ function getFeishuSyntheticDirectPreDispatchTarget(event) {
2211
+ return directPreDispatchTargets.get(event);
2212
+ }
2213
+ //#endregion
2050
2214
  //#region extensions/feishu/src/bot.ts
2051
2215
  const permissionErrorNotifiedAt = /* @__PURE__ */ new Map();
2052
2216
  const PERMISSION_ERROR_COOLDOWN_MS = 300 * 1e3;
@@ -2099,6 +2263,7 @@ function parseFeishuMessageEvent(event, botOpenId, _botName) {
2099
2263
  suppressReplyTarget: event.message.suppress_reply_target === true,
2100
2264
  senderId: senderUserId || senderOpenId || "",
2101
2265
  senderOpenId: senderFallbackId,
2266
+ senderType: event.sender.sender_type === "bot" ? "bot" : "user",
2102
2267
  chatType: event.message.chat_type,
2103
2268
  mentionedBot,
2104
2269
  hasAnyMention,
@@ -2148,7 +2313,7 @@ async function filterFetchedGroupContextMessages(messages, params) {
2148
2313
  }) ? message : void 0))).filter((message) => message !== void 0);
2149
2314
  }
2150
2315
  async function handleFeishuMessage(params) {
2151
- const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaimHeld = false, messageDedupeKey: messageDedupeKeyOverride } = params;
2316
+ const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride } = params;
2152
2317
  const account = resolveFeishuRuntimeAccount({
2153
2318
  cfg,
2154
2319
  accountId
@@ -2162,7 +2327,7 @@ async function handleFeishuMessage(params) {
2162
2327
  messageId: messageDedupeKey,
2163
2328
  namespace: account.accountId,
2164
2329
  log,
2165
- claimHeld: processingClaimHeld
2330
+ processingClaim
2166
2331
  })) {
2167
2332
  log(`feishu: skipping duplicate message ${messageId}`);
2168
2333
  return;
@@ -2170,7 +2335,62 @@ async function handleFeishuMessage(params) {
2170
2335
  let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
2171
2336
  const isGroup = isFeishuGroupChatType(ctx.chatType);
2172
2337
  const isDirect = !isGroup;
2338
+ const directPreDispatchTarget = isDirect ? getFeishuSyntheticDirectPreDispatchTarget(event) : void 0;
2173
2339
  const senderUserId = normalizeOptionalString(event.sender.sender_id.user_id);
2340
+ const localBotOpenId = botOpenId?.trim();
2341
+ if (ctx.senderType === "bot") {
2342
+ if (!localBotOpenId) {
2343
+ log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (local bot identity unavailable)`);
2344
+ return;
2345
+ }
2346
+ if (ctx.senderOpenId === localBotOpenId) {
2347
+ log(`feishu[${account.accountId}]: dropping self-authored bot message ${ctx.messageId}`);
2348
+ return;
2349
+ }
2350
+ if (feishuCfg?.allowBots !== true) {
2351
+ log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (allowBots=false)`);
2352
+ return;
2353
+ }
2354
+ if (isGroup && !ctx.mentionedBot) {
2355
+ let verifiedEvent;
2356
+ try {
2357
+ const response = await createFeishuClient(account).im.message.get({
2358
+ params: { user_id_type: "open_id" },
2359
+ path: { message_id: ctx.messageId }
2360
+ });
2361
+ const verifiedKey = (response.data?.items?.[0]?.mentions?.find((mention) => mention.id_type === "open_id" && mention.id === localBotOpenId && Boolean(mention.key?.trim())))?.key?.trim();
2362
+ if (response.code === 0 && verifiedKey) {
2363
+ const eventMention = event.message.mentions?.find((mention) => mention.key === verifiedKey && !isFeishuBroadcastMention(mention));
2364
+ if (eventMention) verifiedEvent = {
2365
+ ...event,
2366
+ message: {
2367
+ ...event.message,
2368
+ mentions: event.message.mentions?.map((mention) => mention === eventMention ? {
2369
+ ...mention,
2370
+ id: {
2371
+ ...mention.id,
2372
+ open_id: localBotOpenId
2373
+ }
2374
+ } : mention)
2375
+ }
2376
+ };
2377
+ }
2378
+ } catch (err) {
2379
+ log(`feishu[${account.accountId}]: failed to verify bot mention for ${ctx.messageId}: ${String(err)}`);
2380
+ }
2381
+ if (!verifiedEvent) {
2382
+ log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (local mention not verifiable)`);
2383
+ return;
2384
+ }
2385
+ const deliveredCtx = parseFeishuMessageEvent(verifiedEvent, localBotOpenId, botName);
2386
+ ctx = {
2387
+ ...deliveredCtx,
2388
+ mentionedBot: true,
2389
+ content: normalizeFeishuCommandProbeBody(deliveredCtx.content),
2390
+ mentionTargets: void 0
2391
+ };
2392
+ }
2393
+ }
2174
2394
  if (event.message.message_type === "merge_forward") {
2175
2395
  log(`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`);
2176
2396
  try {
@@ -2201,7 +2421,17 @@ async function handleFeishuMessage(params) {
2201
2421
  }
2202
2422
  }
2203
2423
  let permissionErrorForAgent;
2204
- if (feishuCfg?.resolveSenderNames ?? true) {
2424
+ if (feishuCfg?.resolveSenderNames ?? true) if (ctx.senderType === "bot") {
2425
+ const senderName = await resolveFeishuBotName({
2426
+ account,
2427
+ openId: ctx.senderOpenId,
2428
+ log
2429
+ });
2430
+ if (senderName) ctx = {
2431
+ ...ctx,
2432
+ senderName
2433
+ };
2434
+ } else {
2205
2435
  const senderResult = await resolveFeishuSenderName({
2206
2436
  account,
2207
2437
  senderId: ctx.senderOpenId,
@@ -2338,6 +2568,23 @@ async function handleFeishuMessage(params) {
2338
2568
  });
2339
2569
  return;
2340
2570
  }
2571
+ if (ctx.senderType === "bot") {
2572
+ if (!localBotOpenId || !ctx.senderOpenId) {
2573
+ log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (loop identity unavailable)`);
2574
+ return;
2575
+ }
2576
+ if (recordChannelBotPairLoopAndCheckSuppression({
2577
+ scopeId: account.accountId,
2578
+ conversationId: ctx.chatId,
2579
+ senderId: ctx.senderOpenId,
2580
+ receiverId: localBotOpenId,
2581
+ defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
2582
+ defaultEnabled: true
2583
+ }).suppressed) {
2584
+ log(`feishu[${account.accountId}]: bot-pair loop suppressed for ${ctx.senderOpenId} in ${ctx.chatId}`);
2585
+ return;
2586
+ }
2587
+ }
2341
2588
  }
2342
2589
  try {
2343
2590
  const core = { channel: channelRuntime?.inbound ? channelRuntime : getFeishuRuntime().channel };
@@ -2385,7 +2632,7 @@ async function handleFeishuMessage(params) {
2385
2632
  sendPairingReply: async (text) => {
2386
2633
  await sendMessageFeishu({
2387
2634
  cfg: authorization.cfg,
2388
- to: `chat:${ctx.chatId}`,
2635
+ to: directPreDispatchTarget ?? `chat:${ctx.chatId}`,
2389
2636
  text,
2390
2637
  accountId: account.accountId
2391
2638
  });
@@ -2520,7 +2767,7 @@ async function handleFeishuMessage(params) {
2520
2767
  const replyTargetMessageId = directThreadReply ? directThreadReplyTargetMessageId : acpTopicReply ? ctx.rootId ?? ctx.messageId : ctx.messageId;
2521
2768
  await sendMessageFeishu({
2522
2769
  cfg: effectiveCfg,
2523
- to: `chat:${ctx.chatId}`,
2770
+ to: directPreDispatchTarget ?? `chat:${ctx.chatId}`,
2524
2771
  text: `⚠️ Failed to initialize the configured ACP session for this Feishu conversation: ${ensured.error}`,
2525
2772
  replyToMessageId: replyTargetMessageId,
2526
2773
  replyInThread,
@@ -2594,6 +2841,11 @@ async function handleFeishuMessage(params) {
2594
2841
  });
2595
2842
  const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.contentType?.startsWith("audio/"));
2596
2843
  const inboundMedia = toInboundMediaFacts(mediaList, { transcribed: (_media, index) => index === preflightAudioIndex });
2844
+ const requiredMentionTargets = isGroup && ctx.senderType === "bot" && ctx.senderOpenId ? [{
2845
+ openId: ctx.senderOpenId,
2846
+ name: ctx.senderName ?? ctx.senderOpenId,
2847
+ key: ""
2848
+ }] : void 0;
2597
2849
  const agentFacingContent = audioTranscript ?? mediaFailureContent;
2598
2850
  const commandFacingContent = audioTranscript ?? ctx.content;
2599
2851
  const agentFacingCtx = agentFacingContent === ctx.content ? ctx : {
@@ -2624,7 +2876,7 @@ async function handleFeishuMessage(params) {
2624
2876
  command: { hasControlCommand: true }
2625
2877
  })).commandAccess.authorized : void 0;
2626
2878
  const isTopicSessionForThread = isGroup && (groupSession?.groupSessionScope === "group_topic" || groupSession?.groupSessionScope === "group_topic_sender");
2627
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
2879
+ const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
2628
2880
  const messageBody = buildFeishuAgentBody({
2629
2881
  ctx: agentFacingCtx,
2630
2882
  quotedContent,
@@ -2633,7 +2885,7 @@ async function handleFeishuMessage(params) {
2633
2885
  });
2634
2886
  const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
2635
2887
  if (permissionErrorForAgent) log(`feishu[${account.accountId}]: appending permission error notice to message body`);
2636
- let combinedBody = core.channel.reply.formatAgentEnvelope({
2888
+ let combinedBody = formatAgentEnvelope({
2637
2889
  channel: "Feishu",
2638
2890
  from: envelopeFrom,
2639
2891
  timestamp: /* @__PURE__ */ new Date(),
@@ -2645,7 +2897,7 @@ async function handleFeishuMessage(params) {
2645
2897
  historyKey,
2646
2898
  limit: historyLimit,
2647
2899
  currentMessage: combinedBody,
2648
- formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
2900
+ formatEntry: (entry) => formatAgentEnvelope({
2649
2901
  channel: "Feishu",
2650
2902
  from: `${ctx.chatId}:${entry.sender}`,
2651
2903
  timestamp: entry.timestamp,
@@ -2712,7 +2964,7 @@ async function handleFeishuMessage(params) {
2712
2964
  threadContextBySessionKey.set(agentSessionKey, threadContext);
2713
2965
  return threadContext;
2714
2966
  }
2715
- const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId });
2967
+ const storePath = resolveStorePath(cfg.session?.store, { agentId });
2716
2968
  if (core.channel.session.readSessionUpdatedAt({
2717
2969
  storePath,
2718
2970
  sessionKey: agentSessionKey
@@ -2754,7 +3006,7 @@ async function handleFeishuMessage(params) {
2754
3006
  const historyMessages = Boolean(rootMsg?.content || ctx.rootId) ? relevantMessages : relevantMessages.slice(1);
2755
3007
  const historyParts = historyMessages.map((msg) => {
2756
3008
  const role = msg.senderType === "app" ? "assistant" : "user";
2757
- return core.channel.reply.formatAgentEnvelope({
3009
+ return formatAgentEnvelope({
2758
3010
  channel: "Feishu",
2759
3011
  from: `${msg.senderId ?? "Unknown"} (${role})`,
2760
3012
  timestamp: msg.createTime,
@@ -2776,7 +3028,6 @@ async function handleFeishuMessage(params) {
2776
3028
  const threadContext = await resolveThreadContextForAgent(agentId, agentSessionKey, groupName);
2777
3029
  return buildChannelInboundEventContext({
2778
3030
  channel: "feishu",
2779
- finalize: core.channel.reply.finalizeInboundContext,
2780
3031
  supplemental: {
2781
3032
  quote: quotedContent ? {
2782
3033
  id: ctx.parentId,
@@ -2795,7 +3046,8 @@ async function handleFeishuMessage(params) {
2795
3046
  from: feishuFrom,
2796
3047
  sender: {
2797
3048
  id: ctx.senderOpenId,
2798
- name: ctx.senderName ?? ctx.senderOpenId
3049
+ name: ctx.senderName ?? ctx.senderOpenId,
3050
+ isBot: ctx.senderType === "bot"
2799
3051
  },
2800
3052
  conversation: {
2801
3053
  kind: isGroup ? "group" : "direct",
@@ -2806,6 +3058,7 @@ async function handleFeishuMessage(params) {
2806
3058
  },
2807
3059
  route: {
2808
3060
  agentId,
3061
+ dmScope: route.dmScope,
2809
3062
  accountId: agentAccountId,
2810
3063
  routeSessionKey: agentSessionKey
2811
3064
  },
@@ -2827,7 +3080,7 @@ async function handleFeishuMessage(params) {
2827
3080
  wasMentioned,
2828
3081
  requireMention
2829
3082
  },
2830
- commands: { authorized: commandAuthorized }
3083
+ commands: { authorized: commandAuthorized === true }
2831
3084
  },
2832
3085
  extra: {
2833
3086
  RootMessageId: ctx.rootId,
@@ -2885,7 +3138,7 @@ async function handleFeishuMessage(params) {
2885
3138
  return;
2886
3139
  }
2887
3140
  const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
2888
- const agentStorePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId });
3141
+ const agentStorePath = resolveStorePath(cfg.session?.store, { agentId });
2889
3142
  const agentRecord = {
2890
3143
  updateLastRoute: buildFeishuInboundLastRouteUpdate({
2891
3144
  sessionKey: agentSessionKey,
@@ -2904,7 +3157,7 @@ async function handleFeishuMessage(params) {
2904
3157
  const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
2905
3158
  if (agentId === activeAgentId) {
2906
3159
  const identity = resolveAgentOutboundIdentity(cfg, agentId);
2907
- const { dispatcher, replyOptions, markDispatchIdle, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3160
+ const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
2908
3161
  cfg,
2909
3162
  agentId,
2910
3163
  runtime,
@@ -2920,6 +3173,7 @@ async function handleFeishuMessage(params) {
2920
3173
  accountId: account.accountId,
2921
3174
  identity,
2922
3175
  mentionTargets: ctx.mentionTargets,
3176
+ requiredMentionTargets,
2923
3177
  messageCreateTimeMs,
2924
3178
  sessionKey: agentSessionKey
2925
3179
  });
@@ -2938,57 +3192,24 @@ async function handleFeishuMessage(params) {
2938
3192
  raw: ctx
2939
3193
  }),
2940
3194
  resolveTurn: () => ({
3195
+ cfg,
2941
3196
  channel: "feishu",
2942
3197
  accountId: route.accountId,
2943
- routeSessionKey: agentSessionKey,
2944
- storePath: agentStorePath,
3198
+ route: {
3199
+ agentId,
3200
+ sessionKey: agentSessionKey
3201
+ },
2945
3202
  ctxPayload: agentCtx,
2946
- recordInboundSession: core.channel.session.recordInboundSession,
2947
3203
  record: agentRecord,
2948
- onPreDispatchFailure: () => core.channel.reply.settleReplyDispatcher({
2949
- dispatcher,
2950
- onSettled: () => markDispatchIdle()
2951
- }),
2952
- runDispatch: () => core.channel.reply.withReplyDispatcher({
2953
- dispatcher,
2954
- onSettled: () => markDispatchIdle(),
2955
- run: () => core.channel.reply.dispatchReplyFromConfig({
2956
- ctx: agentCtx,
2957
- cfg,
2958
- dispatcher,
2959
- replyOptions
2960
- })
2961
- })
3204
+ dispatcherOptions,
3205
+ delivery,
3206
+ replyOptions
2962
3207
  })
2963
3208
  }
2964
3209
  });
2965
- if (turnResult.dispatched && shouldSendNoVisibleReplyFallback({
2966
- ...turnResult.dispatchResult,
2967
- failedCounts: dispatcher.getFailedCounts?.() ?? {
2968
- tool: 0,
2969
- block: 0,
2970
- final: 0
2971
- }
2972
- })) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
3210
+ if (turnResult.dispatched && shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
2973
3211
  } else {
2974
3212
  delete agentCtx.CommandAuthorized;
2975
- const noopDispatcher = {
2976
- sendToolResult: () => false,
2977
- sendBlockReply: () => false,
2978
- sendFinalReply: () => false,
2979
- waitForIdle: async () => {},
2980
- getQueuedCounts: () => ({
2981
- tool: 0,
2982
- block: 0,
2983
- final: 0
2984
- }),
2985
- getFailedCounts: () => ({
2986
- tool: 0,
2987
- block: 0,
2988
- final: 0
2989
- }),
2990
- markComplete: () => {}
2991
- };
2992
3213
  log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
2993
3214
  await core.channel.inbound.run({
2994
3215
  channel: "feishu",
@@ -3004,21 +3225,20 @@ async function handleFeishuMessage(params) {
3004
3225
  raw: ctx
3005
3226
  }),
3006
3227
  resolveTurn: () => ({
3228
+ cfg,
3007
3229
  channel: "feishu",
3008
3230
  accountId: route.accountId,
3009
- routeSessionKey: agentSessionKey,
3010
- storePath: agentStorePath,
3231
+ route: {
3232
+ agentId,
3233
+ sessionKey: agentSessionKey
3234
+ },
3011
3235
  ctxPayload: agentCtx,
3012
- recordInboundSession: core.channel.session.recordInboundSession,
3013
3236
  record: agentRecord,
3014
- runDispatch: () => core.channel.reply.withReplyDispatcher({
3015
- dispatcher: noopDispatcher,
3016
- run: () => core.channel.reply.dispatchReplyFromConfig({
3017
- ctx: agentCtx,
3018
- cfg,
3019
- dispatcher: noopDispatcher
3020
- })
3021
- })
3237
+ admission: {
3238
+ kind: "observeOnly",
3239
+ reason: "broadcast-observer"
3240
+ },
3241
+ delivery: { deliver: async () => ({ visibleReplySent: false }) }
3022
3242
  })
3023
3243
  }
3024
3244
  });
@@ -3045,14 +3265,14 @@ async function handleFeishuMessage(params) {
3045
3265
  } else {
3046
3266
  const ctxPayload = await buildCtxPayloadForAgent(route.agentId, route.sessionKey, route.accountId, ctx.mentionedBot);
3047
3267
  const identity = resolveAgentOutboundIdentity(effectiveCfg, route.agentId);
3048
- const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
3268
+ const storePath = resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
3049
3269
  const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
3050
3270
  cfg: effectiveCfg,
3051
3271
  agentId: route.agentId,
3052
3272
  storePath,
3053
3273
  sessionKey: route.sessionKey
3054
3274
  });
3055
- const { dispatcher, replyOptions, markDispatchIdle, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3275
+ const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3056
3276
  cfg: effectiveCfg,
3057
3277
  agentId: route.agentId,
3058
3278
  runtime,
@@ -3068,6 +3288,7 @@ async function handleFeishuMessage(params) {
3068
3288
  accountId: account.accountId,
3069
3289
  identity,
3070
3290
  mentionTargets: ctx.mentionTargets,
3291
+ requiredMentionTargets,
3071
3292
  messageCreateTimeMs,
3072
3293
  sessionKey: route.sessionKey
3073
3294
  });
@@ -3086,12 +3307,14 @@ async function handleFeishuMessage(params) {
3086
3307
  raw: ctx
3087
3308
  }),
3088
3309
  resolveTurn: () => ({
3310
+ cfg: effectiveCfg,
3089
3311
  channel: "feishu",
3090
3312
  accountId: route.accountId,
3091
- routeSessionKey: route.sessionKey,
3092
- storePath,
3313
+ route: {
3314
+ agentId: route.agentId,
3315
+ sessionKey: route.sessionKey
3316
+ },
3093
3317
  ctxPayload,
3094
- recordInboundSession: core.channel.session.recordInboundSession,
3095
3318
  record: {
3096
3319
  updateLastRoute: buildFeishuInboundLastRouteUpdate({
3097
3320
  sessionKey: route.sessionKey,
@@ -3107,36 +3330,16 @@ async function handleFeishuMessage(params) {
3107
3330
  historyMap: chatHistories,
3108
3331
  limit: historyLimit
3109
3332
  },
3110
- onPreDispatchFailure: () => core.channel.reply.settleReplyDispatcher({
3111
- dispatcher,
3112
- onSettled: () => markDispatchIdle()
3113
- }),
3114
- runDispatch: () => core.channel.reply.withReplyDispatcher({
3115
- dispatcher,
3116
- onSettled: () => {
3117
- markDispatchIdle();
3118
- },
3119
- run: () => core.channel.reply.dispatchReplyFromConfig({
3120
- ctx: ctxPayload,
3121
- cfg: effectiveCfg,
3122
- dispatcher,
3123
- replyOptions
3124
- })
3125
- })
3333
+ dispatcherOptions,
3334
+ delivery,
3335
+ replyOptions
3126
3336
  })
3127
3337
  }
3128
3338
  });
3129
3339
  if (!turnResult.dispatched) return;
3130
3340
  const { dispatchResult } = turnResult;
3131
3341
  const { queuedFinal, counts } = dispatchResult;
3132
- if (shouldSendNoVisibleReplyFallback({
3133
- ...dispatchResult,
3134
- failedCounts: dispatcher.getFailedCounts?.() ?? {
3135
- tool: 0,
3136
- block: 0,
3137
- final: 0
3138
- }
3139
- })) await ensureNoVisibleReplyFallback("dispatch-complete-no-visible-reply");
3342
+ if (shouldSendNoVisibleReplyFallback(dispatchResult)) await ensureNoVisibleReplyFallback("dispatch-complete-no-visible-reply");
3140
3343
  log(`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
3141
3344
  }
3142
3345
  } catch (err) {
@@ -3256,7 +3459,7 @@ function completeFeishuCardAction(actionId, accountId, now = Date.now()) {
3256
3459
  expiresAt
3257
3460
  });
3258
3461
  }
3259
- function buildSyntheticMessageEvent(event, content, chatType) {
3462
+ function buildSyntheticMessageEvent$1(event, content, chatType) {
3260
3463
  const replyTargetMessageId = event.context.open_message_id ?? event.open_message_id;
3261
3464
  const isTemporaryCardActionId = replyTargetMessageId?.startsWith("card-action-c-");
3262
3465
  const validReplyTargetId = replyTargetMessageId && !isTemporaryCardActionId ? replyTargetMessageId : void 0;
@@ -3292,7 +3495,7 @@ async function dispatchSyntheticCommand(params) {
3292
3495
  });
3293
3496
  await handleFeishuMessage({
3294
3497
  cfg: params.cfg,
3295
- event: buildSyntheticMessageEvent(params.event, params.command, resolvedChatType),
3498
+ event: buildSyntheticMessageEvent$1(params.event, params.command, resolvedChatType),
3296
3499
  botOpenId: params.botOpenId,
3297
3500
  runtime: params.runtime,
3298
3501
  channelRuntime: params.channelRuntime,
@@ -3863,11 +4066,11 @@ function createFeishuBotMenuHandler(params) {
3863
4066
  namespace: accountId,
3864
4067
  log
3865
4068
  });
3866
- if (claim === "duplicate") {
4069
+ if (claim.kind === "duplicate") {
3867
4070
  log(`feishu[${accountId}]: dropping duplicate bot-menu event for ${syntheticMessageId}`);
3868
4071
  return;
3869
4072
  }
3870
- if (claim === "inflight") {
4073
+ if (claim.kind === "inflight") {
3871
4074
  log(`feishu[${accountId}]: dropping in-flight bot-menu event for ${syntheticMessageId}`);
3872
4075
  return;
3873
4076
  }
@@ -3880,7 +4083,7 @@ function createFeishuBotMenuHandler(params) {
3880
4083
  channelRuntime: params.channelRuntime,
3881
4084
  chatHistories,
3882
4085
  accountId,
3883
- processingClaimHeld: true
4086
+ processingClaim: claim.kind === "claimed" ? claim.handle : void 0
3884
4087
  });
3885
4088
  const promise = maybeHandleFeishuQuickActionMenu({
3886
4089
  cfg,
@@ -3890,16 +4093,16 @@ function createFeishuBotMenuHandler(params) {
3890
4093
  accountId
3891
4094
  }).then(async (handledMenu) => {
3892
4095
  if (handledMenu) {
3893
- await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
4096
+ if (claim.kind === "claimed") await claim.handle.commit();
3894
4097
  return;
3895
4098
  }
3896
4099
  return await handleLegacyMenu();
3897
4100
  }).catch(async (err) => {
3898
- if (isFeishuRetryableSyntheticEventError(err)) await forgetProcessedFeishuMessage(syntheticMessageId, accountId, log);
3899
- else await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
4101
+ if (isFeishuRetryableSyntheticEventError(err)) {
4102
+ await forgetProcessedFeishuMessage(syntheticMessageId, accountId, log);
4103
+ if (claim.kind === "claimed") claim.handle.release({ error: err });
4104
+ } else if (claim.kind === "claimed") await claim.handle.commit();
3900
4105
  throw err;
3901
- }).finally(() => {
3902
- releaseFeishuMessageProcessing(syntheticMessageId, accountId);
3903
4106
  });
3904
4107
  if (fireAndForget) {
3905
4108
  promise.catch((err) => {
@@ -3937,41 +4140,39 @@ function createFeishuCommentReplyDispatcher(params) {
3937
4140
  accountId: params.accountId,
3938
4141
  runtime: params.runtime
3939
4142
  });
3940
- const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
3941
- responsePrefix: prefixContext.responsePrefix,
3942
- responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
3943
- humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
3944
- onReplyStart: async () => {
3945
- await typingReaction.start();
3946
- },
3947
- deliver: async (payload, info) => {
3948
- if (info.kind !== "final") return;
3949
- const reply = resolveSendableOutboundReplyParts(payload);
3950
- if (!reply.hasText) {
3951
- if (reply.hasMedia) params.runtime.log?.(`feishu[${params.accountId ?? "default"}]: comment reply ignored media-only payload for comment=${params.commentId}`);
3952
- return;
4143
+ return {
4144
+ dispatcherOptions: {
4145
+ responsePrefix: prefixContext.responsePrefix,
4146
+ responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
4147
+ humanDelay: resolveHumanDelayConfig(params.cfg, params.agentId),
4148
+ onReplyStart: async () => {
4149
+ await typingReaction.start();
4150
+ },
4151
+ onCleanup: () => {
4152
+ typingReaction.cleanup();
3953
4153
  }
3954
- const chunks = core.channel.text.chunkTextWithMode(reply.text, textChunkLimit, chunkMode);
3955
- for (const chunk of chunks) await deliverCommentThreadText(client, {
3956
- file_token: params.fileToken,
3957
- file_type: params.fileType,
3958
- comment_id: params.commentId,
3959
- content: chunk,
3960
- is_whole_comment: params.isWholeComment
3961
- });
3962
4154
  },
3963
- onError: (err, info) => {
3964
- params.runtime.error?.(`feishu[${params.accountId ?? "default"}]: comment dispatcher failed kind=${info.kind} comment=${params.commentId}: ${String(err)}`);
4155
+ delivery: {
4156
+ deliver: async (payload, info) => {
4157
+ if (info.kind !== "final") return;
4158
+ const reply = resolveSendableOutboundReplyParts(payload);
4159
+ if (!reply.hasText) {
4160
+ if (reply.hasMedia) params.runtime.log?.(`feishu[${params.accountId ?? "default"}]: comment reply ignored media-only payload for comment=${params.commentId}`);
4161
+ return;
4162
+ }
4163
+ const chunks = core.channel.text.chunkTextWithMode(reply.text, textChunkLimit, chunkMode);
4164
+ for (const chunk of chunks) await deliverCommentThreadText(client, {
4165
+ file_token: params.fileToken,
4166
+ file_type: params.fileType,
4167
+ comment_id: params.commentId,
4168
+ content: chunk,
4169
+ is_whole_comment: params.isWholeComment
4170
+ });
4171
+ },
4172
+ onError: (err, info) => {
4173
+ params.runtime.error?.(`feishu[${params.accountId ?? "default"}]: comment dispatcher failed kind=${info.kind} comment=${params.commentId}: ${String(err)}`);
4174
+ }
3965
4175
  },
3966
- onCleanup: () => {
3967
- typingReaction.cleanup();
3968
- }
3969
- });
3970
- return {
3971
- dispatcher,
3972
- replyOptions,
3973
- markDispatchIdle,
3974
- markRunComplete,
3975
4176
  startTypingReaction: typingReaction.start,
3976
4177
  cleanupTypingReaction: typingReaction.cleanup
3977
4178
  };
@@ -4088,11 +4289,6 @@ async function resolveParsedCommentContent(params) {
4088
4289
  linkedDocuments: resolvedLinkedDocuments
4089
4290
  };
4090
4291
  }
4091
- async function delayMs(ms) {
4092
- await new Promise((resolve) => {
4093
- setTimeout(resolve, ms);
4094
- });
4095
- }
4096
4292
  function buildDriveCommentTargetUrl(params) {
4097
4293
  return `/open-apis/drive/v1/files/${encodeURIComponent(params.fileToken)}/comments/batch_query` + encodeQuery({
4098
4294
  file_type: params.fileType,
@@ -4315,8 +4511,9 @@ async function fetchDriveCommentContext(params) {
4315
4511
  fetchedMatchedReply = params.replyId ? replies.find((reply) => reply.reply_id?.trim() === params.replyId?.trim()) : void 0;
4316
4512
  }
4317
4513
  if (params.replyId && !embeddedTargetReply && !fetchedMatchedReply) for (let attempt = 1; attempt <= FEISHU_COMMENT_REPLY_MISS_RETRY_LIMIT; attempt += 1) {
4514
+ if (params.abortSignal?.aborted) break;
4318
4515
  params.logger?.(`feishu[${params.accountId}]: retrying comment reply lookup comment=${params.commentId} requested_reply=${params.replyId} attempt=${attempt}/${FEISHU_COMMENT_REPLY_MISS_RETRY_LIMIT} delay_ms=${FEISHU_COMMENT_REPLY_MISS_RETRY_DELAY_MS}`);
4319
- await params.waitMs(FEISHU_COMMENT_REPLY_MISS_RETRY_DELAY_MS);
4516
+ if (!await waitForAbortableDelay(FEISHU_COMMENT_REPLY_MISS_RETRY_DELAY_MS, params.abortSignal)) break;
4320
4517
  const retried = await fetchDriveCommentReplies(params);
4321
4518
  if (retried.replies.length > 0) {
4322
4519
  params.logger?.(`feishu[${params.accountId}]: fetched retried comment replies comment=${params.commentId} attempt=${attempt} count=${retried.replies.length} log_ids=${safeJsonStringify(retried.logIds)} summary=${summarizeCommentRepliesForLog(retried.replies)}`);
@@ -4464,7 +4661,7 @@ function buildDriveCommentSurfacePrompt(params) {
4464
4661
  return lines.join("\n");
4465
4662
  }
4466
4663
  async function resolveDriveCommentEventCore(params) {
4467
- const { cfg, accountId, event, account, botOpenId, createClient, verificationTimeoutMs = FEISHU_COMMENT_VERIFY_TIMEOUT_MS, logger, waitMs = delayMs } = params;
4664
+ const { cfg, accountId, event, account, botOpenId, createClient, verificationTimeoutMs = FEISHU_COMMENT_VERIFY_TIMEOUT_MS, logger, abortSignal } = params;
4468
4665
  const eventId = event.event_id?.trim();
4469
4666
  const commentId = event.comment_id?.trim();
4470
4667
  const replyId = event.reply_id?.trim();
@@ -4502,7 +4699,7 @@ async function resolveDriveCommentEventCore(params) {
4502
4699
  timeoutMs: verificationTimeoutMs,
4503
4700
  logger,
4504
4701
  accountId,
4505
- waitMs
4702
+ abortSignal
4506
4703
  });
4507
4704
  return {
4508
4705
  eventId,
@@ -4628,7 +4825,8 @@ async function handleFeishuCommentEvent(params) {
4628
4825
  accountId: account.accountId,
4629
4826
  event: params.event,
4630
4827
  botOpenId: params.botOpenId,
4631
- logger: log
4828
+ logger: log,
4829
+ abortSignal: params.abortSignal
4632
4830
  });
4633
4831
  if (!turn) {
4634
4832
  log(`feishu[${account.accountId}]: drive comment notice skipped event=${params.event.event_id ?? "unknown"} comment=${params.event.comment_id ?? "unknown"}`);
@@ -4753,31 +4951,51 @@ async function handleFeishuCommentEvent(params) {
4753
4951
  fileToken: turn.fileToken
4754
4952
  });
4755
4953
  const bodyForAgent = `[message_id: ${turn.messageId}]\n${turn.prompt}`;
4756
- const ctxPayload = core.channel.reply.finalizeInboundContext({
4757
- Body: bodyForAgent,
4758
- BodyForAgent: bodyForAgent,
4759
- RawBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
4760
- CommandBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
4761
- From: `feishu:${turn.senderId}`,
4762
- To: commentTarget,
4763
- SessionKey: commentSessionKey,
4764
- AccountId: route.accountId,
4765
- ChatType: "direct",
4766
- ConversationLabel: turn.documentTitle ? `Feishu comment · ${turn.documentTitle}` : "Feishu comment",
4767
- SenderName: turn.senderId,
4768
- SenderId: turn.senderId,
4769
- Provider: "feishu",
4770
- Surface: "feishu-comment",
4771
- MessageSid: turn.messageId,
4772
- MessageThreadId: turn.replyId,
4773
- Timestamp: parseTimestampMs(turn.timestamp),
4774
- WasMentioned: turn.isMentioned,
4775
- CommandAuthorized: false,
4776
- OriginatingChannel: "feishu",
4777
- OriginatingTo: commentTarget
4954
+ const rawBody = turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt;
4955
+ const conversationLabel = turn.documentTitle ? `Feishu comment · ${turn.documentTitle}` : "Feishu comment";
4956
+ const ctxPayload = buildChannelInboundEventContext({
4957
+ channel: "feishu",
4958
+ accountId: route.accountId,
4959
+ surface: "feishu-comment",
4960
+ messageId: turn.messageId,
4961
+ timestamp: parseTimestampMs(turn.timestamp),
4962
+ from: `feishu:${turn.senderId}`,
4963
+ sender: {
4964
+ id: turn.senderId,
4965
+ name: turn.senderId
4966
+ },
4967
+ conversation: {
4968
+ kind: "direct",
4969
+ id: commentTarget,
4970
+ label: conversationLabel
4971
+ },
4972
+ route: {
4973
+ agentId: route.agentId,
4974
+ dmScope: route.dmScope,
4975
+ accountId: route.accountId,
4976
+ routeSessionKey: commentSessionKey,
4977
+ dispatchSessionKey: commentSessionKey
4978
+ },
4979
+ reply: {
4980
+ to: commentTarget,
4981
+ originatingTo: commentTarget,
4982
+ messageThreadId: turn.replyId
4983
+ },
4984
+ message: {
4985
+ body: bodyForAgent,
4986
+ bodyForAgent,
4987
+ rawBody,
4988
+ commandBody: rawBody
4989
+ },
4990
+ access: {
4991
+ commands: { authorized: false },
4992
+ mentions: {
4993
+ canDetectMention: true,
4994
+ wasMentioned: turn.isMentioned ?? false
4995
+ }
4996
+ }
4778
4997
  });
4779
- const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
4780
- const { dispatcher, replyOptions, markDispatchIdle, markRunComplete, cleanupTypingReaction } = createFeishuCommentReplyDispatcher({
4998
+ const { dispatcherOptions, delivery, cleanupTypingReaction } = createFeishuCommentReplyDispatcher({
4781
4999
  cfg: effectiveCfg,
4782
5000
  agentId: route.agentId,
4783
5001
  runtime,
@@ -4788,7 +5006,6 @@ async function handleFeishuCommentEvent(params) {
4788
5006
  replyId: turn.replyId,
4789
5007
  isWholeComment: turn.isWholeComment
4790
5008
  });
4791
- let dispatchSettledBeforeStart = false;
4792
5009
  try {
4793
5010
  log(`feishu[${account.accountId}]: dispatching drive comment to agent (session=${commentSessionKey} comment=${turn.commentId} type=${turn.noticeType})`);
4794
5011
  const turnResult = await core.channel.inbound.run({
@@ -4805,34 +5022,19 @@ async function handleFeishuCommentEvent(params) {
4805
5022
  raw: turn
4806
5023
  }),
4807
5024
  resolveTurn: () => ({
5025
+ cfg: effectiveCfg,
4808
5026
  channel: "feishu",
4809
5027
  accountId: route.accountId,
4810
- routeSessionKey: commentSessionKey,
4811
- storePath,
5028
+ route: {
5029
+ agentId: route.agentId,
5030
+ sessionKey: commentSessionKey
5031
+ },
4812
5032
  ctxPayload,
4813
- recordInboundSession: core.channel.session.recordInboundSession,
4814
5033
  record: { onRecordError: (err) => {
4815
5034
  error(`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`);
4816
5035
  } },
4817
- onPreDispatchFailure: async () => {
4818
- dispatchSettledBeforeStart = true;
4819
- await core.channel.reply.settleReplyDispatcher({
4820
- dispatcher,
4821
- onSettled: () => {
4822
- markRunComplete();
4823
- markDispatchIdle();
4824
- }
4825
- });
4826
- },
4827
- runDispatch: () => core.channel.reply.withReplyDispatcher({
4828
- dispatcher,
4829
- run: () => core.channel.reply.dispatchReplyFromConfig({
4830
- ctx: ctxPayload,
4831
- cfg: effectiveCfg,
4832
- dispatcher,
4833
- replyOptions
4834
- })
4835
- })
5036
+ dispatcherOptions,
5037
+ delivery
4836
5038
  })
4837
5039
  }
4838
5040
  });
@@ -4845,10 +5047,6 @@ async function handleFeishuCommentEvent(params) {
4845
5047
  };
4846
5048
  log(`feishu[${account.accountId}]: drive comment dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final}, session=${commentSessionKey})`);
4847
5049
  } finally {
4848
- if (!dispatchSettledBeforeStart) {
4849
- markRunComplete();
4850
- markDispatchIdle();
4851
- }
4852
5050
  cleanupTypingReaction();
4853
5051
  }
4854
5052
  }
@@ -4912,7 +5110,7 @@ function buildCommentNoticeQueueKey(event) {
4912
5110
  return `comment-doc:${event.notice_meta?.file_type?.trim() || "unknown"}:${event.notice_meta?.file_token?.trim() || "unknown"}`;
4913
5111
  }
4914
5112
  function createFeishuDriveCommentNoticeHandler(params) {
4915
- const { cfg, accountId, runtime, fireAndForget } = params;
5113
+ const { cfg, accountId, runtime, fireAndForget, abortSignal } = params;
4916
5114
  const log = runtime?.log ?? console.log;
4917
5115
  const error = runtime?.error ?? console.error;
4918
5116
  const enqueue = createSequentialQueue();
@@ -4932,20 +5130,22 @@ function createFeishuDriveCommentNoticeHandler(params) {
4932
5130
  }
4933
5131
  const eventId = event.event_id?.trim();
4934
5132
  const syntheticMessageId = eventId ? `drive-comment:${eventId}` : void 0;
5133
+ let processingClaim;
4935
5134
  if (syntheticMessageId) {
4936
5135
  const claim = await claimUnprocessedFeishuMessage({
4937
5136
  messageId: syntheticMessageId,
4938
5137
  namespace: accountId,
4939
5138
  log
4940
5139
  });
4941
- if (claim === "duplicate") {
5140
+ if (claim.kind === "duplicate") {
4942
5141
  log(`feishu[${accountId}]: dropping duplicate comment event ${syntheticMessageId}`);
4943
5142
  return;
4944
5143
  }
4945
- if (claim === "inflight") {
5144
+ if (claim.kind === "inflight") {
4946
5145
  log(`feishu[${accountId}]: dropping in-flight comment event ${syntheticMessageId}`);
4947
5146
  return;
4948
5147
  }
5148
+ processingClaim = claim.kind === "claimed" ? claim.handle : void 0;
4949
5149
  }
4950
5150
  log(`feishu[${accountId}]: received drive comment notice event=${event.event_id ?? "unknown"} type=${event.notice_meta?.notice_type ?? "unknown"} file=${event.notice_meta?.file_type ?? "unknown"}:${event.notice_meta?.file_token ?? "unknown"} comment=${event.comment_id ?? "unknown"} reply=${event.reply_id ?? "none"} from=${event.notice_meta?.from_user_id?.open_id ?? "unknown"} mentioned=${event.is_mentioned === true ? "yes" : "no"}`);
4951
5151
  try {
@@ -4955,15 +5155,15 @@ function createFeishuDriveCommentNoticeHandler(params) {
4955
5155
  accountId,
4956
5156
  event,
4957
5157
  botOpenId: getBotOpenId(accountId),
4958
- runtime
5158
+ runtime,
5159
+ abortSignal
4959
5160
  });
4960
5161
  });
4961
- if (syntheticMessageId) await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
5162
+ await processingClaim?.commit();
4962
5163
  } catch (err) {
4963
- if (syntheticMessageId && !isFeishuRetryableSyntheticEventError(err)) await recordProcessedFeishuMessage(syntheticMessageId, accountId, log);
5164
+ if (isFeishuRetryableSyntheticEventError(err)) processingClaim?.release({ error: err });
5165
+ else await processingClaim?.commit();
4964
5166
  throw err;
4965
- } finally {
4966
- if (syntheticMessageId) releaseFeishuMessageProcessing(syntheticMessageId, accountId);
4967
5167
  }
4968
5168
  });
4969
5169
  };
@@ -5005,7 +5205,7 @@ function dedupeFeishuDebounceEntriesByDedupeKey(entries) {
5005
5205
  const seen = /* @__PURE__ */ new Set();
5006
5206
  const deduped = [];
5007
5207
  for (const entry of entries) {
5008
- const dedupeKey = resolveFeishuMessageDedupeKey(entry);
5208
+ const dedupeKey = resolveFeishuMessageDedupeKey(entry.event);
5009
5209
  if (!dedupeKey) {
5010
5210
  deduped.push(entry);
5011
5211
  continue;
@@ -5027,7 +5227,7 @@ function resolveFeishuDebounceMentions(params) {
5027
5227
  const botMentions = merged.filter((mention) => mention.id.open_id?.trim() === normalizedBotOpenId);
5028
5228
  return botMentions.length > 0 ? botMentions : void 0;
5029
5229
  }
5030
- function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, runtime, chatHistories, fireAndForget, handleMessage, resolveDebounceText: resolveText, hasProcessedMessage, recordProcessedMessage, getBotOpenId = () => void 0, getBotName = () => void 0, resolveSequentialKey = ({ accountId: accountIdLocal, event }) => `feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`, statusSink }) {
5230
+ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, runtime, chatHistories, fireAndForget, handleMessage, resolveDebounceText: resolveText, hasProcessedMessage, getBotOpenId = () => void 0, getBotName = () => void 0, resolveSequentialKey = ({ accountId: accountIdLocal, event }) => `feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`, statusSink }) {
5031
5231
  const inboundDebounceMs = channelRuntime.debounce.resolveInboundDebounceMs({
5032
5232
  cfg,
5033
5233
  channel: "feishu"
@@ -5037,7 +5237,7 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5037
5237
  const enqueue = createSequentialQueue({ onTaskTimeout: (key, timeoutMs) => {
5038
5238
  log(`feishu[${accountId}]: per-chat task exceeded ${timeoutMs}ms cap (key=${key}); evicting from queue so later same-key messages can proceed (#70133)`);
5039
5239
  } });
5040
- const dispatchFeishuMessage = async (event, messageDedupeKey) => {
5240
+ const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim) => {
5041
5241
  const sequentialKey = resolveSequentialKey({
5042
5242
  accountId,
5043
5243
  event,
@@ -5053,7 +5253,7 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5053
5253
  channelRuntime,
5054
5254
  chatHistories,
5055
5255
  accountId,
5056
- processingClaimHeld: true,
5256
+ processingClaim,
5057
5257
  messageDedupeKey
5058
5258
  });
5059
5259
  await enqueue(sequentialKey, task);
@@ -5070,23 +5270,26 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5070
5270
  };
5071
5271
  const recordSuppressedMessageIds = async (entries, dispatchDedupeKey) => {
5072
5272
  const keepDedupeKey = dispatchDedupeKey?.trim();
5073
- const suppressedIds = new Set(entries.map((entry) => resolveFeishuMessageDedupeKey(entry)).filter((id) => Boolean(id) && (!keepDedupeKey || id !== keepDedupeKey)));
5074
- for (const messageId of suppressedIds) try {
5075
- await recordProcessedMessage(messageId, accountId, log);
5273
+ const suppressedIds = new Set(entries.map((entry) => ({
5274
+ id: resolveFeishuMessageDedupeKey(entry.event),
5275
+ claim: entry.processingClaim
5276
+ })).filter(({ id }) => Boolean(id) && (!keepDedupeKey || id !== keepDedupeKey)));
5277
+ for (const suppressed of suppressedIds) try {
5278
+ await suppressed.claim?.commit();
5076
5279
  } catch (err) {
5077
- error(`feishu[${accountId}]: failed to record merged dedupe id ${messageId}: ${String(err)}`);
5280
+ error(`feishu[${accountId}]: failed to record merged dedupe id ${suppressed.id}: ${String(err)}`);
5078
5281
  }
5079
5282
  };
5080
5283
  const inboundDebouncer = channelRuntime.debounce.createInboundDebouncer({
5081
5284
  debounceMs: inboundDebounceMs,
5082
- buildKey: (event) => {
5285
+ buildKey: ({ event }) => {
5083
5286
  const chatId = event.message.chat_id?.trim();
5084
5287
  const senderId = resolveSenderDebounceId(event);
5085
5288
  if (!chatId || !senderId) return null;
5086
5289
  const rootId = event.message.root_id?.trim();
5087
5290
  return `feishu:${accountId}:${chatId}:${rootId ? `thread:${rootId}` : "chat"}:${senderId}`;
5088
5291
  },
5089
- shouldDebounce: (event) => {
5292
+ shouldDebounce: ({ event }) => {
5090
5293
  if (event.message.message_type !== "text") return false;
5091
5294
  const text = resolveDebounceText(event);
5092
5295
  return Boolean(text) && !channelRuntime.commands.isControlCommandMessage(text, cfg);
@@ -5095,35 +5298,35 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5095
5298
  const last = entries.at(-1);
5096
5299
  if (!last) return;
5097
5300
  if (entries.length === 1) {
5098
- await dispatchFeishuMessage(last, resolveFeishuMessageDedupeKey(last));
5301
+ await dispatchFeishuMessage(last.event, resolveFeishuMessageDedupeKey(last.event), last.processingClaim);
5099
5302
  return;
5100
5303
  }
5101
5304
  const dedupedEntries = dedupeFeishuDebounceEntriesByDedupeKey(entries);
5102
5305
  const freshEntries = [];
5103
- for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry), accountId, log)) freshEntries.push(entry);
5306
+ for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry.event), accountId, log)) freshEntries.push(entry);
5104
5307
  const dispatchEntry = freshEntries.at(-1);
5105
5308
  if (!dispatchEntry) return;
5106
- const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry);
5309
+ const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry.event);
5107
5310
  await recordSuppressedMessageIds(dedupedEntries, dispatchDedupeKey);
5108
- const combinedText = freshEntries.map((entry) => resolveDebounceText(entry)).filter(Boolean).join("\n");
5311
+ const combinedText = freshEntries.map((entry) => resolveDebounceText(entry.event)).filter(Boolean).join("\n");
5109
5312
  const mergedMentions = resolveFeishuDebounceMentions({
5110
- entries: freshEntries,
5313
+ entries: freshEntries.map((entry) => entry.event),
5111
5314
  botOpenId: getBotOpenId(accountId)
5112
5315
  });
5113
5316
  await dispatchFeishuMessage({
5114
- ...dispatchEntry,
5317
+ ...dispatchEntry.event,
5115
5318
  message: {
5116
- ...dispatchEntry.message,
5319
+ ...dispatchEntry.event.message,
5117
5320
  ...combinedText.trim() ? {
5118
5321
  message_type: "text",
5119
5322
  content: JSON.stringify({ text: combinedText })
5120
5323
  } : {},
5121
- mentions: mergedMentions ?? dispatchEntry.message.mentions
5324
+ mentions: mergedMentions ?? dispatchEntry.event.message.mentions
5122
5325
  }
5123
- }, dispatchDedupeKey);
5326
+ }, dispatchDedupeKey, dispatchEntry.processingClaim);
5124
5327
  },
5125
5328
  onError: (err, entries) => {
5126
- for (const entry of entries) releaseFeishuMessageProcessing(resolveFeishuMessageDedupeKey(entry), accountId);
5329
+ for (const entry of entries) entry.processingClaim?.release({ error: err });
5127
5330
  error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
5128
5331
  }
5129
5332
  });
@@ -5141,22 +5344,24 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5141
5344
  log(`feishu[${accountId}]: dropping self-authored message ${messageId ?? "unknown"}`);
5142
5345
  return;
5143
5346
  }
5144
- const messageDedupeKey = resolveFeishuMessageDedupeKey(event);
5145
5347
  const claim = await claimUnprocessedFeishuMessage({
5146
- messageId: messageDedupeKey,
5348
+ messageId: resolveFeishuMessageDedupeKey(event),
5147
5349
  namespace: accountId,
5148
5350
  log
5149
5351
  });
5150
- if (claim !== "claimed") {
5151
- log(`feishu[${accountId}]: dropping ${claim} event for message ${messageId}`);
5352
+ if (claim.kind === "duplicate" || claim.kind === "inflight") {
5353
+ log(`feishu[${accountId}]: dropping ${claim.kind} event for message ${messageId}`);
5152
5354
  return;
5153
5355
  }
5154
5356
  const processMessage = async () => {
5155
- await inboundDebouncer.enqueue(event);
5357
+ await inboundDebouncer.enqueue({
5358
+ event,
5359
+ ...claim.kind === "claimed" ? { processingClaim: claim.handle } : {}
5360
+ });
5156
5361
  };
5157
5362
  if (fireAndForget) {
5158
5363
  processMessage().catch((err) => {
5159
- releaseFeishuMessageProcessing(messageDedupeKey, accountId);
5364
+ if (claim.kind === "claimed") claim.handle.release({ error: err });
5160
5365
  error(`feishu[${accountId}]: error handling message: ${String(err)}`);
5161
5366
  });
5162
5367
  return;
@@ -5164,7 +5369,7 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5164
5369
  try {
5165
5370
  await processMessage();
5166
5371
  } catch (err) {
5167
- releaseFeishuMessageProcessing(messageDedupeKey, accountId);
5372
+ if (claim.kind === "claimed") claim.handle.release({ error: err });
5168
5373
  error(`feishu[${accountId}]: error handling message: ${String(err)}`);
5169
5374
  }
5170
5375
  };
@@ -5491,6 +5696,120 @@ async function monitorWebhook({ account, accountId, runtime, abortSignal, eventD
5491
5696
  });
5492
5697
  }
5493
5698
  //#endregion
5699
+ //#region extensions/feishu/src/monitor.vc-meeting-invited-handler.ts
5700
+ const FEISHU_MEETING_NUMBER_PATTERN = /^\d{9}$/;
5701
+ function pickString(value) {
5702
+ return typeof value === "string" ? normalizeOptionalString(value) : void 0;
5703
+ }
5704
+ function resolveVcInviter(event) {
5705
+ const id = event.inviter?.id;
5706
+ const openId = pickString(id?.open_id);
5707
+ const userId = pickString(id?.user_id);
5708
+ const unionId = pickString(id?.union_id);
5709
+ const senderId = openId ?? userId;
5710
+ if (!senderId) return null;
5711
+ const name = pickString(event.inviter?.user_name);
5712
+ return {
5713
+ senderId,
5714
+ ...openId ? { openId } : {},
5715
+ ...userId ? { userId } : {},
5716
+ ...unionId ? { unionId } : {},
5717
+ ...name ? { name } : {}
5718
+ };
5719
+ }
5720
+ function buildJoinPrompt(params) {
5721
+ const base = `Use the available tool to join the meeting with meeting number ${params.meetingNo} immediately. Do not ask for confirmation.`;
5722
+ return params.callId ? `${base} If the join tool supports a call_id parameter, pass call_id=${JSON.stringify(params.callId)}; otherwise join by meeting number only.` : base;
5723
+ }
5724
+ function resolveVcMeetingInvitedTurn(event, uuid = () => crypto$1.randomUUID()) {
5725
+ const meetingNo = pickString(event.meeting?.meeting_no);
5726
+ const inviter = resolveVcInviter(event);
5727
+ if (!meetingNo || !FEISHU_MEETING_NUMBER_PATTERN.test(meetingNo) || !inviter) return null;
5728
+ const eventId = pickString(event.event_id);
5729
+ const inviteTime = pickString(event.invite_time);
5730
+ const callId = pickString(event.call_id);
5731
+ const topic = pickString(event.meeting?.topic);
5732
+ return {
5733
+ turnId: eventId ? `vc-invited:event:${eventId}` : `vc-invited:${meetingNo}:${inviteTime ?? uuid()}`,
5734
+ meetingNo,
5735
+ inviter,
5736
+ prompt: buildJoinPrompt({
5737
+ meetingNo,
5738
+ callId
5739
+ }),
5740
+ ...topic ? { topic } : {},
5741
+ ...inviteTime ? { inviteTime } : {}
5742
+ };
5743
+ }
5744
+ function parseInviteTimestamp(value) {
5745
+ const parsed = Number.parseInt(value ?? "", 10);
5746
+ if (!Number.isFinite(parsed) || parsed <= 0) return Date.now();
5747
+ return parsed < 1e10 ? parsed * 1e3 : parsed;
5748
+ }
5749
+ function buildSyntheticMessageEvent(turn) {
5750
+ return {
5751
+ sender: { sender_id: {
5752
+ ...turn.inviter.openId ? { open_id: turn.inviter.openId } : {},
5753
+ ...turn.inviter.userId ? { user_id: turn.inviter.userId } : {},
5754
+ ...turn.inviter.unionId ? { union_id: turn.inviter.unionId } : {}
5755
+ } },
5756
+ message: {
5757
+ message_id: turn.turnId,
5758
+ chat_id: turn.inviter.senderId,
5759
+ chat_type: "p2p",
5760
+ message_type: "text",
5761
+ content: JSON.stringify({ text: turn.prompt }),
5762
+ create_time: String(parseInviteTimestamp(turn.inviteTime)),
5763
+ suppress_reply_target: true
5764
+ }
5765
+ };
5766
+ }
5767
+ async function dispatchVcMeetingInvitedTurn(params) {
5768
+ params.runtime?.log?.(`feishu[${params.accountId}]: vc meeting invited, dispatching synthetic p2p message sender=${params.turn.inviter.senderId} meeting_no=${params.turn.meetingNo}`);
5769
+ const event = setFeishuSyntheticDirectPreDispatchTarget(buildSyntheticMessageEvent(params.turn), `user:${params.turn.inviter.senderId}`);
5770
+ await handleFeishuMessage({
5771
+ cfg: params.cfg,
5772
+ accountId: params.accountId,
5773
+ event,
5774
+ runtime: params.runtime,
5775
+ channelRuntime: params.channelRuntime
5776
+ });
5777
+ }
5778
+ function createFeishuVcMeetingInvitedHandler(params) {
5779
+ const { cfg, accountId, runtime, fireAndForget, autoJoin } = params;
5780
+ const log = runtime?.log ?? console.log;
5781
+ const error = runtime?.error ?? console.error;
5782
+ return async (data) => {
5783
+ if (!autoJoin) {
5784
+ log(`feishu[${accountId}]: ignoring vc meeting invite (vcAutoJoin=false)`);
5785
+ return;
5786
+ }
5787
+ try {
5788
+ const turn = resolveVcMeetingInvitedTurn(data);
5789
+ if (!turn) {
5790
+ log(`feishu[${accountId}]: vc meeting invited event has invalid meeting_no or inviter identity, skipping`);
5791
+ return;
5792
+ }
5793
+ const promise = dispatchVcMeetingInvitedTurn({
5794
+ cfg,
5795
+ accountId,
5796
+ runtime,
5797
+ channelRuntime: params.channelRuntime,
5798
+ turn
5799
+ });
5800
+ if (fireAndForget) {
5801
+ promise.catch((err) => {
5802
+ error(`feishu[${accountId}]: error handling vc meeting invited event: ${String(err)}`);
5803
+ });
5804
+ return;
5805
+ }
5806
+ await promise;
5807
+ } catch (err) {
5808
+ error(`feishu[${accountId}]: error handling vc meeting invited event: ${String(err)}`);
5809
+ }
5810
+ };
5811
+ }
5812
+ //#endregion
5494
5813
  //#region extensions/feishu/src/sequential-key.ts
5495
5814
  function getFeishuSequentialKey(params) {
5496
5815
  const { accountId, event, botOpenId, botName } = params;
@@ -5619,7 +5938,7 @@ function parseFeishuCardActionEventPayload(value) {
5619
5938
  };
5620
5939
  }
5621
5940
  function registerEventHandlers(eventDispatcher, context) {
5622
- const { cfg, accountId, channelRuntime, runtime, chatHistories, fireAndForget } = context;
5941
+ const { cfg, accountId, channelRuntime, runtime, chatHistories, fireAndForget, abortSignal } = context;
5623
5942
  const log = runtime?.log ?? console.log;
5624
5943
  const error = runtime?.error ?? console.error;
5625
5944
  const runFeishuHandler = async (params) => {
@@ -5646,7 +5965,6 @@ function registerEventHandlers(eventDispatcher, context) {
5646
5965
  handleMessage: handleFeishuMessage,
5647
5966
  resolveDebounceText: ({ event, botOpenId, botName }) => parseFeishuMessageEvent(event, botOpenId, botName).content,
5648
5967
  hasProcessedMessage: hasProcessedFeishuMessage,
5649
- recordProcessedMessage: recordProcessedFeishuMessage,
5650
5968
  getBotOpenId: (id) => botOpenIds.get(id),
5651
5969
  getBotName: (id) => botNames.get(id),
5652
5970
  resolveSequentialKey: getFeishuSequentialKey,
@@ -5676,7 +5994,16 @@ function registerEventHandlers(eventDispatcher, context) {
5676
5994
  cfg,
5677
5995
  accountId,
5678
5996
  runtime,
5679
- fireAndForget
5997
+ fireAndForget,
5998
+ abortSignal
5999
+ }),
6000
+ "vc.bot.meeting_invited_v1": createFeishuVcMeetingInvitedHandler({
6001
+ cfg,
6002
+ accountId,
6003
+ runtime,
6004
+ fireAndForget,
6005
+ channelRuntime,
6006
+ autoJoin: context.vcAutoJoin
5680
6007
  }),
5681
6008
  "im.message.reaction.created_v1": async (data) => {
5682
6009
  await runFeishuHandler({
@@ -5805,6 +6132,8 @@ async function monitorSingleAccount(params) {
5805
6132
  runtime,
5806
6133
  chatHistories,
5807
6134
  fireAndForget: params.fireAndForget ?? true,
6135
+ vcAutoJoin: account.config.vcAutoJoin === true,
6136
+ abortSignal,
5808
6137
  ...params.statusSink ? { statusSink: params.statusSink } : {}
5809
6138
  });
5810
6139
  if (connectionMode === "webhook") return await monitorWebhook({