Files
rui40000-RUI-Nodes/openai_node.py
T
rui40000andClaude Opus 4.6 4b61f92262 fix: OpenAI节点URL被截断报错及无图像时不调用API的问题
1. 修复ComfyUI前端吞掉URL中'//'导致 Invalid URL 'https:' 报错
   - 新增 _fix_url() 函数,在请求前自动补回被吞的'//'
2. 修复无图像输入时节点直接返回user_prompt而不调用API的问题
   - 有图像时:多模态格式(image_url+text)发送请求
   - 无图像+有prompt时:纯文本模式正常调用API对话
   - 无图像+无prompt时:返回明确的中文错误提示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 13:53:53 +08:00

257 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
import requests
import json
import base64
import io
import os
import re
from PIL import Image
# 快速解决方案:清除可能导致连接错误的代理环境变量
# Fast solution: Clear proxy environment variables that might cause connection errors
# 许多用户在使用 requests 库连接 OpenAI API 时会遇到 ProxyError
# 这是因为 Python 环境可能读取了不正确的系统代理设置
# Many users encounter ProxyError when connecting to OpenAI API with requests
# This is because the Python environment might read incorrect system proxy settings
os.environ['HTTP_PROXY'] = ''
os.environ['HTTPS_PROXY'] = ''
os.environ['http_proxy'] = ''
os.environ['https_proxy'] = ''
def _fix_url(url: str) -> str:
"""
修复 ComfyUI 前端吞掉 URL 中 '//' 的问题。
例如:
'https:api.openai.com/v1/...' -> 'https://api.openai.com/v1/...'
'https:' -> 原样返回(后续会报更清晰的错误)
'https://api.openai.com/v1/...' -> 不变
"""
url = url.strip()
# 匹配 http: 或 https: 后面紧跟的不是 // 的情况
fixed = re.sub(r'^(https?:)(?!//)', r'\1//', url)
return fixed
class OpenAINode:
"""
OpenAI API 节点:
支持连接 OpenAI 及其兼容 API(如 DeepSeek, Moonshot 等),
支持文本生成和多模态图像理解。
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_url": ("STRING", {
"default": "https://api.openai.com/v1/chat/completions",
"multiline": False
}),
"api_key": ("STRING", {
"default": "",
"multiline": False
}),
"model": ("STRING", {
"default": "gpt-4o",
"multiline": False
}),
"system_prompt": ("STRING", {
"default": "You are a helpful assistant.",
"multiline": True
}),
"user_prompt": ("STRING", {
"default": "",
"multiline": True
}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff
}),
},
"optional": {
"image_1": ("IMAGE",),
"image_2": ("IMAGE",),
"image_3": ("IMAGE",),
"image_4": ("IMAGE",),
"image_5": ("IMAGE",),
"image_6": ("IMAGE",),
"temperature": ("FLOAT", {
"default": 0.3,
"min": 0.0,
"max": 2.0,
"step": 0.1
}),
"max_tokens": ("INT", {
"default": 500,
"min": 1,
"max": 8192
}),
"detail": (["low", "high", "auto"], {
"default": "auto"
}),
"image_max_size": ("INT", {
"default": 1024,
"min": 256,
"max": 4096,
"step": 64
}),
"proxy_url": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "e.g., http://127.0.0.1:7890"
}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "generate_content"
CATEGORY = "Rui-Node🐶/AI模型🤖"
def _encode_image_tensor(self, img_tensor, image_max_size):
img_np = img_tensor.cpu().numpy()
img_np = np.clip(img_np, 0, 1)
img_pil = Image.fromarray((img_np * 255).astype(np.uint8), 'RGB')
width, height = img_pil.size
if max(width, height) > image_max_size:
ratio = image_max_size / max(width, height)
img_pil = img_pil.resize(
(max(1, int(width * ratio)), max(1, int(height * ratio))),
Image.LANCZOS
)
buffered = io.BytesIO()
img_pil.save(buffered, format="JPEG", quality=85)
return base64.b64encode(buffered.getvalue()).decode('utf-8')
def generate_content(
self,
api_url,
api_key,
model,
system_prompt,
user_prompt,
seed,
image_1=None,
image_2=None,
image_3=None,
image_4=None,
image_5=None,
image_6=None,
temperature=0.3,
max_tokens=500,
detail="auto",
image_max_size=1024,
proxy_url=""
):
"""
调用 OpenAI API 生成内容。
- 有图像时:使用多模态格式(image_url + text)发送请求
- 无图像时:使用纯文本格式发送请求,实现普通对话
"""
# ---- 修复 URL:ComfyUI 前端可能吞掉 '//' ----
api_url = _fix_url(api_url)
all_images = [
img for img in [
image_1,
image_2,
image_3,
image_4,
image_5,
image_6,
] if img is not None
]
# ---- 构造 messages ----
messages = [
{"role": "system", "content": system_prompt}
]
if all_images:
# 多模态模式:图像 + 文本
user_content = []
if user_prompt:
user_content.append({
"type": "text",
"text": user_prompt
})
for image in all_images:
img_tensor = image[0]
img_base64 = self._encode_image_tensor(img_tensor, image_max_size)
user_content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{img_base64}",
"detail": detail
}
})
if not user_content:
user_content.append({
"type": "text",
"text": " "
})
messages.append({
"role": "user",
"content": user_content
})
else:
# 纯文本模式:无图像,直接发送文本对话
prompt_text = user_prompt.strip() if user_prompt else ""
if not prompt_text:
return ("(错误:未提供图片也未提供提示词,请至少输入 user_prompt)",)
messages.append({
"role": "user",
"content": prompt_text
})
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
payload = {
"model": model,
"messages": messages,
"seed": seed,
"temperature": temperature,
"max_tokens": max_tokens,
}
proxies = None
if proxy_url and proxy_url.strip():
proxies = {
"http": proxy_url,
"https": proxy_url
}
try:
response = requests.post(api_url, headers=headers, json=payload, proxies=proxies, timeout=60)
response.raise_for_status()
result = response.json()
if "choices" in result and len(result["choices"]) > 0:
content = result["choices"][0]["message"]["content"]
return (content,)
else:
return (f"Error: API response format unexpected. Response: {json.dumps(result)}",)
except Exception as e:
return (f"Error calling OpenAI API: {str(e)}",)
# 节点映射字典
NODE_CLASS_MAPPINGS = {
"OpenAIAPINode": OpenAINode
}
# 节点显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {
"OpenAIAPINode": "OpenAI API 连接 / OpenAI API Connector"
}