Files
LangBot/docs/event-based-agents/05-plugin-sdk.md
2026-05-07 16:25:39 +08:00

20 KiB
Raw Blame History

插件 SDK 改造

1. 概述

插件 SDK 需要配合 EBA 架构进行以下改造:

  1. 新事件类型:将所有通用事件暴露给插件
  2. 新 API:将新增的平台 API 通过 LangBotAPIProxy 暴露给插件
  3. 兼容层:保证现有插件零修改运行
  4. 通信协议扩展:新增 action 枚举支持新 API

2. 新事件类型暴露

2.1 插件事件模型扩展

当前插件 SDK 的事件模型(api/entities/events.py)只有消息相关事件。需要新增所有通用事件的插件级包装:

# api/entities/events.py — 新增事件

# ---- 消息事件(扩展) ----

class MessageEditedReceived(BaseEventModel):
    """消息被编辑事件"""
    launcher_type: str
    launcher_id: typing.Union[int, str]
    message_id: typing.Union[int, str]
    editor_id: typing.Union[int, str]
    new_content: MessageChain
    chat_type: str  # "private" | "group"

class MessageDeletedReceived(BaseEventModel):
    """消息被删除/撤回事件"""
    launcher_type: str
    launcher_id: typing.Union[int, str]
    message_id: typing.Union[int, str]
    operator_id: typing.Optional[typing.Union[int, str]] = None
    chat_type: str

class MessageReactionReceived(BaseEventModel):
    """消息表情回应事件"""
    launcher_type: str
    launcher_id: typing.Union[int, str]
    message_id: typing.Union[int, str]
    user_id: typing.Union[int, str]
    reaction: str
    is_add: bool

# ---- 用户反馈事件 ----

class FeedbackReceived(BaseEventModel):
    """用户对 Bot 回复提交反馈"""
    feedback_id: str
    feedback_type: int  # 1=like, 2=dislike, 3=cancel/remove feedback
    feedback_content: typing.Optional[str] = None
    inaccurate_reasons: typing.Optional[list[str]] = None
    user_id: typing.Optional[str] = None
    session_id: typing.Optional[str] = None
    message_id: typing.Optional[str] = None
    stream_id: typing.Optional[str] = None

# ---- 群组事件 ----

class GroupMemberJoined(BaseEventModel):
    """新成员加入群组"""
    group_id: typing.Union[int, str]
    group_name: str
    member_id: typing.Union[int, str]
    member_name: str
    inviter_id: typing.Optional[typing.Union[int, str]] = None
    join_type: typing.Optional[str] = None

class GroupMemberLeft(BaseEventModel):
    """成员离开群组"""
    group_id: typing.Union[int, str]
    group_name: str
    member_id: typing.Union[int, str]
    member_name: str
    is_kicked: bool = False
    operator_id: typing.Optional[typing.Union[int, str]] = None

class GroupMemberBanned(BaseEventModel):
    """成员被禁言"""
    group_id: typing.Union[int, str]
    member_id: typing.Union[int, str]
    operator_id: typing.Optional[typing.Union[int, str]] = None
    duration: typing.Optional[int] = None

class GroupMemberUnbanned(BaseEventModel):
    """成员被解除禁言"""
    group_id: typing.Union[int, str]
    member_id: typing.Union[int, str]
    operator_id: typing.Optional[typing.Union[int, str]] = None

class GroupInfoUpdated(BaseEventModel):
    """群组信息被修改"""
    group_id: typing.Union[int, str]
    group_name: str
    operator_id: typing.Optional[typing.Union[int, str]] = None
    changed_fields: list[str] = []

# ---- 好友事件 ----

class FriendRequestReceived(BaseEventModel):
    """收到好友请求"""
    request_id: typing.Union[int, str]
    user_id: typing.Union[int, str]
    user_name: str
    message: typing.Optional[str] = None

class FriendAdded(BaseEventModel):
    """成功添加好友"""
    user_id: typing.Union[int, str]
    user_name: str

class FriendRemoved(BaseEventModel):
    """好友被移除"""
    user_id: typing.Union[int, str]
    user_name: str

# ---- Bot 状态事件 ----

class BotInvitedToGroup(BaseEventModel):
    """Bot 被邀请加入群组"""
    group_id: typing.Union[int, str]
    group_name: str
    inviter_id: typing.Optional[typing.Union[int, str]] = None
    request_id: typing.Optional[typing.Union[int, str]] = None

class BotRemovedFromGroup(BaseEventModel):
    """Bot 被移出群组"""
    group_id: typing.Union[int, str]
    group_name: str
    operator_id: typing.Optional[typing.Union[int, str]] = None

class BotMuted(BaseEventModel):
    """Bot 被禁言"""
    group_id: typing.Union[int, str]
    operator_id: typing.Optional[typing.Union[int, str]] = None
    duration: typing.Optional[int] = None

class BotUnmuted(BaseEventModel):
    """Bot 被解除禁言"""
    group_id: typing.Union[int, str]
    operator_id: typing.Optional[typing.Union[int, str]] = None

# ---- 平台特有事件 ----

class PlatformSpecificEventReceived(BaseEventModel):
    """平台特有事件"""
    adapter_name: str
    action: str
    data: dict = {}

2.2 EventListener 注册方式

插件的 EventListener 继续使用 @self.handler(EventType) 装饰器注册,只是可以注册的事件类型大幅增加:

class MyEventListener(EventListener):
    def __init__(self, host):
        super().__init__(host)

        # 现有方式(继续工作)
        @self.handler(PersonNormalMessageReceived)
        async def on_person_message(ctx: EventContext):
            ...

        # 新事件类型
        @self.handler(GroupMemberJoined)
        async def on_member_joined(ctx: EventContext):
            group_name = ctx.event.group_name
            member_name = ctx.event.member_name
            await ctx.reply(MessageChain([
                Plain(f"欢迎 {member_name} 加入 {group_name}")
            ]))

        @self.handler(FriendRequestReceived)
        async def on_friend_request(ctx: EventContext):
            # 自动通过好友请求
            await ctx.approve_friend_request(
                ctx.event.request_id, approve=True
            )

        @self.handler(FeedbackReceived)
        async def on_feedback(ctx: EventContext):
            if ctx.event.feedback_type == 2:
                await self.log_warning(
                    f"用户点踩了回复: {ctx.event.feedback_content or ''}"
                )

        @self.handler(PlatformSpecificEventReceived)
        async def on_platform_event(ctx: EventContext):
            if ctx.event.adapter_name == "telegram" and ctx.event.action == "chat_join_request":
                ...

3. 新 API 暴露

3.1 LangBotAPIProxy 扩展

LangBotAPIProxy 中新增以下方法,插件通过 self.xxx() 调用(在 BasePlugin 中继承):

class LangBotAPIProxy:
    # ---- 现有方法(保留) ----
    # get_langbot_version, get_bots, get_bot_info,
    # send_message, invoke_llm, get/set/delete_plugin_storage, ...

    # ---- 新增消息 API ----

    async def edit_message(
        self,
        bot_uuid: str,
        chat_type: str,
        chat_id: typing.Union[int, str],
        message_id: typing.Union[int, str],
        new_content: MessageChain,
    ) -> None:
        """编辑已发送的消息"""
        ...

    async def delete_message(
        self,
        bot_uuid: str,
        chat_type: str,
        chat_id: typing.Union[int, str],
        message_id: typing.Union[int, str],
    ) -> None:
        """删除/撤回消息"""
        ...

    async def forward_message(
        self,
        bot_uuid: str,
        from_chat_type: str,
        from_chat_id: typing.Union[int, str],
        message_id: typing.Union[int, str],
        to_chat_type: str,
        to_chat_id: typing.Union[int, str],
    ) -> dict:
        """转发消息"""
        ...

    async def get_message(
        self,
        bot_uuid: str,
        chat_type: str,
        chat_id: typing.Union[int, str],
        message_id: typing.Union[int, str],
    ) -> dict:
        """获取指定消息"""
        ...

    # ---- 新增群组 API ----

    async def get_group_info(
        self,
        bot_uuid: str,
        group_id: typing.Union[int, str],
    ) -> dict:
        """获取群组信息"""
        ...

    async def get_group_list(
        self,
        bot_uuid: str,
    ) -> list[dict]:
        """获取 Bot 加入的群组列表"""
        ...

    async def get_group_member_list(
        self,
        bot_uuid: str,
        group_id: typing.Union[int, str],
    ) -> list[dict]:
        """获取群成员列表"""
        ...

    async def get_group_member_info(
        self,
        bot_uuid: str,
        group_id: typing.Union[int, str],
        user_id: typing.Union[int, str],
    ) -> dict:
        """获取指定群成员信息"""
        ...

    async def mute_member(
        self,
        bot_uuid: str,
        group_id: typing.Union[int, str],
        user_id: typing.Union[int, str],
        duration: int = 0,
    ) -> None:
        """禁言群成员"""
        ...

    async def unmute_member(
        self,
        bot_uuid: str,
        group_id: typing.Union[int, str],
        user_id: typing.Union[int, str],
    ) -> None:
        """解除禁言"""
        ...

    async def kick_member(
        self,
        bot_uuid: str,
        group_id: typing.Union[int, str],
        user_id: typing.Union[int, str],
    ) -> None:
        """踢出群成员"""
        ...

    # ---- 新增用户 API ----

    async def get_user_info(
        self,
        bot_uuid: str,
        user_id: typing.Union[int, str],
    ) -> dict:
        """获取用户信息"""
        ...

    async def get_friend_list(
        self,
        bot_uuid: str,
    ) -> list[dict]:
        """获取好友列表"""
        ...

    async def approve_friend_request(
        self,
        bot_uuid: str,
        request_id: typing.Union[int, str],
        approve: bool = True,
        remark: typing.Optional[str] = None,
    ) -> None:
        """处理好友请求"""
        ...

    async def approve_group_invite(
        self,
        bot_uuid: str,
        request_id: typing.Union[int, str],
        approve: bool = True,
    ) -> None:
        """处理入群邀请"""
        ...

    # ---- 新增透传 API ----

    async def call_platform_api(
        self,
        bot_uuid: str,
        action: str,
        params: dict = {},
    ) -> dict:
        """调用适配器特有 API

        Examples:
            # Telegram: pin 消息
            result = await self.call_platform_api(
                bot_uuid, "pin_message",
                {"chat_id": 123456, "message_id": 789}
            )

            # Discord: 创建频道
            result = await self.call_platform_api(
                bot_uuid, "create_channel",
                {"guild_id": "...", "name": "new-channel"}
            )
        """
        ...

    # ---- 新增能力查询 API ----

    async def get_supported_events(
        self,
        bot_uuid: str,
    ) -> list[str]:
        """获取指定 Bot 的适配器支持的事件类型"""
        ...

    async def get_supported_apis(
        self,
        bot_uuid: str,
    ) -> list[str]:
        """获取指定 Bot 的适配器支持的 API"""
        ...

3.2 QueryBasedAPIProxy 扩展

在事件处理上下文中EventContext通过 QueryBasedAPIProxy 新增便捷方法:

class QueryBasedAPIProxy:
    # ---- 现有方法(保留) ----
    # reply, get_bot_uuid, set_query_var, get_query_var,
    # create_new_conversation, ...

    # ---- 新增便捷方法 ----

    async def edit_message(
        self,
        message_id: typing.Union[int, str],
        new_content: MessageChain,
    ) -> None:
        """在当前会话中编辑消息(自动使用当前 bot_uuid 和 chat 信息)"""
        ...

    async def delete_message(
        self,
        message_id: typing.Union[int, str],
    ) -> None:
        """在当前会话中删除消息"""
        ...

    async def approve_friend_request(
        self,
        request_id: typing.Union[int, str],
        approve: bool = True,
        remark: typing.Optional[str] = None,
    ) -> None:
        """处理好友请求(上下文中自动获取 bot_uuid"""
        ...

    async def approve_group_invite(
        self,
        request_id: typing.Union[int, str],
        approve: bool = True,
    ) -> None:
        """处理入群邀请"""
        ...

    async def get_group_info(self) -> dict:
        """获取当前群组信息(仅群聊事件中可用)"""
        ...

    async def get_group_member_list(self) -> list[dict]:
        """获取当前群组成员列表(仅群聊事件中可用)"""
        ...

    async def call_platform_api(
        self,
        action: str,
        params: dict = {},
    ) -> dict:
        """调用平台特有 API自动使用当前 bot_uuid"""
        ...

4. 兼容层设计

4.1 事件兼容层

当 PluginHandler 将新的 MessageReceivedEvent 分发给插件时,需要同时生成旧格式事件:

class PluginEventCompatLayer:
    """插件事件兼容层

    将新的统一事件转换为旧的插件事件格式,
    确保监听旧事件类型的插件仍能正常工作。
    """

    @staticmethod
    def convert_to_legacy_events(
        event: Event,
    ) -> list[BaseEventModel]:
        """将统一事件转换为旧插件事件列表

        一个统一事件可能生成多个旧插件事件。
        例如 MessageReceivedEvent 会同时生成:
        - PersonMessageReceived / GroupMessageReceived总是生成
        - PersonNormalMessageReceived / GroupNormalMessageReceived非命令时
        - PersonCommandSent / GroupCommandSent命令时
        """
        legacy_events = []

        if isinstance(event, MessageReceivedEvent):
            if event.chat_type == ChatType.PRIVATE:
                legacy_events.append(
                    PersonMessageReceived(
                        launcher_type="person",
                        launcher_id=event.chat_id,
                        sender_id=event.sender.id,
                        message_event=event.to_legacy_friend_message(),
                        message_chain=event.message_chain,
                    )
                )
                # 命令检测后还会生成 PersonNormalMessageReceived
                # 或 PersonCommandSent在 Pipeline 阶段处理
            elif event.chat_type == ChatType.GROUP:
                legacy_events.append(
                    GroupMessageReceived(
                        launcher_type="group",
                        launcher_id=event.chat_id,
                        sender_id=event.sender.id,
                        message_event=event.to_legacy_group_message(),
                        message_chain=event.message_chain,
                    )
                )

        # 新事件类型没有旧的对应物,不生成兼容事件
        # 只有监听了新事件类型的插件才会收到

        return legacy_events

4.2 分发流程

统一事件 (MessageReceivedEvent)
    │
    ├─→ 转换为旧格式 (PersonMessageReceived / GroupMessageReceived)
    │   └─→ 分发给监听旧事件类型的插件 EventListener
    │
    └─→ 直接分发为新格式 (MessageReceivedEvent → 对应的插件事件)
        └─→ 分发给监听新事件类型的插件 EventListener

插件 Runtime 在分发事件时检查每个 EventListener 注册的事件类型:

  • 如果注册的是旧类型(PersonMessageReceived 等),发送兼容层生成的旧格式事件
  • 如果注册的是新类型(GroupMemberJoined 等),发送新格式事件
  • 两者可以共存,同一个插件可以同时监听新旧类型

4.3 API 兼容层

现有插件使用的 API 不受影响:

现有 API 新架构行为
self.send_message(bot_uuid, target_type, target_id, message_chain) 不变,直接调用适配器的 send_message
ctx.reply(message_chain, quote_origin) 不变,在 MessageReceivedEvent 上下文中调用适配器的 reply_message
self.get_bots() 不变
self.get_bot_info(bot_uuid) 不变

新 API 只是额外新增的方法,不影响现有方法。

5. 通信协议扩展

5.1 新增 Action 枚举

entities/io/actions/enums.py 中新增 action

class PluginToRuntimeAction(str, Enum):
    # ---- 现有 actions保留 ----
    REGISTER_PLUGIN = "register_plugin"
    REPLY = "reply"
    SEND_MESSAGE = "send_message"
    # ...

    # ---- 新增消息 API ----
    EDIT_MESSAGE = "edit_message"
    DELETE_MESSAGE = "delete_message"
    FORWARD_MESSAGE = "forward_message"
    GET_MESSAGE = "get_message"

    # ---- 新增群组 API ----
    GET_GROUP_INFO = "get_group_info"
    GET_GROUP_LIST = "get_group_list"
    GET_GROUP_MEMBER_LIST = "get_group_member_list"
    GET_GROUP_MEMBER_INFO = "get_group_member_info"
    MUTE_MEMBER = "mute_member"
    UNMUTE_MEMBER = "unmute_member"
    KICK_MEMBER = "kick_member"

    # ---- 新增用户 API ----
    GET_USER_INFO = "get_user_info"
    GET_FRIEND_LIST = "get_friend_list"
    APPROVE_FRIEND_REQUEST = "approve_friend_request"
    APPROVE_GROUP_INVITE = "approve_group_invite"

    # ---- 新增透传 API ----
    CALL_PLATFORM_API = "call_platform_api"

    # ---- 新增能力查询 ----
    GET_SUPPORTED_EVENTS = "get_supported_events"
    GET_SUPPORTED_APIS = "get_supported_apis"


class RuntimeToPluginAction(str, Enum):
    # ---- 现有 actions保留 ----
    EMIT_EVENT = "emit_event"
    # ...
    # EMIT_EVENT 的 data 结构扩展以支持新事件类型

5.2 新增 Action 的请求/响应格式

EDIT_MESSAGE 为例:

// 请求 (Plugin → Runtime)
{
    "action": "edit_message",
    "seq_id": 12345,
    "data": {
        "bot_uuid": "...",
        "chat_type": "group",
        "chat_id": "123456",
        "message_id": "789",
        "new_content": [
            { "type": "Plain", "text": "edited message" }
        ]
    }
}

// 响应 (Runtime → Plugin)
{
    "seq_id": 12345,
    "code": 0,
    "message": "ok",
    "data": {}
}

GET_GROUP_MEMBER_LIST 为例:

// 请求
{
    "action": "get_group_member_list",
    "seq_id": 12346,
    "data": {
        "bot_uuid": "...",
        "group_id": "123456"
    }
}

// 响应
{
    "seq_id": 12346,
    "code": 0,
    "message": "ok",
    "data": {
        "members": [
            {
                "user": { "id": "111", "nickname": "Alice" },
                "group_id": "123456",
                "role": "admin",
                "display_name": "管理员Alice"
            },
            ...
        ]
    }
}

CALL_PLATFORM_API 为例:

// 请求
{
    "action": "call_platform_api",
    "seq_id": 12347,
    "data": {
        "bot_uuid": "...",
        "action": "pin_message",
        "params": {
            "chat_id": "123456",
            "message_id": "789"
        }
    }
}

// 响应
{
    "seq_id": 12347,
    "code": 0,
    "message": "ok",
    "data": {
        "result": { ... }
    }
}

5.3 LangBot 侧 Handler 实现

ControlConnectionHandlerLangBot → Runtime 侧)和 PluginConnectionHandlerRuntime → Plugin 侧)中新增对应的 action 处理逻辑:

# PluginConnectionHandler 中新增
async def _handle_edit_message(self, data):
    bot_uuid = data["bot_uuid"]
    bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
    await bot.adapter.edit_message(
        chat_type=data["chat_type"],
        chat_id=data["chat_id"],
        message_id=data["message_id"],
        new_content=MessageChain.model_validate(data["new_content"]),
    )
    return {}

async def _handle_call_platform_api(self, data):
    bot_uuid = data["bot_uuid"]
    bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
    result = await bot.adapter.call_platform_api(
        action=data["action"],
        params=data.get("params", {}),
    )
    return {"result": result}

6. 插件开发者迁移指南

6.1 无需迁移(零修改运行)

以下场景的现有插件不需要任何修改

  • 使用 PersonNormalMessageReceived / GroupNormalMessageReceived 监听消息
  • 使用 PersonCommandSent / GroupCommandSent 处理命令
  • 使用 ctx.reply() 回复消息
  • 使用 self.send_message() 主动发消息
  • 使用 LLM / 存储 / RAG 等现有 API

6.2 推荐迁移(获得新能力)

如果插件希望利用新功能,可以:

  1. 监听新事件类型:在 EventListener 中注册新事件类型的 handler
  2. 使用新 API:调用 self.edit_message(), self.get_group_info()
  3. 使用透传 API:调用 self.call_platform_api() 使用平台特有功能

6.3 SDK 版本号

新功能通过提升 SDK minor 版本发布:

  • 现有版本:langbot-plugin-sdk >= x.y.z
  • 新版本:langbot-plugin-sdk >= x.(y+1).0

插件的 manifest.yaml 中的 min_sdk_version 决定是否能使用新 API。使用旧 SDK 版本的插件在新 LangBot 上正常运行(兼容层保证),只是无法调用新 API。