c0b67fd879
补齐「启动确认窗自进化闭环」里 app-server 这一侧的读路径:pricebot 的 launch_confirm LLM 兜底成功样本会 server→server 落到 app-server 的 launch_confirm_sample 表;本提交 新增「样本列表查询」内部接口, 供 pricebot 的 scripts/distill_launch_confirm.py 回读这些 样本、按「宿主包 × 文案变体」聚合沉淀出候选规则。 - app/api/internal/launch_confirm.py: 新增样本列表内部接口(server→server, 不走用户 JWT) - app/repositories/launch_confirm_sample.py: 样本列表查询 - app/schemas/launch_confirm_sample.py: 列表响应 schema Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Reviewed-on: #91 Co-authored-by: wuqi <wuqi@wonderable.ai> Co-committed-by: wuqi <wuqi@wonderable.ai>
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""启动确认窗兜底样本内部上报端点(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]
|