From a1777860630810b9fc6e4a64bfc0308dd982be54 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 16 Jun 2025 13:18:59 +0800 Subject: [PATCH] feat: switch message platform adapters to sdk --- libs/qq_official_api/api.py | 2 +- libs/slack_api/api.py | 2 +- libs/wecom_api/api.py | 2 +- libs/wecom_customer_service_api/api.py | 2 +- pkg/command/entities.py | 2 +- pkg/pipeline/cntfilter/cntfilter.py | 2 +- pkg/pipeline/entities.py | 2 +- pkg/pipeline/longtext/longtext.py | 2 +- pkg/pipeline/longtext/strategies/forward.py | 2 +- pkg/pipeline/longtext/strategies/image.py | 2 +- pkg/pipeline/longtext/strategy.py | 3 +- pkg/pipeline/pipelinemgr.py | 3 +- pkg/pipeline/pool.py | 8 +- pkg/pipeline/preproc/preproc.py | 2 +- pkg/pipeline/process/handlers/chat.py | 2 +- pkg/pipeline/process/handlers/command.py | 4 +- pkg/pipeline/respback/respback.py | 4 +- pkg/pipeline/resprule/entities.py | 2 +- pkg/pipeline/resprule/rule.py | 2 +- pkg/pipeline/resprule/rules/atbot.py | 2 +- pkg/pipeline/resprule/rules/prefix.py | 2 +- pkg/pipeline/resprule/rules/random.py | 2 +- pkg/pipeline/resprule/rules/regexp.py | 2 +- pkg/pipeline/wrapper/wrapper.py | 2 +- pkg/platform/adapter.py | 156 ---- pkg/platform/adapter.yaml | 14 - pkg/platform/botmgr.py | 65 +- pkg/platform/logger.py | 5 +- pkg/platform/sources/aiocqhttp.py | 42 +- pkg/platform/sources/dingtalk.py | 23 +- pkg/platform/sources/discord.py | 50 +- pkg/platform/sources/lark.py | 62 +- pkg/platform/sources/legacy/gewechat.py | 24 +- pkg/platform/sources/legacy/nakuru.py | 22 +- pkg/platform/sources/legacy/qqbotpy.py | 22 +- pkg/platform/sources/officialaccount.py | 22 +- pkg/platform/sources/qqofficial.py | 22 +- pkg/platform/sources/slack.py | 22 +- pkg/platform/sources/telegram.py | 54 +- pkg/platform/sources/webchat.py | 50 +- pkg/platform/sources/wechatpad.py | 43 +- pkg/platform/sources/wecom.py | 22 +- pkg/platform/sources/wecomcs.py | 49 +- pkg/platform/types/__init__.py | 3 - pkg/platform/types/base.py | 107 --- pkg/platform/types/entities.py | 88 -- pkg/platform/types/events.py | 106 --- pkg/platform/types/message.py | 975 -------------------- pkg/plugin/context.py | 8 +- pkg/plugin/events.py | 6 +- pkg/provider/entities.py | 2 +- 51 files changed, 361 insertions(+), 1763 deletions(-) delete mode 100644 pkg/platform/adapter.py delete mode 100644 pkg/platform/adapter.yaml delete mode 100644 pkg/platform/types/__init__.py delete mode 100644 pkg/platform/types/base.py delete mode 100644 pkg/platform/types/entities.py delete mode 100644 pkg/platform/types/events.py delete mode 100644 pkg/platform/types/message.py diff --git a/libs/qq_official_api/api.py b/libs/qq_official_api/api.py index cb5f658a..c5728437 100644 --- a/libs/qq_official_api/api.py +++ b/libs/qq_official_api/api.py @@ -3,7 +3,7 @@ from quart import request import httpx from quart import Quart from typing import Callable, Dict, Any -from pkg.platform.types import events as platform_events +import langbot_plugin.api.entities.builtin.platform.events as platform_events from .qqofficialevent import QQOfficialEvent import json import traceback diff --git a/libs/slack_api/api.py b/libs/slack_api/api.py index 746d15da..241a42cf 100644 --- a/libs/slack_api/api.py +++ b/libs/slack_api/api.py @@ -4,7 +4,7 @@ from quart import Quart, jsonify, request from slack_sdk.web.async_client import AsyncWebClient from .slackevent import SlackEvent from typing import Callable -from pkg.platform.types import events as platform_events +import langbot_plugin.api.entities.builtin.platform.events as platform_events class SlackClient: diff --git a/libs/wecom_api/api.py b/libs/wecom_api/api.py index c1328b0d..352a550c 100644 --- a/libs/wecom_api/api.py +++ b/libs/wecom_api/api.py @@ -8,7 +8,7 @@ from quart import Quart import xml.etree.ElementTree as ET from typing import Callable, Dict, Any from .wecomevent import WecomEvent -from pkg.platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import aiofiles diff --git a/libs/wecom_customer_service_api/api.py b/libs/wecom_customer_service_api/api.py index 32fab7f7..f912326e 100644 --- a/libs/wecom_customer_service_api/api.py +++ b/libs/wecom_customer_service_api/api.py @@ -8,7 +8,7 @@ from quart import Quart import xml.etree.ElementTree as ET from typing import Callable from .wecomcsevent import WecomCSEvent -from pkg.platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import aiofiles diff --git a/pkg/command/entities.py b/pkg/command/entities.py index 7d6eecdc..2e4f8a96 100644 --- a/pkg/command/entities.py +++ b/pkg/command/entities.py @@ -6,7 +6,7 @@ import pydantic import langbot_plugin.api.entities.builtin.provider.session as provider_session from . import errors -from ..platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index 1708363a..b40ecd3c 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -5,7 +5,7 @@ from ...core import app from .. import stage, entities from . import filter as filter_model, entities as filter_entities from langbot_plugin.api.entities.builtin.provider import message as provider_message -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ...utils import importutil import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from . import filters diff --git a/pkg/pipeline/entities.py b/pkg/pipeline/entities.py index 7e7f23ce..5426685e 100644 --- a/pkg/pipeline/entities.py +++ b/pkg/pipeline/entities.py @@ -4,9 +4,9 @@ import enum import typing import pydantic -from ..platform.types import message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.platform.message as platform_message class ResultType(enum.Enum): diff --git a/pkg/pipeline/longtext/longtext.py b/pkg/pipeline/longtext/longtext.py index 6356a16f..097c166a 100644 --- a/pkg/pipeline/longtext/longtext.py +++ b/pkg/pipeline/longtext/longtext.py @@ -5,7 +5,7 @@ import traceback from . import strategy from .. import stage, entities -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ...utils import importutil import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from . import strategies diff --git a/pkg/pipeline/longtext/strategies/forward.py b/pkg/pipeline/longtext/strategies/forward.py index 574239b8..8040efff 100644 --- a/pkg/pipeline/longtext/strategies/forward.py +++ b/pkg/pipeline/longtext/strategies/forward.py @@ -4,8 +4,8 @@ from __future__ import annotations from .. import strategy as strategy_model -from ....platform.types import message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.platform.message as platform_message ForwardMessageDiaplay = platform_message.ForwardMessageDiaplay Forward = platform_message.Forward diff --git a/pkg/pipeline/longtext/strategies/image.py b/pkg/pipeline/longtext/strategies/image.py index ba6ddc1b..110f1f81 100644 --- a/pkg/pipeline/longtext/strategies/image.py +++ b/pkg/pipeline/longtext/strategies/image.py @@ -8,10 +8,10 @@ import re from PIL import Image, ImageDraw, ImageFont import functools -from ....platform.types import message as platform_message from .. import strategy as strategy_model import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.platform.message as platform_message @strategy_model.strategy_class('image') diff --git a/pkg/pipeline/longtext/strategy.py b/pkg/pipeline/longtext/strategy.py index dd69b2bb..018fb991 100644 --- a/pkg/pipeline/longtext/strategy.py +++ b/pkg/pipeline/longtext/strategy.py @@ -4,7 +4,8 @@ import typing from ...core import app -from ...platform.types import message as platform_message + +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index debdbb93..6dbfd1fa 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -9,7 +9,8 @@ from ..core import app from . import entities as pipeline_entities from ..entity.persistence import pipeline as persistence_pipeline from . import stage -from ..platform.types import message as platform_message, events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events from ..plugin import events from ..utils import importutil diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index a4313cdd..eb32fce6 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -3,11 +3,11 @@ from __future__ import annotations import asyncio import typing -from ..platform import adapter as msadapter -from ..platform.types import message as platform_message -from ..platform.types import events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter class QueryPool: @@ -35,7 +35,7 @@ class QueryPool: sender_id: typing.Union[int, str], message_event: platform_events.MessageEvent, message_chain: platform_message.MessageChain, - adapter: msadapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, pipeline_uuid: typing.Optional[str] = None, ) -> pipeline_query.Query: async with self.condition: diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index af851c96..894ceebf 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -5,7 +5,7 @@ import datetime from .. import stage, entities from langbot_plugin.api.entities.builtin.provider import message as provider_message from ...plugin import events -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index b871de81..717727d0 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -9,7 +9,7 @@ from ... import entities from ....provider import runner as runner_module from ....plugin import events -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ....utils import importutil from ....provider import runners import langbot_plugin.api.entities.builtin.provider.session as provider_session diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index 15c33ebd..a6156946 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -4,9 +4,9 @@ import typing from .. import handler from ... import entities -from langbot_plugin.api.entities.builtin.provider import message as provider_message from ....plugin import events -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.provider.message as provider_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index b5a1ed74..4ffc9ca4 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -4,8 +4,8 @@ import random import asyncio -from ...platform.types import events as platform_events -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message from .. import stage, entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/entities.py b/pkg/pipeline/resprule/entities.py index c2d964fe..71973c8a 100644 --- a/pkg/pipeline/resprule/entities.py +++ b/pkg/pipeline/resprule/entities.py @@ -1,6 +1,6 @@ import pydantic -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message class RuleJudgeResult(pydantic.BaseModel): diff --git a/pkg/pipeline/resprule/rule.py b/pkg/pipeline/resprule/rule.py index 7c91373f..34e89a72 100644 --- a/pkg/pipeline/resprule/rule.py +++ b/pkg/pipeline/resprule/rule.py @@ -5,7 +5,7 @@ import typing from ...core import app from . import entities -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/rules/atbot.py b/pkg/pipeline/resprule/rules/atbot.py index fc3b5510..cf31cc31 100644 --- a/pkg/pipeline/resprule/rules/atbot.py +++ b/pkg/pipeline/resprule/rules/atbot.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import rule as rule_model from .. import entities -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/rules/prefix.py b/pkg/pipeline/resprule/rules/prefix.py index 2ae89fe1..72f0de77 100644 --- a/pkg/pipeline/resprule/rules/prefix.py +++ b/pkg/pipeline/resprule/rules/prefix.py @@ -1,6 +1,6 @@ from .. import rule as rule_model from .. import entities -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/rules/random.py b/pkg/pipeline/resprule/rules/random.py index 04818ef0..2bfe8b71 100644 --- a/pkg/pipeline/resprule/rules/random.py +++ b/pkg/pipeline/resprule/rules/random.py @@ -3,7 +3,7 @@ import random from .. import rule as rule_model from .. import entities -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/rules/regexp.py b/pkg/pipeline/resprule/rules/regexp.py index 51589e0c..41e1df8e 100644 --- a/pkg/pipeline/resprule/rules/regexp.py +++ b/pkg/pipeline/resprule/rules/regexp.py @@ -3,7 +3,7 @@ import re from .. import rule as rule_model from .. import entities -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index 8063ff36..2c6e218e 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -5,7 +5,7 @@ import typing from .. import entities from .. import stage from ...plugin import events -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py deleted file mode 100644 index f27efc75..00000000 --- a/pkg/platform/adapter.py +++ /dev/null @@ -1,156 +0,0 @@ -from __future__ import annotations - -# MessageSource的适配器 -import typing -import abc -import pydantic - -from .types import message as platform_message -from .types import events as platform_events -from .logger import EventLogger - - -class MessagePlatformAdapter(pydantic.BaseModel, metaclass=abc.ABCMeta): - """消息平台适配器基类""" - - name: str - - bot_account_id: int - """机器人账号ID,需要在初始化时设置""" - - config: dict - - logger: EventLogger = pydantic.Field(exclude=True) - - def __init__(self, config: dict, logger: EventLogger): - """初始化适配器 - - Args: - config (dict): 对应的配置 - ap (app.Application): 应用上下文 - """ - self.config = config - self.logger = logger - - async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): - """主动发送消息 - - Args: - target_type (str): 目标类型,`person`或`group` - target_id (str): 目标ID - message (platform.types.MessageChain): 消息链 - """ - raise NotImplementedError - - async def reply_message( - self, - message_source: platform_events.MessageEvent, - message: platform_message.MessageChain, - quote_origin: bool = False, - ): - """回复消息 - - Args: - message_source (platform.types.MessageEvent): 消息源事件 - message (platform.types.MessageChain): 消息链 - quote_origin (bool, optional): 是否引用原消息. Defaults to False. - """ - raise NotImplementedError - - async def is_muted(self, group_id: int) -> bool: - """获取账号是否在指定群被禁言""" - raise NotImplementedError - - def register_listener( - self, - event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None], - ): - """注册事件监听器 - - Args: - event_type (typing.Type[platform.types.Event]): 事件类型 - callback (typing.Callable[[platform.types.Event], None]): 回调函数,接收一个参数,为事件 - """ - raise NotImplementedError - - def unregister_listener( - self, - event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None], - ): - """注销事件监听器 - - Args: - event_type (typing.Type[platform.types.Event]): 事件类型 - callback (typing.Callable[[platform.types.Event], None]): 回调函数,接收一个参数,为事件 - """ - raise NotImplementedError - - async def run_async(self): - """异步运行""" - raise NotImplementedError - - async def kill(self) -> bool: - """关闭适配器 - - Returns: - bool: 是否成功关闭,热重载时若此函数返回False则不会重载MessageSource底层 - """ - raise NotImplementedError - - -class MessageConverter: - """消息链转换器基类""" - - @staticmethod - def yiri2target(message_chain: platform_message.MessageChain): - """将源平台消息链转换为目标平台消息链 - - Args: - message_chain (platform.types.MessageChain): 源平台消息链 - - Returns: - typing.Any: 目标平台消息链 - """ - raise NotImplementedError - - @staticmethod - def target2yiri(message_chain: typing.Any) -> platform_message.MessageChain: - """将目标平台消息链转换为源平台消息链 - - Args: - message_chain (typing.Any): 目标平台消息链 - - Returns: - platform.types.MessageChain: 源平台消息链 - """ - raise NotImplementedError - - -class EventConverter: - """事件转换器基类""" - - @staticmethod - def yiri2target(event: typing.Type[platform_message.Event]): - """将源平台事件转换为目标平台事件 - - Args: - event (typing.Type[platform.types.Event]): 源平台事件 - - Returns: - typing.Any: 目标平台事件 - """ - raise NotImplementedError - - @staticmethod - def target2yiri(event: typing.Any) -> platform_message.Event: - """将目标平台事件的调用参数转换为源平台的事件参数对象 - - Args: - event (typing.Any): 目标平台事件 - - Returns: - typing.Type[platform.types.Event]: 源平台事件 - """ - raise NotImplementedError diff --git a/pkg/platform/adapter.yaml b/pkg/platform/adapter.yaml deleted file mode 100644 index d32b412d..00000000 --- a/pkg/platform/adapter.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: ComponentTemplate -metadata: - name: MessagePlatformAdapter - label: - en_US: Message Platform Adapter - zh_Hans: 消息平台适配器模板类 -spec: - type: - - python -execution: - python: - path: ./adapter.py - attr: MessagePlatformAdapter diff --git a/pkg/platform/botmgr.py b/pkg/platform/botmgr.py index 8f247ca4..59341493 100644 --- a/pkg/platform/botmgr.py +++ b/pkg/platform/botmgr.py @@ -1,15 +1,10 @@ from __future__ import annotations -import sys import asyncio import traceback import sqlalchemy -# FriendMessage, Image, MessageChain, Plain -from . import adapter as msadapter - from ..core import app, entities as core_entities, taskmgr -from .types import events as platform_events, message as platform_message from ..discover import engine @@ -20,11 +15,9 @@ from ..entity.errors import platform as platform_errors from .logger import EventLogger import langbot_plugin.api.entities.builtin.provider.session as provider_session - -# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题 -from . import types as mirai - -sys.modules['mirai'] = mirai +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter class RuntimeBot: @@ -36,7 +29,7 @@ class RuntimeBot: enable: bool - adapter: msadapter.MessagePlatformAdapter + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter task_wrapper: taskmgr.TaskWrapper @@ -48,7 +41,7 @@ class RuntimeBot: self, ap: app.Application, bot_entity: persistence_bot.Bot, - adapter: msadapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, logger: EventLogger, ): self.ap = ap @@ -61,7 +54,7 @@ class RuntimeBot: async def initialize(self): async def on_friend_message( event: platform_events.FriendMessage, - adapter: msadapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, ): image_components = [ component for component in event.message_chain if isinstance(component, platform_message.Image) @@ -86,7 +79,7 @@ class RuntimeBot: async def on_group_message( event: platform_events.GroupMessage, - adapter: msadapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, ): image_components = [ component for component in event.message_chain if isinstance(component, platform_message.Image) @@ -153,7 +146,7 @@ class PlatformManager: adapter_components: list[engine.Component] - adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]] + adapter_dict: dict[str, type[abstract_platform_adapter.AbstractMessagePlatformAdapter]] def __init__(self, ap: app.Application = None): self.ap = ap @@ -163,7 +156,7 @@ class PlatformManager: async def initialize(self): self.adapter_components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter') - adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]] = {} + adapter_dict: dict[str, type[abstract_platform_adapter.AbstractMessagePlatformAdapter]] = {} for component in self.adapter_components: adapter_dict[component.metadata.name] = component.get_python_component_class() self.adapter_dict = adapter_dict @@ -175,6 +168,7 @@ class PlatformManager: webchat_adapter_inst = webchat_adapter_class( {}, webchat_logger, + ap=self.ap, ) webchat_adapter_inst.ap = self.ap @@ -195,7 +189,7 @@ class PlatformManager: await self.load_bots_from_db() - def get_running_adapters(self) -> list[msadapter.MessagePlatformAdapter]: + def get_running_adapters(self) -> list[abstract_platform_adapter.AbstractMessagePlatformAdapter]: return [bot.adapter for bot in self.bots if bot.enable] async def load_bots_from_db(self): @@ -275,43 +269,6 @@ class PlatformManager: return component return None - async def write_back_config( - self, - adapter_name: str, - adapter_inst: msadapter.MessagePlatformAdapter, - config: dict, - ): - # index = -2 - - # for i, adapter in enumerate(self.adapters): - # if adapter == adapter_inst: - # index = i - # break - - # if index == -2: - # raise Exception('平台适配器未找到') - - # # 只修改启用的适配器 - # real_index = -1 - - # for i, adapter in enumerate(self.ap.platform_cfg.data['platform-adapters']): - # if adapter['enable']: - # index -= 1 - # if index == -1: - # real_index = i - # break - - # new_cfg = { - # 'adapter': adapter_name, - # 'enable': True, - # **config - # } - # self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg - # await self.ap.platform_cfg.dump_config() - - # TODO implement this - pass - async def run(self): # This method will only be called when the application launching await self.webchat_proxy_bot.run() diff --git a/pkg/platform/logger.py b/pkg/platform/logger.py index 340baa07..cedaeb50 100644 --- a/pkg/platform/logger.py +++ b/pkg/platform/logger.py @@ -9,7 +9,8 @@ import traceback import uuid from ..core import app -from .types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_event_logger class EventLogLevel(enum.Enum): @@ -55,7 +56,7 @@ MAX_LOG_COUNT = 200 DELETE_COUNT_PER_TIME = 50 -class EventLogger: +class EventLogger(abstract_platform_event_logger.AbstractEventLogger): """used for logging bot events""" ap: app.Application diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py index b2616bb0..ccfd3e53 100644 --- a/pkg/platform/sources/aiocqhttp.py +++ b/pkg/platform/sources/aiocqhttp.py @@ -5,16 +5,17 @@ import traceback import datetime import aiocqhttp +import pydantic -from .. import adapter -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ...utils import image -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger -class AiocqhttpMessageConverter(adapter.MessageConverter): +class AiocqhttpMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, @@ -69,7 +70,6 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): elif msg.face_type=='dice': msg_list.append(aiocqhttp.MessageSegment.dice()) - else: msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) @@ -190,6 +190,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): file_data = await bot.get_file(file_id=file_id) file_name = file_data.get('file_name') file_path = file_data.get('file') + _ = file_path file_url = file_data.get('file_url') file_size = file_data.get('file_size') yiri_msg_list.append(platform_message.File(id=file_id, name=file_name,url=file_url,size=file_size)) @@ -211,7 +212,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): return chain -class AiocqhttpEventConverter(adapter.EventConverter): +class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int): return event.source_platform_object @@ -262,21 +263,19 @@ class AiocqhttpEventConverter(adapter.EventConverter): ) -class AiocqhttpAdapter(adapter.MessagePlatformAdapter): - bot: aiocqhttp.CQHttp - - bot_account_id: int +class AiocqhttpAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: aiocqhttp.CQHttp = pydantic.Field(exclude=True, default_factory=aiocqhttp.CQHttp) message_converter: AiocqhttpMessageConverter = AiocqhttpMessageConverter() event_converter: AiocqhttpEventConverter = AiocqhttpEventConverter() - config: dict - on_websocket_connection_event_cache: typing.List[typing.Callable[[aiocqhttp.Event], None]] = [] - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): + super().__init__( + config=config, + logger=logger, + ) async def shutdown_trigger_placeholder(): while True: @@ -296,7 +295,6 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0] if target_type == 'group': - await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg) elif target_type == 'person': await self.bot.send_private_msg(user_id=int(target_id), message=aiocq_msg) @@ -320,7 +318,9 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: aiocqhttp.Event): self.bot_account_id = event.self_id @@ -351,7 +351,9 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 1727a771..2a76c219 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -1,17 +1,16 @@ import traceback import typing from libs.dingtalk_api.dingtalkevent import DingTalkEvent -from pkg.platform.types import message as platform_message -from pkg.platform.adapter import MessagePlatformAdapter -from .. import adapter -from ..types import events as platform_events -from ..types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from libs.dingtalk_api.api import DingTalkClient import datetime from ..logger import EventLogger -class DingTalkMessageConverter(adapter.MessageConverter): +class DingTalkMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content = '' @@ -47,7 +46,7 @@ class DingTalkMessageConverter(adapter.MessageConverter): return chain -class DingTalkEventConverter(adapter.EventConverter): +class DingTalkEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent): return event.source_platform_object @@ -91,7 +90,7 @@ class DingTalkEventConverter(adapter.EventConverter): ) -class DingTalkAdapter(adapter.MessagePlatformAdapter): +class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: DingTalkClient bot_account_id: str message_converter: DingTalkMessageConverter = DingTalkMessageConverter() @@ -137,7 +136,9 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: DingTalkEvent): try: @@ -171,6 +172,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index 52bd5e5b..5d24c77b 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -10,15 +10,16 @@ import os import datetime import aiohttp +import pydantic -from .. import adapter -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger -class DiscordMessageConverter(adapter.MessageConverter): +class DiscordMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, @@ -111,7 +112,7 @@ class DiscordMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(element_list) -class DiscordEventConverter(adapter.EventConverter): +class DiscordEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.Event) -> discord.Message: pass @@ -153,26 +154,21 @@ class DiscordEventConverter(adapter.EventConverter): ) -class DiscordAdapter(adapter.MessagePlatformAdapter): - bot: discord.Client - - bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识 - - config: dict +class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: discord.Client = pydantic.Field(exclude=True) message_converter: DiscordMessageConverter = DiscordMessageConverter() event_converter: DiscordEventConverter = DiscordEventConverter() listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): + bot_account_id = config['client_id'] - self.bot_account_id = self.config['client_id'] + listeners = {} adapter_self = self @@ -192,7 +188,15 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): if os.getenv('http_proxy'): args['proxy'] = os.getenv('http_proxy') - self.bot = MyClient(intents=intents, **args) + bot = MyClient(intents=intents, **args) + + super().__init__( + config=config, + logger=logger, + bot_account_id=bot_account_id, + listeners=listeners, + bot=bot, + ) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass @@ -227,14 +231,18 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 9e727ad3..7c8ae2eb 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -17,12 +17,13 @@ import aiohttp import lark_oapi.ws.exception import quart from lark_oapi.api.im.v1 import * +import pydantic -from .. import adapter -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger class AESCipher(object): @@ -51,7 +52,7 @@ class AESCipher(object): return self.decrypt(enc).decode('utf8') -class LarkMessageConverter(adapter.MessageConverter): +class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, api_client: lark_oapi.Client @@ -275,7 +276,7 @@ class LarkMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(lb_msg_list) -class LarkEventConverter(adapter.EventConverter): +class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target( event: platform_events.MessageEvent, @@ -319,31 +320,24 @@ class LarkEventConverter(adapter.EventConverter): ) -class LarkAdapter(adapter.MessagePlatformAdapter): - bot: lark_oapi.ws.Client - api_client: lark_oapi.Client - - bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识 - lark_tenant_key: str # 飞书企业key +class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: lark_oapi.ws.Client = pydantic.Field(exclude=True) + api_client: lark_oapi.Client = pydantic.Field(exclude=True) message_converter: LarkMessageConverter = LarkMessageConverter() event_converter: LarkEventConverter = LarkEventConverter() listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] - config: dict - quart_app: quart.Quart + quart_app: quart.Quart = pydantic.Field(exclude=True) - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger - self.quart_app = quart.Quart(__name__) - self.listeners = {} + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): + quart_app = quart.Quart(__name__) - @self.quart_app.route('/lark/callback', methods=['POST']) + @quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): try: data = await quart.request.json @@ -396,10 +390,20 @@ class LarkAdapter(adapter.MessagePlatformAdapter): lark_oapi.EventDispatcherHandler.builder('', '').register_p2_im_message_receive_v1(sync_on_message).build() ) - self.bot_account_id = config['bot_name'] + bot_account_id = config['bot_name'] - self.bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler) - self.api_client = lark_oapi.Client.builder().app_id(config['app_id']).app_secret(config['app_secret']).build() + bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler) + api_client = lark_oapi.Client.builder().app_id(config['app_id']).app_secret(config['app_secret']).build() + + super().__init__( + config=config, + logger=logger, + listeners={}, + quart_app=quart_app, + bot=bot, + api_client=api_client, + bot_account_id=bot_account_id, + ) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass @@ -448,14 +452,18 @@ class LarkAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/legacy/gewechat.py b/pkg/platform/sources/legacy/gewechat.py index 7e7b7715..cd5dcf22 100644 --- a/pkg/platform/sources/legacy/gewechat.py +++ b/pkg/platform/sources/legacy/gewechat.py @@ -11,11 +11,11 @@ import threading import quart import aiohttp -from ... import adapter +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from ....core import app -from ...types import message as platform_message -from ...types import events as platform_events -from ...types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ....utils import image import xml.etree.ElementTree as ET from typing import Optional, Tuple @@ -23,7 +23,7 @@ from functools import partial from ...logger import EventLogger -class GewechatMessageConverter(adapter.MessageConverter): +class GewechatMessageConverter(abstract_platform_adapter.AbstractMessageConverter): def __init__(self, config: dict): self.config = config @@ -398,7 +398,7 @@ class GewechatMessageConverter(adapter.MessageConverter): return from_user_name.endswith('@chatroom') -class GewechatEventConverter(adapter.EventConverter): +class GewechatEventConverter(abstract_platform_adapter.AbstractEventConverter): def __init__(self, config: dict): self.config = config self.message_converter = GewechatMessageConverter(config) @@ -458,7 +458,7 @@ class GewechatEventConverter(adapter.EventConverter): ) -class GeWeChatAdapter(adapter.MessagePlatformAdapter): +class GeWeChatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): name: str = 'gewechat' # 定义适配器名称 bot: gewechat_client.GewechatClient @@ -475,7 +475,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} def __init__(self, config: dict, ap: app.Application, logger: EventLogger): @@ -625,14 +625,18 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): pass diff --git a/pkg/platform/sources/legacy/nakuru.py b/pkg/platform/sources/legacy/nakuru.py index 5afb6356..52609792 100644 --- a/pkg/platform/sources/legacy/nakuru.py +++ b/pkg/platform/sources/legacy/nakuru.py @@ -9,15 +9,15 @@ import traceback import nakuru import nakuru.entities.components as nkc -from ... import adapter as adapter_model from ....pipeline.longtext.strategies import forward -from ...types import message as platform_message -from ...types import entities as platform_entities -from ...types import events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from ...logger import EventLogger -class NakuruProjectMessageConverter(adapter_model.MessageConverter): +class NakuruProjectMessageConverter(abstract_platform_adapter.AbstractMessageConverter): """消息转换器""" @staticmethod @@ -109,7 +109,7 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): return chain -class NakuruProjectEventConverter(adapter_model.EventConverter): +class NakuruProjectEventConverter(abstract_platform_adapter.AbstractEventConverter): """事件转换器""" @staticmethod @@ -164,7 +164,7 @@ class NakuruProjectEventConverter(adapter_model.EventConverter): raise Exception('未支持转换的事件类型: ' + str(event)) -class NakuruAdapter(adapter_model.MessagePlatformAdapter): +class NakuruAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): """nakuru-project适配器""" bot: nakuru.CQHTTP @@ -256,7 +256,9 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): try: source_cls = NakuruProjectEventConverter.yiri2target(event_type) @@ -283,7 +285,9 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): nakuru_event_name = self.event_converter.yiri2target(event_type).__name__ diff --git a/pkg/platform/sources/legacy/qqbotpy.py b/pkg/platform/sources/legacy/qqbotpy.py index 7e8fb125..90e4c2d7 100644 --- a/pkg/platform/sources/legacy/qqbotpy.py +++ b/pkg/platform/sources/legacy/qqbotpy.py @@ -10,13 +10,13 @@ import botpy import botpy.message as botpy_message import botpy.types.message as botpy_message_type -from ... import adapter as adapter_model +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from ....pipeline.longtext.strategies import forward from ....core import app from ....config import manager as cfg_mgr -from ...types import entities as platform_entities -from ...types import events as platform_events -from ...types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ...logger import EventLogger @@ -133,7 +133,7 @@ class OpenIDMapping(typing.Generic[K, V]): return value -class OfficialMessageConverter(adapter_model.MessageConverter): +class OfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter): """QQ 官方消息转换器""" @staticmethod @@ -237,7 +237,7 @@ class OfficialMessageConverter(adapter_model.MessageConverter): return chain -class OfficialEventConverter(adapter_model.EventConverter): +class OfficialEventConverter(abstract_platform_adapter.AbstractEventConverter): """事件转换器""" def __init__(self): @@ -333,7 +333,7 @@ class OfficialEventConverter(adapter_model.EventConverter): ) -class OfficialAdapter(adapter_model.MessagePlatformAdapter): +class OfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): """QQ 官方消息适配器""" bot: botpy.Client = None @@ -484,7 +484,9 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): try: @@ -507,7 +509,9 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): delattr(self.bot, event_handler_mapping[event_type]) diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 925b0ee4..74321b92 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -4,18 +4,18 @@ import asyncio import traceback import datetime -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.official_account_api.oaevent import OAEvent from libs.official_account_api.api import OAClient from libs.official_account_api.api import OAClientForLongerResponse -from .. import adapter -from ..types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events from ...command.errors import ParamNotEnoughError from ..logger import EventLogger -class OAMessageConverter(adapter.MessageConverter): +class OAMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): for msg in message_chain: @@ -33,7 +33,7 @@ class OAMessageConverter(adapter.MessageConverter): return chain -class OAEventConverter(adapter.EventConverter): +class OAEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def target2yiri(event: OAEvent): if event.type == 'text': @@ -55,7 +55,7 @@ class OAEventConverter(adapter.EventConverter): return None -class OfficialAccountAdapter(adapter.MessagePlatformAdapter): +class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: OAClient | OAClientForLongerResponse bot_account_id: str message_converter: OAMessageConverter = OAMessageConverter() @@ -116,7 +116,9 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: OAEvent): self.bot_account_id = event.receiver_id @@ -147,6 +149,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index cd7beb31..1160fd0e 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -5,10 +5,10 @@ import traceback import datetime -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message -from .. import adapter -from ..types import entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ...command.errors import ParamNotEnoughError from libs.qq_official_api.api import QQOfficialClient from libs.qq_official_api.qqofficialevent import QQOfficialEvent @@ -16,7 +16,7 @@ from ...utils import image from ..logger import EventLogger -class QQOfficialMessageConverter(adapter.MessageConverter): +class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content_list = [] @@ -45,7 +45,7 @@ class QQOfficialMessageConverter(adapter.MessageConverter): return chain -class QQOfficialEventConverter(adapter.EventConverter): +class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent: return event.source_platform_object @@ -131,7 +131,7 @@ class QQOfficialEventConverter(adapter.EventConverter): ) -class QQOfficialAdapter(adapter.MessagePlatformAdapter): +class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: QQOfficialClient config: dict bot_account_id: str @@ -212,7 +212,9 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: QQOfficialEvent): self.bot_account_id = 'justbot' @@ -245,6 +247,8 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index ff14ce1c..c2997828 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -6,17 +6,17 @@ import traceback import datetime from libs.slack_api.api import SlackClient -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.slack_api.slackevent import SlackEvent -from .. import adapter -from ..types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ...command.errors import ParamNotEnoughError from ...utils import image from ..logger import EventLogger -class SlackMessageConverter(adapter.MessageConverter): +class SlackMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content_list = [] @@ -43,7 +43,7 @@ class SlackMessageConverter(adapter.MessageConverter): return chain -class SlackEventConverter(adapter.EventConverter): +class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent) -> SlackEvent: return event.source_platform_object @@ -83,7 +83,7 @@ class SlackEventConverter(adapter.EventConverter): ) -class SlackAdapter(adapter.MessagePlatformAdapter): +class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: SlackClient bot_account_id: str message_converter: SlackMessageConverter = SlackMessageConverter() @@ -132,7 +132,9 @@ class SlackAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: SlackEvent): self.bot_account_id = 'SlackBot' @@ -163,6 +165,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 52d79853..69e781ef 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -9,15 +9,16 @@ import typing import traceback import base64 import aiohttp +import pydantic -from .. import adapter -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger -class TelegramMessageConverter(adapter.MessageConverter): +class TelegramMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain, bot: telegram.Bot) -> list[dict]: components = [] @@ -86,7 +87,7 @@ class TelegramMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(message_components) -class TelegramEventConverter(adapter.EventConverter): +class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent, bot: telegram.Bot): return event.source_platform_object @@ -128,26 +129,19 @@ class TelegramEventConverter(adapter.EventConverter): ) -class TelegramAdapter(adapter.MessagePlatformAdapter): - bot: telegram.Bot - application: telegram.ext.Application - - bot_account_id: str +class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: telegram.Bot = pydantic.Field(exclude=True) + application: telegram.ext.Application = pydantic.Field(exclude=True) message_converter: TelegramMessageConverter = TelegramMessageConverter() event_converter: TelegramEventConverter = TelegramEventConverter() - config: dict - listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger - + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.message.from_user.is_bot: return @@ -158,10 +152,16 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): except Exception: await self.logger.error(f'Error in telegram callback: {traceback.format_exc()}') - self.application = ApplicationBuilder().token(self.config['token']).build() - self.bot = self.application.bot - self.application.add_handler( - MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO, telegram_callback) + application = ApplicationBuilder().token(config['token']).build() + bot = application.bot + application.add_handler(MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO, telegram_callback)) + super().__init__( + config=config, + logger=logger, + bot=bot, + application=application, + bot_account_id='', + listeners={}, ) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): @@ -201,14 +201,18 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 0a35c1ac..0b5fc0ff 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -3,17 +3,19 @@ import logging import typing from datetime import datetime -from pydantic import BaseModel +import pydantic -from .. import adapter as msadapter -from ..types import events as platform_events, message as platform_message, entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ...core import app -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger logger = logging.getLogger(__name__) -class WebChatMessage(BaseModel): +class WebChatMessage(pydantic.BaseModel): id: int role: str content: str @@ -38,28 +40,35 @@ class WebChatSession: return self.message_lists[pipeline_uuid] -class WebChatAdapter(msadapter.MessagePlatformAdapter): +class WebChatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): """WebChat调试适配器,用于流水线调试""" - webchat_person_session: WebChatSession - webchat_group_session: WebChatSession + webchat_person_session: WebChatSession = pydantic.Field(exclude=True, default_factory=WebChatSession) + webchat_group_session: WebChatSession = pydantic.Field(exclude=True, default_factory=WebChatSession) - ap: app.Application # set by bot manager + ap: app.Application = pydantic.Field(exclude=True) # set by bot manager - listeners: typing.Dict[ + listeners: dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], None], - ] = {} + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], + ] = pydantic.Field(default_factory=dict, exclude=True) - def __init__(self, config: dict, logger: EventLogger): - self.logger = logger - self.config = config + debug_messages: dict[str, list[dict]] = pydantic.Field(default_factory=dict, exclude=True) + + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, ap: app.Application): + super().__init__( + config=config, + logger=logger, + ap=ap, + ) self.webchat_person_session = WebChatSession(id='webchatperson') self.webchat_group_session = WebChatSession(id='webchatgroup') self.bot_account_id = 'webchatbot' + self.debug_messages = {} + async def send_message( self, target_type: str, @@ -112,7 +121,9 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]], + func: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], typing.Awaitable[None] + ], ): """注册事件监听器""" self.listeners[event_type] = func @@ -120,11 +131,16 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]], + func: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], typing.Awaitable[None] + ], ): """取消注册事件监听器""" del self.listeners[event_type] + async def is_muted(self, group_id: int) -> bool: + return False + async def run_async(self): """运行适配器""" await self.logger.info('WebChat调试适配器已启动') diff --git a/pkg/platform/sources/wechatpad.py b/pkg/platform/sources/wechatpad.py index 0188d788..53d7a952 100644 --- a/pkg/platform/sources/wechatpad.py +++ b/pkg/platform/sources/wechatpad.py @@ -17,19 +17,19 @@ import threading import quart -from .. import adapter from ...core import app -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities from ..logger import EventLogger import xml.etree.ElementTree as ET from typing import Optional, Tuple from functools import partial import logging +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter -class WeChatPadMessageConverter(adapter.MessageConverter): +class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConverter): def __init__(self, config: dict): self.config = config self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) @@ -281,7 +281,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): """处理文件消息 (data_type=6)""" file_data = xml_data.find('.//appmsg') - if file_data.findtext('.//type', "") == "74": + if file_data.findtext('.//type', '') == '74': return None else: @@ -304,16 +304,19 @@ class WeChatPadMessageConverter(adapter.MessageConverter): file_data = self.bot.cdn_download(aeskey=aeskey, file_type=5, file_url=cdnthumburl) - file_base64 = file_data["Data"]['FileData'] + file_base64 = file_data['Data']['FileData'] # print(file_data) - file_size = file_data["Data"]['TotalSize'] + file_size = file_data['Data']['TotalSize'] # print(file_base64) - return platform_message.MessageChain([ - platform_message.WeChatFile(file_id=file_id, file_name=file_name, file_size=file_size, - file_base64=file_base64), - platform_message.WeChatForwardFile(xml_data=xml_data_str) - ]) + return platform_message.MessageChain( + [ + platform_message.WeChatFile( + file_id=file_id, file_name=file_name, file_size=file_size, file_base64=file_base64 + ), + platform_message.WeChatForwardFile(xml_data=xml_data_str), + ] + ) async def _handler_compound_link(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理链接消息(如公众号文章、外部网页)""" @@ -416,7 +419,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return from_user_name.endswith('@chatroom') -class WeChatPadEventConverter(adapter.EventConverter): +class WeChatPadEventConverter(abstract_platform_adapter.AbstractEventConverter): def __init__(self, config: dict): self.config = config self.message_converter = WeChatPadMessageConverter(config) @@ -476,7 +479,7 @@ class WeChatPadEventConverter(adapter.EventConverter): ) -class WeChatPadAdapter(adapter.MessagePlatformAdapter): +class WeChatPadAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): name: str = 'WeChatPad' # 定义适配器名称 bot: WeChatPadClient @@ -495,7 +498,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} def __init__(self, config: dict, ap: app.Application, logger: EventLogger): @@ -596,14 +599,18 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): pass diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 7bb0a757..88b89e03 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -6,17 +6,17 @@ import traceback import datetime from libs.wecom_api.api import WecomClient -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.wecom_api.wecomevent import WecomEvent -from .. import adapter -from ..types import entities as platform_entities from ...command.errors import ParamNotEnoughError from ...utils import image from ..logger import EventLogger +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities -class WecomMessageConverter(adapter.MessageConverter): +class WecomMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain, bot: WecomClient): content_list = [] @@ -70,7 +70,7 @@ class WecomMessageConverter(adapter.MessageConverter): return chain -class WecomEventConverter: +class WecomEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.Event, bot_account_id: int, bot: WecomClient) -> WecomEvent: # only for extracting user information @@ -126,7 +126,7 @@ class WecomEventConverter: return platform_events.FriendMessage(sender=friend, message_chain=yiri_chain, time=event.timestamp) -class WecomAdapter(adapter.MessagePlatformAdapter): +class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: WecomClient bot_account_id: str message_converter: WecomMessageConverter = WecomMessageConverter() @@ -192,7 +192,9 @@ class WecomAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: WecomEvent): self.bot_account_id = event.receiver_id @@ -224,6 +226,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/wecomcs.py b/pkg/platform/sources/wecomcs.py index fcd5378e..0958db68 100644 --- a/pkg/platform/sources/wecomcs.py +++ b/pkg/platform/sources/wecomcs.py @@ -4,18 +4,19 @@ import asyncio import traceback import datetime +import pydantic from libs.wecom_customer_service_api.api import WecomCSClient -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.wecom_customer_service_api.wecomcsevent import WecomCSEvent -from .. import adapter -from ..types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events from ...command.errors import ParamNotEnoughError -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger -class WecomMessageConverter(adapter.MessageConverter): +class WecomMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain, bot: WecomCSClient): content_list = [] @@ -68,7 +69,7 @@ class WecomMessageConverter(adapter.MessageConverter): return chain -class WecomEventConverter: +class WecomEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.Event, bot_account_id: int, bot: WecomCSClient) -> WecomCSEvent: # only for extracting user information @@ -116,17 +117,12 @@ class WecomEventConverter: ) -class WecomCSAdapter(adapter.MessagePlatformAdapter): - bot: WecomCSClient - bot_account_id: str +class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: WecomCSClient = pydantic.Field(exclude=True) message_converter: WecomMessageConverter = WecomMessageConverter() event_converter: WecomEventConverter = WecomEventConverter() - config: dict - - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): required_keys = [ 'corpid', 'secret', @@ -137,12 +133,20 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): if missing_keys: raise ParamNotEnoughError('企业微信客服缺少相关配置项,请查看文档或联系管理员') - self.bot = WecomCSClient( + bot = WecomCSClient( corpid=config['corpid'], secret=config['secret'], token=config['token'], EncodingAESKey=config['EncodingAESKey'], - logger=self.logger, + logger=logger, + ) + + super().__init__( + config=config, + logger=logger, + bot=bot, + bot_account_id='', + listeners={}, ) async def reply_message( @@ -169,7 +173,9 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: WecomCSEvent): self.bot_account_id = event.receiver_id @@ -198,9 +204,14 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): async def kill(self) -> bool: return False + async def is_muted(self, group_id: int) -> bool: + return False + async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/types/__init__.py b/pkg/platform/types/__init__.py deleted file mode 100644 index 998b0fb8..00000000 --- a/pkg/platform/types/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .entities import * -from .events import * -from .message import * diff --git a/pkg/platform/types/base.py b/pkg/platform/types/base.py deleted file mode 100644 index da58d4ed..00000000 --- a/pkg/platform/types/base.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import Dict, List, Type - -import pydantic.v1.main as pdm -from pydantic.v1 import BaseModel - - -class PlatformMetaclass(pdm.ModelMetaclass): - """此类是平台中使用的 pydantic 模型的元类的基类。""" - - -def to_camel(name: str) -> str: - """将下划线命名风格转换为小驼峰命名。""" - if name[:2] == '__': # 不处理双下划线开头的特殊命名。 - return name - name_parts = name.split('_') - return ''.join(name_parts[:1] + [x.title() for x in name_parts[1:]]) - - -class PlatformBaseModel(BaseModel, metaclass=PlatformMetaclass): - """模型基类。 - - 启用了三项配置: - 1. 允许解析时传入额外的值,并将额外值保存在模型中。 - 2. 允许通过别名访问字段。 - 3. 自动生成小驼峰风格的别名。 - """ - - def __init__(self, *args, **kwargs): - """""" - super().__init__(*args, **kwargs) - - def __repr__(self) -> str: - return ( - self.__class__.__name__ + '(' + ', '.join((f'{k}={repr(v)}' for k, v in self.__dict__.items() if v)) + ')' - ) - - class Config: - extra = 'allow' - allow_population_by_field_name = True - alias_generator = to_camel - - -class PlatformIndexedMetaclass(PlatformMetaclass): - """可以通过子类名获取子类的类的元类。""" - - __indexedbases__: List[Type['PlatformIndexedModel']] = [] - __indexedmodel__ = None - - def __new__(cls, name, bases, attrs, **kwargs): - new_cls = super().__new__(cls, name, bases, attrs, **kwargs) - # 第一类:PlatformIndexedModel - if name == 'PlatformIndexedModel': - cls.__indexedmodel__ = new_cls - new_cls.__indexes__ = {} - return new_cls - # 第二类:PlatformIndexedModel 的直接子类,这些是可以通过子类名获取子类的类。 - if cls.__indexedmodel__ in bases: - cls.__indexedbases__.append(new_cls) - new_cls.__indexes__ = {} - return new_cls - # 第三类:PlatformIndexedModel 的直接子类的子类,这些添加到直接子类的索引中。 - for base in cls.__indexedbases__: - if issubclass(new_cls, base): - base.__indexes__[name] = new_cls - return new_cls - - def __getitem__(cls, name): - return cls.get_subtype(name) - - -class PlatformIndexedModel(PlatformBaseModel, metaclass=PlatformIndexedMetaclass): - """可以通过子类名获取子类的类。""" - - __indexes__: Dict[str, Type['PlatformIndexedModel']] - - @classmethod - def get_subtype(cls, name: str) -> Type['PlatformIndexedModel']: - """根据类名称,获取相应的子类类型。 - - Args: - name: 类名称。 - - Returns: - Type['PlatformIndexedModel']: 子类类型。 - """ - try: - type_ = cls.__indexes__.get(name) - if not (type_ and issubclass(type_, cls)): - raise ValueError(f'`{name}` 不是 `{cls.__name__}` 的子类!') - return type_ - except AttributeError: - raise ValueError(f'`{name}` 不是 `{cls.__name__}` 的子类!') from None - - @classmethod - def parse_subtype(cls, obj: dict) -> 'PlatformIndexedModel': - """通过字典,构造对应的模型对象。 - - Args: - obj: 一个字典,包含了模型对象的属性。 - - Returns: - PlatformIndexedModel: 构造的对象。 - """ - if cls in PlatformIndexedModel.__subclasses__(): - ModelType = cls.get_subtype(obj['type']) - return ModelType.parse_obj(obj) - return super().parse_obj(obj) diff --git a/pkg/platform/types/entities.py b/pkg/platform/types/entities.py deleted file mode 100644 index d989ffce..00000000 --- a/pkg/platform/types/entities.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -""" -此模块提供实体和配置项模型。 -""" - -import abc -from datetime import datetime -from enum import Enum -import typing - -import pydantic.v1 as pydantic - - -class Entity(pydantic.BaseModel): - """实体,表示一个用户或群。""" - - id: int - """ID。""" - - @abc.abstractmethod - def get_name(self) -> str: - """名称。""" - - -class Friend(Entity): - """私聊对象。""" - - id: typing.Union[int, str] - """ID。""" - nickname: typing.Optional[str] - """昵称。""" - remark: typing.Optional[str] - """备注。""" - - def get_name(self) -> str: - return self.nickname or self.remark or '' - - -class Permission(str, Enum): - """群成员身份权限。""" - - Member = 'MEMBER' - """成员。""" - Administrator = 'ADMINISTRATOR' - """管理员。""" - Owner = 'OWNER' - """群主。""" - - def __repr__(self) -> str: - return repr(self.value) - - -class Group(Entity): - """群。""" - - id: typing.Union[int, str] - """群号。""" - name: str - """群名称。""" - permission: Permission - """Bot 在群中的权限。""" - - def get_name(self) -> str: - return self.name - - -class GroupMember(Entity): - """群成员。""" - - id: typing.Union[int, str] - """群员 ID。""" - member_name: str - """群员名称。""" - permission: Permission - """在群中的权限。""" - group: Group - """群。""" - special_title: str = '' - """群头衔。""" - join_timestamp: datetime = datetime.utcfromtimestamp(0) - """加入群的时间。""" - last_speak_timestamp: datetime = datetime.utcfromtimestamp(0) - """最后一次发言的时间。""" - mute_time_remaining: int = 0 - """禁言剩余时间。""" - - def get_name(self) -> str: - return self.member_name diff --git a/pkg/platform/types/events.py b/pkg/platform/types/events.py deleted file mode 100644 index 5ffccb9b..00000000 --- a/pkg/platform/types/events.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -""" -此模块提供事件模型。 -""" - -import typing - -import pydantic.v1 as pydantic - -from . import entities as platform_entities -from . import message as platform_message - - -class Event(pydantic.BaseModel): - """事件基类。 - - Args: - type: 事件名。 - """ - - type: str - """事件名。""" - - def __repr__(self): - return ( - self.__class__.__name__ - + '(' - + ', '.join((f'{k}={repr(v)}' for k, v in self.__dict__.items() if k != 'type' and v)) - + ')' - ) - - @classmethod - def parse_subtype(cls, obj: dict) -> 'Event': - try: - return typing.cast(Event, super().parse_subtype(obj)) - except ValueError: - return Event(type=obj['type']) - - @classmethod - def get_subtype(cls, name: str) -> typing.Type['Event']: - try: - return typing.cast(typing.Type[Event], super().get_subtype(name)) - except ValueError: - return Event - - -############################### -# Message Event -class MessageEvent(Event): - """消息事件。 - - Args: - type: 事件名。 - message_chain: 消息内容。 - """ - - type: str - """事件名。""" - message_chain: platform_message.MessageChain - """消息内容。""" - - time: float | None = None - """消息发送时间戳。""" - - source_platform_object: typing.Optional[typing.Any] = None - """原消息平台对象。 - 供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息, - 那么可以将其存到这个字段以供之后取出使用。""" - - -class FriendMessage(MessageEvent): - """私聊消息。 - - Args: - type: 事件名。 - sender: 发送消息的好友。 - message_chain: 消息内容。 - """ - - type: str = 'FriendMessage' - """事件名。""" - sender: platform_entities.Friend - """发送消息的好友。""" - message_chain: platform_message.MessageChain - """消息内容。""" - - -class GroupMessage(MessageEvent): - """群消息。 - - Args: - type: 事件名。 - sender: 发送消息的群成员。 - message_chain: 消息内容。 - """ - - type: str = 'GroupMessage' - """事件名。""" - sender: platform_entities.GroupMember - """发送消息的群成员。""" - message_chain: platform_message.MessageChain - """消息内容。""" - - @property - def group(self) -> platform_entities.Group: - return self.sender.group diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py deleted file mode 100644 index 7dad4145..00000000 --- a/pkg/platform/types/message.py +++ /dev/null @@ -1,975 +0,0 @@ -import itertools -import logging -import typing -from datetime import datetime -from pathlib import Path -import base64 - -import aiofiles -import httpx -import pydantic.v1 as pydantic - -from . import entities as platform_entities -from .base import PlatformBaseModel, PlatformIndexedMetaclass, PlatformIndexedModel - -logger = logging.getLogger(__name__) - - -class MessageComponentMetaclass(PlatformIndexedMetaclass): - """消息组件元类。""" - - __message_component__ = None - - def __new__(cls, name, bases, attrs, **kwargs): - new_cls = super().__new__(cls, name, bases, attrs, **kwargs) - if name == 'MessageComponent': - cls.__message_component__ = new_cls - - if not cls.__message_component__: - return new_cls - - for base in bases: - if issubclass(base, cls.__message_component__): - # 获取字段名 - if hasattr(new_cls, '__fields__'): - # 忽略 type 字段 - new_cls.__parameter_names__ = list(new_cls.__fields__)[1:] - else: - new_cls.__parameter_names__ = [] - break - - return new_cls - - -class MessageComponent(PlatformIndexedModel, metaclass=MessageComponentMetaclass): - """消息组件。""" - - type: str - """消息组件类型。""" - - def __str__(self): - return '' - - def __repr__(self): - return ( - self.__class__.__name__ - + '(' - + ', '.join((f'{k}={repr(v)}' for k, v in self.__dict__.items() if k != 'type' and v)) - + ')' - ) - - def __init__(self, *args, **kwargs): - # 解析参数列表,将位置参数转化为具名参数 - parameter_names = self.__parameter_names__ - if len(args) > len(parameter_names): - raise TypeError(f'`{self.type}`需要{len(parameter_names)}个参数,但传入了{len(args)}个。') - for name, value in zip(parameter_names, args): - if name in kwargs: - raise TypeError(f'在 `{self.type}` 中,具名参数 `{name}` 与位置参数重复。') - kwargs[name] = value - - super().__init__(**kwargs) - - -TMessageComponent = typing.TypeVar('TMessageComponent', bound=MessageComponent) - - -class MessageChain(PlatformBaseModel): - """消息链。 - - 一个构造消息链的例子: - ```py - message_chain = MessageChain([ - AtAll(), - Plain("Hello World!"), - ]) - ``` - - `Plain` 可以省略。 - ```py - message_chain = MessageChain([ - AtAll(), - "Hello World!", - ]) - ``` - - 在调用 API 时,参数中需要 MessageChain 的,也可以使用 `List[MessageComponent]` 代替。 - 例如,以下两种写法是等价的: - ```py - await bot.send_friend_message(12345678, [ - Plain("Hello World!") - ]) - ``` - ```py - await bot.send_friend_message(12345678, MessageChain([ - Plain("Hello World!") - ])) - ``` - - 可以使用 `in` 运算检查消息链中: - 1. 是否有某个消息组件。 - 2. 是否有某个类型的消息组件。 - - ```py - if AtAll in message_chain: - print('AtAll') - - if At(bot.qq) in message_chain: - print('At Me') - ``` - - """ - - __root__: typing.List[MessageComponent] - - @staticmethod - def _parse_message_chain(msg_chain: typing.Iterable): - result = [] - for msg in msg_chain: - if isinstance(msg, dict): - result.append(MessageComponent.parse_subtype(msg)) - elif isinstance(msg, MessageComponent): - result.append(msg) - elif isinstance(msg, str): - result.append(Plain(msg)) - else: - raise TypeError(f'消息链中元素需为 dict 或 str 或 MessageComponent,当前类型:{type(msg)}') - return result - - @pydantic.validator('__root__', always=True, pre=True) - def _parse_component(cls, msg_chain): - if isinstance(msg_chain, (str, MessageComponent)): - msg_chain = [msg_chain] - if not msg_chain: - msg_chain = [] - return cls._parse_message_chain(msg_chain) - - @classmethod - def parse_obj(cls, msg_chain: typing.Iterable): - """通过列表形式的消息链,构造对应的 `MessageChain` 对象。 - - Args: - msg_chain: 列表形式的消息链。 - """ - result = cls._parse_message_chain(msg_chain) - return cls(__root__=result) - - def __init__(self, __root__: typing.Iterable[MessageComponent] = None): - super().__init__(__root__=__root__) - - def __str__(self): - return ''.join(str(component) for component in self.__root__) - - def __repr__(self): - return f'{self.__class__.__name__}({self.__root__!r})' - - def __iter__(self): - yield from self.__root__ - - def get_first(self, t: typing.Type[TMessageComponent]) -> typing.Optional[TMessageComponent]: - """获取消息链中第一个符合类型的消息组件。""" - for component in self: - if isinstance(component, t): - return component - return None - - @typing.overload - def __getitem__(self, index: int) -> MessageComponent: ... - - @typing.overload - def __getitem__(self, index: slice) -> typing.List[MessageComponent]: ... - - @typing.overload - def __getitem__(self, index: typing.Type[TMessageComponent]) -> typing.List[TMessageComponent]: ... - - @typing.overload - def __getitem__( - self, index: typing.Tuple[typing.Type[TMessageComponent], int] - ) -> typing.List[TMessageComponent]: ... - - def __getitem__( - self, - index: typing.Union[ - int, - slice, - typing.Type[TMessageComponent], - typing.Tuple[typing.Type[TMessageComponent], int], - ], - ) -> typing.Union[MessageComponent, typing.List[MessageComponent], typing.List[TMessageComponent]]: - return self.get(index) - - def __setitem__( - self, - key: typing.Union[int, slice], - value: typing.Union[MessageComponent, str, typing.Iterable[typing.Union[MessageComponent, str]]], - ): - if isinstance(value, str): - value = Plain(value) - if isinstance(value, typing.Iterable): - value = (Plain(c) if isinstance(c, str) else c for c in value) - self.__root__[key] = value # type: ignore - - def __delitem__(self, key: typing.Union[int, slice]): - del self.__root__[key] - - def __reversed__(self) -> typing.Iterable[MessageComponent]: - return reversed(self.__root__) - - def has( - self, - sub: typing.Union[MessageComponent, typing.Type[MessageComponent], 'MessageChain', str], - ) -> bool: - """判断消息链中: - 1. 是否有某个消息组件。 - 2. 是否有某个类型的消息组件。 - - Args: - sub (`Union[MessageComponent, Type[MessageComponent], 'MessageChain', str]`): - 若为 `MessageComponent`,则判断该组件是否在消息链中。 - 若为 `Type[MessageComponent]`,则判断该组件类型是否在消息链中。 - - Returns: - bool: 是否找到。 - """ - if isinstance(sub, type): # 检测消息链中是否有某种类型的对象 - for i in self: - if type(i) is sub: - return True - return False - if isinstance(sub, MessageComponent): # 检查消息链中是否有某个组件 - for i in self: - if i == sub: - return True - return False - raise TypeError(f'类型不匹配,当前类型:{type(sub)}') - - def __contains__(self, sub) -> bool: - return self.has(sub) - - def __ge__(self, other): - return other in self - - def __len__(self) -> int: - return len(self.__root__) - - def __add__(self, other: typing.Union['MessageChain', MessageComponent, str]) -> 'MessageChain': - if isinstance(other, MessageChain): - return self.__class__(self.__root__ + other.__root__) - if isinstance(other, str): - return self.__class__(self.__root__ + [Plain(other)]) - if isinstance(other, MessageComponent): - return self.__class__(self.__root__ + [other]) - return NotImplemented - - def __radd__(self, other: typing.Union[MessageComponent, str]) -> 'MessageChain': - if isinstance(other, MessageComponent): - return self.__class__([other] + self.__root__) - if isinstance(other, str): - return self.__class__([typing.cast(MessageComponent, Plain(other))] + self.__root__) - return NotImplemented - - def __mul__(self, other: int): - if isinstance(other, int): - return self.__class__(self.__root__ * other) - return NotImplemented - - def __rmul__(self, other: int): - return self.__mul__(other) - - def __iadd__(self, other: typing.Iterable[typing.Union[MessageComponent, str]]): - self.extend(other) - - def __imul__(self, other: int): - if isinstance(other, int): - self.__root__ *= other - return NotImplemented - - def index( - self, - x: typing.Union[MessageComponent, typing.Type[MessageComponent]], - i: int = 0, - j: int = -1, - ) -> int: - """返回 x 在消息链中首次出现项的索引号(索引号在 i 或其后且在 j 之前)。 - - Args: - x (`Union[MessageComponent, Type[MessageComponent]]`): - 要查找的消息元素或消息元素类型。 - i: 从哪个位置开始查找。 - j: 查找到哪个位置结束。 - - Returns: - int: 如果找到,则返回索引号。 - - Raises: - ValueError: 没有找到。 - TypeError: 类型不匹配。 - """ - if isinstance(x, type): - l = len(self) - if i < 0: - i += l - if i < 0: - i = 0 - if j < 0: - j += l - if j > l: - j = l - for index in range(i, j): - if type(self[index]) is x: - return index - raise ValueError('消息链中不存在该类型的组件。') - if isinstance(x, MessageComponent): - return self.__root__.index(x, i, j) - raise TypeError(f'类型不匹配,当前类型:{type(x)}') - - def count(self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]]) -> int: - """返回消息链中 x 出现的次数。 - - Args: - x (`Union[MessageComponent, Type[MessageComponent]]`): - 要查找的消息元素或消息元素类型。 - - Returns: - int: 次数。 - """ - if isinstance(x, type): - return sum(1 for i in self if type(i) is x) - if isinstance(x, MessageComponent): - return self.__root__.count(x) - raise TypeError(f'类型不匹配,当前类型:{type(x)}') - - def extend(self, x: typing.Iterable[typing.Union[MessageComponent, str]]): - """将另一个消息链中的元素添加到消息链末尾。 - - Args: - x: 另一个消息链,也可为消息元素或字符串元素的序列。 - """ - self.__root__.extend(Plain(c) if isinstance(c, str) else c for c in x) - - def append(self, x: typing.Union[MessageComponent, str]): - """将一个消息元素或字符串元素添加到消息链末尾。 - - Args: - x: 消息元素或字符串元素。 - """ - self.__root__.append(Plain(x) if isinstance(x, str) else x) - - def insert(self, i: int, x: typing.Union[MessageComponent, str]): - """将一个消息元素或字符串添加到消息链中指定位置。 - - Args: - i: 插入位置。 - x: 消息元素或字符串元素。 - """ - self.__root__.insert(i, Plain(x) if isinstance(x, str) else x) - - def pop(self, i: int = -1) -> MessageComponent: - """从消息链中移除并返回指定位置的元素。 - - Args: - i: 移除位置。默认为末尾。 - - Returns: - MessageComponent: 移除的元素。 - """ - return self.__root__.pop(i) - - def remove(self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]]): - """从消息链中移除指定元素或指定类型的一个元素。 - - Args: - x: 指定的元素或元素类型。 - """ - if isinstance(x, type): - self.pop(self.index(x)) - if isinstance(x, MessageComponent): - self.__root__.remove(x) - - def exclude( - self, - x: typing.Union[MessageComponent, typing.Type[MessageComponent]], - count: int = -1, - ) -> 'MessageChain': - """返回移除指定元素或指定类型的元素后剩余的消息链。 - - Args: - x: 指定的元素或元素类型。 - count: 至多移除的数量。默认为全部移除。 - - Returns: - MessageChain: 剩余的消息链。 - """ - - def _exclude(): - nonlocal count - x_is_type = isinstance(x, type) - for c in self: - if count > 0 and ((x_is_type and type(c) is x) or c == x): - count -= 1 - continue - yield c - - return self.__class__(_exclude()) - - def reverse(self): - """将消息链原地翻转。""" - self.__root__.reverse() - - @classmethod - def join(cls, *args: typing.Iterable[typing.Union[str, MessageComponent]]): - return cls(Plain(c) if isinstance(c, str) else c for c in itertools.chain(*args)) - - @property - def source(self) -> typing.Optional['Source']: - """获取消息链中的 `Source` 对象。""" - return self.get_first(Source) - - @property - def message_id(self) -> int: - """获取消息链的 message_id,若无法获取,返回 -1。""" - source = self.source - return source.id if source else -1 - - -TMessage = typing.Union[ - MessageChain, - typing.Iterable[typing.Union[MessageComponent, str]], - MessageComponent, - str, -] -"""可以转化为 MessageChain 的类型。""" - - -class Source(MessageComponent): - """源。包含消息的基本信息。""" - - type: str = 'Source' - """消息组件类型。""" - id: typing.Union[int, str] - """消息的识别号,用于引用回复(Source 类型永远为 MessageChain 的第一个元素)。""" - time: datetime - """消息时间。""" - - -class Plain(MessageComponent): - """纯文本。""" - - type: str = 'Plain' - """消息组件类型。""" - text: str - """文字消息。""" - - def __str__(self): - return self.text - - def __repr__(self): - return f'Plain({self.text!r})' - - -class Quote(MessageComponent): - """引用。""" - - type: str = 'Quote' - """消息组件类型。""" - id: typing.Optional[int] = None - """被引用回复的原消息的 message_id。""" - group_id: typing.Optional[typing.Union[int, str]] = None - """被引用回复的原消息所接收的群号,当为好友消息时为0。""" - sender_id: typing.Optional[typing.Union[int, str]] = None - """被引用回复的原消息的发送者的ID。""" - target_id: typing.Optional[typing.Union[int, str]] = None - """被引用回复的原消息的接收者者的ID或群ID。""" - origin: MessageChain - """被引用回复的原消息的消息链对象。""" - - @pydantic.validator('origin', always=True, pre=True) - def origin_formater(cls, v): - return MessageChain.parse_obj(v) - - -class At(MessageComponent): - """At某人。""" - - type: str = 'At' - """消息组件类型。""" - target: typing.Union[int, str] - """群员 ID。""" - display: typing.Optional[str] = None - """At时显示的文字,发送消息时无效,自动使用群名片。""" - - def __eq__(self, other): - return isinstance(other, At) and self.target == other.target - - def __str__(self): - return f'@{self.display or self.target}' - - -class AtAll(MessageComponent): - """At全体。""" - - type: str = 'AtAll' - """消息组件类型。""" - - def __str__(self): - return '@全体成员' - - -class Image(MessageComponent): - """图片。""" - - type: str = 'Image' - """消息组件类型。""" - image_id: typing.Optional[str] = None - """图片的 image_id,不为空时将忽略 url 属性。""" - url: typing.Optional[pydantic.HttpUrl] = None - """图片的 URL,发送时可作网络图片的链接;接收时为图片的链接,可用于图片下载。""" - path: typing.Union[str, Path, None] = None - """图片的路径,发送本地图片。""" - base64: typing.Optional[str] = None - """图片的 Base64 编码。""" - - def __eq__(self, other): - return isinstance(other, Image) and self.type == other.type and self.uuid == other.uuid - - def __str__(self): - return '[图片]' - - @pydantic.validator('path') - def validate_path(cls, path: typing.Union[str, Path, None]): - """修复 path 参数的行为,使之相对于 LangBot 的启动路径。""" - if path: - try: - return str(Path(path).resolve(strict=True)) - except FileNotFoundError: - raise ValueError(f'无效路径:{path}') - else: - return path - - @property - def uuid(self): - image_id = self.image_id - if image_id[0] == '{': # 群图片 - image_id = image_id[1:37] - elif image_id[0] == '/': # 好友图片 - image_id = image_id[1:] - return image_id - - async def get_bytes(self) -> typing.Tuple[bytes, str]: - """获取图片的 bytes 和 mime type""" - if self.url: - async with httpx.AsyncClient() as client: - response = await client.get(self.url) - response.raise_for_status() - return response.content, response.headers.get('Content-Type') - elif self.base64: - mime_type = 'image/jpeg' - - split_index = self.base64.find(';base64,') - if split_index == -1: - raise ValueError('Invalid base64 string') - - mime_type = self.base64[5:split_index] - base64_data = self.base64[split_index + 8 :] - - return base64.b64decode(base64_data), mime_type - elif self.path: - async with aiofiles.open(self.path, 'rb') as f: - return await f.read(), 'image/jpeg' - else: - raise ValueError('Can not get bytes from image') - - @classmethod - async def from_local( - cls, - filename: typing.Union[str, Path, None] = None, - content: typing.Optional[bytes] = None, - ) -> 'Image': - """从本地文件路径加载图片,以 base64 的形式传递。 - - Args: - filename: 从本地文件路径加载图片,与 `content` 二选一。 - content: 从本地文件内容加载图片,与 `filename` 二选一。 - - Returns: - Image: 图片对象。 - """ - if content: - pass - elif filename: - path = Path(filename) - import aiofiles - - async with aiofiles.open(path, 'rb') as f: - content = await f.read() - else: - raise ValueError('请指定图片路径或图片内容!') - import base64 - - img = cls(base64=base64.b64encode(content).decode()) - return img - - @classmethod - def from_unsafe_path(cls, path: typing.Union[str, Path]) -> 'Image': - """从不安全的路径加载图片。 - - Args: - path: 从不安全的路径加载图片。 - - Returns: - Image: 图片对象。 - """ - return cls.construct(path=str(path)) - - -class Unknown(MessageComponent): - """未知。""" - - type: str = 'Unknown' - """消息组件类型。""" - text: str - """文本。""" - - def __str__(self): - return f'Unknown Message: {self.text}' - - -class Voice(MessageComponent): - """语音。""" - - type: str = 'Voice' - """消息组件类型。""" - voice_id: typing.Optional[str] = None - """语音的 voice_id,不为空时将忽略 url 属性。""" - url: typing.Optional[str] = None - """语音的 URL,发送时可作网络语音的链接;接收时为语音文件的链接,可用于语音下载。""" - path: typing.Optional[str] = None - """语音的路径,发送本地语音。""" - base64: typing.Optional[str] = None - """语音的 Base64 编码。""" - length: typing.Optional[int] = None - """语音的长度,单位为秒。""" - - @pydantic.validator('path') - def validate_path(cls, path: typing.Optional[str]): - """修复 path 参数的行为,使之相对于 LangBot 的启动路径。""" - if path: - try: - return str(Path(path).resolve(strict=True)) - except FileNotFoundError: - raise ValueError(f'无效路径:{path}') - else: - return path - - def __str__(self): - return '[语音]' - - async def download( - self, - filename: typing.Union[str, Path, None] = None, - directory: typing.Union[str, Path, None] = None, - ): - """下载语音到本地。 - - Args: - filename: 下载到本地的文件路径。与 `directory` 二选一。 - directory: 下载到本地的文件夹路径。与 `filename` 二选一。 - """ - if not self.url: - logger.warning(f'语音 `{self.voice_id}` 无 url 参数,下载失败。') - return - - import httpx - - async with httpx.AsyncClient() as client: - response = await client.get(self.url) - response.raise_for_status() - content = response.content - - if filename: - path = Path(filename) - path.parent.mkdir(parents=True, exist_ok=True) - elif directory: - path = Path(directory) - path.mkdir(parents=True, exist_ok=True) - path = path / f'{self.voice_id}.silk' - else: - raise ValueError('请指定文件路径或文件夹路径!') - - import aiofiles - - async with aiofiles.open(path, 'wb') as f: - await f.write(content) - - @classmethod - async def from_local( - cls, - filename: typing.Union[str, Path, None] = None, - content: typing.Optional[bytes] = None, - ) -> 'Voice': - """从本地文件路径加载语音,以 base64 的形式传递。 - - Args: - filename: 从本地文件路径加载语音,与 `content` 二选一。 - content: 从本地文件内容加载语音,与 `filename` 二选一。 - """ - if content: - pass - if filename: - path = Path(filename) - import aiofiles - - async with aiofiles.open(path, 'rb') as f: - content = await f.read() - else: - raise ValueError('请指定语音路径或语音内容!') - import base64 - - img = cls(base64=base64.b64encode(content).decode()) - return img - - -class ForwardMessageNode(pydantic.BaseModel): - """合并转发中的一条消息。""" - - sender_id: typing.Optional[typing.Union[int, str]] = None - """发送人ID。""" - sender_name: typing.Optional[str] = None - """显示名称。""" - message_chain: typing.Optional[MessageChain] = None - """消息内容。""" - message_id: typing.Optional[int] = None - """消息的 message_id。""" - time: typing.Optional[datetime] = None - """发送时间。""" - - @pydantic.validator('message_chain', check_fields=False) - def _validate_message_chain(cls, value: typing.Union[MessageChain, list]): - if isinstance(value, list): - return MessageChain.parse_obj(value) - return value - - @classmethod - def create( - cls, - sender: typing.Union[platform_entities.Friend, platform_entities.GroupMember], - message: MessageChain, - ) -> 'ForwardMessageNode': - """从消息链生成转发消息。 - - Args: - sender: 发送人。 - message: 消息内容。 - - Returns: - ForwardMessageNode: 生成的一条消息。 - """ - return ForwardMessageNode(sender_id=sender.id, sender_name=sender.get_name(), message_chain=message) - - -class ForwardMessageDiaplay(pydantic.BaseModel): - title: str = '群聊的聊天记录' - brief: str = '[聊天记录]' - source: str = '聊天记录' - preview: typing.List[str] = [] - summary: str = '查看x条转发消息' - - -class Forward(MessageComponent): - """合并转发。""" - - type: str = 'Forward' - """消息组件类型。""" - display: ForwardMessageDiaplay - """显示信息""" - node_list: typing.List[ForwardMessageNode] - """转发消息节点列表。""" - - def __init__(self, *args, **kwargs): - if len(args) == 1: - self.node_list = args[0] - super().__init__(**kwargs) - super().__init__(*args, **kwargs) - - def __str__(self): - return '[聊天记录]' - - -class File(MessageComponent): - """文件。""" - - type: str = 'File' - """消息组件类型。""" - id: str = '' - """文件识别 ID。""" - name: str - """文件名称。""" - size: int = 0 - """文件大小。""" - url: str - """文件路径""" - - def __str__(self): - return f'[文件]{self.name}' - -class Face(MessageComponent): - """系统表情 - 此处将超级表情骰子/划拳,一同归类于face - 当face_type为rps(划拳)时 face_id 对应的是手势 - 当face_type为dice(骰子)时 face_id 对应的是点数 - """ - type: str = 'Face' - """表情类型""" - face_type: str = 'face' - """表情id""" - face_id: int = 0 - """表情名""" - face_name: str = '' - - def __str__(self): - if self.face_type == 'face': - return f'[表情]{self.face_name}' - elif self.face_type == 'dice': - return f'[表情]{self.face_id}点的{self.face_name}' - elif self.face_type == 'rps': - return f'[表情]{self.face_name}({self.rps_data(self.face_id)})' - - - def rps_data(self,face_id): - rps_dict ={ - 1 : "布", - 2 : "剪刀", - 3 : "石头", - } - return rps_dict[face_id] - -# ================ 个人微信专用组件 ================ - - -class WeChatMiniPrograms(MessageComponent): - """小程序。个人微信专用组件。""" - - type: str = 'WeChatMiniPrograms' - """小程序id""" - mini_app_id: str - """小程序归属用户id""" - user_name: str - """小程序名称""" - display_name: typing.Optional[str] = '' - """打开地址""" - page_path: typing.Optional[str] = '' - """小程序标题""" - title: typing.Optional[str] = '' - """首页图片""" - image_url: typing.Optional[str] = '' - - -class WeChatForwardMiniPrograms(MessageComponent): - """转发小程序。个人微信专用组件。""" - - type: str = 'WeChatForwardMiniPrograms' - """xml数据""" - xml_data: str - """首页图片""" - image_url: typing.Optional[str] = None - - def __str__(self): - return self.xml_data - - -class WeChatEmoji(MessageComponent): - """emoji表情。个人微信专用组件。""" - - type: str = 'WeChatEmoji' - """emojimd5""" - emoji_md5: str - """emoji大小""" - emoji_size: int - - -class WeChatLink(MessageComponent): - """发送链接。个人微信专用组件。""" - - type: str = 'WeChatLink' - """标题""" - link_title: str = '' - """链接描述""" - link_desc: str = '' - """链接地址""" - link_url: str = '' - """链接略缩图""" - link_thumb_url: str = '' - - -class WeChatForwardLink(MessageComponent): - """转发链接。个人微信专用组件。""" - - type: str = 'WeChatForwardLink' - """xml数据""" - xml_data: str - - def __str__(self): - return self.xml_data - - -class WeChatForwardImage(MessageComponent): - """转发图片。个人微信专用组件。""" - - type: str = 'WeChatForwardImage' - """xml数据""" - xml_data: str - - def __str__(self): - return self.xml_data - - -class WeChatForwardFile(MessageComponent): - """转发文件。个人微信专用组件。""" - - type: str = 'WeChatForwardFile' - """xml数据""" - xml_data: str - - def __str__(self): - return self.xml_data - - -class WeChatAppMsg(MessageComponent): - """通用appmsg发送。个人微信专用组件。""" - - type: str = 'WeChatAppMsg' - """xml数据""" - app_msg: str - - def __str__(self): - return self.app_msg - - -class WeChatForwardQuote(MessageComponent): - """转发引用消息。个人微信专用组件。""" - - type: str = 'WeChatForwardQuote' - """xml数据""" - app_msg: str - - def __str__(self): - return self.app_msg - - -class WeChatFile(MessageComponent): - """文件。""" - - type: str = 'File' - """消息组件类型。""" - file_id: str = '' - """文件识别 ID。""" - file_name: str = '' - """文件名称。""" - file_size: int = 0 - """文件大小。""" - file_path: str = '' - """文件地址""" - file_base64: str = '' - """base64""" - def __str__(self): - return f'[文件]{self.file_name}' \ No newline at end of file diff --git a/pkg/plugin/context.py b/pkg/plugin/context.py index 86d940c4..2a081127 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -5,8 +5,8 @@ import abc from . import events from ..core import app -from ..platform.types import message as platform_message -from ..platform import adapter as platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter def register( @@ -115,7 +115,7 @@ class APIHost: # ========== 插件可调用的 API(主程序API) ========== - def get_platform_adapters(self) -> list[platform_adapter.MessagePlatformAdapter]: + def get_platform_adapters(self) -> list[abstract_platform_adapter.AbstractMessagePlatformAdapter]: """获取已启用的消息平台适配器列表 Returns: @@ -125,7 +125,7 @@ class APIHost: async def send_active_message( self, - adapter: platform_adapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, target_type: str, target_id: str, message: platform_message.MessageChain, diff --git a/pkg/plugin/events.py b/pkg/plugin/events.py index e6e2dccb..f60dddfa 100644 --- a/pkg/plugin/events.py +++ b/pkg/plugin/events.py @@ -4,10 +4,10 @@ import typing import pydantic.v1 as pydantic -from ..provider import entities as llm_entities -from ..platform.types import message as platform_message -import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.provider.session as provider_session +from ..provider import entities as llm_entities class BaseEventModel(pydantic.BaseModel): diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index 1f38ca01..6de61e39 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -6,7 +6,7 @@ import pydantic from pkg.provider import entities -from ..platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message class FunctionCall(pydantic.BaseModel):