UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

243 lines (242 loc) • 9.44 kB
import { l as normalizeOptionalString } from "./string-coerce-6kR-86XC.mjs"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import { T as tryResolveLegacyCompatibilityAgentId, d as resolveAgentWorkspaceDir } from "./agent-scope-config-BU4qc9bA.mjs"; import "./session-key-BC4m_Ly5.mjs"; import { n as listAgentEntries } from "./agent-roster-B45JERT5.mjs"; import "./legacy.default-agent-owner-DlHTJ13Y.mjs"; import "./agent-scope-DWB4x52T.mjs"; import { n as AVATAR_MAX_DATA_URL_CHARS, o as isRenderableAvatarImageDataUrl } from "./avatar-limits-2506OuP3.mjs"; import { c as looksLikeAvatarPath, l as resolveAvatarMime, n as isAvatarDataUrl, r as isAvatarHttpUrl, s as isWindowsAbsolutePath, t as hasAvatarUriScheme } from "./avatar-policy-C8qNaShx.mjs"; import { n as prepareLocalAgentAvatarFile } from "./identity-avatar-file-BNFbP3yM.mjs"; import { a as loadAgentIdentityFromWorkspaceAsync } from "./identity-file-BQ5VsB0d.mjs"; import { n as resolveAgentIdentity } from "./identity-cOgj0nSe.mjs"; import { n as buildControlUiResourcePath, r as matchControlUiResourceUrl } from "./control-ui-resource-routes-y_jZ6jnp.mjs"; import "./control-ui-contract-B_YMMx3J.mjs"; import { createHash } from "node:crypto"; //#region src/gateway/assistant-avatar-cache.ts const fileSources = /* @__PURE__ */ new WeakMap(); const inlineFiles = /* @__PURE__ */ new WeakMap(); const dataSources = /* @__PURE__ */ new Map(); function prepareGatewayAvatarFile(file) { let source = fileSources.get(file); if (!source) { source = { file, revision: createHash("sha256").update("thumbnail-128-png-v1:").update(JSON.stringify([file.path, file.stat])).digest("hex").slice(0, 16) }; fileSources.set(file, source); } return source; } function prepareGatewayAvatarDataUrl(dataUrl) { const cached = dataSources.get(dataUrl); if (cached) { dataSources.delete(dataUrl); dataSources.set(dataUrl, cached); return cached; } if (!isRenderableAvatarImageDataUrl(dataUrl)) return; const source = { dataUrl, revision: createHash("sha256").update("thumbnail-128-png-v1:").update(dataUrl).digest("hex").slice(0, 16) }; dataSources.set(dataUrl, source); pruneMapToMaxSize(dataSources, 4); return source; } function gatewayAvatarFileDataUrl(file) { if (!file.body) return; let dataUrl = inlineFiles.get(file); if (!dataUrl) { dataUrl = `data:${resolveAvatarMime(file.path)};base64,${file.body.toString("base64")}`; inlineFiles.set(file, dataUrl); } return dataUrl; } //#endregion //#region src/gateway/assistant-identity.ts const ASSISTANT_IDENTITY_LIMITS = { name: 50, emoji: 16 }; const preparedIdentities = /* @__PURE__ */ new WeakMap(); const DEFAULT_ASSISTANT_IDENTITY = { name: "Assistant", avatar: "A" }; function normalizeIdentityValue(field, value) { const trimmed = normalizeOptionalString(value); return trimmed ? truncateUtf16Safe(trimmed, ASSISTANT_IDENTITY_LIMITS[field]) : void 0; } function isAvatarUrl(value) { return isAvatarHttpUrl(value) || isRenderableAvatarImageDataUrl(value); } function normalizeAvatarValue(value) { const trimmed = normalizeOptionalString(value); if (!trimmed || trimmed.length > AVATAR_MAX_DATA_URL_CHARS) return; if (isAvatarUrl(trimmed)) return trimmed; if (hasAvatarUriScheme(trimmed) && !isWindowsAbsolutePath(trimmed)) return; if (looksLikeAvatarPath(trimmed)) return trimmed; if (!/\s/.test(trimmed) && trimmed.length <= 4) return trimmed; } function normalizeEmojiValue(value) { if (!value || !/\P{ASCII}/u.test(value)) return; if (isAvatarUrl(value) || hasAvatarUriScheme(value) && !isWindowsAbsolutePath(value) || looksLikeAvatarPath(value)) return; return value; } function resolveAssistantAgentId(cfg, agentId) { return normalizeAgentId(agentId ?? tryResolveLegacyCompatibilityAgentId(cfg) ?? listAgentEntries(cfg)[0]?.id ?? "main"); } /** Resolve the display name/avatar/emoji for an agent-facing assistant identity. */ async function resolveAssistantIdentity(params) { const agentId = resolveAssistantAgentId(params.cfg, params.agentId); const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId); const { name: configuredName, emoji: configuredEmoji, avatar: configuredAvatar } = resolveAgentIdentity(params.cfg, agentId) ?? {}; const fileIdentity = workspaceDir ? await loadAgentIdentityFromWorkspaceAsync(workspaceDir) : null; let prepared = preparedIdentities.get(params.cfg); if (!prepared) { prepared = /* @__PURE__ */ new Map(); preparedIdentities.set(params.cfg, prepared); } const key = JSON.stringify([agentId, workspaceDir]); const cached = prepared.get(key); if (cached && cached.file === fileIdentity && cached.name === configuredName && cached.emoji === configuredEmoji && cached.avatar === configuredAvatar) { prepared.delete(key); prepared.set(key, cached); return cached.identity; } const agentName = normalizeIdentityValue("name", configuredName); const fileName = normalizeIdentityValue("name", fileIdentity?.name); const [name, nameSource] = (agentName ? [agentName, "agent"] : fileName ? [fileName, "workspace"] : void 0) ?? [DEFAULT_ASSISTANT_IDENTITY.name, "default"]; const identity = { agentId, name, nameSource, avatar: [ normalizeAvatarValue(configuredAvatar), normalizeAvatarValue(configuredEmoji), normalizeAvatarValue(fileIdentity?.avatar), normalizeAvatarValue(fileIdentity?.emoji) ].find(Boolean) ?? DEFAULT_ASSISTANT_IDENTITY.avatar, emoji: [ normalizeIdentityValue("emoji", configuredEmoji), normalizeIdentityValue("emoji", fileIdentity?.emoji), normalizeIdentityValue("emoji", configuredAvatar), normalizeIdentityValue("emoji", fileIdentity?.avatar) ].map((candidate) => normalizeEmojiValue(candidate)).find(Boolean) }; prepared.set(key, { file: fileIdentity, name: configuredName, emoji: configuredEmoji, avatar: configuredAvatar, identity }); pruneMapToMaxSize(prepared, 4); return identity; } //#endregion //#region src/gateway/assistant-avatar.ts function gatewayAssistantAvatarUrl(projection, basePath, agentId) { return projection.image ? `${buildControlUiResourcePath("agentAvatar", basePath, agentId)}?v=${projection.image.revision}` : void 0; } function resolveSameOriginAvatarUrl(basePath, source) { const unbased = matchControlUiResourceUrl("agentAvatar", source); if (unbased) return `${buildControlUiResourcePath("agentAvatar", basePath, unbased.value)}${unbased.search}${unbased.hash}`; return matchControlUiResourceUrl("agentAvatar", source, basePath) ? source : void 0; } /** Prepare a selected source; the file owner retains descriptor custody in its worker. */ async function prepareGatewayAssistantAvatar(params) { const { cfg, identity } = params; const source = identity.avatar; if (isAvatarHttpUrl(source)) return { resolution: { kind: "remote", url: source, source } }; if (isAvatarDataUrl(source)) { const image = prepareGatewayAvatarDataUrl(source); return image ? { resolution: { kind: "data", url: source, source }, image } : { resolution: { kind: "none", reason: "unsupported_data_url", source } }; } if (hasAvatarUriScheme(source) && !isWindowsAbsolutePath(source)) return { resolution: { kind: "none", reason: "unsupported_uri", source } }; if (resolveSameOriginAvatarUrl(cfg.gateway?.controlUi?.basePath, source)) return { resolution: null }; if (!looksLikeAvatarPath(source)) return { resolution: null }; const prepared = await prepareLocalAgentAvatarFile({ cfg, agentId: identity.agentId, source, readBody: params.readBody }); if (!prepared.ok) return { resolution: { kind: "none", reason: prepared.reason, source } }; return { resolution: { kind: "local", filePath: prepared.file.path, source }, file: prepared.file, image: prepareGatewayAvatarFile(prepared.file) }; } /** Resolve one selected identity avatar and its matching public metadata. */ async function resolveGatewayAssistantAvatar(params) { const { cfg, identity } = params; const source = identity.avatar; const sameOriginAvatarUrl = resolveSameOriginAvatarUrl(params.httpBasePath ?? cfg.gateway?.controlUi?.basePath, source); if (sameOriginAvatarUrl) return { avatar: sameOriginAvatarUrl, resolution: null }; const prepared = await prepareGatewayAssistantAvatar({ ...params, readBody: params.httpBasePath === void 0 }); if (prepared.resolution?.kind === "none") return { avatar: identity.emoji ?? DEFAULT_ASSISTANT_IDENTITY.avatar, resolution: prepared.resolution }; if (params.httpBasePath !== void 0) return { avatar: gatewayAssistantAvatarUrl(prepared, params.httpBasePath, identity.agentId) ?? source, resolution: prepared.resolution }; if (!prepared.file) return { avatar: source, resolution: prepared.resolution }; const dataUrl = gatewayAvatarFileDataUrl(prepared.file); if (!dataUrl) return { avatar: identity.emoji ?? DEFAULT_ASSISTANT_IDENTITY.avatar, resolution: { kind: "none", reason: "unreadable", source } }; return { avatar: dataUrl, resolution: prepared.resolution }; } //#endregion export { resolveAssistantAgentId as a, DEFAULT_ASSISTANT_IDENTITY as i, prepareGatewayAssistantAvatar as n, resolveAssistantIdentity as o, resolveGatewayAssistantAvatar as r, gatewayAssistantAvatarUrl as t };