【笔记】ComfyUI 源码部署版更新后一键修复:从手动补丁到自动化工作流

【笔记】ComfyUI 源码部署版更新后一键修复:从手动补丁到自动化工作流

适用环境 :Windows 10/11 + NVIDIA RTX 30/40 系列 + Python 虚拟环境

核心痛点 :每次从源码压缩包全量更新 ComfyUI 后,需要手动重装依赖、逐个运行补丁脚本、修改 main.py,过程繁琐且容易遗漏

解决方案comfyui_post_update.py 一键修复脚本,将更新后的所有修复操作自动化


ComfyUI 源码最新版------ v0.24.0 (截止 2026 年 06月 04 日)https://github.com/Comfy-Org/ComfyUI/releases/tag/v0.24.0
ComfyUI 消除 cu130 警告并强制开启 comfy-kitchen triton/ eager/ cuda全后端加速:原理与实战【含一键补丁】
ComfyUI MediaPipe 猴子补丁终极完善版:补全上下文管理与姿态检测兼容
【技术分享】ComfyUI中protobuf版本兼容性问题的优雅解决方案:猴子补丁实战

一、背景:为什么需要这个工作流

ComfyUI 的源码部署版(非便携包)更新方式通常有两种:

  1. git pull ------ 适合有 Git 环境的用户,但可能遇到合并冲突
  2. 下载最新源码压缩包,解压后全量替换 ------ 适合无 Git 环境或希望完全干净的更新

第二种方式虽然简单,但每次更新后会丢失所有手动修改:

  • 已安装的 Python 依赖需要重新 pip install
  • 手动打的源码补丁(如 comfy-kitchen 解锁、CSDN 博客补丁等)需要重新应用
  • main.py 中的自定义修改(如监听地址、导入路径、版本门槛等)需要重新修改

1.1 我的具体补丁清单

在我的环境中,每次更新后需要重新应用以下补丁:

补丁来源 作用 修改文件
CSDN 博客 161630960 自定义节点路径修复等 多个文件
CSDN 博客 161632686 模型加载优化等 多个文件
comfy_kitchen_patcher.py 消除 cu130 警告 + 解锁 CUDA/Triton 后端 quant_ops.py, registry.py, cuda/__init__.py
手动修改 添加 MediaPipe 代理兼容层导入 main.py 顶部
手动修改 降低动态 VRAM 的 PyTorch 版本门槛(2.8 → 2.7) main.py 约第 222 行

每次更新后手动重复这些操作非常耗时,且容易遗漏某一步导致启动报错。


二、核心原理:三道"安检锁"与解锁逻辑

2.1 comfy-kitchen 的三道锁

comfy-kitchen 是 Comfy-Org 官方推出的高性能算子库,负责 FP8/FP4 量化、低精度 GEMM、算子融合等优化。但源码中存在三道拦截机制,导致 RTX 30/40 系 + CUDA 12.x 环境被误判为"不兼容":

第一锁:版本锁(quant_ops.py

python 复制代码
# 约第 22 行
if cuda_version < (13,):
    warnings.warn("You need pytorch with cu130 or higher...")

PyTorch 稳定版在 Windows 上最高仅提供 CUDA 12.6/12.8,因此 cuda_version 永远小于 (13,),触发警告并跳过优化初始化。这是一个过度保守的前置检查。

第二锁:后端禁用锁(registry.py

python 复制代码
def disable(self, backend_name):
    self._disabled.add(backend_name)  # 加入黑名单

def is_available(self, backend_name):
    return backend_name in self._backends and backend_name not in self._disabled

当版本检查或算力检查失败时,会调用 registry.disable("cuda") 将后端永久加入黑名单。即使后续修复了触发条件,黑名单状态依然持续生效。

第三锁:算力门槛锁(backends/cuda/__init__.py

python 复制代码
min_compute_capability = (10, 0)  # Blackwell 架构门槛

CUDA Compute Capability 10.0 对应 RTX 50 系列(尚未发布)。当前主流的 RTX 3090(sm_86,算力 8.6)和 RTX 4090(sm_89,算力 8.9)远低于此门槛,被源码判定为"硬件不支持"。

2.2 解锁方案

通过精准修改三处源码:

  1. quant_ops.py :将 cuda_version < (13,) 替换为恒假条件,消除警告
  2. registry.py :让 disable() 空实现,is_available() 忽略 _disabled 检查
  3. cuda/__init__.py :降低 min_compute_capability(8, 6),并注入 Windows DLL 搜索路径

三、工具设计:从手动到自动

详细改动及使用原理请见:ComfyUI 消除 cu130 警告并强制开启 comfy-kitchen triton/ eager/ cuda全后端加速:原理与实战【含一键补丁】

3.1 补丁脚本:comfy_kitchen_patcher.py

该脚本封装了 comfy-kitchen 的三处源码修改,提供自动路径发现、原子级备份、试运行、一键恢复等功能。

核心特性

  • 自动从当前目录向上搜索 ComfyUI 根目录
  • 自动检测 .venvpython_embeded 虚拟环境
  • 修改前自动创建 .comfy_kitchen_backup 备份
  • 多策略匹配(正则 + 字符串),兼容不同代码版本
  • 全英文输出,避免 Windows cmd 编码问题

使用方法

bash 复制代码
# 自动查找并应用
python comfy_kitchen_patcher.py

# 指定 ComfyUI 目录
python comfy_kitchen_patcher.py --comfyui-root "H:\PythonProjects3\Win_ComfyUI"

# 试运行(只打印不修改)
python comfy_kitchen_patcher.py --dry-run

# 恢复备份
python comfy_kitchen_patcher.py --restore

脚本源码 (升级版comfy_kitchen_patcher.py):

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ComfyUI comfy-kitchen Full Backend Unlock Patch
===============================================

Features:
1. Remove "pytorch with cu130 or higher" warning
2. Force-enable comfy_kitchen CUDA / Triton backends
3. Support RTX 30/40 series (compute capability 8.6/8.9)
4. Auto backup & restore

Usage:
    python comfy_kitchen_patcher.py --comfyui-root "H:\\PythonProjects3\\Win_ComfyUI"
    python comfy_kitchen_patcher.py --restore
"""

import os
import sys
import shutil
import re
import argparse
from pathlib import Path
from typing import List, Optional

# =============================================================================
# Console Output (English only, safe for Windows cmd cp1252/gbk)
# =============================================================================
class C:
    H = '\033[95m'      # Header
    B = '\033[94m'      # Blue
    C = '\033[96m'      # Cyan
    G = '\033[92m'      # Green
    W = '\033[93m'      # Warning
    F = '\033[91m'      # Fail
    E = '\033[0m'       # End
    D = '\033[1m'       # Bold

def p(text: str, color: str = ""):
    """Print with color, flush immediately."""
    print(f"{color}{text}{C.E}", flush=True)

# =============================================================================
# Core Patcher
# =============================================================================
class Patcher:
    SUFFIX = ".comfy_kitchen_backup"

    def __init__(self, comfyui_root: Optional[str] = None,
                 python_env: Optional[str] = None,
                 dry_run: bool = False):
        self.dry_run = dry_run
        self.patched_files: List[Path] = []
        self.backup_files: List[Path] = []

        if comfyui_root:
            self.root = Path(comfyui_root).resolve()
        else:
            self.root = self._find_comfyui()

        if python_env:
            self.py_env = Path(python_env)
        else:
            self.py_env = Path(sys.executable).parent

        self.site = self._find_site_packages()

    def _find_comfyui(self) -> Path:
        current = Path.cwd()
        for _ in range(5):
            if (current / "comfy" / "quant_ops.py").exists():
                return current
            if current.parent == current:
                break
            current = current.parent

        candidates = [
            Path("H:/PythonProjects3/Win_ComfyUI"),
            Path("C:/ComfyUI"),
            Path.home() / "ComfyUI",
            Path.home() / "Documents" / "ComfyUI",
        ]
        for c in candidates:
            if (c / "comfy" / "quant_ops.py").exists():
                return c
        raise FileNotFoundError("Cannot auto-locate ComfyUI root. Use --comfyui-root.")

    def _find_site_packages(self) -> Path:
        py_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
        candidates = [
            self.py_env / "Lib" / "site-packages",
            self.py_env / "lib" / f"python{py_ver}" / "site-packages",
            self.py_env / "lib" / "site-packages",
        ]
        for sp in candidates:
            if sp.exists():
                return sp
        for p in sys.path:
            if "site-packages" in p and Path(p).exists():
                return Path(p)
        raise FileNotFoundError("Cannot locate site-packages.")

    def _find_ck(self) -> Path:
        ck = self.site / "comfy_kitchen"
        if ck.exists() and (ck / "registry.py").exists():
            return ck
        try:
            import comfy_kitchen
            return Path(comfy_kitchen.__file__).parent
        except ImportError:
            pass
        raise FileNotFoundError(f"comfy_kitchen not found in {self.site}. Run: pip install comfy-kitchen")

    def _backup(self, fp: Path) -> Path:
        bp = fp.with_suffix(fp.suffix + self.SUFFIX)
        if not bp.exists():
            if not self.dry_run:
                shutil.copy2(fp, bp)
            self.backup_files.append(bp)
            p(f"[BACKUP] {fp.name} -> {bp.name}", C.B)
        else:
            p(f"[SKIP] Backup exists: {bp.name}", C.W)
        return bp

    def _restore_file(self, fp: Path) -> bool:
        bp = fp.with_suffix(fp.suffix + self.SUFFIX)
        if not bp.exists():
            backups = list(fp.parent.glob(f"{fp.name}*{self.SUFFIX}"))
            if backups:
                bp = backups[0]
        if bp.exists():
            if not self.dry_run:
                shutil.copy2(bp, fp)
            p(f"[RESTORE] {fp.name} <- {bp.name}", C.G)
            return True
        p(f"[WARN] No backup for: {fp.name}", C.W)
        return False

    def patch_quant_ops(self) -> bool:
        target = self.root / "comfy" / "quant_ops.py"
        if not target.exists():
            p(f"[ERROR] Not found: {target}", C.F)
            return False
        self._backup(target)
        content = target.read_text(encoding='utf-8')
        orig = content
        patched = False
        patterns = [
            (r'if\s+cuda_version\s*<\s*\(\s*13\s*,\s*\)\s*:', 'if False:  # PATCHED: bypass cu130 check'),
            (r'if\s+cuda_version\s*<\s*\(13,\):', 'if False:  # PATCHED: bypass cu130 check'),
            (r'if\s+cuda_version\s*<\s*\(13,\s*0\):', 'if False:  # PATCHED: bypass cu130 check'),
        ]
        for pattern, replacement in patterns:
            if re.search(pattern, content):
                content = re.sub(pattern, replacement, content, count=1)
                patched = True
                break
        if not patched and 'cuda_version < (13' in content:
            content = content.replace('cuda_version < (13,)', 'False  # PATCHED: bypass cu130 check')
            content = content.replace('cuda_version < (13, 0)', 'False  # PATCHED: bypass cu130 check')
            patched = True
        if not patched:
            p("[INFO] quant_ops.py: version check not found (may already be patched)", C.W)
            return False
        if content != orig:
            if not self.dry_run:
                target.write_text(content, encoding='utf-8')
            p("[PATCH] quant_ops.py - cu130 check disabled", C.G)
            self.patched_files.append(target)
            return True
        return False

    def patch_registry(self) -> bool:
        try:
            ck = self._find_ck()
        except FileNotFoundError as e:
            p(f"[ERROR] {e}", C.F)
            return False
        target = ck / "registry.py"
        if not target.exists():
            p(f"[ERROR] Not found: {target}", C.F)
            return False
        self._backup(target)
        content = target.read_text(encoding='utf-8')
        orig = content

        disable_re = re.compile(
            r'(def\s+disable\s*\(\s*self\s*,\s*backend_name\s*\)\s*:[^\n]*\n)'
            r'(\s+)'
            r'(self\._disabled\.add\s*\(\s*backend_name\s*\)\s*)',
            re.MULTILINE
        )
        def repl_disable(m):
            indent = m.group(2)
            return f"{m.group(1)}{indent}pass  # PATCHED: force keep backend enabled\n{indent}# original: {m.group(3).strip()}"
        new_content, count = disable_re.subn(repl_disable, content)
        if count == 0 and 'self._disabled.add(backend_name)' in content:
            new_content = content.replace(
                'self._disabled.add(backend_name)',
                'pass  # PATCHED: force keep backend enabled\n        # original: self._disabled.add(backend_name)'
            )
            count = 1
        if count == 0:
            p("[INFO] registry.py: disable() not found (may already be patched)", C.W)
            return False

        avail_re = re.compile(
            r'return\s+backend_name\s+in\s+self\._backends\s+and\s+backend_name\s+not\s+in\s+self\._disabled'
        )
        new_content, count2 = avail_re.subn(
            'return backend_name in self._backends  # PATCHED: ignore _disabled blacklist',
            new_content
        )
        if count2 == 0 and 'backend_name not in self._disabled' in new_content:
            new_content = new_content.replace(
                'backend_name in self._backends and backend_name not in self._disabled',
                'backend_name in self._backends  # PATCHED: ignore _disabled blacklist'
            )
            count2 = 1

        list_re = re.compile(r'("disabled"\s*:\s*)(backend_name\s+in\s+self\._disabled)')
        new_content, count3 = list_re.subn(r'\1False  # PATCHED: always report enabled', new_content)

        if new_content != orig:
            if not self.dry_run:
                target.write_text(new_content, encoding='utf-8')
            p("[PATCH] registry.py - backend blacklist disabled", C.G)
            self.patched_files.append(target)
            return True
        return False

    def patch_cuda_backend(self) -> bool:
        try:
            ck = self._find_ck()
        except FileNotFoundError as e:
            p(f"[ERROR] {e}", C.F)
            return False
        target = ck / "backends" / "cuda" / "__init__.py"
        if not target.exists():
            p(f"[ERROR] Not found: {target}", C.F)
            return False
        self._backup(target)
        content = target.read_text(encoding='utf-8')
        orig = content

        dll_marker = "# === PATCHED: Add CUDA DLL paths for Windows ==="
        if dll_marker not in content:
            dll_block = (
                '# === PATCHED: Add CUDA DLL paths for Windows ===\n'
                'import os as _os\n'
                'import sys as _sys\n'
                'if _sys.platform == "win32":\n'
                '    _cuda_dll_paths = [\n'
                '        r"C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v13.1\\bin",\n'
                '        r"C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v13.0\\bin",\n'
                '        r"C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v12.8\\bin",\n'
                '    ]\n'
                '    for _p in _cuda_dll_paths:\n'
                '        if _os.path.exists(_p):\n'
                '            _os.add_dll_directory(_p)\n'
                '            break\n'
                '# === END PATCH ===\n'
                '\n'
            )
            lines = content.split('\n')
            insert_idx = 0
            for i, line in enumerate(lines):
                stripped = line.strip()
                if stripped and not stripped.startswith('#'):
                    insert_idx = i
                    break
            lines.insert(insert_idx, dll_block.rstrip())
            content = '\n'.join(lines)

        if 'min_compute_capability=(10, 0)' in content:
            content = content.replace(
                'min_compute_capability=(10, 0)',
                'min_compute_capability=(8, 6)  # PATCHED: support RTX 30/40 series (sm86/89)'
            )
        else:
            content = re.sub(
                r'min_compute_capability\s*=\s*\(\s*10\s*,\s*0\s*\)',
                'min_compute_capability=(8, 6)  # PATCHED: support RTX 30/40 series (sm86/89)',
                content
            )

        if content != orig:
            if not self.dry_run:
                target.write_text(content, encoding='utf-8')
            p("[PATCH] cuda/__init__.py - compute capability lowered + DLL paths added", C.G)
            self.patched_files.append(target)
            return True
        return False

    def run(self) -> bool:
        p("=" * 65, C.H)
        p("  ComfyUI comfy-kitchen Full Backend Unlock Patch", C.H + C.D)
        p("=" * 65, C.H)
        p(f"  ComfyUI Root : {self.root}", C.C)
        p(f"  Python Env   : {self.py_env}", C.C)
        p(f"  Site-Packages: {self.site}", C.C)
        p(f"  Dry Run      : {self.dry_run}", C.C)
        p("")

        results = []
        results.append(("quant_ops.py  (remove cu130 warning)", self.patch_quant_ops()))
        results.append(("registry.py   (unlock backend blacklist)", self.patch_registry()))
        results.append(("cuda/__init__.py (lower compute cap)", self.patch_cuda_backend()))

        p("")
        p("-" * 65, C.H)
        p("Patch Summary:", C.D)
        all_ok = all(r[1] for r in results)
        for name, ok in results:
            status = "OK" if ok else "SKIP/ALREADY_PATCHED"
            color = C.G if ok else C.W
            p(f"  [{name}] {status}", color)

        p("")
        if all_ok:
            p("[DONE] All patches applied. Restart ComfyUI to verify.", C.G)
            p("Expected log lines:", C.C)
            p("  [INFO] Found comfy_kitchen backend cuda: ... 'disabled': False ...", C.C)
            p("  [INFO] Found comfy_kitchen backend triton: ... 'disabled': False ...", C.C)
        else:
            p("[INFO] Some patches skipped (already patched or version mismatch).", C.W)
            p("       If cu130 warning still appears, check the files manually.", C.W)
        return True

    def restore(self) -> bool:
        p("=" * 65, C.H)
        p("  Restore Mode", C.H + C.D)
        p("=" * 65, C.H)

        qo = self.root / "comfy" / "quant_ops.py"
        self._restore_file(qo)

        try:
            ck = self._find_ck()
            self._restore_file(ck / "registry.py")
            self._restore_file(ck / "backends" / "cuda" / "__init__.py")
        except FileNotFoundError:
            p("[INFO] comfy_kitchen not found, skipping related restore", C.C)

        p("")
        p("[DONE] Restore complete.", C.G)
        return True


# =============================================================================
# CLI Entry
# =============================================================================
def main():
    parser = argparse.ArgumentParser(
        description="ComfyUI comfy-kitchen Full Backend Unlock Patch",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            "  python comfy_kitchen_patcher.py\n"
            "  python comfy_kitchen_patcher.py --comfyui-root \"H:\\\\PythonProjects3\\\\Win_ComfyUI\"\n"
            "  python comfy_kitchen_patcher.py --restore\n"
            "  python comfy_kitchen_patcher.py --dry-run"
        )
    )
    parser.add_argument('--comfyui-root', type=str, default=None, help='ComfyUI root directory')
    parser.add_argument('--python-env', type=str, default=None, help='Python env directory for site-packages')
    parser.add_argument('--restore', action='store_true', help='Restore from backups')
    parser.add_argument('--dry-run', action='store_true', help='Preview only, do not modify files')

    args = parser.parse_args()

    try:
        patcher = Patcher(
            comfyui_root=args.comfyui_root,
            python_env=args.python_env,
            dry_run=args.dry_run
        )
        if args.restore:
            patcher.restore()
        else:
            success = patcher.run()
            sys.exit(0 if success else 1)
    except KeyboardInterrupt:
        p("\n[!] Interrupted by user", C.W)
        sys.exit(130)
    except Exception as e:
        p(f"\n[FATAL ERROR] {e}", C.F)
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()

3.2 总控脚本:comfyui_post_update.py

该脚本是更新后的"一站式修复入口",负责按顺序执行:

  1. 依赖检查 :检测 requirements.txt 和 Manager 的依赖文件,默认提示手动安装(安全),可选 --auto-pip 自动安装
  2. 补丁队列 :按顺序运行所有登记的补丁脚本(如 comfy_kitchen_patcher.py
  3. main.py 修改:自动应用预设的查找替换 / 正则 / 插入规则

核心设计

python 复制代码
# 用户配置区 ------ 根据你的环境自定义

# 依赖文件列表
REQUIREMENTS_FILES = [
    "requirements.txt",
    "custom_nodes/ComfyUI-Manager/requirements.txt",
]

# 补丁脚本队列(按顺序执行)
PATCH_SCRIPTS = [
    "comfy_kitchen_patcher.py",
    # "patch_161630960.py",  # 你的其他补丁
    # "patch_161632686.py",  # 你的其他补丁
]

# main.py 修改规则
MAIN_PY_PATCHES = [
    {
        "name": "Add MediaPipe proxy import",
        "mode": "insert_head",
        "content": "import mediapipe_patch  # MediaPipe proxy compatibility layer\n",
    },
    {
        "name": "Lower dynamic VRAM PyTorch threshold (2.8 -> 2.7)",
        "mode": "regex",
        "pattern": r"torch_version_numeric\s*<\s*\(2,\s*[78]\)",
        "replace": "torch_version_numeric < (2, 7)",
    },
]

四种修改模式

模式 用途 示例
replace 精确字符串查找替换 修改特定行代码
regex 正则表达式替换 兼容多种原始值(如同时匹配 (2, 8)(2, 7)
insert_head 文件头部插入 添加 import 语句
insert_after 某行代码后插入 在特定函数后添加钩子

使用方法

bash 复制代码
# 默认模式:提示手动安装依赖 + 运行补丁 + 修改 main.py
python comfyui_post_update.py

# 自动安装依赖(需确认网络稳定)
python comfyui_post_update.py --auto-pip

# 跳过依赖检查(已手动装完时)
python comfyui_post_update.py --skip-pip

# 试运行(只打印不修改)
python comfyui_post_update.py --dry-run

# 恢复所有备份
python comfyui_post_update.py --restore

脚本源码comfyui_post_update.py):

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ComfyUI Post-Update Bootstrap Script
====================================

Features:
1. Detect and prompt for missing requirements (manual install by default)
2. Run patch scripts in sequence (e.g. comfy_kitchen_patcher.py)
3. Auto-patch main.py with configurable rules
4. Auto backup & one-click restore

Usage:
    # Default: check deps + run patches + patch main.py (pip is manual-only)
    python comfyui_post_update.py

    # Auto-install dependencies (use with caution)
    python comfyui_post_update.py --auto-pip

    # Skip everything except patches and main.py
    python comfyui_post_update.py --skip-pip

    # Restore all backups
    python comfyui_post_update.py --restore

    # Dry run (preview only)
    python comfyui_post_update.py --dry-run
"""

import os
import sys
import shutil
import re
import subprocess
import argparse
from pathlib import Path
from typing import List, Optional

# =============================================================================
# Console Output (English only, safe for Windows cmd cp1252/gbk)
# =============================================================================
class C:
    H = '\033[95m'
    B = '\033[94m'
    C = '\033[96m'
    G = '\033[92m'
    W = '\033[93m'
    F = '\033[91m'
    E = '\033[0m'
    D = '\033[1m'

def p(text: str, color: str = ""):
    print(f"{color}{text}{C.E}", flush=True)

# =============================================================================
# User Config Area
# =============================================================================

COMFYUI_ROOT = Path(__file__).parent.resolve()
PYTHON_EXE = None  # None = auto-detect

# Requirements files to check (relative to COMFYUI_ROOT)
REQUIREMENTS_FILES = [
    "requirements.txt",
    "custom_nodes/ComfyUI-Manager/requirements.txt",
    # "manager_requirements.txt",
]

# Patch scripts to run in order (relative to COMFYUI_ROOT)
PATCH_SCRIPTS = [
    "comfy_kitchen_patcher.py",
    # "patch_161630960.py",
    # "patch_161632686.py",
]

# main.py patch rules (applied in order)
# Modes: replace | regex | insert_head | insert_after
MAIN_PY_PATCHES = [
    {
        "name": "Add MediaPipe proxy import",
        "mode": "insert_head",
        "content": "import mediapipe_patch  # MediaPipe proxy compatibility layer\n",
    },
    {
        "name": "Lower dynamic VRAM PyTorch threshold (2.8 -> 2.7)",
        "mode": "regex",
        # Matches both (2, 8) and (2, 7), ensures final result is (2, 7)
        "pattern": r"torch_version_numeric\s*<\s*\(2,\s*[78]\)",
        "replace": "torch_version_numeric < (2, 7)",
    },
]

BACKUP_SUFFIX = ".post_update_backup"
PIP_TIMEOUT = 600

# =============================================================================
# Core Bootstrap
# =============================================================================
class Bootstrap:
    def __init__(self, comfyui_root: Optional[Path] = None,
                 python_exe: Optional[str] = None,
                 dry_run: bool = False,
                 skip_pip: bool = False,
                 auto_pip: bool = False):
        self.dry_run = dry_run
        self.skip_pip = skip_pip
        self.auto_pip = auto_pip
        self.root = Path(comfyui_root).resolve() if comfyui_root else COMFYUI_ROOT
        self.python = self._resolve_python(python_exe)
        self.patched_files: List[Path] = []
        self.backup_files: List[Path] = []

    def _resolve_python(self, explicit: Optional[str]) -> str:
        if explicit:
            return explicit
        venv = self.root / ".venv" / "Scripts" / "python.exe"
        if venv.exists():
            p(f"[ENV] Virtual env detected: {venv}", C.C)
            return str(venv)
        embed = self.root / "python_embeded" / "python.exe"
        if embed.exists():
            p(f"[ENV] Embedded Python detected: {embed}", C.C)
            return str(embed)
        p(f"[ENV] Using current Python: {sys.executable}", C.C)
        return sys.executable

    def _backup(self, fp: Path) -> Path:
        bp = fp.with_suffix(fp.suffix + BACKUP_SUFFIX)
        if not bp.exists():
            if not self.dry_run:
                shutil.copy2(fp, bp)
            self.backup_files.append(bp)
            p(f"[BACKUP] {fp.name} -> {bp.name}", C.B)
        else:
            p(f"[SKIP] Backup exists: {bp.name}", C.W)
        return bp

    # -------------------------------------------------------------------------
    # Step 1: Dependency Check / Install
    # -------------------------------------------------------------------------
    def handle_requirements(self) -> bool:
        if self.skip_pip:
            p("[SKIP] --skip-pip set, skipping dependency check", C.W)
            return True

        p("\n" + "=" * 65, C.H)
        p("  STEP 1/3: Dependency Check", C.H + C.D)
        p("=" * 65, C.H)

        missing_files = []
        for req_file in REQUIREMENTS_FILES:
            req_path = self.root / req_file
            if req_path.exists():
                missing_files.append(req_path)
            else:
                p(f"[SKIP] Not found: {req_file}", C.W)

        if not missing_files:
            p("[INFO] No requirements files to process", C.C)
            return True

        if self.auto_pip:
            p("[MODE] Auto-pip enabled. Will install dependencies automatically.", C.W)
            all_ok = True
            for req_path in missing_files:
                p(f"\n[INSTALL] {req_path.name} ...", C.C)
                cmd = [self.python, "-m", "pip", "install", "-r", str(req_path), "--upgrade"]
                p(f"  CMD: {' '.join(cmd)}", C.C)
                if self.dry_run:
                    p("  [DRY-RUN] Skipped", C.W)
                    continue
                try:
                    result = subprocess.run(
                        cmd, cwd=str(self.root), capture_output=True, text=True,
                        encoding='utf-8', errors='replace', timeout=PIP_TIMEOUT
                    )
                    if result.returncode == 0:
                        for line in result.stdout.strip().split('\n')[-5:]:
                            if line.strip():
                                p(f"    {line}", C.G)
                        p(f"  [OK] {req_path.name} installed", C.G)
                    else:
                        p(f"  [FAIL] Exit code {result.returncode}", C.F)
                        p(f"  stderr: {result.stderr[:500]}", C.F)
                        all_ok = False
                except subprocess.TimeoutExpired:
                    p(f"  [FAIL] Timeout ({PIP_TIMEOUT}s)", C.F)
                    p(f"  Tip: Run manually with mirror:", C.W)
                    p(f"    {self.python} -m pip install -r {req_path} -i https://pypi.tuna.tsinghua.edu.cn/simple", C.C)
                    all_ok = False
                except Exception as e:
                    p(f"  [FAIL] {e}", C.F)
                    all_ok = False
            return all_ok
        else:
            p("[MODE] Manual dependency install. Copy-paste the commands below:", C.W)
            p("       (To auto-install, run with --auto-pip)", C.W)
            p("")
            for req_path in missing_files:
                p(f"  {self.python} -m pip install -r \"{req_path}\" --upgrade", C.C)
            p("")
            p("[INFO] Please run the above commands manually, then re-run this script.", C.G)
            p("       Or run: python comfyui_post_update.py --auto-pip", C.C)
            return True

    # -------------------------------------------------------------------------
    # Step 2: Run Patch Scripts
    # -------------------------------------------------------------------------
    def run_patches(self) -> bool:
        p("\n" + "=" * 65, C.H)
        p("  STEP 2/3: Run Patch Scripts", C.H + C.D)
        p("=" * 65, C.H)

        if not PATCH_SCRIPTS:
            p("[INFO] PATCH_SCRIPTS empty, skipping", C.C)
            return True

        all_ok = True
        for script_name in PATCH_SCRIPTS:
            script_path = self.root / script_name
            if not script_path.exists():
                p(f"[SKIP] Not found: {script_name}", C.W)
                continue

            p(f"\n[EXEC] {script_name} ...", C.C)
            cmd = [self.python, str(script_path), "--comfyui-root", str(self.root)]
            p(f"  CMD: {' '.join(cmd)}", C.C)

            if self.dry_run:
                p("  [DRY-RUN] Skipped", C.W)
                continue

            try:
                result = subprocess.run(
                    cmd, cwd=str(self.root), capture_output=True, text=True,
                    encoding='utf-8', errors='replace', timeout=120
                )
                if result.stdout:
                    for line in result.stdout.strip().split('\n')[-30:]:
                        p(f"    {line}", C.C)
                if result.returncode == 0:
                    p(f"  [OK] {script_name} success", C.G)
                else:
                    p(f"  [WARN] Exit code {result.returncode}", C.W)
                    if result.stderr:
                        for line in result.stderr.strip().split('\n')[-10:]:
                            p(f"    {line}", C.W)
            except Exception as e:
                p(f"  [FAIL] {e}", C.F)
                all_ok = False

        return all_ok

    # -------------------------------------------------------------------------
    # Step 3: Patch main.py
    # -------------------------------------------------------------------------
    def patch_main_py(self) -> bool:
        p("\n" + "=" * 65, C.H)
        p("  STEP 3/3: Patch main.py", C.H + C.D)
        p("=" * 65, C.H)

        target = self.root / "main.py"
        if not target.exists():
            p(f"[ERROR] Not found: {target}", C.F)
            return False

        if not MAIN_PY_PATCHES:
            p("[INFO] MAIN_PY_PATCHES empty, skipping", C.C)
            return True

        self._backup(target)
        content = target.read_text(encoding='utf-8')
        orig = content
        applied = 0

        for rule in MAIN_PY_PATCHES:
            name = rule.get("name", "untitled")
            mode = rule.get("mode", "replace")
            p(f"\n[APPLY] {name} (mode={mode})", C.C)

            if mode == "replace":
                find = rule.get("find", "")
                replace = rule.get("replace", "")
                if find in content:
                    new_content = content.replace(find, replace, 1)
                    if new_content != content:
                        content = new_content
                        p("  [OK] String replaced", C.G)
                        applied += 1
                    else:
                        p("  [OK] String matched (no change needed)", C.G)
                else:
                    p("  [WARN] Target string not found (may be already patched or version changed)", C.W)

            elif mode == "regex":
                pattern = rule.get("pattern", "")
                replace = rule.get("replace", "")
                new_content, count = re.subn(pattern, replace, content, count=1)
                if count > 0:
                    if new_content != content:
                        content = new_content
                        p("  [OK] Regex replaced", C.G)
                        applied += 1
                    else:
                        p("  [OK] Regex matched (already correct)", C.G)
                else:
                    p("  [WARN] Regex did not match", C.W)

            elif mode == "insert_head":
                insert = rule.get("content", "")
                if insert not in content:
                    content = insert + "\n" + content
                    p("  [OK] Inserted at head", C.G)
                    applied += 1
                else:
                    p("  [SKIP] Content already exists", C.W)

            elif mode == "insert_after":
                anchor = rule.get("anchor", "")
                insert = rule.get("content", "")
                if anchor in content:
                    new_content = content.replace(anchor, anchor + insert, 1)
                    if new_content != content:
                        content = new_content
                        p("  [OK] Inserted after anchor", C.G)
                        applied += 1
                    else:
                        p("  [OK] Anchor matched (no change needed)", C.G)
                else:
                    p("  [WARN] Anchor not found", C.W)

            else:
                p(f"  [ERROR] Unknown mode: {mode}", C.F)

        if content != orig:
            if not self.dry_run:
                target.write_text(content, encoding='utf-8')
            p(f"\n[OK] main.py patched, {applied} rule(s) changed content", C.G)
            self.patched_files.append(target)
            return True
        else:
            p(f"\n[INFO] main.py unchanged (all rules already applied or no match)", C.C)
            return True

    # -------------------------------------------------------------------------
    # Restore Mode
    # -------------------------------------------------------------------------
    def restore(self) -> bool:
        p("=" * 65, C.H)
        p("  Restore Mode", C.H + C.D)
        p("=" * 65, C.H)

        restored = 0
        main_py = self.root / "main.py"
        if self._restore_single(main_py):
            restored += 1

        ck = self.root / ".venv" / "Lib" / "site-packages" / "comfy_kitchen"
        if not ck.exists():
            ck = self.root / "python_embeded" / "Lib" / "site-packages" / "comfy_kitchen"
        if ck.exists():
            if self._restore_single(ck / "registry.py"):
                restored += 1
            if self._restore_single(ck / "backends" / "cuda" / "__init__.py"):
                restored += 1

        qo = self.root / "comfy" / "quant_ops.py"
        if self._restore_single(qo):
            restored += 1

        p(f"\n[DONE] Restored {restored} files", C.G)
        return True

    def _restore_single(self, fp: Path) -> bool:
        bp = fp.with_suffix(fp.suffix + BACKUP_SUFFIX)
        if not bp.exists():
            backups = list(fp.parent.glob(f"{fp.name}*{BACKUP_SUFFIX}"))
            if backups:
                bp = backups[0]
        if bp.exists():
            if not self.dry_run:
                shutil.copy2(bp, fp)
            p(f"[RESTORE] {fp.name} <- {bp.name}", C.G)
            return True
        p(f"[SKIP] No backup: {fp.name}", C.W)
        return False

    # -------------------------------------------------------------------------
    # Main Flow
    # -------------------------------------------------------------------------
    def run(self) -> bool:
        p("=" * 65, C.H)
        p("  ComfyUI Post-Update Bootstrap", C.H + C.D)
        p("=" * 65, C.H)
        p(f"  ComfyUI Root : {self.root}", C.C)
        p(f"  Python       : {self.python}", C.C)
        p(f"  Dry Run      : {self.dry_run}", C.C)
        p(f"  Skip Pip     : {self.skip_pip}", C.C)
        p(f"  Auto Pip     : {self.auto_pip}", C.C)
        p("")

        ok1 = self.handle_requirements()
        ok2 = self.run_patches()
        ok3 = self.patch_main_py()

        p("\n" + "=" * 65, C.H)
        p("  Summary", C.D)
        p("=" * 65, C.H)
        p(f"  [Dependencies] {'OK' if ok1 else 'FAIL'}", C.G if ok1 else C.F)
        p(f"  [Patches]      {'OK' if ok2 else 'FAIL'}", C.G if ok2 else C.F)
        p(f"  [main.py]      {'OK' if ok3 else 'FAIL'}", C.G if ok3 else C.F)

        if ok1 and ok2 and ok3:
            p("\n[DONE] All steps completed. Start ComfyUI to verify.", C.G)
            return True
        else:
            p("\n[!] Some steps failed. Check logs above.", C.W)
            p("  Use --restore to revert all changes.", C.W)
            return False


# =============================================================================
# CLI Entry
# =============================================================================
def main():
    parser = argparse.ArgumentParser(
        description="ComfyUI Post-Update Bootstrap Script",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            "  # Default: check deps (manual) + run patches + patch main.py\n"
            "  python comfyui_post_update.py\n\n"
            "  # Auto-install dependencies (use with caution)\n"
            "  python comfyui_post_update.py --auto-pip\n\n"
            "  # Skip dependency check entirely\n"
            "  python comfyui_post_update.py --skip-pip\n\n"
            "  # Dry run (preview only)\n"
            "  python comfyui_post_update.py --dry-run\n\n"
            "  # Restore all backups\n"
            "  python comfyui_post_update.py --restore\n"
        )
    )
    parser.add_argument('--comfyui-root', type=str, default=None, help='ComfyUI root directory')
    parser.add_argument('--python-exe', type=str, default=None, help='Force specific Python executable')
    parser.add_argument('--skip-pip', action='store_true', help='Skip dependency check entirely')
    parser.add_argument('--auto-pip', action='store_true', help='Auto-install dependencies (default is manual prompt)')
    parser.add_argument('--restore', action='store_true', help='Restore all backups')
    parser.add_argument('--dry-run', action='store_true', help='Preview only, do not modify files')

    args = parser.parse_args()

    try:
        bs = Bootstrap(
            comfyui_root=args.comfyui_root,
            python_exe=args.python_exe,
            dry_run=args.dry_run,
            skip_pip=args.skip_pip,
            auto_pip=args.auto_pip
        )
        if args.restore:
            bs.restore()
        else:
            success = bs.run()
            sys.exit(0 if success else 1)
    except KeyboardInterrupt:
        p("\n[!] Interrupted", C.W)
        sys.exit(130)
    except Exception as e:
        p(f"\n[FATAL ERROR] {e}", C.F)
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()

四、实战:完整更新流程演示

4.1 更新前准备

将两个脚本放在 ComfyUI 根目录:

复制代码
H:\PythonProjects3\Win_ComfyUI\
├── main.py
├── requirements.txt
├── manager_requirements.txt
├── comfyui_post_update.py      <-- 总控脚本
├── comfy_kitchen_patcher.py    <-- 补丁脚本
├── mediapipe_patch.py            <-- mediapipe 补丁脚本
├── folder_paths.py                   <-- 模型 补丁脚本
├── protobuf_patch.py               <-- protobuf 补丁脚本
└── ...

ComfyUI 消除 cu130 警告并强制开启 comfy-kitchen triton/ eager/ cuda全后端加速:原理与实战【含一键补丁】
ComfyUI MediaPipe 猴子补丁终极完善版:补全上下文管理与姿态检测兼容
ComfyUI MediaPipe 终极填坑:解决 incompatible function arguments 报错,基于代理模式的猴子补丁升级版
ComfyUI MediaPipe 猴子补丁实战记录(解决solutions缺失及相关报错)
【技术分享】ComfyUI中protobuf版本兼容性问题的优雅解决方案:猴子补丁实战

4.2 执行更新

Step 1:下载并替换源码

从 GitHub 下载最新 ComfyUI 源码压缩包,解压后全量替换到 H:\PythonProjects3\Win_ComfyUI。遇到文件冲突时选择"全部替换"。

Step 2:运行一键修复脚本

bash 复制代码
cd H:\PythonProjects3\Win_ComfyUI
python comfyui_post_update.py

输出示例:

text 复制代码
[ENV] Virtual env detected: H:\PythonProjects3\Win_ComfyUI\.venv\Scripts\python.exe
=================================================================
  ComfyUI Post-Update Bootstrap
=================================================================
  ComfyUI Root : H:\PythonProjects3\Win_ComfyUI
  Python       : H:\PythonProjects3\Win_ComfyUI\.venv\Scripts\python.exe
  Dry Run      : False
  Skip Pip     : False
  Auto Pip     : False


=================================================================
  STEP 1/3: Dependency Check
=================================================================
[MODE] Manual dependency install. Copy-paste the commands below:
       (To auto-install, run with --auto-pip)

  H:\PythonProjects3\Win_ComfyUI\.venv\Scripts\python.exe -m pip install -r "H:\PythonProjects3\Win_ComfyUI\requirements.txt" --upgrade
  H:\PythonProjects3\Win_ComfyUI\.venv\Scripts\python.exe -m pip install -r "H:\PythonProjects3\Win_ComfyUI\custom_nodes\ComfyUI-Manager\requirements.txt" --upgrade

[INFO] Please run the above commands manually, then re-run this script.
       Or run: python comfyui_post_update.py --auto-pip

Step 3:手动安装依赖(或自动)

复制粘贴脚本打印的命令执行。如果网络较慢,可使用国内镜像:

bash 复制代码
H:\PythonProjects3\Win_ComfyUI\.venv\Scripts\python.exe -m pip install -r "H:\PythonProjects3\Win_ComfyUI\requirements.txt" --upgrade -i https://pypi.tuna.tsinghua.edu.cn/simple

Step 4:再次运行脚本完成补丁

bash 复制代码
python comfyui_post_update.py --skip-pip

输出示例:

text 复制代码
=================================================================
  STEP 2/3: Run Patch Scripts
=================================================================

[EXEC] comfy_kitchen_patcher.py ...
  CMD: H:\PythonProjects3\Win_ComfyUI\.venv\Scripts\python.exe H:\PythonProjects3\Win_ComfyUI\comfy_kitchen_patcher.py --comfyui-root H:\PythonProjects3\Win_ComfyUI
    ...
    [PATCH] quant_ops.py - cu130 check disabled
    [PATCH] registry.py - backend blacklist disabled
    [PATCH] cuda/__init__.py - compute capability lowered + DLL paths added
    ...
  [OK] comfy_kitchen_patcher.py success

=================================================================
  STEP 3/3: Patch main.py
=================================================================
[BACKUP] main.py -> main.py.post_update_backup

[APPLY] Add MediaPipe proxy import (mode=insert_head)
  [OK] Inserted at head

[APPLY] Lower dynamic VRAM PyTorch threshold (2.8 -> 2.7) (mode=regex)
  [OK] Regex replaced

[OK] main.py patched, 2 rule(s) changed content

=================================================================
  Summary
=================================================================
  [Dependencies] OK
  [Patches]      OK
  [main.py]      OK

[DONE] All steps completed. Start ComfyUI to verify.

Step 5:启动验证

ComfyUI 消除 cu130 警告并强制开启 comfy-kitchen triton/ eager/ cuda全后端加速:原理与实战【含一键补丁】

bash 复制代码
python main.py

检查启动日志,确认 comfy-kitchen 后端状态:

text 复制代码
[INFO] Found comfy_kitchen backend eager:  {'available': True, 'disabled': False, ...}
[INFO] Found comfy_kitchen backend triton: {'available': True, 'disabled': False, ...}
[INFO] Found comfy_kitchen backend cuda:   {'available': True, 'disabled': False, ...}

三个后端 disabled 均为 False,且无 WARNING: cu130 警告,说明补丁生效。


五、高级配置:扩展你的补丁队列

5.1 添加更多补丁脚本

如果你有来自其他博客或自己编写的补丁脚本,只需将其放入 ComfyUI 根目录,并在 PATCH_SCRIPTS 中登记:

python 复制代码
PATCH_SCRIPTS = [
    "comfy_kitchen_patcher.py",
    "patch_custom_nodes_path.py",      # 来自 CSDN 博客 161630960
    "patch_model_loading.py",          # 来自 CSDN 博客 161632686
    "my_custom_fix.py",                # 你自己的补丁
]

5.2 添加更多 main.py 修改规则

示例 1:修改监听地址为 0.0.0.0

python 复制代码
{
    "name": "Change listen address to 0.0.0.0",
    "mode": "replace",
    "find": 'server_address = ("127.0.0.1", port)',
    "replace": 'server_address = ("0.0.0.0", port)',
}

示例 2:正则替换启动参数

python 复制代码
{
    "name": "Add extra model paths argument",
    "mode": "regex",
    "pattern": r'loop\.run_until_complete\(run\(([^)]+)\)\)',
    "replace": r'loop.run_until_complete(run(\1, extra_model_paths=r"H:\\models.yaml"))',
}

示例 3:在某行后插入自定义钩子

python 复制代码
{
    "name": "Import custom hook after comfy.utils",
    "mode": "insert_after",
    "anchor": "import comfy.utils",
    "content": "\nimport my_custom_hook  # PATCHED: load custom hook",
}

六、注意事项与排错

6.1 硬件真实限制

RTX 3090/4090 的 Tensor Core 不支持 FP4(NVFP4) 硬件原生加速。补丁解锁后,涉及 FP4 的算子会回退到软件模拟。建议使用 FP8BF16 精度。

6.2 更新覆盖问题

全量替换更新会覆盖所有源码修改。脚本通过备份机制确保可恢复,但建议:

  • comfyui_post_update.py 和补丁脚本放在 ComfyUI 根目录
  • 更新后重新运行一次脚本
  • 或编写批处理脚本 update.bat
batch 复制代码
@echo off
cd /d "%~dp0"
python comfyui_post_update.py --skip-pip
pause

6.3 常见报错处理

现象 原因 解决
Cannot auto-locate ComfyUI root 脚本未在 ComfyUI 目录内执行 显式使用 --comfyui-root
comfy_kitchen not found 未安装 comfy-kitchen pip install comfy-kitchen
Target string not found 源码版本变化,原始字符串已不存在 更新 find / pattern 匹配新源码
Regex did not match 正则表达式不匹配当前代码 调整 pattern 或使用 replace 模式
pip 安装超时 网络较慢或依赖较多 使用 --skip-pip 后手动安装,或换国内镜像

6.4 与官方更新的兼容性

本补丁属于非官方支持的修改。如果未来 ComfyUI 官方放宽了版本/算力检查:

  1. 先执行 --restore 还原备份
  2. 更新 ComfyUI 和 comfy-kitchen
  3. 检查是否已原生支持,若仍被锁再重新打补丁

七、总结

通过 comfyui_post_update.py 总控脚本 + comfy_kitchen_patcher.py 补丁脚本的组合,可以将 ComfyUI 源码部署版更新后的所有修复操作自动化:

  • 依赖重装:从手动复制粘贴命令到一键提示
  • 补丁应用:从逐个文件手动修改到脚本自动匹配替换
  • main.py 修改:从每次重新编辑到规则化自动应用
  • 备份恢复:从担心改坏到随时一键还原

这个工作流不仅适用于 comfy-kitchen 解锁,也适用于任何需要频繁更新源码并重新应用自定义修改的场景。将补丁规则化、脚本化,是维护大型开源项目本地部署的最佳实践。

相关推荐
qq_452396231 小时前
第十七篇:《Docker 日志管理:驱动配置与集中收集》
运维·docker·容器
石榴树下的七彩鱼1 小时前
发票OCR识别API接入教程:从图像到结构化数据的完整实战(附Python/Java/PHP/JS代码)
java·python·ocr·api接口·财务自动化·石榴智能·发票ocr
hj2862511 小时前
Linux + 计算机网络全套精炼整理笔记
linux·运维
IT空门:门主1 小时前
MySQL MCP Server 从零安装到使用实战,AI 直接查询数据库
数据库·人工智能·mysql
Evand J1 小时前
【自适应滤波】基于新息协方差匹配的自适应CKF目标跟踪 MATLAB 实战——在目标跟踪、雷达定位、组合导航和传感器融合等问题
人工智能·matlab·目标跟踪
Aipollo1 小时前
多Agent架构设计模式、通讯间沟通对比分析
人工智能·ai
InternLM1 小时前
从「模型类型不支持」到成功推理:Intern-S2-Preview oMLX 4bit 量化实录 | 与书生共创
人工智能·大模型·多模态模型
kcuwu.1 小时前
模型压缩技术深度解析博客
人工智能
剑神一笑1 小时前
Linux chmod 命令深度解析:从权限位到符号模式的完整指南
linux·运维·chrome