Co-authored-by: MajoorWaldi <129623145+MajoorWaldi@users.noreply.github.com>
693 lines
25 KiB
TypeScript
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)}`;
|
|
}
|