


























@@ -12,9 +12,14 @@ const sendMarkdownCardFeishuMock = vi.hoisted(() => vi.fn());
1212const sendStructuredCardFeishuMock = vi.hoisted(() => vi.fn());
1313const deliverCommentThreadTextMock = vi.hoisted(() => vi.fn());
1414const cleanupAmbientCommentTypingReactionMock = vi.hoisted(() => vi.fn(async () => false));
15+const shouldSuppressFeishuTextForVoiceMediaMock = vi.hoisted(
16+() => (params: { mediaUrl?: string; audioAsVoice?: boolean }) =>
17+params.audioAsVoice === true || /\.(?:ogg|opus)(?:[?#]|$)/i.test(params.mediaUrl ?? ""),
18+);
15191620vi.mock("./media.js", () => ({
1721sendMediaFeishu: sendMediaFeishuMock,
22+shouldSuppressFeishuTextForVoiceMedia: shouldSuppressFeishuTextForVoiceMediaMock,
1823}));
19242025vi.mock("./send.js", () => ({
@@ -406,13 +411,13 @@ describe("feishuOutbound.sendPayload native cards", () => {
406411await feishuOutbound.sendPayload?.({
407412cfg: emptyConfig,
408413to: "chat_1",
409-text: "Choose <at id=\"ou_1\">",
414+text: 'Choose <at id="ou_1">',
410415accountId: "main",
411416payload: {
412-text: "Choose <at id=\"ou_1\">",
417+text: 'Choose <at id="ou_1">',
413418presentation: {
414419blocks: [
415-{ type: "context", text: "</font><at id=\"ou_2\">Injected</at>" },
420+{ type: "context", text: '</font><at id="ou_2">Injected</at>' },
416421{
417422type: "buttons",
418423buttons: [
@@ -428,10 +433,11 @@ describe("feishuOutbound.sendPayload native cards", () => {
428433const card = sendCardFeishuMock.mock.calls[0][0].card;
429434expect(card.body.elements).toEqual(
430435expect.arrayContaining([
431-{ tag: "markdown", content: "Choose <at id=\"ou_1\">" },
436+{ tag: "markdown", content: 'Choose <at id="ou_1">' },
432437{
433438tag: "markdown",
434-content: "<font color='grey'></font><at id=\"ou_2\">Injected</at></font>",
439+content:
440+"<font color='grey'></font><at id=\"ou_2\">Injected</at></font>",
435441},
436442{
437443tag: "action",
@@ -466,7 +472,7 @@ describe("feishuOutbound.sendPayload native cards", () => {
466472body: {
467473elements: [
468474{ tag: "img", img_key: "image-secret" },
469-{ tag: "markdown", content: "<at id=\"ou_1\">ping</at>" },
475+{ tag: "markdown", content: '<at id="ou_1">ping</at>' },
470476{
471477tag: "action",
472478actions: [
@@ -493,7 +499,7 @@ describe("feishuOutbound.sendPayload native cards", () => {
493499const card = sendCardFeishuMock.mock.calls[0][0].card;
494500expect(card.header.template).toBe("blue");
495501expect(card.body.elements).toEqual([
496-{ tag: "markdown", content: "<at id=\"ou_1\">ping</at>" },
502+{ tag: "markdown", content: '<at id="ou_1">ping</at>' },
497503{
498504tag: "action",
499505actions: [
@@ -855,6 +861,83 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
855861);
856862});
857863864+it("suppresses duplicate text when sending voice media", async () => {
865+await feishuOutbound.sendMedia?.({
866+cfg: emptyConfig,
867+to: "chat_1",
868+text: "spoken reply",
869+mediaUrl: "https://example.com/reply.mp3",
870+audioAsVoice: true,
871+accountId: "main",
872+});
873+874+expect(sendMessageFeishuMock).not.toHaveBeenCalled();
875+expect(sendMediaFeishuMock).toHaveBeenCalledWith(
876+expect.objectContaining({
877+mediaUrl: "https://example.com/reply.mp3",
878+audioAsVoice: true,
879+}),
880+);
881+});
882+883+it("suppresses duplicate text for native voice media without audioAsVoice", async () => {
884+await feishuOutbound.sendMedia?.({
885+cfg: emptyConfig,
886+to: "chat_1",
887+text: "spoken reply",
888+mediaUrl: "https://example.com/reply.ogg?download=1",
889+accountId: "main",
890+});
891+892+expect(sendMessageFeishuMock).not.toHaveBeenCalled();
893+expect(sendMediaFeishuMock).toHaveBeenCalledWith(
894+expect.objectContaining({
895+mediaUrl: "https://example.com/reply.ogg?download=1",
896+}),
897+);
898+});
899+900+it("keeps captions for regular audio file attachments", async () => {
901+await feishuOutbound.sendMedia?.({
902+cfg: emptyConfig,
903+to: "chat_1",
904+text: "caption text",
905+mediaUrl: "https://example.com/song.mp3",
906+accountId: "main",
907+});
908+909+expect(sendMessageFeishuMock).toHaveBeenCalledWith(
910+expect.objectContaining({
911+text: "caption text",
912+}),
913+);
914+expect(sendMediaFeishuMock).toHaveBeenCalledWith(
915+expect.objectContaining({
916+mediaUrl: "https://example.com/song.mp3",
917+}),
918+);
919+});
920+921+it("keeps skipped voice text in the upload failure fallback", async () => {
922+sendMediaFeishuMock.mockRejectedValueOnce(new Error("upload failed"));
923+924+await feishuOutbound.sendMedia?.({
925+cfg: emptyConfig,
926+to: "chat_1",
927+text: "spoken reply",
928+mediaUrl: "https://example.com/reply.mp3",
929+audioAsVoice: true,
930+accountId: "main",
931+});
932+933+expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
934+expect(sendMessageFeishuMock).toHaveBeenCalledWith(
935+expect.objectContaining({
936+text: "spoken reply\n\n📎 https://example.com/reply.mp3",
937+}),
938+);
939+});
940+858941it("forwards replyToId to text caption send", async () => {
859942await feishuOutbound.sendMedia?.({
860943cfg: emptyConfig,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。