mirror of
https://git.datalinker.icu/ltdrdata/ComfyUI-Manager
synced 2025-12-09 22:24:23 +08:00
- Updated all POST endpoints to use proper Pydantic model validation: - `/v2/manager/queue/task` - validates QueueTaskItem - `/v2/manager/queue/install_model` - validates ModelMetadata - `/v2/manager/queue/reinstall` - validates InstallPackParams - `/v2/customnode/import_fail_info` - validates cnr_id/url fields - Added proper error handling with ValidationError for detailed error messages - Updated TaskQueue.put() to handle both dict and Pydantic model inputs - Added missing imports: InstallPackParams, ModelMetadata, ValidationError Benefits: - Early validation catches invalid data at API boundaries - Better error messages for clients with specific validation failures - Type safety throughout the request processing pipeline - Consistent validation behavior across all endpoints All ruff checks pass and validation is now enabled by default.
2035 lines
69 KiB
Python
2035 lines
69 KiB
Python
import traceback
|
|
|
|
import folder_paths
|
|
import locale
|
|
import subprocess # don't remove this
|
|
import concurrent
|
|
import nodes
|
|
import os
|
|
import sys
|
|
import threading
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import uuid
|
|
from datetime import datetime
|
|
import heapq
|
|
import copy
|
|
from typing import NamedTuple, List, Literal, Optional
|
|
from pydantic import ValidationError
|
|
from comfy.cli_args import args
|
|
import latent_preview
|
|
from aiohttp import web
|
|
import json
|
|
import zipfile
|
|
import urllib.request
|
|
|
|
from comfyui_manager.glob.utils import (
|
|
formatting_utils,
|
|
model_utils,
|
|
security_utils,
|
|
node_pack_utils,
|
|
environment_utils,
|
|
)
|
|
|
|
|
|
from server import PromptServer
|
|
import logging
|
|
import asyncio
|
|
|
|
from . import manager_core as core
|
|
from ..common import manager_util
|
|
from ..common import cm_global
|
|
from ..common import manager_downloader
|
|
from ..common import context
|
|
|
|
|
|
from ..data_models import (
|
|
QueueTaskItem,
|
|
TaskHistoryItem,
|
|
TaskStateMessage,
|
|
MessageTaskDone,
|
|
MessageTaskStarted,
|
|
MessageUpdate,
|
|
ManagerMessageName,
|
|
BatchExecutionRecord,
|
|
ComfyUISystemState,
|
|
BatchOperation,
|
|
InstalledNodeInfo,
|
|
InstalledModelInfo,
|
|
ComfyUIVersionInfo,
|
|
InstallPackParams,
|
|
ModelMetadata,
|
|
)
|
|
|
|
from .constants import (
|
|
model_dir_name_map,
|
|
SECURITY_MESSAGE_MIDDLE_OR_BELOW,
|
|
SECURITY_MESSAGE_NORMAL_MINUS_MODEL,
|
|
SECURITY_MESSAGE_GENERAL,
|
|
SECURITY_MESSAGE_NORMAL_MINUS,
|
|
)
|
|
|
|
# For legacy compatibility - these may need to be implemented in the new structure
|
|
temp_queue_batch = []
|
|
task_worker_lock = threading.RLock()
|
|
|
|
def finalize_temp_queue_batch():
|
|
"""Temporary compatibility function - to be implemented with new queue system"""
|
|
pass
|
|
|
|
|
|
if not manager_util.is_manager_pip_package():
|
|
network_mode_description = "offline"
|
|
else:
|
|
network_mode_description = core.get_config()["network_mode"]
|
|
logging.info("[ComfyUI-Manager] network_mode: " + network_mode_description)
|
|
|
|
comfy_ui_hash = "-"
|
|
comfyui_tag = None
|
|
|
|
MAXIMUM_HISTORY_SIZE = 10000
|
|
routes = PromptServer.instance.routes
|
|
|
|
def handle_stream(stream, prefix):
|
|
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
|
|
for msg in stream:
|
|
if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg):
|
|
if msg.startswith('100%'):
|
|
print('\r' + msg, end="", file=sys.stderr),
|
|
else:
|
|
print('\r' + msg[:-1], end="", file=sys.stderr),
|
|
else:
|
|
if prefix == '[!]':
|
|
print(prefix, msg, end="", file=sys.stderr)
|
|
else:
|
|
print(prefix, msg, end="")
|
|
|
|
def is_loopback(address):
|
|
import ipaddress
|
|
try:
|
|
return ipaddress.ip_address(address).is_loopback
|
|
except ValueError:
|
|
return False
|
|
|
|
is_local_mode = is_loopback(args.listen)
|
|
|
|
|
|
# Code quality checks have been completed
|
|
|
|
|
|
class ManagerFuncsInComfyUI(core.ManagerFuncs):
|
|
def get_current_preview_method(self):
|
|
if args.preview_method == latent_preview.LatentPreviewMethod.Auto:
|
|
return "auto"
|
|
elif args.preview_method == latent_preview.LatentPreviewMethod.Latent2RGB:
|
|
return "latent2rgb"
|
|
elif args.preview_method == latent_preview.LatentPreviewMethod.TAESD:
|
|
return "taesd"
|
|
else:
|
|
return "none"
|
|
|
|
def run_script(self, cmd, cwd="."):
|
|
if len(cmd) > 0 and cmd[0].startswith("#"):
|
|
logging.error(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
|
|
return 0
|
|
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
cwd=cwd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
bufsize=1,
|
|
env=core.get_script_env(),
|
|
)
|
|
|
|
stdout_thread = threading.Thread(
|
|
target=formatting_utils.handle_stream, args=(process.stdout, "")
|
|
)
|
|
stderr_thread = threading.Thread(
|
|
target=formatting_utils.handle_stream, args=(process.stderr, "[!]")
|
|
)
|
|
|
|
stdout_thread.start()
|
|
stderr_thread.start()
|
|
|
|
stdout_thread.join()
|
|
stderr_thread.join()
|
|
|
|
return process.wait()
|
|
|
|
|
|
core.manager_funcs = ManagerFuncsInComfyUI()
|
|
|
|
from comfyui_manager.common.manager_downloader import (
|
|
download_url,
|
|
download_url_with_agent,
|
|
)
|
|
|
|
|
|
class TaskQueue:
|
|
instance = None
|
|
|
|
def __init__(self):
|
|
TaskQueue.instance = self
|
|
self.mutex = threading.RLock()
|
|
self.not_empty = threading.Condition(self.mutex)
|
|
self.current_index = 0
|
|
self.pending_tasks = []
|
|
self.running_tasks = {}
|
|
self.history_tasks = {}
|
|
self.task_counter = 0
|
|
self.batch_id = None
|
|
self.batch_start_time = None
|
|
self.batch_state_before = None
|
|
# Client tracking implemented - see client_id support in QueueTaskItem and TaskHistoryItem
|
|
# Batch history serialization implemented - see finalize() method
|
|
class ExecutionStatus(NamedTuple):
|
|
status_str: Literal["success", "error", "skip"]
|
|
completed: bool
|
|
messages: List[str]
|
|
|
|
def get_current_state(self) -> TaskStateMessage:
|
|
return TaskStateMessage(
|
|
history=self.get_history(),
|
|
running_queue=self.get_current_queue()[0],
|
|
pending_queue=self.get_current_queue()[1],
|
|
)
|
|
|
|
@staticmethod
|
|
def send_queue_state_update(
|
|
msg: str, update: MessageUpdate, client_id: Optional[str] = None
|
|
) -> None:
|
|
"""Send queue state update to clients.
|
|
|
|
Args:
|
|
msg: Message type/event name
|
|
update: Update data to send
|
|
client_id: Optional client ID. If None, broadcasts to all clients.
|
|
If provided, sends only to that specific client.
|
|
"""
|
|
PromptServer.instance.send_sync(msg, update.model_dump(), client_id)
|
|
|
|
def put(self, item) -> None:
|
|
"""Add a task to the queue. Item can be a dict or QueueTaskItem model."""
|
|
with self.mutex:
|
|
# Start a new batch if this is the first task after queue was empty
|
|
if self.batch_id is None and len(self.pending_tasks) == 0 and len(self.running_tasks) == 0:
|
|
self._start_new_batch()
|
|
|
|
# Convert to dict if it's a Pydantic model
|
|
if hasattr(item, 'model_dump'):
|
|
item = item.model_dump()
|
|
|
|
heapq.heappush(self.pending_tasks, item)
|
|
self.not_empty.notify()
|
|
|
|
def _start_new_batch(self) -> None:
|
|
"""Start a new batch session for tracking operations."""
|
|
self.batch_id = f"batch_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
|
|
self.batch_start_time = datetime.now().isoformat()
|
|
self.batch_state_before = self._capture_system_state()
|
|
logging.info(f"[ComfyUI-Manager] Started new batch: {self.batch_id}")
|
|
|
|
def get(
|
|
self, timeout: Optional[float] = None
|
|
) -> tuple[Optional[QueueTaskItem], int]:
|
|
with self.not_empty:
|
|
while len(self.pending_tasks) == 0:
|
|
self.not_empty.wait(timeout=timeout)
|
|
if timeout is not None and len(self.pending_tasks) == 0:
|
|
return None
|
|
item = heapq.heappop(self.pending_tasks)
|
|
task_index = self.task_counter
|
|
self.running_tasks[task_index] = copy.deepcopy(item)
|
|
self.task_counter += 1
|
|
TaskQueue.send_queue_state_update(
|
|
ManagerMessageName.TASK_STARTED.value,
|
|
MessageTaskStarted(
|
|
ui_id=item["ui_id"],
|
|
kind=item["kind"],
|
|
timestamp=datetime.now().isoformat(),
|
|
state=self.get_current_state(),
|
|
),
|
|
client_id=item[
|
|
"client_id"
|
|
], # Send task started only to the client that requested it
|
|
)
|
|
return item, task_index
|
|
|
|
def task_done(
|
|
self,
|
|
item: QueueTaskItem,
|
|
result_msg: str,
|
|
status: Optional["TaskQueue.ExecutionStatus"] = None,
|
|
) -> None:
|
|
"""Mark task as completed and add to history"""
|
|
|
|
with self.mutex:
|
|
timestamp = datetime.now().isoformat()
|
|
|
|
# Manage history size
|
|
if len(self.history_tasks) > MAXIMUM_HISTORY_SIZE:
|
|
self.history_tasks.pop(next(iter(self.history_tasks)))
|
|
|
|
status_dict: Optional[dict] = None
|
|
if status is not None:
|
|
status_dict = status._asdict()
|
|
|
|
# Update history
|
|
self.history_tasks[item["ui_id"]] = TaskHistoryItem(
|
|
ui_id=item["ui_id"],
|
|
client_id=item["client_id"],
|
|
timestamp=timestamp,
|
|
result=result_msg,
|
|
kind=item["kind"],
|
|
status=status_dict,
|
|
)
|
|
|
|
# Send WebSocket message indicating task is complete
|
|
TaskQueue.send_queue_state_update(
|
|
ManagerMessageName.TASK_DONE.value,
|
|
MessageTaskDone(
|
|
ui_id=item["ui_id"],
|
|
result=result_msg,
|
|
kind=item["kind"],
|
|
status=status_dict,
|
|
timestamp=timestamp,
|
|
state=self.get_current_state(),
|
|
),
|
|
client_id=item[
|
|
"client_id"
|
|
], # Send completion only to the client that requested it
|
|
)
|
|
|
|
def get_current_queue(self) -> tuple[list[QueueTaskItem], list[QueueTaskItem]]:
|
|
"""Get current running and remaining tasks"""
|
|
with self.mutex:
|
|
running = list(self.running_tasks.values())
|
|
remaining = copy.copy(self.pending_tasks)
|
|
return running, remaining
|
|
|
|
def get_tasks_remaining(self) -> int:
|
|
"""Get number of tasks remaining"""
|
|
with self.mutex:
|
|
return len(self.pending_tasks) + len(self.running_tasks)
|
|
|
|
def wipe_queue(self) -> None:
|
|
"""Clear all task queue"""
|
|
with self.mutex:
|
|
self.pending_tasks = []
|
|
|
|
def abort(self) -> None:
|
|
"""Abort current operations"""
|
|
with self.mutex:
|
|
self.pending_tasks = []
|
|
self.running_tasks = {}
|
|
|
|
def delete_history_item(self, ui_id: str) -> None:
|
|
"""Delete specific task from history"""
|
|
with self.mutex:
|
|
self.history_tasks.pop(ui_id, None)
|
|
|
|
def get_history(
|
|
self,
|
|
ui_id: Optional[str] = None,
|
|
max_items: Optional[int] = None,
|
|
offset: int = -1,
|
|
) -> dict[str, TaskHistoryItem]:
|
|
"""Get task history. If ui_id (task id) is passsed, only return that task's history item entry."""
|
|
with self.mutex:
|
|
if ui_id is None:
|
|
out = {}
|
|
i = 0
|
|
if offset < 0 and max_items is not None:
|
|
offset = len(self.history_tasks) - max_items
|
|
for k in self.history_tasks:
|
|
if i >= offset:
|
|
out[k] = self.history_tasks[k]
|
|
if max_items is not None and len(out) >= max_items:
|
|
break
|
|
i += 1
|
|
return out
|
|
elif ui_id in self.history_tasks:
|
|
return self.history_tasks[ui_id]
|
|
else:
|
|
return {}
|
|
|
|
def done_count(self) -> int:
|
|
"""Get the number of completed tasks in history.
|
|
|
|
Returns:
|
|
int: Number of tasks that have been completed and are stored in history.
|
|
Returns 0 if history_tasks is None (defensive programming).
|
|
"""
|
|
return len(self.history_tasks) if self.history_tasks is not None else 0
|
|
|
|
def total_count(self) -> int:
|
|
"""Get the total number of tasks currently in the system (pending + running).
|
|
|
|
Returns:
|
|
int: Combined count of pending and running tasks.
|
|
Returns 0 if either collection is None (defensive programming).
|
|
"""
|
|
return (
|
|
len(self.pending_tasks) + len(self.running_tasks)
|
|
if self.pending_tasks is not None and self.running_tasks is not None
|
|
else 0
|
|
)
|
|
|
|
def finalize(self) -> None:
|
|
"""Finalize a completed task batch by saving execution history to disk.
|
|
|
|
This method is intended to be called when the queue transitions from having
|
|
tasks to being completely empty (no pending or running tasks). It will create
|
|
a comprehensive snapshot of the ComfyUI state and all operations performed.
|
|
"""
|
|
if self.batch_id is not None:
|
|
batch_path = os.path.join(
|
|
context.manager_batch_history_path, self.batch_id + ".json"
|
|
)
|
|
|
|
try:
|
|
end_time = datetime.now().isoformat()
|
|
state_after = self._capture_system_state()
|
|
operations = self._extract_batch_operations()
|
|
|
|
batch_record = BatchExecutionRecord(
|
|
batch_id=self.batch_id,
|
|
start_time=self.batch_start_time,
|
|
end_time=end_time,
|
|
state_before=self.batch_state_before,
|
|
state_after=state_after,
|
|
operations=operations,
|
|
total_operations=len(operations),
|
|
successful_operations=len([op for op in operations if op.result == "success"]),
|
|
failed_operations=len([op for op in operations if op.result == "failed"]),
|
|
skipped_operations=len([op for op in operations if op.result == "skipped"])
|
|
)
|
|
|
|
# Save to disk
|
|
with open(batch_path, "w", encoding="utf-8") as json_file:
|
|
json.dump(batch_record.model_dump(), json_file, indent=4, default=str)
|
|
|
|
logging.info(f"[ComfyUI-Manager] Batch history saved: {batch_path}")
|
|
|
|
# Reset batch tracking
|
|
self.batch_id = None
|
|
self.batch_start_time = None
|
|
self.batch_state_before = None
|
|
|
|
except Exception as e:
|
|
logging.error(f"[ComfyUI-Manager] Failed to save batch history: {e}")
|
|
|
|
def _capture_system_state(self) -> ComfyUISystemState:
|
|
"""Capture current ComfyUI system state for batch record."""
|
|
return ComfyUISystemState(
|
|
snapshot_time=datetime.now().isoformat(),
|
|
comfyui_version=self._get_comfyui_version_info(),
|
|
python_version=platform.python_version(),
|
|
platform_info=f"{platform.system()} {platform.release()} ({platform.machine()})",
|
|
installed_nodes=self._get_installed_nodes(),
|
|
installed_models=self._get_installed_models()
|
|
)
|
|
|
|
def _get_comfyui_version_info(self) -> ComfyUIVersionInfo:
|
|
"""Get ComfyUI version information."""
|
|
try:
|
|
version_info = core.get_comfyui_versions()
|
|
current_version = version_info[1] if len(version_info) > 1 else "unknown"
|
|
return ComfyUIVersionInfo(version=current_version)
|
|
except Exception:
|
|
return ComfyUIVersionInfo(version="unknown")
|
|
|
|
def _get_installed_nodes(self) -> dict[str, InstalledNodeInfo]:
|
|
"""Get information about installed node packages."""
|
|
installed_nodes = {}
|
|
|
|
try:
|
|
node_packs = core.get_installed_node_packs()
|
|
for pack_name, pack_info in node_packs.items():
|
|
installed_nodes[pack_name] = InstalledNodeInfo(
|
|
name=pack_name,
|
|
version=pack_info.get("ver", "unknown"),
|
|
install_method="unknown",
|
|
enabled=pack_info.get("enabled", True)
|
|
)
|
|
except Exception as e:
|
|
logging.warning(f"[ComfyUI-Manager] Failed to get installed nodes: {e}")
|
|
|
|
return installed_nodes
|
|
|
|
def _get_installed_models(self) -> dict[str, InstalledModelInfo]:
|
|
"""Get information about installed models."""
|
|
installed_models = {}
|
|
|
|
try:
|
|
model_dirs = ["checkpoints", "loras", "vae", "embeddings", "controlnet", "upscale_models"]
|
|
|
|
for model_type in model_dirs:
|
|
try:
|
|
files = folder_paths.get_filename_list(model_type)
|
|
for filename in files:
|
|
model_paths = folder_paths.get_folder_paths(model_type)
|
|
if model_paths:
|
|
full_path = os.path.join(model_paths[0], filename)
|
|
if os.path.exists(full_path):
|
|
installed_models[filename] = InstalledModelInfo(
|
|
name=filename,
|
|
path=full_path,
|
|
type=model_type,
|
|
size_bytes=os.path.getsize(full_path)
|
|
)
|
|
except Exception:
|
|
continue
|
|
|
|
except Exception as e:
|
|
logging.warning(f"[ComfyUI-Manager] Failed to get installed models: {e}")
|
|
|
|
return installed_models
|
|
|
|
def _extract_batch_operations(self) -> list[BatchOperation]:
|
|
"""Extract operations from completed task history for this batch."""
|
|
operations = []
|
|
|
|
try:
|
|
for ui_id, task in self.history_tasks.items():
|
|
result_status = "success"
|
|
if task.status:
|
|
status_str = task.status.get("status_str", "success")
|
|
if status_str == "error":
|
|
result_status = "failed"
|
|
elif status_str == "skip":
|
|
result_status = "skipped"
|
|
|
|
operation = BatchOperation(
|
|
operation_id=ui_id,
|
|
operation_type=task.kind,
|
|
target=f"task_{ui_id}",
|
|
result=result_status,
|
|
start_time=task.timestamp,
|
|
client_id=task.client_id
|
|
)
|
|
operations.append(operation)
|
|
except Exception as e:
|
|
logging.warning(f"[ComfyUI-Manager] Failed to extract batch operations: {e}")
|
|
|
|
return operations
|
|
|
|
|
|
task_queue = TaskQueue()
|
|
|
|
# Preview method initialization
|
|
if args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
|
|
environment_utils.set_preview_method(core.get_config()['preview_method'])
|
|
else:
|
|
logging.warning("[ComfyUI-Manager] Since --preview-method is set, ComfyUI-Manager's preview method feature will be ignored.")
|
|
|
|
# Legacy variables for compatibility
|
|
task_worker_thread = None
|
|
task_worker_lock = threading.Lock()
|
|
|
|
def get_model_dir(data, show_log=False):
|
|
if 'download_model_base' in folder_paths.folder_names_and_paths:
|
|
models_base = folder_paths.folder_names_and_paths['download_model_base'][0][0]
|
|
else:
|
|
models_base = folder_paths.models_dir
|
|
|
|
# NOTE: Validate to prevent path traversal.
|
|
if any(char in data['filename'] for char in {'/', '\\', ':'}):
|
|
return None
|
|
|
|
def resolve_custom_node(save_path):
|
|
save_path = save_path[13:] # remove 'custom_nodes/'
|
|
|
|
# NOTE: Validate to prevent path traversal.
|
|
if save_path.startswith(os.path.sep) or ':' in save_path:
|
|
return None
|
|
|
|
repo_name = save_path.replace('\\','/').split('/')[0] # get custom node repo name
|
|
|
|
# NOTE: The creation of files within the custom node path should be removed in the future.
|
|
repo_path = core.lookup_installed_custom_nodes_legacy(repo_name)
|
|
if repo_path is not None and repo_path[0]:
|
|
# Returns the retargeted path based on the actually installed repository
|
|
return os.path.join(os.path.dirname(repo_path[1]), save_path)
|
|
else:
|
|
return None
|
|
|
|
if data['save_path'] != 'default':
|
|
if '..' in data['save_path'] or data['save_path'].startswith('/'):
|
|
if show_log:
|
|
logging.info(f"[WARN] '{data['save_path']}' is not allowed path. So it will be saved into 'models/etc'.")
|
|
base_model = os.path.join(models_base, "etc")
|
|
else:
|
|
if data['save_path'].startswith("custom_nodes"):
|
|
base_model = resolve_custom_node(data['save_path'])
|
|
if base_model is None:
|
|
if show_log:
|
|
logging.info(f"[ComfyUI-Manager] The target custom node for model download is not installed: {data['save_path']}")
|
|
return None
|
|
else:
|
|
base_model = os.path.join(models_base, data['save_path'])
|
|
else:
|
|
model_dir_name = model_dir_name_map.get(data['type'].lower())
|
|
if model_dir_name is not None:
|
|
base_model = folder_paths.folder_names_and_paths[model_dir_name][0][0]
|
|
else:
|
|
base_model = os.path.join(models_base, "etc")
|
|
|
|
return base_model
|
|
|
|
def get_model_path(data, show_log=False):
|
|
base_model = get_model_dir(data, show_log)
|
|
if base_model is None:
|
|
return None
|
|
else:
|
|
if data['filename'] == '<huggingface>':
|
|
return os.path.join(base_model, os.path.basename(data['url']))
|
|
else:
|
|
return os.path.join(base_model, data['filename'])
|
|
|
|
|
|
async def task_worker():
|
|
await core.unified_manager.reload("cache")
|
|
|
|
async def do_install(item) -> str:
|
|
node_id = item.get("id")
|
|
node_version = item.get("selected_version")
|
|
channel = item.get("channel")
|
|
mode = item.get("mode")
|
|
skip_post_install = item.get("skip_post_install")
|
|
|
|
try:
|
|
node_spec = core.unified_manager.resolve_node_spec(
|
|
f"{node_id}@{node_version}"
|
|
)
|
|
if node_spec is None:
|
|
logging.error(
|
|
f"Cannot resolve install target: '{node_id}@{node_version}'"
|
|
)
|
|
return f"Cannot resolve install target: '{node_id}@{node_version}'"
|
|
|
|
node_name, version_spec, is_specified = node_spec
|
|
res = await core.unified_manager.install_by_id(
|
|
node_name,
|
|
version_spec,
|
|
channel,
|
|
mode,
|
|
return_postinstall=skip_post_install,
|
|
) # discard post install if skip_post_install mode
|
|
|
|
if res.action not in [
|
|
"skip",
|
|
"enable",
|
|
"install-git",
|
|
"install-cnr",
|
|
"switch-cnr",
|
|
]:
|
|
logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}")
|
|
return res.msg
|
|
|
|
elif not res.result:
|
|
logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}")
|
|
return res.msg
|
|
|
|
return "success"
|
|
except Exception:
|
|
traceback.print_exc()
|
|
return "Installation failed"
|
|
|
|
async def do_enable(item) -> str:
|
|
cnr_id = item.get("cnr_id")
|
|
core.unified_manager.unified_enable(cnr_id)
|
|
return "success"
|
|
|
|
async def do_update(item):
|
|
node_name = item.get("node_name")
|
|
node_ver = item.get("node_ver")
|
|
|
|
try:
|
|
res = core.unified_manager.unified_update(node_name, node_ver)
|
|
|
|
if res.ver == "unknown":
|
|
url = core.unified_manager.unknown_active_nodes[node_name][0]
|
|
try:
|
|
title = os.path.basename(url)
|
|
except Exception:
|
|
title = node_name
|
|
else:
|
|
url = core.unified_manager.cnr_map[node_name].get("repository")
|
|
title = core.unified_manager.cnr_map[node_name]["name"]
|
|
|
|
manager_util.clear_pip_cache()
|
|
|
|
if url is not None:
|
|
base_res = {"url": url, "title": title}
|
|
else:
|
|
base_res = {"title": title}
|
|
|
|
if res.result:
|
|
if res.action == "skip":
|
|
base_res["msg"] = "skip"
|
|
return base_res
|
|
else:
|
|
base_res["msg"] = "success"
|
|
return base_res
|
|
|
|
base_res["msg"] = f"An error occurred while updating '{node_name}'."
|
|
logging.error(
|
|
f"\nERROR: An error occurred while updating '{node_name}'. (res.result={res.result}, res.action={res.action})"
|
|
)
|
|
return base_res
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
return {"msg": f"An error occurred while updating '{node_name}'."}
|
|
|
|
async def do_update_comfyui(is_stable) -> str:
|
|
try:
|
|
repo_path = os.path.dirname(folder_paths.__file__)
|
|
latest_tag = None
|
|
if is_stable:
|
|
res, latest_tag = core.update_to_stable_comfyui(repo_path)
|
|
else:
|
|
res = core.update_path(repo_path)
|
|
|
|
if res == "fail":
|
|
logging.error("ComfyUI update failed")
|
|
return "fail"
|
|
elif res == "updated":
|
|
if is_stable:
|
|
logging.info("ComfyUI is updated to latest stable version.")
|
|
return "success-stable-" + latest_tag
|
|
else:
|
|
logging.info("ComfyUI is updated to latest nightly version.")
|
|
return "success-nightly"
|
|
else: # skipped
|
|
logging.info("ComfyUI is up-to-date.")
|
|
return "skip"
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
return "An error occurred while updating 'comfyui'."
|
|
|
|
async def do_fix(item) -> str:
|
|
node_name = item.get("node_name")
|
|
node_ver = item.get("node_ver")
|
|
|
|
try:
|
|
res = core.unified_manager.unified_fix(node_name, node_ver)
|
|
|
|
if res.result:
|
|
return "success"
|
|
else:
|
|
logging.error(res.msg)
|
|
|
|
logging.error(
|
|
f"\nERROR: An error occurred while fixing '{node_name}@{node_ver}'."
|
|
)
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
return f"An error occurred while fixing '{node_name}@{node_ver}'."
|
|
|
|
async def do_uninstall(item) -> str:
|
|
node_name = item.get("node_name")
|
|
is_unknown = item.get("is_unknown")
|
|
|
|
try:
|
|
res = core.unified_manager.unified_uninstall(node_name, is_unknown)
|
|
|
|
if res.result:
|
|
return "success"
|
|
|
|
logging.error(
|
|
f"\nERROR: An error occurred while uninstalling '{node_name}'."
|
|
)
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
return f"An error occurred while uninstalling '{node_name}'."
|
|
|
|
async def do_disable(item) -> str:
|
|
node_name = item.get("node_name")
|
|
try:
|
|
res = core.unified_manager.unified_disable(
|
|
node_name, item.get("is_unknown")
|
|
)
|
|
|
|
if res:
|
|
return "success"
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
return f"Failed to disable: '{node_name}'"
|
|
|
|
async def do_install_model(item) -> str:
|
|
json_data = item.get("json_data")
|
|
|
|
model_path = model_utils.get_model_path(json_data)
|
|
model_url = json_data.get("url")
|
|
|
|
res = False
|
|
|
|
try:
|
|
if model_path is not None:
|
|
logging.info(
|
|
f"Install model '{json_data['name']}' from '{model_url}' into '{model_path}'"
|
|
)
|
|
|
|
if json_data["filename"] == "<huggingface>":
|
|
if os.path.exists(
|
|
os.path.join(model_path, os.path.dirname(json_data["url"]))
|
|
):
|
|
logging.error(
|
|
f"[ComfyUI-Manager] the model path already exists: {model_path}"
|
|
)
|
|
return f"The model path already exists: {model_path}"
|
|
|
|
logging.info(
|
|
f"[ComfyUI-Manager] Downloading '{model_url}' into '{model_path}'"
|
|
)
|
|
manager_downloader.download_repo_in_bytes(
|
|
repo_id=model_url, local_dir=model_path
|
|
)
|
|
|
|
return "success"
|
|
|
|
elif not core.get_config()["model_download_by_agent"] and (
|
|
model_url.startswith("https://github.com")
|
|
or model_url.startswith("https://huggingface.co")
|
|
or model_url.startswith("https://heibox.uni-heidelberg.de")
|
|
):
|
|
model_dir = model_utils.get_model_dir(json_data, True)
|
|
download_url(model_url, model_dir, filename=json_data["filename"])
|
|
if model_path.endswith(".zip"):
|
|
res = core.unzip(model_path)
|
|
else:
|
|
res = True
|
|
|
|
if res:
|
|
return "success"
|
|
else:
|
|
res = download_url_with_agent(model_url, model_path)
|
|
if res and model_path.endswith(".zip"):
|
|
res = core.unzip(model_path)
|
|
else:
|
|
logging.error(
|
|
f"[ComfyUI-Manager] Model installation error: invalid model type - {json_data['type']}"
|
|
)
|
|
|
|
if res:
|
|
return "success"
|
|
|
|
except Exception as e:
|
|
logging.error(f"[ComfyUI-Manager] ERROR: {e}", file=sys.stderr)
|
|
|
|
return f"Model installation error: {model_url}"
|
|
|
|
async def do_update_all(item):
|
|
res = await _update_all(item["mode"])
|
|
return res
|
|
|
|
while True:
|
|
timeout = 4096
|
|
task = task_queue.get(timeout)
|
|
if task is None:
|
|
# Check if queue is truly empty (no pending or running tasks)
|
|
if task_queue.total_count() == 0 and len(task_queue.running_tasks) == 0:
|
|
logging.info("\n[ComfyUI-Manager] All tasks are completed.")
|
|
|
|
# Trigger batch history serialization if there are completed tasks
|
|
if task_queue.done_count() > 0:
|
|
logging.info("[ComfyUI-Manager] Finalizing batch history...")
|
|
task_queue.finalize()
|
|
logging.info("[ComfyUI-Manager] Batch history saved.")
|
|
|
|
logging.info("\nAfter restarting ComfyUI, please refresh the browser.")
|
|
|
|
res = {"status": "all-done"}
|
|
|
|
# Broadcast general status updates to all clients
|
|
PromptServer.instance.send_sync("cm-queue-status", res)
|
|
|
|
return
|
|
|
|
item, task_index = task
|
|
kind = item["kind"]
|
|
|
|
print(f"Processing task: {kind} with item: {item} at index: {task_index}")
|
|
|
|
try:
|
|
if kind == "install":
|
|
msg = await do_install(item)
|
|
elif kind == "enable":
|
|
msg = await do_enable(item)
|
|
elif kind == "install-model":
|
|
msg = await do_install_model(item)
|
|
elif kind == "update":
|
|
msg = await do_update(item)
|
|
elif kind == "update-all":
|
|
msg = await do_update_all(item)
|
|
elif kind == "update-main":
|
|
msg = await do_update(item)
|
|
elif kind == "update-comfyui":
|
|
msg = await do_update_comfyui(item[1])
|
|
elif kind == "fix":
|
|
msg = await do_fix(item)
|
|
elif kind == "uninstall":
|
|
msg = await do_uninstall(item)
|
|
elif kind == "disable":
|
|
msg = await do_disable(item)
|
|
else:
|
|
msg = "Unexpected kind: " + kind
|
|
except Exception:
|
|
msg = f"Exception: {(kind, item)}"
|
|
task_queue.task_done(
|
|
item, msg, TaskQueue.ExecutionStatus("error", True, [msg])
|
|
)
|
|
|
|
# Determine status and message for task completion
|
|
if isinstance(msg, dict) and "msg" in msg:
|
|
result_msg = msg["msg"]
|
|
else:
|
|
result_msg = msg
|
|
|
|
# Determine status
|
|
if result_msg == "success":
|
|
status = TaskQueue.ExecutionStatus("success", True, [])
|
|
elif result_msg == "skip":
|
|
status = TaskQueue.ExecutionStatus("skip", True, [])
|
|
else:
|
|
status = TaskQueue.ExecutionStatus("error", True, [result_msg])
|
|
|
|
task_queue.task_done(item, msg, status)
|
|
|
|
|
|
@routes.post("/v2/manager/queue/task")
|
|
async def queue_task(request) -> web.Response:
|
|
"""Add a new task to the processing queue.
|
|
|
|
Accepts task data via JSON POST and adds it to the TaskQueue for processing.
|
|
The task worker will automatically pick up and process queued tasks.
|
|
|
|
Args:
|
|
request: aiohttp request containing JSON task data
|
|
|
|
Returns:
|
|
web.Response: HTTP 200 on successful queueing, HTTP 400 on validation error
|
|
"""
|
|
try:
|
|
json_data = await request.json()
|
|
# Validate input using Pydantic model
|
|
task_item = QueueTaskItem.model_validate(json_data)
|
|
TaskQueue.instance.put(task_item)
|
|
# maybe start worker
|
|
return web.Response(status=200)
|
|
except ValidationError as e:
|
|
logging.error(f"[ComfyUI-Manager] Invalid task data: {e}")
|
|
return web.Response(status=400, text=f"Invalid task data: {e}")
|
|
except Exception as e:
|
|
logging.error(f"[ComfyUI-Manager] Error processing task: {e}")
|
|
return web.Response(status=500, text="Internal server error")
|
|
|
|
|
|
@routes.get("/v2/manager/queue/history_list")
|
|
async def get_history_list(request) -> web.Response:
|
|
"""Get list of available batch history files.
|
|
|
|
Returns a list of batch history IDs sorted by modification time (newest first).
|
|
These IDs can be used with the history endpoint to retrieve detailed batch information.
|
|
|
|
Returns:
|
|
web.Response: JSON response with 'ids' array of history file IDs
|
|
"""
|
|
history_path = context.manager_batch_history_path
|
|
|
|
try:
|
|
files = [
|
|
os.path.join(history_path, f)
|
|
for f in os.listdir(history_path)
|
|
if os.path.isfile(os.path.join(history_path, f))
|
|
]
|
|
files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
|
|
history_ids = [os.path.basename(f)[:-5] for f in files]
|
|
|
|
return web.json_response(
|
|
{"ids": list(history_ids)}, content_type="application/json"
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"[ComfyUI-Manager] /v2/manager/queue/history_list - {e}")
|
|
return web.Response(status=400)
|
|
|
|
|
|
@routes.get("/v2/manager/queue/history")
|
|
async def get_history(request):
|
|
"""Get task history with optional client filtering.
|
|
|
|
Query parameters:
|
|
id: Batch history ID (for file-based history)
|
|
client_id: Optional client ID to filter current session history
|
|
ui_id: Optional specific task ID to get single task history
|
|
max_items: Maximum number of items to return
|
|
offset: Offset for pagination
|
|
|
|
Returns:
|
|
JSON with filtered history data
|
|
"""
|
|
try:
|
|
# Handle file-based batch history
|
|
if "id" in request.rel_url.query:
|
|
json_name = request.rel_url.query["id"] + ".json"
|
|
batch_path = os.path.join(context.manager_batch_history_path, json_name)
|
|
|
|
with open(batch_path, "r", encoding="utf-8") as file:
|
|
json_str = file.read()
|
|
json_obj = json.loads(json_str)
|
|
return web.json_response(json_obj, content_type="application/json")
|
|
|
|
# Handle current session history with optional filtering
|
|
client_id = request.rel_url.query.get("client_id")
|
|
ui_id = request.rel_url.query.get("ui_id")
|
|
max_items = request.rel_url.query.get("max_items")
|
|
offset = request.rel_url.query.get("offset", -1)
|
|
|
|
if max_items:
|
|
max_items = int(max_items)
|
|
if offset:
|
|
offset = int(offset)
|
|
|
|
# Get history from TaskQueue
|
|
if ui_id:
|
|
history = task_queue.get_history(ui_id=ui_id)
|
|
else:
|
|
history = task_queue.get_history(max_items=max_items, offset=offset)
|
|
|
|
# Filter by client_id if provided
|
|
if client_id and isinstance(history, dict):
|
|
filtered_history = {
|
|
task_id: task_data
|
|
for task_id, task_data in history.items()
|
|
if hasattr(task_data, "client_id") and task_data.client_id == client_id
|
|
}
|
|
history = filtered_history
|
|
|
|
return web.json_response({"history": history}, content_type="application/json")
|
|
|
|
except Exception as e:
|
|
logging.error(f"[ComfyUI-Manager] /v2/manager/queue/history - {e}")
|
|
|
|
return web.Response(status=400)
|
|
|
|
|
|
@routes.get("/v2/customnode/getmappings")
|
|
async def fetch_customnode_mappings(request):
|
|
"""
|
|
provide unified (node -> node pack) mapping list
|
|
"""
|
|
mode = request.rel_url.query["mode"]
|
|
|
|
nickname_mode = False
|
|
if mode == "nickname":
|
|
mode = "local"
|
|
nickname_mode = True
|
|
|
|
json_obj = await core.get_data_by_mode(mode, "extension-node-map.json")
|
|
json_obj = core.map_to_unified_keys(json_obj)
|
|
|
|
if nickname_mode:
|
|
json_obj = node_pack_utils.nickname_filter(json_obj)
|
|
|
|
all_nodes = set()
|
|
patterns = []
|
|
for k, x in json_obj.items():
|
|
all_nodes.update(set(x[0]))
|
|
|
|
if "nodename_pattern" in x[1]:
|
|
patterns.append((x[1]["nodename_pattern"], x[0]))
|
|
|
|
missing_nodes = set(nodes.NODE_CLASS_MAPPINGS.keys()) - all_nodes
|
|
|
|
for x in missing_nodes:
|
|
for pat, item in patterns:
|
|
if re.match(pat, x):
|
|
item.append(x)
|
|
|
|
return web.json_response(json_obj, content_type="application/json")
|
|
|
|
|
|
@routes.get("/v2/customnode/fetch_updates")
|
|
async def fetch_updates(request):
|
|
try:
|
|
if request.rel_url.query["mode"] == "local":
|
|
channel = "local"
|
|
else:
|
|
channel = core.get_config()["channel_url"]
|
|
|
|
await core.unified_manager.reload(request.rel_url.query["mode"])
|
|
await core.unified_manager.get_custom_nodes(
|
|
channel, request.rel_url.query["mode"]
|
|
)
|
|
|
|
res = core.unified_manager.fetch_or_pull_git_repo(is_pull=False)
|
|
|
|
for x in res["failed"]:
|
|
logging.error(f"FETCH FAILED: {x}")
|
|
|
|
logging.info("\nDone.")
|
|
|
|
if len(res["updated"]) > 0:
|
|
return web.Response(status=201)
|
|
|
|
return web.Response(status=200)
|
|
except Exception:
|
|
traceback.print_exc()
|
|
return web.Response(status=400)
|
|
|
|
|
|
@routes.get("/v2/manager/queue/update_all")
|
|
async def update_all(request):
|
|
json_data = dict(request.rel_url.query)
|
|
return await _update_all(json_data)
|
|
|
|
|
|
async def _update_all(json_data):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
|
return web.Response(status=403)
|
|
|
|
with task_worker_lock:
|
|
is_processing = task_worker_thread is not None and task_worker_thread.is_alive()
|
|
if is_processing:
|
|
return web.Response(status=401)
|
|
|
|
await core.save_snapshot_with_postfix("autosave")
|
|
|
|
if json_data["mode"] == "local":
|
|
channel = "local"
|
|
else:
|
|
channel = core.get_config()["channel_url"]
|
|
|
|
await core.unified_manager.reload(json_data["mode"])
|
|
await core.unified_manager.get_custom_nodes(channel, json_data["mode"])
|
|
|
|
for k, v in core.unified_manager.active_nodes.items():
|
|
if k == "comfyui-manager":
|
|
# skip updating comfyui-manager if desktop version
|
|
if os.environ.get("__COMFYUI_DESKTOP_VERSION__"):
|
|
continue
|
|
|
|
update_item = k, k, v[0]
|
|
temp_queue_batch.append(("update-main", update_item))
|
|
|
|
for k, v in core.unified_manager.unknown_active_nodes.items():
|
|
if k == "comfyui-manager":
|
|
# skip updating comfyui-manager if desktop version
|
|
if os.environ.get("__COMFYUI_DESKTOP_VERSION__"):
|
|
continue
|
|
|
|
update_item = k, k, "unknown"
|
|
temp_queue_batch.append(("update-main", update_item))
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
def convert_markdown_to_html(input_text):
|
|
pattern_a = re.compile(r"\[a/([^]]+)]\(([^)]+)\)")
|
|
pattern_w = re.compile(r"\[w/([^]]+)]")
|
|
pattern_i = re.compile(r"\[i/([^]]+)]")
|
|
pattern_bold = re.compile(r"\*\*([^*]+)\*\*")
|
|
pattern_white = re.compile(r"%%([^*]+)%%")
|
|
|
|
def replace_a(match):
|
|
return f"<a href='{match.group(2)}' target='blank'>{match.group(1)}</a>"
|
|
|
|
def replace_w(match):
|
|
return f"<p class='cm-warn-note'>{match.group(1)}</p>"
|
|
|
|
def replace_i(match):
|
|
return f"<p class='cm-info-note'>{match.group(1)}</p>"
|
|
|
|
def replace_bold(match):
|
|
return f"<B>{match.group(1)}</B>"
|
|
|
|
def replace_white(match):
|
|
return f"<font color='white'>{match.group(1)}</font>"
|
|
|
|
input_text = (
|
|
input_text.replace("\\[", "[")
|
|
.replace("\\]", "]")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
)
|
|
|
|
result_text = re.sub(pattern_a, replace_a, input_text)
|
|
result_text = re.sub(pattern_w, replace_w, result_text)
|
|
result_text = re.sub(pattern_i, replace_i, result_text)
|
|
result_text = re.sub(pattern_bold, replace_bold, result_text)
|
|
result_text = re.sub(pattern_white, replace_white, result_text)
|
|
|
|
return result_text.replace("\n", "<BR>")
|
|
|
|
|
|
@routes.get("/v2/manager/is_legacy_manager_ui")
|
|
async def is_legacy_manager_ui(request):
|
|
return web.json_response(
|
|
{"is_legacy_manager_ui": args.enable_manager_legacy_ui},
|
|
content_type="application/json",
|
|
status=200,
|
|
)
|
|
|
|
|
|
# freeze imported version
|
|
startup_time_installed_node_packs = core.get_installed_node_packs()
|
|
|
|
|
|
@routes.get("/v2/customnode/installed")
|
|
async def installed_list(request):
|
|
mode = request.query.get("mode", "default")
|
|
|
|
if mode == "imported":
|
|
res = startup_time_installed_node_packs
|
|
else:
|
|
res = core.get_installed_node_packs()
|
|
|
|
return web.json_response(res, content_type="application/json")
|
|
|
|
|
|
def check_model_installed(json_obj):
|
|
def is_exists(model_dir_name, filename, url):
|
|
if filename == "<huggingface>":
|
|
filename = os.path.basename(url)
|
|
|
|
dirs = folder_paths.get_folder_paths(model_dir_name)
|
|
|
|
for x in dirs:
|
|
if os.path.exists(os.path.join(x, filename)):
|
|
return True
|
|
|
|
return False
|
|
|
|
model_dir_names = [
|
|
"checkpoints",
|
|
"loras",
|
|
"vae",
|
|
"text_encoders",
|
|
"diffusion_models",
|
|
"clip_vision",
|
|
"embeddings",
|
|
"diffusers",
|
|
"vae_approx",
|
|
"controlnet",
|
|
"gligen",
|
|
"upscale_models",
|
|
"hypernetworks",
|
|
"photomaker",
|
|
"classifiers",
|
|
]
|
|
|
|
total_models_files = set()
|
|
for x in model_dir_names:
|
|
for y in folder_paths.get_filename_list(x):
|
|
total_models_files.add(y)
|
|
|
|
def process_model_phase(item):
|
|
if (
|
|
"diffusion" not in item["filename"]
|
|
and "pytorch" not in item["filename"]
|
|
and "model" not in item["filename"]
|
|
):
|
|
# non-general name case
|
|
if item["filename"] in total_models_files:
|
|
item["installed"] = "True"
|
|
return
|
|
|
|
if item["save_path"] == "default":
|
|
model_dir_name = model_dir_name_map.get(item["type"].lower())
|
|
if model_dir_name is not None:
|
|
item["installed"] = str(
|
|
is_exists(model_dir_name, item["filename"], item["url"])
|
|
)
|
|
else:
|
|
item["installed"] = "False"
|
|
else:
|
|
model_dir_name = item["save_path"].split("/")[0]
|
|
if model_dir_name in folder_paths.folder_names_and_paths:
|
|
if is_exists(model_dir_name, item["filename"], item["url"]):
|
|
item["installed"] = "True"
|
|
|
|
if "installed" not in item:
|
|
if item["filename"] == "<huggingface>":
|
|
filename = os.path.basename(item["url"])
|
|
else:
|
|
filename = item["filename"]
|
|
|
|
fullpath = os.path.join(
|
|
folder_paths.models_dir, item["save_path"], filename
|
|
)
|
|
|
|
item["installed"] = "True" if os.path.exists(fullpath) else "False"
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(8) as executor:
|
|
for item in json_obj["models"]:
|
|
executor.submit(process_model_phase, item)
|
|
|
|
|
|
@routes.get("/v2/snapshot/getlist")
|
|
async def get_snapshot_list(request):
|
|
items = [
|
|
f[:-5] for f in os.listdir(context.manager_snapshot_path) if f.endswith(".json")
|
|
]
|
|
items.sort(reverse=True)
|
|
return web.json_response({"items": items}, content_type="application/json")
|
|
|
|
|
|
@routes.get("/v2/snapshot/remove")
|
|
async def remove_snapshot(request):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
|
return web.Response(status=403)
|
|
|
|
try:
|
|
target = request.rel_url.query["target"]
|
|
|
|
path = os.path.join(context.manager_snapshot_path, f"{target}.json")
|
|
if os.path.exists(path):
|
|
os.remove(path)
|
|
|
|
return web.Response(status=200)
|
|
except Exception:
|
|
return web.Response(status=400)
|
|
|
|
|
|
@routes.get("/v2/snapshot/restore")
|
|
async def restore_snapshot(request):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
|
return web.Response(status=403)
|
|
|
|
try:
|
|
target = request.rel_url.query["target"]
|
|
|
|
path = os.path.join(context.manager_snapshot_path, f"{target}.json")
|
|
if os.path.exists(path):
|
|
if not os.path.exists(context.manager_startup_script_path):
|
|
os.makedirs(context.manager_startup_script_path)
|
|
|
|
target_path = os.path.join(
|
|
context.manager_startup_script_path, "restore-snapshot.json"
|
|
)
|
|
shutil.copy(path, target_path)
|
|
|
|
logging.info(f"Snapshot restore scheduled: `{target}`")
|
|
return web.Response(status=200)
|
|
|
|
logging.error(f"Snapshot file not found: `{path}`")
|
|
return web.Response(status=400)
|
|
except Exception:
|
|
return web.Response(status=400)
|
|
|
|
|
|
@routes.get("/v2/snapshot/get_current")
|
|
async def get_current_snapshot_api(request):
|
|
try:
|
|
return web.json_response(
|
|
await core.get_current_snapshot(), content_type="application/json"
|
|
)
|
|
except Exception:
|
|
return web.Response(status=400)
|
|
|
|
|
|
@routes.get("/v2/snapshot/save")
|
|
async def save_snapshot(request):
|
|
try:
|
|
await core.save_snapshot_with_postfix("snapshot")
|
|
return web.Response(status=200)
|
|
except Exception:
|
|
return web.Response(status=400)
|
|
|
|
|
|
def unzip_install(files):
|
|
temp_filename = "manager-temp.zip"
|
|
for url in files:
|
|
if url.endswith("/"):
|
|
url = url[:-1]
|
|
try:
|
|
headers = {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
|
|
}
|
|
|
|
req = urllib.request.Request(url, headers=headers)
|
|
response = urllib.request.urlopen(req)
|
|
data = response.read()
|
|
|
|
with open(temp_filename, "wb") as f:
|
|
f.write(data)
|
|
|
|
with zipfile.ZipFile(temp_filename, "r") as zip_ref:
|
|
zip_ref.extractall(core.get_default_custom_nodes_path())
|
|
|
|
os.remove(temp_filename)
|
|
except Exception as e:
|
|
logging.error(f"Install(unzip) error: {url} / {e}", file=sys.stderr)
|
|
return False
|
|
|
|
logging.info("Installation was successful.")
|
|
return True
|
|
|
|
|
|
@routes.post("/v2/customnode/import_fail_info")
|
|
async def import_fail_info(request):
|
|
try:
|
|
json_data = await request.json()
|
|
|
|
# Basic validation - ensure we have either cnr_id or url
|
|
if not isinstance(json_data, dict):
|
|
return web.Response(status=400, text="Request body must be a JSON object")
|
|
|
|
if "cnr_id" not in json_data and "url" not in json_data:
|
|
return web.Response(status=400, text="Either 'cnr_id' or 'url' field is required")
|
|
|
|
if "cnr_id" in json_data:
|
|
if not isinstance(json_data["cnr_id"], str):
|
|
return web.Response(status=400, text="'cnr_id' must be a string")
|
|
module_name = core.unified_manager.get_module_name(json_data["cnr_id"])
|
|
else:
|
|
if not isinstance(json_data["url"], str):
|
|
return web.Response(status=400, text="'url' must be a string")
|
|
module_name = core.unified_manager.get_module_name(json_data["url"])
|
|
|
|
if module_name is not None:
|
|
info = cm_global.error_dict.get(module_name)
|
|
if info is not None:
|
|
return web.json_response(info)
|
|
|
|
return web.Response(status=400)
|
|
except Exception as e:
|
|
logging.error(f"[ComfyUI-Manager] Error processing import fail info: {e}")
|
|
return web.Response(status=500, text="Internal server error")
|
|
|
|
|
|
@routes.post("/v2/manager/queue/reinstall")
|
|
async def reinstall_custom_node(request):
|
|
try:
|
|
json_data = await request.json()
|
|
# Validate input using Pydantic model
|
|
pack_data = InstallPackParams.model_validate(json_data)
|
|
validated_data = pack_data.model_dump()
|
|
await _uninstall_custom_node(validated_data)
|
|
await _install_custom_node(validated_data)
|
|
return web.Response(status=200)
|
|
except ValidationError as e:
|
|
logging.error(f"[ComfyUI-Manager] Invalid pack data: {e}")
|
|
return web.Response(status=400, text=f"Invalid pack data: {e}")
|
|
except Exception as e:
|
|
logging.error(f"[ComfyUI-Manager] Error processing reinstall: {e}")
|
|
return web.Response(status=500, text="Internal server error")
|
|
|
|
|
|
@routes.get("/v2/manager/queue/reset")
|
|
async def reset_queue(request):
|
|
task_queue.wipe_queue()
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/manager/queue/abort_current")
|
|
async def abort_queue(request):
|
|
# task_queue.abort() # Method not implemented yet
|
|
task_queue.wipe_queue()
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/manager/queue/status")
|
|
async def queue_count(request):
|
|
"""Get current queue status with optional client filtering.
|
|
|
|
Query parameters:
|
|
client_id: Optional client ID to filter tasks
|
|
|
|
Returns:
|
|
JSON with queue counts and processing status
|
|
"""
|
|
client_id = request.query.get("client_id")
|
|
|
|
if client_id:
|
|
# Filter tasks by client_id
|
|
running_client_tasks = [
|
|
task
|
|
for task in task_queue.running_tasks.values()
|
|
if task.get("client_id") == client_id
|
|
]
|
|
pending_client_tasks = [
|
|
task
|
|
for task in task_queue.pending_tasks
|
|
if task.get("client_id") == client_id
|
|
]
|
|
history_client_tasks = {
|
|
ui_id: task
|
|
for ui_id, task in task_queue.history_tasks.items()
|
|
if hasattr(task, "client_id") and task.client_id == client_id
|
|
}
|
|
|
|
return web.json_response(
|
|
{
|
|
"client_id": client_id,
|
|
"total_count": len(pending_client_tasks) + len(running_client_tasks),
|
|
"done_count": len(history_client_tasks),
|
|
"in_progress_count": len(running_client_tasks),
|
|
"pending_count": len(pending_client_tasks),
|
|
"is_processing": task_worker_thread is not None
|
|
and task_worker_thread.is_alive(),
|
|
}
|
|
)
|
|
else:
|
|
# Return overall status
|
|
return web.json_response(
|
|
{
|
|
"total_count": task_queue.total_count(),
|
|
"done_count": task_queue.done_count(),
|
|
"in_progress_count": len(task_queue.running_tasks),
|
|
"pending_count": len(task_queue.pending_tasks),
|
|
"is_processing": task_worker_thread is not None
|
|
and task_worker_thread.is_alive(),
|
|
}
|
|
)
|
|
|
|
|
|
async def _install_custom_node(json_data):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
|
return web.Response(
|
|
status=403,
|
|
text="A security error has occurred. Please check the terminal logs",
|
|
)
|
|
|
|
# non-nightly cnr is safe
|
|
risky_level = None
|
|
cnr_id = json_data.get("id")
|
|
skip_post_install = json_data.get("skip_post_install")
|
|
|
|
git_url = None
|
|
|
|
selected_version = json_data.get("selected_version")
|
|
if json_data["version"] != "unknown" and selected_version != "unknown":
|
|
if skip_post_install:
|
|
if (
|
|
cnr_id in core.unified_manager.nightly_inactive_nodes
|
|
or cnr_id in core.unified_manager.cnr_inactive_nodes
|
|
):
|
|
enable_item = str(uuid.uuid4()), cnr_id
|
|
temp_queue_batch.append(("enable", enable_item))
|
|
return web.Response(status=200)
|
|
|
|
elif selected_version is None:
|
|
selected_version = "latest"
|
|
|
|
if selected_version != "nightly":
|
|
risky_level = "low"
|
|
node_spec_str = f"{cnr_id}@{selected_version}"
|
|
else:
|
|
node_spec_str = f"{cnr_id}@nightly"
|
|
git_url = [json_data.get("repository")]
|
|
if git_url is None:
|
|
logging.error(
|
|
f"[ComfyUI-Manager] Following node pack doesn't provide `nightly` version: ${git_url}"
|
|
)
|
|
return web.Response(
|
|
status=404,
|
|
text=f"Following node pack doesn't provide `nightly` version: ${git_url}",
|
|
)
|
|
|
|
elif json_data["version"] != "unknown" and selected_version == "unknown":
|
|
logging.error(f"[ComfyUI-Manager] Invalid installation request: {json_data}")
|
|
return web.Response(status=400, text="Invalid installation request")
|
|
|
|
else:
|
|
# unknown
|
|
unknown_name = os.path.basename(json_data["files"][0])
|
|
node_spec_str = f"{unknown_name}@unknown"
|
|
git_url = json_data.get("files")
|
|
|
|
# apply security policy if not cnr node (nightly isn't regarded as cnr node)
|
|
if risky_level is None:
|
|
if git_url is not None:
|
|
risky_level = await security_utils.get_risky_level(git_url, json_data.get("pip", []))
|
|
else:
|
|
return web.Response(
|
|
status=404,
|
|
text=f"Following node pack doesn't provide `nightly` version: ${git_url}",
|
|
)
|
|
|
|
if not security_utils.is_allowed_security_level(risky_level):
|
|
logging.error(SECURITY_MESSAGE_GENERAL)
|
|
return web.Response(
|
|
status=404,
|
|
text="A security error has occurred. Please check the terminal logs",
|
|
)
|
|
|
|
install_item = (
|
|
json_data.get("ui_id"),
|
|
node_spec_str,
|
|
json_data["channel"],
|
|
json_data["mode"],
|
|
skip_post_install,
|
|
)
|
|
temp_queue_batch.append(("install", install_item))
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
task_worker_thread: threading.Thread = None
|
|
|
|
|
|
@routes.get("/v2/manager/queue/start")
|
|
async def queue_start(request):
|
|
with task_worker_lock:
|
|
finalize_temp_queue_batch()
|
|
return _queue_start()
|
|
|
|
|
|
def _queue_start():
|
|
global task_worker_thread
|
|
|
|
if task_worker_thread is not None and task_worker_thread.is_alive():
|
|
return web.Response(status=201) # already in-progress
|
|
|
|
task_worker_thread = threading.Thread(target=lambda: asyncio.run(task_worker()))
|
|
task_worker_thread.start()
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
# Duplicate queue_start function removed - using the earlier one with proper implementation
|
|
|
|
|
|
async def _fix_custom_node(json_data):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_GENERAL)
|
|
return web.Response(
|
|
status=403,
|
|
text="A security error has occurred. Please check the terminal logs",
|
|
)
|
|
|
|
node_id = json_data.get("id")
|
|
node_ver = json_data["version"]
|
|
if node_ver != "unknown":
|
|
node_name = node_id
|
|
else:
|
|
# unknown
|
|
node_name = os.path.basename(json_data["files"][0])
|
|
|
|
update_item = json_data.get("ui_id"), node_name, json_data["version"]
|
|
temp_queue_batch.append(("fix", update_item))
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.post("/v2/customnode/install/git_url")
|
|
async def install_custom_node_git_url(request):
|
|
if not security_utils.is_allowed_security_level("high"):
|
|
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
|
|
return web.Response(status=403)
|
|
|
|
url = await request.text()
|
|
res = await core.gitclone_install(url)
|
|
|
|
if res.action == "skip":
|
|
logging.info(f"\nAlready installed: '{res.target}'")
|
|
return web.Response(status=200)
|
|
elif res.result:
|
|
logging.info("\nAfter restarting ComfyUI, please refresh the browser.")
|
|
return web.Response(status=200)
|
|
|
|
logging.error(res.msg)
|
|
return web.Response(status=400)
|
|
|
|
|
|
@routes.post("/v2/customnode/install/pip")
|
|
async def install_custom_node_pip(request):
|
|
if not security_utils.is_allowed_security_level("high"):
|
|
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
|
|
return web.Response(status=403)
|
|
|
|
packages = await request.text()
|
|
core.pip_install(packages.split(" "))
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
async def _uninstall_custom_node(json_data):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
|
return web.Response(
|
|
status=403,
|
|
text="A security error has occurred. Please check the terminal logs",
|
|
)
|
|
|
|
node_id = json_data.get("id")
|
|
if json_data["version"] != "unknown":
|
|
is_unknown = False
|
|
node_name = node_id
|
|
else:
|
|
# unknown
|
|
is_unknown = True
|
|
node_name = os.path.basename(json_data["files"][0])
|
|
|
|
uninstall_item = json_data.get("ui_id"), node_name, is_unknown
|
|
temp_queue_batch.append(("uninstall", uninstall_item))
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
async def _update_custom_node(json_data):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
|
return web.Response(
|
|
status=403,
|
|
text="A security error has occurred. Please check the terminal logs",
|
|
)
|
|
|
|
node_id = json_data.get("id")
|
|
if json_data["version"] != "unknown":
|
|
node_name = node_id
|
|
else:
|
|
# unknown
|
|
node_name = os.path.basename(json_data["files"][0])
|
|
|
|
update_item = json_data.get("ui_id"), node_name, json_data["version"]
|
|
temp_queue_batch.append(("update", update_item))
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/manager/queue/update_comfyui")
|
|
async def update_comfyui(request):
|
|
is_stable = core.get_config()["update_policy"] != "nightly-comfyui"
|
|
temp_queue_batch.append(("update-comfyui", ("comfyui", is_stable)))
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/comfyui_manager/comfyui_versions")
|
|
async def comfyui_versions(request):
|
|
try:
|
|
res, current, latest = core.get_comfyui_versions()
|
|
return web.json_response(
|
|
{"versions": res, "current": current},
|
|
status=200,
|
|
content_type="application/json",
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"ComfyUI update fail: {e}", file=sys.stderr)
|
|
|
|
return web.Response(status=400)
|
|
|
|
|
|
@routes.get("/v2/comfyui_manager/comfyui_switch_version")
|
|
async def comfyui_switch_version(request):
|
|
try:
|
|
if "ver" in request.rel_url.query:
|
|
core.switch_comfyui(request.rel_url.query["ver"])
|
|
|
|
return web.Response(status=200)
|
|
except Exception as e:
|
|
logging.error(f"ComfyUI update fail: {e}", file=sys.stderr)
|
|
|
|
return web.Response(status=400)
|
|
|
|
|
|
async def _disable_node(json_data):
|
|
node_id = json_data.get("id")
|
|
if json_data["version"] != "unknown":
|
|
is_unknown = False
|
|
node_name = node_id
|
|
else:
|
|
# unknown
|
|
is_unknown = True
|
|
node_name = os.path.basename(json_data["files"][0])
|
|
|
|
update_item = json_data.get("ui_id"), node_name, is_unknown
|
|
temp_queue_batch.append(("disable", update_item))
|
|
|
|
|
|
async def check_whitelist_for_model(item):
|
|
json_obj = await core.get_data_by_mode("cache", "model-list.json")
|
|
|
|
for x in json_obj.get("models", []):
|
|
if (
|
|
x["save_path"] == item["save_path"]
|
|
and x["base"] == item["base"]
|
|
and x["filename"] == item["filename"]
|
|
):
|
|
return True
|
|
|
|
json_obj = await core.get_data_by_mode("local", "model-list.json")
|
|
|
|
for x in json_obj.get("models", []):
|
|
if (
|
|
x["save_path"] == item["save_path"]
|
|
and x["base"] == item["base"]
|
|
and x["filename"] == item["filename"]
|
|
):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
@routes.post("/v2/manager/queue/install_model")
|
|
async def install_model(request):
|
|
try:
|
|
json_data = await request.json()
|
|
# Validate input using Pydantic model
|
|
model_data = ModelMetadata.model_validate(json_data)
|
|
return await _install_model(model_data.model_dump())
|
|
except ValidationError as e:
|
|
logging.error(f"[ComfyUI-Manager] Invalid model data: {e}")
|
|
return web.Response(status=400, text=f"Invalid model data: {e}")
|
|
except Exception as e:
|
|
logging.error(f"[ComfyUI-Manager] Error processing model install: {e}")
|
|
return web.Response(status=500, text="Internal server error")
|
|
|
|
|
|
async def _install_model(json_data):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
|
return web.Response(
|
|
status=403,
|
|
text="A security error has occurred. Please check the terminal logs",
|
|
)
|
|
|
|
# validate request
|
|
if not await check_whitelist_for_model(json_data):
|
|
logging.error(
|
|
f"[ComfyUI-Manager] Invalid model install request is detected: {json_data}"
|
|
)
|
|
return web.Response(
|
|
status=400, text="Invalid model install request is detected"
|
|
)
|
|
|
|
if not json_data["filename"].endswith(
|
|
".safetensors"
|
|
) and not security_utils.is_allowed_security_level("high"):
|
|
models_json = await core.get_data_by_mode("cache", "model-list.json", "default")
|
|
|
|
is_belongs_to_whitelist = False
|
|
for x in models_json["models"]:
|
|
if x.get("url") == json_data["url"]:
|
|
is_belongs_to_whitelist = True
|
|
break
|
|
|
|
if not is_belongs_to_whitelist:
|
|
logging.error(SECURITY_MESSAGE_NORMAL_MINUS_MODEL)
|
|
return web.Response(
|
|
status=403,
|
|
text="A security error has occurred. Please check the terminal logs",
|
|
)
|
|
|
|
install_item = json_data.get("ui_id"), json_data
|
|
temp_queue_batch.append(("install-model", install_item))
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/manager/preview_method")
|
|
async def preview_method(request):
|
|
if "value" in request.rel_url.query:
|
|
environment_utils.set_preview_method(request.rel_url.query["value"])
|
|
core.write_config()
|
|
else:
|
|
return web.Response(
|
|
text=core.manager_funcs.get_current_preview_method(), status=200
|
|
)
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/manager/db_mode")
|
|
async def db_mode(request):
|
|
if "value" in request.rel_url.query:
|
|
environment_utils.set_db_mode(request.rel_url.query["value"])
|
|
core.write_config()
|
|
else:
|
|
return web.Response(text=core.get_config()["db_mode"], status=200)
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/manager/policy/update")
|
|
async def update_policy(request):
|
|
if "value" in request.rel_url.query:
|
|
environment_utils.set_update_policy(request.rel_url.query["value"])
|
|
core.write_config()
|
|
else:
|
|
return web.Response(text=core.get_config()["update_policy"], status=200)
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/manager/channel_url_list")
|
|
async def channel_url_list(request):
|
|
channels = core.get_channel_dict()
|
|
if "value" in request.rel_url.query:
|
|
channel_url = channels.get(request.rel_url.query["value"])
|
|
if channel_url is not None:
|
|
core.get_config()["channel_url"] = channel_url
|
|
core.write_config()
|
|
else:
|
|
selected = "custom"
|
|
selected_url = core.get_config()["channel_url"]
|
|
|
|
for name, url in channels.items():
|
|
if url == selected_url:
|
|
selected = name
|
|
break
|
|
|
|
res = {"selected": selected, "list": core.get_channel_list()}
|
|
return web.json_response(res, status=200)
|
|
|
|
return web.Response(status=200)
|
|
|
|
|
|
@routes.get("/v2/manager/reboot")
|
|
def restart(self):
|
|
if not security_utils.is_allowed_security_level("middle"):
|
|
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
|
return web.Response(status=403)
|
|
|
|
try:
|
|
sys.stdout.close_log()
|
|
except Exception:
|
|
pass
|
|
|
|
if "__COMFY_CLI_SESSION__" in os.environ:
|
|
with open(os.path.join(os.environ["__COMFY_CLI_SESSION__"] + ".reboot"), "w"):
|
|
pass
|
|
|
|
print(
|
|
"\nRestarting...\n\n"
|
|
) # This printing should not be logging - that will be ugly
|
|
exit(0)
|
|
|
|
print(
|
|
"\nRestarting... [Legacy Mode]\n\n"
|
|
) # This printing should not be logging - that will be ugly
|
|
|
|
sys_argv = sys.argv.copy()
|
|
if "--windows-standalone-build" in sys_argv:
|
|
sys_argv.remove("--windows-standalone-build")
|
|
|
|
if sys_argv[0].endswith("__main__.py"): # this is a python module
|
|
module_name = os.path.basename(os.path.dirname(sys_argv[0]))
|
|
cmds = [sys.executable, "-m", module_name] + sys_argv[1:]
|
|
elif sys.platform.startswith("win32"):
|
|
cmds = ['"' + sys.executable + '"', '"' + sys_argv[0] + '"'] + sys_argv[1:]
|
|
else:
|
|
cmds = [sys.executable] + sys_argv
|
|
|
|
print(f"Command: {cmds}", flush=True)
|
|
|
|
return os.execv(sys.executable, cmds)
|
|
|
|
|
|
@routes.get("/v2/manager/version")
|
|
async def get_version(request):
|
|
return web.Response(text=core.version_str, status=200)
|
|
|
|
|
|
async def _confirm_try_install(sender, custom_node_url, msg):
|
|
json_obj = await core.get_data_by_mode("default", "custom-node-list.json")
|
|
|
|
sender = manager_util.sanitize_tag(sender)
|
|
msg = manager_util.sanitize_tag(msg)
|
|
target = core.lookup_customnode_by_url(json_obj, custom_node_url)
|
|
|
|
if target is not None:
|
|
PromptServer.instance.send_sync(
|
|
"cm-api-try-install-customnode",
|
|
{"sender": sender, "target": target, "msg": msg},
|
|
)
|
|
else:
|
|
logging.error(
|
|
f"[ComfyUI Manager API] Failed to try install - Unknown custom node url '{custom_node_url}'"
|
|
)
|
|
|
|
|
|
def confirm_try_install(sender, custom_node_url, msg):
|
|
asyncio.run(_confirm_try_install(sender, custom_node_url, msg))
|
|
|
|
|
|
cm_global.register_api("cm.try-install-custom-node", confirm_try_install)
|
|
|
|
|
|
async def default_cache_update():
|
|
core.refresh_channel_dict()
|
|
channel_url = core.get_config()["channel_url"]
|
|
|
|
async def get_cache(filename):
|
|
try:
|
|
if core.get_config()["default_cache_as_channel_url"]:
|
|
uri = f"{channel_url}/{filename}"
|
|
else:
|
|
uri = f"{core.DEFAULT_CHANNEL}/{filename}"
|
|
|
|
cache_uri = str(manager_util.simple_hash(uri)) + "_" + filename
|
|
cache_uri = os.path.join(manager_util.cache_dir, cache_uri)
|
|
|
|
json_obj = await manager_util.get_data(uri, True)
|
|
|
|
with manager_util.cache_lock:
|
|
with open(cache_uri, "w", encoding="utf-8") as file:
|
|
json.dump(json_obj, file, indent=4, sort_keys=True)
|
|
logging.info(f"[ComfyUI-Manager] default cache updated: {uri}")
|
|
except Exception as e:
|
|
logging.error(
|
|
f"[ComfyUI-Manager] Failed to perform initial fetching '{filename}': {e}"
|
|
)
|
|
traceback.print_exc()
|
|
|
|
if (
|
|
core.get_config()["network_mode"] != "offline"
|
|
and not manager_util.is_manager_pip_package()
|
|
):
|
|
a = get_cache("custom-node-list.json")
|
|
b = get_cache("extension-node-map.json")
|
|
c = get_cache("model-list.json")
|
|
d = get_cache("alter-list.json")
|
|
e = get_cache("github-stats.json")
|
|
|
|
await asyncio.gather(a, b, c, d, e)
|
|
|
|
if core.get_config()["network_mode"] == "private":
|
|
logging.info(
|
|
"[ComfyUI-Manager] The private comfyregistry is not yet supported in `network_mode=private`."
|
|
)
|
|
else:
|
|
# load at least once
|
|
await core.unified_manager.reload("remote", dont_wait=False)
|
|
await core.unified_manager.get_custom_nodes(channel_url, "remote")
|
|
else:
|
|
await core.unified_manager.reload(
|
|
"remote", dont_wait=False, update_cnr_map=False
|
|
)
|
|
|
|
logging.info("[ComfyUI-Manager] All startup tasks have been completed.")
|
|
|
|
|
|
threading.Thread(target=lambda: asyncio.run(default_cache_update())).start()
|
|
|
|
if not os.path.exists(context.manager_config_path):
|
|
core.get_config()
|
|
core.write_config()
|
|
|
|
|
|
cm_global.register_extension(
|
|
"ComfyUI-Manager",
|
|
{
|
|
"version": core.version,
|
|
"name": "ComfyUI Manager",
|
|
"nodes": {},
|
|
"description": "This extension provides the ability to manage custom nodes in ComfyUI.",
|
|
},
|
|
)
|