mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:11:37 +00:00
fix(tts): update tool description to prevent duplicate audio delivery (#18046)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 70c096abaa
Co-authored-by: zerone0x <39543393+zerone0x@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
This commit is contained in:
@@ -165,7 +165,7 @@ describe("dispatchReplyFromConfig", () => {
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not provide onToolResult in group sessions", async () => {
|
||||
it("suppresses group tool summaries but still forwards tool media", async () => {
|
||||
mocks.tryFastAbortFromMessage.mockResolvedValue({
|
||||
handled: false,
|
||||
aborted: false,
|
||||
@@ -182,11 +182,23 @@ describe("dispatchReplyFromConfig", () => {
|
||||
opts: GetReplyOptions | undefined,
|
||||
_cfg: OpenClawConfig,
|
||||
) => {
|
||||
expect(opts?.onToolResult).toBeUndefined();
|
||||
expect(opts?.onToolResult).toBeDefined();
|
||||
await opts?.onToolResult?.({ text: "🔧 exec: ls" });
|
||||
await opts?.onToolResult?.({
|
||||
text: "NO_REPLY",
|
||||
mediaUrls: ["https://example.com/tts-group.opus"],
|
||||
});
|
||||
return { text: "hi" } satisfies ReplyPayload;
|
||||
};
|
||||
|
||||
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||
|
||||
expect(dispatcher.sendToolResult).toHaveBeenCalledTimes(1);
|
||||
const sent = (dispatcher.sendToolResult as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as
|
||||
| ReplyPayload
|
||||
| undefined;
|
||||
expect(sent?.mediaUrls).toEqual(["https://example.com/tts-group.opus"]);
|
||||
expect(sent?.text).toBeUndefined();
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -219,7 +231,7 @@ describe("dispatchReplyFromConfig", () => {
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not provide onToolResult for native slash commands", async () => {
|
||||
it("suppresses native tool summaries but still forwards tool media", async () => {
|
||||
mocks.tryFastAbortFromMessage.mockResolvedValue({
|
||||
handled: false,
|
||||
aborted: false,
|
||||
@@ -237,11 +249,22 @@ describe("dispatchReplyFromConfig", () => {
|
||||
opts: GetReplyOptions | undefined,
|
||||
_cfg: OpenClawConfig,
|
||||
) => {
|
||||
expect(opts?.onToolResult).toBeUndefined();
|
||||
expect(opts?.onToolResult).toBeDefined();
|
||||
await opts?.onToolResult?.({ text: "🔧 tools/sessions_send" });
|
||||
await opts?.onToolResult?.({
|
||||
mediaUrl: "https://example.com/tts-native.opus",
|
||||
});
|
||||
return { text: "hi" } satisfies ReplyPayload;
|
||||
};
|
||||
|
||||
await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
|
||||
|
||||
expect(dispatcher.sendToolResult).toHaveBeenCalledTimes(1);
|
||||
const sent = (dispatcher.sendToolResult as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as
|
||||
| ReplyPayload
|
||||
| undefined;
|
||||
expect(sent?.mediaUrl).toBe("https://example.com/tts-native.opus");
|
||||
expect(sent?.text).toBeUndefined();
|
||||
expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -293,30 +293,45 @@ export async function dispatchReplyFromConfig(params: {
|
||||
|
||||
const shouldSendToolSummaries = ctx.ChatType !== "group" && ctx.CommandSource !== "native";
|
||||
|
||||
const resolveToolDeliveryPayload = (payload: ReplyPayload): ReplyPayload | null => {
|
||||
if (shouldSendToolSummaries) {
|
||||
return payload;
|
||||
}
|
||||
// Group/native flows intentionally suppress tool summary text, but media-only
|
||||
// tool results (for example TTS audio) must still be delivered.
|
||||
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
|
||||
if (!hasMedia) {
|
||||
return null;
|
||||
}
|
||||
return { ...payload, text: undefined };
|
||||
};
|
||||
|
||||
const replyResult = await (params.replyResolver ?? getReplyFromConfig)(
|
||||
ctx,
|
||||
{
|
||||
...params.replyOptions,
|
||||
onToolResult: shouldSendToolSummaries
|
||||
? (payload: ReplyPayload) => {
|
||||
const run = async () => {
|
||||
const ttsPayload = await maybeApplyTtsToPayload({
|
||||
payload,
|
||||
cfg,
|
||||
channel: ttsChannel,
|
||||
kind: "tool",
|
||||
inboundAudio,
|
||||
ttsAuto: sessionTtsAuto,
|
||||
});
|
||||
if (shouldRouteToOriginating) {
|
||||
await sendPayloadAsync(ttsPayload, undefined, false);
|
||||
} else {
|
||||
dispatcher.sendToolResult(ttsPayload);
|
||||
}
|
||||
};
|
||||
return run();
|
||||
onToolResult: (payload: ReplyPayload) => {
|
||||
const run = async () => {
|
||||
const ttsPayload = await maybeApplyTtsToPayload({
|
||||
payload,
|
||||
cfg,
|
||||
channel: ttsChannel,
|
||||
kind: "tool",
|
||||
inboundAudio,
|
||||
ttsAuto: sessionTtsAuto,
|
||||
});
|
||||
const deliveryPayload = resolveToolDeliveryPayload(ttsPayload);
|
||||
if (!deliveryPayload) {
|
||||
return;
|
||||
}
|
||||
: undefined,
|
||||
if (shouldRouteToOriginating) {
|
||||
await sendPayloadAsync(deliveryPayload, undefined, false);
|
||||
} else {
|
||||
dispatcher.sendToolResult(deliveryPayload);
|
||||
}
|
||||
};
|
||||
return run();
|
||||
},
|
||||
onBlockReply: (payload: ReplyPayload, context) => {
|
||||
const run = async () => {
|
||||
// Accumulate block text for TTS generation after streaming
|
||||
|
||||
Reference in New Issue
Block a user