From 2987c036e2b025a863f824e404144f86b80ef2be Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sat, 18 Jul 2026 10:21:49 +0200 Subject: [PATCH] refactor: unify settings schema and dynamic node rules exposure across backend and frontend --- __init__.py | 10 ++++-- core/metadata_audit.py | 11 +++--- core/settings.py | 34 +++++++++++++++++++ .../search/download_target_methods.js | 10 ++++++ web/resolver/shell/workflow_state_methods.js | 14 ++++++-- web/resolver/views/options_methods.js | 17 +++++++--- 6 files changed, 83 insertions(+), 13 deletions(-) diff --git a/__init__.py b/__init__.py index dc3c5e0..ccc340b 100644 --- a/__init__.py +++ b/__init__.py @@ -4724,12 +4724,14 @@ class ModelResolverExtension: @json_api_endpoint("capabilities") async def get_capabilities(request): """Get optional source capabilities available in this install.""" + from .core.workflow_analyzer import NODE_TYPE_MODEL_WIDGET_CATEGORIES return web.json_response( { "sources": { "civarchive": is_civarchive_available(), "lora_manager_archive": is_lora_manager_archive_available() - } + }, + "node_rules": NODE_TYPE_MODEL_WIDGET_CATEGORIES } ) @@ -5011,7 +5013,11 @@ class ModelResolverExtension: async def get_settings_route(request): """Return persisted settings (API keys, preferences).""" data = await asyncio.to_thread(load_resolver_settings) - return web.json_response(data) + from .core.settings import get_settings_schema + return web.json_response({ + "settings": data, + "schema": get_settings_schema() + }) @routes.post("/model_resolver/settings") @json_api_endpoint("settings POST") diff --git a/core/metadata_audit.py b/core/metadata_audit.py index a6ac796..b924371 100644 --- a/core/metadata_audit.py +++ b/core/metadata_audit.py @@ -73,10 +73,13 @@ def _extract_size_from_object(data: Any, prefix: str = "") -> Optional[Tuple[int if not isinstance(data, dict): return None - for field_path, value, multiplier in _iter_size_fields(data, prefix): - size = _coerce_size_bytes(value, multiplier=multiplier) - if size is not None: - return size, field_path + from .type_utils import extract_file_size + size = extract_file_size(data) + if size is not None: + for field_path, value, multiplier in _iter_size_fields(data, prefix): + if _coerce_size_bytes(value, multiplier=multiplier) == size: + return size, field_path + return size, f"{prefix}size" return None diff --git a/core/settings.py b/core/settings.py index 6e65cd1..3cd17bb 100644 --- a/core/settings.py +++ b/core/settings.py @@ -16,6 +16,35 @@ from .type_utils import normalize_download_category, to_bool DOWNLOAD_PATH_MODES = {"suggested", "template", "manual"} DOWNLOAD_BACKENDS = {"python", "aria2"} +SETTINGS_SCHEMA: List[Dict[str, Any]] = [ + { "serverKey": "civitai_key", "localKey": "ModelResolver.civitaiApiKey", "type": "string", "default": "" }, + { "serverKey": "civitai_session_token", "localKey": "ModelResolver.civitaiSessionToken", "type": "string", "default": "" }, + { "serverKey": "civitai_use_trpc_search", "localKey": "ModelResolver.civitaiUseTrpcSearch", "type": "boolean", "default": True }, + { "serverKey": "civitai_use_api_search", "localKey": "ModelResolver.civitaiUseApiSearch", "type": "boolean", "default": True }, + { "serverKey": "civitai_use_html_fallback", "localKey": "ModelResolver.civitaiUseHtmlFallback", "type": "boolean", "default": True }, + { "serverKey": "hf_token", "localKey": "ModelResolver.huggingFaceToken", "type": "string", "default": "" }, + { "serverKey": "brave_search_api_key", "localKey": "ModelResolver.braveSearchApiKey", "type": "string", "default": "" }, + { "serverKey": "hf_use_api_search", "localKey": "ModelResolver.hfUseApiSearch", "type": "boolean", "default": True }, + { "serverKey": "hf_use_comfy_org_fallback", "localKey": "ModelResolver.hfUseComfyOrgFallback", "type": "boolean", "default": True }, + { "serverKey": "hf_use_brave_fallback", "localKey": "ModelResolver.hfUseBraveFallback", "type": "boolean", "default": True }, + { "serverKey": "auto_fill_base_model", "localKey": "ModelResolver.autoFillBaseModel", "type": "boolean", "default": True }, + { "serverKey": "auto_fill_subfolder", "localKey": "ModelResolver.autoFillSubfolder", "type": "boolean", "default": True }, + { "serverKey": "auto_refresh_comfy_models_after_apply", "localKey": "ModelResolver.autoRefreshComfyModelsAfterApply", "type": "boolean", "default": True }, + { "serverKey": "workflow_hash_metadata_enabled", "localKey": "ModelResolver.workflowHashMetadataEnabled", "type": "boolean", "default": True }, + { "serverKey": "workflow_dependency_marker_enabled", "localKey": "ModelResolver.workflowDependencyMarkerEnabled", "type": "boolean", "default": False }, + { "serverKey": "download_backend", "localKey": "ModelResolver.downloadBackend", "type": "backend", "default": "python" }, + { "serverKey": "aria2c_path", "localKey": "ModelResolver.aria2cPath", "type": "string", "default": "" }, + { "serverKey": "aria2_auto_stop_daemon", "localKey": "ModelResolver.aria2AutoStopDaemon", "type": "boolean", "default": True }, + { "serverKey": "download_path_mode", "localKey": "ModelResolver.downloadPathMode", "type": "pathMode", "default": "suggested" }, + { "serverKey": "download_path_templates", "localKey": "ModelResolver.downloadPathTemplates", "type": "json", "default": {} }, + { "serverKey": "base_model_path_mappings", "localKey": "ModelResolver.baseModelPathMappings", "type": "json", "default": {} }, + { "serverKey": "frontend_logs_enabled", "localKey": "ModelResolver.frontendLogsEnabled", "type": "frontendLogsEnabled", "default": True }, + { "serverKey": "backend_logs_enabled", "localKey": "ModelResolver.backendLogsEnabled", "type": "backendLogsEnabled", "default": True }, + { "serverKey": "frontend_log_level", "localKey": "ModelResolver.frontendLogLevel", "type": "string", "default": "ERROR" }, + { "serverKey": "backend_log_level", "localKey": "ModelResolver.backendLogLevel", "type": "string", "default": "ERROR" }, + { "serverKey": "civitai_candidate_limit", "localKey": "ModelResolver.civitaiCandidateLimit", "type": "candidateLimit", "default": 5 }, +] + DEFAULT_DOWNLOAD_PATH_TEMPLATES: Dict[str, str] = { "loras": "{base_model}/{first_tag}", "checkpoints": "{base_model}", @@ -358,3 +387,8 @@ def get_default_root_for_category( if not key: return "" return str(active_settings.get(key) or "").strip() + + +def get_settings_schema() -> List[Dict[str, Any]]: + """Return the schema dictionary for frontend setting options synchronization.""" + return SETTINGS_SCHEMA diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index 2ea3ed1..35d05a2 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -281,6 +281,16 @@ export const downloadTargetMethods = { getExactNodeTypeDownloadCategory(nodeType = '') { const normalizedNodeType = String(nodeType || '').trim(); + const dynamicRules = this.capabilities?.node_rules; + if (dynamicRules && typeof dynamicRules === 'object' && dynamicRules[normalizedNodeType]) { + const widgetsMap = dynamicRules[normalizedNodeType]; + if (widgetsMap && typeof widgetsMap === 'object') { + const indices = Object.keys(widgetsMap).map(k => parseInt(k, 10)).sort((a, b) => a - b); + if (indices.length > 0) { + return widgetsMap[indices[0]] || ''; + } + } + } const exactMap = { CheckpointLoaderSimple: 'checkpoints', CheckpointLoader: 'checkpoints', diff --git a/web/resolver/shell/workflow_state_methods.js b/web/resolver/shell/workflow_state_methods.js index 3c3bd1a..9441cc6 100644 --- a/web/resolver/shell/workflow_state_methods.js +++ b/web/resolver/shell/workflow_state_methods.js @@ -1,4 +1,4 @@ -import { app } from "../../../../../scripts/app.js"; +import { app } from "../../../../../scripts/app.js"; import { api } from "../../../../../scripts/api.js"; import { $el } from "../../../../../scripts/ui.js"; import { createModuleLogger } from "../../log_system/log_funcs.js"; @@ -67,7 +67,7 @@ export const workflowStateMethods = { 'on' ]); const loraStrengthNodeTypes = new Set(['LoraLoader', 'LoraLoaderModelOnly']); - const modelWidgetIndicesByNodeType = { + const staticFallback = { CheckpointLoaderSimple: [0], CheckpointLoader: [1], DiffusersLoader: [0], @@ -131,6 +131,16 @@ export const workflowStateMethods = { LTXAVTextEncoderLoader: [0, 1] }; + const modelWidgetIndicesByNodeType = { ...staticFallback }; + const dynamicRules = this.capabilities?.node_rules; + if (dynamicRules && typeof dynamicRules === 'object') { + for (const [nodeType, widgetsMap] of Object.entries(dynamicRules)) { + if (widgetsMap && typeof widgetsMap === 'object') { + modelWidgetIndicesByNodeType[nodeType] = Object.keys(widgetsMap).map(k => parseInt(k, 10)); + } + } + } + const normalizeKey = (key = '') => String(key).replace(/[-\s]/g, '_').toLowerCase(); const isModelWidgetIndex = (nodeType = '', index = -1) => ( modelWidgetIndicesByNodeType[nodeType] || [] diff --git a/web/resolver/views/options_methods.js b/web/resolver/views/options_methods.js index 08f7a08..83d76d3 100644 --- a/web/resolver/views/options_methods.js +++ b/web/resolver/views/options_methods.js @@ -3572,8 +3572,15 @@ export const optionsMethods = { return; } + let settingsObj = data; + if (data.schema && Array.isArray(data.schema)) { + SETTINGS_MAP.length = 0; + SETTINGS_MAP.push(...data.schema); + settingsObj = data.settings || {}; + } + for (const opt of SETTINGS_MAP) { - const serverValue = data[opt.serverKey]; + const serverValue = settingsObj[opt.serverKey]; if (serverValue === undefined) continue; if (opt.type === 'boolean' || opt.type === 'frontendLogsEnabled' || opt.type === 'backendLogsEnabled') { @@ -3591,14 +3598,14 @@ export const optionsMethods = { } } this.getDefaultRootCategoryDefinitions().forEach((item) => { - if (data[item.settingKey] !== undefined) { - safeStorage.setItem(item.storageKey, String(data[item.settingKey] || '')); + if (settingsObj[item.settingKey] !== undefined) { + safeStorage.setItem(item.storageKey, String(settingsObj[item.settingKey] || '')); } }); // Source-enabled flags stored as a nested object - if (data.search_source_enabled && typeof data.search_source_enabled === 'object') { - Object.entries(data.search_source_enabled).forEach(([key, val]) => { + if (settingsObj.search_source_enabled && typeof settingsObj.search_source_enabled === 'object') { + Object.entries(settingsObj.search_source_enabled).forEach(([key, val]) => { if (key) safeStorage.setItem(key, val ? 'true' : 'false'); }); }