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
12 changed files with 268 additions and 3775 deletions
-4
View File
@@ -2,10 +2,6 @@
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
@@ -1,137 +0,0 @@
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
@@ -1,168 +0,0 @@
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)",
}
+6 -9
View File
@@ -4,7 +4,6 @@ import numpy as np
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import folder_paths
from comfy.cli_args import args
class ComfyDeployOutputImage:
@@ -65,14 +64,12 @@ 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 = 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]))
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}"
+33 -429
View File
@@ -1,5 +1,8 @@
from io import BytesIO
from pprint import pprint
from aiohttp import web
import os
import requests
import folder_paths
import json
import server
@@ -10,24 +13,34 @@ import random
import traceback
import uuid
import asyncio
import inspect
from urllib.parse import quote, urlencode
import logging
from urllib.parse import quote
import threading
import hashlib
import aiohttp
from aiohttp import web, ClientSession, ClientError, ClientTimeout
from aiohttp import ClientSession, web
import aiofiles
from typing import Dict, Any
from typing import Dict, List, Union, Any, Optional
from PIL import Image
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
@@ -69,6 +82,8 @@ 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
@@ -319,28 +334,9 @@ async def post_prompt(json_data):
extra_data["client_id"] = json_data["client_id"]
if valid[0]:
outputs_to_execute = valid[2]
# 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)
)
prompt_server.prompt_queue.put(
(number, prompt_id, prompt, extra_data, outputs_to_execute)
)
response = {
"prompt_id": prompt_id,
"number": number,
@@ -520,9 +516,6 @@ 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']}"
@@ -679,14 +672,6 @@ 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")
@@ -903,10 +888,6 @@ 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
@@ -1327,9 +1308,6 @@ 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,
@@ -1342,14 +1320,12 @@ 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
# Build args list - add ui_outputs only for v0.3.68+
args = [
result = await origin_execute(
server,
dynprompt,
caches,
@@ -1360,16 +1336,12 @@ 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,
@@ -1567,26 +1539,18 @@ async def send_json_override(self, event, data, sid=None):
)
if event == "executing" and data.get("node") is not None:
raw_node = data.get("node")
node = str(raw_node)
node = data.get("node")
if prompt_id in prompt_metadata:
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
# if 'progress' not in prompt_metadata[prompt_id]:
# prompt_metadata[prompt_id]["progress"] = set()
prompt_metadata[prompt_id].progress.add(node)
calculated_progress = len(prompt_metadata[prompt_id].progress) / len(wf_api)
calculated_progress = len(prompt_metadata[prompt_id].progress) / len(
prompt_metadata[prompt_id].workflow_api
)
calculated_progress = round(calculated_progress, 2)
# logger.info("calculated_progress", calculated_progress)
if (
prompt_metadata[prompt_id].last_updated_node is not None
@@ -1594,8 +1558,7 @@ async def send_json_override(self, event, data, sid=None):
):
return
prompt_metadata[prompt_id].last_updated_node = node
class_type = wf_api[node]["class_type"]
class_type = prompt_metadata[prompt_id].workflow_api[node]["class_type"]
logger.info(f"At: {round(calculated_progress * 100)}% - {class_type}")
asyncio.create_task(
send(
@@ -2127,10 +2090,6 @@ 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(
@@ -2596,10 +2555,6 @@ 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")
@@ -3185,68 +3140,6 @@ 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):
@@ -3389,292 +3282,3 @@ 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,7 +18,6 @@ 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.10"
version = "2.3.0"
license = { file = "LICENSE" }
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
+208 -1049
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,7 +1,7 @@
// Machine Manager Script
// Handles local storage machine ID functionality
import { fetchSnapshot } from "./snapshot-utils.js";
import { fetchSnapshot, fetchSnapshotSimple } 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; font-size: 12px;">No machine found</div>
<div style="font-weight: 500; margin-bottom: 12px;">No machine found</div>
<div style="display: flex; gap: 8px; justify-content: center;">
<button
onclick="showAddMachineDialog()"
@@ -1475,7 +1475,7 @@ window.applySyncChanges = async function (machineId) {
// Close the dialog
closeSyncDialog();
if (response.id) {
if (response.ok) {
window.app.extensionManager.toast.add({
severity: "success",
summary: "Machine updated successfully",
File diff suppressed because it is too large Load Diff
+14
View File
@@ -66,3 +66,17 @@ 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