├── README.md ├── __init__.py ├── config.toml ├── main.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # 🎨 Gemini 图像生成器 (GeminiImage) 2 | 3 | > 🚀 使用 Google 最先进的 Gemini AI 模型生成和编辑精美图像! 4 | > **本插件是 [XYBotv2](https://github.com/HenryXiaoYang/XYBotv2) 的一个插件。** 5 | 6 | 7 | 8 | ## ✨ 功能特点 9 | 10 | - 🖼️ **AI 图像生成** - 从文本描述生成高质量图像 11 | - 🎭 **图像编辑** - 修改现有图像,添加新元素或改变风格 12 | - 💬 **上下文对话** - 支持多轮对话,连续优化你的创意 13 | - 🔄 **自动使用上一张图片** - 多轮对话时自动将上一次生成的图片作为下一次编辑的输入 14 | - 📝 **显示 AI 文本解释** - 展示模型对生成图片的文字解释,帮助理解创作过程 15 | - 🌐 **代理支持** - 支持 HTTP/SOCKS 代理,解决网络访问问题 16 | - 🚪 **手动结束对话** - 支持随时结束对话会话,提高资源利用效率 17 | - 🧠 **Gemini 2.0** - 使用 Google 最新的 Gemini 2.0 Flash 实验版模型 18 | - 🔄 **简单易用** - 简洁的命令,快速获得结果 19 | 20 | ## 📋 使用指南 21 | 22 | ### 图像生成命令 23 | 24 | 使用以下命令生成图像: 25 | 26 | ``` 27 | #生成图片 [你的描述] 28 | #画图 [你的描述] 29 | #图片生成 [你的描述] 30 | ``` 31 | 32 | ### 图像编辑命令 33 | 34 | 上传图片并添加以下描述: 35 | 36 | ``` 37 | #编辑图片 [编辑描述] 38 | #修改图片 [编辑描述] 39 | ``` 40 | 41 | ### 结束对话命令 42 | 43 | 使用以下任一命令结束当前对话: 44 | 45 | ``` 46 | #结束对话 47 | #退出对话 48 | #关闭对话 49 | #结束 50 | ``` 51 | 52 | ### 🔄 连续对话 53 | 54 | 启动对话后,可以直接发送新消息调整或修改,无需重复命令前缀。系统会自动使用上一次生成的图片作为输入,例如: 55 | 56 | 1. 首先发送:`#生成图片 一只猫咪` 57 | 2. 然后直接发送:`给它戴上一顶帽子`(无需命令前缀) 58 | 3. 再发送:`背景改成草地`(会基于上一张图片继续修改) 59 | 4. 最后发送:`#结束对话`(结束当前会话,释放资源) 60 | 61 | ## 💎 示例提示词 62 | 63 | ### 生成图像 64 | 65 | - `#生成图片 一只戴着太阳镜的猫咪在沙滩上冲浪` 66 | - `#画图 未来城市中的飞行汽车和高耸入云的建筑` 67 | - `#图片生成 充满星星的夜空下的湖泊,倒映着北极光` 68 | 69 | ### 编辑图像 70 | 71 | - `#编辑图片 将背景改为繁华的城市街道` 72 | - `#修改图片 给人物添加一顶帽子和一副墨镜` 73 | 74 | ## ⚙️ 配置说明 75 | 76 | 在`config.toml`中设置: 77 | 78 | ```toml 79 | [GeminiImage] 80 | # 基本配置 81 | enable = true 82 | gemini_api_key = "你的API密钥" 83 | model = "gemini-2.0-flash-exp-image-generation" 84 | 85 | # 命令配置 86 | commands = ["#生成图片", "#画图", "#图片生成"] 87 | edit_commands = ["#编辑图片", "#修改图片"] 88 | 89 | # 积分配置 90 | enable_points = true 91 | generate_image_cost = 10 92 | edit_image_cost = 15 93 | 94 | # 图片保存路径 95 | save_path = "temp" 96 | 97 | # 管理员列表(免费使用) 98 | admins = [] 99 | 100 | # 代理配置 101 | enable_proxy = false # 是否启用代理 102 | proxy_url = "http://127.0.0.1:7890" # 代理服务器地址 103 | ``` 104 | 105 | ### 代理设置说明 106 | 107 | 如果您无法直接访问 Google Gemini API,可以配置代理: 108 | 109 | 1. 将`enable_proxy`设置为`true` 110 | 2. 在`proxy_url`中设置您的代理服务器地址: 111 | - HTTP 代理格式:`http://127.0.0.1:7890` 112 | - SOCKS5 代理格式:`socks5://127.0.0.1:1080` 113 | 114 | 注意:使用代理前请确保代理服务器正常工作且能访问 Google 服务。 115 | 116 | ## 📊 积分系统 117 | 118 | - 生成图片:消耗 10 积分 119 | - 编辑图片:消耗 15 积分 120 | - 管理员可免费使用 121 | 122 | ## 📝 开发日志 123 | 124 | - v1.0.0: 初始版本发布 125 | - v1.1.0: 添加上下文对话支持 126 | - v1.2.0: 添加文本响应显示和自动使用上一张图片功能 127 | - v1.3.0: 添加代理支持,解决网络访问问题 128 | - v1.4.0: 添加手动结束对话功能 129 | 130 | ## 👨‍💻 作者 131 | 132 | **老夏的金库** ©️ 2024 133 | 134 | **给个 ⭐ Star 支持吧!** 😊 135 | 136 | **开源不易,感谢打赏支持!** 137 | 138 | ![image](https://github.com/user-attachments/assets/2dde3b46-85a1-4f22-8a54-3928ef59b85f) 139 | 140 | ## �� 许可证 141 | 142 | MIT License 143 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | [GeminiImage] 2 | # 基本配置 3 | enable = true 4 | gemini_api_key = "" # Gemini API密钥 5 | model = "gemini-2.0-flash-exp-image-generation" # 使用的模型名称 6 | 7 | # 命令配置 8 | commands = ["#生成图片", "#画图", "#图片生成"] # 生成图片的命令 9 | edit_commands = ["#编辑图片", "#修改图片"] # 编辑图片的命令 10 | exit_commands = ["#结束对话", "#退出对话", "#关闭对话", "#结束"] # 结束对话的命令 11 | 12 | # 积分系统配置 13 | enable_points = true 14 | generate_image_cost = 0 # 生成图片消耗的积分 15 | edit_image_cost = 0 # 编辑图片消耗的积分 16 | 17 | # 图片保存配置 18 | save_path = "temp" # 临时保存生成图片的路径 19 | 20 | # 超级用户设置,可免费使用 21 | admins = [] # 管理员列表 22 | 23 | # 代理配置 24 | enable_proxy = false 25 | proxy_url = "http://127.0.0.1:7890" # 代理地址,格式如:http://127.0.0.1:7890 或 socks5://127.0.0.1:1080 26 | 27 | # 群聊中继续对话的唤醒词 28 | wake_words = ["#生成图片", "#画图", "#图片生成", "#编辑图片", "#修改图片", "#继续", "#图片", "#修改"] 29 | 30 | # 机器人名称配置(用于检测@消息) 31 | robot_names = ["XYBot", "小歪", "机器人"] -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import tomllib 4 | import traceback 5 | import uuid 6 | import time 7 | import base64 8 | from io import BytesIO 9 | from pathlib import Path 10 | from typing import Dict, Any, Optional, List, Tuple 11 | from collections import defaultdict 12 | 13 | from loguru import logger 14 | import aiohttp 15 | from PIL import Image 16 | 17 | from WechatAPI import WechatAPIClient 18 | from database.XYBotDB import XYBotDB 19 | from utils.decorators import * 20 | from utils.plugin_base import PluginBase 21 | 22 | 23 | class GeminiImage(PluginBase): 24 | """基于Google Gemini的图像生成插件""" 25 | 26 | description = "基于Google Gemini的图像生成插件" 27 | author = "老夏的金库" 28 | version = "1.0.0" 29 | 30 | def __init__(self): 31 | super().__init__() 32 | 33 | try: 34 | # 读取配置 35 | config_path = os.path.join(os.path.dirname(__file__), "config.toml") 36 | with open(config_path, "rb") as f: 37 | config = tomllib.load(f) 38 | 39 | # 获取Gemini配置 40 | plugin_config = config.get("GeminiImage", {}) 41 | self.enable = plugin_config.get("enable", False) 42 | self.api_key = plugin_config.get("gemini_api_key", "") 43 | self.model = plugin_config.get("model", "gemini-2.0-flash-exp-image-generation") 44 | 45 | # 获取命令配置 46 | self.commands = plugin_config.get("commands", ["#生成图片", "#画图", "#图片生成"]) 47 | self.edit_commands = plugin_config.get("edit_commands", ["#编辑图片", "#修改图片"]) 48 | self.exit_commands = plugin_config.get("exit_commands", ["#结束对话", "#退出对话", "#关闭对话", "#结束"]) # 从配置读取结束对话命令 49 | 50 | # 获取积分配置 51 | self.enable_points = plugin_config.get("enable_points", True) 52 | self.generate_cost = plugin_config.get("generate_image_cost", 10) 53 | self.edit_cost = plugin_config.get("edit_image_cost", 15) 54 | 55 | # 获取图片保存配置 56 | self.save_path = plugin_config.get("save_path", "temp") 57 | self.save_dir = os.path.join(os.path.dirname(__file__), self.save_path) 58 | os.makedirs(self.save_dir, exist_ok=True) 59 | 60 | # 获取管理员列表 61 | self.admins = plugin_config.get("admins", []) 62 | 63 | # 获取代理配置 64 | self.enable_proxy = plugin_config.get("enable_proxy", False) 65 | self.proxy_url = plugin_config.get("proxy_url", "") 66 | 67 | # 初始化数据库 68 | self.db = XYBotDB() 69 | 70 | # 初始化会话状态,用于保存上下文 71 | self.conversations = defaultdict(list) # 用户ID -> 对话历史列表 72 | self.conversation_expiry = 600 # 会话过期时间(秒) 73 | self.conversation_timestamps = {} # 用户ID -> 最后活动时间 74 | 75 | # 存储最后一次生成的图片路径 76 | self.last_images = {} # 会话标识 -> 最后一次生成的图片路径 77 | 78 | # 全局图片缓存,用于存储最近接收到的图片 79 | # 修改为使用(聊天ID, 用户ID)作为键,以区分群聊中不同用户 80 | self.image_cache = {} # (聊天ID, 用户ID) -> {content: bytes, timestamp: float} 81 | self.image_cache_timeout = 300 # 图片缓存过期时间(秒) 82 | 83 | # 验证关键配置 84 | if not self.api_key: 85 | logger.warning("GeminiImage插件未配置API密钥") 86 | 87 | logger.info("GeminiImage插件初始化成功") 88 | if self.enable_proxy: 89 | logger.info(f"GeminiImage插件已启用代理: {self.proxy_url}") 90 | 91 | except Exception as e: 92 | logger.error(f"GeminiImage插件初始化失败: {str(e)}") 93 | logger.error(traceback.format_exc()) 94 | self.enable = False 95 | 96 | @on_text_message(priority=30) 97 | async def handle_generate_image(self, bot: WechatAPIClient, message: dict) -> bool: 98 | """处理生成图片的命令""" 99 | if not self.enable: 100 | return True # 插件未启用,继续执行后续插件 101 | 102 | content = message.get("Content", "").strip() 103 | from_wxid = message.get("FromWxid", "") 104 | sender_wxid = message.get("SenderWxid", "") 105 | 106 | # 清理过期的会话 107 | self._cleanup_expired_conversations() 108 | 109 | # 会话标识 110 | conversation_key = f"{from_wxid}_{sender_wxid}" 111 | 112 | # 检查是否是结束对话命令 113 | if content in self.exit_commands: 114 | if conversation_key in self.conversations: 115 | # 清除会话数据 116 | del self.conversations[conversation_key] 117 | if conversation_key in self.conversation_timestamps: 118 | del self.conversation_timestamps[conversation_key] 119 | if conversation_key in self.last_images: 120 | del self.last_images[conversation_key] 121 | 122 | await bot.send_at_message(from_wxid, "\n已结束Gemini图像生成对话,下次需要时请使用命令重新开始", [sender_wxid]) 123 | return False # 阻止后续插件执行 124 | else: 125 | # 没有活跃会话 126 | await bot.send_at_message(from_wxid, "\n您当前没有活跃的Gemini图像生成对话", [sender_wxid]) 127 | return False # 阻止后续插件执行 128 | 129 | # 检查是否是生成图片命令 130 | for cmd in self.commands: 131 | if content.startswith(cmd): 132 | # 提取提示词 133 | prompt = content[len(cmd):].strip() 134 | if not prompt: 135 | await bot.send_at_message(from_wxid, "\n请提供描述内容,格式:#生成图片 [描述]", [sender_wxid]) 136 | return False # 命令格式错误,阻止后续插件执行 137 | 138 | # 检查API密钥是否配置 139 | if not self.api_key: 140 | await bot.send_at_message(from_wxid, "\n请先在配置文件中设置Gemini API密钥", [sender_wxid]) 141 | return False 142 | 143 | # 检查积分 144 | if self.enable_points and sender_wxid not in self.admins: 145 | points = self.db.get_points(sender_wxid) 146 | if points < self.generate_cost: 147 | await bot.send_at_message(from_wxid, f"\n您的积分不足,生成图片需要{self.generate_cost}积分,您当前有{points}积分", [sender_wxid]) 148 | return False # 积分不足,阻止后续插件执行 149 | 150 | # 生成图片 151 | try: 152 | # 发送处理中消息 153 | await bot.send_at_message(from_wxid, "\n正在生成图片,请稍候...", [sender_wxid]) 154 | 155 | # 获取上下文历史 156 | conversation_history = self.conversations[conversation_key] 157 | 158 | # 添加用户提示到会话 159 | user_message = {"role": "user", "parts": [{"text": prompt}]} 160 | 161 | # 调用Gemini API生成图片 162 | image_data, text_response = await self._generate_image(prompt, conversation_history) 163 | 164 | if image_data: 165 | # 保存图片到本地 166 | image_path = os.path.join(self.save_dir, f"gemini_{int(time.time())}_{uuid.uuid4().hex[:8]}.png") 167 | with open(image_path, "wb") as f: 168 | f.write(image_data) 169 | 170 | # 保存最后生成的图片路径 171 | self.last_images[conversation_key] = image_path 172 | 173 | # 扣除积分 174 | if self.enable_points and sender_wxid not in self.admins: 175 | self.db.add_points(sender_wxid, -self.generate_cost) 176 | points_msg = f"已扣除{self.generate_cost}积分,当前剩余{points - self.generate_cost}积分" 177 | else: 178 | points_msg = "" 179 | 180 | # 发送文本回复(如果有) 181 | if text_response: 182 | await bot.send_text_message(from_wxid, f"{text_response}\n\n{points_msg if points_msg else ''}") 183 | else: 184 | await bot.send_text_message(from_wxid, f"图片生成成功!{points_msg if points_msg else ''}") 185 | 186 | # 发送图片 187 | with open(image_path, "rb") as f: 188 | await bot.send_image_message(from_wxid, f.read()) 189 | 190 | # 提示可以结束对话 191 | if not conversation_history: # 如果是新会话 192 | await bot.send_text_message(from_wxid, f"已开始图像对话,可以直接发消息继续修改图片。需要结束时请发送\"{self.exit_commands[0]}\"") 193 | 194 | # 更新会话历史 195 | conversation_history.append(user_message) 196 | assistant_message = { 197 | "role": "model", 198 | "parts": [ 199 | {"text": text_response if text_response else "我已生成了图片"}, 200 | {"image_url": image_path} 201 | ] 202 | } 203 | conversation_history.append(assistant_message) 204 | 205 | # 限制会话历史长度 206 | if len(conversation_history) > 10: # 保留最近5轮对话 207 | conversation_history = conversation_history[-10:] 208 | 209 | # 更新会话时间戳 210 | self.conversation_timestamps[conversation_key] = time.time() 211 | else: 212 | # 检查是否有文本响应,可能是内容被拒绝 213 | if text_response: 214 | # 内容审核拒绝的情况,翻译并转发拒绝消息给用户 215 | translated_response = self._translate_gemini_message(text_response) 216 | await bot.send_at_message(from_wxid, f"\n{translated_response}", [sender_wxid]) 217 | else: 218 | await bot.send_at_message(from_wxid, "\n图片生成失败,请稍后再试或修改提示词", [sender_wxid]) 219 | except Exception as e: 220 | logger.error(f"生成图片失败: {str(e)}") 221 | logger.error(traceback.format_exc()) 222 | await bot.send_at_message(from_wxid, f"\n生成图片失败: {str(e)}", [sender_wxid]) 223 | return False # 已处理命令,阻止后续插件执行 224 | 225 | # 检查是否是编辑图片命令(针对已保存的图片) 226 | for cmd in self.edit_commands: 227 | if content.startswith(cmd): 228 | # 提取提示词 229 | prompt = content[len(cmd):].strip() 230 | if not prompt: 231 | await bot.send_at_message(from_wxid, "\n请提供编辑描述,格式:#编辑图片 [描述]", [sender_wxid]) 232 | return False # 命令格式错误,阻止后续插件执行 233 | 234 | # 检查API密钥是否配置 235 | if not self.api_key: 236 | await bot.send_at_message(from_wxid, "\n请先在配置文件中设置Gemini API密钥", [sender_wxid]) 237 | return False 238 | 239 | # 先尝试从缓存获取最近的图片 240 | image_data = await self._get_recent_image(from_wxid, sender_wxid) 241 | if image_data: 242 | # 如果找到缓存的图片,保存到本地再处理 243 | image_path = os.path.join(self.save_dir, f"temp_{int(time.time())}_{uuid.uuid4().hex[:8]}.png") 244 | with open(image_path, "wb") as f: 245 | f.write(image_data) 246 | self.last_images[conversation_key] = image_path 247 | logger.info(f"找到最近缓存的图片,保存到:{image_path}") 248 | 249 | # 检查是否有上一次上传/生成的图片 250 | last_image_path = self.last_images.get(conversation_key) 251 | if not last_image_path or not os.path.exists(last_image_path): 252 | await bot.send_at_message(from_wxid, "\n未找到可编辑的图片,请先上传一张图片", [sender_wxid]) 253 | return False 254 | 255 | # 检查积分 256 | if self.enable_points and sender_wxid not in self.admins: 257 | points = self.db.get_points(sender_wxid) 258 | if points < self.edit_cost: 259 | await bot.send_at_message(from_wxid, f"\n您的积分不足,编辑图片需要{self.edit_cost}积分,您当前有{points}积分", [sender_wxid]) 260 | return False # 积分不足,阻止后续插件执行 261 | 262 | # 编辑图片 263 | try: 264 | # 发送处理中消息 265 | await bot.send_at_message(from_wxid, "\n正在编辑图片,请稍候...", [sender_wxid]) 266 | 267 | # 读取上一次的图片 268 | with open(last_image_path, "rb") as f: 269 | image_data = f.read() 270 | 271 | # 获取会话上下文 272 | conversation_history = self.conversations[conversation_key] 273 | 274 | # 调用Gemini API编辑图片 275 | result_image, text_response = await self._edit_image(prompt, image_data, conversation_history) 276 | 277 | if result_image: 278 | # 保存编辑后的图片 279 | edited_image_path = os.path.join(self.save_dir, f"edited_{int(time.time())}_{uuid.uuid4().hex[:8]}.png") 280 | with open(edited_image_path, "wb") as f: 281 | f.write(result_image) 282 | 283 | # 更新最后生成的图片路径 284 | self.last_images[conversation_key] = edited_image_path 285 | 286 | # 扣除积分 287 | if self.enable_points and sender_wxid not in self.admins: 288 | self.db.add_points(sender_wxid, -self.edit_cost) 289 | points_msg = f"已扣除{self.edit_cost}积分,当前剩余{points - self.edit_cost}积分" 290 | else: 291 | points_msg = "" 292 | 293 | # 发送文本回复(如果有) 294 | if text_response: 295 | await bot.send_text_message(from_wxid, f"{text_response}\n\n{points_msg if points_msg else ''}") 296 | else: 297 | await bot.send_text_message(from_wxid, f"图片编辑成功!{points_msg if points_msg else ''}") 298 | 299 | # 发送图片 300 | with open(edited_image_path, "rb") as f: 301 | await bot.send_image_message(from_wxid, f.read()) 302 | 303 | # 提示可以结束对话 304 | if not conversation_history: # 如果是新会话 305 | await bot.send_text_message(from_wxid, f"已开始图像对话,可以直接发消息继续修改图片。需要结束时请发送\"{self.exit_commands[0]}\"") 306 | 307 | # 更新会话历史 308 | user_message = { 309 | "role": "user", 310 | "parts": [ 311 | {"text": prompt}, 312 | {"image_url": last_image_path} 313 | ] 314 | } 315 | conversation_history.append(user_message) 316 | 317 | assistant_message = { 318 | "role": "model", 319 | "parts": [ 320 | {"text": text_response if text_response else "我已编辑完成图片"}, 321 | {"image_url": edited_image_path} 322 | ] 323 | } 324 | conversation_history.append(assistant_message) 325 | 326 | # 限制会话历史长度 327 | if len(conversation_history) > 10: # 保留最近5轮对话 328 | conversation_history = conversation_history[-10:] 329 | 330 | # 更新会话时间戳 331 | self.conversation_timestamps[conversation_key] = time.time() 332 | else: 333 | # 检查是否有文本响应,可能是内容被拒绝 334 | if text_response: 335 | # 内容审核拒绝的情况,翻译并转发拒绝消息给用户 336 | translated_response = self._translate_gemini_message(text_response) 337 | await bot.send_at_message(from_wxid, f"\n{translated_response}", [sender_wxid]) 338 | logger.warning(f"API拒绝编辑图片,提示: {text_response}") 339 | else: 340 | logger.error(f"编辑图片失败,未获取到有效的图片数据") 341 | await bot.send_at_message(from_wxid, "\n图片编辑失败,请稍后再试或修改描述", [sender_wxid]) 342 | except Exception as e: 343 | logger.error(f"编辑图片失败: {str(e)}") 344 | logger.error(traceback.format_exc()) 345 | await bot.send_at_message(from_wxid, f"\n编辑图片失败: {str(e)}", [sender_wxid]) 346 | return False # 已处理命令,阻止后续插件执行 347 | 348 | # 检查是否是对话继续(没有前缀命令,但有活跃会话) 349 | if conversation_key in self.conversations and content and not any(content.startswith(cmd) for cmd in self.commands + self.edit_commands): 350 | # 有活跃会话,且不是其他命令,视为继续对话 351 | try: 352 | logger.info(f"继续对话: 用户={sender_wxid}, 内容='{content}'") 353 | 354 | # 检查积分 355 | if self.enable_points and sender_wxid not in self.admins: 356 | points = self.db.get_points(sender_wxid) 357 | if points < self.generate_cost: 358 | await bot.send_at_message(from_wxid, f"\n您的积分不足,生成图片需要{self.generate_cost}积分,您当前有{points}积分", [sender_wxid]) 359 | return False # 积分不足,阻止后续插件执行 360 | 361 | # 发送处理中消息 362 | await bot.send_at_message(from_wxid, "\n正在处理您的请求,请稍候...", [sender_wxid]) 363 | 364 | # 获取上下文历史 365 | conversation_history = self.conversations[conversation_key] 366 | logger.info(f"对话历史长度: {len(conversation_history)}") 367 | 368 | # 添加用户提示到会话 369 | user_message = {"role": "user", "parts": [{"text": content}]} 370 | 371 | # 检查是否有上一次生成的图片,如果有则自动作为输入 372 | last_image_path = self.last_images.get(conversation_key) 373 | logger.info(f"上一次图片路径: {last_image_path}") 374 | 375 | if last_image_path and os.path.exists(last_image_path): 376 | logger.info(f"找到上一次图片,将使用该图片进行编辑") 377 | # 读取上一次生成的图片 378 | with open(last_image_path, "rb") as f: 379 | image_data = f.read() 380 | 381 | # 调用编辑图片API 382 | logger.info(f"调用编辑图片API") 383 | result_image, text_response = await self._edit_image(content, image_data, conversation_history) 384 | 385 | if result_image: 386 | logger.info(f"成功获取编辑后的图片结果") 387 | # 保存编辑后的图片 388 | new_image_path = os.path.join(self.save_dir, f"gemini_{int(time.time())}_{uuid.uuid4().hex[:8]}.png") 389 | with open(new_image_path, "wb") as f: 390 | f.write(result_image) 391 | 392 | # 更新最后生成的图片路径 393 | self.last_images[conversation_key] = new_image_path 394 | 395 | # 扣除积分 396 | if self.enable_points and sender_wxid not in self.admins: 397 | self.db.add_points(sender_wxid, -self.edit_cost) # 使用编辑积分 398 | points_msg = f"已扣除{self.edit_cost}积分,当前剩余{points - self.edit_cost}积分" 399 | else: 400 | points_msg = "" 401 | 402 | # 发送文本回复(如果有) 403 | if text_response: 404 | await bot.send_text_message(from_wxid, f"{text_response}\n\n{points_msg if points_msg else ''}") 405 | else: 406 | await bot.send_text_message(from_wxid, f"图片编辑成功!{points_msg if points_msg else ''}") 407 | 408 | # 发送图片 409 | logger.info(f"发送编辑后的图片") 410 | with open(new_image_path, "rb") as f: 411 | await bot.send_image_message(from_wxid, f.read()) 412 | 413 | # 更新会话历史 414 | # 添加包含图片的用户消息 415 | user_message = { 416 | "role": "user", 417 | "parts": [ 418 | {"text": content}, 419 | {"image_url": last_image_path} 420 | ] 421 | } 422 | conversation_history.append(user_message) 423 | 424 | assistant_message = { 425 | "role": "model", 426 | "parts": [ 427 | {"text": text_response if text_response else "我已编辑了图片"}, 428 | {"image_url": new_image_path} 429 | ] 430 | } 431 | conversation_history.append(assistant_message) 432 | 433 | # 限制会话历史长度 434 | if len(conversation_history) > 10: 435 | conversation_history = conversation_history[-10:] 436 | 437 | # 更新会话时间戳 438 | self.conversation_timestamps[conversation_key] = time.time() 439 | 440 | return False # 已处理命令,阻止后续插件执行 441 | else: 442 | # 检查是否有文本响应,可能是内容被拒绝 443 | if text_response: 444 | # 内容审核拒绝的情况,翻译并转发拒绝消息给用户 445 | translated_response = self._translate_gemini_message(text_response) 446 | await bot.send_at_message(from_wxid, f"\n{translated_response}", [sender_wxid]) 447 | logger.warning(f"API拒绝编辑图片,提示: {text_response}") 448 | else: 449 | logger.error(f"编辑图片失败,未获取到有效的图片数据") 450 | await bot.send_at_message(from_wxid, "\n图片编辑失败,请稍后再试或修改描述", [sender_wxid]) 451 | else: 452 | logger.info(f"没有找到上一次图片或文件不存在,将生成新图片") 453 | # 没有上一次图片,当作生成新图片处理 454 | image_data, text_response = await self._generate_image(content, conversation_history) 455 | 456 | if image_data: 457 | logger.info(f"成功获取生成的图片结果") 458 | # 保存图片到本地 459 | image_path = os.path.join(self.save_dir, f"gemini_{int(time.time())}_{uuid.uuid4().hex[:8]}.png") 460 | with open(image_path, "wb") as f: 461 | f.write(image_data) 462 | 463 | # 更新最后生成的图片路径 464 | self.last_images[conversation_key] = image_path 465 | 466 | # 扣除积分 467 | if self.enable_points and sender_wxid not in self.admins: 468 | self.db.add_points(sender_wxid, -self.generate_cost) 469 | points_msg = f"已扣除{self.generate_cost}积分,当前剩余{points - self.generate_cost}积分" 470 | else: 471 | points_msg = "" 472 | 473 | # 发送文本回复(如果有) 474 | if text_response: 475 | await bot.send_text_message(from_wxid, f"{text_response}\n\n{points_msg if points_msg else ''}") 476 | else: 477 | await bot.send_text_message(from_wxid, f"图片生成成功!{points_msg if points_msg else ''}") 478 | 479 | # 发送图片 480 | logger.info(f"发送生成的图片") 481 | with open(image_path, "rb") as f: 482 | await bot.send_image_message(from_wxid, f.read()) 483 | 484 | # 更新会话历史 485 | conversation_history.append(user_message) 486 | assistant_message = { 487 | "role": "model", 488 | "parts": [ 489 | {"text": text_response if text_response else "我已基于您的提示生成了图片"}, 490 | {"image_url": image_path} 491 | ] 492 | } 493 | conversation_history.append(assistant_message) 494 | 495 | # 限制会话历史长度 496 | if len(conversation_history) > 10: 497 | conversation_history = conversation_history[-10:] 498 | 499 | # 更新会话时间戳 500 | self.conversation_timestamps[conversation_key] = time.time() 501 | else: 502 | # 检查是否有文本响应,可能是内容被拒绝 503 | if text_response: 504 | # 内容审核拒绝的情况,翻译并转发拒绝消息给用户 505 | translated_response = self._translate_gemini_message(text_response) 506 | await bot.send_at_message(from_wxid, f"\n{translated_response}", [sender_wxid]) 507 | logger.warning(f"API拒绝生成图片,提示: {text_response}") 508 | else: 509 | logger.error(f"生成图片失败,未获取到有效的图片数据") 510 | await bot.send_at_message(from_wxid, "\n图片生成失败,请稍后再试或修改提示词", [sender_wxid]) 511 | return False 512 | except Exception as e: 513 | logger.error(f"对话继续生成图片失败: {str(e)}") 514 | logger.error(traceback.format_exc()) 515 | await bot.send_at_message(from_wxid, f"\n生成失败: {str(e)}", [sender_wxid]) 516 | return False # 已处理命令,阻止后续插件执行 517 | 518 | # 不是本插件的命令,继续执行后续插件 519 | return True 520 | 521 | @on_file_message(priority=30) 522 | async def handle_edit_image(self, bot: WechatAPIClient, message: dict) -> bool: 523 | """处理编辑图片的命令""" 524 | if not self.enable: 525 | return True # 插件未启用,继续执行后续插件 526 | 527 | from_wxid = message.get("FromWxid", "") 528 | sender_wxid = message.get("SenderWxid", "") 529 | file_info = message.get("FileInfo", {}) 530 | 531 | # 清理过期的会话 532 | self._cleanup_expired_conversations() 533 | 534 | # 会话标识 535 | conversation_key = f"{from_wxid}_{sender_wxid}" 536 | 537 | # 检查消息是否含有文件信息 538 | if not file_info or "FileID" not in file_info: 539 | return True # 不是有效的文件消息,继续执行后续插件 540 | 541 | # 检查是否是图片编辑命令 542 | if "FileSummary" in file_info: 543 | summary = file_info.get("FileSummary", "").strip() 544 | 545 | for cmd in self.edit_commands: 546 | if summary.startswith(cmd): 547 | # 提取提示词 548 | prompt = summary[len(cmd):].strip() 549 | if not prompt: 550 | await bot.send_at_message(from_wxid, "\n请提供编辑描述,格式:#编辑图片 [描述]", [sender_wxid]) 551 | return False # 命令格式错误,阻止后续插件执行 552 | 553 | # 检查API密钥是否配置 554 | if not self.api_key: 555 | await bot.send_at_message(from_wxid, "\n请先在配置文件中设置Gemini API密钥", [sender_wxid]) 556 | return False 557 | 558 | # 检查文件类型是否为图片 559 | file_name = file_info.get("FileName", "").lower() 560 | valid_extensions = [".jpg", ".jpeg", ".png", ".webp"] 561 | is_image = any(file_name.endswith(ext) for ext in valid_extensions) 562 | 563 | if not is_image: 564 | await bot.send_at_message(from_wxid, "\n请上传图片文件(支持JPG、PNG、WEBP格式)", [sender_wxid]) 565 | return False 566 | 567 | # 检查积分 568 | if self.enable_points and sender_wxid not in self.admins: 569 | points = self.db.get_points(sender_wxid) 570 | if points < self.edit_cost: 571 | await bot.send_at_message(from_wxid, f"\n您的积分不足,编辑图片需要{self.edit_cost}积分,您当前有{points}积分", [sender_wxid]) 572 | return False # 积分不足,阻止后续插件执行 573 | 574 | # 编辑图片 575 | try: 576 | # 发送处理中消息 577 | await bot.send_at_message(from_wxid, "\n正在编辑图片,请稍候...", [sender_wxid]) 578 | 579 | # 下载用户上传的图片 580 | file_id = file_info.get("FileID") 581 | file_content = await bot.download_file(file_id) 582 | 583 | # 保存原始图片 584 | orig_image_path = os.path.join(self.save_dir, f"orig_{int(time.time())}_{uuid.uuid4().hex[:8]}.png") 585 | with open(orig_image_path, "wb") as f: 586 | f.write(file_content) 587 | 588 | # 获取会话上下文 589 | conversation_history = self.conversations[conversation_key] 590 | 591 | # 调用Gemini API编辑图片 592 | image_data, text_response = await self._edit_image(prompt, file_content, conversation_history) 593 | 594 | if image_data: 595 | # 保存编辑后的图片 596 | edited_image_path = os.path.join(self.save_dir, f"edited_{int(time.time())}_{uuid.uuid4().hex[:8]}.png") 597 | with open(edited_image_path, "wb") as f: 598 | f.write(image_data) 599 | 600 | # 更新最后生成的图片路径 601 | self.last_images[conversation_key] = edited_image_path 602 | 603 | # 扣除积分 604 | if self.enable_points and sender_wxid not in self.admins: 605 | self.db.add_points(sender_wxid, -self.edit_cost) 606 | points_msg = f"已扣除{self.edit_cost}积分,当前剩余{points - self.edit_cost}积分" 607 | else: 608 | points_msg = "" 609 | 610 | # 发送文本回复(如果有) 611 | if text_response: 612 | await bot.send_text_message(from_wxid, f"{text_response}\n\n{points_msg if points_msg else ''}") 613 | else: 614 | await bot.send_text_message(from_wxid, f"图片编辑成功!{points_msg if points_msg else ''}") 615 | 616 | # 发送图片 617 | with open(edited_image_path, "rb") as f: 618 | await bot.send_image_message(from_wxid, f.read()) 619 | 620 | # 提示可以结束对话 621 | if not conversation_history: # 如果是新会话 622 | await bot.send_text_message(from_wxid, f"已开始图像对话,可以直接发消息继续修改图片。需要结束时请发送\"{self.exit_commands[0]}\"") 623 | 624 | # 更新会话历史 625 | user_message = { 626 | "role": "user", 627 | "parts": [ 628 | {"text": prompt}, 629 | {"image_url": orig_image_path} 630 | ] 631 | } 632 | conversation_history.append(user_message) 633 | 634 | assistant_message = { 635 | "role": "model", 636 | "parts": [ 637 | {"text": text_response if text_response else "我已编辑完成图片"}, 638 | {"image_url": edited_image_path} 639 | ] 640 | } 641 | conversation_history.append(assistant_message) 642 | 643 | # 限制会话历史长度 644 | if len(conversation_history) > 10: # 保留最近5轮对话 645 | conversation_history = conversation_history[-10:] 646 | 647 | # 更新会话时间戳 648 | self.conversation_timestamps[conversation_key] = time.time() 649 | else: 650 | # 检查是否有文本响应,可能是内容被拒绝 651 | if text_response: 652 | # 内容审核拒绝的情况,翻译并转发拒绝消息给用户 653 | translated_response = self._translate_gemini_message(text_response) 654 | await bot.send_at_message(from_wxid, f"\n{translated_response}", [sender_wxid]) 655 | logger.warning(f"API拒绝编辑图片,提示: {text_response}") 656 | else: 657 | logger.error(f"编辑图片失败,未获取到有效的图片数据") 658 | await bot.send_at_message(from_wxid, "\n图片编辑失败,请稍后再试或修改描述", [sender_wxid]) 659 | except Exception as e: 660 | logger.error(f"编辑图片失败: {str(e)}") 661 | logger.error(traceback.format_exc()) 662 | await bot.send_at_message(from_wxid, f"\n编辑图片失败: {str(e)}", [sender_wxid]) 663 | return False # 已处理命令,阻止后续插件执行 664 | 665 | # 不是本插件的命令,继续执行后续插件 666 | return True 667 | 668 | @on_image_message(priority=30) 669 | async def handle_image_edit(self, bot: WechatAPIClient, message: dict) -> bool: 670 | """处理图片消息,缓存图片数据以备后续编辑使用""" 671 | if not self.enable: 672 | return True # 插件未启用,继续执行后续插件 673 | 674 | from_wxid = message.get("FromWxid", "") 675 | sender_wxid = message.get("SenderWxid", "") 676 | 677 | # 在群聊中,使用发送者ID作为图片所有者 678 | # 在私聊中,FromWxid和SenderWxid相同 679 | is_group = message.get("IsGroup", False) 680 | image_owner = sender_wxid if is_group else from_wxid 681 | 682 | try: 683 | # 清理过期缓存 684 | self._cleanup_image_cache() 685 | 686 | # 提取图片数据 - 首先尝试直接从ImgBuf获取 687 | if "ImgBuf" in message and message["ImgBuf"] and len(message["ImgBuf"]) > 100: 688 | image_data = message["ImgBuf"] 689 | logger.info(f"从ImgBuf提取到图片数据,大小: {len(image_data)} 字节") 690 | 691 | # 保存图片到缓存 - 使用(聊天ID, 用户ID)作为键 692 | cache_key = (from_wxid, image_owner) 693 | self.image_cache[cache_key] = { 694 | "content": image_data, 695 | "timestamp": time.time() 696 | } 697 | return True 698 | 699 | # 如果ImgBuf中没有有效数据,尝试从Content中提取Base64图片数据 700 | content = message.get("Content", "") 701 | if content and isinstance(content, str): 702 | # 检查是否是XML格式 703 | if content.startswith("") 708 | if xml_end > 0 and len(content) > xml_end + 6: 709 | # XML后面可能有Base64数据 710 | base64_data = content[xml_end + 6:].strip() 711 | if base64_data: 712 | try: 713 | image_data = base64.b64decode(base64_data) 714 | logger.info(f"从XML后面提取到Base64数据,长度: {len(image_data)} 字节") 715 | 716 | # 保存图片到缓存 - 使用(聊天ID, 用户ID)作为键 717 | cache_key = (from_wxid, image_owner) 718 | self.image_cache[cache_key] = { 719 | "content": image_data, 720 | "timestamp": time.time() 721 | } 722 | return True 723 | except Exception as e: 724 | logger.error(f"XML后Base64解码失败: {e}") 725 | 726 | # 如果上面的方法失败,尝试直接检测任何位置的Base64图片头部标识 727 | base64_markers = ["iVBOR", "/9j/", "R0lGOD", "UklGR", "PD94bWw", "Qk0", "SUkqAA"] 728 | for marker in base64_markers: 729 | if marker in content: 730 | idx = content.find(marker) 731 | if idx > 0: 732 | try: 733 | # 可能的Base64数据,截取从标记开始到结束的部分 734 | base64_data = content[idx:] 735 | # 去除可能的非Base64字符 736 | base64_data = ''.join(c for c in base64_data if c in 737 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=') 738 | 739 | # 修正长度确保是4的倍数 740 | padding = len(base64_data) % 4 741 | if padding: 742 | base64_data += '=' * (4 - padding) 743 | 744 | # 尝试解码 745 | image_data = base64.b64decode(base64_data) 746 | if len(image_data) > 1000: # 确保至少有一些数据 747 | logger.info(f"从内容中提取到{marker}格式图片数据,长度: {len(image_data)} 字节") 748 | 749 | # 保存图片到缓存 - 使用(聊天ID, 用户ID)作为键 750 | cache_key = (from_wxid, image_owner) 751 | self.image_cache[cache_key] = { 752 | "content": image_data, 753 | "timestamp": time.time() 754 | } 755 | return True 756 | except Exception as e: 757 | logger.error(f"提取{marker}格式图片数据失败: {e}") 758 | except Exception as e: 759 | logger.error(f"提取XML中图片数据失败: {e}") 760 | 761 | # 如果前面的方法都失败了,再尝试一种方法,直接提取整个content作为可能的Base64数据 762 | # 这对于某些不标准的消息格式可能有效 763 | try: 764 | # 尝试将整个content作为Base64处理 765 | base64_content = content.replace(' ', '+') # 修复可能的URL安全编码 766 | # 修正长度确保是4的倍数 767 | padding = len(base64_content) % 4 768 | if padding: 769 | base64_content += '=' * (4 - padding) 770 | 771 | image_data = base64.b64decode(base64_content) 772 | # 如果解码成功且数据量足够大,可能是图片 773 | if len(image_data) > 10000: # 图片数据通常较大 774 | try: 775 | # 仅尝试打开,不进行验证,避免某些非标准图片格式失败 776 | with Image.open(BytesIO(image_data)) as img: 777 | width, height = img.size 778 | if width > 10 and height > 10: # 确保是有效图片 779 | logger.info(f"从内容作为Base64解码成功,图片尺寸: {width}x{height}") 780 | 781 | # 保存图片到缓存 - 使用(聊天ID, 用户ID)作为键 782 | cache_key = (from_wxid, image_owner) 783 | self.image_cache[cache_key] = { 784 | "content": image_data, 785 | "timestamp": time.time() 786 | } 787 | return True 788 | except Exception as img_e: 789 | logger.error(f"解码后数据不是有效图片: {img_e}") 790 | except Exception as e: 791 | # 解码失败不是错误,只是这种方法不适用 792 | pass 793 | 794 | logger.warning("未能从消息中提取有效的图片数据") 795 | except Exception as e: 796 | logger.error(f"处理图片消息失败: {str(e)}") 797 | logger.error(traceback.format_exc()) 798 | 799 | return True # 继续执行后续插件 800 | 801 | def _cleanup_expired_conversations(self): 802 | """清理过期的会话""" 803 | current_time = time.time() 804 | expired_keys = [] 805 | 806 | for key, timestamp in self.conversation_timestamps.items(): 807 | if current_time - timestamp > self.conversation_expiry: 808 | expired_keys.append(key) 809 | 810 | for key in expired_keys: 811 | if key in self.conversations: 812 | del self.conversations[key] 813 | if key in self.conversation_timestamps: 814 | del self.conversation_timestamps[key] 815 | if key in self.last_images: 816 | del self.last_images[key] 817 | 818 | async def _generate_image(self, prompt: str, conversation_history: List[Dict] = None) -> Tuple[Optional[bytes], Optional[str]]: 819 | """调用Gemini API生成图片,返回图片数据和文本响应""" 820 | url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent" 821 | headers = { 822 | "Content-Type": "application/json", 823 | } 824 | 825 | params = { 826 | "key": self.api_key 827 | } 828 | 829 | # 构建请求数据 830 | if conversation_history and len(conversation_history) > 0: 831 | # 有会话历史,构建上下文 832 | # 需要处理会话历史中的图片格式 833 | processed_history = [] 834 | for msg in conversation_history: 835 | # 转换角色名称,确保使用 "user" 或 "model" 836 | role = msg["role"] 837 | if role == "assistant": 838 | role = "model" 839 | 840 | processed_msg = {"role": role, "parts": []} 841 | for part in msg["parts"]: 842 | if "text" in part: 843 | processed_msg["parts"].append({"text": part["text"]}) 844 | elif "image_url" in part: 845 | # 需要读取图片并转换为inlineData格式 846 | try: 847 | with open(part["image_url"], "rb") as f: 848 | image_data = f.read() 849 | image_base64 = base64.b64encode(image_data).decode("utf-8") 850 | processed_msg["parts"].append({ 851 | "inlineData": { 852 | "mimeType": "image/png", 853 | "data": image_base64 854 | } 855 | }) 856 | except Exception as e: 857 | logger.error(f"处理历史图片失败: {e}") 858 | # 跳过这个图片 859 | processed_history.append(processed_msg) 860 | 861 | data = { 862 | "contents": processed_history + [ 863 | { 864 | "role": "user", 865 | "parts": [ 866 | { 867 | "text": prompt 868 | } 869 | ] 870 | } 871 | ], 872 | "generation_config": { 873 | "response_modalities": ["Text", "Image"] 874 | } 875 | } 876 | else: 877 | # 无会话历史,直接使用提示 878 | data = { 879 | "contents": [ 880 | { 881 | "parts": [ 882 | { 883 | "text": prompt 884 | } 885 | ] 886 | } 887 | ], 888 | "generation_config": { 889 | "response_modalities": ["Text", "Image"] 890 | } 891 | } 892 | 893 | # 创建代理配置 894 | proxy = None 895 | if self.enable_proxy and self.proxy_url: 896 | proxy = self.proxy_url 897 | 898 | try: 899 | # 创建客户端会话,设置代理(如果启用) 900 | async with aiohttp.ClientSession() as session: 901 | try: 902 | # 使用代理发送请求 903 | logger.info(f"开始调用Gemini API生成图片") 904 | async with session.post( 905 | url, 906 | headers=headers, 907 | params=params, 908 | json=data, 909 | proxy=proxy, 910 | timeout=aiohttp.ClientTimeout(total=60) # 增加超时时间到60秒 911 | ) as response: 912 | response_text = await response.text() 913 | logger.info(f"Gemini API响应状态码: {response.status}") 914 | 915 | if response.status == 200: 916 | try: 917 | result = json.loads(response_text) 918 | 919 | # 记录完整响应内容,方便调试 920 | logger.info(f"Gemini API响应内容: {response_text}") 921 | 922 | # 提取响应 923 | candidates = result.get("candidates", []) 924 | if candidates and len(candidates) > 0: 925 | content = candidates[0].get("content", {}) 926 | parts = content.get("parts", []) 927 | 928 | # 处理文本和图片响应 929 | text_response = None 930 | image_data = None 931 | 932 | for part in parts: 933 | # 处理文本部分 934 | if "text" in part and part["text"]: 935 | text_response = part["text"] 936 | 937 | # 处理图片部分 938 | if "inlineData" in part: 939 | inline_data = part.get("inlineData", {}) 940 | if inline_data and "data" in inline_data: 941 | # 返回Base64解码后的图片数据 942 | image_data = base64.b64decode(inline_data["data"]) 943 | 944 | if not image_data: 945 | logger.error(f"API响应中没有找到图片数据: {result}") 946 | 947 | return image_data, text_response 948 | 949 | logger.error(f"未找到生成的图片数据: {result}") 950 | return None, None 951 | except json.JSONDecodeError as je: 952 | logger.error(f"解析JSON响应失败: {je}") 953 | logger.error(f"响应内容: {response_text[:1000]}...") # 记录部分响应内容 954 | return None, None 955 | else: 956 | logger.error(f"Gemini API调用失败 (状态码: {response.status}): {response_text}") 957 | return None, None 958 | except aiohttp.ClientError as ce: 959 | logger.error(f"API请求客户端错误: {ce}") 960 | return None, None 961 | except Exception as e: 962 | logger.error(f"API调用异常: {str(e)}") 963 | logger.error(traceback.format_exc()) 964 | return None, None 965 | 966 | async def _edit_image(self, prompt: str, image_data: bytes, conversation_history: List[Dict] = None) -> Tuple[Optional[bytes], Optional[str]]: 967 | """调用Gemini API编辑图片,返回图片数据和文本响应""" 968 | url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent" 969 | headers = { 970 | "Content-Type": "application/json", 971 | } 972 | 973 | params = { 974 | "key": self.api_key 975 | } 976 | 977 | # 将图片数据转换为Base64编码 978 | image_base64 = base64.b64encode(image_data).decode("utf-8") 979 | 980 | # 构建请求数据 981 | if conversation_history and len(conversation_history) > 0: 982 | # 有会话历史,构建上下文 983 | # 需要处理会话历史中的图片格式 984 | processed_history = [] 985 | for msg in conversation_history: 986 | # 转换角色名称,确保使用 "user" 或 "model" 987 | role = msg["role"] 988 | if role == "assistant": 989 | role = "model" 990 | 991 | processed_msg = {"role": role, "parts": []} 992 | for part in msg["parts"]: 993 | if "text" in part: 994 | processed_msg["parts"].append({"text": part["text"]}) 995 | elif "image_url" in part: 996 | # 需要读取图片并转换为inlineData格式 997 | try: 998 | with open(part["image_url"], "rb") as f: 999 | img_data = f.read() 1000 | img_base64 = base64.b64encode(img_data).decode("utf-8") 1001 | processed_msg["parts"].append({ 1002 | "inlineData": { 1003 | "mimeType": "image/png", 1004 | "data": img_base64 1005 | } 1006 | }) 1007 | except Exception as e: 1008 | logger.error(f"处理历史图片失败: {e}") 1009 | # 跳过这个图片 1010 | processed_history.append(processed_msg) 1011 | 1012 | data = { 1013 | "contents": processed_history + [ 1014 | { 1015 | "role": "user", 1016 | "parts": [ 1017 | { 1018 | "text": prompt 1019 | }, 1020 | { 1021 | "inlineData": { 1022 | "mimeType": "image/png", 1023 | "data": image_base64 1024 | } 1025 | } 1026 | ] 1027 | } 1028 | ], 1029 | "generation_config": { 1030 | "response_modalities": ["Text", "Image"] 1031 | } 1032 | } 1033 | else: 1034 | # 无会话历史,直接使用提示和图片 1035 | data = { 1036 | "contents": [ 1037 | { 1038 | "parts": [ 1039 | { 1040 | "text": prompt 1041 | }, 1042 | { 1043 | "inlineData": { 1044 | "mimeType": "image/png", 1045 | "data": image_base64 1046 | } 1047 | } 1048 | ] 1049 | } 1050 | ], 1051 | "generation_config": { 1052 | "response_modalities": ["Text", "Image"] 1053 | } 1054 | } 1055 | 1056 | # 创建代理配置 1057 | proxy = None 1058 | if self.enable_proxy and self.proxy_url: 1059 | proxy = self.proxy_url 1060 | 1061 | try: 1062 | # 创建客户端会话,设置代理(如果启用) 1063 | async with aiohttp.ClientSession() as session: 1064 | try: 1065 | # 使用代理发送请求 1066 | logger.info(f"开始调用Gemini API编辑图片") 1067 | async with session.post( 1068 | url, 1069 | headers=headers, 1070 | params=params, 1071 | json=data, 1072 | proxy=proxy, 1073 | timeout=aiohttp.ClientTimeout(total=60) # 增加超时时间到60秒 1074 | ) as response: 1075 | response_text = await response.text() 1076 | logger.info(f"Gemini API响应状态码: {response.status}") 1077 | 1078 | if response.status == 200: 1079 | try: 1080 | result = json.loads(response_text) 1081 | 1082 | # 记录完整响应内容,方便调试 1083 | logger.info(f"Gemini API响应内容: {response_text}") 1084 | 1085 | # 提取响应 1086 | candidates = result.get("candidates", []) 1087 | if candidates and len(candidates) > 0: 1088 | content = candidates[0].get("content", {}) 1089 | parts = content.get("parts", []) 1090 | 1091 | # 处理文本和图片响应 1092 | text_response = None 1093 | image_data = None 1094 | 1095 | for part in parts: 1096 | # 处理文本部分 1097 | if "text" in part and part["text"]: 1098 | text_response = part["text"] 1099 | 1100 | # 处理图片部分 1101 | if "inlineData" in part: 1102 | inline_data = part.get("inlineData", {}) 1103 | if inline_data and "data" in inline_data: 1104 | # 返回Base64解码后的图片数据 1105 | image_data = base64.b64decode(inline_data["data"]) 1106 | 1107 | if not image_data: 1108 | logger.error(f"API响应中没有找到图片数据: {result}") 1109 | 1110 | return image_data, text_response 1111 | 1112 | logger.error(f"未找到编辑后的图片数据: {result}") 1113 | return None, None 1114 | except json.JSONDecodeError as je: 1115 | logger.error(f"解析JSON响应失败: {je}") 1116 | logger.error(f"响应内容: {response_text[:1000]}...") # 记录部分响应内容 1117 | return None, None 1118 | else: 1119 | logger.error(f"Gemini API调用失败 (状态码: {response.status}): {response_text}") 1120 | return None, None 1121 | except aiohttp.ClientError as ce: 1122 | logger.error(f"API请求客户端错误: {ce}") 1123 | return None, None 1124 | except Exception as e: 1125 | logger.error(f"API调用异常: {str(e)}") 1126 | logger.error(traceback.format_exc()) 1127 | return None, None 1128 | 1129 | def _translate_gemini_message(self, text: str) -> str: 1130 | """将Gemini API的英文消息翻译成中文""" 1131 | # 常见的内容审核拒绝消息翻译 1132 | if "I'm unable to create this image" in text: 1133 | if "sexually suggestive" in text: 1134 | return "抱歉,我无法创建这张图片。我不能生成带有性暗示或促进有害刻板印象的内容。请提供其他描述。" 1135 | elif "harmful" in text or "dangerous" in text: 1136 | return "抱歉,我无法创建这张图片。我不能生成可能有害或危险的内容。请提供其他描述。" 1137 | elif "violent" in text: 1138 | return "抱歉,我无法创建这张图片。我不能生成暴力或血腥的内容。请提供其他描述。" 1139 | else: 1140 | return "抱歉,我无法创建这张图片。请尝试修改您的描述,提供其他内容。" 1141 | 1142 | # 其他常见拒绝消息 1143 | if "cannot generate" in text or "can't generate" in text: 1144 | return "抱歉,我无法生成符合您描述的图片。请尝试其他描述。" 1145 | 1146 | if "against our content policy" in text: 1147 | return "抱歉,您的请求违反了内容政策,无法生成相关图片。请提供其他描述。" 1148 | 1149 | # 默认情况,原样返回 1150 | return text 1151 | 1152 | def _cleanup_image_cache(self): 1153 | """清理过期的图片缓存""" 1154 | current_time = time.time() 1155 | expired_keys = [] 1156 | 1157 | for key, cache_data in self.image_cache.items(): 1158 | if current_time - cache_data["timestamp"] > self.image_cache_timeout: 1159 | expired_keys.append(key) 1160 | 1161 | for key in expired_keys: 1162 | del self.image_cache[key] 1163 | 1164 | async def _get_recent_image(self, chat_id: str, user_id: str) -> Optional[bytes]: 1165 | """获取最近的图片数据,区分群聊中的不同用户""" 1166 | # 先尝试从用户专属缓存获取 1167 | cache_key = (chat_id, user_id) 1168 | if cache_key in self.image_cache: 1169 | cache_data = self.image_cache[cache_key] 1170 | if time.time() - cache_data["timestamp"] <= self.image_cache_timeout: 1171 | logger.info(f"找到用户 {user_id} 在聊天 {chat_id} 中的图片缓存") 1172 | return cache_data["content"] 1173 | 1174 | # 如果是私聊且没找到,尝试使用旧格式的键 1175 | if chat_id == user_id and chat_id in self.image_cache: 1176 | cache_data = self.image_cache[chat_id] 1177 | if time.time() - cache_data["timestamp"] <= self.image_cache_timeout: 1178 | logger.info(f"找到旧格式的图片缓存,键: {chat_id}") 1179 | return cache_data["content"] 1180 | 1181 | return None 1182 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | google-generativeai>=0.3.0 2 | Pillow>=9.0.0 3 | aiohttp>=3.8.0 --------------------------------------------------------------------------------