feat:优化开发剧集创作部分

This commit is contained in:
hjjjj 2026-01-28 11:18:53 +08:00
parent c99f66895b
commit 5b0f8833ba
22 changed files with 1413 additions and 162 deletions

View File

@ -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, {

View File

@ -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
})
# ============================================ # ============================================
# 辅助函数 - 用于从其他模块发送消息 # 辅助函数 - 用于从其他模块发送消息
# ============================================ # ============================================

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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
] ]

View File

@ -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)

View File

@ -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

View File

@ -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,12 +85,36 @@ 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:
"""创建新项目""" """创建新项目"""
project_id = str(uuid.uuid4()) project_id = str(uuid.uuid4())
@ -165,12 +189,31 @@ 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:
"""创建剧集""" """创建剧集"""
if not episode.id: if not episode.id:
@ -220,7 +263,24 @@ 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):
"""添加消息""" """添加消息"""

516
backend/data/episodes.json Normal file
View 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
}
}

View 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"
}
]
}

View File

@ -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"
} }
} }

View File

@ -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>
</> </>
), ),
}, },

View File

@ -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) => (
@ -124,7 +125,7 @@ export const DirectorInbox: React.FC<DirectorInboxProps> = ({
<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"
@ -144,7 +145,7 @@ 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',

View File

@ -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'}

View File

@ -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 */}

View File

@ -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,

View File

@ -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'
@ -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>

View File

@ -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)
}, },
// 执行单集创作 // 执行单集创作

View File

@ -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 })
} }

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

10
test.md
View File

@ -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自动更新