diff --git a/CHANGELOG.md b/CHANGELOG.md index e30d9189235..d27cc769c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ Docs: https://docs.openclaw.ai - CLI/thinking help: add the missing `xhigh` level hints to `openclaw cron add`, `openclaw cron edit`, and `openclaw agent` so the help text matches the levels already accepted at runtime. (#44819) Thanks @kiki830621. - Agents/Anthropic replay: drop replayed assistant thinking blocks for native Anthropic and Bedrock Claude providers so persisted follow-up turns no longer fail on stored thinking blocks. (#44843) Thanks @jmcte. - Docs/Brave pricing: escape literal dollar signs in Brave Search cost text so the docs render the free credit and per-request pricing correctly. (#44989) Thanks @keelanfh. +- Feishu/file uploads: preserve literal UTF-8 filenames in `im.file.create` so Chinese and other non-ASCII filenames no longer appear percent-encoded in chat. (#34262) Thanks @fabiaodemianyang and @KangShuaiFu. ## 2026.3.11 diff --git a/extensions/feishu/src/media.test.ts b/extensions/feishu/src/media.test.ts index b0226669df1..80555c294ae 100644 --- a/extensions/feishu/src/media.test.ts +++ b/extensions/feishu/src/media.test.ts @@ -384,7 +384,7 @@ describe("sendMediaFeishu msg_type routing", () => { expect(messageResourceGetMock).not.toHaveBeenCalled(); }); - it("encodes Chinese filenames for file uploads", async () => { + it("preserves Chinese filenames for file uploads", async () => { await sendMediaFeishu({ cfg: {} as any, to: "user:ou_target", @@ -393,8 +393,7 @@ describe("sendMediaFeishu msg_type routing", () => { }); const createCall = fileCreateMock.mock.calls[0][0]; - expect(createCall.data.file_name).not.toBe("测试文档.pdf"); - expect(createCall.data.file_name).toBe(encodeURIComponent("测试文档") + ".pdf"); + expect(createCall.data.file_name).toBe("测试文档.pdf"); }); it("preserves ASCII filenames unchanged for file uploads", async () => { @@ -409,7 +408,7 @@ describe("sendMediaFeishu msg_type routing", () => { expect(createCall.data.file_name).toBe("report-2026.pdf"); }); - it("encodes special characters (em-dash, full-width brackets) in filenames", async () => { + it("preserves special Unicode characters (em-dash, full-width brackets) in filenames", async () => { await sendMediaFeishu({ cfg: {} as any, to: "user:ou_target", @@ -418,9 +417,7 @@ describe("sendMediaFeishu msg_type routing", () => { }); const createCall = fileCreateMock.mock.calls[0][0]; - expect(createCall.data.file_name).toMatch(/\.md$/); - expect(createCall.data.file_name).not.toContain("—"); - expect(createCall.data.file_name).not.toContain("("); + expect(createCall.data.file_name).toBe("报告—详情(2026).md"); }); }); @@ -430,56 +427,41 @@ describe("sanitizeFileNameForUpload", () => { expect(sanitizeFileNameForUpload("my-file_v2.txt")).toBe("my-file_v2.txt"); }); - it("encodes Chinese characters in basename, preserves extension", () => { - const result = sanitizeFileNameForUpload("测试文件.md"); - expect(result).toBe(encodeURIComponent("测试文件") + ".md"); - expect(result).toMatch(/\.md$/); + it("preserves Chinese characters", () => { + expect(sanitizeFileNameForUpload("测试文件.md")).toBe("测试文件.md"); + expect(sanitizeFileNameForUpload("武汉15座山登山信息汇总.csv")).toBe( + "武汉15座山登山信息汇总.csv", + ); }); - it("encodes em-dash and full-width brackets", () => { - const result = sanitizeFileNameForUpload("文件—说明(v2).pdf"); - expect(result).toMatch(/\.pdf$/); - expect(result).not.toContain("—"); - expect(result).not.toContain("("); - expect(result).not.toContain(")"); + it("preserves em-dash and full-width brackets", () => { + expect(sanitizeFileNameForUpload("文件—说明(v2).pdf")).toBe("文件—说明(v2).pdf"); }); - it("encodes single quotes and parentheses per RFC 5987", () => { - const result = sanitizeFileNameForUpload("文件'(test).txt"); - expect(result).toContain("%27"); - expect(result).toContain("%28"); - expect(result).toContain("%29"); - expect(result).toMatch(/\.txt$/); + it("preserves single quotes and parentheses", () => { + expect(sanitizeFileNameForUpload("文件'(test).txt")).toBe("文件'(test).txt"); }); - it("handles filenames without extension", () => { - const result = sanitizeFileNameForUpload("测试文件"); - expect(result).toBe(encodeURIComponent("测试文件")); + it("preserves filenames without extension", () => { + expect(sanitizeFileNameForUpload("测试文件")).toBe("测试文件"); }); - it("handles mixed ASCII and non-ASCII", () => { - const result = sanitizeFileNameForUpload("Report_报告_2026.xlsx"); - expect(result).toMatch(/\.xlsx$/); - expect(result).not.toContain("报告"); + it("preserves mixed ASCII and non-ASCII", () => { + expect(sanitizeFileNameForUpload("Report_报告_2026.xlsx")).toBe("Report_报告_2026.xlsx"); }); - it("encodes non-ASCII extensions", () => { - const result = sanitizeFileNameForUpload("报告.文档"); - expect(result).toContain("%E6%96%87%E6%A1%A3"); - expect(result).not.toContain("文档"); + it("preserves emoji filenames", () => { + expect(sanitizeFileNameForUpload("report_😀.txt")).toBe("report_😀.txt"); }); - it("encodes emoji filenames", () => { - const result = sanitizeFileNameForUpload("report_😀.txt"); - expect(result).toContain("%F0%9F%98%80"); - expect(result).toMatch(/\.txt$/); + it("strips control characters", () => { + expect(sanitizeFileNameForUpload("bad\x00file.txt")).toBe("bad_file.txt"); + expect(sanitizeFileNameForUpload("inject\r\nheader.txt")).toBe("inject__header.txt"); }); - it("encodes mixed ASCII and non-ASCII extensions", () => { - const result = sanitizeFileNameForUpload("notes_总结.v测试"); - expect(result).toContain("notes_"); - expect(result).toContain("%E6%B5%8B%E8%AF%95"); - expect(result).not.toContain("测试"); + it("strips quotes and backslashes to prevent header injection", () => { + expect(sanitizeFileNameForUpload('file"name.txt')).toBe("file_name.txt"); + expect(sanitizeFileNameForUpload("file\\name.txt")).toBe("file_name.txt"); }); }); diff --git a/extensions/feishu/src/media.ts b/extensions/feishu/src/media.ts index 41438c570f2..45596fe45ed 100644 --- a/extensions/feishu/src/media.ts +++ b/extensions/feishu/src/media.ts @@ -226,21 +226,17 @@ export async function uploadImageFeishu(params: { } /** - * Encode a filename for safe use in Feishu multipart/form-data uploads. - * Non-ASCII characters (Chinese, em-dash, full-width brackets, etc.) cause - * the upload to silently fail when passed raw through the SDK's form-data - * serialization. RFC 5987 percent-encoding keeps headers 7-bit clean while - * Feishu's server decodes and preserves the original display name. + * Sanitize a filename for safe use in Feishu multipart/form-data uploads. + * Strips control characters and multipart-injection vectors (CWE-93) while + * preserving the original UTF-8 display name (Chinese, emoji, etc.). + * + * Previous versions percent-encoded non-ASCII characters, but the Feishu + * `im.file.create` API uses `file_name` as a literal display name — it does + * NOT decode percent-encoding — so encoded filenames appeared as garbled text + * in chat (regression in v2026.3.2). */ export function sanitizeFileNameForUpload(fileName: string): string { - const ASCII_ONLY = /^[\x20-\x7E]+$/; - if (ASCII_ONLY.test(fileName)) { - return fileName; - } - return encodeURIComponent(fileName) - .replace(/'/g, "%27") - .replace(/\(/g, "%28") - .replace(/\)/g, "%29"); + return fileName.replace(/[\x00-\x1F\x7F\r\n"\\]/g, "_"); } /**