feat: 更新文档和代码,优化运行时模块的导出,确保高级原语的稳定性和兼容性

This commit is contained in:
whatevertogo
2026-03-13 06:23:00 +08:00
parent 9c5ecf0a13
commit 4b6e20468b
6 changed files with 94 additions and 185 deletions

View File

@@ -11,6 +11,7 @@
- 2026-03-13: Old Star docs under `docs/zh/dev/star/` describe end-to-end legacy behavior, not just import surfaces. Current compat layer can import `AstrMessageEvent`, `MessageChain`, and some `filter.*` helpers, but handler result consumption is still effectively plain-text only and many documented legacy features remain absent or partial, including command groups, lifecycle/LLM hooks, session waiters, rich media helper constructors, config schema loading, and persona/provider management. Do not treat "type exists" as "old plugin behavior is compatible"; verify the runtime path end to end before declaring parity.
- 2026-03-13: Old Star docs and examples frequently import message components via `astrbot.api.message_components`, not only `astrbot.api.message`. When checking message compatibility, verify the dedicated `api.message_components` import path, legacy constructor aliases like `At(qq=...)` / `Node(uin=..., name=...)`, and helper factories such as `Image.fromURL()` before declaring the message compat surface complete.
- 2026-03-13: `api.message.components.BaseMessageComponent.to_dict()` must emit JSON-ready primitive values, not raw `Enum` members. Leaving `ComponentType` objects in payloads only looks harmless when a later JSON serializer fixes them; it breaks direct mock assertions, in-process capability routing, and any non-JSON send path.
- 2026-03-13: Keep `astrbot_sdk.runtime` root exports narrow. `Peer` / `Transport` / `CapabilityRouter` / `HandlerDispatcher` are reasonable advanced runtime primitives, but loader/bootstrap data structures and orchestration helpers (`LoadedPlugin`, `PluginEnvironmentManager`, `WorkerSession`, `run_supervisor`, etc.) should stay in their submodules instead of becoming accidental root-level stable API.
# 开发命令

View File

@@ -11,6 +11,7 @@
- 2026-03-13: Old Star docs under `docs/zh/dev/star/` describe end-to-end legacy behavior, not just import surfaces. Current compat layer can import `AstrMessageEvent`, `MessageChain`, and some `filter.*` helpers, but handler result consumption is still effectively plain-text only and many documented legacy features remain absent or partial, including command groups, lifecycle/LLM hooks, session waiters, rich media helper constructors, config schema loading, and persona/provider management. Do not treat "type exists" as "old plugin behavior is compatible"; verify the runtime path end to end before declaring parity.
- 2026-03-13: Old Star docs and examples frequently import message components via `astrbot.api.message_components`, not only `astrbot.api.message`. When checking message compatibility, verify the dedicated `api.message_components` import path, legacy constructor aliases like `At(qq=...)` / `Node(uin=..., name=...)`, and helper factories such as `Image.fromURL()` before declaring the message compat surface complete.
- 2026-03-13: `api.message.components.BaseMessageComponent.to_dict()` must emit JSON-ready primitive values, not raw `Enum` members. Leaving `ComponentType` objects in payloads only looks harmless when a later JSON serializer fixes them; it breaks direct mock assertions, in-process capability routing, and any non-JSON send path.
- 2026-03-13: Keep `astrbot_sdk.runtime` root exports narrow. `Peer` / `Transport` / `CapabilityRouter` / `HandlerDispatcher` are reasonable advanced runtime primitives, but loader/bootstrap data structures and orchestration helpers (`LoadedPlugin`, `PluginEnvironmentManager`, `WorkerSession`, `run_supervisor`, etc.) should stay in their submodules instead of becoming accidental root-level stable API.
# 开发命令

View File

@@ -1,155 +1,13 @@
"""运行时模块
"""AstrBot SDK 的高级运行时原语
定义 AstrBot SDK 的运行时架构,包括插件加载、能力路由、处理器分发和通信抽象
架构说明:
旧版:
- 目录结构复杂api/, rpc/, stars/ 等多个子目录
- 使用 JSON-RPC 2.0 协议进行通信
- StarManager 负责插件发现和加载
- StarRunner 负责处理器执行
- Galaxy 负责虚拟星层管理
- 传输层分离为 client/server 两套实现
新版:
- 目录结构精简:仅 6 个核心文件
- 使用自描述协议进行通信
- Peer 统一处理协议层消息收发
- Transport 抽象传输层,支持多种实现
- CapabilityRouter 注册和路由能力调用
- HandlerDispatcher 分发处理器调用
- SupervisorRuntime 管理多 Worker 会话
核心概念对比:
旧版概念:
- StarManager: 插件发现和加载
- StarRunner: 处理器执行
- Galaxy: 虚拟星层管理
- JSONRPCServer/Client: JSON-RPC 通信
- HandshakeHandler: 握手处理
- HandlerExecutor: 处理器执行
新版概念:
- Peer: 协议对等端,统一处理消息
- Transport: 传输层抽象
- CapabilityRouter: 能力路由
- HandlerDispatcher: 处理器分发
- SupervisorRuntime: 多 Worker 管理
- WorkerSession: 单个 Worker 会话
- PluginWorkerRuntime: 插件 Worker 运行时
通信流程对比:
旧版 JSON-RPC 流程:
1. Core -> Plugin: {"method": "handshake", ...}
2. Plugin -> Core: {"result": {"handlers": [...]}}
3. Core -> Plugin: {"method": "call_handler", "params": {...}}
4. Plugin -> Core: {"method": "handler_stream_start", ...}
5. Plugin -> Core: {"method": "handler_stream_update", ...}
6. Plugin -> Core: {"method": "handler_stream_end", ...}
新版协议流程:
1. Plugin -> Core: {"type": "initialize", "handlers": [...], "provided_capabilities": [...]}
2. Core -> Plugin: {"type": "result", "kind": "initialize_result", ...}
3. Core -> Plugin: {"type": "invoke", "capability": "handler.invoke", ...}
4. Plugin -> Core: {"type": "event", "phase": "started"}
5. Plugin -> Core: {"type": "event", "phase": "delta", "data": {...}}
6. Plugin -> Core: {"type": "event", "phase": "completed", "output": {...}}
插件加载对比:
旧版 StarManager:
- 通过 plugin.yaml 发现插件
- 动态导入组件类并实例化
- 注册到 star_handlers_registry
- 使用 functools.partial 绑定实例
新版 loader.py:
- PluginSpec 描述插件规范
- PluginEnvironmentManager 管理虚拟环境
- load_plugin() 加载并解析组件
- LoadedHandler 封装处理器和描述符
- 支持新旧 Star 组件兼容
传输层对比:
旧版传输层:
- 分离的 client/ 和 server/ 目录
- JSONRPCClient 基类 + StdioClient/WebSocketClient
- JSONRPCServer 基类 + StdioServer/WebSocketServer
- 通过 set_message_handler 设置回调
新版传输层:
- 统一的 Transport 抽象基类
- StdioTransport: 支持进程模式和文件模式
- WebSocketServerTransport: WebSocket 服务端
- WebSocketClientTransport: WebSocket 客户端
- 通过 set_message_handler 设置回调
处理器执行对比:
旧版 HandlerExecutor:
- 从 star_handlers_registry 获取处理器
- 调用 handler(event, **args)
- 通过 JSON-RPC notification 发送流式结果
- 无参数注入支持
新版 HandlerDispatcher:
- 从 LoadedHandler 映射获取处理器
- 支持类型注解注入 (MessageEvent, Context)
- 支持参数名注入 (event, ctx, context)
- 支持 legacy_args 注入 (命令参数等)
- 支持 Optional[Type] 类型
- 统一的错误处理和生命周期回调
能力系统对比:
旧版:
- 无显式的能力声明系统
- 通过 call_context_function 调用核心功能
- 上下文函数硬编码在核心侧
新版 CapabilityRouter:
- CapabilityDescriptor 声明能力
- JSON Schema 验证输入输出
- 支持流式能力 (stream_handler)
- 内置能力llm.chat, memory.*, db.*, platform.*
- 支持 Supervisor 聚合并转发插件自定义 capability
`runtime` 负责把协议、传输、插件加载和生命周期管理拼成一条完整执行链:
- `Transport`: 只负责字符串级别收发
- `Peer`: 负责协议消息、请求关联、流式事件和取消
- `CapabilityRouter`: 核心侧能力注册与路由
- `HandlerDispatcher`: 插件侧 handler 调用适配
- `loader` / `bootstrap`: 插件发现、Worker 启动和 Supervisor 编排
设计上legacy 兼容只出现在加载与分发边界;`Transport` 和 `Peer` 不直接携带
旧版业务语义。
这里仅暴露相对稳定的运行时构件:协议 `Peer`、传输抽象以及能力/处理器分发器
大多数插件作者应优先使用顶层 `astrbot_sdk` 或 `astrbot_sdk.api`。
`loader` / `bootstrap` 等编排细节保留在各自子模块中,不作为根级稳定契约。
"""
from .bootstrap import (
PluginWorkerRuntime,
SupervisorRuntime,
WorkerSession,
run_plugin_worker,
run_supervisor,
run_websocket_server,
)
from .capability_router import CapabilityRouter, StreamExecution
from .handler_dispatcher import HandlerDispatcher
from .loader import (
LoadedCapability,
LoadedHandler,
LoadedPlugin,
PluginDiscoveryResult,
PluginEnvironmentManager,
PluginSpec,
discover_plugins,
load_plugin,
load_plugin_spec,
)
from .peer import (
CancelHandler,
InitializeHandler,
InvokeHandler,
Peer,
)
from .peer import Peer
from .transport import (
MessageHandler,
StdioTransport,
@@ -159,31 +17,13 @@ from .transport import (
)
__all__ = [
"CancelHandler",
"CapabilityRouter",
"HandlerDispatcher",
"InitializeHandler",
"InvokeHandler",
"LoadedCapability",
"LoadedHandler",
"LoadedPlugin",
"MessageHandler",
"Peer",
"PluginDiscoveryResult",
"PluginEnvironmentManager",
"PluginSpec",
"PluginWorkerRuntime",
"StdioTransport",
"StreamExecution",
"SupervisorRuntime",
"Transport",
"WebSocketClientTransport",
"WebSocketServerTransport",
"WorkerSession",
"discover_plugins",
"load_plugin",
"load_plugin_spec",
"run_plugin_worker",
"run_supervisor",
"run_websocket_server",
]

View File

@@ -10,6 +10,7 @@
from astrbot_sdk.api.components.command import CommandComponent
from astrbot_sdk.api.event import AstrMessageEvent, filter
from astrbot_sdk.api.event.filter import EventMessageType, PlatformAdapterType
from astrbot_sdk.api.star.context import Context
from astrbot_sdk.api.message import MessageChain
from astrbot_sdk.api.message_components import Plain, At, Image
@@ -77,15 +78,16 @@ class HelloCommand(CommandComponent):
at = At(user_id="123", user_name="test_user")
# 测试 Image
img = Image.fromURL("https://example.com/img.png")
image = Image.fromURL("https://example.com/img.png")
# 测试 to_dict
plain_dict = plain.to_dict()
at_dict = at.to_dict()
image_dict = image.to_dict()
logger.info(f"Plain: {plain_dict}, At: {at_dict}")
logger.info(f"Plain: {plain_dict}, At: {at_dict}, Image: {image_dict}")
yield event.plain_result(f"Components created: Plain, At, Image")
yield event.plain_result("Components created: Plain, At, Image")
@filter.regex(r"^ping.*")
async def ping_regex(self, event: AstrMessageEvent):
@@ -97,12 +99,12 @@ class HelloCommand(CommandComponent):
"""测试权限过滤 (应该被跳过,因为没有 require_admin)。"""
yield event.plain_result("Admin command executed")
@filter.event_message_type(filter.EventMessageType.GROUP_MESSAGE)
@filter.event_message_type(EventMessageType.GROUP_MESSAGE)
async def group_only(self, event: AstrMessageEvent):
"""测试消息类型过滤。"""
yield event.plain_result("Group message received")
@filter.platform_adapter_type("aiocqhttp")
@filter.platform_adapter_type(PlatformAdapterType.AIOCQHTTP)
async def cqhttp_only(self, event: AstrMessageEvent):
"""测试平台过滤。"""
yield event.plain_result("CQHttp platform detected")

View File

@@ -9,14 +9,12 @@ import sys
import tempfile
import textwrap
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import yaml
from astrbot_sdk.protocol.descriptors import CommandTrigger, MessageTrigger
from astrbot_sdk.runtime.loader import (
LoadedPlugin,
load_plugin,
load_plugin_spec,
)
@@ -186,8 +184,9 @@ class TestFilterDecorators:
def test_event_message_type_decorator(self):
"""测试 event_message_type 装饰器。"""
from astrbot_sdk.api.event import filter
from astrbot_sdk.api.event.filter import EventMessageType
@filter.event_message_type(filter.EventMessageType.GROUP_MESSAGE)
@filter.event_message_type(EventMessageType.GROUP_MESSAGE)
@filter.command("group_cmd")
async def handler(event):
pass
@@ -323,11 +322,12 @@ class TestLoadLegacyStylePlugin:
manifest_path = plugin_dir / "plugin.yaml"
requirements_path = plugin_dir / "requirements.txt"
commands_dir = plugin_dir / "commands"
commands_dir.mkdir()
(commands_dir / "__init__.py").write_text("", encoding="utf-8")
# 使用唯一的模块名避免与其他测试冲突
handlers_dir = plugin_dir / "chain_handlers"
handlers_dir.mkdir()
(handlers_dir / "__init__.py").write_text("", encoding="utf-8")
(commands_dir / "chain.py").write_text(
(handlers_dir / "chain_cmd.py").write_text(
textwrap.dedent("""
from astrbot_sdk.api.components.command import CommandComponent
from astrbot_sdk.api.event import AstrMessageEvent, filter
@@ -355,7 +355,7 @@ class TestLoadLegacyStylePlugin:
"runtime": {"python": "3.12"},
"components": [
{
"class": "commands.chain:ChainCommand",
"class": "chain_handlers.chain_cmd:ChainCommand",
"type": "command",
"name": "chain",
}
@@ -368,8 +368,10 @@ class TestLoadLegacyStylePlugin:
spec = load_plugin_spec(plugin_dir)
path_added = False
if str(plugin_dir) not in sys.path:
sys.path.insert(0, str(plugin_dir))
path_added = True
try:
loaded = load_plugin(spec)
@@ -377,22 +379,30 @@ class TestLoadLegacyStylePlugin:
assert len(loaded.instances) == 1
assert len(loaded.handlers) >= 1
finally:
if str(plugin_dir) in sys.path:
# 清理导入的模块
modules_to_remove = [
k for k in list(sys.modules.keys()) if k.startswith("chain_handlers")
]
for mod in modules_to_remove:
del sys.modules[mod]
if path_added and str(plugin_dir) in sys.path:
sys.path.remove(str(plugin_dir))
def test_load_plugin_with_regex_handler(self):
"""测试加载使用正则处理器的插件。"""
with tempfile.TemporaryDirectory() as temp_dir:
plugin_dir = Path(temp_dir) / "regex_plugin"
plugin_dir.mkdir()
manifest_path = plugin_dir / "plugin.yaml"
requirements_path = plugin_dir / "requirements.txt"
commands_dir = plugin_dir / "commands"
commands_dir.mkdir()
(commands_dir / "__init__.py").write_text("", encoding="utf-8")
# 使用唯一的模块名避免与其他测试冲突
regex_handlers_dir = plugin_dir / "regex_handlers"
regex_handlers_dir.mkdir()
(regex_handlers_dir / "__init__.py").write_text("", encoding="utf-8")
(commands_dir / "matchers.py").write_text(
(regex_handlers_dir / "matcher.py").write_text(
textwrap.dedent("""
from astrbot_sdk.api.components.command import CommandComponent
from astrbot_sdk.api.event import AstrMessageEvent, filter
@@ -416,7 +426,7 @@ class TestLoadLegacyStylePlugin:
"runtime": {"python": "3.12"},
"components": [
{
"class": "commands.matchers:RegexCommand",
"class": "regex_handlers.matcher:RegexCommand",
"type": "command",
"name": "regex",
}
@@ -429,8 +439,10 @@ class TestLoadLegacyStylePlugin:
spec = load_plugin_spec(plugin_dir)
path_added = False
if str(plugin_dir) not in sys.path:
sys.path.insert(0, str(plugin_dir))
path_added = True
try:
loaded = load_plugin(spec)
@@ -440,7 +452,13 @@ class TestLoadLegacyStylePlugin:
assert isinstance(handler.descriptor.trigger, MessageTrigger)
assert handler.descriptor.trigger.regex == r"^ping.*"
finally:
if str(plugin_dir) in sys.path:
# 清理导入的模块
modules_to_remove = [
k for k in sys.modules if k.startswith("regex_handlers")
]
for mod in modules_to_remove:
del sys.modules[mod]
if path_added and str(plugin_dir) in sys.path:
sys.path.remove(str(plugin_dir))

View File

@@ -12,6 +12,7 @@ from unittest.mock import AsyncMock, Mock, patch
import astrbot_sdk
import astrbot_sdk.compat as compat_module
import astrbot_sdk.runtime as runtime_module
import pytest
from click.testing import CliRunner
@@ -25,6 +26,16 @@ from astrbot_sdk.context import Context
from astrbot_sdk.decorators import on_command
from astrbot_sdk.errors import AstrBotError, ErrorCodes
from astrbot_sdk.events import MessageEvent
from astrbot_sdk.runtime.capability_router import CapabilityRouter, StreamExecution
from astrbot_sdk.runtime.handler_dispatcher import HandlerDispatcher
from astrbot_sdk.runtime.peer import Peer
from astrbot_sdk.runtime.transport import (
MessageHandler,
StdioTransport,
Transport,
WebSocketClientTransport,
WebSocketServerTransport,
)
from astrbot_sdk.star import Star
TOP_LEVEL_MODULES = [
@@ -36,6 +47,7 @@ TOP_LEVEL_MODULES = [
"astrbot_sdk.decorators",
"astrbot_sdk.errors",
"astrbot_sdk.events",
"astrbot_sdk.runtime",
"astrbot_sdk.star",
]
@@ -89,6 +101,41 @@ class TestTopLevelImports:
"LegacyConversationManager",
]
def test_runtime_module_reexports_advanced_runtime_primitives(self):
"""runtime module should expose only the small advanced runtime surface."""
assert runtime_module.Peer is Peer
assert runtime_module.CapabilityRouter is CapabilityRouter
assert runtime_module.HandlerDispatcher is HandlerDispatcher
assert runtime_module.Transport is Transport
assert runtime_module.MessageHandler is MessageHandler
assert runtime_module.StdioTransport is StdioTransport
assert runtime_module.WebSocketClientTransport is WebSocketClientTransport
assert runtime_module.WebSocketServerTransport is WebSocketServerTransport
assert runtime_module.StreamExecution is StreamExecution
def test_runtime_module_does_not_reexport_loader_or_bootstrap_details(self):
"""runtime root should not expose loader/bootstrap internals as stable API."""
assert not hasattr(runtime_module, "PluginEnvironmentManager")
assert not hasattr(runtime_module, "PluginWorkerRuntime")
assert not hasattr(runtime_module, "SupervisorRuntime")
assert not hasattr(runtime_module, "WorkerSession")
assert not hasattr(runtime_module, "LoadedPlugin")
assert not hasattr(runtime_module, "run_supervisor")
def test_runtime_module_all_matches_narrow_public_surface(self):
"""runtime.__all__ should stay aligned with the narrowed advanced API."""
assert runtime_module.__all__ == [
"CapabilityRouter",
"HandlerDispatcher",
"MessageHandler",
"Peer",
"StdioTransport",
"StreamExecution",
"Transport",
"WebSocketClientTransport",
"WebSocketServerTransport",
]
class TestCliModule:
"""Tests for cli.py and __main__.py."""