"""食谱系统信号源 API。""" from __future__ import annotations import json from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from ...core.logging import get_logger from ...core.database import get_db from ...dependencies.tenant import get_tenant_id from ...models import ProjectSignalLink, Signal from .schemas import RecipeBatchRequest, RecipeRecognitionRequest, RecipeRecognitionResponse from .helpers import resolve_target_project_id router = APIRouter(prefix="/signal-sources", tags=["Signal Sources"]) logger = get_logger(__name__) @router.post("/recipe/webhook", response_model=RecipeRecognitionResponse, summary="食谱系统 Webhook") async def recipe_webhook( request: RecipeRecognitionRequest, project_id: Optional[int] = Query(None, description="项目ID,不传则自动查找食谱系统项目"), db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id), ): """接收食谱系统的识别请求。""" try: target_project_id = await resolve_target_project_id( db=db, tenant_id=tenant_id, project_id=project_id, ) content_parts = [f"食物:{request.food_name}"] if request.food_description: content_parts.append(f"描述:{request.food_description}") if request.calories or request.protein or request.fat or request.carbs or request.fiber: nutrition = [] if request.calories: nutrition.append(f"热量 {request.calories} kcal") if request.protein: nutrition.append(f"蛋白 {request.protein}g") if request.fat: nutrition.append(f"脂肪 {request.fat}g") if request.carbs: nutrition.append(f"碳水 {request.carbs}g") if request.fiber: nutrition.append(f"纤维 {request.fiber}g") content_parts.append("营养:" + ", ".join(nutrition)) if request.category: content_parts.append(f"分类:{request.category}") if request.cuisine: content_parts.append(f"菜系:{request.cuisine}") content_parts.append(f"意图:{request.intent}") content = "\n".join(content_parts) intent_text = { "analyze": "热量分析", "save": "保存食谱", "recommend": "推荐菜谱", }.get(request.intent, request.intent) title = f"🍽️ 食谱{intent_text}:{request.food_name}" summary = f"食物:{request.food_name}" if request.calories: summary += f"\n热量:{request.calories} kcal" if request.category: summary += f"\n分类:{request.category}" summary += f"\n意图:{intent_text}" signal_metadata = { "source": "recipe_system", "intent": request.intent, "category": request.category, "cuisine": request.cuisine, "nutrition": { "calories": request.calories, "protein": request.protein, "fat": request.fat, "carbs": request.carbs, "fiber": request.fiber, }, "image_url": request.image_url, "user_name": request.user_name, } signal = Signal( tenant_id=tenant_id, source="recipe_system", title=title, content=content, summary=summary, signal_type="recipe_recognition", priority="medium", impact_score=0.5, status="待处理", processed=False, strategic_analysis=json.dumps(signal_metadata, ensure_ascii=False), ) db.add(signal) await db.flush() link = ProjectSignalLink( tenant_id=tenant_id, project_id=target_project_id, signal_id=signal.id, ) db.add(link) await db.commit() await db.refresh(signal) return RecipeRecognitionResponse( success=True, signal_id=signal.id, project_id=target_project_id, message="信号创建成功并已关联到食谱系统项目", details={ "signal_id": signal.id, "project_id": target_project_id, "title": title, "status": signal.status, }, ) except HTTPException: await db.rollback() raise except Exception as exc: await db.rollback() logger.exception( "recipe_webhook_create_signal_failed", tenant_id=tenant_id, project_id=project_id, food_name=request.food_name, error=str(exc), ) raise HTTPException(status_code=500, detail=f"创建信号失败: {str(exc)}") @router.get("/recipe/status/{signal_id}", summary="查询食谱信号状态") async def get_recipe_signal_status( signal_id: int, db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id), ): """查询食谱识别信号的处理状态""" result = await db.execute( select(Signal).where( Signal.id == signal_id, Signal.tenant_id == tenant_id, Signal.source == "recipe_system", ) ) signal = result.scalar_one_or_none() if not signal: raise HTTPException(status_code=404, detail="信号不存在") return { "signal_id": signal.id, "title": signal.title, "status": signal.status, "processed": signal.processed, "summary": signal.summary, } @router.post("/recipe/batch", response_model=RecipeRecognitionResponse, summary="批量导入食谱") async def recipe_batch_import( request: RecipeBatchRequest, project_id: Optional[int] = Query(None, description="项目ID"), db: AsyncSession = Depends(get_db), tenant_id: int = Depends(get_tenant_id), ): """批量导入食谱记录,每个都会创建信号""" try: target_project_id = await resolve_target_project_id( db=db, tenant_id=tenant_id, project_id=project_id, ) signal_ids = [] for recipe in request.recipes: content = f"食物:{recipe.food_name}" if recipe.calories: content += f"\n热量:{recipe.calories} kcal" title = f"🍽️ 食谱记录:{recipe.food_name}" metadata = { "source": "recipe_system", "intent": recipe.intent, "nutrition": { "calories": recipe.calories, "protein": recipe.protein, "fat": recipe.fat, "carbs": recipe.carbs, }, } signal = Signal( tenant_id=tenant_id, source="recipe_system", title=title, content=content, summary=f"食物:{recipe.food_name}", signal_type="recipe_recognition", priority="low", impact_score=0.3, status="待处理", processed=True, strategic_analysis=json.dumps(metadata), ) db.add(signal) await db.flush() link = ProjectSignalLink( tenant_id=tenant_id, project_id=target_project_id, signal_id=signal.id, ) db.add(link) signal_ids.append(signal.id) await db.commit() return RecipeRecognitionResponse( success=True, signal_id=len(signal_ids), project_id=target_project_id, message=f"成功导入 {len(signal_ids)} 条食谱记录", details={"signal_count": len(signal_ids)}, ) except HTTPException: await db.rollback() raise except Exception as exc: await db.rollback() logger.exception( "recipe_batch_import_failed", tenant_id=tenant_id, project_id=project_id, recipe_count=len(request.recipes), error=str(exc), ) raise HTTPException(status_code=500, detail=f"批量导入失败: {str(exc)}")