Files
DumiFlex-ComfyUI-Wildcard-P…/wp_api/__init__.py
T
DumiFlex 09257471f8 feat(autocomplete): lora and embedding sources, in sections beside the tags
Two more completion sources for a bare word, each behind its own setting.
Far cheaper than the tag list: ComfyUI already enumerates both folders for its
own node combos, so there is nothing to download, cache, byte-cap or parse, and
the security surface that dominated the tag work does not exist here.

Sections rather than one ranked list, because the sources have no common
ranking key. A tag ranks by post count in the tens of thousands; a model has no
count at all — you either have the file or you do not. Any flat ordering has to
invent a comparison between "12,000 posts" and "a file on disk", and whichever
is invented, tags win on weight of numbers and the user's own models sink below
the fold. Sections also let each source cap its own rows, so 20 tags cannot
crowd out 3 matching LoRAs.

The rows stay ONE FLAT ARRAY internally even though they render grouped,
because `acActive` indexes it. Grouping the state would put the keyboard
selection in charge of two coordinates and make "skip the header" a case to
handle; flat, a header simply is not in the array and cannot be selected. The
header also only appears when more than one source has hits — with a single
group it would cost a row to say what the group already says.

A model row inserts its FULL RELATIVE PATH, not the display name. Two folders
can hold the same filename and ComfyUI resolves by path, so inserting the short
name would silently pick a different file from the one on screen. The folder is
shown as the row subtitle for the same reason.

Icons are `pi-asterisk` for lora and `pi-code` for embedding — the only two
candidates from the shortlist with zero existing uses in `src/`. That check is
the method: `pi-tags` was chosen for booru tags precisely because `pi-tag`
singular is already the fixed_values module kind.

Each source is independently switchable. Someone writing danbooru prompts may
have no LoRAs at all; someone with 200 of them may find the tag list noise.
`tagSetting.ts` generalises to answer per source, keeping its host-aware split —
the canvas reads ComfyUI's settings store, the SPA reads localStorage directly
because this is called on every keystroke probe.

The settings parity test earned its keep again: it failed the moment the two
new canvas settings existed without playground mirrors.

Sources settle independently rather than being awaited together, so the popover
is never held at the speed of the slower one — the model lookup is an
in-process list scan, the tag lookup walks a multi-megabyte index.

The new routes need a ComfyUI restart before they serve. Verified that until
then the 404s degrade silently: settings persist and read back, tag suggestions
are unaffected, nothing throws.
2026-08-07 15:52:07 +03:00

109 lines
4.4 KiB
Python

"""HTTP routes for the SPA Manager + extension API."""
from __future__ import annotations
import logging
import uuid
from aiohttp import web
from engine.db.connection import get_connection
from engine.db.migrations import migrate
from engine.db.pending_move import execute_pending_move
from wp_api import bundles as _bundles
from wp_api import cascade as _cascade
from wp_api import categories as _categories
from wp_api import database as _database
from wp_api import import_export as _import_export
from wp_api import models as _models
from wp_api import modules as _modules
from wp_api import preview as _preview
from wp_api import spa as _spa
from wp_api import tags as _tags
from wp_api import templates as _templates
from wp_api import test_runner as _test_runner
logger = logging.getLogger(__name__)
# Generated once per process import. Survives reloads of the same process
# (which don't happen in ComfyUI's normal lifecycle anyway) but flips on
# every fresh ComfyUI start. The SPA caches the first value it sees and
# compares on every subsequent response; mismatch = restart happened =
# show "page is stale, refresh" banner.
STARTUP_ID = uuid.uuid4().hex
@web.middleware
async def _startup_id_middleware(request: web.Request, handler):
"""Tag our own responses with X-WP-Startup-Id.
The path check is load-bearing and was missing until 2026-08-06. This
middleware is registered on ComfyUI's application, not on a sub-app, so
without it every response the whole server sends carried the header —
ComfyUI's own index, `/api/extensions`, image outputs, other extensions'
routes. Confirmed by a remote probe: `GET /` came back stamped.
Nothing depended on that: the only consumer is `manager/api/client.ts`,
which talks exclusively to `/wp/api/*`. Being a guest in someone else's
application means not writing on their responses — see CLAUDE.md's
extension-isolation section, which draws the same line for CSS selectors
and litegraph node properties.
"""
response = await handler(request)
is_ours = request.path == "/wp" or request.path.startswith("/wp/")
if is_ours and isinstance(response, web.StreamResponse):
response.headers["X-WP-Startup-Id"] = STARTUP_ID
return response
def _ensure_db_migrated() -> None:
"""Run pending migrations on the configured DB. Idempotent."""
conn = get_connection()
try:
migrate(conn)
finally:
conn.close()
def register_routes(app: web.Application) -> None:
"""Mount all /wp + /wp/api/* routes on the given app."""
# Pending-move runs FIRST: the file operation must complete before
# any DB connection opens, otherwise the migration step below would
# operate on the wrong file (or an empty new file while the user's
# data still sits at the old location).
try:
execute_pending_move()
except Exception: # noqa: BLE001 - never crash ComfyUI on pending-move failure
logger.exception("wildcard-pipeline: pending db move failed")
try:
_ensure_db_migrated()
except Exception: # noqa: BLE001 - never crash ComfyUI on migration failure
logger.exception("wildcard-pipeline: db migration failed")
# Tag every response with the process startup id so the SPA can
# detect a ComfyUI restart and prompt the user to refresh stale tabs.
# ``app.middlewares`` is an aiohttp FrozenList — mutable until the
# app starts. Best-effort try/except so we degrade quietly if the
# host has already frozen it (we lose the banner, not the API).
try:
app.middlewares.append(_startup_id_middleware)
except RuntimeError:
logger.warning("wildcard-pipeline: app middlewares already frozen, "
"stale-page detection disabled")
_modules.register(app.router)
_bundles.register(app.router)
_templates.register(app.router)
_categories.register(app.router)
_database.register(app.router)
_test_runner.register(app.router)
_import_export.register(app.router)
_cascade.register(app.router)
_preview.register(app.router)
_tags.register(app.router)
_models.register(app.router)
# SPA fallback last — broad catch-all `/wp/{path:.*}` must not shadow
# specific `/wp/api/...` routes. aiohttp resolves more-specific routes
# first regardless of registration order, but late registration keeps
# intent obvious.
_spa.register(app.router)