Compare commits

...
Author SHA1 Message Date
KarrixLee 98c1aca606 fix: for old version dont have SENSITIVE_EXTRA_DATA_KEYS 2025-11-12 23:16:57 +08:00
KarrixLee 09eab3202f chore: support comfyui v0.3.68 or later; bump version to v2.3.10 2025-11-08 03:18:37 +08:00
KarrixLee f66adfa04f refactor: clean up imports and enhance async request handling with backward compatibility checks 2025-11-08 03:08:40 +08:00
KarrixLee 0ff751b60f refactor: add sensitive to prompt_queue 2025-11-08 02:50:27 +08:00
KarrixLee a4e3bc2571 bump version to v2.3.9 2025-10-10 21:27:38 +08:00
KarrixLee bf8e2a17b9 Enhance node handling in send_json_override: normalize dotted display IDs and improve logging for unknown nodes 2025-10-10 21:14:20 +08:00
KarrixLee 60c93b65f4 bump version to v2.3.8 2025-09-22 18:32:27 +08:00
KarrixLee 5d2ade4fd4 Karrix/add remove metadata boolean in save image (#109)
* fix: remove metadata in output node

* set to true

* refactor
2025-09-22 18:18:40 +08:00
BennyKok 1c0a4d5950 Update README with additional resources and recruitment note 2025-09-17 07:05:14 -07:00
bennykok 1a8e089c7d version bump 2025-09-12 10:37:45 -07:00
bennykok 557799cdb1 fix 2025-09-11 23:56:33 -07:00
bennykok d5f52131db fix 2025-09-11 23:53:26 -07:00
bennykok 755399a255 fix: symlink 2025-09-11 23:44:38 -07:00
bennykok ee3717b4b3 any file + psd psb support 2025-09-11 23:31:28 -07:00
KarrixLee c13828af56 fix: comfy manager error, bump to v2.3.6 2025-09-11 04:13:36 +08:00
KarrixLee ffda951cfa Bump version to 2.3.5 in Manager 2025-09-10 15:14:56 +08:00
KarrixLee ac0d8ba725 Bump version to 2.3.5 in Manager (#107)
* init

* tweak

* add: model display

* test

* add: real logic to upload model

* fix native mode

* tweak again

* add: workflow version control

* fix: versioning display

* refactor: remove unnecessary code
2025-09-10 15:13:04 +08:00
bennykok 31c38ddb9b version bump 2025-08-29 15:48:42 -07:00
bennykok 2f2c63fa11 proxy interrupt request 2025-08-29 15:47:54 -07:00
BennyKok 7675c5ba90 Update README with demo link and recruitment note
Added a link to the latest local demo and a call for team members.
2025-08-29 13:03:39 -07:00
12 changed files with 2921 additions and 152 deletions
+4
View File
@@ -2,6 +2,10 @@
Open source comfyui deployment platform, a `vercel` for generative workflow infra. (serverless hosted gpu with vertical intergation with comfyui)
Check out our latest lcoal demo -> https://github.com/comfy-deploy/comfyui-api-comfydeploy
Full backend and frontend is here -> https://github.com/comfy-deploy/comfydeploy
> [!NOTE]
> Im looking for creative hacker to join ComfyDeploy's core team! DM me on [twitter](https://x.com/BennyKokMusic)
+137
View File
@@ -0,0 +1,137 @@
import folder_paths
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalFile:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_file"},
),
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"file_url": (
"STRING",
{"multiline": False, "default": ""},
),
},
}
RETURN_TYPES = (WILDCARD,)
RETURN_NAMES = ("path",)
FUNCTION = "run"
CATEGORY = "🔗ComfyDeploy"
def run(
self,
input_id,
display_name=None,
description=None,
file_url=None,
):
import requests
import os
import uuid
from urllib.parse import urlparse
if file_url:
if file_url.startswith("http"):
# Use cache directory for saving files
cache_dir = folder_paths.get_temp_directory()
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
# Always generate random filename to avoid conflicts
parsed_url = urlparse(file_url)
original_filename = os.path.basename(parsed_url.path)
# Extract file extension from original filename if available
file_extension = ""
if original_filename and "." in original_filename:
file_extension = os.path.splitext(original_filename)[1]
else:
# Try to determine extension from content-type if no extension found
file_extension = ".bin"
# Generate random filename with preserved extension
filename = str(uuid.uuid4()) + file_extension
destination_path = os.path.join(cache_dir, filename)
print(f"Cache directory: {cache_dir}")
print(f"Destination path: {destination_path}")
print(
"Downloading external file - "
+ file_url
+ " to "
+ destination_path
)
headers = {"User-Agent": "Mozilla/5.0"}
try:
response = requests.get(
file_url,
headers=headers,
allow_redirects=True,
timeout=30, # Add timeout to prevent hanging
)
response.raise_for_status()
with open(destination_path, "wb") as out_file:
out_file.write(response.content)
print(f"External file downloaded: {file_url} to {destination_path}")
return (destination_path,)
except requests.exceptions.HTTPError as e:
error_msg = f"HTTP Error {e.response.status_code}: {e.response.reason} for URL: {file_url}"
print(f"⚠️ Download failed - {error_msg}")
if e.response.status_code == 404:
print(
"💡 This URL might have expired or the file may have been deleted"
)
# Return empty string instead of crashing
return ("",)
except requests.exceptions.RequestException as e:
error_msg = (
f"Network error downloading file from {file_url}: {str(e)}"
)
print(f"⚠️ Download failed - {error_msg}")
return ("",)
except Exception as e:
error_msg = (
f"Unexpected error downloading file from {file_url}: {str(e)}"
)
print(f"⚠️ Download failed - {error_msg}")
return ("",)
else:
print(f"External file loading: {file_url}")
return (file_url,)
else:
print(f"No file URL provided")
return ("",)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalFile": ComfyUIDeployExternalFile}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalFile": "External File (ComfyUI Deploy)"
}
+168
View File
@@ -0,0 +1,168 @@
import folder_paths
import os
import shutil
import uuid
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyDeployOutputFile:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"file_path": (
"STRING",
{
"forceInput": True,
"tooltip": "Path to the file to output and upload.",
},
),
},
"optional": {
"output_id": (
"STRING",
{"multiline": False, "default": "output_file"},
),
},
}
RETURN_TYPES = ()
FUNCTION = "run"
OUTPUT_NODE = True
CATEGORY = "🔗ComfyDeploy"
DESCRIPTION = "Outputs any file by path for upload to ComfyDeploy."
def run(self, file_path, output_id="output_file"):
if not file_path or not os.path.exists(file_path):
print(f"⚠️ File not found: {file_path}")
return {"ui": {"files": []}}
# Security checks - ensure file is within safe ComfyUI paths
try:
# Get absolute paths for comparison
file_abs_path = os.path.abspath(file_path)
base_path = folder_paths.base_path
temp_dir = folder_paths.get_temp_directory()
# Check if file is within ComfyUI base path or temp directory
if not (
file_abs_path.startswith(os.path.abspath(base_path))
or file_abs_path.startswith(os.path.abspath(temp_dir))
):
print(f"⚠️ Security: File outside allowed ComfyUI paths: {file_path}")
return {"ui": {"files": []}}
# Check for path traversal attempts (but allow absolute paths within ComfyUI)
if ".." in file_path:
print(f"⚠️ Security: Path traversal attempt detected: {file_path}")
return {"ui": {"files": []}}
except Exception as e:
print(f"⚠️ Security check failed: {str(e)}")
return {"ui": {"files": []}}
# Get the original filename and extension
original_filename = os.path.basename(file_path)
file_extension = os.path.splitext(original_filename)[1]
# Additional filename security check
if ".." in original_filename:
print(f"⚠️ Security: Insecure filename: {original_filename}")
return {"ui": {"files": []}}
results = []
# Check if file is in output folder, if not, symlink it there
try:
if file_path.startswith(self.output_dir):
# File is already in output directory - use as is
relative_path = os.path.relpath(file_path, self.output_dir)
path_parts = relative_path.split(os.sep)
if len(path_parts) > 1:
subfolder = os.sep.join(path_parts[:-1])
else:
subfolder = ""
filename = path_parts[-1]
file_type = self.type
else:
# File is not in output folder - symlink it to output/temp
print(
f"File is not in output folder, symlinking to output/temp: {file_path}"
)
output_temp_dir = os.path.join(self.output_dir, "temp")
if not os.path.exists(output_temp_dir):
os.makedirs(output_temp_dir)
# Use the existing filename but with UUID prefix to avoid conflicts
file_ext = os.path.splitext(original_filename)[1]
temp_filename = f"{uuid.uuid4()}{file_ext}"
temp_path = os.path.join(output_temp_dir, temp_filename)
# Create symlink to file in output/temp directory where upload system expects it
try:
# Remove existing symlink if it exists
if os.path.exists(temp_path):
os.remove(temp_path)
os.symlink(file_path, temp_path)
print(f"File symlinked to output/temp: {temp_path} -> {file_path}")
except OSError as e:
# Fall back to copying if symlink fails
print(f"Symlink failed ({e}), falling back to copy")
shutil.copy2(file_path, temp_path)
print(f"File copied to output/temp: {temp_path}")
# Use output/temp directory structure for upload
subfolder = "temp"
filename = temp_filename
file_type = self.type
results.append(
{
"filename": filename,
"subfolder": subfolder,
"type": file_type,
"output_id": output_id,
}
)
except Exception as e:
print(f"⚠️ Error processing file path: {str(e)}")
return {"ui": {"files": []}}
# Determine the appropriate UI key based on file type
file_ext = file_extension.lower()
if file_ext in [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff"]:
ui_key = "images"
elif file_ext in [".mp3", ".wav", ".flac", ".aac", ".ogg"]:
ui_key = "audio"
elif file_ext in [".txt", ".json", ".md", ".csv"]:
ui_key = "text_file"
elif file_ext in [".exr", ".hdr"]:
ui_key = "images" # EXR files are still images
elif file_ext in [".zip", ".psb", ".psd"]:
ui_key = "files" # Archives and Photoshop project files
else:
ui_key = "files" # Generic files
return {"ui": {ui_key: results}}
NODE_CLASS_MAPPINGS = {
"ComfyDeployOutputFile": ComfyDeployOutputFile,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyDeployOutputFile": "File Output (ComfyDeploy)",
}
+9 -6
View File
@@ -4,6 +4,7 @@ import numpy as np
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import folder_paths
from comfy.cli_args import args
class ComfyDeployOutputImage:
@@ -64,12 +65,14 @@ class ComfyDeployOutputImage:
for batch_number, image in enumerate(images):
i = 255.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
metadata = None
if not args.disable_metadata:
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.{file_type}"
+429 -33
View File
@@ -1,8 +1,5 @@
from io import BytesIO
from pprint import pprint
from aiohttp import web
import os
import requests
import folder_paths
import json
import server
@@ -13,34 +10,24 @@ import random
import traceback
import uuid
import asyncio
import logging
from urllib.parse import quote
import inspect
from urllib.parse import quote, urlencode
import threading
import hashlib
import aiohttp
from aiohttp import ClientSession, web
from aiohttp import web, ClientSession, ClientError, ClientTimeout
import aiofiles
from typing import Dict, List, Union, Any, Optional
from PIL import Image
from typing import Dict, Any
import copy
import struct
from aiohttp import web, ClientSession, ClientError, ClientTimeout, ClientResponseError
import atexit
from model_management import get_torch_device
import torch
import psutil
from collections import OrderedDict
import io
from urllib.parse import urlencode
# Global session
client_session = None
# def create_client_session():
# global client_session
# if client_session is None:
# client_session = aiohttp.ClientSession()
async def ensure_client_session():
global client_session
@@ -82,8 +69,6 @@ retry_delay_multiplier = float(os.environ.get("RETRY_DELAY_MULTIPLIER", "2"))
print(f"max_retries: {max_retries}, retry_delay_multiplier: {retry_delay_multiplier}")
import time
async def async_request_with_retry(
method, url, disable_timeout=False, token=None, **kwargs
@@ -334,9 +319,28 @@ async def post_prompt(json_data):
extra_data["client_id"] = json_data["client_id"]
if valid[0]:
outputs_to_execute = valid[2]
prompt_server.prompt_queue.put(
(number, prompt_id, prompt, extra_data, outputs_to_execute)
)
# Backward compatibility: sensitive data handling added in newer ComfyUI
sensitive_keys = getattr(execution, "SENSITIVE_EXTRA_DATA_KEYS", None)
if sensitive_keys:
sensitive = {}
for sensitive_val in sensitive_keys:
if sensitive_val in extra_data:
sensitive[sensitive_val] = extra_data.pop(sensitive_val)
prompt_server.prompt_queue.put(
(
number,
prompt_id,
prompt,
extra_data,
outputs_to_execute,
sensitive,
)
)
else:
# Old ComfyUI version without sensitive data support
prompt_server.prompt_queue.put(
(number, prompt_id, prompt, extra_data, outputs_to_execute)
)
response = {
"prompt_id": prompt_id,
"number": number,
@@ -516,6 +520,9 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
if value["class_type"] == "ComfyUIDeployExternalEXR":
value["inputs"]["exr_file"] = new_value
if value["class_type"] == "ComfyUIDeployExternalFile":
value["inputs"]["file_url"] = new_value
if value["class_type"] == "ComfyUIDeployExternalSeed":
logger.info(
f"Applied random seed {new_value} to {value['class_type']}"
@@ -672,6 +679,14 @@ async def comfy_deploy_run(request):
return web.json_response(res, status=status)
@server.PromptServer.instance.routes.post("/comfyui-deploy/interrupt")
async def interrupt_prompt(request):
data = await request.json()
prompt_id = data.get("prompt_id")
await update_run(prompt_id, Status.CANCELLED)
return web.json_response({"message": "Prompt interrupted"}, status=200)
async def stream_prompt(data, token):
# In older version, we use workflow_api, but this has inputs already swapped in nextjs frontend, which is tricky
workflow_api = data.get("workflow_api_raw")
@@ -888,6 +903,10 @@ async def upload_file_endpoint(request):
file_type = "image/png"
elif file_extension == ".webp":
file_type = "image/webp"
elif file_extension == ".zip":
file_type = "application/zip"
elif file_extension in [".psd", ".psb"]:
file_type = "image/vnd.adobe.photoshop"
else:
file_type = (
"application/octet-stream" # Default to binary file type if unknown
@@ -1308,6 +1327,9 @@ try:
is_async = asyncio.iscoroutinefunction(origin_execute)
if is_async:
# Check signature for backward compatibility (v0.3.67 has 10 params, v0.3.68+ has 11)
sig = inspect.signature(origin_execute)
has_ui_outputs = len(sig.parameters) >= 11
async def swizzle_execute(
server,
@@ -1320,12 +1342,14 @@ try:
execution_list,
pending_subgraph_results,
pending_async_nodes,
ui_outputs=None,
):
unique_id = current_item
class_type = dynprompt.get_node(unique_id)["class_type"]
last_node_id = server.last_node_id
result = await origin_execute(
# Build args list - add ui_outputs only for v0.3.68+
args = [
server,
dynprompt,
caches,
@@ -1336,12 +1360,16 @@ try:
execution_list,
pending_subgraph_results,
pending_async_nodes,
)
]
if has_ui_outputs:
args.append(ui_outputs)
result = await origin_execute(*args)
handle_execute(class_type, last_node_id, prompt_id, server, unique_id)
return result
else:
# Sync version for very old ComfyUI versions
def swizzle_execute(
server,
dynprompt,
@@ -1539,18 +1567,26 @@ async def send_json_override(self, event, data, sid=None):
)
if event == "executing" and data.get("node") is not None:
node = data.get("node")
raw_node = data.get("node")
node = str(raw_node)
if prompt_id in prompt_metadata:
# if 'progress' not in prompt_metadata[prompt_id]:
# prompt_metadata[prompt_id]["progress"] = set()
wf_api = prompt_metadata[prompt_id].workflow_api
# Normalize dotted display ids like "23.0.0.1" to base "23"
if node not in wf_api and "." in node:
base = node.split(".")[0]
if base in wf_api:
node = base
# If still unknown, skip safely
if node not in wf_api:
logger.info(f"Skipping unknown node id in 'executing': {raw_node}")
return
prompt_metadata[prompt_id].progress.add(node)
calculated_progress = len(prompt_metadata[prompt_id].progress) / len(
prompt_metadata[prompt_id].workflow_api
)
calculated_progress = len(prompt_metadata[prompt_id].progress) / len(wf_api)
calculated_progress = round(calculated_progress, 2)
# logger.info("calculated_progress", calculated_progress)
if (
prompt_metadata[prompt_id].last_updated_node is not None
@@ -1558,7 +1594,8 @@ async def send_json_override(self, event, data, sid=None):
):
return
prompt_metadata[prompt_id].last_updated_node = node
class_type = prompt_metadata[prompt_id].workflow_api[node]["class_type"]
class_type = wf_api[node]["class_type"]
logger.info(f"At: {round(calculated_progress * 100)}% - {class_type}")
asyncio.create_task(
send(
@@ -2090,6 +2127,10 @@ async def handle_upload(
file_type = "image/png"
elif file_extension == ".webp":
file_type = "image/webp"
elif file_extension == ".zip":
file_type = "application/zip"
elif file_extension in [".psd", ".psb"]:
file_type = "image/vnd.adobe.photoshop"
upload_tasks.append(
upload_file(
@@ -2555,6 +2596,10 @@ class UploadQueue:
content_type = "image/webp"
elif file_extension == ".gif":
content_type = "image/gif"
elif file_extension == ".zip":
content_type = "application/zip"
elif file_extension in [".psd", ".psb"]:
content_type = "image/vnd.adobe.photoshop"
else:
content_type = file_info.get("content_type", "application/octet-stream")
@@ -3140,6 +3185,68 @@ async def get_workflow_proxy(request):
return web.json_response({"error": str(e)}, status=500)
# fetch workflow versions (infinite scroll support)
@server.PromptServer.instance.routes.get("/comfyui-deploy/workflow/versions")
async def get_workflow_versions_proxy(request):
workflow_id = request.rel_url.query.get("workflow_id")
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", "20")
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 target URL with query params
params = {"limit": limit, "offset": offset}
if search:
params["search"] = search
query = urlencode(params)
target_url = f"{api_url}/api/workflow/{workflow_id}/versions"
if query:
target_url += f"?{query}"
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)
# fetch a specific workflow version json
@server.PromptServer.instance.routes.get("/comfyui-deploy/workflow/version")
async def get_workflow_version_proxy(request):
workflow_id = request.rel_url.query.get("workflow_id")
version = request.rel_url.query.get("version")
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}/version/{version}"
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):
@@ -3282,3 +3389,292 @@ async def get_comfyui_version_proxy(request):
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# Proxy: generate single-part upload URL
@server.PromptServer.instance.routes.post(
"/comfyui-deploy/volume/file/generate-upload-url"
)
async def proxy_generate_upload_url(request):
data = await request.json()
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/volume/file/generate-upload-url"
try:
await ensure_client_session()
async with client_session.post(
target_url,
json={
"filename": data.get("filename"),
"contentType": data.get("contentType"),
"size": data.get("size"),
},
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)
# Proxy: initiate multipart upload
@server.PromptServer.instance.routes.post(
"/comfyui-deploy/volume/file/initiate-multipart-upload"
)
async def proxy_initiate_multipart_upload(request):
data = await request.json()
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/volume/file/initiate-multipart-upload"
try:
await ensure_client_session()
async with client_session.post(
target_url,
json={
"filename": data.get("filename"),
"contentType": data.get("contentType"),
"size": data.get("size"),
},
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)
# Proxy: generate part upload URL
@server.PromptServer.instance.routes.post(
"/comfyui-deploy/volume/file/generate-part-upload-url"
)
async def proxy_generate_part_upload_url(request):
data = await request.json()
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/volume/file/generate-part-upload-url"
try:
await ensure_client_session()
async with client_session.post(
target_url,
json={
"uploadId": data.get("uploadId"),
"key": data.get("key"),
"partNumber": data.get("partNumber"),
},
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)
# Proxy: complete multipart upload
@server.PromptServer.instance.routes.post(
"/comfyui-deploy/volume/file/complete-multipart-upload"
)
async def proxy_complete_multipart_upload(request):
data = await request.json()
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/volume/file/complete-multipart-upload"
try:
await ensure_client_session()
async with client_session.post(
target_url,
json={
"uploadId": data.get("uploadId"),
"key": data.get("key"),
"parts": data.get("parts"),
},
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)
# Proxy: abort multipart upload
@server.PromptServer.instance.routes.post(
"/comfyui-deploy/volume/file/abort-multipart-upload"
)
async def proxy_abort_multipart_upload(request):
data = await request.json()
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/volume/file/abort-multipart-upload"
try:
await ensure_client_session()
async with client_session.post(
target_url,
json={
"uploadId": data.get("uploadId"),
"key": data.get("key"),
},
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)
# Proxy: add model (unified endpoint)
@server.PromptServer.instance.routes.post("/comfyui-deploy/volume/model")
async def proxy_add_model(request):
data = await request.json()
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/volume/model"
# pass body through but remove api_url key
forward_body = dict(data)
if "api_url" in forward_body:
forward_body.pop("api_url")
try:
await ensure_client_session()
async with client_session.post(
target_url, json=forward_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)
# FS: stat file (size)
@server.PromptServer.instance.routes.get("/comfyui-deploy/fs/stat")
async def fs_stat(request):
try:
import os
file_path = request.rel_url.query.get("path")
if not file_path:
return web.json_response({"error": "path is required"}, status=400)
# Basic safeguard: ensure it's a ComfyUI models path
if "/models/" not in file_path:
return web.json_response({"error": "invalid path"}, status=400)
st = os.stat(file_path)
return web.json_response({"size": st.st_size})
except FileNotFoundError:
return web.json_response({"error": "not found"}, status=404)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# Upload a multipart part directly from machine filesystem to S3 upload URL
@server.PromptServer.instance.routes.post(
"/comfyui-deploy/volume/file/upload-part-from-path"
)
async def upload_part_from_path(request):
try:
import os
import io
data = await request.json()
file_path = data.get("filePath")
upload_url = data.get("uploadUrl")
start = int(data.get("start", 0))
end = int(data.get("end", 0))
if not file_path or not upload_url:
return web.json_response(
{"error": "filePath and uploadUrl are required"}, status=400
)
if "/models/" not in file_path:
return web.json_response({"error": "invalid filePath"}, status=400)
if end <= start:
return web.json_response({"error": "invalid byte range"}, status=400)
size = end - start
await ensure_client_session()
# Important: S3 pre-signed part uploads do not support chunked transfer
# Buffer the exact part into memory to provide a Content-Length header
buffer = bytearray()
chunk_size = 4 * 1024 * 1024
with open(file_path, "rb") as f:
f.seek(start)
remaining = size
while remaining > 0:
to_read = chunk_size if remaining >= chunk_size else remaining
chunk = f.read(to_read)
if not chunk:
break
buffer.extend(chunk)
remaining -= len(chunk)
if len(buffer) != size:
return web.json_response(
{
"error": "read size mismatch",
"expected": size,
"actual": len(buffer),
},
status=500,
)
headers = {
"Content-Length": str(size),
"Content-Type": "application/octet-stream",
}
async with client_session.put(
upload_url, data=bytes(buffer), headers=headers
) as resp:
text = await resp.text()
if resp.status < 200 or resp.status >= 300:
return web.json_response(
{"error": f"upload failed: {resp.status}", "body": text},
status=resp.status,
)
etag = resp.headers.get("ETag") or resp.headers.get("etag") or ""
etag = etag.replace('"', "")
return web.json_response({"eTag": etag, "bytesSent": size})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
+1
View File
@@ -18,6 +18,7 @@ class Status(Enum):
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
CANCELLED = "cancelled"
class StreamingPrompt(BaseModel):
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-deploy"
description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
version = "2.3.3"
version = "2.3.10"
license = { file = "LICENSE" }
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
+202 -69
View File
@@ -3,12 +3,13 @@ import { api } from "../../scripts/api.js";
// import { LGraphNode } from "../../scripts/widgets.js";
LGraphNode = LiteGraph.LGraphNode;
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { initializeWorkflowsList, addWorkflowSearch } from "./workflow-list.js";
import {
initializeWorkflowsList,
addWorkflowSearch,
refreshCurrentWorkflowCard,
} 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 { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69";
import { initializeModelManager } from "./model-manager.js";
const styles = `
.comfydeploy-menu-item {
@@ -538,7 +539,9 @@ const ext = {
}
if (toInput.length) {
if (true) {
let optionIndex = options.findIndex((o) => o && o.content === "Outputs");
let optionIndex = options.findIndex(
(o) => o && o.content === "Outputs"
);
if (optionIndex === -1) optionIndex = options.length;
else optionIndex++;
options.splice(
@@ -735,7 +738,7 @@ const ext = {
"string",
inputName,
/* value=*/ "",
() => { },
() => {},
{ serialize: true }
);
@@ -1330,6 +1333,14 @@ async function deployWorkflow() {
deployMetaNode.widgets[2].value = data.version;
graph.change();
if (window.refreshCurrentWorkflowCard) {
console.log(
"Refreshing current workflow card after version update with version",
data.version
);
window.refreshCurrentWorkflowCard(workflow_id, data.version);
}
}
} catch (e) {
infoDialog.showError("Error", e.message);
@@ -1343,19 +1354,6 @@ async function deployWorkflow() {
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetchSnapshotSimple();
console.log(snapshot);
loadingDialog.close();
if (!snapshot) {
showError(
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager"
);
return;
}
const title = deploy.querySelector("#button-title");
@@ -1424,6 +1422,20 @@ async function deployWorkflow() {
if (new_version_data.status !== 200) {
throw new Error(await new_version_data.text());
} else {
// Parse the JSON response to get the actual version data
const versionData = await new_version_data.json();
if (window.refreshCurrentWorkflowCard) {
console.log(
"Refreshing current workflow card with new version",
versionData.version
);
window.refreshCurrentWorkflowCard(
data.workflow_id,
versionData.version
);
}
}
}
@@ -1718,12 +1730,14 @@ export class LoadingDialog extends ComfyDialog {
showLoading(title, message) {
this.show(`
<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>
${message
? `<label style="max-width: 100%; white-space: pre-wrap; word-wrap: break-word;">${message}</label>`
: ""
}
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${
this.loadingIcon
}</h3>
${
message
? `<label style="max-width: 100%; white-space: pre-wrap; word-wrap: break-word;">${message}</label>`
: ""
}
</div>
`);
}
@@ -2308,10 +2322,12 @@ export class ConfigDialog extends ComfyDialog {
transition: all 0.2s ease;
box-sizing: border-box;
" onfocus="this.style.borderColor='#0ea5e9'; this.style.background='#252525'; this.style.boxShadow='0 0 0 3px rgba(14, 165, 233, 0.1)'" onblur="this.style.borderColor='#404040'; this.style.background='#1a1a1a'; this.style.boxShadow='none'" onmouseover="this.style.borderColor='#525252'" onmouseout="if(document.activeElement !== this) this.style.borderColor='#404040'">
<option value="cloud" ${data.environment === "cloud" ? "selected" : ""
}>☁️ Cloud Deployment</option>
<option value="local" ${data.environment === "local" ? "selected" : ""
}>🏠 Local Development</option>
<option value="cloud" ${
data.environment === "cloud" ? "selected" : ""
}>☁️ Cloud Deployment</option>
<option value="local" ${
data.environment === "local" ? "selected" : ""
}>🏠 Local Development</option>
</select>
<div style="
position: absolute;
@@ -2376,8 +2392,9 @@ export class ConfigDialog extends ComfyDialog {
text-transform: uppercase;
letter-spacing: 0.5px;
">API Key</label>
${data.displayName
? `
${
data.displayName
? `
<div style="
background: linear-gradient(135deg, #0ea5e9, #3b82f6);
color: white;
@@ -2396,8 +2413,8 @@ export class ConfigDialog extends ComfyDialog {
${data.displayName}
</div>
`
: ""
}
: ""
}
</div>
<input
id="apiKey"
@@ -2443,10 +2460,11 @@ export class ConfigDialog extends ComfyDialog {
<polyline points="10,17 15,12 10,7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
${data.apiKey
? "Re-authenticate with ComfyDeploy"
: "Login with ComfyDeploy"
}
${
data.apiKey
? "Re-authenticate with ComfyDeploy"
: "Login with ComfyDeploy"
}
</button>
</div>
</div>
@@ -2526,14 +2544,12 @@ export class ConfigDialog extends ComfyDialog {
export const configDialog = new ConfigDialog();
const currentOrigin = window.location.origin;
// const client = new ComfyDeploy({
// bearerAuth: getData().apiKey,
// serverURL: `${currentOrigin}/comfydeploy/api/`,
// });
const referrer = document.referrer;
const isComfyDeployDashboard = currentOrigin.includes("comfydeploy.com");
const isComfyDeployDashboard =
currentOrigin.includes("comfydeploy.com") ||
(referrer && referrer.includes("comfydeploy.com"));
// Only register the sidebar tab if we're on localhost
if (!isComfyDeployDashboard) {
app.extensionManager.registerSidebarTab({
id: "search",
@@ -2547,11 +2563,67 @@ if (!isComfyDeployDashboard) {
<h3>Comfy Deploy</h3>
<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 style="
display: flex;
border-bottom: 1px solid #333;
margin-bottom: 16px;
position: relative;
">
<button
id="machine-tab"
onclick="switchTab('machine')"
style="
background: transparent;
color: white;
border: none;
padding: 12px 16px 8px 16px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
border-bottom: 2px solid #3498db;
"
onmouseover="this.style.color='#fff'"
onmouseout="if(this.style.borderBottomColor !== 'rgb(52, 152, 219)') this.style.color='#999'"
>
Machine
</button>
<button
id="model-tab"
onclick="switchTab('model')"
style="
background: transparent;
color: #999;
border: none;
padding: 12px 16px 8px 16px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
border-bottom: 2px solid transparent;
"
onmouseover="this.style.color='#fff'"
onmouseout="if(this.style.borderBottomColor !== 'rgb(52, 152, 219)') this.style.color='#999'"
>
Model
</button>
</div>
<div id="machine-tab-content">
<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="model-tab-content" style="display: none;">
<div id="model-loading" style="display: flex; justify-content: center; align-items: center; height: 100px;">
${loadingIcon}
</div>
<ul id="model-list" style="list-style-type: none; padding: 0; display: none;"></ul>
</div>
<ul id="machine-list" style="list-style-type: none; padding: 0; display: none;"></ul>
</div>
<div id="workflows-container" style="display: none;">
<h4>Your Workflows</h4>
@@ -2613,10 +2685,41 @@ if (!isComfyDeployDashboard) {
const workflowsList = el.querySelector("#workflows-list");
const workflowsLoading = el.querySelector("#workflows-loading");
// Initialize machine manager
// Initialize machine and model managers
const data = getData();
if (data.apiKey) {
await initializeMachineManager(el, getData);
await initializeModelManager(el, getData);
// Add tab switching functionality
window.switchTab = function (tabName) {
const machineTab = el.querySelector("#machine-tab");
const modelTab = el.querySelector("#model-tab");
const machineContent = el.querySelector("#machine-tab-content");
const modelContent = el.querySelector("#model-tab-content");
if (tabName === "machine") {
// Active machine tab
machineTab.style.color = "white";
machineTab.style.borderBottomColor = "#3498db";
// Inactive model tab
modelTab.style.color = "#999";
modelTab.style.borderBottomColor = "transparent";
// Show/hide content
machineContent.style.display = "block";
modelContent.style.display = "none";
} else if (tabName === "model") {
// Active model tab
modelTab.style.color = "white";
modelTab.style.borderBottomColor = "#3498db";
// Inactive machine tab
machineTab.style.color = "#999";
machineTab.style.borderBottomColor = "transparent";
// Show/hide content
modelContent.style.display = "block";
machineContent.style.display = "none";
}
};
} else {
// Hide machine container when no API key
const machineContainer = el.querySelector("#machine-container");
@@ -2672,31 +2775,57 @@ const orginal_fetch_api = api.fetchApi;
api.fetchApi = async (route, options) => {
// console.log("Fetch API called with args:", route, options, ext.native_mode);
if (route.startsWith("/prompt") && ext.native_mode) {
const info = await getSelectedWorkflowInfo();
if (ext.native_mode) {
if (route.startsWith("/prompt")) {
const info = await getSelectedWorkflowInfo();
if (!info.workflow_id) {
console.log("No workflow id found, fallback to original fetch");
return await orginal_fetch_api.call(api, route, options);
}
if (!info.workflow_id) {
console.log("No workflow id found, fallback to original fetch");
return await orginal_fetch_api.call(api, route, options);
}
console.log("info", info);
if (info) {
console.log("info", info);
if (info) {
const body = JSON.parse(options.body);
const data = {
client_id: body.client_id,
workflow_api_json: body.prompt,
workflow: body?.extra_data?.extra_pnginfo?.workflow,
is_native_run: true,
machine_id: info.machine_id,
workflow_id: info.workflow_id,
native_run_api_endpoint: info.native_run_api_endpoint,
gpu_event_id: info.gpu_event_id,
gpu: info.gpu,
};
return await fetch("/comfyui-deploy/run", {
method: "POST",
headers: {
Authorization: `Bearer ${info.cd_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
}
} else if (route.startsWith("/interrupt")) {
const info = await getSelectedWorkflowInfo();
if (!info.workflow_id) {
console.log("No workflow id found, fallback to original fetch");
return await orginal_fetch_api.call(api, route, options);
}
const body = JSON.parse(options.body);
const data = {
client_id: body.client_id,
workflow_api_json: body.prompt,
workflow: body?.extra_data?.extra_pnginfo?.workflow,
is_native_run: true,
machine_id: info.machine_id,
workflow_id: info.workflow_id,
native_run_api_endpoint: info.native_run_api_endpoint,
gpu_event_id: info.gpu_event_id,
gpu: info.gpu,
prompt_id: body.prompt_id,
};
return await fetch("/comfyui-deploy/run", {
const original_response = await orginal_fetch_api.call(
api,
route,
options
);
await fetch("/comfyui-deploy/interrupt", {
method: "POST",
headers: {
Authorization: `Bearer ${info.cd_token}`,
@@ -2704,6 +2833,7 @@ api.fetchApi = async (route, options) => {
},
body: JSON.stringify(data),
});
return original_response;
}
}
@@ -3038,6 +3168,9 @@ async function refreshWorkflowListIfOpen() {
// Reinitialize the workflow list
await initializeWorkflowsList(document, getData, getTimeAgo);
// Also refresh the current workflow card
refreshCurrentWorkflowCard();
console.log("Workflow list refreshed successfully");
} catch (error) {
console.error("Error refreshing workflow list:", error);
+2 -2
View File
@@ -1,7 +1,7 @@
// Machine Manager Script
// Handles local storage machine ID functionality
import { fetchSnapshot, fetchSnapshotSimple } from "./snapshot-utils.js";
import { fetchSnapshot } from "./snapshot-utils.js";
const MACHINE_STORAGE_KEY = "comfy_deploy_machine_id";
@@ -88,7 +88,7 @@ function showNoMachineMessage(machineLoading, machineList) {
<line x1="6" x2="6.01" y1="18" y2="18"/>
</svg>
</div>
<div style="font-weight: 500; margin-bottom: 12px;">No machine found</div>
<div style="font-weight: 500; margin-bottom: 12px; font-size: 12px;">No machine found</div>
<div style="display: flex; gap: 8px; justify-content: center;">
<button
onclick="showAddMachineDialog()"
File diff suppressed because it is too large Load Diff
-14
View File
@@ -66,17 +66,3 @@ export async function fetchSnapshot(getDataFn = null) {
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();
}
File diff suppressed because it is too large Load Diff