diff --git a/app/api/internal/launch_confirm.py b/app/api/internal/launch_confirm.py index 5f24d3f..d4dca53 100644 --- a/app/api/internal/launch_confirm.py +++ b/app/api/internal/launch_confirm.py @@ -11,6 +11,7 @@ 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 @@ -21,6 +22,7 @@ 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") @@ -61,3 +63,34 @@ def report_launch_confirm_sample( 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] diff --git a/app/repositories/launch_confirm_sample.py b/app/repositories/launch_confirm_sample.py index 4d0b226..5e6ecbd 100644 --- a/app/repositories/launch_confirm_sample.py +++ b/app/repositories/launch_confirm_sample.py @@ -2,8 +2,10 @@ from __future__ import annotations import logging +from datetime import datetime from typing import Optional +from sqlalchemy import select from sqlalchemy.orm import Session from app.models.launch_confirm_sample import LaunchConfirmSample @@ -33,3 +35,27 @@ def insert_sample(db: Session, payload: LaunchConfirmSampleIn) -> int: db.commit() db.refresh(row) return row.id + + +def list_samples( + db: Session, + *, + exec_success: Optional[bool] = None, + host_package: Optional[str] = None, + since: Optional[datetime] = None, + limit: int = 1000, +) -> list[LaunchConfirmSample]: + """按条件查样本(pricebot 的 distill_launch_confirm.py 沉淀工具读)。created_at 升序。 + + 过滤项都可空:exec_success(只看放行成功的)/ host_package(只看某宿主包)/ + since(>= created_at)。limit 兜底防一次拉爆全表。 + """ + stmt = select(LaunchConfirmSample) + if exec_success is not None: + stmt = stmt.where(LaunchConfirmSample.exec_success == exec_success) + if host_package: + stmt = stmt.where(LaunchConfirmSample.host_package == host_package) + if since is not None: + stmt = stmt.where(LaunchConfirmSample.created_at >= since) + stmt = stmt.order_by(LaunchConfirmSample.created_at.asc()).limit(limit) + return list(db.execute(stmt).scalars().all()) diff --git a/app/schemas/launch_confirm_sample.py b/app/schemas/launch_confirm_sample.py index e2ed223..7c44636 100644 --- a/app/schemas/launch_confirm_sample.py +++ b/app/schemas/launch_confirm_sample.py @@ -1,7 +1,9 @@ """启动确认窗兜底样本的内部上报模型(pricebot → app-server)。""" from __future__ import annotations -from pydantic import BaseModel +from datetime import datetime + +from pydantic import BaseModel, ConfigDict class LaunchConfirmSampleIn(BaseModel): @@ -24,3 +26,20 @@ class LaunchConfirmSampleOut(BaseModel): """落库结果。""" id: int + + +class LaunchConfirmSampleRow(BaseModel): + """单条样本(列表读出;沉淀脚本 distill 用,payload 原样带出)。""" + + model_config = ConfigDict(from_attributes=True) + + id: int + created_at: datetime + trace_id: str | None = None + device_id: str | None = None + host_package: str | None = None + target_app: str | None = None + system_locale: str | None = None + exec_success: bool = False + dialog_title: str | None = None + payload: dict | None = None