Support uninstalled / missing nodes

Previously the index was built only from the live registry, so a custom node
that wasn't installed (a red "missing" node in a downloaded workflow) was
invisible — the main point of the tool. Now:

- Backend: utfcn_core split into build_context / build_index / match. build_index
  also emits curated candidates for uninstalled source types (curated-only), and
  a new POST /utfcn/match matches missing nodes by their serialized signature
  against installed core/other-pack nodes.
- Frontend: nodeType() reads a missing placeholder's last_serialization.type;
  matchMissing() feeds serialized slots to /utfcn/match and merges the results;
  the right-click item moved to a canvas-level getNodeMenuOptions patch so it
  reaches missing placeholders too. Bulk dialog labels them "not installed".

Replace a missing node with core without installing its pack. Links are rewired
losslessly; widget values can't be carried for a node whose def is absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 10:43:13 +02:00
parent 16f4e93a3a
commit cc728eb50b
4 changed files with 202 additions and 70 deletions
+28 -4
View File
@@ -13,7 +13,9 @@ All of that is frontend behaviour (see web/utfcn.js). This backend only serves
the *analysis*: it has the live node registry, so it computes — accurately, from
real INPUT_TYPES / RETURN_TYPES — which custom nodes have safe equivalents.
GET /utfcn/scan[?refresh=1] -> { sources, candidates, stats }
GET /utfcn/scan[?refresh=1] -> { sources, candidates, stats }
POST /utfcn/match {nodes:[{type,inputs,outputs,output_names}]} -> { candidates }
(for UNINSTALLED / missing nodes in a workflow)
Curated overrides live in mappings.json (shipped) and user_mappings.json (yours).
"""
@@ -28,15 +30,23 @@ from . import utfcn_core
VERSION = "1.0.0"
_DIR = os.path.dirname(os.path.realpath(__file__))
# The scan walks the whole registry, so we cache it and only rebuild on demand.
# Snapshotting the registry is the expensive part, so we cache the context and
# the derived scan index, rebuilding only on ?refresh=1.
_CTX_CACHE = None
_INDEX_CACHE = None
def _get_ctx(refresh=False):
global _CTX_CACHE
if refresh or _CTX_CACHE is None:
_CTX_CACHE = utfcn_core.build_context(utfcn_core.load_rules(_DIR))
return _CTX_CACHE
def _get_index(refresh=False):
global _INDEX_CACHE
if refresh or _INDEX_CACHE is None:
rules = utfcn_core.load_rules(_DIR)
_INDEX_CACHE = utfcn_core.build_index(rules)
_INDEX_CACHE = utfcn_core.build_index(_get_ctx(refresh))
return _INDEX_CACHE
@@ -54,6 +64,20 @@ async def utfcn_scan(request):
return web.json_response({"sources": {}, "candidates": {}, "stats": {}, "error": str(e)}, status=500)
@routes.post("/utfcn/match")
async def utfcn_match(request):
"""Match uninstalled/missing nodes by their serialized signature."""
try:
data = await request.json()
except Exception:
return web.json_response({"candidates": {}, "error": "invalid json"}, status=400)
try:
return web.json_response({"candidates": utfcn_core.match(_get_ctx(), data.get("nodes") or [])})
except Exception as e:
print(f"[UTFCN] match failed: {e}")
return web.json_response({"candidates": {}, "error": str(e)}, status=500)
WEB_DIRECTORY = "./web"
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}