Preserve local matches and download target selections across workflow changes

This commit is contained in:
Dariusz L
2026-06-18 02:46:33 +02:00
parent c9dd0e47ac
commit 9399f52033
5 changed files with 208 additions and 1 deletions
@@ -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);
+1
View File
@@ -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';
@@ -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;
@@ -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() {
+11 -1
View File
@@ -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);