UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

101 lines (100 loc) • 5.25 kB
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { t as basenameFromAnyPath } from "./file-name-CKnWacTs.mjs"; import { o as isAudioFileName } from "./mime-C3j3dfQb.mjs"; import { u as normalizeMediaFacts } from "./media-facts-BIPREXi-.mjs"; import { s as getMediaDir } from "./store-7jnF8fA5.mjs"; import path from "node:path"; //#region src/auto-reply/media-note.ts /** Builds compact prompt notes for inbound media attachments. */ function stripDarwinPrivatePrefix(value) { return value.startsWith("/private/var/") ? value.slice(8) : value; } function normalizeManagedInboundMediaRef(value) { if (!path.isAbsolute(value)) return value; const mediaDir = stripDarwinPrivatePrefix(path.resolve(getMediaDir())); const candidate = stripDarwinPrivatePrefix(path.resolve(value)); const inboundDir = path.join(mediaDir, "inbound"); const relativeToInbound = path.relative(inboundDir, candidate); if (!relativeToInbound || relativeToInbound.startsWith("..") || path.isAbsolute(relativeToInbound)) return value; return `media://inbound/${path.basename(candidate)}`; } function sanitizeInlineMediaNoteValue(value) { const trimmed = value?.trim(); if (!trimmed) return ""; return normalizeManagedInboundMediaRef(trimmed).replace(/[\p{Cc}\]]+/gu, " ").replace(/\s+/g, " ").trim(); } function formatMediaAttachedLine(fact, index, total) { const prefix = total > 1 ? `[media attached ${index}/${total}: ` : "[media attached: "; const pathValue = sanitizeInlineMediaNoteValue(fact.path); const typeRaw = sanitizeInlineMediaNoteValue(fact.contentType ?? fact.kind); const typePart = typeRaw ? ` (${typeRaw})` : ""; const urlRaw = sanitizeInlineMediaNoteValue(fact.url); const urlPart = urlRaw && urlRaw !== pathValue ? ` | ${urlRaw}` : ""; const fileName = truncateUtf16Safe(sanitizeInlineMediaNoteValue(basenameFromAnyPath(fact.fileName ?? "")), 256); return `${prefix}${pathValue}${typePart}${urlPart}${fileName ? ` ${JSON.stringify(fileName)}` : ""}]`; } const AUDIO_EXTENSIONS_WITHOUT_CANONICAL_MIME = [ ".webm", ".wma", ".alac" ]; function isAudioPath(pathLocal) { if (!pathLocal) return false; if (isAudioFileName(pathLocal)) return true; const lower = normalizeLowercaseStringOrEmpty(pathLocal); return AUDIO_EXTENSIONS_WITHOUT_CANONICAL_MIME.some((extension) => lower.endsWith(extension)); } function isValidAttachmentIndex(index, attachmentCount) { return Number.isSafeInteger(index) && index >= 0 && index < attachmentCount; } function collectTranscribedAudioAttachmentIndices(ctx, attachmentCount) { const transcribedAudioIndices = /* @__PURE__ */ new Set(); if (Array.isArray(ctx.MediaUnderstanding)) { for (const output of ctx.MediaUnderstanding) if (output.kind === "audio.transcription" && isValidAttachmentIndex(output.attachmentIndex, attachmentCount)) transcribedAudioIndices.add(output.attachmentIndex); } if (Array.isArray(ctx.MediaUnderstandingDecisions)) for (const decision of ctx.MediaUnderstandingDecisions) { if (decision.capability !== "audio" || decision.outcome !== "success") continue; for (const attachment of decision.attachments) if (attachment.chosen?.outcome === "success" && isValidAttachmentIndex(attachment.attachmentIndex, attachmentCount)) transcribedAudioIndices.add(attachment.attachmentIndex); } return transcribedAudioIndices; } /** Formats prompt-visible attachment text and retains facts that still need native hydration. */ function buildInboundMediaNoteProjection(ctx) { const facts = normalizeMediaFacts(ctx.media); const entries = facts.flatMap((fact, index) => fact.path || fact.url ? [{ fact, index }] : []); if (entries.length === 0) return { media: [], mediaIndexes: [] }; const transcribedAudioIndices = collectTranscribedAudioAttachmentIndices(ctx, facts.length); const canStripSingleAttachmentByTranscript = Boolean(ctx.Transcript?.trim()) && facts.length === 1; const visibleEntries = entries.filter((entry) => { const normalizedType = normalizeLowercaseStringOrEmpty(entry.fact.contentType ?? entry.fact.kind); const isAudioByMime = normalizedType === "audio" || normalizedType.startsWith("audio/"); return !(entry.fact.kind === "audio" || isAudioPath(entry.fact.path) || isAudioByMime) || !(entry.fact.transcribed === true || transcribedAudioIndices.has(entry.index) || canStripSingleAttachmentByTranscript && entry.index === 0); }); if (visibleEntries.length === 0) return { media: [], mediaIndexes: [] }; const describedImageIndices = new Set(ctx.MediaUnderstanding?.flatMap((output) => output.kind === "image.description" ? [output.attachmentIndex] : []) ?? []); const media = visibleEntries.map((entry) => ({ ...entry.fact, ...describedImageIndices.has(entry.index) ? { hydrationSuppressed: true } : {} })); const mediaIndexes = visibleEntries.map((entry) => entry.index); const count = visibleEntries.length; const lines = visibleEntries.map((entry, index) => formatMediaAttachedLine(entry.fact, index + 1, count)); if (count > 1) lines.unshift(`[media attached: ${count} files]`); return { text: lines.join("\n"), media, mediaIndexes }; } //#endregion export { buildInboundMediaNoteProjection as t };