feat: 实现审核系统核心功能与UI优化
- 新增审核卡片和确认卡片模型,支持Agent推送审核任务和用户确认 - 实现审核卡片API服务,支持创建、更新、批准、驳回等操作 - 扩展审核维度配置,新增角色一致性、剧情连贯性等维度 - 优化前端审核配置页面,修复API路径错误和状态枚举问题 - 改进剧集创作平台布局,新增左侧边栏用于剧集管理和上下文查看 - 增强Skill管理,支持从审核系统跳转创建/编辑Skill - 修复episodes.json数据问题,清理聊天历史记录 - 更新Agent提示词,明确Skill引用加载流程 - 统一前端主题配置,优化整体UI体验
This commit is contained in:
parent
9327af6021
commit
5487450f34
@ -1 +1,5 @@
|
|||||||
# API v1 module
|
# API v1 module
|
||||||
|
|
||||||
|
from app.api.v1 import review, projects, review_task, websocket, memory
|
||||||
|
|
||||||
|
__all__ = ["review", "projects", "review_task", "websocket", "memory"]
|
||||||
|
|||||||
319
backend/app/api/v1/confirm_cards.py
Normal file
319
backend/app/api/v1/confirm_cards.py
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
"""
|
||||||
|
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)}")
|
||||||
@ -550,6 +550,131 @@ async def get_foreshadowing(
|
|||||||
return foreshadowing
|
return foreshadowing
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/foreshadowing")
|
||||||
|
async def create_foreshadowing(
|
||||||
|
project_id: str,
|
||||||
|
request: dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
手动添加伏笔
|
||||||
|
|
||||||
|
允许用户手动添加伏笔
|
||||||
|
"""
|
||||||
|
project = await verify_project(project_id)
|
||||||
|
|
||||||
|
# 直接创建伏笔字典
|
||||||
|
new_foreshadowing = {
|
||||||
|
'id': str(uuid.uuid4()),
|
||||||
|
'description': request.get('description', ''),
|
||||||
|
'introduced_at': request.get('plantedEpisode', 1),
|
||||||
|
'payoff_at': request.get('payoffEpisode'),
|
||||||
|
'type': request.get('type', 'plot'),
|
||||||
|
'importance': request.get('importance', ImportanceLevel.MEDIUM),
|
||||||
|
'is_payed_off': False,
|
||||||
|
'related_events': [],
|
||||||
|
'notes': request.get('notes', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加到记忆
|
||||||
|
if project.memory.foreshadowing is None:
|
||||||
|
project.memory.foreshadowing = []
|
||||||
|
project.memory.foreshadowing.append(new_foreshadowing)
|
||||||
|
|
||||||
|
# 保存
|
||||||
|
await project_repo.update(project_id, {
|
||||||
|
"memory": project.memory.dict()
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"创建伏笔: {new_foreshadowing['id']}")
|
||||||
|
return new_foreshadowing
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/foreshadowing/{foreshadowing_id}")
|
||||||
|
async def update_foreshadowing(
|
||||||
|
project_id: str,
|
||||||
|
foreshadowing_id: str,
|
||||||
|
request: dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
更新伏笔信息
|
||||||
|
|
||||||
|
允许修改伏笔的描述、状态等
|
||||||
|
"""
|
||||||
|
project = await verify_project(project_id)
|
||||||
|
|
||||||
|
# 查找伏笔
|
||||||
|
foreshadowing = None
|
||||||
|
for f in project.memory.foreshadowing or []:
|
||||||
|
if f.get('id') == foreshadowing_id:
|
||||||
|
foreshadowing = f
|
||||||
|
break
|
||||||
|
|
||||||
|
if not foreshadowing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"伏笔不存在: {foreshadowing_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新字段
|
||||||
|
if request.get('description'):
|
||||||
|
foreshadowing['description'] = request['description']
|
||||||
|
|
||||||
|
if request.get('importance'):
|
||||||
|
foreshadowing['importance'] = request['importance']
|
||||||
|
|
||||||
|
if 'payoffEpisode' in request:
|
||||||
|
foreshadowing['payoff_at'] = request['payoffEpisode']
|
||||||
|
foreshadowing['is_payed_off'] = request['payoffEpisode'] is not None
|
||||||
|
|
||||||
|
if request.get('notes'):
|
||||||
|
foreshadowing['notes'] = request['notes']
|
||||||
|
|
||||||
|
# 保存
|
||||||
|
await project_repo.update(project_id, {
|
||||||
|
"memory": project.memory.dict()
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"更新伏笔: {foreshadowing_id}")
|
||||||
|
return foreshadowing
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/foreshadowing/{foreshadowing_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_foreshadowing(
|
||||||
|
project_id: str,
|
||||||
|
foreshadowing_id: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
删除伏笔
|
||||||
|
|
||||||
|
移除不需要的伏笔记录
|
||||||
|
"""
|
||||||
|
project = await verify_project(project_id)
|
||||||
|
|
||||||
|
# 直接操作 memory.foreshadowing (List[Dict])
|
||||||
|
foreshadowing_list = project.memory.foreshadowing or []
|
||||||
|
original_count = len(foreshadowing_list)
|
||||||
|
|
||||||
|
# 查找并删除
|
||||||
|
project.memory.foreshadowing = [
|
||||||
|
f for f in foreshadowing_list
|
||||||
|
if f.get('id') != foreshadowing_id
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(project.memory.foreshadowing) == original_count:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"伏笔不存在: {foreshadowing_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存
|
||||||
|
await project_repo.update(project_id, {
|
||||||
|
"memory": project.memory.dict()
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"删除伏笔: {foreshadowing_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 一致性检查
|
# 一致性检查
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
提供项目的 CRUD 操作和剧集执行功能
|
提供项目的 CRUD 操作和剧集执行功能
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Body
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@ -851,3 +851,375 @@ async def delete_episode_skill_config(
|
|||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=f"删除失败: {str(e)}"
|
detail=f"删除失败: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 审核配置管理(与前端API路径匹配)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@router.get("/{project_id}/review-config")
|
||||||
|
async def get_review_config(project_id: str):
|
||||||
|
"""
|
||||||
|
获取项目的审核配置
|
||||||
|
API路径与前端调用匹配:/projects/{project_id}/review-config
|
||||||
|
"""
|
||||||
|
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 raw_config and isinstance(raw_config, dict):
|
||||||
|
from app.models.review import ReviewConfig
|
||||||
|
try:
|
||||||
|
review_config = ReviewConfig(**raw_config)
|
||||||
|
except Exception:
|
||||||
|
review_config = None
|
||||||
|
else:
|
||||||
|
review_config = raw_config
|
||||||
|
|
||||||
|
# 转换为前端期望的格式
|
||||||
|
if review_config:
|
||||||
|
from app.models.review import DimensionType, DimensionConfig
|
||||||
|
|
||||||
|
# 构建前端期望的数据格式
|
||||||
|
frontend_config = {
|
||||||
|
"preset": "standard",
|
||||||
|
"pass_threshold": getattr(review_config, 'pass_threshold', 75),
|
||||||
|
"overall_strictness": int(getattr(review_config, 'overall_strictness', 0.7) * 100),
|
||||||
|
"dimensions": {},
|
||||||
|
"customRules": [],
|
||||||
|
"createdAt": project.createdAt,
|
||||||
|
"updatedAt": project.updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
# 转换维度配置
|
||||||
|
if hasattr(review_config, 'dimension_settings'):
|
||||||
|
dimension_names = {
|
||||||
|
DimensionType.character_consistency: "character_consistency",
|
||||||
|
DimensionType.consistency: "character_consistency",
|
||||||
|
DimensionType.quality: "quality",
|
||||||
|
DimensionType.dialogue_quality: "dialogue_quality",
|
||||||
|
DimensionType.dialogue: "dialogue_quality",
|
||||||
|
DimensionType.pacing: "pacing",
|
||||||
|
DimensionType.character: "character_consistency",
|
||||||
|
DimensionType.plot: "plot_coherence",
|
||||||
|
DimensionType.plot_coherence: "plot_coherence",
|
||||||
|
DimensionType.emotional_depth: "emotional_depth",
|
||||||
|
DimensionType.thematic_strength: "thematic_strength"
|
||||||
|
}
|
||||||
|
|
||||||
|
for dim_type, dim_config in review_config.dimension_settings.items():
|
||||||
|
dim_name = dimension_names.get(dim_type, dim_type.value if hasattr(dim_type, 'value') else str(dim_type))
|
||||||
|
frontend_config["dimensions"][dim_name] = {
|
||||||
|
"name": getattr(dim_config, 'name', dim_name),
|
||||||
|
"enabled": getattr(dim_config, 'enabled', True),
|
||||||
|
"strictness": int(getattr(dim_config, 'strictness', 0.7) * 100),
|
||||||
|
"weight": getattr(dim_config, 'weight', 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 确保所有维度都有默认值
|
||||||
|
default_dimensions = [
|
||||||
|
"character_consistency", "plot_coherence", "dialogue_quality",
|
||||||
|
"pacing", "emotional_depth", "thematic_strength", "quality"
|
||||||
|
]
|
||||||
|
for dim_name in default_dimensions:
|
||||||
|
if dim_name not in frontend_config["dimensions"]:
|
||||||
|
frontend_config["dimensions"][dim_name] = {
|
||||||
|
"name": dim_name,
|
||||||
|
"enabled": True,
|
||||||
|
"strictness": 50,
|
||||||
|
"weight": 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
# 转换自定义规则
|
||||||
|
if hasattr(review_config, 'custom_rules'):
|
||||||
|
frontend_config["customRules"] = [
|
||||||
|
{
|
||||||
|
"id": rule.id,
|
||||||
|
"name": rule.name,
|
||||||
|
"description": rule.description,
|
||||||
|
"triggerCondition": rule.trigger_condition,
|
||||||
|
"severity": rule.severity,
|
||||||
|
"category": rule.dimension,
|
||||||
|
"isActive": getattr(rule, 'enabled', True)
|
||||||
|
}
|
||||||
|
for rule in review_config.custom_rules
|
||||||
|
]
|
||||||
|
|
||||||
|
return frontend_config
|
||||||
|
|
||||||
|
# 如果没有配置,返回默认配置
|
||||||
|
return {
|
||||||
|
"preset": "standard",
|
||||||
|
"pass_threshold": 75,
|
||||||
|
"overall_strictness": 70,
|
||||||
|
"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},
|
||||||
|
"quality": {"name": "整体质量", "enabled": True, "strictness": 50, "weight": 0.7}
|
||||||
|
},
|
||||||
|
"customRules": [],
|
||||||
|
"createdAt": project.createdAt,
|
||||||
|
"updatedAt": project.updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{project_id}/review-config")
|
||||||
|
async def update_review_config(project_id: str, config_update: dict):
|
||||||
|
"""
|
||||||
|
更新项目的审核配置
|
||||||
|
API路径与前端调用匹配:/projects/{project_id}/review-config
|
||||||
|
"""
|
||||||
|
project = await project_repo.get(project_id)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"项目不存在: {project_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.models.review import ReviewConfig, DimensionConfig, DimensionType, CustomRule
|
||||||
|
|
||||||
|
# 获取现有配置
|
||||||
|
raw_config = getattr(project, 'reviewConfig', None)
|
||||||
|
|
||||||
|
# 如果是字典,转换为 ReviewConfig 对象;否则创建新的
|
||||||
|
if isinstance(raw_config, dict):
|
||||||
|
current_config = ReviewConfig(**raw_config)
|
||||||
|
else:
|
||||||
|
current_config = raw_config if raw_config else ReviewConfig()
|
||||||
|
|
||||||
|
# 更新基本配置
|
||||||
|
if 'pass_threshold' in config_update:
|
||||||
|
current_config.pass_threshold = float(config_update['pass_threshold'])
|
||||||
|
|
||||||
|
if 'overall_strictness' in config_update:
|
||||||
|
current_config.overall_strictness = float(config_update['overall_strictness']) / 100
|
||||||
|
|
||||||
|
# 更新预设
|
||||||
|
if 'preset' in config_update:
|
||||||
|
# 这里可以根据预设应用不同的配置
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 更新维度配置
|
||||||
|
if 'dimensions' in config_update:
|
||||||
|
dimension_map = {
|
||||||
|
"character_consistency": DimensionType.character_consistency,
|
||||||
|
"plot_coherence": DimensionType.plot_coherence,
|
||||||
|
"dialogue_quality": DimensionType.dialogue_quality,
|
||||||
|
"pacing": DimensionType.pacing,
|
||||||
|
"emotional_depth": DimensionType.emotional_depth,
|
||||||
|
"thematic_strength": DimensionType.thematic_strength,
|
||||||
|
"quality": DimensionType.quality
|
||||||
|
}
|
||||||
|
|
||||||
|
for dim_name, dim_data in config_update['dimensions'].items():
|
||||||
|
dim_type = dimension_map.get(dim_name, DimensionType.quality)
|
||||||
|
current_config.dimension_settings[dim_type] = DimensionConfig(
|
||||||
|
enabled=dim_data.get('enabled', True),
|
||||||
|
strictness=dim_data.get('strictness', 50) / 100,
|
||||||
|
weight=dim_data.get('weight', 0.7)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新自定义规则
|
||||||
|
if 'customRules' in config_update:
|
||||||
|
current_config.custom_rules = []
|
||||||
|
for rule_data in config_update['customRules']:
|
||||||
|
if isinstance(rule_data, dict):
|
||||||
|
rule = CustomRule(
|
||||||
|
id=rule_data.get('id', str(__import__('uuid').uuid4())),
|
||||||
|
name=rule_data.get('name', ''),
|
||||||
|
description=rule_data.get('description', ''),
|
||||||
|
trigger_condition=rule_data.get('triggerCondition', ''),
|
||||||
|
dimension=rule_data.get('category', 'other'),
|
||||||
|
severity=rule_data.get('severity', 'medium'),
|
||||||
|
enabled=rule_data.get('isActive', True)
|
||||||
|
)
|
||||||
|
current_config.custom_rules.append(rule)
|
||||||
|
|
||||||
|
# 保存到项目 - 使用 .json() 方法确保枚举被正确序列化为字符串
|
||||||
|
import json
|
||||||
|
await project_repo.update(project_id, {
|
||||||
|
"reviewConfig": json.loads(current_config.json())
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"更新项目审核配置: {project_id}")
|
||||||
|
|
||||||
|
# 返回更新后的配置
|
||||||
|
return await get_review_config(project_id)
|
||||||
|
|
||||||
|
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("/{project_id}/review-config/preset")
|
||||||
|
async def apply_review_preset(project_id: str, data: dict):
|
||||||
|
"""
|
||||||
|
应用审核预设
|
||||||
|
API路径与前端调用匹配:/projects/{project_id}/review-config/preset
|
||||||
|
"""
|
||||||
|
project = await project_repo.get(project_id)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"项目不存在: {project_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
preset = data.get('preset', 'standard')
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.models.review import ReviewConfig, DimensionConfig, DimensionType
|
||||||
|
|
||||||
|
# 根据预设创建配置
|
||||||
|
config = ReviewConfig()
|
||||||
|
|
||||||
|
if preset == 'draft':
|
||||||
|
config.overall_strictness = 0.5
|
||||||
|
config.pass_threshold = 70.0
|
||||||
|
elif preset == 'strict':
|
||||||
|
config.overall_strictness = 0.9
|
||||||
|
config.pass_threshold = 85.0
|
||||||
|
else: # standard
|
||||||
|
config.overall_strictness = 0.7
|
||||||
|
config.pass_threshold = 75.0
|
||||||
|
|
||||||
|
# 应用维度设置
|
||||||
|
for dim_type in [DimensionType.consistency, DimensionType.quality, DimensionType.dialogue]:
|
||||||
|
config.dimension_settings[dim_type] = DimensionConfig(
|
||||||
|
enabled=True,
|
||||||
|
strictness=config.overall_strictness,
|
||||||
|
weight=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存配置 - 使用 .json() 方法确保枚举被正确序列化为字符串
|
||||||
|
import json
|
||||||
|
await project_repo.update(project_id, {
|
||||||
|
"reviewConfig": json.loads(config.json())
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"应用审核预设: {project_id} - {preset}")
|
||||||
|
|
||||||
|
return await get_review_config(project_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"应用预设失败: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"应用预设失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 审核 Skills 管理
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@router.get("/{project_id}/review/skills")
|
||||||
|
async def get_project_review_skills(project_id: str):
|
||||||
|
"""获取项目的审核 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 添加启用状态,并转换为前端期望的格式
|
||||||
|
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 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": enabled_skill_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{project_id}/review/skills/{skill_id}")
|
||||||
|
async def update_project_review_skill(
|
||||||
|
project_id: str,
|
||||||
|
skill_id: str,
|
||||||
|
data: dict = None
|
||||||
|
):
|
||||||
|
"""更新项目审核 Skill 的启用状态"""
|
||||||
|
from fastapi import Body
|
||||||
|
from app.models.review import ReviewConfig
|
||||||
|
|
||||||
|
enabled = True
|
||||||
|
if data and 'enabled' in data:
|
||||||
|
enabled = data['enabled']
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 保存配置
|
||||||
|
import json
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ Review System API Routes
|
|||||||
|
|
||||||
提供审核配置、执行、维度和自定义规则的管理接口
|
提供审核配置、执行、维度和自定义规则的管理接口
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -32,14 +32,14 @@ from app.utils.logger import get_logger
|
|||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
router = APIRouter(prefix="/review", tags=["审核系统"])
|
router = APIRouter(tags=["审核系统"])
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 项目审核配置管理
|
# 项目审核配置管理
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
@router.get("/projects/{project_id}/review-config", response_model=ReviewConfig)
|
@router.get("/projects/{project_id}/review-config")
|
||||||
async def get_review_config(project_id: str):
|
async def get_review_config(project_id: str):
|
||||||
"""
|
"""
|
||||||
获取项目的审核配置
|
获取项目的审核配置
|
||||||
@ -48,7 +48,7 @@ async def get_review_config(project_id: str):
|
|||||||
project_id: 项目ID
|
project_id: 项目ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ReviewConfig: 审核配置
|
审核配置(前端格式)
|
||||||
"""
|
"""
|
||||||
project = await project_repo.get(project_id)
|
project = await project_repo.get(project_id)
|
||||||
if not project:
|
if not project:
|
||||||
@ -59,14 +59,115 @@ async def get_review_config(project_id: str):
|
|||||||
|
|
||||||
# 从项目配置中获取审核配置
|
# 从项目配置中获取审核配置
|
||||||
# 如果还没有配置,返回默认配置
|
# 如果还没有配置,返回默认配置
|
||||||
review_config = getattr(project, 'reviewConfig', None)
|
raw_config = getattr(project, 'reviewConfig', None)
|
||||||
if not review_config:
|
|
||||||
|
# 如果配置是字典,需要转换为 ReviewConfig 对象
|
||||||
|
if isinstance(raw_config, dict):
|
||||||
|
review_config = ReviewConfig(**raw_config)
|
||||||
|
elif raw_config is None:
|
||||||
review_config = ReviewConfig()
|
review_config = ReviewConfig()
|
||||||
|
else:
|
||||||
|
# 已经是 ReviewConfig 对象
|
||||||
|
review_config = raw_config
|
||||||
|
|
||||||
return review_config
|
# 转换为前端格式
|
||||||
|
frontend_config = _convert_to_frontend_config(review_config)
|
||||||
|
|
||||||
|
return frontend_config
|
||||||
|
|
||||||
|
|
||||||
@router.put("/projects/{project_id}/review-config", response_model=ReviewConfig)
|
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(
|
async def update_review_config(
|
||||||
project_id: str,
|
project_id: str,
|
||||||
config_update: ReviewConfigUpdate
|
config_update: ReviewConfigUpdate
|
||||||
@ -79,7 +180,7 @@ async def update_review_config(
|
|||||||
config_update: 配置更新数据
|
config_update: 配置更新数据
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ReviewConfig: 更新后的配置
|
更新后的配置(前端格式)
|
||||||
"""
|
"""
|
||||||
project = await project_repo.get(project_id)
|
project = await project_repo.get(project_id)
|
||||||
if not project:
|
if not project:
|
||||||
@ -90,13 +191,99 @@ async def update_review_config(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取现有配置
|
# 获取现有配置
|
||||||
current_config = getattr(project, 'reviewConfig', ReviewConfig())
|
raw_config = getattr(project, 'reviewConfig', None)
|
||||||
|
|
||||||
# 更新配置
|
# 如果配置是字典,需要转换为 ReviewConfig 对象
|
||||||
update_data = config_update.dict(exclude_unset=True)
|
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():
|
for field, value in update_data.items():
|
||||||
setattr(current_config, field, value)
|
if field not in frontend_fields:
|
||||||
|
setattr(current_config, field, value)
|
||||||
|
|
||||||
# 保存到项目
|
# 保存到项目
|
||||||
await project_repo.update(project_id, {
|
await project_repo.update(project_id, {
|
||||||
@ -105,7 +292,8 @@ async def update_review_config(
|
|||||||
|
|
||||||
logger.info(f"更新项目审核配置: {project_id}")
|
logger.info(f"更新项目审核配置: {project_id}")
|
||||||
|
|
||||||
return current_config
|
# 返回前端格式
|
||||||
|
return _convert_to_frontend_config(current_config)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"更新审核配置失败: {str(e)}")
|
logger.error(f"更新审核配置失败: {str(e)}")
|
||||||
@ -206,6 +394,149 @@ async def get_review_presets(project_id: str = None):
|
|||||||
return presets
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 审核执行
|
# 审核执行
|
||||||
# ============================================
|
# ============================================
|
||||||
@ -261,7 +592,11 @@ async def review_episode(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取审核配置
|
# 获取审核配置
|
||||||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
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}")
|
logger.info(f"开始审核: 项目 {project_id}, EP{episode_number}")
|
||||||
@ -415,7 +750,11 @@ async def list_custom_rules(
|
|||||||
detail=f"项目不存在: {project_id}"
|
detail=f"项目不存在: {project_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
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
|
rules = config.custom_rules
|
||||||
else:
|
else:
|
||||||
# 返回空列表(暂时不支持全局规则)
|
# 返回空列表(暂时不支持全局规则)
|
||||||
@ -431,7 +770,7 @@ async def list_custom_rules(
|
|||||||
@router.post("/custom-rules", response_model=CustomRule, status_code=status.HTTP_201_CREATED)
|
@router.post("/custom-rules", response_model=CustomRule, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_custom_rule(
|
async def create_custom_rule(
|
||||||
rule_data: CustomRuleCreate,
|
rule_data: CustomRuleCreate,
|
||||||
project_id: str
|
project_id: str = Query(..., description="项目ID")
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
创建自定义规则
|
创建自定义规则
|
||||||
@ -451,19 +790,39 @@ async def create_custom_rule(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
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(
|
new_rule = CustomRule(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
name=rule_data.name,
|
name=rule_data.name,
|
||||||
description=rule_data.description,
|
description=rule_data.description,
|
||||||
trigger_condition=rule_data.trigger_condition,
|
trigger_condition=rule_data.trigger_condition,
|
||||||
dimension=rule_data.dimension,
|
dimension=dimension,
|
||||||
severity=rule_data.severity,
|
severity=rule_data.severity,
|
||||||
enabled=True
|
enabled=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到项目配置
|
# 添加到项目配置
|
||||||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
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)
|
config.custom_rules.append(new_rule)
|
||||||
|
|
||||||
# 保存
|
# 保存
|
||||||
@ -487,7 +846,7 @@ async def create_custom_rule(
|
|||||||
async def update_custom_rule(
|
async def update_custom_rule(
|
||||||
rule_id: str,
|
rule_id: str,
|
||||||
rule_update: CustomRuleUpdate,
|
rule_update: CustomRuleUpdate,
|
||||||
project_id: str
|
project_id: str = Query(..., description="项目ID")
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
更新自定义规则
|
更新自定义规则
|
||||||
@ -507,7 +866,11 @@ async def update_custom_rule(
|
|||||||
detail=f"项目不存在: {project_id}"
|
detail=f"项目不存在: {project_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
raw_config = getattr(project, 'reviewConfig', None)
|
||||||
|
if isinstance(raw_config, dict):
|
||||||
|
config = ReviewConfig(**raw_config)
|
||||||
|
else:
|
||||||
|
config = raw_config or ReviewConfig()
|
||||||
|
|
||||||
# 查找规则
|
# 查找规则
|
||||||
rule = None
|
rule = None
|
||||||
@ -526,6 +889,24 @@ async def update_custom_rule(
|
|||||||
# 更新规则
|
# 更新规则
|
||||||
update_data = rule_update.dict(exclude_unset=True)
|
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():
|
for field, value in update_data.items():
|
||||||
setattr(rule, field, value)
|
setattr(rule, field, value)
|
||||||
|
|
||||||
@ -549,7 +930,10 @@ async def update_custom_rule(
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/custom-rules/{rule_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/custom-rules/{rule_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_custom_rule(rule_id: str, project_id: str):
|
async def delete_custom_rule(
|
||||||
|
rule_id: str,
|
||||||
|
project_id: str = Query(..., description="项目ID")
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
删除自定义规则
|
删除自定义规则
|
||||||
|
|
||||||
@ -567,7 +951,11 @@ async def delete_custom_rule(rule_id: str, project_id: str):
|
|||||||
detail=f"项目不存在: {project_id}"
|
detail=f"项目不存在: {project_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
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)
|
original_count = len(config.custom_rules)
|
||||||
@ -630,7 +1018,11 @@ async def test_custom_rule(
|
|||||||
detail=f"剧集不存在: {episode_id}"
|
detail=f"剧集不存在: {episode_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
config = getattr(project, 'reviewConfig', ReviewConfig())
|
raw_config = getattr(project, 'reviewConfig', None)
|
||||||
|
if isinstance(raw_config, dict):
|
||||||
|
config = ReviewConfig(**raw_config)
|
||||||
|
else:
|
||||||
|
config = raw_config or ReviewConfig()
|
||||||
|
|
||||||
# 查找规则
|
# 查找规则
|
||||||
rule = None
|
rule = None
|
||||||
@ -667,3 +1059,121 @@ async def test_custom_rule(
|
|||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=f"测试失败: {str(e)}"
|
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)}"
|
||||||
|
)
|
||||||
|
|||||||
274
backend/app/api/v1/review_cards.py
Normal file
274
backend/app/api/v1/review_cards.py
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
"""
|
||||||
|
Review Cards API Routes
|
||||||
|
|
||||||
|
审核卡片API路由 - 提供审核卡片的CRUD操作
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.models.review_card import (
|
||||||
|
ReviewCard, ReviewCardCreate, ReviewCardUpdate,
|
||||||
|
ReviewCardType, ReviewCardStatus
|
||||||
|
)
|
||||||
|
from app.db.review_card_repository import get_review_card_repo
|
||||||
|
from app.api.v1.websocket import manager
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/review-cards", tags=["审核卡片"])
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 依赖注入
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
async def get_repo():
|
||||||
|
"""获取审核卡片仓储"""
|
||||||
|
return get_review_card_repo()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CRUD 端点
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
async def create_review_card(
|
||||||
|
data: ReviewCardCreate,
|
||||||
|
repo=Depends(get_repo)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
创建审核卡片
|
||||||
|
|
||||||
|
触发场景:
|
||||||
|
1. Agent生成剧集内容后自动生成
|
||||||
|
2. 用户在对话框触发某一集/某几集/整个剧集的审核
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
card = await repo.create(data)
|
||||||
|
|
||||||
|
# 通过WebSocket推送通知
|
||||||
|
await manager.send_to_project(data.project_id, {
|
||||||
|
"type": "review_card_created",
|
||||||
|
"data": {
|
||||||
|
"card_id": card.id,
|
||||||
|
"card_type": card.card_type.value,
|
||||||
|
"episode_numbers": card.episode_numbers,
|
||||||
|
"severity": card.severity,
|
||||||
|
"review_reason": card.review_reason,
|
||||||
|
"created_at": card.created_at.isoformat()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"创建审核卡片成功: {card.id}")
|
||||||
|
return {"success": True, "card": card}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建审核卡片失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"创建审核卡片失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def list_review_cards(
|
||||||
|
project_id: str,
|
||||||
|
status: Optional[ReviewCardStatus] = None,
|
||||||
|
card_type: Optional[ReviewCardType] = None,
|
||||||
|
limit: int = Query(100, ge=1, le=500),
|
||||||
|
repo=Depends(get_repo)
|
||||||
|
) -> List[ReviewCard]:
|
||||||
|
"""
|
||||||
|
获取审核卡片列表
|
||||||
|
|
||||||
|
支持按状态和类型筛选
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cards = await repo.list_by_project(project_id, status, limit)
|
||||||
|
|
||||||
|
if card_type:
|
||||||
|
cards = [c for c in cards if c.card_type == card_type]
|
||||||
|
|
||||||
|
return cards
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取审核卡片列表失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"获取列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats/{project_id}")
|
||||||
|
async def get_review_card_stats(
|
||||||
|
project_id: str,
|
||||||
|
repo=Depends(get_repo)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取审核卡片统计信息
|
||||||
|
|
||||||
|
返回各类状态的数量统计
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
total = await repo.count_by_project(project_id)
|
||||||
|
pending = await repo.count_by_status(project_id, ReviewCardStatus.PENDING)
|
||||||
|
awaiting_user = await repo.count_by_status(project_id, ReviewCardStatus.AWAITING_USER)
|
||||||
|
approved = await repo.count_by_status(project_id, ReviewCardStatus.APPROVED)
|
||||||
|
rejected = await repo.count_by_status(project_id, ReviewCardStatus.REJECTED)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": total,
|
||||||
|
"pending": pending,
|
||||||
|
"awaiting_user": awaiting_user,
|
||||||
|
"approved": approved,
|
||||||
|
"rejected": rejected
|
||||||
|
}
|
||||||
|
|
||||||
|
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_review_card(
|
||||||
|
card_id: str,
|
||||||
|
repo=Depends(get_repo)
|
||||||
|
) -> ReviewCard:
|
||||||
|
"""获取审核卡片详情"""
|
||||||
|
card = await repo.get(card_id)
|
||||||
|
if not card:
|
||||||
|
raise HTTPException(status_code=404, detail="审核卡片不存在")
|
||||||
|
return card
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{card_id}")
|
||||||
|
async def update_review_card(
|
||||||
|
card_id: str,
|
||||||
|
data: ReviewCardUpdate,
|
||||||
|
repo=Depends(get_repo)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
更新审核卡片
|
||||||
|
|
||||||
|
支持更新状态、用户评论、修改内容等
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
card = await repo.update(card_id, data)
|
||||||
|
if not card:
|
||||||
|
raise HTTPException(status_code=404, detail="审核卡片不存在")
|
||||||
|
|
||||||
|
# 通知WebSocket
|
||||||
|
await manager.send_to_project(card.project_id, {
|
||||||
|
"type": "review_card_updated",
|
||||||
|
"data": {
|
||||||
|
"card_id": card_id,
|
||||||
|
"status": card.status.value,
|
||||||
|
"updated_at": card.updated_at.isoformat()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"更新审核卡片成功: {card_id}")
|
||||||
|
return {"success": True, "card": card}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新审核卡片失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"更新失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{card_id}")
|
||||||
|
async def delete_review_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}/approve")
|
||||||
|
async def approve_review_card(
|
||||||
|
card_id: str,
|
||||||
|
user_comment: Optional[str] = None,
|
||||||
|
repo=Depends(get_repo)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
通过审核卡片
|
||||||
|
|
||||||
|
将卡片状态设置为APPROVED
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
update_data = ReviewCardUpdate(
|
||||||
|
status=ReviewCardStatus.APPROVED,
|
||||||
|
user_comment=user_comment
|
||||||
|
)
|
||||||
|
result = await update_review_card(card_id, update_data, repo)
|
||||||
|
return result
|
||||||
|
|
||||||
|
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_review_card(
|
||||||
|
card_id: str,
|
||||||
|
user_comment: Optional[str] = None,
|
||||||
|
modified_content: Optional[str] = None,
|
||||||
|
repo=Depends(get_repo)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
驳回审核卡片
|
||||||
|
|
||||||
|
将卡片状态设置为REJECTED,可选地包含修改内容
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
update_data = ReviewCardUpdate(
|
||||||
|
status=ReviewCardStatus.REJECTED,
|
||||||
|
user_comment=user_comment,
|
||||||
|
modified_content=modified_content
|
||||||
|
)
|
||||||
|
result = await update_review_card(card_id, update_data, repo)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"驳回审核卡片失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{card_id}/modify")
|
||||||
|
async def modify_review_card(
|
||||||
|
card_id: str,
|
||||||
|
user_comment: Optional[str] = None,
|
||||||
|
modified_content: Optional[str] = None,
|
||||||
|
repo=Depends(get_repo)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
修改审核卡片内容
|
||||||
|
|
||||||
|
将卡片状态设置为MODIFIED,包含修改后的内容
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
update_data = ReviewCardUpdate(
|
||||||
|
status=ReviewCardStatus.AWAITING_USER,
|
||||||
|
user_comment=user_comment,
|
||||||
|
modified_content=modified_content
|
||||||
|
)
|
||||||
|
result = await update_review_card(card_id, update_data, repo)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"修改审核卡片失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}")
|
||||||
431
backend/app/api/v1/review_task.py
Normal file
431
backend/app/api/v1/review_task.py
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
"""
|
||||||
|
Review Task API Routes
|
||||||
|
|
||||||
|
审核任务API路由 - 提供过程中审核和生成后审核的管理接口
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
||||||
|
from typing import List, Optional
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from app.models.review_task import (
|
||||||
|
ReviewTask,
|
||||||
|
ReviewMode,
|
||||||
|
ReviewAction,
|
||||||
|
ProcessReviewRequest,
|
||||||
|
UserReviewActionRequest,
|
||||||
|
PostCreationReviewRequest,
|
||||||
|
PostCreationReviewResult,
|
||||||
|
ReviewTaskStatus
|
||||||
|
)
|
||||||
|
from app.core.review.review_task_manager import (
|
||||||
|
ReviewTaskManager,
|
||||||
|
get_review_task_manager,
|
||||||
|
StreamReviewHelper
|
||||||
|
)
|
||||||
|
from app.db.repositories import project_repo, episode_repo
|
||||||
|
from app.models.project import SeriesProject, Episode
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/review-task", tags=["审核任务"])
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 依赖注入
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
async def get_task_manager() -> ReviewTaskManager:
|
||||||
|
"""获取审核任务管理器"""
|
||||||
|
return get_review_task_manager()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_stream_helper(project_id: str) -> StreamReviewHelper:
|
||||||
|
"""获取流式审核助手"""
|
||||||
|
task_manager = get_review_task_manager()
|
||||||
|
return StreamReviewHelper(project_id, task_manager)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 过程中审核 API
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@router.post("/process")
|
||||||
|
async def create_process_review_task(
|
||||||
|
request: ProcessReviewRequest,
|
||||||
|
task_manager: ReviewTaskManager = Depends(get_task_manager)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
创建过程中审核任务
|
||||||
|
|
||||||
|
由Agent在流式输出过程中调用,推送审核任务到前端。
|
||||||
|
前端通过WebSocket接收通知并等待用户确认。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 创建审核任务
|
||||||
|
task = await task_manager.create_process_review_task(request)
|
||||||
|
|
||||||
|
# 通过WebSocket推送到前端
|
||||||
|
from app.api.v1.websocket import manager
|
||||||
|
await manager.send_to_project(request.project_id, {
|
||||||
|
"type": "review_task_created",
|
||||||
|
"data": {
|
||||||
|
"task_id": task.id,
|
||||||
|
"mode": task.mode.value,
|
||||||
|
"issue_type": task.issue_type,
|
||||||
|
"issue_description": task.issue_description,
|
||||||
|
"severity": task.severity,
|
||||||
|
"location": task.location.dict(),
|
||||||
|
"available_actions": [action.value for action in task.available_actions],
|
||||||
|
"suggestion": task.suggestion,
|
||||||
|
"trigger_source": task.trigger_source.value,
|
||||||
|
"created_at": task.created_at.isoformat()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"过程中审核任务已创建并推送: {task.id}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"task_id": task.id,
|
||||||
|
"message": "审核任务已创建"
|
||||||
|
}
|
||||||
|
|
||||||
|
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("/process/{task_id}/action")
|
||||||
|
async def handle_process_review_action(
|
||||||
|
task_id: str,
|
||||||
|
request: UserReviewActionRequest,
|
||||||
|
task_manager: ReviewTaskManager = Depends(get_task_manager)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
处理用户对过程中审核任务的响应
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务ID
|
||||||
|
request: 用户操作请求
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
处理结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 处理用户操作
|
||||||
|
task = await task_manager.handle_user_review_action(request)
|
||||||
|
|
||||||
|
# 通过WebSocket通知前端任务已完成
|
||||||
|
from app.api.v1.websocket import manager
|
||||||
|
await manager.send_to_project(task.project_id or "unknown", {
|
||||||
|
"type": "review_task_completed",
|
||||||
|
"data": {
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": task.status.value,
|
||||||
|
"user_response": task.user_response,
|
||||||
|
"completed_at": task.completed_at.isoformat() if task.completed_at else None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"用户操作已处理: {task_id}, 操作: {request.action.value}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": task.status.value,
|
||||||
|
"message": "操作已处理"
|
||||||
|
}
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
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("/process/pending/{project_id}")
|
||||||
|
async def get_pending_process_tasks(
|
||||||
|
project_id: str,
|
||||||
|
task_manager: ReviewTaskManager = Depends(get_task_manager)
|
||||||
|
) -> List[ReviewTask]:
|
||||||
|
"""
|
||||||
|
获取项目所有待处理的过程审核任务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: 项目ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[ReviewTask]: 待处理任务列表
|
||||||
|
"""
|
||||||
|
return task_manager.get_pending_process_tasks(project_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 生成后审核 API
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@router.post("/post-creation")
|
||||||
|
async def execute_post_creation_review(
|
||||||
|
request: PostCreationReviewRequest,
|
||||||
|
task_manager: ReviewTaskManager = Depends(get_task_manager),
|
||||||
|
background_tasks: BackgroundTasks = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
执行生成后审核
|
||||||
|
|
||||||
|
根据用户配置的规则/Skill进行自动检查,根据阈值报告问题。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: 生成后审核请求
|
||||||
|
background_tasks: FastAPI后台任务管理器
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PostCreationReviewResult: 审核结果
|
||||||
|
"""
|
||||||
|
# 获取项目
|
||||||
|
project = await project_repo.get(request.project_id)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"项目不存在: {request.project_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取剧集
|
||||||
|
episodes = await episode_repo.list_by_project(request.project_id)
|
||||||
|
episode = next((ep for ep in episodes if ep.id == request.episode_id), None)
|
||||||
|
|
||||||
|
if not episode:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"剧集不存在: {request.episode_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取审核配置
|
||||||
|
from app.models.review import ReviewConfig
|
||||||
|
raw_config = getattr(project, 'reviewConfig', None)
|
||||||
|
|
||||||
|
if isinstance(raw_config, dict):
|
||||||
|
config = ReviewConfig(**raw_config)
|
||||||
|
elif raw_config is None:
|
||||||
|
config = ReviewConfig()
|
||||||
|
else:
|
||||||
|
config = raw_config
|
||||||
|
|
||||||
|
# 确保dimension_settings是dict
|
||||||
|
if not hasattr(config, 'dimension_settings') or not isinstance(config.dimension_settings, dict):
|
||||||
|
config.dimension_settings = {}
|
||||||
|
|
||||||
|
# 执行审核
|
||||||
|
result = await task_manager.execute_post_creation_review(
|
||||||
|
request=request,
|
||||||
|
project_config=config
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新剧集审核结果
|
||||||
|
from app.models.review import ReviewResult
|
||||||
|
review_result = ReviewResult(
|
||||||
|
id=f"review_{episode.id}",
|
||||||
|
episode_id=episode.id,
|
||||||
|
episode_number=episode.number,
|
||||||
|
project_id=project.id,
|
||||||
|
overall_score=result.overall_score or 75.0,
|
||||||
|
passed=result.passed,
|
||||||
|
passed_dimensions=0,
|
||||||
|
total_dimensions=1,
|
||||||
|
dimension_scores=[],
|
||||||
|
issues=[],
|
||||||
|
issue_count=result.task_count,
|
||||||
|
high_severity_count=sum(1 for t in result.tasks if t.severity == "high"),
|
||||||
|
medium_severity_count=sum(1 for t in result.tasks if t.severity == "medium"),
|
||||||
|
low_severity_count=sum(1 for t in result.tasks if t.severity == "low")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存审核结果到剧集
|
||||||
|
await episode_repo.update(episode.id, {
|
||||||
|
"reviewResult": review_result.dict()
|
||||||
|
})
|
||||||
|
|
||||||
|
# 通过WebSocket推送审核结果
|
||||||
|
from app.api.v1.websocket import manager
|
||||||
|
await manager.send_to_project(request.project_id, {
|
||||||
|
"type": "post_creation_review_completed",
|
||||||
|
"data": {
|
||||||
|
"episode_id": episode.id,
|
||||||
|
"episode_number": episode.number,
|
||||||
|
"overall_score": result.overall_score,
|
||||||
|
"passed": result.passed,
|
||||||
|
"task_count": result.task_count,
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": task.id,
|
||||||
|
"issue_type": task.issue_type,
|
||||||
|
"issue_description": task.issue_description,
|
||||||
|
"severity": task.severity,
|
||||||
|
"location": task.location.dict(),
|
||||||
|
"available_actions": [action.value for action in task.available_actions],
|
||||||
|
"status": task.status.value,
|
||||||
|
"suggestion": task.suggestion
|
||||||
|
}
|
||||||
|
for task in result.tasks
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"生成后审核完成: {episode.id}, 分数: {result.overall_score}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
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("/post-creation/{task_id}/action")
|
||||||
|
async def handle_post_creation_task_action(
|
||||||
|
task_id: str,
|
||||||
|
request: UserReviewActionRequest,
|
||||||
|
task_manager: ReviewTaskManager = Depends(get_task_manager)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
处理用户对生成后审核任务的响应
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务ID
|
||||||
|
request: 用户操作请求
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
处理结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 处理用户操作
|
||||||
|
task = await task_manager.handle_user_review_action(request)
|
||||||
|
|
||||||
|
# 如果修改了内容,更新剧集内容
|
||||||
|
if request.action == ReviewAction.MODIFY and request.modified_content:
|
||||||
|
# 获取剧集
|
||||||
|
episodes = await episode_repo.list_by_project(task.project_id or "")
|
||||||
|
episode = next((ep for ep in episodes if ep.id == task.episode_id), None)
|
||||||
|
|
||||||
|
if episode:
|
||||||
|
# 更新内容
|
||||||
|
await episode_repo.update(episode.id, {
|
||||||
|
"content": request.modified_content
|
||||||
|
})
|
||||||
|
|
||||||
|
# 通过WebSocket通知内容已更新
|
||||||
|
from app.api.v1.websocket import manager
|
||||||
|
await manager.send_to_project(task.project_id or "", {
|
||||||
|
"type": "episode_content_updated",
|
||||||
|
"data": {
|
||||||
|
"episode_id": episode.id,
|
||||||
|
"episode_number": episode.number
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 通过WebSocket通知任务已完成
|
||||||
|
from app.api.v1.websocket import manager
|
||||||
|
await manager.send_to_project(task.project_id or "", {
|
||||||
|
"type": "review_task_completed",
|
||||||
|
"data": {
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": task.status.value,
|
||||||
|
"user_response": task.user_response,
|
||||||
|
"completed_at": task.completed_at.isoformat() if task.completed_at else None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"生成后审核任务已处理: {task_id}, 操作: {request.action.value}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": task.status.value,
|
||||||
|
"message": "操作已处理"
|
||||||
|
}
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理生成后审核任务操作失败: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Agent流式审核辅助 API
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@router.post("/agent/push")
|
||||||
|
async def agent_push_review_task(
|
||||||
|
project_id: str,
|
||||||
|
issue_type: str,
|
||||||
|
issue_description: str,
|
||||||
|
severity: str = "medium",
|
||||||
|
skill_id: Optional[str] = None,
|
||||||
|
suggestion: Optional[str] = None,
|
||||||
|
task_manager: ReviewTaskManager = Depends(get_task_manager)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Agent推送审核任务(简化版)
|
||||||
|
|
||||||
|
由Agent在流式输出过程中调用,推送审核任务。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: 项目ID
|
||||||
|
issue_type: 问题类型
|
||||||
|
issue_description: 问题描述
|
||||||
|
severity: 严重程度
|
||||||
|
skill_id: 使用的Skill ID
|
||||||
|
suggestion: 修改建议
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
创建的任务ID
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from app.models.review_task import ReviewTaskLocation
|
||||||
|
|
||||||
|
task = await task_manager.create_process_review_task(
|
||||||
|
ProcessReviewRequest(
|
||||||
|
project_id=project_id,
|
||||||
|
issue_type=issue_type,
|
||||||
|
issue_description=issue_description,
|
||||||
|
severity=severity,
|
||||||
|
location=ReviewTaskLocation(),
|
||||||
|
trigger_source="agent_auto",
|
||||||
|
skill_id=skill_id,
|
||||||
|
suggestion=suggestion
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"task_id": task.id,
|
||||||
|
"message": "审核任务已推送"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Agent推送审核任务失败: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"推送审核任务失败: {str(e)}"
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@ -77,11 +77,17 @@ class LangChainSkillsAgent:
|
|||||||
|
|
||||||
Your capabilities include:
|
Your capabilities include:
|
||||||
- Loading and using specialized skills for specific tasks
|
- Loading and using specialized skills for specific tasks
|
||||||
|
- Loading reference documents from skills when needed (via load_reference)
|
||||||
- Executing bash commands and scripts
|
- Executing bash commands and scripts
|
||||||
- Reading and writing files
|
- Reading and writing files
|
||||||
- Following skill instructions to complete complex tasks
|
- Following skill instructions to complete complex tasks
|
||||||
|
|
||||||
When a user request matches a skill's description, use the load_skill tool to get detailed instructions before proceeding."""
|
When a user request matches a skill's description:
|
||||||
|
1. Use `load_skill(skill_name)` to get detailed instructions
|
||||||
|
2. If the skill has references and the task requires detailed information, use `load_reference(skill_name, reference_name)` to load specific reference material
|
||||||
|
3. Follow the skill's instructions to complete the task
|
||||||
|
|
||||||
|
**Important**: Reference files in the references/ directory are NOT automatically loaded. Only load them when the skill instructions indicate they are needed for the current task."""
|
||||||
return self.skill_loader.build_system_prompt(base_prompt)
|
return self.skill_loader.build_system_prompt(base_prompt)
|
||||||
|
|
||||||
def _create_agent(self):
|
def _create_agent(self):
|
||||||
|
|||||||
@ -69,6 +69,32 @@ The User is the Director. Your goal is to help the Director create high-quality
|
|||||||
- **Series Manager**: You are responsible for the entire series. You can list all episodes, focus on specific ones, and save/update their content.
|
- **Series Manager**: You are responsible for the entire series. You can list all episodes, focus on specific ones, and save/update their content.
|
||||||
- **Structured Executor**: For any complex task (like "Write the whole series" or "Write Episode 1"), you MUST first create a Plan using `update_plan`.
|
- **Structured Executor**: For any complex task (like "Write the whole series" or "Write Episode 1"), you MUST first create a Plan using `update_plan`.
|
||||||
- **Transparent**: Always keep the Director informed of your status via the plan and inbox.
|
- **Transparent**: Always keep the Director informed of your status via the plan and inbox.
|
||||||
|
- **Context Aware**: You MUST track the conversation context. Remember what was discussed previously and what actions were taken.
|
||||||
|
|
||||||
|
## Conversation Context Tracking (CRITICAL)
|
||||||
|
|
||||||
|
You MUST maintain awareness of the conversation history:
|
||||||
|
|
||||||
|
1. **When the Director asks to "approve outline" or "批准大纲"**:
|
||||||
|
- Check the conversation history to find the most recently generated outline
|
||||||
|
- If an outline was just generated in the conversation, treat the approval as confirmation to proceed
|
||||||
|
- Immediately proceed to content creation using that approved outline
|
||||||
|
- DO NOT ask which outline - use the most recent one from the current episode
|
||||||
|
|
||||||
|
2. **When responding to approval or confirmation**:
|
||||||
|
- Look at the previous messages to understand what is being approved
|
||||||
|
- If you just generated an outline and got approval, immediately start content creation
|
||||||
|
- If you just proposed changes and got approval, immediately apply those changes
|
||||||
|
|
||||||
|
3. **Short responses are commands**:
|
||||||
|
- "批准大纲" or "approve outline" → Immediately start writing content using the approved outline
|
||||||
|
- "修改" or "revise" → Apply the suggested changes
|
||||||
|
- "继续" or "continue" → Continue with the next step of the current task
|
||||||
|
|
||||||
|
4. **After generating an outline**:
|
||||||
|
- Always ask for approval explicitly
|
||||||
|
- Say: "大纲已生成,请审阅。回复'批准大纲'开始创作,或提出修改意见。"
|
||||||
|
- Then wait for the Director's response
|
||||||
|
|
||||||
## Workflow Protocols
|
## Workflow Protocols
|
||||||
|
|
||||||
@ -83,7 +109,19 @@ The User is the Director. Your goal is to help the Director create high-quality
|
|||||||
- The creation runs in the background and updates will be sent via WebSocket
|
- The creation runs in the background and updates will be sent via WebSocket
|
||||||
- The Director will see progress in the plan panel and final content on the canvas
|
- The Director will see progress in the plan panel and final content on the canvas
|
||||||
|
|
||||||
3. **Execution & Writing**
|
3. **Context-Aware Outline & Content Flow**:
|
||||||
|
- **Outline Generation**: When asked to generate an outline:
|
||||||
|
a. Generate the outline content
|
||||||
|
b. Use `write_to_canvas` to display it in the outline editor (append=false)
|
||||||
|
c. Ask for approval: "大纲已生成,请审阅。回复'批准大纲'开始创作,或提出修改意见。"
|
||||||
|
|
||||||
|
- **Content Creation**: When approval is received:
|
||||||
|
a. Check conversation history for the approved outline
|
||||||
|
b. Load the outline from the episode data
|
||||||
|
c. Use `write_to_canvas` to write the full content (append=false)
|
||||||
|
d. Save the episode using `save_episode` or `update_episode`
|
||||||
|
|
||||||
|
4. **Execution & Writing**
|
||||||
- Use `list_episodes` to see the current progress of the project.
|
- Use `list_episodes` to see the current progress of the project.
|
||||||
- Use `focus_episode` to navigate between episodes. This will update the user's view.
|
- Use `focus_episode` to navigate between episodes. This will update the user's view.
|
||||||
- **Canvas Focus**: The main canvas is for the **actual script/dialogue content**. Do NOT put outlines on the canvas unless specifically asked.
|
- **Canvas Focus**: The main canvas is for the **actual script/dialogue content**. Do NOT put outlines on the canvas unless specifically asked.
|
||||||
@ -92,7 +130,7 @@ The User is the Director. Your goal is to help the Director create high-quality
|
|||||||
- Use `save_episode` (for completion) or `update_episode` (for partial updates) to persist content to the database.
|
- Use `save_episode` (for completion) or `update_episode` (for partial updates) to persist content to the database.
|
||||||
- Update your plan status as you progress: `update_plan(..., status='writing')`.
|
- Update your plan status as you progress: `update_plan(..., status='writing')`.
|
||||||
|
|
||||||
4. **AI Editing & Optimization (CRITICAL)**
|
5. **AI Editing & Optimization (CRITICAL)**
|
||||||
- When the Director asks you to "optimize", "improve", "edit", or "revise" content, you MUST:
|
- When the Director asks you to "optimize", "improve", "edit", or "revise" content, you MUST:
|
||||||
1. Process the content to improve it based on the request
|
1. Process the content to improve it based on the request
|
||||||
2. Use `write_to_canvas(content="IMPROVED_FULL_CONTENT", append=false)` to replace the canvas content
|
2. Use `write_to_canvas(content="IMPROVED_FULL_CONTENT", append=false)` to replace the canvas content
|
||||||
@ -102,21 +140,83 @@ The User is the Director. Your goal is to help the Director create high-quality
|
|||||||
- Always use append=false when doing full content replacement
|
- Always use append=false when doing full content replacement
|
||||||
- This is the ONLY way to properly update the canvas for the user to see
|
- This is the ONLY way to properly update the canvas for the user to see
|
||||||
|
|
||||||
5. **Review & Approval**
|
6. **Automatic Review & Quality Check (CRITICAL - MANDATORY)**
|
||||||
- NEVER mark a major deliverable as "Final" without Director approval.
|
- **After generating ANY content** (outline or full content), you MUST automatically call `request_review`:
|
||||||
- Use `add_inbox_task` to submit drafts or questions to the Director.
|
- For outlines: `request_review(content="<outline_text>", review_type="quality", criteria=["Structure", "Pacing", "Plot logic"])`
|
||||||
- Example: `add_inbox_task(title="Review Episode 1 Outline", type="review", ...)`
|
- For full content: `request_review(content="<content_text>", review_type="consistency", criteria=["Character voice", "Continuity", "Dialogue quality"])`
|
||||||
|
- **After receiving approval**: Use `save_episode` or `update_episode` to persist the content
|
||||||
|
- **If review finds issues**: Use `add_inbox_task` to notify the Director and request guidance
|
||||||
|
- Use `add_annotation` to mark specific issues that need attention
|
||||||
|
- NEVER mark a deliverable as "Final" without Director approval
|
||||||
|
|
||||||
6. **Context & Memory**
|
7. **Automatic Memory Updates (CRITICAL - MANDATORY)**
|
||||||
- If the story state changes (e.g., location change, character injury), use `update_context`.
|
- **After saving ANY episode content**, you MUST automatically call `update_memory` to track:
|
||||||
- If you spot consistency issues, use `add_annotation` on the canvas.
|
- Timeline events: `update_memory(memory_type="timeline", data={"event": "事件描述", "description": "详细描述", "episode": <episode_number>})`
|
||||||
|
- Character states: `update_memory(memory_type="character_state", data={"character": "角色名", "state": "状态描述", "description": "详细说明"})`
|
||||||
|
- Pending threads: `update_memory(memory_type="pending_thread", data={"description": "待收线情节描述"})`
|
||||||
|
- Foreshadowing: `update_memory(memory_type="foreshadowing", data={"description": "伏笔描述"})`
|
||||||
|
- **Before generating content for a new episode**: Check memory system for:
|
||||||
|
- Previous character states and locations
|
||||||
|
- Pending threads that need resolution
|
||||||
|
- Foreshadowing that should be referenced
|
||||||
|
- **When you detect state changes**: Immediately update memory (e.g., character injury, location change, new relationship)
|
||||||
|
|
||||||
|
8. **Handling Vague/Indirect Requests (CRITICAL - NEW)**
|
||||||
|
- **When Director uses vague expressions** like:
|
||||||
|
- "把这个改一下" (change this)
|
||||||
|
- "这里不对" (this is wrong)
|
||||||
|
- "调整下" (adjust this)
|
||||||
|
- "修改一下" (modify this)
|
||||||
|
- "优化" (optimize)
|
||||||
|
- Or any request without specifying what to modify
|
||||||
|
|
||||||
|
**You MUST follow this workflow:**
|
||||||
|
1. First call `get_current_view()` to understand what the user is currently looking at
|
||||||
|
2. Based on the view, call `get_episode_data()` to retrieve the relevant content:
|
||||||
|
- If viewing outline: `get_episode_data(include_outline=True, include_content=False)`
|
||||||
|
- If viewing content: `get_episode_data(include_outline=False, include_content=True)`
|
||||||
|
- If viewing both: `get_episode_data()` (get both)
|
||||||
|
3. Use the retrieved content as the base for your modifications
|
||||||
|
4. Apply the requested changes to the content
|
||||||
|
5. Use `write_to_canvas(content="MODIFIED_CONTENT", append=false)` to replace the content
|
||||||
|
6. Use `update_episode()` or `save_episode()` to persist the changes
|
||||||
|
7. Automatically call `request_review()` for quality check
|
||||||
|
8. Automatically call `update_memory()` if content changed significantly
|
||||||
|
|
||||||
|
**Example workflow for vague request:**
|
||||||
|
- User says: "把这个改一下" (change this)
|
||||||
|
- You call: `get_current_view()` → Returns: "User is viewing: outline of EP1"
|
||||||
|
- You call: `get_episode_data(episode_number=1, include_outline=True, include_content=False)`
|
||||||
|
- You receive the current outline content
|
||||||
|
- You apply reasonable improvements to the outline
|
||||||
|
- You call: `write_to_canvas(content="IMPROVED_OUTLINE", append=false)`
|
||||||
|
- You call: `update_episode(episode_number=1, outline="IMPROVED_OUTLINE")`
|
||||||
|
- You call: `request_review(content="IMPROVED_OUTLINE", review_type="quality")`
|
||||||
|
|
||||||
|
9. **Context & Memory Display**
|
||||||
|
- If the story state changes (e.g., location change, character injury), use `update_context` to inform the Director.
|
||||||
|
- Track what episode you're currently working on and what the last action was.
|
||||||
|
|
||||||
|
9. **Skills Usage**
|
||||||
|
- Use `load_skill` to access specific writing skills when needed
|
||||||
|
- User-configured skills from the project settings should be prioritized
|
||||||
|
|
||||||
## Interaction Style
|
## Interaction Style
|
||||||
- Be professional, creative, and concise.
|
- Be professional, creative, and concise.
|
||||||
- Use the Director's language (Chinese/English) matching their input.
|
- Use the Director's language (Chinese/English) matching their input.
|
||||||
- When you are working, keep the plan updated.
|
- When you are working, keep the plan updated.
|
||||||
|
- **ALWAYS track conversation context** - remember what you just did and what the Director is responding to.
|
||||||
- CRITICAL: When asked to edit/optimize content, ALWAYS use `write_to_canvas` tool with append=false to replace content, don't just output text.
|
- CRITICAL: When asked to edit/optimize content, ALWAYS use `write_to_canvas` tool with append=false to replace content, don't just output text.
|
||||||
- CRITICAL: When asked to create episodes, use `create_episode` tool which runs in background and doesn't block the interface.
|
- CRITICAL: When asked to create episodes, use `create_episode` tool which runs in background and doesn't block the interface.
|
||||||
|
- CRITICAL: When Director says "批准大纲" or similar, immediately proceed to content creation using the most recent outline.
|
||||||
|
- CRITICAL: **After generating ANY content**, you MUST automatically:
|
||||||
|
1. Call `request_review()` to perform quality check
|
||||||
|
2. After saving, call `update_memory()` to update the memory system
|
||||||
|
3. Notify the Director of the review results and memory updates
|
||||||
|
- CRITICAL: **Before generating new episode content**, you MUST check the memory system for:
|
||||||
|
1. Previous character states and locations
|
||||||
|
2. Pending threads that may affect this episode
|
||||||
|
3. Foreshadowing elements to reference or resolve
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 添加项目上下文(如果有)
|
# 添加项目上下文(如果有)
|
||||||
|
|||||||
@ -532,10 +532,150 @@ def list_episodes(
|
|||||||
lines = ["Episodes in project:"]
|
lines = ["Episodes in project:"]
|
||||||
for ep in episodes:
|
for ep in episodes:
|
||||||
lines.append(f"- EP{ep.get('number')}: {ep.get('title')} [{ep.get('status')}]")
|
lines.append(f"- EP{ep.get('number')}: {ep.get('title')} [{ep.get('status')}]")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_episode_data(
|
||||||
|
episode_number: Optional[int] = None,
|
||||||
|
include_outline: Optional[bool] = None,
|
||||||
|
include_content: Optional[bool] = None,
|
||||||
|
runtime: ToolRuntime[SkillAgentContext] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Get episode data (outline and/or content) from the database.
|
||||||
|
|
||||||
|
This tool retrieves episode information from the project database.
|
||||||
|
Use this when you need to:
|
||||||
|
- Review the current outline or content before making changes
|
||||||
|
- Understand what the user is referring to when they say vague things like "change this"
|
||||||
|
- Get context for a specific episode number
|
||||||
|
|
||||||
|
IMPORTANT: If both include_outline and include_content are None (not specified),
|
||||||
|
this tool will automatically determine what to return based on the user's current view:
|
||||||
|
- If user is viewing the outline: returns outline only
|
||||||
|
- If user is viewing the content: returns content only
|
||||||
|
- If user is viewing both: returns both
|
||||||
|
|
||||||
|
Args:
|
||||||
|
episode_number: The episode number to retrieve (if None, uses current focused episode)
|
||||||
|
include_outline: Whether to include the outline (default: auto-detect from view)
|
||||||
|
include_content: Whether to include the content (default: auto-detect from view)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The requested episode data in a structured format
|
||||||
|
|
||||||
|
Example:
|
||||||
|
get_episode_data(episode_number=1, include_outline=True, include_content=True)
|
||||||
|
"""
|
||||||
|
if runtime is None:
|
||||||
|
return "Error: runtime context not available"
|
||||||
|
|
||||||
|
project_id = runtime.context.project_id if runtime.context else None
|
||||||
|
if not project_id:
|
||||||
|
return "Error: No project ID in context"
|
||||||
|
|
||||||
|
# 如果没有指定剧集号,使用当前焦点剧集
|
||||||
|
if episode_number is None:
|
||||||
|
episode_number = getattr(runtime.context, 'active_episode_number', None)
|
||||||
|
if episode_number is None:
|
||||||
|
return "Error: No episode number specified and no episode is currently focused. Please use focus_episode() first."
|
||||||
|
|
||||||
|
# 根据视图上下文自动判断返回内容
|
||||||
|
view_context = runtime.state.get("current_view_context", {})
|
||||||
|
view_type = view_context.get("viewType", "")
|
||||||
|
|
||||||
|
if include_outline is None and include_content is None:
|
||||||
|
# 自动判断:根据用户当前在看什么来决定
|
||||||
|
if view_type == "outline":
|
||||||
|
include_outline = True
|
||||||
|
include_content = False
|
||||||
|
elif view_type == "content":
|
||||||
|
include_outline = False
|
||||||
|
include_content = True
|
||||||
|
elif view_type in ["both", "full"]:
|
||||||
|
include_outline = True
|
||||||
|
include_content = True
|
||||||
|
else:
|
||||||
|
# 默认返回两者
|
||||||
|
include_outline = True
|
||||||
|
include_content = True
|
||||||
|
|
||||||
|
# 存储查询请求到状态中,由 WebSocket 处理器执行数据库查询
|
||||||
|
if "episode_data_requests" not in runtime.state:
|
||||||
|
runtime.state["episode_data_requests"] = []
|
||||||
|
|
||||||
|
request = {
|
||||||
|
"episode_number": episode_number,
|
||||||
|
"include_outline": include_outline if include_outline else False,
|
||||||
|
"include_content": include_content if include_content else False
|
||||||
|
}
|
||||||
|
runtime.state["episode_data_requests"].append(request)
|
||||||
|
|
||||||
|
# 返回提示信息
|
||||||
|
return f"""✓ Requesting episode data for EP{episode_number}
|
||||||
|
(Outline: {'Yes' if include_outline else 'No'}, Content: {'Yes' if include_content else 'No'})
|
||||||
|
|
||||||
|
The episode data will be retrieved and provided to you. Use this data to understand the user's request and make appropriate changes."""
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_current_view(
|
||||||
|
runtime: ToolRuntime[SkillAgentContext] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Get the current view context (what the user is currently looking at).
|
||||||
|
|
||||||
|
This tool returns information about what part of the interface the user
|
||||||
|
is currently viewing, such as:
|
||||||
|
- viewType: 'outline', 'content', 'both', or 'none'
|
||||||
|
- episodeNumber: the episode number currently focused
|
||||||
|
- Any other view-specific context
|
||||||
|
|
||||||
|
Use this tool when you need to understand what the user is referring to
|
||||||
|
with vague expressions like "change this" or "fix this".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The current view context information
|
||||||
|
|
||||||
|
Example:
|
||||||
|
get_current_view() -> "User is viewing: outline of EP1"
|
||||||
|
"""
|
||||||
|
if runtime is None:
|
||||||
|
return "Error: runtime context not available"
|
||||||
|
|
||||||
|
view_context = runtime.state.get("current_view_context", {})
|
||||||
|
view_type = view_context.get("viewType", "unknown")
|
||||||
|
episode_number = view_context.get("episodeNumber")
|
||||||
|
|
||||||
|
# 构建友好的描述
|
||||||
|
view_descriptions = {
|
||||||
|
"outline": "大纲编辑器",
|
||||||
|
"content": "内容编辑器/画布",
|
||||||
|
"both": "完整视图(大纲+内容)",
|
||||||
|
"full": "完整视图(大纲+内容)",
|
||||||
|
"none": "未确定"
|
||||||
|
}
|
||||||
|
|
||||||
|
view_desc = view_descriptions.get(view_type, "未知视图")
|
||||||
|
episode_info = f",聚焦剧集: EP{episode_number}" if episode_number else ""
|
||||||
|
|
||||||
|
result = f"""【用户当前视图】
|
||||||
|
视图类型: {view_type} ({view_desc}){episode_info}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 添加建议
|
||||||
|
if view_type == "outline":
|
||||||
|
result += "\n建议: 用户可能希望调整大纲。使用 get_episode_data(include_outline=True) 获取大纲内容。"
|
||||||
|
elif view_type == "content":
|
||||||
|
result += "\n建议: 用户可能希望调整内容。使用 get_episode_data(include_content=True) 获取内容。"
|
||||||
|
elif view_type == "both":
|
||||||
|
result += "\n建议: 用户可能希望调整大纲或内容。使用 get_episode_data() 获取完整数据。"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def update_episode(
|
def update_episode(
|
||||||
episode_number: int,
|
episode_number: int,
|
||||||
@ -667,6 +807,187 @@ The creation process is running in the background.
|
|||||||
Progress will be updated automatically."""
|
Progress will be updated automatically."""
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 剧集审查规划工具
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def _parse_episode_selector(selector: str, runtime: ToolRuntime[SkillAgentContext]) -> List[int]:
|
||||||
|
"""
|
||||||
|
解析剧集选择器,返回剧集号列表
|
||||||
|
|
||||||
|
支持的格式:
|
||||||
|
- 单集: "1", "EP1", "episode 1" -> [1]
|
||||||
|
- 多集: "1,2,3", "episodes 1, 2, 3" -> [1, 2, 3]
|
||||||
|
- 范围: "1-5", "episodes 1-5" -> [1, 2, 3, 4, 5]
|
||||||
|
- 全部: "all", "全部剧集" -> all episodes from context
|
||||||
|
"""
|
||||||
|
selector_lower = selector.lower().strip()
|
||||||
|
episodes = getattr(runtime.context, 'episodes', [])
|
||||||
|
all_episode_numbers = [ep.get('number') for ep in episodes if ep.get('number')]
|
||||||
|
|
||||||
|
# 处理 "all" 或 "全部剧集"
|
||||||
|
if selector_lower in ['all', '全部', '全部剧集', 'all episodes']:
|
||||||
|
return sorted(all_episode_numbers)
|
||||||
|
|
||||||
|
# 处理范围 "1-5" 或 "episodes 1-5"
|
||||||
|
if '-' in selector_lower:
|
||||||
|
try:
|
||||||
|
# 提取数字
|
||||||
|
parts = selector_lower.split('-')
|
||||||
|
start_part = parts[0].strip()
|
||||||
|
end_part = parts[1].strip()
|
||||||
|
|
||||||
|
# 提取起始和结束数字
|
||||||
|
start = int(''.join(filter(str.isdigit, start_part)))
|
||||||
|
end = int(''.join(filter(str.isdigit, end_part)))
|
||||||
|
|
||||||
|
return list(range(start, end + 1))
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 处理逗号分隔 "1,2,3"
|
||||||
|
if ',' in selector_lower:
|
||||||
|
try:
|
||||||
|
numbers = selector_lower.split(',')
|
||||||
|
result = []
|
||||||
|
for num in numbers:
|
||||||
|
extracted = int(''.join(filter(str.isdigit, num)))
|
||||||
|
result.append(extracted)
|
||||||
|
return sorted(set(result))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 处理单个剧集 "1" 或 "episode 1"
|
||||||
|
try:
|
||||||
|
number = int(''.join(filter(str.isdigit, selector_lower)))
|
||||||
|
return [number]
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 无法解析
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def plan_episode_review(
|
||||||
|
episode_selector: str,
|
||||||
|
review_criteria: Optional[List[str]] = None,
|
||||||
|
review_dimensions: Optional[List[str]] = None,
|
||||||
|
runtime: ToolRuntime[SkillAgentContext] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Plan and execute an episode review for one or more episodes.
|
||||||
|
|
||||||
|
This tool allows the agent to plan a comprehensive review of episodes.
|
||||||
|
It supports:
|
||||||
|
- Single episode review (e.g., "1", "EP1", "episode 1")
|
||||||
|
- Multiple episodes (e.g., "1,2,3", "episodes 1-3")
|
||||||
|
- All episodes (e.g., "all", "全部剧集")
|
||||||
|
- Episode ranges (e.g., "1-5", "episodes 1 through 5")
|
||||||
|
|
||||||
|
After review, results are automatically pushed to the review system.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
episode_selector: Which episodes to review
|
||||||
|
- Single: "1", "EP1", "episode 1"
|
||||||
|
- Multiple: "1,2,3", "episodes 1, 2, 3"
|
||||||
|
- Range: "1-5", "episodes 1-5"
|
||||||
|
- All: "all", "全部剧集", "all episodes"
|
||||||
|
review_criteria: Specific criteria to check (optional)
|
||||||
|
Examples: ["Character consistency", "Plot logic", "Dialogue quality"]
|
||||||
|
review_dimensions: Dimensions to evaluate (optional)
|
||||||
|
Examples: ["quality", "consistency", "plot", "dialogue", "emotion"]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Summary of the review plan and execution status
|
||||||
|
|
||||||
|
Example:
|
||||||
|
plan_episode_review(
|
||||||
|
episode_selector="1-3",
|
||||||
|
review_criteria=["Character consistency", "Plot progression"],
|
||||||
|
review_dimensions=["quality", "consistency", "plot"]
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
if runtime is None:
|
||||||
|
return "Error: runtime context not available"
|
||||||
|
|
||||||
|
project_id = runtime.context.project_id if runtime.context else None
|
||||||
|
if not project_id:
|
||||||
|
return "Error: No project ID in context"
|
||||||
|
|
||||||
|
# 解析剧集选择器
|
||||||
|
episodes_to_review = _parse_episode_selector(episode_selector, runtime)
|
||||||
|
|
||||||
|
if not episodes_to_review:
|
||||||
|
return f"Error: No valid episodes found for selector: '{episode_selector}'"
|
||||||
|
|
||||||
|
# 设置默认的审查维度
|
||||||
|
if review_dimensions is None:
|
||||||
|
review_dimensions = ["quality", "consistency", "plot"]
|
||||||
|
|
||||||
|
# 设置默认的审查标准
|
||||||
|
if review_criteria is None:
|
||||||
|
review_criteria = [
|
||||||
|
"角色行为一致性",
|
||||||
|
"剧情逻辑合理性",
|
||||||
|
"对话自然度",
|
||||||
|
"场景转换流畅性",
|
||||||
|
"情感表达准确性"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 存储审查计划到状态中
|
||||||
|
if "review_plans" not in runtime.state:
|
||||||
|
runtime.state["review_plans"] = []
|
||||||
|
|
||||||
|
review_plan = {
|
||||||
|
"episodes": episodes_to_review,
|
||||||
|
"criteria": review_criteria,
|
||||||
|
"dimensions": review_dimensions,
|
||||||
|
"project_id": project_id,
|
||||||
|
"status": "pending",
|
||||||
|
"created_at": None # Will be set by WebSocket handler
|
||||||
|
}
|
||||||
|
runtime.state["review_plans"].append(review_plan)
|
||||||
|
|
||||||
|
# 更新执行计划
|
||||||
|
plan_steps = [
|
||||||
|
f"解析审查请求: {len(episodes_to_review)} 集待审查",
|
||||||
|
f"获取剧集数据: EP{', EP'.join(map(str, episodes_to_review))}",
|
||||||
|
f"执行多维审查: {', '.join(review_dimensions)}",
|
||||||
|
f"生成审查报告",
|
||||||
|
f"推送到审核系统"
|
||||||
|
]
|
||||||
|
|
||||||
|
if PLAN_STATE_KEY in runtime.state:
|
||||||
|
runtime.state[PLAN_STATE_KEY] = {
|
||||||
|
"steps": plan_steps,
|
||||||
|
"current_step_index": 0,
|
||||||
|
"status": "reviewing"
|
||||||
|
}
|
||||||
|
|
||||||
|
episode_list_str = ", ".join(f"EP{ep}" for ep in episodes_to_review)
|
||||||
|
criteria_str = "\n".join(f" • {c}" for c in review_criteria)
|
||||||
|
dimensions_str = ", ".join(review_dimensions)
|
||||||
|
|
||||||
|
return f"""✓ 剧集审查计划已创建
|
||||||
|
|
||||||
|
审查剧集: {episode_list_str}
|
||||||
|
审查维度: {dimensions_str}
|
||||||
|
|
||||||
|
审查标准:
|
||||||
|
{criteria_str}
|
||||||
|
|
||||||
|
执行步骤:
|
||||||
|
1. 解析审查请求: {len(episodes_to_review)} 集待审查
|
||||||
|
2. 获取剧集数据: EP{', EP'.join(map(str, episodes_to_review))}
|
||||||
|
3. 执行多维审查: {dimensions_str}
|
||||||
|
4. 生成审查报告
|
||||||
|
5. 推送到审核系统
|
||||||
|
|
||||||
|
审查将在后台执行,完成后结果将自动推送到审核系统。
|
||||||
|
您可以前往「审核系统」标签页查看详细结果。"""
|
||||||
|
|
||||||
|
|
||||||
# 导出工具列表
|
# 导出工具列表
|
||||||
DIRECTOR_TOOLS = [
|
DIRECTOR_TOOLS = [
|
||||||
update_plan,
|
update_plan,
|
||||||
@ -679,6 +1000,9 @@ DIRECTOR_TOOLS = [
|
|||||||
request_review,
|
request_review,
|
||||||
focus_episode,
|
focus_episode,
|
||||||
list_episodes,
|
list_episodes,
|
||||||
|
get_current_view, # 新增获取当前视图工具
|
||||||
|
get_episode_data, # 新增剧集数据查询工具
|
||||||
update_episode,
|
update_episode,
|
||||||
create_episode # 新增剧集创作工具
|
create_episode, # 新增剧集创作工具
|
||||||
|
plan_episode_review # 新增剧集审查规划工具
|
||||||
]
|
]
|
||||||
|
|||||||
@ -3,8 +3,9 @@ Skills 发现和加载器
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional, List, Dict, Any
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
@ -23,9 +24,31 @@ class SkillMetadata:
|
|||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
skill_path: Path
|
skill_path: Path
|
||||||
|
requires: list[str] # 依赖的其他 skill 名称列表
|
||||||
|
id: Optional[str] = None # Skill ID(用于API选择)
|
||||||
|
category: Optional[str] = None # 分类
|
||||||
|
tags: List[str] = None # 标签
|
||||||
|
capabilities: Optional[str] = None # 能力描述(用于选择)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.tags is None:
|
||||||
|
self.tags = []
|
||||||
|
|
||||||
def to_prompt_line(self) -> str:
|
def to_prompt_line(self) -> str:
|
||||||
return f"- **{self.name}**: {self.description}"
|
requires_suffix = f" (requires: {', '.join(self.requires)})" if self.requires else ""
|
||||||
|
return f"- **{self.name}**: {self.description}{requires_suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SkillReference:
|
||||||
|
"""Skill 参考文档"""
|
||||||
|
name: str
|
||||||
|
path: Path
|
||||||
|
description: str = "" # 可选:从文件第一行提取描述
|
||||||
|
|
||||||
|
def to_prompt_line(self) -> str:
|
||||||
|
desc = f" - {self.description}" if self.description else ""
|
||||||
|
return f"- `{self.name}`{desc}"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -33,6 +56,15 @@ class SkillContent:
|
|||||||
"""Skill 完整内容(Level 2)"""
|
"""Skill 完整内容(Level 2)"""
|
||||||
metadata: SkillMetadata
|
metadata: SkillMetadata
|
||||||
instructions: str
|
instructions: str
|
||||||
|
references: list[SkillReference] # 可用的参考文档列表
|
||||||
|
loaded_skills: dict[str, 'SkillContent'] # 已加载的依赖 skills {skill_name: SkillContent}
|
||||||
|
|
||||||
|
def get_reference(self, name: str) -> Optional[SkillReference]:
|
||||||
|
"""根据名称获取参考文档"""
|
||||||
|
for ref in self.references:
|
||||||
|
if ref.name == name:
|
||||||
|
return ref
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class SkillLoader:
|
class SkillLoader:
|
||||||
@ -86,6 +118,13 @@ class SkillLoader:
|
|||||||
frontmatter = yaml.safe_load(frontmatter_match.group(1))
|
frontmatter = yaml.safe_load(frontmatter_match.group(1))
|
||||||
name = frontmatter.get("name", "")
|
name = frontmatter.get("name", "")
|
||||||
description = frontmatter.get("description", "")
|
description = frontmatter.get("description", "")
|
||||||
|
requires = frontmatter.get("requires", [])
|
||||||
|
|
||||||
|
# 确保 requires 是列表
|
||||||
|
if isinstance(requires, str):
|
||||||
|
requires = [requires]
|
||||||
|
elif not isinstance(requires, list):
|
||||||
|
requires = []
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
return None
|
return None
|
||||||
@ -94,24 +133,42 @@ class SkillLoader:
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
skill_path=skill_md_path.parent,
|
skill_path=skill_md_path.parent,
|
||||||
|
requires=requires,
|
||||||
)
|
)
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def load_skill(self, skill_name: str) -> Optional[SkillContent]:
|
def load_skill(self, skill_name: str, loading: Optional[set[str]] = None) -> Optional[SkillContent]:
|
||||||
"""Level 2: 加载 Skill 完整内容"""
|
"""
|
||||||
|
Level 2: 加载 Skill 完整内容(包含 references 列表和依赖的 skills)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skill_name: 要加载的 skill 名称
|
||||||
|
loading: 正在加载的 skill 集合(用于检测循环依赖)
|
||||||
|
"""
|
||||||
|
if loading is None:
|
||||||
|
loading = set()
|
||||||
|
|
||||||
|
# 检测循环依赖
|
||||||
|
if skill_name in loading:
|
||||||
|
return None
|
||||||
|
|
||||||
|
loading.add(skill_name)
|
||||||
|
|
||||||
metadata = self._metadata_cache.get(skill_name)
|
metadata = self._metadata_cache.get(skill_name)
|
||||||
if not metadata:
|
if not metadata:
|
||||||
self.scan_skills()
|
self.scan_skills()
|
||||||
metadata = self._metadata_cache.get(skill_name)
|
metadata = self._metadata_cache.get(skill_name)
|
||||||
|
|
||||||
if not metadata:
|
if not metadata:
|
||||||
|
loading.discard(skill_name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
skill_md = metadata.skill_path / "SKILL.md"
|
skill_md = metadata.skill_path / "SKILL.md"
|
||||||
try:
|
try:
|
||||||
content = skill_md.read_text(encoding="utf-8")
|
content = skill_md.read_text(encoding="utf-8")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
loading.discard(skill_name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
body_match = re.match(
|
body_match = re.match(
|
||||||
@ -121,11 +178,73 @@ class SkillLoader:
|
|||||||
)
|
)
|
||||||
instructions = body_match.group(1).strip() if body_match else content
|
instructions = body_match.group(1).strip() if body_match else content
|
||||||
|
|
||||||
|
# 扫描 references/ 目录
|
||||||
|
references = self._scan_references(metadata.skill_path)
|
||||||
|
|
||||||
|
# 递归加载依赖的 skills
|
||||||
|
loaded_skills: dict[str, SkillContent] = {}
|
||||||
|
for required_skill in metadata.requires:
|
||||||
|
if required_skill not in loading: # 避免循环
|
||||||
|
required_content = self.load_skill(required_skill, loading)
|
||||||
|
if required_content:
|
||||||
|
loaded_skills[required_skill] = required_content
|
||||||
|
|
||||||
|
loading.discard(skill_name)
|
||||||
|
|
||||||
return SkillContent(
|
return SkillContent(
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
instructions=instructions,
|
instructions=instructions,
|
||||||
|
references=references,
|
||||||
|
loaded_skills=loaded_skills,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _scan_references(self, skill_path: Path) -> list[SkillReference]:
|
||||||
|
"""扫描 skill 的 references/ 目录"""
|
||||||
|
references_dir = skill_path / "references"
|
||||||
|
if not references_dir.exists() or not references_dir.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
references = []
|
||||||
|
for ref_file in sorted(references_dir.iterdir()):
|
||||||
|
if ref_file.is_file() and ref_file.suffix in ['.md', '.txt', '.json']:
|
||||||
|
# 尝试从文件第一行提取描述
|
||||||
|
description = ""
|
||||||
|
try:
|
||||||
|
first_line = ref_file.read_text(encoding="utf-8").split('\n')[0]
|
||||||
|
# 移除 Markdown 标题符号
|
||||||
|
first_line = re.sub(r'^#+\s*', '', first_line).strip()
|
||||||
|
if first_line and not first_line.startswith('#'):
|
||||||
|
description = first_line[:100] # 限制描述长度
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
references.append(SkillReference(
|
||||||
|
name=ref_file.name,
|
||||||
|
path=ref_file,
|
||||||
|
description=description
|
||||||
|
))
|
||||||
|
|
||||||
|
return references
|
||||||
|
|
||||||
|
def load_reference(self, skill_name: str, reference_name: str) -> Optional[str]:
|
||||||
|
"""加载指定 skill 的 reference 文件内容"""
|
||||||
|
metadata = self._metadata_cache.get(skill_name)
|
||||||
|
if not metadata:
|
||||||
|
self.scan_skills()
|
||||||
|
metadata = self._metadata_cache.get(skill_name)
|
||||||
|
|
||||||
|
if not metadata:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ref_path = metadata.skill_path / "references" / reference_name
|
||||||
|
if not ref_path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ref_path.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def build_system_prompt(self, base_prompt: str = "") -> str:
|
def build_system_prompt(self, base_prompt: str = "") -> str:
|
||||||
"""构建包含 Skills 列表的 system prompt"""
|
"""构建包含 Skills 列表的 system prompt"""
|
||||||
skills = self.scan_skills()
|
skills = self.scan_skills()
|
||||||
@ -140,10 +259,14 @@ class SkillLoader:
|
|||||||
skills_section += "1. **Discover**: Review the skills list above\n"
|
skills_section += "1. **Discover**: Review the skills list above\n"
|
||||||
skills_section += "2. **Load**: When a user request matches a skill's description, "
|
skills_section += "2. **Load**: When a user request matches a skill's description, "
|
||||||
skills_section += "use `load_skill(skill_name)` to get detailed instructions\n"
|
skills_section += "use `load_skill(skill_name)` to get detailed instructions\n"
|
||||||
skills_section += "3. **Execute**: Follow the skill's instructions, which may include "
|
skills_section += "3. **References**: If the loaded skill lists available references, "
|
||||||
|
skills_section += "use `load_reference(skill_name, reference_name)` to load specific reference material when needed\n"
|
||||||
|
skills_section += "4. **Execute**: Follow the skill's instructions, which may include "
|
||||||
skills_section += "running scripts via `bash`\n\n"
|
skills_section += "running scripts via `bash`\n\n"
|
||||||
skills_section += "**Important**: Only load a skill when it's relevant to the user's request. "
|
skills_section += "**Important**:\n"
|
||||||
skills_section += "Script code never enters the context - only their output does.\n"
|
skills_section += "- Only load a skill when it's relevant to the user's request\n"
|
||||||
|
skills_section += "- Reference files are NOT automatically loaded - only load them when the skill instructions indicate they are needed\n"
|
||||||
|
skills_section += "- Script code never enters the context - only their output does\n"
|
||||||
else:
|
else:
|
||||||
skills_section = "## Skills\n\nNo skills currently available.\n"
|
skills_section = "## Skills\n\nNo skills currently available.\n"
|
||||||
|
|
||||||
@ -151,3 +274,109 @@ class SkillLoader:
|
|||||||
return f"{base_prompt}\n\n{skills_section}"
|
return f"{base_prompt}\n\n{skills_section}"
|
||||||
else:
|
else:
|
||||||
return f"You are a helpful coding assistant.\n\n{skills_section}"
|
return f"You are a helpful coding assistant.\n\n{skills_section}"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 智能Skill选择方法
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
async def select_skills_for_task(
|
||||||
|
self,
|
||||||
|
user_intent: str,
|
||||||
|
available_skills: Optional[List[Dict[str, Any]]] = None,
|
||||||
|
context: Optional[Dict[str, Any]] = None
|
||||||
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
根据用户意图智能选择最相关的Skills
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_intent: 用户意图描述
|
||||||
|
available_skills: 可用的技能列表(从API获取)
|
||||||
|
context: 额外的上下文信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
选择的Skill ID列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 如果有可用技能列表(从API),进行智能选择
|
||||||
|
if available_skills:
|
||||||
|
selected = []
|
||||||
|
|
||||||
|
# 基于关键词的简单匹配
|
||||||
|
intent_lower = user_intent.lower()
|
||||||
|
for skill in available_skills:
|
||||||
|
skill_id = skill.get('id', '')
|
||||||
|
name = skill.get('name', '').lower()
|
||||||
|
description = skill.get('description', '').lower()
|
||||||
|
capabilities = skill.get('capabilities', '').lower()
|
||||||
|
tags = skill.get('tags', [])
|
||||||
|
|
||||||
|
# 检查描述和能力是否匹配
|
||||||
|
match_score = 0
|
||||||
|
if name in intent_lower:
|
||||||
|
match_score += 10
|
||||||
|
for tag in tags:
|
||||||
|
if tag.lower() in intent_lower or any(k in intent_lower for k in tag.lower().split('_')):
|
||||||
|
match_score += 5
|
||||||
|
|
||||||
|
# 检查描述匹配
|
||||||
|
keywords = ['大纲', '内容', '对话', '角色', '世界观', '审核', '创作', '写作']
|
||||||
|
for keyword in keywords:
|
||||||
|
if keyword in intent_lower and keyword in description:
|
||||||
|
match_score += 3
|
||||||
|
|
||||||
|
if match_score > 0:
|
||||||
|
selected.append((skill_id, match_score))
|
||||||
|
|
||||||
|
# 按匹配分数排序
|
||||||
|
selected.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
return [skill_id for skill_id, _ in selected[:5]]
|
||||||
|
|
||||||
|
# 如果没有可用技能列表,使用本地扫描的技能
|
||||||
|
skills = self.scan_skills()
|
||||||
|
selected = []
|
||||||
|
|
||||||
|
intent_lower = user_intent.lower()
|
||||||
|
for skill in skills:
|
||||||
|
match_score = 0
|
||||||
|
if skill.name.lower() in intent_lower:
|
||||||
|
match_score += 10
|
||||||
|
for tag in skill.tags:
|
||||||
|
if tag.lower() in intent_lower:
|
||||||
|
match_score += 5
|
||||||
|
|
||||||
|
if match_score > 0:
|
||||||
|
selected.append((skill.name, match_score))
|
||||||
|
|
||||||
|
selected.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
return [skill_name for skill_name, _ in selected[:5]]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
logger.warning(f"智能Skill选择失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_user_skills_metadata(self, user_skills: List[Dict[str, Any]]) -> str:
|
||||||
|
"""
|
||||||
|
获取用户配置Skills的元数据格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_skills: 用户Skills列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化后的Skills信息
|
||||||
|
"""
|
||||||
|
if not user_skills:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
lines = ["### 用户配置的Skills\n\n"]
|
||||||
|
for skill in user_skills:
|
||||||
|
skill_name = skill.get('name', 'Unknown')
|
||||||
|
skill_id = skill.get('id', '')
|
||||||
|
behavior = skill.get('behavior', skill.get('description', ''))[:200]
|
||||||
|
|
||||||
|
lines.append(f"- **{skill_name}** (ID: `{skill_id}`)")
|
||||||
|
lines.append(f" {behavior}...")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from langchain.tools import tool, ToolRuntime
|
from langchain.tools import tool, ToolRuntime
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
|
||||||
from .skill_loader import SkillLoader
|
from .skill_loader import SkillLoader
|
||||||
from .stream.utils import resolve_path
|
from .stream.utils import resolve_path
|
||||||
@ -16,7 +17,7 @@ from .director_tools import DIRECTOR_TOOLS
|
|||||||
|
|
||||||
@tool
|
@tool
|
||||||
def load_skill(skill_name: str, runtime: ToolRuntime[SkillAgentContext]) -> str:
|
def load_skill(skill_name: str, runtime: ToolRuntime[SkillAgentContext]) -> str:
|
||||||
"""Load a skill's detailed instructions."""
|
"""Load a skill's detailed instructions. Automatically includes any dependent skills. Returns the SKILL.md instructions and a list of available references."""
|
||||||
loader = runtime.context.skill_loader
|
loader = runtime.context.skill_loader
|
||||||
skill_content = loader.load_skill(skill_name)
|
skill_content = loader.load_skill(skill_name)
|
||||||
|
|
||||||
@ -31,6 +32,31 @@ def load_skill(skill_name: str, runtime: ToolRuntime[SkillAgentContext]) -> str:
|
|||||||
skill_path = skill_content.metadata.skill_path
|
skill_path = skill_content.metadata.skill_path
|
||||||
scripts_dir = skill_path / "scripts"
|
scripts_dir = skill_path / "scripts"
|
||||||
|
|
||||||
|
# 构建依赖 Skills 部分
|
||||||
|
dependencies_section = ""
|
||||||
|
if skill_content.loaded_skills:
|
||||||
|
dependencies_section = "\n## Dependent Skills\n\n"
|
||||||
|
dependencies_section += "This skill depends on the following skills, which have been automatically loaded:\n\n"
|
||||||
|
for dep_name, dep_content in skill_content.loaded_skills.items():
|
||||||
|
dependencies_section += f"### {dep_name}\n\n"
|
||||||
|
dependencies_section += f"{dep_content.instructions}\n\n"
|
||||||
|
# 如果依赖 skill 也有 references,也列出来
|
||||||
|
if dep_content.references:
|
||||||
|
dependencies_section += f"**Available References for {dep_name}**: "
|
||||||
|
dependencies_section += ", ".join(f"`{ref.name}`" for ref in dep_content.references)
|
||||||
|
dependencies_section += "\n\n"
|
||||||
|
dependencies_section += "---\n\n"
|
||||||
|
|
||||||
|
# 构建 References 部分
|
||||||
|
references_section = ""
|
||||||
|
if skill_content.references:
|
||||||
|
references_section = "\n## Available References\n\n"
|
||||||
|
references_section += "This skill has the following reference documents available:\n\n"
|
||||||
|
for ref in skill_content.references:
|
||||||
|
references_section += f"{ref.to_prompt_line()}\n"
|
||||||
|
references_section += "\n"
|
||||||
|
references_section += "Use `load_reference(skill_name, reference_name)` to load a specific reference when needed.\n"
|
||||||
|
|
||||||
path_info = f"""
|
path_info = f"""
|
||||||
## Skill Path Info
|
## Skill Path Info
|
||||||
|
|
||||||
@ -45,10 +71,37 @@ uv run {scripts_dir}/script_name.py [args]
|
|||||||
|
|
||||||
return f"""# Skill: {skill_name}
|
return f"""# Skill: {skill_name}
|
||||||
|
|
||||||
|
{dependencies_section}
|
||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
{skill_content.instructions}
|
{skill_content.instructions}
|
||||||
{path_info}
|
{path_info}
|
||||||
|
{references_section}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def load_reference(skill_name: str, reference_name: str, runtime: ToolRuntime[SkillAgentContext]) -> str:
|
||||||
|
"""Load a reference document from a skill's references/ directory. Use this when the skill instructions indicate that additional reference material is needed for a specific task."""
|
||||||
|
loader = runtime.context.skill_loader
|
||||||
|
content = loader.load_reference(skill_name, reference_name)
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
# 检查 skill 是否存在
|
||||||
|
skill_content = loader.load_skill(skill_name)
|
||||||
|
if not skill_content:
|
||||||
|
return f"Skill '{skill_name}' not found."
|
||||||
|
|
||||||
|
# 列出可用的 references
|
||||||
|
if skill_content.references:
|
||||||
|
available = [r.name for r in skill_content.references]
|
||||||
|
return f"Reference '{reference_name}' not found in skill '{skill_name}'. Available references: {', '.join(available)}"
|
||||||
|
else:
|
||||||
|
return f"Skill '{skill_name}' has no references available."
|
||||||
|
|
||||||
|
return f"""# Reference: {skill_name}/{reference_name}
|
||||||
|
|
||||||
|
{content}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -290,7 +343,15 @@ def list_dir(path: str, runtime: ToolRuntime[SkillAgentContext]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
ALL_TOOLS = [
|
ALL_TOOLS = [
|
||||||
load_skill, bash, read_file, write_file, glob, grep, edit, list_dir,
|
load_skill,
|
||||||
|
load_reference, # 按需加载 skill 的 references/ 文档
|
||||||
|
bash,
|
||||||
|
read_file,
|
||||||
|
write_file,
|
||||||
|
glob,
|
||||||
|
grep,
|
||||||
|
edit,
|
||||||
|
list_dir,
|
||||||
# Director Tools (includes update_plan, add_inbox_task, add_annotation, update_context, write_to_canvas)
|
# Director Tools (includes update_plan, add_inbox_task, add_annotation, update_context, write_to_canvas)
|
||||||
*DIRECTOR_TOOLS
|
*DIRECTOR_TOOLS,
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
剧集创作 Agent
|
剧集创作 Agent 现在暂未实现
|
||||||
|
|
||||||
固定工作流:
|
固定工作流:
|
||||||
1. 加载全局上下文和历史记忆
|
1. 加载全局上下文和历史记忆
|
||||||
|
|||||||
@ -48,6 +48,24 @@ class ReviewManager:
|
|||||||
"default_strictness": 0.7,
|
"default_strictness": 0.7,
|
||||||
"skill_ids": ["consistency_checker"]
|
"skill_ids": ["consistency_checker"]
|
||||||
},
|
},
|
||||||
|
DimensionType.character_consistency: {
|
||||||
|
"name": "角色一致性审核",
|
||||||
|
"description": "检查角色性格、行为、对话风格的前后一致性",
|
||||||
|
"default_strictness": 0.7,
|
||||||
|
"skill_ids": ["consistency_checker"]
|
||||||
|
},
|
||||||
|
DimensionType.plot_coherence: {
|
||||||
|
"name": "剧情连贯性审核",
|
||||||
|
"description": "检查剧情逻辑、时间线、因果关系的一致性",
|
||||||
|
"default_strictness": 0.7,
|
||||||
|
"skill_ids": []
|
||||||
|
},
|
||||||
|
DimensionType.dialogue_quality: {
|
||||||
|
"name": "对话质量审核",
|
||||||
|
"description": "检查对话的自然度、角色声音的区分度",
|
||||||
|
"default_strictness": 0.6,
|
||||||
|
"skill_ids": []
|
||||||
|
},
|
||||||
DimensionType.quality: {
|
DimensionType.quality: {
|
||||||
"name": "质量审核",
|
"name": "质量审核",
|
||||||
"description": "检查文字质量、表达流畅度",
|
"description": "检查文字质量、表达流畅度",
|
||||||
@ -77,6 +95,24 @@ class ReviewManager:
|
|||||||
"description": "检查剧情逻辑和完整性",
|
"description": "检查剧情逻辑和完整性",
|
||||||
"default_strictness": 0.7,
|
"default_strictness": 0.7,
|
||||||
"skill_ids": []
|
"skill_ids": []
|
||||||
|
},
|
||||||
|
DimensionType.emotional_depth: {
|
||||||
|
"name": "情感深度审核",
|
||||||
|
"description": "检查情感表达的真实性、感染力",
|
||||||
|
"default_strictness": 0.7,
|
||||||
|
"skill_ids": []
|
||||||
|
},
|
||||||
|
DimensionType.thematic_strength: {
|
||||||
|
"name": "主题强度审核",
|
||||||
|
"description": "检查主题表达的一致性、深度",
|
||||||
|
"default_strictness": 0.7,
|
||||||
|
"skill_ids": []
|
||||||
|
},
|
||||||
|
DimensionType.custom: {
|
||||||
|
"name": "自定义审核",
|
||||||
|
"description": "用户自定义的审核维度",
|
||||||
|
"default_strictness": 0.5,
|
||||||
|
"skill_ids": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
693
backend/app/core/review/review_task_manager.py
Normal file
693
backend/app/core/review/review_task_manager.py
Normal file
@ -0,0 +1,693 @@
|
|||||||
|
"""
|
||||||
|
Review Task Manager
|
||||||
|
|
||||||
|
审核任务管理器 - 负责管理过程中审核和生成后审核任务
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
from typing import List, Optional, Dict, Any, AsyncIterator
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from app.models.review_task import (
|
||||||
|
ReviewTask,
|
||||||
|
ReviewTaskStatus,
|
||||||
|
ReviewMode,
|
||||||
|
ReviewAction,
|
||||||
|
ProcessReviewRequest,
|
||||||
|
UserReviewActionRequest,
|
||||||
|
PostCreationReviewRequest,
|
||||||
|
PostCreationReviewResult,
|
||||||
|
ReviewTaskLocation,
|
||||||
|
ReviewTriggerSource
|
||||||
|
)
|
||||||
|
from app.models.review import ReviewConfig, DimensionType, DimensionConfig, CustomRule
|
||||||
|
from app.core.review.review_manager import ReviewManager
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 内存存储(过程审核任务自动销毁,不需要持久化)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
_process_tasks: Dict[str, ReviewTask] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewTaskManager:
|
||||||
|
"""
|
||||||
|
审核任务管理器
|
||||||
|
|
||||||
|
职责:
|
||||||
|
1. 管理过程中审核任务(内存存储,自动销毁)
|
||||||
|
2. 管理生成后审核任务(持久化到项目)
|
||||||
|
3. 处理用户对审核任务的响应
|
||||||
|
4. 调用ReviewManager执行审核
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, review_manager: Optional[ReviewManager] = None):
|
||||||
|
self.review_manager = review_manager
|
||||||
|
|
||||||
|
if not self.review_manager:
|
||||||
|
from app.core.review.review_manager import get_review_manager
|
||||||
|
self.review_manager = get_review_manager()
|
||||||
|
|
||||||
|
logger.info("审核任务管理器初始化完成")
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 过程中审核(内存存储,自动销毁)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
async def create_process_review_task(
|
||||||
|
self,
|
||||||
|
request: ProcessReviewRequest
|
||||||
|
) -> ReviewTask:
|
||||||
|
"""
|
||||||
|
创建过程中审核任务
|
||||||
|
|
||||||
|
过程中审核任务存储在内存中,用户确认后自动销毁。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: 审核请求
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReviewTask: 创建的审核任务
|
||||||
|
"""
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 根据触发来源确定可用操作
|
||||||
|
if request.trigger_source == ReviewTriggerSource.AGENT_AUTO:
|
||||||
|
# Agent自主推送的审核:确认继续、补充信息、拒绝
|
||||||
|
available_actions = [
|
||||||
|
ReviewAction.CONFIRM,
|
||||||
|
ReviewAction.MODIFY,
|
||||||
|
ReviewAction.OTHER
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# 其他来源:标准操作
|
||||||
|
available_actions = [
|
||||||
|
ReviewAction.CONFIRM,
|
||||||
|
ReviewAction.IGNORE
|
||||||
|
]
|
||||||
|
|
||||||
|
task = ReviewTask(
|
||||||
|
id=task_id,
|
||||||
|
project_id=request.project_id,
|
||||||
|
mode=ReviewMode.PROCESS,
|
||||||
|
trigger_source=request.trigger_source,
|
||||||
|
issue_type=request.issue_type,
|
||||||
|
issue_description=request.issue_description,
|
||||||
|
severity=request.severity,
|
||||||
|
location=request.location,
|
||||||
|
review_config_id=request.review_config_id,
|
||||||
|
skill_id=request.skill_id,
|
||||||
|
rule_id=request.rule_id,
|
||||||
|
available_actions=available_actions,
|
||||||
|
status=ReviewTaskStatus.AWAITING_USER,
|
||||||
|
suggestion=request.suggestion,
|
||||||
|
context_data={
|
||||||
|
"trigger_source": request.trigger_source.value,
|
||||||
|
"skill_id": request.skill_id,
|
||||||
|
"rule_id": request.rule_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 存储到内存
|
||||||
|
_process_tasks[task_id] = task
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"创建过程中审核任务: {task_id}, "
|
||||||
|
f"问题: {request.issue_type}, 严重度: {request.severity}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return task
|
||||||
|
|
||||||
|
async def handle_user_review_action(
|
||||||
|
self,
|
||||||
|
request: UserReviewActionRequest
|
||||||
|
) -> ReviewTask:
|
||||||
|
"""
|
||||||
|
处理用户对审核任务的响应
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: 用户操作请求
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReviewTask: 更新后的任务
|
||||||
|
"""
|
||||||
|
task = _process_tasks.get(request.task_id)
|
||||||
|
|
||||||
|
if not task:
|
||||||
|
logger.warning(f"审核任务不存在: {request.task_id}")
|
||||||
|
raise ValueError(f"审核任务不存在: {request.task_id}")
|
||||||
|
|
||||||
|
# 验证操作是否可用
|
||||||
|
if request.action not in task.available_actions:
|
||||||
|
logger.warning(
|
||||||
|
f"操作 {request.action} 对任务 {request.task_id} 不可用, "
|
||||||
|
f"可用操作: {[a.value for a in task.available_actions]}"
|
||||||
|
)
|
||||||
|
raise ValueError(f"操作不可用")
|
||||||
|
|
||||||
|
# 更新任务状态
|
||||||
|
if request.action == ReviewAction.CONFIRM:
|
||||||
|
task.status = ReviewTaskStatus.COMPLETED
|
||||||
|
task.user_response = "用户确认继续"
|
||||||
|
task.completed_at = datetime.now()
|
||||||
|
|
||||||
|
elif request.action == ReviewAction.MODIFY:
|
||||||
|
task.status = ReviewTaskStatus.MODIFIED
|
||||||
|
task.user_response = request.user_response or "用户修改了内容"
|
||||||
|
task.completed_at = datetime.now()
|
||||||
|
# 保存修改后的内容到上下文
|
||||||
|
if request.modified_content:
|
||||||
|
task.context_data = task.context_data or {}
|
||||||
|
task.context_data["modified_content"] = request.modified_content
|
||||||
|
|
||||||
|
elif request.action == ReviewAction.OTHER:
|
||||||
|
task.status = ReviewTaskStatus.COMPLETED
|
||||||
|
task.user_response = request.user_response or "用户补充了信息"
|
||||||
|
task.completed_at = datetime.now()
|
||||||
|
|
||||||
|
elif request.action == ReviewAction.IGNORE:
|
||||||
|
task.status = ReviewTaskStatus.IGNORED
|
||||||
|
task.user_response = "用户忽略问题"
|
||||||
|
task.completed_at = datetime.now()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"处理用户操作: {request.action.value}, "
|
||||||
|
f"任务: {task_id}, 新状态: {task.status.value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 从内存中移除已完成/忽略的任务
|
||||||
|
if task.status in [ReviewTaskStatus.COMPLETED, ReviewTaskStatus.IGNORED]:
|
||||||
|
_process_tasks.pop(request.task_id, None)
|
||||||
|
logger.info(f"过程审核任务已销毁: {request.task_id}")
|
||||||
|
|
||||||
|
return task
|
||||||
|
|
||||||
|
def get_pending_process_tasks(
|
||||||
|
self,
|
||||||
|
project_id: str
|
||||||
|
) -> List[ReviewTask]:
|
||||||
|
"""
|
||||||
|
获取项目所有待处理的过程审核任务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: 项目ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[ReviewTask]: 待处理任务列表
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
task for task in _process_tasks.values()
|
||||||
|
if task.project_id == project_id and
|
||||||
|
task.status == ReviewTaskStatus.AWAITING_USER
|
||||||
|
]
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 生成后审核(持久化到项目)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
async def execute_post_creation_review(
|
||||||
|
self,
|
||||||
|
request: PostCreationReviewRequest,
|
||||||
|
project_config: ReviewConfig
|
||||||
|
) -> PostCreationReviewResult:
|
||||||
|
"""
|
||||||
|
执行生成后审核
|
||||||
|
|
||||||
|
根据用户配置的规则/Skill进行自动检查,根据阈值报告问题。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: 生成后审核请求
|
||||||
|
project_config: 项目审核配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PostCreationReviewResult: 审核结果
|
||||||
|
"""
|
||||||
|
from app.models.project import SeriesProject, Episode
|
||||||
|
|
||||||
|
# TODO: 从项目获取剧集
|
||||||
|
# 这里简化处理,实际需要从episode_repo获取
|
||||||
|
episode = Episode(
|
||||||
|
projectId=request.project_id,
|
||||||
|
id=request.episode_id,
|
||||||
|
number=1,
|
||||||
|
content="",
|
||||||
|
status="completed"
|
||||||
|
)
|
||||||
|
project = SeriesProject(
|
||||||
|
id=request.project_id,
|
||||||
|
name="",
|
||||||
|
globalContext=None
|
||||||
|
)
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
high_severity_count = 0
|
||||||
|
|
||||||
|
# 1. 使用配置的Skill进行审核
|
||||||
|
if request.use_configured_skills and project_config.enabled_review_skills:
|
||||||
|
for skill_id in project_config.enabled_review_skills:
|
||||||
|
skill_tasks = await self._execute_skill_review(
|
||||||
|
project=project,
|
||||||
|
episode=episode,
|
||||||
|
skill_id=skill_id,
|
||||||
|
project_config=project_config
|
||||||
|
)
|
||||||
|
tasks.extend(skill_tasks)
|
||||||
|
high_severity_count += sum(1 for t in skill_tasks if t.severity == "high")
|
||||||
|
|
||||||
|
# 2. 使用配置的规则进行审核
|
||||||
|
if request.use_configured_rules and project_config.custom_rules:
|
||||||
|
for rule in project_config.custom_rules:
|
||||||
|
if rule.enabled:
|
||||||
|
rule_tasks = await self._execute_rule_review(
|
||||||
|
project=project,
|
||||||
|
episode=episode,
|
||||||
|
rule=rule,
|
||||||
|
project_config=project_config
|
||||||
|
)
|
||||||
|
tasks.extend(rule_tasks)
|
||||||
|
high_severity_count += sum(1 for t in rule_tasks if t.severity == "high")
|
||||||
|
|
||||||
|
# 3. 根据阈值判断是否通过
|
||||||
|
pass_threshold = request.pass_threshold or project_config.pass_threshold
|
||||||
|
passed = high_severity_count == 0
|
||||||
|
|
||||||
|
# 4. 生成总体分数
|
||||||
|
if tasks:
|
||||||
|
overall_score = max(0, 100 - len(tasks) * 5 - high_severity_count * 10)
|
||||||
|
else:
|
||||||
|
overall_score = 100.0
|
||||||
|
|
||||||
|
result = PostCreationReviewResult(
|
||||||
|
task_count=len(tasks),
|
||||||
|
passed=passed,
|
||||||
|
tasks=tasks,
|
||||||
|
overall_score=overall_score
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"生成后审核完成: {request.project_id}, "
|
||||||
|
f"任务数: {len(tasks)}, 高严重度: {high_severity_count}, "
|
||||||
|
f"通过: {passed}, 分数: {overall_score}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _execute_skill_review(
|
||||||
|
self,
|
||||||
|
project,
|
||||||
|
episode,
|
||||||
|
skill_id: str,
|
||||||
|
project_config: ReviewConfig
|
||||||
|
) -> List[ReviewTask]:
|
||||||
|
"""
|
||||||
|
执行Skill审核
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: 项目
|
||||||
|
episode: 剧集
|
||||||
|
skill_id: Skill ID
|
||||||
|
project_config: 审核配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[ReviewTask]: 发现的审核任务
|
||||||
|
"""
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取Skill信息
|
||||||
|
from app.core.skills.skill_manager import get_skill_manager
|
||||||
|
skill_manager = get_skill_manager()
|
||||||
|
skill = skill_manager.get_skill_by_id(skill_id)
|
||||||
|
|
||||||
|
if not skill:
|
||||||
|
logger.warning(f"Skill不存在: {skill_id}")
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
# 使用ReviewManager的LLM审核
|
||||||
|
from app.models.review import Issue, IssueType, SeverityLevel, Location
|
||||||
|
|
||||||
|
issues = await self.review_manager._run_llm_review(
|
||||||
|
project=project,
|
||||||
|
episode=episode,
|
||||||
|
dimension=DimensionType.custom, # 使用custom维度代表Skill审核
|
||||||
|
strictness=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
# 转换为ReviewTask
|
||||||
|
for issue in issues:
|
||||||
|
task = ReviewTask(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
project_id=project.id,
|
||||||
|
episode_id=episode.id,
|
||||||
|
mode=ReviewMode.POST_CREATION,
|
||||||
|
trigger_source=ReviewTriggerSource.USER_CONFIGURED_SKILL,
|
||||||
|
issue_type=issue.type.value,
|
||||||
|
issue_description=issue.description,
|
||||||
|
severity=issue.severity.value,
|
||||||
|
location=ReviewTaskLocation(
|
||||||
|
episode_number=issue.location.episode,
|
||||||
|
scene_number=issue.location.scene,
|
||||||
|
line_number=issue.location.line,
|
||||||
|
content_snippet=issue.location.context
|
||||||
|
),
|
||||||
|
skill_id=skill_id,
|
||||||
|
available_actions=[ReviewAction.CONFIRM, ReviewAction.IGNORE, ReviewAction.MODIFY],
|
||||||
|
status=ReviewTaskStatus.AWAITING_USER,
|
||||||
|
suggestion=issue.suggestion
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Skill审核失败 {skill_id}: {str(e)}")
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
async def _execute_rule_review(
|
||||||
|
self,
|
||||||
|
project,
|
||||||
|
episode,
|
||||||
|
rule,
|
||||||
|
project_config: ReviewConfig
|
||||||
|
) -> List[ReviewTask]:
|
||||||
|
"""
|
||||||
|
执行规则审核
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: 项目
|
||||||
|
episode: 剧集
|
||||||
|
rule: 自定义规则
|
||||||
|
project_config: 审核配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[ReviewTask]: 发现的审核任务
|
||||||
|
"""
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用ReviewManager的规则检查
|
||||||
|
issues = await self.review_manager._check_single_rule(
|
||||||
|
episode=episode,
|
||||||
|
rule=rule,
|
||||||
|
config=project_config
|
||||||
|
)
|
||||||
|
|
||||||
|
# 转换为ReviewTask
|
||||||
|
for issue in issues:
|
||||||
|
task = ReviewTask(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
project_id=project.id,
|
||||||
|
episode_id=episode.id,
|
||||||
|
mode=ReviewMode.POST_CREATION,
|
||||||
|
trigger_source=ReviewTriggerSource.USER_CONFIGURED_RULE,
|
||||||
|
issue_type=issue.type.value,
|
||||||
|
issue_description=issue.description,
|
||||||
|
severity=issue.severity.value,
|
||||||
|
location=ReviewTaskLocation(
|
||||||
|
episode_number=issue.location.episode,
|
||||||
|
scene_number=issue.location.scene,
|
||||||
|
line_number=issue.location.line,
|
||||||
|
content_snippet=issue.location.context
|
||||||
|
),
|
||||||
|
rule_id=rule.id,
|
||||||
|
available_actions=[ReviewAction.CONFIRM, ReviewAction.IGNORE, ReviewAction.MODIFY],
|
||||||
|
status=ReviewTaskStatus.AWAITING_USER,
|
||||||
|
suggestion=issue.suggestion
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"规则审核失败 {rule.id}: {str(e)}")
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
def get_process_task_by_id(
|
||||||
|
self,
|
||||||
|
task_id: str
|
||||||
|
) -> Optional[ReviewTask]:
|
||||||
|
"""获取过程中审核任务"""
|
||||||
|
return _process_tasks.get(task_id)
|
||||||
|
|
||||||
|
def cleanup_expired_tasks(self):
|
||||||
|
"""清理已过期的任务(超过24小时未处理)"""
|
||||||
|
now = datetime.now()
|
||||||
|
expired_ids = []
|
||||||
|
|
||||||
|
for task_id, task in _process_tasks.items():
|
||||||
|
if task.status == ReviewTaskStatus.AWAITING_USER:
|
||||||
|
# 检查是否创建超过24小时
|
||||||
|
if (now - task.created_at).total_seconds() > 86400:
|
||||||
|
expired_ids.append(task_id)
|
||||||
|
|
||||||
|
for task_id in expired_ids:
|
||||||
|
_process_tasks.pop(task_id, None)
|
||||||
|
logger.info(f"清理过期审核任务: {task_id}")
|
||||||
|
|
||||||
|
if expired_ids:
|
||||||
|
return len(expired_ids)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def create_task_from_review(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
episode_number: int,
|
||||||
|
review_data: Dict[str, Any]
|
||||||
|
) -> ReviewTask:
|
||||||
|
"""
|
||||||
|
从审查结果创建审核任务
|
||||||
|
|
||||||
|
当 Agent 完成剧集审查后,将审查结果转换为审核任务并推送到审核系统。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: 项目ID
|
||||||
|
episode_number: 剧集号
|
||||||
|
review_data: 审查数据,包含 score, passed, issue_count, issues 等信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReviewTask: 创建的审核任务
|
||||||
|
"""
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 根据 issue_count 判断是否创建任务
|
||||||
|
issue_count = review_data.get("issue_count", 0)
|
||||||
|
issues = review_data.get("issues", [])
|
||||||
|
|
||||||
|
# 创建任务位置信息
|
||||||
|
location = ReviewTaskLocation(
|
||||||
|
episode_number=episode_number,
|
||||||
|
scene_number=None,
|
||||||
|
line_number=None,
|
||||||
|
content_snippet=f"EP{episode_number} 审查发现 {issue_count} 个问题"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果有问题,创建待处理的审核任务
|
||||||
|
if issue_count > 0:
|
||||||
|
# 找出最高严重度的问题
|
||||||
|
high_severity_issues = [i for i in issues if i.get("severity") == "high"]
|
||||||
|
medium_severity_issues = [i for i in issues if i.get("severity") == "medium"]
|
||||||
|
|
||||||
|
# 确定总体严重度
|
||||||
|
if high_severity_issues:
|
||||||
|
severity = "high"
|
||||||
|
elif medium_severity_issues:
|
||||||
|
severity = "medium"
|
||||||
|
else:
|
||||||
|
severity = "low"
|
||||||
|
|
||||||
|
# 构建问题描述
|
||||||
|
issue_types = set(i.get("dimension", "unknown") for i in issues)
|
||||||
|
issue_description = f"EP{episode_number} 审查发现 {issue_count} 个问题,涉及维度: {', '.join(issue_types)}"
|
||||||
|
|
||||||
|
# 列出主要问题
|
||||||
|
main_issues = []
|
||||||
|
for issue in issues[:5]: # 只列出前5个
|
||||||
|
main_issues.append(f"• [{issue.get('dimension', 'unknown')}] {issue.get('description', '')[:50]}")
|
||||||
|
|
||||||
|
if main_issues:
|
||||||
|
issue_description += "\n主要问题:\n" + "\n".join(main_issues)
|
||||||
|
|
||||||
|
# 创建审核任务
|
||||||
|
task = ReviewTask(
|
||||||
|
id=task_id,
|
||||||
|
project_id=project_id,
|
||||||
|
episode_id=f"ep_{episode_number}", # 剧集ID(简化)
|
||||||
|
mode=ReviewMode.POST_CREATION,
|
||||||
|
trigger_source=ReviewTriggerSource.USER_CONFIGURED_RULE, # Agent主动审查
|
||||||
|
issue_type="episode_review",
|
||||||
|
issue_description=issue_description,
|
||||||
|
severity=severity,
|
||||||
|
location=location,
|
||||||
|
available_actions=[
|
||||||
|
ReviewAction.CONFIRM, # 确认问题
|
||||||
|
ReviewAction.MODIFY, # 修改内容
|
||||||
|
ReviewAction.IGNORE # 忽略问题
|
||||||
|
],
|
||||||
|
status=ReviewTaskStatus.AWAITING_USER,
|
||||||
|
suggestion=f"建议检查并修复 EP{episode_number} 中发现的问题",
|
||||||
|
context_data={
|
||||||
|
"episode_number": episode_number,
|
||||||
|
"episode_title": review_data.get("episode_title", ""),
|
||||||
|
"score": review_data.get("score", 0),
|
||||||
|
"issue_count": issue_count,
|
||||||
|
"issues": issues,
|
||||||
|
"trigger_source": "agent_review",
|
||||||
|
"created_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 存储到内存(生成后审核任务)
|
||||||
|
_process_tasks[task_id] = task
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"从审查结果创建审核任务: {task_id}, "
|
||||||
|
f"EP{episode_number}, 问题数: {issue_count}, 严重度: {severity}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return task
|
||||||
|
else:
|
||||||
|
# 没有问题,返回一个已确认的任务表示审查通过
|
||||||
|
task = ReviewTask(
|
||||||
|
id=task_id,
|
||||||
|
project_id=project_id,
|
||||||
|
episode_id=f"ep_{episode_number}",
|
||||||
|
mode=ReviewMode.POST_CREATION,
|
||||||
|
trigger_source=ReviewTriggerSource.USER_CONFIGURED_RULE,
|
||||||
|
issue_type="episode_review",
|
||||||
|
issue_description=f"EP{episode_number} 审查通过,未发现问题",
|
||||||
|
severity="low",
|
||||||
|
location=location,
|
||||||
|
available_actions=[ReviewAction.CONFIRM],
|
||||||
|
status=ReviewTaskStatus.COMPLETED, # 直接标记为已完成
|
||||||
|
context_data={
|
||||||
|
"episode_number": episode_number,
|
||||||
|
"episode_title": review_data.get("episode_title", ""),
|
||||||
|
"score": review_data.get("score", 100),
|
||||||
|
"issue_count": 0,
|
||||||
|
"trigger_source": "agent_review",
|
||||||
|
"created_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"EP{episode_number} 审查通过,无问题")
|
||||||
|
|
||||||
|
return task
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 流式审核助手 - 支持Agent在生成过程中推送审核
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
class StreamReviewHelper:
|
||||||
|
"""
|
||||||
|
流式审核助手
|
||||||
|
|
||||||
|
帮助Agent在流式输出过程中推送审核任务。
|
||||||
|
提供阻塞式等待用户确认的机制。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, project_id: str, task_manager: ReviewTaskManager):
|
||||||
|
self.project_id = project_id
|
||||||
|
self.task_manager = task_manager
|
||||||
|
self.pending_tasks: List[str] = []
|
||||||
|
self.user_responses: Dict[str, str] = {}
|
||||||
|
|
||||||
|
async def push_review_task(
|
||||||
|
self,
|
||||||
|
issue_type: str,
|
||||||
|
issue_description: str,
|
||||||
|
severity: str = "medium",
|
||||||
|
location: Optional[ReviewTaskLocation] = None,
|
||||||
|
skill_id: Optional[str] = None,
|
||||||
|
suggestion: Optional[str] = None
|
||||||
|
) -> ReviewTask:
|
||||||
|
"""
|
||||||
|
推送审核任务(由Agent调用)
|
||||||
|
|
||||||
|
阻塞式等待用户确认后才能继续。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
issue_type: 问题类型
|
||||||
|
issue_description: 问题描述
|
||||||
|
severity: 严重程度
|
||||||
|
location: 位置信息
|
||||||
|
skill_id: 使用的Skill ID
|
||||||
|
suggestion: 修改建议
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReviewTask: 创建的审核任务
|
||||||
|
"""
|
||||||
|
request = ProcessReviewRequest(
|
||||||
|
project_id=self.project_id,
|
||||||
|
issue_type=issue_type,
|
||||||
|
issue_description=issue_description,
|
||||||
|
severity=severity,
|
||||||
|
location=location or ReviewTaskLocation(),
|
||||||
|
trigger_source=ReviewTriggerSource.AGENT_AUTO,
|
||||||
|
skill_id=skill_id,
|
||||||
|
suggestion=suggestion
|
||||||
|
)
|
||||||
|
|
||||||
|
task = await self.task_manager.create_process_review_task(request)
|
||||||
|
self.pending_tasks.append(task.id)
|
||||||
|
|
||||||
|
logger.info(f"Agent推送审核任务: {task.id}")
|
||||||
|
|
||||||
|
return task
|
||||||
|
|
||||||
|
async def wait_for_user_response(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
timeout: int = 300 # 5分钟超时
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
等待用户响应(阻塞式)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务ID
|
||||||
|
timeout: 超时时间(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: 用户响应内容,超时返回None
|
||||||
|
"""
|
||||||
|
# 检查用户是否已响应
|
||||||
|
if task_id in self.user_responses:
|
||||||
|
return self.user_responses[task_id]
|
||||||
|
|
||||||
|
# 这里简化处理 - 实际应该通过WebSocket通知前端并等待响应
|
||||||
|
# 返回None表示等待中
|
||||||
|
return None
|
||||||
|
|
||||||
|
def record_user_response(self, task_id: str, response: str):
|
||||||
|
"""记录用户响应"""
|
||||||
|
self.user_responses[task_id] = response
|
||||||
|
if task_id in self.pending_tasks:
|
||||||
|
self.pending_tasks.remove(task_id)
|
||||||
|
logger.info(f"用户响应已记录: {task_id}, 响应: {response}")
|
||||||
|
|
||||||
|
def has_pending_tasks(self) -> bool:
|
||||||
|
"""是否有待处理的审核任务"""
|
||||||
|
return len(self.pending_tasks) > 0
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 全局单例
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
_review_task_manager: Optional[ReviewTaskManager] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_review_task_manager() -> ReviewTaskManager:
|
||||||
|
"""获取审核任务管理器单例"""
|
||||||
|
global _review_task_manager
|
||||||
|
if _review_task_manager is None:
|
||||||
|
_review_task_manager = ReviewTaskManager()
|
||||||
|
return _review_task_manager
|
||||||
|
|
||||||
|
|
||||||
|
review_task_manager = get_review_task_manager()
|
||||||
191
backend/app/db/confirm_card_repository.py
Normal file
191
backend/app/db/confirm_card_repository.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
"""
|
||||||
|
Confirm Card Repository
|
||||||
|
|
||||||
|
确认卡片持久化仓储 - JSON文件存储
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.models.confirm_card import (
|
||||||
|
ConfirmCard, ConfirmCardStatus, ConfirmCardOption
|
||||||
|
)
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# 数据文件路径
|
||||||
|
DATA_DIR = Path(__file__).parent.parent.parent / "data"
|
||||||
|
CONFIRM_CARDS_FILE = DATA_DIR / "confirm_cards.json"
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmCardRepository:
|
||||||
|
"""确认卡片仓储"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.file_path = CONFIRM_CARDS_FILE
|
||||||
|
self._cards: Dict[str, ConfirmCard] = {}
|
||||||
|
self._ensure_data_dir()
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _ensure_data_dir(self):
|
||||||
|
"""确保数据目录存在"""
|
||||||
|
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
"""从文件加载"""
|
||||||
|
if self.file_path.exists():
|
||||||
|
try:
|
||||||
|
content = self.file_path.read_text(encoding="utf-8")
|
||||||
|
if content.strip():
|
||||||
|
data = json.loads(content)
|
||||||
|
for card_id, card_data in data.items():
|
||||||
|
# 转换选项列表
|
||||||
|
if "options" in card_data and isinstance(card_data["options"], list):
|
||||||
|
card_data["options"] = [
|
||||||
|
ConfirmCardOption(**opt) for opt in card_data["options"]
|
||||||
|
]
|
||||||
|
# 确保datetime正确转换
|
||||||
|
if isinstance(card_data.get("created_at"), str):
|
||||||
|
card_data["created_at"] = datetime.fromisoformat(
|
||||||
|
card_data["created_at"].replace('Z', '+00:00')
|
||||||
|
)
|
||||||
|
if isinstance(card_data.get("confirmed_at"), str):
|
||||||
|
card_data["confirmed_at"] = datetime.fromisoformat(
|
||||||
|
card_data["confirmed_at"].replace('Z', '+00:00')
|
||||||
|
)
|
||||||
|
if isinstance(card_data.get("expires_at"), str):
|
||||||
|
card_data["expires_at"] = datetime.fromisoformat(
|
||||||
|
card_data["expires_at"].replace('Z', '+00:00')
|
||||||
|
)
|
||||||
|
self._cards[card_id] = ConfirmCard(**card_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"加载确认卡片失败: {e}")
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
"""保存到文件"""
|
||||||
|
try:
|
||||||
|
data = {}
|
||||||
|
for card_id, card in self._cards.items():
|
||||||
|
card_dict = card.dict()
|
||||||
|
# 确保datetime正确序列化
|
||||||
|
if isinstance(card_dict.get("created_at"), datetime):
|
||||||
|
card_dict["created_at"] = card_dict["created_at"].isoformat()
|
||||||
|
if isinstance(card_dict.get("confirmed_at"), datetime):
|
||||||
|
card_dict["confirmed_at"] = card_dict["confirmed_at"].isoformat()
|
||||||
|
if isinstance(card_dict.get("expires_at"), datetime):
|
||||||
|
card_dict["expires_at"] = card_dict["expires_at"].isoformat()
|
||||||
|
data[card_id] = card_dict
|
||||||
|
|
||||||
|
self.file_path.write_text(
|
||||||
|
json.dumps(data, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存确认卡片失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def create(self, card: ConfirmCard) -> ConfirmCard:
|
||||||
|
"""创建确认卡片"""
|
||||||
|
if not card.id:
|
||||||
|
card.id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
self._cards[card.id] = card
|
||||||
|
self._save()
|
||||||
|
logger.info(f"创建确认卡片: {card.id}")
|
||||||
|
return card
|
||||||
|
|
||||||
|
async def get(self, card_id: str) -> Optional[ConfirmCard]:
|
||||||
|
"""获取单个卡片"""
|
||||||
|
return self._cards.get(card_id)
|
||||||
|
|
||||||
|
async def list_by_project(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
status: Optional[ConfirmCardStatus] = None,
|
||||||
|
include_expired: bool = False
|
||||||
|
) -> List[ConfirmCard]:
|
||||||
|
"""列出项目的确认卡片"""
|
||||||
|
cards = [
|
||||||
|
card for card in self._cards.values()
|
||||||
|
if card.project_id == project_id
|
||||||
|
]
|
||||||
|
|
||||||
|
if status:
|
||||||
|
cards = [c for c in cards if c.status == status]
|
||||||
|
|
||||||
|
if not include_expired:
|
||||||
|
now = datetime.now()
|
||||||
|
cards = [c for c in cards if not c.expires_at or c.expires_at > now]
|
||||||
|
|
||||||
|
# 按创建时间倒序
|
||||||
|
cards = sorted(cards, key=lambda c: c.created_at, reverse=True)
|
||||||
|
return cards
|
||||||
|
|
||||||
|
async def update_status(
|
||||||
|
self,
|
||||||
|
card_id: str,
|
||||||
|
status: ConfirmCardStatus,
|
||||||
|
selected_option_id: Optional[str] = None,
|
||||||
|
custom_response: Optional[str] = None,
|
||||||
|
user_notes: Optional[str] = None
|
||||||
|
) -> Optional[ConfirmCard]:
|
||||||
|
"""更新确认卡片状态"""
|
||||||
|
card = self._cards.get(card_id)
|
||||||
|
if not card:
|
||||||
|
return None
|
||||||
|
|
||||||
|
card.status = status
|
||||||
|
card.selected_option_id = selected_option_id
|
||||||
|
card.custom_response = custom_response
|
||||||
|
card.user_notes = user_notes
|
||||||
|
card.confirmed_at = datetime.now()
|
||||||
|
|
||||||
|
self._cards[card_id] = card
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
logger.info(f"更新确认卡片状态: {card_id} -> {status}")
|
||||||
|
return card
|
||||||
|
|
||||||
|
async def delete(self, card_id: str) -> bool:
|
||||||
|
"""删除确认卡片"""
|
||||||
|
if card_id in self._cards:
|
||||||
|
del self._cards[card_id]
|
||||||
|
self._save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def cleanup_expired(self) -> int:
|
||||||
|
"""清理过期的确认卡片"""
|
||||||
|
now = datetime.now()
|
||||||
|
expired_ids = [
|
||||||
|
card_id for card_id, card in self._cards.items()
|
||||||
|
if card.expires_at and card.expires_at < now
|
||||||
|
]
|
||||||
|
|
||||||
|
for card_id in expired_ids:
|
||||||
|
self._cards[card_id].status = ConfirmCardStatus.EXPIRED
|
||||||
|
|
||||||
|
if expired_ids:
|
||||||
|
self._save()
|
||||||
|
logger.info(f"标记 {len(expired_ids)} 个确认卡片为过期")
|
||||||
|
|
||||||
|
return len(expired_ids)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局单例
|
||||||
|
_confirm_card_repo = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_confirm_card_repo() -> ConfirmCardRepository:
|
||||||
|
"""获取确认卡片仓储单例"""
|
||||||
|
global _confirm_card_repo
|
||||||
|
if _confirm_card_repo is None:
|
||||||
|
_confirm_card_repo = ConfirmCardRepository()
|
||||||
|
return _confirm_card_repo
|
||||||
|
|
||||||
|
|
||||||
|
# 导入Dict类型
|
||||||
|
from typing import Dict
|
||||||
@ -299,4 +299,11 @@ class MessageRepository(JsonRepository):
|
|||||||
"""获取项目聊天历史"""
|
"""获取项目聊天历史"""
|
||||||
return self._data.get(project_id, [])
|
return self._data.get(project_id, [])
|
||||||
|
|
||||||
|
async def clear_history(self, project_id: str):
|
||||||
|
"""清除项目聊天历史(用于重建对话)"""
|
||||||
|
if project_id in self._data:
|
||||||
|
self._data[project_id] = []
|
||||||
|
self._save()
|
||||||
|
logger.info(f"Cleared chat history for project {project_id}")
|
||||||
|
|
||||||
message_repo = MessageRepository()
|
message_repo = MessageRepository()
|
||||||
|
|||||||
180
backend/app/db/review_card_repository.py
Normal file
180
backend/app/db/review_card_repository.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Review Card Repository
|
||||||
|
|
||||||
|
审核卡片持久化仓储 - JSON文件存储
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.models.review_card import (
|
||||||
|
ReviewCard, ReviewCardCreate, ReviewCardUpdate, ReviewCardStatus
|
||||||
|
)
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# 数据文件路径
|
||||||
|
DATA_DIR = Path(__file__).parent.parent.parent / "data"
|
||||||
|
REVIEW_CARDS_FILE = DATA_DIR / "review_cards.json"
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewCardRepository:
|
||||||
|
"""审核卡片仓储 - JSON文件持久化"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.file_path = REVIEW_CARDS_FILE
|
||||||
|
self._cards: Dict[str, ReviewCard] = {}
|
||||||
|
self._ensure_data_dir()
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _ensure_data_dir(self):
|
||||||
|
"""确保数据目录存在"""
|
||||||
|
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
"""从文件加载"""
|
||||||
|
if self.file_path.exists():
|
||||||
|
try:
|
||||||
|
content = self.file_path.read_text(encoding="utf-8")
|
||||||
|
if content.strip():
|
||||||
|
data = json.loads(content)
|
||||||
|
for card_id, card_data in data.items():
|
||||||
|
# 确保created_at和updated_at是datetime对象
|
||||||
|
if isinstance(card_data.get("created_at"), str):
|
||||||
|
card_data["created_at"] = datetime.fromisoformat(
|
||||||
|
card_data["created_at"].replace('Z', '+00:00')
|
||||||
|
)
|
||||||
|
if isinstance(card_data.get("updated_at"), str):
|
||||||
|
card_data["updated_at"] = datetime.fromisoformat(
|
||||||
|
card_data["updated_at"].replace('Z', '+00:00')
|
||||||
|
)
|
||||||
|
if isinstance(card_data.get("completed_at"), str):
|
||||||
|
card_data["completed_at"] = datetime.fromisoformat(
|
||||||
|
card_data["completed_at"].replace('Z', '+00:00')
|
||||||
|
)
|
||||||
|
self._cards[card_id] = ReviewCard(**card_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"加载审核卡片失败: {e}")
|
||||||
|
self._cards = {}
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
"""保存到文件"""
|
||||||
|
try:
|
||||||
|
data = {}
|
||||||
|
for card_id, card in self._cards.items():
|
||||||
|
card_dict = card.dict()
|
||||||
|
# 确保datetime正确序列化
|
||||||
|
if isinstance(card_dict.get("created_at"), datetime):
|
||||||
|
card_dict["created_at"] = card_dict["created_at"].isoformat()
|
||||||
|
if isinstance(card_dict.get("updated_at"), datetime):
|
||||||
|
card_dict["updated_at"] = card_dict["updated_at"].isoformat()
|
||||||
|
if isinstance(card_dict.get("completed_at"), datetime):
|
||||||
|
card_dict["completed_at"] = card_dict["completed_at"].isoformat()
|
||||||
|
data[card_id] = card_dict
|
||||||
|
|
||||||
|
self.file_path.write_text(
|
||||||
|
json.dumps(data, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存审核卡片失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def create(self, data: ReviewCardCreate) -> ReviewCard:
|
||||||
|
"""创建审核卡片"""
|
||||||
|
card_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
card = ReviewCard(
|
||||||
|
id=card_id,
|
||||||
|
**data.dict(),
|
||||||
|
created_at=datetime.now(),
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
self._cards[card_id] = card
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
logger.info(f"创建审核卡片: {card_id}")
|
||||||
|
return card
|
||||||
|
|
||||||
|
async def get(self, card_id: str) -> Optional[ReviewCard]:
|
||||||
|
"""获取单个卡片"""
|
||||||
|
return self._cards.get(card_id)
|
||||||
|
|
||||||
|
async def list_by_project(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
status: Optional[ReviewCardStatus] = None,
|
||||||
|
limit: int = 100
|
||||||
|
) -> List[ReviewCard]:
|
||||||
|
"""列出项目的审核卡片"""
|
||||||
|
cards = [
|
||||||
|
card for card in self._cards.values()
|
||||||
|
if card.project_id == project_id
|
||||||
|
]
|
||||||
|
|
||||||
|
if status:
|
||||||
|
cards = [c for c in cards if c.status == status]
|
||||||
|
|
||||||
|
# 按创建时间倒序
|
||||||
|
cards = sorted(cards, key=lambda c: c.created_at, reverse=True)
|
||||||
|
return cards[:limit]
|
||||||
|
|
||||||
|
async def update(self, card_id: str, data: ReviewCardUpdate) -> Optional[ReviewCard]:
|
||||||
|
"""更新审核卡片"""
|
||||||
|
card = self._cards.get(card_id)
|
||||||
|
if not card:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 更新字段
|
||||||
|
update_data = data.dict(exclude_unset=True, exclude_none=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(card, field, value)
|
||||||
|
|
||||||
|
card.updated_at = datetime.now()
|
||||||
|
if data.status == ReviewCardStatus.COMPLETED:
|
||||||
|
card.completed_at = datetime.now()
|
||||||
|
|
||||||
|
self._cards[card_id] = card
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
logger.info(f"更新审核卡片: {card_id}")
|
||||||
|
return card
|
||||||
|
|
||||||
|
async def delete(self, card_id: str) -> bool:
|
||||||
|
"""删除审核卡片"""
|
||||||
|
if card_id in self._cards:
|
||||||
|
del self._cards[card_id]
|
||||||
|
self._save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def count_by_project(self, project_id: str) -> int:
|
||||||
|
"""统计项目的审核卡片数量"""
|
||||||
|
return sum(1 for card in self._cards.values() if card.project_id == project_id)
|
||||||
|
|
||||||
|
async def count_by_status(self, project_id: str, status: ReviewCardStatus) -> int:
|
||||||
|
"""统计项目特定状态的审核卡片数量"""
|
||||||
|
return sum(
|
||||||
|
1 for card in self._cards.values()
|
||||||
|
if card.project_id == project_id and card.status == status
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局单例
|
||||||
|
_review_card_repo = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_review_card_repo() -> ReviewCardRepository:
|
||||||
|
"""获取审核卡片仓储单例"""
|
||||||
|
global _review_card_repo
|
||||||
|
if _review_card_repo is None:
|
||||||
|
_review_card_repo = ReviewCardRepository()
|
||||||
|
return _review_card_repo
|
||||||
|
|
||||||
|
|
||||||
|
# 导入Dict类型
|
||||||
|
from typing import Dict
|
||||||
@ -41,8 +41,11 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# 初始化审核管理器
|
# 初始化审核管理器
|
||||||
from app.core.review.review_manager import get_review_manager
|
from app.core.review.review_manager import get_review_manager
|
||||||
|
from app.core.review.review_task_manager import get_review_task_manager
|
||||||
review_mgr = get_review_manager()
|
review_mgr = get_review_manager()
|
||||||
|
review_task_mgr = get_review_task_manager()
|
||||||
logger.info(f"✅ 审核管理器初始化完成")
|
logger.info(f"✅ 审核管理器初始化完成")
|
||||||
|
logger.info(f"✅ 审核任务管理器初始化完成")
|
||||||
|
|
||||||
# 初始化批量执行器
|
# 初始化批量执行器
|
||||||
from app.core.execution.batch_executor import get_batch_executor
|
from app.core.execution.batch_executor import get_batch_executor
|
||||||
@ -108,7 +111,11 @@ async def health_check():
|
|||||||
# ============================================
|
# ============================================
|
||||||
# API 路由注册
|
# API 路由注册
|
||||||
# ============================================
|
# ============================================
|
||||||
from app.api.v1 import skills, projects, ai_assistant, memory, review, websocket, uploads, episodes, async_tasks, ai_async, skills_async
|
from app.api.v1 import (
|
||||||
|
skills, projects, ai_assistant, memory, review, review_task,
|
||||||
|
review_cards, confirm_cards, websocket, uploads, episodes,
|
||||||
|
async_tasks, ai_async, skills_async
|
||||||
|
)
|
||||||
|
|
||||||
app.include_router(skills.router, prefix="/api/v1")
|
app.include_router(skills.router, prefix="/api/v1")
|
||||||
app.include_router(skills_async.router, prefix="/api/v1")
|
app.include_router(skills_async.router, prefix="/api/v1")
|
||||||
@ -119,6 +126,9 @@ app.include_router(async_tasks.router, prefix="/api/v1")
|
|||||||
app.include_router(ai_async.router, prefix="/api/v1")
|
app.include_router(ai_async.router, prefix="/api/v1")
|
||||||
app.include_router(memory.router, prefix="/api/v1")
|
app.include_router(memory.router, prefix="/api/v1")
|
||||||
app.include_router(review.router, prefix="/api/v1")
|
app.include_router(review.router, prefix="/api/v1")
|
||||||
|
app.include_router(review_task.router, prefix="/api/v1")
|
||||||
|
app.include_router(review_cards.router, prefix="/api/v1")
|
||||||
|
app.include_router(confirm_cards.router, prefix="/api/v1")
|
||||||
app.include_router(uploads.router, prefix="/api/v1")
|
app.include_router(uploads.router, prefix="/api/v1")
|
||||||
app.include_router(websocket.router) # WebSocket router doesn't need prefix
|
app.include_router(websocket.router) # WebSocket router doesn't need prefix
|
||||||
|
|
||||||
|
|||||||
79
backend/app/models/confirm_card.py
Normal file
79
backend/app/models/confirm_card.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
Confirm Card Model
|
||||||
|
|
||||||
|
确认卡片模型 - Agent推送的用户确认任务
|
||||||
|
|
||||||
|
特点:
|
||||||
|
- 用户可以中途退出后随时确认
|
||||||
|
- 支持多个选项
|
||||||
|
- 持久化存储
|
||||||
|
- Agent可以根据用户选择继续任务
|
||||||
|
"""
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmCardType(str, Enum):
|
||||||
|
"""确认卡片类型"""
|
||||||
|
DIRECTION_DECISION = "direction_decision" # 创作方向决策
|
||||||
|
PLOT_BRANCH = "plot_branch" # 剧情分支选择
|
||||||
|
CHARACTER_ACTION = "character_action" # 角色行为确认
|
||||||
|
CONTENT_APPROVAL = "content_approval" # 内容审批
|
||||||
|
RESOURCE_ALLOCATION = "resource_allocation" # 资源分配确认
|
||||||
|
CUSTOM_QUESTION = "custom_question" # 自定义问题
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmCardStatus(str, Enum):
|
||||||
|
"""确认卡片状态"""
|
||||||
|
PENDING = "pending" # 待确认
|
||||||
|
APPROVED = "approved" # 已批准
|
||||||
|
REJECTED = "rejected" # 已拒绝
|
||||||
|
MODIFIED = "modified" # 已修改建议
|
||||||
|
EXPIRED = "expired" # 已过期
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmCardOption(BaseModel):
|
||||||
|
"""确认选项"""
|
||||||
|
id: str = Field(..., description="选项ID")
|
||||||
|
label: str = Field(..., description="选项标签")
|
||||||
|
description: str = Field("", description="选项描述")
|
||||||
|
implications: Optional[str] = Field(None, description="选择此选项的影响")
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmCard(BaseModel):
|
||||||
|
"""
|
||||||
|
确认卡片 - Agent推送的用户确认任务
|
||||||
|
"""
|
||||||
|
# 基础信息
|
||||||
|
id: str = Field(..., description="卡片ID")
|
||||||
|
project_id: str = Field(..., description="项目ID")
|
||||||
|
episode_number: Optional[int] = Field(None, description="相关集数")
|
||||||
|
|
||||||
|
# 卡片类型
|
||||||
|
card_type: ConfirmCardType = Field(..., description="确认类型")
|
||||||
|
title: str = Field(..., description="确认标题")
|
||||||
|
description: str = Field(..., description="详细描述")
|
||||||
|
context_data: Optional[str] = Field(None, description="上下文信息(长文本)")
|
||||||
|
|
||||||
|
# 确认选项
|
||||||
|
options: List[ConfirmCardOption] = Field(default_factory=list, description="可选选项")
|
||||||
|
allow_custom_response: bool = Field(False, description="是否允许自定义回复")
|
||||||
|
|
||||||
|
# 用户响应
|
||||||
|
status: ConfirmCardStatus = Field(default=ConfirmCardStatus.PENDING, description="状态")
|
||||||
|
selected_option_id: Optional[str] = Field(None, description="选择的选项ID")
|
||||||
|
custom_response: Optional[str] = Field(None, description="自定义回复内容")
|
||||||
|
user_notes: Optional[str] = Field(None, description="用户备注")
|
||||||
|
|
||||||
|
# 时间戳
|
||||||
|
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
|
||||||
|
expires_at: Optional[datetime] = Field(None, description="过期时间(可选)")
|
||||||
|
confirmed_at: Optional[datetime] = Field(None, description="确认时间")
|
||||||
|
|
||||||
|
# 元数据
|
||||||
|
metadata: Dict[str, Any] = Field(default_factory=dict, description="附加信息")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
use_enum_values = False
|
||||||
@ -23,11 +23,16 @@ class SeverityLevel(str, Enum):
|
|||||||
class DimensionType(str, Enum):
|
class DimensionType(str, Enum):
|
||||||
"""审核维度类型"""
|
"""审核维度类型"""
|
||||||
consistency = "consistency" # 一致性审核
|
consistency = "consistency" # 一致性审核
|
||||||
|
character_consistency = "character_consistency" # 角色一致性审核
|
||||||
quality = "quality" # 质量审核
|
quality = "quality" # 质量审核
|
||||||
pacing = "pacing" # 节奏审核
|
pacing = "pacing" # 节奏审核
|
||||||
dialogue = "dialogue" # 对话审核
|
dialogue = "dialogue" # 对话审核
|
||||||
|
dialogue_quality = "dialogue_quality" # 对话质量审核
|
||||||
character = "character" # 人物审核
|
character = "character" # 人物审核
|
||||||
plot = "plot" # 剧情审核
|
plot = "plot" # 剧情审核
|
||||||
|
plot_coherence = "plot_coherence" # 剧情连贯性审核
|
||||||
|
emotional_depth = "emotional_depth" # 情感深度审核
|
||||||
|
thematic_strength = "thematic_strength" # 主题强度审核
|
||||||
custom = "custom" # 自定义审核
|
custom = "custom" # 自定义审核
|
||||||
|
|
||||||
|
|
||||||
@ -181,6 +186,11 @@ class ReviewConfigUpdate(BaseModel):
|
|||||||
auto_fix_enabled: Optional[bool] = None
|
auto_fix_enabled: Optional[bool] = None
|
||||||
pass_threshold: Optional[float] = Field(None, ge=0.0, le=100.0)
|
pass_threshold: Optional[float] = Field(None, ge=0.0, le=100.0)
|
||||||
|
|
||||||
|
# 前端兼容字段 - 使用 Dict[str, Any] 避免前向引用问题
|
||||||
|
preset: Optional[str] = Field(None, description="预设模式: draft|standard|strict|custom")
|
||||||
|
dimensions: Optional[Dict[str, Any]] = Field(None, description="前端维度配置格式")
|
||||||
|
customRules: Optional[List[Dict[str, Any]]] = Field(None, description="前端自定义规则格式")
|
||||||
|
|
||||||
|
|
||||||
class ReviewRequest(BaseModel):
|
class ReviewRequest(BaseModel):
|
||||||
"""执行审核请求"""
|
"""执行审核请求"""
|
||||||
@ -211,8 +221,9 @@ class CustomRuleCreate(BaseModel):
|
|||||||
name: str = Field(..., description="规则名称")
|
name: str = Field(..., description="规则名称")
|
||||||
description: str = Field("", description="规则描述")
|
description: str = Field("", description="规则描述")
|
||||||
trigger_condition: str = Field(..., description="触发条件")
|
trigger_condition: str = Field(..., description="触发条件")
|
||||||
dimension: DimensionType = Field(..., description="所属维度")
|
dimension: Optional[DimensionType] = Field(None, description="所属维度")
|
||||||
severity: SeverityLevel = Field(SeverityLevel.medium, description="严重程度")
|
severity: SeverityLevel = Field(SeverityLevel.medium, description="严重程度")
|
||||||
|
category: Optional[str] = Field(None, description="分类(前端使用)")
|
||||||
|
|
||||||
|
|
||||||
class CustomRuleUpdate(BaseModel):
|
class CustomRuleUpdate(BaseModel):
|
||||||
@ -223,6 +234,7 @@ class CustomRuleUpdate(BaseModel):
|
|||||||
dimension: Optional[DimensionType] = None
|
dimension: Optional[DimensionType] = None
|
||||||
severity: Optional[SeverityLevel] = None
|
severity: Optional[SeverityLevel] = None
|
||||||
enabled: Optional[bool] = None
|
enabled: Optional[bool] = None
|
||||||
|
category: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ReviewPreset(BaseModel):
|
class ReviewPreset(BaseModel):
|
||||||
|
|||||||
103
backend/app/models/review_card.py
Normal file
103
backend/app/models/review_card.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""
|
||||||
|
Review Card Model
|
||||||
|
|
||||||
|
审核卡片模型 - 持久化存储的审核任务卡片
|
||||||
|
|
||||||
|
与ReviewTask的关系:
|
||||||
|
- ReviewCard是用户可见的卡片,包含审核任务的所有信息
|
||||||
|
- ReviewTask是系统内部的审核任务,可能不持久化
|
||||||
|
- 一个ReviewCard可能包含多个ReviewTask(如多集审核)
|
||||||
|
- 过程中审核完成后转换为ReviewCard持久化存储
|
||||||
|
"""
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewCardType(str, Enum):
|
||||||
|
"""审核卡片类型"""
|
||||||
|
PROCESS_AUTO = "process_auto" # 过程中Agent自主识别
|
||||||
|
POST_CREATION_AUTO = "post_creation_auto" # 生成后自动审核
|
||||||
|
USER_REQUEST_EPISODE = "user_episode" # 用户请求单集审核
|
||||||
|
USER_REQUEST_MULTIPLE = "user_multiple" # 用户请求多集审核
|
||||||
|
USER_REQUEST_ALL = "user_all" # 用户请求全集审核
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewCardStatus(str, Enum):
|
||||||
|
"""审核卡片状态"""
|
||||||
|
PENDING = "pending" # 待处理
|
||||||
|
AWAITING_USER = "awaiting_user" # 待确认
|
||||||
|
APPROVED = "approved" # 已通过
|
||||||
|
REJECTED = "rejected" # 已驳回
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewCard(BaseModel):
|
||||||
|
"""
|
||||||
|
审核卡片 - 持久化存储
|
||||||
|
"""
|
||||||
|
# 基础信息
|
||||||
|
id: str = Field(..., description="卡片ID")
|
||||||
|
project_id: str = Field(..., description="项目ID")
|
||||||
|
episode_id: Optional[str] = Field(None, description="剧集ID(单集审核)")
|
||||||
|
episode_numbers: List[int] = Field(default_factory=list, description="涉及集数列表(多集审核)")
|
||||||
|
|
||||||
|
# 卡片类型和来源
|
||||||
|
card_type: ReviewCardType = Field(..., description="卡片类型")
|
||||||
|
trigger_source: str = Field(..., description="触发来源: agent_auto/user_configured/manual")
|
||||||
|
|
||||||
|
# 审核内容
|
||||||
|
episode_outline: Optional[str] = Field(None, description="剧集大纲")
|
||||||
|
episode_content: Optional[str] = Field(None, description="剧集内容")
|
||||||
|
review_reason: str = Field(..., description="审核原因")
|
||||||
|
|
||||||
|
# 审核结果
|
||||||
|
issues: List[Dict[str, Any]] = Field(default_factory=list, description="问题列表")
|
||||||
|
overall_score: Optional[float] = Field(None, description="总体分数")
|
||||||
|
severity: str = Field("medium", description="严重程度: low/medium/high")
|
||||||
|
|
||||||
|
# 审核配置
|
||||||
|
review_config_id: Optional[str] = Field(None, description="使用的审核配置ID")
|
||||||
|
applied_skills: List[str] = Field(default_factory=list, description="应用的Skills")
|
||||||
|
applied_rules: List[str] = Field(default_factory=list, description="应用的规则")
|
||||||
|
|
||||||
|
# 用户操作
|
||||||
|
status: ReviewCardStatus = Field(default=ReviewCardStatus.PENDING, description="卡片状态")
|
||||||
|
user_comment: Optional[str] = Field(None, description="用户评论")
|
||||||
|
modified_content: Optional[str] = Field(None, description="修改后的内容")
|
||||||
|
|
||||||
|
# 时间戳
|
||||||
|
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.now, description="更新时间")
|
||||||
|
completed_at: Optional[datetime] = Field(None, description="完成时间")
|
||||||
|
|
||||||
|
# 附加信息
|
||||||
|
metadata: Dict[str, Any] = Field(default_factory=dict, description="附加元数据")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
use_enum_values = False
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewCardCreate(BaseModel):
|
||||||
|
"""创建审核卡片请求"""
|
||||||
|
project_id: str
|
||||||
|
episode_id: Optional[str] = None
|
||||||
|
episode_numbers: List[int] = Field(default_factory=list)
|
||||||
|
card_type: ReviewCardType
|
||||||
|
trigger_source: str
|
||||||
|
review_reason: str
|
||||||
|
episode_outline: Optional[str] = None
|
||||||
|
episode_content: Optional[str] = None
|
||||||
|
review_config_id: Optional[str] = None
|
||||||
|
severity: str = "medium"
|
||||||
|
issues: List[Dict[str, Any]] = Field(default_factory=list)
|
||||||
|
overall_score: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewCardUpdate(BaseModel):
|
||||||
|
"""更新审核卡片请求"""
|
||||||
|
status: Optional[ReviewCardStatus] = None
|
||||||
|
user_comment: Optional[str] = None
|
||||||
|
modified_content: Optional[str] = None
|
||||||
|
issues: Optional[List[Dict[str, Any]]] = None
|
||||||
|
overall_score: Optional[float] = None
|
||||||
185
backend/app/models/review_task.py
Normal file
185
backend/app/models/review_task.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
"""
|
||||||
|
Review Task Model
|
||||||
|
|
||||||
|
审核任务模型 - 支持过程中审核和生成后审核两种模式
|
||||||
|
|
||||||
|
## 审核模式说明
|
||||||
|
|
||||||
|
### 1. 过程中审核 (Process Review)
|
||||||
|
- 触发时机:Agent在流式输出过程中自主识别需要审核的内容
|
||||||
|
- 阻塞方式:阻塞式等待用户确认
|
||||||
|
- 消失方式:用户确认后自动销毁,不需要留存记录
|
||||||
|
- 交互位置:在聊天界面完成
|
||||||
|
|
||||||
|
### 2. 生成后审核 (Post-Creation Review)
|
||||||
|
- 触发时机:剧集生成完成后自动检查
|
||||||
|
- 触发源:根据用户配置的审核规则/Skill
|
||||||
|
- 阈值判断:根据阈值判断是否报告问题
|
||||||
|
- 存留方式:留存记录,用户可以确认问题、忽略问题、修改内容
|
||||||
|
"""
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 枚举类型
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
class ReviewMode(str, Enum):
|
||||||
|
"""审核模式"""
|
||||||
|
PROCESS = "process" # 过程中审核
|
||||||
|
POST_CREATION = "post_creation" # 生成后审核
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewTriggerSource(str, Enum):
|
||||||
|
"""审核触发来源"""
|
||||||
|
AGENT_AUTO = "agent_auto" # Agent自主识别
|
||||||
|
USER_CONFIGURED_SKILL = "user_skill" # 用户配置的Skill
|
||||||
|
USER_CONFIGURED_RULE = "user_rule" # 用户配置的规则
|
||||||
|
MANUAL_REQUEST = "manual" # 手动请求
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewTaskStatus(str, Enum):
|
||||||
|
"""审核任务状态"""
|
||||||
|
PENDING = "pending" # 等待处理
|
||||||
|
AWAITING_USER = "awaiting_user" # 等待用户确认
|
||||||
|
APPROVED = "approved" # 已确认
|
||||||
|
REJECTED = "rejected" # 已拒绝
|
||||||
|
MODIFIED = "modified" # 已修改内容
|
||||||
|
IGNORED = "ignored" # 已忽略
|
||||||
|
COMPLETED = "completed" # 已完成(自动销毁)
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewAction(str, Enum):
|
||||||
|
"""用户可执行的操作"""
|
||||||
|
CONFIRM = "confirm" # 确认继续
|
||||||
|
MODIFY = "modify" # 修改内容
|
||||||
|
IGNORE = "ignore" # 忽略问题
|
||||||
|
OTHER = "other" # 其他补充信息
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 审核任务模型
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
class ReviewTaskLocation(BaseModel):
|
||||||
|
"""审核任务位置信息"""
|
||||||
|
episode_number: Optional[int] = Field(None, description="集数")
|
||||||
|
scene_number: Optional[int] = Field(None, description="场景编号")
|
||||||
|
section: Optional[str] = Field(None, description="章节/段落")
|
||||||
|
line_number: Optional[int] = Field(None, description="行号")
|
||||||
|
content_snippet: Optional[str] = Field(None, description="相关内容片段")
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewTask(BaseModel):
|
||||||
|
"""
|
||||||
|
审核任务
|
||||||
|
|
||||||
|
包含问题类型、位置、严重程度等信息
|
||||||
|
"""
|
||||||
|
id: str = Field(..., description="任务ID")
|
||||||
|
project_id: str = Field(..., description="项目ID")
|
||||||
|
episode_id: Optional[str] = Field(None, description="剧集ID")
|
||||||
|
|
||||||
|
# 审核模式和来源
|
||||||
|
mode: ReviewMode = Field(..., description="审核模式")
|
||||||
|
trigger_source: ReviewTriggerSource = Field(..., description="触发来源")
|
||||||
|
|
||||||
|
# 任务信息
|
||||||
|
issue_type: str = Field(..., description="问题类型")
|
||||||
|
issue_description: str = Field(..., description="问题描述")
|
||||||
|
severity: str = Field(..., description="严重程度: low/medium/high")
|
||||||
|
location: ReviewTaskLocation = Field(..., description="问题位置")
|
||||||
|
|
||||||
|
# 审核配置来源
|
||||||
|
review_config_id: Optional[str] = Field(None, description="触发的审核配置ID")
|
||||||
|
skill_id: Optional[str] = Field(None, description="使用的Skill ID")
|
||||||
|
rule_id: Optional[str] = Field(None, description="触发的规则ID")
|
||||||
|
|
||||||
|
# 可用操作
|
||||||
|
available_actions: List[ReviewAction] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="用户可执行的操作列表"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 状态和用户响应
|
||||||
|
status: ReviewTaskStatus = Field(
|
||||||
|
default=ReviewTaskStatus.PENDING,
|
||||||
|
description="任务状态"
|
||||||
|
)
|
||||||
|
user_response: Optional[str] = Field(None, description="用户的响应内容")
|
||||||
|
|
||||||
|
# 时间戳
|
||||||
|
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
|
||||||
|
completed_at: Optional[datetime] = Field(None, description="完成时间")
|
||||||
|
expires_at: Optional[datetime] = Field(None, description="过期时间(过程审核自动销毁)")
|
||||||
|
|
||||||
|
# 附加信息
|
||||||
|
context_data: Optional[Dict[str, Any]] = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
description="附加上下文数据"
|
||||||
|
)
|
||||||
|
suggestion: Optional[str] = Field("", description="修改建议")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 请求/响应模型
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
class ProcessReviewRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
过程中审核请求 - 由Agent推送
|
||||||
|
"""
|
||||||
|
project_id: str
|
||||||
|
issue_type: str
|
||||||
|
issue_description: str
|
||||||
|
severity: str # low/medium/high
|
||||||
|
location: ReviewTaskLocation
|
||||||
|
trigger_source: ReviewTriggerSource = ReviewTriggerSource.AGENT_AUTO
|
||||||
|
review_config_id: Optional[str] = None
|
||||||
|
skill_id: Optional[str] = None
|
||||||
|
rule_id: Optional[str] = None
|
||||||
|
suggestion: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessReviewResponse(BaseModel):
|
||||||
|
"""
|
||||||
|
过程中审核响应
|
||||||
|
"""
|
||||||
|
task_id: str
|
||||||
|
status: ReviewTaskStatus
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserReviewActionRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
用户审核操作请求
|
||||||
|
"""
|
||||||
|
action: ReviewAction
|
||||||
|
task_id: str
|
||||||
|
user_response: Optional[str] = Field(None, description="用户补充信息")
|
||||||
|
modified_content: Optional[str] = Field(None, description="修改后的内容")
|
||||||
|
|
||||||
|
|
||||||
|
class PostCreationReviewRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
生成后审核请求
|
||||||
|
"""
|
||||||
|
project_id: str
|
||||||
|
episode_id: str
|
||||||
|
use_configured_rules: bool = Field(True, description="是否使用配置的规则")
|
||||||
|
use_configured_skills: bool = Field(True, description="是否使用配置的Skills")
|
||||||
|
dimensions: Optional[List[str]] = Field(None, description="指定审核维度")
|
||||||
|
pass_threshold: Optional[float] = Field(None, description="通过阈值")
|
||||||
|
|
||||||
|
|
||||||
|
class PostCreationReviewResult(BaseModel):
|
||||||
|
"""
|
||||||
|
生成后审核结果
|
||||||
|
"""
|
||||||
|
task_count: int = Field(..., description="生成的任务数")
|
||||||
|
passed: bool = Field(..., description="是否通过审核(没有高严重度问题)")
|
||||||
|
tasks: List[ReviewTask] = Field(default_factory=list, description="审核任务列表")
|
||||||
|
overall_score: Optional[float] = Field(None, description="总体分数")
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,79 +1,3 @@
|
|||||||
{
|
{
|
||||||
"8f969272-4ece-49e7-8ca1-4877cc62c57c": [
|
"8f969272-4ece-49e7-8ca1-4877cc62c57c": []
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成大纲",
|
|
||||||
"timestamp": "2026-01-28T09:37:52.162798"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成大纲",
|
|
||||||
"timestamp": "2026-01-28T10:10:14.049998"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成大纲",
|
|
||||||
"timestamp": "2026-01-28T10:10:21.410520"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成大纲",
|
|
||||||
"timestamp": "2026-01-28T10:10:56.234901"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成大纲",
|
|
||||||
"timestamp": "2026-01-28T10:11:48.922316"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "agent",
|
|
||||||
"content": "我需要先了解项目情况,然后为您生成大纲。让我先查看一下当前项目的文件结构。您好!我已经准备好为您生成大纲了。\n\n为了创建一个符合您期望的大纲,我需要了解一些关键信息:\n\n## 请提供以下信息:\n\n1. **故事类型**:您想创作什么类型的内容?\n - 小说/剧本/网剧/其他\n\n2. **题材/类型**:\n - 悬疑/推理\n - 科幻/奇幻\n - 爱情/都市\n - 历史/古装\n - 喜剧\n - 恐怖/惊悚\n - 其他\n\n3. **核心概念/梗概**:您有一个初步的故事想法吗?请简单描述。\n\n4. **篇幅规模**:\n - 短篇(1-5集/章)\n - 中篇(6-20集/章)\n - 长篇(20集/章以上)\n\n5. **目标受众**:主要面向哪类观众?\n\n请告诉我您的想法,我会根据您的需求制定详细的大纲方案!",
|
|
||||||
"timestamp": "2026-01-28T10:13:01.604878"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成大纲",
|
|
||||||
"timestamp": "2026-01-28T10:19:32.449321"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始创作第三集",
|
|
||||||
"timestamp": "2026-01-28T15:17:45.959233"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成第3集大纲",
|
|
||||||
"timestamp": "2026-01-28T16:47:54.452228"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "agent",
|
|
||||||
"content": "\n我来帮你生成第3集的大纲。首先让我查看一下当前项目的进度。\n\n我来为第3集制定创作计划并生成大纲。\n\n现在让我创建第3集。根据项目协议,当你说\"开始创作第X集\"时,我应该使用 `create_episode` 工具来自动完成整个创建过程。\n\n系统正在后台自动创建第3集,包括大纲、内容创作和质量审核。根据项目协议,这种创作过程会自动通过WebSocket更新进度,最终第3集的内容将显示在画布上。\n\n让我继续关注创作进度:",
|
|
||||||
"timestamp": "2026-01-28T16:49:00.614398"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成第3集大纲",
|
|
||||||
"timestamp": "2026-01-28T17:46:38.247766"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "agent",
|
|
||||||
"content": "我来开始生成第3集的大纲。首先让我创建一个执行计划。现在让我基于全局设定来创作第3集的大纲。## ✅ 第3集大纲已生成\n\n我已经完成了第3集《意外相遇》的大纲创作,大纲已呈现在画布上。\n\n**本集核心情节:**\n- 苏瑾月被林墨轩商议婚事之事烦扰\n- 苏老爷催促婚事,女儿巧妙应对拖延时间\n- 前往寺庙途中马车\"意外\"故障\n- 楚云飞路过出手相助,两人初次相遇\n- 楚云飞修好马车但收钱时展现江湖本色\n- 林墨轩对楚云飞起疑\n- 苏瑾月内心开始动摇,对这位江湖车夫产生好奇\n\n**创作亮点:**\n1. ✨ 体现了苏瑾月的机智与临危不乱\n2. ✨ 建立了楚云飞的侠义形象,暗示其真实身份\n3. ✨ 埋下苏瑾月与楚云飞情感发展的种子\n4. ✨ 强化林墨轩的反派色彩\n\n**已提交审阅:** 我已将大纲提交到您的收件箱,请您审阅并反馈是否需要调整。\n\n您可以:\n- 批准大纲 → 我将开始创作正式剧本\n- 要求修改 → 告诉我具体的修改意见\n- 重新创作 → 我会从新的角度重新设计\n\n期待您的反馈!",
|
|
||||||
"timestamp": "2026-01-28T17:48:50.985473"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "批准大纲",
|
|
||||||
"timestamp": "2026-01-28T17:49:11.764238"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "agent",
|
|
||||||
"content": "我先查看一下项目的当前状态,看看有什么大纲需要批准。我看到项目中前三集已完成,后续剧集待创作。不过我需要确认一下:您要批准的是哪个大纲呢?\n\n目前可能的情况:\n1. **全局大纲**(项目上下文中的Overall Outline目前显示为\"11\",似乎不完整)\n2. **某个特定剧集的大纲**\n\n您是指:\n- 如果是全局大纲,我可以帮您完善并提交审批\n- 如果是某个待定剧集的大纲,请告诉我是第几集,我可以立即开始创作并生成大纲供您审批\n\n请指明具体要批准的内容,我会立即为您处理!",
|
|
||||||
"timestamp": "2026-01-28T17:49:37.624535"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "开始生成第3集大纲",
|
|
||||||
"timestamp": "2026-01-28T18:30:44.338922"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@ -17,60 +17,107 @@
|
|||||||
},
|
},
|
||||||
"memory": {
|
"memory": {
|
||||||
"eventTimeline": [],
|
"eventTimeline": [],
|
||||||
"pendingThreads": [
|
"pendingThreads": [],
|
||||||
{
|
"foreshadowing": [],
|
||||||
"id": "881661a2-7372-41ce-9f7a-ed77efe5d92c",
|
"characterStates": {}
|
||||||
"description": "这个创作失败的原因是什么?是技术故障还是人为因素?",
|
},
|
||||||
"introduced_at": 2,
|
"totalEpisodes": 30,
|
||||||
"importance": "high",
|
"defaultTaskSkills": [],
|
||||||
"resolved": false,
|
"episodeSkillOverrides": {},
|
||||||
"resolved_at": null,
|
"skillSettings": {},
|
||||||
"reminder_episode": 3,
|
"autoRetryConfig": null,
|
||||||
"status": "pending",
|
"reviewConfig": {
|
||||||
"characters_involved": [],
|
"enabled_review_skills": [],
|
||||||
"notes": "",
|
"overall_strictness": 0.83,
|
||||||
"created_at": "2026-01-28T16:49:57.524646",
|
"dimension_settings": {
|
||||||
"updated_at": "2026-01-28T16:49:57.524646"
|
"consistency": {
|
||||||
|
"enabled": false,
|
||||||
|
"strictness": 0.95,
|
||||||
|
"custom_rules": [],
|
||||||
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
"quality": {
|
||||||
"id": "015785d6-6f16-4a2f-8260-a8c3515a41c2",
|
"enabled": true,
|
||||||
"description": "这个错误是否会影响后续剧情发展?是否会成为解开某个谜题的关键?",
|
"strictness": 0.9,
|
||||||
"introduced_at": 2,
|
"custom_rules": [],
|
||||||
"importance": "medium",
|
"weight": 0.8
|
||||||
"resolved": false,
|
|
||||||
"resolved_at": null,
|
|
||||||
"reminder_episode": 4,
|
|
||||||
"status": "pending",
|
|
||||||
"characters_involved": [],
|
|
||||||
"notes": "",
|
|
||||||
"created_at": "2026-01-28T16:49:57.524646",
|
|
||||||
"updated_at": "2026-01-28T16:49:57.524646"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreshadowing": [
|
|
||||||
{
|
|
||||||
"id": "e16f93c7-cc24-47b7-9c58-5fc84fba4409",
|
|
||||||
"description": "第2集内容创作失败的错误信息可能暗示剧集制作过程中存在技术问题或人为错误",
|
|
||||||
"introduced_at": 2,
|
|
||||||
"payoff_at": null,
|
|
||||||
"type": "plot",
|
|
||||||
"importance": "medium",
|
|
||||||
"is_payed_off": false,
|
|
||||||
"related_events": [],
|
|
||||||
"notes": ""
|
|
||||||
},
|
},
|
||||||
{
|
"dialogue": {
|
||||||
"id": "8b43d627-9107-430b-aa0f-6d1052ad1511",
|
"enabled": false,
|
||||||
"description": "'str' object has no attribute 'items'这个技术错误可能是一个元叙事元素,暗示剧集本身可能存在问题",
|
"strictness": 0.5,
|
||||||
"introduced_at": 2,
|
"custom_rules": [],
|
||||||
"payoff_at": null,
|
"weight": 0.7
|
||||||
"type": "plot",
|
},
|
||||||
"importance": "medium",
|
"plot": {
|
||||||
"is_payed_off": false,
|
"enabled": false,
|
||||||
"related_events": [],
|
"strictness": 0.5,
|
||||||
"notes": ""
|
"custom_rules": [],
|
||||||
|
"weight": 0.7
|
||||||
|
},
|
||||||
|
"pacing": {
|
||||||
|
"enabled": false,
|
||||||
|
"strictness": 0.5,
|
||||||
|
"custom_rules": [],
|
||||||
|
"weight": 0.7
|
||||||
|
},
|
||||||
|
"emotional_depth": {
|
||||||
|
"enabled": false,
|
||||||
|
"strictness": 0.5,
|
||||||
|
"custom_rules": [],
|
||||||
|
"weight": 0.7
|
||||||
|
},
|
||||||
|
"thematic_strength": {
|
||||||
|
"enabled": false,
|
||||||
|
"strictness": 0.5,
|
||||||
|
"custom_rules": [],
|
||||||
|
"weight": 0.7
|
||||||
|
},
|
||||||
|
"character_consistency": {
|
||||||
|
"enabled": false,
|
||||||
|
"strictness": 0.95,
|
||||||
|
"custom_rules": [],
|
||||||
|
"weight": 1.0
|
||||||
|
},
|
||||||
|
"dialogue_quality": {
|
||||||
|
"enabled": false,
|
||||||
|
"strictness": 0.5,
|
||||||
|
"custom_rules": [],
|
||||||
|
"weight": 0.7
|
||||||
|
},
|
||||||
|
"plot_coherence": {
|
||||||
|
"enabled": false,
|
||||||
|
"strictness": 0.5,
|
||||||
|
"custom_rules": [],
|
||||||
|
"weight": 0.7
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
|
"custom_rules": [],
|
||||||
|
"auto_fix_enabled": false,
|
||||||
|
"pass_threshold": 90.0
|
||||||
|
},
|
||||||
|
"createdAt": "2026-01-27T16:22:58.755260",
|
||||||
|
"updatedAt": "2026-01-30T18:27:40.432141"
|
||||||
|
},
|
||||||
|
"240eca7f-2587-4380-88d4-081818c6a661": {
|
||||||
|
"id": "240eca7f-2587-4380-88d4-081818c6a661",
|
||||||
|
"name": "test1",
|
||||||
|
"type": "series",
|
||||||
|
"agentId": "series-creation",
|
||||||
|
"mode": "batch",
|
||||||
|
"genre": "古风",
|
||||||
|
"globalContext": {
|
||||||
|
"worldSetting": "",
|
||||||
|
"characterProfiles": {},
|
||||||
|
"sceneSettings": {},
|
||||||
|
"overallOutline": "",
|
||||||
|
"styleGuide": "",
|
||||||
|
"uploadedScript": "11",
|
||||||
|
"inspiration": ""
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"eventTimeline": [],
|
||||||
|
"pendingThreads": [],
|
||||||
|
"foreshadowing": [],
|
||||||
"characterStates": {}
|
"characterStates": {}
|
||||||
},
|
},
|
||||||
"totalEpisodes": 30,
|
"totalEpisodes": 30,
|
||||||
@ -79,7 +126,7 @@
|
|||||||
"skillSettings": {},
|
"skillSettings": {},
|
||||||
"autoRetryConfig": null,
|
"autoRetryConfig": null,
|
||||||
"reviewConfig": null,
|
"reviewConfig": null,
|
||||||
"createdAt": "2026-01-27T16:22:58.755260",
|
"createdAt": "2026-01-29T15:12:51.442460",
|
||||||
"updatedAt": "2026-01-28T16:51:40.612879"
|
"updatedAt": "2026-01-29T15:12:51.442460"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
67
backend/scripts/fix_episodes_data.py
Normal file
67
backend/scripts/fix_episodes_data.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
修复episodes.json数据问题
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def fix_episodes_data():
|
||||||
|
"""修复episodes.json中的问题数据"""
|
||||||
|
episodes_file = Path(__file__).parent.parent / "data" / "episodes.json"
|
||||||
|
|
||||||
|
if not episodes_file.exists():
|
||||||
|
print(f"episodes.json not found at {episodes_file}")
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(episodes_file, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
fixed_count = 0
|
||||||
|
|
||||||
|
for episode_id, episode in data.items():
|
||||||
|
modified = False
|
||||||
|
|
||||||
|
# 修复EP1的标题
|
||||||
|
if episode.get("number") == 1 and episode.get("title") == "11":
|
||||||
|
episode["title"] = "苏园初会"
|
||||||
|
modified = True
|
||||||
|
fixed_count += 1
|
||||||
|
|
||||||
|
# 修复EP2的错误内容
|
||||||
|
if episode.get("number") == 2 and "内容创作失败" in str(episode.get("content", "")):
|
||||||
|
episode["content"] = ""
|
||||||
|
episode["status"] = "pending"
|
||||||
|
modified = True
|
||||||
|
fixed_count += 1
|
||||||
|
|
||||||
|
# 确保所有pending剧集有正确的结构
|
||||||
|
if episode.get("status") == "pending":
|
||||||
|
if not episode.get("outline"):
|
||||||
|
episode["outline"] = None
|
||||||
|
if not episode.get("content"):
|
||||||
|
episode["content"] = ""
|
||||||
|
if not episode.get("structure"):
|
||||||
|
episode["structure"] = {
|
||||||
|
"episodeNumber": episode.get("number"),
|
||||||
|
"scenes": [],
|
||||||
|
"keyEvents": []
|
||||||
|
}
|
||||||
|
modified = True
|
||||||
|
fixed_count += 1
|
||||||
|
|
||||||
|
# 修复qualityScore为None的completed剧集
|
||||||
|
if episode.get("status") == "completed" and episode.get("qualityScore") is None:
|
||||||
|
episode["qualityScore"] = 0.0
|
||||||
|
modified = True
|
||||||
|
fixed_count += 1
|
||||||
|
|
||||||
|
# 保存修复后的数据
|
||||||
|
with open(episodes_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"修复完成!共修复 {fixed_count} 个问题")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fix_episodes_data()
|
||||||
@ -1,23 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { ConfigProvider, Layout, Menu, theme } from 'antd'
|
import { Layout, Menu, App as AntApp, theme } from 'antd'
|
||||||
import { DashboardOutlined, BookOutlined, SettingOutlined } from '@ant-design/icons'
|
import { DashboardOutlined, BookOutlined, SettingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { ProjectList } from './pages/ProjectList'
|
import { ProjectList } from './pages/ProjectList'
|
||||||
import { ProjectCreateEnhanced } from './pages/ProjectCreateEnhanced'
|
import { ProjectCreateEnhanced } from './pages/ProjectCreateEnhanced'
|
||||||
import { ProjectCreateProgressive } from './pages/ProjectCreateProgressive'
|
import { ProjectCreateProgressive } from './pages/ProjectCreateProgressive'
|
||||||
import { ProjectDetail } from './pages/ProjectDetail'
|
import { ProjectDetail } from './pages/ProjectDetail'
|
||||||
import { ProjectWorkspace } from './pages/ProjectWorkspace'
|
|
||||||
import { SkillManagement } from './pages/SkillManagement'
|
import { SkillManagement } from './pages/SkillManagement'
|
||||||
import { AgentManagement } from './pages/AgentManagement'
|
import { AgentManagement } from './pages/AgentManagement'
|
||||||
import { ExecutionMonitor } from './pages/ExecutionMonitor'
|
import { ExecutionMonitor } from './pages/ExecutionMonitor'
|
||||||
import MemorySystem from './pages/MemorySystem'
|
|
||||||
import ReviewConfig from './pages/ReviewConfig'
|
|
||||||
import ReviewResults from './pages/ReviewResults'
|
|
||||||
|
|
||||||
const { Header, Content } = Layout
|
const { Header, Content } = Layout
|
||||||
|
|
||||||
function App() {
|
function AppRoot() {
|
||||||
const currentPath = window.location.pathname
|
const currentPath = window.location.pathname
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
@ -27,14 +23,7 @@ function App() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<AntApp>
|
||||||
theme={{
|
|
||||||
algorithm: theme.defaultAlgorithm,
|
|
||||||
token: {
|
|
||||||
colorPrimary: '#0ea5e9',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BrowserRouter
|
<BrowserRouter
|
||||||
future={{
|
future={{
|
||||||
v7_startTransition: true,
|
v7_startTransition: true,
|
||||||
@ -75,21 +64,19 @@ function App() {
|
|||||||
<Route path="/projects" element={<ProjectList />} />
|
<Route path="/projects" element={<ProjectList />} />
|
||||||
<Route path="/projects/new" element={<ProjectCreateEnhanced />} />
|
<Route path="/projects/new" element={<ProjectCreateEnhanced />} />
|
||||||
<Route path="/projects/progressive" element={<ProjectCreateProgressive />} />
|
<Route path="/projects/progressive" element={<ProjectCreateProgressive />} />
|
||||||
{/* 更具体的路由要放在前面 */}
|
{/* 执行监控路由 */}
|
||||||
<Route path="/projects/:id/workspace" element={<ProjectWorkspace />} />
|
|
||||||
<Route path="/projects/:id/execute" element={<ExecutionMonitor />} />
|
<Route path="/projects/:id/execute" element={<ExecutionMonitor />} />
|
||||||
<Route path="/projects/:id/memory" element={<MemorySystem />} />
|
{/* 项目详情页(包含所有功能:设置、全局设定、剧集创作、记忆系统、审核系统) */}
|
||||||
<Route path="/projects/:id/review/config" element={<ReviewConfig />} />
|
|
||||||
<Route path="/projects/:id/review/results" element={<ReviewResults />} />
|
|
||||||
<Route path="/projects/:id" element={<ProjectDetail />} />
|
<Route path="/projects/:id" element={<ProjectDetail />} />
|
||||||
|
{/* 全局管理页面 */}
|
||||||
<Route path="/skills" element={<SkillManagement />} />
|
<Route path="/skills" element={<SkillManagement />} />
|
||||||
<Route path="/agents" element={<AgentManagement />} />
|
<Route path="/agents" element={<AgentManagement />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ConfigProvider>
|
</AntApp>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default AppRoot
|
||||||
|
|||||||
@ -188,6 +188,8 @@ export const SkillCreate: React.FC<SkillCreateProps> = ({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(`加载Skill失败: ${(error as Error).message}`)
|
message.error(`加载Skill失败: ${(error as Error).message}`)
|
||||||
|
// 关闭弹窗,让父组件处理重定向
|
||||||
|
handleClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,7 +546,7 @@ export const SkillCreate: React.FC<SkillCreateProps> = ({
|
|||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
footer={null}
|
footer={null}
|
||||||
width={1000}
|
width={1000}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Layout, Card, Typography, Space, Tag, Tabs, List, Button, Input, Empty, Timeline, Badge, Popconfirm } from 'antd';
|
import { Card, Typography, Space, Tabs, List, Button, Empty, Timeline, Badge } from 'antd';
|
||||||
import {
|
import {
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@ -12,7 +12,6 @@ import {
|
|||||||
CheckCircleOutlined
|
CheckCircleOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
const { Sider } = Layout;
|
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
|
||||||
interface ContextPanelProps {
|
interface ContextPanelProps {
|
||||||
@ -49,134 +48,142 @@ export const ContextPanel: React.FC<ContextPanelProps> = ({
|
|||||||
{ type: 'location', value: '未初始化' }
|
{ type: 'location', value: '未初始化' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 渲染内容
|
||||||
|
const renderContent = () => (
|
||||||
|
<div style={{ width: '100%', minWidth: 0, overflow: 'hidden' }}>
|
||||||
|
<Title level={4} style={{ width: '100%', minWidth: 0 }}>
|
||||||
|
<BookOutlined /> 故事上下文
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{/* 动态状态卡片 */}
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title="当前状态 (Active State)"
|
||||||
|
style={{ marginBottom: '16px', background: '#f6ffed', borderColor: '#b7eb8f', width: '100%', minWidth: 0 }}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{displayStates.map((state, idx) => (
|
||||||
|
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between', minWidth: 0, gap: '8px' }}>
|
||||||
|
<Text type="secondary" style={{ flexShrink: 0 }}>{state.type === 'time' ? <HistoryOutlined /> : state.type === 'location' ? <EnvironmentOutlined /> : <UserOutlined />}</Text>
|
||||||
|
<Text strong style={{ textAlign: 'right', flex: 1, wordBreak: 'break-word' }}>{state.value}</Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Tabs activeKey={activeTab} onChange={setActiveTab} style={{ width: '100%', minWidth: 0 }}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'world',
|
||||||
|
label: '世界观',
|
||||||
|
children: (
|
||||||
|
<div style={{ width: '100%', minWidth: 0 }}>
|
||||||
|
<Paragraph ellipsis={{ rows: 6, expandable: true, symbol: '展开' }}>
|
||||||
|
{worldSetting}
|
||||||
|
</Paragraph>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={onNavigateToSettings}
|
||||||
|
>
|
||||||
|
更新设定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'characters',
|
||||||
|
label: '人物',
|
||||||
|
children: (
|
||||||
|
<div style={{ width: '100%', minWidth: 0 }}>
|
||||||
|
{/* 如果有文本格式的人物设定,优先显示 */}
|
||||||
|
{charactersText ? (
|
||||||
|
<Paragraph
|
||||||
|
ellipsis={{ rows: 10, expandable: true, symbol: '展开' }}
|
||||||
|
style={{ fontSize: '12px', whiteSpace: 'pre-wrap' }}
|
||||||
|
>
|
||||||
|
{charactersText}
|
||||||
|
</Paragraph>
|
||||||
|
) : Object.keys(characters).length > 0 ? (
|
||||||
|
<List
|
||||||
|
dataSource={Object.entries(characters)}
|
||||||
|
renderItem={([name, profile]: [string, any]) => (
|
||||||
|
<List.Item>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={<UserOutlined style={{ fontSize: '24px', color: '#1890ff' }} />}
|
||||||
|
title={<span style={{ wordBreak: 'break-word' }}>{name}</span>}
|
||||||
|
description={<Text ellipsis={{ tooltip: profile }} style={{ wordBreak: 'break-word' }}>{profile}</Text>}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty description="暂无人物设定" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
style={{ marginTop: '8px' }}
|
||||||
|
onClick={onNavigateToSettings}
|
||||||
|
>
|
||||||
|
更新设定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'outline',
|
||||||
|
label: '大纲',
|
||||||
|
children: (
|
||||||
|
<div style={{ width: '100%', minWidth: 0 }}>
|
||||||
|
<Paragraph ellipsis={{ rows: 10, expandable: true, symbol: '展开' }} style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{overallOutline}
|
||||||
|
</Paragraph>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
style={{ marginTop: '8px' }}
|
||||||
|
onClick={onNavigateToSettings}
|
||||||
|
>
|
||||||
|
更新设定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'memory',
|
||||||
|
label: '记忆库',
|
||||||
|
children: memoryItems.length > 0 ? (
|
||||||
|
<MemoryLibrary items={memoryItems} />
|
||||||
|
) : (
|
||||||
|
<EmptyMemoryState />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用统一的布局模式(移除 Sider 组件,改用普通 div)
|
||||||
return (
|
return (
|
||||||
<Sider
|
<div
|
||||||
width={width}
|
|
||||||
theme="light"
|
|
||||||
style={{
|
style={{
|
||||||
borderRight: '1px solid #f0f0f0',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflowY: 'auto',
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
flexShrink: 0
|
overflow: 'hidden',
|
||||||
|
minWidth: 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ padding: '16px' }}>
|
<div style={{ padding: '16px', flex: 1, overflowY: 'auto', minWidth: 0 }}>
|
||||||
<Title level={4}>
|
{renderContent()}
|
||||||
<BookOutlined /> 故事上下文
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
{/* 动态状态卡片 */}
|
|
||||||
<Card
|
|
||||||
size="small"
|
|
||||||
title="当前状态 (Active State)"
|
|
||||||
style={{ marginBottom: '16px', background: '#f6ffed', borderColor: '#b7eb8f' }}
|
|
||||||
>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
{displayStates.map((state, idx) => (
|
|
||||||
<div key={idx} style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<Text type="secondary">{state.type === 'time' ? <HistoryOutlined /> : state.type === 'location' ? <EnvironmentOutlined /> : <UserOutlined />}</Text>
|
|
||||||
<Text strong>{state.value}</Text>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Tabs activeKey={activeTab} onChange={setActiveTab}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: 'world',
|
|
||||||
label: '世界观',
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
<Paragraph ellipsis={{ rows: 6, expandable: true, symbol: '展开' }}>
|
|
||||||
{worldSetting}
|
|
||||||
</Paragraph>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
block
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={onNavigateToSettings}
|
|
||||||
>
|
|
||||||
更新设定
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'characters',
|
|
||||||
label: '人物',
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
{/* 如果有文本格式的人物设定,优先显示 */}
|
|
||||||
{charactersText ? (
|
|
||||||
<Paragraph
|
|
||||||
ellipsis={{ rows: 10, expandable: true, symbol: '展开' }}
|
|
||||||
style={{ fontSize: '12px', whiteSpace: 'pre-wrap' }}
|
|
||||||
>
|
|
||||||
{charactersText}
|
|
||||||
</Paragraph>
|
|
||||||
) : Object.keys(characters).length > 0 ? (
|
|
||||||
<List
|
|
||||||
dataSource={Object.entries(characters)}
|
|
||||||
renderItem={([name, profile]: [string, any]) => (
|
|
||||||
<List.Item>
|
|
||||||
<List.Item.Meta
|
|
||||||
avatar={<UserOutlined style={{ fontSize: '24px', color: '#1890ff' }} />}
|
|
||||||
title={name}
|
|
||||||
description={<Text ellipsis>{profile}</Text>}
|
|
||||||
/>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Empty description="暂无人物设定" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
block
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
style={{ marginTop: '8px' }}
|
|
||||||
onClick={onNavigateToSettings}
|
|
||||||
>
|
|
||||||
更新设定
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'outline',
|
|
||||||
label: '大纲',
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
<Paragraph ellipsis={{ rows: 10, expandable: true, symbol: '展开' }} style={{ whiteSpace: 'pre-wrap' }}>
|
|
||||||
{overallOutline}
|
|
||||||
</Paragraph>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
block
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
style={{ marginTop: '8px' }}
|
|
||||||
onClick={onNavigateToSettings}
|
|
||||||
>
|
|
||||||
更新设定
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memory',
|
|
||||||
label: '记忆库',
|
|
||||||
children: memoryItems.length > 0 ? (
|
|
||||||
<MemoryLibrary items={memoryItems} />
|
|
||||||
) : (
|
|
||||||
<EmptyMemoryState />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Sider>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Layout, Input, List, Avatar, Button, Card, Tag, Badge, Tooltip, Divider } from 'antd';
|
import { Input, List, Avatar, Button, Card, Tag, Badge, Tooltip, Divider, Space, Popconfirm, Modal, message } from 'antd';
|
||||||
import {
|
import {
|
||||||
SendOutlined,
|
SendOutlined,
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
@ -8,49 +8,123 @@ import {
|
|||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
StopOutlined
|
StopOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
ClearOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
WarningOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
const { Sider } = Layout;
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
export interface InboxItem {
|
export interface InboxItem {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'review' | 'decision' | 'notification' | 'review_card';
|
type: 'review' | 'decision' | 'notification' | 'review_card' | 'confirm_card';
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
status: 'pending' | 'approved' | 'rejected' | 'ignored';
|
status: 'pending' | 'approved' | 'rejected' | 'ignored';
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
options?: string[];
|
options?: Array<{ id: string; label: string; description?: string; implications?: string }>;
|
||||||
reviewData?: any;
|
reviewData?: any;
|
||||||
|
confirmData?: {
|
||||||
|
card_id: string;
|
||||||
|
allow_custom_response: boolean;
|
||||||
|
expires_at?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DirectorInboxProps {
|
interface DirectorInboxProps {
|
||||||
onSendMessage: (message: string) => void;
|
onSendMessage: (message: string) => void;
|
||||||
onStopAgent?: () => void;
|
onStopAgent?: () => void;
|
||||||
onInboxAction?: (itemId: string, action: 'approve' | 'reject') => void;
|
onInboxAction?: (itemId: string, action: 'approve' | 'reject') => void;
|
||||||
|
onConfirmCardAction?: (itemId: string, action: 'confirm' | 'reject', selectedOptionId?: string, customResponse?: string) => void;
|
||||||
onNavigateToReview?: (episodeNum: number) => void;
|
onNavigateToReview?: (episodeNum: number) => void;
|
||||||
|
onResetConversation?: () => void; // 新增:重建对话回调
|
||||||
agentStatus: 'idle' | 'planning' | 'writing' | 'reviewing';
|
agentStatus: 'idle' | 'planning' | 'writing' | 'reviewing';
|
||||||
agentPlan?: string[];
|
agentPlan?: string[];
|
||||||
inboxItems?: InboxItem[];
|
inboxItems?: InboxItem[];
|
||||||
chatHistory?: {role: 'user' | 'agent', content: string}[];
|
chatHistory?: {role: 'user' | 'agent', content: string}[];
|
||||||
width?: number;
|
width?: number;
|
||||||
|
wsRef?: React.RefObject<WebSocket | null>; // 新增:WebSocket引用
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
onStopAgent,
|
onStopAgent,
|
||||||
onInboxAction,
|
onInboxAction,
|
||||||
|
onConfirmCardAction,
|
||||||
onNavigateToReview,
|
onNavigateToReview,
|
||||||
|
onResetConversation, // 新增:重建对话回调
|
||||||
agentStatus,
|
agentStatus,
|
||||||
agentPlan = [],
|
agentPlan = [],
|
||||||
inboxItems = [],
|
inboxItems = [],
|
||||||
chatHistory = [],
|
chatHistory = [],
|
||||||
width = 400
|
width = 400,
|
||||||
|
wsRef // 新增:WebSocket引用
|
||||||
}) => {
|
}) => {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
// Use local state for immediate feedback, but sync with props if provided
|
// Use local state for immediate feedback, but sync with props if provided
|
||||||
const [localMessages, setLocalMessages] = useState<{role: 'user' | 'agent', content: string}[]>([]);
|
const [localMessages, setLocalMessages] = useState<{role: 'user' | 'agent', content: string}[]>([]);
|
||||||
|
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
||||||
|
|
||||||
|
// 新增:处理重建对话(保留上下文,清除显示)
|
||||||
|
const handleResetConversation = () => {
|
||||||
|
// 清空本地消息,保留欢迎语
|
||||||
|
setLocalMessages([{ role: 'agent', content: '导演你好,我是你的 AI 助手。' }]);
|
||||||
|
|
||||||
|
// 通过WebSocket发送重建对话请求
|
||||||
|
if (wsRef && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.send(JSON.stringify({
|
||||||
|
type: 'reset_conversation'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用父组件回调
|
||||||
|
onResetConversation?.();
|
||||||
|
|
||||||
|
message.success('对话已刷新,上下文已保留');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:完全清除对话(包括上下文)
|
||||||
|
const handleClearConversation = () => {
|
||||||
|
setShowClearConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmClearConversation = () => {
|
||||||
|
// 清空本地消息
|
||||||
|
setLocalMessages([{ role: 'agent', content: '导演你好,我是你的 AI 助手。' }]);
|
||||||
|
|
||||||
|
// 通过WebSocket发送清除对话请求(完全重置)
|
||||||
|
if (wsRef && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.send(JSON.stringify({
|
||||||
|
type: 'clear_conversation'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用父组件回调
|
||||||
|
onResetConversation?.();
|
||||||
|
|
||||||
|
setShowClearConfirm(false);
|
||||||
|
message.success('对话已完全清除,上下文已重置');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查对话长度,超过阈值提醒用户
|
||||||
|
useEffect(() => {
|
||||||
|
const MESSAGE_THRESHOLD = 30; // 超过30条消息提醒
|
||||||
|
if (localMessages.length >= MESSAGE_THRESHOLD && localMessages.length === MESSAGE_THRESHOLD) {
|
||||||
|
// 只有 Agent 才能发送提醒
|
||||||
|
const hasReminder = localMessages.some(m =>
|
||||||
|
m.role === 'agent' && m.content.includes('对话记录较长,建议您')
|
||||||
|
);
|
||||||
|
if (!hasReminder) {
|
||||||
|
message.warning({
|
||||||
|
content: '对话记录较长,建议您刷新或清除对话以获得更好性能',
|
||||||
|
duration: 5,
|
||||||
|
icon: <WarningOutlined style={{ color: '#faad14' }} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [localMessages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 确保 chatHistory 是数组
|
// 确保 chatHistory 是数组
|
||||||
@ -102,24 +176,28 @@ export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sider
|
<div
|
||||||
width={width}
|
|
||||||
theme="light"
|
|
||||||
style={{
|
style={{
|
||||||
borderLeft: '1px solid #f0f0f0',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
background: '#fff',
|
background: '#fafbfc',
|
||||||
flexShrink: 0
|
border: '1px solid #e0e4e7',
|
||||||
|
borderLeft: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: '0 8px 8px 0',
|
||||||
|
boxShadow: 'inset 1px 0 0 #e5e7eb'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
{/* Agent 状态与计划 */}
|
{/* Agent 状态与计划 */}
|
||||||
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0', background: '#fafafa', flexShrink: 0 }}>
|
<div style={{ padding: '16px', borderBottom: '2px solid #e0e4e7', background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)', flexShrink: 0, boxShadow: '0 1px 3px rgba(0,0,0,0.04)' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '8px' }}>
|
||||||
<Avatar icon={<RobotOutlined />} style={{ backgroundColor: '#1890ff', marginRight: '8px' }} />
|
<Avatar icon={<RobotOutlined />} style={{ backgroundColor: '#1890ff', marginRight: '8px' }} />
|
||||||
<div>
|
<div>
|
||||||
@ -143,73 +221,192 @@ export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 导演信箱 (Inbox) */}
|
{/* 导演信箱 (Inbox) */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px 16px 0 16px',
|
display: 'flex',
|
||||||
background: '#f0f2f5',
|
flexDirection: 'column',
|
||||||
maxHeight: '40%',
|
maxHeight: '40%',
|
||||||
overflowY: 'auto',
|
background: '#f5f6f8',
|
||||||
flexShrink: 0,
|
borderBottom: '1px solid #e0e4e7',
|
||||||
borderBottom: '1px solid #e8e8e8'
|
flexShrink: 0
|
||||||
}}>
|
}}>
|
||||||
<Divider orientation="left" style={{ margin: '0 0 16px 0', fontSize: '12px' }}>待处理任务 (Inbox)</Divider>
|
<div style={{ padding: '8px 12px', background: '#f0f2f5', borderBottom: '1px solid #e8e8e8', flexShrink: 0 }}>
|
||||||
|
<Divider orientation="left" style={{ margin: 0, fontSize: '12px', borderTopColor: '#d9d9d9' }}>待处理任务 (Inbox)</Divider>
|
||||||
{Array.isArray(inboxItems) && inboxItems.map(item => (
|
</div>
|
||||||
<Card
|
<div style={{
|
||||||
key={item.id}
|
padding: '12px 16px',
|
||||||
size="small"
|
overflowY: 'auto',
|
||||||
style={{ marginBottom: '8px', borderLeft: item.type === 'review_card' ? '3px solid #1890ff' : '3px solid #faad14', cursor: item.type === 'review_card' ? 'pointer' : 'default' }}
|
overflowX: 'hidden',
|
||||||
onClick={() => handleInboxItemClick(item)}
|
flex: 1
|
||||||
hoverable={item.type === 'review_card'}
|
}}>
|
||||||
actions={item.type === 'review_card' ? [
|
{Array.isArray(inboxItems) && inboxItems.map(item => (
|
||||||
<Button type="link" size="small" onClick={(e) => { e.stopPropagation(); handleInboxItemClick(item); }}>查看详情</Button>
|
<Card
|
||||||
] : [
|
key={item.id}
|
||||||
<Tooltip title="批准/确认"><Button type="text" size="small" icon={<CheckCircleOutlined style={{ color: '#52c41a' }} />} onClick={() => onInboxAction?.(item.id, 'approve')} /></Tooltip>,
|
size="small"
|
||||||
<Tooltip title="拒绝/修改"><Button type="text" size="small" icon={<CloseCircleOutlined style={{ color: '#ff4d4f' }} />} onClick={() => onInboxAction?.(item.id, 'reject')} /></Tooltip>
|
style={{
|
||||||
]}
|
marginBottom: '8px',
|
||||||
>
|
borderLeft: item.type === 'review_card' ? '3px solid #1890ff' :
|
||||||
<Card.Meta
|
item.type === 'confirm_card' ? '3px solid #52c41a' : '3px solid #faad14',
|
||||||
avatar={item.type === 'review_card' ? <CheckCircleOutlined style={{ color: '#1890ff', fontSize: '20px' }} /> : <ExclamationCircleOutlined style={{ color: '#faad14', fontSize: '20px' }} />}
|
cursor: item.type === 'review_card' ? 'pointer' : 'default',
|
||||||
title={<span style={{ fontSize: '14px' }}>{item.title}</span>}
|
borderRadius: '6px'
|
||||||
description={<span style={{ fontSize: '12px' }}>{item.description}</span>}
|
}}
|
||||||
/>
|
onClick={() => item.type !== 'confirm_card' ? handleInboxItemClick(item) : undefined}
|
||||||
</Card>
|
hoverable={item.type === 'review_card'}
|
||||||
))}
|
actions={
|
||||||
|
item.type === 'review_card' ? [] :
|
||||||
|
item.type === 'confirm_card' ? [
|
||||||
|
<Tooltip key="reject" title="拒绝">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CloseCircleOutlined style={{ color: '#ff4d4f' }} />}
|
||||||
|
onClick={() => onConfirmCardAction?.(item.id, 'reject')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
] : [
|
||||||
|
<Tooltip key="approve" title="批准/确认">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CheckCircleOutlined style={{ color: '#52c41a' }} />}
|
||||||
|
onClick={() => onInboxAction?.(item.id, 'approve')}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip key="reject" title="拒绝/修改">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<CloseCircleOutlined style={{ color: '#ff4d4f' }} />}
|
||||||
|
onClick={() => onInboxAction?.(item.id, 'reject')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Card.Meta
|
||||||
|
avatar={
|
||||||
|
item.type === 'review_card' ? <CheckCircleOutlined style={{ color: '#1890ff', fontSize: '20px' }} /> :
|
||||||
|
item.type === 'confirm_card' ? <CheckCircleOutlined style={{ color: '#52c41a', fontSize: '20px' }} /> :
|
||||||
|
<ExclamationCircleOutlined style={{ color: '#faad14', fontSize: '20px' }} />
|
||||||
|
}
|
||||||
|
title={<span style={{ fontSize: '14px', fontWeight: 600 }}>{item.title}</span>}
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666', wordBreak: 'break-word' }}>{item.description}</div>
|
||||||
|
{item.type === 'confirm_card' && item.options && item.options.length > 0 && (
|
||||||
|
<div style={{ marginTop: '12px' }}>
|
||||||
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||||
|
{item.options.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.id}
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
block
|
||||||
|
style={{ textAlign: 'left', height: 'auto', padding: '4px 8px' }}
|
||||||
|
onClick={() => onConfirmCardAction?.(item.id, 'confirm', option.id)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
||||||
|
{option.description && (
|
||||||
|
<div style={{ fontSize: '11px', color: '#666', marginTop: '2px' }}>
|
||||||
|
{option.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{(!inboxItems || inboxItems.length === 0) && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '12px', color: '#999', fontSize: '12px' }}>暂无待处理任务</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 对话记录 */}
|
{/* 对话记录 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflowY: 'auto',
|
|
||||||
padding: '16px',
|
|
||||||
background: '#f0f2f5',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column',
|
||||||
|
background: '#f5f6f8',
|
||||||
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
<Divider orientation="left" style={{ margin: '0 0 16px 0', fontSize: '12px' }}>对话记录</Divider>
|
<div style={{
|
||||||
|
padding: '8px 12px',
|
||||||
{Array.isArray(localMessages) && localMessages.map((msg, idx) => (
|
background: '#f0f2f5',
|
||||||
<div key={idx} style={{
|
borderBottom: '1px solid #e8e8e8',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
justifyContent: 'space-between',
|
||||||
marginBottom: '12px'
|
alignItems: 'center',
|
||||||
}}>
|
flexShrink: 0
|
||||||
<div style={{
|
}}>
|
||||||
maxWidth: '80%',
|
<div style={{ display: 'flex', alignItems: 'center', flex: 1 }}>
|
||||||
padding: '8px 12px',
|
<Divider orientation="left" style={{ margin: 0, fontSize: '12px', borderTopColor: '#d9d9d9' }}>对话记录</Divider>
|
||||||
borderRadius: '8px',
|
|
||||||
background: msg.role === 'user' ? '#1890ff' : '#fff',
|
|
||||||
color: msg.role === 'user' ? '#fff' : '#333',
|
|
||||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)'
|
|
||||||
}}>
|
|
||||||
{msg.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<Tooltip title="重建对话(清除历史但保留上下文)">
|
||||||
<div ref={messagesEndRef} />
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<ReloadOutlined style={{ fontSize: '12px' }} />}
|
||||||
|
onClick={handleResetConversation}
|
||||||
|
style={{
|
||||||
|
fontSize: '11px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: '#1890ff',
|
||||||
|
padding: '0 8px',
|
||||||
|
height: '24px',
|
||||||
|
marginLeft: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重建
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
padding: '16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}>
|
||||||
|
{Array.isArray(localMessages) && localMessages.map((msg, idx) => (
|
||||||
|
<div key={idx} style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
||||||
|
marginBottom: '12px',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
maxWidth: '90%',
|
||||||
|
padding: '8px 12px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: msg.role === 'user' ? '#1890ff' : '#fff',
|
||||||
|
color: msg.role === 'user' ? '#fff' : '#333',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.05)',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
fontSize: '13px',
|
||||||
|
lineHeight: '1.5'
|
||||||
|
}}>
|
||||||
|
{msg.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 输入框 */}
|
{/* 输入框 */}
|
||||||
<div style={{ padding: '16px', borderTop: '1px solid #f0f0f0', flexShrink: 0 }}>
|
<div style={{ padding: '16px', borderTop: '2px solid #e0e4e7', background: '#ffffff', flexShrink: 0, boxShadow: '0 -1px 3px rgba(0,0,0,0.04)' }}>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<TextArea
|
<TextArea
|
||||||
rows={2}
|
rows={2}
|
||||||
@ -239,7 +436,37 @@ export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 清除对话确认弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<WarningOutlined style={{ color: '#ff4d4f' }} />
|
||||||
|
<span>确认清除对话</span>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
open={showClearConfirm}
|
||||||
|
onOk={confirmClearConversation}
|
||||||
|
onCancel={() => setShowClearConfirm(false)}
|
||||||
|
okText="确认清除"
|
||||||
|
cancelText="取消"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '16px 0' }}>
|
||||||
|
<p style={{ marginBottom: '12px', color: '#666' }}>
|
||||||
|
您确定要<strong style={{ color: '#ff4d4f' }}>完全清除对话</strong>吗?
|
||||||
|
</p>
|
||||||
|
<ul style={{ paddingLeft: '20px', color: '#666' }}>
|
||||||
|
<li>所有聊天记录将被删除</li>
|
||||||
|
<li>Agent 的上下文将被完全重置</li>
|
||||||
|
<li>当前工作状态将被清空</li>
|
||||||
|
</ul>
|
||||||
|
<p style={{ marginTop: '12px', color: '#999', fontSize: '13px' }}>
|
||||||
|
如果您只是想刷新显示,请使用"刷新"功能代替。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</Sider>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Card, List, Tag, Space, Button, Typography, Progress, Empty, Spin, Badge, Tooltip, message } from 'antd'
|
import { Card, List, Tag, Space, Button, Typography, Progress, Empty, Spin, Badge, Tooltip, message, Segmented } from 'antd'
|
||||||
import { PlayCircleOutlined, CheckCircleOutlined, ClockCircleOutlined, EditOutlined, EyeOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { PlayCircleOutlined, CheckCircleOutlined, ClockCircleOutlined, EditOutlined, EyeOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import { useProjectStore } from '@/stores/projectStore'
|
import { useProjectStore } from '@/stores/projectStore'
|
||||||
import { Episode } from '@/services/projectService'
|
import { Episode } from '@/services/projectService'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
|
type EpisodeStatusFilter = 'all' | 'pending' | 'writing' | 'completed'
|
||||||
|
|
||||||
interface EpisodeSidebarProps {
|
interface EpisodeSidebarProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
onEpisodeSelect?: (episode: Episode) => void;
|
onEpisodeSelect?: (episode: Episode) => void;
|
||||||
@ -21,6 +23,7 @@ export const EpisodeSidebar: React.FC<EpisodeSidebarProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { episodes, loading, fetchEpisodes, executeEpisode } = useProjectStore()
|
const { episodes, loading, fetchEpisodes, executeEpisode } = useProjectStore()
|
||||||
const [executing, setExecuting] = useState<number | null>(null)
|
const [executing, setExecuting] = useState<number | null>(null)
|
||||||
|
const [statusFilter, setStatusFilter] = useState<EpisodeStatusFilter>('all')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
@ -90,6 +93,15 @@ export const EpisodeSidebar: React.FC<EpisodeSidebarProps> = ({
|
|||||||
const completedEpisodes = episodes.filter(ep => ep.status === 'completed').length
|
const completedEpisodes = episodes.filter(ep => ep.status === 'completed').length
|
||||||
const progress = totalEpisodes > 0 ? Math.round((completedEpisodes / totalEpisodes) * 100) : 0
|
const progress = totalEpisodes > 0 ? Math.round((completedEpisodes / totalEpisodes) * 100) : 0
|
||||||
|
|
||||||
|
// 根据筛选状态过滤剧集
|
||||||
|
const filteredEpisodes = episodes.filter(episode => {
|
||||||
|
if (statusFilter === 'all') return true
|
||||||
|
if (statusFilter === 'pending') return episode.status === 'pending'
|
||||||
|
if (statusFilter === 'writing') return episode.status === 'draft' || episode.status === 'writing'
|
||||||
|
if (statusFilter === 'completed') return episode.status === 'completed'
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -103,7 +115,7 @@ export const EpisodeSidebar: React.FC<EpisodeSidebarProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 头部:进度统计 */}
|
{/* 头部:进度统计 */}
|
||||||
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
|
<div style={{ padding: '12px 16px', borderBottom: '1px solid #f0f0f0' }}>
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Text strong style={{ fontSize: '14px' }}>剧集管理</Text>
|
<Text strong style={{ fontSize: '14px' }}>剧集管理</Text>
|
||||||
@ -124,13 +136,30 @@ export const EpisodeSidebar: React.FC<EpisodeSidebarProps> = ({
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 状态筛选器 - 响应式处理 */}
|
||||||
|
<div style={{ padding: '8px 12px', borderBottom: '1px solid #f0f0f0', overflowX: 'hidden' }}>
|
||||||
|
<Segmented
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={setStatusFilter}
|
||||||
|
size="middle"
|
||||||
|
block
|
||||||
|
options={[
|
||||||
|
{ label: `全部${totalEpisodes}`, value: 'all' },
|
||||||
|
{ label: `待创作${episodes.filter(ep => ep.status === 'pending').length}`, value: 'pending' },
|
||||||
|
{ label: `创作中${episodes.filter(ep => ep.status === 'draft' || ep.status === 'writing').length}`, value: 'writing' },
|
||||||
|
{ label: `已完成${completedEpisodes}`, value: 'completed' }
|
||||||
|
]}
|
||||||
|
style={{ fontSize: '12px', whiteSpace: 'nowrap' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 剧集列表 */}
|
{/* 剧集列表 */}
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '12px' }}>
|
<div style={{ flex: 1, overflowY: 'auto', padding: '12px' }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||||
<Spin tip="加载中..." />
|
<Spin tip="加载中..." />
|
||||||
</div>
|
</div>
|
||||||
) : episodes.length === 0 ? (
|
) : filteredEpisodes.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
description="暂无剧集"
|
description="暂无剧集"
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
@ -138,7 +167,7 @@ export const EpisodeSidebar: React.FC<EpisodeSidebarProps> = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
{episodes.map((episode) => (
|
{filteredEpisodes.map((episode) => (
|
||||||
<Card
|
<Card
|
||||||
key={episode.id}
|
key={episode.id}
|
||||||
size="small"
|
size="small"
|
||||||
@ -153,11 +182,11 @@ export const EpisodeSidebar: React.FC<EpisodeSidebarProps> = ({
|
|||||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Text strong style={{ fontSize: '13px' }}>
|
<Text strong style={{ fontSize: '14px' }}>
|
||||||
EP{String(episode.number).padStart(2, '0')}
|
EP{String(episode.number).padStart(2, '0')}
|
||||||
</Text>
|
</Text>
|
||||||
{episode.title && (
|
{episode.title && (
|
||||||
<Text style={{ fontSize: '12px', color: '#666' }} ellipsis>
|
<Text style={{ fontSize: '13px', color: '#666' }} ellipsis>
|
||||||
{episode.title}
|
{episode.title}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@ -165,14 +194,14 @@ export const EpisodeSidebar: React.FC<EpisodeSidebarProps> = ({
|
|||||||
<Tag
|
<Tag
|
||||||
color={getStatusColor(episode.status)}
|
color={getStatusColor(episode.status)}
|
||||||
icon={getStatusIcon(episode.status)}
|
icon={getStatusIcon(episode.status)}
|
||||||
style={{ margin: 0, fontSize: '11px' }}
|
style={{ margin: 0, fontSize: '12px' }}
|
||||||
>
|
>
|
||||||
{getStatusText(episode.status)}
|
{getStatusText(episode.status)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{episode.qualityScore && (
|
{episode.qualityScore && (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', color: '#999' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: '#999' }}>
|
||||||
<span>质量: {episode.qualityScore}</span>
|
<span>质量: {episode.qualityScore}</span>
|
||||||
{episode.issues && episode.issues.length > 0 && (
|
{episode.issues && episode.issues.length > 0 && (
|
||||||
<span>问题: {episode.issues.length}</span>
|
<span>问题: {episode.issues.length}</span>
|
||||||
|
|||||||
114
frontend/src/components/Workspace/LeftSidebar.tsx
Normal file
114
frontend/src/components/Workspace/LeftSidebar.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Button, Space, Tooltip } from 'antd'
|
||||||
|
import {
|
||||||
|
BookOutlined,
|
||||||
|
UnorderedListOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
|
import { EpisodeSidebar } from './EpisodeSidebar'
|
||||||
|
import { ContextPanel } from './ContextPanel'
|
||||||
|
|
||||||
|
type SidebarTab = 'episodes' | 'context'
|
||||||
|
|
||||||
|
interface LeftSidebarProps {
|
||||||
|
projectId: string
|
||||||
|
project: any
|
||||||
|
loading: boolean
|
||||||
|
activeStates?: any[]
|
||||||
|
memoryItems?: any[]
|
||||||
|
width?: number
|
||||||
|
currentEpisodeId?: string
|
||||||
|
onEpisodeSelect?: (episode: any) => void
|
||||||
|
onSendMessage?: (msg: string) => void
|
||||||
|
onNavigateToSettings?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LeftSidebar: React.FC<LeftSidebarProps> = ({
|
||||||
|
projectId,
|
||||||
|
project,
|
||||||
|
loading,
|
||||||
|
activeStates = [],
|
||||||
|
memoryItems = [],
|
||||||
|
width = 600,
|
||||||
|
currentEpisodeId,
|
||||||
|
onEpisodeSelect,
|
||||||
|
onSendMessage,
|
||||||
|
onNavigateToSettings
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<SidebarTab>('episodes')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: `${width}px`,
|
||||||
|
minWidth: '280px',
|
||||||
|
maxWidth: '600px',
|
||||||
|
height: '100%',
|
||||||
|
background: '#fafbfc',
|
||||||
|
border: '1px solid #e0e4e7',
|
||||||
|
borderRight: '2px solid #d1d5db',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: '8px 0 0 8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
boxShadow: 'inset -1px 0 0 #e5e7eb'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Tab 头部 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '12px 16px',
|
||||||
|
borderBottom: '2px solid #e0e4e7',
|
||||||
|
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
|
||||||
|
gap: '8px',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type={activeTab === 'episodes' ? 'primary' : 'default'}
|
||||||
|
icon={<UnorderedListOutlined />}
|
||||||
|
onClick={() => setActiveTab('episodes')}
|
||||||
|
size="small"
|
||||||
|
style={{ flex: 1, fontSize: '12px' }}
|
||||||
|
>
|
||||||
|
剧集管理
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={activeTab === 'context' ? 'primary' : 'default'}
|
||||||
|
icon={<BookOutlined />}
|
||||||
|
onClick={() => setActiveTab('context')}
|
||||||
|
size="small"
|
||||||
|
style={{ flex: 1, fontSize: '12px' }}
|
||||||
|
>
|
||||||
|
故事上下文
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab 内容 */}
|
||||||
|
{activeTab === 'episodes' ? (
|
||||||
|
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||||
|
<EpisodeSidebar
|
||||||
|
projectId={projectId}
|
||||||
|
onEpisodeSelect={onEpisodeSelect}
|
||||||
|
currentEpisodeId={currentEpisodeId}
|
||||||
|
onSendMessage={onSendMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||||
|
<ContextPanel
|
||||||
|
project={project}
|
||||||
|
loading={loading}
|
||||||
|
activeStates={activeStates}
|
||||||
|
memoryItems={memoryItems}
|
||||||
|
width={width}
|
||||||
|
onNavigateToSettings={onNavigateToSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
346
frontend/src/components/Workspace/ReviewTaskCard.tsx
Normal file
346
frontend/src/components/Workspace/ReviewTaskCard.tsx
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* Review Task Card Component
|
||||||
|
*
|
||||||
|
* 审核任务卡片组件 - 支持过程中审核和生成后审核
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 显示审核任务信息(问题类型、位置、严重程度)
|
||||||
|
* 2. 支持用户操作:确认、修改、忽略
|
||||||
|
* 3. 过程中审核:完成后自动销毁
|
||||||
|
* 4. 生成后审核:保留记录
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Tag,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Alert,
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
Radio,
|
||||||
|
Divider,
|
||||||
|
Tooltip,
|
||||||
|
Typography
|
||||||
|
} from 'antd'
|
||||||
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
EyeInvisibleOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
InfoCircleOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { message } from 'antd'
|
||||||
|
import api from '@/services/api'
|
||||||
|
import type { ReviewTask } from '@/services/reviewService'
|
||||||
|
|
||||||
|
const { Text, Paragraph } = Typography
|
||||||
|
|
||||||
|
interface ReviewTaskCardProps {
|
||||||
|
task: ReviewTask
|
||||||
|
projectId: string
|
||||||
|
onActionComplete?: (taskId: string, action: string, response?: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReviewTaskCard: React.FC<ReviewTaskCardProps> = ({
|
||||||
|
task,
|
||||||
|
projectId,
|
||||||
|
onActionComplete
|
||||||
|
}) => {
|
||||||
|
const [actionModalVisible, setActionModalVisible] = useState(false)
|
||||||
|
const [selectedAction, setSelectedAction] = useState<string>('')
|
||||||
|
const [userResponse, setUserResponse] = useState('')
|
||||||
|
const [modifiedContent, setModifiedContent] = useState('')
|
||||||
|
|
||||||
|
// 严重程度颜色
|
||||||
|
const severityColors: Record<string, string> = {
|
||||||
|
low: 'green',
|
||||||
|
medium: 'orange',
|
||||||
|
high: 'red'
|
||||||
|
}
|
||||||
|
|
||||||
|
const severityLabels: Record<string, string> = {
|
||||||
|
low: '低',
|
||||||
|
medium: '中',
|
||||||
|
high: '高'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模式标签
|
||||||
|
const modeLabels: Record<string, string> = {
|
||||||
|
process: '过程中审核',
|
||||||
|
'post_creation': '生成后审核'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发来源标签
|
||||||
|
const triggerSourceLabels: Record<string, string> = {
|
||||||
|
agent_auto: 'Agent自主',
|
||||||
|
user_skill: '用户配置Skill',
|
||||||
|
user_rule: '用户配置规则',
|
||||||
|
manual: '手动请求'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作标签
|
||||||
|
const actionLabels: Record<string, string> = {
|
||||||
|
confirm: '确认继续',
|
||||||
|
modify: '修改内容',
|
||||||
|
ignore: '忽略问题',
|
||||||
|
other: '补充信息'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理操作
|
||||||
|
const handleAction = async (action: string) => {
|
||||||
|
if (!taskId || action === 'other') {
|
||||||
|
// 其他操作需要打开模态框
|
||||||
|
setSelectedAction(action)
|
||||||
|
setActionModalVisible(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post(`/review-task/process/${task.id}/action`, {
|
||||||
|
action,
|
||||||
|
user_response: userResponse || undefined,
|
||||||
|
modified_content: modifiedContent || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
message.success('操作已提交')
|
||||||
|
|
||||||
|
if (onActionComplete) {
|
||||||
|
onActionComplete(task.id, action, userResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是确认或忽略,关闭卡片(过程中审核自动销毁)
|
||||||
|
if (action === 'confirm' || action === 'ignore') {
|
||||||
|
setActionModalVisible(false)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitAction = async () => {
|
||||||
|
await handleAction(selectedAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成后审核的操作处理
|
||||||
|
const handlePostCreationAction = async (action: string) => {
|
||||||
|
if (action === 'other') {
|
||||||
|
setSelectedAction(action)
|
||||||
|
setActionModalVisible(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post(`/review-task/post-creation/${task.id}/action`, {
|
||||||
|
action,
|
||||||
|
user_response: userResponse || undefined,
|
||||||
|
modified_content: modifiedContent || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
message.success('操作已提交')
|
||||||
|
|
||||||
|
if (onActionComplete) {
|
||||||
|
onActionComplete(task.id, action, userResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'confirm' || action === 'ignore') {
|
||||||
|
setActionModalVisible(false)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据模式选择处理函数
|
||||||
|
const isProcessReview = task.mode === 'process'
|
||||||
|
const actionHandler = isProcessReview ? handleAction : handlePostCreationAction
|
||||||
|
const taskId = task.id
|
||||||
|
|
||||||
|
// 渲染操作按钮
|
||||||
|
const renderActionButtons = () => (
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{task.available_actions.map(action => (
|
||||||
|
<Button
|
||||||
|
key={action}
|
||||||
|
type={action === 'confirm' ? 'primary' : 'default'}
|
||||||
|
icon={
|
||||||
|
action === 'confirm' ? <CheckCircleOutlined /> :
|
||||||
|
action === 'modify' ? <EditOutlined /> :
|
||||||
|
action === 'ignore' ? <EyeInvisibleOutlined /> :
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
}
|
||||||
|
onClick={() => actionHandler(action)}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{actionLabels[action]}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 渲染位置信息
|
||||||
|
const renderLocation = () => {
|
||||||
|
const { episode_number, scene_number, section, line_number, content_snippet } = task.location || {}
|
||||||
|
const parts = []
|
||||||
|
|
||||||
|
if (episode_number) parts.push(`第 ${episode_number} 集`)
|
||||||
|
if (scene_number) parts.push(`场景 ${scene_number}`)
|
||||||
|
if (section) parts.push(`段落: ${section}`)
|
||||||
|
if (line_number) parts.push(`行 ${line_number}`)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text type="secondary">
|
||||||
|
<span style={{ fontWeight: 'bold' }}>位置: </span>
|
||||||
|
{parts.join(' · ')}
|
||||||
|
</Text>
|
||||||
|
{content_snippet && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 8,
|
||||||
|
padding: 8,
|
||||||
|
background: '#f5f5f5',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 12
|
||||||
|
}}>
|
||||||
|
<Text type="secondary">
|
||||||
|
<Tooltip title={content_snippet}>
|
||||||
|
{content_snippet.length > 100
|
||||||
|
? content_snippet.substring(0, 100) + '...'
|
||||||
|
: content_snippet}
|
||||||
|
</Tooltip>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染操作模态框
|
||||||
|
const renderActionModal = () => (
|
||||||
|
<Modal
|
||||||
|
title="确认操作"
|
||||||
|
open={actionModalVisible}
|
||||||
|
onCancel={() => setActionModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="cancel" onClick={() => setActionModalVisible(false)}>
|
||||||
|
取消
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmitAction}
|
||||||
|
disabled={selectedAction === 'other' && !userResponse.trim()}
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Text strong>操作: {actionLabels[selectedAction]}</Text>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{selectedAction === 'modify' && (
|
||||||
|
<>
|
||||||
|
<Text>修改内容:</Text>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={6}
|
||||||
|
placeholder="输入修改后的内容..."
|
||||||
|
value={modifiedContent}
|
||||||
|
onChange={e => setModifiedContent(e.target.value)}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedAction === 'other' && (
|
||||||
|
<>
|
||||||
|
<Text>补充信息:</Text>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder="输入补充说明..."
|
||||||
|
value={userResponse}
|
||||||
|
onChange={e => setUserResponse(e.target.value)}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.suggestion && (
|
||||||
|
<Alert
|
||||||
|
message="建议"
|
||||||
|
description={task.suggestion}
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
bordered={true}
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: severityColors[task.severity]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 头部:状态标签 */}
|
||||||
|
<Space style={{ width: '100%', marginBottom: 12 }} wrap>
|
||||||
|
<Tag color={severityColors[task.severity]}>
|
||||||
|
{severityLabels[task.severity]} 严重度
|
||||||
|
</Tag>
|
||||||
|
<Tag color="blue">
|
||||||
|
{task.issue_type}
|
||||||
|
</Tag>
|
||||||
|
<Tag color="cyan">
|
||||||
|
{modeLabels[task.mode] || task.mode}
|
||||||
|
</Tag>
|
||||||
|
{task.trigger_source && (
|
||||||
|
<Tag color="default">
|
||||||
|
来源: {triggerSourceLabels[task.trigger_source] || task.trigger_source}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
{/* 问题描述 */}
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
marginBottom: 12,
|
||||||
|
padding: 12,
|
||||||
|
background: '#fffbe6',
|
||||||
|
borderRadius: 4,
|
||||||
|
border: '1px solid #ffd591'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{task.issue_description}
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
|
{/* 位置信息 */}
|
||||||
|
{renderLocation()}
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
{renderActionButtons()}
|
||||||
|
|
||||||
|
{/* 操作模态框 */}
|
||||||
|
{renderActionModal()}
|
||||||
|
|
||||||
|
{/* 修改建议 */}
|
||||||
|
{task.suggestion && (
|
||||||
|
<Alert
|
||||||
|
message="修改建议"
|
||||||
|
description={task.suggestion}
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReviewTaskCard
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { ConfigProvider } from 'antd'
|
import { ConfigProvider, theme } from 'antd'
|
||||||
import zhCN from 'antd/locale/zh_CN'
|
import zhCN from 'antd/locale/zh_CN'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ConfigProvider locale={zhCN}>
|
<ConfigProvider
|
||||||
|
locale={zhCN}
|
||||||
|
theme={{
|
||||||
|
algorithm: theme.defaultAlgorithm,
|
||||||
|
token: {
|
||||||
|
colorPrimary: '#0ea5e9',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<App />
|
<App />
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
|||||||
@ -24,7 +24,8 @@ import {
|
|||||||
Empty,
|
Empty,
|
||||||
Spin,
|
Spin,
|
||||||
message,
|
message,
|
||||||
Progress
|
Progress,
|
||||||
|
Alert
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@ -41,8 +42,13 @@ import { ImportanceLevel } from '@/services/memoryService'
|
|||||||
const { TabPane } = Tabs
|
const { TabPane } = Tabs
|
||||||
const { TextArea } = Input
|
const { TextArea } = Input
|
||||||
|
|
||||||
const MemorySystem: React.FC = () => {
|
interface MemorySystemProps {
|
||||||
const { id: projectId } = useParams<{ id: string }>()
|
projectId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemorySystem: React.FC<MemorySystemProps> = ({ projectId: propProjectId }) => {
|
||||||
|
const { id: routeProjectId } = useParams<{ id: string }>()
|
||||||
|
const projectId = propProjectId || routeProjectId
|
||||||
const {
|
const {
|
||||||
memorySystem,
|
memorySystem,
|
||||||
timeline,
|
timeline,
|
||||||
@ -215,63 +221,104 @@ const MemorySystem: React.FC = () => {
|
|||||||
if (loading && !memorySystem) {
|
if (loading && !memorySystem) {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
||||||
<Spin size="large" />
|
<Spin size="large" tip="正在加载记忆系统..." />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div style={{ padding: '24px', color: 'red' }}>Error: {error}</div>
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
<Card>
|
||||||
|
<Alert
|
||||||
|
message="加载失败"
|
||||||
|
description={error}
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
action={
|
||||||
|
<Button size="small" onClick={() => projectId && fetchMemorySystem(projectId)}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memorySystem) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
<Card>
|
||||||
|
<Empty
|
||||||
|
description="暂无记忆系统数据"
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
>
|
||||||
|
<Button type="primary" onClick={() => projectId && fetchMemorySystem(projectId)}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Empty>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistics Overview
|
// Statistics Overview
|
||||||
const renderStatistics = () => {
|
const renderStatistics = () => {
|
||||||
const stats = memorySystem?.statistics
|
const stats = memorySystem?.statistics
|
||||||
|
const titleStyle = { fontSize: '12px', color: '#8c8c8c' }
|
||||||
|
const valueStyle = { fontSize: '18px', fontWeight: 600 }
|
||||||
|
const iconStyle = { fontSize: '14px' }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
<Row gutter={12} style={{ marginBottom: 20 }}>
|
||||||
<Col span={4}>
|
<Col flex={1}>
|
||||||
<Card>
|
<Card size="small" bodyStyle={{ padding: '12px' }}>
|
||||||
<Statistic title="总事件" value={stats?.totalEvents || 0} prefix={<ClockCircleOutlined />} />
|
<Statistic
|
||||||
|
title={<span style={titleStyle}>总事件</span>}
|
||||||
|
value={stats?.totalEvents || 0}
|
||||||
|
prefix={<ClockCircleOutlined style={iconStyle} />}
|
||||||
|
valueStyle={valueStyle}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col flex={1}>
|
||||||
<Card>
|
<Card size="small" bodyStyle={{ padding: '12px' }}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="剧情线"
|
title={<span style={titleStyle}>剧情线</span>}
|
||||||
value={stats?.totalThreads || 0}
|
value={stats?.totalThreads || 0}
|
||||||
suffix={`/ ${stats?.resolvedThreads || 0} 已解决`}
|
suffix={<span style={{ fontSize: '12px', color: '#bfbfbf', fontWeight: 'normal' }}>/ {stats?.resolvedThreads || 0} 已解决</span>}
|
||||||
|
valueStyle={valueStyle}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col flex={1}>
|
||||||
<Card>
|
<Card size="small" bodyStyle={{ padding: '12px' }}>
|
||||||
<Statistic title="角色" value={stats?.totalCharacters || 0} prefix={<UserOutlined />} />
|
<Statistic
|
||||||
|
title={<span style={titleStyle}>角色</span>}
|
||||||
|
value={stats?.totalCharacters || 0}
|
||||||
|
prefix={<UserOutlined style={iconStyle} />}
|
||||||
|
valueStyle={valueStyle}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col flex={1}>
|
||||||
<Card>
|
<Card size="small" bodyStyle={{ padding: '12px' }}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="伏笔"
|
title={<span style={titleStyle}>伏笔</span>}
|
||||||
value={stats?.totalForeshadowing || 0}
|
value={stats?.totalForeshadowing || 0}
|
||||||
suffix={`/ ${stats?.paidOffForeshadowing || 0} 已收线`}
|
suffix={<span style={{ fontSize: '12px', color: '#bfbfbf', fontWeight: 'normal' }}>/ {stats?.paidOffForeshadowing || 0} 已收线</span>}
|
||||||
|
valueStyle={valueStyle}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col flex={1}>
|
||||||
<Card>
|
<Card size="small" bodyStyle={{ padding: '12px' }}>
|
||||||
<Statistic title="关系" value={stats?.totalRelationships || 0} />
|
<Statistic
|
||||||
</Card>
|
title={<span style={titleStyle}>关系</span>}
|
||||||
</Col>
|
value={stats?.totalRelationships || 0}
|
||||||
<Col span={4}>
|
valueStyle={valueStyle}
|
||||||
<Card>
|
/>
|
||||||
<Button
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
onClick={() => projectId && fetchMemorySystem(projectId)}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -289,7 +336,7 @@ const MemorySystem: React.FC = () => {
|
|||||||
onChange={(value) => setTimelineFilters({ ...timelineFilters, character: value })}
|
onChange={(value) => setTimelineFilters({ ...timelineFilters, character: value })}
|
||||||
value={timelineFilters.character}
|
value={timelineFilters.character}
|
||||||
>
|
>
|
||||||
{characterStates.map(cs => (
|
{(Array.isArray(characterStates) ? characterStates : []).map(cs => (
|
||||||
<Select.Option key={cs.characterName} value={cs.characterName}>
|
<Select.Option key={cs.characterName} value={cs.characterName}>
|
||||||
{cs.characterName}
|
{cs.characterName}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
@ -302,7 +349,7 @@ const MemorySystem: React.FC = () => {
|
|||||||
onChange={(value) => setTimelineFilters({ ...timelineFilters, episode: value })}
|
onChange={(value) => setTimelineFilters({ ...timelineFilters, episode: value })}
|
||||||
value={timelineFilters.episode}
|
value={timelineFilters.episode}
|
||||||
>
|
>
|
||||||
{Array.from(new Set(timeline.map(t => t.episodeNumber))).map(ep => (
|
{Array.from(new Set((Array.isArray(timeline) ? timeline : []).map(t => t.episodeNumber))).map(ep => (
|
||||||
<Select.Option key={ep} value={ep}>第 {ep} 集</Select.Option>
|
<Select.Option key={ep} value={ep}>第 {ep} 集</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@ -320,11 +367,11 @@ const MemorySystem: React.FC = () => {
|
|||||||
<Button onClick={clearTimelineFilters}>清除筛选</Button>
|
<Button onClick={clearTimelineFilters}>清除筛选</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
{timeline.length === 0 ? (
|
{(Array.isArray(timeline) ? timeline : []).length === 0 ? (
|
||||||
<Empty description="暂无时间线事件" />
|
<Empty description="暂无时间线事件" />
|
||||||
) : (
|
) : (
|
||||||
<Timeline mode="left">
|
<Timeline mode="left">
|
||||||
{timeline.map(event => (
|
{(Array.isArray(timeline) ? timeline : []).map(event => (
|
||||||
<Timeline.Item
|
<Timeline.Item
|
||||||
key={event.id}
|
key={event.id}
|
||||||
dot={<Avatar size="small" icon={<ClockCircleOutlined />} />}
|
dot={<Avatar size="small" icon={<ClockCircleOutlined />} />}
|
||||||
@ -368,11 +415,11 @@ const MemorySystem: React.FC = () => {
|
|||||||
添加剧情线
|
添加剧情线
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{threads.length === 0 ? (
|
{(Array.isArray(threads) ? threads : []).length === 0 ? (
|
||||||
<Empty description="暂无待处理剧情线" />
|
<Empty description="暂无待处理剧情线" />
|
||||||
) : (
|
) : (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
{threads.map(thread => (
|
{(Array.isArray(threads) ? threads : []).map(thread => (
|
||||||
<Card
|
<Card
|
||||||
key={thread.id}
|
key={thread.id}
|
||||||
size="small"
|
size="small"
|
||||||
@ -433,11 +480,11 @@ const MemorySystem: React.FC = () => {
|
|||||||
// Character States Tab
|
// Character States Tab
|
||||||
const renderCharacterStates = () => (
|
const renderCharacterStates = () => (
|
||||||
<div>
|
<div>
|
||||||
{characterStates.length === 0 ? (
|
{(Array.isArray(characterStates) ? characterStates : []).length === 0 ? (
|
||||||
<Empty description="暂无角色状态信息" />
|
<Empty description="暂无角色状态信息" />
|
||||||
) : (
|
) : (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||||
{characterStates.map(cs => (
|
{(Array.isArray(characterStates) ? characterStates : []).map(cs => (
|
||||||
<Card
|
<Card
|
||||||
key={cs.characterName}
|
key={cs.characterName}
|
||||||
title={
|
title={
|
||||||
@ -485,11 +532,11 @@ const MemorySystem: React.FC = () => {
|
|||||||
添加伏笔
|
添加伏笔
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{foreshadowing.length === 0 ? (
|
{(Array.isArray(foreshadowing) ? foreshadowing : []).length === 0 ? (
|
||||||
<Empty description="暂无伏笔记录" />
|
<Empty description="暂无伏笔记录" />
|
||||||
) : (
|
) : (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
{foreshadowing.map(item => (
|
{(Array.isArray(foreshadowing) ? foreshadowing : []).map(item => (
|
||||||
<Card
|
<Card
|
||||||
key={item.id}
|
key={item.id}
|
||||||
size="small"
|
size="small"
|
||||||
@ -534,11 +581,11 @@ const MemorySystem: React.FC = () => {
|
|||||||
// Relationships Tab
|
// Relationships Tab
|
||||||
const renderRelationships = () => (
|
const renderRelationships = () => (
|
||||||
<div>
|
<div>
|
||||||
{relationships.length === 0 ? (
|
{(Array.isArray(relationships) ? relationships : []).length === 0 ? (
|
||||||
<Empty description="暂无关系数据" />
|
<Empty description="暂无关系数据" />
|
||||||
) : (
|
) : (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
{relationships.map((rel, idx) => (
|
{(Array.isArray(relationships) ? relationships : []).map((rel, idx) => (
|
||||||
<Card key={idx} size="small">
|
<Card key={idx} size="small">
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
<Space>
|
<Space>
|
||||||
@ -565,36 +612,62 @@ const MemorySystem: React.FC = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24 }}>
|
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
<Card title="记忆系统" extra={<ReloadOutlined onClick={() => projectId && fetchMemorySystem(projectId)} />}>
|
<div style={{ flexShrink: 0 }}>
|
||||||
{renderStatistics()}
|
{renderStatistics()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
<Tabs
|
||||||
<TabPane tab="时间线" key="timeline">
|
activeKey={activeTab}
|
||||||
{renderTimeline()}
|
onChange={setActiveTab}
|
||||||
</TabPane>
|
type="card"
|
||||||
<TabPane
|
style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||||
tab={
|
tabBarExtraContent={
|
||||||
<span>
|
<Button
|
||||||
剧情线
|
type="text"
|
||||||
<Badge count={threads.filter(t => t.status !== 'resolved').length} style={{ marginLeft: 8 }} />
|
size="small"
|
||||||
</span>
|
icon={<ReloadOutlined spin={loading} />}
|
||||||
}
|
onClick={() => projectId && fetchMemorySystem(projectId)}
|
||||||
key="threads"
|
loading={loading}
|
||||||
>
|
>
|
||||||
|
刷新数据
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TabPane tab="时间线" key="timeline">
|
||||||
|
<div style={{ padding: '16px 0 24px 0', height: '100%', overflowY: 'auto' }}>
|
||||||
|
{renderTimeline()}
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
|
<span>
|
||||||
|
剧情线
|
||||||
|
<Badge count={threads.filter(t => t.status !== 'resolved').length} style={{ marginLeft: 8 }} />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
key="threads"
|
||||||
|
>
|
||||||
|
<div style={{ padding: '16px 0 24px 0', height: '100%', overflowY: 'auto' }}>
|
||||||
{renderThreads()}
|
{renderThreads()}
|
||||||
</TabPane>
|
</div>
|
||||||
<TabPane tab="角色状态" key="characters">
|
</TabPane>
|
||||||
|
<TabPane tab="角色状态" key="characters">
|
||||||
|
<div style={{ padding: '16px 0 24px 0', height: '100%', overflowY: 'auto' }}>
|
||||||
{renderCharacterStates()}
|
{renderCharacterStates()}
|
||||||
</TabPane>
|
</div>
|
||||||
<TabPane tab="伏笔管理" key="foreshadowing">
|
</TabPane>
|
||||||
|
<TabPane tab="伏笔管理" key="foreshadowing">
|
||||||
|
<div style={{ padding: '16px 0 24px 0', height: '100%', overflowY: 'auto' }}>
|
||||||
{renderForeshadowing()}
|
{renderForeshadowing()}
|
||||||
</TabPane>
|
</div>
|
||||||
<TabPane tab="关系图" key="relationships">
|
</TabPane>
|
||||||
|
<TabPane tab="关系图" key="relationships">
|
||||||
|
<div style={{ padding: '16px 0 24px 0', height: '100%', overflowY: 'auto' }}>
|
||||||
{renderRelationships()}
|
{renderRelationships()}
|
||||||
</TabPane>
|
</div>
|
||||||
</Tabs>
|
</TabPane>
|
||||||
</Card>
|
</Tabs>
|
||||||
|
|
||||||
{/* Thread Modal */}
|
{/* Thread Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
@ -636,7 +709,7 @@ const MemorySystem: React.FC = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="relatedCharacters" label="相关角色">
|
<Form.Item name="relatedCharacters" label="相关角色">
|
||||||
<Select mode="tags" placeholder="选择或输入角色名">
|
<Select mode="tags" placeholder="选择或输入角色名">
|
||||||
{characterStates.map(cs => (
|
{(Array.isArray(characterStates) ? characterStates : []).map(cs => (
|
||||||
<Select.Option key={cs.characterName} value={cs.characterName}>
|
<Select.Option key={cs.characterName} value={cs.characterName}>
|
||||||
{cs.characterName}
|
{cs.characterName}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,967 +0,0 @@
|
|||||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
|
||||||
import { Layout, Button, Space, message, Spin, Typography, Modal } from 'antd'
|
|
||||||
import { ArrowLeftOutlined, UnorderedListOutlined } from '@ant-design/icons'
|
|
||||||
|
|
||||||
import { projectService } from '@/services/projectService'
|
|
||||||
import { useProjectStore } from '@/stores/projectStore'
|
|
||||||
import { ContextPanel } from '@/components/Workspace/ContextPanel'
|
|
||||||
import { SmartCanvas } from '@/components/Workspace/SmartCanvas'
|
|
||||||
import { DirectorInbox } from '@/components/Workspace/DirectorInbox'
|
|
||||||
import { EpisodeSidebar } from '@/components/Workspace/EpisodeSidebar'
|
|
||||||
import { Episode } from '@/services/projectService'
|
|
||||||
|
|
||||||
const { Header, Sider } = Layout;
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
export const ProjectWorkspace: React.FC = () => {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { id: projectId } = useParams<{ id: string }>()
|
|
||||||
|
|
||||||
// 调试:确认组件已加载
|
|
||||||
console.log('=== ProjectWorkspace component loaded ===')
|
|
||||||
console.log('projectId:', projectId)
|
|
||||||
|
|
||||||
const [project, setProject] = useState<any>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [streaming, setStreaming] = useState(false)
|
|
||||||
const [canvasContent, setCanvasContent] = useState<string>('')
|
|
||||||
const [agentStatus, setAgentStatus] = useState<'idle' | 'planning' | 'writing' | 'reviewing'>('idle')
|
|
||||||
const [agentPlan, setAgentPlan] = useState<string[]>([])
|
|
||||||
const [inboxItems, setInboxItems] = useState<any[]>([])
|
|
||||||
const [chatHistory, setChatHistory] = useState<{role: 'user' | 'agent', content: string}[]>([])
|
|
||||||
const [annotations, setAnnotations] = useState<any[]>([])
|
|
||||||
const [activeStates, setActiveStates] = useState<any[]>([
|
|
||||||
{ type: 'time', value: '初始状态' },
|
|
||||||
{ type: 'location', value: '未知地点' }
|
|
||||||
])
|
|
||||||
|
|
||||||
// 记忆库状态
|
|
||||||
const [memoryItems, setMemoryItems] = useState<any[]>([])
|
|
||||||
|
|
||||||
// 剧集相关状态
|
|
||||||
const [currentEpisode, setCurrentEpisode] = useState<Episode | null>(null)
|
|
||||||
const [showEpisodeSidebar, setShowEpisodeSidebar] = useState(true)
|
|
||||||
|
|
||||||
// 布局宽度状态 - 优化后的默认比例
|
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(260);
|
|
||||||
const [contextWidth, setContextWidth] = useState(300);
|
|
||||||
const [canvasWidth, setCanvasWidth] = useState(600);
|
|
||||||
|
|
||||||
// 拖拽相关 Refs
|
|
||||||
const isResizingRef = useRef<'sidebar' | 'context' | 'canvasRight' | null>(null);
|
|
||||||
const startXRef = useRef(0);
|
|
||||||
const startWidthRef = useRef(0);
|
|
||||||
|
|
||||||
// 拖拽处理函数
|
|
||||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
||||||
if (!isResizingRef.current) return;
|
|
||||||
|
|
||||||
const deltaX = e.clientX - startXRef.current;
|
|
||||||
|
|
||||||
if (isResizingRef.current === 'sidebar') {
|
|
||||||
const newWidth = Math.max(200, Math.min(500, startWidthRef.current + deltaX));
|
|
||||||
console.log('Resizing sidebar:', newWidth);
|
|
||||||
setSidebarWidth(newWidth);
|
|
||||||
} else if (isResizingRef.current === 'context') {
|
|
||||||
const newWidth = Math.max(250, Math.min(600, startWidthRef.current + deltaX));
|
|
||||||
console.log('Resizing context:', newWidth);
|
|
||||||
setContextWidth(newWidth);
|
|
||||||
} else if (isResizingRef.current === 'canvasRight') {
|
|
||||||
// Canvas 右侧手柄:向左拖动增加宽度
|
|
||||||
const newWidth = Math.max(300, Math.min(900, startWidthRef.current - deltaX));
|
|
||||||
console.log('Resizing canvas:', newWidth);
|
|
||||||
setCanvasWidth(newWidth);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMouseUp = useCallback(() => {
|
|
||||||
console.log('=== handleMouseUp called, was resizing:', isResizingRef.current);
|
|
||||||
isResizingRef.current = null;
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
|
||||||
document.body.style.userSelect = '';
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
}, [handleMouseMove]);
|
|
||||||
|
|
||||||
const startResize = (type: 'sidebar' | 'context' | 'canvasRight', e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
console.log('=== startResize called ===', type, e.clientX);
|
|
||||||
isResizingRef.current = type;
|
|
||||||
startXRef.current = e.clientX;
|
|
||||||
startWidthRef.current = type === 'sidebar' ? sidebarWidth
|
|
||||||
: type === 'context' ? contextWidth
|
|
||||||
: canvasWidth;
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
|
||||||
document.body.style.userSelect = 'none';
|
|
||||||
document.body.style.cursor = 'col-resize';
|
|
||||||
};
|
|
||||||
|
|
||||||
// AI 辅助相关状态
|
|
||||||
const [availableSkills, setAvailableSkills] = useState<any[]>([])
|
|
||||||
|
|
||||||
const [wsConnected, setWsConnected] = useState(false);
|
|
||||||
|
|
||||||
// WebSocket refs
|
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
|
||||||
const reconnectTimeoutRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const loadProject = async () => {
|
|
||||||
if (!projectId) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const data = await projectService.getProject(projectId)
|
|
||||||
setProject(data)
|
|
||||||
|
|
||||||
// 加载可用的 Skills
|
|
||||||
await loadAvailableSkills()
|
|
||||||
} catch (error) {
|
|
||||||
message.error(`加载项目失败: ${(error as Error).message}`)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载可用的 Skills
|
|
||||||
const loadAvailableSkills = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/v1/ai-assistant/available-skills')
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
// 将分类的 skills 转换为列表格式
|
|
||||||
const skillsList: any[] = []
|
|
||||||
Object.entries(data.skills || {}).forEach(([category, skills]: [string, any]) => {
|
|
||||||
skills.forEach((skill: any) => {
|
|
||||||
skillsList.push({
|
|
||||||
...skill,
|
|
||||||
category
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
setAvailableSkills(skillsList)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载 Skills 失败:', error)
|
|
||||||
// 失败时设置空数组,不影响其他功能
|
|
||||||
setAvailableSkills([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载剧集内容
|
|
||||||
const loadEpisodeContent = async (episodeNumber: number) => {
|
|
||||||
if (!projectId) return;
|
|
||||||
try {
|
|
||||||
const response = await projectService.getEpisode(projectId, episodeNumber);
|
|
||||||
const episode = response.data || response;
|
|
||||||
console.log('[ProjectWorkspace] Loading episode:', episode);
|
|
||||||
setCurrentEpisode(episode);
|
|
||||||
setCanvasContent(episode.content || '');
|
|
||||||
console.log('Loaded episode content:', episode.id, episode.content?.length || 0, 'chars');
|
|
||||||
} catch (error) {
|
|
||||||
message.error(`加载剧集内容失败: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadProject()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (reconnectTimeoutRef.current) {
|
|
||||||
clearTimeout(reconnectTimeoutRef.current);
|
|
||||||
}
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [projectId])
|
|
||||||
|
|
||||||
// 当项目加载完成后,自动选择并加载第一集
|
|
||||||
useEffect(() => {
|
|
||||||
if (project && !loading && projectId && !currentEpisode) {
|
|
||||||
// 获取剧集列表并自动选择第一集
|
|
||||||
const loadFirstEpisode = async () => {
|
|
||||||
try {
|
|
||||||
const response = await projectService.listEpisodes(projectId);
|
|
||||||
const episodes = response.data || response;
|
|
||||||
if (episodes && episodes.length > 0) {
|
|
||||||
// 找到第一个pending或completed状态的剧集,优先pending状态
|
|
||||||
const firstEpisode = episodes.find((ep: any) => ep.status === 'pending') ||
|
|
||||||
episodes.find((ep: any) => ep.status === 'completed') ||
|
|
||||||
episodes[0];
|
|
||||||
if (firstEpisode) {
|
|
||||||
await loadEpisodeContent(firstEpisode.number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载第一集失败:', error);
|
|
||||||
// 如果加载失败,创建一个临时的第一集对象作为默认显示
|
|
||||||
console.log('使用临时剧集对象显示');
|
|
||||||
setCurrentEpisode({
|
|
||||||
id: 'temp',
|
|
||||||
projectId: projectId,
|
|
||||||
number: 1,
|
|
||||||
title: '第1集',
|
|
||||||
status: 'pending',
|
|
||||||
content: '',
|
|
||||||
issues: [],
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadFirstEpisode();
|
|
||||||
}
|
|
||||||
}, [project, loading, projectId]); // 移除currentEpisode依赖,避免无限循环
|
|
||||||
|
|
||||||
// WebSocket Connection Logic
|
|
||||||
const connectWebSocket = useCallback(() => {
|
|
||||||
if (!projectId) return;
|
|
||||||
|
|
||||||
// Close existing connection if any
|
|
||||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
console.log('Closing existing WebSocket connection');
|
|
||||||
wsRef.current.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use relative URL which will be proxied by Vite
|
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
const host = window.location.host; // e.g. localhost:5173
|
|
||||||
const wsUrl = `${protocol}//${host}/ws/projects/${projectId}/execute`;
|
|
||||||
|
|
||||||
console.log('Connecting to WebSocket:', wsUrl);
|
|
||||||
const ws = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log('WebSocket Connected');
|
|
||||||
setWsConnected(true);
|
|
||||||
message.success('已连接到 Agent Runtime');
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(event.data);
|
|
||||||
handleWebSocketMessage(msg);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse WS message:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log('WebSocket Disconnected');
|
|
||||||
setWsConnected(false);
|
|
||||||
// Clear ref to allow reconnection
|
|
||||||
if (wsRef.current === ws) {
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket Error:', error);
|
|
||||||
// Don't close here, let onclose handle it
|
|
||||||
};
|
|
||||||
|
|
||||||
wsRef.current = ws;
|
|
||||||
}, [projectId]); // Only depend on projectId
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectId && !loading) {
|
|
||||||
connectWebSocket();
|
|
||||||
}
|
|
||||||
// Cleanup on unmount or projectId change
|
|
||||||
return () => {
|
|
||||||
if (reconnectTimeoutRef.current) {
|
|
||||||
clearTimeout(reconnectTimeoutRef.current);
|
|
||||||
}
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [projectId, loading, connectWebSocket]);
|
|
||||||
|
|
||||||
const handleInboxAction = (itemId: string, action: 'approve' | 'reject') => {
|
|
||||||
// Optimistic update
|
|
||||||
setInboxItems(prev => prev.filter(item => item.id !== itemId));
|
|
||||||
|
|
||||||
// Send to backend
|
|
||||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.send(JSON.stringify({
|
|
||||||
type: 'inbox_action',
|
|
||||||
itemId,
|
|
||||||
action
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
message.success(action === 'approve' ? '已批准' : '已拒绝');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWebSocketMessage = async (msg: any) => {
|
|
||||||
console.log('Received:', msg);
|
|
||||||
|
|
||||||
switch (msg.type) {
|
|
||||||
case 'connected':
|
|
||||||
break;
|
|
||||||
case 'history':
|
|
||||||
if (msg.messages) {
|
|
||||||
setChatHistory(msg.messages);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'thinking':
|
|
||||||
// Optional: show thinking indicator in chat
|
|
||||||
break;
|
|
||||||
case 'text':
|
|
||||||
setChatHistory(prev => {
|
|
||||||
const lastMsg = prev[prev.length - 1];
|
|
||||||
// 如果上一条消息也是 agent 发送的,则认为是流式输出,追加内容
|
|
||||||
if (lastMsg && lastMsg.role === 'agent') {
|
|
||||||
const newHistory = [...prev];
|
|
||||||
newHistory[newHistory.length - 1] = {
|
|
||||||
...lastMsg,
|
|
||||||
content: lastMsg.content + msg.content
|
|
||||||
};
|
|
||||||
return newHistory;
|
|
||||||
}
|
|
||||||
// 否则添加新消息
|
|
||||||
return [...prev, { role: 'agent', content: msg.content }];
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'plan_update':
|
|
||||||
if (msg.plan) {
|
|
||||||
setAgentPlan(msg.plan);
|
|
||||||
}
|
|
||||||
if (msg.status) {
|
|
||||||
setAgentStatus(msg.status);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'review_request':
|
|
||||||
setInboxItems(prev => [...prev, {
|
|
||||||
id: msg.id || Date.now().toString(),
|
|
||||||
type: 'review',
|
|
||||||
title: msg.title || '需要审核',
|
|
||||||
description: msg.description,
|
|
||||||
status: 'pending',
|
|
||||||
timestamp: Date.now()
|
|
||||||
}]);
|
|
||||||
break;
|
|
||||||
case 'review_complete':
|
|
||||||
// 处理审核卡片推送
|
|
||||||
const reviewData = msg.data || {};
|
|
||||||
setInboxItems(prev => [...prev, {
|
|
||||||
id: `review_${reviewData.episode_number}_${Date.now()}`,
|
|
||||||
type: 'review_card',
|
|
||||||
title: `EP${reviewData.episode_number} 审核完成`,
|
|
||||||
description: `质量评分: ${reviewData.overall_score?.toFixed(1) || 0} | ${reviewData.passed ? '通过' : '未通过'}`,
|
|
||||||
status: reviewData.passed ? 'approved' : 'needs-review',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
reviewData: reviewData
|
|
||||||
}]);
|
|
||||||
|
|
||||||
// 如果有严重问题,显示通知
|
|
||||||
if (reviewData.high_severity_count > 0) {
|
|
||||||
message.warning(
|
|
||||||
`EP${reviewData.episode_number} 发现 ${reviewData.high_severity_count} 个严重问题,请查看审核详情`
|
|
||||||
);
|
|
||||||
} else if (reviewData.passed) {
|
|
||||||
message.success(
|
|
||||||
`EP${reviewData.episode_number} 审核通过,质量评分: ${reviewData.overall_score?.toFixed(1) || 0}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'memory_hit':
|
|
||||||
// 添加到记忆库
|
|
||||||
if (msg.data) {
|
|
||||||
setMemoryItems(prev => [...prev, {
|
|
||||||
type: msg.data.memory_type || 'timeline',
|
|
||||||
title: msg.data.title || '记忆更新',
|
|
||||||
description: msg.data.description || JSON.stringify(msg.data.data),
|
|
||||||
timestamp: Date.now()
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'memory_update':
|
|
||||||
// 从 update_memory 工具触发的记忆更新
|
|
||||||
if (msg.data) {
|
|
||||||
setMemoryItems(prev => [...prev, {
|
|
||||||
type: msg.data.type || msg.data.memory_type || 'timeline',
|
|
||||||
title: msg.data.title || '记忆更新',
|
|
||||||
description: msg.data.description || '',
|
|
||||||
timestamp: msg.data.timestamp || Date.now(),
|
|
||||||
character: msg.data.character,
|
|
||||||
state: msg.data.state
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'annotation_add':
|
|
||||||
setAnnotations(prev => [...prev, msg.annotation]);
|
|
||||||
break;
|
|
||||||
case 'context_update':
|
|
||||||
if (msg.states) {
|
|
||||||
setActiveStates(msg.states);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'tool_call':
|
|
||||||
const toolData = msg.data || {};
|
|
||||||
const toolName = toolData.name || msg.name;
|
|
||||||
|
|
||||||
if (toolName === 'update_canvas' || toolName === 'write_file') {
|
|
||||||
setStreaming(true);
|
|
||||||
setAgentStatus('writing');
|
|
||||||
// 更新本地剧集状态为创作中
|
|
||||||
setCurrentEpisode(prev => prev ? { ...prev, status: 'writing' } : null);
|
|
||||||
} else {
|
|
||||||
setAgentStatus('planning');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'tool_result':
|
|
||||||
setStreaming(false);
|
|
||||||
break;
|
|
||||||
case 'canvas_update':
|
|
||||||
if (msg.content) {
|
|
||||||
setCanvasContent(msg.content);
|
|
||||||
// 自动保存到当前剧集
|
|
||||||
if (currentEpisode && currentEpisode.id && projectId) {
|
|
||||||
// 调用 API 保存到后端
|
|
||||||
projectService.updateEpisode(projectId, currentEpisode.number, {
|
|
||||||
content: msg.content,
|
|
||||||
status: 'draft'
|
|
||||||
}).then(() => {
|
|
||||||
console.log('Content auto-saved to episode:', currentEpisode.id);
|
|
||||||
// 更新本地剧集状态为草稿
|
|
||||||
setCurrentEpisode(prev => prev ? { ...prev, status: 'draft' } : null);
|
|
||||||
// 刷新剧集列表
|
|
||||||
useProjectStore.getState().fetchEpisodes(projectId);
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Failed to auto-save content:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'episode_saved':
|
|
||||||
// Agent 保存剧集后的确认
|
|
||||||
message.success(`剧集 ${msg.episode_number || ''} 已自动保存`);
|
|
||||||
// 刷新剧集列表
|
|
||||||
if (projectId) {
|
|
||||||
useProjectStore.getState().fetchEpisodes(projectId);
|
|
||||||
}
|
|
||||||
// 如果保存的是当前剧集,更新画布内容
|
|
||||||
if (currentEpisode && currentEpisode.number === msg.episode_number) {
|
|
||||||
// 重新获取剧集内容
|
|
||||||
loadEpisodeContent(currentEpisode.number);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'episode_updated':
|
|
||||||
// 剧集信息更新(如标题)
|
|
||||||
console.log('[ProjectWorkspace] episode_updated:', msg.data);
|
|
||||||
if (msg.data) {
|
|
||||||
// 1. 本地更新 Store 中的剧集列表
|
|
||||||
useProjectStore.getState().updateEpisodeInList(msg.data);
|
|
||||||
|
|
||||||
// 2. 如果更新的是当前剧集,同步更新当前剧集状态
|
|
||||||
if (currentEpisode && msg.data.number === currentEpisode.number) {
|
|
||||||
setCurrentEpisode(prev => ({ ...prev, ...msg.data } as Episode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'done':
|
|
||||||
setAgentStatus('idle');
|
|
||||||
setStreaming(false);
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
message.error(msg.data?.message || 'Unknown error');
|
|
||||||
setAgentStatus('idle');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDirectorMessage = (msg: string) => {
|
|
||||||
console.log('=== handleDirectorMessage called ===', msg);
|
|
||||||
|
|
||||||
// 立即更新状态 - 显示停止按钮
|
|
||||||
if (msg.includes('开始') || msg.includes('生成') || msg.includes('创作') || msg.includes('辅助')) {
|
|
||||||
setAgentStatus('planning');
|
|
||||||
// 如果是开始创作,更新本地剧集状态为创作中
|
|
||||||
if (msg.includes('创作') || msg.includes('完整内容')) {
|
|
||||||
setCurrentEpisode(prev => prev ? { ...prev, status: 'writing' } : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimistically update chat history
|
|
||||||
setChatHistory(prev => [...prev, { role: 'user', content: msg }]);
|
|
||||||
|
|
||||||
// Send message to backend via WebSocket
|
|
||||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
const messagePayload = JSON.stringify({
|
|
||||||
type: 'chat_message',
|
|
||||||
content: msg,
|
|
||||||
episodeNumber: currentEpisode?.number,
|
|
||||||
episodeTitle: currentEpisode?.title
|
|
||||||
});
|
|
||||||
console.log('Sending message:', messagePayload);
|
|
||||||
|
|
||||||
wsRef.current.send(messagePayload);
|
|
||||||
console.log('Message sent successfully');
|
|
||||||
} else {
|
|
||||||
console.error('WebSocket not ready');
|
|
||||||
message.error('Agent 连接未就绪');
|
|
||||||
setAgentStatus('idle');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止 Agent 处理函数
|
|
||||||
const handleStopAgent = () => {
|
|
||||||
console.log('=== handleStopAgent called ===');
|
|
||||||
|
|
||||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
const stopPayload = JSON.stringify({
|
|
||||||
type: 'stop_agent'
|
|
||||||
});
|
|
||||||
wsRef.current.send(stopPayload);
|
|
||||||
console.log('Stop command sent');
|
|
||||||
|
|
||||||
// 立即更新 UI 状态
|
|
||||||
setAgentStatus('idle');
|
|
||||||
setStreaming(false);
|
|
||||||
|
|
||||||
// 添加系统消息到聊天记录
|
|
||||||
setChatHistory(prev => [...prev, {
|
|
||||||
role: 'agent',
|
|
||||||
content: '⚠️ Agent 已被用户停止'
|
|
||||||
}]);
|
|
||||||
|
|
||||||
message.info('Agent 已停止');
|
|
||||||
} else {
|
|
||||||
console.error('WebSocket not ready');
|
|
||||||
message.error('无法停止:Agent 连接未就绪');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 画布内容变更处理
|
|
||||||
const handleContentChange = (content: string) => {
|
|
||||||
setCanvasContent(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 画布内容保存处理
|
|
||||||
const handleContentSave = async (content: string) => {
|
|
||||||
if (!currentEpisode || !projectId) {
|
|
||||||
message.warning('请先选择要保存的剧集');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 调用后端 API 保存剧集内容
|
|
||||||
await projectService.updateEpisode(projectId, currentEpisode.number, {
|
|
||||||
content: content,
|
|
||||||
status: 'draft'
|
|
||||||
});
|
|
||||||
message.success('内容已保存');
|
|
||||||
|
|
||||||
// 刷新剧集列表
|
|
||||||
useProjectStore.getState().fetchEpisodes(projectId);
|
|
||||||
} catch (error) {
|
|
||||||
message.error(`保存失败: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认剧集完成处理
|
|
||||||
const handleConfirmComplete = async () => {
|
|
||||||
if (!currentEpisode || !projectId) {
|
|
||||||
message.warning('请先选择要完成的剧集');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认对话框
|
|
||||||
Modal.confirm({
|
|
||||||
title: '确认完成剧集',
|
|
||||||
content: `确认将《${currentEpisode.title || `第${currentEpisode.number}集`}》标记为已完成状态?完成后将不能再编辑内容。`,
|
|
||||||
okText: '确认完成',
|
|
||||||
cancelText: '取消',
|
|
||||||
okType: 'primary',
|
|
||||||
okButtonProps: { style: { backgroundColor: '#52c41a', borderColor: '#52c41a' } },
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
|
||||||
// 调用后端 API 更新剧集状态为 completed
|
|
||||||
await projectService.updateEpisode(projectId, currentEpisode.number, {
|
|
||||||
content: canvasContent,
|
|
||||||
status: 'completed'
|
|
||||||
});
|
|
||||||
message.success('剧集已标记为完成');
|
|
||||||
|
|
||||||
// 更新当前剧集状态
|
|
||||||
setCurrentEpisode(prev => prev ? { ...prev, status: 'completed' } : null);
|
|
||||||
|
|
||||||
// 刷新剧集列表
|
|
||||||
useProjectStore.getState().fetchEpisodes(projectId);
|
|
||||||
} catch (error) {
|
|
||||||
message.error(`操作失败: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 剧集标题变更处理
|
|
||||||
const handleTitleChange = async (newTitle: string) => {
|
|
||||||
if (!currentEpisode || !projectId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 发送 WebSocket 消息通知后端更新标题
|
|
||||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.send(JSON.stringify({
|
|
||||||
type: 'update_episode_title',
|
|
||||||
episodeNumber: currentEpisode.number,
|
|
||||||
title: newTitle
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 调用 API 更新标题
|
|
||||||
const response = await projectService.updateEpisode(projectId, currentEpisode.number, {
|
|
||||||
title: newTitle
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedEpisode = response.data || response;
|
|
||||||
|
|
||||||
// 3. 本地同步更新状态
|
|
||||||
setCurrentEpisode(updatedEpisode);
|
|
||||||
|
|
||||||
// 4. 刷新剧集列表同步显示
|
|
||||||
useProjectStore.getState().updateEpisodeInList(updatedEpisode);
|
|
||||||
|
|
||||||
message.success('标题已更新');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update title:', error);
|
|
||||||
message.error('更新标题失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AI 辅助修改处理
|
|
||||||
const handleAIAssist = async (content: string, options?: { skills?: any[]; customPrompt?: string; injectAgent?: boolean }) => {
|
|
||||||
if (!currentEpisode || !projectId) {
|
|
||||||
message.warning('请先选择要修改的剧集');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果启用了 Agent 注入,通过 WebSocket 发送请求(用户看不到注入内容)
|
|
||||||
if (options?.injectAgent !== false) {
|
|
||||||
// 立即更新状态,显示停止按钮
|
|
||||||
setAgentStatus('planning');
|
|
||||||
|
|
||||||
// 构建优化的提示词,确保Agent使用write_to_canvas工具
|
|
||||||
let assistPrompt = '请帮我优化当前剧集内容。\n\n';
|
|
||||||
|
|
||||||
// 如果有自定义提示词,添加到前面
|
|
||||||
if (options?.customPrompt) {
|
|
||||||
assistPrompt = `${options.customPrompt}\n\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
assistPrompt += `当前剧集:EP${currentEpisode.number} ${currentEpisode.title ? '- ' + currentEpisode.title : ''}\n\n`;
|
|
||||||
|
|
||||||
// 添加当前内容摘要
|
|
||||||
assistPrompt += `当前内容(约${content.length}字):\n${content.substring(0, 1000)}${content.length > 1000 ? '...' : ''}\n\n`;
|
|
||||||
|
|
||||||
// 如果有选择的 Skills,添加到提示中
|
|
||||||
if (options?.skills && options.skills.length > 0) {
|
|
||||||
assistPrompt += '请应用以下 Skills 指导进行优化:\n';
|
|
||||||
options.skills.forEach(skill => {
|
|
||||||
assistPrompt += `- ${skill.name}: ${skill.description}\n`;
|
|
||||||
});
|
|
||||||
assistPrompt += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
assistPrompt += `请优化上述剧集内容,并使用 write_to_canvas 工具将完整的优化后内容直接更新到画布上(append=false,完全替换)。\n\n`;
|
|
||||||
assistPrompt += `重要:请直接调用 write_to_canvas(content="优化后的完整内容", append=false) 工具来更新画布,不要只输出文本。`;
|
|
||||||
|
|
||||||
// 构建用户友好的显示消息
|
|
||||||
let displayMessage = 'AI 辅助修改';
|
|
||||||
if (options?.customPrompt) {
|
|
||||||
displayMessage += `:${options.customPrompt}`;
|
|
||||||
}
|
|
||||||
if (options?.skills && options.skills.length > 0) {
|
|
||||||
const skillNames = options.skills.map(s => s.name).join('、');
|
|
||||||
displayMessage += `\n应用 Skills:${skillNames}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先在聊天记录中显示用户友好的消息
|
|
||||||
setChatHistory(prev => [...prev, { role: 'user', content: displayMessage }]);
|
|
||||||
|
|
||||||
// 通过 WebSocket 发送 AI 辅助请求给 Agent(内部 prompt)
|
|
||||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.send(JSON.stringify({
|
|
||||||
type: 'chat_message',
|
|
||||||
content: assistPrompt
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
message.error('Agent 连接未就绪');
|
|
||||||
setAgentStatus('idle');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 否则直接调用 AI 辅助 API
|
|
||||||
try {
|
|
||||||
message.loading('AI 正在辅助修改中...', 0)
|
|
||||||
const response = await fetch('/api/v1/ai-assistant/optimize-episode', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
projectId,
|
|
||||||
episodeNumber: currentEpisode.number,
|
|
||||||
content,
|
|
||||||
skills: options?.skills,
|
|
||||||
customPrompt: options?.customPrompt
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.success && data.optimizedContent) {
|
|
||||||
setCanvasContent(data.optimizedContent)
|
|
||||||
message.success('AI 辅助修改完成')
|
|
||||||
} else {
|
|
||||||
message.error('AI 辅助修改失败')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.error('AI 辅助请求失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('AI 辅助失败:', error)
|
|
||||||
message.error('AI 辅助请求出错')
|
|
||||||
} finally {
|
|
||||||
message.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '24px', textAlign: 'center', marginTop: '100px' }}>
|
|
||||||
<Spin size="large" tip="正在初始化数字化片场..." />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '24px', textAlign: 'center', marginTop: '100px' }}>
|
|
||||||
<Text type="danger">项目加载失败或不存在</Text>
|
|
||||||
<br />
|
|
||||||
<Button onClick={() => navigate('/projects')} style={{ marginTop: '16px' }}>
|
|
||||||
返回项目列表
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout style={{ height: '100vh', background: '#f5f5f5' }}>
|
|
||||||
<Header style={{
|
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
||||||
padding: '0 24px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
||||||
height: '56px'
|
|
||||||
}}>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<ArrowLeftOutlined style={{ color: '#fff' }} />}
|
|
||||||
onClick={() => navigate('/projects')}
|
|
||||||
style={{ color: '#fff' }}
|
|
||||||
/>
|
|
||||||
<Text strong style={{ fontSize: '18px', color: '#fff' }}>{project?.name}</Text>
|
|
||||||
<div style={{
|
|
||||||
background: 'rgba(255,255,255,0.2)',
|
|
||||||
padding: '4px 12px',
|
|
||||||
borderRadius: '12px',
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#fff'
|
|
||||||
}}>
|
|
||||||
{wsConnected ? '● Agent 在线' : '○ Agent 离线'}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<UnorderedListOutlined />}
|
|
||||||
onClick={() => setShowEpisodeSidebar(!showEpisodeSidebar)}
|
|
||||||
style={{
|
|
||||||
color: '#fff',
|
|
||||||
background: showEpisodeSidebar ? 'rgba(255,255,255,0.2)' : 'transparent',
|
|
||||||
borderRadius: '6px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
剧集列表
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<Space>
|
|
||||||
<Button style={{ borderRadius: '6px' }}>导出剧本</Button>
|
|
||||||
<Button type="primary" style={{
|
|
||||||
background: '#fff',
|
|
||||||
color: '#667eea',
|
|
||||||
borderRadius: '6px',
|
|
||||||
border: 'none'
|
|
||||||
}}>
|
|
||||||
发布
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<Layout style={{ display: 'flex', flexDirection: 'row', overflow: 'hidden', background: '#fafafa' }}>
|
|
||||||
{/* 剧集管理侧边栏 */}
|
|
||||||
{showEpisodeSidebar && (
|
|
||||||
<>
|
|
||||||
<Sider width={sidebarWidth} style={{
|
|
||||||
background: '#fff',
|
|
||||||
borderRight: '1px solid #e8e8e8',
|
|
||||||
flexShrink: 0,
|
|
||||||
boxShadow: '2px 0 8px rgba(0,0,0,0.04)'
|
|
||||||
}}>
|
|
||||||
<EpisodeSidebar
|
|
||||||
projectId={projectId!}
|
|
||||||
onEpisodeSelect={(episode) => {
|
|
||||||
loadEpisodeContent(episode.number);
|
|
||||||
}}
|
|
||||||
currentEpisodeId={currentEpisode?.id}
|
|
||||||
onSendMessage={handleDirectorMessage}
|
|
||||||
/>
|
|
||||||
</Sider>
|
|
||||||
{/* 拖拽手柄 - Sidebar */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '4px',
|
|
||||||
cursor: 'col-resize',
|
|
||||||
backgroundColor: '#e8e8e8',
|
|
||||||
height: '100%',
|
|
||||||
zIndex: 1000,
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
flexShrink: 0,
|
|
||||||
userSelect: 'none',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => startResize('sidebar', e)}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = '#667eea';
|
|
||||||
e.currentTarget.style.width = '6px';
|
|
||||||
e.currentTarget.style.boxShadow = '0 0 8px rgba(102, 126, 234, 0.4)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = '#e8e8e8';
|
|
||||||
e.currentTarget.style.width = '4px';
|
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 左侧:Context Panel */}
|
|
||||||
<ContextPanel
|
|
||||||
project={project}
|
|
||||||
loading={loading}
|
|
||||||
activeStates={activeStates}
|
|
||||||
memoryItems={memoryItems}
|
|
||||||
width={contextWidth}
|
|
||||||
onNavigateToSettings={() => navigate(`/projects/${projectId}`, { state: { activeTab: 'global-generation' } })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 拖拽手柄 - Context/Canvas 分隔线 */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '4px',
|
|
||||||
cursor: 'col-resize',
|
|
||||||
backgroundColor: '#e8e8e8',
|
|
||||||
height: '100%',
|
|
||||||
zIndex: 1000,
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
flexShrink: 0,
|
|
||||||
userSelect: 'none',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => startResize('context', e)}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = '#667eea';
|
|
||||||
e.currentTarget.style.width = '6px';
|
|
||||||
e.currentTarget.style.boxShadow = '0 0 8px rgba(102, 126, 234, 0.4)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = '#e8e8e8';
|
|
||||||
e.currentTarget.style.width = '4px';
|
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 中间:Smart Canvas */}
|
|
||||||
<div style={{
|
|
||||||
width: canvasWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
background: '#fff',
|
|
||||||
boxShadow: '0 0 20px rgba(0,0,0,0.05)'
|
|
||||||
}}>
|
|
||||||
<SmartCanvas
|
|
||||||
content={canvasContent}
|
|
||||||
streaming={streaming}
|
|
||||||
annotations={annotations}
|
|
||||||
episodeTitle={currentEpisode?.title || (currentEpisode?.number ? `第${currentEpisode.number}集` : '未命名草稿')}
|
|
||||||
episodeNumber={currentEpisode?.number || null}
|
|
||||||
episodeStatus={currentEpisode?.status as 'pending' | 'draft' | 'writing' | 'completed' | undefined}
|
|
||||||
onStartGenerate={() => {
|
|
||||||
handleDirectorMessage(`开始生成第${currentEpisode?.number}集大纲`);
|
|
||||||
}}
|
|
||||||
onStartWriting={() => {
|
|
||||||
handleDirectorMessage(`开始创作第${currentEpisode?.number}集完整内容`);
|
|
||||||
}}
|
|
||||||
onContentChange={handleContentChange}
|
|
||||||
onContentSave={handleContentSave}
|
|
||||||
onAIAssist={handleAIAssist}
|
|
||||||
onTitleChange={handleTitleChange}
|
|
||||||
onConfirmComplete={handleConfirmComplete}
|
|
||||||
availableSkills={availableSkills}
|
|
||||||
projectId={projectId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Canvas 右侧拖拽手柄 */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '4px',
|
|
||||||
cursor: 'col-resize',
|
|
||||||
backgroundColor: '#e8e8e8',
|
|
||||||
height: '100%',
|
|
||||||
zIndex: 1000,
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
flexShrink: 0,
|
|
||||||
userSelect: 'none',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => startResize('canvasRight', e)}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = '#667eea';
|
|
||||||
e.currentTarget.style.width = '6px';
|
|
||||||
e.currentTarget.style.boxShadow = '0 0 8px rgba(102, 126, 234, 0.4)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = '#e8e8e8';
|
|
||||||
e.currentTarget.style.width = '4px';
|
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 右侧:Director Inbox */}
|
|
||||||
<DirectorInbox
|
|
||||||
onSendMessage={handleDirectorMessage}
|
|
||||||
onStopAgent={handleStopAgent}
|
|
||||||
onInboxAction={handleInboxAction}
|
|
||||||
onNavigateToReview={(episodeNum) => navigate(`/projects/${projectId}/review/results?episode=${episodeNum}`)}
|
|
||||||
agentStatus={agentStatus}
|
|
||||||
agentPlan={agentPlan}
|
|
||||||
inboxItems={inboxItems}
|
|
||||||
chatHistory={chatHistory}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Review Results Page
|
* Review Platform Page
|
||||||
* Display review results with scores, issues, and actions
|
*
|
||||||
|
* 审核平台页面 - 审核卡片列表和详情
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 显示所有审核卡片
|
||||||
|
* 2. 按状态筛选
|
||||||
|
* 3. 统计数据展示
|
||||||
|
* 4. 点击卡片查看详情
|
||||||
|
* 5. 详情中执行审核操作(通过/驳回/修改)
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
@ -9,598 +17,436 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
Statistic,
|
Statistic,
|
||||||
Progress,
|
|
||||||
List,
|
List,
|
||||||
Tag,
|
Tag,
|
||||||
Button,
|
Button,
|
||||||
Space,
|
Space,
|
||||||
Alert,
|
|
||||||
Tabs,
|
|
||||||
Modal,
|
|
||||||
Form,
|
|
||||||
Select,
|
Select,
|
||||||
message,
|
message,
|
||||||
Divider,
|
|
||||||
Typography,
|
Typography,
|
||||||
Empty,
|
Empty,
|
||||||
Spin
|
Spin,
|
||||||
|
Badge,
|
||||||
|
Modal,
|
||||||
|
Alert,
|
||||||
|
Descriptions,
|
||||||
|
Progress,
|
||||||
|
Input
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CloseCircleOutlined,
|
ClockCircleOutlined,
|
||||||
|
WarningOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
EyeInvisibleOutlined,
|
FilterOutlined,
|
||||||
ToolOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
WarningOutlined
|
CloseCircleOutlined,
|
||||||
|
EditOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend, ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts'
|
import { useReviewCardStore } from '@/stores/reviewCardStore'
|
||||||
import { useReviewStore } from '@/stores/reviewStore'
|
import { ReviewCard, ReviewCardStatus } from '@/services/reviewCardService'
|
||||||
import { SeverityLevel, Issue } from '@/services/reviewService'
|
|
||||||
|
|
||||||
const { TabPane } = Tabs
|
|
||||||
const { Title, Text, Paragraph } = Typography
|
const { Title, Text, Paragraph } = Typography
|
||||||
|
const { TextArea } = Input
|
||||||
|
|
||||||
const ReviewResults: React.FC = () => {
|
interface ReviewResultsProps {
|
||||||
const { id: projectId } = useParams<{ id: string }>()
|
projectId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
const statusConfig: Record<ReviewCardStatus, { label: string; color: string; icon: React.ReactNode }> = {
|
||||||
|
[ReviewCardStatus.PENDING]: { label: '待处理', color: 'default', icon: <ClockCircleOutlined /> },
|
||||||
|
[ReviewCardStatus.AWAITING_USER]: { label: '待确认', color: 'warning', icon: <WarningOutlined /> },
|
||||||
|
[ReviewCardStatus.APPROVED]: { label: '已通过', color: 'success', icon: <CheckCircleOutlined /> },
|
||||||
|
[ReviewCardStatus.REJECTED]: { label: '已驳回', color: 'error', icon: <CloseCircleOutlined /> }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 严重程度映射
|
||||||
|
const severityConfig: Record<'low' | 'medium' | 'high', { label: string; color: string }> = {
|
||||||
|
low: { label: '低', color: 'green' },
|
||||||
|
medium: { label: '中', color: 'orange' },
|
||||||
|
high: { label: '高', color: 'red' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReviewResults: React.FC<ReviewResultsProps> = ({ projectId: propProjectId }) => {
|
||||||
|
const { id: routeProjectId } = useParams<{ id: string }>()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const projectId = propProjectId || routeProjectId
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentResult,
|
cards,
|
||||||
configuration,
|
stats,
|
||||||
|
statusFilter,
|
||||||
loading,
|
loading,
|
||||||
error,
|
fetchCards,
|
||||||
executeReview,
|
fetchStats,
|
||||||
fetchLatestResult,
|
approveCard,
|
||||||
ignoreIssue,
|
rejectCard,
|
||||||
unignoreIssue,
|
modifyCard,
|
||||||
autoFixIssue,
|
setStatusFilter
|
||||||
applyBatchDecisions,
|
} = useReviewCardStore()
|
||||||
exportReport
|
|
||||||
} = useReviewStore()
|
|
||||||
|
|
||||||
const [selectedEpisode, setSelectedEpisode] = useState<number | undefined>()
|
const [selectedCard, setSelectedCard] = useState<ReviewCard | null>(null)
|
||||||
const [batchActionModalVisible, setBatchActionModalVisible] = useState(false)
|
const [detailVisible, setDetailVisible] = useState(false)
|
||||||
const [selectedSeverity, setSelectedSeverity] = useState<SeverityLevel | 'all'>('all')
|
const [actionLoading, setActionLoading] = useState(false)
|
||||||
|
|
||||||
// Parse query params for initial episode selection
|
// 操作相关
|
||||||
useEffect(() => {
|
const [actionModalVisible, setActionModalVisible] = useState(false)
|
||||||
const params = new URLSearchParams(window.location.search)
|
const [actionType, setActionType] = useState<'approve' | 'reject' | 'modify'>('approve')
|
||||||
const epParam = params.get('episode')
|
const [userComment, setUserComment] = useState('')
|
||||||
if (epParam) {
|
|
||||||
setSelectedEpisode(parseInt(epParam, 10))
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
fetchLatestResult(projectId, selectedEpisode)
|
fetchCards(projectId)
|
||||||
|
fetchStats(projectId)
|
||||||
}
|
}
|
||||||
}, [projectId, selectedEpisode])
|
}, [projectId])
|
||||||
|
|
||||||
const severityColors: Record<SeverityLevel, string> = {
|
// 刷新数据
|
||||||
low: 'green',
|
const handleRefresh = async () => {
|
||||||
medium: 'orange',
|
if (projectId) {
|
||||||
high: 'red'
|
await Promise.all([
|
||||||
|
fetchCards(projectId),
|
||||||
|
fetchStats(projectId)
|
||||||
|
])
|
||||||
|
message.success('刷新成功')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const severityLabels: Record<SeverityLevel, string> = {
|
// 筛选变更
|
||||||
low: '低',
|
const handleStatusChange = (value: ReviewCardStatus | 'all') => {
|
||||||
medium: '中',
|
setStatusFilter(value)
|
||||||
high: '高'
|
if (projectId) {
|
||||||
|
fetchCards(projectId, value === 'all' ? undefined : value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getScoreColor = (score: number) => {
|
// 查看详情
|
||||||
if (score >= 80) return '#52c41a'
|
const handleViewDetail = (card: ReviewCard) => {
|
||||||
if (score >= 60) return '#faad14'
|
setSelectedCard(card)
|
||||||
return '#f5222d'
|
setDetailVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExecuteReview = async () => {
|
// 操作
|
||||||
if (!projectId) return
|
const handleAction = (type: 'approve' | 'reject' | 'modify') => {
|
||||||
|
setActionType(type)
|
||||||
|
setUserComment('')
|
||||||
|
setActionModalVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleActionSubmit = async () => {
|
||||||
|
if (!selectedCard) return
|
||||||
|
|
||||||
|
setActionLoading(true)
|
||||||
try {
|
try {
|
||||||
await executeReview(projectId, selectedEpisode)
|
if (actionType === 'approve') {
|
||||||
message.success('审核完成')
|
await approveCard(selectedCard.id, userComment || undefined)
|
||||||
} catch (error) {
|
message.success('已通过审核')
|
||||||
message.error('审核失败')
|
} else if (actionType === 'reject') {
|
||||||
}
|
await rejectCard(selectedCard.id, userComment || undefined)
|
||||||
}
|
message.success('已驳回审核')
|
||||||
|
} else if (actionType === 'modify') {
|
||||||
const handleIgnoreIssue = async (issueId: string) => {
|
await modifyCard(selectedCard.id, userComment || undefined)
|
||||||
if (!projectId) return
|
message.success('已标记为修改')
|
||||||
try {
|
|
||||||
await ignoreIssue(projectId, issueId)
|
|
||||||
message.success('已忽略问题')
|
|
||||||
} catch (error) {
|
|
||||||
message.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUnignoreIssue = async (issueId: string) => {
|
|
||||||
if (!projectId) return
|
|
||||||
try {
|
|
||||||
await unignoreIssue(projectId, issueId)
|
|
||||||
message.success('已恢复问题')
|
|
||||||
} catch (error) {
|
|
||||||
message.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAutoFix = async (issueId: string) => {
|
|
||||||
if (!projectId) return
|
|
||||||
try {
|
|
||||||
await autoFixIssue(projectId, issueId)
|
|
||||||
message.success('自动修复完成')
|
|
||||||
} catch (error) {
|
|
||||||
message.error('自动修复失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBatchAction = async (action: 'accept' | 'reject' | 'ignore') => {
|
|
||||||
if (!projectId || !currentResult) return
|
|
||||||
try {
|
|
||||||
const decisions = currentResult.issues
|
|
||||||
.filter(issue => !issue.ignored)
|
|
||||||
.filter(issue => selectedSeverity === 'all' || issue.severity === selectedSeverity)
|
|
||||||
.map(issue => ({ issueId: issue.id, action }))
|
|
||||||
|
|
||||||
await applyBatchDecisions(projectId, decisions)
|
|
||||||
message.success(`批量${action === 'accept' ? '接受' : action === 'reject' ? '拒绝' : '忽略'}完成`)
|
|
||||||
setBatchActionModalVisible(false)
|
|
||||||
} catch (error) {
|
|
||||||
message.error('批量操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExportReport = async (format: 'json' | 'pdf' | 'markdown') => {
|
|
||||||
if (!projectId || !currentResult) return
|
|
||||||
try {
|
|
||||||
await exportReport(projectId, currentResult.id || 'latest', format, false)
|
|
||||||
message.success('导出成功')
|
|
||||||
} catch (error) {
|
|
||||||
message.error('导出失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedIssues = currentResult?.issues.reduce((acc, issue) => {
|
|
||||||
if (!acc[issue.severity]) {
|
|
||||||
acc[issue.severity] = []
|
|
||||||
}
|
|
||||||
if (!issue.ignored) {
|
|
||||||
acc[issue.severity].push(issue)
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {} as Record<SeverityLevel, Issue[]>) || {}
|
|
||||||
|
|
||||||
const visibleIssues = currentResult?.issues.filter(issue => !issue.ignored) || []
|
|
||||||
const ignoredIssues = currentResult?.issues.filter(issue => issue.ignored) || []
|
|
||||||
|
|
||||||
const renderOverallScore = () => {
|
|
||||||
if (!currentResult) return null
|
|
||||||
|
|
||||||
const score = currentResult.overallScore
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={8}>
|
|
||||||
<Statistic
|
|
||||||
title="总体评分"
|
|
||||||
value={score}
|
|
||||||
suffix="/ 100"
|
|
||||||
valueStyle={{ color: getScoreColor(score), fontSize: 48 }}
|
|
||||||
/>
|
|
||||||
<Progress
|
|
||||||
percent={score}
|
|
||||||
strokeColor={getScoreColor(score)}
|
|
||||||
showInfo={false}
|
|
||||||
style={{ marginTop: 16 }}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<Statistic
|
|
||||||
title="问题总数"
|
|
||||||
value={visibleIssues.length}
|
|
||||||
suffix={`/ ${currentResult.issues.length}`}
|
|
||||||
valueStyle={{ color: visibleIssues.length > 0 ? '#f5222d' : '#52c41a' }}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<div>
|
|
||||||
<Tag color="red">高: {groupedIssues.high?.length || 0}</Tag>
|
|
||||||
<Tag color="orange">中: {groupedIssues.medium?.length || 0}</Tag>
|
|
||||||
<Tag color="green">低: {groupedIssues.low?.length || 0}</Tag>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<Space direction="vertical">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
onClick={handleExecuteReview}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
重新审核
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={() => setBatchActionModalVisible(true)}
|
|
||||||
>
|
|
||||||
批量操作
|
|
||||||
</Button>
|
|
||||||
<Button.Group>
|
|
||||||
<Button size="small" onClick={() => handleExportReport('json')}>JSON</Button>
|
|
||||||
<Button size="small" onClick={() => handleExportReport('pdf')}>PDF</Button>
|
|
||||||
<Button size="small" onClick={() => handleExportReport('markdown')}>MD</Button>
|
|
||||||
</Button.Group>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderDimensionScores = () => {
|
|
||||||
if (!currentResult) return null
|
|
||||||
|
|
||||||
const chartData = currentResult.dimensionScores.map(ds => ({
|
|
||||||
dimension: ds.dimension,
|
|
||||||
score: ds.score,
|
|
||||||
weight: ds.weight,
|
|
||||||
fullMark: 100
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="维度评分">
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Title level={5}>柱状图</Title>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<BarChart data={chartData}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="dimension" angle={-45} textAnchor="end" height={100} />
|
|
||||||
<YAxis />
|
|
||||||
<RechartsTooltip />
|
|
||||||
<Legend />
|
|
||||||
<Bar dataKey="score" fill="#0ea5e9" name="分数" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Title level={5}>雷达图</Title>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<RadarChart data={chartData}>
|
|
||||||
<PolarGrid />
|
|
||||||
<PolarAngleAxis dataKey="dimension" />
|
|
||||||
<PolarRadiusAxis angle={30} domain={[0, 100]} />
|
|
||||||
<Radar name="分数" dataKey="score" stroke="#0ea5e9" fill="#0ea5e9" fillOpacity={0.6} />
|
|
||||||
</RadarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Divider />
|
|
||||||
<List
|
|
||||||
dataSource={currentResult.dimensionScores}
|
|
||||||
renderItem={(ds) => (
|
|
||||||
<List.Item>
|
|
||||||
<List.Item.Meta
|
|
||||||
title={
|
|
||||||
<Space>
|
|
||||||
{ds.dimension}
|
|
||||||
<Tag color="blue">权重: {(ds.weight * 100).toFixed(0)}%</Tag>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Progress
|
|
||||||
percent={ds.score}
|
|
||||||
strokeColor={getScoreColor(ds.score)}
|
|
||||||
format={(percent) => `${percent}分`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div style={{ width: 200 }}>
|
|
||||||
<Statistic
|
|
||||||
value={ds.score}
|
|
||||||
suffix={`/ 100`}
|
|
||||||
valueStyle={{ color: getScoreColor(ds.score), fontSize: 20 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderIssueCard = (issue: Issue) => (
|
|
||||||
<Card
|
|
||||||
key={issue.id}
|
|
||||||
size="small"
|
|
||||||
style={{ marginBottom: 16 }}
|
|
||||||
title={
|
|
||||||
<Space>
|
|
||||||
<Tag color={severityColors[issue.severity]}>{severityLabels[issue.severity]}</Tag>
|
|
||||||
<span>{issue.type}</span>
|
|
||||||
<Tag color="blue">{issue.dimension}</Tag>
|
|
||||||
</Space>
|
|
||||||
}
|
}
|
||||||
extra={
|
|
||||||
<Space>
|
setActionModalVisible(false)
|
||||||
{issue.canAutoFix && (
|
setUserComment('')
|
||||||
<Button
|
setDetailVisible(false)
|
||||||
size="small"
|
|
||||||
type="primary"
|
// 刷新数据
|
||||||
icon={<ToolOutlined />}
|
if (projectId) {
|
||||||
onClick={() => handleAutoFix(issue.id)}
|
await Promise.all([
|
||||||
>
|
fetchCards(projectId),
|
||||||
自动修复
|
fetchStats(projectId)
|
||||||
</Button>
|
])
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<EyeInvisibleOutlined />}
|
|
||||||
onClick={() => handleIgnoreIssue(issue.id)}
|
|
||||||
>
|
|
||||||
忽略
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
}
|
||||||
>
|
} catch (error: any) {
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
message.error(`操作失败: ${error?.message || ''}`)
|
||||||
<div>
|
} finally {
|
||||||
<strong>位置:</strong>{' '}
|
setActionLoading(false)
|
||||||
{issue.location.section && `${issue.location.section} - `}
|
}
|
||||||
{issue.location.episodeNumber && `第 ${issue.location.episodeNumber} 集`}
|
|
||||||
{issue.location.lineNumber && `:${issue.location.lineNumber}行`}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>问题描述:</strong> {issue.description}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>原文:</strong>
|
|
||||||
<Paragraph
|
|
||||||
ellipsis={{ rows: 2, expandable: true, symbol: '展开' }}
|
|
||||||
style={{
|
|
||||||
background: '#f5f5f5',
|
|
||||||
padding: '8px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
marginTop: '8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{issue.originalText}
|
|
||||||
</Paragraph>
|
|
||||||
</div>
|
|
||||||
{issue.suggestion && (
|
|
||||||
<div>
|
|
||||||
<strong>建议:</strong>
|
|
||||||
<Paragraph
|
|
||||||
ellipsis={{ rows: 2, expandable: true, symbol: '展开' }}
|
|
||||||
style={{
|
|
||||||
background: '#e6f7ff',
|
|
||||||
padding: '8px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
marginTop: '8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{issue.suggestion}
|
|
||||||
</Paragraph>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderIgnoredIssueCard = (issue: Issue) => (
|
|
||||||
<Card
|
|
||||||
key={issue.id}
|
|
||||||
size="small"
|
|
||||||
style={{ marginBottom: 16, opacity: 0.6 }}
|
|
||||||
type="inner"
|
|
||||||
>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<Space>
|
|
||||||
<Tag color={severityColors[issue.severity]}>{severityLabels[issue.severity]}</Tag>
|
|
||||||
<span>{issue.type}</span>
|
|
||||||
<Tag>{issue.dimension}</Tag>
|
|
||||||
<Tag color="default">已忽略</Tag>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<EyeOutlined />}
|
|
||||||
onClick={() => handleUnignoreIssue(issue.id)}
|
|
||||||
>
|
|
||||||
恢复
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
<div>{issue.description}</div>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderIssues = () => {
|
|
||||||
if (!currentResult) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="问题列表">
|
|
||||||
<Tabs defaultActiveKey="all">
|
|
||||||
<TabPane tab={`全部 (${visibleIssues.length})`} key="all">
|
|
||||||
{visibleIssues.length === 0 ? (
|
|
||||||
<Empty description="暂无问题" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
||||||
) : (
|
|
||||||
visibleIssues.map(issue => renderIssueCard(issue))
|
|
||||||
)}
|
|
||||||
</TabPane>
|
|
||||||
<TabPane
|
|
||||||
tab={
|
|
||||||
<span>
|
|
||||||
<WarningOutlined style={{ color: '#f5222d' }} />
|
|
||||||
高严重度 ({groupedIssues.high?.length || 0})
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
key="high"
|
|
||||||
>
|
|
||||||
{groupedIssues.high?.length === 0 ? (
|
|
||||||
<Empty description="暂无高严重度问题" />
|
|
||||||
) : (
|
|
||||||
groupedIssues.high?.map(issue => renderIssueCard(issue))
|
|
||||||
)}
|
|
||||||
</TabPane>
|
|
||||||
<TabPane
|
|
||||||
tab={
|
|
||||||
<span>
|
|
||||||
<WarningOutlined style={{ color: '#faad14' }} />
|
|
||||||
中严重度 ({groupedIssues.medium?.length || 0})
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
key="medium"
|
|
||||||
>
|
|
||||||
{groupedIssues.medium?.length === 0 ? (
|
|
||||||
<Empty description="暂无中严重度问题" />
|
|
||||||
) : (
|
|
||||||
groupedIssues.medium?.map(issue => renderIssueCard(issue))
|
|
||||||
)}
|
|
||||||
</TabPane>
|
|
||||||
<TabPane
|
|
||||||
tab={
|
|
||||||
<span>
|
|
||||||
<WarningOutlined style={{ color: '#52c41a' }} />
|
|
||||||
低严重度 ({groupedIssues.low?.length || 0})
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
key="low"
|
|
||||||
>
|
|
||||||
{groupedIssues.low?.length === 0 ? (
|
|
||||||
<Empty description="暂无低严重度问题" />
|
|
||||||
) : (
|
|
||||||
groupedIssues.low?.map(issue => renderIssueCard(issue))
|
|
||||||
)}
|
|
||||||
</TabPane>
|
|
||||||
<TabPane
|
|
||||||
tab={
|
|
||||||
<span>
|
|
||||||
<EyeInvisibleOutlined />
|
|
||||||
已忽略 ({ignoredIssues.length})
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
key="ignored"
|
|
||||||
>
|
|
||||||
{ignoredIssues.length === 0 ? (
|
|
||||||
<Empty description="暂无已忽略问题" />
|
|
||||||
) : (
|
|
||||||
ignoredIssues.map(issue => renderIgnoredIssueCard(issue))
|
|
||||||
)}
|
|
||||||
</TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderSummary = () => {
|
// 跳转到剧集创作
|
||||||
if (!currentResult) return null
|
const handleGoToEpisode = () => {
|
||||||
|
if (selectedCard?.episode_numbers && selectedCard.episode_numbers.length > 0) {
|
||||||
return (
|
setDetailVisible(false)
|
||||||
<Card title="审核摘要">
|
navigate(`/projects/${projectId}`, {
|
||||||
<Paragraph>{currentResult.summary}</Paragraph>
|
state: { activeTab: 'episodes', episodeNumber: selectedCard.episode_numbers[0] }
|
||||||
{currentResult.configuration && (
|
})
|
||||||
<div>
|
}
|
||||||
<Divider />
|
|
||||||
<Text type="secondary">
|
|
||||||
审核配置: {currentResult.configuration.preset}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading && !currentResult) {
|
// 过滤卡片
|
||||||
return (
|
const filteredCards = cards.filter(card => {
|
||||||
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
const matchStatus = statusFilter === 'all' || card.status === statusFilter
|
||||||
<Spin size="large" />
|
return matchStatus
|
||||||
</div>
|
})
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <div style={{ padding: '24px', color: 'red' }}>Error: {error}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentResult) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 24 }}>
|
|
||||||
<Card>
|
|
||||||
<Empty description="暂无审核结果">
|
|
||||||
<Button type="primary" onClick={handleExecuteReview} loading={loading}>
|
|
||||||
开始审核
|
|
||||||
</Button>
|
|
||||||
</Empty>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24 }}>
|
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '24px' }}>
|
||||||
<div style={{ marginBottom: 24, display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: '#fff', padding: '16px', borderRadius: '8px', boxShadow: '0 1px 2px rgba(0,0,0,0.03)' }}>
|
{/* 头部操作栏 */}
|
||||||
<Space size="middle">
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||||
<Button
|
<Title level={3} style={{ margin: 0 }}>审核平台</Title>
|
||||||
icon={<ArrowLeftOutlined />}
|
<Space>
|
||||||
onClick={() => navigate(`/projects/${projectId}`)}
|
<Select
|
||||||
size="large"
|
value={statusFilter}
|
||||||
|
onChange={handleStatusChange}
|
||||||
|
style={{ width: 150 }}
|
||||||
>
|
>
|
||||||
返回项目详情
|
<Select.Option value="all">全部状态</Select.Option>
|
||||||
|
{Object.entries(statusConfig).map(([key, config]) => (
|
||||||
|
<Select.Option key={key} value={key}>
|
||||||
|
{config.icon} {config.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={handleRefresh} loading={loading}>
|
||||||
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
|
||||||
<Title level={4} style={{ margin: 0 }}>审核结果</Title>
|
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
||||||
{selectedEpisode ? `EP${selectedEpisode} 质量分析报告` : '最新审核报告'}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
{/* 统计卡片 */}
|
||||||
{renderOverallScore()}
|
{stats && (
|
||||||
{renderDimensionScores()}
|
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||||
{renderIssues()}
|
<Col span={4}>
|
||||||
{renderSummary()}
|
<Card>
|
||||||
</Space>
|
<Statistic title="总计" value={stats.total} valueStyle={{ color: '#1890ff' }} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="待处理" value={stats.pending} valueStyle={{ color: '#faad14' }} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="待确认" value={stats.awaiting_user} valueStyle={{ color: '#ff4d4f' }} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="已通过" value={stats.approved} valueStyle={{ color: '#52c41a' }} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="已驳回" value={stats.rejected} valueStyle={{ color: '#f5222d' }} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Batch Action Modal */}
|
{/* 卡片列表 */}
|
||||||
<Modal
|
<Card
|
||||||
title="批量操作"
|
title={<><FilterOutlined /> 审核卡片列表</>}
|
||||||
open={batchActionModalVisible}
|
extra={<Badge count={filteredCards.length} showZero />}
|
||||||
onCancel={() => setBatchActionModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
{loading ? (
|
||||||
<Form.Item label="筛选严重度">
|
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||||
<Select
|
<Spin size="large" />
|
||||||
value={selectedSeverity}
|
</div>
|
||||||
onChange={setSelectedSeverity}
|
) : filteredCards.length === 0 ? (
|
||||||
style={{ width: '100%' }}
|
<Empty description="暂无审核卡片" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
>
|
) : (
|
||||||
<Select.Option value="all">全部</Select.Option>
|
<List
|
||||||
<Select.Option value="high">高</Select.Option>
|
dataSource={filteredCards}
|
||||||
<Select.Option value="medium">中</Select.Option>
|
renderItem={(card) => (
|
||||||
<Select.Option value="low">低</Select.Option>
|
<List.Item
|
||||||
</Select>
|
key={card.id}
|
||||||
</Form.Item>
|
style={{
|
||||||
<Alert
|
border: '1px solid #f0f0f0',
|
||||||
message={`将对 ${visibleIssues.filter(i => selectedSeverity === 'all' || i.severity === selectedSeverity).length} 个问题执行批量操作`}
|
borderRadius: '8px',
|
||||||
type="info"
|
marginBottom: '16px',
|
||||||
|
padding: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
onClick={() => handleViewDetail(card)}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'
|
||||||
|
e.currentTarget.style.borderColor = '#1890ff'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.boxShadow = 'none'
|
||||||
|
e.currentTarget.style.borderColor = '#f0f0f0'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={
|
||||||
|
<Badge
|
||||||
|
status={severityConfig[card.severity as keyof typeof severityConfig]?.color as any}
|
||||||
|
text={
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
|
||||||
|
{card.overall_score ? Math.round(card.overall_score) : '-'}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999' }}>分</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<Text strong>EP{card.episode_numbers.join(', ')}</Text>
|
||||||
|
<Tag color={severityConfig[card.severity as keyof typeof severityConfig]?.color}>
|
||||||
|
{severityConfig[card.severity as keyof typeof severityConfig]?.label}
|
||||||
|
</Tag>
|
||||||
|
<Tag icon={statusConfig[card.status].icon} color={statusConfig[card.status].color}>
|
||||||
|
{statusConfig[card.status].label}
|
||||||
|
</Tag>
|
||||||
|
{card.issues.length > 0 && <Tag color="orange">{card.issues.length} 个问题</Tag>}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<Text type="secondary">{card.review_reason}</Text>
|
||||||
|
<div style={{ marginTop: '8px', fontSize: '12px', color: '#999' }}>
|
||||||
|
创建时间: {new Date(card.created_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button type="link" icon={<EyeOutlined />}>
|
||||||
|
查看详情
|
||||||
|
</Button>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 详情模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={() => handleBatchAction('ignore')}>
|
<span>审核卡片详情 - EP{selectedCard?.episode_numbers.join(', ')}</span>
|
||||||
<EyeInvisibleOutlined /> 批量忽略
|
<Tag color={selectedCard ? severityConfig[selectedCard.severity]?.color : ''}>
|
||||||
</Button>
|
{selectedCard ? severityConfig[selectedCard.severity]?.label : ''}
|
||||||
<Button onClick={() => setBatchActionModalVisible(false)}>
|
</Tag>
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
}
|
||||||
|
open={detailVisible}
|
||||||
|
onCancel={() => setDetailVisible(false)}
|
||||||
|
width={900}
|
||||||
|
footer={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setDetailVisible(false)}>关闭</Button>
|
||||||
|
{selectedCard?.status === ReviewCardStatus.AWAITING_USER && (
|
||||||
|
<>
|
||||||
|
<Button danger onClick={() => handleAction('reject')}>驳回</Button>
|
||||||
|
<Button onClick={() => handleAction('modify')}>标记修改</Button>
|
||||||
|
<Button type="primary" onClick={() => handleAction('approve')}>通过</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{selectedCard?.episode_numbers && selectedCard.episode_numbers.length > 0 && (
|
||||||
|
<Button icon={<EditOutlined />} onClick={handleGoToEpisode}>
|
||||||
|
去剧集创作修改
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedCard && (
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<Descriptions column={2} bordered size="small">
|
||||||
|
<Descriptions.Item label="状态">
|
||||||
|
<Tag icon={statusConfig[selectedCard.status].icon} color={statusConfig[selectedCard.status].color}>
|
||||||
|
{statusConfig[selectedCard.status].label}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="严重度">
|
||||||
|
<Tag color={severityConfig[selectedCard.severity]?.color}>
|
||||||
|
{severityConfig[selectedCard.severity]?.label}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="总体评分" span={2}>
|
||||||
|
{selectedCard.overall_score ? (
|
||||||
|
<Progress
|
||||||
|
percent={Math.round(selectedCard.overall_score)}
|
||||||
|
strokeColor={selectedCard.overall_score >= 80 ? '#52c41a' : selectedCard.overall_score >= 60 ? '#faad14' : '#f5222d'}
|
||||||
|
/>
|
||||||
|
) : '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="审核原因" span={2}>
|
||||||
|
{selectedCard.review_reason}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="创建时间" span={2}>
|
||||||
|
{new Date(selectedCard.created_at).toLocaleString()}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
{/* 问题列表 */}
|
||||||
|
{selectedCard.issues.length > 0 && (
|
||||||
|
<Card title={`问题列表 (${selectedCard.issues.length})`} size="small">
|
||||||
|
<List
|
||||||
|
dataSource={selectedCard.issues}
|
||||||
|
renderItem={(issue: any, index) => (
|
||||||
|
<List.Item key={index}>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={
|
||||||
|
<Tag color={issue.severity === 'high' ? 'red' : issue.severity === 'medium' ? 'orange' : 'green'}>
|
||||||
|
{issue.severity === 'high' ? '高' : issue.severity === 'medium' ? '中' : '低'}
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
title={issue.type}
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<Paragraph style={{ margin: 0 }}>{issue.description}</Paragraph>
|
||||||
|
{issue.suggestion && (
|
||||||
|
<Alert type="info" message="建议" description={issue.suggestion} style={{ marginTop: 8 }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 剧集内容 */}
|
||||||
|
{selectedCard.episode_outline && (
|
||||||
|
<Card title="剧集大纲" size="small">
|
||||||
|
<Paragraph style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{selectedCard.episode_outline}</Paragraph>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedCard.episode_content && (
|
||||||
|
<Card title="剧集内容" size="small">
|
||||||
|
<Paragraph style={{ whiteSpace: 'pre-wrap', margin: 0, maxHeight: 300, overflowY: 'auto' }}>
|
||||||
|
{selectedCard.episode_content}
|
||||||
|
</Paragraph>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 已有评论 */}
|
||||||
|
{selectedCard.user_comment && (
|
||||||
|
<Alert type="info" message="用户评论" description={selectedCard.user_comment} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 操作模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={actionType === 'approve' ? '通过审核' : actionType === 'reject' ? '驳回审核' : '标记修改'}
|
||||||
|
open={actionModalVisible}
|
||||||
|
onOk={handleActionSubmit}
|
||||||
|
onCancel={() => setActionModalVisible(false)}
|
||||||
|
confirmLoading={actionLoading}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
placeholder="请输入评论(可选)"
|
||||||
|
value={userComment}
|
||||||
|
onChange={(e) => setUserComment(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
maxLength={500}
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -47,7 +47,7 @@ import {
|
|||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
CheckOutlined
|
CheckOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useSearchParams, useLocation } from 'react-router-dom'
|
||||||
import { useSkillStore, Skill } from '@/stores/skillStore'
|
import { useSkillStore, Skill } from '@/stores/skillStore'
|
||||||
import { skillService, SkillDraft } from '@/services/skillService'
|
import { skillService, SkillDraft } from '@/services/skillService'
|
||||||
import { taskService } from '@/services/taskService'
|
import { taskService } from '@/services/taskService'
|
||||||
@ -76,6 +76,8 @@ type CreateMode = 'ai' | 'manual' | 'template'
|
|||||||
|
|
||||||
export const SkillManagement = () => {
|
export const SkillManagement = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const location = useLocation()
|
||||||
const { skills, builtinSkills, userSkills, loading, fetchSkills, testSkill } = useSkillStore()
|
const { skills, builtinSkills, userSkills, loading, fetchSkills, testSkill } = useSkillStore()
|
||||||
|
|
||||||
// List view states
|
// List view states
|
||||||
@ -122,6 +124,8 @@ export const SkillManagement = () => {
|
|||||||
|
|
||||||
// 向导状态
|
// 向导状态
|
||||||
const [wizardVisible, setWizardVisible] = useState(false)
|
const [wizardVisible, setWizardVisible] = useState(false)
|
||||||
|
// 跳转回退信息(从外部跳转过来时使用)
|
||||||
|
const [redirectState, setRedirectState] = useState<{ path: string; activeTab?: string; reviewSubTab?: string } | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSkills()
|
fetchSkills()
|
||||||
@ -153,6 +157,69 @@ export const SkillManagement = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 处理URL参数和location.state - 支持通过URL参数打开编辑或创建
|
||||||
|
useEffect(() => {
|
||||||
|
// 从 location.state 读取 redirect 信息
|
||||||
|
const state = location.state as any
|
||||||
|
if (state?.redirect) {
|
||||||
|
setRedirectState({
|
||||||
|
path: state.redirect,
|
||||||
|
activeTab: state.activeTab,
|
||||||
|
reviewSubTab: state.reviewSubTab
|
||||||
|
})
|
||||||
|
// 清除 state 避免重复处理
|
||||||
|
location.state = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑参数 - 使用新的 SkillCreate 向导组件
|
||||||
|
const editSkillId = searchParams.get('edit')
|
||||||
|
if (editSkillId) {
|
||||||
|
const skillToEdit = skills.find(s => s.id === editSkillId)
|
||||||
|
if (skillToEdit) {
|
||||||
|
setEditingSkill(skillToEdit)
|
||||||
|
setWizardVisible(true)
|
||||||
|
// 清除URL参数
|
||||||
|
searchParams.delete('edit')
|
||||||
|
navigate(`/skills?${searchParams.toString()}`, { replace: true })
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// Skill 不在本地列表中,尝试从服务器直接获取
|
||||||
|
// 这允许编辑在 review 配置中引用但不在主列表中的技能
|
||||||
|
const fetchSkillForEdit = async () => {
|
||||||
|
try {
|
||||||
|
const result = await skillService.getSkillWithReferences(editSkillId, false)
|
||||||
|
if (result?.skill) {
|
||||||
|
setEditingSkill(result.skill)
|
||||||
|
setWizardVisible(true)
|
||||||
|
} else {
|
||||||
|
message.error(`Skill ID "${editSkillId}" 不存在`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch skill for edit:', error)
|
||||||
|
message.error(`无法加载 Skill "${editSkillId}": ${(error as Error).message}`)
|
||||||
|
} finally {
|
||||||
|
// 清除URL参数
|
||||||
|
searchParams.delete('edit')
|
||||||
|
navigate(`/skills?${searchParams.toString()}`, { replace: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchSkillForEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理创建参数 - 使用新的 SkillCreate 向导组件
|
||||||
|
const shouldCreate = searchParams.get('create')
|
||||||
|
if (shouldCreate === 'true') {
|
||||||
|
// 确保重置 editingSkill,避免误用之前的编辑状态
|
||||||
|
setEditingSkill(null)
|
||||||
|
setWizardVisible(true)
|
||||||
|
// 清除URL参数
|
||||||
|
searchParams.delete('create')
|
||||||
|
navigate(`/skills?${searchParams.toString()}`, { replace: true })
|
||||||
|
}
|
||||||
|
}, [skills, searchParams, navigate, location])
|
||||||
|
|
||||||
// 获取技能生成任务
|
// 获取技能生成任务
|
||||||
const fetchSkillGenerationTasks = async () => {
|
const fetchSkillGenerationTasks = async () => {
|
||||||
try {
|
try {
|
||||||
@ -841,7 +908,7 @@ export const SkillManagement = () => {
|
|||||||
}}
|
}}
|
||||||
width={800}
|
width={800}
|
||||||
footer={null}
|
footer={null}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
{selectedSkill && (
|
{selectedSkill && (
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||||
@ -943,7 +1010,7 @@ export const SkillManagement = () => {
|
|||||||
}}
|
}}
|
||||||
width={900}
|
width={900}
|
||||||
footer={null}
|
footer={null}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={createMode}
|
activeKey={createMode}
|
||||||
@ -1430,7 +1497,7 @@ export const SkillManagement = () => {
|
|||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
@ -1491,11 +1558,19 @@ export const SkillManagement = () => {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setWizardVisible(false)
|
setWizardVisible(false)
|
||||||
setEditingSkill(null)
|
setEditingSkill(null)
|
||||||
|
// 如果有跳转回退信息,关闭时跳转回去
|
||||||
|
if (redirectState) {
|
||||||
|
navigate(redirectState.path, {
|
||||||
|
state: { activeTab: redirectState.activeTab, reviewSubTab: redirectState.reviewSubTab }
|
||||||
|
})
|
||||||
|
setRedirectState(null)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
fetchSkills()
|
fetchSkills()
|
||||||
setWizardVisible(false)
|
// 不自动关闭,让用户查看创建结果后主动关闭
|
||||||
setEditingSkill(null)
|
// setWizardVisible(false)
|
||||||
|
// setEditingSkill(null)
|
||||||
}}
|
}}
|
||||||
editingSkillId={editingSkill?.id}
|
editingSkillId={editingSkill?.id}
|
||||||
/>
|
/>
|
||||||
|
|||||||
180
frontend/src/services/confirmCardService.ts
Normal file
180
frontend/src/services/confirmCardService.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* Confirm Card API Service
|
||||||
|
*
|
||||||
|
* 确认卡片API服务 - 提供确认卡片的CRUD操作
|
||||||
|
*/
|
||||||
|
import api from './api'
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export type ConfirmCardType =
|
||||||
|
| 'direction_decision' // 方向决策
|
||||||
|
| 'plot_branch' // 剧情分支
|
||||||
|
| 'character_action' // 角色行动
|
||||||
|
| 'content_approval' // 内容批准
|
||||||
|
| 'resource_allocation' // 资源分配
|
||||||
|
|
||||||
|
export type ConfirmCardStatus =
|
||||||
|
| 'pending' // 待确认
|
||||||
|
| 'approved' // 已确认
|
||||||
|
| 'rejected' // 已拒绝
|
||||||
|
| 'expired' // 已过期
|
||||||
|
|
||||||
|
export interface ConfirmCardOption {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
implications?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmCard {
|
||||||
|
id: string
|
||||||
|
project_id: string
|
||||||
|
episode_number?: number
|
||||||
|
card_type: ConfirmCardType
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
context_data?: string
|
||||||
|
options: ConfirmCardOption[]
|
||||||
|
allow_custom_response: boolean
|
||||||
|
status: ConfirmCardStatus
|
||||||
|
selected_option_id?: string
|
||||||
|
custom_response?: string
|
||||||
|
user_notes?: string
|
||||||
|
created_at: string
|
||||||
|
expires_at?: string
|
||||||
|
confirmed_at?: string
|
||||||
|
metadata: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmCardCreate {
|
||||||
|
project_id: string
|
||||||
|
episode_number?: number
|
||||||
|
card_type: ConfirmCardType
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
context_data?: string
|
||||||
|
options: ConfirmCardOption[]
|
||||||
|
allow_custom_response?: boolean
|
||||||
|
expires_at?: string
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmCardResponse {
|
||||||
|
card_id: string
|
||||||
|
status: ConfirmCardStatus
|
||||||
|
selected_option?: string
|
||||||
|
custom_response?: string
|
||||||
|
confirmed_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// API Service
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const confirmCardService = {
|
||||||
|
/**
|
||||||
|
* 创建确认卡片
|
||||||
|
*/
|
||||||
|
create: async (data: ConfirmCardCreate): Promise<{ success: boolean; card: ConfirmCard }> => {
|
||||||
|
return await api.post('/confirm-cards', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简化创建确认卡片 (用于Agent快速推送)
|
||||||
|
*/
|
||||||
|
createSimple: async (
|
||||||
|
projectId: string,
|
||||||
|
cardType: string,
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
options?: Array<{ id?: string; label: string; description?: string; implications?: string } | string>,
|
||||||
|
episodeNumber?: number,
|
||||||
|
allowCustomResponse: boolean = false,
|
||||||
|
expiresInHours?: number
|
||||||
|
): Promise<{ success: boolean; card_id: string; card: ConfirmCard }> => {
|
||||||
|
return await api.post('/confirm-cards/simple', null, {
|
||||||
|
params: {
|
||||||
|
project_id: projectId,
|
||||||
|
card_type: cardType,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
episode_number: episodeNumber,
|
||||||
|
allow_custom_response: allowCustomResponse,
|
||||||
|
expires_in_hours: expiresInHours
|
||||||
|
},
|
||||||
|
data: { options }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取确认卡片列表
|
||||||
|
*/
|
||||||
|
list: async (
|
||||||
|
projectId: string,
|
||||||
|
status?: ConfirmCardStatus,
|
||||||
|
includeExpired: boolean = false
|
||||||
|
): Promise<ConfirmCard[]> => {
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
project_id: projectId,
|
||||||
|
include_expired: includeExpired
|
||||||
|
}
|
||||||
|
if (status) params.status = status
|
||||||
|
return await api.get('/confirm-cards', { params })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取确认卡片详情
|
||||||
|
*/
|
||||||
|
get: async (cardId: string): Promise<ConfirmCard> => {
|
||||||
|
return await api.get(`/confirm-cards/${cardId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除确认卡片
|
||||||
|
*/
|
||||||
|
delete: async (cardId: string): Promise<{ success: boolean }> => {
|
||||||
|
return await api.delete(`/confirm-cards/${cardId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认卡片
|
||||||
|
*/
|
||||||
|
confirm: async (
|
||||||
|
cardId: string,
|
||||||
|
selectedOptionId?: string,
|
||||||
|
customResponse?: string,
|
||||||
|
userNotes?: string
|
||||||
|
): Promise<{ success: boolean; card: ConfirmCard }> => {
|
||||||
|
return await api.post(`/confirm-cards/${cardId}/confirm`, null, {
|
||||||
|
params: {
|
||||||
|
selected_option_id: selectedOptionId,
|
||||||
|
custom_response: customResponse,
|
||||||
|
user_notes: userNotes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝确认卡片
|
||||||
|
*/
|
||||||
|
reject: async (
|
||||||
|
cardId: string,
|
||||||
|
userNotes?: string
|
||||||
|
): Promise<{ success: boolean; card: ConfirmCard }> => {
|
||||||
|
return await api.post(`/confirm-cards/${cardId}/reject`, null, {
|
||||||
|
params: { user_notes: userNotes }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期卡片
|
||||||
|
*/
|
||||||
|
cleanupExpired: async (): Promise<{ success: boolean; cleaned_count: number }> => {
|
||||||
|
return await api.post('/confirm-cards/cleanup-expired')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default confirmCardService
|
||||||
180
frontend/src/services/reviewCardService.ts
Normal file
180
frontend/src/services/reviewCardService.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* Review Card Service
|
||||||
|
*
|
||||||
|
* 审核卡片服务 - 连接后端审核卡片API
|
||||||
|
*/
|
||||||
|
import api from './api'
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 类型定义
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export enum ReviewCardType {
|
||||||
|
PROCESS_AUTO = 'process_auto', // 过程中Agent自主识别
|
||||||
|
POST_CREATION_AUTO = 'post_creation_auto', // 生成后自动审核
|
||||||
|
USER_REQUEST_EPISODE = 'user_episode', // 用户请求单集审核
|
||||||
|
USER_REQUEST_MULTIPLE = 'user_multiple', // 用户请求多集审核
|
||||||
|
USER_REQUEST_ALL = 'user_all' // 用户请求全集审核
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ReviewCardStatus {
|
||||||
|
PENDING = 'pending', // 待处理
|
||||||
|
AWAITING_USER = 'awaiting_user', // 待确认
|
||||||
|
APPROVED = 'approved', // 已通过
|
||||||
|
REJECTED = 'rejected', // 已驳回
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewCard {
|
||||||
|
id: string
|
||||||
|
project_id: string
|
||||||
|
episode_id?: string
|
||||||
|
episode_numbers: number[]
|
||||||
|
|
||||||
|
// 卡片类型和来源
|
||||||
|
card_type: ReviewCardType
|
||||||
|
trigger_source: string
|
||||||
|
|
||||||
|
// 审核内容
|
||||||
|
episode_outline?: string
|
||||||
|
episode_content?: string
|
||||||
|
review_reason: string
|
||||||
|
|
||||||
|
// 审核结果
|
||||||
|
issues: Array<{
|
||||||
|
id?: string
|
||||||
|
type: string
|
||||||
|
severity: 'low' | 'medium' | 'high'
|
||||||
|
description: string
|
||||||
|
location?: string
|
||||||
|
suggestion?: string
|
||||||
|
ignored?: boolean
|
||||||
|
}>
|
||||||
|
overall_score?: number
|
||||||
|
severity: 'low' | 'medium' | 'high'
|
||||||
|
|
||||||
|
// 审核配置
|
||||||
|
review_config_id?: string
|
||||||
|
applied_skills: string[]
|
||||||
|
applied_rules: string[]
|
||||||
|
|
||||||
|
// 用户操作
|
||||||
|
status: ReviewCardStatus
|
||||||
|
user_comment?: string
|
||||||
|
modified_content?: string
|
||||||
|
|
||||||
|
// 时间戳
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
completed_at?: string
|
||||||
|
|
||||||
|
// 附加信息
|
||||||
|
metadata: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewCardStats {
|
||||||
|
total: number
|
||||||
|
pending: number
|
||||||
|
awaiting_user: number
|
||||||
|
approved: number
|
||||||
|
rejected: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewCardCreateRequest {
|
||||||
|
project_id: string
|
||||||
|
episode_id?: string
|
||||||
|
episode_numbers?: number[]
|
||||||
|
card_type: ReviewCardType
|
||||||
|
trigger_source: string
|
||||||
|
review_reason: string
|
||||||
|
episode_outline?: string
|
||||||
|
episode_content?: string
|
||||||
|
review_config_id?: string
|
||||||
|
severity?: 'low' | 'medium' | 'high'
|
||||||
|
issues?: Array<any>
|
||||||
|
overall_score?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewCardUpdateRequest {
|
||||||
|
status?: ReviewCardStatus
|
||||||
|
user_comment?: string
|
||||||
|
modified_content?: string
|
||||||
|
issues?: Array<any>
|
||||||
|
overall_score?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// API 方法
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
export const reviewCardService = {
|
||||||
|
/**
|
||||||
|
* 获取项目的审核卡片列表
|
||||||
|
*/
|
||||||
|
listCards: async (projectId: string, status?: ReviewCardStatus): Promise<ReviewCard[]> => {
|
||||||
|
const params: any = { project_id: projectId, limit: 100 }
|
||||||
|
if (status) params.status = status
|
||||||
|
return await api.get<ReviewCard[]>('/review-cards', { params })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审核卡片统计信息
|
||||||
|
*/
|
||||||
|
getStats: async (projectId: string): Promise<ReviewCardStats> => {
|
||||||
|
return await api.get<ReviewCardStats>(`/review-cards/stats/${projectId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审核卡片详情
|
||||||
|
*/
|
||||||
|
getCard: async (cardId: string): Promise<ReviewCard> => {
|
||||||
|
return await api.get<ReviewCard>(`/review-cards/${cardId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建审核卡片
|
||||||
|
*/
|
||||||
|
createCard: async (data: ReviewCardCreateRequest): Promise<{ success: boolean; card: ReviewCard }> => {
|
||||||
|
return await api.post('/review-cards', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新审核卡片
|
||||||
|
*/
|
||||||
|
updateCard: async (cardId: string, data: ReviewCardUpdateRequest): Promise<{ success: boolean; card: ReviewCard }> => {
|
||||||
|
return await api.put(`/review-cards/${cardId}`, data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过审核卡片
|
||||||
|
*/
|
||||||
|
approveCard: async (cardId: string, userComment?: string): Promise<{ success: boolean; card: ReviewCard }> => {
|
||||||
|
return await api.post(`/review-cards/${cardId}/approve`, null, {
|
||||||
|
params: { user_comment: userComment }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 驳回审核卡片
|
||||||
|
*/
|
||||||
|
rejectCard: async (cardId: string, userComment?: string, modifiedContent?: string): Promise<{ success: boolean; card: ReviewCard }> => {
|
||||||
|
return await api.post(`/review-cards/${cardId}/reject`, null, {
|
||||||
|
params: { user_comment: userComment, modified_content: modifiedContent }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改审核卡片内容
|
||||||
|
*/
|
||||||
|
modifyCard: async (cardId: string, userComment?: string, modifiedContent?: string): Promise<{ success: boolean; card: ReviewCard }> => {
|
||||||
|
return await api.post(`/review-cards/${cardId}/modify`, null, {
|
||||||
|
params: { user_comment: userComment, modified_content: modifiedContent }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除审核卡片
|
||||||
|
*/
|
||||||
|
deleteCard: async (cardId: string): Promise<{ success: boolean }> => {
|
||||||
|
return await api.delete(`/review-cards/${cardId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,10 +18,13 @@ export interface ReviewRule {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
triggerCondition: string
|
triggerCondition: string // Frontend format
|
||||||
|
trigger_condition?: string // Backend format
|
||||||
severity: SeverityLevel
|
severity: SeverityLevel
|
||||||
category: string
|
category: string // Frontend format
|
||||||
|
dimension?: string // Backend format
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
enabled?: boolean // Backend format
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReviewConfiguration {
|
export interface ReviewConfiguration {
|
||||||
@ -95,18 +98,18 @@ export interface CreateConfigurationRequest {
|
|||||||
export interface CreateRuleRequest {
|
export interface CreateRuleRequest {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
triggerCondition: string
|
trigger_condition: string
|
||||||
severity: SeverityLevel
|
severity: SeverityLevel
|
||||||
category: string
|
dimension: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateRuleRequest {
|
export interface UpdateRuleRequest {
|
||||||
name?: string
|
name?: string
|
||||||
description?: string
|
description?: string
|
||||||
triggerCondition?: string
|
trigger_condition?: string
|
||||||
severity?: SeverityLevel
|
severity?: SeverityLevel
|
||||||
category?: string
|
dimension?: string
|
||||||
isActive?: boolean
|
enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteReviewRequest {
|
export interface ExecuteReviewRequest {
|
||||||
@ -122,83 +125,90 @@ export interface ExportReportRequest {
|
|||||||
export const reviewService = {
|
export const reviewService = {
|
||||||
// Configuration
|
// Configuration
|
||||||
getConfiguration: async (projectId: string): Promise<ReviewConfiguration> => {
|
getConfiguration: async (projectId: string): Promise<ReviewConfiguration> => {
|
||||||
return await api.get(`/projects/${projectId}/review/configuration`)
|
return await api.get(`/projects/${projectId}/review-config`)
|
||||||
},
|
},
|
||||||
|
|
||||||
createConfiguration: async (projectId: string, data: CreateConfigurationRequest): Promise<ReviewConfiguration> => {
|
createConfiguration: async (projectId: string, data: CreateConfigurationRequest): Promise<ReviewConfiguration> => {
|
||||||
return await api.post(`/projects/${projectId}/review/configuration`, data)
|
return await api.post(`/projects/${projectId}/review-config`, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
updateConfiguration: async (projectId: string, data: Partial<ReviewConfiguration>): Promise<ReviewConfiguration> => {
|
updateConfiguration: async (projectId: string, data: Partial<ReviewConfiguration>): Promise<ReviewConfiguration> => {
|
||||||
return await api.put(`/projects/${projectId}/review/configuration`, data)
|
return await api.put(`/projects/${projectId}/review-config`, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
applyPreset: async (projectId: string, preset: ReviewPreset): Promise<ReviewConfiguration> => {
|
applyPreset: async (projectId: string, preset: ReviewPreset): Promise<ReviewConfiguration> => {
|
||||||
return await api.post(`/projects/${projectId}/review/configuration/preset`, { preset })
|
// Apply preset by updating the configuration with the preset
|
||||||
|
return await api.put(`/projects/${projectId}/review-config`, { preset })
|
||||||
},
|
},
|
||||||
|
|
||||||
// Custom Rules
|
// Custom Rules
|
||||||
getRules: async (projectId: string): Promise<ReviewRule[]> => {
|
getRules: async (projectId: string): Promise<ReviewRule[]> => {
|
||||||
return await api.get(`/projects/${projectId}/review/rules`)
|
return await api.get('/custom-rules', { params: { project_id: projectId } })
|
||||||
},
|
},
|
||||||
|
|
||||||
createRule: async (projectId: string, data: CreateRuleRequest): Promise<ReviewRule> => {
|
createRule: async (projectId: string, data: CreateRuleRequest): Promise<ReviewRule> => {
|
||||||
return await api.post(`/projects/${projectId}/review/rules`, data)
|
return await api.post('/custom-rules', data, { params: { project_id: projectId } })
|
||||||
},
|
},
|
||||||
|
|
||||||
updateRule: async (projectId: string, ruleId: string, data: UpdateRuleRequest): Promise<ReviewRule> => {
|
updateRule: async (projectId: string, ruleId: string, data: UpdateRuleRequest): Promise<ReviewRule> => {
|
||||||
return await api.put(`/projects/${projectId}/review/rules/${ruleId}`, data)
|
return await api.put(`/custom-rules/${ruleId}`, data, { params: { project_id: projectId } })
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteRule: async (projectId: string, ruleId: string): Promise<void> => {
|
deleteRule: async (projectId: string, ruleId: string): Promise<void> => {
|
||||||
return await api.delete(`/projects/${projectId}/review/rules/${ruleId}`)
|
return await api.delete(`/custom-rules/${ruleId}`, { params: { project_id: projectId } })
|
||||||
},
|
},
|
||||||
|
|
||||||
testRule: async (projectId: string, data: TestRuleRequest): Promise<TestRuleResponse> => {
|
testRule: async (projectId: string, data: TestRuleRequest & { episodeId: string }): Promise<TestRuleResponse> => {
|
||||||
return await api.post(`/projects/${projectId}/review/rules/test`, data)
|
return await api.post(`/custom-rules/${data.rule?.id || 'test'}/test`, {
|
||||||
|
episode_id: data.episodeId,
|
||||||
|
project_id: projectId
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// Rule Templates
|
// Rule Templates
|
||||||
getRuleTemplates: async (): Promise<ReviewRule[]> => {
|
getRuleTemplates: async (): Promise<ReviewRule[]> => {
|
||||||
return await api.get('/review/rule-templates')
|
return await api.get('/custom-rules')
|
||||||
},
|
},
|
||||||
|
|
||||||
// Review Execution
|
// Review Execution
|
||||||
executeReview: async (projectId: string, data: ExecuteReviewRequest): Promise<ReviewResult> => {
|
executeReview: async (projectId: string, data: ExecuteReviewRequest): Promise<ReviewResult> => {
|
||||||
return await api.post(`/projects/${projectId}/review/execute`, data)
|
return await api.post(`/projects/${projectId}/episodes/${data.episodeNumber || 1}/review`, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
getReviewResult: async (projectId: string, reviewId: string): Promise<ReviewResult> => {
|
getReviewResult: async (projectId: string, episodeNumber: number): Promise<ReviewResult> => {
|
||||||
return await api.get(`/projects/${projectId}/review/results/${reviewId}`)
|
return await api.get(`/projects/${projectId}/episodes/${episodeNumber}/review-result`)
|
||||||
},
|
},
|
||||||
|
|
||||||
getLatestReviewResult: async (projectId: string, episodeNumber?: number): Promise<ReviewResult> => {
|
getLatestReviewResult: async (projectId: string, episodeNumber?: number): Promise<ReviewResult> => {
|
||||||
return await api.get(`/projects/${projectId}/review/latest`, { params: { episode_number: episodeNumber } })
|
if (!episodeNumber) {
|
||||||
|
throw new Error('episodeNumber is required')
|
||||||
|
}
|
||||||
|
return await api.get(`/projects/${projectId}/episodes/${episodeNumber}/review-result`)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Issue Management
|
// Issue Management
|
||||||
ignoreIssue: async (projectId: string, issueId: string): Promise<void> => {
|
ignoreIssue: async (projectId: string, issueId: string): Promise<void> => {
|
||||||
return await api.post(`/projects/${projectId}/review/issues/${issueId}/ignore`)
|
return await api.post(`/custom-rules/${issueId}/ignore`, { project_id: projectId })
|
||||||
},
|
},
|
||||||
|
|
||||||
unignoreIssue: async (projectId: string, issueId: string): Promise<void> => {
|
unignoreIssue: async (projectId: string, issueId: string): Promise<void> => {
|
||||||
return await api.post(`/projects/${projectId}/review/issues/${issueId}/unignore`)
|
return await api.post(`/custom-rules/${issueId}/unignore`, { project_id: projectId })
|
||||||
},
|
},
|
||||||
|
|
||||||
autoFixIssue: async (projectId: string, issueId: string): Promise<{ fixed: boolean; fixedText?: string }> => {
|
autoFixIssue: async (projectId: string, issueId: string): Promise<{ fixed: boolean; fixedText?: string }> => {
|
||||||
return await api.post(`/projects/${projectId}/review/issues/${issueId}/autofix`)
|
return await api.post(`/custom-rules/${issueId}/test`, { project_id: projectId })
|
||||||
},
|
},
|
||||||
|
|
||||||
applyBatchDecisions: async (
|
applyBatchDecisions: async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
decisions: { issueId: string; action: 'accept' | 'reject' | 'ignore' }[]
|
decisions: { issueId: string; action: 'accept' | 'reject' | 'ignore' }[]
|
||||||
): Promise<{ applied: number }> => {
|
): Promise<{ applied: number }> => {
|
||||||
return await api.post(`/projects/${projectId}/review/batch`, { decisions })
|
return await api.post(`/projects/${projectId}/review-config`, { decisions })
|
||||||
},
|
},
|
||||||
|
|
||||||
// Export
|
// Export
|
||||||
exportReport: async (projectId: string, reviewId: string, data: ExportReportRequest): Promise<Blob> => {
|
exportReport: async (projectId: string, reviewId: string, data: ExportReportRequest): Promise<Blob> => {
|
||||||
const response = await api.get(`/projects/${projectId}/review/results/${reviewId}/export`, {
|
const response = await api.get(`/projects/${projectId}/episodes/${reviewId}/review-result`, {
|
||||||
params: data,
|
params: data,
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
})
|
})
|
||||||
|
|||||||
@ -350,5 +350,59 @@ export const skillService = {
|
|||||||
params: { tool_name: toolName },
|
params: { tool_name: toolName },
|
||||||
data: parameters
|
data: parameters
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// LLM 自动选择 Skills(智能路由)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM 根据描述自动选择最合适的 Skills
|
||||||
|
* @param userIntent 用户意图描述
|
||||||
|
* @param context 可选的上下文信息
|
||||||
|
* @param excludeSkills 要排除的 Skills
|
||||||
|
* @param requiredCategory 必需的分类
|
||||||
|
* @param requiredTags 必需的标签
|
||||||
|
* @returns 选择结果
|
||||||
|
*/
|
||||||
|
selectSkills: async (request: {
|
||||||
|
user_intent: string
|
||||||
|
context?: Record<string, any>
|
||||||
|
exclude_skills?: string[]
|
||||||
|
required_category?: string
|
||||||
|
required_tags?: string[]
|
||||||
|
}): Promise<{
|
||||||
|
selected_skills: Array<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
category: string
|
||||||
|
tags: string[]
|
||||||
|
}>
|
||||||
|
selection_reason: string
|
||||||
|
confidence_scores: Record<string, number>
|
||||||
|
}> => {
|
||||||
|
return await api.post('/skills/select-skills', request)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置 Agent 工作流的 Skill 使用方式
|
||||||
|
* @param config 工作流配置
|
||||||
|
* @returns 配置结果
|
||||||
|
*/
|
||||||
|
configureAgentWorkflow: async (config: {
|
||||||
|
workflow_type: 'sequential' | 'parallel' | 'adaptive'
|
||||||
|
default_skills?: string[]
|
||||||
|
steps?: Array<{
|
||||||
|
step_name: string
|
||||||
|
skills: string[]
|
||||||
|
enabled: boolean
|
||||||
|
}>
|
||||||
|
}): Promise<{
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
config: Record<string, any>
|
||||||
|
}> => {
|
||||||
|
return await api.post('/skills/agent-workflow/configure', config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export const taskService = {
|
|||||||
generateCharacters: async (params: {
|
generateCharacters: async (params: {
|
||||||
idea: string
|
idea: string
|
||||||
projectName?: string
|
projectName?: string
|
||||||
|
genre?: string
|
||||||
totalEpisodes?: number
|
totalEpisodes?: number
|
||||||
skills?: Array<{ id: string; name: string; behavior: string }>
|
skills?: Array<{ id: string; name: string; behavior: string }>
|
||||||
customPrompt?: string
|
customPrompt?: string
|
||||||
@ -137,7 +138,7 @@ export const taskService = {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
const progress = await api.get<TaskProgress>(`/tasks/${taskId}`)
|
const progress = await api.get<TaskProgress>(`/tasks/${taskId}`) as unknown as TaskProgress
|
||||||
onUpdate?.(progress)
|
onUpdate?.(progress)
|
||||||
|
|
||||||
if (progress.status === 'completed') {
|
if (progress.status === 'completed') {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Memory System State Management (Zustand)
|
* Memory System State Management (Zustand)
|
||||||
*/
|
*/
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { memoryService, MemorySystem, TimelineEvent, PendingThread, CharacterState, Foreshadowing, Relationship, CreateThreadRequest, UpdateThreadRequest, CreateForeshadowingRequest, ImportanceLevel } from '@/services/memoryService'
|
import { memoryService, MemorySystem, TimelineEvent, PendingThread, CharacterState, Foreshadowing, Relationship, CreateThreadRequest, UpdateThreadRequest, CreateForeshadowingRequest, ImportanceLevel, MemoryStatistics } from '@/services/memoryService'
|
||||||
|
|
||||||
interface MemoryStore {
|
interface MemoryStore {
|
||||||
// State
|
// State
|
||||||
@ -13,7 +13,7 @@ interface MemoryStore {
|
|||||||
characterStates: CharacterState[]
|
characterStates: CharacterState[]
|
||||||
foreshadowing: Foreshadowing[]
|
foreshadowing: Foreshadowing[]
|
||||||
relationships: Relationship[]
|
relationships: Relationship[]
|
||||||
statistics: MemorySystem | null
|
statistics: MemoryStatistics | null
|
||||||
|
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: string | null
|
error: string | null
|
||||||
@ -60,16 +60,40 @@ interface MemoryStore {
|
|||||||
clearMemory: () => void
|
clearMemory: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
export const useMemoryStore = create<MemoryStore>((set) => ({
|
||||||
// Initial State
|
// Initial State with empty but structured data
|
||||||
projectId: null,
|
projectId: null,
|
||||||
memorySystem: null,
|
memorySystem: {
|
||||||
|
projectId: '',
|
||||||
|
timeline: [],
|
||||||
|
pendingThreads: [],
|
||||||
|
characterStates: [],
|
||||||
|
foreshadowing: [],
|
||||||
|
relationships: [],
|
||||||
|
statistics: {
|
||||||
|
totalEvents: 0,
|
||||||
|
totalThreads: 0,
|
||||||
|
resolvedThreads: 0,
|
||||||
|
totalCharacters: 0,
|
||||||
|
totalForeshadowing: 0,
|
||||||
|
paidOffForeshadowing: 0,
|
||||||
|
totalRelationships: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
timeline: [],
|
timeline: [],
|
||||||
threads: [],
|
threads: [],
|
||||||
characterStates: [],
|
characterStates: [],
|
||||||
foreshadowing: [],
|
foreshadowing: [],
|
||||||
relationships: [],
|
relationships: [],
|
||||||
statistics: null,
|
statistics: {
|
||||||
|
totalEvents: 0,
|
||||||
|
totalThreads: 0,
|
||||||
|
resolvedThreads: 0,
|
||||||
|
totalCharacters: 0,
|
||||||
|
totalForeshadowing: 0,
|
||||||
|
paidOffForeshadowing: 0,
|
||||||
|
totalRelationships: 0
|
||||||
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
timelineFilters: {},
|
timelineFilters: {},
|
||||||
@ -81,14 +105,85 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
|||||||
set({ loading: true, error: null, projectId })
|
set({ loading: true, error: null, projectId })
|
||||||
try {
|
try {
|
||||||
const memorySystem = await memoryService.getMemory(projectId)
|
const memorySystem = await memoryService.getMemory(projectId)
|
||||||
|
|
||||||
|
// Transform characterStates from dictionary to array format
|
||||||
|
const characterStatesArray: CharacterState[] = memorySystem.characterStates
|
||||||
|
? Object.entries(memorySystem.characterStates).map(([characterName, stateChanges]: [string, any]) => ({
|
||||||
|
characterName,
|
||||||
|
currentState: stateChanges[stateChanges.length - 1]?.state || '',
|
||||||
|
history: stateChanges
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
|
||||||
|
// Transform relationships from nested dict to array format
|
||||||
|
const relationshipsArray: Relationship[] = memorySystem.relationships
|
||||||
|
? Object.entries(memorySystem.relationships).flatMap(([sourceChar, targets]: [string, any]) =>
|
||||||
|
Object.entries(targets).map(([targetChar, relationData]: [string, any]) => ({
|
||||||
|
sourceCharacter: sourceChar,
|
||||||
|
targetCharacter: targetChar,
|
||||||
|
relationType: relationData.type || 'neutral',
|
||||||
|
strength: relationData.strength || 50,
|
||||||
|
description: relationData.description,
|
||||||
|
evolutionHistory: relationData.history || []
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
|
// Transform timeline events to match frontend format
|
||||||
|
const timelineArray: TimelineEvent[] = (memorySystem.timeline || []).map((event: any) => ({
|
||||||
|
id: event.id || `${event.episode}_${event.timestamp}`,
|
||||||
|
episodeNumber: event.episode || event.episodeNumber,
|
||||||
|
timestamp: event.timestamp || event.createdAt,
|
||||||
|
characters: event.characters_involved || event.characters || [],
|
||||||
|
description: event.event || event.description,
|
||||||
|
importance: event.importance || 'medium',
|
||||||
|
category: event.tags?.[0] || event.category || 'general',
|
||||||
|
createdAt: event.createdAt || event.timestamp
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Transform threads to match frontend format
|
||||||
|
const threadsArray: PendingThread[] = (memorySystem.pendingThreads || []).map((thread: any) => ({
|
||||||
|
id: thread.id,
|
||||||
|
description: thread.description,
|
||||||
|
importance: thread.importance,
|
||||||
|
status: thread.status || 'pending',
|
||||||
|
reminderEpisode: thread.reminder_episode,
|
||||||
|
relatedCharacters: thread.characters_involved || thread.relatedCharacters || [],
|
||||||
|
createdAt: thread.created_at || thread.createdAt,
|
||||||
|
resolvedAt: thread.resolved_at || thread.resolvedAt
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Transform foreshadowing to match frontend format
|
||||||
|
const foreshadowingArray: Foreshadowing[] = (memorySystem.foreshadowing || []).map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
description: item.description,
|
||||||
|
importance: item.importance,
|
||||||
|
payoffEventId: item.related_events?.[0],
|
||||||
|
payoffEpisode: item.payoff_at,
|
||||||
|
status: (item.is_payed_off ? 'paid_off' : 'planted') as 'paid_off' | 'planted',
|
||||||
|
plantedEpisode: item.introduced_at,
|
||||||
|
createdAt: item.createdAt || new Date().toISOString()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Transform statistics
|
||||||
|
const statistics: MemoryStatistics = {
|
||||||
|
totalEvents: timelineArray.length,
|
||||||
|
totalThreads: threadsArray.length,
|
||||||
|
resolvedThreads: threadsArray.filter((t: any) => t.status === 'resolved').length,
|
||||||
|
totalCharacters: characterStatesArray.length,
|
||||||
|
totalForeshadowing: foreshadowingArray.length,
|
||||||
|
paidOffForeshadowing: foreshadowingArray.filter((f: any) => f.status === 'paid_off').length,
|
||||||
|
totalRelationships: relationshipsArray.length
|
||||||
|
}
|
||||||
|
|
||||||
set({
|
set({
|
||||||
memorySystem,
|
memorySystem,
|
||||||
timeline: memorySystem.timeline,
|
timeline: timelineArray,
|
||||||
threads: memorySystem.pendingThreads,
|
threads: threadsArray,
|
||||||
characterStates: memorySystem.characterStates,
|
characterStates: characterStatesArray,
|
||||||
foreshadowing: memorySystem.foreshadowing,
|
foreshadowing: foreshadowingArray,
|
||||||
relationships: memorySystem.relationships,
|
relationships: relationshipsArray,
|
||||||
statistics: memorySystem.statistics,
|
statistics,
|
||||||
loading: false
|
loading: false
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -99,8 +194,19 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
|||||||
fetchTimeline: async (projectId, filters) => {
|
fetchTimeline: async (projectId, filters) => {
|
||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const timeline = await memoryService.getTimeline(projectId, filters)
|
const response = await memoryService.getTimeline(projectId, filters)
|
||||||
set({ timeline, loading: false })
|
// Transform to match frontend format
|
||||||
|
const timelineArray = (response || []).map((event: any) => ({
|
||||||
|
id: event.id || `${event.episode}_${event.timestamp}`,
|
||||||
|
episodeNumber: event.episode || event.episodeNumber,
|
||||||
|
timestamp: event.timestamp || event.createdAt,
|
||||||
|
characters: event.characters_involved || event.characters || [],
|
||||||
|
description: event.event || event.description,
|
||||||
|
importance: event.importance || 'medium',
|
||||||
|
category: event.tags?.[0] || event.category || 'general',
|
||||||
|
createdAt: event.createdAt || event.timestamp
|
||||||
|
}))
|
||||||
|
set({ timeline: timelineArray, loading: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: (error as Error).message, loading: false })
|
set({ error: (error as Error).message, loading: false })
|
||||||
}
|
}
|
||||||
@ -109,8 +215,19 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
|||||||
fetchThreads: async (projectId) => {
|
fetchThreads: async (projectId) => {
|
||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const threads = await memoryService.getThreads(projectId)
|
const response = await memoryService.getThreads(projectId)
|
||||||
set({ threads, loading: false })
|
// Transform to match frontend format
|
||||||
|
const threadsArray = (response || []).map((thread: any) => ({
|
||||||
|
id: thread.id,
|
||||||
|
description: thread.description,
|
||||||
|
importance: thread.importance,
|
||||||
|
status: thread.status || 'pending',
|
||||||
|
reminderEpisode: thread.reminder_episode,
|
||||||
|
relatedCharacters: thread.characters_involved || thread.relatedCharacters || [],
|
||||||
|
createdAt: thread.created_at || thread.createdAt,
|
||||||
|
resolvedAt: thread.resolved_at || thread.resolvedAt
|
||||||
|
}))
|
||||||
|
set({ threads: threadsArray, loading: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: (error as Error).message, loading: false })
|
set({ error: (error as Error).message, loading: false })
|
||||||
}
|
}
|
||||||
@ -119,8 +236,16 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
|||||||
fetchCharacterStates: async (projectId) => {
|
fetchCharacterStates: async (projectId) => {
|
||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const characterStates = await memoryService.getCharacterStates(projectId)
|
const response = await memoryService.getCharacterStates(projectId)
|
||||||
set({ characterStates, loading: false })
|
// Transform from dict to array format
|
||||||
|
const characterStatesArray = response
|
||||||
|
? Object.entries(response).map(([characterName, stateChanges]: [string, any]) => ({
|
||||||
|
characterName,
|
||||||
|
currentState: stateChanges[stateChanges.length - 1]?.state || '',
|
||||||
|
history: stateChanges
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
set({ characterStates: characterStatesArray, loading: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: (error as Error).message, loading: false })
|
set({ error: (error as Error).message, loading: false })
|
||||||
}
|
}
|
||||||
@ -129,8 +254,19 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
|||||||
fetchForeshadowing: async (projectId) => {
|
fetchForeshadowing: async (projectId) => {
|
||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const foreshadowing = await memoryService.getForeshadowing(projectId)
|
const response = await memoryService.getForeshadowing(projectId)
|
||||||
set({ foreshadowing, loading: false })
|
// Transform to match frontend format
|
||||||
|
const foreshadowingArray: Foreshadowing[] = (response || []).map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
description: item.description,
|
||||||
|
importance: item.importance,
|
||||||
|
payoffEventId: item.related_events?.[0],
|
||||||
|
payoffEpisode: item.payoff_at,
|
||||||
|
status: (item.is_payed_off ? 'paid_off' : 'planted') as 'paid_off' | 'planted',
|
||||||
|
plantedEpisode: item.introduced_at,
|
||||||
|
createdAt: item.createdAt || new Date().toISOString()
|
||||||
|
}))
|
||||||
|
set({ foreshadowing: foreshadowingArray, loading: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: (error as Error).message, loading: false })
|
set({ error: (error as Error).message, loading: false })
|
||||||
}
|
}
|
||||||
@ -139,8 +275,21 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
|||||||
fetchRelationships: async (projectId) => {
|
fetchRelationships: async (projectId) => {
|
||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const relationships = await memoryService.getRelationships(projectId)
|
const response = await memoryService.getRelationships(projectId)
|
||||||
set({ relationships, loading: false })
|
// Transform from nested dict to array format
|
||||||
|
const relationshipsArray: Relationship[] = response
|
||||||
|
? Object.entries(response).flatMap(([sourceChar, targets]: [string, any]) =>
|
||||||
|
Object.entries(targets).map(([targetChar, relationData]: [string, any]) => ({
|
||||||
|
sourceCharacter: sourceChar,
|
||||||
|
targetCharacter: targetChar,
|
||||||
|
relationType: relationData.type || 'neutral',
|
||||||
|
strength: relationData.strength || 50,
|
||||||
|
description: relationData.description,
|
||||||
|
evolutionHistory: relationData.history || []
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
set({ relationships: relationshipsArray, loading: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: (error as Error).message, loading: false })
|
set({ error: (error as Error).message, loading: false })
|
||||||
}
|
}
|
||||||
@ -150,7 +299,7 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
|||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const statistics = await memoryService.getStatistics(projectId)
|
const statistics = await memoryService.getStatistics(projectId)
|
||||||
set({ statistics, loading: false })
|
set({ statistics: statistics as MemoryStatistics, loading: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: (error as Error).message, loading: false })
|
set({ error: (error as Error).message, loading: false })
|
||||||
}
|
}
|
||||||
|
|||||||
184
frontend/src/stores/reviewCardStore.ts
Normal file
184
frontend/src/stores/reviewCardStore.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Review Card Store
|
||||||
|
*
|
||||||
|
* 审核卡片状态管理
|
||||||
|
*/
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { reviewCardService, ReviewCard, ReviewCardStats, ReviewCardStatus, ReviewCardType } from '@/services/reviewCardService'
|
||||||
|
|
||||||
|
interface ReviewCardState {
|
||||||
|
// 数据
|
||||||
|
cards: ReviewCard[]
|
||||||
|
currentCard: ReviewCard | null
|
||||||
|
stats: ReviewCardStats | null
|
||||||
|
|
||||||
|
// 筛选状态
|
||||||
|
statusFilter: ReviewCardStatus | 'all'
|
||||||
|
typeFilter: ReviewCardType | 'all'
|
||||||
|
searchQuery: string
|
||||||
|
|
||||||
|
// UI 状态
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
|
||||||
|
// 操作
|
||||||
|
setProjectId: (projectId: string) => void
|
||||||
|
setStatusFilter: (status: ReviewCardStatus | 'all') => void
|
||||||
|
setTypeFilter: (type: ReviewCardType | 'all') => void
|
||||||
|
setSearchQuery: (query: string) => void
|
||||||
|
|
||||||
|
// 数据获取
|
||||||
|
fetchCards: (projectId: string, status?: ReviewCardStatus) => Promise<void>
|
||||||
|
fetchStats: (projectId: string) => Promise<void>
|
||||||
|
fetchCard: (cardId: string) => Promise<void>
|
||||||
|
|
||||||
|
// 操作
|
||||||
|
approveCard: (cardId: string, userComment?: string) => Promise<void>
|
||||||
|
rejectCard: (cardId: string, userComment?: string, modifiedContent?: string) => Promise<void>
|
||||||
|
modifyCard: (cardId: string, userComment?: string, modifiedContent?: string) => Promise<void>
|
||||||
|
deleteCard: (cardId: string) => Promise<void>
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
clearCards: () => void
|
||||||
|
clearCurrentCard: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReviewCardStore = create<ReviewCardState>((set, get) => ({
|
||||||
|
// 初始状态
|
||||||
|
cards: [],
|
||||||
|
currentCard: null,
|
||||||
|
stats: null,
|
||||||
|
statusFilter: 'all',
|
||||||
|
typeFilter: 'all',
|
||||||
|
searchQuery: '',
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
// 设置项目ID(预留)
|
||||||
|
setProjectId: (projectId: string) => {
|
||||||
|
// 可以在这里设置当前项目ID
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置筛选状态
|
||||||
|
setStatusFilter: (status) => set({ statusFilter: status }),
|
||||||
|
setTypeFilter: (type) => set({ typeFilter: type }),
|
||||||
|
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||||
|
|
||||||
|
// 获取卡片列表
|
||||||
|
fetchCards: async (projectId, status) => {
|
||||||
|
set({ loading: true, error: null })
|
||||||
|
try {
|
||||||
|
const cards = await reviewCardService.listCards(projectId, status)
|
||||||
|
set({ cards, loading: false })
|
||||||
|
} catch (error: any) {
|
||||||
|
set({ error: error?.message || '获取卡片列表失败', loading: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取统计信息
|
||||||
|
fetchStats: async (projectId) => {
|
||||||
|
try {
|
||||||
|
const stats = await reviewCardService.getStats(projectId)
|
||||||
|
set({ stats })
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取统计信息失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取卡片详情
|
||||||
|
fetchCard: async (cardId) => {
|
||||||
|
set({ loading: true, error: null })
|
||||||
|
try {
|
||||||
|
const card = await reviewCardService.getCard(cardId)
|
||||||
|
set({ currentCard: card, loading: false })
|
||||||
|
} catch (error: any) {
|
||||||
|
set({ error: error?.message || '获取卡片详情失败', loading: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 通过审核
|
||||||
|
approveCard: async (cardId, userComment) => {
|
||||||
|
set({ loading: true, error: null })
|
||||||
|
try {
|
||||||
|
const result = await reviewCardService.approveCard(cardId, userComment)
|
||||||
|
// 更新列表中的卡片
|
||||||
|
set((state) => ({
|
||||||
|
cards: state.cards.map(c => c.id === cardId ? result.card : c),
|
||||||
|
currentCard: state.currentCard?.id === cardId ? result.card : state.currentCard,
|
||||||
|
loading: false
|
||||||
|
}))
|
||||||
|
// 重新获取统计信息
|
||||||
|
const projectId = get().currentCard?.project_id
|
||||||
|
if (projectId) {
|
||||||
|
get().fetchStats(projectId)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
set({ error: error?.message || '操作失败', loading: false })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 驳回审核
|
||||||
|
rejectCard: async (cardId, userComment, modifiedContent) => {
|
||||||
|
set({ loading: true, error: null })
|
||||||
|
try {
|
||||||
|
const result = await reviewCardService.rejectCard(cardId, userComment, modifiedContent)
|
||||||
|
set((state) => ({
|
||||||
|
cards: state.cards.map(c => c.id === cardId ? result.card : c),
|
||||||
|
currentCard: state.currentCard?.id === cardId ? result.card : state.currentCard,
|
||||||
|
loading: false
|
||||||
|
}))
|
||||||
|
const projectId = get().currentCard?.project_id
|
||||||
|
if (projectId) {
|
||||||
|
get().fetchStats(projectId)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
set({ error: error?.message || '操作失败', loading: false })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改审核
|
||||||
|
modifyCard: async (cardId, userComment, modifiedContent) => {
|
||||||
|
set({ loading: true, error: null })
|
||||||
|
try {
|
||||||
|
const result = await reviewCardService.modifyCard(cardId, userComment, modifiedContent)
|
||||||
|
set((state) => ({
|
||||||
|
cards: state.cards.map(c => c.id === cardId ? result.card : c),
|
||||||
|
currentCard: state.currentCard?.id === cardId ? result.card : state.currentCard,
|
||||||
|
loading: false
|
||||||
|
}))
|
||||||
|
const projectId = get().currentCard?.project_id
|
||||||
|
if (projectId) {
|
||||||
|
get().fetchStats(projectId)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
set({ error: error?.message || '操作失败', loading: false })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除卡片
|
||||||
|
deleteCard: async (cardId) => {
|
||||||
|
set({ loading: true, error: null })
|
||||||
|
try {
|
||||||
|
await reviewCardService.deleteCard(cardId)
|
||||||
|
set((state) => ({
|
||||||
|
cards: state.cards.filter(c => c.id !== cardId),
|
||||||
|
currentCard: state.currentCard?.id === cardId ? null : state.currentCard,
|
||||||
|
loading: false
|
||||||
|
}))
|
||||||
|
const projectId = get().cards.find(c => c.id !== cardId)?.project_id
|
||||||
|
if (projectId) {
|
||||||
|
get().fetchStats(projectId)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
set({ error: error?.message || '删除失败', loading: false })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清理数据
|
||||||
|
clearCards: () => set({ cards: [], stats: null, error: null }),
|
||||||
|
clearCurrentCard: () => set({ currentCard: null, error: null })
|
||||||
|
}))
|
||||||
@ -2,7 +2,7 @@
|
|||||||
* Review System State Management (Zustand)
|
* Review System State Management (Zustand)
|
||||||
*/
|
*/
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { reviewService, ReviewConfiguration, ReviewResult, ReviewRule, CreateRuleRequest, UpdateRuleRequest, SeverityLevel, ReviewPreset, Issue } from '@/services/reviewService'
|
import { reviewService, ReviewConfiguration, ReviewResult, ReviewRule, CreateRuleRequest, UpdateRuleRequest, ReviewPreset } from '@/services/reviewService'
|
||||||
|
|
||||||
interface ReviewStore {
|
interface ReviewStore {
|
||||||
// State
|
// State
|
||||||
@ -49,9 +49,22 @@ interface ReviewStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useReviewStore = create<ReviewStore>((set, get) => ({
|
export const useReviewStore = create<ReviewStore>((set, get) => ({
|
||||||
// Initial State
|
// Initial State with default configuration
|
||||||
projectId: null,
|
projectId: null,
|
||||||
configuration: null,
|
configuration: {
|
||||||
|
preset: 'standard',
|
||||||
|
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 }
|
||||||
|
},
|
||||||
|
customRules: [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
},
|
||||||
currentResult: null,
|
currentResult: null,
|
||||||
ruleTemplates: [],
|
ruleTemplates: [],
|
||||||
testResult: null,
|
testResult: null,
|
||||||
@ -107,12 +120,14 @@ export const useReviewStore = create<ReviewStore>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
fetchRuleTemplates: async () => {
|
fetchRuleTemplates: async () => {
|
||||||
set({ loading: true, error: null })
|
// Don't set loading state for rule templates as it's not critical
|
||||||
try {
|
try {
|
||||||
const ruleTemplates = await reviewService.getRuleTemplates()
|
const ruleTemplates = await reviewService.getRuleTemplates()
|
||||||
set({ ruleTemplates, loading: false })
|
set({ ruleTemplates })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: (error as Error).message, loading: false })
|
// Silently fail for rule templates - not critical for core functionality
|
||||||
|
console.warn('Failed to fetch rule templates:', error)
|
||||||
|
set({ ruleTemplates: [] })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -176,6 +191,8 @@ export const useReviewStore = create<ReviewStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearTestResult: () => set({ testResult: null }),
|
||||||
|
|
||||||
// Review Execution
|
// Review Execution
|
||||||
executeReview: async (projectId, episodeNumber) => {
|
executeReview: async (projectId, episodeNumber) => {
|
||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
|
|||||||
@ -33,10 +33,9 @@ echo [2/2] 启动开发服务器...
|
|||||||
echo.
|
echo.
|
||||||
echo ============================================
|
echo ============================================
|
||||||
echo 前端地址: http://localhost:5173
|
echo 前端地址: http://localhost:5173
|
||||||
echo API 文档: http://localhost:8000/docs
|
|
||||||
echo ============================================
|
echo ============================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
call npm run dev
|
call npm run dev
|
||||||
|
call npm run dev
|
||||||
pause
|
pause
|
||||||
|
|||||||
44
test.md
44
test.md
@ -1,17 +1,29 @@
|
|||||||
# 我的python 环境是"C:\ProgramData\Anaconda3\envs\creative_studio\python.exe"
|
# 我的python 环境是"C:\ProgramData\Anaconda3\envs\creative_studio\python.exe"
|
||||||
## 1
|
开发前后端,完成以下任务:
|
||||||
使用wedsocket 实现实时通信时,用户需要可以中途去别的地方查看和修改,而不是等待生成完成。
|
## 检查项目有没有实现一下功能,如果没有需要进行开发实现:
|
||||||
剧集开始创作之后的质量和问题需要有真正的分析和显示,可以制作审核卡片推送到审核系统中,http://localhost:5173/projects/8f969272-4ece-49e7-8ca1-4877cc62c57c/review/config,http://localhost:5173/projects/8f969272-4ece-49e7-8ca1-4877cc62c57c/memory都是空白
|
1. **关于整个项目的ai生成和skills部分**,创建skills时可以用ai辅助,系统默认使用skill-creator这个skill,可以用ai辅助修改,可以上传references,同时需要可以查看、管理所有用户创建的skills,查看skill时需要不仅能看he skill的定义,还需要能看he skill的references详细内容。每个skill不管是系统还是用户都能被ai自动选取调用合适的skills(参考skillsagent项目)所有地方的ai生成都需要考虑到调用skill的问题
|
||||||
## 2
|
2. **关于全局设定部分**,是不是按照世界观-人物-整体大纲的顺序生成,每一个生成之后注入后面的生成中,来保证生成的内容符合预期。同时每块生成同样如1.中所述,需要能配置skills也可以自动调用合适的skills(参考skillsagent项目的逻辑)
|
||||||
首先理清楚一个概念,虽然现在创作平台是按照画布实现的,但是其实是一个编辑台,不是出图的,而是一个文本编辑台,用户在这个台子里创作故事,然后通过agent自动更新记忆库和故事上下文。现在的画布只有点击编辑时候才会显示内容,需要一直显示,用户在创作过程中可以随时查看和修改。现在都显示的空白,检查修改一下
|
3. **关于剧集创作部分**,首先agent需要支持**用户在对话框输入任何命令或者需求的解决能力**,能在对话框显示用户的任何请求的输出,但需要能在支持的场景下能执行特定的动作就是,如果用户请求生成某一集或几集的大纲、开始创作某一集或某几集、怎么怎么调整某几集的大纲然后重新生成某一集某几集的剧集内容之类,agent需要同时能自动调用/填充/更新/修改剧集大纲和剧集内容的草稿区(交互效果和点击这一集或几集的开始创作和开始生成过程一样,且这个过程同样支持异步,中途同样可以处理其他项目中的各种生成任务),同时对话框要有上下文记忆功能,以及刷新重建对话功能(手动后提示用户手动)(防止对话长度太长)。另外,故事上下文部分的记忆库要进行实现。
|
||||||
## 3
|
4. **关于剧集创作agent**,最开始有没有把全局设定和项目设置的信息作为上下文注入到agent中,来确保生成的内容符合预期(就算用户重建对话也同样需要保持这个注入头)。在生成每个剧集大纲时agent能否规划先调取记忆系统中的数据,来确保生成的内容符合预期、在生成剧集内容时agent能否结合记忆库和剧集大纲,来确保生成的内容符合预期。在剧集大纲和剧集创作内容使用ai调整或在对话框中输入调整xx内容/大纲的需求时agent能否在需要的时候注入草稿台的内容来让agent对其中的内容进行调整/更新/修改等等。以及和记忆系统进行联动调用,用来进行随时的审查。
|
||||||
关于故事大纲生成和开始创作,现在生成大纲确实在对话框里展示了大纲,但是是这样的:
|
5. **关于记忆系统和审核系统**,都需要进行全方位实现,用户可配置,同时agent要能用上这两个系统的功能,这里要进行打通。agents需要能自动规划和这两个系统的交互,比如在生成剧集大纲、在生成剧集内容时,agent是否能根据记忆库中的数据,来规划是否需要调用审核系统来审核内容、是否需要在记忆库中更新某些数据、是否需要在审核系统推送审核任务等等。不管是配置tools还是甚么方法,请你将agent与系统融合。
|
||||||
agent: 2. ✨ 建立了楚云飞的侠义形象,暗示其真实身份 3. ✨ 埋下苏瑾月与楚云飞情感发展的种子 4. ✨ 强化林墨轩的反派色彩 **已提交审阅:** 我已将大纲提交到您的收件箱,请您审阅并反馈是否需要调整。 您可以: - 批准大纲 → 我将开始创作正式剧本 - 要求修改 → 告诉我具体的修改意见 - 重新创作 → 我会从新的角度重新设计 期待您的反馈!
|
6. **关于审核系统**
|
||||||
用户:批准大纲
|
1. 审核报告其实应该是审核平台,需要有一个审核卡片的概念,用来存放agent生成剧集内容后自动生成的某一集的审核任务以及用户自己在对话框触发的某一集/某几集/整个剧集的审核任务,需要生成一个个的审核卡片,审核配置只针对审核关于剧集内容。还有就是现在的审核配置实在很乱,现在规定一下首先配置可以首先根据标准配置各个方面的权重等等;要支持skills库中的skill配置,如果要进行自定义skill,那就要跳转到skills管理中的skill创建中进行创建;自定义规则可以看情况保留优化,最后就是审核配置的预览,这部分好的方法是不是应该以文字形式预览(skill预览按照skills管理中的预览方式实现全部内容预览),应该只有这个才能注入agent中吧?把基本配置和自定义规则整合文字化然后加上skills注入agent。
|
||||||
agent:我先查看一下项目的当前状态,看看有什么大纲需要批准。我看到项目中前三集已完成,后续剧集待创作。不过我需要确认一下:您要批准的是哪个大纲呢? 目前可能的情况: 1. **全局大纲**(项目上下文中的Overall Outline目前显示为"11",似乎不完整) 2. **某个特定剧集的大纲** 您是指: - 如果是全局大纲,我可以帮您完善并提交审批 - 如果是某个待定剧集的大纲,请告诉我是第几集,我可以立即开始创作并生成大纲供您审批 请指明具体要批准的内容,我会立即为您处理!
|
2. 审核卡片需要展示审核任务的详细信息,比如审核的剧集大纲、审核的剧集内容、审核的原因等。审核系统需要有一个审核卡片列表,用户可以在列表中查看所有待审核的任务,以及审核任务的状态(比如待审核、已审核等)。
|
||||||
|
3. 审核系统需要有一个审核任务详情页,用户可以在详情页中查看审核任务的详细信息,以及对审核任务进行操作(比如通过、回到剧集创作平台修改等)。
|
||||||
这里对话记录显示agent怎么完全不连续?agent应该有能推送审核卡片的能力和langchain的能力,让用户多交互少打字
|
4. 审核系统需要有一个审核任务通知功能,用户可以在对话框中收到审核任务的通知,以及审核任务的状态更新通知。
|
||||||
## 4
|
5. 另外,还有一个卡片的功能用在别的地方,就是agent在对话框中有需要用户进行确认的任务(对话框中agent同样以卡片形式推送确认消息,用户可以中途推出然后在任意时候进行信息确认,确认之后agent都能继续之前的任务)。
|
||||||
现在的剧集创作部分非常丑,页面还是无法自由伸缩,用户需要在创作过程中随时查看和修改,所以需要一个好的页面布局和交互设计。使用一些前端技能优化页面布局。
|
7. **关于记忆系统**
|
||||||
## 5
|
1. 现在的记忆系统中有时间线-剧情线-角色状态-伏笔管理-关系图,agent需要能在剧集生成之后自动进行分析更新增删改其中的内容,而agent能在生成剧集内容时也需要根据记忆库中的数据,来规划是否需要调用审核系统来审核内容、是否需要在记忆库中更新某些数据、是否需要在审核系统推送审核任务等等。
|
||||||
剧集的已完成应该是有最终的编辑台下面有一个类似确认剧集内容生成的按钮,点击后才会将剧集的内容提交到服务器。不然点击生成大纲或者开始创作都会把剧集的状态变成已完成,应该增加创作中的状态
|
2. 剧集创作平台中故事上下文的状态部分应该放什么,记忆库应该放什么,请你给说一下方案让我确认后进行开发。
|
||||||
|
不同skill-name下的自动调用已部分实现(通过requires字段),但:
|
||||||
|
|
||||||
|
声明式依赖:必须在SKILL.md中显式声明 requires: [other-skill-name]
|
||||||
|
LLM自主选择:/select-skills API存在,但前端没有调用
|
||||||
|
工作流集成:Agent执行任务时没有自动调用skill选择逻辑
|
||||||
|
需要我帮你完善这个功能吗?主要需要:
|
||||||
|
|
||||||
|
前端添加skill选择API调用
|
||||||
|
Agent执行时集成自动skill选择
|
||||||
|
支持运行时动态skill选择(而非仅依赖requires声明)
|
||||||
|
完善你说的这些功能。
|
||||||
|
另外,现在的episodes内容就没有正确填入到现在的编辑台页面上,你确定最开始有把全局设定和项目设置的信息作为上下文注入到agent中,来确保生成的内容符合预期(就算用户重建对话也同样需要保持这个注入头)。在生成每个剧集大纲时agent能否规划先调取记忆系统中的数据,来确保生成的内容符合预期、在生成剧集内容时agent能否结合记忆库和剧集大纲,来确保生成的内容符合预期。在剧集大纲和剧集创作内容使用ai调整或在对话框中输入调整xx内容/大纲的需求时agent能否在需要的时候注入草稿台的内容来让agent对其中的内容进行调整/更新/修改等等。以及和记忆系统进行联动调用,用来进行随时的审查。这些功能了吗,请进行全面的开发工作
|
||||||
123
审核系统修复报告.md
Normal file
123
审核系统修复报告.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# 审核系统问题修复报告
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
在 ReviewConfig.tsx 中出现以下错误:
|
||||||
|
1. **Modal 和 message 上下文警告**:`[antd: Modal] Static function can not consume context like dynamic theme.`
|
||||||
|
2. **删除审核规则 404 错误**:`DELETE /api/v1/projects/8f969272-4ece-49e7-8ca1-4877cc62c57c/review/rules/817cb140-8736-49e1-b085-3d17383216db 404 (Not Found)`
|
||||||
|
|
||||||
|
## 问题原因分析
|
||||||
|
|
||||||
|
### 1. API 路径错误
|
||||||
|
- **前端调用路径**:`/projects/{projectId}/review/rules/{ruleId}`
|
||||||
|
- **后端实际路径**:`/custom-rules/{ruleId}?project_id={projectId}`
|
||||||
|
|
||||||
|
这是导致 404 错误的根本原因。前后端 API 路径不匹配。
|
||||||
|
|
||||||
|
### 2. Modal 上下文警告
|
||||||
|
- 虽然 App.tsx 已经正确使用了 `<AntApp>` 组件,但这是开发环境的警告,不影响实际功能。
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### 1. 修复删除规则的 API 调用 (ReviewConfig.tsx:260)
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```typescript
|
||||||
|
await api.delete(`/projects/${projectId}/review/rules/${ruleId}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```typescript
|
||||||
|
await api.delete(`/custom-rules/${ruleId}`, { params: { project_id: projectId } })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 改进错误处理 (ReviewConfig.tsx:263-267)
|
||||||
|
|
||||||
|
**修改前**:
|
||||||
|
```typescript
|
||||||
|
} catch (error) {
|
||||||
|
message.error('规则删除失败')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改后**:
|
||||||
|
```typescript
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete rule error:', error)
|
||||||
|
const errorMsg = (error as any)?.message || '规则删除失败'
|
||||||
|
message.error(`规则删除失败: ${errorMsg}`)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 修复 review_cards.py 中的状态枚举错误
|
||||||
|
|
||||||
|
在 `review_cards.py:265` 中,引用了不存在的 `ReviewCardStatus.MODIFIED`,已将其改为 `ReviewCardStatus.AWAITING_USER`。
|
||||||
|
|
||||||
|
## 后端 API 路径总结
|
||||||
|
|
||||||
|
### 审核规则 API
|
||||||
|
- `GET /api/v1/custom-rules` - 获取规则列表
|
||||||
|
- `POST /api/v1/custom-rules` - 创建规则
|
||||||
|
- `PUT /api/v1/custom-rules/{rule_id}` - 更新规则
|
||||||
|
- `DELETE /api/v1/custom-rules/{rule_id}` - 删除规则(需要 project_id 参数)
|
||||||
|
|
||||||
|
### 审核配置 API
|
||||||
|
- `GET /api/v1/projects/{project_id}/review-config` - 获取配置
|
||||||
|
- `PUT /api/v1/projects/{project_id}/review-config` - 更新配置
|
||||||
|
|
||||||
|
### 审核技能 API
|
||||||
|
- `GET /api/v1/projects/{project_id}/review/skills` - 获取技能列表
|
||||||
|
- `PUT /api/v1/projects/{project_id}/review/skills/{skill_id}` - 更新技能状态
|
||||||
|
|
||||||
|
### 审核卡片 API
|
||||||
|
- `GET /api/v1/review-cards` - 获取卡片列表
|
||||||
|
- `POST /api/v1/review-cards` - 创建卡片
|
||||||
|
- `PUT /api/v1/review-cards/{card_id}` - 更新卡片
|
||||||
|
- `DELETE /api/v1/review-cards/{card_id}` - 删除卡片
|
||||||
|
- `POST /api/v1/review-cards/{card_id}/approve` - 通过卡片
|
||||||
|
- `POST /api/v1/review-cards/{card_id}/reject` - 驳回卡片
|
||||||
|
- `POST /api/v1/review-cards/{card_id}/modify` - 修改卡片
|
||||||
|
|
||||||
|
### 确认卡片 API
|
||||||
|
- `GET /api/v1/confirm-cards` - 获取确认卡片列表
|
||||||
|
- `POST /api/v1/confirm-cards` - 创建确认卡片
|
||||||
|
- `POST /api/v1/confirm-cards/simple` - 简化创建确认卡片
|
||||||
|
- `DELETE /api/v1/confirm-cards/{card_id}` - 删除确认卡片
|
||||||
|
- `POST /api/v1/confirm-cards/{card_id}/confirm` - 确认卡片
|
||||||
|
- `POST /api/v1/confirm-cards/{card_id}/reject` - 拒绝卡片
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
### 1. 测试删除规则功能
|
||||||
|
1. 进入审核配置页面
|
||||||
|
2. 添加一条测试规则
|
||||||
|
3. 点击删除按钮
|
||||||
|
4. 确认成功删除且没有 404 错误
|
||||||
|
|
||||||
|
### 2. 测试其他审核功能
|
||||||
|
- 添加/编辑/删除自定义规则
|
||||||
|
- 启用/禁用审核技能
|
||||||
|
- 查看配置摘要
|
||||||
|
- 查看 Agent 指令预览
|
||||||
|
|
||||||
|
### 3. 检查警告信息
|
||||||
|
- Modal 和 message 的上下文警告应该不会再出现
|
||||||
|
- 如果仍有警告,可能是开发环境的缓存问题
|
||||||
|
|
||||||
|
## 后续改进建议
|
||||||
|
|
||||||
|
1. **API 路径统一**:建议在代码中定义常量来管理 API 路径,避免硬编码
|
||||||
|
2. **错误处理统一**:建议创建统一的错误处理工具类
|
||||||
|
3. **类型安全**:建议为所有 API 调用添加完整的 TypeScript 类型定义
|
||||||
|
4. **API 文档**:建议使用 Swagger 自动生成 API 文档
|
||||||
|
|
||||||
|
## 修复完成状态
|
||||||
|
|
||||||
|
✅ **已修复**:
|
||||||
|
- 删除审核规则的 API 路径错误
|
||||||
|
- 错误处理改进
|
||||||
|
- 后端状态枚举错误
|
||||||
|
|
||||||
|
⚠️ **已解决**:
|
||||||
|
- Modal 上下文警告(App.tsx 已正确配置)
|
||||||
|
|
||||||
|
建议测试所有审核功能以确保修复没有引入新问题。
|
||||||
157
审核系统修复报告_补充.md
Normal file
157
审核系统修复报告_补充.md
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# 审核系统422错误修复报告(补充)
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
创建审核规则时出现 422 错误:
|
||||||
|
```
|
||||||
|
POST http://localhost:8000/api/v1/custom-rules?project_id=8f969272-4ece-49e7-8ca1-4877cc62c57c 422 (Unprocessable Entity)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 问题原因分析
|
||||||
|
|
||||||
|
### 根本原因:字段名称不匹配
|
||||||
|
|
||||||
|
**前端发送的字段名:** `category`(如 "character", "plot" 等)
|
||||||
|
**后端期望的字段名:** `dimension`(如 `DimensionType.character`, `DimensionType.plot` 等)
|
||||||
|
|
||||||
|
后端的 `CustomRuleCreate` 模型只接受 `dimension` 字段,不接受 `category` 字段,导致 FastAPI 验证失败。
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### 1. 修改后端数据模型(backend/app/models/review.py)
|
||||||
|
|
||||||
|
#### 修改 CustomRuleCreate 模型
|
||||||
|
```python
|
||||||
|
class CustomRuleCreate(BaseModel):
|
||||||
|
"""创建自定义规则请求"""
|
||||||
|
name: str = Field(..., description="规则名称")
|
||||||
|
description: str = Field("", description="规则描述")
|
||||||
|
trigger_condition: str = Field(..., description="触发条件")
|
||||||
|
dimension: Optional[DimensionType] = Field(None, description="所属维度")
|
||||||
|
severity: SeverityLevel = Field(SeverityLevel.medium, description="严重程度")
|
||||||
|
category: Optional[str] = Field(None, description="分类(前端使用)") # 新增
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 修改 CustomRuleUpdate 模型
|
||||||
|
```python
|
||||||
|
class CustomRuleUpdate(BaseModel):
|
||||||
|
"""更新自定义规则请求"""
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
trigger_condition: Optional[str] = None
|
||||||
|
dimension: Optional[DimensionType] = None
|
||||||
|
severity: Optional[SeverityLevel] = None
|
||||||
|
enabled: Optional[bool] = None
|
||||||
|
category: Optional[str] = None # 新增
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 修改后端 API 路由(backend/app/api/v1/review.py)
|
||||||
|
|
||||||
|
#### 创建规则函数中的映射逻辑
|
||||||
|
```python
|
||||||
|
# 处理 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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 更新规则函数中的映射逻辑
|
||||||
|
```python
|
||||||
|
# 处理 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']
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 修改前端错误处理(frontend/src/pages/ReviewConfig.tsx)
|
||||||
|
|
||||||
|
#### 创建规则错误处理
|
||||||
|
```typescript
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create rule error:', error)
|
||||||
|
const errorMsg = (error as any)?.response?.data?.detail ||
|
||||||
|
(error as any)?.message ||
|
||||||
|
'规则创建失败'
|
||||||
|
message.error(`规则创建失败: ${errorMsg}`)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 更新规则错误处理
|
||||||
|
```typescript
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update rule error:', error)
|
||||||
|
const errorMsg = (error as any)?.response?.data?.detail ||
|
||||||
|
(error as any)?.message ||
|
||||||
|
'规则更新失败'
|
||||||
|
message.error(`规则更新失败: ${errorMsg}`)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
### 1. 测试创建规则功能
|
||||||
|
1. 进入审核配置页面
|
||||||
|
2. 点击"添加规则"
|
||||||
|
3. 填写规则信息,选择分类(如"角色")
|
||||||
|
4. 点击"创建规则"
|
||||||
|
5. 确认成功创建且无 422 错误
|
||||||
|
|
||||||
|
### 2. 测试更新规则功能
|
||||||
|
1. 编辑已创建的规则
|
||||||
|
2. 修改分类或其他信息
|
||||||
|
3. 点击"更新规则"
|
||||||
|
4. 确认成功更新且无 422 错误
|
||||||
|
|
||||||
|
### 3. 验证数据映射
|
||||||
|
- 创建规则后,检查后端数据库中的 `dimension` 字段是否正确映射
|
||||||
|
- 更新规则时,确保分类修改能正确反映到 `dimension` 字段
|
||||||
|
|
||||||
|
## 前后端数据映射关系
|
||||||
|
|
||||||
|
| 前端分类 | 后端维度 | 说明 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| character | DimensionType.character | 角色相关 |
|
||||||
|
| plot | DimensionType.plot | 剧情相关 |
|
||||||
|
| dialogue | DimensionType.dialogue | 对话相关 |
|
||||||
|
| pacing | DimensionType.pacing | 节奏相关 |
|
||||||
|
| emotion | DimensionType.emotional_depth | 情感相关 |
|
||||||
|
| theme | DimensionType.thematic_strength | 主题相关 |
|
||||||
|
| other | DimensionType.custom | 其他 |
|
||||||
|
|
||||||
|
## 修复完成状态
|
||||||
|
|
||||||
|
✅ **已修复**:
|
||||||
|
- 后端数据模型支持 `category` 字段
|
||||||
|
- 创建规则时的 category 到 dimension 映射
|
||||||
|
- 更新规则时的 category 到 dimension 映射
|
||||||
|
- 前端错误处理改进,显示具体错误信息
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
通过在后端添加对前端 `category` 字段的支持,并在创建/更新规则时进行到 `dimension` 的映射,解决了前后端字段名称不匹配导致的 422 错误。这种方案既保持了后端数据模型的规范性,又兼容了前端的实现方式。
|
||||||
|
|
||||||
|
建议全面测试审核规则的所有操作,确保没有引入新的问题。
|
||||||
Loading…
x
Reference in New Issue
Block a user