mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
203 lines
6.9 KiB
Python
203 lines
6.9 KiB
Python
"""
|
|
Sidecar Runtime (F46).
|
|
|
|
Orchestrates the Sidecar process:
|
|
1. Connects to Remote Bridge (BridgeClient).
|
|
2. Connects to Local ComfyUI (OpenClawClient).
|
|
3. Polls for jobs and executes them locally.
|
|
4. Reports results back to Bridge.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
import signal
|
|
import sys
|
|
import uuid
|
|
|
|
from connector.config import ConnectorConfig
|
|
from connector.openclaw_client import OpenClawClient
|
|
|
|
from .bridge_client import BridgeClient
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
handlers=[logging.StreamHandler()],
|
|
)
|
|
logger = logging.getLogger("Sidecar")
|
|
|
|
|
|
class SidecarRuntime:
|
|
def __init__(self):
|
|
# Configuration
|
|
self.bridge_url = os.environ.get(
|
|
"OPENCLAW_BRIDGE_URL", "https://api.openclaw.app"
|
|
)
|
|
self.worker_token = os.environ.get("OPENCLAW_WORKER_TOKEN", "")
|
|
self.worker_id = os.environ.get(
|
|
"OPENCLAW_WORKER_ID", f"worker-{uuid.uuid4().hex[:8]}"
|
|
)
|
|
|
|
# Local ComfyUI Config
|
|
self.local_config = ConnectorConfig()
|
|
|
|
# Clients
|
|
self.bridge = BridgeClient(self.bridge_url, self.worker_token, self.worker_id)
|
|
self.local = OpenClawClient(self.local_config)
|
|
|
|
self.running = False
|
|
|
|
async def start(self):
|
|
logger.info(f"Starting Sidecar Runtime (Worker ID: {self.worker_id})")
|
|
logger.info(f"Bridge URL: {self.bridge_url}")
|
|
logger.info(f"Local ComfyUI: {self.local_config.openclaw_url}")
|
|
|
|
# Validate configuration
|
|
if not self.worker_token:
|
|
logger.error("Missing OPENCLAW_WORKER_TOKEN. Exiting.")
|
|
sys.exit(1)
|
|
|
|
await self.bridge.start()
|
|
await self.local.start()
|
|
|
|
# Check connectivity
|
|
if not await self.bridge.get_health():
|
|
logger.error("Failed to connect to Bridge. Check URL/Network.")
|
|
# Continue? Retry logic inside loop is better.
|
|
|
|
local_health = await self.local.get_health()
|
|
if not local_health.get("ok"):
|
|
logger.warning(f"Local ComfyUI not reachable: {local_health.get('error')}")
|
|
|
|
self.running = True
|
|
|
|
# Register signal handlers
|
|
loop = asyncio.get_running_loop()
|
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
try:
|
|
loop.add_signal_handler(sig, lambda: asyncio.create_task(self.stop()))
|
|
except NotImplementedError:
|
|
# Windows doesn't support add_signal_handler fully
|
|
pass
|
|
|
|
await self.run_loop()
|
|
|
|
async def stop(self):
|
|
logger.info("Stopping Sidecar...")
|
|
self.running = False
|
|
await self.bridge.stop()
|
|
await self.local.stop()
|
|
|
|
async def run_loop(self):
|
|
"""Main polling loop."""
|
|
while self.running:
|
|
try:
|
|
# 1. Heartbeat
|
|
await self.bridge.report_status("idle")
|
|
|
|
# 2. Poll Jobs
|
|
jobs = await self.bridge.fetch_jobs()
|
|
for job in jobs:
|
|
await self.execute_job(job)
|
|
|
|
# 3. Backoff
|
|
await asyncio.sleep(5 if not jobs else 1)
|
|
|
|
except asyncio.CancelledError:
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"Loop error: {e}")
|
|
await asyncio.sleep(5)
|
|
|
|
async def execute_job(self, job: dict):
|
|
job_id = job.get("job_id")
|
|
template_id = job.get("template_id")
|
|
inputs = job.get("inputs", {})
|
|
|
|
logger.info(f"Executing Job {job_id} (Template: {template_id})")
|
|
|
|
try:
|
|
await self.bridge.report_status("working", {"job_id": job_id})
|
|
|
|
# Submit to Local ComfyUI
|
|
# Use submit_job (which calls /triggers/fire)
|
|
# NOTE: submit_job in OpenClawClient adds trace_id
|
|
submit_res = await self.local.submit_job(template_id, inputs)
|
|
|
|
if not submit_res.get("ok"):
|
|
error_msg = submit_res.get("error", "Submission failed")
|
|
logger.error(f"Job {job_id} failed submission: {error_msg}")
|
|
await self.bridge.submit_result(
|
|
job_id, {"status": "failed", "error": error_msg}
|
|
)
|
|
return
|
|
|
|
# Wait for result?
|
|
# OpenClawClient doesn't have "wait_for_job".
|
|
# Real implementation needs to poll history or wait for prompt_id.
|
|
# Local ComfyUI returns {"prompt_id": ...} usually.
|
|
data = submit_res.get("data", {})
|
|
prompt_id = data.get("prompt_id")
|
|
|
|
if not prompt_id:
|
|
# Synchronous error?
|
|
await self.bridge.submit_result(
|
|
job_id, {"status": "failed", "error": "No prompt_id returned"}
|
|
)
|
|
return
|
|
|
|
# Poll for completion (Simple implementation for F46 MVP)
|
|
# Ideal: WebSocket. For now: Poll history.
|
|
result = await self._wait_for_completion(prompt_id)
|
|
|
|
# Submit result to Bridge
|
|
await self.bridge.submit_result(job_id, result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Job {job_id} execution error: {e}")
|
|
await self.bridge.submit_result(
|
|
job_id, {"status": "failed", "error": str(e)}
|
|
)
|
|
|
|
async def _wait_for_completion(self, prompt_id: str, timeout: int = 300) -> dict:
|
|
"""Poll local history for job completion."""
|
|
elapsed = 0
|
|
while elapsed < timeout:
|
|
history_res = await self.local.get_history(prompt_id)
|
|
if history_res.get("ok"):
|
|
# ComfyUI history format: {prompt_id: {outputs: ..., status: ...}}
|
|
# Our OpenClaw client might wrapper this.
|
|
# Assuming OpenClaw /history/{id} wrapper returns standard shape.
|
|
# If wrapped: res["data"] -> {prompt_id: ...}
|
|
# Check implementation of /history endpoint in Connector?
|
|
# Actually OpenClawClient calls /history/{id}.
|
|
# Local ComfyUI returns {prompt_id: {outputs: ...}}
|
|
|
|
data = history_res.get("data", {})
|
|
if prompt_id in data:
|
|
# Job done!
|
|
# Extract outputs
|
|
outputs = data[prompt_id].get("outputs", {})
|
|
# Need to process outputs (upload images to bridge?)
|
|
# For F46 MVP, sending raw output metadata or first image.
|
|
return {"status": "completed", "outputs": outputs}
|
|
|
|
await asyncio.sleep(1)
|
|
elapsed += 1
|
|
|
|
return {"status": "timeout", "error": "Execution timed out"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if sys.platform == "win32":
|
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
|
|
runtime = SidecarRuntime()
|
|
try:
|
|
asyncio.run(runtime.start())
|
|
except KeyboardInterrupt:
|
|
pass
|