diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index 5021de0e7..cb1cfbd9e 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -1,6 +1,7 @@ import random from typing import Any, ClassVar +from mmc.src.plugin_system.base.base_http_component import BaseRouterComponent from src.common.logger import get_logger # 修正导入路径,让Pylance不再抱怨 @@ -24,6 +25,7 @@ from src.plugin_system.base.component_types import InjectionRule, InjectionType logger = get_logger("hello_world_plugin") + class StartupMessageHandler(BaseEventHandler): """启动时打印消息的事件处理器。""" @@ -198,12 +200,25 @@ class WeatherPrompt(BasePrompt): return "当前天气:晴朗,温度25°C。" +class HelloWorldRouter(BaseRouterComponent): + """一个简单的HTTP端点示例。""" + + component_name = "hello_world_router" + component_description = "提供一个简单的 /greet HTTP GET 端点。" + + def register_endpoints(self) -> None: + @self.router.get("/greet", summary="返回一个问候消息") + def greet(): + """这个端点返回一个固定的问候语。""" + return {"message": "Hello from your new API endpoint!"} + + @register_plugin class HelloWorldPlugin(BasePlugin): """一个包含四大核心组件和高级配置功能的入门示例插件。""" plugin_name = "hello_world_plugin" - enable_plugin = False + enable_plugin = True dependencies: ClassVar = [] python_dependencies: ClassVar = [] config_file_name = "config.toml" @@ -225,7 +240,7 @@ class HelloWorldPlugin(BasePlugin): def get_plugin_components(self) -> list[tuple[ComponentInfo, type]]: """根据配置文件动态注册插件的功能组件。""" - components: ClassVar[list[tuple[ComponentInfo, type]] ] = [] + components: list[tuple[ComponentInfo, type]] = [] components.append((StartupMessageHandler.get_handler_info(), StartupMessageHandler)) components.append((GetSystemInfoTool.get_tool_info(), GetSystemInfoTool)) @@ -239,4 +254,7 @@ class HelloWorldPlugin(BasePlugin): # 注册新的Prompt组件 components.append((WeatherPrompt.get_prompt_info(), WeatherPrompt)) + # 注册新的Router组件 + components.append((HelloWorldRouter.get_router_info(), HelloWorldRouter)) + return components diff --git a/src/api/memory_visualizer_router.py b/src/api/memory_visualizer_router.py index b1ff00e65..2ec47779b 100644 --- a/src/api/memory_visualizer_router.py +++ b/src/api/memory_visualizer_router.py @@ -10,10 +10,12 @@ from pathlib import Path from typing import Any import orjson -from fastapi import APIRouter, HTTPException, Query, Request +from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.templating import Jinja2Templates +from src.common.security import get_api_key + # 调整项目根目录的计算方式 project_root = Path(__file__).parent.parent.parent data_dir = project_root / "data" / "memory_graph" @@ -23,7 +25,7 @@ graph_data_cache = None current_data_file = None # FastAPI 路由 -router = APIRouter() +router = APIRouter(dependencies=[Depends(get_api_key)]) # Jinja2 模板引擎 templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates")) diff --git a/src/api/message_router.py b/src/api/message_router.py index a8551ba04..f7a57bed7 100644 --- a/src/api/message_router.py +++ b/src/api/message_router.py @@ -1,16 +1,17 @@ import time from typing import Literal -from fastapi import APIRouter, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query from src.chat.message_receive.chat_stream import get_chat_manager from src.common.logger import get_logger +from src.common.security import get_api_key from src.config.config import global_config from src.plugin_system.apis import message_api, person_api logger = get_logger("HTTP消息API") -router = APIRouter() +router = APIRouter(dependencies=[Depends(get_api_key)]) @router.get("/messages/recent") @@ -161,5 +162,3 @@ async def get_message_stats_by_chat( # 统一异常处理 logger.error(f"获取消息统计时发生错误: {e}") raise HTTPException(status_code=500, detail=str(e)) - - diff --git a/src/api/statistic_router.py b/src/api/statistic_router.py index 54f6836bf..a9bba25f1 100644 --- a/src/api/statistic_router.py +++ b/src/api/statistic_router.py @@ -1,16 +1,17 @@ from datetime import datetime, timedelta from typing import Literal -from fastapi import APIRouter, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query from src.chat.utils.statistic import ( StatisticOutputTask, ) from src.common.logger import get_logger +from src.common.security import get_api_key logger = get_logger("LLM统计API") -router = APIRouter() +router = APIRouter(dependencies=[Depends(get_api_key)]) # 定义统计数据的键,以减少魔法字符串 TOTAL_REQ_CNT = "total_requests" diff --git a/src/common/security.py b/src/common/security.py new file mode 100644 index 000000000..132d32102 --- /dev/null +++ b/src/common/security.py @@ -0,0 +1,32 @@ +from fastapi import Depends, HTTPException, Security +from fastapi.security.api_key import APIKeyHeader +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN + +from src.common.logger import get_logger +from src.config.config import global_config as bot_config + +logger = get_logger("security") + +API_KEY_HEADER = "X-API-Key" +api_key_header_auth = APIKeyHeader(name=API_KEY_HEADER, auto_error=True) + + +async def get_api_key(api_key: str = Security(api_key_header_auth)) -> str: + """ + FastAPI 依赖项,用于验证API密钥。 + 从请求头中提取 X-API-Key 并验证它是否存在于配置的有效密钥列表中。 + """ + valid_keys = bot_config.plugin_http_system.plugin_api_valid_keys + if not valid_keys: + logger.warning("API密钥认证已启用,但未配置任何有效的API密钥。所有请求都将被拒绝。") + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="服务未正确配置API密钥", + ) + if api_key not in valid_keys: + logger.warning(f"无效的API密钥: {api_key}") + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, + detail="无效的API密钥", + ) + return api_key \ No newline at end of file diff --git a/src/common/server.py b/src/common/server.py index f4553f537..527663be2 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -1,32 +1,60 @@ import os import socket -from fastapi import APIRouter, FastAPI +from fastapi import APIRouter, FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from rich.traceback import install from uvicorn import Config from uvicorn import Server as UvicornServer +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware +from slowapi.util import get_remote_address + from src.common.logger import get_logger +from src.config.config import global_config as bot_config install(extra_lines=3) logger = get_logger("Server") +def rate_limit_exceeded_handler(request: Request, exc: Exception) -> Response: + """自定义速率限制超出处理器以解决类型提示问题""" + # 由于此处理器专门用于 RateLimitExceeded,我们可以安全地断言异常类型。 + # 这满足了类型检查器的要求,并确保了运行时安全。 + assert isinstance(exc, RateLimitExceeded) + return _rate_limit_exceeded_handler(request, exc) + + class Server: - def __init__(self, host: str | None = None, port: int | None = None, app_name: str = "MaiMCore"): + def __init__(self, host: str | None = None, port: int | None = None, app_name: str = "MoFox-Bot"): + # 根据配置初始化速率限制器 + limiter = Limiter( + key_func=get_remote_address, + default_limits=[bot_config.plugin_http_system.plugin_api_rate_limit_default], + ) + self.app = FastAPI(title=app_name) self.host: str = "127.0.0.1" self.port: int = 8080 self._server: UvicornServer | None = None self.set_address(host, port) + # 设置速率限制 + self.app.state.limiter = limiter + self.app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler) + + # 根据配置决定是否添加中间件 + if bot_config.plugin_http_system.plugin_api_rate_limit_enable: + logger.info(f"已为插件API启用全局速率限制: {bot_config.plugin_http_system.plugin_api_rate_limit_default}") + self.app.add_middleware(SlowAPIMiddleware) + # 配置 CORS origins = [ "http://localhost:3000", # 允许的前端源 "http://127.0.0.1:3000", - "http://127.0.0.1:3000", # 在生产环境中,您应该添加实际的前端域名 ] diff --git a/src/config/config.py b/src/config/config.py index b3925e608..49f7b2be8 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -34,6 +34,7 @@ from src.config.official_configs import ( PermissionConfig, PersonalityConfig, PlanningSystemConfig, + PluginHttpSystemConfig, ProactiveThinkingConfig, ReactionConfig, ResponsePostProcessConfig, @@ -414,6 +415,9 @@ class Config(ValidatedConfigBase): proactive_thinking: ProactiveThinkingConfig = Field( default_factory=lambda: ProactiveThinkingConfig(), description="主动思考配置" ) + plugin_http_system: PluginHttpSystemConfig = Field( + default_factory=lambda: PluginHttpSystemConfig(), description="插件HTTP端点系统配置" + ) class APIAdapterConfig(ValidatedConfigBase): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index edb2438f1..6b58df292 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -736,6 +736,23 @@ class CommandConfig(ValidatedConfigBase): command_prefixes: list[str] = Field(default_factory=lambda: ["/", "!", ".", "#"], description="支持的命令前缀列表") +class PluginHttpSystemConfig(ValidatedConfigBase): + """插件http系统相关配置""" + + enable_plugin_http_endpoints: bool = Field( + default=True, description="总开关,是否允许插件创建HTTP端点" + ) + plugin_api_rate_limit_enable: bool = Field( + default=True, description="是否为插件API启用全局速率限制" + ) + plugin_api_rate_limit_default: str = Field( + default="100/minute", description="插件API的默认速率限制策略" + ) + plugin_api_valid_keys: list[str] = Field( + default_factory=list, description="有效的API密钥列表,用于插件认证" + ) + + class MasterPromptConfig(ValidatedConfigBase): """主人身份提示词配置""" diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index 1bffac3c8..3a8c92966 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -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 diff --git a/src/plugin_system/base/__init__.py b/src/plugin_system/base/__init__.py index 9b0bc1325..014ea4852 100644 --- a/src/plugin_system/base/__init__.py +++ b/src/plugin_system/base/__init__.py @@ -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", diff --git a/src/plugin_system/base/base_http_component.py b/src/plugin_system/base/base_http_component.py new file mode 100644 index 000000000..218cd4a54 --- /dev/null +++ b/src/plugin_system/base/base_http_component.py @@ -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, + ) diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index b34bcf20e..2584608af 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -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 diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index a82c9e792..3390cd0a5 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -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": { diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 6346167f8..43a2f22f3 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -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): diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index c7f011d81..f6c5061b7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.7.1" +version = "7.7.3" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -59,6 +59,26 @@ cache_max_item_size_mb = 5 # 单个缓存条目最大大小(MB),超过此 # 示例:[["qq", "123456"], ["telegram", "user789"]] master_users = []# ["qq", "123456789"], # 示例:QQ平台的Master用户 +# ==================== 插件HTTP端点系统配置 ==================== +[plugin_http_system] +# 总开关,用于启用或禁用所有插件的HTTP端点功能 +enable_plugin_http_endpoints = true + +# ==================== 安全相关配置 ==================== +[security] +# --- 插件API速率限制 --- +# 是否为插件暴露的API启用全局速率限制 +plugin_api_rate_limit_enable = true +# 默认的速率限制策略 (格式: "次数/时间单位") +# 可用单位: second, minute, hour, day +plugin_api_rate_limit_default = "100/minute" + +# --- 插件API密钥认证 --- +# 用于访问需要认证的插件API的有效密钥列表 +# 如果列表为空,则所有需要认证的API都将无法访问 +# 例如: ["your-secret-key-1", "your-secret-key-2"] +plugin_api_valid_keys = [] + [permission.master_prompt] # 主人身份提示词配置 enable = false # 是否启用主人/非主人提示注入 master_hint = "你正在与自己的主人交流,注意展现亲切与尊重。" # 主人提示词