UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

277 lines (276 loc) 11 kB
import { c as normalizeOptionalLowercaseString } from "./string-coerce-CIXf7egm.js"; import { t as createLazyImportLoader } from "./lazy-promise-DGqyc4Y4.js"; import { n as normalizeAccountId } from "./account-id-CETVCrTz.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { i as normalizeChatChannelId } from "./ids-BVZRYG0I.js"; import { n as getBundledChannelPlugin } from "./bundled-0Ib5Dos0.js"; import { _ as resolveSessionAgentId } from "./agent-scope-DbtJyKUL.js"; import "./registry-COqNvmCg.js"; import "./message-channel-constants-2zSoJXQC.js"; import { n as normalizeMessageChannel } from "./message-channel-core-AaEWic-A.js"; import "./message-channel-BQrhwUEA.js"; import { t as normalizeChatType } from "./chat-type-CG0X_HJM.js"; import { a as getReplyPayloadMetadata, i as copyReplyPayloadMetadata } from "./reply-payload-DahKk-7n.js"; import { a as normalizeChannelId, n as getLoadedChannelPlugin } from "./registry-Cz6cv4VC.js"; import "./plugins-Cozj1Enf.js"; import { o as hasReplyPayloadContent } from "./payload-DYE7bQ2k.js"; import { r as normalizeReplyPayloadOutcome } from "./normalize-reply-C1iRjnEc.js"; import { r as resolveEffectiveMessagesConfig } from "./identity-L3YWE18q.js"; import { o as shouldSuppressReasoningPayload, r as formatBtwTextForExternalDelivery } from "./reply-payloads-OWAnL0iD.js"; import { t as buildOutboundSessionContext } from "./session-context-CpdGMrv6.js"; import { r as createChannelReplyTransform } from "./reply-transform-C8Hh1eV1.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-Seugmqy-.js")); const BLOCK_REPLY_COMPLETION_RETENTION = { idPrefix: "block-reply:v1:", maxAgeMs: 864e5, maxEntries: 2e3 }; function loadDeliverRuntime() { return messageRuntimeLoader.load(); } function replyDeliverySourceMatchesRoute(params) { return (normalizeMessageChannel(params.source.channel) ?? normalizeOptionalLowercaseString(params.source.channel)) === (normalizeMessageChannel(params.channel) ?? normalizeOptionalLowercaseString(params.channel)) && normalizeAccountId(params.source.accountId) === normalizeAccountId(params.accountId) && normalizeChatType(params.payloadDelivery.chatType ?? void 0) === normalizeChatType(params.routeDelivery.chatType ?? void 0); } function summarizeVisibleRouteReplyDelivery(results) { let delivered = false; let lastVisibleMessageId; for (let index = results.length - 1; index >= 0; index -= 1) { const result = results[index]; if (!result) continue; const messageId = result.messageId?.trim().toLowerCase(); if (messageId === "skipped" || messageId === "suppressed") continue; if (!delivered) { delivered = true; if (!messageId) lastVisibleMessageId = result.messageId; } if (messageId && messageId !== "unknown" && messageId !== "ok") return { delivered: true, messageId: result.messageId }; } return { delivered, messageId: delivered ? lastVisibleMessageId : void 0 }; } /** * 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, delivered: false, suppressed: true, reason: "reasoning_payload_not_external" }; 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 = resolveSessionAgentId({ sessionKey: params.sessionKey, config: cfg, fallbackAgentId: params.agentId }); const responsePrefix = resolveEffectiveMessagesConfig(cfg, resolvedAgentId, { channel: normalizedChannel, accountId }).responsePrefix; const transformReplyPayload = createChannelReplyTransform({ messaging, cfg, accountId }); const normalization = normalizeReplyPayloadOutcome(payload, { responsePrefix, responsePrefixContext: params.responsePrefixContext, transformReplyPayload }); if (normalization.kind === "suppress") { if (normalization.reason === "channel_transform") return { ok: true, delivered: false, suppressed: true, reason: normalization.reason }; return { ok: true, delivered: false }; } const normalized = normalization.payload; 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, delivered: false }; if (channel === "webchat") return { ok: false, delivered: false, error: "Webchat routing not supported for queued replies" }; if (!channelId) return { ok: false, delivered: false, error: `Unknown channel: ${String(channel)}` }; if (abortSignal?.aborted) return { ok: false, delivered: false, error: "Reply routing aborted" }; const payloadMetadata = getReplyPayloadMetadata(normalized); const payloadReplyDelivery = payloadMetadata?.replyDelivery; const replyDelivery = (payloadReplyDelivery && params.replyDelivery && payloadMetadata.replyDeliverySource ? replyDeliverySourceMatchesRoute({ source: payloadMetadata.replyDeliverySource, payloadDelivery: payloadReplyDelivery, routeDelivery: params.replyDelivery, channel: channelId, accountId }) : false) ? payloadReplyDelivery : params.replyDelivery ?? payloadReplyDelivery; const replyTransport = threading?.resolveReplyTransport?.({ cfg, accountId, threadId, replyToId, replyToIsExplicit: Boolean(payloadMetadata?.replyToIdExplicit || normalized.replyToTag || normalized.replyToCurrent), replyToCurrent: normalized.replyToCurrent, replyDelivery }) ?? null; const resolvedReplyToId = replyTransport?.replyToId === null ? void 0 : replyTransport?.replyToId ?? replyToId ?? void 0; const resolvedThreadId = replyTransport && Object.hasOwn(replyTransport, "threadId") ? replyTransport.threadId ?? null : threadId ?? null; const deliveryPayload = copyReplyPayloadMetadata(normalized, { ...externalPayload, replyToId: resolvedReplyToId }); try { const { durableMessageBatchMayHaveReachedRecipient, sendDurableMessageBatchCore } = 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 sendDurableMessageBatchCore({ cfg, channel: channelId, to, accountId: accountId ?? void 0, payloads: [deliveryPayload], 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, ...params.deliveryIntentId ? { deliveryIntentId: params.deliveryIntentId, reusePendingDeliveryIntent: true, completionRetention: BLOCK_REPLY_COMPLETION_RETENTION, durability: "required" } : {}, 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") throw send.error; if (send.status === "partial_failed") { const delivery = summarizeVisibleRouteReplyDelivery(send.results); return { ok: false, delivered: delivery.delivered, error: `Failed to route reply to ${channel}: ${formatErrorMessage(send.error)}`, messageId: delivery.messageId }; } if (send.status === "suppressed" && (send.reason === "cancelled_by_message_sending_hook" || send.reason === "adapter_returned_no_send" || send.reason === "cancelled_by_reply_payload_sending_hook" || send.reason === "empty_after_message_sending_hook" || send.reason === "empty_after_reply_payload_sending_hook")) return { ok: true, delivered: false, suppressed: true, reason: send.reason }; if (send.status === "suppressed" && durableMessageBatchMayHaveReachedRecipient(send)) return { ok: true, delivered: true, ambiguous: true, reason: "adapter_returned_no_identity" }; const delivery = summarizeVisibleRouteReplyDelivery(send.status === "sent" ? send.results : []); return { ok: true, delivered: delivery.delivered, messageId: delivery.messageId }; } catch (err) { return { ok: false, delivered: 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 };