mirror of https://github.com/openclaw/openclaw.git
* fix(feishu): preserve non-ASCII filenames in file uploads (#33912) * style(feishu): format media test file * fix(feishu): preserve UTF-8 filenames in file uploads (openclaw#34262) thanks @fabiaodemianyang --------- Co-authored-by: Robin Waslander <r.waslander@gmail.com>
This commit is contained in:
parent
2083b0581d
commit
983fecc106
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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, "_");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue