UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

198 lines (197 loc) 9.48 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js"; import { m as resolveAgentWorkspaceDir } from "./agent-scope-config-DcbEhP0R.js"; import { f as resolveAgentIdFromSessionKey } from "./session-key-BnWWjqNc.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import "./agent-scope-DbtJyKUL.js"; import { n as normalizeMessageChannel } from "./message-channel-core-AaEWic-A.js"; import { t as isDeliverableMessageChannel } from "./message-channel-normalize-B81s0XMO.js"; import "./message-channel-BQrhwUEA.js"; import { d as normalizeMimeType, n as detectMime, t as FILE_TYPE_SNIFF_MAX_BYTES } from "./mime-CVpcq9ju.js"; import { n as resolvePathFromInput } from "./path-policy-s_BvJZSk.js"; import { t as extractDeliveryInfo } from "./delivery-info-CfG8kavj.js"; import { n as resolveWorkspaceRoot } from "./workspace-dir-CA-p1R3_.js"; import * as fsPromises from "node:fs/promises"; import { lstat } from "node:fs/promises"; //#region src/plugins/host-hook-attachments.ts const DEFAULT_ATTACHMENT_MAX_BYTES = 26214400; /** Filesystem adapter used by attachment MIME probes and tests. */ const attachmentProbeFs = { open: (...args) => fsPromises.open(...args) }; const MAX_ATTACHMENT_FILES = 10; const loadSendMessage = createLazyRuntimeModule(() => import("./message-BJzxaHQV.js").then((module) => module.sendMessage)); const loadGetChannelPlugin = createLazyRuntimeModule(() => import("./plugins-D2edCor-.js").then((module) => module.getChannelPlugin)); function captionFormatToParseMode(captionFormat) { if (captionFormat === "html") return "HTML"; } function escapeHtmlText(text) { return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); } function normalizeOptionalThreadId(value) { if (typeof value === "number" && Number.isFinite(value)) return value; return normalizeOptionalString(value); } async function readMimeSniffBuffer(filePath, size) { let handle; try { handle = await attachmentProbeFs.open(filePath, "r"); const length = Math.min(Math.max(0, size), FILE_TYPE_SNIFF_MAX_BYTES); const buffer = Buffer.alloc(length); const { bytesRead } = await handle.read(buffer, 0, length, 0); return buffer.subarray(0, bytesRead); } catch (error) { return { error: `attachment file MIME read failed for ${filePath}: ${formatErrorMessage(error)}` }; } finally { await handle?.close().catch(() => void 0); } } /** Resolves portable attachment delivery options while honoring shipped channel-specific hints. */ function resolveAttachmentDelivery(params) { const fallbackParseMode = captionFormatToParseMode(params.captionFormat); const channel = params.channel.trim().toLowerCase(); const hints = params.channelHints; const legacyTelegram = channel === "telegram" ? hints?.telegram : void 0; const legacySlack = channel === "slack" ? hints?.slack : void 0; const parseMode = hints?.parseMode ?? legacyTelegram?.parseMode ?? (channel === "telegram" && params.captionFormat === "plain" ? "HTML" : fallbackParseMode); const escapePlainHtmlCaption = params.captionFormat === "plain" && parseMode === "HTML"; const silent = hints?.silent ?? legacyTelegram?.disableNotification; const forceDocumentMime = normalizeMimeType(hints?.forceDocumentMime ?? legacyTelegram?.forceDocumentMime); const threadId = normalizeOptionalThreadId(hints?.threadId) ?? normalizeOptionalString(legacySlack?.threadTs); return { ...parseMode ? { parseMode } : {}, ...escapePlainHtmlCaption ? { escapePlainHtmlCaption: true } : {}, ...silent !== void 0 ? { silent } : {}, ...forceDocumentMime ? { forceDocumentMime } : {}, ...threadId !== void 0 ? { threadId } : {} }; } async function validateAttachmentFiles(files, maxBytes, options) { if (files.length > MAX_ATTACHMENT_FILES) return { error: `at most ${MAX_ATTACHMENT_FILES} attachment files are allowed` }; const paths = []; let totalBytes = 0; for (const file of files) { if (!file || typeof file !== "object" || Array.isArray(file)) return { error: "attachment file entry must be an object" }; const filePath = normalizeOptionalString(file.path); if (!filePath) return { error: "attachment file path is required" }; const resolvedPath = resolveAttachmentFilePath({ filePath, config: options?.config, sessionKey: options?.sessionKey }); const info = await lstat(resolvedPath).catch(() => void 0); if (info?.isSymbolicLink()) return { error: `attachment file symlinks are not allowed: ${resolvedPath}` }; if (!info?.isFile()) return { error: `attachment file not found: ${resolvedPath}` }; if (info.size > maxBytes) return { error: `attachment file exceeds ${maxBytes} bytes: ${resolvedPath}` }; if (options?.forceDocumentMime) { const fileBuffer = await readMimeSniffBuffer(resolvedPath, info.size); if (!Buffer.isBuffer(fileBuffer)) return fileBuffer; let detectedMime; try { detectedMime = normalizeMimeType(await detectMime({ buffer: fileBuffer })); } catch (error) { return { error: `attachment file MIME detection failed for ${filePath}: ` + formatErrorMessage(error) }; } if (detectedMime !== options.forceDocumentMime) return { error: `attachment file MIME mismatch for ${resolvedPath}: expected ${options.forceDocumentMime}, got ${detectedMime ?? "unknown"}` }; } totalBytes += info.size; if (totalBytes > maxBytes) return { error: `attachment files exceed ${maxBytes} bytes total` }; paths.push(resolvedPath); } return paths; } function resolveAttachmentFilePath(params) { const workspaceDir = params.sessionKey && params.config ? resolveAgentWorkspaceDir(params.config, resolveAgentIdFromSessionKey(params.sessionKey)) : void 0; return resolvePathFromInput(params.filePath, resolveWorkspaceRoot(workspaceDir)); } /** Resolves the thread id used when delivering a plugin session attachment. */ function resolveSessionAttachmentThreadId(params) { return normalizeOptionalThreadId(params.hintThreadId) ?? normalizeOptionalThreadId(params.explicitThreadId) ?? normalizeOptionalThreadId(params.fallbackThreadId) ?? normalizeOptionalThreadId(params.deliveryThreadId); } /** Sends a bundled-plugin session attachment through the session's active delivery route. */ async function sendPluginSessionAttachment(params) { if (params.origin !== "bundled") return { ok: false, error: "session attachments are restricted to bundled plugins" }; const sessionKey = normalizeOptionalString(params.sessionKey); if (!sessionKey) return { ok: false, error: "sessionKey is required" }; if (!Array.isArray(params.files) || params.files.length === 0) return { ok: false, error: "at least one attachment file is required" }; const maxBytes = typeof params.maxBytes === "number" && Number.isFinite(params.maxBytes) ? Math.min(DEFAULT_ATTACHMENT_MAX_BYTES, Math.max(1, Math.floor(params.maxBytes))) : DEFAULT_ATTACHMENT_MAX_BYTES; const { deliveryContext, threadId } = extractDeliveryInfo(sessionKey, { cfg: params.config }); if (!deliveryContext?.channel || !deliveryContext.to) return { ok: false, error: `session has no active delivery route: ${sessionKey}` }; const normalizedChannel = normalizeMessageChannel(deliveryContext.channel); try { if ((normalizedChannel && isDeliverableMessageChannel(normalizedChannel) ? (await loadGetChannelPlugin())(normalizedChannel) : void 0)?.outbound?.deliveryMode === "gateway") return { ok: false, error: `session attachments require direct outbound delivery for channel ${deliveryContext.channel}; channel uses gateway delivery` }; } catch (error) { return { ok: false, error: `attachment delivery setup failed: ${formatErrorMessage(error)}` }; } const rawText = normalizeOptionalString(params.text) ?? ""; const resolvedDelivery = resolveAttachmentDelivery({ channel: deliveryContext.channel, captionFormat: params.captionFormat, channelHints: params.channelHints }); const validated = await validateAttachmentFiles(params.files, maxBytes, { forceDocumentMime: resolvedDelivery.forceDocumentMime, config: params.config, sessionKey }); if (!Array.isArray(validated)) return { ok: false, error: validated.error }; const resolvedThreadId = resolveSessionAttachmentThreadId({ deliveryThreadId: deliveryContext.threadId, explicitThreadId: params.threadId, fallbackThreadId: threadId, hintThreadId: resolvedDelivery.threadId }); let result; try { result = await (await loadSendMessage())({ to: deliveryContext.to, content: resolvedDelivery.escapePlainHtmlCaption ? escapeHtmlText(rawText) : rawText, channel: deliveryContext.channel, accountId: deliveryContext.accountId, threadId: resolvedThreadId, requesterSessionKey: sessionKey, mediaUrls: validated, forceDocument: resolvedDelivery.forceDocumentMime ? true : params.forceDocument, bestEffort: false, cfg: params.config, ...resolvedDelivery.parseMode ? { parseMode: resolvedDelivery.parseMode } : {}, ...resolvedDelivery.silent !== void 0 ? { silent: resolvedDelivery.silent } : {} }); } catch (error) { return { ok: false, error: `attachment delivery failed: ${formatErrorMessage(error)}` }; } if (!result.result) return { ok: false, error: "attachment delivery failed: no delivery result returned" }; return { ok: true, channel: result.channel, deliveredTo: deliveryContext.to, count: validated.length }; } //#endregion export { sendPluginSessionAttachment };