@m1heng-clawd/feishu 0.1.13 → 0.1.15
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/README.md +82 -2
- package/index.ts +7 -2
- package/package.json +5 -4
- package/skills/feishu-chat/SKILL.md +139 -0
- package/skills/feishu-urgent/SKILL.md +107 -0
- package/src/bot.ts +569 -59
- package/src/channel.ts +2 -0
- package/src/chat-tools/actions.ts +433 -0
- package/src/chat-tools/common.ts +13 -0
- package/src/chat-tools/index.ts +2 -0
- package/src/chat-tools/register.ts +72 -0
- package/src/chat-tools/schemas.ts +66 -0
- package/src/client.ts +13 -0
- package/src/config-schema.ts +6 -0
- package/src/drive-tools/actions.ts +57 -21
- package/src/drive-tools/schemas.ts +2 -2
- package/src/media-duration.ts +190 -0
- package/src/media.ts +60 -5
- package/src/mention.ts +1 -1
- package/src/monitor.ts +32 -8
- package/src/policy.ts +7 -3
- package/src/reply-dispatcher.ts +97 -13
- package/src/send.ts +360 -42
- package/src/streaming-card.ts +32 -4
- package/src/tools-config.ts +9 -2
- package/src/types.ts +8 -1
- package/src/typing.ts +115 -5
- package/src/urgent-tools/actions.ts +84 -0
- package/src/urgent-tools/index.ts +3 -0
- package/src/urgent-tools/register.ts +64 -0
- package/src/urgent-tools/schemas.ts +25 -0
package/src/bot.ts
CHANGED
|
@@ -53,6 +53,7 @@ function extractFirstUrl(raw: string): string | undefined {
|
|
|
53
53
|
return urlMatch?.[0];
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
|
|
56
57
|
function extractPermissionError(err: unknown): PermissionError | null {
|
|
57
58
|
if (!err || typeof err !== "object") return null;
|
|
58
59
|
|
|
@@ -99,6 +100,137 @@ type SenderNameResult = {
|
|
|
99
100
|
permissionError?: PermissionError;
|
|
100
101
|
};
|
|
101
102
|
|
|
103
|
+
type FeishuPairingApiMode = "legacy" | "scoped";
|
|
104
|
+
let detectedFeishuPairingApiMode: FeishuPairingApiMode | null = null;
|
|
105
|
+
|
|
106
|
+
async function resolveFeishuPairingApiMode(params: {
|
|
107
|
+
readAllowFromStore: (...args: any[]) => Promise<Array<string | number>>;
|
|
108
|
+
}): Promise<FeishuPairingApiMode> {
|
|
109
|
+
if (detectedFeishuPairingApiMode) return detectedFeishuPairingApiMode;
|
|
110
|
+
|
|
111
|
+
let channelObserved = false;
|
|
112
|
+
const probe = new Proxy<Record<PropertyKey, unknown>>(
|
|
113
|
+
{},
|
|
114
|
+
{
|
|
115
|
+
get(_target, prop) {
|
|
116
|
+
if (prop === "channel") {
|
|
117
|
+
channelObserved = true;
|
|
118
|
+
return "__feishu_pairing_probe__";
|
|
119
|
+
}
|
|
120
|
+
if (prop === "accountId") return "__feishu_pairing_probe__";
|
|
121
|
+
if (prop === "env") return undefined;
|
|
122
|
+
if (prop === Symbol.toPrimitive) return () => "__feishu_pairing_probe__";
|
|
123
|
+
if (prop === "toString") return () => "__feishu_pairing_probe__";
|
|
124
|
+
return undefined;
|
|
125
|
+
},
|
|
126
|
+
has(_target, prop) {
|
|
127
|
+
if (prop === "channel") {
|
|
128
|
+
channelObserved = true;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Probe once to detect whether runtime reads object-style params (scoped API)
|
|
138
|
+
// or positional params (legacy API).
|
|
139
|
+
await params.readAllowFromStore(probe as any, undefined, "__feishu_pairing_probe__");
|
|
140
|
+
} catch {
|
|
141
|
+
// Ignore probe errors; fallback below still keeps behavior safe.
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
detectedFeishuPairingApiMode = channelObserved ? "scoped" : "legacy";
|
|
145
|
+
return detectedFeishuPairingApiMode;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function readFeishuPairingAllowFrom(params: {
|
|
149
|
+
core: ReturnType<typeof getFeishuRuntime>;
|
|
150
|
+
accountId: string;
|
|
151
|
+
}): Promise<Array<string | number>> {
|
|
152
|
+
const pairing = params.core.channel.pairing as {
|
|
153
|
+
readAllowFromStore: (...args: any[]) => Promise<Array<string | number>>;
|
|
154
|
+
};
|
|
155
|
+
const pairingMode = await resolveFeishuPairingApiMode({
|
|
156
|
+
readAllowFromStore: pairing.readAllowFromStore,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (pairingMode === "scoped") {
|
|
160
|
+
try {
|
|
161
|
+
return await pairing.readAllowFromStore({
|
|
162
|
+
channel: "feishu",
|
|
163
|
+
accountId: params.accountId,
|
|
164
|
+
});
|
|
165
|
+
} catch {
|
|
166
|
+
// Compatibility fallback for older OpenClaw implementations.
|
|
167
|
+
return await pairing.readAllowFromStore("feishu", undefined, params.accountId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// OpenClaw <= 2026.2.19 signature: readAllowFromStore(channel, env?, accountId?)
|
|
173
|
+
return await pairing.readAllowFromStore("feishu", undefined, params.accountId);
|
|
174
|
+
} catch {
|
|
175
|
+
// Compatibility fallback for newer OpenClaw implementations.
|
|
176
|
+
return await pairing.readAllowFromStore({
|
|
177
|
+
channel: "feishu",
|
|
178
|
+
accountId: params.accountId,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function upsertFeishuPairingRequest(params: {
|
|
184
|
+
core: ReturnType<typeof getFeishuRuntime>;
|
|
185
|
+
accountId: string;
|
|
186
|
+
senderId: string;
|
|
187
|
+
senderName?: string;
|
|
188
|
+
}): Promise<{ code: string; created: boolean }> {
|
|
189
|
+
const pairing = params.core.channel.pairing as {
|
|
190
|
+
upsertPairingRequest: (...args: any[]) => Promise<{ code: string; created: boolean }>;
|
|
191
|
+
};
|
|
192
|
+
const meta = params.senderName ? { name: params.senderName } : undefined;
|
|
193
|
+
const pairingMode = await resolveFeishuPairingApiMode({
|
|
194
|
+
readAllowFromStore: params.core.channel.pairing.readAllowFromStore as (...args: any[]) => Promise<
|
|
195
|
+
Array<string | number>
|
|
196
|
+
>,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (pairingMode === "scoped") {
|
|
200
|
+
try {
|
|
201
|
+
return await pairing.upsertPairingRequest({
|
|
202
|
+
channel: "feishu",
|
|
203
|
+
id: params.senderId,
|
|
204
|
+
accountId: params.accountId,
|
|
205
|
+
meta,
|
|
206
|
+
});
|
|
207
|
+
} catch {
|
|
208
|
+
// Compatibility fallback for older OpenClaw implementations.
|
|
209
|
+
return await pairing.upsertPairingRequest({
|
|
210
|
+
channel: "feishu",
|
|
211
|
+
id: params.senderId,
|
|
212
|
+
meta,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
return await pairing.upsertPairingRequest({
|
|
219
|
+
channel: "feishu",
|
|
220
|
+
id: params.senderId,
|
|
221
|
+
meta,
|
|
222
|
+
});
|
|
223
|
+
} catch {
|
|
224
|
+
// Compatibility fallback for newer OpenClaw implementations.
|
|
225
|
+
return await pairing.upsertPairingRequest({
|
|
226
|
+
channel: "feishu",
|
|
227
|
+
id: params.senderId,
|
|
228
|
+
accountId: params.accountId,
|
|
229
|
+
meta,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
102
234
|
async function resolveFeishuSenderName(params: {
|
|
103
235
|
account: ResolvedFeishuAccount;
|
|
104
236
|
senderOpenId: string;
|
|
@@ -123,9 +255,7 @@ async function resolveFeishuSenderName(params: {
|
|
|
123
255
|
|
|
124
256
|
const name: string | undefined =
|
|
125
257
|
res?.data?.user?.name ||
|
|
126
|
-
res?.data?.user?.
|
|
127
|
-
res?.data?.user?.nickname ||
|
|
128
|
-
res?.data?.user?.en_name;
|
|
258
|
+
res?.data?.user?.nickname;
|
|
129
259
|
|
|
130
260
|
if (name && typeof name === "string") {
|
|
131
261
|
senderNameCache.set(senderOpenId, { name, expireAt: now + SENDER_NAME_TTL_MS });
|
|
@@ -150,6 +280,67 @@ async function resolveFeishuSenderName(params: {
|
|
|
150
280
|
// Cache group bot counts for command mention bypass policy checks.
|
|
151
281
|
const GROUP_BOT_COUNT_TTL_MS = 10 * 60 * 1000;
|
|
152
282
|
const groupBotCountCache = new Map<string, { count: number; expireAt: number }>();
|
|
283
|
+
// Fallback flush only for standalone forwarded messages.
|
|
284
|
+
// Quote-reply merging should rely on parent_id relation instead of time windows.
|
|
285
|
+
const FORWARDED_COALESCE_WINDOW_MS = 1500;
|
|
286
|
+
const FORWARDED_COMPANION_TTL_MS = 20 * 1000;
|
|
287
|
+
type PendingForwardedDispatchEntry = {
|
|
288
|
+
token: string;
|
|
289
|
+
timer: ReturnType<typeof setTimeout>;
|
|
290
|
+
content: string;
|
|
291
|
+
messageKeys: Set<string>;
|
|
292
|
+
};
|
|
293
|
+
const pendingForwardedDispatch = new Map<string, PendingForwardedDispatchEntry>();
|
|
294
|
+
const pendingForwardedDispatchByMessageId = new Map<string, string>();
|
|
295
|
+
const recentForwardedCompanionReplies = new Map<string, number>();
|
|
296
|
+
|
|
297
|
+
function getForwardedKey(params: {
|
|
298
|
+
accountId: string;
|
|
299
|
+
chatId: string;
|
|
300
|
+
forwardedMessageId: string;
|
|
301
|
+
}): string {
|
|
302
|
+
return `${params.accountId}:${params.chatId}:${params.forwardedMessageId}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function clearPendingForwardedDispatchEntry(dispatchKey: string, entry: PendingForwardedDispatchEntry): void {
|
|
306
|
+
clearTimeout(entry.timer);
|
|
307
|
+
pendingForwardedDispatch.delete(dispatchKey);
|
|
308
|
+
for (const key of entry.messageKeys) {
|
|
309
|
+
pendingForwardedDispatchByMessageId.delete(key);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function markForwardedCompanionReply(params: {
|
|
314
|
+
accountId: string;
|
|
315
|
+
chatId: string;
|
|
316
|
+
forwardedMessageId: string;
|
|
317
|
+
}): void {
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
for (const [key, expireAt] of recentForwardedCompanionReplies) {
|
|
320
|
+
if (expireAt <= now) recentForwardedCompanionReplies.delete(key);
|
|
321
|
+
}
|
|
322
|
+
recentForwardedCompanionReplies.set(
|
|
323
|
+
getForwardedKey(params),
|
|
324
|
+
now + FORWARDED_COMPANION_TTL_MS,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function consumeForwardedCompanionReply(params: {
|
|
329
|
+
accountId: string;
|
|
330
|
+
chatId: string;
|
|
331
|
+
forwardedMessageId: string;
|
|
332
|
+
}): boolean {
|
|
333
|
+
const key = getForwardedKey(params);
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
const expireAt = recentForwardedCompanionReplies.get(key);
|
|
336
|
+
if (!expireAt) return false;
|
|
337
|
+
if (expireAt <= now) {
|
|
338
|
+
recentForwardedCompanionReplies.delete(key);
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
recentForwardedCompanionReplies.delete(key);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
153
344
|
|
|
154
345
|
async function resolveFeishuGroupBotCount(params: {
|
|
155
346
|
account: ResolvedFeishuAccount;
|
|
@@ -196,7 +387,8 @@ export type FeishuMessageEvent = {
|
|
|
196
387
|
root_id?: string;
|
|
197
388
|
parent_id?: string;
|
|
198
389
|
chat_id: string;
|
|
199
|
-
chat_type: "p2p" | "group";
|
|
390
|
+
chat_type: "p2p" | "group" | "private";
|
|
391
|
+
create_time?: string;
|
|
200
392
|
message_type: string;
|
|
201
393
|
content: string;
|
|
202
394
|
mentions?: Array<{
|
|
@@ -223,21 +415,89 @@ export type FeishuBotAddedEvent = {
|
|
|
223
415
|
operator_tenant_key?: string;
|
|
224
416
|
};
|
|
225
417
|
|
|
226
|
-
function
|
|
418
|
+
function isForwardedMessageType(messageType: string): boolean {
|
|
419
|
+
return messageType === "forwarded" || messageType === "merged_forwarded" || messageType === "merge_forward";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
type ParsedFeishuMessageContent = {
|
|
423
|
+
text: string;
|
|
424
|
+
placeholder?: string;
|
|
425
|
+
rawPayload?: string;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
function parseMessageContent(content: string, messageType: string): ParsedFeishuMessageContent {
|
|
227
429
|
try {
|
|
228
430
|
const parsed = JSON.parse(content);
|
|
229
|
-
|
|
230
|
-
|
|
431
|
+
const normalizedMessageType = normalizeFeishuInboundMessageType(messageType);
|
|
432
|
+
if (normalizedMessageType === "text") {
|
|
433
|
+
return { text: parsed.text || "", rawPayload: content };
|
|
231
434
|
}
|
|
232
|
-
if (
|
|
435
|
+
if (normalizedMessageType === "post") {
|
|
233
436
|
// Extract text content from rich text post
|
|
234
437
|
const { textContent } = parsePostContent(content);
|
|
235
|
-
return textContent;
|
|
438
|
+
return { text: textContent, rawPayload: content };
|
|
439
|
+
}
|
|
440
|
+
if (["image", "file", "audio", "video", "sticker"].includes(normalizedMessageType)) {
|
|
441
|
+
const ph = inferPlaceholder(normalizedMessageType);
|
|
442
|
+
return { text: ph, placeholder: ph, rawPayload: content };
|
|
443
|
+
}
|
|
444
|
+
// Handle forwarded messages (single message forwarded)
|
|
445
|
+
if (messageType === "forwarded") {
|
|
446
|
+
const senderName = parsed.sender_name || parsed.sender?.name || "unknown";
|
|
447
|
+
const text = parsed.text || parsed.title || "";
|
|
448
|
+
const forwarded = text ? `[Forwarded from ${senderName}]: ${text.substring(0, 100)}` : `[Forwarded from ${senderName}]`;
|
|
449
|
+
return { text: forwarded, rawPayload: content };
|
|
236
450
|
}
|
|
237
|
-
|
|
451
|
+
// Handle merged_forwarded messages (multiple messages merged)
|
|
452
|
+
// Note: Feishu uses "merge_forward" for merged forwarded messages
|
|
453
|
+
if (messageType === "merged_forwarded" || messageType === "merge_forward") {
|
|
454
|
+
const messageCount = parsed.message_count || parsed.messages?.length || 0;
|
|
455
|
+
const senderNames = parsed.messages
|
|
456
|
+
?.map((m: any) => m.sender_name || m.sender?.name)
|
|
457
|
+
.filter(Boolean)
|
|
458
|
+
.join(", ") || "unknown";
|
|
459
|
+
// Extract preview of first few messages
|
|
460
|
+
let preview = "";
|
|
461
|
+
if (parsed.messages && parsed.messages.length > 0) {
|
|
462
|
+
const previews = parsed.messages
|
|
463
|
+
.slice(0, 3)
|
|
464
|
+
.map((m: any) => m.text || m.title || "")
|
|
465
|
+
.filter(Boolean)
|
|
466
|
+
.map((t: string) => t.substring(0, 50));
|
|
467
|
+
if (previews.length > 0) {
|
|
468
|
+
preview = `\n└ ${previews.join("\n└ ")}`;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return { text: `[Merged forward (${messageCount} messages), from: ${senderNames}]${preview}`, rawPayload: content };
|
|
472
|
+
}
|
|
473
|
+
return { text: content, rawPayload: content };
|
|
238
474
|
} catch {
|
|
239
|
-
return content;
|
|
475
|
+
return { text: content, rawPayload: content };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Feishu may use "media" for inbound video messages.
|
|
481
|
+
* Normalize to "video" so downstream media handling is consistent.
|
|
482
|
+
*/
|
|
483
|
+
export function normalizeFeishuInboundMessageType(messageType: string): string {
|
|
484
|
+
return messageType === "media" ? "video" : messageType;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Select the correct resource key for messageResource download.
|
|
489
|
+
* - image message: prefer image_key
|
|
490
|
+
* - other media (video/audio/file/sticker): prefer file_key
|
|
491
|
+
*/
|
|
492
|
+
export function selectFeishuMessageResourceKey(
|
|
493
|
+
messageType: string,
|
|
494
|
+
keys: { imageKey?: string; fileKey?: string },
|
|
495
|
+
): string | undefined {
|
|
496
|
+
const normalized = normalizeFeishuInboundMessageType(messageType);
|
|
497
|
+
if (normalized === "image") {
|
|
498
|
+
return keys.imageKey ?? keys.fileKey;
|
|
240
499
|
}
|
|
500
|
+
return keys.fileKey ?? keys.imageKey;
|
|
241
501
|
}
|
|
242
502
|
|
|
243
503
|
function checkBotMentioned(
|
|
@@ -288,6 +548,7 @@ function parseMediaKeys(
|
|
|
288
548
|
case "audio":
|
|
289
549
|
return { fileKey: parsed.file_key };
|
|
290
550
|
case "video":
|
|
551
|
+
case "media":
|
|
291
552
|
// Video has both file_key (video) and image_key (thumbnail)
|
|
292
553
|
return { fileKey: parsed.file_key, imageKey: parsed.image_key };
|
|
293
554
|
case "sticker":
|
|
@@ -342,12 +603,33 @@ function parsePostContent(content: string): {
|
|
|
342
603
|
}
|
|
343
604
|
|
|
344
605
|
return {
|
|
345
|
-
textContent: textContent.trim() || "[
|
|
606
|
+
textContent: textContent.trim() || "[Rich text message]",
|
|
346
607
|
imageKeys,
|
|
347
608
|
mentionIds,
|
|
348
609
|
};
|
|
349
610
|
} catch {
|
|
350
|
-
return { textContent: "[
|
|
611
|
+
return { textContent: "[Rich text message]", imageKeys: [], mentionIds: [] };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function isGenericMimeType(mime?: string): boolean {
|
|
616
|
+
if (!mime) return true;
|
|
617
|
+
const normalized = mime.split(";")[0]?.trim().toLowerCase();
|
|
618
|
+
return !normalized || normalized === "application/octet-stream";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function resolveFallbackMimeType(messageType: string): string | undefined {
|
|
622
|
+
switch (messageType) {
|
|
623
|
+
case "audio":
|
|
624
|
+
// Feishu voice messages are typically opus in ogg container.
|
|
625
|
+
return "audio/ogg; codecs=opus";
|
|
626
|
+
case "video":
|
|
627
|
+
return "video/mp4";
|
|
628
|
+
case "image":
|
|
629
|
+
case "post":
|
|
630
|
+
return "image/jpeg";
|
|
631
|
+
default:
|
|
632
|
+
return undefined;
|
|
351
633
|
}
|
|
352
634
|
}
|
|
353
635
|
|
|
@@ -363,6 +645,7 @@ function inferPlaceholder(messageType: string): string {
|
|
|
363
645
|
case "audio":
|
|
364
646
|
return "<media:audio>";
|
|
365
647
|
case "video":
|
|
648
|
+
case "media":
|
|
366
649
|
return "<media:video>";
|
|
367
650
|
case "sticker":
|
|
368
651
|
return "<media:sticker>";
|
|
@@ -385,10 +668,11 @@ async function resolveFeishuMediaList(params: {
|
|
|
385
668
|
accountId?: string;
|
|
386
669
|
}): Promise<FeishuMediaInfo[]> {
|
|
387
670
|
const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
|
|
671
|
+
const normalizedMessageType = normalizeFeishuInboundMessageType(messageType);
|
|
388
672
|
|
|
389
673
|
// Only process media message types (including post for embedded images)
|
|
390
674
|
const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
|
|
391
|
-
if (!mediaTypes.includes(
|
|
675
|
+
if (!mediaTypes.includes(normalizedMessageType)) {
|
|
392
676
|
return [];
|
|
393
677
|
}
|
|
394
678
|
|
|
@@ -396,7 +680,7 @@ async function resolveFeishuMediaList(params: {
|
|
|
396
680
|
const core = getFeishuRuntime();
|
|
397
681
|
|
|
398
682
|
// Handle post (rich text) messages with embedded images
|
|
399
|
-
if (
|
|
683
|
+
if (normalizedMessageType === "post") {
|
|
400
684
|
const { imageKeys } = parsePostContent(content);
|
|
401
685
|
if (imageKeys.length === 0) {
|
|
402
686
|
return [];
|
|
@@ -416,8 +700,12 @@ async function resolveFeishuMediaList(params: {
|
|
|
416
700
|
});
|
|
417
701
|
|
|
418
702
|
let contentType = result.contentType;
|
|
419
|
-
if (!contentType) {
|
|
420
|
-
contentType =
|
|
703
|
+
if (!contentType || isGenericMimeType(contentType)) {
|
|
704
|
+
contentType =
|
|
705
|
+
(await core.media.detectMime({
|
|
706
|
+
buffer: result.buffer,
|
|
707
|
+
headerMime: contentType,
|
|
708
|
+
})) ?? resolveFallbackMimeType("image");
|
|
421
709
|
}
|
|
422
710
|
|
|
423
711
|
const saved = await core.channel.media.saveMediaBuffer(
|
|
@@ -443,7 +731,7 @@ async function resolveFeishuMediaList(params: {
|
|
|
443
731
|
}
|
|
444
732
|
|
|
445
733
|
// Handle other media types
|
|
446
|
-
const mediaKeys = parseMediaKeys(content,
|
|
734
|
+
const mediaKeys = parseMediaKeys(content, normalizedMessageType);
|
|
447
735
|
if (!mediaKeys.imageKey && !mediaKeys.fileKey) {
|
|
448
736
|
return [];
|
|
449
737
|
}
|
|
@@ -455,12 +743,12 @@ async function resolveFeishuMediaList(params: {
|
|
|
455
743
|
|
|
456
744
|
// For message media, always use messageResource API
|
|
457
745
|
// The image.get API is only for images uploaded via im/v1/images, not for message attachments
|
|
458
|
-
const fileKey =
|
|
746
|
+
const fileKey = selectFeishuMessageResourceKey(normalizedMessageType, mediaKeys);
|
|
459
747
|
if (!fileKey) {
|
|
460
748
|
return [];
|
|
461
749
|
}
|
|
462
750
|
|
|
463
|
-
const resourceType =
|
|
751
|
+
const resourceType = normalizedMessageType === "image" ? "image" : "file";
|
|
464
752
|
const result = await downloadMessageResourceFeishu({
|
|
465
753
|
cfg,
|
|
466
754
|
messageId,
|
|
@@ -472,9 +760,17 @@ async function resolveFeishuMediaList(params: {
|
|
|
472
760
|
contentType = result.contentType;
|
|
473
761
|
fileName = result.fileName || mediaKeys.fileName;
|
|
474
762
|
|
|
475
|
-
// Detect
|
|
476
|
-
if (!contentType) {
|
|
477
|
-
contentType =
|
|
763
|
+
// Detect MIME type and apply message-type fallback when needed.
|
|
764
|
+
if (!contentType || isGenericMimeType(contentType)) {
|
|
765
|
+
contentType =
|
|
766
|
+
(await core.media.detectMime({
|
|
767
|
+
buffer,
|
|
768
|
+
headerMime: contentType,
|
|
769
|
+
filePath: fileName,
|
|
770
|
+
})) ?? resolveFallbackMimeType(normalizedMessageType);
|
|
771
|
+
}
|
|
772
|
+
if (isGenericMimeType(contentType)) {
|
|
773
|
+
contentType = resolveFallbackMimeType(normalizedMessageType) ?? contentType;
|
|
478
774
|
}
|
|
479
775
|
|
|
480
776
|
// Save to disk using core's saveMediaBuffer
|
|
@@ -489,12 +785,12 @@ async function resolveFeishuMediaList(params: {
|
|
|
489
785
|
out.push({
|
|
490
786
|
path: saved.path,
|
|
491
787
|
contentType: saved.contentType,
|
|
492
|
-
placeholder: inferPlaceholder(
|
|
788
|
+
placeholder: inferPlaceholder(normalizedMessageType),
|
|
493
789
|
});
|
|
494
790
|
|
|
495
|
-
log?.(`feishu: downloaded ${
|
|
791
|
+
log?.(`feishu: downloaded ${normalizedMessageType} media, saved to ${saved.path}`);
|
|
496
792
|
} catch (err) {
|
|
497
|
-
log?.(`feishu: failed to download ${
|
|
793
|
+
log?.(`feishu: failed to download ${normalizedMessageType} media: ${String(err)}`);
|
|
498
794
|
}
|
|
499
795
|
|
|
500
796
|
return out;
|
|
@@ -527,17 +823,44 @@ function buildFeishuMediaPayload(
|
|
|
527
823
|
};
|
|
528
824
|
}
|
|
529
825
|
|
|
826
|
+
function skipGroupMessageForMentionGate(params: {
|
|
827
|
+
accountId: string;
|
|
828
|
+
chatId: string;
|
|
829
|
+
senderOpenId: string;
|
|
830
|
+
senderName?: string;
|
|
831
|
+
content: string;
|
|
832
|
+
messageId: string;
|
|
833
|
+
historyLimit: number;
|
|
834
|
+
chatHistories?: Map<string, HistoryEntry[]>;
|
|
835
|
+
log: (...args: any[]) => void;
|
|
836
|
+
}): void {
|
|
837
|
+
params.log(`feishu[${params.accountId}]: message in group ${params.chatId} skipped (mention required)`);
|
|
838
|
+
if (params.chatHistories) {
|
|
839
|
+
recordPendingHistoryEntryIfEnabled({
|
|
840
|
+
historyMap: params.chatHistories,
|
|
841
|
+
historyKey: params.chatId,
|
|
842
|
+
limit: params.historyLimit,
|
|
843
|
+
entry: {
|
|
844
|
+
sender: params.senderOpenId,
|
|
845
|
+
body: `${params.senderName ?? params.senderOpenId}: ${params.content}`,
|
|
846
|
+
timestamp: Date.now(),
|
|
847
|
+
messageId: params.messageId,
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
530
853
|
export function parseFeishuMessageEvent(
|
|
531
854
|
event: FeishuMessageEvent,
|
|
532
855
|
botOpenId?: string,
|
|
533
856
|
): FeishuMessageContext {
|
|
534
857
|
const parsedPost =
|
|
535
858
|
event.message.message_type === "post" ? parsePostContent(event.message.content) : undefined;
|
|
536
|
-
const
|
|
859
|
+
const parsedContent = parseMessageContent(event.message.content, event.message.message_type);
|
|
537
860
|
const mentionedBot = checkBotMentioned(event, botOpenId, parsedPost?.mentionIds ?? []);
|
|
538
861
|
const hasAnyMention =
|
|
539
862
|
(event.message.mentions?.length ?? 0) > 0 || (parsedPost?.mentionIds.length ?? 0) > 0;
|
|
540
|
-
const content = stripBotMention(
|
|
863
|
+
const content = stripBotMention(parsedContent.text, event.message.mentions);
|
|
541
864
|
|
|
542
865
|
const ctx: FeishuMessageContext = {
|
|
543
866
|
chatId: event.message.chat_id,
|
|
@@ -550,6 +873,8 @@ export function parseFeishuMessageEvent(
|
|
|
550
873
|
parentId: event.message.parent_id || undefined,
|
|
551
874
|
content,
|
|
552
875
|
contentType: event.message.message_type,
|
|
876
|
+
placeholder: parsedContent.placeholder,
|
|
877
|
+
rawPayload: parsedContent.rawPayload,
|
|
553
878
|
hasAnyMention,
|
|
554
879
|
};
|
|
555
880
|
|
|
@@ -574,8 +899,9 @@ export async function handleFeishuMessage(params: {
|
|
|
574
899
|
runtime?: RuntimeEnv;
|
|
575
900
|
chatHistories?: Map<string, HistoryEntry[]>;
|
|
576
901
|
accountId?: string;
|
|
902
|
+
replyToMessageIdOverride?: string;
|
|
577
903
|
}): Promise<void> {
|
|
578
|
-
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
|
|
904
|
+
const { cfg, event, botOpenId, runtime, chatHistories, accountId, replyToMessageIdOverride } = params;
|
|
579
905
|
|
|
580
906
|
// Resolve account with merged config
|
|
581
907
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
@@ -593,6 +919,144 @@ export async function handleFeishuMessage(params: {
|
|
|
593
919
|
}
|
|
594
920
|
|
|
595
921
|
let ctx = parseFeishuMessageEvent(event, botOpenId);
|
|
922
|
+
|
|
923
|
+
// Parse message create_time (Feishu uses millisecond epoch string).
|
|
924
|
+
const messageCreateTimeMs = event.message.create_time
|
|
925
|
+
? parseInt(event.message.create_time, 10)
|
|
926
|
+
: undefined;
|
|
927
|
+
|
|
928
|
+
const messageType = event.message.message_type;
|
|
929
|
+
const isForwardedInbound = isForwardedMessageType(messageType);
|
|
930
|
+
const forwardedDispatchKey = getForwardedKey({
|
|
931
|
+
accountId: account.accountId,
|
|
932
|
+
chatId: ctx.chatId,
|
|
933
|
+
forwardedMessageId: ctx.messageId,
|
|
934
|
+
});
|
|
935
|
+
if (!isForwardedInbound && ctx.parentId) {
|
|
936
|
+
markForwardedCompanionReply({
|
|
937
|
+
accountId: account.accountId,
|
|
938
|
+
chatId: ctx.chatId,
|
|
939
|
+
forwardedMessageId: ctx.parentId,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// For forwarded/merged_forwarded messages, fetch full message details via API
|
|
944
|
+
// Note: Feishu uses "merge_forward" for merged forwarded messages
|
|
945
|
+
if (isForwardedInbound) {
|
|
946
|
+
if (
|
|
947
|
+
consumeForwardedCompanionReply({
|
|
948
|
+
accountId: account.accountId,
|
|
949
|
+
chatId: ctx.chatId,
|
|
950
|
+
forwardedMessageId: ctx.messageId,
|
|
951
|
+
})
|
|
952
|
+
) {
|
|
953
|
+
log(
|
|
954
|
+
`feishu[${account.accountId}]: skipping companion forwarded message ${ctx.messageId} (covered by parent reply)`,
|
|
955
|
+
);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
const fullMessage = await getMessageFeishu({
|
|
960
|
+
cfg,
|
|
961
|
+
messageId,
|
|
962
|
+
accountId: account.accountId,
|
|
963
|
+
});
|
|
964
|
+
if (fullMessage?.content) {
|
|
965
|
+
// Use the already-parsed content from getMessageFeishu (includes all child messages for merge_forward)
|
|
966
|
+
ctx = { ...ctx, content: fullMessage.content };
|
|
967
|
+
}
|
|
968
|
+
} catch (err) {
|
|
969
|
+
log(`feishu: failed to fetch forwarded message details: ${err}`);
|
|
970
|
+
}
|
|
971
|
+
// Handle race: parent-reply may arrive and be processed while we're awaiting getMessageFeishu above.
|
|
972
|
+
// In that case, do not enqueue deferred forwarded flush again.
|
|
973
|
+
if (
|
|
974
|
+
consumeForwardedCompanionReply({
|
|
975
|
+
accountId: account.accountId,
|
|
976
|
+
chatId: ctx.chatId,
|
|
977
|
+
forwardedMessageId: ctx.messageId,
|
|
978
|
+
})
|
|
979
|
+
) {
|
|
980
|
+
log(
|
|
981
|
+
`feishu[${account.accountId}]: skipping companion forwarded message ${ctx.messageId} (covered during fetch)`,
|
|
982
|
+
);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const existing = pendingForwardedDispatch.get(forwardedDispatchKey);
|
|
986
|
+
if (existing) clearPendingForwardedDispatchEntry(forwardedDispatchKey, existing);
|
|
987
|
+
const mergedMessageKeys = new Set<string>([forwardedDispatchKey]);
|
|
988
|
+
const token = `${ctx.messageId}:${Date.now()}`;
|
|
989
|
+
const deferredMessageIdBase = ctx.messageId;
|
|
990
|
+
const timer = setTimeout(() => {
|
|
991
|
+
const current = pendingForwardedDispatch.get(forwardedDispatchKey);
|
|
992
|
+
if (!current || current.token !== token) return;
|
|
993
|
+
clearPendingForwardedDispatchEntry(forwardedDispatchKey, current);
|
|
994
|
+
|
|
995
|
+
const syntheticEvent: FeishuMessageEvent = {
|
|
996
|
+
...event,
|
|
997
|
+
message: {
|
|
998
|
+
...event.message,
|
|
999
|
+
message_id: `${deferredMessageIdBase}:forwarded-flush:${Date.now()}`,
|
|
1000
|
+
message_type: "text",
|
|
1001
|
+
content: JSON.stringify({ text: current.content }),
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
void handleFeishuMessage({
|
|
1006
|
+
cfg,
|
|
1007
|
+
event: syntheticEvent,
|
|
1008
|
+
botOpenId,
|
|
1009
|
+
runtime,
|
|
1010
|
+
chatHistories,
|
|
1011
|
+
accountId: account.accountId,
|
|
1012
|
+
replyToMessageIdOverride: deferredMessageIdBase,
|
|
1013
|
+
}).catch((err) => {
|
|
1014
|
+
error(`feishu[${account.accountId}]: failed deferred forwarded dispatch: ${String(err)}`);
|
|
1015
|
+
});
|
|
1016
|
+
}, FORWARDED_COALESCE_WINDOW_MS);
|
|
1017
|
+
|
|
1018
|
+
pendingForwardedDispatch.set(forwardedDispatchKey, {
|
|
1019
|
+
token,
|
|
1020
|
+
timer,
|
|
1021
|
+
content: ctx.content,
|
|
1022
|
+
messageKeys: mergedMessageKeys,
|
|
1023
|
+
});
|
|
1024
|
+
for (const messageKey of mergedMessageKeys) {
|
|
1025
|
+
pendingForwardedDispatchByMessageId.set(messageKey, forwardedDispatchKey);
|
|
1026
|
+
}
|
|
1027
|
+
log(
|
|
1028
|
+
`feishu[${account.accountId}]: queued forwarded message ${ctx.messageId} for deferred dispatch`,
|
|
1029
|
+
);
|
|
1030
|
+
return;
|
|
1031
|
+
} else {
|
|
1032
|
+
let pending: PendingForwardedDispatchEntry | undefined;
|
|
1033
|
+
let pendingDispatchKey: string | undefined;
|
|
1034
|
+
if (ctx.parentId) {
|
|
1035
|
+
const parentMessageKey = getForwardedKey({
|
|
1036
|
+
accountId: account.accountId,
|
|
1037
|
+
chatId: ctx.chatId,
|
|
1038
|
+
forwardedMessageId: ctx.parentId,
|
|
1039
|
+
});
|
|
1040
|
+
const parentDispatchKey = pendingForwardedDispatchByMessageId.get(parentMessageKey);
|
|
1041
|
+
if (parentDispatchKey) {
|
|
1042
|
+
pendingDispatchKey = parentDispatchKey;
|
|
1043
|
+
pending = pendingForwardedDispatch.get(parentDispatchKey);
|
|
1044
|
+
} else {
|
|
1045
|
+
pendingForwardedDispatchByMessageId.delete(parentMessageKey);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (pending && pendingDispatchKey) {
|
|
1049
|
+
clearPendingForwardedDispatchEntry(pendingDispatchKey, pending);
|
|
1050
|
+
ctx = {
|
|
1051
|
+
...ctx,
|
|
1052
|
+
content: `${pending.content}\n\n---\n\n${ctx.content}`,
|
|
1053
|
+
};
|
|
1054
|
+
log(
|
|
1055
|
+
`feishu[${account.accountId}]: merged pending forwarded context into follow-up ${ctx.messageId}`,
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
596
1060
|
const isGroup = ctx.chatType === "group";
|
|
597
1061
|
|
|
598
1062
|
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
|
|
@@ -648,7 +1112,10 @@ export async function handleFeishuMessage(params: {
|
|
|
648
1112
|
const hasControlCommand = core.channel.text.hasControlCommand(ctx.content, cfg);
|
|
649
1113
|
const storeAllowFrom =
|
|
650
1114
|
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized)
|
|
651
|
-
? await
|
|
1115
|
+
? await readFeishuPairingAllowFrom({
|
|
1116
|
+
core,
|
|
1117
|
+
accountId: account.accountId,
|
|
1118
|
+
}).catch(() => [])
|
|
652
1119
|
: [];
|
|
653
1120
|
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
654
1121
|
const dmAllowed = resolveFeishuAllowlistMatch({
|
|
@@ -659,17 +1126,18 @@ export async function handleFeishuMessage(params: {
|
|
|
659
1126
|
|
|
660
1127
|
if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
|
|
661
1128
|
if (dmPolicy === "pairing") {
|
|
662
|
-
const { code, created } = await
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1129
|
+
const { code, created } = await upsertFeishuPairingRequest({
|
|
1130
|
+
core,
|
|
1131
|
+
accountId: account.accountId,
|
|
1132
|
+
senderId: ctx.senderOpenId,
|
|
1133
|
+
senderName: ctx.senderName,
|
|
666
1134
|
});
|
|
667
1135
|
if (created) {
|
|
668
1136
|
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
|
669
1137
|
try {
|
|
670
1138
|
await sendMessageFeishu({
|
|
671
1139
|
cfg,
|
|
672
|
-
to: `
|
|
1140
|
+
to: `chat:${ctx.chatId}`,
|
|
673
1141
|
text: core.channel.pairing.buildPairingReply({
|
|
674
1142
|
channel: "feishu",
|
|
675
1143
|
idLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
|
@@ -748,7 +1216,7 @@ export async function handleFeishuMessage(params: {
|
|
|
748
1216
|
}
|
|
749
1217
|
}
|
|
750
1218
|
|
|
751
|
-
const { requireMention } = resolveFeishuReplyPolicy({
|
|
1219
|
+
const { requireMention, allowMentionlessInMultiBotGroup } = resolveFeishuReplyPolicy({
|
|
752
1220
|
isDirectMessage: false,
|
|
753
1221
|
globalConfig: feishuCfg,
|
|
754
1222
|
groupConfig,
|
|
@@ -788,22 +1256,49 @@ export async function handleFeishuMessage(params: {
|
|
|
788
1256
|
effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
|
789
1257
|
|
|
790
1258
|
if (mentionGate.shouldSkip) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1259
|
+
skipGroupMessageForMentionGate({
|
|
1260
|
+
accountId: account.accountId,
|
|
1261
|
+
chatId: ctx.chatId,
|
|
1262
|
+
senderOpenId: ctx.senderOpenId,
|
|
1263
|
+
senderName: ctx.senderName,
|
|
1264
|
+
content: ctx.content,
|
|
1265
|
+
messageId: ctx.messageId,
|
|
1266
|
+
historyLimit,
|
|
1267
|
+
chatHistories,
|
|
1268
|
+
log,
|
|
1269
|
+
});
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
} else if (!ctx.mentionedBot && !allowMentionlessInMultiBotGroup) {
|
|
1273
|
+
// Safety default for mention-free mode: only allow non-@ traffic when the group has <= 1 bot.
|
|
1274
|
+
// In multi-bot groups, explicit @ is required unless allowMentionlessInMultiBotGroup=true.
|
|
1275
|
+
const botCount = await resolveFeishuGroupBotCount({
|
|
1276
|
+
account,
|
|
1277
|
+
chatId: ctx.chatId,
|
|
1278
|
+
log,
|
|
1279
|
+
});
|
|
1280
|
+
const singleBotGroup = botCount !== undefined && botCount <= 1;
|
|
1281
|
+
if (!singleBotGroup) {
|
|
1282
|
+
if (botCount === undefined) {
|
|
1283
|
+
log(
|
|
1284
|
+
`feishu[${account.accountId}]: unable to resolve bot_count for ${ctx.chatId}, mention-free mode disabled for safety`,
|
|
1285
|
+
);
|
|
1286
|
+
} else {
|
|
1287
|
+
log(
|
|
1288
|
+
`feishu[${account.accountId}]: group ${ctx.chatId} has ${botCount} bots, explicit @ required unless allowMentionlessInMultiBotGroup=true`,
|
|
1289
|
+
);
|
|
806
1290
|
}
|
|
1291
|
+
skipGroupMessageForMentionGate({
|
|
1292
|
+
accountId: account.accountId,
|
|
1293
|
+
chatId: ctx.chatId,
|
|
1294
|
+
senderOpenId: ctx.senderOpenId,
|
|
1295
|
+
senderName: ctx.senderName,
|
|
1296
|
+
content: ctx.content,
|
|
1297
|
+
messageId: ctx.messageId,
|
|
1298
|
+
historyLimit,
|
|
1299
|
+
chatHistories,
|
|
1300
|
+
log,
|
|
1301
|
+
});
|
|
807
1302
|
return;
|
|
808
1303
|
}
|
|
809
1304
|
}
|
|
@@ -900,7 +1395,7 @@ export async function handleFeishuMessage(params: {
|
|
|
900
1395
|
const quotedMsg = await getMessageFeishu({ cfg, messageId: ctx.parentId, accountId: account.accountId });
|
|
901
1396
|
if (quotedMsg) {
|
|
902
1397
|
quotedContent = quotedMsg.content;
|
|
903
|
-
log(`feishu[${account.accountId}]: fetched quoted message
|
|
1398
|
+
log(`feishu[${account.accountId}]: fetched quoted message ${ctx.parentId}`);
|
|
904
1399
|
}
|
|
905
1400
|
} catch (err) {
|
|
906
1401
|
log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
|
|
@@ -909,12 +1404,23 @@ export async function handleFeishuMessage(params: {
|
|
|
909
1404
|
|
|
910
1405
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
911
1406
|
|
|
912
|
-
// Build message
|
|
913
|
-
|
|
1407
|
+
// Build core message content with quoted content if available.
|
|
1408
|
+
// Keep this separate from speaker/system prefixes so RawBody preserves quote context.
|
|
1409
|
+
let rawBody = ctx.content;
|
|
914
1410
|
if (quotedContent) {
|
|
915
|
-
|
|
1411
|
+
const normalizedQuoted = quotedContent.replace(/\s+/g, " ").trim();
|
|
1412
|
+
const normalizedBody = ctx.content.replace(/\s+/g, " ").trim();
|
|
1413
|
+
const quoteAlreadyInBody =
|
|
1414
|
+
normalizedQuoted.length > 16 && normalizedBody.length > 0 && normalizedBody.includes(normalizedQuoted);
|
|
1415
|
+
if (quoteAlreadyInBody) {
|
|
1416
|
+
log(`feishu[${account.accountId}]: skip duplicate quote merge for message ${ctx.messageId}`);
|
|
1417
|
+
} else {
|
|
1418
|
+
rawBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
|
|
1419
|
+
}
|
|
916
1420
|
}
|
|
917
1421
|
|
|
1422
|
+
let messageBody = rawBody;
|
|
1423
|
+
|
|
918
1424
|
// Include a readable speaker label so the model can attribute instructions.
|
|
919
1425
|
// (DMs already have per-sender sessions, but the prefix is still useful for clarity.)
|
|
920
1426
|
const speaker = ctx.senderName ?? ctx.senderOpenId;
|
|
@@ -927,6 +1433,7 @@ export async function handleFeishuMessage(params: {
|
|
|
927
1433
|
}
|
|
928
1434
|
|
|
929
1435
|
const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
|
|
1436
|
+
const replyToMessageId = replyToMessageIdOverride ?? ctx.rootId ?? ctx.messageId;
|
|
930
1437
|
|
|
931
1438
|
// If there's a permission error, dispatch a separate notification first
|
|
932
1439
|
if (permissionErrorForAgent) {
|
|
@@ -969,8 +1476,9 @@ export async function handleFeishuMessage(params: {
|
|
|
969
1476
|
agentId: route.agentId,
|
|
970
1477
|
runtime: runtime as RuntimeEnv,
|
|
971
1478
|
chatId: ctx.chatId,
|
|
972
|
-
replyToMessageId
|
|
1479
|
+
replyToMessageId,
|
|
973
1480
|
accountId: account.accountId,
|
|
1481
|
+
messageCreateTimeMs,
|
|
974
1482
|
});
|
|
975
1483
|
|
|
976
1484
|
log(`feishu[${account.accountId}]: dispatching permission error notification to agent`);
|
|
@@ -1025,8 +1533,8 @@ export async function handleFeishuMessage(params: {
|
|
|
1025
1533
|
|
|
1026
1534
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
1027
1535
|
Body: combinedBody,
|
|
1028
|
-
RawBody:
|
|
1029
|
-
CommandBody:
|
|
1536
|
+
RawBody: rawBody,
|
|
1537
|
+
CommandBody: rawBody,
|
|
1030
1538
|
From: feishuFrom,
|
|
1031
1539
|
To: feishuTo,
|
|
1032
1540
|
SessionKey: route.sessionKey,
|
|
@@ -1043,6 +1551,7 @@ export async function handleFeishuMessage(params: {
|
|
|
1043
1551
|
CommandAuthorized: commandAuthorized,
|
|
1044
1552
|
OriginatingChannel: "feishu" as const,
|
|
1045
1553
|
OriginatingTo: feishuTo,
|
|
1554
|
+
GroupSystemPrompt: isGroup ? groupConfig?.systemPrompt?.trim() || undefined : undefined,
|
|
1046
1555
|
ReplyToBody: quotedContent,
|
|
1047
1556
|
...mediaPayload,
|
|
1048
1557
|
});
|
|
@@ -1052,9 +1561,10 @@ export async function handleFeishuMessage(params: {
|
|
|
1052
1561
|
agentId: route.agentId,
|
|
1053
1562
|
runtime: runtime as RuntimeEnv,
|
|
1054
1563
|
chatId: ctx.chatId,
|
|
1055
|
-
replyToMessageId
|
|
1564
|
+
replyToMessageId,
|
|
1056
1565
|
mentionTargets: ctx.mentionTargets,
|
|
1057
1566
|
accountId: account.accountId,
|
|
1567
|
+
messageCreateTimeMs,
|
|
1058
1568
|
});
|
|
1059
1569
|
|
|
1060
1570
|
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
|