diff --git a/pkg/core/app.py b/pkg/core/app.py index f91d8986..595b01a8 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -63,19 +63,24 @@ def __init__(self): async def initialize(self): pass + # async def wait_loop(self): + async def run(self): await self.plugin_mgr.load_plugins() await self.plugin_mgr.initialize_plugins() + tasks = [] + try: tasks = [ asyncio.create_task(self.im_mgr.run()), asyncio.create_task(self.ctrl.run()) ] - await asyncio.wait(tasks) + await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.CancelledError: - self.logger.info("程序退出.") + pass except Exception as e: self.logger.error(f"应用运行致命异常: {e}") self.logger.debug(f"Traceback: {traceback.format_exc()}") + self.logger.info("程序退出.") diff --git a/pkg/core/bootutils/log.py b/pkg/core/bootutils/log.py index a63b7c92..308ca8c4 100644 --- a/pkg/core/bootutils/log.py +++ b/pkg/core/bootutils/log.py @@ -44,6 +44,7 @@ async def init_logging() -> logging.Logger: handler.setFormatter(color_formatter) qcg_logger.addHandler(handler) + qcg_logger.debug("日志初始化完成,日志级别:%s" % level) logging.basicConfig( level=logging.INFO, # 设置日志输出格式 format="[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s", diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index cdaa33ea..ffb8b77a 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -74,7 +74,7 @@ async def run_async(self): """异步运行""" raise NotImplementedError - def kill(self) -> bool: + async def kill(self) -> bool: """关闭适配器 Returns: diff --git a/pkg/platform/manager.py b/pkg/platform/manager.py index 7c07459d..3b359ee0 100644 --- a/pkg/platform/manager.py +++ b/pkg/platform/manager.py @@ -37,6 +37,11 @@ async def initialize(self): mirai_http_api_config = self.ap.platform_cfg.data['yiri-mirai-config'] self.bot_account_id = mirai_http_api_config['qq'] self.adapter = YiriMiraiAdapter(mirai_http_api_config) + elif self.ap.platform_cfg.data['platform-adapter'] == 'aiocqhttp': + from pkg.platform.sources.aiocqhttp import AiocqhttpAdapter + + aiocqhttp_config = self.ap.platform_cfg.data['aiocqhttp-config'] + self.adapter = AiocqhttpAdapter(aiocqhttp_config, self.ap) # elif config['msg_source_adapter'] == 'nakuru': # from pkg.platform.sources.nakuru import NakuruProjectAdapter # self.adapter = NakuruProjectAdapter(config['nakuru_config']) diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py new file mode 100644 index 00000000..c29c4b5c --- /dev/null +++ b/pkg/platform/sources/aiocqhttp.py @@ -0,0 +1,267 @@ +from __future__ import annotations +import typing +import asyncio +import traceback +import time +import datetime + +import mirai +import mirai.models.message as yiri_message +import aiocqhttp + +from .. import adapter +from ...pipeline.longtext.strategies import forward +from ...core import app + + +class AiocqhttpMessageConverter(adapter.MessageConverter): + + @staticmethod + def yiri2target(message_chain: mirai.MessageChain) -> typing.Tuple[list, int, datetime.datetime]: + msg_list = aiocqhttp.Message() + + msg_id = 0 + msg_time = None + + for msg in message_chain: + if type(msg) is mirai.Plain: + msg_list.append(aiocqhttp.MessageSegment.text(msg.text)) + elif type(msg) is yiri_message.Source: + msg_id = msg.id + msg_time = msg.time + elif type(msg) is mirai.Image: + msg_list.append(aiocqhttp.MessageSegment.image(msg.path)) + elif type(msg) is mirai.At: + msg_list.append(aiocqhttp.MessageSegment.at(msg.target)) + elif type(msg) is mirai.AtAll: + msg_list.append(aiocqhttp.MessageSegment.at("all")) + elif type(msg) is mirai.Face: + msg_list.append(aiocqhttp.MessageSegment.face(msg.face_id)) + elif type(msg) is mirai.Voice: + msg_list.append(aiocqhttp.MessageSegment.record(msg.path)) + elif type(msg) is forward.Forward: + print("aiocqhttp 暂不支持转发消息组件的转换,使用普通消息链发送") + + for node in msg.node_list: + msg_list.extend(AiocqhttpMessageConverter.yiri2target(node.message_chain)[0]) + + else: + msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) + + return msg_list, msg_id, msg_time + + @staticmethod + def target2yiri(message: str, message_id: int = -1): + message = aiocqhttp.Message(message) + + yiri_msg_list = [] + + yiri_msg_list.append( + yiri_message.Source(id=message_id, time=datetime.datetime.now()) + ) + + for msg in message: + if msg.type == "at": + if msg.data["qq"] == "all": + yiri_msg_list.append(yiri_message.AtAll()) + else: + yiri_msg_list.append( + yiri_message.At( + target=msg.data["qq"], + ) + ) + elif msg.type == "text": + yiri_msg_list.append(yiri_message.Plain(text=msg.data["text"])) + elif msg.type == "image": + yiri_msg_list.append(yiri_message.Image(url=msg.data["url"])) + + chain = mirai.MessageChain(yiri_msg_list) + + return chain + + +class AiocqhttpEventConverter(adapter.EventConverter): + + @staticmethod + def yiri2target(event: mirai.Event, bot_account_id: int): + + msg, msg_id, msg_time = AiocqhttpMessageConverter.yiri2target(event.message_chain) + + if type(event) is mirai.GroupMessage: + role = "member" + + if event.sender.permission == "ADMINISTRATOR": + role = "admin" + elif event.sender.permission == "OWNER": + role = "owner" + + payload = { + "post_type": "message", + "message_type": "group", + "time": int(msg_time.timestamp()), + "self_id": bot_account_id, + "sub_type": "normal", + "anonymous": None, + "font": 0, + "message": str(msg), + "raw_message": str(msg), + "sender": { + "age": 0, + "area": "", + "card": "", + "level": "", + "nickname": event.sender.member_name, + "role": role, + "sex": "unknown", + "title": "", + "user_id": event.sender.id, + }, + "user_id": event.sender.id, + "message_id": msg_id, + "group_id": event.group.id, + "message_seq": 0, + } + + return aiocqhttp.Event.from_payload(payload) + elif type(event) is mirai.FriendMessage: + + payload = { + "post_type": "message", + "message_type": "private", + "time": int(msg_time.timestamp()), + "self_id": bot_account_id, + "sub_type": "friend", + "target_id": bot_account_id, + "message": str(msg), + "raw_message": str(msg), + "font": 0, + "sender": { + "age": 0, + "nickname": event.sender.nickname, + "sex": "unknown", + "user_id": event.sender.id, + }, + "message_id": msg_id, + "user_id": event.sender.id, + } + + return aiocqhttp.Event.from_payload(payload) + + @staticmethod + def target2yiri(event: aiocqhttp.Event): + yiri_chain = AiocqhttpMessageConverter.target2yiri( + event.message, event.message_id + ) + + if event.message_type == "group": + permission = "MEMBER" + + if event.sender["role"] == "admin": + permission = "ADMINISTRATOR" + elif event.sender["role"] == "owner": + permission = "OWNER" + converted_event = mirai.GroupMessage( + sender=mirai.models.entities.GroupMember( + id=event.sender["user_id"], # message_seq 放哪? + member_name=event.sender["nickname"], + permission=permission, + group=mirai.models.entities.Group( + id=event.group_id, + name=event.sender["nickname"], + permission=mirai.models.entities.Permission.Member, + ), + special_title=event.sender["title"], + join_timestamp=0, + last_speak_timestamp=0, + mute_time_remaining=0, + ), + message_chain=yiri_chain, + time=event.time, + ) + return converted_event + elif event.message_type == "private": + return mirai.FriendMessage( + sender=mirai.models.entities.Friend( + id=event.sender["user_id"], + nickname=event.sender["nickname"], + remark="", + ), + message_chain=yiri_chain, + time=event.time, + ) + + +class AiocqhttpAdapter(adapter.MessageSourceAdapter): + + bot: aiocqhttp.CQHttp + + bot_account_id: int + + message_converter: AiocqhttpMessageConverter = AiocqhttpMessageConverter() + event_converter: AiocqhttpEventConverter = AiocqhttpEventConverter() + + config: dict + + ap: app.Application + + def __init__(self, config: dict, ap: app.Application): + self.config = config + self.ap = ap + + self.bot = aiocqhttp.CQHttp() + + async def send_message( + self, target_type: str, target_id: str, message: mirai.MessageChain + ): + return super().send_message(target_type, target_id, message) + + async def reply_message( + self, + message_source: mirai.MessageEvent, + message: mirai.MessageChain, + quote_origin: bool = False, + ): + + aiocq_event = AiocqhttpEventConverter.yiri2target(message_source, self.bot_account_id) + aiocq_msg = AiocqhttpMessageConverter.yiri2target(message)[0] + if quote_origin: + aiocq_msg = aiocqhttp.MessageSegment.reply(aiocq_event.message_id) + aiocq_msg + + return await self.bot.send( + aiocq_event, + aiocq_msg + ) + + async def is_muted(self, group_id: int) -> bool: + return False + + def register_listener( + self, + event_type: typing.Type[mirai.Event], + callback: typing.Callable[[mirai.Event], None], + ): + async def on_message(event: aiocqhttp.Event): + self.bot_account_id = event.self_id + self.ap.im_mgr.bot_account_id = event.self_id + try: + return await callback(self.event_converter.target2yiri(event)) + except: + traceback.print_exc() + + if event_type == mirai.GroupMessage: + self.bot.on_message("group")(on_message) + elif event_type == mirai.FriendMessage: + self.bot.on_message("private")(on_message) + + def unregister_listener( + self, + event_type: typing.Type[mirai.Event], + callback: typing.Callable[[mirai.Event], None], + ): + return super().unregister_listener(event_type, callback) + + async def run_async(self): + await self.bot._server_app.run_task(**self.config) + + async def kill(self) -> bool: + return False diff --git a/pkg/platform/sources/nakuru.py b/pkg/platform/sources/nakuru.py index 46e0ee54..ccdfe6ae 100644 --- a/pkg/platform/sources/nakuru.py +++ b/pkg/platform/sources/nakuru.py @@ -9,8 +9,7 @@ import nakuru.entities.components as nkc from .. import adapter as adapter_model -from ...platform import blob -from ...utils import context +from ...pipeline.longtext.strategies import forward class NakuruProjectMessageConverter(adapter_model.MessageConverter): @@ -49,7 +48,7 @@ def yiri2target(message_chain: mirai.MessageChain) -> list: nakuru_msg_list.append(nkc.Record.fromURL(component.url)) elif component.path is not None: nakuru_msg_list.append(nkc.Record.fromFileSystem(component.path)) - elif type(component) is blob.Forward: + elif type(component) is forward.Forward: # 转发消息 yiri_forward_node_list = component.node_list nakuru_forward_node_list = [] @@ -324,8 +323,5 @@ def run_sync(self): asyncio.set_event_loop(loop) self.bot.run() - async def run_async(self): - return await self.bot._run() - def kill(self) -> bool: - return False + return False \ No newline at end of file diff --git a/pkg/platform/sources/yirimirai.py b/pkg/platform/sources/yirimirai.py index 98b38ab2..04149623 100644 --- a/pkg/platform/sources/yirimirai.py +++ b/pkg/platform/sources/yirimirai.py @@ -113,5 +113,5 @@ def unregister_listener( async def run_async(self): return await MiraiRunner(self.bot)._run() - def kill(self) -> bool: + async def kill(self) -> bool: return False diff --git a/requirements.txt b/requirements.txt index 38560047..1a1274c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,11 @@ openai dulwich~=0.21.6 colorlog~=6.6.0 yiri-mirai-rc +aiocqhttp websockets urllib3 func_timeout~=4.3.5 Pillow -nakuru-project-idk CallingGPT tiktoken PyYaml diff --git a/templates/platform.json b/templates/platform.json index a25ba3e5..54100038 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -2,11 +2,15 @@ "platform-adapter": "yiri-mirai", "yiri-mirai-config": { "adapter": "WebSocketAdapter", - "host": "localhost", + "host": "127.0.0.1", "port": 8080, "verifyKey": "yirimirai", "qq": 123456789 }, + "aiocqhttp-config": { + "host": "127.0.0.1", + "port": 8080 + }, "track-function-calls": true, "quote-origin": false, "at-sender": false,