openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
175 lines (174 loc) • 7.26 kB
JavaScript
import { s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as createLazyImportLoader } from "./lazy-promise-BONnzNfb.js";
import { r as normalizeChatChannelId } from "./ids-BVNPk-iM.js";
import { v as resolveSessionAgentId } from "./agent-scope-MrLta7Pq.js";
import { r as getBundledChannelPlugin } from "./bundled-DcYUmgzC.js";
import "./registry-9YoS2BJP.js";
import "./message-channel-constants-CgI32lqD.js";
import { a as normalizeChannelId, n as getLoadedChannelPlugin } from "./registry-MkxNn1Ue.js";
import "./plugins-t2ejWcVy.js";
import { i as normalizeMessageChannel } from "./message-channel-normalize-BLLf0ubu.js";
import "./message-channel-BiOeMu0l.js";
import { a as hasReplyPayloadContent } from "./payload-CZlThETZ.js";
import { r as resolveEffectiveMessagesConfig } from "./identity-Qcvvo6SY.js";
import { t as buildOutboundSessionContext } from "./session-context-Bz96-75Y.js";
import { t as normalizeReplyPayload } from "./normalize-reply-BJvr7poh.js";
import { a as shouldSuppressReasoningPayload, r as formatBtwTextForExternalDelivery } from "./reply-payloads-BuXISxei.js";
//#region src/auto-reply/reply/route-reply.ts
/**
* Provider-agnostic reply router.
*
* Routes replies to the originating channel based on OriginatingChannel/OriginatingTo
* instead of using the session's lastChannel. This ensures replies go back to the
* provider where the message originated, even when the main session is shared
* across multiple providers.
*/
const messageRuntimeLoader = createLazyImportLoader(() => import("./runtime-KXUVT2qQ.js"));
function loadDeliverRuntime() {
return messageRuntimeLoader.load();
}
/**
* Routes a reply payload to the specified channel.
*
* This function provides a unified interface for sending messages to any
* supported provider. It's used by the followup queue to route replies
* back to the originating channel when OriginatingChannel/OriginatingTo
* are set.
*/
async function routeReply(params) {
const { payload, channel, to, accountId, threadId, cfg, abortSignal } = params;
if (shouldSuppressReasoningPayload(payload)) return { ok: true };
const normalizedChannel = normalizeMessageChannel(channel);
const channelId = normalizeChannelId(channel) ?? normalizeOptionalLowercaseString(channel) ?? null;
const loadedPlugin = channelId ? getLoadedChannelPlugin(channelId) : void 0;
const bundledPlugin = channelId && !loadedPlugin ? getBundledChannelPlugin(channelId) : void 0;
const messaging = loadedPlugin?.messaging ?? bundledPlugin?.messaging;
const threading = loadedPlugin?.threading ?? bundledPlugin?.threading;
const resolvedAgentId = params.sessionKey ? resolveSessionAgentId({
sessionKey: params.sessionKey,
config: cfg
}) : void 0;
const normalized = normalizeReplyPayload(payload, {
responsePrefix: params.sessionKey ? resolveEffectiveMessagesConfig(cfg, resolvedAgentId ?? resolveSessionAgentId({ config: cfg }), {
channel: normalizedChannel,
accountId
}).responsePrefix : cfg.messages?.responsePrefix === "auto" ? void 0 : cfg.messages?.responsePrefix,
transformReplyPayload: messaging?.transformReplyPayload ? (nextPayload) => messaging.transformReplyPayload?.({
payload: nextPayload,
cfg,
accountId
}) ?? nextPayload : void 0
});
if (!normalized) return { ok: true };
const externalPayload = {
...normalized,
text: formatBtwTextForExternalDelivery(normalized)
};
const text = externalPayload.text ?? "";
let mediaUrls = [];
for (const url of externalPayload.mediaUrls ?? []) if (url) mediaUrls.push(url);
if (mediaUrls.length === 0 && externalPayload.mediaUrl) mediaUrls = [externalPayload.mediaUrl];
const replyToId = externalPayload.replyToId;
const hasChannelData = messaging?.hasStructuredReplyPayload?.({ payload: externalPayload });
if (!hasReplyPayloadContent({
...externalPayload,
text,
mediaUrls
}, { hasChannelData })) return { ok: true };
if (channel === "webchat") return {
ok: false,
error: "Webchat routing not supported for queued replies"
};
if (!channelId) return {
ok: false,
error: `Unknown channel: ${String(channel)}`
};
if (abortSignal?.aborted) return {
ok: false,
error: "Reply routing aborted"
};
const replyTransport = threading?.resolveReplyTransport?.({
cfg,
accountId,
threadId,
replyToId
}) ?? null;
const resolvedReplyToId = replyTransport?.replyToId ?? replyToId ?? void 0;
const resolvedThreadId = replyTransport && Object.hasOwn(replyTransport, "threadId") ? replyTransport.threadId ?? null : threadId ?? null;
try {
const { sendDurableMessageBatch } = await loadDeliverRuntime();
const outboundSession = buildOutboundSessionContext({
cfg,
agentId: resolvedAgentId,
sessionKey: params.sessionKey,
policySessionKey: params.policySessionKey,
conversationType: params.policyConversationType,
isGroup: params.policySessionKey || params.policyConversationType ? void 0 : params.isGroup,
requesterSenderId: params.requesterSenderId,
requesterSenderName: params.requesterSenderName,
requesterSenderUsername: params.requesterSenderUsername,
requesterSenderE164: params.requesterSenderE164
});
const send = await sendDurableMessageBatch({
cfg,
channel: channelId,
to,
accountId: accountId ?? void 0,
payloads: [externalPayload],
replyPayloadSendingHook: {
kind: params.replyKind,
channel: channelId,
...params.sessionKey ? { sessionKey: params.sessionKey } : {},
...params.runId ? { runId: params.runId } : {},
context: {
channelId,
...accountId ? { accountId } : {},
conversationId: to,
...params.sessionKey ? { sessionKey: params.sessionKey } : {},
...params.requesterSenderId ? { senderId: params.requesterSenderId } : {},
...params.runId ? { runId: params.runId } : {}
}
},
replyToId: resolvedReplyToId ?? null,
threadId: resolvedThreadId,
session: outboundSession,
signal: abortSignal,
mirror: params.mirror !== false && params.sessionKey ? {
sessionKey: params.sessionKey,
agentId: resolvedAgentId,
text,
mediaUrls,
...params.isGroup != null ? { isGroup: params.isGroup } : {},
...params.groupId ? { groupId: params.groupId } : {}
} : void 0
});
if (send.status === "failed" || send.status === "partial_failed") throw send.error;
if (send.status === "suppressed" && (send.reason === "cancelled_by_reply_payload_sending_hook" || send.reason === "empty_after_reply_payload_sending_hook")) return {
ok: true,
suppressed: true,
reason: send.reason
};
return {
ok: true,
messageId: (send.status === "sent" ? send.results : []).at(-1)?.messageId
};
} catch (err) {
return {
ok: false,
error: `Failed to route reply to ${channel}: ${formatErrorMessage(err)}`
};
}
}
/**
* Checks if a channel type is routable via routeReply.
*
* Some channels (webchat) require special handling and cannot be routed through
* this generic interface.
*/
function isRoutableChannel(channel) {
if (!channel || channel === "webchat") return false;
return normalizeChatChannelId(channel) !== null || normalizeChannelId(channel) !== null;
}
//#endregion
export { routeReply as n, isRoutableChannel as t };