feat: support gated/private Hugging Face repos via access token

Add a huggingface_api_key setting (Settings UI, HF_TOKEN /
HUGGING_FACE_HUB_TOKEN env override) and attach it as a Bearer token
to Hugging Face file listing, model card fetching and downloads, so
gated and private repositories can be downloaded once the user has
accepted the repo terms.

- fetch_json/fetch_text accept custom headers; ModelSource gains an
  auth_headers() hook so handlers stay platform-agnostic
- 401/403 from the tree API now explain how to fix (configure token /
  accept gated terms)
- aria2 pre-resolves huggingface.co redirects and strips credentials
  before handing the signed CDN URL to aria2, mirroring the CivitAI
  handling so the token never leaks to the CDN
- settings API exposes huggingface_api_key_set only; the raw key joins
  _NO_SYNC_KEYS
This commit is contained in:
Will Miao
2026-09-25 18:44:06 +08:00
parent 067e605e75
commit 8b7ba59263
23 changed files with 446 additions and 23 deletions
+45 -10
View File
@@ -928,6 +928,7 @@ export class SettingsManager {
// Update API key status display (do NOT pre-fill the input)
this.updateApiKeyStatus();
this.updateHfApiKeyStatus();
this.updateLlmApiKeyStatus();
// ── AI Provider settings ──────────────────────────────────────
@@ -4350,6 +4351,28 @@ export class SettingsManager {
}
}
updateHfApiKeyStatus() {
const hasKey = !!(state.global.settings.huggingface_api_key_set ||
state.global.settings.huggingface_api_key);
const statusText = document.getElementById('huggingfaceApiKeyStatusText');
const actionBtn = document.getElementById('huggingfaceApiKeyActionBtn');
if (!statusText || !actionBtn) return;
if (hasKey) {
statusText.classList.remove('api-key-status--unconfigured');
statusText.classList.add('api-key-status--configured');
statusText.innerHTML = '<i class="fas fa-check-circle text-success"></i> '
+ translate('settings.huggingfaceApiKeyConfigured', {}, 'Configured');
actionBtn.textContent = translate('common.actions.change', {}, 'Change');
} else {
statusText.classList.remove('api-key-status--configured');
statusText.classList.add('api-key-status--unconfigured');
statusText.innerHTML = '<i class="fas fa-times-circle text-error"></i> '
+ translate('settings.huggingfaceApiKeyNotConfigured', {}, 'Not configured');
actionBtn.textContent = translate('settings.huggingfaceApiKeySet', {}, 'Set up');
}
}
updateLlmApiKeyStatus() {
const hasKey = !!(state.global.settings.llm_api_key_set || state.global.settings.llm_api_key);
const statusText = document.getElementById('llmApiKeyStatusText');
@@ -4397,9 +4420,17 @@ export class SettingsManager {
const input = document.getElementById(inputId);
if (input) input.value = '';
if (!silent) {
if (inputId === 'civitaiApiKey') {
this.updateApiKeyStatus();
}
this.refreshApiKeyStatus(inputId);
}
}
refreshApiKeyStatus(inputId) {
if (inputId === 'civitaiApiKey') {
this.updateApiKeyStatus();
} else if (inputId === 'huggingfaceApiKey') {
this.updateHfApiKeyStatus();
} else if (inputId === 'llmApiKey') {
this.updateLlmApiKeyStatus();
}
}
@@ -4409,11 +4440,16 @@ export class SettingsManager {
const value = input.value.trim();
const labelNames = {
civitai_api_key: 'CivitAI API Key',
huggingface_api_key: 'Hugging Face Access Token',
llm_api_key: 'LLM API Key',
};
try {
await this.saveSetting(settingsKey, value);
const labelName = settingsKey === 'civitai_api_key' ? 'CivitAI API Key' : 'LLM API Key';
showToast('toast.settings.settingsUpdated',
{ setting: labelName }, 'success');
{ setting: labelNames[settingsKey] || 'API Key' }, 'success');
} catch (error) {
showToast('toast.settings.settingSaveFailed',
{ message: error.message }, 'error');
@@ -4421,13 +4457,12 @@ export class SettingsManager {
}
// Update the in-memory flag so the UI reflects the change
if (settingsKey === 'civitai_api_key') {
state.global.settings.civitai_api_key_set = !!value;
const setFlagKey = `${settingsKey}_set`;
if (setFlagKey in state.global.settings) {
state.global.settings[setFlagKey] = !!value;
}
this.cancelEditApiKey(true, inputId);
if (inputId === 'civitaiApiKey') {
this.updateApiKeyStatus();
}
this.refreshApiKeyStatus(inputId);
}
toggleInputVisibility(button) {