fix(recipes): resolve stale LoRA hash on import and add hashInvalid state

- import: prefer A1111 Lora hashes (12-char AutoV3) over conflicting Hashes
  JSON values; recover the quote-wrapped AutoV3 from CivitAI image API meta;
  merge EXIF-parsed LoRAs when the API-only parse yields none (meta=null)
- rematch: treat entries whose hash failed CivitAI resolution (hashInvalid)
  as unresolved candidates; clear the flag on rematch/reconnect write-back
- download: persist hashInvalid and show a distinct toast when hash lookup
  returns "Model not found", so unresolvable entries become recoverable
- ui: add Unresolvable Hash badge styling and reconnect affordance
- i18n: translate the new keys across all 10 locales
This commit is contained in:
Will Miao
2026-08-28 22:24:07 +08:00
parent a7d65fe84a
commit 856c9a87ac
24 changed files with 757 additions and 28 deletions
+41 -4
View File
@@ -897,6 +897,11 @@ class RecipeModal {
<div class="deleted-badge" title="${escapeHtml(translate('recipes.resources.deletedTooltip', {}, 'This LoRA was deleted from the source and is no longer available for download'))}">
<i class="fas fa-trash-alt" aria-hidden="true"></i> ${escapeHtml(translate('recipes.resources.deleted', {}, 'Deleted'))}
</div>`;
} else if (lora.hashInvalid) {
statusBadge = `
<div class="invalid-hash-badge" title="${escapeHtml(translate('recipes.resources.hashInvalidTooltip', {}, 'This LoRA hash cannot be resolved on CivitAI - the model may have been updated'))}">
<i class="fas fa-question-circle" aria-hidden="true"></i> ${escapeHtml(translate('recipes.resources.hashInvalid', {}, 'Unresolvable Hash'))}
</div>`;
} else {
statusBadge = `
<div class="missing-badge" title="${escapeHtml(translate('recipes.resources.notInLibraryTooltip', {}, 'This model is not in your library'))}">
@@ -1982,7 +1987,7 @@ class RecipeModal {
}
const controls = [];
if (isDeleted) {
if (isDeleted || lora.hashInvalid) {
const reconnectLabel = translate('recipes.resources.reconnect', {}, 'Reconnect');
const reconnectTooltip = translate('recipes.resources.reconnectTooltip', {}, 'Reconnect with a local LoRA');
controls.push(`
@@ -2032,7 +2037,7 @@ class RecipeModal {
const loraIndex = parseInt(button.dataset.loraIndex, 10);
const lora = this.currentRecipe?.loras?.[loraIndex];
if (lora) {
this.downloadRecipeLora(lora, button);
this.downloadRecipeLora(lora, button, loraIndex);
}
});
});
@@ -2112,7 +2117,7 @@ class RecipeModal {
}
}
async downloadRecipeLora(lora, button) {
async downloadRecipeLora(lora, button, loraIndex) {
if (!this.canDownloadLora(lora)) {
showToast('toast.recipes.missingLoraDownloadInfo', {}, 'error');
return;
@@ -2141,7 +2146,12 @@ class RecipeModal {
state.loadingManager.hide();
}
if (!identifiers) {
showToast('toast.recipes.missingLoraDownloadInfo', {}, 'error');
if (!hasDirectIds && lora.hash) {
await this.markLoraHashInvalid(loraIndex);
showToast('toast.recipes.hashNotFoundOnCivitai', {}, 'error');
} else {
showToast('toast.recipes.missingLoraDownloadInfo', {}, 'error');
}
return;
}
@@ -2170,6 +2180,33 @@ class RecipeModal {
}
}
async markLoraHashInvalid(loraIndex) {
const recipeId =
this.recipeId ||
extractRecipeId(this.listFilePath || this.currentRecipe?.file_path);
if (!recipeId) {
return;
}
try {
await fetch('/api/lm/recipe/lora/mark-hash-invalid', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipe_id: recipeId,
lora_index: loraIndex,
}),
});
if (this.currentRecipe?.loras?.[loraIndex]) {
this.currentRecipe.loras[loraIndex].hashInvalid = true;
this.syncResourcesSection(this.currentRecipe);
}
} catch (error) {
console.warn('Failed to mark LoRA hash invalid:', error);
}
}
navigateToCheckpointPage(checkpoint) {
const checkpointHash = this._getCheckpointHash(checkpoint);