fix(feishu): preserve all accepted voice fallback content (#128130)

This commit is contained in:
Peter Steinberger
2026-08-23 05:05:43 -07:00
committed by GitHub
parent 1475b67111
commit 8a0ddd3311
3 changed files with 73 additions and 2 deletions
@@ -74,12 +74,17 @@ export function mergeFeishuReplyDeliveryResults(
content?: string,
): FeishuReplyDeliveryResult {
const visible = results.filter((result) => result.visibleReplySent === true);
const acceptedContent = visible.flatMap((result) =>
result.content === undefined ? [] : [result.content],
);
return createFeishuReplyDeliveryResult({
results: visible,
visibleReplySent: visible.length > 0,
content:
content === undefined
? results.find((result) => result.content !== undefined)?.content
? acceptedContent.length > 0
? acceptedContent.join("\n\n")
: undefined
: content,
});
}
@@ -2381,6 +2381,71 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
});
});
it("reports every accepted voice upload fallback in the successful delivery result", async () => {
sendMediaFeishuMock
.mockRejectedValueOnce(new Error("first upload failed"))
.mockRejectedValueOnce(new Error("second upload failed"));
sendMessageFeishuMock
.mockResolvedValueOnce({ messageId: "om-first-fallback" })
.mockResolvedValueOnce({ messageId: "om-second-fallback" });
const { options } = createDispatcherHarness();
const delivery = await options.deliver(
{
text: "spoken reply",
mediaUrls: ["https://example.com/first.mp3", "https://example.com/second.mp3"],
audioAsVoice: true,
},
{ kind: "final" },
);
expect(delivery).toMatchObject({
messageIds: ["om-first-fallback", "om-second-fallback"],
visibleReplySent: true,
content:
"spoken reply\n\n📎 https://example.com/first.mp3\n\n📎 https://example.com/second.mp3",
});
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(2);
});
it("retains every accepted voice upload fallback when a later fallback fails", async () => {
sendMediaFeishuMock
.mockRejectedValueOnce(new Error("first upload failed"))
.mockRejectedValueOnce(new Error("second upload failed"))
.mockRejectedValueOnce(new Error("third upload failed"));
sendMessageFeishuMock
.mockResolvedValueOnce({ messageId: "om-first-fallback" })
.mockResolvedValueOnce({ messageId: "om-second-fallback" })
.mockRejectedValueOnce(new Error("third fallback failed"));
const { options } = createDispatcherHarness();
const error = await options
.deliver(
{
text: "spoken reply",
mediaUrls: [
"https://example.com/first.mp3",
"https://example.com/second.mp3",
"https://example.com/third.mp3",
],
audioAsVoice: true,
},
{ kind: "final" },
)
.catch((caught: unknown) => caught);
expect(error).toMatchObject({
code: "CHANNEL_PARTIAL_DELIVERY",
deliveryResult: {
messageIds: ["om-first-fallback", "om-second-fallback"],
visibleReplySent: true,
content:
"spoken reply\n\n📎 https://example.com/first.mp3\n\n📎 https://example.com/second.mp3",
},
});
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(3);
});
it("does not leak local media paths in the upload failure fallback", async () => {
const mediaPath = path.join(os.tmpdir(), "openclaw-feishu-reply-local-voice.mp3");
sendMediaFeishuMock.mockRejectedValueOnce(new Error("media failed"));
+2 -1
View File
@@ -1495,7 +1495,8 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
hasVoiceMedia && hasText ? { fallbackText: text } : undefined,
);
}
const result = mergeFeishuReplyDeliveryResults(deliveredResults, text);
const deliveredContent = hasVoiceMedia ? (deliveredResults.at(-1)?.content ?? text) : text;
const result = mergeFeishuReplyDeliveryResults(deliveredResults, deliveredContent);
if (priorClosedStreamingSettlement?.error !== undefined) {
throw createFeishuPartialReplyDeliveryError(
isChannelPartialDeliveryError(priorClosedStreamingSettlement.error) &&