UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

497 lines (496 loc) 17.1 kB
import { r as logVerbose, t as danger } from "./globals-GTrXU4s9.js"; import { i as resolveOpenProviderRuntimeGroupPolicy } from "./runtime-group-policy-CSD1Is8G.js"; import { n as resolveBatchedReplyThreadingPolicy } from "./reply-threading-D1LzTZCY.js"; import "./runtime-env-D_2q8-VK.js"; import { n as resolveChannelSourceReplyDeliveryMode } from "./reply-pipeline-eJPstCAz.js"; import { n as createChannelRunQueue } from "./channel-lifecycle.core-Bfh0_sXw.js"; import "./reply-reference-CYxjzSEp.js"; import "./channel-outbound-B3_Zy-kG.js"; import { c as createChannelInboundDebouncer, l as shouldDebounceTextInbound } from "./channel-inbound-bl7VmTdr.js"; import { t as createClaimableDedupe } from "./persistent-dedupe-tc3aSkBu.js"; import { a as resolveDiscordChannelParentSafe, n as resolveDiscordChannelInfoSafe, r as resolveDiscordChannelNameSafe, t as resolveDiscordChannelIdSafe } from "./channel-access-C12aDZ0p.js"; import { o as mergeAbortSignals } from "./timeouts-BG9tJyKJ.js"; import { c as hasDiscordMessageStickers, d as resolveDiscordMessageChannelId, r as resolveDiscordMessageText } from "./message-utils-BjIDHgFC.js"; import { t as createDiscordReplyTypingFeedback } from "./reply-typing-feedback-DYF0iQs7.js"; //#region extensions/discord/src/monitor/inbound-dedupe.ts const RECENT_DISCORD_MESSAGE_TTL_MS = 5 * 6e4; const RECENT_DISCORD_MESSAGE_MAX = 5e3; function createDiscordInboundReplayGuard() { return createClaimableDedupe({ ttlMs: RECENT_DISCORD_MESSAGE_TTL_MS, memoryMaxSize: RECENT_DISCORD_MESSAGE_MAX }); } var DiscordRetryableInboundError = class extends Error { constructor(message, options) { super(message, options); this.name = "DiscordRetryableInboundError"; } }; function buildDiscordInboundReplayKey(params) { const messageId = params.data.message?.id?.trim(); if (!messageId) return null; const channelId = resolveDiscordMessageChannelId({ message: params.data.message, eventChannelId: params.data.channel_id }); if (!channelId) return null; return `${params.accountId}:${channelId}:${messageId}`; } async function claimDiscordInboundReplay(params) { const replayKey = params.replayKey?.trim(); if (!replayKey) return true; return (await params.replayGuard.claim(replayKey)).kind === "claimed"; } async function commitDiscordInboundReplay(params) { const replayKeys = normalizeDiscordInboundReplayKeys(params.replayKeys); await Promise.all(replayKeys.map((replayKey) => params.replayGuard.commit(replayKey))); } function releaseDiscordInboundReplay(params) { normalizeDiscordInboundReplayKeys(params.replayKeys).forEach((replayKey) => params.replayGuard.release(replayKey, { error: params.error })); } function normalizeDiscordInboundReplayKeys(replayKeys) { return [...new Set((replayKeys ?? []).map((replayKey) => replayKey?.trim()).filter((replayKey) => Boolean(replayKey)))]; } //#endregion //#region extensions/discord/src/monitor/inbound-job.ts function resolveDiscordInboundJobQueueKey(ctx) { const sessionKey = ctx.route.sessionKey?.trim(); if (sessionKey) return sessionKey; const baseSessionKey = ctx.baseSessionKey?.trim(); if (baseSessionKey) return baseSessionKey; return ctx.messageChannelId; } function buildDiscordInboundJob(ctx, options) { const { runtime, abortSignal, guildHistories, client, threadBindings, replyTypingFeedback, discordRestFetch, message, data, threadChannel, ...payload } = ctx; const sanitizedMessage = sanitizeDiscordInboundMessage(message); return { queueKey: resolveDiscordInboundJobQueueKey(ctx), payload: { ...payload, message: sanitizedMessage, data: { ...data, message: sanitizedMessage }, threadChannel: normalizeDiscordThreadChannel(threadChannel) }, runtime: { runtime, abortSignal, guildHistories, client, threadBindings, replyTypingFeedback, discordRestFetch }, replayKeys: options?.replayKeys ? [...options.replayKeys] : void 0 }; } function materializeDiscordInboundJob(job, abortSignal) { return { ...job.payload, ...job.runtime, abortSignal: abortSignal ?? job.runtime.abortSignal }; } function sanitizeDiscordInboundMessage(message) { const descriptors = Object.getOwnPropertyDescriptors(message); delete descriptors.channel; return Object.create(Object.getPrototypeOf(message), descriptors); } function normalizeDiscordThreadChannel(threadChannel) { if (!threadChannel) return null; const channelInfo = resolveDiscordChannelInfoSafe(threadChannel); const parent = resolveDiscordChannelParentSafe(threadChannel); return { id: threadChannel.id, name: channelInfo.name, parentId: channelInfo.parentId, parent: parent ? { id: resolveDiscordChannelIdSafe(parent), name: resolveDiscordChannelNameSafe(parent) } : void 0, ownerId: channelInfo.ownerId }; } //#endregion //#region extensions/discord/src/monitor/message-handler.batch-gate.ts function applyImplicitReplyBatchGate(ctx, replyToMode, isBatched) { const replyThreading = resolveBatchedReplyThreadingPolicy(replyToMode, isBatched); if (!replyThreading) return; ctx.ReplyThreading = replyThreading; } //#endregion //#region extensions/discord/src/monitor/message-handler.reply-typing-policy.ts function resolveDiscordSourceReplyDeliveryMode(ctx) { return resolveChannelSourceReplyDeliveryMode({ cfg: ctx.cfg, ctx: { ChatType: ctx.isDirectMessage ? "direct" : ctx.isGroupDm ? "group" : ctx.isGuildMessage ? "channel" : void 0, InboundEventKind: ctx.inboundEventKind } }); } function resolveDiscordAcceptedTypingPrestart(ctx) { const sourceReplyDeliveryMode = resolveDiscordSourceReplyDeliveryMode(ctx); if (ctx.abortSignal?.aborted) return { sourceReplyDeliveryMode, shouldPrestart: false, reason: "aborted" }; if (!ctx.messageText.trim()) return { sourceReplyDeliveryMode, shouldPrestart: false, reason: "empty" }; if (ctx.inboundEventKind === "room_event") return { sourceReplyDeliveryMode, shouldPrestart: false, reason: "room-event" }; const configuredTypingMode = ctx.cfg.session?.typingMode ?? ctx.cfg.agents?.defaults?.typingMode; if (configuredTypingMode !== void 0) return { sourceReplyDeliveryMode, shouldPrestart: configuredTypingMode === "instant", reason: configuredTypingMode === "instant" ? "configured-instant" : "configured-not-instant" }; if (sourceReplyDeliveryMode === "message_tool_only") return { sourceReplyDeliveryMode, shouldPrestart: true, reason: "tool-only" }; if (!ctx.isGuildMessage && !ctx.isGroupDm) return { sourceReplyDeliveryMode, shouldPrestart: true, reason: "direct" }; if (ctx.effectiveWasMentioned) return { sourceReplyDeliveryMode, shouldPrestart: true, reason: "mentioned-group" }; return { sourceReplyDeliveryMode, shouldPrestart: false, reason: "defer-to-message" }; } //#endregion //#region extensions/discord/src/monitor/message-run-queue.ts let messageProcessRuntimePromise; async function loadMessageProcessRuntime() { messageProcessRuntimePromise ??= import("./message-handler.process-DGX1-IzX.js"); return await messageProcessRuntimePromise; } async function processDiscordQueuedMessage(params) { const processDiscordMessageImpl = params.testing?.processDiscordMessage ?? (await loadMessageProcessRuntime()).processDiscordMessage; const abortSignal = mergeAbortSignals([params.job.runtime.abortSignal, params.lifecycleSignal]); try { await processDiscordMessageImpl(materializeDiscordInboundJob(params.job, abortSignal)); await commitDiscordInboundReplay({ replayKeys: params.job.replayKeys, replayGuard: params.replayGuard }); } catch (error) { if (error instanceof DiscordRetryableInboundError) releaseDiscordInboundReplay({ replayKeys: params.job.replayKeys, error, replayGuard: params.replayGuard }); else await commitDiscordInboundReplay({ replayKeys: params.job.replayKeys, replayGuard: params.replayGuard }); throw error; } } function cleanupSkippedDiscordQueuedMessage(params) { try { params.job.runtime.replyTypingFeedback?.onCleanup?.(); } finally { releaseDiscordInboundReplay({ replayKeys: params.job.replayKeys, error: new DiscordRetryableInboundError("discord queued run skipped before processing"), replayGuard: params.replayGuard }); } } function createDiscordMessageRunQueue(params) { const replayGuard = params.replayGuard ?? createDiscordInboundReplayGuard(); const skippedCleanup = /* @__PURE__ */ new Set(); const runQueue = createChannelRunQueue({ setStatus: params.setStatus, abortSignal: params.abortSignal, onError: (error) => { params.runtime.error(danger(`discord message run failed: ${String(error)}`)); } }); let lifecycleActive = !params.abortSignal?.aborted; const cleanupSkippedQueuedMessages = () => { if (!lifecycleActive && skippedCleanup.size === 0) return; lifecycleActive = false; const cleanups = [...skippedCleanup]; skippedCleanup.clear(); for (const cleanup of cleanups) cleanup(); }; if (params.abortSignal?.aborted) cleanupSkippedQueuedMessages(); else params.abortSignal?.addEventListener("abort", cleanupSkippedQueuedMessages, { once: true }); return { enqueue(job) { const cleanupSkipped = () => { cleanupSkippedDiscordQueuedMessage({ job, replayGuard }); }; if (!lifecycleActive) { cleanupSkipped(); return; } skippedCleanup.add(cleanupSkipped); runQueue.enqueue(job.queueKey, async ({ lifecycleSignal }) => { skippedCleanup.delete(cleanupSkipped); await processDiscordQueuedMessage({ job, lifecycleSignal, replayGuard, testing: params.testing }); }); }, deactivate() { runQueue.deactivate(); cleanupSkippedQueuedMessages(); } }; } //#endregion //#region extensions/discord/src/monitor/message-handler.ts let messagePreflightRuntimePromise; async function loadMessagePreflightRuntime() { messagePreflightRuntimePromise ??= import("./message-handler.preflight-Bz7YGt08.js"); return await messagePreflightRuntimePromise; } function isNonEmptyString(value) { return typeof value === "string" && value.length > 0; } function startAcceptedTypingFeedback(params) { const { ctx, createFeedback, dedupeKey, activeFeedback } = params; if (!resolveDiscordAcceptedTypingPrestart(ctx).shouldPrestart) return; const channelId = ctx.messageChannelId.trim(); if (activeFeedback.get(dedupeKey)) return; const replyTypingFeedback = ctx.replyTypingFeedback ?? (createFeedback ?? createDiscordReplyTypingFeedback)({ cfg: ctx.cfg, token: ctx.token, accountId: ctx.accountId, channelId: ctx.messageChannelId, log: logVerbose }); const cleanup = replyTypingFeedback.onCleanup; replyTypingFeedback.onCleanup = () => { cleanup?.(); if (activeFeedback.get(dedupeKey)?.feedback === replyTypingFeedback) activeFeedback.delete(dedupeKey); }; activeFeedback.set(dedupeKey, { channelId, feedback: replyTypingFeedback }); ctx.replyTypingFeedback = replyTypingFeedback; replyTypingFeedback.onReplyStart().catch((err) => { logVerbose(`discord accepted typing feedback failed: ${String(err)}`); }); return replyTypingFeedback; } function createDiscordMessageHandler(params) { const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({ providerConfigPresent: params.cfg.channels?.discord !== void 0, groupPolicy: params.discordConfig?.groupPolicy, defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy }); const ackReactionScope = params.discordConfig?.ackReactionScope ?? params.cfg.messages?.ackReactionScope ?? "group-mentions"; const preflightDiscordMessageImpl = params.testing?.preflightDiscordMessage; const replayGuard = createDiscordInboundReplayGuard(); const prestartedTypingFeedback = /* @__PURE__ */ new Map(); const messageRunQueue = createDiscordMessageRunQueue({ runtime: params.runtime, setStatus: params.setStatus, abortSignal: params.abortSignal, replayGuard, testing: params.testing }); const { debouncer } = createChannelInboundDebouncer({ cfg: params.cfg, channel: "discord", buildKey: (entry) => { const message = entry.data.message; const authorId = entry.data.author?.id; if (!message || !authorId) return null; const channelId = resolveDiscordMessageChannelId({ message, eventChannelId: entry.data.channel_id }); if (!channelId) return null; return `discord:${params.accountId}:${channelId}:${authorId}`; }, shouldDebounce: (entry) => { const message = entry.data.message; if (!message) return false; return shouldDebounceTextInbound({ text: resolveDiscordMessageText(message, { includeForwarded: false }), cfg: params.cfg, hasMedia: message.attachments && message.attachments.length > 0 || hasDiscordMessageStickers(message) }); }, onFlush: async (entries) => { const last = entries.at(-1); if (!last) return; const replayKeys = entries.map((entry) => entry.replayKey).filter(isNonEmptyString); const abortSignal = last.abortSignal; if (abortSignal?.aborted) { releaseDiscordInboundReplay({ replayKeys, error: abortSignal.reason, replayGuard }); return; } try { if (entries.length === 1) { const ctx = await (preflightDiscordMessageImpl ?? (await loadMessagePreflightRuntime()).preflightDiscordMessage)({ ...params, ackReactionScope, groupPolicy, abortSignal, data: last.data, client: last.client }); if (!ctx) { await commitDiscordInboundReplay({ replayKeys, replayGuard }); return; } const queueKey = resolveDiscordInboundJobQueueKey(ctx); startAcceptedTypingFeedback({ ctx, createFeedback: params.testing?.createReplyTypingFeedback, dedupeKey: queueKey, activeFeedback: prestartedTypingFeedback }); applyImplicitReplyBatchGate(ctx, params.replyToMode, false); messageRunQueue.enqueue(buildDiscordInboundJob(ctx, { replayKeys })); return; } const combinedBaseText = entries.map((entry) => resolveDiscordMessageText(entry.data.message, { includeForwarded: false })).filter(Boolean).join("\n"); const syntheticMessage = Object.create(Object.getPrototypeOf(last.data.message), { ...Object.getOwnPropertyDescriptors(last.data.message), content: { value: combinedBaseText, enumerable: true, configurable: true }, attachments: { value: [], enumerable: true, configurable: true }, message_snapshots: { value: last.data.message.message_snapshots, enumerable: true, configurable: true }, messageSnapshots: { value: last.data.message.messageSnapshots, enumerable: true, configurable: true }, rawData: { value: { ...last.data.message.rawData }, enumerable: true, configurable: true } }); const syntheticData = { ...last.data, message: syntheticMessage }; const ctx = await (preflightDiscordMessageImpl ?? (await loadMessagePreflightRuntime()).preflightDiscordMessage)({ ...params, ackReactionScope, groupPolicy, abortSignal, data: syntheticData, client: last.client }); if (!ctx) { await commitDiscordInboundReplay({ replayKeys, replayGuard }); return; } const queueKey = resolveDiscordInboundJobQueueKey(ctx); startAcceptedTypingFeedback({ ctx, createFeedback: params.testing?.createReplyTypingFeedback, dedupeKey: queueKey, activeFeedback: prestartedTypingFeedback }); applyImplicitReplyBatchGate(ctx, params.replyToMode, true); if (entries.length > 1) { const ids = entries.map((entry) => entry.data.message?.id).filter(isNonEmptyString); if (ids.length > 0) { const ctxBatch = ctx; ctxBatch.MessageSids = ids; ctxBatch.MessageSidFirst = ids[0]; ctxBatch.MessageSidLast = ids[ids.length - 1]; } } messageRunQueue.enqueue(buildDiscordInboundJob(ctx, { replayKeys })); } catch (error) { if (error instanceof DiscordRetryableInboundError) releaseDiscordInboundReplay({ replayKeys, error, replayGuard }); else await commitDiscordInboundReplay({ replayKeys, replayGuard }); throw error; } }, onError: (err) => { params.runtime.error(danger(`discord debounce flush failed: ${String(err)}`)); } }); const handler = async (data, client, options) => { try { if (options?.abortSignal?.aborted) return; const msgAuthorId = data.message?.author?.id ?? data.author?.id; if (params.botUserId && msgAuthorId === params.botUserId) return; const replayKey = buildDiscordInboundReplayKey({ accountId: params.accountId, data }); if (!await claimDiscordInboundReplay({ replayKey, replayGuard })) return; await debouncer.enqueue({ data, client, abortSignal: options?.abortSignal, replayKey: replayKey ?? void 0 }); } catch (err) { params.runtime.error(danger(`handler failed: ${String(err)}`)); } }; handler.deactivate = messageRunQueue.deactivate; return handler; } //#endregion export { createDiscordMessageHandler as t };