"""装配根:把配置变成可用的适配器。 **这是唯一允许同时知道「配置」与「具体供应商」的地方。** 其余模块只依赖抽象: 适配器通过构造参数拿配置(而非自己读环境变量),上层通过 `ProviderRegistry` 按 `provider_code` 拿适配器。新增一家供应商只需在此登记一行。 `app/providers/base.py`、`registry.py` 与 `app/domain/` 对具体供应商零依赖, 这一点由 `tests/test_layering.py` 自动守着——靠纪律守不住,靠测试才守得住。 """ from __future__ import annotations import logging from app.config import Settings from app.providers.base import GenerationProvider from app.providers.openai.client import OpenAiClient from app.providers.openai.image import OpenAiImageProvider from app.providers.openai.tasks import SyncTaskRegistry from app.providers.registry import ProviderRegistry from app.providers.volcengine.client import ArkClient from app.providers.volcengine.video import SeedanceVideoProvider log = logging.getLogger(__name__) def build_registry(settings: Settings) -> ProviderRegistry: """按配置装配所有可用的适配器。 **缺凭证的供应商被跳过而不是让装配失败**:本地只配了一家凭证时,服务仍应能 起来并提供其余能力与健康检查。真正的失败点在调用时——那时 `find()` 返回 None,调用方按「适配器不可用」处理,而不是把一个半死的实例放出去。 """ providers: list[GenerationProvider] = [] volcengine = settings.volcengine if volcengine.api_key: providers.append( SeedanceVideoProvider( ArkClient( api_key=volcengine.api_key, base_url=volcengine.base_url, timeout_sec=volcengine.timeout_sec, ), default_model=volcengine.video_model, task_expiry_sec=volcengine.task_expiry_sec, ) ) else: # 警告而非静默:这是最容易被忽略的配置缺失,而症状(任务永远排队) # 与它相隔很远。 log.warning("ARK_API_KEY 未配置,火山方舟视频适配器未装配") openai = settings.openai if openai.api_key: providers.append( OpenAiImageProvider( OpenAiClient( api_key=openai.api_key, base_url=openai.base_url, timeout_sec=openai.timeout_sec, organization=openai.organization or None, ), # 同步→异步的任务注册表。每个适配器实例一份:它承载的是「本进程 # 内哪些同步调用正在跑」,共享一份会让两个适配器互相挤占容量。 SyncTaskRegistry( max_workers=openai.max_concurrent, completed_ttl_sec=openai.completed_ttl_sec, max_entries=openai.max_inflight, ), default_model=openai.image_model, ) ) else: log.warning("OPENAI_API_KEY 未配置,图片生成适配器未装配") registry = ProviderRegistry(providers) log.info("已装配 %d 个生成适配器:%s", len(registry), sorted(registry.codes)) return registry