import os import sys import json import subprocess import requests import shutil import tempfile from datetime import datetime from urllib.parse import urlparse import threading from concurrent.futures import ThreadPoolExecutor __version__ = "2.0" class GitHubRepoUpdater: """GitHub仓库更新器,用于检查和更新指定的GitHub仓库""" def __init__(self): self.comfyui_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) self.custom_nodes_path = os.path.join(self.comfyui_path, "custom_nodes") self.updater_config_path = os.path.join(os.path.dirname(__file__), "updater_config.json") self.config = self.load_config() self.repo_cache = {} self.updatable_repos = [] self.updated_count = 0 def load_config(self): """加载更新器配置文件""" default_config = { "repos": [], # 存储要监控的仓库信息 "auto_update_on_start": True, "last_check_time": "", "check_interval_days": 1, "max_workers": 5 } try: if os.path.exists(self.updater_config_path): with open(self.updater_config_path, 'r', encoding='utf-8') as f: return json.load(f) else: with open(self.updater_config_path, 'w', encoding='utf-8') as f: json.dump(default_config, f, indent=4, ensure_ascii=False) return default_config except Exception as e: print(f"[CCXManager Updater] 加载配置失败: {str(e)}") return default_config def save_config(self): """保存更新器配置文件""" try: with open(self.updater_config_path, 'w', encoding='utf-8') as f: json.dump(self.config, f, indent=4, ensure_ascii=False) except Exception as e: print(f"[CCXManager Updater] 保存配置失败: {str(e)}") def is_git_installed(self): """检查Git是否安装""" try: subprocess.run(["git", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) return True except (subprocess.SubprocessError, FileNotFoundError): return False def add_repo(self, repo_url, node_name=None, branch="main", auto_update=True): """添加要监控的仓库""" if not repo_url: return False, "仓库URL不能为空" # 如果没有提供节点名称,从URL中提取 if not node_name: parsed_url = urlparse(repo_url) path_parts = parsed_url.path.strip('/').split('/') if len(path_parts) >= 2: node_name = path_parts[1].replace('.git', '') else: node_name = "unknown_repo" # 获取当前SHA current_sha = "" try: current_sha = self.get_remote_sha(repo_url, branch) except Exception as e: print(f"[CCXManager Updater] 获取SHA失败: {str(e)}") # 清空现有仓库列表,只保留当前设置的仓库 self.config["repos"] = [{ "repo_url": repo_url, "node_name": node_name, "branch": branch, "auto_update": auto_update, "last_commit_sha": current_sha, "last_update_time": "" }] self.save_config() return True, f"成功添加仓库监控: {node_name},已自动清除旧仓库配置" def get_remote_sha(self, repo_url, branch="main"): """获取远程仓库的最新SHA""" # 检查缓存 cache_key = f"{repo_url}:{branch}" if cache_key in self.repo_cache: cached_sha, cached_time = self.repo_cache[cache_key] # 缓存5分钟 if (datetime.now() - cached_time).total_seconds() < 300: return cached_sha # 尝试使用git命令直接获取(参考Comfy-NodeUpdater的方式) if self.is_git_installed(): try: result = subprocess.run( ["git", "ls-remote", repo_url, branch], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if result.returncode == 0 and result.stdout: sha = result.stdout.split()[0] self.repo_cache[cache_key] = (sha, datetime.now()) return sha except Exception as e: # 不打印详细错误信息 pass # 尝试使用GitHub API(备选方案) try: parsed_url = urlparse(repo_url) if parsed_url.netloc == "github.com": path_parts = parsed_url.path.strip('/').split('/') if len(path_parts) >= 2: owner, repo = path_parts[:2] # 移除仓库名称中的.git后缀 if repo.endswith('.git'): repo = repo[:-4] api_url = f"https://api.github.com/repos/{owner}/{repo}/commits/{branch}" # 添加错误处理和超时设置 try: response = requests.get(api_url, timeout=10) if response.status_code == 200: sha = response.json().get("sha", "") if sha: self.repo_cache[cache_key] = (sha, datetime.now()) return sha except requests.RequestException: # 捕获网络相关异常,不打印详细信息 pass except Exception as e: # 不打印详细错误信息 pass # 所有方法都失败,抛出异常 raise Exception("无法获取远程仓库的SHA") def check_repo_for_update(self, repo_info): """检查单个仓库是否有更新""" node_name = repo_info["node_name"] if not repo_info["auto_update"]: return node_name, False, "自动更新已禁用" # 检查本地仓库是否存在 node_path = os.path.join(self.custom_nodes_path, node_name) git_path = os.path.join(node_path, ".git") if not os.path.exists(git_path): # 本地没有仓库,视为需要更新 return node_name, True, "本地仓库不存在" try: # 尝试使用本地git命令检查更新 try: # 执行git fetch,移除重复的打印信息 fetch_result = subprocess.run( ["git", "-C", node_path, "fetch"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if fetch_result.returncode == 0: # 检查本地与远程的差异 status_result = subprocess.run( ["git", "-C", node_path, "status", "-uno"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) has_updates = "Your branch is behind" in status_result.stdout return node_name, has_updates, "本地git检查成功" else: # git fetch失败,记录错误但不打印到控制台 error_msg = f"git fetch失败" return node_name, False, error_msg # 立即返回错误,不继续执行 except Exception as e: # 本地git操作异常,不打印详细错误到控制台 error_msg = f"本地git操作异常" return node_name, False, error_msg # 立即返回错误,不继续执行 # 注意:以下代码在git操作失败时不会执行,因为上面已经返回了 current_sha = self.get_remote_sha(repo_info["repo_url"], repo_info["branch"]) has_update = current_sha != repo_info["last_commit_sha"] or repo_info["last_commit_sha"] == "" return node_name, has_update, current_sha except Exception as e: # 检查更新异常,不打印详细错误到控制台 return node_name, False, "检查更新异常" def update_repo(self, repo_info, force_override=False): """更新单个仓库(参考Comfy-NodeUpdater的实现)""" node_name = repo_info["node_name"] node_path = os.path.join(self.custom_nodes_path, node_name) repo_url = repo_info["repo_url"] branch = repo_info["branch"] try: # 检查目录是否存在 if not os.path.exists(node_path): # 目录不存在,执行克隆 print(f"[CCXManager Updater] 开始克隆仓库: {node_name}") result = subprocess.run( ["git", "clone", "-b", branch, repo_url, node_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if result.returncode != 0: return False, f"克隆失败: {result.stderr}" else: # 目录存在,执行pull(参考Comfy-NodeUpdater的错误处理) print(f"[CCXManager Updater] 开始更新仓库: {node_name}") # 先尝试直接pull result = subprocess.run( ["git", "-C", node_path, "pull"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if result.returncode != 0: # 检查是否是因为本地更改冲突导致的失败 if "Your local changes to the following files would be overwritten by merge" in result.stderr or force_override: print(f"[CCXManager Updater] 检测到{node_name}有未提交的更改,正在自动放弃本地更改...") # 尝试常规的checkout checkout_result = subprocess.run( ["git", "-C", node_path, "checkout", "--", "."], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if checkout_result.returncode != 0 or force_override: # 如果常规checkout失败或用户选择强制覆盖,使用更强制的方法 print(f"[CCXManager Updater] 尝试强制清理{node_name}的未跟踪文件...") # 清理未跟踪的文件和目录 clean_result = subprocess.run( ["git", "-C", node_path, "clean", "-fd"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if clean_result.returncode != 0: return False, f"强制清理失败: {clean_result.stderr}" # 再次尝试checkout checkout_result = subprocess.run( ["git", "-C", node_path, "checkout", "--", "."], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if checkout_result.returncode == 0: print(f"[CCXManager Updater] {node_name}本地更改已放弃,正在重新尝试更新...") # 再次尝试git pull retry_result = subprocess.run( ["git", "-C", node_path, "pull"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if retry_result.returncode == 0: # 更新成功 pass else: return False, f"放弃更改后更新仍失败: {retry_result.stderr}" else: return False, f"放弃本地更改失败: {checkout_result.stderr}" else: return False, f"更新失败: {result.stderr}" # 更新SHA和时间 current_sha = self.get_remote_sha(repo_url, branch) for repo in self.config["repos"]: if repo["node_name"] == node_name: repo["last_commit_sha"] = current_sha repo["last_update_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") break self.save_config() return True, "更新成功" except Exception as e: return False, f"更新异常: {str(e)}" def check_all_repos(self): """检查所有监控的仓库是否有更新(使用线程池并发处理)""" if not self.is_git_installed(): print("[CCXManager Updater] 未安装Git,请先安装Git") return [], "未安装Git" if not self.config["repos"]: return [], "没有监控的仓库" self.updatable_repos = [] status_message = "检查仓库更新结果:\n" has_any_update = False has_any_disabled = False # 使用线程池并发检查 with ThreadPoolExecutor(max_workers=self.config.get("max_workers", 5)) as executor: results = list(executor.map(self.check_repo_for_update, self.config["repos"])) for node_name, has_update, result in results: if has_update: # 查找对应的repo_info repo_info = next((r for r in self.config["repos"] if r["node_name"] == node_name), None) if repo_info: self.updatable_repos.append(repo_info) status_message += f"✅ {node_name}: 发现更新\n" has_any_update = True else: if isinstance(result, str): if result != "自动更新已禁用" and "成功" not in result: # 不显示详细的错误信息 status_message += f"❌ {node_name}: {result}\n" elif result != "自动更新已禁用": status_message += f"✅ {node_name}: 已是最新版本\n" else: status_message += f"🔄 {node_name}: 自动更新已禁用\n" has_any_disabled = True else: status_message += f"✅ {node_name}: 已是最新版本\n" # 只输出一次状态信息,避免重复 if has_any_update: print("[CCXManager Updater] 发现有更新,正在更新中") elif has_any_disabled: print("[CCXManager Updater] 已经忽略更新") else: print("[CCXManager Updater] 没发现更新") self.config["last_check_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.save_config() return self.updatable_repos, status_message def update_all_repos(self, force_override=False): """更新所有有更新的仓库(使用线程池并发处理)""" if not self.updatable_repos: # 如果没有预先检查,先检查 self.check_all_repos() if not self.updatable_repos: print("[CCXManager Updater] 没有需要更新的仓库") return 0, "没有需要更新的仓库" self.updated_count = 0 status_message = "更新结果:\n" update_results = [] # 使用线程池并发更新,并传递force_override参数(参考Comfy-NodeUpdater) with ThreadPoolExecutor(max_workers=self.config.get("max_workers", 5)) as executor: results = list(executor.map(lambda repo: (repo, self.update_repo(repo, force_override)), self.updatable_repos)) for repo_info, (success, message) in results: node_name = repo_info["node_name"] if success: self.updated_count += 1 result_msg = f"✅ {node_name}: {message}\n" print("[CCXManager Updater] SD-PPP节点更新成功") else: result_msg = f"❌ {node_name}: {message}\n" print(f"[CCXManager Updater] {result_msg.strip()}") # 打印每个节点的更新结果到控制台 status_message += result_msg update_results.append(result_msg.strip()) return self.updated_count, status_message def run_auto_update(self, force_override=False, force_update=False): """执行自动更新""" if not self.config.get("auto_update_on_start", True): print("[CCXManager Updater] Comfyui侧自动更新SD-PPP[更新禁用]") return 0, "自动更新已禁用" # 添加时间间隔检查,但仅在非强制更新模式下 if not force_update and self.config.get("last_check_time") and self.config.get("check_interval_days", 1) > 0: try: last_check = datetime.strptime(self.config["last_check_time"], "%Y-%m-%d %H:%M:%S") current_time = datetime.now() days_since_last_check = (current_time - last_check).days # 检查是否是未来时间 if last_check > current_time: print(f"[CCXManager Updater] 检测到未来的检查时间,重置并执行检查") # 重置为当前时间 self.config["last_check_time"] = current_time.strftime("%Y-%m-%d %H:%M:%S") self.save_config() elif days_since_last_check < self.config["check_interval_days"]: print(f"[CCXManager Updater] 距离上次检查不足{self.config['check_interval_days']}天,跳过自动更新") return 0, f"距离上次检查不足{self.config['check_interval_days']}天,跳过自动更新" except Exception as e: print(f"[CCXManager Updater] 时间检查出错: {str(e)}") # 如果时间解析失败,继续执行更新检查 print("[CCXManager Updater] 正在SD-PPP节点检查更新...") # 确保只保留当前设置的仓库(如果有的话) if len(self.config["repos"]) > 1: print("[CCXManager Updater] 检测到多个仓库配置,仅保留当前设置的仓库") # 保留最后一个仓库配置 self.config["repos"] = [self.config["repos"][-1]] self.save_config() self.check_all_repos() if self.updatable_repos: count, message = self.update_all_repos(force_override) return count, message else: return 0, "没有需要更新的仓库" class CCXRepoUpdaterNode: """CCX仓库更新器节点,简化版本""" @classmethod def INPUT_TYPES(cls): return { "required": { "repo_url": ("STRING", {"default": "", "multiline": False, "placeholder": "GitHub仓库URL,如:https://github.com/user/repo.git"}), "auto_update": ("BOOLEAN", {"default": True}), "force_override": ("BOOLEAN", {"default": False}) }, } RETURN_TYPES = ("STRING",) RETURN_NAMES = ("Status",) FUNCTION = "process" CATEGORY = "Update of SD-PPP Plugin" def process(self, repo_url, auto_update, force_override): updater = GitHubRepoUpdater() status_message = "" if not repo_url: status_message = "错误:仓库URL不能为空" return (status_message,) # 从URL提取节点名称 parsed_url = urlparse(repo_url) path_parts = parsed_url.path.strip('/').split('/') if len(path_parts) >= 2: node_name = path_parts[1].replace('.git', '') else: node_name = "unknown_repo" # 无论仓库是否存在,都调用add_repo方法以确保旧仓库被删除 success, message = updater.add_repo(repo_url, node_name, "main", auto_update) if not success: status_message = message return (status_message,) # 先检测更新状态 print(f"[CCXManager Updater] Comfyui侧自动检测{node_name} 仓库的更新状态") repo_info = next((repo for repo in updater.config["repos"] if repo["node_name"] == node_name), None) if repo_info: node_name_result, has_update, result = updater.check_repo_for_update(repo_info) if has_update and auto_update: # 有更新且开启自动更新,执行更新 print(f"[CCXManager Updater] 发现{node_name} 的更新,开始更新...") success, update_message = updater.update_repo(repo_info, force_override) if success: status_message = f"✅ {node_name}: 更新成功 [自动更新: 开启]" print(f"[CCXManager Updater] {status_message}") else: status_message = f"❌ {node_name}: 更新失败 - {update_message} [自动更新: 开启]" print(f"[CCXManager Updater] {status_message}") elif has_update: # 有更新但未开启自动更新 status_message = f"ℹ️ {node_name}: 发现更新,但自动更新已禁用" print(f"[CCXManager Updater] {status_message}") elif isinstance(result, str) and result != "自动更新已禁用": # 检查失败 - 直接使用返回的具体错误信息并添加自动更新状态 update_status = "[自动更新: 开启]" if auto_update else "[自动更新: 禁用]" if result.startswith("本地git检查成功"): status_message = f"{node_name} 检查成功,{update_status}" else: status_message = f"❌ {node_name}: {result} {update_status}" print(f"[CCXManager Updater] {status_message}") else: # 已是最新版本并添加自动更新状态 update_status = "[自动更新: 开启]" if auto_update else "[自动更新: 禁用]" status_message = f"✅ {node_name}: 已是最新版本 {update_status}" print(f"[CCXManager Updater] {status_message}") else: status_message = "错误:无法找到仓库信息" return (status_message,) # 全局自动更新函数 def auto_check_for_repo_updates(): """ComfyUI启动时自动检查仓库更新""" try: updater = GitHubRepoUpdater() # 获取当前自动更新设置状态 auto_update_status = updater.config.get("auto_update_on_start", True) # 在控制台显示自动更新状态 print(f"[CCXManager Updater] Comfyui侧自动更新SD-PPP[{'启动成功' if auto_update_status else '更新禁用'}]") if auto_update_status: # 自动更新时不使用强制覆盖,但使用强制更新模式(忽略时间间隔) updater.run_auto_update(False, True) else: print("[CCXManager Updater] 由于自动更新功能已禁用,跳过更新检查") except Exception as e: print(f"[CCXManager Updater] 自动更新失败: {str(e)}") # 节点注册映射 NODE_CLASS_MAPPINGS = { "CCXRepoUpdaterNode": CCXRepoUpdaterNode } NODE_DISPLAY_NAME_MAPPINGS = { "CCXRepoUpdaterNode": "Comfyui side automatic update SD-PPP" }