"""启动确认窗兜底样本内部上报端点(pricebot → app-server)。 pricebot 的 launch_confirm_agent 每次靠 LLM 兜底放行一个"静态 PROFILES 没认出"的跨 App 启动确认窗时,把完整样本 POST 到这里落库。**不是给客户端的接口**:不走用户 JWT,靠 server 间共享密钥头 `X-Internal-Secret` 校验(== settings.INTERNAL_API_SECRET)。密钥未配置 (默认空)时直接 503,避免裸奔写端点。 研发定期人工把 exec_success=true 的样本沉淀进 pricebot 的 launch_confirm.PROFILES。 """ from __future__ import annotations import hmac import logging from datetime import datetime, timedelta, timezone from typing import Annotated from fastapi import APIRouter, Header, HTTPException, status from app.api.deps import DbSession from app.core.config import settings from app.repositories import launch_confirm_sample as repo from app.schemas.launch_confirm_sample import ( LaunchConfirmSampleIn, LaunchConfirmSampleOut, LaunchConfirmSampleRow, ) logger = logging.getLogger("shagua.internal.launch_confirm") router = APIRouter(prefix="/internal", tags=["internal"]) def _check_secret(x_internal_secret: str | None) -> None: """共享密钥校验。未配置 → 503(挡裸奔写端点);不匹配 → 401(常量时间比较)。""" configured = settings.INTERNAL_API_SECRET if not configured: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="internal api not configured", ) if not x_internal_secret or not hmac.compare_digest(x_internal_secret, configured): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid internal secret", ) @router.post( "/launch-confirm-sample", response_model=LaunchConfirmSampleOut, summary="启动确认窗兜底样本上报(pricebot→app-server,落 launch_confirm_sample)", ) def report_launch_confirm_sample( payload: LaunchConfirmSampleIn, db: DbSession, x_internal_secret: Annotated[str | None, Header()] = None, ) -> LaunchConfirmSampleOut: _check_secret(x_internal_secret) sid = repo.insert_sample(db, payload) logger.info( "launch_confirm_sample id=%d host=%s locale=%s success=%s trace=%s", sid, payload.host_package, payload.system_locale, payload.exec_success, payload.trace_id, ) return LaunchConfirmSampleOut(id=sid) @router.get( "/launch-confirm-samples", response_model=list[LaunchConfirmSampleRow], summary="启动确认窗兜底样本列表(沉淀脚本 distill 读; server→server, 不走 JWT)", ) def list_launch_confirm_samples( db: DbSession, x_internal_secret: Annotated[str | None, Header()] = None, exec_success: bool | None = None, host_package: str | None = None, since_days: int | None = None, limit: int = 1000, ) -> list[LaunchConfirmSampleRow]: """读样本供 pricebot 的 distill_launch_confirm.py 聚合沉淀回 PROFILES。 与上报端点同一把共享密钥;exec_success/host_package/since_days 均可选,limit 默认 1000。 """ _check_secret(x_internal_secret) since = None if since_days and since_days > 0: since = datetime.now(timezone.utc) - timedelta(days=since_days) rows = repo.list_samples( db, exec_success=exec_success, host_package=host_package, since=since, limit=limit, ) return [LaunchConfirmSampleRow.model_validate(r) for r in rows]