| (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'); | |
| })(); | |