feat: add server-side settings persistence for API keys and preferences
This commit is contained in:
@@ -29,3 +29,5 @@ Thumbs.db
|
||||
*.bak
|
||||
*.cache
|
||||
|
||||
# User settings – contains API keys and tokens, must NOT be committed
|
||||
model_resolver_settings.json
|
||||
|
||||
+62
-1
@@ -1,4 +1,4 @@
|
||||
"""
|
||||
"""
|
||||
@author: Model Resolver Team
|
||||
@title: ComfyUI Model Resolver
|
||||
@nickname: Model Resolver
|
||||
@@ -1603,6 +1603,67 @@ class ModelResolverExtension:
|
||||
)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
# ==================== SETTINGS (server-side persistence) ====================
|
||||
|
||||
import os as _os
|
||||
import json as _json
|
||||
|
||||
_SETTINGS_FILE = _os.path.join(
|
||||
_os.path.dirname(_os.path.abspath(__file__)),
|
||||
"model_resolver_settings.json",
|
||||
)
|
||||
|
||||
# Keys that contain sensitive credentials – stored in the file but
|
||||
# never logged.
|
||||
_CREDENTIAL_KEYS = {
|
||||
"civitai_key",
|
||||
"civitai_session_token",
|
||||
"hf_token",
|
||||
"brave_search_api_key",
|
||||
}
|
||||
|
||||
def _load_settings() -> dict:
|
||||
try:
|
||||
if _os.path.isfile(_SETTINGS_FILE):
|
||||
with open(_SETTINGS_FILE, "r", encoding="utf-8") as f:
|
||||
data = _json.load(f)
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
except Exception as exc:
|
||||
self.logger.warning(f"Model Resolver: could not read settings file: {exc}")
|
||||
return {}
|
||||
|
||||
def _save_settings(payload: dict) -> None:
|
||||
# Merge with existing settings so we never lose keys not sent
|
||||
# by the current request.
|
||||
current = _load_settings()
|
||||
current.update({k: v for k, v in payload.items() if k})
|
||||
with open(_SETTINGS_FILE, "w", encoding="utf-8") as f:
|
||||
_json.dump(current, f, indent=2, ensure_ascii=False)
|
||||
|
||||
@routes.get("/model_resolver/settings")
|
||||
async def get_settings_route(request):
|
||||
"""Return persisted settings (API keys, preferences)."""
|
||||
try:
|
||||
data = await asyncio.to_thread(_load_settings)
|
||||
return web.json_response(data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Model Resolver settings GET error: {e}", exc_info=True)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.post("/model_resolver/settings")
|
||||
async def save_settings_route(request):
|
||||
"""Persist settings (API keys, preferences) to disk."""
|
||||
try:
|
||||
payload = await request.json()
|
||||
if not isinstance(payload, dict):
|
||||
return web.json_response({"error": "Expected JSON object"}, status=400)
|
||||
await asyncio.to_thread(_save_settings, payload)
|
||||
return web.json_response({"success": True})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Model Resolver settings POST error: {e}", exc_info=True)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
self.routes_setup = True
|
||||
self.logger.info("Model Resolver: API routes registered successfully")
|
||||
return True
|
||||
|
||||
@@ -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 { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
@@ -484,6 +484,55 @@ export const downloadTargetMethods = {
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch settings saved on the server and sync them into localStorage.
|
||||
* Call this once when the dialog initialises so every browser gets the
|
||||
* same tokens without the user having to re-enter them.
|
||||
*/
|
||||
async loadSettingsFromServer() {
|
||||
try {
|
||||
const resp = await api.fetchApi('/model_resolver/settings');
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
if (!data || typeof data !== 'object') return;
|
||||
|
||||
// Helper: write to localStorage only when the value from server is
|
||||
// non-empty, so we don't overwrite a key the user already has locally.
|
||||
const sync = (localKey, serverValue) => {
|
||||
if (serverValue !== undefined && serverValue !== null && serverValue !== '') {
|
||||
localStorage.setItem(localKey, serverValue);
|
||||
}
|
||||
};
|
||||
|
||||
sync('ModelResolver.civitaiApiKey', data.civitai_key);
|
||||
sync('ModelResolver.civitaiSessionToken', data.civitai_session_token);
|
||||
sync('ModelResolver.huggingFaceToken', data.hf_token);
|
||||
sync('ModelResolver.braveSearchApiKey', data.brave_search_api_key);
|
||||
|
||||
if (data.civitai_use_trpc_search !== undefined)
|
||||
localStorage.setItem('ModelResolver.civitaiUseTrpcSearch', data.civitai_use_trpc_search ? 'true' : 'false');
|
||||
if (data.civitai_use_html_fallback !== undefined)
|
||||
localStorage.setItem('ModelResolver.civitaiUseHtmlFallback', data.civitai_use_html_fallback ? 'true' : 'false');
|
||||
if (data.hf_use_api_search !== undefined)
|
||||
localStorage.setItem('ModelResolver.hfUseApiSearch', data.hf_use_api_search ? 'true' : 'false');
|
||||
if (data.hf_use_comfy_org_fallback !== undefined)
|
||||
localStorage.setItem('ModelResolver.hfUseComfyOrgFallback', data.hf_use_comfy_org_fallback ? 'true' : 'false');
|
||||
if (data.hf_use_brave_fallback !== undefined)
|
||||
localStorage.setItem('ModelResolver.hfUseBraveFallback', data.hf_use_brave_fallback ? 'true' : 'false');
|
||||
if (data.civitai_candidate_limit !== undefined)
|
||||
localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${data.civitai_candidate_limit}`);
|
||||
|
||||
// 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 (key) localStorage.setItem(key, val ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Model Resolver: could not load settings from server, using localStorage only.', err);
|
||||
}
|
||||
},
|
||||
|
||||
async clearSearchCaches() {
|
||||
for (const state of this.searchResultCache.values()) {
|
||||
state.activeSearchRunId = null;
|
||||
|
||||
@@ -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 { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
@@ -23,6 +23,13 @@ export const lifecycleGraphMethods = {
|
||||
// Update button state in case there are active downloads
|
||||
this.updateDownloadAllButtonState();
|
||||
|
||||
// On first open: load settings from server so tokens are available on
|
||||
// every browser without re-entering them.
|
||||
if (!this._serverSettingsLoaded) {
|
||||
this._serverSettingsLoaded = true;
|
||||
await this.loadSettingsFromServer();
|
||||
}
|
||||
|
||||
// Ensure all models are loaded for dropdown
|
||||
await this.ensureCapabilitiesLoaded();
|
||||
await this.ensureAllModelsLoaded();
|
||||
|
||||
@@ -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 { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
@@ -717,34 +717,68 @@ export const optionsMethods = {
|
||||
const civitaiCandidateLimit = Number.isFinite(civitaiCandidateLimitRaw)
|
||||
? Math.min(20, Math.max(1, civitaiCandidateLimitRaw))
|
||||
: 5;
|
||||
localStorage.setItem('ModelResolver.civitaiApiKey', civitaiInput?.value || '');
|
||||
localStorage.setItem('ModelResolver.civitaiSessionToken', civitaiSessionInput?.value || '');
|
||||
localStorage.setItem('ModelResolver.civitaiUseTrpcSearch', civitaiUseTrpcSearchInput?.checked ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.civitaiUseHtmlFallback', civitaiUseHtmlFallbackInput?.checked ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.huggingFaceToken', hfInput?.value || '');
|
||||
localStorage.setItem('ModelResolver.braveSearchApiKey', braveInput?.value || '');
|
||||
localStorage.setItem('ModelResolver.hfUseApiSearch', hfUseApiSearchInput?.checked ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.hfUseComfyOrgFallback', hfUseComfyOrgFallbackInput?.checked ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.hfUseBraveFallback', hfUseBraveFallbackInput?.checked ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${civitaiCandidateLimit}`);
|
||||
|
||||
// Collect source-enabled flags
|
||||
const sourceEnabled = {};
|
||||
if (sourceEnabledInputs.length && !sourceEnabledInputs.some(input => input.checked)) {
|
||||
const localInput = sourceEnabledInputs.find(input => input.dataset.source === 'local');
|
||||
if (localInput) {
|
||||
localInput.checked = true;
|
||||
}
|
||||
if (localInput) localInput.checked = true;
|
||||
}
|
||||
sourceEnabledInputs.forEach((input) => {
|
||||
const key = input.dataset.storageKey;
|
||||
if (key) {
|
||||
localStorage.setItem(key, input.checked ? 'true' : 'false');
|
||||
sourceEnabled[key] = input.checked;
|
||||
}
|
||||
});
|
||||
if (civitaiLimitInput) {
|
||||
civitaiLimitInput.value = `${civitaiCandidateLimit}`;
|
||||
|
||||
const newSettings = {
|
||||
civitai_key: civitaiInput?.value || '',
|
||||
civitai_session_token: civitaiSessionInput?.value || '',
|
||||
civitai_use_trpc_search: Boolean(civitaiUseTrpcSearchInput?.checked),
|
||||
civitai_use_html_fallback: Boolean(civitaiUseHtmlFallbackInput?.checked),
|
||||
civitai_candidate_limit: civitaiCandidateLimit,
|
||||
hf_token: hfInput?.value || '',
|
||||
hf_use_api_search: Boolean(hfUseApiSearchInput?.checked),
|
||||
hf_use_comfy_org_fallback: Boolean(hfUseComfyOrgFallbackInput?.checked),
|
||||
hf_use_brave_fallback: Boolean(hfUseBraveFallbackInput?.checked),
|
||||
brave_search_api_key: braveInput?.value || '',
|
||||
search_source_enabled: sourceEnabled,
|
||||
};
|
||||
|
||||
// 1. Write to localStorage (fast local cache)
|
||||
localStorage.setItem('ModelResolver.civitaiApiKey', newSettings.civitai_key);
|
||||
localStorage.setItem('ModelResolver.civitaiSessionToken', newSettings.civitai_session_token);
|
||||
localStorage.setItem('ModelResolver.civitaiUseTrpcSearch', newSettings.civitai_use_trpc_search ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.civitaiUseHtmlFallback', newSettings.civitai_use_html_fallback ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.huggingFaceToken', newSettings.hf_token);
|
||||
localStorage.setItem('ModelResolver.braveSearchApiKey', newSettings.brave_search_api_key);
|
||||
localStorage.setItem('ModelResolver.hfUseApiSearch', newSettings.hf_use_api_search ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.hfUseComfyOrgFallback', newSettings.hf_use_comfy_org_fallback ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.hfUseBraveFallback', newSettings.hf_use_brave_fallback ? 'true' : 'false');
|
||||
localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${civitaiCandidateLimit}`);
|
||||
Object.entries(sourceEnabled).forEach(([key, val]) => {
|
||||
localStorage.setItem(key, val ? 'true' : 'false');
|
||||
});
|
||||
if (civitaiLimitInput) civitaiLimitInput.value = `${civitaiCandidateLimit}`;
|
||||
|
||||
// 2. Persist to server (survives new browsers / profiles)
|
||||
try {
|
||||
setStatus('Saving…', '');
|
||||
const resp = await api.fetchApi('/model_resolver/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newSettings),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
setStatus('Saved on this machine (all browsers).', 'is-saved');
|
||||
this.showNotification('Options saved', 'success');
|
||||
} catch (err) {
|
||||
console.warn('Model Resolver: could not save settings to server, kept locally only.', err);
|
||||
setStatus('Saved locally only (server write failed).', 'is-saved');
|
||||
this.showNotification('Options saved locally only', 'warning');
|
||||
}
|
||||
|
||||
await this.clearSearchCaches();
|
||||
setStatus('Options saved locally.', 'is-saved');
|
||||
this.showNotification('Options saved and search cache cleared', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user