fix(feishu): include video upload duration (#98235)

This commit is contained in:
areslp
2026-07-02 17:28:21 +08:00
committed by GitHub
parent 9d3e263365
commit 22d686d13e
2 changed files with 50 additions and 5 deletions
+17
View File
@@ -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<string[]>(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<string[]>(runFfprobeMock, 0, 0);
expect(ffprobeArgs.at(-1)).toMatch(/input\.mp4$/);
expect(callData<{ msg_type?: string }>(messageCreateMock).msg_type).toBe("media");
});
+33 -5
View File
@@ -822,12 +822,23 @@ async function prepareFeishuVoiceMedia(params: {
}
}
async function probeAudioDurationMs(buffer: Buffer): Promise<number | undefined> {
async function probeMediaDurationMs(params: {
buffer: Buffer;
fileName: string;
contentType?: string;
}): Promise<number | undefined> {
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<number | undefined>
},
);
} 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<number | undefined> {
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,