"""伏笔管理API路由""" from fastapi import APIRouter, Depends, HTTPException, Request, Query from sqlalchemy.ext.asyncio import AsyncSession from typing import Optional, List from app.database import get_db from app.api.common import verify_project_access from app.services.foreshadow_service import foreshadow_service from app.schemas.foreshadow import ( ForeshadowCreate, ForeshadowUpdate, ForeshadowResponse, ForeshadowListResponse, ForeshadowStatsResponse, PlantForeshadowRequest, ResolveForeshadowRequest, SyncFromAnalysisRequest, SyncFromAnalysisResponse, ForeshadowContextResponse ) from app.logger import get_logger logger = get_logger(__name__) router = APIRouter(prefix="/api/foreshadows", tags=["foreshadows"]) @router.get("/projects/{project_id}", response_model=ForeshadowListResponse) async def get_project_foreshadows( project_id: str, request: Request, status: Optional[str] = Query(None, description="状态筛选: pending/planted/resolved/abandoned"), category: Optional[str] = Query(None, description="分类筛选"), source_type: Optional[str] = Query(None, description="来源筛选: analysis/manual"), is_long_term: Optional[bool] = Query(None, description="是否长线伏笔"), page: int = Query(1, ge=1, description="页码"), limit: int = Query(50, ge=1, le=100, description="每页数量"), db: AsyncSession = Depends(get_db) ): """ 获取项目所有伏笔 支持按状态、分类、来源筛选,支持分页 """ try: user_id = getattr(request.state, 'user_id', None) await verify_project_access(project_id, user_id, db) result = await foreshadow_service.get_project_foreshadows( db=db, project_id=project_id, status=status, category=category, source_type=source_type, is_long_term=is_long_term, page=page, limit=limit ) return result except HTTPException: raise except Exception as e: logger.error(f"❌ 获取伏笔列表失败: {str(e)}") raise HTTPException(status_code=500, detail=f"获取伏笔列表失败: {str(e)}") @router.get("/projects/{project_id}/stats", response_model=ForeshadowStatsResponse) async def get_foreshadow_stats( project_id: str, request: Request, current_chapter: Optional[int] = Query(None, ge=1, description="当前章节号(用于计算超期)"), db: AsyncSession = Depends(get_db) ): """获取项目伏笔统计""" try: user_id = getattr(request.state, 'user_id', None) await verify_project_access(project_id, user_id, db) stats = await foreshadow_service.get_stats(db, project_id, current_chapter) return stats except HTTPException: raise except Exception as e: logger.error(f"❌ 获取伏笔统计失败: {str(e)}") raise HTTPException(status_code=500, detail=f"获取伏笔统计失败: {str(e)}") @router.get("/projects/{project_id}/context/{chapter_number}", response_model=ForeshadowContextResponse) async def get_chapter_foreshadow_context( project_id: str, chapter_number: int, request: Request, include_pending: bool = Query(True, description="包含待埋入伏笔"), include_overdue: bool = Query(True, description="包含超期伏笔"), lookahead: int = Query(5, ge=1, le=20, description="向前看几章"), db: AsyncSession = Depends(get_db) ): """ 获取章节生成的伏笔上下文 用于在章节生成时提供伏笔提醒 """ try: user_id = getattr(request.state, 'user_id', None) await verify_project_access(project_id, user_id, db) context = await foreshadow_service.build_chapter_context( db=db, project_id=project_id, chapter_number=chapter_number, include_pending=include_pending, include_overdue=include_overdue, lookahead=lookahead ) return context except HTTPException: raise except Exception as e: logger.error(f"❌ 获取伏笔上下文失败: {str(e)}") raise HTTPException(status_code=500, detail=f"获取伏笔上下文失败: {str(e)}") @router.get("/projects/{project_id}/pending-resolve") async def get_pending_resolve_foreshadows( project_id: str, request: Request, current_chapter: int = Query(..., ge=1, description="当前章节号"), lookahead: int = Query(5, ge=1, le=20, description="向前看几章"), db: AsyncSession = Depends(get_db) ): """获取待回收伏笔列表(用于章节生成提醒)""" try: user_id = getattr(request.state, 'user_id', None) await verify_project_access(project_id, user_id, db) foreshadows = await foreshadow_service.get_pending_resolve_foreshadows( db=db, project_id=project_id, current_chapter=current_chapter, lookahead=lookahead ) return { "total": len(foreshadows), "items": [f.to_dict() for f in foreshadows] } except HTTPException: raise except Exception as e: logger.error(f"❌ 获取待回收伏笔失败: {str(e)}") raise HTTPException(status_code=500, detail=f"获取待回收伏笔失败: {str(e)}") @router.get("/{foreshadow_id}", response_model=ForeshadowResponse) async def get_foreshadow( foreshadow_id: str, request: Request, db: AsyncSession = Depends(get_db) ): """获取单个伏笔详情""" try: foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id) if not foreshadow: raise HTTPException(status_code=404, detail="伏笔不存在") # 验证权限 user_id = getattr(request.state, 'user_id', None) await verify_project_access(foreshadow.project_id, user_id, db) return foreshadow.to_dict() except HTTPException: raise except Exception as e: logger.error(f"❌ 获取伏笔详情失败: {str(e)}") raise HTTPException(status_code=500, detail=f"获取伏笔详情失败: {str(e)}") @router.post("", response_model=ForeshadowResponse) async def create_foreshadow( data: ForeshadowCreate, request: Request, db: AsyncSession = Depends(get_db) ): """ 创建伏笔(手动添加) 创建一个新的自定义伏笔 """ try: user_id = getattr(request.state, 'user_id', None) await verify_project_access(data.project_id, user_id, db) foreshadow = await foreshadow_service.create_foreshadow(db, data) return foreshadow.to_dict() except HTTPException: raise except Exception as e: logger.error(f"❌ 创建伏笔失败: {str(e)}") raise HTTPException(status_code=500, detail=f"创建伏笔失败: {str(e)}") @router.put("/{foreshadow_id}", response_model=ForeshadowResponse) async def update_foreshadow( foreshadow_id: str, data: ForeshadowUpdate, request: Request, db: AsyncSession = Depends(get_db) ): """更新伏笔""" try: foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id) if not foreshadow: raise HTTPException(status_code=404, detail="伏笔不存在") user_id = getattr(request.state, 'user_id', None) await verify_project_access(foreshadow.project_id, user_id, db) updated = await foreshadow_service.update_foreshadow(db, foreshadow_id, data) return updated.to_dict() except HTTPException: raise except Exception as e: logger.error(f"❌ 更新伏笔失败: {str(e)}") raise HTTPException(status_code=500, detail=f"更新伏笔失败: {str(e)}") @router.delete("/{foreshadow_id}") async def delete_foreshadow( foreshadow_id: str, request: Request, db: AsyncSession = Depends(get_db) ): """删除伏笔""" try: foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id) if not foreshadow: raise HTTPException(status_code=404, detail="伏笔不存在") user_id = getattr(request.state, 'user_id', None) await verify_project_access(foreshadow.project_id, user_id, db) await foreshadow_service.delete_foreshadow(db, foreshadow_id) return {"message": "伏笔删除成功", "id": foreshadow_id} except HTTPException: raise except Exception as e: logger.error(f"❌ 删除伏笔失败: {str(e)}") raise HTTPException(status_code=500, detail=f"删除伏笔失败: {str(e)}") @router.post("/{foreshadow_id}/plant", response_model=ForeshadowResponse) async def plant_foreshadow( foreshadow_id: str, data: PlantForeshadowRequest, request: Request, db: AsyncSession = Depends(get_db) ): """ 标记伏笔为已埋入 将伏笔状态从pending改为planted,记录埋入章节 """ try: foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id) if not foreshadow: raise HTTPException(status_code=404, detail="伏笔不存在") user_id = getattr(request.state, 'user_id', None) await verify_project_access(foreshadow.project_id, user_id, db) updated = await foreshadow_service.mark_as_planted(db, foreshadow_id, data) return updated.to_dict() except HTTPException: raise except Exception as e: logger.error(f"❌ 标记伏笔埋入失败: {str(e)}") raise HTTPException(status_code=500, detail=f"标记伏笔埋入失败: {str(e)}") @router.post("/{foreshadow_id}/resolve", response_model=ForeshadowResponse) async def resolve_foreshadow( foreshadow_id: str, data: ResolveForeshadowRequest, request: Request, db: AsyncSession = Depends(get_db) ): """ 标记伏笔为已回收 将伏笔状态改为resolved或partially_resolved """ try: foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id) if not foreshadow: raise HTTPException(status_code=404, detail="伏笔不存在") user_id = getattr(request.state, 'user_id', None) await verify_project_access(foreshadow.project_id, user_id, db) updated = await foreshadow_service.mark_as_resolved(db, foreshadow_id, data) return updated.to_dict() except HTTPException: raise except Exception as e: logger.error(f"❌ 标记伏笔回收失败: {str(e)}") raise HTTPException(status_code=500, detail=f"标记伏笔回收失败: {str(e)}") @router.post("/{foreshadow_id}/abandon", response_model=ForeshadowResponse) async def abandon_foreshadow( foreshadow_id: str, request: Request, reason: Optional[str] = Query(None, description="废弃原因"), db: AsyncSession = Depends(get_db) ): """ 标记伏笔为已废弃 决定不再使用此伏笔 """ try: foreshadow = await foreshadow_service.get_foreshadow(db, foreshadow_id) if not foreshadow: raise HTTPException(status_code=404, detail="伏笔不存在") user_id = getattr(request.state, 'user_id', None) await verify_project_access(foreshadow.project_id, user_id, db) updated = await foreshadow_service.mark_as_abandoned(db, foreshadow_id, reason) return updated.to_dict() except HTTPException: raise except Exception as e: logger.error(f"❌ 标记伏笔废弃失败: {str(e)}") raise HTTPException(status_code=500, detail=f"标记伏笔废弃失败: {str(e)}") @router.post("/projects/{project_id}/sync-from-analysis", response_model=SyncFromAnalysisResponse) async def sync_foreshadows_from_analysis( project_id: str, data: SyncFromAnalysisRequest, request: Request, db: AsyncSession = Depends(get_db) ): """ 从分析结果同步伏笔 从章节分析结果中提取伏笔信息,同步到伏笔管理表 """ try: user_id = getattr(request.state, 'user_id', None) await verify_project_access(project_id, user_id, db) result = await foreshadow_service.sync_from_analysis(db, project_id, data) return result except HTTPException: raise except Exception as e: logger.error(f"❌ 同步伏笔失败: {str(e)}") raise HTTPException(status_code=500, detail=f"同步伏笔失败: {str(e)}")