mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +00: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 () => {
|
it("skips remote URLs", async () => {
|
||||||
const blocks = await buildWebchatAudioContentBlocksFromReplyPayloads([
|
const blocks = await buildWebchatAudioContentBlocksFromReplyPayloads([
|
||||||
{ mediaUrl: "https://example.com/a.mp3", trustedLocalMedia: true },
|
{ 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 () => {
|
it("suppresses control tokens and falls back to synthetic image text", async () => {
|
||||||
const message = await buildWebchatAssistantMessageFromReplyPayloads([
|
const message = await buildWebchatAssistantMessageFromReplyPayloads([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ export async function buildWebchatAudioContentBlocksFromReplyPayloads(
|
|||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const blocks: Array<Record<string, unknown>> = [];
|
const blocks: Array<Record<string, unknown>> = [];
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
|
if (payload.isReasoning === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const parts = resolveSendableOutboundReplyParts(payload);
|
const parts = resolveSendableOutboundReplyParts(payload);
|
||||||
for (const raw of parts.mediaUrls) {
|
for (const raw of parts.mediaUrls) {
|
||||||
const url = raw.trim();
|
const url = raw.trim();
|
||||||
@@ -194,6 +197,9 @@ export async function buildWebchatAssistantMessageFromReplyPayloads(
|
|||||||
let hasImage = false;
|
let hasImage = false;
|
||||||
|
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
|
if (payload.isReasoning === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const visibleText = payload.text?.trim();
|
const visibleText = payload.text?.trim();
|
||||||
const text =
|
const text =
|
||||||
visibleText && !isSuppressedControlReplyText(visibleText) ? visibleText : undefined;
|
visibleText && !isSuppressedControlReplyText(visibleText) ? visibleText : undefined;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const mockState = vi.hoisted(() => ({
|
|||||||
sensitiveMedia?: boolean;
|
sensitiveMedia?: boolean;
|
||||||
replyToId?: string;
|
replyToId?: string;
|
||||||
replyToCurrent?: boolean;
|
replyToCurrent?: boolean;
|
||||||
|
isReasoning?: boolean;
|
||||||
} | null,
|
} | null,
|
||||||
dispatchedReplies: [] as Array<{
|
dispatchedReplies: [] as Array<{
|
||||||
kind: "tool" | "block" | "final";
|
kind: "tool" | "block" | "final";
|
||||||
@@ -36,6 +37,7 @@ const mockState = vi.hoisted(() => ({
|
|||||||
trustedLocalMedia?: boolean;
|
trustedLocalMedia?: boolean;
|
||||||
replyToId?: string;
|
replyToId?: string;
|
||||||
replyToCurrent?: boolean;
|
replyToCurrent?: boolean;
|
||||||
|
isReasoning?: boolean;
|
||||||
};
|
};
|
||||||
}>,
|
}>,
|
||||||
dispatchError: null as Error | null,
|
dispatchError: null as Error | null,
|
||||||
@@ -114,6 +116,7 @@ vi.mock("../../auto-reply/dispatch.js", () => ({
|
|||||||
sensitiveMedia?: boolean;
|
sensitiveMedia?: boolean;
|
||||||
replyToId?: string;
|
replyToId?: string;
|
||||||
replyToCurrent?: boolean;
|
replyToCurrent?: boolean;
|
||||||
|
isReasoning?: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
sendBlockReply: (payload: {
|
sendBlockReply: (payload: {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -122,6 +125,7 @@ vi.mock("../../auto-reply/dispatch.js", () => ({
|
|||||||
trustedLocalMedia?: boolean;
|
trustedLocalMedia?: boolean;
|
||||||
replyToId?: string;
|
replyToId?: string;
|
||||||
replyToCurrent?: boolean;
|
replyToCurrent?: boolean;
|
||||||
|
isReasoning?: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
sendToolResult: (payload: {
|
sendToolResult: (payload: {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -130,6 +134,7 @@ vi.mock("../../auto-reply/dispatch.js", () => ({
|
|||||||
trustedLocalMedia?: boolean;
|
trustedLocalMedia?: boolean;
|
||||||
replyToId?: string;
|
replyToId?: string;
|
||||||
replyToCurrent?: boolean;
|
replyToCurrent?: boolean;
|
||||||
|
isReasoning?: boolean;
|
||||||
}) => boolean;
|
}) => boolean;
|
||||||
markComplete: () => void;
|
markComplete: () => void;
|
||||||
waitForIdle: () => Promise<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");
|
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 () => {
|
it("chat.inject keeps message defined when directive tag is the only content", async () => {
|
||||||
createTranscriptFixture("openclaw-chat-inject-directive-only-");
|
createTranscriptFixture("openclaw-chat-inject-directive-only-");
|
||||||
const respond = vi.fn();
|
const respond = vi.fn();
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ type ChatAbortRequester = {
|
|||||||
|
|
||||||
/** True when a reply payload carries at least one media reference (mediaUrl or mediaUrls). */
|
/** True when a reply payload carries at least one media reference (mediaUrl or mediaUrls). */
|
||||||
function isMediaBearingPayload(payload: ReplyPayload): boolean {
|
function isMediaBearingPayload(payload: ReplyPayload): boolean {
|
||||||
|
if (payload.isReasoning === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (payload.mediaUrl?.trim()) {
|
if (payload.mediaUrl?.trim()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -227,6 +230,9 @@ type SideResultPayload = {
|
|||||||
function buildTranscriptReplyText(payloads: ReplyPayload[]): string {
|
function buildTranscriptReplyText(payloads: ReplyPayload[]): string {
|
||||||
const chunks = payloads
|
const chunks = payloads
|
||||||
.map((payload) => {
|
.map((payload) => {
|
||||||
|
if (payload.isReasoning === true) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
const parts = resolveSendableOutboundReplyParts(payload);
|
const parts = resolveSendableOutboundReplyParts(payload);
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
const replyToId = sanitizeReplyDirectiveId(payload.replyToId);
|
const replyToId = sanitizeReplyDirectiveId(payload.replyToId);
|
||||||
@@ -301,7 +307,10 @@ async function buildAssistantDisplayContentFromReplyPayloads(params: {
|
|||||||
onManagedImagePrepareError?: (message: string) => void;
|
onManagedImagePrepareError?: (message: string) => void;
|
||||||
}): Promise<AssistantDisplayContentBlock[] | undefined> {
|
}): Promise<AssistantDisplayContentBlock[] | undefined> {
|
||||||
const rawTextPayloadCount = params.payloads.filter(
|
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;
|
).length;
|
||||||
const normalized = normalizeReplyPayloadsForDelivery(params.payloads);
|
const normalized = normalizeReplyPayloadsForDelivery(params.payloads);
|
||||||
if (normalized.length === 0) {
|
if (normalized.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user