mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 实现功能模块系统,支持模块的动态注册与启用控制
- 新增 ModuleRegistry 核心,支持模块注册、可用性检查和启用状态管理 - 新增模块管理 API(管理端和公共端),提供模块状态查询和启用控制 - 将 LDAP 功能迁移到模块系统,支持通过环境变量和数据库配置控制 - 前端新增模块管理页面和 store,支持模块启用/禁用操作 - 路由守卫集成模块状态检查,未激活模块的页面自动重定向 - 面包屑导航支持模块配置页面的层级显示 - Switch 组件新增 disabled 属性支持
This commit is contained in:
@@ -5,8 +5,8 @@ from fastapi import APIRouter
|
||||
from .adaptive import router as adaptive_router
|
||||
from .api_keys import router as api_keys_router
|
||||
from .endpoints import router as endpoints_router
|
||||
from .ldap import router as ldap_router
|
||||
from .management_tokens import router as management_tokens_router
|
||||
from .modules import router as modules_router
|
||||
from .models import router as models_router
|
||||
from .monitoring import router as monitoring_router
|
||||
from .provider_query import router as provider_query_router
|
||||
@@ -30,7 +30,10 @@ router.include_router(adaptive_router)
|
||||
router.include_router(models_router)
|
||||
router.include_router(security_router)
|
||||
router.include_router(provider_query_router)
|
||||
router.include_router(ldap_router)
|
||||
router.include_router(management_tokens_router)
|
||||
router.include_router(modules_router)
|
||||
|
||||
# 注意:ldap_router 已迁移到模块系统,由 ModuleRegistry 动态注册
|
||||
# 当 LDAP_AVAILABLE=true 时才会注册路由
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
189
src/api/admin/modules.py
Normal file
189
src/api/admin/modules.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""模块管理 API 端点"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.api.base.admin_adapter import AdminApiAdapter
|
||||
from src.api.base.pipeline import ApiRequestPipeline
|
||||
from src.core.exceptions import InvalidRequestException, NotFoundException
|
||||
from src.core.modules import ModuleStatus, get_module_registry
|
||||
from src.database import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/admin/modules", tags=["Admin - Modules"])
|
||||
pipeline = ApiRequestPipeline()
|
||||
|
||||
|
||||
# ========== Response Models ==========
|
||||
|
||||
|
||||
class ModuleStatusResponse(BaseModel):
|
||||
"""模块状态响应"""
|
||||
|
||||
name: str
|
||||
available: bool
|
||||
enabled: bool
|
||||
active: bool
|
||||
display_name: str
|
||||
description: str
|
||||
category: str
|
||||
admin_route: Optional[str]
|
||||
admin_menu_icon: Optional[str]
|
||||
admin_menu_group: Optional[str]
|
||||
admin_menu_order: int
|
||||
health: str
|
||||
|
||||
@classmethod
|
||||
def from_status(cls, status: ModuleStatus) -> "ModuleStatusResponse":
|
||||
return cls(
|
||||
name=status.name,
|
||||
available=status.available,
|
||||
enabled=status.enabled,
|
||||
active=status.active,
|
||||
display_name=status.display_name,
|
||||
description=status.description,
|
||||
category=status.category.value,
|
||||
admin_route=status.admin_route,
|
||||
admin_menu_icon=status.admin_menu_icon,
|
||||
admin_menu_group=status.admin_menu_group,
|
||||
admin_menu_order=status.admin_menu_order,
|
||||
health=status.health.value,
|
||||
)
|
||||
|
||||
|
||||
class SetModuleEnabledRequest(BaseModel):
|
||||
"""设置模块启用状态请求"""
|
||||
|
||||
enabled: bool
|
||||
|
||||
|
||||
# ========== API Endpoints ==========
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_all_modules_status(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
获取所有模块状态
|
||||
|
||||
返回系统中所有已注册模块的状态信息,包括可用性、启用状态等。
|
||||
需要管理员权限。
|
||||
|
||||
**返回字段**:
|
||||
- 模块名称到状态的映射字典
|
||||
"""
|
||||
adapter = AdminGetAllModulesStatusAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/status/{module_name}")
|
||||
async def get_module_status(
|
||||
module_name: str, request: Request, db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取单个模块状态
|
||||
|
||||
获取指定模块的详细状态信息。需要管理员权限。
|
||||
|
||||
**路径参数**:
|
||||
- `module_name`: 模块名称
|
||||
|
||||
**返回字段**:
|
||||
- 模块状态详情
|
||||
"""
|
||||
adapter = AdminGetModuleStatusAdapter(module_name=module_name)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.put("/status/{module_name}/enabled")
|
||||
async def set_module_enabled(
|
||||
module_name: str, request: Request, db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
设置模块启用状态
|
||||
|
||||
启用或禁用指定模块。需要管理员权限。
|
||||
注意:只有 available=true 的模块才能被启用。
|
||||
|
||||
**路径参数**:
|
||||
- `module_name`: 模块名称
|
||||
|
||||
**请求体**:
|
||||
- `enabled`: 是否启用
|
||||
|
||||
**返回字段**:
|
||||
- 更新后的模块状态
|
||||
"""
|
||||
adapter = AdminSetModuleEnabledAdapter(module_name=module_name)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
# ========== Adapters ==========
|
||||
|
||||
|
||||
class AdminGetAllModulesStatusAdapter(AdminApiAdapter):
|
||||
"""获取所有模块状态"""
|
||||
|
||||
async def handle(self, context) -> Dict[str, Any]:
|
||||
registry = get_module_registry()
|
||||
all_status = await registry.get_all_status_async(context.db)
|
||||
|
||||
return {
|
||||
name: ModuleStatusResponse.from_status(status).model_dump()
|
||||
for name, status in all_status.items()
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminGetModuleStatusAdapter(AdminApiAdapter):
|
||||
"""获取单个模块状态"""
|
||||
|
||||
module_name: str
|
||||
|
||||
async def handle(self, context) -> Dict[str, Any]:
|
||||
registry = get_module_registry()
|
||||
status = await registry.get_module_status_async(self.module_name, context.db)
|
||||
|
||||
if status is None:
|
||||
raise NotFoundException(f"模块 '{self.module_name}' 不存在")
|
||||
|
||||
return ModuleStatusResponse.from_status(status).model_dump()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminSetModuleEnabledAdapter(AdminApiAdapter):
|
||||
"""设置模块启用状态"""
|
||||
|
||||
module_name: str
|
||||
|
||||
async def handle(self, context) -> Dict[str, Any]:
|
||||
registry = get_module_registry()
|
||||
|
||||
# 检查模块是否存在
|
||||
module = registry.get_module(self.module_name)
|
||||
if module is None:
|
||||
raise NotFoundException(f"模块 '{self.module_name}' 不存在")
|
||||
|
||||
# 检查模块是否可用
|
||||
if not registry.is_available(self.module_name):
|
||||
raise InvalidRequestException(
|
||||
f"模块 '{self.module_name}' 不可用,无法启用。"
|
||||
f"请检查环境变量 {module.metadata.env_key} 和依赖库。"
|
||||
)
|
||||
|
||||
# 解析请求体
|
||||
payload = context.ensure_json_body()
|
||||
try:
|
||||
req = SetModuleEnabledRequest.model_validate(payload)
|
||||
except Exception:
|
||||
raise InvalidRequestException("请求体格式错误,需要 enabled 字段")
|
||||
|
||||
# 设置启用状态
|
||||
registry.set_enabled(self.module_name, req.enabled, context.db)
|
||||
|
||||
# 返回更新后的状态(模块已在上面检查存在,此处必定返回非 None)
|
||||
status = registry.get_module_status(self.module_name, context.db)
|
||||
assert status is not None
|
||||
return ModuleStatusResponse.from_status(status).model_dump()
|
||||
@@ -7,6 +7,7 @@ from .catalog import router as catalog_router
|
||||
from .claude import router as claude_router
|
||||
from .gemini import router as gemini_router
|
||||
from .models import router as models_router
|
||||
from .modules import router as modules_router
|
||||
from .openai import router as openai_router
|
||||
from .system_catalog import router as system_catalog_router
|
||||
|
||||
@@ -19,5 +20,6 @@ router.include_router(gemini_router, tags=["Gemini API"])
|
||||
router.include_router(system_catalog_router, tags=["System Catalog"])
|
||||
router.include_router(catalog_router)
|
||||
router.include_router(capabilities_router)
|
||||
router.include_router(modules_router)
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
46
src/api/public/modules.py
Normal file
46
src/api/public/modules.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""公开模块状态 API(供登录页等使用)"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.modules import ModuleCategory, get_module_registry
|
||||
from src.database import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/modules", tags=["Modules"])
|
||||
|
||||
|
||||
class AuthModuleInfo(BaseModel):
|
||||
"""认证模块简要信息"""
|
||||
|
||||
name: str
|
||||
display_name: str
|
||||
active: bool
|
||||
|
||||
|
||||
@router.get("/auth-status", response_model=List[AuthModuleInfo])
|
||||
async def get_auth_modules_status(db: Session = Depends(get_db)):
|
||||
"""
|
||||
获取认证模块状态(公开接口)
|
||||
|
||||
供登录页使用,返回所有可用的认证模块及其激活状态。
|
||||
不需要认证即可访问。
|
||||
|
||||
**返回字段**:
|
||||
- `name`: 模块名称
|
||||
- `display_name`: 显示名称
|
||||
- `active`: 是否激活
|
||||
"""
|
||||
registry = get_module_registry()
|
||||
auth_modules = registry.get_auth_modules_status(db)
|
||||
|
||||
return [
|
||||
AuthModuleInfo(
|
||||
name=status.name,
|
||||
display_name=status.display_name,
|
||||
active=status.active,
|
||||
)
|
||||
for status in auth_modules
|
||||
]
|
||||
26
src/core/modules/__init__.py
Normal file
26
src/core/modules/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
模块化系统核心
|
||||
|
||||
提供可扩展的功能模块管理,支持:
|
||||
- 声明式模块注册
|
||||
- available/enabled 双层状态控制
|
||||
- 延迟导入避免重依赖加载
|
||||
- 前后端状态同步
|
||||
"""
|
||||
|
||||
from src.core.modules.base import (
|
||||
ModuleCategory,
|
||||
ModuleDefinition,
|
||||
ModuleMetadata,
|
||||
ModuleStatus,
|
||||
)
|
||||
from src.core.modules.registry import ModuleRegistry, get_module_registry
|
||||
|
||||
__all__ = [
|
||||
"ModuleCategory",
|
||||
"ModuleMetadata",
|
||||
"ModuleDefinition",
|
||||
"ModuleStatus",
|
||||
"ModuleRegistry",
|
||||
"get_module_registry",
|
||||
]
|
||||
113
src/core/modules/base.py
Normal file
113
src/core/modules/base.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
模块基础定义
|
||||
|
||||
包含模块元数据、定义和状态的数据结构
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
class ModuleCategory(str, Enum):
|
||||
"""模块分类"""
|
||||
|
||||
AUTH = "auth" # 认证相关
|
||||
MONITORING = "monitoring" # 监控相关
|
||||
SECURITY = "security" # 安全相关
|
||||
INTEGRATION = "integration" # 第三方集成
|
||||
|
||||
|
||||
class ModuleHealth(str, Enum):
|
||||
"""模块健康状态"""
|
||||
|
||||
HEALTHY = "healthy"
|
||||
DEGRADED = "degraded"
|
||||
UNHEALTHY = "unhealthy"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleMetadata:
|
||||
"""
|
||||
模块元数据 - 纯数据描述,无重依赖
|
||||
|
||||
用于声明式定义模块的基本信息和配置
|
||||
"""
|
||||
|
||||
# 基本信息
|
||||
name: str # 唯一标识: ldap, audit_log
|
||||
display_name: str # 显示名称: "LDAP 认证"
|
||||
description: str # 模块描述
|
||||
|
||||
# 分类
|
||||
category: ModuleCategory
|
||||
|
||||
# 可用性控制(部署级)
|
||||
env_key: str # 环境变量名: LDAP_AVAILABLE
|
||||
default_available: bool = False # 默认是否可用
|
||||
required_packages: List[str] = field(default_factory=list) # 依赖的 Python 包
|
||||
dependencies: List[str] = field(default_factory=list) # 依赖的其他模块
|
||||
|
||||
# 路由配置 - 模块自定义前缀
|
||||
api_prefix: Optional[str] = None # 如 "/api/admin/ldap"
|
||||
|
||||
# 前端配置
|
||||
admin_route: Optional[str] = None # 管理页面路由: "/admin/ldap"
|
||||
admin_menu_icon: Optional[str] = None # 菜单图标
|
||||
admin_menu_group: Optional[str] = None # 菜单分组: "system", "security"
|
||||
admin_menu_order: int = 100 # 菜单排序(越小越靠前)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleDefinition:
|
||||
"""
|
||||
完整模块定义
|
||||
|
||||
包含元数据和生命周期钩子,钩子函数内部延迟导入重依赖
|
||||
"""
|
||||
|
||||
metadata: ModuleMetadata
|
||||
|
||||
# 工厂函数 - 内部再 import 重依赖
|
||||
router_factory: Optional[Callable[[], "APIRouter"]] = None
|
||||
service_factory: Optional[Callable[[], Any]] = None
|
||||
|
||||
# 生命周期钩子
|
||||
on_startup: Optional[Callable[[], Awaitable[None]]] = None
|
||||
on_shutdown: Optional[Callable[[], Awaitable[None]]] = None
|
||||
health_check: Optional[Callable[[], Awaitable[ModuleHealth]]] = None
|
||||
|
||||
# 自定义依赖检测(可选,用于检测 ldap3 等库是否安装)
|
||||
check_dependencies: Optional[Callable[[], bool]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleStatus:
|
||||
"""
|
||||
模块运行状态
|
||||
|
||||
用于 API 返回,供前端使用
|
||||
"""
|
||||
|
||||
name: str
|
||||
available: bool # 部署级可用(环境变量 + 依赖库)
|
||||
enabled: bool # 运行级启用(数据库配置)
|
||||
active: bool # 最终激活状态 (available && enabled && dependencies_ok)
|
||||
|
||||
# 显示信息
|
||||
display_name: str
|
||||
description: str
|
||||
category: ModuleCategory
|
||||
|
||||
# 前端配置
|
||||
admin_route: Optional[str]
|
||||
admin_menu_icon: Optional[str]
|
||||
admin_menu_group: Optional[str]
|
||||
admin_menu_order: int
|
||||
|
||||
# 健康状态
|
||||
health: ModuleHealth = ModuleHealth.UNKNOWN
|
||||
288
src/core/modules/registry.py
Normal file
288
src/core/modules/registry.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
模块注册中心
|
||||
|
||||
负责模块的注册、状态管理和生命周期控制
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set
|
||||
|
||||
from src.core.logger import logger
|
||||
from src.core.modules.base import (
|
||||
ModuleCategory,
|
||||
ModuleDefinition,
|
||||
ModuleHealth,
|
||||
ModuleStatus,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
class ModuleRegistry:
|
||||
"""
|
||||
模块注册中心 - 单例模式
|
||||
|
||||
职责:
|
||||
- 注册模块定义(仅元数据,不加载重依赖)
|
||||
- 检查模块可用性(环境变量 + 依赖库)
|
||||
- 管理模块启用状态(数据库配置)
|
||||
- 提供模块状态查询
|
||||
"""
|
||||
|
||||
_instance: Optional["ModuleRegistry"] = None
|
||||
|
||||
def __init__(self):
|
||||
self._modules: Dict[str, ModuleDefinition] = {}
|
||||
self._initialized: Set[str] = set()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "ModuleRegistry":
|
||||
"""获取单例实例"""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def reset_instance(cls) -> None:
|
||||
"""重置单例(仅用于测试)"""
|
||||
cls._instance = None
|
||||
|
||||
def register(self, module: ModuleDefinition) -> None:
|
||||
"""
|
||||
注册模块
|
||||
|
||||
仅注册元数据,不加载重依赖
|
||||
"""
|
||||
name = module.metadata.name
|
||||
if name in self._modules:
|
||||
logger.warning(f"Module [{name}] already registered, skipping")
|
||||
return
|
||||
|
||||
self._modules[name] = module
|
||||
logger.debug(f"Module [{name}] registered")
|
||||
|
||||
def get_module(self, name: str) -> Optional[ModuleDefinition]:
|
||||
"""获取模块定义"""
|
||||
return self._modules.get(name)
|
||||
|
||||
def get_all_modules(self) -> List[ModuleDefinition]:
|
||||
"""获取所有已注册模块"""
|
||||
return list(self._modules.values())
|
||||
|
||||
# ========== 可用性检查(部署级)==========
|
||||
|
||||
def is_available(self, name: str) -> bool:
|
||||
"""
|
||||
检查模块是否部署可用
|
||||
|
||||
检查顺序:
|
||||
1. 模块是否已注册
|
||||
2. 环境变量是否启用
|
||||
3. 依赖的 Python 包是否安装
|
||||
4. 自定义依赖检测(如果有)
|
||||
"""
|
||||
if name not in self._modules:
|
||||
return False
|
||||
|
||||
module = self._modules[name]
|
||||
meta = module.metadata
|
||||
|
||||
# 1. 检查环境变量
|
||||
env_value = os.getenv(meta.env_key)
|
||||
if env_value is not None:
|
||||
if env_value.lower() not in ("true", "1", "yes"):
|
||||
return False
|
||||
elif not meta.default_available:
|
||||
return False
|
||||
|
||||
# 2. 检查依赖的 Python 包
|
||||
for pkg in meta.required_packages:
|
||||
if importlib.util.find_spec(pkg) is None:
|
||||
logger.debug(f"Module [{name}] unavailable: package '{pkg}' not installed")
|
||||
return False
|
||||
|
||||
# 3. 自定义依赖检测
|
||||
if module.check_dependencies:
|
||||
try:
|
||||
if not module.check_dependencies():
|
||||
logger.debug(f"Module [{name}] unavailable: custom dependency check failed")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"Module [{name}] dependency check error: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_available_modules(self) -> List[ModuleDefinition]:
|
||||
"""获取所有部署可用的模块"""
|
||||
return [m for m in self._modules.values() if self.is_available(m.metadata.name)]
|
||||
|
||||
# ========== 启用状态检查(运行级)==========
|
||||
|
||||
def is_enabled(self, name: str, db: "Session") -> bool:
|
||||
"""
|
||||
检查模块是否运行启用(数据库配置)
|
||||
|
||||
Args:
|
||||
name: 模块名称
|
||||
db: 数据库会话
|
||||
"""
|
||||
from src.services.system.config import SystemConfigService
|
||||
|
||||
config_key = f"module.{name}.enabled"
|
||||
value = SystemConfigService.get_config(db, config_key, default=False)
|
||||
return bool(value)
|
||||
|
||||
def set_enabled(self, name: str, enabled: bool, db: "Session") -> None:
|
||||
"""
|
||||
设置模块启用状态
|
||||
|
||||
Args:
|
||||
name: 模块名称
|
||||
enabled: 是否启用
|
||||
db: 数据库会话
|
||||
"""
|
||||
from src.services.system.config import SystemConfigService
|
||||
|
||||
if name not in self._modules:
|
||||
raise ValueError(f"Module [{name}] not registered")
|
||||
|
||||
config_key = f"module.{name}.enabled"
|
||||
module = self._modules[name]
|
||||
description = f"模块 [{module.metadata.display_name}] 启用状态"
|
||||
SystemConfigService.set_config(db, config_key, enabled, description)
|
||||
|
||||
# ========== 激活状态检查 ==========
|
||||
|
||||
def is_active(self, name: str, db: "Session") -> bool:
|
||||
"""
|
||||
检查模块是否最终激活
|
||||
|
||||
激活条件:available && enabled && 依赖模块都激活
|
||||
"""
|
||||
if not self.is_available(name):
|
||||
return False
|
||||
if not self.is_enabled(name, db):
|
||||
return False
|
||||
|
||||
# 检查依赖模块
|
||||
module = self._modules[name]
|
||||
for dep in module.metadata.dependencies:
|
||||
if not self.is_active(dep, db):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# ========== 状态查询 ==========
|
||||
|
||||
def get_module_status(
|
||||
self, name: str, db: "Session", health: Optional[ModuleHealth] = None
|
||||
) -> Optional[ModuleStatus]:
|
||||
"""
|
||||
获取单个模块状态
|
||||
|
||||
Args:
|
||||
name: 模块名称
|
||||
db: 数据库会话
|
||||
health: 预先获取的健康状态(可选,用于异步场景)
|
||||
"""
|
||||
if name not in self._modules:
|
||||
return None
|
||||
|
||||
module = self._modules[name]
|
||||
meta = module.metadata
|
||||
available = self.is_available(name)
|
||||
|
||||
return ModuleStatus(
|
||||
name=name,
|
||||
available=available,
|
||||
enabled=self.is_enabled(name, db) if available else False,
|
||||
active=self.is_active(name, db) if available else False,
|
||||
display_name=meta.display_name,
|
||||
description=meta.description,
|
||||
category=meta.category,
|
||||
admin_route=meta.admin_route if available else None,
|
||||
admin_menu_icon=meta.admin_menu_icon,
|
||||
admin_menu_group=meta.admin_menu_group,
|
||||
admin_menu_order=meta.admin_menu_order,
|
||||
health=health if health else ModuleHealth.UNKNOWN,
|
||||
)
|
||||
|
||||
async def check_health(self, name: str) -> ModuleHealth:
|
||||
"""
|
||||
执行模块健康检查
|
||||
|
||||
Args:
|
||||
name: 模块名称
|
||||
|
||||
Returns:
|
||||
健康状态
|
||||
"""
|
||||
if name not in self._modules:
|
||||
return ModuleHealth.UNKNOWN
|
||||
|
||||
module = self._modules[name]
|
||||
if not module.health_check:
|
||||
return ModuleHealth.UNKNOWN
|
||||
|
||||
try:
|
||||
return await module.health_check()
|
||||
except Exception as e:
|
||||
logger.warning(f"Module [{name}] health check failed: {e}")
|
||||
return ModuleHealth.UNHEALTHY
|
||||
|
||||
async def get_module_status_async(
|
||||
self, name: str, db: "Session"
|
||||
) -> Optional[ModuleStatus]:
|
||||
"""异步获取模块状态(包含健康检查)"""
|
||||
if name not in self._modules:
|
||||
return None
|
||||
|
||||
health = await self.check_health(name) if self.is_available(name) else ModuleHealth.UNKNOWN
|
||||
return self.get_module_status(name, db, health=health)
|
||||
|
||||
async def get_all_status_async(self, db: "Session") -> Dict[str, ModuleStatus]:
|
||||
"""异步获取所有模块状态(包含健康检查)"""
|
||||
result = {}
|
||||
for name in self._modules:
|
||||
status = await self.get_module_status_async(name, db)
|
||||
if status:
|
||||
result[name] = status
|
||||
return result
|
||||
|
||||
def get_all_status(self, db: "Session") -> Dict[str, ModuleStatus]:
|
||||
"""获取所有模块状态(同步版本,不含健康检查)"""
|
||||
result = {}
|
||||
for name in self._modules:
|
||||
status = self.get_module_status(name, db)
|
||||
if status:
|
||||
result[name] = status
|
||||
return result
|
||||
|
||||
def get_available_status(self, db: "Session") -> Dict[str, ModuleStatus]:
|
||||
"""获取所有可用模块的状态"""
|
||||
result = {}
|
||||
for name, module in self._modules.items():
|
||||
if self.is_available(name):
|
||||
status = self.get_module_status(name, db)
|
||||
if status:
|
||||
result[name] = status
|
||||
return result
|
||||
|
||||
def get_auth_modules_status(self, db: "Session") -> List[ModuleStatus]:
|
||||
"""获取认证模块状态(供登录页使用)"""
|
||||
result = []
|
||||
for name, module in self._modules.items():
|
||||
if module.metadata.category == ModuleCategory.AUTH:
|
||||
if self.is_available(name):
|
||||
status = self.get_module_status(name, db)
|
||||
if status:
|
||||
result.append(status)
|
||||
return result
|
||||
|
||||
|
||||
def get_module_registry() -> ModuleRegistry:
|
||||
"""获取模块注册中心实例"""
|
||||
return ModuleRegistry.get_instance()
|
||||
31
src/main.py
31
src/main.py
@@ -24,6 +24,7 @@ from src.clients.http_client import HTTPClientPool, close_http_clients
|
||||
from src.config import config
|
||||
from src.core.exceptions import ExceptionHandlers, ProxyException
|
||||
from src.core.logger import logger
|
||||
from src.core.modules import get_module_registry
|
||||
from src.database import init_db
|
||||
|
||||
from src.middleware.plugin_middleware import PluginMiddleware
|
||||
@@ -159,6 +160,30 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
register_all_converters()
|
||||
|
||||
# 初始化功能模块系统
|
||||
logger.info("初始化功能模块系统...")
|
||||
from src.modules import ALL_MODULES
|
||||
|
||||
module_registry = get_module_registry()
|
||||
for module in ALL_MODULES:
|
||||
module_registry.register(module)
|
||||
|
||||
# 注册可用模块的路由
|
||||
# 注意:模块的 router 自带 prefix,api_prefix 字段仅用于日志和文档
|
||||
available_modules = module_registry.get_available_modules()
|
||||
for module in available_modules:
|
||||
if module.router_factory:
|
||||
router = module.router_factory()
|
||||
app.include_router(router)
|
||||
prefix = module.metadata.api_prefix or "(default)"
|
||||
logger.info(f"模块 [{module.metadata.name}] 路由已注册: {prefix}")
|
||||
|
||||
# 执行启动钩子
|
||||
if module.on_startup:
|
||||
await module.on_startup()
|
||||
|
||||
logger.info(f"功能模块初始化完成: {len(available_modules)}/{len(ALL_MODULES)} 个模块可用")
|
||||
|
||||
logger.info(f"服务启动成功: http://{config.host}:{config.port}")
|
||||
logger.info("=" * 60)
|
||||
|
||||
@@ -250,6 +275,12 @@ async def lifespan(app: FastAPI):
|
||||
logger.info("关闭插件系统...")
|
||||
await plugin_manager.shutdown_all()
|
||||
|
||||
# 关闭功能模块
|
||||
logger.info("关闭功能模块...")
|
||||
for module in available_modules:
|
||||
if module.on_shutdown:
|
||||
await module.on_shutdown()
|
||||
|
||||
# 关闭并发管理器
|
||||
logger.info("关闭并发管理器...")
|
||||
if concurrency_manager:
|
||||
|
||||
19
src/modules/__init__.py
Normal file
19
src/modules/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
功能模块注册
|
||||
|
||||
所有可选功能模块在此注册
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from src.core.modules.base import ModuleDefinition
|
||||
|
||||
# 导入所有模块定义
|
||||
from src.modules.ldap import ldap_module
|
||||
|
||||
# 所有模块列表
|
||||
ALL_MODULES: List[ModuleDefinition] = [
|
||||
ldap_module,
|
||||
]
|
||||
|
||||
__all__ = ["ALL_MODULES"]
|
||||
50
src/modules/ldap/__init__.py
Normal file
50
src/modules/ldap/__init__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
LDAP 认证模块
|
||||
|
||||
提供 LDAP/Active Directory 用户认证支持
|
||||
"""
|
||||
|
||||
from src.core.modules.base import (
|
||||
ModuleCategory,
|
||||
ModuleDefinition,
|
||||
ModuleHealth,
|
||||
ModuleMetadata,
|
||||
)
|
||||
|
||||
|
||||
def _get_router():
|
||||
"""延迟导入路由(避免启动时加载重依赖)"""
|
||||
from src.api.admin.ldap import router
|
||||
|
||||
return router
|
||||
|
||||
|
||||
async def _health_check() -> ModuleHealth:
|
||||
"""健康检查 - 简化版,不依赖数据库连接"""
|
||||
# 健康检查在启动时调用,此时可能没有数据库会话
|
||||
# 返回 UNKNOWN 表示需要进一步检查
|
||||
return ModuleHealth.UNKNOWN
|
||||
|
||||
|
||||
# LDAP 模块定义
|
||||
ldap_module = ModuleDefinition(
|
||||
metadata=ModuleMetadata(
|
||||
name="ldap",
|
||||
display_name="LDAP 认证",
|
||||
description="支持通过 LDAP/Active Directory 进行用户认证",
|
||||
category=ModuleCategory.AUTH,
|
||||
# 可用性控制
|
||||
env_key="LDAP_AVAILABLE",
|
||||
default_available=True,
|
||||
required_packages=["ldap3"],
|
||||
# 路由配置(使用现有路由,不改变路径)
|
||||
api_prefix="/api/admin/ldap",
|
||||
# 前端配置
|
||||
admin_route="/admin/ldap",
|
||||
admin_menu_icon="Users",
|
||||
admin_menu_group="system",
|
||||
admin_menu_order=50,
|
||||
),
|
||||
router_factory=_get_router,
|
||||
health_check=_health_check,
|
||||
)
|
||||
@@ -130,7 +130,19 @@ class LDAPService:
|
||||
def get_config_data(db: Session) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
提前获取并解密配置,供线程池使用,避免跨线程共享 Session。
|
||||
|
||||
检查顺序:
|
||||
1. LDAP 模块是否激活(available && enabled)
|
||||
2. LDAP 配置是否启用
|
||||
3. 绑定密码是否可解密
|
||||
"""
|
||||
# 检查 LDAP 模块是否激活
|
||||
from src.core.modules import get_module_registry
|
||||
|
||||
registry = get_module_registry()
|
||||
if not registry.is_active("ldap", db):
|
||||
return None
|
||||
|
||||
config = LDAPService.get_config(db)
|
||||
if not config or config.is_enabled is not True:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user