@openclaw/feishu 2026.3.1 → 2026.3.7
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/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +268 -11
- package/src/accounts.ts +101 -14
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +9 -1
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +945 -77
- package/src/bot.ts +492 -165
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +72 -68
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +221 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +33 -6
- package/src/config-schema.ts +18 -10
- package/src/dedup.ts +47 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/doc-schema.ts +16 -22
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +10 -16
- package/src/docx.test.ts +41 -189
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +164 -14
- package/src/media.ts +44 -10
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +284 -25
- package/src/monitor.reaction.test.ts +395 -46
- package/src/monitor.startup.test.ts +25 -8
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +88 -9
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +13 -11
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +213 -106
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +54 -36
- package/src/probe.ts +57 -37
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +216 -0
- package/src/reply-dispatcher.ts +89 -22
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +7 -3
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.test.ts +29 -0
- package/src/targets.ts +25 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +11 -4
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
package/src/media.test.ts
CHANGED
|
@@ -10,11 +10,14 @@ const resolveReceiveIdTypeMock = vi.hoisted(() => vi.fn());
|
|
|
10
10
|
const loadWebMediaMock = vi.hoisted(() => vi.fn());
|
|
11
11
|
|
|
12
12
|
const fileCreateMock = vi.hoisted(() => vi.fn());
|
|
13
|
+
const imageCreateMock = vi.hoisted(() => vi.fn());
|
|
13
14
|
const imageGetMock = vi.hoisted(() => vi.fn());
|
|
14
15
|
const messageCreateMock = vi.hoisted(() => vi.fn());
|
|
15
16
|
const messageResourceGetMock = vi.hoisted(() => vi.fn());
|
|
16
17
|
const messageReplyMock = vi.hoisted(() => vi.fn());
|
|
17
18
|
|
|
19
|
+
const FEISHU_MEDIA_HTTP_TIMEOUT_MS = 120_000;
|
|
20
|
+
|
|
18
21
|
vi.mock("./client.js", () => ({
|
|
19
22
|
createFeishuClient: createFeishuClientMock,
|
|
20
23
|
}));
|
|
@@ -36,7 +39,12 @@ vi.mock("./runtime.js", () => ({
|
|
|
36
39
|
}),
|
|
37
40
|
}));
|
|
38
41
|
|
|
39
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
downloadImageFeishu,
|
|
44
|
+
downloadMessageResourceFeishu,
|
|
45
|
+
sanitizeFileNameForUpload,
|
|
46
|
+
sendMediaFeishu,
|
|
47
|
+
} from "./media.js";
|
|
40
48
|
|
|
41
49
|
function expectPathIsolatedToTmpRoot(pathValue: string, key: string): void {
|
|
42
50
|
expect(pathValue).not.toContain(key);
|
|
@@ -48,6 +56,14 @@ function expectPathIsolatedToTmpRoot(pathValue: string, key: string): void {
|
|
|
48
56
|
expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
function expectMediaTimeoutClientConfigured(): void {
|
|
60
|
+
expect(createFeishuClientMock).toHaveBeenCalledWith(
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
describe("sendMediaFeishu msg_type routing", () => {
|
|
52
68
|
beforeEach(() => {
|
|
53
69
|
vi.clearAllMocks();
|
|
@@ -70,6 +86,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
70
86
|
create: fileCreateMock,
|
|
71
87
|
},
|
|
72
88
|
image: {
|
|
89
|
+
create: imageCreateMock,
|
|
73
90
|
get: imageGetMock,
|
|
74
91
|
},
|
|
75
92
|
message: {
|
|
@@ -86,6 +103,10 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
86
103
|
code: 0,
|
|
87
104
|
data: { file_key: "file_key_1" },
|
|
88
105
|
});
|
|
106
|
+
imageCreateMock.mockResolvedValue({
|
|
107
|
+
code: 0,
|
|
108
|
+
data: { image_key: "image_key_1" },
|
|
109
|
+
});
|
|
89
110
|
|
|
90
111
|
messageCreateMock.mockResolvedValue({
|
|
91
112
|
code: 0,
|
|
@@ -108,7 +129,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
108
129
|
messageResourceGetMock.mockResolvedValue(Buffer.from("resource-bytes"));
|
|
109
130
|
});
|
|
110
131
|
|
|
111
|
-
it("uses msg_type=
|
|
132
|
+
it("uses msg_type=media for mp4 video", async () => {
|
|
112
133
|
await sendMediaFeishu({
|
|
113
134
|
cfg: {} as any,
|
|
114
135
|
to: "user:ou_target",
|
|
@@ -124,7 +145,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
124
145
|
|
|
125
146
|
expect(messageCreateMock).toHaveBeenCalledWith(
|
|
126
147
|
expect.objectContaining({
|
|
127
|
-
data: expect.objectContaining({ msg_type: "
|
|
148
|
+
data: expect.objectContaining({ msg_type: "media" }),
|
|
128
149
|
}),
|
|
129
150
|
);
|
|
130
151
|
});
|
|
@@ -171,7 +192,23 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
171
192
|
);
|
|
172
193
|
});
|
|
173
194
|
|
|
174
|
-
it("
|
|
195
|
+
it("configures the media client timeout for image uploads", async () => {
|
|
196
|
+
await sendMediaFeishu({
|
|
197
|
+
cfg: {} as any,
|
|
198
|
+
to: "user:ou_target",
|
|
199
|
+
mediaBuffer: Buffer.from("image"),
|
|
200
|
+
fileName: "photo.png",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expectMediaTimeoutClientConfigured();
|
|
204
|
+
expect(messageCreateMock).toHaveBeenCalledWith(
|
|
205
|
+
expect.objectContaining({
|
|
206
|
+
data: expect.objectContaining({ msg_type: "image" }),
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("uses msg_type=media when replying with mp4", async () => {
|
|
175
212
|
await sendMediaFeishu({
|
|
176
213
|
cfg: {} as any,
|
|
177
214
|
to: "user:ou_target",
|
|
@@ -183,7 +220,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
183
220
|
expect(messageReplyMock).toHaveBeenCalledWith(
|
|
184
221
|
expect.objectContaining({
|
|
185
222
|
path: { message_id: "om_parent" },
|
|
186
|
-
data: expect.objectContaining({ msg_type: "
|
|
223
|
+
data: expect.objectContaining({ msg_type: "media" }),
|
|
187
224
|
}),
|
|
188
225
|
);
|
|
189
226
|
|
|
@@ -203,7 +240,10 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
203
240
|
expect(messageReplyMock).toHaveBeenCalledWith(
|
|
204
241
|
expect.objectContaining({
|
|
205
242
|
path: { message_id: "om_parent" },
|
|
206
|
-
data: expect.objectContaining({
|
|
243
|
+
data: expect.objectContaining({
|
|
244
|
+
msg_type: "media",
|
|
245
|
+
reply_in_thread: true,
|
|
246
|
+
}),
|
|
207
247
|
}),
|
|
208
248
|
);
|
|
209
249
|
});
|
|
@@ -283,6 +323,12 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
283
323
|
imageKey,
|
|
284
324
|
});
|
|
285
325
|
|
|
326
|
+
expect(imageGetMock).toHaveBeenCalledWith(
|
|
327
|
+
expect.objectContaining({
|
|
328
|
+
path: { image_key: imageKey },
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
expectMediaTimeoutClientConfigured();
|
|
286
332
|
expect(result.buffer).toEqual(Buffer.from("image-data"));
|
|
287
333
|
expect(capturedPath).toBeDefined();
|
|
288
334
|
expectPathIsolatedToTmpRoot(capturedPath as string, imageKey);
|
|
@@ -334,6 +380,104 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
334
380
|
|
|
335
381
|
expect(messageResourceGetMock).not.toHaveBeenCalled();
|
|
336
382
|
});
|
|
383
|
+
|
|
384
|
+
it("encodes Chinese filenames for file uploads", async () => {
|
|
385
|
+
await sendMediaFeishu({
|
|
386
|
+
cfg: {} as any,
|
|
387
|
+
to: "user:ou_target",
|
|
388
|
+
mediaBuffer: Buffer.from("doc"),
|
|
389
|
+
fileName: "测试文档.pdf",
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const createCall = fileCreateMock.mock.calls[0][0];
|
|
393
|
+
expect(createCall.data.file_name).not.toBe("测试文档.pdf");
|
|
394
|
+
expect(createCall.data.file_name).toBe(encodeURIComponent("测试文档") + ".pdf");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("preserves ASCII filenames unchanged for file uploads", async () => {
|
|
398
|
+
await sendMediaFeishu({
|
|
399
|
+
cfg: {} as any,
|
|
400
|
+
to: "user:ou_target",
|
|
401
|
+
mediaBuffer: Buffer.from("doc"),
|
|
402
|
+
fileName: "report-2026.pdf",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const createCall = fileCreateMock.mock.calls[0][0];
|
|
406
|
+
expect(createCall.data.file_name).toBe("report-2026.pdf");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("encodes special characters (em-dash, full-width brackets) in filenames", async () => {
|
|
410
|
+
await sendMediaFeishu({
|
|
411
|
+
cfg: {} as any,
|
|
412
|
+
to: "user:ou_target",
|
|
413
|
+
mediaBuffer: Buffer.from("doc"),
|
|
414
|
+
fileName: "报告—详情(2026).md",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const createCall = fileCreateMock.mock.calls[0][0];
|
|
418
|
+
expect(createCall.data.file_name).toMatch(/\.md$/);
|
|
419
|
+
expect(createCall.data.file_name).not.toContain("—");
|
|
420
|
+
expect(createCall.data.file_name).not.toContain("(");
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe("sanitizeFileNameForUpload", () => {
|
|
425
|
+
it("returns ASCII filenames unchanged", () => {
|
|
426
|
+
expect(sanitizeFileNameForUpload("report.pdf")).toBe("report.pdf");
|
|
427
|
+
expect(sanitizeFileNameForUpload("my-file_v2.txt")).toBe("my-file_v2.txt");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("encodes Chinese characters in basename, preserves extension", () => {
|
|
431
|
+
const result = sanitizeFileNameForUpload("测试文件.md");
|
|
432
|
+
expect(result).toBe(encodeURIComponent("测试文件") + ".md");
|
|
433
|
+
expect(result).toMatch(/\.md$/);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("encodes em-dash and full-width brackets", () => {
|
|
437
|
+
const result = sanitizeFileNameForUpload("文件—说明(v2).pdf");
|
|
438
|
+
expect(result).toMatch(/\.pdf$/);
|
|
439
|
+
expect(result).not.toContain("—");
|
|
440
|
+
expect(result).not.toContain("(");
|
|
441
|
+
expect(result).not.toContain(")");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("encodes single quotes and parentheses per RFC 5987", () => {
|
|
445
|
+
const result = sanitizeFileNameForUpload("文件'(test).txt");
|
|
446
|
+
expect(result).toContain("%27");
|
|
447
|
+
expect(result).toContain("%28");
|
|
448
|
+
expect(result).toContain("%29");
|
|
449
|
+
expect(result).toMatch(/\.txt$/);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("handles filenames without extension", () => {
|
|
453
|
+
const result = sanitizeFileNameForUpload("测试文件");
|
|
454
|
+
expect(result).toBe(encodeURIComponent("测试文件"));
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("handles mixed ASCII and non-ASCII", () => {
|
|
458
|
+
const result = sanitizeFileNameForUpload("Report_报告_2026.xlsx");
|
|
459
|
+
expect(result).toMatch(/\.xlsx$/);
|
|
460
|
+
expect(result).not.toContain("报告");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("encodes non-ASCII extensions", () => {
|
|
464
|
+
const result = sanitizeFileNameForUpload("报告.文档");
|
|
465
|
+
expect(result).toContain("%E6%96%87%E6%A1%A3");
|
|
466
|
+
expect(result).not.toContain("文档");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("encodes emoji filenames", () => {
|
|
470
|
+
const result = sanitizeFileNameForUpload("report_😀.txt");
|
|
471
|
+
expect(result).toContain("%F0%9F%98%80");
|
|
472
|
+
expect(result).toMatch(/\.txt$/);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("encodes mixed ASCII and non-ASCII extensions", () => {
|
|
476
|
+
const result = sanitizeFileNameForUpload("notes_总结.v测试");
|
|
477
|
+
expect(result).toContain("notes_");
|
|
478
|
+
expect(result).toContain("%E6%B5%8B%E8%AF%95");
|
|
479
|
+
expect(result).not.toContain("测试");
|
|
480
|
+
});
|
|
337
481
|
});
|
|
338
482
|
|
|
339
483
|
describe("downloadMessageResourceFeishu", () => {
|
|
@@ -370,10 +514,13 @@ describe("downloadMessageResourceFeishu", () => {
|
|
|
370
514
|
type: "file",
|
|
371
515
|
});
|
|
372
516
|
|
|
373
|
-
expect(messageResourceGetMock).toHaveBeenCalledWith(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
517
|
+
expect(messageResourceGetMock).toHaveBeenCalledWith(
|
|
518
|
+
expect.objectContaining({
|
|
519
|
+
path: { message_id: "om_audio_msg", file_key: "file_key_audio" },
|
|
520
|
+
params: { type: "file" },
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
expectMediaTimeoutClientConfigured();
|
|
377
524
|
expect(result.buffer).toBeInstanceOf(Buffer);
|
|
378
525
|
});
|
|
379
526
|
|
|
@@ -387,10 +534,13 @@ describe("downloadMessageResourceFeishu", () => {
|
|
|
387
534
|
type: "image",
|
|
388
535
|
});
|
|
389
536
|
|
|
390
|
-
expect(messageResourceGetMock).toHaveBeenCalledWith(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
537
|
+
expect(messageResourceGetMock).toHaveBeenCalledWith(
|
|
538
|
+
expect.objectContaining({
|
|
539
|
+
path: { message_id: "om_img_msg", file_key: "img_key_1" },
|
|
540
|
+
params: { type: "image" },
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
expectMediaTimeoutClientConfigured();
|
|
394
544
|
expect(result.buffer).toBeInstanceOf(Buffer);
|
|
395
545
|
});
|
|
396
546
|
});
|
package/src/media.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { Readable } from "stream";
|
|
4
|
-
import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
4
|
+
import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
5
5
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
6
6
|
import { createFeishuClient } from "./client.js";
|
|
7
7
|
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
|
@@ -9,6 +9,8 @@ import { getFeishuRuntime } from "./runtime.js";
|
|
|
9
9
|
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
|
|
10
10
|
import { resolveFeishuSendTarget } from "./send-target.js";
|
|
11
11
|
|
|
12
|
+
const FEISHU_MEDIA_HTTP_TIMEOUT_MS = 120_000;
|
|
13
|
+
|
|
12
14
|
export type DownloadImageResult = {
|
|
13
15
|
buffer: Buffer;
|
|
14
16
|
contentType?: string;
|
|
@@ -97,7 +99,10 @@ export async function downloadImageFeishu(params: {
|
|
|
97
99
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
const client = createFeishuClient(
|
|
102
|
+
const client = createFeishuClient({
|
|
103
|
+
...account,
|
|
104
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
105
|
+
});
|
|
101
106
|
|
|
102
107
|
const response = await client.im.image.get({
|
|
103
108
|
path: { image_key: normalizedImageKey },
|
|
@@ -132,7 +137,10 @@ export async function downloadMessageResourceFeishu(params: {
|
|
|
132
137
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
const client = createFeishuClient(
|
|
140
|
+
const client = createFeishuClient({
|
|
141
|
+
...account,
|
|
142
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
143
|
+
});
|
|
136
144
|
|
|
137
145
|
const response = await client.im.messageResource.get({
|
|
138
146
|
path: { message_id: messageId, file_key: normalizedFileKey },
|
|
@@ -176,7 +184,10 @@ export async function uploadImageFeishu(params: {
|
|
|
176
184
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
177
185
|
}
|
|
178
186
|
|
|
179
|
-
const client = createFeishuClient(
|
|
187
|
+
const client = createFeishuClient({
|
|
188
|
+
...account,
|
|
189
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
190
|
+
});
|
|
180
191
|
|
|
181
192
|
// SDK accepts Buffer directly or fs.ReadStream for file paths
|
|
182
193
|
// Using Readable.from(buffer) causes issues with form-data library
|
|
@@ -207,6 +218,24 @@ export async function uploadImageFeishu(params: {
|
|
|
207
218
|
return { imageKey };
|
|
208
219
|
}
|
|
209
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Encode a filename for safe use in Feishu multipart/form-data uploads.
|
|
223
|
+
* Non-ASCII characters (Chinese, em-dash, full-width brackets, etc.) cause
|
|
224
|
+
* the upload to silently fail when passed raw through the SDK's form-data
|
|
225
|
+
* serialization. RFC 5987 percent-encoding keeps headers 7-bit clean while
|
|
226
|
+
* Feishu's server decodes and preserves the original display name.
|
|
227
|
+
*/
|
|
228
|
+
export function sanitizeFileNameForUpload(fileName: string): string {
|
|
229
|
+
const ASCII_ONLY = /^[\x20-\x7E]+$/;
|
|
230
|
+
if (ASCII_ONLY.test(fileName)) {
|
|
231
|
+
return fileName;
|
|
232
|
+
}
|
|
233
|
+
return encodeURIComponent(fileName)
|
|
234
|
+
.replace(/'/g, "%27")
|
|
235
|
+
.replace(/\(/g, "%28")
|
|
236
|
+
.replace(/\)/g, "%29");
|
|
237
|
+
}
|
|
238
|
+
|
|
210
239
|
/**
|
|
211
240
|
* Upload a file to Feishu and get a file_key for sending.
|
|
212
241
|
* Max file size: 30MB
|
|
@@ -225,17 +254,22 @@ export async function uploadFileFeishu(params: {
|
|
|
225
254
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
226
255
|
}
|
|
227
256
|
|
|
228
|
-
const client = createFeishuClient(
|
|
257
|
+
const client = createFeishuClient({
|
|
258
|
+
...account,
|
|
259
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
260
|
+
});
|
|
229
261
|
|
|
230
262
|
// SDK accepts Buffer directly or fs.ReadStream for file paths
|
|
231
263
|
// Using Readable.from(buffer) causes issues with form-data library
|
|
232
264
|
// See: https://github.com/larksuite/node-sdk/issues/121
|
|
233
265
|
const fileData = typeof file === "string" ? fs.createReadStream(file) : file;
|
|
234
266
|
|
|
267
|
+
const safeFileName = sanitizeFileNameForUpload(fileName);
|
|
268
|
+
|
|
235
269
|
const response = await client.im.file.create({
|
|
236
270
|
data: {
|
|
237
271
|
file_type: fileType,
|
|
238
|
-
file_name:
|
|
272
|
+
file_name: safeFileName,
|
|
239
273
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK accepts Buffer or ReadStream
|
|
240
274
|
file: fileData as any,
|
|
241
275
|
...(duration !== undefined && { duration }),
|
|
@@ -308,8 +342,8 @@ export async function sendFileFeishu(params: {
|
|
|
308
342
|
cfg: ClawdbotConfig;
|
|
309
343
|
to: string;
|
|
310
344
|
fileKey: string;
|
|
311
|
-
/** Use "audio" for audio
|
|
312
|
-
msgType?: "file" | "audio";
|
|
345
|
+
/** Use "audio" for audio, "media" for video (mp4), "file" for documents */
|
|
346
|
+
msgType?: "file" | "audio" | "media";
|
|
313
347
|
replyToMessageId?: string;
|
|
314
348
|
replyInThread?: boolean;
|
|
315
349
|
accountId?: string;
|
|
@@ -447,8 +481,8 @@ export async function sendMediaFeishu(params: {
|
|
|
447
481
|
fileType,
|
|
448
482
|
accountId,
|
|
449
483
|
});
|
|
450
|
-
// Feishu API: opus -> "audio",
|
|
451
|
-
const msgType = fileType === "opus" ? "audio" : "file";
|
|
484
|
+
// Feishu API: opus -> "audio", mp4/video -> "media" (playable), others -> "file"
|
|
485
|
+
const msgType = fileType === "opus" ? "audio" : fileType === "mp4" ? "media" : "file";
|
|
452
486
|
return sendFileFeishu({
|
|
453
487
|
cfg,
|
|
454
488
|
to,
|
package/src/mention.ts
CHANGED
|
@@ -53,7 +53,7 @@ export function isMentionForwardRequest(event: FeishuMessageEvent, botOpenId?: s
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const isDirectMessage = event.message.chat_type
|
|
56
|
+
const isDirectMessage = event.message.chat_type !== "group";
|
|
57
57
|
const hasOtherMention = mentions.some((m) => m.id.open_id !== botOpenId);
|
|
58
58
|
|
|
59
59
|
if (isDirectMessage) {
|