diff --git a/README.md b/README.md index d4f935dc..4f846c4c 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ 🌍支持的AI语言模型一览: -**文字模型** +**文字模型/图片理解** - OpenAI GPT-3模型(原生支持) - OpenAI GPT-3.5模型(原生支持) - OpenAI GPT-4模型(原生支持) -- ChatGPT网页版 GPT-3.5模型(免费,原生支持) -- ChatGPT网页版 GPT-4模型(需订阅Plus账户,原生支持) -- Bing(免费,原生支持) +- ~~ChatGPT网页版 GPT-3.5模型(免费,原生支持)~~ +- ~~ChatGPT网页版 GPT-4模型(需订阅Plus账户,原生支持)~~ +- ~~Bing(免费,原生支持)~~ - Claude模型(免费,由[LLMs插件](https://github.com/Soulter/llms)支持) - HuggingChat模型(免费,由[LLMs插件](https://github.com/Soulter/llms)支持) - Google Bard(免费,由[LLMs插件](https://github.com/Soulter/llms)支持) @@ -44,9 +44,9 @@ 🌍机器人支持的能力一览: -- 同时部署机器人到QQ和QQ频道 +- 同时部署机器人到 QQ 和 QQ 频道 - 大模型对话 -- 大模型网页搜索能力 **(目前仅支持OpenAI系的模型,最新版本下使用web on指令打开)** +- 大模型网页搜索能力 **(目前仅支持OpenAI系的模型,最新版本下使用 web on 指令打开)** - 插件安装(在QQ或QQ频道聊天框内输入`plugin`了解详情) - 回复文字图片渲染(以图片markdown格式回复,**大幅度降低被风控概率**,需手动在`cmd_config.json`内开启qq_pic_mode) - 人格设置 diff --git a/addons/plugins/helloworld/helloworld.py b/addons/plugins/helloworld/helloworld.py index c76d4a9f..5e8dccb9 100644 --- a/addons/plugins/helloworld/helloworld.py +++ b/addons/plugins/helloworld/helloworld.py @@ -50,6 +50,11 @@ class HelloWorldPlugin: return True, tuple([True, "Hello World!!", "helloworld"]) else: return False, None + else: + """ + 其他平台处理逻辑 + """ + return False, None """ 帮助函数,当用户输入 plugin v 插件名称 时,会调用此函数,返回帮助信息 返回参数要求(必填):dict{ diff --git a/cores/qqbot/core.py b/cores/qqbot/core.py index 95000a6b..87079eb4 100644 --- a/cores/qqbot/core.py +++ b/cores/qqbot/core.py @@ -13,6 +13,13 @@ from cores.qqbot.personality import personalities from addons.baidu_aip_judge import BaiduJudge from model.platform.qqchan import QQChan, NakuruGuildMember, NakuruGuildMessage from model.platform.qq import QQ +from model.platform.qqgroup import ( + UnofficialQQBotSDK, + Event as QQEvent, + Message as QQMessage, + MessageChain, + PlainText +) from nakuru import ( CQHTTP, GroupMessage, @@ -86,25 +93,35 @@ PLATFORM_QQCHAN = 'qqchan' qqchan_loop = None client = None -# 配置 -cc.init_attributes(["qq_forward_threshold"], 200) -cc.init_attributes(["qq_welcome"], "欢迎加入本群!\n欢迎给https://github.com/Soulter/QQChannelChatGPT项目一个Star😊~\n输入help查看帮助~\n") -cc.init_attributes(["bing_proxy"], "") -cc.init_attributes(["qq_pic_mode"], False) -cc.init_attributes(["rev_chatgpt_model"], "") -cc.init_attributes(["rev_chatgpt_plugin_ids"], []) -cc.init_attributes(["rev_chatgpt_PUID"], "") -cc.init_attributes(["rev_chatgpt_unverified_plugin_domains"], []) -cc.init_attributes(["gocq_host"], "127.0.0.1") -cc.init_attributes(["gocq_http_port"], 5700) -cc.init_attributes(["gocq_websocket_port"], 6700) -cc.init_attributes(["gocq_react_group"], True) -cc.init_attributes(["gocq_react_guild"], True) -cc.init_attributes(["gocq_react_friend"], True) -cc.init_attributes(["gocq_react_group_increase"], True) -cc.init_attributes(["gocq_qqchan_admin"], "") -cc.init_attributes(["other_admins"], []) -cc.init_attributes(["CHATGPT_BASE_URL"], "") +# QQ群机器人 +PLATFROM_QQBOT = 'qqbot' + +# CLI +PLATFORM_CLI = 'cli' + +# 加载默认配置 +cc.init_attributes("qq_forward_threshold", 200) +cc.init_attributes("qq_welcome", "欢迎加入本群!\n欢迎给https://github.com/Soulter/QQChannelChatGPT项目一个Star😊~\n输入help查看帮助~\n") +cc.init_attributes("bing_proxy", "") +cc.init_attributes("qq_pic_mode", False) +cc.init_attributes("rev_chatgpt_model", "") +cc.init_attributes("rev_chatgpt_plugin_ids", []) +cc.init_attributes("rev_chatgpt_PUID", "") +cc.init_attributes("rev_chatgpt_unverified_plugin_domains", []) +cc.init_attributes("gocq_host", "127.0.0.1") +cc.init_attributes("gocq_http_port", 5700) +cc.init_attributes("gocq_websocket_port", 6700) +cc.init_attributes("gocq_react_group", True) +cc.init_attributes("gocq_react_guild", True) +cc.init_attributes("gocq_react_friend", True) +cc.init_attributes("gocq_react_group_increase", True) +cc.init_attributes("gocq_qqchan_admin", "") +cc.init_attributes("other_admins", []) +cc.init_attributes("CHATGPT_BASE_URL", "") +cc.init_attributes("qqbot_appid", "") +cc.init_attributes("qqbot_secret", "") +cc.init_attributes("llm_env_prompt", "> hint: 末尾根据内容和心情添加 1-2 个emoji") +cc.init_attributes("default_personality_str", "") # cc.init_attributes(["qq_forward_mode"], False) # QQ机器人 @@ -115,8 +132,14 @@ gocq_app = CQHTTP( port=cc.get("gocq_websocket_port", 6700), http_port=cc.get("gocq_http_port", 5700), ) +qq_bot: UnofficialQQBotSDK = UnofficialQQBotSDK( + cc.get("qqbot_appid", None), + cc.get("qqbot_secret", None) +) + +gocq_loop: asyncio.AbstractEventLoop = None +qqbot_loop: asyncio.AbstractEventLoop = None -gocq_loop = None # 全局对象 _global_object: GlobalObject = None @@ -200,6 +223,9 @@ def initBot(cfg, prov): global frequency_count, frequency_time, announcement, direct_message_mode, version global keywords, _global_object + _event_loop = asyncio.new_event_loop() + asyncio.set_event_loop(_event_loop) + # 初始化 global_object _global_object = GlobalObject() _global_object.base_config = cfg @@ -207,61 +233,6 @@ def initBot(cfg, prov): if 'reply_prefix' in cfg: _global_object.reply_prefix = cfg['reply_prefix'] - - gu.log("--------加载机器人平台--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - thread_inst = None - admin_qq = cc.get('admin_qq', None) - admin_qqchan = cc.get('admin_qqchan', None) - if admin_qq == None: - gu.log("未设置管理者QQ号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING) - admin_qq = input("请输入管理者QQ号(必须设置): ") - gu.log("管理者QQ号设置为: " + admin_qq, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - cc.put('admin_qq', admin_qq) - if admin_qqchan == None: - gu.log("未设置管理者QQ频道用户号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING) - admin_qqchan = input("请输入管理者频道用户号(不是QQ号, 可以先回车跳过然后在频道发送指令!myid获取): ") - if admin_qqchan == "": - gu.log("跳过设置管理者频道用户号", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - else: - gu.log("管理者频道用户号设置为: " + admin_qqchan, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - cc.put('admin_qqchan', admin_qqchan) - - gu.log("管理者QQ: " + admin_qq, gu.LEVEL_INFO) - gu.log("管理者频道用户号: " + admin_qqchan, gu.LEVEL_INFO) - _global_object.admin_qq = admin_qq - _global_object.admin_qqchan = admin_qqchan - - # GOCQ - global gocq_bot - - if 'gocqbot' in cfg and cfg['gocqbot']['enable']: - gu.log("- 启用QQ机器人 -", gu.LEVEL_INFO) - - global gocq_app, gocq_loop - gocq_loop = asyncio.new_event_loop() - gocq_bot = QQ(True, cc, gocq_loop) - thread_inst = threading.Thread(target=run_gocq_bot, args=(gocq_loop, gocq_bot, gocq_app), daemon=False) - thread_inst.start() - else: - gocq_bot = QQ(False) - - _global_object.platform_qq = gocq_bot - - gu.log("机器人部署教程: https://github.com/Soulter/QQChannelChatGPT/wiki/", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - gu.log("如果有任何问题, 请在 https://github.com/Soulter/QQChannelChatGPT 上提交issue或加群322154837", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - gu.log("请给 https://github.com/Soulter/QQChannelChatGPT 点个star!", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) - - # QQ频道 - if 'qqbot' in cfg and cfg['qqbot']['enable']: - gu.log("- 启用QQ频道机器人 -", gu.LEVEL_INFO) - global qqchannel_bot, qqchan_loop - qqchannel_bot = QQChan() - qqchan_loop = asyncio.new_event_loop() - _global_object.platform_qqchan = qqchannel_bot - thread_inst = threading.Thread(target=run_qqchan_bot, args=(cfg, qqchan_loop, qqchannel_bot), daemon=False) - thread_inst.start() - # thread.join() - # 语言模型提供商 gu.log("--------加载语言模型--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) @@ -380,12 +351,110 @@ def initBot(cfg, prov): llm_command_instance[NONE_LLM] = _command chosen_provider = NONE_LLM + gu.log("--------加载机器人平台--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + + admin_qq = cc.get('admin_qq', None) + admin_qqchan = cc.get('admin_qqchan', None) + if admin_qq == None: + gu.log("未设置管理者QQ号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING) + admin_qq = input("请输入管理者QQ号(必须设置): ") + gu.log("管理者QQ号设置为: " + admin_qq, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + cc.put('admin_qq', admin_qq) + if admin_qqchan == None: + gu.log("未设置管理者QQ频道用户号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING) + admin_qqchan = input("请输入管理者频道用户号(不是QQ号, 可以先回车跳过然后在频道发送指令!myid获取): ") + if admin_qqchan == "": + gu.log("跳过设置管理者频道用户号", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + else: + gu.log("管理者频道用户号设置为: " + admin_qqchan, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + cc.put('admin_qqchan', admin_qqchan) + + gu.log("管理者QQ: " + admin_qq, gu.LEVEL_INFO) + gu.log("管理者频道用户号: " + admin_qqchan, gu.LEVEL_INFO) + _global_object.admin_qq = admin_qq + _global_object.admin_qqchan = admin_qqchan + + + global qq_bot, qqbot_loop + qqbot_loop = asyncio.new_event_loop() + if cc.get("qqbot_appid", '') != '' and cc.get("qqbot_secret", '') != '': + gu.log("- 启用QQ群机器人 -", gu.LEVEL_INFO) + thread_inst = threading.Thread(target=run_qqbot, args=(qqbot_loop, qq_bot,), daemon=True) + thread_inst.start() + + + # GOCQ + global gocq_bot + if 'gocqbot' in cfg and cfg['gocqbot']['enable']: + gu.log("- 启用QQ机器人 -", gu.LEVEL_INFO) + + global gocq_app, gocq_loop + gocq_loop = asyncio.new_event_loop() + gocq_bot = QQ(True, cc, gocq_loop) + thread_inst = threading.Thread(target=run_gocq_bot, args=(gocq_loop, gocq_bot, gocq_app), daemon=True) + thread_inst.start() + else: + gocq_bot = QQ(False) + + _global_object.platform_qq = gocq_bot + + gu.log("机器人部署教程: https://github.com/Soulter/QQChannelChatGPT/wiki/", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + gu.log("如果有任何问题, 请在 https://github.com/Soulter/QQChannelChatGPT 上提交issue或加群322154837", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + gu.log("请给 https://github.com/Soulter/QQChannelChatGPT 点个star!", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow']) + + # QQ频道 + if 'qqbot' in cfg and cfg['qqbot']['enable']: + gu.log("- 启用QQ频道机器人 -", gu.LEVEL_INFO) + global qqchannel_bot, qqchan_loop + qqchannel_bot = QQChan() + qqchan_loop = asyncio.new_event_loop() + _global_object.platform_qqchan = qqchannel_bot + thread_inst = threading.Thread(target=run_qqchan_bot, args=(cfg, qqchan_loop, qqchannel_bot), daemon=True) + thread_inst.start() + # thread.join() if thread_inst == None: input("[System-Error] 没有启用/成功启用任何机器人,程序退出") exit() - thread_inst.join() + default_personality_str = cc.get("default_personality_str", "") + if default_personality_str == "": + _global_object.default_personality = None + else: + _global_object.default_personality = { + "name": "default", + "prompt": default_personality_str, + } + + gu.log("🎉 项目启动完成。") + + # thread_inst.join() + asyncio.get_event_loop().run_until_complete(cli()) + +async def cli(): + time.sleep(1) + while True: + prompt = input(">>> ") + if prompt == "": + continue + ngm = await cli_pack_message(prompt) + await oper_msg(ngm, True, PLATFORM_CLI) + +async def cli_pack_message(prompt: str) -> NakuruGuildMessage: + ngm = NakuruGuildMessage() + ngm.channel_id = 6180 + ngm.user_id = 6180 + ngm.message = [Plain(prompt)] + ngm.type = "GuildMessage" + ngm.self_id = 6180 + ngm.self_tiny_id = 6180 + ngm.guild_id = 6180 + ngm.sender = NakuruGuildMember() + ngm.sender.tiny_id = 6180 + ngm.sender.user_id = 6180 + ngm.sender.nickname = "CLI" + ngm.sender.role = 0 + return ngm ''' 运行QQ频道机器人 @@ -416,9 +485,12 @@ def run_qqchan_bot(cfg, loop, qqchannel_bot: QQChan): def run_gocq_bot(loop, gocq_bot, gocq_app): asyncio.set_event_loop(loop) gu.log("正在检查本地GO-CQHTTP连接...端口5700, 6700", tag="QQ") + noticed = False while True: if not gu.port_checker(5700, cc.get("gocq_host", "127.0.0.1")) or not gu.port_checker(6700, cc.get("gocq_host", "127.0.0.1")): - gu.log("与GO-CQHTTP通信失败, 请检查GO-CQHTTP是否启动并正确配置。5秒后自动重试。", gu.LEVEL_CRITICAL, tag="QQ") + if not noticed: + noticed = True + gu.log("与GO-CQHTTP通信失败, 请检查GO-CQHTTP是否启动并正确配置。程序会每隔 5s 自动重试。", gu.LEVEL_CRITICAL, tag="QQ") time.sleep(5) else: gu.log("检查完毕,未发现问题。", tag="QQ") @@ -431,6 +503,15 @@ def run_gocq_bot(loop, gocq_bot, gocq_app): except BaseException as e: input("启动QQ机器人出现错误"+str(e)) +''' +启动QQ群机器人(官方接口) +''' +def run_qqbot(loop: asyncio.AbstractEventLoop, qq_bot: UnofficialQQBotSDK): + asyncio.set_event_loop(loop) + QQBotClient() + qq_bot.run_bot() + + ''' 检查发言频率 ''' @@ -469,6 +550,12 @@ async def send_message(platform, message, res, session_id = None): qqchannel_bot.send_qq_msg(message, res) if platform == PLATFORM_GOCQ: await gocq_bot.send_qq_msg(message, res) + if platform == PLATFROM_QQBOT: + message_chain = MessageChain() + message_chain.parse_from_nakuru(res) + await qq_bot.send(message, message_chain) + if platform == PLATFORM_CLI: + print(res) async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, NakuruGuildMessage], group: bool=False, @@ -493,61 +580,60 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak with_tag = False # 是否带有昵称 - if platform == PLATFORM_GOCQ or platform == PLATFORM_QQCHAN: - _len = 0 - for i in message.message: - if isinstance(i, Plain): - qq_msg += str(i.text).strip() - if isinstance(i, At): - # @机器人 - if message.type == "GuildMessage": - if i.qq == message.user_id or i.qq == message.self_tiny_id: - with_tag = True - if message.type == "FriendMessage": - if i.qq == message.self_id: - with_tag = True - if message.type == "GroupMessage": - if i.qq == message.self_id: - with_tag = True - - for i in _global_object.nick: - if i != '' and qq_msg.startswith(i): - _len = len(i) - with_tag = True - break - qq_msg = qq_msg[_len:].strip() - - gu.log(f"收到消息:{qq_msg}", gu.LEVEL_INFO, tag="QQ") - user_id = message.user_id - - if group: - # 适配GO-CQHTTP的频道功能 - if message.type == "GuildMessage": - session_id = message.channel_id - else: - session_id = message.group_id - else: - with_tag = True - session_id = message.user_id - role = "member" - - if message.type == "GuildMessage": - sender_id = str(message.sender.tiny_id) - else: - sender_id = str(message.sender.user_id) - if sender_id == _global_object.admin_qq or \ - sender_id == _global_object.admin_qqchan or \ - sender_id in cc.get("other_admins", []) or \ - sender_id == cc.get("gocq_qqchan_admin", ""): - # gu.log("检测到管理员身份", gu.LEVEL_INFO, tag="GOCQ") - role = "admin" - if _global_object.uniqueSession: - # 独立会话时,一个用户一个session - session_id = sender_id - - if platform == PLATFORM_QQCHAN: + if platform == PLATFORM_QQCHAN or platform == PLATFROM_QQBOT or platform == PLATFORM_CLI: with_tag = True + _len = 0 + for i in message.message: + if isinstance(i, Plain) or isinstance(i, PlainText): + qq_msg += str(i.text).strip() + if isinstance(i, At): + if message.type == "GuildMessage": + if i.qq == message.user_id or i.qq == message.self_tiny_id: + with_tag = True + if message.type == "FriendMessage": + if i.qq == message.self_id: + with_tag = True + if message.type == "GroupMessage": + if i.qq == message.self_id: + with_tag = True + + for i in _global_object.nick: + if i != '' and qq_msg.startswith(i): + _len = len(i) + with_tag = True + break + qq_msg = qq_msg[_len:].strip() + + gu.log(f"收到消息:{qq_msg}", gu.LEVEL_INFO, tag="QQ") + user_id = message.user_id + + if group: + # 适配GO-CQHTTP的频道功能 + if message.type == "GuildMessage": + session_id = message.channel_id + else: + session_id = message.group_id + else: + with_tag = True + session_id = message.user_id + + if message.type == "GuildMessage": + sender_id = str(message.sender.tiny_id) + else: + sender_id = str(message.sender.user_id) + if sender_id == _global_object.admin_qq or \ + sender_id == _global_object.admin_qqchan or \ + sender_id in cc.get("other_admins", []) or \ + sender_id == cc.get("gocq_qqchan_admin", "") or \ + platform == PLATFORM_CLI: + role = "admin" + + if _global_object.uniqueSession: + # 独立会话时,一个用户一个 session + session_id = sender_id + + if qq_msg == "": await send_message(platform, message, f"Hi~", session_id=session_id) return @@ -603,10 +689,15 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak chatgpt_res = "" - if session_id in gocq_bot.waiting and gocq_bot.waiting[session_id] == '': - gocq_bot.waiting[session_id] = qq_msg + # 如果是等待回复的消息 + if platform == PLATFORM_GOCQ and session_id in gocq_bot.waiting and gocq_bot.waiting[session_id] == '': + gocq_bot.waiting[session_id] = message return - hit, command_result = await llm_command_instance[chosen_provider].check_command( + if platform == PLATFORM_QQCHAN and session_id in qqchannel_bot.waiting and qqchannel_bot.waiting[session_id] == '': + qqchannel_bot.waiting[session_id] = message + return + + hit, command_result = llm_command_instance[chosen_provider].check_command( qq_msg, session_id, role, @@ -649,13 +740,13 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak qq_msg = qq_msg[3:] web_sch_flag = True else: - qq_msg += "> hint: 末尾根据内容和心情添加1-2个emoji" + qq_msg += " " + cc.get("llm_env_prompt", "") if chosen_provider == REV_CHATGPT or chosen_provider == OPENAI_OFFICIAL: if _global_object.web_search or web_sch_flag: official_fc = chosen_provider == OPENAI_OFFICIAL chatgpt_res = gplugin.web_search(qq_msg, llm_instance[chosen_provider], session_id, official_fc) else: - chatgpt_res = str(llm_instance[chosen_provider].text_chat(qq_msg, session_id, image_url)) + chatgpt_res = str(llm_instance[chosen_provider].text_chat(qq_msg, session_id, image_url, default_personality = _global_object.default_personality)) elif chosen_provider == REV_EDGEGPT: res, res_code = await llm_instance[chosen_provider].text_chat(qq_msg, platform) if res_code == 0: # bing不想继续话题,重置会话后重试。 @@ -811,4 +902,10 @@ class gocqClient(): if source.message[0].qq == source.self_tiny_id: new_sub_thread(oper_msg, (source, True, PLATFORM_GOCQ)) else: - return \ No newline at end of file + return + +class QQBotClient(): + @qq_bot.on('GroupMessage') + async def _(bot: UnofficialQQBotSDK, message: QQMessage): + print(message) + new_sub_thread(oper_msg, (message, True, PLATFROM_QQBOT)) \ No newline at end of file diff --git a/cores/qqbot/global_object.py b/cores/qqbot/global_object.py index ace53fe2..d9871429 100644 --- a/cores/qqbot/global_object.py +++ b/cores/qqbot/global_object.py @@ -26,6 +26,7 @@ class GlobalObject: cnt_total: int platform_qq: QQ platform_qqchan: QQChan + default_personality: dict def __init__(self): self.nick = None # gocq 的昵称 @@ -39,6 +40,7 @@ class GlobalObject: self.cnt_total = 0 self.platform_qq = None self.platform_qqchan = None + self.default_personality = None class AstrMessageEvent(): message_str: str # 纯消息字符串 @@ -48,6 +50,7 @@ class AstrMessageEvent(): platform: str # `gocq` 或 `qqchan` role: str # `admin` 或 `member` global_object: GlobalObject # 一些公用数据 + session_id: int # 会话id (可能是群id,也可能是某个user的id。取决于是否开启了 uniqueSession) def __init__(self, message_str: str, message_obj: Union[GroupMessage, FriendMessage, GuildMessage, NakuruGuildMessage], @@ -56,7 +59,8 @@ class AstrMessageEvent(): platform: str, role: str, global_object: GlobalObject, - llm_provider: Provider = None): + llm_provider: Provider = None, + session_id: int = None): self.message_str = message_str self.message_obj = message_obj self.gocq_platform = gocq_platform @@ -64,4 +68,5 @@ class AstrMessageEvent(): self.platform = platform self.role = role self.global_object = global_object - self.llm_provider = llm_provider \ No newline at end of file + self.llm_provider = llm_provider + self.session_id = session_id \ No newline at end of file diff --git a/main.py b/main.py index 635205e0..082d835c 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import os, sys from pip._internal import main as pipmain import warnings import traceback +import threading warnings.filterwarnings("ignore") abs_path = os.path.dirname(os.path.realpath(sys.argv[0])) + '/' @@ -128,4 +129,7 @@ if __name__ == "__main__": except BaseException as e: print(e) print(f"[System-err] Replit Web保活服务启动失败:{str(e)}") - main() + + t = threading.Thread(target=main, daemon=False) + t.start() + t.join() diff --git a/model/command/command.py b/model/command/command.py index b25f774b..64ee9a10 100644 --- a/model/command/command.py +++ b/model/command/command.py @@ -58,7 +58,8 @@ class Command: qq_sdk_platform=self.global_object.platform_qqchan, platform=platform, role=role, - global_object=self.global_object + global_object=self.global_object, + session_id = session_id ) for k, v in cached_plugins.items(): try: @@ -131,8 +132,6 @@ class Command: fail_rec = "" if plugins is None: return False, "未找到任何插件模块" - - print(plugins) for plugin in plugins: try: diff --git a/model/command/openai_official.py b/model/command/openai_official.py index 5974f12e..adf92df9 100644 --- a/model/command/openai_official.py +++ b/model/command/openai_official.py @@ -191,7 +191,7 @@ class CommandOpenAIOfficial(Command): def unset(self, session_id: str): if self.provider is None: return False, "未启动OpenAI ChatGPT语言模型.", "unset" - self.provider.now_personality = {} + self.provider.curr_personality = {} self.provider.forget(session_id) return True, "已清除人格并重置历史记录。", "unset" @@ -202,7 +202,7 @@ class CommandOpenAIOfficial(Command): if len(l) == 1: return True, f"【人格文本由PlexPt开源项目awesome-chatgpt-pr \ ompts-zh提供】\n设置人格: \n/set 人格名。例如/set 编剧\n人格列表: /set list\n人格详细信息: \ - /set view 人格名\n自定义人格: /set 人格文本\n重置会话(清除人格): /reset\n重置会话(保留人格): /reset p\n【当前人格】: {str(self.provider.now_personality)}", "set" + /set view 人格名\n自定义人格: /set 人格文本\n重置会话(清除人格): /reset\n重置会话(保留人格): /reset p\n【当前人格】: {str(self.provider.curr_personality)}", "set" elif l[1] == "list": msg = "人格列表:\n" for key in personalities.keys(): @@ -223,7 +223,7 @@ class CommandOpenAIOfficial(Command): else: ps = l[1].strip() if ps in personalities: - self.provider.now_personality = { + self.provider.curr_personality = { 'name': ps, 'prompt': personalities[ps] } @@ -245,7 +245,7 @@ class CommandOpenAIOfficial(Command): self.personality_str = message return True, f"人格{ps}已设置。", "set" else: - self.provider.now_personality = { + self.provider.curr_personality = { 'name': '自定义人格', 'prompt': ps } diff --git a/model/platform/qq.py b/model/platform/qq.py index a5234262..1b7a910d 100644 --- a/model/platform/qq.py +++ b/model/platform/qq.py @@ -4,8 +4,11 @@ from util.cmd_config import CmdConfig import asyncio from nakuru import ( CQHTTP, - GuildMessage + GuildMessage, + GroupMessage, + FriendMessage ) +from typing import Union import time @@ -155,18 +158,22 @@ class QQ: except Exception as e: raise e - def wait_for_message(self, group_id): + def wait_for_message(self, group_id) -> Union[GroupMessage, FriendMessage, GuildMessage]: ''' - 等待下一条消息 + 等待下一条消息,超时 300s 后抛出异常 ''' self.waiting[group_id] = '' + cnt = 0 while True: if group_id in self.waiting and self.waiting[group_id] != '': # 去掉 ret = self.waiting[group_id] del self.waiting[group_id] return ret - time.sleep(0.5) + cnt += 1 + if cnt > 300: + raise Exception("等待消息超时。") + time.sleep(1) def get_client(self): return self.client diff --git a/model/platform/qqchan.py b/model/platform/qqchan.py index a9e46ad8..7a7e902d 100644 --- a/model/platform/qqchan.py +++ b/model/platform/qqchan.py @@ -10,6 +10,7 @@ from util import general_utils as gu from nakuru.entities.components import Plain, At, Image from botpy.types.message import Reference from botpy import Client +import time class NakuruGuildMember(): tiny_id: int # 发送者识别号 @@ -38,6 +39,7 @@ class NakuruGuildMessage(): class QQChan(): def __init__(self, cnt: dict = None) -> None: self.qqchan_cnt = 0 + self.waiting: dict = {} def get_cnt(self): return self.qqchan_cnt @@ -133,7 +135,7 @@ class QQChan(): try: # reply_res = asyncio.run_coroutine_threadsafe(message.raw_message.reply(content=str(plain_text), message_reference = msg_ref, file_image=image_path), self.client.loop) - reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=message.channel_id, + reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=str(message.channel_id), content=str(plain_text), msg_id=message.message_id, file_image=image_path, @@ -146,7 +148,7 @@ class QQChan(): split_res.append(plain_text[:len(plain_text)//2]) split_res.append(plain_text[len(plain_text)//2:]) for i in split_res: - reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=message.channel_id, + reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=str(message.channel_id), content=str(i), msg_id=message.message_id, file_image=image_path, @@ -157,7 +159,7 @@ class QQChan(): try: # 防止被qq频道过滤消息 plain_text = plain_text.replace(".", " . ") - reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=message.channel_id, + reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=str(message.channel_id), content=str(plain_text), msg_id=message.message_id, file_image=image_path, @@ -166,7 +168,7 @@ class QQChan(): print("QQ频道API错误: \n"+str(e)) try: # reply_res = asyncio.run_coroutine_threadsafe(message.raw_message.reply(content=str(str.join(" ", plain_text)), message_reference = msg_ref, file_image=image_path), self.client.loop) - reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=message.channel_id, + reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=str(message.channel_id), content=str(str.join(" ", plain_text)), msg_id=message.message_id, file_image=image_path, @@ -174,23 +176,42 @@ class QQChan(): except BaseException as e: plain_text = re.sub(r'(https|http)?:\/\/(\w|\.|\/|\?|\=|\&|\%)*\b', '[被隐藏的链接]', str(e), flags=re.MULTILINE) plain_text = plain_text.replace(".", "·") - reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=message.channel_id, + reply_res = asyncio.run_coroutine_threadsafe(self.client.api.post_message(channel_id=str(message.channel_id), content=plain_text, msg_id=message.message_id, file_image=image_path, message_reference=msg_ref), self.client.loop).result() # send(message, f"QQ频道API错误:{str(e)}\n下面是格式化后的回答:\n{f_res}") - def push_message(self, channel_id: int, message_chain: list): + def push_message(self, channel_id: int, message_chain: list, message_id: int = None): ''' - 推送消息 + 推送消息, 如果有 message_id,那么就是回复消息。 ''' _n = NakuruGuildMessage() _n.channel_id = channel_id + _n.message_id = message_id self.send_qq_msg(_n, message_chain) - def send(self, message: NakuruGuildMessage, res: list): + def send(self, message_obj, message_chain: list): ''' - 同 send_qq_msg。回复频道消息 + 发送信息 ''' - self.send_qq_msg(message, res) \ No newline at end of file + self.send_qq_msg(message_obj, message_chain) + + def wait_for_message(self, channel_id: int) -> NakuruGuildMessage: + ''' + 等待指定 channel_id 的下一条信息,超时 300s 后抛出异常 + ''' + self.waiting[channel_id] = '' + cnt = 0 + while True: + if channel_id in self.waiting and self.waiting[channel_id] != '': + # 去掉 + ret = self.waiting[channel_id] + del self.waiting[channel_id] + return ret + cnt += 1 + if cnt > 300: + raise Exception("等待消息超时。") + time.sleep(1) + diff --git a/model/platform/qqgroup.py b/model/platform/qqgroup.py index bcbcacf6..75368999 100644 --- a/model/platform/qqgroup.py +++ b/model/platform/qqgroup.py @@ -2,8 +2,64 @@ import requests import asyncio import websockets from websockets import WebSocketClientProtocol -import threading import json +import inspect +from typing import Callable, Awaitable, Union +from pydantic import BaseModel +import datetime + +class Event(BaseModel): + GroupMessage: str = "GuildMessage" + +class Sender(BaseModel): + user_id: str + member_openid: str + + +class MessageComponent(BaseModel): + type: str + +class PlainText(MessageComponent): + text: str + +class Image(MessageComponent): + path: str + file: str + url: str + +class MessageChain(list): + + def append(self, __object: MessageComponent) -> None: + if not isinstance(__object, MessageComponent): + raise TypeError("不受支持的消息链元素类型。回复的消息链必须是 MessageComponent 的子类。") + return super().append(__object) + + def insert(self, __index: int, __object: MessageComponent) -> None: + if not isinstance(__object, MessageComponent): + raise TypeError("不受支持的消息链元素类型。回复的消息链必须是 MessageComponent 的子类。") + return super().insert(__index, __object) + + def parse_from_nakuru(self, nakuru_message_chain: Union[list, str]) -> None: + if isinstance(nakuru_message_chain, str): + self.append(PlainText(type='Plain', text=nakuru_message_chain)) + else: + for i in nakuru_message_chain: + if i['type'] == 'Plain': + self.append(PlainText(type='Plain', text=i['text'])) + elif i['type'] == 'Image': + self.append(Image(path=i['path'], file=i['file'], url=i['url'])) + +class Message(BaseModel): + type: str + user_id: str + member_openid: str + message_id: str + group_id: str + group_openid: str + content: str + message: MessageChain + time: int + sender: Sender class UnofficialQQBotSDK: @@ -13,43 +69,42 @@ class UnofficialQQBotSDK: def __init__(self, appid: str, client_secret: str) -> None: self.appid = appid self.client_secret = client_secret - self.get_access_token() - self.get_wss_endpoint() - asyncio.get_event_loop().run_until_complete(self.ws_client()) + self.events: dict[str, Awaitable] = {} - - - - def get_access_token(self) -> None: + def run_bot(self) -> None: + self.__get_access_token() + self.__get_wss_endpoint() + asyncio.get_event_loop().run_until_complete(self.__ws_client()) + + def __get_access_token(self) -> None: res = requests.post(self.GET_APP_ACCESS_TOKEN_URL, json={ "appId": self.appid, "clientSecret": self.client_secret }, headers={ "Content-Type": "application/json" }) - print(res.text) - self.access_token = 'QQBot ' + res.json()['access_token'] - print("access_token: " + self.access_token) + res = res.json() + code = res['code'] if 'code' in res else 1 + if 'access_token' not in res: + raise Exception(f"获取 access_token 失败。原因:{res['message'] if 'message' in res else '未知'}") + self.access_token = 'QQBot ' + res['access_token'] - def auth_header(self) -> str: + def __auth_header(self) -> str: return { 'Authorization': self.access_token, 'X-Union-Appid': self.appid, } - def get_wss_endpoint(self): - # self.wss_endpoint = requests.get(self.OPENAPI_BASE_URL + "/gateway", headers=self.auth_header()).json()['url'] - res = requests.get(self.OPENAPI_BASE_URL + "/gateway", headers=self.auth_header()) - print(res.text) + def __get_wss_endpoint(self): + res = requests.get(self.OPENAPI_BASE_URL + "/gateway", headers=self.__auth_header()) self.wss_endpoint = res.json()['url'] - print("wss_endpoint: " + self.wss_endpoint) + # print("wss_endpoint: " + self.wss_endpoint) - async def behav_heartbeat(self, ws: WebSocketClientProtocol, t: int): + async def __behav_heartbeat(self, ws: WebSocketClientProtocol, t: int): while True: await asyncio.sleep(t - 1) try: - print("heartbeat., s: " + str(self.s)) await ws.send(json.dumps({ "op": 1, "d": self.s @@ -57,12 +112,9 @@ class UnofficialQQBotSDK: except: print("heartbeat error.") - async def handle_msg(self, ws: WebSocketClientProtocol, msg: dict): + async def __handle_msg(self, ws: WebSocketClientProtocol, msg: dict): if msg['op'] == 10: - # hello - # 创建心跳任务 - print("hello.") - asyncio.get_event_loop().create_task(self.behav_heartbeat(ws, msg['d']['heartbeat_interval'] / 1000)) + asyncio.get_event_loop().create_task(self.__behav_heartbeat(ws, msg['d']['heartbeat_interval'] / 1000)) # 鉴权,获得session await ws.send(json.dumps({ "op": 2, @@ -77,36 +129,60 @@ class UnofficialQQBotSDK: } } })) - if msg['op'] == 0: # ready - print("ready.") data = msg['d'] - print(data) + event_typ: str = msg['t'] if 't' in msg else None + if event_typ == 'GROUP_AT_MESSAGE_CREATE': + if 'GroupMessage' in self.events: + coro = self.events['GroupMessage'] + else: + return + message_chain = MessageChain() + message_chain.append(PlainText(type="Plain", text=data['content'])) + group_message = Message( + type='GroupMessage', + user_id=data['author']['id'], + member_openid=data['author']['member_openid'], + message_id=data['id'], + group_id=data['group_id'], + group_openid=data['group_openid'], + content=data['content'], + # 2023-11-24T19:51:11+08:00 + time=int(datetime.datetime.strptime(data['timestamp'], "%Y-%m-%dT%H:%M:%S%z").timestamp()), + sender=Sender( + user_id=data['author']['id'], + member_openid=data['author']['member_openid'] + ), + message=message_chain + ) + await coro(self, group_message) - if 'group_openid' in data: - group_openid = data['group_openid'] - message_str = data['content'].strip() - message_id = data['id'] - # 发送消息 - requests.post(self.OPENAPI_BASE_URL + f"/v2/groups/{group_openid}/messages", headers=self.auth_header(), json={ - "content": message_str, - "message_type": 0, - "msg_id": message_id - }) + async def send(self, message: Message, message_chain: MessageChain) -> None: + # todo: 消息链转换支持更多类型。 + plain_text = "" + for i in message_chain: + if isinstance(i, PlainText): + plain_text += i.text + requests.post(self.OPENAPI_BASE_URL + f"/v2/groups/{message.group_openid}/messages", headers=self.__auth_header(), json={ + "content": plain_text, + "message_type": 0, + "msg_id": message.message_id + }) - async def ws_client(self): + async def __ws_client(self): self.s = 0 async with websockets.connect(self.wss_endpoint) as websocket: - print("ws connected.") while True: msg = await websocket.recv() msg = json.loads(msg) if 's' in msg: self.s = msg['s'] - print("recv: " + str(msg)) - await self.handle_msg(websocket, msg) - + await self.__handle_msg(websocket, msg) -if __name__ == "__main__": - UnofficialQQBotSDK("", "") \ No newline at end of file + def on(self, event: str) -> None: + def wrapper(func: Awaitable): + if inspect.iscoroutinefunction(func) == False: + raise TypeError("func must be a coroutine function") + self.events[event] = func + return wrapper \ No newline at end of file diff --git a/model/provider/openai_official.py b/model/provider/openai_official.py index 405fa930..9f469501 100644 --- a/model/provider/openai_official.py +++ b/model/provider/openai_official.py @@ -35,7 +35,7 @@ class ProviderOpenAIOfficial(Provider): self.api_base = None if 'api_base' in cfg and cfg['api_base'] != 'none' and cfg['api_base'] != '': self.api_base = cfg['api_base'] - print(f"设置 api_base 为: {self.api_base}") + gu.log(f"设置 api_base 为: {self.api_base}") # openai client self.client = OpenAI( api_key=self.key_list[0], @@ -83,7 +83,7 @@ class ProviderOpenAIOfficial(Provider): threading.Thread(target=self.dump_history, daemon=True).start() # 人格 - self.now_personality = {} + self.curr_personality = {} # 转储历史记录 @@ -109,11 +109,30 @@ class ProviderOpenAIOfficial(Provider): # 每隔10分钟转储一次 time.sleep(10*self.history_dump_interval) + def personality_set(self, default_personality: dict, session_id: str): + self.curr_personality = default_personality + new_record = { + "user": { + "role": "user", + "content": default_personality['prompt'], + }, + "AI": { + "role": "assistant", + "content": "好的,接下来我会扮演这个角色。" + }, + 'type': "personality", + 'usage_tokens': 0, + 'single-tokens': 0 + } + self.session_dict[session_id].append(new_record) + + def text_chat(self, prompt, session_id = None, image_url = None, function_call=None, - extra_conf: dict = None): + extra_conf: dict = None, + default_personality: dict = None): if session_id is None: session_id = "unknown" if "unknown" in self.session_dict: @@ -136,6 +155,12 @@ class ProviderOpenAIOfficial(Provider): f.flush() f.close() + if len(self.session_dict[session_id]) == 0: + # 设置默认人格 + if default_personality is not None: + self.personality_set(default_personality, session_id) + + # 使用 tictoken 截断消息 _encoded_prompt = self.enc.encode(prompt) if self.openai_model_configs['max_tokens'] < len(_encoded_prompt): diff --git a/model/provider/provider.py b/model/provider/provider.py index 23d07f56..a3a77867 100644 --- a/model/provider/provider.py +++ b/model/provider/provider.py @@ -5,7 +5,7 @@ class Provider: pass @abc.abstractmethod - def text_chat(self, prompt, session_id, image_url: None, function_call: None): + def text_chat(self, prompt, session_id, image_url: None, function_call: None, extra_conf: dict = None, default_personality: dict = None) -> str: pass @abc.abstractmethod diff --git a/model/provider/rev_chatgpt.py b/model/provider/rev_chatgpt.py index 24f94b22..fd1ece95 100644 --- a/model/provider/rev_chatgpt.py +++ b/model/provider/rev_chatgpt.py @@ -101,7 +101,12 @@ class ProviderRevChatGPT(Provider): # print("[RevChatGPT] "+str(resp)) return resp - def text_chat(self, prompt, session_id = None, image_url = None, function_call=None) -> str: + def text_chat(self, prompt, + session_id = None, + image_url = None, + function_call=None, + extra_conf: dict = None, + default_personality: dict = None) -> str: # 选择一个人少的账号。 selected_revstat = None diff --git a/model/provider/rev_edgegpt.py b/model/provider/rev_edgegpt.py index ce122f28..aadb8117 100644 --- a/model/provider/rev_edgegpt.py +++ b/model/provider/rev_edgegpt.py @@ -1,15 +1,15 @@ from model.provider.provider import Provider -# from EdgeGPT import Chatbot, ConversationStyle +from EdgeGPT import Chatbot, ConversationStyle import json import os from util import general_utils as gu from util.cmd_config import CmdConfig as cc import time -from EdgeGPT.EdgeUtils import Query, Cookie -from EdgeGPT.EdgeGPT import Chatbot as EdgeChatbot, ConversationStyle, NotAllowedToAccess class ProviderRevEdgeGPT(Provider): def __init__(self): + raise Exception("Bing 逆向已停止维护,不可用,请使用 ChatGPT 官方 API。") + self.busy = False self.wait_stack = [] with open('./cookies.json', 'r') as f: diff --git a/util/cmd_config.py b/util/cmd_config.py index 7ec087f6..9a138829 100644 --- a/util/cmd_config.py +++ b/util/cmd_config.py @@ -1,12 +1,13 @@ import os import json +from typing import Union cpath = "cmd_config.json" def check_exist(): if not os.path.exists(cpath): - with open(cpath, "w", encoding="utf-8") as f: - json.dump({}, f, indent=4) + with open(cpath, "w", encoding="utf-8-sig") as f: + json.dump({}, f, indent=4, ensure_ascii=False) f.flush() class CmdConfig(): @@ -14,7 +15,7 @@ class CmdConfig(): @staticmethod def get(key, default=None): check_exist() - with open(cpath, "r", encoding="utf-8") as f: + with open(cpath, "r", encoding="utf-8-sig") as f: d = json.load(f) if key in d: return d[key] @@ -24,30 +25,40 @@ class CmdConfig(): @staticmethod def get_all(): check_exist() - with open(cpath, "r", encoding="utf-8") as f: + with open(cpath, "r", encoding="utf-8-sig") as f: return json.load(f) @staticmethod def put(key, value): check_exist() - with open(cpath, "r", encoding="utf-8") as f: + with open(cpath, "r", encoding="utf-8-sig") as f: d = json.load(f) d[key] = value - with open(cpath, "w", encoding="utf-8") as f: - json.dump(d, f, indent=4) + with open(cpath, "w", encoding="utf-8-sig") as f: + json.dump(d, f, indent=4, ensure_ascii=False) f.flush() @staticmethod - def init_attributes(keys: list, init_val = ""): + def init_attributes(key: Union[str, list], init_val = ""): check_exist() - with open(cpath, "r", encoding="utf-8") as f: - d = json.load(f) - _tag = False - for k in keys: + conf_str = '' + with open(cpath, "r", encoding="utf-8-sig") as f: + conf_str = f.read() + if conf_str.startswith(u'/ufeff'): + conf_str = conf_str.encode('utf8')[3:].decode('utf8') + d = json.loads(conf_str) + _tag = False + + if isinstance(key, str): + if key not in d: + d[key] = init_val + _tag = True + elif isinstance(key, list): + for k in key: if k not in d: d[k] = init_val _tag = True - if _tag: - with open(cpath, "w", encoding="utf-8") as f: - json.dump(d, f, indent=4) - f.flush() \ No newline at end of file + if _tag: + with open(cpath, "w", encoding="utf-8-sig") as f: + json.dump(d, f, indent=4, ensure_ascii=False) + f.flush() \ No newline at end of file diff --git a/util/gplugin.py b/util/gplugin.py index 0c8a455a..5795179f 100644 --- a/util/gplugin.py +++ b/util/gplugin.py @@ -259,7 +259,7 @@ def web_search(question, provider: Provider, session_id, official_fc=False): if has_func: provider.forget(session_id) - question3 = f"""请你用活泼的语气回答`{question}`问题。\n以下是相关材料,请直接拿此材料针对问题进行总结回答。在引文末加上参考链接的标号,如` [1]`;在文章末尾加上各参考链接,如`[1] <标题> <网址>`;不要提到任何函数调用的信息;在总结的末尾加上 1-2 个相关的emoji。```\n{function_invoked_ret}\n```\n""" + question3 = f"""请你用活泼的语气回答`{question}`问题。\n以下是相关材料,请直接拿此材料针对问题进行总结回答。在文章末尾加上各参考链接,如`[1]