openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
618 lines (617 loc) • 27 kB
JavaScript
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { S as safeFileURLToPath } from "./fs-safe-aqmM_n6V.js";
import { i as readLocalFileSafely } from "./secure-temp-dir-XAWcZnE2.js";
import { p as resolveUserPath } from "./utils-CCC-BEJH.js";
import { m as writeJson, o as tryReadJson } from "./json-files-2umMHm0W.js";
import { c as resolveDefaultAgentId } from "./agent-scope-config-CgCYpZfK.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import "./config-C9RxTsn1.js";
import { n as authorizeOperatorScopesForMethod } from "./method-scopes-BtdN3y7s.js";
import { t as isPassThroughRemoteMediaSource } from "./media-source-url-BL9SUd7E.js";
import "./local-file-access-CBe_wA_B.js";
import { d as saveMediaSource, t as MEDIA_MAX_BYTES, u as saveMediaBuffer } from "./store-C9M_0A1p.js";
import { o as createImageProcessor, p as readImageProbeFromHeader, s as getImageMetadata } from "./image-ops-PfTOpg74.js";
import { d as readSessionMessagesAsync } from "./session-utils.fs-D_8-X4jl.js";
import { u as loadSessionEntry } from "./session-utils-CHNCuY8a.js";
import "./media-services-DE3J1qM-.js";
import { n as assertLocalMediaAllowed } from "./local-media-access-Bw3v5URj.js";
import { a as sendJson, o as sendMethodNotAllowed, s as sendMissingScopeForbidden } from "./http-common-DunL6val.js";
import { l as resolveOpenAiCompatibleHttpOperatorScopes, t as authorizeGatewayHttpRequestOrReply, u as resolveOpenAiCompatibleHttpSenderIsOwner } from "./http-auth-utils-Du8mRZIw.js";
import "./http-utils-leZWdpma.js";
import path from "node:path";
import fs from "node:fs/promises";
import { randomUUID } from "node:crypto";
//#region src/gateway/managed-image-attachments.ts
const OUTGOING_IMAGE_ROUTE_PREFIX = "/api/chat/media/outgoing";
const DEFAULT_TRANSIENT_OUTGOING_IMAGE_TTL_MS = 900 * 1e3;
const MANAGED_OUTGOING_ATTACHMENT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DATA_URL_RE = /^data:/i;
const WINDOWS_DRIVE_RE = /^[A-Za-z]:[\\/]/;
const DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS = {
maxBytes: 12 * 1024 * 1024,
maxWidth: 4096,
maxHeight: 4096,
maxPixels: 2e7
};
const sessionManagedOutgoingAttachmentIndexCache = /* @__PURE__ */ new Map();
const MAX_SESSION_MANAGED_OUTGOING_ATTACHMENT_INDEX_CACHE_ENTRIES = 500;
function buildSessionManagedOutgoingAttachmentIndexCacheKey(sessionKey, agentId) {
return sessionKey === "global" && agentId ? `agent:${agentId}:global` : sessionKey;
}
function resolveManagedImageAttachmentLimits(config) {
return {
maxBytes: config?.maxBytes ?? DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS.maxBytes,
maxWidth: config?.maxWidth ?? DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS.maxWidth,
maxHeight: config?.maxHeight ?? DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS.maxHeight,
maxPixels: config?.maxPixels ?? DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS.maxPixels
};
}
function formatLimitMiB(bytes) {
if (bytes < 1024 * 1024) return `${bytes} bytes`;
return Number.isInteger(bytes / (1024 * 1024)) ? `${bytes / (1024 * 1024)} MiB` : `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
}
function createManagedImageAttachmentError(message) {
const error = new Error(message);
error.name = "ManagedImageAttachmentError";
return error;
}
function isManagedImageAttachmentSafeError(error) {
if (!(error instanceof Error)) return false;
if (error.name === "ManagedImageAttachmentError") return true;
return error.message.startsWith("Managed image attachment ") || error.message.startsWith("Invalid image data URL");
}
function getSanitizedManagedImageAttachmentError(error, alt) {
if (isManagedImageAttachmentSafeError(error)) return error;
return createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} could not be prepared`);
}
function validateManagedImageBuffer(buffer, alt, limits) {
if (buffer.byteLength > limits.maxBytes) throw createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`);
}
function estimateBase64DecodedByteLength(base64) {
const normalized = base64.replace(/\s+/g, "");
const paddingMatch = /=+$/u.exec(normalized);
const padding = Math.min(paddingMatch?.[0].length ?? 0, 2);
return Math.floor(normalized.length * 3 / 4) - padding;
}
function getManagedImageMetadataLimitError(metadata, alt, limits) {
if (!metadata) return `Managed image attachment ${JSON.stringify(alt)} is missing readable dimensions`;
if (metadata.width > limits.maxWidth) return `Managed image attachment ${JSON.stringify(alt)} exceeds the ${limits.maxWidth}px width limit`;
if (metadata.height > limits.maxHeight) return `Managed image attachment ${JSON.stringify(alt)} exceeds the ${limits.maxHeight}px height limit`;
if (metadata.width * metadata.height > limits.maxPixels) return `Managed image attachment ${JSON.stringify(alt)} exceeds the ${limits.maxPixels.toLocaleString("en-US")} pixel limit`;
return null;
}
function orientManagedImageMetadata(buffer, metadata) {
if (!metadata) return null;
const orientation = readImageProbeFromHeader(buffer)?.orientation;
return orientation === 5 || orientation === 6 || orientation === 7 || orientation === 8 ? {
width: metadata.height,
height: metadata.width
} : metadata;
}
async function resizeManagedImageBufferToLimits(params) {
const resized = await createImageProcessor().encode(params.buffer, {
format: "auto",
limits: {
maxWidth: params.limits.maxWidth,
maxHeight: params.limits.maxHeight,
maxPixels: params.limits.maxPixels
},
opaque: {
format: "jpeg",
quality: 92
},
transparent: {
format: "png",
compressionLevel: 9
},
transparency: "auto"
});
return {
buffer: resized.data,
contentType: resized.mimeType,
width: resized.width,
height: resized.height
};
}
function resolveOutgoingRecordsDir(stateDir = resolveStateDir()) {
return path.join(stateDir, "media", "outgoing", "records");
}
function resolveOutgoingOriginalsDir(stateDir = resolveStateDir()) {
return path.join(stateDir, "media", "outgoing", "originals");
}
function resolveOutgoingRecordPath(attachmentId, stateDir = resolveStateDir()) {
return path.join(resolveOutgoingRecordsDir(stateDir), `${attachmentId}.json`);
}
function buildOutgoingVariantUrl(sessionKey, attachmentId, variant) {
return `${OUTGOING_IMAGE_ROUTE_PREFIX}/${encodeURIComponent(sessionKey)}/${attachmentId}/${variant}`;
}
function deriveAltText(source, index) {
const fallback = `Generated image ${index + 1}`;
try {
if (/^https?:\/\//i.test(source)) {
const parsed = new URL(source);
return path.basename(parsed.pathname || "").trim() || fallback;
}
} catch {}
return path.basename(source).trim() || fallback;
}
function resolveLocalMediaPath(source) {
const trimmed = source.trim();
if (!trimmed || isPassThroughRemoteMediaSource(trimmed) || DATA_URL_RE.test(trimmed)) return;
if (trimmed.startsWith("file://")) try {
return safeFileURLToPath(trimmed);
} catch {
return;
}
if (trimmed.startsWith("~")) return resolveUserPath(trimmed);
if (path.isAbsolute(trimmed) || WINDOWS_DRIVE_RE.test(trimmed)) return path.resolve(trimmed);
}
function parseImageDataUrl(source, alt, limits) {
const trimmed = source.trim();
if (!trimmed.startsWith("data:")) return { kind: "not-data-url" };
const match = /^data:([^;,]+)(?:;[^,]*)*;base64,([A-Za-z0-9+/=\s]+)$/i.exec(trimmed);
if (!match) throw new Error("Invalid image data URL");
const contentType = match[1]?.trim().toLowerCase() ?? "";
if (!contentType.startsWith("image/")) return { kind: "non-image-data-url" };
if (estimateBase64DecodedByteLength(match[2]) > limits.maxBytes) throw createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`);
return {
kind: "image-data-url",
buffer: Buffer.from(match[2].replace(/\s+/g, ""), "base64"),
contentType
};
}
async function getVariantStats(filePath) {
const { buffer: metadataBuffer, stat } = await readLocalFileSafely({ filePath });
const metadata = await getImageMetadata(metadataBuffer).catch(() => null) ?? {
width: null,
height: null
};
return {
width: metadata.width ?? null,
height: metadata.height ?? null,
sizeBytes: Number.isFinite(stat.size) ? stat.size : null
};
}
async function writeManagedImageRecord(record, stateDir = resolveStateDir()) {
await writeJson(resolveOutgoingRecordPath(record.attachmentId, stateDir), record, { trailingNewline: true });
}
async function deleteManagedImageRecordArtifacts(record, stateDir = resolveStateDir()) {
const files = /* @__PURE__ */ new Set();
if (record.original?.path) files.add(record.original.path);
let deletedFileCount = 0;
for (const filePath of files) try {
await fs.rm(filePath, { force: true });
deletedFileCount += 1;
} catch {}
try {
await fs.rm(resolveOutgoingRecordPath(record.attachmentId, stateDir), { force: true });
} catch {}
return deletedFileCount;
}
async function deleteOrphanManagedImageFiles(params) {
let deletedFileCount = 0;
for (const dir of [resolveOutgoingOriginalsDir(params.stateDir)]) {
let names;
try {
names = await fs.readdir(dir);
} catch {
continue;
}
for (const name of names) {
const filePath = path.join(dir, name);
if (params.referencedPaths.has(filePath)) continue;
try {
if (!(await fs.stat(filePath)).isFile()) continue;
} catch {
continue;
}
try {
await fs.rm(filePath, { force: true });
deletedFileCount += 1;
} catch {}
}
}
return deletedFileCount;
}
async function cleanupManagedOutgoingImageRecords(params) {
const stateDir = params?.stateDir ?? resolveStateDir();
const nowMs = params?.nowMs ?? Date.now();
const transientMaxAgeMs = params?.transientMaxAgeMs ?? DEFAULT_TRANSIENT_OUTGOING_IMAGE_TTL_MS;
const sessionKeyFilter = params?.sessionKey ?? null;
const agentIdFilter = params?.agentId?.trim() || void 0;
const defaultAgentId = sessionKeyFilter === "global" ? resolveDefaultAgentId(getRuntimeConfig()) : void 0;
const forceDeleteSessionRecords = params?.forceDeleteSessionRecords === true;
const recordsDir = resolveOutgoingRecordsDir(stateDir);
let names;
try {
names = await fs.readdir(recordsDir);
} catch {
names = [];
}
let deletedRecordCount = 0;
let deletedFileCount = 0;
let retainedCount = 0;
const retainedReferencedPaths = /* @__PURE__ */ new Set();
const transcriptAttachmentIndexCache = /* @__PURE__ */ new Map();
for (const name of names) {
if (!name.endsWith(".json")) continue;
const recordPath = path.join(recordsDir, name);
const record = await tryReadJson(recordPath);
if (!record) {
try {
await fs.rm(recordPath, { force: true });
} catch {}
deletedRecordCount += 1;
continue;
}
if (sessionKeyFilter && record.sessionKey !== sessionKeyFilter) {
if (record.original?.path) retainedReferencedPaths.add(record.original.path);
retainedCount += 1;
continue;
}
if (sessionKeyFilter === "global" && record.sessionKey === "global" && (agentIdFilter && resolveManagedImageRecordAgentId(record, defaultAgentId) !== agentIdFilter || !agentIdFilter && typeof record.agentId === "string" && record.agentId.trim())) {
if (record.original?.path) retainedReferencedPaths.add(record.original.path);
retainedCount += 1;
continue;
}
let shouldDelete;
if (forceDeleteSessionRecords && (!sessionKeyFilter || record.sessionKey === sessionKeyFilter)) shouldDelete = true;
else if (record.messageId) shouldDelete = !await recordMatchesTranscriptMessage(record, transcriptAttachmentIndexCache);
else {
const createdAtMs = Date.parse(record.createdAt);
shouldDelete = Number.isFinite(createdAtMs) && nowMs - createdAtMs >= transientMaxAgeMs;
}
if (shouldDelete) {
deletedRecordCount += 1;
deletedFileCount += await deleteManagedImageRecordArtifacts(record, stateDir);
} else {
if (record.original?.path) retainedReferencedPaths.add(record.original.path);
retainedCount += 1;
}
}
deletedFileCount += await deleteOrphanManagedImageFiles({
stateDir,
referencedPaths: retainedReferencedPaths
});
return {
deletedRecordCount,
deletedFileCount,
retainedCount
};
}
function resolveManagedImageRecordAgentId(record, defaultAgentId) {
return record.agentId?.trim() || defaultAgentId;
}
async function readManagedImageRecord(attachmentId, stateDir = resolveStateDir()) {
try {
const raw = await fs.readFile(resolveOutgoingRecordPath(attachmentId, stateDir), "utf-8");
return JSON.parse(raw);
} catch {
return null;
}
}
function buildManagedImageBlock(record) {
const fullUrl = buildOutgoingVariantUrl(record.sessionKey, record.attachmentId, "full");
return {
type: "image",
url: fullUrl,
openUrl: fullUrl,
alt: record.alt,
mimeType: record.original.contentType,
width: record.original.width,
height: record.original.height
};
}
function buildManagedOutgoingAttachmentRefKey(messageId, attachmentId) {
return `${messageId}::${attachmentId}`;
}
function buildManagedImageResizeWarningBlock(params) {
return {
type: "text",
text: `[Image warning] ${params.alt} exceeded gateway dimension/pixel limits and was resized from ${params.originalWidth}×${params.originalHeight} to ${params.resizedWidth}×${params.resizedHeight}.`
};
}
function toRecordFilename(filePath) {
return path.basename(filePath).trim() || null;
}
function asArray(value) {
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim()) : [];
}
function parseManagedOutgoingRoute(value) {
try {
const match = new URL(value, "http://localhost").pathname.match(/^\/api\/chat\/media\/outgoing\/([^/]+)\/([^/]+)\/full$/);
if (!match) return null;
if (!MANAGED_OUTGOING_ATTACHMENT_ID_RE.test(match[2])) return null;
return {
sessionKey: decodeURIComponent(match[1]),
attachmentId: match[2]
};
} catch {
return null;
}
}
function collectManagedOutgoingAttachmentRefs(blocks, expectedSessionKey) {
const refs = /* @__PURE__ */ new Map();
for (const block of blocks ?? []) {
if (block?.type !== "image") continue;
for (const candidate of [block.url, block.openUrl]) {
if (typeof candidate !== "string") continue;
const parsed = parseManagedOutgoingRoute(candidate);
if (!parsed) continue;
if (expectedSessionKey && parsed.sessionKey !== expectedSessionKey) continue;
refs.set(parsed.attachmentId, {
attachmentId: parsed.attachmentId,
sessionKey: parsed.sessionKey
});
}
}
return [...refs.values()];
}
function getCachedSessionManagedOutgoingAttachmentIndex(sessionKey, agentId, stat) {
const cacheKey = buildSessionManagedOutgoingAttachmentIndexCacheKey(sessionKey, agentId);
const cached = sessionManagedOutgoingAttachmentIndexCache.get(cacheKey);
if (!cached) return null;
if (cached.transcriptPath !== stat.transcriptPath || cached.mtimeMs !== stat.mtimeMs || cached.size !== stat.size) {
sessionManagedOutgoingAttachmentIndexCache.delete(cacheKey);
return null;
}
sessionManagedOutgoingAttachmentIndexCache.delete(cacheKey);
sessionManagedOutgoingAttachmentIndexCache.set(cacheKey, cached);
return cached.index;
}
function setCachedSessionManagedOutgoingAttachmentIndex(sessionKey, agentId, stat, index) {
sessionManagedOutgoingAttachmentIndexCache.set(buildSessionManagedOutgoingAttachmentIndexCacheKey(sessionKey, agentId), {
transcriptPath: stat.transcriptPath,
mtimeMs: stat.mtimeMs,
size: stat.size,
index
});
while (sessionManagedOutgoingAttachmentIndexCache.size > MAX_SESSION_MANAGED_OUTGOING_ATTACHMENT_INDEX_CACHE_ENTRIES) {
const oldestKey = sessionManagedOutgoingAttachmentIndexCache.keys().next().value;
if (!oldestKey) break;
sessionManagedOutgoingAttachmentIndexCache.delete(oldestKey);
}
}
async function getSessionManagedOutgoingAttachmentIndex(sessionKey, cache, agentId) {
const cacheKey = buildSessionManagedOutgoingAttachmentIndexCacheKey(sessionKey, agentId);
if (cache?.has(cacheKey)) return cache.get(cacheKey) ?? null;
const { storePath, entry } = loadSessionEntry(sessionKey, sessionKey === "global" && agentId ? { agentId } : void 0);
const sessionId = entry?.sessionId;
if (!sessionId) {
cache?.set(cacheKey, null);
return null;
}
let transcriptStat = null;
const transcriptPath = typeof entry?.sessionFile === "string" ? entry.sessionFile.trim() : "";
if (transcriptPath) try {
const stat = await fs.stat(transcriptPath);
transcriptStat = {
transcriptPath,
mtimeMs: stat.mtimeMs,
size: stat.size
};
const cachedIndex = getCachedSessionManagedOutgoingAttachmentIndex(sessionKey, agentId, transcriptStat);
if (cachedIndex) {
cache?.set(cacheKey, cachedIndex);
return cachedIndex;
}
} catch {
sessionManagedOutgoingAttachmentIndexCache.delete(cacheKey);
}
const messages = await readSessionMessagesAsync(sessionId, storePath, entry.sessionFile, {
mode: "full",
reason: "managed outgoing attachment index"
});
const index = /* @__PURE__ */ new Set();
for (const message of messages) {
const messageId = (message?.["__openclaw"])?.id;
if (typeof messageId !== "string" || !messageId) continue;
for (const ref of collectManagedOutgoingAttachmentRefs(Array.isArray(message?.content) ? message.content : [], sessionKey)) index.add(buildManagedOutgoingAttachmentRefKey(messageId, ref.attachmentId));
}
if (transcriptStat) setCachedSessionManagedOutgoingAttachmentIndex(sessionKey, agentId, transcriptStat, index);
cache?.set(cacheKey, index);
return index;
}
async function recordMatchesTranscriptMessage(record, cache) {
if (!record.messageId) return false;
return (await getSessionManagedOutgoingAttachmentIndex(record.sessionKey, cache, record.agentId))?.has(buildManagedOutgoingAttachmentRefKey(record.messageId, record.attachmentId)) ?? false;
}
async function attachManagedOutgoingImagesToMessage(params) {
const messageId = params.messageId.trim();
if (!messageId) return;
const refs = collectManagedOutgoingAttachmentRefs(params.blocks);
if (refs.length === 0) return;
await Promise.all(refs.map(async ({ attachmentId, sessionKey }) => {
const record = await readManagedImageRecord(attachmentId, params.stateDir);
if (!record || record.sessionKey !== sessionKey) return;
if (record.messageId === messageId && record.retentionClass === "history") return;
await writeManagedImageRecord({
...record,
messageId,
retentionClass: "history",
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
}, params.stateDir);
}));
}
async function createManagedOutgoingImageBlocks(params) {
const sessionKey = params.sessionKey.trim();
if (!sessionKey) return [];
const mediaUrls = asArray(params.mediaUrls);
if (mediaUrls.length === 0) return [];
const stateDir = params.stateDir ?? resolveStateDir();
const limits = resolveManagedImageAttachmentLimits(params.limits);
const blocks = [];
for (const [index, mediaUrl] of mediaUrls.entries()) {
const fallbackAlt = `Generated image ${index + 1}`;
const parsedDataUrl = parseImageDataUrl(mediaUrl, fallbackAlt, limits);
const alt = parsedDataUrl.kind === "image-data-url" ? fallbackAlt : deriveAltText(mediaUrl, index);
if (parsedDataUrl.kind === "non-image-data-url") continue;
let savedOriginalPath = null;
try {
let resizeWarning = null;
if (parsedDataUrl.kind === "image-data-url") validateManagedImageBuffer(parsedDataUrl.buffer, alt, limits);
let savedOriginal = parsedDataUrl.kind === "image-data-url" ? await saveMediaBuffer(parsedDataUrl.buffer, parsedDataUrl.contentType, "outgoing/originals", limits.maxBytes, `generated-image-${index + 1}`) : await (async () => {
const localMediaPath = resolveLocalMediaPath(mediaUrl);
if (localMediaPath) await assertLocalMediaAllowed(localMediaPath, params.localRoots);
return await saveMediaSource(mediaUrl, void 0, "outgoing/originals", Math.max(limits.maxBytes, MEDIA_MAX_BYTES));
})();
savedOriginalPath = savedOriginal.path;
let savedOriginalContentType = savedOriginal.contentType;
if (!savedOriginalContentType?.startsWith("image/")) {
await fs.rm(savedOriginal.path, { force: true }).catch(() => {});
savedOriginalPath = null;
continue;
}
if (savedOriginal.size > limits.maxBytes) throw createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`);
let originalBuffer = parsedDataUrl.kind === "image-data-url" ? parsedDataUrl.buffer : (await readLocalFileSafely({ filePath: savedOriginal.path })).buffer;
validateManagedImageBuffer(originalBuffer, alt, limits);
let originalStats = await getVariantStats(savedOriginal.path);
if (originalStats.sizeBytes != null && originalStats.sizeBytes > limits.maxBytes) throw createManagedImageAttachmentError(`Managed image attachment ${JSON.stringify(alt)} exceeds the ${formatLimitMiB(limits.maxBytes)} byte limit`);
const originalMetadata = originalStats.width != null && originalStats.height != null ? {
width: originalStats.width,
height: originalStats.height
} : await getImageMetadata(originalBuffer);
const originalDisplayMetadata = orientManagedImageMetadata(originalBuffer, originalMetadata);
let effectiveMetadata = originalDisplayMetadata;
let metadataLimitError = getManagedImageMetadataLimitError(effectiveMetadata, alt, limits);
for (let resizeAttempt = 0; metadataLimitError; resizeAttempt += 1) {
if (!effectiveMetadata) throw createManagedImageAttachmentError(metadataLimitError);
if (resizeAttempt >= 3) throw createManagedImageAttachmentError(metadataLimitError);
const resized = await resizeManagedImageBufferToLimits({
buffer: originalBuffer,
limits
});
validateManagedImageBuffer(resized.buffer, alt, limits);
const replacement = await saveMediaBuffer(resized.buffer, resized.contentType, "outgoing/originals", limits.maxBytes, toRecordFilename(savedOriginal.path) ?? `generated-image-${index + 1}`);
await fs.rm(savedOriginal.path, { force: true }).catch(() => {});
savedOriginal = replacement;
savedOriginalContentType = replacement.contentType ?? resized.contentType;
savedOriginalPath = savedOriginal.path;
originalBuffer = resized.buffer;
originalStats = await getVariantStats(savedOriginal.path);
effectiveMetadata = orientManagedImageMetadata(originalBuffer, originalStats.width != null && originalStats.height != null ? {
width: originalStats.width,
height: originalStats.height
} : await getImageMetadata(originalBuffer));
metadataLimitError = getManagedImageMetadataLimitError(effectiveMetadata, alt, limits);
if (!metadataLimitError) resizeWarning = buildManagedImageResizeWarningBlock({
alt,
originalWidth: originalDisplayMetadata?.width ?? effectiveMetadata?.width ?? resized.width,
originalHeight: originalDisplayMetadata?.height ?? effectiveMetadata?.height ?? resized.height,
resizedWidth: effectiveMetadata?.width ?? resized.width,
resizedHeight: effectiveMetadata?.height ?? resized.height
});
}
const record = {
attachmentId: randomUUID(),
sessionKey,
...sessionKey === "global" && params.agentId?.trim() ? { agentId: params.agentId.trim() } : {},
messageId: params.messageId ?? null,
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
retentionClass: params.messageId ? "history" : "transient",
alt,
original: {
path: savedOriginal.path,
contentType: savedOriginalContentType,
width: originalStats.width,
height: originalStats.height,
sizeBytes: originalStats.sizeBytes,
filename: toRecordFilename(savedOriginal.path)
}
};
await writeManagedImageRecord(record, stateDir);
blocks.push(buildManagedImageBlock(record));
if (resizeWarning) blocks.push(resizeWarning);
} catch (error) {
if (savedOriginalPath) await fs.rm(savedOriginalPath, { force: true }).catch(() => {});
const sanitizedError = getSanitizedManagedImageAttachmentError(error, alt);
if (params.continueOnPrepareError) {
params.onPrepareError?.(sanitizedError);
continue;
}
throw sanitizedError;
}
}
return blocks;
}
function sendStatus(res, statusCode, body) {
if (res.writableEnded) return;
res.statusCode = statusCode;
res.setHeader("content-type", "text/plain; charset=utf-8");
res.end(body);
}
function safeAttachmentFilename(value) {
const fallback = "generated-image";
return (value ?? fallback).replace(/[\r\n"\\]/g, "_").trim() || fallback;
}
async function handleManagedOutgoingImageHttpRequest(req, res, opts) {
const match = new URL(req.url ?? "/", "http://localhost").pathname.match(/^\/api\/chat\/media\/outgoing\/([^/]+)\/([^/]+)\/full$/);
if (!match) return false;
if (req.method !== "GET") {
sendMethodNotAllowed(res, "GET");
return true;
}
const requestAuth = await authorizeGatewayHttpRequestOrReply({
req,
res,
auth: opts.auth,
trustedProxies: opts.trustedProxies,
allowRealIpFallback: opts.allowRealIpFallback,
rateLimiter: opts.rateLimiter
});
if (!requestAuth) return true;
const scopeAuth = authorizeOperatorScopesForMethod("chat.history", resolveOpenAiCompatibleHttpOperatorScopes(req, requestAuth));
if (!scopeAuth.allowed) {
sendMissingScopeForbidden(res, scopeAuth.missingScope);
return true;
}
const encodedSessionKey = match[1];
const attachmentId = match[2];
if (!encodedSessionKey || !attachmentId) return false;
if (!MANAGED_OUTGOING_ATTACHMENT_ID_RE.test(attachmentId)) {
sendStatus(res, 404, "not found");
return true;
}
let sessionKey;
try {
sessionKey = decodeURIComponent(encodedSessionKey);
} catch {
sendStatus(res, 404, "not found");
return true;
}
const record = await readManagedImageRecord(attachmentId, opts.stateDir);
if (!record || record.sessionKey !== sessionKey) {
sendStatus(res, 404, "not found");
return true;
}
if (!resolveOpenAiCompatibleHttpSenderIsOwner(req, requestAuth)) {
sendJson(res, 403, {
ok: false,
error: {
type: "forbidden",
message: "owner access required"
}
});
return true;
}
if (!await recordMatchesTranscriptMessage(record)) {
sendStatus(res, 404, "not found");
return true;
}
let body;
try {
body = (await readLocalFileSafely({ filePath: record.original.path })).buffer;
} catch {
sendStatus(res, 404, "not found");
return true;
}
res.statusCode = 200;
res.setHeader("content-type", record.original.contentType || "application/octet-stream");
res.setHeader("content-length", String(body.byteLength));
res.setHeader("cache-control", "private, max-age=31536000, immutable");
res.setHeader("content-disposition", `inline; filename="${safeAttachmentFilename(record.original.filename)}"`);
res.end(body);
return true;
}
//#endregion
export { handleManagedOutgoingImageHttpRequest as a, createManagedOutgoingImageBlocks as i, attachManagedOutgoingImagesToMessage as n, resolveManagedImageAttachmentLimits as o, cleanupManagedOutgoingImageRecords as r, DEFAULT_MANAGED_IMAGE_ATTACHMENT_LIMITS as t };