UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

504 lines (503 loc) 22.5 kB
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { g as readStringValue, l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { t as AgentSelectionRequiredError } from "./agent-scope-config-DcbEhP0R.js"; import { O as parseAgentSessionKey, b as toAgentStoreSessionKey, f as resolveAgentIdFromSessionKey } from "./session-key-BnWWjqNc.js"; import { _ as resolveSessionAgentId } from "./agent-scope-DbtJyKUL.js"; import { n as resolvePersistedSessionStoreOwnerForKey } from "./session-store-owner-CR2Ag3xK.js"; import { n as isHttpUrl } from "./url-protocol-OU3K-ySz.js"; import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { _ as validateArtifactsDownloadParams, v as validateArtifactsGetParams, y as validateArtifactsListParams } from "./src-BiL5aQto.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import { a as resolveStoredSessionKeyForAgentStore, n as resolveSessionStoreAgentId } from "./session-store-key-8xEjWSNi.js"; import { i as getTaskSessionLookupByIdForStatus } from "./task-status-access-C6caarxH.js"; import { r as readAssistantDisplayContent } from "./assistant-display-content-BZljc_tT.js"; import { n as hasOperatorBoundary } from "./operator-role-policy-wsr1DeJv.js"; import { m as visitSessionMessagesAsync } from "./session-transcript-readers-CYDRQsH5.js"; import { i as tryResolveSessionCompatibilityOwnerAgentId, n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js"; import { i as loadGatewaySessionEntryReadOnly } from "./session-utils-store-CInT2loy.js"; import "./session-utils-Cai0_C6U.js"; import { k as authorizeIncognitoSessionTarget, r as createSessionListEntryFilter, z as resolveSessionSharingTarget } from "./session-sharing-B7MI8hNo.js"; import { t as assertValidParams } from "./validation-pzrlzFvo.js"; import { l as parseManagedOutgoingArtifactId, m as resolveManagedOutgoingMediaUrlDownload, p as resolveManagedOutgoingMediaArtifactDownload } from "./managed-image-attachments-CwIi4Xb-.js"; import { n as resolveSessionKeyForRun } from "./server-session-key-CesREwK4.js"; import { createHash } from "node:crypto"; //#region src/gateway/server-methods/artifacts-base64.ts function mimeFromDataUrl(value) { return /^data:([^;,]+)(?:;[^,]*)?,/i.exec(value.trim())?.[1]?.toLowerCase(); } function base64FromDataUrl(value) { const trimmed = value.trim(); const commaIndex = trimmed.indexOf(","); if (commaIndex < 0 || trimmed.slice(0, 5).toLowerCase() !== "data:") return; if (!trimmed.slice(0, commaIndex).toLowerCase().includes(";base64")) return; return trimmed.slice(commaIndex + 1); } function isBase64Whitespace(value) { return value === " " || value === "\n" || value === "\r" || value === " "; } function isArtifactBase64DataChar(value) { const code = value.charCodeAt(0); return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || value === "+" || value === "/" || value === "-" || value === "_"; } function normalizeArtifactBase64Char(value) { if (value === "-") return "+"; if (value === "_") return "/"; return value; } function readArtifactBase64Payload(value, opts) { if (value === void 0) return; let encodedLength = 0; let padding = 0; let sawPadding = false; let data = opts.includeData ? "" : void 0; for (const char of value) { if (isBase64Whitespace(char)) continue; if (char === "=") { padding += 1; if (padding > 2) return; sawPadding = true; encodedLength += 1; if (data !== void 0) data += char; continue; } if (sawPadding || !isArtifactBase64DataChar(char)) return; encodedLength += 1; if (data !== void 0) data += normalizeArtifactBase64Char(char); } const remainder = encodedLength % 4; if (padding > 0 && remainder !== 0 || remainder === 1) return; if (data !== void 0 && padding === 0 && remainder > 0) data += "=".repeat(4 - remainder); return { ...data !== void 0 ? { data } : {}, sizeBytes: Math.max(0, Math.floor(encodedLength * 3 / 4) - padding) }; } //#endregion //#region src/gateway/server-methods/artifacts-session-resolution.ts function resolveArtifactSessionAgentId(sessionKey, cfg) { const key = normalizeOptionalString(sessionKey); if (!key) return; const parsed = parseAgentSessionKey(key); if (!parsed && key.toLowerCase().startsWith("agent:")) return; if (cfg) { const owner = resolveRequestedSessionAgentId(cfg, key); if (!owner.ok) throw new ArtifactSessionResolutionError(owner.error); return owner.agentId; } return parsed?.agentId ?? resolveAgentIdFromSessionKey(key); } function resolveScopedArtifactSessionKey(sessionKey, agentId, cfg) { const key = normalizeOptionalString(sessionKey); if (!key) return; const scopedAgentId = normalizeOptionalString(agentId); if (!scopedAgentId) return key; const parsed = parseAgentSessionKey(key); if (!parsed && key.toLowerCase().startsWith("agent:")) return; if (!cfg) return parsed && parsed.agentId !== normalizeAgentId(scopedAgentId) ? void 0 : toAgentStoreSessionKey({ agentId: scopedAgentId, requestKey: key }); const scopedKey = resolveStoredSessionKeyForAgentStore({ cfg, agentId: scopedAgentId, sessionKey: key }); return scopedKey !== "global" && scopedKey !== "unknown" && resolveSessionStoreAgentId(cfg, scopedKey) !== normalizeAgentId(scopedAgentId) ? void 0 : scopedKey; } function resolveQuerySession(query, cfg) { if (query.sessionKey) { const sessionKey = resolveScopedArtifactSessionKey(query.sessionKey, query.agentId, cfg); return sessionKey ? { sessionKey, ...query.agentId ? { agentId: query.agentId } : {} } : void 0; } if (query.runId) { const sessionKey = resolveSessionKeyForRun(query.runId, query.agentId ? { agentId: query.agentId } : {}); const agentId = query.agentId ?? resolveArtifactSessionAgentId(sessionKey, cfg) ?? resolveSessionAgentId({ config: cfg }); const scopedSessionKey = resolveScopedArtifactSessionKey(sessionKey, agentId, cfg); return scopedSessionKey ? { sessionKey: scopedSessionKey, agentId } : void 0; } if (!query.taskId) return; const task = getTaskSessionLookupByIdForStatus(query.taskId); const requesterSessionKey = normalizeOptionalString(task?.requesterSessionKey); const ownerAgentId = parseAgentSessionKey(task?.ownerKey)?.agentId; const persistedRequesterOwner = requesterSessionKey ? resolvePersistedSessionStoreOwnerForKey(cfg ?? {}, requesterSessionKey) : { kind: "none" }; const requesterAgentId = normalizeOptionalString(task?.requesterAgentId) ?? ownerAgentId ?? (persistedRequesterOwner.kind === "configured" ? persistedRequesterOwner.agentId : resolveArtifactSessionAgentId(requesterSessionKey, cfg)); const taskAgentId = normalizeOptionalString(task?.agentId) ?? requesterAgentId; if (query.agentId && taskAgentId && normalizeAgentId(query.agentId) !== normalizeAgentId(taskAgentId)) return; if (requesterSessionKey) { const sessionAgentId = requesterAgentId ?? resolveArtifactSessionAgentId(requesterSessionKey, cfg); const scopedSessionKey = sessionAgentId ? resolveScopedArtifactSessionKey(requesterSessionKey, sessionAgentId, cfg) : void 0; return scopedSessionKey ? { sessionKey: scopedSessionKey, agentId: sessionAgentId } : void 0; } const agentId = query.agentId ?? taskAgentId ?? resolveSessionAgentId({ config: cfg }); const runId = normalizeOptionalString(task?.runId); const scopedSessionKey = resolveScopedArtifactSessionKey(runId ? resolveSessionKeyForRun(runId, { agentId }) : void 0, agentId, cfg); return scopedSessionKey ? { sessionKey: scopedSessionKey, agentId } : void 0; } var ArtifactSessionResolutionError = class extends Error { constructor(shape) { super(shape.message); this.shape = shape; } }; function resolveAuthorizedArtifactSession(query, cfg, client) { const resolved = resolveQuerySession(query, cfg); if (!resolved) return; const target = resolveSessionSharingTarget({ cfg: cfg ?? {}, sessionKey: resolved.sessionKey, agentId: resolved.agentId }); const error = authorizeIncognitoSessionTarget({ client, sessionKey: query.sessionKey ?? resolved.sessionKey, target }); const roleVisibilityDenied = Boolean(cfg && hasOperatorBoundary(client, cfg) && target && createSessionListEntryFilter({ client, cfg })?.(target.storeKey, target.entry) === false); if (!error && !roleVisibilityDenied) return resolved; throw new ArtifactSessionResolutionError(query.sessionKey && error ? error : errorShape(ErrorCodes.INVALID_REQUEST, "no session found for artifact query", { details: { type: "artifact_scope_not_found" } })); } //#endregion //#region src/gateway/server-methods/artifacts.ts function admitArtifactQuery(query, cfg, respond) { const sessionKey = normalizeOptionalString(query.sessionKey); if (!sessionKey || !cfg) return query; const owner = resolveRequestedSessionAgentId(cfg, sessionKey, query.agentId); if (!owner.ok) { respond(false, void 0, owner.error); return; } return { ...query, agentId: owner.agentId }; } function artifactError(type, message, details) { return errorShape(ErrorCodes.INVALID_REQUEST, message, { details: { type, ...details } }); } function normalizeArtifactType(value) { const normalized = value.trim().toLowerCase(); if (normalized === "image" || normalized === "input_image" || normalized === "image_url") return "image"; if (normalized === "audio" || normalized === "input_audio") return "audio"; if (normalized === "video" || normalized === "input_video") return "video"; if (normalized === "file" || normalized === "input_file") return "file"; if (normalized === "attachment") return "file"; return "file"; } function mediaUrlValue(value) { if (typeof value === "string") return normalizeOptionalString(value); const record = asOptionalRecord(value); return normalizeOptionalString(record?.url); } function isSafeDownloadUrl(value) { const trimmed = value.trim(); if (!trimmed || /^data:/i.test(trimmed)) return false; if (trimmed.startsWith("/")) return !trimmed.startsWith("//") && trimmed.startsWith("/api/"); return isHttpUrl(trimmed); } /** Generates a stable id from transcript position plus display metadata. */ function artifactId(parts) { return `artifact_${createHash("sha256").update(`${parts.sessionKey}\0${parts.messageSeq}\0${parts.contentIndex}\0${parts.type}\0${parts.title}`).digest("base64url").slice(0, 18)}`; } function resolveMessageSeq(message, fallback) { const seq = asOptionalRecord(message["__openclaw"])?.seq; return typeof seq === "number" && Number.isInteger(seq) && seq > 0 ? seq : fallback; } function resolveMessageRunId(message) { const meta = asOptionalRecord(message["__openclaw"]); return normalizeOptionalString(meta?.runId) ?? normalizeOptionalString(message.runId); } function resolveMessageTaskId(message) { const meta = asOptionalRecord(message["__openclaw"]); return normalizeOptionalString(meta?.messageTaskId) ?? normalizeOptionalString(meta?.taskId) ?? normalizeOptionalString(message.messageTaskId) ?? normalizeOptionalString(message.taskId); } function resolveBlockDownload(block, opts) { const data = readStringValue(block.data)?.trim(); const content = readStringValue(block.content)?.trim(); const url = normalizeOptionalString(block.url) ?? normalizeOptionalString(block.openUrl); const imageUrl = mediaUrlValue(block.image_url); const audioUrl = normalizeOptionalString(block.audio_url); const source = asOptionalRecord(block.source); const sourceData = readStringValue(source?.data)?.trim(); const sourceUrl = normalizeOptionalString(source?.url); const dataUrl = [ url, sourceUrl, imageUrl, audioUrl, data, content, sourceData ].find((value) => typeof value === "string" && /^data:/i.test(value)); const base64FromDetectedDataUrl = readArtifactBase64Payload(dataUrl ? base64FromDataUrl(dataUrl) : void 0, opts); const directBase64 = [ data, sourceData, content ].filter((value) => typeof value === "string" && !/^data:/i.test(value)).map((value) => readArtifactBase64Payload(value, opts)).find((value) => value !== void 0); const base64 = base64FromDetectedDataUrl ?? directBase64; const remoteUrl = [ url, sourceUrl, imageUrl, audioUrl ].find((value) => typeof value === "string" && isSafeDownloadUrl(value)); const mimeType = normalizeOptionalString(block.mimeType) ?? normalizeOptionalString(block.media_type) ?? normalizeOptionalString(source?.media_type) ?? normalizeOptionalString(source?.mimeType) ?? (dataUrl ? mimeFromDataUrl(dataUrl) : void 0); const explicitSize = block.sizeBytes ?? source?.sizeBytes; const sizeBytes = typeof explicitSize === "number" && Number.isFinite(explicitSize) && explicitSize >= 0 ? Math.floor(explicitSize) : base64?.sizeBytes; if (base64) return { mode: "bytes", data: base64.data, mimeType, sizeBytes }; if (remoteUrl) return { mode: "url", url: remoteUrl, mimeType, sizeBytes }; return { mode: "unsupported", mimeType, sizeBytes }; } function isArtifactBlock(block) { const type = normalizeOptionalString(block.type)?.toLowerCase(); if (type === "image" || type === "audio" || type === "video" || type === "file" || type === "attachment" || type === "input_image" || type === "input_audio" || type === "input_video" || type === "input_file" || type === "image_url") return true; return typeof block.data === "string" || Boolean(block.url || block.openUrl || block.source || block.image_url || block.audio_url); } function collectArtifactsFromMessage(params) { const msg = asOptionalRecord(params.message); if (!msg) return; const messageSeq = resolveMessageSeq(msg, params.messageFallbackSeq); const messageRunId = resolveMessageRunId(msg); const messageTaskId = resolveMessageTaskId(msg); if (params.runId && messageRunId !== params.runId) return; if (params.taskId && messageTaskId !== params.taskId) return; const content = readAssistantDisplayContent(msg); for (let contentIndex = 0; contentIndex < content.length; contentIndex += 1) { const block = asOptionalRecord(content[contentIndex]); if (!block || !isArtifactBlock(block)) continue; const type = normalizeArtifactType(normalizeOptionalString(block.type) ?? "file"); const attachment = asOptionalRecord(block.attachment); const title = normalizeOptionalString(block.title) ?? normalizeOptionalString(block.fileName) ?? normalizeOptionalString(block.filename) ?? normalizeOptionalString(block.alt) ?? normalizeOptionalString(attachment?.label) ?? `${type} ${params.artifacts.length + 1}`; const declaredArtifactId = normalizeOptionalString(block.artifactId) ?? normalizeOptionalString(attachment?.artifactId); const id = declaredArtifactId && parseManagedOutgoingArtifactId(declaredArtifactId) ? declaredArtifactId : artifactId({ sessionKey: params.sessionKey, messageSeq, contentIndex, title, type }); const includeData = params.downloadArtifactId ? params.downloadArtifactId === id : params.includeDownloadData !== false; const download = resolveBlockDownload(attachment ?? block, { includeData }); const summary = { id, type, title, ...download.mimeType ? { mimeType: download.mimeType } : {}, ...download.sizeBytes !== void 0 ? { sizeBytes: download.sizeBytes } : {}, sessionKey: params.sessionKey, ...messageRunId ? { runId: messageRunId } : {}, ...messageTaskId ? { taskId: messageTaskId } : {}, messageSeq, source: "session-transcript", download: { mode: download.mode }, ...download.data !== void 0 ? { data: download.data } : {}, ...download.url ? { url: download.url } : {} }; params.artifacts.push(summary); } } /** Loads artifacts from the transcript selected by sessionKey, runId, or taskId. */ async function loadArtifacts(query, cfg, opts = {}, client = null) { const resolved = resolveAuthorizedArtifactSession(query, cfg, client); if (!resolved) return { artifacts: [] }; const { sessionKey } = resolved; const unscopedAgentId = parseAgentSessionKey(sessionKey) ? void 0 : resolved.agentId; const { storePath, entry } = unscopedAgentId ? loadGatewaySessionEntryReadOnly(sessionKey, { agentId: unscopedAgentId }) : loadGatewaySessionEntryReadOnly(sessionKey); const sessionId = entry?.sessionId; if (!sessionId || !storePath) return { sessionKey, artifacts: [] }; const artifacts = []; await visitSessionMessagesAsync({ agentId: resolved.agentId ?? resolveAgentIdFromSessionKey(sessionKey), sessionEntry: entry, sessionId, sessionKey, storePath }, (message, seq) => { collectArtifactsFromMessage({ message, messageFallbackSeq: seq, artifacts, sessionKey, runId: query.runId, taskId: query.taskId, includeDownloadData: opts.includeDownloadData, downloadArtifactId: opts.downloadArtifactId }); }); return { sessionKey, artifacts }; } function requireQueryable(params, respond) { if (params.sessionKey || params.runId || params.taskId) return true; respond(false, void 0, artifactError("artifact_query_unsupported", "artifacts require one of sessionKey, runId, or taskId")); return false; } function respondArtifactNotFound(respond, requestedArtifactId) { respond(false, void 0, artifactError("artifact_not_found", "artifact not found", { artifactId: requestedArtifactId })); } async function runArtifactSessionOperation(respond, operation) { try { return { ok: true, value: await operation() }; } catch (error) { if (error instanceof ArtifactSessionResolutionError) { respond(false, void 0, error.shape); return { ok: false }; } if (error instanceof AgentSelectionRequiredError) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message)); return { ok: false }; } throw error; } } async function findArtifact(params, cfg, opts = {}, client = null) { const loaded = await loadArtifacts(params, cfg, opts, client); return { sessionKey: loaded.sessionKey, artifact: loaded.artifacts.find((artifact) => artifact.id === params.artifactId) }; } function toSummary(artifact) { const { data: _dataValue, url: _url, ...summary } = artifact; return summary; } /** Gateway handlers for listing, summarizing, and downloading transcript artifacts. */ const artifactsHandlers = { "artifacts.list": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateArtifactsListParams, "artifacts.list", respond)) return; if (!requireQueryable(params, respond)) return; const cfg = context.getRuntimeConfig?.(); const admittedQuery = admitArtifactQuery(params, cfg, respond); if (!admittedQuery) return; const loaded = await runArtifactSessionOperation(respond, () => loadArtifacts(admittedQuery, cfg, { includeDownloadData: false }, client)); if (!loaded.ok) return; const { artifacts, sessionKey } = loaded.value; if (!sessionKey && (params.runId || params.taskId)) { respond(false, void 0, artifactError("artifact_scope_not_found", "no session found for artifact query")); return; } respond(true, { artifacts: artifacts.map(toSummary) }); }, "artifacts.get": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateArtifactsGetParams, "artifacts.get", respond)) return; if (!requireQueryable(params, respond)) return; const cfg = context.getRuntimeConfig?.(); const admittedQuery = admitArtifactQuery(params, cfg, respond); if (!admittedQuery) return; const found = await runArtifactSessionOperation(respond, () => findArtifact(admittedQuery, cfg, { includeDownloadData: false }, client)); if (!found.ok) return; const { artifact } = found.value; if (!artifact) { respondArtifactNotFound(respond, params.artifactId); return; } respond(true, { artifact: toSummary(artifact) }); }, "artifacts.download": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateArtifactsDownloadParams, "artifacts.download", respond)) return; if (!requireQueryable(params, respond)) return; const cfg = context.getRuntimeConfig?.(); const admittedQuery = admitArtifactQuery(params, cfg, respond); if (!admittedQuery) return; if (admittedQuery.sessionKey && !admittedQuery.runId && !admittedQuery.taskId && parseManagedOutgoingArtifactId(params.artifactId)) { const resolvedResult = await runArtifactSessionOperation(respond, () => resolveAuthorizedArtifactSession(admittedQuery, cfg, client)); if (!resolvedResult.ok) return; const resolved = resolvedResult.value; const defaultAgentId = resolved ? tryResolveSessionCompatibilityOwnerAgentId(cfg ?? {}, resolved.sessionKey) : void 0; const managed = resolved ? await resolveManagedOutgoingMediaArtifactDownload({ sessionKey: resolved.sessionKey, ...resolved.agentId ? { agentId: resolved.agentId } : {}, ...defaultAgentId ? { defaultAgentId } : {}, artifactId: params.artifactId }) : null; if (managed) { respond(true, { artifact: { id: managed.artifactId, type: managed.type, title: managed.title, ...managed.mimeType ? { mimeType: managed.mimeType } : {}, ...managed.sizeBytes !== void 0 ? { sizeBytes: managed.sizeBytes } : {}, sessionKey: managed.sessionKey, source: "session-transcript", download: { mode: "url" } }, url: managed.url, expiresAt: managed.expiresAt }); return; } respondArtifactNotFound(respond, params.artifactId); return; } const found = await runArtifactSessionOperation(respond, () => findArtifact(admittedQuery, cfg, { downloadArtifactId: params.artifactId }, client)); if (!found.ok) return; const { artifact } = found.value; if (!artifact) { respondArtifactNotFound(respond, params.artifactId); return; } if (artifact.download.mode === "unsupported") { respond(false, void 0, artifactError("artifact_download_unsupported", "artifact download is unsupported", { artifactId: artifact.id })); return; } const managedUrl = artifact.download.mode === "url" && artifact.url && artifact.sessionKey ? await resolveManagedOutgoingMediaUrlDownload({ sessionKey: artifact.sessionKey, url: artifact.url }) : null; respond(true, { artifact: toSummary(artifact), ...artifact.download.mode === "bytes" ? { encoding: "base64", data: artifact.data } : {}, ...artifact.download.mode === "url" ? { url: managedUrl?.url ?? artifact.url, ...managedUrl ? { expiresAt: managedUrl.expiresAt } : {} } : {} }); } }; //#endregion export { artifactsHandlers };