feat: add local model hash comparison functionality
Implement end-to-end hash comparison for local models and local matches. Backend: - Add /model_resolver/local-model-hashes API endpoint - Add get_local_model_hash_metadata() to read sidecar SHA256 metadata Frontend: - Add Compare Hashes context menu item and CSS - Pass missing_key/missing_search_key through resolveDownloadMethods - Enhance local match rendering with hash status and context data - Add hash normalization, formatting, and candidate collection helpers - Wire compareLocalModelHashesWithCurrentFinding action Also update HuggingFace author index metadata.
This commit is contained in:
+25
@@ -93,6 +93,7 @@ class ModelResolverExtension:
|
||||
from .core.resolver import (
|
||||
analyze_and_find_matches,
|
||||
apply_resolution,
|
||||
get_local_model_hash_metadata,
|
||||
normalize_sha256,
|
||||
search_local_matches_by_hash,
|
||||
search_local_matches,
|
||||
@@ -522,6 +523,30 @@ class ModelResolverExtension:
|
||||
)
|
||||
return web.json_response({"matches": matches})
|
||||
|
||||
@routes.post("/model_resolver/local-model-hashes")
|
||||
@json_api_endpoint("local-model-hashes")
|
||||
async def local_model_hashes(request):
|
||||
"""Return SHA256 hashes already stored in local sidecar metadata."""
|
||||
data = await request.json()
|
||||
model = data.get("model") if isinstance(data.get("model"), dict) else {}
|
||||
path = (
|
||||
data.get("path")
|
||||
or data.get("file_path")
|
||||
or data.get("resolved_path")
|
||||
or model.get("path")
|
||||
or model.get("resolved_path")
|
||||
or ""
|
||||
)
|
||||
|
||||
if not path:
|
||||
return web.json_response(
|
||||
{"error": "path is required"}, status=400
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
get_local_model_hash_metadata(path, model=model)
|
||||
)
|
||||
|
||||
@routes.post("/model_resolver/open-containing-folder")
|
||||
@json_api_endpoint("open-containing-folder")
|
||||
async def open_containing_folder(request):
|
||||
|
||||
@@ -479,6 +479,68 @@ def search_local_matches_by_hash(
|
||||
return matches
|
||||
|
||||
|
||||
def get_local_model_hash_metadata(
|
||||
model_path: str,
|
||||
model: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return SHA256 hashes already stored in sidecar metadata for a local model.
|
||||
|
||||
This intentionally does not hash model files. It only reads metadata sidecars
|
||||
next to the selected local model, so callers can do quick hash comparisons.
|
||||
"""
|
||||
raw_path = str(model_path or "").strip()
|
||||
if not raw_path:
|
||||
return {"exists": False, "metadata_path": "", "hashes": [], "sha256": ""}
|
||||
|
||||
normalized_path = os.path.abspath(os.path.normpath(raw_path))
|
||||
exists = os.path.exists(normalized_path)
|
||||
model_info: Dict[str, Any] = {
|
||||
**(model if isinstance(model, dict) else {}),
|
||||
"path": normalized_path,
|
||||
}
|
||||
model_info.setdefault("filename", os.path.basename(normalized_path))
|
||||
model_info.setdefault("relative_path", model_info.get("filename", ""))
|
||||
|
||||
first_metadata_path = ""
|
||||
last_hash_status = ""
|
||||
for metadata_path in _metadata_sidecar_candidates(normalized_path):
|
||||
if not os.path.exists(metadata_path):
|
||||
continue
|
||||
|
||||
if not first_metadata_path:
|
||||
first_metadata_path = metadata_path
|
||||
|
||||
try:
|
||||
with open(metadata_path, "r", encoding="utf-8") as handle:
|
||||
metadata = json.load(handle)
|
||||
except Exception as exc:
|
||||
log.debug(f"Could not read metadata sidecar for local hash lookup: {metadata_path} ({exc})")
|
||||
continue
|
||||
|
||||
if not isinstance(metadata, dict):
|
||||
continue
|
||||
|
||||
last_hash_status = str(metadata.get("hash_status") or "").strip()
|
||||
hashes = _extract_model_sha256_from_metadata(metadata, model_info)
|
||||
if hashes:
|
||||
return {
|
||||
"exists": exists,
|
||||
"metadata_path": metadata_path,
|
||||
"hash_status": last_hash_status,
|
||||
"hashes": hashes,
|
||||
"sha256": hashes[0],
|
||||
}
|
||||
|
||||
return {
|
||||
"exists": exists,
|
||||
"metadata_path": first_metadata_path,
|
||||
"hash_status": last_hash_status,
|
||||
"hashes": [],
|
||||
"sha256": "",
|
||||
}
|
||||
|
||||
|
||||
def extract_workflow_urls(workflow_json: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Extract model URLs from workflow JSON.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2637,6 +2637,19 @@
|
||||
color: #72d38a;
|
||||
}
|
||||
|
||||
.mr-search-match-hash {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 20px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid rgba(125, 211, 252, 0.22);
|
||||
border-radius: var(--mr-missing-control-radius, var(--mr-ui-radius-sm));
|
||||
line-height: 1;
|
||||
color: #7dd3fc;
|
||||
background: rgba(125, 211, 252, 0.12);
|
||||
}
|
||||
|
||||
.mr-search-match-medium {
|
||||
color: #f1cf64;
|
||||
}
|
||||
|
||||
@@ -457,6 +457,10 @@ export const resolveDownloadMethods = {
|
||||
const targetPath = directory || filePath;
|
||||
if (!targetPath) return null;
|
||||
|
||||
const missing = info?.missing || progress?.missing || null;
|
||||
const missingKey = missing ? this.getMissingModelKey?.(missing) : (info?.missing_key || progress?.missing_key || '');
|
||||
const missingSearchKey = missing ? this.getMissingSearchKey?.(missing) : (info?.missing_search_key || progress?.missing_search_key || '');
|
||||
|
||||
return {
|
||||
context_scope: 'download_folder',
|
||||
name: progress?.filename || info?.filename || 'Download',
|
||||
@@ -466,7 +470,9 @@ export const resolveDownloadMethods = {
|
||||
folder_path: directory || targetPath,
|
||||
download_directory: directory || '',
|
||||
download_path: filePath || '',
|
||||
category: info?.category || progress?.category || ''
|
||||
category: info?.category || progress?.category || '',
|
||||
missing_key: missingKey || '',
|
||||
missing_search_key: missingSearchKey || ''
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -165,6 +165,15 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
$el("div.mr-context-menu-divider.mr-context-menu-divider-folder", {
|
||||
"data-menu-divider": "folder"
|
||||
}),
|
||||
$el("div.mr-context-menu-item.mr-context-menu-action-compare-hashes", {
|
||||
"data-menu-action": "compareHashes",
|
||||
onclick: () => this.handleContextMenuAction('compareHashes')
|
||||
}, [
|
||||
$el("span.mr-context-menu-item-icon", {
|
||||
innerHTML: getSvgIcon('hash', 'currentColor', 'mr-context-menu-item-svg')
|
||||
}),
|
||||
$el("span", { textContent: "Compare Hashes" })
|
||||
]),
|
||||
$el("div.mr-context-menu-item.mr-context-menu-action-open-folder", {
|
||||
"data-menu-action": "openFolder",
|
||||
onclick: () => this.handleContextMenuAction('openFolder')
|
||||
|
||||
@@ -1653,6 +1653,10 @@ export const searchPanelMethods = {
|
||||
|
||||
getSearchResultMatchDisplay(result = {}, fallbackLabel = 'Match', fallbackClass = 'neutral') {
|
||||
const matchType = String(result.match_type || '').toLowerCase();
|
||||
if (result.hash_verified || result.hash_verified_sha256 || matchType === 'hash') {
|
||||
return { label: 'Hash', className: 'hash' };
|
||||
}
|
||||
|
||||
if (matchType === 'exact') {
|
||||
return { label: 'Exact', className: 'strong' };
|
||||
}
|
||||
@@ -1998,16 +2002,59 @@ export const searchPanelMethods = {
|
||||
`;
|
||||
},
|
||||
|
||||
buildContextMenuModelData(model = {}, fallbackName = '') {
|
||||
const resolvedPath = model.path || model.resolved_path || '';
|
||||
const filename = model.filename || fallbackName || resolvedPath.split(/[\/\\]/).pop() || '';
|
||||
return {
|
||||
buildContextMenuModelData(model = {}, fallbackName = '', extra = {}) {
|
||||
const merged = {
|
||||
...model,
|
||||
name: model.name || filename,
|
||||
original_path: model.original_path || filename,
|
||||
resolved_path: resolvedPath,
|
||||
category: model.category || ''
|
||||
...(extra && typeof extra === 'object' ? extra : {})
|
||||
};
|
||||
const resolvedPath = merged.path || merged.resolved_path || '';
|
||||
const filename = merged.filename || fallbackName || resolvedPath.split(/[\/\\]/).pop() || '';
|
||||
return {
|
||||
...merged,
|
||||
name: merged.name || filename,
|
||||
original_path: merged.original_path || filename,
|
||||
resolved_path: resolvedPath,
|
||||
category: merged.category || ''
|
||||
};
|
||||
},
|
||||
|
||||
getLocalMatchHash(match = {}) {
|
||||
const model = match.model || {};
|
||||
const hashes = model.hashes && typeof model.hashes === 'object' ? model.hashes : {};
|
||||
return String(
|
||||
match.sha256
|
||||
|| match.hash
|
||||
|| model.sha256
|
||||
|| model.hash
|
||||
|| hashes.SHA256
|
||||
|| hashes.sha256
|
||||
|| ''
|
||||
).trim();
|
||||
},
|
||||
|
||||
getLocalMatchContextData(missing = {}, match = {}) {
|
||||
const hash = this.getLocalMatchHash(match);
|
||||
const metadataPath = match.metadata_path || match.model?.metadata_path || '';
|
||||
return {
|
||||
context_scope: 'local_match',
|
||||
missing_key: this.getMissingModelKey?.(missing) || '',
|
||||
missing_search_key: this.getMissingSearchKey?.(missing) || '',
|
||||
local_match_confidence: match.confidence || 0,
|
||||
local_match_sha256: hash,
|
||||
sha256: hash || match.model?.sha256 || '',
|
||||
hash: hash || match.model?.hash || '',
|
||||
hash_match: Boolean(match.hash_match || match.match_type === 'hash'),
|
||||
metadata_path: metadataPath
|
||||
};
|
||||
},
|
||||
|
||||
renderLocalMatchStatus(match = {}) {
|
||||
const confidence = Number(match.confidence || 0);
|
||||
const label = confidence === 100 ? 'Exact' : 'Partial';
|
||||
const className = confidence === 100
|
||||
? 'mr-match-status-exact'
|
||||
: 'mr-match-status-partial';
|
||||
return `<span class="mr-match-status ${className}">${label}</span>`;
|
||||
},
|
||||
|
||||
areLocalMatchAlternativesCollapsed() {
|
||||
@@ -2059,12 +2106,16 @@ export const searchPanelMethods = {
|
||||
const buttonId = `resolve-${missingIndex}-${missing.node_id}-${missing.widget_index}-${matchIndex}`;
|
||||
const matchPath = match.model?.relative_path || match.model?.path || match.path || match.filename || '';
|
||||
const isBestMatch = matchIndex === 0 && match.confidence >= 95;
|
||||
const contextModel = this.buildContextMenuModelData(match.model || {}, match.filename || '');
|
||||
const contextModel = this.buildContextMenuModelData(
|
||||
match.model || {},
|
||||
match.filename || '',
|
||||
this.getLocalMatchContextData(missing, match)
|
||||
);
|
||||
|
||||
html += `<div class="mr-match-row ${isBestMatch ? 'mr-best-match' : ''}"${this.getContextMenuAttrs(contextModel)}>`;
|
||||
html += this.getConfidenceBadge(match.confidence);
|
||||
html += `<span class="mr-match-filename" data-tooltip="${this.escapeHtml(matchPath)}">${this.escapeHtml(matchPath)}</span>`;
|
||||
html += `<span class="mr-match-status ${match.confidence === 100 ? 'mr-match-status-exact' : 'mr-match-status-partial'}">${match.confidence === 100 ? 'Exact' : 'Partial'}</span>`;
|
||||
html += this.renderLocalMatchStatus(match);
|
||||
html += `<button id="${buttonId}" class="mr-btn mr-btn-secondary mr-btn-sm mr-btn-icon-only mr-local-link-btn" data-tooltip="Link this local match" aria-label="Link this local match">`;
|
||||
html += getSvgIcon('link');
|
||||
html += `</button>`;
|
||||
@@ -2084,12 +2135,16 @@ export const searchPanelMethods = {
|
||||
for (let mIdx = 0; mIdx < otherMatches.length; mIdx++) {
|
||||
const match = otherMatches[mIdx];
|
||||
const altBtnId = `resolve-alt-${missingIndex}-${missing.node_id}-${missing.widget_index}-${mIdx}`;
|
||||
const contextModel = this.buildContextMenuModelData(match.model || {}, match.filename || '');
|
||||
const contextModel = this.buildContextMenuModelData(
|
||||
match.model || {},
|
||||
match.filename || '',
|
||||
this.getLocalMatchContextData(missing, match)
|
||||
);
|
||||
const matchPath = match.model?.relative_path || match.model?.path || match.path || match.filename || '';
|
||||
html += `<div class="mr-match-row"${this.getContextMenuAttrs(contextModel)}>`;
|
||||
html += this.getConfidenceBadge(match.confidence);
|
||||
html += `<span class="mr-match-filename" data-tooltip="${this.escapeHtml(matchPath)}">${this.escapeHtml(matchPath)}</span>`;
|
||||
html += `<span class="mr-match-status mr-match-status-partial">Partial</span>`;
|
||||
html += this.renderLocalMatchStatus(match);
|
||||
html += `<button id="${altBtnId}" class="mr-btn mr-btn-secondary mr-btn-sm mr-btn-icon-only mr-local-link-btn" data-tooltip="Link this local match" aria-label="Link this local match">${getSvgIcon('link')}</button>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
@@ -29,19 +29,24 @@ export const modelInfoMethods = {
|
||||
const isDownloadRootContext = model?.context_scope === 'download_root';
|
||||
const isDownloadQueueContext = model?.context_scope === 'download_queue';
|
||||
const isDownloadHistoryContext = model?.context_scope === 'download_history';
|
||||
const isLocalModelContext = model?.context_scope === 'local_model' || model?.context_scope === 'local_match';
|
||||
const isFolderOnlyContext = isDownloadFolderContext || isDownloadRootContext;
|
||||
const isSourceModelContext = !isDownloadTableContext && !isFolderOnlyContext && !isDownloadQueueContext && !isDownloadHistoryContext;
|
||||
const hasLocalPath = Boolean(model?.open_path || model?.folder_path || model?.download_directory || model?.directory || model?.path || model?.resolved_path);
|
||||
const showOpenFolder = !isDownloadTableContext && hasLocalPath;
|
||||
const showCompareHashes = (isLocalModelContext || isDownloadFolderContext)
|
||||
&& hasLocalPath
|
||||
&& Boolean(model?.missing_key || model?.missing_search_key);
|
||||
const showSwitchWorkflow = (isDownloadQueueContext || isDownloadHistoryContext) && Boolean(this.canSwitchToDownloadWorkflow?.(model));
|
||||
this.setContextMenuItemVisible('showInfo', isSourceModelContext);
|
||||
this.setContextMenuItemVisible('showMore', canShowMore);
|
||||
this.setContextMenuItemVisible('civitai', isSourceModelContext);
|
||||
this.setContextMenuItemVisible('switchWorkflow', showSwitchWorkflow);
|
||||
this.setContextMenuItemVisible('compareHashes', showCompareHashes);
|
||||
this.setContextMenuItemVisible('openFolder', showOpenFolder);
|
||||
this.setContextMenuDividerVisible('source', isSourceModelContext || canShowMore);
|
||||
this.setContextMenuDividerVisible('workflow', showSwitchWorkflow && (isSourceModelContext || canShowMore));
|
||||
this.setContextMenuDividerVisible('folder', showOpenFolder && (isSourceModelContext || canShowMore || showSwitchWorkflow));
|
||||
this.setContextMenuDividerVisible('folder', (showCompareHashes || showOpenFolder) && (isSourceModelContext || canShowMore || showSwitchWorkflow));
|
||||
|
||||
const openFolderLabel = this.contextMenu.querySelector('.mr-context-menu-action-open-folder span:last-child');
|
||||
if (openFolderLabel) {
|
||||
@@ -86,6 +91,8 @@ export const modelInfoMethods = {
|
||||
this.openInCivitAI(model);
|
||||
} else if (action === 'openFolder') {
|
||||
this.openContainingFolder(model);
|
||||
} else if (action === 'compareHashes') {
|
||||
this.compareLocalModelHashesWithCurrentFinding(model);
|
||||
} else if (action === 'switchWorkflow') {
|
||||
this.switchToDownloadWorkflow(model);
|
||||
} else if (action === 'showInfo') {
|
||||
@@ -134,6 +141,227 @@ export const modelInfoMethods = {
|
||||
return (this.missingModels || []).find(missing => this.getMissingModelKey?.(missing) === key) || null;
|
||||
},
|
||||
|
||||
getMissingForHashCompareContext(model = {}) {
|
||||
const missingKey = String(model?.missing_key || '').trim();
|
||||
if (missingKey) {
|
||||
const byMissingKey = this.getMissingByKey(missingKey);
|
||||
if (byMissingKey) return byMissingKey;
|
||||
}
|
||||
|
||||
const searchKey = String(model?.missing_search_key || '').trim();
|
||||
if (searchKey && typeof this.getMissingSearchKey === 'function') {
|
||||
return (this.missingModels || []).find(missing => this.getMissingSearchKey(missing) === searchKey) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
normalizeSha256ForCompare(value = '') {
|
||||
let text = String(value || '').trim();
|
||||
text = text.replace(/^sha256[:=]/i, '').trim().toLowerCase();
|
||||
return /^[a-f0-9]{64}$/.test(text) ? text : '';
|
||||
},
|
||||
|
||||
formatSha256Short(value = '') {
|
||||
const hash = this.normalizeSha256ForCompare(value) || String(value || '').trim();
|
||||
if (hash.length <= 18) return hash;
|
||||
return `${hash.slice(0, 8)}...${hash.slice(-8)}`;
|
||||
},
|
||||
|
||||
collectHashCandidatesForCompare(value, source = 'metadata', seen = new Set(), depth = 0) {
|
||||
const candidates = [];
|
||||
if (value === undefined || value === null || depth > 5) return candidates;
|
||||
|
||||
const addHash = (rawValue, hashSource = source) => {
|
||||
const hash = this.normalizeSha256ForCompare(rawValue);
|
||||
if (!hash || seen.has(hash)) return;
|
||||
seen.add(hash);
|
||||
candidates.push({ hash, source: hashSource || source || 'metadata' });
|
||||
};
|
||||
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
addHash(value);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => {
|
||||
candidates.push(...this.collectHashCandidatesForCompare(item, source, seen, depth + 1));
|
||||
});
|
||||
return candidates;
|
||||
}
|
||||
|
||||
if (typeof value !== 'object') return candidates;
|
||||
|
||||
[
|
||||
['local_match_sha256', 'local match'],
|
||||
['sha256', source],
|
||||
['hash', source],
|
||||
['SHA256', source],
|
||||
['file_hash', source],
|
||||
['fileHash', source]
|
||||
].forEach(([key, label]) => addHash(value[key], label));
|
||||
|
||||
const hashes = value.hashes;
|
||||
if (Array.isArray(hashes)) {
|
||||
candidates.push(...this.collectHashCandidatesForCompare(hashes, source, seen, depth + 1));
|
||||
} else if (hashes && typeof hashes === 'object') {
|
||||
['SHA256', 'sha256', 'hash'].forEach(key => addHash(hashes[key], source));
|
||||
}
|
||||
|
||||
[
|
||||
['file_info', value.file_info],
|
||||
['file', value.file],
|
||||
['selected_file', value.selected_file],
|
||||
['path_metadata', value.path_metadata],
|
||||
['download_metadata', value.download_metadata],
|
||||
['metadata', value.metadata],
|
||||
['selected_version', value.selected_version || value.selectedVersion],
|
||||
['version', value.version],
|
||||
['civitai', value.civitai]
|
||||
].forEach(([key, nestedValue]) => {
|
||||
if (nestedValue !== undefined && nestedValue !== null) {
|
||||
candidates.push(...this.collectHashCandidatesForCompare(nestedValue, `${source} ${key}`, seen, depth + 1));
|
||||
}
|
||||
});
|
||||
|
||||
['files', 'mirrors', 'download_files', 'downloadFiles', 'modelVersions', 'versions'].forEach(key => {
|
||||
if (Array.isArray(value[key])) {
|
||||
candidates.push(...this.collectHashCandidatesForCompare(value[key], `${source} ${key}`, seen, depth + 1));
|
||||
}
|
||||
});
|
||||
|
||||
return candidates;
|
||||
},
|
||||
|
||||
getLocalHashCandidatesForCompare(model = {}) {
|
||||
const seen = new Set();
|
||||
return this.collectHashCandidatesForCompare(model, 'local model metadata', seen);
|
||||
},
|
||||
|
||||
getLocalHashComparePath(model = {}) {
|
||||
return model?.open_path
|
||||
|| model?.resolved_path
|
||||
|| model?.path
|
||||
|| model?.file_path
|
||||
|| '';
|
||||
},
|
||||
|
||||
async fetchLocalHashCandidatesForCompare(model = {}) {
|
||||
const path = this.getLocalHashComparePath(model);
|
||||
if (!path) {
|
||||
return { candidates: [], data: null };
|
||||
}
|
||||
|
||||
const data = await this.fetchJson('/model_resolver/local-model-hashes', {
|
||||
method: 'POST',
|
||||
silent: true,
|
||||
body: JSON.stringify({ path, model })
|
||||
}, 'Read local model hash metadata');
|
||||
|
||||
return {
|
||||
data,
|
||||
candidates: this.collectHashCandidatesForCompare(data, 'local metadata', new Set())
|
||||
};
|
||||
},
|
||||
|
||||
markSearchResultHashBadgesForMissing(missing = {}, sha256 = '') {
|
||||
const hash = this.normalizeSha256ForCompare(sha256);
|
||||
if (!missing || !hash) return false;
|
||||
|
||||
const markIfMatches = (result, sourceLabel = 'result') => {
|
||||
if (!result || typeof result !== 'object') return false;
|
||||
const hashes = this.collectHashCandidatesForCompare(result, sourceLabel, new Set());
|
||||
if (!hashes.some(candidate => candidate.hash === hash)) return false;
|
||||
|
||||
result.hash_verified = true;
|
||||
result.hash_verified_sha256 = hash;
|
||||
return true;
|
||||
};
|
||||
|
||||
const state = this.getSearchState?.(missing)
|
||||
|| this.searchResultCache?.get(this.getMissingSearchKey?.(missing));
|
||||
const results = state?.results || {};
|
||||
const sources = [
|
||||
['download_source', missing.download_source],
|
||||
['popular', results.popular],
|
||||
['model_list', results.model_list],
|
||||
['huggingface', results.huggingface],
|
||||
['civitai', results.civitai],
|
||||
['civarchive', results.civarchive],
|
||||
['lora_manager_archive', results.lora_manager_archive]
|
||||
];
|
||||
let changed = false;
|
||||
|
||||
sources.forEach(([sourceKey, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => {
|
||||
changed = markIfMatches(item, sourceKey) || changed;
|
||||
});
|
||||
return;
|
||||
}
|
||||
changed = markIfMatches(value, sourceKey) || changed;
|
||||
});
|
||||
|
||||
if (changed && state) {
|
||||
const workflowKey = this.getWorkflowScopedQueueKey?.();
|
||||
this.persistSearchStateForWorkflow?.(workflowKey, missing, state);
|
||||
this.refreshSearchUiForMissing?.(missing, state, { workflowKey });
|
||||
}
|
||||
return changed;
|
||||
},
|
||||
|
||||
async compareLocalModelHashesWithCurrentFinding(model = {}) {
|
||||
const missing = this.getMissingForHashCompareContext(model);
|
||||
if (!missing) {
|
||||
this.showNotification?.('Cannot find the search results for this model.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
let localHashes = this.getLocalHashCandidatesForCompare(model);
|
||||
let metadataResult = null;
|
||||
if (!localHashes.length) {
|
||||
try {
|
||||
const lookup = await this.fetchLocalHashCandidatesForCompare(model);
|
||||
metadataResult = lookup.data;
|
||||
localHashes = lookup.candidates;
|
||||
} catch (error) {
|
||||
this.showNotification?.(`Local hash metadata lookup failed: ${error?.message || error}`, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const filename = model.filename
|
||||
|| model.name
|
||||
|| String(this.getLocalHashComparePath(model) || '').split(/[\/\\]/).pop()
|
||||
|| 'Selected local model';
|
||||
if (!localHashes.length) {
|
||||
const metadataPath = metadataResult?.metadata_path || model.metadata_path || '';
|
||||
this.showNotification?.(
|
||||
metadataPath
|
||||
? 'Local metadata exists, but no completed SHA256 hash was found.'
|
||||
: 'No local SHA256 metadata found for this model.',
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const marked = localHashes.some(candidate => (
|
||||
this.markSearchResultHashBadgesForMissing(missing, candidate.hash)
|
||||
));
|
||||
if (marked) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localPreview = localHashes
|
||||
.map(candidate => this.formatSha256Short(candidate.hash))
|
||||
.join(', ');
|
||||
this.showNotification?.(
|
||||
`No search result hash matches ${filename} (${localPreview}).`,
|
||||
'warning'
|
||||
);
|
||||
},
|
||||
|
||||
async openContainingFolder(model) {
|
||||
const path = model?.open_path || model?.folder_path || model?.download_directory || model?.directory || model?.path || model?.resolved_path || '';
|
||||
if (!path) {
|
||||
|
||||
Reference in New Issue
Block a user