Add CivitAI session token & clear search cache
Add support for an optional CivitAI session token and cache-management for search results. UI: new input for __Secure-civitai-token saved to localStorage (modelLinker.civitaiSessionToken) and cleared via the options dialog; saving/clearing tokens triggers a backend cache clear and updates notifications. Backend: add clear_search_cache() helpers in core.sources.huggingface and core.sources.civitai, register /model_linker/clear-search-cache route to flush both caches, and wire the civitai session token into search flows. CivitAI changes include sending the session token as a Cookie header for tRPC searches, incorporating session and model_type into the search cache key, and passing session_token through search_civitai_for_file and related calls. Purpose: ensure authenticated/session-aware search results (including NSFW/visibility differences) and avoid stale cached search results after token/settings changes.
This commit is contained in:
+23
-3
@@ -97,12 +97,16 @@ class ModelLinkerExtension:
|
||||
search_model_list,
|
||||
search_model_list_multiple,
|
||||
)
|
||||
from .core.sources.huggingface import search_huggingface_for_file
|
||||
from .core.sources.huggingface import (
|
||||
search_huggingface_for_file,
|
||||
clear_search_cache as clear_huggingface_search_cache,
|
||||
)
|
||||
from .core.sources.civitai import (
|
||||
search_civitai_for_file,
|
||||
search_civitai,
|
||||
get_civitai_download_url,
|
||||
resolve_urn,
|
||||
clear_search_cache as clear_civitai_search_cache,
|
||||
)
|
||||
|
||||
download_available = True
|
||||
@@ -804,6 +808,7 @@ class ModelLinkerExtension:
|
||||
category = data.get("category", "")
|
||||
# Handle both boolean and string forms
|
||||
is_urn_raw = data.get("is_urn", False)
|
||||
civitai_session_token = data.get("civitai_session_token", "")
|
||||
is_urn = (
|
||||
is_urn_raw
|
||||
if isinstance(is_urn_raw, bool)
|
||||
@@ -972,7 +977,8 @@ class ModelLinkerExtension:
|
||||
f"URN: No model_id/version_id, falling back to CivitAI search"
|
||||
)
|
||||
civitai_results = search_civitai(
|
||||
filename, model_type=category
|
||||
filename,
|
||||
model_type=category,
|
||||
)
|
||||
log_search_result(
|
||||
"civitai/fallback",
|
||||
@@ -999,7 +1005,9 @@ class ModelLinkerExtension:
|
||||
results["found"] = True
|
||||
else:
|
||||
civitai_result = search_civitai_for_file(
|
||||
filename, model_type=category
|
||||
filename,
|
||||
model_type=category,
|
||||
session_token=civitai_session_token or None,
|
||||
)
|
||||
log_search_result("civitai", civitai_result)
|
||||
if civitai_result:
|
||||
@@ -1015,6 +1023,18 @@ class ModelLinkerExtension:
|
||||
log_exception(f"Model Linker search error: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.post("/model_linker/clear-search-cache")
|
||||
async def clear_search_cache_route(request):
|
||||
"""Clear backend search caches after token/settings changes."""
|
||||
try:
|
||||
clear_huggingface_search_cache()
|
||||
clear_civitai_search_cache()
|
||||
log_info("Cleared backend search caches")
|
||||
return web.json_response({"success": True})
|
||||
except Exception as e:
|
||||
log_exception(f"Clear search cache error: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.post("/model_linker/download")
|
||||
async def download_model(request):
|
||||
"""Start downloading a model."""
|
||||
|
||||
+17
-3
@@ -42,6 +42,12 @@ CIVITAI_TYPE_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def clear_search_cache():
|
||||
"""Clear cached CivitAI search results."""
|
||||
global _search_cache
|
||||
_search_cache.clear()
|
||||
|
||||
|
||||
def _build_civitai_result_from_version(
|
||||
model_id: int,
|
||||
model_name: str,
|
||||
@@ -210,6 +216,7 @@ def _extract_trpc_model_candidates(
|
||||
def _search_civitai_trpc_candidates(
|
||||
filename: str,
|
||||
model_type: Optional[str] = None,
|
||||
session_token: Optional[str] = None,
|
||||
timeout: int = 15,
|
||||
limit: int = 5,
|
||||
) -> List[Dict[str, Optional[int]]]:
|
||||
@@ -266,9 +273,11 @@ def _search_civitai_trpc_candidates(
|
||||
"x-client": "web",
|
||||
"x-client-version": "5.0.1657",
|
||||
}
|
||||
if session_token:
|
||||
headers["Cookie"] = f"__Secure-civitai-token={session_token}"
|
||||
|
||||
log_info(
|
||||
f"CivitAI tRPC search start: filename={filename}, model_type={model_type}, url={url}"
|
||||
f"CivitAI tRPC search start: filename={filename}, model_type={model_type}, session_token={'yes' if session_token else 'no'}, url={url}"
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -539,6 +548,7 @@ def search_civitai_for_file(
|
||||
api_key: Optional[str] = None,
|
||||
exact_only: bool = False,
|
||||
model_type: Optional[str] = None,
|
||||
session_token: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Search CivitAI for a specific model file.
|
||||
@@ -555,7 +565,11 @@ def search_civitai_for_file(
|
||||
"""
|
||||
global _search_cache
|
||||
|
||||
cache_key = f"civit_{filename}_exact{exact_only}"
|
||||
session_key = "session" if session_token else "anon"
|
||||
model_type_key = str(model_type or "").lower()
|
||||
cache_key = (
|
||||
f"civit_{filename}_exact{exact_only}_type{model_type_key}_session{session_key}"
|
||||
)
|
||||
if cache_key in _search_cache:
|
||||
log_debug(f"CivitAI search cache hit for {filename} (exact_only={exact_only})")
|
||||
return _search_cache[cache_key]
|
||||
@@ -564,7 +578,7 @@ def search_civitai_for_file(
|
||||
# Prefer the CivitAI.red tRPC search because it is much closer to what
|
||||
# the browser search UI returns than the broad public /models API.
|
||||
trpc_candidates = _search_civitai_trpc_candidates(
|
||||
filename, model_type=model_type
|
||||
filename, model_type=model_type, session_token=session_token
|
||||
)
|
||||
for candidate in trpc_candidates:
|
||||
model_id = candidate["model_id"]
|
||||
|
||||
@@ -24,6 +24,12 @@ HF_API_URL = "https://huggingface.co/api"
|
||||
_search_cache: Dict[str, Any] = {}
|
||||
|
||||
|
||||
def clear_search_cache():
|
||||
"""Clear cached HuggingFace search results."""
|
||||
global _search_cache
|
||||
_search_cache.clear()
|
||||
|
||||
|
||||
def parse_huggingface_url(url: str) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Parse a HuggingFace URL to extract repo and filename.
|
||||
|
||||
+39
-6
@@ -3567,10 +3567,25 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
getStoredTokens() {
|
||||
return {
|
||||
civitai_key: localStorage.getItem('modelLinker.civitaiApiKey') || '',
|
||||
civitai_session_token: localStorage.getItem('modelLinker.civitaiSessionToken') || '',
|
||||
hf_token: localStorage.getItem('modelLinker.huggingFaceToken') || ''
|
||||
};
|
||||
}
|
||||
|
||||
async clearSearchCaches() {
|
||||
this.searchResultCache.clear();
|
||||
try {
|
||||
const response = await api.fetchApi('/model_linker/clear-search-cache', {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to clear backend search cache');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Model Linker: Clear search cache error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
displayOptions() {
|
||||
if (!this.contentElement) return;
|
||||
|
||||
@@ -3579,13 +3594,18 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<div class="ml-options-wrap">
|
||||
<div class="ml-options-card">
|
||||
<h3 class="ml-options-title">API Tokens</h3>
|
||||
<p class="ml-options-subtitle">Stored locally in your browser. Used only for downloads that require authentication.</p>
|
||||
<p class="ml-options-subtitle">Stored locally in your browser. API keys are used for downloads. Session token can improve CivitAI web search results, including NSFW items visible to your logged-in account.</p>
|
||||
<div class="ml-options-grid">
|
||||
<div class="ml-options-field">
|
||||
<label for="ml-options-civitai" class="ml-options-label">CivitAI API Key</label>
|
||||
<input id="ml-options-civitai" class="ml-options-input" type="password" placeholder="Paste CivitAI API key" value="${tokens.civitai_key}">
|
||||
<div class="ml-options-help">Used for direct CivitAI downloads that otherwise return HTTP 401 or 403.</div>
|
||||
</div>
|
||||
<div class="ml-options-field">
|
||||
<label for="ml-options-civitai-session" class="ml-options-label">CivitAI Session Token</label>
|
||||
<input id="ml-options-civitai-session" class="ml-options-input" type="password" placeholder="Paste __Secure-civitai-token" value="${tokens.civitai_session_token}">
|
||||
<div class="ml-options-help">Used only for CivitAI web search (civitai.red) to include results available to your logged-in session, including NSFW. Keep it private.</div>
|
||||
</div>
|
||||
<div class="ml-options-field">
|
||||
<label for="ml-options-hf" class="ml-options-label">HuggingFace Token</label>
|
||||
<input id="ml-options-hf" class="ml-options-input" type="password" placeholder="Paste HuggingFace token" value="${tokens.hf_token}">
|
||||
@@ -3602,28 +3622,34 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
`;
|
||||
|
||||
const civitaiInput = this.contentElement.querySelector('#ml-options-civitai');
|
||||
const civitaiSessionInput = this.contentElement.querySelector('#ml-options-civitai-session');
|
||||
const hfInput = this.contentElement.querySelector('#ml-options-hf');
|
||||
const status = this.contentElement.querySelector('#ml-options-status');
|
||||
const saveBtn = this.contentElement.querySelector('#ml-options-save');
|
||||
const clearBtn = this.contentElement.querySelector('#ml-options-clear');
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', () => {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
localStorage.setItem('modelLinker.civitaiApiKey', civitaiInput?.value || '');
|
||||
localStorage.setItem('modelLinker.civitaiSessionToken', civitaiSessionInput?.value || '');
|
||||
localStorage.setItem('modelLinker.huggingFaceToken', hfInput?.value || '');
|
||||
await this.clearSearchCaches();
|
||||
if (status) status.textContent = 'Tokens saved locally.';
|
||||
this.showNotification('API tokens saved', 'success');
|
||||
this.showNotification('API tokens saved and search cache cleared', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
clearBtn.addEventListener('click', async () => {
|
||||
localStorage.removeItem('modelLinker.civitaiApiKey');
|
||||
localStorage.removeItem('modelLinker.civitaiSessionToken');
|
||||
localStorage.removeItem('modelLinker.huggingFaceToken');
|
||||
if (civitaiInput) civitaiInput.value = '';
|
||||
if (civitaiSessionInput) civitaiSessionInput.value = '';
|
||||
if (hfInput) hfInput.value = '';
|
||||
await this.clearSearchCaches();
|
||||
if (status) status.textContent = 'Tokens cleared.';
|
||||
this.showNotification('API tokens cleared', 'info');
|
||||
this.showNotification('API tokens cleared and search cache reset', 'info');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5878,7 +5904,14 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
// For URNs, include model_id and version_id for direct download
|
||||
const searchData = { filename, category, is_urn: isUrn, sources: [selectedSource] };
|
||||
const tokens = this.getStoredTokens();
|
||||
const searchData = {
|
||||
filename,
|
||||
category,
|
||||
is_urn: isUrn,
|
||||
sources: [selectedSource],
|
||||
civitai_session_token: tokens.civitai_session_token
|
||||
};
|
||||
if (isUrn && missing.urn) {
|
||||
searchData.model_id = missing.urn.model_id;
|
||||
searchData.version_id = missing.urn.version_id;
|
||||
|
||||
Reference in New Issue
Block a user