@tobeyoureyes/feishu 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -10
- package/src/channel.ts +69 -1
- package/src/webhook.ts +48 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tobeyoureyes/feishu",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "OpenClaw Feishu (Lark) channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -8,17 +8,11 @@
|
|
|
8
8
|
"src",
|
|
9
9
|
"openclaw.plugin.json"
|
|
10
10
|
],
|
|
11
|
-
"
|
|
12
|
-
"@larksuiteoapi/node-sdk": "^1.58.0",
|
|
13
|
-
"openclaw": "workspace:*"
|
|
14
|
-
},
|
|
15
|
-
"peerDependencies": {
|
|
11
|
+
"dependencies": {
|
|
16
12
|
"@larksuiteoapi/node-sdk": "^1.58.0"
|
|
17
13
|
},
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"optional": true
|
|
21
|
-
}
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"openclaw": "workspace:*"
|
|
22
16
|
},
|
|
23
17
|
"openclaw": {
|
|
24
18
|
"extensions": [
|
package/src/channel.ts
CHANGED
|
@@ -37,6 +37,11 @@ import { createGroupHistoryManager, type HistoryEntry } from "./history.js";
|
|
|
37
37
|
import { buildFeishuMessageContext } from "./context.js";
|
|
38
38
|
import { dispatchFeishuMessage, createDefaultReplySender, createDefaultMediaSender } from "./dispatch.js";
|
|
39
39
|
|
|
40
|
+
// Node.js imports for media handling
|
|
41
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
42
|
+
import { tmpdir } from "node:os";
|
|
43
|
+
import { join } from "node:path";
|
|
44
|
+
|
|
40
45
|
// ============ Helper Functions ============
|
|
41
46
|
|
|
42
47
|
/**
|
|
@@ -68,6 +73,66 @@ async function fetchReplyContext(
|
|
|
68
73
|
return inbound;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Save media data to a temporary file
|
|
78
|
+
*/
|
|
79
|
+
async function saveTempMedia(
|
|
80
|
+
data: ArrayBuffer,
|
|
81
|
+
key: string,
|
|
82
|
+
fileName?: string,
|
|
83
|
+
defaultExt: string = "bin",
|
|
84
|
+
): Promise<string> {
|
|
85
|
+
const dir = join(tmpdir(), "openclaw-feishu-media");
|
|
86
|
+
await mkdir(dir, { recursive: true });
|
|
87
|
+
const ext = fileName?.split(".").pop() ?? defaultExt;
|
|
88
|
+
const path = join(dir, `${key}.${ext}`);
|
|
89
|
+
await writeFile(path, Buffer.from(data));
|
|
90
|
+
return path;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Download media from Feishu message (image/file/audio/video)
|
|
95
|
+
*/
|
|
96
|
+
async function downloadMessageMedia(
|
|
97
|
+
account: ResolvedFeishuAccount,
|
|
98
|
+
message: FeishuInboundMessage,
|
|
99
|
+
log?: { warn?: (msg: string) => void },
|
|
100
|
+
): Promise<FeishuInboundMessage> {
|
|
101
|
+
// Image message
|
|
102
|
+
if (message.imageKey) {
|
|
103
|
+
try {
|
|
104
|
+
const result = await api.downloadImage(account, message.imageKey);
|
|
105
|
+
if (result.ok && result.data) {
|
|
106
|
+
const tempPath = await saveTempMedia(result.data, message.imageKey, undefined, "png");
|
|
107
|
+
return { ...message, mediaPath: tempPath, mediaType: "image/png" };
|
|
108
|
+
}
|
|
109
|
+
log?.warn?.(`failed to download image ${message.imageKey}: ${result.error ?? "unknown error"}`);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
log?.warn?.(`failed to download image ${message.imageKey}: ${String(err)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// File/audio/video message
|
|
116
|
+
if (message.fileKey) {
|
|
117
|
+
try {
|
|
118
|
+
const result = await api.downloadFile(account, message.fileKey);
|
|
119
|
+
if (result.ok && result.data) {
|
|
120
|
+
const ext = message.messageType === "audio" ? "ogg" : message.messageType === "media" ? "mp4" : undefined;
|
|
121
|
+
const tempPath = await saveTempMedia(result.data, message.fileKey, message.fileName, ext);
|
|
122
|
+
const mimeType = message.messageType === "audio" ? "audio/ogg"
|
|
123
|
+
: message.messageType === "media" ? "video/mp4"
|
|
124
|
+
: "application/octet-stream";
|
|
125
|
+
return { ...message, mediaPath: tempPath, mediaType: mimeType };
|
|
126
|
+
}
|
|
127
|
+
log?.warn?.(`failed to download file ${message.fileKey}: ${result.error ?? "unknown error"}`);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
log?.warn?.(`failed to download file ${message.fileKey}: ${String(err)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return message;
|
|
134
|
+
}
|
|
135
|
+
|
|
71
136
|
// Channel metadata
|
|
72
137
|
const meta: ChannelMeta = {
|
|
73
138
|
id: "feishu",
|
|
@@ -748,6 +813,9 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
748
813
|
// Fetch reply context if this is a reply
|
|
749
814
|
const messageWithContext = await fetchReplyContext(account, inbound, ctx.log);
|
|
750
815
|
|
|
816
|
+
// Download media (image/file/audio/video) if present
|
|
817
|
+
const messageWithMedia = await downloadMessageMedia(account, messageWithContext, ctx.log);
|
|
818
|
+
|
|
751
819
|
// Prepare group history
|
|
752
820
|
const isGroup = inbound.chatType === "group";
|
|
753
821
|
const historyEntry: HistoryEntry | undefined = isGroup
|
|
@@ -765,7 +833,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
765
833
|
try {
|
|
766
834
|
// Build message context using local module
|
|
767
835
|
const context = await buildFeishuMessageContext({
|
|
768
|
-
message:
|
|
836
|
+
message: messageWithMedia,
|
|
769
837
|
account,
|
|
770
838
|
cfg: ctx.cfg,
|
|
771
839
|
botOpenId,
|
package/src/webhook.ts
CHANGED
|
@@ -341,6 +341,10 @@ function handleBotRemoved(event: FeishuBotRemovedEvent): FeishuWebhookResult {
|
|
|
341
341
|
|
|
342
342
|
/**
|
|
343
343
|
* Extract plain text from post content
|
|
344
|
+
* Supports multiple formats:
|
|
345
|
+
* - { zh_cn: { title, content } } - with language tag
|
|
346
|
+
* - { title, content } - direct format (common in received messages)
|
|
347
|
+
* - { post: { zh_cn: { ... } } } - nested post field
|
|
344
348
|
*/
|
|
345
349
|
function extractTextFromPost(content: unknown): string {
|
|
346
350
|
const texts: string[] = [];
|
|
@@ -351,30 +355,58 @@ function extractTextFromPost(content: unknown): string {
|
|
|
351
355
|
continue;
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
const el = element as { tag?: string; text?: string;
|
|
358
|
+
const el = element as { tag?: string; text?: string; user_name?: string };
|
|
355
359
|
|
|
356
|
-
if (el.tag === "text" && el.text) {
|
|
357
|
-
texts.push(el.text);
|
|
358
|
-
} else if (el.tag === "a" && el.text) {
|
|
360
|
+
if ((el.tag === "text" || el.tag === "a") && el.text) {
|
|
359
361
|
texts.push(el.text);
|
|
362
|
+
} else if (el.tag === "at" && el.user_name) {
|
|
363
|
+
texts.push(`@${el.user_name}`);
|
|
360
364
|
}
|
|
361
365
|
}
|
|
362
366
|
}
|
|
363
367
|
|
|
364
|
-
if (typeof content
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
368
|
+
if (typeof content !== "object" || content === null) {
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const obj = content as Record<string, unknown>;
|
|
373
|
+
|
|
374
|
+
let postContent: unknown[][] | undefined;
|
|
375
|
+
let title: string | undefined;
|
|
376
|
+
|
|
377
|
+
// Format 1: { zh_cn/en_us: { title, content } }
|
|
378
|
+
const langPost = (obj.zh_cn || obj.en_us) as { title?: string; content?: unknown[][] } | undefined;
|
|
379
|
+
if (langPost?.content) {
|
|
380
|
+
postContent = langPost.content;
|
|
381
|
+
title = langPost.title;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Format 2: { title, content } - direct format
|
|
385
|
+
if (!postContent && Array.isArray(obj.content)) {
|
|
386
|
+
postContent = obj.content as unknown[][];
|
|
387
|
+
title = obj.title as string | undefined;
|
|
388
|
+
}
|
|
369
389
|
|
|
370
|
-
|
|
371
|
-
|
|
390
|
+
// Format 3: { post: { zh_cn: { ... } } }
|
|
391
|
+
if (!postContent && obj.post) {
|
|
392
|
+
const nested = obj.post as Record<string, unknown>;
|
|
393
|
+
const nestedLang = (nested.zh_cn || nested.en_us) as { title?: string; content?: unknown[][] } | undefined;
|
|
394
|
+
if (nestedLang?.content) {
|
|
395
|
+
postContent = nestedLang.content;
|
|
396
|
+
title = nestedLang.title;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Extract title
|
|
401
|
+
if (title) {
|
|
402
|
+
texts.push(title);
|
|
403
|
+
}
|
|
372
404
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
405
|
+
// Extract content
|
|
406
|
+
if (Array.isArray(postContent)) {
|
|
407
|
+
for (const line of postContent) {
|
|
408
|
+
if (Array.isArray(line)) {
|
|
409
|
+
extractFromElements(line);
|
|
378
410
|
}
|
|
379
411
|
}
|
|
380
412
|
}
|