From d92ee2376461976a0ac18ed4fb4c7b61936abc7b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 22 Feb 2025 14:49:05 +0800 Subject: [PATCH] feat: discover engine & manifests for platform adapters --- .gitignore | 1 + components.yaml | 15 ++ pkg/core/app.py | 3 + pkg/core/entities.py | 2 +- pkg/core/stages/build_app.py | 7 + pkg/discover/__init__.py | 0 pkg/discover/engine.py | 197 ++++++++++++++++++++++ pkg/pipeline/pool.py | 2 +- pkg/platform/adapter.py | 10 +- pkg/platform/adapter.yaml | 15 ++ pkg/platform/manager.py | 30 ++-- pkg/platform/sources/aiocqhttp.py | 7 +- pkg/platform/sources/aiocqhttp.yaml | 37 ++++ pkg/platform/sources/dingtalk.py | 10 +- pkg/platform/sources/dingtalk.yaml | 44 +++++ pkg/platform/sources/discord.py | 10 +- pkg/platform/sources/discord.yaml | 30 ++++ pkg/platform/sources/gewechat.py | 9 +- pkg/platform/sources/gewechat.yaml | 51 ++++++ pkg/platform/sources/lark.py | 9 +- pkg/platform/sources/lark.yaml | 58 +++++++ pkg/platform/sources/nakuru.py | 7 +- pkg/platform/sources/nakuru.yaml | 44 +++++ pkg/platform/sources/officialaccount.py | 11 +- pkg/platform/sources/officialaccount.yaml | 58 +++++++ pkg/platform/sources/qqbotpy.py | 7 +- pkg/platform/sources/qqbotpy.yaml | 37 ++++ pkg/platform/sources/qqofficial.py | 11 +- pkg/platform/sources/qqofficial.yaml | 44 +++++ pkg/platform/sources/telegram.py | 10 +- pkg/platform/sources/telegram.yaml | 23 +++ pkg/platform/sources/wecom.py | 9 +- pkg/platform/sources/wecom.yaml | 65 +++++++ pkg/plugin/context.py | 4 +- 34 files changed, 802 insertions(+), 75 deletions(-) create mode 100644 components.yaml create mode 100644 pkg/discover/__init__.py create mode 100644 pkg/discover/engine.py create mode 100644 pkg/platform/adapter.yaml create mode 100644 pkg/platform/sources/aiocqhttp.yaml create mode 100644 pkg/platform/sources/dingtalk.yaml create mode 100644 pkg/platform/sources/discord.yaml create mode 100644 pkg/platform/sources/gewechat.yaml create mode 100644 pkg/platform/sources/lark.yaml create mode 100644 pkg/platform/sources/nakuru.yaml create mode 100644 pkg/platform/sources/officialaccount.yaml create mode 100644 pkg/platform/sources/qqbotpy.yaml create mode 100644 pkg/platform/sources/qqofficial.yaml create mode 100644 pkg/platform/sources/telegram.yaml create mode 100644 pkg/platform/sources/wecom.yaml diff --git a/.gitignore b/.gitignore index a0f8d3d9..a4478803 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ qcapi claude.json bard.json /*yaml +!components.yaml !/docker-compose.yaml data/labels/instance_id.json .DS_Store diff --git a/components.yaml b/components.yaml new file mode 100644 index 00000000..728ea318 --- /dev/null +++ b/components.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Blueprint +metadata: + name: builtin-components + label: + en_US: Builtin Components + zh_CN: 内置组件 +spec: + components: + ComponentTemplate: + fromFiles: + - pkg/platform/adapter.yaml + MessagePlatformAdapter: + fromDirs: + - path: pkg/platform/sources/ diff --git a/pkg/core/app.py b/pkg/core/app.py index 60cd1d03..87072a0a 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -25,6 +25,7 @@ from ..utils import version as version_mgr, proxy as proxy_mgr, announce as anno from ..persistence import mgr as persistencemgr from ..api.http.controller import main as http_controller from ..api.http.service import user as user_service +from ..discover import engine as discover_engine from ..utils import logcache, ip from . import taskmgr from . import entities as core_entities @@ -38,6 +39,8 @@ class Application: # asyncio_tasks: list[asyncio.Task] = [] task_mgr: taskmgr.AsyncTaskManager = None + discover: discover_engine.ComponentDiscoveryEngine = None + platform_mgr: im_mgr.PlatformManager = None cmd_mgr: cmdmgr.CommandManager = None diff --git a/pkg/core/entities.py b/pkg/core/entities.py index a57fbbfd..8bdd18ef 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -57,7 +57,7 @@ class Query(pydantic.BaseModel): message_chain: platform_message.MessageChain """消息链,platform收到的原始消息链""" - adapter: msadapter.MessageSourceAdapter + adapter: msadapter.MessagePlatformAdapter """消息平台适配器对象,单个app中可能启用了多个消息平台适配器,此对象表明发起此query的适配器""" session: typing.Optional[Session] = None diff --git a/pkg/core/stages/build_app.py b/pkg/core/stages/build_app.py index c5f5c005..e4fd0ea5 100644 --- a/pkg/core/stages/build_app.py +++ b/pkg/core/stages/build_app.py @@ -18,6 +18,7 @@ from ...platform import manager as im_mgr from ...persistence import mgr as persistencemgr from ...api.http.controller import main as http_controller from ...api.http.service import user as user_service +from ...discover import engine as discover_engine from ...utils import logcache from .. import taskmgr @@ -32,6 +33,12 @@ class BuildAppStage(stage.BootingStage): """ ap.task_mgr = taskmgr.AsyncTaskManager(ap) + discover = discover_engine.ComponentDiscoveryEngine(ap) + discover.discover_blueprint( + "components.yaml" + ) + ap.discover = discover + proxy_mgr = proxy.ProxyManager(ap) await proxy_mgr.initialize() ap.proxy_mgr = proxy_mgr diff --git a/pkg/discover/__init__.py b/pkg/discover/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg/discover/engine.py b/pkg/discover/engine.py new file mode 100644 index 00000000..89920eee --- /dev/null +++ b/pkg/discover/engine.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +import typing +import importlib +import os +import inspect + +import yaml +import pydantic + +from ..core import app + + +class I18nString(pydantic.BaseModel): + """国际化字符串""" + + en_US: str + """英文""" + + zh_CN: typing.Optional[str] = None + """中文""" + + ja_JP: typing.Optional[str] = None + """日文""" + + +class Metadata(pydantic.BaseModel): + """元数据""" + + name: str + """名称""" + + label: I18nString + """标签""" + + description: typing.Optional[I18nString] = None + """描述""" + + icon: typing.Optional[str] = None + """图标""" + + +class PythonExecution(pydantic.BaseModel): + """Python执行""" + + path: str + """路径""" + + attr: str + """属性""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if self.path.startswith('./'): + self.path = self.path[2:] + + +class Execution(pydantic.BaseModel): + """执行""" + + python: PythonExecution + """Python执行""" + + +class Component(pydantic.BaseModel): + """组件清单""" + + owner: str + """组件所属""" + + manifest: typing.Dict[str, typing.Any] + """组件清单内容""" + + rel_path: str + """组件清单相对main.py的路径""" + + _metadata: Metadata + """组件元数据""" + + _spec: typing.Dict[str, typing.Any] + """组件规格""" + + _execution: Execution + """组件执行""" + + def __init__(self, owner: str, manifest: typing.Dict[str, typing.Any], rel_path: str): + super().__init__( + owner=owner, + manifest=manifest, + rel_path=rel_path + ) + self._metadata = Metadata(**manifest['metadata']) + self._spec = manifest['spec'] + self._execution = Execution(**manifest['execution']) if 'execution' in manifest else None + + @property + def kind(self) -> str: + """组件类型""" + return self.manifest['kind'] + + @property + def metadata(self) -> Metadata: + """组件元数据""" + return self._metadata + + @property + def spec(self) -> typing.Dict[str, typing.Any]: + """组件规格""" + return self._spec + + @property + def execution(self) -> Execution: + """组件执行""" + return self._execution + + def get_python_component_class(self) -> typing.Type[typing.Any]: + """获取Python组件类""" + parent_path = os.path.dirname(self.rel_path) + module_path = os.path.join(parent_path, self.execution.python.path) + if module_path.endswith('.py'): + module_path = module_path[:-3] + module_path = module_path.replace('/', '.').replace('\\', '.') + module = importlib.import_module(module_path) + return getattr(module, self.execution.python.attr) + + +class ComponentDiscoveryEngine: + """组件发现引擎""" + + ap: app.Application + """应用实例""" + + components: typing.Dict[str, typing.List[Component]] = {} + """组件列表""" + + def __init__(self, ap: app.Application): + self.ap = ap + + def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component: + """加载组件清单""" + with open(path, 'r') as f: + manifest = yaml.safe_load(f) + comp = Component( + owner=owner, + manifest=manifest, + rel_path=path + ) + if not no_save: + if comp.kind not in self.components: + self.components[comp.kind] = [] + self.components[comp.kind].append(comp) + return comp + + def load_component_manifests_in_dir(self, path: str, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]: + """加载目录中的组件清单""" + components: typing.List[Component] = [] + for file in os.listdir(path): + if file.endswith('.yaml') or file.endswith('.yml'): + components.append(self.load_component_manifest(os.path.join(path, file), owner, no_save)) + return components + + def load_blueprint_comp_group(self, group: dict, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]: + """加载蓝图组件组""" + components: typing.List[Component] = [] + if 'fromFiles' in group: + for file in group['fromFiles']: + components.append(self.load_component_manifest(file, owner, no_save)) + if 'fromDirs' in group: + for dir in group['fromDirs']: + path = dir['path'] + # depth = dir['depth'] + components.extend(self.load_component_manifests_in_dir(path, owner, no_save)) + return components + + def discover_blueprint(self, blueprint_manifest_path: str, owner: str = 'builtin'): + """发现蓝图""" + blueprint_manifest = self.load_component_manifest(blueprint_manifest_path, owner, no_save=True) + assert blueprint_manifest.kind == 'Blueprint', '`Kind` must be `Blueprint`' + components: typing.Dict[str, typing.List[Component]] = {} + + # load ComponentTemplate first + if 'ComponentTemplate' in blueprint_manifest.spec['components']: + components['ComponentTemplate'] = self.load_blueprint_comp_group(blueprint_manifest.spec['components']['ComponentTemplate'], owner) + + for name, component in blueprint_manifest.spec['components'].items(): + if name == 'ComponentTemplate': + continue + components[name] = self.load_blueprint_comp_group(component, owner) + return blueprint_manifest, components + + + def get_components_by_kind(self, kind: str) -> typing.List[Component]: + """获取指定类型的组件""" + if kind not in self.components: + raise ValueError(f'No components found for kind: {kind}') + return self.components[kind] diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index 5b18b08c..e358d249 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -33,7 +33,7 @@ class QueryPool: sender_id: typing.Union[int, str], message_event: platform_events.MessageEvent, message_chain: platform_message.MessageChain, - adapter: msadapter.MessageSourceAdapter + adapter: msadapter.MessagePlatformAdapter ) -> entities.Query: async with self.condition: query = entities.Query( diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index e04ec29c..ee867d98 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -10,7 +10,7 @@ from .types import message as platform_message from .types import events as platform_events -preregistered_adapters: list[typing.Type[MessageSourceAdapter]] = [] +preregistered_adapters: list[typing.Type[MessagePlatformAdapter]] = [] def adapter_class( name: str @@ -23,14 +23,14 @@ def adapter_class( Returns: typing.Callable[[typing.Type[MessageSourceAdapter]], typing.Type[MessageSourceAdapter]]: 装饰器 """ - def decorator(cls: typing.Type[MessageSourceAdapter]) -> typing.Type[MessageSourceAdapter]: + def decorator(cls: typing.Type[MessagePlatformAdapter]) -> typing.Type[MessagePlatformAdapter]: cls.name = name preregistered_adapters.append(cls) return cls return decorator -class MessageSourceAdapter(metaclass=abc.ABCMeta): +class MessagePlatformAdapter(metaclass=abc.ABCMeta): """消息平台适配器基类""" name: str @@ -89,7 +89,7 @@ class MessageSourceAdapter(metaclass=abc.ABCMeta): def register_listener( self, event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessageSourceAdapter], None] + callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None] ): """注册事件监听器 @@ -102,7 +102,7 @@ class MessageSourceAdapter(metaclass=abc.ABCMeta): def unregister_listener( self, event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessageSourceAdapter], None] + callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None] ): """注销事件监听器 diff --git a/pkg/platform/adapter.yaml b/pkg/platform/adapter.yaml new file mode 100644 index 00000000..1404833b --- /dev/null +++ b/pkg/platform/adapter.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ComponentTemplate +metadata: + icon: + name: MessagePlatformAdapter + label: + en_US: Message Platform Adapter + zh_CN: 消息平台适配器模板类 +spec: + type: + - python +execution: + python: + path: ./adapter.py + attr: MessagePlatformAdapter diff --git a/pkg/platform/manager.py b/pkg/platform/manager.py index 24a347ce..3b1e27ae 100644 --- a/pkg/platform/manager.py +++ b/pkg/platform/manager.py @@ -18,6 +18,8 @@ from .types import message as platform_message from .types import events as platform_events from .types import entities as platform_entities +from ..discover import engine + # 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题 from . import types as mirai sys.modules['mirai'] = mirai @@ -27,7 +29,9 @@ sys.modules['mirai'] = mirai class PlatformManager: # adapter: msadapter.MessageSourceAdapter = None - adapters: list[msadapter.MessageSourceAdapter] = [] + adapters: list[msadapter.MessagePlatformAdapter] = [] + + message_platform_adapter_components: list[engine.Component] = [] # modern ap: app.Application = None @@ -39,9 +43,13 @@ class PlatformManager: async def initialize(self): - from .sources import nakuru, aiocqhttp, qqbotpy, qqofficial, wecom, lark, discord, gewechat, officialaccount, telegram, dingtalk + components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter') - async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter): + self.message_platform_adapter_components = components + + # from .sources import nakuru, aiocqhttp, qqbotpy, qqofficial, wecom, lark, discord, gewechat, officialaccount, telegram, dingtalk + + async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessagePlatformAdapter): await self.ap.query_pool.add_query( launcher_type=core_entities.LauncherTypes.PERSON, @@ -52,7 +60,7 @@ class PlatformManager: adapter=adapter ) - async def on_group_message(event: platform_events.GroupMessage, adapter: msadapter.MessageSourceAdapter): + async def on_group_message(event: platform_events.GroupMessage, adapter: msadapter.MessagePlatformAdapter): await self.ap.query_pool.add_query( launcher_type=core_entities.LauncherTypes.GROUP, @@ -76,10 +84,10 @@ class PlatformManager: found = False - for adapter in msadapter.preregistered_adapters: - if adapter.name == adapter_name: + for adapter in self.message_platform_adapter_components: + if adapter.metadata.name == adapter_name: found = True - adapter_cls = adapter + adapter_cls = adapter.get_python_component_class() adapter_inst = adapter_cls( cfg_copy, @@ -102,7 +110,7 @@ class PlatformManager: if len(self.adapters) == 0: self.ap.logger.warning('未运行平台适配器,请根据文档配置并启用平台适配器。') - async def write_back_config(self, adapter_inst: msadapter.MessageSourceAdapter, config: dict): + async def write_back_config(self, adapter_inst: msadapter.MessagePlatformAdapter, config: dict): index = -2 for i, adapter in enumerate(self.adapters): @@ -131,7 +139,7 @@ class PlatformManager: self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg await self.ap.platform_cfg.dump_config() - async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessageSourceAdapter): + async def send(self, event: platform_events.MessageEvent, msg: platform_message.MessageChain, adapter: msadapter.MessagePlatformAdapter): if self.ap.platform_cfg.data['at-sender'] and isinstance(event, platform_events.GroupMessage): @@ -152,7 +160,7 @@ class PlatformManager: try: tasks = [] for adapter in self.adapters: - async def exception_wrapper(adapter: msadapter.MessageSourceAdapter): + async def exception_wrapper(adapter: msadapter.MessagePlatformAdapter): try: await adapter.run_async() except Exception as e: @@ -167,7 +175,7 @@ class PlatformManager: self.ap.task_mgr.create_task( task, kind="platform-adapter", - name=f"platform-adapter-{adapter.name}", + name=f"platform-adapter-{adapter.__class__.__name__}", scopes=[core_entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.PLATFORM], ) diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py index 1d548f30..4fa730d0 100644 --- a/pkg/platform/sources/aiocqhttp.py +++ b/pkg/platform/sources/aiocqhttp.py @@ -210,8 +210,7 @@ class AiocqhttpEventConverter(adapter.EventConverter): ) -@adapter.adapter_class("aiocqhttp") -class AiocqhttpAdapter(adapter.MessageSourceAdapter): +class AiocqhttpAdapter(adapter.MessagePlatformAdapter): bot: aiocqhttp.CQHttp @@ -273,7 +272,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): async def on_message(event: aiocqhttp.Event): self.bot_account_id = event.self_id @@ -290,7 +289,7 @@ class AiocqhttpAdapter(adapter.MessageSourceAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/aiocqhttp.yaml b/pkg/platform/sources/aiocqhttp.yaml new file mode 100644 index 00000000..a2c230a7 --- /dev/null +++ b/pkg/platform/sources/aiocqhttp.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: aiocqhttp + label: + en_US: OneBot v11 Adapter + zh_CN: OneBot v11 适配器 + description: + en_US: OneBot v11 Adapter + zh_CN: OneBot v11 适配器 +spec: + config: + - name: host + label: + en_US: Host + zh_CN: 主机 + type: string + required: true + default: 0.0.0.0 + - name: port + label: + en_US: Port + zh_CN: 端口 + type: int + required: true + default: 2280 + - name: access-token + label: + en_US: Access Token + zh_CN: 访问令牌 + type: string + required: false + default: "" +execution: + python: + path: ./aiocqhttp.py + attr: AiocqhttpAdapter \ No newline at end of file diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index b70489df..7d1fc1e0 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -3,7 +3,7 @@ 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 MessageSourceAdapter +from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from pkg.core import app from .. import adapter @@ -96,8 +96,8 @@ class DingTalkEventConverter(adapter.EventConverter): source_platform_object=event ) -@adapter.adapter_class("dingtalk") -class DingTalkAdapter(adapter.MessageSourceAdapter): + +class DingTalkAdapter(adapter.MessagePlatformAdapter): bot: DingTalkClient ap: app.Application bot_account_id: str @@ -149,7 +149,7 @@ class DingTalkAdapter(adapter.MessageSourceAdapter): self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ - [platform_events.Event, adapter.MessageSourceAdapter], None + [platform_events.Event, adapter.MessagePlatformAdapter], None ], ): async def on_message(event: DingTalkEvent): @@ -176,7 +176,7 @@ class DingTalkAdapter(adapter.MessageSourceAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/dingtalk.yaml b/pkg/platform/sources/dingtalk.yaml new file mode 100644 index 00000000..b4c1c692 --- /dev/null +++ b/pkg/platform/sources/dingtalk.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: dingtalk + label: + en_US: DingTalk + zh_CN: 钉钉 + description: + en_US: DingTalk Adapter + zh_CN: 钉钉适配器 +spec: + config: + - name: client_id + label: + en_US: Client ID + zh_CN: 客户端ID + type: string + required: true + default: "" + - name: client_secret + label: + en_US: Client Secret + zh_CN: 客户端密钥 + type: string + required: true + default: "" + - name: robot_code + label: + en_US: Robot Code + zh_CN: 机器人代码 + type: string + required: true + default: "" + - name: robot_name + label: + en_US: Robot Name + zh_CN: 机器人名称 + type: string + required: true + default: "" +execution: + python: + path: ./dingtalk.py + attr: DingTalkAdapter diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index 577272a3..961b031a 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -168,8 +168,8 @@ class DiscordEventConverter(adapter.EventConverter): source_platform_object=event, ) -@adapter.adapter_class("discord") -class DiscordMessageSourceAdapter(adapter.MessageSourceAdapter): + +class DiscordAdapter(adapter.MessagePlatformAdapter): bot: discord.Client @@ -184,7 +184,7 @@ class DiscordMessageSourceAdapter(adapter.MessageSourceAdapter): listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ] = {} def __init__(self, config: dict, ap: app.Application): @@ -249,14 +249,14 @@ class DiscordMessageSourceAdapter(adapter.MessageSourceAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/discord.yaml b/pkg/platform/sources/discord.yaml new file mode 100644 index 00000000..fd523057 --- /dev/null +++ b/pkg/platform/sources/discord.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: discord + label: + en_US: Discord + zh_CN: Discord + description: + en_US: Discord Adapter + zh_CN: Discord 适配器 +spec: + config: + - name: client_id + label: + en_US: Client ID + zh_CN: 客户端ID + type: string + required: true + default: "" + - name: token + label: + en_US: Token + zh_CN: 令牌 + type: string + required: true + default: "" +execution: + python: + path: ./discord.py + attr: DiscordAdapter diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/gewechat.py index c75b1add..9d3921fc 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/gewechat.py @@ -133,8 +133,7 @@ class GewechatEventConverter(adapter.EventConverter): ) -@adapter.adapter_class("gewechat") -class GewechatMessageSourceAdapter(adapter.MessageSourceAdapter): +class GeWeChatAdapter(adapter.MessagePlatformAdapter): bot: gewechat_client.GewechatClient quart_app: quart.Quart @@ -150,7 +149,7 @@ class GewechatMessageSourceAdapter(adapter.MessageSourceAdapter): listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ] = {} def __init__(self, config: dict, ap: app.Application): @@ -222,14 +221,14 @@ class GewechatMessageSourceAdapter(adapter.MessageSourceAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None] + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None] ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None] + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None] ): pass diff --git a/pkg/platform/sources/gewechat.yaml b/pkg/platform/sources/gewechat.yaml new file mode 100644 index 00000000..6a6aec28 --- /dev/null +++ b/pkg/platform/sources/gewechat.yaml @@ -0,0 +1,51 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: gewechat + label: + en_US: GeWeChat + zh_CN: GeWeChat(个人微信) + description: + en_US: GeWeChat Adapter + zh_CN: GeWeChat 适配器 +spec: + config: + - name: gewechat_url + label: + en_US: GeWeChat URL + zh_CN: GeWeChat URL + type: string + required: true + default: "" + - name: port + label: + en_US: Port + zh_CN: 端口 + type: int + required: true + default: 2286 + - name: callback_url + label: + en_US: Callback URL + zh_CN: 回调URL + type: string + required: true + default: "" + - name: app_id + label: + en_US: App ID + zh_CN: 应用ID + type: string + required: true + default: "" + - name: token + label: + en_US: Token + zh_CN: 令牌 + type: string + required: true + default: "" +execution: + python: + path: ./gewechat.py + attr: GeWeChatAdapter diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 81ae76c5..4c87640b 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -298,8 +298,7 @@ class LarkEventConverter(adapter.EventConverter): ) -@adapter.adapter_class("lark") -class LarkMessageSourceAdapter(adapter.MessageSourceAdapter): +class LarkAdapter(adapter.MessagePlatformAdapter): bot: lark_oapi.ws.Client api_client: lark_oapi.Client @@ -312,7 +311,7 @@ class LarkMessageSourceAdapter(adapter.MessageSourceAdapter): listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ] = {} config: dict @@ -450,7 +449,7 @@ class LarkMessageSourceAdapter(adapter.MessageSourceAdapter): self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ - [platform_events.Event, adapter.MessageSourceAdapter], None + [platform_events.Event, adapter.MessagePlatformAdapter], None ], ): self.listeners[event_type] = callback @@ -459,7 +458,7 @@ class LarkMessageSourceAdapter(adapter.MessageSourceAdapter): self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ - [platform_events.Event, adapter.MessageSourceAdapter], None + [platform_events.Event, adapter.MessagePlatformAdapter], None ], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/lark.yaml b/pkg/platform/sources/lark.yaml new file mode 100644 index 00000000..6170e367 --- /dev/null +++ b/pkg/platform/sources/lark.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: lark + label: + en_US: Lark + zh_CN: 飞书 + description: + en_US: Lark Adapter + zh_CN: 飞书适配器 +spec: + config: + - name: app_id + label: + en_US: App ID + zh_CN: 应用ID + type: string + required: true + default: "" + - name: app_secret + label: + en_US: App Secret + zh_CN: 应用密钥 + type: string + required: true + default: "" + - name: bot_name + label: + en_US: Bot Name + zh_CN: 机器人名称 + type: string + required: true + default: "" + - name: enable-webhook + label: + en_US: Enable Webhook Mode + zh_CN: 启用Webhook模式 + type: boolean + required: true + default: false + - name: port + label: + en_US: Webhook Port + zh_CN: Webhook端口 + type: int + required: true + default: 2285 + - name: encrypt-key + label: + en_US: Encrypt Key + zh_CN: 加密密钥 + type: string + required: true + default: "" +execution: + python: + path: ./lark.py + attr: LarkAdapter \ No newline at end of file diff --git a/pkg/platform/sources/nakuru.py b/pkg/platform/sources/nakuru.py index 94993dc7..8dcf6e52 100644 --- a/pkg/platform/sources/nakuru.py +++ b/pkg/platform/sources/nakuru.py @@ -158,8 +158,7 @@ class NakuruProjectEventConverter(adapter_model.EventConverter): raise Exception("未支持转换的事件类型: " + str(event)) -@adapter_model.adapter_class("nakuru") -class NakuruProjectAdapter(adapter_model.MessageSourceAdapter): +class NakuruAdapter(adapter_model.MessagePlatformAdapter): """nakuru-project适配器""" bot: nakuru.CQHTTP bot_account_id: int @@ -256,7 +255,7 @@ class NakuruProjectAdapter(adapter_model.MessageSourceAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessageSourceAdapter], None] + callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None] ): try: @@ -284,7 +283,7 @@ class NakuruProjectAdapter(adapter_model.MessageSourceAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessageSourceAdapter], None] + callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None] ): nakuru_event_name = self.event_converter.yiri2target(event_type).__name__ diff --git a/pkg/platform/sources/nakuru.yaml b/pkg/platform/sources/nakuru.yaml new file mode 100644 index 00000000..b64e191b --- /dev/null +++ b/pkg/platform/sources/nakuru.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: nakuru + label: + en_US: Nakuru + zh_CN: Nakuru + description: + en_US: Nakuru Adapter + zh_CN: Nakuru 适配器(go-cqhttp) +spec: + config: + - name: host + label: + en_US: Host + zh_CN: 主机 + type: string + required: true + default: "127.0.0.1" + - name: http_port + label: + en_US: HTTP Port + zh_CN: HTTP端口 + type: int + required: true + default: 5700 + - name: ws_port + label: + en_US: WebSocket Port + zh_CN: WebSocket端口 + type: int + required: true + default: 8080 + - name: token + label: + en_US: Token + zh_CN: 令牌 + type: string + required: true + default: "" +execution: + python: + path: ./nakuru.py + attr: NakuruAdapter diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 856388f2..8202946b 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -5,13 +5,13 @@ import traceback import time import datetime from pkg.core import app -from pkg.platform.adapter import MessageSourceAdapter +from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message import aiocqhttp import aiohttp from libs.official_account_api.oaevent import OAEvent -from pkg.platform.adapter import MessageSourceAdapter +from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from libs.official_account_api.api import OAClient from pkg.core import app @@ -68,8 +68,7 @@ class OAEventConverter(adapter.EventConverter): else: return None -@adapter.adapter_class("officialaccount") -class OfficialAccountAdapter(adapter.MessageSourceAdapter): +class OfficialAccountAdapter(adapter.MessagePlatformAdapter): bot : OAClient ap : app.Application @@ -116,7 +115,7 @@ class OfficialAccountAdapter(adapter.MessageSourceAdapter): pass - def register_listener(self, event_type: type, callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None]): + def register_listener(self, event_type: type, callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None]): async def on_message(event: OAEvent): self.bot_account_id = event.receiver_id try: @@ -148,7 +147,7 @@ class OfficialAccountAdapter(adapter.MessageSourceAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/officialaccount.yaml b/pkg/platform/sources/officialaccount.yaml new file mode 100644 index 00000000..dbd84a77 --- /dev/null +++ b/pkg/platform/sources/officialaccount.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: officialaccount + label: + en_US: Official Account + zh_CN: 微信公众号 + description: + en_US: Official Account Adapter + zh_CN: 微信公众号适配器 +spec: + config: + - name: token + label: + en_US: Token + zh_CN: 令牌 + type: string + required: true + default: "" + - name: EncodingAESKey + label: + en_US: EncodingAESKey + zh_CN: 消息加解密密钥 + type: string + required: true + default: "" + - name: AppID + label: + en_US: App ID + zh_CN: 应用ID + type: string + required: true + default: "" + - name: AppSecret + label: + en_US: App Secret + zh_CN: 应用密钥 + type: string + required: true + default: "" + - name: host + label: + en_US: Host + zh_CN: 监听主机 + type: string + required: true + default: 0.0.0.0 + - name: port + label: + en_US: Port + zh_CN: 监听端口 + type: int + required: true + default: 2287 +execution: + python: + path: ./officialaccount.py + attr: OfficialAccountAdapter diff --git a/pkg/platform/sources/qqbotpy.py b/pkg/platform/sources/qqbotpy.py index 3a4e681f..9f407b7f 100644 --- a/pkg/platform/sources/qqbotpy.py +++ b/pkg/platform/sources/qqbotpy.py @@ -360,8 +360,7 @@ class OfficialEventConverter(adapter_model.EventConverter): ) -@adapter_model.adapter_class("qq-botpy") -class OfficialAdapter(adapter_model.MessageSourceAdapter): +class OfficialAdapter(adapter_model.MessagePlatformAdapter): """QQ 官方消息适配器""" bot: botpy.Client = None @@ -535,7 +534,7 @@ class OfficialAdapter(adapter_model.MessageSourceAdapter): self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ - [platform_events.Event, adapter_model.MessageSourceAdapter], None + [platform_events.Event, adapter_model.MessagePlatformAdapter], None ], ): @@ -561,7 +560,7 @@ class OfficialAdapter(adapter_model.MessageSourceAdapter): self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ - [platform_events.Event, adapter_model.MessageSourceAdapter], None + [platform_events.Event, adapter_model.MessagePlatformAdapter], None ], ): delattr(self.bot, event_handler_mapping[event_type]) diff --git a/pkg/platform/sources/qqbotpy.yaml b/pkg/platform/sources/qqbotpy.yaml new file mode 100644 index 00000000..79653194 --- /dev/null +++ b/pkg/platform/sources/qqbotpy.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: qq-botpy + label: + en_US: QQBotPy + zh_CN: QQBotPy + description: + en_US: QQ Official API (WebSocket) + zh_CN: QQ 官方 API (WebSocket) +spec: + config: + - name: appid + label: + en_US: App ID + zh_CN: 应用ID + type: string + required: true + default: "" + - name: secret + label: + en_US: Secret + zh_CN: 密钥 + type: string + required: true + default: "" + - name: intents + label: + en_US: Intents + zh_CN: 权限 + type: array[string] + required: true + default: [] +execution: + python: + path: ./qqbotpy.py + attr: OfficialAdapter diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index 7993062a..518dfdb8 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -6,7 +6,7 @@ import time import datetime import aiocqhttp import aiohttp -from pkg.platform.adapter import MessageSourceAdapter +from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from pkg.core import app from .. import adapter @@ -137,10 +137,7 @@ class QQOfficialEventConverter(adapter.EventConverter): ) - - -@adapter.adapter_class("qqofficial") -class QQOfficialAdapter(adapter.MessageSourceAdapter): +class QQOfficialAdapter(adapter.MessagePlatformAdapter): bot:QQOfficialClient ap:app.Application config:dict @@ -213,7 +210,7 @@ class QQOfficialAdapter(adapter.MessageSourceAdapter): self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ - [platform_events.Event, adapter.MessageSourceAdapter], None + [platform_events.Event, adapter.MessagePlatformAdapter], None ], ): async def on_message(event:QQOfficialEvent): @@ -250,7 +247,7 @@ class QQOfficialAdapter(adapter.MessageSourceAdapter): def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/qqofficial.yaml b/pkg/platform/sources/qqofficial.yaml new file mode 100644 index 00000000..4d7430ea --- /dev/null +++ b/pkg/platform/sources/qqofficial.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: qqofficial + label: + en_US: QQ Official API + zh_CN: QQ 官方 API + description: + en_US: QQ Official API (Webhook) + zh_CN: QQ 官方 API (Webhook) +spec: + config: + - name: appid + label: + en_US: App ID + zh_CN: 应用ID + type: string + required: true + default: "" + - name: secret + label: + en_US: Secret + zh_CN: 密钥 + type: string + required: true + default: "" + - name: port + label: + en_US: Port + zh_CN: 监听端口 + type: int + required: true + default: 2284 + - name: token + label: + en_US: Token + zh_CN: 令牌 + type: string + required: true + default: "" +execution: + python: + path: ./qqofficial.py + attr: QQOfficialAdapter diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 1a7a4c20..49822673 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -147,8 +147,8 @@ class TelegramEventConverter(adapter.EventConverter): source_platform_object=event ) -@adapter.adapter_class("telegram") -class TelegramMessageSourceAdapter(adapter.MessageSourceAdapter): + +class TelegramAdapter(adapter.MessagePlatformAdapter): bot: telegram.Bot application: telegram.ext.Application @@ -163,7 +163,7 @@ class TelegramMessageSourceAdapter(adapter.MessageSourceAdapter): listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ] = {} def __init__(self, config: dict, ap: app.Application): @@ -218,14 +218,14 @@ class TelegramMessageSourceAdapter(adapter.MessageSourceAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/telegram.yaml b/pkg/platform/sources/telegram.yaml new file mode 100644 index 00000000..67dfb5bc --- /dev/null +++ b/pkg/platform/sources/telegram.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: telegram + label: + en_US: Telegram + zh_CN: 电报 + description: + en_US: Telegram Adapter + zh_CN: 电报适配器 +spec: + config: + - name: token + label: + en_US: Token + zh_CN: 令牌 + type: string + required: true + default: "" +execution: + python: + path: ./telegram.py + attr: TelegramAdapter diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 24f22bd4..dd6cdf19 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -8,7 +8,7 @@ import datetime import aiocqhttp import aiohttp from libs.wecom_api.api import WecomClient -from pkg.platform.adapter import MessageSourceAdapter +from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from libs.wecom_api.wecomevent import WecomEvent from pkg.core import app @@ -144,8 +144,7 @@ class WecomEventConverter: ) -@adapter.adapter_class("wecom") -class WecomeAdapter(adapter.MessageSourceAdapter): +class WecomAdapter(adapter.MessagePlatformAdapter): bot: WecomClient ap: app.Application @@ -207,7 +206,7 @@ class WecomeAdapter(adapter.MessageSourceAdapter): self, event_type: typing.Type[platform_events.Event], callback: typing.Callable[ - [platform_events.Event, adapter.MessageSourceAdapter], None + [platform_events.Event, adapter.MessagePlatformAdapter], None ], ): async def on_message(event: WecomEvent): @@ -242,6 +241,6 @@ class WecomeAdapter(adapter.MessageSourceAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None], + callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/wecom.yaml b/pkg/platform/sources/wecom.yaml new file mode 100644 index 00000000..6b6c26eb --- /dev/null +++ b/pkg/platform/sources/wecom.yaml @@ -0,0 +1,65 @@ +apiVersion: v1 +kind: MessagePlatformAdapter +metadata: + name: wecom + label: + en_US: WeCom + zh_CN: 企业微信 + description: + en_US: WeCom Adapter + zh_CN: 企业微信适配器 +spec: + config: + - name: host + label: + en_US: Host + zh_CN: 监听主机 + type: string + required: true + default: "0.0.0.0" + - name: port + label: + en_US: Port + zh_CN: 监听端口 + type: int + required: true + default: 2290 + - name: corpid + label: + en_US: Corpid + zh_CN: 企业ID + type: string + required: true + default: "" + - name: secret + label: + en_US: Secret + zh_CN: 密钥 + type: string + required: true + default: "" + - name: token + label: + en_US: Token + zh_CN: 令牌 + type: string + required: true + default: "" + - name: EncodingAESKey + label: + en_US: EncodingAESKey + zh_CN: 消息加解密密钥 + type: string + required: true + default: "" + - name: contacts_secret + label: + en_US: Contacts Secret + zh_CN: 通讯录密钥 + type: string + required: true + default: "" +execution: + python: + path: ./wecom.py + attr: WecomAdapter diff --git a/pkg/plugin/context.py b/pkg/plugin/context.py index 2131a59a..76a49bf4 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -116,7 +116,7 @@ class APIHost: # ========== 插件可调用的 API(主程序API) ========== - def get_platform_adapters(self) -> list[platform_adapter.MessageSourceAdapter]: + def get_platform_adapters(self) -> list[platform_adapter.MessagePlatformAdapter]: """获取已启用的消息平台适配器列表 Returns: @@ -126,7 +126,7 @@ class APIHost: async def send_active_message( self, - adapter: platform_adapter.MessageSourceAdapter, + adapter: platform_adapter.MessagePlatformAdapter, target_type: str, target_id: str, message: platform_message.MessageChain,