591 lines
19 KiB
TypeScript
591 lines
19 KiB
TypeScript
/**
|
|
* API Client - Safe fetch wrapper (never throws)
|
|
*/
|
|
|
|
import { SETTINGS_KEY } from "../app/settingsStore.js";
|
|
import {
|
|
buildWorkflowContentURL,
|
|
buildWorkflowDiffURL,
|
|
buildWorkflowValidateURL,
|
|
buildWorkflowVersionsURL,
|
|
ENDPOINTS,
|
|
} from "./endpoints.js";
|
|
import { createApiFetchClient } from "./fetchUtils.js";
|
|
import { normalizeAssetId, pickRootId } from "../utils/ids.js";
|
|
import { createTTLCache } from "../utils/ttlCache.js";
|
|
import {
|
|
readAuthToken as _readAuthToken,
|
|
ensureWriteAuthToken,
|
|
normalizeWriteAuthFailure as _normalizeWriteAuthFailure,
|
|
invalidateAuthTokenCache,
|
|
hasRuntimeSecurityToken,
|
|
setRuntimeSecurityToken,
|
|
} from "./clientAuth.js";
|
|
|
|
/**
|
|
* @template T
|
|
* @typedef {{
|
|
* ok: boolean,
|
|
* data: (T|null),
|
|
* error?: (string|null),
|
|
* code?: string,
|
|
* meta?: any,
|
|
* status?: number
|
|
* }} ApiResult
|
|
*/
|
|
|
|
/**
|
|
* Compact viewer-oriented media info returned by `/mjr/am/viewer/info`.
|
|
*
|
|
* @typedef {{
|
|
* kind?: ('image'|'video'|'audio'|'model3d'|'unknown'),
|
|
* mime?: (string|null),
|
|
* width?: (number|null),
|
|
* height?: (number|null),
|
|
* fps?: (number|string|null),
|
|
* fps_raw?: (string|null),
|
|
* frame_count?: (number|null),
|
|
* duration_s?: (number|null),
|
|
* loader?: (string|null),
|
|
* previewable?: (boolean|null),
|
|
* interactive?: (boolean|null),
|
|
* resource_endpoint?: (string|null)
|
|
* }} ViewerInfo
|
|
*/
|
|
|
|
export { setRuntimeSecurityToken, ensureWriteAuthToken, hasRuntimeSecurityToken };
|
|
|
|
const TAGS_CACHE_TTL_MS = 30_000;
|
|
const DEFAULT_TAGS_CACHE_TTL_MS = TAGS_CACHE_TTL_MS;
|
|
const CLIENT_GLOBAL_KEY = "__MJR_API_CLIENT__";
|
|
const SETTINGS_FAST_CACHE_TTL_MS = 2000;
|
|
const MAX_BATCH_ASSET_IDS = 200;
|
|
export const VECTOR_BACKFILL_DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
export const VECTOR_BACKFILL_DEFAULT_POLL_TIMEOUT_MS = 30 * 60_000;
|
|
export const VECTOR_BACKFILL_MAX_POLL_TIMEOUT_MS = 12 * 60 * 60_000;
|
|
const SETTINGS_CACHE_KEY = "settings";
|
|
const TAGS_CACHE_KEY = "available-tags";
|
|
const _obsCache = createTTLCache({ ttlMs: SETTINGS_FAST_CACHE_TTL_MS, maxSize: 1 });
|
|
const _rtSyncCache = createTTLCache({ ttlMs: SETTINGS_FAST_CACHE_TTL_MS, maxSize: 1 });
|
|
const _tagsCache = createTTLCache({ ttlMs: () => _getTagsCacheTTL(), maxSize: 1 });
|
|
|
|
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
|
|
|
|
function _coerceBool(value: any, fallback = false) {
|
|
if (typeof value === "boolean") return value;
|
|
if (typeof value === "number") return value !== 0;
|
|
if (typeof value === "string") {
|
|
const s = value.trim().toLowerCase();
|
|
if (TRUE_VALUES.has(s)) return true;
|
|
if (FALSE_VALUES.has(s)) return false;
|
|
}
|
|
return Boolean(fallback);
|
|
}
|
|
|
|
function _getTagsCacheTTL() {
|
|
try {
|
|
const raw = localStorage?.getItem?.(SETTINGS_KEY) || "{}";
|
|
const parsed = JSON.parse(raw);
|
|
const ttl =
|
|
parsed?.cache?.tagsTTLms ??
|
|
parsed?.cache?.tagsTTL ??
|
|
parsed?.cache?.tags_ttl_ms ??
|
|
null;
|
|
const n = Number(ttl);
|
|
if (!Number.isFinite(n)) return DEFAULT_TAGS_CACHE_TTL_MS;
|
|
return Math.max(1_000, Math.min(10 * 60_000, Math.floor(n)));
|
|
} catch {
|
|
return DEFAULT_TAGS_CACHE_TTL_MS;
|
|
}
|
|
}
|
|
|
|
function invalidateObsCache() {
|
|
_obsCache.clear();
|
|
}
|
|
|
|
function invalidateRatingTagsSyncCache() {
|
|
_rtSyncCache.clear();
|
|
}
|
|
|
|
function invalidateTagsCache() {
|
|
_tagsCache.clear();
|
|
}
|
|
|
|
function _normalizeTagCacheKey(raw: any) {
|
|
const value = String(raw ?? "")
|
|
.trim()
|
|
.toLowerCase();
|
|
return value || "";
|
|
}
|
|
|
|
function _dedupeTagList(tags: any) {
|
|
const next = [];
|
|
const seen = new Set();
|
|
for (const raw of Array.isArray(tags) ? tags : []) {
|
|
const value = String(raw ?? "").trim();
|
|
if (!value) continue;
|
|
const key = _normalizeTagCacheKey(value);
|
|
if (!key || seen.has(key)) continue;
|
|
seen.add(key);
|
|
next.push(value);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
// Best-effort cache invalidation when settings change (ComfyUI settings, dev tools, etc.).
|
|
try {
|
|
const w = typeof window !== "undefined" ? window : null;
|
|
if (w && !(w as any)[CLIENT_GLOBAL_KEY]) {
|
|
(w as any)[CLIENT_GLOBAL_KEY] = { initialized: true };
|
|
|
|
w.addEventListener?.("storage", (event) => {
|
|
try {
|
|
if (event?.key === SETTINGS_KEY) {
|
|
invalidateObsCache();
|
|
invalidateRatingTagsSyncCache();
|
|
invalidateTagsCache();
|
|
invalidateAuthTokenCache();
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
});
|
|
|
|
w.addEventListener?.("mjr-settings-changed", () => {
|
|
invalidateObsCache();
|
|
invalidateRatingTagsSyncCache();
|
|
invalidateTagsCache();
|
|
invalidateAuthTokenCache();
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
const _readObsEnabled = () => {
|
|
const cached = _obsCache.get(SETTINGS_CACHE_KEY);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
const now = Date.now();
|
|
try {
|
|
const raw = localStorage?.getItem?.(SETTINGS_KEY);
|
|
if (!raw) {
|
|
_obsCache.set(SETTINGS_CACHE_KEY, false, { at: now });
|
|
return false;
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
const value = !!parsed?.observability?.enabled;
|
|
_obsCache.set(SETTINGS_CACHE_KEY, value, { at: now });
|
|
return value;
|
|
} catch {
|
|
_obsCache.set(SETTINGS_CACHE_KEY, false, { at: now });
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const _readRatingTagsSyncEnabled = () => {
|
|
const cached = _rtSyncCache.get(SETTINGS_CACHE_KEY);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
const now = Date.now();
|
|
try {
|
|
const raw = localStorage?.getItem?.(SETTINGS_KEY);
|
|
if (!raw) {
|
|
_rtSyncCache.set(SETTINGS_CACHE_KEY, true, { at: now });
|
|
return true;
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
const configured = parsed?.ratingTagsSync?.enabled;
|
|
const value =
|
|
configured === undefined || configured === null ? true : _coerceBool(configured, true);
|
|
_rtSyncCache.set(SETTINGS_CACHE_KEY, value, { at: now });
|
|
return value;
|
|
} catch {
|
|
_rtSyncCache.set(SETTINGS_CACHE_KEY, true, { at: now });
|
|
return true;
|
|
}
|
|
};
|
|
|
|
const _apiFetchClient = createApiFetchClient({
|
|
readObsEnabled: _readObsEnabled as () => boolean,
|
|
readAuthToken: _readAuthToken as () => string,
|
|
ensureWriteAuthToken: ensureWriteAuthToken as () => Promise<string>,
|
|
normalizeWriteAuthFailure: _normalizeWriteAuthFailure,
|
|
});
|
|
const fetchAPI = _apiFetchClient.fetchAPI;
|
|
|
|
export async function get(url: string, options: Record<string, any> = {}) {
|
|
return _apiFetchClient.get(url, options);
|
|
}
|
|
|
|
export async function post(url: string, body: unknown, options: Record<string, any> = {}) {
|
|
return _apiFetchClient.post(url, body, options);
|
|
}
|
|
|
|
/**
|
|
* Update asset rating (0-5 stars)
|
|
*/
|
|
export async function updateAssetRating(assetId: any, rating: any, options: Record<string, any> = {}) {
|
|
const enabled = _readRatingTagsSyncEnabled();
|
|
const asset = assetId && typeof assetId === "object" ? assetId : null;
|
|
const resolvedId = asset ? asset.id : assetId;
|
|
const normalizedId = normalizeAssetId(resolvedId);
|
|
const payload: Record<string, any> = {
|
|
rating: Math.max(0, Math.min(5, Number(rating) || 0)),
|
|
};
|
|
if (normalizedId) {
|
|
payload.asset_id = normalizedId;
|
|
} else if (asset) {
|
|
payload.filepath = asset.filepath || asset.path || asset?.file_info?.filepath || "";
|
|
payload.type = asset.type || "output";
|
|
payload.root_id = pickRootId(asset);
|
|
}
|
|
return fetchAPI("/mjr/am/asset/rating", {
|
|
...options,
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(enabled ? { "X-MJR-RTSYNC": "on" } : {}),
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update asset tags
|
|
*/
|
|
export async function updateAssetTags(assetId: any, tags: any, options: Record<string, any> = {}) {
|
|
const enabled = _readRatingTagsSyncEnabled();
|
|
const asset = assetId && typeof assetId === "object" ? assetId : null;
|
|
const resolvedId = asset ? asset.id : assetId;
|
|
const normalizedId = normalizeAssetId(resolvedId);
|
|
const isWorkflowAsset = String(asset?.kind || asset?.type || "").trim().toLowerCase() === "workflow";
|
|
const workflowFilepath = String(asset?.filepath || asset?.path || asset?.file_info?.filepath || "").trim();
|
|
const payload: Record<string, any> = {
|
|
tags: Array.isArray(tags) ? tags : [],
|
|
};
|
|
if (isWorkflowAsset && workflowFilepath) {
|
|
payload.filepath = workflowFilepath;
|
|
} else if (normalizedId) {
|
|
payload.asset_id = normalizedId;
|
|
} else if (asset) {
|
|
payload.filepath = asset.filepath || asset.path || asset?.file_info?.filepath || "";
|
|
payload.type = asset.type || "output";
|
|
payload.root_id = pickRootId(asset);
|
|
}
|
|
const endpoint = isWorkflowAsset && workflowFilepath ? ENDPOINTS.WORKFLOWS_TAGS : "/mjr/am/asset/tags";
|
|
const result = await fetchAPI(endpoint, {
|
|
...options,
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(enabled ? { "X-MJR-RTSYNC": "on" } : {}),
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
if (result?.ok) {
|
|
invalidateTagsCache();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get all available tags from the database
|
|
*/
|
|
export async function getAvailableTags() {
|
|
const cachedTags = _tagsCache.get(TAGS_CACHE_KEY);
|
|
if (Array.isArray(cachedTags)) {
|
|
return { ok: true, data: cachedTags, error: null, code: "OK", meta: { cached: true } };
|
|
}
|
|
|
|
const result = await get("/mjr/am/tags");
|
|
if (result?.ok && Array.isArray(result.data)) {
|
|
const deduped = _dedupeTagList(result.data);
|
|
_tagsCache.set(TAGS_CACHE_KEY, deduped);
|
|
return { ...result, data: deduped };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get full asset metadata by ID
|
|
*/
|
|
export async function getAssetMetadata(assetId: any, options: Record<string, any> = {}) {
|
|
const id = encodeURIComponent(normalizeAssetId(assetId));
|
|
return get(`/mjr/am/asset/${id}`, {
|
|
...options,
|
|
dedupeKey: options?.dedupeKey || `meta:${id}`,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get compact viewer media info by asset ID (fps/frame count/dimensions/etc).
|
|
*/
|
|
/** @returns {Promise<ApiResult<ViewerInfo>>} */
|
|
export async function getViewerInfo(assetId: any, options: Record<string, any> = {}) {
|
|
const id = normalizeAssetId(assetId);
|
|
if (!id) return { ok: false, data: null, error: "Missing assetId", code: "INVALID_INPUT" };
|
|
let url = `/mjr/am/viewer/info?asset_id=${encodeURIComponent(id)}`;
|
|
if (options.refresh) url += "&refresh=1";
|
|
const { refresh: _ignored, ...fetchOpts } = options;
|
|
return get(url, fetchOpts);
|
|
}
|
|
|
|
/**
|
|
* Batch fetch assets by ID (no per-asset tool invocations / self-heal).
|
|
*/
|
|
export async function getAssetsBatch(assetIds: any, options: Record<string, any> = {}) {
|
|
const ids = Array.isArray(assetIds) ? assetIds : [];
|
|
const cleaned = [];
|
|
for (const id of ids) {
|
|
const n = Number(id);
|
|
if (!Number.isFinite(n)) continue;
|
|
cleaned.push(Math.trunc(n));
|
|
if (cleaned.length >= MAX_BATCH_ASSET_IDS) break;
|
|
}
|
|
if (!cleaned.length) return { ok: true, data: [], error: null, code: "OK" };
|
|
return post("/mjr/am/assets/batch", { asset_ids: cleaned }, options);
|
|
}
|
|
|
|
export async function getWorkflowContent(filepath: any, options: Record<string, any> = {}) {
|
|
const url = buildWorkflowContentURL(filepath);
|
|
if (!url) {
|
|
return { ok: false, data: null, error: "Missing workflow filepath", code: "INVALID_INPUT" };
|
|
}
|
|
return get(url, options);
|
|
}
|
|
|
|
export async function validateWorkflow(filepath: any, options: Record<string, any> = {}) {
|
|
const url = buildWorkflowValidateURL(filepath);
|
|
if (!url) {
|
|
return { ok: false, data: null, error: "Missing workflow filepath", code: "INVALID_INPUT" };
|
|
}
|
|
return get(url, options);
|
|
}
|
|
|
|
export async function listWorkflowVersions(filepath: any, options: Record<string, any> = {}) {
|
|
const url = buildWorkflowVersionsURL(filepath);
|
|
if (!url) {
|
|
return { ok: false, data: null, error: "Missing workflow filepath", code: "INVALID_INPUT" };
|
|
}
|
|
return get(url, options);
|
|
}
|
|
|
|
export async function diffWorkflow(filepath: any, version_filepath: any = "", options: Record<string, any> = {}) {
|
|
const url = buildWorkflowDiffURL(filepath, version_filepath);
|
|
if (!url) {
|
|
return { ok: false, data: null, error: "Missing workflow filepath", code: "INVALID_INPUT" };
|
|
}
|
|
return get(url, options);
|
|
}
|
|
|
|
export async function saveWorkflow(
|
|
{
|
|
workflow = null,
|
|
name = "",
|
|
category = "",
|
|
overwrite = false,
|
|
filepath = "",
|
|
task = "",
|
|
model_family = "",
|
|
provider = "",
|
|
runs_on = "",
|
|
notes = "",
|
|
}: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
return post(
|
|
ENDPOINTS.WORKFLOWS_SAVE,
|
|
{ workflow, name, category, overwrite, filepath, task, model_family, provider, runs_on, notes },
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function duplicateWorkflow(
|
|
{ filepath = "", name = "" }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
return post(ENDPOINTS.WORKFLOWS_DUPLICATE, { filepath, name }, options);
|
|
}
|
|
|
|
export async function moveWorkflow(
|
|
{ filepath = "", name = "", category = "" }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
return post(ENDPOINTS.WORKFLOWS_MOVE, { filepath, name, category }, options);
|
|
}
|
|
|
|
export async function deleteWorkflow(
|
|
{ filepath = "" }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
return post(ENDPOINTS.WORKFLOWS_DELETE, { filepath }, options);
|
|
}
|
|
|
|
export async function markWorkflowLoaded(
|
|
{ filepath = "" }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
return post(ENDPOINTS.WORKFLOWS_MARK_LOADED, { filepath }, options);
|
|
}
|
|
|
|
export async function setWorkflowFavorite(
|
|
{ filepath = "", favorite = false }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
return post(ENDPOINTS.WORKFLOWS_FAVORITE, { filepath, favorite: !!favorite }, options);
|
|
}
|
|
|
|
export async function setWorkflowInfo(
|
|
{
|
|
filepath = "",
|
|
task = "",
|
|
model_family = "",
|
|
provider = "",
|
|
runs_on = "",
|
|
notes = "",
|
|
}: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
return post(ENDPOINTS.WORKFLOWS_INFO, { filepath, task, model_family, provider, runs_on, notes }, options);
|
|
}
|
|
|
|
export async function listWorkflowThumbnailCandidates(
|
|
{ filepath = "", limit = 12 }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
const safeLimit = Math.max(1, Math.min(50, Number(limit) || 12));
|
|
const url = `${ENDPOINTS.WORKFLOWS_THUMBNAIL_CANDIDATES}?filepath=${encodeURIComponent(
|
|
String(filepath || "").trim(),
|
|
)}&limit=${encodeURIComponent(String(safeLimit))}`;
|
|
return get(url, options);
|
|
}
|
|
|
|
export async function listWorkflowModelFamilies(options: Record<string, any> = {}) {
|
|
return get(ENDPOINTS.WORKFLOWS_MODEL_FAMILIES, options);
|
|
}
|
|
|
|
export async function listWorkflows(
|
|
{ q = "*", limit = 100, offset = 0, sort = "mtime" }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
const safeLimit = Math.max(1, Math.min(500, Number(limit) || 100));
|
|
const safeOffset = Math.max(0, Number(offset) || 0);
|
|
const url = `${ENDPOINTS.LIST}?scope=workflow&q=${encodeURIComponent(
|
|
String(q || "*"),
|
|
)}&limit=${encodeURIComponent(String(safeLimit))}&offset=${encodeURIComponent(
|
|
String(safeOffset),
|
|
)}&sort=${encodeURIComponent(String(sort || "mtime"))}`;
|
|
return get(url, options);
|
|
}
|
|
|
|
export async function setWorkflowThumbnail(
|
|
{ filepath = "", source_filepath = "" }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
return post(ENDPOINTS.WORKFLOWS_THUMBNAIL_SET, { filepath, source_filepath }, options);
|
|
}
|
|
|
|
export async function hydrateAssetRatingTags(assetId: any) {
|
|
const id = normalizeAssetId(assetId);
|
|
if (!id) return { ok: false, error: "Missing assetId" };
|
|
return get(`/mjr/am/asset/${encodeURIComponent(id)}?hydrate=rating_tags`);
|
|
}
|
|
|
|
/**
|
|
* Get metadata for a file reference (preferred over absolute paths).
|
|
* Works on /view URLs where ComfyUI provides type/filename/subfolder.
|
|
*/
|
|
export async function getFileMetadataScoped(
|
|
{
|
|
type = "output",
|
|
filename = "",
|
|
subfolder = "",
|
|
root_id = "",
|
|
rootId = "",
|
|
filepath = "",
|
|
} = {},
|
|
options = {},
|
|
) {
|
|
const t =
|
|
String(type || "output")
|
|
.trim()
|
|
.toLowerCase() || "output";
|
|
const fn = String(filename || "").trim();
|
|
const sub = String(subfolder || "").trim();
|
|
const rid = String(root_id || rootId || "").trim();
|
|
const fp = String(filepath || "").trim();
|
|
if (!fn) return { ok: false, data: null, error: "Missing filename", code: "INVALID_INPUT" };
|
|
let url = `/mjr/am/metadata?type=${encodeURIComponent(t)}&filename=${encodeURIComponent(fn)}`;
|
|
if (fp) url += `&filepath=${encodeURIComponent(fp)}`;
|
|
if (sub) url += `&subfolder=${encodeURIComponent(sub)}`;
|
|
if (rid) url += `&root_id=${encodeURIComponent(rid)}`;
|
|
return get(url, options);
|
|
}
|
|
|
|
export async function getFolderInfo(
|
|
{ filepath = "", root_id = "", subfolder = "" }: Record<string, any> = {},
|
|
options: Record<string, any> = {},
|
|
) {
|
|
try {
|
|
if ((globalThis as any).__mjrFolderInfoSupported === false) {
|
|
return {
|
|
ok: false,
|
|
data: null,
|
|
error: "Folder info endpoint unavailable",
|
|
code: "UNAVAILABLE",
|
|
};
|
|
}
|
|
if ((globalThis as any).__mjrFolderInfoSupported == null) {
|
|
const rr = await get("/mjr/am/routes");
|
|
if (rr?.ok && Array.isArray(rr.data)) {
|
|
const hasRoute = rr.data.some(
|
|
(r: any) => String(r?.path || "").trim() === "/mjr/am/folder-info",
|
|
);
|
|
(globalThis as any).__mjrFolderInfoSupported = !!hasRoute;
|
|
if (!hasRoute) {
|
|
return {
|
|
ok: false,
|
|
data: null,
|
|
error: "Folder info endpoint unavailable",
|
|
code: "UNAVAILABLE",
|
|
};
|
|
}
|
|
} else {
|
|
// Soft-fail: keep null so future calls can retry route discovery.
|
|
(globalThis as any).__mjrFolderInfoSupported = null;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
const fp = String(filepath || "").trim();
|
|
const rid = String(root_id || "").trim();
|
|
const sub = String(subfolder || "").trim();
|
|
let url = ENDPOINTS.FOLDER_INFO;
|
|
const params = [];
|
|
if (fp) {
|
|
params.push(`filepath=${encodeURIComponent(fp)}`);
|
|
params.push("browser_mode=1");
|
|
} else {
|
|
if (rid) params.push(`root_id=${encodeURIComponent(rid)}`);
|
|
if (sub) params.push(`subfolder=${encodeURIComponent(sub)}`);
|
|
}
|
|
if (params.length) url += `?${params.join("&")}`;
|
|
const res = await get(url, options);
|
|
try {
|
|
if (!res?.ok && Number(res?.status || 0) === 404) {
|
|
(globalThis as any).__mjrFolderInfoSupported = false;
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
export * from "./clientOps.js";
|