diff --git a/backend/app/api/v1/projects.py b/backend/app/api/v1/projects.py index 8505e10..1d1ab5c 100644 --- a/backend/app/api/v1/projects.py +++ b/backend/app/api/v1/projects.py @@ -37,9 +37,25 @@ router = APIRouter(prefix="/projects", tags=["项目管理"]) @router.post("/", response_model=SeriesProject, status_code=status.HTTP_201_CREATED) async def create_project(project_data: SeriesProjectCreate): - """创建新项目""" + """创建新项目并自动生成剧集记录""" try: + # 创建项目 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 except Exception as 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]) async def list_episodes(project_id: str): - """列出项目的所有剧集""" + """列出项目的所有剧集,如果为空则自动初始化""" # 先验证项目存在 project = await project_repo.get(project_id) if not project: @@ -108,6 +124,29 @@ async def list_episodes(project_id: str): ) 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 @@ -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}") + + # 先获取现有剧集记录,避免重复创建 + 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( project=project, episode_number=request.episodeNumber, title=request.title ) - # 保存剧集 - episode.projectId = project_id - await episode_repo.create(episode) + # 保持原有的 ID 如果记录已存在 + if episode_record: + 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, { diff --git a/backend/app/api/v1/websocket.py b/backend/app/api/v1/websocket.py index 0db053f..e24b8e2 100644 --- a/backend/app/api/v1/websocket.py +++ b/backend/app/api/v1/websocket.py @@ -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( websocket: WebSocket, 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": # 用户在 Inbox 中点击批准或拒绝 action = message.get("action") @@ -302,6 +454,9 @@ async def _handle_client_message( feedback = f"User {action}ed inbox item {item_id}." agent = manager.get_agent(project_id, project_dir) + # 确保上下文已加载 + await _ensure_agent_context(agent, project_id) + try: for event in agent.stream_events(feedback, thread_id=project_id): # 同样的事件处理逻辑 @@ -317,6 +472,9 @@ async def _handle_client_message( elif message_type == "chat_message": # 用户发送聊天消息 -> 触发 Agent 执行 content = message.get("content", "") + episode_number = message.get("episodeNumber") + episode_title = message.get("episodeTitle") + if not content: return @@ -326,56 +484,8 @@ async def _handle_client_message( # 获取 Agent agent = manager.get_agent(project_id, project_dir) - # 加载项目上下文并更新 Agent(如果尚未加载) - if agent.context and not agent.context.project_id: - 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}") + # 确保上下文已加载(包含当前剧集信息) + await _ensure_agent_context(agent, project_id, episode_number, episode_title) # 异步运行 Agent 并将事件流推送到前端 full_response = "" @@ -541,9 +651,48 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]): }) elif name == "save_episode": - # 处理剧集保存 - episode_number = args.get("episode_number", 0) - title = args.get("title", "") + # 处理剧集保存并持久化 + episode_number = args.get("episode_number") + title = args.get("title") + content = args.get("content") + outline = args.get("outline") + + if episode_number: + try: + from app.db.repositories import episode_repo, project_repo + from app.core.memory.memory_manager import 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, { "type": "episode_saved", @@ -551,6 +700,64 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]): "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 + }) + # ============================================ # 辅助函数 - 用于从其他模块发送消息 # ============================================ diff --git a/backend/app/core/agent_runtime/agent.py b/backend/app/core/agent_runtime/agent.py index 43d78c4..d4b6fe1 100644 --- a/backend/app/core/agent_runtime/agent.py +++ b/backend/app/core/agent_runtime/agent.py @@ -60,11 +60,16 @@ class LangChainSkillsAgent: self.working_directory = working_directory or Path.cwd() self.skill_loader = SkillLoader(skill_paths) - self.system_prompt = self._build_system_prompt() self.context = SkillAgentContext( skill_loader=self.skill_loader, 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() 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, system_prompt=self.system_prompt, context_schema=SkillAgentContext, - checkpointer=InMemorySaver(), + checkpointer=self.checkpointer, ) 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, system_prompt=self.system_prompt, context_schema=SkillAgentContext, - checkpointer=InMemorySaver(), + checkpointer=self.checkpointer, ) return agent diff --git a/backend/app/core/agent_runtime/context.py b/backend/app/core/agent_runtime/context.py index 1be6bf4..893e390 100644 --- a/backend/app/core/agent_runtime/context.py +++ b/backend/app/core/agent_runtime/context.py @@ -28,5 +28,12 @@ class SkillAgentContext: creation_mode: Optional[str] = None # 'script' or 'inspiration' 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 user_skills: List[Dict[str, Any]] = field(default_factory=list) diff --git a/backend/app/core/agent_runtime/director_agent.py b/backend/app/core/agent_runtime/director_agent.py index 8191c0b..2b46981 100644 --- a/backend/app/core/agent_runtime/director_agent.py +++ b/backend/app/core/agent_runtime/director_agent.py @@ -39,11 +39,25 @@ class DirectorAgent(LangChainSkillsAgent): 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 if project_context: self.context = project_context - # 重新构建 system prompt + # 重新构建 system prompt 并刷新 agent 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: # 基础 prompt @@ -52,18 +66,23 @@ The User is the Director. Your goal is to help the Director create high-quality ## Your Role - **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. ## Workflow Protocols 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')`. 2. **Execution & Writing** - - Use `write_file` to generate content. - - Use `update_canvas` (or write to the active file) to show progress. + - Use `list_episodes` to see the current progress of the project. + - Use `focus_episode` to navigate between episodes. This will update the user's view. + - **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')`. 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 += f"**Project**: {self.context.project_name}\n" + base_prompt += f"**Project**: {context.project_name}\n" - if self.context.project_genre: - base_prompt += f"**Genre**: {self.context.project_genre}\n" + if context.project_genre: + base_prompt += f"**Genre**: {context.project_genre}\n" - if self.context.total_episodes: - base_prompt += f"**Total Episodes**: {self.context.total_episodes}\n" + if context.total_episodes: + base_prompt += f"**Total Episodes**: {context.total_episodes}\n" base_prompt += "\n### Global Settings\n\n" - if self.context.world_setting: - base_prompt += f"**World Setting**:\n{self.context.world_setting}\n\n" + if context.world_setting: + base_prompt += f"**World Setting**:\n{context.world_setting}\n\n" - if self.context.characters: - base_prompt += f"**Characters**:\n{self.context.characters}\n\n" + if context.characters: + base_prompt += f"**Characters**:\n{context.characters}\n\n" - if self.context.overall_outline: - base_prompt += f"**Overall Outline**:\n{self.context.overall_outline}\n\n" + if context.overall_outline: + base_prompt += f"**Overall Outline**:\n{context.overall_outline}\n\n" - if self.context.source_content: - mode = "剧本改编" if self.context.creation_mode == "script" else "创意灵感" - base_prompt += f"**Source** ({mode}):\n{self.context.source_content[:1000]}...\n\n" + if context.source_content: + mode = "剧本改编" if context.creation_mode == "script" else "创意灵感" + 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 = self.skill_loader.build_system_prompt("") # 添加用户配置的 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 += "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 += "\nUse `load_skill` to load detailed instructions for these skills when relevant.\n" diff --git a/backend/app/core/agent_runtime/director_tools.py b/backend/app/core/agent_runtime/director_tools.py index a38b318..201ab9b 100644 --- a/backend/app/core/agent_runtime/director_tools.py +++ b/backend/app/core/agent_runtime/director_tools.py @@ -462,6 +462,129 @@ Content: {content[:100]}{'...' if len(content) > 100 else ''} 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 = [ update_plan, @@ -471,5 +594,8 @@ DIRECTOR_TOOLS = [ write_to_canvas, save_episode, update_memory, - request_review + request_review, + focus_episode, + list_episodes, + update_episode ] diff --git a/backend/app/core/agents/series_creation_agent.py b/backend/app/core/agents/series_creation_agent.py index 2d72111..e4e40cf 100644 --- a/backend/app/core/agents/series_creation_agent.py +++ b/backend/app/core/agents/series_creation_agent.py @@ -42,45 +42,51 @@ class SeriesCreationAgent: ) -> Episode: """ 执行单集创作 - - Args: - project: 剧集项目 - episode_number: 集数 - title: 集标题(可选) - - Returns: - 创作的 Episode 对象 """ 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( projectId=project.id, number=episode_number, - title=title or f"第{episode_number}集", + title=title or (existing_episode.title if existing_episode else f"第{episode_number}集"), status="writing" ) + if existing_episode: + episode.id = existing_episode.id + episode.outline = existing_episode.outline + episode.content = existing_episode.content try: # ============================================ # 阶段 1: 结构分析 # ============================================ - logger.info(f"EP{episode_number} - 阶段 1: 结构分析") - structure = await self._analyze_structure(project, episode_number) - episode.structure = structure + if not episode.structure: + logger.info(f"EP{episode_number} - 阶段 1: 结构分析") + structure = await self._analyze_structure(project, episode_number) + episode.structure = structure # ============================================ # 阶段 2: 大纲生成 + # 如果已有大纲且非空,则跳过 # ============================================ - logger.info(f"EP{episode_number} - 阶段 2: 大纲生成") - outline = await self._generate_outline(project, episode_number, structure) - episode.outline = outline + if not episode.outline or len(episode.outline.strip()) < 10: + logger.info(f"EP{episode_number} - 阶段 2: 大纲生成") + outline = await self._generate_outline(project, episode_number, episode.structure) + episode.outline = outline + else: + 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 # ============================================ @@ -103,8 +109,9 @@ class SeriesCreationAgent: logger.info(f"EP{episode_number} 创作完成,质量分数: {episode.qualityScore}") 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.content = f"第{episode_number}集内容创作失败: {str(e)}" episode.issues = [ EpisodeIssue( type="execution_error", @@ -349,22 +356,39 @@ class SeriesCreationAgent: """构建上下文字符串""" context_parts = [] - # 世界观 - if project.globalContext.worldSetting: - context_parts.append(f"世界观:{project.globalContext.worldSetting}") + # 1. 世界观 (防御性检查) + world_setting = getattr(project.globalContext, 'worldSetting', '') + if world_setting: + context_parts.append(f"世界观:{world_setting}") - # 人物设定 - if project.globalContext.characterProfiles: + # 2. 人物设定 (防御性检查) + profiles = getattr(project.globalContext, 'characterProfiles', {}) + if isinstance(profiles, dict) and profiles: context_parts.append("\n人物设定:") - for char_id, char in project.globalContext.characterProfiles.items(): - context_parts.append(f"- {char.name}:{char.personality},说话风格:{char.speechStyle}") + for char_id, char in profiles.items(): + 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集) - if project.memory.eventTimeline: + # 3. 风格指南 + 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历史剧情:") - recent_events = project.memory.eventTimeline[-3:] + recent_events = memory_timeline[-3:] 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) diff --git a/backend/app/core/memory/memory_manager.py b/backend/app/core/memory/memory_manager.py index 92589b0..8c476ea 100644 --- a/backend/app/core/memory/memory_manager.py +++ b/backend/app/core/memory/memory_manager.py @@ -878,27 +878,48 @@ class MemoryManager: def _convert_to_enhanced_memory(self, memory: 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( eventTimeline=[ 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=[ 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={ - char: [ - CharacterStateChange(**state) if isinstance(state, dict) else state - for state in states - ] - for char, states in memory.characterStates.items() - }, + characterStates=char_states, foreshadowing=[ 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=[], last_updated=datetime.now(), last_episode_processed=0 diff --git a/backend/app/db/repositories.py b/backend/app/db/repositories.py index 9ec2112..f36848d 100644 --- a/backend/app/db/repositories.py +++ b/backend/app/db/repositories.py @@ -64,7 +64,7 @@ class JsonRepository: for k, v in self._data.items(): if hasattr(v, "dict"): serialized_data[k] = json.loads(v.json()) - elif isinstance(v, dict): + elif isinstance(v, (dict, list)): serialized_data[k] = v else: serialized_data[k] = str(v) @@ -85,11 +85,35 @@ class ProjectRepository(JsonRepository): super().__init__(PROJECTS_FILE) # 将加载的字典转换为对象 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: + # 修复可能存在的字符串化数据 + 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) except Exception as e: logger.error(f"Failed to parse project {k}: {e}") + + if is_dirty: + self._save() async def create(self, project_data: SeriesProjectCreate) -> SeriesProject: """创建新项目""" @@ -165,11 +189,30 @@ class EpisodeRepository(JsonRepository): def __init__(self): super().__init__(EPISODES_FILE) 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: - 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: 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: """创建剧集""" @@ -220,8 +263,25 @@ class MessageRepository(JsonRepository): def __init__(self): 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): """添加消息""" if project_id not in self._data: diff --git a/backend/data/episodes.json b/backend/data/episodes.json new file mode 100644 index 0000000..70b05d2 --- /dev/null +++ b/backend/data/episodes.json @@ -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 + } +} \ No newline at end of file diff --git a/backend/data/messages.json b/backend/data/messages.json new file mode 100644 index 0000000..2cbce01 --- /dev/null +++ b/backend/data/messages.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/backend/data/projects.json b/backend/data/projects.json index 3a71ab7..8d5776d 100644 --- a/backend/data/projects.json +++ b/backend/data/projects.json @@ -28,6 +28,6 @@ "autoRetryConfig": null, "reviewConfig": null, "createdAt": "2026-01-27T16:22:58.755260", - "updatedAt": "2026-01-27T18:11:56.500700" + "updatedAt": "2026-01-28T10:49:27.517654" } } \ No newline at end of file diff --git a/frontend/src/components/Workspace/ContextPanel.tsx b/frontend/src/components/Workspace/ContextPanel.tsx index a87d9de..4f5612b 100644 --- a/frontend/src/components/Workspace/ContextPanel.tsx +++ b/frontend/src/components/Workspace/ContextPanel.tsx @@ -21,18 +21,21 @@ interface ContextPanelProps { activeStates?: any[]; memoryItems?: any[]; onUpdateContext?: (type: string, data: any) => void; + onNavigateToSettings?: () => void; } export const ContextPanel: React.FC = ({ project, loading, activeStates = [], - memoryItems = [] + memoryItems = [], + onNavigateToSettings }) => { const [activeTab, setActiveTab] = useState('world'); // 模拟数据 - 实际应从 project.globalContext 获取 const worldSetting = project?.globalContext?.worldSetting || "暂无世界观设定"; + const overallOutline = project?.globalContext?.overallOutline || "暂无整体大纲设定"; const rawCharacters = project?.globalContext?.characterProfiles; // 人物设定可能存储在 characterProfiles (对象) 或 styleGuide (文本字符串) const characters = (rawCharacters && typeof rawCharacters === 'object') ? rawCharacters : {}; @@ -86,7 +89,14 @@ export const ContextPanel: React.FC = ({ {worldSetting} - + ), }, @@ -119,7 +129,35 @@ export const ContextPanel: React.FC = ({ ) : ( )} - + + + ), + }, + { + key: 'outline', + label: '大纲', + children: ( + <> + + {overallOutline} + + ), }, diff --git a/frontend/src/components/Workspace/DirectorInbox.tsx b/frontend/src/components/Workspace/DirectorInbox.tsx index 85e6181..cb44601 100644 --- a/frontend/src/components/Workspace/DirectorInbox.tsx +++ b/frontend/src/components/Workspace/DirectorInbox.tsx @@ -45,7 +45,8 @@ export const DirectorInbox: React.FC = ({ const [localMessages, setLocalMessages] = useState<{role: 'user' | 'agent', content: string}[]>([]); useEffect(() => { - if (chatHistory.length > 0) { + // 确保 chatHistory 是数组 + if (Array.isArray(chatHistory) && chatHistory.length > 0) { setLocalMessages(chatHistory); } else if (localMessages.length === 0) { setLocalMessages([{ role: 'agent', content: '导演你好,我是你的 AI 助手。' }]); @@ -109,7 +110,7 @@ export const DirectorInbox: React.FC = ({ - {agentPlan.length > 0 && ( + {Array.isArray(agentPlan) && agentPlan.length > 0 && (
    {agentPlan.map((step, idx) => ( @@ -123,8 +124,8 @@ export const DirectorInbox: React.FC = ({ {/* 导演信箱 (Inbox) */}
    待处理任务 (Inbox) - - {inboxItems.map(item => ( + + {Array.isArray(inboxItems) && inboxItems.map(item => ( = ({ ))} 对话记录 - - {localMessages.map((msg, idx) => ( + + {Array.isArray(localMessages) && localMessages.map((msg, idx) => (
    void; onContentChange?: (content: string) => void; onContentSave?: (content: string) => void; + onAIAssist?: (content: string) => void; episodeTitle?: string; - episodeNumber?: number; + episodeNumber?: number | null; } export const SmartCanvas: React.FC = ({ @@ -23,8 +24,9 @@ export const SmartCanvas: React.FC = ({ onStartGenerate, onContentChange, onContentSave, + onAIAssist, episodeTitle = '未命名草稿', - episodeNumber = 5 + episodeNumber = null }) => { const [isEditing, setIsEditing] = useState(false); const [editContent, setEditContent] = useState(content); @@ -32,6 +34,11 @@ export const SmartCanvas: React.FC = ({ const [selectedText, setSelectedText] = useState(''); const textareaRef = useRef(null); + // 显示标题处理 + const displayTitle = episodeNumber !== null + ? `第 ${episodeNumber} 集:${episodeTitle}` + : episodeTitle; + // Update editContent when content changes (e.g., from agent streaming) useEffect(() => { if (!isEditing) { @@ -83,6 +90,13 @@ export const SmartCanvas: React.FC = ({ navigator.clipboard.writeText(selectedText); }; + const handleAIAssist = () => { + if (onAIAssist) { + onAIAssist(editContent || content); + message.loading('AI 正在辅助修改中...', 0); + } + }; + return ( = ({ }}>
    - 第 {episodeNumber} 集:{episodeTitle} + {displayTitle} {/* 操作按钮 */} {!streaming && content && (
    + {isEditing && onAIAssist && ( + + + + )}