feat: enable local docker demo mode
Some checks failed
Build and Push Docker Images / changes (push) Has been cancelled
Build and Push Docker Images / build-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled
Build and Push Docker Images / build-admin-frontend (push) Has been cancelled

This commit is contained in:
2026-04-18 12:01:27 +08:00
parent 0613238a37
commit 44405ff7ac
12 changed files with 341 additions and 36 deletions

View File

@@ -33,14 +33,13 @@ class RoutingStrategy(str, Enum):
ROUND_ROBIN = "round_robin" # 轮询
# 默认配置映射(当 DB 无配置时使用)
# 默认配置映射(当 DB 无配置时使用)
# 这是“代码级”的默认策略,对应 .env 为空的情况
DEFAULT_PROVIDERS: dict[ProviderType, list[str]] = {
"text": ["gemini", "openai"],
"image": ["cqtai"],
"tts": ["minimax", "elevenlabs", "edge_tts"],
"storybook": ["gemini"],
"storybook": ["storybook_primary"],
}
# API Key 映射adapter_name -> settings 属性名
@@ -88,6 +87,13 @@ def _get_api_key(config_ref: str | None, adapter_name: str) -> str:
def _get_default_config(adapter_name: str) -> AdapterConfig | None:
"""获取适配器的默认配置(无 DB 记录时使用)。返回 None 表示未知适配器。"""
if adapter_name == "demo":
return AdapterConfig(
api_key="",
model="demo",
timeout_ms=1000,
)
# --- Text Defaults ---
if adapter_name in ("gemini", "text_primary"):
return AdapterConfig(
@@ -103,7 +109,7 @@ def _get_default_config(adapter_name: str) -> AdapterConfig | None:
)
# --- Image Defaults ---
if adapter_name in ("cqtai"):
if adapter_name == "cqtai":
return AdapterConfig(
api_key=getattr(settings, "cqtai_api_key", ""),
model=settings.image_model or "nano-banana-pro",
@@ -194,8 +200,12 @@ async def _get_providers_with_config(
"text": settings.text_providers,
"image": settings.image_providers,
"tts": settings.tts_providers,
"storybook": settings.storybook_providers,
}
names = settings_map.get(provider_type) or DEFAULT_PROVIDERS[provider_type]
if settings.enable_demo_providers and "demo" not in names:
names = ["demo", *names]
result = []
for name in names:
config = _get_default_config(name)
@@ -273,12 +283,17 @@ async def _route_with_failover(
# 按策略排序
sorted_providers = _sort_by_strategy(providers, strategy, provider_type)
# 如果有 db 会话,过滤掉熔断的供应商
# 如果有 db 会话,过滤掉后台管理台中已熔断的供应商
# .env/default provider 没有 providers 表记录,不能写入带外键的健康表。
if db:
healthy_providers = []
for item in sorted_providers:
name, config, db_provider = item
provider_id = db_provider.id if db_provider else name
if db_provider is None:
healthy_providers.append(item)
continue
provider_id = db_provider.id
if await health_checker.is_healthy(db, provider_id):
healthy_providers.append(item)
else:
@@ -295,7 +310,7 @@ async def _route_with_failover(
errors.append(f"{name}: 适配器未注册")
continue
provider_id = db_provider.id if db_provider else name
provider_id = db_provider.id if db_provider else None
try:
logger.debug(
@@ -315,8 +330,8 @@ async def _route_with_failover(
# 更新延迟缓存
_latency_cache[name] = latency_ms
# 记录成功指标
if db:
# 记录成功指标。Provider 指标/健康表带外键,只记录后台管理台里的真实 provider。
if db and db_provider and provider_id:
await metrics_collector.record_call(
db,
provider_id=provider_id,
@@ -326,16 +341,16 @@ async def _route_with_failover(
)
await health_checker.record_call_result(db, provider_id, success=True)
# 记录用户成本
if user_id:
await cost_tracker.record_cost(
db,
user_id=user_id,
provider_name=name,
capability=provider_type,
estimated_cost=adapter.estimated_cost,
provider_id=provider_id if db_provider else None,
)
# 记录用户成本;环境变量/default provider 没有 provider_id保留 provider_name 即可。
if db and user_id:
await cost_tracker.record_cost(
db,
user_id=user_id,
provider_name=name,
capability=provider_type,
estimated_cost=adapter.estimated_cost,
provider_id=provider_id,
)
logger.info(
"provider_success",
@@ -355,8 +370,8 @@ async def _route_with_failover(
)
errors.append(f"{name}: {exc}")
# 记录失败指标
if db:
# 记录失败指标。Provider 指标/健康表带外键,只记录后台管理台里的真实 provider。
if db and db_provider and provider_id:
await metrics_collector.record_call(
db,
provider_id=provider_id,