9c6c477f55
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m22s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 4m3s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
P0 关键修复: - P0-1: 注册接口添加 RateLimitMiddleware 限流保护 - P0-3: /metrics 端点添加 JWT 认证(移除匿名访问) - P0-4: 修复 Celery 任务名冲突(generation_task vs generate_video) - P1-5: JWT logout token 黑名单机制 P1 修复: - P1-1: forgot_password 硬编码 localhost → 使用 settings.APP_BASE_URL - P1-2: generation.py 直接创建 DB 连接 → 使用依赖注入 - P1-6: Image.open() 未关闭 → 统一使用 with 语句 - P1-7: 订阅续费事务修复 P2 代码质量: - P2-1: 修复 EditingMode 枚举重复定义 → 统一引用 shared 包 - P2-2: 修复 SMTP_FRON_NAME → SMTP_FROM_NAME 拼写 - P2-3: UserModel subscription_quota 类型统一为 float - P2-4: .env.production DATABASE_MAX_OVERFLOW 30 → 10 - 清理 15 处 except:pass(保留 2 处有注释说明的) - 禁用 SVG 上传(XSS 风险) - 删除 decode_token_unsafe() 不安全函数 - 简化 /ready 端点 - 删除 8 处死代码、10 个空文件/模块 - 合并 3 对 100% 重复函数 - 对齐 6 个废弃环境变量 v2 修复(代码审查后): - 修复密码重置路由路径: /password/forgot → /forgot-password, /password/reset → /reset-password(与前端 API 对齐) - 合并 _check_project_access: asset_libraries.py 和 edit_plans.py 中的重复函数统一到 _helpers.py(含空字符串守卫 + 中文错误信息) - 顺手修复: HTTPException 统一从 fastapi 导入(替换 starlette 导入) - OSS_ENDPOINT 拼写修复拆分为单独 PR,本 PR 不包含
226 lines
7.8 KiB
Python
226 lines
7.8 KiB
Python
"""Module Registry - 模块注册中心
|
|
|
|
提供功能模块的动态注册、发现和管理能力。所有扩展模块(如 AI 配音、
|
|
多平台发布、去重检测等)通过此注册中心声明自身能力、依赖和配额消耗规则。
|
|
|
|
设计原则:
|
|
- 模块自描述:每个模块声明自己的能力、依赖、配额规则
|
|
- 松耦合:模块间通过注册中心间接通信,不直接依赖
|
|
- 可扩展:新模块只需注册即可被系统识别,无需修改核心代码
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ModuleStatus(str, Enum):
|
|
"""模块运行状态"""
|
|
|
|
REGISTERED = "registered" # 已注册,未激活
|
|
ACTIVE = "active" # 已激活,可用
|
|
DISABLED = "disabled" # 已禁用(管理员/Feature Flag 控制)
|
|
ERROR = "error" # 注册或初始化出错
|
|
|
|
|
|
@dataclass
|
|
class QuotaRule:
|
|
"""模块定义的配额消耗规则
|
|
|
|
描述该模块消耗哪些配额维度,以及每个操作消耗多少。
|
|
例如:AI 配音模块每生成一条配音消耗 1 个 ai_voice_credit。
|
|
"""
|
|
|
|
dimension: str # 配额维度名,如 "ai_voice_credits", "storage_gb"
|
|
per_operation: float # 每次操作消耗量
|
|
description: str = "" # 人类可读描述
|
|
|
|
|
|
@dataclass
|
|
class ModuleCapability:
|
|
"""模块声明的一个能力
|
|
|
|
能力是模块对外暴露的可调用功能单元。
|
|
"""
|
|
|
|
name: str # 能力名,如 "generate_voice"
|
|
description: str = "" # 人类可读描述
|
|
quota_rules: List[QuotaRule] = field(default_factory=list) # 该能力消耗的配额规则
|
|
metadata: Dict[str, Any] = field(default_factory=dict) # 扩展元数据
|
|
|
|
|
|
@dataclass
|
|
class Module:
|
|
"""模块注册信息
|
|
|
|
每个扩展模块通过 Module 描述自身,注册到 ModuleRegistry。
|
|
"""
|
|
|
|
name: str # 模块唯一标识,如 "ai_voice"
|
|
version: str = "1.0.0" # 模块版本
|
|
description: str = "" # 人类可读描述
|
|
capabilities: List[ModuleCapability] = field(default_factory=list)
|
|
dependencies: List[str] = field(default_factory=list) # 依赖的其他模块名
|
|
status: ModuleStatus = ModuleStatus.REGISTERED
|
|
config: Dict[str, Any] = field(default_factory=dict) # 模块配置
|
|
_init_func: Optional[Callable] = field(default=None, repr=False) # 初始化回调
|
|
|
|
def activate(self) -> None:
|
|
"""激活模块"""
|
|
if self.status == ModuleStatus.ERROR:
|
|
logger.error(f"Cannot activate module '{self.name}': in error state")
|
|
return
|
|
self.status = ModuleStatus.ACTIVE
|
|
logger.info(f"Module '{self.name}' v{self.version} activated")
|
|
|
|
def disable(self) -> None:
|
|
"""禁用模块"""
|
|
self.status = ModuleStatus.DISABLED
|
|
logger.info(f"Module '{self.name}' disabled")
|
|
|
|
|
|
class ModuleRegistry:
|
|
"""模块注册中心
|
|
|
|
单例模式,全局唯一实例(module_registry)。
|
|
提供模块注册、发现、依赖检查、能力查询等功能。
|
|
|
|
Usage:
|
|
from packages.infrastructure.module_registry import module_registry
|
|
|
|
# 注册模块
|
|
module_registry.register(Module(
|
|
name="ai_voice",
|
|
version="1.0.0",
|
|
description="AI 配音生成模块",
|
|
capabilities=[
|
|
ModuleCapability(
|
|
name="generate_voice",
|
|
description="文本转配音",
|
|
quota_rules=[QuotaRule("ai_voice_credits", 1.0, "每次配音消耗 1 积分")],
|
|
),
|
|
],
|
|
))
|
|
|
|
# 查询
|
|
mod = module_registry.get("ai_voice")
|
|
has_cap = module_registry.has_capability("generate_voice")
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._modules: Dict[str, Module] = {}
|
|
|
|
def register(self, module: Module) -> None:
|
|
"""注册一个模块
|
|
|
|
Args:
|
|
module: 要注册的模块
|
|
|
|
Raises:
|
|
ValueError: 模块名已存在
|
|
"""
|
|
if module.name in self._modules:
|
|
raise ValueError(f"Module '{module.name}' already registered")
|
|
|
|
# 检查依赖是否已注册
|
|
for dep in module.dependencies:
|
|
if dep not in self._modules:
|
|
logger.warning(
|
|
f"Module '{module.name}' depends on '{dep}' which is not yet registered. "
|
|
f"Call check_dependencies() after all modules are registered."
|
|
)
|
|
|
|
self._modules[module.name] = module
|
|
logger.info(f"Module '{module.name}' v{module.version} registered")
|
|
|
|
# 自动尝试激活
|
|
if self.check_dependencies(module.name):
|
|
module.activate()
|
|
|
|
def unregister(self, name: str) -> None:
|
|
"""注销一个模块"""
|
|
if name not in self._modules:
|
|
raise KeyError(f"Module '{name}' not found")
|
|
|
|
# 检查是否有其他模块依赖它
|
|
dependents = [m.name for m in self._modules.values() if name in m.dependencies and m.name != name]
|
|
if dependents:
|
|
raise ValueError(f"Cannot unregister module '{name}': depended on by {dependents}")
|
|
|
|
del self._modules[name]
|
|
logger.info(f"Module '{name}' unregistered")
|
|
|
|
def get(self, name: str) -> Optional[Module]:
|
|
"""按名称获取模块,不存在返回 None"""
|
|
return self._modules.get(name)
|
|
|
|
def list_modules(self, status: Optional[ModuleStatus] = None) -> List[Module]:
|
|
"""列出所有模块,可按状态过滤"""
|
|
modules = list(self._modules.values())
|
|
if status is not None:
|
|
modules = [m for m in modules if m.status == status]
|
|
return modules
|
|
|
|
def has_capability(self, capability_name: str) -> bool:
|
|
"""检查是否有任何已激活模块提供指定能力"""
|
|
for module in self._modules.values():
|
|
if module.status != ModuleStatus.ACTIVE:
|
|
continue
|
|
for cap in module.capabilities:
|
|
if cap.name == capability_name:
|
|
return True
|
|
return False
|
|
|
|
def get_capability(self, capability_name: str) -> Optional[ModuleCapability]:
|
|
"""获取指定能力的定义,返回第一个匹配的"""
|
|
for module in self._modules.values():
|
|
if module.status != ModuleStatus.ACTIVE:
|
|
continue
|
|
for cap in module.capabilities:
|
|
if cap.name == capability_name:
|
|
return cap
|
|
return None
|
|
|
|
def get_quota_rules(self, capability_name: str) -> List[QuotaRule]:
|
|
"""获取指定能力的配额消耗规则"""
|
|
cap = self.get_capability(capability_name)
|
|
if cap is None:
|
|
return []
|
|
return cap.quota_rules
|
|
|
|
def check_dependencies(self, module_name: str) -> bool:
|
|
"""检查指定模块的所有依赖是否都已注册且激活"""
|
|
module = self._modules.get(module_name)
|
|
if module is None:
|
|
return False
|
|
for dep in module.dependencies:
|
|
dep_module = self._modules.get(dep)
|
|
if dep_module is None or dep_module.status != ModuleStatus.ACTIVE:
|
|
return False
|
|
return True
|
|
|
|
def get_active_capabilities(self) -> Dict[str, List[str]]:
|
|
"""获取所有已激活模块的能力列表
|
|
|
|
Returns:
|
|
{module_name: [capability_name, ...]}
|
|
"""
|
|
result = {}
|
|
for module in self._modules.values():
|
|
if module.status == ModuleStatus.ACTIVE and module.capabilities:
|
|
result[module.name] = [cap.name for cap in module.capabilities]
|
|
return result
|
|
|
|
def clear(self) -> None:
|
|
"""清空所有注册(仅用于测试)"""
|
|
self._modules.clear()
|
|
|
|
|
|
# 全局单例
|
|
module_registry = ModuleRegistry()
|