@openclaw/feishu 2026.7.2-beta.2 → 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,14 +1,14 @@
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 { 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-DsqTk27v.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
5
  import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-87BmeMpj.js";
6
6
  import { t as getFeishuRuntime } from "./runtime-C5JxBWZp.js";
7
- import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-B4E8OXZ0.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-DrWM_Ilf.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
12
  import { i as waitForAbortableDelay, r as raceWithTimeoutAndAbort } from "./probe-BgboTHIn.js";
13
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";
@@ -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";
37
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
36
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 {};
@@ -1489,11 +1634,6 @@ function rememberStreamingStartFailure(accountId, now = Date.now()) {
1489
1634
  streamingStartBackoffUntilByAccount.set(accountId, backoffUntil);
1490
1635
  return backoffUntil;
1491
1636
  }
1492
- function formatMediaFallbackText(text, mediaUrl) {
1493
- const trimmedText = text?.trim() ?? "";
1494
- const attachmentText = `📎 ${mediaUrl}`;
1495
- return trimmedText ? `${trimmedText}\n\n${attachmentText}` : attachmentText;
1496
- }
1497
1637
  function normalizeEpochMs(timestamp) {
1498
1638
  if (!Number.isFinite(timestamp) || timestamp === void 0 || timestamp <= 0) return;
1499
1639
  return timestamp < MS_EPOCH_MIN ? timestamp * 1e3 : timestamp;
@@ -1518,7 +1658,7 @@ function resolveCardNote(agentId, identity, prefixCtx) {
1518
1658
  }
1519
1659
  function createFeishuReplyDispatcher(params) {
1520
1660
  const core = getFeishuRuntime();
1521
- 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;
1522
1662
  const sendReplyToMessageId = skipReplyToInMessages ? void 0 : replyToMessageId;
1523
1663
  const typingTargetMessageId = explicitTypingTargetMessageId?.trim() || replyToMessageId;
1524
1664
  const threadReplyMode = threadReply === true;
@@ -1583,7 +1723,7 @@ function createFeishuReplyDispatcher(params) {
1583
1723
  channel: "feishu"
1584
1724
  });
1585
1725
  const renderMode = account.config?.renderMode ?? "auto";
1586
- const streamingEnabled = resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
1726
+ const streamingEnabled = !requiredMentionTargets?.length && resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
1587
1727
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1588
1728
  const coreBlockStreamingEnabled = blockStreamingEnabled === true;
1589
1729
  const reasoningPreviewEnabled = streamingEnabled && params.allowReasoningPreview === true;
@@ -1731,12 +1871,15 @@ function createFeishuReplyDispatcher(params) {
1731
1871
  limit: textChunkLimit,
1732
1872
  mode: chunkMode,
1733
1873
  firstChunkMentions: paramsLocal.firstChunkMentions,
1874
+ chunkMentions: paramsLocal.chunkMentions,
1734
1875
  initialChunks
1735
1876
  }));
1736
1877
  for (const [index, chunk] of chunks.entries()) {
1878
+ const mentions = [...paramsLocal.chunkMentions ?? [], ...index === 0 ? paramsLocal.firstChunkMentions ?? [] : []];
1737
1879
  await paramsLocal.sendChunk({
1738
1880
  chunk,
1739
- isFirst: index === 0
1881
+ isFirst: index === 0,
1882
+ mentions: mentions.length > 0 ? mentions : void 0
1740
1883
  });
1741
1884
  markVisibleReplySent();
1742
1885
  }
@@ -1765,7 +1908,8 @@ function createFeishuReplyDispatcher(params) {
1765
1908
  text: options.fallbackText,
1766
1909
  useCard: false,
1767
1910
  infoKind: "final",
1768
- sendChunk: async ({ chunk }) => {
1911
+ chunkMentions: requiredMentionTargets,
1912
+ sendChunk: async ({ chunk, mentions }) => {
1769
1913
  await sendMessageFeishu({
1770
1914
  cfg,
1771
1915
  to: sendTarget,
@@ -1773,20 +1917,25 @@ function createFeishuReplyDispatcher(params) {
1773
1917
  replyToMessageId: sendReplyToMessageId,
1774
1918
  replyInThread: effectiveReplyInThread,
1775
1919
  allowTopLevelReplyFallback,
1776
- accountId
1920
+ accountId,
1921
+ ...mentions ? { mentions } : {}
1777
1922
  });
1778
1923
  }
1779
1924
  });
1780
1925
  }
1781
1926
  },
1782
1927
  onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
1783
- const fallbackText = formatMediaFallbackText(sentFallbackText ? void 0 : options.fallbackText, mediaUrl);
1928
+ const fallbackText = await buildFeishuMediaFallbackText({
1929
+ text: sentFallbackText ? void 0 : options.fallbackText,
1930
+ mediaUrl
1931
+ });
1784
1932
  sentFallbackText = true;
1785
1933
  await sendChunkedTextReply({
1786
1934
  text: fallbackText,
1787
1935
  useCard: false,
1788
1936
  infoKind: "final",
1789
- sendChunk: async ({ chunk }) => {
1937
+ chunkMentions: requiredMentionTargets,
1938
+ sendChunk: async ({ chunk, mentions }) => {
1790
1939
  await sendMessageFeishu({
1791
1940
  cfg,
1792
1941
  to: sendTarget,
@@ -1794,7 +1943,8 @@ function createFeishuReplyDispatcher(params) {
1794
1943
  replyToMessageId: sendReplyToMessageId,
1795
1944
  replyInThread: effectiveReplyInThread,
1796
1945
  allowTopLevelReplyFallback,
1797
- accountId
1946
+ accountId,
1947
+ ...mentions ? { mentions } : {}
1798
1948
  });
1799
1949
  }
1800
1950
  });
@@ -1815,7 +1965,8 @@ function createFeishuReplyDispatcher(params) {
1815
1965
  replyToMessageId: sendReplyToMessageId,
1816
1966
  replyInThread: effectiveReplyInThread,
1817
1967
  allowTopLevelReplyFallback,
1818
- accountId
1968
+ accountId,
1969
+ ...requiredMentionTargets?.length ? { mentions: requiredMentionTargets } : {}
1819
1970
  });
1820
1971
  markVisibleReplySent();
1821
1972
  params.runtime.error?.(`feishu[${account.accountId}]: sent no-visible-reply fallback (${reason})`);
@@ -1829,10 +1980,10 @@ function createFeishuReplyDispatcher(params) {
1829
1980
  idleSideEffectsPromise = nextIdleSideEffects.catch(() => {});
1830
1981
  return nextIdleSideEffects;
1831
1982
  };
1832
- const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
1983
+ const dispatcherOptions = {
1833
1984
  responsePrefix: prefixContext.responsePrefix,
1834
1985
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
1835
- humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
1986
+ humanDelay: resolveHumanDelayConfig(cfg, agentId),
1836
1987
  silentReplyContext: {
1837
1988
  cfg,
1838
1989
  sessionKey: params.sessionKey,
@@ -1855,142 +2006,151 @@ function createFeishuReplyDispatcher(params) {
1855
2006
  if (streamingEnabled && renderMode === "card") startStreaming();
1856
2007
  await Promise.resolve(typingCallbacks?.onReplyStart?.());
1857
2008
  },
1858
- deliver: async (payload, info) => {
1859
- if (info?.kind === "final") skippedFinalReason = null;
1860
- const payloadText = payload.isReasoning && payload.text ? formatReasoningMessage(payload.text) : payload.text;
1861
- const reply = resolveSendableOutboundReplyParts({
1862
- ...payload,
1863
- text: payloadText
1864
- });
1865
- const text = info?.kind === "final" ? mergeStreamingFinalText(streamText, reply.text, payload.isError === true && hasStreamingFinalText) : reply.text;
1866
- const hasText = reply.hasText;
1867
- const hasMedia = reply.hasMedia;
1868
- const hasVoiceMedia = hasMedia && reply.mediaUrls.some((mediaUrl) => shouldSuppressFeishuTextForVoiceMedia({
1869
- mediaUrl,
1870
- ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
1871
- }));
1872
- const finalTextExceedsStreamingLimit = info?.kind === "final" && hasText && text.length > textChunkLimit;
1873
- const useStaticCard = hasText && (renderMode === "card" || info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw" || renderMode === "auto" && shouldUseCard(text));
1874
- const useStreamingCard = hasText && streamingEnabled && !finalTextExceedsStreamingLimit && (info?.kind === "final" || useStaticCard);
1875
- const finalTextWouldUseStreamingCard = info?.kind === "final" && hasText && streamingEnabled;
1876
- const useCard = useStaticCard || useStreamingCard;
1877
- const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
1878
- const skipTextForClosedStreamingFinal = info?.kind === "final" && hasText && streamingClosedForReply && !streamingCloseErroredForReply && finalTextWouldUseStreamingCard;
1879
- const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal && !skipTextForClosedStreamingFinal;
1880
- const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText || skipTextForDuplicateFinal));
1881
- if (!shouldDeliverText && !hasMedia) return;
1882
- if (shouldDiscardStreamingPreview) await discardStreamingPreview();
1883
- if (shouldDeliverText) {
1884
- if (info?.kind === "block") {
1885
- if (!useStreamingCard) {
1886
- if (coreBlockStreamingEnabled) {
1887
- const firstChunkMentions = !sentIndependentBlockText && mentionTargets?.length ? mentionTargets : void 0;
1888
- await sendChunkedTextReply({
1889
- text,
1890
- useCard: false,
1891
- infoKind: "block",
1892
- firstChunkMentions,
1893
- sendChunk: async ({ chunk, isFirst }) => {
1894
- await sendMessageFeishu({
1895
- cfg,
1896
- to: sendTarget,
1897
- text: chunk,
1898
- replyToMessageId: sendReplyToMessageId,
1899
- replyInThread: effectiveReplyInThread,
1900
- allowTopLevelReplyFallback,
1901
- accountId,
1902
- ...isFirst && firstChunkMentions ? { mentions: firstChunkMentions } : {}
1903
- });
1904
- }
1905
- });
1906
- sentIndependentBlockText = true;
1907
- 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;
1908
2079
  }
1909
- return;
2080
+ startStreaming();
2081
+ if (streamingStartPromise) await streamingStartPromise;
1910
2082
  }
1911
- startStreaming();
1912
- if (streamingStartPromise) await streamingStartPromise;
1913
- }
1914
- if (info?.kind === "final" && useStreamingCard) {
1915
- startStreaming();
1916
- if (streamingStartPromise) await streamingStartPromise;
1917
- }
1918
- const shouldStreamText = info?.kind === "block" || info?.kind === "final";
1919
- if (streaming?.isActive() && shouldStreamText) {
1920
- if (info?.kind === "block") queueStreamingUpdate(text, {
1921
- mode: "delta",
1922
- dedupeWithLastPartial: true
1923
- });
1924
- if (info?.kind === "final") {
1925
- streamText = text;
1926
- hasStreamingFinalText = true;
1927
- snapshotBaseText = "";
1928
- lastSnapshotTextLength = text.length;
1929
- flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2083
+ if (info?.kind === "final" && useStreamingCard) {
2084
+ startStreaming();
2085
+ if (streamingStartPromise) await streamingStartPromise;
1930
2086
  }
1931
- if (hasMedia) await sendMediaReplies(payload);
1932
- return;
1933
- }
1934
- if (useCard) {
1935
- const cardHeader = resolveCardHeader(agentId, identity);
1936
- const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1937
- await sendChunkedTextReply({
1938
- text,
1939
- useCard: true,
1940
- infoKind: info?.kind,
1941
- sendChunk: async ({ chunk }) => {
1942
- await sendStructuredCardFeishu({
1943
- cfg,
1944
- to: sendTarget,
1945
- text: chunk,
1946
- replyToMessageId: sendReplyToMessageId,
1947
- replyInThread: effectiveReplyInThread,
1948
- allowTopLevelReplyFallback,
1949
- accountId,
1950
- header: cardHeader,
1951
- note: cardNote
1952
- });
1953
- }
1954
- });
1955
- } else {
1956
- const firstChunkMentions = info?.kind === "final" && mentionTargets?.length ? mentionTargets : void 0;
1957
- await sendChunkedTextReply({
1958
- text,
1959
- useCard: false,
1960
- infoKind: info?.kind,
1961
- firstChunkMentions,
1962
- sendChunk: async ({ chunk, isFirst }) => {
1963
- await sendMessageFeishu({
1964
- cfg,
1965
- to: sendTarget,
1966
- text: chunk,
1967
- replyToMessageId: sendReplyToMessageId,
1968
- replyInThread: effectiveReplyInThread,
1969
- allowTopLevelReplyFallback,
1970
- accountId,
1971
- ...isFirst && firstChunkMentions ? { mentions: firstChunkMentions } : {}
1972
- });
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));
1973
2099
  }
1974
- });
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
+ }
1975
2148
  }
1976
- }
1977
- if (hasMedia) await sendMediaReplies(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
1978
- },
1979
- onError: async (error, info) => {
1980
- streamingCloseErroredForReply = true;
1981
- streamingClosedForReply = false;
1982
- params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
1983
- await queueIdleSideEffects({ markClosedForReply: false });
2149
+ if (hasMedia) await sendMediaReplies(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
2150
+ },
2151
+ onError: handleDeliveryError
1984
2152
  },
1985
- onIdle: () => queueIdleSideEffects(),
1986
- onCleanup: () => {
1987
- typingCallbacks?.onCleanup?.();
1988
- }
1989
- });
1990
- return {
1991
- dispatcher,
1992
2153
  replyOptions: {
1993
- ...replyOptions,
1994
2154
  onModelSelected: prefixContext.onModelSelected,
1995
2155
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : true,
1996
2156
  onPartialReply: streamingEnabled ? (payload) => {
@@ -2032,7 +2192,6 @@ function createFeishuReplyDispatcher(params) {
2032
2192
  updateStreamingStatusLine("");
2033
2193
  } : void 0
2034
2194
  },
2035
- markDispatchIdle,
2036
2195
  ensureNoVisibleReplyFallback,
2037
2196
  getVisibleReplyState: () => ({
2038
2197
  visibleReplySent,
@@ -2041,6 +2200,17 @@ function createFeishuReplyDispatcher(params) {
2041
2200
  };
2042
2201
  }
2043
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
2044
2214
  //#region extensions/feishu/src/bot.ts
2045
2215
  const permissionErrorNotifiedAt = /* @__PURE__ */ new Map();
2046
2216
  const PERMISSION_ERROR_COOLDOWN_MS = 300 * 1e3;
@@ -2093,6 +2263,7 @@ function parseFeishuMessageEvent(event, botOpenId, _botName) {
2093
2263
  suppressReplyTarget: event.message.suppress_reply_target === true,
2094
2264
  senderId: senderUserId || senderOpenId || "",
2095
2265
  senderOpenId: senderFallbackId,
2266
+ senderType: event.sender.sender_type === "bot" ? "bot" : "user",
2096
2267
  chatType: event.message.chat_type,
2097
2268
  mentionedBot,
2098
2269
  hasAnyMention,
@@ -2164,7 +2335,62 @@ async function handleFeishuMessage(params) {
2164
2335
  let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
2165
2336
  const isGroup = isFeishuGroupChatType(ctx.chatType);
2166
2337
  const isDirect = !isGroup;
2338
+ const directPreDispatchTarget = isDirect ? getFeishuSyntheticDirectPreDispatchTarget(event) : void 0;
2167
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
+ }
2168
2394
  if (event.message.message_type === "merge_forward") {
2169
2395
  log(`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`);
2170
2396
  try {
@@ -2195,7 +2421,17 @@ async function handleFeishuMessage(params) {
2195
2421
  }
2196
2422
  }
2197
2423
  let permissionErrorForAgent;
2198
- 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 {
2199
2435
  const senderResult = await resolveFeishuSenderName({
2200
2436
  account,
2201
2437
  senderId: ctx.senderOpenId,
@@ -2332,6 +2568,23 @@ async function handleFeishuMessage(params) {
2332
2568
  });
2333
2569
  return;
2334
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
+ }
2335
2588
  }
2336
2589
  try {
2337
2590
  const core = { channel: channelRuntime?.inbound ? channelRuntime : getFeishuRuntime().channel };
@@ -2379,7 +2632,7 @@ async function handleFeishuMessage(params) {
2379
2632
  sendPairingReply: async (text) => {
2380
2633
  await sendMessageFeishu({
2381
2634
  cfg: authorization.cfg,
2382
- to: `chat:${ctx.chatId}`,
2635
+ to: directPreDispatchTarget ?? `chat:${ctx.chatId}`,
2383
2636
  text,
2384
2637
  accountId: account.accountId
2385
2638
  });
@@ -2514,7 +2767,7 @@ async function handleFeishuMessage(params) {
2514
2767
  const replyTargetMessageId = directThreadReply ? directThreadReplyTargetMessageId : acpTopicReply ? ctx.rootId ?? ctx.messageId : ctx.messageId;
2515
2768
  await sendMessageFeishu({
2516
2769
  cfg: effectiveCfg,
2517
- to: `chat:${ctx.chatId}`,
2770
+ to: directPreDispatchTarget ?? `chat:${ctx.chatId}`,
2518
2771
  text: `⚠️ Failed to initialize the configured ACP session for this Feishu conversation: ${ensured.error}`,
2519
2772
  replyToMessageId: replyTargetMessageId,
2520
2773
  replyInThread,
@@ -2588,6 +2841,11 @@ async function handleFeishuMessage(params) {
2588
2841
  });
2589
2842
  const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.contentType?.startsWith("audio/"));
2590
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;
2591
2849
  const agentFacingContent = audioTranscript ?? mediaFailureContent;
2592
2850
  const commandFacingContent = audioTranscript ?? ctx.content;
2593
2851
  const agentFacingCtx = agentFacingContent === ctx.content ? ctx : {
@@ -2618,7 +2876,7 @@ async function handleFeishuMessage(params) {
2618
2876
  command: { hasControlCommand: true }
2619
2877
  })).commandAccess.authorized : void 0;
2620
2878
  const isTopicSessionForThread = isGroup && (groupSession?.groupSessionScope === "group_topic" || groupSession?.groupSessionScope === "group_topic_sender");
2621
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
2879
+ const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
2622
2880
  const messageBody = buildFeishuAgentBody({
2623
2881
  ctx: agentFacingCtx,
2624
2882
  quotedContent,
@@ -2627,7 +2885,7 @@ async function handleFeishuMessage(params) {
2627
2885
  });
2628
2886
  const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
2629
2887
  if (permissionErrorForAgent) log(`feishu[${account.accountId}]: appending permission error notice to message body`);
2630
- let combinedBody = core.channel.reply.formatAgentEnvelope({
2888
+ let combinedBody = formatAgentEnvelope({
2631
2889
  channel: "Feishu",
2632
2890
  from: envelopeFrom,
2633
2891
  timestamp: /* @__PURE__ */ new Date(),
@@ -2639,7 +2897,7 @@ async function handleFeishuMessage(params) {
2639
2897
  historyKey,
2640
2898
  limit: historyLimit,
2641
2899
  currentMessage: combinedBody,
2642
- formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
2900
+ formatEntry: (entry) => formatAgentEnvelope({
2643
2901
  channel: "Feishu",
2644
2902
  from: `${ctx.chatId}:${entry.sender}`,
2645
2903
  timestamp: entry.timestamp,
@@ -2706,7 +2964,7 @@ async function handleFeishuMessage(params) {
2706
2964
  threadContextBySessionKey.set(agentSessionKey, threadContext);
2707
2965
  return threadContext;
2708
2966
  }
2709
- const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId });
2967
+ const storePath = resolveStorePath(cfg.session?.store, { agentId });
2710
2968
  if (core.channel.session.readSessionUpdatedAt({
2711
2969
  storePath,
2712
2970
  sessionKey: agentSessionKey
@@ -2748,7 +3006,7 @@ async function handleFeishuMessage(params) {
2748
3006
  const historyMessages = Boolean(rootMsg?.content || ctx.rootId) ? relevantMessages : relevantMessages.slice(1);
2749
3007
  const historyParts = historyMessages.map((msg) => {
2750
3008
  const role = msg.senderType === "app" ? "assistant" : "user";
2751
- return core.channel.reply.formatAgentEnvelope({
3009
+ return formatAgentEnvelope({
2752
3010
  channel: "Feishu",
2753
3011
  from: `${msg.senderId ?? "Unknown"} (${role})`,
2754
3012
  timestamp: msg.createTime,
@@ -2770,7 +3028,6 @@ async function handleFeishuMessage(params) {
2770
3028
  const threadContext = await resolveThreadContextForAgent(agentId, agentSessionKey, groupName);
2771
3029
  return buildChannelInboundEventContext({
2772
3030
  channel: "feishu",
2773
- finalize: core.channel.reply.finalizeInboundContext,
2774
3031
  supplemental: {
2775
3032
  quote: quotedContent ? {
2776
3033
  id: ctx.parentId,
@@ -2789,7 +3046,8 @@ async function handleFeishuMessage(params) {
2789
3046
  from: feishuFrom,
2790
3047
  sender: {
2791
3048
  id: ctx.senderOpenId,
2792
- name: ctx.senderName ?? ctx.senderOpenId
3049
+ name: ctx.senderName ?? ctx.senderOpenId,
3050
+ isBot: ctx.senderType === "bot"
2793
3051
  },
2794
3052
  conversation: {
2795
3053
  kind: isGroup ? "group" : "direct",
@@ -2800,6 +3058,7 @@ async function handleFeishuMessage(params) {
2800
3058
  },
2801
3059
  route: {
2802
3060
  agentId,
3061
+ dmScope: route.dmScope,
2803
3062
  accountId: agentAccountId,
2804
3063
  routeSessionKey: agentSessionKey
2805
3064
  },
@@ -2879,7 +3138,7 @@ async function handleFeishuMessage(params) {
2879
3138
  return;
2880
3139
  }
2881
3140
  const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
2882
- const agentStorePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId });
3141
+ const agentStorePath = resolveStorePath(cfg.session?.store, { agentId });
2883
3142
  const agentRecord = {
2884
3143
  updateLastRoute: buildFeishuInboundLastRouteUpdate({
2885
3144
  sessionKey: agentSessionKey,
@@ -2898,7 +3157,7 @@ async function handleFeishuMessage(params) {
2898
3157
  const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
2899
3158
  if (agentId === activeAgentId) {
2900
3159
  const identity = resolveAgentOutboundIdentity(cfg, agentId);
2901
- const { dispatcher, replyOptions, markDispatchIdle, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3160
+ const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
2902
3161
  cfg,
2903
3162
  agentId,
2904
3163
  runtime,
@@ -2914,6 +3173,7 @@ async function handleFeishuMessage(params) {
2914
3173
  accountId: account.accountId,
2915
3174
  identity,
2916
3175
  mentionTargets: ctx.mentionTargets,
3176
+ requiredMentionTargets,
2917
3177
  messageCreateTimeMs,
2918
3178
  sessionKey: agentSessionKey
2919
3179
  });
@@ -2932,57 +3192,24 @@ async function handleFeishuMessage(params) {
2932
3192
  raw: ctx
2933
3193
  }),
2934
3194
  resolveTurn: () => ({
3195
+ cfg,
2935
3196
  channel: "feishu",
2936
3197
  accountId: route.accountId,
2937
- routeSessionKey: agentSessionKey,
2938
- storePath: agentStorePath,
3198
+ route: {
3199
+ agentId,
3200
+ sessionKey: agentSessionKey
3201
+ },
2939
3202
  ctxPayload: agentCtx,
2940
- recordInboundSession: core.channel.session.recordInboundSession,
2941
3203
  record: agentRecord,
2942
- onPreDispatchFailure: () => core.channel.reply.settleReplyDispatcher({
2943
- dispatcher,
2944
- onSettled: () => markDispatchIdle()
2945
- }),
2946
- runDispatch: () => core.channel.reply.withReplyDispatcher({
2947
- dispatcher,
2948
- onSettled: () => markDispatchIdle(),
2949
- run: () => core.channel.reply.dispatchReplyFromConfig({
2950
- ctx: agentCtx,
2951
- cfg,
2952
- dispatcher,
2953
- replyOptions
2954
- })
2955
- })
3204
+ dispatcherOptions,
3205
+ delivery,
3206
+ replyOptions
2956
3207
  })
2957
3208
  }
2958
3209
  });
2959
- if (turnResult.dispatched && shouldSendNoVisibleReplyFallback({
2960
- ...turnResult.dispatchResult,
2961
- failedCounts: dispatcher.getFailedCounts?.() ?? {
2962
- tool: 0,
2963
- block: 0,
2964
- final: 0
2965
- }
2966
- })) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
3210
+ if (turnResult.dispatched && shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
2967
3211
  } else {
2968
3212
  delete agentCtx.CommandAuthorized;
2969
- const noopDispatcher = {
2970
- sendToolResult: () => false,
2971
- sendBlockReply: () => false,
2972
- sendFinalReply: () => false,
2973
- waitForIdle: async () => {},
2974
- getQueuedCounts: () => ({
2975
- tool: 0,
2976
- block: 0,
2977
- final: 0
2978
- }),
2979
- getFailedCounts: () => ({
2980
- tool: 0,
2981
- block: 0,
2982
- final: 0
2983
- }),
2984
- markComplete: () => {}
2985
- };
2986
3213
  log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
2987
3214
  await core.channel.inbound.run({
2988
3215
  channel: "feishu",
@@ -2998,21 +3225,20 @@ async function handleFeishuMessage(params) {
2998
3225
  raw: ctx
2999
3226
  }),
3000
3227
  resolveTurn: () => ({
3228
+ cfg,
3001
3229
  channel: "feishu",
3002
3230
  accountId: route.accountId,
3003
- routeSessionKey: agentSessionKey,
3004
- storePath: agentStorePath,
3231
+ route: {
3232
+ agentId,
3233
+ sessionKey: agentSessionKey
3234
+ },
3005
3235
  ctxPayload: agentCtx,
3006
- recordInboundSession: core.channel.session.recordInboundSession,
3007
3236
  record: agentRecord,
3008
- runDispatch: () => core.channel.reply.withReplyDispatcher({
3009
- dispatcher: noopDispatcher,
3010
- run: () => core.channel.reply.dispatchReplyFromConfig({
3011
- ctx: agentCtx,
3012
- cfg,
3013
- dispatcher: noopDispatcher
3014
- })
3015
- })
3237
+ admission: {
3238
+ kind: "observeOnly",
3239
+ reason: "broadcast-observer"
3240
+ },
3241
+ delivery: { deliver: async () => ({ visibleReplySent: false }) }
3016
3242
  })
3017
3243
  }
3018
3244
  });
@@ -3039,14 +3265,14 @@ async function handleFeishuMessage(params) {
3039
3265
  } else {
3040
3266
  const ctxPayload = await buildCtxPayloadForAgent(route.agentId, route.sessionKey, route.accountId, ctx.mentionedBot);
3041
3267
  const identity = resolveAgentOutboundIdentity(effectiveCfg, route.agentId);
3042
- const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
3268
+ const storePath = resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
3043
3269
  const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
3044
3270
  cfg: effectiveCfg,
3045
3271
  agentId: route.agentId,
3046
3272
  storePath,
3047
3273
  sessionKey: route.sessionKey
3048
3274
  });
3049
- const { dispatcher, replyOptions, markDispatchIdle, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3275
+ const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3050
3276
  cfg: effectiveCfg,
3051
3277
  agentId: route.agentId,
3052
3278
  runtime,
@@ -3062,6 +3288,7 @@ async function handleFeishuMessage(params) {
3062
3288
  accountId: account.accountId,
3063
3289
  identity,
3064
3290
  mentionTargets: ctx.mentionTargets,
3291
+ requiredMentionTargets,
3065
3292
  messageCreateTimeMs,
3066
3293
  sessionKey: route.sessionKey
3067
3294
  });
@@ -3080,12 +3307,14 @@ async function handleFeishuMessage(params) {
3080
3307
  raw: ctx
3081
3308
  }),
3082
3309
  resolveTurn: () => ({
3310
+ cfg: effectiveCfg,
3083
3311
  channel: "feishu",
3084
3312
  accountId: route.accountId,
3085
- routeSessionKey: route.sessionKey,
3086
- storePath,
3313
+ route: {
3314
+ agentId: route.agentId,
3315
+ sessionKey: route.sessionKey
3316
+ },
3087
3317
  ctxPayload,
3088
- recordInboundSession: core.channel.session.recordInboundSession,
3089
3318
  record: {
3090
3319
  updateLastRoute: buildFeishuInboundLastRouteUpdate({
3091
3320
  sessionKey: route.sessionKey,
@@ -3101,36 +3330,16 @@ async function handleFeishuMessage(params) {
3101
3330
  historyMap: chatHistories,
3102
3331
  limit: historyLimit
3103
3332
  },
3104
- onPreDispatchFailure: () => core.channel.reply.settleReplyDispatcher({
3105
- dispatcher,
3106
- onSettled: () => markDispatchIdle()
3107
- }),
3108
- runDispatch: () => core.channel.reply.withReplyDispatcher({
3109
- dispatcher,
3110
- onSettled: () => {
3111
- markDispatchIdle();
3112
- },
3113
- run: () => core.channel.reply.dispatchReplyFromConfig({
3114
- ctx: ctxPayload,
3115
- cfg: effectiveCfg,
3116
- dispatcher,
3117
- replyOptions
3118
- })
3119
- })
3333
+ dispatcherOptions,
3334
+ delivery,
3335
+ replyOptions
3120
3336
  })
3121
3337
  }
3122
3338
  });
3123
3339
  if (!turnResult.dispatched) return;
3124
3340
  const { dispatchResult } = turnResult;
3125
3341
  const { queuedFinal, counts } = dispatchResult;
3126
- if (shouldSendNoVisibleReplyFallback({
3127
- ...dispatchResult,
3128
- failedCounts: dispatcher.getFailedCounts?.() ?? {
3129
- tool: 0,
3130
- block: 0,
3131
- final: 0
3132
- }
3133
- })) await ensureNoVisibleReplyFallback("dispatch-complete-no-visible-reply");
3342
+ if (shouldSendNoVisibleReplyFallback(dispatchResult)) await ensureNoVisibleReplyFallback("dispatch-complete-no-visible-reply");
3134
3343
  log(`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
3135
3344
  }
3136
3345
  } catch (err) {
@@ -3250,7 +3459,7 @@ function completeFeishuCardAction(actionId, accountId, now = Date.now()) {
3250
3459
  expiresAt
3251
3460
  });
3252
3461
  }
3253
- function buildSyntheticMessageEvent(event, content, chatType) {
3462
+ function buildSyntheticMessageEvent$1(event, content, chatType) {
3254
3463
  const replyTargetMessageId = event.context.open_message_id ?? event.open_message_id;
3255
3464
  const isTemporaryCardActionId = replyTargetMessageId?.startsWith("card-action-c-");
3256
3465
  const validReplyTargetId = replyTargetMessageId && !isTemporaryCardActionId ? replyTargetMessageId : void 0;
@@ -3286,7 +3495,7 @@ async function dispatchSyntheticCommand(params) {
3286
3495
  });
3287
3496
  await handleFeishuMessage({
3288
3497
  cfg: params.cfg,
3289
- event: buildSyntheticMessageEvent(params.event, params.command, resolvedChatType),
3498
+ event: buildSyntheticMessageEvent$1(params.event, params.command, resolvedChatType),
3290
3499
  botOpenId: params.botOpenId,
3291
3500
  runtime: params.runtime,
3292
3501
  channelRuntime: params.channelRuntime,
@@ -3931,41 +4140,39 @@ function createFeishuCommentReplyDispatcher(params) {
3931
4140
  accountId: params.accountId,
3932
4141
  runtime: params.runtime
3933
4142
  });
3934
- const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
3935
- responsePrefix: prefixContext.responsePrefix,
3936
- responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
3937
- humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
3938
- onReplyStart: async () => {
3939
- await typingReaction.start();
3940
- },
3941
- deliver: async (payload, info) => {
3942
- if (info.kind !== "final") return;
3943
- const reply = resolveSendableOutboundReplyParts(payload);
3944
- if (!reply.hasText) {
3945
- if (reply.hasMedia) params.runtime.log?.(`feishu[${params.accountId ?? "default"}]: comment reply ignored media-only payload for comment=${params.commentId}`);
3946
- 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();
3947
4153
  }
3948
- const chunks = core.channel.text.chunkTextWithMode(reply.text, textChunkLimit, chunkMode);
3949
- for (const chunk of chunks) await deliverCommentThreadText(client, {
3950
- file_token: params.fileToken,
3951
- file_type: params.fileType,
3952
- comment_id: params.commentId,
3953
- content: chunk,
3954
- is_whole_comment: params.isWholeComment
3955
- });
3956
4154
  },
3957
- onError: (err, info) => {
3958
- 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
+ }
3959
4175
  },
3960
- onCleanup: () => {
3961
- typingReaction.cleanup();
3962
- }
3963
- });
3964
- return {
3965
- dispatcher,
3966
- replyOptions,
3967
- markDispatchIdle,
3968
- markRunComplete,
3969
4176
  startTypingReaction: typingReaction.start,
3970
4177
  cleanupTypingReaction: typingReaction.cleanup
3971
4178
  };
@@ -4744,31 +4951,51 @@ async function handleFeishuCommentEvent(params) {
4744
4951
  fileToken: turn.fileToken
4745
4952
  });
4746
4953
  const bodyForAgent = `[message_id: ${turn.messageId}]\n${turn.prompt}`;
4747
- const ctxPayload = core.channel.reply.finalizeInboundContext({
4748
- Body: bodyForAgent,
4749
- BodyForAgent: bodyForAgent,
4750
- RawBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
4751
- CommandBody: turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt,
4752
- From: `feishu:${turn.senderId}`,
4753
- To: commentTarget,
4754
- SessionKey: commentSessionKey,
4755
- AccountId: route.accountId,
4756
- ChatType: "direct",
4757
- ConversationLabel: turn.documentTitle ? `Feishu comment · ${turn.documentTitle}` : "Feishu comment",
4758
- SenderName: turn.senderId,
4759
- SenderId: turn.senderId,
4760
- Provider: "feishu",
4761
- Surface: "feishu-comment",
4762
- MessageSid: turn.messageId,
4763
- MessageThreadId: turn.replyId,
4764
- Timestamp: parseTimestampMs(turn.timestamp),
4765
- WasMentioned: turn.isMentioned,
4766
- CommandAuthorized: false,
4767
- OriginatingChannel: "feishu",
4768
- 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
+ }
4769
4997
  });
4770
- const storePath = core.channel.session.resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
4771
- const { dispatcher, replyOptions, markDispatchIdle, markRunComplete, cleanupTypingReaction } = createFeishuCommentReplyDispatcher({
4998
+ const { dispatcherOptions, delivery, cleanupTypingReaction } = createFeishuCommentReplyDispatcher({
4772
4999
  cfg: effectiveCfg,
4773
5000
  agentId: route.agentId,
4774
5001
  runtime,
@@ -4779,7 +5006,6 @@ async function handleFeishuCommentEvent(params) {
4779
5006
  replyId: turn.replyId,
4780
5007
  isWholeComment: turn.isWholeComment
4781
5008
  });
4782
- let dispatchSettledBeforeStart = false;
4783
5009
  try {
4784
5010
  log(`feishu[${account.accountId}]: dispatching drive comment to agent (session=${commentSessionKey} comment=${turn.commentId} type=${turn.noticeType})`);
4785
5011
  const turnResult = await core.channel.inbound.run({
@@ -4796,34 +5022,19 @@ async function handleFeishuCommentEvent(params) {
4796
5022
  raw: turn
4797
5023
  }),
4798
5024
  resolveTurn: () => ({
5025
+ cfg: effectiveCfg,
4799
5026
  channel: "feishu",
4800
5027
  accountId: route.accountId,
4801
- routeSessionKey: commentSessionKey,
4802
- storePath,
5028
+ route: {
5029
+ agentId: route.agentId,
5030
+ sessionKey: commentSessionKey
5031
+ },
4803
5032
  ctxPayload,
4804
- recordInboundSession: core.channel.session.recordInboundSession,
4805
5033
  record: { onRecordError: (err) => {
4806
5034
  error(`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`);
4807
5035
  } },
4808
- onPreDispatchFailure: async () => {
4809
- dispatchSettledBeforeStart = true;
4810
- await core.channel.reply.settleReplyDispatcher({
4811
- dispatcher,
4812
- onSettled: () => {
4813
- markRunComplete();
4814
- markDispatchIdle();
4815
- }
4816
- });
4817
- },
4818
- runDispatch: () => core.channel.reply.withReplyDispatcher({
4819
- dispatcher,
4820
- run: () => core.channel.reply.dispatchReplyFromConfig({
4821
- ctx: ctxPayload,
4822
- cfg: effectiveCfg,
4823
- dispatcher,
4824
- replyOptions
4825
- })
4826
- })
5036
+ dispatcherOptions,
5037
+ delivery
4827
5038
  })
4828
5039
  }
4829
5040
  });
@@ -4836,10 +5047,6 @@ async function handleFeishuCommentEvent(params) {
4836
5047
  };
4837
5048
  log(`feishu[${account.accountId}]: drive comment dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final}, session=${commentSessionKey})`);
4838
5049
  } finally {
4839
- if (!dispatchSettledBeforeStart) {
4840
- markRunComplete();
4841
- markDispatchIdle();
4842
- }
4843
5050
  cleanupTypingReaction();
4844
5051
  }
4845
5052
  }
@@ -5489,6 +5696,120 @@ async function monitorWebhook({ account, accountId, runtime, abortSignal, eventD
5489
5696
  });
5490
5697
  }
5491
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
5492
5813
  //#region extensions/feishu/src/sequential-key.ts
5493
5814
  function getFeishuSequentialKey(params) {
5494
5815
  const { accountId, event, botOpenId, botName } = params;
@@ -5676,6 +5997,14 @@ function registerEventHandlers(eventDispatcher, context) {
5676
5997
  fireAndForget,
5677
5998
  abortSignal
5678
5999
  }),
6000
+ "vc.bot.meeting_invited_v1": createFeishuVcMeetingInvitedHandler({
6001
+ cfg,
6002
+ accountId,
6003
+ runtime,
6004
+ fireAndForget,
6005
+ channelRuntime,
6006
+ autoJoin: context.vcAutoJoin
6007
+ }),
5679
6008
  "im.message.reaction.created_v1": async (data) => {
5680
6009
  await runFeishuHandler({
5681
6010
  errorMessage: `feishu[${accountId}]: error handling reaction event`,
@@ -5803,6 +6132,7 @@ async function monitorSingleAccount(params) {
5803
6132
  runtime,
5804
6133
  chatHistories,
5805
6134
  fireAndForget: params.fireAndForget ?? true,
6135
+ vcAutoJoin: account.config.vcAutoJoin === true,
5806
6136
  abortSignal,
5807
6137
  ...params.statusSink ? { statusSink: params.statusSink } : {}
5808
6138
  });