fix(feishu): recover mojibake filenames from Content-Disposition (#72388)

This commit is contained in:
Vincent Koc
2026-04-27 22:23:16 -07:00
committed by GitHub
parent d7e67b455a
commit 4c72e605cd
3 changed files with 71 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Memory/Dreaming: retry Dream Diary once with the session default when a configured dreaming model is unavailable, while leaving subagent trust and allowlist errors visible instead of silently masking configuration problems. Refs #67409 and #69209. Thanks @Ghiggins18 and @everySympathy.
- Feishu/inbound files: recover CJK filenames from plain `Content-Disposition: filename=` download headers when Feishu exposes UTF-8 bytes through Latin-1 header decoding, while leaving valid Latin-1 and JSON-derived names unchanged. (#48578, #50435, #59431) Thanks @alex-xuweilong, @lishuaigit, and @DoChaoing.
## 2026.4.27

View File

@@ -716,4 +716,60 @@ describe("downloadMessageResourceFeishu", () => {
fileName: "clip.mp4",
});
});
it("recovers CJK filenames from plain Content-Disposition headers decoded as Latin-1", async () => {
const fileName = "武汉15座山登山信息汇总.csv";
const latin1HeaderFileName = Buffer.from(fileName, "utf8").toString("latin1");
messageResourceGetMock.mockResolvedValueOnce({
data: Buffer.from("fake-file-data"),
headers: {
"content-disposition": `attachment; filename="${latin1HeaderFileName}"`,
},
});
const result = await downloadMessageResourceFeishu({
cfg: emptyConfig,
messageId: "om_file_msg",
fileKey: "file_key_csv",
type: "file",
});
expect(result.fileName).toBe(fileName);
});
it("keeps valid Latin-1 filenames from plain Content-Disposition headers unchanged", async () => {
messageResourceGetMock.mockResolvedValueOnce({
data: Buffer.from("fake-file-data"),
headers: {
"content-disposition": `attachment; filename="café-©.txt"`,
},
});
const result = await downloadMessageResourceFeishu({
cfg: emptyConfig,
messageId: "om_latin1_msg",
fileKey: "file_key_latin1",
type: "file",
});
expect(result.fileName).toBe("café-©.txt");
});
it("keeps JSON-derived file_name metadata unchanged", async () => {
const fileName = "武汉15座山登山信息汇总.csv";
const latin1LookingFileName = Buffer.from(fileName, "utf8").toString("latin1");
messageResourceGetMock.mockResolvedValueOnce({
data: Buffer.from("fake-file-data"),
file_name: latin1LookingFileName,
});
const result = await downloadMessageResourceFeishu({
cfg: emptyConfig,
messageId: "om_json_file_msg",
fileKey: "file_key_json",
type: "file",
});
expect(result.fileName).toBe(latin1LookingFileName);
});
});

View File

@@ -146,6 +146,18 @@ function readHeaderValue(
return undefined;
}
function containsEastAsianScript(value: string): boolean {
return /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(value);
}
function recoverUtf8FileNameFromLatin1Header(value: string): string {
const recovered = Buffer.from(value, "latin1").toString("utf8");
if (recovered !== value && !recovered.includes("\uFFFD") && containsEastAsianScript(recovered)) {
return recovered;
}
return value;
}
function decodeDispositionFileName(value: string): string | undefined {
const utf8Match = value.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match?.[1]) {
@@ -157,7 +169,8 @@ function decodeDispositionFileName(value: string): string | undefined {
}
const plainMatch = value.match(/filename="?([^";]+)"?/i);
return plainMatch?.[1]?.trim();
const plainFileName = plainMatch?.[1]?.trim();
return plainFileName ? recoverUtf8FileNameFromLatin1Header(plainFileName) : undefined;
}
function extractFeishuDownloadMetadata(response: FeishuDownloadResponse): {