2026-01-25 19:27:44 +08:00

670 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)}"
)