openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
349 lines (348 loc) • 15.4 kB
JavaScript
import { F as resolveTimerTimeoutMs } from "./number-coercion-CuVhdCBs.mjs";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CJrzFzqU.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-C8RatpKO.mjs";
import "./message-channel-j-k-Nup3.mjs";
import { a as projectOutboundPayloadPlanForDelivery, s as projectOutboundPayloadPlanForMirror, t as createOutboundPayloadPlan } from "./payloads-RF8yW6z7.mjs";
import { s as resolveOutboundDurableFinalDeliverySupport } from "./deliver-prepare-eSoKByIL.mjs";
import { r as normalizeOutboundReplyFacts } from "./reply-policy-D40tu-98.mjs";
import { c as findOutboundHandoffRejectedError, s as assertOutboundHandoffCurrent } from "./delivery-queue-reconciliation-D24sjkey.mjs";
import { t as buildOutboundSessionContext } from "./session-context-Kov7ff_A.mjs";
import { i as serializeDurableMessagePayloadOutcomes, n as sendDurableMessageBatchCore, t as durableMessageBatchMayHaveReachedRecipient } from "./send-CqAjSzNt.mjs";
import { n as deriveDurableFinalDeliveryRequirementsForBatch } from "./capabilities-CZr1Vdri.mjs";
import "./deliver-ByaMG-sT.mjs";
import { u as createChannelPartialDeliveryError } from "./live-TWzvpgza.mjs";
import "./runtime-BZREclVS.mjs";
import { n as normalizePollInput } from "./polls-C-v11_tu.mjs";
import { r as resolveMessageChannelSelection } from "./channel-selection-H42iGAM4.mjs";
import { a as resolveOutboundTarget } from "./targets-BUerMF9S.mjs";
//#region src/infra/outbound/message-gateway-options.ts
/** Normalizes outbound gateway options and fills CLI defaults. */
function resolveOutboundMessageGatewayOptions(gateway) {
const clientName = gateway?.clientName ?? GATEWAY_CLIENT_NAMES.CLI;
const mode = gateway?.mode ?? GATEWAY_CLIENT_MODES.CLI;
const url = mode === GATEWAY_CLIENT_MODES.BACKEND || clientName === GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT ? void 0 : gateway?.url;
return {
config: gateway?.config,
localPortOverride: gateway?.localPortOverride,
ignoreEnvUrlOverride: gateway?.ignoreEnvUrlOverride,
tlsFingerprint: gateway?.tlsFingerprint,
url,
token: gateway?.token,
timeoutMs: resolveTimerTimeoutMs(gateway?.timeoutMs, 1e4),
clientName,
clientDisplayName: gateway?.clientDisplayName,
mode
};
}
//#endregion
//#region src/infra/outbound/message.ts
const SEND_BUFFER_MEDIA_URL = "buffer://message-send/attachment";
const loadMessageConfigRuntime = createLazyRuntimeModule(() => import("./message.config.runtime.js"));
const loadMessageGatewayRuntime = createLazyRuntimeModule(() => import("./message.gateway.runtime.js"));
function normalizeMessagePollDeliveryResult(result) {
const { channelId, conversationId, ...delivery } = result;
return {
...delivery,
...channelId ? { target: {
kind: "channel",
id: channelId
} } : conversationId ? { target: {
kind: "conversation",
id: conversationId
} } : {}
};
}
function assertPollOptionSupport(params) {
if (typeof params.durationSeconds === "number" && params.outbound.supportsPollDurationSeconds !== true) throw new Error(`durationSeconds is not supported for ${params.channel} polls`);
if (typeof params.isAnonymous === "boolean" && params.outbound.supportsAnonymousPolls !== true) throw new Error(`isAnonymous is not supported for ${params.channel} polls`);
}
async function assertRequiredMessageSendDurability(params) {
const support = await resolveOutboundDurableFinalDeliverySupport({
cfg: params.cfg,
agentId: params.agentId,
channel: params.channel,
requirements: deriveDurableFinalDeliveryRequirementsForBatch({
...params,
reconcileUnknownSend: true
})
});
if (support.ok) return;
const suffix = support.reason === "capability_mismatch" && support.capability ? `missing ${support.capability}` : support.reason;
throw new Error(`Required durable message send is unsupported for ${params.channel}: ${suffix}. Use queuePolicy:"best_effort" for best-effort delivery, omit bestEffort:false in message-tool calls, or use a channel with required durable delivery support.`);
}
async function callMessageGateway(params) {
const gateway = resolveOutboundMessageGatewayOptions(params.gateway);
const agentRuntimeIdentityToken = params.gateway?.request ? void 0 : await params.gateway?.resolveAgentRuntimeIdentityToken?.();
await params.onPlatformSendDispatch?.();
assertOutboundHandoffCurrent(params.assertDirectAdapterHandoff);
if (params.gateway?.request) return await params.gateway.request({
method: params.method,
params: params.params,
timeoutMs: gateway.timeoutMs
});
const { callGatewayLeastPrivilege } = await loadMessageGatewayRuntime();
return await callGatewayLeastPrivilege({
...gateway,
method: params.method,
params: params.params,
agentRuntimeIdentityToken
});
}
async function resolveMessageConfig(cfg) {
if (cfg) return cfg;
const { getRuntimeConfig } = await loadMessageConfigRuntime();
return getRuntimeConfig();
}
async function resolveGatewayIdempotencyKey(idempotencyKey) {
if (idempotencyKey) return idempotencyKey;
const { randomIdempotencyKey } = await loadMessageGatewayRuntime();
return randomIdempotencyKey();
}
async function sendMessage(params) {
const cfg = await resolveMessageConfig(params.cfg);
const reply = normalizeOutboundReplyFacts({
reply: params.reply,
replyToId: params.replyToId
});
const { channel, plugin } = params.preparedPlugin ? {
channel: params.preparedPlugin.id,
plugin: params.preparedPlugin
} : await resolveMessageChannelSelection({
cfg,
channel: params.channel
});
const deliveryMode = plugin.outbound?.deliveryMode ?? "direct";
const hasRealMediaSource = [params.mediaUrl, ...params.mediaUrls ?? []].filter((source) => Boolean(source)).some((source) => source !== SEND_BUFFER_MEDIA_URL);
const shouldForwardBuffer = deliveryMode === "gateway" && Boolean(params.buffer) && !hasRealMediaSource;
const mediaUrl = params.mediaUrl ?? (shouldForwardBuffer ? SEND_BUFFER_MEDIA_URL : void 0);
const mediaUrls = params.mediaUrls ?? (shouldForwardBuffer ? [SEND_BUFFER_MEDIA_URL] : void 0);
const outboundPayloads = params.payloads && params.payloads.length > 0 ? params.payloads : [{
text: params.content,
mediaUrl,
mediaUrls,
audioAsVoice: params.asVoice === true
}];
const outboundPlan = createOutboundPayloadPlan(outboundPayloads);
const normalizedPayloads = projectOutboundPayloadPlanForDelivery(outboundPlan);
const mirrorProjection = projectOutboundPayloadPlanForMirror(outboundPlan);
const mirrorText = mirrorProjection.text;
const mirrorMediaUrls = mirrorProjection.mediaUrls;
const primaryMediaUrl = mirrorMediaUrls[0] ?? mediaUrl ?? null;
const baseResult = {
channel,
to: params.to,
via: deliveryMode === "gateway" ? "gateway" : "direct",
mediaUrl: primaryMediaUrl,
mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : void 0
};
if (params.dryRun) return {
...baseResult,
dryRun: true
};
if (deliveryMode !== "gateway" || params.gatewayOwnedDelivery === true) {
const outboundChannel = channel;
const resolvedTarget = resolveOutboundTarget({
channel: outboundChannel,
plugin,
to: params.to,
cfg,
accountId: params.accountId,
mode: "explicit"
});
if (!resolvedTarget.ok) throw resolvedTarget.error;
const outboundSession = buildOutboundSessionContext({
cfg,
agentId: params.agentId,
sessionKey: params.requesterSessionKey ?? params.mirror?.sessionKey,
conversationType: params.conversationType,
requesterAccountId: params.requesterAccountId ?? params.accountId,
requesterSenderId: params.requesterSenderId,
requesterSenderName: params.requesterSenderName,
requesterSenderUsername: params.requesterSenderUsername,
requesterSenderE164: params.requesterSenderE164
});
const requireUnknownSendReconciliation = params.requireUnknownSendReconciliation ?? params.queuePolicy === "required";
if (requireUnknownSendReconciliation) await assertRequiredMessageSendDurability({
cfg,
agentId: params.agentId,
channel: outboundChannel,
payloads: normalizedPayloads,
replyToId: reply?.replyToId,
threadId: params.threadId,
silent: params.silent
});
const send = await sendDurableMessageBatchCore({
cfg,
channel: outboundChannel,
to: resolvedTarget.to,
session: outboundSession,
runId: params.runId,
executionIdentityToken: params.executionIdentityToken,
accountId: params.accountId,
conversationReadOrigin: params.conversationReadOrigin,
payloads: normalizedPayloads,
reply,
threadId: params.threadId,
gifPlayback: params.gifPlayback,
forceDocument: params.forceDocument,
deps: params.deps,
bestEffort: params.bestEffort,
...requireUnknownSendReconciliation ? { requireUnknownSendReconciliation: true } : {},
durability: params.bestEffort || params.queuePolicy === "best_effort" ? "best_effort" : "required",
signal: params.abortSignal,
silent: params.silent,
mediaAccess: params.mediaAccess,
formatting: params.parseMode ? { parseMode: params.parseMode } : void 0,
preparedMessageId: params.preparedMessageId,
deliveryIntentId: params.deliveryIntentId,
deliveryCompletion: params.deliveryCompletion,
reusePendingDeliveryIntent: params.reusePendingDeliveryIntent,
deliveryRetryOwner: params.deliveryRetryOwner,
completionRetention: params.completionRetention,
...params.onDeliveryIntent ? { onDeliveryIntent: params.onDeliveryIntent } : {},
...params.onDeliveryAttempt ? { onDeliveryAttempt: params.onDeliveryAttempt } : {},
...params.onDeliveryResult ? { onDeliveryResult: params.onDeliveryResult } : {},
...params.onPlatformSendDispatch ? { onPlatformSendDispatch: params.onPlatformSendDispatch } : {},
assertDirectAdapterHandoff: params.assertDirectAdapterHandoff,
skipQueue: params.skipQueue,
...params.onDeliveredPayload ? { onDeliveredPayload: params.onDeliveredPayload } : {},
mirror: params.mirror ? {
...params.mirror,
text: mirrorText || params.content,
mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : void 0,
idempotencyKey: params.mirror.idempotencyKey ?? params.idempotencyKey
} : void 0
}, params.conversationDeliveryTarget);
const sendMayHaveReachedRecipient = durableMessageBatchMayHaveReachedRecipient(send);
const handoffRejection = send.status === "failed" && !sendMayHaveReachedRecipient ? send.payloadOutcomes?.map((outcome) => outcome.status === "failed" ? findOutboundHandoffRejectedError(outcome.error) : void 0).find((error) => error !== void 0) ?? findOutboundHandoffRejectedError(send.error) : void 0;
if (handoffRejection) throw handoffRejection;
if (!params.bestEffort && params.gateway?.clientName !== GATEWAY_CLIENT_NAMES.CLI && (send.status === "failed" || send.status === "partial_failed")) {
if (send.status === "partial_failed") throw createChannelPartialDeliveryError(send.error, {
messageIds: send.results.map((result) => result.messageId),
receipt: send.receipt,
visibleReplySent: true
});
throw send.error;
}
const results = send.status === "sent" || send.status === "partial_failed" ? send.results : [];
const payloadOutcomes = serializeDurableMessagePayloadOutcomes(send.payloadOutcomes);
const sentBeforeError = send.status !== "sent" && sendMayHaveReachedRecipient;
return {
...baseResult,
result: results.at(-1),
deliveryStatus: send.status,
...send.status === "suppressed" ? { suppressionReason: send.reason } : {},
...send.status === "failed" || send.status === "partial_failed" ? { error: formatErrorMessage(send.error) } : {},
...sentBeforeError ? { sentBeforeError: true } : {},
...payloadOutcomes ? { payloadOutcomes } : {}
};
}
const result = await callMessageGateway({
gateway: params.gateway,
method: "send",
onPlatformSendDispatch: params.onPlatformSendDispatch,
assertDirectAdapterHandoff: params.assertDirectAdapterHandoff,
params: {
to: params.to,
message: params.content,
mediaUrl,
mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : mediaUrls,
buffer: shouldForwardBuffer ? params.buffer : void 0,
filename: shouldForwardBuffer ? params.filename : void 0,
contentType: shouldForwardBuffer ? params.contentType : void 0,
asVoice: params.asVoice,
gifPlayback: params.gifPlayback,
accountId: params.accountId,
agentId: params.agentId,
channel,
replyToId: reply?.replyToId,
threadId: params.threadId != null ? String(params.threadId) : void 0,
forceDocument: params.forceDocument,
silent: params.silent,
parseMode: params.parseMode,
sessionKey: params.mirror?.sessionKey,
idempotencyKey: await resolveGatewayIdempotencyKey(params.idempotencyKey)
}
});
return {
...baseResult,
result
};
}
async function sendPoll(params) {
const cfg = await resolveMessageConfig(params.cfg);
const { channel, plugin } = params.preparedPlugin ? {
channel: params.preparedPlugin.id,
plugin: params.preparedPlugin
} : await resolveMessageChannelSelection({
cfg,
channel: params.channel
});
const outbound = plugin.outbound;
if (!outbound?.sendPoll) throw new Error(`Unsupported poll channel: ${channel}`);
const deliveryMode = outbound.deliveryMode ?? "direct";
const normalized = normalizePollInput(params, outbound.pollMaxOptions ? { maxOptions: outbound.pollMaxOptions } : void 0);
const buildResult = (delivery) => ({
channel,
to: params.to,
question: normalized.question,
options: normalized.options,
maxSelections: normalized.maxSelections,
durationSeconds: normalized.durationSeconds ?? null,
durationHours: normalized.durationHours ?? null,
via: deliveryMode === "gateway" ? "gateway" : "direct",
...delivery
});
if (params.dryRun) return buildResult({ dryRun: true });
assertPollOptionSupport({
channel,
outbound,
durationSeconds: params.durationSeconds,
isAnonymous: params.isAnonymous
});
if (deliveryMode !== "gateway" || params.gatewayOwnedDelivery === true) {
const resolvedTarget = resolveOutboundTarget({
channel,
plugin,
to: params.to,
cfg,
accountId: params.accountId,
mode: "explicit"
});
if (!resolvedTarget.ok) throw resolvedTarget.error;
params.assertDirectAdapterHandoff?.();
return buildResult({ result: normalizeMessagePollDeliveryResult(await outbound.sendPoll({
cfg,
to: resolvedTarget.to,
poll: normalized,
content: params.content,
accountId: params.accountId,
threadId: params.threadId,
silent: params.silent,
isAnonymous: params.isAnonymous,
sessionKey: params.sessionKey,
inboundEventKind: params.inboundEventKind,
onPlatformSendDispatch: params.onPlatformSendDispatch,
assertDirectAdapterHandoff: params.assertDirectAdapterHandoff
})) });
}
return buildResult({ result: normalizeMessagePollDeliveryResult(await callMessageGateway({
gateway: params.gateway,
method: "poll",
onPlatformSendDispatch: params.onPlatformSendDispatch,
assertDirectAdapterHandoff: params.assertDirectAdapterHandoff,
params: {
...normalized,
to: params.to,
threadId: params.threadId,
silent: params.silent,
isAnonymous: params.isAnonymous,
channel,
accountId: params.accountId,
idempotencyKey: await resolveGatewayIdempotencyKey(params.idempotencyKey)
}
})) });
}
//#endregion
export { sendPoll as n, resolveOutboundMessageGatewayOptions as r, sendMessage as t };