feat(search): draw real node box from disabled-pack source
Publish to Comfy registry / Publish Custom Node to registry (push) Has been cancelled

Click a node name (or 'Draw this node') in the mirror-search palette to render
an imitation ComfyUI node box with its real input sockets, widget defaults, and
output sockets. Since disabled packs aren't loaded (no /object_info entry), a
new read-only backend module (node_introspect.py) AST-parses the pack on disk —
never importing or executing it — to recover INPUT_TYPES / RETURN_TYPES from a
literal NODE_CLASS_MAPPINGS. New GET /nodes-stats/node-schema endpoint resolves
the disabled pack dir (handling @version suffixes / case) and returns the schema
off the event loop. Frontend lazily fetches + caches per node, renders sockets
vs widgets with type-colored dots, and falls back to a placeholder for packs
that build their node list dynamically.

End-to-end against the live install: 67/68 disabled packs resolve on disk, ~92%
of nodes render a real box, the rest fall back cleanly. Adds 7 parser unit
tests (36 total green). Bump to 1.6.0.
This commit is contained in:
2026-06-21 15:23:58 +02:00
parent 77c159a918
commit acaa9f0168
6 changed files with 569 additions and 12 deletions
+29
View File
@@ -1,3 +1,4 @@
import asyncio
import logging
import threading
@@ -5,6 +6,7 @@ from aiohttp import web
from server import PromptServer
from .mapper import NodePackageMapper, ModelMapper
from .node_introspect import find_disabled_pack_path, get_node_schema
from .tracker import UsageTracker
logger = logging.getLogger(__name__)
@@ -115,6 +117,33 @@ async def reset_stats(request):
return web.json_response({"error": "internal error"}, status=500)
@routes.get("/nodes-stats/node-schema")
async def get_node_schema_route(request):
"""Parse a disabled pack's source and return one node's input/output schema.
Read-only and never imports/executes the pack. Used by the mirror-search
palette to draw a faithful node box for a node that isn't loaded.
"""
try:
class_type = request.query.get("class_type")
pack = request.query.get("pack")
if not class_type or not pack:
return web.json_response({"error": "class_type and pack required"}, status=400)
def _resolve():
path = find_disabled_pack_path(pack)
if not path:
return {"parseable": False, "reason": "source_not_found"}
return get_node_schema(class_type, path)
# AST-parsing a whole pack can take tens of ms; keep it off the event loop.
schema = await asyncio.get_event_loop().run_in_executor(None, _resolve)
return web.json_response(schema)
except Exception:
logger.error("nodes-stats: error parsing node schema", exc_info=True)
return web.json_response({"error": "internal error"}, status=500)
@routes.get("/nodes-stats/trials")
async def get_trials(request):
try: