""" AI Provider 版本追蹤器 — 每小時探測 5 Provider 並寫入 DB,偵測版本變更 職責: - 排程呼叫 probe_all_providers() - 與 DB 最後一筆比對,判斷 changed 旗標 - 寫入 AIProviderVersionHistory - 若有 changed → 記錄 warning log(P3.2.3 alerter 後續整合) # 2026-04-27 P3.2.2 by Claude """ from __future__ import annotations import asyncio import structlog logger = structlog.get_logger(__name__) class ModelVersionTracker: """每小時探測所有 AI Provider 版本並寫入 DB""" async def run_probe_cycle(self) -> dict: """執行一輪探測:probe → 比對上一筆 → 寫入 DB Returns: dict with keys: - probed : int — 成功探測的 provider 數 - changed : list[str] — 版本有變更的 provider names """ from src.db.base import get_db_context from src.db.models import AIProviderVersionHistory from src.services.model_version_probe import probe_all_providers from sqlalchemy import desc, select results = await probe_all_providers() changed_providers: list[str] = [] async with get_db_context() as db: for info in results: # 取最近一筆比對 stmt = ( select(AIProviderVersionHistory) .where(AIProviderVersionHistory.provider == info.provider) .order_by(desc(AIProviderVersionHistory.captured_at)) .limit(1) ) last = (await db.execute(stmt)).scalar_one_or_none() changed = ( last is None or last.version != info.version or last.digest != info.digest ) if changed: changed_providers.append(info.provider) db.add( AIProviderVersionHistory( provider=info.provider, model=info.model, version=info.version, digest=info.digest, captured_at=info.captured_at, prev_version=last.version if last else None, changed=changed, ) ) await db.commit() if changed_providers: logger.warning( "provider_version_changed", changed=changed_providers, total_probed=len(results), ) # P3.2.3: Telegram 告警(dedup 1h/provider) try: from src.services.failover_alerter import get_failover_alerter await get_failover_alerter().alert_provider_version_changed( changed_providers=changed_providers, probed=len(results), ) except Exception as _alert_err: logger.warning("provider_version_alert_failed", error=str(_alert_err)) else: logger.info( "provider_version_stable", total_probed=len(results), ) return {"probed": len(results), "changed": changed_providers} # ============================================================================= # Singleton # ============================================================================= _tracker: ModelVersionTracker | None = None def get_model_version_tracker() -> ModelVersionTracker: """取得 ModelVersionTracker singleton""" global _tracker if _tracker is None: _tracker = ModelVersionTracker() return _tracker