Files
loom/backend/app/services/lobster_config_service.py
T
2026-04-24 17:46:03 +08:00

153 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""龙虾运行时配置:读取(DB 优先 → .env 兜底)+ 更新。"""
from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.crypto import decrypt, encrypt, mask
from app.models import LobsterConfig
from app.schemas.lobster_config import LobsterConfigRead, LobsterConfigUpdate
@dataclass
class EffectiveLobsterConfig:
"""运行时真正生效的值(已合并 DB + .env)。"""
api_base: str
api_key: str
callback_secret: str
timeout_seconds: int
poll_interval_seconds: int
# 各字段的来源:db / env / unset,仅用于展示
api_base_source: str = "env"
api_key_source: str = "env"
callback_secret_source: str = "env"
timeout_source: str = "env"
poll_interval_source: str = "env"
@property
def is_mock(self) -> bool:
return not self.api_base or not self.api_key
async def _get_or_create_row(db: AsyncSession) -> LobsterConfig:
row = (await db.execute(select(LobsterConfig).where(LobsterConfig.id == 1))).scalar_one_or_none()
if row is None:
row = LobsterConfig(id=1)
db.add(row)
await db.flush()
return row
async def load_effective(db: AsyncSession) -> EffectiveLobsterConfig:
row = (await db.execute(select(LobsterConfig).where(LobsterConfig.id == 1))).scalar_one_or_none()
# DB 字段(可能 None)
db_api_base = (row.api_base if row else None) or None
db_api_key = decrypt(row.api_key_cipher) if row and row.api_key_cipher else None
db_callback_secret = (
decrypt(row.callback_secret_cipher) if row and row.callback_secret_cipher else None
)
db_timeout = row.timeout_seconds if row else None
db_poll = row.poll_interval_seconds if row else None
# env 兜底
env_api_base = settings.LOBSTER_API_BASE or ""
env_api_key = settings.LOBSTER_API_KEY or ""
env_callback_secret = settings.LOBSTER_CALLBACK_SECRET or ""
env_timeout = settings.LOBSTER_TIMEOUT_SECONDS or 30
env_poll = settings.LOBSTER_POLL_INTERVAL_SECONDS or 5
api_base = db_api_base or env_api_base
api_base_source = "db" if db_api_base else ("env" if env_api_base else "unset")
api_key = db_api_key or env_api_key
api_key_source = "db" if db_api_key else ("env" if env_api_key else "unset")
callback = db_callback_secret or env_callback_secret
callback_source = (
"db" if db_callback_secret else ("env" if env_callback_secret else "unset")
)
timeout = db_timeout or env_timeout
timeout_source = "db" if db_timeout else "env"
poll = db_poll or env_poll
poll_source = "db" if db_poll else "env"
return EffectiveLobsterConfig(
api_base=api_base,
api_key=api_key,
callback_secret=callback,
timeout_seconds=timeout,
poll_interval_seconds=poll,
api_base_source=api_base_source,
api_key_source=api_key_source,
callback_secret_source=callback_source,
timeout_source=timeout_source,
poll_interval_source=poll_source,
)
async def read(db: AsyncSession) -> LobsterConfigRead:
eff = await load_effective(db)
note = None
row = (await db.execute(select(LobsterConfig).where(LobsterConfig.id == 1))).scalar_one_or_none()
if row:
note = row.note
return LobsterConfigRead(
api_base=eff.api_base or "",
api_base_source=eff.api_base_source,
api_key_mask=mask(eff.api_key) if eff.api_key else "",
api_key_source=eff.api_key_source,
has_api_key=bool(eff.api_key),
callback_secret_mask=mask(eff.callback_secret) if eff.callback_secret else "",
callback_secret_source=eff.callback_secret_source,
has_callback_secret=bool(eff.callback_secret),
timeout_seconds=eff.timeout_seconds,
timeout_source=eff.timeout_source,
poll_interval_seconds=eff.poll_interval_seconds,
poll_interval_source=eff.poll_interval_source,
mode="real" if not eff.is_mock else "mock",
note=note,
)
async def update(db: AsyncSession, data: LobsterConfigUpdate) -> LobsterConfigRead:
row = await _get_or_create_row(db)
changes = data.model_dump(exclude_unset=True)
# api_key / callback_secret 空串表示清空 → fallback env
if "api_key" in changes:
v = changes.pop("api_key")
if v is None:
pass # 不改
elif v == "":
row.api_key_cipher = None
else:
row.api_key_cipher = encrypt(v)
if "callback_secret" in changes:
v = changes.pop("callback_secret")
if v is None:
pass
elif v == "":
row.callback_secret_cipher = None
else:
row.callback_secret_cipher = encrypt(v)
# api_base 空串 = 清空回落 env
if "api_base" in changes:
v = changes.pop("api_base")
row.api_base = v if v else None
for k, v in changes.items():
setattr(row, k, v)
await db.commit()
return await read(db)