From aba7af43968bc13a45be87175734f7ea8519b902 Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Sat, 15 Nov 2025 20:07:48 +0800 Subject: [PATCH 1/8] =?UTF-8?q?refactor(maizone):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB=E4=BB=A5=E5=B0=BD=E6=97=A9?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E6=96=87=E4=BB=B6=E5=8F=A5=E6=9F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据迁移逻辑已更新为先将整个文件读入内存,然后立即关闭文件句柄。 这可以防止旧数据文件在随后的 JSON 解析、验证和写入新存储的过程中保持打开状态,从而提高迁移过程的稳健性。 --- .../services/reply_tracker_service.py | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py b/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py index 22b833cec..30984cd3e 100644 --- a/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py +++ b/src/plugins/built_in/maizone_refactored/services/reply_tracker_service.py @@ -80,37 +80,39 @@ class ReplyTrackerService: if old_data_file.exists(): logger.info(f"检测到旧的数据文件 '{old_data_file}',开始执行一次性迁移...") try: - # 读取旧文件内容 + # 步骤1: 读取旧文件内容并立即关闭文件 with open(old_data_file, "rb") as f: file_content = f.read() - # 如果文件为空,直接删除,无需迁移 - if not file_content.strip(): - logger.warning("旧数据文件为空,无需迁移。") - os.remove(old_data_file) - logger.info(f"空的旧数据文件 '{old_data_file}' 已被删除。") - return - # 解析JSON数据 - old_data = orjson.loads(file_content) + # 步骤2: 处理文件内容 + # 如果文件为空,直接删除,无需迁移 + if not file_content.strip(): + logger.warning("旧数据文件为空,无需迁移。") + os.remove(old_data_file) + logger.info(f"空的旧数据文件 '{old_data_file}' 已被删除。") + return - # 验证数据格式是否正确 - if self._validate_data(old_data): - # 验证通过,将数据写入新的存储API - self.storage.set("data", old_data) - # 立即强制保存,确保迁移数据落盘 - self.storage._save_data() - logger.info("旧数据已成功迁移到新的存储API。") + # 解析JSON数据 + old_data = orjson.loads(file_content) - # 将旧文件重命名为备份文件,而不是直接删除,以防万一 - backup_file = old_data_file.with_suffix(f".json.bak.migrated.{int(time.time())}") - old_data_file.rename(backup_file) - logger.info(f"旧数据文件已成功迁移并备份为: {backup_file}") - else: - # 如果数据格式无效,迁移中止,并备份损坏的文件 - logger.error("旧数据文件格式无效,迁移中止。") - backup_file = old_data_file.with_suffix(f".json.bak.invalid.{int(time.time())}") - old_data_file.rename(backup_file) - logger.warning(f"已将无效的旧数据文件备份为: {backup_file}") + # 步骤3: 验证数据并执行迁移/备份 + if self._validate_data(old_data): + # 验证通过,将数据写入新的存储API + self.storage.set("data", old_data) + # 立即强制保存,确保迁移数据落盘 + self.storage._save_data() + logger.info("旧数据已成功迁移到新的存储API。") + + # 将旧文件重命名为备份文件 + backup_file = old_data_file.with_suffix(f".json.bak.migrated.{int(time.time())}") + old_data_file.rename(backup_file) + logger.info(f"旧数据文件已成功迁移并备份为: {backup_file}") + else: + # 如果数据格式无效,迁移中止,并备份损坏的文件 + logger.error("旧数据文件格式无效,迁移中止。") + backup_file = old_data_file.with_suffix(f".json.bak.invalid.{int(time.time())}") + old_data_file.rename(backup_file) + logger.warning(f"已将无效的旧数据文件备份为: {backup_file}") except Exception as e: # 捕获迁移过程中可能出现的任何异常 From 7e323e3d0f2a17f144e4e42f3116a0d876bbdefb Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 15 Nov 2025 21:08:29 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat(prompt):=20=E9=BB=98=E8=AE=A4=E5=90=AF?= =?UTF-8?q?=E7=94=A8=E6=8A=97=E5=AE=A1=E6=9F=A5=E6=8C=87=E4=BB=A4=E5=B9=B6?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为了简化用户配置并默认提高模型的抗审查能力,本次更新移除了 `prepend_noise_instruction` 配置选项。 现在,抗审查指令将作为默认行为,在处理所有提示词时自动添加,不再需要用户手动开启。同时,更新了模板文件中的注释,使其更清晰易懂。 --- src/config/api_ada_configs.py | 2 -- src/llm_models/utils_model.py | 3 +-- template/model_config_template.toml | 11 ++++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/config/api_ada_configs.py b/src/config/api_ada_configs.py index 157692919..d5478f8b4 100644 --- a/src/config/api_ada_configs.py +++ b/src/config/api_ada_configs.py @@ -76,8 +76,6 @@ class ModelInfo(ValidatedConfigBase): default="light", description="扰动强度(light/medium/heavy)" ) enable_semantic_variants: bool = Field(default=False, description="是否启用语义变体作为扰动策略") - - prepend_noise_instruction: bool = Field(default=False, description="是否在提示词前部添加抗审查指令") @classmethod def validate_prices(cls, v): """验证价格必须为非负数""" diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index c26bb752d..c3f4dc567 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -501,8 +501,7 @@ class _PromptProcessor: user_prompt = prompt # 步骤 A: (可选) 添加抗审查指令 - if getattr(model_info, "prepend_noise_instruction", False): - final_prompt_parts.append(self.noise_instruction) + final_prompt_parts.append(self.noise_instruction) # 步骤 B: (可选) 应用统一的提示词扰动 if getattr(model_info, "enable_prompt_perturbation", False): diff --git a/template/model_config_template.toml b/template/model_config_template.toml index c1c84087a..527ede4a3 100644 --- a/template/model_config_template.toml +++ b/template/model_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.3.8" +version = "1.3.9" # 配置文件版本号迭代规则同bot_config.toml @@ -37,10 +37,11 @@ name = "deepseek-v3" # 模型名称(可随意命名,在后面 api_provider = "DeepSeek" # API服务商名称(对应在api_providers中配置的服务商名称) price_in = 2.0 # 输入价格(用于API调用统计,单位:元/ M token)(可选,若无该字段,默认值为0) price_out = 8.0 # 输出价格(用于API调用统计,单位:元/ M token)(可选,若无该字段,默认值为0) -#force_stream_mode = true # 强制流式输出模式(若模型不支持非流式输出,请取消该注释,启用强制流式输出,若无该字段,默认值为false) -#use_anti_truncation = true # [可选] 启用反截断功能。当模型输出不完整时,系统会自动重试。建议只为有需要的模型(如Gemini)开启。 -#enable_content_obfuscation = true # [可选] 启用内容混淆功能,用于特定场景下的内容处理(例如某些内容审查比较严的模型和稀疏注意模型) -#obfuscation_intensity = 2 # 混淆强度(1-3级,1=低强度,2=中强度,3=高强度) +#force_stream_mode = false # [可选] 强制流式输出模式。如果模型不支持非流式输出,请取消注释以启用。默认为 false。 +#anti_truncation = false # [可选] 启用反截断功能。当模型输出不完整时,系统会自动重试。建议只为需要的模型(如Gemini)开启。默认为 false。 +#enable_prompt_perturbation = false # [可选] 启用提示词扰动。此功能整合了内容混淆和注意力优化,默认为 false。 +#perturbation_strength = "light" # [可选] 扰动强度。仅在 enable_prompt_perturbation 为 true 时生效。可选值为 "light", "medium", "heavy"。默认为 "light"。 +#enable_semantic_variants = false # [可选] 启用语义变体。作为一种扰动策略,生成语义上相似但表达不同的提示。默认为 false。 [[models]] model_identifier = "deepseek-ai/DeepSeek-V3.2-Exp" From 4d67cc8d8335a63eaff575441c5bde455baac58f Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 15 Nov 2025 21:09:13 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix(prompt):=20=E4=BF=AE=E5=A4=8D=E6=8A=97?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E6=8C=87=E4=BB=A4=E8=A2=AB=E6=97=A0=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E6=B7=BB=E5=8A=A0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在之前的提交中,抗审查指令被错误地设置为无条件添加。本次提交修正了此逻辑,将其与 `enable_prompt_perturbation` 开关关联,确保只有在启用提示词扰动时才会添加该指令,恢复了预期的行为。 此外,还简化了反截断指令的条件判断,直接访问 `model_info.anti_truncation` 属性以提高代码的可读性。 --- src/llm_models/utils_model.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index c3f4dc567..a46824a72 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -500,8 +500,9 @@ class _PromptProcessor: final_prompt_parts = [] user_prompt = prompt - # 步骤 A: (可选) 添加抗审查指令 - final_prompt_parts.append(self.noise_instruction) + # 步骤 A: 添加抗审查指令 + if model_info.enable_prompt_perturbation: + final_prompt_parts.append(self.noise_instruction) # 步骤 B: (可选) 应用统一的提示词扰动 if getattr(model_info, "enable_prompt_perturbation", False): @@ -515,7 +516,7 @@ class _PromptProcessor: final_prompt_parts.append(user_prompt) # 步骤 C: (可选) 添加反截断指令 - if getattr(model_info, "use_anti_truncation", False): + if model_info.anti_truncation: final_prompt_parts.append(self.anti_truncation_instruction) logger.info(f"模型 '{model_info.name}' (任务: '{task_name}') 已启用反截断功能。") @@ -881,7 +882,7 @@ class _RequestStrategy: # --- 响应内容处理和空回复/截断检查 --- content = response.content or "" - use_anti_truncation = getattr(model_info, "use_anti_truncation", False) + use_anti_truncation = model_info.anti_truncation processed_content, reasoning, is_truncated = await self.prompt_processor.process_response( content, use_anti_truncation ) From 42f0e0e02351d14cf8c896548d34fe095bbdd372 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 16 Nov 2025 12:41:35 +0800 Subject: [PATCH 4/8] =?UTF-8?q?feat(plugin=5Fsystem):=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E6=8F=92=E4=BB=B6HTTP=E7=AB=AF=E7=82=B9=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了全新的 `BaseRouterComponent` 组件类型,允许插件开发者通过继承并实现 `register_endpoints` 方法来创建 FastAPI 路由。 - 插件系统现在可以自动发现并注册这些路由组件,并将它们挂载到主 FastAPI 应用的 `/plugins/` 前缀下。 - 新增了全局配置 `[plugin_http_system]`,提供了总开关、API 速率限制和 API 密钥认证 (`X-API-Key`) 等功能,以确保端点的安全性和稳定性。 - 更新了 `hello_world_plugin` 插件,增加了一个简单的 `/greet` 端点作为实现示例。 --- plugins/hello_world_plugin/plugin.py | 22 ++++++- src/api/memory_visualizer_router.py | 6 +- src/api/message_router.py | 7 +-- src/api/statistic_router.py | 5 +- src/common/security.py | 32 ++++++++++ src/common/server.py | 34 ++++++++++- src/config/config.py | 4 ++ src/config/official_configs.py | 17 ++++++ src/plugin_system/__init__.py | 6 +- src/plugin_system/base/__init__.py | 3 +- src/plugin_system/base/base_http_component.py | 37 ++++++++++++ src/plugin_system/base/component_types.py | 12 ++++ src/plugin_system/core/component_registry.py | 59 +++++++++++++++++++ src/plugin_system/core/plugin_manager.py | 9 ++- template/bot_config_template.toml | 22 ++++++- 15 files changed, 257 insertions(+), 18 deletions(-) create mode 100644 src/common/security.py create mode 100644 src/plugin_system/base/base_http_component.py 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 = "你正在与自己的主人交流,注意展现亲切与尊重。" # 主人提示词 From 6a5af6f69e11cf0b5523c2dc734b9a230dc73c90 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 16 Nov 2025 12:45:27 +0800 Subject: [PATCH 5/8] =?UTF-8?q?refactor(api):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=86=85=E5=AD=98=E5=8F=AF=E8=A7=86=E5=8C=96=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E7=9A=84=20API=20=E5=AF=86=E9=92=A5=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 该路由旨在用于本地调试和可视化,不再需要进行 API 密钥认证。 BREAKING CHANGE: 内存可视化路由现在是公开访问的,不再需要 API 密钥。 --- src/api/memory_visualizer_router.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/api/memory_visualizer_router.py b/src/api/memory_visualizer_router.py index 2ec47779b..dd8f3aa07 100644 --- a/src/api/memory_visualizer_router.py +++ b/src/api/memory_visualizer_router.py @@ -10,11 +10,10 @@ from pathlib import Path from typing import Any import orjson -from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi import APIRouter, 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 @@ -25,7 +24,7 @@ graph_data_cache = None current_data_file = None # FastAPI 路由 -router = APIRouter(dependencies=[Depends(get_api_key)]) +router = APIRouter() # Jinja2 模板引擎 templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates")) From 164963b6f765a87fd60a2028d1038470e3c7ebba Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 16 Nov 2025 13:31:59 +0800 Subject: [PATCH 6/8] =?UTF-8?q?refactor(plugin=5Fsystem):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E8=B7=AF=E7=94=B1=E7=BA=A7=E8=AE=A4=E8=AF=81=EF=BC=8C?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E7=AB=AF=E7=82=B9=E7=BA=A7=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前的插件路由认证机制通过在 `RouterInfo` 中设置 `auth_required` 标志,对整个路由组件统一应用API密钥验证。这种方式缺乏灵活性,无法对单个端点进行细粒度的安全控制。 本次重构移除了 `auth_required` 机制,转而引入一个可重用的 FastAPI 依赖项 `VerifiedDep`。插件开发者现在可以按需将其应用到需要保护的特定端点上,从而实现更灵活、更精确的访问控制。 `hello_world_plugin` 已更新,以演示新的认证方式。 BREAKING CHANGE: 移除了 `RouterInfo` 中的 `auth_required` 属性。所有依赖此属性进行认证的插件路由都需要更新,改为在需要保护的端点上使用 `VerifiedDep` 依赖项。 --- plugins/hello_world_plugin/plugin.py | 7 ++++--- src/common/security.py | 7 ++++++- src/plugin_system/base/base_http_component.py | 3 +++ src/plugin_system/base/component_types.py | 2 -- src/plugin_system/core/component_registry.py | 12 ++++-------- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index cb1cfbd9e..5fcfad730 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -1,8 +1,8 @@ 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 +from src.common.security import VerifiedDep # 修正导入路径,让Pylance不再抱怨 from src.plugin_system import ( @@ -21,6 +21,7 @@ from src.plugin_system import ( register_plugin, ) from src.plugin_system.base.base_event import HandlerResult +from src.plugin_system.base.base_http_component import BaseRouterComponent from src.plugin_system.base.component_types import InjectionRule, InjectionType logger = get_logger("hello_world_plugin") @@ -208,7 +209,7 @@ class HelloWorldRouter(BaseRouterComponent): def register_endpoints(self) -> None: @self.router.get("/greet", summary="返回一个问候消息") - def greet(): + def greet(_=VerifiedDep): """这个端点返回一个固定的问候语。""" return {"message": "Hello from your new API endpoint!"} @@ -218,7 +219,7 @@ class HelloWorldPlugin(BasePlugin): """一个包含四大核心组件和高级配置功能的入门示例插件。""" plugin_name = "hello_world_plugin" - enable_plugin = True + enable_plugin: bool = True dependencies: ClassVar = [] python_dependencies: ClassVar = [] config_file_name = "config.toml" diff --git a/src/common/security.py b/src/common/security.py index 132d32102..b151dfd09 100644 --- a/src/common/security.py +++ b/src/common/security.py @@ -29,4 +29,9 @@ async def get_api_key(api_key: str = Security(api_key_header_auth)) -> str: status_code=HTTP_403_FORBIDDEN, detail="无效的API密钥", ) - return api_key \ No newline at end of file + return api_key + +# 创建一个可重用的依赖项,供插件开发者在其需要验证的端点上使用 +# 用法: @router.get("/protected_route", dependencies=[VerifiedDep]) +# 或者: async def my_endpoint(_=VerifiedDep): ... +VerifiedDep = Depends(get_api_key) \ No newline at end of file diff --git a/src/plugin_system/base/base_http_component.py b/src/plugin_system/base/base_http_component.py index 218cd4a54..067aca184 100644 --- a/src/plugin_system/base/base_http_component.py +++ b/src/plugin_system/base/base_http_component.py @@ -1,7 +1,10 @@ from abc import ABC, abstractmethod + from fastapi import APIRouter + from .component_types import ComponentType, RouterInfo + class BaseRouterComponent(ABC): """ 用于暴露HTTP端点的组件基类。 diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index 2584608af..d58a5d2e9 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -449,8 +449,6 @@ class MaiMessages: 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 3390cd0a5..ab996fe79 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -390,7 +390,6 @@ class ComponentRegistry: 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 @@ -408,14 +407,10 @@ class ComponentRegistry: # 5. 生成唯一的URL前缀 prefix = f"/plugins/{plugin_name}" - # 6. 根据需要应用安全依赖项 - dependencies = [] - if router_info.auth_required: - dependencies.append(Depends(get_api_key)) - - # 7. 注册路由,并使用插件名作为API文档的分组标签 + # 6. 注册路由,并使用插件名作为API文档的分组标签 + # 移除了dependencies参数,因为现在由每个端点自行决定是否需要验证 server.app.include_router( - plugin_router, prefix=prefix, tags=[plugin_name], dependencies=dependencies + plugin_router, prefix=prefix, tags=[plugin_name] ) logger.debug(f"成功将插件 '{plugin_name}' 的路由组件 '{router_name}' 挂载到: {prefix}") @@ -880,6 +875,7 @@ class ComponentRegistry: def get_plugin_components(self, plugin_name: str) -> list["ComponentInfo"]: """获取插件的所有组件""" plugin_info = self.get_plugin_info(plugin_name) + logger.info(plugin_info.components) return plugin_info.components if plugin_info else [] def get_plugin_config(self, plugin_name: str) -> dict: From cbab331633bc396e1b707c02537f2201becc6ed6 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 16 Nov 2025 13:34:56 +0800 Subject: [PATCH 7/8] =?UTF-8?q?refactor(config):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E6=A8=A1=E6=9D=BF=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=20[security]=20=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在最近的安全相关重构之后,独立的 [security] 配置部分已不再需要。 此提交将其从模板文件中移除以简化配置结构,并相应地更新了版本号。 --- template/bot_config_template.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index f6c5061b7..ea2d29c00 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.7.3" +version = "7.7.4" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -65,7 +65,6 @@ master_users = []# ["qq", "123456789"], # 示例:QQ平台的Master用户 enable_plugin_http_endpoints = true # ==================== 安全相关配置 ==================== -[security] # --- 插件API速率限制 --- # 是否为插件暴露的API启用全局速率限制 plugin_api_rate_limit_enable = true From 8f4e376e4aaa7f6e08b24fb80a93d16e49fa322a Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 16 Nov 2025 13:58:44 +0800 Subject: [PATCH 8/8] =?UTF-8?q?build(deps):=20=E6=B7=BB=E5=8A=A0=20slowapi?= =?UTF-8?q?=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 7aae8254b..2f70c2c4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ version = "0.12.0" description = "MoFox-Bot 是一个基于大语言模型的可交互智能体" requires-python = ">=3.11,<=3.13" dependencies = [ + "slowapi>=0.1.8", "aiohttp>=3.12.14", "aiohttp-cors>=0.8.1", "aiofiles>=23.1.0", diff --git a/requirements.txt b/requirements.txt index eb6b499a2..4fa4c3705 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ faiss-cpu fastapi fastmcp filetype +slowapi rjieba jsonlines maim_message