Major version update with enhanced functionality

Added environment-based configuration to streamline deployments
Improved data validation logic for more robust CRUD operations
Refined folder structure to optimize code organization
This commit is contained in:
RiceRound
2025-01-14 04:20:56 +08:00
parent 4d7d7fdc3f
commit e614d4e974
19 changed files with 2047 additions and 931 deletions
+84 -32
View File
@@ -1,47 +1,99 @@
from functools import wraps
import json
from .trim_workflow import PromptTrim
from .crypto_node import SaveCryptoNode, ExcuteCryptoNode, RandomSeedNode,CryptoCatImage
from functools import partial, wraps
import os
from .trim_workflow import WorkflowTrimHandler
from .crypto_node import (
SaveCryptoNode,
RandomSeedNode,
SaveCryptoBridgeNode,
DecodeCryptoNode,
)
from .crypto_node_old import ExcuteCryptoNode, CryptoCatImage
NODE_CLASS_MAPPINGS = {
"SaveCryptoNode": SaveCryptoNode,
"ExcuteCryptoNode": ExcuteCryptoNode,
"RandomSeedNode": RandomSeedNode,
"CryptoCatImage":CryptoCatImage,
"CryptoCatImage": CryptoCatImage,
"SaveCryptoBridgeNode": SaveCryptoBridgeNode,
"DecodeCryptoNode": DecodeCryptoNode,
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
NODE_DISPLAY_NAME_MAPPINGS = {
"SaveCryptoNode": "InputCrypto",
"SaveCryptoNode": "加密组件",
"RandomSeedNode": "随机种子",
"SaveCryptoBridgeNode": "加密结束桥接",
"DecodeCryptoNode": "解密组件",
"ExcuteCryptoNode": "OutputCrypto",
"RandomSeedNode": "RandomSeedNode",
"CryptoCatImage":"CryptoCatImage",
"CryptoCatImage": "CryptoCatImage",
}
WEB_DIRECTORY = "./js"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAMES_MAPPINGS', 'WEB_DIRECTORY']
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAMES_MAPPINGS", "WEB_DIRECTORY"]
from server import PromptServer
import aiohttp
workspace_path = os.path.join(os.path.dirname(__file__))
dist_path = os.path.join(workspace_path, "static")
if os.path.exists(dist_path):
PromptServer.instance.app.add_routes(
[aiohttp.web.static("/cryptocat/static", dist_path)]
)
from urllib.parse import unquote
from .auth_unit import AuthUnit
from .updown_workflow import UploadWorkflow, UserWorkflowSetting
handler_instance = WorkflowTrimHandler()
onprompt_callback = partial(handler_instance.onprompt_handler)
PromptServer.instance.add_on_prompt_handler(onprompt_callback)
routes = PromptServer.instance.routes
method_name = "trigger_on_prompt"
original_method = getattr(PromptServer, method_name)
@routes.get("/cryptocat/auth_callback")
async def auth_callback(request):
auth_query = request.query
token = auth_query.get("token", "")
client_key = auth_query.get("client_key", "")
if token and client_key:
token = unquote(token)
client_key = unquote(client_key)
AuthUnit().save_user_token(token, client_key)
return aiohttp.web.json_response({"status": "success"}, status=200)
# 创建 hook
@wraps(original_method)
def new_trigger_on_prompt(self, json_data):
prompt = json_data["prompt"]
pr = PromptTrim(prompt)
if pr.has_crypto_node():
print("has crypto node")
json_data["prompt"] = pr.replace_prompt()
# 删除 extra_data 字段
if "extra_data" in json_data:
json_data["extra_data"] = {}
print("hook new_trigger_on_prompt delete extra_data")
result = original_method(self, json_data)
return result
setattr(PromptServer, method_name, new_trigger_on_prompt)
@routes.post("/cryptocat/keygen")
async def keygen(request):
data = await request.json()
template_id = data.get("template_id", "").strip()
if not template_id or len(template_id) != 32:
return aiohttp.web.json_response(
{"error_msg": "template_id is required"}, status=500
)
user_token, error_msg = AuthUnit().get_user_token()
if not user_token:
return aiohttp.web.json_response({"error_msg": error_msg}, status=200)
user_workflow = UploadWorkflow(user_token)
serial_numbers = user_workflow.generate_serial_number(template_id, 1)
if not serial_numbers:
return aiohttp.web.json_response({"error_msg": "获取失败"}, status=200)
serial_number = serial_numbers[0]
return aiohttp.web.json_response({"serial_number": serial_number}, status=200)
@routes.get("/cryptocat/logout")
async def logout(request):
AuthUnit().clear_user_token()
return aiohttp.web.json_response({"status": "success"}, status=200)
@routes.post("/cryptocat/set_long_token")
async def set_long_token(request):
data = await request.json()
long_token = data.get("long_token", "")
AuthUnit().save_long_token(long_token)
return aiohttp.web.json_response({"status": "success"}, status=200)
@routes.post("/cryptocat/set_auto_overwrite")
async def set_auto_overwrite(request):
data = await request.json()
auto_overwrite = data.get("auto_overwrite")
UserWorkflowSetting().set_auto_overwrite(auto_overwrite)
return aiohttp.web.json_response({"status": "success"}, status=200)
+107
View File
@@ -0,0 +1,107 @@
_G='clear_key'
_F='cryptocat_clear_user_info'
_E='long_token'
_D='user_token'
_C=None
_B='Auth'
_A='utf-8'
import base64,json,logging,os
from pathlib import Path
import time
from.url_config import CatUrlConfig
from server import PromptServer
import requests,configparser
from.utils import generate_random_string,get_local_app_setting_path,get_machine_id
CRYPT_FLAG='CAT_CRYPT'
CRYPT_FLAG_LENGTH=len(CRYPT_FLAG)
class AuthUnit:
_instance=_C
def __new__(A,*B,**C):
if A._instance is _C:A._instance=super(AuthUnit,A).__new__(A)
return A._instance
def __init__(A):
B=True
if not hasattr(A,'initialized'):A.machine_id=get_machine_id();C=get_local_app_setting_path();C.mkdir(parents=B,exist_ok=B);A.config_path=C/'config.ini';A.token=A.read_user_token();A.long_token=A.read_long_token();A.client_key='';A.shift_key=sum(ord(A)for A in A.machine_id[:8])%20+1;A.last_check_time=0;A.initialized=B
def empty_token(A,token,clear=False):
C=clear;B=token
if B==A.token:
A.token=''
if C:PromptServer.instance.send_sync(_F,{_G:_D})
elif B==A.long_token:
A.long_token=''
if C:PromptServer.instance.send_sync(_F,{_G:_E})
else:print('empty_token error',B)
def get_user_token(B):
A=B.token
if not A:
A=B.read_user_token()
if not A:
if B.long_token and len(B.long_token)>50:A=B.long_token
else:return _C,'no token found'
else:B.token=A
if time.time()-B.last_check_time>120 and A:
try:
D={'Content-Type':'application/json','Authorization':f"Bearer {A}"};print('login_api_url',CatUrlConfig().login_api_url);C=requests.get(CatUrlConfig().login_api_url,headers=D)
if C.status_code==200:B.last_check_time=time.time()
else:print('crypto cat login result error',C);B.empty_token(A,C.status_code==401);A=_C;return _C,'login result error'
except requests.RequestException as E:B.empty_token(A);A=_C;print('crypto cat login failed',E);return _C,'login failed'
return A,''
def login_dialog(A,title=''):A.client_key=generate_random_string(8);PromptServer.instance.send_sync('cryptocat_login_dialog',{'machine_id':A.machine_id,'client_key':A.client_key,'title':title})
@staticmethod
def _encrypt(text,shift_key):
'Encrypt text using a simple character shift and base64 encoding.'
if not text:return''
try:B=CRYPT_FLAG+''.join(chr((ord(A)+shift_key)%65536)for A in text);return base64.b64encode(B.encode(_A)).decode(_A)
except Exception as A:logging.error(f"Encryption error: {A}");raise RuntimeError(f"Encryption failed: {A}")
@staticmethod
def _decrypt(encoded_text,shift_key):
'Decrypt text that was encrypted with the _encrypt method.';B=encoded_text
try:
A=base64.b64decode(B.encode(_A)).decode(_A)
if not A.startswith(CRYPT_FLAG):return B
A=A[CRYPT_FLAG_LENGTH:];return''.join(chr((ord(A)-shift_key)%65536)for A in A)
except Exception as C:print(f"Decryption error: {C}");return B
def read_user_token(A):
'Retrieve and decrypt the user token.'
if not os.path.exists(A.config_path):return''
try:
B=configparser.ConfigParser();B.read(A.config_path,encoding=_A);C=B.get(_B,_D,fallback='')
if not C:return''
return AuthUnit._decrypt(C,A.shift_key)
except Exception as D:print(f"Error reading token: {D}");return''
def save_user_token(A,user_token,client_key):
'Encrypt and save the user token.';D=client_key;C=user_token
if not C:return
if not D or A.client_key!=D:return
try:
A.token=C;B=configparser.ConfigParser()
if os.path.exists(A.config_path):B.read(A.config_path,encoding=_A)
if _B not in B:B.add_section(_B)
F=AuthUnit._encrypt(C,A.shift_key);B[_B][_D]=F
with open(A.config_path,'w',encoding=_A)as G:B.write(G)
except Exception as E:print(f"Error saving token: {E}");raise RuntimeError(f"Failed to save token: {E}")
def save_long_token(A,long_token):
C=long_token
try:
if not C or C=='null':A.long_token='';C=''
else:A.long_token=C
B=configparser.ConfigParser()
if os.path.exists(A.config_path):B.read(A.config_path,encoding=_A)
if _B not in B:B.add_section(_B)
B[_B][_E]=A.long_token
with open(A.config_path,'w',encoding=_A)as E:B.write(E)
except Exception as D:print(f"Error saving long token: {D}");raise RuntimeError(f"Failed to save long token: {D}")
def read_long_token(A):
if not os.path.exists(A.config_path):return''
try:B=configparser.ConfigParser();B.read(A.config_path,encoding=_A);return B.get(_B,_E,fallback='')
except Exception as C:print(f"Error reading long token: {C}");return''
def clear_user_token(B):
B.token='';B.long_token='';PromptServer.instance.send_sync(_F,{_G:'all'})
if not os.path.exists(B.config_path):return
try:
A=configparser.ConfigParser();A.read(B.config_path,encoding=_A)
if _B not in A:return
if _D not in A[_B]:return
A[_B][_D]='';A[_B][_E]=''
with open(B.config_path,'w',encoding=_A)as D:A.write(D)
except Exception as C:print(f"Error clearing token: {C}");raise RuntimeError(f"Failed to clear token: {C}")
+217 -139
View File
@@ -1,187 +1,265 @@
import json
import os
import random
import uuid
import torch
from nodes import SaveImage
from .trim_workflow import PromptTrim, WorkflowTrim
from.file_compressor import FileCompressor
import folder_paths
from comfy_execution.graph import ExecutionBlocker
from comfy_execution.graph_utils import GraphBuilder
from .updown_workflow import UploadWorkflow
from server import PromptServer
from .trim_workflow import CryptoWorkflow, DecodeCryptoWorkflow
from .auth_unit import AuthUnit
class SaveCryptoNode():
class SaveCryptoNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"crypto_folder": ("STRING", {"default": folder_paths.output_directory}),
"crypto_name": ("STRING", {"default": "my_cat.json"}),
"output_images" : ("IMAGE",),
},
"optional": {
"input_anything" : ("*",),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
"required": {"template_id": ("STRING", {"default": uuid.uuid4().hex})},
"optional": {"input_anything": ("*", {})},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
# @classmethod
# def VALIDATE_INPUTS(s, input_types):
# return True
RETURN_TYPES = () #"STRING",
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "crypto" #这个节点中的主函数
CATEGORY = "advanced/CryptoCat"
def crypto(self, crypto_folder, crypto_name, output_images, **kwargs):
if not crypto_name or len(crypto_folder) < 2 or len(crypto_name) == 0:
raise Exception("CryptoCat folder and filename must be at least two characters long")
# 从 kwargs 中提取特定参数
unique_id = kwargs.pop('unique_id', None)
prompt = kwargs.pop('prompt', None)
extra_pnginfo = kwargs.pop('extra_pnginfo', None)
FUNCTION = "crypto"
CATEGORY = "advanced/CryptoCat"
# 检查必需参数是否存在
@classmethod
def IS_CHANGED(s, **kwargs):
return float("NaN")
@classmethod
def VALIDATE_INPUTS(s, input_types):
return True
def crypto(self, template_id, **kwargs):
unique_id = kwargs.pop("unique_id", None)
prompt = kwargs.pop("prompt", None)
extra_pnginfo = kwargs.pop("extra_pnginfo", None)
if unique_id is None:
raise Exception("Warning: 'unique_id' is missing.")
if prompt is None:
raise Exception("Warning: 'prompt' is missing.")
# 将剩余的 kwargs 存储在 inputs 列表中
inputs = list(kwargs.values())
temp_dir = os.environ.get('TEMP') or os.environ.get('TMP') or '/tmp'
project_temp_folder = os.path.join(temp_dir, crypto_name)
if not os.path.exists(project_temp_folder):
os.makedirs(project_temp_folder)
if len(template_id) != 32:
raise Exception("Warning: 'template_id' length is not 32.")
crypto_workflow = CryptoWorkflow(extra_pnginfo["workflow"], prompt, template_id)
crypto_workflow.invalid_workflow()
crypto_workflow.load_workflow()
crypto_workflow.load_prompt()
crypto_workflow.analysis_node()
crypto_dir = crypto_workflow.calculate_crypto_result(
f"crypto_{template_id}.json"
)
output_dir = crypto_workflow.output_workflow_simple_shell(
f"cryptocat_{template_id}.json"
)
crypto_workflow.save_original_workflow(
f"original_workflow_{template_id}.json", crypto_dir
)
crypto_workflow.save_original_prompt(
f"original_prompt_{template_id}.json", crypto_dir
)
user_token, error_msg = AuthUnit().get_user_token()
if not user_token:
if error_msg == "no token found" or error_msg == "login failed":
AuthUnit().login_dialog("输出CryptoCat加密工作流,请先完成登录")
else:
PromptServer.instance.send_sync(
"cryptocat_toast",
{"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"},
)
return (ExecutionBlocker(None),)
upload = UploadWorkflow(user_token)
ret = upload.upload_workflow(template_id, crypto_dir)
if not ret:
print("crypto cat upload failed")
return (ExecutionBlocker(None),)
serial_numbers = upload.generate_serial_number(template_id, 10)
if serial_numbers:
sn_file_path = os.path.join(output_dir, f"serial_numbers_{template_id}.txt")
with open(sn_file_path, "w", encoding="utf-8") as f:
f.write("\n".join(serial_numbers))
PromptServer.instance.send_sync(
"cryptocat_toast",
{
"content": f"加密完成,在{sn_file_path}中查看序列号",
"type": "info",
"duration": 5000,
},
)
return (template_id,)
# 保存源文件,用于调试
with open(os.path.join(project_temp_folder, "prompt.json"), "w", encoding="utf-8") as f:
f.write(json.dumps(prompt, indent=4, ensure_ascii=False))
with open(os.path.join(project_temp_folder, "workflow.json"), "w", encoding="utf-8") as f:
f.write(json.dumps(extra_pnginfo, indent=4, ensure_ascii=False))
# 保存需要的输出文件
project_folder = os.path.join(crypto_folder, crypto_name)
if not os.path.exists(project_folder):
os.makedirs(project_folder)
hide_prompt_path = os.path.join(project_folder, "prompt.dat")
# 解析workflow
wt = WorkflowTrim(extra_pnginfo)
wt.trim_workflow()
show_workflow = wt.replace_workflow(hide_prompt_path)
with open(os.path.join(project_folder, "workflow.json"), "w", encoding="utf-8") as f:
f.write(json.dumps(show_workflow, indent=4, ensure_ascii=False))
# 拆分prompt
pr = PromptTrim(prompt)
show_part_prompt, hide_part_prompt = pr.split_prompt(wt.get_remaining_node_ids())
with open(os.path.join(project_temp_folder, "prompt_show.json"), "w", encoding="utf-8") as f:
f.write(json.dumps(show_part_prompt, indent=4, ensure_ascii=False))
FileCompressor.compress_to_json(hide_part_prompt, hide_prompt_path, "19040822")
return (hide_part_prompt,)
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
any = AnyType("*")
class ExcuteCryptoNode():
class SaveCryptoBridgeNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"crypto_file_path": ("STRING", {"default": folder_paths.output_directory}),
},
"optional": {
"input_anything" : ("*",),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "excute" #这个节点中的主函数
CATEGORY = "advanced/CryptoCat"
def excute(self, **kwargs):
batch_size = 1
height = 1024
width = 1024
color = 0xFF0000
r = torch.full([batch_size, height, width, 1], ((color >> 16) & 0xFF) / 0xFF)
g = torch.full([batch_size, height, width, 1], ((color >> 8) & 0xFF) / 0xFF)
b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF)
return (torch.cat((r, g, b), dim=-1), )
return {"required": {"value": (any,)}}
# 产生随机数
class RandomSeedNode():
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
},
"optional": {
},
"hidden": {
}
}
RETURN_TYPES = ("INT",)
FUNCTION = "random" #这个节点中的主函数
CATEGORY = "advanced/CryptoCat"
def VALIDATE_INPUTS(s, input_types):
return True
def IS_CHANGED():
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
def random(self):
# 产生随机数
return (random.randint(0, 999999), )
class CryptoCatImage(SaveImage):
RETURN_TYPES = (any,)
FUNCTION = "doit"
CATEGORY = "advanced/CryptoCat"
def doit(self, value):
return (value,)
def is_link(obj):
if not isinstance(obj, list):
return False
if len(obj) != 2:
return False
if not isinstance(obj[0], str):
return False
if (
not isinstance(obj[1], int)
and not isinstance(obj[1], float)
and not isinstance(obj[1], str)
):
return False
return True
class AlwaysEqualProxy(str):
def __eq__(self, _):
return True
def __ne__(self, _):
return False
class AlwaysTupleZero(tuple):
def __getitem__(self, _):
return AlwaysEqualProxy(super().__getitem__(0))
class DecodeCryptoNode:
def __init__(self):
super().__init__()
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", {"tooltip": "The images to save."}),
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."})
"template_id": ("STRING",),
"serial_number": (
"STRING",
{"multiline": True, "placeholder": "请输入序列号"},
),
},
"optional": {"input_anything": (any,)},
"hidden": {
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ()
FUNCTION = "save_images"
@classmethod
def VALIDATE_INPUTS(s, input_types):
return True
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
RETURN_TYPES = AlwaysTupleZero(AlwaysEqualProxy("*"))
FUNCTION = "decode"
CATEGORY = "advanced/CryptoCat"
DESCRIPTION = "Saves the input images to your ComfyUI output directory."
OUTPUT_NODE = False
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
return super().save_images(images, filename_prefix, None, None)
def decode(self, template_id, serial_number, **kwargs):
unique_id = kwargs.pop("unique_id", None)
prompt = kwargs.pop("prompt", None)
extra_pnginfo = kwargs.pop("extra_pnginfo", None)
decode_crypto_workflow = DecodeCryptoWorkflow(
prompt, extra_pnginfo["workflow"], template_id
)
crypto_prompt = decode_crypto_workflow.load_crypto_prompt(serial_number)
decode_crypto_workflow.calculate_input_anything_map()
processed_nodes = {}
graph = GraphBuilder()
def get_node_result(nodeData, id):
inputKeys = []
for ikey in nodeData["inputs"].keys():
input_value = nodeData["inputs"][ikey]
if (
is_link(input_value)
and decode_crypto_workflow.get_hidden_input(input_value) is None
and input_value[0] not in processed_nodes
):
inputKeys.append(input_value[0])
for ikey in inputKeys:
if ikey not in crypto_prompt:
continue
node = get_node_result(crypto_prompt[ikey], ikey)
processed_nodes[ikey] = node
inputs = nodeData["inputs"]
newInputs = {}
for ikey in inputs.keys():
if is_link(inputs[ikey]):
hidden_input_name = decode_crypto_workflow.get_hidden_input(
inputs[ikey]
)
if hidden_input_name:
if hidden_input_name in kwargs:
newInputs[ikey] = kwargs[hidden_input_name]
elif inputs[ikey][0] in processed_nodes:
newInputs[ikey] = processed_nodes[inputs[ikey][0]].out(
inputs[ikey][1]
)
else:
newInputs[ikey] = inputs[ikey]
return graph.node(nodeData["class_type"], id, **newInputs)
node_id, link_idx = decode_crypto_workflow.get_outputs()
nodeData = crypto_prompt[node_id]
node = get_node_result(nodeData, node_id)
value = node.out(link_idx)
return {"result": tuple([value]), "expand": graph.finalize()}
class RandomSeedNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {"required": {}, "optional": {}, "hidden": {}}
RETURN_TYPES = ("INT",)
FUNCTION = "random"
CATEGORY = "advanced/CryptoCat"
def IS_CHANGED():
return float("NaN")
def random(self):
return (random.randint(0, 999999),)
+71
View File
@@ -0,0 +1,71 @@
import torch
from nodes import SaveImage
import folder_paths
class ExcuteCryptoNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"crypto_file_path": (
"STRING",
{"default": folder_paths.output_directory},
)
},
"optional": {"input_anything": ("*",)},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "excute"
CATEGORY = "__hidden__"
def excute(self, **kwargs):
batch_size = 1
height = 1024
width = 1024
color = 16711680
r = torch.full([batch_size, height, width, 1], (color >> 16 & 255) / 255)
g = torch.full([batch_size, height, width, 1], (color >> 8 & 255) / 255)
b = torch.full([batch_size, height, width, 1], (color & 255) / 255)
return (torch.cat((r, g, b), dim=-1),)
class CryptoCatImage(SaveImage):
def __init__(self):
super().__init__()
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", {"tooltip": "The images to save."}),
"filename_prefix": (
"STRING",
{
"default": "ComfyUI",
"tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes.",
},
),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "save_images"
OUTPUT_NODE = True
CATEGORY = "__hidden__"
DESCRIPTION = "Saves the input images to your ComfyUI output directory."
def save_images(
self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None
):
return super().save_images(images, filename_prefix, None, None)
@@ -0,0 +1,258 @@
{
"last_node_id": 17,
"last_link_id": 21,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
-476,
301
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1,
13,
21
],
"slot_index": 0,
"label": "模型"
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5,
16
],
"slot_index": 1,
"label": "CLIP"
},
{
"name": "VAE",
"type": "VAE",
"links": [
8,
17
],
"slot_index": 2,
"label": "VAE"
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"1.5\\majicmixRealistic_v7.safetensors"
]
},
{
"id": 12,
"type": "DecodeCryptoNode",
"pos": [
267,
-157
],
"size": [
400,
162
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "input_anything",
"type": "*",
"link": 19,
"label": "input_anything",
"shape": 7
},
{
"name": "input_anything1",
"type": "*",
"link": 16,
"label": "input_anything1"
},
{
"name": "input_anything2",
"type": "*",
"link": 17,
"label": "input_anything2"
},
{
"name": "input_anything3",
"type": "*",
"link": 21,
"label": "input_anything3"
},
{
"name": "input_anything4",
"type": "*",
"link": null,
"label": "input_anything4"
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": [
18
],
"slot_index": 0,
"label": "*"
}
],
"properties": {
"Node name for S&R": "DecodeCryptoNode"
},
"widgets_values": [
"ffbb8ad2c5574cdfa702e9b08b0ccaa2",
""
]
},
{
"id": 14,
"type": "PreviewImage",
"pos": [
1698,
158
],
"size": [
210,
246
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 18,
"label": "图像"
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 17,
"type": "DF_Text",
"pos": [
-505,
20
],
"size": [
315,
58
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
19,
20
],
"slot_index": 0,
"label": "STRING"
}
],
"properties": {
"Node name for S&R": "DF_Text"
},
"widgets_values": [
""
]
}
],
"links": [
[
13,
4,
0,
12,
0,
"*"
],
[
16,
4,
1,
12,
1,
"*"
],
[
17,
4,
2,
12,
2,
"*"
],
[
18,
12,
0,
14,
0,
"IMAGE"
],
[
19,
17,
0,
12,
0,
"*"
],
[
21,
4,
0,
12,
3,
"*"
]
],
"config": {},
"extra": {
"ds": {
"scale": 1.167184107045001,
"offset": [
793.6073786214808,
404.0759897478937
]
}
},
"version": 0.4,
"widget_idx_map": {
"3": {
"sampler_name": 4,
"scheduler": 5
}
},
"seed_widgets": {}
}
+433 -342
View File
@@ -1,20 +1,234 @@
{
"last_node_id": 11,
"last_link_id": 16,
"last_node_id": 17,
"last_link_id": 21,
"nodes": [
{
"id": 3,
"type": "KSampler",
"pos": [
863,
186
],
"size": [
315,
446
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1,
"label": "模型"
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4,
"label": "正面条件"
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6,
"label": "负面条件"
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2,
"label": "Latent"
},
{
"name": "seed",
"type": "INT",
"link": 11,
"widget": {
"name": "seed"
},
"label": "随机种"
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0,
"label": "Latent"
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
665777626286280,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
-476,
301
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1,
13,
21
],
"slot_index": 0,
"label": "模型"
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5,
16
],
"slot_index": 1,
"label": "CLIP"
},
{
"name": "VAE",
"type": "VAE",
"links": [
8,
17
],
"slot_index": 2,
"label": "VAE"
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"1.5\\majicmixRealistic_v7.safetensors"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
473,
609
],
"size": [
315,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0,
"label": "Latent"
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
168,
200
],
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3,
"label": "CLIP"
},
{
"name": "text",
"type": "STRING",
"link": 20,
"widget": {
"name": "text"
},
"label": "文本"
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0,
"label": "条件"
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
true
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": {
"0": 413,
"1": 389
},
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"pos": [
413,
389
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 3,
"order": 4,
"mode": 0,
"inputs": [
{
@@ -43,203 +257,19 @@
true
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": {
"0": 473,
"1": 609
},
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0,
"label": "Latent"
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 3,
"type": "KSampler",
"pos": {
"0": 863,
"1": 186
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1,
"label": "模型"
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4,
"label": "正面条件"
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6,
"label": "负面条件"
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2,
"label": "Latent"
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0,
"label": "Latent"
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 9,
"type": "SaveImage",
"pos": {
"0": 1451,
"1": 189
},
"size": {
"0": 210,
"1": 58
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9,
"label": "图像"
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveImage"
},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": {
"0": 26,
"1": 474
},
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1,
14
],
"slot_index": 0,
"label": "模型"
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5,
15
],
"slot_index": 1,
"label": "CLIP"
},
{
"name": "VAE",
"type": "VAE",
"links": [
8,
16
],
"slot_index": 2,
"label": "VAE"
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"1.5\\majicmixRealistic_v7.safetensors"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": {
"0": 1209,
"1": 188
},
"size": {
"0": 210,
"1": 46
},
"pos": [
1209,
188
],
"size": [
210,
46
],
"flags": {},
"order": 6,
"order": 8,
"mode": 0,
"inputs": [
{
@@ -260,8 +290,7 @@
"name": "IMAGE",
"type": "IMAGE",
"links": [
9,
11
14
],
"slot_index": 0,
"label": "图像"
@@ -269,139 +298,85 @@
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": {
"0": 314,
"1": 93
},
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3,
"label": "CLIP"
},
{
"name": "text",
"type": "STRING",
"link": 12,
"widget": {
"name": "text"
},
"label": "文本"
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0,
"label": "条件"
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
true
]
"widgets_values": []
},
{
"id": 11,
"type": "Text Multiline",
"pos": {
"0": -88,
"1": -194
},
"size": {
"0": 400,
"1": 200
},
"type": "RandomSeedNode",
"pos": [
509,
64
],
"size": [
210,
26
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"name": "INT",
"type": "INT",
"links": [
12,
13
11
],
"shape": 3,
"label": "字符串",
"slot_index": 0
"slot_index": 0,
"label": "INT"
}
],
"properties": {
"Node name for S&R": "Text Multiline"
"Node name for S&R": "RandomSeedNode"
},
"widgets_values": [
"",
true
]
"widgets_values": []
},
{
"id": 10,
"id": 12,
"type": "SaveCryptoNode",
"pos": {
"0": 562,
"1": -298
},
"pos": [
267,
-157
],
"size": [
515.0016206873283,
223.84233134141914
400,
162
],
"flags": {},
"order": 8,
"order": 5,
"mode": 0,
"inputs": [
{
"name": "output_images",
"type": "IMAGE",
"link": 11,
"label": "output_images"
"name": "input_anything",
"type": "*",
"link": 19,
"label": "input_anything",
"shape": 7
},
{
"name": "input_anything1",
"type": "*",
"link": 13
"link": 16,
"label": "input_anything1"
},
{
"name": "input_anything2",
"type": "*",
"link": 14
"link": 17,
"label": "input_anything2"
},
{
"name": "input_anything3",
"type": "*",
"link": 15
"link": 21,
"label": "input_anything3"
},
{
"name": "input_anything4",
"type": "*",
"link": 16
},
{
"name": "input_anything5",
"type": "*",
"link": null
"link": null,
"label": "input_anything4"
}
],
"outputs": [],
@@ -409,8 +384,108 @@
"Node name for S&R": "SaveCryptoNode"
},
"widgets_values": [
"D:\\work\\ComfyUI\\ComfyUI\\output",
"demo"
"ffbb8ad2c5574cdfa702e9b08b0ccaa2",
null
]
},
{
"id": 13,
"type": "SaveCryptoBridgeNode",
"pos": [
1467,
199
],
"size": [
189.17031860351562,
26
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "value",
"type": "*",
"link": 14,
"label": "value"
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": [
18
],
"slot_index": 0,
"label": "*"
}
],
"properties": {
"Node name for S&R": "SaveCryptoBridgeNode"
},
"widgets_values": []
},
{
"id": 14,
"type": "PreviewImage",
"pos": [
1698,
158
],
"size": [
210,
246
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 18,
"label": "图像"
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 17,
"type": "DF_Text",
"pos": [
-505,
20
],
"size": [
315,
58
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
19,
20
],
"slot_index": 0,
"label": "STRING"
}
],
"properties": {
"Node name for S&R": "DF_Text"
},
"widgets_values": [
""
]
}
],
@@ -479,71 +554,87 @@
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
11,
8,
11,
0,
10,
0,
"IMAGE"
3,
4,
"INT"
],
[
13,
4,
0,
12,
11,
0,
"*"
],
[
14,
8,
0,
13,
0,
"*"
],
[
16,
4,
1,
12,
1,
"*"
],
[
17,
4,
2,
12,
2,
"*"
],
[
18,
13,
0,
14,
0,
"IMAGE"
],
[
19,
17,
0,
12,
0,
"*"
],
[
20,
17,
0,
6,
1,
"STRING"
],
[
13,
11,
0,
10,
1,
"*"
],
[
14,
21,
4,
0,
10,
2,
"*"
],
[
15,
4,
1,
10,
12,
3,
"*"
],
[
16,
4,
2,
10,
4,
"*"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8769226950000005,
"scale": 1.167184107045001,
"offset": [
772.3852019447384,
464.1369668451785
793.6073786214808,
404.0759897478937
]
}
},
@@ -0,0 +1,10 @@
FM5bb7MgAIDhf9SIfF8zLpt6ghScxgNwsyh2BeepnVbl1y9ev3mT574TXYfKxIbg3GLADP7FQ/pfXfEZ/0y8uBJ0uu/EkSVY6zBwZIEmecRe9qLH/0SZ7MLmgNm0laHUzF52mXWGetrQkLWi9HdsVqNc/VZD+q36YJEhmjlkneo7XQdoaUpgJMcH4slcBiqYrzQqYOXOQ+0SePyS6xW348Yshcy7bDR7bLcrmQRPTNz6kFoB40zY2AvQKfocnGYZX/4DkSZ/0oWTr+oyjx+3l4kqUGSpzs+JfruZ8xcAAP//
FM7djqowFEDhNzJykJNwafjp7M60jYpC980kVrQtVIkzaNunn3C9spKvD1SfiTLCUDhGSLiBH7jvM1XAfxim7lTQfNUHusY2eZ9JvcZTPuESHTrpYCPbXZDxmPC4t0hQ87gN2IyGldowwq1sqwDmbdQ//VL3/VW5ekaS/3YpH5Ub9bnO50ubGOxgQQyKZCm3tWcfQ9Y39MWbnV9+7PQb7MPzyFJebj1rbv6roJPsdkbYKmVRpqKRUZR1vlLPw0OMB7xUuLk9+41183d1CENdZnAN4qanT9sXhZ9P8BcAAP//
FMvRkqogGADgN2oyT6f1skQZ3AU2x1K4W8mFnzQtLYWn3+n6m69xmamxAg4ZOXkSMCAjueVbFZP/5DpU5ziLVo3L1rIM5hqna3mOBvnGTnaiI/9EeXTCnwLmcyuxNMzvnSxaoMgAxcyKMnEEZlAb81K3/Fd16VPiaKpC1qquNXUaPS9lALIiwIFslL+ARHrLkVhEcehZmWzfX1ZmJrZfmKchQ/uFFnr5irNBVEfgNgmpFyEvhOcojVat4w+9e9n1HVOfTB9jcb0fGv3txuBH75oe2UecT59n3Yu/AAAA//8=
FMvRbqsgAADQP2pqrTfl0auFwgauhirwslTWClaqy3QoX7/0+eTcVmIapG1hCb4EHDGLf/CzTHSG/+HHKKqMgM1tJVtVR75BcKsqMKoXOuWkw3tZn1cZLhELZaeQMiykq+K9pbmxFLFO1scVW2/1zvzqZ3nXDs4KgUnErNeuNw0E81cdWSWwLSz2FLVechJRMeyurgrXegyvr4TxuBsWFmjM8nShvF3eMzJKcbZFd4xpkHHBZShyCDb0zveLZlk/L3w66OT/4wTT6oPLZJBve3Bqvj+nCfq2rQ5/AQAA//8=
FMvRboIwFADQL5oRGCY8OpDm4mgDorT3ZQlV14sW2dRB+/WLzyfn5ArTMU2CCth7CDjBHYY61ims4DLKQ1oki5MrltgGU8fyJR6SEV9o0SoL76qtnPL7gPu6R4aG+7XD5kplZqhkvFftxgFNpEPzp4f6rG3+RJY8ZMSv2l5NlyfPYxsQSiBBEKswDnG4eB599MreZh3m4eujNBP0t5n7MuLZei6b7/kzLUYlKxL9Jiq9ikSjvMjyZDHcz/IX60audlv1aMURxurgNlmeOrZtup/lGcnu3uKvqfoPAAD//w==
FM7dboIwHEDxNzIC6tJLB7ZrsxYlfPV/Y6Q6WkYVNxm0T79wfXKS380x3RBlUsNo4WkgDP2l92yrYrqj30Ndxgytbo6toQqmhuA1lGiAJVqw0tKNrE5O+iIQPuuAgBZ+7yDvDU+04UR0sjo4aiajQv2n7tmXsngEgl51JHple91gNF6rwEBNF8TjEmKTkuApff9srH6pBLrlh1pPtHvMwvNIJPuZ5+38GbNB1ieTdoeIexmlufRpgtHqSEoSXjk0m+NIwg+zO6Nz67eavF2muHhXeR4zJd2Pxe1/AAAA//8=
FM7fbqsgAIDxN2pq6TkNlw1UBpmYOv8Al6IrMHE21So8/eL1ly/59YGZlmibW0arSBNu6YuOxT+N6H/6M4kaMXjoAzuqJllbkh5VDSe1R6+89PQsm3uQsUp4LJwiyvB4DaocbIaNzQh3srkFalerT+atx+Jb+3RRBM4C8EH7wbQpXLomsUrQHXHmzgQNHsdeGNOdhncr2Lr/SpiVut+NxwxwfN2y8rF9IjZJcbe5u4EsSpCXMuY4hQeMr0/oEPoYib7MNWfVq5sHD/DS3WbgwWYQ+ro80dlUfwEAAP//
FM7bboIwAIDhNzIi06yXzgJrtxYlaA83SyjYwyiSOIT26Reu//zJ1wVsmkLZ0mJ0jSihFj3RUO3VCR3Q78hvJww2XcBbyZK5KfKtvIFRrtFLLzx6E+wSRLwmNFZOFtLQeAyy7i2BxpKCOsGygOxs1c681FDdlc8nWYA/ntJe+d40OZhalljJ0Yp4kpjtOmhCN2RBMRRb38b1l9zMyD0WGklK4XEhtV6+T3gU/GJLl6UkirSsRSxhDjaUYTXqsz5/7nvHJwbemdK11q5m95/z1wvCBT/A8fJxyP4DAAD//w==
HM7dboMgHEDxJ1pTStuFy6ZWBhmYmlbkf7NMZgUV6z6cwtMv3fXJSX514LaixmWOs2tkSDr2zYZ8Z45sz7qxLI6crOrA16DQXNF0DQUZ4RE9eO3ZVqtz0PGKZMxboGBlPAS49E4k1gkqW61OgbnZmY39NUN+Mz6dgJKfEsve+N5WKZk+FHJQsgeik0OxEy/dRvodlqofKiz+fyjtzNr7IqPAMjks4tIsr0c+6vLssvaERdQ4u+iYJSlZvdXvk90D588dqVvUfG7lF4LC32uVb5OTZWJZ+NMtDLT5CwAA//8=
FMvbcuogFADQP3KMUc/hMY0J3bRAdaIB3gpe2DFYO14S+PqOz2vWITJvqUOJDLYJMoFwg8tm4UpYwvmqdiUjk0NkU9Nmg6X11OzI1bwwmKADzHW7jjptM5E2naHGi1RE0/TIVx45FZ1uqwg4oJv5p7tsji7UD0PJXeWid6H3tiaPfZuhUYASYe4CH3TbR0MXF/leDd90nL2+UX6A7mcUiediVYy8OY2fJbtqtUbZVTlPOpeNTnJVk8lvY9n8a//2Lxf+zo9Rnj+eXVWcCU6XqjwVaNn/I9R2V93+AgAA//8=
+13 -11
View File
@@ -2,31 +2,33 @@ import json
import zlib
import os
class FileCompressor:
class FileCompressor:
@staticmethod
def obfuscate(data, password):
return bytes([b ^ ord(password[i % len(password)]) for i, b in enumerate(data)])
return bytes(
[b ^ ord(password[i % len(password)]) for (i, b) in enumerate(data)]
)
@staticmethod
def compress_string(string_data, zip_path, password):
compressed_data = zlib.compress(string_data.encode('utf-8'))
compressed_data = zlib.compress(string_data.encode("utf-8"))
obfuscated_data = FileCompressor.obfuscate(compressed_data, password)
with open(zip_path, 'wb') as zipf:
with open(zip_path, "wb") as zipf:
zipf.write(obfuscated_data)
@staticmethod
def decompress_to_string(zip_path, password):
with open(zip_path, 'rb') as zipf:
with open(zip_path, "rb") as zipf:
obfuscated_data = zipf.read()
compressed_data = FileCompressor.obfuscate(obfuscated_data, password) # 反混淆
return zlib.decompress(compressed_data).decode('utf-8')
compressed_data = FileCompressor.obfuscate(obfuscated_data, password)
return zlib.decompress(compressed_data).decode("utf-8")
@staticmethod
def compress_to_json(data, zip_path, password):
try:
json_string = json.dumps(data, indent=4, ensure_ascii=False) # Convert data to JSON string
FileCompressor.compress_string(json_string, zip_path, password) # Call compress_string
json_string = json.dumps(data, indent=4, ensure_ascii=False)
FileCompressor.compress_string(json_string, zip_path, password)
except Exception as e:
print(f"Compression failed: {e}")
@@ -35,7 +37,7 @@ class FileCompressor:
try:
if not os.path.exists(zip_path):
raise FileNotFoundError(f"{zip_path} does not exist")
json_string = FileCompressor.decompress_to_string(zip_path, password) # Call decompress_to_string
return json.loads(json_string) # Convert back to dictionary
json_string = FileCompressor.decompress_to_string(zip_path, password)
return json.loads(json_string)
except Exception as e:
print(f"Decompression failed: {e}")
+26
View File
@@ -0,0 +1,26 @@
@echo off
set "requirements_txt=%~dp0\requirements.txt"
set "requirements_repair_txt=%~dp0\repair_dependency_list.txt"
set "python_exec=..\..\..\python_embeded\python.exe"
set "aki_python_exec=..\..\python\python.exe"
echo Installing EasyUse Requirements...
if exist "%python_exec%" (
echo Installing with ComfyUI Portable
"%python_exec%" -s -m pip install -r "%requirements_txt%"
)^
else if exist "%aki_python_exec%" (
echo Installing with ComfyUI Aki
"%aki_python_exec%" -s -m pip install -r "%requirements_txt%"
for /f "delims=" %%i in (%requirements_repair_txt%) do (
%aki_python_exec% -s -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple "%%i"
)
)^
else (
echo Installing with system Python
pip install -r "%requirements_txt%"
)
pause
+1
View File
@@ -0,0 +1 @@
import{api}from"../../../scripts/api.js";import{ComfyApp,app}from"../../../scripts/app.js";export const UserTokenKey="cryptocat_user_token";async function showKeygenMessageBox(e){const t=document.createElement("div");t.style.position="fixed",t.style.top="0",t.style.left="0",t.style.width="100%",t.style.height="100%",t.style.backgroundColor="rgba(0, 0, 0, 0.5)",t.style.zIndex="9999",t.style.display="flex",t.style.justifyContent="center",t.style.alignItems="center";const o=document.createElement("div");o.style.backgroundColor="#fff",o.style.padding="20px",o.style.borderRadius="10px",o.style.width="400px",o.style.boxShadow="0 4px 8px rgba(0, 0, 0, 0.2)",o.style.display="flex",o.style.flexDirection="column",o.style.alignItems="center";const n=document.createElement("h2");n.innerText=e,n.style.color="#333",n.style.marginBottom="20px",o.appendChild(n);const r=document.createElement("input");r.type="text",r.placeholder="请输入template_id",r.style.width="100%",r.style.padding="10px",r.style.marginBottom="20px",r.style.border="1px solid #ccc",r.style.borderRadius="4px",r.style.fontSize="16px",o.appendChild(r);const s=document.createElement("div");s.style.marginBottom="20px",s.style.width="100%",s.style.backgroundColor="#f9f9f9",s.style.border="1px solid #ddd",s.style.borderRadius="4px",s.style.padding="10px",s.style.fontSize="14px",s.style.color="#333",s.style.wordWrap="break-word",s.style.wordBreak="break-all",s.style.minHeight="50px",s.innerText="生成的序列号: ",o.appendChild(s);const i=document.createElement("button");function a(e){i.disabled=e,i.innerText=e?"生成中...":"生成序列号",e&&(s.innerText="正在获取序列号...")}i.innerText="生成序列号",i.style.padding="10px 20px",i.style.border="none",i.style.backgroundColor="#4CAF50",i.style.color="white",i.style.borderRadius="4px",i.style.cursor="pointer",i.style.fontSize="16px",i.style.alignSelf="center",i.addEventListener("click",(async()=>{a(!0);try{const e=r.value.trim(),t=await async function(e){try{const t=await api.fetchApi("/cryptocat/keygen",{method:"POST",body:JSON.stringify({template_id:e})}),o=await t.json();if(!t.ok)throw new Error(o.error_msg||`HTTP error! status: ${t.status}`);return o.serial_number||o.error_msg||"获取失败:服务器返回数据格式错误"}catch(e){throw new Error(`获取序列号失败: ${e.message}`)}}(e);s.innerText=`${t}`}catch(e){s.innerText=`发生错误: ${e.message||"未知错误"}`}finally{a(!1)}})),o.appendChild(i),t.appendChild(o),document.body.appendChild(t),t.addEventListener("click",(function(e){e.target===t&&t.remove()}))}function isValidJWTFormat(e){if("string"!=typeof e)return!1;if(e.length<50)return!1;const t=e.split(".");if(3!==t.length)return!1;const o=/^[A-Za-z0-9_-]+$/;return t.every((e=>e.length>0&&o.test(e)))}app.registerExtension({name:"CryptoCat.config",async setup(){app.ui.settings.addSetting({id:"CryptoCat.Keygen.name",name:"输入template_id,计算序列号,序列号有效期7天",type:()=>{const e=document.createElement("tr"),t=document.createElement("td"),o=document.createElement("input");return o.type="button",o.value="算号器",o.style.borderRadius="8px",o.style.padding="8px 16px",o.style.fontSize="14px",o.style.cursor="pointer",o.style.border="1px solid #666",o.style.backgroundColor="#444",o.style.color="#fff",o.onmouseover=()=>{o.style.backgroundColor="#555"},o.onmouseout=()=>{o.style.backgroundColor="#444"},o.onclick=()=>{showKeygenMessageBox("算号器","算号器","question")},t.appendChild(o),e.appendChild(t),e}}),app.ui.settings.addSetting({id:"CryptoCat.User.logout",name:"登出当前用户",type:()=>{const e=document.createElement("tr"),t=document.createElement("td"),o=document.createElement("input");return o.type="button",o.value="登出",o.style.borderRadius="8px",o.style.padding="8px 16px",o.style.fontSize="14px",o.style.cursor="pointer",o.style.border="1px solid #666",o.style.backgroundColor="#444",o.style.color="#fff",o.onclick=async()=>{localStorage.setItem(UserTokenKey,""),window.clearUserInfo(),await api.fetchApi("/cryptocat/logout")},t.appendChild(o),e.appendChild(t),e}}),app.ui.settings.addSetting({id:"CryptoCat.User.long_token",name:"设置长效token",type:"text",textType:"password",defaultValue:"",tooltip:"用于非本机授权登录情况,请勿泄露!提倡使用本机登录授权更安全!",onChange:async function(e){!isValidJWTFormat(e)&&e||await api.fetchApi("/cryptocat/set_long_token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({long_token:e})})}}),app.ui.settings.addSetting({id:"CryptoCat.Setting.auto_overwrite",name:"自动覆盖更新同名工作流",type:"boolean",defaultValue:!1,tooltip:"设置为true时,会自动覆盖已有的template_id的数据",onChange:function(e){api.fetchApi("/cryptocat/set_auto_overwrite",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({auto_overwrite:e})})}})}});
+1 -73
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -3,13 +3,13 @@ name = "comfyui_cryptocat"
description = "a lightweight open-source node for ComfyUI, designed to simplify workflows while providing encryption protection for them."
version = "1.0.0"
license = {file = "LICENSE"}
dependencies = ["torch", "aiohttp"]
dependencies = ["pyzipper", "aiohttp", "websockets"]
[project.urls]
Repository = "https://github.com/RiceRound/ComfyUI_CryptoCat"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = ""
PublisherId = "RiceRound"
DisplayName = "ComfyUI_CryptoCat"
Icon = ""
Icon = "https://www.riceround.online/CryptoCat.svg"
+2 -1
View File
@@ -1,2 +1,3 @@
torch
aiohttp
pyzipper
websockets
File diff suppressed because one or more lines are too long
+318 -330
View File
@@ -1,348 +1,336 @@
from collections import defaultdict
import copy
import os
import random
import sys
from .updown_workflow import DownloadWorkflow
import folder_paths
import json
from.file_compressor import FileCompressor
from .file_compressor import FileCompressor
from typing import Dict, Any
from .auth_unit import AuthUnit
class WorkflowTrim():
def __init__(self, workflow):
if 'workflow' in workflow :
self.workflow = copy.deepcopy(workflow['workflow'])
class CryptoWorkflow:
def __init__(self, workflow, prompt, template_id):
self.workflow = copy.deepcopy(workflow)
self.prompt = copy.deepcopy(prompt)
self.template_id = template_id
self.workflow_nodes_dict = {}
self.link_owner_map = defaultdict(dict)
self.node_prompt_map = {}
self.last_node_id = 0
self.last_link_id = 0
self.save_crypto_node_id = 0
self.crypto_bridge_node_id = 0
self.input_nodes_ids = set()
self.output_nodes_ids = set()
self.crypto_nodes_ids = set()
self.crypto_result = {}
def invalid_workflow(self):
for node in self.workflow["nodes"]:
node_type = node.get("type", "")
if node_type == "SaveCryptoBridgeNode":
if self.crypto_bridge_node_id == 0:
self.crypto_bridge_node_id = int(node["id"])
else:
raise ValueError(
"Error: Multiple 'SaveCryptoBridgeNode' instances found."
)
continue
elif node_type == "SaveCryptoNode":
if self.save_crypto_node_id == 0:
self.save_crypto_node_id = int(node["id"])
else:
raise ValueError(
"Error: Multiple 'SaveCryptoNode' instances found."
)
continue
if self.save_crypto_node_id == 0:
raise ValueError("Error: No 'SaveCryptoNode' instances found.")
if self.crypto_bridge_node_id == 0:
raise ValueError("Error: No 'SaveCryptoBridgeNode' instances found.")
def load_workflow(self):
simplify_workflow = self.workflow
self.workflow_nodes_dict = {
int(node["id"]): node for node in simplify_workflow["nodes"]
}
for node in simplify_workflow["nodes"]:
output_nodes = node.get("outputs", [])
if not output_nodes:
continue
for output in output_nodes:
links = output.get("links", [])
if not links:
continue
for link in links:
link = int(link)
self.link_owner_map[link]["links"] = copy.deepcopy(links)
self.link_owner_map[link]["slot_index"] = output.get(
"slot_index", 0
)
self.link_owner_map[link]["owner_id"] = int(node["id"])
self.link_owner_map[link]["type"] = output.get("type", "")
self.last_node_id = int(simplify_workflow.get("last_node_id", 0))
self.last_link_id = int(simplify_workflow.get("last_link_id", 0))
def load_prompt(self):
simplify_prompt = self.prompt
self.node_prompt_map = {
int(node_id): node for (node_id, node) in simplify_prompt.items()
}
def analysis_node(self):
self.input_nodes_ids.clear()
self.output_nodes_ids.clear()
self.crypto_nodes_ids.clear()
def find_input_nodes(node_id, visited=None):
if visited is None:
visited = set()
if node_id in visited:
return
visited.add(node_id)
self.input_nodes_ids.add(node_id)
node = self.workflow_nodes_dict.get(node_id)
if not node:
return
for input_node in node.get("inputs", []):
input_link = input_node.get("link")
if input_link is not None and input_link in self.link_owner_map:
owner_id = self.link_owner_map[input_link]["owner_id"]
find_input_nodes(owner_id, visited)
for input_node in self.workflow_nodes_dict[self.save_crypto_node_id].get(
"inputs", []
):
if input_node["name"] and input_node["name"].startswith("input_anything"):
input_link = input_node["link"]
if input_link is not None and input_link in self.link_owner_map:
owner_id = self.link_owner_map[input_link]["owner_id"]
find_input_nodes(owner_id)
def find_output_nodes(node_id, visited=None):
if visited is None:
visited = set()
if node_id in visited:
return
visited.add(node_id)
self.output_nodes_ids.add(node_id)
node = self.workflow_nodes_dict.get(node_id)
if not node:
return
for output in node.get("outputs", []):
for link in output.get("links", []):
if link is not None:
for workflow_link in self.workflow.get("links", []):
if workflow_link[0] == link:
target_node_id = workflow_link[3]
find_output_nodes(target_node_id, visited)
output_links = set()
for output_node in self.workflow_nodes_dict[self.crypto_bridge_node_id].get(
"outputs", []
):
_links = output_node["links"]
output_links.update(_links)
for link in self.workflow.get("links", []):
if len(link) > 3 and link[0] in output_links:
find_output_nodes(link[3])
self.crypto_nodes_ids = (
self.workflow_nodes_dict.keys()
- self.input_nodes_ids
- self.output_nodes_ids
)
self.crypto_nodes_ids = self.crypto_nodes_ids - {
self.save_crypto_node_id,
self.crypto_bridge_node_id,
}
def calculate_crypto_result(self, crypto_file_name):
self.crypto_result = {"prompt": {}, "workflow": {}, "outputs": []}
for node_id in self.crypto_nodes_ids:
if node_id in self.node_prompt_map:
self.crypto_result["prompt"][node_id] = self.node_prompt_map[node_id]
if node_id in self.workflow_nodes_dict:
self.crypto_result["workflow"][node_id] = self.workflow_nodes_dict[
node_id
]
crypto_bridge_node = self.node_prompt_map[self.crypto_bridge_node_id]
for input_name, input_value in crypto_bridge_node.get("inputs", {}).items():
if isinstance(input_value, list) and len(input_value) == 2:
self.crypto_result["outputs"] = input_value[0], input_value[1]
json_result = json.dumps(self.crypto_result, indent=4, ensure_ascii=False)
save_dir = folder_paths.temp_directory
with open(os.path.join(save_dir, crypto_file_name), "w", encoding="utf-8") as f:
f.write(json_result)
return save_dir
def output_workflow_simple_shell(self, output_workflow_name):
simplify_workflow = copy.deepcopy(self.workflow)
save_crypto_node = None
crypto_bridge_node = None
for node in simplify_workflow["nodes"]:
if node["id"] == self.save_crypto_node_id:
save_crypto_node = node
if node["id"] == self.crypto_bridge_node_id:
crypto_bridge_node = node
except_nodes_ids = self.crypto_nodes_ids
except_nodes_ids.add(self.crypto_bridge_node_id)
simplify_workflow["nodes"] = [
node
for node in simplify_workflow["nodes"]
if int(node["id"]) not in except_nodes_ids
]
if save_crypto_node is None:
raise ValueError("SaveCryptoNode not found in workflow")
if crypto_bridge_node is None:
raise ValueError("CryptoBridgeNode not found in workflow")
save_crypto_node["type"] = "DecodeCryptoNode"
save_crypto_node["widgets_values"] = (
[save_crypto_node.get("widgets_values", [None])[0]]
if "widgets_values" in save_crypto_node
else [None]
)
save_crypto_node["widgets_values"].append("")
save_crypto_node["properties"] = {"Node name for S&R": "DecodeCryptoNode"}
if "outputs" not in crypto_bridge_node:
save_crypto_node["outputs"] = []
else:
self.workflow = copy.deepcopy(workflow)
save_crypto_node["outputs"] = copy.deepcopy(crypto_bridge_node["outputs"])
output_nodes_ids = [int(node["id"]) for node in simplify_workflow["nodes"]]
filtered_links = []
for link in simplify_workflow["links"]:
if len(link) < 5:
continue
if link[1] == self.crypto_bridge_node_id:
link[1] = self.save_crypto_node_id
if link[1] in output_nodes_ids and link[3] in output_nodes_ids:
filtered_links.append(link)
else:
pass
simplify_workflow["links"] = filtered_links
simplify_workflow.pop("groups", None)
save_dir = folder_paths.output_directory
with open(
os.path.join(folder_paths.output_directory, output_workflow_name),
"w",
encoding="utf-8",
) as f:
json.dump(simplify_workflow, f, ensure_ascii=False, indent=4)
return save_dir
self.output_images_link = 0
def save_original_workflow(self, output_workflow_name, save_dir):
with open(
os.path.join(save_dir, output_workflow_name), "w", encoding="utf-8"
) as f:
json.dump(self.workflow, f, ensure_ascii=False, indent=4)
return save_dir
nodes_dict = {node['id']: node for node in self.workflow['nodes']}
self.max_id_index = max(nodes_dict.keys()) + 1
def save_original_prompt(self, output_prompt_name, save_dir):
with open(
os.path.join(save_dir, output_prompt_name), "w", encoding="utf-8"
) as f:
json.dump(self.prompt, f, ensure_ascii=False, indent=4)
return save_dir
class DecodeCryptoWorkflow:
def __init__(self, prompt, workflow, template_id):
self.prompt = prompt
self.workflow = workflow
self.template_id = template_id
self.crypto_result = {}
self.input_anything_map = {}
def calculate_input_anything_map(self):
self.input_anything_map.clear()
for node_id, node in self.prompt.items():
if node.get("class_type") == "DecodeCryptoNode":
for input_name, input_value in node.get("inputs", {}).items():
if input_name.startswith("input_anything"):
if isinstance(input_value, list) and len(input_value) == 2:
input_link_key = f"{input_value[0]}_{input_value[1]}"
self.input_anything_map[input_link_key] = input_name
return self.input_anything_map
def load_crypto_prompt(self, serial_number_token, user_token=None):
if not user_token:
user_token = AuthUnit().read_user_token()
content = DownloadWorkflow().download_workflow(
self.template_id, serial_number_token, user_token
)
if not isinstance(content, str) or content == "":
raise ValueError("failed to download workflow")
self.crypto_result = json.loads(content)
return self.crypto_result["prompt"]
def get_hidden_input(self, input_value):
if isinstance(input_value, list) and len(input_value) == 2:
key = f"{input_value[0]}_{input_value[1]}"
return self.input_anything_map.get(key, None)
def get_outputs(self):
return self.crypto_result["outputs"]
class WorkflowTrimHandler:
@staticmethod
def onprompt_handler(json_data: Dict[str, Any]) -> Dict[str, Any]:
prompt = json_data["prompt"]
has_new_component = False
has_old_component = False
for node in prompt.values():
class_type = node.get("class_type")
if class_type == "SaveCryptoNode":
has_new_component = True
break
if class_type == "ExcuteCryptoNode":
has_old_component = True
break
if has_new_component:
user_token, error_msg = AuthUnit().get_user_token()
if not user_token and (
error_msg == "no token found" or error_msg == "login result error"
):
AuthUnit().login_dialog("输出CryptoCat加密工作流,请先完成登录")
json_data["prompt"] = {}
elif has_old_component:
prompt = WorkflowTrimHandler.replace_prompt(prompt)
json_data["prompt"] = prompt
return json_data
@staticmethod
def find_workflow_related_nodes(nodes, input_ids):
found_ids = set()
stack = list(input_ids)
while stack:
link_id = stack.pop()
for node_id, node in nodes.items():
outputs = node.get('outputs', None)
if not outputs:
continue
for output in outputs:
links = output.get('links', None)
if not links:
continue
if link_id in links:
if node_id not in found_ids:
found_ids.add(node_id)
inputs = node.get('inputs', [])
for input_node in inputs:
link_id = input_node.get('link')
if link_id is not None:
stack.append(link_id)
break
return found_ids
def trim_workflow(self):
if not self.workflow:
def replace_prompt(prompt: Dict[str, Any]) -> Dict[str, Any]:
if not prompt:
raise ValueError("Invalid JSON format.")
nodes_dict = {node['id']: node for node in self.workflow['nodes']}
save_crypto_node_ids = [node_id for node_id, details in nodes_dict.items() if details.get('type') == 'SaveCryptoNode']
if len(save_crypto_node_ids) == 1:
save_crypto_node_id = save_crypto_node_ids[0]
# 获取所有以 'input_anything' 开头的输入链接
input_ids = set()
for input_node in nodes_dict[save_crypto_node_id].get('inputs', []):
if input_node['name'] and input_node['name'].startswith('input_anything'):
input_ids.add(input_node['link'])
if input_node['name'] and input_node['name'] == "output_images":
self.output_images_link = input_node['link']
# 只处理有效的链接 ID
input_ids = {link_id for link_id in input_ids if link_id is not None}
# 找到与输入链接相关的节点
related_node_ids = self.find_workflow_related_nodes(nodes_dict, input_ids)
# 添加 SaveCryptoNode 本身
related_node_ids.add(save_crypto_node_id)
# # 打印和删除不相关的节点
# for node_id in nodes_dict.keys():
# if node_id not in related_node_ids:
# print(f"Removing node ID: {node_id} - {nodes_dict[node_id]}")
# 更新节点和链接
self.workflow['nodes'] = [details for node_id, details in nodes_dict.items() if node_id in related_node_ids]
remaining_node_ids = {node['id'] for node in self.workflow['nodes']}
self.workflow.pop('groups', None)
self.workflow['output_images_id'] = self.output_images_link
# 保留包含任一节点 ID 的链接
self.workflow['links'] = [
link for link in self.workflow['links']
if link[1] in remaining_node_ids and link[3] in remaining_node_ids
]
return self.workflow
elif len(save_crypto_node_ids) > 1:
raise ValueError("Error: Multiple 'SaveCryptoNode' instances found.")
else:
raise ValueError("Error: No 'SaveCryptoNode' instances found.")
def replace_workflow(self,hide_prompt_path):
if not self.workflow:
raise ValueError("Invalid JSON format.")
nodes_dict = {node['id']: node for node in self.workflow['nodes']}
output_images_link = None
# 替换 SaveCryptoNode 为 ExcuteCryptoNode
for node in self.workflow['nodes']:
if node.get('type') == 'SaveCryptoNode':
node['type'] = 'ExcuteCryptoNode'
node['properties']['Node name for S&R'] = 'ExcuteCryptoNode'
# 移除 output_images 输入并获取 link
for inp in node['inputs']:
if inp['name'] == 'output_images':
output_images_link = inp.get('link')
node['inputs'] = [inp for inp in node['inputs'] if inp['name'] != 'output_images']
node['widgets_values'] = [hide_prompt_path]
# 找到 ExcuteCryptoNode 的位置
execute_node = next((node for node in self.workflow['nodes'] if node.get('type') == 'ExcuteCryptoNode'), None)
if not execute_node:
raise ValueError("No 'ExcuteCryptoNode' instance found.")
# 创建新的 CryptoCatImage 节点
execute_node_pos = execute_node['pos']
self.max_id_index = max(max(nodes_dict.keys()) + 1, self.max_id_index)
new_save_image_node = {
"id": self.max_id_index, # 新节点 ID,确保唯一
"type": "CryptoCatImage",
"size": {
"0": 210,
"1": 162
},
"flags": {},
"order": 50,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": output_images_link, # 使用获取的 link
"label": "images"
}
],
"outputs": [], # 新节点没有输出
"properties": {
"Node name for S&R": "CryptoCatImage"
}
}
# 将新节点添加到节点列表
self.workflow['nodes'].append(new_save_image_node)
# 删除原有的链接以避免冲突
self.workflow['links'] = [
link for link in self.workflow['links'] if link[0] != output_images_link
]
# 用新的 outputs 替换 ExcuteCryptoNode 的 outputs
execute_node['outputs'] = [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [output_images_link], # 连接到新节点
"shape": 3,
"label": "IMAGE",
"slot_index": 0
}
]
# 添加链接信息
self.workflow['links'].append([
output_images_link, # ExcuteCryptoNode 的输出 ID
execute_node['id'], # 新节点 ID
0, # 新节点的输入索引
new_save_image_node['id'], # ExcuteCryptoNode ID(旧节点 ID)
0, # 旧节点的输出索引
"IMAGE" # 数据类型
])
return self.workflow
def set_excute_crypto_node_path(self, path):
if not self.workflow:
raise ValueError("Invalid JSON format.")
for node in self.workflow['nodes']:
if node.get('type') == 'ExcuteCryptoNode':
node['properties']['Path to executable file'] = path
def get_remaining_node_ids(self):
if not self.workflow:
raise ValueError("Invalid JSON format.")
remaining_node_ids = {node['id'] for node in self.workflow['nodes']}
return remaining_node_ids
class PromptTrim():
def __init__(self, prompt):
self.prompt = prompt
self.debug = True
self.hide_part_prompt = {}
self.show_part_prompt = {}
def split_prompt(self, related_node_ids):
if not self.prompt:
raise ValueError("Invalid JSON format.")
# 查找第一个 'SaveCryptoNode' 实例并转换为 int
# save_crypto_node_id = next((int(node_id) for node_id, details in self.prompt.items() if details['class_type'] == 'SaveCryptoNode'), None)
save_crypto_node_id = None
output_images_ids = []
for node_id, details in self.prompt.items():
if details['class_type'] == 'SaveCryptoNode':
output_images_ids = details['inputs'].get('output_images')
save_crypto_node_id = int(node_id)
break
if save_crypto_node_id is None:
raise ValueError("Error: No 'SaveCryptoNode' instances found.")
if save_crypto_node_id not in related_node_ids:
raise AssertionError("SaveCryptoNode not found in related node list.")
self.hide_part_prompt = {}
self.show_part_prompt = {}
# 输出不在链中的节点
for node_id in self.prompt.keys():
node_id_int = int(node_id) # 转换为 int 类型
if node_id_int not in related_node_ids:
self.hide_part_prompt[node_id_int] = self.prompt[node_id]
else:
self.show_part_prompt[node_id_int] = self.prompt[node_id]
self.hide_part_prompt['output_images_ids'] = output_images_ids
return self.show_part_prompt, self.hide_part_prompt
def replace_prompt(self):
if not self.prompt:
raise ValueError("Invalid JSON format.")
crypto_file_path = ''
crypto_file_path = ""
excute_crypto_id = None
# 查找 ExcuteCryptoNode 并获取路径,同时构建需要删除的节点
for node_id, node in list(self.prompt.items()):
if node.get('class_type') == 'ExcuteCryptoNode':
crypto_file_path = node['inputs'].get('crypto_file_path')
for node_id, node in list(prompt.items()):
if node.get("class_type") == "ExcuteCryptoNode":
crypto_file_path = node["inputs"].get("crypto_file_path")
excute_crypto_id = node_id
# 删除当前节点
del self.prompt[node_id]
break
del prompt[node_id]
break
if not crypto_file_path:
print("No 'ExcuteCryptoNode' found in prompt.")
return self.prompt
# print(f"{crypto_file_path=}")
inject_json = FileCompressor.decompress_from_json(crypto_file_path, "19040822")
output_images_ids = inject_json['output_images_ids']
inject_json.pop('output_images_ids', None)
for node_id, node in list(self.prompt.items()):
if node.get('class_type') == 'CryptoCatImage':
ids = node['inputs']['images'] #input_images
if excute_crypto_id in ids:
node['inputs']['images'] = output_images_ids
# 随机生成一个种子,避免缓存问题
random_seed_node = next((node for node in inject_json.values() if node.get('class_type') == 'RandomSeedNode'), None)
return prompt
inject_json = FileCompressor.decompress_from_json(crypto_file_path, "19040822")
output_images_ids = inject_json.pop("output_images_ids")
for node in prompt.values():
if node.get("class_type") == "CryptoCatImage":
if excute_crypto_id in node["inputs"]["images"]:
node["inputs"]["images"] = output_images_ids
random_seed_node = next(
(
node
for node in inject_json.values()
if node.get("class_type") == "RandomSeedNode"
),
None,
)
if random_seed_node:
random_seed_node["inputs"]["is_changed"] = random.randint(0, 999999)
# 将 inject_json 的节点合并到当前 prompt
# for node in inject_json.get('nodes', []):
# self.prompt[node['id']] = node # 使用 ID 添加或更新节点
self.prompt.update(inject_json)
if self.debug == True:
temp_dir = os.environ.get('TEMP') or os.environ.get('TMP') or '/tmp'
filename = os.path.basename(crypto_file_path) + "_prompt.json"
with open(os.path.join(temp_dir, filename), "w", encoding="utf-8") as f:
json.dump(self.prompt, f, indent=4)
print(f"prompt len = {len(self.prompt)}")
return self.prompt
def has_crypto_node(self):
if not self.prompt:
return False
return any(node.get('class_type') == 'ExcuteCryptoNode' for node in self.prompt.values())
if __name__ == "__main__":
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(os.path.dirname(current_dir))
sys.path.append(parent_dir)
import folder_paths
with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版_original_workflow.json", "r", encoding="utf-8") as f:
workflow = json.load(f)
wt = WorkflowTrim(workflow)
wt.trim_workflow()
replace_json = wt.replace_workflow('')
with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版_trim.json", "w", encoding="utf-8") as f:
json.dump(replace_json, f, indent=4)
######################################################
with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版.txt", "r", encoding="utf-8") as f:
prompt = json.load(f)
pt = PromptTrim(prompt)
show_part_prompt, hide_part_prompt = pt.split_prompt(wt.get_remaining_node_ids())
with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版_show_part_prompt.json", "w", encoding="utf-8") as f:
json.dump(show_part_prompt, f, indent=4)
with open("D:\\work\\ComfyUI\\temp\\flux+lora简单版_hide_part_prompt.json", "w", encoding="utf-8") as f:
json.dump(hide_part_prompt, f, indent=4)
random_seed_node["inputs"]["is_changed"] = random.randint(0, 999999)
prompt.update(inject_json)
return prompt
+238
View File
@@ -0,0 +1,238 @@
import json
import os
import requests
from .url_config import CatUrlConfig, download_crypto_workflow, user_upload_workflow
from .utils import combine_files, get_machine_id, get_local_app_setting_path
from .auth_unit import AuthUnit
from server import PromptServer
from aiohttp import web
import time
class Cancelled(Exception):
pass
class MessageHolder:
stash = {}
messages = {}
cancelled = False
@classmethod
def addMessage(cls, id, message):
if message == "__cancel__":
cls.messages = {}
cls.cancelled = True
elif message == "__start__":
cls.messages = {}
cls.stash = {}
cls.cancelled = False
else:
cls.messages[str(id)] = message
@classmethod
def waitForMessage(cls, id, period=0.1, timeout=60):
sid = str(id)
cls.messages.clear()
start_time = time.time()
while not sid in cls.messages and not "-1" in cls.messages:
if cls.cancelled:
cls.cancelled = False
raise Cancelled()
if time.time() - start_time > timeout:
raise Cancelled("Operation timed out")
time.sleep(period)
if cls.cancelled:
cls.cancelled = False
raise Cancelled()
message = cls.messages.pop(str(id), None) or cls.messages.pop("-1")
return message.strip()
routes = PromptServer.instance.routes
@routes.post("/cryptocat/message")
async def message_handler(request):
post = await request.post()
MessageHolder.addMessage(post.get("id"), post.get("message"))
return web.json_response({})
class UploadWorkflow:
def __init__(self, user_token):
self.user_token = user_token
def check_workflow(self, template_id):
url = CatUrlConfig().workflow_url
headers = {"Authorization": f"Bearer {self.user_token}"}
response = requests.get(
url, params={"template_id": template_id, "action": "check"}, headers=headers
)
if response.status_code == 200:
response_data = response.json()
error_code = response_data.get("code")
error_msg = response_data.get("message")
return error_code, error_msg
else:
return -1, ""
def upload_workflow(self, template_id, temp_dir):
error_code, error_msg = self.check_workflow(template_id)
if error_code == 1:
auto_overwrite = UserWorkflowSetting().get_auto_overwrite()
if not auto_overwrite:
json_content = {
"title": "已经存在相同template_id的数据,是否覆盖?",
"icon": "info",
"confirmButtonText": "覆盖",
"cancelButtonText": "取消",
"showCancelButton": True,
"timer": 50000,
}
PromptServer.instance.send_sync(
"cryptocat_dialog",
{"json_content": json.dumps(json_content), "id": template_id},
)
msg_result = MessageHolder.waitForMessage(template_id, timeout=60000)
try:
result_code = int(msg_result)
except ValueError:
print("crypto cat upload cancel: Invalid response format")
return False
if result_code != 1:
print("crypto cat upload cancel: User rejected overwrite")
return False
elif error_code != 0:
print(f"crypto cat upload failed: {error_msg}")
PromptServer.instance.send_sync(
"cryptocat_toast", {"content": f"异常情况,{error_msg}", "type": "error"}
)
return False
combine_target_path = os.path.join(temp_dir, f"cryptocat_{template_id}.zip")
combine_file_paths = []
for file in [
f"crypto_{template_id}.json",
f"original_workflow_{template_id}.json",
f"original_prompt_{template_id}.json",
]:
src_path = os.path.join(temp_dir, file)
combine_file_paths.append(src_path)
combine_files(combine_file_paths, template_id, combine_target_path)
for file_path in combine_file_paths:
os.remove(file_path)
success, message = user_upload_workflow(
template_id, combine_target_path, self.user_token
)
if success:
PromptServer.instance.send_sync(
"cryptocat_toast", {"content": "上传成功", "type": "info", "duration": 5000}
)
else:
PromptServer.instance.send_sync(
"cryptocat_toast",
{"content": f"上传失败: {message}", "type": "error", "duration": 5000},
)
return success
def generate_serial_number(self, template_id, count):
url = CatUrlConfig().serial_number_url
headers = {"Authorization": f"Bearer {self.user_token}"}
response = requests.get(
url, params={"template_id": template_id, "count": count}, headers=headers
)
if response.status_code == 200:
response_data = response.json()
error_code = response_data.get("code")
error_msg = response_data.get("message", "")
if error_code == 0:
return response_data.get("data", [])
else:
raise ValueError(f"failed to generate serial number: {error_msg}")
else:
raise ValueError(
f"failed to generate serial number: {response.status_code}"
)
class DownloadWorkflow:
_instance = None
_cache = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super(DownloadWorkflow, cls).__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, "_initialized"):
self._initialized = True
def download_workflow(self, template_id, serial_number_token, user_token):
if not serial_number_token:
raise ValueError("serial_number_token is required")
cache_key = f"{template_id}_{serial_number_token}_{user_token}"
if cache_key in self._cache:
return self._cache[cache_key]
hardware_id = get_machine_id()
success, message = download_crypto_workflow(
template_id, hardware_id, serial_number_token, user_token
)
if success:
self._cache[cache_key] = message
return message
elif message is not None:
if message == "need login" or message == "需要登录":
AuthUnit.login()
else:
raise ValueError(message)
else:
raise ValueError("failed to download workflow")
class UserWorkflowSetting:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(UserWorkflowSetting, cls).__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, "initialized"):
local_app_path = get_local_app_setting_path()
local_app_path.mkdir(parents=True, exist_ok=True)
self.config_path = local_app_path / "config.ini"
self.section_name = "Workflow"
self.initialized = True
def get_auto_overwrite(self):
try:
import configparser
config = configparser.ConfigParser()
config.read(self.config_path, encoding="utf-8")
if not config.has_section(self.section_name):
return False
return config.getboolean(
self.section_name, "auto_overwrite", fallback=False
)
except Exception as e:
print(f"Error reading auto_overwrite setting: {str(e)}")
return False
def set_auto_overwrite(self, value):
try:
import configparser
config = configparser.ConfigParser()
config.read(self.config_path, encoding="utf-8")
if not config.has_section(self.section_name):
config.add_section(self.section_name)
config.set(self.section_name, "auto_overwrite", str(value).lower())
with open(self.config_path, "w", encoding="utf-8") as configfile:
config.write(configfile)
return True
except Exception as e:
print(f"Error saving auto_overwrite setting: {str(e)}")
return False
+90
View File
@@ -0,0 +1,90 @@
_E='template_id'
_D='message'
_C='code'
_B='Authorization'
_A=False
from enum import IntEnum
import json,os
from PIL import Image
from io import BytesIO
import numpy as np,requests,pyzipper
from requests.exceptions import RequestException,Timeout,HTTPError
from urllib.parse import urljoin
from.utils import get_local_app_setting_path
DEFAULT_SUBDOMAIN='api'if os.getenv('DEBUG')!='true'else'test'
DEFAULT_URL_PREFIX=f"https://{DEFAULT_SUBDOMAIN}.riceround.online"
DEFAULT_WS_PREFIX=f"wss://{DEFAULT_SUBDOMAIN}.riceround.online"
class UploadType(IntEnum):TEMPLATE_PUBLISH_IMAGE=1;USER_UPLOAD_TASK_IMAGE=2;MACHINE_TASK_RESULT=1000
class CatUrlConfig:
_instance=None;_initialized=_A
def __new__(A,*B,**C):
if A._instance is None:A._instance=super(CatUrlConfig,A).__new__(A)
return A._instance
def __init__(A):
if not A._initialized:A._initialized=True
def get_server_url(A,url_path):return urljoin(A.url_prefix,url_path)
def get_ws_url(A,url_path):return urljoin(A.ws_prefix,url_path)
@property
def ws_prefix(self):return DEFAULT_WS_PREFIX
@property
def url_prefix(self):return DEFAULT_URL_PREFIX
@property
def login_api_url(self):return self.get_server_url('/api/workflow/get_info')
@property
def workflow_url(self):return self.get_server_url(f"/api/cryptocat/workflow")
@property
def serial_number_url(self):return self.get_server_url(f"/api/cryptocat/serial_number")
@property
def user_client_workflow(self):return self.get_server_url(f"/api/cryptocat/client_workflow")
def user_upload_image(image,user_token):
H='data';G='image/png';D=CatUrlConfig().user_upload_sign_url;I={_B:f"Bearer {user_token}"};J={'upload_type':UploadType.USER_UPLOAD_TASK_IMAGE.value,'file_type':G};A=requests.get(D,headers=I,params=J);E='';B=''
if A.status_code==200:
C=A.json()
if C.get(_C)==0:E=C.get(H,{}).get('upload_sign_url','');B=C.get(H,{}).get('download_url','')
else:raise ValueError(f"failed to upload image. Status code: {A.status_code}")
if not E or not B:raise ValueError(f"failed to upload image. upload_sign_url is empty")
K=255.*image.cpu().numpy();L=Image.fromarray(np.clip(K,0,255).astype(np.uint8));F=BytesIO();L.save(F,format='PNG',quality=95,compress_level=1);M=F.getvalue();A=requests.put(D,data=M,headers={'Content-Type':G})
if A.status_code==200:return B
else:print(f"failed to upload image. Status code: {A.status_code}");raise ValueError(f"failed to upload image. Status code: {A.status_code}")
def user_upload_workflow(template_id,workflow_path,user_token,timeout=30):
'\n 用户上传工作流文件到服务器。\n\n :param workflow_id: 工作流的模板ID\n :param workflow_path: 工作流文件的本地路径\n :param user_token: 用户的认证Token\n :param timeout: 请求超时时间(秒)\n :return: (bool, str) 成功与否及相关消息\n ';K='An unknown error occurred';J='Request timed out';I='Workflow uploaded successfully';A=workflow_path;B=CatUrlConfig().workflow_url;L={_B:f"Bearer {user_token}"};C=os.path.basename(A)
if not os.path.isfile(A):print(f"File does not exist: {A}");return _A,'File does not exist'
try:
with open(A,'rb')as M:N=M.read()
O={'file':(C,N,'application/octet-stream')};P={_E:template_id,'action':'upload'};print(f"Uploading file: {C} to {B}");D=requests.put(B,headers=L,files=O,data=P,timeout=timeout);D.raise_for_status()
try:E=D.json()
except ValueError:print('Response content is not valid JSON');return _A,'Server returned invalid response'
if E.get(_C)==0:print(I);return True,I
else:F=E.get(_D,'Upload failed');print(f"Upload failed: {F}");return _A,F
except FileNotFoundError:print(f"File not found: {A}");return _A,'File not found'
except Timeout:print(J);return _A,J
except HTTPError as G:print(f"HTTP error occurred: {G}");return _A,f"HTTP error: {G}"
except RequestException as H:print(f"Request exception occurred: {H}");return _A,f"Request exception: {H}"
except Exception as Q:print(K);return _A,K
def download_crypto_workflow(template_id,hardware_id,serial_number,user_token=None):
'\n 下载并解密工作流的函数\n\n :param template_id: 模板ID\n :param hardware_id: 硬件ID\n :param serial_number: 序列号\n :param user_token: 用户凭证 (可选)\n :return: (status, content)\n - status: bool, 表示操作是否成功\n - content: 当status为True时,返回工作流内容;否则返回错误信息\n ';M='utf-8';H=user_token;G=serial_number
if not G:raise ValueError('serial_number (序列号) 参数不能为空')
N=CatUrlConfig();O=N.user_client_workflow;I={}
if H:I[_B]=f"Bearer {H}"
P={_E:template_id,'hardware_id':hardware_id,'serial_number':G}
try:B=requests.post(O,data=P,headers=I)
except requests.RequestException as D:A=f"网络请求出现异常: {str(D)}";print(A);return _A,A
if B.status_code!=200:
if B.status_code==500:
try:C=B.json();A=C.get(_D,f"未知错误 (status_code={B.status_code})")
except Exception:A=f"服务器内部错误 (status_code={B.status_code})"
else:A=f"下载工作流失败: status_code={B.status_code}"
print(A);return _A,A
try:C=B.json()
except ValueError:A='无法解析返回的 JSON 数据';print(A);return _A,A
J=C.get(_C,-1)
if J!=0:A=C.get(_D,f"未知错误 code={J}");print(A);return _A,A
K=C.get('workflow_url','');L=C.get('password','')
if not K or not L:A='下载工作流失败: workflow_url 或 password 缺失';print(A);return _A,A
try:E=requests.get(K)
except requests.RequestException as D:A=f"下载工作流内容出现异常: {str(D)}";print(A);return _A,A
if E.status_code!=200:A=f"下载工作流失败: status_code={E.status_code}";print(A);return _A,A
try:
Q=BytesIO(E.content)
with pyzipper.AESZipFile(Q)as F:F.setpassword(L.encode(M));R=F.namelist()[0];S=F.read(R);return True,S.decode(M)
except Exception as D:A=f"解密工作流失败: {str(D)}";print(A);return _A,A
+147
View File
@@ -0,0 +1,147 @@
import configparser
import hashlib
from io import BytesIO
import os
from pathlib import Path
import uuid
import torch
import numpy as np
from PIL import Image
import platform
import subprocess
import random
import string
def pil2tensor(images: Image.Image | list[Image.Image]) -> torch.Tensor:
"Converts a PIL Image or a list of PIL Images to a tensor."
def single_pil2tensor(image: Image.Image) -> torch.Tensor:
np_image = np.array(image).astype(np.float32) / 255.0
if np_image.ndim == 2:
return torch.from_numpy(np_image).unsqueeze(0)
else:
return torch.from_numpy(np_image).unsqueeze(0)
if isinstance(images, Image.Image):
return single_pil2tensor(images)
else:
return torch.cat([single_pil2tensor(img) for img in images], dim=0)
def calculate_machine_id():
"\n 获取跨平台的机器唯一标识符,类似于 gopsutil 的 HostID\n"
system = platform.system()
if system == "Linux":
try:
with open("/etc/machine-id", "r") as f:
return f.read().strip()
except FileNotFoundError:
try:
with open("/var/lib/dbus/machine-id", "r") as f:
return f.read().strip()
except FileNotFoundError:
pass
try:
output = subprocess.check_output(["cat", "/sys/class/dmi/id/product_uuid"])
return output.decode().strip()
except Exception:
pass
elif system == "Windows":
try:
import winreg
reg_key = "SOFTWARE\\Microsoft\\Cryptography"
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_key) as key:
machine_guid, _ = winreg.QueryValueEx(key, "MachineGuid")
return machine_guid
except Exception:
pass
elif system == "Darwin":
try:
output = subprocess.check_output(
["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"]
)
for line in output.decode().splitlines():
if "IOPlatformUUID" in line:
return line.split("=")[-1].strip().strip('"')
except Exception:
pass
return str(uuid.getnode())
def normalize_machine_id(machine_id: str) -> str:
"\n 接受一个机器标识符,并返回经过 MD5 哈希处理的规范化标识符\n"
salt = "CryptoCat"
trimmed_id = machine_id.strip()
lowercase_id = trimmed_id.lower()
salted_id = lowercase_id + salt
hash_obj = hashlib.md5(salted_id.encode("utf-8"))
return hash_obj.hexdigest()
def get_local_app_setting_path() -> Path:
home = Path.home()
config_dir = home / "CryptoCat"
return config_dir
def get_machine_id() -> str:
"\n 返回机器ID,为了兼容各个平台,各个语言,需要统一读写这个值\n"
config_dir = get_local_app_setting_path()
config_file = config_dir / "machine.ini"
try:
config_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
print(f"Error creating directory '{config_dir}': {e}")
return ""
config = configparser.ConfigParser()
try:
if config_file.exists():
config.read(config_file, encoding="utf-8")
if "Machine" in config and "machine_id" in config["Machine"]:
return config["Machine"]["machine_id"]
original_host_id = calculate_machine_id()
machine_id = normalize_machine_id(original_host_id)
if "Machine" not in config:
config.add_section("Machine")
config.set("Machine", "machine_id", machine_id)
with open(config_file, "w", encoding="utf-8") as file:
config.write(file)
return machine_id
except Exception as e:
print(f"Error handling machine ID in '{config_file}': {e}")
return ""
def combine_files(files: list[str], password: str, zip_file_path: str) -> bool:
import pyzipper
for file_path in files:
if not os.path.exists(file_path):
raise FileNotFoundError(f"file not found: {file_path}")
try:
with pyzipper.AESZipFile(
zip_file_path,
"w",
compression=pyzipper.ZIP_DEFLATED,
encryption=pyzipper.WZ_AES,
) as zipf:
if isinstance(password, str):
password = password.encode("utf-8")
else:
raise ValueError("password must be a string")
zipf.setpassword(password)
for index, file_path in enumerate(files, start=1):
arcname = f"{index}.bin"
zipf.write(file_path, arcname)
return True
except Exception as e:
print(f"Error creating zip: {str(e)}")
return False
def generate_random_string(length: int) -> str:
"\n Generate a random string of specified length using uppercase and lowercase letters.\n \n Args:\n length (int): The desired length of the random string\n \n Returns:\n str: A random string of the specified length\n"
letters = string.ascii_letters
return "".join(random.choice(letters) for _ in range(length))
+20
View File
@@ -0,0 +1,20 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}