feat: 实现 v4 API 兼容层,支持旧版插件导入路径

新增模块:
- api/basic/: AstrBotConfig, BaseConversationManager, Conversation
- api/event/: AstrMessageEvent, AstrBotMessage, EventType, MessageType,
  MessageSession, EventResult 等核心事件类型
- api/message/: MessageChain 及所有消息组件 (Plain, At, Image, File 等)
- api/platform/: PlatformMetadata 平台元数据类型
- api/provider/: LLMResponse 提供者响应实体
- api/star/star.py: StarMetadata 插件元数据类型

更新模块:
- api/__init__.py: 导出所有子模块
- api/components/: 扩展 Command 兼容性
- api/event/filter.py: 增强 filter 装饰器兼容性
- context.py, decorators.py, events.py: 顶层兼容入口

测试更新:
- test_api_event_filter.py: 验证 filter 兼容性
- test_api_modules.py: 验证新模块可导入
- test_handler_dispatcher.py: 验证处理器分发

文档更新:
- AGENTS.md/CLAUDE.md: 添加兼容层设计说明,避免重复造轮子

设计原则:
- 兼容层通过 thin re-export 方式暴露旧版 API
- 不复制独立运行时逻辑,保持架构清晰
- 新版推荐使用顶层模块导入路径
This commit is contained in:
whatevertogo
2026-03-13 03:17:03 +08:00
parent 54b6ca7b7d
commit cdf72b75a4
35 changed files with 1512 additions and 625 deletions

View File

@@ -7,6 +7,7 @@
- 2026-03-13: Transport-pair startup tests for `SupervisorRuntime` must start a real peer on the opposite transport and provide an `initialize` response. Wiring only the supervisor side drops or captures the outgoing initialize message without replying, and `Peer.initialize()` then waits forever.
- 2026-03-13: `CapabilityRouter.register(..., stream_handler=...)` uses the signature `(request_id, payload, cancel_token)`, not the peer-level `(message, token)`. Reusing the peer handler shape in router tests causes an immediate failed stream event before the test logic runs.
- 2026-03-13: `Peer` had an early-cancel race for inbound invokes: if `CancelMessage` arrived before the invoke task executed its first line, the task could be cancelled before sending any terminal event, leaving the caller's stream iterator waiting forever. Preserve a per-request start event and pre-check the cancel token at the top of `_handle_invoke`.
- 2026-03-13: `src-new/astrbot_sdk/api` and several top-level module docstrings overstated v4 compatibility gaps by labeling migrated or compat-backed APIs as "missing". Treat `_legacy_api.py`, `astrbot_sdk.api.*` thin re-exports, and top-level `events.py` / `decorators.py` as a split compatibility surface; do not mechanically recreate the old tree from stale TODO comments.
# 开发命令
@@ -33,4 +34,4 @@ python run_tests.py --cov # 运行测试并生成覆盖率报告
```
## 重要
新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践
新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践

View File

@@ -7,6 +7,7 @@
- 2026-03-13: Transport-pair startup tests for `SupervisorRuntime` must start a real peer on the opposite transport and provide an `initialize` response. Wiring only the supervisor side drops or captures the outgoing initialize message without replying, and `Peer.initialize()` then waits forever.
- 2026-03-13: `CapabilityRouter.register(..., stream_handler=...)` uses the signature `(request_id, payload, cancel_token)`, not the peer-level `(message, token)`. Reusing the peer handler shape in router tests causes an immediate failed stream event before the test logic runs.
- 2026-03-13: `Peer` had an early-cancel race for inbound invokes: if `CancelMessage` arrived before the invoke task executed its first line, the task could be cancelled before sending any terminal event, leaving the caller's stream iterator waiting forever. Preserve a per-request start event and pre-check the cancel token at the top of `_handle_invoke`.
- 2026-03-13: `src-new/astrbot_sdk/api` and several top-level module docstrings overstated v4 compatibility gaps by labeling migrated or compat-backed APIs as "missing". Treat `_legacy_api.py`, `astrbot_sdk.api.*` thin re-exports, and top-level `events.py` / `decorators.py` as a split compatibility surface; do not mechanically recreate the old tree from stale TODO comments.
# 开发命令
@@ -33,4 +34,4 @@ python run_tests.py --cov # 运行测试并生成覆盖率报告
```
## 重要
新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践
新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践

View File

@@ -1,124 +1,23 @@
"""AstrBot SDK 公共 API 模块
"""旧版 ``astrbot_sdk.api`` 的兼容入口
此模块提供插件开发所需的公共接口,包括
- components: 命令组件基类
- event: 事件处理相关工具(过滤器、事件类)
- star: 插件上下文
新版 SDK 的推荐导入路径在顶层模块
注意:大部分 API 是为了兼容旧版插件而保留的兼容层。
新版 API 请参考 astrbot_sdk.context.Context 和 astrbot_sdk.decorators 模块。
- ``astrbot_sdk.context.Context``
- ``astrbot_sdk.events.MessageEvent``
- ``astrbot_sdk.decorators``
- ``astrbot_sdk.star.Star``
# TODO: 相比旧版 API (src/astrbot_sdk/api),新版缺少以下模块:
## 1. basic/ 模块 (完全缺失)
旧版路径: src/astrbot_sdk/api/basic/
- AstrBotConfig: AstrBot 配置类,继承自 dict
- BaseConversationManager: 会话管理基类,提供以下方法:
- new_conversation(): 新建对话
- switch_conversation(): 切换对话
- delete_conversation(): 删除对话
- get_curr_conversation_id(): 获取当前对话 ID
- get_conversation(): 获取对话
- get_conversations(): 获取对话列表
- get_filtered_conversations(): 获取过滤后的对话列表
- update_conversation(): 更新对话
- add_message_pair(): 添加消息对
- get_human_readable_context(): 获取人类可读上下文
- Conversation: 对话实体数据类 (platform_id, user_id, cid, history, title, persona_id, created_at, updated_at)
## 2. message/ 模块 (完全缺失)
旧版路径: src/astrbot_sdk/api/message/
- MessageChain: 消息链类,提供链式 API
- message(): 添加文本
- at(): 添加 @ 提及
- at_all(): 添加 @ 全体成员
- url_image(): 添加网络图片
- file_image(): 添加本地图片
- base64_image(): 添加 base64 图片
- use_t2i(): 设置文本转图片
- get_plain_text(): 获取纯文本
- squash_plain(): 合并文本段
- 消息组件类 (components.py):
- Plain, Image, Record, Video, File (基础类型)
- At, AtAll, Reply, Face, Node, Nodes (IM 类型)
- Share, Contact, Location, Music, Poke, Forward, Json, WechatEmoji 等
- ComponentType 枚举, BaseMessageComponent 基类
## 3. event/ 模块 (部分缺失)
旧版路径: src/astrbot_sdk/api/event/
已有:
- filter.py (简化版): command, regex, permission 装饰器
缺失:
- astrbot_message.py:
- MessageMember: 消息成员数据类 (user_id, nickname)
- Group: 群组数据类 (group_id, group_name, group_avatar, group_owner, group_admins, members)
- AstrBotMessage: AstrBot 消息对象 (type, self_id, session_id, message_id, sender, message, message_str, raw_message, timestamp, group)
- astr_message_event.py:
- AstrMessageEvent: 消息事件类,核心方法:
- get_platform_name/id(), get_message_str/messages/type()
- get_session_id/group_id/self_id/sender_id/sender_name()
- set_extra/get_extra/clear_extra()
- is_private_chat/is_wake_up/is_admin()
- set_result/stop_event/continue_event/is_stopped()
- make_result/plain_result/image_result/chain_result()
- send(), react(), get_group()
- AstrMessageEventModel: Pydantic 模型版本
- event_result.py:
- EventResultType: 事件结果类型枚举 (CONTINUE, STOP)
- ResultContentType: 结果内容类型枚举 (LLM_RESULT, GENERAL_RESULT, STREAMING_RESULT, STREAMING_FINISH)
- MessageEventResult: 消息事件结果类,继承 MessageChain
- event_type.py:
- EventType: 内部事件类型枚举 (OnAstrBotLoadedEvent, OnPlatformLoadedEvent, AdapterMessageEvent, OnLLMRequestEvent, OnLLMResponseEvent, OnDecoratingResultEvent, OnCallingFuncToolEvent, OnAfterMessageSentEvent)
- message_session.py:
- MessageSession: 消息会话标识类 (platform_name, message_type, session_id)
- message_type.py:
- MessageType: 消息类型枚举 (GROUP_MESSAGE, FRIEND_MESSAGE, OTHER_MESSAGE)
## 4. platform/ 模块 (完全缺失)
旧版路径: src/astrbot_sdk/api/platform/
- PlatformMetadata: 平台元数据类 (name, description, id, default_config_tmpl, adapter_display_name, logo_path)
## 5. provider/ 模块 (完全缺失)
旧版路径: src/astrbot_sdk/api/provider/
- LLMResponse: LLM 响应数据类
- role, result_chain, tools_call_args/tools_call_name/tools_call_ids
- raw_completion (支持 OpenAI/Anthropic/Google 格式)
- to_openai_tool_calls(), to_openai_to_calls_model()
## 6. star/ 模块 (部分缺失)
旧版路径: src/astrbot_sdk/api/star/
已有:
- context.py (兼容层): Context 类
缺失:
- star.py:
- StarMetadata: 插件元数据类 (name, author, desc, version, repo, module_path, root_dir_name, reserved, activated, config, star_handler_full_names, display_name, logo_path)
## 7. components/ 模块 (部分缺失)
旧版路径: src/astrbot_sdk/api/components/
已有:
- command.py: CommandComponent 兼容层
缺失: (无其他文件,旧版也只有 command.py)
## 8. filter.py 装饰器 (部分缺失)
旧版 filter.py 导出的装饰器:
已有: command, regex, permission
缺失:
- custom_filter: 自定义过滤器
- event_message_type: 事件消息类型
- platform_adapter_type: 平台适配器类型
- after_message_sent: 消息发送后钩子
- on_astrbot_loaded: AstrBot 加载完成钩子
- on_platform_loaded: 平台加载完成钩子
- on_decorating_result: 结果装饰钩子
- on_llm_request: LLM 请求钩子
- on_llm_response: LLM 响应钩子
- command_group: 命令组
- llm_tool: LLM 工具注册 (已注释)
旧版还导出:
- CustomFilter, EventMessageType, EventMessageTypeFilter
- PermissionType, PermissionTypeFilter
- PlatformAdapterType, PlatformAdapterTypeFilter
这里保留 ``api`` 目录,目的是兼容旧版插件的导入路径,而不是复制一套独立的新运行时。
"""
__all__: list[str] = []
from . import basic, components, event, message, platform, provider, star
__all__ = [
"basic",
"components",
"event",
"message",
"platform",
"provider",
"star",
]

View File

@@ -0,0 +1,7 @@
"""旧版 ``astrbot_sdk.api.basic`` 的兼容入口。"""
from .astrbot_config import AstrBotConfig
from .conversation_mgr import BaseConversationManager
from .entities import Conversation
__all__ = ["AstrBotConfig", "BaseConversationManager", "Conversation"]

View File

@@ -0,0 +1,8 @@
"""旧版配置对象兼容类型。"""
class AstrBotConfig(dict):
"""兼容旧版 ``AstrBotConfig``。
旧版实现本身就是 ``dict`` 的薄封装,兼容层保持这一行为。
"""

View File

@@ -0,0 +1,5 @@
"""旧版会话管理器兼容导出。"""
from ..._legacy_api import LegacyConversationManager as BaseConversationManager
__all__ = ["BaseConversationManager"]

View File

@@ -0,0 +1,20 @@
"""旧版基础实体兼容类型。"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass(slots=True)
class Conversation:
"""兼容旧版对话实体。"""
platform_id: str
user_id: str
cid: str
history: list[dict[str, Any]] = field(default_factory=list)
title: str | None = ""
persona_id: str | None = ""
created_at: int = 0
updated_at: int = 0

View File

@@ -1,20 +1,4 @@
"""命令组件模块。
提供 CommandComponent 基类,用于定义命令处理器。
CommandComponent 是旧版 Star 基类的别名,用于向后兼容。
新版插件建议直接使用 astrbot_sdk.star.Star 和装饰器模式。
# TODO: 新旧版 components 模块对比:
## 旧版 components 模块结构:
- command.py: CommandComponent 基类
## 新版与旧版基本一致:
此模块功能完整,与旧版保持兼容。
## 无缺失内容
"""
"""旧版 ``astrbot_sdk.api.components`` 的兼容入口。"""
from .command import CommandComponent

View File

@@ -1,37 +1,4 @@
"""命令组件兼容层。
此模块提供旧版 CommandComponent 的向后兼容导入。
CommandComponent 是插件命令处理器的基类。
迁移说明:
- 旧版: 继承 CommandComponent实现 command() 方法
- 新版: 继承 astrbot_sdk.star.Star使用 @on_command 装饰器
示例:
# 旧版写法
class MyCommand(CommandComponent):
async def command(self, ctx: Context):
...
# 新版写法
from astrbot_sdk import Star, on_command
class MyPlugin(Star):
@on_command("hello")
async def handle_hello(self, ctx: Context):
...
# TODO: 新旧版 components 模块对比:
## 旧版 components 模块结构:
- command.py: CommandComponent 基类(已有兼容层)
## 新版与旧版基本一致:
旧版 components 模块仅包含 command.py新版已提供兼容层。
## 无缺失内容
此模块功能完整,与旧版保持兼容。
"""
"""旧版 ``CommandComponent`` 的兼容导出。"""
from ..._legacy_api import CommandComponent

View File

@@ -1,51 +1,26 @@
"""事件处理 API 模块。
"""旧版 ``astrbot_sdk.api.event`` 的兼容入口。"""
提供事件相关的公共接口:
- AstrMessageEvent: 消息事件类,包含消息文本、用户信息、平台信息等
- filter: 事件过滤器命名空间,提供命令、正则、权限等装饰器
- ADMIN: 管理员权限常量
此模块是旧版 astrbot_sdk.api.event 的兼容层。
新版 API 建议直接使用 astrbot_sdk.events.MessageEvent 和 astrbot_sdk.decorators。
# TODO: 相比旧版 event 模块,新版缺少以下内容:
## 缺失的文件:
1. astrbot_message.py:
- MessageMember: 消息成员数据类
- Group: 群组数据类
- AstrBotMessage: AstrBot 消息对象
2. astr_message_event.py:
- AstrMessageEvent: 完整的消息事件类(当前只有 MessageEvent 别名)
- AstrMessageEventModel: Pydantic 模型版本
3. event_result.py:
- EventResultType: 事件结果类型枚举
- ResultContentType: 结果内容类型枚举
- MessageEventResult: 消息事件结果类
4. event_type.py:
- EventType: 内部事件类型枚举
5. message_session.py:
- MessageSession: 消息会话标识类
6. message_type.py:
- MessageType: 消息类型枚举
## 缺失的功能:
旧版 AstrMessageEvent 提供的便捷方法:
- get_platform_name/id(), get_message_str/messages/type()
- get_session_id/group_id/self_id/sender_id/sender_name()
- set_extra/get_extra/clear_extra() 额外信息存储
- is_private_chat/is_wake_up/is_admin() 状态检查
- set_result/stop_event/continue_event/is_stopped() 事件控制
- make_result/plain_result/image_result/chain_result() 结果构建
- send(), react(), get_group() 消息操作
"""
from ...events import MessageEvent as AstrMessageEvent
from .astr_message_event import AstrMessageEvent, AstrMessageEventModel
from .astrbot_message import AstrBotMessage, Group, MessageMember
from .event_result import EventResultType, MessageEventResult, ResultContentType
from .event_type import EventType
from .filter import ADMIN, filter
from .message_session import MessageSesion, MessageSession
from .message_type import MessageType
__all__ = ["ADMIN", "AstrMessageEvent", "filter"]
__all__ = [
"ADMIN",
"AstrBotMessage",
"AstrMessageEvent",
"AstrMessageEventModel",
"EventResultType",
"EventType",
"Group",
"MessageEventResult",
"MessageMember",
"MessageSesion",
"MessageSession",
"MessageType",
"ResultContentType",
"filter",
]

View File

@@ -0,0 +1,335 @@
"""旧版 ``AstrMessageEvent`` 的兼容包装。"""
from __future__ import annotations
from typing import Any, Literal
from pydantic import BaseModel, Field
from ...events import MessageEvent
from ..message.chain import MessageChain
from ..message.components import BaseMessageComponent
from ..platform.platform_metadata import PlatformMetadata
from .astrbot_message import AstrBotMessage, Group, MessageMember
from .event_result import MessageEventResult
from .message_session import MessageSession
from .message_type import MessageType
def _coerce_message_type(
message_type: MessageType | str | None,
*,
has_group: bool = False,
has_user: bool = False,
) -> MessageType:
if isinstance(message_type, MessageType):
return message_type
if isinstance(message_type, str):
try:
return MessageType(message_type)
except ValueError:
pass
if has_group:
return MessageType.GROUP_MESSAGE
if has_user:
return MessageType.FRIEND_MESSAGE
return MessageType.OTHER_MESSAGE
class AstrMessageEventModel(BaseModel):
message_str: str
message_obj: AstrBotMessage
platform_meta: PlatformMetadata | None = None
session_id: str
role: Literal["admin", "member"] = "member"
is_wake: bool = False
is_at_or_wake_command: bool = False
extras: dict[str, Any] = Field(default_factory=dict)
result: MessageEventResult | None = None
has_send_oper: bool = False
call_llm: bool = False
plugins_name: list[str] = Field(default_factory=list)
@classmethod
def from_event(cls, event: "AstrMessageEvent") -> "AstrMessageEventModel":
return cls(
message_str=event.get_message_str(),
message_obj=event.message_obj,
platform_meta=event.platform_meta,
session_id=event.session_id,
role=event.role,
is_wake=event.is_wake,
is_at_or_wake_command=event.is_at_or_wake_command,
extras=dict(event.get_extra()),
result=event.get_result(),
has_send_oper=event.has_send_oper,
call_llm=event.call_llm,
plugins_name=list(event._plugins_name),
)
def to_event(self) -> "AstrMessageEvent":
return AstrMessageEvent(
text=self.message_str,
user_id=self.message_obj.sender.user_id,
group_id=self.message_obj.group_id,
platform=self.platform_meta.id if self.platform_meta else None,
session_id=self.session_id,
raw=self.message_obj.raw_message,
message_obj=self.message_obj,
platform_meta=self.platform_meta,
role=self.role,
is_wake=self.is_wake,
is_at_or_wake_command=self.is_at_or_wake_command,
extras=self.extras,
result=self.result,
has_send_oper=self.has_send_oper,
call_llm=self.call_llm,
plugins_name=self.plugins_name,
)
class AstrMessageEvent(MessageEvent):
def __init__(
self,
*,
text: str = "",
user_id: str | None = None,
group_id: str | None = None,
platform: str | None = None,
session_id: str | None = None,
raw: dict[str, Any] | None = None,
context=None,
reply_handler=None,
message_obj: AstrBotMessage | None = None,
platform_meta: PlatformMetadata | None = None,
role: Literal["admin", "member"] = "member",
is_wake: bool = False,
is_at_or_wake_command: bool = False,
extras: dict[str, Any] | None = None,
result: MessageEventResult | None = None,
has_send_oper: bool = False,
call_llm: bool = False,
plugins_name: list[str] | None = None,
) -> None:
super().__init__(
text=text,
user_id=user_id,
group_id=group_id,
platform=platform,
session_id=session_id,
raw=raw,
context=context,
reply_handler=reply_handler,
)
self.message_obj = message_obj or self._build_message_obj()
self.platform_meta = platform_meta
self.role = role
self.is_wake = is_wake
self.is_at_or_wake_command = is_at_or_wake_command
self._extras = dict(extras or {})
self._result = result
self.has_send_oper = has_send_oper
self.call_llm = call_llm
self._plugins_name = list(plugins_name or [])
self.session = MessageSession(
platform_name=self.get_platform_id(),
message_type=self.get_message_type(),
session_id=self.session_id,
)
self.unified_msg_origin = str(self.session)
self.platform = self.platform_meta or self.platform
@classmethod
def from_payload(
cls,
payload: dict[str, Any],
*,
context=None,
reply_handler=None,
) -> "AstrMessageEvent":
return cls(
text=str(payload.get("text", payload.get("message_str", ""))),
user_id=payload.get("user_id"),
group_id=payload.get("group_id"),
platform=payload.get("platform"),
session_id=payload.get("session_id"),
raw=payload,
context=context,
reply_handler=reply_handler,
)
@classmethod
def from_message_event(cls, event: MessageEvent) -> "AstrMessageEvent":
if isinstance(event, cls):
return event
return cls(
text=event.text,
user_id=event.user_id,
group_id=event.group_id,
platform=event.platform,
session_id=event.session_id,
raw=event.raw,
reply_handler=getattr(event, "_reply_handler", None),
)
def _build_message_obj(self) -> AstrBotMessage:
sender_payload = (
self.raw.get("sender") if isinstance(self.raw, dict) else {}
) or {}
sender = MessageMember(
user_id=str(sender_payload.get("user_id") or self.user_id or ""),
nickname=sender_payload.get("nickname"),
)
group = None
group_payload = (
self.raw.get("group") if isinstance(self.raw, dict) else None
) or None
if isinstance(group_payload, dict):
group = Group(
group_id=str(group_payload.get("group_id") or self.group_id or ""),
group_name=group_payload.get("group_name"),
group_avatar=group_payload.get("group_avatar"),
group_owner=group_payload.get("group_owner"),
group_admins=group_payload.get("group_admins"),
members=group_payload.get("members"),
)
elif self.group_id:
group = Group(group_id=self.group_id)
message_components = self.raw.get("message")
if not isinstance(message_components, list):
message_components = []
message_type = _coerce_message_type(
self.raw.get("message_type"),
has_group=bool(group),
has_user=bool(self.user_id),
)
return AstrBotMessage(
type=message_type,
self_id=str(self.raw.get("self_id", "")),
session_id=self.session_id,
message_id=str(self.raw.get("message_id", "")),
sender=sender,
message=message_components,
message_str=self.text,
raw_message=self.raw,
group=group,
)
def get_platform_name(self) -> str:
if self.platform_meta is not None:
return self.platform_meta.name
return str(self.raw.get("platform_name") or self.raw.get("platform") or "")
def get_platform_id(self) -> str:
if self.platform_meta is not None:
return self.platform_meta.id
return str(self.raw.get("platform_id") or self.raw.get("platform") or "")
def get_message_str(self) -> str:
return self.text
def get_messages(self) -> list[BaseMessageComponent]:
return list(self.message_obj.message)
def get_message_type(self) -> MessageType:
return self.message_obj.type
def get_session_id(self) -> str:
return self.session_id
def get_group_id(self) -> str:
return self.message_obj.group_id
def get_self_id(self) -> str:
return self.message_obj.self_id
def get_sender_id(self) -> str:
return self.message_obj.sender.user_id
def get_sender_name(self) -> str | None:
return self.message_obj.sender.nickname
def set_extra(self, key: str, value: Any) -> None:
self._extras[key] = value
def get_extra(self, key: str | None = None, default: Any = None) -> Any:
if key is None:
return self._extras
return self._extras.get(key, default)
def clear_extra(self) -> None:
self._extras.clear()
def is_private_chat(self) -> bool:
return self.get_message_type() == MessageType.FRIEND_MESSAGE
def is_wake_up(self) -> bool:
return self.is_wake
def is_admin(self) -> bool:
return self.role == "admin"
def set_result(self, result: MessageEventResult | str) -> None:
if isinstance(result, str):
result = MessageEventResult().message(result)
self._result = result
def stop_event(self) -> None:
if self._result is None:
self._result = MessageEventResult().stop_event()
return
self._result.stop_event()
def continue_event(self) -> None:
if self._result is None:
self._result = MessageEventResult().continue_event()
return
self._result.continue_event()
def is_stopped(self) -> bool:
if self._result is None:
return False
return self._result.is_stopped()
def should_call_llm(self, call_llm: bool) -> None:
self.call_llm = call_llm
def get_result(self) -> MessageEventResult | None:
return self._result
def clear_result(self) -> None:
self._result = None
def make_result(self) -> MessageEventResult:
return MessageEventResult()
def plain_result(self, text: str) -> MessageEventResult:
return MessageEventResult().message(text)
def image_result(self, url_or_path: str) -> MessageEventResult:
result = MessageEventResult()
if url_or_path.startswith("http"):
return result.url_image(url_or_path)
return result.file_image(url_or_path)
def chain_result(self, chain: list[BaseMessageComponent]) -> MessageEventResult:
result = MessageEventResult()
result.chain = chain
return result
async def send(self, message: MessageChain) -> None:
self.has_send_oper = True
await self.reply(message.get_plain_text())
async def react(self, emoji: str) -> None:
self.has_send_oper = True
await self.reply(emoji)
async def get_group(self, group_id: str | None = None, **kwargs) -> Group | None:
if self.message_obj.group is None:
return None
if group_id is None or self.message_obj.group.group_id == group_id:
return self.message_obj.group
return None

View File

@@ -0,0 +1,55 @@
"""旧版消息对象兼容类型。"""
from __future__ import annotations
import time
from dataclasses import dataclass, field
from ..message.components import BaseMessageComponent
from .message_type import MessageType
@dataclass(slots=True)
class MessageMember:
user_id: str
nickname: str | None = None
@dataclass(slots=True)
class Group:
group_id: str
group_name: str | None = None
group_avatar: str | None = None
group_owner: str | None = None
group_admins: list[str] | None = None
members: list[MessageMember] | None = None
@dataclass(slots=True)
class AstrBotMessage:
type: MessageType
self_id: str
session_id: str
message_id: str
sender: MessageMember
message: list[BaseMessageComponent] = field(default_factory=list)
message_str: str = ""
raw_message: dict = field(default_factory=dict)
timestamp: int = field(default_factory=lambda: int(time.time()))
group: Group | None = None
@property
def group_id(self) -> str:
if self.group is None:
return ""
return self.group.group_id
@group_id.setter
def group_id(self, value: str) -> None:
if value:
if self.group is None:
self.group = Group(group_id=value)
else:
self.group.group_id = value
return
self.group = None

View File

@@ -0,0 +1,57 @@
"""旧版事件结果兼容类型。"""
from __future__ import annotations
import enum
from dataclasses import dataclass, field
from typing import Any
from ..message.chain import MessageChain
class EventResultType(enum.Enum):
CONTINUE = enum.auto()
STOP = enum.auto()
class ResultContentType(enum.Enum):
LLM_RESULT = enum.auto()
GENERAL_RESULT = enum.auto()
STREAMING_RESULT = enum.auto()
STREAMING_FINISH = enum.auto()
@dataclass
class MessageEventResult(MessageChain):
result_type: EventResultType | None = field(
default_factory=lambda: EventResultType.CONTINUE
)
result_content_type: ResultContentType | None = field(
default_factory=lambda: ResultContentType.GENERAL_RESULT
)
async_stream: Any | None = None
def stop_event(self) -> "MessageEventResult":
self.result_type = EventResultType.STOP
return self
def continue_event(self) -> "MessageEventResult":
self.result_type = EventResultType.CONTINUE
return self
def is_stopped(self) -> bool:
return self.result_type == EventResultType.STOP
def set_async_stream(self, stream: Any) -> "MessageEventResult":
self.async_stream = stream
return self
def set_result_content_type(self, typ: ResultContentType) -> "MessageEventResult":
self.result_content_type = typ
return self
def is_llm_result(self) -> bool:
return self.result_content_type == ResultContentType.LLM_RESULT
CommandResult = MessageEventResult

View File

@@ -0,0 +1,16 @@
"""旧版事件类型兼容枚举。"""
from __future__ import annotations
import enum
class EventType(enum.Enum):
OnAstrBotLoadedEvent = enum.auto()
OnPlatformLoadedEvent = enum.auto()
AdapterMessageEvent = enum.auto()
OnLLMRequestEvent = enum.auto()
OnLLMResponseEvent = enum.auto()
OnDecoratingResultEvent = enum.auto()
OnCallingFuncToolEvent = enum.auto()
OnAfterMessageSentEvent = enum.auto()

View File

@@ -1,47 +1,253 @@
"""事件过滤器模块
"""旧版事件过滤器兼容层
提供事件处理器的注册装饰器,用于声明式地定义事件触发条件。
此模块是旧版 filter API 的兼容层。
当前兼容层保证以下能力可运行:
使用方式:
# 方式一:直接使用函数
@command("hello")
async def handle_hello(ctx):
...
- ``command(name)`` -> ``on_command(name)``
- ``regex(pattern)`` -> ``on_message(regex=pattern)``
- ``permission(ADMIN)`` / ``permission_type(PermissionType.ADMIN)``
-> ``require_admin``
# 方式二:使用 filter 命名空间(旧版风格)
@filter.command("hello")
async def handle_hello(ctx):
...
其余旧版高级过滤器和生命周期钩子在 v4 运行时中没有等价执行链路,
兼容层保留名称用于导入兼容,但会在调用时显式报错,避免静默失效。
"""
新版建议直接使用 astrbot_sdk.decorators 模块中的装饰器。
from __future__ import annotations
# TODO: 相比旧版 filter.py新版缺少以下装饰器和类型
import enum
from abc import ABCMeta, abstractmethod
from typing import Any
## 缺失的装饰器:
1. custom_filter: 自定义过滤器装饰器
2. event_message_type: 事件消息类型过滤器
3. platform_adapter_type: 平台适配器类型过滤器
4. after_message_sent: 消息发送后钩子
5. on_astrbot_loaded: AstrBot 加载完成钩子
6. on_platform_loaded: 平台加载完成钩子
7. on_decorating_result: 结果装饰钩子
8. on_llm_request: LLM 请求钩子
9. on_llm_response: LLM 响应钩子
10. command_group: 命令组装饰器
11. llm_tool: LLM 工具注册 (旧版已注释)
from ...decorators import on_command, on_message, require_admin
from ..basic.astrbot_config import AstrBotConfig
from .astr_message_event import AstrMessageEvent
from .message_type import MessageType
## 缺失的类型导出:
1. CustomFilter: 自定义过滤器基类
2. EventMessageType: 事件消息类型枚举
3. EventMessageTypeFilter: 事件消息类型过滤器类
4. PermissionType: 权限类型枚举
5. PermissionTypeFilter: 权限类型过滤器类
6. PlatformAdapterType: 平台适配器类型枚举
7. PlatformAdapterTypeFilter: 平台适配器类型过滤器类
ADMIN = "admin"
class PermissionType(enum.Flag):
ADMIN = enum.auto()
MEMBER = enum.auto()
class PermissionTypeFilter:
def __init__(self, permission_type: PermissionType, raise_error: bool = True):
self.permission_type = permission_type
self.raise_error = raise_error
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
if self.permission_type == PermissionType.ADMIN:
return event.is_admin()
return True
class EventMessageType(enum.Flag):
GROUP_MESSAGE = enum.auto()
PRIVATE_MESSAGE = enum.auto()
OTHER_MESSAGE = enum.auto()
ALL = GROUP_MESSAGE | PRIVATE_MESSAGE | OTHER_MESSAGE
MESSAGE_TYPE_2_EVENT_MESSAGE_TYPE = {
MessageType.GROUP_MESSAGE: EventMessageType.GROUP_MESSAGE,
MessageType.FRIEND_MESSAGE: EventMessageType.PRIVATE_MESSAGE,
MessageType.OTHER_MESSAGE: EventMessageType.OTHER_MESSAGE,
}
class EventMessageTypeFilter:
def __init__(self, event_message_type: EventMessageType):
self.event_message_type = event_message_type
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
event_message_type = MESSAGE_TYPE_2_EVENT_MESSAGE_TYPE.get(
event.get_message_type()
)
if event_message_type is None:
return False
return bool(event_message_type & self.event_message_type)
class PlatformAdapterType(enum.Flag):
AIOCQHTTP = enum.auto()
QQOFFICIAL = enum.auto()
TELEGRAM = enum.auto()
WECOM = enum.auto()
LARK = enum.auto()
WECHATPADPRO = enum.auto()
DINGTALK = enum.auto()
DISCORD = enum.auto()
SLACK = enum.auto()
KOOK = enum.auto()
VOCECHAT = enum.auto()
WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
SATORI = enum.auto()
MISSKEY = enum.auto()
ALL = (
AIOCQHTTP
| QQOFFICIAL
| TELEGRAM
| WECOM
| LARK
| WECHATPADPRO
| DINGTALK
| DISCORD
| SLACK
| KOOK
| VOCECHAT
| WEIXIN_OFFICIAL_ACCOUNT
| SATORI
| MISSKEY
)
ADAPTER_NAME_2_TYPE = {
"aiocqhttp": PlatformAdapterType.AIOCQHTTP,
"qq_official": PlatformAdapterType.QQOFFICIAL,
"telegram": PlatformAdapterType.TELEGRAM,
"wecom": PlatformAdapterType.WECOM,
"lark": PlatformAdapterType.LARK,
"dingtalk": PlatformAdapterType.DINGTALK,
"discord": PlatformAdapterType.DISCORD,
"slack": PlatformAdapterType.SLACK,
"kook": PlatformAdapterType.KOOK,
"wechatpadpro": PlatformAdapterType.WECHATPADPRO,
"vocechat": PlatformAdapterType.VOCECHAT,
"weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
"satori": PlatformAdapterType.SATORI,
"misskey": PlatformAdapterType.MISSKEY,
}
class PlatformAdapterTypeFilter:
def __init__(self, platform_adapter_type_or_str: PlatformAdapterType | str):
if isinstance(platform_adapter_type_or_str, str):
self.platform_type = ADAPTER_NAME_2_TYPE.get(platform_adapter_type_or_str)
else:
self.platform_type = platform_adapter_type_or_str
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
adapter_type = ADAPTER_NAME_2_TYPE.get(event.get_platform_name())
if adapter_type is None or self.platform_type is None:
return False
return bool(adapter_type & self.platform_type)
class CustomFilterMeta(ABCMeta):
def __and__(cls, other):
if not issubclass(other, CustomFilter):
raise TypeError("Operands must be subclasses of CustomFilter.")
return CustomFilterAnd(cls(), other())
def __or__(cls, other):
if not issubclass(other, CustomFilter):
raise TypeError("Operands must be subclasses of CustomFilter.")
return CustomFilterOr(cls(), other())
class CustomFilter(metaclass=CustomFilterMeta):
def __init__(self, raise_error: bool = True, **kwargs: Any):
self.raise_error = raise_error
@abstractmethod
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
raise NotImplementedError
def __or__(self, other):
return CustomFilterOr(self, other)
def __and__(self, other):
return CustomFilterAnd(self, other)
class CustomFilterOr(CustomFilter):
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
super().__init__()
self.filter1 = filter1
self.filter2 = filter2
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
return self.filter1.filter(event, cfg) or self.filter2.filter(event, cfg)
class CustomFilterAnd(CustomFilter):
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
super().__init__()
self.filter1 = filter1
self.filter2 = filter2
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
return self.filter1.filter(event, cfg) and self.filter2.filter(event, cfg)
def command(name: str):
return on_command(name)
def regex(pattern: str):
return on_message(regex=pattern)
def permission(level: str | PermissionType):
if level in {ADMIN, PermissionType.ADMIN}:
return require_admin
def decorator(func):
return func
return decorator
def permission_type(level: PermissionType, raise_error: bool = True):
return permission(level)
def _unsupported_factory(name: str, replacement: str | None = None):
suggestion = f"请改用 {replacement}" if replacement else "当前没有直接替代实现"
message = (
f"astrbot_sdk.api.event.filter.{name}() 尚未在 v4 兼容层中实现。"
f"{suggestion},或改写为新版插件结构。"
)
def factory(*args, **kwargs):
raise NotImplementedError(message)
return factory
custom_filter = _unsupported_factory("custom_filter")
event_message_type = _unsupported_factory("event_message_type")
platform_adapter_type = _unsupported_factory("platform_adapter_type")
after_message_sent = _unsupported_factory("after_message_sent")
on_astrbot_loaded = _unsupported_factory("on_astrbot_loaded")
on_platform_loaded = _unsupported_factory("on_platform_loaded")
on_decorating_result = _unsupported_factory("on_decorating_result")
on_llm_request = _unsupported_factory("on_llm_request")
on_llm_response = _unsupported_factory("on_llm_response")
command_group = _unsupported_factory("command_group")
class _FilterNamespace:
command = staticmethod(command)
regex = staticmethod(regex)
permission = staticmethod(permission)
permission_type = staticmethod(permission_type)
custom_filter = staticmethod(custom_filter)
event_message_type = staticmethod(event_message_type)
platform_adapter_type = staticmethod(platform_adapter_type)
after_message_sent = staticmethod(after_message_sent)
on_astrbot_loaded = staticmethod(on_astrbot_loaded)
on_platform_loaded = staticmethod(on_platform_loaded)
on_decorating_result = staticmethod(on_decorating_result)
on_llm_request = staticmethod(on_llm_request)
on_llm_response = staticmethod(on_llm_response)
command_group = staticmethod(command_group)
filter = _FilterNamespace()
## 旧版导出列表 (参考):
__all__ = [
"ADMIN",
"CustomFilter",
"EventMessageType",
"EventMessageTypeFilter",
@@ -54,103 +260,14 @@ __all__ = [
"command_group",
"custom_filter",
"event_message_type",
# "llm_tool",
"filter",
"on_astrbot_loaded",
"on_decorating_result",
"on_llm_request",
"on_llm_response",
"on_platform_loaded",
"permission",
"permission_type",
"platform_adapter_type",
"regex",
]
"""
from __future__ import annotations
from ...decorators import on_command, on_message, require_admin
# 管理员权限级别常量
ADMIN = "admin"
def command(name: str):
"""注册命令处理器装饰器。
Args:
name: 命令名称,用户发送以此开头的消息时触发
Returns:
装饰器函数
示例:
@command("hello")
async def handle_hello(ctx):
await ctx.reply("Hello!")
"""
return on_command(name)
def regex(pattern: str):
"""注册正则匹配处理器装饰器。
Args:
pattern: 正则表达式模式,匹配的消息将触发处理器
Returns:
装饰器函数
示例:
@regex(r"hello\\s+(\\w+)")
async def handle_hello(ctx, match):
name = match.group(1)
await ctx.reply(f"Hello, {name}!")
"""
return on_message(regex=pattern)
def permission(level):
"""权限检查装饰器。
Args:
level: 权限级别,目前仅支持 ADMIN
Returns:
装饰器函数,仅当用户具有指定权限时才执行处理器
示例:
@command("admin_cmd")
@permission(ADMIN)
async def admin_only(ctx):
await ctx.reply("管理员命令已执行")
"""
if level == ADMIN:
return require_admin
def decorator(func):
return func
return decorator
class _FilterNamespace:
"""过滤器命名空间,提供旧版风格的方法调用。
用于支持 filter.command()、filter.regex() 等调用方式,
保持与旧版 API 的兼容性。
示例:
@filter.command("hello")
async def handle_hello(ctx):
...
"""
command = staticmethod(command)
regex = staticmethod(regex)
permission = staticmethod(permission)
# 过滤器命名空间实例,支持 filter.command() 等调用方式
filter = _FilterNamespace()
__all__ = ["ADMIN", "command", "regex", "permission", "filter"]

View File

@@ -0,0 +1,29 @@
"""旧版消息会话标识兼容类型。"""
from __future__ import annotations
from dataclasses import dataclass
from .message_type import MessageType
@dataclass(slots=True)
class MessageSession:
platform_name: str
message_type: MessageType
session_id: str
platform_id: str | None = None
def __post_init__(self) -> None:
self.platform_id = self.platform_name
def __str__(self) -> str:
return f"{self.platform_id}:{self.message_type.value}:{self.session_id}"
@staticmethod
def from_str(session_str: str) -> "MessageSession":
platform_id, message_type, session_id = session_str.split(":")
return MessageSession(platform_id, MessageType(message_type), session_id)
MessageSesion = MessageSession

View File

@@ -0,0 +1,11 @@
"""旧版消息类型兼容枚举。"""
from __future__ import annotations
from enum import Enum
class MessageType(Enum):
GROUP_MESSAGE = "GroupMessage"
FRIEND_MESSAGE = "FriendMessage"
OTHER_MESSAGE = "OtherMessage"

View File

@@ -0,0 +1,59 @@
"""旧版 ``astrbot_sdk.api.message`` 的兼容入口。"""
from .chain import MessageChain
from .components import (
At,
AtAll,
BaseMessageComponent,
ComponentType,
Contact,
Dice,
Face,
File,
Forward,
Image,
Json,
Location,
Music,
Node,
Nodes,
Plain,
Poke,
Record,
Reply,
RPS,
Shake,
Share,
Unknown,
Video,
WechatEmoji,
)
__all__ = [
"At",
"AtAll",
"BaseMessageComponent",
"ComponentType",
"Contact",
"Dice",
"Face",
"File",
"Forward",
"Image",
"Json",
"Location",
"MessageChain",
"Music",
"Node",
"Nodes",
"Plain",
"Poke",
"Record",
"Reply",
"RPS",
"Shake",
"Share",
"Unknown",
"Video",
"WechatEmoji",
]

View File

@@ -0,0 +1,76 @@
"""旧版消息链兼容实现。"""
from __future__ import annotations
from dataclasses import dataclass, field
from . import components as Comp
@dataclass(slots=True)
class MessageChain:
chain: list[Comp.BaseMessageComponent] = field(default_factory=list)
use_t2i_: bool | None = None
type: str | None = None
def message(self, message: str) -> "MessageChain":
self.chain.append(Comp.Plain(text=message))
return self
def at(self, name: str, qq: str) -> "MessageChain":
self.chain.append(Comp.At(user_id=qq, user_name=name))
return self
def at_all(self) -> "MessageChain":
self.chain.append(Comp.AtAll())
return self
def error(self, message: str) -> "MessageChain":
self.chain.append(Comp.Plain(text=message))
return self
def url_image(self, url: str) -> "MessageChain":
self.chain.append(Comp.Image(file=url))
return self
def file_image(self, path: str) -> "MessageChain":
self.chain.append(Comp.Image(file=path))
return self
def base64_image(self, base64_str: str) -> "MessageChain":
self.chain.append(Comp.Image(file=base64_str))
return self
def use_t2i(self, use_t2i: bool) -> "MessageChain":
self.use_t2i_ = use_t2i
return self
def get_plain_text(self) -> str:
return " ".join(
component.text
for component in self.chain
if isinstance(component, Comp.Plain)
)
def squash_plain(self) -> "MessageChain":
if not self.chain:
return self
new_chain: list[Comp.BaseMessageComponent] = []
first_plain: Comp.Plain | None = None
plain_texts: list[str] = []
for component in self.chain:
if isinstance(component, Comp.Plain):
if first_plain is None:
first_plain = component
new_chain.append(component)
plain_texts.append(component.text)
else:
new_chain.append(component)
if first_plain is not None:
first_plain.text = "".join(plain_texts)
self.chain = new_chain
return self

View File

@@ -0,0 +1,207 @@
"""旧版消息组件兼容类型。"""
from __future__ import annotations
from enum import Enum
from typing import Literal
from pydantic import BaseModel, Field
class ComponentType(str, Enum):
Plain = "Plain"
Image = "Image"
Record = "Record"
Video = "Video"
File = "File"
Face = "Face"
At = "At"
Node = "Node"
Nodes = "Nodes"
Poke = "Poke"
Reply = "Reply"
Forward = "Forward"
RPS = "RPS"
Dice = "Dice"
Shake = "Shake"
Share = "Share"
Contact = "Contact"
Location = "Location"
Music = "Music"
Json = "Json"
Unknown = "Unknown"
WechatEmoji = "WechatEmoji"
CompT = ComponentType
class BaseMessageComponent(BaseModel):
type: CompT
def to_dict(self) -> dict:
return self.model_dump()
class Plain(BaseMessageComponent):
type: Literal[CompT.Plain] = CompT.Plain
text: str
class Image(BaseMessageComponent):
type: Literal[CompT.Image] = CompT.Image
file: str
class Record(BaseMessageComponent):
type: Literal[CompT.Record] = CompT.Record
file: str
class Video(BaseMessageComponent):
type: Literal[CompT.Video] = CompT.Video
file: str
class File(BaseMessageComponent):
type: Literal[CompT.File] = CompT.File
file_name: str
mime_type: str | None = None
file: str
class At(BaseMessageComponent):
type: Literal[CompT.At] = CompT.At
user_id: str | None = None
user_name: str | None = None
class AtAll(At):
user_id: str = "all"
class Reply(BaseMessageComponent):
type: Literal[CompT.Reply] = CompT.Reply
id: str | int
chain: list[BaseMessageComponent] = Field(default_factory=list)
sender_id: int | str | None = 0
sender_nickname: str | None = ""
time: int | None = 0
message_str: str | None = ""
class Node(BaseMessageComponent):
type: Literal[CompT.Node] = CompT.Node
sender_id: str
nickname: str | None = None
content: list[BaseMessageComponent] = Field(default_factory=list)
class Nodes(BaseMessageComponent):
type: Literal[CompT.Nodes] = CompT.Nodes
nodes: list[Node] = Field(default_factory=list)
class Face(BaseMessageComponent):
type: Literal[CompT.Face] = CompT.Face
id: int
class RPS(BaseMessageComponent):
type: Literal[CompT.RPS] = CompT.RPS
class Dice(BaseMessageComponent):
type: Literal[CompT.Dice] = CompT.Dice
class Shake(BaseMessageComponent):
type: Literal[CompT.Shake] = CompT.Shake
class Share(BaseMessageComponent):
type: Literal[CompT.Share] = CompT.Share
url: str
title: str
content: str | None = ""
image: str | None = ""
class Contact(BaseMessageComponent):
type: Literal[CompT.Contact] = CompT.Contact
_type: str
id: int | None = 0
class Location(BaseMessageComponent):
type: Literal[CompT.Location] = CompT.Location
lat: float
lon: float
title: str | None = ""
content: str | None = ""
class Music(BaseMessageComponent):
type: Literal[CompT.Music] = CompT.Music
_type: str
id: int | None = 0
url: str | None = ""
audio: str | None = ""
title: str | None = ""
content: str | None = ""
image: str | None = ""
class Poke(BaseMessageComponent):
type: Literal[CompT.Poke] = CompT.Poke
id: int | None = 0
qq: int | None = 0
class Forward(BaseMessageComponent):
type: Literal[CompT.Forward] = CompT.Forward
id: str
class Json(BaseMessageComponent):
type: Literal[CompT.Json] = CompT.Json
data: dict
class Unknown(BaseMessageComponent):
type: Literal[CompT.Unknown] = CompT.Unknown
text: str
class WechatEmoji(BaseMessageComponent):
type: Literal[CompT.WechatEmoji] = CompT.WechatEmoji
md5: str | None = ""
md5_len: int | None = 0
cdnurl: str | None = ""
ComponentTypes = {
"plain": Plain,
"text": Plain,
"image": Image,
"record": Record,
"video": Video,
"file": File,
"face": Face,
"at": At,
"rps": RPS,
"dice": Dice,
"shake": Shake,
"share": Share,
"contact": Contact,
"location": Location,
"music": Music,
"reply": Reply,
"poke": Poke,
"forward": Forward,
"node": Node,
"nodes": Nodes,
"json": Json,
"unknown": Unknown,
"WechatEmoji": WechatEmoji,
}

View File

@@ -0,0 +1,5 @@
"""旧版 ``astrbot_sdk.api.platform`` 的兼容入口。"""
from .platform_metadata import PlatformMetadata
__all__ = ["PlatformMetadata"]

View File

@@ -0,0 +1,15 @@
"""旧版平台元数据兼容类型。"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(slots=True)
class PlatformMetadata:
name: str
description: str
id: str
default_config_tmpl: dict | None = None
adapter_display_name: str | None = None
logo_path: str | None = None

View File

@@ -0,0 +1,5 @@
"""旧版 ``astrbot_sdk.api.provider`` 的兼容入口。"""
from .entities import LLMResponse
__all__ = ["LLMResponse"]

View File

@@ -0,0 +1,110 @@
"""旧版 Provider 实体兼容类型。"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any
from ..message import components as Comp
from ..message.chain import MessageChain
try:
from astr_agent_sdk.message import ToolCall as _ToolCall
except ImportError:
@dataclass(slots=True)
class _ToolCallFunctionBody:
name: str
arguments: str
@dataclass(slots=True)
class _ToolCall:
id: str
function: _ToolCallFunctionBody
FunctionBody = _ToolCallFunctionBody
@dataclass(init=False)
class LLMResponse:
"""兼容旧版 LLM 响应对象。"""
role: str
result_chain: MessageChain | None
tools_call_args: list[dict[str, Any]]
tools_call_name: list[str]
tools_call_ids: list[str]
raw_completion: Any | None
_new_record: dict[str, Any] | None
_completion_text: str
is_chunk: bool
def __init__(
self,
role: str,
completion_text: str = "",
result_chain: MessageChain | None = None,
tools_call_args: list[dict[str, Any]] | None = None,
tools_call_name: list[str] | None = None,
tools_call_ids: list[str] | None = None,
raw_completion: Any | None = None,
_new_record: dict[str, Any] | None = None,
is_chunk: bool = False,
) -> None:
self.role = role
self.result_chain = result_chain
self.tools_call_args = list(tools_call_args or [])
self.tools_call_name = list(tools_call_name or [])
self.tools_call_ids = list(tools_call_ids or [])
self.raw_completion = raw_completion
self._new_record = _new_record
self._completion_text = completion_text
self.is_chunk = is_chunk
@property
def completion_text(self) -> str:
if self.result_chain:
return self.result_chain.get_plain_text()
return self._completion_text
@completion_text.setter
def completion_text(self, value: str) -> None:
if self.result_chain:
self.result_chain.chain = [
component
for component in self.result_chain.chain
if not isinstance(component, Comp.Plain)
]
self.result_chain.chain.insert(0, Comp.Plain(text=value))
return
self._completion_text = value
def to_openai_tool_calls(self) -> list[dict[str, Any]]:
ret: list[dict[str, Any]] = []
for idx, tool_call_arg in enumerate(self.tools_call_args):
ret.append(
{
"id": self.tools_call_ids[idx],
"function": {
"name": self.tools_call_name[idx],
"arguments": json.dumps(tool_call_arg),
},
"type": "function",
}
)
return ret
def to_openai_to_calls_model(self) -> list[_ToolCall]:
ret: list[_ToolCall] = []
for idx, tool_call_arg in enumerate(self.tools_call_args):
ret.append(
_ToolCall(
id=self.tools_call_ids[idx],
function=_ToolCall.FunctionBody(
name=self.tools_call_name[idx],
arguments=json.dumps(tool_call_arg),
),
)
)
return ret

View File

@@ -1,36 +1,6 @@
"""插件上下文 API 模块。
提供插件运行时上下文 Context 类,用于:
- 调用 LLM 生成文本
- 发送消息
- 管理会话
- 存储键值数据
此模块是旧版 astrbot_sdk.api.star 的兼容层。
Context 实际上是 LegacyContext 的别名,用于向后兼容旧版插件。
新版插件建议使用 astrbot_sdk.context.Context。
# TODO: 相比旧版 star 模块,新版缺少以下内容:
## 缺失的文件:
- star.py: StarMetadata 插件元数据类
## 旧版 star 模块结构:
1. context.py - Context 抽象类(已有兼容层)
2. star.py - StarMetadata 数据类(缺失)
StarMetadata 用于描述插件的元信息,包括:
- 基础信息: name, author, desc, version, repo
- 模块路径: module_path, root_dir_name
- 状态标志: reserved, activated
- 配置: config (AstrBotConfig)
- Handler 信息: star_handler_full_names
- 展示信息: display_name, logo_path
建议在新版中提供等效的插件元数据访问接口。
"""
"""旧版 ``astrbot_sdk.api.star`` 的兼容入口。"""
from .context import Context
from .star import StarMetadata
__all__ = ["Context"]
__all__ = ["Context", "StarMetadata"]

View File

@@ -1,41 +1,4 @@
"""插件上下文兼容层。
此模块提供旧版 Context 类的向后兼容导入。
Context 是插件运行时的核心接口,提供:
- llm_generate(): 调用 LLM 生成文本
- tool_loop_agent(): 运行 LLM Agent 循环
- send_message(): 发送消息到会话
- conversation_manager: 会话管理器
- put_kv_data()/get_kv_data()/delete_kv_data(): 键值存储
迁移说明:
- 旧版: from astrbot_sdk.api.star import Context
- 新版: from astrbot_sdk import Context (astrbot_sdk.context.Context)
新版 Context 提供更丰富的接口:
- ctx.llm.chat_raw(): LLM 调用
- ctx.platform.send(): 发送消息
- ctx.db.set()/get()/delete(): 数据存储
注意:使用旧版 API 会触发弃用警告。
# TODO: 相比旧版 star 模块,新版缺少以下内容:
## 缺失的文件:
1. star.py:
- StarMetadata: 插件元数据类
- name, author, desc, version, repo: 基础信息
- module_path, root_dir_name: 模块路径信息
- reserved, activated: 状态标志
- config: AstrBotConfig 插件配置
- star_handler_full_names: Handler 全名列表
- display_name, logo_path: 展示信息
## 缺失的导入(旧版 star/__init__.py 为空):
旧版 star 模块主要通过 context.py 提供 Context 类,
新版通过兼容层导入,但缺少 StarMetadata 的公开导出。
"""
"""旧版 ``Context`` 的兼容导出。"""
from ..._legacy_api import Context

View File

@@ -0,0 +1,30 @@
"""旧版插件元数据兼容类型。"""
from __future__ import annotations
from dataclasses import dataclass, field
from ..basic.astrbot_config import AstrBotConfig
@dataclass(slots=True)
class StarMetadata:
name: str | None = None
author: str | None = None
desc: str | None = None
version: str | None = None
repo: str | None = None
module_path: str | None = None
root_dir_name: str | None = None
reserved: bool = False
activated: bool = True
config: AstrBotConfig | None = None
star_handler_full_names: list[str] = field(default_factory=list)
display_name: str | None = None
logo_path: str | None = None
def __str__(self) -> str:
return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
def __repr__(self) -> str:
return str(self)

View File

@@ -1,50 +1,4 @@
# =============================================================================
# 新旧对比 - context.py
# =============================================================================
#
# 【旧版 src/astrbot_sdk/api/star/context.py】
# - Context 是抽象类 (ABC)
# - 包含 conversation_manager、persona_manager 属性
# - 提供方法: llm_generate(), tool_loop_agent(), send_message(),
# add_llm_tools(), put_kv_data(), get_kv_data(), delete_kv_data()
# - 所有方法都是抽象方法 (...)
# - 依赖: BaseConversationManager, ToolSet, FunctionTool, Message, LLMResponse, MessageChain
#
# 【新版 src-new/astrbot_sdk/context.py】
# - Context 是具体类,直接实例化
# - 包含客户端属性: llm, memory, db, platform
# - 包含: plugin_id, logger, cancel_token
# - 新增 CancelToken 数据类用于取消控制
# - 通过 CapabilityProxy 代理实现跨进程调用
#
# 【架构差异】
# - 旧版: 抽象基类,由 AstrBot Core 实现具体逻辑
# - 新版: 具体类,通过 CapabilityProxy 代理调用远程能力
#
# =============================================================================
# TODO: 功能缺失
# =============================================================================
#
# 1. 缺少 conversation_manager 属性
# - 旧版: conversation_manager: BaseConversationManager
# - 新版: 已移至 _legacy_api.py 的 LegacyConversationManager
# - 迁移: 使用 ctx.db 直接操作会话数据,或使用 compat.LegacyConversationManager
#
# 2. 缺少 persona_manager 属性
# - 旧版: persona_manager: Any
# - 新版: 无对应实现
# - TODO: 需要确定是否需要在 clients/ 中添加 PersonaClient
#
# 3. 缺少 _register_component() 方法
# - 旧版: 用于注册组件实例及其公共方法
# - 新版: 架构变化,不再需要此方法
#
# 4. 方法签名变化
# - 旧版 llm_generate(chat_provider_id, ...): 需要 chat_provider_id
# - 新版 LLMClient.chat_raw(prompt, ...): 不需要 chat_provider_id
# - 迁移: 使用 ctx.llm.chat_raw() 替代 ctx.llm_generate()
#
# =============================================================================
"""v4 原生运行时上下文。"""
from __future__ import annotations

View File

@@ -1,58 +1,8 @@
# =============================================================================
# 新旧对比 - decorators.py
# =============================================================================
#
# 【旧版 src/astrbot_sdk/api/event/filter.py】
# 导出的装饰器和类型:
# - 装饰器: command, regex, custom_filter, event_message_type, permission_type,
# platform_adapter_type, after_message_sent, on_astrbot_loaded,
# on_platform_loaded, on_decorating_result, on_llm_request, on_llm_response,
# command_group, llm_tool (注释掉)
# - 类型: CustomFilter, EventMessageType, EventMessageTypeFilter,
# PermissionType, PermissionTypeFilter, PlatformAdapterType, PlatformAdapterTypeFilter
#
# 【新版 src-new/astrbot_sdk/decorators.py】
# 提供的装饰器:
# - on_command, on_message, on_event, on_schedule, require_admin
# - 内部类型: HandlerMeta, HandlerCallable
#
# =============================================================================
# TODO: 功能缺失
# =============================================================================
#
# 1. 缺少装饰器
# - custom_filter: 自定义过滤器装饰器
# - event_message_type: 消息类型过滤器
# - permission_type: 权限类型过滤器 (有 require_admin 但更通用)
# - platform_adapter_type: 平台适配器类型过滤器
# - after_message_sent: 消息发送后钩子
# - on_astrbot_loaded: AstrBot 加载完成钩子
# - on_platform_loaded: 平台加载完成钩子
# - on_decorating_result: 结果装饰钩子
# - on_llm_request: LLM 请求钩子
# - on_llm_response: LLM 响应钩子
# - command_group: 命令组装饰器
# - llm_tool: LLM 工具装饰器 (旧版已注释)
#
# 2. 缺少类型定义
# - CustomFilter: 自定义过滤器基类
# - EventMessageType: 消息类型枚举
# - EventMessageTypeFilter: 消息类型过滤器
# - PermissionType: 权限类型枚举
# - PermissionTypeFilter: 权限类型过滤器
# - PlatformAdapterType: 平台适配器类型枚举
# - PlatformAdapterTypeFilter: 平台适配器类型过滤器
#
# 3. 命名差异
# - 旧版 command -> 新版 on_command
# - 旧版 regex -> 新版 on_message(regex=...)
# - 新版 on_message 支持关键词和平台过滤
#
# 4. 新增功能
# - on_schedule: 定时任务装饰器 (旧版无)
# - require_admin: 管理员权限快捷装饰器 (旧版用 permission_type)
#
# =============================================================================
"""v4 原生装饰器。
旧版 ``astrbot_sdk.api.event.filter`` 的兼容与降级边界由 compat 模块处理,
这里仅保留 v4 原生 trigger/permission 元数据建模。
"""
from __future__ import annotations

View File

@@ -1,59 +1,9 @@
# =============================================================================
# 新旧对比 - events.py
# =============================================================================
#
# 【旧版 src/astrbot_sdk/api/event/】
# 包含多个文件:
# - astr_message_event.py: AstrMessageEvent 类(约 370 行)
# - astrbot_message.py: AstrBotMessage 消息对象
# - event_result.py: MessageEventResult 事件结果
# - event_type.py: EventType 枚举
# - message_session.py: MessageSession 会话
# - message_type.py: MessageType 枚举
#
# 【新版 src-new/astrbot_sdk/events.py】
# 仅包含:
# - MessageEvent: 简化的消息事件类
# - PlainTextResult: 纯文本结果数据类
#
# =============================================================================
# TODO: 功能缺失
# =============================================================================
#
# 1. AstrMessageEvent 大量功能缺失
# - 属性缺失:
# - message_obj: AstrBotMessage 消息对象
# - platform_meta: PlatformMetadata 平台元信息
# - role: "admin" | "member" 角色
# - is_wake, is_at_or_wake_command: 唤醒状态
# - session, unified_msg_origin: 会话标识
#
# - 方法缺失:
# - get_platform_name(), get_platform_id(): 平台信息
# - get_messages(): 获取消息链
# - get_message_type(): 获取消息类型
# - get_group_id(), get_self_id(), get_sender_id(), get_sender_name(): ID 获取
# - set_extra(), get_extra(), clear_extra(): 额外信息存储
# - is_private_chat(), is_wake_up(), is_admin(): 状态判断
# - set_result(), stop_event(), continue_event(), is_stopped(): 事件控制
# - should_call_llm(), get_result(), clear_result(): LLM 控制
# - make_result(), plain_result(), image_result(), chain_result(): 结果构建
# - send(), react(), get_group(): 消息操作
#
# 2. 缺少关联类型
# - AstrBotMessage: 消息对象(包含 sender, message, type 等)
# - MessageEventResult: 事件结果(包含 chain, result_type 等)
# - MessageSession: 会话标识
# - MessageType: 消息类型枚举 (FRIEND_MESSAGE, GROUP_MESSAGE 等)
# - PlatformMetadata: 平台元信息
#
# 3. 新版 MessageEvent 特性
# - 简化设计,仅包含核心属性: text, user_id, group_id, platform, session_id
# - 通过 reply_handler 实现回复功能
# - 支持从 payload 构建: from_payload()
# - 支持序列化: to_payload()
#
# =============================================================================
"""v4 原生事件对象。
顶层 ``MessageEvent`` 保持精简,只承载 v4 运行时真正需要的基础能力。
旧版 ``AstrMessageEvent`` 的便捷方法与结果对象由
``astrbot_sdk.api.event`` 兼容层承接,而不是继续塞回顶层事件类型。
"""
from __future__ import annotations

View File

@@ -241,10 +241,16 @@ class HandlerDispatcher:
if len(non_none_types) == 1:
param_type = non_none_types[0]
# 注入 MessageEvent 及其子类
if param_type is MessageEvent or (
isinstance(param_type, type) and issubclass(param_type, MessageEvent)
):
# 注入 MessageEvent 及其子类。旧版 compat 事件类型会通过
# from_message_event() 包装成带便捷方法的对象。
if param_type is MessageEvent:
return event
if isinstance(param_type, type) and issubclass(param_type, MessageEvent):
if isinstance(event, param_type):
return event
factory = getattr(param_type, "from_message_event", None)
if callable(factory):
return factory(event)
return event
# 注入 Context 及其子类
@@ -256,6 +262,13 @@ class HandlerDispatcher:
return None
async def _consume_legacy_result(self, item: Any, event: MessageEvent) -> None:
from ..api.event.event_result import MessageEventResult
if isinstance(item, MessageEventResult):
plain_text = item.get_plain_text()
if plain_text:
await event.reply(plain_text)
return
if isinstance(item, PlainTextResult):
await event.reply(item.text)
return
@@ -273,6 +286,8 @@ class HandlerDispatcher:
ctx: Context,
) -> None:
if hasattr(owner, "on_error") and callable(owner.on_error):
await owner.on_error(exc, event, ctx)
result = owner.on_error(exc, event, ctx)
if inspect.isawaitable(result):
await result
return
await Star().on_error(exc, event, ctx)

View File

@@ -1,43 +1,8 @@
# =============================================================================
# 新旧对比 - star.py
# =============================================================================
#
# 【旧版 src/astrbot_sdk/api/star/star.py】
# - StarMetadata: 插件元数据类dataclass
# - 属性: name, author, desc, version, repo, module_path, root_dir_name,
# reserved, activated, config, star_handler_full_names, display_name, logo_path
# - 旧版没有 Star 基类定义(定义在其他文件中)
#
# 【新版 src-new/astrbot_sdk/star.py】
# - Star: 插件基类
# - __handlers__: 处理器方法名元组
# - __init_subclass__(): 自动收集装饰器标记的方法
# - on_start(), on_stop(), on_error(): 生命周期钩子
# - __astrbot_is_new_star__(): 标识新版 Star
#
# 【架构差异】
# - 旧版: StarMetadata 独立定义Star 类在其他地方
# - 新版: Star 基类在此文件,元数据通过装饰器自动收集
#
# =============================================================================
# TODO: 功能缺失
# =============================================================================
#
# 1. 缺少 StarMetadata 类
# - 旧版: @dataclass class StarMetadata
# - 新版: 无对应实现
# - 属性: name, author, desc, version, repo, module_path, root_dir_name,
# reserved, activated, config, star_handler_full_names, display_name, logo_path
# - 迁移: 可能需要在 api/star/__init__.py 或单独文件中实现
#
# 2. 生命周期方法差异
# - 新版 on_start/on_stop 参数类型为 Any | None
# - 应该使用更精确的类型注解
#
# 3. 缺少旧版 Star 接口方法(如果有的话)
# - 需要确认旧版 Star 是否有其他必须实现的接口方法
#
# =============================================================================
"""v4 原生插件基类。
旧版 ``StarMetadata`` 等兼容数据类型保留在 ``astrbot_sdk.api.star``
这里仅承载新版插件生命周期与 handler 收集逻辑。
"""
from __future__ import annotations

View File

@@ -5,7 +5,18 @@ Tests for api/event/filter.py - Event filter decorators and utilities.
from __future__ import annotations
from astrbot_sdk.api.event.filter import ADMIN, command, filter, permission, regex
import pytest
from astrbot_sdk.api.event.filter import (
ADMIN,
PermissionType,
command,
command_group,
filter,
permission,
permission_type,
regex,
)
from astrbot_sdk.decorators import get_handler_meta
from astrbot_sdk.protocol.descriptors import CommandTrigger, MessageTrigger
@@ -99,6 +110,27 @@ class TestPermissionFilter:
assert meta.trigger.command == "admin"
assert meta.permissions.require_admin is True
def test_permission_type_admin_sets_require_admin(self):
"""permission_type(PermissionType.ADMIN) should map to admin permission."""
@permission_type(PermissionType.ADMIN)
async def admin_handler():
pass
meta = get_handler_meta(admin_handler)
assert meta is not None
assert meta.permissions.require_admin is True
def test_permission_type_member_passes_through(self):
"""permission_type(PermissionType.MEMBER) should be a no-op."""
@permission_type(PermissionType.MEMBER)
async def member_handler():
pass
meta = get_handler_meta(member_handler)
assert meta is None or not meta.permissions.require_admin
class TestFilterNamespace:
"""Tests for filter namespace object."""
@@ -169,3 +201,12 @@ class TestModuleExports:
assert "regex" in __all__
assert "permission" in __all__
assert "filter" in __all__
class TestUnsupportedCompatFilters:
"""Tests for explicitly unsupported legacy helpers."""
def test_unsupported_filter_raises_explicitly(self):
"""Unsupported helpers should fail loudly instead of silently no-oping."""
with pytest.raises(NotImplementedError, match="command_group"):
command_group("group_name")

View File

@@ -21,6 +21,14 @@ class TestApiStarModule:
assert Context is LegacyContext
def test_star_module_exports_metadata(self):
"""api.star should export StarMetadata."""
from astrbot_sdk.api.star import StarMetadata
metadata = StarMetadata(name="demo", version="1.0.0")
assert metadata.name == "demo"
assert metadata.version == "1.0.0"
class TestApiComponentsModule:
"""Tests for api/components module exports."""
@@ -50,12 +58,12 @@ class TestApiEventModule:
assert filter is not None
assert AstrMessageEvent is not None
def test_astr_message_event_is_message_event(self):
"""AstrMessageEvent should be MessageEvent."""
def test_astr_message_event_is_message_event_subclass(self):
"""AstrMessageEvent should be a MessageEvent-compatible subclass."""
from astrbot_sdk.api.event import AstrMessageEvent
from astrbot_sdk.events import MessageEvent
assert AstrMessageEvent is MessageEvent
assert issubclass(AstrMessageEvent, MessageEvent)
def test_all_exports(self):
"""api.event should export all expected names."""
@@ -65,6 +73,18 @@ class TestApiEventModule:
assert "AstrMessageEvent" in __all__
assert "filter" in __all__
def test_event_module_exports_legacy_types(self):
"""api.event should expose common legacy helper types."""
from astrbot_sdk.api.event import (
MessageEventResult,
MessageSession,
MessageType,
)
assert MessageEventResult is not None
assert MessageSession is not None
assert MessageType is not None
class TestApiModule:
"""Tests for top-level api module."""
@@ -74,3 +94,12 @@ class TestApiModule:
import astrbot_sdk.api
assert astrbot_sdk.api is not None
def test_api_subpackages_exist(self):
"""New compat subpackages should be importable."""
from astrbot_sdk.api import basic, message, platform, provider
assert basic is not None
assert message is not None
assert platform is not None
assert provider is not None

View File

@@ -10,6 +10,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from astrbot_sdk.api.event import AstrMessageEvent
from astrbot_sdk.context import CancelToken, Context
from astrbot_sdk.events import MessageEvent, PlainTextResult
from astrbot_sdk.protocol.descriptors import (
@@ -39,7 +40,10 @@ class MockPeer:
) -> dict[str, Any]:
"""模拟 peer.invoke 方法,用于 CapabilityProxy"""
if name == "platform.send":
await self.send(payload.get("session_id", ""), payload.get("text", ""))
await self.send(
payload.get("session", payload.get("session_id", "")),
payload.get("text", ""),
)
return {}
return {}
@@ -314,6 +318,58 @@ class TestHandlerDispatcherInvoke:
# After completion, should be cleared
assert "msg_001" not in dispatcher._active
@pytest.mark.asyncio
async def test_invoke_wraps_legacy_astr_message_event(self):
"""Annotated AstrMessageEvent handlers should receive the compat wrapper."""
peer = MockPeer()
received_types = []
replies = []
async def handler_func(event: AstrMessageEvent):
received_types.append(type(event))
yield event.plain_result("legacy reply")
async def track_send(session_id: str, text: str) -> None:
replies.append({"session_id": session_id, "text": text})
peer.send = track_send
descriptor = HandlerDescriptor(
id="legacy.handler",
trigger=CommandTrigger(command="hello"),
)
handler = LoadedHandler(
descriptor=descriptor,
callable=handler_func,
owner=MagicMock(),
legacy_context=None,
)
dispatcher = HandlerDispatcher(
plugin_id="test_plugin",
peer=peer,
handlers=[handler],
)
message = InvokeMessage(
id="msg_legacy",
capability="handler.invoke",
input={
"handler_id": "legacy.handler",
"event": {
"text": "hello",
"session_id": "session-legacy",
"user_id": "user-1",
"platform": "test",
},
},
)
await dispatcher.invoke(message, CancelToken())
assert received_types == [AstrMessageEvent]
assert replies == [{"session_id": "session-legacy", "text": "legacy reply"}]
class TestHandlerDispatcherCancel:
"""Tests for HandlerDispatcher.cancel method."""