From fdbe993913903a0c829947fb3726d52081b286a9 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 21 Nov 2023 22:38:54 +0800 Subject: [PATCH 01/13] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=85=BC=E5=AE=B9=E7=9A=84=E4=B8=80=E4=BA=9B=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/platform/qqchan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/platform/qqchan.py b/model/platform/qqchan.py index 217598ae4..2147d81d2 100644 --- a/model/platform/qqchan.py +++ b/model/platform/qqchan.py @@ -76,7 +76,7 @@ class QQChan(): ngm.sub_type = "normal" ngm.message_id = message.id - ngm.guild_id = int(message.channel_id) + ngm.guild_id = int(message.guild_id) ngm.channel_id = int(message.channel_id) ngm.user_id = int(message.author.id) msg = [] From b36747c728fdbbd0c57e6b472b009f8717e20ec4 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 23 Nov 2023 11:55:22 +0800 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DQQ=E9=A2=91?= =?UTF-8?q?=E9=81=93=E5=8F=91=E5=9B=BE=E6=96=87=E6=B6=88=E6=81=AF=E6=8A=A5?= =?UTF-8?q?=E9=94=99=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/platform/qqchan.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/model/platform/qqchan.py b/model/platform/qqchan.py index 2147d81d2..486b45b42 100644 --- a/model/platform/qqchan.py +++ b/model/platform/qqchan.py @@ -133,7 +133,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 +146,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 +157,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 +166,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,7 +174,7 @@ 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, From 3e4818d0eeb7512acf6bc2750e41dee9cdbc5144 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 23 Nov 2023 17:50:40 +0800 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=E9=80=82=E9=85=8D=E9=83=A8?= =?UTF-8?q?=E5=88=86=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cores/qqbot/core.py | 9 +++++++-- cores/qqbot/global_object.py | 7 +++++-- model/command/command.py | 3 ++- model/platform/qq.py | 15 +++++++++++---- model/platform/qqchan.py | 30 ++++++++++++++++++++++++++++-- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/cores/qqbot/core.py b/cores/qqbot/core.py index 262483622..1ef1d6eb1 100644 --- a/cores/qqbot/core.py +++ b/cores/qqbot/core.py @@ -603,9 +603,14 @@ 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 + 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, diff --git a/cores/qqbot/global_object.py b/cores/qqbot/global_object.py index ace53fe2c..cc6873450 100644 --- a/cores/qqbot/global_object.py +++ b/cores/qqbot/global_object.py @@ -48,6 +48,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 +57,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 +66,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/model/command/command.py b/model/command/command.py index b23c4ec05..7fc068c61 100644 --- a/model/command/command.py +++ b/model/command/command.py @@ -50,7 +50,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: diff --git a/model/platform/qq.py b/model/platform/qq.py index a52342628..1b7a910d8 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 486b45b42..1f891b6f4 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 @@ -181,11 +183,35 @@ class QQChan(): 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_obj, message_chain: list): + ''' + 发送信息 + ''' + 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) \ No newline at end of file From baa57266b4409399fd78925f69f16bd508d976f9 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 25 Nov 2023 11:50:32 +0800 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=E5=88=9D=E6=AD=A5=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E5=AE=98=E6=96=B9QQ=E7=BE=A4=E6=9C=BA=E5=99=A8?= =?UTF-8?q?=E4=BA=BAAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cores/qqbot/core.py | 62 ++++++++++++--- model/platform/qqgroup.py | 162 ++++++++++++++++++++++++++++---------- 2 files changed, 172 insertions(+), 52 deletions(-) diff --git a/cores/qqbot/core.py b/cores/qqbot/core.py index 262483622..34e778507 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,6 +93,9 @@ PLATFORM_QQCHAN = 'qqchan' qqchan_loop = None client = None +# QQ群机器人 +PLATFROM_QQBOT = 'qqbot' + # 配置 cc.init_attributes(["qq_forward_threshold"], 200) cc.init_attributes(["qq_welcome"], "欢迎加入本群!\n欢迎给https://github.com/Soulter/QQChannelChatGPT项目一个Star😊~\n输入help查看帮助~\n") @@ -105,6 +115,8 @@ 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(["qq_forward_mode"], False) # QQ机器人 @@ -115,8 +127,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 @@ -350,9 +368,17 @@ def initBot(cfg, prov): _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", None) is not None and cc.get("qqbot_secret", None) is not None: + gu.log("- 启用QQ群机器人 -", gu.LEVEL_INFO) + thread_inst = threading.Thread(target=run_qqbot, args=(qqbot_loop, qq_bot,), daemon=False) + thread_inst.start() + + # GOCQ global gocq_bot - if 'gocqbot' in cfg and cfg['gocqbot']['enable']: gu.log("- 启用QQ机器人 -", gu.LEVEL_INFO) @@ -431,6 +457,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 +504,10 @@ 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) async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, NakuruGuildMessage], group: bool=False, @@ -493,13 +532,15 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak with_tag = False # 是否带有昵称 - if platform == PLATFORM_GOCQ or platform == PLATFORM_QQCHAN: + if platform == PLATFORM_QQCHAN or platform == PLATFROM_QQBOT: + with_tag = True + + if platform == PLATFORM_GOCQ or platform == PLATFORM_QQCHAN or platform == PLATFROM_QQBOT: _len = 0 for i in message.message: - if isinstance(i, Plain): + 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 @@ -545,9 +586,6 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak # 独立会话时,一个用户一个session session_id = sender_id - if platform == PLATFORM_QQCHAN: - with_tag = True - if qq_msg == "": await send_message(platform, message, f"Hi~", session_id=session_id) return @@ -812,4 +850,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/model/platform/qqgroup.py b/model/platform/qqgroup.py index bcbcacf66..842c1f6f3 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) - 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 From 89ed31a888131f923b1afa7452fb137c672f1d04 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 25 Nov 2023 11:55:26 +0800 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=9C=A8cmd=5F?= =?UTF-8?q?config=E4=B8=AD=E8=AE=BE=E7=BD=AEllm=5Fenv=5Fprompt=E6=9D=A5?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E7=8E=AF=E5=A2=83=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cores/qqbot/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cores/qqbot/core.py b/cores/qqbot/core.py index d77dc5a1a..61474e52c 100644 --- a/cores/qqbot/core.py +++ b/cores/qqbot/core.py @@ -117,6 +117,7 @@ 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(["qq_forward_mode"], False) # QQ机器人 @@ -692,7 +693,7 @@ 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 From 450dd34f4d271b95db9c7f685d861523bebcd33d Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 25 Nov 2023 11:59:39 +0800 Subject: [PATCH 06/13] =?UTF-8?q?perf:=20dump=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=97=B6=E5=85=B3=E9=97=AD=E5=BC=BA=E5=88=B6ascii?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- util/cmd_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/cmd_config.py b/util/cmd_config.py index 7ec087f6b..d5fc5cdcc 100644 --- a/util/cmd_config.py +++ b/util/cmd_config.py @@ -6,7 +6,7 @@ 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) + json.dump({}, f, indent=4, ensure_ascii=False) f.flush() class CmdConfig(): @@ -34,7 +34,7 @@ class CmdConfig(): d = json.load(f) d[key] = value with open(cpath, "w", encoding="utf-8") as f: - json.dump(d, f, indent=4) + json.dump(d, f, indent=4, ensure_ascii=False) f.flush() @staticmethod @@ -49,5 +49,5 @@ class CmdConfig(): _tag = True if _tag: with open(cpath, "w", encoding="utf-8") as f: - json.dump(d, f, indent=4) + json.dump(d, f, indent=4, ensure_ascii=False) f.flush() \ No newline at end of file From 5a57526aabccb477750eaf721e943f3fc2e93532 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 25 Nov 2023 19:56:11 +0800 Subject: [PATCH 07/13] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6BOM=E7=9A=84=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- util/cmd_config.py | 24 ++++++++++++++---------- util/gplugin.py | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/util/cmd_config.py b/util/cmd_config.py index d5fc5cdcc..0312505ee 100644 --- a/util/cmd_config.py +++ b/util/cmd_config.py @@ -40,14 +40,18 @@ class CmdConfig(): @staticmethod def init_attributes(keys: list, init_val = ""): check_exist() + conf_str = '' with open(cpath, "r", encoding="utf-8") as f: - d = json.load(f) - _tag = False - for k in keys: - 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, ensure_ascii=False) - f.flush() \ No newline at end of file + 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 + for k in keys: + 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, ensure_ascii=False) + f.flush() \ No newline at end of file diff --git a/util/gplugin.py b/util/gplugin.py index 0c8a455a4..5795179ff 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] <url>`;不要提到任何函数调用的信息;在总结的末尾加上1或2个相关的emoji。```\n{function_invoked_ret}\n```\n""" gu.log(f"web_search: {question3}", tag="web_search", level=gu.LEVEL_DEBUG, max_len=99999) _c = 0 while _c < 3: From 311178189f89e24be658553a499e1d4ab7c537ed Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 25 Nov 2023 20:09:21 +0800 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9C=AA?= =?UTF-8?q?=E6=9C=9F=E6=9C=9B=E7=9A=84QQ=E7=BE=A4BOT=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=92=8C=E6=96=87=E4=BB=B6BOM=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cores/qqbot/core.py | 2 +- util/cmd_config.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cores/qqbot/core.py b/cores/qqbot/core.py index 61474e52c..0c8345f81 100644 --- a/cores/qqbot/core.py +++ b/cores/qqbot/core.py @@ -372,7 +372,7 @@ def initBot(cfg, prov): global qq_bot, qqbot_loop qqbot_loop = asyncio.new_event_loop() - if cc.get("qqbot_appid", None) is not None and cc.get("qqbot_secret", None) is not None: + 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=False) thread_inst.start() diff --git a/util/cmd_config.py b/util/cmd_config.py index 0312505ee..03d81c43c 100644 --- a/util/cmd_config.py +++ b/util/cmd_config.py @@ -5,7 +5,7 @@ cpath = "cmd_config.json" def check_exist(): if not os.path.exists(cpath): - with open(cpath, "w", encoding="utf-8") as f: + with open(cpath, "w", encoding="utf-8-sig") as f: json.dump({}, f, indent=4, ensure_ascii=False) f.flush() @@ -14,7 +14,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,16 +24,16 @@ 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: + with open(cpath, "w", encoding="utf-8-sig") as f: json.dump(d, f, indent=4, ensure_ascii=False) f.flush() @@ -41,7 +41,7 @@ class CmdConfig(): def init_attributes(keys: list, init_val = ""): check_exist() conf_str = '' - with open(cpath, "r", encoding="utf-8") as f: + 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') @@ -52,6 +52,6 @@ class CmdConfig(): d[k] = init_val _tag = True if _tag: - with open(cpath, "w", encoding="utf-8") as f: + 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 From 23882bcb8e3ac9c24c8dd1d3f5d416d5244b6db2 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Sat, 25 Nov 2023 23:09:15 +0800 Subject: [PATCH 09/13] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d4f935dcd..6312415ed 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ - 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)支持) From d602041ad00b5b295299c01d40199b63aa0e4092 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Sat, 25 Nov 2023 23:10:54 +0800 Subject: [PATCH 10/13] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6312415ed..4f846c4cd 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) - 人格设置 From be1f8e70758a17bf1d788cf81bb67705fadd257a Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 30 Nov 2023 12:06:37 +0800 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=9C=A8?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E8=A1=8C=E6=93=8D=E4=BD=9Cbot=20fix:=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20windows=20=E4=B8=8B=20ctrl+c=20=E4=B8=8D?= =?UTF-8?q?=E8=83=BD=E9=80=80=E5=87=BA=E7=A8=8B=E5=BA=8F=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/plugins/helloworld/helloworld.py | 5 + cores/qqbot/core.py | 182 ++++++++++++++---------- main.py | 6 +- model/command/command.py | 2 - model/provider/openai_official.py | 2 +- model/provider/rev_edgegpt.py | 6 +- util/cmd_config.py | 16 ++- 7 files changed, 134 insertions(+), 85 deletions(-) diff --git a/addons/plugins/helloworld/helloworld.py b/addons/plugins/helloworld/helloworld.py index c76d4a9f9..5e8dccb98 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 0c8345f81..e4f39f7bb 100644 --- a/cores/qqbot/core.py +++ b/cores/qqbot/core.py @@ -96,28 +96,32 @@ client = None # QQ群机器人 PLATFROM_QQBOT = 'qqbot' -# 配置 -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") +# 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_name", "") # cc.init_attributes(["qq_forward_mode"], False) # QQ机器人 @@ -219,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 @@ -374,7 +381,7 @@ def initBot(cfg, prov): 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=False) + thread_inst = threading.Thread(target=run_qqbot, args=(qqbot_loop, qq_bot,), daemon=True) thread_inst.start() @@ -386,7 +393,7 @@ def initBot(cfg, prov): 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 = threading.Thread(target=run_gocq_bot, args=(gocq_loop, gocq_bot, gocq_app), daemon=True) thread_inst.start() else: gocq_bot = QQ(False) @@ -404,7 +411,7 @@ def initBot(cfg, prov): 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 = threading.Thread(target=run_qqchan_bot, args=(cfg, qqchan_loop, qqchannel_bot), daemon=True) thread_inst.start() # thread.join() @@ -412,7 +419,31 @@ def initBot(cfg, prov): input("[System-Error] 没有启用/成功启用任何机器人,程序退出") exit() - thread_inst.join() + 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 = 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 + await oper_msg(ngm, True, PLATFORM_CLI) ''' 运行QQ频道机器人 @@ -443,9 +474,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") @@ -509,6 +543,8 @@ async def send_message(platform, message, res, session_id = None): 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, @@ -533,59 +569,59 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak with_tag = False # 是否带有昵称 - if platform == PLATFORM_QQCHAN or platform == PLATFROM_QQBOT: + if platform == PLATFORM_QQCHAN or platform == PLATFROM_QQBOT or platform == PLATFORM_CLI: with_tag = True - if platform == PLATFORM_GOCQ or platform == PLATFORM_QQCHAN or platform == PLATFROM_QQBOT: - _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的频道功能 + _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": - session_id = message.channel_id - else: - session_id = message.group_id - else: + 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 - session_id = message.user_id - role = "member" + 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": - sender_id = str(message.sender.tiny_id) + session_id = message.channel_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 + 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) diff --git a/main.py b/main.py index 635205e0a..082d835cb 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 7fc068c61..3a3689d5e 100644 --- a/model/command/command.py +++ b/model/command/command.py @@ -126,8 +126,6 @@ class Command: fail_rec = "" if plugins is None: return False, "未找到任何插件模块" - - print(plugins) for plugin in plugins: try: diff --git a/model/provider/openai_official.py b/model/provider/openai_official.py index 405fa930b..2e0b36a95 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], diff --git a/model/provider/rev_edgegpt.py b/model/provider/rev_edgegpt.py index ce122f28d..aadb81179 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 03d81c43c..396ad721f 100644 --- a/util/cmd_config.py +++ b/util/cmd_config.py @@ -1,5 +1,6 @@ import os import json +from typing import Union cpath = "cmd_config.json" @@ -38,7 +39,7 @@ class CmdConfig(): f.flush() @staticmethod - def init_attributes(keys: list, init_val = ""): + def init_attributes(key: Union[str, list], init_val = ""): check_exist() conf_str = '' with open(cpath, "r", encoding="utf-8-sig") as f: @@ -47,10 +48,15 @@ class CmdConfig(): conf_str = conf_str.encode('utf8')[3:].decode('utf8') d = json.loads(conf_str) _tag = False - for k in keys: - if k not in d: - d[k] = init_val - _tag = True + + if isinstance(key, str): + 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-sig") as f: json.dump(d, f, indent=4, ensure_ascii=False) From 3df387995498c643db5da84efd0aef69b3119bfd Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 30 Nov 2023 12:46:29 +0800 Subject: [PATCH 12/13] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=BB=98=E8=AE=A4=E4=BA=BA=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cores/qqbot/core.py | 45 ++++++++++++++++++++----------- cores/qqbot/global_object.py | 2 ++ model/command/openai_official.py | 8 +++--- model/provider/openai_official.py | 29 ++++++++++++++++++-- model/provider/provider.py | 2 +- model/provider/rev_chatgpt.py | 7 ++++- util/cmd_config.py | 5 ++-- 7 files changed, 72 insertions(+), 26 deletions(-) diff --git a/cores/qqbot/core.py b/cores/qqbot/core.py index e4f39f7bb..a78367184 100644 --- a/cores/qqbot/core.py +++ b/cores/qqbot/core.py @@ -121,7 +121,7 @@ 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_name", "") +cc.init_attributes("default_personality_str", "") # cc.init_attributes(["qq_forward_mode"], False) # QQ机器人 @@ -419,6 +419,15 @@ def initBot(cfg, prov): input("[System-Error] 没有启用/成功启用任何机器人,程序退出") exit() + 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() @@ -430,21 +439,25 @@ async def cli(): prompt = input(">>> ") if prompt == "": continue - 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 + 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频道机器人 ''' @@ -729,13 +742,13 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak qq_msg = qq_msg[3:] web_sch_flag = True else: - qq_msg += cc.get("llm_env_prompt", "") + 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不想继续话题,重置会话后重试。 diff --git a/cores/qqbot/global_object.py b/cores/qqbot/global_object.py index cc6873450..d9871429e 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 # 纯消息字符串 diff --git a/model/command/openai_official.py b/model/command/openai_official.py index aa5247e6f..8d919a989 100644 --- a/model/command/openai_official.py +++ b/model/command/openai_official.py @@ -189,7 +189,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" @@ -200,7 +200,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(): @@ -221,7 +221,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] } @@ -243,7 +243,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/provider/openai_official.py b/model/provider/openai_official.py index 2e0b36a95..9f469501b 100644 --- a/model/provider/openai_official.py +++ b/model/provider/openai_official.py @@ -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 23d07f568..a3a77867c 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 24f94b22b..fd1ece956 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/util/cmd_config.py b/util/cmd_config.py index 396ad721f..9a138829a 100644 --- a/util/cmd_config.py +++ b/util/cmd_config.py @@ -50,8 +50,9 @@ class CmdConfig(): _tag = False if isinstance(key, str): - d[key] = init_val - _tag = True + if key not in d: + d[key] = init_val + _tag = True elif isinstance(key, list): for k in key: if k not in d: From 9bc8ac10fa058aadb2888495cfa0915fc72da710 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 2 Dec 2023 16:19:41 +0800 Subject: [PATCH 13/13] chore: remove some unuseful log --- model/command/command.py | 2 -- model/platform/qqgroup.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/model/command/command.py b/model/command/command.py index 3a3689d5e..15ef84383 100644 --- a/model/command/command.py +++ b/model/command/command.py @@ -71,10 +71,8 @@ class Command: if self.command_start_with(message, "nick"): return True, self.set_nick(message, platform, role) - if self.command_start_with(message, "plugin"): return True, self.plugin_oper(message, role, cached_plugins, platform) - if self.command_start_with(message, "myid") or self.command_start_with(message, "!myid"): return True, self.get_my_id(message_obj) if self.command_start_with(message, "nconf") or self.command_start_with(message, "newconf"): diff --git a/model/platform/qqgroup.py b/model/platform/qqgroup.py index 842c1f6f3..753689991 100644 --- a/model/platform/qqgroup.py +++ b/model/platform/qqgroup.py @@ -99,7 +99,7 @@ class UnofficialQQBotSDK: 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): while True: