mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
feat(recipes): add reconnect remediation paths for missing recipe LoRAs
- Snapshot pre-rematch entry state (reconnectSnapshot) so rematched entries can be undone via the existing restore flow - Bulk missing-LoRA downloads mark unresolvable failures hash-invalid, flipping those entries from download to reconnect candidacy - Recipe modal always offers a reconnect action next to download for missing LoRA entries - Rematch runs collect an opt-in relaxed-matching choice (also reconnect missing models by file name) via a pre-run options dialog on the global, bulk and single-recipe entries - L4 (filename-level) matches are listed in a results dialog with per-entry undo
This commit is contained in:
@@ -4,6 +4,7 @@ import { translate } from '../../utils/i18nHelpers.js';
|
||||
import { state } from '../../state/index.js';
|
||||
import { getCompleteApiConfig, getCurrentModelType } from '../../api/apiConfig.js';
|
||||
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
|
||||
import { rematchModalManager } from '../../managers/RematchModalManager.js';
|
||||
|
||||
export class GlobalContextMenu extends BaseContextMenu {
|
||||
constructor() {
|
||||
@@ -368,6 +369,18 @@ export class GlobalContextMenu extends BaseContextMenu {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect options (relaxed matching) before starting anything; the
|
||||
// run only begins when the user confirms the dialog.
|
||||
rematchModalManager.showOptionsModal({
|
||||
onConfirm: ({ relaxed }) => this._startRematch(menuItem, relaxed),
|
||||
});
|
||||
}
|
||||
|
||||
async _startRematch(menuItem, relaxed = false) {
|
||||
if (this._rematchInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._rematchInProgress = true;
|
||||
menuItem?.classList.add('disabled');
|
||||
|
||||
@@ -384,6 +397,7 @@ export class GlobalContextMenu extends BaseContextMenu {
|
||||
const response = await fetch('/api/lm/recipes/rematch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ relaxed: !!relaxed }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
@@ -458,6 +472,11 @@ export class GlobalContextMenu extends BaseContextMenu {
|
||||
if (window.recipesPage) {
|
||||
window.recipesPage.refresh();
|
||||
}
|
||||
// Filename-level (L4) matches are imprecise —
|
||||
// always surface them for review/undo.
|
||||
if (Array.isArray(p.l4_matches) && p.l4_matches.length > 0) {
|
||||
rematchModalManager.showResultsModal(p.l4_matches);
|
||||
}
|
||||
} else if (p.status === 'error') {
|
||||
throw new Error(p.error || 'Rematch failed');
|
||||
} else if (p.status === 'cancelled') {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js
|
||||
import { updateRecipeMetadata } from '../../api/recipeApi.js';
|
||||
import { state } from '../../state/index.js';
|
||||
import { moveManager } from '../../managers/MoveManager.js';
|
||||
import { rematchModalManager } from '../../managers/RematchModalManager.js';
|
||||
import { probeExtension, delegateReimport, getCivitaiImageInfo } from '../../utils/extensionReimportBridge.js';
|
||||
|
||||
export class RecipeContextMenu extends BaseContextMenu {
|
||||
@@ -303,11 +304,22 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
// Capture before any await: the menu's click handler nulls currentCard
|
||||
const filePath = this.currentCard?.dataset?.filepath;
|
||||
|
||||
// Collect options (relaxed matching) before starting anything; the
|
||||
// run only begins when the user confirms the dialog.
|
||||
rematchModalManager.showOptionsModal({
|
||||
scope: 'single',
|
||||
onConfirm: ({ relaxed }) => this._startRematchRecipe(recipeId, filePath, relaxed),
|
||||
});
|
||||
}
|
||||
|
||||
async _startRematchRecipe(recipeId, filePath, relaxed = false) {
|
||||
try {
|
||||
showToast('Rematching recipe to local models...', {}, 'info');
|
||||
|
||||
const response = await fetch(`/api/lm/recipe/${recipeId}/rematch`, {
|
||||
method: 'POST'
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ relaxed: !!relaxed }),
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
@@ -330,6 +342,11 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
state.virtualScroller.updateSingleItem(filePath, updatedRecipe);
|
||||
}
|
||||
}
|
||||
// Filename-level (L4) matches are imprecise — always
|
||||
// surface them for review/undo.
|
||||
if (Array.isArray(result.l4_matches) && result.l4_matches.length > 0) {
|
||||
rematchModalManager.showResultsModal(result.l4_matches);
|
||||
}
|
||||
} else if (result.unresolved_entries > 0) {
|
||||
// Entries existed but have no local model — expected for
|
||||
// models deleted from Civitai; informational, not an error.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Recipe Modal Component
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow, stripLoraTags, sendPromptToWorkflow, sendGenParamsToWorkflow } from '../utils/uiHelpers.js';
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow, stripLoraTags, sendPromptToWorkflow, sendGenParamsToWorkflow, isUnresolvableDownloadError } from '../utils/uiHelpers.js';
|
||||
import { isModelWeightFile } from '../utils/modelFileTypes.js';
|
||||
import { buildCivitaiUrl } from '../utils/civitaiUtils.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
@@ -1078,8 +1078,9 @@ class RecipeModal {
|
||||
|
||||
// Mirror the checkpoint "broken" rule: deleted, an
|
||||
// unresolvable hash, or a name-only remnant with no CivitAI
|
||||
// identifiers at all cannot be fixed by downloading —
|
||||
// reconnecting a local LoRA is the only remediation.
|
||||
// identifiers at all cannot be fixed by downloading, so no
|
||||
// download button is offered. Reconnect is always available
|
||||
// for missing entries (see renderLoraItemActions).
|
||||
const needsReconnect = !existsLocally
|
||||
&& (isDeleted || lora.hashInvalid || !this.canDownloadLora(lora));
|
||||
|
||||
@@ -1180,7 +1181,7 @@ class RecipeModal {
|
||||
</div>
|
||||
${actionsRow}
|
||||
</div>
|
||||
${needsReconnect ? `
|
||||
${!existsLocally ? `
|
||||
<div class="lora-reconnect-container" data-lora-index="${loraIndex}">
|
||||
<div class="reconnect-instructions">
|
||||
<p>${escapeHtml(translate('recipes.resources.reconnectInstructions', {}, 'Enter LoRA syntax or name to reconnect:'))}</p>
|
||||
@@ -2853,11 +2854,7 @@ class RecipeModal {
|
||||
* the model cannot be resolved — never for transient transport errors.
|
||||
*/
|
||||
_isUnresolvableDownloadError(message) {
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
const text = String(message).toLowerCase();
|
||||
return /(not found|no longer available|deleted|removed|404|410|gone)/.test(text);
|
||||
return isUnresolvableDownloadError(message);
|
||||
}
|
||||
|
||||
getResourceCivitaiUrl(resource) {
|
||||
@@ -2915,19 +2912,9 @@ class RecipeModal {
|
||||
}
|
||||
|
||||
const controls = [];
|
||||
if (needsReconnect) {
|
||||
const reconnectLabel = translate('recipes.resources.reconnect', {}, 'Reconnect');
|
||||
const reconnectTooltip = translate('recipes.resources.reconnectTooltip', {}, 'Reconnect with a local LoRA');
|
||||
controls.push(`
|
||||
<button type="button" class="resource-action ghost compact lora-reconnect" data-lora-index="${loraIndex}"
|
||||
title="${escapeHtml(reconnectTooltip)}" aria-label="${escapeHtml(reconnectTooltip)}">
|
||||
<i class="fas fa-link" aria-hidden="true"></i>
|
||||
<span>${escapeHtml(reconnectLabel)}</span>
|
||||
</button>
|
||||
`);
|
||||
} else {
|
||||
if (!needsReconnect) {
|
||||
// needsReconnect already implies canDownloadLora() here, so the
|
||||
// download action is unconditional.
|
||||
// download action is unconditional in this branch.
|
||||
const downloadLabel = translate('recipes.resources.download', {}, 'Download');
|
||||
const downloadTooltip = translate('recipes.resources.downloadLoraTooltip', {}, 'Download this LoRA');
|
||||
controls.push(`
|
||||
@@ -2938,6 +2925,18 @@ class RecipeModal {
|
||||
</button>
|
||||
`);
|
||||
}
|
||||
// Reconnect is always offered for missing entries — when the LoRA
|
||||
// already exists locally under a different hash, downloading first
|
||||
// just to flip the button would be a waste.
|
||||
const reconnectLabel = translate('recipes.resources.reconnect', {}, 'Reconnect');
|
||||
const reconnectTooltip = translate('recipes.resources.reconnectTooltip', {}, 'Reconnect with a local LoRA');
|
||||
controls.push(`
|
||||
<button type="button" class="resource-action ghost compact lora-reconnect" data-lora-index="${loraIndex}"
|
||||
title="${escapeHtml(reconnectTooltip)}" aria-label="${escapeHtml(reconnectTooltip)}">
|
||||
<i class="fas fa-link" aria-hidden="true"></i>
|
||||
<span>${escapeHtml(reconnectLabel)}</span>
|
||||
</button>
|
||||
`);
|
||||
|
||||
return `<div class="recipe-lora-actions">${controls.join('')}</div>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user