fix(backend): restore imports lost in the module split

Saving a workflow recipe failed for any workflow with a model loader:
recipe_images.py called _resolve_exact_model_reference and hashlib, which
stayed behind in recipe_schema.py when 6ad975c split the module. The
resolver moves to path_utils.py so both recipe modules can import it
without a cycle.

The same split left media_routes.py without time/tempfile (card
thumbnails silently fell back to full-size covers) and model_metadata.py
without _resolve_paths_to_model_info_sync (editing a moved model failed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-09-27 17:57:24 +08:00
co-authored by Claude Opus 5.5
parent a4cbe72086
commit 42d8c7bbfc
7 changed files with 49 additions and 35 deletions
+2 -1
View File
@@ -64,7 +64,8 @@ DOM or live LiteGraph state.
### Backend
- `api/config.py` owns configured paths and active model-folder types.
- `api/path_utils.py` owns containment, filename validation, and atomic JSON writes;
- `api/path_utils.py` owns containment, filename validation, atomic JSON writes, and resolving
a saved model value inside the model folders;
`api/utils.py` is a compatibility export surface.
- `api/metadata.py` owns sidecar and safetensors metadata extraction.
- `api/model_catalog.py`, `api/model_resolution.py`, `api/model_metadata.py`, and
+6 -1
View File
@@ -1,6 +1,11 @@
# 📈 Anomalous Model Browser Changelog
## v1.57.2 Beta (Canvas Performance Hotfix) — 2026-09-26
## v1.57.2 Beta (Hotfix: Canvas Performance, Saving Workflows) — 2026-09-27
### 💾 Saving workflows and Prompt Notes (保存工作流与提示词笔记)
- **Saving a workflow works again (保存工作流失败)**: since v1.57, **Save Current Workflow** failed with "Could not save this Workflow Recipe" for any workflow with a model loader, because splitting the backend into smaller files left the save path calling a function that was no longer imported. The same split also made model cards load full-size cover images instead of thumbnails (slow on big folders), and made editing details of a model that had moved fail.
- **Prompt Notes with UNet models (提示词笔记与 UNet 模型)**: sending a note with a UNet / diffusion model to the canvas built a graph that could not run, because its LoRAs waited for a CLIP the UNet does not have. LoRAs of a UNet now load model-only, and the note says to connect your own text encoder to the two prompt nodes. Models whose metadata names no base model (most models not downloaded from Civitai) were missing from every group; they now appear under **No base model listed**. Listing models no longer blocks ComfyUI while it reads the folders.
- Installed v1.57.2 before 2026-09-27 through the version panel? Pick v1.57.2 there once more to get this build; updating through ComfyUI Manager gets it as usual.
### ⚡ Smooth canvas dragging again (画布拖动卡顿修复)
- **Dragging and zooming the canvas no longer slows down with this plugin installed (安装插件后拖动画布掉帧)**: one stylesheet rule, which only hid the sidebar tooltip while the Toolbox or Settings popup was open, made the browser re-check the whole page every time any element's inline style changed. ComfyUI moves every text-box widget on every frame while you pan or zoom, so each frame paid for it, even with the browser closed. Measured on a 300-node workflow in ComfyUI: style work while panning drops from about 11× the no-plugin cost back to the same as without the plugin.
+2
View File
@@ -5,7 +5,9 @@ import hashlib
import json
import os
import struct
import tempfile
import threading
import time
import urllib.parse
from aiohttp import web
+1
View File
@@ -14,6 +14,7 @@ from .model_constants import (
CIVITAI_BACKUP_SUFFIXES, MEDIA_EXTENSIONS, MODEL_EXTENSIONS,
PREVIEW_SUFFIXES, SIDECAR_SUFFIXES,
)
from .model_catalog import _resolve_paths_to_model_info_sync
from .utils import require_filename, resolve_folder_subdir, resolve_within
def _first_existing_sidecar(base_path, suffixes):
+35
View File
@@ -6,6 +6,8 @@ import tempfile
import folder_paths
from .recipe_constants import MODEL_FILE_SUFFIXES
def resolve_within(base_dir, *parts):
"""Resolve a path and require it to stay inside base_dir, including through symlinks."""
@@ -19,6 +21,39 @@ def resolve_within(base_dir, *parts):
return candidate
def _model_roots():
for folder_type in getattr(folder_paths, "folder_names_and_paths", {}):
try:
paths = folder_paths.get_folder_paths(folder_type)
except Exception:
continue
for path_index, base_dir in enumerate(paths or []):
if os.path.isdir(base_dir):
yield folder_type, path_index, os.path.realpath(base_dir)
def _resolve_exact_model_reference(saved_value):
"""Resolve one saved model value without recursive scanning or hashing."""
if not isinstance(saved_value, str) or not saved_value.strip():
return None
relative_value = saved_value.replace("/", os.sep)
for folder_type, path_index, base_dir in _model_roots():
candidate = os.path.realpath(os.path.join(base_dir, relative_value))
try:
if os.path.commonpath((base_dir, candidate)) != base_dir:
continue
except ValueError:
continue
if not os.path.isfile(candidate) or not candidate.lower().endswith(MODEL_FILE_SUFFIXES):
continue
return {
"path": candidate,
"folder_type": folder_type,
"path_index": path_index,
}
return None
def get_folder_root(folder_type, path_idx=0):
paths = folder_paths.get_folder_paths(folder_type)
if not paths or not isinstance(path_idx, int) or path_idx < 0 or path_idx >= len(paths):
+2
View File
@@ -1,6 +1,7 @@
"""Contained Recipe image assets and bounded output-gallery inspection."""
from io import BytesIO
import hashlib
import heapq
import json
import os
@@ -8,6 +9,7 @@ import os
import folder_paths
from PIL import Image
from .path_utils import _resolve_exact_model_reference
from .recipe_constants import *
from .utils import require_filename, resolve_within
from .workflow_schema import _node_type, _parameter_signature, _workflow_node_signature
+1 -33
View File
@@ -10,6 +10,7 @@ import folder_paths
from .metadata import get_metadata
from .recipe_constants import *
from .path_utils import _model_roots, _resolve_exact_model_reference
from .recipe_images import _attach_preview_snapshots
from .utils import require_filename, resolve_within
from .workflow_schema import (
@@ -44,39 +45,6 @@ def _model_reference_specs(node):
return specs
def _model_roots():
for folder_type in getattr(folder_paths, "folder_names_and_paths", {}):
try:
paths = folder_paths.get_folder_paths(folder_type)
except Exception:
continue
for path_index, base_dir in enumerate(paths or []):
if os.path.isdir(base_dir):
yield folder_type, path_index, os.path.realpath(base_dir)
def _resolve_exact_model_reference(saved_value):
"""Resolve one saved model value without recursive scanning or hashing."""
if not isinstance(saved_value, str) or not saved_value.strip():
return None
relative_value = saved_value.replace("/", os.sep)
for folder_type, path_index, base_dir in _model_roots():
candidate = os.path.realpath(os.path.join(base_dir, relative_value))
try:
if os.path.commonpath((base_dir, candidate)) != base_dir:
continue
except ValueError:
continue
if not os.path.isfile(candidate) or not candidate.lower().endswith(MODEL_FILE_SUFFIXES):
continue
return {
"path": candidate,
"folder_type": folder_type,
"path_index": path_index,
}
return None
def _identity_for_reference(saved_value):
resolved = _resolve_exact_model_reference(saved_value)
if not resolved: