├── .github └── workflows │ └── publish_action.yml ├── CHINESE_README.md ├── ENGLISH_README.md ├── LICENSE ├── README.md ├── __init__.py ├── auth_unit.py ├── crypto_node.py ├── crypto_node_old.py ├── docs ├── image1.png ├── install.png ├── warn.png └── wechat.jpg ├── example_workflows └── simple1.json ├── file_compressor.py ├── install.bat ├── js ├── catconfig.js └── mycat.js ├── pyproject.toml ├── requirements.txt ├── static ├── dialog-lib.umd.cjs └── mycat.css ├── trim_workflow.py ├── updown_workflow.py ├── url_config.py └── utils.py /.github/workflows/publish_action.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "pyproject.toml" 9 | 10 | permissions: 11 | issues: write 12 | 13 | jobs: 14 | publish-node: 15 | name: Publish Custom Node to registry 16 | runs-on: ubuntu-latest 17 | if: ${{ github.repository_owner == 'riceround' }} 18 | steps: 19 | - name: Check out code 20 | uses: actions/checkout@v4 21 | - name: Publish Custom Node 22 | uses: Comfy-Org/publish-node-action@v1 23 | with: 24 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 25 | -------------------------------------------------------------------------------- /CHINESE_README.md: -------------------------------------------------------------------------------- 1 | [Read English](ENGLISH_README.md) 2 | 3 | # CryptoCat: ComfyUI压缩加密节点 4 | 5 | ## 简介 6 | CryptoCat是一个小巧的ComfyUI开源节点,它的作用在于简化工作流,同时给工作流提供加密保护。 7 | 8 | ![image](docs/image1.png) 9 | 10 | ## 应用场景 11 | - 流程简化:可以大幅度简化工作流。 12 | - 加密授权:可以保护工作流里的一些核心思路。 13 | 14 | ## 快速开始 15 | 你可以在这里看到一个简单的 [Workflow Demo](demo/original.json) 16 | 17 | ### 安装和使用步骤 18 | 1. **安装节点** 19 | - 打开 ComfyUI\custom_Nodes\ 目录,克隆仓库到本地 20 | - 或在 ComfyUI-Manager 中安装 ComfyUI Compression and Encryption Node 节点 21 | 22 | 2. **启动和配置** 23 | - 启动 ComfyUI 24 | - 在菜单"高级"(advance)中找到 CryptoCat 目录 25 | 26 | 3. **🔐使用方法** 27 | - 加密组件和加密结束桥接一头一尾,控制工作流的加密区间 28 | - 随机种子会在服务端生成随机数,用于修补工作流封装后随机数不起效的情况 29 | 30 | 31 | > ⚠️ 解密组件无需手动添加 - 加密后系统会在output文件夹自动生成包含解密组件的工作流 32 | > 33 | > ⚠️ 加密时会自动生成10组序列号,用户首次使用时会与硬件信息绑定,后续使用时会验证序列号与硬件信息的一致性 34 | 35 | 36 | 37 | ## 贡献指南 38 | 欢迎对CryptoCat项目做出贡献!你可以通过提交Pull Request或开设Issue来提出新功能建议或报告问题。 39 | 40 | ## 许可证 41 | 本项目遵循MIT许可证。有关详细信息,请参阅LICENSE文件。 42 | 43 | ## 联系方式 44 | Email: 45 | 46 | ![image](docs/wechat.jpg) 47 | 48 | --- 49 | CryptoCat © 2024. All Rights Reserved. 50 | -------------------------------------------------------------------------------- /ENGLISH_README.md: -------------------------------------------------------------------------------- 1 | # CryptoCat: ComfyUI Compression and Encryption Node 2 | 3 | ## Introduction 4 | CryptoCat is a lightweight open-source node for ComfyUI that simplifies workflows while providing encryption protection. 5 | 6 | ![image](docs/image1.png) 7 | 8 | ## Use Cases 9 | - Workflow Simplification: Significantly simplifies complex workflows. 10 | - Encryption Authorization: Protects core concepts within your workflows. 11 | 12 | ## Quick Start 13 | You can view a simple [Workflow Demo](demo/original.json) 14 | 15 | ### Installation and Usage 16 | 1. **Install the Node** 17 | - Open ComfyUI\custom_Nodes\ directory and clone the repository 18 | - Or install "ComfyUI Compression and Encryption Node" through ComfyUI-Manager 19 | 20 | 2. **Launch and Configure** 21 | - Start ComfyUI 22 | - Find the CryptoCat directory in the "Advanced" menu 23 | 24 | 3. **🔐 How to Use** 25 | - Use encryption component and encryption end bridge as start and end points to control the encrypted workflow section 26 | - Random seeds will generate random numbers on the server side to fix issues with randomization after workflow encapsulation 27 | 28 | > ⚠️ Decryption component doesn't need to be added manually - after encryption, the system will automatically generate a workflow with decryption components in the output folder 29 | > 30 | > ⚠️ During encryption, 10 serial numbers will be automatically generated. When users first use it, it will be bound to their hardware information, and subsequent use will verify the consistency between the serial number and hardware information 31 | 32 | ## Contributing 33 | Contributions to CryptoCat are welcome! You can contribute by submitting Pull Requests or opening Issues to suggest new features or report problems. 34 | 35 | ## License 36 | This project is licensed under the MIT License. See the LICENSE file for details. 37 | 38 | ## Contact 39 | Email: 40 | 41 | ![image](docs/wechat.jpg) 42 | 43 | --- 44 | CryptoCat © 2024. All Rights Reserved. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 RiceRound 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Read English](ENGLISH_README.md) 2 | 3 | # CryptoCat: ComfyUI压缩加密节点 4 | 5 | ## 简介 6 | CryptoCat是一个小巧的ComfyUI开源节点,它的作用在于简化工作流,同时给工作流提供加密保护。 7 | 8 | ![image](docs/image1.png) 9 | 10 | ## 应用场景 11 | - 流程简化:可以大幅度简化工作流。 12 | - 加密授权:可以保护工作流里的一些核心思路。 13 | 14 | ## 快速开始 15 | 你可以在这里看到一个简单的 [Workflow Demo](demo/original.json) 16 | 17 | ### 安装和使用步骤 18 | 1. **安装节点** 19 | - 在 ComfyUI-Manager中搜索RiceRound,找到CryptoCat节点 20 | ![image](docs/install.png) 21 | 22 | - 你也可以下载文件夹放入ComfyUI\custom_nodes目录 23 | 24 | 25 | 1. **启动和配置** 26 | - 启动 ComfyUI 27 | - 在菜单"高级"(advance)中找到 CryptoCat 目录 28 | 29 | 2. **🔐使用方法** 30 | - 加密组件和加密结束桥接一头一尾,控制工作流的加密区间 31 | - 随机种子会在服务端生成随机数,用于修补工作流封装后随机数不起效的情况 32 | 33 | > ☘️ 解密组件无需手动添加 - 加密后系统会在output文件夹自动生成包含解密组件的工作流 34 | 35 | > ☘️ 加密时会自动生成10组序列号,用户首次使用时会与硬件信息绑定,不够可以从设置里面找到算号器生成 36 | 37 | ## ⚠️易错强调 38 | 39 | 40 | ![image](docs/warn.png) 41 | 42 | > ⚠️加密组件和加密结束桥接一头一尾! 43 | 44 | > ⚠️输出节点连接不能遗漏!! 45 | 46 | 47 | 48 | 49 | ## 贡献指南 50 | 欢迎对CryptoCat项目做出贡献!你可以通过提交Pull Request或开设Issue来提出新功能建议或报告问题。 51 | 52 | ## 许可证 53 | 本项目遵循MIT许可证。有关详细信息,请参阅LICENSE文件。 54 | 55 | ## 联系方式 56 | Email: 57 | 58 | ![image](docs/wechat.jpg) 59 | 60 | --- 61 | CryptoCat © 2024. All Rights Reserved. 62 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from functools import partial, wraps 2 | import logging 3 | import os 4 | import shutil 5 | from .utils import get_local_app_setting_path 6 | from .trim_workflow import WorkflowTrimHandler 7 | from .crypto_node import ( 8 | SaveCryptoNode, 9 | RandomSeedNode, 10 | SaveCryptoBridgeNode, 11 | DecodeCryptoNode, 12 | ) 13 | from .crypto_node_old import ExcuteCryptoNode, CryptoCatImage 14 | 15 | NODE_CLASS_MAPPINGS = { 16 | "SaveCryptoNode": SaveCryptoNode, 17 | "ExcuteCryptoNode": ExcuteCryptoNode, 18 | "RandomSeedNode": RandomSeedNode, 19 | "CryptoCatImage": CryptoCatImage, 20 | "SaveCryptoBridgeNode": SaveCryptoBridgeNode, 21 | "DecodeCryptoNode": DecodeCryptoNode, 22 | } 23 | NODE_DISPLAY_NAME_MAPPINGS = { 24 | "SaveCryptoNode": "加密组件", 25 | "RandomSeedNode": "随机种子", 26 | "SaveCryptoBridgeNode": "加密结束桥接", 27 | "DecodeCryptoNode": "解密组件", 28 | "ExcuteCryptoNode": "OutputCrypto", 29 | "CryptoCatImage": "CryptoCatImage", 30 | } 31 | WEB_DIRECTORY = "./js" 32 | __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAMES_MAPPINGS", "WEB_DIRECTORY"] 33 | from server import PromptServer 34 | import aiohttp 35 | 36 | workspace_path = os.path.join(os.path.dirname(__file__)) 37 | dist_path = os.path.join(workspace_path, "static") 38 | if os.path.exists(dist_path): 39 | PromptServer.instance.app.add_routes( 40 | [aiohttp.web.static("/cryptocat/static", dist_path)] 41 | ) 42 | from urllib.parse import unquote 43 | from .auth_unit import AuthUnit 44 | from .updown_workflow import UploadWorkflow, UserWorkflowSetting 45 | 46 | handler_instance = WorkflowTrimHandler() 47 | onprompt_callback = partial(handler_instance.onprompt_handler) 48 | PromptServer.instance.add_on_prompt_handler(onprompt_callback) 49 | routes = PromptServer.instance.routes 50 | 51 | 52 | @routes.post("/cryptocat/auth_callback") 53 | async def auth_callback(request): 54 | auth_query = await request.json() 55 | token = auth_query.get("token", "") 56 | client_key = auth_query.get("client_key", "") 57 | if token and client_key: 58 | token = unquote(token) 59 | client_key = unquote(client_key) 60 | AuthUnit().set_user_token(token, client_key) 61 | return aiohttp.web.json_response({"status": "success"}, status=200) 62 | 63 | 64 | @routes.post("/cryptocat/keygen") 65 | async def keygen(request): 66 | data = await request.json() 67 | template_id = data.get("template_id", "").strip() 68 | if not template_id or len(template_id) != 32: 69 | return aiohttp.web.json_response( 70 | {"error_msg": "template_id is required"}, status=500 71 | ) 72 | expire_date = data.get("expire_date", "") 73 | use_days = data.get("use_days", "") 74 | user_token, error_msg, error_code = AuthUnit().get_user_token() 75 | if not user_token: 76 | logging.error(f"crypto cat keygen failed: {error_msg}") 77 | return aiohttp.web.json_response({"error_msg": error_msg}, status=200) 78 | user_workflow = UploadWorkflow(user_token) 79 | serial_numbers = user_workflow.generate_serial_number( 80 | template_id, expire_date, use_days, 1 81 | ) 82 | if not serial_numbers: 83 | logging.error(f"crypto cat keygen failed: {error_msg}") 84 | return aiohttp.web.json_response({"error_msg": "获取失败"}, status=200) 85 | serial_number = serial_numbers[0] 86 | return aiohttp.web.json_response({"serial_number": serial_number}, status=200) 87 | 88 | 89 | @routes.get("/cryptocat/clear") 90 | async def logout(request): 91 | AuthUnit().clear_user_token() 92 | local_app_setting_path = get_local_app_setting_path() 93 | shutil.rmtree(local_app_setting_path, ignore_errors=True) 94 | return aiohttp.web.json_response({"status": "success"}, status=200) 95 | 96 | 97 | @routes.post("/cryptocat/login") 98 | async def login(request): 99 | user_token, error_msg, error_code = AuthUnit().get_user_token() 100 | if user_token and len(user_token) > 10 and error_code == 0: 101 | return aiohttp.web.json_response({"error_msg": "已经登录,如果切换用户先登出"}, status=200) 102 | else: 103 | AuthUnit().login_dialog() 104 | return aiohttp.web.json_response( 105 | {"status": "success", "error_msg": "登录中..."}, status=200 106 | ) 107 | 108 | 109 | @routes.post("/cryptocat/set_long_token") 110 | async def set_long_token(request): 111 | data = await request.json() 112 | long_token = data.get("long_token", "") 113 | AuthUnit().set_long_token(long_token) 114 | return aiohttp.web.json_response({"status": "success"}, status=200) 115 | 116 | 117 | @routes.post("/cryptocat/set_auto_overwrite") 118 | async def set_auto_overwrite(request): 119 | data = await request.json() 120 | auto_overwrite = data.get("auto_overwrite") 121 | UserWorkflowSetting().set_auto_overwrite(auto_overwrite) 122 | return aiohttp.web.json_response({"status": "success"}, status=200) 123 | -------------------------------------------------------------------------------- /auth_unit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import requests 4 | import configparser 5 | from .utils import get_local_app_setting_path, get_machine_id, generate_random_string 6 | from .url_config import CatUrlConfig 7 | from server import PromptServer 8 | 9 | 10 | class AuthUnit: 11 | _instance = None 12 | 13 | def __new__(cls, *args, **kwargs): 14 | if cls._instance is None: 15 | cls._instance = super(AuthUnit, cls).__new__(cls) 16 | return cls._instance 17 | 18 | def __init__(self): 19 | if not hasattr(self, "initialized"): 20 | self.machine_id = get_machine_id() 21 | local_app_path = get_local_app_setting_path() 22 | local_app_path.mkdir(parents=True, exist_ok=True) 23 | self.config_path = local_app_path / "config.ini" 24 | self.last_check_time = 0 25 | self.initialized = True 26 | 27 | def empty_token(self, need_clear=False): 28 | self.token = "" 29 | self.last_check_time = 0 30 | if need_clear: 31 | self.clear_user_token() 32 | 33 | def get_user_token(self): 34 | self.token = self.read_user_token() 35 | if ( 36 | time.time() - self.last_check_time > 120 37 | and self.token 38 | and len(self.token) > 50 39 | ): 40 | try: 41 | headers = { 42 | "Content-Type": "application/json", 43 | "Authorization": f"Bearer {self.token}", 44 | } 45 | response = requests.get( 46 | CatUrlConfig().login_api_url, headers=headers, timeout=10 47 | ) 48 | if response.status_code == 200: 49 | self.last_check_time = time.time() 50 | return self.token, "", 0 51 | else: 52 | error_message = "登录结果错误" 53 | error_code = 1 54 | try: 55 | response_data = response.json() 56 | if "message" in response_data: 57 | error_message = response_data["message"] 58 | except ValueError: 59 | pass 60 | if response.status_code == 401: 61 | error_message = "登录已过期,请重新登录" 62 | error_code = 401 63 | elif response.status_code == 500: 64 | error_message = "服务器内部错误,请稍后重试" 65 | error_code = 500 66 | elif response.status_code == 503: 67 | error_message = "服务不可用,请稍后重试" 68 | error_code = 503 69 | self.empty_token(response.status_code == 401) 70 | return None, error_message, error_code 71 | except requests.exceptions.Timeout: 72 | self.empty_token() 73 | return None, "请求超时,请检查网络连接", 408 74 | except requests.exceptions.ConnectionError: 75 | self.empty_token() 76 | return None, "网络连接失败,请检查网络", -1 77 | except requests.exceptions.RequestException as e: 78 | self.empty_token() 79 | return None, f"请求失败: {str(e)}", -2 80 | if self.token and len(self.token) > 50: 81 | return self.token, "", 0 82 | return None, "未读取到有效的token,请重新登录", -3 83 | 84 | def login_dialog(self, title=""): 85 | self.client_key = generate_random_string(8) 86 | PromptServer.instance.send_sync( 87 | "cryptocat_login_dialog", 88 | { 89 | "machine_id": self.machine_id, 90 | "client_key": self.client_key, 91 | "title": title, 92 | }, 93 | ) 94 | 95 | def read_user_token(self): 96 | if not os.path.exists(self.config_path): 97 | return "" 98 | try: 99 | config = configparser.ConfigParser() 100 | config.read(self.config_path, encoding="utf-8") 101 | return config.get("Auth", "user_token", fallback="") 102 | except Exception as e: 103 | print(f"Error reading token: {e}") 104 | return "" 105 | 106 | def set_user_token(self, user_token, client_key): 107 | if not client_key or self.client_key != client_key: 108 | print("client_key is not match") 109 | return 110 | if not user_token: 111 | user_token = "" 112 | print("user_token is empty") 113 | self._save_user_token(user_token) 114 | 115 | def _save_user_token(self, user_token): 116 | try: 117 | config = configparser.ConfigParser() 118 | if os.path.exists(self.config_path): 119 | config.read(self.config_path, encoding="utf-8") 120 | if "Auth" not in config: 121 | config.add_section("Auth") 122 | config["Auth"]["user_token"] = user_token 123 | with open(self.config_path, "w", encoding="utf-8") as f: 124 | config.write(f) 125 | print(f"Token saved successfully. config_path:{self.config_path}") 126 | except Exception as e: 127 | print(f"Error saving token: {e}") 128 | raise RuntimeError(f"Failed to save token: {e}") 129 | 130 | def set_long_token(self, long_token): 131 | if not long_token or len(long_token) < 50: 132 | return 133 | self._save_user_token(long_token) 134 | self.client_key = "" 135 | 136 | def clear_user_token(self): 137 | PromptServer.instance.send_sync( 138 | "cryptocat_clear_user_info", {"clear_key": "all"} 139 | ) 140 | if os.path.exists(self.config_path): 141 | try: 142 | config = configparser.ConfigParser() 143 | config.read(self.config_path, encoding="utf-8") 144 | if "Auth" not in config: 145 | return 146 | if "user_token" not in config["Auth"]: 147 | return 148 | config["Auth"]["user_token"] = "" 149 | with open(self.config_path, "w", encoding="utf-8") as f: 150 | config.write(f) 151 | except Exception as e: 152 | print(f"Error clearing token: {e}") 153 | raise RuntimeError(f"Failed to clear token: {e}") 154 | -------------------------------------------------------------------------------- /crypto_node.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | import uuid 5 | import torch 6 | from comfy_execution.graph import ExecutionBlocker 7 | from comfy_execution.graph_utils import GraphBuilder 8 | from .updown_workflow import UploadWorkflow 9 | from server import PromptServer 10 | from .trim_workflow import CryptoWorkflow, DecodeCryptoWorkflow 11 | from .auth_unit import AuthUnit 12 | 13 | 14 | class SaveCryptoNode: 15 | def __init__(self): 16 | pass 17 | 18 | @classmethod 19 | def INPUT_TYPES(s): 20 | return { 21 | "required": {"template_id": ("STRING", {"default": uuid.uuid4().hex})}, 22 | "optional": {"input_anything": ("*", {})}, 23 | "hidden": { 24 | "unique_id": "UNIQUE_ID", 25 | "prompt": "PROMPT", 26 | "extra_pnginfo": "EXTRA_PNGINFO", 27 | }, 28 | } 29 | 30 | RETURN_TYPES = () 31 | OUTPUT_NODE = True 32 | FUNCTION = "crypto" 33 | CATEGORY = "advanced/CryptoCat" 34 | 35 | @classmethod 36 | def IS_CHANGED(s, **kwargs): 37 | return float("NaN") 38 | 39 | @classmethod 40 | def VALIDATE_INPUTS(s, input_types): 41 | return True 42 | 43 | def crypto(self, template_id, **kwargs): 44 | unique_id = kwargs.pop("unique_id", None) 45 | prompt = kwargs.pop("prompt", None) 46 | extra_pnginfo = kwargs.pop("extra_pnginfo", None) 47 | if unique_id is None: 48 | raise Exception("Warning: 'unique_id' is missing.") 49 | if prompt is None: 50 | raise Exception("Warning: 'prompt' is missing.") 51 | if len(template_id) != 32: 52 | raise Exception("Warning: 'template_id' length is not 32.") 53 | crypto_workflow = CryptoWorkflow(extra_pnginfo["workflow"], prompt, template_id) 54 | crypto_workflow.invalid_workflow() 55 | crypto_workflow.load_workflow() 56 | crypto_workflow.load_prompt() 57 | crypto_workflow.analysis_node() 58 | crypto_dir = crypto_workflow.calculate_crypto_result( 59 | f"crypto_{template_id}.json" 60 | ) 61 | output_dir = crypto_workflow.output_workflow_simple_shell( 62 | f"cryptocat_{template_id}.json" 63 | ) 64 | crypto_workflow.save_original_workflow( 65 | f"original_workflow_{template_id}.json", crypto_dir 66 | ) 67 | crypto_workflow.save_original_prompt( 68 | f"original_prompt_{template_id}.json", crypto_dir 69 | ) 70 | user_token, error_msg, error_code = AuthUnit().get_user_token() 71 | if not user_token: 72 | print(f"crypto cat upload failed, {error_msg}") 73 | if error_code == 401 or error_code == -3: 74 | AuthUnit().login_dialog("输出CryptoCat加密工作流,请先完成登录") 75 | else: 76 | PromptServer.instance.send_sync( 77 | "cryptocat_toast", 78 | {"content": "无法完成鉴权登录,请检查网络或完成登录步骤", "type": "error"}, 79 | ) 80 | return (ExecutionBlocker(None),) 81 | upload = UploadWorkflow(user_token) 82 | ret = upload.upload_workflow(template_id, crypto_dir) 83 | if not ret: 84 | print("crypto cat upload failed") 85 | return (ExecutionBlocker(None),) 86 | serial_numbers = upload.generate_serial_number(template_id, count=10) 87 | if serial_numbers: 88 | sn_file_path = os.path.join(output_dir, f"serial_numbers_{template_id}.txt") 89 | with open(sn_file_path, "w", encoding="utf-8") as f: 90 | f.write("\n".join(serial_numbers)) 91 | PromptServer.instance.send_sync( 92 | "cryptocat_toast", 93 | { 94 | "content": f"加密完成,在{sn_file_path}中查看序列号", 95 | "type": "info", 96 | "duration": 5000, 97 | }, 98 | ) 99 | return (template_id,) 100 | 101 | 102 | class AnyType(str): 103 | def __ne__(self, __value: object) -> bool: 104 | return False 105 | 106 | 107 | any = AnyType("*") 108 | 109 | 110 | class SaveCryptoBridgeNode: 111 | def __init__(self): 112 | pass 113 | 114 | @classmethod 115 | def INPUT_TYPES(s): 116 | return {"required": {"value": (any,)}} 117 | 118 | @classmethod 119 | def VALIDATE_INPUTS(s, input_types): 120 | return True 121 | 122 | @classmethod 123 | def IS_CHANGED(cls, **kwargs): 124 | return float("NaN") 125 | 126 | RETURN_TYPES = (any,) 127 | FUNCTION = "doit" 128 | CATEGORY = "advanced/CryptoCat" 129 | 130 | def doit(self, value): 131 | return (value,) 132 | 133 | 134 | def is_link(obj): 135 | if not isinstance(obj, list): 136 | return False 137 | if len(obj) != 2: 138 | return False 139 | if not isinstance(obj[0], str): 140 | return False 141 | if ( 142 | not isinstance(obj[1], int) 143 | and not isinstance(obj[1], float) 144 | and not isinstance(obj[1], str) 145 | ): 146 | return False 147 | return True 148 | 149 | 150 | class AlwaysEqualProxy(str): 151 | def __eq__(self, _): 152 | return True 153 | 154 | def __ne__(self, _): 155 | return False 156 | 157 | 158 | class AlwaysTupleZero(tuple): 159 | def __getitem__(self, _): 160 | return AlwaysEqualProxy(super().__getitem__(0)) 161 | 162 | 163 | class DecodeCryptoNode: 164 | def __init__(self): 165 | pass 166 | 167 | @classmethod 168 | def INPUT_TYPES(s): 169 | return { 170 | "required": { 171 | "template_id": ("STRING",), 172 | "serial_number": ( 173 | "STRING", 174 | {"multiline": True, "placeholder": "请输入序列号"}, 175 | ), 176 | }, 177 | "optional": {"input_anything": (any,)}, 178 | "hidden": { 179 | "unique_id": "UNIQUE_ID", 180 | "prompt": "PROMPT", 181 | "extra_pnginfo": "EXTRA_PNGINFO", 182 | }, 183 | } 184 | 185 | @classmethod 186 | def VALIDATE_INPUTS(s, input_types): 187 | return True 188 | 189 | @classmethod 190 | def IS_CHANGED(cls, **kwargs): 191 | return float("NaN") 192 | 193 | RETURN_TYPES = AlwaysTupleZero(AlwaysEqualProxy("*")) 194 | FUNCTION = "decode" 195 | CATEGORY = "advanced/CryptoCat" 196 | OUTPUT_NODE = False 197 | 198 | def decode(self, template_id, serial_number, **kwargs): 199 | unique_id = kwargs.pop("unique_id", None) 200 | prompt = kwargs.pop("prompt", None) 201 | extra_pnginfo = kwargs.pop("extra_pnginfo", None) 202 | decode_crypto_workflow = DecodeCryptoWorkflow( 203 | prompt, extra_pnginfo["workflow"], template_id 204 | ) 205 | crypto_prompt = decode_crypto_workflow.load_crypto_prompt(serial_number) 206 | decode_crypto_workflow.calculate_input_anything_map() 207 | processed_nodes = {} 208 | graph = GraphBuilder() 209 | 210 | def get_node_result(nodeData, id): 211 | inputKeys = [] 212 | for ikey in nodeData["inputs"].keys(): 213 | input_value = nodeData["inputs"][ikey] 214 | if ( 215 | is_link(input_value) 216 | and decode_crypto_workflow.get_hidden_input(input_value) is None 217 | and input_value[0] not in processed_nodes 218 | ): 219 | inputKeys.append(input_value[0]) 220 | for ikey in inputKeys: 221 | if ikey not in crypto_prompt: 222 | continue 223 | node = get_node_result(crypto_prompt[ikey], ikey) 224 | processed_nodes[ikey] = node 225 | inputs = nodeData["inputs"] 226 | newInputs = {} 227 | for ikey in inputs.keys(): 228 | if is_link(inputs[ikey]): 229 | hidden_input_name = decode_crypto_workflow.get_hidden_input( 230 | inputs[ikey] 231 | ) 232 | if hidden_input_name: 233 | if hidden_input_name in kwargs: 234 | newInputs[ikey] = kwargs[hidden_input_name] 235 | elif inputs[ikey][0] in processed_nodes: 236 | newInputs[ikey] = processed_nodes[inputs[ikey][0]].out( 237 | inputs[ikey][1] 238 | ) 239 | else: 240 | newInputs[ikey] = inputs[ikey] 241 | return graph.node(nodeData["class_type"], id, **newInputs) 242 | 243 | node_id, link_idx = decode_crypto_workflow.get_outputs() 244 | nodeData = crypto_prompt[node_id] 245 | node = get_node_result(nodeData, node_id) 246 | value = node.out(link_idx) 247 | return {"result": tuple([value]), "expand": graph.finalize()} 248 | 249 | 250 | class RandomSeedNode: 251 | def __init__(self): 252 | pass 253 | 254 | @classmethod 255 | def INPUT_TYPES(s): 256 | return {"required": {}, "optional": {}, "hidden": {}} 257 | 258 | RETURN_TYPES = ("INT",) 259 | FUNCTION = "random" 260 | CATEGORY = "advanced/CryptoCat" 261 | 262 | def IS_CHANGED(): 263 | return float("NaN") 264 | 265 | def random(self): 266 | return (random.randint(0, 999999),) 267 | -------------------------------------------------------------------------------- /crypto_node_old.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from nodes import SaveImage 3 | import folder_paths 4 | 5 | 6 | class ExcuteCryptoNode: 7 | def __init__(self): 8 | pass 9 | 10 | @classmethod 11 | def INPUT_TYPES(s): 12 | return { 13 | "required": { 14 | "crypto_file_path": ( 15 | "STRING", 16 | {"default": folder_paths.output_directory}, 17 | ) 18 | }, 19 | "optional": {"input_anything": ("*",)}, 20 | "hidden": { 21 | "unique_id": "UNIQUE_ID", 22 | "prompt": "PROMPT", 23 | "extra_pnginfo": "EXTRA_PNGINFO", 24 | }, 25 | } 26 | 27 | RETURN_TYPES = ("IMAGE",) 28 | FUNCTION = "excute" 29 | CATEGORY = "__hidden__" 30 | 31 | def excute(self, **kwargs): 32 | batch_size = 1 33 | height = 1024 34 | width = 1024 35 | color = 16711680 36 | r = torch.full([batch_size, height, width, 1], (color >> 16 & 255) / 255) 37 | g = torch.full([batch_size, height, width, 1], (color >> 8 & 255) / 255) 38 | b = torch.full([batch_size, height, width, 1], (color & 255) / 255) 39 | return (torch.cat((r, g, b), dim=-1),) 40 | 41 | 42 | class CryptoCatImage(SaveImage): 43 | def __init__(self): 44 | super().__init__() 45 | 46 | @classmethod 47 | def INPUT_TYPES(s): 48 | return { 49 | "required": { 50 | "images": ("IMAGE", {"tooltip": "The images to save."}), 51 | "filename_prefix": ( 52 | "STRING", 53 | { 54 | "default": "ComfyUI", 55 | "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.", 56 | }, 57 | ), 58 | }, 59 | "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, 60 | } 61 | 62 | RETURN_TYPES = () 63 | FUNCTION = "save_images" 64 | OUTPUT_NODE = True 65 | CATEGORY = "__hidden__" 66 | DESCRIPTION = "Saves the input images to your ComfyUI output directory." 67 | 68 | def save_images( 69 | self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None 70 | ): 71 | return super().save_images(images, filename_prefix, None, None) 72 | -------------------------------------------------------------------------------- /docs/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiceRound/ComfyUI_CryptoCat/29ce641c31d2da82ce912d5ae5c125a43f9e812f/docs/image1.png -------------------------------------------------------------------------------- /docs/install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiceRound/ComfyUI_CryptoCat/29ce641c31d2da82ce912d5ae5c125a43f9e812f/docs/install.png -------------------------------------------------------------------------------- /docs/warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiceRound/ComfyUI_CryptoCat/29ce641c31d2da82ce912d5ae5c125a43f9e812f/docs/warn.png -------------------------------------------------------------------------------- /docs/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiceRound/ComfyUI_CryptoCat/29ce641c31d2da82ce912d5ae5c125a43f9e812f/docs/wechat.jpg -------------------------------------------------------------------------------- /example_workflows/simple1.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 17, 3 | "last_link_id": 21, 4 | "nodes": [ 5 | { 6 | "id": 3, 7 | "type": "KSampler", 8 | "pos": [ 9 | 863, 10 | 186 11 | ], 12 | "size": [ 13 | 315, 14 | 446 15 | ], 16 | "flags": {}, 17 | "order": 7, 18 | "mode": 0, 19 | "inputs": [ 20 | { 21 | "name": "model", 22 | "type": "MODEL", 23 | "link": 1, 24 | "label": "模型" 25 | }, 26 | { 27 | "name": "positive", 28 | "type": "CONDITIONING", 29 | "link": 4, 30 | "label": "正面条件" 31 | }, 32 | { 33 | "name": "negative", 34 | "type": "CONDITIONING", 35 | "link": 6, 36 | "label": "负面条件" 37 | }, 38 | { 39 | "name": "latent_image", 40 | "type": "LATENT", 41 | "link": 2, 42 | "label": "Latent" 43 | }, 44 | { 45 | "name": "seed", 46 | "type": "INT", 47 | "link": 11, 48 | "widget": { 49 | "name": "seed" 50 | }, 51 | "label": "随机种" 52 | } 53 | ], 54 | "outputs": [ 55 | { 56 | "name": "LATENT", 57 | "type": "LATENT", 58 | "links": [ 59 | 7 60 | ], 61 | "slot_index": 0, 62 | "label": "Latent" 63 | } 64 | ], 65 | "properties": { 66 | "Node name for S&R": "KSampler" 67 | }, 68 | "widgets_values": [ 69 | 665777626286280, 70 | "randomize", 71 | 20, 72 | 8, 73 | "euler", 74 | "normal", 75 | 1 76 | ] 77 | }, 78 | { 79 | "id": 4, 80 | "type": "CheckpointLoaderSimple", 81 | "pos": [ 82 | -476, 83 | 301 84 | ], 85 | "size": [ 86 | 315, 87 | 98 88 | ], 89 | "flags": {}, 90 | "order": 0, 91 | "mode": 0, 92 | "inputs": [], 93 | "outputs": [ 94 | { 95 | "name": "MODEL", 96 | "type": "MODEL", 97 | "links": [ 98 | 1, 99 | 13, 100 | 21 101 | ], 102 | "slot_index": 0, 103 | "label": "模型" 104 | }, 105 | { 106 | "name": "CLIP", 107 | "type": "CLIP", 108 | "links": [ 109 | 3, 110 | 5, 111 | 16 112 | ], 113 | "slot_index": 1, 114 | "label": "CLIP" 115 | }, 116 | { 117 | "name": "VAE", 118 | "type": "VAE", 119 | "links": [ 120 | 8, 121 | 17 122 | ], 123 | "slot_index": 2, 124 | "label": "VAE" 125 | } 126 | ], 127 | "properties": { 128 | "Node name for S&R": "CheckpointLoaderSimple" 129 | }, 130 | "widgets_values": [ 131 | "1.5\\majicmixRealistic_v7.safetensors" 132 | ] 133 | }, 134 | { 135 | "id": 5, 136 | "type": "EmptyLatentImage", 137 | "pos": [ 138 | 473, 139 | 609 140 | ], 141 | "size": [ 142 | 315, 143 | 106 144 | ], 145 | "flags": {}, 146 | "order": 1, 147 | "mode": 0, 148 | "inputs": [], 149 | "outputs": [ 150 | { 151 | "name": "LATENT", 152 | "type": "LATENT", 153 | "links": [ 154 | 2 155 | ], 156 | "slot_index": 0, 157 | "label": "Latent" 158 | } 159 | ], 160 | "properties": { 161 | "Node name for S&R": "EmptyLatentImage" 162 | }, 163 | "widgets_values": [ 164 | 512, 165 | 512, 166 | 1 167 | ] 168 | }, 169 | { 170 | "id": 6, 171 | "type": "CLIPTextEncode", 172 | "pos": [ 173 | 168, 174 | 200 175 | ], 176 | "size": [ 177 | 422.84503173828125, 178 | 164.31304931640625 179 | ], 180 | "flags": {}, 181 | "order": 6, 182 | "mode": 0, 183 | "inputs": [ 184 | { 185 | "name": "clip", 186 | "type": "CLIP", 187 | "link": 3, 188 | "label": "CLIP" 189 | }, 190 | { 191 | "name": "text", 192 | "type": "STRING", 193 | "link": 20, 194 | "widget": { 195 | "name": "text" 196 | }, 197 | "label": "文本" 198 | } 199 | ], 200 | "outputs": [ 201 | { 202 | "name": "CONDITIONING", 203 | "type": "CONDITIONING", 204 | "links": [ 205 | 4 206 | ], 207 | "slot_index": 0, 208 | "label": "条件" 209 | } 210 | ], 211 | "properties": { 212 | "Node name for S&R": "CLIPTextEncode" 213 | }, 214 | "widgets_values": [ 215 | "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,", 216 | true 217 | ] 218 | }, 219 | { 220 | "id": 7, 221 | "type": "CLIPTextEncode", 222 | "pos": [ 223 | 413, 224 | 389 225 | ], 226 | "size": [ 227 | 425.27801513671875, 228 | 180.6060791015625 229 | ], 230 | "flags": {}, 231 | "order": 4, 232 | "mode": 0, 233 | "inputs": [ 234 | { 235 | "name": "clip", 236 | "type": "CLIP", 237 | "link": 5, 238 | "label": "CLIP" 239 | } 240 | ], 241 | "outputs": [ 242 | { 243 | "name": "CONDITIONING", 244 | "type": "CONDITIONING", 245 | "links": [ 246 | 6 247 | ], 248 | "slot_index": 0, 249 | "label": "条件" 250 | } 251 | ], 252 | "properties": { 253 | "Node name for S&R": "CLIPTextEncode" 254 | }, 255 | "widgets_values": [ 256 | "text, watermark", 257 | true 258 | ] 259 | }, 260 | { 261 | "id": 8, 262 | "type": "VAEDecode", 263 | "pos": [ 264 | 1209, 265 | 188 266 | ], 267 | "size": [ 268 | 210, 269 | 46 270 | ], 271 | "flags": {}, 272 | "order": 8, 273 | "mode": 0, 274 | "inputs": [ 275 | { 276 | "name": "samples", 277 | "type": "LATENT", 278 | "link": 7, 279 | "label": "Latent" 280 | }, 281 | { 282 | "name": "vae", 283 | "type": "VAE", 284 | "link": 8, 285 | "label": "VAE" 286 | } 287 | ], 288 | "outputs": [ 289 | { 290 | "name": "IMAGE", 291 | "type": "IMAGE", 292 | "links": [ 293 | 14 294 | ], 295 | "slot_index": 0, 296 | "label": "图像" 297 | } 298 | ], 299 | "properties": { 300 | "Node name for S&R": "VAEDecode" 301 | }, 302 | "widgets_values": [] 303 | }, 304 | { 305 | "id": 11, 306 | "type": "RandomSeedNode", 307 | "pos": [ 308 | 509, 309 | 64 310 | ], 311 | "size": [ 312 | 210, 313 | 26 314 | ], 315 | "flags": {}, 316 | "order": 2, 317 | "mode": 0, 318 | "inputs": [], 319 | "outputs": [ 320 | { 321 | "name": "INT", 322 | "type": "INT", 323 | "links": [ 324 | 11 325 | ], 326 | "slot_index": 0, 327 | "label": "INT" 328 | } 329 | ], 330 | "properties": { 331 | "Node name for S&R": "RandomSeedNode" 332 | }, 333 | "widgets_values": [] 334 | }, 335 | { 336 | "id": 12, 337 | "type": "SaveCryptoNode", 338 | "pos": [ 339 | 267, 340 | -157 341 | ], 342 | "size": [ 343 | 400, 344 | 162 345 | ], 346 | "flags": {}, 347 | "order": 5, 348 | "mode": 0, 349 | "inputs": [ 350 | { 351 | "name": "input_anything", 352 | "type": "*", 353 | "link": 19, 354 | "label": "input_anything", 355 | "shape": 7 356 | }, 357 | { 358 | "name": "input_anything1", 359 | "type": "*", 360 | "link": 16, 361 | "label": "input_anything1" 362 | }, 363 | { 364 | "name": "input_anything2", 365 | "type": "*", 366 | "link": 17, 367 | "label": "input_anything2" 368 | }, 369 | { 370 | "name": "input_anything3", 371 | "type": "*", 372 | "link": 21, 373 | "label": "input_anything3" 374 | }, 375 | { 376 | "name": "input_anything4", 377 | "type": "*", 378 | "link": null, 379 | "label": "input_anything4" 380 | } 381 | ], 382 | "outputs": [], 383 | "properties": { 384 | "Node name for S&R": "SaveCryptoNode" 385 | }, 386 | "widgets_values": [ 387 | "ffbb8ad2c5574cdfa702e9b08b0ccaa2", 388 | null 389 | ] 390 | }, 391 | { 392 | "id": 13, 393 | "type": "SaveCryptoBridgeNode", 394 | "pos": [ 395 | 1467, 396 | 199 397 | ], 398 | "size": [ 399 | 189.17031860351562, 400 | 26 401 | ], 402 | "flags": {}, 403 | "order": 9, 404 | "mode": 0, 405 | "inputs": [ 406 | { 407 | "name": "value", 408 | "type": "*", 409 | "link": 14, 410 | "label": "value" 411 | } 412 | ], 413 | "outputs": [ 414 | { 415 | "name": "*", 416 | "type": "*", 417 | "links": [ 418 | 18 419 | ], 420 | "slot_index": 0, 421 | "label": "*" 422 | } 423 | ], 424 | "properties": { 425 | "Node name for S&R": "SaveCryptoBridgeNode" 426 | }, 427 | "widgets_values": [] 428 | }, 429 | { 430 | "id": 14, 431 | "type": "PreviewImage", 432 | "pos": [ 433 | 1698, 434 | 158 435 | ], 436 | "size": [ 437 | 210, 438 | 246 439 | ], 440 | "flags": {}, 441 | "order": 10, 442 | "mode": 0, 443 | "inputs": [ 444 | { 445 | "name": "images", 446 | "type": "IMAGE", 447 | "link": 18, 448 | "label": "图像" 449 | } 450 | ], 451 | "outputs": [], 452 | "properties": { 453 | "Node name for S&R": "PreviewImage" 454 | }, 455 | "widgets_values": [] 456 | }, 457 | { 458 | "id": 17, 459 | "type": "DF_Text", 460 | "pos": [ 461 | -505, 462 | 20 463 | ], 464 | "size": [ 465 | 315, 466 | 58 467 | ], 468 | "flags": {}, 469 | "order": 3, 470 | "mode": 0, 471 | "inputs": [], 472 | "outputs": [ 473 | { 474 | "name": "STRING", 475 | "type": "STRING", 476 | "links": [ 477 | 19, 478 | 20 479 | ], 480 | "slot_index": 0, 481 | "label": "STRING" 482 | } 483 | ], 484 | "properties": { 485 | "Node name for S&R": "DF_Text" 486 | }, 487 | "widgets_values": [ 488 | "" 489 | ] 490 | } 491 | ], 492 | "links": [ 493 | [ 494 | 1, 495 | 4, 496 | 0, 497 | 3, 498 | 0, 499 | "MODEL" 500 | ], 501 | [ 502 | 2, 503 | 5, 504 | 0, 505 | 3, 506 | 3, 507 | "LATENT" 508 | ], 509 | [ 510 | 3, 511 | 4, 512 | 1, 513 | 6, 514 | 0, 515 | "CLIP" 516 | ], 517 | [ 518 | 4, 519 | 6, 520 | 0, 521 | 3, 522 | 1, 523 | "CONDITIONING" 524 | ], 525 | [ 526 | 5, 527 | 4, 528 | 1, 529 | 7, 530 | 0, 531 | "CLIP" 532 | ], 533 | [ 534 | 6, 535 | 7, 536 | 0, 537 | 3, 538 | 2, 539 | "CONDITIONING" 540 | ], 541 | [ 542 | 7, 543 | 3, 544 | 0, 545 | 8, 546 | 0, 547 | "LATENT" 548 | ], 549 | [ 550 | 8, 551 | 4, 552 | 2, 553 | 8, 554 | 1, 555 | "VAE" 556 | ], 557 | [ 558 | 11, 559 | 11, 560 | 0, 561 | 3, 562 | 4, 563 | "INT" 564 | ], 565 | [ 566 | 13, 567 | 4, 568 | 0, 569 | 12, 570 | 0, 571 | "*" 572 | ], 573 | [ 574 | 14, 575 | 8, 576 | 0, 577 | 13, 578 | 0, 579 | "*" 580 | ], 581 | [ 582 | 16, 583 | 4, 584 | 1, 585 | 12, 586 | 1, 587 | "*" 588 | ], 589 | [ 590 | 17, 591 | 4, 592 | 2, 593 | 12, 594 | 2, 595 | "*" 596 | ], 597 | [ 598 | 18, 599 | 13, 600 | 0, 601 | 14, 602 | 0, 603 | "IMAGE" 604 | ], 605 | [ 606 | 19, 607 | 17, 608 | 0, 609 | 12, 610 | 0, 611 | "*" 612 | ], 613 | [ 614 | 20, 615 | 17, 616 | 0, 617 | 6, 618 | 1, 619 | "STRING" 620 | ], 621 | [ 622 | 21, 623 | 4, 624 | 0, 625 | 12, 626 | 3, 627 | "*" 628 | ] 629 | ], 630 | "groups": [], 631 | "config": {}, 632 | "extra": { 633 | "ds": { 634 | "scale": 1.167184107045001, 635 | "offset": [ 636 | 793.6073786214808, 637 | 404.0759897478937 638 | ] 639 | } 640 | }, 641 | "version": 0.4 642 | } -------------------------------------------------------------------------------- /file_compressor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import zlib 3 | import os 4 | 5 | 6 | class FileCompressor: 7 | @staticmethod 8 | def obfuscate(data, password): 9 | return bytes( 10 | [b ^ ord(password[i % len(password)]) for (i, b) in enumerate(data)] 11 | ) 12 | 13 | @staticmethod 14 | def compress_string(string_data, zip_path, password): 15 | compressed_data = zlib.compress(string_data.encode("utf-8")) 16 | obfuscated_data = FileCompressor.obfuscate(compressed_data, password) 17 | with open(zip_path, "wb") as zipf: 18 | zipf.write(obfuscated_data) 19 | 20 | @staticmethod 21 | def decompress_to_string(zip_path, password): 22 | with open(zip_path, "rb") as zipf: 23 | obfuscated_data = zipf.read() 24 | compressed_data = FileCompressor.obfuscate(obfuscated_data, password) 25 | return zlib.decompress(compressed_data).decode("utf-8") 26 | 27 | @staticmethod 28 | def compress_to_json(data, zip_path, password): 29 | try: 30 | json_string = json.dumps(data, indent=4, ensure_ascii=False) 31 | FileCompressor.compress_string(json_string, zip_path, password) 32 | except Exception as e: 33 | print(f"Compression failed: {e}") 34 | 35 | @staticmethod 36 | def decompress_from_json(zip_path, password): 37 | try: 38 | if not os.path.exists(zip_path): 39 | raise FileNotFoundError(f"{zip_path} does not exist") 40 | json_string = FileCompressor.decompress_to_string(zip_path, password) 41 | return json.loads(json_string) 42 | except Exception as e: 43 | print(f"Decompression failed: {e}") 44 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set "requirements_txt=%~dp0\requirements.txt" 4 | set "requirements_repair_txt=%~dp0\repair_dependency_list.txt" 5 | set "python_exec=..\..\..\python_embeded\python.exe" 6 | set "aki_python_exec=..\..\python\python.exe" 7 | 8 | echo Installing EasyUse Requirements... 9 | 10 | if exist "%python_exec%" ( 11 | echo Installing with ComfyUI Portable 12 | "%python_exec%" -s -m pip install -r "%requirements_txt%" 13 | )^ 14 | else if exist "%aki_python_exec%" ( 15 | echo Installing with ComfyUI Aki 16 | "%aki_python_exec%" -s -m pip install -r "%requirements_txt%" 17 | for /f "delims=" %%i in (%requirements_repair_txt%) do ( 18 | %aki_python_exec% -s -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple "%%i" 19 | ) 20 | )^ 21 | else ( 22 | echo Installing with system Python 23 | pip install -r "%requirements_txt%" 24 | ) 25 | 26 | pause -------------------------------------------------------------------------------- /js/catconfig.js: -------------------------------------------------------------------------------- 1 | import{api}from"../../../scripts/api.js";import{ComfyApp,app}from"../../../scripts/app.js";import{showToast}from"./mycat.js";export const UserTokenKey="cryptocat_user_token";async function showKeygenMessageBox(e){const t=document.createElement("div");document.body.appendChild(t);const{createApp:n,ref:o}=Vue,a=n({template:'\n \n \n \n \n \n \n \n \n \n \n \n 0表示永久有效\n \n \n \n \n
\n \n 生成序列号\n \n
\n \n ',setup(){const n=o(!0),r=o({templateId:"",expireDate:"",useDays:0}),l=o(""),i=o(!1);return{title:e,dialogVisible:n,form:r,result:l,loading:i,generateSerial:async function(){if(r.value.templateId.trim()){i.value=!0;try{const e=await async function(e,t,n){try{const o={template_id:e};t&&(o.expire_date=t),n&&(o.use_days=n);const a=await api.fetchApi("/cryptocat/keygen",{method:"POST",body:JSON.stringify(o)}),r=await a.json();if(!a.ok)throw new Error(r.error_msg||`HTTP error! status: ${a.status}`);return r.serial_number||r.error_msg||"获取失败:服务器返回数据格式错误"}catch(e){throw new Error(`获取序列号失败: ${e.message}`)}}(r.value.templateId.trim(),r.value.expireDate,r.value.useDays);l.value=e}catch(e){ElementPlus.ElMessage.error(e.message||"未知错误")}finally{i.value=!1}}else ElementPlus.ElMessage.warning("请输入template_id")},handleClose:function(){a.unmount(),t.remove()},handleClear:function(){l.value="",r.value={templateId:"",expireDate:"",useDays:0}},copyToClipboard:async function(){try{await navigator.clipboard.writeText(l.value),ElementPlus.ElMessage.success("序列号已复制到剪贴板")}catch(e){ElementPlus.ElMessage.error("复制失败")}},disabledDate:e=>e.getTime()e.length>0&&n.test(e)))}app.registerExtension({name:"CryptoCat.config",async setup(){app.ui.settings.addSetting({id:"CryptoCat.Keygen.name",name:"输入template_id,计算序列号",type:()=>{const e=document.createElement("tr"),t=document.createElement("td"),n=document.createElement("input");return n.type="button",n.value="算号器",n.style.borderRadius="8px",n.style.padding="8px 16px",n.style.fontSize="14px",n.style.cursor="pointer",n.style.border="1px solid #666",n.style.backgroundColor="#444",n.style.color="#fff",n.onmouseover=()=>{n.style.backgroundColor="#555"},n.onmouseout=()=>{n.style.backgroundColor="#444"},n.onclick=()=>{showKeygenMessageBox("算号器","算号器","question")},t.appendChild(n),e.appendChild(t),e}}),app.ui.settings.addSetting({id:"CryptoCat.User.clear",name:"清理所有数据",tooltip:"完成镜像部署前最后一步,清理所有数据",type:()=>{const e=document.createElement("tr"),t=document.createElement("td"),n=document.createElement("input");return n.type="button",n.value="清理",n.style.borderRadius="8px",n.style.padding="8px 16px",n.style.fontSize="14px",n.style.cursor="pointer",n.style.border="1px solid #666",n.style.backgroundColor="#444",n.style.color="#fff",n.onclick=async()=>{localStorage.removeItem(UserTokenKey),localStorage.removeItem("Comfy.Settings.CryptoCat.User.long_token"),app.ui.settings.setSettingValue("CryptoCat.User.long_token",""),await api.fetchApi("/cryptocat/clear"),showToast("清理成功")},t.appendChild(n),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)&&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.User.login",name:"登录RiceRound账户",type:()=>{const e=document.createElement("tr"),t=document.createElement("td"),n=document.createElement("input");return n.type="button",n.value="登录RiceRound账户",n.style.borderRadius="8px",n.style.padding="8px 16px",n.style.fontSize="14px",n.style.cursor="pointer",n.style.border="1px solid #3498db",n.style.backgroundColor="#3498db",n.style.color="#fff",n.onmouseover=()=>{n.style.backgroundColor="#2980b9"},n.onmouseout=()=>{n.style.backgroundColor="#3498db"},n.onclick=async()=>{try{const e=await api.fetchApi("/cryptocat/login",{method:"POST"}),t=await e.json();showToast(t.error_msg||"登录中...")}catch(e){showToast("登录请求失败","error")}},t.appendChild(n),e.appendChild(t),e}}),app.ui.settings.addSetting({id:"CryptoCat.Setting.auto_overwrite",name:"自动覆盖更新同id工作流",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})})}})}}); -------------------------------------------------------------------------------- /js/mycat.js: -------------------------------------------------------------------------------- 1 | import{ComfyApp,app}from"../../../scripts/app.js";import{api}from"../../../scripts/api.js";export async function loadResource(t,e=""){if(!document.querySelector(`script[src="${t}"]`)){const e=document.createElement("script");e.src=t,document.head.appendChild(e);try{await new Promise(((o,a)=>{e.onload=o,e.onerror=()=>a(new Error(`Failed to load script: ${t}`))}))}catch(t){}}if(e){if(!document.querySelector(`link[href="${e}"]`)){const t=document.createElement("link");t.rel="stylesheet",t.href=e,document.head.appendChild(t);try{await new Promise(((o,a)=>{t.onload=o,t.onerror=()=>a(new Error(`Failed to load stylesheet: ${e}`))}))}catch(t){}}}}let toastHasLoaded=!1;async function loadToast(){if(!toastHasLoaded){const t="https://cdn.jsdelivr.net/npm/toastify-js",e="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css";await loadResource(t,e),toastHasLoaded=!0}}export async function showToast(t,e="info",o=3e3){await loadToast(),"info"==e?Toastify({text:t,duration:o,close:!1,gravity:"top",position:"center",backgroundColor:"#3498db",stopOnFocus:!1}).showToast():"error"==e?Toastify({text:t,duration:o,close:!0,gravity:"top",position:"center",backgroundColor:"#FF4444",stopOnFocus:!0}).showToast():"warning"==e&&Toastify({text:t,duration:o,close:!0,gravity:"top",position:"center",backgroundColor:"#FFA500",stopOnFocus:!0}).showToast()}let messageBoxHasLoaded=!1;export async function loadMessageBox(){messageBoxHasLoaded||(await loadResource("https://cdn.jsdelivr.net/npm/sweetalert2@11","https://cdn.jsdelivr.net/npm/@sweetalert2/theme-bootstrap-4/bootstrap-4.css"),messageBoxHasLoaded=!0)}async function serverShowMessageBox(t,e){await loadMessageBox();const o={...t,heightAuto:!1};try{const t=await Swal.fire(o);api.fetchApi("/cryptocat/message",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`id=${e}&message=${t.isConfirmed?"1":"0"}`})}catch(t){api.fetchApi("/cryptocat/message",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`id=${e}&message=0`})}window.addEventListener("beforeunload",(function(){fetch("/cryptocat/message",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`id=${e}&message=0`,keepalive:!0})}),{once:!0})}api.addEventListener("cryptocat_toast",(t=>{showToast(t.detail.content,t.detail.type,t.detail.duration)})),api.addEventListener("cryptocat_dialog",(t=>{serverShowMessageBox(JSON.parse(t.detail.json_content),t.detail.id)}));export async function showMessageBox(t,e,o){await loadMessageBox(),Swal.fire({title:t,text:e,icon:o})}let loginDialogHasLoaded=!1;async function waitForObject(t,e,o=5e3){return new Promise(((a,n)=>{const s=Date.now(),i=()=>{Date.now()-s>o?n(new Error(`Timeout waiting for ${e} to load`)):t()?a():setTimeout(i,50)};i()}))}export async function initLoginDialog(t=!1){if(t?localStorage.setItem("cryptocat_api_host","test.riceround.online"):localStorage.removeItem("cryptocat_api_host"),!loginDialogHasLoaded)try{const t="https://cdn.staticfile.org/vue/3.2.47/vue.global.js";await loadResource(t,""),await waitForObject((()=>window.Vue),"vue");const e="https://cdn.jsdelivr.net/npm/element-plus",o="https://cdn.jsdelivr.net/npm/element-plus/dist/index.css";if(await loadResource(e,o),await waitForObject((()=>window.ElementPlus),"element-plus"),null==window.DialogLib){const t="cryptocat/static/dialog-lib.umd.cjs",e="cryptocat/static/mycat.css";await loadResource(t,e),await waitForObject((()=>window.DialogLib),"showLoginDialog")}loginDialogHasLoaded=!0}catch(t){throw t}}function generateUUID(){let t="";for(let e=0;e<32;e++){t+=Math.floor(16*Math.random()).toString(16)}return t}api.addEventListener("cryptocat_login_dialog",(t=>{const e=t.detail.client_key,o=t.detail.title;window.DialogLib.showLoginDialog({title:o,spyNetworkError:!0,mainKey:"cryptocat"}).then((t=>{api.fetchApi("/cryptocat/auth_callback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t,client_key:e})}),showToast("登录成功")})).catch((t=>{showToast("登录失败","error")}))})),api.addEventListener("cryptocat_clear_user_info",(async t=>{const e=t.detail.clear_key;"all"==e?(localStorage.removeItem("Comfy.Settings.CryptoCat.User.long_token"),localStorage.removeItem("cryptocat_user_token"),localStorage.removeItem("cryptocat_long_token")):"long_token"==e?(localStorage.removeItem("Comfy.Settings.CryptoCat.User.long_token"),localStorage.removeItem("cryptocat_long_token")):"user_token"==e&&localStorage.removeItem("cryptocat_user_token")})),app.registerExtension({name:"cryptocat.mycat",setup(){initLoginDialog()},async beforeRegisterNodeDef(t,e,o){if("SaveCryptoNode"===e.name){const e=400,o="input_anything",a=t.prototype.onConnectionsChange;t.prototype.onConnectionsChange=function(t,e,n,s,i){if(!s||1!==t)return;let r=!1;if(!n&&this.inputs.length>1){const t=(new Error).stack;t.includes("LGraphNode.prototype.connect")||t.includes("LGraphNode.connect")||t.includes("loadGraphData")||!this.inputs[e].name.startsWith(o)||(this.removeInput(e),r=!0)}if(n){if(0===this.inputs.length||null!=this.inputs[this.inputs.length-1].link&&this.inputs[this.inputs.length-1].name.startsWith(o)){const t=0===this.inputs.length?o:`${o}${this.inputs.length}`;this.addInput(t,"*"),r=!0}}if(r)for(let t=0;t{const e=generateUUID(),o=t.widgets.find((t=>"template_id"===t.name));o&&(o.value=e)}))}}); -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui_cryptocat" 3 | description = "a lightweight open-source node for ComfyUI, designed to simplify workflows while providing encryption protection for them." 4 | version = "2.2.3" 5 | license = {file = "LICENSE"} 6 | dependencies = ["aiohttp", "pyzipper", "websockets"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/RiceRound/ComfyUI_CryptoCat" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "riceround" 14 | DisplayName = "ComfyUI_CryptoCat" 15 | Icon = "https://www.riceround.online/CryptoCat.svg" 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | pyzipper 3 | websockets 4 | -------------------------------------------------------------------------------- /static/mycat.css: -------------------------------------------------------------------------------- 1 | .key-box { 2 | background-color: #f5f5f5; 3 | padding: 10px; 4 | border: 1px solid #dcdcdc; 5 | border-radius: 4px; 6 | white-space: pre-wrap; 7 | word-break: break-all; 8 | font-size: 12px; 9 | margin-right: 5px; 10 | flex: 1; 11 | } 12 | 13 | .button-group { 14 | text-align: right; 15 | margin-top: 20px; 16 | } 17 | 18 | 19 | .hint-text { 20 | font-size: 12px; 21 | color: #909399; 22 | margin-left: 8px; 23 | display: inline-flex; 24 | align-items: center; 25 | } -------------------------------------------------------------------------------- /trim_workflow.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import copy 3 | import os 4 | import random 5 | from server import PromptServer 6 | from .updown_workflow import DownloadWorkflow 7 | import folder_paths 8 | import json 9 | from .file_compressor import FileCompressor 10 | from typing import Dict, Any 11 | from .auth_unit import AuthUnit 12 | 13 | 14 | class CryptoWorkflow: 15 | def __init__(self, workflow, prompt, template_id): 16 | self.workflow = copy.deepcopy(workflow) 17 | self.prompt = copy.deepcopy(prompt) 18 | self.template_id = template_id 19 | self.workflow_nodes_dict = {} 20 | self.link_owner_map = defaultdict(dict) 21 | self.node_prompt_map = {} 22 | self.last_node_id = 0 23 | self.last_link_id = 0 24 | self.save_crypto_node_id = 0 25 | self.crypto_bridge_node_id = 0 26 | self.input_nodes_ids = set() 27 | self.output_nodes_ids = set() 28 | self.crypto_nodes_ids = set() 29 | self.crypto_result = {} 30 | 31 | def invalid_workflow(self): 32 | for node in self.workflow["nodes"]: 33 | node_type = node.get("type", "") 34 | if node_type == "SaveCryptoBridgeNode": 35 | if self.crypto_bridge_node_id == 0: 36 | self.crypto_bridge_node_id = int(node["id"]) 37 | else: 38 | raise ValueError( 39 | "Error: Multiple 'SaveCryptoBridgeNode' instances found." 40 | ) 41 | continue 42 | elif node_type == "SaveCryptoNode": 43 | if self.save_crypto_node_id == 0: 44 | self.save_crypto_node_id = int(node["id"]) 45 | else: 46 | raise ValueError( 47 | "Error: Multiple 'SaveCryptoNode' instances found." 48 | ) 49 | continue 50 | if self.save_crypto_node_id == 0: 51 | raise ValueError("Error: No 'SaveCryptoNode' instances found.") 52 | if self.crypto_bridge_node_id == 0: 53 | raise ValueError("Error: No 'SaveCryptoBridgeNode' instances found.") 54 | 55 | def load_workflow(self): 56 | simplify_workflow = self.workflow 57 | self.workflow_nodes_dict = { 58 | int(node["id"]): node for node in simplify_workflow["nodes"] 59 | } 60 | for node in simplify_workflow["nodes"]: 61 | output_nodes = node.get("outputs", []) 62 | if not output_nodes: 63 | continue 64 | for output in output_nodes: 65 | links = output.get("links", []) 66 | if not links: 67 | continue 68 | for link in links: 69 | link = int(link) 70 | self.link_owner_map[link]["links"] = copy.deepcopy(links) 71 | self.link_owner_map[link]["slot_index"] = output.get( 72 | "slot_index", 0 73 | ) 74 | self.link_owner_map[link]["owner_id"] = int(node["id"]) 75 | self.link_owner_map[link]["type"] = output.get("type", "") 76 | self.last_node_id = int(simplify_workflow.get("last_node_id", 0)) 77 | self.last_link_id = int(simplify_workflow.get("last_link_id", 0)) 78 | 79 | def load_prompt(self): 80 | simplify_prompt = self.prompt 81 | self.node_prompt_map = { 82 | int(node_id): node for (node_id, node) in simplify_prompt.items() 83 | } 84 | 85 | def analysis_node(self): 86 | self.input_nodes_ids.clear() 87 | self.output_nodes_ids.clear() 88 | self.crypto_nodes_ids.clear() 89 | 90 | def find_input_nodes(node_id, visited=None): 91 | if visited is None: 92 | visited = set() 93 | if node_id in visited: 94 | return 95 | visited.add(node_id) 96 | self.input_nodes_ids.add(node_id) 97 | node = self.workflow_nodes_dict.get(node_id) 98 | if not node: 99 | return 100 | for input_node in node.get("inputs", []): 101 | input_link = input_node.get("link") 102 | if input_link is not None and input_link in self.link_owner_map: 103 | owner_id = self.link_owner_map[input_link]["owner_id"] 104 | find_input_nodes(owner_id, visited) 105 | 106 | for input_node in self.workflow_nodes_dict[self.save_crypto_node_id].get( 107 | "inputs", [] 108 | ): 109 | if input_node["name"] and input_node["name"].startswith("input_anything"): 110 | input_link = input_node["link"] 111 | if input_link is not None and input_link in self.link_owner_map: 112 | owner_id = self.link_owner_map[input_link]["owner_id"] 113 | find_input_nodes(owner_id) 114 | 115 | def find_output_nodes(node_id, visited=None): 116 | if visited is None: 117 | visited = set() 118 | if node_id in visited: 119 | return 120 | visited.add(node_id) 121 | self.output_nodes_ids.add(node_id) 122 | node = self.workflow_nodes_dict.get(node_id) 123 | if not node: 124 | return 125 | for output in node.get("outputs", []): 126 | if not output or not output.get("links"): 127 | continue 128 | for link in output.get("links", []): 129 | if not link: 130 | continue 131 | if link is not None: 132 | for workflow_link in self.workflow.get("links", []): 133 | if workflow_link[0] == link: 134 | target_node_id = workflow_link[3] 135 | find_output_nodes(target_node_id, visited) 136 | 137 | output_links = set() 138 | for output_node in self.workflow_nodes_dict[self.crypto_bridge_node_id].get( 139 | "outputs", [] 140 | ): 141 | _links = output_node["links"] 142 | output_links.update(_links) 143 | for link in self.workflow.get("links", []): 144 | if len(link) > 3 and link[0] in output_links: 145 | find_output_nodes(link[3]) 146 | self.crypto_nodes_ids = ( 147 | self.workflow_nodes_dict.keys() 148 | - self.input_nodes_ids 149 | - self.output_nodes_ids 150 | ) 151 | self.crypto_nodes_ids = self.crypto_nodes_ids - { 152 | self.save_crypto_node_id, 153 | self.crypto_bridge_node_id, 154 | } 155 | 156 | def calculate_crypto_result(self, crypto_file_name): 157 | self.crypto_result = {"prompt": {}, "workflow": {}, "outputs": []} 158 | for node_id in self.crypto_nodes_ids: 159 | if node_id in self.node_prompt_map: 160 | self.crypto_result["prompt"][node_id] = self.node_prompt_map[node_id] 161 | if node_id in self.workflow_nodes_dict: 162 | self.crypto_result["workflow"][node_id] = self.workflow_nodes_dict[ 163 | node_id 164 | ] 165 | crypto_bridge_node = self.node_prompt_map[self.crypto_bridge_node_id] 166 | for input_name, input_value in crypto_bridge_node.get("inputs", {}).items(): 167 | if isinstance(input_value, list) and len(input_value) == 2: 168 | self.crypto_result["outputs"] = input_value[0], input_value[1] 169 | json_result = json.dumps(self.crypto_result, indent=4, ensure_ascii=False) 170 | save_dir = folder_paths.temp_directory 171 | with open(os.path.join(save_dir, crypto_file_name), "w", encoding="utf-8") as f: 172 | f.write(json_result) 173 | return save_dir 174 | 175 | def output_workflow_simple_shell(self, output_workflow_name): 176 | simplify_workflow = copy.deepcopy(self.workflow) 177 | save_crypto_node = None 178 | crypto_bridge_node = None 179 | for node in simplify_workflow["nodes"]: 180 | if node["id"] == self.save_crypto_node_id: 181 | save_crypto_node = node 182 | if node["id"] == self.crypto_bridge_node_id: 183 | crypto_bridge_node = node 184 | except_nodes_ids = self.crypto_nodes_ids 185 | except_nodes_ids.add(self.crypto_bridge_node_id) 186 | simplify_workflow["nodes"] = [ 187 | node 188 | for node in simplify_workflow["nodes"] 189 | if int(node["id"]) not in except_nodes_ids 190 | ] 191 | if save_crypto_node is None: 192 | raise ValueError("SaveCryptoNode not found in workflow") 193 | if crypto_bridge_node is None: 194 | raise ValueError("CryptoBridgeNode not found in workflow") 195 | save_crypto_node["type"] = "DecodeCryptoNode" 196 | save_crypto_node["widgets_values"] = ( 197 | [save_crypto_node.get("widgets_values", [None])[0]] 198 | if "widgets_values" in save_crypto_node 199 | else [None] 200 | ) 201 | save_crypto_node["widgets_values"].append("") 202 | save_crypto_node["properties"] = {"Node name for S&R": "DecodeCryptoNode"} 203 | if "outputs" not in crypto_bridge_node: 204 | save_crypto_node["outputs"] = [] 205 | else: 206 | save_crypto_node["outputs"] = copy.deepcopy(crypto_bridge_node["outputs"]) 207 | output_nodes_ids = [int(node["id"]) for node in simplify_workflow["nodes"]] 208 | filtered_links = [] 209 | for link in simplify_workflow["links"]: 210 | if len(link) < 5: 211 | continue 212 | if link[1] == self.crypto_bridge_node_id: 213 | link[1] = self.save_crypto_node_id 214 | if link[1] in output_nodes_ids and link[3] in output_nodes_ids: 215 | filtered_links.append(link) 216 | else: 217 | pass 218 | simplify_workflow["links"] = filtered_links 219 | simplify_workflow.pop("groups", None) 220 | save_dir = folder_paths.output_directory 221 | with open( 222 | os.path.join(folder_paths.output_directory, output_workflow_name), 223 | "w", 224 | encoding="utf-8", 225 | ) as f: 226 | json.dump(simplify_workflow, f, ensure_ascii=False, indent=4) 227 | return save_dir 228 | 229 | def save_original_workflow(self, output_workflow_name, save_dir): 230 | with open( 231 | os.path.join(save_dir, output_workflow_name), "w", encoding="utf-8" 232 | ) as f: 233 | json.dump(self.workflow, f, ensure_ascii=False, indent=4) 234 | return save_dir 235 | 236 | def save_original_prompt(self, output_prompt_name, save_dir): 237 | with open( 238 | os.path.join(save_dir, output_prompt_name), "w", encoding="utf-8" 239 | ) as f: 240 | json.dump(self.prompt, f, ensure_ascii=False, indent=4) 241 | return save_dir 242 | 243 | 244 | class DecodeCryptoWorkflow: 245 | def __init__(self, prompt, workflow, template_id): 246 | self.prompt = prompt 247 | self.workflow = workflow 248 | self.template_id = template_id 249 | self.crypto_result = {} 250 | self.input_anything_map = {} 251 | 252 | def calculate_input_anything_map(self): 253 | self.input_anything_map.clear() 254 | for node_id, node in self.prompt.items(): 255 | if node.get("class_type") == "DecodeCryptoNode": 256 | for input_name, input_value in node.get("inputs", {}).items(): 257 | if input_name.startswith("input_anything"): 258 | if isinstance(input_value, list) and len(input_value) == 2: 259 | input_link_key = f"{input_value[0]}_{input_value[1]}" 260 | self.input_anything_map[input_link_key] = input_name 261 | return self.input_anything_map 262 | 263 | def load_crypto_prompt(self, serial_number_token, user_token=None): 264 | if not user_token: 265 | user_token = AuthUnit().read_user_token() 266 | content = DownloadWorkflow().download_workflow( 267 | self.template_id, serial_number_token, user_token 268 | ) 269 | if not isinstance(content, str) or content == "": 270 | raise ValueError("failed to download workflow") 271 | self.crypto_result = json.loads(content) 272 | return self.crypto_result["prompt"] 273 | 274 | def get_hidden_input(self, input_value): 275 | if isinstance(input_value, list) and len(input_value) == 2: 276 | key = f"{input_value[0]}_{input_value[1]}" 277 | return self.input_anything_map.get(key, None) 278 | 279 | def get_outputs(self): 280 | return self.crypto_result["outputs"] 281 | 282 | 283 | class WorkflowTrimHandler: 284 | @staticmethod 285 | def onprompt_handler(json_data: Dict[str, Any]) -> Dict[str, Any]: 286 | prompt = json_data["prompt"] 287 | has_new_component = False 288 | has_old_component = False 289 | for node in prompt.values(): 290 | class_type = node.get("class_type") 291 | if class_type == "SaveCryptoNode": 292 | has_new_component = True 293 | break 294 | if class_type == "ExcuteCryptoNode": 295 | has_old_component = True 296 | break 297 | if has_new_component: 298 | user_token, error_msg, error_code = AuthUnit().get_user_token() 299 | print( 300 | f"user_token: {user_token}, error_msg: {error_msg}, error_code: {error_code}" 301 | ) 302 | if not user_token: 303 | if error_code == 401 or error_code == -3: 304 | AuthUnit().login_dialog("输出CryptoCat加密工作流,请先完成登录") 305 | json_data["prompt"] = {} 306 | else: 307 | PromptServer.instance.send_sync( 308 | "cryptocat_toast", 309 | {"content": f"无法完成鉴权登录,{error_msg}", "type": "error"}, 310 | ) 311 | elif has_old_component: 312 | prompt = WorkflowTrimHandler.replace_prompt(prompt) 313 | json_data["prompt"] = prompt 314 | return json_data 315 | 316 | @staticmethod 317 | def replace_prompt(prompt: Dict[str, Any]) -> Dict[str, Any]: 318 | if not prompt: 319 | raise ValueError("Invalid JSON format.") 320 | crypto_file_path = "" 321 | excute_crypto_id = None 322 | for node_id, node in list(prompt.items()): 323 | if node.get("class_type") == "ExcuteCryptoNode": 324 | crypto_file_path = node["inputs"].get("crypto_file_path") 325 | excute_crypto_id = node_id 326 | del prompt[node_id] 327 | break 328 | if not crypto_file_path: 329 | print("No 'ExcuteCryptoNode' found in prompt.") 330 | return prompt 331 | inject_json = FileCompressor.decompress_from_json(crypto_file_path, "19040822") 332 | output_images_ids = inject_json.pop("output_images_ids") 333 | for node in prompt.values(): 334 | if node.get("class_type") == "CryptoCatImage": 335 | if excute_crypto_id in node["inputs"]["images"]: 336 | node["inputs"]["images"] = output_images_ids 337 | random_seed_node = next( 338 | ( 339 | node 340 | for node in inject_json.values() 341 | if node.get("class_type") == "RandomSeedNode" 342 | ), 343 | None, 344 | ) 345 | if random_seed_node: 346 | random_seed_node["inputs"]["is_changed"] = random.randint(0, 999999) 347 | prompt.update(inject_json) 348 | return prompt 349 | -------------------------------------------------------------------------------- /updown_workflow.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import requests 4 | from .url_config import CatUrlConfig, download_crypto_workflow, user_upload_workflow 5 | from .utils import combine_files, get_machine_id, get_local_app_setting_path 6 | from .auth_unit import AuthUnit 7 | from server import PromptServer 8 | from aiohttp import web 9 | import time 10 | 11 | 12 | class Cancelled(Exception): 13 | pass 14 | 15 | 16 | class MessageHolder: 17 | stash = {} 18 | messages = {} 19 | cancelled = False 20 | 21 | @classmethod 22 | def addMessage(cls, id, message): 23 | if message == "__cancel__": 24 | cls.messages = {} 25 | cls.cancelled = True 26 | elif message == "__start__": 27 | cls.messages = {} 28 | cls.stash = {} 29 | cls.cancelled = False 30 | else: 31 | cls.messages[str(id)] = message 32 | 33 | @classmethod 34 | def waitForMessage(cls, id, period=0.1, timeout=60): 35 | sid = str(id) 36 | cls.messages.clear() 37 | start_time = time.time() 38 | while not sid in cls.messages and not "-1" in cls.messages: 39 | if cls.cancelled: 40 | cls.cancelled = False 41 | raise Cancelled() 42 | if time.time() - start_time > timeout: 43 | raise Cancelled("Operation timed out") 44 | time.sleep(period) 45 | if cls.cancelled: 46 | cls.cancelled = False 47 | raise Cancelled() 48 | message = cls.messages.pop(str(id), None) or cls.messages.pop("-1") 49 | return message.strip() 50 | 51 | 52 | routes = PromptServer.instance.routes 53 | 54 | 55 | @routes.post("/cryptocat/message") 56 | async def message_handler(request): 57 | post = await request.post() 58 | MessageHolder.addMessage(post.get("id"), post.get("message")) 59 | return web.json_response({}) 60 | 61 | 62 | class UploadWorkflow: 63 | def __init__(self, user_token): 64 | self.user_token = user_token 65 | 66 | def check_workflow(self, template_id): 67 | url = CatUrlConfig().workflow_url 68 | headers = {"Authorization": f"Bearer {self.user_token}"} 69 | response = requests.get( 70 | url, params={"template_id": template_id, "action": "check"}, headers=headers 71 | ) 72 | if response.status_code == 200: 73 | response_data = response.json() 74 | error_code = response_data.get("code") 75 | error_msg = response_data.get("message") 76 | return error_code, error_msg 77 | else: 78 | return -1, "" 79 | 80 | def upload_workflow(self, template_id, temp_dir): 81 | error_code, error_msg = self.check_workflow(template_id) 82 | if error_code == 1: 83 | auto_overwrite = UserWorkflowSetting().get_auto_overwrite() 84 | if not auto_overwrite: 85 | json_content = { 86 | "title": "已经存在相同template_id的数据,是否覆盖?", 87 | "icon": "info", 88 | "confirmButtonText": "覆盖", 89 | "cancelButtonText": "取消", 90 | "showCancelButton": True, 91 | "timer": 50000, 92 | } 93 | PromptServer.instance.send_sync( 94 | "cryptocat_dialog", 95 | {"json_content": json.dumps(json_content), "id": template_id}, 96 | ) 97 | msg_result = MessageHolder.waitForMessage(template_id, timeout=60000) 98 | try: 99 | result_code = int(msg_result) 100 | except ValueError: 101 | print("crypto cat upload cancel: Invalid response format") 102 | return False 103 | if result_code != 1: 104 | print("crypto cat upload cancel: User rejected overwrite") 105 | return False 106 | elif error_code != 0: 107 | print(f"crypto cat upload failed: {error_msg}") 108 | PromptServer.instance.send_sync( 109 | "cryptocat_toast", {"content": f"异常情况,{error_msg}", "type": "error"} 110 | ) 111 | return False 112 | combine_target_path = os.path.join(temp_dir, f"cryptocat_{template_id}.zip") 113 | combine_file_paths = [] 114 | for file in [ 115 | f"crypto_{template_id}.json", 116 | f"original_workflow_{template_id}.json", 117 | f"original_prompt_{template_id}.json", 118 | ]: 119 | src_path = os.path.join(temp_dir, file) 120 | combine_file_paths.append(src_path) 121 | combine_files(combine_file_paths, template_id, combine_target_path) 122 | for file_path in combine_file_paths: 123 | os.remove(file_path) 124 | success, message = user_upload_workflow( 125 | template_id, combine_target_path, self.user_token 126 | ) 127 | if success: 128 | PromptServer.instance.send_sync( 129 | "cryptocat_toast", {"content": "上传成功", "type": "info", "duration": 5000} 130 | ) 131 | else: 132 | PromptServer.instance.send_sync( 133 | "cryptocat_toast", 134 | {"content": f"上传失败: {message}", "type": "error", "duration": 5000}, 135 | ) 136 | return success 137 | 138 | def generate_serial_number( 139 | self, template_id, expire_date=None, use_days=None, count=1 140 | ): 141 | url = CatUrlConfig().serial_number_url 142 | headers = {"Authorization": f"Bearer {self.user_token}"} 143 | params = {"template_id": template_id, "count": count} 144 | if expire_date is not None: 145 | params["expire_date"] = expire_date 146 | if use_days is not None: 147 | params["use_days"] = use_days 148 | response = requests.get(url, params=params, headers=headers) 149 | if response.status_code == 200: 150 | response_data = response.json() 151 | error_code = response_data.get("code") 152 | error_msg = response_data.get("message", "") 153 | if error_code == 0: 154 | return response_data.get("data", []) 155 | else: 156 | raise ValueError(f"failed to generate serial number: {error_msg}") 157 | else: 158 | raise ValueError( 159 | f"failed to generate serial number: {response.status_code}" 160 | ) 161 | 162 | 163 | class DownloadWorkflow: 164 | _instance = None 165 | _cache = {} 166 | 167 | def __new__(cls): 168 | if cls._instance is None: 169 | cls._instance = super(DownloadWorkflow, cls).__new__(cls) 170 | return cls._instance 171 | 172 | def __init__(self): 173 | if not hasattr(self, "_initialized"): 174 | self._initialized = True 175 | 176 | def download_workflow(self, template_id, serial_number_token, user_token): 177 | if not serial_number_token: 178 | raise ValueError("需要输入序列号") 179 | cache_key = f"{template_id}_{serial_number_token}_{user_token}" 180 | if cache_key in self._cache: 181 | return self._cache[cache_key] 182 | hardware_id = get_machine_id() 183 | success, message = download_crypto_workflow( 184 | template_id, hardware_id, serial_number_token, user_token 185 | ) 186 | if success: 187 | self._cache[cache_key] = message 188 | return message 189 | elif message is not None: 190 | if message == "need login" or message == "需要登录": 191 | AuthUnit().login_dialog("运行工作流,请先完成登录") 192 | else: 193 | raise ValueError(message) 194 | else: 195 | raise ValueError("failed to download workflow") 196 | 197 | 198 | class UserWorkflowSetting: 199 | _instance = None 200 | 201 | def __new__(cls): 202 | if cls._instance is None: 203 | cls._instance = super(UserWorkflowSetting, cls).__new__(cls) 204 | return cls._instance 205 | 206 | def __init__(self): 207 | if not hasattr(self, "initialized"): 208 | local_app_path = get_local_app_setting_path() 209 | local_app_path.mkdir(parents=True, exist_ok=True) 210 | self.config_path = local_app_path / "config.ini" 211 | self.section_name = "Workflow" 212 | self.initialized = True 213 | 214 | def get_auto_overwrite(self): 215 | try: 216 | import configparser 217 | 218 | config = configparser.ConfigParser() 219 | config.read(self.config_path, encoding="utf-8") 220 | if not config.has_section(self.section_name): 221 | return False 222 | return config.getboolean( 223 | self.section_name, "auto_overwrite", fallback=False 224 | ) 225 | except Exception as e: 226 | print(f"Error reading auto_overwrite setting: {str(e)}") 227 | return False 228 | 229 | def set_auto_overwrite(self, value): 230 | try: 231 | import configparser 232 | 233 | config = configparser.ConfigParser() 234 | config.read(self.config_path, encoding="utf-8") 235 | if not config.has_section(self.section_name): 236 | config.add_section(self.section_name) 237 | config.set(self.section_name, "auto_overwrite", str(value).lower()) 238 | with open(self.config_path, "w", encoding="utf-8") as configfile: 239 | config.write(configfile) 240 | return True 241 | except Exception as e: 242 | print(f"Error saving auto_overwrite setting: {str(e)}") 243 | return False 244 | -------------------------------------------------------------------------------- /url_config.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | import json 3 | import os 4 | from PIL import Image 5 | from io import BytesIO 6 | import numpy as np 7 | import requests 8 | import pyzipper 9 | from requests.exceptions import RequestException, Timeout, HTTPError 10 | from urllib.parse import urljoin 11 | from .utils import get_local_app_setting_path 12 | 13 | DEFAULT_SUBDOMAIN = "api" if os.getenv("RICE_ROUND_DEBUG") != "true" else "test" 14 | DEFAULT_URL_PREFIX = f"https://{DEFAULT_SUBDOMAIN}.riceround.online" 15 | DEFAULT_WS_PREFIX = f"wss://{DEFAULT_SUBDOMAIN}.riceround.online" 16 | 17 | 18 | class UploadType(IntEnum): 19 | TEMPLATE_PUBLISH_IMAGE = 1 20 | USER_UPLOAD_TASK_IMAGE = 2 21 | MACHINE_TASK_RESULT = 1000 22 | 23 | 24 | class CatUrlConfig: 25 | _instance = None 26 | _initialized = False 27 | 28 | def __new__(cls, *args, **kwargs): 29 | if cls._instance is None: 30 | cls._instance = super(CatUrlConfig, cls).__new__(cls) 31 | return cls._instance 32 | 33 | def __init__(self): 34 | if not self._initialized: 35 | self._initialized = True 36 | 37 | def get_server_url(self, url_path): 38 | return urljoin(self.url_prefix, url_path) 39 | 40 | def get_ws_url(self, url_path): 41 | return urljoin(self.ws_prefix, url_path) 42 | 43 | @property 44 | def ws_prefix(self): 45 | return DEFAULT_WS_PREFIX 46 | 47 | @property 48 | def url_prefix(self): 49 | return DEFAULT_URL_PREFIX 50 | 51 | @property 52 | def login_api_url(self): 53 | return self.get_server_url("/api/cryptocat/get_info") 54 | 55 | @property 56 | def workflow_url(self): 57 | return self.get_server_url(f"/api/cryptocat/workflow") 58 | 59 | @property 60 | def serial_number_url(self): 61 | return self.get_server_url(f"/api/cryptocat/serial_number") 62 | 63 | @property 64 | def user_client_workflow(self): 65 | return self.get_server_url(f"/api/cryptocat/client_workflow") 66 | 67 | 68 | def user_upload_image(image, user_token): 69 | upload_sign_url = CatUrlConfig().user_upload_sign_url 70 | headers = {"Authorization": f"Bearer {user_token}"} 71 | params = { 72 | "upload_type": UploadType.USER_UPLOAD_TASK_IMAGE.value, 73 | "file_type": "image/png", 74 | } 75 | response = requests.get(upload_sign_url, headers=headers, params=params) 76 | upload_url = "" 77 | download_url = "" 78 | if response.status_code == 200: 79 | response_data = response.json() 80 | if response_data.get("code") == 0: 81 | upload_url = response_data.get("data", {}).get("upload_sign_url", "") 82 | download_url = response_data.get("data", {}).get("download_url", "") 83 | else: 84 | raise ValueError(f"failed to upload image. Status code: {response.status_code}") 85 | if not upload_url or not download_url: 86 | raise ValueError(f"failed to upload image. upload_sign_url is empty") 87 | i = 255.0 * image.cpu().numpy() 88 | img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) 89 | bytesIO = BytesIO() 90 | img.save(bytesIO, format="PNG", quality=95, compress_level=1) 91 | send_bytes = bytesIO.getvalue() 92 | response = requests.put( 93 | upload_sign_url, data=send_bytes, headers={"Content-Type": "image/png"} 94 | ) 95 | if response.status_code == 200: 96 | return download_url 97 | else: 98 | print(f"failed to upload image. Status code: {response.status_code}") 99 | raise ValueError(f"failed to upload image. Status code: {response.status_code}") 100 | 101 | 102 | def user_upload_workflow(template_id, workflow_path, user_token, timeout=30): 103 | "\n 用户上传工作流文件到服务器。\n\n :param workflow_id: 工作流的模板ID\n :param workflow_path: 工作流文件的本地路径\n :param user_token: 用户的认证Token\n :param timeout: 请求超时时间(秒)\n :return: (bool, str) 成功与否及相关消息\n" 104 | url = CatUrlConfig().workflow_url 105 | headers = {"Authorization": f"Bearer {user_token}"} 106 | filename = os.path.basename(workflow_path) 107 | if not os.path.isfile(workflow_path): 108 | print(f"File does not exist: {workflow_path}") 109 | return False, "File does not exist" 110 | try: 111 | with open(workflow_path, "rb") as file: 112 | file_content = file.read() 113 | files = {"file": (filename, file_content, "application/octet-stream")} 114 | data = {"template_id": template_id, "action": "upload"} 115 | print(f"Uploading file: {filename} to {url}") 116 | response = requests.put( 117 | url, headers=headers, files=files, data=data, timeout=timeout 118 | ) 119 | response.raise_for_status() 120 | try: 121 | response_data = response.json() 122 | except ValueError: 123 | print("Response content is not valid JSON") 124 | return False, "Server returned invalid response" 125 | if response_data.get("code") == 0: 126 | print("Workflow uploaded successfully") 127 | return True, "Workflow uploaded successfully" 128 | else: 129 | error_message = response_data.get("message", "Upload failed") 130 | print(f"Upload failed: {error_message}") 131 | return False, error_message 132 | except FileNotFoundError: 133 | print(f"File not found: {workflow_path}") 134 | return False, "File not found" 135 | except Timeout: 136 | print("Request timed out") 137 | return False, "Request timed out" 138 | except HTTPError as http_err: 139 | print(f"HTTP error occurred: {http_err}") 140 | return False, f"HTTP error: {http_err}" 141 | except RequestException as req_err: 142 | print(f"Request exception occurred: {req_err}") 143 | return False, f"Request exception: {req_err}" 144 | except Exception as e: 145 | print("An unknown error occurred") 146 | return False, "An unknown error occurred" 147 | 148 | 149 | def download_crypto_workflow(template_id, hardware_id, serial_number, user_token=None): 150 | "\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" 151 | if not serial_number: 152 | raise ValueError("serial_number (序列号) 参数不能为空") 153 | url_config = CatUrlConfig() 154 | url = url_config.user_client_workflow 155 | headers = {} 156 | if user_token: 157 | headers["Authorization"] = f"Bearer {user_token}" 158 | form_data = { 159 | "template_id": template_id, 160 | "hardware_id": hardware_id, 161 | "serial_number": serial_number, 162 | } 163 | try: 164 | response = requests.post(url, data=form_data, headers=headers) 165 | except requests.RequestException as e: 166 | error_msg = f"网络请求出现异常: {str(e)}" 167 | print(error_msg) 168 | return False, error_msg 169 | if response.status_code != 200: 170 | if response.status_code == 500: 171 | try: 172 | response_data = response.json() 173 | error_msg = response_data.get( 174 | "message", f"未知错误 (status_code={response.status_code})" 175 | ) 176 | except Exception: 177 | error_msg = f"服务器内部错误 (status_code={response.status_code})" 178 | else: 179 | error_msg = f"下载工作流失败: status_code={response.status_code}" 180 | print(error_msg) 181 | return False, error_msg 182 | try: 183 | response_data = response.json() 184 | except ValueError: 185 | error_msg = "无法解析返回的 JSON 数据" 186 | print(error_msg) 187 | return False, error_msg 188 | response_code = response_data.get("code", -1) 189 | if response_code != 0: 190 | error_msg = response_data.get("message", f"未知错误 code={response_code}") 191 | print(error_msg) 192 | return False, error_msg 193 | workflow_url = response_data.get("workflow_url", "") 194 | password = response_data.get("password", "") 195 | if not workflow_url or not password: 196 | error_msg = "下载工作流失败: workflow_url 或 password 缺失" 197 | print(error_msg) 198 | return False, error_msg 199 | try: 200 | workflow_response = requests.get(workflow_url) 201 | except requests.RequestException as e: 202 | error_msg = f"下载工作流内容出现异常: {str(e)}" 203 | print(error_msg) 204 | return False, error_msg 205 | if workflow_response.status_code != 200: 206 | error_msg = f"下载工作流失败: status_code={workflow_response.status_code}" 207 | print(error_msg) 208 | return False, error_msg 209 | try: 210 | zip_data = BytesIO(workflow_response.content) 211 | with pyzipper.AESZipFile(zip_data) as zip_ref: 212 | zip_ref.setpassword(password.encode("utf-8")) 213 | file_name = zip_ref.namelist()[0] 214 | workflow_content = zip_ref.read(file_name) 215 | return True, workflow_content.decode("utf-8") 216 | except Exception as e: 217 | error_msg = f"解密工作流失败: {str(e)}" 218 | print(error_msg) 219 | return False, error_msg 220 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import hashlib 3 | from io import BytesIO 4 | import os 5 | from pathlib import Path 6 | import uuid 7 | import torch 8 | import numpy as np 9 | from PIL import Image 10 | import platform 11 | import subprocess 12 | import random 13 | import string 14 | import socket 15 | 16 | 17 | def pil2tensor(images: Image.Image | list[Image.Image]) -> torch.Tensor: 18 | "Converts a PIL Image or a list of PIL Images to a tensor." 19 | 20 | def single_pil2tensor(image: Image.Image) -> torch.Tensor: 21 | np_image = np.array(image).astype(np.float32) / 255.0 22 | if np_image.ndim == 2: 23 | return torch.from_numpy(np_image).unsqueeze(0) 24 | else: 25 | return torch.from_numpy(np_image).unsqueeze(0) 26 | 27 | if isinstance(images, Image.Image): 28 | return single_pil2tensor(images) 29 | else: 30 | return torch.cat([single_pil2tensor(img) for img in images], dim=0) 31 | 32 | 33 | def _calculate_machine_id(): 34 | "\n 获取跨平台的机器唯一标识码,优化对云平台和虚拟机的支持。\n 主要通过CPU信息、MAC地址等硬件特征来区分不同机器。\n 在Linux平台上额外添加随机数以区分相同硬件的云主机。\n" 35 | import platform 36 | import socket 37 | import uuid 38 | import hashlib 39 | 40 | def get_cpu_info(): 41 | "获取CPU信息" 42 | try: 43 | if platform.system() == "Windows": 44 | return platform.processor() 45 | else: 46 | return platform.machine() 47 | except: 48 | return "" 49 | 50 | def get_mac_addresses(): 51 | "获取所有网卡的MAC地址" 52 | try: 53 | from uuid import getnode 54 | 55 | mac = getnode() 56 | return str(mac) if mac != 0 else "" 57 | except: 58 | return "" 59 | 60 | system_info = [get_cpu_info(), get_mac_addresses(), platform.machine()] 61 | if platform.system() == "Linux": 62 | system_info.append(str(uuid.uuid4())) 63 | valid_info = list(filter(None, system_info)) 64 | if not valid_info: 65 | return str(uuid.uuid4()) 66 | return "".join(valid_info) 67 | 68 | 69 | def normalize_machine_id(machine_id: str) -> str: 70 | "\n 接受一个机器标识符,并返回经过 MD5 哈希处理的规范化标识符\n" 71 | salt = "CryptoCat" 72 | trimmed_id = machine_id.strip() 73 | lowercase_id = trimmed_id.lower() 74 | salted_id = lowercase_id + salt 75 | hash_obj = hashlib.md5(salted_id.encode("utf-8")) 76 | return hash_obj.hexdigest() 77 | 78 | 79 | def get_local_app_setting_path() -> Path: 80 | home = Path.home() 81 | config_dir = home / "CryptoCat" 82 | return config_dir 83 | 84 | 85 | def get_machine_id() -> str: 86 | "\n 返回机器ID,为了兼容各个平台,各个语言,需要统一读写这个值\n" 87 | config_dir = get_local_app_setting_path() 88 | config_file = config_dir / "machine.ini" 89 | try: 90 | config_dir.mkdir(parents=True, exist_ok=True) 91 | except Exception as e: 92 | print(f"Error creating directory '{config_dir}': {e}") 93 | return "" 94 | config = configparser.ConfigParser() 95 | try: 96 | if config_file.exists(): 97 | config.read(config_file, encoding="utf-8") 98 | if "Machine" in config and "machine_id" in config["Machine"]: 99 | return config["Machine"]["machine_id"] 100 | original_host_id = _calculate_machine_id() 101 | machine_id = normalize_machine_id(original_host_id) 102 | if "Machine" not in config: 103 | config.add_section("Machine") 104 | config.set("Machine", "machine_id", machine_id) 105 | with open(config_file, "w", encoding="utf-8") as file: 106 | config.write(file) 107 | return machine_id 108 | except Exception as e: 109 | print(f"Error handling machine ID in '{config_file}': {e}") 110 | return "" 111 | 112 | 113 | def combine_files(files: list[str], password: str, zip_file_path: str) -> bool: 114 | import pyzipper 115 | 116 | for file_path in files: 117 | if not os.path.exists(file_path): 118 | raise FileNotFoundError(f"file not found: {file_path}") 119 | try: 120 | with pyzipper.AESZipFile( 121 | zip_file_path, 122 | "w", 123 | compression=pyzipper.ZIP_DEFLATED, 124 | encryption=pyzipper.WZ_AES, 125 | ) as zipf: 126 | if isinstance(password, str): 127 | password = password.encode("utf-8") 128 | else: 129 | raise ValueError("password must be a string") 130 | zipf.setpassword(password) 131 | for index, file_path in enumerate(files, start=1): 132 | arcname = f"{index}.bin" 133 | zipf.write(file_path, arcname) 134 | return True 135 | except Exception as e: 136 | print(f"Error creating zip: {str(e)}") 137 | return False 138 | 139 | 140 | def generate_random_string(length: int) -> str: 141 | "\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" 142 | letters = string.ascii_letters 143 | return "".join(random.choice(letters) for _ in range(length)) 144 | --------------------------------------------------------------------------------