mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
419 lines
13 KiB
JavaScript
419 lines
13 KiB
JavaScript
const PREVIEWABLE_MEDIA_TYPES = new Set(["images", "video", "audio", "3d", "text"]);
|
|
const THREE_D_EXTENSIONS = [
|
|
".obj",
|
|
".fbx",
|
|
".gltf",
|
|
".glb",
|
|
".stl",
|
|
".ply",
|
|
".spz",
|
|
".splat",
|
|
".ksplat",
|
|
".usdz",
|
|
];
|
|
const HDR_IMAGE_EXTENSIONS = [".exr", ".hdr"];
|
|
const TEXT_PREVIEW_MAX_LENGTH = 1024;
|
|
const ADVANCED_3D_RESULT_MAX_ENTRIES = 8;
|
|
const ADVANCED_3D_RESULT_PATH_MAX_LENGTH = 1024;
|
|
const UNSAFE_ADVANCED_3D_PATH_CHARACTERS = /[\p{Cc}\p{Cf}\p{Cs}]/u;
|
|
const FILE_TEXT_EXTENSIONS = new Set(["txt", "md", "markdown", "json", "csv", "yaml", "yml", "xml", "log"]);
|
|
const FILE_OUTPUT_TYPES = new Set(["input", "output", "temp"]);
|
|
const FILE_OUTPUT_MAX_REFS = 64;
|
|
const FILE_OUTPUT_FIELD_MAX_LENGTH = 1024;
|
|
|
|
function pickAssetHash(imageRef = {}) {
|
|
if (!imageRef || typeof imageRef !== "object") {
|
|
return "";
|
|
}
|
|
const direct = typeof imageRef.asset_hash === "string"
|
|
? imageRef.asset_hash.trim()
|
|
: (typeof imageRef.hash === "string" ? imageRef.hash.trim() : "");
|
|
if (direct) {
|
|
return direct;
|
|
}
|
|
const nested = imageRef.asset;
|
|
if (nested && typeof nested === "object") {
|
|
if (typeof nested.asset_hash === "string" && nested.asset_hash.trim()) {
|
|
return nested.asset_hash.trim();
|
|
}
|
|
if (typeof nested.hash === "string" && nested.hash.trim()) {
|
|
return nested.hash.trim();
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function pickAssetApiId(imageRef = {}) {
|
|
if (!imageRef || typeof imageRef !== "object") {
|
|
return "";
|
|
}
|
|
const direct = typeof imageRef.asset_api_id === "string"
|
|
? imageRef.asset_api_id.trim()
|
|
: (typeof imageRef.asset_id === "string" ? imageRef.asset_id.trim() : "");
|
|
if (direct) {
|
|
return direct;
|
|
}
|
|
const nested = imageRef.asset;
|
|
if (!nested || typeof nested !== "object") {
|
|
return "";
|
|
}
|
|
if (typeof nested.asset_id === "string" && nested.asset_id.trim()) {
|
|
return nested.asset_id.trim();
|
|
}
|
|
if (typeof nested.id === "string" && nested.id.trim()) {
|
|
return nested.id.trim();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function pickFilename(imageRef = {}) {
|
|
if (!imageRef || typeof imageRef !== "object") {
|
|
return "";
|
|
}
|
|
if (typeof imageRef.filename === "string" && imageRef.filename.trim()) {
|
|
return imageRef.filename.trim();
|
|
}
|
|
if (typeof imageRef.name === "string" && imageRef.name.trim()) {
|
|
return imageRef.name.trim();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function has3dExtension(filename = "") {
|
|
return THREE_D_EXTENSIONS.some((ext) => String(filename).toLowerCase().endsWith(ext));
|
|
}
|
|
|
|
export function isHdrImageFilename(filename = "") {
|
|
return HDR_IMAGE_EXTENSIONS.some((ext) => String(filename).toLowerCase().endsWith(ext));
|
|
}
|
|
|
|
function filenameFromUrl(url = "") {
|
|
if (!url) {
|
|
return "";
|
|
}
|
|
try {
|
|
const base = typeof window !== "undefined" && window.location
|
|
? window.location.origin
|
|
: "http://127.0.0.1";
|
|
const parsed = new URL(url, base);
|
|
return parsed.searchParams.get("filename") || parsed.pathname.split("/").pop() || "";
|
|
} catch {
|
|
return String(url).split("?", 1)[0].split("#", 1)[0].split("/").pop() || "";
|
|
}
|
|
}
|
|
|
|
export function isHdrImageOutputRef(outputRef = {}) {
|
|
if (!outputRef || typeof outputRef !== "object") {
|
|
return false;
|
|
}
|
|
if (outputRef.media_type && outputRef.media_type !== "images") {
|
|
return false;
|
|
}
|
|
if (isHdrImageFilename(outputRef.filename || "")) {
|
|
return true;
|
|
}
|
|
return isHdrImageFilename(filenameFromUrl(outputRef.view_url || ""));
|
|
}
|
|
|
|
function resolveMediaType(imageRef = {}, fallback = "images") {
|
|
if (imageRef && typeof imageRef === "object") {
|
|
const direct = typeof imageRef.media_type === "string"
|
|
? imageRef.media_type.trim()
|
|
: (typeof imageRef.mediaType === "string" ? imageRef.mediaType.trim() : "");
|
|
if (PREVIEWABLE_MEDIA_TYPES.has(direct)) {
|
|
return direct;
|
|
}
|
|
}
|
|
return PREVIEWABLE_MEDIA_TYPES.has(fallback) ? fallback : "images";
|
|
}
|
|
|
|
function normalizeTextOutputRef(value) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
let content = String(value);
|
|
if (!content) {
|
|
return null;
|
|
}
|
|
const textTruncated = content.length > TEXT_PREVIEW_MAX_LENGTH;
|
|
if (textTruncated) {
|
|
content = content.slice(0, TEXT_PREVIEW_MAX_LENGTH);
|
|
}
|
|
return {
|
|
filename: "",
|
|
subfolder: "",
|
|
type: "output",
|
|
media_type: "text",
|
|
asset_hash: "",
|
|
asset_api_id: "",
|
|
asset_api_required: false,
|
|
resolution: "inline_text",
|
|
unsupported_reason: "",
|
|
is_asset_backed: false,
|
|
content,
|
|
text_truncated: textTruncated,
|
|
viewParams: null,
|
|
};
|
|
}
|
|
|
|
function hasUnsafeFileCharacters(value = "") {
|
|
return Array.from(String(value)).some((char) => {
|
|
const code = char.charCodeAt(0);
|
|
return code < 32 || code === 127;
|
|
});
|
|
}
|
|
|
|
function codePointLength(value = "") {
|
|
return Array.from(String(value)).length;
|
|
}
|
|
|
|
function hasUnsafeAdvanced3dPathCharacters(value = "") {
|
|
return UNSAFE_ADVANCED_3D_PATH_CHARACTERS.test(String(value));
|
|
}
|
|
|
|
function normalizeAdvanced3dResult(result) {
|
|
if (
|
|
!Array.isArray(result)
|
|
|| result.length === 0
|
|
|| result.length > ADVANCED_3D_RESULT_MAX_ENTRIES
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const rawPath = result[0];
|
|
if (
|
|
typeof rawPath !== "string"
|
|
|| codePointLength(rawPath) > ADVANCED_3D_RESULT_PATH_MAX_LENGTH
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedPath = rawPath.replaceAll("\\", "/");
|
|
if (
|
|
!normalizedPath
|
|
|| codePointLength(normalizedPath) > ADVANCED_3D_RESULT_PATH_MAX_LENGTH
|
|
|| hasUnsafeAdvanced3dPathCharacters(normalizedPath)
|
|
|| normalizedPath.startsWith("/")
|
|
|| [":", "%", "?", "#"].some((marker) => normalizedPath.includes(marker))
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const segments = normalizedPath.split("/");
|
|
if (segments.some((segment) => (
|
|
!segment
|
|
|| segment === "."
|
|
|| segment === ".."
|
|
|| segment !== segment.trim()
|
|
))) {
|
|
return null;
|
|
}
|
|
|
|
const filename = segments.at(-1);
|
|
if (!has3dExtension(filename)) {
|
|
return null;
|
|
}
|
|
|
|
// SECURITY: later result entries may contain private host metadata. Never
|
|
// inspect or project anything except the validated path at index zero.
|
|
return normalizeComfyOutputRef({
|
|
filename,
|
|
subfolder: segments.slice(0, -1).join("/"),
|
|
type: "output",
|
|
}, "3d");
|
|
}
|
|
|
|
function normalizeFileTextOutputRef(outputRef) {
|
|
if (!outputRef || typeof outputRef !== "object" || Array.isArray(outputRef)) {
|
|
return null;
|
|
}
|
|
|
|
if (typeof outputRef.filename !== "string") {
|
|
return null;
|
|
}
|
|
if (codePointLength(outputRef.filename) > FILE_OUTPUT_FIELD_MAX_LENGTH) {
|
|
return null;
|
|
}
|
|
const filename = outputRef.filename.trim();
|
|
if (
|
|
!filename
|
|
|| codePointLength(filename) > FILE_OUTPUT_FIELD_MAX_LENGTH
|
|
|| hasUnsafeFileCharacters(filename)
|
|
|| filename === "."
|
|
|| filename === ".."
|
|
|| filename.includes("/")
|
|
|| filename.includes("\\")
|
|
) {
|
|
return null;
|
|
}
|
|
const dotIndex = filename.lastIndexOf(".");
|
|
const suffix = dotIndex >= 0 ? filename.slice(dotIndex + 1).toLowerCase() : "";
|
|
if (!FILE_TEXT_EXTENSIONS.has(suffix)) {
|
|
return null;
|
|
}
|
|
|
|
if (outputRef.subfolder !== undefined && typeof outputRef.subfolder !== "string") {
|
|
return null;
|
|
}
|
|
if (
|
|
typeof outputRef.subfolder === "string"
|
|
&& codePointLength(outputRef.subfolder) > FILE_OUTPUT_FIELD_MAX_LENGTH
|
|
) {
|
|
return null;
|
|
}
|
|
const subfolder = typeof outputRef.subfolder === "string" ? outputRef.subfolder.trim() : "";
|
|
if (
|
|
codePointLength(subfolder) > FILE_OUTPUT_FIELD_MAX_LENGTH
|
|
|| hasUnsafeFileCharacters(subfolder)
|
|
|| subfolder.includes("\\")
|
|
|| subfolder.startsWith("/")
|
|
|| subfolder.split("/").some((part) => part === "." || part === "..")
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (outputRef.type !== undefined && typeof outputRef.type !== "string") {
|
|
return null;
|
|
}
|
|
const type = typeof outputRef.type === "string" && outputRef.type.trim()
|
|
? outputRef.type.trim()
|
|
: "output";
|
|
if (!FILE_OUTPUT_TYPES.has(type)) {
|
|
return null;
|
|
}
|
|
|
|
// SECURITY: accept only validated host fields and the existing encoded /view
|
|
// seam. Raw URL or asset fields on a files entry are never fetch targets.
|
|
return {
|
|
filename,
|
|
subfolder,
|
|
type,
|
|
media_type: "text",
|
|
asset_hash: "",
|
|
asset_api_id: "",
|
|
asset_api_required: false,
|
|
resolution: "view",
|
|
unsupported_reason: "",
|
|
is_asset_backed: false,
|
|
content: "",
|
|
text_truncated: false,
|
|
viewParams: {
|
|
filename,
|
|
type,
|
|
...(subfolder ? { subfolder } : {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function normalizeComfyOutputRef(imageRef = {}, mediaType = "images") {
|
|
let outputRef = imageRef;
|
|
const resolvedMediaType = resolveMediaType(outputRef, mediaType);
|
|
|
|
if (!outputRef || typeof outputRef !== "object") {
|
|
if (resolvedMediaType === "text") {
|
|
return normalizeTextOutputRef(outputRef);
|
|
}
|
|
if (resolvedMediaType === "3d" && typeof outputRef === "string" && has3dExtension(outputRef)) {
|
|
outputRef = { filename: outputRef, type: "output", subfolder: "" };
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const finalMediaType = resolveMediaType(outputRef, resolvedMediaType);
|
|
const textContent = typeof outputRef.content === "string" && outputRef.content
|
|
? outputRef.content
|
|
: (typeof outputRef.text === "string" && outputRef.text ? outputRef.text : "");
|
|
if (finalMediaType === "text" && textContent) {
|
|
return normalizeTextOutputRef(textContent);
|
|
}
|
|
|
|
const assetHash = pickAssetHash(outputRef);
|
|
const assetApiId = pickAssetApiId(outputRef);
|
|
const namedFilename = pickFilename(outputRef);
|
|
const filename = namedFilename || assetHash || assetApiId;
|
|
const subfolder = typeof outputRef.subfolder === "string" ? outputRef.subfolder : "";
|
|
const type = typeof outputRef.type === "string" && outputRef.type ? outputRef.type : "output";
|
|
|
|
if (!filename) {
|
|
return null;
|
|
}
|
|
|
|
const explicitAssetApiRequired = outputRef.asset_api_required === true;
|
|
const assetApiRequired = Boolean(explicitAssetApiRequired || (assetApiId && !assetHash && !namedFilename));
|
|
|
|
// IMPORTANT: optional asset-hash metadata still resolves through /view when
|
|
// hosts provide it; do not promote asset-api-only identifiers into implicit
|
|
// /api/assets fetches.
|
|
const viewParams = assetApiRequired
|
|
? null
|
|
: (
|
|
assetHash
|
|
? { filename: assetHash }
|
|
: {
|
|
filename,
|
|
type,
|
|
...(subfolder ? { subfolder } : {}),
|
|
}
|
|
);
|
|
|
|
return {
|
|
filename,
|
|
subfolder,
|
|
type,
|
|
media_type: finalMediaType,
|
|
asset_hash: assetHash || "",
|
|
asset_api_id: assetApiId || "",
|
|
asset_api_required: assetApiRequired,
|
|
resolution: assetApiRequired ? "asset_api_required" : "view",
|
|
unsupported_reason: assetApiRequired ? "asset_api_required" : "",
|
|
is_asset_backed: Boolean(assetHash || assetApiId),
|
|
content: "",
|
|
text_truncated: false,
|
|
viewParams,
|
|
};
|
|
}
|
|
|
|
export function extractHistoryOutputRefs(historyItem = {}) {
|
|
const results = [];
|
|
const outputs = historyItem && typeof historyItem === "object" ? (historyItem.outputs || {}) : {};
|
|
|
|
for (const nodeOutput of Object.values(outputs)) {
|
|
if (!nodeOutput || typeof nodeOutput !== "object") {
|
|
continue;
|
|
}
|
|
for (const [mediaType, refs] of Object.entries(nodeOutput)) {
|
|
if (!PREVIEWABLE_MEDIA_TYPES.has(mediaType) || !Array.isArray(refs)) {
|
|
continue;
|
|
}
|
|
for (const imageRef of refs) {
|
|
const normalized = normalizeComfyOutputRef(imageRef, mediaType);
|
|
if (normalized) {
|
|
results.push(normalized);
|
|
}
|
|
}
|
|
}
|
|
|
|
const advanced3dRef = normalizeAdvanced3dResult(nodeOutput.result);
|
|
if (advanced3dRef) {
|
|
results.push(advanced3dRef);
|
|
}
|
|
|
|
const fileRefs = nodeOutput.files;
|
|
if (Array.isArray(fileRefs) && fileRefs.length <= FILE_OUTPUT_MAX_REFS) {
|
|
for (const fileRef of fileRefs) {
|
|
const normalized = normalizeFileTextOutputRef(fileRef);
|
|
if (normalized) {
|
|
results.push(normalized);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
export function extractHistoryImageRefs(historyItem = {}) {
|
|
return extractHistoryOutputRefs(historyItem).filter((ref) => ref.media_type === "images");
|
|
}
|