Files

182 lines
6.1 KiB
Python

"""
API Routes for ComfyUI-Doctor.
Refactored from __init__.py for testability.
"""
import logging
import time
import aiohttp.web as web
from .admin_guard import validate_admin_request
from .api_response import admin_denied_response, error_response
from . import job_manager
from .providers import registry as provider_registry
try:
from ..config import CONFIG
except ImportError as import_error:
from import_compat import ensure_absolute_import_fallback_allowed
ensure_absolute_import_fallback_allowed(import_error)
from config import CONFIG
logger = logging.getLogger("ComfyUI-Doctor.routes")
def _admin_denied_response(code: str, message: str):
return admin_denied_response(web, code, message)
async def _optional_json_payload(request):
if not getattr(request, "can_read_body", False):
return {}
try:
return await request.json()
except Exception:
return {}
async def api_plugins(request):
"""
Plugin trust report (scan-only).
Returns the trust classification for each discovered community plugin without importing code.
"""
try:
from pathlib import Path
try:
from ..pipeline.plugins import scan_plugins
except ImportError as import_error:
from import_compat import ensure_absolute_import_fallback_allowed
ensure_absolute_import_fallback_allowed(import_error)
from pipeline.plugins import scan_plugins
plugin_dir = Path(__file__).resolve().parent.parent / "pipeline" / "plugins" / "community"
report = scan_plugins(plugin_dir)
def sanitize_manifest(manifest):
if not isinstance(manifest, dict):
return None
keys = [
"id",
"name",
"version",
"author",
"min_doctor_version",
"signature_alg",
]
out = {k: manifest.get(k) for k in keys if k in manifest}
if "signature" in manifest:
out["has_signature"] = bool(manifest.get("signature"))
return out
plugins = []
trust_counts = {}
for entry in report:
trust = entry.get("trust")
trust_counts[trust] = trust_counts.get(trust, 0) + 1
plugins.append(
{
"file": getattr(entry.get("file"), "name", str(entry.get("file"))),
"plugin_id": entry.get("plugin_id"),
"trust": trust,
"reason": entry.get("reason"),
"manifest": sanitize_manifest(entry.get("manifest")),
}
)
payload = {
"config": {
"enabled": bool(getattr(CONFIG, "enable_community_plugins", False)),
"allowlist_count": len(getattr(CONFIG, "plugin_allowlist", []) or []),
"blocklist_count": len(getattr(CONFIG, "plugin_blocklist", []) or []),
"signature_required": bool(getattr(CONFIG, "plugin_signature_required", False)),
"signature_alg": getattr(CONFIG, "plugin_signature_alg", "hmac-sha256"),
"signature_key_configured": bool(getattr(CONFIG, "plugin_signature_key", "") or ""),
},
"trust_counts": trust_counts,
"plugins": plugins,
}
return web.json_response({"success": True, "plugins": payload})
except Exception as e:
logger.error(f"Plugins API error: {str(e)}")
return error_response(web, str(e), status=500)
async def api_get_job(request):
"""
Get job status and details.
"""
job_id = request.match_info.get("job_id", "")
job = job_manager.get_job_manager().get_job(job_id)
if not job:
return error_response(web, "Job not found", status=404, code="not_found")
return web.json_response({
"success": True,
"job": job.to_dict()
})
async def api_resume_job(request):
"""
Resume a suspended/failed job.
Note: Actual resume logic depends on provider implementation.
"""
payload = await _optional_json_payload(request)
allowed, code, message = validate_admin_request(request, payload=payload)
if not allowed:
return _admin_denied_response(code, message)
job_id = request.match_info.get("job_id", "")
job = job_manager.get_job_manager().get_job(job_id)
if not job:
return error_response(web, "Job not found", status=404, code="not_found")
if job.status.value not in ("failed", "cancelled", "pending"):
return error_response(
web,
f"Cannot resume job in state {job.status}",
status=400,
code="invalid_job_state",
)
job_manager.get_job_manager().update_job(job_id, status=job_manager.JobStatus.PENDING, meta_update={"resumed_at": time.time()})
return web.json_response({"success": True, "message": "Job resumed"})
async def api_cancel_job(request):
"""
Cancel a running/pending job.
"""
payload = await _optional_json_payload(request)
allowed, code, message = validate_admin_request(request, payload=payload)
if not allowed:
return _admin_denied_response(code, message)
job_id = request.match_info.get("job_id", "")
if job_manager.get_job_manager().update_job(job_id, status=job_manager.JobStatus.CANCELLED):
return web.json_response({"success": True, "message": "Job cancelled"})
return error_response(web, "Job not found", status=404, code="not_found")
async def api_provider_status(request):
"""
Get specific provider status/capability.
"""
provider_id = request.match_info.get("provider_id", "")
cap = provider_registry.ProviderRegistry.get_capability(provider_id)
if not cap:
return error_response(web, "Provider not found", status=404, code="not_found")
return web.json_response({
"success": True,
"provider_id": provider_id,
"capabilities": {
"supports_submit": cap.supports_submit,
"requires_auth": cap.requires_auth,
"concurrency_limit": cap.concurrency_limit,
}
})