@openclaw/feishu 2026.7.2-beta.2 → 2026.7.2-beta.4
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.
- package/dist/api.js +4 -4
- package/dist/{app-registration-BzkyrVZu.js → app-registration-cN1bVV6s.js} +5 -5
- package/dist/{channel-X30GQVwc.js → channel-DB1x6UJX.js} +33 -24
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.runtime-By2uqa3Q.js → channel.runtime-B5yAAvbT.js} +102 -17
- package/dist/{client-87BmeMpj.js → client-Dee7cKsS.js} +62 -1
- package/dist/{doctor-contract-DXH5hux1.js → doctor-contract-Cw5sdjOm.js} +29 -41
- package/dist/doctor-contract-api.js +1 -1
- package/dist/{drive-B4E8OXZ0.js → drive-_zrcOxzS.js} +2 -2
- package/dist/{send-DrWM_Ilf.js → media-CRJunSRr.js} +1075 -1017
- package/dist/{monitor-CHl0bWzb.js → monitor-8CLu6myv.js} +5 -4
- package/dist/{monitor.account-BSRV2hIF.js → monitor.account-CkYj45PC.js} +2113 -809
- package/dist/monitor.startup-CSUPT_U1.js +143 -0
- package/dist/{probe-BgboTHIn.js → probe-Za1kgUvx.js} +70 -10
- package/dist/{secret-contract-DnlMcrn5.js → secret-contract-BCpDLdg9.js} +17 -26
- package/dist/secret-contract-api.js +1 -1
- package/dist/{send-result-DsqTk27v.js → send-result-DfE5Fhz6.js} +22 -11
- package/dist/setup-api.js +1 -1
- package/node_modules/@larksuiteoapi/node-sdk/README.md +13 -0
- package/node_modules/@larksuiteoapi/node-sdk/README.zh.md +12 -0
- package/node_modules/@larksuiteoapi/node-sdk/es/index.js +8950 -3084
- package/node_modules/@larksuiteoapi/node-sdk/lib/index.js +8951 -3083
- package/node_modules/@larksuiteoapi/node-sdk/package.json +1 -1
- package/node_modules/@larksuiteoapi/node-sdk/types/index.d.ts +12522 -698
- package/node_modules/@protobufjs/utf8/CHANGELOG.md +8 -0
- package/node_modules/@protobufjs/utf8/index.js +29 -18
- package/node_modules/@protobufjs/utf8/package.json +2 -2
- package/node_modules/@protobufjs/utf8/tests/index.js +5 -0
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/fs/promises.d.ts +16 -6
- package/node_modules/@types/node/package.json +2 -2
- package/node_modules/agent-base/README.md +145 -0
- package/node_modules/agent-base/dist/src/index.d.ts +78 -0
- package/node_modules/agent-base/dist/src/index.js +203 -0
- package/node_modules/agent-base/dist/src/index.js.map +1 -0
- package/node_modules/agent-base/dist/src/promisify.d.ts +4 -0
- package/node_modules/agent-base/dist/src/promisify.js +18 -0
- package/node_modules/agent-base/dist/src/promisify.js.map +1 -0
- package/node_modules/agent-base/package.json +64 -0
- package/node_modules/agent-base/src/index.ts +345 -0
- package/node_modules/agent-base/src/promisify.ts +33 -0
- package/node_modules/axios/CHANGELOG.md +197 -1
- package/node_modules/axios/README.md +424 -256
- package/node_modules/axios/dist/axios.js +589 -204
- package/node_modules/axios/dist/axios.min.js +3 -3
- package/node_modules/axios/dist/axios.min.js.map +1 -1
- package/node_modules/axios/dist/browser/axios.cjs +630 -185
- package/node_modules/axios/dist/esm/axios.js +630 -185
- package/node_modules/axios/dist/esm/axios.min.js +2 -2
- package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
- package/node_modules/axios/dist/node/axios.cjs +949 -301
- package/node_modules/axios/index.d.cts +28 -5
- package/node_modules/axios/index.d.ts +24 -3
- package/node_modules/axios/lib/adapters/adapters.js +1 -1
- package/node_modules/axios/lib/adapters/fetch.js +223 -49
- package/node_modules/axios/lib/adapters/http.js +408 -193
- package/node_modules/axios/lib/adapters/xhr.js +3 -1
- package/node_modules/axios/lib/core/Axios.js +4 -2
- package/node_modules/axios/lib/core/AxiosError.js +13 -1
- package/node_modules/axios/lib/core/AxiosHeaders.js +13 -42
- package/node_modules/axios/lib/core/buildFullPath.js +29 -1
- package/node_modules/axios/lib/core/mergeConfig.js +35 -0
- package/node_modules/axios/lib/defaults/transitional.js +2 -0
- package/node_modules/axios/lib/env/data.js +1 -1
- package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -3
- package/node_modules/axios/lib/helpers/Http2Sessions.js +119 -0
- package/node_modules/axios/lib/helpers/buildURL.js +7 -4
- package/node_modules/axios/lib/helpers/composeSignals.js +48 -47
- package/node_modules/axios/lib/helpers/cookies.js +5 -1
- package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +16 -11
- package/node_modules/axios/lib/helpers/formDataToJSON.js +26 -4
- package/node_modules/axios/lib/helpers/formDataToStream.js +2 -2
- package/node_modules/axios/lib/helpers/fromDataURI.js +20 -5
- package/node_modules/axios/lib/helpers/progressEventReducer.js +3 -0
- package/node_modules/axios/lib/helpers/resolveConfig.js +26 -13
- package/node_modules/axios/lib/helpers/sanitizeHeaderValue.js +60 -0
- package/node_modules/axios/lib/helpers/shouldBypassProxy.js +33 -1
- package/node_modules/axios/lib/helpers/toFormData.js +48 -12
- package/node_modules/axios/lib/helpers/validator.js +1 -1
- package/node_modules/axios/lib/utils.js +105 -19
- package/node_modules/axios/package.json +31 -13
- package/node_modules/https-proxy-agent/README.md +137 -0
- package/node_modules/https-proxy-agent/dist/agent.d.ts +30 -0
- package/node_modules/https-proxy-agent/dist/agent.js +177 -0
- package/node_modules/https-proxy-agent/dist/agent.js.map +1 -0
- package/node_modules/https-proxy-agent/dist/index.d.ts +23 -0
- package/node_modules/https-proxy-agent/dist/index.js +14 -0
- package/node_modules/https-proxy-agent/dist/index.js.map +1 -0
- package/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts +7 -0
- package/node_modules/https-proxy-agent/dist/parse-proxy-response.js +66 -0
- package/node_modules/https-proxy-agent/dist/parse-proxy-response.js.map +1 -0
- package/node_modules/https-proxy-agent/package.json +56 -0
- package/node_modules/protobufjs/dist/light/protobuf.js +145 -188
- package/node_modules/protobufjs/dist/light/protobuf.js.map +1 -1
- package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/light/protobuf.min.js.map +1 -1
- package/node_modules/protobufjs/dist/minimal/protobuf.js +33 -76
- package/node_modules/protobufjs/dist/minimal/protobuf.js.map +1 -1
- package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/minimal/protobuf.min.js.map +1 -1
- package/node_modules/protobufjs/dist/protobuf.js +168 -208
- package/node_modules/protobufjs/dist/protobuf.js.map +1 -1
- package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/protobuf.min.js.map +1 -1
- package/node_modules/protobufjs/index.d.ts +0 -8
- package/node_modules/protobufjs/package.json +2 -3
- package/node_modules/protobufjs/src/parse.js +3 -0
- package/node_modules/protobufjs/src/util/minimal.js +0 -3
- package/node_modules/qs/CHANGELOG.md +11 -0
- package/node_modules/qs/dist/qs.js +17 -17
- package/node_modules/qs/lib/parse.js +19 -8
- package/node_modules/qs/lib/utils.js +47 -8
- package/node_modules/qs/package.json +6 -5
- package/node_modules/qs/test/parse.js +230 -0
- package/node_modules/qs/test/utils.js +194 -0
- package/node_modules/typebox/build/compile/validator.d.mts +1 -4
- package/node_modules/typebox/build/guard/guard.d.mts +1 -1
- package/node_modules/typebox/build/guard/guard.mjs +5 -2
- package/node_modules/typebox/build/guard/string.mjs +16 -4
- package/node_modules/typebox/build/schema/engine/_functions.mjs +34 -15
- package/node_modules/typebox/build/type/action/readonly_object.d.mts +5 -0
- package/node_modules/typebox/build/type/action/readonly_object.mjs +8 -0
- package/node_modules/typebox/build/typebox.d.mts +1 -1
- package/node_modules/typebox/build/typebox.mjs +1 -1
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +29 -32
- package/node_modules/ws/lib/receiver.js +15 -32
- package/node_modules/ws/lib/websocket-server.js +4 -4
- package/node_modules/ws/lib/websocket.js +4 -4
- package/node_modules/ws/package.json +5 -1
- package/npm-shrinkwrap.json +58 -38
- package/openclaw.plugin.json +26 -10
- package/package.json +20 -7
- package/dist/monitor.startup-Cy8NPGAX.js +0 -43
- package/node_modules/@protobufjs/inquire/CHANGELOG.md +0 -8
- package/node_modules/@protobufjs/inquire/LICENSE +0 -26
- package/node_modules/@protobufjs/inquire/README.md +0 -13
- package/node_modules/@protobufjs/inquire/index.d.ts +0 -10
- package/node_modules/@protobufjs/inquire/index.js +0 -38
- package/node_modules/@protobufjs/inquire/package.json +0 -21
- package/node_modules/@protobufjs/inquire/tests/data/array.js +0 -1
- package/node_modules/@protobufjs/inquire/tests/data/emptyArray.js +0 -1
- package/node_modules/@protobufjs/inquire/tests/data/emptyObject.js +0 -1
- package/node_modules/@protobufjs/inquire/tests/data/object.js +0 -1
- package/node_modules/@protobufjs/inquire/tests/index.js +0 -20
- package/node_modules/axios/dist/axios.js.map +0 -1
- package/node_modules/axios/dist/browser/axios.cjs.map +0 -1
- package/node_modules/axios/dist/esm/axios.js.map +0 -1
- package/node_modules/axios/dist/node/axios.cjs.map +0 -1
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { _ as normalizeCommentFileType, c as encodeQuery, d as isRecord$
|
|
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-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-
|
|
3
|
+
import { A as resolveFeishuChatType, C as resolveFeishuDmIngressAccess, E as resolveFeishuGroupSenderActivationIngressAccess, M as buildFeishuCardActionTextFallback, N as createFeishuCardInteractionEnvelope, O as resolveFeishuReplyPolicy, P as decodeFeishuCardAction, S as normalizeFeishuAllowEntry, T as resolveFeishuGroupConversationIngressAccess, d as chunkFeishuPostMarkdown, f as materializeFeishuPostMarkdownSoftBreaks, k as normalizeFeishuChatType$2, o as resolveFeishuCardTemplate, w as resolveFeishuGroupConfig, x as hasExplicitFeishuGroupConfig } from "./send-result-DfE5Fhz6.js";
|
|
4
4
|
import { o as resolveConfiguredFeishuGroupSessionScope, t as buildFeishuConversationId } from "./conversation-id-BeJL-wq7.js";
|
|
5
|
-
import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-
|
|
5
|
+
import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-Dee7cKsS.js";
|
|
6
6
|
import { t as getFeishuRuntime } from "./runtime-C5JxBWZp.js";
|
|
7
|
-
import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-
|
|
7
|
+
import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-_zrcOxzS.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
|
|
12
|
-
import {
|
|
13
|
-
import { t as fetchBotIdentityForMonitor } from "./monitor.startup-
|
|
14
|
-
import { createChannelMessageReplyPipeline, formatChannelProgressDraftLineForEntry, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelPreviewStreamMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
|
|
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-CRJunSRr.js";
|
|
12
|
+
import { a as waitForAbortableDelay, i as raceWithTimeoutAndAbort } from "./probe-Za1kgUvx.js";
|
|
13
|
+
import { t as fetchBotIdentityForMonitor } from "./monitor.startup-CSUPT_U1.js";
|
|
14
|
+
import { DEFAULT_INGRESS_ADOPTION_STALL_MS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, formatChannelProgressDraftLineForEntry, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelPreviewStreamMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
|
|
15
15
|
import { createChannelPairingController, createChannelPairingController as createChannelPairingController$1 } from "openclaw/plugin-sdk/channel-pairing";
|
|
16
16
|
import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
|
|
17
17
|
import { asDateTimestampMs, isFutureDateTimestampMs, parseStrictNonNegativeInteger, resolveDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveExpiresAtMsFromDurationSeconds, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
@@ -22,21 +22,24 @@ 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 {
|
|
25
|
+
import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
|
26
|
+
import { collectErrorGraphCandidates, 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, createChannelPartialDeliveryError, formatAgentEnvelope, formatInboundMediaUnavailableText, isChannelPartialDeliveryError, 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";
|
|
42
|
+
import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
|
|
40
43
|
import * as http from "node:http";
|
|
41
44
|
import { installRequestBodyLimitGuard, readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
42
45
|
import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
|
|
@@ -72,6 +75,167 @@ function buildFeishuAgentBody(params) {
|
|
|
72
75
|
}
|
|
73
76
|
//#endregion
|
|
74
77
|
//#region extensions/feishu/src/bot-broadcast.ts
|
|
78
|
+
function createFeishuBroadcastIngressSettlement(params) {
|
|
79
|
+
const lanes = /* @__PURE__ */ new Set();
|
|
80
|
+
const failures = [];
|
|
81
|
+
const fallbackAbortSignal = new AbortController().signal;
|
|
82
|
+
let fanoutSettled = false;
|
|
83
|
+
let terminal;
|
|
84
|
+
let adoption;
|
|
85
|
+
let abandonment;
|
|
86
|
+
let finalizing = false;
|
|
87
|
+
let deferred = false;
|
|
88
|
+
let replayReleased = false;
|
|
89
|
+
const beginFinalizing = () => {
|
|
90
|
+
if (finalizing) return;
|
|
91
|
+
finalizing = true;
|
|
92
|
+
params.lifecycle?.onAdoptionFinalizing();
|
|
93
|
+
};
|
|
94
|
+
const defer = () => {
|
|
95
|
+
if (deferred) return;
|
|
96
|
+
deferred = true;
|
|
97
|
+
params.lifecycle?.onDeferred();
|
|
98
|
+
};
|
|
99
|
+
const reportReplayCommitError = (error) => {
|
|
100
|
+
try {
|
|
101
|
+
params.onReplayCommitError?.(error);
|
|
102
|
+
} catch {}
|
|
103
|
+
};
|
|
104
|
+
const releaseReplayClaim = (error) => {
|
|
105
|
+
if (replayReleased || terminal === "adopted") return;
|
|
106
|
+
replayReleased = true;
|
|
107
|
+
params.replayClaim?.release({ error });
|
|
108
|
+
};
|
|
109
|
+
const runAbandonment = async (error) => {
|
|
110
|
+
if (terminal) return;
|
|
111
|
+
releaseReplayClaim(error);
|
|
112
|
+
try {
|
|
113
|
+
await params.lifecycle?.onAbandoned();
|
|
114
|
+
} finally {
|
|
115
|
+
terminal = "abandoned";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const abandon = async (error) => {
|
|
119
|
+
if (terminal) return;
|
|
120
|
+
if (adoption) {
|
|
121
|
+
await adoption.catch(() => void 0);
|
|
122
|
+
if (terminal) return;
|
|
123
|
+
}
|
|
124
|
+
const activeAbandonment = abandonment ?? runAbandonment(error);
|
|
125
|
+
abandonment = activeAbandonment;
|
|
126
|
+
await activeAbandonment;
|
|
127
|
+
};
|
|
128
|
+
const runAdoption = async () => {
|
|
129
|
+
beginFinalizing();
|
|
130
|
+
try {
|
|
131
|
+
await params.lifecycle?.onAdopted();
|
|
132
|
+
terminal = "adopted";
|
|
133
|
+
try {
|
|
134
|
+
params.onAdopted?.();
|
|
135
|
+
} catch {}
|
|
136
|
+
try {
|
|
137
|
+
await params.replayClaim?.commit();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
reportReplayCommitError(error);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await runAbandonment(error).catch(() => void 0);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const adopt = async () => {
|
|
147
|
+
if (terminal) return;
|
|
148
|
+
if (abandonment) {
|
|
149
|
+
await abandonment.catch(() => void 0);
|
|
150
|
+
if (terminal) return;
|
|
151
|
+
}
|
|
152
|
+
const activeAdoption = adoption ?? runAdoption();
|
|
153
|
+
adoption = activeAdoption;
|
|
154
|
+
await activeAdoption;
|
|
155
|
+
};
|
|
156
|
+
const maybeSettle = async () => {
|
|
157
|
+
if (!fanoutSettled || terminal) return;
|
|
158
|
+
if (failures.length > 0 || [...lanes].some((lane) => lane.status === "failed" || lane.status === "abandoned")) {
|
|
159
|
+
await abandon(failures.length === 1 ? failures[0] : new AggregateError(failures, "Feishu broadcast dispatch failed"));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if ([...lanes].some((lane) => lane.status === "pending" || lane.status === "deferred" || lane.status === "adopted")) return;
|
|
163
|
+
await adopt();
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
createLane: (replayClaim) => {
|
|
167
|
+
const lane = {
|
|
168
|
+
replayClaim,
|
|
169
|
+
status: "pending"
|
|
170
|
+
};
|
|
171
|
+
lanes.add(lane);
|
|
172
|
+
const releaseLane = (error) => {
|
|
173
|
+
lane.replayClaim?.release({ error });
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
lifecycle: {
|
|
177
|
+
abortSignal: params.lifecycle?.abortSignal ?? fallbackAbortSignal,
|
|
178
|
+
onAdopted: async () => {
|
|
179
|
+
if (lane.status === "adopted" || lane.status === "completed" || lane.status === "failed" || lane.status === "abandoned") return;
|
|
180
|
+
lane.status = "adopted";
|
|
181
|
+
beginFinalizing();
|
|
182
|
+
try {
|
|
183
|
+
await lane.replayClaim?.commit();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
reportReplayCommitError(error);
|
|
186
|
+
}
|
|
187
|
+
lane.status = "completed";
|
|
188
|
+
await maybeSettle();
|
|
189
|
+
},
|
|
190
|
+
onDeferred: () => {
|
|
191
|
+
if (lane.status !== "pending") return;
|
|
192
|
+
lane.status = "deferred";
|
|
193
|
+
defer();
|
|
194
|
+
},
|
|
195
|
+
onAdoptionFinalizing: beginFinalizing,
|
|
196
|
+
onAbandoned: async () => {
|
|
197
|
+
if (lane.status === "completed" || lane.status === "failed" || lane.status === "abandoned") return;
|
|
198
|
+
lane.status = "abandoned";
|
|
199
|
+
releaseLane(/* @__PURE__ */ new Error("feishu-broadcast-turn-abandoned"));
|
|
200
|
+
await maybeSettle();
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
onDispatchComplete: async (dispatched) => {
|
|
204
|
+
if (!dispatched && lane.status === "pending") {
|
|
205
|
+
const error = /* @__PURE__ */ new Error("feishu broadcast lane was not dispatched");
|
|
206
|
+
lane.status = "failed";
|
|
207
|
+
failures.push(error);
|
|
208
|
+
releaseLane(error);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (lane.status !== "pending") return;
|
|
212
|
+
const error = /* @__PURE__ */ new Error("feishu broadcast dispatch returned before turn adoption");
|
|
213
|
+
lane.status = "failed";
|
|
214
|
+
failures.push(error);
|
|
215
|
+
releaseLane(error);
|
|
216
|
+
},
|
|
217
|
+
onDispatchFailed: async (error) => {
|
|
218
|
+
failures.push(error);
|
|
219
|
+
if (lane.status !== "completed") {
|
|
220
|
+
lane.status = "failed";
|
|
221
|
+
releaseLane(error);
|
|
222
|
+
}
|
|
223
|
+
await maybeSettle();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
onLanePending: defer,
|
|
228
|
+
onDispatchComplete: async () => {
|
|
229
|
+
fanoutSettled = true;
|
|
230
|
+
await maybeSettle();
|
|
231
|
+
},
|
|
232
|
+
onDispatchFailed: async (error) => {
|
|
233
|
+
failures.push(error);
|
|
234
|
+
fanoutSettled = true;
|
|
235
|
+
await maybeSettle();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
75
239
|
function resolveBroadcastAgents(cfg, peerId) {
|
|
76
240
|
const broadcast = cfg.broadcast;
|
|
77
241
|
if (!broadcast || typeof broadcast !== "object") return null;
|
|
@@ -137,11 +301,14 @@ function resolveFeishuGroupSession(params) {
|
|
|
137
301
|
};
|
|
138
302
|
}
|
|
139
303
|
function parseMessageContent(content, messageType) {
|
|
140
|
-
if (messageType === "post") return parsePostContent(content
|
|
304
|
+
if (messageType === "post") return parsePostContent(content, {
|
|
305
|
+
renderMediaPlaceholders: false,
|
|
306
|
+
emptyTextFallback: ""
|
|
307
|
+
}).textContent;
|
|
141
308
|
try {
|
|
142
309
|
const parsed = JSON.parse(content);
|
|
143
310
|
if (messageType === "text") return parsed.text || "";
|
|
144
|
-
if (FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return formatFeishuMediaContent(parsed, messageType)
|
|
311
|
+
if (FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return formatFeishuMediaContent(parsed, messageType);
|
|
145
312
|
if (messageType === "share_chat") {
|
|
146
313
|
if (parsed && typeof parsed === "object") {
|
|
147
314
|
const share = parsed;
|
|
@@ -154,7 +321,7 @@ function parseMessageContent(content, messageType) {
|
|
|
154
321
|
if (messageType === "merge_forward") return "[Merged and Forwarded Message - loading...]";
|
|
155
322
|
return content;
|
|
156
323
|
} catch {
|
|
157
|
-
return content;
|
|
324
|
+
return FEISHU_MEDIA_MESSAGE_TYPES.has(messageType) ? "" : content;
|
|
158
325
|
}
|
|
159
326
|
}
|
|
160
327
|
const FEISHU_MEDIA_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -167,32 +334,8 @@ const FEISHU_MEDIA_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
167
334
|
]);
|
|
168
335
|
function formatFeishuMediaContent(parsed, messageType) {
|
|
169
336
|
const speechToText = messageType === "audio" && typeof parsed.speech_to_text === "string" ? parsed.speech_to_text.trim() : "";
|
|
170
|
-
if (speechToText) return
|
|
171
|
-
|
|
172
|
-
const fileName = typeof parsed.file_name === "string" ? parsed.file_name.trim() : "";
|
|
173
|
-
return {
|
|
174
|
-
body: fileName ? `${placeholder} (${fileName})` : placeholder,
|
|
175
|
-
mediaPlaceholder: placeholder,
|
|
176
|
-
unavailableBody: fileName || void 0
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
function resolveFeishuMediaFailurePresentation(content, messageType) {
|
|
180
|
-
if (messageType === "post") return { unavailableBody: parsePostContent(content, {
|
|
181
|
-
renderMediaPlaceholders: false,
|
|
182
|
-
emptyTextFallback: ""
|
|
183
|
-
}).textContent };
|
|
184
|
-
if (!FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return {};
|
|
185
|
-
try {
|
|
186
|
-
const parsed = JSON.parse(content);
|
|
187
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
188
|
-
const presentation = formatFeishuMediaContent(parsed, messageType);
|
|
189
|
-
return {
|
|
190
|
-
mediaPlaceholder: presentation.mediaPlaceholder,
|
|
191
|
-
unavailableBody: presentation.unavailableBody
|
|
192
|
-
};
|
|
193
|
-
} catch {
|
|
194
|
-
return {};
|
|
195
|
-
}
|
|
337
|
+
if (speechToText) return speechToText;
|
|
338
|
+
return "";
|
|
196
339
|
}
|
|
197
340
|
function formatSubMessageContent(content, contentType) {
|
|
198
341
|
try {
|
|
@@ -293,15 +436,15 @@ async function resolveSavedFeishuMedia(params) {
|
|
|
293
436
|
const contentType = params.result.contentType ?? await core.media.detectMime({ buffer: params.result.buffer });
|
|
294
437
|
return await core.channel.media.saveMediaBuffer(params.result.buffer, contentType, "inbound", params.maxBytes, params.result.fileName ?? params.originalFilename);
|
|
295
438
|
}
|
|
296
|
-
function
|
|
439
|
+
function resolveFeishuMediaKind(messageType) {
|
|
297
440
|
switch (messageType) {
|
|
298
|
-
case "image": return "
|
|
299
|
-
case "file": return "
|
|
300
|
-
case "audio": return "
|
|
441
|
+
case "image": return "image";
|
|
442
|
+
case "file": return "document";
|
|
443
|
+
case "audio": return "audio";
|
|
301
444
|
case "video":
|
|
302
|
-
case "media": return "
|
|
303
|
-
case "sticker": return "
|
|
304
|
-
default: return "
|
|
445
|
+
case "media": return "video";
|
|
446
|
+
case "sticker": return "sticker";
|
|
447
|
+
default: return "document";
|
|
305
448
|
}
|
|
306
449
|
}
|
|
307
450
|
async function resolveFeishuMediaList(params) {
|
|
@@ -314,18 +457,11 @@ async function resolveFeishuMediaList(params) {
|
|
|
314
457
|
"media",
|
|
315
458
|
"sticker",
|
|
316
459
|
"post"
|
|
317
|
-
].includes(messageType)) return
|
|
318
|
-
media: [],
|
|
319
|
-
unavailableCount: 0
|
|
320
|
-
};
|
|
460
|
+
].includes(messageType)) return [];
|
|
321
461
|
const out = [];
|
|
322
|
-
let unavailableCount = 0;
|
|
323
462
|
if (messageType === "post") {
|
|
324
463
|
const { imageKeys, mediaKeys } = parsePostContent(content);
|
|
325
|
-
if (imageKeys.length === 0 && mediaKeys.length === 0) return
|
|
326
|
-
media: [],
|
|
327
|
-
unavailableCount: 0
|
|
328
|
-
};
|
|
464
|
+
if (imageKeys.length === 0 && mediaKeys.length === 0) return [];
|
|
329
465
|
if (imageKeys.length > 0) log?.(`feishu: post message contains ${imageKeys.length} embedded image(s)`);
|
|
330
466
|
if (mediaKeys.length > 0) log?.(`feishu: post message contains ${mediaKeys.length} embedded media file(s)`);
|
|
331
467
|
for (const imageKey of imageKeys) try {
|
|
@@ -343,11 +479,11 @@ async function resolveFeishuMediaList(params) {
|
|
|
343
479
|
out.push({
|
|
344
480
|
path: saved.path,
|
|
345
481
|
contentType: saved.contentType,
|
|
346
|
-
|
|
482
|
+
kind: "image"
|
|
347
483
|
});
|
|
348
484
|
log?.(`feishu: downloaded embedded image ${imageKey}, saved to ${saved.path}`);
|
|
349
485
|
} catch (err) {
|
|
350
|
-
|
|
486
|
+
out.push({ kind: "image" });
|
|
351
487
|
log?.(`feishu: failed to download embedded image ${imageKey}: ${String(err)}`);
|
|
352
488
|
}
|
|
353
489
|
for (const media of mediaKeys) try {
|
|
@@ -367,29 +503,20 @@ async function resolveFeishuMediaList(params) {
|
|
|
367
503
|
out.push({
|
|
368
504
|
path: saved.path,
|
|
369
505
|
contentType: saved.contentType,
|
|
370
|
-
|
|
506
|
+
kind: "video"
|
|
371
507
|
});
|
|
372
508
|
log?.(`feishu: downloaded embedded media ${media.fileKey}, saved to ${saved.path}`);
|
|
373
509
|
} catch (err) {
|
|
374
|
-
|
|
510
|
+
out.push({ kind: "video" });
|
|
375
511
|
log?.(`feishu: failed to download embedded media ${media.fileKey}: ${String(err)}`);
|
|
376
512
|
}
|
|
377
|
-
return
|
|
378
|
-
media: out,
|
|
379
|
-
unavailableCount
|
|
380
|
-
};
|
|
513
|
+
return out;
|
|
381
514
|
}
|
|
382
515
|
const mediaKeys = parseMediaKeys(content, messageType);
|
|
383
|
-
if (!mediaKeys.imageKey && !mediaKeys.fileKey) return {
|
|
384
|
-
media: [],
|
|
385
|
-
unavailableCount: 1
|
|
386
|
-
};
|
|
516
|
+
if (!mediaKeys.imageKey && !mediaKeys.fileKey) return [{ kind: resolveFeishuMediaKind(messageType) }];
|
|
387
517
|
try {
|
|
388
518
|
const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
|
|
389
|
-
if (!fileKey) return {
|
|
390
|
-
media: [],
|
|
391
|
-
unavailableCount: 1
|
|
392
|
-
};
|
|
519
|
+
if (!fileKey) return [{ kind: resolveFeishuMediaKind(messageType) }];
|
|
393
520
|
const saved = await resolveSavedFeishuMedia({
|
|
394
521
|
result: await saveMessageResourceFeishu({
|
|
395
522
|
cfg,
|
|
@@ -406,17 +533,14 @@ async function resolveFeishuMediaList(params) {
|
|
|
406
533
|
out.push({
|
|
407
534
|
path: saved.path,
|
|
408
535
|
contentType: saved.contentType,
|
|
409
|
-
|
|
536
|
+
kind: resolveFeishuMediaKind(messageType)
|
|
410
537
|
});
|
|
411
538
|
log?.(`feishu: downloaded ${messageType} media, saved to ${saved.path}`);
|
|
412
539
|
} catch (err) {
|
|
413
|
-
|
|
540
|
+
out.push({ kind: resolveFeishuMediaKind(messageType) });
|
|
414
541
|
log?.(`feishu: failed to download ${messageType} media: ${String(err)}`);
|
|
415
542
|
}
|
|
416
|
-
return
|
|
417
|
-
media: out,
|
|
418
|
-
unavailableCount
|
|
419
|
-
};
|
|
543
|
+
return out;
|
|
420
544
|
}
|
|
421
545
|
//#endregion
|
|
422
546
|
//#region extensions/feishu/src/bot-group-name-state.ts
|
|
@@ -478,10 +602,150 @@ async function resolveGroupName(params) {
|
|
|
478
602
|
return resolvedName;
|
|
479
603
|
}
|
|
480
604
|
//#endregion
|
|
605
|
+
//#region extensions/feishu/src/bot-name.ts
|
|
606
|
+
const POSITIVE_TTL_MS = 10 * 6e4;
|
|
607
|
+
const NEGATIVE_TTL_MS = 6e4;
|
|
608
|
+
const MAX_CACHE_ENTRIES = 5e3;
|
|
609
|
+
const BREAKER_FAILURE_THRESHOLD = 10;
|
|
610
|
+
const BREAKER_OPEN_MS = 60 * 6e4;
|
|
611
|
+
const PERMISSION_BACKOFF_MS = 6e4;
|
|
612
|
+
const REQUEST_TIMEOUT_MS = 1500;
|
|
613
|
+
const cache = /* @__PURE__ */ new Map();
|
|
614
|
+
const breakerByAccount = /* @__PURE__ */ new Map();
|
|
615
|
+
const permissionBackoffUntilByAccount = /* @__PURE__ */ new Map();
|
|
616
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
617
|
+
function resolveCacheKey(accountId, openId) {
|
|
618
|
+
return `${accountId}::${openId}`;
|
|
619
|
+
}
|
|
620
|
+
function readCache(key) {
|
|
621
|
+
const entry = cache.get(key);
|
|
622
|
+
if (!entry) return;
|
|
623
|
+
if (entry.expiresAt <= Date.now()) {
|
|
624
|
+
cache.delete(key);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
cache.delete(key);
|
|
628
|
+
cache.set(key, entry);
|
|
629
|
+
return entry;
|
|
630
|
+
}
|
|
631
|
+
function writeCache(key, entry) {
|
|
632
|
+
cache.delete(key);
|
|
633
|
+
cache.set(key, entry);
|
|
634
|
+
pruneMapToMaxSize(cache, MAX_CACHE_ENTRIES);
|
|
635
|
+
}
|
|
636
|
+
function isBreakerOpen(accountId) {
|
|
637
|
+
const state = breakerByAccount.get(accountId);
|
|
638
|
+
if (!state) return false;
|
|
639
|
+
if (state.openUntil > Date.now()) return true;
|
|
640
|
+
if (state.openUntil > 0) breakerByAccount.delete(accountId);
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
function recordFailure(accountId) {
|
|
644
|
+
const state = breakerByAccount.get(accountId) ?? {
|
|
645
|
+
failures: 0,
|
|
646
|
+
openUntil: 0
|
|
647
|
+
};
|
|
648
|
+
state.failures += 1;
|
|
649
|
+
if (state.failures >= BREAKER_FAILURE_THRESHOLD) {
|
|
650
|
+
state.failures = 0;
|
|
651
|
+
state.openUntil = Date.now() + BREAKER_OPEN_MS;
|
|
652
|
+
}
|
|
653
|
+
breakerByAccount.set(accountId, state);
|
|
654
|
+
}
|
|
655
|
+
function recordSuccess(accountId) {
|
|
656
|
+
breakerByAccount.delete(accountId);
|
|
657
|
+
}
|
|
658
|
+
function readFeishuErrorCode(error) {
|
|
659
|
+
if (!error || typeof error !== "object") return;
|
|
660
|
+
const data = error.response?.data;
|
|
661
|
+
if (!data || typeof data !== "object") return;
|
|
662
|
+
const code = data.code;
|
|
663
|
+
return typeof code === "number" ? code : void 0;
|
|
664
|
+
}
|
|
665
|
+
async function requestBotName(params) {
|
|
666
|
+
const { account, openId, log } = params;
|
|
667
|
+
const query = new URLSearchParams({ bot_ids: openId });
|
|
668
|
+
try {
|
|
669
|
+
const response = await createFeishuClient(account).request({
|
|
670
|
+
method: "GET",
|
|
671
|
+
url: `/open-apis/bot/v3/bots/basic_batch?${query.toString()}`,
|
|
672
|
+
timeout: REQUEST_TIMEOUT_MS
|
|
673
|
+
});
|
|
674
|
+
const code = response.code ?? 0;
|
|
675
|
+
if (code === 0) return response;
|
|
676
|
+
if (code === 99991672) {
|
|
677
|
+
log(`feishu[${account.accountId}]: bot.basic_info scope not granted`);
|
|
678
|
+
return "permission";
|
|
679
|
+
}
|
|
680
|
+
log(`feishu[${account.accountId}]: bot name lookup failed (code=${code})`);
|
|
681
|
+
return "failure";
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (readFeishuErrorCode(error) === 99991672) {
|
|
684
|
+
log(`feishu[${account.accountId}]: bot.basic_info scope not granted`);
|
|
685
|
+
return "permission";
|
|
686
|
+
}
|
|
687
|
+
log(`feishu[${account.accountId}]: bot name lookup failed: ${String(error)}`);
|
|
688
|
+
return "failure";
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function resolveUncachedBotName(params) {
|
|
692
|
+
const { account, openId, cacheKey, log } = params;
|
|
693
|
+
if (isBreakerOpen(account.accountId)) {
|
|
694
|
+
log(`feishu[${account.accountId}]: bot name lookup skipped (breaker open)`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const result = await requestBotName({
|
|
698
|
+
account,
|
|
699
|
+
openId,
|
|
700
|
+
log
|
|
701
|
+
});
|
|
702
|
+
if (result === "permission") {
|
|
703
|
+
permissionBackoffUntilByAccount.set(account.accountId, Date.now() + PERMISSION_BACKOFF_MS);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (result === "failure") {
|
|
707
|
+
recordFailure(account.accountId);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
recordSuccess(account.accountId);
|
|
711
|
+
const name = result.data?.bots?.[openId]?.name?.trim();
|
|
712
|
+
writeCache(cacheKey, {
|
|
713
|
+
...name ? { name } : {},
|
|
714
|
+
expiresAt: Date.now() + (name ? POSITIVE_TTL_MS : NEGATIVE_TTL_MS)
|
|
715
|
+
});
|
|
716
|
+
return name;
|
|
717
|
+
}
|
|
718
|
+
async function resolveFeishuBotName(params) {
|
|
719
|
+
const openId = params.openId.trim();
|
|
720
|
+
if (!params.account.configured || !openId) return;
|
|
721
|
+
const permissionBackoffUntil = permissionBackoffUntilByAccount.get(params.account.accountId);
|
|
722
|
+
if (permissionBackoffUntil !== void 0) {
|
|
723
|
+
if (permissionBackoffUntil > Date.now()) return;
|
|
724
|
+
permissionBackoffUntilByAccount.delete(params.account.accountId);
|
|
725
|
+
}
|
|
726
|
+
const key = resolveCacheKey(params.account.accountId, openId);
|
|
727
|
+
const cached = readCache(key);
|
|
728
|
+
if (cached) return cached.name;
|
|
729
|
+
const pending = inflight.get(key);
|
|
730
|
+
if (pending) return pending;
|
|
731
|
+
const lookup = resolveUncachedBotName({
|
|
732
|
+
...params,
|
|
733
|
+
openId,
|
|
734
|
+
cacheKey: key
|
|
735
|
+
});
|
|
736
|
+
inflight.set(key, lookup);
|
|
737
|
+
try {
|
|
738
|
+
return await lookup;
|
|
739
|
+
} finally {
|
|
740
|
+
inflight.delete(key);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
//#endregion
|
|
481
744
|
//#region extensions/feishu/src/bot-sender-name.ts
|
|
482
745
|
const IGNORED_PERMISSION_SCOPE_TOKENS = ["contact:contact.base:readonly"];
|
|
483
746
|
const FEISHU_SCOPE_CORRECTIONS = { "contact:contact.base:readonly": "contact:user.base:readonly" };
|
|
484
747
|
const SENDER_NAME_TTL_MS = 600 * 1e3;
|
|
748
|
+
const SENDER_NAME_CACHE_MAX_SIZE = 500;
|
|
485
749
|
const senderNameCache = /* @__PURE__ */ new Map();
|
|
486
750
|
function correctFeishuScopeInUrl(url) {
|
|
487
751
|
let corrected = url;
|
|
@@ -535,10 +799,13 @@ async function resolveFeishuSenderName(params) {
|
|
|
535
799
|
const name = user?.name ?? user?.nickname ?? user?.en_name;
|
|
536
800
|
if (name) {
|
|
537
801
|
const expireAt = resolveExpiresAtMsFromDurationMs(SENDER_NAME_TTL_MS);
|
|
538
|
-
if (expireAt !== void 0)
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
802
|
+
if (expireAt !== void 0) {
|
|
803
|
+
senderNameCache.set(normalizedSenderId, {
|
|
804
|
+
name,
|
|
805
|
+
expireAt
|
|
806
|
+
});
|
|
807
|
+
pruneMapToMaxSize(senderNameCache, SENDER_NAME_CACHE_MAX_SIZE);
|
|
808
|
+
}
|
|
542
809
|
return { name };
|
|
543
810
|
}
|
|
544
811
|
return {};
|
|
@@ -597,9 +864,7 @@ function dedupeOptions(namespace, log) {
|
|
|
597
864
|
* unidentifiable event is never suppressed.
|
|
598
865
|
*/
|
|
599
866
|
async function claimUnprocessedFeishuMessage(params) {
|
|
600
|
-
|
|
601
|
-
if (claim.kind === "inflight") return { kind: "inflight" };
|
|
602
|
-
return claim;
|
|
867
|
+
return await feishuDedupeState.guard.claim(params.messageId, dedupeOptions(params.namespace, params.log));
|
|
603
868
|
}
|
|
604
869
|
/**
|
|
605
870
|
* Claims (unless the caller already holds the claim) and commits a message.
|
|
@@ -614,11 +879,6 @@ async function finalizeFeishuMessageProcessing(params) {
|
|
|
614
879
|
if ("kind" in claim && claim.kind !== "claimed") return false;
|
|
615
880
|
return await ("kind" in claim ? claim.handle : claim).commit();
|
|
616
881
|
}
|
|
617
|
-
/** Records a handled message so restart/replay cannot dispatch it again; false when already recorded. */
|
|
618
|
-
async function recordProcessedFeishuMessage(messageId, namespace = "global", log) {
|
|
619
|
-
const claim = await feishuDedupeState.guard.claim(messageId, dedupeOptions(namespace, log));
|
|
620
|
-
return claim.kind === "claimed" ? await claim.handle.commit() : false;
|
|
621
|
-
}
|
|
622
882
|
/** Forgets a recorded message so a retryable synthetic event can be handled on redelivery. */
|
|
623
883
|
async function forgetProcessedFeishuMessage(messageId, namespace = "global", log) {
|
|
624
884
|
return await feishuDedupeState.guard.forget(messageId, dedupeOptions(namespace, log));
|
|
@@ -876,6 +1136,47 @@ function resolveFeishuReasoningPreviewEnabled(params) {
|
|
|
876
1136
|
return configDefault === "stream";
|
|
877
1137
|
}
|
|
878
1138
|
//#endregion
|
|
1139
|
+
//#region extensions/feishu/src/reply-delivery-result.ts
|
|
1140
|
+
const noVisibleFeishuReplyDelivery = { visibleReplySent: false };
|
|
1141
|
+
function hasProviderIdentity(result) {
|
|
1142
|
+
return Boolean(result && (result.messageId?.trim() || result.receipt?.primaryPlatformMessageId?.trim() || result.receipt?.platformMessageIds.length));
|
|
1143
|
+
}
|
|
1144
|
+
/** Normalizes every physical Lark send behind one logical reply payload. */
|
|
1145
|
+
function createFeishuReplyDeliveryResult(params) {
|
|
1146
|
+
const results = params.visibleReplySent ? (params.results ?? []).filter(hasProviderIdentity) : [];
|
|
1147
|
+
const receipt = results.length > 0 ? createMessageReceiptFromOutboundResults({
|
|
1148
|
+
results,
|
|
1149
|
+
...params.kind ? { kind: params.kind } : {}
|
|
1150
|
+
}) : void 0;
|
|
1151
|
+
if (!receipt) return {
|
|
1152
|
+
visibleReplySent: params.visibleReplySent,
|
|
1153
|
+
...params.content === void 0 ? {} : { content: params.content }
|
|
1154
|
+
};
|
|
1155
|
+
return {
|
|
1156
|
+
messageIds: [...receipt.platformMessageIds],
|
|
1157
|
+
receipt,
|
|
1158
|
+
visibleReplySent: params.visibleReplySent,
|
|
1159
|
+
...params.content === void 0 ? {} : { content: params.content }
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
/** Preserves the first result's provider identity while retaining supplemental ids. */
|
|
1163
|
+
function mergeFeishuReplyDeliveryResults(results, content) {
|
|
1164
|
+
const visible = results.filter((result) => result.visibleReplySent === true);
|
|
1165
|
+
return createFeishuReplyDeliveryResult({
|
|
1166
|
+
results: visible,
|
|
1167
|
+
visibleReplySent: visible.length > 0,
|
|
1168
|
+
content: content === void 0 ? results.find((result) => result.content !== void 0)?.content : content
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
/** Keeps a failed outcome while exposing the provider-visible subset to core lifecycle hooks. */
|
|
1172
|
+
function createFeishuPartialReplyDeliveryError(cause, result) {
|
|
1173
|
+
if (result.visibleReplySent !== true) return cause instanceof Error ? cause : new Error(formatErrorMessage(cause), { cause });
|
|
1174
|
+
return createChannelPartialDeliveryError(cause, {
|
|
1175
|
+
...result,
|
|
1176
|
+
visibleReplySent: true
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
//#endregion
|
|
879
1180
|
//#region extensions/feishu/src/reply-dispatcher-state.ts
|
|
880
1181
|
const streamingStartBackoffUntilByAccount = /* @__PURE__ */ new Map();
|
|
881
1182
|
//#endregion
|
|
@@ -887,6 +1188,14 @@ function resolveStreamingCardSendMode(options) {
|
|
|
887
1188
|
}
|
|
888
1189
|
//#endregion
|
|
889
1190
|
//#region extensions/feishu/src/streaming-card.ts
|
|
1191
|
+
/** Provider finalization failed after a streaming card may already be visible. */
|
|
1192
|
+
var FeishuStreamingFinalizationError = class extends Error {
|
|
1193
|
+
constructor(cause, result) {
|
|
1194
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
1195
|
+
this.name = "FeishuStreamingFinalizationError";
|
|
1196
|
+
this.result = result;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
890
1199
|
const STREAMING_UPDATE_THROTTLE_MS = 160;
|
|
891
1200
|
const STREAMING_SIGNIFICANT_DELTA_CHARS = 18;
|
|
892
1201
|
const FEISHU_STREAMING_TOKEN_DEFAULT_LIFETIME_SECONDS = 7200;
|
|
@@ -961,23 +1270,17 @@ function truncateSummary(text, max = 50) {
|
|
|
961
1270
|
const clean = text.replace(/\n/g, " ").trim();
|
|
962
1271
|
return clean.length <= max ? clean : sliceUtf16Safe(clean, 0, max - 3) + "...";
|
|
963
1272
|
}
|
|
964
|
-
function hasNaturalStreamingBoundary(text) {
|
|
965
|
-
return /[\n。!?!?;;::]$/.test(text);
|
|
966
|
-
}
|
|
967
1273
|
function shouldPushStreamingUpdate(previousText, nextText) {
|
|
968
|
-
|
|
969
|
-
if (hasNaturalStreamingBoundary(nextText)) return true;
|
|
970
|
-
return nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
|
|
1274
|
+
return !previousText || /[\n。!?!?;;::]$/.test(nextText) || nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
|
|
971
1275
|
}
|
|
1276
|
+
/** Merges cumulative or overlapping streaming snapshots without duplicating content. */
|
|
972
1277
|
function mergeStreamingText(previousText, nextText) {
|
|
973
1278
|
const previous = typeof previousText === "string" ? previousText : "";
|
|
974
1279
|
const next = typeof nextText === "string" ? nextText : "";
|
|
975
1280
|
if (!next) return previous;
|
|
976
1281
|
if (!previous || next === previous) return next;
|
|
977
|
-
if (next.startsWith(previous)) return next;
|
|
978
|
-
if (previous.startsWith(next)) return previous;
|
|
979
|
-
if (next.includes(previous)) return next;
|
|
980
|
-
if (previous.includes(next)) return previous;
|
|
1282
|
+
if (next.startsWith(previous) || next.includes(previous)) return next;
|
|
1283
|
+
if (previous.startsWith(next) || previous.includes(next)) return previous;
|
|
981
1284
|
const maxOverlap = Math.min(previous.length, next.length);
|
|
982
1285
|
for (let overlap = maxOverlap; overlap > 0; overlap -= 1) if (previous.slice(-overlap) === next.slice(0, overlap)) return `${previous}${next.slice(overlap)}`;
|
|
983
1286
|
return `${previous}${next}`;
|
|
@@ -1267,16 +1570,23 @@ var FeishuStreamingSession = class {
|
|
|
1267
1570
|
}
|
|
1268
1571
|
}).catch((e) => this.log?.(`Note update failed: ${String(e)}`));
|
|
1269
1572
|
}
|
|
1270
|
-
async
|
|
1271
|
-
if (!this.state || this.closed) return false;
|
|
1573
|
+
async closeWithResult(finalText, options) {
|
|
1574
|
+
if (!this.state || this.closed) return { visibleReplySent: false };
|
|
1272
1575
|
this.closed = true;
|
|
1273
1576
|
this.clearFlushTimer();
|
|
1274
1577
|
await this.queue;
|
|
1275
1578
|
const text = finalText ?? this.pendingText ?? this.state.currentText;
|
|
1276
1579
|
const apiBase = resolveApiBase(this.creds.domain);
|
|
1277
1580
|
let visibleContentSent = Boolean(this.state.sentText.trim());
|
|
1581
|
+
let finalWriteError;
|
|
1278
1582
|
if ((text || finalText !== void 0) && text !== this.state.sentText) {
|
|
1279
|
-
const sent = text.startsWith(this.state.sentText) ? await this.updateCardContent(text, (e) =>
|
|
1583
|
+
const sent = text.startsWith(this.state.sentText) ? await this.updateCardContent(text, (e) => {
|
|
1584
|
+
finalWriteError = e;
|
|
1585
|
+
this.log?.(`Final update failed: ${String(e)}`);
|
|
1586
|
+
}) : await this.replaceCardContent(text, (e) => {
|
|
1587
|
+
finalWriteError = e;
|
|
1588
|
+
this.log?.(`Final replace failed: ${String(e)}`);
|
|
1589
|
+
});
|
|
1280
1590
|
this.state.currentText = text;
|
|
1281
1591
|
if (sent) {
|
|
1282
1592
|
this.state.sentText = text;
|
|
@@ -1286,44 +1596,63 @@ var FeishuStreamingSession = class {
|
|
|
1286
1596
|
if (options?.note) await this.updateNoteContent(options.note);
|
|
1287
1597
|
const acceptedText = this.state.sentText;
|
|
1288
1598
|
this.state.sequence += 1;
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1599
|
+
let closeError;
|
|
1600
|
+
try {
|
|
1601
|
+
const { response, release } = await fetchWithSsrFGuard({
|
|
1602
|
+
url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`,
|
|
1603
|
+
init: {
|
|
1604
|
+
method: "PATCH",
|
|
1605
|
+
headers: {
|
|
1606
|
+
Authorization: `Bearer ${await getToken(this.creds, {
|
|
1607
|
+
fetchImpl: this.fetchImpl,
|
|
1608
|
+
lookupFn: this.lookupFn
|
|
1609
|
+
})}`,
|
|
1610
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1611
|
+
"User-Agent": getFeishuUserAgent()
|
|
1612
|
+
},
|
|
1613
|
+
body: JSON.stringify({
|
|
1614
|
+
settings: JSON.stringify({ config: {
|
|
1615
|
+
streaming_mode: false,
|
|
1616
|
+
summary: { content: truncateSummary(acceptedText) }
|
|
1617
|
+
} }),
|
|
1618
|
+
sequence: this.state.sequence,
|
|
1619
|
+
uuid: `c_${this.state.cardId}_${this.state.sequence}`
|
|
1620
|
+
})
|
|
1300
1621
|
},
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
uuid: `c_${this.state.cardId}_${this.state.sequence}`
|
|
1308
|
-
})
|
|
1309
|
-
},
|
|
1310
|
-
fetchImpl: this.fetchImpl,
|
|
1311
|
-
lookupFn: this.lookupFn,
|
|
1312
|
-
policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
|
|
1313
|
-
auditContext: "feishu.streaming-card.close",
|
|
1314
|
-
timeoutMs: this.creds.httpTimeoutMs ?? 3e4
|
|
1315
|
-
}).then(async ({ response, release }) => {
|
|
1622
|
+
fetchImpl: this.fetchImpl,
|
|
1623
|
+
lookupFn: this.lookupFn,
|
|
1624
|
+
policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
|
|
1625
|
+
auditContext: "feishu.streaming-card.close",
|
|
1626
|
+
timeoutMs: this.creds.httpTimeoutMs ?? 3e4
|
|
1627
|
+
});
|
|
1316
1628
|
try {
|
|
1317
1629
|
await assertSuccessfulCardKitResponse(response, "feishu.streaming-card.close", "Close streaming card");
|
|
1318
1630
|
} finally {
|
|
1319
1631
|
await release();
|
|
1320
1632
|
}
|
|
1321
|
-
}
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
closeError = error;
|
|
1635
|
+
this.log?.(`Close failed: ${String(error)}`);
|
|
1636
|
+
}
|
|
1322
1637
|
const finalState = this.state;
|
|
1323
1638
|
this.state = null;
|
|
1324
1639
|
this.pendingText = null;
|
|
1325
1640
|
this.log?.(`Closed streaming: cardId=${finalState.cardId}`);
|
|
1326
|
-
|
|
1641
|
+
const result = {
|
|
1642
|
+
visibleReplySent: visibleContentSent,
|
|
1643
|
+
...visibleContentSent ? { content: finalState.sentText } : {},
|
|
1644
|
+
messageId: finalState.messageId
|
|
1645
|
+
};
|
|
1646
|
+
if (finalWriteError !== void 0 || closeError !== void 0) throw new FeishuStreamingFinalizationError(finalWriteError !== void 0 && closeError !== void 0 ? new AggregateError([finalWriteError, closeError], "Feishu streaming card finalization failed") : finalWriteError ?? closeError, result);
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1649
|
+
async close(finalText, options) {
|
|
1650
|
+
try {
|
|
1651
|
+
return (await this.closeWithResult(finalText, options)).visibleReplySent;
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
if (error instanceof FeishuStreamingFinalizationError) return error.result.visibleReplySent;
|
|
1654
|
+
throw error;
|
|
1655
|
+
}
|
|
1327
1656
|
}
|
|
1328
1657
|
async discard() {
|
|
1329
1658
|
if (!this.state || this.closed) return;
|
|
@@ -1469,6 +1798,9 @@ function mergeStreamingFinalText(previousText, nextText, appendError) {
|
|
|
1469
1798
|
if (previousText.endsWith(`\n\n${nextText}`)) return previousText;
|
|
1470
1799
|
return `${previousText}\n\n${nextText}`;
|
|
1471
1800
|
}
|
|
1801
|
+
function toError(error) {
|
|
1802
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1803
|
+
}
|
|
1472
1804
|
/** Maximum age (ms) for a message to receive a typing indicator reaction.
|
|
1473
1805
|
* Messages older than this are likely replays after context compaction (#30418). */
|
|
1474
1806
|
const TYPING_INDICATOR_MAX_AGE_MS = 2 * 6e4;
|
|
@@ -1489,11 +1821,6 @@ function rememberStreamingStartFailure(accountId, now = Date.now()) {
|
|
|
1489
1821
|
streamingStartBackoffUntilByAccount.set(accountId, backoffUntil);
|
|
1490
1822
|
return backoffUntil;
|
|
1491
1823
|
}
|
|
1492
|
-
function formatMediaFallbackText(text, mediaUrl) {
|
|
1493
|
-
const trimmedText = text?.trim() ?? "";
|
|
1494
|
-
const attachmentText = `📎 ${mediaUrl}`;
|
|
1495
|
-
return trimmedText ? `${trimmedText}\n\n${attachmentText}` : attachmentText;
|
|
1496
|
-
}
|
|
1497
1824
|
function normalizeEpochMs(timestamp) {
|
|
1498
1825
|
if (!Number.isFinite(timestamp) || timestamp === void 0 || timestamp <= 0) return;
|
|
1499
1826
|
return timestamp < MS_EPOCH_MIN ? timestamp * 1e3 : timestamp;
|
|
@@ -1518,7 +1845,7 @@ function resolveCardNote(agentId, identity, prefixCtx) {
|
|
|
1518
1845
|
}
|
|
1519
1846
|
function createFeishuReplyDispatcher(params) {
|
|
1520
1847
|
const core = getFeishuRuntime();
|
|
1521
|
-
const { cfg, agentId, chatId, sendTarget, replyToMessageId, typingTargetMessageId: explicitTypingTargetMessageId, skipReplyToInMessages, replyInThread, threadReply, rootId, accountId, identity, mentionTargets } = params;
|
|
1848
|
+
const { cfg, agentId, chatId, sendTarget, replyToMessageId, typingTargetMessageId: explicitTypingTargetMessageId, skipReplyToInMessages, replyInThread, threadReply, rootId, accountId, identity, mentionTargets, requiredMentionTargets } = params;
|
|
1522
1849
|
const sendReplyToMessageId = skipReplyToInMessages ? void 0 : replyToMessageId;
|
|
1523
1850
|
const typingTargetMessageId = explicitTypingTargetMessageId?.trim() || replyToMessageId;
|
|
1524
1851
|
const threadReplyMode = threadReply === true;
|
|
@@ -1583,10 +1910,13 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1583
1910
|
channel: "feishu"
|
|
1584
1911
|
});
|
|
1585
1912
|
const renderMode = account.config?.renderMode ?? "auto";
|
|
1586
|
-
const streamingEnabled = resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
|
|
1913
|
+
const streamingEnabled = !requiredMentionTargets?.length && resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
|
|
1914
|
+
const hookRunner = getGlobalHookRunner();
|
|
1915
|
+
const modifyingHooksRegistered = (hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false);
|
|
1916
|
+
const previewStreamingEnabled = streamingEnabled && !modifyingHooksRegistered;
|
|
1587
1917
|
const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
|
|
1588
1918
|
const coreBlockStreamingEnabled = blockStreamingEnabled === true;
|
|
1589
|
-
const reasoningPreviewEnabled =
|
|
1919
|
+
const reasoningPreviewEnabled = previewStreamingEnabled && params.allowReasoningPreview === true;
|
|
1590
1920
|
let streaming = null;
|
|
1591
1921
|
let streamText = "";
|
|
1592
1922
|
let lastPartial = "";
|
|
@@ -1596,15 +1926,20 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1596
1926
|
let lastSnapshotTextLength = 0;
|
|
1597
1927
|
let hasStreamingFinalText = false;
|
|
1598
1928
|
const deliveredFinalTexts = /* @__PURE__ */ new Set();
|
|
1929
|
+
const closedStreamingSettlements = /* @__PURE__ */ new Map();
|
|
1599
1930
|
let sentIndependentBlockText = false;
|
|
1600
1931
|
let partialUpdateQueue = Promise.resolve();
|
|
1601
1932
|
let streamingStartPromise = null;
|
|
1602
|
-
let
|
|
1603
|
-
let
|
|
1933
|
+
let streamingGeneration = 0;
|
|
1934
|
+
let activeStreamingGeneration;
|
|
1935
|
+
let inFlightStreamingClose;
|
|
1604
1936
|
let visibleReplySent = false;
|
|
1605
1937
|
let skippedFinalReason = null;
|
|
1606
1938
|
let idleSideEffectsPromise = Promise.resolve();
|
|
1939
|
+
let activeIdleSideEffectsPromise = null;
|
|
1940
|
+
let idleRequestedForReply = false;
|
|
1607
1941
|
let replyLifecycleStateInitialized = false;
|
|
1942
|
+
const pendingStreamingDeliveries = [];
|
|
1608
1943
|
const markVisibleReplySent = () => {
|
|
1609
1944
|
visibleReplySent = true;
|
|
1610
1945
|
};
|
|
@@ -1621,9 +1956,12 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1621
1956
|
return parts.join("");
|
|
1622
1957
|
};
|
|
1623
1958
|
const flushStreamingCardUpdate = (combined) => {
|
|
1959
|
+
const session = streaming;
|
|
1960
|
+
const generation = activeStreamingGeneration;
|
|
1961
|
+
const startPromise = streamingStartPromise;
|
|
1624
1962
|
partialUpdateQueue = partialUpdateQueue.then(async () => {
|
|
1625
|
-
if (
|
|
1626
|
-
if (
|
|
1963
|
+
if (startPromise) await startPromise;
|
|
1964
|
+
if (generation !== void 0 && session?.isActive()) await session.update(combined);
|
|
1627
1965
|
});
|
|
1628
1966
|
};
|
|
1629
1967
|
const queueStreamingUpdate = (nextText, options) => {
|
|
@@ -1656,12 +1994,15 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1656
1994
|
httpTimeoutMs: resolveConfiguredHttpTimeoutMs(account)
|
|
1657
1995
|
} : null;
|
|
1658
1996
|
if (!creds) return;
|
|
1659
|
-
|
|
1997
|
+
const session = new FeishuStreamingSession(createFeishuClient(account), creds, (message) => params.runtime.log?.(`feishu[${account.accountId}] ${message}`));
|
|
1998
|
+
const generation = ++streamingGeneration;
|
|
1999
|
+
streaming = session;
|
|
2000
|
+
activeStreamingGeneration = generation;
|
|
1660
2001
|
try {
|
|
1661
2002
|
const cardHeader = resolveCardHeader(agentId, identity);
|
|
1662
2003
|
const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
|
|
1663
2004
|
const streamingTarget = sendTarget.replace(/^(feishu|lark):/i, "").replace(/^(chat|user|group|dm|open_id):/i, "").trim();
|
|
1664
|
-
await
|
|
2005
|
+
await session.start(streamingTarget, resolveReceiveIdType(sendTarget), {
|
|
1665
2006
|
replyToMessageId: sendReplyToMessageId,
|
|
1666
2007
|
replyInThread: effectiveReplyInThread,
|
|
1667
2008
|
rootId,
|
|
@@ -1672,14 +2013,18 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1672
2013
|
} catch (error) {
|
|
1673
2014
|
rememberStreamingStartFailure(account.accountId);
|
|
1674
2015
|
params.runtime.error?.(`feishu[${account.accountId}]: streaming start failed; using non-streaming card fallback for ${STREAMING_START_FAILURE_BACKOFF_MS / 1e3}s: ${String(error)}`);
|
|
1675
|
-
streaming
|
|
1676
|
-
|
|
2016
|
+
if (streaming === session) {
|
|
2017
|
+
streaming = null;
|
|
2018
|
+
streamingStartPromise = null;
|
|
2019
|
+
activeStreamingGeneration = void 0;
|
|
2020
|
+
}
|
|
1677
2021
|
}
|
|
1678
2022
|
})();
|
|
1679
2023
|
};
|
|
1680
2024
|
const resetStreamingState = () => {
|
|
1681
2025
|
streaming = null;
|
|
1682
2026
|
streamingStartPromise = null;
|
|
2027
|
+
activeStreamingGeneration = void 0;
|
|
1683
2028
|
partialUpdateQueue = Promise.resolve();
|
|
1684
2029
|
streamText = "";
|
|
1685
2030
|
lastPartial = "";
|
|
@@ -1689,24 +2034,126 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1689
2034
|
lastSnapshotTextLength = 0;
|
|
1690
2035
|
hasStreamingFinalText = false;
|
|
1691
2036
|
};
|
|
1692
|
-
const
|
|
2037
|
+
const rememberClosedStreamingSettlement = (generation, content, result, error) => {
|
|
2038
|
+
if (generation === void 0 || !content) return;
|
|
2039
|
+
closedStreamingSettlements.set(generation, {
|
|
2040
|
+
result,
|
|
2041
|
+
content,
|
|
2042
|
+
...error === void 0 ? {} : { error }
|
|
2043
|
+
});
|
|
2044
|
+
};
|
|
2045
|
+
const performStreamingClose = async () => {
|
|
2046
|
+
const streamingToClose = streaming;
|
|
2047
|
+
const generationToClose = activeStreamingGeneration;
|
|
2048
|
+
const startPromiseToClose = streamingStartPromise;
|
|
2049
|
+
const updateQueueToClose = partialUpdateQueue;
|
|
2050
|
+
const finalizedAnswerText = streamText;
|
|
2051
|
+
const finalizedReasoningText = reasoningText;
|
|
2052
|
+
if (generationToClose !== void 0 && activeStreamingGeneration === generationToClose) activeStreamingGeneration = void 0;
|
|
1693
2053
|
try {
|
|
1694
|
-
if (
|
|
1695
|
-
await
|
|
1696
|
-
if (
|
|
2054
|
+
if (startPromiseToClose) await startPromiseToClose;
|
|
2055
|
+
await updateQueueToClose;
|
|
2056
|
+
if (streamingToClose?.isActive()) {
|
|
1697
2057
|
statusLine = "";
|
|
1698
|
-
const text = buildCombinedStreamText(
|
|
2058
|
+
const text = buildCombinedStreamText(finalizedReasoningText, finalizedAnswerText);
|
|
1699
2059
|
const finalNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
if (
|
|
2060
|
+
let closed;
|
|
2061
|
+
try {
|
|
2062
|
+
closed = await streamingToClose.closeWithResult(text, { note: finalNote });
|
|
2063
|
+
} catch (error) {
|
|
2064
|
+
if (!(error instanceof FeishuStreamingFinalizationError)) throw error;
|
|
2065
|
+
const failedResult = createFeishuReplyDeliveryResult({
|
|
2066
|
+
results: [error.result],
|
|
2067
|
+
visibleReplySent: error.result.visibleReplySent,
|
|
2068
|
+
content: error.result.content,
|
|
2069
|
+
kind: "card"
|
|
2070
|
+
});
|
|
2071
|
+
if (failedResult.visibleReplySent) markVisibleReplySent();
|
|
2072
|
+
if (failedResult.visibleReplySent && finalizedAnswerText) {
|
|
2073
|
+
const partialError = createFeishuPartialReplyDeliveryError(error.cause ?? error, failedResult);
|
|
2074
|
+
if (failedResult.content === text) deliveredFinalTexts.add(finalizedAnswerText);
|
|
2075
|
+
rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, failedResult, partialError);
|
|
2076
|
+
return {
|
|
2077
|
+
result: failedResult,
|
|
2078
|
+
...generationToClose === void 0 ? {} : { generation: generationToClose },
|
|
2079
|
+
error: partialError
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, failedResult, error);
|
|
2083
|
+
return {
|
|
2084
|
+
result: failedResult,
|
|
2085
|
+
...generationToClose === void 0 ? {} : { generation: generationToClose },
|
|
2086
|
+
error
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
const result = createFeishuReplyDeliveryResult({
|
|
2090
|
+
results: [closed],
|
|
2091
|
+
visibleReplySent: closed.visibleReplySent,
|
|
2092
|
+
content: closed.content,
|
|
2093
|
+
kind: "card"
|
|
2094
|
+
});
|
|
2095
|
+
if (result.visibleReplySent) markVisibleReplySent();
|
|
2096
|
+
if (result.visibleReplySent && finalizedAnswerText) {
|
|
2097
|
+
deliveredFinalTexts.add(finalizedAnswerText);
|
|
2098
|
+
rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, result);
|
|
1705
2099
|
}
|
|
2100
|
+
return {
|
|
2101
|
+
result,
|
|
2102
|
+
...generationToClose === void 0 ? {} : { generation: generationToClose }
|
|
2103
|
+
};
|
|
1706
2104
|
}
|
|
2105
|
+
return {
|
|
2106
|
+
result: noVisibleFeishuReplyDelivery,
|
|
2107
|
+
...generationToClose === void 0 ? {} : { generation: generationToClose }
|
|
2108
|
+
};
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
return {
|
|
2111
|
+
result: noVisibleFeishuReplyDelivery,
|
|
2112
|
+
...generationToClose === void 0 ? {} : { generation: generationToClose },
|
|
2113
|
+
error
|
|
2114
|
+
};
|
|
1707
2115
|
} finally {
|
|
1708
|
-
resetStreamingState();
|
|
2116
|
+
if (streaming === streamingToClose) resetStreamingState();
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
const closeStreaming = () => {
|
|
2120
|
+
const generation = activeStreamingGeneration;
|
|
2121
|
+
if (generation !== void 0 && inFlightStreamingClose?.generation === generation) return inFlightStreamingClose.promise;
|
|
2122
|
+
const content = streamText;
|
|
2123
|
+
const closePromise = performStreamingClose();
|
|
2124
|
+
if (generation !== void 0) {
|
|
2125
|
+
const closing = {
|
|
2126
|
+
generation,
|
|
2127
|
+
content,
|
|
2128
|
+
promise: closePromise
|
|
2129
|
+
};
|
|
2130
|
+
inFlightStreamingClose = closing;
|
|
2131
|
+
const clear = () => {
|
|
2132
|
+
if (inFlightStreamingClose === closing) inFlightStreamingClose = void 0;
|
|
2133
|
+
};
|
|
2134
|
+
closePromise.then(clear, clear);
|
|
1709
2135
|
}
|
|
2136
|
+
return closePromise;
|
|
2137
|
+
};
|
|
2138
|
+
const deferStreamingDelivery = (result, infoKind, ownerGeneration) => {
|
|
2139
|
+
let resolveFinalization;
|
|
2140
|
+
let rejectFinalization;
|
|
2141
|
+
const finalization = new Promise((resolve, reject) => {
|
|
2142
|
+
resolveFinalization = resolve;
|
|
2143
|
+
rejectFinalization = reject;
|
|
2144
|
+
});
|
|
2145
|
+
pendingStreamingDeliveries.push({
|
|
2146
|
+
result,
|
|
2147
|
+
...infoKind ? { infoKind } : {},
|
|
2148
|
+
...ownerGeneration === void 0 ? {} : { streamingGeneration: ownerGeneration },
|
|
2149
|
+
resolve: resolveFinalization,
|
|
2150
|
+
reject: rejectFinalization
|
|
2151
|
+
});
|
|
2152
|
+
if (idleRequestedForReply) queueIdleSideEffects().catch((error) => params.runtime.error?.(`feishu[${account.accountId}] late reply finalization failed: ${String(error)}`));
|
|
2153
|
+
return {
|
|
2154
|
+
...noVisibleFeishuReplyDelivery,
|
|
2155
|
+
finalization
|
|
2156
|
+
};
|
|
1710
2157
|
};
|
|
1711
2158
|
const discardStreamingPreview = async () => {
|
|
1712
2159
|
try {
|
|
@@ -1731,75 +2178,101 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1731
2178
|
limit: textChunkLimit,
|
|
1732
2179
|
mode: chunkMode,
|
|
1733
2180
|
firstChunkMentions: paramsLocal.firstChunkMentions,
|
|
2181
|
+
chunkMentions: paramsLocal.chunkMentions,
|
|
1734
2182
|
initialChunks
|
|
1735
2183
|
}));
|
|
2184
|
+
const results = [];
|
|
2185
|
+
const acceptedChunks = [];
|
|
1736
2186
|
for (const [index, chunk] of chunks.entries()) {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
2187
|
+
const mentions = [...paramsLocal.chunkMentions ?? [], ...index === 0 ? paramsLocal.firstChunkMentions ?? [] : []];
|
|
2188
|
+
try {
|
|
2189
|
+
const result = await paramsLocal.sendChunk({
|
|
2190
|
+
chunk,
|
|
2191
|
+
isFirst: index === 0,
|
|
2192
|
+
mentions: mentions.length > 0 ? mentions : void 0
|
|
2193
|
+
});
|
|
2194
|
+
results.push(result);
|
|
2195
|
+
acceptedChunks.push(chunk);
|
|
2196
|
+
markVisibleReplySent();
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
|
|
2199
|
+
results,
|
|
2200
|
+
visibleReplySent: results.length > 0,
|
|
2201
|
+
content: acceptedChunks.join(""),
|
|
2202
|
+
kind: paramsLocal.useCard ? "card" : "text"
|
|
2203
|
+
}));
|
|
2204
|
+
}
|
|
1742
2205
|
}
|
|
1743
2206
|
if (paramsLocal.infoKind === "final") deliveredFinalTexts.add(paramsLocal.text);
|
|
2207
|
+
return createFeishuReplyDeliveryResult({
|
|
2208
|
+
results,
|
|
2209
|
+
visibleReplySent: results.length > 0,
|
|
2210
|
+
content: paramsLocal.text,
|
|
2211
|
+
kind: paramsLocal.useCard ? "card" : "text"
|
|
2212
|
+
});
|
|
1744
2213
|
};
|
|
1745
2214
|
const sendMediaReplies = async (payload, options) => {
|
|
1746
2215
|
const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
|
|
1747
2216
|
let sentFallbackText = false;
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2217
|
+
let degradedVoiceFallbackText;
|
|
2218
|
+
const results = [];
|
|
2219
|
+
const sendFallbackText = async (text) => await sendChunkedTextReply({
|
|
2220
|
+
text,
|
|
2221
|
+
useCard: false,
|
|
2222
|
+
infoKind: "final",
|
|
2223
|
+
chunkMentions: requiredMentionTargets,
|
|
2224
|
+
sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
|
|
2225
|
+
cfg,
|
|
2226
|
+
to: sendTarget,
|
|
2227
|
+
text: chunk,
|
|
2228
|
+
replyToMessageId: sendReplyToMessageId,
|
|
2229
|
+
replyInThread: effectiveReplyInThread,
|
|
2230
|
+
allowTopLevelReplyFallback,
|
|
2231
|
+
accountId,
|
|
2232
|
+
...mentions ? { mentions } : {}
|
|
2233
|
+
})
|
|
2234
|
+
});
|
|
2235
|
+
try {
|
|
2236
|
+
await sendMediaWithLeadingCaption({
|
|
2237
|
+
mediaUrls,
|
|
2238
|
+
caption: "",
|
|
2239
|
+
send: async ({ mediaUrl }) => {
|
|
2240
|
+
const result = await sendMediaFeishu({
|
|
2241
|
+
cfg,
|
|
2242
|
+
to: sendTarget,
|
|
2243
|
+
mediaUrl,
|
|
2244
|
+
replyToMessageId: sendReplyToMessageId,
|
|
2245
|
+
replyInThread: effectiveReplyInThread,
|
|
2246
|
+
allowTopLevelReplyFallback,
|
|
2247
|
+
accountId,
|
|
2248
|
+
...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
|
|
2249
|
+
});
|
|
2250
|
+
results.push(createFeishuReplyDeliveryResult({
|
|
2251
|
+
results: [result],
|
|
2252
|
+
visibleReplySent: true,
|
|
2253
|
+
kind: result?.voiceIntentDegradedToFile ? "media" : void 0
|
|
2254
|
+
}));
|
|
2255
|
+
markVisibleReplySent();
|
|
2256
|
+
if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) degradedVoiceFallbackText = options.fallbackText;
|
|
2257
|
+
},
|
|
2258
|
+
onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
|
|
2259
|
+
const fallbackText = await buildFeishuMediaFallbackText({
|
|
2260
|
+
text: sentFallbackText ? void 0 : options.fallbackText,
|
|
2261
|
+
mediaUrl
|
|
1779
2262
|
});
|
|
2263
|
+
sentFallbackText = true;
|
|
2264
|
+
results.push(await sendFallbackText(fallbackText));
|
|
1780
2265
|
}
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
const fallbackText = formatMediaFallbackText(sentFallbackText ? void 0 : options.fallbackText, mediaUrl);
|
|
2266
|
+
});
|
|
2267
|
+
if (degradedVoiceFallbackText && !sentFallbackText) {
|
|
1784
2268
|
sentFallbackText = true;
|
|
1785
|
-
await
|
|
1786
|
-
text: fallbackText,
|
|
1787
|
-
useCard: false,
|
|
1788
|
-
infoKind: "final",
|
|
1789
|
-
sendChunk: async ({ chunk }) => {
|
|
1790
|
-
await sendMessageFeishu({
|
|
1791
|
-
cfg,
|
|
1792
|
-
to: sendTarget,
|
|
1793
|
-
text: chunk,
|
|
1794
|
-
replyToMessageId: sendReplyToMessageId,
|
|
1795
|
-
replyInThread: effectiveReplyInThread,
|
|
1796
|
-
allowTopLevelReplyFallback,
|
|
1797
|
-
accountId
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
});
|
|
2269
|
+
results.push(await sendFallbackText(degradedVoiceFallbackText));
|
|
1801
2270
|
}
|
|
1802
|
-
})
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
|
|
2273
|
+
throw createFeishuPartialReplyDeliveryError(error, mergeFeishuReplyDeliveryResults([...results, ...partial ? [partial] : []]));
|
|
2274
|
+
}
|
|
2275
|
+
return mergeFeishuReplyDeliveryResults(results);
|
|
1803
2276
|
};
|
|
1804
2277
|
const ensureNoVisibleReplyFallback = async (reason) => {
|
|
1805
2278
|
await idleSideEffectsPromise;
|
|
@@ -1815,24 +2288,142 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1815
2288
|
replyToMessageId: sendReplyToMessageId,
|
|
1816
2289
|
replyInThread: effectiveReplyInThread,
|
|
1817
2290
|
allowTopLevelReplyFallback,
|
|
1818
|
-
accountId
|
|
2291
|
+
accountId,
|
|
2292
|
+
...requiredMentionTargets?.length ? { mentions: requiredMentionTargets } : {}
|
|
1819
2293
|
});
|
|
1820
2294
|
markVisibleReplySent();
|
|
1821
2295
|
params.runtime.error?.(`feishu[${account.accountId}]: sent no-visible-reply fallback (${reason})`);
|
|
1822
2296
|
return true;
|
|
1823
2297
|
};
|
|
1824
|
-
const
|
|
2298
|
+
const claimClosedStreamingResult = (generation, content) => {
|
|
2299
|
+
if (generation !== void 0) {
|
|
2300
|
+
const settlement = closedStreamingSettlements.get(generation);
|
|
2301
|
+
if (settlement) settlement.contentClaimed = true;
|
|
2302
|
+
return settlement;
|
|
2303
|
+
}
|
|
2304
|
+
let latestKey;
|
|
2305
|
+
for (const [key, settlement] of closedStreamingSettlements) if (settlement.contentClaimed !== true && (content === void 0 || settlement.content === content)) latestKey = key;
|
|
2306
|
+
if (latestKey === void 0) return;
|
|
2307
|
+
const result = closedStreamingSettlements.get(latestKey);
|
|
2308
|
+
if (result) result.contentClaimed = true;
|
|
2309
|
+
return result;
|
|
2310
|
+
};
|
|
2311
|
+
const markClosedStreamingContentClaimed = (generation) => {
|
|
2312
|
+
if (generation !== void 0) {
|
|
2313
|
+
const settlement = closedStreamingSettlements.get(generation);
|
|
2314
|
+
if (settlement) settlement.contentClaimed = true;
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
const ensureVisibleStreamingDelivery = async (result, content, infoKind) => {
|
|
2318
|
+
if (result?.visibleReplySent === true || !content?.trim()) return result;
|
|
2319
|
+
const cardHeader = resolveCardHeader(agentId, identity);
|
|
2320
|
+
const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
|
|
2321
|
+
return await sendChunkedTextReply({
|
|
2322
|
+
text: content,
|
|
2323
|
+
useCard: true,
|
|
2324
|
+
infoKind,
|
|
2325
|
+
chunkMentions: requiredMentionTargets,
|
|
2326
|
+
sendChunk: async ({ chunk, mentions }) => await sendStructuredCardFeishu({
|
|
2327
|
+
cfg,
|
|
2328
|
+
to: sendTarget,
|
|
2329
|
+
text: chunk,
|
|
2330
|
+
replyToMessageId: sendReplyToMessageId,
|
|
2331
|
+
replyInThread: effectiveReplyInThread,
|
|
2332
|
+
allowTopLevelReplyFallback,
|
|
2333
|
+
accountId,
|
|
2334
|
+
header: cardHeader,
|
|
2335
|
+
note: cardNote,
|
|
2336
|
+
...mentions ? { mentions } : {}
|
|
2337
|
+
})
|
|
2338
|
+
});
|
|
2339
|
+
};
|
|
2340
|
+
function queueIdleSideEffects() {
|
|
2341
|
+
idleRequestedForReply = true;
|
|
2342
|
+
if (activeIdleSideEffectsPromise) return activeIdleSideEffectsPromise;
|
|
1825
2343
|
const nextIdleSideEffects = idleSideEffectsPromise.then(async () => {
|
|
1826
|
-
|
|
1827
|
-
|
|
2344
|
+
try {
|
|
2345
|
+
do {
|
|
2346
|
+
const completions = pendingStreamingDeliveries.splice(0);
|
|
2347
|
+
const closeOutcome = await closeStreaming();
|
|
2348
|
+
const finalized = closeOutcome.result;
|
|
2349
|
+
const ownsCurrentClose = (completion) => closeOutcome.generation !== void 0 && completion.streamingGeneration === closeOutcome.generation;
|
|
2350
|
+
if (completions.some((completion) => ownsCurrentClose(completion))) markClosedStreamingContentClaimed(closeOutcome.generation);
|
|
2351
|
+
for (const completion of completions) {
|
|
2352
|
+
const claimedSettlement = ownsCurrentClose(completion) ? {
|
|
2353
|
+
result: finalized,
|
|
2354
|
+
...closeOutcome.error === void 0 ? {} : { error: closeOutcome.error }
|
|
2355
|
+
} : claimClosedStreamingResult(completion.streamingGeneration, completion.result.content);
|
|
2356
|
+
const deliveryError = claimedSettlement?.error;
|
|
2357
|
+
let providerFinalized = claimedSettlement?.result;
|
|
2358
|
+
try {
|
|
2359
|
+
providerFinalized = await ensureVisibleStreamingDelivery(providerFinalized, completion.result.content, completion.infoKind);
|
|
2360
|
+
} catch (fallbackError) {
|
|
2361
|
+
const fallbackPartial = isChannelPartialDeliveryError(fallbackError) ? fallbackError.deliveryResult : void 0;
|
|
2362
|
+
const fallbackCause = fallbackPartial && fallbackError instanceof Error ? fallbackError.cause ?? fallbackError : fallbackError;
|
|
2363
|
+
completion.reject(createFeishuPartialReplyDeliveryError(deliveryError === void 0 ? fallbackCause : new AggregateError([deliveryError, fallbackCause], "Feishu streaming finalization and static fallback failed"), mergeFeishuReplyDeliveryResults([
|
|
2364
|
+
...providerFinalized ? [providerFinalized] : [],
|
|
2365
|
+
...fallbackPartial ? [fallbackPartial] : [],
|
|
2366
|
+
completion.result
|
|
2367
|
+
], fallbackPartial?.content ?? providerFinalized?.content ?? completion.result.content)));
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
const settledResult = mergeFeishuReplyDeliveryResults([...providerFinalized ? [providerFinalized] : [], completion.result], deliveryError === void 0 ? completion.result.content ?? providerFinalized?.content : providerFinalized?.content ?? completion.result.content);
|
|
2371
|
+
if (deliveryError !== void 0) completion.reject(createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(deliveryError) && deliveryError instanceof Error ? deliveryError.cause ?? deliveryError : deliveryError instanceof FeishuStreamingFinalizationError ? deliveryError.cause ?? deliveryError : deliveryError, settledResult));
|
|
2372
|
+
else completion.resolve(settledResult);
|
|
2373
|
+
}
|
|
2374
|
+
if (closeOutcome.error !== void 0) throw toError(closeOutcome.error);
|
|
2375
|
+
} while (pendingStreamingDeliveries.length > 0);
|
|
2376
|
+
} finally {
|
|
2377
|
+
typingCallbacks?.onIdle?.();
|
|
2378
|
+
}
|
|
1828
2379
|
});
|
|
2380
|
+
activeIdleSideEffectsPromise = nextIdleSideEffects;
|
|
1829
2381
|
idleSideEffectsPromise = nextIdleSideEffects.catch(() => {});
|
|
2382
|
+
const finishIdleSideEffects = () => {
|
|
2383
|
+
if (activeIdleSideEffectsPromise === nextIdleSideEffects) activeIdleSideEffectsPromise = null;
|
|
2384
|
+
if (pendingStreamingDeliveries.length > 0) queueIdleSideEffects().catch((error) => params.runtime.error?.(`feishu[${account.accountId}] queued reply finalization failed: ${String(error)}`));
|
|
2385
|
+
};
|
|
2386
|
+
nextIdleSideEffects.then(finishIdleSideEffects, finishIdleSideEffects);
|
|
1830
2387
|
return nextIdleSideEffects;
|
|
2388
|
+
}
|
|
2389
|
+
const throwStreamingDeliveryFailure = async (paramsLocal) => {
|
|
2390
|
+
let finalized = noVisibleFeishuReplyDelivery;
|
|
2391
|
+
let finalizationError;
|
|
2392
|
+
let fallbackPartial;
|
|
2393
|
+
const claimedSettlement = claimClosedStreamingResult(paramsLocal.ownerGeneration, paramsLocal.content);
|
|
2394
|
+
if (claimedSettlement) {
|
|
2395
|
+
finalized = claimedSettlement.result;
|
|
2396
|
+
finalizationError = claimedSettlement.error;
|
|
2397
|
+
} else if (paramsLocal.ownerGeneration !== void 0 && inFlightStreamingClose?.generation === paramsLocal.ownerGeneration) {
|
|
2398
|
+
const closeOutcome = await inFlightStreamingClose.promise;
|
|
2399
|
+
const completedSettlement = claimClosedStreamingResult(paramsLocal.ownerGeneration, paramsLocal.content);
|
|
2400
|
+
finalized = completedSettlement?.result ?? closeOutcome?.result ?? finalized;
|
|
2401
|
+
finalizationError = completedSettlement?.error ?? closeOutcome?.error;
|
|
2402
|
+
} else if (paramsLocal.ownerGeneration !== void 0 && activeStreamingGeneration === paramsLocal.ownerGeneration) {
|
|
2403
|
+
const closeOutcome = await closeStreaming();
|
|
2404
|
+
finalized = closeOutcome.result;
|
|
2405
|
+
finalizationError = closeOutcome.error;
|
|
2406
|
+
}
|
|
2407
|
+
try {
|
|
2408
|
+
finalized = await ensureVisibleStreamingDelivery(finalized, paramsLocal.content, paramsLocal.infoKind) ?? finalized;
|
|
2409
|
+
} catch (fallbackError) {
|
|
2410
|
+
fallbackPartial = isChannelPartialDeliveryError(fallbackError) ? fallbackError.deliveryResult : void 0;
|
|
2411
|
+
const fallbackCause = fallbackPartial && fallbackError instanceof Error ? fallbackError.cause ?? fallbackError : fallbackError;
|
|
2412
|
+
finalizationError = finalizationError ? new AggregateError([finalizationError, fallbackCause], "Feishu streaming finalization and static fallback failed") : fallbackCause;
|
|
2413
|
+
}
|
|
2414
|
+
const mediaPartial = isChannelPartialDeliveryError(paramsLocal.error) ? paramsLocal.error.deliveryResult : void 0;
|
|
2415
|
+
const accepted = mergeFeishuReplyDeliveryResults([
|
|
2416
|
+
finalized,
|
|
2417
|
+
...fallbackPartial ? [fallbackPartial] : [],
|
|
2418
|
+
...mediaPartial ? [mediaPartial] : []
|
|
2419
|
+
], fallbackPartial?.visibleReplySent === true ? fallbackPartial.content : finalized.visibleReplySent === true ? finalized.content : paramsLocal.content);
|
|
2420
|
+
const mediaCause = mediaPartial && paramsLocal.error instanceof Error ? paramsLocal.error.cause ?? paramsLocal.error : paramsLocal.error;
|
|
2421
|
+
throw createFeishuPartialReplyDeliveryError(finalizationError ? new AggregateError([mediaCause, finalizationError instanceof Error ? finalizationError.cause ?? finalizationError : finalizationError], "Feishu reply delivery and streaming finalization failed") : mediaCause, accepted);
|
|
1831
2422
|
};
|
|
1832
|
-
const
|
|
2423
|
+
const dispatcherOptions = {
|
|
1833
2424
|
responsePrefix: prefixContext.responsePrefix,
|
|
1834
2425
|
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
1835
|
-
humanDelay:
|
|
2426
|
+
humanDelay: resolveHumanDelayConfig(cfg, agentId),
|
|
1836
2427
|
silentReplyContext: {
|
|
1837
2428
|
cfg,
|
|
1838
2429
|
sessionKey: params.sessionKey,
|
|
@@ -1846,52 +2437,80 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1846
2437
|
if (!replyLifecycleStateInitialized) {
|
|
1847
2438
|
replyLifecycleStateInitialized = true;
|
|
1848
2439
|
deliveredFinalTexts.clear();
|
|
2440
|
+
closedStreamingSettlements.clear();
|
|
1849
2441
|
sentIndependentBlockText = false;
|
|
1850
|
-
|
|
1851
|
-
streamingCloseErroredForReply = false;
|
|
2442
|
+
idleRequestedForReply = false;
|
|
1852
2443
|
visibleReplySent = false;
|
|
1853
2444
|
skippedFinalReason = null;
|
|
1854
2445
|
}
|
|
1855
|
-
if (
|
|
2446
|
+
if (previewStreamingEnabled && renderMode === "card") startStreaming();
|
|
1856
2447
|
await Promise.resolve(typingCallbacks?.onReplyStart?.());
|
|
1857
2448
|
},
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2449
|
+
onIdle: () => queueIdleSideEffects(),
|
|
2450
|
+
onCleanup: () => {
|
|
2451
|
+
typingCallbacks?.onCleanup?.();
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
const handleDeliveryError = async (error, info) => {
|
|
2455
|
+
params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
|
|
2456
|
+
await queueIdleSideEffects().catch((cleanupError) => params.runtime.error?.(`feishu[${account.accountId}] reply error cleanup failed: ${String(cleanupError)}`));
|
|
2457
|
+
};
|
|
2458
|
+
return {
|
|
2459
|
+
dispatcherOptions,
|
|
2460
|
+
delivery: {
|
|
2461
|
+
observeMessageSent: true,
|
|
2462
|
+
deliver: async (payload, info) => {
|
|
2463
|
+
if (info?.kind === "final") skippedFinalReason = null;
|
|
2464
|
+
const payloadText = payload.isReasoning && payload.text ? formatReasoningMessage(payload.text) : payload.text;
|
|
2465
|
+
const reply = resolveSendableOutboundReplyParts({
|
|
2466
|
+
...payload,
|
|
2467
|
+
text: payloadText
|
|
2468
|
+
});
|
|
2469
|
+
const text = info?.kind === "final" ? mergeStreamingFinalText(streamText, reply.text, payload.isError === true && hasStreamingFinalText) : reply.text;
|
|
2470
|
+
const hasText = reply.hasText;
|
|
2471
|
+
const hasMedia = reply.hasMedia;
|
|
2472
|
+
const ttsSupplement = getReplyPayloadTtsSupplement(payload);
|
|
2473
|
+
const ttsTextAlreadyVisible = ttsSupplement?.visibleTextAlreadyDelivered === true;
|
|
2474
|
+
const hasVoiceMedia = hasMedia && reply.mediaUrls.some((mediaUrl) => shouldSuppressFeishuTextForVoiceMedia({
|
|
2475
|
+
mediaUrl,
|
|
2476
|
+
...payload.audioAsVoice === true ? { audioAsVoice: true } : {},
|
|
2477
|
+
ttsSupplement
|
|
2478
|
+
}));
|
|
2479
|
+
const finalTextExceedsStreamingLimit = info?.kind === "final" && hasText && text.length > textChunkLimit;
|
|
2480
|
+
const useStaticCard = hasText && (renderMode === "card" || info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw" || renderMode === "auto" && shouldUseCard(text));
|
|
2481
|
+
const useStreamingCard = hasText && streamingEnabled && !finalTextExceedsStreamingLimit && (info?.kind === "final" || useStaticCard);
|
|
2482
|
+
const useCard = useStaticCard || useStreamingCard;
|
|
2483
|
+
const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
|
|
2484
|
+
const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal;
|
|
2485
|
+
const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText && !ttsTextAlreadyVisible || skipTextForDuplicateFinal));
|
|
2486
|
+
const priorClosedStreamingSettlement = info?.kind === "final" && hasText && skipTextForDuplicateFinal ? claimClosedStreamingResult(void 0, text) : void 0;
|
|
2487
|
+
if (!shouldDeliverText && !hasMedia) {
|
|
2488
|
+
if (priorClosedStreamingSettlement?.error !== void 0) throw toError(priorClosedStreamingSettlement.error);
|
|
2489
|
+
return priorClosedStreamingSettlement?.result ?? noVisibleFeishuReplyDelivery;
|
|
2490
|
+
}
|
|
2491
|
+
const deliveredResults = priorClosedStreamingSettlement ? [priorClosedStreamingSettlement.result] : [];
|
|
2492
|
+
const collectMediaDelivery = async (mediaPayload, mediaOptions) => {
|
|
2493
|
+
try {
|
|
2494
|
+
deliveredResults.push(await sendMediaReplies(mediaPayload, mediaOptions));
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
|
|
2497
|
+
const accumulated = mergeFeishuReplyDeliveryResults([...deliveredResults, ...partial ? [partial] : []]);
|
|
2498
|
+
throw createFeishuPartialReplyDeliveryError(partial && error instanceof Error ? error.cause ?? error : error, accumulated);
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
if (shouldDiscardStreamingPreview) await discardStreamingPreview();
|
|
2502
|
+
if (shouldDeliverText) {
|
|
2503
|
+
if (info?.kind === "block") {
|
|
2504
|
+
if (!useStreamingCard) {
|
|
2505
|
+
if (coreBlockStreamingEnabled) {
|
|
2506
|
+
const firstChunkMentions = !sentIndependentBlockText && mentionTargets?.length ? mentionTargets : void 0;
|
|
2507
|
+
deliveredResults.push(await sendChunkedTextReply({
|
|
2508
|
+
text,
|
|
2509
|
+
useCard: false,
|
|
2510
|
+
infoKind: "block",
|
|
2511
|
+
firstChunkMentions,
|
|
2512
|
+
chunkMentions: requiredMentionTargets,
|
|
2513
|
+
sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
|
|
1895
2514
|
cfg,
|
|
1896
2515
|
to: sendTarget,
|
|
1897
2516
|
text: chunk,
|
|
@@ -1899,47 +2518,59 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1899
2518
|
replyInThread: effectiveReplyInThread,
|
|
1900
2519
|
allowTopLevelReplyFallback,
|
|
1901
2520
|
accountId,
|
|
1902
|
-
...
|
|
1903
|
-
})
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2521
|
+
...mentions ? { mentions } : {}
|
|
2522
|
+
})
|
|
2523
|
+
}));
|
|
2524
|
+
sentIndependentBlockText = true;
|
|
2525
|
+
if (hasMedia) await collectMediaDelivery(payload);
|
|
2526
|
+
}
|
|
2527
|
+
return mergeFeishuReplyDeliveryResults(deliveredResults, text);
|
|
1908
2528
|
}
|
|
1909
|
-
|
|
2529
|
+
startStreaming();
|
|
2530
|
+
if (streamingStartPromise) await streamingStartPromise;
|
|
1910
2531
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
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));
|
|
2532
|
+
if (info?.kind === "final" && useStreamingCard) {
|
|
2533
|
+
startStreaming();
|
|
2534
|
+
if (streamingStartPromise) await streamingStartPromise;
|
|
1930
2535
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2536
|
+
const shouldStreamText = info?.kind === "block" || info?.kind === "final";
|
|
2537
|
+
const matchingInFlightClose = info?.kind === "final" && inFlightStreamingClose?.content === text ? inFlightStreamingClose : void 0;
|
|
2538
|
+
const ownerGeneration = activeStreamingGeneration ?? matchingInFlightClose?.generation;
|
|
2539
|
+
if (shouldStreamText && ownerGeneration !== void 0 && (streaming?.isActive() || matchingInFlightClose !== void 0)) {
|
|
2540
|
+
if (activeStreamingGeneration !== void 0) {
|
|
2541
|
+
if (info?.kind === "block") queueStreamingUpdate(text, {
|
|
2542
|
+
mode: "delta",
|
|
2543
|
+
dedupeWithLastPartial: true
|
|
2544
|
+
});
|
|
2545
|
+
if (info?.kind === "final") {
|
|
2546
|
+
streamText = text;
|
|
2547
|
+
hasStreamingFinalText = true;
|
|
2548
|
+
snapshotBaseText = "";
|
|
2549
|
+
lastSnapshotTextLength = text.length;
|
|
2550
|
+
flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
if (hasMedia) try {
|
|
2554
|
+
await collectMediaDelivery(payload);
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
await throwStreamingDeliveryFailure({
|
|
2557
|
+
error,
|
|
2558
|
+
content: text,
|
|
2559
|
+
infoKind: info?.kind,
|
|
2560
|
+
ownerGeneration
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
return deferStreamingDelivery(mergeFeishuReplyDeliveryResults(deliveredResults, text), info?.kind, ownerGeneration);
|
|
2564
|
+
}
|
|
2565
|
+
if (useCard) {
|
|
2566
|
+
const cardHeader = resolveCardHeader(agentId, identity);
|
|
2567
|
+
const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
|
|
2568
|
+
deliveredResults.push(await sendChunkedTextReply({
|
|
2569
|
+
text,
|
|
2570
|
+
useCard: true,
|
|
2571
|
+
infoKind: info?.kind,
|
|
2572
|
+
chunkMentions: requiredMentionTargets,
|
|
2573
|
+
sendChunk: async ({ chunk, mentions }) => await sendStructuredCardFeishu({
|
|
1943
2574
|
cfg,
|
|
1944
2575
|
to: sendTarget,
|
|
1945
2576
|
text: chunk,
|
|
@@ -1948,19 +2579,19 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1948
2579
|
allowTopLevelReplyFallback,
|
|
1949
2580
|
accountId,
|
|
1950
2581
|
header: cardHeader,
|
|
1951
|
-
note: cardNote
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
await sendMessageFeishu({
|
|
2582
|
+
note: cardNote,
|
|
2583
|
+
...mentions ? { mentions } : {}
|
|
2584
|
+
})
|
|
2585
|
+
}));
|
|
2586
|
+
} else {
|
|
2587
|
+
const firstChunkMentions = info?.kind === "final" && mentionTargets?.length ? mentionTargets : void 0;
|
|
2588
|
+
deliveredResults.push(await sendChunkedTextReply({
|
|
2589
|
+
text,
|
|
2590
|
+
useCard: false,
|
|
2591
|
+
infoKind: info?.kind,
|
|
2592
|
+
firstChunkMentions,
|
|
2593
|
+
chunkMentions: requiredMentionTargets,
|
|
2594
|
+
sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
|
|
1964
2595
|
cfg,
|
|
1965
2596
|
to: sendTarget,
|
|
1966
2597
|
text: chunk,
|
|
@@ -1968,32 +2599,22 @@ function createFeishuReplyDispatcher(params) {
|
|
|
1968
2599
|
replyInThread: effectiveReplyInThread,
|
|
1969
2600
|
allowTopLevelReplyFallback,
|
|
1970
2601
|
accountId,
|
|
1971
|
-
...
|
|
1972
|
-
})
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
2602
|
+
...mentions ? { mentions } : {}
|
|
2603
|
+
})
|
|
2604
|
+
}));
|
|
2605
|
+
}
|
|
1975
2606
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
|
|
1983
|
-
await queueIdleSideEffects({ markClosedForReply: false });
|
|
2607
|
+
if (hasMedia) await collectMediaDelivery(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
|
|
2608
|
+
const result = mergeFeishuReplyDeliveryResults(deliveredResults, text);
|
|
2609
|
+
if (priorClosedStreamingSettlement?.error !== void 0) throw createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(priorClosedStreamingSettlement.error) && priorClosedStreamingSettlement.error instanceof Error ? priorClosedStreamingSettlement.error.cause ?? priorClosedStreamingSettlement.error : priorClosedStreamingSettlement.error, result);
|
|
2610
|
+
return result;
|
|
2611
|
+
},
|
|
2612
|
+
onError: handleDeliveryError
|
|
1984
2613
|
},
|
|
1985
|
-
onIdle: () => queueIdleSideEffects(),
|
|
1986
|
-
onCleanup: () => {
|
|
1987
|
-
typingCallbacks?.onCleanup?.();
|
|
1988
|
-
}
|
|
1989
|
-
});
|
|
1990
|
-
return {
|
|
1991
|
-
dispatcher,
|
|
1992
2614
|
replyOptions: {
|
|
1993
|
-
...replyOptions,
|
|
1994
2615
|
onModelSelected: prefixContext.onModelSelected,
|
|
1995
2616
|
disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : true,
|
|
1996
|
-
onPartialReply:
|
|
2617
|
+
onPartialReply: previewStreamingEnabled ? (payload) => {
|
|
1997
2618
|
if (!payload.text) return;
|
|
1998
2619
|
const cleaned = stripReasoningTagsFromText(payload.text, {
|
|
1999
2620
|
mode: "strict",
|
|
@@ -2012,7 +2633,7 @@ function createFeishuReplyDispatcher(params) {
|
|
|
2012
2633
|
queueReasoningUpdate(formatReasoningMessage(payload.text));
|
|
2013
2634
|
} : void 0,
|
|
2014
2635
|
onReasoningEnd: reasoningPreviewEnabled ? () => {} : void 0,
|
|
2015
|
-
onToolStart:
|
|
2636
|
+
onToolStart: previewStreamingEnabled ? (payload) => {
|
|
2016
2637
|
if (!isChannelProgressDraftWorkToolName(payload.name)) return;
|
|
2017
2638
|
const statusLineLocal = formatChannelProgressDraftLineForEntry(account.config, {
|
|
2018
2639
|
event: "tool",
|
|
@@ -2022,17 +2643,16 @@ function createFeishuReplyDispatcher(params) {
|
|
|
2022
2643
|
}, { detailMode: payload.detailMode });
|
|
2023
2644
|
if (statusLineLocal) updateStreamingStatusLine(statusLineLocal);
|
|
2024
2645
|
} : void 0,
|
|
2025
|
-
onAssistantMessageStart:
|
|
2646
|
+
onAssistantMessageStart: previewStreamingEnabled ? () => {
|
|
2026
2647
|
updateStreamingStatusLine("", { startIfNeeded: false });
|
|
2027
2648
|
} : void 0,
|
|
2028
|
-
onCompactionStart:
|
|
2649
|
+
onCompactionStart: previewStreamingEnabled ? () => {
|
|
2029
2650
|
updateStreamingStatusLine("📦 **Compacting context...**");
|
|
2030
2651
|
} : void 0,
|
|
2031
|
-
onCompactionEnd:
|
|
2652
|
+
onCompactionEnd: previewStreamingEnabled ? () => {
|
|
2032
2653
|
updateStreamingStatusLine("");
|
|
2033
2654
|
} : void 0
|
|
2034
2655
|
},
|
|
2035
|
-
markDispatchIdle,
|
|
2036
2656
|
ensureNoVisibleReplyFallback,
|
|
2037
2657
|
getVisibleReplyState: () => ({
|
|
2038
2658
|
visibleReplySent,
|
|
@@ -2041,6 +2661,17 @@ function createFeishuReplyDispatcher(params) {
|
|
|
2041
2661
|
};
|
|
2042
2662
|
}
|
|
2043
2663
|
//#endregion
|
|
2664
|
+
//#region extensions/feishu/src/synthetic-event-target.ts
|
|
2665
|
+
const directPreDispatchTargets = /* @__PURE__ */ new WeakMap();
|
|
2666
|
+
/** Keep synthetic-only routing metadata outside the public Feishu event shape. */
|
|
2667
|
+
function setFeishuSyntheticDirectPreDispatchTarget(event, target) {
|
|
2668
|
+
directPreDispatchTargets.set(event, target);
|
|
2669
|
+
return event;
|
|
2670
|
+
}
|
|
2671
|
+
function getFeishuSyntheticDirectPreDispatchTarget(event) {
|
|
2672
|
+
return directPreDispatchTargets.get(event);
|
|
2673
|
+
}
|
|
2674
|
+
//#endregion
|
|
2044
2675
|
//#region extensions/feishu/src/bot.ts
|
|
2045
2676
|
const permissionErrorNotifiedAt = /* @__PURE__ */ new Map();
|
|
2046
2677
|
const PERMISSION_ERROR_COOLDOWN_MS = 300 * 1e3;
|
|
@@ -2055,15 +2686,14 @@ function isFeishuTopicSessionScope(scope) {
|
|
|
2055
2686
|
return scope === "group_topic" || scope === "group_topic_sender";
|
|
2056
2687
|
}
|
|
2057
2688
|
async function resolveFeishuAudioPreflightTranscript(params) {
|
|
2058
|
-
if (params.
|
|
2059
|
-
const audioMedia = params.mediaList.filter((media) => media.contentType?.startsWith("audio/"));
|
|
2689
|
+
if (params.messageType !== "audio" || params.content.trim()) return;
|
|
2690
|
+
const audioMedia = params.mediaList.filter((media) => Boolean(media.path) && (media.kind === "audio" || media.contentType?.startsWith("audio/")));
|
|
2060
2691
|
if (audioMedia.length === 0) return;
|
|
2061
2692
|
try {
|
|
2062
2693
|
const { transcribeFirstAudio } = await import("./audio-preflight.runtime-BBfC9elv.js");
|
|
2063
2694
|
return await transcribeFirstAudio({
|
|
2064
2695
|
ctx: {
|
|
2065
|
-
|
|
2066
|
-
MediaTypes: audioMedia.map((media) => media.contentType).filter(Boolean),
|
|
2696
|
+
media: audioMedia,
|
|
2067
2697
|
ChatType: params.chatType
|
|
2068
2698
|
},
|
|
2069
2699
|
cfg: params.cfg
|
|
@@ -2074,8 +2704,7 @@ async function resolveFeishuAudioPreflightTranscript(params) {
|
|
|
2074
2704
|
}
|
|
2075
2705
|
}
|
|
2076
2706
|
/**
|
|
2077
|
-
*
|
|
2078
|
-
* Similar to Discord's buildDiscordMediaPayload().
|
|
2707
|
+
* Parse an inbound Feishu event into its caption and routing metadata.
|
|
2079
2708
|
*/
|
|
2080
2709
|
function parseFeishuMessageEvent(event, botOpenId, _botName) {
|
|
2081
2710
|
const rawContent = parseMessageContent(event.message.content, event.message.message_type);
|
|
@@ -2093,6 +2722,7 @@ function parseFeishuMessageEvent(event, botOpenId, _botName) {
|
|
|
2093
2722
|
suppressReplyTarget: event.message.suppress_reply_target === true,
|
|
2094
2723
|
senderId: senderUserId || senderOpenId || "",
|
|
2095
2724
|
senderOpenId: senderFallbackId,
|
|
2725
|
+
senderType: event.sender.sender_type === "bot" ? "bot" : "user",
|
|
2096
2726
|
chatType: event.message.chat_type,
|
|
2097
2727
|
mentionedBot,
|
|
2098
2728
|
hasAnyMention,
|
|
@@ -2142,7 +2772,7 @@ async function filterFetchedGroupContextMessages(messages, params) {
|
|
|
2142
2772
|
}) ? message : void 0))).filter((message) => message !== void 0);
|
|
2143
2773
|
}
|
|
2144
2774
|
async function handleFeishuMessage(params) {
|
|
2145
|
-
const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride } = params;
|
|
2775
|
+
const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride, turnAdoptionLifecycle } = params;
|
|
2146
2776
|
const account = resolveFeishuRuntimeAccount({
|
|
2147
2777
|
cfg,
|
|
2148
2778
|
accountId
|
|
@@ -2152,7 +2782,7 @@ async function handleFeishuMessage(params) {
|
|
|
2152
2782
|
const error = runtime?.error ?? console.error;
|
|
2153
2783
|
const messageId = event.message.message_id;
|
|
2154
2784
|
const messageDedupeKey = messageDedupeKeyOverride ?? resolveFeishuMessageDedupeKey(event);
|
|
2155
|
-
if (!await finalizeFeishuMessageProcessing({
|
|
2785
|
+
if (!turnAdoptionLifecycle && !await finalizeFeishuMessageProcessing({
|
|
2156
2786
|
messageId: messageDedupeKey,
|
|
2157
2787
|
namespace: account.accountId,
|
|
2158
2788
|
log,
|
|
@@ -2164,7 +2794,62 @@ async function handleFeishuMessage(params) {
|
|
|
2164
2794
|
let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
|
|
2165
2795
|
const isGroup = isFeishuGroupChatType(ctx.chatType);
|
|
2166
2796
|
const isDirect = !isGroup;
|
|
2797
|
+
const directPreDispatchTarget = isDirect ? getFeishuSyntheticDirectPreDispatchTarget(event) : void 0;
|
|
2167
2798
|
const senderUserId = normalizeOptionalString(event.sender.sender_id.user_id);
|
|
2799
|
+
const localBotOpenId = botOpenId?.trim();
|
|
2800
|
+
if (ctx.senderType === "bot") {
|
|
2801
|
+
if (!localBotOpenId) {
|
|
2802
|
+
log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (local bot identity unavailable)`);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
if (ctx.senderOpenId === localBotOpenId) {
|
|
2806
|
+
log(`feishu[${account.accountId}]: dropping self-authored bot message ${ctx.messageId}`);
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
if (feishuCfg?.allowBots !== true) {
|
|
2810
|
+
log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (allowBots=false)`);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
if (isGroup && !ctx.mentionedBot) {
|
|
2814
|
+
let verifiedEvent;
|
|
2815
|
+
try {
|
|
2816
|
+
const response = await createFeishuClient(account).im.message.get({
|
|
2817
|
+
params: { user_id_type: "open_id" },
|
|
2818
|
+
path: { message_id: ctx.messageId }
|
|
2819
|
+
});
|
|
2820
|
+
const verifiedKey = (response.data?.items?.[0]?.mentions?.find((mention) => mention.id_type === "open_id" && mention.id === localBotOpenId && Boolean(mention.key?.trim())))?.key?.trim();
|
|
2821
|
+
if (response.code === 0 && verifiedKey) {
|
|
2822
|
+
const eventMention = event.message.mentions?.find((mention) => mention.key === verifiedKey && !isFeishuBroadcastMention(mention));
|
|
2823
|
+
if (eventMention) verifiedEvent = {
|
|
2824
|
+
...event,
|
|
2825
|
+
message: {
|
|
2826
|
+
...event.message,
|
|
2827
|
+
mentions: event.message.mentions?.map((mention) => mention === eventMention ? {
|
|
2828
|
+
...mention,
|
|
2829
|
+
id: {
|
|
2830
|
+
...mention.id,
|
|
2831
|
+
open_id: localBotOpenId
|
|
2832
|
+
}
|
|
2833
|
+
} : mention)
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
} catch (err) {
|
|
2838
|
+
log(`feishu[${account.accountId}]: failed to verify bot mention for ${ctx.messageId}: ${String(err)}`);
|
|
2839
|
+
}
|
|
2840
|
+
if (!verifiedEvent) {
|
|
2841
|
+
log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (local mention not verifiable)`);
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
const deliveredCtx = parseFeishuMessageEvent(verifiedEvent, localBotOpenId, botName);
|
|
2845
|
+
ctx = {
|
|
2846
|
+
...deliveredCtx,
|
|
2847
|
+
mentionedBot: true,
|
|
2848
|
+
content: normalizeFeishuCommandProbeBody(deliveredCtx.content),
|
|
2849
|
+
mentionTargets: void 0
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2168
2853
|
if (event.message.message_type === "merge_forward") {
|
|
2169
2854
|
log(`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`);
|
|
2170
2855
|
try {
|
|
@@ -2195,7 +2880,17 @@ async function handleFeishuMessage(params) {
|
|
|
2195
2880
|
}
|
|
2196
2881
|
}
|
|
2197
2882
|
let permissionErrorForAgent;
|
|
2198
|
-
if (feishuCfg?.resolveSenderNames ?? true) {
|
|
2883
|
+
if (feishuCfg?.resolveSenderNames ?? true) if (ctx.senderType === "bot") {
|
|
2884
|
+
const senderName = await resolveFeishuBotName({
|
|
2885
|
+
account,
|
|
2886
|
+
openId: ctx.senderOpenId,
|
|
2887
|
+
log
|
|
2888
|
+
});
|
|
2889
|
+
if (senderName) ctx = {
|
|
2890
|
+
...ctx,
|
|
2891
|
+
senderName
|
|
2892
|
+
};
|
|
2893
|
+
} else {
|
|
2199
2894
|
const senderResult = await resolveFeishuSenderName({
|
|
2200
2895
|
account,
|
|
2201
2896
|
senderId: ctx.senderOpenId,
|
|
@@ -2332,6 +3027,23 @@ async function handleFeishuMessage(params) {
|
|
|
2332
3027
|
});
|
|
2333
3028
|
return;
|
|
2334
3029
|
}
|
|
3030
|
+
if (ctx.senderType === "bot") {
|
|
3031
|
+
if (!localBotOpenId || !ctx.senderOpenId) {
|
|
3032
|
+
log(`feishu[${account.accountId}]: dropping bot message ${ctx.messageId} (loop identity unavailable)`);
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
if (recordChannelBotPairLoopAndCheckSuppression({
|
|
3036
|
+
scopeId: account.accountId,
|
|
3037
|
+
conversationId: ctx.chatId,
|
|
3038
|
+
senderId: ctx.senderOpenId,
|
|
3039
|
+
receiverId: localBotOpenId,
|
|
3040
|
+
defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
|
|
3041
|
+
defaultEnabled: true
|
|
3042
|
+
}).suppressed) {
|
|
3043
|
+
log(`feishu[${account.accountId}]: bot-pair loop suppressed for ${ctx.senderOpenId} in ${ctx.chatId}`);
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
2335
3047
|
}
|
|
2336
3048
|
try {
|
|
2337
3049
|
const core = { channel: channelRuntime?.inbound ? channelRuntime : getFeishuRuntime().channel };
|
|
@@ -2379,7 +3091,7 @@ async function handleFeishuMessage(params) {
|
|
|
2379
3091
|
sendPairingReply: async (text) => {
|
|
2380
3092
|
await sendMessageFeishu({
|
|
2381
3093
|
cfg: authorization.cfg,
|
|
2382
|
-
to: `chat:${ctx.chatId}`,
|
|
3094
|
+
to: directPreDispatchTarget ?? `chat:${ctx.chatId}`,
|
|
2383
3095
|
text,
|
|
2384
3096
|
accountId: account.accountId
|
|
2385
3097
|
});
|
|
@@ -2514,7 +3226,7 @@ async function handleFeishuMessage(params) {
|
|
|
2514
3226
|
const replyTargetMessageId = directThreadReply ? directThreadReplyTargetMessageId : acpTopicReply ? ctx.rootId ?? ctx.messageId : ctx.messageId;
|
|
2515
3227
|
await sendMessageFeishu({
|
|
2516
3228
|
cfg: effectiveCfg,
|
|
2517
|
-
to: `chat:${ctx.chatId}`,
|
|
3229
|
+
to: directPreDispatchTarget ?? `chat:${ctx.chatId}`,
|
|
2518
3230
|
text: `⚠️ Failed to initialize the configured ACP session for this Feishu conversation: ${ensured.error}`,
|
|
2519
3231
|
replyToMessageId: replyTargetMessageId,
|
|
2520
3232
|
replyInThread,
|
|
@@ -2534,7 +3246,7 @@ async function handleFeishuMessage(params) {
|
|
|
2534
3246
|
});
|
|
2535
3247
|
log(`feishu[${account.accountId}]: ${inboundLabel}: ${preview}`);
|
|
2536
3248
|
const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
2537
|
-
const
|
|
3249
|
+
const mediaList = await resolveFeishuMediaList({
|
|
2538
3250
|
cfg,
|
|
2539
3251
|
messageId: ctx.messageId,
|
|
2540
3252
|
messageType: event.message.message_type,
|
|
@@ -2543,12 +3255,10 @@ async function handleFeishuMessage(params) {
|
|
|
2543
3255
|
log,
|
|
2544
3256
|
accountId: account.accountId
|
|
2545
3257
|
});
|
|
2546
|
-
const
|
|
2547
|
-
const
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
mediaPlaceholder: mediaFailurePresentation.mediaPlaceholder,
|
|
2551
|
-
notice: `[feishu ${mediaResolution.unavailableCount > 1 ? `${mediaResolution.unavailableCount} attachments` : "attachment"} unavailable]`
|
|
3258
|
+
const unavailableMediaCount = mediaList.filter((media) => !media.path).length;
|
|
3259
|
+
const mediaFailureContent = unavailableMediaCount > 0 ? formatInboundMediaUnavailableText({
|
|
3260
|
+
body: ctx.content,
|
|
3261
|
+
notice: `[feishu ${unavailableMediaCount > 1 ? `${unavailableMediaCount} attachments` : "attachment"} unavailable]`
|
|
2552
3262
|
}) : ctx.content;
|
|
2553
3263
|
let quotedMessageInfo = null;
|
|
2554
3264
|
let quotedContent;
|
|
@@ -2583,11 +3293,17 @@ async function handleFeishuMessage(params) {
|
|
|
2583
3293
|
cfg: effectiveCfg,
|
|
2584
3294
|
mediaList,
|
|
2585
3295
|
content: ctx.content,
|
|
3296
|
+
messageType: event.message.message_type,
|
|
2586
3297
|
chatType: isGroup ? "group" : "direct",
|
|
2587
3298
|
log
|
|
2588
3299
|
});
|
|
2589
|
-
const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.contentType?.startsWith("audio/"));
|
|
3300
|
+
const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.kind === "audio" || media.contentType?.startsWith("audio/"));
|
|
2590
3301
|
const inboundMedia = toInboundMediaFacts(mediaList, { transcribed: (_media, index) => index === preflightAudioIndex });
|
|
3302
|
+
const requiredMentionTargets = isGroup && ctx.senderType === "bot" && ctx.senderOpenId ? [{
|
|
3303
|
+
openId: ctx.senderOpenId,
|
|
3304
|
+
name: ctx.senderName ?? ctx.senderOpenId,
|
|
3305
|
+
key: ""
|
|
3306
|
+
}] : void 0;
|
|
2591
3307
|
const agentFacingContent = audioTranscript ?? mediaFailureContent;
|
|
2592
3308
|
const commandFacingContent = audioTranscript ?? ctx.content;
|
|
2593
3309
|
const agentFacingCtx = agentFacingContent === ctx.content ? ctx : {
|
|
@@ -2618,7 +3334,7 @@ async function handleFeishuMessage(params) {
|
|
|
2618
3334
|
command: { hasControlCommand: true }
|
|
2619
3335
|
})).commandAccess.authorized : void 0;
|
|
2620
3336
|
const isTopicSessionForThread = isGroup && (groupSession?.groupSessionScope === "group_topic" || groupSession?.groupSessionScope === "group_topic_sender");
|
|
2621
|
-
const envelopeOptions =
|
|
3337
|
+
const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
|
|
2622
3338
|
const messageBody = buildFeishuAgentBody({
|
|
2623
3339
|
ctx: agentFacingCtx,
|
|
2624
3340
|
quotedContent,
|
|
@@ -2627,7 +3343,7 @@ async function handleFeishuMessage(params) {
|
|
|
2627
3343
|
});
|
|
2628
3344
|
const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
|
|
2629
3345
|
if (permissionErrorForAgent) log(`feishu[${account.accountId}]: appending permission error notice to message body`);
|
|
2630
|
-
let combinedBody =
|
|
3346
|
+
let combinedBody = formatAgentEnvelope({
|
|
2631
3347
|
channel: "Feishu",
|
|
2632
3348
|
from: envelopeFrom,
|
|
2633
3349
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -2639,7 +3355,7 @@ async function handleFeishuMessage(params) {
|
|
|
2639
3355
|
historyKey,
|
|
2640
3356
|
limit: historyLimit,
|
|
2641
3357
|
currentMessage: combinedBody,
|
|
2642
|
-
formatEntry: (entry) =>
|
|
3358
|
+
formatEntry: (entry) => formatAgentEnvelope({
|
|
2643
3359
|
channel: "Feishu",
|
|
2644
3360
|
from: `${ctx.chatId}:${entry.sender}`,
|
|
2645
3361
|
timestamp: entry.timestamp,
|
|
@@ -2706,7 +3422,7 @@ async function handleFeishuMessage(params) {
|
|
|
2706
3422
|
threadContextBySessionKey.set(agentSessionKey, threadContext);
|
|
2707
3423
|
return threadContext;
|
|
2708
3424
|
}
|
|
2709
|
-
const storePath =
|
|
3425
|
+
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
|
2710
3426
|
if (core.channel.session.readSessionUpdatedAt({
|
|
2711
3427
|
storePath,
|
|
2712
3428
|
sessionKey: agentSessionKey
|
|
@@ -2748,7 +3464,7 @@ async function handleFeishuMessage(params) {
|
|
|
2748
3464
|
const historyMessages = Boolean(rootMsg?.content || ctx.rootId) ? relevantMessages : relevantMessages.slice(1);
|
|
2749
3465
|
const historyParts = historyMessages.map((msg) => {
|
|
2750
3466
|
const role = msg.senderType === "app" ? "assistant" : "user";
|
|
2751
|
-
return
|
|
3467
|
+
return formatAgentEnvelope({
|
|
2752
3468
|
channel: "Feishu",
|
|
2753
3469
|
from: `${msg.senderId ?? "Unknown"} (${role})`,
|
|
2754
3470
|
timestamp: msg.createTime,
|
|
@@ -2770,7 +3486,6 @@ async function handleFeishuMessage(params) {
|
|
|
2770
3486
|
const threadContext = await resolveThreadContextForAgent(agentId, agentSessionKey, groupName);
|
|
2771
3487
|
return buildChannelInboundEventContext({
|
|
2772
3488
|
channel: "feishu",
|
|
2773
|
-
finalize: core.channel.reply.finalizeInboundContext,
|
|
2774
3489
|
supplemental: {
|
|
2775
3490
|
quote: quotedContent ? {
|
|
2776
3491
|
id: ctx.parentId,
|
|
@@ -2789,7 +3504,8 @@ async function handleFeishuMessage(params) {
|
|
|
2789
3504
|
from: feishuFrom,
|
|
2790
3505
|
sender: {
|
|
2791
3506
|
id: ctx.senderOpenId,
|
|
2792
|
-
name: ctx.senderName ?? ctx.senderOpenId
|
|
3507
|
+
name: ctx.senderName ?? ctx.senderOpenId,
|
|
3508
|
+
isBot: ctx.senderType === "bot"
|
|
2793
3509
|
},
|
|
2794
3510
|
conversation: {
|
|
2795
3511
|
kind: isGroup ? "group" : "direct",
|
|
@@ -2800,6 +3516,7 @@ async function handleFeishuMessage(params) {
|
|
|
2800
3516
|
},
|
|
2801
3517
|
route: {
|
|
2802
3518
|
agentId,
|
|
3519
|
+
dmScope: route.dmScope,
|
|
2803
3520
|
accountId: agentAccountId,
|
|
2804
3521
|
routeSessionKey: agentSessionKey
|
|
2805
3522
|
},
|
|
@@ -2815,6 +3532,7 @@ async function handleFeishuMessage(params) {
|
|
|
2815
3532
|
rawBody: commandFacingContent,
|
|
2816
3533
|
commandBody: commandFacingContent
|
|
2817
3534
|
},
|
|
3535
|
+
sessionTranscript: { historyLimit: isGroup ? historyLimit : 0 },
|
|
2818
3536
|
access: {
|
|
2819
3537
|
mentions: {
|
|
2820
3538
|
canDetectMention: isGroup,
|
|
@@ -2864,189 +3582,225 @@ async function handleFeishuMessage(params) {
|
|
|
2864
3582
|
};
|
|
2865
3583
|
};
|
|
2866
3584
|
if (broadcastAgents) {
|
|
2867
|
-
|
|
3585
|
+
const broadcastDedupeKey = messageDedupeKey ?? ctx.messageId;
|
|
3586
|
+
const broadcastClaim = await claimUnprocessedFeishuMessage({
|
|
3587
|
+
messageId: broadcastDedupeKey,
|
|
3588
|
+
namespace: "broadcast",
|
|
3589
|
+
log
|
|
3590
|
+
});
|
|
3591
|
+
if (broadcastClaim.kind === "duplicate" || broadcastClaim.kind === "inflight") {
|
|
2868
3592
|
log(`feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`);
|
|
2869
3593
|
return;
|
|
2870
3594
|
}
|
|
3595
|
+
const broadcastSettlement = createFeishuBroadcastIngressSettlement({
|
|
3596
|
+
lifecycle: turnAdoptionLifecycle,
|
|
3597
|
+
replayClaim: broadcastClaim.kind === "claimed" ? broadcastClaim.handle : void 0,
|
|
3598
|
+
onReplayCommitError: (err) => error(`feishu[${account.accountId}]: failed to commit broadcast replay guard: ${String(err)}`),
|
|
3599
|
+
onAdopted: () => {
|
|
3600
|
+
if (isGroup && historyKey && chatHistories) createChannelHistoryWindow({ historyMap: chatHistories }).clear({
|
|
3601
|
+
historyKey,
|
|
3602
|
+
limit: historyLimit
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
});
|
|
3606
|
+
const abandonBroadcast = async (err) => {
|
|
3607
|
+
try {
|
|
3608
|
+
await broadcastSettlement.onDispatchFailed(err);
|
|
3609
|
+
} catch (abandonError) {
|
|
3610
|
+
error(`feishu[${account.accountId}]: failed to abandon broadcast ingress: ${String(abandonError)}`);
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
2871
3613
|
const strategy = cfg.broadcast?.strategy === "sequential" ? "sequential" : "parallel";
|
|
2872
3614
|
const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId$2(route.agentId) : null;
|
|
2873
3615
|
const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId$2(a.id));
|
|
2874
3616
|
const hasKnownAgents = agentIds.length > 0;
|
|
2875
3617
|
log(`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`);
|
|
2876
3618
|
const dispatchForAgent = async (agentId) => {
|
|
2877
|
-
|
|
3619
|
+
const normalizedAgentId = normalizeAgentId$2(agentId);
|
|
3620
|
+
if (hasKnownAgents && !agentIds.includes(normalizedAgentId)) {
|
|
2878
3621
|
log(`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`);
|
|
2879
3622
|
return;
|
|
2880
3623
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
})
|
|
2888
|
-
|
|
2889
|
-
|
|
3624
|
+
let agentClaim;
|
|
3625
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
3626
|
+
agentClaim = await claimUnprocessedFeishuMessage({
|
|
3627
|
+
messageId: broadcastDedupeKey,
|
|
3628
|
+
namespace: `broadcast:${normalizedAgentId}`,
|
|
3629
|
+
log
|
|
3630
|
+
});
|
|
3631
|
+
if (agentClaim.kind === "duplicate") return;
|
|
3632
|
+
if (agentClaim.kind !== "inflight") break;
|
|
3633
|
+
broadcastSettlement.onLanePending();
|
|
3634
|
+
try {
|
|
3635
|
+
await agentClaim.pending;
|
|
3636
|
+
return;
|
|
3637
|
+
} catch (err) {
|
|
3638
|
+
if (attempt === 1) throw err;
|
|
2890
3639
|
}
|
|
2891
|
-
}
|
|
2892
|
-
const
|
|
2893
|
-
|
|
2894
|
-
agentId,
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3640
|
+
}
|
|
3641
|
+
const lane = broadcastSettlement.createLane(agentClaim?.kind === "claimed" ? agentClaim.handle : void 0);
|
|
3642
|
+
try {
|
|
3643
|
+
const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
|
|
3644
|
+
const agentStorePath = resolveStorePath(cfg.session?.store, { agentId });
|
|
3645
|
+
const agentRecord = {
|
|
3646
|
+
updateLastRoute: buildFeishuInboundLastRouteUpdate({
|
|
3647
|
+
sessionKey: agentSessionKey,
|
|
3648
|
+
accountId: route.accountId
|
|
3649
|
+
}),
|
|
3650
|
+
onRecordError: (err) => {
|
|
3651
|
+
log(`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`);
|
|
3652
|
+
}
|
|
3653
|
+
};
|
|
3654
|
+
const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
|
|
2902
3655
|
cfg,
|
|
2903
3656
|
agentId,
|
|
2904
|
-
|
|
2905
|
-
chatId: ctx.chatId,
|
|
2906
|
-
sendTarget: feishuTo,
|
|
2907
|
-
allowReasoningPreview,
|
|
2908
|
-
replyToMessageId: replyTargetMessageId,
|
|
2909
|
-
typingTargetMessageId,
|
|
2910
|
-
skipReplyToInMessages: !isGroup && !directThreadReply,
|
|
2911
|
-
replyInThread,
|
|
2912
|
-
rootId: ctx.rootId,
|
|
2913
|
-
threadReply,
|
|
2914
|
-
accountId: account.accountId,
|
|
2915
|
-
identity,
|
|
2916
|
-
mentionTargets: ctx.mentionTargets,
|
|
2917
|
-
messageCreateTimeMs,
|
|
3657
|
+
storePath: agentStorePath,
|
|
2918
3658
|
sessionKey: agentSessionKey
|
|
2919
3659
|
});
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
3660
|
+
const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
|
|
3661
|
+
if (agentId === activeAgentId) {
|
|
3662
|
+
const identity = resolveAgentOutboundIdentity(cfg, agentId);
|
|
3663
|
+
const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
|
|
3664
|
+
cfg,
|
|
3665
|
+
agentId,
|
|
3666
|
+
runtime,
|
|
3667
|
+
chatId: ctx.chatId,
|
|
3668
|
+
sendTarget: feishuTo,
|
|
3669
|
+
allowReasoningPreview,
|
|
3670
|
+
replyToMessageId: replyTargetMessageId,
|
|
3671
|
+
typingTargetMessageId,
|
|
3672
|
+
skipReplyToInMessages: !isGroup && !directThreadReply,
|
|
3673
|
+
replyInThread,
|
|
3674
|
+
rootId: ctx.rootId,
|
|
3675
|
+
threadReply,
|
|
3676
|
+
accountId: account.accountId,
|
|
3677
|
+
identity,
|
|
3678
|
+
mentionTargets: ctx.mentionTargets,
|
|
3679
|
+
requiredMentionTargets,
|
|
3680
|
+
messageCreateTimeMs,
|
|
3681
|
+
sessionKey: agentSessionKey
|
|
3682
|
+
});
|
|
3683
|
+
log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
|
|
3684
|
+
const turnResult = await core.channel.inbound.run({
|
|
3685
|
+
channel: "feishu",
|
|
3686
|
+
accountId: route.accountId,
|
|
3687
|
+
raw: ctx,
|
|
3688
|
+
adapter: {
|
|
3689
|
+
ingest: () => ({
|
|
3690
|
+
id: ctx.messageId,
|
|
3691
|
+
timestamp: messageCreateTimeMs,
|
|
3692
|
+
rawText: ctx.content,
|
|
3693
|
+
textForAgent: agentCtx.BodyForAgent,
|
|
3694
|
+
textForCommands: agentCtx.CommandBody,
|
|
3695
|
+
raw: ctx
|
|
2945
3696
|
}),
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3697
|
+
resolveTurn: () => ({
|
|
3698
|
+
cfg,
|
|
3699
|
+
channel: "feishu",
|
|
3700
|
+
accountId: route.accountId,
|
|
3701
|
+
route: {
|
|
3702
|
+
agentId,
|
|
3703
|
+
sessionKey: agentSessionKey
|
|
3704
|
+
},
|
|
3705
|
+
ctxPayload: agentCtx,
|
|
3706
|
+
record: agentRecord,
|
|
3707
|
+
dispatcherOptions,
|
|
3708
|
+
delivery,
|
|
3709
|
+
replyOptions: {
|
|
3710
|
+
...replyOptions,
|
|
3711
|
+
...bindIngressLifecycleToReplyOptions(lane.lifecycle)
|
|
3712
|
+
}
|
|
2955
3713
|
})
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
ingest: () => ({
|
|
2993
|
-
id: ctx.messageId,
|
|
2994
|
-
timestamp: messageCreateTimeMs,
|
|
2995
|
-
rawText: ctx.content,
|
|
2996
|
-
textForAgent: agentCtx.BodyForAgent,
|
|
2997
|
-
textForCommands: agentCtx.CommandBody,
|
|
2998
|
-
raw: ctx
|
|
2999
|
-
}),
|
|
3000
|
-
resolveTurn: () => ({
|
|
3001
|
-
channel: "feishu",
|
|
3002
|
-
accountId: route.accountId,
|
|
3003
|
-
routeSessionKey: agentSessionKey,
|
|
3004
|
-
storePath: agentStorePath,
|
|
3005
|
-
ctxPayload: agentCtx,
|
|
3006
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
3007
|
-
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
|
-
})
|
|
3714
|
+
}
|
|
3715
|
+
});
|
|
3716
|
+
if (turnResult.dispatched && shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
|
|
3717
|
+
await lane.onDispatchComplete(turnResult.dispatched);
|
|
3718
|
+
} else {
|
|
3719
|
+
delete agentCtx.CommandAuthorized;
|
|
3720
|
+
log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
|
|
3721
|
+
const turnResult = await core.channel.inbound.run({
|
|
3722
|
+
channel: "feishu",
|
|
3723
|
+
accountId: route.accountId,
|
|
3724
|
+
raw: ctx,
|
|
3725
|
+
adapter: {
|
|
3726
|
+
ingest: () => ({
|
|
3727
|
+
id: ctx.messageId,
|
|
3728
|
+
timestamp: messageCreateTimeMs,
|
|
3729
|
+
rawText: ctx.content,
|
|
3730
|
+
textForAgent: agentCtx.BodyForAgent,
|
|
3731
|
+
textForCommands: agentCtx.CommandBody,
|
|
3732
|
+
raw: ctx
|
|
3733
|
+
}),
|
|
3734
|
+
resolveTurn: () => ({
|
|
3735
|
+
cfg,
|
|
3736
|
+
channel: "feishu",
|
|
3737
|
+
accountId: route.accountId,
|
|
3738
|
+
route: {
|
|
3739
|
+
agentId,
|
|
3740
|
+
sessionKey: agentSessionKey
|
|
3741
|
+
},
|
|
3742
|
+
ctxPayload: agentCtx,
|
|
3743
|
+
record: agentRecord,
|
|
3744
|
+
admission: {
|
|
3745
|
+
kind: "observeOnly",
|
|
3746
|
+
reason: "broadcast-observer"
|
|
3747
|
+
},
|
|
3748
|
+
delivery: { deliver: async () => ({ visibleReplySent: false }) },
|
|
3749
|
+
replyOptions: bindIngressLifecycleToReplyOptions(lane.lifecycle)
|
|
3015
3750
|
})
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3751
|
+
}
|
|
3752
|
+
});
|
|
3753
|
+
await lane.onDispatchComplete(turnResult.dispatched);
|
|
3754
|
+
}
|
|
3755
|
+
} catch (err) {
|
|
3756
|
+
await lane.onDispatchFailed(err);
|
|
3757
|
+
throw err;
|
|
3019
3758
|
}
|
|
3020
3759
|
};
|
|
3760
|
+
const results = [];
|
|
3021
3761
|
if (strategy === "sequential") for (const agentId of broadcastAgents) try {
|
|
3022
3762
|
await dispatchForAgent(agentId);
|
|
3023
|
-
|
|
3024
|
-
|
|
3763
|
+
results.push({
|
|
3764
|
+
status: "fulfilled",
|
|
3765
|
+
value: void 0
|
|
3766
|
+
});
|
|
3767
|
+
} catch (reason) {
|
|
3768
|
+
results.push({
|
|
3769
|
+
status: "rejected",
|
|
3770
|
+
reason
|
|
3771
|
+
});
|
|
3025
3772
|
}
|
|
3026
|
-
else
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3773
|
+
else results.push(...await Promise.allSettled(broadcastAgents.map(dispatchForAgent)));
|
|
3774
|
+
const failures = [];
|
|
3775
|
+
for (const [i, result] of results.entries()) if (result.status === "rejected") {
|
|
3776
|
+
const agentId = broadcastAgents.at(i);
|
|
3777
|
+
if (agentId === void 0) continue;
|
|
3778
|
+
log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`);
|
|
3779
|
+
failures.push(result.reason);
|
|
3780
|
+
}
|
|
3781
|
+
if (failures.length > 0) {
|
|
3782
|
+
const failure = failures.length === 1 ? failures[0] : new AggregateError(failures, "Feishu broadcast dispatch failed");
|
|
3783
|
+
await abandonBroadcast(failure);
|
|
3784
|
+
throw failure;
|
|
3785
|
+
}
|
|
3786
|
+
try {
|
|
3787
|
+
await broadcastSettlement.onDispatchComplete();
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
await abandonBroadcast(err);
|
|
3790
|
+
throw err;
|
|
3033
3791
|
}
|
|
3034
|
-
if (isGroup && historyKey && chatHistories) createChannelHistoryWindow({ historyMap: chatHistories }).clear({
|
|
3035
|
-
historyKey,
|
|
3036
|
-
limit: historyLimit
|
|
3037
|
-
});
|
|
3038
3792
|
log(`feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`);
|
|
3039
3793
|
} else {
|
|
3040
3794
|
const ctxPayload = await buildCtxPayloadForAgent(route.agentId, route.sessionKey, route.accountId, ctx.mentionedBot);
|
|
3041
3795
|
const identity = resolveAgentOutboundIdentity(effectiveCfg, route.agentId);
|
|
3042
|
-
const storePath =
|
|
3796
|
+
const storePath = resolveStorePath(effectiveCfg.session?.store, { agentId: route.agentId });
|
|
3043
3797
|
const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
|
|
3044
3798
|
cfg: effectiveCfg,
|
|
3045
3799
|
agentId: route.agentId,
|
|
3046
3800
|
storePath,
|
|
3047
3801
|
sessionKey: route.sessionKey
|
|
3048
3802
|
});
|
|
3049
|
-
const {
|
|
3803
|
+
const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
|
|
3050
3804
|
cfg: effectiveCfg,
|
|
3051
3805
|
agentId: route.agentId,
|
|
3052
3806
|
runtime,
|
|
@@ -3062,6 +3816,7 @@ async function handleFeishuMessage(params) {
|
|
|
3062
3816
|
accountId: account.accountId,
|
|
3063
3817
|
identity,
|
|
3064
3818
|
mentionTargets: ctx.mentionTargets,
|
|
3819
|
+
requiredMentionTargets,
|
|
3065
3820
|
messageCreateTimeMs,
|
|
3066
3821
|
sessionKey: route.sessionKey
|
|
3067
3822
|
});
|
|
@@ -3080,12 +3835,14 @@ async function handleFeishuMessage(params) {
|
|
|
3080
3835
|
raw: ctx
|
|
3081
3836
|
}),
|
|
3082
3837
|
resolveTurn: () => ({
|
|
3838
|
+
cfg: effectiveCfg,
|
|
3083
3839
|
channel: "feishu",
|
|
3084
3840
|
accountId: route.accountId,
|
|
3085
|
-
|
|
3086
|
-
|
|
3841
|
+
route: {
|
|
3842
|
+
agentId: route.agentId,
|
|
3843
|
+
sessionKey: route.sessionKey
|
|
3844
|
+
},
|
|
3087
3845
|
ctxPayload,
|
|
3088
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
3089
3846
|
record: {
|
|
3090
3847
|
updateLastRoute: buildFeishuInboundLastRouteUpdate({
|
|
3091
3848
|
sessionKey: route.sessionKey,
|
|
@@ -3101,40 +3858,24 @@ async function handleFeishuMessage(params) {
|
|
|
3101
3858
|
historyMap: chatHistories,
|
|
3102
3859
|
limit: historyLimit
|
|
3103
3860
|
},
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
onSettled: () => {
|
|
3111
|
-
markDispatchIdle();
|
|
3112
|
-
},
|
|
3113
|
-
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
3114
|
-
ctx: ctxPayload,
|
|
3115
|
-
cfg: effectiveCfg,
|
|
3116
|
-
dispatcher,
|
|
3117
|
-
replyOptions
|
|
3118
|
-
})
|
|
3119
|
-
})
|
|
3861
|
+
dispatcherOptions,
|
|
3862
|
+
delivery,
|
|
3863
|
+
replyOptions: {
|
|
3864
|
+
...replyOptions,
|
|
3865
|
+
...turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(turnAdoptionLifecycle) : {}
|
|
3866
|
+
}
|
|
3120
3867
|
})
|
|
3121
3868
|
}
|
|
3122
3869
|
});
|
|
3123
3870
|
if (!turnResult.dispatched) return;
|
|
3124
3871
|
const { dispatchResult } = turnResult;
|
|
3125
3872
|
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");
|
|
3873
|
+
if (shouldSendNoVisibleReplyFallback(dispatchResult)) await ensureNoVisibleReplyFallback("dispatch-complete-no-visible-reply");
|
|
3134
3874
|
log(`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
|
|
3135
3875
|
}
|
|
3136
3876
|
} catch (err) {
|
|
3137
3877
|
error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
|
|
3878
|
+
if (turnAdoptionLifecycle) throw err;
|
|
3138
3879
|
}
|
|
3139
3880
|
}
|
|
3140
3881
|
//#endregion
|
|
@@ -3250,7 +3991,7 @@ function completeFeishuCardAction(actionId, accountId, now = Date.now()) {
|
|
|
3250
3991
|
expiresAt
|
|
3251
3992
|
});
|
|
3252
3993
|
}
|
|
3253
|
-
function buildSyntheticMessageEvent(event, content, chatType) {
|
|
3994
|
+
function buildSyntheticMessageEvent$1(event, content, chatType) {
|
|
3254
3995
|
const replyTargetMessageId = event.context.open_message_id ?? event.open_message_id;
|
|
3255
3996
|
const isTemporaryCardActionId = replyTargetMessageId?.startsWith("card-action-c-");
|
|
3256
3997
|
const validReplyTargetId = replyTargetMessageId && !isTemporaryCardActionId ? replyTargetMessageId : void 0;
|
|
@@ -3286,7 +4027,7 @@ async function dispatchSyntheticCommand(params) {
|
|
|
3286
4027
|
});
|
|
3287
4028
|
await handleFeishuMessage({
|
|
3288
4029
|
cfg: params.cfg,
|
|
3289
|
-
event: buildSyntheticMessageEvent(params.event, params.command, resolvedChatType),
|
|
4030
|
+
event: buildSyntheticMessageEvent$1(params.event, params.command, resolvedChatType),
|
|
3290
4031
|
botOpenId: params.botOpenId,
|
|
3291
4032
|
runtime: params.runtime,
|
|
3292
4033
|
channelRuntime: params.channelRuntime,
|
|
@@ -3484,32 +4225,376 @@ async function handleFeishuCardAction(params) {
|
|
|
3484
4225
|
completeFeishuCardAction(event.token, account.accountId);
|
|
3485
4226
|
return;
|
|
3486
4227
|
}
|
|
3487
|
-
await sendInvalidInteractionNotice({
|
|
3488
|
-
cfg,
|
|
3489
|
-
event,
|
|
3490
|
-
reason: "malformed",
|
|
3491
|
-
accountId
|
|
3492
|
-
});
|
|
3493
|
-
completeFeishuCardAction(event.token, account.accountId);
|
|
3494
|
-
return;
|
|
4228
|
+
await sendInvalidInteractionNotice({
|
|
4229
|
+
cfg,
|
|
4230
|
+
event,
|
|
4231
|
+
reason: "malformed",
|
|
4232
|
+
accountId
|
|
4233
|
+
});
|
|
4234
|
+
completeFeishuCardAction(event.token, account.accountId);
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4237
|
+
const content = buildFeishuCardActionTextFallback(event);
|
|
4238
|
+
log(`feishu[${account.accountId}]: handling card action from ${event.operator.open_id}: ${content}`);
|
|
4239
|
+
await dispatchSyntheticCommand({
|
|
4240
|
+
cfg,
|
|
4241
|
+
event,
|
|
4242
|
+
command: content,
|
|
4243
|
+
account,
|
|
4244
|
+
botOpenId: params.botOpenId,
|
|
4245
|
+
runtime,
|
|
4246
|
+
channelRuntime: params.channelRuntime,
|
|
4247
|
+
accountId
|
|
4248
|
+
});
|
|
4249
|
+
completeFeishuCardAction(event.token, account.accountId);
|
|
4250
|
+
} catch (err) {
|
|
4251
|
+
completeFeishuCardAction(event.token, account.accountId);
|
|
4252
|
+
throw err;
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
//#endregion
|
|
4256
|
+
//#region extensions/feishu/src/feishu-ingress.ts
|
|
4257
|
+
const FEISHU_INGRESS_PAYLOAD_VERSION = 1;
|
|
4258
|
+
const FEISHU_INGRESS_POLL_INTERVAL_MS = 500;
|
|
4259
|
+
const FEISHU_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
|
|
4260
|
+
const FEISHU_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
4261
|
+
const FEISHU_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
|
|
4262
|
+
const FEISHU_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
4263
|
+
const FEISHU_INGRESS_FAILED_MAX_ENTRIES = 2e4;
|
|
4264
|
+
const FEISHU_DURABLE_EVENT_TYPES = /* @__PURE__ */ new Set(["drive.notice.comment_add_v1", "im.message.receive_v1"]);
|
|
4265
|
+
var FeishuIngressPermanentError = class extends Error {
|
|
4266
|
+
constructor(reason, message, options) {
|
|
4267
|
+
super(message, options);
|
|
4268
|
+
this.reason = reason;
|
|
4269
|
+
this.name = "FeishuIngressPermanentError";
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
function isRecord$1(value) {
|
|
4273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4274
|
+
}
|
|
4275
|
+
function readString(value) {
|
|
4276
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
4277
|
+
}
|
|
4278
|
+
function parseRawEnvelope(rawEnvelope) {
|
|
4279
|
+
let parsed;
|
|
4280
|
+
try {
|
|
4281
|
+
parsed = JSON.parse(rawEnvelope);
|
|
4282
|
+
} catch (error) {
|
|
4283
|
+
throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope contains invalid JSON.", { cause: error });
|
|
4284
|
+
}
|
|
4285
|
+
if (!isRecord$1(parsed)) throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope must be a JSON object.");
|
|
4286
|
+
return parsed;
|
|
4287
|
+
}
|
|
4288
|
+
function decryptEnvelope(envelope, encryptKey) {
|
|
4289
|
+
const encrypted = readString(envelope.encrypt);
|
|
4290
|
+
if (!encrypted) return envelope;
|
|
4291
|
+
const key = encryptKey?.trim();
|
|
4292
|
+
if (!key) throw new FeishuIngressPermanentError("authentication-failed", "Feishu encrypted ingress envelope has no configured encrypt key.");
|
|
4293
|
+
try {
|
|
4294
|
+
return parseRawEnvelope(new Lark.AESCipher(key).decrypt(encrypted));
|
|
4295
|
+
} catch (error) {
|
|
4296
|
+
if (error instanceof FeishuIngressPermanentError) throw error;
|
|
4297
|
+
throw new FeishuIngressPermanentError("authentication-failed", "Feishu ingress envelope decryption failed.", { cause: error });
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
function inspectFeishuIngressEnvelope(rawEnvelope, encryptKey, allowInvalidLane = false) {
|
|
4301
|
+
const envelope = decryptEnvelope(parseRawEnvelope(rawEnvelope), encryptKey);
|
|
4302
|
+
const nestedHeader = isRecord$1(envelope.header) ? envelope.header : null;
|
|
4303
|
+
const nestedEvent = isRecord$1(envelope.event) ? envelope.event : null;
|
|
4304
|
+
const eventType = readString(nestedHeader?.event_type) ?? readString(envelope.event_type);
|
|
4305
|
+
if (!eventType || !FEISHU_DURABLE_EVENT_TYPES.has(eventType)) return null;
|
|
4306
|
+
const eventId = readString(nestedHeader?.event_id) ?? readString(envelope.event_id);
|
|
4307
|
+
if (!eventId) throw new FeishuIngressPermanentError("invalid-event", `Feishu ${eventType} envelope is missing header.event_id.`);
|
|
4308
|
+
const event = nestedEvent ?? envelope;
|
|
4309
|
+
if (eventType === "im.message.receive_v1") {
|
|
4310
|
+
const chatId = readString((isRecord$1(event.message) ? event.message : null)?.chat_id);
|
|
4311
|
+
if (!chatId) {
|
|
4312
|
+
if (allowInvalidLane) return {
|
|
4313
|
+
eventId,
|
|
4314
|
+
eventType,
|
|
4315
|
+
laneKey: `invalid:${eventType}:${eventId}`
|
|
4316
|
+
};
|
|
4317
|
+
throw new FeishuIngressPermanentError("invalid-event", "Feishu message ingress envelope is missing event.message.chat_id.");
|
|
4318
|
+
}
|
|
4319
|
+
return {
|
|
4320
|
+
eventId,
|
|
4321
|
+
eventType,
|
|
4322
|
+
laneKey: `chat:${chatId}`
|
|
4323
|
+
};
|
|
4324
|
+
}
|
|
4325
|
+
const noticeMeta = isRecord$1(event.notice_meta) ? event.notice_meta : null;
|
|
4326
|
+
const fileType = readString(noticeMeta?.file_type);
|
|
4327
|
+
const documentId = readString(noticeMeta?.file_token);
|
|
4328
|
+
if (!fileType || !documentId) {
|
|
4329
|
+
if (allowInvalidLane) return {
|
|
4330
|
+
eventId,
|
|
4331
|
+
eventType,
|
|
4332
|
+
laneKey: `invalid:${eventType}:${eventId}`
|
|
4333
|
+
};
|
|
4334
|
+
throw new FeishuIngressPermanentError("invalid-event", "Feishu comment ingress envelope is missing its document identity.");
|
|
4335
|
+
}
|
|
4336
|
+
return {
|
|
4337
|
+
eventId,
|
|
4338
|
+
eventType,
|
|
4339
|
+
laneKey: `comment-doc:${fileType}:${documentId}`
|
|
4340
|
+
};
|
|
4341
|
+
}
|
|
4342
|
+
function resolveFeishuIngressNonRetryableFailure(error) {
|
|
4343
|
+
if (error instanceof FeishuIngressPermanentError) return {
|
|
4344
|
+
reason: error.reason,
|
|
4345
|
+
message: error.message
|
|
4346
|
+
};
|
|
4347
|
+
for (const candidate of collectErrorGraphCandidates(error, (current) => [current.cause, current.response])) {
|
|
4348
|
+
const status = isRecord$1(candidate) ? candidate.status : void 0;
|
|
4349
|
+
if (status === 401 || status === 403) return {
|
|
4350
|
+
reason: "authentication-failed",
|
|
4351
|
+
message: formatErrorMessage(error)
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
return null;
|
|
4355
|
+
}
|
|
4356
|
+
/** Fan one merged Feishu turn's adoption across every transport and logical claim. */
|
|
4357
|
+
function buildFeishuFlushIngressLifecycle(sources, options) {
|
|
4358
|
+
const durableSources = sources.filter((source) => source.lifecycle !== void 0);
|
|
4359
|
+
const lifecycles = durableSources.map((source) => source.lifecycle);
|
|
4360
|
+
const replayClaims = durableSources.map((source) => source.replayClaim).filter((claim) => claim !== void 0);
|
|
4361
|
+
const [firstLifecycle] = lifecycles;
|
|
4362
|
+
if (!firstLifecycle) return {
|
|
4363
|
+
lifecycle: void 0,
|
|
4364
|
+
settle: async () => {}
|
|
4365
|
+
};
|
|
4366
|
+
let handedOff = false;
|
|
4367
|
+
let terminal;
|
|
4368
|
+
let adopting;
|
|
4369
|
+
let abandoning;
|
|
4370
|
+
const releaseReplayClaims = () => {
|
|
4371
|
+
for (const claim of replayClaims) claim.release({ error: /* @__PURE__ */ new Error("feishu-ingress-not-adopted") });
|
|
4372
|
+
};
|
|
4373
|
+
const runAbandon = async () => {
|
|
4374
|
+
if (terminal) return;
|
|
4375
|
+
releaseReplayClaims();
|
|
4376
|
+
await Promise.all(lifecycles.map(async (lifecycle) => await lifecycle.onAbandoned()));
|
|
4377
|
+
terminal = "abandoned";
|
|
4378
|
+
};
|
|
4379
|
+
const ensureAbandoned = async () => {
|
|
4380
|
+
if (terminal) return;
|
|
4381
|
+
const activeAbandonment = abandoning ?? runAbandon();
|
|
4382
|
+
abandoning = activeAbandonment;
|
|
4383
|
+
try {
|
|
4384
|
+
await activeAbandonment;
|
|
4385
|
+
} finally {
|
|
4386
|
+
if (abandoning === activeAbandonment && terminal !== "abandoned") abandoning = void 0;
|
|
4387
|
+
}
|
|
4388
|
+
};
|
|
4389
|
+
const abandonAll = async () => {
|
|
4390
|
+
if (terminal) return;
|
|
4391
|
+
if (adopting) {
|
|
4392
|
+
await adopting.catch(() => void 0);
|
|
4393
|
+
if (terminal) return;
|
|
4394
|
+
}
|
|
4395
|
+
await ensureAbandoned();
|
|
4396
|
+
};
|
|
4397
|
+
const adoptAll = async () => {
|
|
4398
|
+
if (terminal) return;
|
|
4399
|
+
if (abandoning) {
|
|
4400
|
+
await abandoning.catch(() => void 0);
|
|
4401
|
+
if (terminal) return;
|
|
4402
|
+
}
|
|
4403
|
+
const activeAdoption = adopting ?? (async () => {
|
|
4404
|
+
try {
|
|
4405
|
+
for (const lifecycle of lifecycles) await lifecycle.onAdopted();
|
|
4406
|
+
terminal = "adopted";
|
|
4407
|
+
const results = await Promise.allSettled(replayClaims.map(async (claim) => claim.commit()));
|
|
4408
|
+
for (const result of results) if (result.status === "rejected") try {
|
|
4409
|
+
options?.onReplayCommitError?.(result.reason);
|
|
4410
|
+
} catch {}
|
|
4411
|
+
} catch (error) {
|
|
4412
|
+
await ensureAbandoned().catch(() => void 0);
|
|
4413
|
+
throw error;
|
|
4414
|
+
}
|
|
4415
|
+
})();
|
|
4416
|
+
adopting = activeAdoption;
|
|
4417
|
+
try {
|
|
4418
|
+
await activeAdoption;
|
|
4419
|
+
} finally {
|
|
4420
|
+
if (adopting === activeAdoption && terminal !== "adopted") adopting = void 0;
|
|
4421
|
+
}
|
|
4422
|
+
};
|
|
4423
|
+
return {
|
|
4424
|
+
lifecycle: {
|
|
4425
|
+
abortSignal: lifecycles.length === 1 ? firstLifecycle.abortSignal : AbortSignal.any(lifecycles.map((lifecycle) => lifecycle.abortSignal)),
|
|
4426
|
+
onAdopted: async () => {
|
|
4427
|
+
handedOff = true;
|
|
4428
|
+
await adoptAll();
|
|
4429
|
+
},
|
|
4430
|
+
onDeferred: () => {
|
|
4431
|
+
handedOff = true;
|
|
4432
|
+
for (const lifecycle of lifecycles) lifecycle.onDeferred();
|
|
4433
|
+
},
|
|
4434
|
+
onAdoptionFinalizing: () => {
|
|
4435
|
+
for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
|
|
4436
|
+
},
|
|
4437
|
+
onAbandoned: async () => {
|
|
4438
|
+
handedOff = true;
|
|
4439
|
+
await abandonAll();
|
|
4440
|
+
}
|
|
4441
|
+
},
|
|
4442
|
+
settle: async () => {
|
|
4443
|
+
if (handedOff) return;
|
|
4444
|
+
handedOff = true;
|
|
4445
|
+
releaseReplayClaims();
|
|
4446
|
+
try {
|
|
4447
|
+
for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
|
|
4448
|
+
for (const lifecycle of lifecycles) await lifecycle.onAdopted();
|
|
4449
|
+
terminal = "adopted";
|
|
4450
|
+
} catch (error) {
|
|
4451
|
+
await ensureAbandoned().catch(() => void 0);
|
|
4452
|
+
throw error;
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
};
|
|
4456
|
+
}
|
|
4457
|
+
function createFeishuDurableIngress(options) {
|
|
4458
|
+
let socketTerminator;
|
|
4459
|
+
const activeLifecycles = /* @__PURE__ */ new Map();
|
|
4460
|
+
const deferredClaims = /* @__PURE__ */ new Map();
|
|
4461
|
+
const monitor = createChannelIngressMonitor({
|
|
4462
|
+
queue: options.queue ?? (() => getFeishuRuntime().state.openChannelIngressQueue({ accountId: options.accountId })),
|
|
4463
|
+
inspect: (rawEnvelope, context) => {
|
|
4464
|
+
const facts = inspectFeishuIngressEnvelope(rawEnvelope, options.encryptKey, context.phase === "admission");
|
|
4465
|
+
return facts ? {
|
|
4466
|
+
eventId: facts.eventId,
|
|
4467
|
+
laneKey: facts.laneKey
|
|
4468
|
+
} : null;
|
|
4469
|
+
},
|
|
4470
|
+
payload: {
|
|
4471
|
+
version: FEISHU_INGRESS_PAYLOAD_VERSION,
|
|
4472
|
+
serialize: (rawEnvelope, { receivedAt }) => ({
|
|
4473
|
+
receivedAt,
|
|
4474
|
+
rawEnvelope
|
|
4475
|
+
}),
|
|
4476
|
+
deserialize: (body) => body.rawEnvelope,
|
|
4477
|
+
encode: ({ body }) => ({
|
|
4478
|
+
version: FEISHU_INGRESS_PAYLOAD_VERSION,
|
|
4479
|
+
...body
|
|
4480
|
+
}),
|
|
4481
|
+
decode: (payload) => ({
|
|
4482
|
+
version: payload.version,
|
|
4483
|
+
body: {
|
|
4484
|
+
receivedAt: payload.receivedAt,
|
|
4485
|
+
rawEnvelope: payload.rawEnvelope
|
|
4486
|
+
}
|
|
4487
|
+
}),
|
|
4488
|
+
createClaimError: (kind, claim) => new FeishuIngressPermanentError("invalid-event", kind === "invalid-version" ? `Feishu ingress row ${claim.id} has an unsupported version.` : `Feishu ingress row ${claim.id} has invalid delivery identity.`)
|
|
4489
|
+
},
|
|
4490
|
+
deliver: async (rawEnvelope, lifecycle, claim) => {
|
|
4491
|
+
let resolveDeferred;
|
|
4492
|
+
const deferred = new Promise((resolve) => {
|
|
4493
|
+
resolveDeferred = resolve;
|
|
4494
|
+
});
|
|
4495
|
+
let deferredSettled = false;
|
|
4496
|
+
const settleDeferred = () => {
|
|
4497
|
+
if (deferredSettled) return;
|
|
4498
|
+
deferredSettled = true;
|
|
4499
|
+
if (deferredClaims.get(claim.id) === deferred) deferredClaims.delete(claim.id);
|
|
4500
|
+
resolveDeferred();
|
|
4501
|
+
};
|
|
4502
|
+
const abandonHandlers = /* @__PURE__ */ new Set();
|
|
4503
|
+
const wrappedLifecycle = {
|
|
4504
|
+
...lifecycle,
|
|
4505
|
+
onAdopted: async () => {
|
|
4506
|
+
try {
|
|
4507
|
+
await lifecycle.onAdopted();
|
|
4508
|
+
} finally {
|
|
4509
|
+
settleDeferred();
|
|
4510
|
+
}
|
|
4511
|
+
},
|
|
4512
|
+
onAbandoned: async () => {
|
|
4513
|
+
try {
|
|
4514
|
+
await Promise.allSettled([...abandonHandlers].map(async (handler) => await handler()));
|
|
4515
|
+
await lifecycle.onAbandoned();
|
|
4516
|
+
} finally {
|
|
4517
|
+
settleDeferred();
|
|
4518
|
+
}
|
|
4519
|
+
},
|
|
4520
|
+
registerAbandonHandler: (handler) => {
|
|
4521
|
+
abandonHandlers.add(handler);
|
|
4522
|
+
return () => abandonHandlers.delete(handler);
|
|
4523
|
+
}
|
|
4524
|
+
};
|
|
4525
|
+
activeLifecycles.set(claim.id, wrappedLifecycle);
|
|
4526
|
+
try {
|
|
4527
|
+
const result = await options.dispatcher.invoke(parseRawEnvelope(rawEnvelope), { needCheck: false });
|
|
4528
|
+
if (!isRecord$1(result)) return;
|
|
4529
|
+
if (result.kind === "deferred") {
|
|
4530
|
+
if (!deferredSettled) deferredClaims.set(claim.id, deferred);
|
|
4531
|
+
return { kind: "deferred" };
|
|
4532
|
+
}
|
|
4533
|
+
if (result.kind === "completed") return { kind: "completed" };
|
|
4534
|
+
if (result.kind === "failed-retryable") return {
|
|
4535
|
+
kind: "failed-retryable",
|
|
4536
|
+
error: result.error
|
|
4537
|
+
};
|
|
4538
|
+
return;
|
|
4539
|
+
} finally {
|
|
4540
|
+
if (activeLifecycles.get(claim.id) === wrappedLifecycle) activeLifecycles.delete(claim.id);
|
|
4541
|
+
}
|
|
4542
|
+
},
|
|
4543
|
+
pollIntervalMs: options.pollIntervalMs ?? FEISHU_INGRESS_POLL_INTERVAL_MS,
|
|
4544
|
+
retention: {
|
|
4545
|
+
pruneIntervalMs: FEISHU_INGRESS_PRUNE_INTERVAL_MS,
|
|
4546
|
+
completedTtlMs: FEISHU_INGRESS_COMPLETED_TTL_MS,
|
|
4547
|
+
completedMaxEntries: FEISHU_INGRESS_COMPLETED_MAX_ENTRIES,
|
|
4548
|
+
failedTtlMs: FEISHU_INGRESS_FAILED_TTL_MS,
|
|
4549
|
+
failedMaxEntries: FEISHU_INGRESS_FAILED_MAX_ENTRIES
|
|
4550
|
+
},
|
|
4551
|
+
drain: {
|
|
4552
|
+
adoptionStallTimeoutMs: options.adoptionStallTimeoutMs ?? DEFAULT_INGRESS_ADOPTION_STALL_MS,
|
|
4553
|
+
resolveNonRetryableFailure: resolveFeishuIngressNonRetryableFailure,
|
|
4554
|
+
onLog: (message) => options.runtime.error?.(`feishu ingress: ${message}`)
|
|
4555
|
+
},
|
|
4556
|
+
createStoppedError: () => /* @__PURE__ */ new Error("Feishu ingress is stopped."),
|
|
4557
|
+
onError: (error) => options.runtime.error?.(`feishu ingress drain failed: ${formatErrorMessage(error)}`)
|
|
4558
|
+
});
|
|
4559
|
+
const invoke = async (data, params) => {
|
|
4560
|
+
let rawEnvelope;
|
|
4561
|
+
try {
|
|
4562
|
+
const serialized = JSON.stringify(data);
|
|
4563
|
+
if (serialized === void 0) throw new TypeError("Feishu ingress envelope has no JSON representation.");
|
|
4564
|
+
rawEnvelope = serialized;
|
|
4565
|
+
} catch (error) {
|
|
4566
|
+
throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope is not serializable.", { cause: error });
|
|
3495
4567
|
}
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
4568
|
+
const facts = inspectFeishuIngressEnvelope(rawEnvelope, options.encryptKey, true);
|
|
4569
|
+
if (!facts) return await options.dispatcher.invoke(data, params);
|
|
4570
|
+
try {
|
|
4571
|
+
await monitor.admit(rawEnvelope, { facts: {
|
|
4572
|
+
eventId: facts.eventId,
|
|
4573
|
+
laneKey: facts.laneKey
|
|
4574
|
+
} });
|
|
4575
|
+
} catch (error) {
|
|
4576
|
+
socketTerminator?.();
|
|
4577
|
+
throw error;
|
|
4578
|
+
}
|
|
4579
|
+
};
|
|
4580
|
+
return {
|
|
4581
|
+
invoke,
|
|
4582
|
+
resolveLifecycle: (data) => {
|
|
4583
|
+
const eventId = isRecord$1(data) ? readString(data.event_id) : null;
|
|
4584
|
+
return eventId ? activeLifecycles.get(eventId) : void 0;
|
|
4585
|
+
},
|
|
4586
|
+
setSocketTerminator: (terminate) => {
|
|
4587
|
+
socketTerminator = terminate;
|
|
4588
|
+
},
|
|
4589
|
+
start: monitor.start,
|
|
4590
|
+
stop: async () => {
|
|
4591
|
+
await monitor.stop();
|
|
4592
|
+
await Promise.allSettled(deferredClaims.values());
|
|
4593
|
+
activeLifecycles.clear();
|
|
4594
|
+
socketTerminator = void 0;
|
|
4595
|
+
},
|
|
4596
|
+
waitForIdle: monitor.waitForIdle
|
|
4597
|
+
};
|
|
3513
4598
|
}
|
|
3514
4599
|
//#endregion
|
|
3515
4600
|
//#region extensions/feishu/src/monitor-defaults.ts
|
|
@@ -3662,7 +4747,8 @@ function applyBotIdentityState(accountId, identity) {
|
|
|
3662
4747
|
});
|
|
3663
4748
|
return {
|
|
3664
4749
|
botOpenId,
|
|
3665
|
-
botName
|
|
4750
|
+
botName,
|
|
4751
|
+
source: botOpenId ? identity.source : void 0
|
|
3666
4752
|
};
|
|
3667
4753
|
}
|
|
3668
4754
|
async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
|
|
@@ -3672,12 +4758,13 @@ async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
|
|
|
3672
4758
|
for (const [i, delayMs] of BOT_IDENTITY_RETRY_DELAYS_MS.entries()) {
|
|
3673
4759
|
if (abortSignal?.aborted) return;
|
|
3674
4760
|
if (!await waitForAbortableDelay(delayMs, abortSignal)) return;
|
|
3675
|
-
const
|
|
4761
|
+
const identity = await fetchBotIdentityForMonitor(account, {
|
|
3676
4762
|
runtime,
|
|
3677
|
-
abortSignal
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
4763
|
+
abortSignal,
|
|
4764
|
+
allowCachedFallback: false
|
|
4765
|
+
});
|
|
4766
|
+
if (normalizeOptionalString(identity.botOpenId) && identity.source === "provider") {
|
|
4767
|
+
log(`feishu[${accountId}]: bot open_id recovered via background retry: ${applyBotIdentityState(accountId, identity).botOpenId}`);
|
|
3681
4768
|
return;
|
|
3682
4769
|
}
|
|
3683
4770
|
const nextDelayResult = nextDelays.next();
|
|
@@ -3687,10 +4774,10 @@ async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
|
|
|
3687
4774
|
error(`feishu[${accountId}]: bot identity background retry exhausted; requireMention group messages may be skipped until restart`);
|
|
3688
4775
|
}
|
|
3689
4776
|
function startBotIdentityRecovery(params) {
|
|
3690
|
-
const { account, accountId, runtime, abortSignal } = params;
|
|
4777
|
+
const { account, accountId, runtime, abortSignal, currentSource } = params;
|
|
3691
4778
|
const log = runtime?.log ?? console.log;
|
|
3692
|
-
log(`feishu[${accountId}]: bot open_id unknown; starting background
|
|
3693
|
-
log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
|
|
4779
|
+
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(", ")})`);
|
|
4780
|
+
if (currentSource !== "cache") log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
|
|
3694
4781
|
retryBotIdentityProbe(account, accountId, runtime, abortSignal);
|
|
3695
4782
|
}
|
|
3696
4783
|
//#endregion
|
|
@@ -3931,41 +5018,59 @@ function createFeishuCommentReplyDispatcher(params) {
|
|
|
3931
5018
|
accountId: params.accountId,
|
|
3932
5019
|
runtime: params.runtime
|
|
3933
5020
|
});
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
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;
|
|
5021
|
+
return {
|
|
5022
|
+
dispatcherOptions: {
|
|
5023
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
5024
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
5025
|
+
humanDelay: resolveHumanDelayConfig(params.cfg, params.agentId),
|
|
5026
|
+
onReplyStart: async () => {
|
|
5027
|
+
await typingReaction.start();
|
|
5028
|
+
},
|
|
5029
|
+
onCleanup: () => {
|
|
5030
|
+
typingReaction.cleanup();
|
|
3947
5031
|
}
|
|
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
5032
|
},
|
|
3957
|
-
|
|
3958
|
-
|
|
5033
|
+
delivery: {
|
|
5034
|
+
observeMessageSent: true,
|
|
5035
|
+
deliver: async (payload, info) => {
|
|
5036
|
+
if (info.kind !== "final") return noVisibleFeishuReplyDelivery;
|
|
5037
|
+
const reply = resolveSendableOutboundReplyParts(payload);
|
|
5038
|
+
if (!reply.hasText) {
|
|
5039
|
+
if (reply.hasMedia) params.runtime.log?.(`feishu[${params.accountId ?? "default"}]: comment reply ignored media-only payload for comment=${params.commentId}`);
|
|
5040
|
+
return noVisibleFeishuReplyDelivery;
|
|
5041
|
+
}
|
|
5042
|
+
const chunks = core.channel.text.chunkTextWithMode(reply.text, textChunkLimit, chunkMode);
|
|
5043
|
+
const results = [];
|
|
5044
|
+
const acceptedChunks = [];
|
|
5045
|
+
for (const chunk of chunks) try {
|
|
5046
|
+
const result = await deliverCommentThreadText(client, {
|
|
5047
|
+
file_token: params.fileToken,
|
|
5048
|
+
file_type: params.fileType,
|
|
5049
|
+
comment_id: params.commentId,
|
|
5050
|
+
content: chunk,
|
|
5051
|
+
is_whole_comment: params.isWholeComment
|
|
5052
|
+
});
|
|
5053
|
+
results.push({ messageId: result.delivery_mode === "reply_comment" ? result.reply_id : result.comment_id });
|
|
5054
|
+
acceptedChunks.push(chunk);
|
|
5055
|
+
} catch (error) {
|
|
5056
|
+
throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
|
|
5057
|
+
results,
|
|
5058
|
+
visibleReplySent: results.length > 0,
|
|
5059
|
+
content: acceptedChunks.join(""),
|
|
5060
|
+
kind: "text"
|
|
5061
|
+
}));
|
|
5062
|
+
}
|
|
5063
|
+
return createFeishuReplyDeliveryResult({
|
|
5064
|
+
results,
|
|
5065
|
+
visibleReplySent: results.length > 0,
|
|
5066
|
+
content: reply.text,
|
|
5067
|
+
kind: "text"
|
|
5068
|
+
});
|
|
5069
|
+
},
|
|
5070
|
+
onError: (err, info) => {
|
|
5071
|
+
params.runtime.error?.(`feishu[${params.accountId ?? "default"}]: comment dispatcher failed kind=${info.kind} comment=${params.commentId}: ${String(err)}`);
|
|
5072
|
+
}
|
|
3959
5073
|
},
|
|
3960
|
-
onCleanup: () => {
|
|
3961
|
-
typingReaction.cleanup();
|
|
3962
|
-
}
|
|
3963
|
-
});
|
|
3964
|
-
return {
|
|
3965
|
-
dispatcher,
|
|
3966
|
-
replyOptions,
|
|
3967
|
-
markDispatchIdle,
|
|
3968
|
-
markRunComplete,
|
|
3969
5074
|
startTypingReaction: typingReaction.start,
|
|
3970
5075
|
cleanupTypingReaction: typingReaction.cleanup
|
|
3971
5076
|
};
|
|
@@ -4125,18 +5230,18 @@ async function fetchDriveComments(params) {
|
|
|
4125
5230
|
}
|
|
4126
5231
|
async function requestFeishuOpenApi(params) {
|
|
4127
5232
|
const formatErrorDetails = (error) => {
|
|
4128
|
-
if (!isRecord$
|
|
4129
|
-
const response = isRecord$
|
|
4130
|
-
const responseData = isRecord$
|
|
5233
|
+
if (!isRecord$2(error)) return typeof error === "string" ? error : JSON.stringify(error);
|
|
5234
|
+
const response = isRecord$2(error.response) ? error.response : void 0;
|
|
5235
|
+
const responseData = isRecord$2(response?.data) ? response?.data : void 0;
|
|
4131
5236
|
return safeJsonStringify({
|
|
4132
5237
|
message: typeof error.message === "string" ? error.message : typeof error === "string" ? error : JSON.stringify(error),
|
|
4133
|
-
code: readString(error.code),
|
|
4134
|
-
method: readString(isRecord$
|
|
4135
|
-
url: readString(isRecord$
|
|
5238
|
+
code: readString$1(error.code),
|
|
5239
|
+
method: readString$1(isRecord$2(error.config) ? error.config.method : void 0),
|
|
5240
|
+
url: readString$1(isRecord$2(error.config) ? error.config.url : void 0),
|
|
4136
5241
|
http_status: typeof response?.status === "number" ? response.status : void 0,
|
|
4137
|
-
feishu_code: typeof responseData?.code === "number" ? responseData.code : readString(responseData?.code),
|
|
4138
|
-
feishu_msg: readString(responseData?.msg),
|
|
4139
|
-
feishu_log_id: readString(responseData?.log_id)
|
|
5242
|
+
feishu_code: typeof responseData?.code === "number" ? responseData.code : readString$1(responseData?.code),
|
|
5243
|
+
feishu_msg: readString$1(responseData?.msg),
|
|
5244
|
+
feishu_log_id: readString$1(responseData?.log_id)
|
|
4140
5245
|
});
|
|
4141
5246
|
};
|
|
4142
5247
|
const result = await raceWithTimeoutAndAbort(params.client.request({
|
|
@@ -4192,7 +5297,7 @@ async function resolveCommentReplyContext(params) {
|
|
|
4192
5297
|
createTime: typeof params.reply.create_time === "number" ? params.reply.create_time : void 0,
|
|
4193
5298
|
isBotAuthored: typeof userId === "string" && normalizedBotOpenIds.has(userId),
|
|
4194
5299
|
content: await resolveParsedCommentContent({
|
|
4195
|
-
elements: isRecord$
|
|
5300
|
+
elements: isRecord$2(params.reply.content) ? params.reply.content.elements : void 0,
|
|
4196
5301
|
botOpenIds: params.botOpenIds,
|
|
4197
5302
|
currentDocument: params.currentDocument,
|
|
4198
5303
|
client: params.client,
|
|
@@ -4364,7 +5469,7 @@ async function fetchDriveCommentContext(params) {
|
|
|
4364
5469
|
const rootWholeReply = comment.reply_list?.replies?.[0];
|
|
4365
5470
|
const normalizedBotOpenIds = new Set(Array.from(params.botOpenIds ?? []).map((botId) => normalizeString(botId)).filter((botId) => Boolean(botId)));
|
|
4366
5471
|
const content = await resolveParsedCommentContent({
|
|
4367
|
-
elements: isRecord$
|
|
5472
|
+
elements: isRecord$2(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
|
|
4368
5473
|
botOpenIds: params.botOpenIds,
|
|
4369
5474
|
currentDocument,
|
|
4370
5475
|
client: params.client,
|
|
@@ -4471,11 +5576,14 @@ async function resolveDriveCommentEventCore(params) {
|
|
|
4471
5576
|
logger?.(`feishu[${accountId}]: unsupported drive comment notice type ${noticeType}`);
|
|
4472
5577
|
return null;
|
|
4473
5578
|
}
|
|
4474
|
-
|
|
5579
|
+
const configuredBotOpenId = botOpenId?.trim();
|
|
5580
|
+
const eventRecipientBotOpenId = event.notice_meta?.to_user_id?.open_id?.trim();
|
|
5581
|
+
const effectiveBotOpenId = configuredBotOpenId || (event.is_mentioned === true ? eventRecipientBotOpenId : void 0);
|
|
5582
|
+
if (!effectiveBotOpenId) {
|
|
4475
5583
|
logger?.(`feishu[${accountId}]: skipping drive comment notice because bot open_id is unavailable event=${eventId}`);
|
|
4476
5584
|
return null;
|
|
4477
5585
|
}
|
|
4478
|
-
if (senderId ===
|
|
5586
|
+
if (senderId === effectiveBotOpenId) {
|
|
4479
5587
|
logger?.(`feishu[${accountId}]: ignoring self-authored drive comment notice event=${eventId} sender=${senderId}`);
|
|
4480
5588
|
return null;
|
|
4481
5589
|
}
|
|
@@ -4488,7 +5596,7 @@ async function resolveDriveCommentEventCore(params) {
|
|
|
4488
5596
|
fileType,
|
|
4489
5597
|
commentId,
|
|
4490
5598
|
replyId,
|
|
4491
|
-
botOpenIds: [
|
|
5599
|
+
botOpenIds: [effectiveBotOpenId, event.notice_meta?.to_user_id?.open_id],
|
|
4492
5600
|
timeoutMs: verificationTimeoutMs,
|
|
4493
5601
|
logger,
|
|
4494
5602
|
accountId,
|
|
@@ -4510,32 +5618,32 @@ async function resolveDriveCommentEventCore(params) {
|
|
|
4510
5618
|
};
|
|
4511
5619
|
}
|
|
4512
5620
|
function parseFeishuDriveCommentNoticeEventPayload(value) {
|
|
4513
|
-
if (!isRecord$
|
|
5621
|
+
if (!isRecord$2(value) || !isRecord$2(value.notice_meta)) return null;
|
|
4514
5622
|
const noticeMeta = value.notice_meta;
|
|
4515
|
-
const fromUserId = isRecord$
|
|
4516
|
-
const toUserId = isRecord$
|
|
5623
|
+
const fromUserId = isRecord$2(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
|
|
5624
|
+
const toUserId = isRecord$2(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
|
|
4517
5625
|
return {
|
|
4518
|
-
comment_id: readString(value.comment_id),
|
|
4519
|
-
event_id: readString(value.event_id),
|
|
5626
|
+
comment_id: readString$1(value.comment_id),
|
|
5627
|
+
event_id: readString$1(value.event_id),
|
|
4520
5628
|
is_mentioned: asBoolean(value.is_mentioned),
|
|
4521
5629
|
notice_meta: {
|
|
4522
|
-
file_token: readString(noticeMeta.file_token),
|
|
4523
|
-
file_type: readString(noticeMeta.file_type),
|
|
5630
|
+
file_token: readString$1(noticeMeta.file_token),
|
|
5631
|
+
file_type: readString$1(noticeMeta.file_type),
|
|
4524
5632
|
from_user_id: fromUserId ? {
|
|
4525
|
-
open_id: readString(fromUserId.open_id),
|
|
4526
|
-
user_id: readString(fromUserId.user_id),
|
|
4527
|
-
union_id: readString(fromUserId.union_id)
|
|
5633
|
+
open_id: readString$1(fromUserId.open_id),
|
|
5634
|
+
user_id: readString$1(fromUserId.user_id),
|
|
5635
|
+
union_id: readString$1(fromUserId.union_id)
|
|
4528
5636
|
} : void 0,
|
|
4529
|
-
notice_type: readString(noticeMeta.notice_type),
|
|
5637
|
+
notice_type: readString$1(noticeMeta.notice_type),
|
|
4530
5638
|
to_user_id: toUserId ? {
|
|
4531
|
-
open_id: readString(toUserId.open_id),
|
|
4532
|
-
user_id: readString(toUserId.user_id),
|
|
4533
|
-
union_id: readString(toUserId.union_id)
|
|
5639
|
+
open_id: readString$1(toUserId.open_id),
|
|
5640
|
+
user_id: readString$1(toUserId.user_id),
|
|
5641
|
+
union_id: readString$1(toUserId.union_id)
|
|
4534
5642
|
} : void 0
|
|
4535
5643
|
},
|
|
4536
|
-
reply_id: readString(value.reply_id),
|
|
4537
|
-
timestamp: readString(value.timestamp),
|
|
4538
|
-
type: readString(value.type)
|
|
5644
|
+
reply_id: readString$1(value.reply_id),
|
|
5645
|
+
timestamp: readString$1(value.timestamp),
|
|
5646
|
+
type: readString$1(value.type)
|
|
4539
5647
|
};
|
|
4540
5648
|
}
|
|
4541
5649
|
async function resolveDriveCommentEventTurn(params) {
|
|
@@ -4744,31 +5852,51 @@ async function handleFeishuCommentEvent(params) {
|
|
|
4744
5852
|
fileToken: turn.fileToken
|
|
4745
5853
|
});
|
|
4746
5854
|
const bodyForAgent = `[message_id: ${turn.messageId}]\n${turn.prompt}`;
|
|
4747
|
-
const
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
5855
|
+
const rawBody = turn.targetReplyText ?? turn.rootCommentText ?? turn.prompt;
|
|
5856
|
+
const conversationLabel = turn.documentTitle ? `Feishu comment · ${turn.documentTitle}` : "Feishu comment";
|
|
5857
|
+
const ctxPayload = buildChannelInboundEventContext({
|
|
5858
|
+
channel: "feishu",
|
|
5859
|
+
accountId: route.accountId,
|
|
5860
|
+
surface: "feishu-comment",
|
|
5861
|
+
messageId: turn.messageId,
|
|
5862
|
+
timestamp: parseTimestampMs(turn.timestamp),
|
|
5863
|
+
from: `feishu:${turn.senderId}`,
|
|
5864
|
+
sender: {
|
|
5865
|
+
id: turn.senderId,
|
|
5866
|
+
name: turn.senderId
|
|
5867
|
+
},
|
|
5868
|
+
conversation: {
|
|
5869
|
+
kind: "direct",
|
|
5870
|
+
id: commentTarget,
|
|
5871
|
+
label: conversationLabel
|
|
5872
|
+
},
|
|
5873
|
+
route: {
|
|
5874
|
+
agentId: route.agentId,
|
|
5875
|
+
dmScope: route.dmScope,
|
|
5876
|
+
accountId: route.accountId,
|
|
5877
|
+
routeSessionKey: commentSessionKey,
|
|
5878
|
+
dispatchSessionKey: commentSessionKey
|
|
5879
|
+
},
|
|
5880
|
+
reply: {
|
|
5881
|
+
to: commentTarget,
|
|
5882
|
+
originatingTo: commentTarget,
|
|
5883
|
+
messageThreadId: turn.replyId
|
|
5884
|
+
},
|
|
5885
|
+
message: {
|
|
5886
|
+
body: bodyForAgent,
|
|
5887
|
+
bodyForAgent,
|
|
5888
|
+
rawBody,
|
|
5889
|
+
commandBody: rawBody
|
|
5890
|
+
},
|
|
5891
|
+
access: {
|
|
5892
|
+
commands: { authorized: false },
|
|
5893
|
+
mentions: {
|
|
5894
|
+
canDetectMention: true,
|
|
5895
|
+
wasMentioned: turn.isMentioned ?? false
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
4769
5898
|
});
|
|
4770
|
-
const
|
|
4771
|
-
const { dispatcher, replyOptions, markDispatchIdle, markRunComplete, cleanupTypingReaction } = createFeishuCommentReplyDispatcher({
|
|
5899
|
+
const { dispatcherOptions, delivery, cleanupTypingReaction } = createFeishuCommentReplyDispatcher({
|
|
4772
5900
|
cfg: effectiveCfg,
|
|
4773
5901
|
agentId: route.agentId,
|
|
4774
5902
|
runtime,
|
|
@@ -4779,7 +5907,6 @@ async function handleFeishuCommentEvent(params) {
|
|
|
4779
5907
|
replyId: turn.replyId,
|
|
4780
5908
|
isWholeComment: turn.isWholeComment
|
|
4781
5909
|
});
|
|
4782
|
-
let dispatchSettledBeforeStart = false;
|
|
4783
5910
|
try {
|
|
4784
5911
|
log(`feishu[${account.accountId}]: dispatching drive comment to agent (session=${commentSessionKey} comment=${turn.commentId} type=${turn.noticeType})`);
|
|
4785
5912
|
const turnResult = await core.channel.inbound.run({
|
|
@@ -4796,34 +5923,20 @@ async function handleFeishuCommentEvent(params) {
|
|
|
4796
5923
|
raw: turn
|
|
4797
5924
|
}),
|
|
4798
5925
|
resolveTurn: () => ({
|
|
5926
|
+
cfg: effectiveCfg,
|
|
4799
5927
|
channel: "feishu",
|
|
4800
5928
|
accountId: route.accountId,
|
|
4801
|
-
|
|
4802
|
-
|
|
5929
|
+
route: {
|
|
5930
|
+
agentId: route.agentId,
|
|
5931
|
+
sessionKey: commentSessionKey
|
|
5932
|
+
},
|
|
4803
5933
|
ctxPayload,
|
|
4804
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
4805
5934
|
record: { onRecordError: (err) => {
|
|
4806
5935
|
error(`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`);
|
|
4807
5936
|
} },
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
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
|
-
})
|
|
5937
|
+
dispatcherOptions,
|
|
5938
|
+
delivery,
|
|
5939
|
+
...params.turnAdoptionLifecycle ? { replyOptions: bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) } : {}
|
|
4827
5940
|
})
|
|
4828
5941
|
}
|
|
4829
5942
|
});
|
|
@@ -4836,10 +5949,6 @@ async function handleFeishuCommentEvent(params) {
|
|
|
4836
5949
|
};
|
|
4837
5950
|
log(`feishu[${account.accountId}]: drive comment dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final}, session=${commentSessionKey})`);
|
|
4838
5951
|
} finally {
|
|
4839
|
-
if (!dispatchSettledBeforeStart) {
|
|
4840
|
-
markRunComplete();
|
|
4841
|
-
markDispatchIdle();
|
|
4842
|
-
}
|
|
4843
5952
|
cleanupTypingReaction();
|
|
4844
5953
|
}
|
|
4845
5954
|
}
|
|
@@ -4914,52 +6023,40 @@ function createFeishuDriveCommentNoticeHandler(params) {
|
|
|
4914
6023
|
});
|
|
4915
6024
|
if (!fireAndForget) await promise;
|
|
4916
6025
|
};
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
if (
|
|
4921
|
-
|
|
6026
|
+
const handleNotice = async (data, turnAdoptionLifecycle) => {
|
|
6027
|
+
const event = parseFeishuDriveCommentNoticeEventPayload(data);
|
|
6028
|
+
if (!event) {
|
|
6029
|
+
if (turnAdoptionLifecycle) throw new FeishuIngressPermanentError("invalid-event", "Feishu durable comment event payload is malformed.");
|
|
6030
|
+
error(`feishu[${accountId}]: ignoring malformed drive comment notice payload`);
|
|
6031
|
+
return;
|
|
6032
|
+
}
|
|
6033
|
+
log(`feishu[${accountId}]: received drive comment notice event=${event.event_id ?? "unknown"} type=${event.notice_meta?.notice_type ?? "unknown"} file=${event.notice_meta?.file_type ?? "unknown"}:${event.notice_meta?.file_token ?? "unknown"} comment=${event.comment_id ?? "unknown"} reply=${event.reply_id ?? "none"} from=${event.notice_meta?.from_user_id?.open_id ?? "unknown"} mentioned=${event.is_mentioned === true ? "yes" : "no"}`);
|
|
6034
|
+
await enqueue(buildCommentNoticeQueueKey(event), async () => {
|
|
6035
|
+
if (turnAdoptionLifecycle?.abortSignal.aborted) {
|
|
6036
|
+
await turnAdoptionLifecycle.onAbandoned();
|
|
4922
6037
|
return;
|
|
4923
6038
|
}
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
if (claim.kind === "duplicate") {
|
|
4934
|
-
log(`feishu[${accountId}]: dropping duplicate comment event ${syntheticMessageId}`);
|
|
4935
|
-
return;
|
|
4936
|
-
}
|
|
4937
|
-
if (claim.kind === "inflight") {
|
|
4938
|
-
log(`feishu[${accountId}]: dropping in-flight comment event ${syntheticMessageId}`);
|
|
4939
|
-
return;
|
|
4940
|
-
}
|
|
4941
|
-
processingClaim = claim.kind === "claimed" ? claim.handle : void 0;
|
|
4942
|
-
}
|
|
4943
|
-
log(`feishu[${accountId}]: received drive comment notice event=${event.event_id ?? "unknown"} type=${event.notice_meta?.notice_type ?? "unknown"} file=${event.notice_meta?.file_type ?? "unknown"}:${event.notice_meta?.file_token ?? "unknown"} comment=${event.comment_id ?? "unknown"} reply=${event.reply_id ?? "none"} from=${event.notice_meta?.from_user_id?.open_id ?? "unknown"} mentioned=${event.is_mentioned === true ? "yes" : "no"}`);
|
|
4944
|
-
try {
|
|
4945
|
-
await enqueue(buildCommentNoticeQueueKey(event), async () => {
|
|
4946
|
-
await handleFeishuCommentEvent({
|
|
4947
|
-
cfg,
|
|
4948
|
-
accountId,
|
|
4949
|
-
event,
|
|
4950
|
-
botOpenId: getBotOpenId(accountId),
|
|
4951
|
-
runtime,
|
|
4952
|
-
abortSignal
|
|
4953
|
-
});
|
|
4954
|
-
});
|
|
4955
|
-
await processingClaim?.commit();
|
|
4956
|
-
} catch (err) {
|
|
4957
|
-
if (isFeishuRetryableSyntheticEventError(err)) processingClaim?.release({ error: err });
|
|
4958
|
-
else await processingClaim?.commit();
|
|
4959
|
-
throw err;
|
|
4960
|
-
}
|
|
6039
|
+
await handleFeishuCommentEvent({
|
|
6040
|
+
cfg,
|
|
6041
|
+
accountId,
|
|
6042
|
+
event,
|
|
6043
|
+
botOpenId: getBotOpenId(accountId),
|
|
6044
|
+
runtime,
|
|
6045
|
+
abortSignal,
|
|
6046
|
+
turnAdoptionLifecycle
|
|
6047
|
+
});
|
|
4961
6048
|
});
|
|
4962
6049
|
};
|
|
6050
|
+
return async (data) => {
|
|
6051
|
+
const ingressLifecycle = params.resolveIngressLifecycle?.(data);
|
|
6052
|
+
if (!ingressLifecycle) {
|
|
6053
|
+
await runFeishuHandler(async () => await handleNotice(data));
|
|
6054
|
+
return;
|
|
6055
|
+
}
|
|
6056
|
+
const { lifecycle, settle } = buildFeishuFlushIngressLifecycle([{ lifecycle: ingressLifecycle }]);
|
|
6057
|
+
await handleNotice(data, lifecycle);
|
|
6058
|
+
await settle();
|
|
6059
|
+
};
|
|
4963
6060
|
}
|
|
4964
6061
|
//#endregion
|
|
4965
6062
|
//#region extensions/feishu/src/monitor.message-handler.ts
|
|
@@ -5020,7 +6117,7 @@ function resolveFeishuDebounceMentions(params) {
|
|
|
5020
6117
|
const botMentions = merged.filter((mention) => mention.id.open_id?.trim() === normalizedBotOpenId);
|
|
5021
6118
|
return botMentions.length > 0 ? botMentions : void 0;
|
|
5022
6119
|
}
|
|
5023
|
-
function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, runtime, chatHistories, fireAndForget, handleMessage, resolveDebounceText: resolveText, hasProcessedMessage, getBotOpenId = () => void 0, getBotName = () => void 0, resolveSequentialKey = ({ accountId: accountIdLocal, event }) => `feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`, statusSink }) {
|
|
6120
|
+
function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, runtime, chatHistories, fireAndForget, handleMessage, resolveDebounceText: resolveText, hasProcessedMessage, getBotOpenId = () => void 0, getBotName = () => void 0, resolveSequentialKey = ({ accountId: accountIdLocal, event }) => `feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`, statusSink, resolveIngressLifecycle }) {
|
|
5024
6121
|
const inboundDebounceMs = channelRuntime.debounce.resolveInboundDebounceMs({
|
|
5025
6122
|
cfg,
|
|
5026
6123
|
channel: "feishu"
|
|
@@ -5030,25 +6127,32 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
|
|
|
5030
6127
|
const enqueue = createSequentialQueue({ onTaskTimeout: (key, timeoutMs) => {
|
|
5031
6128
|
log(`feishu[${accountId}]: per-chat task exceeded ${timeoutMs}ms cap (key=${key}); evicting from queue so later same-key messages can proceed (#70133)`);
|
|
5032
6129
|
} });
|
|
5033
|
-
const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim) => {
|
|
6130
|
+
const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim, turnAdoptionLifecycle) => {
|
|
5034
6131
|
const sequentialKey = resolveSequentialKey({
|
|
5035
6132
|
accountId,
|
|
5036
6133
|
event,
|
|
5037
6134
|
botOpenId: getBotOpenId(accountId),
|
|
5038
6135
|
botName: getBotName(accountId)
|
|
5039
6136
|
});
|
|
5040
|
-
const task = () =>
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
6137
|
+
const task = async () => {
|
|
6138
|
+
if (turnAdoptionLifecycle?.abortSignal.aborted) {
|
|
6139
|
+
await turnAdoptionLifecycle.onAbandoned();
|
|
6140
|
+
return;
|
|
6141
|
+
}
|
|
6142
|
+
await handleMessage({
|
|
6143
|
+
cfg,
|
|
6144
|
+
event,
|
|
6145
|
+
botOpenId: getBotOpenId(accountId),
|
|
6146
|
+
botName: getBotName(accountId),
|
|
6147
|
+
runtime,
|
|
6148
|
+
channelRuntime,
|
|
6149
|
+
chatHistories,
|
|
6150
|
+
accountId,
|
|
6151
|
+
processingClaim,
|
|
6152
|
+
messageDedupeKey,
|
|
6153
|
+
turnAdoptionLifecycle
|
|
6154
|
+
});
|
|
6155
|
+
};
|
|
5052
6156
|
await enqueue(sequentialKey, task);
|
|
5053
6157
|
};
|
|
5054
6158
|
const resolveSenderDebounceId = (event) => {
|
|
@@ -5088,45 +6192,80 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
|
|
|
5088
6192
|
return Boolean(text) && !channelRuntime.commands.isControlCommandMessage(text, cfg);
|
|
5089
6193
|
},
|
|
5090
6194
|
onFlush: async (entries) => {
|
|
5091
|
-
const
|
|
6195
|
+
const activeEntries = entries.filter((entry) => !entry.abandoned);
|
|
6196
|
+
const last = activeEntries.at(-1);
|
|
5092
6197
|
if (!last) return;
|
|
5093
|
-
|
|
5094
|
-
|
|
6198
|
+
const { lifecycle, settle } = buildFeishuFlushIngressLifecycle(activeEntries.map((entry) => ({
|
|
6199
|
+
lifecycle: entry.turnAdoptionLifecycle,
|
|
6200
|
+
replayClaim: entry.processingClaim
|
|
6201
|
+
})), { onReplayCommitError: (err) => error(`feishu[${accountId}]: failed to commit logical replay guard: ${String(err)}`) });
|
|
6202
|
+
if (lifecycle?.abortSignal.aborted) {
|
|
6203
|
+
await lifecycle.onAbandoned();
|
|
5095
6204
|
return;
|
|
5096
6205
|
}
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
...dispatchEntry.event,
|
|
5111
|
-
message: {
|
|
5112
|
-
...dispatchEntry.event.message,
|
|
5113
|
-
...combinedText.trim() ? {
|
|
5114
|
-
message_type: "text",
|
|
5115
|
-
content: JSON.stringify({ text: combinedText })
|
|
5116
|
-
} : {},
|
|
5117
|
-
mentions: mergedMentions ?? dispatchEntry.event.message.mentions
|
|
6206
|
+
try {
|
|
6207
|
+
if (activeEntries.length === 1) {
|
|
6208
|
+
await dispatchFeishuMessage(last.event, resolveFeishuMessageDedupeKey(last.event), last.processingClaim, lifecycle);
|
|
6209
|
+
await settle();
|
|
6210
|
+
return;
|
|
6211
|
+
}
|
|
6212
|
+
const dedupedEntries = dedupeFeishuDebounceEntriesByDedupeKey(activeEntries);
|
|
6213
|
+
const freshEntries = [];
|
|
6214
|
+
for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry.event), accountId, log)) freshEntries.push(entry);
|
|
6215
|
+
const dispatchEntry = freshEntries.at(-1);
|
|
6216
|
+
if (!dispatchEntry) {
|
|
6217
|
+
await settle();
|
|
6218
|
+
return;
|
|
5118
6219
|
}
|
|
5119
|
-
|
|
6220
|
+
const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry.event);
|
|
6221
|
+
if (!lifecycle) await recordSuppressedMessageIds(dedupedEntries, dispatchDedupeKey);
|
|
6222
|
+
const combinedText = freshEntries.map((entry) => resolveDebounceText(entry.event)).filter(Boolean).join("\n");
|
|
6223
|
+
const mergedMentions = resolveFeishuDebounceMentions({
|
|
6224
|
+
entries: freshEntries.map((entry) => entry.event),
|
|
6225
|
+
botOpenId: getBotOpenId(accountId)
|
|
6226
|
+
});
|
|
6227
|
+
await dispatchFeishuMessage({
|
|
6228
|
+
...dispatchEntry.event,
|
|
6229
|
+
message: {
|
|
6230
|
+
...dispatchEntry.event.message,
|
|
6231
|
+
...combinedText.trim() ? {
|
|
6232
|
+
message_type: "text",
|
|
6233
|
+
content: JSON.stringify({ text: combinedText })
|
|
6234
|
+
} : {},
|
|
6235
|
+
mentions: mergedMentions ?? dispatchEntry.event.message.mentions
|
|
6236
|
+
}
|
|
6237
|
+
}, dispatchDedupeKey, dispatchEntry.processingClaim, lifecycle);
|
|
6238
|
+
await settle();
|
|
6239
|
+
} catch (err) {
|
|
6240
|
+
await lifecycle?.onAbandoned();
|
|
6241
|
+
throw err;
|
|
6242
|
+
}
|
|
5120
6243
|
},
|
|
5121
6244
|
onError: (err, entries) => {
|
|
5122
|
-
for (const entry of entries)
|
|
6245
|
+
for (const entry of entries) {
|
|
6246
|
+
entry.processingClaim?.release({ error: err });
|
|
6247
|
+
try {
|
|
6248
|
+
Promise.resolve(entry.turnAdoptionLifecycle?.onAbandoned()).catch((abandonError) => {
|
|
6249
|
+
error(`feishu[${accountId}]: failed to abandon durable ingress after debounce error: ${String(abandonError)}`);
|
|
6250
|
+
});
|
|
6251
|
+
} catch (abandonError) {
|
|
6252
|
+
error(`feishu[${accountId}]: failed to abandon durable ingress after debounce error: ${String(abandonError)}`);
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
5123
6255
|
error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
|
|
5124
6256
|
}
|
|
5125
6257
|
});
|
|
5126
6258
|
return async (data) => {
|
|
6259
|
+
const turnAdoptionLifecycle = resolveIngressLifecycle?.(data);
|
|
6260
|
+
const completeSuppressedIngress = async () => {
|
|
6261
|
+
if (!turnAdoptionLifecycle) return;
|
|
6262
|
+
turnAdoptionLifecycle.onAdoptionFinalizing();
|
|
6263
|
+
await turnAdoptionLifecycle.onAdopted();
|
|
6264
|
+
};
|
|
5127
6265
|
statusSink?.({ lastEventAt: Date.now() });
|
|
5128
6266
|
const event = parseFeishuMessageEventPayload(data);
|
|
5129
6267
|
if (!event) {
|
|
6268
|
+
if (turnAdoptionLifecycle) throw new FeishuIngressPermanentError("invalid-event", "Feishu durable message event payload is malformed.");
|
|
5130
6269
|
error(`feishu[${accountId}]: ignoring malformed message event payload`);
|
|
5131
6270
|
return;
|
|
5132
6271
|
}
|
|
@@ -5135,6 +6274,7 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
|
|
|
5135
6274
|
const senderOpenId = event.sender.sender_id.open_id?.trim();
|
|
5136
6275
|
if (botOpenId && senderOpenId === botOpenId) {
|
|
5137
6276
|
log(`feishu[${accountId}]: dropping self-authored message ${messageId ?? "unknown"}`);
|
|
6277
|
+
await completeSuppressedIngress();
|
|
5138
6278
|
return;
|
|
5139
6279
|
}
|
|
5140
6280
|
const claim = await claimUnprocessedFeishuMessage({
|
|
@@ -5144,14 +6284,31 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
|
|
|
5144
6284
|
});
|
|
5145
6285
|
if (claim.kind === "duplicate" || claim.kind === "inflight") {
|
|
5146
6286
|
log(`feishu[${accountId}]: dropping ${claim.kind} event for message ${messageId}`);
|
|
6287
|
+
await completeSuppressedIngress();
|
|
5147
6288
|
return;
|
|
5148
6289
|
}
|
|
6290
|
+
const debounceEntry = {
|
|
6291
|
+
event,
|
|
6292
|
+
...claim.kind === "claimed" ? { processingClaim: claim.handle } : {},
|
|
6293
|
+
...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {}
|
|
6294
|
+
};
|
|
6295
|
+
if (claim.kind === "claimed" && turnAdoptionLifecycle) turnAdoptionLifecycle.registerAbandonHandler?.(() => {
|
|
6296
|
+
debounceEntry.abandoned = true;
|
|
6297
|
+
claim.handle.release({ error: /* @__PURE__ */ new Error("feishu-ingress-abandoned-before-flush") });
|
|
6298
|
+
});
|
|
5149
6299
|
const processMessage = async () => {
|
|
5150
|
-
await inboundDebouncer.enqueue(
|
|
5151
|
-
event,
|
|
5152
|
-
...claim.kind === "claimed" ? { processingClaim: claim.handle } : {}
|
|
5153
|
-
});
|
|
6300
|
+
await inboundDebouncer.enqueue(debounceEntry);
|
|
5154
6301
|
};
|
|
6302
|
+
if (turnAdoptionLifecycle) try {
|
|
6303
|
+
await processMessage();
|
|
6304
|
+
return { kind: "deferred" };
|
|
6305
|
+
} catch (err) {
|
|
6306
|
+
if (claim.kind === "claimed") claim.handle.release({ error: err });
|
|
6307
|
+
return {
|
|
6308
|
+
kind: "failed-retryable",
|
|
6309
|
+
error: err
|
|
6310
|
+
};
|
|
6311
|
+
}
|
|
5155
6312
|
if (fireAndForget) {
|
|
5156
6313
|
processMessage().catch((err) => {
|
|
5157
6314
|
if (claim.kind === "claimed") claim.handle.release({ error: err });
|
|
@@ -5261,7 +6418,7 @@ function waitForFeishuWsCycleEnd(params) {
|
|
|
5261
6418
|
params.terminalError.then(finish);
|
|
5262
6419
|
});
|
|
5263
6420
|
}
|
|
5264
|
-
async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher, statusSink }) {
|
|
6421
|
+
async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher, setSocketTerminator, statusSink }) {
|
|
5265
6422
|
const log = runtime?.log ?? console.log;
|
|
5266
6423
|
const error = runtime?.error ?? console.error;
|
|
5267
6424
|
let attempt = 0;
|
|
@@ -5302,6 +6459,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
|
|
|
5302
6459
|
onReconnected: publishWsConnected,
|
|
5303
6460
|
onReconnecting: publishWsReconnecting
|
|
5304
6461
|
});
|
|
6462
|
+
setSocketTerminator?.(() => wsClient?.close({ force: true }));
|
|
5305
6463
|
if (abortSignal?.aborted) {
|
|
5306
6464
|
cleanupFeishuWsClient({
|
|
5307
6465
|
accountId,
|
|
@@ -5327,6 +6485,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
|
|
|
5327
6485
|
error,
|
|
5328
6486
|
clearIdentity: true
|
|
5329
6487
|
});
|
|
6488
|
+
setSocketTerminator?.(void 0);
|
|
5330
6489
|
return;
|
|
5331
6490
|
}
|
|
5332
6491
|
cleanupFeishuWsClient({
|
|
@@ -5335,6 +6494,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
|
|
|
5335
6494
|
error,
|
|
5336
6495
|
clearIdentity: false
|
|
5337
6496
|
});
|
|
6497
|
+
setSocketTerminator?.(void 0);
|
|
5338
6498
|
if (abortSignal?.aborted) break;
|
|
5339
6499
|
statusSink?.({
|
|
5340
6500
|
connected: false,
|
|
@@ -5351,6 +6511,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
|
|
|
5351
6511
|
error,
|
|
5352
6512
|
clearIdentity: false
|
|
5353
6513
|
});
|
|
6514
|
+
setSocketTerminator?.(void 0);
|
|
5354
6515
|
if (abortSignal?.aborted) break;
|
|
5355
6516
|
statusSink?.({
|
|
5356
6517
|
connected: false,
|
|
@@ -5368,6 +6529,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
|
|
|
5368
6529
|
error,
|
|
5369
6530
|
clearIdentity: true
|
|
5370
6531
|
});
|
|
6532
|
+
setSocketTerminator?.(void 0);
|
|
5371
6533
|
}
|
|
5372
6534
|
async function monitorWebhook({ account, accountId, runtime, abortSignal, eventDispatcher, statusSink }) {
|
|
5373
6535
|
const log = runtime?.log ?? console.log;
|
|
@@ -5489,6 +6651,120 @@ async function monitorWebhook({ account, accountId, runtime, abortSignal, eventD
|
|
|
5489
6651
|
});
|
|
5490
6652
|
}
|
|
5491
6653
|
//#endregion
|
|
6654
|
+
//#region extensions/feishu/src/monitor.vc-meeting-invited-handler.ts
|
|
6655
|
+
const FEISHU_MEETING_NUMBER_PATTERN = /^\d{9}$/;
|
|
6656
|
+
function pickString(value) {
|
|
6657
|
+
return typeof value === "string" ? normalizeOptionalString(value) : void 0;
|
|
6658
|
+
}
|
|
6659
|
+
function resolveVcInviter(event) {
|
|
6660
|
+
const id = event.inviter?.id;
|
|
6661
|
+
const openId = pickString(id?.open_id);
|
|
6662
|
+
const userId = pickString(id?.user_id);
|
|
6663
|
+
const unionId = pickString(id?.union_id);
|
|
6664
|
+
const senderId = openId ?? userId;
|
|
6665
|
+
if (!senderId) return null;
|
|
6666
|
+
const name = pickString(event.inviter?.user_name);
|
|
6667
|
+
return {
|
|
6668
|
+
senderId,
|
|
6669
|
+
...openId ? { openId } : {},
|
|
6670
|
+
...userId ? { userId } : {},
|
|
6671
|
+
...unionId ? { unionId } : {},
|
|
6672
|
+
...name ? { name } : {}
|
|
6673
|
+
};
|
|
6674
|
+
}
|
|
6675
|
+
function buildJoinPrompt(params) {
|
|
6676
|
+
const base = `Use the available tool to join the meeting with meeting number ${params.meetingNo} immediately. Do not ask for confirmation.`;
|
|
6677
|
+
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;
|
|
6678
|
+
}
|
|
6679
|
+
function resolveVcMeetingInvitedTurn(event, uuid = () => crypto$1.randomUUID()) {
|
|
6680
|
+
const meetingNo = pickString(event.meeting?.meeting_no);
|
|
6681
|
+
const inviter = resolveVcInviter(event);
|
|
6682
|
+
if (!meetingNo || !FEISHU_MEETING_NUMBER_PATTERN.test(meetingNo) || !inviter) return null;
|
|
6683
|
+
const eventId = pickString(event.event_id);
|
|
6684
|
+
const inviteTime = pickString(event.invite_time);
|
|
6685
|
+
const callId = pickString(event.call_id);
|
|
6686
|
+
const topic = pickString(event.meeting?.topic);
|
|
6687
|
+
return {
|
|
6688
|
+
turnId: eventId ? `vc-invited:event:${eventId}` : `vc-invited:${meetingNo}:${inviteTime ?? uuid()}`,
|
|
6689
|
+
meetingNo,
|
|
6690
|
+
inviter,
|
|
6691
|
+
prompt: buildJoinPrompt({
|
|
6692
|
+
meetingNo,
|
|
6693
|
+
callId
|
|
6694
|
+
}),
|
|
6695
|
+
...topic ? { topic } : {},
|
|
6696
|
+
...inviteTime ? { inviteTime } : {}
|
|
6697
|
+
};
|
|
6698
|
+
}
|
|
6699
|
+
function parseInviteTimestamp(value) {
|
|
6700
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
6701
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return Date.now();
|
|
6702
|
+
return parsed < 1e10 ? parsed * 1e3 : parsed;
|
|
6703
|
+
}
|
|
6704
|
+
function buildSyntheticMessageEvent(turn) {
|
|
6705
|
+
return {
|
|
6706
|
+
sender: { sender_id: {
|
|
6707
|
+
...turn.inviter.openId ? { open_id: turn.inviter.openId } : {},
|
|
6708
|
+
...turn.inviter.userId ? { user_id: turn.inviter.userId } : {},
|
|
6709
|
+
...turn.inviter.unionId ? { union_id: turn.inviter.unionId } : {}
|
|
6710
|
+
} },
|
|
6711
|
+
message: {
|
|
6712
|
+
message_id: turn.turnId,
|
|
6713
|
+
chat_id: turn.inviter.senderId,
|
|
6714
|
+
chat_type: "p2p",
|
|
6715
|
+
message_type: "text",
|
|
6716
|
+
content: JSON.stringify({ text: turn.prompt }),
|
|
6717
|
+
create_time: String(parseInviteTimestamp(turn.inviteTime)),
|
|
6718
|
+
suppress_reply_target: true
|
|
6719
|
+
}
|
|
6720
|
+
};
|
|
6721
|
+
}
|
|
6722
|
+
async function dispatchVcMeetingInvitedTurn(params) {
|
|
6723
|
+
params.runtime?.log?.(`feishu[${params.accountId}]: vc meeting invited, dispatching synthetic p2p message sender=${params.turn.inviter.senderId} meeting_no=${params.turn.meetingNo}`);
|
|
6724
|
+
const event = setFeishuSyntheticDirectPreDispatchTarget(buildSyntheticMessageEvent(params.turn), `user:${params.turn.inviter.senderId}`);
|
|
6725
|
+
await handleFeishuMessage({
|
|
6726
|
+
cfg: params.cfg,
|
|
6727
|
+
accountId: params.accountId,
|
|
6728
|
+
event,
|
|
6729
|
+
runtime: params.runtime,
|
|
6730
|
+
channelRuntime: params.channelRuntime
|
|
6731
|
+
});
|
|
6732
|
+
}
|
|
6733
|
+
function createFeishuVcMeetingInvitedHandler(params) {
|
|
6734
|
+
const { cfg, accountId, runtime, fireAndForget, autoJoin } = params;
|
|
6735
|
+
const log = runtime?.log ?? console.log;
|
|
6736
|
+
const error = runtime?.error ?? console.error;
|
|
6737
|
+
return async (data) => {
|
|
6738
|
+
if (!autoJoin) {
|
|
6739
|
+
log(`feishu[${accountId}]: ignoring vc meeting invite (vcAutoJoin=false)`);
|
|
6740
|
+
return;
|
|
6741
|
+
}
|
|
6742
|
+
try {
|
|
6743
|
+
const turn = resolveVcMeetingInvitedTurn(data);
|
|
6744
|
+
if (!turn) {
|
|
6745
|
+
log(`feishu[${accountId}]: vc meeting invited event has invalid meeting_no or inviter identity, skipping`);
|
|
6746
|
+
return;
|
|
6747
|
+
}
|
|
6748
|
+
const promise = dispatchVcMeetingInvitedTurn({
|
|
6749
|
+
cfg,
|
|
6750
|
+
accountId,
|
|
6751
|
+
runtime,
|
|
6752
|
+
channelRuntime: params.channelRuntime,
|
|
6753
|
+
turn
|
|
6754
|
+
});
|
|
6755
|
+
if (fireAndForget) {
|
|
6756
|
+
promise.catch((err) => {
|
|
6757
|
+
error(`feishu[${accountId}]: error handling vc meeting invited event: ${String(err)}`);
|
|
6758
|
+
});
|
|
6759
|
+
return;
|
|
6760
|
+
}
|
|
6761
|
+
await promise;
|
|
6762
|
+
} catch (err) {
|
|
6763
|
+
error(`feishu[${accountId}]: error handling vc meeting invited event: ${String(err)}`);
|
|
6764
|
+
}
|
|
6765
|
+
};
|
|
6766
|
+
}
|
|
6767
|
+
//#endregion
|
|
5492
6768
|
//#region extensions/feishu/src/sequential-key.ts
|
|
5493
6769
|
function getFeishuSequentialKey(params) {
|
|
5494
6770
|
const { accountId, event, botOpenId, botName } = params;
|
|
@@ -5561,41 +6837,41 @@ function normalizeFeishuChatType(value) {
|
|
|
5561
6837
|
return value === "group" || value === "topic_group" || value === "private" || value === "p2p" ? value : void 0;
|
|
5562
6838
|
}
|
|
5563
6839
|
function parseFeishuBotAddedEventPayload(value) {
|
|
5564
|
-
if (!isRecord$
|
|
6840
|
+
if (!isRecord$2(value) || !readString$1(value.chat_id) || !isRecord$2(value.operator_id)) return null;
|
|
5565
6841
|
return value;
|
|
5566
6842
|
}
|
|
5567
6843
|
function parseFeishuBotRemovedChatId(value) {
|
|
5568
|
-
if (!isRecord$
|
|
5569
|
-
return readString(value.chat_id) ?? null;
|
|
6844
|
+
if (!isRecord$2(value)) return null;
|
|
6845
|
+
return readString$1(value.chat_id) ?? null;
|
|
5570
6846
|
}
|
|
5571
6847
|
function firstString(...values) {
|
|
5572
6848
|
for (const value of values) {
|
|
5573
|
-
const trimmed = readString(value)?.trim();
|
|
6849
|
+
const trimmed = readString$1(value)?.trim();
|
|
5574
6850
|
if (trimmed) return trimmed;
|
|
5575
6851
|
}
|
|
5576
6852
|
}
|
|
5577
6853
|
function readFeishuIdentityField(value, field) {
|
|
5578
|
-
if (!isRecord$
|
|
6854
|
+
if (!isRecord$2(value)) return;
|
|
5579
6855
|
return firstString(value[field]);
|
|
5580
6856
|
}
|
|
5581
6857
|
function parseFeishuCardActionEventPayload(value) {
|
|
5582
|
-
if (!isRecord$
|
|
5583
|
-
const operator = isRecord$
|
|
6858
|
+
if (!isRecord$2(value)) return null;
|
|
6859
|
+
const operator = isRecord$2(value.operator) ? value.operator : {};
|
|
5584
6860
|
const action = value.action;
|
|
5585
|
-
const context = isRecord$
|
|
5586
|
-
if (!isRecord$
|
|
6861
|
+
const context = isRecord$2(value.context) ? value.context : {};
|
|
6862
|
+
if (!isRecord$2(action)) return null;
|
|
5587
6863
|
const operatorUserId = operator.user_id;
|
|
5588
|
-
const token = readString(value.token);
|
|
6864
|
+
const token = readString$1(value.token);
|
|
5589
6865
|
const openId = firstString(operator.open_id, readFeishuIdentityField(operatorUserId, "open_id"), value.open_id, context.open_id);
|
|
5590
6866
|
const userId = firstString(operator.user_id, readFeishuIdentityField(operatorUserId, "user_id"), value.user_id, context.user_id);
|
|
5591
6867
|
const unionId = firstString(operator.union_id, readFeishuIdentityField(operatorUserId, "union_id"));
|
|
5592
|
-
const tag = readString(action.tag);
|
|
6868
|
+
const tag = readString$1(action.tag);
|
|
5593
6869
|
const actionValue = action.value;
|
|
5594
6870
|
const openMessageId = firstString(context.open_message_id, value.open_message_id);
|
|
5595
6871
|
const contextOpenId = firstString(context.open_id, openId);
|
|
5596
6872
|
const contextUserId = firstString(context.user_id, userId);
|
|
5597
6873
|
const chatId = firstString(context.chat_id, context.open_chat_id);
|
|
5598
|
-
if (!token || !openId || !tag || !isRecord$
|
|
6874
|
+
if (!token || !openId || !tag || !isRecord$2(actionValue)) return null;
|
|
5599
6875
|
return {
|
|
5600
6876
|
operator: {
|
|
5601
6877
|
open_id: openId,
|
|
@@ -5647,6 +6923,7 @@ function registerEventHandlers(eventDispatcher, context) {
|
|
|
5647
6923
|
getBotOpenId: (id) => botOpenIds.get(id),
|
|
5648
6924
|
getBotName: (id) => botNames.get(id),
|
|
5649
6925
|
resolveSequentialKey: getFeishuSequentialKey,
|
|
6926
|
+
resolveIngressLifecycle: context.resolveIngressLifecycle,
|
|
5650
6927
|
...context.statusSink ? { statusSink: context.statusSink } : {}
|
|
5651
6928
|
}),
|
|
5652
6929
|
"im.message.message_read_v1": async () => {},
|
|
@@ -5674,7 +6951,16 @@ function registerEventHandlers(eventDispatcher, context) {
|
|
|
5674
6951
|
accountId,
|
|
5675
6952
|
runtime,
|
|
5676
6953
|
fireAndForget,
|
|
5677
|
-
abortSignal
|
|
6954
|
+
abortSignal,
|
|
6955
|
+
resolveIngressLifecycle: context.resolveIngressLifecycle
|
|
6956
|
+
}),
|
|
6957
|
+
"vc.bot.meeting_invited_v1": createFeishuVcMeetingInvitedHandler({
|
|
6958
|
+
cfg,
|
|
6959
|
+
accountId,
|
|
6960
|
+
runtime,
|
|
6961
|
+
fireAndForget,
|
|
6962
|
+
channelRuntime,
|
|
6963
|
+
autoJoin: context.vcAutoJoin
|
|
5678
6964
|
}),
|
|
5679
6965
|
"im.message.reaction.created_v1": async (data) => {
|
|
5680
6966
|
await runFeishuHandler({
|
|
@@ -5769,19 +7055,22 @@ async function monitorSingleAccount(params) {
|
|
|
5769
7055
|
const { accountId } = account;
|
|
5770
7056
|
const log = runtime?.log ?? console.log;
|
|
5771
7057
|
const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
|
|
5772
|
-
const
|
|
7058
|
+
const botIdentity = botOpenIdSource.kind === "prefetched" ? {
|
|
5773
7059
|
botOpenId: botOpenIdSource.botOpenId,
|
|
5774
|
-
botName: botOpenIdSource.botName
|
|
7060
|
+
botName: botOpenIdSource.botName,
|
|
7061
|
+
source: botOpenIdSource.source
|
|
5775
7062
|
} : await fetchBotIdentityForMonitor(account, {
|
|
5776
7063
|
runtime,
|
|
5777
7064
|
abortSignal
|
|
5778
|
-
})
|
|
7065
|
+
});
|
|
7066
|
+
const { botOpenId } = applyBotIdentityState(accountId, botIdentity);
|
|
5779
7067
|
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
|
5780
|
-
if (!botOpenId && !abortSignal?.aborted) startBotIdentityRecovery({
|
|
7068
|
+
if ((!botOpenId || botIdentity.source === "cache") && !abortSignal?.aborted) startBotIdentityRecovery({
|
|
5781
7069
|
account,
|
|
5782
7070
|
accountId,
|
|
5783
7071
|
runtime,
|
|
5784
|
-
abortSignal
|
|
7072
|
+
abortSignal,
|
|
7073
|
+
currentSource: botIdentity.source
|
|
5785
7074
|
});
|
|
5786
7075
|
const connectionMode = account.config.connectionMode ?? "websocket";
|
|
5787
7076
|
if (connectionMode === "webhook" && !account.verificationToken?.trim()) throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
|
|
@@ -5791,6 +7080,13 @@ async function monitorSingleAccount(params) {
|
|
|
5791
7080
|
let threadBindingManager;
|
|
5792
7081
|
try {
|
|
5793
7082
|
const eventDispatcher = createEventDispatcher(account);
|
|
7083
|
+
const durableIngress = typeof eventDispatcher.invoke === "function" ? createFeishuDurableIngress({
|
|
7084
|
+
accountId,
|
|
7085
|
+
dispatcher: eventDispatcher,
|
|
7086
|
+
...account.encryptKey ? { encryptKey: account.encryptKey } : {},
|
|
7087
|
+
runtime: runtime ?? {}
|
|
7088
|
+
}) : void 0;
|
|
7089
|
+
const durableEventDispatcher = durableIngress ? Object.assign(Object.create(eventDispatcher), { invoke: durableIngress.invoke }) : eventDispatcher;
|
|
5794
7090
|
const chatHistories = /* @__PURE__ */ new Map();
|
|
5795
7091
|
threadBindingManager = createFeishuThreadBindingManager({
|
|
5796
7092
|
accountId,
|
|
@@ -5803,25 +7099,33 @@ async function monitorSingleAccount(params) {
|
|
|
5803
7099
|
runtime,
|
|
5804
7100
|
chatHistories,
|
|
5805
7101
|
fireAndForget: params.fireAndForget ?? true,
|
|
7102
|
+
vcAutoJoin: account.config.vcAutoJoin === true,
|
|
5806
7103
|
abortSignal,
|
|
7104
|
+
...durableIngress ? { resolveIngressLifecycle: durableIngress.resolveLifecycle } : {},
|
|
5807
7105
|
...params.statusSink ? { statusSink: params.statusSink } : {}
|
|
5808
7106
|
});
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
7107
|
+
durableIngress?.start();
|
|
7108
|
+
try {
|
|
7109
|
+
if (connectionMode === "webhook") return await monitorWebhook({
|
|
7110
|
+
account,
|
|
7111
|
+
accountId,
|
|
7112
|
+
runtime,
|
|
7113
|
+
abortSignal,
|
|
7114
|
+
eventDispatcher: durableEventDispatcher,
|
|
7115
|
+
...params.statusSink ? { statusSink: params.statusSink } : {}
|
|
7116
|
+
});
|
|
7117
|
+
return await monitorWebSocket({
|
|
7118
|
+
account,
|
|
7119
|
+
accountId,
|
|
7120
|
+
runtime,
|
|
7121
|
+
abortSignal,
|
|
7122
|
+
eventDispatcher: durableEventDispatcher,
|
|
7123
|
+
...durableIngress ? { setSocketTerminator: durableIngress.setSocketTerminator } : {},
|
|
7124
|
+
...params.statusSink ? { statusSink: params.statusSink } : {}
|
|
7125
|
+
});
|
|
7126
|
+
} finally {
|
|
7127
|
+
await durableIngress?.stop();
|
|
7128
|
+
}
|
|
5825
7129
|
} finally {
|
|
5826
7130
|
threadBindingManager?.stop();
|
|
5827
7131
|
}
|