mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat: 更新协议模块,增强 v4 协议适配器和消息解析功能,添加新测试用例
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
- 2026-03-13: `WorkerSession` cannot assume the caller-provided `repo_root` contains `src-new/astrbot_sdk`. Tests and external bootstraps may pass a temporary repo root while still expecting the in-tree SDK package to launch worker subprocesses via `python -m astrbot_sdk`. Resolve the SDK source directory from the real package location when the supplied root does not contain it.
|
||||
- 2026-03-13: `MemoryClient.get()` is part of the supported v4 client surface and must stay in sync with `CapabilityRouter` built-ins. The client method existed while the router forgot to register `memory.get`, which caused a real runtime gap hidden by API shape alone.
|
||||
- 2026-03-13: When checking whether a peer has finished remote initialization, avoid `getattr(mock, "remote_peer")` style probes in code that may receive `MagicMock` peers. `MagicMock` fabricates truthy child attributes, so `CapabilityProxy` should read explicit state from `peer.__dict__` or another concrete storage location instead of treating arbitrary attribute access as initialization.
|
||||
- 2026-03-13: The repository has no legacy `src/astrbot_sdk/protocol` package to migrate file-for-file. `src-new/astrbot_sdk/protocol` is a v4-native protocol layer; compare it against legacy JSON-RPC behavior in `src/astrbot_sdk/runtime/*` and the maintained migration tests, not against a nonexistent old package tree.
|
||||
|
||||
# 开发命令
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- 2026-03-13: `WorkerSession` cannot assume the caller-provided `repo_root` contains `src-new/astrbot_sdk`. Tests and external bootstraps may pass a temporary repo root while still expecting the in-tree SDK package to launch worker subprocesses via `python -m astrbot_sdk`. Resolve the SDK source directory from the real package location when the supplied root does not contain it.
|
||||
- 2026-03-13: `MemoryClient.get()` is part of the supported v4 client surface and must stay in sync with `CapabilityRouter` built-ins. The client method existed while the router forgot to register `memory.get`, which caused a real runtime gap hidden by API shape alone.
|
||||
- 2026-03-13: When checking whether a peer has finished remote initialization, avoid `getattr(mock, "remote_peer")` style probes in code that may receive `MagicMock` peers. `MagicMock` fabricates truthy child attributes, so `CapabilityProxy` should read explicit state from `peer.__dict__` or another concrete storage location instead of treating arbitrary attribute access as initialization.
|
||||
- 2026-03-13: The repository has no legacy `src/astrbot_sdk/protocol` package to migrate file-for-file. `src-new/astrbot_sdk/protocol` is a v4-native protocol layer; compare it against legacy JSON-RPC behavior in `src/astrbot_sdk/runtime/*` and the maintained migration tests, not against a nonexistent old package tree.
|
||||
|
||||
# 开发命令
|
||||
|
||||
|
||||
@@ -1,76 +1,101 @@
|
||||
"""协议模块。
|
||||
"""AstrBot v4 协议公共入口。
|
||||
|
||||
定义 AstrBot SDK 的消息协议和描述符,用于插件与核心之间的通信。
|
||||
所有消息均使用 Pydantic 定义,确保类型安全和序列化一致性。
|
||||
这里暴露的是协议层的公共模型和 legacy 适配入口。需要区分两件事:
|
||||
|
||||
架构说明:
|
||||
旧版:
|
||||
- 使用标准 JSON-RPC 2.0 协议
|
||||
- 消息类型较少: Request, SuccessResponse, ErrorResponse
|
||||
- 特定请求类型绑定了 AstrMessageEventModel 事件模型
|
||||
- 使用 dataclass 和 pydantic 混合定义
|
||||
1. v4 原生协议:
|
||||
`InitializeMessage` / `InvokeMessage` / `ResultMessage` / `EventMessage`
|
||||
2. legacy JSON-RPC 兼容:
|
||||
`LegacyAdapter` 及其若干便捷转换函数
|
||||
|
||||
新版:
|
||||
- 全新的自描述协议,使用 `type` 字段区分消息类型
|
||||
- 更丰富的消息类型: Initialize, Result, Invoke, Event, Cancel
|
||||
- 强大的描述符系统: HandlerDescriptor, CapabilityDescriptor
|
||||
- 多种触发器类型: Command, Message, Event, Schedule
|
||||
- 纯 Pydantic 定义,支持严格验证
|
||||
- 提供 LegacyAdapter 实现新旧协议互操作
|
||||
|
||||
协议消息流程:
|
||||
1. Initialize: 握手建立连接,交换能力和处理器信息
|
||||
2. Invoke: 调用远程能力
|
||||
3. Event: 流式事件通知 (started/delta/completed/failed)
|
||||
4. Result: 调用结果返回
|
||||
5. Cancel: 取消正在进行的调用
|
||||
|
||||
与旧版对比:
|
||||
旧版 JSON-RPC 消息:
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "xxx",
|
||||
"method": "call_handler",
|
||||
"params": {"handler_full_name": "...", "event": {...}}
|
||||
}
|
||||
|
||||
新版协议消息:
|
||||
{
|
||||
"type": "invoke",
|
||||
"id": "xxx",
|
||||
"capability": "handler.invoke",
|
||||
"input": {"handler_id": "...", "event": {...}}
|
||||
}
|
||||
|
||||
TODO: (功能完善):
|
||||
- 添加消息签名验证支持,确保消息来源可信
|
||||
- 添加消息压缩支持,减少大数据传输开销
|
||||
- 添加批量消息支持 (BatchMessage),提高传输效率
|
||||
- 添加消息追踪 ID (trace_id) 支持,便于日志关联
|
||||
- CapabilityDescriptor 缺少 rate_limit 限流配置
|
||||
- HandlerDescriptor 缺少 timeout 超时配置
|
||||
- 缺少心跳消息 (HeartbeatMessage) 支持
|
||||
- 缺少健康检查消息 (HealthCheckMessage) 支持
|
||||
握手阶段由 `InitializeMessage` 发起,返回值不是另一条 initialize 消息,而是
|
||||
`ResultMessage(kind="initialize_result")`,其 `output` 负载可解析为
|
||||
`InitializeOutput`。
|
||||
"""
|
||||
|
||||
from .descriptors import CapabilityDescriptor, HandlerDescriptor, Permissions
|
||||
from .descriptors import (
|
||||
CapabilityDescriptor,
|
||||
CommandTrigger,
|
||||
EventTrigger,
|
||||
HandlerDescriptor,
|
||||
MessageTrigger,
|
||||
Permissions,
|
||||
ScheduleTrigger,
|
||||
Trigger,
|
||||
)
|
||||
from .legacy_adapter import (
|
||||
LEGACY_ADAPTER_MESSAGE_EVENT,
|
||||
LEGACY_CONTEXT_CAPABILITY,
|
||||
LEGACY_HANDSHAKE_METADATA_KEY,
|
||||
LEGACY_JSONRPC_VERSION,
|
||||
LEGACY_PLUGIN_KEYS_METADATA_KEY,
|
||||
LegacyAdapter,
|
||||
LegacyErrorData,
|
||||
LegacyErrorResponse,
|
||||
LegacyMessage,
|
||||
LegacyRequest,
|
||||
LegacySuccessResponse,
|
||||
LegacyToV4Message,
|
||||
cancel_to_legacy_request,
|
||||
event_to_legacy_notification,
|
||||
initialize_to_legacy_handshake_response,
|
||||
invoke_to_legacy_request,
|
||||
legacy_message_to_v4,
|
||||
legacy_request_to_invoke,
|
||||
legacy_response_to_message,
|
||||
parse_legacy_message,
|
||||
result_to_legacy_response,
|
||||
)
|
||||
from .messages import (
|
||||
CancelMessage,
|
||||
ErrorPayload,
|
||||
EventMessage,
|
||||
InitializeMessage,
|
||||
InitializeOutput,
|
||||
InvokeMessage,
|
||||
PeerInfo,
|
||||
ProtocolMessage,
|
||||
ResultMessage,
|
||||
parse_message,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CapabilityDescriptor",
|
||||
"CommandTrigger",
|
||||
"CancelMessage",
|
||||
"ErrorPayload",
|
||||
"EventTrigger",
|
||||
"EventMessage",
|
||||
"HandlerDescriptor",
|
||||
"InitializeMessage",
|
||||
"InitializeOutput",
|
||||
"InvokeMessage",
|
||||
"LEGACY_ADAPTER_MESSAGE_EVENT",
|
||||
"LEGACY_CONTEXT_CAPABILITY",
|
||||
"LEGACY_HANDSHAKE_METADATA_KEY",
|
||||
"LEGACY_JSONRPC_VERSION",
|
||||
"LEGACY_PLUGIN_KEYS_METADATA_KEY",
|
||||
"LegacyAdapter",
|
||||
"LegacyErrorData",
|
||||
"LegacyErrorResponse",
|
||||
"LegacyMessage",
|
||||
"LegacyRequest",
|
||||
"LegacySuccessResponse",
|
||||
"LegacyToV4Message",
|
||||
"MessageTrigger",
|
||||
"PeerInfo",
|
||||
"Permissions",
|
||||
"ProtocolMessage",
|
||||
"ResultMessage",
|
||||
"ScheduleTrigger",
|
||||
"Trigger",
|
||||
"cancel_to_legacy_request",
|
||||
"event_to_legacy_notification",
|
||||
"initialize_to_legacy_handshake_response",
|
||||
"invoke_to_legacy_request",
|
||||
"legacy_message_to_v4",
|
||||
"legacy_request_to_invoke",
|
||||
"legacy_response_to_message",
|
||||
"parse_legacy_message",
|
||||
"parse_message",
|
||||
"result_to_legacy_response",
|
||||
]
|
||||
|
||||
@@ -1,39 +1,8 @@
|
||||
"""描述符模块。
|
||||
"""v4 协议描述符模型。
|
||||
|
||||
定义处理器和能力的描述符,用于声明式注册和发现。
|
||||
|
||||
描述符类型概览:
|
||||
HandlerDescriptor: 处理器描述符,描述一个事件处理函数
|
||||
CapabilityDescriptor: 能力描述符,描述一个可调用的远程能力
|
||||
Permissions: 权限配置,控制处理器的访问权限
|
||||
Trigger: 触发器联合类型,支持多种触发方式
|
||||
|
||||
触发器类型:
|
||||
CommandTrigger: 命令触发器,响应特定命令(如 /help)
|
||||
MessageTrigger: 消息触发器,响应匹配正则或关键词的消息
|
||||
EventTrigger: 事件触发器,响应特定类型的事件
|
||||
ScheduleTrigger: 定时触发器,按 cron 或间隔时间执行
|
||||
|
||||
与旧版对比:
|
||||
旧版:
|
||||
- 处理器元信息分散在 handshake 响应中
|
||||
- 使用 event_type 整数区分事件类型
|
||||
- 缺少声明式的触发器定义
|
||||
- 配置通过 extras_configs 字典传递
|
||||
|
||||
新版:
|
||||
- 使用 HandlerDescriptor 统一描述处理器
|
||||
- 使用字符串 event_type,更语义化
|
||||
- 支持多种触发器类型,声明式定义
|
||||
- 使用 Pydantic 模型,类型安全
|
||||
|
||||
TODO:
|
||||
- HandlerDescriptor 缺少 timeout 超时配置
|
||||
- HandlerDescriptor 缺少 retry 重试配置
|
||||
- CapabilityDescriptor 缺少 rate_limit 限流配置
|
||||
- ScheduleTrigger 缺错时错过执行的处理策略
|
||||
- 缺少 HandlerGroupDescriptor 处理器组描述符
|
||||
- 缺少 DependencyDescriptor 依赖声明
|
||||
`protocol` 是 v4 新引入的协议层抽象,不对应旧树中的一个同名目录。这里
|
||||
定义的是跨进程握手和调度时使用的声明式元数据,而不是运行时的具体处理器/
|
||||
能力实现。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -85,7 +54,7 @@ class CommandTrigger(_DescriptorBase):
|
||||
|
||||
|
||||
class MessageTrigger(_DescriptorBase):
|
||||
"""消息触发器,响应匹配正则或关键词的消息。
|
||||
"""消息触发器,描述消息类处理器的订阅条件。
|
||||
|
||||
与旧版对比:
|
||||
旧版: 使用 @regex_handler(r"pattern") 或 @message_handler 装饰器
|
||||
@@ -96,6 +65,10 @@ class MessageTrigger(_DescriptorBase):
|
||||
regex: 正则表达式模式,匹配消息文本
|
||||
keywords: 关键词列表,消息包含任一关键词即触发
|
||||
platforms: 目标平台列表,为空表示所有平台
|
||||
|
||||
Note:
|
||||
`regex` 和 `keywords` 可以同时为空,此时表示“任意消息均可触发”,
|
||||
仅由平台过滤或上层运行时进一步筛选。
|
||||
"""
|
||||
|
||||
type: Literal["message"] = "message"
|
||||
@@ -224,3 +197,15 @@ class CapabilityDescriptor(_DescriptorBase):
|
||||
output_schema: dict[str, Any] | None = None
|
||||
supports_stream: bool = False
|
||||
cancelable: bool = False
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CapabilityDescriptor",
|
||||
"CommandTrigger",
|
||||
"EventTrigger",
|
||||
"HandlerDescriptor",
|
||||
"MessageTrigger",
|
||||
"Permissions",
|
||||
"ScheduleTrigger",
|
||||
"Trigger",
|
||||
]
|
||||
|
||||
@@ -1,35 +1,8 @@
|
||||
"""旧版协议适配器模块。
|
||||
"""legacy JSON-RPC 与 v4 协议之间的适配器。
|
||||
|
||||
提供旧版 JSON-RPC 协议与新版协议之间的双向转换。
|
||||
支持旧版插件与新版核心的互操作。
|
||||
|
||||
主要功能:
|
||||
- 将旧版 JSON-RPC 请求转换为新版 InvokeMessage
|
||||
- 将旧版 JSON-RPC 响应转换为新版 ResultMessage
|
||||
- 将旧版 handshake 转换为新版 InitializeMessage
|
||||
- 将新版消息转换回旧版格式(用于与旧版核心通信)
|
||||
|
||||
转换映射表:
|
||||
旧版 method -> 新版 capability
|
||||
------------------------------------------------
|
||||
handshake -> InitializeMessage
|
||||
call_handler -> handler.invoke
|
||||
call_context_function -> internal.legacy.call_context_function
|
||||
handler_stream_start -> EventMessage(phase="started")
|
||||
handler_stream_update -> EventMessage(phase="delta")
|
||||
handler_stream_end -> EventMessage(phase="completed"/"failed")
|
||||
cancel -> CancelMessage
|
||||
|
||||
注意事项:
|
||||
- 旧版 handshake 的 metadata 信息可能丢失部分字段
|
||||
- 新版触发器的详细信息在转换时可能丢失
|
||||
- 使用 LEGACY_HANDSHAKE_METADATA_KEY 保留原始握手数据
|
||||
|
||||
TODO:
|
||||
- 添加旧版消息版本检测和兼容性警告
|
||||
- 添加消息转换日志记录,便于调试
|
||||
- 支持自定义转换规则扩展
|
||||
- 添加转换性能监控
|
||||
旧树没有独立的 `protocol` 包;这里做的是“旧 JSON-RPC 运行时语义”到
|
||||
“v4 协议模型”的转换。它不是完美双向同构,尤其是 legacy handshake 无法
|
||||
保留 v4 触发器的全部细节,因此适配器会保留原始握手载荷供兼容层回退使用。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -627,6 +600,8 @@ def parse_legacy_message(
|
||||
payload = payload.decode("utf-8")
|
||||
if isinstance(payload, str):
|
||||
payload = json.loads(payload)
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("legacy JSON-RPC 消息必须是 JSON object")
|
||||
if "method" in payload:
|
||||
return LegacyRequest.model_validate(payload)
|
||||
if "result" in payload:
|
||||
@@ -642,7 +617,9 @@ def legacy_message_to_v4(
|
||||
return LegacyAdapter().legacy_to_v4(payload)
|
||||
|
||||
|
||||
def legacy_request_to_invoke(payload: dict[str, Any]) -> InvokeMessage:
|
||||
def legacy_request_to_invoke(
|
||||
payload: str | bytes | dict[str, Any] | LegacyRequest,
|
||||
) -> InvokeMessage:
|
||||
message = LegacyAdapter().legacy_request_to_message(payload)
|
||||
if not isinstance(message, InvokeMessage):
|
||||
raise ValueError("legacy request 不能直接映射为 invoke")
|
||||
@@ -650,7 +627,7 @@ def legacy_request_to_invoke(payload: dict[str, Any]) -> InvokeMessage:
|
||||
|
||||
|
||||
def legacy_response_to_message(
|
||||
payload: dict[str, Any],
|
||||
payload: str | bytes | dict[str, Any] | LegacySuccessResponse,
|
||||
) -> InitializeMessage | ResultMessage:
|
||||
message = LegacyAdapter().legacy_response_to_message(payload)
|
||||
return message
|
||||
@@ -695,11 +672,14 @@ __all__ = [
|
||||
"LEGACY_CONTEXT_CAPABILITY",
|
||||
"LEGACY_HANDSHAKE_METADATA_KEY",
|
||||
"LEGACY_PLUGIN_KEYS_METADATA_KEY",
|
||||
"LEGACY_JSONRPC_VERSION",
|
||||
"LegacyAdapter",
|
||||
"LegacyErrorData",
|
||||
"LegacyErrorResponse",
|
||||
"LegacyMessage",
|
||||
"LegacyRequest",
|
||||
"LegacySuccessResponse",
|
||||
"LegacyToV4Message",
|
||||
"cancel_to_legacy_request",
|
||||
"event_to_legacy_notification",
|
||||
"initialize_to_legacy_handshake_response",
|
||||
|
||||
@@ -1,47 +1,9 @@
|
||||
"""协议消息定义模块。
|
||||
"""v4 协议消息模型。
|
||||
|
||||
定义 AstrBot SDK 的核心消息类型,所有消息均继承自 Pydantic BaseModel。
|
||||
|
||||
消息类型概览:
|
||||
InitializeMessage: 握手初始化消息,包含 Peer 信息和处理器列表
|
||||
ResultMessage: 调用结果消息,包含成功/失败状态和输出数据
|
||||
InvokeMessage: 能力调用消息,指定目标能力和输入参数
|
||||
EventMessage: 流式事件消息,用于流式调用的状态通知
|
||||
CancelMessage: 取消消息,用于取消正在进行的调用
|
||||
|
||||
消息生命周期:
|
||||
握手阶段:
|
||||
Plugin -> Core: InitializeMessage (注册处理器)
|
||||
Core -> Plugin: ResultMessage (确认或拒绝)
|
||||
|
||||
调用阶段:
|
||||
Plugin -> Core: InvokeMessage (调用能力)
|
||||
Core -> Plugin: ResultMessage (返回结果)
|
||||
或者 (流式):
|
||||
Core -> Plugin: EventMessage (started -> delta* -> completed/failed)
|
||||
|
||||
取消阶段:
|
||||
Plugin -> Core: CancelMessage (取消调用)
|
||||
|
||||
与旧版对比:
|
||||
旧版 JSON-RPC:
|
||||
- 使用 method 字段区分操作类型
|
||||
- 使用 jsonrpc: "2.0" 标识协议版本
|
||||
- 错误码为整数 (如 -32000)
|
||||
- 无专门的取消消息类型
|
||||
|
||||
新版协议:
|
||||
- 使用 type 字段区分消息类型
|
||||
- 使用 protocol_version 字段标识版本
|
||||
- 错误码为字符串 (如 "internal_error")
|
||||
- 有专门的 CancelMessage 取消消息
|
||||
|
||||
TODO:
|
||||
- 添加消息过期时间 (expires_at) 支持
|
||||
- 添加消息优先级 (priority) 支持
|
||||
- 添加消息重试计数 (retry_count) 支持
|
||||
- ErrorPayload 缺少 stack_trace 字段(调试用)
|
||||
- InitializeMessage 缺少 authentication 认证字段
|
||||
这些模型描述的是 `Peer` 与 `Peer` 之间的线协议。握手阶段通过
|
||||
`InitializeMessage` 发起,再由 `ResultMessage(kind="initialize_result")`
|
||||
返回 `InitializeOutput`;能力调用阶段则使用 `InvokeMessage` / `ResultMessage`
|
||||
或 `EventMessage` 序列。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -184,6 +146,19 @@ class ResultMessage(_MessageBase):
|
||||
output: dict[str, Any] = Field(default_factory=dict)
|
||||
error: ErrorPayload | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_result_state(self) -> "ResultMessage":
|
||||
"""约束 success / output / error 的组合状态。"""
|
||||
if self.success:
|
||||
if self.error is not None:
|
||||
raise ValueError("success=true 时 error 必须为空")
|
||||
return self
|
||||
if self.error is None:
|
||||
raise ValueError("success=false 时必须提供 error")
|
||||
if self.output:
|
||||
raise ValueError("success=false 时 output 必须为空")
|
||||
return self
|
||||
|
||||
|
||||
class InvokeMessage(_MessageBase):
|
||||
"""调用消息,用于请求执行远程能力。
|
||||
@@ -309,15 +284,25 @@ ProtocolMessage = (
|
||||
)
|
||||
"""协议消息联合类型,所有有效消息类型的联合。"""
|
||||
|
||||
_PROTOCOL_MESSAGE_MODELS = {
|
||||
"initialize": InitializeMessage,
|
||||
"result": ResultMessage,
|
||||
"invoke": InvokeMessage,
|
||||
"event": EventMessage,
|
||||
"cancel": CancelMessage,
|
||||
}
|
||||
|
||||
def parse_message(payload: str | bytes | dict[str, Any]) -> ProtocolMessage:
|
||||
|
||||
def parse_message(
|
||||
payload: ProtocolMessage | str | bytes | dict[str, Any],
|
||||
) -> ProtocolMessage:
|
||||
"""解析协议消息。
|
||||
|
||||
从原始载荷(字符串、字节或字典)解析为对应的 ProtocolMessage 类型。
|
||||
根据 "type" 字段自动识别消息类型并验证。
|
||||
|
||||
Args:
|
||||
payload: 原始消息载荷,支持 JSON 字符串、字节或字典
|
||||
payload: 原始消息载荷,支持已解析模型、JSON 字符串、字节或字典
|
||||
|
||||
Returns:
|
||||
解析后的协议消息对象
|
||||
@@ -330,19 +315,39 @@ def parse_message(payload: str | bytes | dict[str, Any]) -> ProtocolMessage:
|
||||
>>> isinstance(msg, InvokeMessage)
|
||||
True
|
||||
"""
|
||||
if isinstance(
|
||||
payload,
|
||||
(
|
||||
InitializeMessage,
|
||||
ResultMessage,
|
||||
InvokeMessage,
|
||||
EventMessage,
|
||||
CancelMessage,
|
||||
),
|
||||
):
|
||||
return payload
|
||||
if isinstance(payload, bytes):
|
||||
payload = payload.decode("utf-8")
|
||||
if isinstance(payload, str):
|
||||
payload = json.loads(payload)
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("协议消息必须是 JSON object")
|
||||
message_type = payload.get("type")
|
||||
if message_type == "initialize":
|
||||
return InitializeMessage.model_validate(payload)
|
||||
if message_type == "result":
|
||||
return ResultMessage.model_validate(payload)
|
||||
if message_type == "invoke":
|
||||
return InvokeMessage.model_validate(payload)
|
||||
if message_type == "event":
|
||||
return EventMessage.model_validate(payload)
|
||||
if message_type == "cancel":
|
||||
return CancelMessage.model_validate(payload)
|
||||
model = _PROTOCOL_MESSAGE_MODELS.get(str(message_type))
|
||||
if model is not None:
|
||||
return model.model_validate(payload)
|
||||
raise ValueError(f"未知消息类型:{message_type}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CancelMessage",
|
||||
"ErrorPayload",
|
||||
"EventMessage",
|
||||
"InitializeMessage",
|
||||
"InitializeOutput",
|
||||
"InvokeMessage",
|
||||
"PeerInfo",
|
||||
"ProtocolMessage",
|
||||
"ResultMessage",
|
||||
"parse_message",
|
||||
]
|
||||
|
||||
@@ -158,6 +158,11 @@ class TestParseLegacyMessage:
|
||||
parse_legacy_message({"jsonrpc": "2.0", "unknown": "field"})
|
||||
assert "未知" in str(exc_info.value)
|
||||
|
||||
def test_parse_non_mapping_raises(self):
|
||||
"""parse_legacy_message should reject non-object payloads."""
|
||||
with pytest.raises(ValueError, match="JSON object"):
|
||||
parse_legacy_message(["not", "an", "object"])
|
||||
|
||||
def test_pass_through_legacy_message(self):
|
||||
"""parse_legacy_message should pass through already-parsed messages."""
|
||||
req = LegacyRequest(method="test")
|
||||
|
||||
@@ -237,6 +237,33 @@ class TestResultMessage:
|
||||
msg = ResultMessage(id="msg_004", success=True)
|
||||
assert msg.output == {}
|
||||
|
||||
def test_success_result_rejects_error(self):
|
||||
"""ResultMessage success=true should not accept error payload."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ResultMessage(
|
||||
id="msg_005",
|
||||
success=True,
|
||||
error=ErrorPayload(code="bad", message="bad"),
|
||||
)
|
||||
assert "success=true 时 error 必须为空" in str(exc_info.value)
|
||||
|
||||
def test_failed_result_requires_error(self):
|
||||
"""ResultMessage success=false should require error payload."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ResultMessage(id="msg_006", success=False)
|
||||
assert "success=false 时必须提供 error" in str(exc_info.value)
|
||||
|
||||
def test_failed_result_rejects_output(self):
|
||||
"""ResultMessage success=false should not carry success output."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ResultMessage(
|
||||
id="msg_007",
|
||||
success=False,
|
||||
output={"text": "bad"},
|
||||
error=ErrorPayload(code="bad", message="bad"),
|
||||
)
|
||||
assert "success=false 时 output 必须为空" in str(exc_info.value)
|
||||
|
||||
|
||||
class TestInvokeMessage:
|
||||
"""Tests for InvokeMessage model."""
|
||||
@@ -436,6 +463,16 @@ class TestParseMessage:
|
||||
assert isinstance(msg, ResultMessage)
|
||||
assert msg.success is True
|
||||
|
||||
def test_parse_pass_through_model(self):
|
||||
"""parse_message should return already-parsed protocol models unchanged."""
|
||||
original = InvokeMessage(id="msg_008", capability="test.cap")
|
||||
assert parse_message(original) is original
|
||||
|
||||
def test_parse_non_mapping_raises(self):
|
||||
"""parse_message should reject non-object payloads."""
|
||||
with pytest.raises(ValueError, match="JSON object"):
|
||||
parse_message(["not", "an", "object"])
|
||||
|
||||
def test_parse_unknown_type_raises(self):
|
||||
"""parse_message should raise for unknown type."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
|
||||
57
tests_v4/test_protocol_package.py
Normal file
57
tests_v4/test_protocol_package.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Tests for protocol package exports."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from astrbot_sdk.protocol import (
|
||||
CapabilityDescriptor,
|
||||
CommandTrigger,
|
||||
ErrorPayload,
|
||||
EventMessage,
|
||||
HandlerDescriptor,
|
||||
InitializeMessage,
|
||||
LegacyAdapter,
|
||||
LegacyRequest,
|
||||
MessageTrigger,
|
||||
PeerInfo,
|
||||
ProtocolMessage,
|
||||
ResultMessage,
|
||||
ScheduleTrigger,
|
||||
parse_legacy_message,
|
||||
parse_message,
|
||||
)
|
||||
|
||||
|
||||
class TestProtocolPackageExports:
|
||||
"""Ensure protocol package exposes the intended public surface."""
|
||||
|
||||
def test_core_exports_are_importable(self):
|
||||
"""Core protocol models and parsers should be importable from package root."""
|
||||
handler = HandlerDescriptor(
|
||||
id="demo.handler",
|
||||
trigger=CommandTrigger(command="hello"),
|
||||
)
|
||||
message = InitializeMessage(
|
||||
id="msg-1",
|
||||
protocol_version="1.0",
|
||||
peer=PeerInfo(name="plugin", role="plugin"),
|
||||
handlers=[handler],
|
||||
)
|
||||
parsed: ProtocolMessage = parse_message(message)
|
||||
|
||||
assert isinstance(parsed, InitializeMessage)
|
||||
assert isinstance(ErrorPayload(code="x", message="y"), ErrorPayload)
|
||||
assert isinstance(
|
||||
CapabilityDescriptor(name="llm.chat", description="chat"),
|
||||
CapabilityDescriptor,
|
||||
)
|
||||
assert isinstance(MessageTrigger(keywords=["hello"]), MessageTrigger)
|
||||
assert isinstance(ScheduleTrigger(interval_seconds=60), ScheduleTrigger)
|
||||
assert isinstance(EventMessage(id="evt-1", phase="started"), EventMessage)
|
||||
assert isinstance(ResultMessage(id="res-1", success=True), ResultMessage)
|
||||
|
||||
def test_legacy_exports_are_importable(self):
|
||||
"""Legacy adapter helpers should also be available from package root."""
|
||||
legacy = parse_legacy_message({"jsonrpc": "2.0", "method": "handshake"})
|
||||
|
||||
assert isinstance(legacy, LegacyRequest)
|
||||
assert isinstance(LegacyAdapter(), LegacyAdapter)
|
||||
Reference in New Issue
Block a user