mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 04:57:09 +02:00
fix(gateway): hide webchat reasoning payloads
This commit is contained in:
@@ -43,6 +43,26 @@ describe("buildWebchatAudioContentBlocksFromReplyPayloads", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses reasoning payload audio", async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-webchat-audio-"));
|
||||
const audioPath = path.join(tmpDir, "clip.mp3");
|
||||
fs.writeFileSync(audioPath, Buffer.from([0xff, 0xfb, 0x90, 0x00]));
|
||||
|
||||
const blocks = await buildWebchatAudioContentBlocksFromReplyPayloads(
|
||||
[
|
||||
{
|
||||
text: "Reasoning:\n_step_",
|
||||
mediaUrl: audioPath,
|
||||
trustedLocalMedia: true,
|
||||
isReasoning: true,
|
||||
},
|
||||
],
|
||||
{ localRoots: [tmpDir] },
|
||||
);
|
||||
|
||||
expect(blocks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("skips remote URLs", async () => {
|
||||
const blocks = await buildWebchatAudioContentBlocksFromReplyPayloads([
|
||||
{ mediaUrl: "https://example.com/a.mp3", trustedLocalMedia: true },
|
||||
@@ -212,6 +232,18 @@ describe("buildWebchatAssistantMessageFromReplyPayloads", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses reasoning payload media transcripts", async () => {
|
||||
const message = await buildWebchatAssistantMessageFromReplyPayloads([
|
||||
{
|
||||
text: "Reasoning:\n_step_",
|
||||
mediaUrl: "data:image/png;base64,cG5n",
|
||||
isReasoning: true,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(message).toBeNull();
|
||||
});
|
||||
|
||||
it("suppresses control tokens and falls back to synthetic image text", async () => {
|
||||
const message = await buildWebchatAssistantMessageFromReplyPayloads([
|
||||
{
|
||||
|
||||
@@ -162,6 +162,9 @@ export async function buildWebchatAudioContentBlocksFromReplyPayloads(
|
||||
const seen = new Set<string>();
|
||||
const blocks: Array<Record<string, unknown>> = [];
|
||||
for (const payload of payloads) {
|
||||
if (payload.isReasoning === true) {
|
||||
continue;
|
||||
}
|
||||
const parts = resolveSendableOutboundReplyParts(payload);
|
||||
for (const raw of parts.mediaUrls) {
|
||||
const url = raw.trim();
|
||||
@@ -194,6 +197,9 @@ export async function buildWebchatAssistantMessageFromReplyPayloads(
|
||||
let hasImage = false;
|
||||
|
||||
for (const payload of payloads) {
|
||||
if (payload.isReasoning === true) {
|
||||
continue;
|
||||
}
|
||||
const visibleText = payload.text?.trim();
|
||||
const text =
|
||||
visibleText && !isSuppressedControlReplyText(visibleText) ? visibleText : undefined;
|
||||
|
||||
@@ -26,6 +26,7 @@ const mockState = vi.hoisted(() => ({
|
||||
sensitiveMedia?: boolean;
|
||||
replyToId?: string;
|
||||
replyToCurrent?: boolean;
|
||||
isReasoning?: boolean;
|
||||
} | null,
|
||||
dispatchedReplies: [] as Array<{
|
||||
kind: "tool" | "block" | "final";
|
||||
@@ -36,6 +37,7 @@ const mockState = vi.hoisted(() => ({
|
||||
trustedLocalMedia?: boolean;
|
||||
replyToId?: string;
|
||||
replyToCurrent?: boolean;
|
||||
isReasoning?: boolean;
|
||||
};
|
||||
}>,
|
||||
dispatchError: null as Error | null,
|
||||
@@ -114,6 +116,7 @@ vi.mock("../../auto-reply/dispatch.js", () => ({
|
||||
sensitiveMedia?: boolean;
|
||||
replyToId?: string;
|
||||
replyToCurrent?: boolean;
|
||||
isReasoning?: boolean;
|
||||
}) => boolean;
|
||||
sendBlockReply: (payload: {
|
||||
text?: string;
|
||||
@@ -122,6 +125,7 @@ vi.mock("../../auto-reply/dispatch.js", () => ({
|
||||
trustedLocalMedia?: boolean;
|
||||
replyToId?: string;
|
||||
replyToCurrent?: boolean;
|
||||
isReasoning?: boolean;
|
||||
}) => boolean;
|
||||
sendToolResult: (payload: {
|
||||
text?: string;
|
||||
@@ -130,6 +134,7 @@ vi.mock("../../auto-reply/dispatch.js", () => ({
|
||||
trustedLocalMedia?: boolean;
|
||||
replyToId?: string;
|
||||
replyToCurrent?: boolean;
|
||||
isReasoning?: boolean;
|
||||
}) => boolean;
|
||||
markComplete: () => void;
|
||||
waitForIdle: () => Promise<void>;
|
||||
@@ -599,6 +604,31 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
|
||||
expect(JSON.stringify(payload?.message)).not.toContain("MEDIA:data:image/png;base64,cG5n");
|
||||
});
|
||||
|
||||
it("suppresses reasoning payloads from webchat transcript replies", async () => {
|
||||
createTranscriptFixture("openclaw-chat-send-reasoning-hidden-");
|
||||
mockState.dispatchedReplies = [
|
||||
{
|
||||
kind: "final",
|
||||
payload: { text: "Reasoning:\n_step_", isReasoning: true },
|
||||
},
|
||||
{
|
||||
kind: "final",
|
||||
payload: { text: "final answer" },
|
||||
},
|
||||
];
|
||||
const respond = vi.fn();
|
||||
const context = createChatContext();
|
||||
|
||||
const payload = await runNonStreamingChatSend({
|
||||
context,
|
||||
respond,
|
||||
idempotencyKey: "idem-reasoning-hidden",
|
||||
});
|
||||
|
||||
expect(JSON.stringify(payload?.message)).toContain("final answer");
|
||||
expect(JSON.stringify(payload?.message)).not.toContain("Reasoning");
|
||||
});
|
||||
|
||||
it("chat.inject keeps message defined when directive tag is the only content", async () => {
|
||||
createTranscriptFixture("openclaw-chat-inject-directive-only-");
|
||||
const respond = vi.fn();
|
||||
|
||||
@@ -131,6 +131,9 @@ type ChatAbortRequester = {
|
||||
|
||||
/** True when a reply payload carries at least one media reference (mediaUrl or mediaUrls). */
|
||||
function isMediaBearingPayload(payload: ReplyPayload): boolean {
|
||||
if (payload.isReasoning === true) {
|
||||
return false;
|
||||
}
|
||||
if (payload.mediaUrl?.trim()) {
|
||||
return true;
|
||||
}
|
||||
@@ -227,6 +230,9 @@ type SideResultPayload = {
|
||||
function buildTranscriptReplyText(payloads: ReplyPayload[]): string {
|
||||
const chunks = payloads
|
||||
.map((payload) => {
|
||||
if (payload.isReasoning === true) {
|
||||
return "";
|
||||
}
|
||||
const parts = resolveSendableOutboundReplyParts(payload);
|
||||
const lines: string[] = [];
|
||||
const replyToId = sanitizeReplyDirectiveId(payload.replyToId);
|
||||
@@ -301,7 +307,10 @@ async function buildAssistantDisplayContentFromReplyPayloads(params: {
|
||||
onManagedImagePrepareError?: (message: string) => void;
|
||||
}): Promise<AssistantDisplayContentBlock[] | undefined> {
|
||||
const rawTextPayloadCount = params.payloads.filter(
|
||||
(payload) => typeof payload.text === "string" && payload.text.trim().length > 0,
|
||||
(payload) =>
|
||||
payload.isReasoning !== true &&
|
||||
typeof payload.text === "string" &&
|
||||
payload.text.trim().length > 0,
|
||||
).length;
|
||||
const normalized = normalizeReplyPayloadsForDelivery(params.payloads);
|
||||
if (normalized.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user