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

739 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件 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。