Refactor plugin runtime components and enhance message handling
- Removed unused core action mirror functionality from PluginRunnerSupervisor. - Simplified action and command execution logic in send_service.py. - Introduced ComponentQueryService for unified component querying in plugin runtime. - Enhanced message component handling with new binary component support. - Improved message sequence construction and detection of outbound message flags. - Updated methods for sending messages to streamline the process and improve readability.
This commit is contained in:
@@ -72,10 +72,10 @@ class _StubPlatformIODriver(PlatformIODriver):
|
||||
|
||||
|
||||
def _build_manager() -> PlatformIOManager:
|
||||
"""构造带有最小 active owner 的 Broker 管理器。
|
||||
"""构造带有最小接收路由的 Broker 管理器。
|
||||
|
||||
Returns:
|
||||
PlatformIOManager: 已注册测试驱动并绑定活动路由的 Broker。
|
||||
PlatformIOManager: 已注册测试驱动并绑定接收路由的 Broker。
|
||||
"""
|
||||
manager = PlatformIOManager()
|
||||
driver = _StubPlatformIODriver(
|
||||
@@ -88,7 +88,7 @@ def _build_manager() -> PlatformIOManager:
|
||||
)
|
||||
)
|
||||
manager.register_driver(driver)
|
||||
manager.bind_route(
|
||||
manager.bind_receive_route(
|
||||
RouteBinding(
|
||||
route_key=RouteKey(platform="qq", account_id="10001", scope="main"),
|
||||
driver_id=driver.driver_id,
|
||||
|
||||
@@ -1,57 +1,109 @@
|
||||
"""核心组件查询层与插件运行时聚合测试。"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.component_registry import component_registry as core_component_registry
|
||||
import src.plugin_runtime.integration as integration_module
|
||||
|
||||
from src.core.types import ActionInfo, ToolInfo
|
||||
from src.plugin_runtime.component_query import component_query_service
|
||||
from src.plugin_runtime.host.supervisor import PluginSupervisor
|
||||
from src.plugin_runtime.protocol.envelope import ComponentDeclaration, RegisterPluginPayload
|
||||
|
||||
|
||||
def _build_action_payload(plugin_id: str, action_name: str) -> RegisterPluginPayload:
|
||||
"""构造用于测试的 runtime Action 注册载荷。
|
||||
class _FakeRuntimeManager:
|
||||
"""测试用插件运行时管理器。"""
|
||||
|
||||
def __init__(self, supervisor: PluginSupervisor, plugin_id: str, plugin_config: dict[str, Any]) -> None:
|
||||
"""初始化测试用运行时管理器。
|
||||
|
||||
Args:
|
||||
supervisor: 持有测试组件的监督器。
|
||||
plugin_id: 目标插件 ID。
|
||||
plugin_config: 需要返回的插件配置。
|
||||
"""
|
||||
|
||||
self.supervisors = [supervisor]
|
||||
self._plugin_id = plugin_id
|
||||
self._plugin_config = plugin_config
|
||||
|
||||
def _get_supervisor_for_plugin(self, plugin_id: str) -> PluginSupervisor | None:
|
||||
"""按插件 ID 返回对应监督器。
|
||||
|
||||
Args:
|
||||
plugin_id: 目标插件 ID。
|
||||
|
||||
Returns:
|
||||
PluginSupervisor | None: 命中时返回监督器。
|
||||
"""
|
||||
|
||||
return self.supervisors[0] if plugin_id == self._plugin_id else None
|
||||
|
||||
def _load_plugin_config_for_supervisor(self, supervisor: Any, plugin_id: str) -> dict[str, Any]:
|
||||
"""返回测试配置。
|
||||
|
||||
Args:
|
||||
supervisor: 监督器实例。
|
||||
plugin_id: 目标插件 ID。
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: 测试配置内容。
|
||||
"""
|
||||
|
||||
del supervisor
|
||||
if plugin_id != self._plugin_id:
|
||||
return {}
|
||||
return dict(self._plugin_config)
|
||||
|
||||
|
||||
def _install_runtime_manager(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
supervisor: PluginSupervisor,
|
||||
plugin_id: str,
|
||||
plugin_config: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""为测试安装假的运行时管理器。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
action_name: Action 名称。
|
||||
|
||||
Returns:
|
||||
RegisterPluginPayload: 测试用注册载荷。
|
||||
monkeypatch: pytest monkeypatch 对象。
|
||||
supervisor: 持有测试组件的监督器。
|
||||
plugin_id: 测试插件 ID。
|
||||
plugin_config: 可选的测试配置内容。
|
||||
"""
|
||||
return RegisterPluginPayload(
|
||||
plugin_id=plugin_id,
|
||||
plugin_version="1.0.0",
|
||||
components=[
|
||||
ComponentDeclaration(
|
||||
name=action_name,
|
||||
component_type="ACTION",
|
||||
plugin_id=plugin_id,
|
||||
metadata={
|
||||
"description": "发送一个测试回复",
|
||||
"enabled": True,
|
||||
"activation_type": "keyword",
|
||||
"activation_probability": 0.25,
|
||||
"activation_keywords": ["测试", "hello"],
|
||||
"action_parameters": {"target": "目标对象"},
|
||||
"action_require": ["需要发送回复时使用"],
|
||||
"associated_types": ["text"],
|
||||
"parallel_action": True,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
fake_manager = _FakeRuntimeManager(supervisor, plugin_id, plugin_config or {"enabled": True})
|
||||
monkeypatch.setattr(integration_module, "get_plugin_runtime_manager", lambda: fake_manager)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_actions_are_mirrored_into_core_registry_and_invoked(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""运行时 Action 应镜像到旧核心注册表,并可由旧 Planner 执行。"""
|
||||
async def test_core_component_registry_reads_runtime_action_and_executor(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""核心查询层应直接读取运行时 Action,并返回 RPC 执行闭包。"""
|
||||
|
||||
plugin_id = "runtime_action_bridge_plugin"
|
||||
action_name = "runtime_action_bridge_test"
|
||||
payload = _build_action_payload(plugin_id=plugin_id, action_name=action_name)
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
core_component_registry.remove_action(action_name)
|
||||
supervisor.component_registry.register_component(
|
||||
name=action_name,
|
||||
component_type="ACTION",
|
||||
plugin_id=plugin_id,
|
||||
metadata={
|
||||
"description": "发送一个测试回复",
|
||||
"enabled": True,
|
||||
"activation_type": "keyword",
|
||||
"activation_probability": 0.25,
|
||||
"activation_keywords": ["测试", "hello"],
|
||||
"action_parameters": {"target": "目标对象"},
|
||||
"action_require": ["需要发送回复时使用"],
|
||||
"associated_types": ["text"],
|
||||
"parallel_action": True,
|
||||
},
|
||||
)
|
||||
_install_runtime_manager(monkeypatch, supervisor, plugin_id, {"enabled": True, "mode": "test"})
|
||||
|
||||
async def fake_invoke_plugin(
|
||||
method: str,
|
||||
@@ -60,18 +112,8 @@ async def test_runtime_actions_are_mirrored_into_core_registry_and_invoked(monke
|
||||
args: dict[str, Any] | None = None,
|
||||
timeout_ms: int = 30000,
|
||||
) -> Any:
|
||||
"""模拟 plugin runtime Action 调用。
|
||||
"""模拟动作 RPC 调用。"""
|
||||
|
||||
Args:
|
||||
method: RPC 方法名。
|
||||
plugin_id: 插件 ID。
|
||||
component_name: 组件名称。
|
||||
args: 调用参数。
|
||||
timeout_ms: RPC 超时时间。
|
||||
|
||||
Returns:
|
||||
Any: 伪造的 RPC 响应对象。
|
||||
"""
|
||||
captured["method"] = method
|
||||
captured["plugin_id"] = plugin_id
|
||||
captured["component_name"] = component_name
|
||||
@@ -81,58 +123,162 @@ async def test_runtime_actions_are_mirrored_into_core_registry_and_invoked(monke
|
||||
|
||||
monkeypatch.setattr(supervisor, "invoke_plugin", fake_invoke_plugin)
|
||||
|
||||
try:
|
||||
supervisor._mirror_runtime_actions_to_core_registry(payload)
|
||||
action_info = component_query_service.get_action_info(action_name)
|
||||
assert isinstance(action_info, ActionInfo)
|
||||
assert action_info.plugin_name == plugin_id
|
||||
assert action_info.description == "发送一个测试回复"
|
||||
assert action_info.activation_keywords == ["测试", "hello"]
|
||||
assert action_info.random_activation_probability == 0.25
|
||||
assert action_info.parallel_action is True
|
||||
assert action_name in component_query_service.get_default_actions()
|
||||
assert component_query_service.get_plugin_config(plugin_id) == {"enabled": True, "mode": "test"}
|
||||
|
||||
action_info = core_component_registry.get_action_info(action_name)
|
||||
assert action_info is not None
|
||||
assert action_info.plugin_name == plugin_id
|
||||
assert action_info.description == "发送一个测试回复"
|
||||
assert action_info.activation_keywords == ["测试", "hello"]
|
||||
assert action_info.random_activation_probability == 0.25
|
||||
assert action_info.parallel_action is True
|
||||
executor = component_query_service.get_action_executor(action_name)
|
||||
assert executor is not None
|
||||
|
||||
executor = core_component_registry.get_action_executor(action_name)
|
||||
assert executor is not None
|
||||
success, reason = await executor(
|
||||
action_data={"target": "MaiBot"},
|
||||
action_reasoning="当前适合使用这个动作",
|
||||
cycle_timers={"planner": 0.1},
|
||||
thinking_id="tid-1",
|
||||
chat_stream=SimpleNamespace(session_id="stream-1"),
|
||||
log_prefix="[test]",
|
||||
shutting_down=False,
|
||||
plugin_config={"enabled": True},
|
||||
)
|
||||
|
||||
success, reason = await executor(
|
||||
action_data={"target": "MaiBot"},
|
||||
action_reasoning="当前适合使用这个动作",
|
||||
cycle_timers={"planner": 0.1},
|
||||
thinking_id="tid-1",
|
||||
chat_stream=SimpleNamespace(session_id="stream-1"),
|
||||
log_prefix="[test]",
|
||||
shutting_down=False,
|
||||
plugin_config={"enabled": True},
|
||||
)
|
||||
|
||||
assert success is True
|
||||
assert reason == "runtime action executed"
|
||||
assert captured["method"] == "plugin.invoke_action"
|
||||
assert captured["plugin_id"] == plugin_id
|
||||
assert captured["component_name"] == action_name
|
||||
assert captured["args"]["stream_id"] == "stream-1"
|
||||
assert captured["args"]["chat_id"] == "stream-1"
|
||||
assert captured["args"]["reasoning"] == "当前适合使用这个动作"
|
||||
assert captured["args"]["target"] == "MaiBot"
|
||||
assert captured["args"]["action_data"] == {"target": "MaiBot"}
|
||||
finally:
|
||||
supervisor._remove_core_action_mirrors(plugin_id)
|
||||
core_component_registry.remove_action(action_name)
|
||||
assert success is True
|
||||
assert reason == "runtime action executed"
|
||||
assert captured["method"] == "plugin.invoke_action"
|
||||
assert captured["plugin_id"] == plugin_id
|
||||
assert captured["component_name"] == action_name
|
||||
assert captured["args"]["stream_id"] == "stream-1"
|
||||
assert captured["args"]["chat_id"] == "stream-1"
|
||||
assert captured["args"]["reasoning"] == "当前适合使用这个动作"
|
||||
assert captured["args"]["target"] == "MaiBot"
|
||||
assert captured["args"]["action_data"] == {"target": "MaiBot"}
|
||||
|
||||
|
||||
def test_clear_runner_state_removes_mirrored_runtime_actions() -> None:
|
||||
"""清理 Runner 状态时应同步移除旧核心注册表中的镜像 Action。"""
|
||||
plugin_id = "runtime_action_bridge_cleanup_plugin"
|
||||
action_name = "runtime_action_bridge_cleanup_test"
|
||||
payload = _build_action_payload(plugin_id=plugin_id, action_name=action_name)
|
||||
@pytest.mark.asyncio
|
||||
async def test_core_component_registry_reads_runtime_command_and_executor(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""核心查询层应直接使用运行时命令匹配与执行闭包。"""
|
||||
|
||||
plugin_id = "runtime_command_bridge_plugin"
|
||||
command_name = "runtime_command_bridge_test"
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
supervisor.component_registry.register_component(
|
||||
name=command_name,
|
||||
component_type="COMMAND",
|
||||
plugin_id=plugin_id,
|
||||
metadata={
|
||||
"description": "测试命令",
|
||||
"enabled": True,
|
||||
"command_pattern": r"^/test(?:\s+.+)?$",
|
||||
"aliases": ["/hello"],
|
||||
"intercept_message_level": 1,
|
||||
},
|
||||
)
|
||||
_install_runtime_manager(monkeypatch, supervisor, plugin_id, {"mode": "command"})
|
||||
|
||||
async def fake_invoke_plugin(
|
||||
method: str,
|
||||
plugin_id: str,
|
||||
component_name: str,
|
||||
args: dict[str, Any] | None = None,
|
||||
timeout_ms: int = 30000,
|
||||
) -> Any:
|
||||
"""模拟命令 RPC 调用。"""
|
||||
|
||||
captured["method"] = method
|
||||
captured["plugin_id"] = plugin_id
|
||||
captured["component_name"] = component_name
|
||||
captured["args"] = args or {}
|
||||
captured["timeout_ms"] = timeout_ms
|
||||
return SimpleNamespace(payload={"success": True, "result": (True, "command ok", True)})
|
||||
|
||||
monkeypatch.setattr(supervisor, "invoke_plugin", fake_invoke_plugin)
|
||||
|
||||
matched = component_query_service.find_command_by_text("/test hello")
|
||||
assert matched is not None
|
||||
command_executor, matched_groups, command_info = matched
|
||||
|
||||
assert matched_groups == {}
|
||||
assert command_info.plugin_name == plugin_id
|
||||
assert command_info.command_pattern == r"^/test(?:\s+.+)?$"
|
||||
|
||||
success, response_text, intercept = await command_executor(
|
||||
message=SimpleNamespace(processed_plain_text="/test hello", session_id="stream-2"),
|
||||
plugin_config={"mode": "command"},
|
||||
matched_groups=matched_groups,
|
||||
)
|
||||
|
||||
assert success is True
|
||||
assert response_text == "command ok"
|
||||
assert intercept is True
|
||||
assert captured["method"] == "plugin.invoke_command"
|
||||
assert captured["plugin_id"] == plugin_id
|
||||
assert captured["component_name"] == command_name
|
||||
assert captured["args"]["text"] == "/test hello"
|
||||
assert captured["args"]["stream_id"] == "stream-2"
|
||||
assert captured["args"]["plugin_config"] == {"mode": "command"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_core_component_registry_reads_runtime_tools_and_executor(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""核心查询层应直接读取运行时 Tool,并返回 RPC 执行闭包。"""
|
||||
|
||||
plugin_id = "runtime_tool_bridge_plugin"
|
||||
tool_name = "runtime_tool_bridge_test"
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
|
||||
core_component_registry.remove_action(action_name)
|
||||
supervisor.component_registry.register_component(
|
||||
name=tool_name,
|
||||
component_type="TOOL",
|
||||
plugin_id=plugin_id,
|
||||
metadata={
|
||||
"description": "测试工具",
|
||||
"enabled": True,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "query",
|
||||
"param_type": "string",
|
||||
"description": "查询词",
|
||||
"required": True,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
_install_runtime_manager(monkeypatch, supervisor, plugin_id)
|
||||
|
||||
supervisor._mirror_runtime_actions_to_core_registry(payload)
|
||||
assert core_component_registry.get_action_info(action_name) is not None
|
||||
async def fake_invoke_plugin(
|
||||
method: str,
|
||||
plugin_id: str,
|
||||
component_name: str,
|
||||
args: dict[str, Any] | None = None,
|
||||
timeout_ms: int = 30000,
|
||||
) -> Any:
|
||||
"""模拟工具 RPC 调用。"""
|
||||
|
||||
supervisor._clear_runner_state()
|
||||
del timeout_ms
|
||||
assert method == "plugin.invoke_tool"
|
||||
assert plugin_id == "runtime_tool_bridge_plugin"
|
||||
assert component_name == "runtime_tool_bridge_test"
|
||||
assert args == {"query": "MaiBot"}
|
||||
return SimpleNamespace(payload={"success": True, "result": {"content": "tool ok"}})
|
||||
|
||||
assert core_component_registry.get_action_info(action_name) is None
|
||||
monkeypatch.setattr(supervisor, "invoke_plugin", fake_invoke_plugin)
|
||||
|
||||
tool_info = component_query_service.get_tool_info(tool_name)
|
||||
assert isinstance(tool_info, ToolInfo)
|
||||
assert tool_info.tool_description == "测试工具"
|
||||
assert tool_name in component_query_service.get_llm_available_tools()
|
||||
|
||||
executor = component_query_service.get_tool_executor(tool_name)
|
||||
assert executor is not None
|
||||
assert await executor({"query": "MaiBot"}) == {"content": "tool ok"}
|
||||
|
||||
Reference in New Issue
Block a user