@m1heng-clawd/feishu 0.1.13 → 0.1.14
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/package.json +1 -1
- package/src/bot.ts +181 -15
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -99,6 +99,137 @@ type SenderNameResult = {
|
|
|
99
99
|
permissionError?: PermissionError;
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
+
type FeishuPairingApiMode = "legacy" | "scoped";
|
|
103
|
+
let detectedFeishuPairingApiMode: FeishuPairingApiMode | null = null;
|
|
104
|
+
|
|
105
|
+
async function resolveFeishuPairingApiMode(params: {
|
|
106
|
+
readAllowFromStore: (...args: any[]) => Promise<Array<string | number>>;
|
|
107
|
+
}): Promise<FeishuPairingApiMode> {
|
|
108
|
+
if (detectedFeishuPairingApiMode) return detectedFeishuPairingApiMode;
|
|
109
|
+
|
|
110
|
+
let channelObserved = false;
|
|
111
|
+
const probe = new Proxy<Record<PropertyKey, unknown>>(
|
|
112
|
+
{},
|
|
113
|
+
{
|
|
114
|
+
get(_target, prop) {
|
|
115
|
+
if (prop === "channel") {
|
|
116
|
+
channelObserved = true;
|
|
117
|
+
return "__feishu_pairing_probe__";
|
|
118
|
+
}
|
|
119
|
+
if (prop === "accountId") return "__feishu_pairing_probe__";
|
|
120
|
+
if (prop === "env") return undefined;
|
|
121
|
+
if (prop === Symbol.toPrimitive) return () => "__feishu_pairing_probe__";
|
|
122
|
+
if (prop === "toString") return () => "__feishu_pairing_probe__";
|
|
123
|
+
return undefined;
|
|
124
|
+
},
|
|
125
|
+
has(_target, prop) {
|
|
126
|
+
if (prop === "channel") {
|
|
127
|
+
channelObserved = true;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Probe once to detect whether runtime reads object-style params (scoped API)
|
|
137
|
+
// or positional params (legacy API).
|
|
138
|
+
await params.readAllowFromStore(probe as any, undefined, "__feishu_pairing_probe__");
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore probe errors; fallback below still keeps behavior safe.
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
detectedFeishuPairingApiMode = channelObserved ? "scoped" : "legacy";
|
|
144
|
+
return detectedFeishuPairingApiMode;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function readFeishuPairingAllowFrom(params: {
|
|
148
|
+
core: ReturnType<typeof getFeishuRuntime>;
|
|
149
|
+
accountId: string;
|
|
150
|
+
}): Promise<Array<string | number>> {
|
|
151
|
+
const pairing = params.core.channel.pairing as {
|
|
152
|
+
readAllowFromStore: (...args: any[]) => Promise<Array<string | number>>;
|
|
153
|
+
};
|
|
154
|
+
const pairingMode = await resolveFeishuPairingApiMode({
|
|
155
|
+
readAllowFromStore: pairing.readAllowFromStore,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (pairingMode === "scoped") {
|
|
159
|
+
try {
|
|
160
|
+
return await pairing.readAllowFromStore({
|
|
161
|
+
channel: "feishu",
|
|
162
|
+
accountId: params.accountId,
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
// Compatibility fallback for older OpenClaw implementations.
|
|
166
|
+
return await pairing.readAllowFromStore("feishu", undefined, params.accountId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// OpenClaw <= 2026.2.19 signature: readAllowFromStore(channel, env?, accountId?)
|
|
172
|
+
return await pairing.readAllowFromStore("feishu", undefined, params.accountId);
|
|
173
|
+
} catch {
|
|
174
|
+
// Compatibility fallback for newer OpenClaw implementations.
|
|
175
|
+
return await pairing.readAllowFromStore({
|
|
176
|
+
channel: "feishu",
|
|
177
|
+
accountId: params.accountId,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function upsertFeishuPairingRequest(params: {
|
|
183
|
+
core: ReturnType<typeof getFeishuRuntime>;
|
|
184
|
+
accountId: string;
|
|
185
|
+
senderId: string;
|
|
186
|
+
senderName?: string;
|
|
187
|
+
}): Promise<{ code: string; created: boolean }> {
|
|
188
|
+
const pairing = params.core.channel.pairing as {
|
|
189
|
+
upsertPairingRequest: (...args: any[]) => Promise<{ code: string; created: boolean }>;
|
|
190
|
+
};
|
|
191
|
+
const meta = params.senderName ? { name: params.senderName } : undefined;
|
|
192
|
+
const pairingMode = await resolveFeishuPairingApiMode({
|
|
193
|
+
readAllowFromStore: params.core.channel.pairing.readAllowFromStore as (...args: any[]) => Promise<
|
|
194
|
+
Array<string | number>
|
|
195
|
+
>,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (pairingMode === "scoped") {
|
|
199
|
+
try {
|
|
200
|
+
return await pairing.upsertPairingRequest({
|
|
201
|
+
channel: "feishu",
|
|
202
|
+
id: params.senderId,
|
|
203
|
+
accountId: params.accountId,
|
|
204
|
+
meta,
|
|
205
|
+
});
|
|
206
|
+
} catch {
|
|
207
|
+
// Compatibility fallback for older OpenClaw implementations.
|
|
208
|
+
return await pairing.upsertPairingRequest({
|
|
209
|
+
channel: "feishu",
|
|
210
|
+
id: params.senderId,
|
|
211
|
+
meta,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
return await pairing.upsertPairingRequest({
|
|
218
|
+
channel: "feishu",
|
|
219
|
+
id: params.senderId,
|
|
220
|
+
meta,
|
|
221
|
+
});
|
|
222
|
+
} catch {
|
|
223
|
+
// Compatibility fallback for newer OpenClaw implementations.
|
|
224
|
+
return await pairing.upsertPairingRequest({
|
|
225
|
+
channel: "feishu",
|
|
226
|
+
id: params.senderId,
|
|
227
|
+
accountId: params.accountId,
|
|
228
|
+
meta,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
102
233
|
async function resolveFeishuSenderName(params: {
|
|
103
234
|
account: ResolvedFeishuAccount;
|
|
104
235
|
senderOpenId: string;
|
|
@@ -226,20 +357,48 @@ export type FeishuBotAddedEvent = {
|
|
|
226
357
|
function parseMessageContent(content: string, messageType: string): string {
|
|
227
358
|
try {
|
|
228
359
|
const parsed = JSON.parse(content);
|
|
229
|
-
|
|
360
|
+
const normalizedMessageType = normalizeFeishuInboundMessageType(messageType);
|
|
361
|
+
if (normalizedMessageType === "text") {
|
|
230
362
|
return parsed.text || "";
|
|
231
363
|
}
|
|
232
|
-
if (
|
|
364
|
+
if (normalizedMessageType === "post") {
|
|
233
365
|
// Extract text content from rich text post
|
|
234
366
|
const { textContent } = parsePostContent(content);
|
|
235
367
|
return textContent;
|
|
236
368
|
}
|
|
369
|
+
if (["image", "file", "audio", "video", "sticker"].includes(normalizedMessageType)) {
|
|
370
|
+
return inferPlaceholder(normalizedMessageType);
|
|
371
|
+
}
|
|
237
372
|
return content;
|
|
238
373
|
} catch {
|
|
239
374
|
return content;
|
|
240
375
|
}
|
|
241
376
|
}
|
|
242
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Feishu may use "media" for inbound video messages.
|
|
380
|
+
* Normalize to "video" so downstream media handling is consistent.
|
|
381
|
+
*/
|
|
382
|
+
export function normalizeFeishuInboundMessageType(messageType: string): string {
|
|
383
|
+
return messageType === "media" ? "video" : messageType;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Select the correct resource key for messageResource download.
|
|
388
|
+
* - image message: prefer image_key
|
|
389
|
+
* - other media (video/audio/file/sticker): prefer file_key
|
|
390
|
+
*/
|
|
391
|
+
export function selectFeishuMessageResourceKey(
|
|
392
|
+
messageType: string,
|
|
393
|
+
keys: { imageKey?: string; fileKey?: string },
|
|
394
|
+
): string | undefined {
|
|
395
|
+
const normalized = normalizeFeishuInboundMessageType(messageType);
|
|
396
|
+
if (normalized === "image") {
|
|
397
|
+
return keys.imageKey ?? keys.fileKey;
|
|
398
|
+
}
|
|
399
|
+
return keys.fileKey ?? keys.imageKey;
|
|
400
|
+
}
|
|
401
|
+
|
|
243
402
|
function checkBotMentioned(
|
|
244
403
|
event: FeishuMessageEvent,
|
|
245
404
|
botOpenId?: string,
|
|
@@ -288,6 +447,7 @@ function parseMediaKeys(
|
|
|
288
447
|
case "audio":
|
|
289
448
|
return { fileKey: parsed.file_key };
|
|
290
449
|
case "video":
|
|
450
|
+
case "media":
|
|
291
451
|
// Video has both file_key (video) and image_key (thumbnail)
|
|
292
452
|
return { fileKey: parsed.file_key, imageKey: parsed.image_key };
|
|
293
453
|
case "sticker":
|
|
@@ -363,6 +523,7 @@ function inferPlaceholder(messageType: string): string {
|
|
|
363
523
|
case "audio":
|
|
364
524
|
return "<media:audio>";
|
|
365
525
|
case "video":
|
|
526
|
+
case "media":
|
|
366
527
|
return "<media:video>";
|
|
367
528
|
case "sticker":
|
|
368
529
|
return "<media:sticker>";
|
|
@@ -385,10 +546,11 @@ async function resolveFeishuMediaList(params: {
|
|
|
385
546
|
accountId?: string;
|
|
386
547
|
}): Promise<FeishuMediaInfo[]> {
|
|
387
548
|
const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
|
|
549
|
+
const normalizedMessageType = normalizeFeishuInboundMessageType(messageType);
|
|
388
550
|
|
|
389
551
|
// Only process media message types (including post for embedded images)
|
|
390
552
|
const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
|
|
391
|
-
if (!mediaTypes.includes(
|
|
553
|
+
if (!mediaTypes.includes(normalizedMessageType)) {
|
|
392
554
|
return [];
|
|
393
555
|
}
|
|
394
556
|
|
|
@@ -396,7 +558,7 @@ async function resolveFeishuMediaList(params: {
|
|
|
396
558
|
const core = getFeishuRuntime();
|
|
397
559
|
|
|
398
560
|
// Handle post (rich text) messages with embedded images
|
|
399
|
-
if (
|
|
561
|
+
if (normalizedMessageType === "post") {
|
|
400
562
|
const { imageKeys } = parsePostContent(content);
|
|
401
563
|
if (imageKeys.length === 0) {
|
|
402
564
|
return [];
|
|
@@ -443,7 +605,7 @@ async function resolveFeishuMediaList(params: {
|
|
|
443
605
|
}
|
|
444
606
|
|
|
445
607
|
// Handle other media types
|
|
446
|
-
const mediaKeys = parseMediaKeys(content,
|
|
608
|
+
const mediaKeys = parseMediaKeys(content, normalizedMessageType);
|
|
447
609
|
if (!mediaKeys.imageKey && !mediaKeys.fileKey) {
|
|
448
610
|
return [];
|
|
449
611
|
}
|
|
@@ -455,12 +617,12 @@ async function resolveFeishuMediaList(params: {
|
|
|
455
617
|
|
|
456
618
|
// For message media, always use messageResource API
|
|
457
619
|
// The image.get API is only for images uploaded via im/v1/images, not for message attachments
|
|
458
|
-
const fileKey =
|
|
620
|
+
const fileKey = selectFeishuMessageResourceKey(normalizedMessageType, mediaKeys);
|
|
459
621
|
if (!fileKey) {
|
|
460
622
|
return [];
|
|
461
623
|
}
|
|
462
624
|
|
|
463
|
-
const resourceType =
|
|
625
|
+
const resourceType = normalizedMessageType === "image" ? "image" : "file";
|
|
464
626
|
const result = await downloadMessageResourceFeishu({
|
|
465
627
|
cfg,
|
|
466
628
|
messageId,
|
|
@@ -489,12 +651,12 @@ async function resolveFeishuMediaList(params: {
|
|
|
489
651
|
out.push({
|
|
490
652
|
path: saved.path,
|
|
491
653
|
contentType: saved.contentType,
|
|
492
|
-
placeholder: inferPlaceholder(
|
|
654
|
+
placeholder: inferPlaceholder(normalizedMessageType),
|
|
493
655
|
});
|
|
494
656
|
|
|
495
|
-
log?.(`feishu: downloaded ${
|
|
657
|
+
log?.(`feishu: downloaded ${normalizedMessageType} media, saved to ${saved.path}`);
|
|
496
658
|
} catch (err) {
|
|
497
|
-
log?.(`feishu: failed to download ${
|
|
659
|
+
log?.(`feishu: failed to download ${normalizedMessageType} media: ${String(err)}`);
|
|
498
660
|
}
|
|
499
661
|
|
|
500
662
|
return out;
|
|
@@ -648,7 +810,10 @@ export async function handleFeishuMessage(params: {
|
|
|
648
810
|
const hasControlCommand = core.channel.text.hasControlCommand(ctx.content, cfg);
|
|
649
811
|
const storeAllowFrom =
|
|
650
812
|
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized)
|
|
651
|
-
? await
|
|
813
|
+
? await readFeishuPairingAllowFrom({
|
|
814
|
+
core,
|
|
815
|
+
accountId: account.accountId,
|
|
816
|
+
}).catch(() => [])
|
|
652
817
|
: [];
|
|
653
818
|
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
|
654
819
|
const dmAllowed = resolveFeishuAllowlistMatch({
|
|
@@ -659,10 +824,11 @@ export async function handleFeishuMessage(params: {
|
|
|
659
824
|
|
|
660
825
|
if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
|
|
661
826
|
if (dmPolicy === "pairing") {
|
|
662
|
-
const { code, created } = await
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
827
|
+
const { code, created } = await upsertFeishuPairingRequest({
|
|
828
|
+
core,
|
|
829
|
+
accountId: account.accountId,
|
|
830
|
+
senderId: ctx.senderOpenId,
|
|
831
|
+
senderName: ctx.senderName,
|
|
666
832
|
});
|
|
667
833
|
if (created) {
|
|
668
834
|
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|