- 新增审核卡片和确认卡片模型,支持Agent推送审核任务和用户确认 - 实现审核卡片API服务,支持创建、更新、批准、驳回等操作 - 扩展审核维度配置,新增角色一致性、剧情连贯性等维度 - 优化前端审核配置页面,修复API路径错误和状态枚举问题 - 改进剧集创作平台布局,新增左侧边栏用于剧集管理和上下文查看 - 增强Skill管理,支持从审核系统跳转创建/编辑Skill - 修复episodes.json数据问题,清理聊天历史记录 - 更新Agent提示词,明确Skill引用加载流程 - 统一前端主题配置,优化整体UI体验
2159 lines
83 KiB
Python
2159 lines
83 KiB
Python
"""
|
||
WebSocket Streaming API
|
||
|
||
提供实时执行进度更新的 WebSocket 端点
|
||
"""
|
||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, Query
|
||
from typing import Dict, Set, Optional, Any, List
|
||
import json
|
||
import asyncio
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
from app.config import settings
|
||
from app.utils.logger import get_logger
|
||
from app.core.agent_runtime.director_agent import DirectorAgent
|
||
from app.db.repositories import message_repo
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
# ============================================
|
||
# 动态Skill选择辅助函数
|
||
# ============================================
|
||
|
||
async def _auto_select_skills_for_context(
|
||
user_message: str,
|
||
project_id: str,
|
||
agent
|
||
) -> List[str]:
|
||
"""
|
||
根据用户消息自动选择相关Skills
|
||
|
||
Args:
|
||
user_message: 用户消息
|
||
project_id: 项目ID
|
||
agent: Agent实例
|
||
|
||
Returns:
|
||
选中的Skill ID列表
|
||
"""
|
||
try:
|
||
# 获取可用的用户技能
|
||
user_skills = []
|
||
if agent.context and agent.context.user_skills:
|
||
user_skills = agent.context.user_skills
|
||
|
||
# 如果有用户配置的Skills,使用智能选择
|
||
if user_skills:
|
||
# 从上下文中提取信息进行智能匹配
|
||
context_info = {
|
||
"episode_number": agent.context.active_episode_number if agent.context else None,
|
||
"project_genre": agent.context.project_genre if agent.context else None,
|
||
"creation_mode": agent.context.creation_mode if agent.context else None
|
||
}
|
||
|
||
selected_skills = await agent.skill_loader.select_skills_for_task(
|
||
user_message,
|
||
available_skills=user_skills,
|
||
context=context_info
|
||
)
|
||
|
||
if selected_skills:
|
||
logger.info(f"自动选择的Skills: {selected_skills}")
|
||
return selected_skills
|
||
|
||
return []
|
||
except Exception as e:
|
||
logger.warning(f"自动Skill选择失败: {e}")
|
||
return []
|
||
|
||
|
||
# ============================================
|
||
# WebSocket 连接管理
|
||
# ============================================
|
||
|
||
class ConnectionManager:
|
||
"""WebSocket 连接管理器"""
|
||
|
||
def __init__(self):
|
||
# 项目ID -> WebSocket连接集合
|
||
self.project_connections: Dict[str, Set[WebSocket]] = {}
|
||
# 批次ID -> WebSocket连接集合
|
||
self.batch_connections: Dict[str, Set[WebSocket]] = {}
|
||
# 项目ID -> Agent实例
|
||
self.project_agents: Dict[str, DirectorAgent] = {}
|
||
|
||
async def connect_to_project(self, websocket: WebSocket, project_id: str):
|
||
"""连接到项目执行流"""
|
||
await websocket.accept()
|
||
if project_id not in self.project_connections:
|
||
self.project_connections[project_id] = set()
|
||
self.project_connections[project_id].add(websocket)
|
||
logger.info(f"WebSocket 已连接到项目: {project_id}")
|
||
|
||
async def connect_to_batch(self, websocket: WebSocket, batch_id: str):
|
||
"""连接到批次执行流"""
|
||
await websocket.accept()
|
||
if batch_id not in self.batch_connections:
|
||
self.batch_connections[batch_id] = set()
|
||
self.batch_connections[batch_id].add(websocket)
|
||
logger.info(f"WebSocket 已连接到批次: {batch_id}")
|
||
|
||
def disconnect(self, websocket: WebSocket):
|
||
"""断开连接"""
|
||
# 从所有项目连接中移除
|
||
for project_id, connections in self.project_connections.items():
|
||
if websocket in connections:
|
||
connections.remove(websocket)
|
||
logger.info(f"WebSocket 已从项目断开: {project_id}")
|
||
if not connections:
|
||
del self.project_connections[project_id]
|
||
# 清理 Agent 实例
|
||
if project_id in self.project_agents:
|
||
del self.project_agents[project_id]
|
||
|
||
# 从所有批次连接中移除
|
||
for batch_id, connections in self.batch_connections.items():
|
||
if websocket in connections:
|
||
connections.remove(websocket)
|
||
logger.info(f"WebSocket 已从批次断开: {batch_id}")
|
||
if not connections:
|
||
del self.batch_connections[batch_id]
|
||
|
||
async def send_to_project(
|
||
self,
|
||
project_id: str,
|
||
message: Dict[str, Any],
|
||
exclude: Optional[WebSocket] = None
|
||
):
|
||
"""向项目的所有连接发送消息"""
|
||
if project_id in self.project_connections:
|
||
disconnected = set()
|
||
for connection in self.project_connections[project_id]:
|
||
if connection != exclude:
|
||
try:
|
||
await connection.send_json(message)
|
||
except Exception as e:
|
||
logger.error(f"发送消息失败: {str(e)}")
|
||
disconnected.add(connection)
|
||
|
||
# 清理断开的连接
|
||
for connection in disconnected:
|
||
self.disconnect(connection)
|
||
|
||
async def send_to_batch(
|
||
self,
|
||
batch_id: str,
|
||
message: Dict[str, Any],
|
||
exclude: Optional[WebSocket] = None
|
||
):
|
||
"""向批次的所有连接发送消息"""
|
||
if batch_id in self.batch_connections:
|
||
disconnected = set()
|
||
for connection in self.batch_connections[batch_id]:
|
||
if connection != exclude:
|
||
try:
|
||
await connection.send_json(message)
|
||
except Exception as e:
|
||
logger.error(f"发送消息失败: {str(e)}")
|
||
disconnected.add(connection)
|
||
|
||
# 清理断开的连接
|
||
for connection in disconnected:
|
||
self.disconnect(connection)
|
||
|
||
def get_agent(self, project_id: str, working_dir: Path) -> DirectorAgent:
|
||
"""获取或创建 Agent 实例"""
|
||
if project_id not in self.project_agents:
|
||
# 确保工作目录存在
|
||
working_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 检查配置的模型类型
|
||
model_name = settings.zai_model
|
||
enable_thinking = True
|
||
|
||
# 如果是 GLM 模型,禁用 thinking 模式(不支持)
|
||
if "glm" in model_name.lower():
|
||
enable_thinking = False
|
||
|
||
# 不在这里加载项目上下文,而是在 WebSocket 消息处理时加载
|
||
# 因为 get_agent 是同步方法,而 project_repo.get 是异步的
|
||
self.project_agents[project_id] = DirectorAgent(
|
||
working_directory=working_dir,
|
||
enable_thinking=enable_thinking,
|
||
model=model_name,
|
||
project_context=None # 稍后在消息处理中更新
|
||
)
|
||
return self.project_agents[project_id]
|
||
|
||
def get_project_connections_count(self, project_id: str) -> int:
|
||
"""获取项目的连接数"""
|
||
return len(self.project_connections.get(project_id, set()))
|
||
|
||
def get_batch_connections_count(self, batch_id: str) -> int:
|
||
"""获取批次的连接数"""
|
||
return len(self.batch_connections.get(batch_id, set()))
|
||
|
||
|
||
# 全局连接管理器
|
||
manager = ConnectionManager()
|
||
|
||
|
||
# ============================================
|
||
# WebSocket 端点
|
||
# ============================================
|
||
|
||
@router.websocket("/ws/projects/{project_id}/execute")
|
||
async def websocket_project_execution(
|
||
websocket: WebSocket,
|
||
project_id: str
|
||
):
|
||
"""
|
||
项目执行 WebSocket 端点
|
||
"""
|
||
await manager.connect_to_project(websocket, project_id)
|
||
|
||
# 准备工作目录 (假设在 projects/{id})
|
||
# 注意:这里需要根据实际配置调整路径
|
||
project_dir = Path(f"d:/platform/creative_studio/workspace/projects/{project_id}")
|
||
|
||
try:
|
||
# 发送连接确认
|
||
await websocket.send_json({
|
||
"type": "connected",
|
||
"data": {
|
||
"project_id": project_id,
|
||
"message": "已连接到项目执行流",
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
})
|
||
|
||
# 加载并发送历史消息
|
||
history = await message_repo.get_history(project_id)
|
||
if history:
|
||
await websocket.send_json({
|
||
"type": "history",
|
||
"messages": history
|
||
})
|
||
|
||
# 保持连接并接收客户端消息
|
||
while True:
|
||
try:
|
||
# 接收客户端消息
|
||
data = await websocket.receive_text()
|
||
message = json.loads(data)
|
||
|
||
# 处理客户端消息
|
||
await _handle_client_message(websocket, project_id, message, project_dir)
|
||
|
||
except WebSocketDisconnect:
|
||
logger.info(f"WebSocket 客户端主动断开: {project_id}")
|
||
break
|
||
except json.JSONDecodeError:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": {
|
||
"message": "无效的 JSON 格式"
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"处理 WebSocket 消息错误: {str(e)}")
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": {
|
||
"message": f"处理消息错误: {str(e)}"
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"WebSocket 连接错误: {str(e)}")
|
||
|
||
finally:
|
||
manager.disconnect(websocket)
|
||
|
||
|
||
@router.websocket("/ws/batches/{batch_id}/execute")
|
||
async def websocket_batch_execution(
|
||
websocket: WebSocket,
|
||
batch_id: str
|
||
):
|
||
"""
|
||
批量执行 WebSocket 端点
|
||
"""
|
||
await manager.connect_to_batch(websocket, batch_id)
|
||
|
||
try:
|
||
# 发送连接确认
|
||
await websocket.send_json({
|
||
"type": "connected",
|
||
"data": {
|
||
"batch_id": batch_id,
|
||
"message": "已连接到批量执行流",
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
})
|
||
|
||
# 保持连接
|
||
while True:
|
||
try:
|
||
data = await websocket.receive_text()
|
||
# 批量执行目前不接受客户端控制消息,仅广播
|
||
# 但为了保持连接活性,可以处理 ping
|
||
message = json.loads(data)
|
||
if message.get("type") == "ping":
|
||
await websocket.send_json({
|
||
"type": "pong",
|
||
"data": {"timestamp": datetime.now().isoformat()}
|
||
})
|
||
|
||
except WebSocketDisconnect:
|
||
logger.info(f"WebSocket 客户端主动断开: {batch_id}")
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"处理 WebSocket 消息错误: {str(e)}")
|
||
|
||
finally:
|
||
manager.disconnect(websocket)
|
||
|
||
|
||
# ============================================
|
||
# 消息处理
|
||
# ============================================
|
||
|
||
async def _ensure_agent_context(
|
||
agent: DirectorAgent,
|
||
project_id: str,
|
||
active_episode_number: Optional[int] = None,
|
||
active_episode_title: Optional[str] = None
|
||
):
|
||
"""确保 Agent 加载了正确的项目上下文"""
|
||
# 如果已经加载了该项目的上下文,且剧集信息一致,则跳过
|
||
if (agent.context and
|
||
agent.context.project_id == project_id and
|
||
agent.context.active_episode_number == active_episode_number):
|
||
return
|
||
|
||
try:
|
||
from app.db.repositories import project_repo
|
||
from app.core.agent_runtime.context import SkillAgentContext
|
||
from app.core.skills.skill_manager import skill_manager
|
||
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
logger.warning(f"Project {project_id} not found in repository")
|
||
return
|
||
|
||
# 1. 加载用户技能
|
||
user_skills = []
|
||
default_task_skills = getattr(project, 'defaultTaskSkills', [])
|
||
if isinstance(default_task_skills, list):
|
||
for task_config in default_task_skills:
|
||
task_skills = getattr(task_config, 'skills', [])
|
||
if isinstance(task_skills, list):
|
||
for skill_config in task_skills:
|
||
try:
|
||
skill_id = getattr(skill_config, 'skill_id', None) or (skill_config.get('skill_id') if isinstance(skill_config, dict) else None)
|
||
if skill_id:
|
||
skill = skill_manager.get_skill_by_id(skill_id)
|
||
if skill:
|
||
user_skills.append({
|
||
'id': skill.id,
|
||
'name': skill.name,
|
||
'behavior': skill.behavior_guide or skill.description or ''
|
||
})
|
||
except Exception as e:
|
||
logger.warning(f"Failed to load skill: {e}")
|
||
|
||
# 2. 整合角色信息
|
||
characters_text = ""
|
||
if project.globalContext:
|
||
if project.globalContext.styleGuide:
|
||
characters_text += project.globalContext.styleGuide + "\n\n"
|
||
|
||
if project.globalContext.characterProfiles:
|
||
# 确保 characterProfiles 是字典类型
|
||
profiles = project.globalContext.characterProfiles
|
||
if isinstance(profiles, dict):
|
||
characters_text += "### Detailed Character Profiles:\n"
|
||
for name, profile in profiles.items():
|
||
characters_text += f"- **{name}**: {getattr(profile, 'description', '')}\n"
|
||
if hasattr(profile, 'personality') and profile.personality:
|
||
characters_text += f" Personality: {profile.personality}\n"
|
||
else:
|
||
# characterProfiles 是字符串或其他类型,直接添加
|
||
characters_text += str(profiles) + "\n\n"
|
||
|
||
# 3. 获取所有剧集
|
||
from app.db.repositories import episode_repo
|
||
episodes_data = await episode_repo.list_by_project(project_id)
|
||
episodes = []
|
||
for ep in episodes_data:
|
||
episodes.append({
|
||
"number": ep.number,
|
||
"title": ep.title,
|
||
"status": ep.status
|
||
})
|
||
|
||
# 4. 创建项目上下文
|
||
project_context = SkillAgentContext(
|
||
skill_loader=agent.context.skill_loader,
|
||
working_directory=agent.context.working_directory,
|
||
project_id=project.id,
|
||
project_name=project.name,
|
||
project_genre=getattr(project, 'genre', '古风'),
|
||
total_episodes=project.totalEpisodes,
|
||
world_setting=project.globalContext.worldSetting if project.globalContext else None,
|
||
characters=characters_text.strip() or None,
|
||
overall_outline=project.globalContext.overallOutline if project.globalContext else None,
|
||
creation_mode='script' if (project.globalContext and project.globalContext.uploadedScript) else 'inspiration',
|
||
source_content=(project.globalContext.uploadedScript if project.globalContext and project.globalContext.uploadedScript
|
||
else project.globalContext.inspiration if project.globalContext else None),
|
||
user_skills=user_skills,
|
||
active_episode_number=active_episode_number,
|
||
active_episode_title=active_episode_title,
|
||
episodes=episodes
|
||
)
|
||
|
||
# 5. 更新 Agent
|
||
agent.context = project_context
|
||
agent.system_prompt = agent._build_system_prompt()
|
||
if hasattr(agent, 'refresh_agent'):
|
||
agent.refresh_agent()
|
||
|
||
msg = f"Successfully injected project context for {project_id}: {project.name}"
|
||
if active_episode_number:
|
||
msg += f" (Focusing on Episode {active_episode_number})"
|
||
logger.info(msg)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to load project context for {project_id}: {e}", exc_info=True)
|
||
|
||
|
||
async def _handle_client_message(
|
||
websocket: WebSocket,
|
||
project_id: str,
|
||
message: Dict[str, Any],
|
||
project_dir: Path
|
||
):
|
||
"""
|
||
处理客户端发送的消息
|
||
"""
|
||
message_type = message.get("type")
|
||
logger.info(f"收到消息: type={message_type}, full_message={message}")
|
||
|
||
if message_type == "ping":
|
||
# 心跳响应
|
||
await websocket.send_json({
|
||
"type": "pong",
|
||
"data": {
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
})
|
||
|
||
elif message_type == "focus_episode":
|
||
# 客户端请求切换关注的剧集
|
||
episode_number = message.get("episodeNumber")
|
||
episode_title = message.get("episodeTitle")
|
||
|
||
agent = manager.get_agent(project_id, project_dir)
|
||
await _ensure_agent_context(agent, project_id, episode_number, episode_title)
|
||
|
||
await websocket.send_json({
|
||
"type": "focus_confirmed",
|
||
"data": {
|
||
"episodeNumber": episode_number,
|
||
"episodeTitle": episode_title
|
||
}
|
||
})
|
||
|
||
elif message_type == "update_episode_title":
|
||
# 用户手动编辑画布标题
|
||
episode_number = message.get("episodeNumber")
|
||
new_title = message.get("title")
|
||
|
||
if episode_number and new_title:
|
||
try:
|
||
from app.db.repositories import episode_repo
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = next((ep for ep in episodes if ep.number == episode_number), None)
|
||
if episode:
|
||
episode.title = new_title
|
||
await episode_repo.update(episode)
|
||
logger.info(f"Manual title update for EP{episode_number}: {new_title}")
|
||
|
||
# 广播更新给所有连接
|
||
await manager.send_to_project(project_id, {
|
||
"type": "episode_updated",
|
||
"data": {
|
||
"number": episode_number,
|
||
"title": new_title,
|
||
"status": episode.status
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"Failed to update episode title: {e}")
|
||
|
||
elif message_type == "inbox_action":
|
||
# 用户在 Inbox 中点击批准或拒绝
|
||
action = message.get("action")
|
||
item_id = message.get("itemId")
|
||
|
||
# 将操作转换为自然语言反馈给 Agent
|
||
feedback = f"User {action}ed inbox item {item_id}."
|
||
|
||
agent = manager.get_agent(project_id, project_dir)
|
||
# 确保上下文已加载
|
||
await _ensure_agent_context(agent, project_id)
|
||
|
||
try:
|
||
for event in agent.stream_events(feedback, thread_id=project_id):
|
||
# 同样的事件处理逻辑
|
||
if event.get("type") == "tool_call":
|
||
await _handle_tool_call(project_id, event)
|
||
await manager.send_to_project(project_id, event)
|
||
except Exception as e:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {"message": str(e)}
|
||
})
|
||
|
||
elif message_type == "reset_conversation":
|
||
# 用户请求重建对话(清除历史但保留上下文)
|
||
try:
|
||
# 清除历史消息
|
||
await message_repo.clear_history(project_id)
|
||
|
||
# 重新注入上下文到agent
|
||
agent = manager.get_agent(project_id, project_dir)
|
||
await _ensure_agent_context(agent, project_id)
|
||
|
||
await websocket.send_json({
|
||
"type": "conversation_reset",
|
||
"data": {
|
||
"message": "对话已重建,上下文已保留",
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
})
|
||
|
||
logger.info(f"Conversation reset for project {project_id}")
|
||
except Exception as e:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": {
|
||
"message": f"重建对话失败: {str(e)}"
|
||
}
|
||
})
|
||
|
||
elif message_type == "clear_conversation":
|
||
# 用户请求完全清除对话(包括重置上下文)
|
||
try:
|
||
# 清除历史消息
|
||
await message_repo.clear_history(project_id)
|
||
|
||
# 重置 Agent 上下文(完全重置)
|
||
if project_id in manager.project_agents:
|
||
del manager.project_agents[project_id]
|
||
|
||
# 创建新的 Agent 实例
|
||
agent = manager.get_agent(project_id, project_dir)
|
||
await _ensure_agent_context(agent, project_id)
|
||
|
||
await websocket.send_json({
|
||
"type": "conversation_cleared",
|
||
"data": {
|
||
"message": "对话已完全清除,上下文已重置",
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
})
|
||
|
||
logger.info(f"Conversation fully cleared for project {project_id}")
|
||
except Exception as e:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": {
|
||
"message": f"清除对话失败: {str(e)}"
|
||
}
|
||
})
|
||
|
||
elif message_type == "chat_message":
|
||
# 用户发送聊天消息 -> 触发 Agent 执行
|
||
content = message.get("content", "")
|
||
episode_number = message.get("episodeNumber")
|
||
episode_title = message.get("episodeTitle")
|
||
view_context = message.get("viewContext", {}) # 新增:接收用户视图上下文
|
||
|
||
if not content:
|
||
return
|
||
|
||
# 保存用户消息
|
||
await message_repo.add_message(project_id, "user", content)
|
||
|
||
# 获取 Agent 并存储视图上下文
|
||
agent = manager.get_agent(project_id, project_dir)
|
||
if view_context:
|
||
# 存储视图上下文到 Agent 状态中
|
||
agent.runtime.state["current_view_context"] = view_context
|
||
logger.info(f"Received view context: {view_context}")
|
||
|
||
# 获取 Agent
|
||
agent = manager.get_agent(project_id, project_dir)
|
||
|
||
# 确保上下文已加载(包含当前剧集信息)
|
||
await _ensure_agent_context(agent, project_id, episode_number, episode_title)
|
||
|
||
# ========== 动态Skill选择 ==========
|
||
# 在执行前自动选择相关的Skills
|
||
selected_skill_ids = await _auto_select_skills_for_context(content, project_id, agent)
|
||
|
||
# 如果选中了Skills,将其注入到系统提示中
|
||
if selected_skill_ids:
|
||
# 动态更新系统提示以包含选中的Skills
|
||
# 这将在后续的agent.stream_events中生效
|
||
agent.runtime.state["selected_skills"] = selected_skill_ids
|
||
|
||
# 通知用户自动选择的Skills
|
||
skill_names = []
|
||
for skill_info in agent.context.user_skills or []:
|
||
if skill_info.get('id') in selected_skill_ids:
|
||
skill_names.append(skill_info.get('name', skill_info.get('id')))
|
||
|
||
if skill_names:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "text",
|
||
"content": f"\n🔧 自动启用技能: {', '.join(skill_names)}\n"
|
||
})
|
||
|
||
# 检查是否是生成大纲的请求
|
||
is_outline_generation = "大纲" in content and "生成" in content
|
||
if is_outline_generation:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "outline_streaming_start"
|
||
})
|
||
|
||
# 检查是否是开始创作的请求,如果是,尝试注入大纲内容
|
||
# 改进匹配逻辑,支持更多创作相关的关键词
|
||
is_content_creation = any(keyword in content for keyword in ["创作", "开始创作", "写内容", "生成内容"])
|
||
if is_content_creation and episode_number:
|
||
try:
|
||
from app.db.repositories import episode_repo
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = next((ep for ep in episodes if ep.number == episode_number), None)
|
||
|
||
if episode and episode.outline and episode.outline.strip():
|
||
# 在用户消息前注入大纲内容
|
||
outline_instruction = f"""
|
||
【当前剧集大纲】
|
||
{episode.outline}
|
||
|
||
请严格按照上述大纲进行创作。大纲中的场景顺序、情节要点、人物对话都必须完整体现在最终的剧本内容中。
|
||
|
||
创作要求:
|
||
1. 完整实现大纲中的所有场景
|
||
2. 保持大纲中的情节要点
|
||
3. 人物对话要符合角色设定
|
||
4. 场景转换要自然流畅
|
||
|
||
现在请开始创作完整的剧集内容。
|
||
|
||
"""
|
||
content = outline_instruction + content
|
||
logger.info(f"已注入大纲内容到EP{episode_number}的创作请求,大纲长度: {len(episode.outline)}")
|
||
|
||
# 通知用户大纲已注入
|
||
await manager.send_to_project(project_id, {
|
||
"type": "text",
|
||
"content": f"\n✅ 已加载 EP{episode_number} 的大纲({len(episode.outline)} 字),将按照大纲进行创作。\n"
|
||
})
|
||
else:
|
||
logger.info(f"EP{episode_number} 没有大纲内容,将自由创作")
|
||
await manager.send_to_project(project_id, {
|
||
"type": "text",
|
||
"content": f"\n⚠️ EP{episode_number} 暂无大纲,将进行自由创作。建议先生成大纲后再开始创作。\n"
|
||
})
|
||
except Exception as e:
|
||
logger.warning(f"注入大纲内容失败: {e}")
|
||
|
||
# 检查是否是模糊请求(需要先获取当前内容)
|
||
# 支持的关键词:改、修改、调整、优化、改进、这里不对、把这个...
|
||
is_vague_request = any(keyword in content for keyword in [
|
||
"改", "修改", "调整", "优化", "改进", "这里不对", "把这个"
|
||
])
|
||
|
||
# 如果是模糊请求且有视图上下文,自动注入草稿内容
|
||
if is_vague_request and view_context and episode_number:
|
||
try:
|
||
from app.db.repositories import episode_repo
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = next((ep for ep in episodes if ep.number == episode_number), None)
|
||
|
||
if episode:
|
||
view_type = view_context.get("viewType", "")
|
||
content_to_inject = ""
|
||
|
||
# 根据视图类型决定注入什么内容
|
||
if view_type == "outline" and episode.outline:
|
||
content_to_inject = f"""
|
||
【用户正在查看:第 {episode_number} 集大纲】
|
||
|
||
【当前大纲内容】
|
||
{episode.outline}
|
||
|
||
用户请求:{content}
|
||
|
||
请基于以上大纲进行用户请求的修改/调整/优化。修改后请使用 update_episode() 保存更新后的大纲,并调用 request_review() 进行质量检查。
|
||
"""
|
||
elif view_type == "content" and episode.content:
|
||
content_preview = episode.content[:3000]
|
||
if len(episode.content) > 3000:
|
||
content_preview += "\n... (内容过长,已截取前3000字)"
|
||
content_to_inject = f"""
|
||
【用户正在查看:第 {episode_number} 集内容】
|
||
|
||
【当前内容】
|
||
{content_preview}
|
||
|
||
用户请求:{content}
|
||
|
||
请基于以上内容进行用户请求的修改/调整/优化。修改后请:
|
||
1. 使用 write_to_canvas(content="修改后的内容", append=false) 更新画布
|
||
2. 使用 update_episode() 保存更新的内容
|
||
3. 调用 request_review() 进行质量检查
|
||
4. 调用 update_memory() 更新记忆系统(如果有重要变化)
|
||
"""
|
||
elif view_type in ["both", "full"]:
|
||
# 同时注入大纲和内容
|
||
outline_part = f"\n【大纲】\n{episode.outline}\n" if episode.outline else ""
|
||
content_preview = episode.content[:1500] if episode.content else ""
|
||
if episode.content and len(episode.content) > 1500:
|
||
content_preview += "\n... (内容过长,已截取前1500字)"
|
||
content_part = f"\n【内容】\n{content_preview}\n" if episode.content else ""
|
||
|
||
content_to_inject = f"""
|
||
【用户正在查看:第 {episode_number} 集(完整视图)】
|
||
|
||
{outline_part}{content_part}
|
||
|
||
用户请求:{content}
|
||
|
||
请基于以上内容进行用户请求的修改/调整/优化。根据用户意图修改大纲或内容。
|
||
修改后请:
|
||
1. 使用 write_to_canvas() 和 update_episode() 更新相应内容
|
||
2. 调用 request_review() 进行质量检查
|
||
3. 调用 update_memory() 更新记忆系统(如果有重要变化)
|
||
"""
|
||
|
||
if content_to_inject:
|
||
content = content_to_inject
|
||
logger.info(f"已为模糊请求注入草稿内容,视图类型: {view_type}, 集数: {episode_number}")
|
||
|
||
# 通知用户
|
||
view_desc = {"outline": "大纲", "content": "内容", "both": "大纲+内容", "full": "大纲+内容"}.get(view_type, "未知")
|
||
await manager.send_to_project(project_id, {
|
||
"type": "text",
|
||
"content": f"\n📝 已加载第 {episode_number} 集的{view_desc},将根据您的要求进行调整。\n"
|
||
})
|
||
else:
|
||
logger.info(f"EP{episode_number} 没有找到剧集数据")
|
||
except Exception as e:
|
||
logger.warning(f"自动注入草稿内容失败: {e}")
|
||
|
||
# 异步运行 Agent 并将事件流推送到前端
|
||
full_response = ""
|
||
outline_content = ""
|
||
try:
|
||
for event in agent.stream_events(content, thread_id=project_id):
|
||
# 检查特殊工具调用并转换格式
|
||
if event.get("type") == "tool_call":
|
||
await _handle_tool_call(project_id, event)
|
||
|
||
# 收集 Agent 回复内容
|
||
if event.get("type") == "text":
|
||
text_content = event.get("content", "")
|
||
full_response += text_content
|
||
|
||
# 如果是生成大纲,收集大纲内容
|
||
if is_outline_generation:
|
||
outline_content += text_content
|
||
|
||
await manager.send_to_project(project_id, event)
|
||
|
||
# 如果是生成大纲,发送大纲更新消息
|
||
if is_outline_generation and outline_content:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "outline_update",
|
||
"content": outline_content
|
||
})
|
||
await manager.send_to_project(project_id, {
|
||
"type": "outline_streaming_end"
|
||
})
|
||
|
||
# 保存 Agent 回复
|
||
if full_response:
|
||
await message_repo.add_message(project_id, "agent", full_response)
|
||
|
||
except Exception as e:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {"message": str(e)}
|
||
})
|
||
if is_outline_generation:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "outline_streaming_end"
|
||
})
|
||
|
||
elif message_type == "get_status":
|
||
# 请求状态
|
||
# 这里可以返回 Agent 的状态,或者之前的 executor 状态
|
||
pass
|
||
|
||
elif message_type == "review_task_created":
|
||
# 审核任务已创建 - 来自Agent推送
|
||
# 前端显示审核卡片
|
||
await manager.send_to_project(project_id, {
|
||
"type": "text",
|
||
"content": f"\n📋 [审核任务] {message.get('data', {}).get('issue_type', '未知')}: {message.get('data', {}).get('issue_description', '')}\n"
|
||
})
|
||
|
||
elif message_type == "review_task_completed":
|
||
# 审核任务已完成 - 用户完成审核任务
|
||
task_data = message.get('data', {})
|
||
await manager.send_to_project(project_id, {
|
||
"type": "text",
|
||
"content": f"\n✅ 审核任务已完成: {task_data.get('status', '未知')}\n"
|
||
})
|
||
|
||
elif message_type == "post_creation_review_completed":
|
||
# 生成后审核完成
|
||
review_data = message.get('data', {})
|
||
task_count = review_data.get('task_count', 0)
|
||
passed = review_data.get('passed', False)
|
||
await manager.send_to_project(project_id, {
|
||
"type": "text",
|
||
"content": f"\n📊 生成后审核完成: 发现 {task_count} 个问题,{'通过' if passed else '未通过'}\n"
|
||
})
|
||
|
||
elif message_type == "review_card_action":
|
||
# 用户对审核卡片的操作(通过/驳回/修改)
|
||
card_id = message.get("cardId")
|
||
action = message.get("action")
|
||
user_comment = message.get("userComment")
|
||
modified_content = message.get("modifiedContent")
|
||
|
||
try:
|
||
from app.db.review_card_repository import get_review_card_repo
|
||
from app.models.review_card import ReviewCardUpdate, ReviewCardStatus
|
||
|
||
repo = get_review_card_repo()
|
||
card = await repo.get(card_id)
|
||
|
||
if not card:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {"message": f"审核卡片不存在: {card_id}"}
|
||
})
|
||
return
|
||
|
||
# 根据操作类型更新卡片
|
||
if action == "approve":
|
||
update_data = ReviewCardUpdate(
|
||
status=ReviewCardStatus.APPROVED,
|
||
user_comment=user_comment
|
||
)
|
||
elif action == "reject":
|
||
update_data = ReviewCardUpdate(
|
||
status=ReviewCardStatus.REJECTED,
|
||
user_comment=user_comment,
|
||
modified_content=modified_content
|
||
)
|
||
elif action == "modify":
|
||
update_data = ReviewCardUpdate(
|
||
status=ReviewCardStatus.MODIFIED,
|
||
user_comment=user_comment,
|
||
modified_content=modified_content
|
||
)
|
||
elif action == "ignore":
|
||
update_data = ReviewCardUpdate(
|
||
status=ReviewCardStatus.IGNORED,
|
||
user_comment=user_comment
|
||
)
|
||
else:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {"message": f"未知操作: {action}"}
|
||
})
|
||
return
|
||
|
||
# 更新卡片
|
||
updated_card = await repo.update(card_id, update_data)
|
||
|
||
# 通知前端操作完成
|
||
await manager.send_to_project(project_id, {
|
||
"type": "review_card_action_completed",
|
||
"data": {
|
||
"card_id": card_id,
|
||
"action": action,
|
||
"status": updated_card.status.value if updated_card else "",
|
||
"updated_at": updated_card.updated_at.isoformat() if updated_card else None
|
||
}
|
||
})
|
||
|
||
logger.info(f"用户对审核卡片执行操作: {card_id}, 操作: {action}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理审核卡片操作失败: {e}")
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {"message": f"操作失败: {str(e)}"}
|
||
})
|
||
|
||
elif message_type == "confirm_card_response":
|
||
# 用户对确认卡片的响应
|
||
card_id = message.get("cardId")
|
||
selected_option_id = message.get("selectedOptionId")
|
||
custom_response = message.get("customResponse")
|
||
user_notes = message.get("userNotes")
|
||
action = message.get("action", "confirm")
|
||
|
||
try:
|
||
from app.db.confirm_card_repository import get_confirm_card_repo
|
||
from app.models.confirm_card import ConfirmCardStatus
|
||
|
||
repo = get_confirm_card_repo()
|
||
|
||
if action == "confirm":
|
||
# 确认卡片
|
||
updated_card = await repo.update_status(
|
||
card_id,
|
||
ConfirmCardStatus.APPROVED,
|
||
selected_option_id,
|
||
custom_response,
|
||
user_notes
|
||
)
|
||
|
||
if updated_card:
|
||
# 通知前端确认完成
|
||
await manager.send_to_project(project_id, {
|
||
"type": "confirm_card_processed",
|
||
"data": {
|
||
"card_id": card_id,
|
||
"status": "approved",
|
||
"selected_option": selected_option_id,
|
||
"custom_response": custom_response
|
||
}
|
||
})
|
||
|
||
# 如果有Agent,将用户选择反馈给Agent
|
||
if project_id in manager.project_agents:
|
||
feedback = f"用户已确认卡片 {card_id},选择了选项 {selected_option_id or '自定义回复'}"
|
||
if custom_response:
|
||
feedback += f": {custom_response}"
|
||
|
||
# 可以在这里将feedback传递给agent继续处理
|
||
logger.info(f"用户确认卡片反馈: {feedback}")
|
||
|
||
elif action == "reject":
|
||
# 拒绝卡片
|
||
updated_card = await repo.update_status(
|
||
card_id,
|
||
ConfirmCardStatus.REJECTED,
|
||
user_notes=user_notes
|
||
)
|
||
|
||
await manager.send_to_project(project_id, {
|
||
"type": "confirm_card_processed",
|
||
"data": {
|
||
"card_id": card_id,
|
||
"status": "rejected",
|
||
"user_notes": user_notes
|
||
}
|
||
})
|
||
|
||
logger.info(f"用户对确认卡片执行操作: {card_id}, 操作: {action}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理确认卡片响应失败: {e}")
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {"message": f"操作失败: {str(e)}"}
|
||
})
|
||
|
||
else:
|
||
await websocket.send_json({
|
||
"type": "error",
|
||
"data": {
|
||
"message": f"未知消息类型: {message_type}"
|
||
}
|
||
})
|
||
|
||
|
||
async def _sync_context_states(
|
||
project_id: str,
|
||
episode_number: int,
|
||
memory: Any
|
||
):
|
||
"""同步上下文状态到前端"""
|
||
try:
|
||
# 提取角色状态作为上下文状态
|
||
context_states = []
|
||
|
||
# 添加时间状态
|
||
context_states.append({
|
||
"type": "time",
|
||
"value": f"EP{episode_number} 完成后"
|
||
})
|
||
|
||
# 添加角色状态
|
||
character_states = getattr(memory, 'characterStates', {})
|
||
if isinstance(character_states, dict):
|
||
for char_name, states in character_states.items():
|
||
if states and isinstance(states, list) and len(states) > 0:
|
||
latest_state = states[-1]
|
||
if isinstance(latest_state, dict):
|
||
state_value = latest_state.get('state', f"{char_name}状态")
|
||
else:
|
||
state_value = getattr(latest_state, 'state', f"{char_name}状态")
|
||
|
||
context_states.append({
|
||
"type": "character",
|
||
"value": f"{char_name}: {state_value}",
|
||
"character": char_name,
|
||
"state": state_value
|
||
})
|
||
|
||
# 添加待收线数量
|
||
pending_threads = getattr(memory, 'pendingThreads', [])
|
||
if pending_threads:
|
||
context_states.append({
|
||
"type": "pending_threads",
|
||
"value": f"待收线: {len(pending_threads)} 条"
|
||
})
|
||
|
||
# 广播上下文更新
|
||
await manager.send_to_project(project_id, {
|
||
"type": "context_update",
|
||
"states": context_states,
|
||
"episode_number": episode_number
|
||
})
|
||
|
||
logger.info(f"已同步上下文状态到项目 {project_id}, {len(context_states)} 个状态")
|
||
|
||
except Exception as e:
|
||
logger.error(f"同步上下文状态失败: {str(e)}")
|
||
|
||
|
||
async def _handle_tool_call(project_id: str, event: Dict[str, Any]):
|
||
"""
|
||
处理工具调用,转换为特定的 WebSocket 消息
|
||
|
||
这个函数在 agent.stream_events() 中被调用,当检测到 director 工具调用时,
|
||
会将其转换为前端可以理解的 WebSocket 事件格式。
|
||
"""
|
||
name = event.get("name")
|
||
args = event.get("args", {})
|
||
|
||
# Director 工具处理
|
||
if name == "update_plan":
|
||
await manager.send_to_project(project_id, {
|
||
"type": "plan_update",
|
||
"plan": args.get("steps", []),
|
||
"status": args.get("status", "planning"),
|
||
"current_step_index": args.get("current_step_index", 0)
|
||
})
|
||
|
||
elif name == "add_inbox_task":
|
||
await manager.send_to_project(project_id, {
|
||
"type": "review_request",
|
||
"id": f"task_{args.get('title', 'unknown')}_{int(datetime.now().timestamp())}",
|
||
"title": args.get("title"),
|
||
"description": args.get("description"),
|
||
"options": args.get("options", ["Approve", "Reject"]),
|
||
"timestamp": int(datetime.now().timestamp() * 1000)
|
||
})
|
||
|
||
elif name == "add_annotation":
|
||
await manager.send_to_project(project_id, {
|
||
"type": "annotation_add",
|
||
"annotation": {
|
||
"content": args.get("content"),
|
||
"type": args.get("annotation_type", "review"),
|
||
"suggestion": args.get("suggestion", ""),
|
||
"timestamp": int(datetime.now().timestamp() * 1000)
|
||
}
|
||
})
|
||
|
||
elif name == "update_context":
|
||
# 解析 data (可能是 JSON string 或 dict)
|
||
data = args.get("data")
|
||
context_type = args.get("context_type", "state")
|
||
|
||
try:
|
||
if isinstance(data, str):
|
||
data = json.loads(data)
|
||
|
||
# 转换为前端期望的 activeStates 格式
|
||
if isinstance(data, dict):
|
||
# 将字典转换为 [{type, value}, ...] 格式
|
||
states = [{"type": k, "value": v} for k, v in data.items()]
|
||
elif isinstance(data, list):
|
||
# 已经是列表格式
|
||
states = data
|
||
else:
|
||
states = [{"type": context_type, "value": str(data)}]
|
||
|
||
await manager.send_to_project(project_id, {
|
||
"type": "context_update",
|
||
"states": states
|
||
})
|
||
except Exception as e:
|
||
logger.warning(f"Failed to process context update: {e}")
|
||
|
||
elif name == "write_to_canvas":
|
||
# 新的 write_to_canvas 工具
|
||
content = args.get("content", "")
|
||
if content:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "canvas_update",
|
||
"content": content
|
||
})
|
||
|
||
elif name == "write_file":
|
||
# 如果写入的是当前画布文件,也更新画布
|
||
# 这里简化:只要有内容就更新画布
|
||
content = args.get("content")
|
||
if content:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "canvas_update",
|
||
"content": content
|
||
})
|
||
|
||
elif name == "update_memory":
|
||
# 处理记忆库更新并持久化
|
||
memory_type = args.get("memory_type", "timeline")
|
||
data = args.get("data", {})
|
||
|
||
# 根据记忆类型格式化数据
|
||
memory_data = {
|
||
"type": memory_type,
|
||
"memory_type": memory_type,
|
||
"timestamp": int(datetime.now().timestamp() * 1000)
|
||
}
|
||
|
||
# 添加具体信息
|
||
if memory_type == "timeline":
|
||
memory_data.update({
|
||
"title": data.get("event", "时间线事件"),
|
||
"description": data.get("description", "")
|
||
})
|
||
elif memory_type == "character_state":
|
||
memory_data.update({
|
||
"title": f"{data.get('character', '角色')}状态变化",
|
||
"description": data.get("state", ""),
|
||
"character": data.get("character", "")
|
||
})
|
||
elif memory_type == "pending_thread":
|
||
memory_data.update({
|
||
"title": "待收线问题",
|
||
"description": data.get("description", "")
|
||
})
|
||
elif memory_type == "foreshadowing":
|
||
memory_data.update({
|
||
"title": "伏笔",
|
||
"description": data.get("description", "")
|
||
})
|
||
|
||
# 持久化到项目记忆库
|
||
try:
|
||
from app.db.repositories import project_repo
|
||
project = await project_repo.get(project_id)
|
||
if project and project.memory:
|
||
import copy
|
||
memory_dict = project.memory.dict()
|
||
|
||
if memory_type == "timeline":
|
||
# 添加到时间线
|
||
timeline_event = {
|
||
"episode": data.get("episode", 0),
|
||
"event": data.get("event", ""),
|
||
"description": data.get("description", ""),
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
memory_dict["eventTimeline"].append(timeline_event)
|
||
logger.info(f"Added timeline event: {data.get('event', 'Unknown')}")
|
||
|
||
elif memory_type == "character_state":
|
||
# 更新角色状态
|
||
character = data.get("character", "")
|
||
if character:
|
||
if character not in memory_dict["characterStates"]:
|
||
memory_dict["characterStates"][character] = []
|
||
character_state = {
|
||
"state": data.get("state", ""),
|
||
"description": data.get("description", ""),
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
memory_dict["characterStates"][character].append(character_state)
|
||
logger.info(f"Updated character state: {character} - {data.get('state', '')}")
|
||
|
||
elif memory_type == "pending_thread":
|
||
# 添加待收线
|
||
thread = {
|
||
"description": data.get("description", ""),
|
||
"status": "pending",
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
memory_dict["pendingThreads"].append(thread)
|
||
logger.info(f"Added pending thread: {data.get('description', 'Unknown')}")
|
||
|
||
elif memory_type == "foreshadowing":
|
||
# 添加伏笔
|
||
foreshadow = {
|
||
"description": data.get("description", ""),
|
||
"status": "planted",
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
memory_dict["foreshadowing"].append(foreshadow)
|
||
logger.info(f"Added foreshadowing: {data.get('description', 'Unknown')}")
|
||
|
||
# 保存更新后的记忆库
|
||
await project_repo.update(project_id, {"memory": memory_dict})
|
||
|
||
# 同步上下文状态到前端
|
||
if memory_type in ["character_state", "timeline"]:
|
||
# 获取当前剧集号(如果可能)
|
||
episode_number = data.get("episode", 0)
|
||
await _sync_context_states(project_id, episode_number, project.memory)
|
||
|
||
except Exception as e:
|
||
logger.warning(f"Failed to persist memory update: {e}")
|
||
|
||
await manager.send_to_project(project_id, {
|
||
"type": "memory_update",
|
||
"data": memory_data
|
||
})
|
||
|
||
elif name == "save_episode":
|
||
# 处理剧集保存并持久化
|
||
episode_number = args.get("episode_number")
|
||
title = args.get("title")
|
||
content = args.get("content")
|
||
outline = args.get("outline")
|
||
|
||
if episode_number:
|
||
try:
|
||
from app.db.repositories import episode_repo, project_repo
|
||
from app.core.memory.memory_manager import get_memory_manager
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = next((ep for ep in episodes if ep.number == episode_number), None)
|
||
|
||
if episode:
|
||
if title: episode.title = title
|
||
if content: episode.content = content
|
||
if outline: episode.outline = outline
|
||
episode.status = "completed"
|
||
await episode_repo.update(episode)
|
||
logger.info(f"Persisted episode {episode_number} via save_episode")
|
||
|
||
# 自动更新记忆库
|
||
try:
|
||
project = await project_repo.get(project_id)
|
||
if project and episode.content:
|
||
memory_manager = get_memory_manager()
|
||
await memory_manager.update_memory_from_episode(project, episode)
|
||
logger.info(f"Updated memory after saving episode {episode_number}")
|
||
|
||
# 同步上下文状态到前端
|
||
await _sync_context_states(project_id, episode_number, project.memory)
|
||
except Exception as memory_error:
|
||
logger.warning(f"Failed to update memory for episode {episode_number}: {memory_error}")
|
||
|
||
# 广播更新
|
||
await manager.send_to_project(project_id, {
|
||
"type": "episode_updated",
|
||
"data": {
|
||
"number": episode_number,
|
||
"title": episode.title,
|
||
"status": episode.status
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"Failed to persist episode via save_episode: {e}")
|
||
|
||
await manager.send_to_project(project_id, {
|
||
"type": "episode_saved",
|
||
"episode_number": episode_number,
|
||
"title": title
|
||
})
|
||
|
||
elif name == "get_episode_data":
|
||
# 处理剧集数据查询 - 直接返回给 Agent 使用
|
||
episode_number = args.get("episode_number")
|
||
include_outline = args.get("include_outline", True)
|
||
include_content = args.get("include_content", True)
|
||
|
||
# 如果没有指定剧集号,使用当前焦点剧集
|
||
if episode_number is None:
|
||
agent = manager.get_agent(project_id, project_dir)
|
||
episode_number = getattr(agent.context, 'active_episode_number', None) if agent and agent.context else None
|
||
|
||
if episode_number:
|
||
try:
|
||
from app.db.repositories import episode_repo
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = next((ep for ep in episodes if ep.number == episode_number), None)
|
||
|
||
if episode:
|
||
# 将数据格式化为 Agent 可读的格式
|
||
formatted_response = f"""
|
||
【剧集数据 - EP{episode_number}】
|
||
标题: {episode.title}
|
||
状态: {episode.status}
|
||
"""
|
||
if include_outline and episode.outline:
|
||
formatted_response += f"\n【大纲】\n{episode.outline}\n"
|
||
if include_content and episode.content:
|
||
content_preview = episode.content[:2000]
|
||
if len(episode.content) > 2000:
|
||
content_preview += "\n... (内容过长,已截取前2000字)"
|
||
formatted_response += f"\n【内容】\n{content_preview}\n"
|
||
|
||
# 发送给前端(用于调试/UI显示)
|
||
await manager.send_to_project(project_id, {
|
||
"type": "episode_data_loaded",
|
||
"data": {
|
||
"episode_number": episode_number,
|
||
"title": episode.title,
|
||
"status": episode.status,
|
||
"outline": episode.outline if include_outline else None,
|
||
"content": episode.content if include_content else None
|
||
}
|
||
})
|
||
|
||
# 将数据发送给 Agent(通过直接返回,由 stream_events 处理)
|
||
return formatted_response
|
||
else:
|
||
return f"Error: Episode {episode_number} not found in project."
|
||
except Exception as e:
|
||
logger.error(f"Failed to get episode data: {e}")
|
||
return f"Error: Failed to get episode data: {str(e)}"
|
||
else:
|
||
return "Error: No episode number specified and no episode is currently focused."
|
||
|
||
elif name == "update_episode":
|
||
# 处理剧集更新并持久化
|
||
episode_number = args.get("episode_number")
|
||
title = args.get("title")
|
||
content = args.get("content")
|
||
status = args.get("status")
|
||
outline = args.get("outline")
|
||
|
||
if episode_number:
|
||
try:
|
||
from app.db.repositories import episode_repo, project_repo
|
||
from app.core.memory.memory_manager import get_memory_manager
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = next((ep for ep in episodes if ep.number == episode_number), None)
|
||
|
||
if episode:
|
||
if title is not None: episode.title = title
|
||
if content is not None: episode.content = content
|
||
if status is not None: episode.status = status
|
||
if outline is not None: episode.outline = outline
|
||
|
||
await episode_repo.update(episode)
|
||
logger.info(f"Updated episode {episode_number} via update_episode")
|
||
|
||
# 如果有新内容,自动更新记忆库
|
||
if content and content.strip():
|
||
try:
|
||
project = await project_repo.get(project_id)
|
||
if project:
|
||
memory_manager = get_memory_manager()
|
||
await memory_manager.update_memory_from_episode(project, episode)
|
||
logger.info(f"Updated memory after updating episode {episode_number}")
|
||
|
||
# 同步上下文状态到前端
|
||
await _sync_context_states(project_id, episode_number, project.memory)
|
||
except Exception as memory_error:
|
||
logger.warning(f"Failed to update memory for episode {episode_number}: {memory_error}")
|
||
|
||
# 广播更新
|
||
await manager.send_to_project(project_id, {
|
||
"type": "episode_updated",
|
||
"data": {
|
||
"number": episode_number,
|
||
"title": episode.title,
|
||
"status": episode.status
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"Failed to update episode: {e}")
|
||
|
||
elif name == "focus_episode":
|
||
# 处理剧集焦点切换
|
||
episode_number = args.get("episode_number")
|
||
title = args.get("title")
|
||
|
||
await manager.send_to_project(project_id, {
|
||
"type": "focus_update",
|
||
"episodeNumber": episode_number,
|
||
"episodeTitle": title
|
||
})
|
||
|
||
elif name == "request_review":
|
||
# 处理审核请求并调用审核管理器
|
||
content = args.get("content", "")
|
||
review_type = args.get("review_type", "quality")
|
||
criteria = args.get("criteria", [])
|
||
|
||
if content:
|
||
try:
|
||
from app.db.repositories import project_repo, episode_repo
|
||
from app.core.review.review_manager import get_review_manager
|
||
from app.models.project import ReviewConfig
|
||
|
||
# 获取项目
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
logger.warning(f"Project {project_id} not found for review")
|
||
else:
|
||
# 尝试找到当前剧集(如果存在)
|
||
episodes = await episode_repo.list_by_project(project_id)
|
||
episode = None
|
||
|
||
# 如果项目有当前剧集焦点,使用它
|
||
from app.core.agent_runtime.director_agent import DirectorAgent
|
||
agent = manager.get_agent(project_id, project_dir)
|
||
if agent and agent.context and agent.context.active_episode_number:
|
||
episode = next((ep for ep in episodes if ep.number == agent.context.active_episode_number), None)
|
||
|
||
# 如果没有找到剧集,创建临时剧集对象用于审核
|
||
if not episode:
|
||
from app.models.project import Episode
|
||
episode = Episode(
|
||
projectId=project_id,
|
||
number=agent.context.active_episode_number if agent and agent.context else 1,
|
||
title="Content Review",
|
||
content=content,
|
||
status="draft"
|
||
)
|
||
|
||
# 获取审核配置(使用项目配置或默认配置)
|
||
review_config = ReviewConfig(
|
||
enabled=True,
|
||
dimensions=["quality", "consistency", "plot"] if not criteria else criteria,
|
||
strictness=0.7,
|
||
autoApprove=False,
|
||
requireHumanReview=True
|
||
)
|
||
|
||
# 调用审核管理器
|
||
review_manager = get_review_manager()
|
||
review_result = await review_manager.review_episode(
|
||
project=project,
|
||
episode=episode,
|
||
config=review_config
|
||
)
|
||
|
||
# 发送审核结果到前端
|
||
await manager.send_to_project(project_id, {
|
||
"type": "review_completed",
|
||
"data": {
|
||
"score": review_result.score,
|
||
"passed": review_result.passed,
|
||
"issue_count": review_result.issue_count,
|
||
"issues": [
|
||
{
|
||
"dimension": issue.dimension,
|
||
"severity": issue.severity,
|
||
"description": issue.description,
|
||
"location": issue.location,
|
||
"suggestion": issue.suggestion
|
||
}
|
||
for issue in review_result.issues
|
||
]
|
||
}
|
||
})
|
||
|
||
logger.info(f"Review completed for project {project_id}: score={review_result.score}, passed={review_result.passed}")
|
||
except Exception as e:
|
||
logger.error(f"Failed to execute review: {e}")
|
||
# 发送错误消息
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {"message": f"审核执行失败: {str(e)}"}
|
||
})
|
||
|
||
# 同时发送原始的 review_request 消息(用于 UI 显示)
|
||
await manager.send_to_project(project_id, {
|
||
"type": "review_request",
|
||
"id": f"review_{int(datetime.now().timestamp())}",
|
||
"title": f"{review_type.capitalize()} Review",
|
||
"description": content[:500] + "..." if len(content) > 500 else content,
|
||
"criteria": criteria,
|
||
"options": ["Approve", "Request Changes"],
|
||
"timestamp": int(datetime.now().timestamp() * 1000)
|
||
})
|
||
|
||
elif name == "create_episode":
|
||
# 处理剧集创作请求
|
||
episode_number = args.get("episode_number")
|
||
analyze_previous_memory = args.get("analyze_previous_memory", True)
|
||
|
||
if episode_number:
|
||
# 使用 ensure_future 确保任务在后台执行,即使 WebSocket 断开也能继续
|
||
asyncio.ensure_future(_execute_episode_creation(
|
||
project_id, episode_number, analyze_previous_memory
|
||
))
|
||
logger.info(f"已启动后台创作任务: EP{episode_number}")
|
||
|
||
elif name == "plan_episode_review":
|
||
# 处理剧集审查规划请求
|
||
episode_selector = args.get("episode_selector")
|
||
review_criteria = args.get("review_criteria")
|
||
review_dimensions = args.get("review_dimensions")
|
||
|
||
if episode_selector:
|
||
# 启动后台审查任务
|
||
asyncio.ensure_future(_execute_episode_review(
|
||
project_id, episode_selector, review_criteria, review_dimensions
|
||
))
|
||
logger.info(f"已启动后台审查任务: {episode_selector}")
|
||
|
||
# ============================================
|
||
# 辅助函数 - 用于从其他模块发送消息
|
||
# ============================================
|
||
|
||
|
||
# 全局后台任务跟踪
|
||
_background_tasks: Dict[str, asyncio.Task] = {}
|
||
|
||
|
||
async def _execute_episode_creation(
|
||
project_id: str,
|
||
episode_number: int,
|
||
analyze_previous_memory: bool
|
||
):
|
||
"""
|
||
异步执行剧集创作
|
||
|
||
这个函数在后台执行,不会阻塞 WebSocket 连接。
|
||
即使 WebSocket 断开,任务也会继续执行并保存到数据库。
|
||
|
||
它会:
|
||
1. 分析上一集的记忆(如果需要)
|
||
2. 执行剧集创作
|
||
3. 通过 WebSocket 发送进度更新(如果连接存在)
|
||
4. 将创作内容发送到画布(如果连接存在)
|
||
5. 无论如何都保存到数据库
|
||
"""
|
||
task_key = f"{project_id}_{episode_number}"
|
||
|
||
try:
|
||
from app.db.repositories import project_repo, episode_repo
|
||
from app.core.agents.series_creation_agent import get_series_agent
|
||
from app.core.memory.memory_manager import get_memory_manager
|
||
|
||
# 注册到后台任务字典
|
||
current_task = asyncio.current_task()
|
||
if current_task:
|
||
_background_tasks[task_key] = current_task
|
||
|
||
logger.info(f"开始后台创作任务: {task_key}")
|
||
|
||
# 辅助函数:安全发送 WebSocket 消息(忽略连接错误)
|
||
async def safe_send(message_type: str, data: dict = None):
|
||
try:
|
||
if data is None:
|
||
data = {}
|
||
await manager.send_to_project(project_id, {
|
||
"type": message_type,
|
||
"data": data
|
||
})
|
||
except Exception as e:
|
||
# WebSocket 可能已断开,忽略错误继续执行
|
||
logger.debug(f"WebSocket 发送失败(可能已断开): {e}")
|
||
|
||
# 获取项目
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
await safe_send("error", {"message": f"项目不存在: {project_id}"})
|
||
return
|
||
|
||
# 更新计划状态 - 开始
|
||
await safe_send("plan_update", {
|
||
"plan": [
|
||
f"分析 EP{episode_number - 1 if episode_number > 1 else 'N/A'} 的记忆系统" if analyze_previous_memory and episode_number > 1 else "跳过记忆分析(首集)",
|
||
f"生成 EP{episode_number} 大纲",
|
||
f"创作 EP{episode_number} 对话内容",
|
||
f"执行质量审核",
|
||
f"更新记忆系统"
|
||
],
|
||
"status": "planning",
|
||
"current_step_index": 0
|
||
})
|
||
|
||
# 步骤 1: 分析上一集记忆(如果需要)
|
||
if analyze_previous_memory and episode_number > 1:
|
||
await safe_send("plan_update", {
|
||
"plan": [
|
||
f"分析 EP{episode_number - 1} 的记忆系统",
|
||
f"生成 EP{episode_number} 大纲",
|
||
f"创作 EP{episode_number} 对话内容",
|
||
f"执行质量审核",
|
||
f"更新记忆系统"
|
||
],
|
||
"status": "planning",
|
||
"current_step_index": 0
|
||
})
|
||
|
||
# 获取上一集内容
|
||
prev_episodes = await episode_repo.list_by_project(project_id)
|
||
prev_episode = next((ep for ep in prev_episodes if ep.number == episode_number - 1), None)
|
||
|
||
if prev_episode and prev_episode.content:
|
||
await safe_send("text", {"content": f"\n\n--- 正在分析 EP{episode_number - 1} 的记忆系统 ---\n"})
|
||
|
||
# 使用 MemoryManager 更新记忆
|
||
try:
|
||
memory_manager = get_memory_manager()
|
||
await memory_manager.update_memory_from_episode(project, prev_episode)
|
||
logger.info(f"EP{episode_number - 1} 记忆已分析并注入到 EP{episode_number}")
|
||
except Exception as e:
|
||
logger.warning(f"分析 EP{episode_number - 1} 记忆失败: {e}")
|
||
|
||
# 步骤 2-5: 执行剧集创作
|
||
agent = get_series_agent()
|
||
|
||
await safe_send("plan_update", {
|
||
"plan": [
|
||
f"分析 EP{episode_number - 1} 的记忆系统" if analyze_previous_memory and episode_number > 1 else "跳过记忆分析",
|
||
f"生成 EP{episode_number} 大纲",
|
||
f"创作 EP{episode_number} 对话内容",
|
||
f"执行质量审核",
|
||
f"更新记忆系统"
|
||
],
|
||
"status": "writing",
|
||
"current_step_index": 1
|
||
})
|
||
|
||
await safe_send("text", {"content": f"\n\n--- 开始创作 EP{episode_number} ---\n"})
|
||
|
||
# 执行创作
|
||
episode = await agent.execute_episode(
|
||
project=project,
|
||
episode_number=episode_number,
|
||
title=f"第{episode_number}集"
|
||
)
|
||
|
||
# 检查是否创作成功(错误处理)
|
||
if episode.status == "needs-review" and not episode.content:
|
||
# 创作失败,没有内容
|
||
await safe_send("error", {
|
||
"message": f"EP{episode_number} 创作失败",
|
||
"episode_number": episode_number
|
||
})
|
||
await safe_send("text", {"content": f"\n\n❌ EP{episode_number} 创作失败。请检查错误日志并重试。\n"})
|
||
logger.error(f"EP{episode_number} 创作失败,无内容生成")
|
||
return
|
||
|
||
# 创作成功,保存到数据库(无论 WebSocket 是否连接)
|
||
existing_episodes = await episode_repo.list_by_project(project_id)
|
||
episode_record = next((ep for ep in existing_episodes if ep.number == episode_number), None)
|
||
|
||
if episode_record:
|
||
episode.id = episode_record.id
|
||
episode.projectId = project_id
|
||
await episode_repo.update(episode)
|
||
logger.info(f"更新现有剧集记录: {episode.id}")
|
||
else:
|
||
episode.projectId = project_id
|
||
await episode_repo.create(episode)
|
||
logger.info(f"创建新剧集记录: {episode.id}")
|
||
|
||
# 发送内容到画布
|
||
if episode.content:
|
||
await safe_send("canvas_update", {"content": episode.content})
|
||
|
||
# 更新记忆
|
||
await project_repo.update(project_id, {
|
||
"memory": project.memory.dict()
|
||
})
|
||
|
||
# 完成消息
|
||
await safe_send("plan_update", {
|
||
"plan": [
|
||
f"分析 EP{episode_number - 1} 的记忆系统" if analyze_previous_memory and episode_number > 1 else "跳过记忆分析",
|
||
f"生成 EP{episode_number} 大纲",
|
||
f"创作 EP{episode_number} 对话内容",
|
||
f"执行质量审核",
|
||
f"更新记忆系统"
|
||
],
|
||
"status": "idle",
|
||
"current_step_index": 4
|
||
})
|
||
|
||
await safe_send("text", {"content": f"\n\n✅ EP{episode_number} 创作完成!质量分数: {episode.qualityScore or 0}\n"})
|
||
|
||
# 推送审核卡片到对话框
|
||
if episode.issues and len(episode.issues) > 0:
|
||
high_severity_count = sum(1 for issue in episode.issues if getattr(issue, 'severity', 'low') == 'high')
|
||
await safe_send("review_complete", {
|
||
"data": {
|
||
"episode_number": episode_number,
|
||
"overall_score": episode.qualityScore or 0,
|
||
"passed": episode.status != "needs-review",
|
||
"high_severity_count": high_severity_count,
|
||
"issues": episode.issues
|
||
}
|
||
})
|
||
|
||
# 广播更新
|
||
await safe_send("episode_updated", {
|
||
"number": episode_number,
|
||
"title": episode.title,
|
||
"status": episode.status
|
||
})
|
||
|
||
logger.info(f"EP{episode_number} 后台创作完成,已保存到数据库")
|
||
|
||
except Exception as e:
|
||
logger.error(f"执行剧集创作失败: {str(e)}", exc_info=True)
|
||
try:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {
|
||
"message": f"EP{episode_number} 创作失败: {str(e)}",
|
||
"episode_number": episode_number
|
||
}
|
||
})
|
||
await manager.send_to_project(project_id, {
|
||
"type": "text",
|
||
"data": {"content": f"\n\n❌ EP{episode_number} 创作失败: {str(e)}\n"}
|
||
})
|
||
except Exception:
|
||
# WebSocket 可能已断开,忽略错误
|
||
pass
|
||
finally:
|
||
# 清理任务跟踪
|
||
if task_key in _background_tasks:
|
||
del _background_tasks[task_key]
|
||
logger.info(f"后台创作任务结束: {task_key}")
|
||
|
||
|
||
async def _execute_episode_review(
|
||
project_id: str,
|
||
episode_selector: str,
|
||
review_criteria: Optional[List[str]] = None,
|
||
review_dimensions: Optional[List[str]] = None
|
||
):
|
||
"""
|
||
异步执行剧集审查
|
||
|
||
这个函数在后台执行剧集审查任务:
|
||
1. 解析剧集选择器
|
||
2. 获取剧集数据
|
||
3. 执行多维审查
|
||
4. 生成审查报告
|
||
5. 推送到审核系统
|
||
"""
|
||
task_key = f"{project_id}_review_{episode_selector}"
|
||
|
||
try:
|
||
from app.db.repositories import project_repo, episode_repo
|
||
from app.core.review.review_manager import get_review_manager
|
||
from app.core.review.review_task_manager import get_review_task_manager
|
||
from app.models.project import ReviewConfig
|
||
|
||
# 注册到后台任务字典
|
||
current_task = asyncio.current_task()
|
||
if current_task:
|
||
_background_tasks[task_key] = current_task
|
||
|
||
logger.info(f"开始后台审查任务: {task_key}")
|
||
|
||
# 辅助函数:安全发送 WebSocket 消息
|
||
async def safe_send(message_type: str, data: dict = None):
|
||
try:
|
||
if data is None:
|
||
data = {}
|
||
await manager.send_to_project(project_id, {
|
||
"type": message_type,
|
||
"data": data
|
||
})
|
||
except Exception as e:
|
||
logger.debug(f"WebSocket 发送失败(可能已断开): {e}")
|
||
|
||
# 获取项目
|
||
project = await project_repo.get(project_id)
|
||
if not project:
|
||
await safe_send("error", {"message": f"项目不存在: {project_id}"})
|
||
return
|
||
|
||
# 解析剧集选择器
|
||
agent = manager.get_agent(project_id, Path(f"d:/platform/creative_studio/workspace/projects/{project_id}"))
|
||
episodes_to_review = _parse_episode_selector_for_review(episode_selector, agent)
|
||
|
||
if not episodes_to_review:
|
||
await safe_send("error", {"message": f"无效的剧集选择器: {episode_selector}"})
|
||
return
|
||
|
||
# 更新计划状态 - 开始
|
||
await safe_send("plan_update", {
|
||
"plan": [
|
||
f"获取剧集数据: {len(episodes_to_review)} 集",
|
||
f"执行审查: {', '.join(review_dimensions or ['quality', 'consistency', 'plot'])}",
|
||
"生成审查报告",
|
||
"推送到审核系统"
|
||
],
|
||
"status": "reviewing",
|
||
"current_step_index": 0
|
||
})
|
||
|
||
await safe_send("text", {"content": f"\n\n--- 开始审查 {len(episodes_to_review)} 集内容 ---\n"})
|
||
|
||
# 获取所有剧集数据
|
||
all_episodes = await episode_repo.list_by_project(project_id)
|
||
review_results = []
|
||
|
||
for idx, episode_number in enumerate(episodes_to_review):
|
||
episode = next((ep for ep in all_episodes if ep.number == episode_number), None)
|
||
if not episode:
|
||
logger.warning(f"EP{episode_number} 不存在,跳过")
|
||
continue
|
||
|
||
await safe_send("text", {"content": f"\n正在审查 EP{episode_number}: {episode.title}...\n"})
|
||
|
||
# 执行审查
|
||
try:
|
||
review_config = ReviewConfig(
|
||
enabled=True,
|
||
dimensions=review_dimensions or ["quality", "consistency", "plot"],
|
||
strictness=0.7,
|
||
autoApprove=False,
|
||
requireHumanReview=True
|
||
)
|
||
|
||
review_manager = get_review_manager()
|
||
review_result = await review_manager.review_episode(
|
||
project=project,
|
||
episode=episode,
|
||
config=review_config
|
||
)
|
||
|
||
review_results.append({
|
||
"episode_number": episode_number,
|
||
"episode_title": episode.title,
|
||
"score": review_result.score,
|
||
"passed": review_result.passed,
|
||
"issue_count": review_result.issue_count,
|
||
"issues": [
|
||
{
|
||
"dimension": issue.dimension,
|
||
"severity": issue.severity,
|
||
"description": issue.description,
|
||
"location": issue.location,
|
||
"suggestion": issue.suggestion
|
||
}
|
||
for issue in review_result.issues
|
||
]
|
||
})
|
||
|
||
# 发送单个剧集的审查结果
|
||
await safe_send("episode_review_completed", {
|
||
"episode_number": episode_number,
|
||
"score": review_result.score,
|
||
"passed": review_result.passed,
|
||
"issue_count": review_result.issue_count
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"审查 EP{episode_number} 失败: {e}")
|
||
await safe_send("error", {"message": f"审查 EP{episode_number} 失败: {str(e)}"})
|
||
|
||
# 更新计划状态 - 完成
|
||
await safe_send("plan_update", {
|
||
"plan": [
|
||
f"获取剧集数据: {len(episodes_to_review)} 集",
|
||
f"执行审查: {', '.join(review_dimensions or ['quality', 'consistency', 'plot'])}",
|
||
"生成审查报告",
|
||
"推送到审核系统"
|
||
],
|
||
"status": "idle",
|
||
"current_step_index": 3
|
||
})
|
||
|
||
# 推送到审核系统
|
||
review_task_manager = get_review_task_manager()
|
||
try:
|
||
for result in review_results:
|
||
if result["issue_count"] > 0:
|
||
# 创建审核任务
|
||
await review_task_manager.create_task_from_review(
|
||
project_id=project_id,
|
||
episode_number=result["episode_number"],
|
||
review_data=result
|
||
)
|
||
logger.info(f"已将 EP{result['episode_number']} 的审查结果推送到审核系统")
|
||
|
||
await safe_send("text", {"content": f"\n\n✅ 审查完成!共审查 {len(review_results)} 集,发现问题的剧集已推送到审核系统。\n"})
|
||
await safe_send("review_tasks_created", {
|
||
"total_episodes": len(review_results),
|
||
"episodes_with_issues": sum(1 for r in review_results if r["issue_count"] > 0),
|
||
"review_results": review_results
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"推送审核系统失败: {e}")
|
||
await safe_send("error", {"message": f"推送审核系统失败: {str(e)}"})
|
||
|
||
logger.info(f"EP{episode_selector} 后台审查完成")
|
||
|
||
except Exception as e:
|
||
logger.error(f"执行剧集审查失败: {str(e)}", exc_info=True)
|
||
try:
|
||
await manager.send_to_project(project_id, {
|
||
"type": "error",
|
||
"data": {
|
||
"message": f"审查失败: {str(e)}",
|
||
"episode_selector": episode_selector
|
||
}
|
||
})
|
||
except Exception:
|
||
pass
|
||
finally:
|
||
# 清理任务跟踪
|
||
if task_key in _background_tasks:
|
||
del _background_tasks[task_key]
|
||
logger.info(f"后台审查任务结束: {task_key}")
|
||
|
||
|
||
def _parse_episode_selector_for_review(selector: str, agent) -> List[int]:
|
||
"""解析剧集选择器,用于审查任务"""
|
||
selector_lower = selector.lower().strip()
|
||
|
||
# 获取所有剧集号
|
||
episodes = getattr(agent.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 []
|
||
|
||
|
||
# ============================================
|
||
# 辅助函数 - 用于从其他模块发送消息
|
||
# ============================================
|
||
|
||
async def broadcast_stage_update(
|
||
project_id: str,
|
||
episode_number: int,
|
||
stage: str,
|
||
data: Dict[str, Any]
|
||
):
|
||
message = {
|
||
"type": "stage_update",
|
||
"data": {
|
||
"project_id": project_id,
|
||
"episode_number": episode_number,
|
||
"stage": stage,
|
||
**data
|
||
},
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
await manager.send_to_project(project_id, message)
|
||
|
||
|
||
async def broadcast_episode_complete(
|
||
project_id: str,
|
||
episode_number: int,
|
||
success: bool,
|
||
quality_score: float,
|
||
data: Dict[str, Any]
|
||
):
|
||
message = {
|
||
"type": "episode_complete",
|
||
"data": {
|
||
"project_id": project_id,
|
||
"episode_number": episode_number,
|
||
"success": success,
|
||
"quality_score": quality_score,
|
||
**data
|
||
},
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
await manager.send_to_project(project_id, message)
|
||
|
||
|
||
async def broadcast_batch_progress(
|
||
batch_id: str,
|
||
current_episode: int,
|
||
total_episodes: int,
|
||
completed: int,
|
||
failed: int,
|
||
data: Dict[str, Any]
|
||
):
|
||
message = {
|
||
"type": "batch_progress",
|
||
"data": {
|
||
"batch_id": batch_id,
|
||
"current_episode": current_episode,
|
||
"total_episodes": total_episodes,
|
||
"completed_episodes": completed,
|
||
"failed_episodes": failed,
|
||
"progress_percentage": (completed / total_episodes * 100) if total_episodes > 0 else 0,
|
||
**data
|
||
},
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
await manager.send_to_batch(batch_id, message)
|
||
|
||
|
||
async def broadcast_error(
|
||
project_id: str,
|
||
episode_number: Optional[int],
|
||
error: str,
|
||
error_type: str = "execution_error"
|
||
):
|
||
message = {
|
||
"type": "error",
|
||
"data": {
|
||
"project_id": project_id,
|
||
"episode_number": episode_number,
|
||
"error": error,
|
||
"error_type": error_type
|
||
},
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
await manager.send_to_project(project_id, message)
|
||
|
||
|
||
async def broadcast_batch_complete(
|
||
batch_id: str,
|
||
summary: Dict[str, Any]
|
||
):
|
||
message = {
|
||
"type": "batch_complete",
|
||
"data": {
|
||
"batch_id": batch_id,
|
||
**summary
|
||
},
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
await manager.send_to_batch(batch_id, message)
|
||
|
||
|
||
async def broadcast_to_project(
|
||
project_id: str,
|
||
message_type: str,
|
||
data: Dict[str, Any]
|
||
):
|
||
"""向项目的所有连接广播消息"""
|
||
message = {
|
||
"type": message_type,
|
||
"data": data,
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
await manager.send_to_project(project_id, message)
|
||
|
||
|
||
async def broadcast_to_project(
|
||
project_id: str,
|
||
message_type: str,
|
||
data: Dict[str, Any]
|
||
):
|
||
"""向项目的所有连接广播消息"""
|
||
message = {
|
||
"type": message_type,
|
||
"data": data,
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
await manager.send_to_project(project_id, message)
|
||
|
||
|
||
async def broadcast_review_card_created(
|
||
project_id: str,
|
||
card_id: str,
|
||
card_data: Dict[str, Any]
|
||
):
|
||
"""广播审核卡片创建通知"""
|
||
await manager.send_to_project(project_id, {
|
||
"type": "review_card_created",
|
||
"data": {
|
||
"card_id": card_id,
|
||
"card_type": card_data.get("card_type"),
|
||
"episode_numbers": card_data.get("episode_numbers", []),
|
||
"severity": card_data.get("severity"),
|
||
"review_reason": card_data.get("review_reason"),
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
})
|
||
|
||
|
||
async def broadcast_confirm_card_created(
|
||
project_id: str,
|
||
card_id: str,
|
||
card_data: Dict[str, Any]
|
||
):
|
||
"""广播确认卡片创建通知"""
|
||
await manager.send_to_project(project_id, {
|
||
"type": "confirm_card_created",
|
||
"data": {
|
||
"card_id": card_id,
|
||
"card_type": card_data.get("card_type"),
|
||
"title": card_data.get("title"),
|
||
"description": card_data.get("description", "")[:200],
|
||
"options": card_data.get("options", []),
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
})
|
||
|
||
|
||
# 导出连接管理器和辅助函数
|
||
__all__ = [
|
||
"manager",
|
||
"broadcast_stage_update",
|
||
"broadcast_episode_complete",
|
||
"broadcast_batch_progress",
|
||
"broadcast_error",
|
||
"broadcast_batch_complete",
|
||
"broadcast_to_project",
|
||
"broadcast_review_card_created",
|
||
"broadcast_confirm_card_created"
|
||
]
|