# 插件 SDK 改造

## 1. 概述

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

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

## 2. 新事件类型暴露

### 2.1 插件事件模型扩展

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

```python
# 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)` 装饰器注册，只是可以注册的事件类型大幅增加：

```python
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 中继承）：

```python
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` 新增便捷方法：

```python
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` 分发给插件时，需要同时生成旧格式事件：

```python
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：

```python
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` 为例：

```json
// 请求 (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` 为例：

```json
// 请求
{
    "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` 为例：

```json
// 请求
{
    "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 实现

在 `ControlConnectionHandler`（LangBot → Runtime 侧）和 `PluginConnectionHandler`（Runtime → Plugin 侧）中新增对应的 action 处理逻辑：

```python
# 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。
