From 9399f52033706e1612ab5d408bd2c09e11a450b0 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 18 Jun 2026 02:46:33 +0200 Subject: [PATCH] Preserve local matches and download target selections across workflow changes --- .../actions/resolve_download_methods.js | 129 ++++++++++++++++++ web/resolver/resolver_dialog.js | 1 + .../search/download_target_methods.js | 27 ++++ web/resolver/shell/workflow_state_methods.js | 40 ++++++ web/resolver/views/missing_browser_methods.js | 12 +- 5 files changed, 208 insertions(+), 1 deletion(-) diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 3f16ba1..8a9a050 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -536,6 +536,129 @@ export const resolveDownloadMethods = { return next; }, + cloneLocalMatches(matches = []) { + if (!Array.isArray(matches)) return []; + try { + return JSON.parse(JSON.stringify(matches)); + } catch (error) { + console.warn('Model Resolver: failed to clone local matches', error); + return matches.map(match => ({ ...match })); + } + }, + + getLocalMatchIdentity(match = {}) { + const model = match.model || {}; + return [ + model.path || match.path || '', + model.relative_path || '', + model.filename || match.filename || '', + model.category || match.category || '' + ].map(value => String(value || '').trim().toLowerCase()).join('::'); + }, + + mergeLocalMatches(existingMatches = [], restoredMatches = []) { + const merged = []; + const byIdentity = new Map(); + + const addMatch = (match) => { + if (!match || typeof match !== 'object') return; + const identity = this.getLocalMatchIdentity(match); + if (!identity.replace(/:/g, '')) return; + + const previous = byIdentity.get(identity); + if (!previous || Number(match.confidence || 0) > Number(previous.confidence || 0)) { + byIdentity.set(identity, match); + } + }; + + existingMatches.forEach(addMatch); + restoredMatches.forEach(addMatch); + + for (const match of byIdentity.values()) { + merged.push(match); + } + + return merged.sort((a, b) => Number(b.confidence || 0) - Number(a.confidence || 0)); + }, + + rememberDownloadedLocalMatchesForMissing(missing, matches = [], metadata = {}) { + if (!missing || !Array.isArray(matches)) return null; + + const snapshot = this.rememberDownloadSnapshotForMissing(missing, { + localMatches: this.cloneLocalMatches(matches), + localMatchesMetadata: { + ...metadata, + updatedAt: Date.now() + } + }); + + this.persistLocalMatchesInAnalysisCache(missing, matches); + this.persistDownloadedLocalMatchSearchState(missing, matches, metadata); + return snapshot; + }, + + restoreDownloadedLocalMatchesForMissing(missing) { + if (!missing) return missing; + + const snapshot = this.getDownloadProgressStore().get(this.getDownloadStateKey(missing)); + const storedMatches = Array.isArray(snapshot?.localMatches) ? snapshot.localMatches : []; + if (!storedMatches.length) return missing; + + const currentMatches = Array.isArray(missing.matches) ? missing.matches : []; + missing.matches = this.mergeLocalMatches(currentMatches, this.cloneLocalMatches(storedMatches)); + return missing; + }, + + persistLocalMatchesInAnalysisCache(missing, matches = []) { + if (!missing || !Array.isArray(matches)) return; + + const missingKey = this.getMissingModelKey(missing); + const patchAnalysisData = (data) => { + const list = data?.missing_models; + if (!Array.isArray(list)) return; + + for (const item of list) { + if (this.getMissingModelKey(item) !== missingKey) continue; + item.matches = this.mergeLocalMatches(item.matches || [], matches); + } + }; + + patchAnalysisData(this.cachedAnalysisData); + const workflowKey = this.getWorkflowScopedQueueKey?.(); + const cached = workflowKey ? this.workflowAnalysisCaches?.get(workflowKey) : null; + if (cached?.data) { + patchAnalysisData(cached.data); + } + }, + + persistDownloadedLocalMatchSearchState(missing, matches = [], metadata = {}) { + if (!missing || !Array.isArray(matches)) return; + + const workflowKey = this.getWorkflowScopedQueueKey?.(); + if (!workflowKey) return; + + const state = this.getSearchStateForWorkflow?.(workflowKey, missing); + if (!state) return; + + const hasRenderableMatch = matches.some(match => Number(match.confidence || 0) >= 70); + const hasExactMatch = matches.some(match => Number(match.confidence || 0) >= 100); + state.lastAttemptSources = Array.from(new Set([...(state.lastAttemptSources || []), 'local'])); + state.lastAttemptFound = hasRenderableMatch || state.lastAttemptFound || false; + state.lastAttemptError = state.lastAttemptError || null; + state.sourceProgress = { + ...(state.sourceProgress || {}), + local: { + status: hasRenderableMatch ? 'found' : 'none', + percent: 100, + message: hasExactMatch ? 'Exact local match' : (hasRenderableMatch ? 'Local match' : 'No local match'), + updatedAt: Date.now(), + filename: metadata.targetFilename || metadata.filename || '' + } + }; + + this.persistSearchStateForWorkflow?.(workflowKey, missing, state); + }, + rememberDownloadUiState(downloadId, info, progress = {}, options = {}) { if (!info?.missing) return null; @@ -779,6 +902,12 @@ export const resolveDownloadMethods = { const currentMissing = (this.missingModels || []).find(item => this.getMissingModelKey(item) === missingKey) || missing; currentMissing.matches = matches; missing.matches = matches; + this.rememberDownloadedLocalMatchesForMissing(currentMissing, matches, { + targetFilename, + category: category || missing.category || '', + downloadPath, + downloadDirectory + }); this.allModels = null; this.invalidateLoadedModelsCacheForActiveWorkflow?.(); this.refreshLocalMatchesUiForMissing(currentMissing); diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index 8837d73..94b7151 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -31,6 +31,7 @@ export class ResolverManagerDialog extends ComfyDialog { this.workflowAnalysisCaches = new Map(); // workflow key -> analyzed missing models this.workflowLoadedModelCaches = new Map(); // workflow key -> loaded model inspector data this.workflowSearchResultCaches = new Map(); // workflow key -> search results by missing model + this.workflowDownloadTargetSelectionCaches = new Map(); // workflow key -> suggested/user download folders this.activeDownloads = {}; // Track active downloads this.downloadProgressByMissingKey = new Map(); // missing model key -> last known download UI state this.queuePanelActiveTab = 'queued'; diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index b7c3f64..41631f7 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -365,6 +365,23 @@ export const downloadTargetMethods = { }); }, + getFirstSearchResult(result) { + return Array.isArray(result) ? (result[0] || null) : (result || null); + }, + + getCachedSearchSuggestionData(missing = {}) { + const state = this.searchResultCache?.get(this.getMissingSearchKey?.(missing)); + const results = state?.results || {}; + const merged = {}; + for (const source of ['popular', 'model_list', 'huggingface', 'civitai', 'civarchive', 'lora_manager_archive']) { + const result = this.getFirstSearchResult(results[source]); + if (result && typeof result === 'object') { + Object.assign(merged, result); + } + } + return merged; + }, + normalizeFolderToken(value = '') { return String(value || '') .toLowerCase() @@ -392,8 +409,10 @@ export const downloadTargetMethods = { return ''; } + const searchSuggestion = this.getCachedSearchSuggestionData(missing); const civitaiData = { ...(missing?.civitai_info || {}), + ...(searchSuggestion || {}), ...(missing?.civitai_search_result || {}), ...(missing?.download_source || {}) }; @@ -440,6 +459,7 @@ export const downloadTargetMethods = { const source = missing.download_source || {}; const civitaiInfo = missing.civitai_info || {}; const civitaiSearch = missing.civitai_search_result || {}; + const searchSuggestion = this.getCachedSearchSuggestionData(missing); const rawValues = [ source.filename, source.name, @@ -450,6 +470,12 @@ export const downloadTargetMethods = { civitaiSearch.filename, civitaiSearch.name, civitaiSearch.model_name, + searchSuggestion.filename, + searchSuggestion.name, + searchSuggestion.model_name, + searchSuggestion.path, + searchSuggestion.repo_id, + searchSuggestion.repo, missing.original_path, missing.name ].filter(Boolean); @@ -1010,6 +1036,7 @@ export const downloadTargetMethods = { this.workflowAnalysisCaches.clear(); this.workflowLoadedModelCaches.clear(); + this.workflowDownloadTargetSelectionCaches?.clear(); this.cachedAnalysisData = null; this.cachedWorkflowSignature = null; this.cachedLoadedModelsData = null; diff --git a/web/resolver/shell/workflow_state_methods.js b/web/resolver/shell/workflow_state_methods.js index a480690..f3e8748 100644 --- a/web/resolver/shell/workflow_state_methods.js +++ b/web/resolver/shell/workflow_state_methods.js @@ -381,6 +381,44 @@ export const workflowStateMethods = { } }, + cloneDownloadTargetSelections(selections = this.downloadTargetSelections) { + const cloned = new Map(); + for (const [missingKey, selection] of (selections || new Map()).entries()) { + try { + cloned.set(missingKey, JSON.parse(JSON.stringify(selection || {}))); + } catch (error) { + console.warn('Model Resolver: failed to clone download target selection', error); + cloned.set(missingKey, { ...(selection || {}) }); + } + } + return cloned; + }, + + saveDownloadTargetSelectionsForActiveWorkflow() { + const key = this.getWorkflowScopedQueueKey(); + if (!key) return; + + const selections = this.downloadTargetSelections instanceof Map + ? this.downloadTargetSelections + : new Map(); + if (selections.size) { + this.workflowDownloadTargetSelectionCaches.set( + key, + this.cloneDownloadTargetSelections(selections) + ); + } else { + this.workflowDownloadTargetSelectionCaches.delete(key); + } + }, + + restoreDownloadTargetSelectionsForActiveWorkflow() { + const key = this.getWorkflowScopedQueueKey(); + const saved = key ? this.workflowDownloadTargetSelectionCaches.get(key) : null; + this.downloadTargetSelections = saved + ? this.cloneDownloadTargetSelections(saved) + : new Map(); + }, + cloneSearchState(state = {}, { preserveActive = false } = {}) { let clone = {}; try { @@ -496,6 +534,7 @@ export const workflowStateMethods = { this.saveAnalysisCacheForActiveWorkflow(); this.saveLoadedModelsCacheForActiveWorkflow(); this.saveSearchCacheForActiveWorkflow(); + this.saveDownloadTargetSelectionsForActiveWorkflow(); this.clearWorkflowScopedState(); this.activeWorkflowRouteKey = nextRoute; this.activeWorkflowSignature = nextSignature; @@ -503,6 +542,7 @@ export const workflowStateMethods = { this.restoreAnalysisCacheForActiveWorkflow(); this.restoreLoadedModelsCacheForActiveWorkflow(); this.restoreSearchCacheForActiveWorkflow(); + this.restoreDownloadTargetSelectionsForActiveWorkflow(); }, clearWorkflowScopedState() { diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index b262b5d..ee3677a 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -71,6 +71,14 @@ export const missingBrowserMethods = { if (source === 'local') { candidates.push(results.model_list, results.popular); + const bestLocalMatch = this.getBestLocalMatch(missing, 70); + if (bestLocalMatch) { + candidates.push({ + source: 'local_match', + confidence: Number(bestLocalMatch.confidence || 0), + match_type: Number(bestLocalMatch.confidence || 0) >= 100 ? 'exact' : 'similar' + }); + } } else if (source === 'huggingface') { candidates.push(results.huggingface); } else if (source === 'civitai') { @@ -653,7 +661,9 @@ export const missingBrowserMethods = { * Display missing models in the dialog */ displayMissingModels(container, data) { - const missingModels = data.missing_models || []; + const missingModels = (data.missing_models || []).map(missing => ( + this.restoreDownloadedLocalMatchesForMissing?.(missing) || missing + )); const totalMissing = data.total_missing || 0; this.missingModels = missingModels; this.syncBatchSelectionForMissingModels(missingModels);