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.
- bizydraft/env.py +3 -0
- bizydraft/hijack_nodes.py +62 -23
- bizydraft/hijack_routes.py +8 -2
- bizydraft/oss_utils.py +223 -2
- bizydraft/patch_handlers.py +197 -8
- bizydraft/static/js/aiAppHandler.js +11 -12
- bizydraft/static/js/clipspaceToOss.js +316 -0
- bizydraft/static/js/disableComfyWebSocket.js +65 -0
- bizydraft/static/js/freezeModeHandler.js +23 -0
- bizydraft/static/js/handleStyle.js +73 -0
- bizydraft/static/js/hookLoad/configLoader.js +68 -0
- bizydraft/static/js/hookLoad/media.js +498 -0
- bizydraft/static/js/hookLoad/model.js +278 -0
- bizydraft/static/js/hookLoadMedia.js +159 -0
- bizydraft/static/js/hookLoadModel.js +159 -198
- bizydraft/static/js/main.js +3 -1
- bizydraft/static/js/postEvent.js +292 -168
- bizydraft/static/js/tool.js +4 -1
- bizydraft/static/js/workflow_io.js +193 -0
- {bizydraft-0.2.31.dist-info → bizydraft-0.2.78.dev20251117024007.dist-info}/METADATA +1 -1
- bizydraft-0.2.78.dev20251117024007.dist-info/RECORD +34 -0
- bizydraft/static/js/hookLoadImage.js +0 -173
- bizydraft-0.2.31.dist-info/RECORD +0 -28
- {bizydraft-0.2.31.dist-info → bizydraft-0.2.78.dev20251117024007.dist-info}/WHEEL +0 -0
- {bizydraft-0.2.31.dist-info → bizydraft-0.2.78.dev20251117024007.dist-info}/top_level.txt +0 -0
bizydraft/patch_handlers.py
CHANGED
|
@@ -3,10 +3,12 @@ import math
|
|
|
3
3
|
import mimetypes
|
|
4
4
|
import os
|
|
5
5
|
import uuid
|
|
6
|
+
from io import BytesIO
|
|
6
7
|
from urllib.parse import unquote
|
|
7
8
|
|
|
8
9
|
from aiohttp import ClientSession, ClientTimeout, web
|
|
9
10
|
from loguru import logger
|
|
11
|
+
from PIL import Image
|
|
10
12
|
|
|
11
13
|
try:
|
|
12
14
|
import execution
|
|
@@ -29,6 +31,7 @@ BIZYDRAFT_CHUNK_SIZE = int(os.getenv("BIZYDRAFT_CHUNK_SIZE", 1024 * 16)) # 16KB
|
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
async def view_image(request):
|
|
34
|
+
|
|
32
35
|
logger.debug(f"Received request for /view with query: {request.rel_url.query}")
|
|
33
36
|
if "filename" not in request.rel_url.query:
|
|
34
37
|
logger.warning("'filename' not provided in query string, returning 404")
|
|
@@ -36,6 +39,8 @@ async def view_image(request):
|
|
|
36
39
|
|
|
37
40
|
filename = request.rel_url.query["filename"]
|
|
38
41
|
subfolder = request.rel_url.query.get("subfolder", "")
|
|
42
|
+
channel = request.rel_url.query.get("channel", "rgba")
|
|
43
|
+
preview = request.rel_url.query.get("preview", None)
|
|
39
44
|
|
|
40
45
|
http_prefix_options = ("http:", "https:")
|
|
41
46
|
|
|
@@ -51,20 +56,35 @@ async def view_image(request):
|
|
|
51
56
|
if "http" in subfolder:
|
|
52
57
|
subfolder = subfolder[subfolder.find("http") :]
|
|
53
58
|
subfolder = unquote(subfolder)
|
|
54
|
-
|
|
59
|
+
if "https:/" in subfolder and not subfolder.startswith("https://"):
|
|
60
|
+
subfolder = subfolder.replace("https:/", "https://", 1)
|
|
61
|
+
if "http:/" in subfolder and not subfolder.startswith("http://"):
|
|
62
|
+
subfolder = subfolder.replace("http:/", "http://", 1)
|
|
63
|
+
|
|
64
|
+
# 构建完整URL
|
|
65
|
+
full_url = (
|
|
55
66
|
f"{subfolder}/{filename}"
|
|
56
67
|
if not filename.startswith(http_prefix_options)
|
|
57
68
|
else filename
|
|
58
|
-
)
|
|
69
|
+
)
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
# 获取原始文件名用于响应头
|
|
72
|
+
original_filename = filename.split("/")[-1] if "/" in filename else filename
|
|
73
|
+
|
|
74
|
+
content_type, _ = mimetypes.guess_type(full_url)
|
|
63
75
|
|
|
64
76
|
timeout = ClientTimeout(total=BIZYDRAFT_REQUEST_TIMEOUT)
|
|
65
77
|
async with ClientSession(timeout=timeout) as session:
|
|
66
|
-
async with session.get(
|
|
78
|
+
async with session.get(full_url) as resp:
|
|
67
79
|
resp.raise_for_status()
|
|
80
|
+
|
|
81
|
+
# 优先使用服务器返回的Content-Type,如果无法获取则使用猜测的类型
|
|
82
|
+
final_content_type = (
|
|
83
|
+
resp.headers.get("Content-Type")
|
|
84
|
+
or content_type
|
|
85
|
+
or "application/octet-stream"
|
|
86
|
+
)
|
|
87
|
+
|
|
68
88
|
content_length = int(resp.headers.get("Content-Length", 0))
|
|
69
89
|
if content_length > BIZYDRAFT_MAX_FILE_SIZE:
|
|
70
90
|
logger.warning(
|
|
@@ -75,9 +95,99 @@ async def view_image(request):
|
|
|
75
95
|
text=f"File size exceeds limit ({human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)})",
|
|
76
96
|
)
|
|
77
97
|
|
|
98
|
+
# 检查是否需要图像处理(preview或channel参数)
|
|
99
|
+
is_image = final_content_type and final_content_type.startswith(
|
|
100
|
+
"image/"
|
|
101
|
+
)
|
|
102
|
+
needs_processing = is_image and (
|
|
103
|
+
preview is not None or channel != "rgba"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if needs_processing:
|
|
107
|
+
logger.debug(f"Image processing requested: {channel=}, {preview=}")
|
|
108
|
+
# 下载完整图像到内存
|
|
109
|
+
image_data = await resp.read()
|
|
110
|
+
|
|
111
|
+
# 检查实际大小
|
|
112
|
+
if len(image_data) > BIZYDRAFT_MAX_FILE_SIZE:
|
|
113
|
+
return web.Response(
|
|
114
|
+
status=413,
|
|
115
|
+
text=f"File size exceeds limit ({human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)})",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# 使用PIL处理图像
|
|
119
|
+
with Image.open(BytesIO(image_data)) as img:
|
|
120
|
+
# 处理preview参数
|
|
121
|
+
if preview is not None:
|
|
122
|
+
preview_info = preview.split(";")
|
|
123
|
+
image_format = preview_info[0]
|
|
124
|
+
if image_format not in ["webp", "jpeg"] or "a" in channel:
|
|
125
|
+
image_format = "webp"
|
|
126
|
+
quality = 90
|
|
127
|
+
if preview_info[-1].isdigit():
|
|
128
|
+
quality = int(preview_info[-1])
|
|
129
|
+
|
|
130
|
+
buffer = BytesIO()
|
|
131
|
+
if image_format in ["jpeg"] or channel == "rgb":
|
|
132
|
+
img = img.convert("RGB")
|
|
133
|
+
img.save(buffer, format=image_format, quality=quality)
|
|
134
|
+
buffer.seek(0)
|
|
135
|
+
|
|
136
|
+
return web.Response(
|
|
137
|
+
body=buffer.read(),
|
|
138
|
+
content_type=f"image/{image_format}",
|
|
139
|
+
headers={
|
|
140
|
+
"Content-Disposition": f'filename="{original_filename}"'
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# 处理channel参数
|
|
145
|
+
if channel == "rgb":
|
|
146
|
+
logger.debug("Converting image to RGB (removing alpha)")
|
|
147
|
+
if img.mode == "RGBA":
|
|
148
|
+
r, g, b, a = img.split()
|
|
149
|
+
new_img = Image.merge("RGB", (r, g, b))
|
|
150
|
+
else:
|
|
151
|
+
new_img = img.convert("RGB")
|
|
152
|
+
|
|
153
|
+
buffer = BytesIO()
|
|
154
|
+
new_img.save(buffer, format="PNG")
|
|
155
|
+
buffer.seek(0)
|
|
156
|
+
|
|
157
|
+
return web.Response(
|
|
158
|
+
body=buffer.read(),
|
|
159
|
+
content_type="image/png",
|
|
160
|
+
headers={
|
|
161
|
+
"Content-Disposition": f'filename="{original_filename}"'
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
elif channel == "a":
|
|
166
|
+
logger.debug("Extracting alpha channel only")
|
|
167
|
+
if img.mode == "RGBA":
|
|
168
|
+
_, _, _, a = img.split()
|
|
169
|
+
else:
|
|
170
|
+
a = Image.new("L", img.size, 255)
|
|
171
|
+
|
|
172
|
+
# 创建alpha通道图像
|
|
173
|
+
alpha_img = Image.new("RGBA", img.size)
|
|
174
|
+
alpha_img.putalpha(a)
|
|
175
|
+
alpha_buffer = BytesIO()
|
|
176
|
+
alpha_img.save(alpha_buffer, format="PNG")
|
|
177
|
+
alpha_buffer.seek(0)
|
|
178
|
+
|
|
179
|
+
return web.Response(
|
|
180
|
+
body=alpha_buffer.read(),
|
|
181
|
+
content_type="image/png",
|
|
182
|
+
headers={
|
|
183
|
+
"Content-Disposition": f'filename="{original_filename}"'
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# 默认流式传输(无需处理或非图像文件)
|
|
78
188
|
headers = {
|
|
79
|
-
"Content-Disposition": f'attachment; filename="{
|
|
80
|
-
"Content-Type":
|
|
189
|
+
"Content-Disposition": f'attachment; filename="{original_filename}"',
|
|
190
|
+
"Content-Type": final_content_type,
|
|
81
191
|
}
|
|
82
192
|
|
|
83
193
|
proxy_response = web.StreamResponse(headers=headers)
|
|
@@ -102,6 +212,7 @@ async def view_image(request):
|
|
|
102
212
|
text=f"Request timed out (max {BIZYDRAFT_REQUEST_TIMEOUT//60} minutes)",
|
|
103
213
|
)
|
|
104
214
|
except Exception as e:
|
|
215
|
+
logger.error(f"Error in view_image: {str(e)}", exc_info=True)
|
|
105
216
|
return web.Response(
|
|
106
217
|
status=502, text=f"Failed to fetch remote resource: {str(e)}"
|
|
107
218
|
)
|
|
@@ -117,6 +228,84 @@ def human_readable_size(size_bytes):
|
|
|
117
228
|
return f"{s} {size_name[i]}"
|
|
118
229
|
|
|
119
230
|
|
|
231
|
+
async def view_video(request):
|
|
232
|
+
"""处理VHS插件的viewvideo接口,支持从OSS URL加载视频"""
|
|
233
|
+
logger.debug(
|
|
234
|
+
f"Received request for /vhs/viewvideo with query: {request.rel_url.query}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if "filename" not in request.rel_url.query:
|
|
238
|
+
logger.warning("'filename' not provided in query string, returning 404")
|
|
239
|
+
return web.Response(status=404, text="'filename' not provided in query string")
|
|
240
|
+
|
|
241
|
+
# VHS插件的filename参数本身就是完整的URL(可能是URL编码的)
|
|
242
|
+
filename = unquote(request.rel_url.query["filename"])
|
|
243
|
+
|
|
244
|
+
http_prefix_options = ("http:", "https:")
|
|
245
|
+
|
|
246
|
+
if not filename.startswith(http_prefix_options):
|
|
247
|
+
logger.warning(f"Invalid filename format: {filename=}, only URLs are supported")
|
|
248
|
+
return web.Response(
|
|
249
|
+
status=400, text="Invalid filename format(only url supported)"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
content_type, _ = mimetypes.guess_type(filename)
|
|
254
|
+
|
|
255
|
+
timeout = ClientTimeout(total=BIZYDRAFT_REQUEST_TIMEOUT)
|
|
256
|
+
async with ClientSession(timeout=timeout) as session:
|
|
257
|
+
async with session.get(filename) as resp:
|
|
258
|
+
resp.raise_for_status()
|
|
259
|
+
|
|
260
|
+
# 优先使用服务器返回的Content-Type
|
|
261
|
+
final_content_type = (
|
|
262
|
+
resp.headers.get("Content-Type")
|
|
263
|
+
or content_type
|
|
264
|
+
or "application/octet-stream"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
content_length = int(resp.headers.get("Content-Length", 0))
|
|
268
|
+
if content_length > BIZYDRAFT_MAX_FILE_SIZE:
|
|
269
|
+
logger.warning(
|
|
270
|
+
f"File size {human_readable_size(content_length)} exceeds limit {human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)}"
|
|
271
|
+
)
|
|
272
|
+
return web.Response(
|
|
273
|
+
status=413,
|
|
274
|
+
text=f"File size exceeds limit ({human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)})",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
headers = {
|
|
278
|
+
"Content-Disposition": f'attachment; filename="{uuid.uuid4()}"',
|
|
279
|
+
"Content-Type": final_content_type,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
proxy_response = web.StreamResponse(headers=headers)
|
|
283
|
+
await proxy_response.prepare(request)
|
|
284
|
+
|
|
285
|
+
total_bytes = 0
|
|
286
|
+
async for chunk in resp.content.iter_chunked(BIZYDRAFT_CHUNK_SIZE):
|
|
287
|
+
total_bytes += len(chunk)
|
|
288
|
+
if total_bytes > BIZYDRAFT_MAX_FILE_SIZE:
|
|
289
|
+
await proxy_response.write(b"")
|
|
290
|
+
return web.Response(
|
|
291
|
+
status=413,
|
|
292
|
+
text=f"File size exceeds limit during streaming ({human_readable_size(BIZYDRAFT_MAX_FILE_SIZE)})",
|
|
293
|
+
)
|
|
294
|
+
await proxy_response.write(chunk)
|
|
295
|
+
|
|
296
|
+
return proxy_response
|
|
297
|
+
|
|
298
|
+
except asyncio.TimeoutError:
|
|
299
|
+
return web.Response(
|
|
300
|
+
status=504,
|
|
301
|
+
text=f"Request timed out (max {BIZYDRAFT_REQUEST_TIMEOUT//60} minutes)",
|
|
302
|
+
)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
return web.Response(
|
|
305
|
+
status=502, text=f"Failed to fetch remote resource: {str(e)}"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
120
309
|
async def post_prompt(request):
|
|
121
310
|
json_data = await request.json()
|
|
122
311
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { app } from "../../scripts/app.js";
|
|
2
|
-
import { setNodeParams
|
|
2
|
+
import { setNodeParams } from "./nodeParamsFilter.js";
|
|
3
3
|
import { processGraphOutput } from './tool.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { freezeWorkflow, unfreezeWorkflow } from './freezeModeHandler.js';
|
|
5
|
+
import { parseWorkflowIO } from './workflow_io.js';
|
|
6
6
|
// 状态变量
|
|
7
7
|
let selectedInputNodes = [];
|
|
8
8
|
let activeMode = null; // 当前活动的模式: "aiapp" 或 "export"
|
|
@@ -39,14 +39,8 @@ function toggleMode(mode, enable, isworkflow = false) {
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
headers: {
|
|
45
|
-
'Content-Type': 'application/json'
|
|
46
|
-
},
|
|
47
|
-
body: JSON.stringify(formattedData)
|
|
48
|
-
});
|
|
49
|
-
const responseData = await response.json();
|
|
42
|
+
// 使用前端workflow_io模块处理数据
|
|
43
|
+
const responseData = parseWorkflowIO(formattedData);
|
|
50
44
|
|
|
51
45
|
// 保存节点参数信息
|
|
52
46
|
if (responseData && responseData.data && Array.isArray(responseData.data.inputs)) {
|
|
@@ -456,7 +450,7 @@ window.addEventListener('message', function(event) {
|
|
|
456
450
|
{};
|
|
457
451
|
|
|
458
452
|
// 直接使用 widget.value,这是截图中显示蓝色高亮的值
|
|
459
|
-
const widgetValue = widget.value
|
|
453
|
+
const widgetValue = widget.value ?? "";
|
|
460
454
|
// 创建一个新的简化widget对象,只包含我们实际需要的属性
|
|
461
455
|
// 确保只使用可序列化的属性
|
|
462
456
|
let safeWidgetValue = "";
|
|
@@ -464,6 +458,9 @@ window.addEventListener('message', function(event) {
|
|
|
464
458
|
// 测试是否可序列化
|
|
465
459
|
JSON.stringify({value: widgetValue});
|
|
466
460
|
safeWidgetValue = widgetValue;
|
|
461
|
+
if (typeof safeWidgetValue === 'string') {
|
|
462
|
+
safeWidgetValue = safeWidgetValue.replace(/pasted\/http/g, 'http');
|
|
463
|
+
}
|
|
467
464
|
} catch (e) {
|
|
468
465
|
console.error('[aiAppHandler] 值不可序列化:', e);
|
|
469
466
|
// 使用安全的字符串值
|
|
@@ -476,6 +473,7 @@ window.addEventListener('message', function(event) {
|
|
|
476
473
|
type: widget.type || "string",
|
|
477
474
|
options: safeOptions,
|
|
478
475
|
displayName: widget.displayName || widget.label || widget.name || "",
|
|
476
|
+
tooltip: widget.tooltip || "", // 添加 tooltip 字段
|
|
479
477
|
node_title: node.title || "",
|
|
480
478
|
node_type: node.type || "",
|
|
481
479
|
node_comfyClass: node.comfyClass || ""
|
|
@@ -499,6 +497,7 @@ window.addEventListener('message', function(event) {
|
|
|
499
497
|
type: widget.type || "string",
|
|
500
498
|
options: widget.options || {},
|
|
501
499
|
displayName: widget.displayName || "",
|
|
500
|
+
tooltip: widget.tooltip || "", // 添加 tooltip 字段
|
|
502
501
|
node_title: widget.node_title || "",
|
|
503
502
|
node_type: widget.node_type || "",
|
|
504
503
|
node_comfyClass: widget.node_comfyClass || ""
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { app } from "../../scripts/app.js";
|
|
2
|
+
import { api } from "../../scripts/api.js";
|
|
3
|
+
|
|
4
|
+
window.CLIPSPACE_TO_OSS_MAP = window.CLIPSPACE_TO_OSS_MAP || {};
|
|
5
|
+
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
7
|
+
// 工具函数:查找 clipspace 文件名对应的 OSS URL
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
+
function findOssUrl(filename) {
|
|
10
|
+
return window.CLIPSPACE_TO_OSS_MAP[filename]
|
|
11
|
+
|| window.CLIPSPACE_TO_OSS_MAP[`${filename} [input]`]
|
|
12
|
+
|| window.CLIPSPACE_TO_OSS_MAP[`${filename} [output]`];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 去掉末尾的 " [input]" 或 " [output]" 后缀
|
|
16
|
+
function stripTypeSuffix(value) {
|
|
17
|
+
if (!value || typeof value !== 'string') return value;
|
|
18
|
+
return value.replace(/\s\[(input|output)\]$/i, '');
|
|
19
|
+
}
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
// 工具函数:替换 clipspace URL 为 OSS URL
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
+
function replaceClipspaceUrl(urlString) {
|
|
24
|
+
if (!urlString || typeof urlString !== 'string') return urlString;
|
|
25
|
+
if (!urlString.includes('/view?') || !urlString.includes('clipspace')) return urlString;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(urlString, window.location.origin);
|
|
29
|
+
const filename = url.searchParams.get('filename');
|
|
30
|
+
const subfolder = url.searchParams.get('subfolder');
|
|
31
|
+
|
|
32
|
+
if (subfolder === 'clipspace' && filename) {
|
|
33
|
+
const ossUrl = findOssUrl(filename);
|
|
34
|
+
if (ossUrl) {
|
|
35
|
+
url.searchParams.set('filename', ossUrl);
|
|
36
|
+
url.searchParams.set('subfolder', '');
|
|
37
|
+
return url.pathname + url.search;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('[BizyDraft] Error replacing clipspace URL:', e);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return urlString;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
// 拦截图片加载请求,将 clipspace URL 替换为 OSS URL
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
(function interceptImageLoading() {
|
|
51
|
+
const originalSrcDescriptor = Object.getOwnPropertyDescriptor(Image.prototype, 'src');
|
|
52
|
+
|
|
53
|
+
Object.defineProperty(Image.prototype, 'src', {
|
|
54
|
+
get() {
|
|
55
|
+
return originalSrcDescriptor.get.call(this);
|
|
56
|
+
},
|
|
57
|
+
set(value) {
|
|
58
|
+
const modifiedValue = replaceClipspaceUrl(value);
|
|
59
|
+
originalSrcDescriptor.set.call(this, modifiedValue);
|
|
60
|
+
},
|
|
61
|
+
configurable: true
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const originalSetAttribute = HTMLImageElement.prototype.setAttribute;
|
|
65
|
+
HTMLImageElement.prototype.setAttribute = function(name, value) {
|
|
66
|
+
if (name === 'src') {
|
|
67
|
+
const modifiedValue = replaceClipspaceUrl(value);
|
|
68
|
+
return originalSetAttribute.call(this, name, modifiedValue);
|
|
69
|
+
}
|
|
70
|
+
return originalSetAttribute.call(this, name, value);
|
|
71
|
+
};
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
// 拦截上传响应,保存映射并篡改返回值
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
77
|
+
const originalFetchApi = api.fetchApi;
|
|
78
|
+
api.fetchApi = async function(url, options) {
|
|
79
|
+
const response = await originalFetchApi.call(this, url, options);
|
|
80
|
+
|
|
81
|
+
const isUploadApi = url === '/upload/image' || url === '/upload/mask'
|
|
82
|
+
|| url === '/api/upload/image' || url === '/api/upload/mask';
|
|
83
|
+
|
|
84
|
+
if (!isUploadApi || !response.ok) {
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const data = await response.clone().json();
|
|
89
|
+
|
|
90
|
+
// 检查是否是 OSS 上传响应
|
|
91
|
+
const isOssUpload = data.subfolder?.includes('http://') || data.subfolder?.includes('https://')
|
|
92
|
+
|| data.name?.startsWith('http://') || data.name?.startsWith('https://');
|
|
93
|
+
|
|
94
|
+
if (!isOssUpload) return response;
|
|
95
|
+
|
|
96
|
+
// 构造完整的 OSS URL
|
|
97
|
+
const ossUrl = data.subfolder?.includes('http')
|
|
98
|
+
? `${data.subfolder}/${data.name}`
|
|
99
|
+
: data.name;
|
|
100
|
+
|
|
101
|
+
// 处理映射逻辑
|
|
102
|
+
let finalUrl = ossUrl;
|
|
103
|
+
|
|
104
|
+
if (options?.body instanceof FormData) {
|
|
105
|
+
const imageFile = options.body.get('image');
|
|
106
|
+
if (imageFile?.name) {
|
|
107
|
+
const filename = imageFile.name;
|
|
108
|
+
const idMatch = filename.match(/(\d+)/);
|
|
109
|
+
const baseId = idMatch?.[1];
|
|
110
|
+
|
|
111
|
+
// 第一次 /upload/mask 的结果是涂改后的完整图片
|
|
112
|
+
if (baseId && url.includes('/upload/mask')) {
|
|
113
|
+
const firstMaskKey = `__FIRST_MASK_${baseId}__`;
|
|
114
|
+
|
|
115
|
+
if (!window.CLIPSPACE_TO_OSS_MAP[firstMaskKey]) {
|
|
116
|
+
// 首次 mask 上传,保存到所有变体
|
|
117
|
+
window.CLIPSPACE_TO_OSS_MAP[firstMaskKey] = ossUrl;
|
|
118
|
+
finalUrl = ossUrl;
|
|
119
|
+
|
|
120
|
+
[`clipspace-mask-${baseId}.png`, `clipspace-paint-${baseId}.png`,
|
|
121
|
+
`clipspace-painted-${baseId}.png`, `clipspace-painted-masked-${baseId}.png`
|
|
122
|
+
].forEach(v => window.CLIPSPACE_TO_OSS_MAP[v] = ossUrl);
|
|
123
|
+
|
|
124
|
+
} else {
|
|
125
|
+
// 后续 mask 上传,使用首次的 URL
|
|
126
|
+
finalUrl = window.CLIPSPACE_TO_OSS_MAP[firstMaskKey];
|
|
127
|
+
}
|
|
128
|
+
} else if (baseId) {
|
|
129
|
+
// /upload/image 的上传,如果已有 mask 则使用 mask 的 URL
|
|
130
|
+
const firstMaskUrl = window.CLIPSPACE_TO_OSS_MAP[`__FIRST_MASK_${baseId}__`];
|
|
131
|
+
if (firstMaskUrl) {
|
|
132
|
+
finalUrl = firstMaskUrl;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 保存映射
|
|
137
|
+
[filename, `${filename} [input]`, `${filename} [output]`].forEach(key => {
|
|
138
|
+
window.CLIPSPACE_TO_OSS_MAP[key] = finalUrl;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const filenameWithoutSuffix = filename.replace(/ \[(input|output)\]$/, '');
|
|
142
|
+
if (filenameWithoutSuffix !== filename) {
|
|
143
|
+
window.CLIPSPACE_TO_OSS_MAP[filenameWithoutSuffix] = finalUrl;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 同时保存后端返回的文件名映射
|
|
150
|
+
window.CLIPSPACE_TO_OSS_MAP[data.name] = finalUrl;
|
|
151
|
+
|
|
152
|
+
// 🔧 修改 ComfyApp.clipspace,让它使用 OSS URL 而不是 clipspace 路径
|
|
153
|
+
if (window.app?.constructor?.clipspace) {
|
|
154
|
+
const clipspace = window.app.constructor.clipspace;
|
|
155
|
+
|
|
156
|
+
// 修改 clipspace.images
|
|
157
|
+
if (clipspace.images && clipspace.images.length > 0) {
|
|
158
|
+
const clipImage = clipspace.images[clipspace.selectedIndex || 0];
|
|
159
|
+
if (clipImage && clipImage.subfolder === 'clipspace') {
|
|
160
|
+
clipspace.images[clipspace.selectedIndex || 0] = {
|
|
161
|
+
filename: finalUrl,
|
|
162
|
+
subfolder: ''
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 修改 clipspace.widgets
|
|
168
|
+
if (clipspace.widgets) {
|
|
169
|
+
const imageWidgetIndex = clipspace.widgets.findIndex(w => w.name === 'image');
|
|
170
|
+
if (imageWidgetIndex >= 0) {
|
|
171
|
+
const widgetValue = clipspace.widgets[imageWidgetIndex].value;
|
|
172
|
+
if (widgetValue && typeof widgetValue === 'object' && widgetValue.subfolder === 'clipspace') {
|
|
173
|
+
clipspace.widgets[imageWidgetIndex].value = {
|
|
174
|
+
filename: finalUrl,
|
|
175
|
+
subfolder: ''
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 篡改响应,让 ComfyUI 使用完整的 OSS URL
|
|
183
|
+
const modifiedData = { ...data, name: finalUrl, subfolder: '' };
|
|
184
|
+
return new Response(JSON.stringify(modifiedData), {
|
|
185
|
+
status: response.status,
|
|
186
|
+
statusText: response.statusText,
|
|
187
|
+
headers: response.headers
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error('[BizyDraft Upload] Error:', e);
|
|
192
|
+
return response;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// 转换 prompt 中的 clipspace 路径为 OSS URL
|
|
197
|
+
function convertClipspacePathsInPrompt(prompt) {
|
|
198
|
+
if (!prompt || typeof prompt !== 'object') {
|
|
199
|
+
return prompt;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const [nodeId, node] of Object.entries(prompt)) {
|
|
203
|
+
if (!node?.inputs) continue;
|
|
204
|
+
|
|
205
|
+
for (const [inputKey, inputValue] of Object.entries(node.inputs)) {
|
|
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);
|
|
211
|
+
|
|
212
|
+
if (ossUrl) {
|
|
213
|
+
node.inputs[inputKey] = ossUrl;
|
|
214
|
+
|
|
215
|
+
if (inputKey === 'image' && node.inputs['image_name']) {
|
|
216
|
+
node.inputs['image_name'] = ossUrl.split('/').pop();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return prompt;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
228
|
+
// 拦截 pasteFromClipspace,确保 widget.value 使用 OSS URL
|
|
229
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
230
|
+
function interceptPasteFromClipspace() {
|
|
231
|
+
const ComfyApp = window.app?.constructor;
|
|
232
|
+
if (!ComfyApp || !ComfyApp.pasteFromClipspace) return;
|
|
233
|
+
|
|
234
|
+
const originalPasteFromClipspace = ComfyApp.pasteFromClipspace;
|
|
235
|
+
ComfyApp.pasteFromClipspace = function(node) {
|
|
236
|
+
// 调用原始函数
|
|
237
|
+
originalPasteFromClipspace.call(this, node);
|
|
238
|
+
|
|
239
|
+
// 修正 widget.value
|
|
240
|
+
if (node.widgets) {
|
|
241
|
+
const imageWidget = node.widgets.find(w => w.name === 'image');
|
|
242
|
+
if (imageWidget && typeof imageWidget.value === 'string') {
|
|
243
|
+
const value = imageWidget.value;
|
|
244
|
+
|
|
245
|
+
// 1) 如果是 clipspace 路径格式,替换为 OSS URL
|
|
246
|
+
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;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// 2) 如果是 "https://... [input]" 这样的字符串,移除后缀
|
|
259
|
+
else if (/https?:\/\/.*\.(png|jpg|jpeg|webp|gif)\s\[(input|output)\]$/i.test(value)) {
|
|
260
|
+
const cleaned = stripTypeSuffix(value);
|
|
261
|
+
if (cleaned !== value) {
|
|
262
|
+
imageWidget.value = cleaned;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// 注册 ComfyUI 扩展
|
|
270
|
+
app.registerExtension({
|
|
271
|
+
name: "bizyair.clipspace.to.oss",
|
|
272
|
+
|
|
273
|
+
async setup() {
|
|
274
|
+
const originalGraphToPrompt = app.graphToPrompt;
|
|
275
|
+
|
|
276
|
+
// 在构建 Prompt 之前,先清理所有 widget 的值,去掉多余的后缀
|
|
277
|
+
function sanitizeGraphWidgets(graph) {
|
|
278
|
+
const nodes = graph?._nodes || [];
|
|
279
|
+
for (const node of nodes) {
|
|
280
|
+
if (!node?.widgets) continue;
|
|
281
|
+
for (const widget of node.widgets) {
|
|
282
|
+
if (typeof widget?.value === 'string') {
|
|
283
|
+
widget.value = stripTypeSuffix(widget.value);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
app.graphToPrompt = async function(...args) {
|
|
290
|
+
// 预清理,避免 workflow.widgets_values 和 prompt 输入里包含 [input]/[output]
|
|
291
|
+
try { sanitizeGraphWidgets(app.graph); } catch (e) {}
|
|
292
|
+
|
|
293
|
+
const result = await originalGraphToPrompt.apply(this, args);
|
|
294
|
+
|
|
295
|
+
if (result?.output) {
|
|
296
|
+
// 二次清理并转换 clipspace
|
|
297
|
+
const cleaned = convertClipspacePathsInPrompt(result.output);
|
|
298
|
+
// 额外移除任何字符串输入中的类型后缀
|
|
299
|
+
for (const nodeId of Object.keys(cleaned || {})) {
|
|
300
|
+
const node = cleaned[nodeId];
|
|
301
|
+
if (!node?.inputs) continue;
|
|
302
|
+
for (const key of Object.keys(node.inputs)) {
|
|
303
|
+
const v = node.inputs[key];
|
|
304
|
+
node.inputs[key] = typeof v === 'string' ? stripTypeSuffix(v) : v;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
result.output = cleaned;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return result;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// 拦截 pasteFromClipspace
|
|
314
|
+
interceptPasteFromClipspace();
|
|
315
|
+
}
|
|
316
|
+
});
|