From c1b95179909826b894bb80f70bcac40e6c690271 Mon Sep 17 00:00:00 2001 From: liuwei Date: Wed, 21 May 2025 16:03:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=9A=84=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/ai_auto_response/__init__.py | 7 + plugins/ai_auto_response/bot_ai.py | 158 +++++++++++++++++++ plugins/ai_auto_response/config.toml | 25 +++ plugins/ai_auto_response/main.py | 219 +++++++++++++++++++++++++++ utils/robot_cmd/robot_command.py | 1 + 5 files changed, 410 insertions(+) create mode 100644 plugins/ai_auto_response/__init__.py create mode 100644 plugins/ai_auto_response/bot_ai.py create mode 100644 plugins/ai_auto_response/config.toml create mode 100644 plugins/ai_auto_response/main.py diff --git a/plugins/ai_auto_response/__init__.py b/plugins/ai_auto_response/__init__.py new file mode 100644 index 0000000..cb8cecf --- /dev/null +++ b/plugins/ai_auto_response/__init__.py @@ -0,0 +1,7 @@ +# 从当前包的main模块导入AIAutoResponsePlugin类 +from .main import AIAutoResponsePlugin + +# 提供get_plugin函数,返回插件实例 +def get_plugin(): + """获取插件实例""" + return AIAutoResponsePlugin() \ No newline at end of file diff --git a/plugins/ai_auto_response/bot_ai.py b/plugins/ai_auto_response/bot_ai.py new file mode 100644 index 0000000..b5cb914 --- /dev/null +++ b/plugins/ai_auto_response/bot_ai.py @@ -0,0 +1,158 @@ +import re +from datetime import datetime, time +import toml +import os + + +class InterventionBot: + def __init__(self, config_path=None): + # 加载配置 + self.config = {} + if config_path and os.path.exists(config_path): + self.config = toml.load(config_path) + + # 从配置中获取关键词 + keywords = self.config.get("Keywords", {}) + time_window = self.config.get("TimeWindow", {}) + + # 表情符号库 + self.emojis = keywords.get("emojis", ["[捂脸]", "[奸笑]", "[可怜]", "[擦汗]", "[发呆]", "[抠鼻]", "[破涕为笑]", "[旺柴]"]) + # 话题关键词 + self.hot_topics = keywords.get("hot_topics", ["咖啡", "手机", "小米", "华为", "苹果", "价格", "流畅", "螺蛳粉", "外卖"]) + self.fish_keywords = keywords.get("fish_keywords", ["鱼缸", "鱼便", "红边", "造浪", "养鱼", "进货", "鳑鲏", "吸鳅"]) + self.tech_keywords = keywords.get("tech_keywords", ["MIUI", "鸿蒙", "iPhone", "安卓", "推送", "充电", "屏幕", "电池"]) + self.mechanism_keywords = keywords.get("mechanism_keywords", ["积分", "AI ", "功能列表", "黑丝", "打劫", "指令"]) + self.news_keywords = keywords.get("news_keywords", ["新闻", "骨灰房", "法院", "判决", "住建局"]) + + # 早晨签到时间窗口 + morning_start_hour = time_window.get("morning_start_hour", 8) + morning_start_minute = time_window.get("morning_start_minute", 0) + morning_end_hour = time_window.get("morning_end_hour", 8) + morning_end_minute = time_window.get("morning_end_minute", 30) + + self.morning_window = ( + time(morning_start_hour, morning_start_minute), + time(morning_end_hour, morning_end_minute) + ) + + def is_morning_window(self, timestamp): + """检查是否在早晨签到时间窗口""" + try: + message_time = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").time() + return self.morning_window[0] <= message_time <= self.morning_window[1] + except ValueError: + return False + + def detect_topic(self, message): + """检测消息所属话题类型""" + message_lower = message.lower() + if any(keyword in message_lower for keyword in self.fish_keywords): + return "fish" + if any(keyword in message_lower for keyword in self.tech_keywords): + return "tech" + if any(keyword in message_lower for keyword in self.mechanism_keywords): + return "mechanism" + if any(keyword in message_lower for keyword in self.news_keywords): + return "news" + if any(keyword in message_lower for keyword in self.hot_topics): + return "hot_topic" + return None + + def rule_morning_signin(self, timestamp, messages): + """规则1:早晨签到""" + return self.is_morning_window(timestamp) and any("签到" in msg or "早" in msg for msg in messages[-5:]) + + def rule_hot_topic(self, message, messages): + """规则2:热点话题参与""" + return self.detect_topic(message) == "hot_topic" and len( + [m for m in messages[-5:] if self.detect_topic(m) == "hot_topic"]) >= 3 + + def rule_tech_discussion(self, message, messages): + """规则3:技术性讨论""" + return self.detect_topic(message) == "tech" + + def rule_fish_discussion(self, message, messages): + """规则4:养鱼话题""" + return self.detect_topic(message) == "fish" + + def rule_mechanism_interaction(self, message, messages): + """规则5:群内机制互动""" + return self.detect_topic(message) == "mechanism" + + def rule_humor_tease(self, message, messages): + """规则6:幽默与调侃""" + return any(emoji in message for emoji in self.emojis) or "哈哈" in message or len( + [m for m in messages[-5:] if any(e in m for e in self.emojis)]) >= 2 + + def rule_news_reaction(self, message, messages): + """规则7:猎奇或社会新闻反应""" + return self.detect_topic(message) == "news" + + def should_intervene(self, timestamp, message, messages): + """判断是否需要介入""" + rules = [ + self.rule_morning_signin, + self.rule_hot_topic, + self.rule_tech_discussion, + self.rule_fish_discussion, + self.rule_mechanism_interaction, + self.rule_humor_tease, + self.rule_news_reaction + ] + + for rule in rules: + if rule == self.rule_morning_signin: + if rule(timestamp, messages): + return True + elif rule(message, messages): + return True + return False + + def process_message(self, timestamp, message, messages): + """处理单条消息,返回介入状态""" + if self.should_intervene(timestamp, message, messages): + return True + return False + + def process_chat_log(self, chat_log): + """处理聊天记录,返回每条消息的介入状态""" + messages = [line["message"] for line in chat_log] + results = [] + + for i, line in enumerate(chat_log): + timestamp = line["timestamp"] + message = line["message"] + intervention = self.process_message(timestamp, message, messages[:i + 1]) + results.append({ + "timestamp": timestamp, + "message": message, + "intervention": intervention + }) + + return results + + +# 示例用法 +if __name__ == "__main__": + # 模拟聊天记录 + sample_chat_log = [ + {"timestamp": "2025-03-14 08:06:38", "user_id": "Jyunere", "message": "签到"}, + {"timestamp": "2025-03-14 08:06:54", "user_id": "Jyunere", "message": "啥情况?卷了?"}, + {"timestamp": "2025-03-14 08:07:20", "user_id": "wxid_qx4z0jq3rp3122", "message": "那你喝咖啡就好了"}, + {"timestamp": "2025-03-14 09:12:28", "user_id": "Jyunere", "message": "我同事的鸿蒙确实流畅。"}, + {"timestamp": "2025-03-14 09:35:21", "user_id": "Jyunere", "message": "垃圾MIUI"}, + {"timestamp": "2025-05-21 14:31:57", "user_id": "wxid_4re8ddo26dxb52", "message": "年轻人随随便便就能深蹲200"}, + {"timestamp": "2025-05-21 14:32:20", "user_id": "liu79830956", + "message": "@水牛 过分了啊,报错还扣积分 赔我200"}, + {"timestamp": "2025-05-21 14:32:39", "user_id": "Jyunere", "message": "哈哈,识别到指令了。"}, + {"timestamp": "2025-05-21 14:32:42", "user_id": "wxid_z8uo70zywfpn12", "message": "检测到天 气了"}, + {"timestamp": "2025-05-21 14:35:08", "user_id": "liu79830956", "message": "这螺蛳粉估计要明天也吃不上了[旺柴]"} + ] + + bot = InterventionBot() + results = bot.process_chat_log(sample_chat_log) + + for result in results: + print(f"[{result['timestamp']}] Message: {result['message']}") + print(f"Intervention: {result['intervention']}") + print("-" * 50) \ No newline at end of file diff --git a/plugins/ai_auto_response/config.toml b/plugins/ai_auto_response/config.toml new file mode 100644 index 0000000..1b93770 --- /dev/null +++ b/plugins/ai_auto_response/config.toml @@ -0,0 +1,25 @@ +enable = true +command = ["ai介入", "ai对话", "ai自动回复"] +command-format = """ +🤖AI自动对话指令: +ai介入 开启/关闭 +""" +dify_api_url = "http://192.168.2.240/v1/chat-messages" +dify_api_key = "app-oDHbln5CzBLt3uS9bIBlJjhZ" # 请在此处填入您的DIFY API密钥 + +[Keywords] +# 表情符号库 +emojis = ["[捂脸]", "[奸笑]", "[可怜]", "[擦汗]", "[发呆]", "[抠鼻]", "[破涕为笑]", "[旺柴]"] +# 话题关键词 +hot_topics = ["咖啡", "手机", "小米", "华为", "苹果", "价格", "流畅", "螺蛳粉", "外卖"] +fish_keywords = ["鱼缸", "鱼便", "红边", "造浪", "养鱼", "进货", "鳑鲏", "吸鳅"] +tech_keywords = ["MIUI", "鸿蒙", "iPhone", "安卓", "推送", "充电", "屏幕", "电池"] +mechanism_keywords = ["积分", "AI ", "功能列表", "黑丝", "打劫", "指令"] +news_keywords = ["新闻", "骨灰房", "法院", "判决", "住建局"] + +[TimeWindow] +# 早晨签到时间窗口(8:00-8:30) +morning_start_hour = 8 +morning_start_minute = 0 +morning_end_hour = 8 +morning_end_minute = 30 \ No newline at end of file diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py new file mode 100644 index 0000000..aa29089 --- /dev/null +++ b/plugins/ai_auto_response/main.py @@ -0,0 +1,219 @@ +from loguru import logger +import os +import json +import requests +from typing import Dict, Any, List, Optional, Tuple + +from plugin_common.message_plugin_interface import MessagePluginInterface +from plugin_common.plugin_interface import PluginStatus +from utils.decorator.plugin_decorators import plugin_stats_decorator +from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager +from utils.decorator.points_decorator import plugin_points_cost +from wechat_ipad import WechatAPIClient + +from .bot_ai import InterventionBot + + +class AIAutoResponsePlugin(MessagePluginInterface): + """AI自动对话插件""" + + @property + def name(self) -> str: + return "AI自动对话" + + @property + def version(self) -> str: + return "1.0.0" + + @property + def description(self) -> str: + return "提供AI自动对话功能,可以在群聊中自动介入对话" + + @property + def author(self) -> str: + return "Trae AI" + + @property + def command_prefix(self) -> Optional[str]: + return "" # 不需要前缀,直接匹配命令 + + @property + def commands(self) -> List[str]: + return self._commands + + def __init__(self): + super().__init__() + self.intervention_bot = None + self.group_messages = {} # 存储每个群的最近消息 + self.max_messages = 20 # 每个群最多存储的消息数量 + self.auto_response_enabled = {} # 存储每个群是否启用自动回复 + + # DIFY API配置 + self.dify_api_url = "http://192.168.2.240/v1/chat-messages" + self.dify_api_key = "" # 需要在配置文件中设置 + + def initialize(self, context: Dict[str, Any]) -> bool: + """初始化插件""" + self.LOG = logger + self.LOG.info(f"正在初始化 {self.name} 插件...") + + # 保存上下文对象 + self.event_system = context.get("event_system") + + # 加载配置 + config_path = os.path.join(os.path.dirname(__file__), "config.toml") + self._commands = self._config.get("AIAutoResponse", {}).get("command", ["ai介入", "ai对话", "ai自动回复"]) + self.command_format = self._config.get("AIAutoResponse", {}).get("command-format", "ai介入 开启/关闭") + self.enable = self._config.get("AIAutoResponse", {}).get("enable", True) + + # 从配置中获取DIFY API密钥 + self.dify_api_key = self._config.get("AIAutoResponse", {}).get("dify_api_key", "") + self.dify_api_url = self._config.get("AIAutoResponse", {}).get("dify_api_url", + "http://192.168.2.240/v1/chat-messages") + + # 初始化介入机器人 + self.intervention_bot = InterventionBot(config_path) + + self.LOG.info(f"[{self.name}] 插件初始化完成,指令:{self._commands}") + return True + + def start(self) -> bool: + """启动插件""" + self.LOG.info(f"[{self.name}] 插件已启动") + self.status = PluginStatus.RUNNING + return True + + def stop(self) -> bool: + """停止插件""" + self.LOG.info(f"[{self.name}] 插件已停止") + self.status = PluginStatus.STOPPED + return True + + def can_process(self, message: Dict[str, Any]) -> bool: + """检查是否可以处理该消息""" + if not self.enable: + return False + + content = str(message.get("content", "")).strip() + command = content.split(" ")[0] + roomid = message.get("roomid", "") + + # 如果是命令,直接处理 + if command in self._commands: + return True + + # 如果是群消息,且该群启用了自动回复,则处理 + if roomid and self.auto_response_enabled.get(roomid, False): + # 存储消息 + if roomid not in self.group_messages: + self.group_messages[roomid] = [] + + # 添加新消息 + self.group_messages[roomid].append({ + "timestamp": message.get("timestamp", ""), + "message": content, + "sender": message.get("sender", "") + }) + + # 限制消息数量 + if len(self.group_messages[roomid]) > self.max_messages: + self.group_messages[roomid] = self.group_messages[roomid][-self.max_messages:] + + # 判断是否需要介入 + messages = [msg["message"] for msg in self.group_messages[roomid]] + timestamp = message.get("timestamp", "") + + return self.intervention_bot.should_intervene(timestamp, content, messages) + + return False + + async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + """处理消息""" + content = str(message.get("content", "")).strip() + self.LOG.debug(f"插件执行: {self.name}:{content}") + command = content.split(" ")[0] + sender = message.get("sender") + roomid = message.get("roomid", "") + gbm: GroupBotManager = message.get("gbm") + bot: WechatAPIClient = message.get("bot") + # 检查权限 + if roomid and gbm.get_group_permission(roomid, Feature.AI_AUTO) == PermissionStatus.DISABLED: + return False, "没有权限" + # 处理命令 + if command in self._commands: + # 检查命令格式 + if len(content.split(" ")) == 1: + await bot.send_text_message((roomid if roomid else sender), f"❌命令格式错误!\n{self.command_format}", + sender) + return False, "命令格式错误" + + # 提取参数 + param = content[len(command):].strip() + + if param == "开启": + self.auto_response_enabled[roomid] = True + await bot.send_text_message(roomid, "✅AI自动对话已开启", sender) + return True, "开启成功" + elif param == "关闭": + self.auto_response_enabled[roomid] = False + await bot.send_text_message(roomid, "❌AI自动对话已关闭", sender) + return True, "关闭成功" + else: + await bot.send_text_message(roomid, f"❌参数错误!\n{self.command_format}", sender) + return False, "参数错误" + + # 处理自动回复 + try: + # 获取最近的消息 + messages = [msg["message"] for msg in self.group_messages[roomid]] + timestamp = message.get("timestamp", "") + + # 生成回复 + response = self._generate_response_with_dify(content, messages) + if response: + # 发送回复 + await bot.send_text_message(roomid, response, sender) + return True, "自动回复成功" + + except Exception as e: + self.LOG.error(f"处理AI自动对话出错: {e}") + return False, f"处理出错: {e}" + + def _generate_response_with_dify(self, message: str, messages: List[str]) -> str: + """使用DIFY API生成自动回复内容""" + try: + # 检测话题类型 + topic_type = self.intervention_bot.detect_topic(message) + + # 构建上下文消息 + context_messages = messages[-5:] if len(messages) > 5 else messages + context = "\n".join(context_messages) + + # 构建提示词 + prompt = f"请根据以下群聊上下文,生成一个自然、友好的回复。\n\n上下文:\n{context}\n\n当前话题类型:{topic_type or '一般聊天'}\n\n请生成回复:" + + # 调用DIFY API + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.dify_api_key}" + } + + payload = { + "inputs": {}, + "query": prompt, + "response_mode": "blocking", + "user": "ai_auto_response" + } + + response = requests.post(self.dify_api_url, headers=headers, json=payload) + + if response.status_code == 200: + result = response.json() + return result.get("answer", "") + else: + self.LOG.error(f"DIFY API调用失败: {response.status_code} - {response.text}") + return "" + + except Exception as e: + self.LOG.error(f"生成回复出错: {e}") + return "" diff --git a/utils/robot_cmd/robot_command.py b/utils/robot_cmd/robot_command.py index 4336dec..3bd2e77 100644 --- a/utils/robot_cmd/robot_command.py +++ b/utils/robot_cmd/robot_command.py @@ -47,6 +47,7 @@ class Feature(Enum): NEWS = 20, "全球政治经济新闻" WEATHER = 21, "天气查询[上海天气 天气上海]" JD_TOKEN = 22, "JD_京豆token设置[设置京东 pt_key=xxx;pt_pin=xxx; 备注名称]" + AI_AUTO = 23, "仿真对话" def __new__(cls, value, description): obj = object.__new__(cls)