diff --git a/web/linker.js b/web/linker.js
index b166bd1..0bf7a6d 100644
--- a/web/linker.js
+++ b/web/linker.js
@@ -26,6 +26,8 @@ class LinkerManagerDialog extends ComfyDialog {
this.activeDownloads = {}; // Track active downloads
this.searchResultCache = new Map();
this.searchProgressTimers = new Map();
+ this.urnResolvePromises = new Map();
+ this.urnLocalMatchPromises = new Map();
this.cachedAnalysisData = null;
this.cachedWorkflowSignature = null;
this.selectedMissingModelKey = null;
@@ -1047,21 +1049,189 @@ class LinkerManagerDialog extends ComfyDialog {
}
}
- async refreshUrnLocalMatches(missing) {
- if (!missing?.civitai_info?.expected_filename || !this.contentElement) return;
+ getUrnIds(missing = {}) {
+ return {
+ modelId: missing.urn_model_id || missing.urn?.model_id,
+ versionId: missing.urn_version_id || missing.urn?.version_id
+ };
+ }
- const bodyId = `local-matches-body-${missing.node_id}-${missing.widget_index}`;
- const container = this.contentElement.querySelector(`#${bodyId}`);
- if (!container) return;
+ getUrnResolveKey(missing = {}) {
+ const ids = this.getUrnIds(missing);
+ return `${ids.modelId || ''}:${ids.versionId || ''}:${this.getMissingModelKey(missing)}`;
+ }
- container.innerHTML = `
Searching local matches for "${missing.civitai_info.expected_filename}"...
`;
+ getCivitaiResultFromMissing(missing = {}) {
+ if (!missing.civitai_info?.expected_filename && !missing.download_source?.url) return null;
+ const ids = this.getUrnIds(missing);
+ return {
+ name: missing.civitai_info?.model_name || missing.download_source?.name,
+ version_name: missing.civitai_info?.version_name || missing.download_source?.version_name,
+ filename: missing.civitai_info?.expected_filename || missing.download_source?.filename,
+ download_url: missing.download_source?.url,
+ url: missing.download_source?.model_url,
+ type: missing.download_source?.type || missing.category,
+ size: missing.download_source?.size,
+ model_id: missing.download_source?.model_id || ids.modelId,
+ version_id: missing.download_source?.version_id || ids.versionId,
+ base_model: missing.civitai_info?.base_model || missing.download_source?.base_model,
+ tags: missing.civitai_info?.tags || missing.download_source?.tags || []
+ };
+ }
- try {
+ applyCivitaiUrnResult(missing, civitai = {}) {
+ if (!missing || !civitai) return;
+ const ids = this.getUrnIds(missing);
+ missing.civitai_info = {
+ model_name: civitai.name,
+ version_name: civitai.version_name,
+ expected_filename: civitai.filename,
+ base_model: civitai.base_model,
+ tags: civitai.tags || []
+ };
+ missing.download_source = {
+ source: 'civitai',
+ url: civitai.download_url,
+ filename: civitai.filename,
+ name: civitai.name,
+ version_name: civitai.version_name,
+ type: civitai.type,
+ directory: missing.category || 'checkpoints',
+ match_type: 'exact',
+ size: civitai.size,
+ model_id: civitai.model_id || ids.modelId,
+ version_id: civitai.version_id || ids.versionId,
+ model_url: civitai.url || `https://civitai.com/models/${ids.modelId}?modelVersionId=${ids.versionId}`,
+ base_model: civitai.base_model,
+ tags: civitai.tags || []
+ };
+ }
+
+ async resolveUrnDataForMissing(missing) {
+ if (!missing?.is_urn) return null;
+
+ const existing = this.getCivitaiResultFromMissing(missing);
+ if (existing?.filename) {
+ return { civitai: existing };
+ }
+
+ const ids = this.getUrnIds(missing);
+ if (!ids.modelId || !ids.versionId) return null;
+
+ const key = this.getUrnResolveKey(missing);
+ if (this.urnResolvePromises.has(key)) {
+ return this.urnResolvePromises.get(key);
+ }
+
+ const promise = (async () => {
+ const tokens = this.getStoredTokens();
+ const payload = {
+ filename: `${ids.modelId}_${ids.versionId}`,
+ category: '',
+ is_urn: true,
+ sources: ['civitai'],
+ model_id: ids.modelId,
+ version_id: ids.versionId,
+ civitai_candidate_limit: tokens.civitai_candidate_limit
+ };
+
+ const response = await api.fetchApi('/model_linker/search', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+
+ if (!response.ok) {
+ throw new Error(`URN resolve failed: ${response.status}`);
+ }
+
+ const data = await response.json();
+ if (data.civitai) {
+ this.applyCivitaiUrnResult(missing, data.civitai);
+ }
+ return data;
+ })().finally(() => {
+ this.urnResolvePromises.delete(key);
+ });
+
+ this.urnResolvePromises.set(key, promise);
+ return promise;
+ }
+
+ refreshMissingListStats() {
+ if (!this.contentElement) return;
+ const statsEl = this.contentElement.querySelector('.ml-missing-list-stats');
+ if (!statsEl) return;
+
+ const stats = this.getMissingModelSummaryStats(this.missingModels || []);
+ statsEl.innerHTML = `
+ ${stats.exact} exact
+ ${stats.partial} partial
+ ${stats.none} no match
+ `;
+ }
+
+ refreshMissingListRowLocalMatch(missing = {}) {
+ if (!this.contentElement) return;
+
+ const key = this.getMissingModelKey(missing);
+ const rows = this.contentElement.querySelectorAll('.ml-missing-list-row');
+ for (const row of rows) {
+ if (row.getAttribute('data-missing-key') !== key) continue;
+
+ const bestMatch = this.getBestLocalMatch(missing, 70);
+ const confidence = bestMatch ? Number(bestMatch.confidence || 0) : 0;
+ const matchName = bestMatch?.model?.relative_path || bestMatch?.filename || bestMatch?.path || '';
+ const matchDisplay = matchName || 'No local match';
+ const matchClass = confidence === 100 ? 'exact' : (bestMatch ? 'partial' : 'none');
+
+ const bestEl = row.querySelector('.ml-missing-row-best');
+ if (bestEl) {
+ bestEl.setAttribute('data-tooltip', matchDisplay);
+ bestEl.innerHTML = bestMatch
+ ? this.escapeHtml(matchDisplay)
+ : '-- No local match';
+ }
+
+ const matchEl = row.querySelector('.ml-missing-row-match');
+ if (matchEl) {
+ matchEl.classList.remove(
+ 'ml-missing-row-match-exact',
+ 'ml-missing-row-match-partial',
+ 'ml-missing-row-match-none'
+ );
+ matchEl.classList.add(`ml-missing-row-match-${matchClass}`);
+ const valueEl = matchEl.querySelector('strong');
+ if (valueEl) {
+ valueEl.textContent = bestMatch
+ ? `${confidence.toFixed(confidence % 1 ? 1 : 0)}%`
+ : '--';
+ }
+ }
+ }
+
+ this.refreshMissingListStats();
+ }
+
+ async fetchUrnLocalMatches(missing) {
+ if (!missing?.civitai_info?.expected_filename) return [];
+
+ const filename = missing.civitai_info.expected_filename;
+ if (missing.__urnLocalMatchesFilename === filename && Array.isArray(missing.matches)) {
+ return missing.matches;
+ }
+
+ const key = this.getUrnResolveKey(missing);
+ if (this.urnLocalMatchPromises.has(key)) {
+ return this.urnLocalMatchPromises.get(key);
+ }
+
+ const promise = (async () => {
const response = await api.fetchApi('/model_linker/local-matches', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- filename: missing.civitai_info.expected_filename,
+ filename,
category: missing.category || ''
})
});
@@ -1072,14 +1242,58 @@ class LinkerManagerDialog extends ComfyDialog {
const data = await response.json();
missing.matches = Array.isArray(data.matches) ? data.matches : [];
- container.innerHTML = this.renderLocalMatchesContent(missing, missing.__displayIndex || 0);
- this.wireLocalMatchButtons(this.contentElement, missing, missing.__displayIndex || 0);
+ missing.__urnLocalMatchesFilename = filename;
+ return missing.matches;
+ })().finally(() => {
+ this.urnLocalMatchPromises.delete(key);
+ });
+
+ this.urnLocalMatchPromises.set(key, promise);
+ return promise;
+ }
+
+ async refreshUrnLocalMatches(missing) {
+ if (!missing?.civitai_info?.expected_filename || !this.contentElement) return;
+
+ const bodyId = `local-matches-body-${missing.node_id}-${missing.widget_index}`;
+ const container = this.contentElement.querySelector(`#${bodyId}`);
+ if (container) {
+ container.innerHTML = `Searching local matches for "${missing.civitai_info.expected_filename}"...
`;
+ }
+
+ try {
+ await this.fetchUrnLocalMatches(missing);
+ if (container) {
+ container.innerHTML = this.renderLocalMatchesContent(missing, missing.__displayIndex || 0);
+ this.wireLocalMatchButtons(this.contentElement, missing, missing.__displayIndex || 0);
+ }
+ this.refreshMissingListRowLocalMatch(missing);
} catch (error) {
console.error('Model Linker: URN local match refresh error:', error);
- container.innerHTML = `Failed to refresh local matches.
`;
+ if (container) {
+ container.innerHTML = `Failed to refresh local matches.
`;
+ }
}
}
+ scheduleInitialUrnLocalMatchRefresh(missingModels = []) {
+ missingModels.forEach((missing) => {
+ if (!missing?.is_urn || missing.__urnLocalRefreshQueued) return;
+ if (this.getBestLocalMatch(missing, 70)) return;
+
+ missing.__urnLocalRefreshQueued = true;
+ setTimeout(async () => {
+ try {
+ await this.resolveUrnDataForMissing(missing);
+ await this.refreshUrnLocalMatches(missing);
+ } catch (error) {
+ missing.__urnLocalRefreshQueued = false;
+ console.error('Model Linker: initial URN local match refresh error:', error);
+ }
+ }, 0);
+ });
+ }
+
/**
* Handle click outside context menu to hide it
*/
@@ -5424,6 +5638,7 @@ class LinkerManagerDialog extends ComfyDialog {
hasAny100Match
);
this.wireMissingModelsBrowser(container, data, sortedMissingModels);
+ this.scheduleInitialUrnLocalMatchRefresh(sortedMissingModels);
}
renderMissingModel(missing, missingIndex = 0) {
@@ -6449,28 +6664,41 @@ class LinkerManagerDialog extends ComfyDialog {
}
try {
- const tokens = this.getStoredTokens();
- const payload = {
- filename: modelId + '_' + versionId,
- category: '',
- is_urn: true,
- sources: ['civitai'],
- model_id: modelId,
- version_id: versionId,
- civitai_candidate_limit: tokens.civitai_candidate_limit
- };
- console.log('resolveUrnAsync payload:', JSON.stringify(payload));
-
- const response = await api.fetchApi('/model_linker/search', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload)
- });
-
- console.log('resolveUrnAsync response status:', response.status);
-
- if (response.ok) {
- const data = await response.json();
+ const downloadContainerId = loadingElementId.replace('urn-loading-', 'urn-download-');
+ const missing = this.missingModels.find(m =>
+ `urn-download-${m.node_id}-${m.widget_index}` === downloadContainerId
+ );
+
+ let data = null;
+ if (missing) {
+ data = await this.resolveUrnDataForMissing(missing);
+ } else {
+ const tokens = this.getStoredTokens();
+ const payload = {
+ filename: `${modelId}_${versionId}`,
+ category: '',
+ is_urn: true,
+ sources: ['civitai'],
+ model_id: modelId,
+ version_id: versionId,
+ civitai_candidate_limit: tokens.civitai_candidate_limit
+ };
+ console.log('resolveUrnAsync payload:', JSON.stringify(payload));
+
+ const response = await api.fetchApi('/model_linker/search', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+
+ console.log('resolveUrnAsync response status:', response.status);
+ if (!response.ok) {
+ throw new Error(`URN resolve failed: ${response.status}`);
+ }
+ data = await response.json();
+ }
+
+ if (data) {
const loadingEl = document.getElementById(loadingElementId);
if (loadingEl && data.civitai) {
loadingEl.classList.remove('ml-is-muted', 'ml-is-error');
@@ -6485,36 +6713,10 @@ class LinkerManagerDialog extends ComfyDialog {
loadingEl.classList.add('ml-is-muted');
}
- const downloadContainerId = loadingElementId.replace('urn-loading-', 'urn-download-');
const downloadEl = document.getElementById(downloadContainerId);
if (downloadEl && data.civitai?.download_url) {
- const missing = this.missingModels.find(m =>
- `urn-download-${m.node_id}-${m.widget_index}` === downloadContainerId
- );
if (missing) {
- missing.civitai_info = {
- model_name: data.civitai.name,
- version_name: data.civitai.version_name,
- expected_filename: data.civitai.filename,
- base_model: data.civitai.base_model,
- tags: data.civitai.tags || []
- };
- missing.download_source = {
- source: 'civitai',
- url: data.civitai.download_url,
- filename: data.civitai.filename,
- name: data.civitai.name,
- version_name: data.civitai.version_name,
- type: data.civitai.type,
- directory: missing.category || 'checkpoints',
- match_type: 'exact',
- size: data.civitai.size,
- model_id: data.civitai.model_id || modelId,
- version_id: data.civitai.version_id || versionId,
- model_url: data.civitai.url || `https://civitai.com/models/${modelId}?modelVersionId=${versionId}`,
- base_model: data.civitai.base_model,
- tags: data.civitai.tags || []
- };
+ this.applyCivitaiUrnResult(missing, data.civitai);
const downloadParent = downloadEl.parentElement;
downloadEl.outerHTML = this.renderKnownDownloadPanel(missing, missing.download_source);
this.wireDownloadSearchPanel(downloadParent || this.contentElement, missing);