"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.A = toPluginMessageReceivedEvent;exports.C = initializeGlobalHookRunner;exports.D = toInternalMessageReceivedContext;exports.E = toInternalMessagePreprocessedContext;exports.O = toInternalMessageTranscribedContext;exports.S = getGlobalHookRunner;exports.T = deriveInboundMessageHookContext;exports._ = normalizeChannelTargetInput;exports.a = normalizeReplyPayloadsForDelivery;exports.b = parseReplyDirectives;exports.c = filterMessagingToolDuplicates;exports.d = shouldSuppressMessagingToolReplies;exports.f = shouldSuppressReasoningPayload;exports.g = buildTargetResolverSignature;exports.h = resolveChannelMediaMaxBytes;exports.i = normalizeOutboundPayloadsForJson;exports.j = fireAndForgetHook;exports.k = toPluginMessageContext;exports.l = filterMessagingToolMediaDuplicates;exports.m = resolveReplyToMode;exports.n = formatOutboundPayloadLog;exports.o = applyReplyTagsToPayload;exports.p = createReplyToModeFilterForChannel;exports.r = normalizeOutboundPayloads;exports.s = applyReplyThreading;exports.t = deliverOutboundPayloads;exports.u = isRenderablePayload;exports.v = normalizeTargetForProvider;exports.w = joinPresentTextSegments;exports.x = splitMediaFromOutput;exports.y = throwIfAborted;var _runWithConcurrency2ga3CMk = require("./run-with-concurrency-2ga3-CMk.js"); var _pathsEFexkPEh = require("./paths-eFexkPEh.js"); var _configDiiPndBn = require("./config-DiiPndBn.js"); var _loggerU3s76KST = require("./logger-U3s76KST.js"); var _thinkingCfIPyoMg = require("./thinking-CfIPyoMg.js"); var _piEmbeddedHelpersDmr3bcbH = require("./pi-embedded-helpers-Dmr3bcbH.js"); var _pluginsBhm3N6Y = require("./plugins-Bhm3N6Y-.js"); var _localRootsZhwi3hFj = require("./local-roots-Zhwi3hFj.js"); var _irKp5uANes = require("./ir-Kp5uANes.js"); var _secureRandom_kmh2emF = require("./secure-random-_kmh2emF.js"); var _tokensCgeKcoW = require("./tokens-CgeKcoW1.js"); var _targetsBP_LjgWp = require("./targets-BP_LjgWp.js"); var _sendCWALh87S = require("./send-CWALh87S.js"); var _nodeFs = _interopRequireDefault(require("node:fs")); var _nodePath = _interopRequireDefault(require("node:path"));function _interopRequireDefault(e) {return e && e.__esModule ? e : { default: e };} //#region src/hooks/fire-and-forget.ts function fireAndForgetHook(task, label, logger = _loggerU3s76KST.R) { task.catch((err) => { logger(`${label}: ${String(err)}`); }); } //#endregion //#region src/hooks/message-hook-mappers.ts function deriveInboundMessageHookContext(ctx, overrides) { const content = overrides?.content ?? (typeof ctx.BodyForCommands === "string" ? ctx.BodyForCommands : typeof ctx.RawBody === "string" ? ctx.RawBody : typeof ctx.Body === "string" ? ctx.Body : ""); const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase(); const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? void 0; const isGroup = Boolean(ctx.GroupSubject || ctx.GroupChannel); return { from: ctx.From ?? "", to: ctx.To, content, body: ctx.Body, bodyForAgent: ctx.BodyForAgent, transcript: ctx.Transcript, timestamp: typeof ctx.Timestamp === "number" && Number.isFinite(ctx.Timestamp) ? ctx.Timestamp : void 0, channelId, accountId: ctx.AccountId, conversationId, messageId: overrides?.messageId ?? ctx.MessageSidFull ?? ctx.MessageSid ?? ctx.MessageSidFirst ?? ctx.MessageSidLast, senderId: ctx.SenderId, senderName: ctx.SenderName, senderUsername: ctx.SenderUsername, senderE164: ctx.SenderE164, provider: ctx.Provider, surface: ctx.Surface, threadId: ctx.MessageThreadId, mediaPath: ctx.MediaPath, mediaType: ctx.MediaType, originatingChannel: ctx.OriginatingChannel, originatingTo: ctx.OriginatingTo, guildId: ctx.GroupSpace, channelName: ctx.GroupChannel, isGroup, groupId: isGroup ? conversationId : void 0 }; } function buildCanonicalSentMessageHookContext(params) { return { to: params.to, content: params.content, success: params.success, error: params.error, channelId: params.channelId, accountId: params.accountId, conversationId: params.conversationId ?? params.to, messageId: params.messageId, isGroup: params.isGroup, groupId: params.groupId }; } function toPluginMessageContext(canonical) { return { channelId: canonical.channelId, accountId: canonical.accountId, conversationId: canonical.conversationId }; } function toPluginMessageReceivedEvent(canonical) { return { from: canonical.from, content: canonical.content, timestamp: canonical.timestamp, metadata: { to: canonical.to, provider: canonical.provider, surface: canonical.surface, threadId: canonical.threadId, originatingChannel: canonical.originatingChannel, originatingTo: canonical.originatingTo, messageId: canonical.messageId, senderId: canonical.senderId, senderName: canonical.senderName, senderUsername: canonical.senderUsername, senderE164: canonical.senderE164, guildId: canonical.guildId, channelName: canonical.channelName } }; } function toPluginMessageSentEvent(canonical) { return { to: canonical.to, content: canonical.content, success: canonical.success, ...(canonical.error ? { error: canonical.error } : {}) }; } function toInternalMessageReceivedContext(canonical) { return { from: canonical.from, content: canonical.content, timestamp: canonical.timestamp, channelId: canonical.channelId, accountId: canonical.accountId, conversationId: canonical.conversationId, messageId: canonical.messageId, metadata: { to: canonical.to, provider: canonical.provider, surface: canonical.surface, threadId: canonical.threadId, senderId: canonical.senderId, senderName: canonical.senderName, senderUsername: canonical.senderUsername, senderE164: canonical.senderE164, guildId: canonical.guildId, channelName: canonical.channelName } }; } function toInternalMessageTranscribedContext(canonical, cfg) { return { ...toInternalInboundMessageHookContextBase(canonical), transcript: canonical.transcript ?? "", cfg }; } function toInternalMessagePreprocessedContext(canonical, cfg) { return { ...toInternalInboundMessageHookContextBase(canonical), transcript: canonical.transcript, isGroup: canonical.isGroup, groupId: canonical.groupId, cfg }; } function toInternalInboundMessageHookContextBase(canonical) { return { from: canonical.from, to: canonical.to, body: canonical.body, bodyForAgent: canonical.bodyForAgent, timestamp: canonical.timestamp, channelId: canonical.channelId, conversationId: canonical.conversationId, messageId: canonical.messageId, senderId: canonical.senderId, senderName: canonical.senderName, senderUsername: canonical.senderUsername, provider: canonical.provider, surface: canonical.surface, mediaPath: canonical.mediaPath, mediaType: canonical.mediaType }; } function toInternalMessageSentContext(canonical) { return { to: canonical.to, content: canonical.content, success: canonical.success, ...(canonical.error ? { error: canonical.error } : {}), channelId: canonical.channelId, accountId: canonical.accountId, conversationId: canonical.conversationId, messageId: canonical.messageId, ...(canonical.isGroup != null ? { isGroup: canonical.isGroup } : {}), ...(canonical.groupId ? { groupId: canonical.groupId } : {}) }; } //#endregion //#region src/shared/text/join-segments.ts function concatOptionalTextSegments(params) { const separator = params.separator ?? "\n\n"; if (params.left && params.right) return `${params.left}${separator}${params.right}`; return params.right ?? params.left; } function joinPresentTextSegments(segments, options) { const separator = options?.separator ?? "\n\n"; const trim = options?.trim ?? false; const values = []; for (const segment of segments) { if (typeof segment !== "string") continue; const normalized = trim ? segment.trim() : segment; if (!normalized) continue; values.push(normalized); } return values.length > 0 ? values.join(separator) : void 0; } //#endregion //#region src/plugins/hooks.ts /** * Plugin Hook Runner * * Provides utilities for executing plugin lifecycle hooks with proper * error handling, priority ordering, and async support. */ /** * Get hooks for a specific hook name, sorted by priority (higher first). */ function getHooksForName(registry, hookName) { return registry.typedHooks.filter((h) => h.hookName === hookName).toSorted((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); } /** * Create a hook runner for a specific registry. */ function createHookRunner(registry, options = {}) { const logger = options.logger; const catchErrors = options.catchErrors ?? true; const mergeBeforeModelResolve = (acc, next) => ({ modelOverride: acc?.modelOverride ?? next.modelOverride, providerOverride: acc?.providerOverride ?? next.providerOverride }); const mergeBeforePromptBuild = (acc, next) => ({ systemPrompt: next.systemPrompt ?? acc?.systemPrompt, prependContext: concatOptionalTextSegments({ left: acc?.prependContext, right: next.prependContext }), prependSystemContext: concatOptionalTextSegments({ left: acc?.prependSystemContext, right: next.prependSystemContext }), appendSystemContext: concatOptionalTextSegments({ left: acc?.appendSystemContext, right: next.appendSystemContext }) }); const mergeSubagentSpawningResult = (acc, next) => { if (acc?.status === "error") return acc; if (next.status === "error") return next; return { status: "ok", threadBindingReady: Boolean(acc?.threadBindingReady || next.threadBindingReady) }; }; const mergeSubagentDeliveryTargetResult = (acc, next) => { if (acc?.origin) return acc; return next; }; const handleHookError = (params) => { const msg = `[hooks] ${params.hookName} handler from ${params.pluginId} failed: ${String(params.error)}`; if (catchErrors) { logger?.error(msg); return; } throw new Error(msg, { cause: params.error }); }; /** * Run a hook that doesn't return a value (fire-and-forget style). * All handlers are executed in parallel for performance. */ async function runVoidHook(hookName, event, ctx) { const hooks = getHooksForName(registry, hookName); if (hooks.length === 0) return; logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers)`); const promises = hooks.map(async (hook) => { try { await hook.handler(event, ctx); } catch (err) { handleHookError({ hookName, pluginId: hook.pluginId, error: err }); } }); await Promise.all(promises); } /** * Run a hook that can return a modifying result. * Handlers are executed sequentially in priority order, and results are merged. */ async function runModifyingHook(hookName, event, ctx, mergeResults) { const hooks = getHooksForName(registry, hookName); if (hooks.length === 0) return; logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers, sequential)`); let result; for (const hook of hooks) try { const handlerResult = await hook.handler(event, ctx); if (handlerResult !== void 0 && handlerResult !== null) if (mergeResults && result !== void 0) result = mergeResults(result, handlerResult);else result = handlerResult; } catch (err) { handleHookError({ hookName, pluginId: hook.pluginId, error: err }); } return result; } /** * Run before_model_resolve hook. * Allows plugins to override provider/model before model resolution. */ async function runBeforeModelResolve(event, ctx) { return runModifyingHook("before_model_resolve", event, ctx, mergeBeforeModelResolve); } /** * Run before_prompt_build hook. * Allows plugins to inject context and system prompt before prompt submission. */ async function runBeforePromptBuild(event, ctx) { return runModifyingHook("before_prompt_build", event, ctx, mergeBeforePromptBuild); } /** * Run before_agent_start hook. * Legacy compatibility hook that combines model resolve + prompt build phases. */ async function runBeforeAgentStart(event, ctx) { return runModifyingHook("before_agent_start", event, ctx, (acc, next) => ({ ...mergeBeforePromptBuild(acc, next), ...mergeBeforeModelResolve(acc, next) })); } /** * Run agent_end hook. * Allows plugins to analyze completed conversations. * Runs in parallel (fire-and-forget). */ async function runAgentEnd(event, ctx) { return runVoidHook("agent_end", event, ctx); } /** * Run llm_input hook. * Allows plugins to observe the exact input payload sent to the LLM. * Runs in parallel (fire-and-forget). */ async function runLlmInput(event, ctx) { return runVoidHook("llm_input", event, ctx); } /** * Run llm_output hook. * Allows plugins to observe the exact output payload returned by the LLM. * Runs in parallel (fire-and-forget). */ async function runLlmOutput(event, ctx) { return runVoidHook("llm_output", event, ctx); } /** * Run before_compaction hook. */ async function runBeforeCompaction(event, ctx) { return runVoidHook("before_compaction", event, ctx); } /** * Run after_compaction hook. */ async function runAfterCompaction(event, ctx) { return runVoidHook("after_compaction", event, ctx); } /** * Run before_reset hook. * Fired when /new or /reset clears a session, before messages are lost. * Runs in parallel (fire-and-forget). */ async function runBeforeReset(event, ctx) { return runVoidHook("before_reset", event, ctx); } /** * Run message_received hook. * Runs in parallel (fire-and-forget). */ async function runMessageReceived(event, ctx) { return runVoidHook("message_received", event, ctx); } /** * Run message_sending hook. * Allows plugins to modify or cancel outgoing messages. * Runs sequentially. */ async function runMessageSending(event, ctx) { return runModifyingHook("message_sending", event, ctx, (acc, next) => ({ content: next.content ?? acc?.content, cancel: next.cancel ?? acc?.cancel })); } /** * Run message_sent hook. * Runs in parallel (fire-and-forget). */ async function runMessageSent(event, ctx) { return runVoidHook("message_sent", event, ctx); } /** * Run before_tool_call hook. * Allows plugins to modify or block tool calls. * Runs sequentially. */ async function runBeforeToolCall(event, ctx) { return runModifyingHook("before_tool_call", event, ctx, (acc, next) => ({ params: next.params ?? acc?.params, block: next.block ?? acc?.block, blockReason: next.blockReason ?? acc?.blockReason })); } /** * Run after_tool_call hook. * Runs in parallel (fire-and-forget). */ async function runAfterToolCall(event, ctx) { return runVoidHook("after_tool_call", event, ctx); } /** * Run tool_result_persist hook. * * This hook is intentionally synchronous: it runs in hot paths where session * transcripts are appended synchronously. * * Handlers are executed sequentially in priority order (higher first). Each * handler may return `{ message }` to replace the message passed to the next * handler. */ function runToolResultPersist(event, ctx) { const hooks = getHooksForName(registry, "tool_result_persist"); if (hooks.length === 0) return; let current = event.message; for (const hook of hooks) try { const out = hook.handler({ ...event, message: current }, ctx); if (out && typeof out.then === "function") { const msg = `[hooks] tool_result_persist handler from ${hook.pluginId} returned a Promise; this hook is synchronous and the result was ignored.`; if (catchErrors) { logger?.warn?.(msg); continue; } throw new Error(msg); } const next = out?.message; if (next) current = next; } catch (err) { const msg = `[hooks] tool_result_persist handler from ${hook.pluginId} failed: ${String(err)}`; if (catchErrors) logger?.error(msg);else throw new Error(msg, { cause: err }); } return { message: current }; } /** * Run before_message_write hook. * * This hook is intentionally synchronous: it runs on the hot path where * session transcripts are appended synchronously. * * Handlers are executed sequentially in priority order (higher first). * If any handler returns { block: true }, the message is NOT written * to the session JSONL and we return immediately. * If a handler returns { message }, the modified message replaces the * original for subsequent handlers and the final write. */ function runBeforeMessageWrite(event, ctx) { const hooks = getHooksForName(registry, "before_message_write"); if (hooks.length === 0) return; let current = event.message; for (const hook of hooks) try { const out = hook.handler({ ...event, message: current }, ctx); if (out && typeof out.then === "function") { const msg = `[hooks] before_message_write handler from ${hook.pluginId} returned a Promise; this hook is synchronous and the result was ignored.`; if (catchErrors) { logger?.warn?.(msg); continue; } throw new Error(msg); } const result = out; if (result?.block) return { block: true }; if (result?.message) current = result.message; } catch (err) { const msg = `[hooks] before_message_write handler from ${hook.pluginId} failed: ${String(err)}`; if (catchErrors) logger?.error(msg);else throw new Error(msg, { cause: err }); } if (current !== event.message) return { message: current }; } /** * Run session_start hook. * Runs in parallel (fire-and-forget). */ async function runSessionStart(event, ctx) { return runVoidHook("session_start", event, ctx); } /** * Run session_end hook. * Runs in parallel (fire-and-forget). */ async function runSessionEnd(event, ctx) { return runVoidHook("session_end", event, ctx); } /** * Run subagent_spawning hook. * Runs sequentially so channel plugins can deterministically provision session bindings. */ async function runSubagentSpawning(event, ctx) { return runModifyingHook("subagent_spawning", event, ctx, mergeSubagentSpawningResult); } /** * Run subagent_delivery_target hook. * Runs sequentially so channel plugins can deterministically resolve routing. */ async function runSubagentDeliveryTarget(event, ctx) { return runModifyingHook("subagent_delivery_target", event, ctx, mergeSubagentDeliveryTargetResult); } /** * Run subagent_spawned hook. * Runs in parallel (fire-and-forget). */ async function runSubagentSpawned(event, ctx) { return runVoidHook("subagent_spawned", event, ctx); } /** * Run subagent_ended hook. * Runs in parallel (fire-and-forget). */ async function runSubagentEnded(event, ctx) { return runVoidHook("subagent_ended", event, ctx); } /** * Run gateway_start hook. * Runs in parallel (fire-and-forget). */ async function runGatewayStart(event, ctx) { return runVoidHook("gateway_start", event, ctx); } /** * Run gateway_stop hook. * Runs in parallel (fire-and-forget). */ async function runGatewayStop(event, ctx) { return runVoidHook("gateway_stop", event, ctx); } /** * Check if any hooks are registered for a given hook name. */ function hasHooks(hookName) { return registry.typedHooks.some((h) => h.hookName === hookName); } /** * Get count of registered hooks for a given hook name. */ function getHookCount(hookName) { return registry.typedHooks.filter((h) => h.hookName === hookName).length; } return { runBeforeModelResolve, runBeforePromptBuild, runBeforeAgentStart, runLlmInput, runLlmOutput, runAgentEnd, runBeforeCompaction, runAfterCompaction, runBeforeReset, runMessageReceived, runMessageSending, runMessageSent, runBeforeToolCall, runAfterToolCall, runToolResultPersist, runBeforeMessageWrite, runSessionStart, runSessionEnd, runSubagentSpawning, runSubagentDeliveryTarget, runSubagentSpawned, runSubagentEnded, runGatewayStart, runGatewayStop, hasHooks, getHookCount }; } //#endregion //#region src/plugins/hook-runner-global.ts /** * Global Plugin Hook Runner * * Singleton hook runner that's initialized when plugins are loaded * and can be called from anywhere in the codebase. */ const log$1 = (0, _loggerU3s76KST.a)("plugins"); let globalHookRunner = null; /** * Initialize the global hook runner with a plugin registry. * Called once when plugins are loaded during gateway startup. */ function initializeGlobalHookRunner(registry) { globalHookRunner = createHookRunner(registry, { logger: { debug: (msg) => log$1.debug(msg), warn: (msg) => log$1.warn(msg), error: (msg) => log$1.error(msg) }, catchErrors: true }); const hookCount = registry.hooks.length; if (hookCount > 0) log$1.info(`hook runner initialized with ${hookCount} registered hooks`); } /** * Get the global hook runner. * Returns null if plugins haven't been loaded yet. */ function getGlobalHookRunner() { return globalHookRunner; } //#endregion //#region src/media/audio-tags.ts /** * Extract audio mode tag from text. * Supports [[audio_as_voice]] to send audio as voice bubble instead of file. * Default is file (preserves backward compatibility). */ function parseAudioTag(text) { const result = (0, _piEmbeddedHelpersDmr3bcbH.Rt)(text, { stripReplyTags: false }); return { text: result.text, audioAsVoice: result.audioAsVoice, hadTag: result.hasAudioTag }; } //#endregion //#region src/media/parse.ts const MEDIA_TOKEN_RE = /\bMEDIA:\s*`?([^\n]+)`?/gi; function normalizeMediaSource(src) { return src.startsWith("file://") ? src.replace("file://", "") : src; } function cleanCandidate(raw) { return raw.replace(/^[`"'[{(]+/, "").replace(/[`"'\\})\],]+$/, ""); } const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/; const SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/; const HAS_FILE_EXT = /\.\w{1,10}$/; function isLikelyLocalPath(candidate) { return candidate.startsWith("/") || candidate.startsWith("./") || candidate.startsWith("../") || candidate.startsWith("~") || WINDOWS_DRIVE_RE.test(candidate) || candidate.startsWith("\\\\") || !SCHEME_RE.test(candidate) && (candidate.includes("/") || candidate.includes("\\")); } function isValidMedia(candidate, opts) { if (!candidate) return false; if (candidate.length > 4096) return false; if (!opts?.allowSpaces && /\s/.test(candidate)) return false; if (/^https?:\/\//i.test(candidate)) return true; if (isLikelyLocalPath(candidate)) return true; if (opts?.allowBareFilename && !SCHEME_RE.test(candidate) && HAS_FILE_EXT.test(candidate)) return true; return false; } function unwrapQuoted(value) { const trimmed = value.trim(); if (trimmed.length < 2) return; const first = trimmed[0]; if (first !== trimmed[trimmed.length - 1]) return; if (first !== `"` && first !== "'" && first !== "`") return; return trimmed.slice(1, -1).trim(); } function mayContainFenceMarkers(input) { return input.includes("```") || input.includes("~~~"); } function isInsideFence(fenceSpans, offset) { return fenceSpans.some((span) => offset >= span.start && offset < span.end); } function splitMediaFromOutput(raw) { const trimmedRaw = raw.trimEnd(); if (!trimmedRaw.trim()) return { text: "" }; const mayContainMediaToken = /media:/i.test(trimmedRaw); const mayContainAudioTag = trimmedRaw.includes("[["); if (!mayContainMediaToken && !mayContainAudioTag) return { text: trimmedRaw }; const media = []; let foundMediaToken = false; const hasFenceMarkers = mayContainFenceMarkers(trimmedRaw); const fenceSpans = hasFenceMarkers ? (0, _irKp5uANes.g)(trimmedRaw) : []; const lines = trimmedRaw.split("\n"); const keptLines = []; let lineOffset = 0; for (const line of lines) { if (hasFenceMarkers && isInsideFence(fenceSpans, lineOffset)) { keptLines.push(line); lineOffset += line.length + 1; continue; } if (!line.trimStart().startsWith("MEDIA:")) { keptLines.push(line); lineOffset += line.length + 1; continue; } const matches = Array.from(line.matchAll(MEDIA_TOKEN_RE)); if (matches.length === 0) { keptLines.push(line); lineOffset += line.length + 1; continue; } const pieces = []; let cursor = 0; for (const match of matches) { const start = match.index ?? 0; pieces.push(line.slice(cursor, start)); const payload = match[1]; const unwrapped = unwrapQuoted(payload); const payloadValue = unwrapped ?? payload; const parts = unwrapped ? [unwrapped] : payload.split(/\s+/).filter(Boolean); const mediaStartIndex = media.length; let validCount = 0; const invalidParts = []; let hasValidMedia = false; for (const part of parts) { const candidate = normalizeMediaSource(cleanCandidate(part)); if (isValidMedia(candidate, unwrapped ? { allowSpaces: true } : void 0)) { media.push(candidate); hasValidMedia = true; foundMediaToken = true; validCount += 1; } else invalidParts.push(part); } const trimmedPayload = payloadValue.trim(); const looksLikeLocalPath = isLikelyLocalPath(trimmedPayload) || trimmedPayload.startsWith("file://"); if (!unwrapped && validCount === 1 && invalidParts.length > 0 && /\s/.test(payloadValue) && looksLikeLocalPath) { const fallback = normalizeMediaSource(cleanCandidate(payloadValue)); if (isValidMedia(fallback, { allowSpaces: true })) { media.splice(mediaStartIndex, media.length - mediaStartIndex, fallback); hasValidMedia = true; foundMediaToken = true; validCount = 1; invalidParts.length = 0; } } if (!hasValidMedia) { const fallback = normalizeMediaSource(cleanCandidate(payloadValue)); if (isValidMedia(fallback, { allowSpaces: true, allowBareFilename: true })) { media.push(fallback); hasValidMedia = true; foundMediaToken = true; invalidParts.length = 0; } } if (hasValidMedia) { if (invalidParts.length > 0) pieces.push(invalidParts.join(" ")); } else if (looksLikeLocalPath) foundMediaToken = true;else pieces.push(match[0]); cursor = start + match[0].length; } pieces.push(line.slice(cursor)); const cleanedLine = pieces.join("").replace(/[ \t]{2,}/g, " ").trim(); if (cleanedLine) keptLines.push(cleanedLine); lineOffset += line.length + 1; } let cleanedText = keptLines.join("\n").replace(/[ \t]+\n/g, "\n").replace(/[ \t]{2,}/g, " ").replace(/\n{2,}/g, "\n").trim(); const audioTagResult = parseAudioTag(cleanedText); const hasAudioAsVoice = audioTagResult.audioAsVoice; if (audioTagResult.hadTag) cleanedText = audioTagResult.text.replace(/\n{2,}/g, "\n").trim(); if (media.length === 0) { const result = { text: foundMediaToken || hasAudioAsVoice ? cleanedText : trimmedRaw }; if (hasAudioAsVoice) result.audioAsVoice = true; return result; } return { text: cleanedText, mediaUrls: media, mediaUrl: media[0], ...(hasAudioAsVoice ? { audioAsVoice: true } : {}) }; } //#endregion //#region src/auto-reply/reply/reply-directives.ts function parseReplyDirectives(raw, options = {}) { const split = splitMediaFromOutput(raw); let text = split.text ?? ""; const replyParsed = (0, _piEmbeddedHelpersDmr3bcbH.Rt)(text, { currentMessageId: options.currentMessageId, stripAudioTag: false, stripReplyTags: true }); if (replyParsed.hasReplyTag) text = replyParsed.text; const silentToken = options.silentToken ?? "NO_REPLY"; const isSilent = (0, _tokensCgeKcoW.i)(text, silentToken); if (isSilent) text = ""; return { text, mediaUrls: split.mediaUrls, mediaUrl: split.mediaUrl, replyToId: replyParsed.replyToId, replyToCurrent: replyParsed.replyToCurrent, replyToTag: replyParsed.hasReplyTag, audioAsVoice: split.audioAsVoice, isSilent }; } //#endregion //#region src/infra/outbound/abort.ts /** * Utility for checking AbortSignal state and throwing a standard AbortError. */ /** * Throws an AbortError if the given signal has been aborted. * Use at async checkpoints to support cancellation. */ function throwIfAborted(abortSignal) { if (abortSignal?.aborted) { const err = /* @__PURE__ */new Error("Operation aborted"); err.name = "AbortError"; throw err; } } //#endregion //#region src/infra/outbound/target-normalization.ts function normalizeChannelTargetInput(raw) { return raw.trim(); } const targetNormalizerCacheByChannelId = /* @__PURE__ */new Map(); function resolveTargetNormalizer(channelId) { const version = (0, _configDiiPndBn.kt)(); const cached = targetNormalizerCacheByChannelId.get(channelId); if (cached?.version === version) return cached.normalizer; const normalizer = (0, _pluginsBhm3N6Y.t)(channelId)?.messaging?.normalizeTarget; targetNormalizerCacheByChannelId.set(channelId, { version, normalizer }); return normalizer; } function normalizeTargetForProvider(provider, raw) { if (!raw) return; const fallback = raw.trim() || void 0; if (!fallback) return; const providerId = (0, _pluginsBhm3N6Y.r)(provider); return ((providerId ? resolveTargetNormalizer(providerId) : void 0)?.(raw) ?? fallback) || void 0; } function buildTargetResolverSignature(channel) { const resolver = (0, _pluginsBhm3N6Y.t)(channel)?.messaging?.targetResolver; const hint = resolver?.hint ?? ""; const looksLike = resolver?.looksLikeId; return hashSignature(`${hint}|${looksLike ? looksLike.toString() : ""}`); } function hashSignature(value) { let hash = 5381; for (let i = 0; i < value.length; i += 1) hash = (hash << 5) + hash ^ value.charCodeAt(i); return (hash >>> 0).toString(36); } //#endregion //#region src/channels/plugins/media-limits.ts const MB = 1024 * 1024; function resolveChannelMediaMaxBytes(params) { const accountId = (0, _runWithConcurrency2ga3CMk.ut)(params.accountId); const channelLimit = params.resolveChannelLimitMb({ cfg: params.cfg, accountId }); if (channelLimit) return channelLimit * MB; if (params.cfg.agents?.defaults?.mediaMaxMb) return params.cfg.agents.defaults.mediaMaxMb * MB; } //#endregion //#region src/channels/plugins/registry-loader.ts function createChannelRegistryLoader(resolveValue) { const cache = /* @__PURE__ */new Map(); let lastRegistry = null; return async (id) => { const registry = (0, _configDiiPndBn.Dt)(); if (registry !== lastRegistry) { cache.clear(); lastRegistry = registry; } const cached = cache.get(id); if (cached) return cached; const pluginEntry = registry?.channels.find((entry) => entry.plugin.id === id); if (!pluginEntry) return; const resolved = resolveValue(pluginEntry); if (resolved) cache.set(id, resolved); return resolved; }; } //#endregion //#region src/channels/plugins/outbound/load.ts const loadOutboundAdapterFromRegistry = createChannelRegistryLoader((entry) => entry.plugin.outbound); async function loadChannelOutboundAdapter(id) { return loadOutboundAdapterFromRegistry(id); } //#endregion //#region src/infra/outbound/delivery-queue.ts const QUEUE_DIRNAME = "delivery-queue"; const FAILED_DIRNAME = "failed"; function resolveQueueDir(stateDir) { const base = stateDir ?? (0, _pathsEFexkPEh.c)(); return _nodePath.default.join(base, QUEUE_DIRNAME); } function resolveFailedDir(stateDir) { return _nodePath.default.join(resolveQueueDir(stateDir), FAILED_DIRNAME); } function resolveQueueEntryPaths(id, stateDir) { const queueDir = resolveQueueDir(stateDir); return { jsonPath: _nodePath.default.join(queueDir, `${id}.json`), deliveredPath: _nodePath.default.join(queueDir, `${id}.delivered`) }; } function getErrnoCode(err) { return err && typeof err === "object" && "code" in err ? String(err.code) : null; } async function unlinkBestEffort(filePath) { try { await _nodeFs.default.promises.unlink(filePath); } catch {} } /** Ensure the queue directory (and failed/ subdirectory) exist. */ async function ensureQueueDir(stateDir) { const queueDir = resolveQueueDir(stateDir); await _nodeFs.default.promises.mkdir(queueDir, { recursive: true, mode: 448 }); await _nodeFs.default.promises.mkdir(resolveFailedDir(stateDir), { recursive: true, mode: 448 }); return queueDir; } async function enqueueDelivery(params, stateDir) { const queueDir = await ensureQueueDir(stateDir); const id = (0, _secureRandom_kmh2emF.n)(); const entry = { id, enqueuedAt: Date.now(), channel: params.channel, to: params.to, accountId: params.accountId, payloads: params.payloads, threadId: params.threadId, replyToId: params.replyToId, bestEffort: params.bestEffort, gifPlayback: params.gifPlayback, silent: params.silent, mirror: params.mirror, retryCount: 0 }; const filePath = _nodePath.default.join(queueDir, `${id}.json`); const tmp = `${filePath}.${process.pid}.tmp`; const json = JSON.stringify(entry, null, 2); await _nodeFs.default.promises.writeFile(tmp, json, { encoding: "utf-8", mode: 384 }); await _nodeFs.default.promises.rename(tmp, filePath); return id; } /** Remove a successfully delivered entry from the queue. * * Uses a two-phase approach so that a crash between delivery and cleanup * does not cause the message to be replayed on the next recovery scan: * Phase 1: atomic rename {id}.json → {id}.delivered * Phase 2: unlink the .delivered marker * If the process dies between phase 1 and phase 2 the marker is cleaned up * by {@link loadPendingDeliveries} on the next startup without re-sending. */ async function ackDelivery(id, stateDir) { const { jsonPath, deliveredPath } = resolveQueueEntryPaths(id, stateDir); try { await _nodeFs.default.promises.rename(jsonPath, deliveredPath); } catch (err) { if (getErrnoCode(err) === "ENOENT") { await unlinkBestEffort(deliveredPath); return; } throw err; } await unlinkBestEffort(deliveredPath); } /** Update a queue entry after a failed delivery attempt. */ async function failDelivery(id, error, stateDir) { const filePath = _nodePath.default.join(resolveQueueDir(stateDir), `${id}.json`); const raw = await _nodeFs.default.promises.readFile(filePath, "utf-8"); const entry = JSON.parse(raw); entry.retryCount += 1; entry.lastAttemptAt = Date.now(); entry.lastError = error; const tmp = `${filePath}.${process.pid}.tmp`; await _nodeFs.default.promises.writeFile(tmp, JSON.stringify(entry, null, 2), { encoding: "utf-8", mode: 384 }); await _nodeFs.default.promises.rename(tmp, filePath); } //#endregion //#region src/auto-reply/reply/reply-tags.ts function extractReplyToTag(text, currentMessageId) { const result = (0, _piEmbeddedHelpersDmr3bcbH.Rt)(text, { currentMessageId, stripAudioTag: false }); return { cleaned: result.text, replyToId: result.replyToId, replyToCurrent: result.replyToCurrent, hasTag: result.hasReplyTag }; } //#endregion //#region src/auto-reply/reply/reply-threading.ts function resolveReplyToMode(cfg, channel, accountId, chatType) { const provider = (0, _pluginsBhm3N6Y.r)(channel); if (!provider) return "all"; return (0, _thinkingCfIPyoMg.d)(provider)?.threading?.resolveReplyToMode?.({ cfg, accountId, chatType }) ?? "all"; } function createReplyToModeFilter(mode, opts = {}) { let hasThreaded = false; return (payload) => { if (!payload.replyToId) return payload; if (mode === "off") { const isExplicit = Boolean(payload.replyToTag) || Boolean(payload.replyToCurrent); if (opts.allowExplicitReplyTagsWhenOff && isExplicit) return payload; return { ...payload, replyToId: void 0 }; } if (mode === "all") return payload; if (hasThreaded) return { ...payload, replyToId: void 0 }; hasThreaded = true; return payload; }; } function createReplyToModeFilterForChannel(mode, channel) { const provider = (0, _pluginsBhm3N6Y.r)(channel); const isWebchat = (typeof channel === "string" ? channel.trim().toLowerCase() : void 0) === "webchat"; const dock = provider ? (0, _thinkingCfIPyoMg.d)(provider) : void 0; return createReplyToModeFilter(mode, { allowExplicitReplyTagsWhenOff: provider ? dock?.threading?.allowExplicitReplyTagsWhenOff ?? dock?.threading?.allowTagsWhenOff ?? true : isWebchat }); } //#endregion //#region src/auto-reply/reply/reply-payloads.ts function resolveReplyThreadingForPayload(params) { const implicitReplyToId = params.implicitReplyToId?.trim() || void 0; const currentMessageId = params.currentMessageId?.trim() || void 0; let resolved = params.payload.replyToId || params.payload.replyToCurrent === false || !implicitReplyToId ? params.payload : { ...params.payload, replyToId: implicitReplyToId }; if (typeof resolved.text === "string" && resolved.text.includes("[[")) { const { cleaned, replyToId, replyToCurrent, hasTag } = extractReplyToTag(resolved.text, currentMessageId); resolved = { ...resolved, text: cleaned ? cleaned : void 0, replyToId: replyToId ?? resolved.replyToId, replyToTag: hasTag || resolved.replyToTag, replyToCurrent: replyToCurrent || resolved.replyToCurrent }; } if (resolved.replyToCurrent && !resolved.replyToId && currentMessageId) resolved = { ...resolved, replyToId: currentMessageId }; return resolved; } function applyReplyTagsToPayload(payload, currentMessageId) { return resolveReplyThreadingForPayload({ payload, currentMessageId }); } function isRenderablePayload(payload) { return Boolean(payload.text || payload.mediaUrl || payload.mediaUrls && payload.mediaUrls.length > 0 || payload.audioAsVoice || payload.channelData); } function shouldSuppressReasoningPayload(payload) { return payload.isReasoning === true; } function applyReplyThreading(params) { const { payloads, replyToMode, replyToChannel, currentMessageId } = params; const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel); const implicitReplyToId = currentMessageId?.trim() || void 0; return payloads.map((payload) => resolveReplyThreadingForPayload({ payload, implicitReplyToId, currentMessageId })).filter(isRenderablePayload).map(applyReplyToMode); } function filterMessagingToolDuplicates(params) { const { payloads, sentTexts } = params; if (sentTexts.length === 0) return payloads; return payloads.filter((payload) => !(0, _piEmbeddedHelpersDmr3bcbH.i)(payload.text ?? "", sentTexts)); } function filterMessagingToolMediaDuplicates(params) { const normalizeMediaForDedupe = (value) => { const trimmed = value.trim(); if (!trimmed) return ""; if (!trimmed.toLowerCase().startsWith("file://")) return trimmed; try { const parsed = new URL(trimmed); if (parsed.protocol === "file:") return decodeURIComponent(parsed.pathname || ""); } catch {} return trimmed.replace(/^file:\/\//i, ""); }; const { payloads, sentMediaUrls } = params; if (sentMediaUrls.length === 0) return payloads; const sentSet = new Set(sentMediaUrls.map(normalizeMediaForDedupe).filter(Boolean)); return payloads.map((payload) => { const mediaUrl = payload.mediaUrl; const mediaUrls = payload.mediaUrls; const stripSingle = mediaUrl && sentSet.has(normalizeMediaForDedupe(mediaUrl)); const filteredUrls = mediaUrls?.filter((u) => !sentSet.has(normalizeMediaForDedupe(u))); if (!stripSingle && (!mediaUrls || filteredUrls?.length === mediaUrls.length)) return payload; return { ...payload, mediaUrl: stripSingle ? void 0 : mediaUrl, mediaUrls: filteredUrls?.length ? filteredUrls : void 0 }; }); } const PROVIDER_ALIAS_MAP = { lark: "feishu" }; function normalizeProviderForComparison(value) { const trimmed = value?.trim(); if (!trimmed) return; const lowered = trimmed.toLowerCase(); const normalizedChannel = (0, _pluginsBhm3N6Y.r)(trimmed); if (normalizedChannel) return normalizedChannel; return PROVIDER_ALIAS_MAP[lowered] ?? lowered; } function normalizeThreadIdForComparison(value) { const trimmed = value?.trim(); if (!trimmed) return; if (/^-?\d+$/.test(trimmed)) return String(Number.parseInt(trimmed, 10)); return trimmed.toLowerCase(); } function resolveTargetProviderForComparison(params) { const targetProvider = normalizeProviderForComparison(params.targetProvider); if (!targetProvider || targetProvider === "message") return params.currentProvider; return targetProvider; } function targetsMatchForSuppression(params) { if (params.provider !== "telegram") return params.targetKey === params.originTarget; const origin = (0, _targetsBP_LjgWp.r)(params.originTarget); const target = (0, _targetsBP_LjgWp.r)(params.targetKey); const targetThreadId = normalizeThreadIdForComparison(params.targetThreadId) ?? (target.messageThreadId != null ? String(target.messageThreadId) : void 0); const originThreadId = origin.messageThreadId != null ? String(origin.messageThreadId) : void 0; if (origin.chatId.trim().toLowerCase() !== target.chatId.trim().toLowerCase()) return false; if (originThreadId && targetThreadId != null) return originThreadId === targetThreadId; if (originThreadId && targetThreadId == null) return false; if (!originThreadId && targetThreadId != null) return false; return true; } function shouldSuppressMessagingToolReplies(params) { const provider = normalizeProviderForComparison(params.messageProvider); if (!provider) return false; const originTarget = normalizeTargetForProvider(provider, params.originatingTo); if (!originTarget) return false; const originAccount = (0, _runWithConcurrency2ga3CMk.dt)(params.accountId); const sentTargets = params.messagingToolSentTargets ?? []; if (sentTargets.length === 0) return false; return sentTargets.some((target) => { const targetProvider = resolveTargetProviderForComparison({ currentProvider: provider, targetProvider: target?.provider }); if (targetProvider !== provider) return false; const targetKey = normalizeTargetForProvider(targetProvider, target.to); if (!targetKey) return false; const targetAccount = (0, _runWithConcurrency2ga3CMk.dt)(target.accountId); if (originAccount && targetAccount && originAccount !== targetAccount) return false; return targetsMatchForSuppression({ provider, originTarget, targetKey, targetThreadId: target.threadId }); }); } //#endregion //#region src/infra/outbound/payloads.ts function mergeMediaUrls(...lists) { const seen = /* @__PURE__ */new Set(); const merged = []; for (const list of lists) { if (!list) continue; for (const entry of list) { const trimmed = entry?.trim(); if (!trimmed) continue; if (seen.has(trimmed)) continue; seen.add(trimmed); merged.push(trimmed); } } return merged; } function normalizeReplyPayloadsForDelivery(payloads) { const normalized = []; for (const payload of payloads) { if (shouldSuppressReasoningPayload(payload)) continue; const parsed = parseReplyDirectives(payload.text ?? ""); const explicitMediaUrls = payload.mediaUrls ?? parsed.mediaUrls; const explicitMediaUrl = payload.mediaUrl ?? parsed.mediaUrl; const mergedMedia = mergeMediaUrls(explicitMediaUrls, explicitMediaUrl ? [explicitMediaUrl] : void 0); const resolvedMediaUrl = (explicitMediaUrls?.length ?? 0) > 1 ? void 0 : explicitMediaUrl; const next = { ...payload, text: parsed.text ?? "", mediaUrls: mergedMedia.length ? mergedMedia : void 0, mediaUrl: resolvedMediaUrl, replyToId: payload.replyToId ?? parsed.replyToId, replyToTag: payload.replyToTag || parsed.replyToTag, replyToCurrent: payload.replyToCurrent || parsed.replyToCurrent, audioAsVoice: Boolean(payload.audioAsVoice || parsed.audioAsVoice) }; if (parsed.isSilent && mergedMedia.length === 0) continue; if (!isRenderablePayload(next)) continue; normalized.push(next); } return normalized; } function normalizeOutboundPayloads(payloads) { const normalizedPayloads = []; for (const payload of normalizeReplyPayloadsForDelivery(payloads)) { const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); const channelData = payload.channelData; const hasChannelData = Boolean(channelData && Object.keys(channelData).length > 0); const text = payload.text ?? ""; if (!text && mediaUrls.length === 0 && !hasChannelData) continue; normalizedPayloads.push({ text, mediaUrls, ...(hasChannelData ? { channelData } : {}) }); } return normalizedPayloads; } function normalizeOutboundPayloadsForJson(payloads) { const normalized = []; for (const payload of normalizeReplyPayloadsForDelivery(payloads)) normalized.push({ text: payload.text ?? "", mediaUrl: payload.mediaUrl ?? null, mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : void 0), channelData: payload.channelData }); return normalized; } function formatOutboundPayloadLog(payload) { const lines = []; if (payload.text) lines.push(payload.text.trimEnd()); for (const url of payload.mediaUrls) lines.push(`MEDIA:${url}`); return lines.join("\n"); } //#endregion //#region src/infra/outbound/sanitize-text.ts /** * Sanitize model output for plain-text messaging surfaces. * * LLMs occasionally produce HTML tags (`
`, ``, ``, etc.) that render * correctly on web but appear as literal text on WhatsApp, Signal, SMS, and IRC. * * Converts common inline HTML to lightweight-markup equivalents used by * WhatsApp/Signal/Telegram and strips any remaining tags. * * @see https://github.com/openclaw/openclaw/issues/31884 * @see https://github.com/openclaw/openclaw/issues/18558 */ /** Channels where HTML tags should be converted/stripped. */ const PLAIN_TEXT_SURFACES = new Set([ "whatsapp", "signal", "sms", "irc", "telegram", "imessage", "googlechat"] ); /** Returns `true` when the channel cannot render raw HTML. */ function isPlainTextSurface(channelId) { return PLAIN_TEXT_SURFACES.has(channelId.toLowerCase()); } /** * Convert common HTML tags to their plain-text/lightweight-markup equivalents * and strip anything that remains. * * The function is intentionally conservative — it only targets tags that models * are known to produce and avoids false positives on angle brackets in normal * prose (e.g. `a < b`). */ function sanitizeForPlainText(text) { return text.replace(/<((?:https?:\/\/|mailto:)[^<>\s]+)>/gi, "$1").replace(//gi, "\n").replace(/<\/?(p|div)>/gi, "\n").replace(/<(b|strong)>(.*?)<\/\1>/gi, "*$2*").replace(/<(i|em)>(.*?)<\/\1>/gi, "_$2_").replace(/<(s|strike|del)>(.*?)<\/\1>/gi, "~$2~").replace(/(.*?)<\/code>/gi, "`$1`").replace(/]*>(.*?)<\/h[1-6]>/gi, "\n*$1*\n").replace(/]*>(.*?)<\/li>/gi, "• $1\n").replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi, "").replace(/\n{3,}/g, "\n\n"); } //#endregion //#region src/infra/outbound/deliver.ts const log = (0, _loggerU3s76KST.a)("outbound/deliver"); const TELEGRAM_TEXT_LIMIT = 4096; async function createChannelHandler(params) { const outbound = await loadChannelOutboundAdapter(params.channel); const handler = createPluginHandler({ ...params, outbound }); if (!handler) throw new Error(`Outbound not configured for channel: ${params.channel}`); return handler; } function createPluginHandler(params) { const outbound = params.outbound; if (!outbound?.sendText) return null; const baseCtx = createChannelOutboundContextBase(params); const sendText = outbound.sendText; const sendMedia = outbound.sendMedia; const chunker = outbound.chunker ?? null; const chunkerMode = outbound.chunkerMode; const resolveCtx = (overrides) => ({ ...baseCtx, replyToId: overrides?.replyToId ?? baseCtx.replyToId, threadId: overrides?.threadId ?? baseCtx.threadId }); return { chunker, chunkerMode, textChunkLimit: outbound.textChunkLimit, supportsMedia: Boolean(sendMedia), sendPayload: outbound.sendPayload ? async (payload, overrides) => outbound.sendPayload({ ...resolveCtx(overrides), text: payload.text ?? "", mediaUrl: payload.mediaUrl, payload }) : void 0, sendText: async (text, overrides) => sendText({ ...resolveCtx(overrides), text }), sendMedia: async (caption, mediaUrl, overrides) => { if (sendMedia) return sendMedia({ ...resolveCtx(overrides), text: caption, mediaUrl }); return sendText({ ...resolveCtx(overrides), text: caption }); } }; } function createChannelOutboundContextBase(params) { return { cfg: params.cfg, to: params.to, accountId: params.accountId, replyToId: params.replyToId, threadId: params.threadId, identity: params.identity, gifPlayback: params.gifPlayback, deps: params.deps, silent: params.silent, mediaLocalRoots: params.mediaLocalRoots }; } const isAbortError = (err) => err instanceof Error && err.name === "AbortError"; function hasMediaPayload(payload) { return Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; } function hasChannelDataPayload(payload) { return Boolean(payload.channelData && Object.keys(payload.channelData).length > 0); } function normalizePayloadForChannelDelivery(payload, channelId) { const hasMedia = hasMediaPayload(payload); const hasChannelData = hasChannelDataPayload(payload); const rawText = typeof payload.text === "string" ? payload.text : ""; const normalizedText = channelId === "whatsapp" ? rawText.replace(/^(?:[ \t]*\r?\n)+/, "") : rawText; if (!normalizedText.trim()) { if (!hasMedia && !hasChannelData) return null; return { ...payload, text: "" }; } if (normalizedText === rawText) return payload; return { ...payload, text: normalizedText }; } function normalizePayloadsForChannelDelivery(payloads, channel) { const normalizedPayloads = []; for (const payload of normalizeReplyPayloadsForDelivery(payloads)) { let sanitizedPayload = payload; if (isPlainTextSurface(channel) && payload.text) { if (!(channel === "telegram" && payload.channelData)) sanitizedPayload = { ...payload, text: sanitizeForPlainText(payload.text) }; } const normalized = normalizePayloadForChannelDelivery(sanitizedPayload, channel); if (normalized) normalizedPayloads.push(normalized); } return normalizedPayloads; } function buildPayloadSummary(payload) { return { text: payload.text ?? "", mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []), channelData: payload.channelData }; } function createMessageSentEmitter(params) { const hasMessageSentHooks = params.hookRunner?.hasHooks("message_sent") ?? false; const canEmitInternalHook = Boolean(params.sessionKeyForInternalHooks); const emitMessageSent = (event) => { if (!hasMessageSentHooks && !canEmitInternalHook) return; const canonical = buildCanonicalSentMessageHookContext({ to: params.to, content: event.content, success: event.success, error: event.error, channelId: params.channel, accountId: params.accountId ?? void 0, conversationId: params.to, messageId: event.messageId, isGroup: params.mirrorIsGroup, groupId: params.mirrorGroupId }); if (hasMessageSentHooks) fireAndForgetHook(params.hookRunner.runMessageSent(toPluginMessageSentEvent(canonical), toPluginMessageContext(canonical)), "deliverOutboundPayloads: message_sent plugin hook failed", (message) => { log.warn(message); }); if (!canEmitInternalHook) return; fireAndForgetHook((0, _configDiiPndBn.Vt)((0, _configDiiPndBn.Bt)("message", "sent", params.sessionKeyForInternalHooks, toInternalMessageSentContext(canonical))), "deliverOutboundPayloads: message:sent internal hook failed", (message) => { log.warn(message); }); }; return { emitMessageSent, hasMessageSentHooks }; } async function applyMessageSendingHook(params) { if (!params.enabled) return { cancelled: false, payload: params.payload, payloadSummary: params.payloadSummary }; try { const sendingResult = await params.hookRunner.runMessageSending({ to: params.to, content: params.payloadSummary.text, metadata: { channel: params.channel, accountId: params.accountId, mediaUrls: params.payloadSummary.mediaUrls } }, { channelId: params.channel, accountId: params.accountId ?? void 0 }); if (sendingResult?.cancel) return { cancelled: true, payload: params.payload, payloadSummary: params.payloadSummary }; if (sendingResult?.content == null) return { cancelled: false, payload: params.payload, payloadSummary: params.payloadSummary }; return { cancelled: false, payload: { ...params.payload, text: sendingResult.content }, payloadSummary: { ...params.payloadSummary, text: sendingResult.content } }; } catch { return { cancelled: false, payload: params.payload, payloadSummary: params.payloadSummary }; } } async function deliverOutboundPayloads(params) { const { channel, to, payloads } = params; const queueId = params.skipQueue ? null : await enqueueDelivery({ channel, to, accountId: params.accountId, payloads, threadId: params.threadId, replyToId: params.replyToId, bestEffort: params.bestEffort, gifPlayback: params.gifPlayback, silent: params.silent, mirror: params.mirror }).catch(() => null); let hadPartialFailure = false; const wrappedParams = params.onError ? { ...params, onError: (err, payload) => { hadPartialFailure = true; params.onError(err, payload); } } : params; try { const results = await deliverOutboundPayloadsCore(wrappedParams); if (queueId) if (hadPartialFailure) await failDelivery(queueId, "partial delivery failure (bestEffort)").catch(() => {});else await ackDelivery(queueId).catch(() => {}); return results; } catch (err) { if (queueId) if (isAbortError(err)) await ackDelivery(queueId).catch(() => {});else await failDelivery(queueId, err instanceof Error ? err.message : String(err)).catch(() => {}); throw err; } } /** Core delivery logic (extracted for queue wrapper). */ async function deliverOutboundPayloadsCore(params) { const { cfg, channel, to, payloads } = params; const accountId = params.accountId; const deps = params.deps; const abortSignal = params.abortSignal; const sendSignal = params.deps?.sendSignal ?? _sendCWALh87S.t; const mediaLocalRoots = (0, _localRootsZhwi3hFj.t)(cfg, params.session?.agentId ?? params.mirror?.agentId); const results = []; const handler = await createChannelHandler({ cfg, channel, to, deps, accountId, replyToId: params.replyToId, threadId: params.threadId, identity: params.identity, gifPlayback: params.gifPlayback, silent: params.silent, mediaLocalRoots }); const configuredTextLimit = handler.chunker ? (0, _irKp5uANes.f)(cfg, channel, accountId, { fallbackLimit: handler.textChunkLimit }) : void 0; const textLimit = channel === "telegram" && typeof configuredTextLimit === "number" ? Math.min(configuredTextLimit, TELEGRAM_TEXT_LIMIT) : configuredTextLimit; const chunkMode = handler.chunker ? (0, _irKp5uANes.d)(cfg, channel, accountId) : "length"; const isSignalChannel = channel === "signal"; const signalTableMode = isSignalChannel ? (0, _irKp5uANes.i)({ cfg, channel: "signal", accountId }) : "code"; const signalMaxBytes = isSignalChannel ? resolveChannelMediaMaxBytes({ cfg, resolveChannelLimitMb: ({ cfg, accountId }) => cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ?? cfg.channels?.signal?.mediaMaxMb, accountId }) : void 0; const sendTextChunks = async (text, overrides) => { throwIfAborted(abortSignal); if (!handler.chunker || textLimit === void 0) { results.push(await handler.sendText(text, overrides)); return; } if (chunkMode === "newline") { const blockChunks = (handler.chunkerMode ?? "text") === "markdown" ? (0, _irKp5uANes.c)(text, textLimit, "newline") : (0, _irKp5uANes.o)(text, textLimit); if (!blockChunks.length && text) blockChunks.push(text); for (const blockChunk of blockChunks) { const chunks = handler.chunker(blockChunk, textLimit); if (!chunks.length && blockChunk) chunks.push(blockChunk); for (const chunk of chunks) { throwIfAborted(abortSignal); results.push(await handler.sendText(chunk, overrides)); } } return; } const chunks = handler.chunker(text, textLimit); for (const chunk of chunks) { throwIfAborted(abortSignal); results.push(await handler.sendText(chunk, overrides)); } }; const sendSignalText = async (text, styles) => { throwIfAborted(abortSignal); return { channel: "signal", ...(await sendSignal(to, text, { cfg, maxBytes: signalMaxBytes, accountId: accountId ?? void 0, textMode: "plain", textStyles: styles })) }; }; const sendSignalTextChunks = async (text) => { throwIfAborted(abortSignal); let signalChunks = textLimit === void 0 ? (0, _sendCWALh87S.c)(text, Number.POSITIVE_INFINITY, { tableMode: signalTableMode }) : (0, _sendCWALh87S.c)(text, textLimit, { tableMode: signalTableMode }); if (signalChunks.length === 0 && text) signalChunks = [{ text, styles: [] }]; for (const chunk of signalChunks) { throwIfAborted(abortSignal); results.push(await sendSignalText(chunk.text, chunk.styles)); } }; const sendSignalMedia = async (caption, mediaUrl) => { throwIfAborted(abortSignal); const formatted = (0, _sendCWALh87S.c)(caption, Number.POSITIVE_INFINITY, { tableMode: signalTableMode })[0] ?? { text: caption, styles: [] }; return { channel: "signal", ...(await sendSignal(to, formatted.text, { cfg, mediaUrl, maxBytes: signalMaxBytes, accountId: accountId ?? void 0, textMode: "plain", textStyles: formatted.styles, mediaLocalRoots })) }; }; const normalizedPayloads = normalizePayloadsForChannelDelivery(payloads, channel); const hookRunner = getGlobalHookRunner(); const sessionKeyForInternalHooks = params.mirror?.sessionKey ?? params.session?.key; const mirrorIsGroup = params.mirror?.isGroup; const mirrorGroupId = params.mirror?.groupId; const { emitMessageSent, hasMessageSentHooks } = createMessageSentEmitter({ hookRunner, channel, to, accountId, sessionKeyForInternalHooks, mirrorIsGroup, mirrorGroupId }); const hasMessageSendingHooks = hookRunner?.hasHooks("message_sending") ?? false; if (hasMessageSentHooks && params.session?.agentId && !sessionKeyForInternalHooks) log.warn("deliverOutboundPayloads: session.agentId present without session key; internal message:sent hook will be skipped", { channel, to, agentId: params.session.agentId }); for (const payload of normalizedPayloads) { let payloadSummary = buildPayloadSummary(payload); try { throwIfAborted(abortSignal); const hookResult = await applyMessageSendingHook({ hookRunner, enabled: hasMessageSendingHooks, payload, payloadSummary, to, channel, accountId }); if (hookResult.cancelled) continue; const effectivePayload = hookResult.payload; payloadSummary = hookResult.payloadSummary; params.onPayload?.(payloadSummary); const sendOverrides = { replyToId: effectivePayload.replyToId ?? params.replyToId ?? void 0, threadId: params.threadId ?? void 0 }; if (handler.sendPayload && effectivePayload.channelData) { const delivery = await handler.sendPayload(effectivePayload, sendOverrides); results.push(delivery); emitMessageSent({ success: true, content: payloadSummary.text, messageId: delivery.messageId }); continue; } if (payloadSummary.mediaUrls.length === 0) { const beforeCount = results.length; if (isSignalChannel) await sendSignalTextChunks(payloadSummary.text);else await sendTextChunks(payloadSummary.text, sendOverrides); const messageId = results.at(-1)?.messageId; emitMessageSent({ success: results.length > beforeCount, content: payloadSummary.text, messageId }); continue; } if (!handler.supportsMedia) { log.warn("Plugin outbound adapter does not implement sendMedia; media URLs will be dropped and text fallback will be used", { channel, to, mediaCount: payloadSummary.mediaUrls.length }); const fallbackText = payloadSummary.text.trim(); if (!fallbackText) throw new Error("Plugin outbound adapter does not implement sendMedia and no text fallback is available for media payload"); const beforeCount = results.length; await sendTextChunks(fallbackText, sendOverrides); const messageId = results.at(-1)?.messageId; emitMessageSent({ success: results.length > beforeCount, content: payloadSummary.text, messageId }); continue; } let first = true; let lastMessageId; for (const url of payloadSummary.mediaUrls) { throwIfAborted(abortSignal); const caption = first ? payloadSummary.text : ""; first = false; if (isSignalChannel) { const delivery = await sendSignalMedia(caption, url); results.push(delivery); lastMessageId = delivery.messageId; } else { const delivery = await handler.sendMedia(caption, url, sendOverrides); results.push(delivery); lastMessageId = delivery.messageId; } } emitMessageSent({ success: true, content: payloadSummary.text, messageId: lastMessageId }); } catch (err) { emitMessageSent({ success: false, content: payloadSummary.text, error: err instanceof Error ? err.message : String(err) }); if (!params.bestEffort) throw err; params.onError?.(err, payloadSummary); } } if (params.mirror && results.length > 0) { const mirrorText = (0, _piEmbeddedHelpersDmr3bcbH.mt)({ text: params.mirror.text, mediaUrls: params.mirror.mediaUrls }); if (mirrorText) await (0, _piEmbeddedHelpersDmr3bcbH.pt)({ agentId: params.mirror.agentId, sessionKey: params.mirror.sessionKey, text: mirrorText }); } return results; } //#endregion /* v9-e7620ea15282cd4c */