UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

172 lines (171 loc) 7.64 kB
import { n as loadWebMedia } from "./web-media-D8G2d_q7.js"; import { t as buildOutboundMediaLoadOptions } from "./load-options-VzbF4ozo.js"; import "./web-media-Nj3kZBh5.js"; import { randomBytes } from "node:crypto"; //#region src/plugin-sdk/outbound-media.ts /** Load outbound media from a remote URL or approved local path using the shared web-media policy. */ async function loadOutboundMediaFromUrl(mediaUrl, options = {}) { return await loadWebMedia(mediaUrl, buildOutboundMediaLoadOptions({ maxBytes: options.maxBytes, mediaAccess: options.mediaAccess, mediaLocalRoots: options.mediaLocalRoots, mediaReadFile: options.mediaReadFile, workspaceDir: options.workspaceDir, proxyUrl: options.proxyUrl, fetchImpl: options.fetchImpl, requestInit: options.requestInit, trustExplicitProxyDns: options.trustExplicitProxyDns })); } const DEFAULT_HOSTED_OUTBOUND_MEDIA_RAW_CHUNK_BYTES = 36 * 1024; const DEFAULT_HOSTED_OUTBOUND_MEDIA_MAX_ENTRIES = 64; const DEFAULT_HOSTED_OUTBOUND_MEDIA_CHUNK_ROWS_PER_ENTRY_BUDGET = 512; const HOSTED_OUTBOUND_MEDIA_METADATA_TTL_GRACE_MS = 6e4; function createHostedOutboundMediaId() { return randomBytes(12).toString("hex"); } function createHostedOutboundMediaToken() { return randomBytes(24).toString("hex"); } function buildHostedOutboundMediaMetaKey(id) { return `media:${id}:meta`; } function buildHostedOutboundMediaChunkKey(id, index) { return `media:${id}:chunk:${String(index).padStart(4, "0")}`; } function resolveHostedOutboundMediaMetadataTtlMs(ttlMs) { return ttlMs + Math.min(ttlMs, HOSTED_OUTBOUND_MEDIA_METADATA_TTL_GRACE_MS); } function isFutureHostedOutboundMediaExpiry(expiresAt, nowMs) { return typeof expiresAt === "number" && Number.isSafeInteger(expiresAt) && expiresAt > nowMs; } function createHostedOutboundMediaMetaRecord(params) { return { id: params.id, routePath: params.routePath, token: params.token, ...params.contentType ? { contentType: params.contentType } : {}, expiresAt: params.expiresAt, chunkCount: params.chunkCount, byteLength: params.byteLength }; } function createHostedOutboundMediaMetadata(meta) { return { routePath: meta.routePath, token: meta.token, ...meta.contentType ? { contentType: meta.contentType } : {}, expiresAt: meta.expiresAt, byteLength: meta.byteLength }; } async function deleteHostedOutboundMediaRows(id, metadataStore, chunkStore, knownChunkCount) { const meta = await metadataStore.lookup(buildHostedOutboundMediaMetaKey(id)); await metadataStore.delete(buildHostedOutboundMediaMetaKey(id)); const chunkCount = meta?.chunkCount ?? knownChunkCount; if (chunkCount == null) return; for (let index = 0; index < chunkCount; index += 1) await chunkStore.delete(buildHostedOutboundMediaChunkKey(id, index)); } function createHostedOutboundMediaStore(options) { const rawChunkBytes = options.rawChunkBytes ?? DEFAULT_HOSTED_OUTBOUND_MEDIA_RAW_CHUNK_BYTES; const maxEntries = options.maxEntries ?? DEFAULT_HOSTED_OUTBOUND_MEDIA_MAX_ENTRIES; const chunkRowsPerEntryBudget = options.chunkRowsPerEntryBudget ?? DEFAULT_HOSTED_OUTBOUND_MEDIA_CHUNK_ROWS_PER_ENTRY_BUDGET; const maxChunkRows = options.maxChunkRows ?? maxEntries * chunkRowsPerEntryBudget; if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) throw new Error("hosted outbound media maxEntries must be a positive integer"); if (!Number.isSafeInteger(maxChunkRows) || maxChunkRows < 1) throw new Error("hosted outbound media maxChunkRows must be a positive integer"); const createId = options.createId ?? createHostedOutboundMediaId; const createToken = options.createToken ?? createHostedOutboundMediaToken; async function deleteEntry(id) { await deleteHostedOutboundMediaRows(id, options.metadataStore, options.chunkStore); } async function deleteEntryRows(id, chunkCount) { await deleteHostedOutboundMediaRows(id, options.metadataStore, options.chunkStore, chunkCount); } async function cleanupExpired(nowMs = Date.now()) { for (const row of await options.metadataStore.entries()) if (!isFutureHostedOutboundMediaExpiry(row.value.expiresAt, nowMs)) await deleteEntry(row.value.id); } async function pruneForCapacity(incomingChunkCount) { const rows = await options.metadataStore.entries(); const validRows = rows.filter((row) => Number.isSafeInteger(row.value.chunkCount) && row.value.chunkCount > 0); const validKeys = new Set(validRows.map((row) => row.key)); const orderedRows = validRows.toSorted((a, b) => a.createdAt - b.createdAt || a.key.localeCompare(b.key)); const invalidRows = rows.filter((row) => !validKeys.has(row.key)); for (const row of invalidRows) await deleteEntry(row.value.id); let entryCount = orderedRows.length; let chunkCount = orderedRows.reduce((total, row) => total + row.value.chunkCount, 0); for (const row of orderedRows) { if (entryCount < maxEntries && chunkCount + incomingChunkCount <= maxChunkRows) break; await deleteEntry(row.value.id); entryCount -= 1; chunkCount -= row.value.chunkCount; } } return { async prepareUrl(params) { await cleanupExpired(); const expiresAt = options.resolveExpiresAtMs(options.ttlMs); if (expiresAt === void 0) throw new Error("hosted outbound media expiry could not be resolved"); const media = await loadOutboundMediaFromUrl(params.mediaUrl, { maxBytes: params.maxBytes, ...params.proxyUrl ? { proxyUrl: params.proxyUrl } : {} }); const id = createId(); const token = createToken(); const metadataTtlMs = resolveHostedOutboundMediaMetadataTtlMs(options.ttlMs); const chunkCount = Math.max(1, Math.ceil(media.buffer.byteLength / rawChunkBytes)); if (chunkCount > maxChunkRows) throw new Error(`hosted outbound media exceeds SQLite chunk row limit (${chunkCount}/${maxChunkRows})`); await pruneForCapacity(chunkCount); try { for (let index = 0; index < chunkCount; index += 1) { const chunk = media.buffer.subarray(index * rawChunkBytes, (index + 1) * rawChunkBytes); await options.chunkStore.register(buildHostedOutboundMediaChunkKey(id, index), { id, index, dataBase64: chunk.toString("base64") }, { ttlMs: options.ttlMs }); } await options.metadataStore.register(buildHostedOutboundMediaMetaKey(id), createHostedOutboundMediaMetaRecord({ id, routePath: params.routePath, token, contentType: media.contentType, expiresAt, chunkCount, byteLength: media.buffer.byteLength }), { ttlMs: metadataTtlMs }); } catch (error) { await deleteEntryRows(id, chunkCount); throw error; } return `${params.publicBaseUrl}${params.routePath}${id}?token=${token}`; }, async read(id, nowMs = Date.now()) { const meta = await options.metadataStore.lookup(buildHostedOutboundMediaMetaKey(id)); if (!meta) return null; if (!isFutureHostedOutboundMediaExpiry(meta.expiresAt, nowMs)) { await deleteEntry(id); return null; } const chunks = []; for (let index = 0; index < meta.chunkCount; index += 1) { const chunk = await options.chunkStore.lookup(buildHostedOutboundMediaChunkKey(id, index)); if (!chunk || chunk.id !== id || chunk.index !== index) { await deleteEntry(id); return null; } chunks.push(Buffer.from(chunk.dataBase64, "base64")); } return { metadata: createHostedOutboundMediaMetadata(meta), buffer: Buffer.concat(chunks, meta.byteLength) }; }, delete: deleteEntry, cleanupExpired, async clear() { await Promise.all([options.metadataStore.clear(), options.chunkStore.clear()]); } }; } //#endregion export { loadOutboundMediaFromUrl as n, createHostedOutboundMediaStore as t };