├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── PyroEdgeGptBot.py ├── README.md ├── README_zh.md ├── assets ├── placeholder0.png ├── placeholder1.png ├── placeholder2.png └── placeholder3.png ├── base64_encode_cookie.py ├── config.py ├── logs └── .keep └── requirements.txt /.env.example: -------------------------------------------------------------------------------- 1 | ########################## 2 | # Required Values 必选配置 3 | ########################## 4 | # API_ID and API_KEY, get from: https://my.telegram.org/apps # API_ID 和 API_KEY, 从 https://my.telegram.org/apps 获取 5 | API_ID=1234567 6 | API_KEY=abcdefg2hijk5lmnopq8rstuvwxyz9 7 | # Telegram bot token, from @botfather # TG 机器人的 Token, 自 @botfather 获取 8 | BOT_TOKEN=123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 9 | # Allowed User's ids, use comma separation, set "*" will allow everyone # 允许的用户 id ,使用逗号分隔,设置为 '*' 则所有人可用 10 | ALLOWED_USER_IDS=112113115,567568569,998997996 11 | # SuperAdmin User's ids, use comma separation, if ALLOWED_USER_IDS has been setted to "*", this is required # 允许的超级用户 id ,使用逗号分隔,如果已将 ALLOWED_USER_IDS 设置为 "*", 那么必须设置此项 12 | SUPER_USER_IDS=112113115,567568569 13 | 14 | # The cookie file path. You can chat with AI without Cookie, but Image generation still needs Cookie. # cookie 数据文件,可以不设置,但是无法使用图片生成 15 | COOKIE_FILE=./cookie.json 16 | 17 | ########################## 18 | # Optional Values 可选配置 19 | ########################## 20 | # Bing AI API proxy, http/socks5 scheme, for example: socks5://127.0.0.1:1080 # Bing AI API 代理, http/socks5 协议, 例如 socks5://127.0.0.1:1080 21 | PROXY_BING="" 22 | # If user is not in Allowed list, this message will be sent (Can be empty String). # 未允许用户访问时的提示信息,可为空 23 | # Supports magic word: %user_id% (Will replaced with the user's id who acceessed). # 魔法变量 %user_id% ,会被自动替换成访问的用户的 id 24 | NOT_ALLOW_INFO="⚠️You(%user_id%) are not authorized to use this bot⚠️" 25 | # The default bot name, show in welcome/help message # 默认机器人名字,在发送欢迎/帮助信息时用 26 | BOT_NAME="PyroEdgeGpt" 27 | # Set suggest message mode. Available value: "callbackquery", "replykeyboard", "copytext" # 设置 bot 建议消息模式,可选值 "callbackquery", "replykeyboard", "copytext" 28 | SUGGEST_MODE=callbackquery 29 | # Set Bing AI temperature, Available value: "creative", "balanced", "precise" # 设置 Bing AI 聊天温度,可用值: "creative", "balanced", "precise" 30 | DEFAULT_CONVERSATION_STYLE_TYPE=creative 31 | # Set bot response type, Available value: "normal", "stream" # 设置 tg 机器人回复模式,可选值: "normal", "stream" 32 | RESPONSE_TYPE=stream 33 | # Set an interval between editing messages to avoid triggering API limits. (unit: second) # 设置 stream 模式编辑消息频率,避免触发 api 限制 (单位:秒) 34 | STREAM_INTERVAL=2 35 | # Set log level, Available value: "DEBUG", "INFO", "WARNING", "ERROR" # 设置日志等级 36 | LOG_LEVEL=WARNING 37 | # Set log timezone # 设置日志时区 38 | LOG_TIMEZONE=Asia/Shanghai 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Project special 132 | cookie.json 133 | *.session 134 | *.session-journal 135 | /logs/*.log 136 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | WORKDIR /PyroEdgeGptBot 4 | 5 | COPY . /PyroEdgeGptBot 6 | 7 | ENV API_ID=YOUR_API_ID \ 8 | API_KEY=YOUR_API_HASH \ 9 | BOT_TOKEN=YOUR_BOT_TOKEN \ 10 | ALLOWED_USER_IDS="*" \ 11 | SUPER_USER_IDS=112113115,567568569 \ 12 | COOKIE_BASE64="" \ 13 | NOT_ALLOW_INFO="⚠️You(%user_id%) are not authorized to use this bot⚠️" \ 14 | BOT_NAME="PyroEdgeGpt" \ 15 | SUGGEST_MODE=callbackquery \ 16 | DEFAULT_CONVERSATION_STYLE_TYPE=creative \ 17 | RESPONSE_TYPE=stream \ 18 | STREAM_INTERVAL=2 \ 19 | LOG_LEVEL=WARNING \ 20 | LOG_TIMEZONE=Asia/Shanghai 21 | 22 | RUN apk add --no-cache --virtual .build-deps gcc musl-dev libffi-dev git \ 23 | && pip install --no-cache-dir -r requirements.txt \ 24 | && apk del .build-deps 25 | 26 | CMD [ "python", "PyroEdgeGptBot.py" ] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 A lucky guy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PyroEdgeGptBot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import sys, importlib 5 | import json, copy 6 | import time 7 | from typing import Dict, List 8 | import pytz 9 | import logging 10 | import asyncio 11 | import contextlib 12 | 13 | import EdgeGPT.EdgeGPT as EdgeGPT 14 | 15 | # import py3langid as langid 16 | from logging.handlers import TimedRotatingFileHandler 17 | from datetime import datetime 18 | from datetime import time as datetime_time 19 | 20 | from BingImageCreator import ImageGenAsync 21 | 22 | from pyrogram import Client, filters 23 | from pyrogram.handlers import MessageHandler 24 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup, \ 25 | ReplyKeyboardRemove, InlineQueryResultPhoto, InlineQueryResultArticle, InputTextMessageContent, \ 26 | InputMediaPhoto 27 | 28 | from config import BAD_CONFIG_ERROR, API_ID, API_KEY, BOT_TOKEN, ALLOWED_USER_IDS, SUPER_USER_IDS, COOKIE_FILE, \ 29 | PROXY_BING, NOT_ALLOW_INFO, BOT_NAME, SUGGEST_MODE, DEFAULT_CONVERSATION_STYLE_TYPE, RESPONSE_TYPE, STREAM_INTERVAL, \ 30 | LOG_LEVEL, LOG_TIMEZONE 31 | 32 | RESPONSE_TEMPLATE = """{msg_main} 33 | {msg_ref} 34 | - - - - - - - - - 35 | {msg_throttling} 36 | """ 37 | 38 | # 设置日志记录级别和格式,创建 logger 39 | class MyFormatter(logging.Formatter): 40 | def formatTime(self, record, datefmt=None): 41 | dt = datetime.fromtimestamp(record.created, tz=pytz.timezone(LOG_TIMEZONE)) 42 | if datefmt: 43 | return dt.strftime(datefmt) 44 | else: 45 | return dt.isoformat() 46 | 47 | myformatter = MyFormatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 48 | # 配置日志文件,使用 utc=True 和 atTime=atTime 根据时区设置日志文件(thanks for Bing AI) 49 | dt = datetime.now(pytz.timezone(LOG_TIMEZONE)) 50 | utc_offset = dt.utcoffset() 51 | atTime = (datetime.combine(dt, datetime_time(0)) - utc_offset).time() 52 | file_handler = TimedRotatingFileHandler( 53 | "logs/" + __file__.split("/")[-1].split("\\")[-1].split(".")[0] + ".log", 54 | when="MIDNIGHT", 55 | interval=1, 56 | backupCount=7, # 保留 7 天备份 57 | utc=True, 58 | atTime=atTime 59 | ) 60 | file_handler.suffix = '%Y-%m-%d.log' 61 | file_handler.setFormatter(myformatter) 62 | file_handler.setLevel(logging.DEBUG) # 将文件日志记录级别设置为 DEBUG 63 | # 配置屏幕日志 64 | screen_handler = logging.StreamHandler() 65 | screen_handler.setFormatter(myformatter) 66 | screen_handler.setLevel(LOG_LEVEL.upper()) 67 | 68 | logging.basicConfig( 69 | level=LOG_LEVEL.upper(), 70 | handlers=[file_handler, screen_handler] 71 | ) 72 | logger = logging.getLogger() 73 | 74 | 75 | def check_conversation_style(style): 76 | if style in EdgeGPT.ConversationStyle.__members__: 77 | return True 78 | return False 79 | 80 | if not check_conversation_style(DEFAULT_CONVERSATION_STYLE_TYPE): 81 | raise BAD_CONFIG_ERROR(f"DEFAULT_CONVERSATION_STYLE_TYPE is invalid") 82 | 83 | # 使用 BOT_TOKEN 登陆 tg 机器人 84 | pyro = Client("PyroEdgeGpt", api_id=int(API_ID), api_hash=API_KEY, bot_token=BOT_TOKEN) 85 | 86 | BING_COOKIE = None 87 | with contextlib.suppress(Exception): # 如果文件不存在,则 BING_COOKIE 为 None 88 | with open(COOKIE_FILE, 'r', encoding="utf-8") as file: 89 | BING_COOKIE = json.load(file) 90 | logger.info(f"BING_COOKIE loaded from {COOKIE_FILE}") 91 | 92 | # 初始化 bing AI 会话字典(存储格式 key: user_id, value: edge_bot_config) 93 | EDGES = {} 94 | FILE_HANDLE_USERS = {} 95 | 96 | if ALLOWED_USER_IDS != None: 97 | tmpLoop = asyncio.get_event_loop() 98 | for user_id in ALLOWED_USER_IDS: 99 | EDGES[user_id] = { 100 | "bot": tmpLoop.run_until_complete(EdgeGPT.Chatbot.create(proxy=PROXY_BING, cookies=BING_COOKIE)), # 共用一个 cookie.json 文件 101 | "style": EdgeGPT.ConversationStyle[DEFAULT_CONVERSATION_STYLE_TYPE], 102 | "response": RESPONSE_TYPE, 103 | "interval": STREAM_INTERVAL, 104 | "suggest": SUGGEST_MODE, 105 | "bot_name": BOT_NAME, 106 | "temp": {}, 107 | "images": {}, 108 | "cookies": None, 109 | "image_U": "" 110 | } 111 | else: 112 | logger.warning("Allow everyone mode") 113 | if BING_COOKIE is not None: 114 | logger.warning("You set BING_COOKIE to not None, but you allowed everyone to use this bot") 115 | USER_TEMPLATE = { 116 | "bot": {}, # 共用一个 cookie.json 文件 117 | "style": EdgeGPT.ConversationStyle[DEFAULT_CONVERSATION_STYLE_TYPE], 118 | "response": RESPONSE_TYPE, 119 | "interval": STREAM_INTERVAL, 120 | "suggest": SUGGEST_MODE, 121 | "bot_name": BOT_NAME, 122 | "temp": {}, 123 | "images": {}, 124 | "cookies": None, 125 | "image_U": "" 126 | } 127 | tmpLoop = asyncio.get_event_loop() 128 | for user_id in SUPER_USER_IDS: 129 | EDGES[user_id] = { 130 | "bot": tmpLoop.run_until_complete(EdgeGPT.Chatbot.create(proxy=PROXY_BING, cookies=BING_COOKIE)), # 共用一个 cookie.json 文件 131 | "style": EdgeGPT.ConversationStyle[DEFAULT_CONVERSATION_STYLE_TYPE], 132 | "response": RESPONSE_TYPE, 133 | "interval": STREAM_INTERVAL, 134 | "suggest": SUGGEST_MODE, 135 | "bot_name": BOT_NAME, 136 | "temp": {}, 137 | "images": {}, 138 | "cookies": None, 139 | "image_U": "" 140 | } 141 | 142 | # 创建自定义过滤器来判断用户是否拥有机器人访问权限 143 | def is_allowed_filter(): 144 | async def func(_, __, update): 145 | if ALLOWED_USER_IDS is not None: 146 | if hasattr(update, "from_user"): 147 | return int(update.from_user.id) in ALLOWED_USER_IDS 148 | if hasattr(update, "chat"): 149 | return int(update.chat.id) in ALLOWED_USER_IDS 150 | return False 151 | return True 152 | return filters.create(func) 153 | 154 | def check_inited(uid): 155 | if uid in EDGES.keys(): 156 | return True 157 | return False 158 | 159 | # start 命令提示信息 160 | @pyro.on_message(filters.command("start") & filters.private) 161 | async def start_handle(bot, update): 162 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 163 | github_link = "https://github.com/tom-snow/PyroEdgeGPTBot" 164 | keyboard = InlineKeyboardMarkup([ 165 | [InlineKeyboardButton("Star me on Github", url=github_link)], 166 | ]) 167 | # 不允许使用的用户返回不允许使用提示 168 | if ALLOWED_USER_IDS is not None and update.chat.id not in ALLOWED_USER_IDS: 169 | logger.warning(f"User [{update.chat.id}] is not allowed") 170 | not_allow_info = NOT_ALLOW_INFO.strip() 171 | if len(not_allow_info.strip()) == 0: 172 | return 173 | not_allow_info = not_allow_info.replace("%user_id%", str(update.chat.id)) 174 | await bot.send_message(chat_id=update.chat.id, text=not_allow_info, reply_markup=keyboard) 175 | return 176 | if not check_inited(update.chat.id): 177 | bot_name = BOT_NAME 178 | else: 179 | bot_name = EDGES[update.chat.id]["bot_name"] 180 | welcome_info = f"Hello, I'm {bot_name}. I'm a telegram bot of Bing AI.\nYou can send /help for more information.\n\n" 181 | if ALLOWED_USER_IDS is None and not check_inited(update.chat.id): 182 | logger.info(f"User [{update.chat.id}] not inited") 183 | welcome_info += "You should send command /new to initialize me first." 184 | # 返回欢迎消息 185 | await bot.send_message(chat_id=update.chat.id, text=welcome_info, reply_markup=keyboard) 186 | 187 | # help 命令提示信息 188 | @pyro.on_message(filters.command("help") & filters.private & is_allowed_filter()) 189 | async def help_handle(bot, update): 190 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 191 | # 帮助信息字符串 192 | if not check_inited(update.chat.id): 193 | bot_name = BOT_NAME 194 | else: 195 | bot_name = EDGES[update.chat.id]["bot_name"] 196 | help_text = f"Hello, I'm {bot_name}, a telegram bot of Bing AI\n" 197 | help_text += "\nAvailable commands:\n" 198 | help_text += "/start - Start the bot and show welcome message\n" 199 | help_text += "/help - Show this message\n" 200 | help_text += "/reset - Reset the bot, optional args: `creative`, `balanced`, `precise`. If this arg is not provided, keep it set before or default.\n" 201 | help_text += " Example: `/reset balanced`\n" 202 | help_text += "/new - Create new conversation. All same as /reset.\n" 203 | help_text += "/cookie - Set your own cookies. With argument `clear` to clear your cookies.\n" 204 | help_text += "/switch - Switch the conversation style.\n" 205 | help_text += "/interval - Set the stream mode message editing interval. (Unit: second)\n" 206 | help_text += "/suggest_mode - Set the suggest mode. Available arguments: `callbackquery`, `replykeyboard`, `copytext`\n" 207 | help_text += "/update - Update the EdgeGPT and reload the bot.\n" 208 | help_text += "/image_gen - Generate images using your custom prompt. Example: `/image_gen cute cats`\n" 209 | help_text += "\nInline Query:\n" 210 | help_text += "`@PyroEdgeGptBot g <prompt> %`. Example: `@PyroEdgeGptBot g cats %`\nGenerate images using your prompt 'cats'.\n" 211 | help_text += "\n\nPS: You should set your own cookie before using image generator. Otherwise, the image generator will not response.\n" 212 | await bot.send_message(chat_id=update.chat.id, text=help_text) 213 | 214 | # 新建/重置会话 215 | @pyro.on_message(filters.command(["new", "reset"]) & filters.private & is_allowed_filter()) 216 | async def reset_handle(bot, update): 217 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 218 | if not check_inited(update.chat.id): 219 | EDGES[update.chat.id] = copy.deepcopy(USER_TEMPLATE) 220 | bot_name = BOT_NAME 221 | try: 222 | global BING_COOKIE 223 | EDGES[update.chat.id]["bot"] = await EdgeGPT.Chatbot.create(proxy=PROXY_BING, cookies=BING_COOKIE) 224 | reply_text = f"{bot_name}: Initialized scussessfully." 225 | await update.reply(reply_text) 226 | return 227 | except Exception as e: 228 | logger.exception(f"Failed to initialize for user [{update.chat.id}]") 229 | del EDGES[update.chat.id] 230 | reply_text = f"{bot_name}: Failed to initialize.({e})" 231 | await update.reply(reply_text) 232 | return 233 | bot_name = EDGES[update.chat.id]["bot_name"] 234 | reply_text = f"{bot_name} has been reset." 235 | if len(update.command) > 1: 236 | arg = update.command[1] 237 | if check_conversation_style(arg): 238 | EDGES[update.chat.id]["style"] = arg 239 | reply_text += f"{bot_name}: Setted CONVERSATION_STYLE_TYPE to '{arg}'." 240 | logger.warning(f"User [{update.chat.id}] have set {arg}") 241 | else: 242 | await bot.send_message(chat_id=update.chat.id, text="Available arguments: `creative`, `balanced`, `precise`") 243 | return 244 | edge = EDGES[update.chat.id]["bot"] 245 | logger.info(f"Reset EdgeGPT for user [{update.chat.id}]") 246 | await edge.reset() 247 | await update.reply(reply_text) 248 | 249 | # 切换回复类型 250 | @pyro.on_message(filters.command("switch") & filters.private & is_allowed_filter()) 251 | async def set_response_handle(bot, update): 252 | if not check_inited(update.chat.id): 253 | await bot.send_message(chat_id=update.chat.id, text="Please initialize me first.") 254 | logger.error(f"User [{update.chat.id}] try to switch RESPONSE_TYPE but been rejected (not initialized).") 255 | return 256 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 257 | if EDGES[update.chat.id]["response"] == "normal": 258 | EDGES[update.chat.id]["response"] = "stream" 259 | else: 260 | EDGES[update.chat.id]["response"] = "normal" 261 | bot_name = EDGES[update.chat.id]["bot_name"] 262 | reply_text = f"{bot_name}: Switched RESPONSE_TYPE to '{EDGES[update.chat.id]['response']}'." 263 | await bot.send_message(chat_id=update.chat.id, text=reply_text) 264 | 265 | # 更新依赖 266 | @pyro.on_message(filters.command("update") & filters.private & is_allowed_filter()) 267 | async def set_update_handle(bot, update): 268 | if update.chat.id not in SUPER_USER_IDS: 269 | await bot.send_message(chat_id=update.chat.id, text="Not Allowed.") 270 | logger.error(f"User [{update.chat.id}] try to update EdgeGPT but been rejected (not initialized).") 271 | return 272 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 273 | bot_name = EDGES[update.chat.id]["bot_name"] 274 | msg = await bot.send_message(chat_id=update.chat.id 275 | , text=f"{bot_name}: Updateing [EdgeGPT](https://github.com/acheong08/EdgeGPT)." 276 | , disable_web_page_preview=True) 277 | # 关闭连接 278 | for user_id in EDGES: 279 | await EDGES[user_id]["bot"].close() 280 | # 更新&重载依赖 281 | python_path = sys.executable 282 | executor = await asyncio.create_subprocess_shell(f"{python_path} -m pip install -U EdgeGPT BingImageCreator" 283 | , stdout=asyncio.subprocess.PIPE 284 | , stderr=asyncio.subprocess.PIPE 285 | , stdin=asyncio.subprocess.PIPE) 286 | stdout, stderr = await executor.communicate() 287 | logger.info(f"[set_update_handle] stdout: {stdout.decode()}") 288 | result = "" 289 | edgegpt_old_version = "" 290 | edgegpt_new_version = "" 291 | image_old_version = "" 292 | image_new_version = "" 293 | for line in stdout.decode().split("\n"): # 解析日志 294 | # pkg_resources.get_distribution("BingImageCreator").version 295 | if "Successfully uninstalled EdgeGPT-" in line: 296 | edgegpt_old_version = line.replace("Successfully uninstalled EdgeGPT-", "").strip() 297 | if "Successfully uninstalled BingImageCreator-" in line: 298 | image_old_version = line.replace("Successfully uninstalled BingImageCreator-", "").strip() 299 | if "Successfully installed" in line: 300 | import re 301 | try: 302 | edgegpt_new_version = re.findall(r"(?<=EdgeGPT-)(\d+\.\d+\.\d+)", line)[0] 303 | except: 304 | logger.exception(f"Warn: Failed to parse EdgeGPT new version: {line}") 305 | try: 306 | image_new_version = re.findall(r"(?<=BingImageCreator-)(\d+\.\d+\.\d+)", line)[0] 307 | except: 308 | logger.exception(f"Warn: Failed to parse BingImageCreator new version: {line}") 309 | if edgegpt_old_version and edgegpt_new_version: 310 | result += f"[EdgeGPT](https://github.com/acheong08/EdgeGPT): {edgegpt_old_version} -> {edgegpt_new_version}\n" 311 | else: 312 | result += f"[EdgeGPT](https://github.com/acheong08/EdgeGPT): already the newest version.\n" 313 | if image_old_version and image_new_version: 314 | result += f"[BingImageCreator](https://github.com/acheong08/BingImageCreator): {image_old_version} -> {image_new_version}\n" 315 | else: 316 | result += f"[BingImageCreator](https://github.com/acheong08/BingImageCreator): already the newest version.\n" 317 | err = False 318 | if "WARNING" not in stderr.decode(): 319 | err = True 320 | if err: 321 | logger.error(f"[set_update_handle] stderr: {stderr.decode()}") 322 | result += stderr.decode() 323 | else: 324 | logger.warning(f"[set_update_handle] stderr: {stderr.decode()}") 325 | 326 | importlib.reload(EdgeGPT) 327 | # 重新连接 328 | for user_id in EDGES: 329 | cookie = EDGES[user_id]["cookies"] or BING_COOKIE 330 | EDGES[user_id]["bot"] = await EdgeGPT.Chatbot.create(proxy=PROXY_BING, cookies=cookie) 331 | EDGES[user_id]["style"] = EdgeGPT.ConversationStyle[DEFAULT_CONVERSATION_STYLE_TYPE] 332 | bot_name = EDGES[update.chat.id]["bot_name"] 333 | await msg.edit_text(f"{bot_name}: Updated!\n\n{result}", disable_web_page_preview=True) 334 | 335 | # 设置 用户cookie 336 | @pyro.on_message(filters.command("cookie") & filters.private & is_allowed_filter()) 337 | async def set_cookie_handle(bot, update): 338 | if not check_inited(update.chat.id): 339 | await bot.send_message(chat_id=update.chat.id, text="Please initialize me first.") 340 | logger.warning(f"User [{update.chat.id}] try to set cookie but been rejected (not initialized).") 341 | return 342 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 343 | left_time = 300 344 | bot_name = EDGES[update.chat.id]["bot_name"] 345 | if len(update.command) > 1 and update.command[1] == "clear": 346 | EDGES[update.chat.id]["cookies"] = "" 347 | EDGES[update.chat.id]["image_U"] = "" 348 | await bot.send_message(chat_id=update.chat.id, text=f"{bot_name}: Cookie cleared.") 349 | return 350 | msg_text = "{bot_name}: Please send a json file of your cookies in {left_time} seconds.\n\n(This cookie will be used only for you.)" 351 | msg = await bot.send_message(chat_id=update.chat.id, text=msg_text.format(bot_name=bot_name, left_time=left_time)) 352 | 353 | logger.info(f"[{update.chat.id}] Allowed to use cookie_file_handle.") 354 | FILE_HANDLE_USERS[update.chat.id] = True 355 | loop = asyncio.get_event_loop() 356 | async def rm_handle_func(): 357 | nonlocal left_time 358 | if left_time > 10: 359 | if not FILE_HANDLE_USERS[update.chat.id]: # 已经更新 cookie 后结束 360 | await msg.delete() 361 | return True 362 | left_time -= 10 363 | await msg.edit_text(msg_text.format(bot_name=bot_name, left_time=left_time)) 364 | loop.call_later(10, callback) 365 | else: 366 | logger.warning(f"[{update.chat.id}] Wait for cookie file timeout.") 367 | FILE_HANDLE_USERS[update.chat.id] = False 368 | await msg.edit_text(f"{bot_name}: Wait for cookie file timeout!") 369 | return True 370 | def callback(): 371 | loop.create_task(rm_handle_func()) 372 | loop.call_later(10, callback) 373 | 374 | @pyro.on_message(filters.document & filters.private & is_allowed_filter()) 375 | async def cookie_file_handle(bot, update): 376 | if not check_inited(update.chat.id): 377 | await bot.send_message(chat_id=update.chat.id, text="Please initialize me first.") 378 | logger.warning(f"User [{update.chat.id}] try to set cookie but been rejected (not initialized).") 379 | return 380 | if update.chat.id not in FILE_HANDLE_USERS or not FILE_HANDLE_USERS[update.chat.id]: 381 | logger.warning(f"User [{update.chat.id}] try to set cookie but been rejected (not use /cookie command first).") 382 | return 383 | logger.info(f"User [{update.chat.id}] send a file [{update.document.file_name}, {update.document.mime_type}, {update.document.file_size}].") 384 | if update.document.mime_type != "application/json": # 非 json 格式判断 385 | await bot.send_message(chat_id=update.chat.id, text=f"Please send a json file. Received ({update.document.mime_type}).") 386 | return 387 | cookie_f = await bot.download_media(update.document.file_id, in_memory=True) 388 | try: # 加载 json 389 | cookies = json.loads(bytes(cookie_f.getbuffer()).decode("utf-8")) 390 | except Exception as e: 391 | logger.exception(f"User [{update.chat.id}] send a non json file") 392 | await bot.send_message(chat_id=update.chat.id, text="Load json file failed, You should send a json file.") 393 | return 394 | cookie_keys = set(["domain", "path", "name", "value"]) 395 | for cookie in cookies: # 检查 cookie 格式 396 | if cookie_keys & set(cookie.keys()) != cookie_keys: 397 | logger.warning(f"User [{update.chat.id}] send invalid cookie file!") 398 | await bot.send_message(chat_id=update.chat.id, text=f"Seems cookie is invalid. Please send a valid cookie json file.") 399 | return 400 | if "bing.com" not in cookie["domain"]: 401 | logger.warning(f"User [{update.chat.id}] send the cookie file not from bing.com!") 402 | await bot.send_message(chat_id=update.chat.id, text=f"Seems cookie is invalid (not from bing.com). Please send a valid cookie json file.") 403 | return 404 | await EDGES[update.chat.id]["bot"].close() 405 | EDGES[update.chat.id]["cookies"] = cookies 406 | for cookie in cookies: 407 | if cookie.get("name") == "_U": 408 | EDGES[update.chat.id]["image_U"] = cookie.get("value") 409 | break 410 | EDGES[update.chat.id]["bot"] = await EdgeGPT.Chatbot.create(proxy=PROXY_BING, cookies=cookies) 411 | FILE_HANDLE_USERS[update.chat.id] = False 412 | bot_name = EDGES[update.chat.id]["bot_name"] 413 | await bot.send_message(chat_id=update.chat.id, text=f"{bot_name}: Cookie set successfully.") 414 | 415 | # 设置 stream 模式消息更新间隔 416 | @pyro.on_message(filters.command("interval") & filters.private & is_allowed_filter()) 417 | async def set_interval_handle(bot, update): 418 | if not check_inited(update.chat.id): 419 | await bot.send_message(chat_id=update.chat.id, text="Please initialize me first.") 420 | logger.warning(f"User [{update.chat.id}] try to set INTERVAL but been rejected (not initialized).") 421 | return 422 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 423 | bot_name = EDGES[update.chat.id]["bot_name"] 424 | if len(update.command) > 1: 425 | arg = update.command[1] 426 | EDGES[update.chat.id]["interval"] = int(arg) 427 | reply_text = f"{bot_name} has been set INTERVAL to '{arg}'." 428 | logger.warning(f"User [{update.chat.id}] have set {arg}") 429 | await bot.send_message(chat_id=update.chat.id, text=reply_text) 430 | else: 431 | reply_text = f"{bot_name}: need an argument 'INTERVAL' (Integer)." 432 | logger.warning(f"User [{update.chat.id}] need an argument 'INTERVAL' (Integer).") 433 | await bot.send_message(chat_id=update.chat.id, text=reply_text) 434 | 435 | # 设置 bot 名称 436 | @pyro.on_message(filters.command("bot_name") & filters.private & is_allowed_filter()) 437 | async def set_interval_handle(bot, update): 438 | if not check_inited(update.chat.id): 439 | await bot.send_message(chat_id=update.chat.id, text="Please initialize me first.") 440 | logger.warning(f"User [{update.chat.id}] try to set INTERVAL but been rejected (not initialized).") 441 | return 442 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 443 | bot_name = EDGES[update.chat.id]["bot_name"] 444 | if len(update.command) > 1: 445 | arg = update.command[1] 446 | EDGES[update.chat.id]["bot_name"] = arg 447 | reply_text = f"{bot_name} has been set 'BOT_NAME' to '{arg}'." 448 | logger.warning(f"User [{update.chat.id}] have set 'BOT_NAME' to {arg}") 449 | await bot.send_message(chat_id=update.chat.id, text=reply_text) 450 | else: 451 | reply_text = f"{bot_name}: need an argument 'BOT_NAME' (String)." 452 | logger.warning(f"User [{update.chat.id}] need an argument 'BOT_NAME' (String).") 453 | await bot.send_message(chat_id=update.chat.id, text=reply_text) 454 | 455 | # 修改建议消息模式 456 | @pyro.on_message(filters.command("suggest_mode") & filters.private & is_allowed_filter()) 457 | async def set_suggest_mode_handle(bot, update): 458 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 459 | if not check_inited(update.chat.id): 460 | await bot.send_message(chat_id=update.chat.id, text="Please initialize me first.") 461 | logger.warning(f"User [{update.chat.id}] try to set SUGGEST_MODE but been rejected (not initialized).") 462 | return 463 | if len(update.command) > 1: 464 | arg = update.command[1] 465 | EDGES[update.chat.id]["suggest"] = arg 466 | if arg in ["callbackquery", "replykeyboard", "copytext"]: 467 | bot_name = EDGES[update.chat.id]["bot_name"] 468 | reply_text = f"{bot_name}: set SUGGEST_MODE to '{EDGES[update.chat.id]['suggest']}'." 469 | await bot.send_message(chat_id=update.chat.id, text=reply_text, reply_markup=ReplyKeyboardRemove()) 470 | return 471 | reply_text = f"Available arguments: `callbackquery`, `replykeyboard`, `copytext`" 472 | await bot.send_message(chat_id=update.chat.id, text=reply_text) 473 | 474 | def can_image_gen(): 475 | async def funcc(_, __, update): 476 | global EDGES 477 | return EDGES[update.chat.id]["image_U"] != "" 478 | return filters.create(funcc) 479 | # 图片生成 480 | @pyro.on_message(filters.command("image_gen") & filters.private & is_allowed_filter() & can_image_gen()) 481 | async def set_suggest_mode_handle(bot, update): 482 | if not check_inited(update.chat.id): 483 | await bot.send_message(chat_id=update.chat.id, text="Please initialize me first.") 484 | logger.warning(f"User [{update.chat.id}] try to use image_gen but been rejected (not initialized).") 485 | return 486 | logger.info(f"Receive commands [{update.command}] from [{update.chat.id}]") 487 | if len(update.command) > 1: 488 | chat_id = update.chat.id 489 | prompt = " ".join(update.command[1:]) 490 | caption = f"ImageGenerator\nImage is generating, this is a placeholder image.\n\nUsing Prompt: {prompt}" 491 | msgs = await bot.send_media_group(chat_id, [ 492 | InputMediaPhoto("assets/placeholder0.png", caption=caption), 493 | InputMediaPhoto("assets/placeholder1.png", caption=caption), 494 | InputMediaPhoto("assets/placeholder2.png", caption=caption), 495 | InputMediaPhoto("assets/placeholder3.png", caption=caption), 496 | ]) 497 | 498 | try: 499 | image_gen_cookie_u = EDGES[chat_id]["image_U"] 500 | all_cookies = EDGES[update.from_user.id]["cookies"] 501 | images = await image_gen_main(prompt, image_gen_cookie_u, all_cookies=all_cookies) 502 | caption = f"ImageGenerator\nImage is generated.\n\nUsing Prompt: {prompt}" 503 | images_count = len(images) 504 | for i in range(len(msgs)): 505 | msg_chat_id = msgs[i].chat.id 506 | msg_id = msgs[i].id 507 | if i < images_count: 508 | await bot.edit_message_media(msg_chat_id, msg_id, InputMediaPhoto(images[i], caption=caption)) 509 | else: 510 | await msgs[i].delete() 511 | logger.info(f"ImageGenerator Successfully, chat_id: {chat_id}, images({images_count}): {images}") 512 | return 513 | except Exception as e: 514 | logger.exception(f"ImageGenerator Error: {e}") 515 | await bot.send_message(chat_id=chat_id, text=f"ImageGenerator Error: {e}.\n\nImageGenerator Usage: `/image_gen <prompt>`") 516 | return 517 | await update.reply(text="ImageGenerator Usage: `/image_gen <prompt>`") 518 | 519 | 520 | def is_chat_text_filter(): 521 | async def funcc(_, __, update): 522 | if bool(update.text): 523 | return not update.text.startswith("/") 524 | return False 525 | return filters.create(funcc) 526 | 527 | # 处理文字对话 528 | @pyro.on_message(is_chat_text_filter() & filters.private & is_allowed_filter()) 529 | async def chat_handle(bot, update): 530 | logger.info(f"Receive text [{update.text}] from [{update.chat.id}]") 531 | if not check_inited(update.chat.id): 532 | await bot.send_message(chat_id=update.chat.id, text="Please initialize me first.") 533 | logger.warning(f"User [{update.chat.id}] not inited") 534 | return 535 | # 调用 AI 536 | bot_name = EDGES[update.chat.id]["bot_name"] 537 | response = f"{bot_name} is thinking..." 538 | msg = await update.reply(text=response) 539 | if EDGES[update.chat.id]["response"] == "normal": 540 | response, reply_markup = await bingAI(update.chat.id, update.text) 541 | await msg.edit(text=response, reply_markup=reply_markup) 542 | elif EDGES[update.chat.id]["response"] == "stream": 543 | try: 544 | async for final, response, reply_markup in bingAIStream(update.chat.id, update.text): 545 | if final: 546 | await msg.edit(text=response, reply_markup=reply_markup) 547 | else: 548 | if response == "": 549 | continue 550 | try: 551 | await msg.edit(text=response) 552 | except Exception as e: # 有时由于 API 返回数据问题,编辑前后消息内容一致,不做处理,只记录 warning 553 | logger.exception(f"Message editing error: {e}") 554 | except Exception as e: 555 | logger.error(f"There seems an error at upsteam library, update the upsteam may help.") 556 | logger.exception(f"[chat_handle, unexpected error]: {e}") 557 | await msg.edit(text=f"Something went wrong, please check the logs.\n\n[{e}]") 558 | raise e 559 | 560 | # 处理 callback query 561 | @pyro.on_callback_query(is_allowed_filter()) 562 | async def callback_query_handle(bot, query): 563 | if not check_inited(query.from_user.id): 564 | await bot.send_message(chat_id=query.from_user.id, text="Please initialize me first.") 565 | logger.warning(f"User [{query.from_user.id}] try to send callback query but been rejected (not initialized).") 566 | return 567 | query_text = EDGES[query.from_user.id]["temp"].get(query.data) 568 | logger.info(f"Receive callback query [{query.data}: {query_text}] from [{query.from_user.id}]") 569 | if query_text is None: 570 | # 重启 bot 后会丢失之前存储的 callback query 对应信息,暂时返回报错让用户手动发消息(后续将 callback query 存储在数据库中,而不是内存) 571 | await bot.send_message(chat_id=query.from_user.id, text="Sorry, the callback query is not found.(May be you have restarted the bot before.)") 572 | return 573 | # 调用 AI 574 | bot_name = EDGES[query.from_user.id]["bot_name"] 575 | response = f"{bot_name} is thinking..." 576 | msg = await bot.send_message(chat_id=query.from_user.id, text=response) 577 | if EDGES[query.from_user.id]["response"] == "normal": 578 | response, reply_markup = await bingAI(query.from_user.id, query_text) 579 | await msg.edit(text=response, reply_markup=reply_markup) 580 | elif EDGES[query.from_user.id]["response"] == "stream": 581 | try: 582 | async for final, response, reply_markup in bingAIStream(query.from_user.id, query_text): 583 | if final: 584 | await msg.edit(text=response, reply_markup=reply_markup) 585 | else: 586 | if response == "": 587 | continue 588 | await msg.edit(text=response) 589 | except Exception as e: 590 | logger.error("There seems an error at upsteam library, update the upsteam may help.") 591 | logger.exception(f"[callback_query_handle, unexpected error]: {e}") 592 | await msg.edit(text=f"Something went wrong, please check the logs.\n\n[{e}]") 593 | raise e 594 | 595 | def is_image_gen_query_filter(): 596 | async def funcg(_, __, update): 597 | if update.query.startswith("g"): 598 | global EDGES 599 | return EDGES[update.from_user.id]["image_U"] != "" 600 | return False 601 | return filters.create(funcg) 602 | 603 | @pyro.on_inline_query(is_allowed_filter() & is_image_gen_query_filter()) 604 | async def inline_query_image_gen_handle(bot, update): 605 | """ 606 | You should enable 'Inline Mode' and set 'Inline Feedback' to '100%' (10% may works well too) at @BotFather. 607 | 你应该在 @BotFather 上启用 'Inline Mode' 并设置 'Inline Feedback' 为 '100%' (10% 或许也能较好工作) 608 | """ 609 | if not check_inited(update.from_user.id): 610 | logger.warning(f"User [{update.from_user.id}] try to send inline_query image_gen but been rejected (not initialized).") 611 | await update.answer( 612 | results=[ 613 | InlineQueryResultArticle( 614 | title="Not initialized", 615 | input_message_content=InputTextMessageContent( 616 | "Not initialized" 617 | ), 618 | description="Please initialize me first.", 619 | ) 620 | ], 621 | cache_time=1 622 | ) 623 | return 624 | tmp = update.query.split(" ", 1) 625 | prompt = "" 626 | if len(tmp) >= 2: 627 | prompt = tmp[1].strip() 628 | logger.info(f"Receive inline_query image_gen result [{prompt}] from [{update.from_user.id}]") 629 | 630 | if prompt == "": # 如果没有输入 prompt 返回使用说明提示 631 | await update.answer( 632 | results=[ 633 | InlineQueryResultArticle( 634 | title="ImageGenerator", 635 | input_message_content=InputTextMessageContent( 636 | "No prompt provide!\n\nUsage: Use `@BotName g <prompt> %` to generate image. (Tips: If it take long time (25s) no response, you can add a '%' and delete it to refresh)" 637 | ), 638 | description="Input a prompt to generate an Image", 639 | ) 640 | ], 641 | cache_time=1 642 | ) 643 | return 644 | 645 | img = -1 646 | while True: 647 | if prompt.endswith("%"): 648 | prompt = prompt[:-1].strip() 649 | img += 1 650 | else: 651 | break 652 | 653 | # lang = langid.classify(prompt)[0] # 语言判断,现在 Bing AI 已经支持中文,暂时不做判断,其他语言不清楚 654 | 655 | if img == -1: # 如果没有输入 prompt 终止符,则返回提示 656 | await update.answer( 657 | results=[ 658 | InlineQueryResultArticle( 659 | title="Prompt: " + prompt + "(Add '%' at the end of prompt to generate image)", 660 | description="Tips: If it take long time (25s) no response, you can add a '%' and delete it to refresh", 661 | input_message_content=InputTextMessageContent( 662 | "Please add '%' at the end of prompt to confirm the prompt. Add more (Max: 4) '%' to select next image. (Tips: If it take long time (25s) no response, you can add a '%' and delete it to refresh)" 663 | ) 664 | ) 665 | ] 666 | ) 667 | return 668 | try: 669 | prompt_hash = str(hash(prompt)) 670 | if EDGES[update.from_user.id]["images"].get(prompt_hash) is not None: 671 | images = EDGES[update.from_user.id]["images"].get(prompt_hash) 672 | else: 673 | image_gen_cookie_u = EDGES[update.from_user.id]["image_U"] 674 | all_cookies = EDGES[update.from_user.id]["cookies"] 675 | images = await image_gen_main(prompt, image_gen_cookie_u, all_cookies=all_cookies) # 获取图片并缓存 676 | EDGES[update.from_user.id]["images"] = {} 677 | EDGES[update.from_user.id]["images"][prompt_hash] = images 678 | 679 | if img >= len(images): 680 | img = len(images) - 1 681 | img_url = images[img] 682 | 683 | await update.answer( 684 | results=[ 685 | InlineQueryResultPhoto( 686 | title="ImageGeneration Prompt: " + prompt, 687 | description="Add more '%' at the end of prompt to select next image", 688 | photo_url=img_url, 689 | caption=f"This Image is generated by Bing AI with prompt: {prompt}", 690 | ) 691 | ], 692 | cache_time=1 693 | ) 694 | except Exception as e: 695 | logger.error(f"Image generation error: {e}") 696 | logger.exception(f"[inline_query_image_gen_handle, unexpected error]: {e}") 697 | await update.answer( 698 | results=[ 699 | InlineQueryResultArticle( 700 | title="[ERROR] Prompt: " + prompt, 701 | description=e.__str__(), 702 | input_message_content=InputTextMessageContent( 703 | f"Something went wrong.\n\nError message: {e.__str__()}\n\nYour Prompt: {prompt}" 704 | ) 705 | ) 706 | ] 707 | ) 708 | 709 | 710 | def is_prompt_select_filter(): 711 | async def funcp(_, __, update): 712 | return update.query.startswith("p") 713 | return filters.create(funcp) 714 | 715 | @pyro.on_inline_query(is_allowed_filter() & is_prompt_select_filter()) 716 | async def inline_query_prompt_select_handle(bot, update): 717 | """ 718 | You should enable 'Inline Mode' and set 'Inline Feedback' to '100%' (10% may works well too) at @BotFather. 719 | 你应该在 @BotFather 上启用 'Inline Mode' 并设置 'Inline Feedback' 为 '100%' (10% 或许也能较好工作) 720 | """ 721 | if not check_inited(update.from_user.id): 722 | logger.warning(f"User [{update.from_user.id}] try to send inline_query prompt select result but been rejected (not initialized).") 723 | await update.answer( 724 | results=[ 725 | InlineQueryResultArticle( 726 | title="Not initialized", 727 | input_message_content=InputTextMessageContent( 728 | "Not initialized" 729 | ), 730 | description="Please initialize me first.", 731 | ) 732 | ], 733 | cache_time=1 734 | ) 735 | return 736 | tmp = update.query.split(" ", 1) 737 | query = "" 738 | if len(tmp) >= 2: 739 | query = tmp[1] 740 | logger.info(f"Receive inline_query prompt select result [{query}] from [{update.from_user.id}]") 741 | await update.answer( 742 | results=[ 743 | InlineQueryResultArticle( 744 | title="PromptSelector", 745 | input_message_content=InputTextMessageContent( 746 | "[Not Supported Yet]Use `@BotName p <query>` to select Prompt. You should use this to send message to AI bot." 747 | ), 748 | description="[Not Supported Yet]Click me to show usage of PromptSelector", 749 | ) 750 | ], 751 | cache_time=1 752 | ) 753 | 754 | def is_default_inline_filter(): 755 | async def funcd(_, __, update): 756 | return not update.query.startswith("p") and not update.query.startswith("g") 757 | return filters.create(funcd) 758 | 759 | @pyro.on_inline_query(is_allowed_filter() & is_default_inline_filter()) 760 | async def inline_query_default_handle(bot, update): 761 | """ 762 | You should enable 'Inline Mode' and set 'Inline Feedback' to '100%' (10% may works well too) at @BotFather. 763 | 你应该在 @BotFather 上启用 'Inline Mode' 并设置 'Inline Feedback' 为 '100%' (10% 或许也能较好工作) 764 | """ 765 | if not check_inited(update.from_user.id): 766 | logger.warning(f"User [{update.from_user.id}] try to send inline_query default result but been rejected (not initialized).") 767 | await update.answer( 768 | results=[ 769 | InlineQueryResultArticle( 770 | title="Not initialized", 771 | input_message_content=InputTextMessageContent( 772 | "Not initialized" 773 | ), 774 | description="Please initialize me first.", 775 | ) 776 | ], 777 | cache_time=1 778 | ) 779 | return 780 | logger.info(f"Receive default result [{update.query}] from [{update.from_user.id}]") 781 | await update.answer( 782 | results=[ 783 | InlineQueryResultArticle( 784 | title="ImageGenerator", 785 | input_message_content=InputTextMessageContent( 786 | # "Usage: Use `@BotName g <prompt>` to generate image. Prompt should be in English, If prompt is not in English, it will automatically use AI to translate prompt to English." 787 | "Usage: Use `@BotName g <prompt>` to generate image. (Tips: If it take long time (25s) no response, you can add a '%' and delete it to refresh)" 788 | ), 789 | description="Click me to show usage of ImageGenerator", 790 | ), 791 | InlineQueryResultArticle( 792 | title="PromptSelector", 793 | input_message_content=InputTextMessageContent( 794 | "[Not Supported Yet]Use `@BotName p <query>` to select Prompt. You should use this to send message to AI bot." 795 | ), 796 | description="[Not Supported Yet]Click me to show usage of PromptSelector", 797 | ) 798 | ], 799 | cache_time=1 800 | ) 801 | 802 | 803 | async def bingAI(user_id, messageText): 804 | rsp = await EDGES[user_id]["bot"].ask(prompt=messageText, conversation_style=EDGES[user_id]["style"]) 805 | rsp_json = json.dumps(rsp, ensure_ascii=False) 806 | logger.info(f"BingAI raw response: {rsp_json}") 807 | 808 | response, msg_suggest = process_message_main(rsp, user_id) 809 | return response, msg_suggest 810 | 811 | async def bingAIStream(user_id, messageText): 812 | last_time = time.time() - 0.5 # 第一次间隔调小(有需要自行根据 EDGES[user_id]["interval"] 调整) 813 | async for final, rsp in EDGES[user_id]["bot"].ask_stream(prompt=messageText, conversation_style=EDGES[user_id]["style"]): 814 | now_time = time.time() 815 | if final: 816 | rsp_json = json.dumps(rsp, ensure_ascii=False) 817 | logger.info(f"BingAI stream response final: {rsp_json}") 818 | 819 | response, msg_suggest = process_message_main(rsp, user_id) 820 | yield final, response, msg_suggest 821 | 822 | if now_time - last_time > EDGES[user_id]["interval"] and not final: 823 | last_time = now_time 824 | logger.info(f"BingAI stream response: {rsp}") 825 | if type(rsp) == str: 826 | rsp = rsp.strip() 827 | if "Searching the web for:" in rsp: 828 | if "Generating answers for you..." in rsp: 829 | search_rsp = rsp.split("Generating answers for you...", 1) 830 | rsp = "" 831 | for line in search_rsp[0].split("\n"): 832 | if "Searching the web for:" in line: 833 | rsp += line + "\n" 834 | if search_rsp[1].strip().startswith("[1]: "): # 删除引用的消息链接, 避免消息闪动幅度过大 835 | search_rsp[1] = search_rsp[1].split("\n\n", 1)[1] 836 | rsp += "\nGenerating answers for you...\n" + search_rsp[1] 837 | response = re.sub(r'\[\^(\d+)\^\]', '', rsp) 838 | 839 | else: 840 | response = "[WARN] BingAI stream response: Returned non-string type data without final" 841 | logger.warning(f"BingAI stream response: Returned non-string type data without final") 842 | yield final, response, "" 843 | 844 | def process_message_main(rsp_obj, user_id=None): 845 | response = RESPONSE_TEMPLATE 846 | 847 | # 回复消息主文本部分 848 | if "messages" in rsp_obj["item"]: 849 | bot_messages = rsp_obj["item"]["messages"] 850 | search_query = "" 851 | for message in bot_messages: 852 | if "messageType" in message: 853 | if message["messageType"] == "InternalSearchQuery": 854 | search_query += "#" + message["hiddenText"].replace(" ", "_") + "\n" 855 | if "sourceAttributions" in message or "suggestedResponses" in message or "spokenText" in message : 856 | msg_main_body, msg_ref, msg_suggest = process_message_body(message, user_id) 857 | msg_main = msg_main_body + "\n\nSearch Query:\n" + search_query 858 | elif "result" in rsp_obj["item"]: 859 | logger.warning(f"[process_message_main] BingAI result: {json.dumps(rsp_obj['item']['result'], ensure_ascii=False)}") 860 | if rsp_obj["item"]["result"]["value"] == "InvalidSession": 861 | response = "Invalid Session (may be session expired), Please /reset the chat" 862 | return response, None 863 | elif rsp_obj["item"]["result"]["value"] == "Throttled": 864 | response = "Request is throttled (You request may contain sensitive content), Please /reset the chat" 865 | return response, None 866 | else: 867 | if "message" in rsp_obj["item"]["result"]: 868 | response = rsp_obj["item"]["result"]["message"] + "Please /reset the chat" 869 | response = "Something wrong. Please /reset the chat" 870 | return response, None 871 | else: 872 | logger.warning(f"[process_message_main] BingAI response: {json.dumps(rsp_obj, ensure_ascii=False)}") 873 | response = "Something wrong. Please /reset the chat" 874 | return response, None 875 | 876 | throttlingMax = rsp_obj["item"]["throttling"]["maxNumUserMessagesInConversation"] 877 | throttlingUser = rsp_obj["item"]["throttling"]["numUserMessagesInConversation"] 878 | msg_throttling = f"Messages: {throttlingUser}/{throttlingMax}" 879 | if throttlingUser >= throttlingMax: 880 | asyncio.run(EDGES[user_id]["bot"].reset()) 881 | msg_throttling += "\nNote: Conversation is over, and I auto reset it successfully." 882 | if msg_main == "": 883 | response = "Something wrong. Please /reset the chat" # default response 884 | else: 885 | response = response.format(msg_main=msg_main, msg_ref=msg_ref, msg_throttling=msg_throttling) 886 | return response, msg_suggest 887 | 888 | def process_message_body(msg_obj, user_id=None): 889 | # 回复消息的主体部分(先设置为空) 890 | msg_main = "" 891 | msg_ref = "" 892 | msg_suggest = None 893 | if "text" in msg_obj: 894 | msg_main = msg_obj["text"] 895 | if "sourceAttributions" in msg_obj: 896 | source_count = len(msg_obj["sourceAttributions"]) # 可用的引用资源数量 897 | # 将引用标号与具体引用的链接绑定 898 | if re.search(r'\[\^(\d+)\^\]', msg_main) is not None: 899 | matches = re.findall(r'\[\^(\d+)\^\]', msg_main) # 匹配引用标号(类似 "[^1^]" 格式) 900 | for match in matches: # 对每个引用标号找到实际引用链接并绑定好 markdown 格式链接 901 | if int(match) > source_count: # 如果“最大引用编号” 大于 “引用资源数量”,则删除它 902 | msg_main = msg_main.replace(f"[^{match}^]", "", 1) 903 | continue 904 | url = msg_obj["sourceAttributions"][int(match) - 1]["seeMoreUrl"] 905 | msg_main = msg_main.replace(f"[^{match}^]", f"[[{match}]]({url})", 1) 906 | 907 | # 消息引用部分(参考链接) 908 | if source_count > 0: 909 | msg_ref = "- - - - - - - - -\nReference:\n" 910 | for ref_index in range(source_count): 911 | providerDisplayName = msg_obj["sourceAttributions"][ref_index]["providerDisplayName"] 912 | url = msg_obj["sourceAttributions"][ref_index]["seeMoreUrl"] 913 | msg_ref += f"{ref_index + 1}. [{providerDisplayName}]({url})\n" 914 | 915 | # 建议消息部分 916 | if "suggestedResponses" in msg_obj: 917 | suggested_count = len(msg_obj["suggestedResponses"]) 918 | if EDGES[user_id]["suggest"] == "callbackquery": 919 | msg_suggest = InlineKeyboardMarkup([]) 920 | EDGES[user_id]["temp"] = {} 921 | for suggested_index in range(suggested_count): 922 | suggested_text = msg_obj["suggestedResponses"][suggested_index]["text"] 923 | suggested_hash = str(hash(suggested_text)) # 使用 hash 值作为 data ,避免 data 过长报错 924 | EDGES[user_id]["temp"][suggested_hash] = suggested_text 925 | msg_suggest.inline_keyboard.append([InlineKeyboardButton(suggested_text, callback_data=suggested_hash)]) 926 | elif EDGES[user_id]["suggest"] == "replykeyboard": 927 | msg_suggest = ReplyKeyboardMarkup([]) 928 | for suggested_index in range(suggested_count): 929 | suggested_text = msg_obj["suggestedResponses"][suggested_index]["text"] 930 | msg_suggest.keyboard.append([suggested_text]) 931 | else: 932 | msg_ref += "- - - - - - - - -\nSuggestion(Click to copy):\n" 933 | for suggested_index in range(suggested_count): 934 | suggested_text = msg_obj["suggestedResponses"][suggested_index]["text"] 935 | msg_ref += f"{suggested_index + 1}. `{suggested_text}`\n" 936 | 937 | return msg_main, msg_ref, msg_suggest 938 | 939 | async def image_gen_main(prompt, image_gen_cookie_u, all_cookies: List[Dict] = None): 940 | if all_cookies is None: 941 | async with ImageGenAsync(image_gen_cookie_u) as image_generator: 942 | images = await image_generator.get_images(prompt) 943 | return images 944 | else: 945 | async with ImageGenAsync(image_gen_cookie_u, all_cookies=all_cookies) as image_generator: 946 | images = await image_generator.get_images(prompt) 947 | return images 948 | 949 | 950 | 951 | pyro.run() 952 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |