Files

1368 lines
47 KiB
Python

import os
import time
import base64
from PIL import Image
import aiohttp
from aiohttp import web
import server
import json
import sys
import re
import uuid
import hashlib
import threading
import asyncio
import websockets
from comfy.cli_args import parser
import logging
import random
import mimetypes
import math
import platform
import subprocess
from datetime import datetime
import folder_paths
from pathlib import Path
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s",
)
logger = logging.getLogger(__name__)
DEBUG = True
BASE_URL = "https://env-00jxh693vso2.dev-hz.cloudbasefunction.cn"
UPLOAD_OSS_URL = "/http/ext-storage-co/getUploadFileOptions"
END_POINT_URL3 = "/kaji-upload-file/uploadFile"
END_POINT_URL1 = "/kaji-upload-file/uploadProduct"
OSS_DOMAIN = "https://kajiai.cn/"
END_POINT_URL2 = "/get-ws-address/getWsAddress"
END_POINT_URL_FOR_PRODUCT_1 = "/plugin/getProducts"
END_POINT_URL_FOR_PRODUCT_2 = "/plugin/createOrUpdateProduct"
END_POINT_URL_FOR_PRODUCT_3 = "/plugin/deleteProduct"
END_POINT_URL_FOR_PRODUCT_4 = "/plugin/toggleAuthorStatus"
END_POINT_URL_FOR_PRODUCT_5 = "/plugin/toggleDistributionStatus"
END_POINT_FILE_IS_EXITS = "/plugin/fileIsExits"
END_POINT_DELETE_FILE = "/plugin/deleteFiles"
END_POINT_GET_WORKFLOW = "/plugin/getWorkflow"
END_POINT_DELETE_WORKFLOW = "/plugin/deleteWorkflowFile"
END_POINT_MOVE_FILE = "/plugin/uploadInputFile"
media_save_dir = folder_paths.get_input_directory()
media_output_dir = folder_paths.get_output_directory()
print(f"输入目录:{media_save_dir}")
print(f"输出目录:{media_output_dir}")
wss_c1 = None
wss_c2 = None
last_value = None
last_time = None
RECONNECT_DELAY = 5
MAX_RECONNECT_ATTEMPTS = 10
HEART_INTERVAL = 30
gc_task_queue = None
taskIdDict = dict()
listeningTasks = set()
numberDict = dict()
runningNumber = -1
queue_size = 0
def parse_args():
args = parser.parse_args()
return args if args.listen else parser.parse_args([])
def get_address_from_args(args):
return args.listen if args.listen != "0.0.0.0" else "127.0.0.1"
def parse_port_from_args(args):
return args.port
def get_mac_address() -> str:
mac_uid = uuid.getnode()
mac_address = ":".join(("%012X" % mac_uid)[i : i + 2] for i in range(0, 12, 2))
return mac_address
def get_port_from_cmd(default_port=8188):
port = None
def extract_port_from_arg(arg):
match = re.search(r"--port[=\s]*(\d+)", arg)
if match:
return int(match.group(1))
return None
for i, arg in enumerate(sys.argv):
if arg == "--port" and i + 1 < len(sys.argv):
try:
port = int(sys.argv[i + 1])
except ValueError:
continue
extracted_port = extract_port_from_arg(arg)
if extracted_port:
port = extracted_port
if port:
break
return port if port else default_port
def get_machine_unique_id():
try:
system = platform.system()
if system == "Linux":
if os.path.exists("/etc/machine-id"):
with open("/etc/machine-id", "r") as f:
return f.read().strip()
try:
result = subprocess.check_output(
["dmidecode", "-s", "system-uuid"], stderr=subprocess.DEVNULL
)
return result.decode().strip()
except (subprocess.CalledProcessError, FileNotFoundError):
pass
try:
with open("/proc/cpuinfo", "r") as f:
cpuinfo = f.read()
for line in cpuinfo.split("\n"):
if line.startswith("Serial"):
return line.split(":")[1].strip()
except FileNotFoundError:
pass
mac_address = uuid.getnode()
return uuid.UUID(int=mac_address).hex
elif system == "Darwin":
try:
result = subprocess.check_output(
["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"]
).decode()
for line in result.split("\n"):
if "IOPlatformUUID" in line:
return line.split('"')[-2]
raise ValueError("IOPlatformUUID not found")
except subprocess.CalledProcessError as e:
print(f"Failed to retrieve IOPlatformUUID: {e}")
mac_address = uuid.getnode()
return uuid.UUID(int=mac_address).hex
elif system == "Windows":
try:
result = subprocess.check_output(["wmic", "csproduct", "get", "UUID"])
return result.decode().split("\n")[1].strip()
except (subprocess.CalledProcessError, FileNotFoundError, IndexError):
pass
mac_address = uuid.getnode()
return uuid.UUID(int=mac_address).hex
else:
raise ValueError("Unsupported platform")
except Exception as e:
raise RuntimeError(f"Failed to retrieve machine ID: {e}")
def generate_unique_hash():
machine_id = get_machine_unique_id()
hash_value = hashlib.sha256(machine_id.encode()).hexdigest()
return hash_value
args = parse_args()
cur_client_id = f"{str(uuid.uuid4())}:{parse_port_from_args(args)}"
cfy_ws_url = "ws://{}:{}/ws?clientId={}".format(
get_address_from_args(args), parse_port_from_args(args), cur_client_id
)
uni_hash = generate_unique_hash()
print(f"每次启动都是相同的机器码,uni_hash:{uni_hash}")
def get_comfyui_address():
args = parse_args()
address = get_address_from_args(args)
port = parse_port_from_args(args)
return f"http://{address}:{port}"
def getInputTypeArr(data):
input_type_arr = []
for key, item in data.items():
if item.get("class_type") == "sdCpm":
inputs = item.get("inputs")
input_text1 = inputs.get("input_text1(optional)")
input_text2 = inputs.get("input_text2(optional)")
input_text3 = inputs.get("input_text3(optional)")
input_img1 = inputs.get("input_img1(optional)")
input_img2 = inputs.get("input_img2(optional)")
input_img3 = inputs.get("input_img3(optional)")
input_video1 = inputs.get("input_video1(optional)")
input_video2 = inputs.get("input_video2(optional)")
input_video3 = inputs.get("input_video3(optional)")
text1_tips = inputs.get("text1_tips")
text2_tips = inputs.get("text2_tips")
text3_tips = inputs.get("text3_tips")
img1_tips = inputs.get("img1_tips")
img2_tips = inputs.get("img2_tips")
img3_tips = inputs.get("img3_tips")
video1_tips = inputs.get("video1_tips")
video2_tips = inputs.get("video2_tips")
video3_tips = inputs.get("video3_tips")
if input_text1:
text1 = {
"index": input_text1[0],
"class_type": data.get(input_text1[0], {}).get("class_type"),
"input_des": text1_tips,
}
input_type_arr.append(text1)
if input_text2:
text2 = {
"index": input_text2[0],
"class_type": data.get(input_text2[0], {}).get("class_type"),
"input_des": text2_tips,
}
input_type_arr.append(text2)
if input_text3:
text3 = {
"index": input_text3[0],
"class_type": data.get(input_text3[0], {}).get("class_type"),
"input_des": text3_tips,
}
input_type_arr.append(text3)
if input_img1:
img1 = {
"index": input_img1[0],
"class_type": data.get(input_img1[0], {}).get("class_type"),
"input_des": img1_tips,
}
input_type_arr.append(img1)
if input_img2:
img2 = {
"index": input_img2[0],
"class_type": data.get(input_img2[0], {}).get("class_type"),
"input_des": img2_tips,
}
input_type_arr.append(img2)
if input_img3:
img3 = {
"index": input_img3[0],
"class_type": data.get(input_img3[0], {}).get("class_type"),
"input_des": img3_tips,
}
input_type_arr.append(img3)
if input_video1:
video1 = {
"index": input_video1[0],
"class_type": data.get(input_video1[0], {}).get("class_type"),
"input_des": video1_tips,
}
input_type_arr.append(video1)
if input_video2:
video2 = {
"index": input_video2[0],
"class_type": data.get(input_video2[0], {}).get("class_type"),
"input_des": video2_tips,
}
input_type_arr.append(video2)
if input_video3:
video3 = {
"index": input_video3[0],
"class_type": data.get(input_video3[0], {}).get("class_type"),
"input_des": video3_tips,
}
input_type_arr.append(video3)
logging.info(f"input_type_arr =====》 {input_type_arr}")
return input_type_arr
def reformat(uploadData):
uniqueid = uploadData.get("uniqueid")
workflow = uploadData.get("workflow")
output = uploadData.get("output")
formMetaData = uploadData.get("formMetaData")
if not uniqueid or not workflow or not output:
raise ValueError("缺少必要字段:uniqueid, workflow 或 output")
if not isinstance(formMetaData, dict):
raise TypeError("formMetaData 必须是一个字典对象")
images = uploadData.get("images", [])
uploadData["media_urls"] = images
uploadData["uni_hash"] = uni_hash
uploadData["formMetaData"] = formMetaData
uploadData.pop("output", None)
uploadData.pop("workflow", None)
return uploadData
async def send_heartbeat(websocket):
while True:
try:
workflow_path = find_plugin_root() + "config/pipeline"
uniqueids = get_filenames(workflow_path)
payload = {
"type": "ping",
"data": {
"uni_hash": uni_hash,
"uniqueids": uniqueids,
},
}
print(f"发送心跳; ping 数据为:{payload}")
heartbeat_message = json.dumps(payload)
await websocket.send(heartbeat_message)
except websockets.ConnectionClosedError:
print("发送ping失败, WebSocket连接意外关闭,可能网络出现问题")
raise e
except Exception as e:
print(f"发送ping失败: {e}")
raise e
await asyncio.sleep(HEART_INTERVAL)
def get_filenames(directory):
if os.path.exists(directory):
all_entries = os.listdir(directory)
all_entries = [
name
for name in all_entries
if os.path.isfile(os.path.join(directory, name))
and not name.startswith(".")
]
all_entries = [name.split(".")[0] for name in all_entries]
return all_entries
else:
return []
async def receive_messages(websocket, c_flag):
while True:
try:
message = await websocket.recv()
except Exception as e:
logger.critical(f"咔叽ws{c_flag} 接收消息失败,可能需要重连: {e}")
raise e
try:
if c_flag == 1:
logger.info(f"接收支付宝云端ws事件数据: {message}")
await process_server_message1(message)
elif c_flag == 2:
await process_server_message2(message)
else:
logger.warning(f"未识别的c_flag: {c_flag}, 丢弃消息: {message}")
except Exception as e:
logger.error(f"处理消息时发生错误 (c_flag={c_flag}): {e}")
async def handle_websocket(c_flag):
global wss_c1, wss_c2
while True:
try:
if c_flag == 1:
url = await get_wss_server_url()
elif c_flag == 2:
url = cfy_ws_url
else:
return
logging.info(f"咔叽ws{c_flag},url: {url},开始发起连接")
async with websockets.connect(url) as websocket:
print(f"咔叽ws{c_flag} 连接成功!~")
if c_flag == 1:
wss_c1 = websocket
tasks = [
asyncio.create_task(send_heartbeat(websocket)),
asyncio.create_task(receive_messages(websocket, c_flag)),
]
elif c_flag == 2:
wss_c2 = websocket
tasks = [
asyncio.create_task(receive_messages(websocket, c_flag)),
]
await asyncio.gather(*tasks)
except (websockets.ConnectionClosedOK, websockets.ConnectionClosedError) as e:
print(f"咔叽ws{c_flag} 连接不上{e}")
except Exception as e:
print(f"咔叽ws{c_flag} 连接失败,请检查网络{e}")
await asyncio.sleep(RECONNECT_DELAY)
async def process_server_message1(message):
try:
message_data = json.loads(message)
message_type = message_data.get("type")
data = message_data.get("data", {})
if message_type == "generate_submit":
print("收到生图消息", data)
await deal_recv_generate_data(data)
elif message_type == "cancel_listen":
print("任务进度监听取消", data)
listeningTasks.discard(data["kaji_generate_record_id"])
except json.JSONDecodeError:
print("Received non-JSON message from server.")
except Exception as e:
print(f"An error occurred while processing the message: {e}")
async def process_server_message2(message):
global last_value, last_time, runningNumber, queue_size
message_json = json.loads(message)
message_type = message_json.get("type")
if message_type == "status":
status_data = message_json.get("data", {})
queue_size = status_data.get("status").get("exec_info").get("queue_remaining")
elif message_type == "execution_start":
prompt_id = message_json["data"]["prompt_id"]
kaji_generate_record_id = taskIdDict.get(prompt_id)
runningNumber = numberDict.pop(prompt_id)
result = []
for prompt_id in numberDict.keys():
if prompt_id in taskIdDict:
id = taskIdDict[prompt_id]
if id in listeningTasks:
result.append(id)
print(result)
startEvent = {
"type": "execution_start",
"data": {
"kaji_generate_record_id": kaji_generate_record_id,
"prompt_id": prompt_id,
"runningNumber": runningNumber,
"ids": result,
},
}
await wss_c1.send(json.dumps(startEvent))
print(f"新任务开始,成功发给服务端: {startEvent}")
elif message_type == "execution_cached":
pass
elif message_type == "executing":
data = message_json.get("data", {})
logger.info(f"executing事件: {data}")
prompt_id = data.get("prompt_id")
node = data.get("node")
if node is None:
runningNumber = -1
queueChangeEvent = {
"type": "update_queue",
"data": {"uni_hash": uni_hash, "queue_size": queue_size},
}
await wss_c1.send(json.dumps(queueChangeEvent))
print(f"上个任务完成了,总队列大小发给服务端: {queueChangeEvent}")
elif message_type == "progress":
progress_data = message_json.get("data", {})
prompt_id = progress_data.get("prompt_id")
kaji_generate_record_id = taskIdDict.get(prompt_id)
if not kaji_generate_record_id in listeningTasks:
return
value = progress_data.get("value")
max_value = progress_data.get("max")
current_time = time.time()
if last_value is not None and last_time is not None:
time_interval = current_time - last_time
value_change = value - last_value
if value_change > 0:
estimated_total_time = (max_value / value_change) * time_interval
remaining_time = estimated_total_time - (value * time_interval)
else:
remaining_time = 0
else:
remaining_time = 0
remaining_time = math.ceil(max(remaining_time, 0))
last_value = value
last_time = current_time
progressEvent = {
"type": "progress_update",
"data": {
"kaji_generate_record_id": kaji_generate_record_id,
"prompt_id": prompt_id,
"remaining_time": remaining_time,
"value": value,
"max_value": max_value,
},
}
await wss_c1.send(json.dumps(progressEvent))
elif message_type == "executed":
pass
elif message_type == "execution_success":
prompt_id = message_json["data"]["prompt_id"]
kaji_generate_record_id = taskIdDict.pop(prompt_id)
cur_history_info = await get_history_from_comfyui(prompt_id)
gif_links, image_links = await upload_single_node_output_image(
cur_history_info, prompt_id
)
print(f"任务完成上传gif的链接: {gif_links}")
print(f"任务完成上传image的链接: {image_links}")
executionEvent = {
"type": "execution_success",
"data": {
"kaji_generate_record_id": kaji_generate_record_id,
"prompt_id": prompt_id,
"media_data": {"gifs": gif_links, "images": image_links},
},
}
await wss_c1.send(json.dumps(executionEvent))
print(f"任务完成成功发给服务端: {executionEvent}")
listeningTasks.discard(kaji_generate_record_id)
pass
elif message_type == "execution_error" or message_type == "execution_interrupted":
print(f"执行错误: {message_json}")
prompt_id = message_json["data"]["prompt_id"]
kaji_generate_record_id = taskIdDict.pop(prompt_id)
errorEvent = {
"type": "execution_error",
"data": {
"kaji_generate_record_id": kaji_generate_record_id,
"prompt_id": prompt_id,
},
}
await wss_c1.send(json.dumps(errorEvent))
print(f"结果(错误)成功发给服务端: {errorEvent}")
listeningTasks.discard(kaji_generate_record_id)
async def upload_single_node_output_image(cur_history_info, prompt_id):
gif_tasks = []
image_tasks = []
history_entry = cur_history_info.get(prompt_id)
if not history_entry or "outputs" not in history_entry:
raise ValueError(f"No outputs found for prompt_id: {prompt_id}")
outputs = history_entry["outputs"]
for output_value in outputs.values():
if "gifs" in output_value:
for gif_info in output_value["gifs"]:
if gif_info.get("type") != "temp":
filename = gif_info["filename"]
task = upload_output_gifs(filename)
gif_tasks.append(task)
if "images" in output_value:
for image_info in output_value["images"]:
if image_info.get("type") != "temp":
filename = image_info["filename"]
task = upload_output_image(filename)
image_tasks.append(task)
gif_links, image_links = await asyncio.gather(
asyncio.gather(*gif_tasks), asyncio.gather(*image_tasks)
)
return list(gif_links), list(image_links)
async def get_wss_server_url():
async with aiohttp.ClientSession() as session:
payload = {"uni_hash": uni_hash}
async with session.post(BASE_URL + END_POINT_URL2, json=payload) as response:
try:
res = await response.json()
wss_server_url = res.get("data")
if not wss_server_url:
raise Exception("Failed to retrieve WebSocket server URL")
return wss_server_url
except json.JSONDecodeError:
return web.Response(
status=response.status, text="Invalid JSON response2"
)
@server.PromptServer.instance.routes.post(END_POINT_URL_FOR_PRODUCT_1)
async def getProducts(req):
try:
body = await req.json()
token = body.get("token")
if not token:
return web.json_response({"error": "Token is required"}, status=400)
jsonData = {
"token": token,
}
async with aiohttp.ClientSession() as session:
async with session.post(
BASE_URL + END_POINT_URL_FOR_PRODUCT_1, json=jsonData
) as response:
res_js = await response.json()
return web.json_response(res_js)
except Exception as e:
print("Error parsing request body:", e)
return web.json_response({"error": "Invalid request body"}, status=400)
@server.PromptServer.instance.routes.post(END_POINT_URL_FOR_PRODUCT_3)
async def deleteProduct(req):
jsonData = await req.json()
async with aiohttp.ClientSession() as session:
async with session.post(
BASE_URL + END_POINT_URL_FOR_PRODUCT_3, json=jsonData
) as response:
res_js = await response.json()
data = res_js.get("data", {})
print("res_js", res_js)
return web.json_response(res_js)
@server.PromptServer.instance.routes.post(END_POINT_URL_FOR_PRODUCT_4)
async def toggleAuthor(req):
jsonData = await req.json()
async with aiohttp.ClientSession() as session:
async with session.post(
BASE_URL + END_POINT_URL_FOR_PRODUCT_4, json=jsonData
) as response:
res_js = await response.json()
data = res_js.get("data", {})
print("res_js", res_js)
return web.json_response(res_js)
@server.PromptServer.instance.routes.post(END_POINT_URL_FOR_PRODUCT_5)
async def toggleDistribution(req):
jsonData = await req.json()
async with aiohttp.ClientSession() as session:
async with session.post(
BASE_URL + END_POINT_URL_FOR_PRODUCT_5, json=jsonData
) as response:
res_js = await response.json()
data = res_js.get("data", {})
print("res_js", res_js)
return web.json_response(res_js)
@server.PromptServer.instance.routes.post(END_POINT_FILE_IS_EXITS)
async def checkFileIsExits(req):
jsonData = await req.json()
file_name = jsonData.get("file_name")
if not file_name:
return web.json_response({"success": False, "errMsg": "文件路径不能为空"})
abs_file_path = os.path.join(find_pipeline_path(), file_name)
file_exists = os.path.exists(abs_file_path)
return web.json_response({"success": True, "fileExists": file_exists})
@server.PromptServer.instance.routes.post(END_POINT_GET_WORKFLOW)
async def getWorkflowJson(req):
jsonData = await req.json()
workflow_id = jsonData.get("workflow_id")
if not workflow_id:
return web.json_response({"success": False, "errMsg": "工作流ID不能为空"})
workflow_data = get_workflow(workflow_id)
if not workflow_data:
return web.json_response({"success": False, "errMsg": "工作流文件不存在或为空"})
return web.json_response({"success": True, "workflow": workflow_data})
def find_pipeline_path():
pipeline_path = os.path.join(find_plugin_root(), "config", "pipeline")
os.makedirs(pipeline_path, exist_ok=True)
return pipeline_path
def sanitize_filename(filename):
sanitized = re.sub(r"[^A-Za-z0-9_.-]", "", filename)
if not sanitized or ".." in sanitized:
raise ValueError("Invalid file name")
return sanitized
def append_json_extension(filename):
if not filename.lower().endswith(".json"):
filename += ".json"
return filename
def get_abs_file_path(pipeline_path, file_name):
sanitized_file_name = sanitize_filename(file_name)
sanitized_file_name_with_ext = append_json_extension(sanitized_file_name)
pipeline_path_obj = Path(pipeline_path)
abs_file_path = (pipeline_path_obj / sanitized_file_name_with_ext).resolve()
try:
abs_file_path.relative_to(pipeline_path_obj)
except ValueError:
raise ValueError("File path traversal detected")
return str(abs_file_path)
@server.PromptServer.instance.routes.post(END_POINT_DELETE_FILE)
async def deleteFile(req):
jsonData = await req.json()
file_name = jsonData.get("file_name")
if not file_name:
return web.json_response({"success": False, "errMsg": "文件路径不能为空"})
abs_file_path = get_abs_file_path(find_pipeline_path(), file_name)
if os.path.exists(abs_file_path):
try:
os.remove(abs_file_path)
return web.json_response({"success": True, "message": "文件删除成功"})
except Exception as e:
return web.json_response(
{"success": False, "errMsg": f"删除文件时出错:{str(e)}"}
)
else:
return web.json_response({"success": False, "errMsg": "文件不存在"})
@server.PromptServer.instance.routes.post(END_POINT_MOVE_FILE)
async def handle_upload(req):
data = await req.post()
files = data.getall("files", [])
if not files:
return web.json_response({"success": False, "errMsg": "没有文件被上传"})
try:
base_dir = os.path.abspath(
os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(find_plugin_root()))),
"input",
)
)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
results = []
for file in files:
filename = file.filename
abs_file_path = os.path.join(base_dir, filename)
with open(abs_file_path, "wb") as f:
f.write(file.file.read())
results.append(
{"filename": filename, "message": "文件上传成功", "path": abs_file_path}
)
return web.json_response({"success": True, "results": results})
except Exception as e:
return web.json_response(
{"success": False, "errMsg": f"文件上传时出错:{str(e)}"}
)
@server.PromptServer.instance.routes.post(END_POINT_DELETE_WORKFLOW)
async def delete_workflow(req):
jsonData = await req.json()
uniqueid = jsonData.get("uniqueid")
if not uniqueid:
return web.json_response({"success": False, "errMsg": "uniqueid 不能为空"})
try:
workflow_data = get_workflow(uniqueid)
if not workflow_data:
return web.json_response(
{"success": False, "errMsg": "未找到对应的工作流数据"}
)
base_dir = os.path.abspath(
os.path.join(
find_plugin_root(),
"config",
"pipeline",
)
)
abs_file_path = os.path.join(base_dir, f"{uniqueid}.json")
if os.path.exists(abs_file_path):
os.remove(abs_file_path)
return web.json_response({"success": True, "message": "工作流文件删除成功"})
else:
return web.json_response({"success": False, "errMsg": "文件不存在"})
except Exception as e:
return web.json_response(
{"success": False, "errMsg": f"删除工作流时出错:{str(e)}"}
)
# 统一获取一个时间戳当作文件名
def getUniFileName():
now = datetime.now()
timestamp = int(now.timestamp()) # 获取 Unix 时间戳
microseconds = now.microsecond # 获取微秒
return f"{timestamp}{microseconds}"
@server.PromptServer.instance.routes.post("/get-upload-token")
async def get_upload_token(req):
try:
json_data = await req.json()
original_file_name = json_data.get("fileName")
if not original_file_name:
return web.json_response({"success": False, "errMsg": "缺少文件名参数"})
file_extension = os.path.splitext(original_file_name)[1]
if not file_extension:
return web.json_response({"success": False, "errMsg": "文件名缺少扩展名"})
biz_code = "product_images"
# cloud_file_name = f"{datetime.now().strftime('%s%f')}{file_extension}"
now = datetime.now()
timestamp = int(now.timestamp()) # 获取 Unix 时间戳
microseconds = now.microsecond # 获取微秒
# 生成文件名
cloud_file_name = f"{getUniFileName()}{file_extension}"
async with aiohttp.ClientSession() as session:
async with session.get(
BASE_URL
+ UPLOAD_OSS_URL
+ f"?bizCode={biz_code}&cloudFileName={cloud_file_name}"
) as response:
if response.status == 200:
upload_options = await response.json()
return web.json_response({"success": True, "data": upload_options})
else:
return web.json_response(
{"success": False, "errMsg": "获取上传凭证失败"}
)
except Exception as e:
print(f"获取上传凭证时出错: {str(e)}")
return web.json_response({"success": False, "errMsg": "服务器内部错误"})
@server.PromptServer.instance.routes.post(END_POINT_URL1)
async def kaji_r(req):
try:
jsonData = await req.json()
logging.info(f"收到的请求数据: {jsonData}")
uniqueid = jsonData.get("uniqueid")
workflow = jsonData.get("workflow")
output = jsonData.get("output")
if not uniqueid:
logging.error("请求中缺少 uniqueid 字段")
return web.Response(status=400, text="uniqueid is missing")
if not workflow or not output:
logging.error("请求中缺少 workflow 或 output 字段")
return web.Response(status=400, text="workflow or output is missing")
newData = reformat(jsonData)
async with aiohttp.ClientSession() as session:
async with session.post(
BASE_URL + END_POINT_URL_FOR_PRODUCT_2, json=newData
) as response:
try:
res = await response.text()
logging.info(f"收到的响应文本: {res}")
res_js = json.loads(res)
data = res_js.get("data", {})
PRODUCT_ID = data.get("_id", None)
if PRODUCT_ID is None:
logging.error("未能从响应中获取 PRODUCT_ID")
raise ValueError("未能从响应中获取 PRODUCT_ID")
txt_audit_status = data.get("txt_audit_status", None)
if txt_audit_status != 1:
logging.error("标题或描述审核未通过,涉嫌违规")
raise ValueError("标题或描述审核未通过,涉嫌违规")
image_audit_status = data.get("image_audit_status", None)
if image_audit_status != 1:
logging.error("图片审核未通过,涉嫌违规")
raise ValueError("图片审核未通过,涉嫌违规")
logging.info("作品上传成功")
logging.info(f"正在保存工作流数据: uniqueid={uniqueid}")
save_workflow(uniqueid, {"workflow": workflow, "output": output})
return web.json_response(res_js)
except json.JSONDecodeError:
logging.error("无法解析 JSON 响应")
return web.Response(
status=response.status, text="Failed to parse JSON response"
)
except ValueError as e:
logging.error(f"值错误: {e}")
return web.Response(status=400, text=str(e))
except Exception as e:
logging.error(f"处理请求时出错: {e}")
return web.Response(status=500, text="Internal Server Error")
def save_merged_data(workflow_data, output_data, file_path):
try:
combined_data = {"workflow": workflow_data, "output": output_data}
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(combined_data, f, ensure_ascii=False, indent=4)
print(f"工作流文件已成功合并保存为 {file_path}")
except Exception as e:
print(f"工作流文件合并保存发生错误: {e}")
def parse_merged_file(file_path):
try:
if not os.path.exists(file_path):
print(f"工作流文件不存在: {file_path}")
return {"workflow": {}, "output": {}}
with open(file_path, "r", encoding="utf-8") as f:
combined_data = json.load(f)
return {
"workflow": combined_data.get("workflow", {}),
"output": combined_data.get("output", {}),
}
except json.JSONDecodeError as e:
print(f"工作流文件解析错误: {e}")
return {"workflow": {}, "output": {}}
except Exception as e:
print(f"读取工作流文件时发生错误: {e}")
return {"workflow": {}, "output": {}}
def save_workflow(uniqueid, data):
base_path = os.path.join(find_plugin_root(), "config/pipeline/")
os.makedirs(base_path, exist_ok=True)
combined_file = os.path.join(base_path, f"{uniqueid}.json")
save_merged_data(data.get("workflow", {}), data.get("output", {}), combined_file)
async def task_generate():
while True:
try:
task_data = await gc_task_queue.get()
await run_gc_task_async(task_data)
gc_task_queue.task_done()
except asyncio.CancelledError:
logging.error("Task was cancelled.")
except Exception as e:
logging.error(f"发生错误: {e}")
async def send_prompt_to_comfyui(prompt, workflow=None):
comfyui_address = get_comfyui_address()
data = {
"prompt": prompt,
"client_id": cur_client_id,
}
logging.info(f"发送到 ComfyUI 的 prompt 数据: {data}")
if workflow and "extra_data" in workflow:
data["extra_data"] = workflow["extra_data"]
async with aiohttp.ClientSession() as session:
async with session.post(f"{comfyui_address}/prompt", json=data) as response:
if response.status == 200:
response_json = await response.json()
logging.info(f"核心接口 /prompt的接口出参: {response_json}")
return response_json
else:
error_text = await response.text()
logging.error(
f"发送prompt失败,状态码: {response.status}, 错误信息: {error_text}"
)
return None
async def get_upload_options(session, bizCode, cloudFileName):
async with session.get(
BASE_URL + UPLOAD_OSS_URL + f"?bizCode={bizCode}&cloudFileName={cloudFileName}"
) as response:
if response.status == 200:
return await response.json()
else:
print(f"获取上传配置信息失败,状态码: {response.status}")
return None
async def upload_file_to_oss(session, url, token, key, file_path, filename):
with open(file_path, "rb") as file:
form_data = aiohttp.FormData()
form_data.add_field("file", file, filename=filename)
form_data.add_field("token", token)
form_data.add_field("key", key)
async with session.post(url, data=form_data) as upload_response:
try:
upload_data = await upload_response.json()
print("文件上传成功:", OSS_DOMAIN + upload_data.get("key"))
return OSS_DOMAIN + upload_data.get("key")
except aiohttp.ClientResponseError as e:
print("上传失败", e)
return None
except json.JSONDecodeError as e:
print("解析上传结果失败", e)
return None
async def find_file(directory, target_filename):
for root, dirs, files in os.walk(directory):
if target_filename in files:
return os.path.join(root, target_filename)
return None
async def upload_output_image(filename, bizCode="workflow_output"):
temp_path = await find_file(media_output_dir, filename)
if not os.path.exists(temp_path):
print(f"File does not exist: {temp_path}")
return None
async with aiohttp.ClientSession() as session:
cloudFileName = f"{getUniFileName()}.png"
uploadOptionsRes = await get_upload_options(session, bizCode, cloudFileName)
if uploadOptionsRes is None:
return None
url = uploadOptionsRes["uploadFileOptions"]["url"]
token = uploadOptionsRes["uploadFileOptions"]["formData"]["token"]
key = uploadOptionsRes["uploadFileOptions"]["formData"]["key"]
return await upload_file_to_oss(session, url, token, key, temp_path, filename)
async def upload_output_gifs(filename, bizCode="workflow_output"):
temp_path = await find_file(media_output_dir, filename)
if not os.path.exists(temp_path):
print(f"File does not exist: {temp_path}")
return None
async with aiohttp.ClientSession() as session:
cloudFileName = (
f"{getUniFileName()}{os.path.splitext(filename)[1]}"
)
uploadOptionsRes = await get_upload_options(session, bizCode, cloudFileName)
if uploadOptionsRes is None:
return None
url = uploadOptionsRes["uploadFileOptions"]["url"]
token = uploadOptionsRes["uploadFileOptions"]["formData"]["token"]
key = uploadOptionsRes["uploadFileOptions"]["formData"]["key"]
return await upload_file_to_oss(session, url, token, key, temp_path, filename)
def validate_prompt(prompt):
for node_id, node_data in prompt.items():
if "class_type" not in node_data:
logging.error(f"节点 {node_id} 缺少 class_type 属性")
return False
return True
async def run_gc_task_async(task_data):
try:
logging.info(f"工作队列获取任务-开始执行: {task_data}")
if "prompt" not in task_data or "kaji_generate_record_id" not in task_data:
logging.error(f"任务数据不完整: {task_data}")
return
prompt = task_data["prompt"]
kaji_generate_record_id = task_data["kaji_generate_record_id"]
if not validate_prompt(prompt):
logging.error("prompt 数据无效")
return
result = await send_prompt_to_comfyui(prompt)
if result and "prompt_id" in result:
prompt_id = result["prompt_id"]
taskIdDict[prompt_id] = kaji_generate_record_id
listeningTasks.add(kaji_generate_record_id)
number = result["number"]
numberDict[prompt_id] = number
if runningNumber == -1:
cur_q = 0
else:
cur_q = number - runningNumber
logging.info(f"立即获取当前任务的排队状态: {cur_q}")
submit_success = {
"type": "submit_success",
"data": {
"kaji_generate_record_id": kaji_generate_record_id,
"prompt_id": prompt_id,
"number": number,
"uni_hash": uni_hash,
"cur_q": cur_q,
},
}
await wss_c1.send(json.dumps(submit_success))
logging.info(f"任务成功提交,prompt_id: {submit_success}")
else:
logging.error("任务提交失败")
except Exception as e:
logging.error(f"执行任务时发生错误: {str(e)}")
logging.error("详细错误信息:", exc_info=True)
async def get_history_from_comfyui(prompt_id):
comfyui_address = get_comfyui_address()
url = f"{comfyui_address}/history/{prompt_id}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
history_data = await response.json()
return history_data
else:
error_text = await response.text()
logging.error(
f"获取历史记录失败,状态码: {response.status}, 错误信息: {error_text}"
)
return None
async def download_media_async(url, save_dir):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status != 200:
logging.error(f"下载媒体文件失败: {url}, 状态码: {response.status}")
return None
content_type = response.headers.get("content-type")
extension = (
mimetypes.guess_extension(content_type) or os.path.splitext(url)[1]
)
if not extension:
extension = ".bin"
filename = f"{os.urandom(8).hex()}{extension}"
save_path = os.path.join(save_dir, filename)
os.makedirs(save_dir, exist_ok=True)
with open(save_path, "wb") as file:
while chunk := await response.content.read(8192):
file.write(chunk)
print(f"媒体文件已成功下载到: {save_path}")
return save_path
except Exception as e:
logging.error(f"下载媒体文件时发生错误: {e}")
return None
async def download_all_media(form_data):
download_tasks = []
for key, value in form_data.items():
if isinstance(value, dict) and "url" in value:
url = value["url"]
download_tasks.append(download_media_async(url, media_save_dir))
downloaded_paths = await asyncio.gather(*download_tasks)
return downloaded_paths
def update_output_from_form_data(form_data, output, downloaded_paths):
download_index = 0
for key, value in form_data.items():
# 解析复合键
if not key.startswith("#"):
logging.warning(f"无效的键格式: {key},跳过")
continue
key_parts = key[1:].split("-") # 去掉开头的 '#' 并分割
if len(key_parts) < 2:
logging.warning(f"无效的键格式: {key},跳过")
continue
node_id, class_type_and_key = key_parts[0], "-".join(key_parts[1:])
# 分离 class_type 和需要替换的键
if ":" in class_type_and_key:
class_type, replace_key = class_type_and_key.split(":", 1)
else:
logging.warning(f"无效的键格式: {key},缺少 ':' 分隔符,跳过")
continue
logging.debug(
f"解析键: {key} -> node_id: {node_id}, class_type: {class_type}, replace_key: {replace_key}"
)
# 查找匹配的输出项
output_item = None
for output_key, output_value in output.items():
if (
str(output_key) == node_id
and output_value.get("class_type") == class_type
):
output_item = output_value
break
if not output_item:
logging.warning(
f"未找到匹配的 node_id: {node_id} 和 class_type: {class_type},跳过 {key}"
)
continue
logging.debug(f"找到匹配的输出项: {output_item}")
# 定位到 inputs 部分,根据后续的 key_parts 更新字段
current = output_item.get("inputs", {})
input_key_parts = replace_key.split(":")[:-1] # 如果有嵌套结构,则处理嵌套部分
for part in input_key_parts:
current = current.setdefault(part, {})
logging.debug(f"定位到输入部分: {current}")
# 如果是媒体文件,替换为本地路径
if isinstance(value, dict) and "url" in value:
local_path = downloaded_paths[download_index]
download_index += 1
if local_path:
current[replace_key.split(":")[-1]] = os.path.basename(local_path)
logging.info(f"媒体文件下载成功并更新: {local_path}")
else:
logging.error(f"媒体文件下载失败: {value['url']}")
else:
# 直接更新字段
current[replace_key.split(":")[-1]] = value
logging.info(f"直接更新字段: {replace_key.split(':')[-1]} = {value}")
logging.info(f"最终结果: {output}")
async def deal_recv_generate_data(recv_data):
uniqueid = recv_data["uniqueid"]
kaji_generate_record_id = recv_data["kaji_generate_record_id"]
form_data = recv_data["formData"]
output = get_output(uniqueid)
downloaded_paths = await download_all_media(form_data)
update_output_from_form_data(form_data, output, downloaded_paths)
await pre_process_data(kaji_generate_record_id, output)
async def pre_process_data(kaji_generate_record_id, output):
try:
for item in output.values():
if item.get("class_type") == "KSampler":
item["inputs"]["seed"] = random.randint(10**14, 10**15 - 1)
task_data = {
"kaji_generate_record_id": kaji_generate_record_id,
"prompt": output,
}
await gc_task_queue.put(task_data)
print("任务(包含工作流的数据)添加至工作队列。")
except Exception as e:
print(f"处理数据时发生错误: {e}")
def get_output(uniqueid, path="config/pipeline/"):
base_path = os.path.join(find_plugin_root(), path)
combined_file = os.path.join(base_path, f"{uniqueid}.json")
parsed_data = parse_merged_file(combined_file)
return parsed_data.get("output", {})
def get_workflow(uniqueid, path="config/pipeline/"):
base_path = os.path.join(find_plugin_root(), path)
combined_file = os.path.join(base_path, f"{uniqueid}.json")
parsed_data = parse_merged_file(combined_file)
return parsed_data.get("workflow", {})
def find_project_root():
script_directory = os.path.dirname(os.path.abspath(__file__))
relative_path = script_directory + "../../../"
absolute_path = os.path.abspath(relative_path)
if not absolute_path.endswith(os.sep):
absolute_path += os.sep
return absolute_path
def find_plugin_root():
script_directory = os.path.dirname(os.path.abspath(__file__))
if not script_directory.endswith(os.sep):
script_directory += os.sep
return script_directory
def thread_run():
logging.info(f"咔叽插件网络资源初始化:如 websocket、异步队列等")
threading.Thread(target=run_asyncio_in_thread, daemon=True).start()
def run_asyncio_in_thread():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
global gc_task_queue
gc_task_queue = asyncio.Queue()
loop.run_until_complete(
asyncio.gather(
handle_websocket(1),
handle_websocket(2),
task_generate(),
)
)