This commit is contained in:
Windpicker-owo
2025-11-16 21:18:30 +08:00
21 changed files with 296 additions and 53 deletions

View File

@@ -2,6 +2,7 @@ import random
from typing import Any, ClassVar from typing import Any, ClassVar
from src.common.logger import get_logger from src.common.logger import get_logger
from src.common.security import VerifiedDep
# 修正导入路径让Pylance不再抱怨 # 修正导入路径让Pylance不再抱怨
from src.plugin_system import ( from src.plugin_system import (
@@ -20,10 +21,12 @@ from src.plugin_system import (
register_plugin, register_plugin,
) )
from src.plugin_system.base.base_event import HandlerResult 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 from src.plugin_system.base.component_types import InjectionRule, InjectionType
logger = get_logger("hello_world_plugin") logger = get_logger("hello_world_plugin")
class StartupMessageHandler(BaseEventHandler): class StartupMessageHandler(BaseEventHandler):
"""启动时打印消息的事件处理器。""" """启动时打印消息的事件处理器。"""
@@ -198,12 +201,25 @@ class WeatherPrompt(BasePrompt):
return "当前天气晴朗温度25°C。" 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(_=VerifiedDep):
"""这个端点返回一个固定的问候语。"""
return {"message": "Hello from your new API endpoint!"}
@register_plugin @register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""一个包含四大核心组件和高级配置功能的入门示例插件。""" """一个包含四大核心组件和高级配置功能的入门示例插件。"""
plugin_name = "hello_world_plugin" plugin_name = "hello_world_plugin"
enable_plugin = False enable_plugin: bool = True
dependencies: ClassVar = [] dependencies: ClassVar = []
python_dependencies: ClassVar = [] python_dependencies: ClassVar = []
config_file_name = "config.toml" config_file_name = "config.toml"
@@ -225,7 +241,7 @@ class HelloWorldPlugin(BasePlugin):
def get_plugin_components(self) -> list[tuple[ComponentInfo, type]]: 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((StartupMessageHandler.get_handler_info(), StartupMessageHandler))
components.append((GetSystemInfoTool.get_tool_info(), GetSystemInfoTool)) components.append((GetSystemInfoTool.get_tool_info(), GetSystemInfoTool))
@@ -239,4 +255,7 @@ class HelloWorldPlugin(BasePlugin):
# 注册新的Prompt组件 # 注册新的Prompt组件
components.append((WeatherPrompt.get_prompt_info(), WeatherPrompt)) components.append((WeatherPrompt.get_prompt_info(), WeatherPrompt))
# 注册新的Router组件
components.append((HelloWorldRouter.get_router_info(), HelloWorldRouter))
return components return components

View File

@@ -4,6 +4,7 @@ version = "0.12.0"
description = "MoFox-Bot 是一个基于大语言模型的可交互智能体" description = "MoFox-Bot 是一个基于大语言模型的可交互智能体"
requires-python = ">=3.11,<=3.13" requires-python = ">=3.11,<=3.13"
dependencies = [ dependencies = [
"slowapi>=0.1.8",
"aiohttp>=3.12.14", "aiohttp>=3.12.14",
"aiohttp-cors>=0.8.1", "aiohttp-cors>=0.8.1",
"aiofiles>=23.1.0", "aiofiles>=23.1.0",

View File

@@ -12,6 +12,7 @@ faiss-cpu
fastapi fastapi
fastmcp fastmcp
filetype filetype
slowapi
rjieba rjieba
jsonlines jsonlines
maim_message maim_message

View File

@@ -14,6 +14,7 @@ from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
# 调整项目根目录的计算方式 # 调整项目根目录的计算方式
project_root = Path(__file__).parent.parent.parent project_root = Path(__file__).parent.parent.parent
data_dir = project_root / "data" / "memory_graph" data_dir = project_root / "data" / "memory_graph"

View File

@@ -1,16 +1,17 @@
import time import time
from typing import Literal 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.chat.message_receive.chat_stream import get_chat_manager
from src.common.logger import get_logger from src.common.logger import get_logger
from src.common.security import get_api_key
from src.config.config import global_config from src.config.config import global_config
from src.plugin_system.apis import message_api, person_api from src.plugin_system.apis import message_api, person_api
logger = get_logger("HTTP消息API") logger = get_logger("HTTP消息API")
router = APIRouter() router = APIRouter(dependencies=[Depends(get_api_key)])
@router.get("/messages/recent") @router.get("/messages/recent")
@@ -161,5 +162,3 @@ async def get_message_stats_by_chat(
# 统一异常处理 # 统一异常处理
logger.error(f"获取消息统计时发生错误: {e}") logger.error(f"获取消息统计时发生错误: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View File

@@ -1,16 +1,17 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Literal from typing import Literal
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from src.chat.utils.statistic import ( from src.chat.utils.statistic import (
StatisticOutputTask, StatisticOutputTask,
) )
from src.common.logger import get_logger from src.common.logger import get_logger
from src.common.security import get_api_key
logger = get_logger("LLM统计API") logger = get_logger("LLM统计API")
router = APIRouter() router = APIRouter(dependencies=[Depends(get_api_key)])
# 定义统计数据的键,以减少魔法字符串 # 定义统计数据的键,以减少魔法字符串
TOTAL_REQ_CNT = "total_requests" TOTAL_REQ_CNT = "total_requests"

37
src/common/security.py Normal file
View File

@@ -0,0 +1,37 @@
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
# 创建一个可重用的依赖项,供插件开发者在其需要验证的端点上使用
# 用法: @router.get("/protected_route", dependencies=[VerifiedDep])
# 或者: async def my_endpoint(_=VerifiedDep): ...
VerifiedDep = Depends(get_api_key)

View File

@@ -1,32 +1,60 @@
import os import os
import socket import socket
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from rich.traceback import install from rich.traceback import install
from uvicorn import Config from uvicorn import Config
from uvicorn import Server as UvicornServer 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.common.logger import get_logger
from src.config.config import global_config as bot_config
install(extra_lines=3) install(extra_lines=3)
logger = get_logger("Server") 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: 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.app = FastAPI(title=app_name)
self.host: str = "127.0.0.1" self.host: str = "127.0.0.1"
self.port: int = 8080 self.port: int = 8080
self._server: UvicornServer | None = None self._server: UvicornServer | None = None
self.set_address(host, port) 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 # 配置 CORS
origins = [ origins = [
"http://localhost:3000", # 允许的前端源 "http://localhost:3000", # 允许的前端源
"http://127.0.0.1:3000", "http://127.0.0.1:3000",
"http://127.0.0.1:3000",
# 在生产环境中,您应该添加实际的前端域名 # 在生产环境中,您应该添加实际的前端域名
] ]

View File

@@ -76,8 +76,6 @@ class ModelInfo(ValidatedConfigBase):
default="light", description="扰动强度light/medium/heavy" default="light", description="扰动强度light/medium/heavy"
) )
enable_semantic_variants: bool = Field(default=False, description="是否启用语义变体作为扰动策略") enable_semantic_variants: bool = Field(default=False, description="是否启用语义变体作为扰动策略")
prepend_noise_instruction: bool = Field(default=False, description="是否在提示词前部添加抗审查指令")
@classmethod @classmethod
def validate_prices(cls, v): def validate_prices(cls, v):
"""验证价格必须为非负数""" """验证价格必须为非负数"""

View File

@@ -34,6 +34,7 @@ from src.config.official_configs import (
PermissionConfig, PermissionConfig,
PersonalityConfig, PersonalityConfig,
PlanningSystemConfig, PlanningSystemConfig,
PluginHttpSystemConfig,
ProactiveThinkingConfig, ProactiveThinkingConfig,
ReactionConfig, ReactionConfig,
ResponsePostProcessConfig, ResponsePostProcessConfig,
@@ -414,6 +415,9 @@ class Config(ValidatedConfigBase):
proactive_thinking: ProactiveThinkingConfig = Field( proactive_thinking: ProactiveThinkingConfig = Field(
default_factory=lambda: ProactiveThinkingConfig(), description="主动思考配置" default_factory=lambda: ProactiveThinkingConfig(), description="主动思考配置"
) )
plugin_http_system: PluginHttpSystemConfig = Field(
default_factory=lambda: PluginHttpSystemConfig(), description="插件HTTP端点系统配置"
)
class APIAdapterConfig(ValidatedConfigBase): class APIAdapterConfig(ValidatedConfigBase):

View File

@@ -736,6 +736,23 @@ class CommandConfig(ValidatedConfigBase):
command_prefixes: list[str] = Field(default_factory=lambda: ["/", "!", ".", "#"], description="支持的命令前缀列表") 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): class MasterPromptConfig(ValidatedConfigBase):
"""主人身份提示词配置""" """主人身份提示词配置"""

View File

@@ -500,8 +500,8 @@ class _PromptProcessor:
final_prompt_parts = [] final_prompt_parts = []
user_prompt = prompt user_prompt = prompt
# 步骤 A: (可选) 添加抗审查指令 # 步骤 A: 添加抗审查指令
if getattr(model_info, "prepend_noise_instruction", False): if model_info.enable_prompt_perturbation:
final_prompt_parts.append(self.noise_instruction) final_prompt_parts.append(self.noise_instruction)
# 步骤 B: (可选) 应用统一的提示词扰动 # 步骤 B: (可选) 应用统一的提示词扰动
@@ -516,7 +516,7 @@ class _PromptProcessor:
final_prompt_parts.append(user_prompt) final_prompt_parts.append(user_prompt)
# 步骤 C: (可选) 添加反截断指令 # 步骤 C: (可选) 添加反截断指令
if getattr(model_info, "use_anti_truncation", False): if model_info.anti_truncation:
final_prompt_parts.append(self.anti_truncation_instruction) final_prompt_parts.append(self.anti_truncation_instruction)
logger.info(f"模型 '{model_info.name}' (任务: '{task_name}') 已启用反截断功能。") logger.info(f"模型 '{model_info.name}' (任务: '{task_name}') 已启用反截断功能。")
@@ -882,7 +882,7 @@ class _RequestStrategy:
# --- 响应内容处理和空回复/截断检查 --- # --- 响应内容处理和空回复/截断检查 ---
content = response.content or "" 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( processed_content, reasoning, is_truncated = await self.prompt_processor.process_response(
content, use_anti_truncation content, use_anti_truncation
) )

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
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" # 聊天处理器组件 CHATTER = "chatter" # 聊天处理器组件
INTEREST_CALCULATOR = "interest_calculator" # 兴趣度计算组件 INTEREST_CALCULATOR = "interest_calculator" # 兴趣度计算组件
PROMPT = "prompt" # Prompt组件 PROMPT = "prompt" # Prompt组件
ROUTER = "router" # 路由组件
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@@ -146,6 +147,7 @@ class PermissionNodeField:
node_name: str # 节点名称 (例如 "manage" 或 "view") node_name: str # 节点名称 (例如 "manage" 或 "view")
description: str # 权限描述 description: str # 权限描述
@dataclass @dataclass
class ComponentInfo: class ComponentInfo:
"""组件信息""" """组件信息"""
@@ -442,3 +444,11 @@ class MaiMessages:
def __post_init__(self): def __post_init__(self):
if self.message_segments is None: if self.message_segments is None:
self.message_segments = [] self.message_segments = []
@dataclass
class RouterInfo(ComponentInfo):
"""路由组件信息"""
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 re import Pattern
from typing import Any, cast from typing import Any, cast
from fastapi import Depends
from src.common.logger import get_logger 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_action import BaseAction
from src.plugin_system.base.base_chatter import BaseChatter from src.plugin_system.base.base_chatter import BaseChatter
from src.plugin_system.base.base_command import BaseCommand 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_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_interest_calculator import BaseInterestCalculator
from src.plugin_system.base.base_prompt import BasePrompt from src.plugin_system.base.base_prompt import BasePrompt
from src.plugin_system.base.base_tool import BaseTool from src.plugin_system.base.base_tool import BaseTool
@@ -24,6 +28,7 @@ from src.plugin_system.base.component_types import (
PluginInfo, PluginInfo,
PlusCommandInfo, PlusCommandInfo,
PromptInfo, PromptInfo,
RouterInfo,
ToolInfo, ToolInfo,
) )
from src.plugin_system.base.plus_command import PlusCommand, create_legacy_command_adapter from src.plugin_system.base.plus_command import PlusCommand, create_legacy_command_adapter
@@ -40,6 +45,7 @@ ComponentClassType = (
| type[BaseChatter] | type[BaseChatter]
| type[BaseInterestCalculator] | type[BaseInterestCalculator]
| type[BasePrompt] | type[BasePrompt]
| type[BaseRouterComponent]
) )
@@ -194,6 +200,10 @@ class ComponentRegistry:
assert isinstance(component_info, PromptInfo) assert isinstance(component_info, PromptInfo)
assert issubclass(component_class, BasePrompt) assert issubclass(component_class, BasePrompt)
ret = self._register_prompt_component(component_info, component_class) 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 _: case _:
logger.warning(f"未知组件类型: {component_type}") logger.warning(f"未知组件类型: {component_type}")
ret = False ret = False
@@ -373,6 +383,43 @@ class ComponentRegistry:
logger.debug(f"已注册Prompt组件: {prompt_name}") logger.debug(f"已注册Prompt组件: {prompt_name}")
return True 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.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. 注册路由并使用插件名作为API文档的分组标签
# 移除了dependencies参数因为现在由每个端点自行决定是否需要验证
server.app.include_router(
plugin_router, prefix=prefix, tags=[plugin_name]
)
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: async def remove_component(self, component_name: str, component_type: ComponentType, plugin_name: str) -> bool:
@@ -616,6 +663,7 @@ class ComponentRegistry:
| BaseChatter | BaseChatter
| BaseInterestCalculator | BaseInterestCalculator
| BasePrompt | BasePrompt
| BaseRouterComponent
] ]
| None | None
): ):
@@ -643,6 +691,8 @@ class ComponentRegistry:
| type[PlusCommand] | type[PlusCommand]
| type[BaseChatter] | type[BaseChatter]
| type[BaseInterestCalculator] | type[BaseInterestCalculator]
| type[BasePrompt]
| type[BaseRouterComponent]
| None, | None,
self._components_classes.get(namespaced_name), self._components_classes.get(namespaced_name),
) )
@@ -825,6 +875,7 @@ class ComponentRegistry:
def get_plugin_components(self, plugin_name: str) -> list["ComponentInfo"]: def get_plugin_components(self, plugin_name: str) -> list["ComponentInfo"]:
"""获取插件的所有组件""" """获取插件的所有组件"""
plugin_info = self.get_plugin_info(plugin_name) plugin_info = self.get_plugin_info(plugin_name)
logger.info(plugin_info.components)
return plugin_info.components if plugin_info else [] return plugin_info.components if plugin_info else []
def get_plugin_config(self, plugin_name: str) -> dict: def get_plugin_config(self, plugin_name: str) -> dict:
@@ -867,6 +918,7 @@ class ComponentRegistry:
plus_command_components: int = 0 plus_command_components: int = 0
chatter_components: int = 0 chatter_components: int = 0
prompt_components: int = 0 prompt_components: int = 0
router_components: int = 0
for component in self._components.values(): for component in self._components.values():
if component.component_type == ComponentType.ACTION: if component.component_type == ComponentType.ACTION:
action_components += 1 action_components += 1
@@ -882,6 +934,8 @@ class ComponentRegistry:
chatter_components += 1 chatter_components += 1
elif component.component_type == ComponentType.PROMPT: elif component.component_type == ComponentType.PROMPT:
prompt_components += 1 prompt_components += 1
elif component.component_type == ComponentType.ROUTER:
router_components += 1
return { return {
"action_components": action_components, "action_components": action_components,
"command_components": command_components, "command_components": command_components,
@@ -891,6 +945,7 @@ class ComponentRegistry:
"plus_command_components": plus_command_components, "plus_command_components": plus_command_components,
"chatter_components": chatter_components, "chatter_components": chatter_components,
"prompt_components": prompt_components, "prompt_components": prompt_components,
"router_components": router_components,
"total_components": len(self._components), "total_components": len(self._components),
"total_plugins": len(self._plugins), "total_plugins": len(self._plugins),
"components_by_type": { "components_by_type": {

View File

@@ -405,13 +405,14 @@ class PluginManager:
plus_command_count = stats.get("plus_command_components", 0) plus_command_count = stats.get("plus_command_components", 0)
chatter_count = stats.get("chatter_components", 0) chatter_count = stats.get("chatter_components", 0)
prompt_count = stats.get("prompt_components", 0) prompt_count = stats.get("prompt_components", 0)
router_count = stats.get("router_components", 0)
total_components = stats.get("total_components", 0) total_components = stats.get("total_components", 0)
# 📋 显示插件加载总览 # 📋 显示插件加载总览
if total_registered > 0: if total_registered > 0:
logger.info("🎉 插件系统加载完成!") logger.info("🎉 插件系统加载完成!")
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 = [ prompt_components = [
c for c in plugin_info.components if c.component_type == ComponentType.PROMPT 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: if action_components:
action_details = [format_component(c) for c in action_components] action_details = [format_component(c) for c in action_components]
@@ -478,6 +482,9 @@ class PluginManager:
if prompt_components: if prompt_components:
prompt_details = [format_component(c) for c in prompt_components] prompt_details = [format_component(c) for c in prompt_components]
logger.info(f" 📝 Prompt组件: {', '.join(prompt_details)}") 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): if plugin_instance := self.loaded_plugins.get(plugin_name):

View File

@@ -80,37 +80,39 @@ class ReplyTrackerService:
if old_data_file.exists(): if old_data_file.exists():
logger.info(f"检测到旧的数据文件 '{old_data_file}',开始执行一次性迁移...") logger.info(f"检测到旧的数据文件 '{old_data_file}',开始执行一次性迁移...")
try: try:
# 读取旧文件内容 # 步骤1: 读取旧文件内容并立即关闭文件
with open(old_data_file, "rb") as f: with open(old_data_file, "rb") as f:
file_content = f.read() file_content = f.read()
# 如果文件为空,直接删除,无需迁移
if not file_content.strip():
logger.warning("旧数据文件为空,无需迁移。")
os.remove(old_data_file)
logger.info(f"空的旧数据文件 '{old_data_file}' 已被删除。")
return
# 解析JSON数据 # 步骤2: 处理文件内容
old_data = orjson.loads(file_content) # 如果文件为空,直接删除,无需迁移
if not file_content.strip():
logger.warning("旧数据文件为空,无需迁移。")
os.remove(old_data_file)
logger.info(f"空的旧数据文件 '{old_data_file}' 已被删除。")
return
# 验证数据格式是否正确 # 解析JSON数据
if self._validate_data(old_data): old_data = orjson.loads(file_content)
# 验证通过将数据写入新的存储API
self.storage.set("data", old_data)
# 立即强制保存,确保迁移数据落盘
self.storage._save_data()
logger.info("旧数据已成功迁移到新的存储API。")
# 将旧文件重命名为备份文件,而不是直接删除,以防万一 # 步骤3: 验证数据并执行迁移/备份
backup_file = old_data_file.with_suffix(f".json.bak.migrated.{int(time.time())}") if self._validate_data(old_data):
old_data_file.rename(backup_file) # 验证通过将数据写入新的存储API
logger.info(f"旧数据文件已成功迁移并备份为: {backup_file}") self.storage.set("data", old_data)
else: # 立即强制保存,确保迁移数据落盘
# 如果数据格式无效,迁移中止,并备份损坏的文件 self.storage._save_data()
logger.error("旧数据文件格式无效,迁移中止") logger.info("旧数据已成功迁移到新的存储API")
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}") 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: except Exception as e:
# 捕获迁移过程中可能出现的任何异常 # 捕获迁移过程中可能出现的任何异常

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "7.7.1" version = "7.7.4"
#----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读---- #----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
@@ -59,6 +59,25 @@ cache_max_item_size_mb = 5 # 单个缓存条目最大大小MB超过此
# 示例:[["qq", "123456"], ["telegram", "user789"]] # 示例:[["qq", "123456"], ["telegram", "user789"]]
master_users = []# ["qq", "123456789"], # 示例QQ平台的Master用户 master_users = []# ["qq", "123456789"], # 示例QQ平台的Master用户
# ==================== 插件HTTP端点系统配置 ====================
[plugin_http_system]
# 总开关用于启用或禁用所有插件的HTTP端点功能
enable_plugin_http_endpoints = true
# ==================== 安全相关配置 ====================
# --- 插件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] # 主人身份提示词配置 [permission.master_prompt] # 主人身份提示词配置
enable = false # 是否启用主人/非主人提示注入 enable = false # 是否启用主人/非主人提示注入
master_hint = "你正在与自己的主人交流,注意展现亲切与尊重。" # 主人提示词 master_hint = "你正在与自己的主人交流,注意展现亲切与尊重。" # 主人提示词

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "1.3.8" version = "1.3.9"
# 配置文件版本号迭代规则同bot_config.toml # 配置文件版本号迭代规则同bot_config.toml
@@ -37,10 +37,11 @@ name = "deepseek-v3" # 模型名称(可随意命名,在后面
api_provider = "DeepSeek" # API服务商名称对应在api_providers中配置的服务商名称 api_provider = "DeepSeek" # API服务商名称对应在api_providers中配置的服务商名称
price_in = 2.0 # 输入价格用于API调用统计单位元/ M token可选若无该字段默认值为0 price_in = 2.0 # 输入价格用于API调用统计单位元/ M token可选若无该字段默认值为0
price_out = 8.0 # 输出价格用于API调用统计单位元/ M token可选若无该字段默认值为0 price_out = 8.0 # 输出价格用于API调用统计单位元/ M token可选若无该字段默认值为0
#force_stream_mode = true # 强制流式输出模式(若模型不支持非流式输出,请取消注释启用强制流式输出,若无该字段,默认值为false #force_stream_mode = false # [可选] 强制流式输出模式。如果模型不支持非流式输出,请取消注释启用。默认为 false
#use_anti_truncation = true # [可选] 启用反截断功能。当模型输出不完整时,系统会自动重试。建议只为需要的模型如Gemini开启。 #anti_truncation = false # [可选] 启用反截断功能。当模型输出不完整时系统会自动重试。建议只为需要的模型如Gemini开启。默认为 false。
#enable_content_obfuscation = true # [可选] 启用内容混淆功能,用于特定场景下的内容处理(例如某些内容审查比较严的模型和稀疏注意模型) #enable_prompt_perturbation = false # [可选] 启用提示词扰动。此功能整合了内容混淆和注意力优化,默认为 false。
#obfuscation_intensity = 2 # 混淆强度1-3级1=低强度2=中强度3=高强度) #perturbation_strength = "light" # [可选] 扰动强度。仅在 enable_prompt_perturbation 为 true 时生效。可选值为 "light", "medium", "heavy"。默认为 "light"。
#enable_semantic_variants = false # [可选] 启用语义变体。作为一种扰动策略,生成语义上相似但表达不同的提示。默认为 false。
[[models]] [[models]]
model_identifier = "deepseek-ai/DeepSeek-V3.2-Exp" model_identifier = "deepseek-ai/DeepSeek-V3.2-Exp"