bizydraft 0.2.31__py3-none-any.whl → 0.2.78.dev20251117024007__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,498 @@
1
+ // 媒体节点配置获取与工具函数(与 hookLoad/model.js 结构一致,面向 media_load_nodes)
2
+ import { fetchMediaConfig } from './configLoader.js'
3
+ import { getCookie, computeExt, hideWidget } from '../tool.js'
4
+
5
+ // 动态配置缓存(仅缓存媒体部分)
6
+ let mediaConfigCache = null;
7
+ let mediaConfigLoadPromise = null;
8
+
9
+ export const mediaNodeList = [
10
+ 'LoadImage',
11
+ 'LoadImageMask',
12
+ 'LoadAudio',
13
+ 'LoadVideo',
14
+ 'Load3D',
15
+ 'VHS_LoadVideo',
16
+ 'VHS_LoadAudioUpload'
17
+ ]
18
+ // 常见的媒体输入字段名(作为回退匹配)
19
+ export const possibleMediaWidgetNames = [
20
+ "image",
21
+ "file",
22
+ "audio",
23
+ "video",
24
+ "model_file"
25
+ ];
26
+
27
+ // 获取媒体配置的API函数(使用共享配置加载器)
28
+ export async function fetchMediaConfigWithCache() {
29
+ if (mediaConfigCache) return mediaConfigCache;
30
+ if (mediaConfigLoadPromise) return mediaConfigLoadPromise;
31
+
32
+ mediaConfigLoadPromise = (async () => {
33
+ const config = await fetchMediaConfig();
34
+ if (config) {
35
+ mediaConfigCache = config;
36
+ }
37
+ return config;
38
+ })();
39
+
40
+ return mediaConfigLoadPromise;
41
+ }
42
+
43
+ // 根据节点名称获取媒体节点配置(仅使用缓存,不阻塞返回;触发后台预取)
44
+ export async function getMediaNodeConfig(nodeName) {
45
+ // 后台触发一次预取
46
+ if (!mediaConfigLoadPromise) { try { void fetchMediaConfigWithCache(); } catch (e) {} }
47
+
48
+ if (mediaConfigCache && mediaConfigCache[nodeName]) {
49
+ return { nodeName, config: mediaConfigCache[nodeName] };
50
+ }
51
+ return null;
52
+ }
53
+
54
+ // 从媒体配置中提取此节点的输入键(过滤 disable_comfyagent)
55
+ export function getMediaInputKeys(mediaNodeConfig) {
56
+ if (!mediaNodeConfig || !mediaNodeConfig.config || !mediaNodeConfig.config.inputs) return [];
57
+ const inputs = mediaNodeConfig.config.inputs;
58
+ const keys = [];
59
+ for (const key of Object.keys(inputs)) {
60
+ const cfg = inputs[key];
61
+ if (cfg && !cfg.disable_comfyagent) keys.push(key);
62
+ }
63
+ return keys;
64
+ }
65
+
66
+
67
+ export async function computeIsMediaNode(nodeName) {
68
+ if (mediaNodeList.includes(nodeName)) {
69
+ return true;
70
+ }
71
+
72
+ // 2. 检查media_load_nodes的keys
73
+ const config = await fetchMediaConfigWithCache();
74
+ if (config) {
75
+ if (config.hasOwnProperty(nodeName)) {
76
+ return true;
77
+ }
78
+ }
79
+
80
+ return false;
81
+ }
82
+
83
+ // 启动时后台预取(不阻塞后续逻辑)
84
+ try { void fetchMediaConfigWithCache(); } catch (e) {}
85
+
86
+ // ==================== 媒体 Widget 处理函数 ====================
87
+
88
+ /**
89
+ * 查找单个媒体 widget(用于 media_widget)
90
+ */
91
+ export function findMediaWidget(nodeWidgets, apiInputKeys) {
92
+ // 优先使用 API 配置的 keys
93
+ if (apiInputKeys && apiInputKeys.length > 0) {
94
+ for (const key of apiInputKeys) {
95
+ const w = nodeWidgets.find(x => x.name === key);
96
+ if (w) return w;
97
+ }
98
+ }
99
+ // 回退到常见媒体 widget 名称
100
+ return nodeWidgets.find(w => possibleMediaWidgetNames.includes(w.name)) || null;
101
+ }
102
+
103
+ /**
104
+ * 查找所有媒体 widget(用于 va_widgets)
105
+ */
106
+ export function findMediaWidgets(nodeWidgets, apiInputKeys) {
107
+ const widgets = [];
108
+
109
+ // 优先使用 API 配置的 keys
110
+ if (apiInputKeys && apiInputKeys.length > 0) {
111
+ for (const key of apiInputKeys) {
112
+ const w = nodeWidgets.find(x => x.name === key);
113
+ if (w) widgets.push(w);
114
+ }
115
+ }
116
+
117
+ // 如果 API 配置没找到,使用回退逻辑
118
+ if (widgets.length === 0) {
119
+ for (const widgetName of possibleMediaWidgetNames) {
120
+ const w = nodeWidgets.find(x => x.name === widgetName);
121
+ if (w) widgets.push(w);
122
+ }
123
+ }
124
+
125
+ return widgets;
126
+ }
127
+
128
+ /**
129
+ * 添加新文件到列表并更新相关数据
130
+ */
131
+ export function addNewFileToList(url, image_list, urlToNameMap, nameToItemMap) {
132
+ const fileName = url.split('/').pop();
133
+ const newItem = { name: fileName, url: url };
134
+ image_list.push(newItem);
135
+ urlToNameMap.set(url, fileName);
136
+ nameToItemMap.set(fileName, newItem);
137
+ return fileName;
138
+ }
139
+
140
+ /**
141
+ * 更新所有相关 widget 的选项列表
142
+ */
143
+ export function updateWidgetsOptions(widgets, image_list) {
144
+ const names = image_list.map(item => item.name);
145
+ widgets.forEach(widget => {
146
+ if (widget && widget.options) {
147
+ widget.options.values = names;
148
+ }
149
+ });
150
+ }
151
+
152
+ /**
153
+ * 处理新上传的文件:添加到列表并更新所有 widget
154
+ */
155
+ export function handleNewUploadedFile(url, image_list, urlToNameMap, nameToItemMap, image_name_widget, actualMediaWidget) {
156
+ const fileName = addNewFileToList(url, image_list, urlToNameMap, nameToItemMap);
157
+
158
+ // 更新所有相关 widget 的选项列表
159
+ const widgetsToUpdate = [image_name_widget, actualMediaWidget].filter(Boolean);
160
+ updateWidgetsOptions(widgetsToUpdate, image_list);
161
+
162
+ return fileName;
163
+ }
164
+
165
+ /**
166
+ * 从输入中提取 URL(支持 string 和 array 格式)
167
+ */
168
+ export function extractUrlFromInput(input) {
169
+ if (typeof input === 'string') {
170
+ return input;
171
+ } else if (Array.isArray(input) && input.length > 0) {
172
+ return typeof input[0] === 'string' ? input[0] : input[0];
173
+ }
174
+ return null;
175
+ }
176
+
177
+ /**
178
+ * 初始化 Map 映射
179
+ */
180
+ export function initMaps(image_list) {
181
+ const urlToNameMap = new Map();
182
+ const nameToItemMap = new Map();
183
+ image_list.forEach(item => {
184
+ urlToNameMap.set(item.url, item.name);
185
+ nameToItemMap.set(item.name, item);
186
+ });
187
+ return { urlToNameMap, nameToItemMap };
188
+ }
189
+
190
+ /**
191
+ * 创建 image_name_widget 的 callback(用于 va_widgets)
192
+ */
193
+ export function createImageNameWidgetCallbackForVaWidgets(nameToItemMap, va_widgets, isBatchUpdating) {
194
+ return function(e) {
195
+ const item = nameToItemMap.get(e);
196
+ if (item) {
197
+ const image_url = decodeURIComponent(item.url);
198
+ isBatchUpdating.value = true;
199
+ va_widgets.forEach(va_widget => {
200
+ va_widget.value = image_url;
201
+ if (va_widget.callback) {
202
+ va_widget.callback(image_url);
203
+ }
204
+ });
205
+ isBatchUpdating.value = false;
206
+ }
207
+ };
208
+ }
209
+
210
+ /**
211
+ * 创建 image_name_widget 的 callback(用于 media_widget)
212
+ */
213
+ export function createImageNameWidgetCallbackForMediaWidget(nameToItemMap, media_widget) {
214
+ return function(e) {
215
+ const item = nameToItemMap.get(e);
216
+ if (item) {
217
+ const image_url = decodeURIComponent(item.url);
218
+ media_widget.value = image_url;
219
+ if (media_widget.callback) {
220
+ media_widget.callback(image_url);
221
+ }
222
+ }
223
+ };
224
+ }
225
+
226
+ /**
227
+ * 创建 value setter(用于 va_widget)
228
+ */
229
+ export function createVaWidgetValueSetter(va_widget, urlToNameMap, nameToItemMap, image_list, image_name_widget, actualMediaWidget, isBatchUpdating) {
230
+ let _value = va_widget.value;
231
+
232
+ return {
233
+ get: () => _value,
234
+ set: function(newValue) {
235
+ const oldValue = _value;
236
+ _value = newValue;
237
+ console.log(`[hookLoadMedia] va_widget.value 被设置, widget.name=${va_widget.name}, oldValue=`, oldValue, ', newValue=', newValue);
238
+
239
+ // 批量更新时跳过监听逻辑
240
+ if (isBatchUpdating.value) {
241
+ console.log(`[hookLoadMedia] 批量更新中,跳过处理, widget.name=${va_widget.name}`);
242
+ return;
243
+ }
244
+
245
+ // 如果值没有变化,不需要处理
246
+ if (oldValue === newValue) {
247
+ console.log(`[hookLoadMedia] 值未变化,跳过处理, widget.name=${va_widget.name}`);
248
+ return;
249
+ }
250
+
251
+ // 使用 Map 快速查找(O(1))
252
+ const name = urlToNameMap.get(newValue);
253
+ if (name && image_name_widget) {
254
+ console.log(`[hookLoadMedia] 找到匹配的name, widget.name=${va_widget.name}, name=`, name);
255
+ image_name_widget.value = name;
256
+ } else if (image_name_widget && newValue && typeof newValue === 'string' && newValue.includes('/')) {
257
+ // 如果没找到,可能是新上传的文件,需要添加到列表
258
+ const fileName = newValue.split('/').pop();
259
+ console.log(`[hookLoadMedia] 未找到匹配的name, widget.name=${va_widget.name}, 可能是新文件, fileName=${fileName}`);
260
+
261
+ // 检查是否真的是新文件(URL格式)
262
+ if (newValue.startsWith('http') || newValue.startsWith('/')) {
263
+ handleNewUploadedFile(newValue, image_list, urlToNameMap, nameToItemMap, image_name_widget, actualMediaWidget);
264
+ console.log(`[hookLoadMedia] 新文件已通过value setter添加到列表, 当前列表长度=${image_list.length}`);
265
+ }
266
+
267
+ image_name_widget.value = fileName;
268
+ }
269
+ }
270
+ };
271
+ }
272
+
273
+ /**
274
+ * 创建 va_widget 的 callback
275
+ */
276
+ export function createVaWidgetCallback(va_widget, urlToNameMap, nameToItemMap, image_list, image_name_widget, actualMediaWidget, originalVaCallback) {
277
+ return function(e) {
278
+ console.log(`[hookLoadMedia] va_widget.callback 被触发, widget.name=${va_widget.name}, e=`, e);
279
+
280
+ if (image_name_widget) {
281
+ const url = extractUrlFromInput(e);
282
+ if (url) {
283
+ const name = urlToNameMap.get(url);
284
+ if (name) {
285
+ image_name_widget.value = name;
286
+ } else {
287
+ // 如果没找到,可能是新上传的文件
288
+ if (typeof e === 'string' && !e.startsWith('http') && !e.startsWith('/')) {
289
+ // 不是 URL 格式,直接使用文件名
290
+ image_name_widget.value = e.split('/').pop();
291
+ } else {
292
+ // 是新上传的文件,添加到列表
293
+ const fileName = handleNewUploadedFile(url, image_list, urlToNameMap, nameToItemMap, image_name_widget, actualMediaWidget);
294
+ console.log(`[hookLoadMedia] 检测到新上传的文件, url=${url}, fileName=${fileName}, 当前列表长度=${image_list.length}`);
295
+ image_name_widget.value = fileName;
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ // 调用原始callback
302
+ if (originalVaCallback) {
303
+ console.log(`[hookLoadMedia] 调用原始callback, widget.name=${va_widget.name}`);
304
+ originalVaCallback(e);
305
+ } else {
306
+ console.log(`[hookLoadMedia] 原始callback不存在, widget.name=${va_widget.name}`);
307
+ }
308
+ };
309
+ }
310
+
311
+ /**
312
+ * 创建 media_widget 的 callback
313
+ */
314
+ export function createMediaWidgetCallback(media_widget, urlToNameMap, nameToItemMap, image_list, image_name_widget, actualMediaWidget, originalCallback) {
315
+ return function(e) {
316
+ console.log('media_widget.callback', e);
317
+ if (typeof e == 'string') {
318
+ // 使用 Map 快速查找(O(1))
319
+ const item = e.includes('http') ?
320
+ (urlToNameMap.has(e) ? {url: e, name: urlToNameMap.get(e)} : null) :
321
+ (nameToItemMap ? nameToItemMap.get(e) : null);
322
+
323
+ const image_url = item ? decodeURIComponent(item.url) : e;
324
+
325
+ image_name_widget.value = item ? item.name : e;
326
+ media_widget.value = image_url;
327
+ if (originalCallback) {
328
+ originalCallback([image_url])
329
+ }
330
+ } else {
331
+ // 处理数组格式的输入(如文件上传)
332
+ const url = extractUrlFromInput(e);
333
+ if (url) {
334
+ const existingName = urlToNameMap.get(url);
335
+
336
+ if (existingName) {
337
+ // 如果已经在列表中,直接使用
338
+ image_name_widget.value = existingName;
339
+ media_widget.value = url;
340
+ } else {
341
+ // 如果不在列表中,说明是新上传的文件,需要添加到列表
342
+ const fileName = handleNewUploadedFile(url, image_list, urlToNameMap, nameToItemMap, image_name_widget, actualMediaWidget);
343
+ console.log(`[hookLoadMedia] 检测到新上传的文件(media_widget分支), url=${url}, fileName=${fileName}, 当前列表长度=${image_list.length}`);
344
+ image_name_widget.value = fileName;
345
+ media_widget.value = url;
346
+ }
347
+ }
348
+
349
+ if (originalCallback) {
350
+ originalCallback(e)
351
+ }
352
+ }
353
+ };
354
+ }
355
+
356
+ /**
357
+ * 设置 va_widget 的 value setter
358
+ */
359
+ export function setupVaWidgetValueSetter(va_widget, urlToNameMap, nameToItemMap, image_list, image_name_widget, actualMediaWidget, isBatchUpdating) {
360
+ const existingDescriptor = Object.getOwnPropertyDescriptor(va_widget, 'value');
361
+
362
+ if (existingDescriptor && !existingDescriptor.configurable) {
363
+ return; // 跳过不可配置的属性
364
+ }
365
+
366
+ if (existingDescriptor) {
367
+ delete va_widget.value;
368
+ }
369
+
370
+ const valueSetter = createVaWidgetValueSetter(va_widget, urlToNameMap, nameToItemMap, image_list, image_name_widget, actualMediaWidget, isBatchUpdating);
371
+
372
+ Object.defineProperty(va_widget, 'value', {
373
+ get: valueSetter.get,
374
+ set: valueSetter.set,
375
+ enumerable: true,
376
+ configurable: true
377
+ });
378
+ }
379
+
380
+ /**
381
+ * 处理 va_widgets
382
+ */
383
+ export function setupVaWidgets(node, va_widgets, image_list, urlToNameMap, nameToItemMap, image_name_widget, actualMediaWidget) {
384
+ const isBatchUpdating = { value: false };
385
+
386
+ // 创建 image_name_widget
387
+ if (!image_name_widget) {
388
+ image_name_widget = node.addWidget("combo", "image_name", "",
389
+ createImageNameWidgetCallbackForVaWidgets(nameToItemMap, va_widgets, isBatchUpdating),
390
+ {
391
+ serialize: true,
392
+ values: image_list.map(item => item.name)
393
+ }
394
+ );
395
+ }
396
+
397
+ // 隐藏所有 va_widgets 并设置监听
398
+ va_widgets.forEach(va_widget => {
399
+ hideWidget(node, va_widget.name);
400
+ setupVaWidgetValueSetter(va_widget, urlToNameMap, nameToItemMap, image_list, image_name_widget, actualMediaWidget, isBatchUpdating);
401
+
402
+ // 重写 callback
403
+ const originalVaCallback = va_widget.callback;
404
+ console.log(`[hookLoadMedia] 重写 va_widget.callback, widget.name=${va_widget.name}, 原始callback=${originalVaCallback ? '存在' : '不存在'}`);
405
+ va_widget.callback = createVaWidgetCallback(va_widget, urlToNameMap, nameToItemMap, image_list, image_name_widget, actualMediaWidget, originalVaCallback);
406
+ });
407
+
408
+ return image_name_widget;
409
+ }
410
+
411
+ /**
412
+ * 应用工作流图像设置
413
+ */
414
+ export async function applyWorkflowImageSettings(workflowParams, image_list, media_widget, image_name_widget, currentNodeId, va_widgets, actualMediaWidget) {
415
+ if (workflowParams && workflowParams.nodes) {
416
+ // 先获取配置,然后将 mediaNodeList 和配置的 keys 合并
417
+ const config = await fetchMediaConfigWithCache();
418
+ const allMediaNodeTypes = new Set(mediaNodeList);
419
+ if (config) {
420
+ // 将配置中的 keys 添加到集合中
421
+ for (const key of Object.keys(config)) {
422
+ allMediaNodeTypes.add(key);
423
+ }
424
+ }
425
+
426
+ // 使用同步的 includes 查找匹配的节点(完全避免循环中的异步)
427
+ const imageNode = workflowParams.nodes.find(item =>
428
+ item.id === currentNodeId && allMediaNodeTypes.has(item.type)
429
+ )
430
+
431
+ if (imageNode && imageNode.widgets_values && imageNode.widgets_values[0]) {
432
+ const item = imageNode.widgets_values[0].split('/')
433
+ image_list.push({
434
+ name: item[item.length - 1],
435
+ url: imageNode.widgets_values[0]
436
+ })
437
+
438
+ // 使用 actualMediaWidget 而不是 media_widget
439
+ const targetWidget = actualMediaWidget || media_widget;
440
+
441
+ if (targetWidget) {
442
+ targetWidget.value = imageNode.widgets_values[0]
443
+
444
+ if (targetWidget.options) {
445
+ targetWidget.options.values = image_list.map(item => item.url)
446
+ }
447
+
448
+ if (image_name_widget) {
449
+ image_name_widget.options.values = image_list.map(item => item.name)
450
+ }
451
+
452
+ // 触发 callback
453
+ if (targetWidget.callback) {
454
+ targetWidget.callback(imageNode.widgets_values[0])
455
+ }
456
+
457
+ // 如果是 va_widgets 的情况,需要同步更新所有 va_widgets
458
+ if (va_widgets && va_widgets.length > 0) {
459
+ va_widgets.forEach(va_widget => {
460
+ if (va_widget !== targetWidget) {
461
+ va_widget.value = imageNode.widgets_values[0];
462
+ if (va_widget.callback) {
463
+ va_widget.callback(imageNode.widgets_values[0]);
464
+ }
465
+ }
466
+ });
467
+ }
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ /**
474
+ * 获取文件列表数据
475
+ */
476
+ export async function fetchImageList(apiHost, nodeName) {
477
+ const res = await fetch(`${apiHost}/special/community/commit_input_resource?${
478
+ new URLSearchParams({
479
+ ext: computeExt(nodeName),
480
+ current: 1,
481
+ page_size: 100
482
+ }).toString()
483
+ }`, {
484
+ method: 'GET',
485
+ headers: {
486
+ 'Content-Type': 'application/json',
487
+ 'Authorization': `Bearer ${getCookie('bizy_token')}`
488
+ }
489
+ });
490
+
491
+ const {data} = await res.json();
492
+ const list = (data && data.data && data.data.data && data.data.data.list) || [];
493
+ return list.filter(item => item.name).map(item => ({
494
+ url: item.url,
495
+ id: item.id,
496
+ name: item.name
497
+ }));
498
+ }