Files

693 lines
25 KiB
TypeScript

/**
* API Endpoints - Centralized URL constants
*/
import { normalizeAssetId, pickRootId } from "../utils/ids.js";
import { hasWindowsDrivePrefix, splitFilenameAndSubfolder, toPosixPath } from "../utils/path.js";
const DEFAULT_QUERY_LIMIT = 200;
const DEFAULT_QUERY_OFFSET = 0;
export interface AssetFilterParams {
subfolder?: string | null;
kind?: string | null;
hasWorkflow?: boolean | null;
minRating?: number | null;
minSizeMB?: number | null;
maxSizeMB?: number | null;
minWidth?: number | null;
minHeight?: number | null;
maxWidth?: number | null;
maxHeight?: number | null;
workflowType?: string | null;
workflowId?: string | null;
workflowModel?: string | null;
runsOn?: string | null;
dateRange?: string | null;
dateExact?: string | null;
}
export interface ListURLParams extends AssetFilterParams {
q?: string;
limit?: number;
offset?: number;
scope?: string;
customRootId?: string | null;
resolutionCompare?: string | null;
sort?: string | null;
cursor?: string | null;
includeTotal?: boolean;
groupStacks?: boolean;
metadataMode?: string | null;
}
export const ENDPOINTS = {
// Health & Config
HEALTH: "/mjr/am/health",
HEALTH_COUNTERS: "/mjr/am/health/counters",
HEALTH_DB: "/mjr/am/health/db",
STATUS: "/mjr/am/status",
RUNTIME_EXECUTION: "/mjr/am/runtime/execution",
CONFIG: "/mjr/am/config",
VERSION: "/mjr/am/version",
// Index & Search
SCAN: "/mjr/am/scan",
INDEX_FILES: "/mjr/am/index-files",
INDEX_RESET: "/mjr/am/index/reset",
DB_RESET: "/mjr/am/db/reset",
SEARCH: "/mjr/am/search",
LIST: "/mjr/am/list",
ROOTS: "/mjr/am/roots",
CUSTOM_ROOTS: "/mjr/am/custom-roots",
CUSTOM_ROOTS_REMOVE: "/mjr/am/custom-roots/remove",
BROWSER_FOLDER_OP: "/mjr/am/browser/folder-op",
BROWSE_FOLDER: "/mjr/sys/browse-folder",
FOLDER_INFO: "/mjr/am/folder-info",
// Metadata
METADATA: "/mjr/am/metadata",
METADATA_CATALOG: "/mjr/am/metadata/catalog",
METADATA_KEYS: "/mjr/am/metadata/keys",
METADATA_PARSER_VERSION: "/mjr/am/metadata/parser-version",
WORKFLOW_QUICK: "/mjr/am/workflow-quick",
RETRY_SERVICES: "/mjr/am/retry-services",
STAGE_TO_INPUT: "/mjr/am/stage-to-input",
OPEN_IN_FOLDER: "/mjr/am/open-in-folder",
COLLECT_FILES: "/mjr/am/collect-files",
TOOLS_STATUS: "/mjr/am/tools/status",
SETTINGS_OUTPUT_DIRECTORY: "/mjr/am/settings/output-directory",
SETTINGS_INDEX_DIRECTORY: "/mjr/am/settings/index-directory",
SETTINGS_WORKFLOW_ROOTS: "/mjr/am/settings/workflow-roots",
SETTINGS_METADATA_FALLBACK: "/mjr/am/settings/metadata-fallback",
SETTINGS_VECTOR_SEARCH: "/mjr/am/settings/vector-search",
SETTINGS_VECTOR_SEARCH_UNLOAD: "/mjr/am/settings/vector-search/unload",
SETTINGS_EXECUTION_GROUPING: "/mjr/am/settings/execution-grouping",
SETTINGS_HUGGINGFACE: "/mjr/am/settings/huggingface",
SETTINGS_AI_LOGGING: "/mjr/am/settings/ai-logging",
SETTINGS_ROUTE_LOGGING: "/mjr/am/settings/route-logging",
SETTINGS_STARTUP_LOGGING: "/mjr/am/settings/startup-logging",
SETTINGS_LTXAV_RGB_FALLBACK: "/mjr/am/settings/ltxav-rgb-fallback",
SETTINGS_JXL: "/mjr/am/settings/jxl",
SETTINGS_BROWSER_SHOW_FOLDERS: "/mjr/am/settings/browser-show-folders",
// View (ComfyUI native)
VIEW: "/view",
// Custom roots view (Majoor)
CUSTOM_VIEW: "/mjr/am/custom-view",
// Viewer helpers (Majoor)
VIEWER_INFO: "/mjr/am/viewer/info",
VIEWER_RESOURCE: "/mjr/am/viewer/resource",
THUMBNAIL: "/mjr/am/thumbnail",
// File upload
UPLOAD_INPUT: "/mjr/am/upload_input",
// File download
DOWNLOAD: "/mjr/am/download",
DOWNLOAD_CLEAN: "/mjr/am/download-clean",
// Drag-out (OS) helpers
BATCH_ZIP_CREATE: "/mjr/am/batch-zip",
// Calendar helpers
DATE_HISTOGRAM: "/mjr/am/date-histogram",
// Asset operations
ASSET_DELETE: "/mjr/am/asset/delete",
ASSET_RENAME: "/mjr/am/asset/rename",
// Watcher
WATCHER_STATUS: "/mjr/am/watcher/status",
WATCHER_TOGGLE: "/mjr/am/watcher/toggle",
WATCHER_SCOPE: "/mjr/am/watcher/scope",
WATCHER_SETTINGS: "/mjr/am/watcher/settings",
// Collections
COLLECTIONS: "/mjr/am/collections",
// Stacks
STACKS: "/mjr/am/stacks",
STACKS_MERGE: "/mjr/am/stacks/merge",
STACKS_DISSOLVE: "/mjr/am/stacks/dissolve",
STACKS_AUTO_STACK: "/mjr/am/stacks/auto-stack",
// Duplicate detection
DUPLICATES_ALERTS: "/mjr/am/duplicates/alerts",
DUPLICATES_ANALYZE: "/mjr/am/duplicates/analyze",
DUPLICATES_MERGE_TAGS: "/mjr/am/duplicates/merge-tags",
// DB backup/restore
DB_BACKUPS: "/mjr/am/db/backups",
DB_BACKUP_SAVE: "/mjr/am/db/backup-save",
DB_BACKUP_RESTORE: "/mjr/am/db/backup-restore",
// Vector / Semantic search
VECTOR_SEARCH: "/mjr/am/vector/search",
VECTOR_SIMILAR: "/mjr/am/vector/similar",
VECTOR_ALIGNMENT: "/mjr/am/vector/alignment",
VECTOR_AUTO_TAGS: "/mjr/am/vector/auto-tags",
VECTOR_CAPTION: "/mjr/am/vector/caption",
// Legacy alias kept for backward compatibility.
VECTOR_ENHANCED_PROMPT: "/mjr/am/vector/enhanced-prompt",
VECTOR_INDEX: "/mjr/am/vector/index",
VECTOR_STATS: "/mjr/am/vector/stats",
VECTOR_SUGGEST_COLLECTIONS: "/mjr/am/vector/suggest-collections",
VECTOR_BACKFILL: "/mjr/am/db/backfill-missing-vectors",
VECTOR_BACKFILL_STATUS: "/mjr/am/db/backfill-missing-vectors/status",
// Hybrid search (FTS + semantic)
HYBRID_SEARCH: "/mjr/am/search/hybrid",
// Library audit
AUDIT: "/mjr/am/audit",
// Workflow library
WORKFLOWS_CONTENT: "/mjr/am/workflows/content",
WORKFLOWS_VALIDATE: "/mjr/am/workflows/validate",
WORKFLOWS_VERSIONS: "/mjr/am/workflows/versions",
WORKFLOWS_DIFF: "/mjr/am/workflows/diff",
WORKFLOWS_SAVE: "/mjr/am/workflows/save",
WORKFLOWS_DUPLICATE: "/mjr/am/workflows/duplicate",
WORKFLOWS_MOVE: "/mjr/am/workflows/move",
WORKFLOWS_DELETE: "/mjr/am/workflows/delete",
WORKFLOWS_MARK_LOADED: "/mjr/am/workflows/mark-loaded",
WORKFLOWS_FAVORITE: "/mjr/am/workflows/favorite",
WORKFLOWS_INFO: "/mjr/am/workflows/info",
WORKFLOWS_THUMBNAIL_CANDIDATES: "/mjr/am/workflows/thumbnail-candidates",
WORKFLOWS_MODEL_FAMILIES: "/mjr/am/workflows/model-families",
WORKFLOWS_TAGS: "/mjr/am/workflows/tags",
WORKFLOWS_THUMBNAIL_SET: "/mjr/am/workflows/thumbnail/set",
WORKFLOWS_OPEN_ROOT: "/mjr/am/workflows/open-root",
};
export function appendAssetFilterQueryParams(url: string, filters: AssetFilterParams = {}): string {
let nextUrl = String(url || "");
// Use correct separator based on whether URL already has query params
const _sep = () => {
const s = nextUrl.includes("?") ? "&" : "?";
return s;
};
const _append = (param: any) => {
nextUrl += _sep() + param;
};
const {
subfolder = null,
kind = null,
hasWorkflow = null,
minRating = null,
minSizeMB = null,
maxSizeMB = null,
minWidth = null,
minHeight = null,
maxWidth = null,
maxHeight = null,
workflowType = null,
workflowId = null,
workflowModel = null,
runsOn = null,
dateRange = null,
dateExact = null,
} = filters || {};
if (subfolder) {
_append(`subfolder=${encodeURIComponent(String(subfolder))}`);
}
if (kind) {
_append(`kind=${encodeURIComponent(kind)}`);
}
if (hasWorkflow !== null && hasWorkflow !== undefined) {
_append(`has_workflow=${encodeURIComponent(hasWorkflow ? "true" : "false")}`);
}
if (minRating !== null && minRating !== undefined && Number(minRating) > 0) {
_append(`min_rating=${encodeURIComponent(String(minRating))}`);
}
if (minSizeMB !== null && minSizeMB !== undefined && Number(minSizeMB) > 0) {
_append(`min_size_mb=${encodeURIComponent(String(minSizeMB))}`);
}
if (maxSizeMB !== null && maxSizeMB !== undefined && Number(maxSizeMB) > 0) {
_append(`max_size_mb=${encodeURIComponent(String(maxSizeMB))}`);
}
if (minWidth !== null && minWidth !== undefined && Number(minWidth) > 0) {
_append(`min_width=${encodeURIComponent(String(minWidth))}`);
}
if (minHeight !== null && minHeight !== undefined && Number(minHeight) > 0) {
_append(`min_height=${encodeURIComponent(String(minHeight))}`);
}
if (maxWidth !== null && maxWidth !== undefined && Number(maxWidth) > 0) {
_append(`max_width=${encodeURIComponent(String(maxWidth))}`);
}
if (maxHeight !== null && maxHeight !== undefined && Number(maxHeight) > 0) {
_append(`max_height=${encodeURIComponent(String(maxHeight))}`);
}
if (workflowType) {
_append(`workflow_type=${encodeURIComponent(String(workflowType))}`);
}
if (workflowId) {
_append(`workflow_id=${encodeURIComponent(String(workflowId))}`);
}
if (workflowModel) {
_append(`workflow_model=${encodeURIComponent(String(workflowModel))}`);
}
if (runsOn) {
_append(`runs_on=${encodeURIComponent(String(runsOn))}`);
}
if (dateRange) {
_append(`date_range=${encodeURIComponent(String(dateRange))}`);
}
if (dateExact) {
_append(`date_exact=${encodeURIComponent(String(dateExact))}`);
}
return nextUrl;
}
/**
* Build view URL for asset
*/
export function buildViewURL(filename: any, subfolder: string | null = null, type = "output"): string {
const fn = String(filename || "").trim();
if (!fn) return "";
let url = `${ENDPOINTS.VIEW}?filename=${encodeURIComponent(fn)}`;
if (subfolder) {
url += `&subfolder=${encodeURIComponent(subfolder)}`;
}
url += `&type=${encodeURIComponent(type)}`;
return url;
}
export function buildThumbnailURL(asset: any, size = 384): string {
const filepath = String(asset?.filepath || asset?.path || asset?.file_path || "").trim();
if (!filepath) return "";
const requestedSize = Math.max(64, Math.min(1024, Math.floor(Number(size) || 384)));
return `${ENDPOINTS.THUMBNAIL}?filepath=${encodeURIComponent(filepath)}&size=${encodeURIComponent(String(requestedSize))}`;
}
/**
* Build list URL with scope support.
*/
export function buildListURL(params: ListURLParams = {}): string {
const {
q = "*",
limit = DEFAULT_QUERY_LIMIT,
offset = DEFAULT_QUERY_OFFSET,
scope = "output",
subfolder = "",
customRootId = null,
kind = null,
hasWorkflow = null,
minRating = null,
minSizeMB = null,
maxSizeMB = null,
resolutionCompare = null,
minWidth = null,
minHeight = null,
maxWidth = null,
maxHeight = null,
workflowType = null,
workflowId = null,
workflowModel = null,
runsOn = null,
dateRange = null,
dateExact = null,
sort = null,
cursor = null,
includeTotal = true,
groupStacks = false,
metadataMode = null,
} = params;
let url = `${ENDPOINTS.LIST}?q=${encodeURIComponent(q)}&limit=${limit}&offset=${offset}&scope=${encodeURIComponent(scope)}`;
if (subfolder) {
url += `&subfolder=${encodeURIComponent(subfolder)}`;
}
if (customRootId) {
url += `&custom_root_id=${encodeURIComponent(customRootId)}`;
} else if (String(scope || "").toLowerCase() === "custom") {
// Browser scope without selected root uses absolute filesystem paths.
url += "&browser_mode=1";
}
url = appendAssetFilterQueryParams(url, {
kind,
hasWorkflow,
minRating,
minSizeMB,
maxSizeMB,
minWidth,
minHeight,
maxWidth,
maxHeight,
workflowType,
workflowId,
workflowModel,
runsOn,
dateRange,
dateExact,
});
if (resolutionCompare) {
url += `&resolution_compare=${encodeURIComponent(String(resolutionCompare))}`;
}
if (sort) {
url += `&sort=${encodeURIComponent(String(sort))}`;
}
if (cursor) {
url += `&cursor=${encodeURIComponent(String(cursor))}`;
}
if (includeTotal === false) {
url += `&include_total=0`;
}
if (groupStacks) {
url += "&group_stacks=1";
}
if (metadataMode) {
url += `&metadata_mode=${encodeURIComponent(String(metadataMode).toUpperCase())}`;
}
return url;
}
export function buildCustomViewURL(filename: any, subfolder = "", rootId = ""): string {
const fn = String(filename || "").trim();
const rid = String(rootId || "").trim();
if (!fn || !rid) return "";
let url = `${ENDPOINTS.CUSTOM_VIEW}?root_id=${encodeURIComponent(rid)}&filename=${encodeURIComponent(fn)}`;
if (subfolder) {
url += `&subfolder=${encodeURIComponent(subfolder)}`;
}
return url;
}
export function buildBatchZipDownloadURL(token: any): string {
return `${ENDPOINTS.BATCH_ZIP_CREATE}/${encodeURIComponent(String(token || ""))}`;
}
export function buildStacksURL({ limit = 50, offset = 0, includeTotal = true }: { limit?: number; offset?: number; includeTotal?: boolean } = {}): string {
let url = `${ENDPOINTS.STACKS}?limit=${encodeURIComponent(String(limit))}&offset=${encodeURIComponent(String(offset))}`;
if (includeTotal === false) url += "&include_total=0";
return url;
}
export function buildStackURL(stackId: any): string {
return `${ENDPOINTS.STACKS}/${encodeURIComponent(String(stackId || ""))}`;
}
export function buildStackMembersURL(stackId: any, options: { limit?: number } = {}): string {
let url = `${buildStackURL(stackId)}/members`;
const limit = Number(options?.limit || 0);
if (Number.isFinite(limit) && limit > 0) {
url += `?limit=${encodeURIComponent(String(Math.floor(limit)))}`;
}
return url;
}
export function buildStackByJobURL(jobId: any): string {
return `${ENDPOINTS.STACKS}/by-job/${encodeURIComponent(String(jobId || ""))}`;
}
export function buildNodeContextMembersURL(
sourceNodeId: any,
options: { jobId?: string; job_id?: string; latest?: boolean; limit?: number } = {},
): string {
const nodeId = String(sourceNodeId || "").trim();
let url = `${ENDPOINTS.STACKS}/by-node/${encodeURIComponent(nodeId)}/members`;
const params = [];
const jobId = String(options?.jobId || options?.job_id || "").trim();
if (jobId) params.push(`job_id=${encodeURIComponent(jobId)}`);
if (options?.latest === false) params.push("latest=0");
const limit = Number(options?.limit || 0);
if (Number.isFinite(limit) && limit > 0) {
params.push(`limit=${encodeURIComponent(String(Math.trunc(limit)))}`);
}
if (params.length) url += `?${params.join("&")}`;
return url;
}
export function buildStackCoverURL(stackId: any): string {
return `${buildStackURL(stackId)}/cover`;
}
export function buildStackRenameURL(stackId: any): string {
return `${buildStackURL(stackId)}/rename`;
}
export function buildViewerResourceURL(assetOrContext: any, relpath = ""): string {
const ctx = (assetOrContext && typeof assetOrContext === "object" ? assetOrContext : {}) as Record<string, any>;
const rel = String(relpath || "").trim();
if (!rel) return "";
const assetId = normalizeAssetId(ctx?.asset_id ?? ctx?.id ?? null);
let url = `${ENDPOINTS.VIEWER_RESOURCE}?relpath=${encodeURIComponent(rel)}`;
if (assetId) {
return `${url}&asset_id=${encodeURIComponent(assetId)}`;
}
const filepath = toPosixPath(
String(
ctx?.filepath ||
ctx?.path ||
ctx?.fullpath ||
ctx?.full_path ||
ctx?.file_info?.filepath ||
ctx?.file_info?.path ||
"",
).trim(),
);
if (filepath) {
return `${url}&filepath=${encodeURIComponent(filepath)}`;
}
const filename = String(ctx?.filename || ctx?.name || ctx?.file_info?.filename || "").trim();
if (!filename) return "";
url += `&filename=${encodeURIComponent(filename)}`;
const subfolder = String(ctx?.subfolder || ctx?.file_info?.subfolder || "").trim();
if (subfolder) url += `&subfolder=${encodeURIComponent(subfolder)}`;
const type = String(ctx?.type || ctx?.file_info?.type || "")
.trim()
.toLowerCase();
if (type && type !== "custom") {
url += `&type=${encodeURIComponent(type)}`;
}
const rootId = String(pickRootId(ctx) || ctx?.root_id || ctx?.custom_root_id || "").trim();
if (rootId) {
url += `&root_id=${encodeURIComponent(rootId)}`;
}
return url;
}
export function buildDateHistogramURL(params: AssetFilterParams & { scope?: string; customRootId?: string | null; month?: string } = {}): string {
const {
scope = "output",
customRootId = null,
month = "",
kind = null,
hasWorkflow = null,
minRating = null,
} = params;
let url = `${ENDPOINTS.DATE_HISTOGRAM}?scope=${encodeURIComponent(scope)}&month=${encodeURIComponent(String(month || ""))}`;
if (customRootId) {
url += `&custom_root_id=${encodeURIComponent(customRootId)}`;
}
return appendAssetFilterQueryParams(url, {
subfolder: params.subfolder ?? null,
kind,
hasWorkflow,
minRating,
minSizeMB: params.minSizeMB ?? null,
maxSizeMB: params.maxSizeMB ?? null,
minWidth: params.minWidth ?? null,
minHeight: params.minHeight ?? null,
maxWidth: params.maxWidth ?? null,
maxHeight: params.maxHeight ?? null,
workflowType: params.workflowType ?? null,
workflowId: params.workflowId ?? null,
workflowModel: params.workflowModel ?? null,
runsOn: params.runsOn ?? null,
dateRange: params.dateRange ?? null,
dateExact: params.dateExact ?? null,
});
}
export function buildAssetViewURL(asset: Record<string, any> | null | undefined): string {
const mtime = asset?.mtime;
const withMtime = (url: any) => {
if (!url || !mtime) return url;
return `${url}${url.includes("?") ? "&" : "?"}v=${encodeURIComponent(mtime)}`;
};
const rawPath = toPosixPath(
String(
asset?.filepath ||
asset?.path ||
asset?.fullpath ||
asset?.full_path ||
asset?.file_info?.filepath ||
asset?.file_info?.path ||
"",
).trim(),
);
const declaredType = String(asset?.type || asset?.file_info?.type || "")
.toLowerCase()
.trim();
const assetId = Number(asset?.id ?? asset?.asset_id);
const isTemporaryAsset = declaredType === "temp" || rawPath.toLowerCase().includes("/temp/");
if (!isTemporaryAsset && Number.isSafeInteger(assetId) && assetId > 0) {
return withMtime(`/mjr/am/viewer/asset/${encodeURIComponent(String(assetId))}`);
}
let filename = String(
asset?.filename || asset?.name || asset?.file_info?.filename || "",
).trim();
let subfolder = String(asset?.subfolder || asset?.file_info?.subfolder || "").trim();
const pickFromPath = (p: any) => {
const out = { type: "", subfolder: "", filename: "" };
const normalized = toPosixPath(p);
if (!normalized) return out;
const lower = normalized.toLowerCase();
const idxOut = lower.indexOf("/output/");
const idxIn = lower.indexOf("/input/");
const idxTemp = lower.indexOf("/temp/");
let baseIdx = -1;
if (idxOut >= 0) {
out.type = "output";
baseIdx = idxOut + "/output/".length;
} else if (lower.startsWith("output/")) {
out.type = "output";
baseIdx = "output/".length;
} else if (idxIn >= 0) {
out.type = "input";
baseIdx = idxIn + "/input/".length;
} else if (lower.startsWith("input/")) {
out.type = "input";
baseIdx = "input/".length;
} else if (idxTemp >= 0) {
out.type = "temp";
baseIdx = idxTemp + "/temp/".length;
} else if (lower.startsWith("temp/")) {
out.type = "temp";
baseIdx = "temp/".length;
}
if (baseIdx >= 0) {
const rel = normalized.slice(baseIdx);
const slash = rel.lastIndexOf("/");
if (slash >= 0) {
out.subfolder = rel.slice(0, slash);
out.filename = rel.slice(slash + 1);
} else {
out.filename = rel;
}
} else {
const split = splitFilenameAndSubfolder(normalized);
out.filename = split.filename;
}
return out;
};
const fromPath = pickFromPath(rawPath);
if (fromPath.filename && fromPath.type) {
filename = fromPath.filename;
subfolder = fromPath.subfolder;
}
// If filename accidentally contains a relative path, split it for /view API.
if (!subfolder && filename.includes("/")) {
const idx = filename.lastIndexOf("/");
if (idx > 0) {
subfolder = filename.slice(0, idx);
filename = filename.slice(idx + 1);
}
}
if (!filename && fromPath.filename) filename = fromPath.filename;
if (!subfolder && fromPath.subfolder) subfolder = fromPath.subfolder;
if (!filename) return "";
let type = String(asset?.type || asset?.file_info?.type || "")
.toLowerCase()
.trim();
if (type !== "input" && type !== "output" && type !== "temp" && type !== "custom") type = "";
if (!type && fromPath.type) type = fromPath.type;
if (!type && rawPath) {
if (rawPath.includes("/input/") || rawPath.startsWith("input/")) type = "input";
else if (rawPath.includes("/output/") || rawPath.startsWith("output/")) type = "output";
else if (rawPath.includes("/temp/") || rawPath.startsWith("temp/")) type = "temp";
}
if (!type) type = "output";
const hasNativeBucket =
rawPath.includes("/output/") ||
rawPath.includes("/input/") ||
rawPath.includes("/temp/") ||
rawPath.startsWith("output/") ||
rawPath.startsWith("input/") ||
rawPath.startsWith("temp/");
const subfolderLooksAbsolute = hasWindowsDrivePrefix(subfolder) || subfolder.startsWith("/");
// Fallback: non-native output/input roots (or broken absolute subfolder values)
// cannot be served by ComfyUI `/view`, so use backend filepath streaming URL.
if (rawPath && type !== "custom" && (subfolderLooksAbsolute || !hasNativeBucket)) {
return withMtime(buildDownloadURL(rawPath, { inline: true }));
}
if (type === "custom") {
const rid = String(pickRootId(asset) || "").trim();
if (rid) return withMtime(buildCustomViewURL(filename, subfolder, rid));
if (rawPath) {
return withMtime(
`${ENDPOINTS.CUSTOM_VIEW}?filepath=${encodeURIComponent(rawPath)}&browser_mode=1`,
);
}
// Fallback for malformed custom assets without root id.
const fallbackType = fromPath.type || "output";
return withMtime(buildViewURL(filename, subfolder, fallbackType));
}
// Prefer path-based type when explicit type conflicts with obvious filepath bucket.
if (rawPath.includes("/output/") || rawPath.startsWith("output/")) type = "output";
if (rawPath.includes("/input/") || rawPath.startsWith("input/")) type = "input";
if (rawPath.includes("/temp/") || rawPath.startsWith("temp/")) type = "temp";
return withMtime(buildViewURL(filename, subfolder, type));
}
/**
* Build download URL for asset
*/
export function buildDownloadURL(filepath: any, options: Record<string, any> = {}) {
if (!filepath) return "";
const inline = !!options?.inline;
let url = `${ENDPOINTS.DOWNLOAD}?filepath=${encodeURIComponent(filepath)}`;
if (inline) url += "&preview=1";
return url;
}
export function buildWorkflowContentURL(filepath: any): string {
const fp = String(filepath || "").trim();
if (!fp) return "";
return `${ENDPOINTS.WORKFLOWS_CONTENT}?filepath=${encodeURIComponent(fp)}`;
}
export function buildWorkflowValidateURL(filepath: any): string {
const fp = String(filepath || "").trim();
if (!fp) return "";
return `${ENDPOINTS.WORKFLOWS_VALIDATE}?filepath=${encodeURIComponent(fp)}`;
}
export function buildWorkflowVersionsURL(filepath: any): string {
const fp = String(filepath || "").trim();
if (!fp) return "";
return `${ENDPOINTS.WORKFLOWS_VERSIONS}?filepath=${encodeURIComponent(fp)}`;
}
export function buildWorkflowDiffURL(filepath: any, version_filepath: any = ""): string {
const fp = String(filepath || "").trim();
if (!fp) return "";
let url = `${ENDPOINTS.WORKFLOWS_DIFF}?filepath=${encodeURIComponent(fp)}`;
const version = String(version_filepath || "").trim();
if (version) url += `&version_filepath=${encodeURIComponent(version)}`;
return url;
}
/**
* Build download URL for asset with ComfyUI metadata stripped.
*/
export function buildCleanDownloadURL(filepath: any) {
if (!filepath) return "";
return `${ENDPOINTS.DOWNLOAD_CLEAN}?filepath=${encodeURIComponent(filepath)}`;
}