Preserve local matches and download target selections across workflow changes
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user