UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

511 lines (510 loc) 19.2 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, l as normalizeOptionalStringifiedId } from "./string-coerce-mnp54Vah.js"; import { p as resolvePayloadMediaUrls, w as createReplyToFanout, x as sendTextMediaPayload, y as sendPayloadMediaSequenceOrFallback } from "./reply-payload-D-VMfpYI.js"; import { n as retryAsync, t as resolveRetryConfig } from "./retry-BSIArBBz.js"; import { n as resolveOutboundSendDep } from "./send-deps-DjbvQHZ4.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import "./channel-outbound-B3_Zy-kG.js"; import { i as createAttachedChannelResultAdapter, t as attachChannelToResult } from "./channel-send-result-Dn_C6AJS.js"; import "./retry-runtime-DdapUPJ5.js"; import { s as resolveDiscordAccount } from "./accounts-BI_6uJlY.js"; import { p as DiscordError } from "./discord-DdqXEGEm.js"; import { t as parseDiscordRetryAfterBodySeconds } from "./retry-after-67N9bXIn.js"; import { t as chunkDiscordTextWithMode } from "./chunk-D_QII2-W.js"; import { i as normalizeDiscordOutboundTarget } from "./normalize-SUovoNvy.js"; import { t as createDiscordSendReceipt } from "./send.receipt-CUG417Vx.js"; import { s as readDiscordComponentSpec } from "./components-CDHzCfc9.js"; import { n as notifyDiscordInboundEventOutboundPayloadSuccess } from "./inbound-event-delivery-BI9WnEey.js"; //#region extensions/discord/src/delivery-retry.ts const DISCORD_DELIVERY_RETRY_DEFAULTS = { attempts: 3, minDelayMs: 1e3, maxDelayMs: 3e4, jitter: 0 }; function isRetryableDiscordDeliveryError(err) { if (err instanceof DiscordError) return false; const status = err.status ?? err.statusCode; return status === 429 || status !== void 0 && status >= 500; } function getDiscordDeliveryRetryAfterMs(err) { if (!err || typeof err !== "object") return; const retryAfterSeconds = "retryAfter" in err ? parseDiscordRetryAfterBodySeconds(err.retryAfter) : void 0; if (retryAfterSeconds !== void 0) return retryAfterSeconds * 1e3; const retryAfterRaw = err.headers?.["retry-after"]; if (!retryAfterRaw) return; const headerSeconds = parseDiscordRetryAfterBodySeconds(retryAfterRaw); return headerSeconds === void 0 ? void 0 : headerSeconds * 1e3; } async function withDiscordDeliveryRetry(params) { const retryConfig = resolveRetryConfig(DISCORD_DELIVERY_RETRY_DEFAULTS, resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId }).config.retry); return await retryAsync(params.fn, { ...retryConfig, shouldRetry: (err) => isRetryableDiscordDeliveryError(err), retryAfterMs: getDiscordDeliveryRetryAfterMs }); } //#endregion //#region extensions/discord/src/media-detection.ts const DISCORD_VIDEO_MEDIA_EXTENSIONS = new Set([ ".avi", ".m4v", ".mkv", ".mov", ".mp4", ".webm" ]); function normalizeMediaPathForExtension(mediaUrl) { const trimmed = mediaUrl.trim(); if (!trimmed) return ""; try { return normalizeLowercaseStringOrEmpty(new URL(trimmed).pathname); } catch { const withoutHash = trimmed.split("#", 1)[0] ?? trimmed; return normalizeLowercaseStringOrEmpty(withoutHash.split("?", 1)[0] ?? withoutHash); } } function isLikelyDiscordVideoMedia(mediaUrl) { const normalized = normalizeMediaPathForExtension(mediaUrl); for (const ext of DISCORD_VIDEO_MEDIA_EXTENSIONS) if (normalized.endsWith(ext)) return true; return false; } //#endregion //#region extensions/discord/src/outbound-approval.ts function hasApprovalChannelData(payload) { const channelData = payload.channelData; if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) return false; return Boolean(channelData.execApproval); } function neutralizeDiscordApprovalMentions(value) { return value.replace(/@everyone/gi, "@​everyone").replace(/@here/gi, "@​here").replace(/<@/g, "<@​").replace(/<#/g, "<#​"); } function normalizeDiscordApprovalPayload(payload) { return hasApprovalChannelData(payload) && payload.text ? { ...payload, text: neutralizeDiscordApprovalMentions(payload.text) } : payload; } //#endregion //#region extensions/discord/src/outbound-components.ts let discordComponentSendPromise; let discordSharedInteractivePromise; async function sendDiscordComponentMessageLazy(...args) { discordComponentSendPromise ??= import("./send.components-YzVnfT3x.js").then((module) => module.sendDiscordComponentMessage); return await (await discordComponentSendPromise)(...args); } function loadDiscordSharedInteractive() { discordSharedInteractivePromise ??= import("./shared-interactive-CTqnlc24.js"); return discordSharedInteractivePromise; } function addPayloadTextFallback(spec, payload) { return spec.text ? spec : { ...spec, text: payload.text?.trim() ? payload.text : void 0 }; } async function buildDiscordPresentationPayload(params) { const componentSpec = (await loadDiscordSharedInteractive()).buildDiscordPresentationComponents(params.presentation); if (!componentSpec) return null; return { ...params.payload, channelData: { ...params.payload.channelData, discord: { ...params.payload.channelData?.discord, presentationComponents: componentSpec } } }; } async function resolveDiscordComponentSpec(payload) { const discordData = payload.channelData?.discord; const rawComponentSpec = discordData?.presentationComponents ?? (discordData?.components && typeof discordData.components === "object" && !Array.isArray(discordData.components) ? readDiscordComponentSpec(discordData.components) : null); if (rawComponentSpec) return addPayloadTextFallback(rawComponentSpec, payload); if (!payload.interactive) return; const interactiveSpec = (await loadDiscordSharedInteractive()).buildDiscordInteractiveComponents(payload.interactive); return interactiveSpec ? addPayloadTextFallback(interactiveSpec, payload) : void 0; } //#endregion //#region extensions/discord/src/outbound-send-context.ts let discordSendRuntimePromise; async function loadDiscordSendRuntime() { discordSendRuntimePromise ??= import("./send-B-l8DwLG.js"); return await discordSendRuntimePromise; } function resolveDiscordOutboundTarget(params) { if (params.threadId == null) return params.to; const threadId = normalizeOptionalStringifiedId(params.threadId) ?? ""; if (!threadId) return params.to; return `channel:${threadId}`; } function resolveDiscordFormattingOptions(ctx) { const formatting = ctx.formatting; return { textLimit: formatting?.textLimit, maxLinesPerMessage: formatting?.maxLinesPerMessage, tableMode: formatting?.tableMode, chunkMode: formatting?.chunkMode }; } async function createDiscordPayloadSendContext(ctx) { const runtime = await loadDiscordSendRuntime(); return { target: resolveDiscordOutboundTarget({ to: ctx.to, threadId: ctx.threadId }), formatting: resolveDiscordFormattingOptions(ctx), resolveReplyTo: createReplyToFanout({ replyToId: ctx.replyToId, replyToIdSource: ctx.replyToIdSource, replyToMode: ctx.replyToMode }), send: resolveOutboundSendDep(ctx.deps, "discord") ?? runtime.sendMessageDiscord, sendVoice: resolveOutboundSendDep(ctx.deps, "discordVoice") ?? runtime.sendVoiceMessageDiscord, withRetry: async (fn) => await withDiscordDeliveryRetry({ cfg: ctx.cfg, accountId: ctx.accountId, fn }) }; } //#endregion //#region extensions/discord/src/outbound-payload.ts function createDiscordUnknownPayloadResult(target) { return { messageId: "", channelId: target, receipt: createDiscordSendReceipt({ platformMessageIds: [], channelId: target, kind: "unknown" }) }; } function resolveDiscordDeliveryOptions(ctx, sendContext) { return { replyTo: sendContext.resolveReplyTo(), accountId: ctx.accountId ?? void 0, silent: ctx.silent ?? void 0, cfg: ctx.cfg }; } function resolveDiscordFormattedDeliveryOptions(ctx, sendContext) { return { ...resolveDiscordDeliveryOptions(ctx, sendContext), ...sendContext.formatting }; } function resolveDiscordMediaDeliveryOptions(ctx, sendContext, mediaUrl) { return { mediaUrl, mediaAccess: ctx.mediaAccess, mediaLocalRoots: ctx.mediaLocalRoots, mediaReadFile: ctx.mediaReadFile, ...resolveDiscordFormattedDeliveryOptions(ctx, sendContext) }; } async function sendDiscordOutboundPayload(params) { const ctx = params.ctx; const payload = normalizeDiscordApprovalPayload({ ...ctx.payload, text: ctx.payload.text ?? "" }); const mediaUrls = resolvePayloadMediaUrls(payload); const sendContext = await createDiscordPayloadSendContext(ctx); if (payload.audioAsVoice && mediaUrls.length > 0) { let lastResult = await sendContext.withRetry(async () => await sendContext.sendVoice(sendContext.target, mediaUrls[0], resolveDiscordDeliveryOptions(ctx, sendContext))); if (payload.text?.trim()) lastResult = await sendContext.withRetry(async () => await sendContext.send(sendContext.target, payload.text, { verbose: false, ...resolveDiscordFormattedDeliveryOptions(ctx, sendContext) })); for (const mediaUrl of mediaUrls.slice(1)) lastResult = await sendContext.withRetry(async () => await sendContext.send(sendContext.target, "", { verbose: false, ...resolveDiscordMediaDeliveryOptions(ctx, sendContext, mediaUrl) })); return attachChannelToResult("discord", lastResult); } const componentSpec = await resolveDiscordComponentSpec(payload); if (!componentSpec) { const discordData = payload.channelData?.discord && typeof payload.channelData.discord === "object" && !Array.isArray(payload.channelData.discord) ? payload.channelData.discord : {}; const nativeComponents = Array.isArray(discordData.components) ? discordData.components : void 0; const embeds = Array.isArray(discordData.embeds) ? discordData.embeds : void 0; const filename = normalizeOptionalString(discordData.filename); if (nativeComponents || embeds?.length || filename) return attachChannelToResult("discord", await sendPayloadMediaSequenceOrFallback({ text: payload.text ?? "", mediaUrls, fallbackResult: createDiscordUnknownPayloadResult(sendContext.target), sendNoMedia: async () => await sendContext.withRetry(async () => await sendContext.send(sendContext.target, payload.text ?? "", { verbose: false, components: nativeComponents, embeds, filename, ...resolveDiscordFormattedDeliveryOptions(ctx, sendContext) })), send: async ({ text, mediaUrl, isFirst }) => await sendContext.withRetry(async () => await sendContext.send(sendContext.target, text, { verbose: false, ...resolveDiscordMediaDeliveryOptions(ctx, sendContext, mediaUrl), components: isFirst ? nativeComponents : void 0, embeds: isFirst ? embeds : void 0, filename: isFirst ? filename : void 0 })) })); return await sendTextMediaPayload({ channel: "discord", ctx: { ...ctx, payload }, adapter: params.fallbackAdapter }); } return attachChannelToResult("discord", await sendPayloadMediaSequenceOrFallback({ text: payload.text ?? "", mediaUrls, fallbackResult: createDiscordUnknownPayloadResult(sendContext.target), sendNoMedia: async () => await sendContext.withRetry(async () => await sendDiscordComponentMessageLazy(sendContext.target, componentSpec, { ...resolveDiscordFormattedDeliveryOptions(ctx, sendContext) })), send: async ({ text, mediaUrl, isFirst }) => { if (isFirst) return await sendContext.withRetry(async () => await sendDiscordComponentMessageLazy(sendContext.target, componentSpec, { ...resolveDiscordMediaDeliveryOptions(ctx, sendContext, mediaUrl) })); return await sendContext.withRetry(async () => await sendContext.send(sendContext.target, text, { verbose: false, ...resolveDiscordMediaDeliveryOptions(ctx, sendContext, mediaUrl) })); } })); } //#endregion //#region extensions/discord/src/outbound-adapter.ts const DISCORD_TEXT_CHUNK_LIMIT = 2e3; const DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_BLOCK_RE = /<\s*(system-reminder|previous_response)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi; const DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_SELF_CLOSING_RE = /<\s*(?:system-reminder|previous_response)\b[^>]*\/\s*>/gi; const DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE = /<\s*\/?\s*(?:system-reminder|previous_response)\b[^>]*>/gi; function stripDiscordInternalRuntimeScaffolding(text) { return text.replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_BLOCK_RE, "").replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_SELF_CLOSING_RE, "").replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE, ""); } let discordThreadBindingsPromise; function loadDiscordThreadBindings() { discordThreadBindingsPromise ??= import("./thread-bindings-Cife0M9D.js"); return discordThreadBindingsPromise; } function resolveDiscordWebhookIdentity(params) { const usernameRaw = normalizeOptionalString(params.identity?.name); const fallbackUsername = normalizeOptionalString(params.binding.label) ?? params.binding.agentId; return { username: (usernameRaw || fallbackUsername || "").slice(0, 80) || void 0, avatarUrl: normalizeOptionalString(params.identity?.avatarUrl) }; } async function maybeSendDiscordWebhookText(params) { if (params.threadId == null) return null; const threadId = normalizeOptionalStringifiedId(params.threadId) ?? ""; if (!threadId) return null; const { getThreadBindingManager } = await loadDiscordThreadBindings(); const manager = getThreadBindingManager(params.accountId ?? void 0); if (!manager) return null; const binding = manager.getByThreadId(threadId); if (!binding?.webhookId || !binding?.webhookToken) return null; const persona = resolveDiscordWebhookIdentity({ identity: params.identity, binding }); const { sendWebhookMessageDiscord } = await loadDiscordSendRuntime(); return await sendWebhookMessageDiscord(params.text, { webhookId: binding.webhookId, webhookToken: binding.webhookToken, accountId: binding.accountId, threadId: binding.threadId, cfg: params.cfg, replyTo: params.replyToId ?? void 0, username: persona.username, avatarUrl: persona.avatarUrl }); } const discordOutbound = { deliveryMode: "direct", chunker: (text, limit, ctx) => chunkDiscordTextWithMode(text, { maxChars: limit, maxLines: ctx?.formatting?.maxLinesPerMessage }), textChunkLimit: DISCORD_TEXT_CHUNK_LIMIT, sanitizeText: ({ text }) => stripDiscordInternalRuntimeScaffolding(text), pollMaxOptions: 10, normalizePayload: ({ payload }) => normalizeDiscordApprovalPayload(payload), presentationCapabilities: { supported: true, buttons: true, selects: true, context: true, divider: true, limits: { actions: { maxActions: 25, maxActionsPerRow: 5, maxRows: 5, maxLabelLength: 80, supportsDisabled: true }, selects: { maxOptions: 25, maxLabelLength: 100, maxValueBytes: 100 }, text: { maxLength: DISCORD_TEXT_CHUNK_LIMIT, encoding: "characters", markdownDialect: "discord-markdown" } } }, deliveryCapabilities: { durableFinal: { text: true, media: true, poll: true, payload: true, silent: true, replyTo: true, thread: true, messageSendingHooks: true } }, renderPresentation: async ({ payload, presentation }) => { return await buildDiscordPresentationPayload({ payload, presentation }); }, resolveTarget: ({ to, allowFrom }) => normalizeDiscordOutboundTarget(to, allowFrom), sendPayload: async (ctx) => await sendDiscordOutboundPayload({ ctx, fallbackAdapter: discordOutbound }), ...createAttachedChannelResultAdapter({ channel: "discord", sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, identity, silent, formatting }) => { if (!silent) { const webhookResult = await maybeSendDiscordWebhookText({ cfg, text, threadId, accountId, identity, replyToId }).catch(() => null); if (webhookResult) return webhookResult; } const send = resolveOutboundSendDep(deps, "discord") ?? (await loadDiscordSendRuntime()).sendMessageDiscord; return await withDiscordDeliveryRetry({ cfg, accountId, fn: async () => await send(resolveDiscordOutboundTarget({ to, threadId }), text, { verbose: false, replyTo: replyToId ?? void 0, accountId: accountId ?? void 0, silent: silent ?? void 0, cfg, ...resolveDiscordFormattingOptions({ formatting }) }) }); }, sendMedia: async ({ cfg, to, text, mediaUrl, audioAsVoice, mediaAccess, mediaLocalRoots, mediaReadFile, accountId, deps, replyToId, threadId, silent, formatting }) => { const send = resolveOutboundSendDep(deps, "discord") ?? (await loadDiscordSendRuntime()).sendMessageDiscord; const target = resolveDiscordOutboundTarget({ to, threadId }); const formattingOptions = resolveDiscordFormattingOptions({ formatting }); if (audioAsVoice && mediaUrl) { const sendVoice = resolveOutboundSendDep(deps, "discordVoice") ?? (await loadDiscordSendRuntime()).sendVoiceMessageDiscord; return await withDiscordDeliveryRetry({ cfg, accountId, fn: async () => await sendVoice(target, mediaUrl, { cfg, replyTo: replyToId ?? void 0, accountId: accountId ?? void 0, silent: silent ?? void 0 }) }); } if (text.trim() && mediaUrl && isLikelyDiscordVideoMedia(mediaUrl)) { await withDiscordDeliveryRetry({ cfg, accountId, fn: async () => await send(target, text, { verbose: false, replyTo: replyToId ?? void 0, accountId: accountId ?? void 0, silent: silent ?? void 0, cfg, ...formattingOptions }) }); return await withDiscordDeliveryRetry({ cfg, accountId, fn: async () => await send(target, "", { verbose: false, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, accountId: accountId ?? void 0, silent: silent ?? void 0, cfg, ...formattingOptions }) }); } return await withDiscordDeliveryRetry({ cfg, accountId, fn: async () => await send(target, text, { verbose: false, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, replyTo: replyToId ?? void 0, accountId: accountId ?? void 0, silent: silent ?? void 0, cfg, ...formattingOptions }) }); }, sendPoll: async ({ cfg, to, poll, accountId, threadId, silent }) => await withDiscordDeliveryRetry({ cfg, accountId, fn: async () => await (await loadDiscordSendRuntime()).sendPollDiscord(resolveDiscordOutboundTarget({ to, threadId }), poll, { accountId: accountId ?? void 0, silent: silent ?? void 0, cfg }) }) }), afterDeliverPayload: async ({ target, payload }) => { notifyDiscordInboundEventOutboundPayloadSuccess({ payload, to: resolveDiscordOutboundTarget({ to: target.to, threadId: target.threadId }), accountId: target.accountId }); const threadId = normalizeOptionalStringifiedId(target.threadId); if (!threadId) return; const { getThreadBindingManager } = await loadDiscordThreadBindings(); const manager = getThreadBindingManager(target.accountId ?? void 0); if (!manager?.getByThreadId(threadId)) return; manager.touchThread({ threadId }); } }; //#endregion export { discordOutbound as n, DISCORD_TEXT_CHUNK_LIMIT as t };