Compare commits

...

146 Commits
main ... 4.0.3

Author SHA1 Message Date
Dr.Lt.Data
3f0fc85b95 refactor(core): add verbose config, improve module lookup, fix is_valid_url
- Add verbose config option to control CNR fetch logging
- Improve get_module_name with cnr_id/aux_id fallback via repo_cnr_map
- Fix is_valid_url misuse of try/finally that could cause runtime errors
- Move SSH_URL_PATTERN to module-level constant for performance
2025-12-27 03:57:19 +09:00
Dr.Lt.Data
b9def4cb6e refactor: remove preview_method and component legacy features
Some checks failed
Publish to PyPI / build-and-publish (push) Has been cancelled
Python Linting / Run Ruff (push) Has been cancelled
Preview Method Removal:
- Remove preview method UI from Manager settings panel
- Remove /v2/manager/preview_method API endpoint (legacy)
- Remove set_preview_method() and get_current_preview_method() functions
- Remove preview_method from config read/write operations
- Clean up latent_preview imports

Use ComfyUI Settings > Execution > Live preview method instead.

Component Feature Removal:
- Delete components-manager.js entirely
- Remove ComponentBuilderDialog, load_components, set_component_policy
- Remove component policy UI from Manager settings panel
- Remove /v2/manager/policy/component API endpoint
- Remove /v2/manager/component/save and /loads API endpoints
- Remove component_policy from config read/write operations
- Remove manager_components_path from context
2025-12-19 22:39:59 +09:00
David
a7eb93fff0 Changed Main Dialog to match aesthetics and close button location as Original ComfyUI Interface (#2349)
* 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 15:04:02 +09:00
Dr.Lt.Data
a542695e9c chore: bump version to 4.0.3b6 and fix git_helper path
Some checks failed
Publish to PyPI / build-and-publish (push) Has been cancelled
Python Linting / Run Ruff (push) Has been cancelled
- Update version from 4.0.3b5 to 4.0.3b6 in pyproject.toml
- Fix git_helper.py path to include 'common' subdirectory in context.py
2025-12-18 18:45:31 +09:00
Dr.Lt.Data
2779c66b39 feat(version): apply semver-based version sorting to glob and add master fallback
Some checks failed
Publish to PyPI / build-and-publish (push) Has been cancelled
Python Linting / Run Ruff (push) Has been cancelled
- Apply PR #2334 changes to glob/manager_core.py (was only in legacy)
- Add master branch fallback when remote/HEAD reference is unavailable
2025-12-15 03:39:13 +09:00
Dr.Lt.Data
952613c07b fix(api): improve import_fail_info_bulk lookup for cnr_id and aux_id
- Add aux_id format (author/repo) support in normalize_to_github_id()
- Fix get_module_name() to use URL normalization for unknown_active_nodes
- Use NormalizedKeyDict in reload() to maintain normalized key lookup
2025-12-15 02:54:30 +09:00
Benjamin Lu
75f27d99e2 ComfyUI version listing + nightly current fix (#2334)
* Improve comfyui version listing

* Fix ComfyUI semver selection and stable update

* Fix nightly current detection on default branch

* Fix: use tag_ref.name explicitly and cache get_remote_name result

- Use tag_ref.name instead of tag_ref object for checkout
- Cache get_remote_name() result to avoid duplicate calls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Dr.Lt.Data <dr.lt.data@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 23:12:01 +09:00
Dr.Lt.Data
8e8b6ca724 fix(git): handle divergent branches safely + datetime fallback
- Use --ff-only flag to detect non-fast-forward situations
- Create backup branch before resetting divergent local branch
- Reset to remote branch when fast-forward is not possible
- Add timestamp_utils.py for Mac datetime module compatibility
- Migrate all datetime usages to centralized utilities
- Bump version to 4.0.3b5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 22:45:05 +09:00
Dr.Lt.Data
3425fb7a14 docs: update manager data path to __manager
Some checks failed
Publish to PyPI / build-and-publish (push) Has been cancelled
Python Linting / Run Ruff (push) Has been cancelled
2025-12-03 02:38:15 +09:00
Dr.Lt.Data
c69e7bcf03 feat(security): use system user directory for manager data
Use folder_paths.get_system_user_directory("manager") to protect manager config and data from HTTP endpoint access.

Ref: comfyanonymous/ComfyUI#10966
2025-12-03 02:34:57 +09:00
Dr.Lt.Data
85ebcd9897 In response to the patch that separates manager_requirements.txt from requirements.txt, this update additionally refreshes manager_requirements.txt when it is present.
Some checks failed
Publish to PyPI / build-and-publish (push) Has been cancelled
Python Linting / Run Ruff (push) Has been cancelled
79fb96488a
2025-11-26 22:35:03 +09:00
Dr.Lt.Data
69b6f1a66b Merge branch 'main' into manager-v4 2025-11-26 22:14:11 +09:00
Dr.Lt.Data
d3906e3cbc bump version 2025-10-21 07:25:56 +09:00
Dr.Lt.Data
079ac254ce
fixed: Bug fix in glob/manager_server.py that prevented cache updates when installed via pip. (#2237)
Until the cacheless implementation is fully applied, the cache must always be updated — otherwise, various parts of the system will malfunction.
2025-10-21 07:16:57 +09:00
Dr.Lt.Data
e0640e7014
fixed: more complete uv support (#2230)
* Previously, only `uv` installed inside a venv was properly handled. Now `uv` installed outside the venv is also supported.
* Even if `use_uv=False`, `uv` is used as a fallback when `pip` is unavailable.
* Even if `use_uv=True`, `pip` is used as a fallback when `uv` is unavailable.

https://github.com/Comfy-Org/ComfyUI-Manager/issues/2125
2025-10-18 08:15:14 +09:00
Dr.Lt.Data
1ab2b1aeb3 modified: Reflection of changing --disable-manager to --enable-manager 2025-09-19 11:58:04 +09:00
Dr.Lt.Data
ffaeb6d3ff from draft-v4 to manager-v4 2025-09-13 08:07:44 +09:00
Dr.Lt.Data
6cc1ad4cc0 Merge branch 'main' into draft-v4 2025-09-13 08:06:45 +09:00
Dr.Lt.Data
d7799964de fixed: Issue where an invalid channel exception occurred when using the default channel
- Mismatch issue between ltdrdata/ and Comfy-Org/
modified: /v2/customnode/installed – cnr_id was being returned in a normalized form
modified: /v2/customnode/installed – when both an enabled nodepack and a disabled nodepack existed, modified to report only the enabled nodepack
fixed: Removed unnecessary warning messages printed during nodepack installation
2025-09-06 03:35:43 +09:00
Dr.Lt.Data
5378f0a8e9 bump version 2025-09-04 08:39:37 +09:00
Jin Yi
e13bf68775
Fix JSON serialization error in bulk import fail info API (#2119)
* fix: import failed info bulk api bug fix

* fix: Remove unused ImportFailInfoBulkResponse import
2025-09-04 08:36:46 +09:00
Dr.Lt.Data
d970fe68ea Merge branch 'main' into draft-v4 2025-09-03 01:24:47 +09:00
Dr.Lt.Data
41be94690f bump version 2025-08-28 00:27:03 +09:00
Christian Byrne
fbecbee4c3
Merge pull request #2106 from viva-jinyi/revert-legacy-hardcoding
Revert "As a temporary measure, the new UI will use the legacy/... ba…
2025-08-25 18:27:57 -07:00
Jin Yi
b9a7d2a78c Revert "As a temporary measure, the new UI will use the legacy/... backend structure."
This reverts commit 121a5a1888.
2025-08-26 10:07:32 +09:00
Dr.Lt.Data
bdfb70a58a bump version 2025-08-24 15:58:23 +09:00
Dr.Lt.Data
00fb9c88e1 modified: remove matrix-nio dependency from the requirements.txt
modified: The matrix share feature is now only available when the `matrix-nio` dependency is installed.

If `matrix-nio` is not installed:
1. Apply a strikethrough to the matrix checkbox text in the share UI and display a tooltip.
2. A warning is logged at startup indicating that `matrix-nio` is missing, along with the installation command.

fixed: Corrected an issue where PR #2025 was merged into draft-v4 but applied only to `legacy/..` and not to `glob/..`
2025-08-22 20:46:32 +09:00
Dr.Lt.Data
68bc8302fd Update publish-to-pypi.yml 2025-08-22 06:17:55 +09:00
Dr.Lt.Data
596dad5cda Update publish-to-pypi.yml 2025-08-22 06:14:51 +09:00
Dr.Lt.Data
a924c280fb
Update publish-to-pypi.yml 2025-08-22 06:08:59 +09:00
Dr.Lt.Data
3d0bcf5979 update workflow 2025-08-22 06:00:26 +09:00
Dr.Lt.Data
10ff90787c Merge branch 'main' into draft-v4 2025-08-21 12:48:17 +09:00
Dr.Lt.Data
648d7e73c6 Merge branch 'main' into draft-v4 2025-08-11 12:51:34 +09:00
Christian Byrne
f599bc22d7
Merge pull request #2047 from viva-jinyi/feat/pydantic-validation-bulk-api
Add Pydantic validation to import_fail_info_bulk endpoint
2025-07-31 12:34:20 -07:00
Jin Yi
300c6e7406 feat: Add Pydantic validation to import_fail_info_bulk endpoint
- Regenerated Pydantic models from updated OpenAPI specification
- Updated import_fail_info_bulk route handler to use ImportFailInfoBulkRequest/Response models
- Replaced manual JSON validation with Pydantic model validation
- Added proper error handling with ValidationError
- Updated data_models/__init__.py to export new models

Following the process outlined in data_models/README.md for type safety and consistency.
2025-07-31 14:15:21 +09:00
Dr.Lt.Data
9c4d6a0773 Merge branch 'main' into draft-v4 2025-07-31 12:44:02 +09:00
Jin Yi
60746c6253
[feat] Add bulk import failure info API endpoint (#2035)
* [feat] Add bulk import failure info API endpoint

- Add import_fail_info_bulk endpoint to both glob and legacy manager servers
- Supports bulk processing of cnr_ids and urls arrays in single request
- Maintains same error handling pattern as original import_fail_info API
- Reduces API calls from N to 1 for conflict detection optimization
- Validates input parameters and provides proper error responses

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* modified: remove manager button completely. Now, even when using the legacy UI, it must always be accessed through the menu.

* chore(api): Add temporary cache reload for import_fail_info_bulk

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Dr.Lt.Data <dr.lt.data@gmail.com>
2025-07-30 07:57:19 +09:00
Dr.Lt.Data
121a5a1888 As a temporary measure, the new UI will use the legacy/... backend structure.
The glob/... version will be applied later after the cacheless implementation is completed.
2025-07-30 01:13:17 +09:00
Dr.Lt.Data
5316ec1b4d Merge branch 'main' into draft-v4 2025-07-29 12:18:55 +09:00
Dr.Lt.Data
a2a7349ce4 Merge branch 'main' into draft-v4 2025-07-27 16:07:57 +09:00
Dr.Lt.Data
6867616973 Merge branch 'main' into draft-v4 2025-07-25 12:26:42 +09:00
Dr.Lt.Data
cf8029ecd4 Merge branch 'main' into draft-v4 2025-07-24 12:41:48 +09:00
Dr.Lt.Data
59264c1fd9 Merge branch 'main' into draft-v4 2025-07-20 19:23:24 +09:00
Dr.Lt.Data
22725b0188 add missing file 2025-07-15 18:52:17 +09:00
Dr.Lt.Data
a0b0c2b963 feat: initial implementation of middleware-based security policy 2025-07-12 11:31:07 +09:00
Dr.Lt.Data
f4ce0fd5f1 Merge branch 'main' into draft-v4 2025-07-08 12:21:47 +09:00
Dr.Lt.Data
086040f858 bump version 2025-07-01 12:55:13 +09:00
Dr.Lt.Data
adbeb527d6 added: middleware manager for security policy 2025-07-01 12:54:29 +09:00
Dr.Lt.Data
043176168d Merge branch 'main' into draft-v4 2025-07-01 12:35:39 +09:00
Dr.Lt.Data
ad79a2ef45 Merge branch 'main' into draft-v4 2025-06-28 19:59:19 +09:00
Dr.Lt.Data
43041cebed modified: Do not modify generated_models.py directly; use openapi.yaml instead. 2025-06-28 07:54:17 +09:00
Dr.Lt.Data
002e549a86 modified: security policy
- Strengthened the default security policy
- Subdivided the risky levels high and middle into high+, high, middle+, and middle
- Added support for personal_cloud network mode
- Updated README.md

fixed: invalid security message
fixed: legacy - crash when security policy violation occurred

modified: default 'use_uv' is now True
2025-06-27 01:38:38 +09:00
Dr.Lt.Data
1de6f859bf Merge branch 'main' into draft-v4 2025-06-26 23:21:04 +09:00
Dr.Lt.Data
89530fc4e7 Merge branch 'main' into draft-v4 2025-06-25 12:58:50 +09:00
Dr.Lt.Data
223d6dad51 Merge branch 'main' into draft-v4 2025-06-25 00:46:12 +09:00
Christian Byrne
cb0fa5829d
Merge pull request #1915 from Comfy-Org/feat/implement-batch-tracking-clean
[feat] Implement comprehensive batch tracking and OpenAPI-driven data models
2025-06-21 19:46:23 -07:00
bymyself
a66f86d4af cleanup records older than 16 days 2025-06-21 16:57:54 -07:00
bymyself
35d98dcea8 add batch_id to history task items 2025-06-21 16:45:50 -07:00
bymyself
38fefde06d add embedded python to system state 2025-06-21 16:29:40 -07:00
bymyself
75ecb31f8c add frontend version to system state capture 2025-06-21 16:28:00 -07:00
bymyself
77133375ad [fix] Ensure batch history is written when queue becomes empty 2025-06-21 16:01:25 -07:00
bymyself
d1ca6288a3 apply formatting 2025-06-19 16:41:16 -07:00
bymyself
624ad4cfe6 remove debug comments 2025-06-19 16:39:14 -07:00
bymyself
d912fb0f8b [fix] Remove unused imports to fix Ruff linting errors 2025-06-17 15:27:21 -07:00
bymyself
e8fc053a32 [fix] Update data models to Pydantic v2 syntax to fix TypeError 2025-06-17 15:12:25 -07:00
bymyself
ce3b2bab39 refactor 2025-06-17 14:58:34 -07:00
bymyself
15e3699535 [cleanup] Remove outdated temp_queue_batch comment 2025-06-17 14:44:58 -07:00
bymyself
a4bf6bddbf [refactor] Use Pydantic models for query parameter validation
- Added query parameter models to OpenAPI spec for GET endpoints
- Regenerated data models to include new query param models
- Replaced manual validation with Pydantic model validation
- Removed obsolete validate_required_params helper function
- Provides better error messages and type safety for API endpoints

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 14:42:25 -07:00
bymyself
f1b3c6b735 [refactor] Move model utility functions to model_utils module 2025-06-17 14:24:31 -07:00
bymyself
e923434d08 [fix] Update client filtering to handle tuple structure in pending_tasks 2025-06-17 13:52:00 -07:00
bymyself
ddc9cd0fd5 [fix] Use tuples in TaskQueue heap for proper comparison support 2025-06-17 13:42:47 -07:00
bymyself
d081db0c30 [cleanup] Remove dead code do_update_all function
- Removed do_update_all function that was never called and only returned an error
- Removed "update-all" from OperationType enum as it's no longer used
- Regenerated data models to reflect the enum change

The update_all functionality now properly creates individual update tasks through the API endpoint rather than being a single monolithic task.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 13:27:51 -07:00
bymyself
14298b0859 [fix] Remove unused imports to fix linting errors 2025-06-17 13:08:52 -07:00
bymyself
03ecda3cfe [feat] Implement comprehensive system state capture for batch records 2025-06-17 13:08:35 -07:00
bymyself
350cb767c3 [feat] Regenerate data models with enhanced ComfyUISystemState
- Add SecurityLevel and RiskLevel enums to generated models
- Enhance ComfyUISystemState with additional system information fields:
  - comfyui_root_path: ComfyUI installation directory
  - model_paths: Map of model types to configured paths
  - manager_version: ComfyUI Manager version
  - security_level: Current security configuration
  - network_mode: Network mode (online/offline/private)
  - cli_args: Selected CLI arguments
  - custom_nodes_count: Total number of custom nodes
  - failed_imports: List of failed imports
  - pip_packages: Installed pip packages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 13:06:14 -07:00
bymyself
f450dcbb57 [feat] Add SecurityLevel and RiskLevel enums to OpenAPI schema
- Add SecurityLevel enum with strong/normal/normal-/weak values
- Add RiskLevel enum with block/high/middle values
- These will be used for security policy management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 13:05:59 -07:00
bymyself
32e003965a fix files description in api 2025-06-17 10:36:52 -07:00
bymyself
65f0764338 fix duplicated schemas in openapi 2025-06-17 10:36:31 -07:00
bymyself
1bdb026079 explain glob vs legacy in claude memory 2025-06-17 10:36:08 -07:00
Dr.Lt.Data
7e51286313 Merge branch 'main' into draft-v4 2025-06-17 00:33:31 +09:00
bymyself
c888ea6435 [fix] Reduce excessive logging output to debug level
- Convert batch tracking messages to debug level (batch start, history saved)
- Convert task processing details to debug level
- Convert cache update messages to debug level
- Replace print() with logging.debug() for task processing
- Keep user-relevant messages at info level (ComfyUI updates, installation success)
- Resolves verbose output appearing without --verbose flag
2025-06-13 20:39:18 -07:00
bymyself
b089db79c5 [fix] Restore proper thread-based TaskQueue worker management
- Fix async/sync mismatch in TaskQueue worker implementation
- Use threading.Thread with asyncio.run() as originally designed
- Remove incorrect async task approach that caused blocking issues
- TaskQueue now properly manages its own thread lifecycle
- Resolves WebSocket message delivery and task processing issues
2025-06-13 20:27:41 -07:00
bymyself
7a73f5db73 [fix] Update CI to only check changed files
- Add tj-actions/changed-files to detect modified files in PR
- Only run OpenAPI validation if openapi.yaml was changed
- Only run Python linting on changed Python files (excluding legacy/)
- Remove incorrect "pip install ast" dependency
- Remove non-standard AST parsing and import checks
- Makes CI more efficient and prevents unrelated failures
2025-06-13 19:41:07 -07:00
bymyself
a96e7b114e [chore] Regenerate data models after OpenAPI fixes
- Updated generated_models.py to reflect OpenAPI 3.1 nullable format changes
- Models now use Optional[type] instead of nullable: true
- All affected models regenerated with datamodel-codegen
- Syntax and linting checks pass
2025-06-13 19:41:07 -07:00
bymyself
0148b5a3cc [fix] Fix OpenAPI validation errors for CI compliance
- Convert all nullable: true to OpenAPI 3.1 format using type: [type, 'null']
- Fix invalid array schema definition in ManagerMappings using oneOf
- Add default security: [] configuration to satisfy security-defined rule
- All 41 validation errors resolved, spec now passes with 0 errors
- 141 warnings remain (mostly missing operationId and example validation)
2025-06-13 19:41:07 -07:00
bymyself
2120a0aa79 [chore] Add dist/ to gitignore to exclude build artifacts 2025-06-13 19:40:27 -07:00
bymyself
706b6d8317 [refactor] Remove legacy thread management for TaskQueue
- Add proper async worker management to TaskQueue class
- Remove redundant task_worker_thread and task_worker_lock global variables
- Replace manual threading with async task management
- Update is_processing() logic to use TaskQueue state instead of thread status
- Implement automatic worker cleanup when queue processing completes
- Simplify queue start endpoint to use TaskQueue.start_worker()
2025-06-13 19:40:27 -07:00
bymyself
a59e6e176e [refactor] Remove redundant ExecutionStatus NamedTuple
- Eliminate TaskQueue.ExecutionStatus NamedTuple in favor of generated TaskExecutionStatus Pydantic model
- Remove manual conversion logic between NamedTuple and Pydantic model
- Use single source of truth for task execution status
- Clean up unused imports (Literal, NamedTuple)
- Maintain consistent data model usage throughout TaskQueue
2025-06-13 19:37:57 -07:00
bymyself
1d575fb654 [refactor] Replace non-standard OpenAPI validation with Redoc CLI
- Replace deprecated openapi-spec-validator with @redocly/cli
- Remove fragile custom regex-based route alignment script
- Use industry-standard OpenAPI validation tooling
- Switch from Python to Node.js for validation pipeline
- New validation catches 41 errors and 141 warnings that old validator missed
2025-06-13 19:37:57 -07:00
bymyself
98af8dc849 add claude memory 2025-06-13 19:37:57 -07:00
bymyself
4d89c69109 add installed packs to openapi 2025-06-13 19:37:57 -07:00
bymyself
b73dc6121f refresh cache before reporting status 2025-06-13 19:37:57 -07:00
bymyself
b55e1404b1 return installed pack list on status update 2025-06-13 19:37:57 -07:00
bymyself
0be0a2e6d7 migrate to data models for all routes 2025-06-13 19:37:57 -07:00
bymyself
3afafdb884 remove dist dir 2025-06-13 19:37:57 -07:00
bymyself
884b503728 [feat] Add comprehensive Pydantic validation to all API endpoints
- Updated all POST endpoints to use proper Pydantic model validation:
  - `/v2/manager/queue/task` - validates QueueTaskItem
  - `/v2/manager/queue/install_model` - validates ModelMetadata
  - `/v2/manager/queue/reinstall` - validates InstallPackParams
  - `/v2/customnode/import_fail_info` - validates cnr_id/url fields

- Added proper error handling with ValidationError for detailed error messages
- Updated TaskQueue.put() to handle both dict and Pydantic model inputs
- Added missing imports: InstallPackParams, ModelMetadata, ValidationError

Benefits:
- Early validation catches invalid data at API boundaries
- Better error messages for clients with specific validation failures
- Type safety throughout the request processing pipeline
- Consistent validation behavior across all endpoints

All ruff checks pass and validation is now enabled by default.
2025-06-13 19:37:57 -07:00
bymyself
7f1ebbe081 [cleanup] Remove completed TODO comments and fix ruff issues
- Removed completed TODO comments about code quality checks and client_id handling
- Updated comments to reflect implemented features
- Fixed ruff linting errors:
  - Removed duplicate constant definitions
  - Added missing locale import
  - Fixed unused imports
  - Moved is_local_mode logic to security_utils module
  - Added model_dir_name_map import to model_utils

All ruff checks now pass successfully.
2025-06-13 19:37:57 -07:00
bymyself
c8882dcb7c [feat] Implement comprehensive batch tracking and OpenAPI-driven data models
Enhances ComfyUI Manager with robust batch execution tracking and unified data model architecture:

- Implemented automatic batch history serialization with before/after system state snapshots
- Added comprehensive state management capturing installed nodes, models, and ComfyUI version info
- Enhanced task queue with proper client ID handling and WebSocket notifications
- Migrated all data models to OpenAPI-generated Pydantic models for consistency
- Added documentation for new TaskQueue methods (done_count, total_count, finalize)
- Fixed 64 linting errors with proper imports and code cleanup

Technical improvements:
- All models now auto-generated from openapi.yaml ensuring API/implementation consistency
- Batch tracking captures complete system state at operation start and completion
- Enhanced REST endpoints with comprehensive documentation
- Removed manual model files in favor of single source of truth
- Added helper methods for system state capture and batch lifecycle management
2025-06-13 19:36:55 -07:00
bymyself
601f1bf452 [feat] Add client_id support to task queue system
- Add client_id field to QueueTaskItem and TaskHistoryItem models
- Implement client-specific WebSocket message routing
- Add client filtering to queue status and history endpoints
- Follow ComfyUI patterns for session management
- Create data_models package for better code organization
2025-06-13 19:33:05 -07:00
Dr.Lt.Data
3870abfd2d Merge branch 'main' into draft-v4 2025-06-09 12:37:10 +09:00
Dr.Lt.Data
8303e7c043 Merge branch 'main' into draft-v4
# Conflicts:
#	comfyui_manager/common/README.md
#	comfyui_manager/glob/manager_core.py
#	comfyui_manager/js/README.md
#	pyproject.toml
2025-06-01 06:23:11 +09:00
Dr.Lt.Data
35464654c1 fixed: cm_global importing error 2025-05-19 06:10:25 +09:00
Dr.Lt.Data
ec9d52d482 Merge branch 'main' into draft-v4 2025-05-19 06:07:31 +09:00
Dr.Lt.Data
90ce448380 Merge branch 'main' into draft-v4 2025-05-12 12:21:18 +09:00
Dr.Lt.Data
56125839ac Merge branch 'main' into draft-v4 2025-04-29 00:30:02 +09:00
Dr.Lt.Data
cd49799bed fixed: crash related to deleted CNR node after installed
modified: convert cm-cli.sh to cm-cli command
2025-04-28 00:13:31 +09:00
Dr.Lt.Data
d547a05106 Merge branch 'main' into draft-v4 2025-04-27 23:17:18 +09:00
Dr.Lt.Data
db0b57a14c Merge branch 'main' into draft-v4 2025-04-24 08:44:50 +09:00
Dr.Lt.Data
2048ac87a9 modified: glob.core - make default network mode as public.
Network mode does not simply determine whether the CNR cache is used. Even after switching to cacheless in the future, it will continue to be used as a policy for user environments.
2025-04-24 08:41:17 +09:00
Dr.Lt.Data
9adf6de850 fixed: missing channels.list.template
modified: /ltdrdata -> /Comfy-Org
modified: set default network as public instead of offline
2025-04-23 08:58:47 +09:00
Dr.Lt.Data
7657c7866f fixed: perform reload when starting task worker 2025-04-22 12:39:09 +09:00
Dr.Lt.Data
d638f75117 modified: prevent displaying ComfyUI-Manager on list 2025-04-22 02:39:56 +09:00
Dr.Lt.Data
efff6b2c18 Merge branch 'main' into draft-v4 2025-04-22 01:20:57 +09:00
Dr.Lt.Data
0c46434164 fixed: avoid except:
fixed: prestartup_script - remove useless exception handling when fallback resolving comfy_path
2025-04-21 12:42:50 +09:00
Dr.Lt.Data
0bb8947c02 Merge branch 'main' into draft-v4 2025-04-21 12:12:27 +09:00
Christian Byrne
09e8e8798c
Add is_legacy_manager_ui route from the legacy package as well (#1748)
* add `is_legacy_manager_ui` route to `legacy` package  as well

* add static
2025-04-15 18:36:38 +09:00
Christian Byrne
abfd85602e
Only load legacy FE extension if --enable-manager-legacy-ui is set (#1746)
* only load JS extensions when legacy arg is set

* add `is_legacy_manager_ui` endpoint
2025-04-15 08:03:04 +09:00
Dr.Lt.Data
1816bb748e use --enable-manager-legacy-ui cli arg instead of env variable 2025-04-15 01:36:35 +09:00
Dr.Lt.Data
05ceab68f8 restructuring
the existing cache-based implementation will be retained as a fallback under legacy/..., while glob/... will be updated to a cacheless implementation.
2025-04-13 09:26:02 +09:00
Christian Byrne
46a37907e6
add development guide (#1739) 2025-04-13 08:40:28 +09:00
Dr.Lt.Data
7fc8ba587e fixed: don't disable legacy ComfyUI-Manager unless --disable-comfyui is set 2025-04-12 21:24:29 +09:00
Dr.Lt.Data
7a35bd9d9a Merge branch 'main' into draft-v4 2025-04-12 21:22:34 +09:00
Dr.Lt.Data
a76ef49d2d Merge branch 'feat/cacheless-v2' into draft-v4 2025-04-12 20:11:33 +09:00
Dr.Lt.Data
bb0fcf6ea6 added: should_be_disabled function 2025-04-12 19:35:41 +09:00
Dr.Lt.Data
539e0a1534 Merge branch 'main' into draft-v4 2025-04-12 19:06:24 +09:00
Dr.Lt.Data
aaae6ce304 Merge branch 'feat/queue_batch' into draft-v4 2025-04-12 19:05:48 +09:00
bymyself
3c413840d7 use parsed version and id even when no cnr map exists 2025-04-11 16:33:12 -07:00
bymyself
29ca93fcb4 fix: installed nodes should still be initialized in offline mode 2025-04-11 15:56:03 -07:00
bymyself
9dc8e630a0 fix is_legacy_front should be a function still 2025-04-10 18:24:34 -07:00
bymyself
10105ad737 if pip package, force offline mode 2025-04-10 13:09:56 -07:00
bymyself
5738ea861a don't load legacy web dir when --disable-manager arg set 2025-04-09 22:19:56 -07:00
Dr.Lt.Data
dbd25b0f0a Merge branch 'main' into feat/cacheless 2025-04-10 12:20:29 +09:00
bymyself
0de9d36c28 enable legacy manager frontend during beta phase 2025-04-09 14:59:32 -07:00
bymyself
05f1a8ab0d add missing v2 prefix to customnode/installed route 2025-04-09 14:59:06 -07:00
bymyself
5ce170b7ce don't handle queue in legacy front if element is not visible 2025-04-09 14:58:39 -07:00
bymyself
2b47aad076 don't show menu buttons if past comfyui front 1.16 2025-04-09 14:58:21 -07:00
bymyself
b4dc59331d fix merge conflict 2025-04-09 14:57:53 -07:00
bymyself
81e84fad78 add workflow to publish to pypi 2025-04-09 08:59:39 -07:00
Dr.Lt.Data
42e8a959dd Modify the structure to be installable via pip. 2025-04-09 08:59:37 -07:00
Dr.Lt.Data
208ca31836 support installation of system added nodepack
modified: install_by_id - Change the install path of the CNR node added by the system to be based on the repo URL instead of the CNR ID.
2025-04-09 08:57:56 -07:00
Dr.Lt.Data
a128baf894 fixed: ruff check 2025-03-25 23:40:15 +09:00
Dr.Lt.Data
57b847eebf fixed: failed[..].ui_id -> failed 2025-03-24 23:12:45 +09:00
Dr.Lt.Data
149257e4f1 Merge branch 'main' into feat/queue_batch 2025-03-24 22:53:13 +09:00
Dr.Lt.Data
212b8e7ed2 feat: support task batch
POST /v2/manager/queue/batch
GET /v2/manager/queue/history_list
GET /v2/manager/queue/history?id={id}
GET /v2/manager/queue/abort_current
2025-03-24 22:49:38 +09:00
Dr.Lt.Data
01ac9c895a Modify the structure to be installable via pip. 2025-03-19 22:15:53 +09:00
Dr.Lt.Data
ebcb14e6aa support installation of system added nodepack
modified: install_by_id - Change the install path of the CNR node added by the system to be based on the repo URL instead of the CNR ID.
2025-03-19 07:41:39 +09:00
86 changed files with 10656 additions and 3204 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
PYPI_TOKEN=your-pypi-token

70
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: CI
on:
push:
branches: [ main, feat/*, fix/* ]
pull_request:
branches: [ main ]
jobs:
validate-openapi:
name: Validate OpenAPI Specification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check if OpenAPI changed
id: openapi-changed
uses: tj-actions/changed-files@v44
with:
files: openapi.yaml
- name: Setup Node.js
if: steps.openapi-changed.outputs.any_changed == 'true'
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install Redoc CLI
if: steps.openapi-changed.outputs.any_changed == 'true'
run: |
npm install -g @redocly/cli
- name: Validate OpenAPI specification
if: steps.openapi-changed.outputs.any_changed == 'true'
run: |
redocly lint openapi.yaml
code-quality:
name: Code Quality Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for proper diff
- name: Get changed Python files
id: changed-py-files
uses: tj-actions/changed-files@v44
with:
files: |
**/*.py
files_ignore: |
comfyui_manager/legacy/**
- name: Setup Python
if: steps.changed-py-files.outputs.any_changed == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install dependencies
if: steps.changed-py-files.outputs.any_changed == 'true'
run: |
pip install ruff
- name: Run ruff linting on changed files
if: steps.changed-py-files.outputs.any_changed == 'true'
run: |
echo "Changed files: ${{ steps.changed-py-files.outputs.all_changed_files }}"
echo "${{ steps.changed-py-files.outputs.all_changed_files }}" | xargs -r ruff check

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch:
push:
branches:
- draft-v4
- manager-v4
paths:
- "pyproject.toml"
@ -21,7 +21,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
python-version: '3.x'
- name: Install build dependencies
run: |
@ -31,28 +31,28 @@ jobs:
- name: Get current version
id: current_version
run: |
CURRENT_VERSION=$(grep -oP 'version = "\K[^"]+' pyproject.toml)
CURRENT_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "Current version: $CURRENT_VERSION"
- name: Build package
run: python -m build
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: dist/*
tag_name: v${{ steps.current_version.outputs.version }}
draft: false
prerelease: false
generate_release_notes: true
# - name: Create GitHub Release
# id: create_release
# uses: softprops/action-gh-release@v2
# env:
# GITHUB_TOKEN: ${{ github.token }}
# with:
# files: dist/*
# tag_name: v${{ steps.current_version.outputs.version }}
# draft: false
# prerelease: false
# generate_release_notes: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
with:
password: ${{ secrets.PYPI_TOKEN }}
skip-existing: true
verbose: true
verbose: true

View File

@ -1,25 +0,0 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main-blocked
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'ltdrdata' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}

6
.gitignore vendored
View File

@ -17,4 +17,8 @@ github-stats-cache.json
pip_overrides.json
*.json
check2.sh
/venv/
/venv/
build
dist
*.egg-info
.env

47
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,47 @@
## Testing Changes
1. Activate the ComfyUI environment.
2. Build package locally after making changes.
```bash
# from inside the ComfyUI-Manager directory, with the ComfyUI environment activated
python -m build
```
3. Install the package locally in the ComfyUI environment.
```bash
# Uninstall existing package
pip uninstall comfyui-manager
# Install the locale package
pip install dist/comfyui-manager-*.whl
```
4. Start ComfyUI.
```bash
# after navigating to the ComfyUI directory
python main.py
```
## Manually Publish Test Version to PyPi
1. Set the `PYPI_TOKEN` environment variable in env file.
2. If manually publishing, you likely want to use a release candidate version, so set the version in [pyproject.toml](pyproject.toml) to something like `0.0.1rc1`.
3. Build the package.
```bash
python -m build
```
4. Upload the package to PyPi.
```bash
python -m twine upload dist/* --username __token__ --password $PYPI_TOKEN
```
5. View at https://pypi.org/project/comfyui-manager/

14
MANIFEST.in Normal file
View File

@ -0,0 +1,14 @@
include comfyui_manager/js/*
include comfyui_manager/*.json
include comfyui_manager/glob/*
include LICENSE.txt
include README.md
include requirements.txt
include pyproject.toml
include custom-node-list.json
include extension-node-list.json
include extras.json
include github-stats.json
include model-list.json
include alter-list.json
include comfyui_manager/channels.list.template

170
README.md
View File

@ -5,6 +5,7 @@
![menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/refs/heads/Main/ComfyUI-Manager/images/dialog.jpg)
## NOTICE
* V4.0: Modify the structure to be installable via pip instead of using git clone.
* V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`.
* V3.10: `double-click feature` is removed
* This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper
@ -13,78 +14,26 @@
## Installation
### Installation[method1] (General installation method: ComfyUI-Manager only)
* When installing the latest ComfyUI, it will be automatically installed as a dependency, so manual installation is no longer necessary.
To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps:
* Manual installation of the nightly version:
* Clone to a temporary directory (**Note:** Do **not** clone into `ComfyUI/custom_nodes`.)
```
git clone https://github.com/Comfy-Org/ComfyUI-Manager
```
* Install via pip
```
cd ComfyUI-Manager
pip install .
```
1. Go to `ComfyUI/custom_nodes` dir in terminal (cmd)
2. `git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager`
3. Restart ComfyUI
### Installation[method2] (Installation for portable ComfyUI version: ComfyUI-Manager only)
1. install git
- https://git-scm.com/download/win
- standalone version
- select option: use windows default console window
2. Download [scripts/install-manager-for-portable-version.bat](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-manager-for-portable-version.bat) into installed `"ComfyUI_windows_portable"` directory
- Don't click. Right-click the link and choose 'Save As...'
3. Double-click `install-manager-for-portable-version.bat` batch file
![portable-install](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/portable-install.jpg)
### Installation[method3] (Installation through comfy-cli: install ComfyUI and ComfyUI-Manager at once.)
> RECOMMENDED: comfy-cli provides various features to manage ComfyUI from the CLI.
* **prerequisite: python 3, git**
Windows:
```commandline
python -m venv venv
venv\Scripts\activate
pip install comfy-cli
comfy install
```
Linux/macOS:
```commandline
python -m venv venv
. venv/bin/activate
pip install comfy-cli
comfy install
```
* See also: https://github.com/Comfy-Org/comfy-cli
### Installation[method4] (Installation for Linux+venv: ComfyUI + ComfyUI-Manager)
## Front-end
To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you can follow these steps:
* **prerequisite: python-is-python3, python3-venv, git**
1. Download [scripts/install-comfyui-venv-linux.sh](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-comfyui-venv-linux.sh) into empty install directory
- Don't click. Right-click the link and choose 'Save As...'
- ComfyUI will be installed in the subdirectory of the specified directory, and the directory will contain the generated executable script.
2. `chmod +x install-comfyui-venv-linux.sh`
3. `./install-comfyui-venv-linux.sh`
### Installation Precautions
* **DO**: `ComfyUI-Manager` files must be accurately located in the path `ComfyUI/custom_nodes/comfyui-manager`
* Installing in a compressed file format is not recommended.
* **DON'T**: Decompress directly into the `ComfyUI/custom_nodes` location, resulting in the Manager contents like `__init__.py` being placed directly in that directory.
* You have to remove all ComfyUI-Manager files from `ComfyUI/custom_nodes`
* **DON'T**: In a form where decompression occurs in a path such as `ComfyUI/custom_nodes/ComfyUI-Manager/ComfyUI-Manager`.
* **DON'T**: In a form where decompression occurs in a path such as `ComfyUI/custom_nodes/ComfyUI-Manager-main`.
* In such cases, `ComfyUI-Manager` may operate, but it won't be recognized within `ComfyUI-Manager`, and updates cannot be performed. It also poses the risk of duplicate installations. Remove it and install properly via `git clone` method.
You can execute ComfyUI by running either `./run_gpu.sh` or `./run_cpu.sh` depending on your system configuration.
## Colab Notebook
This repository provides Colab notebooks that allow you to install and use ComfyUI, including ComfyUI-Manager. To use ComfyUI, [click on this link](https://colab.research.google.com/github/ltdrdata/ComfyUI-Manager/blob/main/notebooks/comfyui_colab_with_manager.ipynb).
* Support for installing ComfyUI
* Support for basic installation of ComfyUI-Manager
* Support for automatically installing dependencies of custom nodes upon restarting Colab notebooks.
* The built-in front-end of ComfyUI-Manager is the legacy front-end. The front-end for ComfyUI-Manager is now provided via [ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend).
* To enable the legacy front-end, set the environment variable `ENABLE_LEGACY_COMFYUI_MANAGER_FRONT` to `true` before running.
## How To Use
@ -140,20 +89,20 @@ This repository provides Colab notebooks that allow you to install and use Comfy
## Paths
In `ComfyUI-Manager` V3.0 and later, configuration files and dynamically generated files are located under `<USER_DIRECTORY>/default/ComfyUI-Manager/`.
In `ComfyUI-Manager` V4.0.3b4 and later, configuration files and dynamically generated files are located under `<USER_DIRECTORY>/__manager/`.
* <USER_DIRECTORY>
* If executed without any options, the path defaults to ComfyUI/user.
* It can be set using --user-directory <USER_DIRECTORY>.
* <USER_DIRECTORY>
* If executed without any options, the path defaults to ComfyUI/user.
* It can be set using --user-directory <USER_DIRECTORY>.
* Basic config files: `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini`
* Configurable channel lists: `<USER_DIRECTORY>/default/ComfyUI-Manager/channels.ini`
* Configurable pip overrides: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_overrides.json`
* Configurable pip blacklist: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_blacklist.list`
* Configurable pip auto fix: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_auto_fix.list`
* Saved snapshot files: `<USER_DIRECTORY>/default/ComfyUI-Manager/snapshots`
* Startup script files: `<USER_DIRECTORY>/default/ComfyUI-Manager/startup-scripts`
* Component files: `<USER_DIRECTORY>/default/ComfyUI-Manager/components`
* Basic config files: `<USER_DIRECTORY>/__manager/config.ini`
* Configurable channel lists: `<USER_DIRECTORY>/__manager/channels.ini`
* Configurable pip overrides: `<USER_DIRECTORY>/__manager/pip_overrides.json`
* Configurable pip blacklist: `<USER_DIRECTORY>/__manager/pip_blacklist.list`
* Configurable pip auto fix: `<USER_DIRECTORY>/__manager/pip_auto_fix.list`
* Saved snapshot files: `<USER_DIRECTORY>/__manager/snapshots`
* Startup script files: `<USER_DIRECTORY>/__manager/startup-scripts`
* Component files: `<USER_DIRECTORY>/__manager/components`
## `extra_model_paths.yaml` Configuration
@ -166,12 +115,12 @@ The following settings are applied based on the section marked as `is_default`.
## Snapshot-Manager
* When you press `Save snapshot` or use `Update All` on `Manager Menu`, the current installation status snapshot is saved.
* Snapshot file dir: `<USER_DIRECTORY>/default/ComfyUI-Manager/snapshots`
* Snapshot file dir: `<USER_DIRECTORY>/__manager/snapshots`
* You can rename snapshot file.
* Press the "Restore" button to revert to the installation status of the respective snapshot.
* However, for custom nodes not managed by Git, snapshot support is incomplete.
* When you press `Restore`, it will take effect on the next ComfyUI startup.
* The selected snapshot file is saved in `<USER_DIRECTORY>/default/ComfyUI-Manager/startup-scripts/restore-snapshot.json`, and upon restarting ComfyUI, the snapshot is applied and then deleted.
* The selected snapshot file is saved in `<USER_DIRECTORY>/__manager/startup-scripts/restore-snapshot.json`, and upon restarting ComfyUI, the snapshot is applied and then deleted.
![model-install-dialog](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/snapshot.jpg)
@ -220,12 +169,12 @@ The following settings are applied based on the section marked as `is_default`.
}
```
* `<current timestamp>` Ensure that the timestamp is always unique.
* "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
* "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/__manager/components`.
* `<component name>`: The name should be in the format `<prefix>::<node name>`.
* `<component node data>`: In the node data of the group node.
* `<version>`: Only two formats are allowed: `major.minor.patch` or `major.minor`. (e.g. `1.0`, `2.2.1`)
* `<datetime>`: Saved time
* `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
* `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/__manager/components`.
* `<category>`: If there is neither a category nor a packname, it is saved in the components category.
```
"version":"1.0",
@ -266,13 +215,14 @@ The following settings are applied based on the section marked as `is_default`.
downgrade_blacklist = <Set a list of packages to prevent downgrades. List them separated by commas.>
security_level = <Set the security level => strong|normal|normal-|weak>
always_lazy_install = <Whether to perform dependency installation on restart even in environments other than Windows.>
network_mode = <Set the network mode => public|private|offline>
network_mode = <Set the network mode => public|private|offline|personal_cloud>
```
* network_mode:
- public: An environment that uses a typical public network.
- private: An environment that uses a closed network, where a private node DB is configured via `channel_url`. (Uses cache if available)
- offline: An environment that does not use any external connections when using an offline network. (Uses cache if available)
- personal_cloud: Applies relaxed security features in cloud environments such as Google Colab or Runpod, where strong security is not required.
## Additional Feature
@ -354,7 +304,7 @@ When you run the `scan.sh` script:
## Troubleshooting
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini` file that is generated.
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/__manager/config.ini` file that is generated.
* If updating ComfyUI-Manager itself fails, please go to the **ComfyUI-Manager** directory and execute the command `git update-ref refs/remotes/origin/main a361cc1 && git fetch --all && git pull`.
* If you encounter the error message `Overlapped Object has pending operation at deallocation on ComfyUI Manager load` under Windows
* Edit `config.ini` file: add `windows_selector_event_loop_policy = True`
@ -363,31 +313,33 @@ When you run the `scan.sh` script:
## Security policy
* Edit `config.ini` file: add `security_level = <LEVEL>`
* `strong`
* doesn't allow `high` and `middle` level risky feature
* `normal`
* doesn't allow `high` level risky feature
* `middle` level risky feature is available
* `normal-`
* doesn't allow `high` level risky feature if `--listen` is specified and not starts with `127.`
* `middle` level risky feature is available
* `weak`
* all feature is available
* `high` level risky features
* `Install via git url`, `pip install`
* Installation of custom nodes registered not in the `default channel`.
* Fix custom nodes
* `middle` level risky features
* Uninstall/Update
* Installation of custom nodes registered in the `default channel`.
* Restore/Remove Snapshot
* Restart
* `low` level risky features
* Update ComfyUI
The security settings are applied based on whether the ComfyUI server's listener is non-local and whether the network mode is set to `personal_cloud`.
* **non-local**: When the server is launched with `--listen` and is bound to a network range other than the local `127.` range, allowing remote IP access.
* **personal\_cloud**: When the `network_mode` is set to `personal_cloud`.
### Risky Level Table
| Risky Level | features |
|-------------|---------------------------------------------------------------------------------------------------------------------------------------|
| high+ | * `Install via git url`, `pip install`<BR>* Installation of nodepack registered not in the `default channel`. |
| high | * Fix nodepack |
| middle+ | * Uninstall/Update<BR>* Installation of nodepack registered in the `default channel`.<BR>* Restore/Remove Snapshot<BR>* Install model |
| middle | * Restart |
| low | * Update ComfyUI |
### Security Level Table
| Security Level | local | non-local (personal_cloud) | non-local (not personal_cloud) |
|----------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| strong | * Only `weak` level risky features are allowed | * Only `weak` level risky features are allowed | * Only `weak` level risky features are allowed |
| normal | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+`, `high` and `middle+` level risky features are not allowed<BR>* `middle` level risky features are available
| normal- | * All features are available | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+`, `high` and `middle+` level risky features are not allowed<BR>* `middle` level risky features are available
| weak | * All features are available | * All features are available | * `high+` and `middle+` level risky features are not allowed<BR>* `high`, `middle` and `low` level risky features are available
# Disclaimer

View File

@ -1,25 +0,0 @@
"""
This file is the entry point for the ComfyUI-Manager package, handling CLI-only mode and initial setup.
"""
import os
import sys
cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode')
if not os.path.exists(cli_mode_flag):
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
import manager_server # noqa: F401
import share_3rdparty # noqa: F401
import cm_global
if not cm_global.disable_front and not 'DISABLE_COMFYUI_MANAGER_FRONT' in os.environ:
WEB_DIRECTORY = "js"
else:
print("\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n")
NODE_CLASS_MAPPINGS = {}
__all__ = ['NODE_CLASS_MAPPINGS']

View File

@ -1,6 +0,0 @@
default::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main
recent::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/new
legacy::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/legacy
forked::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/forked
dev::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/dev
tutorial::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/tutorial

View File

@ -1,2 +0,0 @@
#!/bin/bash
python cm-cli.py $*

View File

@ -2,20 +2,16 @@
This directory contains the Python backend modules that power ComfyUI-Manager, handling the core functionality of node management, downloading, security, and server operations.
## Core Modules
## Directory Structure
- **glob/** - code for new cacheless ComfyUI-Manager
- **legacy/** - code for legacy ComfyUI-Manager
## Core Modules
- **manager_core.py**: The central implementation of management functions, handling configuration, installation, updates, and node management.
- **manager_server.py**: Implements server functionality and API endpoints for the web interface to interact with the backend.
- **manager_downloader.py**: Handles downloading operations for models, extensions, and other resources.
- **manager_util.py**: Provides utility functions used throughout the system.
## Specialized Modules
- **cm_global.py**: Maintains global variables and state management across the system.
- **cnr_utils.py**: Helper utilities for interacting with the custom node registry (CNR).
- **git_utils.py**: Git-specific utilities for repository operations.
- **node_package.py**: Handles the packaging and installation of node extensions.
- **security_check.py**: Implements the multi-level security system for installation safety.
- **share_3rdparty.py**: Manages integration with third-party sharing platforms.
## Architecture

104
comfyui_manager/__init__.py Normal file
View File

@ -0,0 +1,104 @@
import os
import logging
from aiohttp import web
from .common.manager_security import HANDLER_POLICY
from .common import manager_security
from comfy.cli_args import args
def prestartup():
from . import prestartup_script # noqa: F401
logging.info('[PRE] ComfyUI-Manager')
def start():
logging.info('[START] ComfyUI-Manager')
from .common import cm_global # noqa: F401
if args.enable_manager:
if args.enable_manager_legacy_ui:
try:
from .legacy import manager_server # noqa: F401
from .legacy import share_3rdparty # noqa: F401
from .legacy import manager_core as core
import nodes
logging.info("[ComfyUI-Manager] Legacy UI is enabled.")
nodes.EXTENSION_WEB_DIRS['comfyui-manager-legacy'] = os.path.join(os.path.dirname(__file__), 'js')
except Exception as e:
print("Error enabling legacy ComfyUI Manager frontend:", e)
core = None
else:
from .glob import manager_server # noqa: F401
from .glob import share_3rdparty # noqa: F401
from .glob import manager_core as core
if core is not None:
manager_security.is_personal_cloud_mode = core.get_config()['network_mode'].lower() == 'personal_cloud'
def should_be_disabled(fullpath:str) -> bool:
"""
1. Disables the legacy ComfyUI-Manager.
2. The blocklist can be expanded later based on policies.
"""
if args.enable_manager:
# In cases where installation is done via a zip archive, the directory name may not be comfyui-manager, and it may not contain a git repository.
# It is assumed that any installed legacy ComfyUI-Manager will have at least 'comfyui-manager' in its directory name.
dir_name = os.path.basename(fullpath).lower()
if 'comfyui-manager' in dir_name:
return True
return False
def get_client_ip(request):
peername = request.transport.get_extra_info("peername")
if peername is not None:
host, port = peername
return host
return "unknown"
def create_middleware():
connected_clients = set()
is_local_mode = manager_security.is_loopback(args.listen)
@web.middleware
async def manager_middleware(request: web.Request, handler):
nonlocal connected_clients
# security policy for remote environments
prev_client_count = len(connected_clients)
client_ip = get_client_ip(request)
connected_clients.add(client_ip)
next_client_count = len(connected_clients)
if prev_client_count == 1 and next_client_count > 1:
manager_security.multiple_remote_alert()
policy = manager_security.get_handler_policy(handler)
is_banned = False
# policy check
if len(connected_clients) > 1:
if is_local_mode:
if HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NON_LOCAL in policy:
is_banned = True
if HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD in policy:
is_banned = not manager_security.is_personal_cloud_mode
if HANDLER_POLICY.BANNED in policy:
is_banned = True
if is_banned:
logging.warning(f"[Manager] Banning request from {client_ip}: {request.path}")
response = web.Response(text="[Manager] This request is banned.", status=403)
else:
response: web.Response = await handler(request)
return response
return manager_middleware

View File

@ -0,0 +1,6 @@
default::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main
recent::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/new
legacy::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/legacy
forked::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/forked
dev::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/dev
tutorial::https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/node_db/tutorial

View File

@ -15,31 +15,31 @@ import git
import importlib
sys.path.append(os.path.dirname(__file__))
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
import manager_util
from ..common import manager_util
# read env vars
# COMFYUI_FOLDERS_BASE_PATH is not required in cm-cli.py
# `comfy_path` should be resolved before importing manager_core
comfy_path = os.environ.get('COMFYUI_PATH')
if comfy_path is None:
try:
import folder_paths
comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))
except:
print("\n[bold yellow]WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.[/bold yellow]", file=sys.stderr)
comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))
# This should be placed here
comfy_path = os.environ.get('COMFYUI_PATH')
if comfy_path is None:
print("[bold red]cm-cli: environment variable 'COMFYUI_PATH' is not specified.[/bold red]")
exit(-1)
sys.path.append(comfy_path)
if not os.path.exists(os.path.join(comfy_path, 'folder_paths.py')):
print("[bold red]cm-cli: '{comfy_path}' is not a valid 'COMFYUI_PATH' location.[/bold red]")
exit(-1)
import utils.extra_config
import cm_global
import manager_core as core
from manager_core import unified_manager
import cnr_utils
from ..common import cm_global
from ..legacy import manager_core as core
from ..common import context
from ..legacy.manager_core import unified_manager
from ..common import cnr_utils
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
@ -66,7 +66,7 @@ def check_comfyui_hash():
repo = git.Repo(comfy_path)
core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))
core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
except:
except Exception:
print('[bold yellow]INFO: Frozen ComfyUI mode.[/bold yellow]')
core.comfy_ui_revision = 0
core.comfy_ui_commit_datetime = 0
@ -82,7 +82,7 @@ def read_downgrade_blacklist():
try:
import configparser
config = configparser.ConfigParser(strict=False)
config.read(core.manager_config.path)
config.read(context.manager_config_path)
default_conf = config['default']
if 'downgrade_blacklist' in default_conf:
@ -90,7 +90,7 @@ def read_downgrade_blacklist():
items = [x.strip() for x in items if x != '']
cm_global.pip_downgrade_blacklist += items
cm_global.pip_downgrade_blacklist = list(set(cm_global.pip_downgrade_blacklist))
except:
except Exception:
pass
@ -105,7 +105,7 @@ class Ctx:
self.no_deps = False
self.mode = 'cache'
self.user_directory = None
self.custom_nodes_paths = [os.path.join(core.comfy_base_path, 'custom_nodes')]
self.custom_nodes_paths = [os.path.join(context.comfy_base_path, 'custom_nodes')]
self.manager_files_directory = os.path.dirname(__file__)
if Ctx.folder_paths is None:
@ -143,14 +143,14 @@ class Ctx:
if os.path.exists(extra_model_paths_yaml):
utils.extra_config.load_extra_path_config(extra_model_paths_yaml)
core.update_user_directory(user_directory)
context.update_user_directory(user_directory)
if os.path.exists(core.manager_pip_overrides_path):
with open(core.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
if os.path.exists(context.manager_pip_overrides_path):
with open(context.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
cm_global.pip_overrides = json.load(json_file)
if os.path.exists(core.manager_pip_blacklist_path):
with open(core.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
if os.path.exists(context.manager_pip_blacklist_path):
with open(context.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
for x in f.readlines():
y = x.strip()
if y != '':
@ -163,15 +163,15 @@ class Ctx:
@staticmethod
def get_startup_scripts_path():
return os.path.join(core.manager_startup_script_path, "install-scripts.txt")
return os.path.join(context.manager_startup_script_path, "install-scripts.txt")
@staticmethod
def get_restore_snapshot_path():
return os.path.join(core.manager_startup_script_path, "restore-snapshot.json")
return os.path.join(context.manager_startup_script_path, "restore-snapshot.json")
@staticmethod
def get_snapshot_path():
return core.manager_snapshot_path
return context.manager_snapshot_path
@staticmethod
def get_custom_nodes_paths():
@ -438,8 +438,11 @@ def show_list(kind, simple=False):
flag = kind in ['all', 'cnr', 'installed', 'enabled']
for k, v in unified_manager.active_nodes.items():
if flag:
cnr = unified_manager.cnr_map[k]
processed[k] = "[ ENABLED ] ", cnr['name'], k, cnr['publisher']['name'], v[0]
cnr = unified_manager.cnr_map.get(k)
if cnr:
processed[k] = "[ ENABLED ] ", cnr['name'], k, cnr['publisher']['name'], v[0]
else:
processed[k] = None
else:
processed[k] = None
@ -459,8 +462,11 @@ def show_list(kind, simple=False):
continue
if flag:
cnr = unified_manager.cnr_map[k]
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], ", ".join(list(v.keys()))
cnr = unified_manager.cnr_map.get(k) # NOTE: can this be None if removed from CNR after installed
if cnr:
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], ", ".join(list(v.keys()))
else:
processed[k] = None
else:
processed[k] = None
@ -469,8 +475,11 @@ def show_list(kind, simple=False):
continue
if flag:
cnr = unified_manager.cnr_map[k]
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], 'nightly'
cnr = unified_manager.cnr_map.get(k)
if cnr:
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], 'nightly'
else:
processed[k] = None
else:
processed[k] = None
@ -490,9 +499,12 @@ def show_list(kind, simple=False):
continue
if flag:
cnr = unified_manager.cnr_map[k]
ver_spec = v['latest_version']['version'] if 'latest_version' in v else '0.0.0'
processed[k] = "[ NOT INSTALLED ] ", cnr['name'], k, cnr['publisher']['name'], ver_spec
cnr = unified_manager.cnr_map.get(k)
if cnr:
ver_spec = v['latest_version']['version'] if 'latest_version' in v else '0.0.0'
processed[k] = "[ NOT INSTALLED ] ", cnr['name'], k, cnr['publisher']['name'], ver_spec
else:
processed[k] = None
else:
processed[k] = None
@ -658,7 +670,7 @@ def install(
cmd_ctx.set_channel_mode(channel, mode)
cmd_ctx.set_no_deps(no_deps)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
for_each_nodes(nodes, act=install_node, exit_on_fail=exit_on_fail)
pip_fixer.fix_broken()
@ -696,7 +708,7 @@ def reinstall(
cmd_ctx.set_channel_mode(channel, mode)
cmd_ctx.set_no_deps(no_deps)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
for_each_nodes(nodes, act=reinstall_node)
pip_fixer.fix_broken()
@ -750,7 +762,7 @@ def update(
if 'all' in nodes:
asyncio.run(auto_save_snapshot())
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
for x in nodes:
if x.lower() in ['comfyui', 'comfy', 'all']:
@ -851,7 +863,7 @@ def fix(
if 'all' in nodes:
asyncio.run(auto_save_snapshot())
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
for_each_nodes(nodes, fix_node, allow_all=True)
pip_fixer.fix_broken()
@ -1128,7 +1140,7 @@ def restore_snapshot(
print(f"[bold red]ERROR: `{snapshot_path}` is not exists.[/bold red]")
exit(1)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
try:
asyncio.run(core.restore_snapshot(snapshot_path, extras))
except Exception:
@ -1160,7 +1172,7 @@ def restore_dependencies(
total = len(node_paths)
i = 1
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
for x in node_paths:
print("----------------------------------------------------------------------------------------------------")
print(f"Restoring [{i}/{total}]: {x}")
@ -1179,7 +1191,7 @@ def post_install(
):
path = os.path.expanduser(path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
unified_manager.execute_install_script('', path, instant_execution=True)
pip_fixer.fix_broken()
@ -1219,11 +1231,11 @@ def install_deps(
with open(deps, 'r', encoding="UTF-8", errors="ignore") as json_file:
try:
json_obj = json.load(json_file)
except:
except Exception:
print(f"[bold red]Invalid json file: {deps}[/bold red]")
exit(1)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, context.manager_files_path)
for k in json_obj['custom_nodes'].keys():
state = core.simple_check_custom_node(k)
if state == 'installed':
@ -1280,6 +1292,10 @@ def export_custom_node_ids(
print(f"{x['id']}@unknown", file=output_file)
def main():
app()
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(app())

View File

@ -0,0 +1,16 @@
# ComfyUI-Manager: Core Backend (glob)
This directory contains the Python backend modules that power ComfyUI-Manager, handling the core functionality of node management, downloading, security, and server operations.
## Core Modules
- **manager_downloader.py**: Handles downloading operations for models, extensions, and other resources.
- **manager_util.py**: Provides utility functions used throughout the system.
## Specialized Modules
- **cm_global.py**: Maintains global variables and state management across the system.
- **cnr_utils.py**: Helper utilities for interacting with the custom node registry (CNR).
- **git_utils.py**: Git-specific utilities for repository operations.
- **node_package.py**: Handles the packaging and installation of node extensions.
- **security_check.py**: Implements the multi-level security system for installation safety.

View File

@ -0,0 +1,17 @@
from .timestamp_utils import (
current_timestamp,
get_timestamp_for_filename,
get_timestamp_for_path,
get_backup_branch_name,
get_now,
get_unix_timestamp,
)
__all__ = [
'current_timestamp',
'get_timestamp_for_filename',
'get_timestamp_for_path',
'get_backup_branch_name',
'get_now',
'get_unix_timestamp',
]

View File

@ -6,10 +6,12 @@ import time
from dataclasses import dataclass
from typing import List
import manager_core
import manager_util
from . import context
from . import manager_util
import requests
import toml
import logging
base_url = "https://api.comfy.org"
@ -22,7 +24,7 @@ async def get_cnr_data(cache_mode=True, dont_wait=True):
try:
return await _get_cnr_data(cache_mode, dont_wait)
except asyncio.TimeoutError:
print("A timeout occurred during the fetch process from ComfyRegistry.")
logging.info("A timeout occurred during the fetch process from ComfyRegistry.")
return await _get_cnr_data(cache_mode=True, dont_wait=True) # timeout fallback
async def _get_cnr_data(cache_mode=True, dont_wait=True):
@ -47,9 +49,9 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
# Get ComfyUI version tag
if is_desktop:
# extract version from pyproject.toml instead of git tag
comfyui_ver = manager_core.get_current_comfyui_ver() or 'unknown'
comfyui_ver = context.get_current_comfyui_ver() or 'unknown'
else:
comfyui_ver = manager_core.get_comfyui_tag() or 'unknown'
comfyui_ver = context.get_comfyui_tag() or 'unknown'
if is_desktop:
if is_windows:
@ -67,7 +69,10 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
form_factor = 'git-linux'
else:
form_factor = 'other'
from comfyui_manager.glob import manager_core
verbose = manager_core.get_config().get('verbose', False)
while remained:
# Add comfyui_version and form_factor to the API request
sub_uri = f'{base_url}/nodes?page={page}&limit=30&comfyui_version={comfyui_ver}&form_factor={form_factor}'
@ -77,13 +82,13 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
for x in sub_json_obj['nodes']:
full_nodes[x['id']] = x
if page % 5 == 0:
print(f"FETCH ComfyRegistry Data: {page}/{sub_json_obj['totalPages']}")
if page % 5 == 0 and verbose:
logging.info(f"FETCH ComfyRegistry Data: {page}/{sub_json_obj['totalPages']}")
page += 1
time.sleep(0.5)
print("FETCH ComfyRegistry Data [DONE]")
logging.info("FETCH ComfyRegistry Data [DONE]")
for v in full_nodes.values():
if 'latest_version' not in v:
@ -99,7 +104,7 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
if cache_state == 'not-cached':
return {}
else:
print("[ComfyUI-Manager] The ComfyRegistry cache update is still in progress, so an outdated cache is being used.")
logging.info("[ComfyUI-Manager] The ComfyRegistry cache update is still in progress, so an outdated cache is being used.")
with open(manager_util.get_cache_path(uri), 'r', encoding="UTF-8", errors="ignore") as json_file:
return json.load(json_file)['nodes']
@ -111,9 +116,9 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
json_obj = await fetch_all()
manager_util.save_to_cache(uri, json_obj)
return json_obj['nodes']
except:
except Exception:
res = {}
print("Cannot connect to comfyregistry.")
logging.warning("Cannot connect to comfyregistry.")
finally:
if cache_mode:
is_cache_loading = False
@ -210,6 +215,7 @@ def read_cnr_info(fullpath):
project = data.get('project', {})
name = project.get('name').strip().lower()
original_name = project.get('name')
# normalize version
# for example: 2.5 -> 2.5.0
@ -221,6 +227,7 @@ def read_cnr_info(fullpath):
if name and version: # repository is optional
return {
"id": name,
"original_name": original_name,
"version": version,
"url": repository
}
@ -236,8 +243,8 @@ def generate_cnr_id(fullpath, cnr_id):
if not os.path.exists(cnr_id_path):
with open(cnr_id_path, "w") as f:
return f.write(cnr_id)
except:
print(f"[ComfyUI Manager] unable to create file: {cnr_id_path}")
except Exception:
logging.error(f"[ComfyUI Manager] unable to create file: {cnr_id_path}")
def read_cnr_id(fullpath):
@ -246,7 +253,7 @@ def read_cnr_id(fullpath):
if os.path.exists(cnr_id_path):
with open(cnr_id_path) as f:
return f.read().strip()
except:
except Exception:
pass
return None

View File

@ -0,0 +1,105 @@
import sys
import os
import logging
from . import manager_util
import toml
import git
# read env vars
comfy_path: str = os.environ.get('COMFYUI_PATH')
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
if comfy_path is None:
try:
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
os.environ['COMFYUI_PATH'] = comfy_path
except Exception:
logging.error("[ComfyUI-Manager] environment variable 'COMFYUI_PATH' is not specified.")
exit(-1)
if comfy_base_path is None:
comfy_base_path = comfy_path
channel_list_template_path = os.path.join(manager_util.comfyui_manager_path, 'channels.list.template')
git_script_path = os.path.join(manager_util.comfyui_manager_path, "common", "git_helper.py")
manager_files_path = None
manager_config_path = None
manager_channel_list_path = None
manager_startup_script_path:str = None
manager_snapshot_path = None
manager_pip_overrides_path = None
manager_pip_blacklist_path = None
manager_batch_history_path = None
def update_user_directory(manager_dir):
global manager_files_path
global manager_config_path
global manager_channel_list_path
global manager_startup_script_path
global manager_snapshot_path
global manager_pip_overrides_path
global manager_pip_blacklist_path
global manager_batch_history_path
manager_files_path = manager_dir
if not os.path.exists(manager_files_path):
os.makedirs(manager_files_path)
manager_snapshot_path = os.path.join(manager_files_path, "snapshots")
if not os.path.exists(manager_snapshot_path):
os.makedirs(manager_snapshot_path)
manager_startup_script_path = os.path.join(manager_files_path, "startup-scripts")
if not os.path.exists(manager_startup_script_path):
os.makedirs(manager_startup_script_path)
manager_config_path = os.path.join(manager_files_path, 'config.ini')
manager_channel_list_path = os.path.join(manager_files_path, 'channels.list')
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list")
manager_util.cache_dir = os.path.join(manager_files_path, "cache")
manager_batch_history_path = os.path.join(manager_files_path, "batch_history")
if not os.path.exists(manager_util.cache_dir):
os.makedirs(manager_util.cache_dir)
if not os.path.exists(manager_batch_history_path):
os.makedirs(manager_batch_history_path)
try:
import folder_paths
update_user_directory(folder_paths.get_system_user_directory("manager"))
except Exception:
# fallback:
# This case is only possible when running with cm-cli, and in practice, this case is not actually used.
update_user_directory(os.path.abspath(manager_util.comfyui_manager_path))
def get_current_comfyui_ver():
"""
Extract version from pyproject.toml
"""
toml_path = os.path.join(comfy_path, 'pyproject.toml')
if not os.path.exists(toml_path):
return None
else:
try:
with open(toml_path, "r", encoding="utf-8") as f:
data = toml.load(f)
project = data.get('project', {})
return project.get('version')
except Exception:
return None
def get_comfyui_tag():
try:
with git.Repo(comfy_path) as repo:
return repo.git.describe('--tags')
except Exception:
return None

View File

@ -0,0 +1,18 @@
import enum
class NetworkMode(enum.Enum):
PUBLIC = "public"
PRIVATE = "private"
OFFLINE = "offline"
PERSONAL_CLOUD = "personal_cloud"
class SecurityLevel(enum.Enum):
STRONG = "strong"
NORMAL = "normal"
NORMAL_MINUS = "normal-minus"
WEAK = "weak"
class DBMode(enum.Enum):
LOCAL = "local"
CACHE = "cache"
REMOTE = "remote"

View File

@ -9,15 +9,19 @@ import yaml
import requests
from tqdm.auto import tqdm
from git.remote import RemoteProgress
from comfyui_manager.common.timestamp_utils import get_backup_branch_name
comfy_path = os.environ.get('COMFYUI_PATH')
git_exe_path = os.environ.get('GIT_EXE_PATH')
if comfy_path is None:
print("\nWARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.", file=sys.stderr)
comfy_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
print("git_helper: environment variable 'COMFYUI_PATH' is not specified.")
exit(-1)
if not os.path.exists(os.path.join(comfy_path, 'folder_paths.py')):
print("git_helper: '{comfy_path}' is not a valid 'COMFYUI_PATH' location.")
exit(-1)
def download_url(url, dest_folder, filename=None):
# Ensure the destination folder exists
@ -153,27 +157,27 @@ def switch_to_default_branch(repo):
default_branch = repo.git.symbolic_ref(f'refs/remotes/{remote_name}/HEAD').replace(f'refs/remotes/{remote_name}/', '')
repo.git.checkout(default_branch)
return True
except:
except Exception:
# try checkout master
# try checkout main if failed
try:
repo.git.checkout(repo.heads.master)
return True
except:
except Exception:
try:
if remote_name is not None:
repo.git.checkout('-b', 'master', f'{remote_name}/master')
return True
except:
except Exception:
try:
repo.git.checkout(repo.heads.main)
return True
except:
except Exception:
try:
if remote_name is not None:
repo.git.checkout('-b', 'main', f'{remote_name}/main')
return True
except:
except Exception:
pass
print("[ComfyUI Manager] Failed to switch to the default branch")
@ -219,7 +223,14 @@ def gitpull(path):
repo.close()
return
remote.pull()
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
backup_name = get_backup_branch_name(repo)
repo.create_head(backup_name)
print(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
print(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
repo.git.submodule('update', '--init', '--recursive')
new_commit_hash = repo.head.commit.hexsha
@ -444,7 +455,7 @@ def restore_pip_snapshot(pips, options):
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + non_url)
except:
except Exception:
pass
# fallback
@ -453,7 +464,7 @@ def restore_pip_snapshot(pips, options):
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except:
except Exception:
pass
if res != 0:
@ -464,7 +475,7 @@ def restore_pip_snapshot(pips, options):
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except:
except Exception:
pass
if res != 0:
@ -475,7 +486,7 @@ def restore_pip_snapshot(pips, options):
res = 1
try:
res = subprocess.check_call([sys.executable, '-m', 'pip', 'install', x])
except:
except Exception:
pass
if res != 0:

View File

@ -74,6 +74,12 @@ def normalize_to_github_id(url) -> str:
return f"{author}/{repo_name}"
# Handle short format like "author/repo" (aux_id format)
if '/' in url and not url.startswith('http'):
parts = url.split('/')
if len(parts) == 2 and parts[0] and parts[1]:
return url
return None

View File

@ -0,0 +1,36 @@
from enum import Enum
is_personal_cloud_mode = False
handler_policy = {}
class HANDLER_POLICY(Enum):
MULTIPLE_REMOTE_BAN_NON_LOCAL = 1
MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD = 2
BANNED = 3
def is_loopback(address):
import ipaddress
try:
return ipaddress.ip_address(address).is_loopback
except ValueError:
return False
def do_nothing():
pass
def get_handler_policy(x):
return handler_policy.get(x) or set()
def add_handler_policy(x, policy):
s = handler_policy.get(x)
if s is None:
s = set()
handler_policy[x] = s
s.add(policy)
multiple_remote_alert = do_nothing

View File

@ -8,17 +8,18 @@ import aiohttp
import json
import threading
import os
from datetime import datetime
import subprocess
import sys
import re
import logging
import platform
import shlex
import time
from functools import lru_cache
cache_lock = threading.Lock()
session_lock = threading.Lock()
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also updated together in **manager_core.update_user_directory**.
@ -26,6 +27,9 @@ cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also up
use_uv = False
bypass_ssl = False
def is_manager_pip_package():
return not os.path.exists(os.path.join(comfyui_manager_path, '..', 'custom_nodes'))
def add_python_path_to_env():
if platform.system() != "Windows":
sep = ':'
@ -97,7 +101,7 @@ def make_pip_cmd(cmd):
# DON'T USE StrictVersion - cannot handle pre_release version
# try:
# from distutils.version import StrictVersion
# except:
# except Exception:
# print(f"[ComfyUI-Manager] 'distutils' package not found. Activating fallback mode for compatibility.")
class StrictVersion:
def __init__(self, version_string):
@ -172,7 +176,7 @@ def is_file_created_within_one_day(file_path):
return False
file_creation_time = os.path.getctime(file_path)
current_time = datetime.now().timestamp()
current_time = time.time()
time_difference = current_time - file_creation_time
return time_difference <= 86400
@ -552,7 +556,7 @@ def robust_readlines(fullpath):
try:
with open(fullpath, "r") as f:
return f.readlines()
except:
except Exception:
encoding = None
with open(fullpath, "rb") as f:
raw_data = f.read()

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
import os
from git_utils import get_commit_hash
from .git_utils import get_commit_hash
@dataclass

View File

@ -2,7 +2,7 @@ import sys
import subprocess
import os
import manager_util
from . import manager_util
def security_check():

View File

@ -0,0 +1,136 @@
"""
Robust timestamp utilities with datetime fallback.
Some environments (especially Mac) have issues with the datetime module
due to local file name conflicts or Homebrew Python module path issues.
"""
import logging
import time as time_module
import uuid
_datetime_available = None
_dt_datetime = None
def _init_datetime():
"""Initialize datetime availability check (lazy, once)."""
global _datetime_available, _dt_datetime
if _datetime_available is not None:
return
try:
import datetime as dt
if hasattr(dt, 'datetime'):
from datetime import datetime as dt_datetime
_dt_datetime = dt_datetime
_datetime_available = True
return
except Exception as e:
logging.debug(f"[ComfyUI-Manager] datetime import failed: {e}")
_datetime_available = False
logging.warning("[ComfyUI-Manager] datetime unavailable, using time module fallback")
def current_timestamp() -> str:
"""
Get current timestamp for logging.
Format: YYYY-MM-DD HH:MM:SS.mmm (or Unix timestamp if fallback)
"""
_init_datetime()
if _datetime_available:
return _dt_datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
return str(time_module.time()).split('.')[0]
def get_timestamp_for_filename() -> str:
"""
Get timestamp suitable for filenames.
Format: YYYYMMDD_HHMMSS
"""
_init_datetime()
if _datetime_available:
return _dt_datetime.now().strftime('%Y%m%d_%H%M%S')
return time_module.strftime('%Y%m%d_%H%M%S')
def get_timestamp_for_path() -> str:
"""
Get timestamp for path/directory names.
Format: YYYY-MM-DD_HH-MM-SS
"""
_init_datetime()
if _datetime_available:
return _dt_datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
return time_module.strftime('%Y-%m-%d_%H-%M-%S')
def get_backup_branch_name(repo=None) -> str:
"""
Get backup branch name with current timestamp.
Format: backup_YYYYMMDD_HHMMSS (or backup_YYYYMMDD_HHMMSS_N if exists)
Args:
repo: Optional git.Repo object. If provided, checks for name collisions
and adds sequential suffix if needed.
Returns:
Unique backup branch name.
"""
base_name = f'backup_{get_timestamp_for_filename()}'
if repo is None:
return base_name
# Check if branch exists
try:
existing_branches = {b.name for b in repo.heads}
except Exception:
return base_name
if base_name not in existing_branches:
return base_name
# Add sequential suffix
for i in range(1, 100):
new_name = f'{base_name}_{i}'
if new_name not in existing_branches:
return new_name
# Ultimate fallback: use UUID (very unlikely to reach here)
return f'{base_name}_{uuid.uuid4().hex[:6]}'
def get_now():
"""
Get current datetime object.
Returns datetime.now() if available, otherwise a FakeDatetime object
that supports basic operations (timestamp(), strftime()).
"""
_init_datetime()
if _datetime_available:
return _dt_datetime.now()
# Fallback: return object with basic datetime-like interface
t = time_module.localtime()
class FakeDatetime:
def timestamp(self):
return time_module.time()
def strftime(self, fmt):
return time_module.strftime(fmt, t)
def isoformat(self):
return time_module.strftime('%Y-%m-%dT%H:%M:%S', t)
return FakeDatetime()
def get_unix_timestamp() -> float:
"""Get current Unix timestamp."""
_init_datetime()
if _datetime_available:
return _dt_datetime.now().timestamp()
return time_module.time()

View File

@ -0,0 +1,68 @@
# Data Models
This directory contains Pydantic models for ComfyUI Manager, providing type safety, validation, and serialization for the API and internal data structures.
## Overview
- `generated_models.py` - All models auto-generated from OpenAPI spec
- `__init__.py` - Package exports for all models
**Note**: All models are now auto-generated from the OpenAPI specification. Manual model files (`task_queue.py`, `state_management.py`) have been deprecated in favor of a single source of truth.
## Generating Types from OpenAPI
The state management models are automatically generated from the OpenAPI specification using `datamodel-codegen`. This ensures type safety and consistency between the API specification and the Python code.
### Prerequisites
Install the code generator:
```bash
pipx install datamodel-code-generator
```
### Generation Command
To regenerate all models after updating the OpenAPI spec:
```bash
datamodel-codegen \
--use-subclass-enum \
--field-constraints \
--strict-types bytes \
--use-double-quotes \
--input openapi.yaml \
--output comfyui_manager/data_models/generated_models.py \
--output-model-type pydantic_v2.BaseModel
```
### When to Regenerate
You should regenerate the models when:
1. **Adding new API endpoints** that return new data structures
2. **Modifying existing schemas** in the OpenAPI specification
3. **Adding new state management features** that require new models
### Important Notes
- **Single source of truth**: All models are now generated from `openapi.yaml`
- **No manual models**: All previously manual models have been migrated to the OpenAPI spec
- **OpenAPI requirements**: New schemas must be referenced in API paths to be generated by datamodel-codegen
- **Validation**: Always validate the OpenAPI spec before generation:
```bash
python3 -c "import yaml; yaml.safe_load(open('openapi.yaml'))"
```
### Example: Adding New State Models
1. Add your schema to `openapi.yaml` under `components/schemas/`
2. Reference the schema in an API endpoint response
3. Run the generation command above
4. Update `__init__.py` to export the new models
5. Import and use the models in your code
### Troubleshooting
- **Models not generated**: Ensure schemas are under `components/schemas/` (not `parameters/`)
- **Missing models**: Verify schemas are referenced in at least one API path
- **Import errors**: Check that new models are added to `__init__.py` exports

View File

@ -0,0 +1,137 @@
"""
Data models for ComfyUI Manager.
This package contains Pydantic models used throughout the ComfyUI Manager
for data validation, serialization, and type safety.
All models are auto-generated from the OpenAPI specification to ensure
consistency between the API and implementation.
"""
from .generated_models import (
# Core Task Queue Models
QueueTaskItem,
TaskHistoryItem,
TaskStateMessage,
TaskExecutionStatus,
# WebSocket Message Models
MessageTaskDone,
MessageTaskStarted,
MessageTaskFailed,
MessageUpdate,
ManagerMessageName,
# State Management Models
BatchExecutionRecord,
ComfyUISystemState,
BatchOperation,
InstalledNodeInfo,
InstalledModelInfo,
ComfyUIVersionInfo,
# Import Fail Info Models
ImportFailInfoBulkRequest,
ImportFailInfoBulkResponse,
ImportFailInfoItem,
ImportFailInfoItem1,
# Other models
OperationType,
OperationResult,
ManagerPackInfo,
ManagerPackInstalled,
SelectedVersion,
ManagerChannel,
ManagerDatabaseSource,
ManagerPackState,
ManagerPackInstallType,
ManagerPack,
InstallPackParams,
UpdatePackParams,
UpdateAllPacksParams,
UpdateComfyUIParams,
FixPackParams,
UninstallPackParams,
DisablePackParams,
EnablePackParams,
UpdateAllQueryParams,
UpdateComfyUIQueryParams,
ComfyUISwitchVersionQueryParams,
QueueStatus,
ManagerMappings,
ModelMetadata,
NodePackageMetadata,
SnapshotItem,
Error,
InstalledPacksResponse,
HistoryResponse,
HistoryListResponse,
InstallType,
SecurityLevel,
RiskLevel,
)
__all__ = [
# Core Task Queue Models
"QueueTaskItem",
"TaskHistoryItem",
"TaskStateMessage",
"TaskExecutionStatus",
# WebSocket Message Models
"MessageTaskDone",
"MessageTaskStarted",
"MessageTaskFailed",
"MessageUpdate",
"ManagerMessageName",
# State Management Models
"BatchExecutionRecord",
"ComfyUISystemState",
"BatchOperation",
"InstalledNodeInfo",
"InstalledModelInfo",
"ComfyUIVersionInfo",
# Import Fail Info Models
"ImportFailInfoBulkRequest",
"ImportFailInfoBulkResponse",
"ImportFailInfoItem",
"ImportFailInfoItem1",
# Other models
"OperationType",
"OperationResult",
"ManagerPackInfo",
"ManagerPackInstalled",
"SelectedVersion",
"ManagerChannel",
"ManagerDatabaseSource",
"ManagerPackState",
"ManagerPackInstallType",
"ManagerPack",
"InstallPackParams",
"UpdatePackParams",
"UpdateAllPacksParams",
"UpdateComfyUIParams",
"FixPackParams",
"UninstallPackParams",
"DisablePackParams",
"EnablePackParams",
"UpdateAllQueryParams",
"UpdateComfyUIQueryParams",
"ComfyUISwitchVersionQueryParams",
"QueueStatus",
"ManagerMappings",
"ModelMetadata",
"NodePackageMetadata",
"SnapshotItem",
"Error",
"InstalledPacksResponse",
"HistoryResponse",
"HistoryListResponse",
"InstallType",
"SecurityLevel",
"RiskLevel",
]

View File

@ -0,0 +1,561 @@
# generated by datamodel-codegen:
# filename: openapi.yaml
# timestamp: 2025-07-31T04:52:26+00:00
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Field, RootModel
class OperationType(str, Enum):
install = "install"
uninstall = "uninstall"
update = "update"
update_comfyui = "update-comfyui"
fix = "fix"
disable = "disable"
enable = "enable"
install_model = "install-model"
class OperationResult(str, Enum):
success = "success"
failed = "failed"
skipped = "skipped"
error = "error"
skip = "skip"
class TaskExecutionStatus(BaseModel):
status_str: OperationResult
completed: bool = Field(..., description="Whether the task completed")
messages: List[str] = Field(..., description="Additional status messages")
class ManagerMessageName(str, Enum):
cm_task_completed = "cm-task-completed"
cm_task_started = "cm-task-started"
cm_queue_status = "cm-queue-status"
class ManagerPackInfo(BaseModel):
id: str = Field(
...,
description="Either github-author/github-repo or name of pack from the registry",
)
version: str = Field(..., description="Semantic version or Git commit hash")
ui_id: Optional[str] = Field(None, description="Task ID - generated internally")
class ManagerPackInstalled(BaseModel):
ver: str = Field(
...,
description="The version of the pack that is installed (Git commit hash or semantic version)",
)
cnr_id: Optional[str] = Field(
None, description="The name of the pack if installed from the registry"
)
aux_id: Optional[str] = Field(
None,
description="The name of the pack if installed from github (author/repo-name format)",
)
enabled: bool = Field(..., description="Whether the pack is enabled")
class SelectedVersion(str, Enum):
latest = "latest"
nightly = "nightly"
class ManagerChannel(str, Enum):
default = "default"
recent = "recent"
legacy = "legacy"
forked = "forked"
dev = "dev"
tutorial = "tutorial"
class ManagerDatabaseSource(str, Enum):
remote = "remote"
local = "local"
cache = "cache"
class ManagerPackState(str, Enum):
installed = "installed"
disabled = "disabled"
not_installed = "not_installed"
import_failed = "import_failed"
needs_update = "needs_update"
class ManagerPackInstallType(str, Enum):
git_clone = "git-clone"
copy = "copy"
cnr = "cnr"
class SecurityLevel(str, Enum):
strong = "strong"
normal = "normal"
normal_ = "normal-"
weak = "weak"
class RiskLevel(str, Enum):
block = "block"
high_ = "high+"
high = "high"
middle_ = "middle+"
middle = "middle"
class UpdateState(Enum):
false = "false"
true = "true"
class ManagerPack(ManagerPackInfo):
author: Optional[str] = Field(
None, description="Pack author name or 'Unclaimed' if added via GitHub crawl"
)
files: Optional[List[str]] = Field(
None,
description="Repository URLs for installation (typically contains one GitHub URL)",
)
reference: Optional[str] = Field(
None, description="The type of installation reference"
)
title: Optional[str] = Field(None, description="The display name of the pack")
cnr_latest: Optional[SelectedVersion] = None
repository: Optional[str] = Field(None, description="GitHub repository URL")
state: Optional[ManagerPackState] = None
update_state: Optional[UpdateState] = Field(
None, alias="update-state", description="Update availability status"
)
stars: Optional[int] = Field(None, description="GitHub stars count")
last_update: Optional[datetime] = Field(None, description="Last update timestamp")
health: Optional[str] = Field(None, description="Health status of the pack")
description: Optional[str] = Field(None, description="Pack description")
trust: Optional[bool] = Field(None, description="Whether the pack is trusted")
install_type: Optional[ManagerPackInstallType] = None
class InstallPackParams(ManagerPackInfo):
selected_version: Union[str, SelectedVersion] = Field(
..., description="Semantic version, Git commit hash, latest, or nightly"
)
repository: Optional[str] = Field(
None,
description="GitHub repository URL (required if selected_version is nightly)",
)
pip: Optional[List[str]] = Field(None, description="PyPi dependency names")
mode: ManagerDatabaseSource
channel: ManagerChannel
skip_post_install: Optional[bool] = Field(
None, description="Whether to skip post-installation steps"
)
class UpdateAllPacksParams(BaseModel):
mode: Optional[ManagerDatabaseSource] = None
ui_id: Optional[str] = Field(None, description="Task ID - generated internally")
class UpdatePackParams(BaseModel):
node_name: str = Field(..., description="Name of the node package to update")
node_ver: Optional[str] = Field(
None, description="Current version of the node package"
)
class UpdateComfyUIParams(BaseModel):
is_stable: Optional[bool] = Field(
True,
description="Whether to update to stable version (true) or nightly (false)",
)
target_version: Optional[str] = Field(
None,
description="Specific version to switch to (for version switching operations)",
)
class FixPackParams(BaseModel):
node_name: str = Field(..., description="Name of the node package to fix")
node_ver: str = Field(..., description="Version of the node package")
class UninstallPackParams(BaseModel):
node_name: str = Field(..., description="Name of the node package to uninstall")
is_unknown: Optional[bool] = Field(
False, description="Whether this is an unknown/unregistered package"
)
class DisablePackParams(BaseModel):
node_name: str = Field(..., description="Name of the node package to disable")
is_unknown: Optional[bool] = Field(
False, description="Whether this is an unknown/unregistered package"
)
class EnablePackParams(BaseModel):
cnr_id: str = Field(
..., description="ComfyUI Node Registry ID of the package to enable"
)
class UpdateAllQueryParams(BaseModel):
client_id: str = Field(
..., description="Client identifier that initiated the request"
)
ui_id: str = Field(..., description="Base UI identifier for task tracking")
mode: Optional[ManagerDatabaseSource] = None
class UpdateComfyUIQueryParams(BaseModel):
client_id: str = Field(
..., description="Client identifier that initiated the request"
)
ui_id: str = Field(..., description="UI identifier for task tracking")
stable: Optional[bool] = Field(
True,
description="Whether to update to stable version (true) or nightly (false)",
)
class ComfyUISwitchVersionQueryParams(BaseModel):
ver: str = Field(..., description="Version to switch to")
client_id: str = Field(
..., description="Client identifier that initiated the request"
)
ui_id: str = Field(..., description="UI identifier for task tracking")
class QueueStatus(BaseModel):
total_count: int = Field(
..., description="Total number of tasks (pending + running)"
)
done_count: int = Field(..., description="Number of completed tasks")
in_progress_count: int = Field(..., description="Number of tasks currently running")
pending_count: Optional[int] = Field(
None, description="Number of tasks waiting to be executed"
)
is_processing: bool = Field(..., description="Whether the task worker is active")
client_id: Optional[str] = Field(
None, description="Client ID (when filtered by client)"
)
class ManagerMappings1(BaseModel):
title_aux: Optional[str] = Field(None, description="The display name of the pack")
class ManagerMappings(
RootModel[Optional[Dict[str, List[Union[List[str], ManagerMappings1]]]]]
):
root: Optional[Dict[str, List[Union[List[str], ManagerMappings1]]]] = Field(
None, description="Tuple of [node_names, metadata]"
)
class ModelMetadata(BaseModel):
name: str = Field(..., description="Name of the model")
type: str = Field(..., description="Type of model")
base: Optional[str] = Field(None, description="Base model type")
save_path: Optional[str] = Field(None, description="Path for saving the model")
url: str = Field(..., description="Download URL")
filename: str = Field(..., description="Target filename")
ui_id: Optional[str] = Field(None, description="ID for UI reference")
class InstallType(str, Enum):
git = "git"
copy = "copy"
pip = "pip"
class NodePackageMetadata(BaseModel):
title: Optional[str] = Field(None, description="Display name of the node package")
name: Optional[str] = Field(None, description="Repository/package name")
files: Optional[List[str]] = Field(None, description="Source URLs for the package")
description: Optional[str] = Field(
None, description="Description of the node package functionality"
)
install_type: Optional[InstallType] = Field(None, description="Installation method")
version: Optional[str] = Field(None, description="Version identifier")
id: Optional[str] = Field(
None, description="Unique identifier for the node package"
)
ui_id: Optional[str] = Field(None, description="ID for UI reference")
channel: Optional[str] = Field(None, description="Source channel")
mode: Optional[str] = Field(None, description="Source mode")
class SnapshotItem(RootModel[str]):
root: str = Field(..., description="Name of the snapshot")
class Error(BaseModel):
error: str = Field(..., description="Error message")
class InstalledPacksResponse(RootModel[Optional[Dict[str, ManagerPackInstalled]]]):
root: Optional[Dict[str, ManagerPackInstalled]] = None
class HistoryListResponse(BaseModel):
ids: Optional[List[str]] = Field(
None, description="List of available batch history IDs"
)
class InstalledNodeInfo(BaseModel):
name: str = Field(..., description="Node package name")
version: str = Field(..., description="Installed version")
repository_url: Optional[str] = Field(None, description="Git repository URL")
install_method: str = Field(
..., description="Installation method (cnr, git, pip, etc.)"
)
enabled: Optional[bool] = Field(
True, description="Whether the node is currently enabled"
)
install_date: Optional[datetime] = Field(
None, description="ISO timestamp of installation"
)
class InstalledModelInfo(BaseModel):
name: str = Field(..., description="Model filename")
path: str = Field(..., description="Full path to model file")
type: str = Field(..., description="Model type (checkpoint, lora, vae, etc.)")
size_bytes: Optional[int] = Field(None, description="File size in bytes", ge=0)
hash: Optional[str] = Field(None, description="Model file hash for verification")
install_date: Optional[datetime] = Field(
None, description="ISO timestamp when added"
)
class ComfyUIVersionInfo(BaseModel):
version: str = Field(..., description="ComfyUI version string")
commit_hash: Optional[str] = Field(None, description="Git commit hash")
branch: Optional[str] = Field(None, description="Git branch name")
is_stable: Optional[bool] = Field(
False, description="Whether this is a stable release"
)
last_updated: Optional[datetime] = Field(
None, description="ISO timestamp of last update"
)
class BatchOperation(BaseModel):
operation_id: str = Field(..., description="Unique operation identifier")
operation_type: OperationType
target: str = Field(
..., description="Target of the operation (node name, model name, etc.)"
)
target_version: Optional[str] = Field(
None, description="Target version for the operation"
)
result: OperationResult
error_message: Optional[str] = Field(
None, description="Error message if operation failed"
)
start_time: datetime = Field(
..., description="ISO timestamp when operation started"
)
end_time: Optional[datetime] = Field(
None, description="ISO timestamp when operation completed"
)
client_id: Optional[str] = Field(
None, description="Client that initiated the operation"
)
class ComfyUISystemState(BaseModel):
snapshot_time: datetime = Field(
..., description="ISO timestamp when snapshot was taken"
)
comfyui_version: ComfyUIVersionInfo
frontend_version: Optional[str] = Field(
None, description="ComfyUI frontend version if available"
)
python_version: str = Field(..., description="Python interpreter version")
platform_info: str = Field(
..., description="Operating system and platform information"
)
installed_nodes: Optional[Dict[str, InstalledNodeInfo]] = Field(
None, description="Map of installed node packages by name"
)
installed_models: Optional[Dict[str, InstalledModelInfo]] = Field(
None, description="Map of installed models by name"
)
manager_config: Optional[Dict[str, Any]] = Field(
None, description="ComfyUI Manager configuration settings"
)
comfyui_root_path: Optional[str] = Field(
None, description="ComfyUI root installation directory"
)
model_paths: Optional[Dict[str, List[str]]] = Field(
None, description="Map of model types to their configured paths"
)
manager_version: Optional[str] = Field(None, description="ComfyUI Manager version")
security_level: Optional[SecurityLevel] = None
network_mode: Optional[str] = Field(
None, description="Network mode (online, offline, private)"
)
cli_args: Optional[Dict[str, Any]] = Field(
None, description="Selected ComfyUI CLI arguments"
)
custom_nodes_count: Optional[int] = Field(
None, description="Total number of custom node packages", ge=0
)
failed_imports: Optional[List[str]] = Field(
None, description="List of custom nodes that failed to import"
)
pip_packages: Optional[Dict[str, str]] = Field(
None, description="Map of installed pip packages to their versions"
)
embedded_python: Optional[bool] = Field(
None,
description="Whether ComfyUI is running from an embedded Python distribution",
)
class BatchExecutionRecord(BaseModel):
batch_id: str = Field(..., description="Unique batch identifier")
start_time: datetime = Field(..., description="ISO timestamp when batch started")
end_time: Optional[datetime] = Field(
None, description="ISO timestamp when batch completed"
)
state_before: ComfyUISystemState
state_after: Optional[ComfyUISystemState] = Field(
None, description="System state after batch execution"
)
operations: Optional[List[BatchOperation]] = Field(
None, description="List of operations performed in this batch"
)
total_operations: Optional[int] = Field(
0, description="Total number of operations in batch", ge=0
)
successful_operations: Optional[int] = Field(
0, description="Number of successful operations", ge=0
)
failed_operations: Optional[int] = Field(
0, description="Number of failed operations", ge=0
)
skipped_operations: Optional[int] = Field(
0, description="Number of skipped operations", ge=0
)
class ImportFailInfoBulkRequest(BaseModel):
cnr_ids: Optional[List[str]] = Field(
None, description="A list of CNR IDs to check."
)
urls: Optional[List[str]] = Field(
None, description="A list of repository URLs to check."
)
class ImportFailInfoItem1(BaseModel):
error: Optional[str] = None
traceback: Optional[str] = None
class ImportFailInfoItem(RootModel[Optional[ImportFailInfoItem1]]):
root: Optional[ImportFailInfoItem1]
class QueueTaskItem(BaseModel):
ui_id: str = Field(..., description="Unique identifier for the task")
client_id: str = Field(..., description="Client identifier that initiated the task")
kind: OperationType
params: Union[
InstallPackParams,
UpdatePackParams,
UpdateAllPacksParams,
UpdateComfyUIParams,
FixPackParams,
UninstallPackParams,
DisablePackParams,
EnablePackParams,
ModelMetadata,
]
class TaskHistoryItem(BaseModel):
ui_id: str = Field(..., description="Unique identifier for the task")
client_id: str = Field(..., description="Client identifier that initiated the task")
kind: str = Field(..., description="Type of task that was performed")
timestamp: datetime = Field(..., description="ISO timestamp when task completed")
result: str = Field(..., description="Task result message or details")
status: Optional[TaskExecutionStatus] = None
batch_id: Optional[str] = Field(
None, description="ID of the batch this task belongs to"
)
end_time: Optional[datetime] = Field(
None, description="ISO timestamp when task execution ended"
)
class TaskStateMessage(BaseModel):
history: Dict[str, TaskHistoryItem] = Field(
..., description="Map of task IDs to their history items"
)
running_queue: List[QueueTaskItem] = Field(
..., description="Currently executing tasks"
)
pending_queue: List[QueueTaskItem] = Field(
..., description="Tasks waiting to be executed"
)
installed_packs: Dict[str, ManagerPackInstalled] = Field(
..., description="Map of currently installed node packages by name"
)
class MessageTaskDone(BaseModel):
ui_id: str = Field(..., description="Task identifier")
result: str = Field(..., description="Task result message")
kind: str = Field(..., description="Type of task")
status: Optional[TaskExecutionStatus] = None
timestamp: datetime = Field(..., description="ISO timestamp when task completed")
state: TaskStateMessage
class MessageTaskStarted(BaseModel):
ui_id: str = Field(..., description="Task identifier")
kind: str = Field(..., description="Type of task")
timestamp: datetime = Field(..., description="ISO timestamp when task started")
state: TaskStateMessage
class MessageTaskFailed(BaseModel):
ui_id: str = Field(..., description="Task identifier")
error: str = Field(..., description="Error message")
kind: str = Field(..., description="Type of task")
timestamp: datetime = Field(..., description="ISO timestamp when task failed")
state: TaskStateMessage
class MessageUpdate(
RootModel[Union[MessageTaskDone, MessageTaskStarted, MessageTaskFailed]]
):
root: Union[MessageTaskDone, MessageTaskStarted, MessageTaskFailed] = Field(
..., description="Union type for all possible WebSocket message updates"
)
class HistoryResponse(BaseModel):
history: Optional[Dict[str, TaskHistoryItem]] = Field(
None, description="Map of task IDs to their history items"
)
class ImportFailInfoBulkResponse(RootModel[Optional[Dict[str, ImportFailInfoItem]]]):
root: Optional[Dict[str, ImportFailInfoItem]] = None

View File

@ -0,0 +1,11 @@
- Anytime you make a change to the data being sent or received, you should follow this process:
1. Adjust the openapi.yaml file first
2. Verify the syntax of the openapi.yaml file using `yaml.safe_load`
3. Regenerate the types following the instructions in the `data_models/README.md` file
4. Verify the new data model is generated
5. Verify the syntax of the generated types files
6. Run formatting and linting on the generated types files
7. Adjust the `__init__.py` files in the `data_models` directory to match/export the new data model
8. Only then, make the changes to the rest of the codebase
9. Run the CI tests to verify that the changes are working
- The comfyui_manager is a python package that is used to manage the comfyui server. There are two sub-packages `glob` and `legacy`. These represent the current version (`glob`) and the previous version (`legacy`), not including common utilities and data models. When developing, we work in the `glob` package. You can ignore the `legacy` package entirely, unless you have a very good reason to research how things were done in the legacy or prior major versions of the package. But in those cases, you should just look for the sake of knowledge or reflection, not for changing code (unless explicitly asked to do so).

View File

View File

@ -0,0 +1,55 @@
SECURITY_MESSAGE_MIDDLE = "ERROR: To use this action, a security_level of `normal or below` is required. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_MIDDLE_P = "ERROR: To use this action, security_level must be `normal or below`, and network_mode must be set to `personal_cloud`. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower."
def is_loopback(address):
import ipaddress
try:
return ipaddress.ip_address(address).is_loopback
except ValueError:
return False
model_dir_name_map = {
"checkpoints": "checkpoints",
"checkpoint": "checkpoints",
"unclip": "checkpoints",
"text_encoders": "text_encoders",
"clip": "text_encoders",
"vae": "vae",
"lora": "loras",
"t2i-adapter": "controlnet",
"t2i-style": "controlnet",
"controlnet": "controlnet",
"clip_vision": "clip_vision",
"gligen": "gligen",
"upscale": "upscale_models",
"embedding": "embeddings",
"embeddings": "embeddings",
"unet": "diffusion_models",
"diffusion_model": "diffusion_models",
}
# List of all model directory names used for checking installed models
MODEL_DIR_NAMES = [
"checkpoints",
"loras",
"vae",
"text_encoders",
"diffusion_models",
"clip_vision",
"embeddings",
"diffusers",
"vae_approx",
"controlnet",
"gligen",
"upscale_models",
"hypernetworks",
"photomaker",
"classifiers",
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
import mimetypes
import manager_core as core
from ..common import context
from . import manager_core as core
import os
from aiohttp import web
import aiohttp
@ -8,6 +10,16 @@ import hashlib
import folder_paths
from server import PromptServer
import logging
import sys
try:
from nio import AsyncClient, LoginResponse, UploadResponse
matrix_nio_is_available = True
except Exception:
logging.warning(f"[ComfyUI-Manager] The matrix sharing feature has been disabled because the `matrix-nio` dependency is not installed.\n\tTo use this feature, please run the following command:\n\t{sys.executable} -m pip install matrix-nio\n")
matrix_nio_is_available = False
def extract_model_file_names(json_data):
@ -53,7 +65,7 @@ def compute_sha256_checksum(filepath):
return sha256.hexdigest()
@PromptServer.instance.routes.get("/manager/share_option")
@PromptServer.instance.routes.get("/v2/manager/share_option")
async def share_option(request):
if "value" in request.rel_url.query:
core.get_config()['share_option'] = request.rel_url.query['value']
@ -65,21 +77,21 @@ async def share_option(request):
def get_openart_auth():
if not os.path.exists(os.path.join(core.manager_files_path, ".openart_key")):
if not os.path.exists(os.path.join(context.manager_files_path, ".openart_key")):
return None
try:
with open(os.path.join(core.manager_files_path, ".openart_key"), "r") as f:
with open(os.path.join(context.manager_files_path, ".openart_key"), "r") as f:
openart_key = f.read().strip()
return openart_key if openart_key else None
except:
except Exception:
return None
def get_matrix_auth():
if not os.path.exists(os.path.join(core.manager_files_path, "matrix_auth")):
if not os.path.exists(os.path.join(context.manager_files_path, "matrix_auth")):
return None
try:
with open(os.path.join(core.manager_files_path, "matrix_auth"), "r") as f:
with open(os.path.join(context.manager_files_path, "matrix_auth"), "r") as f:
matrix_auth = f.read()
homeserver, username, password = matrix_auth.strip().split("\n")
if not homeserver or not username or not password:
@ -89,40 +101,40 @@ def get_matrix_auth():
"username": username,
"password": password,
}
except:
except Exception:
return None
def get_comfyworkflows_auth():
if not os.path.exists(os.path.join(core.manager_files_path, "comfyworkflows_sharekey")):
if not os.path.exists(os.path.join(context.manager_files_path, "comfyworkflows_sharekey")):
return None
try:
with open(os.path.join(core.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
share_key = f.read()
if not share_key.strip():
return None
return share_key
except:
except Exception:
return None
def get_youml_settings():
if not os.path.exists(os.path.join(core.manager_files_path, ".youml")):
if not os.path.exists(os.path.join(context.manager_files_path, ".youml")):
return None
try:
with open(os.path.join(core.manager_files_path, ".youml"), "r") as f:
with open(os.path.join(context.manager_files_path, ".youml"), "r") as f:
youml_settings = f.read().strip()
return youml_settings if youml_settings else None
except:
except Exception:
return None
def set_youml_settings(settings):
with open(os.path.join(core.manager_files_path, ".youml"), "w") as f:
with open(os.path.join(context.manager_files_path, ".youml"), "w") as f:
f.write(settings)
@PromptServer.instance.routes.get("/manager/get_openart_auth")
@PromptServer.instance.routes.get("/v2/manager/get_openart_auth")
async def api_get_openart_auth(request):
# print("Getting stored Matrix credentials...")
openart_key = get_openart_auth()
@ -131,16 +143,16 @@ async def api_get_openart_auth(request):
return web.json_response({"openart_key": openart_key})
@PromptServer.instance.routes.post("/manager/set_openart_auth")
@PromptServer.instance.routes.post("/v2/manager/set_openart_auth")
async def api_set_openart_auth(request):
json_data = await request.json()
openart_key = json_data['openart_key']
with open(os.path.join(core.manager_files_path, ".openart_key"), "w") as f:
with open(os.path.join(context.manager_files_path, ".openart_key"), "w") as f:
f.write(openart_key)
return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/get_matrix_auth")
@PromptServer.instance.routes.get("/v2/manager/get_matrix_auth")
async def api_get_matrix_auth(request):
# print("Getting stored Matrix credentials...")
matrix_auth = get_matrix_auth()
@ -149,7 +161,7 @@ async def api_get_matrix_auth(request):
return web.json_response(matrix_auth)
@PromptServer.instance.routes.get("/manager/youml/settings")
@PromptServer.instance.routes.get("/v2/manager/youml/settings")
async def api_get_youml_settings(request):
youml_settings = get_youml_settings()
if not youml_settings:
@ -157,14 +169,14 @@ async def api_get_youml_settings(request):
return web.json_response(json.loads(youml_settings))
@PromptServer.instance.routes.post("/manager/youml/settings")
@PromptServer.instance.routes.post("/v2/manager/youml/settings")
async def api_set_youml_settings(request):
json_data = await request.json()
set_youml_settings(json.dumps(json_data))
return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/get_comfyworkflows_auth")
@PromptServer.instance.routes.get("/v2/manager/get_comfyworkflows_auth")
async def api_get_comfyworkflows_auth(request):
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
# in the same directory as the ComfyUI base folder
@ -175,31 +187,39 @@ async def api_get_comfyworkflows_auth(request):
return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
@PromptServer.instance.routes.post("/manager/set_esheep_workflow_and_images")
@PromptServer.instance.routes.post("/v2/manager/set_esheep_workflow_and_images")
async def set_esheep_workflow_and_images(request):
json_data = await request.json()
with open(os.path.join(core.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
json.dump(json_data, file, indent=4)
return web.Response(status=200)
@PromptServer.instance.routes.get("/manager/get_esheep_workflow_and_images")
@PromptServer.instance.routes.get("/v2/manager/get_esheep_workflow_and_images")
async def get_esheep_workflow_and_images(request):
with open(os.path.join(core.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
data = json.load(file)
return web.Response(status=200, text=json.dumps(data))
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
async def get_matrix_dep_status(request):
if matrix_nio_is_available:
return web.Response(status=200, text='available')
else:
return web.Response(status=200, text='unavailable')
def set_matrix_auth(json_data):
homeserver = json_data['homeserver']
username = json_data['username']
password = json_data['password']
with open(os.path.join(core.manager_files_path, "matrix_auth"), "w") as f:
with open(os.path.join(context.manager_files_path, "matrix_auth"), "w") as f:
f.write("\n".join([homeserver, username, password]))
def set_comfyworkflows_auth(comfyworkflows_sharekey):
with open(os.path.join(core.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
f.write(comfyworkflows_sharekey)
@ -211,7 +231,7 @@ def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
return comfyworkflows_sharekey.strip()
@PromptServer.instance.routes.post("/manager/share")
@PromptServer.instance.routes.post("/v2/manager/share")
async def share_art(request):
# get json data
json_data = await request.json()
@ -233,7 +253,7 @@ async def share_art(request):
try:
output_to_share = potential_outputs[int(selected_output_index)]
except:
except Exception:
# for now, pick the first output
output_to_share = potential_outputs[0]
@ -329,14 +349,12 @@ async def share_art(request):
workflowId = upload_workflow_json["workflowId"]
# check if the user has provided Matrix credentials
if "matrix" in share_destinations:
if matrix_nio_is_available and "matrix" in share_destinations:
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
filename = os.path.basename(asset_filepath)
content_type = assetFileType
try:
from nio import AsyncClient, LoginResponse, UploadResponse
homeserver = 'matrix.org'
if matrix_auth:
homeserver = matrix_auth.get('homeserver', 'matrix.org')

View File

View File

@ -0,0 +1,126 @@
import os
import git
import logging
import traceback
from comfyui_manager.common import context
import folder_paths
from comfyui_manager.glob import manager_core as core
from comfyui_manager.common import cm_global
comfy_ui_hash = "-"
comfyui_tag = None
def print_comfyui_version():
global comfy_ui_hash
global comfyui_tag
is_detached = False
try:
repo = git.Repo(os.path.dirname(folder_paths.__file__))
core.comfy_ui_revision = len(list(repo.iter_commits("HEAD")))
comfy_ui_hash = repo.head.commit.hexsha
cm_global.variables["comfyui.revision"] = core.comfy_ui_revision
core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
cm_global.variables["comfyui.commit_datetime"] = core.comfy_ui_commit_datetime
is_detached = repo.head.is_detached
current_branch = repo.active_branch.name
comfyui_tag = context.get_comfyui_tag()
try:
if (
not os.environ.get("__COMFYUI_DESKTOP_VERSION__")
and core.comfy_ui_commit_datetime.date()
< core.comfy_ui_required_commit_datetime.date()
):
logging.warning(
f"\n\n## [WARN] ComfyUI-Manager: Your ComfyUI version ({core.comfy_ui_revision})[{core.comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version. ##\n\n"
)
except Exception:
pass
# process on_revision_detected -->
if "cm.on_revision_detected_handler" in cm_global.variables:
for k, f in cm_global.variables["cm.on_revision_detected_handler"]:
try:
f(core.comfy_ui_revision)
except Exception:
logging.error(f"[ERROR] '{k}' on_revision_detected_handler")
traceback.print_exc()
del cm_global.variables["cm.on_revision_detected_handler"]
else:
logging.warning(
"[ComfyUI-Manager] Some features are restricted due to your ComfyUI being outdated."
)
# <--
if current_branch == "master":
if comfyui_tag:
logging.info(
f"### ComfyUI Version: {comfyui_tag} | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
else:
logging.info(
f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
else:
if comfyui_tag:
logging.info(
f"### ComfyUI Version: {comfyui_tag} on '{current_branch}' | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
else:
logging.info(
f"### ComfyUI Revision: {core.comfy_ui_revision} on '{current_branch}' [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
except Exception:
if is_detached:
logging.info(
f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] *DETACHED | Released on '{core.comfy_ui_commit_datetime.date()}'"
)
else:
logging.info(
"### ComfyUI Revision: UNKNOWN (The currently installed ComfyUI is not a Git repository)"
)
def set_update_policy(mode):
core.get_config()["update_policy"] = mode
def set_db_mode(mode):
core.get_config()["db_mode"] = mode
def setup_environment():
git_exe = core.get_config()["git_exe"]
if git_exe != "":
git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=git_exe)
def initialize_environment():
context.comfy_path = os.path.dirname(folder_paths.__file__)
core.js_path = os.path.join(context.comfy_path, "web", "extensions")
# Legacy database paths - kept for potential future use
# local_db_model = os.path.join(manager_util.comfyui_manager_path, "model-list.json")
# local_db_alter = os.path.join(manager_util.comfyui_manager_path, "alter-list.json")
# local_db_custom_node_list = os.path.join(
# manager_util.comfyui_manager_path, "custom-node-list.json"
# )
# local_db_extension_node_mappings = os.path.join(
# manager_util.comfyui_manager_path, "extension-node-map.json"
# )
print_comfyui_version()
setup_environment()
core.check_invalid_nodes()

View File

@ -0,0 +1,60 @@
import locale
import sys
import re
def handle_stream(stream, prefix):
stream.reconfigure(encoding=locale.getpreferredencoding(), errors="replace")
for msg in stream:
if (
prefix == "[!]"
and ("it/s]" in msg or "s/it]" in msg)
and ("%|" in msg or "it [" in msg)
):
if msg.startswith("100%"):
print("\r" + msg, end="", file=sys.stderr),
else:
print("\r" + msg[:-1], end="", file=sys.stderr),
else:
if prefix == "[!]":
print(prefix, msg, end="", file=sys.stderr)
else:
print(prefix, msg, end="")
def convert_markdown_to_html(input_text):
pattern_a = re.compile(r"\[a/([^]]+)]\(([^)]+)\)")
pattern_w = re.compile(r"\[w/([^]]+)]")
pattern_i = re.compile(r"\[i/([^]]+)]")
pattern_bold = re.compile(r"\*\*([^*]+)\*\*")
pattern_white = re.compile(r"%%([^*]+)%%")
def replace_a(match):
return f"<a href='{match.group(2)}' target='blank'>{match.group(1)}</a>"
def replace_w(match):
return f"<p class='cm-warn-note'>{match.group(1)}</p>"
def replace_i(match):
return f"<p class='cm-info-note'>{match.group(1)}</p>"
def replace_bold(match):
return f"<B>{match.group(1)}</B>"
def replace_white(match):
return f"<font color='white'>{match.group(1)}</font>"
input_text = (
input_text.replace("\\[", "&#91;")
.replace("\\]", "&#93;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
result_text = re.sub(pattern_a, replace_a, input_text)
result_text = re.sub(pattern_w, replace_w, result_text)
result_text = re.sub(pattern_i, replace_i, result_text)
result_text = re.sub(pattern_bold, replace_bold, result_text)
result_text = re.sub(pattern_white, replace_white, result_text)
return result_text.replace("\n", "<BR>")

View File

@ -0,0 +1,161 @@
import os
import logging
import concurrent.futures
import folder_paths
from comfyui_manager.glob import manager_core as core
from comfyui_manager.glob.constants import model_dir_name_map, MODEL_DIR_NAMES
def get_model_dir(data, show_log=False):
if "download_model_base" in folder_paths.folder_names_and_paths:
models_base = folder_paths.folder_names_and_paths["download_model_base"][0][0]
else:
models_base = folder_paths.models_dir
# NOTE: Validate to prevent path traversal.
if any(char in data["filename"] for char in {"/", "\\", ":"}):
return None
def resolve_custom_node(save_path):
save_path = save_path[13:] # remove 'custom_nodes/'
# NOTE: Validate to prevent path traversal.
if save_path.startswith(os.path.sep) or ":" in save_path:
return None
repo_name = save_path.replace("\\", "/").split("/")[
0
] # get custom node repo name
# NOTE: The creation of files within the custom node path should be removed in the future.
repo_path = core.lookup_installed_custom_nodes_legacy(repo_name)
if repo_path is not None and repo_path[0]:
# Returns the retargeted path based on the actually installed repository
return os.path.join(os.path.dirname(repo_path[1]), save_path)
else:
return None
if data["save_path"] != "default":
if ".." in data["save_path"] or data["save_path"].startswith("/"):
if show_log:
logging.info(
f"[WARN] '{data['save_path']}' is not allowed path. So it will be saved into 'models/etc'."
)
base_model = os.path.join(models_base, "etc")
else:
if data["save_path"].startswith("custom_nodes"):
base_model = resolve_custom_node(data["save_path"])
if base_model is None:
if show_log:
logging.info(
f"[ComfyUI-Manager] The target custom node for model download is not installed: {data['save_path']}"
)
return None
else:
base_model = os.path.join(models_base, data["save_path"])
else:
model_dir_name = model_dir_name_map.get(data["type"].lower())
if model_dir_name is not None:
base_model = folder_paths.folder_names_and_paths[model_dir_name][0][0]
else:
base_model = os.path.join(models_base, "etc")
return base_model
def get_model_path(data, show_log=False):
base_model = get_model_dir(data, show_log)
if base_model is None:
return None
else:
if data["filename"] == "<huggingface>":
return os.path.join(base_model, os.path.basename(data["url"]))
else:
return os.path.join(base_model, data["filename"])
def check_model_installed(json_obj):
def is_exists(model_dir_name, filename, url):
if filename == "<huggingface>":
filename = os.path.basename(url)
dirs = folder_paths.get_folder_paths(model_dir_name)
for x in dirs:
if os.path.exists(os.path.join(x, filename)):
return True
return False
total_models_files = set()
for x in MODEL_DIR_NAMES:
for y in folder_paths.get_filename_list(x):
total_models_files.add(y)
def process_model_phase(item):
if (
"diffusion" not in item["filename"]
and "pytorch" not in item["filename"]
and "model" not in item["filename"]
):
# non-general name case
if item["filename"] in total_models_files:
item["installed"] = "True"
return
if item["save_path"] == "default":
model_dir_name = model_dir_name_map.get(item["type"].lower())
if model_dir_name is not None:
item["installed"] = str(
is_exists(model_dir_name, item["filename"], item["url"])
)
else:
item["installed"] = "False"
else:
model_dir_name = item["save_path"].split("/")[0]
if model_dir_name in folder_paths.folder_names_and_paths:
if is_exists(model_dir_name, item["filename"], item["url"]):
item["installed"] = "True"
if "installed" not in item:
if item["filename"] == "<huggingface>":
filename = os.path.basename(item["url"])
else:
filename = item["filename"]
fullpath = os.path.join(
folder_paths.models_dir, item["save_path"], filename
)
item["installed"] = "True" if os.path.exists(fullpath) else "False"
with concurrent.futures.ThreadPoolExecutor(8) as executor:
for item in json_obj["models"]:
executor.submit(process_model_phase, item)
async def check_whitelist_for_model(item):
from comfyui_manager.data_models import ManagerDatabaseSource
json_obj = await core.get_data_by_mode(ManagerDatabaseSource.cache.value, "model-list.json")
for x in json_obj.get("models", []):
if (
x["save_path"] == item["save_path"]
and x["base"] == item["base"]
and x["filename"] == item["filename"]
):
return True
json_obj = await core.get_data_by_mode(ManagerDatabaseSource.local.value, "model-list.json")
for x in json_obj.get("models", []):
if (
x["save_path"] == item["save_path"]
and x["base"] == item["base"]
and x["filename"] == item["filename"]
):
return True
return False

View File

@ -0,0 +1,65 @@
import concurrent.futures
from comfyui_manager.glob import manager_core as core
def check_state_of_git_node_pack(
node_packs, do_fetch=False, do_update_check=True, do_update=False
):
if do_fetch:
print("Start fetching...", end="")
elif do_update:
print("Start updating...", end="")
elif do_update_check:
print("Start update check...", end="")
def process_custom_node(item):
core.check_state_of_git_node_pack_single(
item, do_fetch, do_update_check, do_update
)
with concurrent.futures.ThreadPoolExecutor(4) as executor:
for k, v in node_packs.items():
if v.get("active_version") in ["unknown", "nightly"]:
executor.submit(process_custom_node, v)
if do_fetch:
print("\x1b[2K\rFetching done.")
elif do_update:
update_exists = any(
item.get("updatable", False) for item in node_packs.values()
)
if update_exists:
print("\x1b[2K\rUpdate done.")
else:
print("\x1b[2K\rAll extensions are already up-to-date.")
elif do_update_check:
print("\x1b[2K\rUpdate check done.")
def nickname_filter(json_obj):
preemptions_map = {}
for k, x in json_obj.items():
if "preemptions" in x[1]:
for y in x[1]["preemptions"]:
preemptions_map[y] = k
elif k.endswith("/ComfyUI"):
for y in x[0]:
preemptions_map[y] = k
updates = {}
for k, x in json_obj.items():
removes = set()
for y in x[0]:
k2 = preemptions_map.get(y)
if k2 is not None and k != k2:
removes.add(y)
if len(removes) > 0:
updates[k] = [y for y in x[0] if y not in removes]
for k, v in updates.items():
json_obj[k][0] = v
return json_obj

View File

@ -0,0 +1,67 @@
from comfyui_manager.glob import manager_core as core
from comfy.cli_args import args
from comfyui_manager.data_models import SecurityLevel, RiskLevel, ManagerDatabaseSource
def is_loopback(address):
import ipaddress
try:
return ipaddress.ip_address(address).is_loopback
except ValueError:
return False
def is_allowed_security_level(level):
is_local_mode = is_loopback(args.listen)
is_personal_cloud = core.get_config()['network_mode'].lower() == 'personal_cloud'
if level == RiskLevel.block.value:
return False
elif level == RiskLevel.high_.value:
if is_local_mode:
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal_.value]
elif is_personal_cloud:
return core.get_config()['security_level'] == SecurityLevel.weak.value
else:
return False
elif level == RiskLevel.high.value:
if is_local_mode:
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal_.value]
else:
return core.get_config()['security_level'] == SecurityLevel.weak.value
elif level == RiskLevel.middle_.value:
if is_local_mode or is_personal_cloud:
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal.value, SecurityLevel.normal_.value]
else:
return False
elif level == RiskLevel.middle.value:
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal.value, SecurityLevel.normal_.value]
else:
return True
async def get_risky_level(files, pip_packages):
json_data1 = await core.get_data_by_mode(ManagerDatabaseSource.local.value, "custom-node-list.json")
json_data2 = await core.get_data_by_mode(
ManagerDatabaseSource.cache.value,
"custom-node-list.json",
channel_url="https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main",
)
all_urls = set()
for x in json_data1["custom_nodes"] + json_data2["custom_nodes"]:
all_urls.update(x.get("files", []))
for x in files:
if x not in all_urls:
return RiskLevel.high_.value
all_pip_packages = set()
for x in json_data1["custom_nodes"] + json_data2["custom_nodes"]:
all_pip_packages.update(x.get("pip", []))
for p in pip_packages:
if p not in all_pip_packages:
return RiskLevel.block.value
return RiskLevel.middle_.value

View File

@ -25,7 +25,7 @@ async function tryInstallCustomNode(event) {
const res = await customConfirm(msg);
if(res) {
if(event.detail.target.installed == 'Disabled') {
const response = await api.fetchApi(`/customnode/toggle_active`, {
const response = await api.fetchApi(`/v2/customnode/toggle_active`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event.detail.target)
@ -35,7 +35,7 @@ async function tryInstallCustomNode(event) {
await sleep(300);
app.ui.dialog.show(`Installing... '${event.detail.target.title}'`);
const response = await api.fetchApi(`/customnode/install`, {
const response = await api.fetchApi(`/v2/customnode/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event.detail.target)
@ -52,7 +52,7 @@ async function tryInstallCustomNode(event) {
}
}
let response = await api.fetchApi("/manager/reboot");
let response = await api.fetchApi("/v2/manager/reboot");
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
return false;

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

@ -14,12 +14,12 @@ import { OpenArtShareDialog } from "./comfyui-share-openart.js";
import {
free_models, install_pip, install_via_git_url, manager_instance,
rebootAPI, setManagerInstance, show_message, customAlert, customPrompt,
infoToast, showTerminal, setNeedRestart
infoToast, showTerminal, setNeedRestart, generateUUID
} from "./common.js";
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
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 +44,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 +84,7 @@ docStyle.innerHTML = `
}
.cm-menu-container {
padding : calc(var(--spacing)*2);
column-gap: 20px;
display: flex;
flex-wrap: wrap;
@ -140,8 +145,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);
@ -189,8 +194,7 @@ docStyle.innerHTML = `
}
`;
function is_legacy_front() {
let compareVersion = '1.2.49';
function isBeforeFrontendVersion(compareVersion) {
try {
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__'];
if (typeof frontendVersion !== 'string') {
@ -232,74 +236,56 @@ var restart_stop_button = null;
var update_policy_combo = null;
let share_option = 'all';
var is_updating = false;
var batch_id = null;
// 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 +312,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 {
@ -415,7 +407,7 @@ const style = `
`;
async function init_share_option() {
api.fetchApi('/manager/share_option')
api.fetchApi('/v2/manager/share_option')
.then(response => response.text())
.then(data => {
share_option = data || 'all';
@ -423,7 +415,7 @@ async function init_share_option() {
}
async function init_notice(notice) {
api.fetchApi('/manager/notice')
api.fetchApi('/v2/manager/notice')
.then(response => response.text())
.then(data => {
notice.innerHTML = data;
@ -474,14 +466,19 @@ async function updateComfyUI() {
let prev_text = update_comfyui_button.innerText;
update_comfyui_button.innerText = "Updating ComfyUI...";
set_inprogress_mode();
const response = await api.fetchApi('/manager/queue/update_comfyui');
// set_inprogress_mode();
showTerminal();
is_updating = true;
await api.fetchApi('/manager/queue/start');
batch_id = generateUUID();
let batch = {};
batch['batch_id'] = batch_id;
batch['update_comfyui'] = true;
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
method: 'POST',
body: JSON.stringify(batch)
});
}
function showVersionSelectorDialog(versions, current, onSelect) {
@ -612,7 +609,7 @@ async function switchComfyUI() {
switch_comfyui_button.disabled = true;
switch_comfyui_button.style.backgroundColor = "gray";
let res = await api.fetchApi(`/comfyui_manager/comfyui_versions`, { cache: "no-store" });
let res = await api.fetchApi(`/v2/comfyui_manager/comfyui_versions`, { cache: "no-store" });
switch_comfyui_button.disabled = false;
switch_comfyui_button.style.backgroundColor = "";
@ -631,14 +628,14 @@ async function switchComfyUI() {
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
if(selected_version == 'nightly') {
update_policy_combo.value = 'nightly-comfyui';
api.fetchApi('/manager/policy/update?value=nightly-comfyui');
api.fetchApi('/v2/manager/policy/update?value=nightly-comfyui');
}
else {
update_policy_combo.value = 'stable-comfyui';
api.fetchApi('/manager/policy/update?value=stable-comfyui');
api.fetchApi('/v2/manager/policy/update?value=stable-comfyui');
}
let response = await api.fetchApi(`/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
let response = await api.fetchApi(`/v2/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
if (response.status == 200) {
infoToast(`ComfyUI version is switched to ${selected_version}`);
}
@ -656,18 +653,17 @@ async function onQueueStatus(event) {
const isElectron = 'electronAPI' in window;
if(event.detail.status == 'in_progress') {
set_inprogress_mode();
// set_inprogress_mode();
update_all_button.innerText = `in progress.. (${event.detail.done_count}/${event.detail.total_count})`;
}
else if(event.detail.status == 'done') {
else if(event.detail.status == 'all-done') {
reset_action_buttons();
if(!is_updating) {
}
else if(event.detail.status == 'batch-done') {
if(batch_id != event.detail.batch_id) {
return;
}
is_updating = false;
let success_list = [];
let failed_list = [];
let comfyui_state = null;
@ -767,41 +763,28 @@ api.addEventListener("cm-queue-status", onQueueStatus);
async function updateAll(update_comfyui) {
update_all_button.innerText = "Updating...";
set_inprogress_mode();
// set_inprogress_mode();
var mode = manager_instance.datasrc_combo.value;
showTerminal();
batch_id = generateUUID();
let batch = {};
if(update_comfyui) {
update_all_button.innerText = "Updating ComfyUI...";
await api.fetchApi('/manager/queue/update_comfyui');
batch['update_comfyui'] = true;
}
const response = await api.fetchApi(`/manager/queue/update_all?mode=${mode}`);
batch['update_all'] = mode;
if (response.status == 401) {
customAlert('Another task is already in progress. Please stop the ongoing task first.');
}
else if(response.status == 200) {
is_updating = true;
await api.fetchApi('/manager/queue/start');
}
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
method: 'POST',
body: JSON.stringify(batch)
});
}
function newDOMTokenList(initialTokens) {
const tmp = document.createElement(`div`);
const classList = tmp.classList;
if (initialTokens) {
initialTokens.forEach(token => {
classList.add(token);
});
}
return classList;
}
/**
* Check whether the node is a potential output node (img, gif or video output)
*/
@ -814,7 +797,7 @@ function restartOrStop() {
rebootAPI();
}
else {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/v2/manager/queue/reset');
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
}
@ -826,7 +809,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: {
@ -837,7 +820,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: {
@ -848,7 +831,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()
@ -856,7 +839,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:
@ -865,7 +848,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:
@ -875,7 +858,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:
@ -887,7 +870,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:
@ -899,7 +882,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:
@ -911,8 +894,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:
@ -924,7 +907,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 () => {
@ -936,13 +919,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,
];
@ -955,43 +938,29 @@ 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')
api.fetchApi('/v2/manager/db_mode')
.then(response => response.text())
.then(data => { this.datasrc_combo.value = data; });
this.datasrc_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
api.fetchApi(`/v2/manager/db_mode?value=${event.target.value}`);
});
// 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)' }, []));
api.fetchApi('/manager/preview_method')
.then(response => response.text())
.then(data => { preview_combo.value = data; });
preview_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
});
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_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";
api.fetchApi('/manager/channel_url_list')
channel_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
api.fetchApi('/v2/manager/channel_url_list')
.then(response => response.json())
.then(async data => {
try {
@ -999,12 +968,12 @@ 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]}` }, []));
}
}
channel_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`);
api.fetchApi(`/v2/manager/channel_url_list?value=${event.target.value}`);
});
channel_combo.value = data.selected;
@ -1014,11 +983,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'],
@ -1029,10 +1000,10 @@ 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')
api.fetchApi('/v2/manager/share_option')
.then(response => response.text())
.then(data => {
share_combo.value = data || 'all';
@ -1042,7 +1013,7 @@ class ManagerMenuDialog extends ComfyDialog {
share_combo.addEventListener('change', function (event) {
const value = event.target.value;
share_option = value;
api.fetchApi(`/manager/share_option?value=${value}`);
api.fetchApi(`/v2/manager/share_option?value=${value}`);
const shareButton = document.getElementById("shareButton");
if (value === 'none') {
shareButton.style.display = "none";
@ -1051,57 +1022,38 @@ class ManagerMenuDialog extends ComfyDialog {
}
});
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' }, []));
api.fetchApi('/manager/policy/component')
.then(response => response.text())
.then(data => {
component_policy_combo.value = data;
set_component_policy(data);
});
component_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/policy/component?value=${event.target.value}`);
set_component_policy(event.target.value);
});
const shareSetttingItem = createSettingsCombo("Share", share_combo);
update_policy_combo = document.createElement("select");
if(isElectron)
update_policy_combo.style.display = 'none';
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' }, []));
api.fetchApi('/manager/policy/update')
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('/v2/manager/policy/update')
.then(response => response.text())
.then(data => {
update_policy_combo.value = data;
});
update_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
api.fetchApi(`/v2/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,
shareSetttingItem,
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:
@ -1111,7 +1063,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:
@ -1129,7 +1081,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",
@ -1180,11 +1132,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")
@ -1207,7 +1159,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)',
@ -1223,13 +1175,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");
@ -1244,31 +1195,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() {
@ -1276,7 +1219,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
show() {
this.element.style.display = "block";
this.element.style.display = "flex";
}
toggleVisibility() {
@ -1388,12 +1331,12 @@ class ManagerMenuDialog extends ComfyDialog {
}
async function getVersion() {
let version = await api.fetchApi(`/manager/version`);
let version = await api.fetchApi(`/v2/manager/version`);
return await version.text();
}
app.registerExtension({
name: "Comfy.ManagerMenu",
name: "Comfy.Legacy.ManagerMenu",
aboutPageBadges: [
{
@ -1445,14 +1388,6 @@ app.registerExtension({
});
},
async setup() {
let orig_clear = app.graph.clear;
app.graph.clear = function () {
orig_clear.call(app.graph);
load_components();
};
load_components();
const menu = document.querySelector(".comfy-menu");
const separator = document.createElement("hr");
@ -1524,8 +1459,6 @@ app.registerExtension({
tooltip: "Share"
}).element
);
app.menu?.settingsGroup.element.before(cmGroup.element);
}
catch(exception) {
console.log('ComfyUI is outdated. New style menu based features are disabled.');
@ -1583,19 +1516,6 @@ app.registerExtension({
node.prototype.getExtraMenuOptions = function (_, options) {
origGetExtraMenuOptions?.apply?.(this, arguments);
if (node.category.startsWith('group nodes>')) {
options.push({
content: "Save As Component",
callback: (obj) => {
if (!ComponentBuilderDialog.instance) {
ComponentBuilderDialog.instance = new ComponentBuilderDialog();
}
ComponentBuilderDialog.instance.target_node = node;
ComponentBuilderDialog.instance.show();
}
}, null);
}
if (isOutputNode(node)) {
const { potential_outputs } = getPotentialOutputsAndOutputNodes([this]);
const hasOutput = potential_outputs.length > 0;

View File

@ -172,7 +172,7 @@ export const shareToEsheep= () => {
const nodes = app.graph._nodes
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
const workflow = prompt['workflow']
api.fetchApi(`/manager/set_esheep_workflow_and_images`, {
api.fetchApi(`/v2/manager/set_esheep_workflow_and_images`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -552,6 +552,20 @@ export class ShareDialog extends ComfyDialog {
this.matrix_destination_checkbox.style.color = "var(--fg-color)";
this.matrix_destination_checkbox.checked = this.share_option === 'matrix'; //true;
try {
api.fetchApi(`/v2/manager/get_matrix_dep_status`)
.then(response => response.text())
.then(data => {
if(data == 'unavailable') {
matrix_destination_checkbox_text.style.textDecoration = "line-through";
this.matrix_destination_checkbox.disabled = true;
this.matrix_destination_checkbox.title = "It has been disabled because the 'matrix-nio' dependency is not installed. Please install this dependency to use the matrix sharing feature.";
matrix_destination_checkbox_text.title = "It has been disabled because the 'matrix-nio' dependency is not installed. Please install this dependency to use the matrix sharing feature.";
}
})
.catch(error => {});
} catch (error) {}
this.comfyworkflows_destination_checkbox = $el("input", { type: 'checkbox', id: "comfyworkflows_destination" }, [])
const comfyworkflows_destination_checkbox_text = $el("label", {}, [" ComfyWorkflows.com"])
this.comfyworkflows_destination_checkbox.style.color = "var(--fg-color)";
@ -812,7 +826,7 @@ export class ShareDialog extends ComfyDialog {
// get the user's existing matrix auth and share key
ShareDialog.matrix_auth = { homeserver: "matrix.org", username: "", password: "" };
try {
api.fetchApi(`/manager/get_matrix_auth`)
api.fetchApi(`/v2/manager/get_matrix_auth`)
.then(response => response.json())
.then(data => {
ShareDialog.matrix_auth = data;
@ -831,7 +845,7 @@ export class ShareDialog extends ComfyDialog {
ShareDialog.cw_sharekey = "";
try {
// console.log("Fetching comfyworkflows share key")
api.fetchApi(`/manager/get_comfyworkflows_auth`)
api.fetchApi(`/v2/manager/get_comfyworkflows_auth`)
.then(response => response.json())
.then(data => {
ShareDialog.cw_sharekey = data.comfyworkflows_sharekey;
@ -891,7 +905,7 @@ export class ShareDialog extends ComfyDialog {
// Change the text of the share button to "Sharing..." to indicate that the share process has started
this.share_button.textContent = "Sharing...";
const response = await api.fetchApi(`/manager/share`, {
const response = await api.fetchApi(`/v2/manager/share`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({

View File

@ -67,7 +67,7 @@ export class OpenArtShareDialog extends ComfyDialog {
async readKey() {
let key = ""
try {
key = await api.fetchApi(`/manager/get_openart_auth`)
key = await api.fetchApi(`/v2/manager/get_openart_auth`)
.then(response => response.json())
.then(data => {
return data.openart_key;
@ -82,7 +82,7 @@ export class OpenArtShareDialog extends ComfyDialog {
}
async saveKey(value) {
await api.fetchApi(`/manager/set_openart_auth`, {
await api.fetchApi(`/v2/manager/set_openart_auth`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
@ -399,7 +399,7 @@ export class OpenArtShareDialog extends ComfyDialog {
form.append("file", uploadFile);
try {
const res = await this.fetchApi(
`/workflows/upload_thumbnail`,
`/v2/workflows/upload_thumbnail`,
{
method: "POST",
body: form,
@ -459,7 +459,7 @@ export class OpenArtShareDialog extends ComfyDialog {
throw new Error("Title is required");
}
const current_snapshot = await api.fetchApi(`/snapshot/get_current`)
const current_snapshot = await api.fetchApi(`/v2/snapshot/get_current`)
.then(response => response.json())
.catch(error => {
// console.log(error);
@ -489,7 +489,7 @@ export class OpenArtShareDialog extends ComfyDialog {
try {
const response = await this.fetchApi(
"/workflows/publish",
"/v2/workflows/publish",
{
method: "POST",
headers: {"Content-Type": "application/json"},

View File

@ -179,7 +179,7 @@ export class YouMLShareDialog extends ComfyDialog {
async loadToken() {
let key = ""
try {
const response = await api.fetchApi(`/manager/youml/settings`)
const response = await api.fetchApi(`/v2/manager/youml/settings`)
const settings = await response.json()
return settings.token
} catch (error) {
@ -188,7 +188,7 @@ export class YouMLShareDialog extends ComfyDialog {
}
async saveToken(value) {
await api.fetchApi(`/manager/youml/settings`, {
await api.fetchApi(`/v2/manager/youml/settings`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
@ -380,7 +380,7 @@ export class YouMLShareDialog extends ComfyDialog {
try {
let snapshotData = null;
try {
const snapshot = await api.fetchApi(`/snapshot/get_current`)
const snapshot = await api.fetchApi(`/v2/snapshot/get_current`)
snapshotData = await snapshot.json()
} catch (e) {
console.error("Failed to get snapshot", e)

View File

@ -172,7 +172,7 @@ export function rebootAPI() {
customConfirm("Are you sure you'd like to reboot the server?").then((isConfirmed) => {
if (isConfirmed) {
try {
api.fetchApi("/manager/reboot");
api.fetchApi("/v2/manager/reboot");
}
catch(exception) {}
}
@ -210,7 +210,7 @@ export async function install_pip(packages) {
if(packages.includes('&'))
app.ui.dialog.show(`Invalid PIP package enumeration: '${packages}'`);
const res = await api.fetchApi("/customnode/install/pip", {
const res = await api.fetchApi("/v2/customnode/install/pip", {
method: "POST",
body: packages,
});
@ -245,7 +245,7 @@ export async function install_via_git_url(url, manager_dialog) {
show_message(`Wait...<BR><BR>Installing '${url}'`);
const res = await api.fetchApi("/customnode/install/git_url", {
const res = await api.fetchApi("/v2/customnode/install/git_url", {
method: "POST",
body: url,
});
@ -630,6 +630,14 @@ export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {
});
}
export function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function initTooltip () {
const mouseenterHandler = (e) => {
const target = e.target;

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,13 +1,14 @@
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,
fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt,
sanitizeHTML, infoToast, showTerminal, setNeedRestart,
storeColumnWidth, restoreColumnWidth, getTimeAgo, copyText, loadCss,
showPopover, hidePopover
showPopover, hidePopover, generateUUID
} from "./common.js";
// https://cenfun.github.io/turbogrid/api.html
@ -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>
`;
@ -66,7 +54,7 @@ export class CustomNodesManager {
this.id = "cn-manager";
app.registerExtension({
name: "Comfy.CustomNodesManager",
name: "Comfy.Legacy.CustomNodesManager",
afterConfigureGraph: (missingNodeTypes) => {
const item = this.getFilterItem(ShowMode.MISSING);
if (item) {
@ -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("");
}
@ -459,7 +462,7 @@ export class CustomNodesManager {
".cn-manager-stop": {
click: () => {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/v2/manager/queue/reset');
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
@ -635,7 +638,7 @@ export class CustomNodesManager {
};
}
const response = await api.fetchApi(`/customnode/import_fail_info`, {
const response = await api.fetchApi(`/v2/customnode/import_fail_info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(info)
@ -655,7 +658,6 @@ export class CustomNodesManager {
}
renderGrid() {
// update theme
const globalStyle = window.getComputedStyle(document.body);
this.colorVars = {
@ -1244,7 +1246,7 @@ export class CustomNodesManager {
async loadNodes(node_packs) {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading node mappings (${mode}) ...`);
const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
const res = await fetchData(`/v2/customnode/getmappings?mode=${mode}`);
if (res.error) {
console.log(res.error);
return;
@ -1396,10 +1398,10 @@ export class CustomNodesManager {
this.showLoading();
let res;
if(is_enable) {
res = await api.fetchApi(`/customnode/disabled_versions/${node_id}`, { cache: "no-store" });
res = await api.fetchApi(`/v2/customnode/disabled_versions/${node_id}`, { cache: "no-store" });
}
else {
res = await api.fetchApi(`/customnode/versions/${node_id}`, { cache: "no-store" });
res = await api.fetchApi(`/v2/customnode/versions/${node_id}`, { cache: "no-store" });
}
this.hideLoading();
@ -1441,13 +1443,6 @@ export class CustomNodesManager {
}
async installNodes(list, btn, title, selected_version) {
let stats = await api.fetchApi('/manager/queue/status');
stats = await stats.json();
if(stats.is_processing) {
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
return;
}
const { target, label, mode} = btn;
if(mode === "uninstall") {
@ -1474,10 +1469,10 @@ export class CustomNodesManager {
let needRestart = false;
let errorMsg = "";
await api.fetchApi('/manager/queue/reset');
let target_items = [];
let batch = {};
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
target_items.push(item);
@ -1519,23 +1514,11 @@ export class CustomNodesManager {
api_mode = 'reinstall';
}
const res = await api.fetchApi(`/manager/queue/${api_mode}`, {
method: 'POST',
body: JSON.stringify(data)
});
if (res.status != 200) {
errorMsg = `'${item.title}': `;
if(res.status == 403) {
errorMsg += `This action is not allowed with this security level configuration.\n`;
} else if(res.status == 404) {
errorMsg += `With the current security level configuration, only custom nodes from the <B>"default channel"</B> can be installed.\n`;
} else {
errorMsg += await res.text() + '\n';
}
break;
if(batch[api_mode]) {
batch[api_mode].push(data);
}
else {
batch[api_mode] = [data];
}
}
@ -1552,7 +1535,24 @@ export class CustomNodesManager {
}
}
else {
await api.fetchApi('/manager/queue/start');
this.batch_id = generateUUID();
batch['batch_id'] = this.batch_id;
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
method: 'POST',
body: JSON.stringify(batch)
});
let failed = await res.json();
if(failed.length > 0) {
for(let k in failed) {
let hash = failed[k];
const item = this.grid.getRowItemBy("hash", hash);
errorMsg = `[FAIL] ${item.title}`;
}
}
this.showStop();
showTerminal();
}
@ -1560,6 +1560,9 @@ export class CustomNodesManager {
async onQueueStatus(event) {
let self = CustomNodesManager.instance;
// If legacy manager front is not open, return early (using new manager front)
if (self.element?.style.display === 'none') return
if(event.detail.status == 'in_progress' && event.detail.ui_target == 'nodepack_manager') {
const hash = event.detail.target;
@ -1570,7 +1573,7 @@ export class CustomNodesManager {
self.grid.updateCell(item, "action");
self.grid.setRowSelected(item, false);
}
else if(event.detail.status == 'done') {
else if(event.detail.status == 'batch-done' && event.detail.batch_id == self.batch_id) {
self.hideStop();
self.onQueueCompleted(event.detail);
}
@ -1764,7 +1767,7 @@ export class CustomNodesManager {
async getMissingNodesLegacy(hashMap, missing_nodes) {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading missing nodes (${mode}) ...`);
const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
const res = await fetchData(`/v2/customnode/getmappings?mode=${mode}`);
if (res.error) {
this.showError(`Failed to get custom node mappings: ${res.error}`);
return;
@ -1879,7 +1882,7 @@ export class CustomNodesManager {
async getAlternatives() {
const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading alternatives (${mode}) ...`);
const res = await fetchData(`/customnode/alternatives?mode=${mode}`);
const res = await fetchData(`/v2/customnode/alternatives?mode=${mode}`);
if (res.error) {
this.showError(`Failed to get alternatives: ${res.error}`);
return [];
@ -1927,7 +1930,7 @@ export class CustomNodesManager {
infoToast('Fetching updated information. This may take some time if many custom nodes are installed.');
}
const res = await fetchData(`/customnode/getlist?mode=${mode}${skip_update}`);
const res = await fetchData(`/v2/customnode/getlist?mode=${mode}${skip_update}`);
if (res.error) {
this.showError("Failed to get custom node list.");
this.hideLoading();

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

@ -3,45 +3,28 @@ import { $el } from "../../scripts/ui.js";
import {
manager_instance, rebootAPI,
fetchData, md5, icons, show_message, customAlert, infoToast, showTerminal,
storeColumnWidth, restoreColumnWidth, loadCss
storeColumnWidth, restoreColumnWidth, loadCss, generateUUID
} from "./common.js";
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();
@ -175,7 +170,7 @@ export class ModelManager {
".cmm-manager-stop": {
click: () => {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/v2/manager/queue/reset');
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
@ -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) {
@ -435,24 +430,16 @@ export class ModelManager {
}
async installModels(list, btn) {
let stats = await api.fetchApi('/manager/queue/status');
stats = await stats.json();
if(stats.is_processing) {
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
return;
}
btn.classList.add("cmm-btn-loading");
this.showError("");
let needRefresh = false;
let errorMsg = "";
await api.fetchApi('/manager/queue/reset');
let target_items = [];
let batch = {};
for (const item of list) {
this.grid.scrollRowIntoView(item);
target_items.push(item);
@ -468,21 +455,12 @@ export class ModelManager {
const data = item.originalData;
data.ui_id = item.hash;
const res = await api.fetchApi(`/manager/queue/install_model`, {
method: 'POST',
body: JSON.stringify(data)
});
if (res.status != 200) {
errorMsg = `'${item.name}': `;
if(res.status == 403) {
errorMsg += `This action is not allowed with this security level configuration.\n`;
} else {
errorMsg += await res.text() + '\n';
}
break;
if(batch['install_model']) {
batch['install_model'].push(data);
}
else {
batch['install_model'] = [data];
}
}
@ -499,7 +477,24 @@ export class ModelManager {
}
}
else {
await api.fetchApi('/manager/queue/start');
this.batch_id = generateUUID();
batch['batch_id'] = this.batch_id;
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
method: 'POST',
body: JSON.stringify(batch)
});
let failed = await res.json();
if(failed.length > 0) {
for(let k in failed) {
let hash = failed[k];
const item = self.grid.getRowItemBy("hash", hash);
errorMsg = `[FAIL] ${item.title}`;
}
}
this.showStop();
showTerminal();
}
@ -519,7 +514,7 @@ export class ModelManager {
// self.grid.updateCell(item, "tg-column-select");
self.grid.updateRow(item);
}
else if(event.detail.status == 'done') {
else if(event.detail.status == 'batch-done') {
self.hideStop();
self.onQueueCompleted(event.detail);
}
@ -645,7 +640,7 @@ export class ModelManager {
const mode = manager_instance.datasrc_combo.value;
const res = await fetchData(`/externalmodel/getlist?mode=${mode}`);
const res = await fetchData(`/v2/externalmodel/getlist?mode=${mode}`);
if (res.error) {
this.showError("Failed to get external model list.");
this.hideLoading();

View File

@ -142,7 +142,7 @@ function node_info_copy(src, dest, connect_both, copy_shape) {
}
app.registerExtension({
name: "Comfy.Manager.NodeFixer",
name: "Comfy.Legacy.Manager.NodeFixer",
beforeRegisterNodeDef(nodeType, nodeData, app) {
addMenuHandler(nodeType, function (_, options) {
options.push({

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,13 +1,15 @@
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 } from "./common.js";
import { manager_instance, rebootAPI, show_message, loadCss } from "./common.js";
import { buildGuiFrame } from "./comfyui-gui-builder.js";
loadCss("./snapshot.css");
async function restore_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/snapshot/restore?target=${target}`, { cache: "no-store" });
const response = await api.fetchApi(`/v2/snapshot/restore?target=${target}`, { cache: "no-store" });
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
@ -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');
}
}
}
@ -35,7 +37,7 @@ async function restore_snapshot(target) {
async function remove_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/snapshot/remove?target=${target}`, { cache: "no-store" });
const response = await api.fetchApi(`/v2/snapshot/remove?target=${target}`, { cache: "no-store" });
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
@ -61,7 +63,7 @@ async function remove_snapshot(target) {
async function save_current_snapshot() {
try {
const response = await api.fetchApi('/snapshot/save', { cache: "no-store" });
const response = await api.fetchApi('/v2/snapshot/save', { cache: "no-store" });
app.ui.dialog.close();
return true;
}
@ -76,7 +78,7 @@ async function save_current_snapshot() {
}
async function getSnapshotList() {
const response = await api.fetchApi(`/snapshot/getlist`);
const response = await api.fetchApi(`/v2/snapshot/getlist`);
const data = await response.json();
return data;
}
@ -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

@ -38,7 +38,7 @@ class WorkflowMetadataExtension {
* enabled is true if the node is enabled, false if it is disabled
*/
async getInstalledNodes() {
const res = await api.fetchApi("/customnode/installed");
const res = await api.fetchApi("/v2/customnode/installed");
return await res.json();
}

View File

View File

@ -15,6 +15,7 @@ import platform
from datetime import datetime
import git
from comfyui_manager.common.timestamp_utils import get_timestamp_for_path, get_backup_branch_name
from git.remote import RemoteProgress
from urllib.parse import urlparse
from tqdm.auto import tqdm
@ -23,7 +24,6 @@ import yaml
import zipfile
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed
import toml
orig_print = print
@ -32,23 +32,25 @@ from packaging import version
import uuid
glob_path = os.path.join(os.path.dirname(__file__)) # ComfyUI-Manager/glob
sys.path.append(glob_path)
import cm_global
import cnr_utils
import manager_util
import git_utils
import manager_downloader
from node_package import InstalledNodePackage
from ..common import cm_global
from ..common import cnr_utils
from ..common import manager_util
from ..common import git_utils
from ..common import manager_downloader
from ..common.node_package import InstalledNodePackage
from ..common.enums import NetworkMode, SecurityLevel, DBMode
from ..common import context
version_code = [3, 37, 2]
version_code = [4, 0, 3]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main"
DEFAULT_CHANNEL_LEGACY = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
# SSH git URL pattern (e.g., git@github.com:user/repo.git)
SSH_URL_PATTERN = re.compile(r"^(.+@|ssh://).+:.+$")
default_custom_nodes_path = None
@ -58,13 +60,14 @@ class InvalidChannel(Exception):
self.channel = channel
super().__init__(channel)
def get_default_custom_nodes_path():
global default_custom_nodes_path
if default_custom_nodes_path is None:
try:
import folder_paths
default_custom_nodes_path = folder_paths.get_folder_paths("custom_nodes")[0]
except:
except Exception:
default_custom_nodes_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..'))
return default_custom_nodes_path
@ -74,37 +77,11 @@ def get_custom_nodes_paths():
try:
import folder_paths
return folder_paths.get_folder_paths("custom_nodes")
except:
except Exception:
custom_nodes_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..'))
return [custom_nodes_path]
def get_comfyui_tag():
try:
repo = git.Repo(comfy_path)
return repo.git.describe('--tags')
except:
return None
def get_current_comfyui_ver():
"""
Extract version from pyproject.toml
"""
toml_path = os.path.join(comfy_path, 'pyproject.toml')
if not os.path.exists(toml_path):
return None
else:
try:
with open(toml_path, "r", encoding="utf-8") as f:
data = toml.load(f)
project = data.get('project', {})
return project.get('version')
except:
return None
def get_script_env():
new_env = os.environ.copy()
git_exe = get_config().get('git_exe')
@ -112,10 +89,10 @@ def get_script_env():
new_env['GIT_EXE_PATH'] = git_exe
if 'COMFYUI_PATH' not in new_env:
new_env['COMFYUI_PATH'] = comfy_path
new_env['COMFYUI_PATH'] = context.comfy_path
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
new_env['COMFYUI_FOLDERS_BASE_PATH'] = comfy_path
new_env['COMFYUI_FOLDERS_BASE_PATH'] = context.comfy_path
return new_env
@ -137,12 +114,12 @@ def check_invalid_nodes():
try:
import folder_paths
except:
except Exception:
try:
sys.path.append(comfy_path)
sys.path.append(context.comfy_path)
import folder_paths
except:
raise Exception(f"Invalid COMFYUI_FOLDERS_BASE_PATH: {comfy_path}")
except Exception:
raise Exception(f"Invalid COMFYUI_FOLDERS_BASE_PATH: {context.comfy_path}")
def check(root):
global invalid_nodes
@ -177,75 +154,6 @@ def check_invalid_nodes():
print("\n---------------------------------------------------------------------------\n")
# read env vars
comfy_path: str = os.environ.get('COMFYUI_PATH')
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
if comfy_path is None:
try:
import folder_paths
comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))
except:
comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))
if comfy_base_path is None:
comfy_base_path = comfy_path
channel_list_template_path = os.path.join(manager_util.comfyui_manager_path, 'channels.list.template')
git_script_path = os.path.join(manager_util.comfyui_manager_path, "git_helper.py")
manager_files_path = None
manager_config_path = None
manager_channel_list_path = None
manager_startup_script_path:str = None
manager_snapshot_path = None
manager_pip_overrides_path = None
manager_pip_blacklist_path = None
manager_components_path = None
def update_user_directory(user_dir):
global manager_files_path
global manager_config_path
global manager_channel_list_path
global manager_startup_script_path
global manager_snapshot_path
global manager_pip_overrides_path
global manager_pip_blacklist_path
global manager_components_path
manager_files_path = os.path.abspath(os.path.join(user_dir, 'default', 'ComfyUI-Manager'))
if not os.path.exists(manager_files_path):
os.makedirs(manager_files_path)
manager_snapshot_path = os.path.join(manager_files_path, "snapshots")
if not os.path.exists(manager_snapshot_path):
os.makedirs(manager_snapshot_path)
manager_startup_script_path = os.path.join(manager_files_path, "startup-scripts")
if not os.path.exists(manager_startup_script_path):
os.makedirs(manager_startup_script_path)
manager_config_path = os.path.join(manager_files_path, 'config.ini')
manager_channel_list_path = os.path.join(manager_files_path, 'channels.list')
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list")
manager_components_path = os.path.join(manager_files_path, "components")
manager_util.cache_dir = os.path.join(manager_files_path, "cache")
if not os.path.exists(manager_util.cache_dir):
os.makedirs(manager_util.cache_dir)
try:
import folder_paths
update_user_directory(folder_paths.get_user_directory())
except Exception:
# fallback:
# This case is only possible when running with cm-cli, and in practice, this case is not actually used.
update_user_directory(os.path.abspath(manager_util.comfyui_manager_path))
cached_config = None
js_path = None
@ -256,7 +164,7 @@ comfy_ui_revision = "Unknown"
comfy_ui_commit_datetime = datetime(1900, 1, 1, 0, 0, 0)
channel_dict = None
valid_channels = {'default', 'local'}
valid_channels = {'default', 'local', DEFAULT_CHANNEL, DEFAULT_CHANNEL_LEGACY}
channel_list = None
@ -621,7 +529,7 @@ class UnifiedManager:
ver = str(manager_util.StrictVersion(info['version']))
return {'id': cnr['id'], 'cnr': cnr, 'ver': ver}
else:
return None
return {'id': info['id'], 'ver': info['version']}
else:
return None
@ -797,7 +705,9 @@ class UnifiedManager:
return latest
async def reload(self, cache_mode, dont_wait=True):
async def reload(self, cache_mode, dont_wait=True, update_cnr_map=True):
import folder_paths
self.custom_node_map_cache = {}
self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath
self.nightly_inactive_nodes = {} # node_id -> fullpath
@ -805,17 +715,18 @@ class UnifiedManager:
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
self.active_nodes = {} # node_id -> node_version * fullpath
if get_config()['network_mode'] != 'public':
if get_config()['network_mode'] != 'public' or manager_util.is_manager_pip_package():
dont_wait = True
# reload 'cnr_map' and 'repo_cnr_map'
cnrs = await cnr_utils.get_cnr_data(cache_mode=cache_mode=='cache', dont_wait=dont_wait)
if update_cnr_map:
# reload 'cnr_map' and 'repo_cnr_map'
cnrs = await cnr_utils.get_cnr_data(cache_mode=cache_mode=='cache', dont_wait=dont_wait)
for x in cnrs:
self.cnr_map[x['id']] = x
if 'repository' in x:
normalized_url = git_utils.normalize_url(x['repository'])
self.repo_cnr_map[normalized_url] = x
for x in cnrs:
self.cnr_map[x['id']] = x
if 'repository' in x:
normalized_url = git_utils.normalize_url(x['repository'])
self.repo_cnr_map[normalized_url] = x
# reload node status info from custom_nodes/*
for custom_nodes_path in folder_paths.get_folder_paths('custom_nodes'):
@ -863,7 +774,7 @@ class UnifiedManager:
if 'id' in x:
if x['id'] not in res:
res[x['id']] = (x, True)
except:
except Exception:
logging.error(f"[ComfyUI-Manager] broken item:{x}")
return res
@ -916,7 +827,7 @@ class UnifiedManager:
def safe_version(ver_str):
try:
return version.parse(ver_str)
except:
except Exception:
return version.parse("0.0.0")
def execute_install_script(self, url, repo_path, instant_execution=False, lazy_mode=False, no_deps=False):
@ -930,15 +841,14 @@ class UnifiedManager:
else:
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), context.comfy_path, context.manager_files_path)
lines = manager_util.robust_readlines(requirements_path)
for line in lines:
package_name = remap_pip_package(line.strip())
if package_name and not package_name.startswith('#') and package_name not in self.processed_install:
self.processed_install.add(package_name)
clean_package_name = package_name.split('#')[0].strip()
install_cmd = manager_util.make_pip_cmd(["install", clean_package_name])
if clean_package_name != "" and not clean_package_name.startswith('#'):
install_cmd = manager_util.make_pip_cmd(["install", package_name])
if package_name.strip() != "" and not package_name.startswith('#'):
res = res and try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution)
pip_fixer.fix_broken()
@ -952,7 +862,7 @@ class UnifiedManager:
return res
def reserve_cnr_switch(self, target, zip_url, from_path, to_path, no_deps):
script_path = os.path.join(manager_startup_script_path, "install-scripts.txt")
script_path = os.path.join(context.manager_startup_script_path, "install-scripts.txt")
with open(script_path, "a") as file:
obj = [target, "#LAZY-CNR-SWITCH-SCRIPT", zip_url, from_path, to_path, no_deps, get_default_custom_nodes_path(), sys.executable]
file.write(f"{obj}\n")
@ -1358,7 +1268,7 @@ class UnifiedManager:
print(f"Download: git clone '{clone_url}'")
if not instant_execution and platform.system() == 'Windows':
res = manager_funcs.run_script([sys.executable, git_script_path, "--clone", get_default_custom_nodes_path(), clone_url, repo_path], cwd=get_default_custom_nodes_path())
res = manager_funcs.run_script([sys.executable, context.git_script_path, "--clone", get_default_custom_nodes_path(), clone_url, repo_path], cwd=get_default_custom_nodes_path())
if res != 0:
return result.fail(f"Failed to clone repo: {clone_url}")
else:
@ -1511,12 +1421,20 @@ class UnifiedManager:
return self.unified_enable(node_id, version_spec)
elif version_spec == 'unknown' or version_spec == 'nightly':
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
if version_spec == 'nightly':
# disable cnr nodes
if self.is_enabled(node_id, 'cnr'):
self.unified_disable(node_id, False)
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
# use `repo name` as a dir name instead of `cnr id` if system added nodepack (i.e. publisher is null)
cnr = self.cnr_map.get(node_id)
if cnr is not None and cnr.get('publisher') is None:
repo_name = os.path.basename(git_utils.normalize_url(repo_url))
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), repo_name))
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
if res.result:
if version_spec == 'unknown':
@ -1577,7 +1495,7 @@ def identify_node_pack_from_path(fullpath):
if github_id is None:
try:
github_id = os.path.basename(repo_url)
except:
except Exception:
logging.warning(f"[ComfyUI-Manager] unexpected repo url: {repo_url}")
github_id = module_name
@ -1632,10 +1550,10 @@ def get_channel_dict():
if channel_dict is None:
channel_dict = {}
if not os.path.exists(manager_channel_list_path):
shutil.copy(channel_list_template_path, manager_channel_list_path)
if not os.path.exists(context.manager_channel_list_path):
shutil.copy(context.channel_list_template_path, context.manager_channel_list_path)
with open(manager_channel_list_path, 'r') as file:
with open(context.manager_channel_list_path, 'r') as file:
channels = file.read()
for x in channels.split('\n'):
channel_info = x.split("::")
@ -1661,9 +1579,6 @@ class ManagerFuncs:
def __init__(self):
pass
def get_current_preview_method(self):
return "none"
def run_script(self, cmd, cwd='.'):
if len(cmd) > 0 and cmd[0].startswith("#"):
print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
@ -1681,14 +1596,12 @@ def write_config():
config = configparser.ConfigParser(strict=False)
config['default'] = {
'preview_method': manager_funcs.get_current_preview_method(),
'git_exe': get_config()['git_exe'],
'use_uv': get_config()['use_uv'],
'channel_url': get_config()['channel_url'],
'share_option': get_config()['share_option'],
'bypass_ssl': get_config()['bypass_ssl'],
"file_logging": get_config()['file_logging'],
'component_policy': get_config()['component_policy'],
'update_policy': get_config()['update_policy'],
'windows_selector_event_loop_policy': get_config()['windows_selector_event_loop_policy'],
'model_download_by_agent': get_config()['model_download_by_agent'],
@ -1699,18 +1612,18 @@ def write_config():
'db_mode': get_config()['db_mode'],
}
directory = os.path.dirname(manager_config_path)
directory = os.path.dirname(context.manager_config_path)
if not os.path.exists(directory):
os.makedirs(directory)
with open(manager_config_path, 'w') as configfile:
with open(context.manager_config_path, 'w') as configfile:
config.write(configfile)
def read_config():
try:
config = configparser.ConfigParser(strict=False)
config.read(manager_config_path)
config.read(context.manager_config_path)
default_conf = config['default']
def get_bool(key, default_value):
@ -1721,50 +1634,44 @@ def read_config():
return {
'http_channel_enabled': get_bool('http_channel_enabled', False),
'preview_method': default_conf.get('preview_method', manager_funcs.get_current_preview_method()).lower(),
'git_exe': default_conf.get('git_exe', ''),
'use_uv': get_bool('use_uv', False),
'use_uv': get_bool('use_uv', True),
'channel_url': default_conf.get('channel_url', DEFAULT_CHANNEL),
'default_cache_as_channel_url': get_bool('default_cache_as_channel_url', False),
'share_option': default_conf.get('share_option', 'all').lower(),
'bypass_ssl': get_bool('bypass_ssl', False),
'file_logging': get_bool('file_logging', True),
'component_policy': default_conf.get('component_policy', 'workflow').lower(),
'update_policy': default_conf.get('update_policy', 'stable-comfyui').lower(),
'windows_selector_event_loop_policy': get_bool('windows_selector_event_loop_policy', False),
'model_download_by_agent': get_bool('model_download_by_agent', False),
'downgrade_blacklist': default_conf.get('downgrade_blacklist', '').lower(),
'always_lazy_install': get_bool('always_lazy_install', False),
'network_mode': default_conf.get('network_mode', 'public').lower(),
'security_level': default_conf.get('security_level', 'normal').lower(),
'db_mode': default_conf.get('db_mode', 'cache').lower(),
'network_mode': default_conf.get('network_mode', NetworkMode.PUBLIC.value).lower(),
'security_level': default_conf.get('security_level', SecurityLevel.NORMAL.value).lower(),
'db_mode': default_conf.get('db_mode', DBMode.CACHE.value).lower(),
}
except Exception:
import importlib.util
# temporary disable `uv` on Windows by default (https://github.com/Comfy-Org/ComfyUI-Manager/issues/1969)
manager_util.use_uv = importlib.util.find_spec("uv") is not None and platform.system() != "Windows"
manager_util.use_uv = False
manager_util.bypass_ssl = False
return {
'http_channel_enabled': False,
'preview_method': manager_funcs.get_current_preview_method(),
'git_exe': '',
'use_uv': manager_util.use_uv,
'use_uv': True,
'channel_url': DEFAULT_CHANNEL,
'default_cache_as_channel_url': False,
'share_option': 'all',
'bypass_ssl': manager_util.bypass_ssl,
'file_logging': True,
'component_policy': 'workflow',
'update_policy': 'stable-comfyui',
'windows_selector_event_loop_policy': False,
'model_download_by_agent': False,
'downgrade_blacklist': '',
'always_lazy_install': False,
'network_mode': 'public', # public | private | offline
'security_level': 'normal', # strong | normal | normal- | weak
'db_mode': 'cache', # local | cache | remote
'network_mode': NetworkMode.PUBLIC.value,
'security_level': SecurityLevel.NORMAL.value,
'db_mode': DBMode.CACHE.value,
}
@ -1808,27 +1715,27 @@ def switch_to_default_branch(repo):
default_branch = repo.git.symbolic_ref(f'refs/remotes/{remote_name}/HEAD').replace(f'refs/remotes/{remote_name}/', '')
repo.git.checkout(default_branch)
return True
except:
except Exception:
# try checkout master
# try checkout main if failed
try:
repo.git.checkout(repo.heads.master)
return True
except:
except Exception:
try:
if remote_name is not None:
repo.git.checkout('-b', 'master', f'{remote_name}/master')
return True
except:
except Exception:
try:
repo.git.checkout(repo.heads.main)
return True
except:
except Exception:
try:
if remote_name is not None:
repo.git.checkout('-b', 'main', f'{remote_name}/main')
return True
except:
except Exception:
pass
print("[ComfyUI Manager] Failed to switch to the default branch")
@ -1836,10 +1743,10 @@ def switch_to_default_branch(repo):
def reserve_script(repo_path, install_cmds):
if not os.path.exists(manager_startup_script_path):
os.makedirs(manager_startup_script_path)
if not os.path.exists(context.manager_startup_script_path):
os.makedirs(context.manager_startup_script_path)
script_path = os.path.join(manager_startup_script_path, "install-scripts.txt")
script_path = os.path.join(context.manager_startup_script_path, "install-scripts.txt")
with open(script_path, "a") as file:
obj = [repo_path] + install_cmds
file.write(f"{obj}\n")
@ -1879,7 +1786,7 @@ def try_install_script(url, repo_path, install_cmd, instant_execution=False):
print(f"[WARN] ComfyUI-Manager: Your ComfyUI version ({comfy_ui_revision})[{comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version.")
print("[WARN] The extension installation feature may not work properly in the current installed ComfyUI version on Windows environment.")
print("###################################################################\n\n")
except:
except Exception:
pass
if code != 0:
@ -1894,11 +1801,11 @@ def try_install_script(url, repo_path, install_cmd, instant_execution=False):
# use subprocess to avoid file system lock by git (Windows)
def __win_check_git_update(path, do_fetch=False, do_update=False):
if do_fetch:
command = [sys.executable, git_script_path, "--fetch", path]
command = [sys.executable, context.git_script_path, "--fetch", path]
elif do_update:
command = [sys.executable, git_script_path, "--pull", path]
command = [sys.executable, context.git_script_path, "--pull", path]
else:
command = [sys.executable, git_script_path, "--check", path]
command = [sys.executable, context.git_script_path, "--check", path]
new_env = get_script_env()
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=get_default_custom_nodes_path(), env=new_env)
@ -1952,7 +1859,7 @@ def __win_check_git_update(path, do_fetch=False, do_update=False):
def __win_check_git_pull(path):
command = [sys.executable, git_script_path, "--pull", path]
command = [sys.executable, context.git_script_path, "--pull", path]
process = subprocess.Popen(command, env=get_script_env(), cwd=get_default_custom_nodes_path())
process.wait()
@ -1968,7 +1875,7 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
else:
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), context.comfy_path, context.manager_files_path)
with open(requirements_path, "r") as requirements_file:
for line in requirements_file:
#handle comments
@ -2000,6 +1907,27 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
return True
def install_manager_requirements(repo_path):
"""
Install packages from manager_requirements.txt if it exists.
This is specifically for ComfyUI's manager_requirements.txt.
"""
manager_requirements_path = os.path.join(repo_path, "manager_requirements.txt")
if not os.path.exists(manager_requirements_path):
return
logging.info("[ComfyUI-Manager] Installing manager_requirements.txt")
with open(manager_requirements_path, "r") as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
if '#' in line:
line = line.split('#')[0].strip()
if line:
install_cmd = manager_util.make_pip_cmd(["install", line])
subprocess.run(install_cmd)
def git_repo_update_check_with(path, do_fetch=False, do_update=False, no_deps=False):
"""
@ -2078,7 +2006,15 @@ def git_repo_update_check_with(path, do_fetch=False, do_update=False, no_deps=Fa
return False, True
try:
remote.pull()
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
backup_name = get_backup_branch_name(repo)
repo.create_head(backup_name)
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
repo.git.submodule('update', '--init', '--recursive')
new_commit_hash = repo.head.commit.hexsha
@ -2135,26 +2071,18 @@ class GitProgress(RemoteProgress):
def is_valid_url(url):
try:
# Check for HTTP/HTTPS URL format
result = urlparse(url)
if all([result.scheme, result.netloc]):
return True
finally:
# Check for SSH git URL format
pattern = re.compile(r"^(.+@|ssh://).+:.+$")
if pattern.match(url):
return True
# Check for HTTP/HTTPS URL format
result = urlparse(url)
if result.scheme and result.netloc:
return True
# Check for SSH git URL format
if SSH_URL_PATTERN.match(url):
return True
return False
def extract_url_and_commit_id(s):
index = s.rfind('@')
if index == -1:
return (s, '')
else:
return (s[:index], s[index+1:])
async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=False):
await unified_manager.reload('cache')
await unified_manager.get_custom_nodes('default', 'cache')
@ -2172,11 +2100,8 @@ async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=
cnr = unified_manager.get_cnr_by_repo(url)
if cnr:
cnr_id = cnr['id']
return await unified_manager.install_by_id(cnr_id, version_spec=None, channel='default', mode='cache')
return await unified_manager.install_by_id(cnr_id, version_spec='nightly', channel='default', mode='cache')
else:
new_url, commit_id = extract_url_and_commit_id(url)
if commit_id != "":
url = new_url
repo_name = os.path.splitext(os.path.basename(url))[0]
# NOTE: Keep original name as possible if unknown node
@ -2204,15 +2129,11 @@ async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=
clone_url = git_utils.get_url_for_clone(url)
if not instant_execution and platform.system() == 'Windows':
res = manager_funcs.run_script([sys.executable, git_script_path, "--clone", get_default_custom_nodes_path(), clone_url, repo_path], cwd=get_default_custom_nodes_path())
res = manager_funcs.run_script([sys.executable, context.git_script_path, "--clone", get_default_custom_nodes_path(), clone_url, repo_path], cwd=get_default_custom_nodes_path())
if res != 0:
return result.fail(f"Failed to clone '{clone_url}' into '{repo_path}'")
else:
repo = git.Repo.clone_from(clone_url, repo_path, recursive=True, progress=GitProgress())
if commit_id!= "":
repo.git.checkout(commit_id)
repo.git.submodule('update', '--init', '--recursive')
repo.git.clear_cache()
repo.close()
@ -2247,9 +2168,17 @@ def git_pull(path):
current_branch = repo.active_branch
remote_name = current_branch.tracking_branch().remote_name
remote = repo.remote(name=remote_name)
branch_name = current_branch.name
try:
repo.git.pull('--ff-only')
except git.GitCommandError:
backup_name = get_backup_branch_name(repo)
repo.create_head(backup_name)
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
remote.pull()
repo.git.submodule('update', '--init', '--recursive')
repo.close()
@ -2275,7 +2204,7 @@ async def get_data_by_mode(mode, filename, channel_url=None):
cache_uri = str(manager_util.simple_hash(uri))+'_'+filename
cache_uri = os.path.join(manager_util.cache_dir, cache_uri)
if get_config()['network_mode'] == 'offline':
if get_config()['network_mode'] == 'offline' or manager_util.is_manager_pip_package():
# offline network mode
if os.path.exists(cache_uri):
json_obj = await manager_util.get_data(cache_uri)
@ -2295,7 +2224,7 @@ async def get_data_by_mode(mode, filename, channel_url=None):
with open(cache_uri, "w", encoding='utf-8') as file:
json.dump(json_obj, file, indent=4, sort_keys=True)
except Exception as e:
print(f"[ComfyUI-Manager] Due to a network error, switching to local mode.\n=> {filename}\n=> {e}")
print(f"[ComfyUI-Manager] Due to a network error, switching to local mode.\n=> {filename} @ {channel_url}/{mode}\n=> {e}")
uri = os.path.join(manager_util.comfyui_manager_path, filename)
json_obj = await manager_util.get_data(uri)
@ -2366,7 +2295,7 @@ def gitclone_uninstall(files):
url = url[:-1]
try:
for custom_nodes_dir in get_custom_nodes_paths():
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_name:str = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_path = os.path.join(custom_nodes_dir, dir_name)
# safety check
@ -2414,7 +2343,7 @@ def gitclone_set_active(files, is_disable):
url = url[:-1]
try:
for custom_nodes_dir in get_custom_nodes_paths():
dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_name:str = os.path.splitext(os.path.basename(url))[0].replace(".git", "")
dir_path = os.path.join(custom_nodes_dir, dir_name)
# safety check
@ -2511,31 +2440,31 @@ def update_to_stable_comfyui(repo_path):
repo = git.Repo(repo_path)
try:
repo.git.checkout(repo.heads.master)
except:
except Exception:
logging.error(f"[ComfyUI-Manager] Failed to checkout 'master' branch.\nrepo_path={repo_path}\nAvailable branches:")
for branch in repo.branches:
logging.error('\t'+branch.name)
return "fail", None
versions, current_tag, _ = get_comfyui_versions(repo)
if len(versions) == 0 or (len(versions) == 1 and versions[0] == 'nightly'):
versions, current_tag, latest_tag = get_comfyui_versions(repo)
if latest_tag is None:
logging.info("[ComfyUI-Manager] Unable to update to the stable ComfyUI version.")
return "fail", None
if versions[0] == 'nightly':
latest_tag = versions[1]
else:
latest_tag = versions[0]
if current_tag == latest_tag:
tag_ref = next((t for t in repo.tags if t.name == latest_tag), None)
if tag_ref is None:
logging.info(f"[ComfyUI-Manager] Unable to locate tag '{latest_tag}' in repository.")
return "fail", None
if repo.head.commit == tag_ref.commit:
return "skip", None
else:
logging.info(f"[ComfyUI-Manager] Updating ComfyUI: {current_tag} -> {latest_tag}")
repo.git.checkout(latest_tag)
repo.git.checkout(tag_ref.name)
execute_install_script("ComfyUI", repo_path, instant_execution=False, no_deps=False)
return 'updated', latest_tag
except:
except Exception:
traceback.print_exc()
return "fail", None
@ -2692,7 +2621,7 @@ async def get_current_snapshot(custom_nodes_only = False):
await unified_manager.get_custom_nodes('default', 'cache')
# Get ComfyUI hash
repo_path = comfy_path
repo_path = context.comfy_path
comfyui_commit_hash = None
if not custom_nodes_only:
@ -2737,7 +2666,7 @@ async def get_current_snapshot(custom_nodes_only = False):
commit_hash = git_utils.get_commit_hash(fullpath)
url = git_utils.git_url(fullpath)
git_custom_nodes[url] = dict(hash=commit_hash, disabled=is_disabled)
except:
except Exception:
print(f"Failed to extract snapshots for the custom node '{path}'.")
elif path.endswith('.py'):
@ -2763,12 +2692,10 @@ async def get_current_snapshot(custom_nodes_only = False):
async def save_snapshot_with_postfix(postfix, path=None, custom_nodes_only = False):
if path is None:
now = datetime.now()
date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
date_time_format = get_timestamp_for_path()
file_name = f"{date_time_format}_{postfix}"
path = os.path.join(manager_snapshot_path, f"{file_name}.json")
path = os.path.join(context.manager_snapshot_path, f"{file_name}.json")
else:
file_name = path.replace('\\', '/').split('/')[-1]
file_name = file_name.split('.')[-2]
@ -2795,7 +2722,7 @@ async def extract_nodes_from_workflow(filepath, mode='local', channel_url='defau
with open(filepath, "r", encoding="UTF-8", errors="ignore") as json_file:
try:
workflow = json.load(json_file)
except:
except Exception:
print(f"Invalid workflow file: {filepath}")
exit(-1)
@ -2808,7 +2735,7 @@ async def extract_nodes_from_workflow(filepath, mode='local', channel_url='defau
else:
try:
workflow = json.loads(img.info['workflow'])
except:
except Exception:
print(f"This is not a valid .png file containing a ComfyUI workflow: {filepath}")
exit(-1)
@ -3079,7 +3006,7 @@ def populate_github_stats(node_packs, json_obj_github):
v['stars'] = -1
v['last_update'] = -1
v['trust'] = False
except:
except Exception:
logging.error(f"[ComfyUI-Manager] DB item is broken:\n{v}")
@ -3356,42 +3283,91 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
def get_comfyui_versions(repo=None):
if repo is None:
repo = git.Repo(comfy_path)
repo = repo or git.Repo(context.comfy_path)
remote_name = None
try:
remote = get_remote_name(repo)
repo.remotes[remote].fetch()
except:
remote_name = get_remote_name(repo)
repo.remotes[remote_name].fetch()
except Exception:
logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI")
versions = [x.name for x in repo.tags if x.name.startswith('v')]
def parse_semver(tag_name):
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', tag_name)
return tuple(int(x) for x in match.groups()) if match else None
# nearest tag
versions = sorted(versions, key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
versions = versions[:4]
def normalize_describe(tag_name):
if not tag_name:
return None
base = tag_name.split('-', 1)[0]
return base if parse_semver(base) else None
current_tag = repo.git.describe('--tags')
# Collect semver tags and sort descending (highest first)
semver_tags = []
for tag in repo.tags:
semver = parse_semver(tag.name)
if semver:
semver_tags.append((semver, tag.name))
semver_tags.sort(key=lambda x: x[0], reverse=True)
semver_tags = [name for _, name in semver_tags]
if current_tag not in versions:
versions = sorted(versions + [current_tag], key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
versions = versions[:4]
latest_tag = semver_tags[0] if semver_tags else None
main_branch = repo.heads.master
latest_commit = main_branch.commit
latest_tag = repo.git.describe('--tags', latest_commit.hexsha)
try:
described = repo.git.describe('--tags')
except Exception:
described = ''
if latest_tag != versions[0]:
versions.insert(0, 'nightly')
else:
versions[0] = 'nightly'
try:
exact_tag = repo.git.describe('--tags', '--exact-match')
except Exception:
exact_tag = ''
head_is_default = False
if remote_name:
try:
default_head_ref = repo.refs[f'{remote_name}/HEAD']
default_commit = default_head_ref.reference.commit
head_is_default = repo.head.commit == default_commit
except Exception:
# Fallback: compare directly with master branch
try:
if 'master' in [h.name for h in repo.heads]:
head_is_default = repo.head.commit == repo.heads.master.commit
except Exception:
head_is_default = False
nearest_semver = normalize_describe(described)
exact_semver = exact_tag if parse_semver(exact_tag) else None
if head_is_default and not exact_tag:
current_tag = 'nightly'
else:
current_tag = exact_tag or described or 'nightly'
# Prepare semver list for display: top 4 plus the current/nearest semver if missing
display_semver_tags = semver_tags[:4]
if exact_semver and exact_semver not in display_semver_tags:
display_semver_tags.append(exact_semver)
elif nearest_semver and nearest_semver not in display_semver_tags:
display_semver_tags.append(nearest_semver)
versions = ['nightly']
if current_tag and not exact_semver and current_tag not in versions and current_tag not in display_semver_tags:
versions.append(current_tag)
for tag in display_semver_tags:
if tag not in versions:
versions.append(tag)
versions = versions[:6]
return versions, current_tag, latest_tag
def switch_comfyui(tag):
repo = git.Repo(comfy_path)
repo = git.Repo(context.comfy_path)
if tag == 'nightly':
repo.git.checkout('master')
@ -3431,5 +3407,5 @@ def repo_switch_commit(repo_path, commit_hash):
repo.git.checkout(commit_hash)
return True
except:
except Exception:
return None

View File

@ -0,0 +1,451 @@
import mimetypes
from ..common import context
from . import manager_core as core
import os
from aiohttp import web
import aiohttp
import json
import hashlib
import folder_paths
from server import PromptServer
import logging
import sys
try:
from nio import AsyncClient, LoginResponse, UploadResponse
matrix_nio_is_available = True
except Exception:
logging.warning(f"[ComfyUI-Manager] The matrix sharing feature has been disabled because the `matrix-nio` dependency is not installed.\n\tTo use this feature, please run the following command:\n\t{sys.executable} -m pip install matrix-nio\n")
matrix_nio_is_available = False
def extract_model_file_names(json_data):
"""Extract unique file names from the input JSON data."""
file_names = set()
model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'}
# Recursively search for file names in the JSON data
def recursive_search(data):
if isinstance(data, dict):
for value in data.values():
recursive_search(value)
elif isinstance(data, list):
for item in data:
recursive_search(item)
elif isinstance(data, str) and '.' in data:
file_names.add(os.path.basename(data)) # file_names.add(data)
recursive_search(json_data)
return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions]
def find_file_paths(base_dir, file_names):
"""Find the paths of the files in the base directory."""
file_paths = {}
for root, dirs, files in os.walk(base_dir):
# Exclude certain directories
dirs[:] = [d for d in dirs if d not in ['.git']]
for file in files:
if file in file_names:
file_paths[file] = os.path.join(root, file)
return file_paths
def compute_sha256_checksum(filepath):
"""Compute the SHA256 checksum of a file, in chunks"""
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return sha256.hexdigest()
@PromptServer.instance.routes.get("/v2/manager/share_option")
async def share_option(request):
if "value" in request.rel_url.query:
core.get_config()['share_option'] = request.rel_url.query['value']
core.write_config()
else:
return web.Response(text=core.get_config()['share_option'], status=200)
return web.Response(status=200)
def get_openart_auth():
if not os.path.exists(os.path.join(context.manager_files_path, ".openart_key")):
return None
try:
with open(os.path.join(context.manager_files_path, ".openart_key"), "r") as f:
openart_key = f.read().strip()
return openart_key if openart_key else None
except Exception:
return None
def get_matrix_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "matrix_auth")):
return None
try:
with open(os.path.join(context.manager_files_path, "matrix_auth"), "r") as f:
matrix_auth = f.read()
homeserver, username, password = matrix_auth.strip().split("\n")
if not homeserver or not username or not password:
return None
return {
"homeserver": homeserver,
"username": username,
"password": password,
}
except Exception:
return None
def get_comfyworkflows_auth():
if not os.path.exists(os.path.join(context.manager_files_path, "comfyworkflows_sharekey")):
return None
try:
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
share_key = f.read()
if not share_key.strip():
return None
return share_key
except Exception:
return None
def get_youml_settings():
if not os.path.exists(os.path.join(context.manager_files_path, ".youml")):
return None
try:
with open(os.path.join(context.manager_files_path, ".youml"), "r") as f:
youml_settings = f.read().strip()
return youml_settings if youml_settings else None
except Exception:
return None
def set_youml_settings(settings):
with open(os.path.join(context.manager_files_path, ".youml"), "w") as f:
f.write(settings)
@PromptServer.instance.routes.get("/v2/manager/get_openart_auth")
async def api_get_openart_auth(request):
# print("Getting stored Matrix credentials...")
openart_key = get_openart_auth()
if not openart_key:
return web.Response(status=404)
return web.json_response({"openart_key": openart_key})
@PromptServer.instance.routes.post("/v2/manager/set_openart_auth")
async def api_set_openart_auth(request):
json_data = await request.json()
openart_key = json_data['openart_key']
with open(os.path.join(context.manager_files_path, ".openart_key"), "w") as f:
f.write(openart_key)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_matrix_auth")
async def api_get_matrix_auth(request):
# print("Getting stored Matrix credentials...")
matrix_auth = get_matrix_auth()
if not matrix_auth:
return web.Response(status=404)
return web.json_response(matrix_auth)
@PromptServer.instance.routes.get("/v2/manager/youml/settings")
async def api_get_youml_settings(request):
youml_settings = get_youml_settings()
if not youml_settings:
return web.Response(status=404)
return web.json_response(json.loads(youml_settings))
@PromptServer.instance.routes.post("/v2/manager/youml/settings")
async def api_set_youml_settings(request):
json_data = await request.json()
set_youml_settings(json.dumps(json_data))
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_comfyworkflows_auth")
async def api_get_comfyworkflows_auth(request):
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
# in the same directory as the ComfyUI base folder
# print("Getting stored Comfyworkflows.com auth...")
comfyworkflows_auth = get_comfyworkflows_auth()
if not comfyworkflows_auth:
return web.Response(status=404)
return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
@PromptServer.instance.routes.post("/v2/manager/set_esheep_workflow_and_images")
async def set_esheep_workflow_and_images(request):
json_data = await request.json()
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
json.dump(json_data, file, indent=4)
return web.Response(status=200)
@PromptServer.instance.routes.get("/v2/manager/get_esheep_workflow_and_images")
async def get_esheep_workflow_and_images(request):
with open(os.path.join(context.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
data = json.load(file)
return web.Response(status=200, text=json.dumps(data))
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
async def get_matrix_dep_status(request):
if matrix_nio_is_available:
return web.Response(status=200, text='available')
else:
return web.Response(status=200, text='unavailable')
def set_matrix_auth(json_data):
homeserver = json_data['homeserver']
username = json_data['username']
password = json_data['password']
with open(os.path.join(context.manager_files_path, "matrix_auth"), "w") as f:
f.write("\n".join([homeserver, username, password]))
def set_comfyworkflows_auth(comfyworkflows_sharekey):
with open(os.path.join(context.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
f.write(comfyworkflows_sharekey)
def has_provided_matrix_auth(matrix_auth):
return matrix_auth['homeserver'].strip() and matrix_auth['username'].strip() and matrix_auth['password'].strip()
def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
return comfyworkflows_sharekey.strip()
@PromptServer.instance.routes.post("/v2/manager/share")
async def share_art(request):
# get json data
json_data = await request.json()
matrix_auth = json_data['matrix_auth']
comfyworkflows_sharekey = json_data['cw_auth']['cw_sharekey']
set_matrix_auth(matrix_auth)
set_comfyworkflows_auth(comfyworkflows_sharekey)
share_destinations = json_data['share_destinations']
credits = json_data['credits']
title = json_data['title']
description = json_data['description']
is_nsfw = json_data['is_nsfw']
prompt = json_data['prompt']
potential_outputs = json_data['potential_outputs']
selected_output_index = json_data['selected_output_index']
try:
output_to_share = potential_outputs[int(selected_output_index)]
except Exception:
# for now, pick the first output
output_to_share = potential_outputs[0]
assert output_to_share['type'] in ('image', 'output')
output_dir = folder_paths.get_output_directory()
if output_to_share['type'] == 'image':
asset_filename = output_to_share['image']['filename']
asset_subfolder = output_to_share['image']['subfolder']
if output_to_share['image']['type'] == 'temp':
output_dir = folder_paths.get_temp_directory()
else:
asset_filename = output_to_share['output']['filename']
asset_subfolder = output_to_share['output']['subfolder']
if asset_subfolder:
asset_filepath = os.path.join(output_dir, asset_subfolder, asset_filename)
else:
asset_filepath = os.path.join(output_dir, asset_filename)
# get the mime type of the asset
assetFileType = mimetypes.guess_type(asset_filepath)[0]
share_website_host = "UNKNOWN"
if "comfyworkflows" in share_destinations:
share_website_host = "https://comfyworkflows.com"
share_endpoint = f"{share_website_host}/api"
# get presigned urls
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.post(
f"{share_endpoint}/get_presigned_urls",
json={
"assetFileName": asset_filename,
"assetFileType": assetFileType,
"workflowJsonFileName": 'workflow.json',
"workflowJsonFileType": 'application/json',
},
) as resp:
assert resp.status == 200
presigned_urls_json = await resp.json()
assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"]
assetFileKey = presigned_urls_json["assetFileKey"]
workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"]
workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"]
# upload asset
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.put(assetFilePresignedUrl, data=open(asset_filepath, "rb")) as resp:
assert resp.status == 200
# upload workflow json
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp:
assert resp.status == 200
model_filenames = extract_model_file_names(prompt['workflow'])
model_file_paths = find_file_paths(folder_paths.base_path, model_filenames)
models_info = {}
for filename, filepath in model_file_paths.items():
models_info[filename] = {
"filename": filename,
"sha256_checksum": compute_sha256_checksum(filepath),
"relative_path": os.path.relpath(filepath, folder_paths.base_path),
}
# make a POST request to /api/upload_workflow with form data key values
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
form = aiohttp.FormData()
if comfyworkflows_sharekey:
form.add_field("shareKey", comfyworkflows_sharekey)
form.add_field("source", "comfyui_manager")
form.add_field("assetFileKey", assetFileKey)
form.add_field("assetFileType", assetFileType)
form.add_field("workflowJsonFileKey", workflowJsonFileKey)
form.add_field("sharedWorkflowWorkflowJsonString", json.dumps(prompt['workflow']))
form.add_field("sharedWorkflowPromptJsonString", json.dumps(prompt['output']))
form.add_field("shareWorkflowCredits", credits)
form.add_field("shareWorkflowTitle", title)
form.add_field("shareWorkflowDescription", description)
form.add_field("shareWorkflowIsNSFW", str(is_nsfw).lower())
form.add_field("currentSnapshot", json.dumps(await core.get_current_snapshot()))
form.add_field("modelsInfo", json.dumps(models_info))
async with session.post(
f"{share_endpoint}/upload_workflow",
data=form,
) as resp:
assert resp.status == 200
upload_workflow_json = await resp.json()
workflowId = upload_workflow_json["workflowId"]
# check if the user has provided Matrix credentials
if matrix_nio_is_available and "matrix" in share_destinations:
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
filename = os.path.basename(asset_filepath)
content_type = assetFileType
try:
homeserver = 'matrix.org'
if matrix_auth:
homeserver = matrix_auth.get('homeserver', 'matrix.org')
homeserver = homeserver.replace("http://", "https://")
if not homeserver.startswith("https://"):
homeserver = "https://" + homeserver
client = AsyncClient(homeserver, matrix_auth['username'])
# Login
login_resp = await client.login(matrix_auth['password'])
if not isinstance(login_resp, LoginResponse) or not login_resp.access_token:
await client.close()
return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
# Upload asset
with open(asset_filepath, 'rb') as f:
upload_resp, _maybe_keys = await client.upload(f, content_type=content_type, filename=filename)
asset_data = f.seek(0) or f.read() # get size for info below
if not isinstance(upload_resp, UploadResponse) or not upload_resp.content_uri:
await client.close()
return web.json_response({"error": "Failed to upload asset to Matrix."}, content_type='application/json', status=500)
mxc_url = upload_resp.content_uri
# Upload workflow JSON
import io
workflow_json_bytes = json.dumps(prompt['workflow']).encode('utf-8')
workflow_io = io.BytesIO(workflow_json_bytes)
upload_workflow_resp, _maybe_keys = await client.upload(workflow_io, content_type='application/json', filename='workflow.json')
workflow_io.seek(0)
if not isinstance(upload_workflow_resp, UploadResponse) or not upload_workflow_resp.content_uri:
await client.close()
return web.json_response({"error": "Failed to upload workflow to Matrix."}, content_type='application/json', status=500)
workflow_json_mxc_url = upload_workflow_resp.content_uri
# Send text message
text_content = ""
if title:
text_content += f"{title}\n"
if description:
text_content += f"{description}\n"
if credits:
text_content += f"\ncredits: {credits}\n"
await client.room_send(
room_id=comfyui_share_room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": text_content}
)
# Send image
await client.room_send(
room_id=comfyui_share_room_id,
message_type="m.room.message",
content={
"msgtype": "m.image",
"body": filename,
"url": mxc_url,
"info": {
"mimetype": content_type,
"size": len(asset_data)
}
}
)
# Send workflow JSON file
await client.room_send(
room_id=comfyui_share_room_id,
message_type="m.room.message",
content={
"msgtype": "m.file",
"body": "workflow.json",
"url": workflow_json_mxc_url,
"info": {
"mimetype": "application/json",
"size": len(workflow_json_bytes)
}
}
)
await client.close()
except:
import traceback
traceback.print_exc()
return web.json_response({"error": "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500)
return web.json_response({
"comfyworkflows": {
"url": None if "comfyworkflows" not in share_destinations else f"{share_website_host}/workflows/{workflowId}",
},
"matrix": {
"success": None if "matrix" not in share_destinations else True
}
}, content_type='application/json', status=200)

View File

@ -12,32 +12,15 @@ import ast
import logging
import traceback
glob_path = os.path.join(os.path.dirname(__file__), "glob")
sys.path.append(glob_path)
import security_check
import manager_util
import cm_global
import manager_downloader
from .common import security_check
from .common import manager_util
from .common import cm_global
from .common import manager_downloader
from .common.timestamp_utils import current_timestamp
import folder_paths
manager_util.add_python_path_to_env()
import datetime as dt
if hasattr(dt, 'datetime'):
from datetime import datetime as dt_datetime
def current_timestamp():
return dt_datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
else:
# NOTE: Occurs in some Mac environments.
import time
logging.error(f"[ComfyUI-Manager] fallback timestamp mode\n datetime module is invalid: '{dt.__file__}'")
def current_timestamp():
return str(time.time()).split('.')[0]
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
@ -66,16 +49,14 @@ def is_import_failed_extension(name):
comfy_path = os.environ.get('COMFYUI_PATH')
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
if comfy_path is None:
# legacy env var
comfy_path = os.environ.get('COMFYUI_PATH')
if comfy_path is None:
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
os.environ['COMFYUI_PATH'] = comfy_path
if comfy_base_path is None:
comfy_base_path = comfy_path
sys.__comfyui_manager_register_message_collapse = register_message_collapse
sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension
cm_global.register_api('cm.register_message_collapse', register_message_collapse)
@ -85,15 +66,12 @@ cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extensi
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
custom_nodes_base_path = folder_paths.get_folder_paths('custom_nodes')[0]
manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-Manager'))
manager_files_path = folder_paths.get_system_user_directory("manager")
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list")
restore_snapshot_path = os.path.join(manager_files_path, "startup-scripts", "restore-snapshot.json")
manager_config_path = os.path.join(manager_files_path, 'config.ini')
cm_cli_path = os.path.join(comfyui_manager_path, "cm-cli.py")
default_conf = {}
def read_config():
@ -400,7 +378,11 @@ try:
def emit(self, record):
global is_start_mode
message = record.getMessage()
try:
message = record.getMessage()
except Exception as e:
message = f"<<logging error>>: {record} - {e}"
original_stderr.write(message)
if is_start_mode:
match = re.search(pat_import_fail, message)
@ -443,35 +425,6 @@ except Exception as e:
print(f"[ComfyUI-Manager] Logging failed: {e}")
def ensure_dependencies():
try:
import git # noqa: F401
import toml # noqa: F401
import rich # noqa: F401
import chardet # noqa: F401
except ModuleNotFoundError:
my_path = os.path.dirname(__file__)
requirements_path = os.path.join(my_path, "requirements.txt")
print("## ComfyUI-Manager: installing dependencies. (GitPython)")
try:
subprocess.check_output(manager_util.make_pip_cmd(['install', '-r', requirements_path]))
except subprocess.CalledProcessError:
print("## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.")
try:
subprocess.check_output(manager_util.make_pip_cmd(['install', '--user', '-r', requirements_path]))
except subprocess.CalledProcessError:
print("## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%3Amatrix.org)")
try:
print("## ComfyUI-Manager: installing dependencies done.")
except:
# maybe we should sys.exit() here? there is at least two screens worth of error messages still being pumped after our error messages
print("## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed")
ensure_dependencies()
print("** ComfyUI startup time:", current_timestamp())
print("** Platform:", platform.system())
print("** Python version:", sys.version)
@ -495,7 +448,7 @@ def read_downgrade_blacklist():
items = [x.strip() for x in items if x != '']
cm_global.pip_downgrade_blacklist += items
cm_global.pip_downgrade_blacklist = list(set(cm_global.pip_downgrade_blacklist))
except:
except Exception:
pass
@ -516,7 +469,7 @@ check_bypass_ssl()
# Perform install
processed_install = set()
script_list_path = os.path.join(folder_paths.user_directory, "default", "ComfyUI-Manager", "startup-scripts", "install-scripts.txt")
script_list_path = os.path.join(manager_files_path, "startup-scripts", "install-scripts.txt")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
@ -601,7 +554,10 @@ if os.path.exists(restore_snapshot_path):
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
cmd_str = [sys.executable, cm_cli_path, 'restore-snapshot', restore_snapshot_path]
if 'COMFYUI_PATH' not in new_env:
new_env['COMFYUI_PATH'] = os.path.dirname(folder_paths.__file__)
cmd_str = [sys.executable, '-m', 'comfyui_manager.cm_cli', 'restore-snapshot', restore_snapshot_path]
exit_code = process_wrap(cmd_str, custom_nodes_base_path, handler=msg_capture, env=new_env)
if exit_code != 0:

View File

@ -1,812 +0,0 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { sleep, show_message, customConfirm, customAlert } from "./common.js";
import { GroupNodeConfig, GroupNodeHandler } from "../../extensions/core/groupNode.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
const SEPARATOR = ">"
let pack_map = {};
let rpack_map = {};
export function getPureName(node) {
// group nodes/
let category = null;
if(node.category) {
category = node.category.substring(12);
}
else {
category = node.constructor.category?.substring(12);
}
if(category) {
let purename = node.comfyClass.substring(category.length+1);
return purename;
}
else if(node.comfyClass.startsWith('workflow/') || node.comfyClass.startsWith(`workflow${SEPARATOR}`)) {
return node.comfyClass.substring(9);
}
else {
return node.comfyClass;
}
}
function isValidVersionString(version) {
const versionPattern = /^(\d+)\.(\d+)(\.(\d+))?$/;
const match = version.match(versionPattern);
return match !== null &&
parseInt(match[1], 10) >= 0 &&
parseInt(match[2], 10) >= 0 &&
(!match[3] || parseInt(match[4], 10) >= 0);
}
function register_pack_map(name, data) {
if(data.packname) {
pack_map[data.packname] = name;
rpack_map[name] = data;
}
else {
rpack_map[name] = data;
}
}
function storeGroupNode(name, data, register=true) {
let extra = app.graph.extra;
if (!extra) app.graph.extra = extra = {};
let groupNodes = extra.groupNodes;
if (!groupNodes) extra.groupNodes = groupNodes = {};
groupNodes[name] = data;
if(register) {
register_pack_map(name, data);
}
}
export async function load_components() {
let data = await api.fetchApi('/manager/component/loads', {method: "POST"});
let components = await data.json();
let start_time = Date.now();
let failed = [];
let failed2 = [];
for(let name in components) {
if(app.graph.extra?.groupNodes?.[name]) {
if(data) {
let data = components[name];
let category = data.packname;
if(data.category) {
category += SEPARATOR + data.category;
}
if(category == '') {
category = 'components';
}
const config = new GroupNodeConfig(name, data);
await config.registerType(category);
register_pack_map(name, data);
continue;
}
}
let nodeData = components[name];
storeGroupNode(name, nodeData);
const config = new GroupNodeConfig(name, nodeData);
while(true) {
try {
let category = nodeData.packname;
if(nodeData.category) {
category += SEPARATOR + nodeData.category;
}
if(category == '') {
category = 'components';
}
await config.registerType(category);
register_pack_map(name, nodeData);
break;
}
catch {
let elapsed_time = Date.now() - start_time;
if (elapsed_time > 5000) {
failed.push(name);
break;
} else {
await sleep(100);
}
}
}
}
// fallback1
for(let i in failed) {
let name = failed[i];
if(app.graph.extra?.groupNodes?.[name]) {
continue;
}
let nodeData = components[name];
storeGroupNode(name, nodeData);
const config = new GroupNodeConfig(name, nodeData);
while(true) {
try {
let category = nodeData.packname;
if(nodeData.workflow.category) {
category += SEPARATOR + nodeData.category;
}
if(category == '') {
category = 'components';
}
await config.registerType(category);
register_pack_map(name, nodeData);
break;
}
catch {
let elapsed_time = Date.now() - start_time;
if (elapsed_time > 10000) {
failed2.push(name);
break;
} else {
await sleep(100);
}
}
}
}
// fallback2
for(let name in failed2) {
let name = failed2[i];
let nodeData = components[name];
storeGroupNode(name, nodeData);
const config = new GroupNodeConfig(name, nodeData);
while(true) {
try {
let category = nodeData.workflow.packname;
if(nodeData.workflow.category) {
category += SEPARATOR + nodeData.category;
}
if(category == '') {
category = 'components';
}
await config.registerType(category);
register_pack_map(name, nodeData);
break;
}
catch {
let elapsed_time = Date.now() - start_time;
if (elapsed_time > 30000) {
failed.push(name);
break;
} else {
await sleep(100);
}
}
}
}
}
async function save_as_component(node, version, author, prefix, nodename, packname, category) {
let component_name = `${prefix}::${nodename}`;
let subgraph = app.graph.extra?.groupNodes?.[component_name];
if(!subgraph) {
subgraph = app.graph.extra?.groupNodes?.[getPureName(node)];
}
subgraph.version = version;
subgraph.author = author;
subgraph.datetime = Date.now();
subgraph.packname = packname;
subgraph.category = category;
let body =
{
name: component_name,
workflow: subgraph
};
pack_map[packname] = component_name;
rpack_map[component_name] = subgraph;
const res = await api.fetchApi('/manager/component/save', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if(res.status == 200) {
storeGroupNode(component_name, subgraph);
const config = new GroupNodeConfig(component_name, subgraph);
let category = body.workflow.packname;
if(body.workflow.category) {
category += SEPARATOR + body.workflow.category;
}
if(category == '') {
category = 'components';
}
await config.registerType(category);
let path = await res.text();
show_message(`Component '${component_name}' is saved into:\n${path}`);
}
else
show_message(`Failed to save component.`);
}
async function import_component(component_name, component, mode) {
if(mode) {
let body =
{
name: component_name,
workflow: component
};
const res = await api.fetchApi('/manager/component/save', {
method: "POST",
headers: { "Content-Type": "application/json", },
body: JSON.stringify(body)
});
}
let category = component.packname;
if(component.category) {
category += SEPARATOR + component.category;
}
if(category == '') {
category = 'components';
}
storeGroupNode(component_name, component);
const config = new GroupNodeConfig(component_name, component);
await config.registerType(category);
}
function restore_to_loaded_component(component_name) {
if(rpack_map[component_name]) {
let component = rpack_map[component_name];
storeGroupNode(component_name, component, false);
const config = new GroupNodeConfig(component_name, component);
config.registerType(component.category);
}
}
// Using a timestamp prevents duplicate pastes and ensures the prevention of re-deletion of litegrapheditor_clipboard.
let last_paste_timestamp = null;
function versionCompare(v1, v2) {
let ver1;
let ver2;
if(v1 && v1 != '') {
ver1 = v1.split('.');
ver1[0] = parseInt(ver1[0]);
ver1[1] = parseInt(ver1[1]);
if(ver1.length == 2)
ver1.push(0);
else
ver1[2] = parseInt(ver2[2]);
}
else {
ver1 = [0,0,0];
}
if(v2 && v2 != '') {
ver2 = v2.split('.');
ver2[0] = parseInt(ver2[0]);
ver2[1] = parseInt(ver2[1]);
if(ver2.length == 2)
ver2.push(0);
else
ver2[2] = parseInt(ver2[2]);
}
else {
ver2 = [0,0,0];
}
if(ver1[0] > ver2[0])
return -1;
else if(ver1[0] < ver2[0])
return 1;
if(ver1[1] > ver2[1])
return -1;
else if(ver1[1] < ver2[1])
return 1;
if(ver1[2] > ver2[2])
return -1;
else if(ver1[2] < ver2[2])
return 1;
return 0;
}
function checkVersion(name, component) {
let msg = '';
if(rpack_map[name]) {
let old_version = rpack_map[name].version;
if(!old_version || old_version == '') {
msg = ` '${name}' Upgrade (V0.0 -> V${component.version})`;
}
else {
let c = versionCompare(old_version, component.version);
if(c < 0) {
msg = ` '${name}' Downgrade (V${old_version} -> V${component.version})`;
}
else if(c > 0) {
msg = ` '${name}' Upgrade (V${old_version} -> V${component.version})`;
}
else {
msg = ` '${name}' Same version (V${component.version})`;
}
}
}
else {
msg = `'${name}' NEW (V${component.version})`;
}
return msg;
}
async function handle_import_components(components) {
let msg = 'Components:\n';
let cnt = 0;
for(let name in components) {
let component = components[name];
let v = checkVersion(name, component);
if(cnt < 10) {
msg += v + '\n';
}
else if (cnt == 10) {
msg += '...\n';
}
else {
// do nothing
}
cnt++;
}
let last_name = null;
msg += '\nWill you load components?\n';
const confirmed = await customConfirm(msg);
if(confirmed) {
const mode = await customConfirm('\nWill you save components?\n(cancel=load without save)');
for(let name in components) {
let component = components[name];
import_component(name, component, mode);
last_name = name;
}
if(mode) {
show_message('Components are saved.');
}
else {
show_message('Components are loaded.');
}
}
if(cnt == 1 && last_name) {
const node = LiteGraph.createNode(`workflow${SEPARATOR}${last_name}`);
node.pos = [app.canvas.graph_mouse[0], app.canvas.graph_mouse[1]];
app.canvas.graph.add(node, false);
}
}
async function handlePaste(e) {
let data = (e.clipboardData || window.clipboardData);
const items = data.items;
for(const item of items) {
if(item.kind == 'string' && item.type == 'text/plain') {
data = data.getData("text/plain");
try {
let json_data = JSON.parse(data);
if(json_data.kind == 'ComfyUI Components' && last_paste_timestamp != json_data.timestamp) {
last_paste_timestamp = json_data.timestamp;
await handle_import_components(json_data.components);
// disable paste node
localStorage.removeItem("litegrapheditor_clipboard", null);
}
else {
console.log('This components are already pasted: ignored');
}
}
catch {
// nothing to do
}
}
}
}
document.addEventListener("paste", handlePaste);
export class ComponentBuilderDialog extends ComfyDialog {
constructor() {
super();
}
clear() {
while (this.element.children.length) {
this.element.removeChild(this.element.children[0]);
}
}
show() {
this.invalidateControl();
this.element.style.display = "block";
this.element.style.zIndex = 1099;
this.element.style.width = "500px";
this.element.style.height = "480px";
}
invalidateControl() {
this.clear();
let self = this;
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => self.close() });
this.save_button = $el("button",
{ id: "cm-save-button", type: "button", textContent: "Save", onclick: () =>
{
save_as_component(self.target_node, self.version_string.value.trim(), self.author.value.trim(), self.node_prefix.value.trim(),
self.getNodeName(), self.getPackName(), self.category.value.trim());
}
});
let default_nodename = getPureName(this.target_node).trim();
let groupNode = app.graph.extra.groupNodes[default_nodename];
let default_packname = groupNode.packname;
if(!default_packname) {
default_packname = '';
}
let default_category = groupNode.category;
if(!default_category) {
default_category = '';
}
this.default_ver = groupNode.version;
if(!this.default_ver) {
this.default_ver = '0.0';
}
let default_author = groupNode.author;
if(!default_author) {
default_author = '';
}
let delimiterIndex = default_nodename.indexOf('::');
let default_prefix = "";
if(delimiterIndex != -1) {
default_prefix = default_nodename.substring(0, delimiterIndex);
default_nodename = default_nodename.substring(delimiterIndex + 2);
}
if(!default_prefix) {
this.save_button.disabled = true;
}
this.pack_list = this.createPackListCombo();
let version_string = this.createLabeledInput('input version (e.g. 1.0)', '*Version : ', this.default_ver);
this.version_string = version_string[1];
this.version_string.disabled = true;
let author = this.createLabeledInput('input author (e.g. Dr.Lt.Data)', 'Author : ', default_author);
this.author = author[1];
let node_prefix = this.createLabeledInput('input node prefix (e.g. mypack)', '*Prefix : ', default_prefix);
this.node_prefix = node_prefix[1];
let manual_nodename = this.createLabeledInput('input node name (e.g. MAKE_BASIC_PIPE)', 'Nodename : ', default_nodename);
this.manual_nodename = manual_nodename[1];
let manual_packname = this.createLabeledInput('input pack name (e.g. mypack)', 'Packname : ', default_packname);
this.manual_packname = manual_packname[1];
let category = this.createLabeledInput('input category (e.g. util/pipe)', 'Category : ', default_category);
this.category = category[1];
this.node_label = this.createNodeLabel();
let author_mode = this.createAuthorModeCheck();
this.author_mode = author_mode[0];
const content =
$el("div.comfy-modal-content",
[
$el("tr.cm-title", {}, [
$el("font", {size:6, color:"white"}, [`ComfyUI-Manager: Component Builder`])]
),
$el("br", {}, []),
$el("div.cm-menu-container",
[
author_mode[0],
author_mode[1],
category[0],
author[0],
node_prefix[0],
manual_nodename[0],
manual_packname[0],
version_string[0],
this.pack_list,
$el("br", {}, []),
this.node_label
]),
$el("br", {}, []),
this.save_button,
close_button,
]
);
content.style.width = '100%';
content.style.height = '100%';
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
}
validateInput() {
let msg = "";
if(!isValidVersionString(this.version_string.value)) {
msg += 'Invalid version string: '+event.value+"\n";
}
if(this.node_prefix.value.trim() == '') {
msg += 'Node prefix cannot be empty\n';
}
if(this.manual_nodename.value.trim() == '') {
msg += 'Node name cannot be empty\n';
}
if(msg != '') {
// alert(msg);
}
this.save_button.disabled = msg != "";
}
getPackName() {
if(this.pack_list.selectedIndex == 0) {
return this.manual_packname.value.trim();
}
return this.pack_list.value.trim();
}
getNodeName() {
if(this.manual_nodename.value.trim() != '') {
return this.manual_nodename.value.trim();
}
return getPureName(this.target_node);
}
createAuthorModeCheck() {
let check = $el("input",{type:'checkbox', id:"author-mode"},[])
const check_label = $el("label",{for:"author-mode"},["Enable author mode"]);
check_label.style.color = "var(--fg-color)";
check_label.style.cursor = "pointer";
check.checked = false;
let self = this;
check.onchange = () => {
self.version_string.disabled = !check.checked;
if(!check.checked) {
self.version_string.value = self.default_ver;
}
else {
customAlert('If you are not the author, it is not recommended to change the version, as it may cause component update issues.');
}
};
return [check, check_label];
}
createNodeLabel() {
let label = $el('p');
label.className = 'cb-node-label';
if(this.target_node.comfyClass.includes('::'))
label.textContent = getPureName(this.target_node);
else
label.textContent = " _::" + getPureName(this.target_node);
return label;
}
createLabeledInput(placeholder, label, value) {
let textbox = $el('input.cb-widget-input', {type:'text', placeholder:placeholder, value:value}, []);
let self = this;
textbox.onchange = () => {
this.validateInput.call(self);
this.node_label.textContent = this.node_prefix.value + "::" + this.manual_nodename.value;
}
let row = $el('span.cb-widget', {}, [ $el('span.cb-widget-input-label', label), textbox]);
return [row, textbox];
}
createPackListCombo() {
let combo = document.createElement("select");
combo.className = "cb-widget";
let default_packname_option = { value: '##manual', text: 'Packname: Manual' };
combo.appendChild($el('option', default_packname_option, []));
for(let name in pack_map) {
combo.appendChild($el('option', { value: name, text: 'Packname: '+ name }, []));
}
let self = this;
combo.onchange = function () {
if(combo.selectedIndex == 0) {
self.manual_packname.disabled = false;
}
else {
self.manual_packname.disabled = true;
}
};
return combo;
}
}
let orig_handleFile = app.handleFile;
async function handleFile(file) {
if (file.name?.endsWith(".json") || file.name?.endsWith(".pack")) {
const reader = new FileReader();
reader.onload = async () => {
let is_component = false;
const jsonContent = JSON.parse(reader.result);
for(let name in jsonContent) {
let cand = jsonContent[name];
is_component = cand.datetime && cand.version;
break;
}
if(is_component) {
await handle_import_components(jsonContent);
}
else {
orig_handleFile.call(app, file);
}
};
reader.readAsText(file);
return;
}
orig_handleFile.call(app, file);
}
app.handleFile = handleFile;
let current_component_policy = 'workflow';
try {
api.fetchApi('/manager/policy/component')
.then(response => response.text())
.then(data => { current_component_policy = data; });
}
catch {}
function getChangedVersion(groupNodes) {
if(!Object.keys(pack_map).length || !groupNodes)
return null;
let res = {};
for(let component_name in groupNodes) {
let data = groupNodes[component_name];
if(rpack_map[component_name]) {
let v = versionCompare(data.version, rpack_map[component_name].version);
res[component_name] = v;
}
}
return res;
}
const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () {
if(arguments.length == 0)
return await loadGraphData.apply(this, arguments);
let graphData = arguments[0];
let groupNodes = graphData.extra?.groupNodes;
let res = getChangedVersion(groupNodes);
if(res) {
let target_components = null;
switch(current_component_policy) {
case 'higher':
target_components = Object.keys(res).filter(key => res[key] == 1);
break;
case 'mine':
target_components = Object.keys(res);
break;
default:
// do nothing
}
if(target_components) {
for(let i in target_components) {
let component_name = target_components[i];
let component = rpack_map[component_name];
if(component && graphData.extra?.groupNodes) {
graphData.extra.groupNodes[component_name] = component;
}
}
}
}
else {
console.log('Empty components: policy ignored');
}
arguments[0] = graphData;
return await loadGraphData.apply(this, arguments);
};
export function set_component_policy(v) {
current_component_policy = v;
}
let graphToPrompt = app.graphToPrompt;
app.graphToPrompt = async function () {
let p = await graphToPrompt.call(app);
try {
let groupNodes = p.workflow.extra?.groupNodes;
if(groupNodes) {
p.workflow.extra = { ... p.workflow.extra};
// get used group nodes
let used_group_nodes = new Set();
for(let node of p.workflow.nodes) {
if(node.type.startsWith(`workflow/`) || node.type.startsWith(`workflow${SEPARATOR}`)) {
used_group_nodes.add(node.type.substring(9));
}
}
// remove unused group nodes
let new_groupNodes = {};
for (let key in p.workflow.extra.groupNodes) {
if (used_group_nodes.has(key)) {
new_groupNodes[key] = p.workflow.extra.groupNodes[key];
}
}
p.workflow.extra.groupNodes = new_groupNodes;
}
}
catch(e) {
console.log(`Failed to filtering group nodes: ${e}`);
}
return p;
}

View File

@ -1,373 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "aaaaaaaaaa"
},
"source": [
"Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "bbbbbbbbbb"
},
"outputs": [],
"source": [
"# #@title Environment Setup\n",
"\n",
"from pathlib import Path\n",
"\n",
"OPTIONS = {}\n",
"\n",
"USE_GOOGLE_DRIVE = True #@param {type:\"boolean\"}\n",
"UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n",
"USE_COMFYUI_MANAGER = True #@param {type:\"boolean\"}\n",
"INSTALL_CUSTOM_NODES_DEPENDENCIES = True #@param {type:\"boolean\"}\n",
"OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
"OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n",
"OPTIONS['USE_COMFYUI_MANAGER'] = USE_COMFYUI_MANAGER\n",
"OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES'] = INSTALL_CUSTOM_NODES_DEPENDENCIES\n",
"\n",
"current_dir = !pwd\n",
"WORKSPACE = f\"{current_dir[0]}/ComfyUI\"\n",
"\n",
"if OPTIONS['USE_GOOGLE_DRIVE']:\n",
" !echo \"Mounting Google Drive...\"\n",
" %cd /\n",
"\n",
" from google.colab import drive\n",
" drive.mount('/content/drive')\n",
"\n",
" WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n",
" %cd /content/drive/MyDrive\n",
"\n",
"![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
"%cd $WORKSPACE\n",
"\n",
"if OPTIONS['UPDATE_COMFY_UI']:\n",
" !echo -= Updating ComfyUI =-\n",
"\n",
" # Correction of the issue of permissions being deleted on Google Drive.\n",
" ![ -f \".ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\n",
" ![ -f \".ci/nightly/windows_base_files/run_nvidia_gpu.bat\" ] && chmod 755 .ci/nightly/windows_base_files/run_nvidia_gpu.bat\n",
" ![ -f \".ci/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows/update_comfyui_and_python_dependencies.bat\n",
" ![ -f \".ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\n",
" ![ -f \".ci/update_windows/update.py\" ] && chmod 755 .ci/update_windows/update.py\n",
" ![ -f \".ci/update_windows/update_comfyui.bat\" ] && chmod 755 .ci/update_windows/update_comfyui.bat\n",
" ![ -f \".ci/update_windows/README_VERY_IMPORTANT.txt\" ] && chmod 755 .ci/update_windows/README_VERY_IMPORTANT.txt\n",
" ![ -f \".ci/update_windows/run_cpu.bat\" ] && chmod 755 .ci/update_windows/run_cpu.bat\n",
" ![ -f \".ci/update_windows/run_nvidia_gpu.bat\" ] && chmod 755 .ci/update_windows/run_nvidia_gpu.bat\n",
"\n",
" !git pull\n",
"\n",
"!echo -= Install dependencies =-\n",
"!pip3 install accelerate\n",
"!pip3 install einops transformers>=4.28.1 safetensors>=0.4.2 aiohttp pyyaml Pillow scipy tqdm psutil tokenizers>=0.13.3\n",
"!pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121\n",
"!pip3 install torchsde\n",
"!pip3 install kornia>=0.7.1 spandrel soundfile sentencepiece\n",
"\n",
"if OPTIONS['USE_COMFYUI_MANAGER']:\n",
" %cd custom_nodes\n",
"\n",
" # Correction of the issue of permissions being deleted on Google Drive.\n",
" ![ -f \"ComfyUI-Manager/check.sh\" ] && chmod 755 ComfyUI-Manager/check.sh\n",
" ![ -f \"ComfyUI-Manager/scan.sh\" ] && chmod 755 ComfyUI-Manager/scan.sh\n",
" ![ -f \"ComfyUI-Manager/node_db/dev/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/dev/scan.sh\n",
" ![ -f \"ComfyUI-Manager/node_db/tutorial/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/tutorial/scan.sh\n",
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\n",
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\n",
"\n",
" ![ ! -d ComfyUI-Manager ] && echo -= Initial setup ComfyUI-Manager =- && git clone https://github.com/ltdrdata/ComfyUI-Manager\n",
" %cd ComfyUI-Manager\n",
" !git pull\n",
"\n",
"%cd $WORKSPACE\n",
"\n",
"if OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES']:\n",
" !echo -= Install custom nodes dependencies =-\n",
" !pip install GitPython\n",
" !python custom_nodes/ComfyUI-Manager/cm-cli.py restore-dependencies\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "cccccccccc"
},
"source": [
"Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "dddddddddd"
},
"outputs": [],
"source": [
"# Checkpoints\n",
"\n",
"### SDXL\n",
"### I recommend these workflow examples: https://comfyanonymous.github.io/ComfyUI_examples/sdxl/\n",
"\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors -P ./models/checkpoints/\n",
"\n",
"# SDXL ReVision\n",
"#!wget -c https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors -P ./models/clip_vision/\n",
"\n",
"# SD1.5\n",
"!wget -c https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -P ./models/checkpoints/\n",
"\n",
"# SD2\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors -P ./models/checkpoints/\n",
"\n",
"# Some SD1.5 anime style\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1_orangemixs.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A3_orangemixs.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/Linaqruf/anything-v3.0/resolve/main/anything-v3-fp16-pruned.safetensors -P ./models/checkpoints/\n",
"\n",
"# Waifu Diffusion 1.5 (anime style SD2.x 768-v)\n",
"#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors -P ./models/checkpoints/\n",
"\n",
"\n",
"# unCLIP models\n",
"#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
"\n",
"\n",
"# VAE\n",
"!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",
"#!wget -c https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt -P ./models/vae/\n",
"\n",
"\n",
"# Loras\n",
"#!wget -c https://civitai.com/api/download/models/10350 -O ./models/loras/theovercomer8sContrastFix_sd21768.safetensors #theovercomer8sContrastFix SD2.x 768-v\n",
"#!wget -c https://civitai.com/api/download/models/10638 -O ./models/loras/theovercomer8sContrastFix_sd15.safetensors #theovercomer8sContrastFix SD1.x\n",
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors -P ./models/loras/ #SDXL offset noise lora\n",
"\n",
"\n",
"# T2I-Adapter\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_seg_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_sketch_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_keypose_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_openpose_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_color_sd14v1.pth -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_canny_sd14v1.pth -P ./models/controlnet/\n",
"\n",
"# T2I Styles Model\n",
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_style_sd14v1.pth -P ./models/style_models/\n",
"\n",
"# CLIPVision model (needed for styles model)\n",
"#!wget -c https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/pytorch_model.bin -O ./models/clip_vision/clip_vit14.bin\n",
"\n",
"\n",
"# ControlNet\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11f1p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n",
"\n",
"# ControlNet SDXL\n",
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors -P ./models/controlnet/\n",
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors -P ./models/controlnet/\n",
"\n",
"# Controlnet Preprocessor nodes by Fannovel16\n",
"#!cd custom_nodes && git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors; cd comfy_controlnet_preprocessors && python install.py\n",
"\n",
"\n",
"# GLIGEN\n",
"#!wget -c https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/resolve/main/gligen_sd14_textbox_pruned_fp16.safetensors -P ./models/gligen/\n",
"\n",
"\n",
"# ESRGAN upscale model\n",
"#!wget -c https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ./models/upscale_models/\n",
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.pth -P ./models/upscale_models/\n",
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "kkkkkkkkkkkkkkk"
},
"source": [
"### Run ComfyUI with cloudflared (Recommended Way)\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "jjjjjjjjjjjjjj"
},
"outputs": [],
"source": [
"!wget -P ~ https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
"!dpkg -i ~/cloudflared-linux-amd64.deb\n",
"\n",
"import subprocess\n",
"import threading\n",
"import time\n",
"import socket\n",
"import urllib.request\n",
"\n",
"def iframe_thread(port):\n",
" while True:\n",
" time.sleep(0.5)\n",
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
" result = sock.connect_ex(('127.0.0.1', port))\n",
" if result == 0:\n",
" break\n",
" sock.close()\n",
" print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n",
"\n",
" p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
" for line in p.stderr:\n",
" l = line.decode()\n",
" if \"trycloudflare.com \" in l:\n",
" print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n",
" #print(l, end='')\n",
"\n",
"\n",
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n",
"!python main.py --dont-print-server"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "kkkkkkkkkkkkkk"
},
"source": [
"### Run ComfyUI with localtunnel\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "jjjjjjjjjjjjj"
},
"outputs": [],
"source": [
"!npm install -g localtunnel\n",
"\n",
"import subprocess\n",
"import threading\n",
"import time\n",
"import socket\n",
"import urllib.request\n",
"\n",
"def iframe_thread(port):\n",
" while True:\n",
" time.sleep(0.5)\n",
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
" result = sock.connect_ex(('127.0.0.1', port))\n",
" if result == 0:\n",
" break\n",
" sock.close()\n",
" print(\"\\nComfyUI finished loading, trying to launch localtunnel (if it gets stuck here localtunnel is having issues)\\n\")\n",
"\n",
" print(\"The password/enpoint ip for localtunnel is:\", urllib.request.urlopen('https://ipv4.icanhazip.com').read().decode('utf8').strip(\"\\n\"))\n",
" p = subprocess.Popen([\"lt\", \"--port\", \"{}\".format(port)], stdout=subprocess.PIPE)\n",
" for line in p.stdout:\n",
" print(line.decode(), end='')\n",
"\n",
"\n",
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n",
"!python main.py --dont-print-server"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "gggggggggg"
},
"source": [
"### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n",
"\n",
"You should see the ui appear in an iframe. If you get a 403 error, it's your firefox settings or an extension that's messing things up.\n",
"\n",
"If you want to open it in another window use the link.\n",
"\n",
"Note that some UI features like live image previews won't work because the colab iframe blocks websockets."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "hhhhhhhhhh"
},
"outputs": [],
"source": [
"import threading\n",
"import time\n",
"import socket\n",
"def iframe_thread(port):\n",
" while True:\n",
" time.sleep(0.5)\n",
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
" result = sock.connect_ex(('127.0.0.1', port))\n",
" if result == 0:\n",
" break\n",
" sock.close()\n",
" from google.colab import output\n",
" output.serve_kernel_port_as_iframe(port, height=1024)\n",
" print(\"to open it in a window you can open this link here:\")\n",
" output.serve_kernel_port_as_window(port)\n",
"\n",
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n",
"!python main.py --dont-print-server"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"provenance": []
},
"gpuClass": "standard",
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,65 @@
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "comfyui-manager"
license = { text = "GPL-3.0-only" }
version = "4.0.3"
requires-python = ">= 3.9"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
version = "3.37.2"
license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]
readme = "README.md"
keywords = ["comfyui", "comfyui-manager"]
maintainers = [
{ name = "Dr.Lt.Data", email = "dr.lt.data@gmail.com" },
{ name = "Yoland Yan", email = "yoland@comfy.org" },
{ name = "James Kwon", email = "hongilkwon316@gmail.com" },
{ name = "Robin Huang", email = "robin@comfy.org" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
]
dependencies = [
"GitPython",
"PyGithub",
# "matrix-nio",
"transformers",
"huggingface-hub>0.20",
"typer",
"rich",
"typing-extensions",
"toml",
"uv",
"chardet"
]
[project.optional-dependencies]
dev = ["pre-commit", "pytest", "ruff", "pytest-cov"]
[project.urls]
Repository = "https://github.com/ltdrdata/ComfyUI-Manager"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "drltdata"
DisplayName = "ComfyUI-Manager"
Icon = ""
[tool.setuptools.packages.find]
where = ["."]
include = ["comfyui_manager*"]
[project.scripts]
cm-cli = "comfyui_manager.cm_cli.__main__:main"
[tool.ruff]
line-length = 120
target-version = "py39"
[tool.ruff.lint]
select = [
"E4", # default
"E7", # default
"E9", # default
"F", # default
"I", # isort-like behavior (import statement sorting)
]

View File

@ -1,6 +1,6 @@
GitPython
PyGithub
matrix-nio
# matrix-nio
transformers
huggingface-hub
typer

View File

@ -9,4 +9,4 @@ lint.select = [
"F",
]
exclude = ["*.ipynb"]
exclude = ["*.ipynb", "tests"]

View File

@ -131,7 +131,7 @@ def extract_nodes(code_text):
return s
else:
return set()
except:
except Exception:
return set()
@ -609,10 +609,10 @@ def update_custom_nodes(scan_only_mode=False, url_list_file=None):
if name.endswith(".py"):
node_info[name] = (url, title, preemptions, node_pattern)
try:
download_url(url, temp_dir)
except:
print(f"[ERROR] Cannot download '{url}'")
try:
download_url(url, temp_dir)
except Exception:
print(f"[ERROR] Cannot download '{url}'")
with concurrent.futures.ThreadPoolExecutor(10) as executor:
executor.map(download_and_store_info, py_url_titles_and_pattern)

View File

@ -1,39 +0,0 @@
import os
import subprocess
def get_enabled_subdirectories_with_files(base_directory):
subdirs_with_files = []
for subdir in os.listdir(base_directory):
try:
full_path = os.path.join(base_directory, subdir)
if os.path.isdir(full_path) and not subdir.endswith(".disabled") and not subdir.startswith('.') and subdir != '__pycache__':
print(f"## Install dependencies for '{subdir}'")
requirements_file = os.path.join(full_path, "requirements.txt")
install_script = os.path.join(full_path, "install.py")
if os.path.exists(requirements_file) or os.path.exists(install_script):
subdirs_with_files.append((full_path, requirements_file, install_script))
except Exception as e:
print(f"EXCEPTION During Dependencies INSTALL on '{subdir}':\n{e}")
return subdirs_with_files
def install_requirements(requirements_file_path):
if os.path.exists(requirements_file_path):
subprocess.run(["pip", "install", "-r", requirements_file_path])
def run_install_script(install_script_path):
if os.path.exists(install_script_path):
subprocess.run(["python", install_script_path])
custom_nodes_directory = "custom_nodes"
subdirs_with_files = get_enabled_subdirectories_with_files(custom_nodes_directory)
for subdir, requirements_file, install_script in subdirs_with_files:
install_requirements(requirements_file)
run_install_script(install_script)

View File

@ -1,21 +0,0 @@
git clone https://github.com/comfyanonymous/ComfyUI
cd ComfyUI/custom_nodes
git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
cd ..
python -m venv venv
source venv/bin/activate
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
python -m pip install -r requirements.txt
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
cd ..
echo "#!/bin/bash" > run_gpu.sh
echo "cd ComfyUI" >> run_gpu.sh
echo "source venv/bin/activate" >> run_gpu.sh
echo "python main.py --preview-method auto" >> run_gpu.sh
chmod +x run_gpu.sh
echo "#!/bin/bash" > run_cpu.sh
echo "cd ComfyUI" >> run_cpu.sh
echo "source venv/bin/activate" >> run_cpu.sh
echo "python main.py --preview-method auto --cpu" >> run_cpu.sh
chmod +x run_cpu.sh

View File

@ -1,17 +0,0 @@
git clone https://github.com/comfyanonymous/ComfyUI
cd ComfyUI/custom_nodes
git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
cd ..
python -m venv venv
call venv/Scripts/activate
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
python -m pip install -r requirements.txt
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
cd ..
echo "cd ComfyUI" >> run_gpu.bat
echo "call venv/Scripts/activate" >> run_gpu.bat
echo "python main.py" >> run_gpu.bat
echo "cd ComfyUI" >> run_cpu.bat
echo "call venv/Scripts/activate" >> run_cpu.bat
echo "python main.py --cpu" >> run_cpu.bat

View File

@ -1,3 +0,0 @@
.\python_embeded\python.exe -s -m pip install gitpython
.\python_embeded\python.exe -c "import git; git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager', './ComfyUI/custom_nodes/comfyui-manager')"
.\python_embeded\python.exe -m pip install -r ./ComfyUI/custom_nodes/comfyui-manager/requirements.txt