hjjjj 5487450f34 feat: 实现审核系统核心功能与UI优化
- 新增审核卡片和确认卡片模型,支持Agent推送审核任务和用户确认
- 实现审核卡片API服务,支持创建、更新、批准、驳回等操作
- 扩展审核维度配置,新增角色一致性、剧情连贯性等维度
- 优化前端审核配置页面,修复API路径错误和状态枚举问题
- 改进剧集创作平台布局,新增左侧边栏用于剧集管理和上下文查看
- 增强Skill管理,支持从审核系统跳转创建/编辑Skill
- 修复episodes.json数据问题,清理聊天历史记录
- 更新Agent提示词,明确Skill引用加载流程
- 统一前端主题配置,优化整体UI体验
2026-01-30 18:32:48 +08:00

1180 lines
39 KiB
Python
Raw Permalink 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, Query, Body
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(tags=["审核系统"])
# ============================================
# 项目审核配置管理
# ============================================
@router.get("/projects/{project_id}/review-config")
async def get_review_config(project_id: str):
"""
获取项目的审核配置
Args:
project_id: 项目ID
Returns:
审核配置(前端格式)
"""
project = await project_repo.get(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"项目不存在: {project_id}"
)
# 从项目配置中获取审核配置
# 如果还没有配置,返回默认配置
raw_config = getattr(project, 'reviewConfig', None)
# 如果配置是字典,需要转换为 ReviewConfig 对象
if isinstance(raw_config, dict):
review_config = ReviewConfig(**raw_config)
elif raw_config is None:
review_config = ReviewConfig()
else:
# 已经是 ReviewConfig 对象
review_config = raw_config
# 转换为前端格式
frontend_config = _convert_to_frontend_config(review_config)
return frontend_config
def _convert_to_frontend_config(config: ReviewConfig) -> dict:
"""
将后端 ReviewConfig 转换为前端格式
Args:
config: 后端配置
Returns:
前端格式配置
"""
# 定义默认维度配置(前端格式)
default_dimensions = {
"character_consistency": {"name": "角色一致性", "enabled": True, "strictness": 50, "weight": 0.8},
"plot_coherence": {"name": "剧情连贯性", "enabled": True, "strictness": 50, "weight": 0.9},
"dialogue_quality": {"name": "对话质量", "enabled": True, "strictness": 50, "weight": 0.7},
"pacing": {"name": "节奏控制", "enabled": True, "strictness": 50, "weight": 0.6},
"emotional_depth": {"name": "情感深度", "enabled": True, "strictness": 50, "weight": 0.7},
"thematic_strength": {"name": "主题强度", "enabled": True, "strictness": 50, "weight": 0.8}
}
# 从后端配置中提取维度设置
dimensions = default_dimensions.copy()
for dim_type, dim_config in config.dimension_settings.items():
dim_key = dim_type.value
if dim_key in default_dimensions:
dimensions[dim_key] = {
"name": default_dimensions[dim_key]["name"],
"enabled": dim_config.enabled,
"strictness": int(dim_config.strictness * 100), # 转换 0-1 为 0-100
"weight": dim_config.weight
}
else:
# 处理未知维度
dimensions[dim_key] = {
"name": dim_type.value,
"enabled": dim_config.enabled,
"strictness": int(dim_config.strictness * 100),
"weight": dim_config.weight
}
# 转换自定义规则
custom_rules = []
for rule in config.custom_rules:
dimension_to_category = {
DimensionType.character: "character",
DimensionType.plot: "plot",
DimensionType.dialogue: "dialogue",
DimensionType.pacing: "pacing",
DimensionType.emotional_depth: "emotion",
DimensionType.thematic_strength: "theme",
DimensionType.custom: "other"
}
custom_rules.append({
"id": rule.id,
"name": rule.name,
"description": rule.description,
"triggerCondition": rule.trigger_condition,
"severity": rule.severity.value if hasattr(rule.severity, 'value') else rule.severity,
"category": dimension_to_category.get(rule.dimension, "other"),
"isActive": rule.enabled
})
# 根据 overall_strictness 确定 preset
strictness_to_preset = {
(0.0, 0.6): "draft",
(0.6, 0.8): "standard",
(0.8, 1.0): "strict"
}
preset = "custom"
for (low, high), p in strictness_to_preset.items():
if low <= config.overall_strictness < high:
preset = p
break
return {
"preset": preset,
"dimensions": dimensions,
"customRules": custom_rules,
"createdAt": datetime.now().isoformat(),
"updatedAt": datetime.now().isoformat(),
# 额外的配置信息
"overall_strictness": int(config.overall_strictness * 100), # 转换为 0-100
"pass_threshold": config.pass_threshold,
"auto_fix_enabled": config.auto_fix_enabled,
"enabled_review_skills": config.enabled_review_skills,
"custom_rules_count": len(config.custom_rules),
"enabled_dimensions_count": sum(1 for d in config.dimension_settings.values() if d.enabled),
"total_dimensions_count": len(config.dimension_settings)
}
@router.put("/projects/{project_id}/review-config")
async def update_review_config(
project_id: str,
config_update: ReviewConfigUpdate
):
"""
更新项目的审核配置
Args:
project_id: 项目ID
config_update: 配置更新数据
Returns:
更新后的配置(前端格式)
"""
project = await project_repo.get(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"项目不存在: {project_id}"
)
try:
# 获取现有配置
raw_config = getattr(project, 'reviewConfig', None)
# 如果配置是字典,需要转换为 ReviewConfig 对象
if isinstance(raw_config, dict):
current_config = ReviewConfig(**raw_config)
elif raw_config is None:
current_config = ReviewConfig()
else:
# 已经是 ReviewConfig 对象
current_config = raw_config
# 确保 dimension_settings 存在并是 dict 类型
if not hasattr(current_config, 'dimension_settings') or not isinstance(current_config.dimension_settings, dict):
current_config.dimension_settings = {}
# 获取更新数据
update_data = config_update.dict(exclude_unset=True, exclude_none=True)
# 处理前端格式转换
if "dimensions" in update_data and update_data["dimensions"]:
# 前端发送的 dimensions 需要转换为后端的 dimension_settings
dimension_settings = {}
for dim_key, dim_config in update_data["dimensions"].items():
# 转换为 DimensionType
try:
dim_type = DimensionType(dim_key)
except ValueError:
# 如果不是有效的 DimensionType使用 custom
dim_type = DimensionType.custom
# 转换前端格式到后端格式
# frontend: strictness 0-100, backend: 0-1
strictness = dim_config.get("strictness", 50) / 100.0
dimension_settings[dim_type] = DimensionConfig(
enabled=dim_config.get("enabled", True),
strictness=strictness,
weight=dim_config.get("weight", 0.5),
custom_rules=[]
)
current_config.dimension_settings = dimension_settings
if "customRules" in update_data and update_data["customRules"]:
# 前端发送的 customRules 需要转换为后端的 custom_rules
custom_rules = []
for rule_data in update_data["customRules"]:
# 映射 severity
severity = rule_data.get("severity", "medium")
# 映射 category 到 dimension
category_to_dimension = {
"character": DimensionType.character,
"plot": DimensionType.plot,
"dialogue": DimensionType.dialogue,
"pacing": DimensionType.pacing,
"emotion": DimensionType.emotional_depth,
"theme": DimensionType.thematic_strength,
"other": DimensionType.custom
}
dimension = category_to_dimension.get(rule_data.get("category", "other"), DimensionType.custom)
custom_rules.append(CustomRule(
id=rule_data.get("id", str(uuid.uuid4())),
name=rule_data.get("name", ""),
description=rule_data.get("description", ""),
trigger_condition=rule_data.get("triggerCondition", ""),
dimension=dimension,
severity=severity,
enabled=rule_data.get("isActive", True),
created_at=datetime.now(),
updated_at=datetime.now()
))
current_config.custom_rules = custom_rules
# 处理 preset
if "preset" in update_data and update_data["preset"]:
# 根据 preset 设置 overall_strictness
preset_strictness = {
"draft": 0.5,
"standard": 0.7,
"strict": 0.9,
"custom": current_config.overall_strictness
}
current_config.overall_strictness = preset_strictness.get(
update_data["preset"], current_config.overall_strictness
)
# 更新其他字段(排除已处理的前端字段)
frontend_fields = {"dimensions", "customRules", "preset"}
for field, value in update_data.items():
if field not in frontend_fields:
setattr(current_config, field, value)
# 保存到项目
await project_repo.update(project_id, {
"reviewConfig": current_config.dict()
})
logger.info(f"更新项目审核配置: {project_id}")
# 返回前端格式
return _convert_to_frontend_config(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
# ============================================
# 项目审核 Skills 管理
# ============================================
@router.get("/projects/{project_id}/review/skills")
async def get_project_review_skills(project_id: str):
"""
获取项目的审核 Skills
返回所有可用的 Skills不仅是review类型以及项目中已启用的审核 Skills
Args:
project_id: 项目ID
Returns:
包含可用 Skills 和已启用 Skills 的响应
"""
from app.core.skills.skill_manager import get_skill_manager
project = await project_repo.get(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"项目不存在: {project_id}"
)
skill_manager = get_skill_manager()
# 获取所有 Skills不限制分类
all_skills = await skill_manager.list_skills()
# 获取项目已启用的审核 Skills
raw_config = getattr(project, 'reviewConfig', None)
enabled_skill_ids = []
if raw_config and isinstance(raw_config, dict):
enabled_skill_ids = raw_config.get('enabled_review_skills', [])
# 获取所有有效的 skill IDs
valid_skill_ids = {skill_dict.get("id") if hasattr(skill, 'dict') else skill.get("id")
for skill in all_skills
for skill_dict in [skill.dict() if hasattr(skill, 'dict') else skill]}
# 过滤掉不存在的 skill IDs防止已删除的技能仍在启用列表中
valid_enabled_skill_ids = [sid for sid in enabled_skill_ids if sid in valid_skill_ids]
# 如果过滤后发现有不存在的 skill IDs更新项目配置
if len(valid_enabled_skill_ids) != len(enabled_skill_ids):
logger.warning(f"Project {project_id} has invalid enabled_review_skills: "
f"removed {set(enabled_skill_ids) - set(valid_enabled_skill_ids)}")
# 异步更新项目配置(不阻塞响应)
import json
from app.models.review import ReviewConfig
if isinstance(raw_config, dict):
config = ReviewConfig(**raw_config)
else:
config = ReviewConfig()
config.enabled_review_skills = valid_enabled_skill_ids
await project_repo.update(project_id, {
"reviewConfig": json.loads(config.json())
})
# 为每个 Skill 添加启用状态,并转换为前端期望的格式
skills_with_status = []
for skill in all_skills:
skill_dict = skill.dict() if hasattr(skill, 'dict') else skill
# 转换为前端期望的格式
frontend_skill = {
"id": skill_dict.get("id", ""),
"name": skill_dict.get("name", ""),
"description": skill_dict.get("behavior_guide", "") or skill_dict.get("description", ""),
"category": skill_dict.get("category", "other"),
"enabled": skill_dict.get("id", "") in valid_enabled_skill_ids,
"type": skill_dict.get("type", "user"),
"tags": skill_dict.get("tags", [])
}
skills_with_status.append(frontend_skill)
return {
"skills": skills_with_status,
"enabled_skill_ids": valid_enabled_skill_ids
}
@router.put("/projects/{project_id}/review/skills/{skill_id}")
async def update_project_review_skill(
project_id: str,
skill_id: str,
data: dict = Body(...)
):
"""
更新项目审核 Skill 的启用状态
Args:
project_id: 项目ID
skill_id: Skill ID
data: 请求体,包含 enabled 字段
Returns:
更新后的状态
"""
from app.models.review import ReviewConfig
import json
enabled = data.get('enabled', True)
project = await project_repo.get(project_id)
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"项目不存在: {project_id}"
)
# 获取现有配置
raw_config = getattr(project, 'reviewConfig', None)
if isinstance(raw_config, dict):
current_config = ReviewConfig(**raw_config)
else:
current_config = ReviewConfig()
# 更新启用的 Skills 列表
enabled_skills = set(current_config.enabled_review_skills or [])
if enabled:
enabled_skills.add(skill_id)
else:
enabled_skills.discard(skill_id)
current_config.enabled_review_skills = list(enabled_skills)
# 保存配置
await project_repo.update(project_id, {
"reviewConfig": json.loads(current_config.json())
})
logger.info(f"更新项目审核Skill: {project_id} - {skill_id} = {enabled}, enabled_skills: {list(enabled_skills)}")
return {
"success": True,
"skill_id": skill_id,
"enabled": enabled,
"enabled_skills": list(enabled_skills)
}
# ============================================
# 审核执行
# ============================================
@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:
# 获取审核配置
raw_config = getattr(project, 'reviewConfig', None)
if isinstance(raw_config, dict):
config = ReviewConfig(**raw_config)
else:
config = raw_config or 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}"
)
raw_config = getattr(project, 'reviewConfig', None)
if isinstance(raw_config, dict):
config = ReviewConfig(**raw_config)
else:
config = raw_config or 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 = Query(..., description="项目ID")
):
"""
创建自定义规则
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:
# 处理 category 到 dimension 的映射
if rule_data.category:
# 映射 category 到 dimension
category_to_dimension = {
"character": DimensionType.character,
"plot": DimensionType.plot,
"dialogue": DimensionType.dialogue,
"pacing": DimensionType.pacing,
"emotion": DimensionType.emotional_depth,
"theme": DimensionType.thematic_strength,
"other": DimensionType.custom
}
dimension = category_to_dimension.get(rule_data.category, DimensionType.custom)
else:
dimension = rule_data.dimension or DimensionType.custom
# 创建规则
new_rule = CustomRule(
id=str(uuid.uuid4()),
name=rule_data.name,
description=rule_data.description,
trigger_condition=rule_data.trigger_condition,
dimension=dimension,
severity=rule_data.severity,
enabled=True
)
# 添加到项目配置
raw_config = getattr(project, 'reviewConfig', None)
if isinstance(raw_config, dict):
config = ReviewConfig(**raw_config)
else:
config = raw_config or 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 = Query(..., description="项目ID")
):
"""
更新自定义规则
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}"
)
raw_config = getattr(project, 'reviewConfig', None)
if isinstance(raw_config, dict):
config = ReviewConfig(**raw_config)
else:
config = raw_config or 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)
# 处理 category 到 dimension 的映射
if 'category' in update_data:
category_to_dimension = {
"character": DimensionType.character,
"plot": DimensionType.plot,
"dialogue": DimensionType.dialogue,
"pacing": DimensionType.pacing,
"emotion": DimensionType.emotional_depth,
"theme": DimensionType.thematic_strength,
"other": DimensionType.custom
}
update_data['dimension'] = category_to_dimension.get(
update_data['category'],
rule.dimension or DimensionType.custom
)
# 移除 category 字段
del update_data['category']
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 = Query(..., description="项目ID")
):
"""
删除自定义规则
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}"
)
raw_config = getattr(project, 'reviewConfig', None)
if isinstance(raw_config, dict):
config = ReviewConfig(**raw_config)
else:
config = raw_config or 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}"
)
raw_config = getattr(project, 'reviewConfig', None)
if isinstance(raw_config, dict):
config = ReviewConfig(**raw_config)
else:
config = raw_config or 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)}"
)
# ============================================
# 审核配置预览
# ============================================
@router.post("/projects/{project_id}/review/preview")
async def preview_review_config(
project_id: str,
config: ReviewConfigUpdate
):
"""
预览审核配置
整合配置+Skills内容文字形式展示模拟注入Agent的效果
Args:
project_id: 项目ID
config: 配置数据
Returns:
预览文本和统计信息
"""
try:
from app.core.skills.skill_manager import get_skill_manager
# 获取Skills详情
skill_manager = get_skill_manager()
skills_info = []
if config.enabled_review_skills:
for skill_id in config.enabled_review_skills:
skill = skill_manager.get_skill_by_id(skill_id)
if skill:
skills_info.append({
"id": skill.id,
"name": skill.name,
"description": getattr(skill, 'description', ''),
"behavior_guide": skill.behavior_guide
})
# 生成预览文本模拟注入Agent
preview_lines = []
preview_lines.append("# 审核配置预览")
preview_lines.append("")
preview_lines.append("## 基础配置")
preset_text = config.preset if config.preset else "custom"
preset_map = {
"draft": "草稿模式",
"standard": "标准模式",
"strict": "严格模式"
}
preview_lines.append(f"- 预设模式: {preset_map.get(preset_text, preset_text)}")
if config.overall_strictness is not None:
preview_lines.append(f"- 整体严格度: {int(config.overall_strictness * 100)}%")
if config.pass_threshold is not None:
preview_lines.append(f"- 通过阈值: {config.pass_threshold}")
if config.auto_fix_enabled is not None:
preview_lines.append(f"- 自动修复: {'启用' if config.auto_fix_enabled else '禁用'}")
# 维度配置
if config.dimension_settings:
preview_lines.append("")
preview_lines.append("## 维度配置")
for dim_type, dim_config in config.dimension_settings.items():
enabled_text = "启用" if dim_config.enabled else "禁用"
preview_lines.append(f"")
preview_lines.append(f"### {dim_type.value if hasattr(dim_type, 'value') else dim_type}")
preview_lines.append(f"- 状态: {enabled_text}")
preview_lines.append(f"- 严格度: {int(dim_config.strictness * 100)}%")
preview_lines.append(f"- 权重: {dim_config.weight}")
# Skills配置
if skills_info:
preview_lines.append("")
preview_lines.append("## 启用的Skills")
for skill in skills_info:
preview_lines.append(f"")
preview_lines.append(f"### {skill['name']}")
preview_lines.append(f"- ID: `{skill['id']}`")
if skill.get('description'):
preview_lines.append(f"- 描述: {skill['description']}")
preview_lines.append("")
preview_lines.append("**行为指南:**")
preview_lines.append("```")
preview_lines.append(skill['behavior_guide'])
preview_lines.append("```")
# 自定义规则
custom_rules = config.custom_rules if config.custom_rules else []
if custom_rules:
preview_lines.append("")
preview_lines.append("## 自定义规则")
for rule in custom_rules:
preview_lines.append(f"")
preview_lines.append(f"### {rule.name}")
preview_lines.append(f"- 描述: {rule.description}")
preview_lines.append(f"- 维度: {rule.dimension.value if hasattr(rule.dimension, 'value') else rule.dimension}")
preview_lines.append(f"- 严重度: {rule.severity.value if hasattr(rule.severity, 'value') else rule.severity}")
preview_lines.append(f"- 触发条件: {rule.trigger_condition}")
preview_text = "\n".join(preview_lines)
return {
"preview_text": preview_text,
"skills_count": len(skills_info),
"rules_count": len(custom_rules),
"dimensions_count": len(config.dimension_settings) if config.dimension_settings else 0
}
except Exception as e:
logger.error(f"预览审核配置失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"预览失败: {str(e)}"
)