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.
109 lines
4.4 KiB
Python
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)
|