Files
clawdbot/src/auto-reply/reply/normalize-reply.ts
2026-01-16 03:01:23 +00:00

67 lines
2.1 KiB
TypeScript

import { stripHeartbeatToken } from "../heartbeat.js";
import { HEARTBEAT_TOKEN, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
import type { ReplyPayload } from "../types.js";
import { sanitizeUserFacingText } from "../../agents/pi-embedded-helpers.js";
import {
resolveResponsePrefixTemplate,
type ResponsePrefixContext,
} from "./response-prefix-template.js";
export type NormalizeReplyOptions = {
responsePrefix?: string;
/** Context for template variable interpolation in responsePrefix */
responsePrefixContext?: ResponsePrefixContext;
onHeartbeatStrip?: () => void;
stripHeartbeat?: boolean;
silentToken?: string;
};
export function normalizeReplyPayload(
payload: ReplyPayload,
opts: NormalizeReplyOptions = {},
): ReplyPayload | null {
const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
const trimmed = payload.text?.trim() ?? "";
if (!trimmed && !hasMedia) return null;
const silentToken = opts.silentToken ?? SILENT_REPLY_TOKEN;
let text = payload.text ?? undefined;
if (text && isSilentReplyText(text, silentToken)) {
if (!hasMedia) return null;
text = "";
}
if (text && !trimmed) {
// Keep empty text when media exists so media-only replies still send.
text = "";
}
const shouldStripHeartbeat = opts.stripHeartbeat ?? true;
if (shouldStripHeartbeat && text?.includes(HEARTBEAT_TOKEN)) {
const stripped = stripHeartbeatToken(text, { mode: "message" });
if (stripped.didStrip) opts.onHeartbeatStrip?.();
if (stripped.shouldSkip && !hasMedia) return null;
text = stripped.text;
}
if (text) {
text = sanitizeUserFacingText(text);
}
if (!text?.trim() && !hasMedia) return null;
// Resolve template variables in responsePrefix if context is provided
const effectivePrefix = opts.responsePrefixContext
? resolveResponsePrefixTemplate(opts.responsePrefix, opts.responsePrefixContext)
: opts.responsePrefix;
if (
effectivePrefix &&
text &&
text.trim() !== HEARTBEAT_TOKEN &&
!text.startsWith(effectivePrefix)
) {
text = `${effectivePrefix} ${text}`;
}
return { ...payload, text };
}