More hardening. File deletion logic improvements
This commit is contained in:
+1
-2
@@ -1,3 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
js/vendor/
|
||||
*.pyc
|
||||
+26
-76
@@ -60,9 +60,9 @@ def get_all_candidate_roots(subfolder="", folder_type="output"):
|
||||
return unique
|
||||
|
||||
|
||||
def resolve_existing_path(filename="", subfolder="", folder_type="output"):
|
||||
def resolve_existing_path(filename="", subfolder="", folder_type="output", candidate_roots=None):
|
||||
"""Finds the actual location of a file or folder across all candidate root directories."""
|
||||
candidates = get_all_candidate_roots(subfolder, folder_type)
|
||||
candidates = candidate_roots if candidate_roots is not None else get_all_candidate_roots(subfolder, folder_type)
|
||||
|
||||
if filename:
|
||||
for root in candidates:
|
||||
@@ -97,7 +97,7 @@ def open_file_in_os(target_path):
|
||||
# Passed as a single argument so Windows Explorer highlights the file properly
|
||||
subprocess.Popen(["explorer", f"/select,{norm_path}"])
|
||||
else:
|
||||
os.startfile(norm_path)
|
||||
subprocess.Popen(["explorer", norm_path])
|
||||
|
||||
elif system == "Darwin": # macOS
|
||||
if not os.path.exists(target_path):
|
||||
@@ -146,19 +146,34 @@ async def delete_file_handler(request):
|
||||
if not filename:
|
||||
return web.json_response({"error": "No filename provided"}, status=400)
|
||||
|
||||
target_file, root = resolve_existing_path(filename, subfolder, folder_type)
|
||||
# Strictly restrict deletion to generated output and temp files (protects models/checkpoints)
|
||||
delete_roots = [
|
||||
folder_paths.get_output_directory(),
|
||||
folder_paths.get_temp_directory()
|
||||
]
|
||||
delete_roots = [os.path.realpath(r) for r in delete_roots if r and os.path.exists(r)]
|
||||
|
||||
target_file, root = resolve_existing_path(filename, subfolder, folder_type, candidate_roots=delete_roots)
|
||||
|
||||
if not target_file or not os.path.isfile(target_file) or not is_path_safe(root, target_file):
|
||||
return web.json_response({"error": "File not found or unsafe path"}, status=404)
|
||||
return web.json_response({"error": "File not found or outside output directory"}, status=404)
|
||||
|
||||
# Try moving to Recycle Bin / Trash first, fallback to os.remove
|
||||
try:
|
||||
import send2trash
|
||||
send2trash.send2trash(target_file)
|
||||
except Exception:
|
||||
permanent = bool(data.get("permanent", False))
|
||||
action = "deleted"
|
||||
if not permanent:
|
||||
try:
|
||||
import send2trash
|
||||
send2trash.send2trash(target_file)
|
||||
action = "trashed"
|
||||
except Exception:
|
||||
os.remove(target_file)
|
||||
action = "deleted"
|
||||
else:
|
||||
os.remove(target_file)
|
||||
action = "deleted"
|
||||
|
||||
return web.json_response({"success": True, "deleted": target_file})
|
||||
return web.json_response({"success": True, "deleted": target_file, "action": action})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@@ -185,69 +200,4 @@ async def promote_output_handler(request):
|
||||
shutil.copy2(target_file, dest_path)
|
||||
return web.json_response({"name": filename})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Local Three.js Proxy & Offline Cache (Bypasses CSP & --disable-api-nodes)
|
||||
# ============================================================================
|
||||
SIDEBAR_CACHE_DIR = os.path.join(os.path.dirname(__file__), "js", "vendor", "cache")
|
||||
|
||||
@PromptServer.instance.routes.get("/comfy-sidebar/three-proxy/{path:.*}")
|
||||
async def three_proxy_handler(request):
|
||||
raw_path = request.match_info.get("path", "").lstrip("/")
|
||||
if not raw_path:
|
||||
return web.Response(status=404)
|
||||
|
||||
query = request.query_string
|
||||
full_req = f"{raw_path}?{query}" if query else raw_path
|
||||
|
||||
os.makedirs(SIDEBAR_CACHE_DIR, exist_ok=True)
|
||||
import re
|
||||
import hashlib
|
||||
import urllib.parse
|
||||
|
||||
# Hash the request to prevent Cache Poisoning collisions and OS filename length limits
|
||||
req_hash = hashlib.sha256(full_req.encode("utf-8")).hexdigest()[:16]
|
||||
prefix = re.sub(r'[^a-zA-Z0-9.-]', '_', raw_path)[:40]
|
||||
safe_name = f"{prefix}_{req_hash}"
|
||||
if not any(raw_path.endswith(ext) for ext in [".js", ".mjs", ".wasm", ".bin", ".json"]):
|
||||
safe_name += ".js"
|
||||
|
||||
cache_file = os.path.join(SIDEBAR_CACHE_DIR, safe_name)
|
||||
|
||||
# 1. Serve immediately from disk cache if present
|
||||
if os.path.isfile(cache_file) and os.path.getsize(cache_file) > 0:
|
||||
content_type = "application/wasm" if raw_path.endswith(".wasm") else "application/javascript"
|
||||
return web.FileResponse(cache_file, headers={"Content-Type": content_type})
|
||||
|
||||
# 2. Otherwise download via Python (not blocked by browser CSP)
|
||||
safe_url_path = urllib.parse.quote(full_req, safe="?&=/@+.-")
|
||||
url = f"https://esm.sh/{safe_url_path}"
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
raw_data = resp.read()
|
||||
|
||||
if raw_path.endswith((".wasm", ".bin", ".png", ".jpg")):
|
||||
with open(cache_file, "wb") as f:
|
||||
f.write(raw_data)
|
||||
return web.Response(body=raw_data, content_type="application/octet-stream")
|
||||
|
||||
content = raw_data.decode("utf-8", errors="replace")
|
||||
# Rewrite absolute esm.sh paths to route through our local 'self' proxy
|
||||
content = content.replace("https://esm.sh/", "/comfy-sidebar/three-proxy/")
|
||||
# Rewrite root-relative module paths (e.g. from "/v135/...")
|
||||
content = re.sub(
|
||||
r'((?:from|import)\s*["\'])/(v\d+|[a-zA-Z0-9@_.~-]+/)',
|
||||
r'\1/comfy-sidebar/three-proxy/\2',
|
||||
content
|
||||
)
|
||||
|
||||
with open(cache_file, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
return web.Response(text=content, content_type="application/javascript")
|
||||
except Exception as e:
|
||||
return web.Response(text=f"/* Proxy error fetching {url}: {e} */", status=502, content_type="application/javascript")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
@@ -118,7 +118,7 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
|
||||
prompt.images = nodeImgs;
|
||||
// If fullscreen is open, smoothly transition to the final saved image
|
||||
const finalImg = nodeImgs[0];
|
||||
const finalSrc = finalImg.url || (window.location.origin + `/view?filename=${encodeURIComponent(finalImg.filename)}&type=${finalImg.type || 'output'}&subfolder=${encodeURIComponent(finalImg.subfolder || '')}`);
|
||||
const finalSrc = finalImg.url || (window.location.origin + `/view?filename=${encodeURIComponent(finalImg.filename)}&type=${finalImg.type || 'output'}&subfolder=${encodeURIComponent(finalImg.subfolder || '')}&rand=${encodeURIComponent(pid)}`);
|
||||
updateActiveComparisonPreview(pid, finalSrc);
|
||||
}
|
||||
const nodeTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, prompt.workflow);
|
||||
|
||||
@@ -44,6 +44,12 @@ function registerSettings() {
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "Comfy Sidebar.Permanent Delete on Disk Deletion",
|
||||
name: "Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)",
|
||||
type: "boolean",
|
||||
defaultValue: false
|
||||
});
|
||||
|
||||
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "Templates"];
|
||||
sidebarTabs.forEach(tab => {
|
||||
|
||||
+24
-7
@@ -8,6 +8,7 @@ import { getRunOutputs, extractWorkflowFromPng, isImageFormat } from "../utils/u
|
||||
import { showFullscreenPreview } from "../utils/comparison.js";
|
||||
import { stopAllAudioPlayback } from "./audioController.js";
|
||||
import { deleteHistoryItem, cancelPendingTask, interruptActive } from "../queue/queueService.js";
|
||||
import { showNotification } from "../utils/nodeRenumber.js";
|
||||
|
||||
export const cardPool = new Map();
|
||||
|
||||
@@ -31,6 +32,8 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.removeProperty("display");
|
||||
}
|
||||
|
||||
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
||||
|
||||
// Check if the card has an actual verified file saved on disk
|
||||
const hasRealDiskFiles = state.images && state.images.length > 0 &&
|
||||
state.images.some(img => img.filename && !img.isFallback && (!img.url || !img.url.startsWith("blob:")));
|
||||
@@ -47,7 +50,7 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
ev.stopPropagation();
|
||||
state.images.forEach(img => {
|
||||
const a = document.createElement("a");
|
||||
a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
a.download = img.filename || "output";
|
||||
a.click();
|
||||
});
|
||||
@@ -64,7 +67,7 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
cardObj.btnCopy.style.display = "inline-flex";
|
||||
cardObj.btnCopy.onclick = async (ev) => {
|
||||
ev.stopPropagation();
|
||||
const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}`;
|
||||
const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}${randParam}`;
|
||||
const success = await copyImageToClipboard(src);
|
||||
if (success) {
|
||||
cardObj.btnCopy.className = "pi pi-check comfy-sidebar-card-action-btn";
|
||||
@@ -88,7 +91,7 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
ev.stopPropagation();
|
||||
let wf = state.workflow;
|
||||
if (!wf && currentImg) {
|
||||
const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}`;
|
||||
const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}${randParam}`;
|
||||
wf = await extractWorkflowFromPng(src);
|
||||
if (wf) state.workflow = wf;
|
||||
}
|
||||
@@ -161,7 +164,7 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
// Check if intermediate files still exist on disk
|
||||
for (const out of distinctOutputs) {
|
||||
for (const img of out.images) {
|
||||
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
if (!src.startsWith("blob:")) {
|
||||
fetch(src, { method: "HEAD" }).then(res => {
|
||||
if (res.status === 404) {
|
||||
@@ -197,7 +200,7 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
let anyMissing = false;
|
||||
for (const out of curDistinct) {
|
||||
for (const img of out.images) {
|
||||
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
if (!src.startsWith("blob:")) {
|
||||
try {
|
||||
const res = await fetch(src, { method: "HEAD" });
|
||||
@@ -248,7 +251,8 @@ export function updateCardProgressTargeted(cardObj, progress, activeNodeName, sh
|
||||
// Fast-path: update live preview image directly on active card
|
||||
if (state && state.images && state.images.length > 0) {
|
||||
const firstImg = state.images[0];
|
||||
const previewUrl = firstImg.url || (firstImg.filename ? `/view?filename=${encodeURIComponent(firstImg.filename)}&type=${firstImg.type || 'output'}&subfolder=${encodeURIComponent(firstImg.subfolder || '')}` : null);
|
||||
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
||||
const previewUrl = firstImg.url || (firstImg.filename ? `/view?filename=${encodeURIComponent(firstImg.filename)}&type=${firstImg.type || 'output'}&subfolder=${encodeURIComponent(firstImg.subfolder || '')}${randParam}` : null);
|
||||
if (previewUrl && cardObj.firstImgElement && cardObj.firstImgElement.tagName === "IMG") {
|
||||
const currentSrc = cardObj.firstImgElement.getAttribute("src") || cardObj.firstImgElement.src;
|
||||
if (currentSrc !== previewUrl && !cardObj.firstImgElement.src.endsWith(previewUrl)) {
|
||||
@@ -463,8 +467,21 @@ export function getOrCreateCard(state, callbacks = {}) {
|
||||
const allOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
|
||||
allOutputs.forEach(out => (out.images || []).forEach(addFile));
|
||||
|
||||
let deletedCount = 0;
|
||||
let lastAction = "trashed";
|
||||
for (const fileItem of filesToDelete) {
|
||||
await deleteFileOnServer(fileItem);
|
||||
const action = await deleteFileOnServer(fileItem);
|
||||
if (action) {
|
||||
deletedCount++;
|
||||
lastAction = action;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
const msg = lastAction === "trashed"
|
||||
? `Moved ${deletedCount} file${deletedCount > 1 ? "s" : ""} to Recycle Bin.`
|
||||
: `Permanently deleted ${deletedCount} file${deletedCount > 1 ? "s" : ""}.`;
|
||||
showNotification(msg, "success", "Delete Files");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-4
@@ -50,8 +50,11 @@ export async function openFileOrFolder(img) {
|
||||
a.remove();
|
||||
}
|
||||
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
export async function deleteFileOnServer(fileItem) {
|
||||
if (!fileItem || !fileItem.filename) return false;
|
||||
if (!fileItem || !fileItem.filename) return null;
|
||||
const isPermanent = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Permanent Delete on Disk Deletion") ?? false;
|
||||
try {
|
||||
const res = await fetch("/comfy-sidebar/delete-file", {
|
||||
method: "POST",
|
||||
@@ -59,13 +62,18 @@ export async function deleteFileOnServer(fileItem) {
|
||||
body: JSON.stringify({
|
||||
filename: fileItem.filename,
|
||||
subfolder: fileItem.subfolder || "",
|
||||
type: fileItem.type || "output"
|
||||
type: fileItem.type || "output",
|
||||
permanent: isPermanent
|
||||
})
|
||||
});
|
||||
return res.ok;
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return data.action || (isPermanent ? "deleted" : "trashed");
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error("Comfy Sidebar: Failed to delete file on server", e);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import { PromptStatus } from "../core/constants.js";
|
||||
|
||||
export function render3DCardPreview(cardObj, wrapper, src, img, state) {
|
||||
let preview3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper");
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
|
||||
const imageAssets = state?.images?.filter(i => isImageFormat(i.filename || i.url)) || [];
|
||||
const lastImageAsset = imageAssets.length > 0 ? imageAssets[imageAssets.length - 1] : null;
|
||||
@@ -92,7 +93,8 @@ export function render3DCardPreview(cardObj, wrapper, src, img, state) {
|
||||
|
||||
export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
let previewAudio = wrapper.querySelector(".comfy-sidebar-audio-wrapper");
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
const filename = img.filename || getFilenameFromUrl(src) || "audio.wav";
|
||||
const ext = filename.split('.').pop().toUpperCase();
|
||||
|
||||
@@ -277,7 +279,8 @@ export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state)
|
||||
const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
|
||||
if (playIcon) playIcon.remove();
|
||||
let previewFile = wrapper.querySelector(".comfy-sidebar-file-wrapper");
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
const filename = img.filename || getFilenameFromUrl(src) || "output_file";
|
||||
const ext = (filename.split('.').pop() || "FILE").toUpperCase();
|
||||
const folderName = img.subfolder ? `${img.subfolder}/` : (img.type || "output");
|
||||
@@ -368,7 +371,8 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : "";
|
||||
const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`;
|
||||
const isVideo = isVideoFormat(src) || isVideoFormat(img.filename);
|
||||
const is3D = is3DFormat(src) || is3DFormat(img.filename);
|
||||
const isAudio = isAudioFormat(src) || isAudioFormat(img.filename);
|
||||
|
||||
+15
-2
@@ -512,7 +512,11 @@ export function renderSidebar() {
|
||||
resetDelete();
|
||||
|
||||
if (shouldDeleteFromDisk && img && img.filename) {
|
||||
await deleteFileOnServer(img);
|
||||
const action = await deleteFileOnServer(img);
|
||||
if (action) {
|
||||
const msg = action === "trashed" ? "Moved file to Recycle Bin." : "Permanently deleted file.";
|
||||
showNotification(msg, "success", "Delete File");
|
||||
}
|
||||
}
|
||||
|
||||
const pid = batchInfo.parentPromptId || batchInfo.pid;
|
||||
@@ -779,8 +783,17 @@ export function renderSidebar() {
|
||||
resetDelete();
|
||||
|
||||
if (shouldDeleteFromDisk && out.images) {
|
||||
let count = 0;
|
||||
let lastAct = "trashed";
|
||||
for (const imgItem of out.images) {
|
||||
await deleteFileOnServer(imgItem);
|
||||
const action = await deleteFileOnServer(imgItem);
|
||||
if (action) { count++; lastAct = action; }
|
||||
}
|
||||
if (count > 0) {
|
||||
const msg = lastAct === "trashed"
|
||||
? `Moved ${count} file${count > 1 ? "s" : ""} to Recycle Bin.`
|
||||
: `Permanently deleted ${count} file${count > 1 ? "s" : ""}.`;
|
||||
showNotification(msg, "success", "Delete Files");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { syncAllNodeBadges } from "../comfy/nodes.js";
|
||||
|
||||
export function showNotification(message, type = "success") {
|
||||
const summary = type === "error" ? "Error" : type === "warn" ? "Warning" : "Node Renumber";
|
||||
export function showNotification(message, type = "success", title = null) {
|
||||
const summary = title || (type === "error" ? "Error" : type === "warn" ? "Warning" : "Comfy Sidebar");
|
||||
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
@@ -61,6 +61,13 @@ export async function renumberNodesTopologically() {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = confirm(
|
||||
"Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\n" +
|
||||
"This will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\n" +
|
||||
"Do you want to proceed?"
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
// Compute LiteGraph's topological execution order
|
||||
if (typeof app.graph.computeExecutionOrder === "function") {
|
||||
|
||||
+9
-17
@@ -36,19 +36,9 @@ async function getThreeLibs() {
|
||||
};
|
||||
|
||||
try {
|
||||
let loaded = null;
|
||||
// 1. Try local Python proxy on 'self' (bypasses CSP & --disable-api-nodes)
|
||||
try {
|
||||
loaded = await loadFromBase("/comfy-sidebar/three-proxy/");
|
||||
window.__COMFY_SIDEBAR_THREE_BASE = "/comfy-sidebar/three-proxy/";
|
||||
} catch (proxyErr) {
|
||||
// 2. Fall back to direct CDN if proxy is unreachable
|
||||
console.warn("Comfy Sidebar: Proxy unavailable, trying direct CDN...", proxyErr);
|
||||
loaded = await loadFromBase("https://esm.sh/");
|
||||
window.__COMFY_SIDEBAR_THREE_BASE = "https://esm.sh/";
|
||||
}
|
||||
|
||||
const { three, controls, gltf, obj, stl, ply } = loaded;
|
||||
const base = "https://esm.sh/";
|
||||
window.__COMFY_SIDEBAR_THREE_BASE = base;
|
||||
const { three, controls, gltf, obj, stl, ply } = await loadFromBase(base);
|
||||
cachedThreeLibs = {
|
||||
...three,
|
||||
OrbitControls: controls.OrbitControls || controls.default || controls,
|
||||
@@ -232,7 +222,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
|
||||
loadingSpinner.style.display = "flex";
|
||||
loadingSpinner.innerHTML = `<span class="pi pi-spin pi-spinner"></span> Exporting ${format}...`;
|
||||
|
||||
const base = window.__COMFY_SIDEBAR_THREE_BASE || "/comfy-sidebar/three-proxy/";
|
||||
const base = window.__COMFY_SIDEBAR_THREE_BASE || "https://esm.sh/";
|
||||
if (format === "OBJ") {
|
||||
const mod = await import(`${base}three@0.170.0/examples/jsm/exporters/OBJExporter.js`);
|
||||
const OBJExporter = mod.OBJExporter || mod.default || mod;
|
||||
@@ -283,9 +273,11 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = ()
|
||||
THREE = await getThreeLibs();
|
||||
if (!THREE) {
|
||||
loadingSpinner.innerHTML = `
|
||||
<div style="text-align: center; max-width: 320px; line-height: 1.4;">
|
||||
<span style="color: #f87171; font-weight: bold;">Failed to load 3D engine.</span><br>
|
||||
<span style="font-size: 11px; color: #94a3b8;">Blocked by ComfyUI Content Security Policy (CSP). Check if <code>--disable-api-nodes</code> is enabled in your startup arguments, or vendor Three.js locally.</span>
|
||||
<div style="text-align: center; max-width: 340px; line-height: 1.4; padding: 6px;">
|
||||
<span style="color: #f87171; font-weight: bold; font-size: 13px;">Failed to load 3D engine.</span><br>
|
||||
<span style="font-size: 11px; color: #cbd5e1; display: inline-block; margin-top: 4px;">
|
||||
Unable to download 3D libraries. Make sure your computer is online, or check if <code>--disable-api-nodes</code> is blocking external network requests.
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
|
||||
+4
-2
@@ -1,9 +1,11 @@
|
||||
[project]
|
||||
name = "comfy-sidebar"
|
||||
description = "A custom queue sidebar and image comparison extension that reintroduces and improves a sidebar queue menu and unified compact topbar layout for ComfyUI."
|
||||
version = "1.6.1"
|
||||
version = "1.6.2"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"send2trash>=1.8.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/m0rtus59/ComfyUI-ComfySidebar"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
send2trash>=1.8.0
|
||||
Reference in New Issue
Block a user