feat: enhance session ID calculation and plugin management

- Updated `calculate_session_id` method in `SessionUtils` to include optional `account_id` and `scope` parameters for more granular session ID generation.
- Added new environment variables in `plugin_runtime` for external plugin dependencies and global configuration snapshots.
- Introduced methods in `RuntimeComponentManagerProtocol` for loading and reloading plugins globally, accommodating external dependencies.
- Enhanced `PluginRunnerSupervisor` to manage external available plugin IDs during plugin reloads.
- Implemented dependency extraction and management in `PluginRuntimeManager` to handle cross-supervisor dependencies.
- Added tests for session ID calculation and message registration in `ChatManager` to ensure correct behavior with new parameters.
This commit is contained in:
DrSmoothl
2026-03-23 21:48:19 +08:00
parent 7a304ba549
commit 0c508995dd
12 changed files with 765 additions and 170 deletions

View File

@@ -4,17 +4,26 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
import asyncio
import contextlib
import json
import os
import sys
from src.common.logger import get_logger
from src.config.config import global_config
from src.config.config import config_manager, 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.plugin_runtime import ENV_HOST_VERSION, ENV_IPC_ADDRESS, ENV_PLUGIN_DIRS, ENV_SESSION_TOKEN
from src.plugin_runtime import (
ENV_EXTERNAL_PLUGIN_IDS,
ENV_GLOBAL_CONFIG_SNAPSHOT,
ENV_HOST_VERSION,
ENV_IPC_ADDRESS,
ENV_PLUGIN_DIRS,
ENV_SESSION_TOKEN,
)
from src.plugin_runtime.protocol.envelope import (
BootstrapPluginPayload,
ConfigReloadScope,
ConfigUpdatedPayload,
Envelope,
HealthPayload,
@@ -107,6 +116,7 @@ class PluginRunnerSupervisor:
self._runner_process: Optional[asyncio.subprocess.Process] = None
self._registered_plugins: Dict[str, RegisterPluginPayload] = {}
self._message_gateway_states: Dict[str, Dict[str, _MessageGatewayRuntimeState]] = {}
self._external_available_plugin_ids: List[str] = []
self._runner_ready_events: asyncio.Event = asyncio.Event()
self._runner_ready_payloads: RunnerReadyPayload = RunnerReadyPayload()
self._health_task: Optional[asyncio.Task[None]] = None
@@ -156,6 +166,21 @@ class PluginRunnerSupervisor:
"""返回底层 RPC 服务端。"""
return self._rpc_server
def set_external_available_plugin_ids(self, plugin_ids: List[str]) -> None:
"""设置当前 Runner 启动/重载时可视为已满足的外部依赖列表。"""
normalized_plugin_ids = {
str(plugin_id or "").strip()
for plugin_id in plugin_ids
if str(plugin_id or "").strip()
}
self._external_available_plugin_ids = sorted(normalized_plugin_ids)
def get_loaded_plugin_ids(self) -> List[str]:
"""返回当前 Supervisor 已注册的插件 ID 列表。"""
return sorted(self._registered_plugins.keys())
async def dispatch_event(
self,
event_type: str,
@@ -344,12 +369,18 @@ class PluginRunnerSupervisor:
timeout_ms=timeout_ms,
)
async def reload_plugin(self, plugin_id: str, reason: str = "manual") -> bool:
async def reload_plugin(
self,
plugin_id: str,
reason: str = "manual",
external_available_plugins: Optional[List[str]] = None,
) -> bool:
"""按插件 ID 触发精确重载。
Args:
plugin_id: 目标插件 ID。
reason: 重载原因。
external_available_plugins: 视为已满足的外部依赖插件 ID 列表。
Returns:
bool: 是否重载成功。
@@ -358,7 +389,11 @@ class PluginRunnerSupervisor:
response = await self._rpc_server.send_request(
"plugin.reload",
plugin_id=plugin_id,
payload={"plugin_id": plugin_id, "reason": reason},
payload={
"plugin_id": plugin_id,
"reason": reason,
"external_available_plugins": external_available_plugins or self._external_available_plugin_ids,
},
timeout_ms=max(int(self._runner_spawn_timeout * 1000), 10000),
)
except Exception as exc:
@@ -374,12 +409,14 @@ class PluginRunnerSupervisor:
self,
plugin_ids: Optional[List[str]] = None,
reason: str = "manual",
external_available_plugins: Optional[List[str]] = None,
) -> bool:
"""批量重载插件。
Args:
plugin_ids: 目标插件 ID 列表;为空时重载当前已注册的全部插件。
reason: 重载原因。
external_available_plugins: 视为已满足的外部依赖插件 ID 列表。
Returns:
bool: 是否全部重载成功。
@@ -389,7 +426,11 @@ class PluginRunnerSupervisor:
success = True
for plugin_id in ordered_plugin_ids:
reloaded = await self.reload_plugin(plugin_id=plugin_id, reason=reason)
reloaded = await self.reload_plugin(
plugin_id=plugin_id,
reason=reason,
external_available_plugins=external_available_plugins,
)
success = success and reloaded
return success
@@ -399,7 +440,7 @@ class PluginRunnerSupervisor:
plugin_id: str,
config_data: Optional[Dict[str, Any]] = None,
config_version: str = "",
config_scope: str = "self",
config_scope: str | ConfigReloadScope = "self",
) -> bool:
"""向 Runner 推送插件配置更新。
@@ -412,9 +453,15 @@ class PluginRunnerSupervisor:
Returns:
bool: 请求是否成功送达并被 Runner 接受。
"""
try:
normalized_scope = ConfigReloadScope(config_scope)
except ValueError:
logger.warning(f"插件 {plugin_id} 配置更新通知失败: 非法的 config_scope={config_scope}")
return False
payload = ConfigUpdatedPayload(
plugin_id=plugin_id,
config_scope=config_scope,
config_scope=normalized_scope,
config_version=config_version,
config_data=config_data or {},
)
@@ -441,11 +488,11 @@ class PluginRunnerSupervisor:
List[str]: 已声明订阅该范围的插件 ID 列表。
"""
matched_plugins: List[str] = []
for plugin_id, registration in self._registered_plugins.items():
if scope in registration.config_reload_subscriptions:
matched_plugins.append(plugin_id)
return matched_plugins
return [
plugin_id
for plugin_id, registration in self._registered_plugins.items()
if scope in registration.config_reload_subscriptions
]
async def _wait_for_runner_connection(self, timeout_sec: float) -> None:
"""等待 Runner 建立 RPC 连接。
@@ -706,10 +753,7 @@ class PluginRunnerSupervisor:
)
gateways = self._component_registry.get_message_gateways(plugin_id=plugin_id, enabled_only=False)
if len(gateways) == 1:
return gateways[0]
return None
return gateways[0] if len(gateways) == 1 else None
async def _register_message_gateway_driver(
self,
@@ -823,8 +867,7 @@ class PluginRunnerSupervisor:
ValueError: 当平台信息缺失时抛出。
"""
platform = str(payload.platform or gateway_entry.platform or "").strip()
if not platform:
if not (platform := str(payload.platform or gateway_entry.platform or "").strip()):
raise ValueError(f"消息网关 {gateway_entry.full_name} 未提供有效的平台名称")
return RouteKey(
@@ -1090,7 +1133,11 @@ class PluginRunnerSupervisor:
Returns:
Dict[str, str]: 传递给 Runner 进程的环境变量映射。
"""
global_config_snapshot = config_manager.get_global_config().model_dump()
global_config_snapshot["model"] = config_manager.get_model_config().model_dump()
return {
ENV_EXTERNAL_PLUGIN_IDS: json.dumps(self._external_available_plugin_ids, ensure_ascii=False),
ENV_GLOBAL_CONFIG_SNAPSHOT: json.dumps(global_config_snapshot, ensure_ascii=False),
ENV_HOST_VERSION: PROTOCOL_VERSION,
ENV_IPC_ADDRESS: self._transport.get_address(),
ENV_PLUGIN_DIRS: os.pathsep.join(str(path) for path in self._plugin_dirs),
@@ -1136,8 +1183,7 @@ class PluginRunnerSupervisor:
line = await stream.readline()
if not line:
return
message = line.decode("utf-8", errors="replace").rstrip()
if message:
if message := line.decode("utf-8", errors="replace").rstrip():
logger.warning(f"[runner-stderr] {message}")
except asyncio.CancelledError:
raise