""" Review System API Routes 审核系统 API 路由 提供审核配置、执行、维度和自定义规则的管理接口 """ from fastapi import APIRouter, Depends, HTTPException, status from typing import List, Optional import uuid from datetime import datetime from app.models.review import ( ReviewConfig, ReviewConfigUpdate, ReviewResult, ReviewRequest, ReviewResponse, DimensionInfo, DimensionConfig, DimensionType, CustomRule, CustomRuleCreate, CustomRuleUpdate, ReviewPreset ) from app.models.project import SeriesProject, Episode from app.core.review.review_manager import ReviewManager, get_review_manager from app.db.repositories import project_repo, episode_repo from app.utils.logger import get_logger logger = get_logger(__name__) router = APIRouter(prefix="/review", tags=["审核系统"]) # ============================================ # 项目审核配置管理 # ============================================ @router.get("/projects/{project_id}/review-config", response_model=ReviewConfig) async def get_review_config(project_id: str): """ 获取项目的审核配置 Args: project_id: 项目ID Returns: ReviewConfig: 审核配置 """ project = await project_repo.get(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"项目不存在: {project_id}" ) # 从项目配置中获取审核配置 # 如果还没有配置,返回默认配置 review_config = getattr(project, 'reviewConfig', None) if not review_config: review_config = ReviewConfig() return review_config @router.put("/projects/{project_id}/review-config", response_model=ReviewConfig) async def update_review_config( project_id: str, config_update: ReviewConfigUpdate ): """ 更新项目的审核配置 Args: project_id: 项目ID config_update: 配置更新数据 Returns: ReviewConfig: 更新后的配置 """ project = await project_repo.get(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"项目不存在: {project_id}" ) try: # 获取现有配置 current_config = getattr(project, 'reviewConfig', ReviewConfig()) # 更新配置 update_data = config_update.dict(exclude_unset=True) for field, value in update_data.items(): setattr(current_config, field, value) # 保存到项目 await project_repo.update(project_id, { "reviewConfig": current_config.dict() }) logger.info(f"更新项目审核配置: {project_id}") return current_config except Exception as e: logger.error(f"更新审核配置失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"更新配置失败: {str(e)}" ) @router.get("/projects/{project_id}/review-presets", response_model=List[ReviewPreset]) async def get_review_presets(project_id: str = None): """ 获取审核预设列表 Args: project_id: 可选的项目ID,用于返回项目特定的预设 Returns: List[ReviewPreset]: 预设列表 """ # 内置预设 presets = [ ReviewPreset( id="strict", name="严格审核", description="适用于高质量要求,检查所有维度", category="quality", config=ReviewConfig( enabled_review_skills=["consistency_checker"], overall_strictness=0.9, dimension_settings={ DimensionType.consistency: DimensionConfig(enabled=True, strictness=0.9, weight=1.2), DimensionType.quality: DimensionConfig(enabled=True, strictness=0.8, weight=1.0), DimensionType.pacing: DimensionConfig(enabled=True, strictness=0.7, weight=0.8), DimensionType.dialogue: DimensionConfig(enabled=True, strictness=0.8, weight=1.0), DimensionType.character: DimensionConfig(enabled=True, strictness=0.9, weight=1.1), DimensionType.plot: DimensionConfig(enabled=True, strictness=0.9, weight=1.2) }, pass_threshold=85.0 ) ), ReviewPreset( id="balanced", name="平衡审核", description="适用于一般创作需求,平衡质量和效率", category="general", config=ReviewConfig( enabled_review_skills=["consistency_checker"], overall_strictness=0.7, dimension_settings={ DimensionType.consistency: DimensionConfig(enabled=True, strictness=0.7, weight=1.2), DimensionType.quality: DimensionConfig(enabled=True, strictness=0.6, weight=1.0), DimensionType.pacing: DimensionConfig(enabled=False, strictness=0.5, weight=0.8), DimensionType.dialogue: DimensionConfig(enabled=True, strictness=0.6, weight=1.0), DimensionType.character: DimensionConfig(enabled=True, strictness=0.7, weight=1.1), DimensionType.plot: DimensionConfig(enabled=True, strictness=0.7, weight=1.2) }, pass_threshold=75.0 ) ), ReviewPreset( id="quick", name="快速审核", description="只检查核心维度,适用于快速迭代", category="efficiency", config=ReviewConfig( enabled_review_skills=["consistency_checker"], overall_strictness=0.5, dimension_settings={ DimensionType.consistency: DimensionConfig(enabled=True, strictness=0.7, weight=1.5), DimensionType.quality: DimensionConfig(enabled=False, strictness=0.5, weight=1.0), DimensionType.pacing: DimensionConfig(enabled=False, strictness=0.5, weight=0.8), DimensionType.dialogue: DimensionConfig(enabled=False, strictness=0.5, weight=1.0), DimensionType.character: DimensionConfig(enabled=False, strictness=0.5, weight=1.1), DimensionType.plot: DimensionConfig(enabled=True, strictness=0.6, weight=1.5) }, pass_threshold=70.0 ) ), ReviewPreset( id="dialogue_focused", name="对话专项", description="专注于对话质量和人物口吻", category="specialized", config=ReviewConfig( enabled_review_skills=[], overall_strictness=0.8, dimension_settings={ DimensionType.consistency: DimensionConfig(enabled=True, strictness=0.6, weight=0.8), DimensionType.dialogue: DimensionConfig(enabled=True, strictness=0.9, weight=1.5), DimensionType.character: DimensionConfig(enabled=True, strictness=0.8, weight=1.2) }, pass_threshold=75.0 ) ) ] return presets # ============================================ # 审核执行 # ============================================ @router.post("/projects/{project_id}/episodes/{episode_number}/review", response_model=ReviewResponse) async def review_episode( project_id: str, episode_number: int, request: ReviewRequest, review_mgr: ReviewManager = Depends(get_review_manager) ): """ 执行剧集审核 Args: project_id: 项目ID episode_number: 集数 request: 审核请求 review_mgr: 审核管理器(依赖注入) Returns: ReviewResponse: 审核结果 """ # 获取项目 project = await project_repo.get(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"项目不存在: {project_id}" ) # 获取剧集 episodes = await episode_repo.list_by_project(project_id) episode = None for ep in episodes: if ep.number == episode_number: episode = ep break if not episode: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"剧集不存在: EP{episode_number}" ) # 检查剧集是否有内容 if not episode.content: return ReviewResponse( success=False, message=f"剧集 EP{episode_number} 没有内容,无法审核", episode_id=episode.id ) try: # 获取审核配置 config = getattr(project, 'reviewConfig', ReviewConfig()) # 执行审核 logger.info(f"开始审核: 项目 {project_id}, EP{episode_number}") result = await review_mgr.review_episode( project=project, episode=episode, config=config, dimensions=request.dimensions ) # 保存审核结果到剧集 await episode_repo.update(project_id, { "reviewResult": result.dict(), "status": "completed" if result.passed else "needs-review" }) logger.info(f"审核完成: EP{episode_number}, 分数={result.overall_score:.1f}") return ReviewResponse( success=True, review_result=result, message=f"审核完成,总分: {result.overall_score:.1f}", episode_id=episode.id ) except Exception as e: logger.error(f"审核失败: {str(e)}") return ReviewResponse( success=False, message=f"审核失败: {str(e)}", episode_id=episode.id ) @router.get("/projects/{project_id}/episodes/{episode_number}/review-result", response_model=Optional[ReviewResult]) async def get_review_result(project_id: str, episode_number: int): """ 获取剧集的审核结果 Args: project_id: 项目ID episode_number: 集数 Returns: ReviewResult: 审核结果,如果未审核则返回null """ episodes = await episode_repo.list_by_project(project_id) episode = None for ep in episodes: if ep.number == episode_number: episode = ep break if not episode: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"剧集不存在: EP{episode_number}" ) # 返回审核结果(如果有) return getattr(episode, 'reviewResult', None) # ============================================ # 维度管理 # ============================================ @router.get("/dimensions", response_model=List[DimensionInfo]) async def list_dimensions(): """ 列出所有可用的审核维度 Returns: List[DimensionInfo]: 维度信息列表 """ from app.core.review.review_manager import ReviewManager temp_manager = ReviewManager() dimensions = [] for dim_type, dim_info in temp_manager.DIMENSIONS.items(): dimension_info = DimensionInfo( id=dim_type, name=dim_info["name"], description=dim_info["description"], default_strictness=dim_info["default_strictness"], supported_skill_ids=dim_info["skill_ids"] ) dimensions.append(dimension_info) return dimensions @router.get("/dimensions/{dimension_id}", response_model=DimensionInfo) async def get_dimension_info(dimension_id: DimensionType): """ 获取单个维度的详细信息 Args: dimension_id: 维度ID Returns: DimensionInfo: 维度信息 """ from app.core.review.review_manager import ReviewManager temp_manager = ReviewManager() dim_info = temp_manager.DIMENSIONS.get(dimension_id) if not dim_info: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"维度不存在: {dimension_id}" ) return DimensionInfo( id=dimension_id, name=dim_info["name"], description=dim_info["description"], default_strictness=dim_info["default_strictness"], supported_skill_ids=dim_info["skill_ids"] ) # ============================================ # 自定义规则管理 # ============================================ @router.get("/custom-rules", response_model=List[CustomRule]) async def list_custom_rules( project_id: Optional[str] = None, dimension: Optional[DimensionType] = None ): """ 列出自定义规则 Args: project_id: 可选的项目ID,只返回该项目的规则 dimension: 可选的维度筛选 Returns: List[CustomRule]: 自定义规则列表 """ if project_id: # 从项目获取规则 project = await project_repo.get(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"项目不存在: {project_id}" ) config = getattr(project, 'reviewConfig', ReviewConfig()) rules = config.custom_rules else: # 返回空列表(暂时不支持全局规则) rules = [] # 按维度筛选 if dimension: rules = [r for r in rules if r.dimension == dimension] return rules @router.post("/custom-rules", response_model=CustomRule, status_code=status.HTTP_201_CREATED) async def create_custom_rule( rule_data: CustomRuleCreate, project_id: str ): """ 创建自定义规则 Args: rule_data: 规则创建数据 project_id: 项目ID Returns: CustomRule: 创建的规则 """ project = await project_repo.get(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"项目不存在: {project_id}" ) try: # 创建规则 new_rule = CustomRule( id=str(uuid.uuid4()), name=rule_data.name, description=rule_data.description, trigger_condition=rule_data.trigger_condition, dimension=rule_data.dimension, severity=rule_data.severity, enabled=True ) # 添加到项目配置 config = getattr(project, 'reviewConfig', ReviewConfig()) config.custom_rules.append(new_rule) # 保存 await project_repo.update(project_id, { "reviewConfig": config.dict() }) logger.info(f"创建自定义规则: {new_rule.id}") return new_rule except Exception as e: logger.error(f"创建自定义规则失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"创建规则失败: {str(e)}" ) @router.put("/custom-rules/{rule_id}", response_model=CustomRule) async def update_custom_rule( rule_id: str, rule_update: CustomRuleUpdate, project_id: str ): """ 更新自定义规则 Args: rule_id: 规则ID rule_update: 规则更新数据 project_id: 项目ID Returns: CustomRule: 更新后的规则 """ project = await project_repo.get(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"项目不存在: {project_id}" ) config = getattr(project, 'reviewConfig', ReviewConfig()) # 查找规则 rule = None for r in config.custom_rules: if r.id == rule_id: rule = r break if not rule: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"规则不存在: {rule_id}" ) try: # 更新规则 update_data = rule_update.dict(exclude_unset=True) for field, value in update_data.items(): setattr(rule, field, value) rule.updated_at = datetime.now() # 保存 await project_repo.update(project_id, { "reviewConfig": config.dict() }) logger.info(f"更新自定义规则: {rule_id}") return rule except Exception as e: logger.error(f"更新自定义规则失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"更新规则失败: {str(e)}" ) @router.delete("/custom-rules/{rule_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_custom_rule(rule_id: str, project_id: str): """ 删除自定义规则 Args: rule_id: 规则ID project_id: 项目ID Returns: None """ project = await project_repo.get(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"项目不存在: {project_id}" ) config = getattr(project, 'reviewConfig', ReviewConfig()) # 查找并删除规则 original_count = len(config.custom_rules) config.custom_rules = [r for r in config.custom_rules if r.id != rule_id] if len(config.custom_rules) == original_count: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"规则不存在: {rule_id}" ) try: # 保存 await project_repo.update(project_id, { "reviewConfig": config.dict() }) logger.info(f"删除自定义规则: {rule_id}") return None except Exception as e: logger.error(f"删除自定义规则失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"删除规则失败: {str(e)}" ) @router.post("/custom-rules/{rule_id}/test") async def test_custom_rule( rule_id: str, episode_id: str, project_id: str, review_mgr: ReviewManager = Depends(get_review_manager) ): """ 测试自定义规则 Args: rule_id: 规则ID episode_id: 剧集ID project_id: 项目ID review_mgr: 审核管理器 Returns: 测试结果 """ project = await project_repo.get(project_id) if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"项目不存在: {project_id}" ) episode = await episode_repo.get(episode_id) if not episode: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"剧集不存在: {episode_id}" ) config = getattr(project, 'reviewConfig', ReviewConfig()) # 查找规则 rule = None for r in config.custom_rules: if r.id == rule_id: rule = r break if not rule: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"规则不存在: {rule_id}" ) try: # 测试规则 issues = await review_mgr._check_single_rule( episode=episode, rule=rule, config=config ) return { "rule_id": rule_id, "rule_name": rule.name, "episode_id": episode_id, "violations": len(issues), "issues": [issue.dict() for issue in issues] } except Exception as e: logger.error(f"测试规则失败: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"测试失败: {str(e)}" )