Refactor plugin runtime components and enhance message handling

- Removed unused core action mirror functionality from PluginRunnerSupervisor.
- Simplified action and command execution logic in send_service.py.
- Introduced ComponentQueryService for unified component querying in plugin runtime.
- Enhanced message component handling with new binary component support.
- Improved message sequence construction and detection of outbound message flags.
- Updated methods for sending messages to streamline the process and improve readability.
This commit is contained in:
DrSmoothl
2026-03-23 16:14:13 +08:00
parent d07915eea0
commit 18a0e7664a
15 changed files with 1255 additions and 853 deletions

View File

@@ -238,14 +238,14 @@ class RuntimeCoreCapabilityMixin:
return {"success": False, "value": None, "error": str(e)}
async def _cap_config_get_plugin(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
from src.core.component_registry import component_registry as core_registry
from src.plugin_runtime.component_query import component_query_service
plugin_name: str = args.get("plugin_name", plugin_id)
key: str = args.get("key", "")
default = args.get("default")
try:
config = core_registry.get_plugin_config(plugin_name)
config = component_query_service.get_plugin_config(plugin_name)
if config is None:
return {"success": False, "value": default, "error": f"未找到插件 {plugin_name} 的配置"}
@@ -258,11 +258,11 @@ class RuntimeCoreCapabilityMixin:
return {"success": False, "value": default, "error": str(e)}
async def _cap_config_get_all(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
from src.core.component_registry import component_registry as core_registry
from src.plugin_runtime.component_query import component_query_service
plugin_name: str = args.get("plugin_name", plugin_id)
try:
config = core_registry.get_plugin_config(plugin_name)
config = component_query_service.get_plugin_config(plugin_name)
if config is None:
return {"success": True, "value": {}}
return {"success": True, "value": config}

View File

@@ -648,10 +648,10 @@ class RuntimeDataCapabilityMixin:
return {"success": False, "error": str(e)}
async def _cap_tool_get_definitions(self, plugin_id: str, capability: str, args: Dict[str, Any]) -> Any:
from src.core.component_registry import component_registry as core_registry
from src.plugin_runtime.component_query import component_query_service
try:
tools = core_registry.get_llm_available_tools()
tools = component_query_service.get_llm_available_tools()
return {
"success": True,
"tools": [{"name": name, "definition": info.get_llm_definition()} for name, info in tools.items()],

View File

@@ -0,0 +1,709 @@
"""插件运行时统一组件查询服务。
该模块统一从插件运行时的 Host ComponentRegistry 中聚合只读视图,
供 HFC/PFC、Planner、ToolExecutor 和运行时能力层查询与调用。
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Tuple
from src.common.logger import get_logger
from src.core.types import ActionActivationType, ActionInfo, CommandInfo, ComponentInfo, ComponentType, ToolInfo
from src.llm_models.payload_content.tool_option import ToolParamType
if TYPE_CHECKING:
from src.plugin_runtime.host.component_registry import ActionEntry, CommandEntry, ComponentEntry, ToolEntry
from src.plugin_runtime.host.supervisor import PluginSupervisor
from src.plugin_runtime.integration import PluginRuntimeManager
logger = get_logger("plugin_runtime.component_query")
ActionExecutor = Callable[..., Awaitable[Any]]
CommandExecutor = Callable[..., Awaitable[Tuple[bool, Optional[str], bool]]]
ToolExecutor = Callable[..., Awaitable[Any]]
_HOST_COMPONENT_TYPE_MAP: Dict[ComponentType, str] = {
ComponentType.ACTION: "ACTION",
ComponentType.COMMAND: "COMMAND",
ComponentType.TOOL: "TOOL",
}
_TOOL_PARAM_TYPE_MAP: Dict[str, ToolParamType] = {
"string": ToolParamType.STRING,
"integer": ToolParamType.INTEGER,
"float": ToolParamType.FLOAT,
"boolean": ToolParamType.BOOLEAN,
"bool": ToolParamType.BOOLEAN,
}
class ComponentQueryService:
"""插件运行时统一组件查询服务。
该对象不维护独立状态,只读取插件系统中的注册结果。
所有注册、删除、配置写入等写操作都被显式禁用。
"""
@staticmethod
def _get_runtime_manager() -> "PluginRuntimeManager":
"""获取插件运行时管理器单例。
Returns:
PluginRuntimeManager: 当前全局插件运行时管理器。
"""
from src.plugin_runtime.integration import get_plugin_runtime_manager
return get_plugin_runtime_manager()
def _iter_supervisors(self) -> list["PluginSupervisor"]:
"""获取当前所有活跃的插件运行时监督器。
Returns:
list[PluginSupervisor]: 当前运行中的监督器列表。
"""
runtime_manager = self._get_runtime_manager()
return list(runtime_manager.supervisors)
def _iter_component_entries(
self,
component_type: ComponentType,
*,
enabled_only: bool = True,
) -> list[tuple["PluginSupervisor", "ComponentEntry"]]:
"""遍历指定类型的全部组件条目。
Args:
component_type: 目标组件类型。
enabled_only: 是否仅返回启用状态的组件。
Returns:
list[tuple[PluginSupervisor, ComponentEntry]]: ``(监督器, 组件条目)`` 列表。
"""
host_component_type = _HOST_COMPONENT_TYPE_MAP.get(component_type)
if host_component_type is None:
return []
collected_entries: list[tuple["PluginSupervisor", "ComponentEntry"]] = []
for supervisor in self._iter_supervisors():
for component in supervisor.component_registry.get_components_by_type(
host_component_type,
enabled_only=enabled_only,
):
collected_entries.append((supervisor, component))
return collected_entries
@staticmethod
def _coerce_action_activation_type(raw_value: Any) -> ActionActivationType:
"""规范化动作激活类型。
Args:
raw_value: 原始激活类型值。
Returns:
ActionActivationType: 规范化后的激活类型枚举。
"""
normalized_value = str(raw_value or "").strip().lower()
if normalized_value == ActionActivationType.NEVER.value:
return ActionActivationType.NEVER
if normalized_value == ActionActivationType.RANDOM.value:
return ActionActivationType.RANDOM
if normalized_value == ActionActivationType.KEYWORD.value:
return ActionActivationType.KEYWORD
return ActionActivationType.ALWAYS
@staticmethod
def _coerce_float(value: Any, default: float = 0.0) -> float:
"""将任意值安全转换为浮点数。
Args:
value: 待转换的输入值。
default: 转换失败时返回的默认值。
Returns:
float: 转换后的浮点结果。
"""
try:
return float(value)
except (TypeError, ValueError):
return default
@staticmethod
def _build_action_info(entry: "ActionEntry") -> ActionInfo:
"""将运行时 Action 条目转换为核心动作信息。
Args:
entry: 插件运行时中的 Action 条目。
Returns:
ActionInfo: 供核心 Planner 使用的动作信息。
"""
metadata = dict(entry.metadata)
raw_action_parameters = metadata.get("action_parameters")
action_parameters = (
{
str(param_name): str(param_description)
for param_name, param_description in raw_action_parameters.items()
}
if isinstance(raw_action_parameters, dict)
else {}
)
action_require = [
str(item)
for item in (metadata.get("action_require") or [])
if item is not None and str(item).strip()
]
associated_types = [
str(item)
for item in (metadata.get("associated_types") or [])
if item is not None and str(item).strip()
]
activation_keywords = [
str(item)
for item in (metadata.get("activation_keywords") or [])
if item is not None and str(item).strip()
]
return ActionInfo(
name=entry.name,
component_type=ComponentType.ACTION,
description=str(metadata.get("description", "") or ""),
enabled=bool(entry.enabled),
plugin_name=entry.plugin_id,
metadata=metadata,
action_parameters=action_parameters,
action_require=action_require,
associated_types=associated_types,
activation_type=ComponentQueryService._coerce_action_activation_type(metadata.get("activation_type")),
random_activation_probability=ComponentQueryService._coerce_float(
metadata.get("activation_probability"),
0.0,
),
activation_keywords=activation_keywords,
parallel_action=bool(metadata.get("parallel_action", False)),
)
@staticmethod
def _build_command_info(entry: "CommandEntry") -> CommandInfo:
"""将运行时 Command 条目转换为核心命令信息。
Args:
entry: 插件运行时中的 Command 条目。
Returns:
CommandInfo: 供核心命令链使用的命令信息。
"""
metadata = dict(entry.metadata)
return CommandInfo(
name=entry.name,
component_type=ComponentType.COMMAND,
description=str(metadata.get("description", "") or ""),
enabled=bool(entry.enabled),
plugin_name=entry.plugin_id,
metadata=metadata,
command_pattern=str(metadata.get("command_pattern", "") or ""),
)
@staticmethod
def _coerce_tool_param_type(raw_value: Any) -> ToolParamType:
"""规范化工具参数类型。
Args:
raw_value: 原始工具参数类型值。
Returns:
ToolParamType: 规范化后的工具参数类型。
"""
normalized_value = str(raw_value or "").strip().lower()
return _TOOL_PARAM_TYPE_MAP.get(normalized_value, ToolParamType.STRING)
@staticmethod
def _build_tool_parameters(entry: "ToolEntry") -> list[tuple[str, ToolParamType, str, bool, list[str] | None]]:
"""将运行时工具参数元数据转换为核心 ToolInfo 参数列表。
Args:
entry: 插件运行时中的 Tool 条目。
Returns:
list[tuple[str, ToolParamType, str, bool, list[str] | None]]: 转换后的参数列表。
"""
structured_parameters = entry.parameters if isinstance(entry.parameters, list) else []
if not structured_parameters and isinstance(entry.parameters_raw, dict):
structured_parameters = [
{"name": key, **value}
for key, value in entry.parameters_raw.items()
if isinstance(value, dict)
]
normalized_parameters: list[tuple[str, ToolParamType, str, bool, list[str] | None]] = []
for parameter in structured_parameters:
if not isinstance(parameter, dict):
continue
parameter_name = str(parameter.get("name", "") or "").strip()
if not parameter_name:
continue
enum_values = parameter.get("enum")
normalized_enum_values = (
[str(item) for item in enum_values if item is not None]
if isinstance(enum_values, list)
else None
)
normalized_parameters.append(
(
parameter_name,
ComponentQueryService._coerce_tool_param_type(parameter.get("param_type") or parameter.get("type")),
str(parameter.get("description", "") or ""),
bool(parameter.get("required", True)),
normalized_enum_values,
)
)
return normalized_parameters
@staticmethod
def _build_tool_info(entry: "ToolEntry") -> ToolInfo:
"""将运行时 Tool 条目转换为核心工具信息。
Args:
entry: 插件运行时中的 Tool 条目。
Returns:
ToolInfo: 供 ToolExecutor 与能力层使用的工具信息。
"""
return ToolInfo(
name=entry.name,
component_type=ComponentType.TOOL,
description=entry.description,
enabled=bool(entry.enabled),
plugin_name=entry.plugin_id,
metadata=dict(entry.metadata),
tool_parameters=ComponentQueryService._build_tool_parameters(entry),
tool_description=entry.description,
)
@staticmethod
def _log_duplicate_component(component_type: ComponentType, component_name: str) -> None:
"""记录重复组件名称冲突。
Args:
component_type: 组件类型。
component_name: 发生冲突的组件名称。
"""
logger.warning(f"检测到重复{component_type.value}名称 {component_name},将只保留首个匹配项")
def _get_unique_component_entry(
self,
component_type: ComponentType,
name: str,
) -> Optional[tuple["PluginSupervisor", "ComponentEntry"]]:
"""按组件短名解析唯一条目。
Args:
component_type: 目标组件类型。
name: 组件短名。
Returns:
Optional[tuple[PluginSupervisor, ComponentEntry]]: 唯一命中的组件条目。
"""
matched_entries = [
(supervisor, entry)
for supervisor, entry in self._iter_component_entries(component_type)
if entry.name == name
]
if not matched_entries:
return None
if len(matched_entries) > 1:
self._log_duplicate_component(component_type, name)
return matched_entries[0]
def _collect_unique_component_infos(
self,
component_type: ComponentType,
) -> Dict[str, ComponentInfo]:
"""收集某类组件的唯一信息视图。
Args:
component_type: 目标组件类型。
Returns:
Dict[str, ComponentInfo]: 组件名到核心组件信息的映射。
"""
collected_components: Dict[str, ComponentInfo] = {}
for _supervisor, entry in self._iter_component_entries(component_type):
if entry.name in collected_components:
self._log_duplicate_component(component_type, entry.name)
continue
if component_type == ComponentType.ACTION:
collected_components[entry.name] = self._build_action_info(entry) # type: ignore[arg-type]
elif component_type == ComponentType.COMMAND:
collected_components[entry.name] = self._build_command_info(entry) # type: ignore[arg-type]
elif component_type == ComponentType.TOOL:
collected_components[entry.name] = self._build_tool_info(entry) # type: ignore[arg-type]
return collected_components
@staticmethod
def _extract_stream_id_from_action_kwargs(kwargs: Dict[str, Any]) -> str:
"""从旧 ActionManager 参数中提取聊天流 ID。
Args:
kwargs: 旧动作执行器收到的关键字参数。
Returns:
str: 提取出的 ``stream_id``。
"""
chat_stream = kwargs.get("chat_stream")
if chat_stream is not None:
try:
return str(chat_stream.session_id)
except AttributeError:
pass
return str(kwargs.get("stream_id", "") or "")
@staticmethod
def _build_action_executor(supervisor: "PluginSupervisor", plugin_id: str, component_name: str) -> ActionExecutor:
"""构造动作执行 RPC 闭包。
Args:
supervisor: 负责该组件的监督器。
plugin_id: 插件 ID。
component_name: 组件名称。
Returns:
ActionExecutor: 兼容旧 Planner 的异步执行器。
"""
async def _executor(**kwargs: Any) -> tuple[bool, str]:
"""将核心动作调用桥接到插件运行时。
Args:
**kwargs: 旧 ActionManager 传入的上下文参数。
Returns:
tuple[bool, str]: ``(是否成功, 结果说明)``。
"""
invoke_args: Dict[str, Any] = {}
action_data = kwargs.get("action_data")
if isinstance(action_data, dict):
invoke_args.update(action_data)
stream_id = ComponentQueryService._extract_stream_id_from_action_kwargs(kwargs)
invoke_args["action_data"] = action_data if isinstance(action_data, dict) else {}
invoke_args["stream_id"] = stream_id
invoke_args["chat_id"] = stream_id
invoke_args["reasoning"] = str(kwargs.get("action_reasoning", "") or "")
if (thinking_id := kwargs.get("thinking_id")) is not None:
invoke_args["thinking_id"] = str(thinking_id)
if isinstance(kwargs.get("cycle_timers"), dict):
invoke_args["cycle_timers"] = kwargs["cycle_timers"]
if isinstance(kwargs.get("plugin_config"), dict):
invoke_args["plugin_config"] = kwargs["plugin_config"]
if isinstance(kwargs.get("log_prefix"), str):
invoke_args["log_prefix"] = kwargs["log_prefix"]
if isinstance(kwargs.get("shutting_down"), bool):
invoke_args["shutting_down"] = kwargs["shutting_down"]
try:
response = await supervisor.invoke_plugin(
method="plugin.invoke_action",
plugin_id=plugin_id,
component_name=component_name,
args=invoke_args,
timeout_ms=30000,
)
except Exception as exc:
logger.error(f"运行时 Action {plugin_id}.{component_name} 执行失败: {exc}", exc_info=True)
return False, str(exc)
payload = response.payload if isinstance(response.payload, dict) else {}
success = bool(payload.get("success", False))
result = payload.get("result")
if isinstance(result, (list, tuple)):
if len(result) >= 2:
return bool(result[0]), "" if result[1] is None else str(result[1])
if len(result) == 1:
return bool(result[0]), ""
if success:
return True, "" if result is None else str(result)
return False, "" if result is None else str(result)
return _executor
@staticmethod
def _build_command_executor(
supervisor: "PluginSupervisor",
plugin_id: str,
component_name: str,
metadata: Dict[str, Any],
) -> CommandExecutor:
"""构造命令执行 RPC 闭包。
Args:
supervisor: 负责该组件的监督器。
plugin_id: 插件 ID。
component_name: 组件名称。
metadata: 命令组件元数据。
Returns:
CommandExecutor: 兼容旧消息命令链的执行器。
"""
async def _executor(**kwargs: Any) -> tuple[bool, Optional[str], bool]:
"""将核心命令调用桥接到插件运行时。
Args:
**kwargs: 命令执行上下文参数。
Returns:
tuple[bool, Optional[str], bool]: ``(是否成功, 返回文本, 是否拦截后续消息)``。
"""
message = kwargs.get("message")
matched_groups = kwargs.get("matched_groups")
plugin_config = kwargs.get("plugin_config")
invoke_args: Dict[str, Any] = {
"text": str(getattr(message, "processed_plain_text", "") or ""),
"stream_id": str(getattr(message, "session_id", "") or ""),
"matched_groups": matched_groups if isinstance(matched_groups, dict) else {},
}
if isinstance(plugin_config, dict):
invoke_args["plugin_config"] = plugin_config
try:
response = await supervisor.invoke_plugin(
method="plugin.invoke_command",
plugin_id=plugin_id,
component_name=component_name,
args=invoke_args,
timeout_ms=30000,
)
except Exception as exc:
logger.error(f"运行时 Command {plugin_id}.{component_name} 执行失败: {exc}", exc_info=True)
return False, str(exc), True
payload = response.payload if isinstance(response.payload, dict) else {}
success = bool(payload.get("success", False))
result = payload.get("result")
intercept = bool(metadata.get("intercept_message_level", 0))
response_text: Optional[str]
if isinstance(result, (list, tuple)) and len(result) >= 3:
response_text = None if result[1] is None else str(result[1])
intercept = bool(result[2])
else:
response_text = None if result is None else str(result)
return success, response_text, intercept
return _executor
@staticmethod
def _build_tool_executor(supervisor: "PluginSupervisor", plugin_id: str, component_name: str) -> ToolExecutor:
"""构造工具执行 RPC 闭包。
Args:
supervisor: 负责该组件的监督器。
plugin_id: 插件 ID。
component_name: 组件名称。
Returns:
ToolExecutor: 兼容旧 ToolExecutor 的异步执行器。
"""
async def _executor(function_args: Dict[str, Any]) -> Any:
"""将核心工具调用桥接到插件运行时。
Args:
function_args: 工具调用参数。
Returns:
Any: 插件工具返回结果;若结果不是字典,则会包装为 ``{"content": ...}``。
"""
try:
response = await supervisor.invoke_plugin(
method="plugin.invoke_tool",
plugin_id=plugin_id,
component_name=component_name,
args=function_args,
timeout_ms=30000,
)
except Exception as exc:
logger.error(f"运行时 Tool {plugin_id}.{component_name} 执行失败: {exc}", exc_info=True)
return {"content": f"工具 {component_name} 执行失败: {exc}"}
payload = response.payload if isinstance(response.payload, dict) else {}
result = payload.get("result")
if isinstance(result, dict):
return result
return {"content": "" if result is None else str(result)}
return _executor
def get_action_info(self, name: str) -> Optional[ActionInfo]:
"""获取指定动作的信息。
Args:
name: 动作名称。
Returns:
Optional[ActionInfo]: 匹配到的动作信息。
"""
matched_entry = self._get_unique_component_entry(ComponentType.ACTION, name)
if matched_entry is None:
return None
_supervisor, entry = matched_entry
return self._build_action_info(entry) # type: ignore[arg-type]
def get_action_executor(self, name: str) -> Optional[ActionExecutor]:
"""获取指定动作的执行器。
Args:
name: 动作名称。
Returns:
Optional[ActionExecutor]: 运行时 RPC 执行闭包。
"""
matched_entry = self._get_unique_component_entry(ComponentType.ACTION, name)
if matched_entry is None:
return None
supervisor, entry = matched_entry
return self._build_action_executor(supervisor, entry.plugin_id, entry.name)
def get_default_actions(self) -> Dict[str, ActionInfo]:
"""获取当前默认启用的动作集合。
Returns:
Dict[str, ActionInfo]: 动作名到动作信息的映射。
"""
action_infos = self._collect_unique_component_infos(ComponentType.ACTION)
return {name: info for name, info in action_infos.items() if isinstance(info, ActionInfo) and info.enabled}
def find_command_by_text(self, text: str) -> Optional[Tuple[CommandExecutor, dict, CommandInfo]]:
"""根据文本查找匹配的命令。
Args:
text: 待匹配的文本内容。
Returns:
Optional[Tuple[CommandExecutor, dict, CommandInfo]]: 匹配结果。
"""
for supervisor in self._iter_supervisors():
match_result = supervisor.component_registry.find_command_by_text(text)
if match_result is None:
continue
entry, matched_groups = match_result
command_info = self._build_command_info(entry) # type: ignore[arg-type]
command_executor = self._build_command_executor(
supervisor,
entry.plugin_id,
entry.name,
dict(entry.metadata),
)
return command_executor, matched_groups, command_info
return None
def get_tool_info(self, name: str) -> Optional[ToolInfo]:
"""获取指定工具的信息。
Args:
name: 工具名称。
Returns:
Optional[ToolInfo]: 匹配到的工具信息。
"""
matched_entry = self._get_unique_component_entry(ComponentType.TOOL, name)
if matched_entry is None:
return None
_supervisor, entry = matched_entry
return self._build_tool_info(entry) # type: ignore[arg-type]
def get_tool_executor(self, name: str) -> Optional[ToolExecutor]:
"""获取指定工具的执行器。
Args:
name: 工具名称。
Returns:
Optional[ToolExecutor]: 运行时 RPC 执行闭包。
"""
matched_entry = self._get_unique_component_entry(ComponentType.TOOL, name)
if matched_entry is None:
return None
supervisor, entry = matched_entry
return self._build_tool_executor(supervisor, entry.plugin_id, entry.name)
def get_llm_available_tools(self) -> Dict[str, ToolInfo]:
"""获取当前可供 LLM 选择的工具集合。
Returns:
Dict[str, ToolInfo]: 工具名到工具信息的映射。
"""
tool_infos = self._collect_unique_component_infos(ComponentType.TOOL)
return {name: info for name, info in tool_infos.items() if isinstance(info, ToolInfo) and info.enabled}
def get_components_by_type(self, component_type: ComponentType) -> Dict[str, ComponentInfo]:
"""获取某类组件的全部信息。
Args:
component_type: 组件类型。
Returns:
Dict[str, ComponentInfo]: 组件名到组件信息的映射。
"""
return self._collect_unique_component_infos(component_type)
def get_plugin_config(self, plugin_name: str) -> Optional[dict]:
"""读取指定插件的配置文件内容。
Args:
plugin_name: 插件名称。
Returns:
Optional[dict]: 读取成功时返回配置字典;未找到时返回 ``None``。
"""
runtime_manager = self._get_runtime_manager()
try:
supervisor = runtime_manager._get_supervisor_for_plugin(plugin_name)
except RuntimeError as exc:
logger.error(f"读取插件配置失败: {exc}")
return None
if supervisor is None:
return None
try:
return runtime_manager._load_plugin_config_for_supervisor(supervisor, plugin_name)
except Exception as exc:
logger.error(f"读取插件 {plugin_name} 配置失败: {exc}", exc_info=True)
return None
component_query_service = ComponentQueryService()

View File

@@ -31,12 +31,12 @@ class ComponentTypes(str, Enum):
class StatusDict(TypedDict):
total: int
ACTION: int
COMMAND: int
TOOL: int
EVENT_HANDLER: int
HOOK_HANDLER: int
MESSAGE_GATEWAY: int
action: int
command: int
tool: int
event_handler: int
hook_handler: int
message_gateway: int
plugins: int
@@ -185,6 +185,23 @@ class ComponentRegistry:
# 按插件索引
self._by_plugin: Dict[str, List[ComponentEntry]] = {}
@staticmethod
def _normalize_component_type(component_type: str) -> ComponentTypes:
"""规范化组件类型输入。
Args:
component_type: 原始组件类型字符串。
Returns:
ComponentTypes: 规范化后的组件类型枚举。
Raises:
ValueError: 当组件类型不受支持时抛出。
"""
normalized_value = str(component_type or "").strip().upper()
return ComponentTypes(normalized_value)
def clear(self) -> None:
"""清空全部组件注册状态。"""
self._components.clear()
@@ -205,18 +222,19 @@ class ComponentRegistry:
success (bool): 是否成功注册(失败原因通常是组件类型无效)
"""
try:
if component_type == ComponentTypes.ACTION:
comp = ActionEntry(name, component_type, plugin_id, metadata)
elif component_type == ComponentTypes.COMMAND:
comp = CommandEntry(name, component_type, plugin_id, metadata)
elif component_type == ComponentTypes.TOOL:
comp = ToolEntry(name, component_type, plugin_id, metadata)
elif component_type == ComponentTypes.EVENT_HANDLER:
comp = EventHandlerEntry(name, component_type, plugin_id, metadata)
elif component_type == ComponentTypes.HOOK_HANDLER:
comp = HookHandlerEntry(name, component_type, plugin_id, metadata)
elif component_type == ComponentTypes.MESSAGE_GATEWAY:
comp = MessageGatewayEntry(name, component_type, plugin_id, metadata)
normalized_type = self._normalize_component_type(component_type)
if normalized_type == ComponentTypes.ACTION:
comp = ActionEntry(name, normalized_type.value, plugin_id, metadata)
elif normalized_type == ComponentTypes.COMMAND:
comp = CommandEntry(name, normalized_type.value, plugin_id, metadata)
elif normalized_type == ComponentTypes.TOOL:
comp = ToolEntry(name, normalized_type.value, plugin_id, metadata)
elif normalized_type == ComponentTypes.EVENT_HANDLER:
comp = EventHandlerEntry(name, normalized_type.value, plugin_id, metadata)
elif normalized_type == ComponentTypes.HOOK_HANDLER:
comp = HookHandlerEntry(name, normalized_type.value, plugin_id, metadata)
elif normalized_type == ComponentTypes.MESSAGE_GATEWAY:
comp = MessageGatewayEntry(name, normalized_type.value, plugin_id, metadata)
else:
raise ValueError(f"组件类型 {component_type} 不存在")
except ValueError:
@@ -304,6 +322,20 @@ class ComponentRegistry:
comp.enabled = enabled
return True
def set_component_enabled(self, full_name: str, enabled: bool, session_id: Optional[str] = None) -> bool:
"""设置指定组件的启用状态。
Args:
full_name: 组件全名。
enabled: 目标启用状态。
session_id: 可选的会话 ID仅对该会话生效。
Returns:
bool: 是否设置成功。
"""
return self.toggle_component_status(full_name, enabled, session_id=session_id)
def toggle_plugin_status(self, plugin_id: str, enabled: bool, session_id: Optional[str] = None) -> int:
"""批量启用或禁用某插件的所有组件。
@@ -348,7 +380,7 @@ class ComponentRegistry:
components (List[ComponentEntry]): 组件条目列表
"""
try:
comp_type = ComponentTypes(component_type)
comp_type = self._normalize_component_type(component_type)
except ValueError:
logger.error(f"组件类型 {component_type} 不存在")
raise
@@ -536,6 +568,6 @@ class ComponentRegistry:
"""
stats: StatusDict = {"total": len(self._components)} # type: ignore
for comp_type, type_dict in self._by_type.items():
stats[comp_type.value] = len(type_dict)
stats[comp_type.value.lower()] = len(type_dict)
stats["plugins"] = len(self._by_plugin)
return stats

View File

@@ -9,8 +9,6 @@ import sys
from src.common.logger import get_logger
from src.config.config import global_config
from src.core.component_registry import component_registry as core_component_registry
from src.core.types import ActionActivationType, ActionInfo, ComponentType as CoreComponentType
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
@@ -107,7 +105,6 @@ 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._mirrored_core_actions: Dict[str, List[str]] = {}
self._runner_ready_events: asyncio.Event = asyncio.Event()
self._runner_ready_payloads: RunnerReadyPayload = RunnerReadyPayload()
self._health_task: Optional[asyncio.Task[None]] = None
@@ -510,7 +507,6 @@ class PluginRunnerSupervisor:
except Exception as exc:
return envelope.make_error_response(ErrorCode.E_BAD_PAYLOAD.value, str(exc))
self._remove_core_action_mirrors(payload.plugin_id)
self._component_registry.remove_components_by_plugin(payload.plugin_id)
await self._unregister_all_message_gateway_drivers_for_plugin(payload.plugin_id)
@@ -520,7 +516,6 @@ class PluginRunnerSupervisor:
)
self._registered_plugins[payload.plugin_id] = payload
self._message_gateway_states[payload.plugin_id] = {}
self._mirror_runtime_actions_to_core_registry(payload)
return envelope.make_response(
payload={
@@ -550,7 +545,6 @@ 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
self._remove_core_action_mirrors(payload.plugin_id)
await self._unregister_all_message_gateway_drivers_for_plugin(payload.plugin_id)
self._message_gateway_states.pop(payload.plugin_id, None)
@@ -564,236 +558,6 @@ class PluginRunnerSupervisor:
}
)
@staticmethod
def _coerce_action_activation_type(raw_value: Any) -> ActionActivationType:
"""将运行时 Action 激活类型转换为旧核心枚举。
Args:
raw_value: 插件运行时声明中的激活类型值。
Returns:
ActionActivationType: 可供旧 Planner 使用的激活类型枚举。
"""
normalized_value = str(raw_value or ActionActivationType.ALWAYS.value).strip().lower()
try:
return ActionActivationType(normalized_value)
except ValueError:
return ActionActivationType.ALWAYS
@staticmethod
def _coerce_float(value: Any, default: float = 0.0) -> float:
"""将任意输入尽量转换为浮点数。
Args:
value: 待转换的值。
default: 转换失败时使用的默认值。
Returns:
float: 转换结果。
"""
try:
return float(value)
except (TypeError, ValueError):
return default
@staticmethod
def _build_core_action_info(plugin_id: str, component_name: str, metadata: Dict[str, Any]) -> ActionInfo:
"""将运行时 Action 元数据映射为旧核心 ActionInfo。
Args:
plugin_id: 插件 ID。
component_name: 组件名称。
metadata: 运行时组件元数据。
Returns:
ActionInfo: 兼容旧 Planner 的动作定义。
"""
activation_keywords = [
str(item)
for item in (metadata.get("activation_keywords") or [])
if item is not None and str(item).strip()
]
action_require = [
str(item)
for item in (metadata.get("action_require") or [])
if item is not None and str(item).strip()
]
associated_types = [
str(item)
for item in (metadata.get("associated_types") or [])
if item is not None and str(item).strip()
]
raw_action_parameters = metadata.get("action_parameters") or {}
action_parameters = {
str(param_name): str(param_description)
for param_name, param_description in raw_action_parameters.items()
} if isinstance(raw_action_parameters, dict) else {}
return ActionInfo(
name=component_name,
component_type=CoreComponentType.ACTION,
description=str(metadata.get("description", "") or ""),
enabled=bool(metadata.get("enabled", True)),
plugin_name=plugin_id,
metadata=dict(metadata),
action_parameters=action_parameters,
action_require=action_require,
associated_types=associated_types,
activation_type=PluginRunnerSupervisor._coerce_action_activation_type(metadata.get("activation_type")),
random_activation_probability=PluginRunnerSupervisor._coerce_float(
metadata.get("activation_probability"),
0.0,
),
activation_keywords=activation_keywords,
parallel_action=bool(metadata.get("parallel_action", False)),
)
@staticmethod
def _extract_stream_id_from_action_kwargs(kwargs: Dict[str, Any]) -> str:
"""从旧 ActionManager 传入参数中提取聊天流 ID。
Args:
kwargs: 旧动作执行器收到的关键字参数。
Returns:
str: 可用于新运行时 Action 的 ``stream_id``。
"""
chat_stream = kwargs.get("chat_stream")
if chat_stream is not None:
try:
return str(chat_stream.session_id)
except AttributeError:
pass
raw_stream_id = kwargs.get("stream_id", "")
return str(raw_stream_id or "")
def _build_runtime_action_executor(
self,
plugin_id: str,
component_name: str,
) -> Any:
"""构造一个转发到 plugin runtime 的旧核心 Action 执行器。
Args:
plugin_id: 目标插件 ID。
component_name: 目标 Action 组件名称。
Returns:
Callable[..., Coroutine[Any, Any, tuple[bool, str]]]: 兼容旧 ActionManager 的执行器。
"""
async def _executor(**kwargs: Any) -> tuple[bool, str]:
"""将旧 Planner 的动作调用桥接到 plugin runtime。
Args:
**kwargs: 旧 ActionManager 传入的运行时上下文参数。
Returns:
tuple[bool, str]: ``(是否成功, 动作说明)``。
"""
invoke_args: Dict[str, Any] = {}
action_data = kwargs.get("action_data")
if isinstance(action_data, dict):
invoke_args.update(action_data)
stream_id = self._extract_stream_id_from_action_kwargs(kwargs)
invoke_args["action_data"] = action_data if isinstance(action_data, dict) else {}
invoke_args["stream_id"] = stream_id
invoke_args["chat_id"] = stream_id
invoke_args["reasoning"] = str(kwargs.get("action_reasoning", "") or "")
thinking_id = kwargs.get("thinking_id")
if thinking_id is not None:
invoke_args["thinking_id"] = str(thinking_id)
cycle_timers = kwargs.get("cycle_timers")
if isinstance(cycle_timers, dict):
invoke_args["cycle_timers"] = cycle_timers
plugin_config = kwargs.get("plugin_config")
if isinstance(plugin_config, dict):
invoke_args["plugin_config"] = plugin_config
log_prefix = kwargs.get("log_prefix")
if isinstance(log_prefix, str):
invoke_args["log_prefix"] = log_prefix
shutting_down = kwargs.get("shutting_down")
if isinstance(shutting_down, bool):
invoke_args["shutting_down"] = shutting_down
try:
response = await self.invoke_plugin(
method="plugin.invoke_action",
plugin_id=plugin_id,
component_name=component_name,
args=invoke_args,
timeout_ms=30000,
)
except Exception as exc:
logger.error(f"运行时 Action {plugin_id}.{component_name} 执行失败: {exc}", exc_info=True)
return False, str(exc)
payload = response.payload if isinstance(response.payload, dict) else {}
success = bool(payload.get("success", False))
result = payload.get("result")
if isinstance(result, (list, tuple)):
if len(result) >= 2:
return bool(result[0]), "" if result[1] is None else str(result[1])
if len(result) == 1:
return bool(result[0]), ""
if success:
return True, "" if result is None else str(result)
return False, "" if result is None else str(result)
return _executor
def _mirror_runtime_actions_to_core_registry(self, payload: RegisterPluginPayload) -> None:
"""将 plugin runtime 中声明的 Action 镜像到旧核心注册表。
Args:
payload: 当前插件的注册载荷。
"""
mirrored_action_names: List[str] = []
for component in payload.components:
if str(component.component_type).upper() != CoreComponentType.ACTION.name:
continue
action_info = self._build_core_action_info(
plugin_id=payload.plugin_id,
component_name=component.name,
metadata=component.metadata,
)
action_executor = self._build_runtime_action_executor(
plugin_id=payload.plugin_id,
component_name=component.name,
)
registered = core_component_registry.register_action(action_info, action_executor)
if not registered:
logger.warning(
f"运行时 Action {payload.plugin_id}.{component.name} 无法镜像到旧核心注册表,"
"可能与现有 Action 重名"
)
continue
mirrored_action_names.append(component.name)
if mirrored_action_names:
self._mirrored_core_actions[payload.plugin_id] = mirrored_action_names
def _remove_core_action_mirrors(self, plugin_id: str) -> None:
"""移除某个插件镜像到旧核心注册表的所有 Action。
Args:
plugin_id: 目标插件 ID。
"""
mirrored_action_names = self._mirrored_core_actions.pop(plugin_id, [])
for action_name in mirrored_action_names:
core_component_registry.remove_action(action_name)
@staticmethod
def _build_message_gateway_driver_id(plugin_id: str, gateway_name: str) -> str:
"""构造消息网关驱动 ID。
@@ -1407,8 +1171,6 @@ class PluginRunnerSupervisor:
def _clear_runner_state(self) -> None:
"""清理当前 Runner 对应的 Host 侧注册状态。"""
for plugin_id in list(self._mirrored_core_actions.keys()):
self._remove_core_action_mirrors(plugin_id)
self._authorization.clear()
self._component_registry.clear()
self._registered_plugins.clear()