refactor: update message gateway handling and remove adapter references
- Changed the message sending method to return DeliveryBatch instead of DeliveryReceipt in integration.py. - Removed AdapterDeclarationPayload and related references from envelope.py, replacing them with MessageGatewayStateUpdatePayload and MessageGatewayStateUpdateResultPayload. - Updated runner_main.py to remove adapter-related logic and methods, focusing on message gateway functionality. - Added tests for message gateway runtime state synchronization and action bridge functionality in test files.
This commit is contained in:
@@ -1,162 +0,0 @@
|
||||
"""适配器运行时状态同步测试。"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
|
||||
from src.platform_io.manager import PlatformIOManager
|
||||
from src.platform_io.types import RouteKey
|
||||
from src.plugin_runtime.host.supervisor import PluginSupervisor
|
||||
from src.plugin_runtime.protocol.envelope import (
|
||||
AdapterDeclarationPayload,
|
||||
Envelope,
|
||||
MessageType,
|
||||
)
|
||||
|
||||
|
||||
def _make_request(plugin_id: str, payload: Dict[str, Any]) -> Envelope:
|
||||
"""构造一个适配器状态更新 RPC 请求。
|
||||
|
||||
Args:
|
||||
plugin_id: 目标适配器插件 ID。
|
||||
payload: 请求载荷。
|
||||
|
||||
Returns:
|
||||
Envelope: 标准 RPC 请求信封。
|
||||
"""
|
||||
return Envelope(
|
||||
request_id=1,
|
||||
message_type=MessageType.REQUEST,
|
||||
method="host.update_adapter_state",
|
||||
plugin_id=plugin_id,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_adapter_runtime_state_binds_and_unbinds_routes(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""连接建立后应绑定路由,断开后应撤销路由。"""
|
||||
import src.plugin_runtime.host.supervisor as supervisor_module
|
||||
|
||||
platform_io_manager = PlatformIOManager()
|
||||
monkeypatch.setattr(supervisor_module, "get_platform_io_manager", lambda: platform_io_manager)
|
||||
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
adapter = AdapterDeclarationPayload(platform="qq", protocol="napcat")
|
||||
await supervisor._register_adapter_driver("napcat_adapter_builtin", adapter)
|
||||
|
||||
response = await supervisor._handle_update_adapter_state(
|
||||
_make_request(
|
||||
"napcat_adapter_builtin",
|
||||
{
|
||||
"connected": True,
|
||||
"account_id": "10001",
|
||||
"scope": "",
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.error is None
|
||||
assert response.payload["accepted"] is True
|
||||
assert (
|
||||
platform_io_manager.route_table.get_active_binding(
|
||||
RouteKey(platform="qq", account_id="10001"),
|
||||
exact_only=True,
|
||||
).driver_id
|
||||
== "adapter:napcat_adapter_builtin"
|
||||
)
|
||||
assert (
|
||||
platform_io_manager.route_table.get_active_binding(
|
||||
RouteKey(platform="qq"),
|
||||
exact_only=True,
|
||||
).driver_id
|
||||
== "adapter:napcat_adapter_builtin"
|
||||
)
|
||||
|
||||
response = await supervisor._handle_update_adapter_state(
|
||||
_make_request(
|
||||
"napcat_adapter_builtin",
|
||||
{
|
||||
"connected": False,
|
||||
"account_id": "",
|
||||
"scope": "",
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.error is None
|
||||
assert response.payload["accepted"] is True
|
||||
assert platform_io_manager.route_table.get_active_binding(
|
||||
RouteKey(platform="qq", account_id="10001"),
|
||||
exact_only=True,
|
||||
) is None
|
||||
assert platform_io_manager.route_table.get_active_binding(RouteKey(platform="qq"), exact_only=True) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_platform_default_route_is_removed_when_multiple_exact_routes_exist(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""同一平台存在多个精确路由时不应保留默认平台路由。"""
|
||||
import src.plugin_runtime.host.supervisor as supervisor_module
|
||||
|
||||
platform_io_manager = PlatformIOManager()
|
||||
monkeypatch.setattr(supervisor_module, "get_platform_io_manager", lambda: platform_io_manager)
|
||||
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
adapter = AdapterDeclarationPayload(platform="qq", protocol="napcat")
|
||||
await supervisor._register_adapter_driver("adapter_a", adapter)
|
||||
await supervisor._register_adapter_driver("adapter_b", adapter)
|
||||
|
||||
await supervisor._handle_update_adapter_state(
|
||||
_make_request(
|
||||
"adapter_a",
|
||||
{
|
||||
"connected": True,
|
||||
"account_id": "10001",
|
||||
"scope": "",
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
assert (
|
||||
platform_io_manager.route_table.get_active_binding(
|
||||
RouteKey(platform="qq"),
|
||||
exact_only=True,
|
||||
).driver_id
|
||||
== "adapter:adapter_a"
|
||||
)
|
||||
|
||||
await supervisor._handle_update_adapter_state(
|
||||
_make_request(
|
||||
"adapter_b",
|
||||
{
|
||||
"connected": True,
|
||||
"account_id": "10002",
|
||||
"scope": "",
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
assert platform_io_manager.route_table.get_active_binding(RouteKey(platform="qq"), exact_only=True) is None
|
||||
|
||||
await supervisor._handle_update_adapter_state(
|
||||
_make_request(
|
||||
"adapter_b",
|
||||
{
|
||||
"connected": False,
|
||||
"account_id": "",
|
||||
"scope": "",
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
assert (
|
||||
platform_io_manager.route_table.get_active_binding(
|
||||
RouteKey(platform="qq"),
|
||||
exact_only=True,
|
||||
).driver_id
|
||||
== "adapter:adapter_a"
|
||||
)
|
||||
170
pytests/test_message_gateway_runtime.py
Normal file
170
pytests/test_message_gateway_runtime.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""消息网关运行时状态同步测试。"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
|
||||
from src.platform_io.manager import PlatformIOManager
|
||||
from src.platform_io.types import RouteKey
|
||||
from src.plugin_runtime.host.supervisor import PluginSupervisor
|
||||
from src.plugin_runtime.protocol.envelope import Envelope, MessageType
|
||||
|
||||
|
||||
def _make_request(method: str, plugin_id: str, payload: Dict[str, Any]) -> Envelope:
|
||||
"""构造一个 RPC 请求信封。
|
||||
|
||||
Args:
|
||||
method: RPC 方法名。
|
||||
plugin_id: 目标插件 ID。
|
||||
payload: 请求载荷。
|
||||
|
||||
Returns:
|
||||
Envelope: 标准 RPC 请求信封。
|
||||
"""
|
||||
|
||||
return Envelope(
|
||||
request_id=1,
|
||||
message_type=MessageType.REQUEST,
|
||||
method=method,
|
||||
plugin_id=plugin_id,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_message_gateway_runtime_state_binds_send_and_receive_routes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""消息网关就绪后应同时绑定发送表和接收表。"""
|
||||
|
||||
import src.plugin_runtime.host.supervisor as supervisor_module
|
||||
|
||||
platform_io_manager = PlatformIOManager()
|
||||
monkeypatch.setattr(supervisor_module, "get_platform_io_manager", lambda: platform_io_manager)
|
||||
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
register_response = await supervisor._handle_register_plugin(
|
||||
_make_request(
|
||||
"plugin.register_components",
|
||||
"napcat_plugin",
|
||||
{
|
||||
"plugin_id": "napcat_plugin",
|
||||
"plugin_version": "1.0.0",
|
||||
"components": [
|
||||
{
|
||||
"name": "napcat_gateway",
|
||||
"component_type": "MESSAGE_GATEWAY",
|
||||
"plugin_id": "napcat_plugin",
|
||||
"metadata": {
|
||||
"route_type": "duplex",
|
||||
"platform": "qq",
|
||||
"protocol": "napcat",
|
||||
},
|
||||
}
|
||||
],
|
||||
"capabilities_required": [],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert register_response.error is None
|
||||
response = await supervisor._handle_update_message_gateway_state(
|
||||
_make_request(
|
||||
"host.update_message_gateway_state",
|
||||
"napcat_plugin",
|
||||
{
|
||||
"gateway_name": "napcat_gateway",
|
||||
"ready": True,
|
||||
"platform": "qq",
|
||||
"account_id": "10001",
|
||||
"scope": "primary",
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.error is None
|
||||
assert response.payload["accepted"] is True
|
||||
|
||||
send_bindings = platform_io_manager.send_route_table.resolve_bindings(
|
||||
RouteKey(platform="qq", account_id="10001", scope="primary")
|
||||
)
|
||||
receive_bindings = platform_io_manager.receive_route_table.resolve_bindings(
|
||||
RouteKey(platform="qq", account_id="10001", scope="primary")
|
||||
)
|
||||
|
||||
assert [binding.driver_id for binding in send_bindings] == ["gateway:napcat_plugin:napcat_gateway"]
|
||||
assert [binding.driver_id for binding in receive_bindings] == ["gateway:napcat_plugin:napcat_gateway"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_message_gateway_runtime_state_unbinds_routes_when_not_ready(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""消息网关断开后应撤销发送表和接收表中的绑定。"""
|
||||
|
||||
import src.plugin_runtime.host.supervisor as supervisor_module
|
||||
|
||||
platform_io_manager = PlatformIOManager()
|
||||
monkeypatch.setattr(supervisor_module, "get_platform_io_manager", lambda: platform_io_manager)
|
||||
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
await supervisor._handle_register_plugin(
|
||||
_make_request(
|
||||
"plugin.register_components",
|
||||
"napcat_plugin",
|
||||
{
|
||||
"plugin_id": "napcat_plugin",
|
||||
"plugin_version": "1.0.0",
|
||||
"components": [
|
||||
{
|
||||
"name": "napcat_gateway",
|
||||
"component_type": "MESSAGE_GATEWAY",
|
||||
"plugin_id": "napcat_plugin",
|
||||
"metadata": {
|
||||
"route_type": "duplex",
|
||||
"platform": "qq",
|
||||
"protocol": "napcat",
|
||||
},
|
||||
}
|
||||
],
|
||||
"capabilities_required": [],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
await supervisor._handle_update_message_gateway_state(
|
||||
_make_request(
|
||||
"host.update_message_gateway_state",
|
||||
"napcat_plugin",
|
||||
{
|
||||
"gateway_name": "napcat_gateway",
|
||||
"ready": True,
|
||||
"platform": "qq",
|
||||
"account_id": "10001",
|
||||
"scope": "primary",
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
response = await supervisor._handle_update_message_gateway_state(
|
||||
_make_request(
|
||||
"host.update_message_gateway_state",
|
||||
"napcat_plugin",
|
||||
{
|
||||
"gateway_name": "napcat_gateway",
|
||||
"ready": False,
|
||||
"platform": "qq",
|
||||
"account_id": "",
|
||||
"scope": "",
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.error is None
|
||||
assert response.payload["accepted"] is True
|
||||
assert platform_io_manager.send_route_table.resolve_bindings(RouteKey(platform="qq", account_id="10001")) == []
|
||||
assert (
|
||||
platform_io_manager.receive_route_table.resolve_bindings(RouteKey(platform="qq", account_id="10001")) == []
|
||||
)
|
||||
@@ -159,6 +159,51 @@ class TestPlatformIODedupe:
|
||||
session_message_envelope = _build_envelope(session_message_id="session-1")
|
||||
payload_only_envelope = _build_envelope(payload={"message": "hello"})
|
||||
|
||||
assert PlatformIOManager._build_inbound_dedupe_key(explicit_envelope) == "qq:10001:main:dedupe-1"
|
||||
assert PlatformIOManager._build_inbound_dedupe_key(session_message_envelope) == "qq:10001:main:session-1"
|
||||
assert PlatformIOManager._build_inbound_dedupe_key(explicit_envelope) == "plugin.napcat:dedupe-1"
|
||||
assert PlatformIOManager._build_inbound_dedupe_key(session_message_envelope) == "plugin.napcat:session-1"
|
||||
assert PlatformIOManager._build_inbound_dedupe_key(payload_only_envelope) is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_fans_out_to_all_matching_routes(self) -> None:
|
||||
"""同一路由命中多条发送链路时应全部发送。"""
|
||||
|
||||
manager = PlatformIOManager()
|
||||
first_driver = _StubPlatformIODriver(
|
||||
DriverDescriptor(
|
||||
driver_id="plugin.gateway_a",
|
||||
kind=DriverKind.PLUGIN,
|
||||
platform="qq",
|
||||
)
|
||||
)
|
||||
second_driver = _StubPlatformIODriver(
|
||||
DriverDescriptor(
|
||||
driver_id="plugin.gateway_b",
|
||||
kind=DriverKind.PLUGIN,
|
||||
platform="qq",
|
||||
)
|
||||
)
|
||||
manager.register_driver(first_driver)
|
||||
manager.register_driver(second_driver)
|
||||
manager.bind_send_route(
|
||||
RouteBinding(
|
||||
route_key=RouteKey(platform="qq"),
|
||||
driver_id=first_driver.driver_id,
|
||||
driver_kind=first_driver.descriptor.kind,
|
||||
)
|
||||
)
|
||||
manager.bind_send_route(
|
||||
RouteBinding(
|
||||
route_key=RouteKey(platform="qq"),
|
||||
driver_id=second_driver.driver_id,
|
||||
driver_kind=second_driver.descriptor.kind,
|
||||
)
|
||||
)
|
||||
|
||||
message = SimpleNamespace(message_id="internal-msg-1")
|
||||
result = await manager.send_message(message, RouteKey(platform="qq"))
|
||||
|
||||
assert result.has_success is True
|
||||
assert [receipt.driver_id for receipt in result.sent_receipts] == [
|
||||
"plugin.gateway_a",
|
||||
"plugin.gateway_b",
|
||||
]
|
||||
|
||||
138
pytests/test_plugin_runtime_action_bridge.py
Normal file
138
pytests/test_plugin_runtime_action_bridge.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.component_registry import component_registry as core_component_registry
|
||||
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 注册载荷。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
action_name: Action 名称。
|
||||
|
||||
Returns:
|
||||
RegisterPluginPayload: 测试用注册载荷。
|
||||
"""
|
||||
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,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_actions_are_mirrored_into_core_registry_and_invoked(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""运行时 Action 应镜像到旧核心注册表,并可由旧 Planner 执行。"""
|
||||
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)
|
||||
|
||||
async def fake_invoke_plugin(
|
||||
method: str,
|
||||
plugin_id: str,
|
||||
component_name: str,
|
||||
args: dict[str, Any] | None = None,
|
||||
timeout_ms: int = 30000,
|
||||
) -> Any:
|
||||
"""模拟 plugin runtime Action 调用。
|
||||
|
||||
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
|
||||
captured["args"] = args or {}
|
||||
captured["timeout_ms"] = timeout_ms
|
||||
return SimpleNamespace(payload={"success": True, "result": (True, "runtime action executed")})
|
||||
|
||||
monkeypatch.setattr(supervisor, "invoke_plugin", fake_invoke_plugin)
|
||||
|
||||
try:
|
||||
supervisor._mirror_runtime_actions_to_core_registry(payload)
|
||||
|
||||
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 = 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},
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
supervisor = PluginSupervisor(plugin_dirs=[])
|
||||
|
||||
core_component_registry.remove_action(action_name)
|
||||
|
||||
supervisor._mirror_runtime_actions_to_core_registry(payload)
|
||||
assert core_component_registry.get_action_info(action_name) is not None
|
||||
|
||||
supervisor._clear_runner_state()
|
||||
|
||||
assert core_component_registry.get_action_info(action_name) is None
|
||||
Reference in New Issue
Block a user