Compare commits

...
Author SHA1 Message Date
KarrixLee 3ed9729960 chore: version to 2.3.0 2025-08-21 18:37:30 +08:00
KarrixLee 9147ddd2e1 tweak machine display 2025-08-21 04:09:03 +08:00
KarrixLee 4c4fcc09c0 small tweak 2025-08-21 03:50:43 +08:00
KarrixLee dee5c9e071 refactor: improve snapshot fetching and UI adjustments
- Introduced centralized snapshot fetching utilities in snapshot-utils.js, including a fallback for ComfyUI version retrieval.
- Updated machine-manager.js to utilize the new snapshot fetching methods, enhancing error handling and code clarity.
- Adjusted index.js to streamline snapshot fetching during workflow deployment.
- Made minor UI adjustments in workflow-list.js for better layout consistency.

These changes enhance the application's robustness in handling snapshots and improve overall user experience.
2025-08-21 03:44:11 +08:00
KarrixLee 0539464d62 add: blacklist node 2025-08-21 03:04:12 +08:00
KarrixLee e5d3f66296 add error 2025-08-21 02:57:14 +08:00
KarrixLee 8962549393 add: create machine from local 2025-08-21 02:50:43 +08:00
KarrixLee 1a78c04cba can sync machine 2025-08-21 02:29:41 +08:00
KarrixLee 4fdfe7c18a compare list 2025-08-21 01:53:13 +08:00
KarrixLee 19e175c571 fetch docker steps in backend 2025-08-20 23:30:02 +08:00
KarrixLee de5a1791d4 remove unused code 2025-08-19 22:40:02 +08:00
KarrixLee ab265cf3ec add: machine id when create 2025-08-19 21:57:34 +08:00
KarrixLee 1a8519b5d4 refactor: enhance machine management UI and functionality
- Removed console error logs for improved user experience during initialization.
- Updated the display styles for "No machine found" and error messages to enhance visual clarity.
- Added buttons for adding and updating machines directly from the error and no machine states.
- Implemented a dialog for adding and updating machine IDs, improving user interaction.
- Adjusted the machine addition process to show loading states and ensure proper DOM updates.

These changes significantly improve the usability and functionality of the machine management feature.
2025-08-19 21:08:45 +08:00
KarrixLee 10b84ab722 initt: add machine management functionality
- Implemented a new endpoint in custom_routes.py to retrieve machine details by ID.
- Introduced machine-manager.js to handle machine data fetching, display, and local storage management.
- Updated index.js to initialize the machine manager and display machine information in the UI.
- Adjusted workflow-list.js to modify the layout for better integration with the new machine management features.

These changes enhance the application's capability to manage and display serverless machine information effectively.
2025-08-19 20:49:57 +08:00
KarrixLee 7f6eea361e tweak: add tutorial complete to true by default (#104) 2025-08-18 00:19:57 +08:00
KarrixLee 14829a2b12 Comfyui 0.3.48 compatibility (#102)
* refactor: support 3 attribute for the validation prompt

* add: comments for clarification
2025-08-04 17:23:12 +08:00
KarrixLee 03c4e2d85d Add audio file upload support 2025-07-26 18:37:09 +08:00
KarrixLee bad8b1104f Fix comfyui 0.3.45 (#100)
* Refactor post_prompt function to include prompt_id handling

- Updated the post_prompt function in custom_routes.py to retrieve and validate a prompt_id from the incoming JSON data, defaulting to a new UUID if not provided.
- Adjusted the validation call to use prompt_id alongside the prompt, ensuring proper identification and processing of prompts.

These changes enhance the functionality of prompt handling within the application.

* Refactor post_prompt and send_prompt functions for async handling

- Updated the post_prompt function to be asynchronous, allowing for non-blocking execution when validating prompts.
- Adjusted calls to post_prompt in send_prompt and comfy_deploy_run to await the asynchronous execution, ensuring proper handling of prompt submissions.
- Commented out unused prompt construction code to streamline the function.

These changes enhance the performance and responsiveness of prompt handling in the application.

* Refactor handle_execute to support asynchronous execution

- Updated the swizzle_execute function to be asynchronous, allowing for non-blocking execution of the origin_execute function.
- Added missing parameters for pending_async_nodes in the swizzle_execute function call.
- Ensured that the result from origin_execute is awaited, improving the handling of asynchronous operations.

These changes enhance the performance and responsiveness of the execution handling in the application.

* Enhance error handling in post_prompt and origin_execute functions

- Added try-except blocks in post_prompt to handle TypeErrors during prompt validation, allowing for fallback to an older signature.
- Implemented similar error handling in the origin_execute function to manage potential TypeErrors, ensuring robust execution flow.
- Improved logging to capture issues with function signatures, aiding in debugging.

These changes improve the resilience of the application when dealing with prompt and execution validation.

* Refactor post_prompt and origin_execute for synchronous handling

- Modified the post_prompt function to call validate_prompt synchronously when a TypeError occurs, improving error handling.
- Updated the origin_execute function to execute synchronously, ensuring consistent behavior during execution.

These changes enhance the robustness of prompt validation and execution processes in the application.

* tweak: optional

* Refactor swizzle_execute for async and sync handling

- Enhanced the swizzle_execute function to differentiate between asynchronous and synchronous execution paths based on the origin_execute function's nature.
- Improved the structure of the swizzle_execute function to ensure proper handling of parameters and execution flow for both async and sync scenarios.
- Maintained existing error handling while ensuring consistent behavior across execution types.

These changes improve the flexibility and robustness of the execution handling in the application.
2025-07-23 16:12:35 -07:00
KarrixLee ac8779dc54 fix 2025-07-19 10:24:04 -07:00
KarrixLee 2a5223f2f0 fix 2025-06-29 15:43:12 +08:00
KarrixLee f02aef4cb3 tweak versoin 2025-06-29 13:37:24 +08:00
KarrixLee 22458a1cd6 Merge branch 'local-flow-2' 2025-06-27 14:10:10 +08:00
Tristan-mc-qandtristan22mc 0a58eba554 feat: Add deployable EXR saver node (#98)
Co-authored-by: tristan22mc <tristan22mc@gmail.com>
2025-06-26 14:07:48 -07:00
KarrixLee def54df9c2 tweak 2025-06-17 18:37:02 +08:00
KarrixLee 32a950afe8 tweak 2025-06-17 18:07:43 +08:00
KarrixLee 8130779d94 Enhance configuration saving and workflow list management
- Updated the save method in ConfigDialog to be asynchronous, allowing for smoother handling of configuration saves.
- Added a new function to refresh the workflow list if the sidebar is open, ensuring the UI reflects the latest data after configuration changes.
- Made workflowsState globally accessible for improved state management across components.
- Adjusted the height of the workflows list for better UI layout.

These changes improve the user experience by ensuring that the workflow list is up-to-date and enhancing the overall responsiveness of the configuration dialog.
2025-06-16 21:25:33 +08:00
KarrixLee 4cbd2a8225 Add workflow retrieval functionality and enhance UI interaction
- Introduced a new endpoint in custom_routes.py for fetching workflows by ID, including authorization checks and error handling.
- Updated workflow-list.js to support fetching and displaying workflow data upon user interaction, including loading indicators and error handling.
- Enhanced the createWorkflowItem function to accept additional parameters for improved data handling and user feedback.

These changes improve the user experience by enabling seamless workflow retrieval and interaction within the application.
2025-06-16 21:01:43 +08:00
KarrixLee d6fb2daeff Add workflow list management and search functionality
- Introduced a new workflow-list.js file to manage workflows, including fetching, displaying, and searching workflows.
- Enhanced the custom_routes.py file with a new endpoint for retrieving workflows, ensuring proper authorization and query parameter handling.
- Updated index.js to initialize the workflows list and integrate search functionality within the UI.

These changes improve the user experience by allowing efficient management and retrieval of workflows in the application.
2025-06-16 18:47:18 +08:00
KarrixLee a91effe3c8 Add workflow management endpoints and enhance deployment logic
- Introduced new endpoints for creating workflows and versions in the custom_routes.py file.
- Updated the deployWorkflow function in index.js to include apiUrl in the request body and handle workflow versioning.
- Improved error handling for API requests and ensured required fields are validated before processing.
- Enhanced user feedback during deployment with updated success messages.

These changes streamline workflow management and improve the overall deployment process within the application.
2025-06-16 16:32:23 +08:00
KarrixLee c015b710fe Enhance authentication flow and improve API integration
- Added a new endpoint for handling authentication responses in the UploadQueue class.
- Updated the deployWorkflow function to include apiUrl in the configuration checks.
- Refactored API calls to use the new auth-response endpoint, ensuring proper request handling.
- Improved logging for better debugging during workflow deployment.

These changes streamline the authentication process and enhance the overall API interaction within the application.
2025-06-15 13:55:59 +08:00
KarrixLee 2738d1913a tweak 2025-06-14 22:04:53 +08:00
KarrixLee 3f4c11e3f1 Refactor event dispatching and improve code readability in index.js
- Standardized formatting for CustomEvent dispatches to enhance consistency.
- Simplified async function prompts and improved filtering logic for existing input IDs.
- Enhanced readability by restructuring multiline statements and ensuring consistent indentation.
- Added error handling for deployment processes and improved dialog display methods.

These changes aim to improve maintainability and clarity of the codebase.
2025-06-14 21:49:53 +08:00
7 changed files with 3708 additions and 369 deletions
+78
View File
@@ -0,0 +1,78 @@
# In file: comfyui-deploy/comfy-nodes/output_exr.py
import os
import numpy as np
import folder_paths
# Try to set up OpenCV for EXR writing.
try:
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
import cv2
OPENCV_AVAILABLE = True
except ImportError:
print("Warning: OpenCV not found for ComfyDeployOutputEXR. Please add opencv-python-headless to requirements.txt")
OPENCV_AVAILABLE = False
# ALIGNED: Renamed class to match project conventions
class ComfyDeployOutputEXR:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", ),
"filename_prefix": ("STRING", {"default": "ComfyDeploy_EXR"})
},
# ADDED: Optional output_id for consistency with other ComfyDeploy nodes
"optional": {
"output_id": ("STRING", {"multiline": False, "default": "output_exr"}),
},
}
RETURN_TYPES = ()
# ALIGNED: Changed function name to 'run'
FUNCTION = "run"
OUTPUT_NODE = True
# ALIGNED: Matched the category name
CATEGORY = "🔗ComfyDeploy"
DESCRIPTION = "Saves the input images as EXR (HDR) files."
def run(self, images, filename_prefix="ComfyDeploy_EXR", output_id="output_exr"):
if not OPENCV_AVAILABLE:
raise ImportError("OpenCV is required to save EXR files. Please ensure opencv-python-headless is in requirements.txt.")
full_output_folder, filename, counter, subfolder, filename_prefix = (
folder_paths.get_save_image_path(
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
)
)
results = list()
for image in images:
image_np = image.cpu().numpy()
if image_np.dtype != np.float32:
image_np = image_np.astype(np.float32)
file = f"{filename}_{counter:05}.exr"
file_path = os.path.join(full_output_folder, file)
image_np_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
cv2.imwrite(file_path, image_np_bgr)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type,
"output_id": output_id, # ADDED
})
counter += 1
return {"ui": {"images": results}}
# ALIGNED: Mappings are defined at the bottom of the node file in this project
NODE_CLASS_MAPPINGS = {"ComfyDeployOutputEXR": ComfyDeployOutputEXR}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyDeployOutputEXR": "EXR Output (ComfyDeploy)"}
+433 -36
View File
@@ -31,6 +31,7 @@ import torch
import psutil import psutil
from collections import OrderedDict from collections import OrderedDict
import io import io
from urllib.parse import urlencode
# Global session # Global session
client_session = None client_session = None
@@ -264,7 +265,7 @@ def clear_current_prompt(sid):
streaming_prompt_metadata[sid].running_prompt_ids.clear() streaming_prompt_metadata[sid].running_prompt_ids.clear()
def post_prompt(json_data): async def post_prompt(json_data):
prompt_server = server.PromptServer.instance prompt_server = server.PromptServer.instance
json_data = prompt_server.trigger_on_prompt(json_data) json_data = prompt_server.trigger_on_prompt(json_data)
@@ -280,7 +281,48 @@ def post_prompt(json_data):
if "prompt" in json_data: if "prompt" in json_data:
prompt = json_data["prompt"] prompt = json_data["prompt"]
prompt_id = json_data.get("prompt_id") or str(uuid.uuid4())
partial_execution_targets = None
if "partial_execution_targets" in json_data:
partial_execution_targets = json_data["partial_execution_targets"]
# Handle different validate_prompt signatures (newest to oldest)
valid = None
last_error = None
# v0.3.48 (3 args)
try:
valid = await execution.validate_prompt(
prompt_id, prompt, partial_execution_targets
)
except TypeError as e:
last_error = e
logger.debug(
f"validate_prompt with 3 params not supported, trying with 2. Debug: {last_error}"
)
# v0.3.45 - 0.3.47 (2 args)
if valid is None:
try:
valid = await execution.validate_prompt(prompt_id, prompt)
except TypeError as e:
last_error = e
logger.debug(
f"validate_prompt with 2 params not supported, trying legacy signature. Debug: {last_error}"
)
# v0.3.44 or older (1 arg)
if valid is None:
try:
valid = execution.validate_prompt(prompt) valid = execution.validate_prompt(prompt)
except TypeError as e:
last_error = e
logger.error(
f"validate_prompt failed with all signatures. Last error: {last_error}"
)
raise
extra_data = {} extra_data = {}
if "extra_data" in json_data: if "extra_data" in json_data:
extra_data = json_data["extra_data"] extra_data = json_data["extra_data"]
@@ -291,8 +333,6 @@ def post_prompt(json_data):
if "client_id" in json_data: if "client_id" in json_data:
extra_data["client_id"] = json_data["client_id"] extra_data["client_id"] = json_data["client_id"]
if valid[0]: if valid[0]:
# if the prompt id is provided
prompt_id = json_data.get("prompt_id") or str(uuid.uuid4())
outputs_to_execute = valid[2] outputs_to_execute = valid[2]
prompt_server.prompt_queue.put( prompt_server.prompt_queue.put(
(number, prompt_id, prompt, extra_data, outputs_to_execute) (number, prompt_id, prompt, extra_data, outputs_to_execute)
@@ -499,15 +539,15 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
prompt_id = str(uuid.uuid4()) prompt_id = str(uuid.uuid4())
prompt = { # prompt = {
"prompt": workflow_api, # "prompt": workflow_api,
"client_id": sid, # "comfy_deploy_instance", #api.client_id # "client_id": sid, # "comfy_deploy_instance", #api.client_id
"prompt_id": prompt_id, # "prompt_id": prompt_id,
"extra_data": {"extra_pnginfo": {"workflow": workflow}}, # "extra_data": {"extra_pnginfo": {"workflow": workflow}},
} # }
try: try:
res = post_prompt(prompt) # res = post_prompt(prompt)
inputs.running_prompt_ids.add(prompt_id) inputs.running_prompt_ids.add(prompt_id)
prompt_metadata[prompt_id] = SimplePrompt( prompt_metadata[prompt_id] = SimplePrompt(
status_endpoint=inputs.status_endpoint, status_endpoint=inputs.status_endpoint,
@@ -518,7 +558,7 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
except Exception as e: except Exception as e:
error_type = type(e).__name__ error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split("\n")[-2] stack_trace_short = traceback.format_exc().strip().split("\n")[-2]
stack_trace = traceback.format_exc().strip() # stack_trace = traceback.format_exc().strip()
logger.info(f"error: {error_type}, {e}") logger.info(f"error: {error_type}, {e}")
logger.info(f"stack trace: {stack_trace_short}") logger.info(f"stack trace: {stack_trace_short}")
@@ -594,7 +634,7 @@ async def comfy_deploy_run(request):
) )
try: try:
res = post_prompt(prompt) res = await post_prompt(prompt)
except Exception as e: except Exception as e:
error_type = type(e).__name__ error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split("\n")[-2] stack_trace_short = traceback.format_exc().strip().split("\n")[-2]
@@ -663,7 +703,7 @@ async def stream_prompt(data, token):
# log('info', "Begin prompt", prompt=prompt) # log('info', "Begin prompt", prompt=prompt)
try: try:
res = post_prompt(prompt) res = await post_prompt(prompt)
except Exception as e: except Exception as e:
error_type = type(e).__name__ error_type = type(e).__name__
stack_trace_short = traceback.format_exc().strip().split("\n")[-2] stack_trace_short = traceback.format_exc().strip().split("\n")[-2]
@@ -1265,6 +1305,42 @@ def handle_execute(class_type, last_node_id, prompt_id, server, unique_id):
try: try:
origin_execute = execution.execute origin_execute = execution.execute
is_async = asyncio.iscoroutinefunction(origin_execute)
if is_async:
async def swizzle_execute(
server,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
pending_async_nodes,
):
unique_id = current_item
class_type = dynprompt.get_node(unique_id)["class_type"]
last_node_id = server.last_node_id
result = await origin_execute(
server,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
pending_async_nodes,
)
handle_execute(class_type, last_node_id, prompt_id, server, unique_id)
return result
else:
def swizzle_execute( def swizzle_execute(
server, server,
@@ -1280,6 +1356,7 @@ try:
unique_id = current_item unique_id = current_item
class_type = dynprompt.get_node(unique_id)["class_type"] class_type = dynprompt.get_node(unique_id)["class_type"]
last_node_id = server.last_node_id last_node_id = server.last_node_id
result = origin_execute( result = origin_execute(
server, server,
dynprompt, dynprompt,
@@ -1291,11 +1368,12 @@ try:
execution_list, execution_list,
pending_subgraph_results, pending_subgraph_results,
) )
handle_execute(class_type, last_node_id, prompt_id, server, unique_id) handle_execute(class_type, last_node_id, prompt_id, server, unique_id)
return result return result
execution.execute = swizzle_execute execution.execute = swizzle_execute
except Exception as e: except Exception:
pass pass
@@ -1439,10 +1517,6 @@ async def send_json_override(self, event, data, sid=None):
logger.info(format_table(headers, table_data)) logger.info(format_table(headers, table_data))
# print("========================\n") # print("========================\n")
timeline = format_execution_timeline(NODE_EXECUTION_TIMES)
logger.info(f"\nNode Execution Timeline:\n{timeline}")
# Clear the execution times for the next run
# the last executing event is none, then the workflow is finished # the last executing event is none, then the workflow is finished
if event == "executing" and data.get("node") is None: if event == "executing" and data.get("node") is None:
mark_prompt_done(prompt_id=prompt_id) mark_prompt_done(prompt_id=prompt_id)
@@ -2058,6 +2132,7 @@ async def upload_in_background(
("model_file", "format", "application/octet-stream"), ("model_file", "format", "application/octet-stream"),
("result", "format", "application/octet-stream"), ("result", "format", "application/octet-stream"),
("text_file", "format", "text/plain"), ("text_file", "format", "text/plain"),
("audio", "format", "audio/mpeg"),
]: ]:
items = data.get(file_type, []) items = data.get(file_type, [])
@@ -2145,6 +2220,7 @@ async def update_run_with_output(
or "model_file" in data or "model_file" in data
or "result" in data or "result" in data
or "text_file" in data or "text_file" in data
or "audio" in data
) )
if bypass_upload and have_upload_media: if bypass_upload and have_upload_media:
print( print(
@@ -2753,10 +2829,10 @@ class UploadQueue:
logger.error(f"Upload failed: {str(e)}") logger.error(f"Upload failed: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
finally: finally:
# Remove this upload from tracking async with self.lock: # Acquire lock to protect shared dict access
if prompt_id in self.pending_uploads: if prompt_id in self.pending_uploads:
self.pending_uploads[prompt_id].discard(upload_id) self.pending_uploads[prompt_id].discard(upload_id)
# Remove from node tracking if applicable
if ( if (
node_id node_id
and prompt_id in self.node_uploads and prompt_id in self.node_uploads
@@ -2764,19 +2840,20 @@ class UploadQueue:
): ):
self.node_uploads[prompt_id][node_id].discard(upload_id) self.node_uploads[prompt_id][node_id].discard(upload_id)
# If this was the last upload for this node, clean up node data
if not self.node_uploads[prompt_id][node_id]: if not self.node_uploads[prompt_id][node_id]:
del self.node_uploads[prompt_id][node_id] del self.node_uploads[prompt_id][node_id]
if prompt_id in self.node_output_data:
if node_id in self.node_output_data[prompt_id]: if (
if self.node_output_data[prompt_id][node_id]["data"]: prompt_id in self.node_output_data
# Send final node data to API before cleanup and node_id in self.node_output_data[prompt_id]
if prompt_metadata[prompt_id].status_endpoint: ):
node_data = self.node_output_data[prompt_id][
node_id
]
if node_data["data"]:
body = { body = {
"run_id": prompt_id, "run_id": prompt_id,
"output_data": self.node_output_data[ "output_data": node_data["data"],
prompt_id
][node_id]["data"],
"node_meta": {"node_id": node_id}, "node_meta": {"node_id": node_id},
} }
try: try:
@@ -2785,21 +2862,24 @@ class UploadQueue:
prompt_metadata[ prompt_metadata[
prompt_id prompt_id
].status_endpoint, ].status_endpoint,
token=prompt_metadata[prompt_id].token, token=prompt_metadata[
prompt_id
].token,
json=body, json=body,
) )
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Failed to send final node data: {str(e)}" f"Failed to send final node data: {str(e)}"
) )
# Safe to delete now (re-check not strictly needed with lock, but harmless)
del self.node_output_data[prompt_id][node_id] del self.node_output_data[prompt_id][node_id]
# Send status update
await self.update_queue_status(prompt_id)
# If no more pending uploads for this prompt and it's done, update status # If no more pending uploads for this prompt and it's done, update status
if not self.pending_uploads[prompt_id] and is_prompt_done( if (
prompt_id prompt_id in self.pending_uploads
and not self.pending_uploads[prompt_id]
and is_prompt_done(prompt_id)
): ):
# Clean up all data for this prompt # Clean up all data for this prompt
if prompt_id in self.node_uploads: if prompt_id in self.node_uploads:
@@ -2812,9 +2892,12 @@ class UploadQueue:
loop.create_task(update_run(prompt_id, Status.SUCCESS)) loop.create_task(update_run(prompt_id, Status.SUCCESS))
loop.create_task(send("success", {"prompt_id": prompt_id})) loop.create_task(send("success", {"prompt_id": prompt_id}))
# Mark task as done # Mark task as done (outside lock to avoid holding it unnecessarily)
self.queue.task_done() self.queue.task_done()
# Send status update (also outside lock)
await self.update_queue_status(prompt_id)
except Exception as e: except Exception as e:
logger.error(f"Error in upload worker: {str(e)}") logger.error(f"Error in upload worker: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@@ -2885,3 +2968,317 @@ def format_execution_timeline(execution_times):
current_time += duration current_time += duration
return format_table(headers, rows) return format_table(headers, rows)
@server.PromptServer.instance.routes.get("/comfyui-deploy/auth-response")
async def auth_response_proxy(request):
request_id = request.rel_url.query.get("request_id")
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
if not request_id:
return web.json_response({"error": "request_id is required"}, status=400)
target_url = f"{api_url}/api/platform/comfyui/auth-response?request_id={request_id}"
try:
await ensure_client_session()
async with client_session.get(target_url) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@server.PromptServer.instance.routes.post("/comfyui-deploy/workflow")
async def create_workflow_proxy(request):
data = await request.json()
name = data.get("name")
workflow_json = data.get("workflow_json")
workflow_api = data.get("workflow_api")
machine_id = data.get("machine_id")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
if not name or not workflow_json or not workflow_api:
return web.json_response(
{"error": "name, workflow_json, workflow_api are required"}, status=400
)
target_url = f"{api_url}/api/workflow"
request_body = {
"name": name,
"workflow_json": json.dumps(workflow_json),
"workflow_api": json.dumps(workflow_api),
"machine_id": machine_id,
}
try:
await ensure_client_session()
async with client_session.post(
target_url,
json=request_body,
headers={
"Content-Type": "application/json",
"Authorization": auth_header,
},
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@server.PromptServer.instance.routes.post("/comfyui-deploy/workflow/version")
async def create_workflow_version_proxy(request):
data = await request.json()
workflow_id = data.get("workflow_id")
workflow = data.get("workflow")
workflow_api = data.get("workflow_api")
comment = data.get("comment", "")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/workflow/{workflow_id}/version"
request_body = {
"workflow": workflow,
"workflow_api": workflow_api,
"comment": comment,
}
try:
await ensure_client_session()
async with client_session.post(
target_url,
json=request_body,
headers={
"Content-Type": "application/json",
"Authorization": auth_header,
},
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@server.PromptServer.instance.routes.get("/comfyui-deploy/workflows")
async def get_workflows_proxy(request):
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
search = request.rel_url.query.get("search", "")
limit = request.rel_url.query.get("limit", 10)
offset = request.rel_url.query.get("offset", 0)
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
# Build query parameters properly
params = {}
if search:
params["search"] = search
if limit:
params["limit"] = limit
if offset:
params["offset"] = offset
target_url = f"{api_url}/api/workflows"
if params:
target_url += f"?{urlencode(params)}"
try:
await ensure_client_session()
async with client_session.get(
target_url,
headers={
"Content-Type": "application/json",
"Authorization": auth_header,
},
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# for getting a workflow by id
@server.PromptServer.instance.routes.get("/comfyui-deploy/workflow")
async def get_workflow_proxy(request):
workflow_id = request.rel_url.query.get("workflow_id")
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/workflow/{workflow_id}"
try:
await ensure_client_session()
async with client_session.get(
target_url, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# for getting a machine by id
@server.PromptServer.instance.routes.get("/comfyui-deploy/machine")
async def get_machine_proxy(request):
machine_id = request.rel_url.query.get("machine_id")
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/machine/{machine_id}"
try:
await ensure_client_session()
async with client_session.get(
target_url, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# for fetching docker steps from current snapshot
@server.PromptServer.instance.routes.post("/comfyui-deploy/snapshot-to-docker")
async def snapshot_to_docker_proxy(request):
data = await request.json()
snapshot = data.get("snapshot")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/snapshot-to-docker"
request_body = snapshot
try:
await ensure_client_session()
async with client_session.post(
target_url, json=request_body, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# update a serverless machine with machine id
@server.PromptServer.instance.routes.post("/comfyui-deploy/machine/update")
async def update_machine_proxy(request):
data = await request.json()
machine_id = data.get("machine_id")
comfyui_version = data.get("comfyui_version", None)
docker_steps = data.get("docker_steps")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/machine/serverless/{machine_id}"
request_body = {"docker_command_steps": docker_steps}
if comfyui_version:
request_body["comfyui_version"] = comfyui_version
try:
await ensure_client_session()
async with client_session.patch(
target_url, json=request_body, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@server.PromptServer.instance.routes.post("/comfyui-deploy/machine/create")
async def create_machine_proxy(request):
data = await request.json()
name = data.get("name")
docker_command_steps = data.get("docker_command_steps")
comfyui_version = data.get("comfyui_version")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/machine/serverless"
request_body = {
"name": name,
"docker_command_steps": docker_command_steps,
"comfyui_version": comfyui_version,
"gpu": "A10G",
}
try:
await ensure_client_session()
async with client_session.post(
target_url, json=request_body, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# get latest comfyui version
@server.PromptServer.instance.routes.get("/comfyui-deploy/comfyui-version")
async def get_comfyui_version_proxy(request):
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/latest-hashes"
try:
await ensure_client_session()
async with client_session.get(
target_url, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-deploy" name = "comfyui-deploy"
description = "Open source comfyui deployment platform, a vercel for generative workflow infra." description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
version = "2.1.0" version = "2.3.0"
license = { file = "LICENSE" } license = { file = "LICENSE" }
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"] dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
+292 -268
View File
@@ -3,6 +3,9 @@ import { api } from "../../scripts/api.js";
// import { LGraphNode } from "../../scripts/widgets.js"; // import { LGraphNode } from "../../scripts/widgets.js";
LGraphNode = LiteGraph.LGraphNode; LGraphNode = LiteGraph.LGraphNode;
import { ComfyDialog, $el } from "../../scripts/ui.js"; import { ComfyDialog, $el } from "../../scripts/ui.js";
import { initializeWorkflowsList, addWorkflowSearch } from "./workflow-list.js";
import { initializeMachineManager } from "./machine-manager.js";
import { fetchSnapshotSimple } from "./snapshot-utils.js";
import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25"; import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25";
// import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69"; // import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69";
@@ -96,7 +99,7 @@ function dispatchAPIEventData(data) {
break; break;
case "executing": case "executing":
api.dispatchEvent( api.dispatchEvent(
new CustomEvent("executing", { detail: msg.data.node }), new CustomEvent("executing", { detail: msg.data.node })
); );
break; break;
case "executed": case "executed":
@@ -104,17 +107,17 @@ function dispatchAPIEventData(data) {
break; break;
case "execution_start": case "execution_start":
api.dispatchEvent( api.dispatchEvent(
new CustomEvent("execution_start", { detail: msg.data }), new CustomEvent("execution_start", { detail: msg.data })
); );
break; break;
case "execution_error": case "execution_error":
api.dispatchEvent( api.dispatchEvent(
new CustomEvent("execution_error", { detail: msg.data }), new CustomEvent("execution_error", { detail: msg.data })
); );
break; break;
case "execution_cached": case "execution_cached":
api.dispatchEvent( api.dispatchEvent(
new CustomEvent("execution_cached", { detail: msg.data }), new CustomEvent("execution_cached", { detail: msg.data })
); );
break; break;
default: default:
@@ -217,28 +220,26 @@ async function convertToInput(node, widget, config) {
console.log(node, widget, config); console.log(node, widget, config);
const result = await app.extensionManager.dialog.prompt( const result = await app.extensionManager.dialog.prompt({
{
title: "Convert " + widget.name + " to external input", title: "Convert " + widget.name + " to external input",
message: "Input name", message: "Input name",
defaultValue: widget.name, defaultValue: widget.name,
} });
);
if (!result) return; if (!result) return;
// Check for duplicate input IDs across existing external input nodes // Check for duplicate input IDs across existing external input nodes
const existingInputIds = Object.values(app.graph.nodes) const existingInputIds = Object.values(app.graph.nodes)
.filter(n => n.type.startsWith("ComfyUIDeployExternal")) .filter((n) => n.type.startsWith("ComfyUIDeployExternal"))
.map(n => n.widgets_values?.[0]) .map((n) => n.widgets_values?.[0])
.filter(Boolean); .filter(Boolean);
if (existingInputIds.includes(result)) { if (existingInputIds.includes(result)) {
app.extensionManager.toast.add({ app.extensionManager.toast.add({
severity: 'error', severity: "error",
summary: 'Input ID already exists', summary: "Input ID already exists",
detail: 'Please choose a different name.', detail: "Please choose a different name.",
life: 3000 life: 3000,
}); });
return; return;
} }
@@ -273,7 +274,7 @@ async function convertToInput(node, widget, config) {
console.log(links[link]); console.log(links[link]);
setTimeout( setTimeout(
() => inputNode.connect(0, llink.target_id, llink.target_slot), () => inputNode.connect(0, llink.target_id, llink.target_slot),
100, 100
); );
} }
@@ -309,7 +310,10 @@ async function convertToInput(node, widget, config) {
node.convertWidgetToInput(widget); node.convertWidgetToInput(widget);
var inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId); var inputNode = LiteGraph.createNode(
externalNode,
"External Input: " + inputId
);
// if (type === "COMBO") { // if (type === "COMBO") {
// inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId, { // inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId, {
@@ -342,7 +346,8 @@ async function convertToInput(node, widget, config) {
if (type === "COMBO") { if (type === "COMBO") {
console.log(inputNode); console.log(inputNode);
console.log(options); console.log(options);
inputNode.widgets.find((x) => x.name == "default_value").options.values = options; inputNode.widgets.find((x) => x.name == "default_value").options.values =
options;
} }
app.graph.add(inputNode); app.graph.add(inputNode);
@@ -365,7 +370,11 @@ function getConfig(widgetName) {
function isConvertibleWidget(node, widget, config) { function isConvertibleWidget(node, widget, config) {
// console.log(config); // console.log(config);
if (node.type === "LoadImage" && widget.type === "combo" && widget.name == "image") { if (
node.type === "LoadImage" &&
widget.type === "combo" &&
widget.name == "image"
) {
return true; return true;
} }
@@ -447,7 +456,7 @@ const ext = {
} else { } else {
loadingDialog.showLoading( loadingDialog.showLoading(
"Loading workflow from " + org_display, "Loading workflow from " + org_display,
"Please wait...", "Please wait..."
); );
fetch(endpoint + "/api/workflow-version/" + workflow_version_id, { fetch(endpoint + "/api/workflow-version/" + workflow_version_id, {
method: "GET", method: "GET",
@@ -483,7 +492,7 @@ const ext = {
window.history.replaceState( window.history.replaceState(
{}, {},
document.title, document.title,
window.location.pathname, window.location.pathname
); );
}); });
} }
@@ -507,7 +516,7 @@ const ext = {
content: `Convert ${w.name} to widget`, content: `Convert ${w.name} to widget`,
callback: /* @__PURE__ */ __name( callback: /* @__PURE__ */ __name(
() => convertToWidget(this, w), () => convertToWidget(this, w),
"callback", "callback"
), ),
}); });
} else { } else {
@@ -520,7 +529,7 @@ const ext = {
content: `Convert ${w.name} to external input`, content: `Convert ${w.name} to external input`,
callback: /* @__PURE__ */ __name( callback: /* @__PURE__ */ __name(
async () => convertToInput(this, w, config), async () => convertToInput(this, w, config),
"callback", "callback"
), ),
className: "comfydeploy-menu-item", className: "comfydeploy-menu-item",
}); });
@@ -545,7 +554,7 @@ const ext = {
// className: "comfydeploy-menu-item" // className: "comfydeploy-menu-item"
// }, // },
...toInput, ...toInput,
null, null
); );
} else { } else {
options.push(...toInput, null); options.push(...toInput, null);
@@ -574,9 +583,7 @@ const ext = {
console.log(nodeData.input.optional.default_value_url); console.log(nodeData.input.optional.default_value_url);
} }
if ( if (nodeData?.input?.optional?.default_value?.[1]?.dynamic_enum === true) {
nodeData?.input?.optional?.default_value?.[1]?.dynamic_enum === true
) {
nodeData.input.optional.default_value = ["DYNAMIC_ENUM"]; nodeData.input.optional.default_value = ["DYNAMIC_ENUM"];
// console.log(nodeData.input.optional.default_value); // console.log(nodeData.input.optional.default_value);
} }
@@ -642,7 +649,7 @@ const ext = {
(v) => { (v) => {
this.properties.workflow_name = v; this.properties.workflow_name = v;
}, },
{ multiline: false }, { multiline: false }
); );
this.addWidget( this.addWidget(
@@ -652,7 +659,7 @@ const ext = {
(v) => { (v) => {
this.properties.workflow_id = v; this.properties.workflow_id = v;
}, },
{ multiline: false }, { multiline: false }
); );
this.addWidget( this.addWidget(
@@ -662,7 +669,7 @@ const ext = {
(v) => { (v) => {
this.properties.version = v; this.properties.version = v;
}, },
{ multiline: false }, { multiline: false }
); );
this.widgets_start_y = 10; this.widgets_start_y = 10;
@@ -688,8 +695,19 @@ const ext = {
onConfigure(o) { onConfigure(o) {
// This method is called when the node is being configured (e.g., when loading a saved graph) // This method is called when the node is being configured (e.g., when loading a saved graph)
// Ensure all necessary data is restored // ComfyUI stores widget values in widgets_values array, not properties
if (o.properties) { if (o.widgets_values && o.widgets_values.length >= 3) {
// Set the widget values directly
this.widgets[0].value = o.widgets_values[0] || "";
this.widgets[1].value = o.widgets_values[1] || "";
this.widgets[2].value = o.widgets_values[2] || "1";
// Also update properties to stay in sync
this.properties.workflow_name = o.widgets_values[0] || "";
this.properties.workflow_id = o.widgets_values[1] || "";
this.properties.version = o.widgets_values[2] || "1";
} else if (o.properties) {
// Fallback to properties if widgets_values is not available
this.properties = { ...this.properties, ...o.properties }; this.properties = { ...this.properties, ...o.properties };
this.widgets[0].value = this.properties.workflow_name || ""; this.widgets[0].value = this.properties.workflow_name || "";
this.widgets[1].value = this.properties.workflow_id || ""; this.widgets[1].value = this.properties.workflow_id || "";
@@ -705,7 +723,7 @@ const ext = {
title: "Comfy Deploy", title: "Comfy Deploy",
title_mode: LiteGraph.NORMAL_TITLE, title_mode: LiteGraph.NORMAL_TITLE,
collapsable: true, collapsable: true,
}), })
); );
ComfyDeploy.category = "deploy"; ComfyDeploy.category = "deploy";
@@ -719,8 +737,8 @@ const ext = {
"string", "string",
inputName, inputName,
/* value=*/ "", /* value=*/ "",
() => { }, () => {},
{ serialize: true }, { serialize: true }
); );
const buttonWidget = node.addWidget( const buttonWidget = node.addWidget(
@@ -734,7 +752,7 @@ const ext = {
}); });
// console.log("load image"); // console.log("load image");
}, },
{ serialize: false }, { serialize: false }
); );
console.log(node.widgets); console.log(node.widgets);
@@ -783,12 +801,10 @@ const ext = {
DYNAMIC_ENUM(node, inputName, inputData) { DYNAMIC_ENUM(node, inputName, inputData) {
// console.log("DYNAMIC_ENUM", JSON.parse(JSON.stringify(node)), inputName, inputData); // console.log("DYNAMIC_ENUM", JSON.parse(JSON.stringify(node)), inputName, inputData);
const enumWidget = node.addWidget( const enumWidget = node.addWidget("combo", inputName, "", {
"combo", serialize: true,
inputName, values: [],
"", });
{ serialize: true, values: [] },
);
return { widget: enumWidget }; return { widget: enumWidget };
}, },
@@ -796,10 +812,14 @@ const ext = {
}, },
async afterConfigureGraph() { async afterConfigureGraph() {
app.graph.nodes.forEach(node => { app.graph.nodes.forEach((node) => {
if (node.type === "ComfyUIDeployExternalEnum") { if (node.type === "ComfyUIDeployExternalEnum") {
const default_value_index = node.widgets.findIndex(x => x.name === "default_value"); const default_value_index = node.widgets.findIndex(
const options_index = node.widgets.findIndex(x => x.name === "options"); (x) => x.name === "default_value"
);
const options_index = node.widgets.findIndex(
(x) => x.name === "options"
);
var dynamic_enum_options = [node.widgets[default_value_index].value]; var dynamic_enum_options = [node.widgets[default_value_index].value];
if (node.widgets[options_index].value) { if (node.widgets[options_index].value) {
@@ -828,12 +848,12 @@ const ext = {
try { try {
await window["app"].ui.settings.setSettingValueAsync( await window["app"].ui.settings.setSettingValueAsync(
"Comfy.Validation.Workflows", "Comfy.Validation.Workflows",
true, true
); );
} catch (error) { } catch (error) {
console.warning( console.warning(
"Error setting validation to false, is fine to ignore this", "Error setting validation to false, is fine to ignore this",
error, error
); );
} }
console.log("loadGraphData"); console.log("loadGraphData");
@@ -975,13 +995,16 @@ const ext = {
await app.ui.settings.setSettingValueAsync("Comfy.UseNewMenu", "Top"); await app.ui.settings.setSettingValueAsync("Comfy.UseNewMenu", "Top");
await app.ui.settings.setSettingValueAsync( await app.ui.settings.setSettingValueAsync(
"Comfy.Sidebar.Size", "Comfy.Sidebar.Size",
"small", "small"
); );
await app.ui.settings.setSettingValueAsync( await app.ui.settings.setSettingValueAsync(
"Comfy.Sidebar.Location", "Comfy.Sidebar.Location",
"left", "left"
);
await app.ui.settings.setSettingValueAsync(
"Comfy.TutorialCompleted",
true
); );
// localStorage.setItem("Comfy.MenuPosition.Docked", "true");
console.log("native mode manmanman"); console.log("native mode manmanman");
} catch (error) { } catch (error) {
console.error("Error setting validation to false", error); console.error("Error setting validation to false", error);
@@ -998,7 +1021,6 @@ const ext = {
} }
})(app.graph.onAfterChange); })(app.graph.onAfterChange);
sendEventToCD("cd_plugin_setup"); sendEventToCD("cd_plugin_setup");
}, },
}; };
@@ -1010,7 +1032,7 @@ const ext = {
function showError(title, message) { function showError(title, message) {
infoDialog.show( infoDialog.show(
`<h3 style="margin: 0px; color: red;">${title}</h3><br><span>${message}</span> `, `<h3 style="margin: 0px; color: red;">${title}</h3><br><span>${message}</span> `
); );
} }
@@ -1044,11 +1066,14 @@ function createDynamicUIHtml(data) {
Object.values(data.custom_nodes).forEach((node) => { Object.values(data.custom_nodes).forEach((node) => {
html += ` html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 16px;"> <div style="border-bottom: 1px solid #e2e8f0; padding-top: 16px;">
<a href="${node.url <a href="${
}" target="_blank" style="font-size: 18px; font-weight: semibold; color: white; text-decoration: none;">${node.name node.url
}" target="_blank" style="font-size: 18px; font-weight: semibold; color: white; text-decoration: none;">${
node.name
}</a> }</a>
<p style="font-size: 14px; color: #4b5563;">${node.hash}</p> <p style="font-size: 14px; color: #4b5563;">${node.hash}</p>
${node.warning ${
node.warning
? `<p style="font-size: 14px; color: #d69e2e;">${node.warning}</p>` ? `<p style="font-size: 14px; color: #d69e2e;">${node.warning}</p>`
: "" : ""
} }
@@ -1065,7 +1090,8 @@ function createDynamicUIHtml(data) {
Object.entries(data.models).forEach(([section, items]) => { Object.entries(data.models).forEach(([section, items]) => {
html += ` html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;"> <div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${section.charAt(0).toUpperCase() + section.slice(1) <h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
section.charAt(0).toUpperCase() + section.slice(1)
}</h3>`; }</h3>`;
items.forEach((item) => { items.forEach((item) => {
html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`; html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
@@ -1082,7 +1108,8 @@ function createDynamicUIHtml(data) {
Object.entries(data.files).forEach(([section, items]) => { Object.entries(data.files).forEach(([section, items]) => {
html += ` html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;"> <div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${section.charAt(0).toUpperCase() + section.slice(1) <h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
section.charAt(0).toUpperCase() + section.slice(1)
}</h3>`; }</h3>`;
items.forEach((item) => { items.forEach((item) => {
html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`; html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
@@ -1102,9 +1129,9 @@ async function deployWorkflow() {
/** @type {LGraph} */ /** @type {LGraph} */
const graph = app.graph; const graph = app.graph;
let { endpoint, apiKey, displayName } = getData(); let { endpoint, apiKey, apiUrl, displayName } = getData();
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { if (!endpoint || !apiKey || apiKey === "" || endpoint === "" || !apiUrl) {
configDialog.show(); configDialog.show();
return; return;
} }
@@ -1114,7 +1141,7 @@ async function deployWorkflow() {
if (deployMeta.length == 0) { if (deployMeta.length == 0) {
const text = await inputDialog.input( const text = await inputDialog.input(
"Create your deployment", "Create your deployment",
"Workflow name", "Workflow name"
); );
if (!text) return; if (!text) return;
console.log(text); console.log(text);
@@ -1146,201 +1173,99 @@ async function deployWorkflow() {
<br> <br>
<button style="font-size: 18px;">${endpoint}</button> <button style="font-size: 18px;">${endpoint}</button>
<br><br>
<label>
<input id="include-deps" type="checkbox" checked>Include dependency</input>
</label>
<br>
<label>
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div> </div>
`, `
); );
if (!ok) return; if (!ok) return;
const includeDeps = document.getElementById("include-deps").checked; const prompt = await app.graphToPrompt();
const reuseHash = document.getElementById("reuse-hash").checked;
console.log(prompt);
if (workflow_id.trim() !== "") {
const text = await inputDialog.input("Save changes", "Comment");
if (!text) return;
try {
loadingDialog.showLoading("Saving changes");
const body = {
api_url: apiUrl,
workflow: prompt.workflow,
workflow_id: workflow_id,
workflow_api: prompt.output,
comment: text,
};
let data = await fetch("/comfyui-deploy/workflow/version", {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
});
if (data.status !== 200) {
throw new Error(await data.text());
} else {
data = await data.json();
console.log(data);
infoDialog.show(
`<span style="color:green;">New version created!</span> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`
);
deployMetaNode.widgets[2].value = data.version;
graph.change();
}
} catch (e) {
infoDialog.showError("Error", e.message);
return;
} finally {
loadingDialog.close();
return;
}
}
if (endpoint.endsWith("/")) { if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1); endpoint = endpoint.slice(0, -1);
} }
loadingDialog.showLoading("Generating snapshot"); loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); const snapshot = await fetchSnapshotSimple();
// console.log(snapshot); console.log(snapshot);
loadingDialog.close(); loadingDialog.close();
if (!snapshot) { if (!snapshot) {
showError( showError(
"Error when deploying", "Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager", "Unable to generate snapshot, please install ComfyUI Manager"
); );
return; return;
} }
const title = deploy.querySelector("#button-title"); const title = deploy.querySelector("#button-title");
const prompt = await app.graphToPrompt();
let deps = undefined;
console.log(prompt);
if (includeDeps) {
loadingDialog.showLoading("Fetching existing version");
const existing_workflow = await fetch(
endpoint + "/api/workflow/" + workflow_id,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
},
)
.then((x) => x.json())
.catch(() => {
return {};
});
loadingDialog.close();
loadingDialog.showLoading("Generating dependency graph");
deps = await generateDependencyGraph({
workflow_api: prompt.output,
snapshot: snapshot,
computeFileHash: async (file) => {
console.log(existing_workflow?.dependencies?.models);
// Match previous hash for models
if (reuseHash && existing_workflow?.dependencies?.models) {
const previousModelHash = Object.entries(
existing_workflow?.dependencies?.models,
).flatMap(([key, value]) => {
return Object.values(value).map((x) => ({
...x,
name: "models/" + key + "/" + x.name,
}));
});
console.log(previousModelHash);
const match = previousModelHash.find((x) => {
console.log(file, x.name);
return file == x.name;
});
console.log(match);
if (match && match.hash) {
console.log("cached hash used");
return match.hash;
}
}
console.log(file);
loadingDialog.showLoading("Generating hash", file);
const hash = await fetch(
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`,
).then((x) => x.json());
loadingDialog.showLoading("Generating hash", file);
console.log(hash);
return hash.file_hash;
},
// handleFileUpload: async (file, hash, prevhash) => {
// console.log("Uploading ", file);
// loadingDialog.showLoading("Uploading file", file);
// try {
// const { download_url } = await fetch(`/comfyui-deploy/upload-file`, {
// method: "POST",
// body: JSON.stringify({
// file_path: file,
// token: apiKey,
// url: endpoint + "/api/upload-url",
// }),
// })
// .then((x) => x.json())
// .catch(() => {
// loadingDialog.close();
// confirmDialog.confirm("Error", "Unable to upload file " + file);
// });
// loadingDialog.showLoading("Uploaded file", file);
// console.log(download_url);
// return download_url;
// } catch (error) {
// return undefined;
// }
// },
existingDependencies: existing_workflow.dependencies,
});
// Need to find a way to include this if this is not included in comfyui-json level
if (
!deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] &&
!deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy.git"]
)
deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] = {
url: "https://github.com/BennyKok/comfyui-deploy",
install_type: "git-clone",
hash:
snapshot?.git_custom_nodes?.[
"https://github.com/BennyKok/comfyui-deploy"
]?.hash ?? "HEAD",
name: "ComfyUI Deploy",
};
loadingDialog.close();
const depsOk = await confirmDialog.confirm(
"Check dependencies",
// JSON.stringify(deps, null, 2),
`
<div>
You will need to create a cloud machine with the following configuration on ComfyDeploy
<ol style="text-align: left; margin-top: 10px;">
<li>Review the dependencies listed in the graph below</li>
<li>Create a new cloud machine with the required configuration</li>
<li>Install missing models and check missing files</li>
<li>Deploy your workflow to the newly created machine</li>
</ol>
</div>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps),
)}" />`,
// createDynamicUIHtml(deps),
);
if (!depsOk) return;
console.log(deps);
}
loadingDialog.showLoading("Deploying..."); loadingDialog.showLoading("Deploying...");
title.innerText = "Deploying..."; title.innerText = "Deploying...";
title.style.color = "orange"; title.style.color = "orange";
// console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey
try { try {
const body = { const body = {
workflow_name, name: workflow_name,
workflow_id, workflow_json: prompt.workflow,
workflow: prompt.workflow,
workflow_api: prompt.output, workflow_api: prompt.output,
snapshot: snapshot, api_url: apiUrl,
dependencies: deps,
}; };
const machineId = localStorage.getItem("comfy_deploy_machine_id");
if (machineId) {
body.machine_id = machineId;
}
console.log(body); console.log(body);
let data = await fetch(apiRoute, { let data = await fetch("/comfyui-deploy/workflow", {
method: "POST", method: "POST",
body: JSON.stringify(body), body: JSON.stringify(body),
headers: { headers: {
@@ -1350,11 +1275,11 @@ async function deployWorkflow() {
}); });
console.log(data); console.log(data);
if (data.status !== 200) { if (data.status !== 200) {
throw new Error(await data.text()); throw new Error(await data.text());
} else { } else {
data = await data.json(); data = await data.json();
await refreshWorkflowListIfOpen();
} }
loadingDialog.close(); loadingDialog.close();
@@ -1363,20 +1288,37 @@ async function deployWorkflow() {
title.style.color = "green"; title.style.color = "green";
deployMetaNode.widgets[1].value = data.workflow_id; deployMetaNode.widgets[1].value = data.workflow_id;
deployMetaNode.widgets[2].value = data.version; deployMetaNode.widgets[2].value = 2;
graph.change(); graph.change();
infoDialog.show( if (data.workflow_id) {
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`, const prompt_with_workflow_id = await app.graphToPrompt();
);
// // Refresh the workflows list in the sidebar const body = {
// const sidebarEl = document.querySelector( api_url: apiUrl,
// '.comfy-sidebar-tab[data-id="search"]', workflow: prompt_with_workflow_id.workflow,
// ); workflow_id: data.workflow_id,
// if (sidebarEl) { workflow_api: prompt_with_workflow_id.output,
// refreshWorkflowsList(sidebarEl); comment: "chore: apply workflow id",
// } };
let new_version_data = await fetch("/comfyui-deploy/workflow/version", {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
});
if (new_version_data.status !== 200) {
throw new Error(await new_version_data.text());
}
}
infoDialog.show(
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: 2 <br/>`
);
setTimeout(() => { setTimeout(() => {
title.textContent = "Deploy"; title.textContent = "Deploy";
@@ -1489,6 +1431,24 @@ export class InfoDialog extends ComfyDialog {
</div> </div>
`); `);
} }
showError(title, message) {
this.show(`
<div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; color: #ff4444; display: flex; align-items: center; gap: 8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
${title}
</h3>
<div style="padding: 12px;">
${message}
</div>
</div>
`);
// Set higher z-index to appear above config dialog
this.element.style.zIndex = 1010;
}
} }
export class LoadingDialog extends ComfyDialog { export class LoadingDialog extends ComfyDialog {
@@ -1524,9 +1484,11 @@ export class LoadingDialog extends ComfyDialog {
showLoading(title, message) { showLoading(title, message) {
this.show(` this.show(`
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset"> <div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${this.loadingIcon <h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${
this.loadingIcon
}</h3> }</h3>
${message ${
message
? `<label style="max-width: 100%; white-space: pre-wrap; word-wrap: break-word;">${message}</label>` ? `<label style="max-width: 100%; white-space: pre-wrap; word-wrap: break-word;">${message}</label>`
: "" : ""
} }
@@ -1578,7 +1540,7 @@ export class InputDialog extends InfoDialog {
} }
}, },
}), }),
], ]
), ),
]; ];
} }
@@ -1639,7 +1601,7 @@ export class ConfirmDialog extends InfoDialog {
this.close(); this.close();
}, },
}), }),
], ]
), ),
]; ];
} }
@@ -1674,12 +1636,14 @@ function getData(environment) {
if (!data) { if (!data) {
if (deployOption == "cloud") if (deployOption == "cloud")
return { return {
endpoint: "https://www.comfydeploy.com", endpoint: "https://app.comfydeploy.com",
apiUrl: "https://api.comfydeploy.com",
apiKey: "", apiKey: "",
}; };
else else
return { return {
endpoint: "http://localhost:3000", endpoint: "http://localhost:3001",
apiUrl: "http://localhost:3011",
apiKey: "", apiKey: "",
}; };
} }
@@ -1696,7 +1660,7 @@ function getData(environment) {
function saveData(data) { function saveData(data) {
localStorage.setItem( localStorage.setItem(
"comfy_deploy_env_data_" + data.environment, "comfy_deploy_env_data_" + data.environment,
JSON.stringify(data), JSON.stringify(data)
); );
} }
@@ -1745,7 +1709,7 @@ export class ConfigDialog extends ComfyDialog {
this.close(); this.close();
}, },
}), }),
], ]
), ),
]; ];
} }
@@ -1756,7 +1720,7 @@ export class ConfigDialog extends ComfyDialog {
clearTimeout(this.timeout); clearTimeout(this.timeout);
} }
save(api_key, displayName) { async save(api_key, displayName) {
const deployOption = this.container.querySelector("#deployOption").value; const deployOption = this.container.querySelector("#deployOption").value;
localStorage.setItem("comfy_deploy_env", deployOption); localStorage.setItem("comfy_deploy_env", deployOption);
@@ -1773,10 +1737,14 @@ export class ConfigDialog extends ComfyDialog {
saveData({ saveData({
endpoint, endpoint,
apiUrl: getData(deployOption).apiUrl,
apiKey, apiKey,
displayName, displayName,
environment: deployOption, environment: deployOption,
}); });
// Refresh workflow list after saving configuration
await refreshWorkflowListIfOpen();
} }
show() { show() {
@@ -1789,22 +1757,32 @@ export class ConfigDialog extends ComfyDialog {
<h3 style="margin: 0px;">Comfy Deploy Config</h3> <h3 style="margin: 0px;">Comfy Deploy Config</h3>
<label style="color: white; width: 100%;"> <label style="color: white; width: 100%;">
<select id="deployOption" style="margin: 8px 0px; width: 100%; height:30px; box-sizing: border-box;" > <select id="deployOption" style="margin: 8px 0px; width: 100%; height:30px; box-sizing: border-box;" >
<option value="cloud" ${data.environment === "cloud" ? "selected" : ""}>Cloud</option> <option value="cloud" ${
<option value="local" ${data.environment === "local" ? "selected" : ""}>Local</option> data.environment === "cloud" ? "selected" : ""
}>Cloud</option>
<option value="local" ${
data.environment === "local" ? "selected" : ""
}>Local</option>
</select> </select>
</label> </label>
<label style="color: white; width: 100%;"> <label style="color: white; width: 100%;">
Endpoint: Endpoint:
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${data.endpoint <input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${
data.endpoint
}"> }">
</label> </label>
<div style="color: white;"> <div style="color: white;">
API Key: User / Org <button style="font-size: 18px;">${data.displayName ?? "" API Key: User / Org ${
}</button> data.displayName
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${data.apiKey ? `<button style="font-size: 18px;">${data.displayName}</button>`
: ""
}
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${
data.apiKey
}"> }">
<button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;"> <button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;">
${data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy" ${
data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
} }
</button> </button>
</div> </div>
@@ -1816,21 +1794,23 @@ export class ConfigDialog extends ComfyDialog {
this.save(); this.save();
const data = getData(); const data = getData();
const uuid = const uuid = crypto.randomUUID();
Math.random().toString(36).substring(2, 15) + window.open(data.endpoint + "/auth/request/" + uuid, "_blank");
Math.random().toString(36).substring(2, 15);
window.open(data.endpoint + "/auth-request/" + uuid, "_blank");
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
clearInterval(poll); clearInterval(this.poll);
infoDialog.showMessage( infoDialog.showError(
"Timeout", "Timeout",
"Wait too long for the response, please try re-login", "Wait too long for the response, please try re-login"
); );
}, 30000); // Stop polling after 30 seconds }, 30000); // Stop polling after 30 seconds
this.poll = setInterval(() => { this.poll = setInterval(() => {
fetch(data.endpoint + "/api/auth-response/" + uuid) fetch(
`/comfyui-deploy/auth-response?request_id=${uuid}&api_url=${encodeURIComponent(
data.apiUrl
)}`
)
.then((response) => response.json()) .then((response) => response.json())
.then(async (json) => { .then(async (json) => {
if (json.api_key) { if (json.api_key) {
@@ -1843,8 +1823,9 @@ export class ConfigDialog extends ComfyDialog {
// Refresh dialog // Refresh dialog
const a = await confirmDialog.confirm( const a = await confirmDialog.confirm(
"Authenticated", "Authenticated",
`<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>`, `<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>`
); );
configDialog.show(); configDialog.show();
} }
}) })
@@ -1886,24 +1867,28 @@ const currentOrigin = window.location.origin;
// serverURL: `${currentOrigin}/comfydeploy/api/`, // serverURL: `${currentOrigin}/comfydeploy/api/`,
// }); // });
// Check if the current URL hostname starts with localhost or 127.0.0.1 const isComfyDeployDashboard = currentOrigin.includes("comfydeploy.com");
const isLocalhost =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1";
// Only register the sidebar tab if we're on localhost // Only register the sidebar tab if we're on localhost
if (isLocalhost) { if (!isComfyDeployDashboard) {
app.extensionManager.registerSidebarTab({ app.extensionManager.registerSidebarTab({
id: "search", id: "search",
icon: "pi pi-cloud-upload", icon: "pi pi-cloud-upload",
title: "Deploy", title: "Deploy",
tooltip: "Deploy and Configure", tooltip: "Deploy and Configure",
type: "custom", type: "custom",
render: (el) => { render: async (el) => {
el.innerHTML = ` el.innerHTML = `
<div style="padding: 20px;"> <div style="padding: 20px;">
<h3>Comfy Deploy</h3> <h3>Comfy Deploy</h3>
<div id="deploy-container" style="margin-bottom: 20px;"></div> <div id="deploy-container" style="margin-bottom: 20px;"></div>
<div id="machine-container" style="margin-bottom: 20px;">
<h4>Serverless Machine</h4>
<div id="machine-loading" style="display: flex; justify-content: center; align-items: center; height: 100px;">
${loadingIcon}
</div>
<ul id="machine-list" style="list-style-type: none; padding: 0; display: none;"></ul>
</div>
<div id="workflows-container" style="display: none;"> <div id="workflows-container" style="display: none;">
<h4>Your Workflows</h4> <h4>Your Workflows</h4>
<div id="workflows-loading" style="display: flex; justify-content: center; align-items: center; height: 100px;"> <div id="workflows-loading" style="display: flex; justify-content: center; align-items: center; height: 100px;">
@@ -1964,7 +1949,15 @@ if (isLocalhost) {
const workflowsList = el.querySelector("#workflows-list"); const workflowsList = el.querySelector("#workflows-list");
const workflowsLoading = el.querySelector("#workflows-loading"); const workflowsLoading = el.querySelector("#workflows-loading");
// refreshWorkflowsList(el); // Initialize machine manager
await initializeMachineManager(el, getData);
// Initialize workflows list and search
const data = getData();
if (data.apiKey) {
addWorkflowSearch(el, getData, getTimeAgo);
await initializeWorkflowsList(el, getData, getTimeAgo);
}
}, },
}); });
} }
@@ -2250,7 +2243,7 @@ function addMenuButtons(options) {
// Remove any existing CD buttons // Remove any existing CD buttons
const existingButtons = document.querySelectorAll( const existingButtons = document.querySelectorAll(
`[id^="${buttonIdPrefix}"]`, `[id^="${buttonIdPrefix}"]`
); );
existingButtons.forEach((button) => button.remove()); existingButtons.forEach((button) => button.remove());
@@ -2347,3 +2340,34 @@ function addMenuRightButtons(buttonConfigs) {
containerStyle: {}, containerStyle: {},
}); });
} }
// Function to refresh workflow list if sidebar is open
async function refreshWorkflowListIfOpen() {
try {
const workflowsContainer = document.querySelector("#workflows-container");
const workflowsList = document.querySelector("#workflows-list");
// Only refresh if workflows container is visible and has been initialized
if (!workflowsContainer || !workflowsList) {
return;
}
// Import the functions we need
const { initializeWorkflowsList } = await import("./workflow-list.js");
// Clear existing workflows and reset state safely
workflowsList.innerHTML = "";
if (window.workflowsState) {
window.workflowsState.initialized = false;
window.workflowsState.workflows = [];
window.workflowsState.offset = 0;
window.workflowsState.hasMore = true;
}
// Reinitialize the workflow list
await initializeWorkflowsList(document, getData, getTimeAgo);
console.log("Workflow list refreshed successfully");
} catch (error) {
console.error("Error refreshing workflow list:", error);
}
}
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
// Snapshot Utilities
// Centralized snapshot fetching with ComfyUI version fallback
/**
* Fetches the current snapshot with ComfyUI version fallback
* If the snapshot response has null comfyui field, it will fetch the latest ComfyUI version
* and update the snapshot with the comfyui_hash
*
* @param {Function} getDataFn - Function that returns { apiKey, apiUrl } for ComfyUI version API calls
* @returns {Promise<Object>} - The snapshot data with comfyui field populated
*/
export async function fetchSnapshot(getDataFn = null) {
try {
// Fetch the current snapshot
const response = await fetch("/snapshot/get_current");
if (!response.ok) {
throw new Error(`Snapshot fetch failed: ${response.status}`);
}
const snapshot = await response.json();
// Check if comfyui field is null and we have getDataFn for fallback
if (snapshot.comfyui === null && getDataFn) {
console.log(
"ComfyUI version is null in snapshot, fetching latest version..."
);
try {
const data = getDataFn();
if (data && data.apiKey) {
const comfyuiVersionResponse = await fetch(
`/comfyui-deploy/comfyui-version?api_url=${encodeURIComponent(
data.apiUrl || "https://api.comfydeploy.com"
)}`,
{
headers: {
Authorization: `Bearer ${data.apiKey}`,
},
}
);
if (comfyuiVersionResponse.ok) {
const versionData = await comfyuiVersionResponse.json();
if (versionData.comfyui_hash) {
console.log(
`Using ComfyUI hash from API: ${versionData.comfyui_hash}`
);
snapshot.comfyui = versionData.comfyui_hash;
}
} else {
console.warn(
"Failed to fetch ComfyUI version from API:",
comfyuiVersionResponse.status
);
}
}
} catch (error) {
console.warn("Error fetching ComfyUI version fallback:", error);
// Continue with original snapshot even if fallback fails
}
}
return snapshot;
} catch (error) {
console.error("Error fetching snapshot:", error);
throw error;
}
}
/**
* Simple snapshot fetch without ComfyUI version fallback
* Use this when you don't need the ComfyUI version fallback logic
*
* @returns {Promise<Object>} - The snapshot data as-is
*/
export async function fetchSnapshotSimple() {
const response = await fetch("/snapshot/get_current");
if (!response.ok) {
throw new Error(`Snapshot fetch failed: ${response.status}`);
}
return response.json();
}
+387
View File
@@ -0,0 +1,387 @@
// Workflow list management
let workflowsState = {
workflows: [],
offset: 0,
limit: 20,
loading: false,
hasMore: true,
initialized: false,
currentSearch: "",
};
// Make workflowsState accessible globally
window.workflowsState = workflowsState;
async function fetchWorkflows(getData, offset = 0, limit = 20, search = "") {
try {
const data = getData();
if (!data.apiKey) {
throw new Error("API key not configured");
}
const params = new URLSearchParams({
offset: offset.toString(),
limit: limit.toString(),
api_url: data.apiUrl || "https://api.comfydeploy.com",
...(search && { search }),
});
const response = await fetch(`/comfyui-deploy/workflows?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch workflows: ${response.status}`);
}
const result = await response.json();
console.log("result", result);
return Array.isArray(result) ? result : [];
} catch (error) {
console.error("Error fetching workflows:", error);
return [];
}
}
function createWorkflowItem(workflow, getTimeAgo, getData) {
const li = document.createElement("li");
li.style.cssText = `
border-bottom: 1px solid #444;
background: transparent;
transition: all 0.2s ease;
cursor: pointer;
`;
li.addEventListener("mouseenter", () => {
li.style.background = "#333";
});
li.addEventListener("mouseleave", () => {
li.style.background = "transparent";
});
// Add click handler to fetch and load workflow data
li.addEventListener("click", async () => {
try {
const data = getData();
if (!data.apiKey) {
console.error("No API key configured");
return;
}
// Show loading toast
const loadingToast = window.app.extensionManager.toast.add({
severity: "info",
summary: "Loading workflow...",
detail: `Loading "${workflow.name}"`,
life: 3000,
});
const params = new URLSearchParams({
workflow_id: workflow.id,
api_url: data.apiUrl || "https://api.comfydeploy.com",
});
const response = await fetch(`/comfyui-deploy/workflow?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch workflow: ${response.status}`);
}
const workflowData = await response.json();
console.log("Workflow data:", workflowData);
// Load the workflow into the graph
if (workflowData.versions && workflowData.versions.length > 0) {
const latestVersion = workflowData.versions[0];
if (latestVersion.workflow && window.app) {
// Load the workflow
window.app.loadGraphData(latestVersion.workflow);
// Show success toast
window.app.extensionManager.toast.add({
severity: "success",
summary: "Workflow loaded successfully",
detail: `Loaded "${workflow.name}" v${latestVersion.version}`,
life: 3000,
});
}
}
} catch (error) {
console.error("Error loading workflow:", error);
// Show error toast
window.app.extensionManager.toast.add({
severity: "error",
summary: "Failed to load workflow",
detail: error.message,
life: 5000,
});
} finally {
loadingToast.close();
}
});
const updatedDate = new Date(workflow.updated_at);
const timeAgo = getTimeAgo(updatedDate);
li.innerHTML = `
<div style="padding: 12px 16px;">
<div style="display: flex; align-items: flex-start; gap: 12px;">
${
workflow.cover_image
? `<img src="${workflow.cover_image}"
style="width: 40px; height: 40px; border-radius: 4px; object-fit: cover; flex-shrink: 0;"
onerror="this.style.display='none'">`
: `<div style="width: 40px; height: 40px; border-radius: 4px; background: #444; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #888;">
${workflow.name.charAt(0).toUpperCase()}
</div>`
}
<div style="flex: 1; min-width: 0;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<h4 style="margin: 0; font-size: 14px; font-weight: 400; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${workflow.name}
</h4>
${
workflow.pinned
? `<span style="color: #ffd700; font-size: 12px;">📌</span>`
: ""
}
</div>
${
workflow.description
? `<p style="margin: 0 0 8px 0; font-size: 12px; color: #bbb; line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
${workflow.description}
</p>`
: ""
}
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<img src="${workflow.user_icon}"
style="width: 16px; height: 16px; border-radius: 50%;"
onerror="this.style.display='none'">
<span style="font-size: 11px; color: #888;">
${workflow.user_name} • Updated ${timeAgo}
</span>
</div>
</div>
</div>
</div>
`;
return li;
}
async function loadMoreWorkflows(element, getData, getTimeAgo) {
if (workflowsState.loading || !workflowsState.hasMore) return;
workflowsState.loading = true;
const workflowsList = element.querySelector("#workflows-list");
const workflowsLoading = element.querySelector("#workflows-loading");
// Show loading indicator
workflowsLoading.style.display = "flex";
try {
const newWorkflows = await fetchWorkflows(
getData,
workflowsState.offset,
workflowsState.limit,
workflowsState.currentSearch
);
if (newWorkflows.length === 0) {
workflowsState.hasMore = false;
} else {
workflowsState.workflows.push(...newWorkflows);
workflowsState.offset += newWorkflows.length;
// Render new workflow items
newWorkflows.forEach((workflow) => {
const workflowItem = createWorkflowItem(workflow, getTimeAgo, getData);
workflowsList.appendChild(workflowItem);
});
}
} catch (error) {
console.error("Error loading more workflows:", error);
} finally {
workflowsState.loading = false;
workflowsLoading.style.display = "none";
}
}
function setupInfiniteScroll(container, element, getData, getTimeAgo) {
let isScrolling = false;
container.addEventListener("scroll", () => {
if (isScrolling) return;
const { scrollTop, scrollHeight, clientHeight } = container;
// Load more when scrolled to bottom (with 100px threshold)
if (scrollTop + clientHeight >= scrollHeight - 100) {
isScrolling = true;
loadMoreWorkflows(element, getData, getTimeAgo).finally(() => {
isScrolling = false;
});
}
});
}
async function initializeWorkflowsList(element, getData, getTimeAgo) {
const workflowsContainer = element.querySelector("#workflows-container");
const workflowsList = element.querySelector("#workflows-list");
const workflowsLoading = element.querySelector("#workflows-loading");
// Check if already initialized AND the DOM elements still exist
if (
workflowsState.initialized &&
workflowsList &&
workflowsList.children.length > 0
)
return;
try {
// Reset state (always reset when reinitializing)
workflowsState = {
workflows: [],
offset: 0,
limit: 20,
loading: false,
hasMore: true,
initialized: true,
currentSearch: "",
};
// Clear existing content in case of reinitialization
if (workflowsList) {
workflowsList.innerHTML = "";
}
// Show container and loading
workflowsContainer.style.display = "block";
workflowsLoading.style.display = "flex";
// Style the workflows list for full height scrolling
workflowsList.style.cssText = `
list-style-type: none;
padding: 0;
margin: 0;
height: calc(100vh - 550px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #666 transparent;
border-top: 1px solid #444;
`;
// Add webkit scrollbar styles
const style = document.createElement("style");
style.textContent = `
#workflows-list::-webkit-scrollbar {
width: 6px;
}
#workflows-list::-webkit-scrollbar-track {
background: transparent;
}
#workflows-list::-webkit-scrollbar-thumb {
background: #666;
border-radius: 3px;
}
#workflows-list::-webkit-scrollbar-thumb:hover {
background: #777;
}
`;
document.head.appendChild(style);
// Setup infinite scroll
setupInfiniteScroll(workflowsList, element, getData, getTimeAgo);
// Load initial workflows
await loadMoreWorkflows(element, getData, getTimeAgo);
// Show the list
workflowsList.style.display = "block";
} catch (error) {
console.error("Error initializing workflows list:", error);
workflowsLoading.innerHTML = `
<div style="text-align: center; color: #e74c3c; font-size: 12px; padding: 20px;">
<div>Failed to load workflows</div>
<button onclick="initializeWorkflowsList(this.closest('.comfy-menu'), getData, getTimeAgo)"
style="margin-top: 8px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
Retry
</button>
</div>
`;
}
}
// Search functionality
function addWorkflowSearch(element, getData, getTimeAgo) {
const workflowsContainer = element.querySelector("#workflows-container");
const h4 = workflowsContainer.querySelector("h4");
const searchContainer = document.createElement("div");
searchContainer.style.cssText = "margin-bottom: 12px;";
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "Search workflows...";
searchInput.style.cssText = `
width: 100%;
padding: 8px 12px;
border: 1px solid #555;
border-radius: 6px;
font-size: 12px;
box-sizing: border-box;
background: #333;
color: #fff;
`;
let searchTimeout;
searchInput.addEventListener("input", (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
const searchTerm = e.target.value.trim();
// Update the tracked search term
workflowsState.currentSearch = searchTerm;
// Reset state for new search
workflowsState.workflows = [];
workflowsState.offset = 0;
workflowsState.hasMore = true;
// Clear current list
const workflowsList = element.querySelector("#workflows-list");
workflowsList.innerHTML = "";
// Load with search term
workflowsState.loading = false;
await loadMoreWorkflows(element, getData, getTimeAgo);
}, 300);
});
searchContainer.appendChild(searchInput);
h4.after(searchContainer);
}
// Export the functions
export {
initializeWorkflowsList,
addWorkflowSearch,
workflowsState,
fetchWorkflows,
loadMoreWorkflows,
};