mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 22:00:44 +00:00
fix(channels): preserve degraded voice text and mention boundaries
This commit is contained in:
@@ -361,7 +361,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
||||
contentType: "audio/mpeg",
|
||||
});
|
||||
|
||||
await sendMediaFeishu({
|
||||
const result = await sendMediaFeishu({
|
||||
cfg: emptyConfig,
|
||||
to: "user:ou_target",
|
||||
mediaUrl: "https://example.com/reply.mp3",
|
||||
@@ -382,6 +382,7 @@ describe("sendMediaFeishu msg_type routing", () => {
|
||||
data: expect.objectContaining({ msg_type: "file" }),
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual(expect.objectContaining({ voiceIntentDegradedToFile: true }));
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("audioAsVoice transcode failed"),
|
||||
expect.any(Error),
|
||||
|
||||
@@ -399,6 +399,7 @@ export type UploadFileResult = {
|
||||
export type SendMediaResult = {
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
voiceIntentDegradedToFile?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -872,10 +873,22 @@ export async function sendMediaFeishu(params: {
|
||||
contentType = prepared.contentType;
|
||||
|
||||
const routing = resolveFeishuOutboundMediaKind({ fileName: name, contentType });
|
||||
const voiceIntentDegradedToFile = audioAsVoice === true && routing.msgType !== "audio";
|
||||
|
||||
if (routing.msgType === "image") {
|
||||
const { imageKey } = await uploadImageFeishu({ cfg, image: buffer, accountId });
|
||||
return sendImageFeishu({ cfg, to, imageKey, replyToMessageId, replyInThread, accountId });
|
||||
const result = await sendImageFeishu({
|
||||
cfg,
|
||||
to,
|
||||
imageKey,
|
||||
replyToMessageId,
|
||||
replyInThread,
|
||||
accountId,
|
||||
});
|
||||
return {
|
||||
...result,
|
||||
...(voiceIntentDegradedToFile ? { voiceIntentDegradedToFile: true } : {}),
|
||||
};
|
||||
}
|
||||
const { fileKey } = await uploadFileFeishu({
|
||||
cfg,
|
||||
@@ -884,7 +897,7 @@ export async function sendMediaFeishu(params: {
|
||||
fileType: routing.fileType ?? "stream",
|
||||
accountId,
|
||||
});
|
||||
return sendFileFeishu({
|
||||
const result = await sendFileFeishu({
|
||||
cfg,
|
||||
to,
|
||||
fileKey,
|
||||
@@ -893,4 +906,8 @@ export async function sendMediaFeishu(params: {
|
||||
replyInThread,
|
||||
accountId,
|
||||
});
|
||||
return {
|
||||
...result,
|
||||
...(voiceIntentDegradedToFile ? { voiceIntentDegradedToFile: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -880,6 +880,35 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sends skipped voice text when voice media degrades to a file attachment", async () => {
|
||||
sendMediaFeishuMock.mockResolvedValueOnce({
|
||||
messageId: "file_msg",
|
||||
voiceIntentDegradedToFile: true,
|
||||
});
|
||||
|
||||
await feishuOutbound.sendMedia?.({
|
||||
cfg: emptyConfig,
|
||||
to: "chat_1",
|
||||
text: "spoken reply",
|
||||
mediaUrl: "https://example.com/reply.mp3",
|
||||
audioAsVoice: true,
|
||||
accountId: "main",
|
||||
});
|
||||
|
||||
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mediaUrl: "https://example.com/reply.mp3",
|
||||
audioAsVoice: true,
|
||||
}),
|
||||
);
|
||||
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: "spoken reply",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses duplicate text for native voice media without audioAsVoice", async () => {
|
||||
await feishuOutbound.sendMedia?.({
|
||||
cfg: emptyConfig,
|
||||
|
||||
@@ -693,7 +693,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
||||
// Upload and send media if URL or local path provided
|
||||
if (mediaUrl) {
|
||||
try {
|
||||
return await sendMediaFeishu({
|
||||
const result = await sendMediaFeishu({
|
||||
cfg,
|
||||
to,
|
||||
mediaUrl,
|
||||
@@ -702,6 +702,16 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
||||
replyToMessageId,
|
||||
...(audioAsVoice === true ? { audioAsVoice: true } : {}),
|
||||
});
|
||||
if (result.voiceIntentDegradedToFile && text?.trim()) {
|
||||
await sendOutboundText({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToMessageId,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
// Log the error for debugging
|
||||
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
||||
|
||||
@@ -648,6 +648,37 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sends skipped voice text when final voice media degrades to a file attachment", async () => {
|
||||
sendMediaFeishuMock.mockResolvedValueOnce({
|
||||
messageId: "file_msg",
|
||||
voiceIntentDegradedToFile: true,
|
||||
});
|
||||
|
||||
const { options } = createDispatcherHarness();
|
||||
await options.deliver(
|
||||
{
|
||||
text: "spoken reply",
|
||||
mediaUrl: "https://example.com/reply.mp3",
|
||||
audioAsVoice: true,
|
||||
},
|
||||
{ kind: "final" },
|
||||
);
|
||||
|
||||
expect(sendMediaFeishuMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mediaUrl: "https://example.com/reply.mp3",
|
||||
audioAsVoice: true,
|
||||
}),
|
||||
);
|
||||
expect(sendMessageFeishuMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
text: "spoken reply",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses duplicate text for native voice media without audioAsVoice", async () => {
|
||||
const { options } = createDispatcherHarness();
|
||||
await options.deliver(
|
||||
|
||||
@@ -444,7 +444,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
mediaUrls,
|
||||
caption: "",
|
||||
send: async ({ mediaUrl }) => {
|
||||
await sendMediaFeishu({
|
||||
const result = await sendMediaFeishu({
|
||||
cfg,
|
||||
to: chatId,
|
||||
mediaUrl,
|
||||
@@ -453,6 +453,25 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
accountId,
|
||||
...(payload.audioAsVoice === true ? { audioAsVoice: true } : {}),
|
||||
});
|
||||
if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) {
|
||||
sentFallbackText = true;
|
||||
await sendChunkedTextReply({
|
||||
text: options.fallbackText,
|
||||
useCard: false,
|
||||
infoKind: "final",
|
||||
sendChunk: async ({ chunk, isFirst }) => {
|
||||
await sendMessageFeishu({
|
||||
cfg,
|
||||
to: chatId,
|
||||
text: chunk,
|
||||
replyToMessageId: sendReplyToMessageId,
|
||||
replyInThread: effectiveReplyInThread,
|
||||
mentions: isFirst ? mentionTargets : undefined,
|
||||
accountId,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
onError:
|
||||
options?.fallbackText === undefined
|
||||
|
||||
Reference in New Issue
Block a user