refactor: unify inline directives and media fetch
This commit is contained in:
@@ -33,9 +33,8 @@ import {
|
||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import type { OriginatingChannelType, TemplateContext } from "../templating.js";
|
||||
import { normalizeVerboseLevel, type VerboseLevel } from "../thinking.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import { parseAudioTag } from "./audio-tags.js";
|
||||
import {
|
||||
createAudioAsVoiceBuffer,
|
||||
createBlockReplyPipeline,
|
||||
@@ -48,6 +47,7 @@ import {
|
||||
type QueueSettings,
|
||||
scheduleFollowupDrain,
|
||||
} from "./queue.js";
|
||||
import { parseReplyDirectives } from "./reply-directives.js";
|
||||
import {
|
||||
applyReplyTagsToPayload,
|
||||
applyReplyThreading,
|
||||
@@ -550,28 +550,39 @@ export async function runReplyAgent(params: {
|
||||
!payload.audioAsVoice
|
||||
)
|
||||
return;
|
||||
const audioTagResult = parseAudioTag(taggedPayload.text);
|
||||
const cleaned = audioTagResult.text || undefined;
|
||||
const parsed = parseReplyDirectives(
|
||||
taggedPayload.text ?? "",
|
||||
{
|
||||
currentMessageId: sessionCtx.MessageSid,
|
||||
silentToken: SILENT_REPLY_TOKEN,
|
||||
},
|
||||
);
|
||||
const cleaned = parsed.text || undefined;
|
||||
const hasMedia =
|
||||
Boolean(taggedPayload.mediaUrl) ||
|
||||
(taggedPayload.mediaUrls?.length ?? 0) > 0;
|
||||
// Skip empty payloads unless they have audioAsVoice flag (need to track it)
|
||||
if (!cleaned && !hasMedia && !payload.audioAsVoice) return;
|
||||
if (
|
||||
isSilentReplyText(cleaned, SILENT_REPLY_TOKEN) &&
|
||||
!hasMedia
|
||||
!cleaned &&
|
||||
!hasMedia &&
|
||||
!payload.audioAsVoice &&
|
||||
!parsed.audioAsVoice
|
||||
)
|
||||
return;
|
||||
if (parsed.isSilent && !hasMedia) return;
|
||||
|
||||
const blockPayload: ReplyPayload = applyReplyToMode({
|
||||
...taggedPayload,
|
||||
text: cleaned,
|
||||
audioAsVoice:
|
||||
audioTagResult.audioAsVoice || payload.audioAsVoice,
|
||||
audioAsVoice: parsed.audioAsVoice || payload.audioAsVoice,
|
||||
replyToId: taggedPayload.replyToId ?? parsed.replyToId,
|
||||
replyToTag: taggedPayload.replyToTag || parsed.replyToTag,
|
||||
replyToCurrent:
|
||||
taggedPayload.replyToCurrent || parsed.replyToCurrent,
|
||||
});
|
||||
|
||||
void typingSignals
|
||||
.signalTextDelta(taggedPayload.text)
|
||||
.signalTextDelta(cleaned ?? taggedPayload.text)
|
||||
.catch((err) => {
|
||||
logVerbose(
|
||||
`block reply typing signal failed: ${String(err)}`,
|
||||
@@ -735,11 +746,21 @@ export async function runReplyAgent(params: {
|
||||
currentMessageId: sessionCtx.MessageSid,
|
||||
})
|
||||
.map((payload) => {
|
||||
const audioTagResult = parseAudioTag(payload.text);
|
||||
const parsed = parseReplyDirectives(payload.text ?? "", {
|
||||
currentMessageId: sessionCtx.MessageSid,
|
||||
silentToken: SILENT_REPLY_TOKEN,
|
||||
});
|
||||
const mediaUrls = payload.mediaUrls ?? parsed.mediaUrls;
|
||||
const mediaUrl = payload.mediaUrl ?? parsed.mediaUrl ?? mediaUrls?.[0];
|
||||
return {
|
||||
...payload,
|
||||
text: audioTagResult.text ? audioTagResult.text : undefined,
|
||||
audioAsVoice: audioTagResult.audioAsVoice,
|
||||
text: parsed.text ? parsed.text : undefined,
|
||||
mediaUrls,
|
||||
mediaUrl,
|
||||
replyToId: payload.replyToId ?? parsed.replyToId,
|
||||
replyToTag: payload.replyToTag || parsed.replyToTag,
|
||||
replyToCurrent: payload.replyToCurrent || parsed.replyToCurrent,
|
||||
audioAsVoice: payload.audioAsVoice || parsed.audioAsVoice,
|
||||
};
|
||||
})
|
||||
.filter(isRenderablePayload);
|
||||
@@ -775,8 +796,7 @@ export async function runReplyAgent(params: {
|
||||
|
||||
const shouldSignalTyping = replyPayloads.some((payload) => {
|
||||
const trimmed = payload.text?.trim();
|
||||
if (trimmed && !isSilentReplyText(trimmed, SILENT_REPLY_TOKEN))
|
||||
return true;
|
||||
if (trimmed) return true;
|
||||
if (payload.mediaUrl) return true;
|
||||
if (payload.mediaUrls && payload.mediaUrls.length > 0) return true;
|
||||
return false;
|
||||
|
||||
49
src/auto-reply/reply/reply-directives.ts
Normal file
49
src/auto-reply/reply/reply-directives.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { splitMediaFromOutput } from "../../media/parse.js";
|
||||
import { parseInlineDirectives } from "../../utils/directive-tags.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
|
||||
export type ReplyDirectiveParseResult = {
|
||||
text: string;
|
||||
mediaUrls?: string[];
|
||||
mediaUrl?: string;
|
||||
replyToId?: string;
|
||||
replyToCurrent: boolean;
|
||||
replyToTag: boolean;
|
||||
audioAsVoice?: boolean;
|
||||
isSilent: boolean;
|
||||
};
|
||||
|
||||
export function parseReplyDirectives(
|
||||
raw: string,
|
||||
options: { currentMessageId?: string; silentToken?: string } = {},
|
||||
): ReplyDirectiveParseResult {
|
||||
const split = splitMediaFromOutput(raw);
|
||||
let text = split.text ?? "";
|
||||
|
||||
const replyParsed = parseInlineDirectives(text, {
|
||||
currentMessageId: options.currentMessageId,
|
||||
stripAudioTag: false,
|
||||
stripReplyTags: true,
|
||||
});
|
||||
|
||||
if (replyParsed.hasReplyTag) {
|
||||
text = replyParsed.text;
|
||||
}
|
||||
|
||||
const silentToken = options.silentToken ?? SILENT_REPLY_TOKEN;
|
||||
const isSilent = isSilentReplyText(text, silentToken);
|
||||
if (isSilent) {
|
||||
text = "";
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
mediaUrls: split.mediaUrls,
|
||||
mediaUrl: split.mediaUrl,
|
||||
replyToId: replyParsed.replyToId,
|
||||
replyToCurrent: replyParsed.replyToCurrent,
|
||||
replyToTag: replyParsed.hasReplyTag,
|
||||
audioAsVoice: split.audioAsVoice,
|
||||
isSilent,
|
||||
};
|
||||
}
|
||||
@@ -10,8 +10,23 @@ export function applyReplyTagsToPayload(
|
||||
payload: ReplyPayload,
|
||||
currentMessageId?: string,
|
||||
): ReplyPayload {
|
||||
if (typeof payload.text !== "string") return payload;
|
||||
const { cleaned, replyToId, hasTag } = extractReplyToTag(
|
||||
if (typeof payload.text !== "string") {
|
||||
if (!payload.replyToCurrent || payload.replyToId) return payload;
|
||||
return {
|
||||
...payload,
|
||||
replyToId: currentMessageId?.trim() || undefined,
|
||||
};
|
||||
}
|
||||
const shouldParseTags = payload.text.includes("[[");
|
||||
if (!shouldParseTags) {
|
||||
if (!payload.replyToCurrent || payload.replyToId) return payload;
|
||||
return {
|
||||
...payload,
|
||||
replyToId: currentMessageId?.trim() || undefined,
|
||||
replyToTag: payload.replyToTag ?? true,
|
||||
};
|
||||
}
|
||||
const { cleaned, replyToId, replyToCurrent, hasTag } = extractReplyToTag(
|
||||
payload.text,
|
||||
currentMessageId,
|
||||
);
|
||||
@@ -20,6 +35,7 @@ export function applyReplyTagsToPayload(
|
||||
text: cleaned ? cleaned : undefined,
|
||||
replyToId: replyToId ?? payload.replyToId,
|
||||
replyToTag: hasTag || payload.replyToTag,
|
||||
replyToCurrent: replyToCurrent || payload.replyToCurrent,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
const REPLY_TAG_RE =
|
||||
/\[\[\s*(?:reply_to_current|reply_to\s*:\s*([^\]\n]+))\s*\]\]/gi;
|
||||
|
||||
function normalizeReplyText(text: string) {
|
||||
return text
|
||||
.replace(/[ \t]+/g, " ")
|
||||
.replace(/[ \t]*\n[ \t]*/g, "\n")
|
||||
.trim();
|
||||
}
|
||||
import { parseInlineDirectives } from "../../utils/directive-tags.js";
|
||||
|
||||
export function extractReplyToTag(
|
||||
text?: string,
|
||||
@@ -14,31 +6,17 @@ export function extractReplyToTag(
|
||||
): {
|
||||
cleaned: string;
|
||||
replyToId?: string;
|
||||
replyToCurrent: boolean;
|
||||
hasTag: boolean;
|
||||
} {
|
||||
if (!text) return { cleaned: "", hasTag: false };
|
||||
|
||||
let sawCurrent = false;
|
||||
let lastExplicitId: string | undefined;
|
||||
let hasTag = false;
|
||||
|
||||
const cleaned = normalizeReplyText(
|
||||
text.replace(REPLY_TAG_RE, (_full, idRaw: string | undefined) => {
|
||||
hasTag = true;
|
||||
if (idRaw === undefined) {
|
||||
sawCurrent = true;
|
||||
return " ";
|
||||
}
|
||||
|
||||
const id = idRaw.trim();
|
||||
if (id) lastExplicitId = id;
|
||||
return " ";
|
||||
}),
|
||||
);
|
||||
|
||||
const replyToId =
|
||||
lastExplicitId ??
|
||||
(sawCurrent ? currentMessageId?.trim() || undefined : undefined);
|
||||
|
||||
return { cleaned, replyToId, hasTag };
|
||||
const result = parseInlineDirectives(text, {
|
||||
currentMessageId,
|
||||
stripAudioTag: false,
|
||||
});
|
||||
return {
|
||||
cleaned: result.text,
|
||||
replyToId: result.replyToId,
|
||||
replyToCurrent: result.replyToCurrent,
|
||||
hasTag: result.hasReplyTag,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ export type ReplyPayload = {
|
||||
mediaUrls?: string[];
|
||||
replyToId?: string;
|
||||
replyToTag?: boolean;
|
||||
/** True when [[reply_to_current]] was present but not yet mapped to a message id. */
|
||||
replyToCurrent?: boolean;
|
||||
/** Send audio as voice message (bubble) instead of audio file. Defaults to false. */
|
||||
audioAsVoice?: boolean;
|
||||
isError?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user