@tobeyoureyes/feishu 1.0.0
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 +290 -0
- package/index.ts +18 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +42 -0
- package/src/api.ts +1160 -0
- package/src/auth.ts +133 -0
- package/src/channel.ts +883 -0
- package/src/context.ts +292 -0
- package/src/dedupe.ts +85 -0
- package/src/dispatch.ts +185 -0
- package/src/history.ts +130 -0
- package/src/inbound.ts +83 -0
- package/src/message.ts +386 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +330 -0
- package/src/webhook.ts +549 -0
- package/src/websocket.ts +372 -0
package/src/api.ts
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feishu API wrapper for messaging
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
FeishuApiResponse,
|
|
7
|
+
FeishuSendMessageRequest,
|
|
8
|
+
FeishuSendMessageResponse,
|
|
9
|
+
FeishuReceiveIdType,
|
|
10
|
+
FeishuMsgType,
|
|
11
|
+
FeishuTextContent,
|
|
12
|
+
FeishuPostContent,
|
|
13
|
+
FeishuInteractiveContent,
|
|
14
|
+
ResolvedFeishuAccount,
|
|
15
|
+
FeishuSendResult,
|
|
16
|
+
FeishuSendOptions,
|
|
17
|
+
FeishuRenderMode,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
import { getTenantAccessToken, invalidateToken } from "./auth.js";
|
|
20
|
+
|
|
21
|
+
/** Get API base URL for an account */
|
|
22
|
+
function getApiBase(account: ResolvedFeishuAccount): string {
|
|
23
|
+
return account.apiBase || "https://open.feishu.cn/open-apis";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Make an authenticated API request to Feishu
|
|
28
|
+
*/
|
|
29
|
+
async function feishuRequest<T>(
|
|
30
|
+
account: ResolvedFeishuAccount,
|
|
31
|
+
method: string,
|
|
32
|
+
endpoint: string,
|
|
33
|
+
body?: unknown,
|
|
34
|
+
queryParams?: Record<string, string>,
|
|
35
|
+
retryOnAuthError = true,
|
|
36
|
+
): Promise<FeishuApiResponse<T>> {
|
|
37
|
+
const token = await getTenantAccessToken(account);
|
|
38
|
+
const apiBase = getApiBase(account);
|
|
39
|
+
|
|
40
|
+
let url = `${apiBase}${endpoint}`;
|
|
41
|
+
if (queryParams) {
|
|
42
|
+
const params = new URLSearchParams(queryParams);
|
|
43
|
+
url += `?${params.toString()}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method,
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${token}`,
|
|
50
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
51
|
+
},
|
|
52
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const data = (await response.json()) as FeishuApiResponse<T>;
|
|
56
|
+
|
|
57
|
+
// Handle token expiration
|
|
58
|
+
if (data.code === 99991663 || data.code === 99991664) {
|
|
59
|
+
if (retryOnAuthError) {
|
|
60
|
+
invalidateToken(account.accountId);
|
|
61
|
+
return feishuRequest(account, method, endpoint, body, queryParams, false);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Send a message to Feishu
|
|
70
|
+
*/
|
|
71
|
+
async function sendMessage(
|
|
72
|
+
account: ResolvedFeishuAccount,
|
|
73
|
+
receiveId: string,
|
|
74
|
+
msgType: FeishuMsgType,
|
|
75
|
+
content: string,
|
|
76
|
+
receiveIdType: FeishuReceiveIdType = "chat_id",
|
|
77
|
+
replyToId?: string,
|
|
78
|
+
): Promise<FeishuSendResult> {
|
|
79
|
+
const body: FeishuSendMessageRequest = {
|
|
80
|
+
receive_id: receiveId,
|
|
81
|
+
msg_type: msgType,
|
|
82
|
+
content,
|
|
83
|
+
uuid: generateUuid(),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const endpoint = replyToId
|
|
87
|
+
? `/im/v1/messages/${replyToId}/reply`
|
|
88
|
+
: "/im/v1/messages";
|
|
89
|
+
|
|
90
|
+
const queryParams = replyToId ? undefined : { receive_id_type: receiveIdType };
|
|
91
|
+
|
|
92
|
+
const response = await feishuRequest<FeishuSendMessageResponse>(
|
|
93
|
+
account,
|
|
94
|
+
"POST",
|
|
95
|
+
endpoint,
|
|
96
|
+
body,
|
|
97
|
+
queryParams,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (response.code !== 0) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: `Feishu API error: ${response.code} - ${response.msg}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
messageId: response.data?.message_id,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Send a text message
|
|
115
|
+
*/
|
|
116
|
+
export async function sendText(
|
|
117
|
+
account: ResolvedFeishuAccount,
|
|
118
|
+
receiveId: string,
|
|
119
|
+
text: string,
|
|
120
|
+
options: FeishuSendOptions = {},
|
|
121
|
+
): Promise<FeishuSendResult> {
|
|
122
|
+
const content: FeishuTextContent = { text };
|
|
123
|
+
return sendMessage(
|
|
124
|
+
account,
|
|
125
|
+
receiveId,
|
|
126
|
+
"text",
|
|
127
|
+
JSON.stringify(content),
|
|
128
|
+
options.receiveIdType ?? "chat_id",
|
|
129
|
+
options.replyToId,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Send a rich text (post) message
|
|
135
|
+
*/
|
|
136
|
+
export async function sendPost(
|
|
137
|
+
account: ResolvedFeishuAccount,
|
|
138
|
+
receiveId: string,
|
|
139
|
+
postContent: FeishuPostContent,
|
|
140
|
+
options: FeishuSendOptions = {},
|
|
141
|
+
): Promise<FeishuSendResult> {
|
|
142
|
+
return sendMessage(
|
|
143
|
+
account,
|
|
144
|
+
receiveId,
|
|
145
|
+
"post",
|
|
146
|
+
JSON.stringify(postContent),
|
|
147
|
+
options.receiveIdType ?? "chat_id",
|
|
148
|
+
options.replyToId,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Send an interactive card message
|
|
154
|
+
*/
|
|
155
|
+
export async function sendCard(
|
|
156
|
+
account: ResolvedFeishuAccount,
|
|
157
|
+
receiveId: string,
|
|
158
|
+
cardContent: FeishuInteractiveContent,
|
|
159
|
+
options: FeishuSendOptions = {},
|
|
160
|
+
): Promise<FeishuSendResult> {
|
|
161
|
+
return sendMessage(
|
|
162
|
+
account,
|
|
163
|
+
receiveId,
|
|
164
|
+
"interactive",
|
|
165
|
+
JSON.stringify(cardContent),
|
|
166
|
+
options.receiveIdType ?? "chat_id",
|
|
167
|
+
options.replyToId,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Send an image message
|
|
173
|
+
*/
|
|
174
|
+
export async function sendImage(
|
|
175
|
+
account: ResolvedFeishuAccount,
|
|
176
|
+
receiveId: string,
|
|
177
|
+
imageKey: string,
|
|
178
|
+
options: FeishuSendOptions = {},
|
|
179
|
+
): Promise<FeishuSendResult> {
|
|
180
|
+
const content = { image_key: imageKey };
|
|
181
|
+
return sendMessage(
|
|
182
|
+
account,
|
|
183
|
+
receiveId,
|
|
184
|
+
"image",
|
|
185
|
+
JSON.stringify(content),
|
|
186
|
+
options.receiveIdType ?? "chat_id",
|
|
187
|
+
options.replyToId,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Send a file message
|
|
193
|
+
*/
|
|
194
|
+
export async function sendFile(
|
|
195
|
+
account: ResolvedFeishuAccount,
|
|
196
|
+
receiveId: string,
|
|
197
|
+
fileKey: string,
|
|
198
|
+
options: FeishuSendOptions = {},
|
|
199
|
+
): Promise<FeishuSendResult> {
|
|
200
|
+
const content = { file_key: fileKey };
|
|
201
|
+
return sendMessage(
|
|
202
|
+
account,
|
|
203
|
+
receiveId,
|
|
204
|
+
"file",
|
|
205
|
+
JSON.stringify(content),
|
|
206
|
+
options.receiveIdType ?? "chat_id",
|
|
207
|
+
options.replyToId,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Upload an image to Feishu and get the image_key
|
|
213
|
+
*/
|
|
214
|
+
export async function uploadImage(
|
|
215
|
+
account: ResolvedFeishuAccount,
|
|
216
|
+
imageBuffer: ArrayBuffer,
|
|
217
|
+
imageName: string,
|
|
218
|
+
): Promise<{ ok: boolean; imageKey?: string; error?: string }> {
|
|
219
|
+
const token = await getTenantAccessToken(account);
|
|
220
|
+
const apiBase = getApiBase(account);
|
|
221
|
+
|
|
222
|
+
const formData = new FormData();
|
|
223
|
+
formData.append("image_type", "message");
|
|
224
|
+
formData.append("image", new Blob([imageBuffer]), imageName);
|
|
225
|
+
|
|
226
|
+
const response = await fetch(`${apiBase}/im/v1/images`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: {
|
|
229
|
+
Authorization: `Bearer ${token}`,
|
|
230
|
+
},
|
|
231
|
+
body: formData,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const data = (await response.json()) as FeishuApiResponse<{ image_key: string }>;
|
|
235
|
+
|
|
236
|
+
if (data.code !== 0) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
error: `Failed to upload image: ${data.code} - ${data.msg}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
imageKey: data.data?.image_key,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Upload a file to Feishu and get the file_key
|
|
251
|
+
*/
|
|
252
|
+
export async function uploadFile(
|
|
253
|
+
account: ResolvedFeishuAccount,
|
|
254
|
+
fileBuffer: ArrayBuffer,
|
|
255
|
+
fileName: string,
|
|
256
|
+
fileType: "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream",
|
|
257
|
+
): Promise<{ ok: boolean; fileKey?: string; error?: string }> {
|
|
258
|
+
const token = await getTenantAccessToken(account);
|
|
259
|
+
const apiBase = getApiBase(account);
|
|
260
|
+
|
|
261
|
+
const formData = new FormData();
|
|
262
|
+
formData.append("file_type", fileType);
|
|
263
|
+
formData.append("file_name", fileName);
|
|
264
|
+
formData.append("file", new Blob([fileBuffer]), fileName);
|
|
265
|
+
|
|
266
|
+
const response = await fetch(`${apiBase}/im/v1/files`, {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: {
|
|
269
|
+
Authorization: `Bearer ${token}`,
|
|
270
|
+
},
|
|
271
|
+
body: formData,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const data = (await response.json()) as FeishuApiResponse<{ file_key: string }>;
|
|
275
|
+
|
|
276
|
+
if (data.code !== 0) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
error: `Failed to upload file: ${data.code} - ${data.msg}`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
fileKey: data.data?.file_key,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get chat information
|
|
291
|
+
*/
|
|
292
|
+
export async function getChatInfo(
|
|
293
|
+
account: ResolvedFeishuAccount,
|
|
294
|
+
chatId: string,
|
|
295
|
+
): Promise<FeishuApiResponse<{
|
|
296
|
+
chat_id: string;
|
|
297
|
+
name: string;
|
|
298
|
+
description: string;
|
|
299
|
+
owner_id: string;
|
|
300
|
+
owner_id_type: string;
|
|
301
|
+
chat_mode: string;
|
|
302
|
+
chat_type: string;
|
|
303
|
+
external: boolean;
|
|
304
|
+
}>> {
|
|
305
|
+
return feishuRequest(account, "GET", `/im/v1/chats/${chatId}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get user info by user ID
|
|
310
|
+
*/
|
|
311
|
+
export async function getUserInfo(
|
|
312
|
+
account: ResolvedFeishuAccount,
|
|
313
|
+
userId: string,
|
|
314
|
+
userIdType: "open_id" | "union_id" | "user_id" = "open_id",
|
|
315
|
+
): Promise<FeishuApiResponse<{
|
|
316
|
+
user: {
|
|
317
|
+
open_id: string;
|
|
318
|
+
union_id: string;
|
|
319
|
+
user_id: string;
|
|
320
|
+
name: string;
|
|
321
|
+
en_name: string;
|
|
322
|
+
nickname: string;
|
|
323
|
+
email: string;
|
|
324
|
+
mobile: string;
|
|
325
|
+
avatar: {
|
|
326
|
+
avatar_72: string;
|
|
327
|
+
avatar_240: string;
|
|
328
|
+
avatar_640: string;
|
|
329
|
+
avatar_origin: string;
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
}>> {
|
|
333
|
+
return feishuRequest(account, "GET", `/contact/v3/users/${userId}`, undefined, {
|
|
334
|
+
user_id_type: userIdType,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Test API connectivity (probe)
|
|
340
|
+
*/
|
|
341
|
+
export async function probeFeishu(
|
|
342
|
+
account: ResolvedFeishuAccount,
|
|
343
|
+
timeoutMs = 10000,
|
|
344
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
345
|
+
try {
|
|
346
|
+
const controller = new AbortController();
|
|
347
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
348
|
+
const apiBase = getApiBase(account);
|
|
349
|
+
|
|
350
|
+
// Validate credentials by fetching a token
|
|
351
|
+
await getTenantAccessToken(account);
|
|
352
|
+
|
|
353
|
+
const response = await fetch(`${apiBase}/auth/v3/tenant_access_token/internal`, {
|
|
354
|
+
method: "POST",
|
|
355
|
+
headers: {
|
|
356
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify({
|
|
359
|
+
app_id: account.appId,
|
|
360
|
+
app_secret: account.appSecret,
|
|
361
|
+
}),
|
|
362
|
+
signal: controller.signal,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
clearTimeout(timeoutId);
|
|
366
|
+
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
return { ok: false, error: `HTTP ${response.status}` };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const data = (await response.json()) as FeishuApiResponse;
|
|
372
|
+
|
|
373
|
+
if (data.code !== 0) {
|
|
374
|
+
return { ok: false, error: data.msg };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return { ok: true };
|
|
378
|
+
} catch (error) {
|
|
379
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
380
|
+
return { ok: false, error: message };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Generate a UUID for message deduplication
|
|
386
|
+
*/
|
|
387
|
+
function generateUuid(): string {
|
|
388
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
389
|
+
const r = (Math.random() * 16) | 0;
|
|
390
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
391
|
+
return v.toString(16);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Convert markdown to Feishu post format
|
|
397
|
+
*/
|
|
398
|
+
export function markdownToPost(markdown: string, title?: string): FeishuPostContent {
|
|
399
|
+
const lines = markdown.split("\n");
|
|
400
|
+
const content: Array<Array<{ tag: string; text?: string; href?: string }>> = [];
|
|
401
|
+
|
|
402
|
+
for (const line of lines) {
|
|
403
|
+
if (!line.trim()) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const elements: Array<{ tag: string; text?: string; href?: string }> = [];
|
|
408
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
409
|
+
let lastIndex = 0;
|
|
410
|
+
let match;
|
|
411
|
+
|
|
412
|
+
while ((match = linkRegex.exec(line)) !== null) {
|
|
413
|
+
// Add text before the link
|
|
414
|
+
if (match.index > lastIndex) {
|
|
415
|
+
elements.push({
|
|
416
|
+
tag: "text",
|
|
417
|
+
text: line.slice(lastIndex, match.index),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Add the link
|
|
422
|
+
elements.push({
|
|
423
|
+
tag: "a",
|
|
424
|
+
text: match[1],
|
|
425
|
+
href: match[2],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
lastIndex = match.index + match[0].length;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Add remaining text
|
|
432
|
+
if (lastIndex < line.length) {
|
|
433
|
+
elements.push({
|
|
434
|
+
tag: "text",
|
|
435
|
+
text: line.slice(lastIndex),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (elements.length > 0) {
|
|
440
|
+
content.push(elements);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
zh_cn: {
|
|
446
|
+
title,
|
|
447
|
+
content,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============ Card Building Utilities ============
|
|
453
|
+
|
|
454
|
+
/** Supported card header colors */
|
|
455
|
+
export type CardColor =
|
|
456
|
+
| "blue"
|
|
457
|
+
| "wathet"
|
|
458
|
+
| "turquoise"
|
|
459
|
+
| "green"
|
|
460
|
+
| "yellow"
|
|
461
|
+
| "orange"
|
|
462
|
+
| "red"
|
|
463
|
+
| "carmine"
|
|
464
|
+
| "violet"
|
|
465
|
+
| "purple"
|
|
466
|
+
| "indigo"
|
|
467
|
+
| "grey";
|
|
468
|
+
|
|
469
|
+
/** Options for building a card base */
|
|
470
|
+
export interface CardBaseOptions {
|
|
471
|
+
/** Card title (optional) */
|
|
472
|
+
title?: string;
|
|
473
|
+
/** Header color (default: "blue") */
|
|
474
|
+
color?: CardColor;
|
|
475
|
+
/** Enable wide screen mode (default: true) */
|
|
476
|
+
wideScreen?: boolean;
|
|
477
|
+
/** Enable forward (default: undefined) */
|
|
478
|
+
enableForward?: boolean;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Build a base card structure with common configuration
|
|
483
|
+
* Internal helper to reduce duplication in card creation functions
|
|
484
|
+
*/
|
|
485
|
+
function buildCardBase(options: CardBaseOptions = {}): FeishuInteractiveContent {
|
|
486
|
+
const { title, color = "blue", wideScreen = true, enableForward } = options;
|
|
487
|
+
|
|
488
|
+
const card: FeishuInteractiveContent = {
|
|
489
|
+
config: {
|
|
490
|
+
wide_screen_mode: wideScreen,
|
|
491
|
+
...(enableForward !== undefined && { enable_forward: enableForward }),
|
|
492
|
+
},
|
|
493
|
+
elements: [],
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
if (title) {
|
|
497
|
+
card.header = {
|
|
498
|
+
title: {
|
|
499
|
+
tag: "plain_text",
|
|
500
|
+
content: title,
|
|
501
|
+
},
|
|
502
|
+
template: color,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return card;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create a simple card message
|
|
511
|
+
*/
|
|
512
|
+
export function createSimpleCard(
|
|
513
|
+
title: string,
|
|
514
|
+
content: string,
|
|
515
|
+
color: CardColor = "blue",
|
|
516
|
+
): FeishuInteractiveContent {
|
|
517
|
+
const card = buildCardBase({ title, color });
|
|
518
|
+
card.elements = [{ tag: "markdown", content }];
|
|
519
|
+
return card;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Create a markdown card without header (cleaner look)
|
|
524
|
+
*/
|
|
525
|
+
export function createMarkdownCard(content: string): FeishuInteractiveContent {
|
|
526
|
+
const card = buildCardBase();
|
|
527
|
+
card.elements = [{ tag: "markdown", content }];
|
|
528
|
+
return card;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Create a card with syntax-highlighted code block
|
|
533
|
+
* Feishu supports language-specific code highlighting in markdown cards
|
|
534
|
+
*/
|
|
535
|
+
export function createCodeCard(
|
|
536
|
+
code: string,
|
|
537
|
+
language: string = "plaintext",
|
|
538
|
+
title?: string,
|
|
539
|
+
): FeishuInteractiveContent {
|
|
540
|
+
const card = buildCardBase({ title });
|
|
541
|
+
card.elements = [{ tag: "markdown", content: `\`\`\`${language}\n${code}\n\`\`\`` }];
|
|
542
|
+
return card;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Create a card with a table
|
|
547
|
+
* Feishu cards support markdown tables natively
|
|
548
|
+
*/
|
|
549
|
+
export function createTableCard(
|
|
550
|
+
headers: string[],
|
|
551
|
+
rows: string[][],
|
|
552
|
+
title?: string,
|
|
553
|
+
): FeishuInteractiveContent {
|
|
554
|
+
// Build markdown table
|
|
555
|
+
const headerRow = `| ${headers.join(" | ")} |`;
|
|
556
|
+
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
557
|
+
const dataRows = rows.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
558
|
+
const tableMarkdown = `${headerRow}\n${separator}\n${dataRows}`;
|
|
559
|
+
|
|
560
|
+
const card = buildCardBase({ title });
|
|
561
|
+
card.elements = [{ tag: "markdown", content: tableMarkdown }];
|
|
562
|
+
return card;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Button configuration for interactive cards
|
|
567
|
+
*/
|
|
568
|
+
export interface CardButton {
|
|
569
|
+
/** Button display text */
|
|
570
|
+
text: string;
|
|
571
|
+
/** Button action value */
|
|
572
|
+
value: string;
|
|
573
|
+
/** Button style */
|
|
574
|
+
type?: "primary" | "default" | "danger";
|
|
575
|
+
/** Optional URL for link buttons */
|
|
576
|
+
url?: string;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Create a card with interactive buttons
|
|
581
|
+
*/
|
|
582
|
+
export function createCardWithButtons(
|
|
583
|
+
content: string,
|
|
584
|
+
buttons: CardButton[],
|
|
585
|
+
title?: string,
|
|
586
|
+
): FeishuInteractiveContent {
|
|
587
|
+
const card = buildCardBase({ title });
|
|
588
|
+
card.elements = [
|
|
589
|
+
{ tag: "markdown", content },
|
|
590
|
+
{
|
|
591
|
+
tag: "action",
|
|
592
|
+
actions: buttons.map((btn) => {
|
|
593
|
+
if (btn.url) {
|
|
594
|
+
return {
|
|
595
|
+
tag: "button",
|
|
596
|
+
text: { tag: "plain_text", content: btn.text },
|
|
597
|
+
type: btn.type ?? "default",
|
|
598
|
+
url: btn.url,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
tag: "button",
|
|
603
|
+
text: { tag: "plain_text", content: btn.text },
|
|
604
|
+
type: btn.type ?? "default",
|
|
605
|
+
value: { action: btn.value },
|
|
606
|
+
};
|
|
607
|
+
}),
|
|
608
|
+
},
|
|
609
|
+
];
|
|
610
|
+
return card;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Section configuration for multi-section cards
|
|
615
|
+
*/
|
|
616
|
+
export interface CardSection {
|
|
617
|
+
/** Section type */
|
|
618
|
+
type: "markdown" | "divider" | "note";
|
|
619
|
+
/** Content for markdown sections */
|
|
620
|
+
content?: string;
|
|
621
|
+
/** Elements for note sections */
|
|
622
|
+
elements?: Array<{ tag: string; content?: string }>;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Create a multi-section card with dividers
|
|
627
|
+
*/
|
|
628
|
+
export function createMultiSectionCard(
|
|
629
|
+
sections: CardSection[],
|
|
630
|
+
title?: string,
|
|
631
|
+
color?: CardColor,
|
|
632
|
+
): FeishuInteractiveContent {
|
|
633
|
+
const card = buildCardBase({ title, color });
|
|
634
|
+
card.elements = sections.map((section) => {
|
|
635
|
+
if (section.type === "markdown" && section.content) {
|
|
636
|
+
return { tag: "markdown", content: section.content };
|
|
637
|
+
}
|
|
638
|
+
if (section.type === "divider") {
|
|
639
|
+
return { tag: "hr" };
|
|
640
|
+
}
|
|
641
|
+
if (section.type === "note" && section.elements) {
|
|
642
|
+
return { tag: "note", elements: section.elements };
|
|
643
|
+
}
|
|
644
|
+
return { tag: "markdown", content: section.content ?? "" };
|
|
645
|
+
});
|
|
646
|
+
return card;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Check if text contains code blocks, tables, or other rich content (should use card rendering)
|
|
651
|
+
*/
|
|
652
|
+
export function shouldUseCardRendering(text: string): boolean {
|
|
653
|
+
// Check for code blocks (```code```)
|
|
654
|
+
if (/```[\s\S]*?```/.test(text)) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
// Check for tables (markdown table syntax)
|
|
658
|
+
if (/\|.+\|[\r\n]+\|[-:| ]+\|/.test(text)) {
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
// Check for markdown links [text](url)
|
|
662
|
+
if (/\[.+\]\(.+\)/.test(text)) {
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
// Check for long text (>500 chars) - better display in card
|
|
666
|
+
if (text.length > 500) {
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
// Check for multiple paragraphs
|
|
670
|
+
if ((text.match(/\n\n/g) || []).length >= 3) {
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Convert markdown table to ASCII art table (for raw mode)
|
|
678
|
+
*/
|
|
679
|
+
export function markdownTableToAscii(markdown: string): string {
|
|
680
|
+
// Simple conversion - just clean up the markdown table syntax
|
|
681
|
+
return markdown
|
|
682
|
+
.replace(/\|/g, " | ")
|
|
683
|
+
.replace(/[-:]+\|[-:| ]+/g, (match) => match.replace(/[:|]/g, "-"));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Smart send - automatically choose between text and card based on content and renderMode
|
|
688
|
+
*/
|
|
689
|
+
export async function sendSmart(
|
|
690
|
+
account: ResolvedFeishuAccount,
|
|
691
|
+
receiveId: string,
|
|
692
|
+
text: string,
|
|
693
|
+
options: FeishuSendOptions & { renderMode?: FeishuRenderMode } = {},
|
|
694
|
+
): Promise<FeishuSendResult> {
|
|
695
|
+
const renderMode = options.renderMode ?? account.renderMode ?? "auto";
|
|
696
|
+
|
|
697
|
+
// Determine whether to use card rendering
|
|
698
|
+
let useCard = false;
|
|
699
|
+
let processedText = text;
|
|
700
|
+
|
|
701
|
+
switch (renderMode) {
|
|
702
|
+
case "card":
|
|
703
|
+
useCard = true;
|
|
704
|
+
break;
|
|
705
|
+
case "raw":
|
|
706
|
+
useCard = false;
|
|
707
|
+
// Convert tables to ASCII for raw mode
|
|
708
|
+
processedText = markdownTableToAscii(text);
|
|
709
|
+
break;
|
|
710
|
+
case "auto":
|
|
711
|
+
default:
|
|
712
|
+
useCard = shouldUseCardRendering(text);
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (useCard) {
|
|
717
|
+
// Send as card message
|
|
718
|
+
const card = createMarkdownCard(text);
|
|
719
|
+
return sendCard(account, receiveId, card, options);
|
|
720
|
+
} else {
|
|
721
|
+
// Send as plain text
|
|
722
|
+
return sendText(account, receiveId, processedText, options);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Get chat list that the bot is in
|
|
728
|
+
*/
|
|
729
|
+
export async function getChatList(
|
|
730
|
+
account: ResolvedFeishuAccount,
|
|
731
|
+
pageSize = 50,
|
|
732
|
+
pageToken?: string,
|
|
733
|
+
): Promise<FeishuApiResponse<{
|
|
734
|
+
items: Array<{
|
|
735
|
+
chat_id: string;
|
|
736
|
+
name: string;
|
|
737
|
+
description?: string;
|
|
738
|
+
chat_mode: string;
|
|
739
|
+
chat_type: string;
|
|
740
|
+
external: boolean;
|
|
741
|
+
owner_id?: string;
|
|
742
|
+
}>;
|
|
743
|
+
page_token?: string;
|
|
744
|
+
has_more: boolean;
|
|
745
|
+
}>> {
|
|
746
|
+
const params: Record<string, string> = { page_size: String(pageSize) };
|
|
747
|
+
if (pageToken) {
|
|
748
|
+
params.page_token = pageToken;
|
|
749
|
+
}
|
|
750
|
+
return feishuRequest(account, "GET", "/im/v1/chats", undefined, params);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Get chat members
|
|
755
|
+
*/
|
|
756
|
+
export async function getChatMembers(
|
|
757
|
+
account: ResolvedFeishuAccount,
|
|
758
|
+
chatId: string,
|
|
759
|
+
pageSize = 50,
|
|
760
|
+
pageToken?: string,
|
|
761
|
+
): Promise<FeishuApiResponse<{
|
|
762
|
+
items: Array<{
|
|
763
|
+
member_id: string;
|
|
764
|
+
member_id_type: string;
|
|
765
|
+
name?: string;
|
|
766
|
+
}>;
|
|
767
|
+
page_token?: string;
|
|
768
|
+
has_more: boolean;
|
|
769
|
+
}>> {
|
|
770
|
+
const params: Record<string, string> = { page_size: String(pageSize) };
|
|
771
|
+
if (pageToken) {
|
|
772
|
+
params.page_token = pageToken;
|
|
773
|
+
}
|
|
774
|
+
return feishuRequest(account, "GET", `/im/v1/chats/${chatId}/members`, undefined, params);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Get message history from a chat
|
|
779
|
+
*/
|
|
780
|
+
export async function getMessageHistory(
|
|
781
|
+
account: ResolvedFeishuAccount,
|
|
782
|
+
chatId: string,
|
|
783
|
+
pageSize = 20,
|
|
784
|
+
startTime?: number,
|
|
785
|
+
endTime?: number,
|
|
786
|
+
pageToken?: string,
|
|
787
|
+
): Promise<FeishuApiResponse<{
|
|
788
|
+
items: Array<{
|
|
789
|
+
message_id: string;
|
|
790
|
+
root_id?: string;
|
|
791
|
+
parent_id?: string;
|
|
792
|
+
msg_type: string;
|
|
793
|
+
create_time: string;
|
|
794
|
+
update_time?: string;
|
|
795
|
+
deleted: boolean;
|
|
796
|
+
chat_id: string;
|
|
797
|
+
sender: {
|
|
798
|
+
id: string;
|
|
799
|
+
id_type: string;
|
|
800
|
+
sender_type: string;
|
|
801
|
+
};
|
|
802
|
+
body: {
|
|
803
|
+
content: string;
|
|
804
|
+
};
|
|
805
|
+
}>;
|
|
806
|
+
page_token?: string;
|
|
807
|
+
has_more: boolean;
|
|
808
|
+
}>> {
|
|
809
|
+
const params: Record<string, string> = {
|
|
810
|
+
container_id_type: "chat",
|
|
811
|
+
container_id: chatId,
|
|
812
|
+
page_size: String(pageSize),
|
|
813
|
+
};
|
|
814
|
+
if (startTime) {
|
|
815
|
+
params.start_time = String(startTime);
|
|
816
|
+
}
|
|
817
|
+
if (endTime) {
|
|
818
|
+
params.end_time = String(endTime);
|
|
819
|
+
}
|
|
820
|
+
if (pageToken) {
|
|
821
|
+
params.page_token = pageToken;
|
|
822
|
+
}
|
|
823
|
+
return feishuRequest(account, "GET", "/im/v1/messages", undefined, params);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Download image from Feishu
|
|
828
|
+
*/
|
|
829
|
+
export async function downloadImage(
|
|
830
|
+
account: ResolvedFeishuAccount,
|
|
831
|
+
imageKey: string,
|
|
832
|
+
): Promise<{ ok: boolean; data?: ArrayBuffer; error?: string }> {
|
|
833
|
+
try {
|
|
834
|
+
const token = await getTenantAccessToken(account);
|
|
835
|
+
const apiBase = getApiBase(account);
|
|
836
|
+
|
|
837
|
+
const response = await fetch(`${apiBase}/im/v1/images/${imageKey}`, {
|
|
838
|
+
headers: {
|
|
839
|
+
Authorization: `Bearer ${token}`,
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
if (!response.ok) {
|
|
844
|
+
return { ok: false, error: `HTTP ${response.status}` };
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const data = await response.arrayBuffer();
|
|
848
|
+
return { ok: true, data };
|
|
849
|
+
} catch (error) {
|
|
850
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
851
|
+
return { ok: false, error: message };
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Download file from Feishu
|
|
857
|
+
*/
|
|
858
|
+
export async function downloadFile(
|
|
859
|
+
account: ResolvedFeishuAccount,
|
|
860
|
+
fileKey: string,
|
|
861
|
+
): Promise<{ ok: boolean; data?: ArrayBuffer; error?: string }> {
|
|
862
|
+
try {
|
|
863
|
+
const token = await getTenantAccessToken(account);
|
|
864
|
+
const apiBase = getApiBase(account);
|
|
865
|
+
|
|
866
|
+
const response = await fetch(`${apiBase}/im/v1/files/${fileKey}`, {
|
|
867
|
+
headers: {
|
|
868
|
+
Authorization: `Bearer ${token}`,
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
if (!response.ok) {
|
|
873
|
+
return { ok: false, error: `HTTP ${response.status}` };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const data = await response.arrayBuffer();
|
|
877
|
+
return { ok: true, data };
|
|
878
|
+
} catch (error) {
|
|
879
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
880
|
+
return { ok: false, error: message };
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Add reaction (emoji) to a message - used as typing indicator
|
|
886
|
+
*/
|
|
887
|
+
export async function addReaction(
|
|
888
|
+
account: ResolvedFeishuAccount,
|
|
889
|
+
messageId: string,
|
|
890
|
+
emojiType: string,
|
|
891
|
+
): Promise<FeishuApiResponse<{ reaction_id: string }>> {
|
|
892
|
+
return feishuRequest(
|
|
893
|
+
account,
|
|
894
|
+
"POST",
|
|
895
|
+
`/im/v1/messages/${messageId}/reactions`,
|
|
896
|
+
{ reaction_type: { emoji_type: emojiType } },
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Remove reaction from a message
|
|
902
|
+
*/
|
|
903
|
+
export async function removeReaction(
|
|
904
|
+
account: ResolvedFeishuAccount,
|
|
905
|
+
messageId: string,
|
|
906
|
+
reactionId: string,
|
|
907
|
+
): Promise<FeishuApiResponse> {
|
|
908
|
+
return feishuRequest(
|
|
909
|
+
account,
|
|
910
|
+
"DELETE",
|
|
911
|
+
`/im/v1/messages/${messageId}/reactions/${reactionId}`,
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Reply to a specific message
|
|
917
|
+
*/
|
|
918
|
+
export async function replyMessage(
|
|
919
|
+
account: ResolvedFeishuAccount,
|
|
920
|
+
messageId: string,
|
|
921
|
+
text: string,
|
|
922
|
+
options: { renderMode?: FeishuRenderMode } = {},
|
|
923
|
+
): Promise<FeishuSendResult> {
|
|
924
|
+
const renderMode = options.renderMode ?? account.renderMode ?? "auto";
|
|
925
|
+
const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCardRendering(text));
|
|
926
|
+
|
|
927
|
+
const msgType = useCard ? "interactive" : "text";
|
|
928
|
+
const content = useCard
|
|
929
|
+
? JSON.stringify(createMarkdownCard(text))
|
|
930
|
+
: JSON.stringify({ text });
|
|
931
|
+
|
|
932
|
+
const body = {
|
|
933
|
+
msg_type: msgType,
|
|
934
|
+
content,
|
|
935
|
+
uuid: generateUuid(),
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const response = await feishuRequest<FeishuSendMessageResponse>(
|
|
939
|
+
account,
|
|
940
|
+
"POST",
|
|
941
|
+
`/im/v1/messages/${messageId}/reply`,
|
|
942
|
+
body,
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (response.code !== 0) {
|
|
946
|
+
return { ok: false, error: `${response.code}: ${response.msg}` };
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return { ok: true, messageId: response.data?.message_id };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Send a rich text (post) message with @ mentions that are highlighted
|
|
954
|
+
* This is the recommended way to @ someone in Feishu
|
|
955
|
+
*/
|
|
956
|
+
export async function sendPostWithMentions(
|
|
957
|
+
account: ResolvedFeishuAccount,
|
|
958
|
+
receiveId: string,
|
|
959
|
+
text: string,
|
|
960
|
+
mentions: Array<{ openId: string; name?: string }>,
|
|
961
|
+
options: FeishuSendOptions = {},
|
|
962
|
+
): Promise<FeishuSendResult> {
|
|
963
|
+
const receiveIdType = options.receiveIdType ?? "chat_id";
|
|
964
|
+
|
|
965
|
+
// Build content elements
|
|
966
|
+
const elements: Array<{ tag: string; user_id?: string; text?: string }> = [];
|
|
967
|
+
|
|
968
|
+
// Add @ mentions at the beginning
|
|
969
|
+
for (const mention of mentions) {
|
|
970
|
+
elements.push({
|
|
971
|
+
tag: "at",
|
|
972
|
+
user_id: mention.openId,
|
|
973
|
+
});
|
|
974
|
+
elements.push({
|
|
975
|
+
tag: "text",
|
|
976
|
+
text: " ",
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Add the message text
|
|
981
|
+
elements.push({
|
|
982
|
+
tag: "text",
|
|
983
|
+
text: text,
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const postContent = {
|
|
987
|
+
zh_cn: {
|
|
988
|
+
content: [elements],
|
|
989
|
+
},
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const body: FeishuSendMessageRequest = {
|
|
993
|
+
receive_id: receiveId,
|
|
994
|
+
msg_type: "post",
|
|
995
|
+
content: JSON.stringify(postContent),
|
|
996
|
+
uuid: generateUuid(),
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const response = await feishuRequest<FeishuSendMessageResponse>(
|
|
1000
|
+
account,
|
|
1001
|
+
"POST",
|
|
1002
|
+
"/im/v1/messages",
|
|
1003
|
+
body,
|
|
1004
|
+
{ receive_id_type: receiveIdType },
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
if (response.code !== 0) {
|
|
1008
|
+
return { ok: false, error: `${response.code}: ${response.msg}` };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return { ok: true, messageId: response.data?.message_id };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Send a reply that @mentions the sender
|
|
1016
|
+
* Automatically @ the person who sent the original message
|
|
1017
|
+
*/
|
|
1018
|
+
export async function sendReplyWithMention(
|
|
1019
|
+
account: ResolvedFeishuAccount,
|
|
1020
|
+
chatId: string,
|
|
1021
|
+
text: string,
|
|
1022
|
+
senderOpenId: string,
|
|
1023
|
+
options: FeishuSendOptions = {},
|
|
1024
|
+
): Promise<FeishuSendResult> {
|
|
1025
|
+
return sendPostWithMentions(
|
|
1026
|
+
account,
|
|
1027
|
+
chatId,
|
|
1028
|
+
text,
|
|
1029
|
+
[{ openId: senderOpenId }],
|
|
1030
|
+
options,
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ============ Message Retrieval ============
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Get a single message by ID
|
|
1038
|
+
* Used for fetching reply context (parent message content)
|
|
1039
|
+
*/
|
|
1040
|
+
export async function getMessage(
|
|
1041
|
+
account: ResolvedFeishuAccount,
|
|
1042
|
+
messageId: string,
|
|
1043
|
+
): Promise<{
|
|
1044
|
+
ok: boolean;
|
|
1045
|
+
message?: {
|
|
1046
|
+
messageId: string;
|
|
1047
|
+
msgType: string;
|
|
1048
|
+
content: string;
|
|
1049
|
+
senderId: string;
|
|
1050
|
+
senderType: string;
|
|
1051
|
+
chatId: string;
|
|
1052
|
+
createTime: string;
|
|
1053
|
+
body?: string;
|
|
1054
|
+
};
|
|
1055
|
+
error?: string;
|
|
1056
|
+
}> {
|
|
1057
|
+
const response = await feishuRequest<{
|
|
1058
|
+
items: Array<{
|
|
1059
|
+
message_id: string;
|
|
1060
|
+
msg_type: string;
|
|
1061
|
+
create_time: string;
|
|
1062
|
+
chat_id: string;
|
|
1063
|
+
sender: {
|
|
1064
|
+
id: string;
|
|
1065
|
+
id_type: string;
|
|
1066
|
+
sender_type: string;
|
|
1067
|
+
};
|
|
1068
|
+
body: {
|
|
1069
|
+
content: string;
|
|
1070
|
+
};
|
|
1071
|
+
}>;
|
|
1072
|
+
}>(account, "GET", `/im/v1/messages/${messageId}`);
|
|
1073
|
+
|
|
1074
|
+
if (response.code !== 0) {
|
|
1075
|
+
return {
|
|
1076
|
+
ok: false,
|
|
1077
|
+
error: `Failed to get message: ${response.code} - ${response.msg}`,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const msg = response.data?.items?.[0];
|
|
1082
|
+
if (!msg) {
|
|
1083
|
+
return {
|
|
1084
|
+
ok: false,
|
|
1085
|
+
error: "Message not found",
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Parse the content to extract text
|
|
1090
|
+
let textContent: string | undefined;
|
|
1091
|
+
try {
|
|
1092
|
+
const contentObj = JSON.parse(msg.body.content);
|
|
1093
|
+
if (msg.msg_type === "text") {
|
|
1094
|
+
textContent = contentObj.text;
|
|
1095
|
+
} else if (msg.msg_type === "post") {
|
|
1096
|
+
// Extract text from post content
|
|
1097
|
+
textContent = extractTextFromPostContent(contentObj);
|
|
1098
|
+
} else {
|
|
1099
|
+
textContent = msg.body.content;
|
|
1100
|
+
}
|
|
1101
|
+
} catch {
|
|
1102
|
+
textContent = msg.body.content;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return {
|
|
1106
|
+
ok: true,
|
|
1107
|
+
message: {
|
|
1108
|
+
messageId: msg.message_id,
|
|
1109
|
+
msgType: msg.msg_type,
|
|
1110
|
+
content: msg.body.content,
|
|
1111
|
+
senderId: msg.sender.id,
|
|
1112
|
+
senderType: msg.sender.sender_type,
|
|
1113
|
+
chatId: msg.chat_id,
|
|
1114
|
+
createTime: msg.create_time,
|
|
1115
|
+
body: textContent,
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Extract text from post content structure
|
|
1122
|
+
*/
|
|
1123
|
+
function extractTextFromPostContent(content: unknown): string {
|
|
1124
|
+
const texts: string[] = [];
|
|
1125
|
+
|
|
1126
|
+
function extractFromElements(elements: unknown[]): void {
|
|
1127
|
+
for (const element of elements) {
|
|
1128
|
+
if (typeof element !== "object" || element === null) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const el = element as { tag?: string; text?: string };
|
|
1133
|
+
|
|
1134
|
+
if (el.tag === "text" && el.text) {
|
|
1135
|
+
texts.push(el.text);
|
|
1136
|
+
} else if (el.tag === "a" && el.text) {
|
|
1137
|
+
texts.push(el.text);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (typeof content === "object" && content !== null) {
|
|
1143
|
+
const post = content as {
|
|
1144
|
+
zh_cn?: { content?: unknown[][] };
|
|
1145
|
+
en_us?: { content?: unknown[][] };
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
const postContent = post.zh_cn?.content || post.en_us?.content;
|
|
1149
|
+
|
|
1150
|
+
if (Array.isArray(postContent)) {
|
|
1151
|
+
for (const line of postContent) {
|
|
1152
|
+
if (Array.isArray(line)) {
|
|
1153
|
+
extractFromElements(line);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return texts.join(" ");
|
|
1160
|
+
}
|