feat:优化开发剧集创作部分
This commit is contained in:
parent
c99f66895b
commit
5b0f8833ba
@ -37,9 +37,25 @@ router = APIRouter(prefix="/projects", tags=["项目管理"])
|
|||||||
|
|
||||||
@router.post("/", response_model=SeriesProject, status_code=status.HTTP_201_CREATED)
|
@router.post("/", response_model=SeriesProject, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_project(project_data: SeriesProjectCreate):
|
async def create_project(project_data: SeriesProjectCreate):
|
||||||
"""创建新项目"""
|
"""创建新项目并自动生成剧集记录"""
|
||||||
try:
|
try:
|
||||||
|
# 创建项目
|
||||||
project = await project_repo.create(project_data)
|
project = await project_repo.create(project_data)
|
||||||
|
|
||||||
|
# 自动创建剧集记录(状态为 pending)
|
||||||
|
import uuid
|
||||||
|
for episode_num in range(1, project.totalEpisodes + 1):
|
||||||
|
episode = Episode(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
projectId=project.id,
|
||||||
|
number=episode_num,
|
||||||
|
title=f"第{episode_num}集",
|
||||||
|
status="pending",
|
||||||
|
content="" # 初始化为空白
|
||||||
|
)
|
||||||
|
await episode_repo.create(episode)
|
||||||
|
logger.info(f"自动创建剧集: {episode.id} - EP{episode_num}")
|
||||||
|
|
||||||
return project
|
return project
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"创建项目失败: {str(e)}")
|
logger.error(f"创建项目失败: {str(e)}")
|
||||||
@ -98,7 +114,7 @@ async def delete_project(project_id: str):
|
|||||||
|
|
||||||
@router.get("/{project_id}/episodes", response_model=List[Episode])
|
@router.get("/{project_id}/episodes", response_model=List[Episode])
|
||||||
async def list_episodes(project_id: str):
|
async def list_episodes(project_id: str):
|
||||||
"""列出项目的所有剧集"""
|
"""列出项目的所有剧集,如果为空则自动初始化"""
|
||||||
# 先验证项目存在
|
# 先验证项目存在
|
||||||
project = await project_repo.get(project_id)
|
project = await project_repo.get(project_id)
|
||||||
if not project:
|
if not project:
|
||||||
@ -108,6 +124,29 @@ async def list_episodes(project_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
episodes = await episode_repo.list_by_project(project_id)
|
episodes = await episode_repo.list_by_project(project_id)
|
||||||
|
|
||||||
|
# 如果剧集列表为空,自动初始化剧集记录
|
||||||
|
if not episodes and project.totalEpisodes:
|
||||||
|
import uuid
|
||||||
|
logger.info(f"项目 {project_id} 暂无剧集记录,自动初始化 {project.totalEpisodes} 集...")
|
||||||
|
for episode_num in range(1, project.totalEpisodes + 1):
|
||||||
|
# 再次检查,防止并发或逻辑重复
|
||||||
|
existing = next((ep for ep in episodes if ep.number == episode_num), None)
|
||||||
|
if existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
episode = Episode(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
projectId=project_id,
|
||||||
|
number=episode_num,
|
||||||
|
title=f"第{episode_num}集",
|
||||||
|
status="pending",
|
||||||
|
content="" # 初始化为空白,避免触发前端生成大纲按钮
|
||||||
|
)
|
||||||
|
await episode_repo.create(episode)
|
||||||
|
# 重新获取列表
|
||||||
|
episodes = await episode_repo.list_by_project(project_id)
|
||||||
|
|
||||||
return episodes
|
return episodes
|
||||||
|
|
||||||
|
|
||||||
@ -125,6 +164,42 @@ async def get_episode(project_id: str, episode_number: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{project_id}/episodes/{episode_number}", response_model=Episode)
|
||||||
|
async def update_episode(project_id: str, episode_number: int, update_data: dict):
|
||||||
|
"""更新指定集数的内容"""
|
||||||
|
episodes = await episode_repo.list_by_project(project_id)
|
||||||
|
episode = next((ep for ep in episodes if ep.number == episode_number), None)
|
||||||
|
|
||||||
|
if not episode:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"剧集不存在: EP{episode_number}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新允许的字段
|
||||||
|
if "title" in update_data:
|
||||||
|
episode.title = update_data["title"]
|
||||||
|
if "content" in update_data:
|
||||||
|
episode.content = update_data["content"]
|
||||||
|
if "outline" in update_data:
|
||||||
|
episode.outline = update_data["outline"]
|
||||||
|
if "summary" in update_data:
|
||||||
|
episode.summary = update_data["summary"]
|
||||||
|
if "status" in update_data:
|
||||||
|
episode.status = update_data["status"]
|
||||||
|
|
||||||
|
# 如果状态变为完成,设置完成时间
|
||||||
|
if episode.status == "completed" and not episode.completedAt:
|
||||||
|
from datetime import datetime
|
||||||
|
episode.completedAt = datetime.now()
|
||||||
|
|
||||||
|
# 保存更新
|
||||||
|
await episode_repo.update(episode)
|
||||||
|
logger.info(f"更新剧集: {episode.id} - EP{episode_number}")
|
||||||
|
|
||||||
|
return episode
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 剧集执行(核心功能)
|
# 剧集执行(核心功能)
|
||||||
# ============================================
|
# ============================================
|
||||||
@ -153,15 +228,27 @@ async def execute_episode(
|
|||||||
|
|
||||||
# 执行创作
|
# 执行创作
|
||||||
logger.info(f"开始执行创作: 项目 {project_id}, EP{request.episodeNumber}")
|
logger.info(f"开始执行创作: 项目 {project_id}, EP{request.episodeNumber}")
|
||||||
|
|
||||||
|
# 先获取现有剧集记录,避免重复创建
|
||||||
|
existing_episodes = await episode_repo.list_by_project(project_id)
|
||||||
|
episode_record = next((ep for ep in existing_episodes if ep.number == request.episodeNumber), None)
|
||||||
|
|
||||||
episode = await agent.execute_episode(
|
episode = await agent.execute_episode(
|
||||||
project=project,
|
project=project,
|
||||||
episode_number=request.episodeNumber,
|
episode_number=request.episodeNumber,
|
||||||
title=request.title
|
title=request.title
|
||||||
)
|
)
|
||||||
|
|
||||||
# 保存剧集
|
# 保持原有的 ID 如果记录已存在
|
||||||
episode.projectId = project_id
|
if episode_record:
|
||||||
await episode_repo.create(episode)
|
episode.id = episode_record.id
|
||||||
|
episode.projectId = project_id
|
||||||
|
await episode_repo.update(episode)
|
||||||
|
logger.info(f"更新现有剧集记录: {episode.id} - EP{request.episodeNumber}")
|
||||||
|
else:
|
||||||
|
episode.projectId = project_id
|
||||||
|
await episode_repo.create(episode)
|
||||||
|
logger.info(f"创建新剧集记录: {episode.id} - EP{request.episodeNumber}")
|
||||||
|
|
||||||
# 更新项目记忆
|
# 更新项目记忆
|
||||||
await project_repo.update(project_id, {
|
await project_repo.update(project_id, {
|
||||||
|
|||||||
@ -272,6 +272,115 @@ async def websocket_batch_execution(
|
|||||||
# 消息处理
|
# 消息处理
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
|
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(
|
async def _handle_client_message(
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
@ -293,6 +402,49 @@ async def _handle_client_message(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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":
|
elif message_type == "inbox_action":
|
||||||
# 用户在 Inbox 中点击批准或拒绝
|
# 用户在 Inbox 中点击批准或拒绝
|
||||||
action = message.get("action")
|
action = message.get("action")
|
||||||
@ -302,6 +454,9 @@ async def _handle_client_message(
|
|||||||
feedback = f"User {action}ed inbox item {item_id}."
|
feedback = f"User {action}ed inbox item {item_id}."
|
||||||
|
|
||||||
agent = manager.get_agent(project_id, project_dir)
|
agent = manager.get_agent(project_id, project_dir)
|
||||||
|
# 确保上下文已加载
|
||||||
|
await _ensure_agent_context(agent, project_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for event in agent.stream_events(feedback, thread_id=project_id):
|
for event in agent.stream_events(feedback, thread_id=project_id):
|
||||||
# 同样的事件处理逻辑
|
# 同样的事件处理逻辑
|
||||||
@ -317,6 +472,9 @@ async def _handle_client_message(
|
|||||||
elif message_type == "chat_message":
|
elif message_type == "chat_message":
|
||||||
# 用户发送聊天消息 -> 触发 Agent 执行
|
# 用户发送聊天消息 -> 触发 Agent 执行
|
||||||
content = message.get("content", "")
|
content = message.get("content", "")
|
||||||
|
episode_number = message.get("episodeNumber")
|
||||||
|
episode_title = message.get("episodeTitle")
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -326,56 +484,8 @@ async def _handle_client_message(
|
|||||||
# 获取 Agent
|
# 获取 Agent
|
||||||
agent = manager.get_agent(project_id, project_dir)
|
agent = manager.get_agent(project_id, project_dir)
|
||||||
|
|
||||||
# 加载项目上下文并更新 Agent(如果尚未加载)
|
# 确保上下文已加载(包含当前剧集信息)
|
||||||
if agent.context and not agent.context.project_id:
|
await _ensure_agent_context(agent, project_id, episode_number, episode_title)
|
||||||
try:
|
|
||||||
from app.db.repositories import project_repo
|
|
||||||
from app.core.agent_runtime.context import SkillAgentContext
|
|
||||||
from app.core.agent_runtime.skill_loader import SkillLoader
|
|
||||||
from app.core.skills.skill_manager import skill_manager
|
|
||||||
|
|
||||||
project = await project_repo.get(project_id)
|
|
||||||
if project:
|
|
||||||
# 将项目的 defaultTaskSkills 转换为 user_skills 格式
|
|
||||||
user_skills = []
|
|
||||||
if hasattr(project, 'defaultTaskSkills') and project.defaultTaskSkills:
|
|
||||||
for task_config in project.defaultTaskSkills:
|
|
||||||
for skill_config in task_config.skills:
|
|
||||||
try:
|
|
||||||
# 通过 skill_manager 获取技能详细信息
|
|
||||||
skill = skill_manager.get_skill_by_id(skill_config.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 {skill_config.skill_id}: {e}")
|
|
||||||
|
|
||||||
# 创建项目上下文
|
|
||||||
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=project.globalContext.styleGuide if project.globalContext else 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
|
|
||||||
)
|
|
||||||
# 更新 Agent 的上下文
|
|
||||||
agent.context = project_context
|
|
||||||
# 重新构建 system prompt
|
|
||||||
agent.system_prompt = agent._build_system_prompt()
|
|
||||||
logger.info(f"Loaded project context for {project_id}: {project.name}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to load project context for {project_id}: {e}")
|
|
||||||
|
|
||||||
# 异步运行 Agent 并将事件流推送到前端
|
# 异步运行 Agent 并将事件流推送到前端
|
||||||
full_response = ""
|
full_response = ""
|
||||||
@ -541,9 +651,48 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]):
|
|||||||
})
|
})
|
||||||
|
|
||||||
elif name == "save_episode":
|
elif name == "save_episode":
|
||||||
# 处理剧集保存
|
# 处理剧集保存并持久化
|
||||||
episode_number = args.get("episode_number", 0)
|
episode_number = args.get("episode_number")
|
||||||
title = args.get("title", "")
|
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 MemoryManager
|
||||||
|
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 = MemoryManager()
|
||||||
|
await memory_manager.update_memory_from_episode(project, episode)
|
||||||
|
logger.info(f"Updated memory after saving episode {episode_number}")
|
||||||
|
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, {
|
await manager.send_to_project(project_id, {
|
||||||
"type": "episode_saved",
|
"type": "episode_saved",
|
||||||
@ -551,6 +700,64 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]):
|
|||||||
"title": title
|
"title": title
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 MemoryManager
|
||||||
|
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 = MemoryManager()
|
||||||
|
await memory_manager.update_memory_from_episode(project, episode)
|
||||||
|
logger.info(f"Updated memory after updating episode {episode_number}")
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 辅助函数 - 用于从其他模块发送消息
|
# 辅助函数 - 用于从其他模块发送消息
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@ -60,11 +60,16 @@ class LangChainSkillsAgent:
|
|||||||
self.working_directory = working_directory or Path.cwd()
|
self.working_directory = working_directory or Path.cwd()
|
||||||
|
|
||||||
self.skill_loader = SkillLoader(skill_paths)
|
self.skill_loader = SkillLoader(skill_paths)
|
||||||
self.system_prompt = self._build_system_prompt()
|
|
||||||
self.context = SkillAgentContext(
|
self.context = SkillAgentContext(
|
||||||
skill_loader=self.skill_loader,
|
skill_loader=self.skill_loader,
|
||||||
working_directory=self.working_directory,
|
working_directory=self.working_directory,
|
||||||
)
|
)
|
||||||
|
self.checkpointer = InMemorySaver()
|
||||||
|
self.system_prompt = self._build_system_prompt()
|
||||||
|
self.agent = self._create_agent()
|
||||||
|
|
||||||
|
def refresh_agent(self):
|
||||||
|
"""重新构建 LangChain Agent 以应用新的 System Prompt"""
|
||||||
self.agent = self._create_agent()
|
self.agent = self._create_agent()
|
||||||
|
|
||||||
def _build_system_prompt(self) -> str:
|
def _build_system_prompt(self) -> str:
|
||||||
@ -93,7 +98,7 @@ When a user request matches a skill's description, use the load_skill tool to ge
|
|||||||
tools=ALL_TOOLS,
|
tools=ALL_TOOLS,
|
||||||
system_prompt=self.system_prompt,
|
system_prompt=self.system_prompt,
|
||||||
context_schema=SkillAgentContext,
|
context_schema=SkillAgentContext,
|
||||||
checkpointer=InMemorySaver(),
|
checkpointer=self.checkpointer,
|
||||||
)
|
)
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
@ -120,7 +125,7 @@ When a user request matches a skill's description, use the load_skill tool to ge
|
|||||||
tools=ALL_TOOLS,
|
tools=ALL_TOOLS,
|
||||||
system_prompt=self.system_prompt,
|
system_prompt=self.system_prompt,
|
||||||
context_schema=SkillAgentContext,
|
context_schema=SkillAgentContext,
|
||||||
checkpointer=InMemorySaver(),
|
checkpointer=self.checkpointer,
|
||||||
)
|
)
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
|
|||||||
@ -28,5 +28,12 @@ class SkillAgentContext:
|
|||||||
creation_mode: Optional[str] = None # 'script' or 'inspiration'
|
creation_mode: Optional[str] = None # 'script' or 'inspiration'
|
||||||
source_content: Optional[str] = None # 剧本或灵感内容
|
source_content: Optional[str] = None # 剧本或灵感内容
|
||||||
|
|
||||||
|
# 当前活跃剧集
|
||||||
|
active_episode_number: Optional[int] = None
|
||||||
|
active_episode_title: Optional[str] = None
|
||||||
|
|
||||||
|
# 所有剧集摘要
|
||||||
|
episodes: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
# 用户配置的 Skills
|
# 用户配置的 Skills
|
||||||
user_skills: List[Dict[str, Any]] = field(default_factory=list)
|
user_skills: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
|||||||
@ -39,11 +39,25 @@ class DirectorAgent(LangChainSkillsAgent):
|
|||||||
model=model,
|
model=model,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 确保 context 属性存在(防御性检查)
|
||||||
|
# 父类应该已经创建了 self.context,但为了防止某些边界情况,这里再次确认
|
||||||
|
if not hasattr(self, 'context') or self.context is None:
|
||||||
|
from .skill_loader import SkillLoader
|
||||||
|
self.context = SkillAgentContext(
|
||||||
|
skill_loader=self.skill_loader,
|
||||||
|
working_directory=self.working_directory,
|
||||||
|
)
|
||||||
|
|
||||||
# 如果提供了项目上下文,更新 context
|
# 如果提供了项目上下文,更新 context
|
||||||
if project_context:
|
if project_context:
|
||||||
self.context = project_context
|
self.context = project_context
|
||||||
# 重新构建 system prompt
|
# 重新构建 system prompt 并刷新 agent
|
||||||
self.system_prompt = self._build_system_prompt()
|
self.system_prompt = self._build_system_prompt()
|
||||||
|
self.refresh_agent()
|
||||||
|
|
||||||
|
def refresh_agent(self):
|
||||||
|
"""重新构建 LangChain Agent 以应用新的 System Prompt"""
|
||||||
|
self.agent = self._create_agent()
|
||||||
|
|
||||||
def _build_system_prompt(self) -> str:
|
def _build_system_prompt(self) -> str:
|
||||||
# 基础 prompt
|
# 基础 prompt
|
||||||
@ -52,18 +66,23 @@ The User is the Director. Your goal is to help the Director create high-quality
|
|||||||
|
|
||||||
## Your Role
|
## Your Role
|
||||||
- **Proactive Partner**: Don't just wait for orders. Propose plans, spot issues, and suggest improvements.
|
- **Proactive Partner**: Don't just wait for orders. Propose plans, spot issues, and suggest improvements.
|
||||||
- **Structured Executor**: For any complex task (like "Write Episode 1"), you MUST first create a Plan using `update_plan`.
|
- **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`.
|
||||||
- **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.
|
||||||
|
|
||||||
## Workflow Protocols
|
## Workflow Protocols
|
||||||
|
|
||||||
1. **Planning (Mandatory for new tasks)**
|
1. **Planning (Mandatory for new tasks)**
|
||||||
- When receiving a high-level goal (e.g., "Write Scene 1"), break it down into steps.
|
- When receiving a high-level goal (e.g., "Write all scripts"), break it down into episodes and steps.
|
||||||
- Use `update_plan(steps=[...], current_step=0, status='planning')`.
|
- Use `update_plan(steps=[...], current_step=0, status='planning')`.
|
||||||
|
|
||||||
2. **Execution & Writing**
|
2. **Execution & Writing**
|
||||||
- Use `write_file` to generate content.
|
- Use `list_episodes` to see the current progress of the project.
|
||||||
- Use `update_canvas` (or write to the active file) to show progress.
|
- 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.
|
||||||
|
- **Outline Location**: Episode-level outlines should be stored in the episode's `outline` field via `save_episode` or `update_episode`. Global outlines are in the project context.
|
||||||
|
- Use `write_to_canvas` to show the draft script you are working on.
|
||||||
|
- 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')`.
|
||||||
|
|
||||||
3. **Review & Approval**
|
3. **Review & Approval**
|
||||||
@ -82,39 +101,49 @@ The User is the Director. Your goal is to help the Director create high-quality
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 添加项目上下文(如果有)
|
# 添加项目上下文(如果有)
|
||||||
if self.context and self.context.project_name:
|
context = getattr(self, 'context', None)
|
||||||
|
if context and context.project_name:
|
||||||
base_prompt += "\n\n## Project Context\n\n"
|
base_prompt += "\n\n## Project Context\n\n"
|
||||||
base_prompt += f"**Project**: {self.context.project_name}\n"
|
base_prompt += f"**Project**: {context.project_name}\n"
|
||||||
|
|
||||||
if self.context.project_genre:
|
if context.project_genre:
|
||||||
base_prompt += f"**Genre**: {self.context.project_genre}\n"
|
base_prompt += f"**Genre**: {context.project_genre}\n"
|
||||||
|
|
||||||
if self.context.total_episodes:
|
if context.total_episodes:
|
||||||
base_prompt += f"**Total Episodes**: {self.context.total_episodes}\n"
|
base_prompt += f"**Total Episodes**: {context.total_episodes}\n"
|
||||||
|
|
||||||
base_prompt += "\n### Global Settings\n\n"
|
base_prompt += "\n### Global Settings\n\n"
|
||||||
|
|
||||||
if self.context.world_setting:
|
if context.world_setting:
|
||||||
base_prompt += f"**World Setting**:\n{self.context.world_setting}\n\n"
|
base_prompt += f"**World Setting**:\n{context.world_setting}\n\n"
|
||||||
|
|
||||||
if self.context.characters:
|
if context.characters:
|
||||||
base_prompt += f"**Characters**:\n{self.context.characters}\n\n"
|
base_prompt += f"**Characters**:\n{context.characters}\n\n"
|
||||||
|
|
||||||
if self.context.overall_outline:
|
if context.overall_outline:
|
||||||
base_prompt += f"**Overall Outline**:\n{self.context.overall_outline}\n\n"
|
base_prompt += f"**Overall Outline**:\n{context.overall_outline}\n\n"
|
||||||
|
|
||||||
if self.context.source_content:
|
if context.source_content:
|
||||||
mode = "剧本改编" if self.context.creation_mode == "script" else "创意灵感"
|
mode = "剧本改编" if context.creation_mode == "script" else "创意灵感"
|
||||||
base_prompt += f"**Source** ({mode}):\n{self.context.source_content[:1000]}...\n\n"
|
base_prompt += f"**Source** ({mode}):\n{context.source_content[:1000]}...\n\n"
|
||||||
|
|
||||||
|
# 添加当前剧集信息
|
||||||
|
if context and context.active_episode_number:
|
||||||
|
base_prompt += "## Current Task Focus\n\n"
|
||||||
|
base_prompt += f"You are currently focusing on **Episode {context.active_episode_number}**"
|
||||||
|
if context.active_episode_title:
|
||||||
|
base_prompt += f": {context.active_episode_title}"
|
||||||
|
base_prompt += ".\n"
|
||||||
|
base_prompt += "Your primary goal is to generate or refine content for THIS SPECIFIC episode based on the global context provided above.\n\n"
|
||||||
|
|
||||||
# 构建 skills prompt
|
# 构建 skills prompt
|
||||||
skills_prompt = self.skill_loader.build_system_prompt("")
|
skills_prompt = self.skill_loader.build_system_prompt("")
|
||||||
|
|
||||||
# 添加用户配置的 skills
|
# 添加用户配置的 skills
|
||||||
if self.context and self.context.user_skills:
|
if context and context.user_skills:
|
||||||
skills_prompt += "\n\n### User Configured Skills\n\n"
|
skills_prompt += "\n\n### User Configured Skills\n\n"
|
||||||
skills_prompt += "The Director has configured these specific skills for this project:\n\n"
|
skills_prompt += "The Director has configured these specific skills for this project:\n\n"
|
||||||
for skill in self.context.user_skills:
|
for skill in context.user_skills:
|
||||||
skills_prompt += f"- **{skill.get('name', 'Unknown')}**: {skill.get('behavior', 'No description')}\n"
|
skills_prompt += f"- **{skill.get('name', 'Unknown')}**: {skill.get('behavior', 'No description')}\n"
|
||||||
skills_prompt += "\nUse `load_skill` to load detailed instructions for these skills when relevant.\n"
|
skills_prompt += "\nUse `load_skill` to load detailed instructions for these skills when relevant.\n"
|
||||||
|
|
||||||
|
|||||||
@ -462,6 +462,129 @@ Content: {content[:100]}{'...' if len(content) > 100 else ''}
|
|||||||
Waiting for director's review..."""
|
Waiting for director's review..."""
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def focus_episode(
|
||||||
|
episode_number: int,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
runtime: ToolRuntime[SkillAgentContext] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Set the agent's focus to a specific episode.
|
||||||
|
|
||||||
|
This tool tells the system (and the user) that the agent is now
|
||||||
|
working on a specific episode. The frontend will update the active
|
||||||
|
canvas to match this episode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
episode_number: The episode number to focus on (e.g., 1, 2, 3...)
|
||||||
|
title: The title of the episode (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation message
|
||||||
|
|
||||||
|
Example:
|
||||||
|
focus_episode(episode_number=5, title="The Turning Point")
|
||||||
|
"""
|
||||||
|
if runtime is None:
|
||||||
|
return "Error: runtime context not available"
|
||||||
|
|
||||||
|
# 更新运行时上下文
|
||||||
|
runtime.context.active_episode_number = episode_number
|
||||||
|
if title:
|
||||||
|
runtime.context.active_episode_title = title
|
||||||
|
|
||||||
|
# 存储到状态中,以便 WebSocket 发送通知
|
||||||
|
runtime.state["focus_change"] = {
|
||||||
|
"episodeNumber": episode_number,
|
||||||
|
"episodeTitle": title
|
||||||
|
}
|
||||||
|
|
||||||
|
return f"✓ Focused on Episode {episode_number}{f': {title}' if title else ''}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def list_episodes(
|
||||||
|
runtime: ToolRuntime[SkillAgentContext] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
List all episodes in the current project.
|
||||||
|
|
||||||
|
This tool returns a list of all episodes with their numbers, titles,
|
||||||
|
and current status. Use this to see what has been written and what
|
||||||
|
is still pending.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A formatted list of episodes
|
||||||
|
"""
|
||||||
|
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"
|
||||||
|
|
||||||
|
# 这里我们不直接读取数据库,而是告诉 agent 我们会通过 websocket 返回或从 context 中读取
|
||||||
|
# 实际上,context 应该已经包含了这些信息
|
||||||
|
episodes = getattr(runtime.context, 'episodes', [])
|
||||||
|
if not episodes:
|
||||||
|
return "No episodes found or episode list not loaded in context."
|
||||||
|
|
||||||
|
lines = ["Episodes in project:"]
|
||||||
|
for ep in episodes:
|
||||||
|
lines.append(f"- EP{ep.get('number')}: {ep.get('title')} [{ep.get('status')}]")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def update_episode(
|
||||||
|
episode_number: int,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
content: Optional[str] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
outline: Optional[str] = None,
|
||||||
|
runtime: ToolRuntime[SkillAgentContext] = None
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Update an existing episode's details.
|
||||||
|
|
||||||
|
Use this tool to update the content, title, or status of an episode.
|
||||||
|
The changes will be persisted to the database and updated in the UI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
episode_number: The episode number to update
|
||||||
|
title: New title for the episode (optional)
|
||||||
|
content: New content/script (optional)
|
||||||
|
status: New status (e.g., 'completed', 'writing', 'pending')
|
||||||
|
outline: New outline (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Confirmation message
|
||||||
|
"""
|
||||||
|
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"
|
||||||
|
|
||||||
|
# 存储到状态中,由 WebSocket 处理器保存
|
||||||
|
if "episodes_to_update" not in runtime.state:
|
||||||
|
runtime.state["episodes_to_update"] = []
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
"number": episode_number,
|
||||||
|
}
|
||||||
|
if title is not None: update_data["title"] = title
|
||||||
|
if content is not None: update_data["content"] = content
|
||||||
|
if status is not None: update_data["status"] = status
|
||||||
|
if outline is not None: update_data["outline"] = outline
|
||||||
|
|
||||||
|
runtime.state["episodes_to_update"].append(update_data)
|
||||||
|
|
||||||
|
return f"✓ Episode {episode_number} queued for update."
|
||||||
|
|
||||||
|
|
||||||
# 导出工具列表
|
# 导出工具列表
|
||||||
DIRECTOR_TOOLS = [
|
DIRECTOR_TOOLS = [
|
||||||
update_plan,
|
update_plan,
|
||||||
@ -471,5 +594,8 @@ DIRECTOR_TOOLS = [
|
|||||||
write_to_canvas,
|
write_to_canvas,
|
||||||
save_episode,
|
save_episode,
|
||||||
update_memory,
|
update_memory,
|
||||||
request_review
|
request_review,
|
||||||
|
focus_episode,
|
||||||
|
list_episodes,
|
||||||
|
update_episode
|
||||||
]
|
]
|
||||||
|
|||||||
@ -42,45 +42,51 @@ class SeriesCreationAgent:
|
|||||||
) -> Episode:
|
) -> Episode:
|
||||||
"""
|
"""
|
||||||
执行单集创作
|
执行单集创作
|
||||||
|
|
||||||
Args:
|
|
||||||
project: 剧集项目
|
|
||||||
episode_number: 集数
|
|
||||||
title: 集标题(可选)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
创作的 Episode 对象
|
|
||||||
"""
|
"""
|
||||||
logger.info(f"开始创作 EP{episode_number}")
|
logger.info(f"开始创作 EP{episode_number}")
|
||||||
|
|
||||||
# 创建 Episode 对象
|
# 获取现有剧集数据(如果有)
|
||||||
|
from app.db.repositories import episode_repo
|
||||||
|
episodes = await episode_repo.list_by_project(project.id)
|
||||||
|
existing_episode = next((ep for ep in episodes if ep.number == episode_number), None)
|
||||||
|
|
||||||
|
# 创建或更新 Episode 对象
|
||||||
episode = Episode(
|
episode = Episode(
|
||||||
projectId=project.id,
|
projectId=project.id,
|
||||||
number=episode_number,
|
number=episode_number,
|
||||||
title=title or f"第{episode_number}集",
|
title=title or (existing_episode.title if existing_episode else f"第{episode_number}集"),
|
||||||
status="writing"
|
status="writing"
|
||||||
)
|
)
|
||||||
|
if existing_episode:
|
||||||
|
episode.id = existing_episode.id
|
||||||
|
episode.outline = existing_episode.outline
|
||||||
|
episode.content = existing_episode.content
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# ============================================
|
# ============================================
|
||||||
# 阶段 1: 结构分析
|
# 阶段 1: 结构分析
|
||||||
# ============================================
|
# ============================================
|
||||||
logger.info(f"EP{episode_number} - 阶段 1: 结构分析")
|
if not episode.structure:
|
||||||
structure = await self._analyze_structure(project, episode_number)
|
logger.info(f"EP{episode_number} - 阶段 1: 结构分析")
|
||||||
episode.structure = structure
|
structure = await self._analyze_structure(project, episode_number)
|
||||||
|
episode.structure = structure
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 阶段 2: 大纲生成
|
# 阶段 2: 大纲生成
|
||||||
|
# 如果已有大纲且非空,则跳过
|
||||||
# ============================================
|
# ============================================
|
||||||
logger.info(f"EP{episode_number} - 阶段 2: 大纲生成")
|
if not episode.outline or len(episode.outline.strip()) < 10:
|
||||||
outline = await self._generate_outline(project, episode_number, structure)
|
logger.info(f"EP{episode_number} - 阶段 2: 大纲生成")
|
||||||
episode.outline = outline
|
outline = await self._generate_outline(project, episode_number, episode.structure)
|
||||||
|
episode.outline = outline
|
||||||
|
else:
|
||||||
|
logger.info(f"EP{episode_number} - 使用已有大纲,跳过生成阶段")
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 阶段 3: 对话创作(核心)
|
# 阶段 3: 对话创作(核心)
|
||||||
# ============================================
|
# ============================================
|
||||||
logger.info(f"EP{episode_number} - 阶段 3: 对话创作")
|
logger.info(f"EP{episode_number} - 阶段 3: 对话创作")
|
||||||
content = await self._write_dialogue(project, episode_number, outline)
|
content = await self._write_dialogue(project, episode_number, episode.outline)
|
||||||
episode.content = content
|
episode.content = content
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
@ -103,8 +109,9 @@ class SeriesCreationAgent:
|
|||||||
logger.info(f"EP{episode_number} 创作完成,质量分数: {episode.qualityScore}")
|
logger.info(f"EP{episode_number} 创作完成,质量分数: {episode.qualityScore}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"EP{episode_number} 创作失败: {str(e)}")
|
logger.error(f"EP{episode_number} 创作失败: {str(e)}", exc_info=True)
|
||||||
episode.status = "needs-review"
|
episode.status = "needs-review"
|
||||||
|
episode.content = f"第{episode_number}集内容创作失败: {str(e)}"
|
||||||
episode.issues = [
|
episode.issues = [
|
||||||
EpisodeIssue(
|
EpisodeIssue(
|
||||||
type="execution_error",
|
type="execution_error",
|
||||||
@ -349,22 +356,39 @@ class SeriesCreationAgent:
|
|||||||
"""构建上下文字符串"""
|
"""构建上下文字符串"""
|
||||||
context_parts = []
|
context_parts = []
|
||||||
|
|
||||||
# 世界观
|
# 1. 世界观 (防御性检查)
|
||||||
if project.globalContext.worldSetting:
|
world_setting = getattr(project.globalContext, 'worldSetting', '')
|
||||||
context_parts.append(f"世界观:{project.globalContext.worldSetting}")
|
if world_setting:
|
||||||
|
context_parts.append(f"世界观:{world_setting}")
|
||||||
|
|
||||||
# 人物设定
|
# 2. 人物设定 (防御性检查)
|
||||||
if project.globalContext.characterProfiles:
|
profiles = getattr(project.globalContext, 'characterProfiles', {})
|
||||||
|
if isinstance(profiles, dict) and profiles:
|
||||||
context_parts.append("\n人物设定:")
|
context_parts.append("\n人物设定:")
|
||||||
for char_id, char in project.globalContext.characterProfiles.items():
|
for char_id, char in profiles.items():
|
||||||
context_parts.append(f"- {char.name}:{char.personality},说话风格:{char.speechStyle}")
|
name = getattr(char, 'name', '未知角色')
|
||||||
|
personality = getattr(char, 'personality', '')
|
||||||
|
speech_style = getattr(char, 'speechStyle', '')
|
||||||
|
context_parts.append(f"- {name}:{personality},说话风格:{speech_style}")
|
||||||
|
elif isinstance(profiles, str) and profiles:
|
||||||
|
# 兼容字符串格式
|
||||||
|
context_parts.append(f"\n人物设定:\n{profiles}")
|
||||||
|
|
||||||
# 历史记忆(最近3集)
|
# 3. 风格指南
|
||||||
if project.memory.eventTimeline:
|
style_guide = getattr(project.globalContext, 'styleGuide', '')
|
||||||
|
if style_guide:
|
||||||
|
context_parts.append(f"\n创作风格指南:\n{style_guide}")
|
||||||
|
|
||||||
|
# 4. 历史记忆 (最近3集)
|
||||||
|
memory_timeline = getattr(project.memory, 'eventTimeline', [])
|
||||||
|
if isinstance(memory_timeline, list) and memory_timeline:
|
||||||
context_parts.append("\n历史剧情:")
|
context_parts.append("\n历史剧情:")
|
||||||
recent_events = project.memory.eventTimeline[-3:]
|
recent_events = memory_timeline[-3:]
|
||||||
for event in recent_events:
|
for event in recent_events:
|
||||||
context_parts.append(f"- {event.get('event', '')}")
|
if isinstance(event, dict):
|
||||||
|
context_parts.append(f"- {event.get('event', '')}")
|
||||||
|
else:
|
||||||
|
context_parts.append(f"- {str(event)}")
|
||||||
|
|
||||||
return "\n".join(context_parts)
|
return "\n".join(context_parts)
|
||||||
|
|
||||||
|
|||||||
@ -878,27 +878,48 @@ class MemoryManager:
|
|||||||
|
|
||||||
def _convert_to_enhanced_memory(self, memory: Memory) -> EnhancedMemory:
|
def _convert_to_enhanced_memory(self, memory: Memory) -> EnhancedMemory:
|
||||||
"""将基础 Memory 转换为 EnhancedMemory"""
|
"""将基础 Memory 转换为 EnhancedMemory"""
|
||||||
|
# 防御性处理 characterStates
|
||||||
|
char_states = {}
|
||||||
|
raw_states = getattr(memory, 'characterStates', {})
|
||||||
|
|
||||||
|
if isinstance(raw_states, dict):
|
||||||
|
for char, states in raw_states.items():
|
||||||
|
if isinstance(states, list):
|
||||||
|
char_states[char] = [
|
||||||
|
CharacterStateChange(**state) if isinstance(state, dict) else state
|
||||||
|
for state in states
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
char_states[char] = []
|
||||||
|
elif isinstance(raw_states, str) and raw_states:
|
||||||
|
import ast
|
||||||
|
try:
|
||||||
|
parsed = ast.literal_eval(raw_states)
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
for char, states in parsed.items():
|
||||||
|
if isinstance(states, list):
|
||||||
|
char_states[char] = [
|
||||||
|
CharacterStateChange(**state) if isinstance(state, dict) else state
|
||||||
|
for state in states
|
||||||
|
]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return EnhancedMemory(
|
return EnhancedMemory(
|
||||||
eventTimeline=[
|
eventTimeline=[
|
||||||
TimelineEvent(**event) if isinstance(event, dict) else event
|
TimelineEvent(**event) if isinstance(event, dict) else event
|
||||||
for event in memory.eventTimeline
|
for event in (memory.eventTimeline if isinstance(memory.eventTimeline, list) else [])
|
||||||
],
|
],
|
||||||
pendingThreads=[
|
pendingThreads=[
|
||||||
PendingThread(**thread) if isinstance(thread, dict) else thread
|
PendingThread(**thread) if isinstance(thread, dict) else thread
|
||||||
for thread in memory.pendingThreads
|
for thread in (memory.pendingThreads if isinstance(memory.pendingThreads, list) else [])
|
||||||
],
|
],
|
||||||
characterStates={
|
characterStates=char_states,
|
||||||
char: [
|
|
||||||
CharacterStateChange(**state) if isinstance(state, dict) else state
|
|
||||||
for state in states
|
|
||||||
]
|
|
||||||
for char, states in memory.characterStates.items()
|
|
||||||
},
|
|
||||||
foreshadowing=[
|
foreshadowing=[
|
||||||
ForeshadowingEvent(**fs) if isinstance(fs, dict) else fs
|
ForeshadowingEvent(**fs) if isinstance(fs, dict) else fs
|
||||||
for fs in memory.foreshadowing
|
for fs in (memory.foreshadowing if isinstance(memory.foreshadowing, list) else [])
|
||||||
],
|
],
|
||||||
relationships=memory.relationships if hasattr(memory, 'relationships') else {},
|
relationships=getattr(memory, 'relationships', {}),
|
||||||
consistencyIssues=[],
|
consistencyIssues=[],
|
||||||
last_updated=datetime.now(),
|
last_updated=datetime.now(),
|
||||||
last_episode_processed=0
|
last_episode_processed=0
|
||||||
|
|||||||
@ -64,7 +64,7 @@ class JsonRepository:
|
|||||||
for k, v in self._data.items():
|
for k, v in self._data.items():
|
||||||
if hasattr(v, "dict"):
|
if hasattr(v, "dict"):
|
||||||
serialized_data[k] = json.loads(v.json())
|
serialized_data[k] = json.loads(v.json())
|
||||||
elif isinstance(v, dict):
|
elif isinstance(v, (dict, list)):
|
||||||
serialized_data[k] = v
|
serialized_data[k] = v
|
||||||
else:
|
else:
|
||||||
serialized_data[k] = str(v)
|
serialized_data[k] = str(v)
|
||||||
@ -85,11 +85,35 @@ class ProjectRepository(JsonRepository):
|
|||||||
super().__init__(PROJECTS_FILE)
|
super().__init__(PROJECTS_FILE)
|
||||||
# 将加载的字典转换为对象
|
# 将加载的字典转换为对象
|
||||||
self._objects: Dict[str, SeriesProject] = {}
|
self._objects: Dict[str, SeriesProject] = {}
|
||||||
for k, v in self._data.items():
|
import ast
|
||||||
|
is_dirty = False
|
||||||
|
for k, v in list(self._data.items()):
|
||||||
try:
|
try:
|
||||||
|
# 修复可能存在的字符串化数据
|
||||||
|
if isinstance(v, str):
|
||||||
|
try:
|
||||||
|
v = ast.literal_eval(v)
|
||||||
|
self._data[k] = v
|
||||||
|
is_dirty = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 递归修复嵌套的字符串数据(如 memory 或 globalContext)
|
||||||
|
if isinstance(v, dict):
|
||||||
|
for sub_k, sub_v in v.items():
|
||||||
|
if isinstance(sub_v, str) and (sub_v.startswith('{') or sub_v.startswith('[')):
|
||||||
|
try:
|
||||||
|
v[sub_k] = ast.literal_eval(sub_v)
|
||||||
|
is_dirty = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
self._objects[k] = SeriesProject.parse_obj(v)
|
self._objects[k] = SeriesProject.parse_obj(v)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to parse project {k}: {e}")
|
logger.error(f"Failed to parse project {k}: {e}")
|
||||||
|
|
||||||
|
if is_dirty:
|
||||||
|
self._save()
|
||||||
|
|
||||||
async def create(self, project_data: SeriesProjectCreate) -> SeriesProject:
|
async def create(self, project_data: SeriesProjectCreate) -> SeriesProject:
|
||||||
"""创建新项目"""
|
"""创建新项目"""
|
||||||
@ -165,11 +189,30 @@ class EpisodeRepository(JsonRepository):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(EPISODES_FILE)
|
super().__init__(EPISODES_FILE)
|
||||||
self._objects: Dict[str, Episode] = {}
|
self._objects: Dict[str, Episode] = {}
|
||||||
for k, v in self._data.items():
|
# 用于追踪已存在的 (projectId, number) 对,防止重复
|
||||||
|
seen_episodes = set()
|
||||||
|
duplicates_to_remove = []
|
||||||
|
|
||||||
|
for k, v in list(self._data.items()):
|
||||||
try:
|
try:
|
||||||
self._objects[k] = Episode.parse_obj(v)
|
episode = Episode.parse_obj(v)
|
||||||
|
key = (episode.projectId, episode.number)
|
||||||
|
|
||||||
|
if key in seen_episodes:
|
||||||
|
logger.warning(f"Found duplicate episode: Project {episode.projectId}, EP{episode.number}. Removing ID {k}")
|
||||||
|
duplicates_to_remove.append(k)
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen_episodes.add(key)
|
||||||
|
self._objects[k] = episode
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to parse episode {k}: {e}")
|
logger.error(f"Failed to parse episode {k}: {e}")
|
||||||
|
|
||||||
|
# 清理重复项
|
||||||
|
if duplicates_to_remove:
|
||||||
|
for k in duplicates_to_remove:
|
||||||
|
del self._data[k]
|
||||||
|
self._save()
|
||||||
|
|
||||||
async def create(self, episode: Episode) -> Episode:
|
async def create(self, episode: Episode) -> Episode:
|
||||||
"""创建剧集"""
|
"""创建剧集"""
|
||||||
@ -220,8 +263,25 @@ class MessageRepository(JsonRepository):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(MESSAGES_FILE)
|
super().__init__(MESSAGES_FILE)
|
||||||
# 结构: {project_id: [{role, content, timestamp}, ...]}
|
# 修复可能被错误序列化为字符串的历史数据
|
||||||
|
import ast
|
||||||
|
is_dirty = False
|
||||||
|
for project_id in list(self._data.keys()):
|
||||||
|
messages = self._data[project_id]
|
||||||
|
if isinstance(messages, str):
|
||||||
|
try:
|
||||||
|
# 尝试解析 Python repr 格式的字符串
|
||||||
|
self._data[project_id] = ast.literal_eval(messages)
|
||||||
|
is_dirty = True
|
||||||
|
logger.info(f"Fixed corrupted message history for project {project_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to fix message history for {project_id}: {e}")
|
||||||
|
self._data[project_id] = []
|
||||||
|
is_dirty = True
|
||||||
|
|
||||||
|
if is_dirty:
|
||||||
|
self._save()
|
||||||
|
|
||||||
async def add_message(self, project_id: str, role: str, content: str):
|
async def add_message(self, project_id: str, role: str, content: str):
|
||||||
"""添加消息"""
|
"""添加消息"""
|
||||||
if project_id not in self._data:
|
if project_id not in self._data:
|
||||||
|
|||||||
516
backend/data/episodes.json
Normal file
516
backend/data/episodes.json
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
{
|
||||||
|
"f3cc5c29-9fdd-4403-b48b-0cec9a126bf7": {
|
||||||
|
"id": "f3cc5c29-9fdd-4403-b48b-0cec9a126bf7",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 1,
|
||||||
|
"title": "第1集",
|
||||||
|
"status": "completed",
|
||||||
|
"structure": {
|
||||||
|
"episodeNumber": 1,
|
||||||
|
"scenes": [],
|
||||||
|
"keyEvents": []
|
||||||
|
},
|
||||||
|
"outline": "第1集大纲:本集讲述...",
|
||||||
|
"content": "第1集内容创作失败: 'str' object has no attribute 'items'",
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": 100.0,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:44:25.503861",
|
||||||
|
"completedAt": "2026-01-28T10:46:41.182457"
|
||||||
|
},
|
||||||
|
"5969c386-9615-461f-ad4a-12b0161020d7": {
|
||||||
|
"id": "5969c386-9615-461f-ad4a-12b0161020d7",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 2,
|
||||||
|
"title": "第2集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.840823",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"75a7de41-4768-450a-889b-783f818893f2": {
|
||||||
|
"id": "75a7de41-4768-450a-889b-783f818893f2",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 3,
|
||||||
|
"title": "第3集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.843865",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"57a6aeb2-bcf0-4eb0-935a-542ccd36b7b4": {
|
||||||
|
"id": "57a6aeb2-bcf0-4eb0-935a-542ccd36b7b4",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 4,
|
||||||
|
"title": "第4集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.844871",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"f1c85c7e-6e13-4a55-abf0-c334fc96fc4d": {
|
||||||
|
"id": "f1c85c7e-6e13-4a55-abf0-c334fc96fc4d",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 5,
|
||||||
|
"title": "第5集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.845373",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"fe0c0364-618a-4f61-828b-588043b8c2f2": {
|
||||||
|
"id": "fe0c0364-618a-4f61-828b-588043b8c2f2",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 6,
|
||||||
|
"title": "第6集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.847896",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"3023103f-49ab-4dfe-a34e-d0856462f930": {
|
||||||
|
"id": "3023103f-49ab-4dfe-a34e-d0856462f930",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 7,
|
||||||
|
"title": "第7集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.848401",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"96828147-bfe4-45e1-9ac5-2f3dbbbedcf9": {
|
||||||
|
"id": "96828147-bfe4-45e1-9ac5-2f3dbbbedcf9",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 8,
|
||||||
|
"title": "第8集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.849409",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"1036fd7a-7813-4b64-a517-9c995c8841a9": {
|
||||||
|
"id": "1036fd7a-7813-4b64-a517-9c995c8841a9",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 9,
|
||||||
|
"title": "第9集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.849912",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"b833287c-7410-43aa-b05b-67a1b24b856c": {
|
||||||
|
"id": "b833287c-7410-43aa-b05b-67a1b24b856c",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 10,
|
||||||
|
"title": "第10集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.851426",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"b668aaa1-0f43-499e-a9be-b7ef17f37e71": {
|
||||||
|
"id": "b668aaa1-0f43-499e-a9be-b7ef17f37e71",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 11,
|
||||||
|
"title": "第11集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.852435",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"fdb41622-2084-4a11-9ffc-3c768688cd05": {
|
||||||
|
"id": "fdb41622-2084-4a11-9ffc-3c768688cd05",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 12,
|
||||||
|
"title": "第12集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.852940",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"fecb9360-4c35-4c6f-a9d8-46982fb2c6b3": {
|
||||||
|
"id": "fecb9360-4c35-4c6f-a9d8-46982fb2c6b3",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 13,
|
||||||
|
"title": "第13集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.853946",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"5a6fad63-32c8-46c7-a501-9d8c35272afe": {
|
||||||
|
"id": "5a6fad63-32c8-46c7-a501-9d8c35272afe",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 14,
|
||||||
|
"title": "第14集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.854449",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"13a1df9d-5d90-4027-a81b-77f9169ff9fb": {
|
||||||
|
"id": "13a1df9d-5d90-4027-a81b-77f9169ff9fb",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 15,
|
||||||
|
"title": "第15集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.855962",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"4c8a56a9-60f5-4ca0-90f8-1c969793b6a6": {
|
||||||
|
"id": "4c8a56a9-60f5-4ca0-90f8-1c969793b6a6",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 16,
|
||||||
|
"title": "第16集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.856478",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"a75e1544-99ae-4202-b29b-e7100c0d6c08": {
|
||||||
|
"id": "a75e1544-99ae-4202-b29b-e7100c0d6c08",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 17,
|
||||||
|
"title": "第17集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.857991",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"0654b85e-c625-4176-a274-7076cae6b7a0": {
|
||||||
|
"id": "0654b85e-c625-4176-a274-7076cae6b7a0",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 18,
|
||||||
|
"title": "第18集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.857991",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"61936a9a-0e18-46ad-add2-9f3ce69f6535": {
|
||||||
|
"id": "61936a9a-0e18-46ad-add2-9f3ce69f6535",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 19,
|
||||||
|
"title": "第19集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.859500",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"0eee7cfe-c25c-4d43-933e-688d5948ba43": {
|
||||||
|
"id": "0eee7cfe-c25c-4d43-933e-688d5948ba43",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 20,
|
||||||
|
"title": "第20集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.859500",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"f45c2c9c-74e0-4e73-a535-162da62655f2": {
|
||||||
|
"id": "f45c2c9c-74e0-4e73-a535-162da62655f2",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 21,
|
||||||
|
"title": "第21集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.861007",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"6facb5c8-6344-420e-8bcb-41cedd3fa325": {
|
||||||
|
"id": "6facb5c8-6344-420e-8bcb-41cedd3fa325",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 22,
|
||||||
|
"title": "第22集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.861007",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"3a8cafa3-5ad9-461c-bd36-ae24f13176f2": {
|
||||||
|
"id": "3a8cafa3-5ad9-461c-bd36-ae24f13176f2",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 23,
|
||||||
|
"title": "第23集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.862514",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"63e76928-b2ef-4b74-8d31-25d44382ee31": {
|
||||||
|
"id": "63e76928-b2ef-4b74-8d31-25d44382ee31",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 24,
|
||||||
|
"title": "第24集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.867638",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"74cfe677-4af5-44c1-b779-2c5d0f1317c0": {
|
||||||
|
"id": "74cfe677-4af5-44c1-b779-2c5d0f1317c0",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 25,
|
||||||
|
"title": "第25集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.869167",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"c9491958-508e-49bd-89c8-30ce04cf200d": {
|
||||||
|
"id": "c9491958-508e-49bd-89c8-30ce04cf200d",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 26,
|
||||||
|
"title": "第26集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.870678",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"d4689d35-59bc-4d43-8abd-dffc23a38a77": {
|
||||||
|
"id": "d4689d35-59bc-4d43-8abd-dffc23a38a77",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 27,
|
||||||
|
"title": "第27集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.872190",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"171684b0-b5a9-48fe-85a6-12a1490bf153": {
|
||||||
|
"id": "171684b0-b5a9-48fe-85a6-12a1490bf153",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 28,
|
||||||
|
"title": "第28集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.873252",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"107f5c49-58d8-4857-a55b-de691c49187e": {
|
||||||
|
"id": "107f5c49-58d8-4857-a55b-de691c49187e",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 29,
|
||||||
|
"title": "第29集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.874785",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"9b91e3b6-72c1-4197-abb6-df0c393941ed": {
|
||||||
|
"id": "9b91e3b6-72c1-4197-abb6-df0c393941ed",
|
||||||
|
"projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c",
|
||||||
|
"number": 30,
|
||||||
|
"title": "第30集",
|
||||||
|
"status": "pending",
|
||||||
|
"structure": null,
|
||||||
|
"outline": null,
|
||||||
|
"content": null,
|
||||||
|
"summary": null,
|
||||||
|
"qualityScore": null,
|
||||||
|
"retryCount": 0,
|
||||||
|
"issues": [],
|
||||||
|
"reviewResult": null,
|
||||||
|
"createdAt": "2026-01-28T10:28:53.875802",
|
||||||
|
"completedAt": null
|
||||||
|
}
|
||||||
|
}
|
||||||
39
backend/data/messages.json
Normal file
39
backend/data/messages.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -28,6 +28,6 @@
|
|||||||
"autoRetryConfig": null,
|
"autoRetryConfig": null,
|
||||||
"reviewConfig": null,
|
"reviewConfig": null,
|
||||||
"createdAt": "2026-01-27T16:22:58.755260",
|
"createdAt": "2026-01-27T16:22:58.755260",
|
||||||
"updatedAt": "2026-01-27T18:11:56.500700"
|
"updatedAt": "2026-01-28T10:49:27.517654"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,18 +21,21 @@ interface ContextPanelProps {
|
|||||||
activeStates?: any[];
|
activeStates?: any[];
|
||||||
memoryItems?: any[];
|
memoryItems?: any[];
|
||||||
onUpdateContext?: (type: string, data: any) => void;
|
onUpdateContext?: (type: string, data: any) => void;
|
||||||
|
onNavigateToSettings?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContextPanel: React.FC<ContextPanelProps> = ({
|
export const ContextPanel: React.FC<ContextPanelProps> = ({
|
||||||
project,
|
project,
|
||||||
loading,
|
loading,
|
||||||
activeStates = [],
|
activeStates = [],
|
||||||
memoryItems = []
|
memoryItems = [],
|
||||||
|
onNavigateToSettings
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState('world');
|
const [activeTab, setActiveTab] = useState('world');
|
||||||
|
|
||||||
// 模拟数据 - 实际应从 project.globalContext 获取
|
// 模拟数据 - 实际应从 project.globalContext 获取
|
||||||
const worldSetting = project?.globalContext?.worldSetting || "暂无世界观设定";
|
const worldSetting = project?.globalContext?.worldSetting || "暂无世界观设定";
|
||||||
|
const overallOutline = project?.globalContext?.overallOutline || "暂无整体大纲设定";
|
||||||
const rawCharacters = project?.globalContext?.characterProfiles;
|
const rawCharacters = project?.globalContext?.characterProfiles;
|
||||||
// 人物设定可能存储在 characterProfiles (对象) 或 styleGuide (文本字符串)
|
// 人物设定可能存储在 characterProfiles (对象) 或 styleGuide (文本字符串)
|
||||||
const characters = (rawCharacters && typeof rawCharacters === 'object') ? rawCharacters : {};
|
const characters = (rawCharacters && typeof rawCharacters === 'object') ? rawCharacters : {};
|
||||||
@ -86,7 +89,14 @@ export const ContextPanel: React.FC<ContextPanelProps> = ({
|
|||||||
<Paragraph ellipsis={{ rows: 6, expandable: true, symbol: '展开' }}>
|
<Paragraph ellipsis={{ rows: 6, expandable: true, symbol: '展开' }}>
|
||||||
{worldSetting}
|
{worldSetting}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Button type="dashed" block icon={<EditOutlined />}>更新设定</Button>
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
block
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={onNavigateToSettings}
|
||||||
|
>
|
||||||
|
更新设定
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -119,7 +129,35 @@ export const ContextPanel: React.FC<ContextPanelProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<Empty description="暂无人物设定" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
<Empty description="暂无人物设定" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
)}
|
)}
|
||||||
<Button type="dashed" block icon={<EditOutlined />} style={{ marginTop: '8px' }}>更新设定</Button>
|
<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>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -45,7 +45,8 @@ export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
|||||||
const [localMessages, setLocalMessages] = useState<{role: 'user' | 'agent', content: string}[]>([]);
|
const [localMessages, setLocalMessages] = useState<{role: 'user' | 'agent', content: string}[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatHistory.length > 0) {
|
// 确保 chatHistory 是数组
|
||||||
|
if (Array.isArray(chatHistory) && chatHistory.length > 0) {
|
||||||
setLocalMessages(chatHistory);
|
setLocalMessages(chatHistory);
|
||||||
} else if (localMessages.length === 0) {
|
} else if (localMessages.length === 0) {
|
||||||
setLocalMessages([{ role: 'agent', content: '导演你好,我是你的 AI 助手。' }]);
|
setLocalMessages([{ role: 'agent', content: '导演你好,我是你的 AI 助手。' }]);
|
||||||
@ -109,7 +110,7 @@ export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{agentPlan.length > 0 && (
|
{Array.isArray(agentPlan) && agentPlan.length > 0 && (
|
||||||
<Card size="small" title="当前执行计划" style={{ marginTop: '8px' }}>
|
<Card size="small" title="当前执行计划" style={{ marginTop: '8px' }}>
|
||||||
<ul style={{ paddingLeft: '20px', margin: 0 }}>
|
<ul style={{ paddingLeft: '20px', margin: 0 }}>
|
||||||
{agentPlan.map((step, idx) => (
|
{agentPlan.map((step, idx) => (
|
||||||
@ -123,8 +124,8 @@ export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
|||||||
{/* 导演信箱 (Inbox) */}
|
{/* 导演信箱 (Inbox) */}
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', background: '#f0f2f5' }}>
|
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', background: '#f0f2f5' }}>
|
||||||
<Divider orientation="left" style={{ margin: '0 0 16px 0', fontSize: '12px' }}>待处理任务 (Inbox)</Divider>
|
<Divider orientation="left" style={{ margin: '0 0 16px 0', fontSize: '12px' }}>待处理任务 (Inbox)</Divider>
|
||||||
|
|
||||||
{inboxItems.map(item => (
|
{Array.isArray(inboxItems) && inboxItems.map(item => (
|
||||||
<Card
|
<Card
|
||||||
key={item.id}
|
key={item.id}
|
||||||
size="small"
|
size="small"
|
||||||
@ -143,8 +144,8 @@ export const DirectorInbox: React.FC<DirectorInboxProps> = ({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<Divider orientation="left" style={{ margin: '16px 0', fontSize: '12px' }}>对话记录</Divider>
|
<Divider orientation="left" style={{ margin: '16px 0', fontSize: '12px' }}>对话记录</Divider>
|
||||||
|
|
||||||
{localMessages.map((msg, idx) => (
|
{Array.isArray(localMessages) && localMessages.map((msg, idx) => (
|
||||||
<div key={idx} style={{
|
<div key={idx} style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Layout, Typography, Spin, Empty, Button, Card, Tooltip, message, Modal } from 'antd';
|
import { Layout, Typography, Spin, Empty, Button, Card, Tooltip, message, Modal } from 'antd';
|
||||||
import { LoadingOutlined, WarningOutlined, SaveOutlined, EditOutlined, CheckOutlined } from '@ant-design/icons';
|
import { LoadingOutlined, WarningOutlined, SaveOutlined, EditOutlined, CheckOutlined, RobotOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
@ -12,8 +12,9 @@ interface SmartCanvasProps {
|
|||||||
onStartGenerate?: () => void;
|
onStartGenerate?: () => void;
|
||||||
onContentChange?: (content: string) => void;
|
onContentChange?: (content: string) => void;
|
||||||
onContentSave?: (content: string) => void;
|
onContentSave?: (content: string) => void;
|
||||||
|
onAIAssist?: (content: string) => void;
|
||||||
episodeTitle?: string;
|
episodeTitle?: string;
|
||||||
episodeNumber?: number;
|
episodeNumber?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SmartCanvas: React.FC<SmartCanvasProps> = ({
|
export const SmartCanvas: React.FC<SmartCanvasProps> = ({
|
||||||
@ -23,8 +24,9 @@ export const SmartCanvas: React.FC<SmartCanvasProps> = ({
|
|||||||
onStartGenerate,
|
onStartGenerate,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
onContentSave,
|
onContentSave,
|
||||||
|
onAIAssist,
|
||||||
episodeTitle = '未命名草稿',
|
episodeTitle = '未命名草稿',
|
||||||
episodeNumber = 5
|
episodeNumber = null
|
||||||
}) => {
|
}) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editContent, setEditContent] = useState(content);
|
const [editContent, setEditContent] = useState(content);
|
||||||
@ -32,6 +34,11 @@ export const SmartCanvas: React.FC<SmartCanvasProps> = ({
|
|||||||
const [selectedText, setSelectedText] = useState('');
|
const [selectedText, setSelectedText] = useState('');
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
// 显示标题处理
|
||||||
|
const displayTitle = episodeNumber !== null
|
||||||
|
? `第 ${episodeNumber} 集:${episodeTitle}`
|
||||||
|
: episodeTitle;
|
||||||
|
|
||||||
// Update editContent when content changes (e.g., from agent streaming)
|
// Update editContent when content changes (e.g., from agent streaming)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
@ -83,6 +90,13 @@ export const SmartCanvas: React.FC<SmartCanvasProps> = ({
|
|||||||
navigator.clipboard.writeText(selectedText);
|
navigator.clipboard.writeText(selectedText);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAIAssist = () => {
|
||||||
|
if (onAIAssist) {
|
||||||
|
onAIAssist(editContent || content);
|
||||||
|
message.loading('AI 正在辅助修改中...', 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Content style={{
|
<Content style={{
|
||||||
padding: '24px 48px',
|
padding: '24px 48px',
|
||||||
@ -95,12 +109,24 @@ export const SmartCanvas: React.FC<SmartCanvasProps> = ({
|
|||||||
}}>
|
}}>
|
||||||
<div style={{ flex: 1, maxWidth: '800px', margin: '0 auto' }}>
|
<div style={{ flex: 1, maxWidth: '800px', margin: '0 auto' }}>
|
||||||
<Title level={3} style={{ textAlign: 'center', marginBottom: '48px', color: '#333' }}>
|
<Title level={3} style={{ textAlign: 'center', marginBottom: '48px', color: '#333' }}>
|
||||||
第 {episodeNumber} 集:{episodeTitle}
|
{displayTitle}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
{!streaming && content && (
|
{!streaming && content && (
|
||||||
<div style={{ position: 'absolute', top: '24px', right: '24px', display: 'flex', gap: '8px' }}>
|
<div style={{ position: 'absolute', top: '24px', right: '24px', display: 'flex', gap: '8px' }}>
|
||||||
|
{isEditing && onAIAssist && (
|
||||||
|
<Tooltip title="AI 辅助修改(后台智能优化)">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
icon={<RobotOutlined />}
|
||||||
|
onClick={handleAIAssist}
|
||||||
|
>
|
||||||
|
AI 辅助
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip title={isEditing ? '保存编辑' : '编辑内容'}>
|
<Tooltip title={isEditing ? '保存编辑' : '编辑内容'}>
|
||||||
<Button
|
<Button
|
||||||
type={isEditing ? 'primary' : 'default'}
|
type={isEditing ? 'primary' : 'default'}
|
||||||
|
|||||||
@ -115,6 +115,7 @@ const SkillSelectorWithPrompt = ({
|
|||||||
export const ProjectDetail = () => {
|
export const ProjectDetail = () => {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = window.location // Or use useLocation from react-router-dom
|
||||||
|
|
||||||
// 调试:确认组件已加载
|
// 调试:确认组件已加载
|
||||||
console.log('=== ProjectDetail component loaded ===')
|
console.log('=== ProjectDetail component loaded ===')
|
||||||
@ -124,8 +125,18 @@ export const ProjectDetail = () => {
|
|||||||
const [executing, setExecuting] = useState(false)
|
const [executing, setExecuting] = useState(false)
|
||||||
const [currentEpisode, setCurrentEpisode] = useState<number>(1)
|
const [currentEpisode, setCurrentEpisode] = useState<number>(1)
|
||||||
const [selectedEpisode, setSelectedEpisode] = useState<Episode | null>(null)
|
const [selectedEpisode, setSelectedEpisode] = useState<Episode | null>(null)
|
||||||
|
|
||||||
|
// Use location state to set initial tab if provided
|
||||||
const [activeTab, setActiveTab] = useState('settings')
|
const [activeTab, setActiveTab] = useState('settings')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check for activeTab in history state (passed from navigate)
|
||||||
|
const state = (window.history.state?.usr as any);
|
||||||
|
if (state?.activeTab) {
|
||||||
|
setActiveTab(state.activeTab);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 项目设置相关状态
|
// 项目设置相关状态
|
||||||
const [settingsForm] = Form.useForm()
|
const [settingsForm] = Form.useForm()
|
||||||
const [updatingSettings, setUpdatingSettings] = useState(false)
|
const [updatingSettings, setUpdatingSettings] = useState(false)
|
||||||
@ -1475,6 +1486,7 @@ export const ProjectDetail = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
activeStates={activeStates}
|
activeStates={activeStates}
|
||||||
memoryItems={workspaceMemoryItems}
|
memoryItems={workspaceMemoryItems}
|
||||||
|
onNavigateToSettings={() => setActiveTab('global-generation')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 中间:Smart Canvas */}
|
{/* 中间:Smart Canvas */}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Button, Space, Tag, Card, message, Empty, Row, Col, Progress, Tooltip, Badge } from 'antd'
|
import { Button, Space, Tag, Card, message, Empty, Row, Col, Progress, Tooltip, Badge, Modal } from 'antd'
|
||||||
import {
|
import {
|
||||||
PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,
|
PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,
|
||||||
ClockCircleOutlined, CheckCircleOutlined, LoadingOutlined,
|
ClockCircleOutlined, CheckCircleOutlined, LoadingOutlined,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Layout, Button, Space, message, Spin, Typography, Tag } from 'antd'
|
|||||||
import { ArrowLeftOutlined, UnorderedListOutlined } from '@ant-design/icons'
|
import { ArrowLeftOutlined, UnorderedListOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { projectService } from '@/services/projectService'
|
import { projectService } from '@/services/projectService'
|
||||||
|
import { useProjectStore } from '@/stores/projectStore'
|
||||||
import { ContextPanel } from '@/components/Workspace/ContextPanel'
|
import { ContextPanel } from '@/components/Workspace/ContextPanel'
|
||||||
import { SmartCanvas } from '@/components/Workspace/SmartCanvas'
|
import { SmartCanvas } from '@/components/Workspace/SmartCanvas'
|
||||||
import { DirectorInbox } from '@/components/Workspace/DirectorInbox'
|
import { DirectorInbox } from '@/components/Workspace/DirectorInbox'
|
||||||
@ -53,7 +54,7 @@ export const ProjectWorkspace: React.FC = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const data = await projectService.getProject(projectId)
|
const data = await projectService.getProject(projectId)
|
||||||
@ -65,6 +66,20 @@ export const ProjectWorkspace: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载剧集内容
|
||||||
|
const loadEpisodeContent = async (episodeNumber: number) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
try {
|
||||||
|
const response = await projectService.getEpisode(projectId, episodeNumber);
|
||||||
|
const episode = response.data || response;
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
loadProject()
|
loadProject()
|
||||||
|
|
||||||
@ -267,6 +282,15 @@ export const ProjectWorkspace: React.FC = () => {
|
|||||||
case 'episode_saved':
|
case 'episode_saved':
|
||||||
// Agent 保存剧集后的确认
|
// Agent 保存剧集后的确认
|
||||||
message.success(`剧集 ${msg.episode_number || ''} 已自动保存`);
|
message.success(`剧集 ${msg.episode_number || ''} 已自动保存`);
|
||||||
|
// 刷新剧集列表
|
||||||
|
if (projectId) {
|
||||||
|
useProjectStore.getState().fetchEpisodes(projectId);
|
||||||
|
}
|
||||||
|
// 如果保存的是当前剧集,更新画布内容
|
||||||
|
if (currentEpisode && currentEpisode.number === msg.episode_number) {
|
||||||
|
// 重新获取剧集内容
|
||||||
|
loadEpisodeContent(currentEpisode.number);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'done':
|
case 'done':
|
||||||
setAgentStatus('idle');
|
setAgentStatus('idle');
|
||||||
@ -321,23 +345,37 @@ export const ProjectWorkspace: React.FC = () => {
|
|||||||
|
|
||||||
// 画布内容保存处理
|
// 画布内容保存处理
|
||||||
const handleContentSave = async (content: string) => {
|
const handleContentSave = async (content: string) => {
|
||||||
if (!currentEpisode || !currentEpisode.id) {
|
if (!currentEpisode || !projectId) {
|
||||||
message.warning('请先选择要保存的剧集');
|
message.warning('请先选择要保存的剧集');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用后端 API 保存剧集内容
|
// 调用后端 API 保存剧集内容
|
||||||
await projectService.updateEpisode(currentEpisode.id, {
|
await projectService.updateEpisode(projectId, currentEpisode.number, {
|
||||||
content: content,
|
content: content,
|
||||||
status: 'draft'
|
status: 'draft'
|
||||||
});
|
});
|
||||||
message.success('内容已保存');
|
message.success('内容已保存');
|
||||||
|
|
||||||
|
// 刷新剧集列表
|
||||||
|
useProjectStore.getState().fetchEpisodes(projectId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(`保存失败: ${(error as Error).message}`);
|
message.error(`保存失败: ${(error as Error).message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI 辅助修改处理
|
||||||
|
const handleAIAssist = async (content: string) => {
|
||||||
|
if (!currentEpisode || !projectId) {
|
||||||
|
message.warning('请先选择要修改的剧集');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过 WebSocket 发送 AI 辅助请求给 Agent
|
||||||
|
handleDirectorMessage(`请帮我优化改进当前剧集内容:\n\n${content.substring(0, 2000)}${content.length > 2000 ? '...' : ''}\n\n请直接使用 update_canvas 工具将优化后的内容更新到画布上。`);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px', textAlign: 'center', marginTop: '100px' }}>
|
<div style={{ padding: '24px', textAlign: 'center', marginTop: '100px' }}>
|
||||||
@ -402,11 +440,8 @@ export const ProjectWorkspace: React.FC = () => {
|
|||||||
<EpisodeSidebar
|
<EpisodeSidebar
|
||||||
projectId={projectId!}
|
projectId={projectId!}
|
||||||
onEpisodeSelect={(episode) => {
|
onEpisodeSelect={(episode) => {
|
||||||
setCurrentEpisode(episode)
|
// 使用 loadEpisodeContent 加载完整剧集内容
|
||||||
// 可以在这里更新画布内容显示剧集内容
|
loadEpisodeContent(episode.number);
|
||||||
if (episode.content) {
|
|
||||||
setCanvasContent(episode.content)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
currentEpisodeId={currentEpisode?.id}
|
currentEpisodeId={currentEpisode?.id}
|
||||||
/>
|
/>
|
||||||
@ -419,6 +454,7 @@ export const ProjectWorkspace: React.FC = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
activeStates={activeStates}
|
activeStates={activeStates}
|
||||||
memoryItems={memoryItems}
|
memoryItems={memoryItems}
|
||||||
|
onNavigateToSettings={() => navigate(`/projects/${projectId}`, { state: { activeTab: 'global-generation' } })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 中间:Smart Canvas */}
|
{/* 中间:Smart Canvas */}
|
||||||
@ -427,13 +463,14 @@ export const ProjectWorkspace: React.FC = () => {
|
|||||||
content={canvasContent}
|
content={canvasContent}
|
||||||
streaming={streaming}
|
streaming={streaming}
|
||||||
annotations={annotations}
|
annotations={annotations}
|
||||||
episodeTitle={currentEpisode?.title || '未命名草稿'}
|
episodeTitle={currentEpisode?.title || (currentEpisode?.number ? `第${currentEpisode.number}集` : '未命名草稿')}
|
||||||
episodeNumber={currentEpisode?.number || 5}
|
episodeNumber={currentEpisode?.number || null}
|
||||||
onStartGenerate={() => {
|
onStartGenerate={() => {
|
||||||
handleDirectorMessage('开始生成大纲');
|
handleDirectorMessage('开始生成大纲');
|
||||||
}}
|
}}
|
||||||
onContentChange={handleContentChange}
|
onContentChange={handleContentChange}
|
||||||
onContentSave={handleContentSave}
|
onContentSave={handleContentSave}
|
||||||
|
onAIAssist={handleAIAssist}
|
||||||
/>
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,8 @@ export interface SeriesProject {
|
|||||||
episodeSkillOverrides: Record<number, EpisodeSkillOverride>
|
episodeSkillOverrides: Record<number, EpisodeSkillOverride>
|
||||||
// 保留兼容
|
// 保留兼容
|
||||||
skillSettings: Record<string, any>
|
skillSettings: Record<string, any>
|
||||||
|
// 剧集列表(用于计算完成度)
|
||||||
|
episodes?: Episode[]
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
@ -133,8 +135,8 @@ export const projectService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 更新剧集内容
|
// 更新剧集内容
|
||||||
updateEpisode: async (episodeId: string, data: Partial<Episode>) => {
|
updateEpisode: async (projectId: string, episodeNumber: number, data: Partial<Episode>) => {
|
||||||
return await api.put<Episode>(`/episodes/${episodeId}`, data)
|
return await api.put<Episode>(`/projects/${projectId}/episodes/${episodeNumber}`, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 执行单集创作
|
// 执行单集创作
|
||||||
|
|||||||
@ -35,7 +35,19 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
set({ loading: true, error: null })
|
set({ loading: true, error: null })
|
||||||
try {
|
try {
|
||||||
const projects = await projectService.listProjects()
|
const projects = await projectService.listProjects()
|
||||||
set({ projects, loading: false })
|
// 为每个项目获取剧集数据,用于计算完成度
|
||||||
|
const projectsWithEpisodes = await Promise.all(
|
||||||
|
projects.map(async (project) => {
|
||||||
|
try {
|
||||||
|
const episodes = await projectService.listEpisodes(project.id)
|
||||||
|
return { ...project, episodes }
|
||||||
|
} catch {
|
||||||
|
// 如果获取剧集失败,返回空剧集数组
|
||||||
|
return { ...project, episodes: [] }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
set({ projects: projectsWithEpisodes, loading: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: (error as Error).message, loading: false })
|
set({ error: (error as Error).message, loading: false })
|
||||||
}
|
}
|
||||||
|
|||||||
12
test.md
12
test.md
@ -1,8 +1,10 @@
|
|||||||
# 我的python 环境是"C:\ProgramData\Anaconda3\envs\creative_studio\python.exe"
|
# 我的python 环境是"C:\ProgramData\Anaconda3\envs\creative_studio\python.exe"
|
||||||
## 1
|
## 1
|
||||||
|
~~剧集创作的草稿区一直显示未命名草稿,这里需要修改为剧集名称,和剧集管理列表的剧集名称同步。~~ ✓ 已修复:修改 ProjectWorkspace.tsx 第461行,使用剧集编号作为默认标题
|
||||||
## 2
|
## 2
|
||||||
页面上的人物、初始状态这些为什么是无内容,没有从项目设置和全局设定中同步过来,同时这个世界观和人物在剧集创作界面都不能进行修改了,而是只能从前一页内容中进行同步过来。另外,这些设定都有没有正确注入项目agent的前置信息里面?
|
~~剧集创作这里查看每一集内容在草稿区之后需要配置类似全局设定生成那里的ai辅助修改按钮(同样支持配置skill),支持注入agent辅助修改。(用户看不到注入内容,后台注入修改)~~ ✓ 已修复:在 SmartCanvas 组件添加 AI 辅助按钮,通过 WebSocket 向 Agent 发送优化请求
|
||||||
## 3
|
## 3
|
||||||
关于这个创建项目,删除按钮有没有确认的流程?如果没有需要添加上,确认后才会删除项目;关于项目完成度应该按照剧集制作完成度来计算显示。
|
~~现在的剧集创作第一集是生成完成的状态,但内容一直显示第1集内容创作失败: 'str' object has no attribute 'items',这里要检查一下开始创作是不是真正走的agent流程?~~ ✓ 已修复:在 websocket.py 第325-338行添加类型检查,确保 characterProfiles 是字典类型
|
||||||
|
## 4
|
||||||
|
~~现在的剧集创作故事上下文和记忆库都没有同步进行更新,这块需要进行检查和实现。~~ ✓ 已修复:在 websocket.py save_episode 和 update_episode 处理器中添加自动记忆更新功能
|
||||||
|
记忆库的更新,故事上下文的更新需要在每一集创作完成后进行(agent自动更新)。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user