@openclaw/feishu 2026.7.2-beta.7 → 2026.8.1-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.
Files changed (35) hide show
  1. package/dist/accounts-CCCdMen2.js +203 -0
  2. package/dist/api.js +61 -60
  3. package/dist/{channel-CScE82zY.js → channel-Ceq80_zP.js} +71 -42
  4. package/dist/channel-plugin-api.js +1 -1
  5. package/dist/{channel.runtime-B21e1E0r.js → channel.runtime-DnRyzGoU.js} +201 -152
  6. package/dist/{client-Dcbs6vml.js → client-Hp7uo_cl.js} +27 -16
  7. package/dist/contract-api.js +2 -2
  8. package/dist/{conversation-id-BeJL-wq7.js → conversation-id-VYgGQ-GX.js} +11 -15
  9. package/dist/doctor-contract-api.js +1 -1
  10. package/dist/doctor-contract-bEQXIXyP.js +158 -0
  11. package/dist/{drive-hSi_Utp0.js → drive-BVYM_Y4k.js} +325 -44
  12. package/dist/{media-CEUraFOR.js → media-BDouVaaM.js} +298 -182
  13. package/dist/{monitor-DngbaA6a.js → monitor-CZ8cDXrq.js} +3 -3
  14. package/dist/{monitor.account-BnTWCw55.js → monitor.account-Dq_voVMB.js} +263 -179
  15. package/dist/{monitor.startup-CqH2tiJq.js → monitor.startup-BGErejNH.js} +1 -1
  16. package/dist/{probe-p3POS2RN.js → probe-DVpy58s0.js} +2 -2
  17. package/dist/security-audit-D6Fz2h6p.js +23 -0
  18. package/dist/{send-result-B9_BpUPx.js → send-result-DEAycmOk.js} +18 -9
  19. package/dist/session-binding-contract-api.js +2 -2
  20. package/dist/{session-conversation-BksWrfzm.js → session-conversation-DFCIvQK-.js} +1 -1
  21. package/dist/session-key-api.js +1 -1
  22. package/dist/setup-api.js +1 -1
  23. package/dist/{subagent-hooks-Cx1cX7rW.js → subagent-hooks-KUi-7vUy.js} +4 -64
  24. package/dist/subagent-hooks-api.js +1 -1
  25. package/dist/{thread-bindings-N3wkgkIN.js → thread-bindings-CptM_YU6.js} +7 -9
  26. package/openclaw.plugin.json +143 -1
  27. package/package.json +4 -4
  28. package/skills/feishu-doc/SKILL.md +20 -195
  29. package/skills/feishu-doc/references/block-types.md +9 -14
  30. package/skills/feishu-drive/SKILL.md +16 -102
  31. package/skills/feishu-perm/SKILL.md +10 -110
  32. package/skills/feishu-wiki/SKILL.md +13 -109
  33. package/dist/accounts-u9X5Wsan.js +0 -469
  34. package/dist/doctor-contract-BiD9tyIv.js +0 -102
  35. package/dist/security-audit-D7WK_BHh.js +0 -11
@@ -1,21 +1,24 @@
1
- import { _ as normalizeCommentFileType, c as encodeQuery, d as isRecord$2, f as normalizeString, g as buildFeishuCommentTarget, h as requestFeishuApi, l as extractReplyText, m as readString$1, o as resolveFeishuAccount, p as parseCommentContentElements, s as resolveFeishuRuntimeAccount } from "./accounts-u9X5Wsan.js";
1
+ import { o as resolveFeishuAccount, s as resolveFeishuRuntimeAccount } from "./accounts-CCCdMen2.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-B9_BpUPx.js";
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-Dcbs6vml.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-DEAycmOk.js";
4
+ import { i as normalizeFeishuWebhookPath } from "./doctor-contract-bEQXIXyP.js";
5
+ import { o as resolveConfiguredFeishuGroupSessionScope, t as buildFeishuConversationId } from "./conversation-id-VYgGQ-GX.js";
6
+ import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-Hp7uo_cl.js";
6
7
  import { t as getFeishuRuntime } from "./runtime-C5JxBWZp.js";
7
- import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-hSi_Utp0.js";
8
- import { t as createFeishuThreadBindingManager } from "./thread-bindings-N3wkgkIN.js";
9
- import { createReplyPrefixContext, evaluateSupplementalContextVisibility, getSessionEntry, normalizeAgentId as normalizeAgentId$2, resolveChannelContextVisibilityMode } from "./runtime-api.js";
8
+ import { d as encodeQuery, f as extractReplyText, g as normalizeCommentFileType, h as buildFeishuCommentTarget, i as createCommentTypingReactionLifecycle, m as requestFeishuApi, p as parseCommentContentElements, s as getChatInfo, t as deliverCommentThreadText } from "./drive-BVYM_Y4k.js";
9
+ import { t as createFeishuThreadBindingManager } from "./thread-bindings-CptM_YU6.js";
10
+ import { createReplyPrefixContext, evaluateSupplementalContextVisibility, getSessionEntry, normalizeAgentId as normalizeAgentId$1, resolveChannelContextVisibilityMode } from "./runtime-api.js";
10
11
  import { t as readFeishuJsonResponse } from "./json-response-CheljwGr.js";
11
- import { _ as buildFeishuMediaFallbackText, a as getMessageFeishu, d as parsePostContent, f as extractMentionTargets, g as normalizeFeishuExternalKey, h as isFeishuGroupChatType, l as sendMessageFeishu, m as isMentionForwardRequest, n as sendMediaFeishu, o as listFeishuThreadMessages, p as isFeishuBroadcastMention, r as shouldSuppressFeishuTextForVoiceMedia, s as sendCardFeishu, t as saveMessageResourceFeishu, u as sendStructuredCardFeishu, v as resolveFeishuIdentityEmoji } from "./media-CEUraFOR.js";
12
- import { a as waitForAbortableDelay, i as raceWithTimeoutAndAbort } from "./probe-p3POS2RN.js";
13
- import { t as fetchBotIdentityForMonitor } from "./monitor.startup-CqH2tiJq.js";
12
+ import { _ as normalizeFeishuExternalKey, a as getMessageFeishu, d as extractMentionTargets, f as isFeishuBroadcastMention, g as parsePostContent, h as parseInteractiveCardContent, l as sendMessageFeishu, m as isFeishuGroupChatType, n as sendMediaFeishu, o as listFeishuThreadMessages, p as isMentionForwardRequest, r as shouldSuppressFeishuTextForVoiceMedia, s as sendCardFeishu, t as saveMessageResourceFeishu, u as sendStructuredCardFeishu, v as buildFeishuMediaFallbackText, y as resolveFeishuIdentityEmoji } from "./media-BDouVaaM.js";
13
+ import { a as waitForAbortableDelay, i as raceWithTimeoutAndAbort } from "./probe-DVpy58s0.js";
14
+ import { t as fetchBotIdentityForMonitor } from "./monitor.startup-BGErejNH.js";
15
+ import { resolveAgentConfig } from "openclaw/plugin-sdk/agent-scope-runtime";
14
16
  import { DEFAULT_INGRESS_ADOPTION_STALL_MS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, formatChannelProgressDraftLineForEntry, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelPreviewStreamMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
15
17
  import { createChannelPairingController, createChannelPairingController as createChannelPairingController$1 } from "openclaw/plugin-sdk/channel-pairing";
16
18
  import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
19
+ import { collectErrorGraphCandidates, formatErrorMessage, toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
17
20
  import { asDateTimestampMs, isFutureDateTimestampMs, parseStrictNonNegativeInteger, resolveDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveExpiresAtMsFromDurationSeconds, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
18
- import { asBoolean, asNullableRecord, isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, readStringValue, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
21
+ import { asBoolean, asNullableRecord, isRecord, normalizeLowercaseStringOrEmpty, normalizeNullableString, normalizeOptionalLowercaseString, normalizeOptionalString, readStringValue, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
19
22
  import { stripReasoningTagsFromText } from "openclaw/plugin-sdk/text-chunking";
20
23
  import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
21
24
  import { fanInChannelIngressLifecycles } from "openclaw/plugin-sdk/channel-ingress-runtime";
@@ -25,24 +28,24 @@ import path from "node:path";
25
28
  import { normalizeAccountId, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
26
29
  import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
27
30
  import { isRecord as isRecord$1 } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
28
- import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
31
+ import { createChannelPartialDeliveryError, formatAgentEnvelope, formatInboundMediaUnavailableText, isChannelPartialDeliveryError, recordChannelBotPairLoopAndCheckSuppression, resolveEnvelopeFormatOptions, resolveInboundReplyDispatchCounts, toInboundMediaFactsWithMetadata } from "openclaw/plugin-sdk/channel-inbound";
29
32
  import * as Lark from "@larksuiteoapi/node-sdk";
30
- import { formatReasoningMessage, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
33
+ import { resolvePinnedMainDmOwnerFromAllowlist, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
31
34
  import { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, applyBasicWebhookRequestGuards, createFixedWindowRateLimiter, createWebhookAnomalyTracker, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
32
35
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
33
36
  import { getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts, resolveTextChunksWithFallback, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
34
- import { resolvePinnedMainDmOwnerFromAllowlist, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
35
37
  import * as crypto$1 from "node:crypto";
36
38
  import crypto, { createHash } from "node:crypto";
37
- import { buildChannelInboundEventContext, createChannelPartialDeliveryError, formatAgentEnvelope, formatInboundMediaUnavailableText, isChannelPartialDeliveryError, recordChannelBotPairLoopAndCheckSuppression, resolveEnvelopeFormatOptions, toInboundMediaFactsWithMetadata } from "openclaw/plugin-sdk/channel-inbound";
38
39
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
39
40
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
40
41
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
41
42
  import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
42
43
  import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
44
+ import { formatReasoningMessage, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
43
45
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
44
46
  import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
45
47
  import * as http from "node:http";
48
+ import { channelBlockedPatch, channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
46
49
  import { installRequestBodyLimitGuard, readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
47
50
  import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
48
51
  //#region extensions/feishu/src/bot-agent-body.ts
@@ -321,6 +324,7 @@ function parseMessageContent(content, messageType) {
321
324
  return "[Forwarded message]";
322
325
  }
323
326
  if (messageType === "merge_forward") return "[Merged and Forwarded Message - loading...]";
327
+ if (messageType === "interactive") return parseInteractiveCardContent(parsed);
324
328
  return content;
325
329
  } catch {
326
330
  return FEISHU_MEDIA_MESSAGE_TYPES.has(messageType) ? "" : content;
@@ -345,6 +349,7 @@ function formatSubMessageContent(content, contentType) {
345
349
  switch (contentType) {
346
350
  case "text": return parsed.text || content;
347
351
  case "post": return parsePostContent(content).textContent;
352
+ case "interactive": return parseInteractiveCardContent(parsed);
348
353
  case "image": return "[Image]";
349
354
  case "file": return `[File: ${parsed.file_name || "unknown"}]`;
350
355
  case "audio": return "[Audio]";
@@ -369,7 +374,8 @@ function parseMergeForwardContent(params) {
369
374
  return "[Merged and Forwarded Message - parse error]";
370
375
  }
371
376
  if (!Array.isArray(items) || items.length === 0) return "[Merged and Forwarded Message - no sub-messages]";
372
- const subMessages = items.filter((item) => item.upper_message_id);
377
+ const container = items.find((item) => item.msg_type === "merge_forward" && !item.upper_message_id);
378
+ const subMessages = container ? items.filter((item) => item !== container) : items.filter((item) => item.upper_message_id);
373
379
  if (subMessages.length === 0) return "[Merged and Forwarded Message - no sub-messages found]";
374
380
  log?.(`feishu: merge_forward contains ${subMessages.length} sub-messages`);
375
381
  subMessages.sort((a, b) => (parseStrictNonNegativeInteger(a.create_time) ?? 0) - (parseStrictNonNegativeInteger(b.create_time) ?? 0));
@@ -1133,13 +1139,8 @@ function resolveUserPath(p) {
1133
1139
  }
1134
1140
  //#endregion
1135
1141
  //#region extensions/feishu/src/agent-config.ts
1136
- const DEFAULT_AGENT_ID = "main";
1137
- function normalizeAgentId$1(value) {
1138
- return (value ?? "").trim().toLowerCase() || DEFAULT_AGENT_ID;
1139
- }
1140
1142
  function resolveFeishuConfigReasoningDefault(cfg, agentId) {
1141
- const id = normalizeAgentId$1(agentId);
1142
- return cfg.agents?.list?.find((entry) => normalizeAgentId$1(entry?.id) === id)?.reasoningDefault ?? cfg.agents?.defaults?.reasoningDefault ?? "off";
1143
+ return resolveAgentConfig(cfg, agentId)?.reasoningDefault ?? cfg.agents?.defaults?.reasoningDefault ?? "off";
1143
1144
  }
1144
1145
  //#endregion
1145
1146
  //#region extensions/feishu/src/reasoning-preview.ts
@@ -1185,10 +1186,11 @@ function createFeishuReplyDeliveryResult(params) {
1185
1186
  /** Preserves the first result's provider identity while retaining supplemental ids. */
1186
1187
  function mergeFeishuReplyDeliveryResults(results, content) {
1187
1188
  const visible = results.filter((result) => result.visibleReplySent === true);
1189
+ const acceptedContent = visible.flatMap((result) => result.content === void 0 ? [] : [result.content]);
1188
1190
  return createFeishuReplyDeliveryResult({
1189
1191
  results: visible,
1190
1192
  visibleReplySent: visible.length > 0,
1191
- content: content === void 0 ? results.find((result) => result.content !== void 0)?.content : content
1193
+ content: content === void 0 ? acceptedContent.length > 0 ? acceptedContent.join("\n\n") : void 0 : content
1192
1194
  });
1193
1195
  }
1194
1196
  /** Keeps a failed outcome while exposing the provider-visible subset to core lifecycle hooks. */
@@ -1242,8 +1244,14 @@ function resolveAllowedHostnames(domain) {
1242
1244
  }
1243
1245
  return ["open.feishu.cn"];
1244
1246
  }
1247
+ function cancelUnreadResponseBody(response) {
1248
+ if (!response.bodyUsed) response.body?.cancel().catch(() => void 0);
1249
+ }
1245
1250
  async function assertSuccessfulCardKitResponse(response, auditContext, action) {
1246
- if (!response.ok) throw new Error(`${action} failed with HTTP ${response.status}`);
1251
+ if (!response.ok) {
1252
+ cancelUnreadResponseBody(response);
1253
+ throw new Error(`${action} failed with HTTP ${response.status}`);
1254
+ }
1247
1255
  const data = await readFeishuJsonResponse(response, auditContext);
1248
1256
  if (data.code !== 0) throw new Error(`${action} failed: ${data.msg ?? "unknown error"} (code=${String(data.code)})`);
1249
1257
  }
@@ -1276,7 +1284,10 @@ async function getToken(creds, deps) {
1276
1284
  });
1277
1285
  let data;
1278
1286
  try {
1279
- if (!response.ok) throw new Error(`Token request failed with HTTP ${response.status}`);
1287
+ if (!response.ok) {
1288
+ cancelUnreadResponseBody(response);
1289
+ throw new Error(`Token request failed with HTTP ${response.status}`);
1290
+ }
1280
1291
  data = await readFeishuJsonResponse(response, "feishu.streaming-card.token");
1281
1292
  } finally {
1282
1293
  await release();
@@ -1384,7 +1395,10 @@ var FeishuStreamingSession = class {
1384
1395
  });
1385
1396
  let createData;
1386
1397
  try {
1387
- if (!createRes.ok) throw new Error(`Create card request failed with HTTP ${createRes.status}`);
1398
+ if (!createRes.ok) {
1399
+ cancelUnreadResponseBody(createRes);
1400
+ throw new Error(`Create card request failed with HTTP ${createRes.status}`);
1401
+ }
1388
1402
  createData = await readFeishuJsonResponse(createRes, "feishu.streaming-card.create");
1389
1403
  } finally {
1390
1404
  await releaseCreate();
@@ -1821,9 +1835,6 @@ function mergeStreamingFinalText(previousText, nextText, appendError) {
1821
1835
  if (previousText.endsWith(`\n\n${nextText}`)) return previousText;
1822
1836
  return `${previousText}\n\n${nextText}`;
1823
1837
  }
1824
- function toError(error) {
1825
- return error instanceof Error ? error : new Error(String(error));
1826
- }
1827
1838
  /** Maximum age (ms) for a message to receive a typing indicator reaction.
1828
1839
  * Messages older than this are likely replays after context compaction (#30418). */
1829
1840
  const TYPING_INDICATOR_MAX_AGE_MS = 2 * 6e4;
@@ -2191,9 +2202,10 @@ function createFeishuReplyDispatcher(params) {
2191
2202
  };
2192
2203
  const updateStreamingStatusLine = (nextStatusLine, options) => {
2193
2204
  statusLine = nextStatusLine;
2194
- if (!Boolean(streaming?.isActive() || streamingStartPromise) && (options?.startIfNeeded === false || renderMode !== "card")) return;
2205
+ if (!Boolean(streaming?.isActive() || streamingStartPromise) && (options?.startIfNeeded === false || renderMode !== "card")) return false;
2195
2206
  startStreaming();
2196
2207
  flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2208
+ return false;
2197
2209
  };
2198
2210
  const sendChunkedTextReply = async (paramsLocal) => {
2199
2211
  const chunkSource = paramsLocal.useCard ? paramsLocal.text : materializeFeishuPostMarkdownSoftBreaks(core.channel.text.convertMarkdownTables(paramsLocal.text, tableMode));
@@ -2220,12 +2232,20 @@ function createFeishuReplyDispatcher(params) {
2220
2232
  acceptedChunks.push(chunk);
2221
2233
  markVisibleReplySent();
2222
2234
  } catch (error) {
2223
- throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
2224
- results,
2225
- visibleReplySent: results.length > 0,
2226
- content: acceptedChunks.join(""),
2227
- kind: paramsLocal.useCard ? "card" : "text"
2228
- }));
2235
+ const acceptedChunk = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
2236
+ if (acceptedChunk) {
2237
+ acceptedChunks.push(acceptedChunk.content ?? chunk);
2238
+ markVisibleReplySent();
2239
+ }
2240
+ throw createFeishuPartialReplyDeliveryError(error, {
2241
+ ...acceptedChunk,
2242
+ ...createFeishuReplyDeliveryResult({
2243
+ results,
2244
+ visibleReplySent: results.length > 0 || acceptedChunk !== void 0,
2245
+ content: acceptedChunks.join(""),
2246
+ kind: paramsLocal.useCard ? "card" : "text"
2247
+ })
2248
+ });
2229
2249
  }
2230
2250
  }
2231
2251
  if (paramsLocal.infoKind === "final") deliveredFinalTexts.add(paramsLocal.text);
@@ -2280,7 +2300,11 @@ function createFeishuReplyDispatcher(params) {
2280
2300
  markVisibleReplySent();
2281
2301
  if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) degradedVoiceFallbackText = options.fallbackText;
2282
2302
  },
2283
- onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
2303
+ onError: options?.fallbackText === void 0 ? void 0 : async ({ error, mediaUrl }) => {
2304
+ if (isChannelPartialDeliveryError(error)) {
2305
+ markVisibleReplySent();
2306
+ throw toStringifiedError(error);
2307
+ }
2284
2308
  const fallbackText = await buildFeishuMediaFallbackText({
2285
2309
  text: sentFallbackText ? void 0 : options.fallbackText,
2286
2310
  mediaUrl
@@ -2295,6 +2319,7 @@ function createFeishuReplyDispatcher(params) {
2295
2319
  }
2296
2320
  } catch (error) {
2297
2321
  const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
2322
+ if (partial) markVisibleReplySent();
2298
2323
  throw createFeishuPartialReplyDeliveryError(error, mergeFeishuReplyDeliveryResults([...results, ...partial ? [partial] : []]));
2299
2324
  }
2300
2325
  return mergeFeishuReplyDeliveryResults(results);
@@ -2396,7 +2421,7 @@ function createFeishuReplyDispatcher(params) {
2396
2421
  if (deliveryError !== void 0) completion.reject(createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(deliveryError) && deliveryError instanceof Error ? deliveryError.cause ?? deliveryError : deliveryError instanceof FeishuStreamingFinalizationError ? deliveryError.cause ?? deliveryError : deliveryError, settledResult));
2397
2422
  else completion.resolve(settledResult);
2398
2423
  }
2399
- if (closeOutcome.error !== void 0) throw toError(closeOutcome.error);
2424
+ if (closeOutcome.error !== void 0) throw toStringifiedError(closeOutcome.error);
2400
2425
  } while (pendingStreamingDeliveries.length > 0);
2401
2426
  } finally {
2402
2427
  typingCallbacks?.onIdle?.();
@@ -2486,6 +2511,7 @@ function createFeishuReplyDispatcher(params) {
2486
2511
  }
2487
2512
  };
2488
2513
  const handleDeliveryError = async (error, info) => {
2514
+ if (isChannelPartialDeliveryError(error)) markVisibleReplySent();
2489
2515
  params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
2490
2516
  await queueIdleSideEffects().catch((cleanupError) => params.runtime.error?.(`feishu[${account.accountId}] reply error cleanup failed: ${String(cleanupError)}`));
2491
2517
  };
@@ -2524,7 +2550,7 @@ function createFeishuReplyDispatcher(params) {
2524
2550
  const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText && !ttsTextAlreadyVisible || skipTextForDuplicateFinal));
2525
2551
  const priorClosedStreamingSettlement = info?.kind === "final" && hasText && skipTextForDuplicateFinal ? claimClosedStreamingResult(void 0, text) : void 0;
2526
2552
  if (!shouldDeliverText && !hasMedia) {
2527
- if (priorClosedStreamingSettlement?.error !== void 0) throw toError(priorClosedStreamingSettlement.error);
2553
+ if (priorClosedStreamingSettlement?.error !== void 0) throw toStringifiedError(priorClosedStreamingSettlement.error);
2528
2554
  return priorClosedStreamingSettlement?.result ?? noVisibleFeishuReplyDelivery;
2529
2555
  }
2530
2556
  const deliveredResults = priorClosedStreamingSettlement ? [priorClosedStreamingSettlement.result] : [];
@@ -2644,7 +2670,7 @@ function createFeishuReplyDispatcher(params) {
2644
2670
  }
2645
2671
  }
2646
2672
  if (hasMedia) await collectMediaDelivery(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
2647
- const result = mergeFeishuReplyDeliveryResults(deliveredResults, text);
2673
+ const result = mergeFeishuReplyDeliveryResults(deliveredResults, hasVoiceMedia ? deliveredResults.at(-1)?.content ?? text : text);
2648
2674
  if (priorClosedStreamingSettlement?.error !== void 0) throw createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(priorClosedStreamingSettlement.error) && priorClosedStreamingSettlement.error instanceof Error ? priorClosedStreamingSettlement.error.cause ?? priorClosedStreamingSettlement.error : priorClosedStreamingSettlement.error, result);
2649
2675
  return result;
2650
2676
  },
@@ -2654,43 +2680,40 @@ function createFeishuReplyDispatcher(params) {
2654
2680
  onModelSelected: prefixContext.onModelSelected,
2655
2681
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : true,
2656
2682
  onPartialReply: previewStreamingEnabled ? (payload) => {
2657
- if (!payload.text) return;
2683
+ if (!payload.text) return false;
2658
2684
  const cleaned = stripReasoningTagsFromText(payload.text, {
2659
2685
  mode: "strict",
2660
2686
  trim: "both"
2661
2687
  });
2662
- if (!cleaned) return;
2688
+ if (!cleaned) return false;
2663
2689
  startStreaming();
2664
2690
  queueStreamingUpdate(cleaned, {
2665
2691
  dedupeWithLastPartial: true,
2666
2692
  mode: "snapshot"
2667
2693
  });
2694
+ return false;
2668
2695
  } : void 0,
2669
2696
  onReasoningStream: reasoningPreviewEnabled ? (payload) => {
2670
- if (!payload.text) return;
2697
+ if (!payload.text) return false;
2671
2698
  startStreaming();
2672
2699
  queueReasoningUpdate(formatReasoningMessage(payload.text));
2700
+ return false;
2673
2701
  } : void 0,
2674
- onReasoningEnd: reasoningPreviewEnabled ? () => {} : void 0,
2702
+ onReasoningEnd: reasoningPreviewEnabled ? () => false : void 0,
2675
2703
  onToolStart: previewStreamingEnabled ? (payload) => {
2676
- if (!isChannelProgressDraftWorkToolName(payload.name)) return;
2704
+ if (!isChannelProgressDraftWorkToolName(payload.name)) return false;
2677
2705
  const statusLineLocal = formatChannelProgressDraftLineForEntry(account.config, {
2678
2706
  event: "tool",
2679
2707
  name: payload.name,
2680
2708
  phase: payload.phase,
2681
2709
  args: payload.args
2682
2710
  }, { detailMode: payload.detailMode });
2683
- if (statusLineLocal) updateStreamingStatusLine(statusLineLocal);
2684
- } : void 0,
2685
- onAssistantMessageStart: previewStreamingEnabled ? () => {
2686
- updateStreamingStatusLine("", { startIfNeeded: false });
2711
+ if (statusLineLocal) return updateStreamingStatusLine(statusLineLocal);
2712
+ return false;
2687
2713
  } : void 0,
2688
- onCompactionStart: previewStreamingEnabled ? () => {
2689
- updateStreamingStatusLine("📦 **Compacting context...**");
2690
- } : void 0,
2691
- onCompactionEnd: previewStreamingEnabled ? () => {
2692
- updateStreamingStatusLine("");
2693
- } : void 0
2714
+ onAssistantMessageStart: previewStreamingEnabled ? () => updateStreamingStatusLine("", { startIfNeeded: false }) : void 0,
2715
+ onCompactionStart: previewStreamingEnabled ? () => updateStreamingStatusLine("📦 **Compacting context...**") : void 0,
2716
+ onCompactionEnd: previewStreamingEnabled ? () => updateStreamingStatusLine("") : void 0
2694
2717
  },
2695
2718
  ensureNoVisibleReplyFallback,
2696
2719
  getVisibleReplyState: () => ({
@@ -2715,11 +2738,9 @@ function getFeishuSyntheticDirectPreDispatchTarget(event) {
2715
2738
  const permissionErrorNotifiedAt = /* @__PURE__ */ new Map();
2716
2739
  const PERMISSION_ERROR_COOLDOWN_MS = 300 * 1e3;
2717
2740
  function shouldSendNoVisibleReplyFallback(dispatchResult) {
2718
- const finalCount = dispatchResult.counts.final ?? 0;
2719
- const failedFinalCount = dispatchResult.failedCounts?.final ?? 0;
2720
- const emptyEligibleDispatch = dispatchResult.noVisibleReplyFallbackEligible === true && dispatchResult.queuedFinal !== true && finalCount === 0;
2721
- const queuedFinalFailed = dispatchResult.queuedFinal === true && failedFinalCount > 0;
2722
- return dispatchResult.sendPolicyDenied !== true && dispatchResult.sourceReplyDeliveryMode !== "message_tool_only" && (emptyEligibleDispatch || queuedFinalFailed);
2741
+ const emptyEligibleDispatch = dispatchResult.noVisibleReplyFallbackEligible === true && dispatchResult.settledReceipt?.anyVisibleDelivered !== true;
2742
+ const finalFailedBeforeSend = (dispatchResult.settledReceipt?.counts.final.failedBeforeSend ?? 0) > 0;
2743
+ return dispatchResult.sendPolicyDenied !== true && dispatchResult.sourceReplyDeliveryMode !== "message_tool_only" && (emptyEligibleDispatch || finalFailedBeforeSend);
2723
2744
  }
2724
2745
  function isFeishuTopicSessionScope(scope) {
2725
2746
  return scope === "group_topic" || scope === "group_topic_sender";
@@ -2892,7 +2913,10 @@ async function handleFeishuMessage(params) {
2892
2913
  if (event.message.message_type === "merge_forward") {
2893
2914
  log(`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`);
2894
2915
  try {
2895
- const response = await createFeishuClient(account).im.message.get({ path: { message_id: event.message.message_id } });
2916
+ const response = await createFeishuClient(account).im.message.get({
2917
+ params: { card_msg_content_type: "user_card_content" },
2918
+ path: { message_id: event.message.message_id }
2919
+ });
2896
2920
  if (response.code === 0 && response.data?.items && response.data.items.length > 0) {
2897
2921
  log(`feishu[${account.accountId}]: merge_forward API returned ${response.data.items.length} items`);
2898
2922
  const expandedContent = parseMergeForwardContent({
@@ -2963,22 +2987,25 @@ async function handleFeishuMessage(params) {
2963
2987
  feishuCfg
2964
2988
  }) : null;
2965
2989
  let effectiveThreadId = ctx.threadId;
2966
- if (isGroup && ctx.chatType === "topic_group" && !effectiveThreadId && isFeishuTopicSessionScope(groupSessionScope ?? "group")) try {
2967
- const hydratedThreadId = (await getMessageFeishu({
2968
- cfg,
2969
- accountId: account.accountId,
2970
- messageId: ctx.messageId
2971
- }))?.threadId?.trim();
2972
- if (hydratedThreadId) {
2973
- ctx = {
2974
- ...ctx,
2975
- threadId: hydratedThreadId
2976
- };
2977
- effectiveThreadId = hydratedThreadId;
2978
- log(`feishu[${account.accountId}]: hydrated topic thread_id=${hydratedThreadId} for message=${ctx.messageId}`);
2990
+ if (isGroup && ctx.chatType === "topic_group" && !effectiveThreadId && isFeishuTopicSessionScope(groupSessionScope ?? "group")) {
2991
+ const topicHydrationMessageId = ctx.replyTargetMessageId ?? ctx.messageId;
2992
+ try {
2993
+ const hydratedThreadId = (await getMessageFeishu({
2994
+ cfg,
2995
+ accountId: account.accountId,
2996
+ messageId: topicHydrationMessageId
2997
+ }))?.threadId?.trim();
2998
+ if (hydratedThreadId) {
2999
+ ctx = {
3000
+ ...ctx,
3001
+ threadId: hydratedThreadId
3002
+ };
3003
+ effectiveThreadId = hydratedThreadId;
3004
+ log(`feishu[${account.accountId}]: hydrated topic thread_id=${hydratedThreadId} for message=${topicHydrationMessageId}`);
3005
+ }
3006
+ } catch (err) {
3007
+ log(`feishu[${account.accountId}]: failed to hydrate topic thread_id for message=${topicHydrationMessageId}: ${String(err)}`);
2979
3008
  }
2980
- } catch (err) {
2981
- log(`feishu[${account.accountId}]: failed to hydrate topic thread_id for message=${ctx.messageId}: ${String(err)}`);
2982
3009
  }
2983
3010
  const effectiveGroupSenderAllowFrom = isGroup ? (groupConfig?.allowFrom?.length ?? 0) > 0 ? groupConfig?.allowFrom ?? [] : feishuCfg?.groupSenderAllowFrom ?? [] : [];
2984
3011
  const groupSession = isGroup ? resolveFeishuGroupSession({
@@ -2995,7 +3022,7 @@ async function handleFeishuMessage(params) {
2995
3022
  const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
2996
3023
  const configAllowFrom = feishuCfg?.allowFrom ?? [];
2997
3024
  const rawBroadcastAgents = isGroup ? resolveBroadcastAgents(cfg, ctx.chatId) : null;
2998
- const broadcastAgents = rawBroadcastAgents ? uniqueStrings(rawBroadcastAgents.map((id) => normalizeAgentId$2(id))) : null;
3025
+ const broadcastAgents = rawBroadcastAgents ? uniqueStrings(rawBroadcastAgents.map((id) => normalizeAgentId$1(id))) : null;
2999
3026
  const messageCreateTimeMs = parseStrictNonNegativeInteger(event.message.create_time) ?? Date.now();
3000
3027
  let requireMention = false;
3001
3028
  if (isGroup) {
@@ -3524,7 +3551,37 @@ async function handleFeishuMessage(params) {
3524
3551
  const buildCtxPayloadForAgent = async (agentId, agentSessionKey, agentAccountId, wasMentioned) => {
3525
3552
  const groupName = await resolveGroupNameForLabel();
3526
3553
  const threadContext = await resolveThreadContextForAgent(agentId, agentSessionKey, groupName);
3527
- return buildChannelInboundEventContext({
3554
+ const contextBinding = {
3555
+ agentId,
3556
+ sessionKey: agentSessionKey,
3557
+ messageId: ctx.messageId,
3558
+ inboundEventKind: "user_request"
3559
+ };
3560
+ const boundChannelIngress = isGroup ? await resolveFeishuGroupSenderActivationIngressAccess({
3561
+ cfg: effectiveCfg,
3562
+ accountId: account.accountId,
3563
+ chatId: ctx.chatId,
3564
+ allowFrom: effectiveGroupSenderAllowFrom,
3565
+ senderOpenId: ctx.senderOpenId,
3566
+ senderUserId,
3567
+ requireMention,
3568
+ mentionedBot: ctx.mentionedBot,
3569
+ contextBinding,
3570
+ threadId: ctx.rootId && isTopicSessionForThread ? ctx.rootId : void 0
3571
+ }) : await resolveFeishuDmIngressAccess({
3572
+ cfg: effectiveCfg,
3573
+ accountId: account.accountId,
3574
+ dmPolicy: effectiveDmPolicy,
3575
+ allowFrom: effectiveConfigAllowFrom,
3576
+ readAllowFromStore: pairing.readAllowFromStore,
3577
+ senderOpenId: ctx.senderOpenId,
3578
+ senderUserId,
3579
+ conversationId: ctx.senderOpenId,
3580
+ mayPair: false,
3581
+ contextBinding
3582
+ });
3583
+ return core.channel.inbound.buildContext({
3584
+ channelIngress: boundChannelIngress,
3528
3585
  channel: "feishu",
3529
3586
  supplemental: {
3530
3587
  quote: quotedContent ? {
@@ -3550,7 +3607,12 @@ async function handleFeishuMessage(params) {
3550
3607
  conversation: {
3551
3608
  kind: isGroup ? "group" : "direct",
3552
3609
  id: ctx.chatId,
3610
+ routePeer: {
3611
+ kind: isGroup ? "group" : "direct",
3612
+ id: peerId
3613
+ },
3553
3614
  nativeChannelId: ctx.chatId,
3615
+ parentId: parentPeer?.id,
3554
3616
  label: isGroup && groupName && !isTopicSessionForThread ? groupName : void 0,
3555
3617
  threadId: ctx.rootId && isTopicSessionForThread ? ctx.rootId : void 0
3556
3618
  },
@@ -3651,8 +3713,8 @@ async function handleFeishuMessage(params) {
3651
3713
  }
3652
3714
  };
3653
3715
  const strategy = cfg.broadcast?.strategy === "sequential" ? "sequential" : "parallel";
3654
- const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId$2(route.agentId) : null;
3655
- const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId$2(a.id));
3716
+ const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId$1(route.agentId) : null;
3717
+ const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId$1(a.id));
3656
3718
  const hasKnownAgents = agentIds.length > 0;
3657
3719
  log(`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`);
3658
3720
  const createBroadcastInboundAdapter = (paramsLocal) => ({
@@ -3692,7 +3754,7 @@ async function handleFeishuMessage(params) {
3692
3754
  })
3693
3755
  });
3694
3756
  const dispatchForAgent = async (agentId) => {
3695
- const normalizedAgentId = normalizeAgentId$2(agentId);
3757
+ const normalizedAgentId = normalizeAgentId$1(agentId);
3696
3758
  if (hasKnownAgents && !agentIds.includes(normalizedAgentId)) {
3697
3759
  log(`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`);
3698
3760
  return;
@@ -4293,9 +4355,6 @@ const FEISHU_INGRESS_PAYLOAD_VERSION = 1;
4293
4355
  const FEISHU_INGRESS_POLL_INTERVAL_MS = 500;
4294
4356
  const FEISHU_DURABLE_EVENT_TYPES = /* @__PURE__ */ new Set(["drive.notice.comment_add_v1", "im.message.receive_v1"]);
4295
4357
  const FeishuIngressPermanentError = createChannelIngressError("FeishuIngressPermanentError", { withReason: true });
4296
- function readString(value) {
4297
- return typeof value === "string" && value.trim() ? value.trim() : null;
4298
- }
4299
4358
  function parseRawEnvelope(rawEnvelope) {
4300
4359
  let parsed;
4301
4360
  try {
@@ -4307,7 +4366,7 @@ function parseRawEnvelope(rawEnvelope) {
4307
4366
  return parsed;
4308
4367
  }
4309
4368
  function decryptEnvelope(envelope, encryptKey) {
4310
- const encrypted = readString(envelope.encrypt);
4369
+ const encrypted = normalizeNullableString(envelope.encrypt);
4311
4370
  if (!encrypted) return envelope;
4312
4371
  const key = encryptKey?.trim();
4313
4372
  if (!key) throw new FeishuIngressPermanentError("authentication-failed", "Feishu encrypted ingress envelope has no configured encrypt key.");
@@ -4322,13 +4381,13 @@ function inspectFeishuIngressEnvelope(rawEnvelope, encryptKey, allowInvalidLane
4322
4381
  const envelope = decryptEnvelope(parseRawEnvelope(rawEnvelope), encryptKey);
4323
4382
  const nestedHeader = isRecord$1(envelope.header) ? envelope.header : null;
4324
4383
  const nestedEvent = isRecord$1(envelope.event) ? envelope.event : null;
4325
- const eventType = readString(nestedHeader?.event_type) ?? readString(envelope.event_type);
4384
+ const eventType = normalizeNullableString(nestedHeader?.event_type) ?? normalizeNullableString(envelope.event_type);
4326
4385
  if (!eventType || !FEISHU_DURABLE_EVENT_TYPES.has(eventType)) return null;
4327
- const eventId = readString(nestedHeader?.event_id) ?? readString(envelope.event_id);
4386
+ const eventId = normalizeNullableString(nestedHeader?.event_id) ?? normalizeNullableString(envelope.event_id);
4328
4387
  if (!eventId) throw new FeishuIngressPermanentError("invalid-event", `Feishu ${eventType} envelope is missing header.event_id.`);
4329
4388
  const event = nestedEvent ?? envelope;
4330
4389
  if (eventType === "im.message.receive_v1") {
4331
- const chatId = readString((isRecord$1(event.message) ? event.message : null)?.chat_id);
4390
+ const chatId = normalizeNullableString((isRecord$1(event.message) ? event.message : null)?.chat_id);
4332
4391
  if (!chatId) {
4333
4392
  if (allowInvalidLane) return {
4334
4393
  eventId,
@@ -4344,8 +4403,8 @@ function inspectFeishuIngressEnvelope(rawEnvelope, encryptKey, allowInvalidLane
4344
4403
  };
4345
4404
  }
4346
4405
  const noticeMeta = isRecord$1(event.notice_meta) ? event.notice_meta : null;
4347
- const fileType = readString(noticeMeta?.file_type);
4348
- const documentId = readString(noticeMeta?.file_token);
4406
+ const fileType = normalizeNullableString(noticeMeta?.file_type);
4407
+ const documentId = normalizeNullableString(noticeMeta?.file_token);
4349
4408
  if (!fileType || !documentId) {
4350
4409
  if (allowInvalidLane) return {
4351
4410
  eventId,
@@ -4580,7 +4639,7 @@ function createFeishuDurableIngress(options) {
4580
4639
  invoke,
4581
4640
  invokeWebhook,
4582
4641
  resolveLifecycle: (data) => {
4583
- const eventId = isRecord$1(data) ? readString(data.event_id) : null;
4642
+ const eventId = isRecord$1(data) ? normalizeNullableString(data.event_id) : null;
4584
4643
  return eventId ? activeLifecycles.get(eventId) : void 0;
4585
4644
  },
4586
4645
  setSocketTerminator: (terminate) => {
@@ -4777,7 +4836,9 @@ function startBotIdentityRecovery(params) {
4777
4836
  const log = runtime?.log ?? console.log;
4778
4837
  log(`feishu[${accountId}]: bot open_id ${currentSource === "cache" ? "loaded from cache" : "unknown"}; starting background provider refresh (delays: ${BOT_IDENTITY_RETRY_DELAYS_MS.map((delay) => `${delay / 1e3}s`).join(", ")})`);
4779
4838
  if (currentSource !== "cache") log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
4780
- retryBotIdentityProbe(account, accountId, runtime, abortSignal);
4839
+ retryBotIdentityProbe(account, accountId, runtime, abortSignal).catch((err) => {
4840
+ (runtime?.error ?? console.error)(`feishu[${accountId}]: bot identity background retry failed unexpectedly: ${String(err)}`);
4841
+ });
4781
4842
  }
4782
4843
  //#endregion
4783
4844
  //#region extensions/feishu/src/card-ux-launcher.ts
@@ -5094,7 +5155,7 @@ function safeJsonStringify(value) {
5094
5155
  }
5095
5156
  }
5096
5157
  function truncatePromptText(text, maxLength = FEISHU_PROMPT_TEXT_LIMIT) {
5097
- const normalized = normalizeString(text);
5158
+ const normalized = normalizeOptionalString(text);
5098
5159
  if (!normalized) return "";
5099
5160
  return normalized.length > maxLength ? `${sliceUtf16Safe(normalized, 0, maxLength - 1)}…` : normalized;
5100
5161
  }
@@ -5160,7 +5221,7 @@ async function resolveParsedCommentContent(params) {
5160
5221
  return null;
5161
5222
  }
5162
5223
  const objType = normalizeCommentFileType(response.data?.node?.obj_type);
5163
- const objToken = normalizeString(response.data?.node?.obj_token);
5224
+ const objToken = normalizeOptionalString(response.data?.node?.obj_token);
5164
5225
  if (!objType || !objToken) return null;
5165
5226
  return {
5166
5227
  resolvedObjType: objType,
@@ -5229,18 +5290,18 @@ async function fetchDriveComments(params) {
5229
5290
  }
5230
5291
  async function requestFeishuOpenApi(params) {
5231
5292
  const formatErrorDetails = (error) => {
5232
- if (!isRecord$2(error)) return typeof error === "string" ? error : JSON.stringify(error);
5233
- const response = isRecord$2(error.response) ? error.response : void 0;
5234
- const responseData = isRecord$2(response?.data) ? response?.data : void 0;
5293
+ if (!isRecord(error)) return typeof error === "string" ? error : JSON.stringify(error);
5294
+ const response = isRecord(error.response) ? error.response : void 0;
5295
+ const responseData = isRecord(response?.data) ? response?.data : void 0;
5235
5296
  return safeJsonStringify({
5236
5297
  message: typeof error.message === "string" ? error.message : typeof error === "string" ? error : JSON.stringify(error),
5237
- code: readString$1(error.code),
5238
- method: readString$1(isRecord$2(error.config) ? error.config.method : void 0),
5239
- url: readString$1(isRecord$2(error.config) ? error.config.url : void 0),
5298
+ code: readStringValue(error.code),
5299
+ method: readStringValue(isRecord(error.config) ? error.config.method : void 0),
5300
+ url: readStringValue(isRecord(error.config) ? error.config.url : void 0),
5240
5301
  http_status: typeof response?.status === "number" ? response.status : void 0,
5241
- feishu_code: typeof responseData?.code === "number" ? responseData.code : readString$1(responseData?.code),
5242
- feishu_msg: readString$1(responseData?.msg),
5243
- feishu_log_id: readString$1(responseData?.log_id)
5302
+ feishu_code: typeof responseData?.code === "number" ? responseData.code : readStringValue(responseData?.code),
5303
+ feishu_msg: readStringValue(responseData?.msg),
5304
+ feishu_log_id: readStringValue(responseData?.log_id)
5244
5305
  });
5245
5306
  };
5246
5307
  const result = await raceWithTimeoutAndAbort(params.client.request({
@@ -5288,15 +5349,15 @@ async function fetchDriveCommentReplies(params) {
5288
5349
  };
5289
5350
  }
5290
5351
  async function resolveCommentReplyContext(params) {
5291
- const userId = normalizeString(params.reply.user_id);
5292
- const normalizedBotOpenIds = new Set(Array.from(params.botOpenIds ?? []).map((botId) => normalizeString(botId)).filter((botId) => Boolean(botId)));
5352
+ const userId = normalizeOptionalString(params.reply.user_id);
5353
+ const normalizedBotOpenIds = new Set(Array.from(params.botOpenIds ?? []).map((botId) => normalizeOptionalString(botId)).filter((botId) => Boolean(botId)));
5293
5354
  return {
5294
- replyId: normalizeString(params.reply.reply_id),
5355
+ replyId: normalizeOptionalString(params.reply.reply_id),
5295
5356
  userId,
5296
5357
  createTime: typeof params.reply.create_time === "number" ? params.reply.create_time : void 0,
5297
5358
  isBotAuthored: typeof userId === "string" && normalizedBotOpenIds.has(userId),
5298
5359
  content: await resolveParsedCommentContent({
5299
- elements: isRecord$2(params.reply.content) ? params.reply.content.elements : void 0,
5360
+ elements: isRecord(params.reply.content) ? params.reply.content.elements : void 0,
5300
5361
  botOpenIds: params.botOpenIds,
5301
5362
  currentDocument: params.currentDocument,
5302
5363
  client: params.client,
@@ -5451,8 +5512,8 @@ async function fetchDriveCommentContext(params) {
5451
5512
  createTime: right.createTime,
5452
5513
  stableId: right.replyId
5453
5514
  }));
5454
- const rootReplyContext = resolvedReplies.find((reply) => reply.replyId === normalizeString(rootReply?.reply_id)) ?? resolvedReplies[0];
5455
- const targetReplyContext = resolvedReplies.find((reply) => reply.replyId === normalizeString(targetReply?.reply_id)) ?? (params.replyId ? void 0 : resolvedReplies.at(-1) ?? rootReplyContext);
5515
+ const rootReplyContext = resolvedReplies.find((reply) => reply.replyId === normalizeOptionalString(rootReply?.reply_id)) ?? resolvedReplies[0];
5516
+ const targetReplyContext = resolvedReplies.find((reply) => reply.replyId === normalizeOptionalString(targetReply?.reply_id)) ?? (params.replyId ? void 0 : resolvedReplies.at(-1) ?? rootReplyContext);
5456
5517
  let wholeCommentTimeline = [];
5457
5518
  if (commentCard?.is_whole === true) {
5458
5519
  const wholeComments = (await fetchDriveComments({
@@ -5466,9 +5527,9 @@ async function fetchDriveCommentContext(params) {
5466
5527
  })).filter((comment) => comment.is_whole === true);
5467
5528
  wholeCommentTimeline = await Promise.all(wholeComments.map(async (comment) => {
5468
5529
  const rootWholeReply = comment.reply_list?.replies?.[0];
5469
- const normalizedBotOpenIds = new Set(Array.from(params.botOpenIds ?? []).map((botId) => normalizeString(botId)).filter((botId) => Boolean(botId)));
5530
+ const normalizedBotOpenIds = new Set(Array.from(params.botOpenIds ?? []).map((botId) => normalizeOptionalString(botId)).filter((botId) => Boolean(botId)));
5470
5531
  const content = await resolveParsedCommentContent({
5471
- elements: isRecord$2(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
5532
+ elements: isRecord(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
5472
5533
  botOpenIds: params.botOpenIds,
5473
5534
  currentDocument,
5474
5535
  client: params.client,
@@ -5476,12 +5537,12 @@ async function fetchDriveCommentContext(params) {
5476
5537
  logger: params.logger,
5477
5538
  accountId: params.accountId
5478
5539
  });
5479
- const commentUserId = normalizeString(rootWholeReply?.user_id) || normalizeString(comment.user_id);
5540
+ const commentUserId = normalizeOptionalString(rootWholeReply?.user_id) || normalizeOptionalString(comment.user_id);
5480
5541
  return {
5481
- commentId: normalizeString(comment.comment_id) ?? "",
5542
+ commentId: normalizeOptionalString(comment.comment_id) ?? "",
5482
5543
  userId: commentUserId,
5483
5544
  createTime: typeof comment.create_time === "number" ? comment.create_time : typeof rootWholeReply?.create_time === "number" ? rootWholeReply.create_time : void 0,
5484
- isCurrentComment: normalizeString(comment.comment_id) === params.commentId,
5545
+ isCurrentComment: normalizeOptionalString(comment.comment_id) === params.commentId,
5485
5546
  isBotAuthored: typeof commentUserId === "string" && normalizedBotOpenIds.has(commentUserId),
5486
5547
  content
5487
5548
  };
@@ -5496,10 +5557,10 @@ async function fetchDriveCommentContext(params) {
5496
5557
  }
5497
5558
  const currentWholeCommentIndex = wholeCommentTimeline.findIndex((entry) => entry.commentId === params.commentId);
5498
5559
  return {
5499
- documentTitle: normalizeString(meta?.title),
5500
- documentUrl: normalizeString(meta?.url),
5560
+ documentTitle: normalizeOptionalString(meta?.title),
5561
+ documentUrl: normalizeOptionalString(meta?.url),
5501
5562
  isWholeComment: commentCard?.is_whole,
5502
- quoteText: normalizeString(commentCard?.quote),
5563
+ quoteText: normalizeOptionalString(commentCard?.quote),
5503
5564
  rootCommentText: rootReplyContext?.content.semanticText ?? rootReplyContext?.content.plainText,
5504
5565
  targetReplyText: targetReplyContext?.content.semanticText ?? targetReplyContext?.content.plainText,
5505
5566
  rootCommentContent: rootReplyContext?.content,
@@ -5553,7 +5614,7 @@ function buildDriveCommentSurfacePrompt(params) {
5553
5614
  if (params.nearestBotWholeCommentBefore) lines.push(`Nearest bot-authored whole-comment before the current comment: comment_id=${params.nearestBotWholeCommentBefore.commentId} text=${formatPromptTextValue(params.nearestBotWholeCommentBefore.content.semanticText ?? params.nearestBotWholeCommentBefore.content.plainText)}`);
5554
5615
  lines.push("For this whole-document comment, use the whole-comment timeline above as the primary source for phrases like 'just now', 'previous result', 'that summary', or 'write it back'.", "Document-level session history is auxiliary background only. Do not resolve whole-comment follow-ups by blindly using the most recent document-session output.");
5555
5616
  }
5556
- lines.push("This is a Feishu document comment thread.", "It is not a Feishu IM chat.", "Your final text reply will be posted to the current comment thread automatically.", "Use the thread timeline above as the main context for follow-up requests.", "Do not use another comment card or document-session output as the main reference.", "If you need comment thread context, use feishu_drive.list_comments or feishu_drive.list_comment_replies.", "If you modify the document, post a user-visible follow-up in the comment thread.", "Use feishu_drive.reply_comment or feishu_drive.add_comment for that follow-up.", "Whole-document comments do not support direct replies.", "For whole-document comments, use feishu_drive.add_comment.", "Only treat URLs listed under \"Referenced documents from current user comment\" as structured Feishu document references.", "URLs that appear only in comment text are plain links unless you verify them.", "If the user asks about a linked Feishu document or wiki page, treat that linked document as the read target.", "If the user asks you to use a linked document as guidance, treat the linked document as the reference source and the current commented document as the edit target.", "If a referenced document resolves to the same file_token and file_type as the current commented document, treat it as the current document.", "If the user asks you to modify document content, you must use feishu_doc to make the change.", "Do not reply with only \"done\", \"I'll handle it\", or a restated plan without calling tools.", "If the comment quotes document content, treat the quoted content as the main anchor.", "For requests like \"insert xxx below this content\", locate the quoted content first, then edit the document.", "For requests like \"summarize the content below\", \"explain this section\", or \"continue writing from here\", use the quoted content as the main target.", "If the quote is not enough, use feishu_doc.read or feishu_doc.list_blocks to read nearby context.", "Do not guess document content from the comment alone.", "Do not give a vague answer before reading enough context.", "Unless the user asks for the whole document, handle only the local content around the quoted anchor.", "If document edits are involved, read the anchor first, then edit.", "If the edit fails or the anchor cannot be found, say so clearly.", "If this is a reading task, such as summarization, explanation, or extraction, you may output the final answer directly after confirming the context.", "Use the same language as the user's comment or reply, unless the user asks for another language.", "Use plain text only.", "Do not use Markdown.", "Do not use headings.", "Do not use bullet lists.", "Do not use numbered lists.", "Do not use tables.", "Do not use blockquotes.", "Do not use code blocks.", "Do not show reasoning.", "Do not show analysis.", "Do not show chain-of-thought.", "Do not show scratch work.", "Do not describe your plan.", "Do not describe your steps.", "Do not describe tool use.", "Do not start with phrases like \"I will\", \"I’ll first\", \"I need to\", \"The user wants\", or \"I have updated\".", "Output only the final user-facing reply.", "If you already sent the user-visible reply with feishu_drive.reply_comment or feishu_drive.add_comment, output exactly NO_REPLY.", "If no user-visible reply is needed, output exactly NO_REPLY.", "Be concise.", "Do not omit requested content.");
5617
+ lines.push("This is a Feishu document comment thread.", "It is not a Feishu IM chat.", "Your final text reply will be posted to the current comment thread automatically.", "Use the thread timeline above as the main context for follow-up requests.", "Do not use another comment card or document-session output as the main reference.", "If you need comment thread context, use the available Feishu drive comment-list actions.", "If you modify the document, post a user-visible follow-up in the comment thread.", "Use an available Feishu drive reply-comment or add-comment action for that follow-up.", "Whole-document comments do not support direct replies.", "For whole-document comments, use the Feishu drive add-comment action when available.", "Only treat URLs listed under \"Referenced documents from current user comment\" as structured Feishu document references.", "URLs that appear only in comment text are plain links unless you verify them.", "If the user asks about a linked Feishu document or wiki page, treat that linked document as the read target.", "If the user asks you to use a linked document as guidance, treat the linked document as the reference source and the current commented document as the edit target.", "If a referenced document resolves to the same file_token and file_type as the current commented document, treat it as the current document.", "If the user asks you to modify document content, you must use an available Feishu document tool to make the change.", "Do not reply with only \"done\", \"I'll handle it\", or a restated plan without calling tools.", "If the comment quotes document content, treat the quoted content as the main anchor.", "For requests like \"insert xxx below this content\", locate the quoted content first, then edit the document.", "For requests like \"summarize the content below\", \"explain this section\", or \"continue writing from here\", use the quoted content as the main target.", "If the quote is not enough, use available Feishu document read or list-blocks actions to read nearby context.", "Do not guess document content from the comment alone.", "Do not give a vague answer before reading enough context.", "Unless the user asks for the whole document, handle only the local content around the quoted anchor.", "If document edits are involved, read the anchor first, then edit.", "If the edit fails or the anchor cannot be found, say so clearly.", "If this is a reading task, such as summarization, explanation, or extraction, you may output the final answer directly after confirming the context.", "Use the same language as the user's comment or reply, unless the user asks for another language.", "Use plain text only.", "Do not use Markdown.", "Do not use headings.", "Do not use bullet lists.", "Do not use numbered lists.", "Do not use tables.", "Do not use blockquotes.", "Do not use code blocks.", "Do not show reasoning.", "Do not show analysis.", "Do not show chain-of-thought.", "Do not show scratch work.", "Do not describe your plan.", "Do not describe your steps.", "Do not describe tool use.", "Do not start with phrases like \"I will\", \"I’ll first\", \"I need to\", \"The user wants\", or \"I have updated\".", "Output only the final user-facing reply.", "If you already sent the user-visible reply with a Feishu drive comment action, output exactly NO_REPLY.", "If no user-visible reply is needed, output exactly NO_REPLY.", "Be concise.", "Do not omit requested content.");
5557
5618
  lines.push("Choose one outcome: output the final plain-text reply, edit the document and then post a user-visible follow-up in the comment thread, or output exactly NO_REPLY.");
5558
5619
  return lines.join("\n");
5559
5620
  }
@@ -5566,7 +5627,7 @@ async function resolveDriveCommentEventCore(params) {
5566
5627
  const fileToken = event.notice_meta?.file_token?.trim();
5567
5628
  const fileType = normalizeCommentFileType(event.notice_meta?.file_type);
5568
5629
  const senderId = event.notice_meta?.from_user_id?.open_id?.trim();
5569
- const senderUserId = normalizeString(event.notice_meta?.from_user_id?.user_id);
5630
+ const senderUserId = normalizeOptionalString(event.notice_meta?.from_user_id?.user_id);
5570
5631
  if (!eventId || !commentId || !noticeType || !fileToken || !fileType || !senderId) {
5571
5632
  logger?.(`feishu[${accountId}]: drive comment notice missing required fields event=${eventId ?? "unknown"} comment=${commentId ?? "unknown"}`);
5572
5633
  return null;
@@ -5587,7 +5648,7 @@ async function resolveDriveCommentEventCore(params) {
5587
5648
  return null;
5588
5649
  }
5589
5650
  const context = await fetchDriveCommentContext({
5590
- client: createClient ? createClient(account ?? { accountId }) : createFeishuClient((await import("./accounts-u9X5Wsan.js").then((n) => n.t)).resolveFeishuAccount({
5651
+ client: createClient ? createClient(account ?? { accountId }) : createFeishuClient((await import("./accounts-CCCdMen2.js").then((n) => n.t)).resolveFeishuAccount({
5591
5652
  cfg,
5592
5653
  accountId
5593
5654
  })),
@@ -5617,32 +5678,32 @@ async function resolveDriveCommentEventCore(params) {
5617
5678
  };
5618
5679
  }
5619
5680
  function parseFeishuDriveCommentNoticeEventPayload(value) {
5620
- if (!isRecord$2(value) || !isRecord$2(value.notice_meta)) return null;
5681
+ if (!isRecord(value) || !isRecord(value.notice_meta)) return null;
5621
5682
  const noticeMeta = value.notice_meta;
5622
- const fromUserId = isRecord$2(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
5623
- const toUserId = isRecord$2(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
5683
+ const fromUserId = isRecord(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
5684
+ const toUserId = isRecord(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
5624
5685
  return {
5625
- comment_id: readString$1(value.comment_id),
5626
- event_id: readString$1(value.event_id),
5686
+ comment_id: readStringValue(value.comment_id),
5687
+ event_id: readStringValue(value.event_id),
5627
5688
  is_mentioned: asBoolean(value.is_mentioned),
5628
5689
  notice_meta: {
5629
- file_token: readString$1(noticeMeta.file_token),
5630
- file_type: readString$1(noticeMeta.file_type),
5690
+ file_token: readStringValue(noticeMeta.file_token),
5691
+ file_type: readStringValue(noticeMeta.file_type),
5631
5692
  from_user_id: fromUserId ? {
5632
- open_id: readString$1(fromUserId.open_id),
5633
- user_id: readString$1(fromUserId.user_id),
5634
- union_id: readString$1(fromUserId.union_id)
5693
+ open_id: readStringValue(fromUserId.open_id),
5694
+ user_id: readStringValue(fromUserId.user_id),
5695
+ union_id: readStringValue(fromUserId.union_id)
5635
5696
  } : void 0,
5636
- notice_type: readString$1(noticeMeta.notice_type),
5697
+ notice_type: readStringValue(noticeMeta.notice_type),
5637
5698
  to_user_id: toUserId ? {
5638
- open_id: readString$1(toUserId.open_id),
5639
- user_id: readString$1(toUserId.user_id),
5640
- union_id: readString$1(toUserId.union_id)
5699
+ open_id: readStringValue(toUserId.open_id),
5700
+ user_id: readStringValue(toUserId.user_id),
5701
+ union_id: readStringValue(toUserId.union_id)
5641
5702
  } : void 0
5642
5703
  },
5643
- reply_id: readString$1(value.reply_id),
5644
- timestamp: readString$1(value.timestamp),
5645
- type: readString$1(value.type)
5704
+ reply_id: readStringValue(value.reply_id),
5705
+ timestamp: readStringValue(value.timestamp),
5706
+ type: readStringValue(value.type)
5646
5707
  };
5647
5708
  }
5648
5709
  async function resolveDriveCommentEventTurn(params) {
@@ -5742,7 +5803,7 @@ async function handleFeishuCommentEvent(params) {
5742
5803
  channel: "feishu",
5743
5804
  accountId: account.accountId
5744
5805
  });
5745
- const resolveCommentAuthorization = async (candidateCfg, mayPair) => {
5806
+ const resolveCommentAuthorization = async (candidateCfg, mayPair, contextBinding) => {
5746
5807
  const candidateAccount = resolveFeishuRuntimeAccount({
5747
5808
  cfg: candidateCfg,
5748
5809
  accountId: account.accountId
@@ -5761,7 +5822,8 @@ async function handleFeishuCommentEvent(params) {
5761
5822
  senderOpenId: turn.senderId,
5762
5823
  senderUserId: turn.senderUserId,
5763
5824
  conversationId: turn.senderId,
5764
- mayPair
5825
+ mayPair,
5826
+ ...contextBinding ? { contextBinding } : {}
5765
5827
  })
5766
5828
  };
5767
5829
  };
@@ -5853,7 +5915,14 @@ async function handleFeishuCommentEvent(params) {
5853
5915
  const bodyForAgent = `[message_id: ${turn.messageId}]\n${turn.prompt}`;
5854
5916
  const rawBody = turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt;
5855
5917
  const conversationLabel = turn.documentTitle ? `Feishu comment · ${turn.documentTitle}` : "Feishu comment";
5856
- const ctxPayload = buildChannelInboundEventContext({
5918
+ const boundAuthorization = await resolveCommentAuthorization(effectiveCfg, false, {
5919
+ agentId: route.agentId,
5920
+ sessionKey: commentSessionKey,
5921
+ messageId: turn.messageId,
5922
+ inboundEventKind: "user_request"
5923
+ });
5924
+ const ctxPayload = core.channel.inbound.buildContext({
5925
+ channelIngress: boundAuthorization.ingress,
5857
5926
  channel: "feishu",
5858
5927
  accountId: route.accountId,
5859
5928
  surface: "feishu-comment",
@@ -5939,14 +6008,8 @@ async function handleFeishuCommentEvent(params) {
5939
6008
  })
5940
6009
  }
5941
6010
  });
5942
- const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
5943
- const queuedFinal = dispatchResult?.queuedFinal ?? false;
5944
- const counts = dispatchResult?.counts ?? {
5945
- tool: 0,
5946
- block: 0,
5947
- final: 0
5948
- };
5949
- log(`feishu[${account.accountId}]: drive comment dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final}, session=${commentSessionKey})`);
6011
+ const counts = resolveInboundReplyDispatchCounts(turnResult.dispatched ? turnResult.dispatchResult : void 0);
6012
+ log(`feishu[${account.accountId}]: drive comment dispatch complete (replies=${counts.final}, session=${commentSessionKey})`);
5950
6013
  } finally {
5951
6014
  cleanupTypingReaction();
5952
6015
  }
@@ -6347,7 +6410,7 @@ const FEISHU_WS_LOG_ERROR_MAX_LENGTH = 500;
6347
6410
  const FEISHU_WS_RECONNECT_EXHAUSTED_RE = /^WebSocket reconnect exhausted after \d+ attempts?/;
6348
6411
  const FEISHU_WS_AUTORECONNECT_DISABLED_ERROR = "WebSocket connect failed and autoReconnect is disabled";
6349
6412
  function isFeishuWebhookPayload(value) {
6350
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6413
+ return isRecord(value);
6351
6414
  }
6352
6415
  const BLOCKED_FEISHU_WEBHOOK_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
6353
6416
  "__proto__",
@@ -6452,16 +6515,15 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
6452
6515
  };
6453
6516
  const publishWsConnected = () => {
6454
6517
  const connectedAt = Date.now();
6455
- statusSink?.({
6456
- connected: true,
6518
+ statusSink?.(channelReadyPatch({
6457
6519
  lastConnectedAt: connectedAt,
6458
- lastEventAt: connectedAt,
6459
- lastError: null
6460
- });
6520
+ lastEventAt: connectedAt
6521
+ }));
6461
6522
  };
6462
6523
  const publishWsReconnecting = () => {
6463
6524
  statusSink?.({
6464
6525
  connected: false,
6526
+ lifecycle: "recovering",
6465
6527
  lastEventAt: Date.now()
6466
6528
  });
6467
6529
  };
@@ -6509,10 +6571,11 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
6509
6571
  });
6510
6572
  setSocketTerminator?.(void 0);
6511
6573
  if (abortSignal?.aborted) break;
6512
- statusSink?.({
6574
+ const disconnectedAt = Date.now();
6575
+ statusSink?.(channelBlockedPatch(formatFeishuWsErrorForLog(cycleEnd), {
6513
6576
  connected: false,
6514
- lastEventAt: Date.now()
6515
- });
6577
+ lastEventAt: disconnectedAt
6578
+ }));
6516
6579
  attempt += 1;
6517
6580
  const delayMs = getFeishuWsReconnectDelayMs(attempt);
6518
6581
  error(`feishu[${accountId}]: WebSocket connection ended, recreating client in ${delayMs}ms: ${formatFeishuWsErrorForLog(cycleEnd)}`);
@@ -6526,9 +6589,14 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
6526
6589
  });
6527
6590
  setSocketTerminator?.(void 0);
6528
6591
  if (abortSignal?.aborted) break;
6592
+ const failedAt = Date.now();
6593
+ const terminal = err instanceof Error && isFeishuWsTerminalError(err);
6529
6594
  statusSink?.({
6530
6595
  connected: false,
6531
- lastEventAt: Date.now()
6596
+ lifecycle: terminal ? "blocked" : "recovering",
6597
+ ...terminal ? { terminalDisconnect: true } : {},
6598
+ lastEventAt: failedAt,
6599
+ lastError: formatFeishuWsErrorForLog(err)
6532
6600
  });
6533
6601
  attempt += 1;
6534
6602
  const delayMs = getFeishuWsReconnectDelayMs(attempt);
@@ -6551,10 +6619,18 @@ async function monitorWebhook({ account, accountId, runtime, abortSignal, eventD
6551
6619
  if (!encryptKey) throw new Error(`Feishu account "${accountId}" webhook mode requires encryptKey`);
6552
6620
  const port = account.config.webhookPort ?? 3e3;
6553
6621
  const path = account.config.webhookPath ?? "/feishu/events";
6622
+ if (normalizeFeishuWebhookPath(path) !== path) throw new Error(`Feishu account "${accountId}" webhookPath must be a canonical HTTP request path; run "openclaw doctor --fix" to repair it`);
6554
6623
  const host = account.config.webhookHost ?? "127.0.0.1";
6555
6624
  log(`feishu[${accountId}]: starting Webhook server on ${host}:${port}, path ${path}...`);
6556
6625
  const server = http.createServer();
6557
6626
  server.on("request", (req, res) => {
6627
+ const requestUrl = req.url ?? "/";
6628
+ const requestPath = requestUrl.split("?", 1)[0];
6629
+ const requestRoute = path.includes("?") ? requestUrl : requestPath;
6630
+ if (!requestPath?.startsWith("/") || requestUrl.includes("#") || requestRoute !== path) {
6631
+ respondText(res, 404, "Not Found");
6632
+ return;
6633
+ }
6558
6634
  res.on("finish", () => {
6559
6635
  recordWebhookStatus(runtime, accountId, path, res.statusCode);
6560
6636
  if (res.statusCode >= 200 && res.statusCode < 300) {
@@ -6568,6 +6644,7 @@ async function monitorWebhook({ account, accountId, runtime, abortSignal, eventD
6568
6644
  if (!applyBasicWebhookRequestGuards({
6569
6645
  req,
6570
6646
  res,
6647
+ allowMethods: ["POST"],
6571
6648
  rateLimiter: feishuWebhookRateLimiter,
6572
6649
  rateLimitKey: buildFeishuWebhookRateLimitKey({
6573
6650
  accountId,
@@ -6654,15 +6731,18 @@ async function monitorWebhook({ account, accountId, runtime, abortSignal, eventD
6654
6731
  server.listen(port, host, () => {
6655
6732
  log(`feishu[${accountId}]: Webhook server listening on ${host}:${port}`);
6656
6733
  const webhookConnectedAt = Date.now();
6657
- statusSink?.({
6658
- connected: true,
6734
+ statusSink?.(channelReadyPatch({
6659
6735
  lastConnectedAt: webhookConnectedAt,
6660
- lastEventAt: webhookConnectedAt,
6661
- lastError: null
6662
- });
6736
+ lastEventAt: webhookConnectedAt
6737
+ }));
6663
6738
  });
6664
6739
  server.on("error", (err) => {
6665
6740
  error(`feishu[${accountId}]: Webhook server error: ${err}`);
6741
+ statusSink?.({
6742
+ connected: false,
6743
+ lifecycle: "recovering",
6744
+ lastError: formatFeishuWsErrorForLog(err)
6745
+ });
6666
6746
  abortSignal?.removeEventListener("abort", handleAbort);
6667
6747
  reject(err);
6668
6748
  });
@@ -6799,10 +6879,11 @@ async function resolveReactionSyntheticEvent(params) {
6799
6879
  const { cfg, accountId, event, botOpenId, fetchMessage = getMessageFeishu, verificationTimeoutMs = FEISHU_REACTION_VERIFY_TIMEOUT_MS, logger, uuid = () => crypto$1.randomUUID(), action = "created" } = params;
6800
6880
  const emoji = event.reaction_type?.emoji_type;
6801
6881
  const messageId = event.message_id;
6802
- const senderId = event.user_id?.open_id;
6803
- const senderUserId = event.user_id?.user_id;
6882
+ const senderOpenId = event.user_id?.open_id?.trim();
6883
+ const senderUserId = event.user_id?.user_id?.trim();
6884
+ const senderId = senderOpenId || senderUserId;
6804
6885
  if (!emoji || !messageId || !senderId) return null;
6805
- const { resolveFeishuAccount } = await import("./accounts-u9X5Wsan.js").then((n) => n.t);
6886
+ const { resolveFeishuAccount } = await import("./accounts-CCCdMen2.js").then((n) => n.t);
6806
6887
  const reactionNotifications = resolveFeishuAccount({
6807
6888
  cfg,
6808
6889
  accountId
@@ -6836,14 +6917,17 @@ async function resolveReactionSyntheticEvent(params) {
6836
6917
  return {
6837
6918
  sender: {
6838
6919
  sender_id: {
6839
- open_id: senderId,
6920
+ ...senderOpenId ? { open_id: senderOpenId } : {},
6840
6921
  ...senderUserId ? { user_id: senderUserId } : {}
6841
6922
  },
6842
6923
  sender_type: "user"
6843
6924
  },
6844
6925
  message: {
6845
6926
  message_id: `${messageId}:reaction:${emoji}:${uuid()}`,
6927
+ reply_target_message_id: messageId,
6846
6928
  typing_target_message_id: messageId,
6929
+ ...reactedMsg.rootId ? { root_id: reactedMsg.rootId } : {},
6930
+ ...reactedMsg.threadId ? { thread_id: reactedMsg.threadId } : {},
6847
6931
  chat_id: syntheticChatId,
6848
6932
  chat_type: syntheticChatType,
6849
6933
  message_type: "text",
@@ -6855,41 +6939,41 @@ function normalizeFeishuChatType(value) {
6855
6939
  return value === "group" || value === "topic_group" || value === "private" || value === "p2p" ? value : void 0;
6856
6940
  }
6857
6941
  function parseFeishuBotAddedEventPayload(value) {
6858
- if (!isRecord$2(value) || !readString$1(value.chat_id) || !isRecord$2(value.operator_id)) return null;
6942
+ if (!isRecord(value) || !readStringValue(value.chat_id) || !isRecord(value.operator_id)) return null;
6859
6943
  return value;
6860
6944
  }
6861
6945
  function parseFeishuBotRemovedChatId(value) {
6862
- if (!isRecord$2(value)) return null;
6863
- return readString$1(value.chat_id) ?? null;
6946
+ if (!isRecord(value)) return null;
6947
+ return readStringValue(value.chat_id) ?? null;
6864
6948
  }
6865
6949
  function firstString(...values) {
6866
6950
  for (const value of values) {
6867
- const trimmed = readString$1(value)?.trim();
6951
+ const trimmed = readStringValue(value)?.trim();
6868
6952
  if (trimmed) return trimmed;
6869
6953
  }
6870
6954
  }
6871
6955
  function readFeishuIdentityField(value, field) {
6872
- if (!isRecord$2(value)) return;
6956
+ if (!isRecord(value)) return;
6873
6957
  return firstString(value[field]);
6874
6958
  }
6875
6959
  function parseFeishuCardActionEventPayload(value) {
6876
- if (!isRecord$2(value)) return null;
6877
- const operator = isRecord$2(value.operator) ? value.operator : {};
6960
+ if (!isRecord(value)) return null;
6961
+ const operator = isRecord(value.operator) ? value.operator : {};
6878
6962
  const action = value.action;
6879
- const context = isRecord$2(value.context) ? value.context : {};
6880
- if (!isRecord$2(action)) return null;
6963
+ const context = isRecord(value.context) ? value.context : {};
6964
+ if (!isRecord(action)) return null;
6881
6965
  const operatorUserId = operator.user_id;
6882
- const token = readString$1(value.token);
6966
+ const token = readStringValue(value.token);
6883
6967
  const openId = firstString(operator.open_id, readFeishuIdentityField(operatorUserId, "open_id"), value.open_id, context.open_id);
6884
6968
  const userId = firstString(operator.user_id, readFeishuIdentityField(operatorUserId, "user_id"), value.user_id, context.user_id);
6885
6969
  const unionId = firstString(operator.union_id, readFeishuIdentityField(operatorUserId, "union_id"));
6886
- const tag = readString$1(action.tag);
6970
+ const tag = readStringValue(action.tag);
6887
6971
  const actionValue = action.value;
6888
6972
  const openMessageId = firstString(context.open_message_id, value.open_message_id);
6889
6973
  const contextOpenId = firstString(context.open_id, openId);
6890
6974
  const contextUserId = firstString(context.user_id, userId);
6891
6975
  const chatId = firstString(context.chat_id, context.open_chat_id);
6892
- if (!token || !openId || !tag || !isRecord$2(actionValue)) return null;
6976
+ if (!token || !openId || !tag || !isRecord(actionValue)) return null;
6893
6977
  return {
6894
6978
  operator: {
6895
6979
  open_id: openId,