diff --git a/docs/README.md b/docs/README.md index f291c46..feaa4f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ # XYBot 微信机器人
- +
XYBot是一个可运行于Linux和Windows的基于Hook的微信机器人。😊 具有高度可自定义性,支持自我编写插件。🚀 @@ -21,7 +21,7 @@ XYBot详细的部署教程可以在项目的Wiki中找到。📚 同时,XYBot @@ -151,7 +151,7 @@ python start.py #### 用的什么微信版本?🤔️ -3.9.10.27😄 +3.9.11.25😄 #### 最长能运行多久?🤔️ diff --git a/docs/xybot_logo_32x32.png b/docs/xybot_logo_32x32.png index cd006f7..90ddfaf 100644 Binary files a/docs/xybot_logo_32x32.png and b/docs/xybot_logo_32x32.png differ diff --git "a/docs/zh-cn/XYBot\345\212\237\350\203\275\344\273\213\347\273\215.md" "b/docs/zh-cn/XYBot\345\212\237\350\203\275\344\273\213\347\273\215.md" index 100b7c2..41ef7f2 100644 --- "a/docs/zh-cn/XYBot\345\212\237\350\203\275\344\273\213\347\273\215.md" +++ "b/docs/zh-cn/XYBot\345\212\237\350\203\275\344\273\213\347\273\215.md" @@ -1,3 +1,5 @@ +# 本文档已过期 + # XYBot功能介绍 这一页介绍了所有官方XYBot微信机器人用户可使用的功能、命令。 diff --git "a/docs/zh-cn/XYBot\346\217\222\344\273\266\347\274\226\345\206\231.md" "b/docs/zh-cn/XYBot\346\217\222\344\273\266\347\274\226\345\206\231.md" index c026d1c..fa1cbd0 100644 --- "a/docs/zh-cn/XYBot\346\217\222\344\273\266\347\274\226\345\206\231.md" +++ "b/docs/zh-cn/XYBot\346\217\222\344\273\266\347\274\226\345\206\231.md" @@ -1,3 +1,5 @@ +# 本文档已过期 + # XYBot插件编写 这一页讲述了如何编写一个XYBot插件 diff --git "a/docs/zh-cn/XYBot\347\232\204\350\256\276\347\275\256.md" "b/docs/zh-cn/XYBot\347\232\204\350\256\276\347\275\256.md" index 98ac68a..9abc575 100644 --- "a/docs/zh-cn/XYBot\347\232\204\350\256\276\347\275\256.md" +++ "b/docs/zh-cn/XYBot\347\232\204\350\256\276\347\275\256.md" @@ -2,15 +2,15 @@ 这一页写了如何设置XYBot。 -本篇适用于`XYBot v2.0.0`。 +本篇适用于`XYBot v2.2.0`。 ## 配置主设置 -这是`XYBot v2.0.0`的主设置: +这是`XYBot v2.2.0`的主设置: ```yaml -#Version v2.0.0 -bot_version: "v2.0.0" +#Version v2.2.0 +bot_version: "v2.2.0" # XYBot主设置 diff --git a/docs/zh-cn/_coverpage.md b/docs/zh-cn/_coverpage.md index c540934..9ef7dcc 100644 --- a/docs/zh-cn/_coverpage.md +++ b/docs/zh-cn/_coverpage.md @@ -1,4 +1,4 @@ -![logo](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/v0.0.7/logo/xybot_logo_small.png?raw=true) +![logo](https://github.com/HenryXiaoYang/HXY_Readme_Images/blob/main/XYBot/logo/xybot_logo_medium.png?raw=true) # XYBot v2.0.0 diff --git a/main_config.yml b/main_config.yml index 1b12e33..764a6ce 100644 --- a/main_config.yml +++ b/main_config.yml @@ -1,5 +1,5 @@ -#Version v2.0.0 -bot_version: "v2.0.0" +#Version v2.2.0 +bot_version: "v2.2.0" # XYBot主设置 diff --git a/plugins/command/bot_status.py b/plugins/command/bot_status.py index 1e002ba..e8bf35f 100644 --- a/plugins/command/bot_status.py +++ b/plugins/command/bot_status.py @@ -98,3 +98,4 @@ async def run(self, bot: client.Wcf, recv: XYBotWxMsg): out_message = f"-----XYBot-----\n{self.status_message}\nBot version: {self.bot_version}\n{base64.b64decode(a).decode('utf-8')}" logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') bot.send_text(out_message, recv.roomid) # 发送 + bot.send_pat_msg(recv.roomid, recv.sender) # 发送拍一拍消息 diff --git a/plugins/command/dalle3.py b/plugins/command/dalle3.py index d38d602..87ecec5 100644 --- a/plugins/command/dalle3.py +++ b/plugins/command/dalle3.py @@ -83,6 +83,7 @@ async def run(self, bot: client.Wcf, recv: XYBotWxMsg): if user_wxid not in self.admins and self.db.get_whitelist(user_wxid) == 0: # 如果用户不是管理员或者白名单,扣积分 self.db.add_points(user_wxid, -self.price) await self.send_friend_or_group(bot, recv, f"-----XYBot-----\n🎉图片生成完毕,已扣除 {self.price} 点积分!🙏") + bot.send_pat_msg(recv.roomid, user_wxid) # 拍一拍 bot.send_image(image_path, recv.roomid) logger.info(f'[发送图片]{image_path}| [发送到] {recv.roomid}') diff --git a/plugins/command/lucky_draw.py b/plugins/command/lucky_draw.py index 7e7a753..b0f9fdd 100644 --- a/plugins/command/lucky_draw.py +++ b/plugins/command/lucky_draw.py @@ -140,6 +140,7 @@ async def run(self, bot: client.Wcf, recv: XYBotWxMsg): ) # 组建信息 await self.send_friend_or_group(bot, recv, message) # 发送 + bot.send_pat_msg(recv.roomid, target_wxid) # 发送拍一拍消息 else: await self.send_friend_or_group(bot, recv, error) # 发送错误 diff --git a/plugins/command/points_trade.py b/plugins/command/points_trade.py index 2b125e9..b74c399 100644 --- a/plugins/command/points_trade.py +++ b/plugins/command/points_trade.py @@ -45,6 +45,7 @@ async def run(self, bot: client.Wcf, recv: XYBotWxMsg): self.db.safe_trade_points(trader_wxid, target_wxid, points_num) # 记录日志和发送成功信息 await self.log_and_send_success_message(bot, roomid, trader_wxid, target_wxid, points_num) + bot.send_pat_msg(roomid, target_wxid) else: await self.log_and_send_error_message(bot, roomid, trader_wxid, error_message) # 记录日志和发送错误信息 else: diff --git a/plugins/command/random_picture.py b/plugins/command/random_picture.py index deef422..8a1f1b3 100644 --- a/plugins/command/random_picture.py +++ b/plugins/command/random_picture.py @@ -43,6 +43,7 @@ async def run(self, bot: client.Wcf, recv: XYBotWxMsg): logger.info(f'[发送信息](随机图图图片) {cache_path}| [发送到] {recv.roomid}') bot.send_image(os.path.abspath(cache_path), recv.roomid) # 发送图片 + bot.send_pat_msg(recv.roomid, recv.sender) # 发送拍一拍消息 except Exception as error: out_message = f"-----XYBot-----\n出现错误❌!{error}" diff --git a/plugins/command/random_picture_link.py b/plugins/command/random_picture_link.py deleted file mode 100644 index e9d9c53..0000000 --- a/plugins/command/random_picture_link.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2024. Henry Yang -# -# This program is licensed under the GNU General Public License v3.0. - -import re - -import aiohttp -import yaml -from loguru import logger -from wcferry import client - -from utils.plugin_interface import PluginInterface -from wcferry_helper import XYBotWxMsg - - -class random_picture_link(PluginInterface): - def __init__(self): - config_path = "plugins/command/random_picture_link.yml" - with open(config_path, "r", encoding="utf-8") as f: # 读取设置 - config = yaml.safe_load(f.read()) - - self.random_pic_link_url = config["random_pic_link_url"] # 随机图片api url - self.link_count = config["link_count"] # 链接数量 - - async def run(self, bot: client.Wcf, recv: XYBotWxMsg): - recv.content = re.split(" |\u2005", recv.content) # 拆分消息 - - try: - out_message = "-----XYBot-----\n❓❓❓\n" - - conn_ssl = aiohttp.TCPConnector(verify_ssl=False) - for _ in range(self.link_count): - async with aiohttp.request( - "GET", url=self.random_pic_link_url, connector=conn_ssl - ) as req: - out_message += f"❓: {req.url}\n" - await conn_ssl.close() - - logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') # 发送信息 - bot.send_text(out_message, recv.roomid) # 发送 - - except Exception as error: - out_message = f"-----XYBot-----\n出现错误❌!{error}" - logger.error(error) - logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') - bot.send_text(out_message, recv.roomid) diff --git a/plugins/command/random_picture_link.yml b/plugins/command/random_picture_link.yml deleted file mode 100644 index fcd3677..0000000 --- a/plugins/command/random_picture_link.yml +++ /dev/null @@ -1,6 +0,0 @@ -keywords: [ "随机链接" ] -plugin_name: "random_picture_link" - -random_pic_link_url: "https://api.yimian.xyz/img?type=moe" - -link_count: 3 \ No newline at end of file diff --git a/plugins/command/red_packet.py b/plugins/command/red_packet.py index a546ae1..ab9321a 100644 --- a/plugins/command/red_packet.py +++ b/plugins/command/red_packet.py @@ -135,7 +135,7 @@ async def grab_red_packet(self, bot: client.Wcf, recv: XYBotWxMsg): error = "-----XYBot-----\n❌红包只能在群里抢!" elif red_packet_grabber in self.red_packets[req_captcha]["grabbed"]: error = "-----XYBot-----\n⚠️你已经抢过这个红包了!" - elif self.red_packets[req_captcha].sender == red_packet_grabber: + elif self.red_packets[req_captcha]["sender"] == red_packet_grabber: error = "-----XYBot-----\n❌不能抢自己的红包!" if not error: @@ -156,6 +156,7 @@ async def grab_red_packet(self, bot: client.Wcf, recv: XYBotWxMsg): # 组建信息 out_message = f"-----XYBot-----\n🧧恭喜 {red_packet_grabber_nick} 抢到了 {grabbed_points} 点积分!" await self.send_friend_or_group(bot, recv, out_message) + bot.send_pat_msg(recv.roomid, red_packet_grabber) # 发送拍一拍消息 # 判断是否抢完 if not self.red_packets[req_captcha]["list"]: diff --git a/plugins/command/sign_in.py b/plugins/command/sign_in.py index f8105bb..840ab4c 100644 --- a/plugins/command/sign_in.py +++ b/plugins/command/sign_in.py @@ -68,6 +68,9 @@ async def run(self, bot: client.Wcf, recv: XYBotWxMsg): logger.info(f"[发送@信息]{out_message}| [发送到] {recv.roomid}") bot.send_text(out_message, recv.roomid, sign_wxid) + bot.send_pat_msg(recv.roomid, sign_wxid) + logger.info(f"[发送拍一拍] 拍了拍: {sign_wxid} | 发送到: {recv.roomid}") + def signstat_check(self, signstat): # 检查签到状态 signstat = "20000101" if signstat in ["0", "1"] else signstat last_sign_date = datetime.strptime(signstat, "%Y%m%d").date() diff --git a/plugins/join_group/join_notification.py b/plugins/join_group/join_notification.py new file mode 100644 index 0000000..901c47c --- /dev/null +++ b/plugins/join_group/join_notification.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024. Henry Yang +# +# This program is licensed under the GNU General Public License v3.0. +import os +import re + +from loguru import logger +from wcferry import client + +from utils.plugin_interface import PluginInterface +from wcferry_helper import XYBotWxMsg + + +class join_notification(PluginInterface): + def __init__(self): + self.logo_path = os.path.abspath("resources/XYBotLogo.png") + + async def run(self, bot: client.Wcf, recv: XYBotWxMsg): + join_group_msg = recv.content + + # 邀请进来的 + if "邀请" in join_group_msg: + # 通过正则表达式提取邀请者的名字 + invite_pattern = r'"([^"]+)"邀请"([^"]+)"加入了群聊' + match = re.search(invite_pattern, join_group_msg) + + if match: + joiner = match.group(2) + await self.send_welcome(bot, recv.roomid, joiner) + + async def send_welcome(self, bot: client.Wcf, roomid: str, joiner: str): + out_message = f"-------- XYBot ---------\n👏欢迎新成员 {joiner} 加入本群!⭐️\n⚙️输入 菜单 获取玩法哦😄" + logger.info(f'[发送信息]{out_message}| [发送到] {roomid}') + bot.send_text(out_message, roomid) diff --git a/plugins/mention/mention_gpt.py b/plugins/mention/mention_gpt.py new file mode 100644 index 0000000..3a7ca5f --- /dev/null +++ b/plugins/mention/mention_gpt.py @@ -0,0 +1,219 @@ +# Copyright (c) 2024. Henry Yang +# +# This program is licensed under the GNU General Public License v3.0. + +import base64 +import json +import os +import time + +import yaml +from loguru import logger +from openai import AsyncOpenAI +from wcferry import client + +from utils.database import BotDatabase +from utils.plugin_interface import PluginInterface +from wcferry_helper import XYBotWxMsg + + +class mention_gpt(PluginInterface): + def __init__(self): + config_path = "plugins/mention/mention_gpt.yml" + with open(config_path, "r", encoding="utf-8") as f: # 读取设置 + config = yaml.safe_load(f.read()) + + self.gpt_version = config["gpt_version"] # gpt版本 + self.gpt_point_price = config["gpt_point_price"] # gpt使用价格(单次) + self.gpt_max_token = config["gpt_max_token"] # gpt 最大token + self.gpt_temperature = config["gpt_temperature"] # gpt 温度 + + self.model_name = config["model_name"] # 模型名称 + self.image_quality = config["image_quality"] # 图片质量 + self.image_size = config["image_size"] # 图片尺寸 + self.image_price = config["image_price"] # 图片价格 + + self.max_possible_points = self.gpt_point_price * 2 + self.image_price # 消耗积分极限 + + main_config_path = "main_config.yml" + with open(main_config_path, "r", encoding="utf-8") as f: # 读取设置 + main_config = yaml.safe_load(f.read()) + + self.admins = main_config["admins"] # 获取管理员列表 + + self.openai_api_base = main_config["openai_api_base"] # openai api 链接 + self.openai_api_key = main_config["openai_api_key"] # openai api 密钥 + + sensitive_words_path = "sensitive_words.yml" # 加载敏感词yml + with open(sensitive_words_path, "r", encoding="utf-8") as f: # 读取设置 + sensitive_words_config = yaml.safe_load(f.read()) + self.sensitive_words = sensitive_words_config["sensitive_words"] # 敏感词列表 + + self.db = BotDatabase() + + async def run(self, bot: client.Wcf, recv: XYBotWxMsg): + user_wxid = recv.sender + gpt_request_message = recv.content + + error = "" + if self.db.get_points( + user_wxid) < self.max_possible_points and user_wxid not in self.admins and not self.db.get_whitelist( + user_wxid): # 积分不够 + error = f"本功能可消耗最多 {self.max_possible_points} 点积分,您的积分不足,无法使用GPT功能!⚠️" + elif not self.senstitive_word_check(gpt_request_message): # 有敏感词 + error = "您的问题中包含敏感词,请重新输入!⚠️" + + if error: + await self.send_friend_or_group(bot, recv, error) + return + + chat_completion = await self.chatgpt(gpt_request_message) + + if not chat_completion[0]: + logger.error(str(chat_completion[1])) + out_message = f"出现错误,请稍后再试!⚠️\n错误信息:\n{str(chat_completion[1])}" + await self.send_friend_or_group(bot, recv, out_message) + + chat_completion = chat_completion[1] + if chat_completion.choices[0].message.tool_calls: + tool_call = chat_completion.choices[0].message.tool_calls[0] + prompt = json.loads(tool_call.function.arguments).get("prompt") + + success = await self.generate_and_send_picture(prompt, bot, recv) + + function_call_result_message = { + "role": "tool", + "content": json.dumps({ + "prompt": prompt, + "success": success, + }), + "tool_call_id": tool_call.id + } + + chat_completion_2 = await self.function_call_result_to_gpt(gpt_request_message, chat_completion, function_call_result_message) + await self.send_friend_or_group(bot, recv, chat_completion_2.choices[0].message.content) + + if user_wxid not in self.admins and not self.db.get_whitelist(user_wxid): + minus_points = self.gpt_point_price * 2 + self.image_price + self.db.add_points(user_wxid, minus_points * -1) + + else: + await self.send_friend_or_group(bot, recv, chat_completion.choices[0].message.content) + if user_wxid not in self.admins and not self.db.get_whitelist(user_wxid): + self.db.add_points(user_wxid, self.gpt_point_price * -1) + + async def chatgpt(self, gpt_request_message): + client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) + try: + tools = [ + { + "type": "function", + "function": { + "name": "generate_and_send_picture", + "description": "Generate an image using the user's description. Call this when the user requests an image, for example when a user asks 'Can you show me a picture of a cat?'. The function returns true on sucess.", + "parameters": { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "The prompt of the image to generate." + }, + + }, + "required": ["prompt"], + "additionalProperties": False, + } + } + } + ] + chat_completion = await client.chat.completions.create( + messages=[ + { + "role": "system", + "content": "You are a helpful, creative, clever, and very friendly assistant. You output in plain text instead of markdown.", + }, + { + "role": "user", + "content": gpt_request_message, + } + ], + tools=tools, + model=self.gpt_version, + temperature=self.gpt_temperature, + max_tokens=self.gpt_max_token, + parallel_tool_calls=False + ) + return True, chat_completion + except Exception as error: + return False, error + + async def function_call_result_to_gpt(self, gpt_request_message, chat_completion, function_call_result_message): + client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) + message_payload = [ + { + "role": "system", + "content": "You are a helpful, creative, clever, and very friendly assistant. You output in plain text instead of markdown.", + }, + { + "role": "user", + "content": gpt_request_message, + }, + chat_completion.choices[0].message.dict(), + function_call_result_message + ] + response_chat_completion = await client.chat.completions.create( + messages=message_payload, + model=self.gpt_version, + temperature=self.gpt_temperature, + max_tokens=self.gpt_max_token, + ) + return response_chat_completion + + async def generate_and_send_picture(self, prompt: str, bot: client.Wcf, recv: XYBotWxMsg) -> bool: + try: + await self.send_friend_or_group(bot, recv, f"⚙️生成图片中...") + save_path = await self.dalle3(prompt) + bot.send_image(save_path, recv.roomid) + logger.info(f"发送图片: {save_path}") + return True + except Exception as error: + logger.error(f"Error: {error}") + return False + + async def dalle3(self, prompt): # 返回生成的图片的绝对路径,报错的话返回错误 + logger.info(f"开始生成图片: {prompt}") + client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.openai_api_base) + try: + image_generation = await client.images.generate( + prompt=prompt, + model=self.model_name, + n=1, + response_format="b64_json", + quality=self.image_quality, + size=self.image_size) + + image_b64decode = base64.b64decode(image_generation.data[0].b64_json) + save_path = os.path.abspath(f"resources/cache/dalle3_{time.time_ns()}.png") + with open(save_path, "wb") as f: + f.write(image_b64decode) + except Exception as e: + return e + + logger.info(f"生成图片 {prompt} 成功: {save_path}") + return save_path + + def senstitive_word_check(self, message): # 检查敏感词 + for word in self.sensitive_words: + if word in message: + return False + return True + + async def send_friend_or_group(self, bot: client.Wcf, recv: XYBotWxMsg, out_message="null"): + if recv.from_group(): # 判断是群还是私聊 + out_message = f"@{self.db.get_nickname(recv.sender)}\n{out_message}" + logger.info(f'[发送@信息]{out_message}| [发送到] {recv.roomid}') + bot.send_text(out_message, recv.roomid, recv.sender) # 发送@信息 + + else: + logger.info(f'[发送信息]{out_message}| [发送到] {recv.roomid}') + bot.send_text(out_message, recv.roomid) # 发送 diff --git a/plugins/mention/mention_gpt.yml b/plugins/mention/mention_gpt.yml new file mode 100644 index 0000000..d0e4ef8 --- /dev/null +++ b/plugins/mention/mention_gpt.yml @@ -0,0 +1,15 @@ +gpt_point_price: 3 + +# 调用函数需要特定的版本 https://platform.openai.com/docs/guides/function-calling#which-models-support-function-calling +gpt_version: 'gpt-4o-mini' +gpt_max_token: 1000 +gpt_temperature: 0.5 + +# 每次生成图片消耗的积分 +image_price: 5 + +# https://platform.openai.com/docs/api-reference/images/create +model_name: "dall-e-3" # 模型名 +image_quality: "standard" # 生成质量,更好的质量可设置为"hd" +image_size: "1024x1024" # 图片大小 +image_style: "vivid" # 图片风格 \ No newline at end of file diff --git a/plugins/text/private_chatgpt.py b/plugins/text/private_chatgpt.py index 948fa0e..9f5bb63 100644 --- a/plugins/text/private_chatgpt.py +++ b/plugins/text/private_chatgpt.py @@ -79,7 +79,7 @@ async def run(self, bot: client.Wcf, recv: XYBotWxMsg): if gpt_answer[0]: # 如果没有错误 bot.send_text(gpt_answer[1], wxid) # 发送回答 logger.info(f'[发送信息]{gpt_answer[1]}| [发送到] {wxid}') - if wxid not in self.admins or not self.db.get_whitelist(wxid): + if wxid not in self.admins and not self.db.get_whitelist(wxid): self.db.add_points(wxid, -self.private_chat_gpt_price) # 扣除积分,管理员不扣 else: out_message = f"出现错误⚠️!\n{gpt_answer[1]}" # 如果有错误,发送错误信息 diff --git a/requirements.txt b/requirements.txt index dd13b02..0c1fbcb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,9 +3,9 @@ PyYAML~=6.0.1 loguru~=0.7.2 wcferry~=39.2.4.0 pytz~=2024.1 -requests~=2.31.0 +requests~=2.32.0 openai~=1.35.14 -aiohttp~=3.9.5 +aiohttp~=3.10.11 beautifulsoup4~=4.12.3 pillow~=10.3.0 captcha~=0.5.0 diff --git a/resources/XYBotLogo.svg b/resources/XYBotLogo.svg new file mode 100644 index 0000000..5525991 --- /dev/null +++ b/resources/XYBotLogo.svg @@ -0,0 +1,133 @@ + + + + + +