Files
DumiFlex-ComfyUI-Wildcard-P…/wp_api/__init__.py
T
Claude 4b6e669b32 feat(ai): optional AI assistant with Draft with AI for wildcards
Adds Settings > AI assistant (off by default) to connect a model the user
supplies: Ollama, LM Studio, llama.cpp or any OpenAI-compatible server, or
the Claude or OpenAI API with their own key. Calls run on ComfyUI's Python
side; the key is write-only and never returned to the browser.

The wildcard editor gets Draft with AI: the model writes options in the
style of the existing ones, a compiler drops duplicates and reserved
syntax and validates against the wildcard handler, and the user reviews
the list before it is added. Nothing is saved until Save.

Docs: built-in AI assistant page, README, network-access, release notes.
2026-10-07 22:13:07 +00:00

166 lines
6.7 KiB
Python

"""HTTP routes for the SPA Manager + extension API."""
from __future__ import annotations
import logging
import re
import uuid
from pathlib import Path
from aiohttp import web
from engine.db import backups as _db_backups
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 ai as _ai
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 image_filter as _image_filter
from wp_api import import_export as _import_export
from wp_api import library_tags as _library_tags
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 scenarios as _scenarios
from wp_api import settings as _settings
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
from wp_api import wildcard_files as _wildcard_files
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
def _read_installed_version() -> str:
"""The pack version on disk when this process started (`pyproject.toml`).
An update only takes effect on the backend after a restart, but a browser
tab keeps running the JS it loaded before. Stamping this lets the SPA and
the canvas notice they're older than the installed pack and ask for a
refresh. Regex rather than tomllib so Python 3.10 works too.
"""
try:
text = (Path(__file__).resolve().parent.parent / "pyproject.toml").read_text("utf-8")
except OSError:
return ""
m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
return m.group(1) if m else ""
INSTALLED_VERSION = _read_installed_version()
@web.middleware
async def _startup_id_middleware(request: web.Request, handler):
"""Tag our own responses with X-WP-Startup-Id and X-WP-Version.
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
if INSTALLED_VERSION:
response.headers["X-WP-Version"] = INSTALLED_VERSION
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 _start_backups() -> None:
"""Daily backup check now, then hourly on a daemon thread.
After migration, so the first daily snapshot is of the upgraded schema
(the pre-migration one already covers the old). Its own function so the
API test fixtures can stub it out instead of snapshotting every tmp DB.
"""
try:
_db_backups.start_daily_scheduler()
except Exception: # noqa: BLE001 - never crash ComfyUI over backups
logger.exception("wildcard-pipeline: backup scheduler failed to start")
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")
# Staged restore runs right after the move (which may have changed where
# the live DB is) and still before any connection opens. It swallows its
# own failures; the try is belt and braces.
try:
_db_backups.execute_pending_restore()
except Exception: # noqa: BLE001 - never crash ComfyUI on restore failure
logger.exception("wildcard-pipeline: pending db restore failed")
try:
_ensure_db_migrated()
except Exception: # noqa: BLE001 - never crash ComfyUI on migration failure
logger.exception("wildcard-pipeline: db migration failed")
_start_backups()
# 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)
_settings.register(app.router)
_test_runner.register(app.router)
_scenarios.register(app.router)
_import_export.register(app.router)
_wildcard_files.register(app.router)
_cascade.register(app.router)
_preview.register(app.router)
_tags.register(app.router)
_library_tags.register(app.router)
_models.register(app.router)
_image_filter.register(app.router)
_ai.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)