fix(plugin-runtime): propagate send capability failure details to tools
This commit is contained in:
@@ -542,6 +542,101 @@ class TestSDK:
|
||||
|
||||
assert await plugin.ctx.llm.get_available_models() == ["utils", "replyer"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_injected_context_raises_send_capability_error_details(self):
|
||||
"""Runner 应将 send.* 能力失败的底层错误透传为异常。"""
|
||||
from src.plugin_runtime.runner.runner_main import PluginRunner
|
||||
|
||||
class DummyRPCClient:
|
||||
async def send_request(self, method, plugin_id="", payload=None, timeout_ms=30000):
|
||||
assert method == "cap.call"
|
||||
assert plugin_id == "owner_plugin"
|
||||
assert payload == {
|
||||
"capability": "send.custom",
|
||||
"args": {
|
||||
"message_type": "poke",
|
||||
"content": {"qq_id": "1"},
|
||||
"custom_type": "poke",
|
||||
"data": {"qq_id": "1"},
|
||||
"stream_id": "当前聊天流",
|
||||
},
|
||||
}
|
||||
return SimpleNamespace(
|
||||
error=None,
|
||||
payload={"success": True, "result": {"success": False, "error": "未找到聊天流: 当前聊天流"}},
|
||||
)
|
||||
|
||||
class DummyPlugin:
|
||||
def _set_context(self, ctx):
|
||||
self.ctx = ctx
|
||||
|
||||
runner = PluginRunner(host_address="dummy", session_token="token", plugin_dirs=[])
|
||||
runner._rpc_client = DummyRPCClient()
|
||||
|
||||
plugin = DummyPlugin()
|
||||
runner._inject_context("owner_plugin", plugin)
|
||||
|
||||
with pytest.raises(RuntimeError, match="未找到聊天流: 当前聊天流"):
|
||||
await plugin.ctx.send.custom(
|
||||
custom_type="poke",
|
||||
data={"qq_id": "1"},
|
||||
stream_id="当前聊天流",
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_invoke_tool_propagates_send_failure_details(self):
|
||||
"""插件工具捕获 send.* 失败时,应能拿到底层错误详情。"""
|
||||
from src.plugin_runtime.protocol.envelope import Envelope, MessageType
|
||||
from src.plugin_runtime.runner.runner_main import PluginRunner
|
||||
|
||||
class DummyRPCClient:
|
||||
async def send_request(self, method, plugin_id="", payload=None, timeout_ms=30000):
|
||||
assert method == "cap.call"
|
||||
return SimpleNamespace(
|
||||
error=None,
|
||||
payload={"success": True, "result": {"success": False, "error": "未找到聊天流: 当前聊天流"}},
|
||||
)
|
||||
|
||||
class DummyPlugin:
|
||||
def _set_context(self, ctx):
|
||||
self.ctx = ctx
|
||||
|
||||
async def handle_poke(self, **kwargs):
|
||||
try:
|
||||
await self.ctx.send.custom(
|
||||
custom_type="poke",
|
||||
data={"qq_id": "1"},
|
||||
stream_id=str(kwargs.get("stream_id", "")),
|
||||
)
|
||||
except Exception as exc:
|
||||
return {"success": False, "message": f"戳一戳失败: {exc}"}
|
||||
return {"success": True, "message": "戳一戳成功"}
|
||||
|
||||
runner = PluginRunner(host_address="dummy", session_token="token", plugin_dirs=[])
|
||||
runner._rpc_client = DummyRPCClient()
|
||||
|
||||
plugin = DummyPlugin()
|
||||
runner._inject_context("demo_plugin", plugin)
|
||||
meta = SimpleNamespace(
|
||||
plugin_id="demo_plugin",
|
||||
instance=plugin,
|
||||
component_handlers={"poke": "handle_poke"},
|
||||
)
|
||||
runner._loader._loaded_plugins["demo_plugin"] = meta
|
||||
|
||||
envelope = Envelope(
|
||||
request_id=1,
|
||||
message_type=MessageType.REQUEST,
|
||||
method="plugin.invoke_tool",
|
||||
plugin_id="demo_plugin",
|
||||
payload={"component_name": "poke", "args": {"stream_id": "当前聊天流"}},
|
||||
)
|
||||
|
||||
response = await runner._handle_invoke(envelope)
|
||||
|
||||
assert response.payload["success"] is True
|
||||
assert response.payload["result"] == {"success": False, "message": "戳一戳失败: 未找到聊天流: 当前聊天流"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_applies_initial_plugin_config(self, tmp_path):
|
||||
"""Runner 应在 on_load 前为支持的插件实例注入 config.toml。"""
|
||||
|
||||
@@ -75,6 +75,18 @@ _PLUGIN_ALLOWED_RAW_HOST_METHODS = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
_RAISE_ON_FAILED_CAPABILITIES = frozenset(
|
||||
{
|
||||
"send.command",
|
||||
"send.custom",
|
||||
"send.emoji",
|
||||
"send.forward",
|
||||
"send.hybrid",
|
||||
"send.image",
|
||||
"send.text",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class _ContextAwarePlugin(Protocol):
|
||||
"""支持注入运行时上下文的插件协议。
|
||||
@@ -518,7 +530,18 @@ class PluginRunner:
|
||||
if resp.error:
|
||||
raise RuntimeError(resp.error.get("message", "能力调用失败"))
|
||||
if normalized_method == "cap.call" and isinstance(resp.payload, dict) and "result" in resp.payload:
|
||||
return resp.payload.get("result")
|
||||
capability_result = resp.payload.get("result")
|
||||
capability_name = str((payload or {}).get("capability", "")).strip()
|
||||
if (
|
||||
capability_name in _RAISE_ON_FAILED_CAPABILITIES
|
||||
and isinstance(capability_result, dict)
|
||||
and capability_result.get("success") is False
|
||||
):
|
||||
error_message = str(
|
||||
capability_result.get("error") or capability_result.get("message") or "能力调用失败"
|
||||
).strip()
|
||||
raise RuntimeError(error_message or "能力调用失败")
|
||||
return capability_result
|
||||
return resp.payload
|
||||
|
||||
ctx = PluginContext(plugin_id=plugin_id, rpc_call=_rpc_call)
|
||||
|
||||
Reference in New Issue
Block a user