fix(webchat): keep trustedLocalMedia internal to reply payloads

Restore Omit on public plugin-sdk ReplyPayload; set trustedLocalMedia via
runtime assertion in speech-core and explicitly on dispatch TTS-only finals.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuyangfan 2026-05-17 03:11:15 +08:00 committed by Peter Steinberger
parent eec18fccb4
commit 35e1c7ac41
5 changed files with 9 additions and 11 deletions

View file

@ -210,7 +210,7 @@ async function expectTtsPayloadResult(params: {
expect(result.audioAsVoice).toBe(params.audioAsVoice);
expect(result.mediaUrl).toMatch(new RegExp(`voice-\\d+\\.${params.mediaExtension ?? "ogg"}$`));
expect(result.spokenText).toBe(params.text);
expect(result.trustedLocalMedia).toBe(true);
expect((result as { trustedLocalMedia?: boolean }).trustedLocalMedia).toBe(true);
mediaDir = result.mediaUrl ? path.dirname(result.mediaUrl) : undefined;
} finally {
@ -453,7 +453,7 @@ describe("speech-core native voice-note routing", () => {
});
expect(synthesizeMock).not.toHaveBeenCalled();
expect(result.trustedLocalMedia).toBeUndefined();
expect((result as { trustedLocalMedia?: boolean }).trustedLocalMedia).toBeUndefined();
expect(result.text).toBe("WebChat block stream chunks defer TTS to the final tail.");
});
@ -468,7 +468,7 @@ describe("speech-core native voice-note routing", () => {
});
expect(synthesizeMock).not.toHaveBeenCalled();
expect(result.trustedLocalMedia).toBeUndefined();
expect((result as { trustedLocalMedia?: boolean }).trustedLocalMedia).toBeUndefined();
expect(result.text).toBe("Intermediate tool output should not be spoken.");
});

View file

@ -1850,7 +1850,7 @@ export async function maybeApplyTtsToPayload(params: {
audioAsVoice: result.audioAsVoice || params.payload.audioAsVoice,
spokenText: textForAudio,
trustedLocalMedia: true,
};
} as ReplyPayload;
}
lastTtsAttempt = {

View file

@ -89,16 +89,14 @@ async function main() {
throw new Error("expected final-mode tail TTS to write a local media file");
}
// Same shape as dispatch-from-config accumulated block TTS-only final payload.
const ttsOnlyPayload = {
mediaUrl: tailResult.mediaUrl,
audioAsVoice: tailResult.audioAsVoice,
spokenText: accumulatedBlockText,
trustedLocalMedia: tailResult.trustedLocalMedia,
trustedLocalMedia: true,
};
console.log(
"dispatch ttsOnlyPayload.trustedLocalMedia =",
ttsOnlyPayload.trustedLocalMedia ?? false,
);
console.log("dispatch ttsOnlyPayload.trustedLocalMedia =", ttsOnlyPayload.trustedLocalMedia);
const localRoots = [path.dirname(mediaPath)];
const trustedBlocks = await buildWebchatAudioContentBlocksFromReplyPayloads([ttsOnlyPayload], {

View file

@ -1700,7 +1700,7 @@ export async function dispatchReplyFromConfig(
mediaUrl: ttsSyntheticReply.mediaUrl,
audioAsVoice: ttsSyntheticReply.audioAsVoice,
spokenText: accumulatedBlockTtsText,
trustedLocalMedia: ttsSyntheticReply.trustedLocalMedia,
trustedLocalMedia: true,
};
const normalizedTtsOnlyPayload = await normalizeReplyMediaPayload(ttsOnlyPayload);
const result = await routeReplyToOriginating(normalizedTtsOnlyPayload);

View file

@ -6,7 +6,7 @@ import { normalizeLowercaseStringOrEmpty, readStringValue } from "../shared/stri
export type { MediaPayload, MediaPayloadInput } from "../channels/plugins/media-payload.js";
export { buildMediaPayload } from "../channels/plugins/media-payload.js";
export type ReplyPayload = InternalReplyPayload;
export type ReplyPayload = Omit<InternalReplyPayload, "trustedLocalMedia">;
export type OutboundReplyPayload = {
text?: string;