Compare commits

...
Author SHA1 Message Date
bennykok 3cb449ae42 fix 2025-09-02 19:02:08 -07:00
bennykok 6a888548ef fix 2025-09-02 18:49:38 -07:00
bennykok 99a953c8e7 fix 2025-09-02 18:28:37 -07:00
bennykok e0447f0865 fall back to beam token setup -> cd_token 2025-09-02 18:19:18 -07:00
bennykok 31c38ddb9b version bump 2025-08-29 15:48:42 -07:00
bennykok 2f2c63fa11 proxy interrupt request 2025-08-29 15:47:54 -07:00
BennyKok 7675c5ba90 Update README with demo link and recruitment note
Added a link to the latest local demo and a call for team members.
2025-08-29 13:03:39 -07:00
bennykok 7d65d23f84 fix: add null check in findIndex to prevent TypeError in context menu
- Fixed TypeError when accessing 'content' property of null/undefined array elements
- Removed empty options.push() that was adding undefined to the array
- Bump version to 2.3.3
2025-08-26 18:34:20 -07:00
KarrixLee a91edf6371 Fix serverless machine (#106)
* refresh the machine list automatically

* update ui

* sync non local workflow

* Bump CD ver to 2.3.2 in Manager
2025-08-26 17:38:34 +08:00
KarrixLee 967709dd07 Bump CD ver to 2.3.1 in Manager 2025-08-21 19:07:55 +08:00
KarrixLee 07a827d56b Local: Sync serverless machine (#105)
* 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.

* 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.

* add: machine id when create

* remove unused code

* fetch docker steps in backend

* compare list

* can sync machine

* add: create machine from local

* add error

* add: blacklist node

* 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.

* small tweak

* tweak machine display

* chore: version to 2.3.0
2025-08-21 18:39:39 +08:00
KarrixLee 7f6eea361e tweak: add tutorial complete to true by default (#104) 2025-08-18 00:19:57 +08:00
8 changed files with 3604 additions and 352 deletions
+2
View File
@@ -2,6 +2,8 @@
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)
+172 -16
View File
@@ -579,22 +579,6 @@ async def comfy_deploy_run(request):
data = await request.json()
client_id = data.get("client_id")
# We proxy the request to Comfy Deploy, this is a native run
if "is_native_run" in data:
async with aiohttp.ClientSession() as session:
# pprint(data)
# headers = request.headers.copy()
# headers['Content-Type'] = 'application/json'
async with session.post(
data.get("native_run_api_endpoint"),
json=data,
headers={
"Content-Type": "application/json",
"Authorization": request.headers.get("Authorization"),
},
) as response:
data = await response.json()
# print(data)
if "cd_token" in data:
token = data["cd_token"]
@@ -606,6 +590,24 @@ async def comfy_deploy_run(request):
if len(parts) == 2 and parts[0].lower() == "bearer":
token = parts[1]
# We proxy the request to Comfy Deploy, this is a native run
if "is_native_run" in data:
async with aiohttp.ClientSession() as session:
data.pop("cd_token", None)
# pprint(data)
# headers = request.headers.copy()
# headers['Content-Type'] = 'application/json'
async with session.post(
data.get("native_run_api_endpoint"),
json=data,
headers={
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
) as response:
data = await response.json()
# print(data)
# 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")
# The prompt id generated from comfy deploy, can be None
@@ -672,6 +674,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")
@@ -2995,6 +3005,7 @@ async def create_workflow_proxy(request):
name = data.get("name")
workflow_json = data.get("workflow_json")
workflow_api = data.get("workflow_api")
machine_id = data.get("machine_id")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
@@ -3014,6 +3025,7 @@ async def create_workflow_proxy(request):
"name": name,
"workflow_json": json.dumps(workflow_json),
"workflow_api": json.dumps(workflow_api),
"machine_id": machine_id,
}
try:
@@ -3136,3 +3148,147 @@ async def get_workflow_proxy(request):
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# for getting a machine by id
@server.PromptServer.instance.routes.get("/comfyui-deploy/machine")
async def get_machine_proxy(request):
machine_id = request.rel_url.query.get("machine_id")
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/machine/{machine_id}"
try:
await ensure_client_session()
async with client_session.get(
target_url, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# for fetching docker steps from current snapshot
@server.PromptServer.instance.routes.post("/comfyui-deploy/snapshot-to-docker")
async def snapshot_to_docker_proxy(request):
data = await request.json()
snapshot = data.get("snapshot")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/snapshot-to-docker"
request_body = snapshot
try:
await ensure_client_session()
async with client_session.post(
target_url, json=request_body, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# update a serverless machine with machine id
@server.PromptServer.instance.routes.post("/comfyui-deploy/machine/update")
async def update_machine_proxy(request):
data = await request.json()
machine_id = data.get("machine_id")
comfyui_version = data.get("comfyui_version", None)
docker_steps = data.get("docker_steps")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/machine/serverless/{machine_id}"
request_body = {"docker_command_steps": docker_steps}
if comfyui_version:
request_body["comfyui_version"] = comfyui_version
try:
await ensure_client_session()
async with client_session.patch(
target_url, json=request_body, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@server.PromptServer.instance.routes.post("/comfyui-deploy/machine/create")
async def create_machine_proxy(request):
data = await request.json()
name = data.get("name")
docker_command_steps = data.get("docker_command_steps")
comfyui_version = data.get("comfyui_version")
api_url = data.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/machine/serverless"
request_body = {
"name": name,
"docker_command_steps": docker_command_steps,
"comfyui_version": comfyui_version,
"gpu": "A10G",
}
try:
await ensure_client_session()
async with client_session.post(
target_url, json=request_body, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# get latest comfyui version
@server.PromptServer.instance.routes.get("/comfyui-deploy/comfyui-version")
async def get_comfyui_version_proxy(request):
api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
auth_header = request.headers.get("Authorization")
if not auth_header:
return web.json_response(
{"error": "Authorization header is required"}, status=401
)
target_url = f"{api_url}/api/latest-hashes"
try:
await ensure_client_session()
async with client_session.get(
target_url, headers={"Authorization": auth_header}
) as response:
json_data = await response.json()
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
+1
View File
@@ -18,6 +18,7 @@ class Status(Enum):
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
CANCELLED = "cancelled"
class StreamingPrompt(BaseModel):
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-deploy"
description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
version = "2.2.1"
version = "2.3.4"
license = { file = "LICENSE" }
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
+942 -332
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
// Snapshot Utilities
// Centralized snapshot fetching with ComfyUI version fallback
/**
* Fetches the current snapshot with ComfyUI version fallback
* If the snapshot response has null comfyui field, it will fetch the latest ComfyUI version
* and update the snapshot with the comfyui_hash
*
* @param {Function} getDataFn - Function that returns { apiKey, apiUrl } for ComfyUI version API calls
* @returns {Promise<Object>} - The snapshot data with comfyui field populated
*/
export async function fetchSnapshot(getDataFn = null) {
try {
// Fetch the current snapshot
const response = await fetch("/snapshot/get_current");
if (!response.ok) {
throw new Error(`Snapshot fetch failed: ${response.status}`);
}
const snapshot = await response.json();
// Check if comfyui field is null and we have getDataFn for fallback
if (snapshot.comfyui === null && getDataFn) {
console.log(
"ComfyUI version is null in snapshot, fetching latest version..."
);
try {
const data = getDataFn();
if (data && data.apiKey) {
const comfyuiVersionResponse = await fetch(
`/comfyui-deploy/comfyui-version?api_url=${encodeURIComponent(
data.apiUrl || "https://api.comfydeploy.com"
)}`,
{
headers: {
Authorization: `Bearer ${data.apiKey}`,
},
}
);
if (comfyuiVersionResponse.ok) {
const versionData = await comfyuiVersionResponse.json();
if (versionData.comfyui_hash) {
console.log(
`Using ComfyUI hash from API: ${versionData.comfyui_hash}`
);
snapshot.comfyui = versionData.comfyui_hash;
}
} else {
console.warn(
"Failed to fetch ComfyUI version from API:",
comfyuiVersionResponse.status
);
}
}
} catch (error) {
console.warn("Error fetching ComfyUI version fallback:", error);
// Continue with original snapshot even if fallback fails
}
}
return snapshot;
} catch (error) {
console.error("Error fetching snapshot:", error);
throw error;
}
}
/**
* Simple snapshot fetch without ComfyUI version fallback
* Use this when you don't need the ComfyUI version fallback logic
*
* @returns {Promise<Object>} - The snapshot data as-is
*/
export async function fetchSnapshotSimple() {
const response = await fetch("/snapshot/get_current");
if (!response.ok) {
throw new Error(`Snapshot fetch failed: ${response.status}`);
}
return response.json();
}
+33 -3
View File
@@ -49,6 +49,7 @@ async function fetchWorkflows(getData, offset = 0, limit = 20, search = "") {
function createWorkflowItem(workflow, getTimeAgo, getData) {
const li = document.createElement("li");
let loadingToast = null;
li.style.cssText = `
border-bottom: 1px solid #444;
background: transparent;
@@ -74,7 +75,7 @@ function createWorkflowItem(workflow, getTimeAgo, getData) {
}
// Show loading toast
const loadingToast = window.app.extensionManager.toast.add({
loadingToast = window.app.extensionManager.toast.add({
severity: "info",
summary: "Loading workflow...",
detail: `Loading "${workflow.name}"`,
@@ -108,6 +109,33 @@ function createWorkflowItem(workflow, getTimeAgo, getData) {
// Load the workflow
window.app.loadGraphData(latestVersion.workflow);
// Wait a bit for the graph to fully load before checking for ComfyDeploy node
await new Promise((resolve) => setTimeout(resolve, 100));
// Check if ComfyDeploy node exists, if not add it back
const graph = window.app.graph;
let deployMeta = graph.findNodesByType("ComfyDeploy");
if (deployMeta.length === 0) {
// Add ComfyDeploy node with workflow metadata
graph.beforeChange();
const node = LiteGraph.createNode("ComfyDeploy");
node.configure({
widgets_values: [
workflow.name, // workflow_name
workflow.id, // workflow_id
latestVersion.version, // version
],
});
node.pos = [0, 0];
graph.add(node);
graph.afterChange();
console.log(
`Added ComfyDeploy node with: name="${workflow.name}", id="${workflow.id}", version="${latestVersion.version}"`
);
}
// Show success toast
window.app.extensionManager.toast.add({
severity: "success",
@@ -127,7 +155,9 @@ function createWorkflowItem(workflow, getTimeAgo, getData) {
life: 5000,
});
} finally {
loadingToast.close();
if (loadingToast) {
loadingToast.close();
}
}
});
@@ -279,7 +309,7 @@ async function initializeWorkflowsList(element, getData, getTimeAgo) {
list-style-type: none;
padding: 0;
margin: 0;
height: calc(100vh - 350px);
height: calc(100vh - 550px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #666 transparent;