feat:初版

This commit is contained in:
2025-12-03 15:48:44 +08:00
commit b4df26f61d
199 changed files with 23434 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from .main import Flow2API
__all__ = ["Flow2API"]

556
plugins/Flow2API/main.py Normal file
View File

@@ -0,0 +1,556 @@
"""
Flow2API AI绘图插件
支持命令触发和LLM工具调用支持横屏/竖屏选择,支持图生图
"""
import asyncio
import tomllib
import httpx
import uuid
import xml.etree.ElementTree as ET
import base64
from pathlib import Path
from datetime import datetime
from typing import List, Optional
from loguru import logger
from utils.plugin_base import PluginBase
from utils.decorators import on_text_message
from WechatHook import WechatHookClient
# 定义引用消息装饰器
def on_quote_message(priority=50):
"""引用消息装饰器"""
def decorator(func):
setattr(func, '_event_type', 'quote_message')
setattr(func, '_priority', min(max(priority, 0), 99))
return func
return decorator
class Flow2API(PluginBase):
"""Flow2API AI绘图插件"""
description = "Flow2API AI绘图插件 - 支持横屏/竖屏AI绘图和LLM工具调用"
author = "ShiHao"
version = "1.0.0"
def __init__(self):
super().__init__()
self.config = None
self.images_dir = None
async def async_init(self):
"""异步初始化"""
config_path = Path(__file__).parent / "config.toml"
with open(config_path, "rb") as f:
self.config = tomllib.load(f)
# 创建图片目录
self.images_dir = Path(__file__).parent / "images"
self.images_dir.mkdir(exist_ok=True)
logger.success("Flow2API AI插件初始化完成")
def _get_model(self, orientation: str) -> str:
"""根据方向获取模型名称"""
if orientation == "landscape":
return "gemini-3.0-pro-image-landscape"
else:
return "gemini-3.0-pro-image-portrait"
async def generate_image(self, prompt: str, orientation: str = "portrait", image_base64: str = None) -> List[str]:
"""
生成图像
Args:
prompt: 提示词
orientation: 方向 (portrait/landscape)
Returns:
图片本地路径列表
"""
api_config = self.config["api"]
gen_config = self.config["generation"]
max_retry = gen_config["max_retry_attempts"]
model = self._get_model(orientation)
for attempt in range(max_retry):
if attempt > 0:
await asyncio.sleep(min(2 ** attempt, 10))
try:
url = f"{api_config['base_url'].rstrip('/')}/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_config['token']}"
}
# 构造消息内容
if image_base64:
# 图生图模式
content = [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": image_base64}}
]
else:
# 文生图模式
content = prompt
payload = {
"model": model,
"messages": [{"role": "user", "content": content}],
"stream": True
}
logger.info(f"Flow2API请求: {model}, 提示词长度: {len(prompt)} 字符")
logger.debug(f"完整提示词: {prompt}")
logger.debug(f"请求URL: {url}")
logger.debug(f"Payload大小: {len(str(payload))} 字节")
# 设置合理的超时时间
# 对于流式响应read 超时是指两次数据块之间的最大间隔
max_timeout = min(api_config["timeout"], 600) # 增加到 10 分钟
timeout = httpx.Timeout(
connect=10.0, # 连接超时10秒
read=max_timeout, # 读取超时10分钟
write=10.0, # 写入超时10秒
pool=10.0 # 连接池超时10秒
)
logger.debug(f"超时配置: connect=10s, read={max_timeout}s")
# 添加提示词长度检查和警告
if len(prompt) > 1000:
logger.warning(f"提示词较长 ({len(prompt)} 字符),可能影响处理速度")
# 获取 AIChat 的代理配置
proxy = await self._get_aichat_proxy()
async with httpx.AsyncClient(timeout=timeout, proxy=proxy) as client:
async with client.stream("POST", url, json=payload, headers=headers) as response:
logger.debug(f"收到响应状态码: {response.status_code}")
if response.status_code == 200:
# 处理流式响应
image_url = None
full_content = ""
async for line in response.aiter_lines():
logger.debug(f"收到响应行: {line[:100]}...")
if line.startswith("data: "):
data_str = line[6:]
if data_str == "[DONE]":
logger.debug("收到 [DONE] 标记")
break
try:
import json
data = json.loads(data_str)
if "choices" in data and data["choices"]:
delta = data["choices"][0].get("delta", {})
content = delta.get("content", "")
if content:
full_content += content
logger.debug(f"累积内容: {full_content[:100]}...")
if "http" in content:
# 提取图片URL
import re
urls = re.findall(r'https?://[^\s\)\]"\']+', content)
if urls:
image_url = urls[0].rstrip("'\"")
logger.info(f"提取到图片URL: {image_url}")
except Exception as e:
logger.warning(f"解析响应数据失败: {e}, 数据: {data_str[:100]}")
continue
# 如果没有从流中提取到URL尝试从完整内容中提取
if not image_url and full_content:
import re
urls = re.findall(r'https?://[^\s\)\]"\']+', full_content)
if urls:
image_url = urls[0].rstrip("'\"")
logger.info(f"从完整内容提取到图片URL: {image_url}")
else:
logger.warning(f"完整响应内容中未找到URL: {full_content}")
if not image_url:
logger.error(f"未能提取到图片URL完整响应: {full_content}")
if image_url:
# 下载图片
image_path = await self._download_image(image_url)
if image_path:
logger.success("成功生成图像")
return [image_path]
else:
# 下载失败,继续重试
logger.warning(f"图片下载失败,将重试 ({attempt + 1}/{max_retry})")
continue
elif response.status_code == 401:
logger.error("Token认证失败")
return []
else:
error_text = await response.aread()
logger.error(f"API请求失败: {response.status_code}, {error_text[:200]}")
continue
except asyncio.TimeoutError:
logger.warning(f"请求超时asyncio.TimeoutError重试中... ({attempt + 1}/{max_retry})")
continue
except httpx.ReadTimeout:
logger.warning(f"读取超时ReadTimeout可能是图像生成时间过长重试中... ({attempt + 1}/{max_retry})")
logger.info(f"提示词长度: {len(prompt)} 字符,建议缩短提示词或增加超时时间")
continue
except Exception as e:
import traceback
logger.error(f"请求异常: {type(e).__name__}: {str(e)}")
logger.error(f"异常详情:\n{traceback.format_exc()}")
logger.error(f"提示词长度: {len(prompt)} 字符")
continue
logger.error("图像生成失败")
return []
async def _get_aichat_proxy(self) -> Optional[str]:
"""获取 AIChat 插件的 SOCKS5 代理配置"""
try:
aichat_config_path = Path(__file__).parent.parent / "AIChat" / "config.toml"
if aichat_config_path.exists():
with open(aichat_config_path, "rb") as f:
aichat_config = tomllib.load(f)
proxy_config = aichat_config.get("proxy", {})
if proxy_config.get("enabled", False):
proxy_type = proxy_config.get("type", "socks5")
proxy_host = proxy_config.get("host", "127.0.0.1")
proxy_port = proxy_config.get("port", 7890)
proxy = f"{proxy_type}://{proxy_host}:{proxy_port}"
logger.info(f"使用 AIChat 代理: {proxy}")
return proxy
except Exception as e:
logger.warning(f"读取 AIChat 代理配置失败: {e}")
return None
async def _download_image(self, url: str) -> Optional[str]:
"""下载图片到本地"""
try:
timeout = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0)
# 获取 AIChat 的代理配置
proxy = await self._get_aichat_proxy()
async with httpx.AsyncClient(timeout=timeout, proxy=proxy) as client:
response = await client.get(url)
response.raise_for_status()
# 生成文件名
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
uid = uuid.uuid4().hex[:8]
file_path = self.images_dir / f"flow2_{ts}_{uid}.jpg"
# 保存文件
with open(file_path, "wb") as f:
f.write(response.content)
logger.info(f"图片下载成功: {file_path}")
return str(file_path)
except Exception as e:
logger.error(f"下载图片失败: {e}")
return None
@on_text_message(priority=70)
async def handle_message(self, bot: WechatHookClient, message: dict):
"""处理文本消息"""
if not self.config["behavior"]["enable_command"]:
return True
content = message.get("Content", "").strip()
from_wxid = message.get("FromWxid", "")
is_group = message.get("IsGroup", False)
# 检查群聊/私聊开关
if is_group and not self.config["behavior"]["enable_group"]:
return True
if not is_group and not self.config["behavior"]["enable_private"]:
return True
# 检查是否是绘图命令
keywords = self.config["behavior"]["command_keywords"]
matched_keyword = None
for keyword in keywords:
if content.startswith(keyword + " ") or content.startswith(keyword + "横屏 ") or content.startswith(keyword + "竖屏 "):
matched_keyword = keyword
break
if not matched_keyword:
return True
# 提取方向和提示词
rest = content[len(matched_keyword):].strip()
orientation = self.config["generation"]["default_orientation"]
if rest.startswith("横屏 "):
orientation = "landscape"
prompt = rest[3:].strip()
elif rest.startswith("竖屏 "):
orientation = "portrait"
prompt = rest[3:].strip()
else:
prompt = rest
if not prompt:
await bot.send_text(from_wxid, "❌ 请提供绘图提示词\n用法: /绘画f <提示词> 或 /绘画f横屏 <提示词>")
return False
logger.info(f"收到绘图请求: {prompt[:50]}..., 方向: {orientation}")
try:
# 生成图像
image_paths = await self.generate_image(prompt, orientation)
if image_paths:
# 直接发送图片
await bot.send_image(from_wxid, image_paths[0])
logger.success("绘图成功,已发送图片")
else:
await bot.send_text(from_wxid, "❌ 图像生成失败,请稍后重试")
except Exception as e:
logger.error(f"绘图处理失败: {e}")
await bot.send_text(from_wxid, f"❌ 处理失败: {str(e)}")
return False
@on_quote_message(priority=70)
async def handle_quote_message(self, bot: WechatHookClient, message: dict):
"""处理引用图片的绘图命令"""
if not self.config["behavior"]["enable_command"]:
return True
content = message.get("Content", "").strip()
from_wxid = message.get("FromWxid", "")
is_group = message.get("IsGroup", False)
# 检查群聊/私聊开关
if is_group and not self.config["behavior"]["enable_group"]:
return True
if not is_group and not self.config["behavior"]["enable_private"]:
return True
# 解析 XML 获取标题和引用消息
try:
root = ET.fromstring(content)
title = root.find(".//title")
if title is None or not title.text:
return True
title_text = title.text.strip()
# 检查是否是绘图命令
keywords = self.config["behavior"]["command_keywords"]
matched_keyword = None
for keyword in keywords:
if title_text.startswith(keyword + " ") or title_text.startswith(keyword + "横屏 ") or title_text.startswith(keyword + "竖屏 "):
matched_keyword = keyword
break
if not matched_keyword:
return True
# 提取方向和提示词
rest = title_text[len(matched_keyword):].strip()
orientation = self.config["generation"]["default_orientation"]
if rest.startswith("横屏 "):
orientation = "landscape"
prompt = rest[3:].strip()
elif rest.startswith("竖屏 "):
orientation = "portrait"
prompt = rest[3:].strip()
else:
prompt = rest
if not prompt:
await bot.send_text(from_wxid, "❌ 请提供绘图提示词")
return False
# 获取引用消息中的图片信息
refermsg = root.find(".//refermsg")
if refermsg is None:
return True # 不是引用消息,让普通命令处理
# 解析引用消息的内容
refer_content = refermsg.find("content")
if refer_content is None or not refer_content.text:
await bot.send_text(from_wxid, "❌ 引用的消息中没有图片")
return False
# 解码 HTML 实体
import html
refer_xml = html.unescape(refer_content.text)
refer_root = ET.fromstring(refer_xml)
# 提取图片信息
img = refer_root.find(".//img")
if img is None:
await bot.send_text(from_wxid, "❌ 引用的消息中没有图片")
return False
# 获取图片的 CDN URL 和 AES Key
cdnbigimgurl = img.get("cdnbigimgurl", "")
aeskey = img.get("aeskey", "")
if not cdnbigimgurl or not aeskey:
await bot.send_text(from_wxid, "❌ 无法获取图片信息")
return False
logger.info(f"收到图生图请求: {prompt[:50]}..., 方向: {orientation}")
except Exception as e:
logger.error(f"解析引用消息失败: {e}")
return True
try:
# 下载图片并转换为 base64
image_base64 = await self._download_and_encode_image(bot, cdnbigimgurl, aeskey)
if not image_base64:
await bot.send_text(from_wxid, "❌ 无法下载图片")
return False
# 生成图像
image_paths = await self.generate_image(prompt, orientation, image_base64)
if image_paths:
# 直接发送图片
await bot.send_image(from_wxid, image_paths[0])
logger.success("图生图成功,已发送图片")
else:
await bot.send_text(from_wxid, "❌ 图像生成失败,请稍后重试")
except Exception as e:
logger.error(f"图生图处理失败: {e}")
await bot.send_text(from_wxid, f"❌ 处理失败: {str(e)}")
return False
async def _download_and_encode_image(self, bot, cdnurl: str, aeskey: str) -> str:
"""下载图片并转换为 base64"""
try:
# 创建临时目录
temp_dir = Path(__file__).parent / "temp"
temp_dir.mkdir(exist_ok=True)
# 生成临时文件名
filename = f"temp_{datetime.now():%Y%m%d_%H%M%S}_{uuid.uuid4().hex[:8]}.jpg"
save_path = str((temp_dir / filename).resolve())
# 使用 CDN 下载 API 下载图片
logger.info(f"正在下载图片: {cdnurl[:50]}...")
success = await bot.cdn_download(cdnurl, aeskey, save_path, file_type=2)
if not success:
# 如果中图下载失败,尝试原图
logger.warning("中图下载失败,尝试下载原图...")
success = await bot.cdn_download(cdnurl, aeskey, save_path, file_type=1)
if not success:
logger.error("图片下载失败")
return ""
# 等待文件写入完成
import os
max_wait = 10
wait_time = 0
while wait_time < max_wait:
if os.path.exists(save_path) and os.path.getsize(save_path) > 0:
logger.info(f"文件已就绪: {save_path}")
break
await asyncio.sleep(0.5)
wait_time += 0.5
if not os.path.exists(save_path):
logger.error(f"文件下载超时或失败: {save_path}")
return ""
# 读取图片并转换为 base64
with open(save_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode()
# 删除临时文件
try:
Path(save_path).unlink()
except:
pass
return f"data:image/jpeg;base64,{image_data}"
except Exception as e:
logger.error(f"下载图片失败: {e}")
return ""
def get_llm_tools(self) -> List[dict]:
"""返回LLM工具定义"""
if not self.config["llm_tool"]["enabled"]:
return []
return [{
"type": "function",
"function": {
"name": self.config["llm_tool"]["tool_name"],
"description": self.config["llm_tool"]["tool_description"],
"parameters": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "图像生成提示词,描述想要生成的图像内容"
},
"orientation": {
"type": "string",
"enum": ["portrait", "landscape"],
"description": "图像方向。portrait=竖屏(适合人物、竖版海报)landscape=横屏(适合风景、横版场景)。根据用户描述的内容判断人物肖像、站立的人、竖版构图用portrait风景、全景、横向场景用landscape。"
}
},
"required": ["prompt", "orientation"]
}
}
}]
async def execute_llm_tool(self, tool_name: str, arguments: dict, bot: WechatHookClient, from_wxid: str) -> dict:
"""执行LLM工具调用"""
expected_tool_name = self.config["llm_tool"]["tool_name"]
if tool_name != expected_tool_name:
return None
try:
prompt = arguments.get("prompt")
orientation = arguments.get("orientation", "portrait")
image_base64 = arguments.get("image_base64") # 支持图生图
if not prompt:
return {"success": False, "message": "缺少提示词参数"}
if image_base64:
logger.info(f"LLM工具调用图生图: {prompt[:50]}..., 方向: {orientation}")
else:
logger.info(f"LLM工具调用绘图: {prompt[:50]}..., 方向: {orientation}")
# 生成图像
image_paths = await self.generate_image(prompt, orientation, image_base64)
if image_paths:
# 直接发送图片
await bot.send_image(from_wxid, image_paths[0])
return {
"success": True,
"message": f"已生成并发送{'竖屏' if orientation == 'portrait' else '横屏'}图像",
"images": [image_paths[0]]
}
else:
return {"success": False, "message": "图像生成失败"}
except Exception as e:
logger.error(f"LLM工具执行失败: {e}")
return {"success": False, "message": f"执行失败: {str(e)}"}

View File

@@ -0,0 +1,337 @@
# Flow2API 插件修复说明
## 问题诊断
根据日志:
```
2025-11-26 11:44:16 | INFO | 使用 AIChat 代理: socks5://38.55.107.103:53054
2025-11-26 11:44:35 | ERROR | 图像生成失败
```
**问题:** 只显示"图像生成失败",没有具体的错误信息,无法诊断问题原因。
## 修复方案
### 1. 改进错误日志 (main.py:204-209)
**修改前:**
```python
except Exception as e:
logger.error(f"请求异常: {e}")
continue
```
**修改后:**
```python
except Exception as e:
import traceback
logger.error(f"请求异常: {type(e).__name__}: {str(e)}")
logger.error(f"异常详情:\n{traceback.format_exc()}")
logger.error(f"提示词长度: {len(prompt)} 字符")
continue
```
**改进点:**
- 显示完整的异常类型和堆栈跟踪
- 记录提示词长度,便于诊断
- 不再截断错误信息
### 2. 增强调试信息 (main.py:108-126)
**修改前:**
```python
logger.info(f"Flow2API请求: {model}, 提示词: {prompt[:50]}...")
timeout = httpx.Timeout(connect=10.0, read=api_config["timeout"], write=10.0, pool=10.0)
```
**修改后:**
```python
logger.info(f"Flow2API请求: {model}, 提示词长度: {len(prompt)} 字符")
logger.debug(f"完整提示词: {prompt}")
logger.debug(f"请求URL: {url}")
logger.debug(f"Payload大小: {len(str(payload))} 字节")
# 设置合理的超时时间
max_timeout = min(api_config["timeout"], 600) # 增加到 10 分钟
timeout = httpx.Timeout(
connect=10.0, # 连接超时10秒
read=max_timeout, # 读取超时10分钟
write=10.0, # 写入超时10秒
pool=10.0 # 连接池超时10秒
)
logger.debug(f"超时配置: connect=10s, read={max_timeout}s")
# 添加提示词长度检查和警告
if len(prompt) > 1000:
logger.warning(f"提示词较长 ({len(prompt)} 字符),可能影响处理速度")
```
**改进点:**
- 记录提示词长度而不是截断内容
- 增加超时时间到 10 分钟
- 添加 DEBUG 级别的详细信息
- 长提示词警告
### 3. 改进响应处理 (main.py:133-187)
**新增功能:**
```python
logger.debug(f"收到响应状态码: {response.status_code}")
# 处理流式响应
image_url = None
full_content = ""
async for line in response.aiter_lines():
logger.debug(f"收到响应行: {line[:100]}...")
# ... 解析逻辑 ...
if content:
full_content += content
logger.debug(f"累积内容: {full_content[:100]}...")
# 如果没有从流中提取到URL尝试从完整内容中提取
if not image_url and full_content:
import re
urls = re.findall(r'https?://[^\s\)\]"\']+', full_content)
if urls:
image_url = urls[0].rstrip("'\"")
logger.info(f"从完整内容提取到图片URL: {image_url}")
else:
logger.warning(f"完整响应内容中未找到URL: {full_content}")
if not image_url:
logger.error(f"未能提取到图片URL完整响应: {full_content}")
```
**改进点:**
- 记录每一步的处理过程
- 累积完整响应内容
- 双重 URL 提取机制
- 显示完整响应内容以便调试
### 4. 添加 ReadTimeout 处理 (main.py:200-203)
```python
except httpx.ReadTimeout:
logger.warning(f"读取超时ReadTimeout可能是图像生成时间过长重试中... ({attempt + 1}/{max_retry})")
logger.info(f"提示词长度: {len(prompt)} 字符,建议缩短提示词或增加超时时间")
continue
```
**改进点:**
- 单独捕获 `ReadTimeout` 异常
- 提供友好的错误提示
- 自动重试(最多 3 次)
### 5. 添加下载失败重试 (main.py:184-187)
```python
if image_path:
logger.success("成功生成图像")
return [image_path]
else:
# 下载失败,继续重试
logger.warning(f"图片下载失败,将重试 ({attempt + 1}/{max_retry})")
continue
```
**改进点:**
- 图片下载失败时自动重试
- 重新请求 API 获取新的图片 URL
## 常见错误及解决方案
### 错误 1: 未能提取到图片URL
**日志示例:**
```
ERROR | 未能提取到图片URL完整响应: {"error": "..."}
```
**可能原因:**
- API 返回了错误而不是图片 URL
- 响应格式不符合预期
- Token 无效或过期
**解决方案:**
- 检查 API Token 是否正确
- 查看完整响应内容,了解 API 返回的错误信息
- 检查 API 配置base_url
### 错误 2: ReadTimeout 超时
**日志示例:**
```
WARNING | 读取超时ReadTimeout可能是图像生成时间过长重试中... (1/3)
```
**可能原因:**
- 图像生成时间过长(复杂提示词)
- 网络不稳定
- API 服务器响应慢
**解决方案:**
- 缩短提示词长度
- 增加配置文件中的 `timeout`
- 检查网络连接和代理设置
### 错误 3: 图片下载失败
**日志示例:**
```
ERROR | 下载图片失败: Client error '404 Not Found'
WARNING | 图片下载失败,将重试 (1/3)
```
**可能原因:**
- 图片 URL 已过期
- CDN 链接失效
- 网络问题
**解决方案:**
- 自动重试会重新请求 API 获取新 URL
- 检查代理设置
- 如果持续失败,联系 API 提供商
### 错误 4: Token 认证失败
**日志示例:**
```
ERROR | Token认证失败
```
**解决方案:**
- 检查 `config.toml` 中的 `token` 配置
- 确认 Token 是否有效
- 联系 API 提供商获取新 Token
## 调试步骤
### 1. 启用 DEBUG 日志
修改 `main_config.toml`
```toml
[Performance]
log_level_console = "DEBUG" # 改为 DEBUG
log_level_file = "DEBUG"
```
### 2. 查看详细日志
```bash
# 实时查看日志
tail -f WechatHookBot/logs/hookbot.log | grep -E "Flow2API|图像生成"
# 搜索错误
grep "Flow2API" WechatHookBot/logs/hookbot.log | grep -E "ERROR|WARNING"
```
### 3. 测试命令
```bash
# 在微信中发送
/绘画f 一只可爱的猫咪
# 或横屏
/绘画f横屏 美丽的风景
```
### 4. 检查配置
查看 `plugins/Flow2API/config.toml`
```toml
[api]
base_url = "https://your-api-url.com" # 确认 URL 正确
token = "your-token" # 确认 Token 有效
timeout = 600 # 超时时间(秒)
```
## 预期日志输出
### 成功案例:
```
INFO | Flow2API请求: gemini-3.0-pro-image-portrait, 提示词长度: 15 字符
DEBUG | 完整提示词: 一只可爱的猫咪
DEBUG | 请求URL: https://api.example.com/v1/chat/completions
DEBUG | Payload大小: 234 字节
DEBUG | 超时配置: connect=10s, read=600s
INFO | 使用 AIChat 代理: socks5://127.0.0.1:7890
DEBUG | 收到响应状态码: 200
DEBUG | 收到响应行: data: {"choices":[{"delta":{"content":"https://..."}}]}
INFO | 提取到图片URL: https://cdn.example.com/image.jpg
INFO | 图片下载成功: /path/to/flow2_20251126_114435_abc123.jpg
SUCCESS | 成功生成图像
```
### 失败但重试成功:
```
INFO | Flow2API请求: gemini-3.0-pro-image-portrait, 提示词长度: 15 字符
DEBUG | 收到响应状态码: 200
INFO | 提取到图片URL: https://cdn.example.com/expired.jpg
ERROR | 下载图片失败: Client error '404 Not Found'
WARNING | 图片下载失败,将重试 (1/3)
INFO | Flow2API请求: gemini-3.0-pro-image-portrait, 提示词长度: 15 字符
INFO | 提取到图片URL: https://cdn.example.com/new-image.jpg
INFO | 图片下载成功: /path/to/flow2_20251126_114436_def456.jpg
SUCCESS | 成功生成图像
```
### 完全失败(显示详细错误):
```
INFO | Flow2API请求: gemini-3.0-pro-image-portrait, 提示词长度: 15 字符
ERROR | 请求异常: HTTPStatusError: Client error '401 Unauthorized'
ERROR | 异常详情:
Traceback (most recent call last):
File "main.py", line 131, in generate_image
async with client.stream("POST", url, json=payload, headers=headers) as response:
...
httpx.HTTPStatusError: Client error '401 Unauthorized' for url '...'
ERROR | 提示词长度: 15 字符
ERROR | 图像生成失败
```
## 配置建议
### config.toml 推荐配置
```toml
[api]
base_url = "https://your-api-url.com"
token = "your-api-token"
timeout = 600 # 10分钟适合复杂图像生成
[generation]
default_orientation = "portrait" # 默认竖屏
max_retry_attempts = 3 # 最多重试3次
[behavior]
enable_command = true
command_keywords = ["/绘画f", "/画图f"]
enable_group = true
enable_private = true
[llm_tool]
enabled = true
tool_name = "flow2_image_generation"
tool_description = "使用Flow2 AI生成图像..."
```
## 相关文件
- `main.py` - 主要修改文件
- `config.toml` - 配置文件
- `修复说明.md` - 本文档
## 总结
修复后Flow2API 插件现在会:
1. ✅ 显示完整的错误堆栈跟踪
2. ✅ 记录详细的请求和响应信息
3. ✅ 自动处理 ReadTimeout 超时
4. ✅ 图片下载失败时自动重试
5. ✅ 提供友好的错误提示和调试信息
现在当遇到"图像生成失败"时,你可以在日志中看到具体的错误原因,便于快速定位和解决问题!