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)
This commit is contained in:
Will Miao
2026-10-21 21:53:59 +08:00
parent 8033292ad8
commit 0be3ea7d98
15 changed files with 50 additions and 9 deletions
+1
View File
@@ -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."
},
+1
View File
@@ -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."
},
+1
View File
@@ -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."
},
+1
View File
@@ -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."
},
+1
View File
@@ -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": "לא נמצאו קבצי מודל במאגר זה."
},
+1
View File
@@ -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": "このリポジトリにモデルファイルが見つかりませんでした。"
},
+1
View File
@@ -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": "이 저장소에서 모델 파일을 찾을 수 없습니다."
},
+1
View File
@@ -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": "В этом репозитории не найдено файлов моделей."
},
+1
View File
@@ -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": "在此仓库中未找到模型文件。"
},
+1
View File
@@ -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": "在此倉庫中未找到模型檔案。"
},
-1
View File
@@ -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]: {
+7 -4
View File
@@ -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) {
+3 -1
View File
@@ -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) {
@@ -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();
});
});
+9 -3
View File
@@ -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);