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
8 changed files with 200 additions and 3388 deletions
-2
View File
@@ -2,8 +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
> [!NOTE]
> Im looking for creative hacker to join ComfyDeploy's core team! DM me on [twitter](https://x.com/BennyKokMusic)
-359
View File
@@ -672,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")
@@ -3148,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):
@@ -3352,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.5"
version = "2.3.0"
license = { file = "LICENSE" }
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
+194 -1049
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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
File diff suppressed because it is too large Load Diff