feat(plugin_system): 引入插件HTTP端点系统

引入了全新的 `BaseRouterComponent` 组件类型,允许插件开发者通过继承并实现 `register_endpoints` 方法来创建 FastAPI 路由。

- 插件系统现在可以自动发现并注册这些路由组件,并将它们挂载到主 FastAPI 应用的 `/plugins/<plugin_name>` 前缀下。
- 新增了全局配置 `[plugin_http_system]`,提供了总开关、API 速率限制和 API 密钥认证 (`X-API-Key`) 等功能,以确保端点的安全性和稳定性。
- 更新了 `hello_world_plugin` 插件,增加了一个简单的 `/greet` 端点作为实现示例。
This commit is contained in:
minecraft1024a
2025-11-16 12:41:35 +08:00
committed by Windpicker-owo
parent fea007b429
commit 717d4ba555
15 changed files with 257 additions and 18 deletions

View File

@@ -44,6 +44,7 @@ from .base import (
PluginInfo,
# 新增的增强命令系统
PlusCommand,
BaseRouterComponent,
PythonDependency,
ToolInfo,
ToolParamType,
@@ -56,7 +57,7 @@ from .utils.dependency_manager import configure_dependency_manager, get_dependen
__version__ = "2.0.0"
__all__ = [
__all__ = [ # noqa: RUF022
"ActionActivationType",
"ActionInfo",
"BaseAction",
@@ -82,6 +83,7 @@ __all__ = [
"PluginInfo",
# 增强命令系统
"PlusCommand",
"BaseRouterComponent"
"PythonDependency",
"ToolInfo",
"ToolParamType",
@@ -114,4 +116,4 @@ __all__ = [
# "ManifestGenerator",
# "validate_plugin_manifest",
# "generate_plugin_manifest",
]
] # type: ignore

View File

@@ -7,6 +7,7 @@
from .base_action import BaseAction
from .base_command import BaseCommand
from .base_events_handler import BaseEventHandler
from .base_http_component import BaseRouterComponent
from .base_plugin import BasePlugin
from .base_prompt import BasePrompt
from .base_tool import BaseTool
@@ -55,7 +56,7 @@ __all__ = [
"PluginMetadata",
# 增强命令系统
"PlusCommand",
"PlusCommandAdapter",
"BaseRouterComponent"
"PlusCommandInfo",
"PythonDependency",
"ToolInfo",

View File

@@ -0,0 +1,37 @@
from abc import ABC, abstractmethod
from fastapi import APIRouter
from .component_types import ComponentType, RouterInfo
class BaseRouterComponent(ABC):
"""
用于暴露HTTP端点的组件基类。
插件开发者应继承此类,并实现 register_endpoints 方法来定义API路由。
"""
# 组件元数据,由插件管理器读取
component_name: str
component_description: str
component_version: str = "1.0.0"
# 每个组件实例都会管理自己的APIRouter
router: APIRouter
def __init__(self):
self.router = APIRouter()
self.register_endpoints()
@abstractmethod
def register_endpoints(self) -> None:
"""
【开发者必须实现】
在此方法中定义所有HTTP端点。
"""
pass
@classmethod
def get_router_info(cls) -> "RouterInfo":
"""从类属性生成RouterInfo"""
return RouterInfo(
name=cls.component_name,
description=getattr(cls, "component_description", "路由组件"),
component_type=ComponentType.ROUTER,
)

View File

@@ -53,6 +53,7 @@ class ComponentType(Enum):
CHATTER = "chatter" # 聊天处理器组件
INTEREST_CALCULATOR = "interest_calculator" # 兴趣度计算组件
PROMPT = "prompt" # Prompt组件
ROUTER = "router" # 路由组件
def __str__(self) -> str:
return self.value
@@ -146,6 +147,7 @@ class PermissionNodeField:
node_name: str # 节点名称 (例如 "manage" 或 "view")
description: str # 权限描述
@dataclass
class ComponentInfo:
"""组件信息"""
@@ -442,3 +444,13 @@ class MaiMessages:
def __post_init__(self):
if self.message_segments is None:
self.message_segments = []
@dataclass
class RouterInfo(ComponentInfo):
"""路由组件信息"""
auth_required: bool = False
def __post_init__(self):
super().__post_init__()
self.component_type = ComponentType.ROUTER

View File

@@ -5,11 +5,15 @@ from pathlib import Path
from re import Pattern
from typing import Any, cast
from fastapi import Depends
from src.common.logger import get_logger
from src.config.config import global_config as bot_config
from src.plugin_system.base.base_action import BaseAction
from src.plugin_system.base.base_chatter import BaseChatter
from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.base_events_handler import BaseEventHandler
from src.plugin_system.base.base_http_component import BaseRouterComponent
from src.plugin_system.base.base_interest_calculator import BaseInterestCalculator
from src.plugin_system.base.base_prompt import BasePrompt
from src.plugin_system.base.base_tool import BaseTool
@@ -24,6 +28,7 @@ from src.plugin_system.base.component_types import (
PluginInfo,
PlusCommandInfo,
PromptInfo,
RouterInfo,
ToolInfo,
)
from src.plugin_system.base.plus_command import PlusCommand, create_legacy_command_adapter
@@ -40,6 +45,7 @@ ComponentClassType = (
| type[BaseChatter]
| type[BaseInterestCalculator]
| type[BasePrompt]
| type[BaseRouterComponent]
)
@@ -194,6 +200,10 @@ class ComponentRegistry:
assert isinstance(component_info, PromptInfo)
assert issubclass(component_class, BasePrompt)
ret = self._register_prompt_component(component_info, component_class)
case ComponentType.ROUTER:
assert isinstance(component_info, RouterInfo)
assert issubclass(component_class, BaseRouterComponent)
ret = self._register_router_component(component_info, component_class)
case _:
logger.warning(f"未知组件类型: {component_type}")
ret = False
@@ -373,6 +383,48 @@ class ComponentRegistry:
logger.debug(f"已注册Prompt组件: {prompt_name}")
return True
def _register_router_component(self, router_info: RouterInfo, router_class: type[BaseRouterComponent]) -> bool:
"""注册Router组件并将其端点挂载到主服务器"""
# 1. 检查总开关是否开启
if not bot_config.plugin_http_system.enable_plugin_http_endpoints:
logger.info("插件HTTP端点功能已禁用跳过路由注册")
return True
try:
from src.common.security import get_api_key
from src.common.server import get_global_server
router_name = router_info.name
plugin_name = router_info.plugin_name
# 2. 实例化组件以触发其 __init__ 和 register_endpoints
component_instance = router_class()
# 3. 获取配置好的 APIRouter
plugin_router = component_instance.router
# 4. 获取全局服务器实例
server = get_global_server()
# 5. 生成唯一的URL前缀
prefix = f"/plugins/{plugin_name}"
# 6. 根据需要应用安全依赖项
dependencies = []
if router_info.auth_required:
dependencies.append(Depends(get_api_key))
# 7. 注册路由并使用插件名作为API文档的分组标签
server.app.include_router(
plugin_router, prefix=prefix, tags=[plugin_name], dependencies=dependencies
)
logger.debug(f"成功将插件 '{plugin_name}' 的路由组件 '{router_name}' 挂载到: {prefix}")
return True
except Exception as e:
logger.error(f"注册路由组件 '{router_info.name}' 时出错: {e}", exc_info=True)
return False
# === 组件移除相关 ===
async def remove_component(self, component_name: str, component_type: ComponentType, plugin_name: str) -> bool:
@@ -616,6 +668,7 @@ class ComponentRegistry:
| BaseChatter
| BaseInterestCalculator
| BasePrompt
| BaseRouterComponent
]
| None
):
@@ -643,6 +696,8 @@ class ComponentRegistry:
| type[PlusCommand]
| type[BaseChatter]
| type[BaseInterestCalculator]
| type[BasePrompt]
| type[BaseRouterComponent]
| None,
self._components_classes.get(namespaced_name),
)
@@ -867,6 +922,7 @@ class ComponentRegistry:
plus_command_components: int = 0
chatter_components: int = 0
prompt_components: int = 0
router_components: int = 0
for component in self._components.values():
if component.component_type == ComponentType.ACTION:
action_components += 1
@@ -882,6 +938,8 @@ class ComponentRegistry:
chatter_components += 1
elif component.component_type == ComponentType.PROMPT:
prompt_components += 1
elif component.component_type == ComponentType.ROUTER:
router_components += 1
return {
"action_components": action_components,
"command_components": command_components,
@@ -891,6 +949,7 @@ class ComponentRegistry:
"plus_command_components": plus_command_components,
"chatter_components": chatter_components,
"prompt_components": prompt_components,
"router_components": router_components,
"total_components": len(self._components),
"total_plugins": len(self._plugins),
"components_by_type": {

View File

@@ -405,13 +405,14 @@ class PluginManager:
plus_command_count = stats.get("plus_command_components", 0)
chatter_count = stats.get("chatter_components", 0)
prompt_count = stats.get("prompt_components", 0)
router_count = stats.get("router_components", 0)
total_components = stats.get("total_components", 0)
# 📋 显示插件加载总览
if total_registered > 0:
logger.info("🎉 插件系统加载完成!")
logger.info(
f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, Tool: {tool_count}, PlusCommand: {plus_command_count}, EventHandler: {event_handler_count}, Chatter: {chatter_count}, Prompt: {prompt_count})"
f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, Tool: {tool_count}, PlusCommand: {plus_command_count}, EventHandler: {event_handler_count}, Chatter: {chatter_count}, Prompt: {prompt_count}, Router: {router_count})"
)
# 显示详细的插件列表
@@ -452,6 +453,9 @@ class PluginManager:
prompt_components = [
c for c in plugin_info.components if c.component_type == ComponentType.PROMPT
]
router_components = [
c for c in plugin_info.components if c.component_type == ComponentType.ROUTER
]
if action_components:
action_details = [format_component(c) for c in action_components]
@@ -478,6 +482,9 @@ class PluginManager:
if prompt_components:
prompt_details = [format_component(c) for c in prompt_components]
logger.info(f" 📝 Prompt组件: {', '.join(prompt_details)}")
if router_components:
router_details = [format_component(c) for c in router_components]
logger.info(f" 🌐 Router组件: {', '.join(router_details)}")
# 权限节点信息
if plugin_instance := self.loaded_plugins.get(plugin_name):