""" Confirm Cards API Routes 确认卡片API路由 - 提供确认卡片的CRUD操作 """ from fastapi import APIRouter, Depends, HTTPException, Query from typing import List, Optional from datetime import datetime, timedelta from app.models.confirm_card import ( ConfirmCard, ConfirmCardStatus, ConfirmCardOption ) from app.db.confirm_card_repository import get_confirm_card_repo from app.api.v1.websocket import manager from app.utils.logger import get_logger logger = get_logger(__name__) router = APIRouter(prefix="/confirm-cards", tags=["确认卡片"]) # ============================================ # 依赖注入 # ============================================ async def get_repo(): """获取确认卡片仓储""" return get_confirm_card_repo() # ============================================ # CRUD 端点 # ============================================ @router.post("") async def create_confirm_card( card: ConfirmCard, repo=Depends(get_repo) ): """ 创建确认卡片 由Agent推送,用户可中途退出后随时确认 """ try: created_card = await repo.create(card) # WebSocket推送 await manager.send_to_project(card.project_id, { "type": "confirm_card_created", "data": { "card_id": card.id, "card_type": card.card_type.value, "title": card.title, "description": card.description[:200] + "..." if len(card.description) > 200 else card.description, "options_count": len(card.options), "created_at": card.created_at.isoformat() } }) logger.info(f"创建确认卡片成功: {card.id}") return {"success": True, "card": created_card} except Exception as e: logger.error(f"创建确认卡片失败: {str(e)}") raise HTTPException(status_code=500, detail=f"创建确认卡片失败: {str(e)}") @router.post("/simple") async def create_confirm_card_simple( project_id: str, card_type: str, title: str, description: str, options: Optional[List[dict]] = None, episode_number: Optional[int] = None, allow_custom_response: bool = False, expires_in_hours: Optional[int] = None, repo=Depends(get_repo) ): """ 简化创建确认卡片 用于Agent快速推送确认任务 """ try: # 构建选项列表 card_options = [] if options: for i, opt in enumerate(options): if isinstance(opt, dict): card_options.append(ConfirmCardOption( id=opt.get("id", f"opt_{i}"), label=opt.get("label", f"选项 {i+1}"), description=opt.get("description", ""), implications=opt.get("implications") )) elif isinstance(opt, str): card_options.append(ConfirmCardOption( id=f"opt_{i}", label=opt, description="" )) # 计算过期时间 expires_at = None if expires_in_hours: expires_at = datetime.now() + timedelta(hours=expires_in_hours) from app.models.confirm_card import ConfirmCardType # 创建卡片 card = ConfirmCard( id=None, # 由repository生成 project_id=project_id, episode_number=episode_number, card_type=ConfirmCardType(card_type), title=title, description=description, options=card_options, allow_custom_response=allow_custom_response, expires_at=expires_at ) created_card = await repo.create(card) # WebSocket推送 await manager.send_to_project(project_id, { "type": "confirm_card_created", "data": { "card_id": created_card.id, "card_type": card.card_type.value, "title": card.title, "description": card.description[:200] + "..." if len(card.description) > 200 else card.description, "options": [{"id": o.id, "label": o.label} for o in card_options], "created_at": created_card.created_at.isoformat() } }) logger.info(f"简化创建确认卡片成功: {created_card.id}") return {"success": True, "card_id": created_card.id, "card": created_card} except Exception as e: logger.error(f"简化创建确认卡片失败: {str(e)}") raise HTTPException(status_code=500, detail=f"创建确认卡片失败: {str(e)}") @router.get("") async def list_confirm_cards( project_id: str, status: Optional[ConfirmCardStatus] = None, include_expired: bool = False, repo=Depends(get_repo) ) -> List[ConfirmCard]: """ 获取确认卡片列表 默认不包含过期的卡片 """ try: return await repo.list_by_project(project_id, status, include_expired) except Exception as e: logger.error(f"获取确认卡片列表失败: {str(e)}") raise HTTPException(status_code=500, detail=f"获取列表失败: {str(e)}") @router.get("/{card_id}") async def get_confirm_card( card_id: str, repo=Depends(get_repo) ) -> ConfirmCard: """获取确认卡片详情""" card = await repo.get(card_id) if not card: raise HTTPException(status_code=404, detail="确认卡片不存在") # 检查是否过期 if card.expires_at and card.expires_at < datetime.now(): if card.status == ConfirmCardStatus.PENDING: card.status = ConfirmCardStatus.EXPIRED return card @router.delete("/{card_id}") async def delete_confirm_card( card_id: str, repo=Depends(get_repo) ): """删除确认卡片""" try: success = await repo.delete(card_id) if not success: raise HTTPException(status_code=404, detail="确认卡片不存在") logger.info(f"删除确认卡片成功: {card_id}") return {"success": True} except HTTPException: raise except Exception as e: logger.error(f"删除确认卡片失败: {str(e)}") raise HTTPException(status_code=500, detail=f"删除失败: {str(e)}") # ============================================ # 操作端点 # ============================================ @router.post("/{card_id}/confirm") async def confirm_card( card_id: str, selected_option_id: Optional[str] = None, custom_response: Optional[str] = None, user_notes: Optional[str] = None, repo=Depends(get_repo) ): """ 确认卡片 用户选择一个选项或提供自定义回复 """ try: card = await repo.get(card_id) if not card: raise HTTPException(status_code=404, detail="确认卡片不存在") # 检查是否过期 if card.expires_at and card.expires_at < datetime.now(): await repo.update_status(card_id, ConfirmCardStatus.EXPIRED) raise HTTPException(status_code=400, detail="确认卡片已过期") # 更新状态 updated_card = await repo.update_status( card_id, ConfirmCardStatus.APPROVED, selected_option_id, custom_response, user_notes ) # WebSocket通知 await manager.send_to_project(card.project_id, { "type": "confirm_card_confirmed", "data": { "card_id": card_id, "status": "approved", "selected_option": selected_option_id, "custom_response": custom_response, "confirmed_at": updated_card.confirmed_at.isoformat() if updated_card.confirmed_at else None } }) logger.info(f"确认卡片成功: {card_id}, 选择: {selected_option_id}") return {"success": True, "card": updated_card} except HTTPException: raise except Exception as e: logger.error(f"确认卡片失败: {str(e)}") raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}") @router.post("/{card_id}/reject") async def reject_card( card_id: str, user_notes: Optional[str] = None, repo=Depends(get_repo) ): """ 拒绝确认卡片 用户拒绝该确认任务 """ try: updated_card = await repo.update_status( card_id, ConfirmCardStatus.REJECTED, user_notes=user_notes ) if not updated_card: raise HTTPException(status_code=404, detail="确认卡片不存在") # WebSocket通知 await manager.send_to_project(updated_card.project_id, { "type": "confirm_card_rejected", "data": { "card_id": card_id, "status": "rejected", "user_notes": user_notes } }) logger.info(f"拒绝确认卡片成功: {card_id}") return {"success": True, "card": updated_card} except HTTPException: raise except Exception as e: logger.error(f"拒绝确认卡片失败: {str(e)}") raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}") @router.post("/cleanup-expired") async def cleanup_expired_cards(repo=Depends(get_repo)): """ 清理过期的确认卡片 将过期且仍为pending状态的卡片标记为expired """ try: count = await repo.cleanup_expired() logger.info(f"清理过期确认卡片: {count}个") return {"success": True, "cleaned_count": count} except Exception as e: logger.error(f"清理过期卡片失败: {str(e)}") raise HTTPException(status_code=500, detail=f"清理失败: {str(e)}")