@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.1
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/index.d.ts +23 -0
- package/dist/index.js +45 -0
- package/dist/src/accounts.js +141 -0
- package/dist/src/app-scope-checker.js +36 -0
- package/dist/src/async.js +34 -0
- package/dist/src/auth-errors.js +72 -0
- package/dist/src/bitable.js +495 -0
- package/dist/src/bot.d.ts +35 -0
- package/dist/src/bot.js +941 -0
- package/dist/src/calendar-calendar.js +54 -0
- package/dist/src/calendar-event-attendee.js +98 -0
- package/dist/src/calendar-event.js +193 -0
- package/dist/src/calendar-freebusy.js +40 -0
- package/dist/src/calendar-shared.js +23 -0
- package/dist/src/calendar.js +16 -0
- package/dist/src/card-action.js +49 -0
- package/dist/src/channel.d.ts +7 -0
- package/dist/src/channel.js +413 -0
- package/dist/src/chat-schema.js +25 -0
- package/dist/src/chat.js +87 -0
- package/dist/src/client.d.ts +16 -0
- package/dist/src/client.js +112 -0
- package/dist/src/config-schema.d.ts +357 -0
- package/dist/src/dedup.js +126 -0
- package/dist/src/device-flow.js +109 -0
- package/dist/src/directory.js +101 -0
- package/dist/src/doc-schema.js +148 -0
- package/dist/src/docx-batch-insert.js +104 -0
- package/dist/src/docx-color-text.js +80 -0
- package/dist/src/docx-table-ops.js +197 -0
- package/dist/src/docx.js +858 -0
- package/dist/src/domains.js +14 -0
- package/dist/src/drive-schema.js +41 -0
- package/dist/src/drive.js +126 -0
- package/dist/src/dynamic-agent.js +93 -0
- package/dist/src/external-keys.js +13 -0
- package/dist/src/feishu-fetch.js +12 -0
- package/dist/src/identity.js +92 -0
- package/dist/src/lark-ticket.js +11 -0
- package/dist/src/media.d.ts +75 -0
- package/dist/src/media.js +304 -0
- package/dist/src/mention.d.ts +52 -0
- package/dist/src/mention.js +82 -0
- package/dist/src/monitor.account.d.ts +1 -0
- package/dist/src/monitor.account.js +393 -0
- package/dist/src/monitor.d.ts +11 -0
- package/dist/src/monitor.js +58 -0
- package/dist/src/monitor.startup.js +24 -0
- package/dist/src/monitor.state.d.ts +1 -0
- package/dist/src/monitor.state.js +80 -0
- package/dist/src/monitor.transport.js +167 -0
- package/dist/src/nextclaw-sdk/account-id.js +15 -0
- package/dist/src/nextclaw-sdk/core-channel.js +150 -0
- package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
- package/dist/src/nextclaw-sdk/dedupe.js +164 -0
- package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
- package/dist/src/nextclaw-sdk/feishu.js +14 -0
- package/dist/src/nextclaw-sdk/history.js +69 -0
- package/dist/src/nextclaw-sdk/network-body.js +180 -0
- package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
- package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
- package/dist/src/nextclaw-sdk/network.js +4 -0
- package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
- package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
- package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
- package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
- package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets.js +4 -0
- package/dist/src/nextclaw-sdk/types.d.ts +242 -0
- package/dist/src/oauth.js +171 -0
- package/dist/src/onboarding.js +381 -0
- package/dist/src/outbound.js +150 -0
- package/dist/src/perm-schema.js +49 -0
- package/dist/src/perm.js +90 -0
- package/dist/src/policy.js +61 -0
- package/dist/src/post.js +160 -0
- package/dist/src/probe.d.ts +11 -0
- package/dist/src/probe.js +85 -0
- package/dist/src/raw-request.js +24 -0
- package/dist/src/reactions.d.ts +67 -0
- package/dist/src/reactions.js +91 -0
- package/dist/src/reply-dispatcher.js +250 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/secret-input.js +3 -0
- package/dist/src/send-result.js +12 -0
- package/dist/src/send-target.js +22 -0
- package/dist/src/send.d.ts +51 -0
- package/dist/src/send.js +265 -0
- package/dist/src/sheets-shared.js +193 -0
- package/dist/src/sheets.js +95 -0
- package/dist/src/streaming-card.js +263 -0
- package/dist/src/targets.js +39 -0
- package/dist/src/task-comment.js +76 -0
- package/dist/src/task-shared.js +13 -0
- package/dist/src/task-subtask.js +79 -0
- package/dist/src/task-task.js +144 -0
- package/dist/src/task-tasklist.js +136 -0
- package/dist/src/task.js +16 -0
- package/dist/src/token-store.js +154 -0
- package/dist/src/tool-account.js +65 -0
- package/dist/src/tool-result.js +18 -0
- package/dist/src/tool-scopes.js +62 -0
- package/dist/src/tools-config.js +30 -0
- package/dist/src/types.d.ts +43 -0
- package/dist/src/typing.js +145 -0
- package/dist/src/uat-client.js +102 -0
- package/dist/src/user-tool-client.js +132 -0
- package/dist/src/user-tool-helpers.js +110 -0
- package/dist/src/user-tool-result.js +10 -0
- package/dist/src/wiki-schema.js +45 -0
- package/dist/src/wiki.js +144 -0
- package/package.json +8 -4
- package/index.ts +0 -75
package/dist/src/bot.js
ADDED
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
import { normalizeAgentId } from "./nextclaw-sdk/account-id.js";
|
|
2
|
+
import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "./nextclaw-sdk/core-channel.js";
|
|
3
|
+
import { buildAgentMediaPayload, createScopedPairingAccess, issuePairingChallenge } from "./nextclaw-sdk/core-pairing.js";
|
|
4
|
+
import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "./nextclaw-sdk/history.js";
|
|
5
|
+
import "./nextclaw-sdk/feishu.js";
|
|
6
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
7
|
+
import { createFeishuClient } from "./client.js";
|
|
8
|
+
import { raceWithTimeoutAndAbort } from "./async.js";
|
|
9
|
+
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
|
10
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
11
|
+
import { downloadMessageResourceFeishu } from "./media.js";
|
|
12
|
+
import { extractMentionTargets, isMentionForwardRequest } from "./mention.js";
|
|
13
|
+
import { parsePostContent } from "./post.js";
|
|
14
|
+
import { getMessageFeishu, sendMessageFeishu } from "./send.js";
|
|
15
|
+
import { isFeishuGroupAllowed, resolveFeishuAllowlistMatch, resolveFeishuGroupConfig, resolveFeishuReplyPolicy } from "./policy.js";
|
|
16
|
+
import { finalizeFeishuMessageProcessing, tryRecordMessagePersistent } from "./dedup.js";
|
|
17
|
+
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
|
18
|
+
import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
|
|
19
|
+
//#region src/bot.ts
|
|
20
|
+
const IGNORED_PERMISSION_SCOPE_TOKENS = ["contact:contact.base:readonly"];
|
|
21
|
+
const FEISHU_SCOPE_CORRECTIONS = { "contact:contact.base:readonly": "contact:user.base:readonly" };
|
|
22
|
+
function correctFeishuScopeInUrl(url) {
|
|
23
|
+
let corrected = url;
|
|
24
|
+
for (const [wrong, right] of Object.entries(FEISHU_SCOPE_CORRECTIONS)) {
|
|
25
|
+
corrected = corrected.replaceAll(encodeURIComponent(wrong), encodeURIComponent(right));
|
|
26
|
+
corrected = corrected.replaceAll(wrong, right);
|
|
27
|
+
}
|
|
28
|
+
return corrected;
|
|
29
|
+
}
|
|
30
|
+
function shouldSuppressPermissionErrorNotice(permissionError) {
|
|
31
|
+
const message = permissionError.message.toLowerCase();
|
|
32
|
+
return IGNORED_PERMISSION_SCOPE_TOKENS.some((token) => message.includes(token));
|
|
33
|
+
}
|
|
34
|
+
function extractPermissionError(err) {
|
|
35
|
+
if (!err || typeof err !== "object") return null;
|
|
36
|
+
const data = err.response?.data;
|
|
37
|
+
if (!data || typeof data !== "object") return null;
|
|
38
|
+
const feishuErr = data;
|
|
39
|
+
if (feishuErr.code !== 99991672) return null;
|
|
40
|
+
const msg = feishuErr.msg ?? "";
|
|
41
|
+
const urlMatch = msg.match(/https:\/\/[^\s,]+\/app\/[^\s,]+/);
|
|
42
|
+
const grantUrl = urlMatch?.[0] ? correctFeishuScopeInUrl(urlMatch[0]) : void 0;
|
|
43
|
+
return {
|
|
44
|
+
code: feishuErr.code,
|
|
45
|
+
message: msg,
|
|
46
|
+
grantUrl
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const SENDER_NAME_TTL_MS = 600 * 1e3;
|
|
50
|
+
const SENDER_NAME_LOOKUP_BUDGET_MS = 1500;
|
|
51
|
+
const senderNameCache = /* @__PURE__ */ new Map();
|
|
52
|
+
const permissionErrorNotifiedAt = /* @__PURE__ */ new Map();
|
|
53
|
+
const PERMISSION_ERROR_COOLDOWN_MS = 300 * 1e3;
|
|
54
|
+
function getCachedSenderName(senderId) {
|
|
55
|
+
const normalizedSenderId = senderId.trim();
|
|
56
|
+
if (!normalizedSenderId) return;
|
|
57
|
+
const cached = senderNameCache.get(normalizedSenderId);
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (!cached || cached.expireAt <= now) return;
|
|
60
|
+
return cached.name;
|
|
61
|
+
}
|
|
62
|
+
function resolveSenderLookupIdType(senderId) {
|
|
63
|
+
const trimmed = senderId.trim();
|
|
64
|
+
if (trimmed.startsWith("ou_")) return "open_id";
|
|
65
|
+
if (trimmed.startsWith("on_")) return "union_id";
|
|
66
|
+
return "user_id";
|
|
67
|
+
}
|
|
68
|
+
async function resolveFeishuSenderName(params) {
|
|
69
|
+
const { account, senderId, log } = params;
|
|
70
|
+
if (!account.configured) return {};
|
|
71
|
+
const normalizedSenderId = senderId.trim();
|
|
72
|
+
if (!normalizedSenderId) return {};
|
|
73
|
+
const cached = senderNameCache.get(normalizedSenderId);
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
if (cached && cached.expireAt > now) return { name: cached.name };
|
|
76
|
+
try {
|
|
77
|
+
const client = createFeishuClient(account);
|
|
78
|
+
const userIdType = resolveSenderLookupIdType(normalizedSenderId);
|
|
79
|
+
const res = await client.contact.user.get({
|
|
80
|
+
path: { user_id: normalizedSenderId },
|
|
81
|
+
params: { user_id_type: userIdType }
|
|
82
|
+
});
|
|
83
|
+
const name = res?.data?.user?.name || res?.data?.user?.display_name || res?.data?.user?.nickname || res?.data?.user?.en_name;
|
|
84
|
+
if (name && typeof name === "string") {
|
|
85
|
+
senderNameCache.set(normalizedSenderId, {
|
|
86
|
+
name,
|
|
87
|
+
expireAt: now + SENDER_NAME_TTL_MS
|
|
88
|
+
});
|
|
89
|
+
return { name };
|
|
90
|
+
}
|
|
91
|
+
return {};
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const permErr = extractPermissionError(err);
|
|
94
|
+
if (permErr) {
|
|
95
|
+
if (shouldSuppressPermissionErrorNotice(permErr)) {
|
|
96
|
+
log(`feishu: ignoring stale permission scope error: ${permErr.message}`);
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
log(`feishu: permission error resolving sender name: code=${permErr.code}`);
|
|
100
|
+
return { permissionError: permErr };
|
|
101
|
+
}
|
|
102
|
+
log(`feishu: failed to resolve sender name for ${normalizedSenderId}: ${String(err)}`);
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function resolveFeishuSenderNameWithinBudget(params) {
|
|
107
|
+
const cachedName = getCachedSenderName(params.senderId);
|
|
108
|
+
if (cachedName) return { name: cachedName };
|
|
109
|
+
const timeoutMs = params.timeoutMs ?? SENDER_NAME_LOOKUP_BUDGET_MS;
|
|
110
|
+
const lookupPromise = resolveFeishuSenderName(params);
|
|
111
|
+
const lookupResult = await raceWithTimeoutAndAbort(lookupPromise, { timeoutMs });
|
|
112
|
+
if (lookupResult.status === "resolved") return lookupResult.value;
|
|
113
|
+
params.log(`feishu[${params.account.accountId}]: sender-name lookup exceeded ${timeoutMs}ms; continuing without blocking reply`);
|
|
114
|
+
lookupPromise.catch(() => void 0);
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
function resolveFeishuGroupSession(params) {
|
|
118
|
+
const { chatId, senderOpenId, messageId, rootId, threadId, groupConfig, feishuCfg } = params;
|
|
119
|
+
const normalizedThreadId = threadId?.trim();
|
|
120
|
+
const normalizedRootId = rootId?.trim();
|
|
121
|
+
const threadReply = Boolean(normalizedThreadId || normalizedRootId);
|
|
122
|
+
const replyInThread = (groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled" || threadReply;
|
|
123
|
+
const legacyTopicSessionMode = groupConfig?.topicSessionMode ?? feishuCfg?.topicSessionMode ?? "disabled";
|
|
124
|
+
const groupSessionScope = groupConfig?.groupSessionScope ?? feishuCfg?.groupSessionScope ?? (legacyTopicSessionMode === "enabled" ? "group_topic" : "group");
|
|
125
|
+
const topicScope = groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender" ? normalizedRootId ?? normalizedThreadId ?? (replyInThread ? messageId : null) : null;
|
|
126
|
+
let peerId = chatId;
|
|
127
|
+
switch (groupSessionScope) {
|
|
128
|
+
case "group_sender":
|
|
129
|
+
peerId = `${chatId}:sender:${senderOpenId}`;
|
|
130
|
+
break;
|
|
131
|
+
case "group_topic":
|
|
132
|
+
peerId = topicScope ? `${chatId}:topic:${topicScope}` : chatId;
|
|
133
|
+
break;
|
|
134
|
+
case "group_topic_sender":
|
|
135
|
+
peerId = topicScope ? `${chatId}:topic:${topicScope}:sender:${senderOpenId}` : `${chatId}:sender:${senderOpenId}`;
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
peerId = chatId;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
peerId,
|
|
143
|
+
parentPeer: topicScope && (groupSessionScope === "group_topic" || groupSessionScope === "group_topic_sender") ? {
|
|
144
|
+
kind: "group",
|
|
145
|
+
id: chatId
|
|
146
|
+
} : null,
|
|
147
|
+
groupSessionScope,
|
|
148
|
+
replyInThread,
|
|
149
|
+
threadReply
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function parseMessageContent(content, messageType) {
|
|
153
|
+
if (messageType === "post") {
|
|
154
|
+
const { textContent } = parsePostContent(content);
|
|
155
|
+
return textContent;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(content);
|
|
159
|
+
if (messageType === "text") return parsed.text || "";
|
|
160
|
+
if (messageType === "share_chat") {
|
|
161
|
+
if (parsed && typeof parsed === "object") {
|
|
162
|
+
const share = parsed;
|
|
163
|
+
if (typeof share.body === "string" && share.body.trim().length > 0) return share.body.trim();
|
|
164
|
+
if (typeof share.summary === "string" && share.summary.trim().length > 0) return share.summary.trim();
|
|
165
|
+
if (typeof share.share_chat_id === "string" && share.share_chat_id.trim().length > 0) return `[Forwarded message: ${share.share_chat_id.trim()}]`;
|
|
166
|
+
}
|
|
167
|
+
return "[Forwarded message]";
|
|
168
|
+
}
|
|
169
|
+
if (messageType === "merge_forward") return "[Merged and Forwarded Message - loading...]";
|
|
170
|
+
return content;
|
|
171
|
+
} catch {
|
|
172
|
+
return content;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Parse merge_forward message content and fetch sub-messages.
|
|
177
|
+
* Returns formatted text content of all sub-messages.
|
|
178
|
+
*/
|
|
179
|
+
function parseMergeForwardContent(params) {
|
|
180
|
+
const { content, log } = params;
|
|
181
|
+
const maxMessages = 50;
|
|
182
|
+
log?.(`feishu: parsing merge_forward sub-messages from API response`);
|
|
183
|
+
let items;
|
|
184
|
+
try {
|
|
185
|
+
items = JSON.parse(content);
|
|
186
|
+
} catch {
|
|
187
|
+
log?.(`feishu: merge_forward items parse failed`);
|
|
188
|
+
return "[Merged and Forwarded Message - parse error]";
|
|
189
|
+
}
|
|
190
|
+
if (!Array.isArray(items) || items.length === 0) return "[Merged and Forwarded Message - no sub-messages]";
|
|
191
|
+
const subMessages = items.filter((item) => item.upper_message_id);
|
|
192
|
+
if (subMessages.length === 0) return "[Merged and Forwarded Message - no sub-messages found]";
|
|
193
|
+
log?.(`feishu: merge_forward contains ${subMessages.length} sub-messages`);
|
|
194
|
+
subMessages.sort((a, b) => {
|
|
195
|
+
return parseInt(a.create_time || "0", 10) - parseInt(b.create_time || "0", 10);
|
|
196
|
+
});
|
|
197
|
+
const lines = ["[Merged and Forwarded Messages]"];
|
|
198
|
+
const limitedMessages = subMessages.slice(0, maxMessages);
|
|
199
|
+
for (const item of limitedMessages) {
|
|
200
|
+
const formatted = formatSubMessageContent(item.body?.content || "", item.msg_type || "text");
|
|
201
|
+
lines.push(`- ${formatted}`);
|
|
202
|
+
}
|
|
203
|
+
if (subMessages.length > maxMessages) lines.push(`... and ${subMessages.length - maxMessages} more messages`);
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Format sub-message content based on message type.
|
|
208
|
+
*/
|
|
209
|
+
function formatSubMessageContent(content, contentType) {
|
|
210
|
+
try {
|
|
211
|
+
const parsed = JSON.parse(content);
|
|
212
|
+
switch (contentType) {
|
|
213
|
+
case "text": return parsed.text || content;
|
|
214
|
+
case "post": {
|
|
215
|
+
const { textContent } = parsePostContent(content);
|
|
216
|
+
return textContent;
|
|
217
|
+
}
|
|
218
|
+
case "image": return "[Image]";
|
|
219
|
+
case "file": return `[File: ${parsed.file_name || "unknown"}]`;
|
|
220
|
+
case "audio": return "[Audio]";
|
|
221
|
+
case "video": return "[Video]";
|
|
222
|
+
case "sticker": return "[Sticker]";
|
|
223
|
+
case "merge_forward": return "[Nested Merged Forward]";
|
|
224
|
+
default: return `[${contentType}]`;
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
return content;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function checkBotMentioned(event, botOpenId) {
|
|
231
|
+
if (!botOpenId) return false;
|
|
232
|
+
if ((event.message.content ?? "").includes("@_all")) return true;
|
|
233
|
+
const mentions = event.message.mentions ?? [];
|
|
234
|
+
if (mentions.length > 0) return mentions.some((m) => m.id.open_id === botOpenId);
|
|
235
|
+
if (event.message.message_type === "post") {
|
|
236
|
+
const { mentionedOpenIds } = parsePostContent(event.message.content);
|
|
237
|
+
return mentionedOpenIds.some((id) => id === botOpenId);
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function normalizeMentions(text, mentions, botStripId) {
|
|
242
|
+
if (!mentions || mentions.length === 0) return text;
|
|
243
|
+
const escaped = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
244
|
+
const escapeName = (value) => value.replace(/</g, "<").replace(/>/g, ">");
|
|
245
|
+
let result = text;
|
|
246
|
+
for (const mention of mentions) {
|
|
247
|
+
const mentionId = mention.id.open_id;
|
|
248
|
+
const replacement = botStripId && mentionId === botStripId ? "" : mentionId ? `<at user_id="${mentionId}">${escapeName(mention.name)}</at>` : `@${mention.name}`;
|
|
249
|
+
result = result.replace(new RegExp(escaped(mention.key), "g"), () => replacement).trim();
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
function normalizeFeishuCommandProbeBody(text) {
|
|
254
|
+
if (!text) return "";
|
|
255
|
+
return text.replace(/<at\b[^>]*>[^<]*<\/at>/giu, " ").replace(/(^|\s)@[^/\s]+(?=\s|$|\/)/gu, "$1").replace(/\s+/g, " ").trim();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Parse media keys from message content based on message type.
|
|
259
|
+
*/
|
|
260
|
+
function parseMediaKeys(content, messageType) {
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(content);
|
|
263
|
+
const imageKey = normalizeFeishuExternalKey(parsed.image_key);
|
|
264
|
+
const fileKey = normalizeFeishuExternalKey(parsed.file_key);
|
|
265
|
+
switch (messageType) {
|
|
266
|
+
case "image": return { imageKey };
|
|
267
|
+
case "file": return {
|
|
268
|
+
fileKey,
|
|
269
|
+
fileName: parsed.file_name
|
|
270
|
+
};
|
|
271
|
+
case "audio": return { fileKey };
|
|
272
|
+
case "video":
|
|
273
|
+
case "media": return {
|
|
274
|
+
fileKey,
|
|
275
|
+
imageKey
|
|
276
|
+
};
|
|
277
|
+
case "sticker": return { fileKey };
|
|
278
|
+
default: return {};
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Map Feishu message type to messageResource.get resource type.
|
|
286
|
+
* Feishu messageResource API supports only: image | file.
|
|
287
|
+
*/
|
|
288
|
+
function toMessageResourceType(messageType) {
|
|
289
|
+
return messageType === "image" ? "image" : "file";
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Infer placeholder text based on message type.
|
|
293
|
+
*/
|
|
294
|
+
function inferPlaceholder(messageType) {
|
|
295
|
+
switch (messageType) {
|
|
296
|
+
case "image": return "<media:image>";
|
|
297
|
+
case "file": return "<media:document>";
|
|
298
|
+
case "audio": return "<media:audio>";
|
|
299
|
+
case "video":
|
|
300
|
+
case "media": return "<media:video>";
|
|
301
|
+
case "sticker": return "<media:sticker>";
|
|
302
|
+
default: return "<media:document>";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Resolve media from a Feishu message, downloading and saving to disk.
|
|
307
|
+
* Similar to Discord's resolveMediaList().
|
|
308
|
+
*/
|
|
309
|
+
async function resolveFeishuMediaList(params) {
|
|
310
|
+
const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
|
|
311
|
+
if (![
|
|
312
|
+
"image",
|
|
313
|
+
"file",
|
|
314
|
+
"audio",
|
|
315
|
+
"video",
|
|
316
|
+
"media",
|
|
317
|
+
"sticker",
|
|
318
|
+
"post"
|
|
319
|
+
].includes(messageType)) return [];
|
|
320
|
+
const out = [];
|
|
321
|
+
const core = getFeishuRuntime();
|
|
322
|
+
if (messageType === "post") {
|
|
323
|
+
const { imageKeys, mediaKeys: postMediaKeys } = parsePostContent(content);
|
|
324
|
+
if (imageKeys.length === 0 && postMediaKeys.length === 0) return [];
|
|
325
|
+
if (imageKeys.length > 0) log?.(`feishu: post message contains ${imageKeys.length} embedded image(s)`);
|
|
326
|
+
if (postMediaKeys.length > 0) log?.(`feishu: post message contains ${postMediaKeys.length} embedded media file(s)`);
|
|
327
|
+
for (const imageKey of imageKeys) try {
|
|
328
|
+
const result = await downloadMessageResourceFeishu({
|
|
329
|
+
cfg,
|
|
330
|
+
messageId,
|
|
331
|
+
fileKey: imageKey,
|
|
332
|
+
type: "image",
|
|
333
|
+
accountId
|
|
334
|
+
});
|
|
335
|
+
let contentType = result.contentType;
|
|
336
|
+
if (!contentType) contentType = await core.media.detectMime({ buffer: result.buffer });
|
|
337
|
+
const saved = await core.channel.media.saveMediaBuffer(result.buffer, contentType, "inbound", maxBytes);
|
|
338
|
+
out.push({
|
|
339
|
+
path: saved.path,
|
|
340
|
+
contentType: saved.contentType,
|
|
341
|
+
placeholder: "<media:image>"
|
|
342
|
+
});
|
|
343
|
+
log?.(`feishu: downloaded embedded image ${imageKey}, saved to ${saved.path}`);
|
|
344
|
+
} catch (err) {
|
|
345
|
+
log?.(`feishu: failed to download embedded image ${imageKey}: ${String(err)}`);
|
|
346
|
+
}
|
|
347
|
+
for (const media of postMediaKeys) try {
|
|
348
|
+
const result = await downloadMessageResourceFeishu({
|
|
349
|
+
cfg,
|
|
350
|
+
messageId,
|
|
351
|
+
fileKey: media.fileKey,
|
|
352
|
+
type: "file",
|
|
353
|
+
accountId
|
|
354
|
+
});
|
|
355
|
+
let contentType = result.contentType;
|
|
356
|
+
if (!contentType) contentType = await core.media.detectMime({ buffer: result.buffer });
|
|
357
|
+
const saved = await core.channel.media.saveMediaBuffer(result.buffer, contentType, "inbound", maxBytes);
|
|
358
|
+
out.push({
|
|
359
|
+
path: saved.path,
|
|
360
|
+
contentType: saved.contentType,
|
|
361
|
+
placeholder: "<media:video>"
|
|
362
|
+
});
|
|
363
|
+
log?.(`feishu: downloaded embedded media ${media.fileKey}, saved to ${saved.path}`);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
log?.(`feishu: failed to download embedded media ${media.fileKey}: ${String(err)}`);
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
const mediaKeys = parseMediaKeys(content, messageType);
|
|
370
|
+
if (!mediaKeys.imageKey && !mediaKeys.fileKey) return [];
|
|
371
|
+
try {
|
|
372
|
+
let buffer;
|
|
373
|
+
let contentType;
|
|
374
|
+
let fileName;
|
|
375
|
+
const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
|
|
376
|
+
if (!fileKey) return [];
|
|
377
|
+
const result = await downloadMessageResourceFeishu({
|
|
378
|
+
cfg,
|
|
379
|
+
messageId,
|
|
380
|
+
fileKey,
|
|
381
|
+
type: toMessageResourceType(messageType),
|
|
382
|
+
accountId
|
|
383
|
+
});
|
|
384
|
+
buffer = result.buffer;
|
|
385
|
+
contentType = result.contentType;
|
|
386
|
+
fileName = result.fileName || mediaKeys.fileName;
|
|
387
|
+
if (!contentType) contentType = await core.media.detectMime({ buffer });
|
|
388
|
+
const saved = await core.channel.media.saveMediaBuffer(buffer, contentType, "inbound", maxBytes, fileName);
|
|
389
|
+
out.push({
|
|
390
|
+
path: saved.path,
|
|
391
|
+
contentType: saved.contentType,
|
|
392
|
+
placeholder: inferPlaceholder(messageType)
|
|
393
|
+
});
|
|
394
|
+
log?.(`feishu: downloaded ${messageType} media, saved to ${saved.path}`);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
log?.(`feishu: failed to download ${messageType} media: ${String(err)}`);
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
function resolveBroadcastAgents(cfg, peerId) {
|
|
401
|
+
const broadcast = cfg.broadcast;
|
|
402
|
+
if (!broadcast || typeof broadcast !== "object") return null;
|
|
403
|
+
const agents = broadcast[peerId];
|
|
404
|
+
if (!Array.isArray(agents) || agents.length === 0) return null;
|
|
405
|
+
return agents;
|
|
406
|
+
}
|
|
407
|
+
function buildBroadcastSessionKey(baseSessionKey, originalAgentId, targetAgentId) {
|
|
408
|
+
const prefix = `agent:${originalAgentId}:`;
|
|
409
|
+
if (baseSessionKey.startsWith(prefix)) return `agent:${targetAgentId}:${baseSessionKey.slice(prefix.length)}`;
|
|
410
|
+
return baseSessionKey;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Build media payload for inbound context.
|
|
414
|
+
* Similar to Discord's buildDiscordMediaPayload().
|
|
415
|
+
*/
|
|
416
|
+
function parseFeishuMessageEvent(event, botOpenId, _botName) {
|
|
417
|
+
const rawContent = parseMessageContent(event.message.content, event.message.message_type);
|
|
418
|
+
const mentionedBot = checkBotMentioned(event, botOpenId);
|
|
419
|
+
const hasAnyMention = (event.message.mentions?.length ?? 0) > 0;
|
|
420
|
+
const content = normalizeMentions(rawContent, event.message.mentions, botOpenId);
|
|
421
|
+
const senderOpenId = event.sender.sender_id.open_id?.trim();
|
|
422
|
+
const senderUserId = event.sender.sender_id.user_id?.trim();
|
|
423
|
+
const senderFallbackId = senderOpenId || senderUserId || "";
|
|
424
|
+
const ctx = {
|
|
425
|
+
chatId: event.message.chat_id,
|
|
426
|
+
messageId: event.message.message_id,
|
|
427
|
+
senderId: senderUserId || senderOpenId || "",
|
|
428
|
+
senderOpenId: senderFallbackId,
|
|
429
|
+
chatType: event.message.chat_type,
|
|
430
|
+
mentionedBot,
|
|
431
|
+
hasAnyMention,
|
|
432
|
+
rootId: event.message.root_id || void 0,
|
|
433
|
+
parentId: event.message.parent_id || void 0,
|
|
434
|
+
threadId: event.message.thread_id || void 0,
|
|
435
|
+
content,
|
|
436
|
+
contentType: event.message.message_type
|
|
437
|
+
};
|
|
438
|
+
if (isMentionForwardRequest(event, botOpenId)) {
|
|
439
|
+
const mentionTargets = extractMentionTargets(event, botOpenId);
|
|
440
|
+
if (mentionTargets.length > 0) ctx.mentionTargets = mentionTargets;
|
|
441
|
+
}
|
|
442
|
+
return ctx;
|
|
443
|
+
}
|
|
444
|
+
function buildFeishuAgentBody(params) {
|
|
445
|
+
const { ctx, quotedContent, permissionErrorForAgent, botOpenId } = params;
|
|
446
|
+
let messageBody = ctx.content;
|
|
447
|
+
if (quotedContent) messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
|
|
448
|
+
messageBody = `${ctx.senderName ?? ctx.senderOpenId}: ${messageBody}`;
|
|
449
|
+
if (ctx.hasAnyMention) {
|
|
450
|
+
const botIdHint = botOpenId?.trim();
|
|
451
|
+
messageBody += "\n\n[System: The content may include mention tags in the form <at user_id=\"...\">name</at>. Treat these as real mentions of Feishu entities (users or bots).]";
|
|
452
|
+
if (botIdHint) messageBody += `\n[System: If user_id is "${botIdHint}", that mention refers to you.]`;
|
|
453
|
+
}
|
|
454
|
+
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
|
455
|
+
const targetNames = ctx.mentionTargets.map((t) => t.name).join(", ");
|
|
456
|
+
messageBody += `\n\n[System: Your reply will automatically @mention: ${targetNames}. Do not write @xxx yourself.]`;
|
|
457
|
+
}
|
|
458
|
+
messageBody = `[message_id: ${ctx.messageId}]\n${messageBody}`;
|
|
459
|
+
if (permissionErrorForAgent) {
|
|
460
|
+
const grantUrl = permissionErrorForAgent.grantUrl ?? "";
|
|
461
|
+
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}]`;
|
|
462
|
+
}
|
|
463
|
+
return messageBody;
|
|
464
|
+
}
|
|
465
|
+
async function handleFeishuMessage(params) {
|
|
466
|
+
const { cfg, event, botOpenId, botName, runtime, chatHistories, accountId, processingClaimHeld = false } = params;
|
|
467
|
+
const account = resolveFeishuAccount({
|
|
468
|
+
cfg,
|
|
469
|
+
accountId
|
|
470
|
+
});
|
|
471
|
+
const feishuCfg = account.config;
|
|
472
|
+
const log = runtime?.log ?? console.log;
|
|
473
|
+
const error = runtime?.error ?? console.error;
|
|
474
|
+
const messageId = event.message.message_id;
|
|
475
|
+
if (!await finalizeFeishuMessageProcessing({
|
|
476
|
+
messageId,
|
|
477
|
+
namespace: account.accountId,
|
|
478
|
+
log,
|
|
479
|
+
claimHeld: processingClaimHeld
|
|
480
|
+
})) {
|
|
481
|
+
log(`feishu: skipping duplicate message ${messageId}`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
let ctx = parseFeishuMessageEvent(event, botOpenId, botName);
|
|
485
|
+
const isGroup = ctx.chatType === "group";
|
|
486
|
+
const isDirect = !isGroup;
|
|
487
|
+
const senderUserId = event.sender.sender_id.user_id?.trim() || void 0;
|
|
488
|
+
if (event.message.message_type === "merge_forward") {
|
|
489
|
+
log(`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`);
|
|
490
|
+
try {
|
|
491
|
+
const response = await createFeishuClient(account).im.message.get({ path: { message_id: event.message.message_id } });
|
|
492
|
+
if (response.code === 0 && response.data?.items && response.data.items.length > 0) {
|
|
493
|
+
log(`feishu[${account.accountId}]: merge_forward API returned ${response.data.items.length} items`);
|
|
494
|
+
const expandedContent = parseMergeForwardContent({
|
|
495
|
+
content: JSON.stringify(response.data.items),
|
|
496
|
+
log
|
|
497
|
+
});
|
|
498
|
+
ctx = {
|
|
499
|
+
...ctx,
|
|
500
|
+
content: expandedContent
|
|
501
|
+
};
|
|
502
|
+
} else {
|
|
503
|
+
log(`feishu[${account.accountId}]: merge_forward API returned no items`);
|
|
504
|
+
ctx = {
|
|
505
|
+
...ctx,
|
|
506
|
+
content: "[Merged and Forwarded Message - could not fetch]"
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
} catch (err) {
|
|
510
|
+
log(`feishu[${account.accountId}]: merge_forward fetch failed: ${String(err)}`);
|
|
511
|
+
ctx = {
|
|
512
|
+
...ctx,
|
|
513
|
+
content: "[Merged and Forwarded Message - fetch error]"
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
let permissionErrorForAgent;
|
|
518
|
+
if (feishuCfg?.resolveSenderNames ?? true) {
|
|
519
|
+
const senderResult = await resolveFeishuSenderNameWithinBudget({
|
|
520
|
+
account,
|
|
521
|
+
senderId: ctx.senderOpenId,
|
|
522
|
+
log
|
|
523
|
+
});
|
|
524
|
+
if (senderResult.name) ctx = {
|
|
525
|
+
...ctx,
|
|
526
|
+
senderName: senderResult.name
|
|
527
|
+
};
|
|
528
|
+
if (senderResult.permissionError) {
|
|
529
|
+
const appKey = account.appId ?? "default";
|
|
530
|
+
const now = Date.now();
|
|
531
|
+
if (now - (permissionErrorNotifiedAt.get(appKey) ?? 0) > PERMISSION_ERROR_COOLDOWN_MS) {
|
|
532
|
+
permissionErrorNotifiedAt.set(appKey, now);
|
|
533
|
+
permissionErrorForAgent = senderResult.permissionError;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
log(`feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
|
|
538
|
+
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
|
539
|
+
const names = ctx.mentionTargets.map((t) => t.name).join(", ");
|
|
540
|
+
log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
|
|
541
|
+
}
|
|
542
|
+
const historyLimit = Math.max(0, feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 50);
|
|
543
|
+
const groupConfig = isGroup ? resolveFeishuGroupConfig({
|
|
544
|
+
cfg: feishuCfg,
|
|
545
|
+
groupId: ctx.chatId
|
|
546
|
+
}) : void 0;
|
|
547
|
+
const groupSession = isGroup ? resolveFeishuGroupSession({
|
|
548
|
+
chatId: ctx.chatId,
|
|
549
|
+
senderOpenId: ctx.senderOpenId,
|
|
550
|
+
messageId: ctx.messageId,
|
|
551
|
+
rootId: ctx.rootId,
|
|
552
|
+
threadId: ctx.threadId,
|
|
553
|
+
groupConfig,
|
|
554
|
+
feishuCfg
|
|
555
|
+
}) : null;
|
|
556
|
+
const groupHistoryKey = isGroup ? groupSession?.peerId ?? ctx.chatId : void 0;
|
|
557
|
+
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
|
558
|
+
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
|
559
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
560
|
+
const rawBroadcastAgents = isGroup ? resolveBroadcastAgents(cfg, ctx.chatId) : null;
|
|
561
|
+
const broadcastAgents = rawBroadcastAgents ? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))] : null;
|
|
562
|
+
let requireMention = false;
|
|
563
|
+
if (isGroup) {
|
|
564
|
+
if (groupConfig?.enabled === false) {
|
|
565
|
+
log(`feishu[${account.accountId}]: group ${ctx.chatId} is disabled`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
569
|
+
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
|
|
570
|
+
providerConfigPresent: cfg.channels?.feishu !== void 0,
|
|
571
|
+
groupPolicy: feishuCfg?.groupPolicy,
|
|
572
|
+
defaultGroupPolicy
|
|
573
|
+
});
|
|
574
|
+
warnMissingProviderGroupPolicyFallbackOnce({
|
|
575
|
+
providerMissingFallbackApplied,
|
|
576
|
+
providerKey: "feishu",
|
|
577
|
+
accountId: account.accountId,
|
|
578
|
+
log
|
|
579
|
+
});
|
|
580
|
+
if (!isFeishuGroupAllowed({
|
|
581
|
+
groupPolicy,
|
|
582
|
+
allowFrom: feishuCfg?.groupAllowFrom ?? [],
|
|
583
|
+
senderId: ctx.chatId,
|
|
584
|
+
senderName: void 0
|
|
585
|
+
})) {
|
|
586
|
+
log(`feishu[${account.accountId}]: group ${ctx.chatId} not in groupAllowFrom (groupPolicy=${groupPolicy})`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const perGroupSenderAllowFrom = groupConfig?.allowFrom ?? [];
|
|
590
|
+
const globalSenderAllowFrom = feishuCfg?.groupSenderAllowFrom ?? [];
|
|
591
|
+
const effectiveSenderAllowFrom = perGroupSenderAllowFrom.length > 0 ? perGroupSenderAllowFrom : globalSenderAllowFrom;
|
|
592
|
+
if (effectiveSenderAllowFrom.length > 0) {
|
|
593
|
+
if (!isFeishuGroupAllowed({
|
|
594
|
+
groupPolicy: "allowlist",
|
|
595
|
+
allowFrom: effectiveSenderAllowFrom,
|
|
596
|
+
senderId: ctx.senderOpenId,
|
|
597
|
+
senderIds: [senderUserId],
|
|
598
|
+
senderName: ctx.senderName
|
|
599
|
+
})) {
|
|
600
|
+
log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
({requireMention} = resolveFeishuReplyPolicy({
|
|
605
|
+
isDirectMessage: false,
|
|
606
|
+
globalConfig: feishuCfg,
|
|
607
|
+
groupConfig
|
|
608
|
+
}));
|
|
609
|
+
if (requireMention && !ctx.mentionedBot) {
|
|
610
|
+
log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot`);
|
|
611
|
+
if (!broadcastAgents && chatHistories && groupHistoryKey) recordPendingHistoryEntryIfEnabled({
|
|
612
|
+
historyMap: chatHistories,
|
|
613
|
+
historyKey: groupHistoryKey,
|
|
614
|
+
limit: historyLimit,
|
|
615
|
+
entry: {
|
|
616
|
+
sender: ctx.senderOpenId,
|
|
617
|
+
body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
|
|
618
|
+
timestamp: Date.now(),
|
|
619
|
+
messageId: ctx.messageId
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
const core = getFeishuRuntime();
|
|
627
|
+
const pairing = createScopedPairingAccess({
|
|
628
|
+
core,
|
|
629
|
+
channel: "feishu",
|
|
630
|
+
accountId: account.accountId
|
|
631
|
+
});
|
|
632
|
+
const commandProbeBody = isGroup ? normalizeFeishuCommandProbeBody(ctx.content) : ctx.content;
|
|
633
|
+
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(commandProbeBody, cfg);
|
|
634
|
+
const storeAllowFrom = !isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeCommandAuthorized) ? await pairing.readAllowFromStore().catch(() => []) : [];
|
|
635
|
+
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
636
|
+
const dmAllowed = resolveFeishuAllowlistMatch({
|
|
637
|
+
allowFrom: effectiveDmAllowFrom,
|
|
638
|
+
senderId: ctx.senderOpenId,
|
|
639
|
+
senderIds: [senderUserId],
|
|
640
|
+
senderName: ctx.senderName
|
|
641
|
+
}).allowed;
|
|
642
|
+
if (isDirect && dmPolicy !== "open" && !dmAllowed) {
|
|
643
|
+
if (dmPolicy === "pairing") await issuePairingChallenge({
|
|
644
|
+
channel: "feishu",
|
|
645
|
+
senderId: ctx.senderOpenId,
|
|
646
|
+
senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
|
647
|
+
meta: { name: ctx.senderName },
|
|
648
|
+
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
649
|
+
onCreated: () => {
|
|
650
|
+
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
|
651
|
+
},
|
|
652
|
+
sendPairingReply: async (text) => {
|
|
653
|
+
await sendMessageFeishu({
|
|
654
|
+
cfg,
|
|
655
|
+
to: `chat:${ctx.chatId}`,
|
|
656
|
+
text,
|
|
657
|
+
accountId: account.accountId
|
|
658
|
+
});
|
|
659
|
+
},
|
|
660
|
+
onReplyError: (err) => {
|
|
661
|
+
log(`feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
else log(`feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const commandAllowFrom = isGroup ? groupConfig?.allowFrom ?? configAllowFrom : effectiveDmAllowFrom;
|
|
668
|
+
const senderAllowedForCommands = resolveFeishuAllowlistMatch({
|
|
669
|
+
allowFrom: commandAllowFrom,
|
|
670
|
+
senderId: ctx.senderOpenId,
|
|
671
|
+
senderIds: [senderUserId],
|
|
672
|
+
senderName: ctx.senderName
|
|
673
|
+
}).allowed;
|
|
674
|
+
const commandAuthorized = shouldComputeCommandAuthorized ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
675
|
+
useAccessGroups,
|
|
676
|
+
authorizers: [{
|
|
677
|
+
configured: commandAllowFrom.length > 0,
|
|
678
|
+
allowed: senderAllowedForCommands
|
|
679
|
+
}]
|
|
680
|
+
}) : void 0;
|
|
681
|
+
const feishuFrom = `feishu:${ctx.senderOpenId}`;
|
|
682
|
+
const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
|
|
683
|
+
const peerId = isGroup ? groupSession?.peerId ?? ctx.chatId : ctx.senderOpenId;
|
|
684
|
+
const parentPeer = isGroup ? groupSession?.parentPeer ?? null : null;
|
|
685
|
+
const replyInThread = isGroup ? groupSession?.replyInThread ?? false : false;
|
|
686
|
+
if (isGroup && groupSession) log(`feishu[${account.accountId}]: group session scope=${groupSession.groupSessionScope}, peer=${peerId}`);
|
|
687
|
+
let route = core.channel.routing.resolveAgentRoute({
|
|
688
|
+
cfg,
|
|
689
|
+
channel: "feishu",
|
|
690
|
+
accountId: account.accountId,
|
|
691
|
+
peer: {
|
|
692
|
+
kind: isGroup ? "group" : "direct",
|
|
693
|
+
id: peerId
|
|
694
|
+
},
|
|
695
|
+
parentPeer
|
|
696
|
+
});
|
|
697
|
+
if (!isGroup && route.matchedBy === "default") {
|
|
698
|
+
const dynamicCfg = feishuCfg?.dynamicAgentCreation;
|
|
699
|
+
if (dynamicCfg?.enabled) {
|
|
700
|
+
const result = await maybeCreateDynamicAgent({
|
|
701
|
+
cfg,
|
|
702
|
+
runtime: getFeishuRuntime(),
|
|
703
|
+
senderOpenId: ctx.senderOpenId,
|
|
704
|
+
dynamicCfg,
|
|
705
|
+
log: (msg) => log(msg)
|
|
706
|
+
});
|
|
707
|
+
if (result.created) {
|
|
708
|
+
result.updatedCfg;
|
|
709
|
+
route = core.channel.routing.resolveAgentRoute({
|
|
710
|
+
cfg: result.updatedCfg,
|
|
711
|
+
channel: "feishu",
|
|
712
|
+
accountId: account.accountId,
|
|
713
|
+
peer: {
|
|
714
|
+
kind: "direct",
|
|
715
|
+
id: ctx.senderOpenId
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
log(`feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
|
|
723
|
+
const inboundLabel = isGroup ? `Feishu[${account.accountId}] message in group ${ctx.chatId}` : `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
|
|
724
|
+
log(`feishu[${account.accountId}]: ${inboundLabel}: ${preview}`);
|
|
725
|
+
const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
726
|
+
const mediaPayload = buildAgentMediaPayload(await resolveFeishuMediaList({
|
|
727
|
+
cfg,
|
|
728
|
+
messageId: ctx.messageId,
|
|
729
|
+
messageType: event.message.message_type,
|
|
730
|
+
content: event.message.content,
|
|
731
|
+
maxBytes: mediaMaxBytes,
|
|
732
|
+
log,
|
|
733
|
+
accountId: account.accountId
|
|
734
|
+
}));
|
|
735
|
+
let quotedContent;
|
|
736
|
+
if (ctx.parentId) try {
|
|
737
|
+
const quotedMsg = await getMessageFeishu({
|
|
738
|
+
cfg,
|
|
739
|
+
messageId: ctx.parentId,
|
|
740
|
+
accountId: account.accountId
|
|
741
|
+
});
|
|
742
|
+
if (quotedMsg) {
|
|
743
|
+
quotedContent = quotedMsg.content;
|
|
744
|
+
log(`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`);
|
|
745
|
+
}
|
|
746
|
+
} catch (err) {
|
|
747
|
+
log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
|
|
748
|
+
}
|
|
749
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
750
|
+
const messageBody = buildFeishuAgentBody({
|
|
751
|
+
ctx,
|
|
752
|
+
quotedContent,
|
|
753
|
+
permissionErrorForAgent,
|
|
754
|
+
botOpenId
|
|
755
|
+
});
|
|
756
|
+
const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
|
|
757
|
+
if (permissionErrorForAgent) log(`feishu[${account.accountId}]: appending permission error notice to message body`);
|
|
758
|
+
let combinedBody = core.channel.reply.formatAgentEnvelope({
|
|
759
|
+
channel: "Feishu",
|
|
760
|
+
from: envelopeFrom,
|
|
761
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
762
|
+
envelope: envelopeOptions,
|
|
763
|
+
body: messageBody
|
|
764
|
+
});
|
|
765
|
+
const historyKey = groupHistoryKey;
|
|
766
|
+
if (isGroup && historyKey && chatHistories) combinedBody = buildPendingHistoryContextFromMap({
|
|
767
|
+
historyMap: chatHistories,
|
|
768
|
+
historyKey,
|
|
769
|
+
limit: historyLimit,
|
|
770
|
+
currentMessage: combinedBody,
|
|
771
|
+
formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
|
|
772
|
+
channel: "Feishu",
|
|
773
|
+
from: `${ctx.chatId}:${entry.sender}`,
|
|
774
|
+
timestamp: entry.timestamp,
|
|
775
|
+
body: entry.body,
|
|
776
|
+
envelope: envelopeOptions
|
|
777
|
+
})
|
|
778
|
+
});
|
|
779
|
+
const inboundHistory = isGroup && historyKey && historyLimit > 0 && chatHistories ? (chatHistories.get(historyKey) ?? []).map((entry) => ({
|
|
780
|
+
sender: entry.sender,
|
|
781
|
+
body: entry.body,
|
|
782
|
+
timestamp: entry.timestamp
|
|
783
|
+
})) : void 0;
|
|
784
|
+
const buildCtxPayloadForAgent = (agentSessionKey, agentAccountId, wasMentioned) => core.channel.reply.finalizeInboundContext({
|
|
785
|
+
Body: combinedBody,
|
|
786
|
+
BodyForAgent: messageBody,
|
|
787
|
+
InboundHistory: inboundHistory,
|
|
788
|
+
ReplyToId: ctx.parentId,
|
|
789
|
+
RootMessageId: ctx.rootId,
|
|
790
|
+
RawBody: ctx.content,
|
|
791
|
+
CommandBody: ctx.content,
|
|
792
|
+
From: feishuFrom,
|
|
793
|
+
To: feishuTo,
|
|
794
|
+
SessionKey: agentSessionKey,
|
|
795
|
+
AccountId: agentAccountId,
|
|
796
|
+
ChatType: isGroup ? "group" : "direct",
|
|
797
|
+
GroupSubject: isGroup ? ctx.chatId : void 0,
|
|
798
|
+
SenderName: ctx.senderName ?? ctx.senderOpenId,
|
|
799
|
+
SenderId: ctx.senderOpenId,
|
|
800
|
+
Provider: "feishu",
|
|
801
|
+
Surface: "feishu",
|
|
802
|
+
MessageSid: ctx.messageId,
|
|
803
|
+
ReplyToBody: quotedContent ?? void 0,
|
|
804
|
+
Timestamp: Date.now(),
|
|
805
|
+
WasMentioned: wasMentioned,
|
|
806
|
+
CommandAuthorized: commandAuthorized,
|
|
807
|
+
OriginatingChannel: "feishu",
|
|
808
|
+
OriginatingTo: feishuTo,
|
|
809
|
+
GroupSystemPrompt: isGroup ? groupConfig?.systemPrompt?.trim() || void 0 : void 0,
|
|
810
|
+
...mediaPayload
|
|
811
|
+
});
|
|
812
|
+
const messageCreateTimeMs = event.message.create_time ? parseInt(event.message.create_time, 10) : void 0;
|
|
813
|
+
const isTopicSession = isGroup && (groupSession?.groupSessionScope === "group_topic" || groupSession?.groupSessionScope === "group_topic_sender");
|
|
814
|
+
const configReplyInThread = isGroup && (groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled";
|
|
815
|
+
const replyTargetMessageId = isTopicSession || configReplyInThread ? ctx.rootId ?? ctx.messageId : ctx.messageId;
|
|
816
|
+
const threadReply = isGroup ? groupSession?.threadReply ?? false : false;
|
|
817
|
+
if (broadcastAgents) {
|
|
818
|
+
if (!await tryRecordMessagePersistent(ctx.messageId, "broadcast", log)) {
|
|
819
|
+
log(`feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const strategy = cfg.broadcast?.strategy || "parallel";
|
|
823
|
+
const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId(route.agentId) : null;
|
|
824
|
+
const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId(a.id));
|
|
825
|
+
const hasKnownAgents = agentIds.length > 0;
|
|
826
|
+
log(`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`);
|
|
827
|
+
const dispatchForAgent = async (agentId) => {
|
|
828
|
+
if (hasKnownAgents && !agentIds.includes(normalizeAgentId(agentId))) {
|
|
829
|
+
log(`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
|
|
833
|
+
const agentCtx = buildCtxPayloadForAgent(agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
|
|
834
|
+
if (agentId === activeAgentId) {
|
|
835
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
|
|
836
|
+
cfg,
|
|
837
|
+
agentId,
|
|
838
|
+
runtime,
|
|
839
|
+
chatId: ctx.chatId,
|
|
840
|
+
replyToMessageId: replyTargetMessageId,
|
|
841
|
+
skipReplyToInMessages: !isGroup,
|
|
842
|
+
replyInThread,
|
|
843
|
+
rootId: ctx.rootId,
|
|
844
|
+
threadReply,
|
|
845
|
+
mentionTargets: ctx.mentionTargets,
|
|
846
|
+
accountId: account.accountId,
|
|
847
|
+
messageCreateTimeMs
|
|
848
|
+
});
|
|
849
|
+
log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
|
|
850
|
+
await core.channel.reply.withReplyDispatcher({
|
|
851
|
+
dispatcher,
|
|
852
|
+
onSettled: () => markDispatchIdle(),
|
|
853
|
+
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
854
|
+
ctx: agentCtx,
|
|
855
|
+
cfg,
|
|
856
|
+
dispatcher,
|
|
857
|
+
replyOptions
|
|
858
|
+
})
|
|
859
|
+
});
|
|
860
|
+
} else {
|
|
861
|
+
delete agentCtx.CommandAuthorized;
|
|
862
|
+
const noopDispatcher = {
|
|
863
|
+
sendToolResult: () => false,
|
|
864
|
+
sendBlockReply: () => false,
|
|
865
|
+
sendFinalReply: () => false,
|
|
866
|
+
waitForIdle: async () => {},
|
|
867
|
+
getQueuedCounts: () => ({
|
|
868
|
+
tool: 0,
|
|
869
|
+
block: 0,
|
|
870
|
+
final: 0
|
|
871
|
+
}),
|
|
872
|
+
markComplete: () => {}
|
|
873
|
+
};
|
|
874
|
+
log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
|
|
875
|
+
await core.channel.reply.withReplyDispatcher({
|
|
876
|
+
dispatcher: noopDispatcher,
|
|
877
|
+
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
878
|
+
ctx: agentCtx,
|
|
879
|
+
cfg,
|
|
880
|
+
dispatcher: noopDispatcher
|
|
881
|
+
})
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
if (strategy === "sequential") for (const agentId of broadcastAgents) try {
|
|
886
|
+
await dispatchForAgent(agentId);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(err)}`);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
|
|
892
|
+
for (let i = 0; i < results.length; i++) if (results[i].status === "rejected") log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${broadcastAgents[i]}: ${String(results[i].reason)}`);
|
|
893
|
+
}
|
|
894
|
+
if (isGroup && historyKey && chatHistories) clearHistoryEntriesIfEnabled({
|
|
895
|
+
historyMap: chatHistories,
|
|
896
|
+
historyKey,
|
|
897
|
+
limit: historyLimit
|
|
898
|
+
});
|
|
899
|
+
log(`feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`);
|
|
900
|
+
} else {
|
|
901
|
+
const ctxPayload = buildCtxPayloadForAgent(route.sessionKey, route.accountId, ctx.mentionedBot);
|
|
902
|
+
const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
|
|
903
|
+
cfg,
|
|
904
|
+
agentId: route.agentId,
|
|
905
|
+
runtime,
|
|
906
|
+
chatId: ctx.chatId,
|
|
907
|
+
replyToMessageId: replyTargetMessageId,
|
|
908
|
+
skipReplyToInMessages: !isGroup,
|
|
909
|
+
replyInThread,
|
|
910
|
+
rootId: ctx.rootId,
|
|
911
|
+
threadReply,
|
|
912
|
+
mentionTargets: ctx.mentionTargets,
|
|
913
|
+
accountId: account.accountId,
|
|
914
|
+
messageCreateTimeMs
|
|
915
|
+
});
|
|
916
|
+
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
|
|
917
|
+
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
|
|
918
|
+
dispatcher,
|
|
919
|
+
onSettled: () => {
|
|
920
|
+
markDispatchIdle();
|
|
921
|
+
},
|
|
922
|
+
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
923
|
+
ctx: ctxPayload,
|
|
924
|
+
cfg,
|
|
925
|
+
dispatcher,
|
|
926
|
+
replyOptions
|
|
927
|
+
})
|
|
928
|
+
});
|
|
929
|
+
if (isGroup && historyKey && chatHistories) clearHistoryEntriesIfEnabled({
|
|
930
|
+
historyMap: chatHistories,
|
|
931
|
+
historyKey,
|
|
932
|
+
limit: historyLimit
|
|
933
|
+
});
|
|
934
|
+
log(`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
|
|
935
|
+
}
|
|
936
|
+
} catch (err) {
|
|
937
|
+
error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
//#endregion
|
|
941
|
+
export { handleFeishuMessage, parseFeishuMessageEvent };
|