# -*- coding: utf-8 -*- """ 内部 AI 触发 API — ETL/内部服务调用入口。 端点: - POST /api/internal/ai/trigger — 接收事件触发请求,异步执行 AI 调用链 认证方式:Authorization: Internal-Token {token} """ from __future__ import annotations import logging from fastapi import APIRouter, Depends, Header, HTTPException, status from pydantic import BaseModel, Field from app.ai.config import AIConfig logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/internal/ai", tags=["internal-ai"]) # ── 请求/响应模型 ──────────────────────────────────────────── class TriggerRequest(BaseModel): """内部触发请求体。""" event_type: str = Field(..., description="事件类型: consumption / dws_completed / note_created / task_assigned") connector_type: str = Field("feiqiu", description="连接器类型") site_id: int = Field(..., description="门店 ID") member_id: int | None = Field(None, description="会员 ID(可选)") payload: dict | None = Field(None, description="附加数据") is_forced: bool = Field(False, description="是否强制执行(跳过去重)") class TriggerResponse(BaseModel): """触发响应。""" trigger_job_id: int status: str = "pending" # ── 认证依赖 ───────────────────────────────────────────────── def verify_internal_token(authorization: str = Header(...)) -> str: """校验 Internal-Token 认证。 Header 格式:Authorization: Internal-Token {token} token 不匹配或缺失时返回 HTTP 401。 """ prefix = "Internal-Token " if not authorization.startswith(prefix): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证格式,需要 Internal-Token", ) token = authorization[len(prefix):] if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token 不能为空", ) # 从环境变量加载期望 token try: config = AIConfig.from_env() except ValueError: logger.error("AIConfig 加载失败,无法校验 internal token") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="AI 配置异常", ) if token != config.internal_api_token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token 不匹配", ) return token # ── 端点 ───────────────────────────────────────────────────── @router.post("/trigger", response_model=TriggerResponse) async def trigger_ai_event( body: TriggerRequest, _token: str = Depends(verify_internal_token), ) -> TriggerResponse: """接收 ETL/内部事件,写 ai_trigger_jobs 后异步执行。 立即返回 trigger_job_id,调用链在后台异步执行。 """ from app.ai.dispatcher import AIDispatcher, TriggerEvent # 构建触发事件 event = TriggerEvent( event_type=body.event_type, site_id=body.site_id, member_id=body.member_id, connector_type=body.connector_type, payload=body.payload or {}, is_forced=body.is_forced, ) # 获取 dispatcher 实例并触发 # 延迟导入避免循环依赖,dispatcher 实例由应用启动时创建 dispatcher = _get_dispatcher() job_id = await dispatcher.handle_trigger(event) return TriggerResponse(trigger_job_id=job_id, status="pending") # ── 辅助函数 ───────────────────────────────────────────────── # 全局 dispatcher 实例(应用启动时初始化) _dispatcher_instance: AIDispatcher | None = None def set_dispatcher(dispatcher: "AIDispatcher") -> None: """设置全局 dispatcher 实例(应用启动时调用)。""" global _dispatcher_instance _dispatcher_instance = dispatcher def _get_dispatcher() -> "AIDispatcher": """获取全局 dispatcher 实例。""" if _dispatcher_instance is None: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="AI Dispatcher 尚未初始化", ) return _dispatcher_instance