feat: add force search (ignore cache) toggle to batch search menu

Adds a checkbox in the resolver footer search menu that bypasses all
search result caches for the duration of a batch search run.

Frontend (queue_methods.js, resolve_download_methods.js,
resolver_dialog.js, search_panel.js, resolver-main.css):
- Introduce checkbox-type footer menu items (type: 'checkbox').
- Add 'Force search (ignore cache)' checkbox to the search menu,
  wired to a new forceBatchSearch flag on ResolverManagerDialog.
- Pass forceSearch through searchMissingBatch and searchOnline to
  the backend API call.
- When the manual search button is clicked and results already exist
  for that model, forceSearch defaults to true.
- mergeSearchResults respects forceRefresh: if a forced search
  returns no results for a source, that source is cleared from the
  cached results.

Backend (__init__.py, core/sources/huggingface.py):
- force_search is read from the incoming API request and, when true,
  clears all active source search caches (local, huggingface,
  civitai, civarchive, lora_manager_archive) before searching.
- force_search is forwarded as force_refresh to
  search_huggingface_for_file, which folds it into the cache key so
  the cache is not hit, and propagates it to _get_author_index.
- Search result timestamps are no longer inherited from prior
  cached signatures when force_search is active; a fresh timestamp
  is written unconditionally.

Updated metadata/huggingface-author-index.json reflects the latest
cached author index after rebuild.
This commit is contained in:
Dariusz L
2026-06-06 17:02:25 +02:00
parent 57018b4e0f
commit 57b8bc7cda
8 changed files with 116 additions and 25 deletions
+35 -5
View File
@@ -949,9 +949,31 @@ class ModelResolverExtension:
search_lora_manager_archive_source = (
"lora_manager_archive" in normalized_sources
)
force_search = data.get("force_search", False)
force_search = (
force_search
if isinstance(force_search, bool)
else str(force_search).lower() == "true"
)
if force_search:
if search_local:
reload_popular_databases()
reload_model_list()
if search_huggingface_source:
clear_huggingface_search_cache()
if search_civitai_source:
clear_civitai_search_cache()
if search_civarchive_source:
clear_civarchive_search_cache()
if search_lora_manager_archive_source:
clear_lora_manager_archive_search_cache()
log_info(
f"Force search enabled: cleared cache for sources={sorted(normalized_sources)}"
)
log_info(
f"Search request: filename={filename}, category={category}, is_urn={is_urn}, model_id={data.get('model_id')}, version_id={data.get('version_id')}, sources={sorted(normalized_sources)}, civitai_candidate_limit={civitai_candidate_limit}, civarchive_candidate_limit={civarchive_candidate_limit}"
f"Search request: filename={filename}, category={category}, is_urn={is_urn}, model_id={data.get('model_id')}, version_id={data.get('version_id')}, sources={sorted(normalized_sources)}, force_search={force_search}, civitai_candidate_limit={civitai_candidate_limit}, civarchive_candidate_limit={civarchive_candidate_limit}"
)
results = {
@@ -1013,13 +1035,20 @@ class ModelResolverExtension:
timestamp = (
result.get("searched_at")
or result.get("searchedAt")
or self.search_result_timestamps.get(signature)
or (
None
if force_search
else self.search_result_timestamps.get(signature)
)
)
if not timestamp:
timestamp = current_search_timestamp()
self.search_result_timestamps.setdefault(
signature, timestamp
)
if force_search:
self.search_result_timestamps[signature] = timestamp
else:
self.search_result_timestamps.setdefault(
signature, timestamp
)
result["searched_at"] = timestamp
return result
@@ -1095,6 +1124,7 @@ class ModelResolverExtension:
use_api_search=hf_use_api_search,
use_comfy_org_fallback=hf_use_comfy_org_fallback,
use_brave_fallback=hf_use_brave_fallback,
force_refresh=force_search,
)
log_search_result("huggingface", hf_result)
return {"huggingface": hf_result}, bool(hf_result)
+8 -2
View File
@@ -668,6 +668,7 @@ def search_huggingface_for_file(
use_api_search: bool = True,
use_comfy_org_fallback: bool = True,
use_brave_fallback: bool = True,
force_refresh: bool = False,
) -> Optional[Dict[str, Any]]:
"""
Search HuggingFace for a specific model file.
@@ -689,7 +690,8 @@ def search_huggingface_for_file(
methods_key = (
f"api{int(bool(use_api_search))}_"
f"comfy{int(bool(use_comfy_org_fallback))}_"
f"brave{int(bool(use_brave_fallback))}"
f"brave{int(bool(use_brave_fallback))}_"
f"force{int(bool(force_refresh))}"
)
cache_key = f"hf_{filename}_exact{exact_only}_{token_key}_{brave_key}_{methods_key}"
if cache_key in _search_cache:
@@ -751,7 +753,11 @@ def search_huggingface_for_file(
author_fallback_repo_count = 0
if use_comfy_org_fallback:
for author in HF_AUTHOR_FALLBACKS:
index = _get_author_index(author, headers={})
index = _get_author_index(
author,
headers={},
force_refresh=force_refresh,
)
if index:
result = _find_matching_file_in_author_index(
index, filename, exact_only=exact_only
File diff suppressed because one or more lines are too long
+14
View File
@@ -2470,6 +2470,20 @@
text-align: left;
justify-content: flex-start;
}
.mr-footer-menu-checkbox,
#model-resolver-modal .mr-footer-menu .mr-footer-menu-checkbox {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.mr-footer-menu-checkbox-input {
width: 13px;
height: 13px;
margin: 0;
accent-color: #72d38a;
flex: 0 0 auto;
}
.mr-footer-menu-item:hover,
.mr-footer-menu-item:focus-visible {
outline: none;
+38 -8
View File
@@ -685,6 +685,25 @@ export const queueMethods = {
let element;
if (isDivider) {
element = $el("div.mr-footer-menu-divider", {});
} else if (item?.type === 'checkbox') {
const input = $el("input.mr-footer-menu-checkbox-input", {
type: 'checkbox',
checked: Boolean(item.checked?.())
});
element = $el("label.mr-footer-menu-item.mr-footer-menu-checkbox", {
role: 'menuitemcheckbox',
'aria-checked': input.checked ? 'true' : 'false',
onclick: (event) => {
event.stopPropagation();
}
}, [
input,
$el("span", { textContent: item.label })
]);
input.addEventListener('change', () => {
item.action?.(input.checked);
element.setAttribute('aria-checked', input.checked ? 'true' : 'false');
});
} else {
const attributes = {
type: 'button',
@@ -810,18 +829,29 @@ export const queueMethods = {
this.selectMenuWrap = selectMenu;
const hasSelectedMissingModels = () => this.getSelectedMissingModels().length > 0;
const searchBatchOptions = () => ({ forceSearch: Boolean(this.forceBatchSearch) });
const searchMenu = this.createFooterMenu('search', 'Search', [
{ label: 'Stop Searching', className: 'mr-footer-menu-danger', visibleWhen: () => this.batchSearchRunning, action: () => this.stopBatchSearch() },
{ type: 'divider', visibleWhen: () => this.batchSearchRunning },
{ label: 'Search Selected', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'all') },
{ label: 'Search All Missing', visibleWhen: () => !this.batchSearchRunning, previewModels: () => this.missingModels || [], action: () => this.searchMissingBatch('all', 'all') },
{ label: 'Search Unsearched', visibleWhen: () => !this.batchSearchRunning, previewModels: () => this.getUnsearchedMissingModels(), action: () => this.searchMissingBatch('unsearched', 'all') },
{
type: 'checkbox',
label: 'Force search (ignore cache)',
visibleWhen: () => !this.batchSearchRunning,
checked: () => Boolean(this.forceBatchSearch),
action: (checked) => {
this.forceBatchSearch = Boolean(checked);
}
},
{ type: 'divider', visibleWhen: () => !this.batchSearchRunning },
{ label: 'Selected: Local Database', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'local') },
{ label: 'Selected: CivitAI', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'civitai') },
{ label: 'Selected: HuggingFace', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'huggingface') },
{ label: 'Selected: CivArchive', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'civarchive') },
{ label: 'Selected: LoRA Manager Archive', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'lora_manager_archive') }
{ label: 'Search Selected', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'all', searchBatchOptions()) },
{ label: 'Search All Missing', visibleWhen: () => !this.batchSearchRunning, previewModels: () => this.missingModels || [], action: () => this.searchMissingBatch('all', 'all', searchBatchOptions()) },
{ label: 'Search Unsearched', visibleWhen: () => !this.batchSearchRunning, previewModels: () => this.getUnsearchedMissingModels(), action: () => this.searchMissingBatch('unsearched', 'all', searchBatchOptions()) },
{ type: 'divider', visibleWhen: () => !this.batchSearchRunning },
{ label: 'Selected: Local Database', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'local', searchBatchOptions()) },
{ label: 'Selected: CivitAI', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'civitai', searchBatchOptions()) },
{ label: 'Selected: HuggingFace', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'huggingface', searchBatchOptions()) },
{ label: 'Selected: CivArchive', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'civarchive', searchBatchOptions()) },
{ label: 'Selected: LoRA Manager Archive', visibleWhen: () => !this.batchSearchRunning, disabledWhen: () => !hasSelectedMissingModels(), previewModels: () => this.getSelectedMissingModels(), action: () => this.searchMissingBatch('selected', 'lora_manager_archive', searchBatchOptions()) }
]);
this.searchMenuButton = this.footerMenuButtons.get('search');
this.searchMenuWrap = searchMenu;
@@ -323,7 +323,7 @@ export const resolveDownloadMethods = {
return missingModels;
},
async searchMissingBatch(mode = 'selected', source = 'all') {
async searchMissingBatch(mode = 'selected', source = 'all', { forceSearch = false } = {}) {
if (this.batchSearchRunning) {
this.stopBatchSearch();
return;
@@ -353,7 +353,7 @@ export const resolveDownloadMethods = {
const state = this.getSearchStateForWorkflow(batchWorkflowKey, missing);
state.selectedSource = source || 'all';
try {
await this.searchOnline(missing, { workflowKey: batchWorkflowKey });
await this.searchOnline(missing, { workflowKey: batchWorkflowKey, forceSearch });
} catch (error) {
failed += 1;
console.error('Model Resolver: batch search item failed:', error);
@@ -778,7 +778,7 @@ export const resolveDownloadMethods = {
/**
* Search online for a model
*/
async searchOnline(missing, { workflowKey = this.getWorkflowScopedQueueKey() } = {}) {
async searchOnline(missing, { workflowKey = this.getWorkflowScopedQueueKey(), forceSearch = false } = {}) {
let filename = missing.original_path?.split('/').pop()?.split('\\').pop() || '';
let category = missing.category || '';
const state = this.getSearchStateForWorkflow(workflowKey, missing);
@@ -903,7 +903,8 @@ export const resolveDownloadMethods = {
brave_search_api_key: tokens.brave_search_api_key,
hf_use_api_search: tokens.hf_use_api_search,
hf_use_comfy_org_fallback: tokens.hf_use_comfy_org_fallback,
hf_use_brave_fallback: tokens.hf_use_brave_fallback
hf_use_brave_fallback: tokens.hf_use_brave_fallback,
force_search: Boolean(forceSearch)
};
if (isUrn && missing.urn) {
baseSearchData.model_id = missing.urn.model_id;
@@ -950,7 +951,8 @@ export const resolveDownloadMethods = {
const found = this.hasSearchResults(data);
anyFound = anyFound || found;
state.results = this.mergeSearchResults(state.results, data, {
searchedAt: new Date().toISOString()
searchedAt: new Date().toISOString(),
forceRefresh: Boolean(forceSearch)
});
state.lastAttemptSources = Array.from(attemptedSources);
state.lastAttemptFound = anyFound;
@@ -1193,7 +1195,9 @@ export const resolveDownloadMethods = {
if (searchBtn && searchBtn.dataset.mlSearchBound !== 'true') {
searchBtn.dataset.mlSearchBound = 'true';
searchBtn.addEventListener('click', () => {
this.searchOnline(missing);
this.searchOnline(missing, {
forceSearch: this.hasSearchResultsForMissing(missing)
});
});
}
+1
View File
@@ -44,6 +44,7 @@ export class ResolverManagerDialog extends ComfyDialog {
this.activeFooterMenu = null;
this.batchSearchRunning = false;
this.batchSearchCancelRequested = false;
this.forceBatchSearch = false;
this.boundHandleOutsideClick = this.handleOutsideClick.bind(this);
this.activeTabStorageKey = 'model_resolver_active_tab';
this.activeTab = this.restoreActiveTab(); // Default tab
+9 -3
View File
@@ -135,13 +135,14 @@ export const searchPanelMethods = {
/**
* Merge new search results into cached per-source results.
* Empty responses do not delete previously found results.
* Empty normal responses do not delete previous results; forced refreshes do.
*/
mergeSearchResults(existingResults = {}, newResults = {}, { searchedAt = null } = {}) {
mergeSearchResults(existingResults = {}, newResults = {}, { searchedAt = null, forceRefresh = false } = {}) {
const searchedSources = new Set(Array.isArray(newResults.searched_sources) ? newResults.searched_sources : []);
const pickResult = (source) => {
if (newResults[source]) {
const existingTimestamp = this.getSearchResultTimestamp(existingResults[source]);
const resultTimestamp = this.areSearchResultsSame(existingResults[source], newResults[source])
const resultTimestamp = !forceRefresh && this.areSearchResultsSame(existingResults[source], newResults[source])
? existingTimestamp
: null;
return this.withSearchResultTimestamp(
@@ -149,6 +150,11 @@ export const searchPanelMethods = {
resultTimestamp || searchedAt
);
}
const sourceWasSearched = searchedSources.has(source)
|| (searchedSources.has('local') && (source === 'popular' || source === 'model_list'));
if (forceRefresh && sourceWasSearched) {
return null;
}
return existingResults[source] || null;
};