删了点东西

This commit is contained in:
zihanjian
2025-09-25 11:32:32 +08:00
parent 33731cb83b
commit 4419f16843
22 changed files with 278 additions and 2399 deletions

1
.gitignore vendored
View File

@@ -10,5 +10,4 @@ logs/
*.log.*
config.yaml
duel_ranks.json
data/

View File

@@ -67,7 +67,6 @@ Bubbles 是一个功能丰富的微信机器人框架,基于 [wcferry](https:/
- 支持为不同的群聊和私聊设置不同的 AI 模型和 system prompt
- OpenAI (ChatGPT)
- DeepSeek
- Gemini
#### 🛠️ 双重路由系统
- **命令路由系统**:基于正则表达式的精确匹配,高效处理特定命令

View File

@@ -3,6 +3,7 @@
# -*- coding: utf-8 -*-
import logging
from copy import deepcopy
from datetime import datetime
import time # 引入 time 模块
@@ -24,6 +25,10 @@ class DeepSeek():
self.model = conf.get("model", "deepseek-chat")
# 读取最大历史消息数配置
self.max_history_messages = conf.get("max_history_messages", 10) # 读取配置默认10条
self.strict_functions = bool(conf.get("function_call_strict", False))
if self.strict_functions and conf.get("api") is None:
# 严格模式需要 beta 端点
api = "https://api.deepseek.com/beta"
self.LOG = logging.getLogger("DeepSeek")
# 存储传入的实例和wxid
@@ -119,8 +124,44 @@ class DeepSeek():
self.LOG.error(f"发生未知错误:{str(e0)}")
return "抱歉,处理您的请求时出现了错误"
def call_with_functions(self, messages, functions, wxid):
"""Invoke DeepSeek function-calling API."""
try:
tools = []
for fn in functions:
payload = deepcopy(fn)
tool_entry = {
"type": "function",
"function": {
"name": payload.get("name"),
"description": payload.get("description", ""),
"parameters": payload.get("parameters", {}),
},
}
if self.strict_functions:
tool_entry["function"]["strict"] = True
tools.append(tool_entry)
params = {
"model": self.model,
"messages": messages,
"tools": tools,
"tool_choice": "auto",
"stream": False,
}
response = self.client.chat.completions.create(**params)
return response
except (APIConnectionError, APIError, AuthenticationError) as exc:
self.LOG.error(f"DeepSeek 函数调用失败: {exc}")
raise
except Exception as exc: # pragma: no cover - 兜底保护
self.LOG.error(f"调用 DeepSeek 函数接口时发生未知错误: {exc}", exc_info=True)
raise
if __name__ == "__main__":
# --- 测试代码需要调整 ---
print("请注意:直接运行此文件进行测试需要模拟 MessageSummary 并提供 bot_wxid。")
pass
pass

View File

@@ -1,398 +0,0 @@
# ai_providers/ai_gemini.py
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import os
import time
import httpx
import pathlib # 用于处理文件路径
import mimetypes # 用于猜测图片类型
import google.generativeai as genai
from google.generativeai.types import generation_types, safety_types # 显式导入需要的类型
from google.api_core.exceptions import GoogleAPICallError, ClientError
# 引入 MessageSummary 类型提示
try:
from function.func_summary import MessageSummary
except ImportError:
MessageSummary = object # Fallback
class Gemini:
DEFAULT_MODEL = "gemini-1.5-pro-latest"
DEFAULT_PROMPT = "You are a helpful assistant."
DEFAULT_MAX_HISTORY = 15
SAFETY_SETTINGS = { # 默认安全设置 - 可根据需要调整或从配置加载
safety_types.HarmCategory.HARM_CATEGORY_HARASSMENT: safety_types.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
safety_types.HarmCategory.HARM_CATEGORY_HATE_SPEECH: safety_types.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
safety_types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: safety_types.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
safety_types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: safety_types.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
}
def __init__(self, conf: dict, message_summary_instance: MessageSummary = None, bot_wxid: str = None) -> None:
self.LOG = logging.getLogger("Gemini")
self._api_key = conf.get("api_key")
self._model_name = conf.get("model_name", self.DEFAULT_MODEL)
# 存储原始配置的 prompt用于初始化和可能的重载
self._base_prompt = conf.get("prompt", self.DEFAULT_PROMPT)
self._proxy = conf.get("proxy")
self.max_history_messages = conf.get("max_history_messages", self.DEFAULT_MAX_HISTORY)
self.message_summary = message_summary_instance
self.bot_wxid = bot_wxid
self._model = None
self.support_vision = False # 初始化时假设不支持,成功加载模型后再判断
if not self._api_key:
self.LOG.error("Gemini API Key 未在配置中提供!")
return # 没有 API Key 无法继续
if not self.message_summary:
self.LOG.warning("MessageSummary 实例未提供给 Gemini上下文功能将不可用")
if not self.bot_wxid:
self.LOG.warning("bot_wxid 未提供给 Gemini可能无法正确识别机器人自身消息")
try:
# 1. 配置代理 (如果提供)
transport = None
if self._proxy:
try:
transport = httpx.HTTPTransport(proxy=self._proxy)
self.LOG.info(f"Gemini 使用代理: {self._proxy}")
except Exception as proxy_err:
self.LOG.error(f"配置 Gemini 代理失败: {proxy_err}", exc_info=True)
# 代理配置失败,可以选择不使用代理继续或直接失败
# 这里选择继续,不使用代理
transport = None
# 2. 配置 Google AI Client
genai.configure(api_key=self._api_key, transport=transport)
# 3. 初始化模型
# 将基础 prompt 作为 system_instruction 传递
self._model = genai.GenerativeModel(
self._model_name,
system_instruction=self._base_prompt,
safety_settings=self.SAFETY_SETTINGS # 应用安全设置
)
self.LOG.info(f"已加载 Gemini 模型")
# 4. 检查视觉能力 (依赖模型名称的简单检查)
# 注意:更可靠的方式是调用 list_models 并检查支持的方法
if "vision" in self._model_name or "pro" in self._model_name or "gemini-1.5" in self._model_name or "flash" in self._model_name:
self.support_vision = True
self.LOG.info(f"模型 {self._model_name} 被认为支持视觉能力。")
else:
self.LOG.info(f"模型 {self._model_name} 根据名称判断可能不支持视觉能力。")
except (GoogleAPICallError, ClientError) as api_error:
self.LOG.error(f"初始化 Gemini 时发生 API 错误: {api_error}", exc_info=True)
self._model = None
except Exception as e:
self.LOG.error(f"初始化 Gemini 时发生未知错误: {e}", exc_info=True)
self._model = None
def __repr__(self):
return f'Gemini(model={self._model_name}, initialized={self._model is not None})'
@staticmethod
def value_check(conf: dict) -> bool:
# 只需要 API Key 是必须的
return bool(conf and conf.get("api_key"))
def _format_history(self, history: list) -> list:
"""将数据库历史消息转换为 Gemini API 的 contents 格式"""
contents = []
for msg in history:
role = "model" if msg.get("sender_wxid") == self.bot_wxid else "user"
content = msg.get('content', '')
sender_name = msg.get('sender', '未知用户') # 获取发送者名称
if content: # 避免添加空内容
# Gemini 推荐 user role 包含发言者信息model role 不需要
if role == "user":
formatted_content = f"[{sender_name}]: {content}" # 添加发送者标记
contents.append({'role': role, 'parts': [{'text': formatted_content}]})
else: # role == "model"
contents.append({'role': role, 'parts': [{'text': content}]})
return contents
def _generate_response(self, contents: list, generation_config_override: generation_types.GenerationConfig | None = None) -> str:
"""内部方法,用于调用 Gemini API 并处理响应"""
if not self._model:
return "Gemini 模型未成功初始化,请检查配置和网络。"
# 配置生成参数 (可以从 config 中读取更多参数)
# 默认使用适中的温度,可以根据需要调整
default_config = generation_types.GenerationConfig(temperature=0.7)
config_to_use = generation_config_override if generation_config_override else default_config
self.LOG.debug(f"发送给 Gemini API 的内容条数: {len(contents)}")
# self.LOG.debug(f"使用的 GenerationConfig: {config_to_use}")
# self.LOG.debug(f"发送内容详情: {contents}") # DEBUG: 打印发送内容
rsp_text = ""
try:
# 调用 API
response = self._model.generate_content(
contents=contents,
generation_config=config_to_use,
# safety_settings=... # 如果需要覆盖初始化时的安全设置
stream=False # 非流式响应
)
# 1. 检查 Prompt 是否被阻止 (在获取 candidates 之前)
if response.prompt_feedback and response.prompt_feedback.block_reason:
reason = response.prompt_feedback.block_reason.name
self.LOG.warning(f"Gemini 提示被阻止,原因: {reason}")
return f"抱歉,您的请求因包含不适内容而被阻止 (原因: {reason})。"
# 2. 检查 Candidates 和 Finish Reason
# 尝试从 response 中安全地提取文本
try:
rsp_text = response.text # .text 是获取聚合文本的便捷方式
except ValueError:
# 如果 .text 不可用 (例如,因为 finish_reason 不是 STOP 或 MAX_TOKENS)
# 检查具体的 finish_reason
if response.candidates:
candidate = response.candidates[0]
finish_reason = candidate.finish_reason # 类型是 FinishReason 枚举
finish_reason_name = finish_reason.name
if finish_reason == generation_types.FinishReason.SAFETY:
self.LOG.warning(f"Gemini 响应被安全策略阻止。")
# 可以尝试查看 safety_ratings 获取更详细信息
ratings_info = getattr(candidate, 'safety_ratings', [])
self.LOG.debug(f"Safety Ratings: {ratings_info}")
return "抱歉,生成的响应可能包含不安全内容,已被阻止。"
elif finish_reason == generation_types.FinishReason.RECITATION:
self.LOG.warning(f"Gemini 响应因引用保护被阻止。")
return "抱歉,回答可能包含受版权保护的内容,已被部分阻止。"
elif finish_reason == generation_types.FinishReason.OTHER:
self.LOG.warning(f"Gemini 响应因未知原因停止 (FinishReason: OTHER)")
return "抱歉,生成响应时遇到未知问题。"
else:
# 包括 MAX_TOKENS, STOP 等预期情况,但没有文本
self.LOG.warning(f"Gemini 未返回文本内容,但完成原因可接受: {finish_reason_name}")
return f"生成内容时遇到问题 (完成原因: {finish_reason_name})"
else:
# 没有 candidates也没有 prompt block未知情况
self.LOG.error("Gemini API 调用成功但未返回任何候选内容或提示反馈。")
return "抱歉Gemini未能生成响应。"
except (GoogleAPICallError, ClientError) as api_error:
self.LOG.error(f"Gemini API 调用错误:{api_error}", exc_info=True)
if "API key not valid" in str(api_error):
return "Gemini API 密钥无效或已过期。"
elif "quota" in str(api_error).lower():
return "Gemini API 调用已达配额限制。"
elif "Model not found" in str(api_error):
return f"配置的 Gemini 模型 '{self._model_name}' 未找到或不可用。"
elif "Resource has been exhausted" in str(api_error):
return "Gemini API 资源耗尽,请稍后再试或检查配额。"
else:
return f"与 Gemini 通信时出错: {type(api_error).__name__}"
except generation_types.StopCandidateException as sce: # 明确捕获这个
self.LOG.error(f"Gemini API 响应被停止 (StopCandidateException): {sce}", exc_info=True)
# 通常在流式处理中遇到,但也可能在非流式中因某些原因触发
return "抱歉Gemini 生成的响应被意外停止了。"
# BlockedPromptException 似乎不直接抛出,而是通过 prompt_feedback 反馈
# except generation_types.BlockedPromptException as bpe:
# self.LOG.error(f"Gemini API 提示被阻止:{bpe}", exc_info=True)
# return "抱歉,您的请求内容被 Gemini 阻止了。"
except Exception as e:
self.LOG.error(f"调用 Gemini 时发生未知错误: {e}", exc_info=True)
return f"处理您的请求时发生未知错误: {type(e).__name__}"
return rsp_text.strip()
def get_answer(self, question: str, wxid: str, system_prompt_override=None, specific_max_history=None) -> str:
if not self._model:
return "Gemini 模型未成功初始化,请检查配置和网络。"
if not question:
self.LOG.warning(f"尝试为 wxid={wxid} 获取答案,但问题为空。")
return "您没有提问哦。"
# 1. 准备历史消息
contents = []
if self.message_summary and self.bot_wxid:
history = self.message_summary.get_messages(wxid)
limit = specific_max_history if specific_max_history is not None else self.max_history_messages
self.LOG.debug(f"获取 Gemini 历史 for {wxid}, 原始条数: {len(history)}, 使用限制: {limit}")
if limit > 0:
history = history[-limit:]
elif limit == 0:
history = [] # 明确清空历史
self.LOG.debug(f"应用限制后 Gemini 历史条数: {len(history)}")
contents.extend(self._format_history(history))
else:
self.LOG.warning(f"无法为 wxid={wxid} 获取 Gemini 历史记录。")
# 2. 添加当前用户问题
# 注意:格式化时已包含发送者信息
contents.append({'role': 'user', 'parts': [{'text': question}]})
# 3. 处理 System Prompt Override (如果提供)
# 注意Gemini API 目前不直接支持在 generate_content 中覆盖 system_instruction
# 如果需要动态改变系统提示,通常需要重新初始化模型或在用户消息前插入一条 'user' role 的指令
# 这里我们暂时忽略 system_prompt_override因为标准 API 调用不支持
if system_prompt_override:
self.LOG.warning("Gemini API 当前不支持单次请求覆盖系统提示,将使用初始化时的提示。")
# 可以考虑在这里将 override 的内容作为一条 user message 添加到 contents 开头
# 例如: contents.insert(0, {'role': 'user', 'parts': [{'text': f"[System Instruction Override]: {system_prompt_override}"}]})
# 但这会影响对话历史的结构,需要谨慎使用
# 4. 添加当前时间信息(可选,作为用户消息的一部分)
now_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
# 可以将时间信息添加到最近的用户消息中,或作为一条新的 user message
# 为了简单,暂不自动添加时间信息到内容中,如果需要,可以在 prompt 中说明
# 5. 调用内部生成方法
return self._generate_response(contents)
def get_image_description(self, image_path: str, prompt: str = "请详细描述这张图片中的内容") -> str:
if not self._model:
return "Gemini 模型未初始化。"
if not self.support_vision:
return f"当前 Gemini 模型 '{self._model_name}' 不支持图片理解。"
image_path_obj = pathlib.Path(image_path)
if not image_path_obj.is_file():
self.LOG.error(f"图片文件不存在或不是文件: {image_path}")
return "无法读取图片文件"
try:
# 猜测 MIME 类型
mime_type, _ = mimetypes.guess_type(image_path_obj)
if not mime_type or not mime_type.startswith("image/"):
self.LOG.warning(f"无法确定图片 MIME 类型或类型不是 image/*: {image_path}, 猜测为 jpeg")
mime_type = "image/jpeg" # 使用默认值
self.LOG.info(f"使用 Gemini 分析图片: {image_path} (MIME: {mime_type})")
# 使用 pathlib 生成 file URI
image_uri = image_path_obj.absolute().as_uri()
image_part = {'mime_type': mime_type, 'data': image_path_obj.read_bytes()}
# 构建包含文本提示和图片的消息
contents = [
# Gemini 处理多模态输入时,推荐 prompt 和 image 都在 user role 的 parts 里
{'role': 'user', 'parts': [
{'text': prompt},
image_part
# 或者使用 from_uri (如果 API 支持且网络可访问该文件 URI):
# genai.types.Part.from_uri(mime_type=mime_type, uri=image_uri)
# 使用原始字节通常更可靠
]}
]
# 可以为图片分析设置不同的生成参数,例如更低的温度以获得更客观的描述
image_gen_config = generation_types.GenerationConfig(temperature=0.4)
# 调用内部生成方法
return self._generate_response(contents, generation_config_override=image_gen_config)
except FileNotFoundError:
self.LOG.error(f"读取图片文件时发生 FileNotFoundError: {image_path}", exc_info=True)
return "读取图片文件时出错。"
except Exception as e:
self.LOG.error(f"使用 Gemini 分析图片时发生未知错误: {e}", exc_info=True)
return f"分析图片时发生未知错误: {type(e).__name__}"
# --- Main 测试部分 ---
if __name__ == "__main__":
print("--- 运行 Gemini 本地测试 ---")
# 配置日志记录
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# --- 配置加载 ---
# !!! 强烈建议从环境变量或安全的配置文件加载 API Key !!!
# 例如: api_key = os.environ.get("GEMINI_API_KEY")
# 不要将 API Key 硬编码在代码中提交
api_key_from_env = os.environ.get("GEMINI_API_KEY")
proxy_from_env = os.environ.get("HTTP_PROXY") # 支持 http/https 代理
if not api_key_from_env:
print("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
print("!!! 警告:环境变量 GEMINI_API_KEY 未设置。请设置该变量。 !!!")
print("!!! 测试将无法连接到 Gemini API。 !!!")
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")
# 可以选择退出或继续(如果只想测试初始化逻辑)
# exit(1)
api_key_to_use = "DUMMY_KEY_FOR_INIT_TEST" # 仅用于测试初始化日志,无法实际调用
else:
api_key_to_use = api_key_from_env
mock_config = {
"api_key": api_key_to_use,
"model_name": "gemini-1.5-flash-latest", # 使用较快的模型测试
"prompt": "你是一个风趣幽默的AI助手擅长讲冷笑话。",
"proxy": proxy_from_env,
"max_history_messages": 3 # 测试时减少历史记录
}
print(f"测试配置: Model={mock_config['model_name']}, Proxy={'已设置' if mock_config['proxy'] else '未设置'}")
# --- 初始化 Gemini ---
# 在测试中不依赖 MessageSummary
print("\n--- 初始化 Gemini 实例 ---")
gemini_assistant = Gemini(mock_config, bot_wxid="test_bot_wxid") # 提供一个测试 bot_wxid
# --- 测试文本生成 ---
if gemini_assistant._model: # 检查模型是否成功初始化
print("\n--- 测试文本生成 (get_answer) ---")
test_question = "你好!今天天气怎么样?给我讲个关于程序员的冷笑话吧。"
print(f"提问: {test_question}")
start_time = time.time()
answer = gemini_assistant.get_answer(test_question, "test_user_wxid") # 提供测试 wxid
end_time = time.time()
print(f"\nGemini 回答 (耗时: {end_time - start_time:.2f}s):\n{answer}")
# 测试空问题
print("\n--- 测试空问题 ---")
empty_answer = gemini_assistant.get_answer("", "test_user_wxid")
print(f"空问题回答: {empty_answer}")
# 测试长对话历史(如果需要,可以手动构建一个 contents 列表来模拟)
# print("\n--- 模拟长对话测试 ---")
# mock_history = [
# {'role': 'user', 'parts': [{'text': "[UserA]: 第一次提问"}]},
# {'role': 'model', 'parts': [{'text': "第一次回答"}]},
# {'role': 'user', 'parts': [{'text': "[UserB]: 第二次提问"}]},
# {'role': 'model', 'parts': [{'text': "第二次回答"}]},
# {'role': 'user', 'parts': [{'text': "[UserA]: 第三次提问,关于第一次提问的内容"}]},
# ]
# mock_history.append({'role': 'user', 'parts': [{'text': "当前的第四个问题"}]})
# long_hist_answer = gemini_assistant._generate_response(mock_history)
# print(f"长历史回答:\n{long_hist_answer}")
else:
print("\n--- Gemini 初始化失败,跳过文本生成测试 ---")
# --- 测试图片描述 (可选) ---
if gemini_assistant._model and gemini_assistant.support_vision:
print("\n--- 测试图片描述 (get_image_description) ---")
# 将 'path/to/your/test_image.jpg' 替换为实际的图片路径
image_test_path_str = "test_image.jpg" # 假设图片在脚本同目录下
image_test_path = pathlib.Path(image_test_path_str)
if image_test_path.exists():
desc_prompt = "详细描述这张图片里的所有元素和场景氛围。"
print(f"图片路径: {image_test_path.absolute()}")
print(f"描述提示: {desc_prompt}")
start_time = time.time()
description = gemini_assistant.get_image_description(str(image_test_path), desc_prompt)
end_time = time.time()
print(f"\n图片描述 (耗时: {end_time - start_time:.2f}s):\n{description}")
else:
print(f"\n跳过图片测试,测试图片文件未找到: {image_test_path.absolute()}")
print("请将一张名为 test_image.jpg 的图片放在脚本相同目录下进行测试。")
elif gemini_assistant._model:
print(f"\n--- 跳过图片测试,当前模型 {gemini_assistant._model_name} 不支持视觉 ---")
else:
print("\n--- Gemini 初始化失败,跳过图片描述测试 ---")
print("\n--- Gemini 本地测试结束 ---")

View File

@@ -4,8 +4,6 @@ from typing import Optional, Match, Dict, Any
import json # 确保已导入json
from datetime import datetime # 确保已导入datetime
import os # 导入os模块用于文件路径操作
# from function.func_duel import DuelRankSystem
# 前向引用避免循环导入
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@@ -26,14 +24,6 @@ def handle_help(ctx: 'MessageContext', match: Optional[Match]) -> bool:
"- 新闻",
"- ask [问题]",
"",
"【决斗 & 偷袭】",
"- 决斗@XX",
"- 偷袭@XX",
"- 决斗排行/排行榜",
"- 我的战绩/决斗战绩",
"- 我的装备/查看装备",
"- 改名 [旧名] [新名]",
"",
"【提醒】",
"- 提醒xxxxx一次性、每日、每周",
"- 查看提醒/我的提醒/提醒列表",
@@ -1072,4 +1062,4 @@ def handle_weather_forecast(ctx: 'MessageContext', match: Optional[Match]) -> bo
ctx.logger.error(f"获取城市 {city_name}({city_code}) 天气预报时出错: {e}", exc_info=True)
ctx.send_text(f"😥 获取 {city_name} 天气预报时遇到问题,请稍后再试。")
return True
return True

View File

@@ -107,13 +107,6 @@ deepseek: # -----deepseek配置这行不填-----
show_reasoning: false # 是否在回复中显示思维过程,仅在启用思维链功能时有效
max_history_messages: 10 # <--- 添加这一行,设置 DeepSeek 最多回顾 10 条历史消息
gemini: # -----gemini配置-----
api_key: "YOUR_GOOGLE_API_KEY" # 必须
model_name: "gemini-1.5-pro-latest" # 可选, 默认是 "gemini-1.5-pro-latest"
prompt: "你是一个AI助手请用通俗易懂的语言回答用户的问题。" # 可选
proxy: "http://127.0.0.1:7890" # 可选, 代理地址
max_history_messages: 20 # 可选, 对话历史长度
aliyun_image: # -----如果要使用阿里云文生图,取消下面的注释并填写相关内容,模型到阿里云百炼找通义万相-文生图2.1-Turbo-----
enable: true # 是否启用阿里文生图功能false为关闭默认开启如果未配置则会将消息发送给聊天大模型
api_key: sk-xxxxxxxxxxxxxxxxxxxxxxxx # 替换为你的DashScope API密钥
@@ -124,15 +117,6 @@ aliyun_image: # -----如果要使用阿里云文生图,取消下面的注释
trigger_keyword: 牛阿里 # 触发词,默认为"牛阿里"
fallback_to_chat: true # 当服务不可用时是否转发给聊天模型处理
gemini_image: # -----谷歌AI画图配置这行不填-----
enable: true # 是否启用谷歌AI画图功能
api_key: # 谷歌Gemini API密钥必填
model: gemini-2.0-flash-exp-image-generation # 模型名称,建议保持默认,只有这一个模型可以进行绘画
temp_dir: ./geminiimg # 图片保存目录,可选
trigger_keyword: 牛谷歌 # 触发词,默认为"牛谷歌"
fallback_to_chat: false # 未启用时是否回退到聊天模式
proxy: http://127.0.0.1:7890 # 使用Clash代理格式为http://域名或者IP地址:端口号
perplexity: # -----perplexity配置这行不填-----
key: # 填写你的Perplexity API Key
api: https://api.perplexity.ai # API地址

View File

@@ -40,7 +40,5 @@ class Config(object):
self.DEEPSEEK = yconfig.get("deepseek", {})
self.PERPLEXITY = yconfig.get("perplexity", {})
self.ALIYUN_IMAGE = yconfig.get("aliyun_image", {})
self.GEMINI_IMAGE = yconfig.get("gemini_image", {})
self.GEMINI = yconfig.get("gemini", {})
self.MAX_HISTORY = yconfig.get("MAX_HISTORY", 300)
self.SEND_RATE_LIMIT = yconfig.get("send_rate_limit", 0)

View File

@@ -6,17 +6,15 @@ class ChatType(IntEnum):
# UnKnown = 0 # 未知, 即未设置
CHATGPT = 1 # ChatGPT
DEEPSEEK = 2 # DeepSeek
GEMINI = 3 # Gemini
PERPLEXITY = 4 # Perplexity
PERPLEXITY = 3 # Perplexity
@staticmethod
def is_in_chat_types(chat_type: int) -> bool:
if chat_type in [ChatType.CHATGPT.value,
ChatType.DEEPSEEK.value,
ChatType.PERPLEXITY.value,
ChatType.GEMINI.value]:
return True
return False
return chat_type in {
ChatType.CHATGPT.value,
ChatType.DEEPSEEK.value,
ChatType.PERPLEXITY.value,
}
@staticmethod
def help_hint() -> str:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
# Function Call Agent 消息流程
## 概览
- 所有传入的微信消息都会先由机器人入口处理,并在路由前写入消息记录(`robot.py:190-274`)。
- 使用装饰器注册的函数处理器在运行时组成结构化的注册表,供路由器查询(`function_calls/init_handlers.py:1-9`, `function_calls/handlers.py:37-180`)。
- 路由器优先采用确定性的命令解析,只有在必要时才升级到 LLM 编排循环(`function_calls/router.py:81-200`)。
- LLM 协调器同时支持原生函数调用模型和基于提示词的回退模式,统一返回 `LLMRunResult``function_calls/llm.py:33-186`)。
- 结构化处理器返回 `FunctionResult`,路由器可以直接发送回复或将 JSON 结果反馈给 LLM`function_calls/spec.py:12-36`)。
## 流程图
```mermaid
flowchart TD
A[来自 Wcf 的 WxMsg] --> B[Robot.processMsg]
B --> C[MessageSummary 记录消息]
B --> D[_select_model_for_message]
B --> E[_get_specific_history_limit]
B --> F[preprocess → MessageContext]
F --> G{FunctionCallRouter.dispatch}
G -->|直接匹配| H[_check_scope & require_at]
H --> I[_extract_arguments]
I --> J[_invoke_function]
J --> K{FunctionResult.handled?}
K -->|是| L[FunctionResult.dispatch → ctx.send_text]
K -->|否| M[升级到 LLM]
G -->|无直接匹配| M
M --> N[FunctionCallLLM.run]
N --> O{模型是否支持 call_with_functions?}
O -->|是| P[原生工具循环]
P --> Q[调用 call_with_functions]
Q --> R{是否返回 tool call?}
R -->|是| S[_invoke_function → FunctionResult]
S --> T[formatter → 追加工具 JSON]
T --> P
R -->|否| U[模型最终回答]
U --> V[ctx.send_text 最终回复]
P --> W[达到最大轮次]
W --> X[handled = False]
O -->|否| Y[提示词回退]
Y --> Z[get_answer + 解析 JSON]
Z --> AA{action_type == "function"?}
AA -->|是| S
AA -->|否| X
X --> AB[路由返回 False]
AB --> AC[回退:闲聊/帮助路径]
```
## 分步说明
### 1. 消息接入(`robot.py:190-274`
- 每条消息都会通过 `MessageSummary.process_message_from_wxmsg` 持久化,方便后续上下文检索。
- 机器人依据群聊/私聊映射与历史限制选择 AI 模型,再构建包含发送者信息与清洗文本的 `MessageContext`
- 在进入路由前Context 会注入当前 `chat` 模型及会话级的历史上限。
### 2. 函数注册(`function_calls/init_handlers.py:1-9`
- 机器人启动时导入 `function_calls.init_handlers`,所有 `@tool_function` 装饰器即被执行。
- 每个处理器声明名称、描述、JSON Schema、作用域以及是否需要 @,注册表因此具备自描述能力。
### 3. 直接命令快路(`function_calls/router.py:81-175`
- 路由器先对 `ctx.text` 归一化,再走 `_try_direct_command_match` 匹配已知关键词。
- 作用域、@ 以及(待实现的)权限检查会阻止不符合条件的调用。参数会按 JSON Schema 校验,避免脏数据。
- 命中后处理器立即执行,`FunctionResult.dispatch` 会直接向聊天对象推送,无需经过模型。
### 4. LLM 编排(`function_calls/llm.py:33-186`
- 若无直接命中,`FunctionCallLLM.run` 会判断当前模型是否支持 OpenAI 风格的工具调用。
- **原生循环**:协调器不断发送最新对话,执行指定工具,并把结构化 JSON 响应回灌给模型,直到拿到最终回复或达到轮次上限。
- **提示词回退**:不支持原生工具的模型会收到列出所有函数的 system prompt必须返回 JSON 决策供路由器执行。
- 两种路径最终都返回 `LLMRunResult`,路由器据此决定是否直接回复或继续走其他兜底逻辑。
### 5. 处理器执行(`function_calls/handlers.py:37-180`
- 处理器依赖 `function_calls/services` 中的业务封装,统一返回 `FunctionResult`
- 群聊场景会在 `at` 字段写入 `ctx.msg.sender`,确保回复时点名原始请求者。
### 6. 兜底逻辑(`robot.py:229-273`
- 当路由返回未处理状态时,机器人会回退到旧流程:自动通过好友请求、发送欢迎消息或调用 `handle_chitchat`
- 即使 Function Call 路由失败,整体对话体验依旧有保障。
## 优势
- 对已知命令走直连路径,既避免额外的模型耗时,又能通过 JSON Schema 保证参数质量(`function_calls/router.py:103-175`)。
- LLM 协调器清晰区分原生工具与提示词回退,后续替换模型时无需大改(`function_calls/llm.py:33-186`)。
- `FunctionResult` 既可直接回复,也能作为工具输出反馈给模型,减少重复实现(`function_calls/spec.py:12-36`)。
## 仍需关注的点
- 进入 LLM 流程后,工具输出依赖模型二次组织文本;关键函数可考虑直接派发 `FunctionResult`,避免模型返回空字符串时用户无感知(`function_calls/llm.py:83-136`)。
- 天气命令的直接路径默认关键词与城市之间存在空格;若要支持“天气北京”这类写法,需要放宽解析逻辑(`function_calls/router.py:148-156`)。
- 权限检查字段(`spec.auth`)仍是占位符,新增高权限工具前需补齐校验实现(`function_calls/router.py:35-38`)。
---
为快速理解全新的 Function Call Agent 流程而生成。

View File

@@ -0,0 +1,105 @@
# Function Call 参数调用参考
## 调用流程概览
- Function Call 路由器会将所有消息交给 `FunctionCallLLM.run`,模型基于已注册的函数规格选择要调用的工具(`function_calls/router.py:47`)。
- 每个处理器都通过 `@tool_function` 装饰器注册,它会读取参数类型注解并生成 JSON Schema用于 LLM 约束和运行期验证(`function_calls/handlers.py:24`, `function_calls/registry.py:64`)。
- LLM 返回的参数会在 `_create_args_instance` 中转为对应的 Pydantic 模型;若字段缺失或类型不符则会抛错并被记录(`function_calls/router.py:147`)。
下表总结了当前仍启用的 5 个函数及其参数 Schema 与业务用途。
所有关键字段已通过 Pydantic `Field(..., description=...)` 写入属性说明LLM 在工具定义中可以读取这些 `description` 以理解含义。
## 函数明细
### reminder_set
- **Handler**`function_calls/handlers.py:24`
- **模型**`ReminderArgs``function_calls/models.py:19`
- **Schema 摘要**
```json
{
"type": "object",
"required": ["type", "time", "content"],
"properties": {
"type": {
"type": "string",
"enum": ["once", "daily", "weekly"],
"description": "提醒类型once=一次性提醒daily=每天weekly=每周"
},
"time": {
"type": "string",
"description": "提醒时间。once 使用 'YYYY-MM-DD HH:MM'daily/weekly 使用 'HH:MM'"
},
"content": {
"type": "string",
"description": "提醒内容,将直接发送给用户"
},
"weekday": {
"type": ["integer", "null"],
"default": null,
"description": "当 type=weekly 时的星期索引0=周一 … 6=周日"
}
}
}
```
- **参数含义**
- `type`:提醒频率。一次性(`once`)用于绝对时间点;`daily` 为每天定时;`weekly` 需要同时给出星期索引。
- `time`:触发时间。一次性提醒要求未来时间,格式 `YYYY-MM-DD HH:MM`;每日/每周提醒使用 `HH:MM` 24 小时制。
- `content`:提醒内容文本,最终会被发送给触发对象。
- `weekday`:仅在 `type=weekly` 时有效,使用 0-6 表示周一到周日。
- **调用方式**:处理器从 `args.model_dump()` 得到完整参数字典并传给提醒服务的 `create_reminder``function_calls/handlers.py:37`)。`weekday` 仅在 `type="weekly"` 时使用;未提供值时默认为 `null`。
### reminder_list
- **Handler**`function_calls/handlers.py:46`
- **模型**`ReminderListArgs``function_calls/models.py:28`
- **Schema 摘要**:空对象,没有任何必填字段。
- **调用方式**:函数只验证请求体为空对象,然后调用 `list_reminders` 读取当前用户/群的提醒列表(`function_calls/handlers.py:59`)。
### reminder_delete
- **Handler**`function_calls/handlers.py:63`
- **模型**`ReminderDeleteArgs``function_calls/models.py:33`
- **Schema 摘要**
```json
{
"type": "object",
"required": ["reminder_id"],
"properties": {
"reminder_id": {
"type": "string",
"description": "提醒列表中的 ID日志或界面可显示前几位帮助用户复制"
}
}
}
```
- **参数含义**`reminder_id` 即提醒列表返回的唯一 ID用于定位要删除的记录。
- **调用方式**:直接读取 `args.reminder_id` 并转交给 `delete_reminder` 执行删除逻辑(`function_calls/handlers.py:76`)。
### perplexity_search
- **Handler**`function_calls/handlers.py:80`
- **模型**`PerplexityArgs``function_calls/models.py:39`
- **Schema 摘要**
```json
{
"type": "object",
"required": ["query"],
"properties": {
"query": {
"type": "string",
"description": "要搜索的问题或主题"
}
}
}
```
- **参数含义**`query` 即用户想让 Perplexity 查找的自然语言问题或主题。
- **调用方式**:使用 `args.query` 调用 `run_perplexity`,若外部服务已自行回复则返回空消息,否则把搜索结果转成回复文本(`function_calls/handlers.py:87`)。
### summary
- **Handler**`function_calls/handlers.py:96`
- **模型**`SummaryArgs``function_calls/models.py:49`
- **Schema 摘要**:空对象,无需参数。
- **调用方式**:只要模型选择该函数即执行 `summarize_messages(ctx)`,返回群聊近期消息总结(`function_calls/handlers.py:104`)。
## 运行时验证逻辑
1. **模型侧约束**`FunctionCallLLM` 会把上述 Schema 转成 OpenAI 样式的工具定义,使模型在生成参数时遵循字段、类型与枚举限制(`function_calls/llm.py:112`)。
2. **路由二次校验**:收到工具调用后,路由器会再次调用 `validate_arguments` 确认必填字段存在且类型正确(`function_calls/llm.py:215`)。
3. **Pydantic 转换**:最后在 `_create_args_instance` 中实例化 Pydantic 模型,确保所有字段通过严格校验并可在 handler 中以 `args` 访问(`function_calls/router.py:147`)。
若未来新增函数,只需为 handler 的 `args` 参数提供 Pydantic 模型或 dataclass装饰器会自动生成相应 Schema 并同步到本流程中。

View File

@@ -1,64 +1,25 @@
"""Function Call handlers built on top of structured services."""
from __future__ import annotations
import logging
from commands.context import MessageContext
from .models import (
WeatherArgs,
NewsArgs,
ReminderArgs,
ReminderListArgs,
ReminderDeleteArgs,
PerplexityArgs,
HelpArgs,
SummaryArgs,
ClearMessagesArgs,
InsultArgs,
)
from .registry import tool_function
from .spec import FunctionResult
from .services import (
build_help_text,
build_insult,
clear_group_messages,
create_reminder,
delete_reminder,
get_news_digest,
get_weather_report,
list_reminders,
run_perplexity,
summarize_messages,
)
logger = logging.getLogger(__name__)
@tool_function(
name="weather_query",
description="查询城市天气预报",
examples=["北京天气怎么样", "上海天气", "深圳明天会下雨吗"],
scope="both",
require_at=True,
)
def handle_weather(ctx: MessageContext, args: WeatherArgs) -> FunctionResult:
result = get_weather_report(args.city)
at = ctx.msg.sender if ctx.is_group else ""
return FunctionResult(handled=True, messages=[result.message], at=at if at else "")
@tool_function(
name="news_query",
description="获取今日新闻",
examples=["看看今天的新闻", "今日要闻", "新闻"],
scope="both",
require_at=True,
)
def handle_news(ctx: MessageContext, args: NewsArgs) -> FunctionResult:
result = get_news_digest()
at = ctx.msg.sender if ctx.is_group else ""
return FunctionResult(handled=True, messages=[result.message], at=at if at else "")
@tool_function(
name="reminder_set",
@@ -68,7 +29,7 @@ def handle_news(ctx: MessageContext, args: NewsArgs) -> FunctionResult:
require_at=True,
)
def handle_reminder_set(ctx: MessageContext, args: ReminderArgs) -> FunctionResult:
manager = getattr(ctx.robot, 'reminder_manager', None)
manager = getattr(ctx.robot, "reminder_manager", None)
at = ctx.msg.sender if ctx.is_group else ""
if not manager:
return FunctionResult(handled=True, messages=["❌ 内部错误:提醒管理器未初始化。"], at=at)
@@ -90,7 +51,7 @@ def handle_reminder_set(ctx: MessageContext, args: ReminderArgs) -> FunctionResu
require_at=True,
)
def handle_reminder_list(ctx: MessageContext, args: ReminderListArgs) -> FunctionResult:
manager = getattr(ctx.robot, 'reminder_manager', None)
manager = getattr(ctx.robot, "reminder_manager", None)
at = ctx.msg.sender if ctx.is_group else ""
if not manager:
return FunctionResult(handled=True, messages=["❌ 内部错误:提醒管理器未初始化。"], at=at)
@@ -107,7 +68,7 @@ def handle_reminder_list(ctx: MessageContext, args: ReminderListArgs) -> Functio
require_at=True,
)
def handle_reminder_delete(ctx: MessageContext, args: ReminderDeleteArgs) -> FunctionResult:
manager = getattr(ctx.robot, 'reminder_manager', None)
manager = getattr(ctx.robot, "reminder_manager", None)
at = ctx.msg.sender if ctx.is_group else ""
if not manager:
return FunctionResult(handled=True, messages=["❌ 内部错误:提醒管理器未初始化。"], at=at)
@@ -132,18 +93,6 @@ def handle_perplexity_search(ctx: MessageContext, args: PerplexityArgs) -> Funct
return FunctionResult(handled=True, messages=service_result.messages, at=at if at else "")
@tool_function(
name="help",
description="显示机器人帮助信息",
examples=["help", "帮助", "指令"],
scope="both",
require_at=False,
)
def handle_help(ctx: MessageContext, args: HelpArgs) -> FunctionResult:
help_text = build_help_text()
return FunctionResult(handled=True, messages=[help_text])
@tool_function(
name="summary",
description="总结群聊最近的消息",
@@ -154,27 +103,3 @@ def handle_help(ctx: MessageContext, args: HelpArgs) -> FunctionResult:
def handle_summary(ctx: MessageContext, args: SummaryArgs) -> FunctionResult:
result = summarize_messages(ctx)
return FunctionResult(handled=True, messages=[result.message])
@tool_function(
name="clear_messages",
description="清除群聊历史消息记录",
examples=["clearmessages", "清除历史"],
scope="group",
require_at=True,
)
def handle_clear_messages(ctx: MessageContext, args: ClearMessagesArgs) -> FunctionResult:
result = clear_group_messages(ctx)
return FunctionResult(handled=True, messages=[result.message])
@tool_function(
name="insult",
description="骂指定用户(仅限群聊)",
examples=["骂一下@某人"],
scope="group",
require_at=True,
)
def handle_insult(ctx: MessageContext, args: InsultArgs) -> FunctionResult:
result = build_insult(ctx, args.target_user)
return FunctionResult(handled=True, messages=[result.message])

View File

@@ -3,7 +3,7 @@ Function Call 参数模型定义
"""
from typing import Literal, Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field
class WeatherArgs(BaseModel):
@@ -19,10 +19,17 @@ class NewsArgs(BaseModel):
class ReminderArgs(BaseModel):
"""设置提醒参数"""
type: Literal["once", "daily", "weekly"]
time: str
content: str
weekday: Optional[int] = None
type: Literal["once", "daily", "weekly"] = Field(
..., description="提醒类型once=一次性提醒daily=每天weekly=每周"
)
time: str = Field(
..., description="提醒时间。once 使用 'YYYY-MM-DD HH:MM'daily/weekly 使用 'HH:MM'"
)
content: str = Field(..., description="提醒内容,将直接发送给用户")
weekday: Optional[int] = Field(
default=None,
description="当 type=weekly 时的星期索引0=周一 … 6=周日",
)
class ReminderListArgs(BaseModel):
@@ -33,12 +40,13 @@ class ReminderListArgs(BaseModel):
class ReminderDeleteArgs(BaseModel):
"""删除提醒参数"""
reminder_id: str
reminder_id: str = Field(..., description="提醒列表中的 ID前端可展示前几位")
class PerplexityArgs(BaseModel):
"""Perplexity搜索参数"""
query: str
query: str = Field(..., description="要搜索的问题或主题")
class HelpArgs(BaseModel):

View File

@@ -1,7 +1,7 @@
"""Function Call 路由器"""
import logging
from typing import Any, Dict, Optional
from typing import Any, Dict
from commands.context import MessageContext
@@ -39,45 +39,6 @@ class FunctionCallRouter:
return True
def _try_direct_command_match(self, ctx: MessageContext) -> Optional[str]:
"""
尝试直接命令匹配避免不必要的LLM调用
返回匹配的函数名如果没有匹配则返回None
"""
text = ctx.text.strip().lower()
# 定义一些明确的命令关键字映射
direct_commands = {
"help": "help",
"帮助": "help",
"指令": "help",
"新闻": "news_query",
"summary": "summary",
"总结": "summary",
"clearmessages": "clear_messages",
"清除历史": "clear_messages"
}
# 检查完全匹配
if text in direct_commands:
return direct_commands[text]
# 检查以特定前缀开头的命令
if text.startswith("ask ") and len(text) > 4:
return "perplexity_search"
if text.startswith("天气") or text.startswith("天气预报"):
return "weather_query"
if text in ["查看提醒", "我的提醒", "提醒列表"]:
return "reminder_list"
if text.startswith("骂一下"):
return "insult"
return None
def dispatch(self, ctx: MessageContext) -> bool:
"""
分发消息到函数处理器
@@ -100,26 +61,7 @@ class FunctionCallRouter:
self.logger.warning("没有注册任何函数")
return False
# 第一步:尝试直接命令匹配
direct_function = self._try_direct_command_match(ctx)
if direct_function and direct_function in functions:
spec = functions[direct_function]
if not self._check_scope_and_permissions(ctx, spec):
return False
arguments = self._extract_arguments_for_direct_command(ctx, direct_function)
if not self.llm.validate_arguments(arguments, spec.parameters_schema):
self.logger.warning(f"直接命令 {direct_function} 参数验证失败")
return False
result = self._invoke_function(ctx, spec, arguments)
if result.handled:
result.dispatch(ctx)
return True
# 如果没有处理成功继续尝试LLM流程
# 第二步使用LLM执行多轮函数调用
# 使用 LLM 执行函数调用流程
llm_result = self.llm.run(
ctx,
functions,
@@ -141,38 +83,6 @@ class FunctionCallRouter:
self.logger.error(f"FunctionCallRouter dispatch 异常: {e}")
return False
def _extract_arguments_for_direct_command(self, ctx: MessageContext, function_name: str) -> Dict[str, Any]:
"""为直接命令提取参数"""
text = ctx.text.strip()
if function_name == "weather_query":
# 提取城市名
if text.startswith("天气预报 "):
city = text[4:].strip()
elif text.startswith("天气 "):
city = text[3:].strip()
else:
city = ""
return {"city": city}
elif function_name == "perplexity_search":
# 提取搜索查询
if text.startswith("ask "):
query = text[4:].strip()
else:
query = text
return {"query": query}
elif function_name == "insult":
# 提取要骂的用户
import re
match = re.search(r"骂一下\s*@([^\s@]+)", text)
target_user = match.group(1) if match else ""
return {"target_user": target_user}
# 对于不需要参数的函数,返回空字典
return {}
def _invoke_function(self, ctx: MessageContext, spec: FunctionSpec, arguments: Dict[str, Any]) -> FunctionResult:
"""调用函数处理器,返回结构化结果"""
try:

View File

@@ -1,22 +1,13 @@
"""Service helpers for Function Call handlers."""
from .weather import get_weather_report
from .news import get_news_digest
from .reminder import create_reminder, list_reminders, delete_reminder
from .help import build_help_text
from .group_tools import summarize_messages, clear_group_messages
from .group_tools import summarize_messages
from .perplexity import run_perplexity
from .insult import build_insult
__all__ = [
"get_weather_report",
"get_news_digest",
"create_reminder",
"list_reminders",
"delete_reminder",
"build_help_text",
"summarize_messages",
"clear_group_messages",
"run_perplexity",
"build_insult",
]

View File

@@ -10,14 +10,6 @@ HELP_LINES = [
"- 新闻",
"- ask [问题]",
"",
"【决斗 & 偷袭】",
"- 决斗@XX",
"- 偷袭@XX",
"- 决斗排行/排行榜",
"- 我的战绩/决斗战绩",
"- 我的装备/查看装备",
"- 改名 [旧名] [新名]",
"",
"【提醒】",
"- 提醒xxxxx一次性、每日、每周",
"- 查看提醒/我的提醒/提醒列表",

View File

@@ -1,11 +1,5 @@
"""图像生成功能模块
包含以下功能:
- AliyunImage: 阿里云文生图
- GeminiImage: 谷歌Gemini文生图
"""
"""图像生成功能模块"""
from .img_aliyun_image import AliyunImage
from .img_gemini_image import GeminiImage
__all__ = ['AliyunImage', 'GeminiImage']
__all__ = ['AliyunImage']

View File

@@ -1,113 +0,0 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import os
import mimetypes
import time
import random
from google import genai
from google.genai import types
class GeminiImage:
"""谷歌AI画图API调用
"""
def __init__(self, config={}) -> None:
self.LOG = logging.getLogger("GeminiImage")
self.enable = config.get("enable", True)
self.api_key = config.get("api_key", "") or os.environ.get("GEMINI_API_KEY", "")
self.model = config.get("model", "gemini-2.0-flash-exp-image-generation")
self.proxy = config.get("proxy", "")
project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.temp_dir = config.get("temp_dir", os.path.join(project_dir, "geminiimg"))
if not os.path.exists(self.temp_dir):
os.makedirs(self.temp_dir)
if not self.api_key:
self.enable = False
return
try:
# 设置代理
if self.proxy:
os.environ["HTTP_PROXY"] = self.proxy
os.environ["HTTPS_PROXY"] = self.proxy
# 初始化客户端
self.client = genai.Client(api_key=self.api_key)
except Exception:
self.enable = False
def generate_image(self, prompt: str) -> str:
"""生成图像并返回图像文件路径
"""
try:
# 设置代理
if self.proxy:
os.environ["HTTP_PROXY"] = self.proxy
os.environ["HTTPS_PROXY"] = self.proxy
image_prompt = f"生成一张高质量的图片: {prompt}。请直接提供图像,不需要描述。"
# 发送请求
response = self.client.models.generate_content(
model=self.model,
contents=image_prompt,
config=types.GenerateContentConfig(
response_modalities=['Text', 'Image']
)
)
# 处理响应
if hasattr(response, 'candidates') and response.candidates:
for candidate in response.candidates:
if hasattr(candidate, 'content') and candidate.content:
for part in candidate.content.parts:
if hasattr(part, 'inline_data') and part.inline_data:
# 保存图像
file_name = f"gemini_image_{int(time.time())}_{random.randint(1000, 9999)}"
file_extension = mimetypes.guess_extension(part.inline_data.mime_type) or ".png"
file_path = os.path.join(self.temp_dir, f"{file_name}{file_extension}")
with open(file_path, "wb") as f:
f.write(part.inline_data.data)
return file_path
# 如果没有找到图像,尝试获取文本响应
try:
text_content = response.text
if text_content:
return f"模型未能生成图像: {text_content[:100]}..."
except (AttributeError, TypeError):
pass
return "图像生成失败,可能需要更新模型或调整提示词"
except Exception as e:
error_str = str(e)
self.LOG.error(f"图像生成出错: {error_str}")
# 处理500错误
if "500 INTERNAL" in error_str:
self.LOG.error("遇到谷歌服务器内部错误")
return "谷歌AI服务器临时故障请稍后再试。这是谷歌服务器的问题不是你的请求有误。"
if "timeout" in error_str.lower():
return "图像生成超时,请检查网络或代理设置"
if "violated" in error_str.lower() or "policy" in error_str.lower():
return "请求包含违规内容,无法生成图像"
# 其他常见错误类型处理
if "quota" in error_str.lower() or "rate" in error_str.lower():
return "API使用配额已用尽或请求频率过高请稍后再试"
if "authentication" in error_str.lower() or "auth" in error_str.lower():
return "API密钥验证失败请联系管理员检查配置"
return f"图像生成失败,错误原因: {error_str.split('.')[-1] if '.' in error_str else error_str}"

View File

@@ -5,7 +5,7 @@ import shutil
import time
from wcferry import Wcf
from configuration import Config
from image import AliyunImage, GeminiImage
from image import AliyunImage
class ImageGenerationManager:
@@ -30,22 +30,9 @@ class ImageGenerationManager:
# 初始化图像生成服务
self.aliyun_image = None
self.gemini_image = None
self.LOG.info("开始初始化图像生成服务...")
# 初始化Gemini图像生成服务
try:
if hasattr(self.config, 'GEMINI_IMAGE'):
self.gemini_image = GeminiImage(self.config.GEMINI_IMAGE)
else:
self.gemini_image = GeminiImage({})
if getattr(self.gemini_image, 'enable', False):
self.LOG.info("谷歌Gemini图像生成功能已启用")
except Exception as e:
self.LOG.error(f"初始化谷歌Gemini图像生成服务失败: {e}")
# 初始化AliyunImage服务
if hasattr(self.config, 'ALIYUN_IMAGE') and self.config.ALIYUN_IMAGE.get('enable', False):
try:
@@ -56,7 +43,7 @@ class ImageGenerationManager:
def handle_image_generation(self, service_type, prompt, receiver, at_user=None):
"""处理图像生成请求的通用函数
:param service_type: 服务类型,'aliyun'/'gemini'
:param service_type: 服务类型,当前支持 'aliyun'
:param prompt: 图像生成提示词
:param receiver: 接收者ID
:param at_user: 被@的用户ID用于群聊
@@ -78,13 +65,6 @@ class ImageGenerationManager:
wait_message = "当前模型为阿里V1模型生成速度非常慢可能需要等待较长时间请耐心等候..."
else:
wait_message = "正在生成图像,请稍等..."
elif service_type == 'gemini':
if not self.gemini_image or not getattr(self.gemini_image, 'enable', False):
self.send_text("谷歌文生图服务未启用", receiver, at_user)
return True
service = self.gemini_image
wait_message = "正在通过谷歌AI生成图像请稍等..."
else:
self.LOG.error(f"未知的图像生成服务类型: {service_type}")
return False
@@ -93,12 +73,11 @@ class ImageGenerationManager:
self.send_text(wait_message, receiver, at_user)
image_url = service.generate_image(prompt)
if image_url and (image_url.startswith("http") or os.path.exists(image_url)):
try:
self.LOG.info(f"开始处理图片: {image_url}")
# 谷歌API直接返回本地文件路径无需下载
image_path = image_url if service_type == 'gemini' else service.download_image(image_url)
image_path = service.download_image(image_url)
if image_path:
# 创建一个临时副本,避免文件占用问题
@@ -167,4 +146,4 @@ class ImageGenerationManager:
else:
self.LOG.error(f"无法删除文件 {file_path} 经过 {max_retries} 次尝试: {str(e)}")
return False
return False

View File

@@ -1,58 +1,38 @@
# 图像生成配置说明
#### 文生图相关功能的加入可在此说明文件内加入贡献者的GitHub链接方便以后的更新以及BUG的修改
#### 文生图相关功能的加入,可在此说明文件内加入贡献者的 GitHub 链接,方便以后的更新,以及 BUG 的修改!
智谱AI绘画[JiQingzhe2004 (JiQingzhe)](https://github.com/JiQingzhe2004)
阿里云AI绘画[JiQingzhe2004 (JiQingzhe)](https://github.com/JiQingzhe2004)
谷歌AI绘画[JiQingzhe2004 (JiQingzhe)](https://github.com/JiQingzhe2004)
------
在`config.yaml`中进行以下配置才可以调用:
`config.yaml` 中进行以下配置才可以调用:
```yaml
aliyun_image: # -----如果要使用阿里云文生图,取消下面的注释并填写相关内容,模型到阿里云百炼找通义万相-文生图2.1-Turbo-----
enable: true # 是否启用阿里文生图功能false为关闭默认开启如果未配置则会将消息发送给聊天大模型
api_key: sk-xxxxxxxxxxxxxxxxxxxxxxxx # 替换为你的DashScope API密钥
model: wanx2.1-t2i-turbo # 模型名称默认使用wanx2.1-t2i-turbo(快),wanx2.1-t2i-plus,wanx-v1,会给用户不同的提示!
api_key: sk-xxxxxxxxxxxxxxxxxxxxxxxx # 替换为你的 DashScope API 密钥
model: wanx2.1-t2i-turbo # 模型名称,默认使用 wanx2.1-t2i-turbo(快), wanx2.1-t2i-plus, wanx-v1
size: 1024*1024 # 图像尺寸,格式为宽*高
n: 1 # 生成图像的数量
temp_dir: ./temp # 临时文件存储路径
trigger_keyword: 牛阿里 # 触发词,默认为"牛阿里"
trigger_keyword: 牛阿里 # 触发词,默认为 "牛阿里"
fallback_to_chat: true # 当未启用绘画功能时true=将请求发给聊天模型处理false=回复固定的未启用提示信息
gemini_image: # -----谷歌AI画图配置这行不填-----
enable: true # 是否启用谷歌AI画图功能
api_key: your-api-key-here # 谷歌Gemini API密钥必填
model: gemini-2.0-flash-exp-image-generation # 模型名称,建议保持默认,只有这一个模型可以进行绘画
temp_dir: ./geminiimg # 图片保存目录,可选
trigger_keyword: 牛谷歌 # 触发词,默认为"牛谷歌"
fallback_to_chat: false # 当未启用绘画功能时true=将请求发给聊天模型处理false=回复固定的未启用提示信息
```
## 如何获取API密钥
1. 访问 [Google AI Studio](https://aistudio.google.com/)
2. 创建一个账号或登录
3. 访问 [API Keys](https://aistudio.google.com/app/apikeys) 页面
4. 创建一个新的API密钥
5. 复制API密钥并填入配置文件
## 使用方法
直接发送消息或在群聊中@机器人,使用触发词加提示词,例如:
直接发送消息或在群聊中 @机器人,使用触发词加提示词,例如:
# 单人聊天的使用
### 单人聊天示例
```
牛智谱 一只可爱的猫咪在阳光下玩耍
牛阿里 一只可爱的猫咪在阳光下玩耍
牛谷歌 一只可爱的猫咪在阳光下玩耍
```
## 群组的使用方法
### 群组示例
```
@ 牛图图 一只可爱的猫咪在阳光下玩耍

View File

@@ -13,6 +13,4 @@ pillow
jupyter_client
zhdate
ipykernel
google-generativeai>=0.3.0
dashscope
google-genai

View File

@@ -9,18 +9,15 @@ from threading import Thread
import os
import random
import shutil
from image import AliyunImage, GeminiImage
from image.img_manager import ImageGenerationManager
from wcferry import Wcf, WxMsg
from ai_providers.ai_chatgpt import ChatGPT
from ai_providers.ai_deepseek import DeepSeek
from ai_providers.ai_gemini import Gemini
from ai_providers.ai_perplexity import Perplexity
from function.func_weather import Weather
from function.func_news import News
from function.func_duel import start_duel, get_rank_list, get_player_stats, change_player_name, DuelManager, attempt_sneak_attack
from function.func_summary import MessageSummary # 导入新的MessageSummary类
from function.func_reminder import ReminderManager # 导入ReminderManager类
from configuration import Config
@@ -52,8 +49,6 @@ class Robot(Job):
self.wxid = self.wcf.get_self_wxid() # 获取机器人自己的wxid
self.allContacts = self.getAllContacts()
self._msg_timestamps = []
self.duel_manager = DuelManager(self.sendDuelMsg)
try:
db_path = "data/message_history.db"
# 使用 getattr 安全地获取 MAX_HISTORY如果不存在则默认为 300
@@ -95,19 +90,6 @@ class Robot(Job):
except Exception as e:
self.LOG.error(f"初始化 DeepSeek 模型时出错: {str(e)}")
# 初始化Gemini
if Gemini.value_check(self.config.GEMINI):
try:
# 传入 message_summary 和 wxid
self.chat_models[ChatType.GEMINI.value] = Gemini(
self.config.GEMINI,
message_summary_instance=self.message_summary,
bot_wxid=self.wxid
)
self.LOG.info(f"已加载 Gemini 模型")
except Exception as e:
self.LOG.error(f"初始化 Gemini 模型时出错: {str(e)}")
# 初始化Perplexity
if Perplexity.value_check(self.config.PERPLEXITY):
self.chat_models[ChatType.PERPLEXITY.value] = Perplexity(self.config.PERPLEXITY)
@@ -417,36 +399,12 @@ class Robot(Job):
for r in receivers:
self.sendTextMsg(report, r)
def sendDuelMsg(self, msg: str, receiver: str) -> None:
"""发送决斗消息,不受消息频率限制,不记入历史记录
:param msg: 消息字符串
:param receiver: 接收人wxid或者群id
"""
try:
self.wcf.send_text(f"{msg}", receiver, "")
except Exception as e:
self.LOG.error(f"发送决斗消息失败: {e}")
def cleanup_perplexity_threads(self):
"""清理所有Perplexity线程"""
# 如果已初始化Perplexity实例调用其清理方法
perplexity_instance = self.get_perplexity_instance()
if perplexity_instance:
perplexity_instance.cleanup()
# 检查并等待决斗线程结束
if hasattr(self, 'duel_manager') and self.duel_manager.is_duel_running():
self.LOG.info("等待决斗线程结束...")
# 最多等待5秒
for i in range(5):
if not self.duel_manager.is_duel_running():
break
time.sleep(1)
if self.duel_manager.is_duel_running():
self.LOG.warning("决斗线程在退出时仍在运行")
else:
self.LOG.info("决斗线程已结束")
def cleanup(self):
"""清理所有资源,在程序退出前调用"""