dikdimon's picture
Upload 2 files
2803079 verified
Raw
History Blame Contribute Delete
14.6 kB
(function () {
'use strict';
// =========================================================================
// 0000-a1111-fix/javascript/fix.js — v4.2
//
// Порядок загрузки гарантируется двумя механизмами:
//
// 1. Имя '!0000-a1111-fix' — A1111 сортирует user extensions alphabetically
// (sorted(os.listdir(...))), '!' ставит нас первыми среди пользовательских
// расширений.
//
// 2. metadata.ini [javascript/fix.js] Before = sd-webui-tabs-extension ...
// A1111 строит topological sort через list_scripts() и учитывает
// Before/After зависимости для JS-файлов.
//
// Что НЕ покрывается:
// Builtin extensions (extensions-builtin/) грузятся ДО пользовательских.
// Три builtin используют onUiLoaded: canvas-zoom-and-pan, mobile,
// prompt-bracket-checker. Все три исторически короткие, но реальная длительность зависит от среды.
// Источник медленных callbacks — пользовательские расширения (ControlNet и др.).
//
// Четыре исправления:
//
// [FIX-0] Idempotence guard.
// Гарантирует однократное применение патча при любых условиях.
//
// [FIX-1] scheduleAfterUiUpdateCallbacks — bounded debounce, max-wait 1000ms.
// Оригинал (script.js:111-115): clearTimeout + setTimeout 200ms.
// При 50+ расширениях DOM мутирует 2-5 сек непрерывно — uiAfterUpdateTimeout
// сбрасывается каждый раз, uiAfterUpdateCallbacks так и не вызываются.
// Фикс: добавляем maxWaitTimer, который форсирует вызов через MAX_WAIT_MS
// независимо от новых мутаций. uiAfterUpdateTimeout продолжает жить как
// глобал core (совместимость сохранена).
// Re-entry guard защищает от рекурсии если callback сам вызывает мутацию.
//
// [FIX-2] onUiLoaded timing — полный охват existing + future callbacks.
// Оригинал (script.js:60-62): uiLoadedCallbacks.push(callback).
// executeCallbacks (script.js:95-102) — синхронный, не cancelable.
// v3: оборачивал только БУДУЩИЕ вызовы onUiLoaded — builtin callbacks
// уже в uiLoadedCallbacks ДО нашего патча → не покрыты.
// v4: оборачиваем уже существующие элементы uiLoadedCallbacks + будущие.
// Symbol(WRAPPED) предотвращает двойную обёртку при любом сценарии.
// Логирование только первого краша на callback (Set по имени).
//
// [FIX-3] inputAccordionChecked — resilient resolver для tabs extension.
// tabs_parser.js (строки 92-100): если accordion.id не null и не
// начинается с "component-":
// content.id = accordion.id;
// accordion.id = '.' + accordion.id;
// content.visibleCheckbox = {}; <- plain Object, НЕ HTMLInputElement
// content.onVisibleCheckboxChange = () => {};
// Кроме того, в оригинальный accordion вставляется клонированный checkbox
// (строки 73-85) — т.е. .input-accordion-checkbox остаётся В accordion'е.
//
// Resolver (resolveRealAccordion) проверяет три пути:
// 1. getElementById(id) → если реальный accordion (не перемещён)
// 2. getElementById('.' + id) → tabs: оригинальный accordion переименован
// 3. querySelector .input-accordion-checkbox внутри el → fallback на случай
// если tabs extension изменит схему именования id в будущих версиях
// Log-once через Set предотвращает spam в консоли.
// =========================================================================
// ─── [FIX-0] Idempotence guard ───────────────────────────────────────────
if (window.__a1111_fix_v4_applied) {
console.warn('[a1111-fix] already applied, skipping duplicate load');
return;
}
window.__a1111_fix_v4_applied = true;
// ─── [FIX-1] scheduleAfterUiUpdateCallbacks — bounded debounce ───────────
function applyFix1() {
if (typeof scheduleAfterUiUpdateCallbacks === 'undefined' ||
typeof executeCallbacks === 'undefined' ||
typeof uiAfterUpdateCallbacks === 'undefined') {
console.warn('[a1111-fix] scheduleAfterUiUpdateCallbacks/executeCallbacks/uiAfterUpdateCallbacks not found, skipping FIX-1');
return;
}
const DEBOUNCE_MS = 200;
const MAX_WAIT_MS = 1000;
let maxWaitTimer = null;
let running = false;
function runAfterUpdateCallbacks() {
// uiAfterUpdateTimeout: продолжаем вести как глобал core (совместимость)
clearTimeout(uiAfterUpdateTimeout);
clearTimeout(maxWaitTimer);
window.uiAfterUpdateTimeout = null;
maxWaitTimer = null;
if (running) return;
running = true;
try {
executeCallbacks(uiAfterUpdateCallbacks);
} finally {
running = false;
}
}
window.scheduleAfterUiUpdateCallbacks = function () {
clearTimeout(uiAfterUpdateTimeout);
// uiAfterUpdateTimeout: сохраняем глобал, как делает оригинал (script.js:112-113)
window.uiAfterUpdateTimeout = setTimeout(runAfterUpdateCallbacks, DEBOUNCE_MS);
if (!maxWaitTimer) {
maxWaitTimer = setTimeout(runAfterUpdateCallbacks, MAX_WAIT_MS);
}
};
console.log('[a1111-fix] FIX-1: bounded debounce max-wait=' + MAX_WAIT_MS + 'ms applied');
}
// ─── [FIX-2] onUiLoaded timing — existing + future callbacks ─────────────
function applyFix2() {
if (typeof onUiLoaded === 'undefined' || typeof uiLoadedCallbacks === 'undefined') {
console.warn('[a1111-fix] onUiLoaded or uiLoadedCallbacks not found, skipping FIX-2');
return;
}
const SLOW_THRESHOLD_MS = 100;
const WRAPPED = Symbol('a1111FixWrapped');
// WeakSet по исходному callback (не по имени): Set<string> глушил бы все
// '(anonymous)' callbacks или однофамильцев — один краш скрывал бы другие.
const crashedOnce = new WeakSet();
function wrapCallback(callback) {
if (typeof callback !== 'function') return callback;
if (callback[WRAPPED]) return callback; // уже обёрнут — не дублируем
const wrapped = function (arg) {
const t0 = performance.now();
try {
return callback(arg);
} catch (e) {
// Не бросаем исключение дальше: core executeCallbacks (script.js:95-102)
// сам ловит ошибки и пишет console.error — двойное логирование не нужно.
// WeakSet по callback — каждый упавший callback логируется ровно один раз.
if (!crashedOnce.has(callback)) {
crashedOnce.add(callback);
console.error('[a1111-fix] onUiLoaded callback crashed:', callback.name || '(anonymous)', e);
}
} finally {
const elapsed = performance.now() - t0;
if (elapsed > SLOW_THRESHOLD_MS) {
console.warn(
'[a1111-fix] SLOW onUiLoaded: "' + (callback.name || '(anonymous)') + '" took ' + elapsed.toFixed(0) + 'ms'
);
}
}
};
wrapped[WRAPPED] = true;
wrapped.__a1111_fix_original = callback;
return wrapped;
}
// 1) Уже зарегистрированные callbacks (builtin extensions и ранние user extensions)
// uiLoadedCallbacks — глобальный массив (script.js:29), изменяем на месте.
let existingCount = 0;
for (let i = 0; i < uiLoadedCallbacks.length; i++) {
if (typeof uiLoadedCallbacks[i] === 'function') {
uiLoadedCallbacks[i] = wrapCallback(uiLoadedCallbacks[i]);
existingCount++;
}
}
// 2) Будущие callbacks через onUiLoaded(fn)
const _originalOnUiLoaded = window.onUiLoaded;
window.onUiLoaded = function (callback) {
_originalOnUiLoaded(wrapCallback(callback));
};
console.log(
'[a1111-fix] FIX-2: timing applied to ' + existingCount + ' existing + all future onUiLoaded callbacks'
);
}
// ─── [FIX-3] inputAccordionChecked — resilient accordion resolver ─────────
function applyFix3() {
if (typeof inputAccordionChecked === 'undefined') {
console.warn('[a1111-fix] inputAccordionChecked not found, skipping FIX-3');
return;
}
const warnedOnce = new Set();
/**
* Проверка, является ли el реальным setupAccordion-элементом.
* Реальный accordion после setupAccordion (inputAccordion.js:41-43):
* accordion.visibleCheckbox = visibleCheckbox (document.createElement('INPUT'))
* accordion.onVisibleCheckboxChange = function() {...}
* Стаб tabs_parser.js (строки 98-100):
* content.visibleCheckbox = {} <- plain Object
* content.onVisibleCheckboxChange = () => {} <- no-op
*/
function isRealAccordion(el) {
return !!(
el &&
el.visibleCheckbox instanceof HTMLInputElement &&
typeof el.onVisibleCheckboxChange === 'function'
);
}
/**
* Три пути поиска реального accordion'а:
*
* 1. getElementById(id) — стандартный путь, работает если tabs extension
* не трогал этот accordion (нет isInput, или id начинается с "component-").
*
* 2. getElementById('.' + id) — tabs_parser.js переименовывает оригинальный
* accordion: accordion.id = '.' + originalId (строка 94).
*
* 3. querySelector('.input-accordion-checkbox') внутри el — tabs_parser.js
* вставляет клонированный checkbox в content (строки 73-85), но оригинальный
* .input-accordion-checkbox остаётся внутри переименованного accordion.
* Этот путь — fallback на случай изменения схемы id в будущих версиях.
*/
function resolveRealAccordion(id) {
const direct = gradioApp().getElementById(id);
if (isRealAccordion(direct)) return direct;
// tabs extension: accordion переименован в '.' + id
const dotted = gradioApp().getElementById('.' + id);
if (isRealAccordion(dotted)) return dotted;
// Fallback: поиск .input-accordion-checkbox в ближайшем контейнере
// На случай изменения схемы tabs extension или нестандартных layouts
if (direct) {
const nestedCheckbox = direct.querySelector('.input-accordion-checkbox');
if (nestedCheckbox) {
// Поднимаемся к ближайшему .input-accordion
const parentAccordion = nestedCheckbox.closest('.input-accordion');
if (isRealAccordion(parentAccordion)) return parentAccordion;
}
}
return null;
}
window.inputAccordionChecked = function (id, checked) {
const realEl = resolveRealAccordion(id);
if (!realEl) {
if (!warnedOnce.has(id)) {
warnedOnce.add(id);
const direct = gradioApp().getElementById(id);
console.error(
'[a1111-fix] FIX-3: cannot resolve accordion for id:', id,
'\n direct el:', direct,
'\n direct.visibleCheckbox:', direct && direct.visibleCheckbox,
'\n direct.visibleCheckbox instanceof HTMLInputElement:',
direct && (direct.visibleCheckbox instanceof HTMLInputElement),
'\n dotted el:', gradioApp().getElementById('.' + id)
);
}
return;
}
realEl.visibleCheckbox.checked = checked;
realEl.onVisibleCheckboxChange();
};
console.log('[a1111-fix] FIX-3: resilient accordion resolver applied');
}
// ─── Apply ────────────────────────────────────────────────────────────────
applyFix1();
applyFix2();
applyFix3();
console.log('[a1111-fix] v4.2 ready');
})();