Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f66adfa04f | ||
|
|
0ff751b60f | ||
|
|
a4e3bc2571 | ||
|
|
bf8e2a17b9 | ||
|
|
60c93b65f4 | ||
|
|
5d2ade4fd4 | ||
|
|
1c0a4d5950 | ||
|
|
1a8e089c7d | ||
|
|
557799cdb1 | ||
|
|
d5f52131db | ||
|
|
755399a255 | ||
|
|
ee3717b4b3 | ||
|
|
c13828af56 | ||
|
|
ffda951cfa | ||
|
|
ac0d8ba725 | ||
|
|
31c38ddb9b | ||
|
|
2f2c63fa11 | ||
|
|
7675c5ba90 |
@@ -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)
|
||||
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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)",
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
+412
-31
@@ -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,8 +319,12 @@ async def post_prompt(json_data):
|
||||
extra_data["client_id"] = json_data["client_id"]
|
||||
if valid[0]:
|
||||
outputs_to_execute = valid[2]
|
||||
sensitive = {}
|
||||
for sensitive_val in execution.SENSITIVE_EXTRA_DATA_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)
|
||||
(number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)
|
||||
)
|
||||
response = {
|
||||
"prompt_id": prompt_id,
|
||||
@@ -516,6 +505,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 +664,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 +888,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 +1312,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 +1327,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 +1345,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 +1552,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 +1579,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 +2112,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 +2581,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 +3170,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 +3374,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)
|
||||
|
||||
@@ -18,6 +18,7 @@ class Status(Enum):
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
UPLOADING = "uploading"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class StreamingPrompt(BaseModel):
|
||||
|
||||
+1
-1
@@ -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.9"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
|
||||
|
||||
|
||||
+202
-69
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
+951
-27
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user