Files
rui40000-RUI-Nodes/openai_node.py
T
rui40000andClaude Opus 4.6 830e829108 fix: 彻底解决OpenAI节点URL被ComfyUI前端吞掉的问题
根因:ComfyUI的动态提示系统或第三方扩展(如cg-use-everywhere)
会将STRING控件中的'//'视为注释语法,导致'://'及其后的
主机名+路径被彻底丢弃,仅靠补回'//'无法恢复已丢失的内容。

解决方案:
- 将原来的单个api_url输入拆分为protocol下拉框 + 地址字符串
- protocol下拉框提供 https:// 和 http:// 选项(下拉框不受文本处理影响)
- api_url默认值不再包含'://',从根源避免被吞
- 新增_build_url()函数智能拼接URL,兼容多种用户输入格式
- 即使地址被完全清空也会回退到OpenAI默认地址
- 添加控制台日志输出实际请求地址,便于调试

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

278 lines
8.7 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 _build_url(protocol: str, api_url: str) -> str:
"""
将协议和地址拼接为完整 URL。
兼容多种用户输入格式:
- 'api.openai.com/v1/chat/completions' (标准,无协议前缀)
- 'https://api.openai.com/v1/chat/completions' (用户手动写了完整 URL)
- 'https:api.openai.com/...' (被 ComfyUI 吞掉了 //)
- 'http://127.0.0.1:5000/v1/chat/completions' (本地服务)
"""
url = api_url.strip()
# 如果用户手动输入了完整 URL(含协议),直接修复并使用
if re.match(r'^https?://.+', url):
return url
# 去掉可能残留的被截断的协议前缀,如 "https:" "http:" "https://" "http://"
url = re.sub(r'^https?:/*', '', url)
# 去掉首尾多余的斜杠
url = url.strip('/')
if not url:
# 全部被吞了,回退到 OpenAI 默认地址
url = 'api.openai.com/v1/chat/completions'
return f"{protocol}{url}"
class OpenAINode:
"""
OpenAI API 节点:
支持连接 OpenAI 及其兼容 API(如 DeepSeek, Moonshot 等),
支持文本生成和多模态图像理解。
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"protocol": (["https://", "http://"], {
"default": "https://"
}),
"api_url": ("STRING", {
"default": "api.openai.com/v1/chat/completions",
"multiline": False,
"placeholder": "api.openai.com/v1/chat/completions"
}),
"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,
protocol,
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 ----
full_url = _build_url(protocol, api_url)
print(f"[Rui-Node🐶 OpenAI] 请求地址: {full_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(full_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"
}