dikdimon commited on
Commit
fc32171
·
verified ·
1 Parent(s): d85af2b

Upload 2 files

Browse files
!!!0000-a1111-fix/javascript/fix.js ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ 'use strict';
3
+
4
+ // =========================================================================
5
+ // 0000-a1111-fix/javascript/fix.js — v4.2
6
+ //
7
+ // Порядок загрузки гарантируется двумя механизмами:
8
+ //
9
+ // 1. Имя '!0000-a1111-fix' — A1111 сортирует user extensions alphabetically
10
+ // (sorted(os.listdir(...))), '!' ставит нас первыми среди пользовательских
11
+ // расширений.
12
+ //
13
+ // 2. metadata.ini [javascript/fix.js] Before = sd-webui-tabs-extension ...
14
+ // A1111 строит topological sort через list_scripts() и учитывает
15
+ // Before/After зависимости для JS-файлов.
16
+ //
17
+ // Что НЕ покрывается:
18
+ // Builtin extensions (extensions-builtin/) грузятся ДО пользовательских.
19
+ // Три builtin используют onUiLoaded: canvas-zoom-and-pan, mobile,
20
+ // prompt-bracket-checker. Все три исторически короткие, но реальная длительность зависит от среды.
21
+ // Источник медленных callbacks — пользовательские расширения (ControlNet и др.).
22
+ //
23
+ // Четыре исправления:
24
+ //
25
+ // [FIX-0] Idempotence guard.
26
+ // Гарантирует однократное применение патча при любых условиях.
27
+ //
28
+ // [FIX-1] scheduleAfterUiUpdateCallbacks — bounded debounce, max-wait 1000ms.
29
+ // Оригинал (script.js:111-115): clearTimeout + setTimeout 200ms.
30
+ // При 50+ расширениях DOM мутирует 2-5 сек непрерывно — uiAfterUpdateTimeout
31
+ // сбрасывается каждый раз, uiAfterUpdateCallbacks так и не вызываются.
32
+ // Фикс: добавляем maxWaitTimer, который форсирует вызов через MAX_WAIT_MS
33
+ // независимо от новых мутаций. uiAfterUpdateTimeout продолжает жить как
34
+ // глобал core (совместимость сохранена).
35
+ // Re-entry guard защищает от рекурсии если callback сам вызывает мутацию.
36
+ //
37
+ // [FIX-2] onUiLoaded timing — полный охват existing + future callbacks.
38
+ // Оригинал (script.js:60-62): uiLoadedCallbacks.push(callback).
39
+ // executeCallbacks (script.js:95-102) — синхронный, не cancelable.
40
+ // v3: оборачивал только БУДУЩИЕ вызовы onUiLoaded — builtin callbacks
41
+ // уже в uiLoadedCallbacks ДО нашего патча → не покрыты.
42
+ // v4: оборачиваем уже существующие элементы uiLoadedCallbacks + будущие.
43
+ // Symbol(WRAPPED) предотвращает двойную обёртку при любом сценарии.
44
+ // Логирование только первого краша на callback (Set по имени).
45
+ //
46
+ // [FIX-3] inputAccordionChecked — resilient resolver для tabs extension.
47
+ // tabs_parser.js (строки 92-100): если accordion.id не null и не
48
+ // начинается с "component-":
49
+ // content.id = accordion.id;
50
+ // accordion.id = '.' + accordion.id;
51
+ // content.visibleCheckbox = {}; <- plain Object, НЕ HTMLInputElement
52
+ // content.onVisibleCheckboxChange = () => {};
53
+ // Кроме того, в оригинальный accordion вставляется клонированный checkbox
54
+ // (строки 73-85) — т.е. .input-accordion-checkbox остаётся В accordion'е.
55
+ //
56
+ // Resolver (resolveRealAccordion) проверяет три пути:
57
+ // 1. getElementById(id) → если реальный accordion (не перемещён)
58
+ // 2. getElementById('.' + id) → tabs: оригинальный accordion переименован
59
+ // 3. querySelector .input-accordion-checkbox внутри el → fallback на случай
60
+ // если tabs extension изменит схему именования id в будущих версиях
61
+ // Log-once через Set предотвращает spam в консоли.
62
+ // =========================================================================
63
+
64
+
65
+ // ─── [FIX-0] Idempotence guard ───────────────────────────────────────────
66
+
67
+ if (window.__a1111_fix_v4_applied) {
68
+ console.warn('[a1111-fix] already applied, skipping duplicate load');
69
+ return;
70
+ }
71
+ window.__a1111_fix_v4_applied = true;
72
+
73
+
74
+ // ─── [FIX-1] scheduleAfterUiUpdateCallbacks — bounded debounce ──��────────
75
+
76
+ function applyFix1() {
77
+ if (typeof scheduleAfterUiUpdateCallbacks === 'undefined' ||
78
+ typeof executeCallbacks === 'undefined' ||
79
+ typeof uiAfterUpdateCallbacks === 'undefined') {
80
+ console.warn('[a1111-fix] scheduleAfterUiUpdateCallbacks/executeCallbacks/uiAfterUpdateCallbacks not found, skipping FIX-1');
81
+ return;
82
+ }
83
+
84
+ const DEBOUNCE_MS = 200;
85
+ const MAX_WAIT_MS = 1000;
86
+
87
+ let debounceTimer = null;
88
+ let maxWaitTimer = null;
89
+ let running = false;
90
+
91
+ function runAfterUpdateCallbacks() {
92
+ // Используем собственный debounce-таймер вместо смешивания bare global
93
+ // uiAfterUpdateTimeout и window.uiAfterUpdateTimeout.
94
+ clearTimeout(debounceTimer);
95
+ clearTimeout(maxWaitTimer);
96
+ debounceTimer = null;
97
+ maxWaitTimer = null;
98
+
99
+ if (running) return;
100
+ running = true;
101
+ try {
102
+ executeCallbacks(uiAfterUpdateCallbacks);
103
+ } finally {
104
+ running = false;
105
+ }
106
+ }
107
+
108
+ window.scheduleAfterUiUpdateCallbacks = function () {
109
+ clearTimeout(debounceTimer);
110
+ debounceTimer = setTimeout(runAfterUpdateCallbacks, DEBOUNCE_MS);
111
+
112
+ if (!maxWaitTimer) {
113
+ maxWaitTimer = setTimeout(runAfterUpdateCallbacks, MAX_WAIT_MS);
114
+ }
115
+ };
116
+
117
+ console.log('[a1111-fix] FIX-1: bounded debounce max-wait=' + MAX_WAIT_MS + 'ms applied');
118
+ }
119
+
120
+
121
+ // ─── [FIX-2] onUiLoaded timing — existing + future callbacks ─────────────
122
+
123
+ function applyFix2() {
124
+ if (typeof onUiLoaded === 'undefined' || typeof uiLoadedCallbacks === 'undefined') {
125
+ console.warn('[a1111-fix] onUiLoaded or uiLoadedCallbacks not found, skipping FIX-2');
126
+ return;
127
+ }
128
+
129
+ const SLOW_THRESHOLD_MS = 100;
130
+ const WRAPPED = Symbol('a1111FixWrapped');
131
+ // WeakSet по исходному callback (не по имени): Set<string> глушил бы все
132
+ // '(anonymous)' callbacks или однофамильцев — один краш скрывал бы другие.
133
+ const crashedOnce = new WeakSet();
134
+
135
+ function wrapCallback(callback) {
136
+ if (typeof callback !== 'function') return callback;
137
+ if (callback[WRAPPED]) return callback; // уже обёрнут — не дублируем
138
+
139
+ const wrapped = function (arg) {
140
+ const t0 = performance.now();
141
+ try {
142
+ return callback(arg);
143
+ } catch (e) {
144
+ // Не бросаем исключение дальше: core executeCallbacks (script.js:95-102)
145
+ // сам ловит ошибки и пишет console.error — двойное логирование не нужно.
146
+ // WeakSet по callback — каждый упавший callback логируется ровно один раз.
147
+ if (!crashedOnce.has(callback)) {
148
+ crashedOnce.add(callback);
149
+ console.error('[a1111-fix] onUiLoaded callback crashed:', callback.name || '(anonymous)', e);
150
+ }
151
+ } finally {
152
+ const elapsed = performance.now() - t0;
153
+ if (elapsed > SLOW_THRESHOLD_MS) {
154
+ console.warn(
155
+ '[a1111-fix] SLOW onUiLoaded: "' + (callback.name || '(anonymous)') + '" took ' + elapsed.toFixed(0) + 'ms'
156
+ );
157
+ }
158
+ }
159
+ };
160
+
161
+ wrapped[WRAPPED] = true;
162
+ wrapped.__a1111_fix_original = callback;
163
+ return wrapped;
164
+ }
165
+
166
+ // 1) Уже зарегистрированные callbacks (builtin extensions и ранние user extensions)
167
+ // uiLoadedCallbacks — глобальный массив (script.js:29), изменяем на месте.
168
+ let existingCount = 0;
169
+ for (let i = 0; i < uiLoadedCallbacks.length; i++) {
170
+ if (typeof uiLoadedCallbacks[i] === 'function') {
171
+ uiLoadedCallbacks[i] = wrapCallback(uiLoadedCallbacks[i]);
172
+ existingCount++;
173
+ }
174
+ }
175
+
176
+ // 2) Будущие callbacks через onUiLoaded(fn)
177
+ const _originalOnUiLoaded = window.onUiLoaded;
178
+ window.onUiLoaded = function (callback) {
179
+ _originalOnUiLoaded(wrapCallback(callback));
180
+ };
181
+
182
+ console.log(
183
+ '[a1111-fix] FIX-2: timing applied to ' + existingCount + ' existing + all future onUiLoaded callbacks'
184
+ );
185
+ }
186
+
187
+
188
+ // ─── [FIX-3] inputAccordionChecked — resilient accordion resolver ─────────
189
+
190
+ function applyFix3() {
191
+ if (typeof inputAccordionChecked === 'undefined') {
192
+ console.warn('[a1111-fix] inputAccordionChecked not found, skipping FIX-3');
193
+ return;
194
+ }
195
+
196
+ const warnedOnce = new Set();
197
+
198
+ /**
199
+ * Проверка, является ли el реальным setupAccordion-элементом.
200
+ * Реальный accordion после setupAccordion (inputAccordion.js:41-43):
201
+ * accordion.visibleCheckbox = visibleCheckbox (document.createElement('INPUT'))
202
+ * accordion.onVisibleCheckboxChange = function() {...}
203
+ * Стаб tabs_parser.js (строки 98-100):
204
+ * content.visibleCheckbox = {} <- plain Object
205
+ * content.onVisibleCheckboxChange = () => {} <- no-op
206
+ */
207
+ function isRealAccordion(el) {
208
+ return !!(
209
+ el &&
210
+ el.visibleCheckbox instanceof HTMLInputElement &&
211
+ typeof el.onVisibleCheckboxChange === 'function'
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Три пути поиска реального accordion'а:
217
+ *
218
+ * 1. getElementById(id) — стандартный путь, работает если tabs extension
219
+ * не трогал этот accordion (нет isInput, или id начинается с "component-").
220
+ *
221
+ * 2. getElementById('.' + id) — tabs_parser.js переименовывает оригинальный
222
+ * accordion: accordion.id = '.' + originalId (строка 94).
223
+ *
224
+ * 3. querySelector('.input-accordion-checkbox') внутри el — tabs_parser.js
225
+ * вставляет клонированный checkbox в content (строки 73-85), но оригинальный
226
+ * .input-accordion-checkbox остаётся внутри переименованного accordion.
227
+ * Этот путь — fallback на случай изменения схемы id в будущих версиях.
228
+ */
229
+ function resolveRealAccordion(id) {
230
+ const direct = gradioApp().getElementById(id);
231
+ if (isRealAccordion(direct)) return direct;
232
+
233
+ // tabs extension: accordion переименован в '.' + id
234
+ const dotted = gradioApp().getElementById('.' + id);
235
+ if (isRealAccordion(dotted)) return dotted;
236
+
237
+ // Fallback: поиск .input-accordion-checkbox в ближайшем контейнере
238
+ // На случай изменения схемы tabs extension или нестандартных layouts
239
+ if (direct) {
240
+ const nestedCheckbox = direct.querySelector('.input-accordion-checkbox');
241
+ if (nestedCheckbox) {
242
+ // Поднимаемся к ближайшему .input-accordion
243
+ const parentAccordion = nestedCheckbox.closest('.input-accordion');
244
+ if (isRealAccordion(parentAccordion)) return parentAccordion;
245
+ }
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ window.inputAccordionChecked = function (id, checked) {
252
+ const realEl = resolveRealAccordion(id);
253
+
254
+ if (!realEl) {
255
+ if (!warnedOnce.has(id)) {
256
+ warnedOnce.add(id);
257
+ const direct = gradioApp().getElementById(id);
258
+ console.error(
259
+ '[a1111-fix] FIX-3: cannot resolve accordion for id:', id,
260
+ '\n direct el:', direct,
261
+ '\n direct.visibleCheckbox:', direct && direct.visibleCheckbox,
262
+ '\n direct.visibleCheckbox instanceof HTMLInputElement:',
263
+ direct && (direct.visibleCheckbox instanceof HTMLInputElement),
264
+ '\n dotted el:', gradioApp().getElementById('.' + id)
265
+ );
266
+ }
267
+ return;
268
+ }
269
+
270
+ realEl.visibleCheckbox.checked = checked;
271
+ realEl.onVisibleCheckboxChange();
272
+ };
273
+
274
+ console.log('[a1111-fix] FIX-3: resilient accordion resolver applied');
275
+ }
276
+
277
+
278
+ // ─── Apply ────────────────────────────────────────────────────────────────
279
+
280
+ applyFix1();
281
+ applyFix2();
282
+ applyFix3();
283
+
284
+ console.log('[a1111-fix] v4.3 ready');
285
+
286
+ })();
!!!0000-a1111-fix/metadata.ini ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [Extension]
2
+ Name = 0000-a1111-fix
3
+
4
+ ; Порядок загрузки JS через topological sort A1111.
5
+ ;
6
+ ; Примечание по canonical name:
7
+ ; В официальной wiki A1111 поле Name описано как canonical name.
8
+ ; Но в загруженном dev-tree (modules/extensions.py) self.canonical_name
9
+ ; перезаписывается именем папки (extension_dirname), а не берётся из Name.
10
+ ; Поэтому на практике в этой ветке реальный canonical name = имя папки.
11
+ ; Поле Name здесь оставлено для читаемости, но не влияет на Before/After логику.
12
+ ;
13
+ ; tabs extension устанавливается двумя способами:
14
+ ; git clone -> папка называется "sd-webui-tabs-extension"
15
+ ; скачать zip с GitHub -> папка называется "sd-webui-tabs-extension-main"
16
+ ;
17
+ ; Перечисляем оба варианта. Несуществующие Before-цели A1111 молча игнорирует
18
+ ; (scripts.py: если scripts.get() и loaded_extensions_scripts.get() вернули None —
19
+ ; пропускается без ошибки).
20
+
21
+ [javascript/fix.js]
22
+ Before = sd-webui-tabs-extension sd-webui-tabs-extension-main