@openclaw/feishu 2026.5.2 → 2026.5.3-beta.2
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/accounts-Ba3-WP1z.js +423 -0
- package/dist/api.js +2280 -0
- package/dist/app-registration-B8qc1MCM.js +184 -0
- package/dist/audio-preflight.runtime-BPlzkO3l.js +7 -0
- package/dist/card-interaction-BfRLgvw_.js +96 -0
- package/dist/channel-CSD_Jt8I.js +1668 -0
- package/dist/channel-entry.js +22 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-DYsXcD36.js +700 -0
- package/dist/client-DBVoQL5w.js +157 -0
- package/dist/contract-api.js +9 -0
- package/dist/conversation-id-DWS3Ep2A.js +139 -0
- package/dist/directory.static-f3EeoRJd.js +44 -0
- package/dist/drive-C5eJLJr7.js +883 -0
- package/dist/index.js +68 -0
- package/dist/monitor-CT189QfR.js +60 -0
- package/dist/monitor.account-dJV2jO8C.js +4990 -0
- package/dist/monitor.state-DYM02ipp.js +100 -0
- package/dist/policy-D6c-wMPl.js +118 -0
- package/dist/probe-BNzzU_uR.js +149 -0
- package/dist/rolldown-runtime-DUslC3ob.js +14 -0
- package/dist/runtime-CG0DuRCy.js +8 -0
- package/dist/runtime-api.js +14 -0
- package/dist/secret-contract-Dm4Z_zQN.js +119 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/security-audit-DqJdocrN.js +11 -0
- package/dist/security-audit-shared-ByuMx9cJ.js +38 -0
- package/dist/security-contract-api.js +2 -0
- package/dist/send-DowxxbpH.js +1218 -0
- package/dist/session-conversation-B4nrW-vo.js +27 -0
- package/dist/session-key-api.js +2 -0
- package/dist/setup-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/subagent-hooks-C3UhPVLV.js +227 -0
- package/dist/subagent-hooks-api.js +23 -0
- package/dist/targets-JMFJRKSe.js +48 -0
- package/dist/thread-bindings-BmS6TLes.js +222 -0
- package/package.json +15 -6
- package/api.ts +0 -31
- package/channel-entry.ts +0 -20
- package/channel-plugin-api.ts +0 -1
- package/contract-api.ts +0 -16
- package/index.ts +0 -82
- package/runtime-api.ts +0 -55
- package/secret-contract-api.ts +0 -5
- package/security-contract-api.ts +0 -1
- package/session-key-api.ts +0 -1
- package/setup-api.ts +0 -3
- package/setup-entry.test.ts +0 -14
- package/setup-entry.ts +0 -13
- package/src/accounts.test.ts +0 -459
- package/src/accounts.ts +0 -326
- package/src/app-registration.ts +0 -331
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -25
- package/src/async.test.ts +0 -35
- package/src/async.ts +0 -104
- package/src/audio-preflight.runtime.ts +0 -9
- package/src/bitable.test.ts +0 -131
- package/src/bitable.ts +0 -762
- package/src/bot-content.ts +0 -474
- package/src/bot-group-name.test.ts +0 -108
- package/src/bot-runtime-api.ts +0 -12
- package/src/bot-sender-name.ts +0 -125
- package/src/bot.broadcast.test.ts +0 -463
- package/src/bot.card-action.test.ts +0 -577
- package/src/bot.checkBotMentioned.test.ts +0 -265
- package/src/bot.helpers.test.ts +0 -118
- package/src/bot.stripBotMention.test.ts +0 -126
- package/src/bot.test.ts +0 -3040
- package/src/bot.ts +0 -1559
- package/src/card-action.ts +0 -447
- package/src/card-interaction.test.ts +0 -129
- package/src/card-interaction.ts +0 -159
- package/src/card-test-helpers.ts +0 -47
- package/src/card-ux-approval.ts +0 -65
- package/src/card-ux-launcher.test.ts +0 -99
- package/src/card-ux-launcher.ts +0 -121
- package/src/card-ux-shared.ts +0 -33
- package/src/channel-runtime-api.ts +0 -16
- package/src/channel.runtime.ts +0 -47
- package/src/channel.test.ts +0 -959
- package/src/channel.ts +0 -1313
- package/src/chat-schema.ts +0 -25
- package/src/chat.test.ts +0 -196
- package/src/chat.ts +0 -188
- package/src/client.test.ts +0 -433
- package/src/client.ts +0 -290
- package/src/comment-dispatcher-runtime-api.ts +0 -6
- package/src/comment-dispatcher.test.ts +0 -169
- package/src/comment-dispatcher.ts +0 -107
- package/src/comment-handler-runtime-api.ts +0 -3
- package/src/comment-handler.test.ts +0 -486
- package/src/comment-handler.ts +0 -309
- package/src/comment-reaction.test.ts +0 -166
- package/src/comment-reaction.ts +0 -259
- package/src/comment-shared.test.ts +0 -182
- package/src/comment-shared.ts +0 -406
- package/src/comment-target.ts +0 -44
- package/src/config-schema.test.ts +0 -309
- package/src/config-schema.ts +0 -333
- package/src/conversation-id.test.ts +0 -18
- package/src/conversation-id.ts +0 -199
- package/src/dedup-runtime-api.ts +0 -1
- package/src/dedup.ts +0 -141
- package/src/directory.static.ts +0 -61
- package/src/directory.test.ts +0 -136
- package/src/directory.ts +0 -124
- package/src/doc-schema.ts +0 -182
- package/src/docx-batch-insert.test.ts +0 -91
- package/src/docx-batch-insert.ts +0 -223
- package/src/docx-color-text.ts +0 -154
- package/src/docx-table-ops.test.ts +0 -53
- package/src/docx-table-ops.ts +0 -316
- package/src/docx-types.ts +0 -38
- package/src/docx.account-selection.test.ts +0 -79
- package/src/docx.test.ts +0 -685
- package/src/docx.ts +0 -1616
- package/src/drive-schema.ts +0 -92
- package/src/drive.test.ts +0 -1219
- package/src/drive.ts +0 -829
- package/src/dynamic-agent.ts +0 -137
- package/src/event-types.ts +0 -45
- package/src/external-keys.test.ts +0 -20
- package/src/external-keys.ts +0 -19
- package/src/lifecycle.test-support.ts +0 -220
- package/src/media.test.ts +0 -900
- package/src/media.ts +0 -861
- package/src/mention-target.types.ts +0 -5
- package/src/mention.ts +0 -114
- package/src/message-action-contract.ts +0 -13
- package/src/monitor-state-runtime-api.ts +0 -7
- package/src/monitor-transport-runtime-api.ts +0 -7
- package/src/monitor.account.ts +0 -468
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +0 -219
- package/src/monitor.bot-identity.ts +0 -86
- package/src/monitor.bot-menu-handler.ts +0 -165
- package/src/monitor.bot-menu.lifecycle.test-support.ts +0 -224
- package/src/monitor.bot-menu.test.ts +0 -178
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +0 -264
- package/src/monitor.card-action.lifecycle.test-support.ts +0 -373
- package/src/monitor.cleanup.test.ts +0 -376
- package/src/monitor.comment-notice-handler.ts +0 -105
- package/src/monitor.comment.test.ts +0 -937
- package/src/monitor.comment.ts +0 -1386
- package/src/monitor.lifecycle.test.ts +0 -4
- package/src/monitor.message-handler.ts +0 -339
- package/src/monitor.reaction.lifecycle.test-support.ts +0 -68
- package/src/monitor.reaction.test.ts +0 -713
- package/src/monitor.startup.test.ts +0 -192
- package/src/monitor.startup.ts +0 -74
- package/src/monitor.state.defaults.test.ts +0 -46
- package/src/monitor.state.ts +0 -170
- package/src/monitor.synthetic-error.ts +0 -18
- package/src/monitor.test-mocks.ts +0 -45
- package/src/monitor.transport.ts +0 -424
- package/src/monitor.ts +0 -100
- package/src/monitor.webhook-e2e.test.ts +0 -272
- package/src/monitor.webhook-security.test.ts +0 -264
- package/src/monitor.webhook.test-helpers.ts +0 -116
- package/src/outbound-runtime-api.ts +0 -1
- package/src/outbound.test.ts +0 -935
- package/src/outbound.ts +0 -718
- package/src/perm-schema.ts +0 -52
- package/src/perm.ts +0 -170
- package/src/pins.ts +0 -108
- package/src/policy.test.ts +0 -334
- package/src/policy.ts +0 -236
- package/src/post.test.ts +0 -105
- package/src/post.ts +0 -275
- package/src/probe.test.ts +0 -275
- package/src/probe.ts +0 -166
- package/src/processing-claims.ts +0 -59
- package/src/qr-terminal.ts +0 -1
- package/src/reactions.ts +0 -123
- package/src/reasoning-preview.test.ts +0 -59
- package/src/reasoning-preview.ts +0 -20
- package/src/reply-dispatcher-runtime-api.ts +0 -7
- package/src/reply-dispatcher.test.ts +0 -1144
- package/src/reply-dispatcher.ts +0 -650
- package/src/runtime.ts +0 -9
- package/src/secret-contract.ts +0 -145
- package/src/secret-input.ts +0 -1
- package/src/security-audit-shared.ts +0 -69
- package/src/security-audit.test.ts +0 -61
- package/src/security-audit.ts +0 -1
- package/src/send-result.ts +0 -29
- package/src/send-target.test.ts +0 -80
- package/src/send-target.ts +0 -35
- package/src/send.reply-fallback.test.ts +0 -292
- package/src/send.test.ts +0 -550
- package/src/send.ts +0 -800
- package/src/sequential-key.test.ts +0 -72
- package/src/sequential-key.ts +0 -28
- package/src/sequential-queue.test.ts +0 -92
- package/src/sequential-queue.ts +0 -16
- package/src/session-conversation.ts +0 -42
- package/src/session-route.ts +0 -48
- package/src/setup-core.ts +0 -51
- package/src/setup-surface.test.ts +0 -174
- package/src/setup-surface.ts +0 -581
- package/src/streaming-card.test.ts +0 -190
- package/src/streaming-card.ts +0 -490
- package/src/subagent-hooks.test.ts +0 -603
- package/src/subagent-hooks.ts +0 -397
- package/src/targets.ts +0 -97
- package/src/test-support/lifecycle-test-support.ts +0 -453
- package/src/thread-bindings.test.ts +0 -143
- package/src/thread-bindings.ts +0 -330
- package/src/tool-account-routing.test.ts +0 -187
- package/src/tool-account.test.ts +0 -44
- package/src/tool-account.ts +0 -93
- package/src/tool-factory-test-harness.ts +0 -79
- package/src/tool-result.test.ts +0 -32
- package/src/tool-result.ts +0 -16
- package/src/tools-config.test.ts +0 -21
- package/src/tools-config.ts +0 -22
- package/src/types.ts +0 -104
- package/src/typing.test.ts +0 -144
- package/src/typing.ts +0 -214
- package/src/wiki-schema.ts +0 -55
- package/src/wiki.ts +0 -227
- package/subagent-hooks-api.ts +0 -31
- package/tsconfig.json +0 -16
package/src/bot.ts
DELETED
|
@@ -1,1559 +0,0 @@
|
|
|
1
|
-
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
2
|
-
import {
|
|
3
|
-
ensureConfiguredBindingRouteReady,
|
|
4
|
-
resolveConfiguredBindingRoute,
|
|
5
|
-
resolveRuntimeConversationBindingRoute,
|
|
6
|
-
} from "openclaw/plugin-sdk/conversation-runtime";
|
|
7
|
-
import { resolveAgentOutboundIdentity } from "openclaw/plugin-sdk/outbound-runtime";
|
|
8
|
-
import {
|
|
9
|
-
buildPendingHistoryContextFromMap,
|
|
10
|
-
clearHistoryEntriesIfEnabled,
|
|
11
|
-
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
12
|
-
recordPendingHistoryEntryIfEnabled,
|
|
13
|
-
type HistoryEntry,
|
|
14
|
-
} from "openclaw/plugin-sdk/reply-history";
|
|
15
|
-
import {
|
|
16
|
-
resolveDefaultGroupPolicy,
|
|
17
|
-
resolveOpenProviderRuntimeGroupPolicy,
|
|
18
|
-
warnMissingProviderGroupPolicyFallbackOnce,
|
|
19
|
-
} from "openclaw/plugin-sdk/runtime-group-policy";
|
|
20
|
-
import { resolveOpenDmAllowlistAccess } from "openclaw/plugin-sdk/security-runtime";
|
|
21
|
-
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
22
|
-
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
23
|
-
import {
|
|
24
|
-
checkBotMentioned,
|
|
25
|
-
normalizeFeishuCommandProbeBody,
|
|
26
|
-
normalizeMentions,
|
|
27
|
-
parseMergeForwardContent,
|
|
28
|
-
parseMessageContent,
|
|
29
|
-
resolveFeishuGroupSession,
|
|
30
|
-
resolveFeishuMediaList,
|
|
31
|
-
toMessageResourceType,
|
|
32
|
-
} from "./bot-content.js";
|
|
33
|
-
import {
|
|
34
|
-
buildAgentMediaPayload,
|
|
35
|
-
evaluateSupplementalContextVisibility,
|
|
36
|
-
filterSupplementalContextItems,
|
|
37
|
-
normalizeAgentId,
|
|
38
|
-
resolveChannelContextVisibilityMode,
|
|
39
|
-
} from "./bot-runtime-api.js";
|
|
40
|
-
import type { ClawdbotConfig, RuntimeEnv } from "./bot-runtime-api.js";
|
|
41
|
-
import { type FeishuPermissionError, resolveFeishuSenderName } from "./bot-sender-name.js";
|
|
42
|
-
import { getChatInfo } from "./chat.js";
|
|
43
|
-
import { createFeishuClient } from "./client.js";
|
|
44
|
-
import { finalizeFeishuMessageProcessing, tryRecordMessagePersistent } from "./dedup.js";
|
|
45
|
-
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
|
46
|
-
import { extractMentionTargets, isMentionForwardRequest } from "./mention.js";
|
|
47
|
-
import {
|
|
48
|
-
hasExplicitFeishuGroupConfig,
|
|
49
|
-
isFeishuGroupAllowed,
|
|
50
|
-
resolveFeishuAllowlistMatch,
|
|
51
|
-
resolveFeishuGroupConfig,
|
|
52
|
-
resolveFeishuReplyPolicy,
|
|
53
|
-
} from "./policy.js";
|
|
54
|
-
import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js";
|
|
55
|
-
import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
|
|
56
|
-
import { getFeishuRuntime } from "./runtime.js";
|
|
57
|
-
import { getMessageFeishu, listFeishuThreadMessages, sendMessageFeishu } from "./send.js";
|
|
58
|
-
export type { FeishuBotAddedEvent, FeishuMessageEvent } from "./event-types.js";
|
|
59
|
-
import type { FeishuMessageEvent } from "./event-types.js";
|
|
60
|
-
import {
|
|
61
|
-
isFeishuGroupChatType,
|
|
62
|
-
type FeishuMessageContext,
|
|
63
|
-
type FeishuMediaInfo,
|
|
64
|
-
type FeishuMessageInfo,
|
|
65
|
-
type ResolvedFeishuAccount,
|
|
66
|
-
} from "./types.js";
|
|
67
|
-
import type { DynamicAgentCreationConfig } from "./types.js";
|
|
68
|
-
|
|
69
|
-
export { toMessageResourceType } from "./bot-content.js";
|
|
70
|
-
|
|
71
|
-
// Cache permission errors to avoid spamming the user with repeated notifications.
|
|
72
|
-
// Key: appId or "default", Value: timestamp of last notification
|
|
73
|
-
const permissionErrorNotifiedAt = new Map<string, number>();
|
|
74
|
-
const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
75
|
-
|
|
76
|
-
const groupNameCache = new Map<string, { name: string; expiresAt: number }>();
|
|
77
|
-
const GROUP_NAME_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
78
|
-
const GROUP_NAME_CACHE_MAX_SIZE = 500; // hard cap
|
|
79
|
-
|
|
80
|
-
function evictGroupNameCache(): void {
|
|
81
|
-
const now = Date.now();
|
|
82
|
-
for (const [key, val] of groupNameCache) {
|
|
83
|
-
if (val.expiresAt <= now) {
|
|
84
|
-
groupNameCache.delete(key);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (groupNameCache.size > GROUP_NAME_CACHE_MAX_SIZE) {
|
|
89
|
-
const excess = groupNameCache.size - GROUP_NAME_CACHE_MAX_SIZE;
|
|
90
|
-
let removed = 0;
|
|
91
|
-
for (const key of groupNameCache.keys()) {
|
|
92
|
-
if (removed >= excess) {
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
groupNameCache.delete(key);
|
|
96
|
-
removed++;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function setCacheEntry(key: string, value: { name: string; expiresAt: number }): void {
|
|
102
|
-
groupNameCache.delete(key);
|
|
103
|
-
groupNameCache.set(key, value);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function clearGroupNameCache(): void {
|
|
107
|
-
groupNameCache.clear();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function resolveGroupName(params: {
|
|
111
|
-
account: ResolvedFeishuAccount;
|
|
112
|
-
chatId: string;
|
|
113
|
-
log: (...args: unknown[]) => void;
|
|
114
|
-
}): Promise<string | undefined> {
|
|
115
|
-
const { account, chatId, log } = params;
|
|
116
|
-
if (!account.configured) {
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const cacheKey = `${account.accountId}:${chatId}`;
|
|
121
|
-
|
|
122
|
-
const cached = groupNameCache.get(cacheKey);
|
|
123
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
124
|
-
return cached.name || undefined;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const client = createFeishuClient(account);
|
|
129
|
-
const chatInfo = await getChatInfo(client, chatId);
|
|
130
|
-
const name = chatInfo?.name?.trim();
|
|
131
|
-
if (name) {
|
|
132
|
-
setCacheEntry(cacheKey, {
|
|
133
|
-
name,
|
|
134
|
-
expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS,
|
|
135
|
-
});
|
|
136
|
-
} else {
|
|
137
|
-
setCacheEntry(cacheKey, {
|
|
138
|
-
name: "",
|
|
139
|
-
expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
} catch (err) {
|
|
143
|
-
log(`feishu[${account.accountId}]: getChatInfo failed for ${chatId}: ${String(err)}`);
|
|
144
|
-
setCacheEntry(cacheKey, {
|
|
145
|
-
name: "",
|
|
146
|
-
expiresAt: Date.now() + GROUP_NAME_CACHE_TTL_MS,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const result = groupNameCache.get(cacheKey)?.name || undefined;
|
|
151
|
-
evictGroupNameCache();
|
|
152
|
-
|
|
153
|
-
return result;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function resolveFeishuAudioPreflightTranscript(params: {
|
|
157
|
-
cfg: ClawdbotConfig;
|
|
158
|
-
mediaList: FeishuMediaInfo[];
|
|
159
|
-
content: string;
|
|
160
|
-
chatType: "direct" | "group";
|
|
161
|
-
log: (msg: string) => void;
|
|
162
|
-
}): Promise<string | undefined> {
|
|
163
|
-
if (params.content.trim() !== "<media:audio>") {
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
const audioMedia = params.mediaList.filter((media) => media.contentType?.startsWith("audio/"));
|
|
167
|
-
if (audioMedia.length === 0) {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
const { transcribeFirstAudio } = await import("./audio-preflight.runtime.js");
|
|
173
|
-
return await transcribeFirstAudio({
|
|
174
|
-
ctx: {
|
|
175
|
-
MediaPaths: audioMedia.map((media) => media.path),
|
|
176
|
-
MediaTypes: audioMedia.map((media) => media.contentType).filter(Boolean) as string[],
|
|
177
|
-
ChatType: params.chatType,
|
|
178
|
-
},
|
|
179
|
-
cfg: params.cfg,
|
|
180
|
-
});
|
|
181
|
-
} catch (err) {
|
|
182
|
-
params.log(`feishu: audio preflight transcription failed: ${String(err)}`);
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// --- Broadcast support ---
|
|
188
|
-
// Resolve broadcast agent list for a given peer (group) ID.
|
|
189
|
-
// Returns null if no broadcast config exists or the peer is not in the broadcast list.
|
|
190
|
-
export function resolveBroadcastAgents(cfg: ClawdbotConfig, peerId: string): string[] | null {
|
|
191
|
-
const broadcast = (cfg as Record<string, unknown>).broadcast;
|
|
192
|
-
if (!broadcast || typeof broadcast !== "object") {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
const agents = (broadcast as Record<string, unknown>)[peerId];
|
|
196
|
-
if (!Array.isArray(agents) || agents.length === 0) {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
return agents as string[];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Build a session key for a broadcast target agent by replacing the agent ID prefix.
|
|
203
|
-
// Session keys follow the format: agent:<agentId>:<channel>:<peerKind>:<peerId>
|
|
204
|
-
export function buildBroadcastSessionKey(
|
|
205
|
-
baseSessionKey: string,
|
|
206
|
-
originalAgentId: string,
|
|
207
|
-
targetAgentId: string,
|
|
208
|
-
): string {
|
|
209
|
-
const prefix = `agent:${originalAgentId}:`;
|
|
210
|
-
if (baseSessionKey.startsWith(prefix)) {
|
|
211
|
-
return `agent:${targetAgentId}:${baseSessionKey.slice(prefix.length)}`;
|
|
212
|
-
}
|
|
213
|
-
return baseSessionKey;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Build media payload for inbound context.
|
|
218
|
-
* Similar to Discord's buildDiscordMediaPayload().
|
|
219
|
-
*/
|
|
220
|
-
export function parseFeishuMessageEvent(
|
|
221
|
-
event: FeishuMessageEvent,
|
|
222
|
-
botOpenId?: string,
|
|
223
|
-
_botName?: string,
|
|
224
|
-
): FeishuMessageContext {
|
|
225
|
-
const rawContent = parseMessageContent(event.message.content, event.message.message_type);
|
|
226
|
-
const mentionedBot = checkBotMentioned(event, botOpenId);
|
|
227
|
-
const hasAnyMention = (event.message.mentions?.length ?? 0) > 0;
|
|
228
|
-
// Strip the bot's own mention so slash commands like @Bot /help retain
|
|
229
|
-
// the leading /. This applies in both p2p *and* group contexts — the
|
|
230
|
-
// mentionedBot flag already captures whether the bot was addressed, so
|
|
231
|
-
// keeping the mention tag in content only breaks command detection (#35994).
|
|
232
|
-
// Non-bot mentions (e.g. mention-forward targets) are still normalized to <at> tags.
|
|
233
|
-
const content = normalizeMentions(rawContent, event.message.mentions, botOpenId);
|
|
234
|
-
const senderOpenId = event.sender.sender_id.open_id?.trim();
|
|
235
|
-
const senderUserId = event.sender.sender_id.user_id?.trim();
|
|
236
|
-
const senderFallbackId = senderOpenId || senderUserId || "";
|
|
237
|
-
|
|
238
|
-
const ctx: FeishuMessageContext = {
|
|
239
|
-
chatId: event.message.chat_id,
|
|
240
|
-
messageId: event.message.message_id,
|
|
241
|
-
replyTargetMessageId: event.message.reply_target_message_id?.trim() || undefined,
|
|
242
|
-
suppressReplyTarget: event.message.suppress_reply_target === true,
|
|
243
|
-
senderId: senderUserId || senderOpenId || "",
|
|
244
|
-
// Keep the historical field name, but fall back to user_id when open_id is unavailable
|
|
245
|
-
// (common in some mobile app deliveries).
|
|
246
|
-
senderOpenId: senderFallbackId,
|
|
247
|
-
chatType: event.message.chat_type,
|
|
248
|
-
mentionedBot,
|
|
249
|
-
hasAnyMention,
|
|
250
|
-
rootId: event.message.root_id || undefined,
|
|
251
|
-
parentId: event.message.parent_id || undefined,
|
|
252
|
-
threadId: event.message.thread_id || undefined,
|
|
253
|
-
content,
|
|
254
|
-
contentType: event.message.message_type,
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// Detect mention forward request: message mentions bot + at least one other user
|
|
258
|
-
if (isMentionForwardRequest(event, botOpenId)) {
|
|
259
|
-
const mentionTargets = extractMentionTargets(event, botOpenId);
|
|
260
|
-
if (mentionTargets.length > 0) {
|
|
261
|
-
ctx.mentionTargets = mentionTargets;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return ctx;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function buildFeishuAgentBody(params: {
|
|
269
|
-
ctx: Pick<
|
|
270
|
-
FeishuMessageContext,
|
|
271
|
-
"content" | "senderName" | "senderOpenId" | "mentionTargets" | "messageId" | "hasAnyMention"
|
|
272
|
-
>;
|
|
273
|
-
quotedContent?: string;
|
|
274
|
-
permissionErrorForAgent?: FeishuPermissionError;
|
|
275
|
-
botOpenId?: string;
|
|
276
|
-
}): string {
|
|
277
|
-
const { ctx, quotedContent, permissionErrorForAgent, botOpenId } = params;
|
|
278
|
-
let messageBody = ctx.content;
|
|
279
|
-
if (quotedContent) {
|
|
280
|
-
messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// DMs already have per-sender sessions, but this label still improves attribution.
|
|
284
|
-
const speaker = ctx.senderName ?? ctx.senderOpenId;
|
|
285
|
-
messageBody = `${speaker}: ${messageBody}`;
|
|
286
|
-
|
|
287
|
-
if (ctx.hasAnyMention) {
|
|
288
|
-
const botIdHint = botOpenId?.trim();
|
|
289
|
-
messageBody +=
|
|
290
|
-
`\n\n[System: The content may include mention tags in the form <at user_id="...">name</at>. ` +
|
|
291
|
-
`Treat these as real mentions of Feishu entities (users or bots).]`;
|
|
292
|
-
if (botIdHint) {
|
|
293
|
-
messageBody += `\n[System: If user_id is "${botIdHint}", that mention refers to you.]`;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
|
298
|
-
const targetNames = ctx.mentionTargets.map((t) => t.name).join(", ");
|
|
299
|
-
messageBody += `\n\n[System: Your reply will automatically @mention: ${targetNames}. Do not write @xxx yourself.]`;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Keep message_id on its own line so shared message-id hint stripping can parse it reliably.
|
|
303
|
-
messageBody = `[message_id: ${ctx.messageId}]\n${messageBody}`;
|
|
304
|
-
|
|
305
|
-
if (permissionErrorForAgent) {
|
|
306
|
-
const grantUrl = permissionErrorForAgent.grantUrl ?? "";
|
|
307
|
-
messageBody += `\n\n[System: The bot encountered a Feishu API permission error. Please inform the user about this issue and provide the permission grant URL for the admin to authorize. Permission grant URL: ${grantUrl}]`;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return messageBody;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function isFetchedGroupContextSenderAllowed(params: {
|
|
314
|
-
isGroup: boolean;
|
|
315
|
-
allowFrom: Array<string | number>;
|
|
316
|
-
senderId?: string;
|
|
317
|
-
senderType?: string;
|
|
318
|
-
}): boolean {
|
|
319
|
-
if (!params.isGroup || params.allowFrom.length === 0) {
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
if (params.senderType === "app") {
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
const senderId = params.senderId?.trim();
|
|
326
|
-
const senderAllowed =
|
|
327
|
-
!!senderId &&
|
|
328
|
-
isFeishuGroupAllowed({
|
|
329
|
-
groupPolicy: "allowlist",
|
|
330
|
-
allowFrom: params.allowFrom,
|
|
331
|
-
senderId,
|
|
332
|
-
senderName: undefined,
|
|
333
|
-
});
|
|
334
|
-
return senderAllowed;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function shouldIncludeFetchedGroupContextMessage(params: {
|
|
338
|
-
isGroup: boolean;
|
|
339
|
-
allowFrom: Array<string | number>;
|
|
340
|
-
mode: "all" | "allowlist" | "allowlist_quote";
|
|
341
|
-
kind: "quote" | "thread" | "history";
|
|
342
|
-
senderId?: string;
|
|
343
|
-
senderType?: string;
|
|
344
|
-
}): boolean {
|
|
345
|
-
const senderAllowed = isFetchedGroupContextSenderAllowed({
|
|
346
|
-
isGroup: params.isGroup,
|
|
347
|
-
allowFrom: params.allowFrom,
|
|
348
|
-
senderId: params.senderId,
|
|
349
|
-
senderType: params.senderType,
|
|
350
|
-
});
|
|
351
|
-
return evaluateSupplementalContextVisibility({
|
|
352
|
-
mode: params.mode,
|
|
353
|
-
kind: params.kind,
|
|
354
|
-
senderAllowed,
|
|
355
|
-
}).include;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function filterFetchedGroupContextMessages<
|
|
359
|
-
T extends Pick<FeishuMessageInfo, "senderId" | "senderType">,
|
|
360
|
-
>(
|
|
361
|
-
messages: readonly T[],
|
|
362
|
-
params: {
|
|
363
|
-
isGroup: boolean;
|
|
364
|
-
allowFrom: Array<string | number>;
|
|
365
|
-
mode: "all" | "allowlist" | "allowlist_quote";
|
|
366
|
-
kind: "quote" | "thread" | "history";
|
|
367
|
-
},
|
|
368
|
-
): T[] {
|
|
369
|
-
return filterSupplementalContextItems({
|
|
370
|
-
items: messages,
|
|
371
|
-
mode: params.mode,
|
|
372
|
-
kind: params.kind,
|
|
373
|
-
isSenderAllowed: (message) =>
|
|
374
|
-
isFetchedGroupContextSenderAllowed({
|
|
375
|
-
isGroup: params.isGroup,
|
|
376
|
-
allowFrom: params.allowFrom,
|
|
377
|
-
senderId: message.senderId,
|
|
378
|
-
senderType: message.senderType,
|
|
379
|
-
}),
|
|
380
|
-
}).items;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export async function handleFeishuMessage(params: {
|
|
384
|
-
cfg: ClawdbotConfig;
|
|
385
|
-
event: FeishuMessageEvent;
|
|
386
|
-
botOpenId?: string;
|
|
387
|
-
botName?: string;
|
|
388
|
-
runtime?: RuntimeEnv;
|
|
389
|
-
chatHistories?: Map<string, HistoryEntry[]>;
|
|
390
|
-
accountId?: string;
|
|
391
|
-
processingClaimHeld?: boolean;
|
|
392
|
-
}): Promise<void> {
|
|
393
|
-
const {
|
|
394
|
-
cfg,
|
|
395
|
-
event,
|
|
396
|
-
botOpenId,
|
|
397
|
-
botName,
|
|
398
|
-
runtime,
|
|
399
|
-
chatHistories,
|
|
400
|
-
accountId,
|
|
401
|
-
processingClaimHeld = false,
|
|
402
|
-
} = params;
|
|
403
|
-
|
|
404
|
-
// Resolve account with merged config
|
|
405
|
-
const account = resolveFeishuRuntimeAccount({ cfg, accountId });
|
|
406
|
-
const feishuCfg = account.config;
|
|
407
|
-
|
|
408
|
-
const log = runtime?.log ?? console.log;
|
|
409
|
-
const error = runtime?.error ?? console.error;
|
|
410
|
-
|
|
411
|
-
const messageId = event.message.message_id;
|
|
412
|
-
if (
|
|
413
|
-
!(await finalizeFeishuMessageProcessing({
|
|
414
|
-
messageId,
|
|
415
|
-
namespace: account.accountId,
|
|
416
|
-
log,
|
|
417
|
-
claimHeld: processingClaimHeld,
|
|
418
|
-
}))
|
|
419
|
-
) {
|
|
420
|
-
log(`feishu: skipping duplicate message ${messageId}`);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
|
|
425
|
-
const isGroup = isFeishuGroupChatType(ctx.chatType);
|
|
426
|
-
const isDirect = !isGroup;
|
|
427
|
-
const senderUserId = normalizeOptionalString(event.sender.sender_id.user_id);
|
|
428
|
-
|
|
429
|
-
// Handle merge_forward messages: fetch full message via API then expand sub-messages
|
|
430
|
-
if (event.message.message_type === "merge_forward") {
|
|
431
|
-
log(
|
|
432
|
-
`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`,
|
|
433
|
-
);
|
|
434
|
-
try {
|
|
435
|
-
// Websocket event doesn't include sub-messages, need to fetch via API
|
|
436
|
-
// The API returns all sub-messages in the items array
|
|
437
|
-
const client = createFeishuClient(account);
|
|
438
|
-
const response = (await client.im.message.get({
|
|
439
|
-
path: { message_id: event.message.message_id },
|
|
440
|
-
})) as { code?: number; data?: { items?: unknown[] } };
|
|
441
|
-
|
|
442
|
-
if (response.code === 0 && response.data?.items && response.data.items.length > 0) {
|
|
443
|
-
log(
|
|
444
|
-
`feishu[${account.accountId}]: merge_forward API returned ${response.data.items.length} items`,
|
|
445
|
-
);
|
|
446
|
-
const expandedContent = parseMergeForwardContent({
|
|
447
|
-
content: JSON.stringify(response.data.items),
|
|
448
|
-
log,
|
|
449
|
-
});
|
|
450
|
-
ctx = { ...ctx, content: expandedContent };
|
|
451
|
-
} else {
|
|
452
|
-
log(`feishu[${account.accountId}]: merge_forward API returned no items`);
|
|
453
|
-
ctx = { ...ctx, content: "[Merged and Forwarded Message - could not fetch]" };
|
|
454
|
-
}
|
|
455
|
-
} catch (err) {
|
|
456
|
-
log(`feishu[${account.accountId}]: merge_forward fetch failed: ${String(err)}`);
|
|
457
|
-
ctx = { ...ctx, content: "[Merged and Forwarded Message - fetch error]" };
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
|
|
462
|
-
// Optimization: skip if disabled to save API quota (Feishu free tier limit).
|
|
463
|
-
let permissionErrorForAgent: FeishuPermissionError | undefined;
|
|
464
|
-
if (feishuCfg?.resolveSenderNames ?? true) {
|
|
465
|
-
const senderResult = await resolveFeishuSenderName({
|
|
466
|
-
account,
|
|
467
|
-
senderId: ctx.senderOpenId,
|
|
468
|
-
log,
|
|
469
|
-
});
|
|
470
|
-
if (senderResult.name) {
|
|
471
|
-
ctx = { ...ctx, senderName: senderResult.name };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Track permission error to inform agent later (with cooldown to avoid repetition)
|
|
475
|
-
if (senderResult.permissionError) {
|
|
476
|
-
const appKey = account.appId ?? "default";
|
|
477
|
-
const now = Date.now();
|
|
478
|
-
const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
|
|
479
|
-
|
|
480
|
-
if (now - lastNotified > PERMISSION_ERROR_COOLDOWN_MS) {
|
|
481
|
-
permissionErrorNotifiedAt.set(appKey, now);
|
|
482
|
-
permissionErrorForAgent = senderResult.permissionError;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
log(
|
|
488
|
-
`feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`,
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
// Log mention targets if detected
|
|
492
|
-
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
|
493
|
-
const names = ctx.mentionTargets.map((t) => t.name).join(", ");
|
|
494
|
-
log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const historyLimit = Math.max(
|
|
498
|
-
0,
|
|
499
|
-
feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
|
|
500
|
-
);
|
|
501
|
-
const groupConfig = isGroup
|
|
502
|
-
? resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId })
|
|
503
|
-
: undefined;
|
|
504
|
-
const effectiveGroupSenderAllowFrom = isGroup
|
|
505
|
-
? (groupConfig?.allowFrom?.length ?? 0) > 0
|
|
506
|
-
? (groupConfig?.allowFrom ?? [])
|
|
507
|
-
: (feishuCfg?.groupSenderAllowFrom ?? [])
|
|
508
|
-
: [];
|
|
509
|
-
const groupSession = isGroup
|
|
510
|
-
? resolveFeishuGroupSession({
|
|
511
|
-
chatId: ctx.chatId,
|
|
512
|
-
senderOpenId: ctx.senderOpenId,
|
|
513
|
-
messageId: ctx.messageId,
|
|
514
|
-
rootId: ctx.rootId,
|
|
515
|
-
threadId: ctx.threadId,
|
|
516
|
-
chatType: ctx.chatType,
|
|
517
|
-
groupConfig,
|
|
518
|
-
feishuCfg,
|
|
519
|
-
})
|
|
520
|
-
: null;
|
|
521
|
-
const groupHistoryKey = isGroup ? (groupSession?.peerId ?? ctx.chatId) : undefined;
|
|
522
|
-
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
|
523
|
-
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
|
524
|
-
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
525
|
-
const rawBroadcastAgents = isGroup ? resolveBroadcastAgents(cfg, ctx.chatId) : null;
|
|
526
|
-
const broadcastAgents = rawBroadcastAgents
|
|
527
|
-
? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))]
|
|
528
|
-
: null;
|
|
529
|
-
|
|
530
|
-
// Parse message create_time early so every downstream consumer (pending
|
|
531
|
-
// history, inbound payload, etc.) uses the original authoring timestamp
|
|
532
|
-
// instead of the delivery/processing time. Feishu uses a millisecond
|
|
533
|
-
// epoch string; fall back to Date.now() only when the field is absent.
|
|
534
|
-
const messageCreateTimeMs = event.message.create_time
|
|
535
|
-
? Number.parseInt(event.message.create_time, 10)
|
|
536
|
-
: Date.now();
|
|
537
|
-
|
|
538
|
-
let requireMention = false; // DMs never require mention; groups may override below
|
|
539
|
-
if (isGroup) {
|
|
540
|
-
if (groupConfig?.enabled === false) {
|
|
541
|
-
log(`feishu[${account.accountId}]: group ${ctx.chatId} is disabled`);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
545
|
-
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
|
|
546
|
-
providerConfigPresent: cfg.channels?.feishu !== undefined,
|
|
547
|
-
groupPolicy: feishuCfg?.groupPolicy,
|
|
548
|
-
defaultGroupPolicy,
|
|
549
|
-
});
|
|
550
|
-
warnMissingProviderGroupPolicyFallbackOnce({
|
|
551
|
-
providerMissingFallbackApplied,
|
|
552
|
-
providerKey: "feishu",
|
|
553
|
-
accountId: account.accountId,
|
|
554
|
-
log,
|
|
555
|
-
});
|
|
556
|
-
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
|
557
|
-
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
|
|
558
|
-
|
|
559
|
-
// A group explicitly configured under `channels.feishu.groups.<chat_id>` is
|
|
560
|
-
// treated as admitted in allowlist mode even when `groupAllowFrom` is empty.
|
|
561
|
-
// Wildcard defaults still configure matching groups, but they are not an
|
|
562
|
-
// admission signal by themselves.
|
|
563
|
-
const groupExplicitlyConfigured = hasExplicitFeishuGroupConfig({
|
|
564
|
-
cfg: feishuCfg,
|
|
565
|
-
groupId: ctx.chatId,
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
|
|
569
|
-
const groupAllowed =
|
|
570
|
-
groupPolicy !== "disabled" &&
|
|
571
|
-
(groupExplicitlyConfigured ||
|
|
572
|
-
isFeishuGroupAllowed({
|
|
573
|
-
groupPolicy,
|
|
574
|
-
allowFrom: groupAllowFrom,
|
|
575
|
-
senderId: ctx.chatId, // Check group ID, not sender ID
|
|
576
|
-
senderName: undefined,
|
|
577
|
-
}));
|
|
578
|
-
|
|
579
|
-
if (!groupAllowed) {
|
|
580
|
-
log(
|
|
581
|
-
`feishu[${account.accountId}]: group ${ctx.chatId} not in groupAllowFrom (groupPolicy=${groupPolicy})`,
|
|
582
|
-
);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Sender-level allowlist: per-group allowFrom takes precedence, then global groupSenderAllowFrom
|
|
587
|
-
if (effectiveGroupSenderAllowFrom.length > 0) {
|
|
588
|
-
const senderAllowed = isFeishuGroupAllowed({
|
|
589
|
-
groupPolicy: "allowlist",
|
|
590
|
-
allowFrom: effectiveGroupSenderAllowFrom,
|
|
591
|
-
senderId: ctx.senderOpenId,
|
|
592
|
-
senderIds: [senderUserId],
|
|
593
|
-
senderName: ctx.senderName,
|
|
594
|
-
});
|
|
595
|
-
if (!senderAllowed) {
|
|
596
|
-
log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
({ requireMention } = resolveFeishuReplyPolicy({
|
|
602
|
-
isDirectMessage: false,
|
|
603
|
-
cfg,
|
|
604
|
-
accountId: account.accountId,
|
|
605
|
-
groupId: ctx.chatId,
|
|
606
|
-
groupPolicy,
|
|
607
|
-
}));
|
|
608
|
-
|
|
609
|
-
if (requireMention && !ctx.mentionedBot) {
|
|
610
|
-
log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot`);
|
|
611
|
-
// Record to pending history for non-broadcast groups only. For broadcast groups,
|
|
612
|
-
// the mentioned handler's broadcast dispatch writes the turn directly into all
|
|
613
|
-
// agent sessions — buffering here would cause duplicate replay when this account
|
|
614
|
-
// later becomes active via buildPendingHistoryContextFromMap.
|
|
615
|
-
if (!broadcastAgents && chatHistories && groupHistoryKey) {
|
|
616
|
-
recordPendingHistoryEntryIfEnabled({
|
|
617
|
-
historyMap: chatHistories,
|
|
618
|
-
historyKey: groupHistoryKey,
|
|
619
|
-
limit: historyLimit,
|
|
620
|
-
entry: {
|
|
621
|
-
sender: ctx.senderOpenId,
|
|
622
|
-
body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
|
|
623
|
-
timestamp: messageCreateTimeMs,
|
|
624
|
-
messageId: ctx.messageId,
|
|
625
|
-
},
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
try {
|
|
633
|
-
const core = getFeishuRuntime();
|
|
634
|
-
const pairing = createChannelPairingController({
|
|
635
|
-
core,
|
|
636
|
-
channel: "feishu",
|
|
637
|
-
accountId: account.accountId,
|
|
638
|
-
});
|
|
639
|
-
const commandProbeBody = isGroup ? normalizeFeishuCommandProbeBody(ctx.content) : ctx.content;
|
|
640
|
-
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
|
|
641
|
-
commandProbeBody,
|
|
642
|
-
cfg,
|
|
643
|
-
);
|
|
644
|
-
const storeAllowFrom =
|
|
645
|
-
!isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open"
|
|
646
|
-
? await pairing.readAllowFromStore().catch(() => [])
|
|
647
|
-
: [];
|
|
648
|
-
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
649
|
-
const dmAllowed = resolveFeishuAllowlistMatch({
|
|
650
|
-
allowFrom: effectiveDmAllowFrom,
|
|
651
|
-
senderId: ctx.senderOpenId,
|
|
652
|
-
senderIds: [senderUserId],
|
|
653
|
-
senderName: ctx.senderName,
|
|
654
|
-
}).allowed;
|
|
655
|
-
|
|
656
|
-
const dmAccessAllowed =
|
|
657
|
-
dmPolicy === "open"
|
|
658
|
-
? resolveOpenDmAllowlistAccess({
|
|
659
|
-
effectiveAllowFrom: effectiveDmAllowFrom,
|
|
660
|
-
isSenderAllowed: (allowFrom) =>
|
|
661
|
-
resolveFeishuAllowlistMatch({
|
|
662
|
-
allowFrom,
|
|
663
|
-
senderId: ctx.senderOpenId,
|
|
664
|
-
senderIds: [senderUserId],
|
|
665
|
-
senderName: ctx.senderName,
|
|
666
|
-
}).allowed,
|
|
667
|
-
}).decision === "allow"
|
|
668
|
-
: dmAllowed;
|
|
669
|
-
|
|
670
|
-
if (isDirect && !dmAccessAllowed) {
|
|
671
|
-
if (dmPolicy === "pairing") {
|
|
672
|
-
await pairing.issueChallenge({
|
|
673
|
-
senderId: ctx.senderOpenId,
|
|
674
|
-
senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
|
675
|
-
meta: { name: ctx.senderName },
|
|
676
|
-
onCreated: () => {
|
|
677
|
-
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
|
678
|
-
},
|
|
679
|
-
sendPairingReply: async (text) => {
|
|
680
|
-
await sendMessageFeishu({
|
|
681
|
-
cfg,
|
|
682
|
-
to: `chat:${ctx.chatId}`,
|
|
683
|
-
text,
|
|
684
|
-
accountId: account.accountId,
|
|
685
|
-
});
|
|
686
|
-
},
|
|
687
|
-
onReplyError: (err) => {
|
|
688
|
-
log(
|
|
689
|
-
`feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`,
|
|
690
|
-
);
|
|
691
|
-
},
|
|
692
|
-
});
|
|
693
|
-
} else {
|
|
694
|
-
log(
|
|
695
|
-
`feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`,
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
const commandAllowFrom = isGroup
|
|
702
|
-
? (groupConfig?.allowFrom ?? configAllowFrom)
|
|
703
|
-
: effectiveDmAllowFrom;
|
|
704
|
-
const senderAllowedForCommands = resolveFeishuAllowlistMatch({
|
|
705
|
-
allowFrom: commandAllowFrom,
|
|
706
|
-
senderId: ctx.senderOpenId,
|
|
707
|
-
senderIds: [senderUserId],
|
|
708
|
-
senderName: ctx.senderName,
|
|
709
|
-
}).allowed;
|
|
710
|
-
|
|
711
|
-
// In group chats, the session is scoped to the group, but the *speaker* is the sender.
|
|
712
|
-
// Using a group-scoped From causes the agent to treat different users as the same person.
|
|
713
|
-
const feishuFrom = `feishu:${ctx.senderOpenId}`;
|
|
714
|
-
const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
|
|
715
|
-
const peerId = isGroup ? (groupSession?.peerId ?? ctx.chatId) : ctx.senderOpenId;
|
|
716
|
-
const parentPeer = isGroup ? (groupSession?.parentPeer ?? null) : null;
|
|
717
|
-
const replyInThread = isGroup ? (groupSession?.replyInThread ?? false) : false;
|
|
718
|
-
const feishuAcpConversationSupported =
|
|
719
|
-
!isGroup ||
|
|
720
|
-
groupSession?.groupSessionScope === "group_topic" ||
|
|
721
|
-
groupSession?.groupSessionScope === "group_topic_sender";
|
|
722
|
-
|
|
723
|
-
if (isGroup && groupSession) {
|
|
724
|
-
log(
|
|
725
|
-
`feishu[${account.accountId}]: group session scope=${groupSession.groupSessionScope}, peer=${peerId}`,
|
|
726
|
-
);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
let route = core.channel.routing.resolveAgentRoute({
|
|
730
|
-
cfg,
|
|
731
|
-
channel: "feishu",
|
|
732
|
-
accountId: account.accountId,
|
|
733
|
-
peer: {
|
|
734
|
-
kind: isGroup ? "group" : "direct",
|
|
735
|
-
id: peerId,
|
|
736
|
-
},
|
|
737
|
-
parentPeer,
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
// Dynamic agent creation for DM users
|
|
741
|
-
// When enabled, creates a unique agent instance with its own workspace for each DM user.
|
|
742
|
-
let effectiveCfg = cfg;
|
|
743
|
-
if (!isGroup && route.matchedBy === "default") {
|
|
744
|
-
const dynamicCfg = feishuCfg?.dynamicAgentCreation as DynamicAgentCreationConfig | undefined;
|
|
745
|
-
if (dynamicCfg?.enabled) {
|
|
746
|
-
const runtime = getFeishuRuntime();
|
|
747
|
-
const result = await maybeCreateDynamicAgent({
|
|
748
|
-
cfg,
|
|
749
|
-
runtime,
|
|
750
|
-
senderOpenId: ctx.senderOpenId,
|
|
751
|
-
dynamicCfg,
|
|
752
|
-
log: (msg) => log(msg),
|
|
753
|
-
});
|
|
754
|
-
if (result.created) {
|
|
755
|
-
effectiveCfg = result.updatedCfg;
|
|
756
|
-
// Re-resolve route with updated config
|
|
757
|
-
route = core.channel.routing.resolveAgentRoute({
|
|
758
|
-
cfg: result.updatedCfg,
|
|
759
|
-
channel: "feishu",
|
|
760
|
-
accountId: account.accountId,
|
|
761
|
-
peer: { kind: "direct", id: ctx.senderOpenId },
|
|
762
|
-
});
|
|
763
|
-
log(
|
|
764
|
-
`feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`,
|
|
765
|
-
);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
const currentConversationId = peerId;
|
|
771
|
-
const parentConversationId = isGroup ? (parentPeer?.id ?? ctx.chatId) : undefined;
|
|
772
|
-
let configuredBinding = null;
|
|
773
|
-
if (feishuAcpConversationSupported) {
|
|
774
|
-
const configuredRoute = resolveConfiguredBindingRoute({
|
|
775
|
-
cfg: effectiveCfg,
|
|
776
|
-
route,
|
|
777
|
-
conversation: {
|
|
778
|
-
channel: "feishu",
|
|
779
|
-
accountId: account.accountId,
|
|
780
|
-
conversationId: currentConversationId,
|
|
781
|
-
parentConversationId,
|
|
782
|
-
},
|
|
783
|
-
});
|
|
784
|
-
configuredBinding = configuredRoute.bindingResolution;
|
|
785
|
-
route = configuredRoute.route;
|
|
786
|
-
|
|
787
|
-
// Bound Feishu conversations intentionally require an exact live conversation-id match.
|
|
788
|
-
// Sender-scoped topic sessions therefore bind on `chat:topic:root:sender:user`, while
|
|
789
|
-
// configured ACP bindings may still inherit the shared `chat:topic:root` topic session.
|
|
790
|
-
const runtimeRoute = resolveRuntimeConversationBindingRoute({
|
|
791
|
-
route,
|
|
792
|
-
conversation: {
|
|
793
|
-
channel: "feishu",
|
|
794
|
-
accountId: account.accountId,
|
|
795
|
-
conversationId: currentConversationId,
|
|
796
|
-
...(parentConversationId ? { parentConversationId } : {}),
|
|
797
|
-
},
|
|
798
|
-
});
|
|
799
|
-
route = runtimeRoute.route;
|
|
800
|
-
if (runtimeRoute.bindingRecord) {
|
|
801
|
-
configuredBinding = null;
|
|
802
|
-
log(
|
|
803
|
-
runtimeRoute.boundSessionKey
|
|
804
|
-
? `feishu[${account.accountId}]: routed via bound conversation ${currentConversationId} -> ${runtimeRoute.boundSessionKey}`
|
|
805
|
-
: `feishu[${account.accountId}]: plugin-bound conversation ${currentConversationId}`,
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (configuredBinding) {
|
|
811
|
-
const ensured = await ensureConfiguredBindingRouteReady({
|
|
812
|
-
cfg: effectiveCfg,
|
|
813
|
-
bindingResolution: configuredBinding,
|
|
814
|
-
});
|
|
815
|
-
if (!ensured.ok) {
|
|
816
|
-
const replyTargetMessageId =
|
|
817
|
-
isGroup &&
|
|
818
|
-
(groupSession?.groupSessionScope === "group_topic" ||
|
|
819
|
-
groupSession?.groupSessionScope === "group_topic_sender")
|
|
820
|
-
? (ctx.rootId ?? ctx.messageId)
|
|
821
|
-
: ctx.messageId;
|
|
822
|
-
await sendMessageFeishu({
|
|
823
|
-
cfg: effectiveCfg,
|
|
824
|
-
to: `chat:${ctx.chatId}`,
|
|
825
|
-
text: `⚠️ Failed to initialize the configured ACP session for this Feishu conversation: ${ensured.error}`,
|
|
826
|
-
replyToMessageId: replyTargetMessageId,
|
|
827
|
-
replyInThread: isGroup ? (groupSession?.replyInThread ?? false) : false,
|
|
828
|
-
accountId: account.accountId,
|
|
829
|
-
}).catch((err) => {
|
|
830
|
-
log(`feishu[${account.accountId}]: failed to send ACP init error reply: ${String(err)}`);
|
|
831
|
-
});
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
|
|
837
|
-
const inboundLabel = isGroup
|
|
838
|
-
? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
|
|
839
|
-
: `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
|
|
840
|
-
const contextVisibilityMode = resolveChannelContextVisibilityMode({
|
|
841
|
-
cfg: effectiveCfg,
|
|
842
|
-
channel: "feishu",
|
|
843
|
-
accountId: account.accountId,
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
// Do not enqueue inbound user previews as system events.
|
|
847
|
-
// System events are prepended to future prompts and can be misread as
|
|
848
|
-
// authoritative transcript turns.
|
|
849
|
-
log(`feishu[${account.accountId}]: ${inboundLabel}: ${preview}`);
|
|
850
|
-
|
|
851
|
-
// Resolve media from message
|
|
852
|
-
const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024; // 30MB default
|
|
853
|
-
const mediaList = await resolveFeishuMediaList({
|
|
854
|
-
cfg,
|
|
855
|
-
messageId: ctx.messageId,
|
|
856
|
-
messageType: event.message.message_type,
|
|
857
|
-
content: event.message.content,
|
|
858
|
-
maxBytes: mediaMaxBytes,
|
|
859
|
-
log,
|
|
860
|
-
accountId: account.accountId,
|
|
861
|
-
});
|
|
862
|
-
// Skip messages with no text content and no media attachments. Feishu can
|
|
863
|
-
// deliver empty-text events (e.g. `{"text":""}`) when a user sends a blank
|
|
864
|
-
// message or when media parsing produces an empty string. Writing a blank
|
|
865
|
-
// user turn to the session causes downstream LLM providers (e.g. MiniMax)
|
|
866
|
-
// to reject the request with "messages must not be empty" errors. Logging
|
|
867
|
-
// the skip avoids silent loss without polluting the agent session.
|
|
868
|
-
if (!ctx.content.trim() && mediaList.length === 0) {
|
|
869
|
-
log(
|
|
870
|
-
`feishu[${account.accountId}]: skipping empty message (no text, no media) from ${ctx.senderOpenId}`,
|
|
871
|
-
);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
const mediaPayload = buildAgentMediaPayload(mediaList);
|
|
876
|
-
const audioTranscript = await resolveFeishuAudioPreflightTranscript({
|
|
877
|
-
cfg: effectiveCfg,
|
|
878
|
-
mediaList,
|
|
879
|
-
content: ctx.content,
|
|
880
|
-
chatType: isGroup ? "group" : "direct",
|
|
881
|
-
log,
|
|
882
|
-
});
|
|
883
|
-
const preflightAudioIndex =
|
|
884
|
-
audioTranscript === undefined
|
|
885
|
-
? -1
|
|
886
|
-
: mediaList.findIndex((media) => media.contentType?.startsWith("audio/"));
|
|
887
|
-
const agentFacingContent = audioTranscript ?? ctx.content;
|
|
888
|
-
const agentFacingCtx =
|
|
889
|
-
audioTranscript === undefined
|
|
890
|
-
? ctx
|
|
891
|
-
: {
|
|
892
|
-
...ctx,
|
|
893
|
-
content: audioTranscript,
|
|
894
|
-
};
|
|
895
|
-
const effectiveCommandProbeBody =
|
|
896
|
-
audioTranscript === undefined
|
|
897
|
-
? commandProbeBody
|
|
898
|
-
: isGroup
|
|
899
|
-
? normalizeFeishuCommandProbeBody(audioTranscript)
|
|
900
|
-
: audioTranscript;
|
|
901
|
-
const shouldComputeEffectiveCommandAuthorized =
|
|
902
|
-
audioTranscript === undefined
|
|
903
|
-
? shouldComputeCommandAuthorized
|
|
904
|
-
: core.channel.commands.shouldComputeCommandAuthorized(effectiveCommandProbeBody, cfg);
|
|
905
|
-
const commandAuthorized = shouldComputeEffectiveCommandAuthorized
|
|
906
|
-
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
907
|
-
useAccessGroups,
|
|
908
|
-
authorizers: [
|
|
909
|
-
{ configured: commandAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
|
910
|
-
],
|
|
911
|
-
})
|
|
912
|
-
: undefined;
|
|
913
|
-
|
|
914
|
-
// Fetch quoted/replied message content if parentId exists
|
|
915
|
-
let quotedMessageInfo: Awaited<ReturnType<typeof getMessageFeishu>> = null;
|
|
916
|
-
let quotedContent: string | undefined;
|
|
917
|
-
if (ctx.parentId) {
|
|
918
|
-
try {
|
|
919
|
-
quotedMessageInfo = await getMessageFeishu({
|
|
920
|
-
cfg,
|
|
921
|
-
messageId: ctx.parentId,
|
|
922
|
-
accountId: account.accountId,
|
|
923
|
-
});
|
|
924
|
-
if (
|
|
925
|
-
quotedMessageInfo &&
|
|
926
|
-
shouldIncludeFetchedGroupContextMessage({
|
|
927
|
-
isGroup,
|
|
928
|
-
allowFrom: effectiveGroupSenderAllowFrom,
|
|
929
|
-
mode: contextVisibilityMode,
|
|
930
|
-
kind: "quote",
|
|
931
|
-
senderId: quotedMessageInfo.senderId,
|
|
932
|
-
senderType: quotedMessageInfo.senderType,
|
|
933
|
-
})
|
|
934
|
-
) {
|
|
935
|
-
quotedContent = quotedMessageInfo.content;
|
|
936
|
-
log(
|
|
937
|
-
`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`,
|
|
938
|
-
);
|
|
939
|
-
} else if (quotedMessageInfo) {
|
|
940
|
-
log(
|
|
941
|
-
`feishu[${account.accountId}]: skipped quoted message from sender ${quotedMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
} catch (err) {
|
|
945
|
-
log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const isTopicSessionForThread =
|
|
950
|
-
isGroup &&
|
|
951
|
-
(groupSession?.groupSessionScope === "group_topic" ||
|
|
952
|
-
groupSession?.groupSessionScope === "group_topic_sender");
|
|
953
|
-
|
|
954
|
-
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
955
|
-
const messageBody = buildFeishuAgentBody({
|
|
956
|
-
ctx: agentFacingCtx,
|
|
957
|
-
quotedContent,
|
|
958
|
-
permissionErrorForAgent,
|
|
959
|
-
botOpenId,
|
|
960
|
-
});
|
|
961
|
-
const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
|
|
962
|
-
if (permissionErrorForAgent) {
|
|
963
|
-
// Keep the notice in a single dispatch to avoid duplicate replies (#27372).
|
|
964
|
-
log(`feishu[${account.accountId}]: appending permission error notice to message body`);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const body = core.channel.reply.formatAgentEnvelope({
|
|
968
|
-
channel: "Feishu",
|
|
969
|
-
from: envelopeFrom,
|
|
970
|
-
timestamp: new Date(),
|
|
971
|
-
envelope: envelopeOptions,
|
|
972
|
-
body: messageBody,
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
let combinedBody = body;
|
|
976
|
-
const historyKey = groupHistoryKey;
|
|
977
|
-
|
|
978
|
-
if (isGroup && historyKey && chatHistories) {
|
|
979
|
-
combinedBody = buildPendingHistoryContextFromMap({
|
|
980
|
-
historyMap: chatHistories,
|
|
981
|
-
historyKey,
|
|
982
|
-
limit: historyLimit,
|
|
983
|
-
currentMessage: combinedBody,
|
|
984
|
-
formatEntry: (entry) =>
|
|
985
|
-
core.channel.reply.formatAgentEnvelope({
|
|
986
|
-
channel: "Feishu",
|
|
987
|
-
// Preserve speaker identity in group history as well.
|
|
988
|
-
from: `${ctx.chatId}:${entry.sender}`,
|
|
989
|
-
timestamp: entry.timestamp,
|
|
990
|
-
body: entry.body,
|
|
991
|
-
envelope: envelopeOptions,
|
|
992
|
-
}),
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
const inboundHistory =
|
|
997
|
-
isGroup && historyKey && historyLimit > 0 && chatHistories
|
|
998
|
-
? (chatHistories.get(historyKey) ?? []).map((entry) => ({
|
|
999
|
-
sender: entry.sender,
|
|
1000
|
-
body: entry.body,
|
|
1001
|
-
timestamp: entry.timestamp,
|
|
1002
|
-
}))
|
|
1003
|
-
: undefined;
|
|
1004
|
-
|
|
1005
|
-
const threadContextBySessionKey = new Map<
|
|
1006
|
-
string,
|
|
1007
|
-
{
|
|
1008
|
-
threadStarterBody?: string;
|
|
1009
|
-
threadHistoryBody?: string;
|
|
1010
|
-
threadLabel?: string;
|
|
1011
|
-
}
|
|
1012
|
-
>();
|
|
1013
|
-
let rootMessageInfo: Awaited<ReturnType<typeof getMessageFeishu>> | undefined;
|
|
1014
|
-
let rootMessageThreadId: string | undefined;
|
|
1015
|
-
let rootMessageFetched = false;
|
|
1016
|
-
const getRootMessageInfo = async () => {
|
|
1017
|
-
if (!ctx.rootId) {
|
|
1018
|
-
return null;
|
|
1019
|
-
}
|
|
1020
|
-
if (!rootMessageFetched) {
|
|
1021
|
-
rootMessageFetched = true;
|
|
1022
|
-
if (ctx.rootId === ctx.parentId && quotedMessageInfo) {
|
|
1023
|
-
rootMessageInfo = quotedMessageInfo;
|
|
1024
|
-
} else {
|
|
1025
|
-
try {
|
|
1026
|
-
rootMessageInfo = await getMessageFeishu({
|
|
1027
|
-
cfg,
|
|
1028
|
-
messageId: ctx.rootId,
|
|
1029
|
-
accountId: account.accountId,
|
|
1030
|
-
});
|
|
1031
|
-
} catch (err) {
|
|
1032
|
-
log(`feishu[${account.accountId}]: failed to fetch root message: ${String(err)}`);
|
|
1033
|
-
rootMessageInfo = null;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
rootMessageThreadId = rootMessageInfo?.threadId;
|
|
1037
|
-
if (
|
|
1038
|
-
rootMessageInfo &&
|
|
1039
|
-
!shouldIncludeFetchedGroupContextMessage({
|
|
1040
|
-
isGroup,
|
|
1041
|
-
allowFrom: effectiveGroupSenderAllowFrom,
|
|
1042
|
-
mode: contextVisibilityMode,
|
|
1043
|
-
kind: "thread",
|
|
1044
|
-
senderId: rootMessageInfo.senderId,
|
|
1045
|
-
senderType: rootMessageInfo.senderType,
|
|
1046
|
-
})
|
|
1047
|
-
) {
|
|
1048
|
-
log(
|
|
1049
|
-
`feishu[${account.accountId}]: skipped thread starter from sender ${rootMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
|
|
1050
|
-
);
|
|
1051
|
-
rootMessageInfo = null;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
return rootMessageInfo ?? null;
|
|
1055
|
-
};
|
|
1056
|
-
let groupNamePromise: Promise<string | undefined> | undefined;
|
|
1057
|
-
const resolveGroupNameForLabel = (): Promise<string | undefined> => {
|
|
1058
|
-
if (!isGroup) {
|
|
1059
|
-
return Promise.resolve(undefined);
|
|
1060
|
-
}
|
|
1061
|
-
groupNamePromise ??= resolveGroupName({ account, chatId: ctx.chatId, log });
|
|
1062
|
-
return groupNamePromise;
|
|
1063
|
-
};
|
|
1064
|
-
|
|
1065
|
-
const resolveThreadContextForAgent = async (
|
|
1066
|
-
agentId: string,
|
|
1067
|
-
agentSessionKey: string,
|
|
1068
|
-
groupName: string | undefined,
|
|
1069
|
-
) => {
|
|
1070
|
-
const cached = threadContextBySessionKey.get(agentSessionKey);
|
|
1071
|
-
if (cached) {
|
|
1072
|
-
return cached;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
const threadContext: {
|
|
1076
|
-
threadStarterBody?: string;
|
|
1077
|
-
threadHistoryBody?: string;
|
|
1078
|
-
threadLabel?: string;
|
|
1079
|
-
} = {
|
|
1080
|
-
threadLabel:
|
|
1081
|
-
(ctx.rootId || ctx.threadId) && isTopicSessionForThread
|
|
1082
|
-
? `Feishu thread in ${groupName ?? ctx.chatId}`
|
|
1083
|
-
: undefined,
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
if (!(ctx.rootId || ctx.threadId) || !isTopicSessionForThread) {
|
|
1087
|
-
threadContextBySessionKey.set(agentSessionKey, threadContext);
|
|
1088
|
-
return threadContext;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId });
|
|
1092
|
-
const previousThreadSessionTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
1093
|
-
storePath,
|
|
1094
|
-
sessionKey: agentSessionKey,
|
|
1095
|
-
});
|
|
1096
|
-
if (previousThreadSessionTimestamp) {
|
|
1097
|
-
log(
|
|
1098
|
-
`feishu[${account.accountId}]: skipping thread bootstrap for existing session ${agentSessionKey}`,
|
|
1099
|
-
);
|
|
1100
|
-
threadContextBySessionKey.set(agentSessionKey, threadContext);
|
|
1101
|
-
return threadContext;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
const rootMsg = await getRootMessageInfo();
|
|
1105
|
-
let feishuThreadId = ctx.threadId ?? rootMessageThreadId ?? rootMsg?.threadId;
|
|
1106
|
-
if (feishuThreadId) {
|
|
1107
|
-
log(`feishu[${account.accountId}]: resolved thread ID: ${feishuThreadId}`);
|
|
1108
|
-
}
|
|
1109
|
-
if (!feishuThreadId) {
|
|
1110
|
-
log(
|
|
1111
|
-
`feishu[${account.accountId}]: no threadId found for root message ${ctx.rootId ?? "none"}, skipping thread history`,
|
|
1112
|
-
);
|
|
1113
|
-
threadContextBySessionKey.set(agentSessionKey, threadContext);
|
|
1114
|
-
return threadContext;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
try {
|
|
1118
|
-
const threadMessages = await listFeishuThreadMessages({
|
|
1119
|
-
cfg,
|
|
1120
|
-
threadId: feishuThreadId,
|
|
1121
|
-
currentMessageId: ctx.messageId,
|
|
1122
|
-
rootMessageId: ctx.rootId,
|
|
1123
|
-
limit: 20,
|
|
1124
|
-
accountId: account.accountId,
|
|
1125
|
-
});
|
|
1126
|
-
const senderScoped = groupSession?.groupSessionScope === "group_topic_sender";
|
|
1127
|
-
const senderIds = new Set(
|
|
1128
|
-
[ctx.senderOpenId, senderUserId]
|
|
1129
|
-
.map((id) => id?.trim())
|
|
1130
|
-
.filter((id): id is string => id !== undefined && id.length > 0),
|
|
1131
|
-
);
|
|
1132
|
-
const allowlistedMessages = filterFetchedGroupContextMessages(threadMessages, {
|
|
1133
|
-
isGroup,
|
|
1134
|
-
allowFrom: effectiveGroupSenderAllowFrom,
|
|
1135
|
-
mode: contextVisibilityMode,
|
|
1136
|
-
kind: "history",
|
|
1137
|
-
});
|
|
1138
|
-
const relevantMessages =
|
|
1139
|
-
(senderScoped
|
|
1140
|
-
? allowlistedMessages.filter(
|
|
1141
|
-
(msg) =>
|
|
1142
|
-
msg.senderType === "app" ||
|
|
1143
|
-
(msg.senderId !== undefined && senderIds.has(msg.senderId.trim())),
|
|
1144
|
-
)
|
|
1145
|
-
: allowlistedMessages) ?? [];
|
|
1146
|
-
|
|
1147
|
-
const threadStarterBody = rootMsg?.content ?? relevantMessages[0]?.content;
|
|
1148
|
-
const includeStarterInHistory = Boolean(rootMsg?.content || ctx.rootId);
|
|
1149
|
-
const historyMessages = includeStarterInHistory
|
|
1150
|
-
? relevantMessages
|
|
1151
|
-
: relevantMessages.slice(1);
|
|
1152
|
-
const historyParts = historyMessages.map((msg) => {
|
|
1153
|
-
const role = msg.senderType === "app" ? "assistant" : "user";
|
|
1154
|
-
return core.channel.reply.formatAgentEnvelope({
|
|
1155
|
-
channel: "Feishu",
|
|
1156
|
-
from: `${msg.senderId ?? "Unknown"} (${role})`,
|
|
1157
|
-
timestamp: msg.createTime,
|
|
1158
|
-
body: msg.content,
|
|
1159
|
-
envelope: envelopeOptions,
|
|
1160
|
-
});
|
|
1161
|
-
});
|
|
1162
|
-
|
|
1163
|
-
threadContext.threadStarterBody = threadStarterBody;
|
|
1164
|
-
threadContext.threadHistoryBody =
|
|
1165
|
-
historyParts.length > 0 ? historyParts.join("\n\n") : undefined;
|
|
1166
|
-
log(
|
|
1167
|
-
`feishu[${account.accountId}]: populated thread bootstrap with starter=${threadStarterBody ? "yes" : "no"} history=${historyMessages.length}`,
|
|
1168
|
-
);
|
|
1169
|
-
} catch (err) {
|
|
1170
|
-
log(`feishu[${account.accountId}]: failed to fetch thread history: ${String(err)}`);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
threadContextBySessionKey.set(agentSessionKey, threadContext);
|
|
1174
|
-
return threadContext;
|
|
1175
|
-
};
|
|
1176
|
-
|
|
1177
|
-
// --- Shared context builder for dispatch ---
|
|
1178
|
-
const buildCtxPayloadForAgent = async (
|
|
1179
|
-
agentId: string,
|
|
1180
|
-
agentSessionKey: string,
|
|
1181
|
-
agentAccountId: string,
|
|
1182
|
-
wasMentioned: boolean,
|
|
1183
|
-
) => {
|
|
1184
|
-
const groupName = await resolveGroupNameForLabel();
|
|
1185
|
-
const threadContext = await resolveThreadContextForAgent(agentId, agentSessionKey, groupName);
|
|
1186
|
-
return core.channel.reply.finalizeInboundContext({
|
|
1187
|
-
Body: combinedBody,
|
|
1188
|
-
BodyForAgent: messageBody,
|
|
1189
|
-
InboundHistory: inboundHistory,
|
|
1190
|
-
ReplyToId: ctx.parentId,
|
|
1191
|
-
RootMessageId: ctx.rootId,
|
|
1192
|
-
RawBody: agentFacingContent,
|
|
1193
|
-
CommandBody: agentFacingContent,
|
|
1194
|
-
Transcript: audioTranscript,
|
|
1195
|
-
From: feishuFrom,
|
|
1196
|
-
To: feishuTo,
|
|
1197
|
-
SessionKey: agentSessionKey,
|
|
1198
|
-
AccountId: agentAccountId,
|
|
1199
|
-
ChatType: isGroup ? "group" : "direct",
|
|
1200
|
-
GroupSubject: isGroup ? groupName || ctx.chatId : undefined,
|
|
1201
|
-
ConversationLabel: isGroup && groupName && !isTopicSessionForThread ? groupName : undefined,
|
|
1202
|
-
SenderName: ctx.senderName ?? ctx.senderOpenId,
|
|
1203
|
-
SenderId: ctx.senderOpenId,
|
|
1204
|
-
Provider: "feishu" as const,
|
|
1205
|
-
Surface: "feishu" as const,
|
|
1206
|
-
MessageSid: ctx.messageId,
|
|
1207
|
-
ReplyToBody: quotedContent ?? undefined,
|
|
1208
|
-
ThreadStarterBody: threadContext.threadStarterBody,
|
|
1209
|
-
ThreadHistoryBody: threadContext.threadHistoryBody,
|
|
1210
|
-
ThreadLabel: threadContext.threadLabel,
|
|
1211
|
-
// Only use rootId (om_* message anchor) — threadId (omt_*) is a container
|
|
1212
|
-
// ID and would produce invalid reply targets downstream.
|
|
1213
|
-
MessageThreadId: ctx.rootId && isTopicSessionForThread ? ctx.rootId : undefined,
|
|
1214
|
-
Timestamp: messageCreateTimeMs,
|
|
1215
|
-
WasMentioned: wasMentioned,
|
|
1216
|
-
CommandAuthorized: commandAuthorized,
|
|
1217
|
-
OriginatingChannel: "feishu" as const,
|
|
1218
|
-
OriginatingTo: feishuTo,
|
|
1219
|
-
GroupSystemPrompt: isGroup ? normalizeOptionalString(groupConfig?.systemPrompt) : undefined,
|
|
1220
|
-
...mediaPayload,
|
|
1221
|
-
...(preflightAudioIndex >= 0 ? { MediaTranscribedIndexes: [preflightAudioIndex] } : {}),
|
|
1222
|
-
});
|
|
1223
|
-
};
|
|
1224
|
-
|
|
1225
|
-
// Determine reply target based on group session mode:
|
|
1226
|
-
// - Topic-mode groups (group_topic / group_topic_sender): reply to the topic
|
|
1227
|
-
// root so the bot stays in the same thread.
|
|
1228
|
-
// - Groups with explicit replyInThread config: reply to the root so the bot
|
|
1229
|
-
// stays in the thread the user expects.
|
|
1230
|
-
// - Normal groups (auto-detected threadReply from root_id): reply to the
|
|
1231
|
-
// triggering message itself. Using rootId here would silently push the
|
|
1232
|
-
// reply into a topic thread invisible in the main chat view (#32980).
|
|
1233
|
-
const isTopicSession =
|
|
1234
|
-
isGroup &&
|
|
1235
|
-
(groupSession?.groupSessionScope === "group_topic" ||
|
|
1236
|
-
groupSession?.groupSessionScope === "group_topic_sender");
|
|
1237
|
-
const configReplyInThread =
|
|
1238
|
-
isGroup &&
|
|
1239
|
-
(groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled";
|
|
1240
|
-
const replyTargetMessageId =
|
|
1241
|
-
isTopicSession || configReplyInThread
|
|
1242
|
-
? (ctx.rootId ??
|
|
1243
|
-
ctx.replyTargetMessageId ??
|
|
1244
|
-
(ctx.suppressReplyTarget ? undefined : ctx.messageId))
|
|
1245
|
-
: (ctx.replyTargetMessageId ?? (ctx.suppressReplyTarget ? undefined : ctx.messageId));
|
|
1246
|
-
const threadReply = isGroup ? (groupSession?.threadReply ?? false) : false;
|
|
1247
|
-
|
|
1248
|
-
if (broadcastAgents) {
|
|
1249
|
-
// Cross-account dedup: in multi-account setups, Feishu delivers the same
|
|
1250
|
-
// event to every bot account in the group. Only one account should handle
|
|
1251
|
-
// broadcast dispatch to avoid duplicate agent sessions and race conditions.
|
|
1252
|
-
// Uses a shared "broadcast" namespace (not per-account) so the first handler
|
|
1253
|
-
// to reach this point claims the message; subsequent accounts skip.
|
|
1254
|
-
if (!(await tryRecordMessagePersistent(ctx.messageId, "broadcast", log))) {
|
|
1255
|
-
log(
|
|
1256
|
-
`feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`,
|
|
1257
|
-
);
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// --- Broadcast dispatch: send message to all configured agents ---
|
|
1262
|
-
const rawStrategy = (
|
|
1263
|
-
(cfg as Record<string, unknown>).broadcast as Record<string, unknown> | undefined
|
|
1264
|
-
)?.strategy;
|
|
1265
|
-
const strategy = rawStrategy === "sequential" ? "sequential" : "parallel";
|
|
1266
|
-
const activeAgentId =
|
|
1267
|
-
ctx.mentionedBot || !requireMention ? normalizeAgentId(route.agentId) : null;
|
|
1268
|
-
const agentIds = (cfg.agents?.list ?? []).map((a: { id: string }) => normalizeAgentId(a.id));
|
|
1269
|
-
const hasKnownAgents = agentIds.length > 0;
|
|
1270
|
-
|
|
1271
|
-
log(
|
|
1272
|
-
`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`,
|
|
1273
|
-
);
|
|
1274
|
-
|
|
1275
|
-
const dispatchForAgent = async (agentId: string) => {
|
|
1276
|
-
if (hasKnownAgents && !agentIds.includes(normalizeAgentId(agentId))) {
|
|
1277
|
-
log(
|
|
1278
|
-
`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`,
|
|
1279
|
-
);
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
|
|
1284
|
-
const agentStorePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
1285
|
-
agentId,
|
|
1286
|
-
});
|
|
1287
|
-
const agentRecord = {
|
|
1288
|
-
onRecordError: (err: unknown) => {
|
|
1289
|
-
log(
|
|
1290
|
-
`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`,
|
|
1291
|
-
);
|
|
1292
|
-
},
|
|
1293
|
-
};
|
|
1294
|
-
const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
|
|
1295
|
-
storePath: agentStorePath,
|
|
1296
|
-
sessionKey: agentSessionKey,
|
|
1297
|
-
});
|
|
1298
|
-
const agentCtx = await buildCtxPayloadForAgent(
|
|
1299
|
-
agentId,
|
|
1300
|
-
agentSessionKey,
|
|
1301
|
-
route.accountId,
|
|
1302
|
-
ctx.mentionedBot && agentId === activeAgentId,
|
|
1303
|
-
);
|
|
1304
|
-
|
|
1305
|
-
if (agentId === activeAgentId) {
|
|
1306
|
-
// Active agent: real Feishu dispatcher (responds on Feishu)
|
|
1307
|
-
const identity = resolveAgentOutboundIdentity(cfg, agentId);
|
|
1308
|
-
const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
|
|
1309
|
-
cfg,
|
|
1310
|
-
agentId,
|
|
1311
|
-
runtime: runtime as RuntimeEnv,
|
|
1312
|
-
chatId: ctx.chatId,
|
|
1313
|
-
allowReasoningPreview,
|
|
1314
|
-
replyToMessageId: replyTargetMessageId,
|
|
1315
|
-
skipReplyToInMessages: !isGroup,
|
|
1316
|
-
replyInThread,
|
|
1317
|
-
rootId: ctx.rootId,
|
|
1318
|
-
threadReply,
|
|
1319
|
-
mentionTargets: ctx.mentionTargets,
|
|
1320
|
-
accountId: account.accountId,
|
|
1321
|
-
identity,
|
|
1322
|
-
messageCreateTimeMs,
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
log(
|
|
1326
|
-
`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`,
|
|
1327
|
-
);
|
|
1328
|
-
await core.channel.turn.run({
|
|
1329
|
-
channel: "feishu",
|
|
1330
|
-
accountId: route.accountId,
|
|
1331
|
-
raw: ctx,
|
|
1332
|
-
adapter: {
|
|
1333
|
-
ingest: () => ({
|
|
1334
|
-
id: ctx.messageId,
|
|
1335
|
-
timestamp: messageCreateTimeMs,
|
|
1336
|
-
rawText: ctx.content,
|
|
1337
|
-
textForAgent: agentCtx.BodyForAgent,
|
|
1338
|
-
textForCommands: agentCtx.CommandBody,
|
|
1339
|
-
raw: ctx,
|
|
1340
|
-
}),
|
|
1341
|
-
resolveTurn: () => ({
|
|
1342
|
-
channel: "feishu",
|
|
1343
|
-
accountId: route.accountId,
|
|
1344
|
-
routeSessionKey: agentSessionKey,
|
|
1345
|
-
storePath: agentStorePath,
|
|
1346
|
-
ctxPayload: agentCtx,
|
|
1347
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
1348
|
-
record: agentRecord,
|
|
1349
|
-
onPreDispatchFailure: () =>
|
|
1350
|
-
core.channel.reply.settleReplyDispatcher({
|
|
1351
|
-
dispatcher,
|
|
1352
|
-
onSettled: () => markDispatchIdle(),
|
|
1353
|
-
}),
|
|
1354
|
-
runDispatch: () =>
|
|
1355
|
-
core.channel.reply.withReplyDispatcher({
|
|
1356
|
-
dispatcher,
|
|
1357
|
-
onSettled: () => markDispatchIdle(),
|
|
1358
|
-
run: () =>
|
|
1359
|
-
core.channel.reply.dispatchReplyFromConfig({
|
|
1360
|
-
ctx: agentCtx,
|
|
1361
|
-
cfg,
|
|
1362
|
-
dispatcher,
|
|
1363
|
-
replyOptions,
|
|
1364
|
-
}),
|
|
1365
|
-
}),
|
|
1366
|
-
}),
|
|
1367
|
-
},
|
|
1368
|
-
});
|
|
1369
|
-
} else {
|
|
1370
|
-
// Observer agent: no-op dispatcher (session entry + inference, no Feishu reply).
|
|
1371
|
-
// Strip CommandAuthorized so slash commands (e.g. /reset) don't silently
|
|
1372
|
-
// mutate observer sessions — only the active agent should execute commands.
|
|
1373
|
-
delete (agentCtx as Record<string, unknown>).CommandAuthorized;
|
|
1374
|
-
const noopDispatcher = {
|
|
1375
|
-
sendToolResult: () => false,
|
|
1376
|
-
sendBlockReply: () => false,
|
|
1377
|
-
sendFinalReply: () => false,
|
|
1378
|
-
waitForIdle: async () => {},
|
|
1379
|
-
getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
1380
|
-
getFailedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
1381
|
-
markComplete: () => {},
|
|
1382
|
-
};
|
|
1383
|
-
|
|
1384
|
-
log(
|
|
1385
|
-
`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`,
|
|
1386
|
-
);
|
|
1387
|
-
await core.channel.turn.run({
|
|
1388
|
-
channel: "feishu",
|
|
1389
|
-
accountId: route.accountId,
|
|
1390
|
-
raw: ctx,
|
|
1391
|
-
adapter: {
|
|
1392
|
-
ingest: () => ({
|
|
1393
|
-
id: ctx.messageId,
|
|
1394
|
-
timestamp: messageCreateTimeMs,
|
|
1395
|
-
rawText: ctx.content,
|
|
1396
|
-
textForAgent: agentCtx.BodyForAgent,
|
|
1397
|
-
textForCommands: agentCtx.CommandBody,
|
|
1398
|
-
raw: ctx,
|
|
1399
|
-
}),
|
|
1400
|
-
resolveTurn: () => ({
|
|
1401
|
-
channel: "feishu",
|
|
1402
|
-
accountId: route.accountId,
|
|
1403
|
-
routeSessionKey: agentSessionKey,
|
|
1404
|
-
storePath: agentStorePath,
|
|
1405
|
-
ctxPayload: agentCtx,
|
|
1406
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
1407
|
-
record: agentRecord,
|
|
1408
|
-
runDispatch: () =>
|
|
1409
|
-
core.channel.reply.withReplyDispatcher({
|
|
1410
|
-
dispatcher: noopDispatcher,
|
|
1411
|
-
run: () =>
|
|
1412
|
-
core.channel.reply.dispatchReplyFromConfig({
|
|
1413
|
-
ctx: agentCtx,
|
|
1414
|
-
cfg,
|
|
1415
|
-
dispatcher: noopDispatcher,
|
|
1416
|
-
}),
|
|
1417
|
-
}),
|
|
1418
|
-
}),
|
|
1419
|
-
},
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
};
|
|
1423
|
-
|
|
1424
|
-
if (strategy === "sequential") {
|
|
1425
|
-
for (const agentId of broadcastAgents) {
|
|
1426
|
-
try {
|
|
1427
|
-
await dispatchForAgent(agentId);
|
|
1428
|
-
} catch (err) {
|
|
1429
|
-
log(
|
|
1430
|
-
`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(err)}`,
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
} else {
|
|
1435
|
-
const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
|
|
1436
|
-
for (let i = 0; i < results.length; i++) {
|
|
1437
|
-
if (results[i].status === "rejected") {
|
|
1438
|
-
log(
|
|
1439
|
-
`feishu[${account.accountId}]: broadcast dispatch failed for agent=${broadcastAgents[i]}: ${String((results[i] as PromiseRejectedResult).reason)}`,
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (isGroup && historyKey && chatHistories) {
|
|
1446
|
-
clearHistoryEntriesIfEnabled({
|
|
1447
|
-
historyMap: chatHistories,
|
|
1448
|
-
historyKey,
|
|
1449
|
-
limit: historyLimit,
|
|
1450
|
-
});
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
log(
|
|
1454
|
-
`feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`,
|
|
1455
|
-
);
|
|
1456
|
-
} else {
|
|
1457
|
-
// --- Single-agent dispatch (existing behavior) ---
|
|
1458
|
-
const ctxPayload = await buildCtxPayloadForAgent(
|
|
1459
|
-
route.agentId,
|
|
1460
|
-
route.sessionKey,
|
|
1461
|
-
route.accountId,
|
|
1462
|
-
ctx.mentionedBot,
|
|
1463
|
-
);
|
|
1464
|
-
|
|
1465
|
-
const identity = resolveAgentOutboundIdentity(cfg, route.agentId);
|
|
1466
|
-
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
1467
|
-
agentId: route.agentId,
|
|
1468
|
-
});
|
|
1469
|
-
const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
|
|
1470
|
-
storePath,
|
|
1471
|
-
sessionKey: route.sessionKey,
|
|
1472
|
-
});
|
|
1473
|
-
const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
|
|
1474
|
-
cfg,
|
|
1475
|
-
agentId: route.agentId,
|
|
1476
|
-
runtime: runtime as RuntimeEnv,
|
|
1477
|
-
chatId: ctx.chatId,
|
|
1478
|
-
allowReasoningPreview,
|
|
1479
|
-
replyToMessageId: replyTargetMessageId,
|
|
1480
|
-
skipReplyToInMessages: !isGroup,
|
|
1481
|
-
replyInThread,
|
|
1482
|
-
rootId: ctx.rootId,
|
|
1483
|
-
threadReply,
|
|
1484
|
-
mentionTargets: ctx.mentionTargets,
|
|
1485
|
-
accountId: account.accountId,
|
|
1486
|
-
identity,
|
|
1487
|
-
messageCreateTimeMs,
|
|
1488
|
-
});
|
|
1489
|
-
|
|
1490
|
-
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
|
|
1491
|
-
const turnResult = await core.channel.turn.run({
|
|
1492
|
-
channel: "feishu",
|
|
1493
|
-
accountId: route.accountId,
|
|
1494
|
-
raw: ctx,
|
|
1495
|
-
adapter: {
|
|
1496
|
-
ingest: () => ({
|
|
1497
|
-
id: ctx.messageId,
|
|
1498
|
-
timestamp: messageCreateTimeMs,
|
|
1499
|
-
rawText: ctx.content,
|
|
1500
|
-
textForAgent: ctxPayload.BodyForAgent,
|
|
1501
|
-
textForCommands: ctxPayload.CommandBody,
|
|
1502
|
-
raw: ctx,
|
|
1503
|
-
}),
|
|
1504
|
-
resolveTurn: () => ({
|
|
1505
|
-
channel: "feishu",
|
|
1506
|
-
accountId: route.accountId,
|
|
1507
|
-
routeSessionKey: route.sessionKey,
|
|
1508
|
-
storePath,
|
|
1509
|
-
ctxPayload,
|
|
1510
|
-
recordInboundSession: core.channel.session.recordInboundSession,
|
|
1511
|
-
record: {
|
|
1512
|
-
onRecordError: (err) => {
|
|
1513
|
-
log(
|
|
1514
|
-
`feishu[${account.accountId}]: failed to record inbound session ${route.sessionKey}: ${String(err)}`,
|
|
1515
|
-
);
|
|
1516
|
-
},
|
|
1517
|
-
},
|
|
1518
|
-
history: {
|
|
1519
|
-
isGroup,
|
|
1520
|
-
historyKey,
|
|
1521
|
-
historyMap: chatHistories,
|
|
1522
|
-
limit: historyLimit,
|
|
1523
|
-
},
|
|
1524
|
-
onPreDispatchFailure: () =>
|
|
1525
|
-
core.channel.reply.settleReplyDispatcher({
|
|
1526
|
-
dispatcher,
|
|
1527
|
-
onSettled: () => markDispatchIdle(),
|
|
1528
|
-
}),
|
|
1529
|
-
runDispatch: () =>
|
|
1530
|
-
core.channel.reply.withReplyDispatcher({
|
|
1531
|
-
dispatcher,
|
|
1532
|
-
onSettled: () => {
|
|
1533
|
-
markDispatchIdle();
|
|
1534
|
-
},
|
|
1535
|
-
run: () =>
|
|
1536
|
-
core.channel.reply.dispatchReplyFromConfig({
|
|
1537
|
-
ctx: ctxPayload,
|
|
1538
|
-
cfg,
|
|
1539
|
-
dispatcher,
|
|
1540
|
-
replyOptions,
|
|
1541
|
-
}),
|
|
1542
|
-
}),
|
|
1543
|
-
}),
|
|
1544
|
-
},
|
|
1545
|
-
});
|
|
1546
|
-
if (!turnResult.dispatched) {
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
const { dispatchResult } = turnResult;
|
|
1550
|
-
const { queuedFinal, counts } = dispatchResult;
|
|
1551
|
-
|
|
1552
|
-
log(
|
|
1553
|
-
`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`,
|
|
1554
|
-
);
|
|
1555
|
-
}
|
|
1556
|
-
} catch (err) {
|
|
1557
|
-
error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
|
|
1558
|
-
}
|
|
1559
|
-
}
|