mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +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
|
||||
]
|
||||
Reference in New Issue
Block a user