""" 随机发病文学插件 支持指令触发和定时推送 """ import tomllib import asyncio import aiohttp import random from pathlib import Path from loguru import logger from typing import Optional from utils.plugin_base import PluginBase from utils.decorators import on_text_message, schedule from WechatHook import WechatHookClient # 可选导入代理支持 try: from aiohttp_socks import ProxyConnector PROXY_SUPPORT = True except ImportError: PROXY_SUPPORT = False logger.warning("aiohttp_socks 未安装,代理功能将不可用") class Fabing(PluginBase): """随机发病文学插件""" description = "随机发病文学 - 指令触发和定时推送" author = "ShiHao" version = "1.0.0" def __init__(self): super().__init__() self.config = None async def async_init(self): """异步初始化""" try: config_path = Path(__file__).parent / "config.toml" if not config_path.exists(): logger.error(f"发病文学插件配置文件不存在: {config_path}") return with open(config_path, "rb") as f: self.config = tomllib.load(f) logger.success("随机发病文学插件已加载") except Exception as e: logger.error(f"随机发病文学插件初始化失败: {e}") self.config = None async def _fetch_fabing(self, name: str) -> Optional[str]: """获取发病文学""" try: api_config = self.config["api"] timeout = aiohttp.ClientTimeout(total=api_config["timeout"]) # 配置代理 connector = None proxy_config = self.config.get("proxy", {}) if proxy_config.get("enabled", False): proxy_type = proxy_config.get("type", "socks5").upper() proxy_host = proxy_config.get("host", "127.0.0.1") proxy_port = proxy_config.get("port", 7890) proxy_url = f"{proxy_type}://{proxy_host}:{proxy_port}" if PROXY_SUPPORT: try: connector = ProxyConnector.from_url(proxy_url) except Exception as e: logger.warning(f"代理配置失败,将直连: {e}") connector = None params = {"name": name} async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session: async with session.get(api_config["base_url"], params=params) as resp: if resp.status != 200: error_text = await resp.text() logger.error(f"发病文学 API 错误: {resp.status}, {error_text}") return None result = await resp.json() if result.get("code") != 200: logger.error(f"发病文学 API 返回错误: {result.get('message')}") return None data = result.get("data", {}) saying = data.get("saying", "") if not saying: logger.warning("发病文学 API 返回数据为空") return None logger.info(f"获取发病文学成功: {name}") return saying except Exception as e: logger.error(f"获取发病文学失败: {e}") import traceback logger.error(traceback.format_exc()) return None async def _get_random_group_member(self, bot: WechatHookClient, group_id: str) -> Optional[str]: """从群组中随机抽取一名成员的昵称""" try: # 从MessageLogger数据库中获取该群组的所有成员昵称 from plugins.MessageLogger.main import MessageLogger msg_logger = MessageLogger.get_instance() if not msg_logger: logger.warning("MessageLogger实例不存在,无法获取群成员") return None with msg_logger.get_db_connection() as conn: with conn.cursor() as cursor: # 查询该群组最近活跃的成员昵称(去重) sql = """ SELECT DISTINCT nickname FROM messages WHERE group_id = %s AND nickname != '' AND nickname IS NOT NULL ORDER BY create_time DESC LIMIT 100 """ cursor.execute(sql, (group_id,)) results = cursor.fetchall() if not results: logger.warning(f"群组 {group_id} 没有找到成员昵称") return None # 提取昵称列表 nicknames = [row[0] for row in results] # 随机选择一个昵称 selected_nickname = random.choice(nicknames) logger.info(f"从群组 {group_id} 随机选择了昵称: {selected_nickname}") return selected_nickname except Exception as e: logger.error(f"获取随机群成员失败: {e}") import traceback logger.error(traceback.format_exc()) return None @on_text_message(priority=70) async def handle_command(self, bot: WechatHookClient, message: dict): """处理指令触发""" if self.config is None: return True content = message.get("Content", "").strip() from_wxid = message.get("FromWxid", "") is_group = message.get("IsGroup", False) # 检查是否是触发指令 keywords = self.config["behavior"]["command_keywords"] matched = False name = None for keyword in keywords: # 支持 "发病 xxx" 或 "@机器人 发病 xxx" if content.startswith(keyword + " ") or content.endswith(" " + keyword + " "): matched = True # 提取名字 parts = content.split() for i, part in enumerate(parts): if part == keyword or part == keyword.lstrip("/"): if i + 1 < len(parts): name = parts[i + 1] break break elif content == keyword: matched = True name = None # 没有指定名字 break if not matched: return True if not self.config["behavior"]["enabled"]: return True # 检查群聊过滤 if is_group: enabled_groups = self.config["behavior"]["enabled_groups"] disabled_groups = self.config["behavior"]["disabled_groups"] if from_wxid in disabled_groups: return True if enabled_groups and from_wxid not in enabled_groups: return True # 如果没有指定名字,从群成员中随机选择 if not name and is_group: name = await self._get_random_group_member(bot, from_wxid) if not name: await bot.send_text(from_wxid, "❌ 无法获取群成员信息") return False elif not name: await bot.send_text(from_wxid, "❌ 请指定名字\n格式:发病 名字") return False logger.info(f"收到发病文学请求: {from_wxid}, name={name}") try: saying = await self._fetch_fabing(name) if not saying: await bot.send_text(from_wxid, "❌ 获取发病文学失败,请稍后重试") return False # 发送发病文学 await bot.send_text(from_wxid, saying) logger.success(f"已发送发病文学: {name}") except Exception as e: logger.error(f"处理发病文学请求失败: {e}") await bot.send_text(from_wxid, f"❌ 请求失败: {str(e)}") return False @schedule('cron', minute=0) async def scheduled_push(self, bot=None): """定时推送发病文学(每小时整点)""" if not self.config or not self.config["schedule"]["enabled"]: return logger.info("开始执行发病文学定时推送任务") try: # 获取bot实例 if not bot: from utils.plugin_manager import PluginManager bot = PluginManager().bot if not bot: logger.error("定时任务:无法获取bot实例") return # 获取目标群组 enabled_groups = self.config["behavior"]["enabled_groups"] disabled_groups = self.config["behavior"]["disabled_groups"] # 如果没有配置enabled_groups,跳过 if not enabled_groups: logger.warning("未配置群组白名单,跳过定时推送") return success_count = 0 group_interval = self.config["schedule"]["group_interval"] for group_id in enabled_groups: if group_id in disabled_groups: continue try: logger.info(f"向群聊 {group_id} 推送发病文学") # 从群成员中随机选择一个昵称 name = await self._get_random_group_member(bot, group_id) if not name: logger.warning(f"群聊 {group_id} 无法获取群成员昵称") continue # 获取发病文学 saying = await self._fetch_fabing(name) if not saying: logger.warning(f"群聊 {group_id} 获取发病文学失败") continue # 发送发病文学 await bot.send_text(group_id, saying) success_count += 1 logger.success(f"群聊 {group_id} 推送成功") # 群聊之间的间隔 await asyncio.sleep(group_interval) except Exception as e: logger.error(f"推送到 {group_id} 失败: {e}") import traceback logger.error(traceback.format_exc()) logger.info(f"发病文学定时推送完成 - 成功: {success_count}/{len(enabled_groups)}") except Exception as e: logger.error(f"发病文学定时推送失败: {e}") import traceback logger.error(traceback.format_exc()) def get_llm_tools(self): """返回LLM工具定义""" return [{ "type": "function", "function": { "name": "get_fabing", "description": "获取随机发病文学。当用户要求发病、整活、发疯等内容时调用此工具。", "parameters": { "type": "object", "properties": { "name": { "type": "string", "description": "要发病的对象名字" } }, "required": ["name"] } } }] async def execute_llm_tool(self, tool_name: str, arguments: dict, bot: WechatHookClient, from_wxid: str) -> dict: """执行LLM工具调用""" if tool_name != "get_fabing": return None try: logger.info(f"LLM工具调用发病文学: {from_wxid}") name = arguments.get("name") if not name: return { "success": False, "message": "缺少名字参数" } saying = await self._fetch_fabing(name) if not saying: return { "success": False, "message": "获取发病文学失败,请稍后重试" } # 发送发病文学 await bot.send_text(from_wxid, saying) return { "success": True, "message": f"已发送发病文学", "no_reply": True # 已发送内容,不需要AI再回复 } except Exception as e: logger.error(f"LLM工具执行失败: {e}") return { "success": False, "message": f"执行失败: {str(e)}" }