feat: Enhance API and Outbound Tracking Functionality

- Add test for fallback to bot account in platform IO route metadata when context message is absent.
- Improve PlatformIOManager to avoid duplicate driver entries and streamline fallback driver handling.
- Refactor OutboundTracker to support tracking by both internal message ID and driver ID, enhancing the uniqueness of pending records.
- Introduce dynamic API capabilities in RuntimeComponent, allowing plugins to replace their dynamic API lists.
- Update APIRegistry to manage dynamic APIs more effectively, including registration and toggling of API statuses.
- Implement authorization checks for dynamic API capabilities to ensure proper permissions.
- Restrict direct calls to certain host RPC methods from plugins for enhanced security.
- Refactor send_service to ensure fallback to current platform account when no context message is available.
This commit is contained in:
DrSmoothl
2026-03-23 21:01:55 +08:00
parent d13767ee21
commit 7a304ba549
11 changed files with 771 additions and 200 deletions

View File

@@ -292,3 +292,233 @@ async def test_api_list_and_component_toggle_use_dedicated_registry() -> None:
)
assert enable_result["success"] is True
assert provider_supervisor.api_registry.get_api("provider", "public_api", enabled_only=True) is not None
@pytest.mark.asyncio
async def test_api_registry_supports_multiple_versions_with_distinct_handlers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""同名 API 不同版本应可并存,并按版本路由到不同处理器。"""
provider_supervisor = PluginSupervisor(plugin_dirs=[])
consumer_supervisor = PluginSupervisor(plugin_dirs=[])
await _register_plugin(
provider_supervisor,
"provider",
[
{
"name": "render_html",
"component_type": "API",
"metadata": {
"description": "渲染 HTML v1",
"version": "1",
"public": True,
"handler_name": "handle_render_html_v1",
},
},
{
"name": "render_html",
"component_type": "API",
"metadata": {
"description": "渲染 HTML v2",
"version": "2",
"public": True,
"handler_name": "handle_render_html_v2",
},
},
],
)
await _register_plugin(consumer_supervisor, "consumer", [])
captured: Dict[str, Any] = {}
async def fake_invoke_api(
plugin_id: str,
component_name: str,
args: Dict[str, Any] | None = None,
timeout_ms: int = 30000,
) -> Any:
"""模拟多版本 API 调用。"""
captured["plugin_id"] = plugin_id
captured["component_name"] = component_name
captured["args"] = args or {}
captured["timeout_ms"] = timeout_ms
return SimpleNamespace(error=None, payload={"success": True, "result": {"image": "ok"}})
monkeypatch.setattr(provider_supervisor, "invoke_api", fake_invoke_api)
manager = _build_manager(provider_supervisor, consumer_supervisor)
ambiguous_result = await manager._cap_api_call(
"consumer",
"api.call",
{
"api_name": "provider.render_html",
"args": {"html": "<div>Hello</div>"},
},
)
assert ambiguous_result["success"] is False
assert "多个版本" in str(ambiguous_result["error"])
disable_ambiguous_result = await manager._cap_component_disable(
"consumer",
"component.disable",
{
"name": "provider.render_html",
"component_type": "API",
"scope": "global",
"stream_id": "",
},
)
assert disable_ambiguous_result["success"] is False
assert "多个版本" in str(disable_ambiguous_result["error"])
disable_v1_result = await manager._cap_component_disable(
"consumer",
"component.disable",
{
"name": "provider.render_html",
"component_type": "API",
"scope": "global",
"stream_id": "",
"version": "1",
},
)
assert disable_v1_result["success"] is True
assert provider_supervisor.api_registry.get_api("provider", "render_html", version="1", enabled_only=True) is None
assert provider_supervisor.api_registry.get_api("provider", "render_html", version="2", enabled_only=True) is not None
result = await manager._cap_api_call(
"consumer",
"api.call",
{
"api_name": "provider.render_html",
"version": "2",
"args": {"html": "<div>Hello</div>"},
},
)
assert result == {"success": True, "result": {"image": "ok"}}
assert captured["plugin_id"] == "provider"
assert captured["component_name"] == "handle_render_html_v2"
assert captured["args"] == {"html": "<div>Hello</div>"}
@pytest.mark.asyncio
async def test_api_replace_dynamic_can_offline_removed_entries(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""动态 API 替换后,被移除的 API 应返回明确下线错误。"""
supervisor = PluginSupervisor(plugin_dirs=[])
await _register_plugin(supervisor, "provider", [])
manager = _build_manager(supervisor)
captured: Dict[str, Any] = {}
async def fake_invoke_api(
plugin_id: str,
component_name: str,
args: Dict[str, Any] | None = None,
timeout_ms: int = 30000,
) -> Any:
"""模拟动态 API 调用。"""
captured["plugin_id"] = plugin_id
captured["component_name"] = component_name
captured["args"] = args or {}
captured["timeout_ms"] = timeout_ms
return SimpleNamespace(error=None, payload={"success": True, "result": {"ok": True}})
monkeypatch.setattr(supervisor, "invoke_api", fake_invoke_api)
replace_result = await manager._cap_api_replace_dynamic(
"provider",
"api.replace_dynamic",
{
"apis": [
{
"name": "mcp.search",
"type": "API",
"metadata": {
"version": "1",
"public": True,
"handler_name": "dynamic_search",
},
},
{
"name": "mcp.read",
"type": "API",
"metadata": {
"version": "1",
"public": True,
"handler_name": "dynamic_read",
},
},
],
"offline_reason": "MCP 服务器已关闭",
},
)
assert replace_result["success"] is True
assert replace_result["count"] == 2
list_result = await manager._cap_api_list("provider", "api.list", {"plugin_id": "provider"})
assert {(item["name"], item["version"]) for item in list_result["apis"]} == {
("mcp.read", "1"),
("mcp.search", "1"),
}
call_result = await manager._cap_api_call(
"provider",
"api.call",
{
"api_name": "provider.mcp.search",
"version": "1",
"args": {"query": "hello"},
},
)
assert call_result == {"success": True, "result": {"ok": True}}
assert captured["component_name"] == "dynamic_search"
assert captured["args"]["query"] == "hello"
assert captured["args"]["__maibot_api_name__"] == "mcp.search"
assert captured["args"]["__maibot_api_version__"] == "1"
second_replace_result = await manager._cap_api_replace_dynamic(
"provider",
"api.replace_dynamic",
{
"apis": [
{
"name": "mcp.read",
"type": "API",
"metadata": {
"version": "1",
"public": True,
"handler_name": "dynamic_read",
},
}
],
"offline_reason": "MCP 服务器已关闭",
},
)
assert second_replace_result["success"] is True
assert second_replace_result["count"] == 1
assert second_replace_result["offlined"] == 1
offlined_call_result = await manager._cap_api_call(
"provider",
"api.call",
{
"api_name": "provider.mcp.search",
"version": "1",
"args": {},
},
)
assert offlined_call_result["success"] is False
assert "MCP 服务器已关闭" in str(offlined_call_result["error"])
list_after_replace = await manager._cap_api_list("provider", "api.list", {"plugin_id": "provider"})
assert {(item["name"], item["version"]) for item in list_after_replace["apis"]} == {
("mcp.read", "1"),
}