Compare commits

...

23 Commits

Author SHA1 Message Date
Christian Byrne
237f98d4f0
Merge da87651e53 into f4fdd51ce9 2025-12-20 00:49:23 +03:00
Dr.Lt.Data
f4fdd51ce9 feat(preview): disable Manager preview method when ComfyUI native feature is available
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
- Add detection for ComfyUI PR #11261 (per-queue preview override)
- Return DISABLED status when native feature is detected
- Improve UI loading state and prevent flash of enabled state
- Add accessibility attributes and visual feedback for disabled state
- Show user notification when feature transitions to native
- Version bump to 3.39
2025-12-19 23:05:52 +09:00
David
ae6c7dd673
Changed Main Dialog to match aesthetics and close button location as Original ComfyUI Interface (#2349)
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
* Started changing UI to match the rest of ComfyUI

Completed Main Container

* - Added layout formatting to components of the Manager dialog box
- Pulled name from select and put it into a label (eg "DB: Channel" now has a label of DB and a dropdown with channel, etc)
- Fixed incorrect z-index

* Removed this.close() I added before finding z-index issue.

* Matched buttons and drop downs to match style of ComfyUI interface while keeping the colours the same as OG ComfyUI Manager

* - Took gui building out and put into its own .js
- Applied theme to Nodes Manager
- Made theme respect user theme colors

* - Themed model manager and snapshot manager
- fixed incorrect id in gui builder

* Fix syntax error in color property

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-19 12:34:20 +09:00
Dr.Lt.Data
0cbc773126 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-19 11:38:53 +09:00
Dr.Lt.Data
45bd3473fa update DB 2025-12-19 06:46:21 +09:00
shootthesound
02175844da
Updated my entry to include my new Block Editing Nodes (#2405)
* Add Realtime LoRA Trainer node to custom-node-list

Added a new node for Realtime LoRA Trainer with details.

* Enhance description for ComfyUI Loras training

Updated the description to include Block Edit and Save Loras functionality and Musubi Tuner support.
2025-12-19 06:43:52 +09:00
Dr.Lt.Data
fd60f7ee70 update DB
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
2025-12-17 13:03:57 +09:00
Dr.Lt.Data
9eb4c3ab23 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-17 07:40:52 +09:00
Dr.Lt.Data
72d1aa7d97 update DB 2025-12-17 06:58:00 +09:00
Dr.Lt.Data
57628ead80 update DB 2025-12-17 06:04:44 +09:00
revisiontony
9733c2328b
Update custom-node-list.json (#2398) 2025-12-17 06:03:56 +09:00
Dr.Lt.Data
70663cecc3 update DB 2025-12-17 06:02:43 +09:00
tppp2806
7c77942a92
Add ComfyUI-YoloTrack node to custom-node-list.json (#2400)
* Add ComfyUI-YoloTrack node to custom-node-list.json

Added a new node for ComfyUI-YoloTrack with details.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2025-12-17 06:01:42 +09:00
Dr.Lt.Data
04cf18e149 update DB 2025-12-17 06:00:16 +09:00
akawana
1825edda7e
Add AK XZ Axis node information to custom-node-list (#2399) 2025-12-17 05:58:31 +09:00
Dr.Lt.Data
045f91c411 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-16 12:53:56 +09:00
Dr.Lt.Data
96d24f548c update DB 2025-12-16 12:51:59 +09:00
Dr.Lt.Data
c7f03ad64e update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-15 22:28:27 +09:00
Dr.Lt.Data
1232989d7d update DB 2025-12-15 22:04:50 +09:00
Dr.Lt.Data
8f66a7997f update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-15 12:57:52 +09:00
Dr.Lt.Data
f32dd80c24 update DB
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
2025-12-15 03:16:01 +09:00
Gero Doll
a06ba343de
Add ComfyUI-PromptGenerator to custom node list (#2391)
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
Generate Stable Diffusion prompts using Qwen3-8B via Ollama with 7 style presets (cinematic, anime, photorealistic, fantasy, abstract, cyberpunk, sci-fi)
2025-12-15 00:36:47 +09:00
bymyself
da87651e53 [tests] Add API test suite 2025-05-20 16:35:40 -07:00
42 changed files with 21636 additions and 15033 deletions

View File

@ -3325,6 +3325,16 @@
"install_type": "git-clone",
"description": "Mel-Band RoFormer for Music Source Separation"
},
{
"author": "kijai",
"title": "ComfyUI-SCAIL-Pose",
"reference": "https://github.com/kijai/ComfyUI-SCAIL-Pose",
"files": [
"https://github.com/kijai/ComfyUI-SCAIL-Pose"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for SCAIL input processing"
},
{
"author": "hhhzzyang",
"title": "Comfyui-Lama",
@ -4904,6 +4914,16 @@
"install_type": "git-clone",
"description": "This extension helps generate images through NAI."
},
{
"author": "bedovyy",
"title": "ComfyUI-LLM-Helper",
"reference": "https://github.com/bedovyy/ComfyUI-LLM-Helper",
"files": [
"https://github.com/bedovyy/ComfyUI-LLM-Helper"
],
"install_type": "git-clone",
"description": "A collection of helper nodes for working with LLM APIs in ComfyUI, intended to complement other LLM custom nodes."
},
{
"author": "Off-Live",
"title": "ComfyUI-off-suite",
@ -5191,6 +5211,16 @@
"install_type": "git-clone",
"description": "Logical Utils (compare, string, boolean operations) for ComfyUI"
},
{
"author": "aria1th",
"title": "ComfyUI-Wildcards-rework",
"reference": "https://github.com/aria1th/ComfyUI-Wildcards-rework",
"files": [
"https://github.com/aria1th/ComfyUI-Wildcards-rework"
],
"install_type": "git-clone",
"description": "Powerful ComfyUI custom node for dynamic prompt generation using wildcards and bracket expressions, enabling infinite variations with weighted selection, multi-picks, and nested expansion support."
},
{
"author": "MitoshiroPJ",
"title": "ComfyUI Nearsighted Attention",
@ -8810,6 +8840,16 @@
"install_type": "git-clone",
"description": "ComfyUI implementation of [a/Omost](https://github.com/lllyasviel/Omost), and everything about regional prompt.\nNOTE: You need to install ComfyUI_densediffusion to use this node."
},
{
"author": "huchenlei",
"title": "ComfyUI-execution-glow",
"reference": "https://github.com/huchenlei/ComfyUI-execution-glow",
"files": [
"https://github.com/huchenlei/ComfyUI-execution-glow"
],
"install_type": "git-clone",
"description": "ComfyUI extension that adds a glowing visual effect to nodes during execution for enhanced visual feedback."
},
{
"author": "nathannlu",
"title": "ComfyUI Pets",
@ -9785,16 +9825,6 @@
"install_type": "git-clone",
"description": "Shuffle nodes after queue execution."
},
{
"author": "shinich39",
"title": "comfyui-innnnnpaint",
"reference": "https://github.com/shinich39/comfyui-innnnnpaint",
"files": [
"https://github.com/shinich39/comfyui-innnnnpaint"
],
"install_type": "git-clone",
"description": "Load new workflow after mask editing."
},
{
"author": "shinich39",
"title": "comfyui-break-workflow",
@ -13698,6 +13728,16 @@
"install_type": "git-clone",
"description": "You'll get a new node called SD3 Latent Select Resolution, you can pick the x and y sizes from a list."
},
{
"author": "GavChap",
"title": "ComfyUI_ExtractLora",
"reference": "https://github.com/GavChap/ComfyUI_ExtractLora",
"files": [
"https://github.com/GavChap/ComfyUI_ExtractLora"
],
"install_type": "git-clone",
"description": "Creates lora from two checkpoints by extracting the difference."
},
{
"author": "BenNarum",
"title": "SigmaWaveFormNodes",
@ -17995,6 +18035,26 @@
"install_type": "git-clone",
"description": "ComfyUI nodes for outpainting images with diffusers, based on [a/diffusers-image-outpaint](https://huggingface.co/spaces/fffiloni/diffusers-image-outpaint/tree/main) by fffiloni."
},
{
"author": "GiusTex",
"title": "ComfyUI-Wan-TimeToMove",
"reference": "https://github.com/GiusTex/ComfyUI-Wan-TimeToMove",
"files": [
"https://github.com/GiusTex/ComfyUI-Wan-TimeToMove"
],
"install_type": "git-clone",
"description": "Native ComfyUI port of kijai's WanVideo-Wrapper TimeToMove for video generation, currently supporting LCM sampler with frame generation capabilities. (Description by CC)"
},
{
"author": "GiusTex",
"title": "ComfyUI-MoreEfficientSamplers",
"reference": "https://github.com/GiusTex/ComfyUI-MoreEfficientSamplers",
"files": [
"https://github.com/GiusTex/ComfyUI-MoreEfficientSamplers"
],
"install_type": "git-clone",
"description": "Advanced sampler nodes for ComfyUI based on efficiency-nodes, including live preview rendering and support for custom samplers with schedulers like flowmatch scheduler."
},
{
"author": "CY-CHENYUE",
"title": "ComfyUI-MiniCPM-Plus",
@ -21820,6 +21880,26 @@
"install_type": "git-clone",
"description": "A custom node for ComfyUI that integrates Video-As-Prompt for motion-guided video generation from image inputs."
},
{
"author": "HM-RunningHub",
"title": "ComfyUI_RH_QwenImageI2L",
"reference": "https://github.com/HM-RunningHub/ComfyUI_RH_QwenImageI2L",
"files": [
"https://github.com/HM-RunningHub/ComfyUI_RH_QwenImageI2L"
],
"install_type": "git-clone",
"description": "A ComfyUI custom node that generates Image-to-LoRA (I2L) LoRA from training images using DiffSynth-Studio Qwen-Image i2L pipelines."
},
{
"author": "HM-RunningHub",
"title": "ComfyUI_RH_LLM_API",
"reference": "https://github.com/HM-RunningHub/ComfyUI_RH_LLM_API",
"files": [
"https://github.com/HM-RunningHub/ComfyUI_RH_LLM_API"
],
"install_type": "git-clone",
"description": "An easy-to-use ComfyUI plugin for LLM integration supporting DeepSeek, OpenAI API-compatible models, with video reverse-prompt (captioning) capabilities. (Description by CC)"
},
{
"author": "wqjuser",
"title": "ComfyUI-Chat-Image",
@ -25206,6 +25286,16 @@
"install_type": "git-clone",
"description": "Custom node for ComfyUI that fixes an existing node [a/comfyui-dynamicprompts](https://github.com/adieyal/comfyui-dynamicprompts)."
},
{
"author": "thezveroboy",
"title": "ComfyUI-ClipReshaper",
"reference": "https://github.com/thezveroboy/ComfyUI-ClipReshaper",
"files": [
"https://github.com/thezveroboy/ComfyUI-ClipReshaper"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes to reshape/project Conditioning tensors and ensure SDXL conditioning metadata."
},
{
"author": "tatookan",
"title": "comfyui_ssl_gemini_EXP",
@ -26543,16 +26633,6 @@
"install_type": "git-clone",
"description": "A collection of utility nodes for ComfyUI, focusing on batch processing, stitching, and visualization."
},
{
"author": "AngelCookies",
"title": "ComfyUI-Seed-Tracker",
"reference": "https://github.com/AngelCookies/ComfyUI-Seed-Tracker",
"files": [
"https://github.com/AngelCookies/ComfyUI-Seed-Tracker"
],
"install_type": "git-clone",
"description": "A ComfyUI extension that tracks random seeds throughout your image generation workflows"
},
{
"author": "TiamaTiramisu",
"title": "RisuTools",
@ -27473,6 +27553,16 @@
"install_type": "git-clone",
"description": "A custom ComfyUI node for integrating with the Fal Kontext API for advanced image editing and generation."
},
{
"author": "SanDiegoDude",
"title": "SCG_LocalVLM",
"reference": "https://github.com/SanDiegoDude/SCG_LocalVLM",
"files": [
"https://github.com/SanDiegoDude/SCG_LocalVLM"
],
"install_type": "git-clone",
"description": "This is an implementation of [Qwen3-VL](https://github.com/QwenLM/Qwen3-VL) for [ComfyUI](https://github.com/comfyanonymous/ComfyUI), which supports for text-based and single-image queries."
},
{
"author": "tavyra",
"title": "ComfyUI_Curves",
@ -28021,6 +28111,17 @@
"install_type": "git-clone",
"description": "Advanced seed generator for ComfyUI with multiple modes, state persistence, and cross-library synchronization"
},
{
"author": "Limbicnation",
"title": "ComfyUI-PromptGenerator",
"id": "comfyui-prompt-generator",
"reference": "https://github.com/Limbicnation/ComfyUI-PromptGenerator",
"files": [
"https://github.com/Limbicnation/ComfyUI-PromptGenerator"
],
"install_type": "git-clone",
"description": "Generate Stable Diffusion prompts using Qwen3-8B via Ollama with 7 style presets (cinematic, anime, photorealistic, fantasy, abstract, cyberpunk, sci-fi)"
},
{
"author": "hao-ai-lab",
"title": "FastVideo",
@ -29476,6 +29577,16 @@
"install_type": "git-clone",
"description": "The ListHelper collection is a comprehensive set of custom nodes for ComfyUI that provides powerful list manipulation capabilities. This collection includes audio processing, text splitting, and number generation tools for enhanced workflow automation."
},
{
"author": "dseditor",
"title": "Comfy-MCP",
"reference": "https://github.com/dseditor/Comfy-MCP",
"files": [
"https://github.com/dseditor/Comfy-MCP"
],
"install_type": "git-clone",
"description": "Simple MCP Server for ComfyUI text to image Workflow - ComfyUI Node Integration , Based on lalanikarim/comfy-mcp-server work"
},
{
"author": "Leon",
"title": "Leon's Utility and API Integration Nodes",
@ -29570,17 +29681,6 @@
"install_type": "git-clone",
"description": "Professional AI Image Description Generator\nBased on Zhipu AI GLM-4V multimodal model, batch generate accurate and detailed descriptions for images in Chinese and English"
},
{
"author": "jkhayiying",
"title": "ImageLoadFromLocalOrUrl Node for ComfyUI",
"id": "JkhaImageLoaderPathOrUrl",
"reference": "https://gitee.com/yyh915/jkha-load-img",
"files": [
"https://gitee.com/yyh915/jkha-load-img"
],
"install_type": "git-clone",
"description": "This is a node to load an image from local path or url."
},
{
"author": "jinchanz",
"title": "ComfyUI-ADIC",
@ -32024,6 +32124,16 @@
"install_type": "git-clone",
"description": "A versatile and highly customizable node for ComfyUI to add camera-style watermarks and frames to your images. Whether you want to emulate the classic Leica look, add EXIF data, or create professional-looking framed images, this plugin has you covered."
},
{
"author": "karas17",
"title": "comfyui_GLM_TTS",
"reference": "https://github.com/karas17/comfyui_GLM_TTS",
"files": [
"https://github.com/karas17/comfyui_GLM_TTS"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for GLM-TTS, a high-quality text-to-speech system supporting zero-shot voice cloning."
},
{
"author": "shinyakidoguchi301",
"title": "shinyakidoguchi301/LoRA Tag Loader for ComfyUI",
@ -33705,6 +33815,16 @@
"install_type": "git-clone",
"description": "Custom ComfyUI nodes for integrating WaveSpeed AI API for image and video generation."
},
{
"author": "razvanmatei-sf",
"title": "comfyui-stillfront",
"reference": "https://github.com/razvanmatei-sf/comfyui-stillfront",
"files": [
"https://github.com/razvanmatei-sf/comfyui-stillfront"
],
"install_type": "git-clone",
"description": "Consolidated collection of custom ComfyUI nodes with LLM integration (Claude, Gemini), WaveSpeed API support, and utility nodes for resolution presets and dynamic prompts. (Description by CC)"
},
{
"author": "blurgyy",
"title": "CoMPaSS-ComfyUI",
@ -34049,6 +34169,16 @@
"install_type": "git-clone",
"description": "Remove background scenery from an image of a person. The output image is saved as an RGBA PNG. The alpha channel is included."
},
{
"author": "Sean-Bradley",
"title": "ComfyUI-Get-Line",
"reference": "https://github.com/Sean-Bradley/ComfyUI-Get-Line",
"files": [
"https://github.com/Sean-Bradley/ComfyUI-Get-Line"
],
"install_type": "git-clone",
"description": "A ComfyUI custom node used to get one line of string from a multi line string. Search in the manager for `get line`, or `getline` or `get-line`."
},
{
"author": "LukeCoulson1",
"title": "ComfyUI LoRA Combine Node",
@ -34737,6 +34867,16 @@
"install_type": "git-clone",
"description": "View Image Metadata of ComfyUI as well as of ForgeUI or Automatic 1111 generated images in Easily Readable Format"
},
{
"author": "ShammiG",
"title": "ComfyUI-Show_Any_Text_in_CMD_Console-SG",
"reference": "https://github.com/ShammiG/ComfyUI-Show_Any_Text_in_CMD_Console-SG",
"files": [
"https://github.com/ShammiG/ComfyUI-Show_Any_Text_in_CMD_Console-SG"
],
"install_type": "git-clone",
"description": "Simple node to show ANY output text in the CMD console with color options"
},
{
"author": "bgreene2",
"title": "ComfyUI-Hunyuan-Image-3",
@ -35189,6 +35329,16 @@
"install_type": "git-clone",
"description": "SAM 3D Body integration for ComfyUI - Robust full-body human mesh recovery from single images. Reconstruct 3D human meshes with pose, shape, and hand details. First draft, please open a github issue if you have any problems or feature requests :)"
},
{
"author": "PozzettiAndrea",
"title": "ComfyUI-Sharp",
"reference": "https://github.com/PozzettiAndrea/ComfyUI-Sharp",
"files": [
"https://github.com/PozzettiAndrea/ComfyUI-Sharp"
],
"install_type": "git-clone",
"description": "SHARP integration for ComfyUI - Monocular 3D Gaussian Splatting in under 1 second. Generate 3D Gaussians from a single image using Apple's SHARP model."
},
{
"author": "rookiestar28",
"title": "Danbooru Tags Upsampler for ComfyUI",
@ -35557,6 +35707,16 @@
"install_type": "git-clone",
"description": "This custom ComfyUI node, Find Perfect Resolution, calculates an optimal output resolution for an input image while preserving its aspect ratio and ensuring dimensions are divisible by a specified value. It is designed to work seamlessly in ComfyUI workflows, particularly for resizing images with nodes like 'Resize Image v2'."
},
{
"author": "ashtar1984",
"title": "ComfyUI-SwitchPathLazy",
"reference": "https://github.com/ashtar1984/ComfyUI-SwitchPathLazy",
"files": [
"https://github.com/ashtar1984/ComfyUI-SwitchPathLazy"
],
"install_type": "git-clone",
"description": "Efficient lazy path switch for ComfyUI. Skips execution of the inactive branch entirely using native lazy evaluation. Includes live status display."
},
{
"author": "cybernaut4",
"title": "Arkl1te's Toolkit",
@ -35808,16 +35968,6 @@
"install_type": "git-clone",
"description": "Multi-frame reference conditioning nodes for Wan2.2 A14B I2V models."
},
{
"author": "wallen0322",
"title": "ComfyUI-TTM-WAN22",
"reference": "https://github.com/wallen0322/ComfyUI-TTM-WAN22",
"files": [
"https://github.com/wallen0322/ComfyUI-TTM-WAN22"
],
"install_type": "git-clone",
"description": "TTM (Time-to-Move) node for ComfyUI enabling motion-controlled video generation with Wan2.2 models using dual-clock denoising for independent background and object animation control."
},
{
"author": "wallen0322",
"title": "ComfyUI-AE-Animation",
@ -36167,6 +36317,16 @@
"install_type": "git-clone",
"description": "This repository is a ComfyUI port of the WithAnyone model introduced in the paper.\n[a/WithAnyone: Towards Controllable and ID-Consistent Image Generation (2025)](https://arxiv.org/abs/2510.14975)\nOriginal implementation: [a/Doby-Xu/WithAnyone](https://github.com/Doby-Xu/WithAnyone)."
},
{
"author": "okdalto",
"title": "ComfyUI-PersonaLive",
"reference": "https://github.com/okdalto/ComfyUI-PersonaLive",
"files": [
"https://github.com/okdalto/ComfyUI-PersonaLive"
],
"install_type": "git-clone",
"description": "A ComfyUI custom node implementation of PersonaLive: Expressive Portrait Image Animation for Live Streaming, enabling portrait animation driven by reference images. (Description by CC)"
},
{
"author": "hw5511",
"title": "Woohee HF Upscaler Loader",
@ -37311,6 +37471,17 @@
"install_type": "git-clone",
"description": "Wan Video Extender extends a short video or a single image into a longer, temporally consistent clip using a VACE based control latent. It runs in multiple extension loops. Each loop can have its own prompt and optional LoRA, so you can evolve the scene step by step without rebuilding the graph."
},
{
"author": "akawana",
"title": "AK XZ Axis (XY for any KSampler)",
"reference": "https://github.com/akawana/ComfyUI-AK-XZ-Axis",
"files": [
"https://github.com/akawana/ComfyUI-AK-XZ-Axis"
],
"install_type": "git-clone",
"description": "Nodes for XY-style testing of parameters such as seed, steps, cfg, denoise, prompts, and LoRAs. Does not require a custom KSampler and works with any KSampler, including the default ComfyUI one.",
"tags": ["xy_plot", "xy", "xz", "testing", "ksampler"]
},
{
"author": "akawana",
"title": "Folded prompts",
@ -38105,6 +38276,26 @@
"install_type": "git-clone",
"description": "Custom node for building keyframe timelines in Wan video generation with adjustable influence strength, supporting 1-8 keyframes. (Description by CC)"
},
{
"author": "ckinpdx",
"title": "comfyui-humo-audio-motion",
"reference": "https://github.com/ckinpdx/comfyui-humo-audio-motion",
"files": [
"https://github.com/ckinpdx/comfyui-humo-audio-motion"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for granular Q/K/V/O attention boosting in HuMo models. Provides 12 independent controls for fine-tuning audio-driven motion response."
},
{
"author": "ckinpdx",
"title": "ComfyUI-SCAIL-AudioReactive",
"reference": "https://github.com/ckinpdx/ComfyUI-SCAIL-AudioReactive",
"files": [
"https://github.com/ckinpdx/ComfyUI-SCAIL-AudioReactive"
],
"install_type": "git-clone",
"description": "Generate audio-reactive SCAIL pose sequences for character animation without requiring input video tracking. Now supports Multi-Character Choreography. (Description by CC)"
},
{
"author": "jessesep",
"title": "SimpleVariables",
@ -38468,16 +38659,366 @@
"install_type": "git-clone",
"description": "The official Draw Things extension for ComfyUI. Sends image-generation requests to Draw Things over gRPC. Supports Bridge Mode for DT+ cloud and local model execution."
},
{
"author": "g7b2",
"title": "ComfyUI-Artist-Tester",
"reference": "https://github.com/g7b2/ComfyUI-Artist-Tester",
"files": [
"https://github.com/g7b2/ComfyUI-Artist-Tester"
],
"install_type": "git-clone",
"description": "A dedicated suite of custom nodes for batch testing artists, styles, and prompts in ComfyUI, solving the caching problem with anti-cache logic and dynamic filename generation."
},
{
"author": "ubisoft",
"title": "ComfyUI-Chord",
"reference": "https://github.com/ubisoft/ComfyUI-Chord",
"files": [
"https://github.com/ubisoft/ComfyUI-Chord"
],
"install_type": "git-clone",
"description": "Custom nodes for the paper: Chord: Chain of Rendering Decomposition for PBR Material Estimation from Generated Texture Images"
},
{
"author": "Deathspike",
"title": "ComfyUI-MyOriginalWaifu",
"reference": "https://github.com/Deathspike/ComfyUI-MyOriginalWaifu",
"files": [
"https://github.com/Deathspike/ComfyUI-MyOriginalWaifu"
],
"install_type": "git-clone",
"description": "My Original Waifu is a tag-based prompt-transformation engine for ComfyUI designed for creators who want their original characters to feel consistent, expressive, and faithfully rendered across every scene. Instead of manually adjusting tags each time you generate an image, you define the essence of your waifu, her look, her outfits, the way she appears in different contexts, and the engine transforms your prompt to match those intentions. It stays out of the way and simply follows your rules with clarity and consistency. Your Waifu. Your Rules. Your Perfect Prompt."
},
{
"author": "LeonQ8",
"title": "ComfyUI-Dynamic-Lora-Scheduler",
"reference": "https://github.com/LeonQ8/ComfyUI-Dynamic-Lora-Scheduler",
"files": [
"https://github.com/LeonQ8/ComfyUI-Dynamic-Lora-Scheduler"
],
"install_type": "git-clone",
"description": "Dynamically balance weights of multiple LoRAs over generation steps for ComfyUI."
},
{
"author": "sebagallo",
"title": "comfyui-sg-llama-cpp",
"reference": "https://github.com/sebagallo/comfyui-sg-llama-cpp",
"files": [
"https://github.com/sebagallo/comfyui-sg-llama-cpp"
],
"install_type": "git-clone",
"description": "llama-cpp-python wrapper, with support for vision models. It allows the user to generate text responses from prompts using llama.cpp."
},
{
"author": "hubo502",
"title": "ComfyUI-Env-Loader",
"reference": "https://github.com/hubo502/ComfyUI-Env-Loader",
"files": [
"https://github.com/hubo502/ComfyUI-Env-Loader"
],
"install_type": "git-clone",
"description": "ComfyUI custom node set that reads .env files at startup and provides dropdown selection or dynamic multi-port output for environment variable access in workflows. (Description by CC)"
},
{
"author": "fabbarix",
"title": "comfyui-promptstore",
"reference": "https://github.com/fabbarix/comfyui-promptstore",
"files": [
"https://github.com/fabbarix/comfyui-promptstore"
],
"install_type": "git-clone",
"description": "Custom node system for ComfyUI enabling efficient prompt management with YAML-based datastore, categories, selection interface, and dynamic text interpolation for creating complex prompts."
},
{
"author": "ARM64-EC",
"title": "ComfyUI-LongCatPlugin",
"reference": "https://github.com/ARM64-EC/ComfyUI-LongCatPlugin",
"files": [
"https://github.com/ARM64-EC/ComfyUI-LongCatPlugin"
],
"install_type": "git-clone",
"description": "ComfyUI nodes wrapping LongCat image generation and editing pipelines with text-to-image and multi-image edit flows using diffusers framework. (Description by CC)"
},
{
"author": "NOLABEL-VFX",
"title": "ComfyUI-NL_Nodes",
"reference": "https://github.com/NOLABEL-VFX/ComfyUI-NL_Nodes",
"files": [
"https://github.com/NOLABEL-VFX/ComfyUI-NL_Nodes"
],
"install_type": "git-clone",
"description": "Custom ComfyUI nodes by NOLABEL for studio workflows, featuring Shot Path Builder that generates standardized, sanitized file and folder names with versioning for renders."
},
{
"author": "pantaleone-ai",
"title": "Comfy-Firefly",
"reference": "https://github.com/pantaleone-ai/Comfy-Firefly",
"files": [
"https://github.com/pantaleone-ai/Comfy-Firefly"
],
"install_type": "git-clone",
"description": "Custom ComfyUI node for generating images using Adobe Firefly Services API v3 with automatic authentication and standard tensor output."
},
{
"author": "febogallo",
"title": "ComfyUI-Freepik",
"reference": "https://github.com/febogallo/ComfyUI-Freepik",
"files": [
"https://github.com/febogallo/ComfyUI-Freepik"
],
"install_type": "git-clone",
"description": "Integrates Freepik's AI capabilities into ComfyUI workflows with features for photorealistic generation, upscaling, and background removal, plus smart caching and cost management. (Description by CC)"
},
{
"author": "thnikk",
"title": "comfyui-thnikk-utils",
"reference": "https://github.com/thnikk/comfyui-thnikk-utils",
"files": [
"https://github.com/thnikk/comfyui-thnikk-utils"
],
"install_type": "git-clone",
"description": "Nodes to clean up your workflow."
},
{
"author": "XYMikky12138",
"title": "ComfyUI-MIKKY-Mask-Editor",
"reference": "https://github.com/XYMikky12138/ComfyUI-MIKKY-Mask-Editor",
"files": [
"https://github.com/XYMikky12138/ComfyUI-MIKKY-Mask-Editor"
],
"install_type": "git-clone",
"description": "A powerful frame-by-frame video mask editor for ComfyUI with painting, auto BBox, hole filling, blur/feathering, and video slicing features."
},
{
"author": "XYMikky12138",
"title": "ComfyUI-NanoBanana-inpaint",
"reference": "https://github.com/XYMikky12138/ComfyUI-NanoBanana-inpaint",
"files": [
"https://github.com/XYMikky12138/ComfyUI-NanoBanana-inpaint"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for API-based inpainting (Gemini, Imagen) with aspect ratio constraints, smart cropping, resize fitting, intelligent paste-back with transparency support. (Description by CC)"
},
{
"author": "SiegeKeebsOffical",
"title": "comfyui-lmstudio",
"reference": "https://github.com/SiegeKeebsOffical/comfyui-lmstudio",
"files": [
"https://github.com/SiegeKeebsOffical/comfyui-lmstudio"
],
"install_type": "git-clone",
"description": "Custom ComfyUI nodes designed to interface with a separate LM Studio instance for language model operations."
},
{
"author": "bombdefuser-124",
"title": "Newbie-Teacache-ComfyUI",
"reference": "https://github.com/bombdefuser-124/Newbie-Teacache-ComfyUI",
"files": [
"https://github.com/bombdefuser-124/Newbie-Teacache-ComfyUI"
],
"install_type": "git-clone",
"description": "TeaCache optimization nodes for ComfyUI's NewBie implementation, featuring patched loader and coefficient calculator for faster inference with configurable quality. (Description by CC)"
},
{
"author": "maxczc",
"title": "comfyui-sora-node",
"reference": "https://github.com/maxczc/comfyui-sora-node",
"files": [
"https://github.com/maxczc/comfyui-sora-node"
],
"install_type": "git-clone",
"description": "Comprehensive set of ComfyUI custom nodes for interacting with a Sora-compatible REST API, supporting text-to-video, image-to-video, and video-to-video generation."
},
{
"author": "logicalor",
"title": "comfyui_multi_replace",
"reference": "https://github.com/logicalor/comfyui_multi_replace",
"files": [
"https://github.com/logicalor/comfyui_multi_replace"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for creating and applying multiple find/replace pairs to text"
},
{
"author": "logicalor",
"title": "comfyui_text_to_pose",
"reference": "https://github.com/logicalor/comfyui_text_to_pose",
"files": [
"https://github.com/logicalor/comfyui_text_to_pose"
],
"install_type": "git-clone",
"description": "Generate human poses from text descriptions using T2P Transformer for ControlNet/T2I-Adapter workflows"
},
{
"author": "ssspace1",
"title": "SSpack_ComfyUI",
"reference": "https://github.com/ssspace1/SSpack_ComfyUI",
"files": [
"https://github.com/ssspace1/SSpack_ComfyUI"
],
"install_type": "git-clone",
"description": "Compact, UI-friendly nodes for ComfyUI with LoRA/checkpoint selectors with thumbnails, text utilities, image helpers, and a lightweight cache cleaner script."
},
{
"author": "SofianeAlla",
"title": "ComfyUI-BespokeAI-3D",
"reference": "https://github.com/SofianeAlla/ComfyUI-BespokeAI-3D",
"files": [
"https://github.com/SofianeAlla/ComfyUI-BespokeAI-3D"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for BespokeAI image-to-3D generation\nNOTE: The files in the repo are not organized."
},
{
"author": "laboratoiresonore",
"title": "ComfyUI_PerformanceLab",
"reference": "https://github.com/laboratoiresonore/ComfyUI_PerformanceLab",
"files": [
"https://github.com/laboratoiresonore/ComfyUI_PerformanceLab"
],
"install_type": "git-clone",
"description": "Make any ComfyUI workflow faster, use less VRAM, or produce better quality - with AI assistance"
},
{
"author": "tppp2806",
"title": "ComfyUI-YoloTrack",
"reference": "https://github.com/tppp2806/ComfyUI-YoloTrack",
"files": [
"https://github.com/tppp2806/ComfyUI-YoloTrack"
],
"install_type": "git-clone",
"description": "Based on a YOLO model, it performs object detection, capture, and smooth tracking-based cropping on images."
},
{
"author": "revisiontony",
"title": "ComfyUI Lora Manager Web Frame",
"reference": "https://github.com/revisiontony/LoraMangerWebFrame",
"files": [
"https://github.com/revisiontony/LoraMangerWebFrame"
],
"install_type": "git-clone",
"description": "Embeds the ComfyUI-Lora-Manager web interface directly into your ComfyUI workflow graph, eliminating the need for a separate browser tab."
},
{
"author": "Maenvaeru",
"title": "comfyui-vram-overlay",
"reference": "https://github.com/Maenvaeru/comfyui-vram-overlay",
"files": [
"https://github.com/Maenvaeru/comfyui-vram-overlay"
],
"install_type": "git-clone",
"description": "Professional VRAM monitoring overlay for ComfyUI that displays real-time GPU memory usage as an independent, non-intrusive window. (Description by CC)"
},
{
"author": "PROJECTMAD",
"title": "PROJECT-MAD-NODES",
"reference": "https://github.com/PROJECTMAD/PROJECT-MAD-NODES",
"files": [
"https://github.com/PROJECTMAD/PROJECT-MAD-NODES"
],
"install_type": "git-clone",
"description": "Collection of custom ComfyUI nodes for LoRA scheduling and prompt management, featuring visual curve editor and visual prompt gallery with EXIF metadata extraction."
},
{
"author": "flowers6421",
"title": "ComfyUI-SimpleTunerFlux2",
"reference": "https://github.com/flowers6421/ComfyUI-SimpleTunerFlux2",
"files": [
"https://github.com/flowers6421/ComfyUI-SimpleTunerFlux2"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for using SimpleTuner-trained Flux 2 LoRA models. SimpleTuner's Flux 2 architecture uses fused layers, making its LoRAs incompatible with standard ComfyUI Flux nodes. (Description by CC)"
},
{
"author": "scott-createplay",
"title": "ComfyUI_video_essentials",
"reference": "https://github.com/scott-createplay/ComfyUI_video_essentials",
"files": [
"https://github.com/scott-createplay/ComfyUI_video_essentials"
],
"install_type": "git-clone",
"description": "Essential video processing nodes for ComfyUI"
},
{
"author": "revisionhiep-create",
"title": "comfyui-standard-trigger-words",
"reference": "https://github.com/revisionhiep-create/comfyui-standard-trigger-words",
"files": [
"https://github.com/revisionhiep-create/comfyui-standard-trigger-words"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for managing trigger words with 50+ editable presets optimized for SDXL Illustrious"
},
{
"author": "Faildes",
"title": "ComfyUI-NegativeFold",
"reference": "https://github.com/Faildes/ComfyUI-NegativeFold",
"files": [
"https://github.com/Faildes/ComfyUI-NegativeFold"
],
"install_type": "git-clone",
"description": "Fold negative prompts into positive prompts for use with Turbo models. (Description by CC)"
},
{
"author": "isekai-sh",
"title": "isekai-comfy-node",
"reference": "https://github.com/isekai-sh/isekai-comfy-node",
"files": [
"https://github.com/isekai-sh/isekai-comfy-node"
],
"install_type": "git-clone",
"description": "Upload and compress AI-generated images and enhance your ComfyUI workflows with powerful string utilities and AI integration."
},
{
"author": "QL-boy",
"title": "ComfyUI-Advanced-Tile-Processing",
"reference": "https://github.com/QL-boy/ComfyUI-Advanced-Tile-Processing",
"files": [
"https://github.com/QL-boy/ComfyUI-Advanced-Tile-Processing"
],
"install_type": "git-clone",
"description": "Advanced tiling plugin for ComfyUI solving VRAM limitations in 4K/8K+ image generation using intelligent tiling and seamless weighted fusion with multiple blending modes."
},
{
"author": "gateway",
"title": "ComfyUI-Kie-API",
"reference": "https://github.com/gateway/ComfyUI-Kie-API",
"files": [
"https://github.com/gateway/ComfyUI-Kie-API"
],
"install_type": "git-clone",
"description": "Integration framework for KIE Nano Banana Pro API into ComfyUI custom nodes with API key setup and placeholder for upcoming node implementations. (Description by CC)"
},
{
"author": "lazyq666",
"title": "gemini-3-simply-comfyui",
"reference": "https://github.com/lazyq666/gemini-3-simply-comfyui",
"files": [
"https://github.com/lazyq666/gemini-3-simply-comfyui"
],
"install_type": "git-clone",
"description": "Two lightweight ComfyUI nodes for Gemini 3 preview models: Gemini 3 Pro (Text) and Gemini 3 Pro Image. (Description by CC)"
},
{
"author": "aslanxie",
"title": "comfyui_qwen_image_edit",
"reference": "https://github.com/aslanxie/comfyui_qwen_image_edit",
"files": [
"https://github.com/aslanxie/comfyui_qwen_image_edit"
],
"install_type": "git-clone",
"description": "ComfyUI Qwen-Image-Edit-2509 node integration for image editing using OpenAI Image API. (Description by CC)"
},
{
"author": "danieljanata",
"title": "ComfyUI-QwenVL-Override",
"reference": "https://github.com/danieljanata/ComfyUI-QwenVL-Override",
"files": [
"https://github.com/danieljanata/ComfyUI-QwenVL-Override"
],
"install_type": "git-clone",
"description": "Adds two nodes that reuse upstream ComfyUI-QwenVL presets but add a runtime override that can be wired/unwired without getting stuck."
},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ import manager_migration
from node_package import InstalledNodePackage
version_code = [3, 38, 3]
version_code = [3, 39]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')

View File

@ -38,6 +38,25 @@ SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in
routes = PromptServer.instance.routes
def has_per_queue_preview():
"""
Check if ComfyUI PR #11261 (per-queue live preview override) is merged
Returns:
bool: True if ComfyUI has per-queue preview feature
"""
try:
import latent_preview
return hasattr(latent_preview, 'set_preview_method')
except ImportError:
return False
# Detect ComfyUI per-queue preview override feature (PR #11261)
COMFYUI_HAS_PER_QUEUE_PREVIEW = has_per_queue_preview()
def handle_stream(stream, prefix):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
for msg in stream:
@ -182,10 +201,19 @@ def set_preview_method(method):
core.get_config()['preview_method'] = method
if args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
logging.info(
"[ComfyUI-Manager] ComfyUI per-queue preview override detected (PR #11261). "
"Manager's preview method feature is disabled. "
"Use ComfyUI's --preview-method CLI option or 'Settings > Execution > Live preview method'."
)
elif args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
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.")
logging.warning(
"[ComfyUI-Manager] Since --preview-method is set, "
"ComfyUI-Manager's preview method feature will be ignored."
)
def set_component_policy(mode):
@ -1482,13 +1510,25 @@ async def install_model(request):
@routes.get("/manager/preview_method")
async def preview_method(request):
# Setting change request
if "value" in request.rel_url.query:
# Reject setting change if per-queue preview feature is available
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=403)
# Process normally if not available
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)
return web.Response(status=200)
# Status query request
else:
# Return DISABLED if per-queue preview feature is available
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=200)
# Return current value if not available
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
@routes.get("/manager/db_mode")

227
js/comfyui-gui-builder.js Normal file
View File

@ -0,0 +1,227 @@
import { $el } from "../../scripts/ui.js";
function normalizeContent(content) {
const tmp = document.createElement('div');
if (typeof content === 'string') {
tmp.innerHTML = content;
return Array.from(tmp.childNodes);
}
if (content instanceof Node) {
return content;
}
return content;
}
export function createSettingsCombo(label, content) {
const settingItem = $el("div.setting-item", {}, [
$el("div.flex.flex-row.items-center.gap-2",[
$el("div.form-label.flex.grow.items-center", [
$el("span.text-muted", { textContent: label },)
]),
$el("div.form-input.flex.justify-end",
[content]
)
]
)
]);
return settingItem;
}
export function buildGuiFrame(dialogId, title, iconClass, content, owner) {
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
parent: document.body,
style: {
position: "fixed",
height: "100%",
width: "100%",
left: "0px",
top: "0px",
display: "flex",
justifyContent: "center",
alignItems: "center",
pointerEvents: "auto",
zIndex: "1000"
},
onclick: (e) => {
if (e.target === dialog_mask) {
owner.close();
}
}
// data-pc-section="mask"
});
const header_actions = $el("div.p-dialog-header-actions", {
// [TODO]
// data-pc-section="headeractions"
}
);
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
parent: header_actions,
type: "button",
ariaLabel: "Close",
onclick: () => owner.close(),
// "data-pc-name": "pcclosebutton",
// "data-p-disabled": "false",
// "data-p-severity": "secondary",
// "data-pc-group-section": "headericon",
// "data-pc-extend": "button",
// "data-pc-section": "root",
// [FIXME] Not sure how to do most of the SVG using $el
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label">&nbsp;</span><!---->'
}
);
const dialog_header = $el("div.p-dialog-header",
[
$el("div", [
$el("div",
{
id: "frame-title-container",
},
[
$el("h2.px-4", [
$el(iconClass, {
style: {
"font-size": "1.25rem",
"margin-right": ".5rem"
}
}),
$el("span", { textContent: title })
])
]
)
]),
header_actions
]
);
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
id: dialogId,
parent: dialog_mask,
style: {
'display': 'flex',
'flex-direction': 'column',
'pointer-events': 'auto',
'margin': '0px',
},
role: 'dialog',
ariaModal: 'true',
// [TODO]
// ariaLabbelledby: 'cm-title',
// maximized: 'false',
// data-pc-name: 'dialog',
// data-pc-section: 'root',
// data-pd-focustrap: 'true'
},
[ dialog_header, contentFrame ]
);
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
parent: manager_dialog,
tabindex: "0",
role: "presentation",
ariaHidden: "true",
"data-p-hidden-accessible": "true",
"data-p-hidden-focusable": "true",
"data-pc-section": "firstfocusableelement"
});
return dialog_mask;
}
export function buildGuiFrameCustomHeader(dialogId, customHeader, content, owner) {
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
parent: document.body,
style: {
position: "fixed",
height: "100%",
width: "100%",
left: "0px",
top: "0px",
display: "flex",
justifyContent: "center",
alignItems: "center",
pointerEvents: "auto",
zIndex: "1000"
},
onclick: (e) => {
if (e.target === dialog_mask) {
owner.close();
}
}
// data-pc-section="mask"
});
const header_actions = $el("div.p-dialog-header-actions", {
// [TODO]
// data-pc-section="headeractions"
}
);
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
parent: header_actions,
type: "button",
ariaLabel: "Close",
onclick: () => owner.close(),
// "data-pc-name": "pcclosebutton",
// "data-p-disabled": "false",
// "data-p-severity": "secondary",
// "data-pc-group-section": "headericon",
// "data-pc-extend": "button",
// "data-pc-section": "root",
// [FIXME] Not sure how to do most of the SVG using $el
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label">&nbsp;</span><!---->'
}
);
const _customHeader = normalizeContent(customHeader);
const dialog_header = $el("div.p-dialog-header",
[
$el("div", [
$el("div",
{
id: "frame-title-container",
},
Array.isArray(_customHeader) ? _customHeader : [_customHeader]
)
]),
header_actions
]
);
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
id: dialogId,
parent: dialog_mask,
style: {
'display': 'flex',
'flex-direction': 'column',
'pointer-events': 'auto',
'margin': '0px',
},
role: 'dialog',
ariaModal: 'true',
// [TODO]
// ariaLabbelledby: 'cm-title',
// maximized: 'false',
// data-pc-name: 'dialog',
// data-pc-section: 'root',
// data-pd-focustrap: 'true'
},
[ dialog_header, contentFrame ]
);
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
parent: manager_dialog,
tabindex: "0",
role: "presentation",
ariaHidden: "true",
"data-p-hidden-accessible": "true",
"data-p-hidden-focusable": "true",
"data-pc-section": "firstfocusableelement"
});
return dialog_mask;
}

View File

@ -20,6 +20,7 @@ import { ComponentBuilderDialog, getPureName, load_components, set_component_pol
import { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js";
import { SnapshotManager } from "./snapshot.js";
import { buildGuiFrame, createSettingsCombo } from "./comfyui-gui-builder.js";
let manager_version = await getVersion();
@ -44,12 +45,16 @@ docStyle.innerHTML = `
#cm-manager-dialog {
width: 1000px;
height: 455px;
height: auto;
box-sizing: content-box;
z-index: 1000;
overflow-y: auto;
}
#cm-manager-dialog br {
margin-bottom: 1em;
}
.cb-widget {
width: 400px;
height: 25px;
@ -80,6 +85,7 @@ docStyle.innerHTML = `
}
.cm-menu-container {
padding : calc(var(--spacing)*2);
column-gap: 20px;
display: flex;
flex-wrap: wrap;
@ -140,8 +146,8 @@ docStyle.innerHTML = `
}
.cm-notice-board {
width: 290px;
height: 230px;
width: auto;
height: 280px;
overflow: auto;
color: var(--input-text);
border: 1px solid var(--descrip-text);
@ -238,68 +244,50 @@ var is_updating = false;
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
const style = `
#workflowgallery-button {
width: 310px;
height: 27px;
height: 50px;
padding: 0px !important;
position: relative;
overflow: hidden;
font-size: 17px !important;
}
#cm-nodeinfo-button {
width: 310px;
height: 27px;
padding: 0px !important;
position: relative;
overflow: hidden;
font-size: 17px !important;
}
#cm-manual-button {
width: 310px;
height: 27px;
position: relative;
overflow: hidden;
}
.cm-button {
width: 310px;
height: 30px;
width: auto;
position: relative;
overflow: hidden;
font-size: 17px !important;
background-color: var(--comfy-menu-secondary-bg);
border-color: var(--border-color);
color: var(--input-text);
}
.cm-button:hover {
filter: brightness(125%);
}
.cm-button-red {
width: 310px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
background-color: #500000 !important;
border-color: #88181b !important;
color: white !important;
}
.cm-button-red:hover {
background-color: #88181b !important;
}
.cm-button-orange {
width: 310px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
font-weight: bold;
background-color: orange !important;
color: black !important;
}
.cm-experimental-button {
width: 290px;
height: 30px;
position: relative;
overflow: hidden;
font-size: 17px !important;
width: 100%;
}
.cm-experimental {
width: 310px;
border: 1px solid #555;
border-radius: 5px;
padding: 10px;
@ -326,8 +314,14 @@ const style = `
.cm-menu-combo {
cursor: pointer;
width: 310px;
box-sizing: border-box;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-menu-secondary-bg);
}
.cm-menu-combo:hover {
filter: brightness(125%);
}
.cm-small-button {
@ -831,7 +825,7 @@ class ManagerMenuDialog extends ComfyDialog {
const isElectron = 'electronAPI' in window;
update_comfyui_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update ComfyUI",
style: {
@ -842,7 +836,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
switch_comfyui_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Switch ComfyUI",
style: {
@ -853,7 +847,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
restart_stop_button =
$el("button.cm-button-red", {
$el("button.p-button.p-component.cm-button-red", {
type: "button",
textContent: "Restart",
onclick: () => restartOrStop()
@ -861,7 +855,7 @@ class ManagerMenuDialog extends ComfyDialog {
if(isElectron) {
update_all_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update All Custom Nodes",
onclick:
@ -870,7 +864,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
else {
update_all_button =
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Update All",
onclick:
@ -880,7 +874,7 @@ class ManagerMenuDialog extends ComfyDialog {
const res =
[
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Custom Nodes Manager",
onclick:
@ -892,7 +886,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Install Missing Custom Nodes",
onclick:
@ -904,7 +898,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Custom Nodes In Workflow",
onclick:
@ -916,8 +910,8 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("br", {}, []),
$el("button.cm-button", {
$el("div", {}, []),
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Model Manager",
onclick:
@ -929,7 +923,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
type: "button",
textContent: "Install via Git URL",
onclick: async () => {
@ -941,13 +935,13 @@ class ManagerMenuDialog extends ComfyDialog {
}
}),
$el("br", {}, []),
$el("div", {}, []),
update_all_button,
update_comfyui_button,
switch_comfyui_button,
// fetch_updates_button,
$el("br", {}, []),
$el("div", {}, []),
restart_stop_button,
];
@ -960,12 +954,13 @@ class ManagerMenuDialog extends ComfyDialog {
let self = this;
// db mode
this.datasrc_combo = document.createElement("select");
this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened.");
this.datasrc_combo.className = "cm-menu-combo";
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'DB: Channel (1day cache)' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'DB: Channel (remote)' }, []));
this.datasrc_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled ";
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'Channel (1day cache)' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'Local' }, []));
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'Channel (remote)' }, []));
api.fetchApi('/manager/db_mode')
.then(response => response.text())
@ -975,27 +970,110 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
});
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
// preview method
let preview_combo = document.createElement("select");
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process.");
preview_combo.className = "cm-menu-combo";
preview_combo.appendChild($el('option', { value: 'auto', text: 'Preview method: Auto' }, []));
preview_combo.appendChild($el('option', { value: 'taesd', text: 'Preview method: TAESD (slow)' }, []));
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Preview method: Latent2RGB (fast)' }, []));
preview_combo.appendChild($el('option', { value: 'none', text: 'Preview method: None (very fast)' }, []));
preview_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
// Loading state to prevent flash of enabled state
preview_combo.appendChild($el('option', { value: '', text: 'Loading...', disabled: true }, []));
preview_combo.appendChild($el('option', { value: 'auto', text: 'Auto' }, []));
preview_combo.appendChild($el('option', { value: 'taesd', text: 'TAESD (slow)' }, []));
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Latent2RGB (fast)' }, []));
preview_combo.appendChild($el('option', { value: 'none', text: 'None (very fast)' }, []));
// Start disabled to prevent flash
preview_combo.disabled = true;
preview_combo.value = '';
// Fetch current state
api.fetchApi('/manager/preview_method')
.then(response => response.text())
.then(data => { preview_combo.value = data; });
.then(data => {
// Remove loading option
preview_combo.querySelector('option[value=""]')?.remove();
if (data === "DISABLED") {
// ComfyUI per-queue preview feature is active
preview_combo.disabled = true;
preview_combo.value = 'auto';
// Accessibility attributes
preview_combo.setAttribute("aria-disabled", "true");
preview_combo.setAttribute("aria-label",
"Preview method setting (disabled - managed by ComfyUI). " +
"Use Settings > Execution > Live preview method instead."
);
// Tooltip for mouse users
preview_combo.setAttribute("title",
"This feature is now provided natively by ComfyUI. " +
"Please use 'Settings > Execution > Live preview method' instead."
);
// Visual feedback
preview_combo.style.opacity = '0.6';
preview_combo.style.cursor = 'not-allowed';
} else {
// Manager feature is active
preview_combo.disabled = false;
preview_combo.value = data;
// Accessibility for enabled state
preview_combo.setAttribute("aria-label",
"Preview method setting. Select how latent variables are decoded during preview."
);
}
})
.catch(error => {
console.error('[ComfyUI-Manager] Failed to fetch preview method status:', error);
// Error recovery: fallback to enabled
preview_combo.querySelector('option[value=""]')?.remove();
preview_combo.disabled = false;
preview_combo.value = 'auto';
});
preview_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
// Ignore if disabled
if (preview_combo.disabled) {
event.preventDefault();
return;
}
// Normal operation
api.fetchApi(`/manager/preview_method?value=${event.target.value}`)
.then(response => {
if (response.status === 403) {
// Feature transitioned to native
alert(
'This feature is now provided natively by ComfyUI.\n' +
'Please use \'Settings > Execution > Live preview method\' instead.'
);
preview_combo.disabled = true;
preview_combo.style.opacity = '0.6';
preview_combo.style.cursor = 'not-allowed';
// Update aria attributes
preview_combo.setAttribute("aria-disabled", "true");
preview_combo.setAttribute("aria-label",
"Preview method setting (disabled - managed by ComfyUI). " +
"Use Settings > Execution > Live preview method instead."
);
}
})
.catch(error => {
console.error('[ComfyUI-Manager] Preview method update failed:', error);
});
});
const previewSetttingItem = createSettingsCombo("Preview method", preview_combo);
// channel
let channel_combo = document.createElement("select");
channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list.");
channel_combo.className = "cm-menu-combo";
channel_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
api.fetchApi('/manager/channel_url_list')
.then(response => response.json())
.then(async data => {
@ -1004,7 +1082,7 @@ class ManagerMenuDialog extends ComfyDialog {
for (let i in urls) {
if (urls[i] != '') {
let name_url = urls[i].split('::');
channel_combo.appendChild($el('option', { value: name_url[0], text: `Channel: ${name_url[0]}` }, []));
channel_combo.appendChild($el('option', { value: name_url[0], text: `${name_url[0]}` }, []));
}
}
@ -1019,11 +1097,13 @@ class ManagerMenuDialog extends ComfyDialog {
}
});
const channelSetttingItem = createSettingsCombo("Channel", channel_combo);
// share
let share_combo = document.createElement("select");
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button.");
share_combo.className = "cm-menu-combo";
share_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
const share_options = [
['none', 'None'],
['openart', 'OpenArt AI'],
@ -1034,7 +1114,7 @@ class ManagerMenuDialog extends ComfyDialog {
['all', 'All'],
];
for (const option of share_options) {
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
share_combo.appendChild($el('option', { value: option[0], text: `${option[1]}` }, []));
}
api.fetchApi('/manager/share_option')
@ -1056,12 +1136,14 @@ class ManagerMenuDialog extends ComfyDialog {
}
});
const shareSetttingItem = createSettingsCombo("Share", share_combo);
let component_policy_combo = document.createElement("select");
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
component_policy_combo.className = "cm-menu-combo";
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, []));
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, []));
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, []));
component_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Use workflow version' }, []));
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Use higher version' }, []));
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Use my version' }, []));
api.fetchApi('/manager/policy/component')
.then(response => response.text())
.then(data => {
@ -1074,15 +1156,14 @@ class ManagerMenuDialog extends ComfyDialog {
set_component_policy(event.target.value);
});
update_policy_combo = document.createElement("select");
const componentSetttingItem = createSettingsCombo("Component", component_policy_combo);
if(isElectron)
update_policy_combo.style.display = 'none';
update_policy_combo = document.createElement("select");
update_policy_combo.setAttribute("title", "Sets the policy to be applied when performing an update.");
update_policy_combo.className = "cm-menu-combo";
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'Update: ComfyUI Stable Version' }, []));
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'Update: ComfyUI Nightly Version' }, []));
update_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'ComfyUI Stable Version' }, []));
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'ComfyUI Nightly Version' }, []));
api.fetchApi('/manager/policy/update')
.then(response => response.text())
.then(data => {
@ -1093,20 +1174,22 @@ class ManagerMenuDialog extends ComfyDialog {
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
});
return [
$el("br", {}, []),
this.datasrc_combo,
channel_combo,
preview_combo,
share_combo,
component_policy_combo,
update_policy_combo,
$el("br", {}, []),
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
if(isElectron)
updateSetttingItem.style.display = 'none';
$el("br", {}, []),
$el("filedset.cm-experimental", {}, [
return [
dbRetrievalSetttingItem,
channelSetttingItem,
previewSetttingItem,
shareSetttingItem,
componentSetttingItem,
updateSetttingItem,
//[TODO] replace mt-2 with wrapper div with flex column gap
$el("filedset.cm-experimental.mt-auto", {}, [
$el("legend.cm-experimental-legend", {}, ["EXPERIMENTAL"]),
$el("button.cm-experimental-button", {
$el("button.p-button.p-component.cm-button.cm-experimental-button", {
type: "button",
textContent: "Snapshot Manager",
onclick:
@ -1116,7 +1199,7 @@ class ManagerMenuDialog extends ComfyDialog {
SnapshotManager.instance.show();
}
}),
$el("button.cm-experimental-button", {
$el("button.p-button.p-component.cm-button.cm-experimental-button.mt-2", {
type: "button",
textContent: "Install PIP packages",
onclick:
@ -1134,7 +1217,7 @@ class ManagerMenuDialog extends ComfyDialog {
createControlsRight() {
const elts = [
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
id: 'cm-manual-button',
type: "button",
textContent: "Community Manual",
@ -1185,11 +1268,11 @@ class ManagerMenuDialog extends ComfyDialog {
})
]),
$el("button", {
$el("button.p-button.p-component.cm-button", {
id: 'workflowgallery-button',
type: "button",
style: {
...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
// ...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
},
onclick: (e) => {
const last_visited_site = localStorage.getItem("wg_last_visited")
@ -1212,7 +1295,7 @@ class ManagerMenuDialog extends ComfyDialog {
}, [
$el("p", {
id: 'workflowgallery-button-last-visited-label',
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : ''})`,
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : 'none selected'})`,
style: {
'text-align': 'center',
'color': 'var(--input-text)',
@ -1228,13 +1311,12 @@ class ManagerMenuDialog extends ComfyDialog {
})
]),
$el("button.cm-button", {
$el("button.p-button.p-component.cm-button", {
id: 'cm-nodeinfo-button',
type: "button",
textContent: "Nodes Info",
onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); }
}),
$el("br", {}, []),
];
var textarea = document.createElement("div");
@ -1249,31 +1331,23 @@ class ManagerMenuDialog extends ComfyDialog {
constructor() {
super();
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => this.close() });
const content = $el("div.cm-menu-container",
[
$el("div.cm-menu-column.gap-2", [...this.createControlsLeft()]),
$el("div.cm-menu-column.gap-2", [...this.createControlsMid()]),
$el("div.cm-menu-column.gap-2", [...this.createControlsRight()])
]
);
const content =
$el("div.comfy-modal-content",
[
$el("tr.cm-title", {}, [
$el("font", {size:6, color:"white"}, [`ComfyUI Manager ${manager_version}`])]
),
$el("br", {}, []),
$el("div.cm-menu-container",
[
$el("div.cm-menu-column", [...this.createControlsLeft()]),
$el("div.cm-menu-column", [...this.createControlsMid()]),
$el("div.cm-menu-column", [...this.createControlsRight()])
]),
const frame = buildGuiFrame(
'cm-manager-dialog', // dialog id
`ComfyUI Manager ${manager_version}`, // dialog title
"i.mdi.mdi-puzzle", // dialog icon class to show before title
content, // dialog content element
this
); // send this so we can attach close functions
$el("br", {}, []),
close_button,
]
);
content.style.width = '100%';
content.style.height = '100%';
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
this.element = frame;
}
get isVisible() {
@ -1281,7 +1355,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
show() {
this.element.style.display = "block";
this.element.style.display = "flex";
}
toggleVisibility() {

View File

@ -1,8 +1,9 @@
.cn-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80%;
height: 80%;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
@ -10,6 +11,7 @@
font-family: arial, sans-serif;
text-underline-offset: 3px;
outline: none;
margin: calc(var(--spacing)*2);
}
.cn-manager .cn-flex-auto {
@ -17,17 +19,21 @@
}
.cn-manager button {
width: auto;
position: relative;
overflow: hidden;
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
margin: 0;
padding: 4px 8px;
min-width: 100px;
}
.cn-manager button:hover {
filter: brightness(125%);
}
.cn-manager button:disabled,
.cn-manager input:disabled,
.cn-manager select:disabled {
@ -40,8 +46,13 @@
.cn-manager .cn-manager-restart {
display: none;
background-color: #500000;
color: white;
background-color: #500000 !important;
border-color: #88181b !important;
color: white !important;
}
.cn-manager .cn-manager-restart:hover {
background-color: #88181b !important;
}
.cn-manager .cn-manager-stop {
@ -79,7 +90,6 @@
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.cn-manager-header label {
@ -91,16 +101,32 @@
.cn-manager-filter {
height: 28px;
line-height: 28px;
cursor: pointer;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-input-bg);
}
.cn-manager-filter:hover {
filter: brightness(125%);
}
.cn-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background: var(--comfy-input-bg);
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
border: 1px solid var(--border-color);
border-radius: 6px;
outline-color: transparent;
}
.cn-manager-status {
@ -588,6 +614,10 @@
height: 100%;
}
.cn-install-buttons button {
padding: 4px 8px;
}
.cn-selected-buttons {
display: flex;
gap: 5px;

View File

@ -1,6 +1,7 @@
import { app } from "../../scripts/app.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { api } from "../../scripts/api.js";
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
import {
manager_instance, rebootAPI, install_via_git_url,
@ -18,32 +19,19 @@ loadCss("./custom-nodes-manager.css");
const gridId = "node";
const pageHtml = `
<div class="cn-manager-header">
<label>Filter
<select class="cn-manager-filter"></select>
</label>
<input class="cn-manager-keywords" type="search" placeholder="Search" />
<div class="cn-manager-status"></div>
<div class="cn-flex-auto"></div>
<div class="cn-manager-channel"></div>
</div>
<div class="cn-manager-grid"></div>
<div class="cn-manager-selection"></div>
<div class="cn-manager-message"></div>
<div class="cn-manager-footer">
<button class="cn-manager-back">
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<button class="cn-manager-restart">Restart</button>
<button class="cn-manager-stop">Stop</button>
<div class="cn-flex-auto"></div>
<button class="cn-manager-used-in-workflow">Used In Workflow</button>
<button class="cn-manager-check-update">Check Update</button>
<button class="cn-manager-check-missing">Check Missing</button>
<button class="cn-manager-install-url">Install via Git URL</button>
<div class="cn-manager cn-manager-dark">
<div class="cn-manager-grid"></div>
<div class="cn-manager-selection"></div>
<div class="cn-manager-message"></div>
<div class="cn-manager-footer">
<button class="cn-manager-restart p-button p-component">Restart</button>
<button class="cn-manager-stop p-button p-component">Stop</button>
<div class="cn-flex-auto"></div>
<button class="cn-manager-used-in-workflow p-button p-component">Used In Workflow</button>
<button class="cn-manager-check-update p-button p-component">Check Update</button>
<button class="cn-manager-check-missing p-button p-component">Check Missing</button>
<button class="cn-manager-install-url p-button p-component">Install via Git URL</button>
</div>
</div>
`;
@ -89,11 +77,26 @@ export class CustomNodesManager {
}
init() {
this.element = $el("div", {
parent: document.body,
className: "comfy-modal cn-manager"
});
this.element.innerHTML = pageHtml;
const header = $el("div.cn-manager-header.px-2", {}, [
// $el("label", {}, [
// $el("span", { textContent: "Filter" }),
// $el("select.cn-manager-filter")
// ]),
createSettingsCombo("Filter", $el("select.cn-manager-filter")),
$el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
$el("div.cn-manager-status"),
$el("div.cn-flex-auto"),
$el("div.cn-manager-channel")
]);
const frame = buildGuiFrameCustomHeader(
'cn-manager-dialog', // dialog id
header, // custom header element
pageHtml, // dialog content element
this
); // send this so we can attach close functions
this.element = frame;
this.element.setAttribute("tabindex", 0);
this.element.focus();
@ -372,7 +375,7 @@ export class CustomNodesManager {
return list.map(id => {
const bt = buttons[id];
return `<button class="cn-btn-${id}" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
return `<button class="cn-btn-${id} p-button p-component" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
}).join("");
}
@ -655,7 +658,6 @@ export class CustomNodesManager {
}
renderGrid() {
// update theme
const globalStyle = window.getComputedStyle(document.body);
this.colorVars = {

View File

@ -1,13 +1,15 @@
.cmm-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80%;
height: 80%;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
margin: calc(var(--spacing)*2);
}
.cmm-manager .cmm-flex-auto {
@ -18,14 +20,15 @@
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-radius: 8px;
border-color: var(--border-color);
border-style: solid;
margin: 0;
padding: 4px 8px;
min-width: 100px;
}
.cmm-manager button:hover {
filter: brightness(125%);
}
.cmm-manager button:disabled,
.cmm-manager input:disabled,
.cmm-manager select:disabled {
@ -38,7 +41,7 @@
.cmm-manager .cmm-manager-refresh {
display: none;
background-color: #000080;
background-color: #000080 !important;
color: white;
}
@ -53,7 +56,6 @@
flex-wrap: wrap;
gap: 5px;
align-items: center;
padding: 0 5px;
}
.cmm-manager-header label {
@ -67,16 +69,34 @@
.cmm-manager-filter {
height: 28px;
line-height: 28px;
cursor: pointer;
padding: 0.5em 0.5em;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--comfy-input-bg);
}
.cmm-manager-type:hover,
.cmm-manager-base:hover,
.cmm-manager-filter:hover {
filter: brightness(125%);
}
.cmm-manager-keywords {
height: 28px;
line-height: 28px;
padding: 0 5px 0 26px;
background: var(--comfy-input-bg);
background-size: 16px;
background-position: 5px center;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
border: 1px solid var(--border-color);
border-radius: 6px;
outline-color: transparent;
}
.cmm-manager-status {
@ -148,6 +168,10 @@
color: white;
}
.cmm-btn-install {
padding: 4px 8px;
}
.cmm-btn-download {
width: 18px;
height: 18px;

View File

@ -9,39 +9,22 @@ import { api } from "../../scripts/api.js";
// https://cenfun.github.io/turbogrid/api.html
import TG from "./turbogrid.esm.js";
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
loadCss("./model-manager.css");
const gridId = "model";
const pageHtml = `
<div class="cmm-manager-header">
<label>Filter
<select class="cmm-manager-filter"></select>
</label>
<label>Type
<select class="cmm-manager-type"></select>
</label>
<label>Base
<select class="cmm-manager-base"></select>
</label>
<input class="cmm-manager-keywords" type="search" placeholder="Search" />
<div class="cmm-manager-status"></div>
<div class="cmm-flex-auto"></div>
</div>
<div class="cmm-manager-grid"></div>
<div class="cmm-manager-selection"></div>
<div class="cmm-manager-message"></div>
<div class="cmm-manager-footer">
<button class="cmm-manager-back">
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<button class="cmm-manager-refresh">Refresh</button>
<button class="cmm-manager-stop">Stop</button>
<div class="cmm-flex-auto"></div>
<div class="cmm-manager cmm-manager-dark">
<div class="cmm-manager-grid"></div>
<div class="cmm-manager-selection"></div>
<div class="cmm-manager-message"></div>
<div class="cmm-manager-footer">
<button class="cmm-manager-refresh p-button p-component">Refresh</button>
<button class="cmm-manager-stop p-button p-component">Stop</button>
<div class="cmm-flex-auto"></div>
</div>
</div>
`;
@ -64,11 +47,23 @@ export class ModelManager {
}
init() {
this.element = $el("div", {
parent: document.body,
className: "comfy-modal cmm-manager"
});
this.element.innerHTML = pageHtml;
const header = $el("div.cmm-manager-header", {}, [
createSettingsCombo("Filter", $el("select.cmm-manager-filter")),
createSettingsCombo("Type", $el("select.cmm-manager-type")),
createSettingsCombo("Base", $el("select.cmm-manager-base")),
$el("input.cmm-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
$el("div.cmm-manager-status"),
$el("div.cmm-flex-auto")
]);
const frame = buildGuiFrameCustomHeader(
'cmm-manager-dialog', // dialog id
header, // custom header element
pageHtml, // dialog content element
this
); // send this so we can attach close functions
this.element = frame;
this.initFilter();
this.bindEvents();
this.initGrid();
@ -347,7 +342,7 @@ export class ModelManager {
if (installed === "True") {
return `<div class="cmm-icon-passed">${icons.passed}</div>`;
}
return `<button class="cmm-btn-install" mode="install">Install</button>`;
return `<button class="cmm-btn-install p-button p-component" mode="install">Install</button>`;
}
}, {
id: 'url',
@ -420,7 +415,7 @@ export class ModelManager {
}
this.selectedModels = selectedList;
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install" mode="install">Install</button>`);
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install p-button p-component" mode="install">Install</button>`);
}
focusInstall(item) {

65
js/snapshot.css Normal file
View File

@ -0,0 +1,65 @@
.snapshot-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099;
width: 80vw;
height: 75vh;
min-height: 30em;
display: flex;
flex-direction: column;
gap: 10px;
color: var(--fg-color);
font-family: arial, sans-serif;
text-underline-offset: 3px;
outline: none;
margin: calc(var(--spacing)*2);
}
.snapshot-manager button {
width: auto;
position: relative;
overflow: hidden;
font-size: 16px;
color: var(--input-text);
background-color: var(--comfy-input-bg);
border-color: var(--border-color);
margin: 0;
min-width: 100px;
padding: 4px 8px;
}
.snapshot-manager .snapshot-restore-btn {
background-color: #00158f !important;
border-color: #2025b9 !important;
color: white !important;
}
.snapshot-manager .snapshot-remove-btn {
background-color: #970000 !important;
border-color: #be2127 !important;
color: white !important;
}
.snapshot-manager button:hover {
filter: brightness(125%);
}
.snapshot-manager .data-btns {
display: flex;
flex-direction: column;
gap: calc(var(--spacing)*2);
padding: calc(var(--spacing)*2);
align-items: center;
justify-content: center;
height: 100%;
}
.snapshot-footer {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.snapshot-manager .cn-flex-auto {
flex: auto;
}

View File

@ -1,8 +1,10 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { manager_instance, rebootAPI, show_message, handle403Response } from "./common.js";
import { manager_instance, rebootAPI, show_message, handle403Response, loadCss } from "./common.js";
import { buildGuiFrame } from "./comfyui-gui-builder.js";
loadCss("./snapshot.css");
async function restore_snapshot(target) {
if(SnapshotManager.instance) {
@ -27,7 +29,7 @@ async function restore_snapshot(target) {
}
finally {
await SnapshotManager.instance.invalidateControl();
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='cm-small-button'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='p-button p-component'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
}
}
}
@ -88,6 +90,8 @@ export class SnapshotManager extends ComfyDialog {
message_box = null;
data = null;
content = $el("div.snapshot-manager");
clear() {
this.restore_buttons = [];
this.message_box = null;
@ -96,9 +100,18 @@ export class SnapshotManager extends ComfyDialog {
constructor(app, manager_dialog) {
super();
this.manager_dialog = manager_dialog;
// this.manager_dialog = manager_dialog;
this.search_keyword = '';
this.element = $el("div.comfy-modal", { parent: document.body }, []);
const frame = buildGuiFrame(
'snapshot-manager-dialog', // dialog id
'Snapshot Manager', // title
'i.mdi.mdi-puzzle', // icon class
this.content, // dialog content element
this
); // send this so we can attach close functions
this.element = frame;
}
async remove_item() {
@ -109,7 +122,7 @@ export class SnapshotManager extends ComfyDialog {
createControls() {
return [
$el("button.cm-small-button", {
$el("button.p-button.p-component", {
type: "button",
textContent: "Close",
onclick: () => { this.close(); }
@ -132,8 +145,8 @@ export class SnapshotManager extends ComfyDialog {
this.clear();
this.data = (await getSnapshotList()).items;
while (this.element.children.length) {
this.element.removeChild(this.element.children[0]);
while (this.content.children.length) {
this.content.removeChild(this.content.children[0]);
}
await this.createGrid();
@ -204,20 +217,21 @@ export class SnapshotManager extends ComfyDialog {
data2.innerHTML = `&nbsp;${data}`;
var data_button = document.createElement('td');
data_button.style.textAlign = "center";
data_button.className = "data-btns";
var restoreBtn = document.createElement('button');
restoreBtn.className = "snapshot-restore-btn p-button p-component";
restoreBtn.innerHTML = 'Restore';
restoreBtn.style.width = "100px";
restoreBtn.style.backgroundColor = 'blue';
restoreBtn.addEventListener('click', function() {
restore_snapshot(data);
});
var removeBtn = document.createElement('button');
removeBtn.className = "snapshot-remove-btn p-button p-component";
removeBtn.innerHTML = 'Remove';
removeBtn.style.width = "100px";
removeBtn.style.backgroundColor = 'red';
removeBtn.addEventListener('click', function() {
remove_snapshot(data);
@ -241,13 +255,14 @@ export class SnapshotManager extends ComfyDialog {
let self = this;
const panel = document.createElement('div');
panel.style.width = "100%";
panel.style.height = "100%";
panel.appendChild(grid);
function handleResize() {
const parentHeight = self.element.clientHeight;
const gridHeight = parentHeight - 200;
grid.style.height = gridHeight + "px";
// grid.style.height = gridHeight + "px";
}
window.addEventListener("resize", handleResize);
@ -256,25 +271,17 @@ export class SnapshotManager extends ComfyDialog {
grid.style.width = "100%";
grid.style.height = "100%";
grid.style.overflowY = "scroll";
this.element.style.height = "85%";
this.element.style.width = "80%";
this.element.appendChild(panel);
this.content.appendChild(panel);
handleResize();
}
async createBottomControls() {
var close_button = document.createElement("button");
close_button.className = "cm-small-button";
close_button.innerHTML = "Close";
close_button.onclick = () => { this.close(); }
close_button.style.display = "inline-block";
var save_button = document.createElement("button");
save_button.className = "cm-small-button";
save_button.className = "p-button p-component";
save_button.innerHTML = "Save snapshot";
save_button.onclick = () => { save_current_snapshot(); }
save_button.style.display = "inline-block";
save_button.style.horizontalAlign = "right";
save_button.style.width = "170px";
@ -282,15 +289,19 @@ export class SnapshotManager extends ComfyDialog {
this.message_box.style.height = '60px';
this.message_box.style.verticalAlign = 'middle';
this.element.appendChild(this.message_box);
this.element.appendChild(close_button);
this.element.appendChild(save_button);
const footer = $el("div.snapshot-footer");
const spacer = $el("div.cn-flex-auto");
footer.appendChild(spacer);
footer.appendChild(save_button);
this.content.appendChild(this.message_box);
this.content.appendChild(footer);
}
async show() {
try {
this.invalidateControl();
this.element.style.display = "block";
this.element.style.display = "flex";
this.element.style.zIndex = 1099;
}
catch(exception) {

View File

@ -1,5 +1,335 @@
{
"custom_nodes": [
"custom_nodes": [
{
"author": "love530love",
"title": "[WIP] ComfyUI-TorchMonitor",
"reference": "https://github.com/love530love/ComfyUI-TorchMonitor",
"files": [
"https://github.com/love530love/ComfyUI-TorchMonitor"
],
"install_type": "git-clone",
"description": "Fixed-position real-time monitor for ComfyUI displaying CPU, RAM, VRAM, and GPU temperature metrics with zero configuration and single-file installation.\nNOTE: The files in the repo are not organized."
},
{
"author": "Enferlain",
"title": "ComfyUI-SamplerCustom-3Decimals",
"reference": "https://github.com/Enferlain/ComfyUI-SamplerCustom-3Decimals",
"files": [
"https://github.com/Enferlain/ComfyUI-SamplerCustom-3Decimals"
],
"install_type": "git-clone",
"description": "ComfyUI sampler node with custom 3 decimals precision for sampling parameters. (Description by CC)"
},
{
"author": "lfelipegg",
"title": "[WIP] lfgg_custom_nodes_comfyui",
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes_comfyui",
"files": [
"https://github.com/lfelipegg/lfgg_custom_nodes_comfyui"
],
"install_type": "git-clone",
"description": "LFGG custom nodes for ComfyUI providing resolution-first utilities, latent sizing by aspect ratio, and divisibility-aware image processing.\nNOTE: The files in the repo are not organized."
},
{
"author": "saltchicken",
"title": "ComfyUI-Selector",
"reference": "https://github.com/saltchicken/ComfyUI-Selector",
"files": [
"https://github.com/saltchicken/ComfyUI-Selector"
],
"install_type": "git-clone",
"description": "ComfyUI node providing simple selector functionality. (Description by CC)"
},
{
"author": "saltchicken",
"title": "ComfyUI-Video-Utils",
"reference": "https://github.com/saltchicken/ComfyUI-Video-Utils",
"files": [
"https://github.com/saltchicken/ComfyUI-Video-Utils"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for video processing including FinalFrameSelector and VideoMerge functionality. (Description by CC)"
},
{
"author": "EricRorich",
"title": "[WIP] ComfyUI-MegaTran-cutom-node",
"reference": "https://github.com/EricRorich/ComfyUI-MegaTran-cutom-node",
"files": [
"https://github.com/EricRorich/ComfyUI-MegaTran-cutom-node"
],
"install_type": "git-clone",
"description": "ComfyUI custom node applying image transformations (scaling, rotation, translation) around a pivot point with optional canvas expansion and pivot visualization. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "OhSeongHyeon",
"title": "comfyui-random-image-size",
"reference": "https://github.com/OhSeongHyeon/comfyui-random-image-size",
"files": [
"https://github.com/OhSeongHyeon/comfyui-random-image-size"
],
"install_type": "git-clone",
"description": "ComfyUI Random Image Size"
},
{
"author": "Enferlain",
"title": "ComfyUI-Model-Comparison [WIP]",
"reference": "https://github.com/Enferlain/ComfyUI-Model-Comparison",
"files": [
"https://github.com/Enferlain/ComfyUI-Model-Comparison"
],
"install_type": "git-clone",
"description": "ComfyUI node for comparing multiple models side-by-side. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "AMTPorn",
"title": "comfyui_amt",
"reference": "https://github.com/AMTPorn/comfyui_amt",
"files": [
"https://github.com/AMTPorn/comfyui_amt"
],
"install_type": "git-clone",
"description": "NODES: AMTStringDeduplication"
},
{
"author": "gajjar4",
"title": "ComfyUI-Qwen-Image-i2L [UNSAFE]",
"reference": "https://github.com/gajjar4/ComfyUI-Qwen-Image-i2L",
"files": [
"https://github.com/gajjar4/ComfyUI-Qwen-Image-i2L"
],
"install_type": "git-clone",
"description": "A fully optimized ComfyUI custom node for Qwen-Image-i2L (Image-to-LoRA) that extracts style, composition, or details from images and saves them as lightweight LoRA files with intelligent VRAM optimization. (Description by CC)[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "edvardtoth",
"title": "ComfyUI-ETNodes",
"reference": "https://github.com/edvardtoth/ComfyUI-ETNodes",
"files": [
"https://github.com/edvardtoth/ComfyUI-ETNodes"
],
"install_type": "git-clone",
"description": "NODES: ETNodes-Color-Selector, ETNodes-Gemini-API-Image, ETNodes-Gemini-API-Text, ETNodes-List-Items, ETNodes-List-Selector, ETNodes-Text-Previe"
},
{
"author": "jtydhr88",
"title": "ComfyUI-PolotnoCanvasEditor [UNSAFE]",
"reference": "https://github.com/jtydhr88/ComfyUI-PolotnoCanvasEditor",
"files": [
"https://github.com/jtydhr88/ComfyUI-PolotnoCanvasEditor"
],
"install_type": "git-clone",
"description": "Integrates Polotno Canvas Editor into ComfyUI for advanced image editing and design.[w/This nodepack contains a path traversal vulnerability.]"
},
{
"author": "Taremin",
"title": "comfyui-remove-print",
"reference": "https://github.com/Taremin/comfyui-remove-print",
"files": [
"https://github.com/Taremin/comfyui-remove-print"
],
"install_type": "git-clone",
"description": "ComfyUI extension for removing or suppressing print statements in node output. (Description by CC)"
},
{
"author": "JiangAogo",
"title": "ComfyUI-Gemini-API [WIP]",
"reference": "https://github.com/JiangAogo/ComfyUI-Gemini-API",
"files": [
"https://github.com/JiangAogo/ComfyUI-Gemini-API"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for Google Gemini API integration, supporting both text generation (LLM) and image generation.\nNOTE: The files in the repo are not organized."
},
{
"author": "AprEcho",
"title": "ComfyUI-RandomSeed",
"reference": "https://github.com/AprEcho/ComfyUI-RandomSeed",
"files": [
"https://github.com/AprEcho/ComfyUI-RandomSeed"
],
"install_type": "git-clone",
"description": "Generates random seed values for ComfyUI workflows. (Description by CC)"
},
{
"author": "Suzu008",
"title": "ComfyUI-ImageCritic",
"reference": "https://github.com/Suzu008/ComfyUI-ImageCritic",
"files": [
"https://github.com/Suzu008/ComfyUI-ImageCritic"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for image analysis and evaluation. (Description by CC)"
},
{
"author": "Spicely",
"title": "[WIP] ComfyUI-Luma",
"reference": "https://github.com/Spicely/ComfyUI-Luma",
"files": [
"https://github.com/Spicely/ComfyUI-Luma"
],
"install_type": "git-clone",
"description": "ComfyUI utility providing video and audio processing capabilities including text watermarking, audio-video separation, and audio-to-subtitle conversion. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "SSYCloud",
"title": "comfyui-ssy-syncapi [WIP]",
"reference": "https://github.com/SSYCloud/comfyui-ssy-syncapi",
"files": [
"https://github.com/SSYCloud/comfyui-ssy-syncapi"
],
"install_type": "git-clone",
"description": "Powerful ComfyUI custom node collection providing 4 dedicated nodes to access SSY Cloud synchronous image generation and processing models including Google Gemini, ByteDance Doubao, OpenAI, and image enhancement APIs. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "LMietkiewicz",
"title": "HiggsfieldAPI_Node",
"reference": "https://github.com/LMietkiewicz/HiggsfieldAPI_Node",
"files": [
"https://github.com/LMietkiewicz/HiggsfieldAPI_Node"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for Higgsfield API integration. (Description by CC)"
},
{
"author": "starsFriday",
"title": "ComfyUI-LongCat-Image [WIP]",
"reference": "https://github.com/starsFriday/ComfyUI-LongCat-Image",
"files": [
"https://github.com/starsFriday/ComfyUI-LongCat-Image"
],
"install_type": "git-clone",
"description": "ComfyUI wrapper nodes for the LongCat-Image text-to-image and image-editing pipelines with Python 3.12 support.\nNOTE: The files in the repo are not organized."
},
{
"author": "logicalor",
"title": "comfyui_mv_adapter [WIP]",
"reference": "https://github.com/logicalor/comfyui_mv_adapter",
"files": [
"https://github.com/logicalor/comfyui_mv_adapter"
],
"install_type": "git-clone",
"description": "MV-Adapter nodes for ComfyUI - Multi-view image generation\nNOTE: The files in the repo are not organized."
},
{
"author": "rafstahelin",
"title": "ComfyUI_KieNanoBananaPro",
"reference": "https://github.com/rafstahelin/ComfyUI_KieNanoBananaPro",
"files": [
"https://github.com/rafstahelin/ComfyUI_KieNanoBananaPro"
],
"install_type": "git-clone",
"description": "ComfyUI nodes for processing and filtering using KieNanoBananaPro algorithm. (Description by CC)"
},
{
"author": "jinchanz",
"title": "ComfyUI-Midjourney",
"reference": "https://github.com/jinchanz/ComfyUI-Midjourney",
"files": [
"https://github.com/jinchanz/ComfyUI-Midjourney"
],
"install_type": "git-clone",
"description": "ComfyUI integration for Midjourney API with nodes for submitting requests, polling results, and extracting JSON data. (Description by CC)"
},
{
"author": "saltchicken",
"title": "ComfyUI-Local-Loader",
"reference": "https://github.com/saltchicken/ComfyUI-Local-Loader",
"files": [
"https://github.com/saltchicken/ComfyUI-Local-Loader"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for loading images from specified directories and file paths. (Description by CC)"
},
{
"author": "satyasairazole",
"title": "ComfyUI-Turbandetection [WIP]",
"reference": "https://github.com/satyasairazole/ComfyUI-Turbandetection",
"files": [
"https://github.com/satyasairazole/ComfyUI-Turbandetection"
],
"install_type": "git-clone",
"description": "ComfyUI node for detecting turbans in images using computer vision. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "dougbtv",
"title": "comfyui-vllm-omni",
"reference": "https://github.com/dougbtv/comfyui-vllm-omni",
"files": [
"https://github.com/dougbtv/comfyui-vllm-omni"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for vLLM-Omni text-to-image generation"
},
{
"author": "u5dev",
"title": "ComfyUI_u5_EasyScripter [UNSAFE]",
"reference": "https://github.com/u5dev/ComfyUI_u5_EasyScripter",
"files": [
"https://github.com/u5dev/ComfyUI_u5_EasyScripter"
],
"install_type": "git-clone",
"description": "EASY VBA-style script for ComfyUI. Anything you want be in 1 node. Conditional branching, iteration, prompt generation, parameter adjustment, memory release, file input/output, Using HTTP RestAPI ... with 100+ built-in functions![w/This nodepack has RCE, SSRF, and arbitrary file read vulnerabilities.]"
},
{
"author": "stalkervr",
"title": "ComfyUI-StalkerVr",
"reference": "https://github.com/stalkervr/ComfyUI-StalkerVr",
"files": [
"https://github.com/stalkervr/ComfyUI-StalkerVr"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for image processing, aspect ratio fixing, batch cropping, grid manipulation, JSON handling, and value extraction. (Description by CC)"
},
{
"author": "baoanhng",
"title": "ComfyUI-utils",
"reference": "https://github.com/baoanhng/ComfyUI-utils",
"files": [
"https://github.com/baoanhng/ComfyUI-utils"
],
"install_type": "git-clone",
"description": "Provides text utility nodes (TextJoiner, TextSplitter) for ComfyUI workflows. (Description by CC)"
},
{
"author": "xuchenxu168",
"title": "[WIP] comfyui_meituan_image",
"reference": "https://github.com/xuchenxu168/comfyui_meituan_image",
"files": [
"https://github.com/xuchenxu168/comfyui_meituan_image"
],
"install_type": "git-clone",
"description": "Generate high-quality images from text prompts with excellent Chinese text renderingEdit images using natural language instructions..\nNOTE: The files in the repo are not organized."
},
{
"author": "saltchicken",
"title": "ComfyUI-Identity-Mixer",
"reference": "https://github.com/saltchicken/ComfyUI-Identity-Mixer",
"files": [
"https://github.com/saltchicken/ComfyUI-Identity-Mixer"
],
"install_type": "git-clone",
"description": "Mixes multiple identity LoRAs with normalized strength."
},
{
"author": "saltchicken",
"title": "ComfyUI-Prompter",
"reference": "https://github.com/saltchicken/ComfyUI-Prompter",
"files": [
"https://github.com/saltchicken/ComfyUI-Prompter"
],
"install_type": "git-clone",
"description": "ComfyUI custom node providing customizable prompt generation capabilities. (Description by CC)"
},
{
"author": "JBKing514",
"title": "[WIP] map_comfyui",
"reference": "https://github.com/JBKing514/map_comfyui",
"files": [
"https://github.com/JBKing514/map_comfyui"
],
"install_type": "git-clone",
"description": "A custom node implementing the Manifold Alignment Protocol (MAP) within ComfyUI, transforming diffusion sampling into a measurable and visualizable geometric process. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "Nynxz",
"title": "ComfyUI_DiffsynthPause",
@ -50,16 +380,6 @@
"install_type": "git-clone",
"description": "ComfyUI custom node that splits multi-line prompts by line, enabling batch image generation with each line triggering one execution and supporting custom prompt boxes. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "ashtar1984",
"title": "comfyui-switch-bypass-mute-by-group",
"reference": "https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group",
"files": [
"https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for group-based node switching, bypassing, and muting control. (Description by CC)"
},
{
"author": "rookiestar28",
"title": "ComfyUI_Security_Audit",
@ -1434,16 +1754,6 @@
"install_type": "git-clone",
"description": "A powerful ComfyUI extension that helps you focus on selected nodes by highlighting their connections while dimming or hiding unrelated links\nNOTE: The files in the repo are not organized."
},
{
"author": "pizurny",
"title": "ComfyUI-Just-DWPose [WIP]",
"reference": "https://github.com/pizurny/ComfyUI-Just-DWPose",
"files": [
"https://github.com/pizurny/ComfyUI-Just-DWPose"
],
"install_type": "git-clone",
"description": "An advanced DWPose annotator for ComfyUI with TorchScript and ONNX backends, featuring comprehensive pose detection, bone validation, temporal smoothing, and custom visualization tools."
},
{
"author": "pizurny",
"title": "ComfyUI Feedback Sampler [WIP]",
@ -1864,16 +2174,6 @@
"install_type": "git-clone",
"description": "NODES: 'Pluto : Auto Crop Faces', 'Pluto : Composite Image', 'Pluto : Float Math', 'Pluto : GetSizeFromImage', 'Pluto : Text Append', ..."
},
{
"author": "lfelipegg",
"title": "lfgg_custom_nodes [WIP]",
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes",
"files": [
"https://github.com/lfelipegg/lfgg_custom_nodes"
],
"install_type": "git-clone",
"description": "NODES: ModelMergeCombos\nNOTE: The files in the repo are not organized."
},
{
"author": "lu64k",
"title": "ks_nodes",
@ -5138,16 +5438,6 @@
"install_type": "git-clone",
"description": "ComfyUI implementation of the partfield nvidea segmentation models\nNOTE: The files in the repo are not organized."
},
{
"author": "shinich39",
"title": "comfyui-nothing-happened",
"reference": "httphttps://github.com/shinich39/comfyui-nothing-happened",
"files": [
"https://github.com/shinich39/comfyui-nothing-happened"
],
"description": "Save image and keep metadata.",
"install_type": "git-clone"
},
{
"author": "silveroxides",
"title": "ComfyUI_ReduxEmbedToolkit",
@ -8440,16 +8730,6 @@
"install_type": "git-clone",
"description": "Gets a random file from a directory. Returns the filpath as a STRING. [w/This node allows access to arbitrary files through the workflow, which could pose a security threat.]"
},
{
"author": "neeltheninja",
"title": "ComfyUI-ControlNeXt [WIP]",
"reference": "https://github.com/neverbiasu/ComfyUI-ControlNeXt",
"files": [
"https://github.com/neverbiasu/ComfyUI-ControlNeXt"
],
"install_type": "git-clone",
"description": "In progress🚧"
},
{
"author": "neeltheninja",
"title": "ComfyUI-TextOverlay",

View File

@ -375,6 +375,7 @@
"PD_Image_centerCrop",
"PD_JoinStringMultiLine",
"PD_LoadImageMetadata",
"PD_LoadImageWithMeta",
"PD_LoadImagesFromDir",
"PD_LoadImagesFromZip",
"PD_LoadTextsFromDir",
@ -404,6 +405,7 @@
"PD_Zip_Simple",
"PD_del_word",
"PD_empty_word",
"PD_if",
"PD_image_coversaver",
"PD_image_ratio_size",
"PD_image_to_text_v1",
@ -632,6 +634,16 @@
"title_aux": "AIGCZero-comfyui-Qwen_edit-zero"
}
],
"https://github.com/AMTPorn/comfyui_amt": [
[
"AMTMultiConditionRegexReplace",
"AMTReplaceAndMaybeDropString",
"AMTStringDeduplication"
],
{
"title_aux": "comfyui_amt"
}
],
"https://github.com/APZmedia/ComfyUI-folder-parser": [
[
"APZFolderParser"
@ -857,6 +869,7 @@
],
"https://github.com/AlexYez/comfyui-timesaver": [
[
"ModelScanner",
"TS Cube to Equirectangular",
"TS Equirectangular to Cube",
"TS Files Downloader",
@ -869,7 +882,6 @@
"TS_Color_Grade",
"TS_DeflickerNode",
"TS_FilePathLoader",
"TS_FilmEmulation",
"TS_FilmGrain",
"TS_Film_Emulation",
"TS_Free_Video_Memory",
@ -965,6 +977,14 @@
"title_aux": "ComfyUI_deepDeband [WIP]"
}
],
"https://github.com/AprEcho/ComfyUI-RandomSeed": [
[
"RandomSeed"
],
{
"title_aux": "ComfyUI-RandomSeed"
}
],
"https://github.com/ArmandAlbert/Kwai_font_comfyui": [
[
"Kwaifont_Image_Cropper",
@ -1784,6 +1804,22 @@
"title_aux": "ComfyUI-Math [WIP]"
}
],
"https://github.com/Enferlain/ComfyUI-Model-Comparison": [
[
"ModelComparisoner"
],
{
"title_aux": "ComfyUI-Model-Comparison [WIP]"
}
],
"https://github.com/Enferlain/ComfyUI-SamplerCustom-3Decimals": [
[
"SamplerCustom3Decimals"
],
{
"title_aux": "ComfyUI-SamplerCustom-3Decimals"
}
],
"https://github.com/Enferlain/ComfyUI-extra-schedulers": [
[
"BetaSchedulerV2C",
@ -1832,6 +1868,15 @@
"title_aux": "Comfy-Metadata-System [WIP]"
}
],
"https://github.com/EricRorich/ComfyUI-MegaTran-cutom-node": [
[
"Mega tran",
"Megatran"
],
{
"title_aux": "[WIP] ComfyUI-MegaTran-cutom-node"
}
],
"https://github.com/Estanislao-Oviedo/ComfyUI-CustomNodes": [
[
"Attention couple",
@ -2144,8 +2189,11 @@
],
"https://github.com/IgPoly/ComfyUI-igTools": [
[
"IGT_AspectRatioResizer",
"IGT_FloatMinMax",
"IGT_ImageResizer",
"IGT_ImageTilesCalc",
"IGT_IntMinMax",
"IGT_SimpleTilesCalc"
],
{
@ -2197,6 +2245,14 @@
"title_aux": "comfyui-codeformer [WIP]"
}
],
"https://github.com/JBKing514/map_comfyui": [
[
"MAP_Pro_Suite"
],
{
"title_aux": "[WIP] map_comfyui"
}
],
"https://github.com/JHBOY-ha/ComfyUI-jh-essential": [
[
"EndTimer",
@ -2249,6 +2305,15 @@
"title_aux": "ComfyUI-yolov5-face [WIP]"
}
],
"https://github.com/JiangAogo/ComfyUI-Gemini-API": [
[
"GeminiImageGenerator",
"GeminiLLM"
],
{
"title_aux": "ComfyUI-Gemini-API [WIP]"
}
],
"https://github.com/Jiffies-64/ComfyUI-SaveImagePlus": [
[
"SaveImagePlus"
@ -2307,15 +2372,18 @@
"https://github.com/JosDigby/ComfyUI-DigbyWan": [
[
"DigbyLoopClose",
"DigbyLoopCloseState",
"DigbyLoopOpen",
"DigbyLoopOpenState",
"DigbyLoopStatePack",
"DigbyLoopStateUnpack",
"ImageBatchLoopExtract",
"WanMiddleFrameToVideo",
"WanSmoothVideoTransition",
"WanVACEVideoSmoother"
"DigbyLoopRetrieveImages",
"DigbyLoopStoreImages",
"DigbyLoopVariables",
"DigbyLoopVariablesInit",
"DigbyWan22MiddleFrameToVideo",
"DigbyWan22SmoothVideoTransition",
"DigbyWanMoeKSampler",
"DigbyWanMoeKSamplerBasic",
"DigbyWanVACEVideoBridge",
"DigbyWanVACEVideoExtend",
"DigbyWanVACEVideoSmooth"
],
{
"title_aux": "ComfyUI-DigbyWan"
@ -2571,6 +2639,14 @@
"title_aux": "comfyui_LK_selfuse"
}
],
"https://github.com/LMietkiewicz/HiggsfieldAPI_Node": [
[
"HiggsfieldAPI"
],
{
"title_aux": "HiggsfieldAPI_Node"
}
],
"https://github.com/LSDJesus/ComfyUI-Luna-Collection": [
[
"LunaBatchPromptExtractor",
@ -2591,6 +2667,8 @@
"LunaExpressionPromptBuilder",
"LunaExpressionSlicerSaver",
"LunaGGUFConverter",
"LunaKSampler",
"LunaKSamplerAdvanced",
"LunaLoRARandomizer",
"LunaLoRAStacker",
"LunaLoRATriggerInjector",
@ -2599,8 +2677,10 @@
"LunaModelRouter",
"LunaMultiSaver",
"LunaOptimizedWeightsManager",
"LunaPipeExpander",
"LunaPromptCraft",
"LunaPromptCraftDebug",
"LunaResetModelWeights",
"LunaSecondaryModelLoader",
"LunaSmartLoRALinker",
"LunaSuperUpscaler",
@ -2617,7 +2697,6 @@
"LunaZImageEncoder",
"LunaZImageProcessor",
"Luna_Advanced_Upscaler",
"Luna_Detailer",
"Luna_SimpleUpscaler",
"Luna_UltimateSDUpscale"
],
@ -3120,6 +3199,7 @@
"DJZColorWheel",
"DJZDatamosh",
"DJZDatamoshV2",
"DJZImageScaleToMegabytes",
"DJZImageScaleToTotalPixels",
"DJZ_ParallaxV1",
"DatasetWordcloud",
@ -3270,7 +3350,6 @@
"DeepSeek_Node",
"Delay_node",
"Delete_folder_Node",
"DongShowTextNode",
"Dong_Pixelate_Node",
"Dong_Text_Node",
"DownloadNode",
@ -3279,12 +3358,14 @@
"FolderIteratorNODE",
"GLM_Node",
"GetImageListFromFloderNode",
"GetImageListFromFloderNode2",
"GetRefModelImageListNode",
"Get_cookies_Node",
"Get_json_value_Node",
"Get_video_Node",
"HashCalculationsNode",
"HuggingFaceUploadNode",
"IFEXISTTEXTNODE",
"IMG2URLNode",
"INTNODE",
"Image2GIFNode",
@ -3293,8 +3374,10 @@
"LibLib_upload_Node",
"LogicToolsNode",
"LoraIterator",
"Notice_Node",
"PromptConcatNode",
"Qwen3VL_235_Node",
"Qwen3VL_30_Node",
"QwenVL_Node",
"RandomNumbersNode",
"RenameNode",
"ResolutionNode",
@ -3306,11 +3389,11 @@
"Wan21_post_Node",
"ZIPwith7zNode",
"bailian_model_select_Node",
"checkvram_node",
"cogvideox_flash_get_Node",
"cogvideox_flash_post_Node",
"cogview_3_flash_Node",
"doubaoNode",
"douyin_remove_watermark_Node",
"file_analysis_Node",
"file_extract_Node",
"find_files_by_extension_Node",
@ -3319,11 +3402,16 @@
"image_iterator",
"img2url_v2_Node",
"img_understanding_Node",
"kie_base64_upload_node",
"kie_nano_get_node",
"kie_nano_post_node",
"klingai_video_Node",
"path_join_Node",
"save_img_NODE",
"save_img_v2_NODE",
"set_api_Node",
"suchuang_get_node",
"suchuang_nano_post_node",
"text_replace_node"
],
{
@ -3471,6 +3559,14 @@
"title_aux": "ComfyUI-LaplaMask"
}
],
"https://github.com/OhSeongHyeon/comfyui-random-image-size": [
[
"RandomImageSize"
],
{
"title_aux": "comfyui-random-image-size"
}
],
"https://github.com/Omario92/ComfyUI-OmarioNodes": [
[
"DualEndpointColorBlendScheduler"
@ -3596,21 +3692,27 @@
"CADAnalysisViewer",
"CADBoundingBox",
"CADCheckOverlappingFaces",
"CADCurvature",
"CADCurvePlotter",
"CADEdgeAnalysis",
"CADEdgeDetailAnalyzer",
"CADEdgeViewer",
"CADEdgeViewerVTK",
"CADExtractFaces",
"CADFaceAnalysis",
"CADFixDegenerateFaces",
"CADGetFilename",
"CADHealShape",
"CADHierarchyTree",
"CADMergeVertices",
"CADPrimitiveReconstruction",
"CADProjectFacesXY",
"CADROISelector",
"CADRaytracerBVH",
"CADRecodeInference",
"CADSave",
"CADSewFaces",
"CADSplineViewer",
"CADSplitComponents",
"CADTransform",
"CAD_Convert_Format",
@ -3649,6 +3751,7 @@
"Point2CADSegmentation",
"Point2CADSurfaceFitting",
"Point2CADSurfaceFittingOCC",
"Point2CADToWireframeInfo",
"Point2CADTopologyExtraction",
"PointCloudSegmentation",
"PreviewCADBatch",
@ -3980,6 +4083,17 @@
"title_aux": "Retro Engine Node for ComfyUI"
}
],
"https://github.com/SSYCloud/comfyui-ssy-syncapi": [
[
"SSYBytedanceProcessor",
"SSYDoubaoGenerator",
"SSYGoogleGenerator",
"SSYOpenAIGenerator"
],
{
"title_aux": "comfyui-ssy-syncapi [WIP]"
}
],
"https://github.com/SXQBW/ComfyUI-Qwen3": [
[
"QwenVisionParser"
@ -4393,6 +4507,17 @@
"title_aux": "ComfyUI_Remaker_FaceSwap"
}
],
"https://github.com/Spicely/ComfyUI-Luma": [
[
"AddVideoTextWatermark",
"GetDeviceType",
"SeparateVideoAudio",
"Wav2Srt"
],
{
"title_aux": "[WIP] ComfyUI-Luma"
}
],
"https://github.com/Stable-X/ComfyUI-Hi3DGen": [
[
"DifferenceExtractorNode",
@ -4625,6 +4750,14 @@
"title_aux": "comfyui_flowrider_nodes [UNSAFE]"
}
],
"https://github.com/Suzu008/ComfyUI-ImageCritic": [
[
"DetailEncoder"
],
{
"title_aux": "ComfyUI-ImageCritic"
}
],
"https://github.com/Symbiomatrix/Comfyui-Sort-Files": [
[
"ImageSaverSBM",
@ -5713,14 +5846,6 @@
"title_aux": "ComfyUI_MoreComfy"
}
],
"https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group": [
[
"SwitchBypassMute"
],
{
"title_aux": "comfyui-switch-bypass-mute-by-group"
}
],
"https://github.com/avocadori/ComfyUI-AudioAmplitudeConverter": [
[
"NormalizeAmpToFloatNode"
@ -5804,6 +5929,15 @@
"title_aux": "BigModelPipe [WIP]"
}
],
"https://github.com/baoanhng/ComfyUI-utils": [
[
"TextJoiner",
"TextSplitter"
],
{
"title_aux": "ComfyUI-utils"
}
],
"https://github.com/barakapa/barakapa-nodes": [
[
"brkp_ConcatenateString",
@ -6371,6 +6505,7 @@
"https://github.com/chetusangolgi/Comfyui-supabase": [
[
"SupabaseAudioUploader",
"SupabaseGLBUploader",
"SupabaseImageUploader",
"SupabaseTableWatcherNode"
],
@ -6752,13 +6887,6 @@
"PerturbedAttentionGuidance",
"PhotoMakerEncode",
"PhotoMakerLoader",
"PikaImageToVideoNode2_2",
"PikaScenesV2_2",
"PikaStartEndFrameNode2_2",
"PikaTextToVideoNode2_2",
"Pikadditions",
"Pikaffects",
"Pikaswaps",
"PixverseImageToVideoNode",
"PixverseTemplateNode",
"PixverseTextToVideoNode",
@ -6802,6 +6930,7 @@
"ReplaceVideoLatentFrames",
"RescaleCFG",
"ResizeAndPadImage",
"ResolutionBucket",
"Rodin3D_Detail",
"Rodin3D_Gen2",
"Rodin3D_Regular",
@ -6829,6 +6958,7 @@
"SamplerLCMUpscale",
"SamplerLMS",
"SamplerSASolver",
"SamplerSEEDS2",
"SamplingPercentToSigma",
"SaveAnimatedPNG",
"SaveAnimatedWEBP",
@ -7010,6 +7140,7 @@
"WanTrackToVideo",
"WanVaceToVideo",
"WebcamCapture",
"ZImageFunControlnet",
"unCLIPCheckpointLoader",
"unCLIPConditioning",
"wanBlockSwap"
@ -7272,6 +7403,15 @@
"title_aux": "ComfyUI_WcpD_Utility_Kit"
}
],
"https://github.com/dougbtv/comfyui-vllm-omni": [
[
"VLLMImageEdit",
"VLLMTextToImage"
],
{
"title_aux": "comfyui-vllm-omni"
}
],
"https://github.com/dowands/ComfyUI-AddMaskForICLora": [
[
"AddMaskForICLora"
@ -7457,6 +7597,19 @@
"title_aux": "ComfyUI-Sysinfo"
}
],
"https://github.com/edvardtoth/ComfyUI-ETNodes": [
[
"ETNodes-Color-Selector",
"ETNodes-Gemini-API-Image",
"ETNodes-Gemini-API-Text",
"ETNodes-List-Items",
"ETNodes-List-Selector",
"ETNodes-Text-Preview"
],
{
"title_aux": "ComfyUI-ETNodes"
}
],
"https://github.com/eggsbenedicto/DiffusionRenderer-ComfyUI": [
[
"Cosmos1ForwardRenderer",
@ -7769,29 +7922,6 @@
"title_aux": "ComfyUI-LLM-Utils [WIP]"
}
],
"https://github.com/frost-byte/fbTools": [
[
"FBTextEncodeQwenImageEditPlus",
"NodeInputSelect",
"OpaqueAlpha",
"QwenAspectRatio",
"SAMPreprocessNHWC",
"SceneCreate",
"SceneInput",
"SceneOutput",
"SceneSave",
"SceneSelect",
"SceneUpdate",
"SceneView",
"SceneWanVideoLoraMultiSave",
"SubdirLister",
"TailEnhancePro",
"TailSplit"
],
{
"title_aux": "fb-tools"
}
],
"https://github.com/ftechmax/ComfyUI-NovaKit-Pack": [
[
"CountTokens"
@ -7876,6 +8006,16 @@
"title_aux": "ComfyUI_gaga_utils"
}
],
"https://github.com/gajjar4/ComfyUI-Qwen-Image-i2L": [
[
"QwenI2L_Apply",
"QwenI2L_PipelineLoader",
"QwenI2L_Save"
],
{
"title_aux": "ComfyUI-Qwen-Image-i2L [UNSAFE]"
}
],
"https://github.com/galoreware/ComfyUI-GaloreNodes": [
[
"GNI_HEX_TO_COLOR",
@ -8109,9 +8249,12 @@
"https://github.com/grokuku/ComfyUI-Holaf": [
[
"HolafBypasser",
"HolafGroupBypasser",
"HolafImageBatchSlice",
"HolafImageComparer",
"HolafInstagramResize",
"HolafKSampler",
"HolafLoadImageVideo",
"HolafLutGenerator",
"HolafLutSaver",
"HolafMaskToBoolean",
@ -8120,7 +8263,11 @@
"HolafRemote",
"HolafResolutionPreset",
"HolafSaveImage",
"HolafSaveVideo",
"HolafShortcut",
"HolafShortcutUser",
"HolafTiledKSampler",
"HolafVideoPreview",
"UpscaleImageHolaf"
],
{
@ -8836,6 +8983,17 @@
"title_aux": "ComfyUI-AliCloud-Bailian [WIP]"
}
],
"https://github.com/jinchanz/ComfyUI-Midjourney": [
[
"MidjourneyAPI",
"MidjourneyAPIPoll",
"MidjourneyAPISubmit",
"MidjourneyJSONExtractor"
],
{
"title_aux": "ComfyUI-Midjourney"
}
],
"https://github.com/jn-jairo/jn_node_suite_comfyui": [
[
"JN_AreaInfo",
@ -9591,15 +9749,14 @@
"title_aux": "ComfyUI_Scoring-Nodes"
}
],
"https://github.com/lfelipegg/lfgg_custom_nodes": [
"https://github.com/lfelipegg/lfgg_custom_nodes_comfyui": [
[
"LFGG - Resolution Tools",
"LFGG KSampler (advanced) - Config",
"LFGG KSampler - Config",
"ModelMergeCombos"
"LfggImageResolutionByRatio",
"LfggLatentSizeByRatio",
"LfggPixelBudgetLatentSize"
],
{
"title_aux": "lfgg_custom_nodes [WIP]"
"title_aux": "[WIP] lfgg_custom_nodes_comfyui"
}
],
"https://github.com/lggcfx2020/ComfyUI-LGGCFX-Tools": [
@ -9640,7 +9797,6 @@
"MaskToSAMCoordsV2",
"MorseCode",
"PoseReformer",
"SaveITAsZip",
"SaveImageAsZip",
"SaveTextAsZip",
"StrFormat",
@ -9716,6 +9872,28 @@
"title_aux": "ComfyUI-Alternatives"
}
],
"https://github.com/logicalor/comfyui_mv_adapter": [
[
"MVAdapterBackgroundRemoval",
"MVAdapterCameraEmbed",
"MVAdapterClearVRAM",
"MVAdapterI2MVSampler",
"MVAdapterImageGrid",
"MVAdapterImagePreprocess",
"MVAdapterLoRALoader",
"MVAdapterModelSetup",
"MVAdapterPipelineLoader",
"MVAdapterReferencePreprocess",
"MVAdapterSchedulerConfig",
"MVAdapterSplitViews",
"MVAdapterT2MVSampler",
"MVAdapterVAEDecode",
"MVAdapterViewSelector"
],
{
"title_aux": "comfyui_mv_adapter [WIP]"
}
],
"https://github.com/logtd/ComfyUI-HunyuanLoom": [
[
"ConfigureModifiedHY",
@ -9809,15 +9987,28 @@
[
"DiffusersImageEditGenerator",
"DiffusersImageGenerator",
"DiffusersLoadLoraOnly",
"DiffusersLoraLayersOperation",
"DiffusersLoraLoader",
"DiffusersLoraStatViewer",
"DiffusersLoraUnloader",
"DiffusersMergeLoraToPipeline",
"DiffusersModelLoader",
"DiffusersPipeline",
"DiffusersPipelineBuilder",
"DiffusersPreprocessorLoader",
"DiffusersSampling",
"DiffusersSaveLora",
"DiffusersTextEncode",
"DiffusersTextEncoderLoader",
"DiffusersTokenizerLoader",
"DiffusersTransformerLoader",
"DiffusersVAELoader",
"LoadLoraOnly",
"LoraLayersOperation",
"LoraStatViewer",
"MergeLoraToTransformer",
"SaveLora",
"TextEncodeDiffusersLongCat",
"TextEncodeDiffusersLongCatCached",
"TextEncodeDiffusersLongCatImageEdit"
@ -10502,15 +10693,6 @@
"title_aux": "ComfyUI-WanPlus"
}
],
"https://github.com/neverbiasu/ComfyUI-ControlNeXt": [
[
"ControlNextPipelineConfig",
"ControlNextSDXL"
],
{
"title_aux": "ComfyUI-ControlNeXt [WIP]"
}
],
"https://github.com/neverbiasu/ComfyUI-DeepSeek": [
[
"DeepSeekCaller"
@ -10613,7 +10795,9 @@
"ExtractPromptFromImage",
"FloatToString",
"FloatToStringWithPrefix",
"LoraWildcardGenerator"
"IsComfyQueueEmpty",
"LoraWildcardGenerator",
"RepeatTextLines"
],
{
"title_aux": "comfyui-samenodes"
@ -10929,15 +11113,6 @@
"title_aux": "ComfyUI-PixuAI"
}
],
"https://github.com/pizurny/ComfyUI-Just-DWPose": [
[
"DWPoseAnnotator",
"DWPoseJSONToImage"
],
{
"title_aux": "ComfyUI-Just-DWPose [WIP]"
}
],
"https://github.com/pizurny/Comfyui-FeedbackSampler": [
[
"FeedbackSampler"
@ -11142,6 +11317,15 @@
"title_aux": "comfyui_api_executor_nodes"
}
],
"https://github.com/rafstahelin/ComfyUI_KieNanoBananaPro": [
[
"KieNanoBananaPro",
"KieNanoBananaProBatch"
],
{
"title_aux": "ComfyUI_KieNanoBananaPro"
}
],
"https://github.com/rakete/comfyui-rakete": [
[
"rakete.BuildString",
@ -11487,6 +11671,57 @@
"title_aux": "ComfyUI Sahib Nodes"
}
],
"https://github.com/saltchicken/ComfyUI-Identity-Mixer": [
[
"IdentityLoraMixer",
"IdentityLoraMixerStack"
],
{
"title_aux": "ComfyUI-Identity-Mixer"
}
],
"https://github.com/saltchicken/ComfyUI-Local-Loader": [
[
"LoadImageFromDir",
"LoadImageFromPath"
],
{
"title_aux": "ComfyUI-Local-Loader"
}
],
"https://github.com/saltchicken/ComfyUI-Prompter": [
[
"CustomizablePromptGenerator"
],
{
"title_aux": "ComfyUI-Prompter"
}
],
"https://github.com/saltchicken/ComfyUI-Selector": [
[
"SimpleSelectorNode"
],
{
"title_aux": "ComfyUI-Selector"
}
],
"https://github.com/saltchicken/ComfyUI-Video-Utils": [
[
"FinalFrameSelector",
"VideoMerge"
],
{
"title_aux": "ComfyUI-Video-Utils"
}
],
"https://github.com/satyasairazole/ComfyUI-Turbandetection": [
[
"TurbanDetectorNode"
],
{
"title_aux": "ComfyUI-Turbandetection [WIP]"
}
],
"https://github.com/saulchiu/comfy_saul_plugin": [
[
"Blend Images",
@ -11585,18 +11820,6 @@
"title_aux": "comfyui-hydit"
}
],
"https://github.com/shinich39/comfyui-nothing-happened": [
[
"NothingHappened"
],
{
"author": "shinich39",
"description": "Save image and keep metadata.",
"nickname": "comfyui-nothing-happened",
"title": "comfyui-nothing-happened",
"title_aux": "comfyui-nothing-happened"
}
],
"https://github.com/shinich39/comfyui-run-js": [
[
"RunJS"
@ -11721,10 +11944,13 @@
],
"https://github.com/silveroxides/ComfyUI_SamplingUtils": [
[
"Frakturpad",
"GetJsonKeyValue",
"ImageBlendByMask",
"Image_Color_Noise",
"ModifyMask",
"SamplingParameters",
"SystemMessagePresets",
"TextEncodeFlux2SystemPrompt",
"TextEncodeZITSystemPrompt"
],
@ -11922,6 +12148,39 @@
"title_aux": "comfyui-lingshang"
}
],
"https://github.com/stalkervr/ComfyUI-StalkerVr": [
[
"AnyCollector",
"ImageAspectRatioFixer",
"ImageBatchCrop",
"ImageGridCropper",
"JsonArraySplitter",
"JsonBuilder",
"JsonDeserializeObject",
"JsonFieldRemover",
"JsonFieldReplaceAdvanced",
"JsonFieldValueExtractor",
"JsonPairInput",
"JsonPathLoader",
"JsonPromptToTextPromptConverter",
"JsonRootListExtractor",
"JsonSerializeObject",
"JsonToString",
"ListItemExtractor",
"LogValue",
"LoopAny",
"PromptPartJoin",
"StringBuilder",
"StringCollector",
"StringConcatenation",
"StringListToString",
"StringWrapper",
"WanVideoMultiPrompt"
],
{
"title_aux": "ComfyUI-StalkerVr"
}
],
"https://github.com/stalkervr/comfyui-custom-path-nodes": [
[
"AnyCollector",
@ -11964,6 +12223,16 @@
"title_aux": "ComfyUI-Audio-Subtitle [WIP]"
}
],
"https://github.com/starsFriday/ComfyUI-LongCat-Image": [
[
"LongCatImageEdit",
"LongCatImageModelLoader",
"LongCatImageTextToImage"
],
{
"title_aux": "ComfyUI-LongCat-Image [WIP]"
}
],
"https://github.com/starsFriday/ComfyUI-Tracker-Person": [
[
"YoloTrackNode"
@ -12501,6 +12770,23 @@
"title_aux": "ComfyUI-SaveImg-W-MetaData"
}
],
"https://github.com/u5dev/ComfyUI_u5_EasyScripter": [
[
"comfyUI_u5_easyscripter",
"u5_CLIPLoader",
"u5_CLIPVisionLoader",
"u5_CheckpointLoader",
"u5_ControlNetLoader",
"u5_GLIGENLoader",
"u5_LoraLoader",
"u5_StyleModelLoader",
"u5_UNETLoader",
"u5_VAELoader"
],
{
"title_aux": "ComfyUI_u5_EasyScripter [UNSAFE]"
}
],
"https://github.com/umisetokikaze/comfyui_mergekit": [
[
"DefineSaveName",
@ -13019,6 +13305,16 @@
"title_aux": "ComfyUI-DreamOmni2-GGUF [WIP]"
}
],
"https://github.com/xuchenxu168/comfyui_meituan_image": [
[
"MeituanLongCatEdit",
"MeituanLongCatLoader",
"MeituanLongCatT2I"
],
{
"title_aux": "[WIP] comfyui_meituan_image"
}
],
"https://github.com/xzuyn/ComfyUI-xzuynodes": [
[
"CLIPLoaderXZ",
@ -13407,6 +13703,7 @@
"https://github.com/zyd232/ComfyUI-zyd232-Nodes": [
[
"zyd232 ImagesPixelsCompare",
"zyd232 MaskBatchBlend",
"zyd232_SavePreviewImages"
],
{

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,106 @@
{
"custom_nodes": [
{
"author": "jkhayiying",
"title": "ImageLoadFromLocalOrUrl Node for ComfyUI [REMOVED]",
"id": "JkhaImageLoaderPathOrUrl",
"reference": "https://gitee.com/yyh915/jkha-load-img",
"files": [
"https://gitee.com/yyh915/jkha-load-img"
],
"install_type": "git-clone",
"description": "This is a node to load an image from local path or url."
},
{
"author": "pizurny",
"title": "ComfyUI-Just-DWPose [REMOVED]",
"reference": "https://github.com/pizurny/ComfyUI-Just-DWPose",
"files": [
"https://github.com/pizurny/ComfyUI-Just-DWPose"
],
"install_type": "git-clone",
"description": "An advanced DWPose annotator for ComfyUI with TorchScript and ONNX backends, featuring comprehensive pose detection, bone validation, temporal smoothing, and custom visualization tools."
},
{
"author": "lfelipegg",
"title": "lfgg_custom_nodes [REMOVED]",
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes",
"files": [
"https://github.com/lfelipegg/lfgg_custom_nodes"
],
"install_type": "git-clone",
"description": "NODES: ModelMergeCombos\nNOTE: The files in the repo are not organized."
},
{
"author": "AndSni",
"title": "Comfy-FL-Nodes [REMOVED]",
"reference": "https://github.com/AndSni/Comfy-FL-Nodes",
"files": [
"https://github.com/AndSni/Comfy-FL-Nodes"
],
"install_type": "git-clone",
"description": "Generates human characters for commerce applications in ComfyUI. (Description by CC)"
},
{
"author": "neeltheninja",
"title": "ComfyUI-ControlNeXt [REMOVED]",
"reference": "https://github.com/neverbiasu/ComfyUI-ControlNeXt",
"files": [
"https://github.com/neverbiasu/ComfyUI-ControlNeXt"
],
"install_type": "git-clone",
"description": "In progress🚧"
},
{
"author": "TheArtOfficial",
"title": "ComfyUI-Nitra [REMOVED]",
"reference": "https://github.com/TheArtOfficial/ComfyUI-Nitra",
"files": [
"https://github.com/TheArtOfficial/ComfyUI-Nitra"
],
"install_type": "git-clone",
"description": "Nitra custom node for ComfyUI"
},
{
"author": "AngelCookies",
"title": "ComfyUI-Seed-Tracker [REMOVED]",
"reference": "https://github.com/AngelCookies/ComfyUI-Seed-Tracker",
"files": [
"https://github.com/AngelCookies/ComfyUI-Seed-Tracker"
],
"install_type": "git-clone",
"description": "A ComfyUI extension that tracks random seeds throughout your image generation workflows"
},
{
"author": "shinich39",
"title": "comfyui-nothing-happened [REMOVED]",
"reference": "httphttps://github.com/shinich39/comfyui-nothing-happened",
"files": [
"https://github.com/shinich39/comfyui-nothing-happened"
],
"description": "Save image and keep metadata.",
"install_type": "git-clone"
},
{
"author": "ashtar1984",
"title": "comfyui-switch-bypass-mute-by-group [REMOVED]",
"reference": "https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group",
"files": [
"https://github.com/ashtar1984/comfyui-switch-bypass-mute-by-group"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for group-based node switching, bypassing, and muting control. (Description by CC)"
},
{
"author": "wallen0322",
"title": "ComfyUI-TTM-WAN22 [REMOVED]",
"reference": "https://github.com/wallen0322/ComfyUI-TTM-WAN22",
"files": [
"https://github.com/wallen0322/ComfyUI-TTM-WAN22"
],
"install_type": "git-clone",
"description": "TTM (Time-to-Move) node for ComfyUI enabling motion-controlled video generation with Wan2.2 models using dual-clock denoising for independent background and object animation control."
},
{
"author": "cdanielp",
"title": "COMFYUI_PROMPTMODELS [REMOVED]",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[project]
name = "comfyui-manager"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
version = "3.38.3"
version = "3.39"
license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]

19
tests-api/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Python cache files
__pycache__/
*.py[cod]
*$py.class
# Pytest cache
.pytest_cache/
# Coverage reports
.coverage
htmlcov/
# Virtual environments
venv/
env/
ENV/
# Test-specific resources
resources/tmp/

91
tests-api/README.md Normal file
View File

@ -0,0 +1,91 @@
# ComfyUI-Manager API Tests
This directory contains tests for the ComfyUI-Manager API endpoints, validating the OpenAPI specification and ensuring API functionality.
## Setup
1. Install test dependencies:
```bash
pip install -r requirements-test.txt
```
2. Ensure ComfyUI is running with ComfyUI-Manager installed:
```bash
# Start ComfyUI with the default server
python main.py
```
## Running Tests
### Run all tests
```bash
pytest -xvs
```
### Run specific test files
```bash
# Run only the spec validation tests
pytest -xvs test_spec_validation.py
# Run only the custom node API tests
pytest -xvs test_customnode_api.py
```
### Run specific test functions
```bash
# Run a specific test
pytest -xvs test_customnode_api.py::test_get_custom_node_list
```
## Test Configuration
The tests use the following default configuration:
- Server URL: `http://localhost:8188`
- Server timeout: 2 seconds
- Wait between requests: 0.5 seconds
- Maximum retries: 3
You can override these settings with environment variables:
```bash
# Use a different server URL
COMFYUI_SERVER_URL=http://localhost:8189 pytest -xvs
```
## Test Categories
The tests are organized into the following categories:
1. **Spec Validation** (`test_spec_validation.py`): Validates that the OpenAPI specification is correct and complete.
2. **Custom Node API** (`test_customnode_api.py`): Tests for custom node management endpoints.
3. **Snapshot API** (`test_snapshot_api.py`): Tests for snapshot management endpoints.
4. **Queue API** (`test_queue_api.py`): Tests for queue management endpoints.
5. **Config API** (`test_config_api.py`): Tests for configuration endpoints.
6. **Model API** (`test_model_api.py`): Tests for model management endpoints (minimal as these are being deprecated).
## Test Implementation Details
### Fixtures
- `test_config`: Provides the test configuration
- `server_url`: Returns the server URL from the configuration
- `openapi_spec`: Loads the OpenAPI specification
- `api_client`: Creates a requests Session for API calls
- `api_request`: Helper function for making consistent API requests
### Utilities
- `validation.py`: Functions for validating responses against the OpenAPI schema
- `schema_utils.py`: Utilities for extracting and manipulating schemas
## Notes
- Some tests are skipped with `@pytest.mark.skip` to avoid modifying state in automated testing
- Security-level restricted endpoints have minimal tests to avoid security issues
- Tests focus on read operations rather than write operations where possible

1
tests-api/__init__.py Normal file
View File

@ -0,0 +1 @@
# Make tests-api directory a proper package

237
tests-api/conftest.py Normal file
View File

@ -0,0 +1,237 @@
"""
PyTest configuration and fixtures for API tests.
"""
import os
import sys
import json
import pytest
import requests
import tempfile
import time
import yaml
from pathlib import Path
from typing import Dict, Generator, Optional, Tuple
# Import test utilities
import sys
import os
from pathlib import Path
# Get the absolute path to the current file (conftest.py)
current_file = Path(os.path.abspath(__file__))
# Get the directory containing the current file (the tests-api directory)
tests_api_dir = current_file.parent
# Add the tests-api directory to the Python path
if str(tests_api_dir) not in sys.path:
sys.path.insert(0, str(tests_api_dir))
# Apply mocks for ComfyUI imports
from mocks.patch import apply_mocks
apply_mocks()
# Now we can import from utils.validation
from utils.validation import load_openapi_spec
# Default test configuration
DEFAULT_TEST_CONFIG = {
"server_url": "http://localhost:8188",
"server_timeout": 2, # seconds
"wait_between_requests": 0.5, # seconds
"max_retries": 3,
}
@pytest.fixture(scope="session")
def test_config() -> Dict:
"""
Load test configuration from environment variables or use defaults.
"""
config = DEFAULT_TEST_CONFIG.copy()
# Override from environment variables if present
if "COMFYUI_SERVER_URL" in os.environ:
config["server_url"] = os.environ["COMFYUI_SERVER_URL"]
return config
@pytest.fixture(scope="session")
def server_url(test_config: Dict) -> str:
"""
Get the server URL from the test configuration.
"""
return test_config["server_url"]
@pytest.fixture(scope="session")
def openapi_spec() -> Dict:
"""
Load the OpenAPI specification.
"""
return load_openapi_spec()
@pytest.fixture(scope="session")
def api_client(server_url: str, test_config: Dict) -> requests.Session:
"""
Create a requests Session for API calls.
"""
session = requests.Session()
# Check if the server is running
try:
response = session.get(f"{server_url}/", timeout=test_config["server_timeout"])
response.raise_for_status()
except (requests.ConnectionError, requests.Timeout, requests.HTTPError):
pytest.skip("ComfyUI server is not running or not accessible")
return session
@pytest.fixture(scope="function")
def temp_dir() -> Generator[Path, None, None]:
"""
Create a temporary directory for test files.
"""
with tempfile.TemporaryDirectory() as temp_dir:
yield Path(temp_dir)
class SecurityLevelContext:
"""
Context manager for setting and restoring security levels.
"""
def __init__(self, api_client: requests.Session, server_url: str, security_level: str):
self.api_client = api_client
self.server_url = server_url
self.security_level = security_level
self.original_level = None
async def __aenter__(self):
# Get the current security level (not directly exposed in API, would require more setup)
# For now, we'll just set the new level
# Set the new security level
# Note: In a real implementation, we would need a way to set this
# This is a placeholder - the actual implementation would depend on how
# security levels are managed in ComfyUI-Manager
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
# Restore the original security level if needed
pass
@pytest.fixture
def security_level_context(api_client: requests.Session, server_url: str):
"""
Create a context manager for setting security levels.
"""
return lambda level: SecurityLevelContext(api_client, server_url, level)
def make_api_url(server_url: str, path: str) -> str:
"""
Construct a full API URL from the server URL and path.
"""
# Ensure the path starts with a slash
if not path.startswith("/"):
path = f"/{path}"
# Remove trailing slash from server_url if present
if server_url.endswith("/"):
server_url = server_url[:-1]
return f"{server_url}{path}"
@pytest.fixture
def api_request(api_client: requests.Session, server_url: str, test_config: Dict):
"""
Helper function for making API requests with consistent behavior.
"""
def _request(
method: str,
path: str,
params: Optional[Dict] = None,
json_data: Optional[Dict] = None,
headers: Optional[Dict] = None,
expected_status: int = 200,
retry_on_error: bool = True,
) -> Tuple[requests.Response, Optional[Dict]]:
"""
Make an API request with automatic validation.
Args:
method: HTTP method
path: API path
params: Query parameters
json_data: JSON request body
headers: HTTP headers
expected_status: Expected HTTP status code
retry_on_error: Whether to retry on connection errors
Returns:
Tuple of (Response object, JSON response data or None)
"""
method = method.lower()
url = make_api_url(server_url, path)
if headers is None:
headers = {}
# Add common headers
headers.setdefault("Accept", "application/json")
# Sleep between requests to avoid overwhelming the server
time.sleep(test_config["wait_between_requests"])
retries = test_config["max_retries"] if retry_on_error else 0
last_exception = None
for attempt in range(retries + 1):
try:
if method == "get":
response = api_client.get(url, params=params, headers=headers)
elif method == "post":
response = api_client.post(url, params=params, json=json_data, headers=headers)
elif method == "put":
response = api_client.put(url, params=params, json=json_data, headers=headers)
elif method == "delete":
response = api_client.delete(url, params=params, headers=headers)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
# Check status code
assert response.status_code == expected_status, (
f"Expected status code {expected_status}, got {response.status_code}"
)
# Parse JSON response if possible
json_response = None
if response.headers.get("Content-Type", "").startswith("application/json"):
try:
json_response = response.json()
except json.JSONDecodeError:
if expected_status == 200:
raise ValueError("Response was not valid JSON")
return response, json_response
except (requests.ConnectionError, requests.Timeout) as e:
last_exception = e
if attempt < retries:
# Wait before retrying
time.sleep(1)
continue
break
if last_exception:
raise last_exception
raise RuntimeError("Failed to make API request")
return _request

View File

@ -0,0 +1 @@
# Make tests-api/mocks directory a proper package

View File

@ -0,0 +1,26 @@
"""
Mock CustomNodeManager for testing purposes
"""
class CustomNodeManager:
"""
Mock implementation of the CustomNodeManager class
"""
instance = None
def __init__(self):
self.custom_nodes = {}
self.node_paths = []
self.refresh_timeout = None
def get_node_path(self, node_class):
"""
Mock implementation to get the path for a node class
"""
return self.custom_nodes.get(node_class, None)
def update_node_paths(self):
"""
Mock implementation to update node paths
"""
pass

116
tests-api/mocks/patch.py Normal file
View File

@ -0,0 +1,116 @@
"""
Patch module to mock imports for testing
"""
import sys
import importlib.util
import os
from pathlib import Path
# Import mock modules
from mocks.prompt_server import PromptServer
from mocks.custom_node_manager import CustomNodeManager
# Current directory
current_dir = Path(__file__).parent.parent # tests-api directory
# Define mocks
class MockModule:
"""Base class for mock modules"""
pass
# Create server mock module with PromptServer
server_mock = MockModule()
server_mock.PromptServer = PromptServer
prompt_server_instance = PromptServer()
server_mock.PromptServer.instance = prompt_server_instance
server_mock.PromptServer.inst = prompt_server_instance
# Create app mock module with custom_node_manager submodule
app_mock = MockModule()
app_custom_node_manager = MockModule()
app_custom_node_manager.CustomNodeManager = CustomNodeManager
app_custom_node_manager.CustomNodeManager.instance = CustomNodeManager()
# Create utils mock module with json_util submodule
utils_mock = MockModule()
utils_json_util = MockModule()
# Create utils.validation and utils.schema_utils submodules
utils_validation = MockModule()
utils_schema_utils = MockModule()
# Import actual modules (make sure path is set up correctly)
sys.path.insert(0, str(current_dir))
try:
# Import the validation module
from utils.validation import load_openapi_spec
utils_validation.load_openapi_spec = load_openapi_spec
# Import all schema_utils functions
from utils.schema_utils import (
get_all_paths,
get_grouped_paths,
get_methods_for_path,
find_paths_with_security,
get_content_types_for_response,
get_required_parameters
)
utils_schema_utils.get_all_paths = get_all_paths
utils_schema_utils.get_grouped_paths = get_grouped_paths
utils_schema_utils.get_methods_for_path = get_methods_for_path
utils_schema_utils.find_paths_with_security = find_paths_with_security
utils_schema_utils.get_content_types_for_response = get_content_types_for_response
utils_schema_utils.get_required_parameters = get_required_parameters
except ImportError as e:
print(f"Error importing test utilities: {e}")
# Define dummy functions if imports fail
def dummy_load_openapi_spec():
"""Dummy function for testing"""
return {"paths": {}}
utils_validation.load_openapi_spec = dummy_load_openapi_spec
def dummy_get_all_paths(spec):
return list(spec.get("paths", {}).keys())
utils_schema_utils.get_all_paths = dummy_get_all_paths
def dummy_get_grouped_paths(spec):
return {}
utils_schema_utils.get_grouped_paths = dummy_get_grouped_paths
def dummy_get_methods_for_path(spec, path):
return []
utils_schema_utils.get_methods_for_path = dummy_get_methods_for_path
def dummy_find_paths_with_security(spec, security_scheme=None):
return []
utils_schema_utils.find_paths_with_security = dummy_find_paths_with_security
def dummy_get_content_types_for_response(spec, path, method, status_code="200"):
return []
utils_schema_utils.get_content_types_for_response = dummy_get_content_types_for_response
def dummy_get_required_parameters(spec, path, method):
return []
utils_schema_utils.get_required_parameters = dummy_get_required_parameters
# Add merge_json_recursive from our mock utils
from mocks.utils import merge_json_recursive
utils_json_util.merge_json_recursive = merge_json_recursive
# Apply the mocks to sys.modules
def apply_mocks():
"""Apply all mocks to sys.modules"""
sys.modules['server'] = server_mock
sys.modules['app'] = app_mock
sys.modules['app.custom_node_manager'] = app_custom_node_manager
sys.modules['utils'] = utils_mock
sys.modules['utils.json_util'] = utils_json_util
sys.modules['utils.validation'] = utils_validation
sys.modules['utils.schema_utils'] = utils_schema_utils
# Make sure our actual utils module is importable
if current_dir not in sys.path:
sys.path.insert(0, str(current_dir))

View File

@ -0,0 +1,71 @@
"""
Mock PromptServer for testing purposes
"""
class MockRoutes:
"""
Mock routing class with method decorators
"""
def __init__(self):
self.routes = {}
def get(self, path):
"""Decorator for GET routes"""
def decorator(f):
self.routes[('GET', path)] = f
return f
return decorator
def post(self, path):
"""Decorator for POST routes"""
def decorator(f):
self.routes[('POST', path)] = f
return f
return decorator
def put(self, path):
"""Decorator for PUT routes"""
def decorator(f):
self.routes[('PUT', path)] = f
return f
return decorator
def delete(self, path):
"""Decorator for DELETE routes"""
def decorator(f):
self.routes[('DELETE', path)] = f
return f
return decorator
class PromptServer:
"""
Mock implementation of the PromptServer class
"""
instance = None
inst = None
def __init__(self):
self.routes = MockRoutes()
self.registered_paths = set()
self.base_url = "http://127.0.0.1:8188" # Assuming server is running on default port
self.queue_lock = None
def add_route(self, method, path, handler, *args, **kwargs):
"""
Add a mock route to the server
"""
self.routes.routes[(method.upper(), path)] = handler
self.registered_paths.add(path)
async def send_msg(self, message, data=None):
"""
Mock send_msg method (does nothing in the mock)
"""
pass
def send_sync(self, message, data=None):
"""
Mock send_sync method (does nothing in the mock)
"""
pass

20
tests-api/mocks/utils.py Normal file
View File

@ -0,0 +1,20 @@
"""
Mock utils module for testing purposes
"""
def merge_json_recursive(a, b):
"""
Mock implementation of merge_json_recursive
"""
if isinstance(a, dict) and isinstance(b, dict):
result = a.copy()
for key, value in b.items():
if key in result and isinstance(result[key], (dict, list)) and isinstance(value, (dict, list)):
result[key] = merge_json_recursive(result[key], value)
else:
result[key] = value
return result
elif isinstance(a, list) and isinstance(b, list):
return a + b
else:
return b

382
tests-api/openapi.yaml Normal file
View File

@ -0,0 +1,382 @@
openapi: 3.0.3
info:
title: ComfyUI-Manager API
description: API for managing ComfyUI extensions, custom nodes, and models
version: 1.0.0
contact:
name: ComfyUI Community
url: https://github.com/comfyanonymous/ComfyUI
servers:
- url: http://localhost:8188
description: Local ComfyUI server
paths:
/customnode/getlist:
get:
summary: Get the list of custom nodes
description: Returns the list of custom nodes from all configured channels
parameters:
- name: mode
in: query
description: "The mode to retrieve (local=installed nodes, remote=available nodes)"
schema:
type: string
enum: [local, remote]
default: remote
responses:
'200':
description: List of custom nodes
content:
application/json:
schema:
type: object
properties:
nodes:
type: array
items:
$ref: '#/components/schemas/CustomNode'
'500':
description: Server error
/customnode/get_node_mappings:
get:
summary: Get mappings between node class names and their custom nodes
description: Returns mappings that help identify which custom node package provides specific node classes
parameters:
- name: mode
in: query
description: "The mode for mappings (local=installed nodes, nickname=node nicknames)"
schema:
type: string
enum: [local, nickname]
default: local
required: true
responses:
'200':
description: Node mappings
content:
application/json:
schema:
type: object
additionalProperties:
type: string
'500':
description: Server error
/customnode/get_node_alternatives:
get:
summary: Get alternative nodes for specific node classes
description: Returns alternative implementations of node classes from different custom node packages
parameters:
- name: mode
in: query
description: "The mode to retrieve alternatives (local=installed nodes, remote=all available nodes)"
schema:
type: string
enum: [local, remote]
default: remote
responses:
'200':
description: Node alternatives
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items:
type: string
'500':
description: Server error
/externalmodel/getlist:
get:
summary: Get the list of external models
description: Returns the list of models from all configured channels
parameters:
- name: mode
in: query
description: "The mode to retrieve (local=installed models, remote=available models)"
schema:
type: string
enum: [local, remote]
default: remote
responses:
'200':
description: List of external models
content:
application/json:
schema:
type: object
properties:
models:
type: array
items:
$ref: '#/components/schemas/ExternalModel'
'500':
description: Server error
/manager/get_config:
get:
summary: Get manager configuration
description: Returns the current configuration of ComfyUI-Manager
parameters:
- name: key
in: query
description: "The configuration key to retrieve"
schema:
type: string
required: true
responses:
'200':
description: Configuration value
content:
application/json:
schema:
type: object
properties:
value:
type: string
'400':
description: Invalid key or missing parameter
'500':
description: Server error
/manager/set_config:
post:
summary: Set manager configuration
description: Updates the configuration of ComfyUI-Manager
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- key
- value
properties:
key:
type: string
description: "The configuration key to update"
value:
type: string
description: "The new value for the configuration key"
responses:
'200':
description: Configuration updated successfully
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
'400':
description: Invalid key or value
'500':
description: Server error
/snapshot/getlist:
get:
summary: Get the list of snapshots
description: Returns the list of saved snapshots
responses:
'200':
description: List of snapshots
content:
application/json:
schema:
type: object
properties:
snapshots:
type: array
items:
$ref: '#/components/schemas/Snapshot'
'500':
description: Server error
/comfyui_manager/queue/status:
get:
summary: Get queue status
description: Returns the current status of the operation queue
responses:
'200':
description: Queue status
content:
application/json:
schema:
$ref: '#/components/schemas/QueueStatus'
'500':
description: Server error
components:
schemas:
CustomNode:
type: object
required:
- name
- title
- reference
properties:
name:
type: string
description: "Internal name/ID of the custom node"
title:
type: string
description: "Display title of the custom node"
reference:
type: string
description: "Reference URL (usually GitHub repository URL)"
description:
type: string
description: "Description of what the custom node does"
install_type:
type: string
enum: [git, pip, copy]
description: "Installation method for the custom node"
files:
type: array
items:
type: string
description: "List of files provided by this custom node"
node_class_names:
type: array
items:
type: string
description: "List of node class names provided by this custom node"
installed:
type: boolean
description: "Whether the custom node is installed"
version:
type: string
description: "Version of the custom node"
tags:
type: array
items:
type: string
description: "Tags associated with the custom node"
ExternalModel:
type: object
required:
- name
- type
- url
properties:
name:
type: string
description: "Name of the model"
type:
type: string
description: "Type of the model (checkpoint, lora, embedding, etc.)"
url:
type: string
description: "Download URL for the model"
description:
type: string
description: "Description of the model"
size:
type: integer
description: "Size of the model in bytes"
installed:
type: boolean
description: "Whether the model is installed"
version:
type: string
description: "Version of the model"
tags:
type: array
items:
type: string
description: "Tags associated with the model"
Snapshot:
type: object
required:
- name
- date
properties:
name:
type: string
description: "Name of the snapshot"
date:
type: string
format: date-time
description: "Date when the snapshot was created"
description:
type: string
description: "Description of the snapshot"
nodes:
type: array
items:
type: string
description: "List of custom nodes in the snapshot"
models:
type: array
items:
type: string
description: "List of models in the snapshot"
QueueStatus:
type: object
properties:
pending:
type: array
items:
$ref: '#/components/schemas/QueueItem'
description: "List of pending operations in the queue"
completed:
type: array
items:
$ref: '#/components/schemas/QueueItem'
description: "List of completed operations in the queue"
failed:
type: array
items:
$ref: '#/components/schemas/QueueItem'
description: "List of failed operations in the queue"
running:
type: boolean
description: "Whether the queue is currently running"
QueueItem:
type: object
required:
- id
- type
- target
properties:
id:
type: string
description: "Unique ID of the queue item"
type:
type: string
enum: [install, update, uninstall]
description: "Type of operation"
target:
type: string
description: "Target of the operation (e.g., custom node name, model name)"
status:
type: string
enum: [pending, processing, completed, failed]
description: "Current status of the operation"
error:
type: string
description: "Error message if the operation failed"
created_at:
type: string
format: date-time
description: "Time when the operation was added to the queue"
completed_at:
type: string
format: date-time
description: "Time when the operation was completed"
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: "API key for authentication"

View File

@ -0,0 +1,6 @@
pytest>=7.3.1
requests>=2.31.0
openapi-spec-validator>=0.6.0
jsonschema>=4.17.3
pytest-asyncio>=0.21.0
pyyaml>=6.0

View File

@ -0,0 +1,270 @@
"""
Tests for configuration endpoints.
"""
import pytest
from typing import Callable, Dict, List, Tuple
from utils.validation import validate_response
def test_get_preview_method(
api_request: Callable
):
"""
Test getting the current preview method.
"""
# Make the API request
path = "/manager/preview_method"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is one of the valid preview methods
assert response.text in ["auto", "latent2rgb", "taesd", "none"]
def test_get_db_mode(
api_request: Callable
):
"""
Test getting the current database mode.
"""
# Make the API request
path = "/manager/db_mode"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is one of the valid database modes
assert response.text in ["channel", "local", "remote"]
def test_get_component_policy(
api_request: Callable
):
"""
Test getting the current component policy.
"""
# Make the API request
path = "/manager/policy/component"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Component policy could be any string
assert response.text is not None
def test_get_update_policy(
api_request: Callable
):
"""
Test getting the current update policy.
"""
# Make the API request
path = "/manager/policy/update"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is one of the valid update policies
assert response.text in ["stable", "nightly", "nightly-comfyui"]
def test_get_channel_url_list(
api_request: Callable,
openapi_spec: Dict
):
"""
Test getting the channel URL list.
"""
# Make the API request
path = "/manager/channel_url_list"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "selected" in json_data
assert "list" in json_data
assert isinstance(json_data["list"], list)
# Each channel should have a name and URL
if json_data["list"]:
first_channel = json_data["list"][0]
assert "name" in first_channel
assert "url" in first_channel
def test_get_manager_version(
api_request: Callable
):
"""
Test getting the manager version.
"""
# Make the API request
path = "/manager/version"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is a version string
assert response.text.startswith("V") # Version strings start with V
def test_get_manager_notice(
api_request: Callable
):
"""
Test getting the manager notice.
"""
# Make the API request
path = "/manager/notice"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Verify the response is HTML content
assert response.headers.get("Content-Type", "").startswith("text/html") or "ComfyUI" in response.text
@pytest.mark.skip(reason="State-modifying operations")
class TestConfigChanges:
"""
Tests for changing configuration settings.
These are skipped to avoid modifying state in automated tests.
"""
@pytest.fixture(scope="class", autouse=True)
def save_original_config(self, api_request: Callable):
"""
Save the original configuration to restore after tests.
"""
# Save original values
response, _ = api_request(
method="get",
path="/manager/preview_method",
expected_status=200,
)
self.original_preview_method = response.text
response, _ = api_request(
method="get",
path="/manager/db_mode",
expected_status=200,
)
self.original_db_mode = response.text
response, _ = api_request(
method="get",
path="/manager/policy/update",
expected_status=200,
)
self.original_update_policy = response.text
yield
# Restore original values
api_request(
method="get",
path="/manager/preview_method",
params={"value": self.original_preview_method},
expected_status=200,
)
api_request(
method="get",
path="/manager/db_mode",
params={"value": self.original_db_mode},
expected_status=200,
)
api_request(
method="get",
path="/manager/policy/update",
params={"value": self.original_update_policy},
expected_status=200,
)
def test_set_preview_method(self, api_request: Callable):
"""
Test setting the preview method.
"""
# Set to a different value (taesd)
api_request(
method="get",
path="/manager/preview_method",
params={"value": "taesd"},
expected_status=200,
)
# Verify it was changed
response, _ = api_request(
method="get",
path="/manager/preview_method",
expected_status=200,
)
assert response.text == "taesd"
def test_set_db_mode(self, api_request: Callable):
"""
Test setting the database mode.
"""
# Set to local mode
api_request(
method="get",
path="/manager/db_mode",
params={"value": "local"},
expected_status=200,
)
# Verify it was changed
response, _ = api_request(
method="get",
path="/manager/db_mode",
expected_status=200,
)
assert response.text == "local"
def test_set_update_policy(self, api_request: Callable):
"""
Test setting the update policy.
"""
# Set to stable
api_request(
method="get",
path="/manager/policy/update",
params={"value": "stable"},
expected_status=200,
)
# Verify it was changed
response, _ = api_request(
method="get",
path="/manager/policy/update",
expected_status=200,
)
assert response.text == "stable"

View File

@ -0,0 +1,200 @@
"""
Tests for custom node management endpoints.
"""
import pytest
from pathlib import Path
from typing import Callable, Dict, Tuple
from utils.validation import validate_response
@pytest.mark.parametrize(
"mode",
["local", "remote"]
)
def test_get_custom_node_list(
api_request: Callable,
openapi_spec: Dict,
mode: str
):
"""
Test the endpoint for listing custom nodes.
"""
# Make the API request
path = "/customnode/getlist"
response, json_data = api_request(
method="get",
path=path,
params={"mode": mode, "skip_update": "true"},
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "channel" in json_data
assert "node_packs" in json_data
assert isinstance(json_data["node_packs"], dict)
# If there are any node packs, verify they have the expected structure
if json_data["node_packs"]:
# Take the first node pack to validate
first_node_pack = next(iter(json_data["node_packs"].values()))
assert "title" in first_node_pack
assert "name" in first_node_pack
def test_get_installed_nodes(
api_request: Callable,
openapi_spec: Dict
):
"""
Test the endpoint for listing installed nodes.
"""
# Make the API request
path = "/customnode/installed"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response is a dictionary of node packs
assert isinstance(json_data, dict)
@pytest.mark.parametrize(
"mode",
["local", "nickname"]
)
def test_get_node_mappings(
api_request: Callable,
openapi_spec: Dict,
mode: str
):
"""
Test the endpoint for getting node-to-package mappings.
"""
# Make the API request
path = "/customnode/getmappings"
response, json_data = api_request(
method="get",
path=path,
params={"mode": mode},
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response is a dictionary mapping extension IDs to node info
assert isinstance(json_data, dict)
# If there are any mappings, verify they have the expected structure
if json_data:
# Take the first mapping to validate
first_mapping = next(iter(json_data.values()))
assert isinstance(first_mapping, list)
assert len(first_mapping) == 2
assert isinstance(first_mapping[0], list) # List of node classes
assert isinstance(first_mapping[1], dict) # Metadata
@pytest.mark.parametrize(
"mode",
["local", "remote"]
)
def test_get_node_alternatives(
api_request: Callable,
openapi_spec: Dict,
mode: str
):
"""
Test the endpoint for getting alternative node options.
"""
# Make the API request
path = "/customnode/alternatives"
response, json_data = api_request(
method="get",
path=path,
params={"mode": mode},
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response is a dictionary
assert isinstance(json_data, dict)
def test_fetch_updates(
api_request: Callable
):
"""
Test the endpoint for fetching updates.
This might modify state, so we just check for a valid response.
"""
# Make the API request with skip_update=true to avoid actual updates
path = "/customnode/fetch_updates"
response, _ = api_request(
method="get",
path=path,
params={"mode": "local"},
# Don't validate JSON since this endpoint doesn't return JSON
expected_status=200,
retry_on_error=False, # Don't retry as this might have side effects
)
# Just check the status code is as expected (covered by api_request)
assert response.status_code in [200, 201]
@pytest.mark.skip(reason="Queue endpoints are better tested with queue operations")
def test_queue_update_all(
api_request: Callable
):
"""
Test the endpoint for queuing updates for all nodes.
Skipping as this would actually modify the installation.
"""
pass
@pytest.mark.skip(reason="Security-restricted endpoint")
def test_install_node_via_git_url(
api_request: Callable
):
"""
Test the endpoint for installing a node via Git URL.
Skipping as this requires high security level and would modify the installation.
"""
pass

23
tests-api/test_import.py Normal file
View File

@ -0,0 +1,23 @@
import os
import sys
# Print current working directory
print(f"Current directory: {os.getcwd()}")
# Print module search path
print(f"System path: {sys.path}")
# Try to import
try:
from utils.validation import load_openapi_spec
print("Import successful!")
except ImportError as e:
print(f"Import error: {e}")
# Try direct import
try:
sys.path.insert(0, os.path.join(os.getcwd(), "custom_nodes/ComfyUI-Manager/tests-api"))
from utils.validation import load_openapi_spec
print("Direct import successful!")
except ImportError as e:
print(f"Direct import error: {e}")

View File

@ -0,0 +1,62 @@
"""
Tests for model management endpoints.
These features are scheduled for deprecation, so tests are minimal.
"""
import pytest
from typing import Callable, Dict
from utils.validation import validate_response
@pytest.mark.parametrize(
"mode",
["local", "remote"]
)
def test_get_external_model_list(
api_request: Callable,
openapi_spec: Dict,
mode: str
):
"""
Test the endpoint for listing external models.
"""
# Make the API request
path = "/externalmodel/getlist"
response, json_data = api_request(
method="get",
path=path,
params={"mode": mode},
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "models" in json_data
assert isinstance(json_data["models"], list)
# If there are any models, verify they have the expected structure
if json_data["models"]:
first_model = json_data["models"][0]
assert "name" in first_model
assert "type" in first_model
assert "url" in first_model
assert "filename" in first_model
assert "installed" in first_model
@pytest.mark.skip(reason="State-modifying operation that requires auth")
def test_install_model():
"""
Test queuing a model installation.
Skipped to avoid modifying state and requires authentication.
This feature is also scheduled for deprecation.
"""
pass

213
tests-api/test_queue_api.py Normal file
View File

@ -0,0 +1,213 @@
"""
Tests for queue management endpoints.
"""
import pytest
import time
from pathlib import Path
from typing import Callable, Dict, Tuple
from utils.validation import validate_response
def test_get_queue_status(
api_request: Callable,
openapi_spec: Dict
):
"""
Test the endpoint for getting queue status.
"""
# Make the API request
path = "/manager/queue/status"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "total_count" in json_data
assert "done_count" in json_data
assert "in_progress_count" in json_data
assert "is_processing" in json_data
# Type checks
assert isinstance(json_data["total_count"], int)
assert isinstance(json_data["done_count"], int)
assert isinstance(json_data["in_progress_count"], int)
assert isinstance(json_data["is_processing"], bool)
def test_reset_queue(
api_request: Callable
):
"""
Test the endpoint for resetting the queue.
"""
# Make the API request
path = "/manager/queue/reset"
response, _ = api_request(
method="get",
path=path,
expected_status=200,
)
# Now check the queue status to verify it was reset
response2, json_data = api_request(
method="get",
path="/manager/queue/status",
expected_status=200,
)
# Queue should be empty after reset
assert json_data["total_count"] == json_data["done_count"] + json_data["in_progress_count"]
@pytest.mark.skip(reason="State-modifying operation that requires auth")
def test_queue_install_node():
"""
Test queuing a node installation.
Skipped to avoid modifying state and requires authentication.
"""
pass
@pytest.mark.skip(reason="State-modifying operation that requires auth")
def test_queue_update_node():
"""
Test queuing a node update.
Skipped to avoid modifying state and requires authentication.
"""
pass
@pytest.mark.skip(reason="State-modifying operation that requires auth")
def test_queue_uninstall_node():
"""
Test queuing a node uninstallation.
Skipped to avoid modifying state and requires authentication.
"""
pass
@pytest.mark.skip(reason="State-modifying operation")
def test_queue_start():
"""
Test starting the queue.
Skipped to avoid modifying state.
"""
pass
class TestQueueOperations:
"""
Test a complete queue workflow.
These tests are grouped to ensure proper sequencing but are still skipped
to avoid modifying state in automated tests.
"""
@pytest.fixture(scope="class")
def node_data(self) -> Dict:
"""
Create test data for a node operation.
"""
# This would be replaced with actual data for a known safe node
return {
"ui_id": "test_node_1",
"id": "comfyui-manager", # Manager itself
"version": "latest",
"channel": "default",
"mode": "local",
}
@pytest.mark.skip(reason="State-modifying operation")
def test_queue_operation_sequence(
self,
api_request: Callable,
node_data: Dict
):
"""
Test the queue operation sequence.
"""
# 1. Reset the queue
api_request(
method="get",
path="/manager/queue/reset",
expected_status=200,
)
# 2. Queue a node operation (we'll use the manager itself)
api_request(
method="post",
path="/manager/queue/update",
json_data=node_data,
expected_status=200,
)
# 3. Check queue status - should have one operation
response, json_data = api_request(
method="get",
path="/manager/queue/status",
expected_status=200,
)
assert json_data["total_count"] > 0
assert not json_data["is_processing"] # Queue hasn't started yet
# 4. Start the queue
api_request(
method="get",
path="/manager/queue/start",
expected_status=200,
)
# 5. Check queue status again - should be processing
response, json_data = api_request(
method="get",
path="/manager/queue/status",
expected_status=200,
)
# Queue should be processing or already done
assert json_data["is_processing"] or json_data["done_count"] == json_data["total_count"]
# 6. Wait for queue to complete (with timeout)
max_wait_time = 60 # seconds
start_time = time.time()
completed = False
while time.time() - start_time < max_wait_time:
response, json_data = api_request(
method="get",
path="/manager/queue/status",
expected_status=200,
)
if json_data["done_count"] == json_data["total_count"] and not json_data["is_processing"]:
completed = True
break
time.sleep(2) # Wait before checking again
assert completed, "Queue did not complete within timeout period"
@pytest.mark.skip(reason="State-modifying operation")
def test_concurrent_queue_operations(
self,
api_request: Callable,
node_data: Dict
):
"""
Test concurrent queue operations.
"""
# This would test adding multiple operations to the queue
# and verifying they all complete correctly
pass

View File

@ -0,0 +1,198 @@
"""
Tests for snapshot management endpoints.
"""
import pytest
import time
from datetime import datetime
from pathlib import Path
from typing import Callable, Dict, List, Optional
from utils.validation import validate_response
def test_get_snapshot_list(
api_request: Callable,
openapi_spec: Dict
):
"""
Test the endpoint for listing snapshots.
"""
# Make the API request
path = "/snapshot/getlist"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Verify the response contains the expected fields
assert "items" in json_data
assert isinstance(json_data["items"], list)
def test_get_current_snapshot(
api_request: Callable,
openapi_spec: Dict
):
"""
Test the endpoint for getting the current snapshot.
"""
# Make the API request
path = "/snapshot/get_current"
response, json_data = api_request(
method="get",
path=path,
expected_status=200,
)
# Validate response structure against the schema
assert json_data is not None
validate_response(
response_data=json_data,
path=path,
method="get",
spec=openapi_spec,
)
# Check for basic snapshot structure
assert "snapshot_date" in json_data
assert "custom_nodes" in json_data
@pytest.mark.skip(reason="This test creates a snapshot which is a state-modifying operation")
def test_save_snapshot(
api_request: Callable
):
"""
Test the endpoint for saving a new snapshot.
Skipped to avoid modifying state in tests.
"""
pass
@pytest.mark.skip(reason="This test removes a snapshot which is a destructive operation")
def test_remove_snapshot(
api_request: Callable
):
"""
Test the endpoint for removing a snapshot.
Skipped to avoid modifying state in tests.
"""
pass
@pytest.mark.skip(reason="This test restores a snapshot which is a state-modifying operation")
def test_restore_snapshot(
api_request: Callable
):
"""
Test the endpoint for restoring a snapshot.
Skipped to avoid modifying state in tests.
"""
pass
class TestSnapshotWorkflow:
"""
Test the complete snapshot workflow (create, list, get, remove).
These tests are grouped to ensure proper sequencing but are still skipped
to avoid modifying state in automated tests.
"""
@pytest.fixture(scope="class")
def snapshot_name(self) -> str:
"""
Generate a unique snapshot name for testing.
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return f"test_snapshot_{timestamp}"
@pytest.mark.skip(reason="State-modifying test")
def test_create_snapshot(
self,
api_request: Callable,
snapshot_name: str
):
"""
Test creating a snapshot.
"""
# Make the API request to save a snapshot
response, _ = api_request(
method="get",
path="/snapshot/save",
expected_status=200,
)
# Verify a snapshot was created (would need to check the snapshot list)
response2, json_data = api_request(
method="get",
path="/snapshot/getlist",
expected_status=200,
)
# The most recently created snapshot should be first in the list
assert json_data["items"]
# Store the snapshot name for later tests
self.actual_snapshot_name = json_data["items"][0]
@pytest.mark.skip(reason="State-modifying test")
def test_get_snapshot_details(
self,
api_request: Callable,
openapi_spec: Dict
):
"""
Test getting details of the created snapshot.
"""
# This would check the current snapshot, not a specific one
# since there's no direct API to get a specific snapshot
response, json_data = api_request(
method="get",
path="/snapshot/get_current",
expected_status=200,
)
# Validate the snapshot data
assert json_data is not None
validate_response(
response_data=json_data,
path="/snapshot/get_current",
method="get",
spec=openapi_spec,
)
@pytest.mark.skip(reason="State-modifying test")
def test_remove_test_snapshot(
self,
api_request: Callable
):
"""
Test removing the test snapshot.
"""
# Make the API request to remove the snapshot
response, _ = api_request(
method="get",
path="/snapshot/remove",
params={"target": self.actual_snapshot_name},
expected_status=200,
)
# Verify the snapshot was removed
response2, json_data = api_request(
method="get",
path="/snapshot/getlist",
expected_status=200,
)
# The snapshot should no longer be in the list
assert self.actual_snapshot_name not in json_data["items"]

View File

@ -0,0 +1,150 @@
"""
Tests for validating the OpenAPI specification.
"""
import json
import pytest
import yaml
from typing import Dict, Any, List, Tuple
from pathlib import Path
from openapi_spec_validator import validate_spec
from utils.validation import load_openapi_spec
from utils.schema_utils import (
get_all_paths,
get_methods_for_path,
find_paths_with_security,
get_required_parameters
)
def test_spec_is_valid():
"""
Test that the OpenAPI specification is valid according to the spec validator.
"""
spec = load_openapi_spec()
validate_spec(spec)
def test_spec_has_info():
"""
Test that the OpenAPI specification has basic info.
"""
spec = load_openapi_spec()
assert "info" in spec
assert "title" in spec["info"]
assert "version" in spec["info"]
assert spec["info"]["title"] == "ComfyUI-Manager API"
def test_spec_has_paths():
"""
Test that the OpenAPI specification has paths defined.
"""
spec = load_openapi_spec()
assert "paths" in spec
assert len(spec["paths"]) > 0
def test_paths_have_responses():
"""
Test that all paths have responses defined.
"""
spec = load_openapi_spec()
for path, path_item in spec["paths"].items():
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
continue
assert "responses" in operation, f"Path {path} method {method} has no responses"
assert len(operation["responses"]) > 0, f"Path {path} method {method} has empty responses"
def test_responses_have_schemas():
"""
Test that responses with application/json content type have schemas.
"""
spec = load_openapi_spec()
for path, path_item in spec["paths"].items():
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
continue
for status, response in operation["responses"].items():
if "content" not in response:
continue
if "application/json" in response["content"]:
assert "schema" in response["content"]["application/json"], (
f"Path {path} method {method} status {status} "
f"application/json content has no schema"
)
def test_required_parameters_have_schemas():
"""
Test that all required parameters have schemas.
"""
spec = load_openapi_spec()
for path, path_item in spec["paths"].items():
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
continue
if "parameters" not in operation:
continue
for param in operation["parameters"]:
if param.get("required", False):
assert "schema" in param, (
f"Path {path} method {method} required parameter {param.get('name')} has no schema"
)
def test_security_schemes_defined():
"""
Test that security schemes are properly defined.
"""
spec = load_openapi_spec()
# Get paths requiring security
secure_paths = find_paths_with_security(spec)
if secure_paths:
assert "components" in spec, "Spec has secure paths but no components"
assert "securitySchemes" in spec["components"], "Spec has secure paths but no securitySchemes"
# Check each security reference is defined
for path, method in secure_paths:
operation = spec["paths"][path][method]
for security_req in operation["security"]:
for scheme_name in security_req:
assert scheme_name in spec["components"]["securitySchemes"], (
f"Security scheme {scheme_name} used by {method.upper()} {path} "
f"is not defined in components.securitySchemes"
)
def test_common_endpoint_groups_present():
"""
Test that the spec includes the main endpoint groups.
"""
spec = load_openapi_spec()
paths = get_all_paths(spec)
# Define the expected endpoint prefixes
expected_prefixes = [
"/customnode/",
"/externalmodel/",
"/manager/",
"/snapshot/",
"/comfyui_manager/",
]
# Check that at least one path exists for each expected prefix
for prefix in expected_prefixes:
matching_paths = [p for p in paths if p.startswith(prefix)]
assert matching_paths, f"No endpoints found with prefix {prefix}"

View File

@ -0,0 +1 @@
# Make utils directory a proper package

View File

@ -0,0 +1,174 @@
"""
Schema utilities for extracting and manipulating OpenAPI schemas.
"""
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple
from .validation import load_openapi_spec
def get_all_paths(spec: Dict[str, Any]) -> List[str]:
"""
Get all paths defined in the OpenAPI specification.
Args:
spec: The OpenAPI specification
Returns:
List of all paths
"""
return list(spec.get("paths", {}).keys())
def get_grouped_paths(spec: Dict[str, Any]) -> Dict[str, List[str]]:
"""
Group paths by their top-level segment.
Args:
spec: The OpenAPI specification
Returns:
Dictionary mapping top-level segments to lists of paths
"""
result = {}
for path in get_all_paths(spec):
segments = path.strip("/").split("/")
if not segments:
continue
top_segment = segments[0]
if top_segment not in result:
result[top_segment] = []
result[top_segment].append(path)
return result
def get_methods_for_path(spec: Dict[str, Any], path: str) -> List[str]:
"""
Get all HTTP methods defined for a path.
Args:
spec: The OpenAPI specification
path: The API path
Returns:
List of HTTP methods (lowercase)
"""
if path not in spec.get("paths", {}):
return []
return [
method.lower()
for method in spec["paths"][path].keys()
if method.lower() in {"get", "post", "put", "delete", "patch", "options", "head"}
]
def find_paths_with_security(
spec: Dict[str, Any],
security_scheme: Optional[str] = None
) -> List[Tuple[str, str]]:
"""
Find all paths that require security.
Args:
spec: The OpenAPI specification
security_scheme: Optional specific security scheme to filter by
Returns:
List of (path, method) tuples that require security
"""
result = []
for path, path_item in spec.get("paths", {}).items():
for method, operation in path_item.items():
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
continue
if "security" in operation:
if security_scheme is None:
result.append((path, method.lower()))
else:
# Check if this security scheme is required
for security_req in operation["security"]:
if security_scheme in security_req:
result.append((path, method.lower()))
break
return result
def get_content_types_for_response(
spec: Dict[str, Any],
path: str,
method: str,
status_code: str = "200"
) -> List[str]:
"""
Get content types defined for a response.
Args:
spec: The OpenAPI specification
path: The API path
method: The HTTP method
status_code: The HTTP status code
Returns:
List of content types
"""
method = method.lower()
if path not in spec["paths"]:
return []
if method not in spec["paths"][path]:
return []
if "responses" not in spec["paths"][path][method]:
return []
if status_code not in spec["paths"][path][method]["responses"]:
return []
response_def = spec["paths"][path][method]["responses"][status_code]
if "content" not in response_def:
return []
return list(response_def["content"].keys())
def get_required_parameters(
spec: Dict[str, Any],
path: str,
method: str
) -> List[Dict[str, Any]]:
"""
Get all required parameters for a path/method.
Args:
spec: The OpenAPI specification
path: The API path
method: The HTTP method
Returns:
List of parameter objects that are required
"""
method = method.lower()
if path not in spec["paths"]:
return []
if method not in spec["paths"][path]:
return []
if "parameters" not in spec["paths"][path][method]:
return []
return [
param for param in spec["paths"][path][method]["parameters"]
if param.get("required", False)
]

View File

@ -0,0 +1,155 @@
"""
Validation utilities for API tests.
"""
import json
import jsonschema
import yaml
from pathlib import Path
from typing import Any, Dict, Optional, Union
def load_openapi_spec(spec_path: Union[str, Path] = None) -> Dict[str, Any]:
"""
Load the OpenAPI specification document.
Args:
spec_path: Path to the OpenAPI specification file
Returns:
The OpenAPI specification as a dictionary
"""
if spec_path is None:
# Default to the root openapi.yaml file
spec_path = Path(__file__).parents[2] / "openapi.yaml"
with open(spec_path, "r") as f:
if str(spec_path).endswith(".yaml") or str(spec_path).endswith(".yml"):
return yaml.safe_load(f)
else:
return json.load(f)
def get_schema_for_path(
spec: Dict[str, Any],
path: str,
method: str,
status_code: str = "200",
content_type: str = "application/json"
) -> Optional[Dict[str, Any]]:
"""
Extract the response schema for a specific path, method, and status code.
Args:
spec: The OpenAPI specification
path: The API path (e.g., "/customnode/getlist")
method: The HTTP method (e.g., "get", "post")
status_code: The HTTP status code (default: "200")
content_type: The response content type (default: "application/json")
Returns:
The schema for the specified path and method, or None if not found
"""
method = method.lower()
if path not in spec["paths"]:
return None
if method not in spec["paths"][path]:
return None
if "responses" not in spec["paths"][path][method]:
return None
if status_code not in spec["paths"][path][method]["responses"]:
return None
response_def = spec["paths"][path][method]["responses"][status_code]
if "content" not in response_def:
return None
if content_type not in response_def["content"]:
return None
if "schema" not in response_def["content"][content_type]:
return None
return response_def["content"][content_type]["schema"]
def validate_response_schema(
response_data: Any,
schema: Dict[str, Any],
spec: Dict[str, Any] = None
) -> bool:
"""
Validate a response against a schema from the OpenAPI specification.
Args:
response_data: The response data to validate
schema: The schema to validate against
spec: The complete OpenAPI specification (for resolving references)
Returns:
True if validation succeeds, raises an exception otherwise
"""
if spec is None:
spec = load_openapi_spec()
# Create a resolver for references within the schema
resolver = jsonschema.RefResolver.from_schema(spec)
# Validate the response against the schema
jsonschema.validate(
instance=response_data,
schema=schema,
resolver=resolver
)
return True
def validate_response(
response_data: Any,
path: str,
method: str,
status_code: str = "200",
content_type: str = "application/json",
spec: Dict[str, Any] = None
) -> bool:
"""
Validate a response against the schema defined in the OpenAPI specification.
Args:
response_data: The response data to validate
path: The API path
method: The HTTP method
status_code: The HTTP status code (default: "200")
content_type: The response content type (default: "application/json")
spec: The OpenAPI specification (loaded from default location if None)
Returns:
True if validation succeeds, raises an exception otherwise
"""
if spec is None:
spec = load_openapi_spec()
schema = get_schema_for_path(
spec=spec,
path=path,
method=method,
status_code=status_code,
content_type=content_type
)
if schema is None:
raise ValueError(
f"No schema found for {method.upper()} {path} "
f"with status {status_code} and content type {content_type}"
)
return validate_response_schema(
response_data=response_data,
schema=schema,
spec=spec
)