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:
Will Miao
2026-09-09 06:59:54 +08:00
parent e747946f7a
commit 1b5cbbbaa0
33 changed files with 2103 additions and 76 deletions
+17 -1
View File
@@ -3,6 +3,7 @@ import { showToast, showActionToast, copyToClipboard, sendLoraToWorkflow, sendEm
import { handleUndoDelete } from '../utils/undoHelpers.js';
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
import { modalManager } from './ModalManager.js';
import { rematchModalManager } from './RematchModalManager.js';
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
import { RecipeSidebarApiClient, updateRecipeMetadata, extractRecipeId } from '../api/recipeApi.js';
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
@@ -978,6 +979,15 @@ export class BulkManager {
return;
}
// Collect options (relaxed matching) before starting anything; the
// run only begins when the user confirms the dialog.
rematchModalManager.showOptionsModal({
recipeCount: state.selectedModels.size,
onConfirm: ({ relaxed }) => this._startRematchSelectedRecipes(relaxed),
});
}
async _startRematchSelectedRecipes(relaxed = false) {
try {
const apiClient = this.getActiveApiClient();
const filePaths = Array.from(state.selectedModels);
@@ -989,7 +999,7 @@ export class BulkManager {
state.loadingManager.showSimpleLoading('Rematching recipes to local models...');
const result = await apiClient.rematchBulkModels(filePaths);
const result = await apiClient.rematchBulkModels(filePaths, { relaxed: !!relaxed });
if (result.success) {
const total = result.total || filePaths.length;
@@ -1050,6 +1060,12 @@ export class BulkManager {
}
if (state.bulkMode) this.toggleBulkMode();
// 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 {
throw new Error(result.error || 'Bulk rematch failed');
}