openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
108 lines (107 loc) • 5.82 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { a as wrapExternalContent } from "./external-content-Bkm4w5qo.mjs";
import { n as escapeXml } from "./xml-mfWEXuUN.mjs";
import { t as renderDocumentTruncationNotice } from "./document-extraction-metadata-dXxmZ0ie.mjs";
import { sanitizeUntrustedFileName } from "@openclaw/fs-safe/advanced";
//#region src/media/file-context.ts
function escapeFileBlockContent(value) {
return value.replace(/<\s*\/\s*file\s*>/gi, "</file>").replace(/<\s*file\b/gi, "<file");
}
function sanitizeFileName(value, fallbackName) {
const normalized = normalizeOptionalString(typeof value === "string" ? value.replace(/[\r\n\t]+/g, " ") : void 0) ?? "";
return sanitizeUntrustedFileName(normalized, fallbackName);
}
/** Renders sanitized attachment text as a model-visible file block without allowing file-tag injection. */
function renderFileContextBlock(params) {
const fallbackName = normalizeOptionalString(params.fallbackName) ?? "attachment";
const safeName = sanitizeFileName(params.filename, fallbackName);
const safeContent = escapeFileBlockContent(params.content);
const mimeType = normalizeOptionalString(params.mimeType);
const attrs = [`name="${escapeXml(safeName)}"`, mimeType ? `mime="${escapeXml(mimeType)}"` : void 0].filter(Boolean).join(" ");
if (params.surroundContentWithNewlines === false) return `<file ${attrs}>${safeContent}</file>`;
return `<file ${attrs}>\n${safeContent}\n</file>`;
}
//#endregion
//#region src/media-understanding/file-attachment-outcomes.ts
const MIME_TYPE = String.raw`([a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+)`;
const HTTP_TOKEN = String.raw`[a-z0-9!#$%&'*+.^_\x60|~-]+`;
const HTTP_QUOTED_STRING = String.raw`"(?:[\t !#-\[\]-~]|\\[\t -~])*"`;
const MIME_PARAMETER = String.raw`[ \t]*;[ \t]*${HTTP_TOKEN}=(?:${HTTP_TOKEN}|${HTTP_QUOTED_STRING})`;
const MIME_TYPE_WITH_OPTIONAL_PARAMS = new RegExp(String.raw`^${MIME_TYPE}(?:${MIME_PARAMETER})*$`, "i");
const MARKER_MIME_MAX_CHARS = 100;
function sanitizeMimeType(value) {
const trimmed = normalizeOptionalString(value);
if (!trimmed) return;
return trimmed.match(MIME_TYPE_WITH_OPTIONAL_PARAMS)?.[1]?.toLowerCase();
}
function markerSafeMime(value) {
const mime = sanitizeMimeType(value);
return mime && mime.length <= MARKER_MIME_MAX_CHARS ? mime : void 0;
}
function resolveFileExtractionOutcome(extraction) {
const text = extraction.text ?? "";
const images = extraction.images ?? [];
const metadata = extraction.metadata ? { metadata: extraction.metadata } : {};
if (text.trim() || extraction.metadata?.textTruncated) return {
kind: "extracted",
text,
images,
...metadata
};
return images.length > 0 ? {
kind: "rendered-to-images",
images,
...metadata
} : {
kind: "no-extractable-text",
...metadata
};
}
function wrapUntrustedAttachmentContent(content) {
return wrapExternalContent(content, {
source: "unknown",
includeWarning: false
});
}
const MARKER_LOCAL_PATH_MAX_CHARS = 300;
const POSIX_ABSOLUTE_PATH = /^\//;
const WINDOWS_ABSOLUTE_PATH = /^[A-Za-z]:\\/;
const MARKER_PATH_SAFE = /^[\p{L}\p{M}\p{N} /\\:._-]+$/u;
function markerSafeLocalPath(value, allowWorkspaceRelative = false) {
if (!value || value.length > MARKER_LOCAL_PATH_MAX_CHARS) return;
if (!(POSIX_ABSOLUTE_PATH.test(value) || WINDOWS_ABSOLUTE_PATH.test(value)) && (!allowWorkspaceRelative || value.includes("\\") || value.split("/").some((segment) => !segment || segment === "." || segment === ".."))) return;
return MARKER_PATH_SAFE.test(value) ? value : void 0;
}
const SKIPPED_FILE_OUTCOME_KINDS = /* @__PURE__ */ new Set([
"unsupported-format",
"policy-rejected",
"read-failure",
"url-sources-disabled"
]);
function isSkippedFileOutcome(outcome) {
return SKIPPED_FILE_OUTCOME_KINDS.has(outcome.kind);
}
function renderFileAttachmentOutcome(outcome, options) {
switch (outcome.kind) {
case "extracted": return [renderDocumentTruncationNotice(outcome.metadata), wrapUntrustedAttachmentContent(outcome.text)].filter(Boolean).join("\n");
case "rendered-to-images": return [renderDocumentTruncationNotice(outcome.metadata), "[PDF content rendered to images]"].filter(Boolean).join("\n");
case "no-extractable-text": return [renderDocumentTruncationNotice(outcome.metadata), "[No extractable text]"].filter(Boolean).join("\n");
case "unsupported-format": {
const mime = markerSafeMime(outcome.mime);
const formatClause = mime ? `Unsupported document format: ${mime}.` : "Unsupported document format.";
const localPath = markerSafeLocalPath(options?.selfServeLocalPath === false ? void 0 : options?.selfServeLocalPath ?? outcome.localPath, typeof options?.selfServeLocalPath === "string");
const formatHint = outcome.mime?.startsWith("application/vnd.openxmlformats-officedocument") ? " (this Office file is a zip archive containing XML)" : "";
return localPath ? [`[${formatClause} The approved local file path follows as external attachment metadata. Its text is not extracted automatically. Read the file yourself with your tools before answering${formatHint}; do not ask the user to paste the contents.]`, wrapUntrustedAttachmentContent(localPath)].join("") : `[${formatClause} PDF and plain-text attachments can be read.]`;
}
case "policy-rejected": {
const mime = markerSafeMime(outcome.mime);
return mime ? `[Attachment type not allowed: ${mime}]` : "[Attachment type not allowed]";
}
case "read-failure": return "[Attachment could not be read]";
case "url-sources-disabled": return "[Attachment skipped: URL file sources are disabled]";
case "claimed-elsewhere": return null;
default: return outcome;
}
}
//#endregion
export { renderFileContextBlock as a, sanitizeMimeType as i, renderFileAttachmentOutcome as n, resolveFileExtractionOutcome as r, isSkippedFileOutcome as t };