From 0be3ea7d98bdce94eae759b3fe74da04f2e4091e Mon Sep 17 00:00:00 2001 From: Will Miao Date: Wed, 21 Oct 2026 21:53:59 +0800 Subject: [PATCH] fix(download): accept mirror/www Civitai hosts and clarify linked-file errors Follow-up to #1152: - Accept civitai.green and www-prefixed hosts in isSupportedCivitaiPageHost, aligning with py/utils/civitai_utils.py and extensionReimportBridge.js, so download URLs on those hosts resolve too - Drop the duplicate civitaiModelByVersion entry from the LoRA-specific endpoint map; the shared getApiEndpoints map now provides it for all types - Report a distinct error when a linked fileId exists in the version but is not a downloadable model weight file (new linkedFileNotWeightFile locale key) --- locales/de.json | 1 + locales/en.json | 1 + locales/es.json | 1 + locales/fr.json | 1 + locales/he.json | 1 + locales/ja.json | 1 + locales/ko.json | 1 + locales/ru.json | 1 + locales/zh-CN.json | 1 + locales/zh-TW.json | 1 + static/js/api/apiConfig.js | 1 - static/js/managers/DownloadManager.js | 11 ++++++---- static/js/utils/civitaiUtils.js | 4 +++- .../downloadManager.downloadUrl.test.js | 21 +++++++++++++++++++ tests/frontend/utils/civitaiUtils.test.js | 12 ++++++++--- 15 files changed, 50 insertions(+), 9 deletions(-) diff --git a/locales/de.json b/locales/de.json index 97f47329..c80f0366 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1723,6 +1723,7 @@ "invalidUrl": "Ungültiges Modell-URL-Format", "noVersions": "Keine Versionen für dieses Modell verfügbar", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "CivitAI- und Hugging Face-/ModelScope-URLs können nicht in derselben Charge gemischt werden.", "noModelFiles": "In diesem Repository wurden keine Modelldateien gefunden." }, diff --git a/locales/en.json b/locales/en.json index 969b10de..96a8b110 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1723,6 +1723,7 @@ "invalidUrl": "Invalid model URL format", "noVersions": "No versions available for this model", "linkedFileNotFound": "File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "File {fileId} is not a downloadable model file.", "mixedSources": "Cannot mix CivitAI and Hugging Face / ModelScope URLs in the same batch.", "noModelFiles": "No model files found in this repository." }, diff --git a/locales/es.json b/locales/es.json index 5fb4b933..1da6ae5c 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1723,6 +1723,7 @@ "invalidUrl": "Formato de URL de modelo inválido", "noVersions": "No hay versiones disponibles para este modelo", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "No se pueden mezclar URL de CivitAI y Hugging Face / ModelScope en el mismo lote.", "noModelFiles": "No se encontraron archivos de modelo en este repositorio." }, diff --git a/locales/fr.json b/locales/fr.json index 4229ae65..e214afe8 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1723,6 +1723,7 @@ "invalidUrl": "Format d'URL de modèle invalide", "noVersions": "Aucune version disponible pour ce modèle", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "Impossible de mélanger les URL CivitAI et Hugging Face / ModelScope dans le même lot.", "noModelFiles": "Aucun fichier de modèle trouvé dans ce dépôt." }, diff --git a/locales/he.json b/locales/he.json index 98be2fc9..54660902 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1723,6 +1723,7 @@ "invalidUrl": "פורמט URL של מודל לא חוקי", "noVersions": "אין גרסאות זמינות למודל זה", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "לא ניתן לערבב כתובות URL של CivitAI ו-Hugging Face / ModelScope באותה קבוצה.", "noModelFiles": "לא נמצאו קבצי מודל במאגר זה." }, diff --git a/locales/ja.json b/locales/ja.json index 2768d0a2..dff9ad79 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1723,6 +1723,7 @@ "invalidUrl": "無効なモデル URL 形式", "noVersions": "このモデルの利用可能なバージョンがありません", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "同じバッチ内でCivitAIとHugging Face / ModelScopeのURLを混在させることはできません。", "noModelFiles": "このリポジトリにモデルファイルが見つかりませんでした。" }, diff --git a/locales/ko.json b/locales/ko.json index 3da8222b..7d10c956 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1723,6 +1723,7 @@ "invalidUrl": "잘못된 모델 URL 형식", "noVersions": "이 모델에 사용 가능한 버전이 없습니다", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "동일한 배치에서 CivitAI와 Hugging Face / ModelScope URL을 혼합할 수 없습니다.", "noModelFiles": "이 저장소에서 모델 파일을 찾을 수 없습니다." }, diff --git a/locales/ru.json b/locales/ru.json index bddc516d..30e1a504 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1723,6 +1723,7 @@ "invalidUrl": "Неверный формат URL модели", "noVersions": "Нет доступных версий для этой модели", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "Нельзя смешивать URL-адреса CivitAI и Hugging Face / ModelScope в одном пакете.", "noModelFiles": "В этом репозитории не найдено файлов моделей." }, diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 9ad728f9..02a1fd5c 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1723,6 +1723,7 @@ "invalidUrl": "无效的模型 URL 格式", "noVersions": "此模型没有可用版本", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "无法在同一批次中混合使用 CivitAI 和 Hugging Face / ModelScope URL。", "noModelFiles": "在此仓库中未找到模型文件。" }, diff --git a/locales/zh-TW.json b/locales/zh-TW.json index d1282447..c05e1d5a 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1723,6 +1723,7 @@ "invalidUrl": "模型網址格式無效", "noVersions": "此模型無可用版本", "linkedFileNotFound": "[TODO: Translate] File {fileId} was not found in the requested model version.", + "linkedFileNotWeightFile": "[TODO: Translate] File {fileId} is not a downloadable model file.", "mixedSources": "無法在同一批次中混合使用 CivitAI 和 Hugging Face / ModelScope URL。", "noModelFiles": "在此倉庫中未找到模型檔案。" }, diff --git a/static/js/api/apiConfig.js b/static/js/api/apiConfig.js index ad7a6f1d..5e57a808 100644 --- a/static/js/api/apiConfig.js +++ b/static/js/api/apiConfig.js @@ -154,7 +154,6 @@ export const MODEL_SPECIFIC_ENDPOINTS = { civitaiUrl: `/api/lm/${MODEL_TYPES.LORA}/civitai-url`, metadata: `/api/lm/${MODEL_TYPES.LORA}/metadata`, getTriggerWordsPost: `/api/lm/${MODEL_TYPES.LORA}/get_trigger_words`, - civitaiModelByVersion: `/api/lm/${MODEL_TYPES.LORA}/civitai/model/version`, civitaiModelByHash: `/api/lm/${MODEL_TYPES.LORA}/civitai/model/hash`, }, [MODEL_TYPES.CHECKPOINT]: { diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index 47699864..f257749e 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -553,11 +553,14 @@ export class DownloadManager { getLinkedFile(version, fileId) { if (!fileId) return null; const file = this._getWeightFiles(version).find(f => String(f.id) === fileId); - if (!file) { - throw new Error(translate('modals.download.errors.linkedFileNotFound', { fileId }, - `File ${fileId} was not found in the requested model version.`)); + if (file) return file; + const existsAsNonWeightFile = (version?.files || []).some(f => String(f.id) === fileId); + if (existsAsNonWeightFile) { + throw new Error(translate('modals.download.errors.linkedFileNotWeightFile', { fileId }, + `File ${fileId} is not a downloadable model file.`)); } - return file; + throw new Error(translate('modals.download.errors.linkedFileNotFound', { fileId }, + `File ${fileId} was not found in the requested model version.`)); } static parseModelUrl(url) { diff --git a/static/js/utils/civitaiUtils.js b/static/js/utils/civitaiUtils.js index cc927d8b..af9a4d4e 100644 --- a/static/js/utils/civitaiUtils.js +++ b/static/js/utils/civitaiUtils.js @@ -20,9 +20,11 @@ export const OptimizationMode = { export const DEFAULT_CIVITAI_PAGE_HOST = 'civitai.com'; +// Mirrors py/utils/civitai_utils.py (_SUPPORTED_CIVITAI_PAGE_HOSTS). const SUPPORTED_CIVITAI_PAGE_HOSTS = new Set([ 'civitai.com', 'civitai.red', + 'civitai.green', ]); export function normalizeCivitaiPageHost(hostname) { @@ -218,7 +220,7 @@ export function isSupportedCivitaiPageHost(hostname) { return false; } - return SUPPORTED_CIVITAI_PAGE_HOSTS.has(hostname.toLowerCase()); + return SUPPORTED_CIVITAI_PAGE_HOSTS.has(hostname.toLowerCase().replace(/^www\./, '')); } export function extractCivitaiModelUrlParts(url) { diff --git a/tests/frontend/managers/downloadManager.downloadUrl.test.js b/tests/frontend/managers/downloadManager.downloadUrl.test.js index 3c4dc169..367da3e1 100644 --- a/tests/frontend/managers/downloadManager.downloadUrl.test.js +++ b/tests/frontend/managers/downloadManager.downloadUrl.test.js @@ -330,4 +330,25 @@ describe('DownloadManager Civitai download URLs', () => { expect(mockApiClient.fetchCivitaiModelByVersion).not.toHaveBeenCalled(); expect(mockApiClient.fetchCivitaiVersions).not.toHaveBeenCalled(); }); + + it.each([ + 'https://www.civitai.com/api/download/models/3409322?fileId=3301284', + 'https://civitai.red/api/download/models/3409322?fileId=3301284', + 'https://civitai.green/api/download/models/3409322?fileId=3301284', + ])('resolves download URLs on mirror and www-prefixed hosts: %s', async (mirrorUrl) => { + document.getElementById('modelUrl').value = mirrorUrl; + await manager.validateAndFetchVersions(); + expect(mockApiClient.fetchCivitaiModelByVersion).toHaveBeenCalledWith('3409322'); + expect(manager.currentVersion.id).toBe(3409322); + expect(manager.selectedFile.id).toBe(3301284); + }); + + it('reports a linked fileId that exists but is not a model weight file', async () => { + requestedVersion.files.push({ id: 555, type: 'Training Data', name: 'dataset.zip' }); + document.getElementById('modelUrl').value = url.replace('3301284', '555'); + await manager.validateAndFetchVersions(); + expect(document.getElementById('urlError').textContent).toBe( + 'File 555 is not a downloadable model file.'); + expect(manager.selectedFile).toBeNull(); + }); }); diff --git a/tests/frontend/utils/civitaiUtils.test.js b/tests/frontend/utils/civitaiUtils.test.js index d905a366..17caa313 100644 --- a/tests/frontend/utils/civitaiUtils.test.js +++ b/tests/frontend/utils/civitaiUtils.test.js @@ -337,14 +337,20 @@ describe('civitaiUtils', () => { }); describe('isSupportedCivitaiPageHost', () => { - it('accepts civitai.com and civitai.red page hosts', () => { + it('accepts civitai.com, civitai.red and civitai.green page hosts', () => { expect(isSupportedCivitaiPageHost('civitai.com')).toBe(true); expect(isSupportedCivitaiPageHost('civitai.red')).toBe(true); + expect(isSupportedCivitaiPageHost('civitai.green')).toBe(true); + }); + + it('accepts www-prefixed page hosts', () => { + expect(isSupportedCivitaiPageHost('www.civitai.com')).toBe(true); + expect(isSupportedCivitaiPageHost('www.civitai.red')).toBe(true); + expect(isSupportedCivitaiPageHost('www.civitai.green')).toBe(true); }); it('rejects unrelated hosts', () => { - expect(isSupportedCivitaiPageHost('www.civitai.com')).toBe(false); - expect(isSupportedCivitaiPageHost('www.civitai.red')).toBe(false); + expect(isSupportedCivitaiPageHost('civitai.com.evil.test')).toBe(false); expect(isSupportedCivitaiPageHost('example.com')).toBe(false); expect(isSupportedCivitaiPageHost('')).toBe(false); expect(isSupportedCivitaiPageHost(null)).toBe(false);