feat: Add NapCat adapter plugin and enhance message handling
- Introduced a built-in NapCat adapter plugin for MVP message forwarding. - Implemented core functionalities for connecting to NapCat/OneBot v11 WebSocket service. - Added message serialization capabilities for WebUI chat routes. - Enhanced the RegisterPluginPayload to include optional adapter declarations. - Implemented methods for handling external messages and adapter declarations in the PluginRunner. - Improved the send_service to inherit platform IO route metadata for outgoing messages.
This commit is contained in:
@@ -3,9 +3,11 @@ Message Gateway 模块
|
||||
适配器专用,用于将其他平台的消息转换为系统内部的消息格式,并将系统消息转换为其他平台的格式。
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.platform_io import DeliveryStatus, get_platform_io_manager
|
||||
|
||||
from .message_utils import PluginMessageUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -17,25 +19,53 @@ logger = get_logger("plugin_runtime.host.message_gateway")
|
||||
|
||||
|
||||
class MessageGateway:
|
||||
def __init__(self, component_registry: "ComponentRegistry") -> None:
|
||||
self._component_registry = component_registry
|
||||
"""Host 侧消息网关包装器。"""
|
||||
|
||||
async def receive_external_message(self, external_message: Dict[str, Any]):
|
||||
"""
|
||||
接收外部消息,转换为系统内部格式,并返回转换结果
|
||||
def __init__(self, component_registry: "ComponentRegistry") -> None:
|
||||
"""初始化消息网关。
|
||||
|
||||
Args:
|
||||
external_message: 外部消息的字典格式数据
|
||||
component_registry: 组件注册表。
|
||||
"""
|
||||
self._component_registry = component_registry
|
||||
|
||||
def build_session_message(self, external_message: Dict[str, Any]) -> "SessionMessage":
|
||||
"""将标准消息字典转换为 ``SessionMessage``。
|
||||
|
||||
Args:
|
||||
external_message: 外部消息的字典格式数据。
|
||||
|
||||
Returns:
|
||||
转换后的 SessionMessage 对象
|
||||
SessionMessage: 转换后的内部消息对象。
|
||||
|
||||
Raises:
|
||||
ValueError: 消息字典不合法时抛出。
|
||||
"""
|
||||
return PluginMessageUtils._build_session_message_from_dict(external_message)
|
||||
|
||||
def build_message_dict(self, internal_message: "SessionMessage") -> Dict[str, Any]:
|
||||
"""将 ``SessionMessage`` 转换为标准消息字典。
|
||||
|
||||
Args:
|
||||
internal_message: 内部消息对象。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 供适配器插件消费的标准消息字典。
|
||||
"""
|
||||
return dict(PluginMessageUtils._session_message_to_dict(internal_message))
|
||||
|
||||
async def receive_external_message(self, external_message: Dict[str, Any]) -> None:
|
||||
"""接收外部消息并送入主消息链。
|
||||
|
||||
Args:
|
||||
external_message: 外部消息的字典格式数据。
|
||||
"""
|
||||
# 使用递归函数将外部消息字典转换为 SessionMessage
|
||||
try:
|
||||
session_message = PluginMessageUtils._build_session_message_from_dict(external_message)
|
||||
session_message = self.build_session_message(external_message)
|
||||
except Exception as e:
|
||||
logger.error(f"转换外部消息失败: {e}")
|
||||
return
|
||||
|
||||
from src.chat.message_receive.bot import chat_bot
|
||||
|
||||
await chat_bot.receive_message(session_message)
|
||||
@@ -48,46 +78,32 @@ class MessageGateway:
|
||||
enabled_only: bool = True,
|
||||
save_to_db: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
接收系统内部消息,转换为外部格式,并返回转换结果
|
||||
"""将内部消息通过 Platform IO 发送到外部平台。
|
||||
|
||||
Args:
|
||||
internal_message: 系统内部的 SessionMessage 对象
|
||||
internal_message: 系统内部的 ``SessionMessage`` 对象。
|
||||
supervisor: 当前持有该消息网关的 Supervisor。
|
||||
enabled_only: 兼容旧签名的保留参数,当前由 Platform IO 统一裁决。
|
||||
save_to_db: 发送成功后是否写入数据库。
|
||||
|
||||
Returns:
|
||||
转换是否成功
|
||||
bool: 是否发送成功。
|
||||
"""
|
||||
try:
|
||||
# 将 SessionMessage 转换为字典格式
|
||||
message_dict = PluginMessageUtils._session_message_to_dict(internal_message)
|
||||
except Exception as e:
|
||||
logger.error(f"转换内部消息失败:{e}")
|
||||
return False
|
||||
gateway_entry = self._component_registry.get_message_gateways(
|
||||
internal_message.platform,
|
||||
enabled_only=enabled_only,
|
||||
session_id=internal_message.session_id,
|
||||
)
|
||||
if not gateway_entry:
|
||||
logger.warning(f"未找到适配平台 {internal_message.platform} 的消息网关组件,无法发送消息到外部平台")
|
||||
return False
|
||||
args = {"platform": internal_message.platform, "message": message_dict}
|
||||
try:
|
||||
resp_envelope = await supervisor.invoke_plugin(
|
||||
"plugin.emit_event", gateway_entry.plugin_id, gateway_entry.name, args
|
||||
)
|
||||
logger.debug("信息发送成功")
|
||||
except Exception as e:
|
||||
logger.error(f"调用消息网关组件失败:{e}")
|
||||
del enabled_only
|
||||
del supervisor
|
||||
|
||||
platform_io_manager = get_platform_io_manager()
|
||||
if not platform_io_manager.is_started:
|
||||
logger.warning("Platform IO 尚未启动,无法通过适配器链路发送消息")
|
||||
return False
|
||||
|
||||
# 更新为实际id(如果组件返回了新的id)
|
||||
actual_message_id = resp_envelope.payload.get("message_id")
|
||||
try:
|
||||
actual_message_id = str(actual_message_id)
|
||||
except Exception:
|
||||
actual_message_id = None
|
||||
internal_message.message_id = actual_message_id or internal_message.message_id
|
||||
route_key = platform_io_manager.build_route_key_from_message(internal_message)
|
||||
receipt = await platform_io_manager.send_message(internal_message, route_key)
|
||||
if receipt.status != DeliveryStatus.SENT:
|
||||
logger.warning(f"通过适配器链路发送消息失败: {receipt.error or receipt.status}")
|
||||
return False
|
||||
|
||||
internal_message.message_id = receipt.external_message_id or internal_message.message_id
|
||||
if save_to_db:
|
||||
try:
|
||||
from src.common.utils.utils_message import MessageUtils
|
||||
|
||||
@@ -209,6 +209,9 @@ class PluginMessageUtils:
|
||||
session_message.is_notify = message_dict.get("is_notify", False)
|
||||
if not isinstance(session_message.is_notify, bool):
|
||||
session_message.is_notify = False
|
||||
session_message.session_id = message_dict.get("session_id", "")
|
||||
if not isinstance(session_message.session_id, str):
|
||||
session_message.session_id = ""
|
||||
session_message.reply_to = message_dict.get("reply_to")
|
||||
if session_message.reply_to is not None and not isinstance(session_message.reply_to, str):
|
||||
session_message.reply_to = None
|
||||
|
||||
@@ -8,13 +8,20 @@ import sys
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.platform_io import DriverKind, InboundMessageEnvelope, RouteBinding, RouteKey, get_platform_io_manager
|
||||
from src.platform_io.drivers import PluginPlatformDriver
|
||||
from src.platform_io.route_key_factory import RouteKeyFactory
|
||||
from src.platform_io.routing import RouteBindingConflictError
|
||||
from src.plugin_runtime import ENV_HOST_VERSION, ENV_IPC_ADDRESS, ENV_PLUGIN_DIRS, ENV_SESSION_TOKEN
|
||||
from src.plugin_runtime.protocol.envelope import (
|
||||
AdapterDeclarationPayload,
|
||||
BootstrapPluginPayload,
|
||||
ConfigUpdatedPayload,
|
||||
Envelope,
|
||||
HealthPayload,
|
||||
PROTOCOL_VERSION,
|
||||
ReceiveExternalMessagePayload,
|
||||
ReceiveExternalMessageResultPayload,
|
||||
RegisterPluginPayload,
|
||||
ReloadPluginResultPayload,
|
||||
RunnerReadyPayload,
|
||||
@@ -86,6 +93,7 @@ class PluginRunnerSupervisor:
|
||||
|
||||
self._runner_process: Optional[asyncio.subprocess.Process] = None
|
||||
self._registered_plugins: Dict[str, RegisterPluginPayload] = {}
|
||||
self._registered_adapters: Dict[str, AdapterDeclarationPayload] = {}
|
||||
self._runner_ready_events: asyncio.Event = asyncio.Event()
|
||||
self._runner_ready_payloads: RunnerReadyPayload = RunnerReadyPayload()
|
||||
self._health_task: Optional[asyncio.Task[None]] = None
|
||||
@@ -257,6 +265,32 @@ class PluginRunnerSupervisor:
|
||||
timeout_ms,
|
||||
)
|
||||
|
||||
async def invoke_adapter(
|
||||
self,
|
||||
plugin_id: str,
|
||||
method_name: str,
|
||||
args: Optional[Dict[str, Any]] = None,
|
||||
timeout_ms: int = 30000,
|
||||
) -> Envelope:
|
||||
"""调用适配器插件的专用方法。
|
||||
|
||||
Args:
|
||||
plugin_id: 目标适配器插件 ID。
|
||||
method_name: 要调用的插件方法名,例如 ``send_to_platform``。
|
||||
args: 传递给插件方法的关键字参数。
|
||||
timeout_ms: RPC 超时时间,单位毫秒。
|
||||
|
||||
Returns:
|
||||
Envelope: Runner 返回的响应信封。
|
||||
"""
|
||||
return await self.invoke_plugin(
|
||||
method="plugin.invoke_adapter",
|
||||
plugin_id=plugin_id,
|
||||
component_name=method_name,
|
||||
args=args,
|
||||
timeout_ms=timeout_ms,
|
||||
)
|
||||
|
||||
async def reload_plugin(self, plugin_id: str, reason: str = "manual") -> bool:
|
||||
"""按插件 ID 触发精确重载。
|
||||
|
||||
@@ -384,6 +418,7 @@ class PluginRunnerSupervisor:
|
||||
def _register_internal_methods(self) -> None:
|
||||
"""注册 Host 侧内部 RPC 方法。"""
|
||||
self._rpc_server.register_method("cap.call", self._capability_service.handle_capability_request)
|
||||
self._rpc_server.register_method("host.receive_external_message", self._handle_receive_external_message)
|
||||
self._rpc_server.register_method("plugin.bootstrap", self._handle_bootstrap_plugin)
|
||||
self._rpc_server.register_method("plugin.register_components", self._handle_register_plugin)
|
||||
self._rpc_server.register_method("plugin.register_plugin", self._handle_register_plugin)
|
||||
@@ -427,6 +462,17 @@ class PluginRunnerSupervisor:
|
||||
return envelope.make_error_response(ErrorCode.E_BAD_PAYLOAD.value, str(exc))
|
||||
|
||||
self._component_registry.remove_components_by_plugin(payload.plugin_id)
|
||||
if payload.plugin_id in self._registered_adapters:
|
||||
await self._unregister_adapter_driver(payload.plugin_id)
|
||||
|
||||
try:
|
||||
if payload.adapter is not None:
|
||||
await self._register_adapter_driver(payload.plugin_id, payload.adapter)
|
||||
except RouteBindingConflictError as exc:
|
||||
return envelope.make_error_response(ErrorCode.E_METHOD_NOT_ALLOWED.value, str(exc))
|
||||
except Exception as exc:
|
||||
return envelope.make_error_response(ErrorCode.E_UNKNOWN.value, str(exc))
|
||||
|
||||
registered_count = self._component_registry.register_plugin_components(
|
||||
payload.plugin_id,
|
||||
[component.model_dump() for component in payload.components],
|
||||
@@ -438,6 +484,7 @@ class PluginRunnerSupervisor:
|
||||
"accepted": True,
|
||||
"plugin_id": payload.plugin_id,
|
||||
"registered_components": registered_count,
|
||||
"adapter_registered": payload.adapter is not None,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -458,6 +505,7 @@ class PluginRunnerSupervisor:
|
||||
removed_components = self._component_registry.remove_components_by_plugin(payload.plugin_id)
|
||||
self._authorization.revoke_permission_token(payload.plugin_id)
|
||||
removed_registration = self._registered_plugins.pop(payload.plugin_id, None) is not None
|
||||
await self._unregister_adapter_driver(payload.plugin_id)
|
||||
|
||||
return envelope.make_response(
|
||||
payload={
|
||||
@@ -469,6 +517,221 @@ class PluginRunnerSupervisor:
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_adapter_driver_id(plugin_id: str) -> str:
|
||||
"""构造适配器驱动 ID。
|
||||
|
||||
Args:
|
||||
plugin_id: 适配器插件 ID。
|
||||
|
||||
Returns:
|
||||
str: 对应 Platform IO 中的驱动 ID。
|
||||
"""
|
||||
return f"adapter:{plugin_id}"
|
||||
|
||||
async def _register_adapter_driver(self, plugin_id: str, adapter: AdapterDeclarationPayload) -> None:
|
||||
"""将适配器插件注册到 Platform IO。
|
||||
|
||||
Args:
|
||||
plugin_id: 适配器插件 ID。
|
||||
adapter: 经过校验的适配器声明。
|
||||
|
||||
Raises:
|
||||
ValueError: 适配器路由冲突或驱动注册失败时抛出。
|
||||
"""
|
||||
await self._unregister_adapter_driver(plugin_id)
|
||||
|
||||
platform_io_manager = get_platform_io_manager()
|
||||
driver = PluginPlatformDriver(
|
||||
driver_id=self._build_adapter_driver_id(plugin_id),
|
||||
platform=adapter.platform,
|
||||
account_id=adapter.account_id or None,
|
||||
scope=adapter.scope or None,
|
||||
plugin_id=plugin_id,
|
||||
send_method=adapter.send_method,
|
||||
supervisor=self,
|
||||
metadata={
|
||||
"protocol": adapter.protocol,
|
||||
**adapter.metadata,
|
||||
},
|
||||
)
|
||||
binding = RouteBinding(
|
||||
route_key=driver.descriptor.route_key,
|
||||
driver_id=driver.driver_id,
|
||||
driver_kind=DriverKind.PLUGIN,
|
||||
metadata={
|
||||
"plugin_id": plugin_id,
|
||||
"protocol": adapter.protocol,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
if platform_io_manager.is_started:
|
||||
await platform_io_manager.add_driver(driver)
|
||||
else:
|
||||
platform_io_manager.register_driver(driver)
|
||||
platform_io_manager.bind_route(binding)
|
||||
except Exception:
|
||||
with contextlib.suppress(Exception):
|
||||
if platform_io_manager.is_started:
|
||||
await platform_io_manager.remove_driver(driver.driver_id)
|
||||
else:
|
||||
platform_io_manager.unregister_driver(driver.driver_id)
|
||||
raise
|
||||
|
||||
self._registered_adapters[plugin_id] = adapter
|
||||
|
||||
async def _unregister_adapter_driver(self, plugin_id: str) -> None:
|
||||
"""从 Platform IO 注销一个适配器驱动。
|
||||
|
||||
Args:
|
||||
plugin_id: 适配器插件 ID。
|
||||
"""
|
||||
platform_io_manager = get_platform_io_manager()
|
||||
driver_id = self._build_adapter_driver_id(plugin_id)
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
if platform_io_manager.is_started:
|
||||
await platform_io_manager.remove_driver(driver_id)
|
||||
else:
|
||||
platform_io_manager.unregister_driver(driver_id)
|
||||
|
||||
self._registered_adapters.pop(plugin_id, None)
|
||||
|
||||
async def _unregister_all_adapter_drivers(self) -> None:
|
||||
"""注销当前 Supervisor 管理的全部适配器驱动。"""
|
||||
plugin_ids = list(self._registered_adapters.keys())
|
||||
for plugin_id in plugin_ids:
|
||||
await self._unregister_adapter_driver(plugin_id)
|
||||
|
||||
@staticmethod
|
||||
def _attach_inbound_route_metadata(
|
||||
session_message: "SessionMessage",
|
||||
route_key: RouteKey,
|
||||
route_metadata: Dict[str, Any],
|
||||
) -> None:
|
||||
"""将入站路由信息写回消息的 ``additional_config``。
|
||||
|
||||
Args:
|
||||
session_message: 已构造好的内部消息对象。
|
||||
route_key: Host 为该消息解析出的标准路由键。
|
||||
route_metadata: 适配器通过 RPC 补充的原始路由辅助元数据。
|
||||
"""
|
||||
additional_config = session_message.message_info.additional_config
|
||||
if not isinstance(additional_config, dict):
|
||||
additional_config = {}
|
||||
session_message.message_info.additional_config = additional_config
|
||||
|
||||
for key, value in route_metadata.items():
|
||||
if value is None:
|
||||
continue
|
||||
normalized_value = str(value).strip()
|
||||
if normalized_value:
|
||||
additional_config[key] = value
|
||||
|
||||
if route_key.account_id:
|
||||
additional_config.setdefault("platform_io_account_id", route_key.account_id)
|
||||
if route_key.scope:
|
||||
additional_config.setdefault("platform_io_scope", route_key.scope)
|
||||
|
||||
def _build_inbound_route_key(
|
||||
self,
|
||||
adapter: AdapterDeclarationPayload,
|
||||
message: Dict[str, Any],
|
||||
route_metadata: Dict[str, Any],
|
||||
) -> RouteKey:
|
||||
"""为适配器入站消息构造归一路由键。
|
||||
|
||||
Args:
|
||||
adapter: 当前适配器声明。
|
||||
message: 标准消息字典。
|
||||
route_metadata: 插件补充的路由辅助元数据。
|
||||
|
||||
Returns:
|
||||
RouteKey: 供 Platform IO 使用的规范化路由键。
|
||||
|
||||
Raises:
|
||||
ValueError: 消息平台字段与适配器平台声明不一致时抛出。
|
||||
"""
|
||||
message_platform = str(message.get("platform") or adapter.platform).strip()
|
||||
if message_platform != adapter.platform:
|
||||
raise ValueError(
|
||||
f"外部消息平台 {message_platform} 与适配器 {adapter.platform} 不一致"
|
||||
)
|
||||
|
||||
try:
|
||||
route_key = RouteKeyFactory.from_message_dict(message)
|
||||
except Exception:
|
||||
route_key = RouteKey(platform=message_platform)
|
||||
|
||||
route_account_id, route_scope = RouteKeyFactory.extract_components(route_metadata)
|
||||
account_id = route_key.account_id or route_account_id or adapter.account_id or None
|
||||
scope = route_key.scope or route_scope or adapter.scope or None
|
||||
return RouteKey(
|
||||
platform=message_platform,
|
||||
account_id=account_id,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
async def _handle_receive_external_message(self, envelope: Envelope) -> Envelope:
|
||||
"""处理适配器插件上报的外部入站消息。
|
||||
|
||||
Args:
|
||||
envelope: RPC 请求信封。
|
||||
|
||||
Returns:
|
||||
Envelope: 注入结果响应。
|
||||
"""
|
||||
try:
|
||||
payload = ReceiveExternalMessagePayload.model_validate(envelope.payload)
|
||||
except Exception as exc:
|
||||
return envelope.make_error_response(ErrorCode.E_BAD_PAYLOAD.value, str(exc))
|
||||
|
||||
adapter = self._registered_adapters.get(envelope.plugin_id)
|
||||
if adapter is None:
|
||||
return envelope.make_error_response(
|
||||
ErrorCode.E_METHOD_NOT_ALLOWED.value,
|
||||
f"插件 {envelope.plugin_id} 未声明为适配器,不能注入外部消息",
|
||||
)
|
||||
|
||||
try:
|
||||
route_key = self._build_inbound_route_key(
|
||||
adapter=adapter,
|
||||
message=payload.message,
|
||||
route_metadata=payload.route_metadata,
|
||||
)
|
||||
session_message = self._message_gateway.build_session_message(payload.message)
|
||||
self._attach_inbound_route_metadata(session_message, route_key, payload.route_metadata)
|
||||
except Exception as exc:
|
||||
return envelope.make_error_response(ErrorCode.E_BAD_PAYLOAD.value, str(exc))
|
||||
|
||||
platform_io_manager = get_platform_io_manager()
|
||||
accepted = await platform_io_manager.accept_inbound(
|
||||
InboundMessageEnvelope(
|
||||
route_key=route_key,
|
||||
driver_id=self._build_adapter_driver_id(envelope.plugin_id),
|
||||
driver_kind=DriverKind.PLUGIN,
|
||||
external_message_id=payload.external_message_id or str(payload.message.get("message_id") or "") or None,
|
||||
dedupe_key=payload.dedupe_key or None,
|
||||
session_message=session_message,
|
||||
payload=payload.message,
|
||||
metadata={
|
||||
"plugin_id": envelope.plugin_id,
|
||||
"protocol": adapter.protocol,
|
||||
**payload.route_metadata,
|
||||
},
|
||||
)
|
||||
)
|
||||
response = ReceiveExternalMessageResultPayload(
|
||||
accepted=accepted,
|
||||
route_key={
|
||||
"platform": route_key.platform,
|
||||
"account_id": route_key.account_id,
|
||||
"scope": route_key.scope,
|
||||
},
|
||||
)
|
||||
return envelope.make_response(payload=response.model_dump())
|
||||
|
||||
async def _handle_runner_ready(self, envelope: Envelope) -> Envelope:
|
||||
"""处理 Runner 就绪通知。
|
||||
|
||||
@@ -595,6 +858,7 @@ class PluginRunnerSupervisor:
|
||||
await self._stderr_drain_task
|
||||
self._stderr_drain_task = None
|
||||
|
||||
await self._unregister_all_adapter_drivers()
|
||||
self._clear_runner_state()
|
||||
|
||||
async def _health_check_loop(self) -> None:
|
||||
@@ -671,6 +935,7 @@ class PluginRunnerSupervisor:
|
||||
self._authorization.clear()
|
||||
self._component_registry.clear()
|
||||
self._registered_plugins.clear()
|
||||
self._registered_adapters.clear()
|
||||
self._runner_ready_events = asyncio.Event()
|
||||
self._runner_ready_payloads = RunnerReadyPayload()
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Coroutine, Dict, Ite
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import tomlkit
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.config.file_watcher import FileChange, FileWatcher
|
||||
from src.platform_io import DeliveryReceipt, InboundMessageEnvelope, get_platform_io_manager
|
||||
from src.plugin_runtime.capabilities import (
|
||||
RuntimeComponentCapabilityMixin,
|
||||
RuntimeCoreCapabilityMixin,
|
||||
@@ -57,6 +59,7 @@ class PluginRuntimeManager(
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""初始化插件运行时管理器。"""
|
||||
from src.plugin_runtime.host.supervisor import PluginSupervisor
|
||||
|
||||
self._builtin_supervisor: Optional[PluginSupervisor] = None
|
||||
@@ -66,6 +69,22 @@ class PluginRuntimeManager(
|
||||
self._plugin_source_watcher_subscription_id: Optional[str] = None
|
||||
self._plugin_config_watcher_subscriptions: Dict[str, Tuple[Path, str]] = {}
|
||||
|
||||
async def _dispatch_platform_inbound(self, envelope: InboundMessageEnvelope) -> None:
|
||||
"""接收 Platform IO 审核后的入站消息并送入主消息链。
|
||||
|
||||
Args:
|
||||
envelope: Platform IO 产出的入站封装。
|
||||
"""
|
||||
session_message = envelope.session_message
|
||||
if session_message is None and envelope.payload is not None:
|
||||
session_message = PluginMessageUtils._build_session_message_from_dict(dict(envelope.payload))
|
||||
if session_message is None:
|
||||
raise ValueError("Platform IO 入站封装缺少可用的 SessionMessage 或 payload")
|
||||
|
||||
from src.chat.message_receive.bot import chat_bot
|
||||
|
||||
await chat_bot.receive_message(session_message)
|
||||
|
||||
# ─── 插件目录 ─────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
@@ -110,6 +129,8 @@ class PluginRuntimeManager(
|
||||
logger.info("未找到任何插件目录,跳过插件运行时启动")
|
||||
return
|
||||
|
||||
platform_io_manager = get_platform_io_manager()
|
||||
|
||||
# 从配置读取自定义 IPC socket 路径(留空则自动生成)
|
||||
socket_path_base = _cfg.ipc_socket_path or None
|
||||
|
||||
@@ -134,6 +155,9 @@ class PluginRuntimeManager(
|
||||
|
||||
started_supervisors: List[PluginSupervisor] = []
|
||||
try:
|
||||
platform_io_manager.set_inbound_dispatcher(self._dispatch_platform_inbound)
|
||||
await platform_io_manager.start()
|
||||
|
||||
if self._builtin_supervisor:
|
||||
await self._builtin_supervisor.start()
|
||||
started_supervisors.append(self._builtin_supervisor)
|
||||
@@ -147,6 +171,11 @@ class PluginRuntimeManager(
|
||||
logger.error(f"插件运行时启动失败: {e}", exc_info=True)
|
||||
await self._stop_plugin_file_watcher()
|
||||
await asyncio.gather(*(sv.stop() for sv in started_supervisors), return_exceptions=True)
|
||||
platform_io_manager.clear_inbound_dispatcher()
|
||||
try:
|
||||
await platform_io_manager.stop()
|
||||
except Exception as platform_io_exc:
|
||||
logger.warning(f"Platform IO 停止失败: {platform_io_exc}")
|
||||
self._started = False
|
||||
self._builtin_supervisor = None
|
||||
self._third_party_supervisor = None
|
||||
@@ -156,6 +185,7 @@ class PluginRuntimeManager(
|
||||
if not self._started:
|
||||
return
|
||||
|
||||
platform_io_manager = get_platform_io_manager()
|
||||
await self._stop_plugin_file_watcher()
|
||||
|
||||
coroutines: List[Coroutine[Any, Any, None]] = []
|
||||
@@ -164,11 +194,23 @@ class PluginRuntimeManager(
|
||||
if self._third_party_supervisor:
|
||||
coroutines.append(self._third_party_supervisor.stop())
|
||||
|
||||
stop_errors: List[str] = []
|
||||
try:
|
||||
await asyncio.gather(*coroutines, return_exceptions=True)
|
||||
logger.info("插件运行时已停止")
|
||||
except Exception as e:
|
||||
logger.error(f"插件运行时停止失败: {e}", exc_info=True)
|
||||
results = await asyncio.gather(*coroutines, return_exceptions=True)
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
stop_errors.append(str(result))
|
||||
|
||||
platform_io_manager.clear_inbound_dispatcher()
|
||||
try:
|
||||
await platform_io_manager.stop()
|
||||
except Exception as exc:
|
||||
stop_errors.append(f"Platform IO: {exc}")
|
||||
|
||||
if stop_errors:
|
||||
logger.error(f"插件运行时停止过程中存在错误: {'; '.join(stop_errors)}")
|
||||
else:
|
||||
logger.info("插件运行时已停止")
|
||||
finally:
|
||||
self._started = False
|
||||
self._builtin_supervisor = None
|
||||
@@ -176,6 +218,7 @@ class PluginRuntimeManager(
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""返回插件运行时是否处于启动状态。"""
|
||||
return self._started
|
||||
|
||||
@property
|
||||
@@ -303,6 +346,37 @@ class PluginRuntimeManager(
|
||||
timeout_ms=timeout_ms,
|
||||
)
|
||||
|
||||
async def try_send_message_via_platform_io(
|
||||
self,
|
||||
message: "SessionMessage",
|
||||
) -> Optional[DeliveryReceipt]:
|
||||
"""尝试通过 Platform IO 中间层发送消息。
|
||||
|
||||
Args:
|
||||
message: 待发送的内部会话消息。
|
||||
|
||||
Returns:
|
||||
Optional[DeliveryReceipt]: 若当前消息存在 active 路由,则返回实际发送
|
||||
结果;若没有可用路由或 Platform IO 尚未启动,则返回 ``None``。
|
||||
"""
|
||||
if not self._started:
|
||||
return None
|
||||
|
||||
platform_io_manager = get_platform_io_manager()
|
||||
if not platform_io_manager.is_started:
|
||||
return None
|
||||
|
||||
try:
|
||||
route_key = platform_io_manager.build_route_key_from_message(message)
|
||||
except Exception as exc:
|
||||
logger.warning(f"根据消息构造 Platform IO 路由键失败: {exc}")
|
||||
return None
|
||||
|
||||
if platform_io_manager.resolve_driver(route_key) is None:
|
||||
return None
|
||||
|
||||
return await platform_io_manager.send_message(message, route_key)
|
||||
|
||||
def _get_supervisors_for_plugin(self, plugin_id: str) -> List["PluginSupervisor"]:
|
||||
"""返回当前持有指定插件的所有 Supervisor。
|
||||
|
||||
@@ -426,6 +500,11 @@ class PluginRuntimeManager(
|
||||
"""为指定插件生成配置文件变更回调。"""
|
||||
|
||||
async def _callback(changes: Sequence[FileChange]) -> None:
|
||||
"""将 watcher 事件转发到指定插件的配置处理逻辑。
|
||||
|
||||
Args:
|
||||
changes: 当前批次收集到的文件变更列表。
|
||||
"""
|
||||
await self._handle_plugin_config_changes(plugin_id, changes)
|
||||
|
||||
return _callback
|
||||
@@ -542,6 +621,11 @@ class PluginRuntimeManager(
|
||||
# ─── 能力实现注册 ──────────────────────────────────────────
|
||||
|
||||
def _register_capability_impls(self, supervisor: "PluginSupervisor") -> None:
|
||||
"""向指定 Supervisor 注册主程序能力实现。
|
||||
|
||||
Args:
|
||||
supervisor: 需要注册能力实现的目标 Supervisor。
|
||||
"""
|
||||
register_capability_impls(self, supervisor)
|
||||
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import logging as stdlib_logging
|
||||
import time
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ====== 协议常量 ======
|
||||
PROTOCOL_VERSION = "1.0.0"
|
||||
@@ -156,6 +156,8 @@ class RegisterPluginPayload(BaseModel):
|
||||
"""插件版本"""
|
||||
components: List[ComponentDeclaration] = Field(default_factory=list, description="组件列表")
|
||||
"""组件列表"""
|
||||
adapter: Optional["AdapterDeclarationPayload"] = Field(default=None, description="可选的适配器声明")
|
||||
"""可选的适配器声明"""
|
||||
capabilities_required: List[str] = Field(default_factory=list, description="所需能力列表")
|
||||
"""所需能力列表"""
|
||||
|
||||
@@ -285,6 +287,48 @@ class ReloadPluginResultPayload(BaseModel):
|
||||
"""重载失败的插件及原因"""
|
||||
|
||||
|
||||
class AdapterDeclarationPayload(BaseModel):
|
||||
"""适配器插件声明载荷。"""
|
||||
|
||||
platform: str = Field(description="适配器负责的平台名称,例如 qq")
|
||||
"""适配器负责的平台名称,例如 qq"""
|
||||
protocol: str = Field(default="", description="接入协议或实现名称,例如 napcat")
|
||||
"""接入协议或实现名称,例如 napcat"""
|
||||
account_id: str = Field(default="", description="可选的账号 ID 或 self_id")
|
||||
"""可选的账号 ID 或 self_id"""
|
||||
scope: str = Field(default="", description="可选的路由作用域")
|
||||
"""可选的路由作用域"""
|
||||
send_method: str = Field(default="send_to_platform", description="Host 出站调用的插件方法名")
|
||||
"""Host 出站调用的插件方法名"""
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="适配器附加元数据")
|
||||
"""适配器附加元数据"""
|
||||
|
||||
|
||||
class ReceiveExternalMessagePayload(BaseModel):
|
||||
"""适配器插件向 Host 注入外部消息的请求载荷。"""
|
||||
|
||||
message: Dict[str, Any] = Field(description="符合 MessageDict 结构的标准消息字典")
|
||||
"""符合 MessageDict 结构的标准消息字典"""
|
||||
route_metadata: Dict[str, Any] = Field(default_factory=dict, description="可选的路由辅助元数据")
|
||||
"""可选的路由辅助元数据"""
|
||||
external_message_id: str = Field(default="", description="可选的外部平台消息 ID")
|
||||
"""可选的外部平台消息 ID"""
|
||||
dedupe_key: str = Field(default="", description="可选的显式去重键")
|
||||
"""可选的显式去重键"""
|
||||
|
||||
|
||||
class ReceiveExternalMessageResultPayload(BaseModel):
|
||||
"""外部消息注入结果载荷。"""
|
||||
|
||||
accepted: bool = Field(description="Host 是否接受了本次消息注入")
|
||||
"""Host 是否接受了本次消息注入"""
|
||||
route_key: Dict[str, Any] = Field(default_factory=dict, description="本次消息使用的归一路由键")
|
||||
"""本次消息使用的归一路由键"""
|
||||
|
||||
|
||||
RegisterPluginPayload.model_rebuild()
|
||||
|
||||
|
||||
# ====== 日志传输 ======
|
||||
|
||||
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
6. 转发插件的能力调用到 Host
|
||||
"""
|
||||
|
||||
from typing import Any, Callable, Dict, List, Optional, Protocol, Set, Tuple, cast
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Protocol, Set, Tuple, cast
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
@@ -26,6 +25,7 @@ import tomllib
|
||||
from src.common.logger import get_console_handler, get_logger, initialize_logging
|
||||
from src.plugin_runtime import ENV_HOST_VERSION, ENV_IPC_ADDRESS, ENV_PLUGIN_DIRS, ENV_SESSION_TOKEN
|
||||
from src.plugin_runtime.protocol.envelope import (
|
||||
AdapterDeclarationPayload,
|
||||
BootstrapPluginPayload,
|
||||
ComponentDeclaration,
|
||||
Envelope,
|
||||
@@ -227,7 +227,7 @@ class PluginRunner:
|
||||
plugin_id: str = "",
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
) -> Any:
|
||||
"""桥接 PluginContext.call_capability → RPCClient.send_request。
|
||||
"""桥接 PluginContext 的原始 RPC 调用到 Host。
|
||||
|
||||
无论调用方传入何种 plugin_id,实际发往 Host 的 plugin_id
|
||||
始终绑定为当前插件实例,避免伪造其他插件身份申请能力。
|
||||
@@ -237,17 +237,13 @@ class PluginRunner:
|
||||
f"插件 {bound_plugin_id} 尝试以 {plugin_id} 身份发起 RPC,已强制绑定回自身身份"
|
||||
)
|
||||
resp = await rpc_client.send_request(
|
||||
method="cap.call",
|
||||
method=method,
|
||||
plugin_id=bound_plugin_id,
|
||||
payload={
|
||||
"capability": method,
|
||||
"args": payload or {},
|
||||
},
|
||||
payload=payload or {},
|
||||
)
|
||||
# 从响应信封中提取业务结果
|
||||
if resp.error:
|
||||
raise RuntimeError(resp.error.get("message", "能力调用失败"))
|
||||
return resp.payload.get("result")
|
||||
return resp.payload
|
||||
|
||||
ctx = PluginContext(plugin_id=plugin_id, rpc_call=_rpc_call)
|
||||
cast(_ContextAwarePlugin, instance)._set_context(ctx)
|
||||
@@ -286,6 +282,7 @@ class PluginRunner:
|
||||
self._rpc_client.register_method("plugin.invoke_command", self._handle_invoke)
|
||||
self._rpc_client.register_method("plugin.invoke_action", self._handle_invoke)
|
||||
self._rpc_client.register_method("plugin.invoke_tool", self._handle_invoke)
|
||||
self._rpc_client.register_method("plugin.invoke_adapter", self._handle_invoke)
|
||||
self._rpc_client.register_method("plugin.emit_event", self._handle_event_invoke)
|
||||
self._rpc_client.register_method("plugin.invoke_hook", self._handle_hook_invoke)
|
||||
self._rpc_client.register_method("plugin.invoke_workflow_step", self._handle_workflow_step)
|
||||
@@ -306,12 +303,14 @@ class PluginRunner:
|
||||
)
|
||||
|
||||
try:
|
||||
await self._rpc_client.send_request(
|
||||
response = await self._rpc_client.send_request(
|
||||
"plugin.bootstrap",
|
||||
plugin_id=meta.plugin_id,
|
||||
payload=payload.model_dump(),
|
||||
timeout_ms=10000,
|
||||
)
|
||||
if response.error:
|
||||
raise RuntimeError(response.error.get("message", "插件 bootstrap 失败"))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"插件 {meta.plugin_id} bootstrap 失败: {e}")
|
||||
@@ -321,6 +320,29 @@ class PluginRunner:
|
||||
"""撤销 bootstrap 期间为插件签发的能力令牌。"""
|
||||
await self._bootstrap_plugin(meta, capabilities_required=[])
|
||||
|
||||
def _collect_adapter_declaration(self, meta: PluginMeta) -> Optional[AdapterDeclarationPayload]:
|
||||
"""从插件实例中提取适配器声明。
|
||||
|
||||
Args:
|
||||
meta: 待提取声明的插件元数据。
|
||||
|
||||
Returns:
|
||||
Optional[AdapterDeclarationPayload]: 若插件声明了适配器角色,则返回
|
||||
经过校验的适配器声明;否则返回 ``None``。
|
||||
|
||||
Raises:
|
||||
ValueError: 插件导出的适配器声明结构非法时抛出。
|
||||
"""
|
||||
instance = meta.instance
|
||||
if not hasattr(instance, "get_adapter_info"):
|
||||
return None
|
||||
|
||||
adapter_info = instance.get_adapter_info()
|
||||
if adapter_info is None:
|
||||
return None
|
||||
|
||||
return AdapterDeclarationPayload.model_validate(adapter_info)
|
||||
|
||||
async def _register_plugin(self, meta: PluginMeta) -> bool:
|
||||
"""向 Host 注册单个插件。
|
||||
|
||||
@@ -346,20 +368,29 @@ class PluginRunner:
|
||||
for comp_info in instance.get_components()
|
||||
)
|
||||
|
||||
try:
|
||||
adapter = self._collect_adapter_declaration(meta)
|
||||
except Exception as exc:
|
||||
logger.error(f"插件 {meta.plugin_id} 适配器声明非法: {exc}", exc_info=True)
|
||||
return False
|
||||
|
||||
reg_payload = RegisterPluginPayload(
|
||||
plugin_id=meta.plugin_id,
|
||||
plugin_version=meta.version,
|
||||
components=components,
|
||||
adapter=adapter,
|
||||
capabilities_required=meta.capabilities_required,
|
||||
)
|
||||
|
||||
try:
|
||||
_resp = await self._rpc_client.send_request(
|
||||
response = await self._rpc_client.send_request(
|
||||
"plugin.register_components",
|
||||
plugin_id=meta.plugin_id,
|
||||
payload=reg_payload.model_dump(),
|
||||
timeout_ms=10000,
|
||||
)
|
||||
if response.error:
|
||||
raise RuntimeError(response.error.get("message", "插件注册失败"))
|
||||
logger.info(f"插件 {meta.plugin_id} 注册完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user