选错 AI 真的会被坑到破防。

最近在学习安装 ComfyUI,虽然官方提供了一键安装包,但在执行的过程中,确实是会因为不同电脑配置和安装路径问题,遇到超多麻烦。

全球最火的开源节点式 AI 生成工作流引擎

以前,我可能需要去求助敲代码的同学,但现在有了 AI 就不一样了,我能通过 AI 去学习如何安装、如何使用、如何提效。

但在开始之前,我选错了我的 AI 小能手。

我们都知道现在有很多知名大模型。

比如谷歌耳熟能详的香蕉模型,生成出来的图片效果在目前所有模型里的排名数一数二;

比如火山的 Seedance2.0,一经出现,人人都能成为大导演;

还有豆包,相信没有哪个打工人不会在自己的电脑里装一个。

而今天我要强烈推荐的是,由 Anthropic 最新推出的 Claude Sonnet 4.6,它是目前最适合开发者日常使用的 AI 助手,没有之一。

它解决了绝大多数 AI “只会说不会做、只会讲理论不会改代码” 的通病,尤其擅长排查复杂的工程问题,是你写代码、调插件、搭环境的最佳搭档。

我为什么会感受这么深呢?

我在学习 ComfyUI 的过程中,发现有一个 ComfyUI-Photoshop 的插件,它能让你实现在 PS 里,一边生成图片,一边修改图片,解决了 AI 抽卡问题,提高效率。

可直接在PS上修改AI的出图 优化细节

我跟着教程一步一步安装,但始终会出现各式各样的问题。一开始,我使用的是豆包,我对他确实有依赖性。

我的问题是:为什么插件安装成功了, ComfyUI 端也是能正常运行生图,但始终无法传回图片到 PS 端?

豆包给我很多解决方案,比如让我更换节点,让我改链路,让我改代码,我一一照做,但我始终只能看到这只丑蛙。

绿蛙是初始界面 说明节点没有真正连上

他甚至让我直接把 ComfyUI 里面生成好的图片直接拖进去 PS,这样就能物理上解决这个问题。

不是,那我辛辛苦苦装那么久这个插件是为了什么???

大家可以看一下,这是豆包给我的代码。我是一个完全不懂代码的小白,我也只能跟着 AI 来操作,但它始终无法解决我的问题。

from nodes import SaveImage
import hashlib
import asyncio
import json
import base64
import os
import time
import torch
import numpy as np
from PIL import Image, ImageOps
from io import BytesIO
import folder_paths
import torchvision.transforms.functional as tf
import aiohttp
nodepath = os.path.join(
    folder_paths.get_folder_paths("custom_nodes")[0], "comfyui-photoshop"
)
def is_changed_file(filepath):
    try:
        with open(filepath, "rb") as f:
            file_hash = hashlib.md5(f.read()).hexdigest()
        if not hasattr(is_changed_file, "file_hashes"):
            is_changed_file.file_hashes = {}
        if filepath in is_changed_file.file_hashes:
            if is_changed_file.file_hashes[filepath] == file_hash:
                return False
        is_changed_file.file_hashes[filepath] = file_hash
        return float("NaN")
    except Exception as e:
        print(f"Error in is_changed_file for {filepath}: {e}")
        return False
class PhotoshopToComfyUI:
    @classmethod
    def INPUT_TYPES(cls):
        return {"required": {}}
    RETURN_TYPES = ("IMAGE", "MASK", "FLOAT", "INT", "STRING", "STRING", "INT", "INT")
    RETURN_NAMES = ("Canvas", "Mask", "Slider", "Seed", "+", "-", "W", "H")
    FUNCTION = "PS_Execute"
    CATEGORY = "Photoshop"
    def PS_Execute(self):
        self.LoadDir()
        self.loadConfig()
        self.SendImg()
        sliderValue = self.slider / 100
        return (
            self.canvas,
            self.mask.unsqueeze(0),
            sliderValue,
            int(self.seed),
            self.psPrompt,
            self.ngPrompt,
            int(self.width),
            int(self.height),
        )
    def LoadDir(self, retry_count=0):
        try:
            self.canvasDir = os.path.join(
                nodepath, "data", "ps_inputs", "PS_canvas.png"
            )
            self.maskImgDir = os.path.join(nodepath, "data", "ps_inputs", "PS_mask.png")
            self.configJson = os.path.join(nodepath, "data", "ps_inputs", "config.json")
        except:
            time.sleep(0.5)
            if retry_count < 4:
                self.LoadDir(retry_count + 1)
            else:
                raise Exception(
                    "Failed to load directory after 5 attempts. \n 🔴 Make sure you have installed and started the Photoshop Plugin Successfully. \n 🔴 otherwise you can restart your Photoshop and your plugin to fix this problem."
                )
    def loadConfig(self, retry_count=0):
        try:
            with open(self.configJson, "r", encoding="utf-8") as file:
                self.ConfigData = json.load(file)
        except:
            time.sleep(0.5)
            if retry_count < 4:
                self.loadConfig(retry_count + 1)
            else:
                raise Exception(
                    "Failed to load config after 5 attempts. \n 🔴 Make sure you have installed and started the Photoshop Plugin Successfully. \n 🔴 otherwise you can restart your Photoshop and your plugin to fix this problem."
                )
        self.psPrompt = self.ConfigData["positive"]
        self.ngPrompt = self.ConfigData["negative"]
        self.seed = self.ConfigData["seed"]
        self.slider = self.ConfigData["slider"]
    def SendImg(self):
        self.loadImg(self.canvasDir)
        self.canvas = self.i.convert("RGB")
        self.canvas = np.array(self.canvas).astype(np.float32) / 255.0
        self.canvas = torch.from_numpy(self.canvas)[None,]
        self.width, self.height = self.i.size
        self.loadImg(self.maskImgDir)
        self.i = ImageOps.exif_transpose(self.i)
        self.mask = np.array(self.i.getchannel("B")).astype(np.float32) / 255.0
        self.mask = torch.from_numpy(self.mask)
        # Convert #010101 to #000000
        self.mask = self.mask.numpy()
        target_color = 1 / 255.0
        self.mask[self.mask == target_color] = 0.0
        self.mask = torch.from_numpy(self.mask)
    def loadImg(self, path):
        try:
            with open(path, "rb") as file:
                img_data = file.read()
            self.i = Image.open(BytesIO(img_data))
            self.i.verify()
            self.i = Image.open(BytesIO(img_data))
        except:
            self.i = Image.new(mode="RGB", size=(24, 24), color=(0, 0, 0))
        if not self.i:
            return
    @classmethod
    def IS_CHANGED(cls):
        try:
            configJson = os.path.join(nodepath, "data", "ps_inputs", "config.json")
            canvasDir = os.path.join(nodepath, "data", "ps_inputs", "PS_canvas.png")
            maskImgDir = os.path.join(nodepath, "data", "ps_inputs", "PS_mask.png")
            config_changed = is_changed_file(configJson)
            canvas_changed = is_changed_file(canvasDir)
            mask_changed = is_changed_file(maskImgDir)
            return config_changed or canvas_changed or mask_changed
        except Exception as e:
            print("Error in IS_CHANGED:", e)
            return 0
class ComfyUIToPhotoshop(SaveImage):
    def __init__(self):
        self.output_dir = folder_paths.get_temp_directory()
        self.type = "temp"
        self.prefix_append = "_temp_"
        self.compress_level = 4
    @staticmethod
    def INPUT_TYPES():
        return {
            "required": {
                "output": ("IMAGE",),
            },
            "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
        }
    FUNCTION = "execute"
    CATEGORY = "Photoshop"
    async def connect_to_backend(self, filename):
        try:
            url = f"http://127.0.0.1:8188/ps/renderdone?filename={filename}"
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as response:
                    return await response.text()
        except Exception as e:
            print(f"_PS_ error on send2Ps: {e}")
    def execute(
        self,
        output: torch.Tensor,
        filename_prefix="PS_OUTPUTS",
        prompt=None,
        extra_pnginfo=None,
    ):
        x = self.save_images(output, filename_prefix, prompt, extra_pnginfo)
        import asyncio
        loop = asyncio.get_event_loop()
        loop.create_task(self.connect_to_backend(x["ui"]["images"][0]["filename"]))
        return x
class ClipPass:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {"clip": ("CLIP",)}}
    RETURN_TYPES = ("CLIP",)
    RETURN_NAMES = ("clip",)
    FUNCTION = "exe"
    CATEGORY = "utils"
    def exe(self, clip):
        return (clip,)
class modelPass:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {"model": ("MODEL",)}}
    RETURN_TYPES = ("MODEL",)
    RETURN_NAMES = ("model",)
    FUNCTION = "exe"
    CATEGORY = "utils"
    def exe(self, model):
        return (model,)
NODE_CLASS_MAPPINGS = {
    "🔹Photoshop ComfyUI Plugin": PhotoshopToComfyUI,
    "🔹SendTo Photoshop Plugin": ComfyUIToPhotoshop,
    "🔹ClipPass": ClipPass,
    "🔹modelPass": modelPass,
}
NODE_DISPLAY_NAME_MAPPINGS = {
    "PhotoshopToComfyUI": "🔹Photoshop ComfyUI Plugin",
    "SendToPhotoshop": "🔹Send To Photoshop",
    "ClipPass": "🔹ClipPass",
    "modelPass": "🔹modelPass",
}

我对它的信任持续了一周,在这期间,它一直带着我在原地打转。我觉得不能再这样下去了,这会我想到了 Claude。

我为什么会忘记这个模型的存在呢?作为一名运营,我确实很少需要用到专业敲代码的模型,但他真的太太太太太强了。

我只问了他2个问题:

请你帮我排查一下,为什么我能在 ComfyUI 生成,在 PS 里点渲染,后端是会跑图,但是无法将图自动返回 PS,再者,第三张图片那里一直是一只丑蛙,怎么点都点不动?同时附上我的界面图片和后端代码。

他先是给我排查问题:

□ 1. ComfyUI 启动加 --enable-cors-header 

□ 2. PS插件 Settings 页面显示 "Connected"(绿色)

□ 3. 运行时不要切换 PS 焦点(保持插件面板可见) 

□ 4. SendTo Photoshop 节点检查连接 

□ 5. 点击"重新加载插件"后再试 

□ 6. Ai Panel ≠ ComfyUI Web,两者分开使用

再检查我的 nodePlugin.py 文件

他立马找到了我的核心 bug:

def execute(self, output, filename_prefix="PS_OUTPUTS", prompt=None, extra_pnginfo=None):
    x = self.save_images(output, filename_prefix, prompt, extra_pnginfo)
    import asyncio
    loop = asyncio.get_event_loop()
    loop.create_task(self.connect_to_backend(x["ui"]["images"][0]["filename"]))  # ← 问题在这
    return x

这是的问题分析过程

loop.create_task()    

↓ 

只是"注册"了一个异步任务    

↓ 

但 execute() 是同步函数,立刻 return 了    

↓ 

异步任务根本没有等待执行完成    

↓ 

图像保存了,但通知PS的请求从未真正发出去

然后他给我一整个正确的代码,这个是 Claude给我的代码,大家可以对比一下上面的。

from nodes import SaveImage
import hashlib
import asyncio
import json
import base64
import os
import time
import torch
import numpy as np
from PIL import Image, ImageOps
from io import BytesIO
import folder_paths
import torchvision.transforms.functional as tf
import aiohttp
import urllib.request
import urllib.error
import threading
nodepath = os.path.join(
    folder_paths.get_folder_paths("custom_nodes")[0], "comfyui-photoshop"
)
def is_changed_file(filepath):
    try:
        with open(filepath, "rb") as f:
            file_hash = hashlib.md5(f.read()).hexdigest()
        if not hasattr(is_changed_file, "file_hashes"):
            is_changed_file.file_hashes = {}
        if filepath in is_changed_file.file_hashes:
            if is_changed_file.file_hashes[filepath] == file_hash:
                return False
        is_changed_file.file_hashes[filepath] = file_hash
        return float("NaN")
    except Exception as e:
        print(f"Error in is_changed_file for {filepath}: {e}")
        return False
class PhotoshopToComfyUI:
    @classmethod
    def INPUT_TYPES(cls):
        return {"required": {}}
    RETURN_TYPES = ("IMAGE", "MASK", "FLOAT", "INT", "STRING", "STRING", "INT", "INT")
    RETURN_NAMES = ("Canvas", "Mask", "Slider", "Seed", "+", "-", "W", "H")
    FUNCTION = "PS_Execute"
    CATEGORY = "Photoshop"
    def PS_Execute(self):
        self.LoadDir()
        self.loadConfig()
        self.SendImg()
        sliderValue = self.slider / 100
        return (
            self.canvas,
            self.mask.unsqueeze(0),
            sliderValue,
            int(self.seed),
            self.psPrompt,
            self.ngPrompt,
            int(self.width),
            int(self.height),
        )
    def LoadDir(self, retry_count=0):
        try:
            self.canvasDir = os.path.join(
                nodepath, "data", "ps_inputs", "PS_canvas.png"
            )
            self.maskImgDir = os.path.join(nodepath, "data", "ps_inputs", "PS_mask.png")
            self.configJson = os.path.join(nodepath, "data", "ps_inputs", "config.json")
        except:
            time.sleep(0.5)
            if retry_count < 4:
                self.LoadDir(retry_count + 1)
            else:
                raise Exception(
                    "Failed to load directory after 5 attempts. \n 🔴 Make sure you have installed and started the Photoshop Plugin Successfully."
                )
    def loadConfig(self, retry_count=0):
        try:
            with open(self.configJson, "r", encoding="utf-8") as file:
                self.ConfigData = json.load(file)
        except:
            time.sleep(0.5)
            if retry_count < 4:
                self.loadConfig(retry_count + 1)
            else:
                raise Exception(
                    "Failed to load config after 5 attempts. \n 🔴 Make sure you have installed and started the Photoshop Plugin Successfully."
                )
        self.psPrompt = self.ConfigData["positive"]
        self.ngPrompt = self.ConfigData["negative"]
        self.seed = self.ConfigData["seed"]
        self.slider = self.ConfigData["slider"]
    def SendImg(self):
        self.loadImg(self.canvasDir)
        self.canvas = self.i.convert("RGB")
        self.canvas = np.array(self.canvas).astype(np.float32) / 255.0
        self.canvas = torch.from_numpy(self.canvas)[None,]
        self.width, self.height = self.i.size
        self.loadImg(self.maskImgDir)
        self.i = ImageOps.exif_transpose(self.i)
        self.mask = np.array(self.i.getchannel("B")).astype(np.float32) / 255.0
        self.mask = torch.from_numpy(self.mask)
        self.mask = self.mask.numpy()
        target_color = 1 / 255.0
        self.mask[self.mask == target_color] = 0.0
        self.mask = torch.from_numpy(self.mask)
    def loadImg(self, path):
        try:
            with open(path, "rb") as file:
                img_data = file.read()
            self.i = Image.open(BytesIO(img_data))
            self.i.verify()
            self.i = Image.open(BytesIO(img_data))
        except:
            self.i = Image.new(mode="RGB", size=(24, 24), color=(0, 0, 0))
        if not self.i:
            return
    @classmethod
    def IS_CHANGED(cls):
        try:
            configJson = os.path.join(nodepath, "data", "ps_inputs", "config.json")
            canvasDir = os.path.join(nodepath, "data", "ps_inputs", "PS_canvas.png")
            maskImgDir = os.path.join(nodepath, "data", "ps_inputs", "PS_mask.png")
            config_changed = is_changed_file(configJson)
            canvas_changed = is_changed_file(canvasDir)
            mask_changed = is_changed_file(maskImgDir)
            return config_changed or canvas_changed or mask_changed
        except Exception as e:
            print("Error in IS_CHANGED:", e)
            return 0
class ComfyUIToPhotoshop(SaveImage):
    def __init__(self):
        self.output_dir = folder_paths.get_temp_directory()
        self.type = "temp"
        self.prefix_append = "_temp_"
        self.compress_level = 4
    @staticmethod
    def INPUT_TYPES():
        return {
            "required": {
                "output": ("IMAGE",),
            },
            "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
        }
    FUNCTION = "execute"
    CATEGORY = "Photoshop"
    def send_to_ps_sync(self, filename):
        """
        用独立线程+同步请求通知PS,避免异步任务丢失问题
        带3次重试,每次间隔1秒
        "
""
        url = f"http://127.0.0.1:8188/ps/renderdone?filename={filename}"
        
        for attempt in range(3):
            try:
                print(f"_PS_ 正在回传图像到Photoshop... 第{attempt + 1}次尝试")
                print(f"_PS_ 请求URL: {url}")
                
                req = urllib.request.Request(url)
                with urllib.request.urlopen(req, timeout=15) as response:
                    result = response.read().decode()
                    print(f"_PS_ ✅ 回传成功! 响应: {result}")
                    return
                    
            except urllib.error.URLError as e:
                print(f"_PS_ ❌ 第{attempt + 1}次失败 (URLError): {e.reason}")
            except Exception as e:
                print(f"_PS_ ❌ 第{attempt + 1}次失败 (Exception): {e}")
            
            # 最后一次不需要等待
            if attempt < 2:
                print(f"_PS_ 等待1秒后重试...")
                time.sleep(1)
        
        print("_PS_ ❌ 3次重试全部失败,图像未能回传到Photoshop")
        print("_PS_ 请检查: 1.PS插件是否连接 2.端口8188是否正确")
    def execute(
        self,
        output: torch.Tensor,
        filename_prefix="PS_OUTPUTS",
        prompt=None,
        extra_pnginfo=None,
    ):
        # 先保存图像
        x = self.save_images(output, filename_prefix, prompt, extra_pnginfo)
        
        filename = x["ui"]["images"][0]["filename"]
        print(f"_PS_ 图像已保存: {filename}")
        
        # 用独立线程发送,不阻塞ComfyUI主线程
        t = threading.Thread(
            target=self.send_to_ps_sync,
            args=(filename,),
            daemon=True
        )
        t.start()
        
        # 等待最多20秒让回传完成
        t.join(timeout=20)
        
        if t.is_alive():
            print("_PS_ ⚠️ 回传超时(20s),继续后台尝试...")
        
        return x
class ClipPass:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {"clip": ("CLIP",)}}
    RETURN_TYPES = ("CLIP",)
    RETURN_NAMES = ("clip",)
    FUNCTION = "exe"
    CATEGORY = "utils"
    def exe(self, clip):
        return (clip,)
class modelPass:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": {"model": ("MODEL",)}}
    RETURN_TYPES = ("MODEL",)
    RETURN_NAMES = ("model",)
    FUNCTION = "exe"
    CATEGORY = "utils"
    def exe(self, model):
        return (model,)
NODE_CLASS_MAPPINGS = {
    "🔹Photoshop ComfyUI Plugin": PhotoshopToComfyUI,
    "🔹SendTo Photoshop Plugin": ComfyUIToPhotoshop,
    "🔹ClipPass": ClipPass,
    "🔹modelPass": modelPass,
}
NODE_DISPLAY_NAME_MAPPINGS = {
    "PhotoshopToComfyUI": "🔹Photoshop ComfyUI Plugin",
    "SendToPhotoshop": "🔹Send To Photoshop",
    "ClipPass": "🔹ClipPass",
    "modelPass": "🔹modelPass",
}

问题解决了😭

前后花费时间不到10分钟。

术业有专攻这句话,不仅体现在人类身上,还体现在 AI 大模型里。

豆包很强,能解决很多我日常的问题,能给我写工作总结、制作PPT 等,但某些领域上,我们还是需要选择专业的大模型。

比如现在我要制作海报、生成图片,我会优先选择 Gemini 的模型;我想生成音乐,我会选择 Minimax;我想写长篇小说,我会选择 GPT;我需要安装软件涉及到代码的,我会选择 Claude。

想知道如何能一键快速调用 AI 大模型吗? 

欢迎来到Nebula API:openai-nebula.com

你想要的全都有⬇️

Nebula Data 星雲數據,总部位于新加坡,在雅加达、广州、上海、香港设有分支机构。公司自主研发 Nebula Lab 一站式 AI 内容生成与模型聚合平台,搭载企业级 AI Agent,聚合全球通用大模型与行业垂直模型;同步推出 Nebula AIoT 硬件生态体系(含智能交互终端、物联网网关等产品),形成 “云 - 边 - 端” 全链路智能解决方案,为电商、制造、零售等多领域客户提供从云端算力支撑、AI 智能决策到终端场景落地的一体化服务;同时提供全球 AIDC(AI 智算中心)+ 低延迟网络服务,以技术底座赋能企业拥抱 AI、链接物理世界,拓展全球业务。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐