






















@@ -9,6 +9,7 @@ const resolveFeishuAccountMock = vi.hoisted(() => vi.fn());
99const normalizeFeishuTargetMock = vi.hoisted(() => vi.fn());
1010const resolveReceiveIdTypeMock = vi.hoisted(() => vi.fn());
1111const loadWebMediaMock = vi.hoisted(() => vi.fn());
12+const runFfmpegMock = vi.hoisted(() => vi.fn());
12131314const fileCreateMock = vi.hoisted(() => vi.fn());
1415const imageCreateMock = vi.hoisted(() => vi.fn());
@@ -42,6 +43,14 @@ vi.mock("./runtime.js", () => ({
4243}),
4344}));
444546+vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => {
47+const actual = await importOriginal<typeof import("openclaw/plugin-sdk/media-runtime")>();
48+return {
49+ ...actual,
50+runFfmpeg: runFfmpegMock,
51+};
52+});
53+4554vi.mock("../../../src/channels/plugins/bundled.js", () => ({
4655bundledChannelPlugins: [],
4756bundledChannelSetupPlugins: [],
@@ -145,6 +154,10 @@ describe("sendMediaFeishu msg_type routing", () => {
145154146155imageGetMock.mockResolvedValue(Buffer.from("image-bytes"));
147156messageResourceGetMock.mockResolvedValue(Buffer.from("resource-bytes"));
157+runFfmpegMock.mockImplementation(async (args: string[]) => {
158+await fs.writeFile(args.at(-1) ?? "", Buffer.from("opus-output"));
159+return "";
160+});
148161});
149162150163it("uses msg_type=media for mp4 video", async () => {
@@ -260,6 +273,104 @@ describe("sendMediaFeishu msg_type routing", () => {
260273data: expect.objectContaining({ msg_type: "file" }),
261274}),
262275);
276+expect(runFfmpegMock).not.toHaveBeenCalled();
277+});
278+279+it("transcodes voice-intent mp3 to msg_type=audio", async () => {
280+loadWebMediaMock.mockResolvedValueOnce({
281+buffer: Buffer.from("remote-mp3"),
282+fileName: "reply.mp3",
283+kind: "audio",
284+contentType: "audio/mpeg",
285+});
286+287+await sendMediaFeishu({
288+cfg: emptyConfig,
289+to: "user:ou_target",
290+mediaUrl: "https://example.com/reply.mp3",
291+audioAsVoice: true,
292+});
293+294+expect(runFfmpegMock).toHaveBeenCalledWith(
295+expect.arrayContaining(["-c:a", "libopus", "-ar", "48000", "-b:a", "64k"]),
296+);
297+expect(fileCreateMock).toHaveBeenCalledWith(
298+expect.objectContaining({
299+data: expect.objectContaining({
300+file_type: "opus",
301+file_name: "voice.ogg",
302+file: Buffer.from("opus-output"),
303+}),
304+}),
305+);
306+expect(messageCreateMock).toHaveBeenCalledWith(
307+expect.objectContaining({
308+data: expect.objectContaining({ msg_type: "audio" }),
309+}),
310+);
311+});
312+313+it("leaves native voice audio unchanged when audioAsVoice is true", async () => {
314+await sendMediaFeishu({
315+cfg: emptyConfig,
316+to: "user:ou_target",
317+mediaBuffer: Buffer.from("opus"),
318+fileName: "reply.ogg",
319+audioAsVoice: true,
320+});
321+322+expect(runFfmpegMock).not.toHaveBeenCalled();
323+expect(fileCreateMock).toHaveBeenCalledWith(
324+expect.objectContaining({
325+data: expect.objectContaining({
326+file_type: "opus",
327+file_name: "reply.ogg",
328+}),
329+}),
330+);
331+expect(messageCreateMock).toHaveBeenCalledWith(
332+expect.objectContaining({
333+data: expect.objectContaining({ msg_type: "audio" }),
334+}),
335+);
336+});
337+338+it("falls back to file when voice-intent audio cannot be transcoded", async () => {
339+const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
340+runFfmpegMock.mockRejectedValueOnce(new Error("ffmpeg missing"));
341+loadWebMediaMock.mockResolvedValueOnce({
342+buffer: Buffer.from("remote-mp3"),
343+fileName: "reply.mp3",
344+kind: "audio",
345+contentType: "audio/mpeg",
346+});
347+348+await sendMediaFeishu({
349+cfg: emptyConfig,
350+to: "user:ou_target",
351+mediaUrl: "https://example.com/reply.mp3",
352+audioAsVoice: true,
353+});
354+355+expect(fileCreateMock).toHaveBeenCalledWith(
356+expect.objectContaining({
357+data: expect.objectContaining({
358+file_type: "stream",
359+file_name: "reply.mp3",
360+file: Buffer.from("remote-mp3"),
361+}),
362+}),
363+);
364+expect(messageCreateMock).toHaveBeenCalledWith(
365+expect.objectContaining({
366+data: expect.objectContaining({ msg_type: "file" }),
367+}),
368+);
369+expect(warnSpy).toHaveBeenCalledWith(
370+expect.stringContaining("audioAsVoice transcode failed"),
371+expect.any(Error),
372+);
373+warnSpy.mockRestore();
263374});
264375265376it("configures the media client timeout for image uploads", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。