bizydraft 0.2.78.dev20251030100819__py3-none-any.whl → 0.2.82.dev20251209023307__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.
@@ -62,7 +62,7 @@ function replaceClipspaceUrl(urlString) {
62
62
  });
63
63
 
64
64
  const originalSetAttribute = HTMLImageElement.prototype.setAttribute;
65
- HTMLImageElement.prototype.setAttribute = function(name, value) {
65
+ HTMLImageElement.prototype.setAttribute = function (name, value) {
66
66
  if (name === 'src') {
67
67
  const modifiedValue = replaceClipspaceUrl(value);
68
68
  return originalSetAttribute.call(this, name, modifiedValue);
@@ -75,11 +75,11 @@ function replaceClipspaceUrl(urlString) {
75
75
  // 拦截上传响应,保存映射并篡改返回值
76
76
  // ═══════════════════════════════════════════════════════════════════════════
77
77
  const originalFetchApi = api.fetchApi;
78
- api.fetchApi = async function(url, options) {
78
+ api.fetchApi = async function (url, options) {
79
79
  const response = await originalFetchApi.call(this, url, options);
80
80
 
81
81
  const isUploadApi = url === '/upload/image' || url === '/upload/mask'
82
- || url === '/api/upload/image' || url === '/api/upload/mask';
82
+ || url === '/api/upload/image' || url === '/api/upload/mask';
83
83
 
84
84
  if (!isUploadApi || !response.ok) {
85
85
  return response;
@@ -89,7 +89,7 @@ api.fetchApi = async function(url, options) {
89
89
 
90
90
  // 检查是否是 OSS 上传响应
91
91
  const isOssUpload = data.subfolder?.includes('http://') || data.subfolder?.includes('https://')
92
- || data.name?.startsWith('http://') || data.name?.startsWith('https://');
92
+ || data.name?.startsWith('http://') || data.name?.startsWith('https://');
93
93
 
94
94
  if (!isOssUpload) return response;
95
95
 
@@ -118,7 +118,7 @@ api.fetchApi = async function(url, options) {
118
118
  finalUrl = ossUrl;
119
119
 
120
120
  [`clipspace-mask-${baseId}.png`, `clipspace-paint-${baseId}.png`,
121
- `clipspace-painted-${baseId}.png`, `clipspace-painted-masked-${baseId}.png`
121
+ `clipspace-painted-${baseId}.png`, `clipspace-painted-masked-${baseId}.png`
122
122
  ].forEach(v => window.CLIPSPACE_TO_OSS_MAP[v] = ossUrl);
123
123
 
124
124
  } else {
@@ -204,16 +204,29 @@ function convertClipspacePathsInPrompt(prompt) {
204
204
 
205
205
  for (const [inputKey, inputValue] of Object.entries(node.inputs)) {
206
206
  if (typeof inputValue === 'string' && inputValue.includes('clipspace')) {
207
- const match = inputValue.match(/clipspace\/([\w-]+\.(?:png|jpg|jpeg|webp|gif))/i);
208
- if (match) {
209
- const filename = match[1];
210
- const ossUrl = findOssUrl(filename);
207
+ // 1) 特殊情况:clipspace/https://... clipspace/http://...
208
+ const ossUrlMatch = inputValue.match(/clipspace\/(https?:\/\/[^\s]+)/i);
209
+ if (ossUrlMatch) {
210
+ // 移除可能的 [input] 或 [output] 后缀
211
+ let cleanUrl = ossUrlMatch[1].replace(/\s*\[(input|output)\]$/i, '');
212
+ node.inputs[inputKey] = cleanUrl;
213
+
214
+ if (inputKey === 'image' && node.inputs['image_name']) {
215
+ node.inputs['image_name'] = cleanUrl.split('/').pop();
216
+ }
217
+ } else {
218
+ // 2) 常规情况:clipspace/xxx.png
219
+ const match = inputValue.match(/clipspace\/([\w-]+\.(?:png|jpg|jpeg|webp|gif))/i);
220
+ if (match) {
221
+ const filename = match[1];
222
+ const ossUrl = findOssUrl(filename);
211
223
 
212
- if (ossUrl) {
213
- node.inputs[inputKey] = ossUrl;
224
+ if (ossUrl) {
225
+ node.inputs[inputKey] = ossUrl;
214
226
 
215
- if (inputKey === 'image' && node.inputs['image_name']) {
216
- node.inputs['image_name'] = ossUrl.split('/').pop();
227
+ if (inputKey === 'image' && node.inputs['image_name']) {
228
+ node.inputs['image_name'] = ossUrl.split('/').pop();
229
+ }
217
230
  }
218
231
  }
219
232
  }
@@ -232,7 +245,7 @@ function interceptPasteFromClipspace() {
232
245
  if (!ComfyApp || !ComfyApp.pasteFromClipspace) return;
233
246
 
234
247
  const originalPasteFromClipspace = ComfyApp.pasteFromClipspace;
235
- ComfyApp.pasteFromClipspace = function(node) {
248
+ ComfyApp.pasteFromClipspace = function (node) {
236
249
  // 调用原始函数
237
250
  originalPasteFromClipspace.call(this, node);
238
251
 
@@ -244,14 +257,23 @@ function interceptPasteFromClipspace() {
244
257
 
245
258
  // 1) 如果是 clipspace 路径格式,替换为 OSS URL
246
259
  if (value.includes('clipspace/')) {
247
- // 提取文件名
248
- const match = value.match(/clipspace\/([\w-]+\.(?:png|jpg|jpeg|webp|gif))(\s\[(input|output)\])?/i);
249
- if (match) {
250
- const filename = match[1];
251
- const ossUrl = findOssUrl(filename);
252
-
253
- if (ossUrl) {
254
- imageWidget.value = ossUrl;
260
+ // 1.1) 特殊情况:clipspace/https://... 或 clipspace/http://...
261
+ // 这种情况是 OSS URL 被错误地加了 clipspace/ 前缀,直接移除前缀
262
+ const ossUrlMatch = value.match(/clipspace\/(https?:\/\/[^\s]+)/i);
263
+ if (ossUrlMatch) {
264
+ // 移除可能的 [input] 或 [output] 后缀
265
+ let cleanUrl = ossUrlMatch[1].replace(/\s*\[(input|output)\]$/i, '');
266
+ imageWidget.value = cleanUrl;
267
+ } else {
268
+ // 1.2) 常规情况:clipspace/xxx.png,提取文件名并查找映射
269
+ const match = value.match(/clipspace\/([\w-]+\.(?:png|jpg|jpeg|webp|gif))(\s\[(input|output)\])?/i);
270
+ if (match) {
271
+ const filename = match[1];
272
+ const ossUrl = findOssUrl(filename);
273
+
274
+ if (ossUrl) {
275
+ imageWidget.value = ossUrl;
276
+ }
255
277
  }
256
278
  }
257
279
  }
@@ -273,22 +295,31 @@ app.registerExtension({
273
295
  async setup() {
274
296
  const originalGraphToPrompt = app.graphToPrompt;
275
297
 
276
- // 在构建 Prompt 之前,先清理所有 widget 的值,去掉多余的后缀
298
+ // 在构建 Prompt 之前,先清理所有 widget 的值,去掉多余的后缀和错误的 clipspace 前缀
277
299
  function sanitizeGraphWidgets(graph) {
278
300
  const nodes = graph?._nodes || [];
279
301
  for (const node of nodes) {
280
302
  if (!node?.widgets) continue;
281
303
  for (const widget of node.widgets) {
282
304
  if (typeof widget?.value === 'string') {
283
- widget.value = stripTypeSuffix(widget.value);
305
+ let value = widget.value;
306
+ // 先处理 clipspace/https://... 格式
307
+ if (value.includes('clipspace/')) {
308
+ const ossUrlMatch = value.match(/clipspace\/(https?:\/\/[^\s]+)/i);
309
+ if (ossUrlMatch) {
310
+ value = ossUrlMatch[1];
311
+ }
312
+ }
313
+ // 再移除类型后缀
314
+ widget.value = stripTypeSuffix(value);
284
315
  }
285
316
  }
286
317
  }
287
318
  }
288
319
 
289
- app.graphToPrompt = async function(...args) {
320
+ app.graphToPrompt = async function (...args) {
290
321
  // 预清理,避免 workflow.widgets_values 和 prompt 输入里包含 [input]/[output]
291
- try { sanitizeGraphWidgets(app.graph); } catch (e) {}
322
+ try { sanitizeGraphWidgets(app.graph); } catch (e) { }
292
323
 
293
324
  const result = await originalGraphToPrompt.apply(this, args);
294
325
 
@@ -1,7 +1,8 @@
1
1
  import { app } from "../../scripts/app.js";
2
2
  import { $el } from "../../scripts/ui.js";
3
3
  const styleMenus = `
4
- .p-panel-content-container{
4
+ /* 隐藏Panel内容容器,但排除选择工具箱 */
5
+ .p-panel:not(.selection-toolbox) .p-panel-content-container{
5
6
  display: none;
6
7
  }
7
8
  // .side-tool-bar-container.small-sidebar{
@@ -28,10 +29,11 @@ const styleMenus = `
28
29
  #comfyui-body-bottom{
29
30
  display: none;
30
31
  }
31
-
32
+ /* 隐藏左侧的工作流按钮 */
32
33
  .p-button.p-component.p-button-icon-only.p-button-text.workflows-tab-button.side-bar-button.p-button-secondary{
33
34
  display: none;
34
35
  }
36
+ /* 隐藏左侧的输入输出按钮 */
35
37
  .p-button.p-component.p-button-icon-only.p-button-text.mtb-inputs-outputs-tab-button.side-bar-button.p-button-secondary{
36
38
  display: none;
37
39
  }
@@ -56,9 +58,46 @@ const styleMenus = `
56
58
  .side-tool-bar-container.small-sidebar .side-bar-button-label{
57
59
  display: none;
58
60
  }
59
- body .comfy-img-preview video{
60
- width: 100%;
61
- height: 100%;
61
+ /* 隐藏整个comfy-menu-button-wrapper元素 */
62
+ .comfy-menu-button-wrapper{
63
+ display: none;
64
+ }
65
+ /* 隐藏帮助中心按钮 */
66
+ .comfy-help-center-btn{
67
+ display: none;
68
+ }
69
+ /* 隐藏底部面板(Console)按钮 */
70
+ button[aria-label="底部面板"]{
71
+ display: none;
72
+ }
73
+ /* 隐藏键盘快捷键按钮 */
74
+ button[aria-label^="键盘快捷键"]{
75
+ display: none;
76
+ }
77
+ /* 隐藏右下角按钮组(包含鼠标指针、适应视图、缩放控制、小地图、隐藏链接等按钮) */
78
+ .p-buttongroup.absolute.right-0.bottom-0{
79
+ display: none;
80
+ }
81
+ .pointer-events-auto.relative.w-full.h-10.bg-gradient-to-r.from-blue-600.to-blue-700.flex.items-center.justify-center.px-4 {
82
+ display: none;
83
+ }
84
+ .p-splitterpanel.p-splitterpanel-nested.flex.flex-col .ml-1.flex.pt-1{
85
+ display: none;
86
+ }
87
+ .p-button.p-component.p-button-text.size-8.bg-primary-background.text-white.p-0{
88
+ display: none;
89
+ }
90
+ .p-button.p-component.p-button-secondary.p-button-text.h-8.w-8.px-0{
91
+ display: none;
92
+ }
93
+
94
+ /* 隐藏左侧的资产按钮 */
95
+ .p-button.p-component.p-button-icon-only.p-button-text.assets-tab-button.side-bar-button.p-button-secondary {
96
+ display: none;
97
+ }
98
+ /* 隐藏左侧的模型库按钮 */
99
+ .p-button.p-component.p-button-icon-only.p-button-text.model-library-tab-button.side-bar-button.p-button-secondary{
100
+ display: none;
62
101
  }
63
102
  `;
64
103
  app.registerExtension({
@@ -69,13 +108,16 @@ app.registerExtension({
69
108
  parent: document.head,
70
109
  });
71
110
  const getCloseBtn = () => {
72
- let temp = null
73
- document.querySelectorAll('h3').forEach(e => {
74
- if (e.innerHTML == "<span>从模板开始</span>") {
75
- temp = e.parentNode.parentNode.parentNode.querySelector('.p-dialog-close-button')
76
- }
77
- })
78
- return temp
111
+ // let temp = null
112
+ // document.querySelectorAll('h2').forEach(e => {
113
+ // if (e.innerHTML == "<span>模板</span>") {
114
+ // const dialogContent = e.closest('.p-dialog-content')
115
+ // if (dialogContent) {
116
+ // temp = dialogContent.querySelector('i.pi.pi-times.text-sm')
117
+ // }
118
+ // }
119
+ // })
120
+ return document.querySelector('i.pi.pi-times.text-sm')
79
121
  }
80
122
  const getFengrossmentBtn = () => {
81
123
  let temp = null
@@ -1,5 +1,6 @@
1
1
  // 媒体节点配置获取与工具函数(与 hookLoad/model.js 结构一致,面向 media_load_nodes)
2
2
  import { fetchMediaConfig } from './configLoader.js'
3
+ import { getCookie, computeExt, hideWidget } from '../tool.js'
3
4
 
4
5
  // 动态配置缓存(仅缓存媒体部分)
5
6
  let mediaConfigCache = null;
@@ -81,3 +82,417 @@ export async function computeIsMediaNode(nodeName) {
81
82
 
82
83
  // 启动时后台预取(不阻塞后续逻辑)
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
+ }