






















@@ -1,11 +1,16 @@
11import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
2+import { sanitizeUserFacingText } from "../../agents/pi-embedded-helpers/sanitize-user-facing-text.js";
23import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.types.js";
34import type { ReplyToMode } from "../../config/types.js";
45import { logVerbose } from "../../globals.js";
56import { createLazyImportLoader } from "../../shared/lazy-promise.js";
67import { stripLegacyBracketToolCallBlocks } from "../../shared/text/assistant-visible-text.js";
78import { stripHeartbeatToken } from "../heartbeat.js";
8-import { copyReplyPayloadMetadata } from "../reply-payload.js";
9+import {
10+copyReplyPayloadMetadata,
11+getReplyPayloadMetadata,
12+setReplyPayloadMetadata,
13+} from "../reply-payload.js";
914import type { OriginatingChannelType } from "../templating.js";
1015import { SILENT_REPLY_TOKEN } from "../tokens.js";
1116import type { ReplyPayload, ReplyThreadingPolicy } from "../types.js";
@@ -97,17 +102,52 @@ function shouldKeepPayloadDuringSilentTurn(payload: ReplyPayload): boolean {
97102return payload.audioAsVoice === true && resolveSendableOutboundReplyParts(payload).hasMedia;
98103}
99104105+function sanitizeFinalReplyText(
106+payload: ReplyPayload,
107+text: string | undefined,
108+): string | undefined {
109+if (!text) {
110+return text;
111+}
112+return sanitizeUserFacingText(text, { errorContext: Boolean(payload.isError) });
113+}
114+100115function sanitizeHeartbeatPayload(payload: ReplyPayload): ReplyPayload {
101116const text = payload.text;
102117if (!text) {
103118return payload;
104119}
105-const cleaned = stripLegacyBracketToolCallBlocks(text);
120+const withoutLegacyBlocks = stripLegacyBracketToolCallBlocks(text);
121+const cleaned = sanitizeFinalReplyText(payload, withoutLegacyBlocks);
106122if (cleaned === text) {
107123return payload;
108124}
109-logVerbose("Stripped legacy tool-call block from heartbeat reply");
110-return copyReplyPayloadMetadata(payload, { ...payload, text: cleaned });
125+if (withoutLegacyBlocks !== text) {
126+logVerbose("Stripped legacy tool-call block from heartbeat reply");
127+}
128+return copyPayloadWithSanitizedText(payload, cleaned);
129+}
130+131+function copyPayloadWithSanitizedText(
132+payload: ReplyPayload,
133+text: string | undefined,
134+): ReplyPayload {
135+const sanitizedText = sanitizeFinalReplyText(payload, text);
136+const next = copyReplyPayloadMetadata(payload, {
137+ ...payload,
138+text: sanitizedText,
139+});
140+const mirror = getReplyPayloadMetadata(payload)?.sourceReplyTranscriptMirror;
141+if (!mirror?.text) {
142+return next;
143+}
144+setReplyPayloadMetadata(next, {
145+sourceReplyTranscriptMirror: {
146+ ...mirror,
147+text: sanitizeFinalReplyText(payload, mirror.text) || undefined,
148+},
149+});
150+return next;
111151}
112152113153export async function buildReplyPayloads(params: {
@@ -148,7 +188,7 @@ export async function buildReplyPayloads(params: {
148188}
149189150190if (!text || !text.includes("HEARTBEAT_OK")) {
151-sanitizedPayloads.push(copyReplyPayloadMetadata(payload, { ...payload, text }));
191+sanitizedPayloads.push(copyPayloadWithSanitizedText(payload, text));
152192continue;
153193}
154194const stripped = stripHeartbeatToken(text, { mode: "message" });
@@ -160,9 +200,7 @@ export async function buildReplyPayloads(params: {
160200if (stripped.shouldSkip && !hasMedia) {
161201continue;
162202}
163-sanitizedPayloads.push(
164-copyReplyPayloadMetadata(payload, { ...payload, text: stripped.text }),
165-);
203+sanitizedPayloads.push(copyPayloadWithSanitizedText(payload, stripped.text));
166204}
167205}
168206此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。