UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

468 lines (467 loc) 20.8 kB
import { l as asNonNegativeFiniteNumber, u as asPositiveFiniteNumber } from "./number-coercion-CLj0HTDM.js"; import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { c as normalizeOptionalLowercaseString, g as readStringValue, l as normalizeOptionalString, u as normalizeOptionalStringifiedId } from "./string-coerce-CIXf7egm.js"; import { t as AUTOMATIONS_TOOL_NAME } from "./automations-tool-name-DBMZPbPL.js"; import { c as normalizeToolPolicyName } from "./tool-policy-shared-DIyS0iQC.js"; import "./tool-policy-Wi0C45cX.js"; import { a as normalizeChannelId, t as getChannelPlugin } from "./registry-Cz6cv4VC.js"; import "./plugins-Cozj1Enf.js"; import { d as normalizeLegacyInteractiveReply, f as normalizeMessagePresentation } from "./payload-DYE7bQ2k.js"; import { o as readToolResultDetails } from "./tool-result-error-CbDLJzG-.js"; import { a as normalizeTargetForProvider } from "./target-normalization-RYHhn0bW.js"; import { f as isMessagingToolTargetEvidenceAction } from "./tool-loop-detection-C39fRNx6.js"; import { _ as isDeliveredCurrentSourceReply } from "./message-action-normalization-CgPHpKwA.js"; import { a as extractToolResultText } from "./embedded-agent-tool-results-ZMp1V0-a.js"; //#region src/agents/embedded-agent-messaging-extraction.ts /** Extracts message delivery evidence from embedded-agent tool calls and results. */ function extractMessagingToolSourceReplyPayload(result) { const details = readToolResultDetails(result); if (!details || details.sourceReplySink !== "internal-ui") return; const status = normalizeOptionalLowercaseString(details.deliveryStatus); if (status && status !== "sent") return; const sourceReply = asOptionalRecord(details.sourceReply) ?? details; const payload = {}; const text = readStringValue(sourceReply.text) ?? readStringValue(details.message); if (text) payload.text = text; const mediaUrl = readStringValue(sourceReply.mediaUrl) ?? readStringValue(details.mediaUrl); if (mediaUrl) payload.mediaUrl = mediaUrl; const mediaUrls = (Array.isArray(sourceReply.mediaUrls) ? sourceReply.mediaUrls : Array.isArray(details.mediaUrls) ? details.mediaUrls : []).filter((value) => typeof value === "string"); if (mediaUrls.length > 0) payload.mediaUrls = mediaUrls; if (Array.isArray(sourceReply.attachments)) { const attachments = sourceReply.attachments.flatMap((value) => { const attachment = asOptionalRecord(value); if (!attachment) return []; const durationMs = asNonNegativeFiniteNumber(attachment.durationMs); const width = asNonNegativeFiniteNumber(attachment.width); const height = asNonNegativeFiniteNumber(attachment.height); const attachmentPath = readStringValue(attachment.path); const attachmentUrl = readStringValue(attachment.url); const attachmentMediaUrl = readStringValue(attachment.mediaUrl); const filePath = readStringValue(attachment.filePath); const mimeType = readStringValue(attachment.mimeType); const name = readStringValue(attachment.name); return [{ ...attachmentPath ? { path: attachmentPath } : {}, ...attachmentUrl ? { url: attachmentUrl } : {}, ...attachmentMediaUrl ? { mediaUrl: attachmentMediaUrl } : {}, ...filePath ? { filePath } : {}, ...mimeType ? { mimeType } : {}, ...name ? { name } : {}, ...typeof attachment.trustedLocalMedia === "boolean" ? { trustedLocalMedia: attachment.trustedLocalMedia } : {}, ...durationMs !== void 0 ? { durationMs } : {}, ...width !== void 0 ? { width } : {}, ...height !== void 0 ? { height } : {} }]; }); if (attachments.length > 0) payload.attachments = attachments; } if (typeof sourceReply.trustedLocalMedia === "boolean") payload.trustedLocalMedia = sourceReply.trustedLocalMedia; if (sourceReply.audioAsVoice === true || details.audioAsVoice === true) payload.audioAsVoice = true; const presentation = normalizeMessagePresentation(sourceReply.presentation); if (presentation) payload.presentation = presentation; const interactive = normalizeLegacyInteractiveReply(sourceReply.interactive); if (interactive) payload.interactive = interactive; const channelData = asOptionalRecord(sourceReply.channelData); if (channelData) payload.channelData = { ...channelData }; const idempotencyKey = readStringValue(sourceReply.idempotencyKey) ?? readStringValue(details.idempotencyKey); if (idempotencyKey) payload.idempotencyKey = idempotencyKey; if (details.sourceReplyTranscriptOwner === true) payload.transcriptOwner = true; return Object.keys(payload).length > 0 ? payload : void 0; } function resolveMessageToolTarget(params) { const directTarget = normalizeOptionalString(params.args.target) ?? normalizeOptionalString(params.args.to) ?? normalizeOptionalString(params.args.channelId); if (directTarget) return directTarget; const aliases = params.providerId ? getChannelPlugin(params.providerId)?.actions?.messageActionTargetAliases?.[params.action]?.deliveryTargetAliases : void 0; for (const alias of aliases ?? []) { const aliasTarget = normalizeOptionalStringifiedId(params.args[alias]); if (aliasTarget) return aliasTarget; } return params.currentMessagingTarget ?? params.currentChannelId; } function resolveMessagingToolThreadEvidence(params) { const threading = getChannelPlugin(params.providerId)?.threading; const autoThreadResolver = params.allowImplicitThread ? threading?.resolveAutoThreadId : void 0; const replyTransport = params.replyToId ? threading?.resolveReplyTransport?.({ cfg: params.options?.config ?? {}, accountId: params.accountId, threadId: params.threadId, replyToId: params.replyToId }) : void 0; const transportThreadId = normalizeOptionalStringifiedId(replyTransport?.threadId); const replyToThreadId = replyTransport?.threadId === null ? normalizeOptionalString(replyTransport.replyToId) : void 0; const explicitThreadId = transportThreadId ?? replyToThreadId ?? params.threadId; const currentChannelId = normalizeOptionalString(params.options?.currentChannelId); const currentMessagingTarget = normalizeOptionalString(params.options?.currentMessagingTarget); const currentThreadId = normalizeOptionalString(params.options?.currentThreadId); const replyToMode = params.options?.replyToMode ?? (currentThreadId ? "all" : void 0); const canResolveCurrentThread = Boolean((currentChannelId || currentMessagingTarget) && currentThreadId); const resolvedCurrentThreadId = !explicitThreadId && !params.threadSuppressed && autoThreadResolver && canResolveCurrentThread ? autoThreadResolver({ cfg: params.options?.config ?? {}, accountId: params.accountId, to: params.to, replyToId: params.replyToId, toolContext: { currentChannelId, currentMessagingTarget, currentThreadTs: currentThreadId, currentMessageId: params.options?.currentMessageId, replyToMode, hasRepliedRef: params.options?.hasRepliedRef } }) : void 0; const threadImplicit = !explicitThreadId && !params.threadSuppressed && Boolean(autoThreadResolver) && (!canResolveCurrentThread || Boolean(resolvedCurrentThreadId)); return { ...explicitThreadId ?? resolvedCurrentThreadId ? { threadId: explicitThreadId ?? resolvedCurrentThreadId } : {}, ...threadImplicit ? { threadImplicit: true } : {}, ...params.threadSuppressed ? { threadSuppressed: true } : {} }; } function extractMessagingToolSend(toolName, args, options) { const action = normalizeOptionalString(args.action) ?? ""; const accountId = normalizeOptionalString(args.accountId); if (toolName === "conversations_send" || toolName === "conversations_turn") { const conversationRef = normalizeOptionalString(args.conversationRef); return conversationRef ? { tool: toolName, provider: "conversation", to: conversationRef } : void 0; } if (toolName === "message") { if (!isMessagingToolTargetEvidenceAction(toolName, args)) return; const providerRaw = normalizeOptionalString(args.provider) ?? ""; const channelRaw = normalizeOptionalString(args.channel) ?? ""; const providerHint = providerRaw || channelRaw; const providerId = providerHint ? normalizeChannelId(providerHint) : null; const toRaw = resolveMessageToolTarget({ action, args, providerId, currentChannelId: options?.currentChannelId, currentMessagingTarget: options?.currentMessagingTarget }); if (!toRaw) return; const provider = providerId ?? normalizeOptionalLowercaseString(providerHint) ?? "message"; const pluginExtractionArgs = { ...args, to: toRaw }; const pluginExtracted = providerId ? getChannelPlugin(providerId)?.actions?.extractToolSend?.({ args: pluginExtractionArgs }) : null; const to = normalizeTargetForProvider(provider, pluginExtracted?.to ?? toRaw); const resolvedAccountId = normalizeOptionalString(pluginExtracted?.accountId) ?? accountId; const threadId = normalizeOptionalString(pluginExtracted?.threadId) ?? normalizeOptionalString(args.threadId); const replyToId = normalizeOptionalString(args.replyTo); const outboundReplyToId = action === "send" ? replyToId : void 0; const threadSuppressed = pluginExtracted?.threadSuppressed === true || args.topLevel === true || args.threadId === null; return to ? { tool: toolName, provider, accountId: resolvedAccountId, to, ...providerId ? resolveMessagingToolThreadEvidence({ providerId, to, accountId: resolvedAccountId, threadId, replyToId: outboundReplyToId, allowImplicitThread: pluginExtracted ? pluginExtracted.threadImplicit === true : true, threadSuppressed, options }) : { ...threadId ? { threadId } : {}, ...threadSuppressed ? { threadSuppressed: true } : {} } } : void 0; } const providerId = normalizeChannelId(toolName); if (!providerId) return; const extracted = getChannelPlugin(providerId)?.actions?.extractToolSend?.({ args }); if (!extracted?.to) return; const to = normalizeTargetForProvider(providerId, extracted.to); const threadId = normalizeOptionalString(extracted.threadId); const threadSuppressed = extracted.threadSuppressed === true; const extractedAccountId = normalizeOptionalString(extracted.accountId) ?? accountId; const nativeReplyToMode = options?.replyToMode; const nativeSingleUseMode = nativeReplyToMode === "first" || nativeReplyToMode === "batched"; const canResolveNativeImplicitThread = extracted.threadImplicit === true && nativeReplyToMode !== void 0 && (!nativeSingleUseMode || options?.hasRepliedRef !== void 0); return to ? { tool: toolName, provider: providerId, accountId: extractedAccountId, to, ...resolveMessagingToolThreadEvidence({ providerId, to, accountId: extractedAccountId, threadId, allowImplicitThread: canResolveNativeImplicitThread, threadSuppressed, options }) } : void 0; } /** Reconciles pending send evidence with the provider's successful action result. */ function extractMessagingToolSendResult(pending, result) { const providerId = normalizeChannelId(pending.provider); const extracted = providerId ? getChannelPlugin(providerId)?.actions?.extractToolSendResult?.({ result, send: { to: pending.to ?? "", accountId: pending.accountId, threadId: pending.threadId, threadImplicit: pending.threadImplicit, threadSuppressed: pending.threadSuppressed } }) : null; if (!extracted?.to) return pending; const threadEvidence = normalizeOptionalString(extracted.threadId) != null || extracted.threadImplicit === true || extracted.threadSuppressed === true ? extracted : pending; return { ...pending, ...extracted, accountId: normalizeOptionalString(extracted.accountId) ?? pending.accountId, to: normalizeTargetForProvider(providerId ?? pending.provider, extracted.to), threadId: normalizeOptionalString(threadEvidence.threadId), threadImplicit: threadEvidence.threadImplicit === true ? true : void 0, threadSuppressed: threadEvidence.threadSuppressed === true ? true : void 0 }; } function isDeliveredMessagingToolSendToCurrentSource(params) { const send = params.send; if (!send?.to) return false; return isDeliveredCurrentSourceReply({ action: "send", channel: send.provider, accountId: send.accountId, currentAccountId: params.currentAccountId, actionParams: { target: send.to, ...send.threadSuppressed ? { topLevel: true } : send.threadId ? { threadId: send.threadId } : {} }, cfg: params.config ?? {}, sessionKey: params.sessionKey, toolContext: { currentChannelProvider: params.currentProvider, currentChannelId: params.currentChannelId, currentMessagingTarget: params.currentMessagingTarget, currentThreadTs: params.currentThreadId }, deliveredPayload: params.deliveredPayload }); } //#endregion //#region src/agents/embedded-agent-tool-media.ts /** Extracts and trust-filters media from embedded-agent tool results. */ function pushUniqueMessagingMediaUrl(urls, seen, value) { if (typeof value !== "string") return; const normalized = value.trim(); if (!normalized || seen.has(normalized)) return; seen.add(normalized); urls.push(normalized); } /** Collects messaging attachment references from tool-call arguments or result records. */ function collectMessagingMediaUrlsFromRecord(record) { const urls = []; const seen = /* @__PURE__ */ new Set(); const pushAttachment = (value) => { if (!value || typeof value !== "object" || Array.isArray(value)) return; const attachment = value; for (const candidate of [ attachment.media, attachment.mediaUrl, attachment.path, attachment.filePath, attachment.fileUrl, attachment.url ]) pushUniqueMessagingMediaUrl(urls, seen, candidate); }; for (const candidate of [ record.media, record.mediaUrl, record.path, record.filePath, record.fileUrl ]) pushUniqueMessagingMediaUrl(urls, seen, candidate); if (Array.isArray(record.mediaUrls)) for (const mediaUrl of record.mediaUrls) pushUniqueMessagingMediaUrl(urls, seen, mediaUrl); if (Array.isArray(record.attachments)) for (const attachment of record.attachments) pushAttachment(attachment); return urls; } /** Collects messaging attachment references from a completed tool result. */ function collectMessagingMediaUrlsFromToolResult(result) { const urls = []; const seen = /* @__PURE__ */ new Set(); const appendFromRecord = (value) => { if (!value || typeof value !== "object") return; for (const url of collectMessagingMediaUrlsFromRecord(value)) if (!seen.has(url)) { seen.add(url); urls.push(url); } }; appendFromRecord(result); if (result && typeof result === "object") appendFromRecord(result.details); const outputText = extractToolResultText(result); if (outputText) try { appendFromRecord(JSON.parse(outputText)); } catch {} return urls; } /** Extract an internal source-reply payload from a completed message tool result. */ const TRUSTED_TOOL_RESULT_MEDIA = /* @__PURE__ */ new Set([ "agents_list", "apply_patch", "browser", "canvas", AUTOMATIONS_TOOL_NAME, "edit", "exec", "gateway", "view_image", "image_generate", "memory_get", "memory_search", "message", "music_generate", "nodes", "process", "read", "session_status", "sessions_history", "sessions_list", "sessions_search", "sessions_send", "sessions_spawn", "subagents", "tts", "video_generate", "web_fetch", "web_search", "x_search", "write" ]); const HTTP_URL_RE = /^https?:\/\//i; function isCoreToolResultMediaTrustedName(toolName) { if (!toolName) return false; return TRUSTED_TOOL_RESULT_MEDIA.has(normalizeToolPolicyName(toolName)); } function isExternalToolResult(result) { const details = readToolResultDetails(result); if (!details) return false; return typeof details.mcpServer === "string" || typeof details.mcpTool === "string"; } function isToolResultMediaTrusted(toolName, result, trustedLocalMediaToolNames) { if (!toolName || isExternalToolResult(result)) return false; const registeredName = toolName.trim(); if (registeredName && trustedLocalMediaToolNames?.has(registeredName) === true) return true; return isCoreToolResultMediaTrustedName(toolName); } if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.embeddedSubscribeToolsTestApi")] = { isToolResultMediaTrusted }; function isTrustedOwnedTtsLocalMedia(toolName, result, trustedLocalMediaToolNames) { if (!toolName || !isToolResultMediaTrusted(toolName, result, trustedLocalMediaToolNames) || normalizeToolPolicyName(toolName) !== "tts") return false; const media = readToolResultDetails(result)?.media; if (!media || typeof media !== "object" || Array.isArray(media)) return false; return media.trustedLocalMedia === true; } function filterToolResultMediaUrls(toolName, mediaUrls, result, trustedLocalMediaToolNames) { if (mediaUrls.length === 0) return mediaUrls; const trustedOwnedTtsLocalMedia = isTrustedOwnedTtsLocalMedia(toolName, result, trustedLocalMediaToolNames); if (isToolResultMediaTrusted(toolName, result, trustedLocalMediaToolNames)) { if (trustedLocalMediaToolNames !== void 0) { if (!trustedOwnedTtsLocalMedia) { const registeredName = toolName?.trim(); if (!registeredName || !trustedLocalMediaToolNames.has(registeredName)) return mediaUrls.filter((url) => HTTP_URL_RE.test(url.trim())); } } return mediaUrls; } return mediaUrls.filter((url) => HTTP_URL_RE.test(url.trim())); } function readToolResultDetailsMedia(result) { const details = readToolResultDetails(result); return details?.media && typeof details.media === "object" && !Array.isArray(details.media) ? details.media : void 0; } const REPLY_ATTACHMENT_METADATA_KEYS = /* @__PURE__ */ new Set([ "type", "path", "url", "mediaUrl", "filePath", "mimeType", "name", "sizeBytes", "durationMs", "width", "height" ]); function collectStructuredMedia(media) { const mediaUrls = []; const seen = /* @__PURE__ */ new Set(); const attachmentsByUrl = /* @__PURE__ */ new Map(); const pushString = (value, attachment) => { pushUniqueMessagingMediaUrl(mediaUrls, seen, value); const normalized = typeof value === "string" ? value.trim() : ""; if (normalized && attachment && !attachmentsByUrl.has(normalized)) attachmentsByUrl.set(normalized, attachment); }; const pushAttachment = (value) => { if (!value || typeof value !== "object" || Array.isArray(value)) return; const record = value; const attachment = Object.fromEntries(Object.entries(record).filter(([key, entry]) => { if (!REPLY_ATTACHMENT_METADATA_KEYS.has(key)) return false; if (key === "type") return entry === "image" || entry === "audio" || entry === "video" || entry === "file"; if (key === "width" || key === "height") return asPositiveFiniteNumber(entry) !== void 0; if (key === "sizeBytes" || key === "durationMs") return asNonNegativeFiniteNumber(entry) !== void 0; return typeof entry === "string"; })); for (const key of [ "media", "path", "url", "mediaUrl", "filePath", "fileUrl" ]) pushString(record[key], attachment); }; pushString(media.media); pushString(media.path); pushString(media.url); pushString(media.mediaUrl); pushString(media.filePath); pushString(media.fileUrl); if (Array.isArray(media.mediaUrls)) for (const value of media.mediaUrls) pushString(value); if (Array.isArray(media.attachments)) for (const attachment of media.attachments) pushAttachment(attachment); return { mediaUrls, ...attachmentsByUrl.size > 0 ? { attachments: mediaUrls.map((url) => attachmentsByUrl.get(url) ?? {}) } : {} }; } function isNonOutboundToolResultMedia(media) { return media.outbound === false; } function hasImageContentBlock(content) { for (const item of content) { if (!item || typeof item !== "object") continue; if (item.type === "image") return true; } return false; } function extractToolResultMediaArtifact(result) { if (!result || typeof result !== "object") return; const record = result; const detailsMedia = readToolResultDetailsMedia(record); if (detailsMedia) { if (isNonOutboundToolResultMedia(detailsMedia)) return; const structuredMedia = collectStructuredMedia(detailsMedia); if (structuredMedia.mediaUrls.length > 0) return { ...structuredMedia, ...detailsMedia.audioAsVoice === true ? { audioAsVoice: true } : {}, ...detailsMedia.trustedLocalMedia === true ? { trustedLocalMedia: true } : {} }; } const content = Array.isArray(record.content) ? record.content : null; if (!content) return; if (hasImageContentBlock(content)) { const details = record.details; const p = normalizeOptionalString(details?.path) ?? ""; if (p) return { mediaUrls: [p] }; } } //#endregion export { extractMessagingToolSend as a, isDeliveredMessagingToolSendToCurrentSource as c, filterToolResultMediaUrls as i, collectMessagingMediaUrlsFromToolResult as n, extractMessagingToolSendResult as o, extractToolResultMediaArtifact as r, extractMessagingToolSourceReplyPayload as s, collectMessagingMediaUrlsFromRecord as t };