@openclaw/feishu 2026.3.1 → 2026.3.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 +1 -1
- package/src/accounts.test.ts +74 -3
- package/src/accounts.ts +69 -10
- package/src/bot.checkBotMentioned.test.ts +1 -1
- package/src/bot.test.ts +390 -29
- package/src/bot.ts +131 -61
- package/src/channel.ts +20 -4
- package/src/client.test.ts +14 -0
- package/src/config-schema.test.ts +19 -0
- package/src/config-schema.ts +13 -9
- package/src/dedup.ts +47 -1
- package/src/doc-schema.ts +16 -22
- package/src/docx.account-selection.test.ts +7 -13
- package/src/docx.test.ts +41 -189
- package/src/media.test.ts +104 -1
- package/src/media.ts +21 -1
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +266 -18
- package/src/monitor.reaction.test.ts +345 -2
- package/src/monitor.startup.test.ts +17 -1
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +84 -8
- package/src/monitor.test-mocks.ts +12 -0
- package/src/monitor.webhook-security.test.ts +26 -9
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.ts +144 -52
- package/src/probe.test.ts +38 -20
- package/src/probe.ts +57 -37
- package/src/reply-dispatcher.test.ts +41 -0
- package/src/reply-dispatcher.ts +26 -7
- package/src/secret-input.ts +19 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +6 -2
- package/src/targets.test.ts +29 -0
- package/src/targets.ts +21 -1
- package/src/types.ts +9 -1
package/src/docx.test.ts
CHANGED
|
@@ -114,6 +114,29 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
114
114
|
scopeListMock.mockResolvedValue({ code: 0, data: { scopes: [] } });
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
function resolveFeishuDocTool(context: Record<string, unknown> = {}) {
|
|
118
|
+
const registerTool = vi.fn();
|
|
119
|
+
registerFeishuDocTools({
|
|
120
|
+
config: {
|
|
121
|
+
channels: {
|
|
122
|
+
feishu: {
|
|
123
|
+
appId: "app_id",
|
|
124
|
+
appSecret: "app_secret",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
} as any,
|
|
128
|
+
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
129
|
+
registerTool,
|
|
130
|
+
} as any);
|
|
131
|
+
|
|
132
|
+
const tool = registerTool.mock.calls
|
|
133
|
+
.map((call) => call[0])
|
|
134
|
+
.map((candidate) => (typeof candidate === "function" ? candidate(context) : candidate))
|
|
135
|
+
.find((candidate) => candidate.name === "feishu_doc");
|
|
136
|
+
expect(tool).toBeDefined();
|
|
137
|
+
return tool as { execute: (callId: string, params: Record<string, unknown>) => Promise<any> };
|
|
138
|
+
}
|
|
139
|
+
|
|
117
140
|
it("inserts blocks sequentially to preserve document order", async () => {
|
|
118
141
|
const blocks = [
|
|
119
142
|
{ block_type: 3, block_id: "h1" },
|
|
@@ -135,22 +158,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
135
158
|
data: { children: [{ block_type: 3, block_id: "h1" }] },
|
|
136
159
|
});
|
|
137
160
|
|
|
138
|
-
const
|
|
139
|
-
registerFeishuDocTools({
|
|
140
|
-
config: {
|
|
141
|
-
channels: {
|
|
142
|
-
feishu: { appId: "app_id", appSecret: "app_secret" },
|
|
143
|
-
},
|
|
144
|
-
} as any,
|
|
145
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
146
|
-
registerTool,
|
|
147
|
-
} as any);
|
|
148
|
-
|
|
149
|
-
const feishuDocTool = registerTool.mock.calls
|
|
150
|
-
.map((call) => call[0])
|
|
151
|
-
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
152
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
153
|
-
expect(feishuDocTool).toBeDefined();
|
|
161
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
154
162
|
|
|
155
163
|
const result = await feishuDocTool.execute("tool-call", {
|
|
156
164
|
action: "append",
|
|
@@ -194,22 +202,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
194
202
|
},
|
|
195
203
|
}));
|
|
196
204
|
|
|
197
|
-
const
|
|
198
|
-
registerFeishuDocTools({
|
|
199
|
-
config: {
|
|
200
|
-
channels: {
|
|
201
|
-
feishu: { appId: "app_id", appSecret: "app_secret" },
|
|
202
|
-
},
|
|
203
|
-
} as any,
|
|
204
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
205
|
-
registerTool,
|
|
206
|
-
} as any);
|
|
207
|
-
|
|
208
|
-
const feishuDocTool = registerTool.mock.calls
|
|
209
|
-
.map((call) => call[0])
|
|
210
|
-
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
211
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
212
|
-
expect(feishuDocTool).toBeDefined();
|
|
205
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
213
206
|
|
|
214
207
|
const longMarkdown = Array.from(
|
|
215
208
|
{ length: 120 },
|
|
@@ -254,22 +247,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
254
247
|
data: { children: data.children },
|
|
255
248
|
}));
|
|
256
249
|
|
|
257
|
-
const
|
|
258
|
-
registerFeishuDocTools({
|
|
259
|
-
config: {
|
|
260
|
-
channels: {
|
|
261
|
-
feishu: { appId: "app_id", appSecret: "app_secret" },
|
|
262
|
-
},
|
|
263
|
-
} as any,
|
|
264
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
265
|
-
registerTool,
|
|
266
|
-
} as any);
|
|
267
|
-
|
|
268
|
-
const feishuDocTool = registerTool.mock.calls
|
|
269
|
-
.map((call) => call[0])
|
|
270
|
-
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
271
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
272
|
-
expect(feishuDocTool).toBeDefined();
|
|
250
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
273
251
|
|
|
274
252
|
const fencedMarkdown = [
|
|
275
253
|
"## Section",
|
|
@@ -306,25 +284,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
306
284
|
new Error("Blocked: resolves to private/internal IP address"),
|
|
307
285
|
);
|
|
308
286
|
|
|
309
|
-
const
|
|
310
|
-
registerFeishuDocTools({
|
|
311
|
-
config: {
|
|
312
|
-
channels: {
|
|
313
|
-
feishu: {
|
|
314
|
-
appId: "app_id",
|
|
315
|
-
appSecret: "app_secret",
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
} as any,
|
|
319
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
320
|
-
registerTool,
|
|
321
|
-
} as any);
|
|
322
|
-
|
|
323
|
-
const feishuDocTool = registerTool.mock.calls
|
|
324
|
-
.map((call) => call[0])
|
|
325
|
-
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
326
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
327
|
-
expect(feishuDocTool).toBeDefined();
|
|
287
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
328
288
|
|
|
329
289
|
const result = await feishuDocTool.execute("tool-call", {
|
|
330
290
|
action: "write",
|
|
@@ -341,29 +301,10 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
341
301
|
});
|
|
342
302
|
|
|
343
303
|
it("create grants permission only to trusted Feishu requester", async () => {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
feishu: {
|
|
349
|
-
appId: "app_id",
|
|
350
|
-
appSecret: "app_secret",
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
} as any,
|
|
354
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
355
|
-
registerTool,
|
|
356
|
-
} as any);
|
|
357
|
-
|
|
358
|
-
const feishuDocTool = registerTool.mock.calls
|
|
359
|
-
.map((call) => call[0])
|
|
360
|
-
.map((tool) =>
|
|
361
|
-
typeof tool === "function"
|
|
362
|
-
? tool({ messageChannel: "feishu", requesterSenderId: "ou_123" })
|
|
363
|
-
: tool,
|
|
364
|
-
)
|
|
365
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
366
|
-
expect(feishuDocTool).toBeDefined();
|
|
304
|
+
const feishuDocTool = resolveFeishuDocTool({
|
|
305
|
+
messageChannel: "feishu",
|
|
306
|
+
requesterSenderId: "ou_123",
|
|
307
|
+
});
|
|
367
308
|
|
|
368
309
|
const result = await feishuDocTool.execute("tool-call", {
|
|
369
310
|
action: "create",
|
|
@@ -386,25 +327,9 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
386
327
|
});
|
|
387
328
|
|
|
388
329
|
it("create skips requester grant when trusted requester identity is unavailable", async () => {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
channels: {
|
|
393
|
-
feishu: {
|
|
394
|
-
appId: "app_id",
|
|
395
|
-
appSecret: "app_secret",
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
} as any,
|
|
399
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
400
|
-
registerTool,
|
|
401
|
-
} as any);
|
|
402
|
-
|
|
403
|
-
const feishuDocTool = registerTool.mock.calls
|
|
404
|
-
.map((call) => call[0])
|
|
405
|
-
.map((tool) => (typeof tool === "function" ? tool({ messageChannel: "feishu" }) : tool))
|
|
406
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
407
|
-
expect(feishuDocTool).toBeDefined();
|
|
330
|
+
const feishuDocTool = resolveFeishuDocTool({
|
|
331
|
+
messageChannel: "feishu",
|
|
332
|
+
});
|
|
408
333
|
|
|
409
334
|
const result = await feishuDocTool.execute("tool-call", {
|
|
410
335
|
action: "create",
|
|
@@ -417,29 +342,10 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
417
342
|
});
|
|
418
343
|
|
|
419
344
|
it("create never grants permissions when grant_to_requester is false", async () => {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
feishu: {
|
|
425
|
-
appId: "app_id",
|
|
426
|
-
appSecret: "app_secret",
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
} as any,
|
|
430
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
431
|
-
registerTool,
|
|
432
|
-
} as any);
|
|
433
|
-
|
|
434
|
-
const feishuDocTool = registerTool.mock.calls
|
|
435
|
-
.map((call) => call[0])
|
|
436
|
-
.map((tool) =>
|
|
437
|
-
typeof tool === "function"
|
|
438
|
-
? tool({ messageChannel: "feishu", requesterSenderId: "ou_123" })
|
|
439
|
-
: tool,
|
|
440
|
-
)
|
|
441
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
442
|
-
expect(feishuDocTool).toBeDefined();
|
|
345
|
+
const feishuDocTool = resolveFeishuDocTool({
|
|
346
|
+
messageChannel: "feishu",
|
|
347
|
+
requesterSenderId: "ou_123",
|
|
348
|
+
});
|
|
443
349
|
|
|
444
350
|
const result = await feishuDocTool.execute("tool-call", {
|
|
445
351
|
action: "create",
|
|
@@ -457,25 +363,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
457
363
|
data: { document: { title: "Created Doc" } },
|
|
458
364
|
});
|
|
459
365
|
|
|
460
|
-
const
|
|
461
|
-
registerFeishuDocTools({
|
|
462
|
-
config: {
|
|
463
|
-
channels: {
|
|
464
|
-
feishu: {
|
|
465
|
-
appId: "app_id",
|
|
466
|
-
appSecret: "app_secret",
|
|
467
|
-
},
|
|
468
|
-
},
|
|
469
|
-
} as any,
|
|
470
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
471
|
-
registerTool,
|
|
472
|
-
} as any);
|
|
473
|
-
|
|
474
|
-
const feishuDocTool = registerTool.mock.calls
|
|
475
|
-
.map((call) => call[0])
|
|
476
|
-
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
477
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
478
|
-
expect(feishuDocTool).toBeDefined();
|
|
366
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
479
367
|
|
|
480
368
|
const result = await feishuDocTool.execute("tool-call", {
|
|
481
369
|
action: "create",
|
|
@@ -496,25 +384,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
496
384
|
const localPath = join(tmpdir(), `feishu-docx-upload-${Date.now()}.txt`);
|
|
497
385
|
await fs.writeFile(localPath, "hello from local file", "utf8");
|
|
498
386
|
|
|
499
|
-
const
|
|
500
|
-
registerFeishuDocTools({
|
|
501
|
-
config: {
|
|
502
|
-
channels: {
|
|
503
|
-
feishu: {
|
|
504
|
-
appId: "app_id",
|
|
505
|
-
appSecret: "app_secret",
|
|
506
|
-
},
|
|
507
|
-
},
|
|
508
|
-
} as any,
|
|
509
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
510
|
-
registerTool,
|
|
511
|
-
} as any);
|
|
512
|
-
|
|
513
|
-
const feishuDocTool = registerTool.mock.calls
|
|
514
|
-
.map((call) => call[0])
|
|
515
|
-
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
516
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
517
|
-
expect(feishuDocTool).toBeDefined();
|
|
387
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
518
388
|
|
|
519
389
|
const result = await feishuDocTool.execute("tool-call", {
|
|
520
390
|
action: "upload_file",
|
|
@@ -557,25 +427,7 @@ describe("feishu_doc image fetch hardening", () => {
|
|
|
557
427
|
await fs.writeFile(localPath, "hello from local file", "utf8");
|
|
558
428
|
|
|
559
429
|
try {
|
|
560
|
-
const
|
|
561
|
-
registerFeishuDocTools({
|
|
562
|
-
config: {
|
|
563
|
-
channels: {
|
|
564
|
-
feishu: {
|
|
565
|
-
appId: "app_id",
|
|
566
|
-
appSecret: "app_secret",
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
} as any,
|
|
570
|
-
logger: { debug: vi.fn(), info: vi.fn() } as any,
|
|
571
|
-
registerTool,
|
|
572
|
-
} as any);
|
|
573
|
-
|
|
574
|
-
const feishuDocTool = registerTool.mock.calls
|
|
575
|
-
.map((call) => call[0])
|
|
576
|
-
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
|
|
577
|
-
.find((tool) => tool.name === "feishu_doc");
|
|
578
|
-
expect(feishuDocTool).toBeDefined();
|
|
430
|
+
const feishuDocTool = resolveFeishuDocTool();
|
|
579
431
|
|
|
580
432
|
const result = await feishuDocTool.execute("tool-call", {
|
|
581
433
|
action: "upload_file",
|
package/src/media.test.ts
CHANGED
|
@@ -36,7 +36,12 @@ vi.mock("./runtime.js", () => ({
|
|
|
36
36
|
}),
|
|
37
37
|
}));
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
downloadImageFeishu,
|
|
41
|
+
downloadMessageResourceFeishu,
|
|
42
|
+
sanitizeFileNameForUpload,
|
|
43
|
+
sendMediaFeishu,
|
|
44
|
+
} from "./media.js";
|
|
40
45
|
|
|
41
46
|
function expectPathIsolatedToTmpRoot(pathValue: string, key: string): void {
|
|
42
47
|
expect(pathValue).not.toContain(key);
|
|
@@ -334,6 +339,104 @@ describe("sendMediaFeishu msg_type routing", () => {
|
|
|
334
339
|
|
|
335
340
|
expect(messageResourceGetMock).not.toHaveBeenCalled();
|
|
336
341
|
});
|
|
342
|
+
|
|
343
|
+
it("encodes Chinese filenames for file uploads", async () => {
|
|
344
|
+
await sendMediaFeishu({
|
|
345
|
+
cfg: {} as any,
|
|
346
|
+
to: "user:ou_target",
|
|
347
|
+
mediaBuffer: Buffer.from("doc"),
|
|
348
|
+
fileName: "测试文档.pdf",
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const createCall = fileCreateMock.mock.calls[0][0];
|
|
352
|
+
expect(createCall.data.file_name).not.toBe("测试文档.pdf");
|
|
353
|
+
expect(createCall.data.file_name).toBe(encodeURIComponent("测试文档") + ".pdf");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("preserves ASCII filenames unchanged for file uploads", async () => {
|
|
357
|
+
await sendMediaFeishu({
|
|
358
|
+
cfg: {} as any,
|
|
359
|
+
to: "user:ou_target",
|
|
360
|
+
mediaBuffer: Buffer.from("doc"),
|
|
361
|
+
fileName: "report-2026.pdf",
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const createCall = fileCreateMock.mock.calls[0][0];
|
|
365
|
+
expect(createCall.data.file_name).toBe("report-2026.pdf");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("encodes special characters (em-dash, full-width brackets) in filenames", async () => {
|
|
369
|
+
await sendMediaFeishu({
|
|
370
|
+
cfg: {} as any,
|
|
371
|
+
to: "user:ou_target",
|
|
372
|
+
mediaBuffer: Buffer.from("doc"),
|
|
373
|
+
fileName: "报告—详情(2026).md",
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const createCall = fileCreateMock.mock.calls[0][0];
|
|
377
|
+
expect(createCall.data.file_name).toMatch(/\.md$/);
|
|
378
|
+
expect(createCall.data.file_name).not.toContain("—");
|
|
379
|
+
expect(createCall.data.file_name).not.toContain("(");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("sanitizeFileNameForUpload", () => {
|
|
384
|
+
it("returns ASCII filenames unchanged", () => {
|
|
385
|
+
expect(sanitizeFileNameForUpload("report.pdf")).toBe("report.pdf");
|
|
386
|
+
expect(sanitizeFileNameForUpload("my-file_v2.txt")).toBe("my-file_v2.txt");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("encodes Chinese characters in basename, preserves extension", () => {
|
|
390
|
+
const result = sanitizeFileNameForUpload("测试文件.md");
|
|
391
|
+
expect(result).toBe(encodeURIComponent("测试文件") + ".md");
|
|
392
|
+
expect(result).toMatch(/\.md$/);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("encodes em-dash and full-width brackets", () => {
|
|
396
|
+
const result = sanitizeFileNameForUpload("文件—说明(v2).pdf");
|
|
397
|
+
expect(result).toMatch(/\.pdf$/);
|
|
398
|
+
expect(result).not.toContain("—");
|
|
399
|
+
expect(result).not.toContain("(");
|
|
400
|
+
expect(result).not.toContain(")");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("encodes single quotes and parentheses per RFC 5987", () => {
|
|
404
|
+
const result = sanitizeFileNameForUpload("文件'(test).txt");
|
|
405
|
+
expect(result).toContain("%27");
|
|
406
|
+
expect(result).toContain("%28");
|
|
407
|
+
expect(result).toContain("%29");
|
|
408
|
+
expect(result).toMatch(/\.txt$/);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("handles filenames without extension", () => {
|
|
412
|
+
const result = sanitizeFileNameForUpload("测试文件");
|
|
413
|
+
expect(result).toBe(encodeURIComponent("测试文件"));
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("handles mixed ASCII and non-ASCII", () => {
|
|
417
|
+
const result = sanitizeFileNameForUpload("Report_报告_2026.xlsx");
|
|
418
|
+
expect(result).toMatch(/\.xlsx$/);
|
|
419
|
+
expect(result).not.toContain("报告");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("encodes non-ASCII extensions", () => {
|
|
423
|
+
const result = sanitizeFileNameForUpload("报告.文档");
|
|
424
|
+
expect(result).toContain("%E6%96%87%E6%A1%A3");
|
|
425
|
+
expect(result).not.toContain("文档");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("encodes emoji filenames", () => {
|
|
429
|
+
const result = sanitizeFileNameForUpload("report_😀.txt");
|
|
430
|
+
expect(result).toContain("%F0%9F%98%80");
|
|
431
|
+
expect(result).toMatch(/\.txt$/);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("encodes mixed ASCII and non-ASCII extensions", () => {
|
|
435
|
+
const result = sanitizeFileNameForUpload("notes_总结.v测试");
|
|
436
|
+
expect(result).toContain("notes_");
|
|
437
|
+
expect(result).toContain("%E6%B5%8B%E8%AF%95");
|
|
438
|
+
expect(result).not.toContain("测试");
|
|
439
|
+
});
|
|
337
440
|
});
|
|
338
441
|
|
|
339
442
|
describe("downloadMessageResourceFeishu", () => {
|
package/src/media.ts
CHANGED
|
@@ -207,6 +207,24 @@ export async function uploadImageFeishu(params: {
|
|
|
207
207
|
return { imageKey };
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Encode a filename for safe use in Feishu multipart/form-data uploads.
|
|
212
|
+
* Non-ASCII characters (Chinese, em-dash, full-width brackets, etc.) cause
|
|
213
|
+
* the upload to silently fail when passed raw through the SDK's form-data
|
|
214
|
+
* serialization. RFC 5987 percent-encoding keeps headers 7-bit clean while
|
|
215
|
+
* Feishu's server decodes and preserves the original display name.
|
|
216
|
+
*/
|
|
217
|
+
export function sanitizeFileNameForUpload(fileName: string): string {
|
|
218
|
+
const ASCII_ONLY = /^[\x20-\x7E]+$/;
|
|
219
|
+
if (ASCII_ONLY.test(fileName)) {
|
|
220
|
+
return fileName;
|
|
221
|
+
}
|
|
222
|
+
return encodeURIComponent(fileName)
|
|
223
|
+
.replace(/'/g, "%27")
|
|
224
|
+
.replace(/\(/g, "%28")
|
|
225
|
+
.replace(/\)/g, "%29");
|
|
226
|
+
}
|
|
227
|
+
|
|
210
228
|
/**
|
|
211
229
|
* Upload a file to Feishu and get a file_key for sending.
|
|
212
230
|
* Max file size: 30MB
|
|
@@ -232,10 +250,12 @@ export async function uploadFileFeishu(params: {
|
|
|
232
250
|
// See: https://github.com/larksuite/node-sdk/issues/121
|
|
233
251
|
const fileData = typeof file === "string" ? fs.createReadStream(file) : file;
|
|
234
252
|
|
|
253
|
+
const safeFileName = sanitizeFileNameForUpload(fileName);
|
|
254
|
+
|
|
235
255
|
const response = await client.im.file.create({
|
|
236
256
|
data: {
|
|
237
257
|
file_type: fileType,
|
|
238
|
-
file_name:
|
|
258
|
+
file_name: safeFileName,
|
|
239
259
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK accepts Buffer or ReadStream
|
|
240
260
|
file: fileData as any,
|
|
241
261
|
...(duration !== undefined && { duration }),
|
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) {
|