From 22d686d13ee4a31701eb18c2dc96aede311452f0 Mon Sep 17 00:00:00 2001 From: areslp Date: Thu, 2 Jul 2026 17:28:21 +0800 Subject: [PATCH] fix(feishu): include video upload duration (#98235) --- extensions/feishu/src/media.test.ts | 17 +++++++++++++ extensions/feishu/src/media.ts | 38 +++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/extensions/feishu/src/media.test.ts b/extensions/feishu/src/media.test.ts index 552369c01ed0..ee407425bc13 100644 --- a/extensions/feishu/src/media.test.ts +++ b/extensions/feishu/src/media.test.ts @@ -214,6 +214,8 @@ describe("sendMediaFeishu msg_type routing", () => { }); it("uses msg_type=media for mp4 video", async () => { + runFfprobeMock.mockResolvedValueOnce("4.2\n"); + await sendMediaFeishu({ cfg: emptyConfig, to: "user:ou_target", @@ -222,6 +224,17 @@ describe("sendMediaFeishu msg_type routing", () => { }); expect(callData<{ file_type?: string }>(fileCreateMock).file_type).toBe("mp4"); + expect(callData<{ duration?: number }>(fileCreateMock).duration).toBe(4200); + const ffprobeArgs = mockCallArg(runFfprobeMock, 0, 0); + expect(ffprobeArgs.slice(0, -1)).toEqual([ + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "csv=p=0", + ]); + expect(ffprobeArgs.at(-1)).toMatch(/input\.mp4$/); expect(callData<{ msg_type?: string }>(messageCreateMock).msg_type).toBe("media"); }); @@ -297,6 +310,7 @@ describe("sendMediaFeishu msg_type routing", () => { }); it("uses msg_type=media for remote mp4 content even when the filename is generic", async () => { + runFfprobeMock.mockResolvedValueOnce("6.789\n"); loadWebMediaMock.mockResolvedValueOnce({ buffer: Buffer.from("remote-video"), fileName: "download", @@ -311,6 +325,9 @@ describe("sendMediaFeishu msg_type routing", () => { }); expect(callData<{ file_type?: string }>(fileCreateMock).file_type).toBe("mp4"); + expect(callData<{ duration?: number }>(fileCreateMock).duration).toBe(6789); + const ffprobeArgs = mockCallArg(runFfprobeMock, 0, 0); + expect(ffprobeArgs.at(-1)).toMatch(/input\.mp4$/); expect(callData<{ msg_type?: string }>(messageCreateMock).msg_type).toBe("media"); }); diff --git a/extensions/feishu/src/media.ts b/extensions/feishu/src/media.ts index 8beb03e5c2bd..ac811f00f48b 100644 --- a/extensions/feishu/src/media.ts +++ b/extensions/feishu/src/media.ts @@ -822,12 +822,23 @@ async function prepareFeishuVoiceMedia(params: { } } -async function probeAudioDurationMs(buffer: Buffer): Promise { +async function probeMediaDurationMs(params: { + buffer: Buffer; + fileName: string; + contentType?: string; +}): Promise { try { return await withTempWorkspace( - { rootDir: resolvePreferredOpenClawTmpDir(), prefix: "feishu-audio-probe-" }, + { rootDir: resolvePreferredOpenClawTmpDir(), prefix: "feishu-media-probe-" }, async (workspace) => { - const inputPath = await workspace.write("input.ogg", buffer); + const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName)); + const inferredExt = + ext && ext.length <= 12 + ? ext + : mediaKindFromMime(params.contentType) === "video" + ? ".mp4" + : ".ogg"; + const inputPath = await workspace.write(`input${inferredExt}`, params.buffer); const stdout = await runFfprobe( ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", inputPath], { timeoutMs: 5_000 }, @@ -840,11 +851,23 @@ async function probeAudioDurationMs(buffer: Buffer): Promise }, ); } catch (err) { - console.warn("[feishu] failed to probe audio duration; voice bubble will omit it:", err); + console.warn("[feishu] failed to probe media duration; upload will omit it:", err); return undefined; } } +async function maybeProbeUploadDurationMs(params: { + buffer: Buffer; + fileName: string; + contentType?: string; + msgType: "file" | "audio" | "media"; +}): Promise { + if (params.msgType !== "audio" && params.msgType !== "media") { + return undefined; + } + return await probeMediaDurationMs(params); +} + /** * Upload and send media (image or file) from URL, local path, or buffer. * When mediaUrl is a local path, mediaLocalRoots (from core outbound context) @@ -930,7 +953,12 @@ export async function sendMediaFeishu(params: { ...(voiceIntentDegradedToFile ? { voiceIntentDegradedToFile: true } : {}), }; } - const durationMs = routing.msgType === "audio" ? await probeAudioDurationMs(buffer) : undefined; + const durationMs = await maybeProbeUploadDurationMs({ + buffer, + fileName: name, + contentType, + msgType: routing.msgType, + }); const { fileKey } = await uploadFileFeishu({ cfg, file: buffer,