mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
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:
@@ -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 # 运行测试并生成覆盖率报告
|
||||
```
|
||||
|
||||
## 重要
|
||||
新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践
|
||||
新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践
|
||||
|
||||
@@ -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 # 运行测试并生成覆盖率报告
|
||||
```
|
||||
|
||||
## 重要
|
||||
新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践
|
||||
新实现要兼容旧实现但是还要保证架构良好,设计原则不变和最佳实践
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
7
src-new/astrbot_sdk/api/basic/__init__.py
Normal file
7
src-new/astrbot_sdk/api/basic/__init__.py
Normal 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"]
|
||||
8
src-new/astrbot_sdk/api/basic/astrbot_config.py
Normal file
8
src-new/astrbot_sdk/api/basic/astrbot_config.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""旧版配置对象兼容类型。"""
|
||||
|
||||
|
||||
class AstrBotConfig(dict):
|
||||
"""兼容旧版 ``AstrBotConfig``。
|
||||
|
||||
旧版实现本身就是 ``dict`` 的薄封装,兼容层保持这一行为。
|
||||
"""
|
||||
5
src-new/astrbot_sdk/api/basic/conversation_mgr.py
Normal file
5
src-new/astrbot_sdk/api/basic/conversation_mgr.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""旧版会话管理器兼容导出。"""
|
||||
|
||||
from ..._legacy_api import LegacyConversationManager as BaseConversationManager
|
||||
|
||||
__all__ = ["BaseConversationManager"]
|
||||
20
src-new/astrbot_sdk/api/basic/entities.py
Normal file
20
src-new/astrbot_sdk/api/basic/entities.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
335
src-new/astrbot_sdk/api/event/astr_message_event.py
Normal file
335
src-new/astrbot_sdk/api/event/astr_message_event.py
Normal 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
|
||||
55
src-new/astrbot_sdk/api/event/astrbot_message.py
Normal file
55
src-new/astrbot_sdk/api/event/astrbot_message.py
Normal 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
|
||||
57
src-new/astrbot_sdk/api/event/event_result.py
Normal file
57
src-new/astrbot_sdk/api/event/event_result.py
Normal 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
|
||||
16
src-new/astrbot_sdk/api/event/event_type.py
Normal file
16
src-new/astrbot_sdk/api/event/event_type.py
Normal 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()
|
||||
@@ -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"]
|
||||
|
||||
29
src-new/astrbot_sdk/api/event/message_session.py
Normal file
29
src-new/astrbot_sdk/api/event/message_session.py
Normal 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
|
||||
11
src-new/astrbot_sdk/api/event/message_type.py
Normal file
11
src-new/astrbot_sdk/api/event/message_type.py
Normal 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"
|
||||
59
src-new/astrbot_sdk/api/message/__init__.py
Normal file
59
src-new/astrbot_sdk/api/message/__init__.py
Normal 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",
|
||||
]
|
||||
76
src-new/astrbot_sdk/api/message/chain.py
Normal file
76
src-new/astrbot_sdk/api/message/chain.py
Normal 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
|
||||
207
src-new/astrbot_sdk/api/message/components.py
Normal file
207
src-new/astrbot_sdk/api/message/components.py
Normal 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,
|
||||
}
|
||||
5
src-new/astrbot_sdk/api/platform/__init__.py
Normal file
5
src-new/astrbot_sdk/api/platform/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""旧版 ``astrbot_sdk.api.platform`` 的兼容入口。"""
|
||||
|
||||
from .platform_metadata import PlatformMetadata
|
||||
|
||||
__all__ = ["PlatformMetadata"]
|
||||
15
src-new/astrbot_sdk/api/platform/platform_metadata.py
Normal file
15
src-new/astrbot_sdk/api/platform/platform_metadata.py
Normal 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
|
||||
5
src-new/astrbot_sdk/api/provider/__init__.py
Normal file
5
src-new/astrbot_sdk/api/provider/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""旧版 ``astrbot_sdk.api.provider`` 的兼容入口。"""
|
||||
|
||||
from .entities import LLMResponse
|
||||
|
||||
__all__ = ["LLMResponse"]
|
||||
110
src-new/astrbot_sdk/api/provider/entities.py
Normal file
110
src-new/astrbot_sdk/api/provider/entities.py
Normal 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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
30
src-new/astrbot_sdk/api/star/star.py
Normal file
30
src-new/astrbot_sdk/api/star/star.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user