Files
xiaoxia-saas/packages/infrastructure/module_registry.py
T
xiaoxia 53fb25efcf
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 48s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m30s
CI/CD Pipeline / Validate - Style (push) Successful in 4m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m33s
CI/CD Pipeline / Validate - Security (push) Successful in 7m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Successful in 10m11s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Failing after 26h14m3s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 26h24m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 26h19m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 26h23m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 26h23m44s
CI/CD Pipeline / Deploy Production (push) Failing after 26h13m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 26h13m26s
CI/CD Pipeline / CI Gate (push) Failing after 26h13m25s
CI/CD Pipeline / Build Production API Image (push) Failing after 26h13m26s
CI/CD Pipeline / Canary Release to Production (push) Failing after 26h13m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 26h19m46s
CI/CD Pipeline / Frontend Lint (push) Failing after 26h23m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 26h23m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 26h19m46s
fix(#1834): 批量修复 UP 系列静态分析警告(UP007/UP006/UP017/UP035) (#1928)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-15 12:59:17 +08:00

227 lines
7.8 KiB
Python

"""Module Registry - 模块注册中心
提供功能模块的动态注册、发现和管理能力。所有扩展模块(如 AI 配音、
多平台发布、去重检测等)通过此注册中心声明自身能力、依赖和配额消耗规则。
设计原则:
- 模块自描述:每个模块声明自己的能力、依赖、配额规则
- 松耦合:模块间通过注册中心间接通信,不直接依赖
- 可扩展:新模块只需注册即可被系统识别,无需修改核心代码
"""
from __future__ import annotations
import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, 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()