From 49549ddcb83e4febe8ce2f2f2a41090c71b81980 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sun, 8 Jun 2025 01:18:14 -0700 Subject: [PATCH] [feat] Implement comprehensive batch tracking and OpenAPI-driven data models Enhances ComfyUI Manager with robust batch execution tracking and unified data model architecture: - Implemented automatic batch history serialization with before/after system state snapshots - Added comprehensive state management capturing installed nodes, models, and ComfyUI version info - Enhanced task queue with proper client ID handling and WebSocket notifications - Migrated all data models to OpenAPI-generated Pydantic models for consistency - Added documentation for new TaskQueue methods (done_count, total_count, finalize) - Fixed 64 linting errors with proper imports and code cleanup Technical improvements: - All models now auto-generated from openapi.yaml ensuring API/implementation consistency - Batch tracking captures complete system state at operation start and completion - Enhanced REST endpoints with comprehensive documentation - Removed manual model files in favor of single source of truth - Added helper methods for system state capture and batch lifecycle management --- comfyui_manager/data_models/README.md | 67 + comfyui_manager/data_models/__init__.py | 85 +- .../data_models/generated_models.py | 417 ++++++ comfyui_manager/data_models/task_queue.py | 69 - comfyui_manager/glob/manager_server.py | 377 +++-- openapi.yaml | 1293 +++++++++++------ 6 files changed, 1679 insertions(+), 629 deletions(-) create mode 100644 comfyui_manager/data_models/README.md create mode 100644 comfyui_manager/data_models/generated_models.py delete mode 100644 comfyui_manager/data_models/task_queue.py diff --git a/comfyui_manager/data_models/README.md b/comfyui_manager/data_models/README.md new file mode 100644 index 00000000..adf840f0 --- /dev/null +++ b/comfyui_manager/data_models/README.md @@ -0,0 +1,67 @@ +# Data Models + +This directory contains Pydantic models for ComfyUI Manager, providing type safety, validation, and serialization for the API and internal data structures. + +## Overview + +- `generated_models.py` - All models auto-generated from OpenAPI spec +- `__init__.py` - Package exports for all models + +**Note**: All models are now auto-generated from the OpenAPI specification. Manual model files (`task_queue.py`, `state_management.py`) have been deprecated in favor of a single source of truth. + +## Generating Types from OpenAPI + +The state management models are automatically generated from the OpenAPI specification using `datamodel-codegen`. This ensures type safety and consistency between the API specification and the Python code. + +### Prerequisites + +Install the code generator: +```bash +pipx install datamodel-code-generator +``` + +### Generation Command + +To regenerate all models after updating the OpenAPI spec: + +```bash +datamodel-codegen \ + --use-subclass-enum \ + --field-constraints \ + --strict-types bytes \ + --input openapi.yaml \ + --output comfyui_manager/data_models/generated_models.py \ + --output-model-type pydantic_v2.BaseModel +``` + +### When to Regenerate + +You should regenerate the models when: + +1. **Adding new API endpoints** that return new data structures +2. **Modifying existing schemas** in the OpenAPI specification +3. **Adding new state management features** that require new models + +### Important Notes + +- **Single source of truth**: All models are now generated from `openapi.yaml` +- **No manual models**: All previously manual models have been migrated to the OpenAPI spec +- **OpenAPI requirements**: New schemas must be referenced in API paths to be generated by datamodel-codegen +- **Validation**: Always validate the OpenAPI spec before generation: + ```bash + python3 -c "import yaml; yaml.safe_load(open('openapi.yaml'))" + ``` + +### Example: Adding New State Models + +1. Add your schema to `openapi.yaml` under `components/schemas/` +2. Reference the schema in an API endpoint response +3. Run the generation command above +4. Update `__init__.py` to export the new models +5. Import and use the models in your code + +### Troubleshooting + +- **Models not generated**: Ensure schemas are under `components/schemas/` (not `parameters/`) +- **Missing models**: Verify schemas are referenced in at least one API path +- **Import errors**: Check that new models are added to `__init__.py` exports \ No newline at end of file diff --git a/comfyui_manager/data_models/__init__.py b/comfyui_manager/data_models/__init__.py index bd7e54e7..b7163321 100644 --- a/comfyui_manager/data_models/__init__.py +++ b/comfyui_manager/data_models/__init__.py @@ -3,24 +3,105 @@ Data models for ComfyUI Manager. This package contains Pydantic models used throughout the ComfyUI Manager for data validation, serialization, and type safety. + +All models are auto-generated from the OpenAPI specification to ensure +consistency between the API and implementation. """ -from .task_queue import ( +from .generated_models import ( + # Core Task Queue Models QueueTaskItem, TaskHistoryItem, TaskStateMessage, + TaskExecutionStatus, + + # WebSocket Message Models MessageTaskDone, MessageTaskStarted, + MessageTaskFailed, MessageUpdate, ManagerMessageName, + + # State Management Models + BatchExecutionRecord, + ComfyUISystemState, + BatchOperation, + InstalledNodeInfo, + InstalledModelInfo, + ComfyUIVersionInfo, + + # Other models + Kind, + StatusStr, + ManagerPackInfo, + ManagerPackInstalled, + SelectedVersion, + ManagerChannel, + ManagerDatabaseSource, + ManagerPackState, + ManagerPackInstallType, + ManagerPack, + InstallPackParams, + UpdateAllPacksParams, + QueueStatus, + ManagerMappings, + ModelMetadata, + NodePackageMetadata, + SnapshotItem, + Error, + InstalledPacksResponse, + HistoryResponse, + HistoryListResponse, + InstallType, + OperationType, + Result, ) __all__ = [ + # Core Task Queue Models "QueueTaskItem", "TaskHistoryItem", "TaskStateMessage", + "TaskExecutionStatus", + + # WebSocket Message Models "MessageTaskDone", "MessageTaskStarted", + "MessageTaskFailed", "MessageUpdate", "ManagerMessageName", -] + + # State Management Models + "BatchExecutionRecord", + "ComfyUISystemState", + "BatchOperation", + "InstalledNodeInfo", + "InstalledModelInfo", + "ComfyUIVersionInfo", + + # Other models + "Kind", + "StatusStr", + "ManagerPackInfo", + "ManagerPackInstalled", + "SelectedVersion", + "ManagerChannel", + "ManagerDatabaseSource", + "ManagerPackState", + "ManagerPackInstallType", + "ManagerPack", + "InstallPackParams", + "UpdateAllPacksParams", + "QueueStatus", + "ManagerMappings", + "ModelMetadata", + "NodePackageMetadata", + "SnapshotItem", + "Error", + "InstalledPacksResponse", + "HistoryResponse", + "HistoryListResponse", + "InstallType", + "OperationType", + "Result", +] \ No newline at end of file diff --git a/comfyui_manager/data_models/generated_models.py b/comfyui_manager/data_models/generated_models.py new file mode 100644 index 00000000..c4ae65ff --- /dev/null +++ b/comfyui_manager/data_models/generated_models.py @@ -0,0 +1,417 @@ +# generated by datamodel-codegen: +# filename: openapi.yaml +# timestamp: 2025-06-08T08:07:38+00:00 + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Any, Dict, List, Optional, Union + +from pydantic import BaseModel, Field, RootModel + + +class Kind(str, Enum): + install = 'install' + uninstall = 'uninstall' + update = 'update' + update_all = 'update-all' + update_comfyui = 'update-comfyui' + fix = 'fix' + disable = 'disable' + enable = 'enable' + install_model = 'install-model' + + +class QueueTaskItem(BaseModel): + ui_id: str = Field(..., description='Unique identifier for the task') + client_id: str = Field(..., description='Client identifier that initiated the task') + kind: Kind = Field(..., description='Type of task being performed') + + +class StatusStr(str, Enum): + success = 'success' + error = 'error' + skip = 'skip' + + +class TaskExecutionStatus(BaseModel): + status_str: StatusStr = Field(..., description='Overall task execution status') + completed: bool = Field(..., description='Whether the task completed') + messages: List[str] = Field(..., description='Additional status messages') + + +class ManagerMessageName(str, Enum): + cm_task_completed = 'cm-task-completed' + cm_task_started = 'cm-task-started' + cm_queue_status = 'cm-queue-status' + + +class ManagerPackInfo(BaseModel): + id: str = Field( + ..., + description='Either github-author/github-repo or name of pack from the registry', + ) + version: str = Field(..., description='Semantic version or Git commit hash') + ui_id: Optional[str] = Field(None, description='Task ID - generated internally') + + +class ManagerPackInstalled(BaseModel): + ver: str = Field( + ..., + description='The version of the pack that is installed (Git commit hash or semantic version)', + ) + cnr_id: Optional[str] = Field( + None, description='The name of the pack if installed from the registry' + ) + aux_id: Optional[str] = Field( + None, + description='The name of the pack if installed from github (author/repo-name format)', + ) + enabled: bool = Field(..., description='Whether the pack is enabled') + + +class SelectedVersion(str, Enum): + latest = 'latest' + nightly = 'nightly' + + +class ManagerChannel(str, Enum): + default = 'default' + recent = 'recent' + legacy = 'legacy' + forked = 'forked' + dev = 'dev' + tutorial = 'tutorial' + + +class ManagerDatabaseSource(str, Enum): + remote = 'remote' + local = 'local' + cache = 'cache' + + +class ManagerPackState(str, Enum): + installed = 'installed' + disabled = 'disabled' + not_installed = 'not_installed' + import_failed = 'import_failed' + needs_update = 'needs_update' + + +class ManagerPackInstallType(str, Enum): + git_clone = 'git-clone' + copy = 'copy' + cnr = 'cnr' + + +class UpdateState(str, Enum): + false = 'false' + true = 'true' + + +class ManagerPack(ManagerPackInfo): + author: Optional[str] = Field( + None, description="Pack author name or 'Unclaimed' if added via GitHub crawl" + ) + files: Optional[List[str]] = Field(None, description='Files included in the pack') + reference: Optional[str] = Field( + None, description='The type of installation reference' + ) + title: Optional[str] = Field(None, description='The display name of the pack') + cnr_latest: Optional[SelectedVersion] = None + repository: Optional[str] = Field(None, description='GitHub repository URL') + state: Optional[ManagerPackState] = None + update_state: Optional[UpdateState] = Field( + None, alias='update-state', description='Update availability status' + ) + stars: Optional[int] = Field(None, description='GitHub stars count') + last_update: Optional[datetime] = Field(None, description='Last update timestamp') + health: Optional[str] = Field(None, description='Health status of the pack') + description: Optional[str] = Field(None, description='Pack description') + trust: Optional[bool] = Field(None, description='Whether the pack is trusted') + install_type: Optional[ManagerPackInstallType] = None + + +class InstallPackParams(ManagerPackInfo): + selected_version: Union[str, SelectedVersion] = Field( + ..., description='Semantic version, Git commit hash, latest, or nightly' + ) + repository: Optional[str] = Field( + None, + description='GitHub repository URL (required if selected_version is nightly)', + ) + pip: Optional[List[str]] = Field(None, description='PyPi dependency names') + mode: ManagerDatabaseSource + channel: ManagerChannel + skip_post_install: Optional[bool] = Field( + None, description='Whether to skip post-installation steps' + ) + + +class UpdateAllPacksParams(BaseModel): + mode: Optional[ManagerDatabaseSource] = None + ui_id: Optional[str] = Field(None, description='Task ID - generated internally') + + +class QueueStatus(BaseModel): + total_count: int = Field( + ..., description='Total number of tasks (pending + running)' + ) + done_count: int = Field(..., description='Number of completed tasks') + in_progress_count: int = Field(..., description='Number of tasks currently running') + pending_count: Optional[int] = Field( + None, description='Number of tasks waiting to be executed' + ) + is_processing: bool = Field(..., description='Whether the task worker is active') + client_id: Optional[str] = Field( + None, description='Client ID (when filtered by client)' + ) + + +class ManagerMapping(BaseModel): + title_aux: Optional[str] = Field(None, description='The display name of the pack') + + +class ManagerMappings( + RootModel[Optional[Dict[str, List[Union[List[str], ManagerMapping]]]]] +): + root: Optional[Dict[str, List[Union[List[str], ManagerMapping]]]] = None + + +class ModelMetadata(BaseModel): + name: str = Field(..., description='Name of the model') + type: str = Field(..., description='Type of model') + base: Optional[str] = Field(None, description='Base model type') + save_path: Optional[str] = Field(None, description='Path for saving the model') + url: str = Field(..., description='Download URL') + filename: str = Field(..., description='Target filename') + ui_id: Optional[str] = Field(None, description='ID for UI reference') + + +class InstallType(str, Enum): + git = 'git' + copy = 'copy' + pip = 'pip' + + +class NodePackageMetadata(BaseModel): + title: Optional[str] = Field(None, description='Display name of the node package') + name: Optional[str] = Field(None, description='Repository/package name') + files: Optional[List[str]] = Field(None, description='Source URLs for the package') + description: Optional[str] = Field( + None, description='Description of the node package functionality' + ) + install_type: Optional[InstallType] = Field(None, description='Installation method') + version: Optional[str] = Field(None, description='Version identifier') + id: Optional[str] = Field( + None, description='Unique identifier for the node package' + ) + ui_id: Optional[str] = Field(None, description='ID for UI reference') + channel: Optional[str] = Field(None, description='Source channel') + mode: Optional[str] = Field(None, description='Source mode') + + +class SnapshotItem(RootModel[str]): + root: str = Field(..., description='Name of the snapshot') + + +class Error(BaseModel): + error: str = Field(..., description='Error message') + + +class InstalledPacksResponse(RootModel[Optional[Dict[str, ManagerPackInstalled]]]): + root: Optional[Dict[str, ManagerPackInstalled]] = None + + +class HistoryListResponse(BaseModel): + ids: Optional[List[str]] = Field( + None, description='List of available batch history IDs' + ) + + +class InstalledNodeInfo(BaseModel): + name: str = Field(..., description='Node package name') + version: str = Field(..., description='Installed version') + repository_url: Optional[str] = Field(None, description='Git repository URL') + install_method: str = Field( + ..., description='Installation method (cnr, git, pip, etc.)' + ) + enabled: Optional[bool] = Field( + True, description='Whether the node is currently enabled' + ) + install_date: Optional[datetime] = Field( + None, description='ISO timestamp of installation' + ) + + +class InstalledModelInfo(BaseModel): + name: str = Field(..., description='Model filename') + path: str = Field(..., description='Full path to model file') + type: str = Field(..., description='Model type (checkpoint, lora, vae, etc.)') + size_bytes: Optional[int] = Field(None, description='File size in bytes', ge=0) + hash: Optional[str] = Field(None, description='Model file hash for verification') + install_date: Optional[datetime] = Field( + None, description='ISO timestamp when added' + ) + + +class ComfyUIVersionInfo(BaseModel): + version: str = Field(..., description='ComfyUI version string') + commit_hash: Optional[str] = Field(None, description='Git commit hash') + branch: Optional[str] = Field(None, description='Git branch name') + is_stable: Optional[bool] = Field( + False, description='Whether this is a stable release' + ) + last_updated: Optional[datetime] = Field( + None, description='ISO timestamp of last update' + ) + + +class OperationType(str, Enum): + install = 'install' + update = 'update' + uninstall = 'uninstall' + fix = 'fix' + disable = 'disable' + enable = 'enable' + install_model = 'install-model' + + +class Result(str, Enum): + success = 'success' + failed = 'failed' + skipped = 'skipped' + + +class BatchOperation(BaseModel): + operation_id: str = Field(..., description='Unique operation identifier') + operation_type: OperationType = Field(..., description='Type of operation') + target: str = Field( + ..., description='Target of the operation (node name, model name, etc.)' + ) + target_version: Optional[str] = Field( + None, description='Target version for the operation' + ) + result: Result = Field(..., description='Operation result') + error_message: Optional[str] = Field( + None, description='Error message if operation failed' + ) + start_time: datetime = Field( + ..., description='ISO timestamp when operation started' + ) + end_time: Optional[datetime] = Field( + None, description='ISO timestamp when operation completed' + ) + client_id: Optional[str] = Field( + None, description='Client that initiated the operation' + ) + + +class ComfyUISystemState(BaseModel): + snapshot_time: datetime = Field( + ..., description='ISO timestamp when snapshot was taken' + ) + comfyui_version: ComfyUIVersionInfo + frontend_version: Optional[str] = Field( + None, description='ComfyUI frontend version if available' + ) + python_version: str = Field(..., description='Python interpreter version') + platform_info: str = Field( + ..., description='Operating system and platform information' + ) + installed_nodes: Optional[Dict[str, InstalledNodeInfo]] = Field( + None, description='Map of installed node packages by name' + ) + installed_models: Optional[Dict[str, InstalledModelInfo]] = Field( + None, description='Map of installed models by name' + ) + manager_config: Optional[Dict[str, Any]] = Field( + None, description='ComfyUI Manager configuration settings' + ) + + +class BatchExecutionRecord(BaseModel): + batch_id: str = Field(..., description='Unique batch identifier') + start_time: datetime = Field(..., description='ISO timestamp when batch started') + end_time: Optional[datetime] = Field( + None, description='ISO timestamp when batch completed' + ) + state_before: ComfyUISystemState + state_after: Optional[ComfyUISystemState] = Field( + None, description='System state after batch execution' + ) + operations: Optional[List[BatchOperation]] = Field( + None, description='List of operations performed in this batch' + ) + total_operations: Optional[int] = Field( + 0, description='Total number of operations in batch', ge=0 + ) + successful_operations: Optional[int] = Field( + 0, description='Number of successful operations', ge=0 + ) + failed_operations: Optional[int] = Field( + 0, description='Number of failed operations', ge=0 + ) + skipped_operations: Optional[int] = Field( + 0, description='Number of skipped operations', ge=0 + ) + + +class TaskHistoryItem(BaseModel): + ui_id: str = Field(..., description='Unique identifier for the task') + client_id: str = Field(..., description='Client identifier that initiated the task') + kind: str = Field(..., description='Type of task that was performed') + timestamp: datetime = Field(..., description='ISO timestamp when task completed') + result: str = Field(..., description='Task result message or details') + status: Optional[TaskExecutionStatus] = None + + +class TaskStateMessage(BaseModel): + history: Dict[str, TaskHistoryItem] = Field( + ..., description='Map of task IDs to their history items' + ) + running_queue: List[QueueTaskItem] = Field( + ..., description='Currently executing tasks' + ) + pending_queue: List[QueueTaskItem] = Field( + ..., description='Tasks waiting to be executed' + ) + + +class MessageTaskDone(BaseModel): + ui_id: str = Field(..., description='Task identifier') + result: str = Field(..., description='Task result message') + kind: str = Field(..., description='Type of task') + status: Optional[TaskExecutionStatus] = None + timestamp: datetime = Field(..., description='ISO timestamp when task completed') + state: TaskStateMessage + + +class MessageTaskStarted(BaseModel): + ui_id: str = Field(..., description='Task identifier') + kind: str = Field(..., description='Type of task') + timestamp: datetime = Field(..., description='ISO timestamp when task started') + state: TaskStateMessage + + +class MessageTaskFailed(BaseModel): + ui_id: str = Field(..., description='Task identifier') + error: str = Field(..., description='Error message') + kind: str = Field(..., description='Type of task') + timestamp: datetime = Field(..., description='ISO timestamp when task failed') + state: TaskStateMessage + + +class MessageUpdate( + RootModel[Union[MessageTaskDone, MessageTaskStarted, MessageTaskFailed]] +): + root: Union[MessageTaskDone, MessageTaskStarted, MessageTaskFailed] = Field( + ..., description='Union type for all possible WebSocket message updates' + ) + + +class HistoryResponse(BaseModel): + history: Optional[Dict[str, TaskHistoryItem]] = Field( + None, description='Map of task IDs to their history items' + ) diff --git a/comfyui_manager/data_models/task_queue.py b/comfyui_manager/data_models/task_queue.py deleted file mode 100644 index 0c611faf..00000000 --- a/comfyui_manager/data_models/task_queue.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Task queue data models for ComfyUI Manager. - -Contains Pydantic models for task queue management, WebSocket messaging, -and task state tracking. -""" - -from typing import Optional, Union, Dict -from enum import Enum -from pydantic import BaseModel - - -class QueueTaskItem(BaseModel): - """Represents a task item in the queue.""" - - ui_id: str - client_id: str - kind: str - - -class TaskHistoryItem(BaseModel): - """Represents a completed task in the history.""" - - ui_id: str - client_id: str - kind: str - timestamp: str - result: str - status: Optional[dict] = None - - -class TaskStateMessage(BaseModel): - """Current state of the task queue system.""" - - history: Dict[str, TaskHistoryItem] - running_queue: list[QueueTaskItem] - pending_queue: list[QueueTaskItem] - - -class MessageTaskDone(BaseModel): - """WebSocket message sent when a task completes.""" - - ui_id: str - result: str - kind: str - status: Optional[dict] - timestamp: str - state: TaskStateMessage - - -class MessageTaskStarted(BaseModel): - """WebSocket message sent when a task starts.""" - - ui_id: str - kind: str - timestamp: str - state: TaskStateMessage - - -# Union type for all possible WebSocket message updates -MessageUpdate = Union[MessageTaskDone, MessageTaskStarted] - - -class ManagerMessageName(Enum): - """WebSocket message type constants.""" - - TASK_DONE = "cm-task-completed" - TASK_STARTED = "cm-task-started" - STATUS = "cm-queue-status" diff --git a/comfyui_manager/glob/manager_server.py b/comfyui_manager/glob/manager_server.py index e062f7f6..1bafcce5 100644 --- a/comfyui_manager/glob/manager_server.py +++ b/comfyui_manager/glob/manager_server.py @@ -1,42 +1,39 @@ 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 git import uuid from datetime import datetime import heapq import copy -from typing import NamedTuple, List, Literal, Optional, Union -from enum import Enum +from typing import NamedTuple, List, Literal, Optional from comfy.cli_args import args import latent_preview from aiohttp import web -import aiohttp import json import zipfile import urllib.request from comfyui_manager.glob.utils import ( - environment_utils, + formatting_utils, model_utils, security_utils, - formatting_utils, + node_pack_utils, + environment_utils, ) from server import PromptServer import logging import asyncio -from collections import deque from . import manager_core as core from ..common import manager_util @@ -44,8 +41,6 @@ from ..common import cm_global from ..common import manager_downloader from ..common import context -from pydantic import BaseModel -import heapq from ..data_models import ( QueueTaskItem, @@ -55,8 +50,30 @@ from ..data_models import ( MessageTaskStarted, MessageUpdate, ManagerMessageName, + BatchExecutionRecord, + ComfyUISystemState, + BatchOperation, + InstalledNodeInfo, + InstalledModelInfo, + ComfyUIVersionInfo, ) +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" @@ -135,7 +152,9 @@ class TaskQueue: self.running_tasks = {} self.history_tasks = {} self.task_counter = 0 - self.batch_id = 0 + self.batch_id = None + self.batch_start_time = None + self.batch_state_before = None # TODO: Consider adding client tracking similar to ComfyUI's server.client_id # to track which client is currently executing for better session management @@ -154,9 +173,11 @@ class TaskQueue: ) @staticmethod - def send_queue_state_update(msg: str, update: MessageUpdate, client_id: Optional[str] = None) -> None: + 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 @@ -167,8 +188,19 @@ class TaskQueue: def put(self, item: QueueTaskItem) -> None: 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() + 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 @@ -190,7 +222,9 @@ class TaskQueue: timestamp=datetime.now().isoformat(), state=self.get_current_state(), ), - client_id=item["client_id"] # Send task started only to the client that requested it + client_id=item[ + "client_id" + ], # Send task started only to the client that requested it ) return item, task_index @@ -234,7 +268,9 @@ class TaskQueue: timestamp=timestamp, state=self.get_current_state(), ), - client_id=item["client_id"] # Send completion only to the client that requested it + client_id=item[ + "client_id" + ], # Send completion only to the client that requested it ) def get_current_queue(self) -> tuple[list[QueueTaskItem], list[QueueTaskItem]]: @@ -286,7 +322,7 @@ class TaskQueue: 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). @@ -295,7 +331,7 @@ class TaskQueue: 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). @@ -308,21 +344,142 @@ class TaskQueue: 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. - - Note: Currently incomplete - requires implementation of state management models. """ if self.batch_id is not None: batch_path = os.path.join( context.manager_batch_history_path, self.batch_id + ".json" ) - # TODO: create a pydantic model for state of ComfyUI (installed nodes, models, ComfyUI version, ComfyUI frontend version) + the operations that occurred in the batch. Then add a serialization method that can work nicely for saving to json file. Finally, add post creation validation methods on the pydantic model. Then, anytime the queue goes from full to completely empty (also none running) -> run this finalize to save the snapshot. - # Add logic here to instanitation model then save below using the serialization methodd of the object - # with open(batch_path, "w") as json_file: - # json.dump(json_obj, json_file, indent=4) + + 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() @@ -374,7 +531,7 @@ async def task_worker(): return "success" except Exception: traceback.print_exc() - return f"Installation failed:\n{node_spec_str}" + return "Installation failed" async def do_enable(item) -> str: cnr_id = item.get("cnr_id") @@ -507,7 +664,7 @@ async def task_worker(): async def do_install_model(item) -> str: json_data = item.get("json_data") - model_path = get_model_path(json_data) + model_path = model_utils.get_model_path(json_data) model_url = json_data.get("url") res = False @@ -541,7 +698,7 @@ async def task_worker(): or model_url.startswith("https://huggingface.co") or model_url.startswith("https://heibox.uni-heidelberg.de") ): - model_dir = get_model_dir(json_data, True) + 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) @@ -575,18 +732,26 @@ async def task_worker(): timeout = 4096 task = task_queue.get(timeout) if task is None: - logging.info("\n[ComfyUI-Manager] All tasks are completed.") - logging.info("\nAfter restarting ComfyUI, please refresh the browser.") + # 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"} + res = {"status": "all-done"} - # Broadcast general status updates to all clients - PromptServer.instance.send_sync("cm-queue-status", res) + # Broadcast general status updates to all clients + PromptServer.instance.send_sync("cm-queue-status", res) return item, task_index = task - ui_id = item["ui_id"] kind = item["kind"] print(f"Processing task: {kind} with item: {item} at index: {task_index}") @@ -616,7 +781,9 @@ async def task_worker(): msg = "Unexpected kind: " + kind except Exception: msg = f"Exception: {(kind, item)}" - task_queue.task_done(item, msg, TaskQueue.ExecutionStatus("error", True, [msg])) + 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: @@ -638,13 +805,13 @@ async def task_worker(): @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 """ @@ -657,10 +824,10 @@ async def queue_task(request) -> web.Response: @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 """ @@ -686,14 +853,14 @@ async def get_history_list(request) -> web.Response: @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 """ @@ -707,32 +874,33 @@ async def get_history(request): 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 + 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: @@ -757,7 +925,7 @@ async def fetch_customnode_mappings(request): json_obj = core.map_to_unified_keys(json_obj) if nickname_mode: - json_obj = nickname_filter(json_obj) + json_obj = node_pack_utils.nickname_filter(json_obj) all_nodes = set() patterns = [] @@ -813,7 +981,7 @@ async def update_all(request): async def _update_all(json_data): - if not is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response(status=403) @@ -1005,7 +1173,7 @@ async def get_snapshot_list(request): @routes.get("/v2/snapshot/remove") async def remove_snapshot(request): - if not is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response(status=403) @@ -1023,7 +1191,7 @@ async def remove_snapshot(request): @routes.get("/v2/snapshot/restore") async def restore_snapshot(request): - if not is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response(status=403) @@ -1116,8 +1284,8 @@ async def import_fail_info(request): @routes.post("/v2/manager/queue/reinstall") async def reinstall_custom_node(request): - await uninstall_custom_node(request) - await install_custom_node(request) + await _uninstall_custom_node(await request.json()) + await _install_custom_node(await request.json()) @routes.get("/v2/manager/queue/reset") @@ -1128,58 +1296,68 @@ async def reset_queue(request): @routes.get("/v2/manager/queue/abort_current") async def abort_queue(request): - task_queue.abort() + # 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() + 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 + 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 + 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(), - }) + + 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(), - }) + 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 is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response( status=403, @@ -1235,14 +1413,14 @@ async def _install_custom_node(json_data): # 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 get_risky_level(git_url, json_data.get("pip", [])) + 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 is_allowed_security_level(risky_level): + if not security_utils.is_allowed_security_level(risky_level): logging.error(SECURITY_MESSAGE_GENERAL) return web.Response( status=404, @@ -1263,15 +1441,17 @@ async def _install_custom_node(json_data): 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 @@ -1281,16 +1461,11 @@ def _queue_start(): return web.Response(status=200) -@routes.get("/v2/manager/queue/start") -async def queue_start(request): - _queue_start() - # with task_worker_lock: - # finalize_temp_queue_batch() - # return _queue_start() +# Duplicate queue_start function removed - using the earlier one with proper implementation async def _fix_custom_node(json_data): - if not is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_GENERAL) return web.Response( status=403, @@ -1313,7 +1488,7 @@ async def _fix_custom_node(json_data): @routes.post("/v2/customnode/install/git_url") async def install_custom_node_git_url(request): - if not is_allowed_security_level("high"): + if not security_utils.is_allowed_security_level("high"): logging.error(SECURITY_MESSAGE_NORMAL_MINUS) return web.Response(status=403) @@ -1333,7 +1508,7 @@ async def install_custom_node_git_url(request): @routes.post("/v2/customnode/install/pip") async def install_custom_node_pip(request): - if not is_allowed_security_level("high"): + if not security_utils.is_allowed_security_level("high"): logging.error(SECURITY_MESSAGE_NORMAL_MINUS) return web.Response(status=403) @@ -1344,7 +1519,7 @@ async def install_custom_node_pip(request): async def _uninstall_custom_node(json_data): - if not is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response( status=403, @@ -1367,7 +1542,7 @@ async def _uninstall_custom_node(json_data): async def _update_custom_node(json_data): - if not is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response( status=403, @@ -1464,10 +1639,10 @@ async def check_whitelist_for_model(item): async def install_model(request): json_data = await request.json() return await _install_model(json_data) - + async def _install_model(json_data): - if not is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response( status=403, @@ -1485,7 +1660,7 @@ async def _install_model(json_data): if not json_data["filename"].endswith( ".safetensors" - ) and not is_allowed_security_level("high"): + ) 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 @@ -1510,7 +1685,7 @@ async def _install_model(json_data): @routes.get("/v2/manager/preview_method") async def preview_method(request): if "value" in request.rel_url.query: - set_preview_method(request.rel_url.query["value"]) + environment_utils.set_preview_method(request.rel_url.query["value"]) core.write_config() else: return web.Response( @@ -1523,7 +1698,7 @@ async def preview_method(request): @routes.get("/v2/manager/db_mode") async def db_mode(request): if "value" in request.rel_url.query: - set_db_mode(request.rel_url.query["value"]) + 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) @@ -1534,7 +1709,7 @@ async def db_mode(request): @routes.get("/v2/manager/policy/update") async def update_policy(request): if "value" in request.rel_url.query: - set_update_policy(request.rel_url.query["value"]) + 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) @@ -1567,7 +1742,7 @@ async def channel_url_list(request): @routes.get("/v2/manager/reboot") def restart(self): - if not is_allowed_security_level("middle"): + if not security_utils.is_allowed_security_level("middle"): logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response(status=403) diff --git a/openapi.yaml b/openapi.yaml index d79b79ec..6222d47a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -15,13 +15,368 @@ servers: # Common API components components: schemas: - Error: + # Core Task Queue Models + QueueTaskItem: type: object properties: + ui_id: + type: string + description: Unique identifier for the task + client_id: + type: string + description: Client identifier that initiated the task + kind: + type: string + description: Type of task being performed + enum: [install, uninstall, update, update-all, update-comfyui, fix, disable, enable, install-model] + required: [ui_id, client_id, kind] + + TaskHistoryItem: + type: object + properties: + ui_id: + type: string + description: Unique identifier for the task + client_id: + type: string + description: Client identifier that initiated the task + kind: + type: string + description: Type of task that was performed + timestamp: + type: string + format: date-time + description: ISO timestamp when task completed + result: + type: string + description: Task result message or details + status: + $ref: '#/components/schemas/TaskExecutionStatus' + required: [ui_id, client_id, kind, timestamp, result] + + TaskExecutionStatus: + type: object + properties: + status_str: + type: string + enum: [success, error, skip] + description: Overall task execution status + completed: + type: boolean + description: Whether the task completed + messages: + type: array + items: + type: string + description: Additional status messages + required: [status_str, completed, messages] + + TaskStateMessage: + type: object + properties: + history: + type: object + additionalProperties: + $ref: '#/components/schemas/TaskHistoryItem' + description: Map of task IDs to their history items + running_queue: + type: array + items: + $ref: '#/components/schemas/QueueTaskItem' + description: Currently executing tasks + pending_queue: + type: array + items: + $ref: '#/components/schemas/QueueTaskItem' + description: Tasks waiting to be executed + required: [history, running_queue, pending_queue] + + # WebSocket Message Models + ManagerMessageName: + type: string + enum: [cm-task-completed, cm-task-started, cm-queue-status] + description: WebSocket message type constants for manager events + + MessageTaskDone: + type: object + properties: + ui_id: + type: string + description: Task identifier + result: + type: string + description: Task result message + kind: + type: string + description: Type of task + status: + $ref: '#/components/schemas/TaskExecutionStatus' + timestamp: + type: string + format: date-time + description: ISO timestamp when task completed + state: + $ref: '#/components/schemas/TaskStateMessage' + required: [ui_id, result, kind, timestamp, state] + + MessageTaskStarted: + type: object + properties: + ui_id: + type: string + description: Task identifier + kind: + type: string + description: Type of task + timestamp: + type: string + format: date-time + description: ISO timestamp when task started + state: + $ref: '#/components/schemas/TaskStateMessage' + required: [ui_id, kind, timestamp, state] + + MessageTaskFailed: + type: object + properties: + ui_id: + type: string + description: Task identifier error: type: string description: Error message - + kind: + type: string + description: Type of task + timestamp: + type: string + format: date-time + description: ISO timestamp when task failed + state: + $ref: '#/components/schemas/TaskStateMessage' + required: [ui_id, error, kind, timestamp, state] + + MessageUpdate: + oneOf: + - $ref: '#/components/schemas/MessageTaskDone' + - $ref: '#/components/schemas/MessageTaskStarted' + - $ref: '#/components/schemas/MessageTaskFailed' + description: Union type for all possible WebSocket message updates + + # Manager Package Models + ManagerPackInfo: + type: object + properties: + id: + type: string + description: Either github-author/github-repo or name of pack from the registry + version: + type: string + description: Semantic version or Git commit hash + ui_id: + type: string + description: Task ID - generated internally + required: [id, version] + + ManagerPackInstalled: + type: object + properties: + ver: + type: string + description: The version of the pack that is installed (Git commit hash or semantic version) + cnr_id: + type: string + nullable: true + description: The name of the pack if installed from the registry + aux_id: + type: string + nullable: true + description: The name of the pack if installed from github (author/repo-name format) + enabled: + type: boolean + description: Whether the pack is enabled + required: [ver, enabled] + + SelectedVersion: + type: string + enum: [latest, nightly] + description: Version selection for pack installation + + ManagerChannel: + type: string + enum: [default, recent, legacy, forked, dev, tutorial] + description: Channel for pack sources + + ManagerDatabaseSource: + type: string + enum: [remote, local, cache] + description: Source for pack information + + ManagerPackState: + type: string + enum: [installed, disabled, not_installed, import_failed, needs_update] + description: Current state of a pack + + ManagerPackInstallType: + type: string + enum: [git-clone, copy, cnr] + description: Type of installation used for the pack + + ManagerPack: + allOf: + - $ref: '#/components/schemas/ManagerPackInfo' + - type: object + properties: + author: + type: string + description: Pack author name or 'Unclaimed' if added via GitHub crawl + files: + type: array + items: + type: string + description: Files included in the pack + reference: + type: string + description: The type of installation reference + title: + type: string + description: The display name of the pack + cnr_latest: + $ref: '#/components/schemas/SelectedVersion' + repository: + type: string + description: GitHub repository URL + state: + $ref: '#/components/schemas/ManagerPackState' + update-state: + type: string + enum: ['false', 'true'] + nullable: true + description: Update availability status + stars: + type: integer + description: GitHub stars count + last_update: + type: string + format: date-time + description: Last update timestamp + health: + type: string + description: Health status of the pack + description: + type: string + description: Pack description + trust: + type: boolean + description: Whether the pack is trusted + install_type: + $ref: '#/components/schemas/ManagerPackInstallType' + + # Installation Parameters + InstallPackParams: + allOf: + - $ref: '#/components/schemas/ManagerPackInfo' + - type: object + properties: + selected_version: + oneOf: + - type: string + - $ref: '#/components/schemas/SelectedVersion' + description: Semantic version, Git commit hash, latest, or nightly + repository: + type: string + description: GitHub repository URL (required if selected_version is nightly) + pip: + type: array + items: + type: string + description: PyPi dependency names + mode: + $ref: '#/components/schemas/ManagerDatabaseSource' + channel: + $ref: '#/components/schemas/ManagerChannel' + skip_post_install: + type: boolean + description: Whether to skip post-installation steps + required: [selected_version, mode, channel] + + UpdateAllPacksParams: + type: object + properties: + mode: + $ref: '#/components/schemas/ManagerDatabaseSource' + ui_id: + type: string + description: Task ID - generated internally + + # Queue Status Models + QueueStatus: + type: object + properties: + total_count: + type: integer + description: Total number of tasks (pending + running) + done_count: + type: integer + description: Number of completed tasks + in_progress_count: + type: integer + description: Number of tasks currently running + pending_count: + type: integer + description: Number of tasks waiting to be executed + is_processing: + type: boolean + description: Whether the task worker is active + client_id: + type: string + description: Client ID (when filtered by client) + required: [total_count, done_count, in_progress_count, is_processing] + + # Mappings Model + ManagerMappings: + type: object + additionalProperties: + type: array + items: + - type: array + items: + type: string + description: List of ComfyNode names included in the pack + - type: object + properties: + title_aux: + type: string + description: The display name of the pack + + # Model Management + ModelMetadata: + type: object + properties: + name: + type: string + description: Name of the model + type: + type: string + description: Type of model + base: + type: string + description: Base model type + save_path: + type: string + description: Path for saving the model + url: + type: string + description: Download URL + filename: + type: string + description: Target filename + ui_id: + type: string + description: ID for UI reference + required: [name, type, url, filename] + + # Legacy Node Package Model (for backward compatibility) NodePackageMetadata: type: object properties: @@ -58,51 +413,249 @@ components: mode: type: string description: Source mode - - ModelMetadata: - type: object - properties: - name: - type: string - description: Name of the model - type: - type: string - description: Type of model - base: - type: string - description: Base model type - save_path: - type: string - description: Path for saving the model - url: - type: string - description: Download URL - filename: - type: string - description: Target filename - ui_id: - type: string - description: ID for UI reference - + + # Snapshot Models SnapshotItem: type: string description: Name of the snapshot - QueueStatus: + # Error Models + Error: type: object properties: - total_count: - type: integer - description: Total number of tasks - done_count: - type: integer - description: Number of completed tasks - in_progress_count: - type: integer - description: Number of tasks in progress - is_processing: + error: + type: string + description: Error message + required: [error] + + # Response Models + InstalledPacksResponse: + type: object + additionalProperties: + $ref: '#/components/schemas/ManagerPackInstalled' + description: Map of pack names to their installation info + + HistoryResponse: + type: object + properties: + history: + type: object + additionalProperties: + $ref: '#/components/schemas/TaskHistoryItem' + description: Map of task IDs to their history items + + HistoryListResponse: + type: object + properties: + ids: + type: array + items: + type: string + description: List of available batch history IDs + + # State Management Models + InstalledNodeInfo: + type: object + properties: + name: + type: string + description: Node package name + version: + type: string + description: Installed version + repository_url: + type: string + nullable: true + description: Git repository URL + install_method: + type: string + description: Installation method (cnr, git, pip, etc.) + enabled: type: boolean - description: Whether the queue is currently processing + description: Whether the node is currently enabled + default: true + install_date: + type: string + format: date-time + nullable: true + description: ISO timestamp of installation + required: [name, version, install_method] + + InstalledModelInfo: + type: object + properties: + name: + type: string + description: Model filename + path: + type: string + description: Full path to model file + type: + type: string + description: Model type (checkpoint, lora, vae, etc.) + size_bytes: + type: integer + nullable: true + description: File size in bytes + minimum: 0 + hash: + type: string + nullable: true + description: Model file hash for verification + install_date: + type: string + format: date-time + nullable: true + description: ISO timestamp when added + required: [name, path, type] + + ComfyUIVersionInfo: + type: object + properties: + version: + type: string + description: ComfyUI version string + commit_hash: + type: string + nullable: true + description: Git commit hash + branch: + type: string + nullable: true + description: Git branch name + is_stable: + type: boolean + description: Whether this is a stable release + default: false + last_updated: + type: string + format: date-time + nullable: true + description: ISO timestamp of last update + required: [version] + + BatchOperation: + type: object + properties: + operation_id: + type: string + description: Unique operation identifier + operation_type: + type: string + description: Type of operation + enum: [install, update, uninstall, fix, disable, enable, install-model] + target: + type: string + description: Target of the operation (node name, model name, etc.) + target_version: + type: string + nullable: true + description: Target version for the operation + result: + type: string + description: Operation result + enum: [success, failed, skipped] + error_message: + type: string + nullable: true + description: Error message if operation failed + start_time: + type: string + format: date-time + description: ISO timestamp when operation started + end_time: + type: string + format: date-time + nullable: true + description: ISO timestamp when operation completed + client_id: + type: string + nullable: true + description: Client that initiated the operation + required: [operation_id, operation_type, target, result, start_time] + + ComfyUISystemState: + type: object + properties: + snapshot_time: + type: string + format: date-time + description: ISO timestamp when snapshot was taken + comfyui_version: + $ref: '#/components/schemas/ComfyUIVersionInfo' + frontend_version: + type: string + nullable: true + description: ComfyUI frontend version if available + python_version: + type: string + description: Python interpreter version + platform_info: + type: string + description: Operating system and platform information + installed_nodes: + type: object + additionalProperties: + $ref: '#/components/schemas/InstalledNodeInfo' + description: Map of installed node packages by name + installed_models: + type: object + additionalProperties: + $ref: '#/components/schemas/InstalledModelInfo' + description: Map of installed models by name + manager_config: + type: object + additionalProperties: true + description: ComfyUI Manager configuration settings + required: [snapshot_time, comfyui_version, python_version, platform_info] + + BatchExecutionRecord: + type: object + properties: + batch_id: + type: string + description: Unique batch identifier + start_time: + type: string + format: date-time + description: ISO timestamp when batch started + end_time: + type: string + format: date-time + nullable: true + description: ISO timestamp when batch completed + state_before: + $ref: '#/components/schemas/ComfyUISystemState' + state_after: + $ref: '#/components/schemas/ComfyUISystemState' + nullable: true + description: System state after batch execution + operations: + type: array + items: + $ref: '#/components/schemas/BatchOperation' + description: List of operations performed in this batch + total_operations: + type: integer + description: Total number of operations in batch + minimum: 0 + default: 0 + successful_operations: + type: integer + description: Number of successful operations + minimum: 0 + default: 0 + failed_operations: + type: integer + description: Number of failed operations + minimum: 0 + default: 0 + skipped_operations: + type: integer + description: Number of skipped operations + minimum: 0 + default: 0 + required: [batch_id, start_time, state_before] securitySchemes: securityLevel: @@ -117,8 +670,7 @@ components: in: query description: Source mode (e.g., "local", "remote") schema: - type: string - enum: [local, remote, default] + $ref: '#/components/schemas/ManagerDatabaseSource' targetParam: name: target @@ -136,10 +688,194 @@ components: schema: type: string + clientIdParam: + name: client_id + in: query + description: Client ID for filtering tasks + schema: + type: string + + uiIdParam: + name: ui_id + in: query + description: Specific task ID to retrieve + schema: + type: string + + maxItemsParam: + name: max_items + in: query + description: Maximum number of items to return + schema: + type: integer + minimum: 1 + + offsetParam: + name: offset + in: query + description: Offset for pagination + schema: + type: integer + minimum: 0 + # API Paths paths: - # Custom Nodes Endpoints - /customnode/getmappings: + # Task Queue Management (v2 endpoints) + /v2/manager/queue/task: + post: + summary: Add task to queue + description: Adds a new task to the processing queue + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/QueueTaskItem' + responses: + '200': + description: Task queued successfully + + /v2/manager/queue/status: + get: + summary: Get queue status + description: Returns the current status of the operation queue with optional client filtering + parameters: + - $ref: '#/components/parameters/clientIdParam' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/QueueStatus' + + /v2/manager/queue/history: + get: + summary: Get task history + description: Get task history with optional filtering + parameters: + - name: id + in: query + description: Batch history ID (for file-based history) + schema: + type: string + - $ref: '#/components/parameters/clientIdParam' + - $ref: '#/components/parameters/uiIdParam' + - $ref: '#/components/parameters/maxItemsParam' + - $ref: '#/components/parameters/offsetParam' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/HistoryResponse' + - type: object # File-based batch history + '400': + description: Error retrieving history + + /v2/manager/queue/history_list: + get: + summary: Get available batch history files + description: Returns a list of batch history IDs sorted by modification time + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/HistoryListResponse' + '400': + description: Error retrieving history list + + /v2/manager/queue/batch/{batch_id}: + get: + summary: Get batch execution record + description: Returns detailed execution record for a specific batch including before/after state snapshots and all operations performed + parameters: + - name: batch_id + in: path + required: true + description: Unique batch identifier + schema: + type: string + responses: + '200': + description: Batch record retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/BatchExecutionRecord' + '404': + description: Batch not found + '400': + description: Error retrieving batch record + + /v2/manager/queue/start: + get: + summary: Start queue processing + description: Starts processing the operation queue + responses: + '200': + description: Processing started + '201': + description: Processing already in progress + + /v2/manager/queue/reset: + get: + summary: Reset queue + description: Resets the operation queue + responses: + '200': + description: Queue reset successfully + + /v2/manager/queue/update_all: + get: + summary: Update all custom nodes + description: Queues update operations for all installed custom nodes + security: + - securityLevel: [] + parameters: + - $ref: '#/components/parameters/modeParam' + responses: + '200': + description: Update queued successfully + '401': + description: Processing already in progress + '403': + description: Security policy violation + + /v2/manager/queue/update_comfyui: + get: + summary: Update ComfyUI + description: Queues an update operation for ComfyUI itself + responses: + '200': + description: Update queued successfully + + /v2/manager/queue/install_model: + post: + summary: Install model + description: Queues installation of a model + security: + - securityLevel: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ModelMetadata' + responses: + '200': + description: Installation queued successfully + '400': + description: Invalid model request + '403': + description: Security policy violation + + # Custom Nodes Endpoints (v2) + /v2/customnode/getmappings: get: summary: Get node-to-package mappings description: Provides unified mapping between nodes and node packages @@ -151,14 +887,9 @@ paths: content: application/json: schema: - type: object - additionalProperties: - type: array - items: - type: array - description: Mapping of node packages to node classes + $ref: '#/components/schemas/ManagerMappings' - /customnode/fetch_updates: + /v2/customnode/fetch_updates: get: summary: Check for updates description: Fetches updates for custom nodes @@ -172,7 +903,7 @@ paths: '400': description: Error occurred - /customnode/installed: + /v2/customnode/installed: get: summary: Get installed custom nodes description: Returns a list of installed node packages @@ -189,103 +920,9 @@ paths: content: application/json: schema: - type: object - additionalProperties: - $ref: '#/components/schemas/NodePackageMetadata' - - /customnode/getlist: - get: - summary: Get custom node list - description: Provides a list of available custom nodes - parameters: - - $ref: '#/components/parameters/modeParam' - - name: skip_update - in: query - description: Skip update check - schema: - type: boolean - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - properties: - channel: - type: string - node_packs: - type: object - additionalProperties: - $ref: '#/components/schemas/NodePackageMetadata' - - /customnode/alternatives: - get: - summary: Get alternative node options - description: Provides alternatives for nodes - parameters: - - $ref: '#/components/parameters/modeParam' - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - additionalProperties: - type: object - - /customnode/versions/{node_name}: - get: - summary: Get available versions for a node - description: Lists all available versions for a specific node - parameters: - - name: node_name - in: path - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: array - items: - type: object - properties: - version: - type: string - '400': - description: Node not found - - /customnode/disabled_versions/{node_name}: - get: - summary: Get disabled versions for a node - description: Lists all disabled versions for a specific node - parameters: - - name: node_name - in: path - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: array - items: - type: object - properties: - version: - type: string - '400': - description: Node not found - - /customnode/import_fail_info: + $ref: '#/components/schemas/InstalledPacksResponse' + + /v2/customnode/import_fail_info: post: summary: Get import failure information description: Returns information about why a node failed to import @@ -305,8 +942,8 @@ paths: description: Successful operation '400': description: No information available - - /customnode/install/git_url: + + /v2/customnode/install/git_url: post: summary: Install custom node via Git URL description: Installs a custom node from a Git repository URL @@ -325,8 +962,8 @@ paths: description: Installation failed '403': description: Security policy violation - - /customnode/install/pip: + + /v2/customnode/install/pip: post: summary: Install custom node dependencies via pip description: Installs Python package dependencies for custom nodes @@ -343,210 +980,9 @@ paths: description: Installation successful '403': description: Security policy violation - - # Model Management Endpoints - /externalmodel/getlist: - get: - summary: Get external model list - description: Provides a list of available external models - parameters: - - $ref: '#/components/parameters/modeParam' - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - properties: - models: - type: array - items: - $ref: '#/components/schemas/ModelMetadata' - - # Queue Management Endpoints - /manager/queue/update_all: - get: - summary: Update all custom nodes - description: Queues update operations for all installed custom nodes - security: - - securityLevel: [] - parameters: - - $ref: '#/components/parameters/modeParam' - responses: - '200': - description: Update queued successfully - '401': - description: Processing already in progress - '403': - description: Security policy violation - - /manager/queue/reset: - get: - summary: Reset queue - description: Resets the operation queue - responses: - '200': - description: Queue reset successfully - - /manager/queue/status: - get: - summary: Get queue status - description: Returns the current status of the operation queue - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/QueueStatus' - - /manager/queue/install: - post: - summary: Install custom node - description: Queues installation of a custom node - security: - - securityLevel: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NodePackageMetadata' - responses: - '200': - description: Installation queued successfully - '403': - description: Security policy violation - '404': - description: Target node not found or security issue - - /manager/queue/start: - get: - summary: Start queue processing - description: Starts processing the operation queue - responses: - '200': - description: Processing started - '201': - description: Processing already in progress - - /manager/queue/fix: - post: - summary: Fix custom node - description: Attempts to fix a broken custom node installation - security: - - securityLevel: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NodePackageMetadata' - responses: - '200': - description: Fix operation queued successfully - '403': - description: Security policy violation - - /manager/queue/reinstall: - post: - summary: Reinstall custom node - description: Uninstalls and then reinstalls a custom node - security: - - securityLevel: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NodePackageMetadata' - responses: - '200': - description: Reinstall operation queued successfully - '403': - description: Security policy violation - - /manager/queue/uninstall: - post: - summary: Uninstall custom node - description: Queues uninstallation of a custom node - security: - - securityLevel: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NodePackageMetadata' - responses: - '200': - description: Uninstallation queued successfully - '403': - description: Security policy violation - - /manager/queue/update: - post: - summary: Update custom node - description: Queues update of a custom node - security: - - securityLevel: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NodePackageMetadata' - responses: - '200': - description: Update queued successfully - '403': - description: Security policy violation - - /manager/queue/disable: - post: - summary: Disable custom node - description: Disables a custom node without uninstalling it - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NodePackageMetadata' - responses: - '200': - description: Disable operation queued successfully - - /manager/queue/update_comfyui: - get: - summary: Update ComfyUI - description: Queues an update operation for ComfyUI itself - responses: - '200': - description: Update queued successfully - - /manager/queue/install_model: - post: - summary: Install model - description: Queues installation of a model - security: - - securityLevel: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ModelMetadata' - responses: - '200': - description: Installation queued successfully - '400': - description: Invalid model request - '403': - description: Security policy violation - - # Snapshot Management Endpoints - /snapshot/getlist: + + # Snapshot Management Endpoints (v2) + /v2/snapshot/getlist: get: summary: Get snapshot list description: Returns a list of available snapshots @@ -562,8 +998,8 @@ paths: type: array items: $ref: '#/components/schemas/SnapshotItem' - - /snapshot/remove: + + /v2/snapshot/remove: get: summary: Remove snapshot description: Removes a specified snapshot @@ -578,8 +1014,8 @@ paths: description: Error removing snapshot '403': description: Security policy violation - - /snapshot/restore: + + /v2/snapshot/restore: get: summary: Restore snapshot description: Restores a specified snapshot @@ -594,8 +1030,8 @@ paths: description: Error restoring snapshot '403': description: Security policy violation - - /snapshot/get_current: + + /v2/snapshot/get_current: get: summary: Get current snapshot description: Returns the current system state as a snapshot @@ -608,8 +1044,8 @@ paths: type: object '400': description: Error creating snapshot - - /snapshot/save: + + /v2/snapshot/save: get: summary: Save snapshot description: Saves the current system state as a new snapshot @@ -618,9 +1054,9 @@ paths: description: Snapshot saved successfully '400': description: Error saving snapshot - - # ComfyUI Management Endpoints - /comfyui_manager/comfyui_versions: + + # ComfyUI Management Endpoints (v2) + /v2/comfyui_manager/comfyui_versions: get: summary: Get ComfyUI versions description: Returns available and current ComfyUI versions @@ -640,8 +1076,8 @@ paths: type: string '400': description: Error retrieving versions - - /comfyui_manager/comfyui_switch_version: + + /v2/comfyui_manager/comfyui_switch_version: get: summary: Switch ComfyUI version description: Switches to a specified ComfyUI version @@ -656,21 +1092,9 @@ paths: description: Version switch successful '400': description: Error switching version - - /manager/reboot: - get: - summary: Reboot ComfyUI - description: Restarts the ComfyUI server - security: - - securityLevel: [] - responses: - '200': - description: Reboot initiated - '403': - description: Security policy violation - - # Configuration Endpoints - /manager/preview_method: + + # Configuration Endpoints (v2) + /v2/manager/preview_method: get: summary: Get or set preview method description: Gets or sets the latent preview method @@ -689,8 +1113,8 @@ paths: text/plain: schema: type: string - - /manager/db_mode: + + /v2/manager/db_mode: get: summary: Get or set database mode description: Gets or sets the database mode @@ -709,27 +1133,8 @@ paths: text/plain: schema: type: string - - /manager/policy/component: - get: - summary: Get or set component policy - description: Gets or sets the component policy - parameters: - - name: value - in: query - required: false - description: New component policy - schema: - type: string - responses: - '200': - description: Setting updated or current value returned - content: - text/plain: - schema: - type: string - - /manager/policy/update: + + /v2/manager/policy/update: get: summary: Get or set update policy description: Gets or sets the update policy @@ -748,8 +1153,8 @@ paths: text/plain: schema: type: string - - /manager/channel_url_list: + + /v2/manager/channel_url_list: get: summary: Get or set channel URL description: Gets or sets the channel URL for custom node sources @@ -779,49 +1184,20 @@ paths: type: string url: type: string - - # Component Management Endpoints - /manager/component/save: - post: - summary: Save component - description: Saves a reusable workflow component - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - workflow: - type: object + + /v2/manager/reboot: + get: + summary: Reboot ComfyUI + description: Restarts the ComfyUI server + security: + - securityLevel: [] responses: '200': - description: Component saved successfully - content: - text/plain: - schema: - type: string - '400': - description: Error saving component - - /manager/component/loads: - post: - summary: Load components - description: Loads all available workflow components - responses: - '200': - description: Components loaded successfully - content: - application/json: - schema: - type: object - '400': - description: Error loading components - - # Miscellaneous Endpoints - /manager/version: + description: Reboot initiated + '403': + description: Security policy violation + + /v2/manager/version: get: summary: Get manager version description: Returns the current version of ComfyUI-Manager @@ -832,15 +1208,18 @@ paths: text/plain: schema: type: string - - /manager/notice: + + /v2/manager/is_legacy_manager_ui: get: - summary: Get manager notice - description: Returns HTML content with notices and version information + summary: Check if legacy manager UI is enabled + description: Returns whether the legacy manager UI is enabled responses: '200': description: Successful operation content: - text/html: + application/json: schema: - type: string \ No newline at end of file + type: object + properties: + is_legacy_manager_ui: + type: boolean \ No newline at end of file