"""
用户主导的任务系统
版本: 3.3.0
功能: 支持分步执行、结果修改、图/文生视频切换、采集入库、字幕添加、视频拼接
"""

import os
import json
import uuid
import time
import logging
import subprocess
import tempfile
from datetime import datetime
from typing import Dict, List, Optional, Any
from pathlib import Path

logger = logging.getLogger(__name__)

# 导入简化的真实Agent数据生成
from simple_real_agent import SimpleRealAgent

# 导入真实API客户端
from real_api_client import generate_video, generate_video_from_image, generate_image, generate_text, stitch_videos, DEFAULT_VIDEO_MODEL, DEFAULT_IMAGE_MODEL

class UserDrivenTaskSystem:
    """用户主导的任务系统 - 支持分步控制和结果修改"""

    def __init__(self, base_dir: str = "/root/data/disk/projects"):
        """
        初始化用户主导任务系统

        Args:
            base_dir: 任务数据存储基础目录
        """
        self.base_dir = Path(base_dir)
        self.tasks_dir = self.base_dir / "tasks"
        self.assets_dir = self.base_dir / "assets"

        # 确保目录存在
        self.tasks_dir.mkdir(parents=True, exist_ok=True)
        self.assets_dir.mkdir(parents=True, exist_ok=True)

        # 任务状态定义
        self.STATUS = {
            "CREATED": "created",
            "WAITING_COLLECT": "waiting_collect",
            "COLLECTING": "collecting",
            "COLLECTED": "collected",
            "WAITING_WRITE": "waiting_write",
            "WRITING": "writing",
            "WRITTEN": "written",
            "WAITING_PRODUCE": "waiting_produce",
            "PRODUCING": "producing",
            "PRODUCED": "produced",
            "WAITING_REVIEW": "waiting_review",
            "REVIEWING": "reviewing",
            "REVIEWED": "reviewed",
            "COMPLETED": "completed",
            "PAUSED": "paused",
            "MODIFYING": "modifying",
            "REGENERATING": "regenerating"
        }

        # Agent定义
        self.AGENTS = {
            "collector": {
                "name": "采集官",
                "description": "负责热点素材采集",
                "weight": 0.16,
                "color": "warning",
                "can_regenerate": True
            },
            "writer": {
                "name": "创作官",
                "description": "负责文案创作和脚本编写",
                "weight": 0.24,
                "color": "info",
                "can_modify": True,
                "can_regenerate": True
            },
            "producer": {
                "name": "制作官",
                "description": "负责视频生成和制作",
                "weight": 0.20,
                "color": "primary",
                "can_regenerate": True
            },
            "publisher": {
                "name": "审核官",
                "description": "负责内容审核和质量控制",
                "weight": 0.13,
                "color": "success",
                "can_modify": True,
                "can_regenerate": True
            }
        }

    def create_task(self, description: str, user_id: str = "default") -> Dict[str, Any]:
        """创建新任务"""
        task_id = f"task_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
        task_dir = self.tasks_dir / task_id
        assets_dir = self.assets_dir / task_id

        task_dir.mkdir(parents=True, exist_ok=True)
        assets_dir.mkdir(parents=True, exist_ok=True)

        task_data = {
            "task_id": task_id,
            "description": description,
            "user_id": user_id,
            "status": self.STATUS["CREATED"],
            "created_at": datetime.now().isoformat(),
            "updated_at": datetime.now().isoformat(),
            "current_step": "collect",
            "progress": 0,
            "user_actions": [],
            "agents": {},
            "results": {
                "collection": None,
                "script": None,
                "storyboard": None,
                "characters": None,
                "backgrounds": None,
                "video": None,
                "review": None
            },
            "modifications": {
                "collection": [],
                "script": [],
                "storyboard": [],
                "characters": [],
                "backgrounds": [],
                "video": [],
                "review": []
            },
            "files": {
                "task_dir": str(task_dir),
                "assets_dir": str(assets_dir),
                "collection_file": None,
                "script_file": None,
                "storyboard_file": None,
                "characters_file": None,
                "backgrounds_file": None,
                "video_file": None,
                "review_file": None
            }
        }

        # 保存任务数据
        task_file = task_dir / "task.json"
        with open(task_file, 'w', encoding='utf-8') as f:
            json.dump(task_data, f, ensure_ascii=False, indent=2)

        return task_data

    def get_next_step(self, task_id: str) -> str:
        """获取下一个可执行的步骤"""
        task_data = self._load_task(task_id)
        if not task_data:
            return "error"

        current_step = task_data.get("current_step", "collect")
        status = task_data.get("status", "")

        # 根据当前步骤和状态决定下一步
        steps = ["collect", "write", "produce", "review"]

        if status == self.STATUS["CREATED"]:
            return "collect"
        elif status == self.STATUS["COLLECTED"]:
            return "write"
        elif status == self.STATUS["WRITTEN"]:
            return "produce"
        elif status == self.STATUS["PRODUCED"]:
            return "review"
        elif status == self.STATUS["REVIEWED"]:
            return "complete"
        else:
            # 如果当前步骤已完成，找下一个未完成的步骤
            for i, step in enumerate(steps):
                if step == current_step:
                    if i + 1 < len(steps):
                        return steps[i + 1]
            return "complete"

    def can_execute_step(self, task_id: str, step: str) -> bool:
        """检查是否可以执行指定步骤"""
        task_data = self._load_task(task_id)
        if not task_data:
            return False

        status = task_data.get("status", "")

        step_status_map = {
            "collect": [self.STATUS["CREATED"], self.STATUS["WAITING_COLLECT"]],
            "write": [self.STATUS["COLLECTED"], self.STATUS["WAITING_WRITE"]],
            "produce": [self.STATUS["WRITTEN"], self.STATUS["WAITING_PRODUCE"]],
            "review": [self.STATUS["PRODUCED"], self.STATUS["WAITING_REVIEW"]]
        }

        return status in step_status_map.get(step, [])

    def execute_collect(self, task_id: str, user_input: Optional[Dict] = None) -> Dict[str, Any]:
        """执行采集步骤 - 支持修改意见重新生成，优先查询已入库素材"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        if not self.can_execute_step(task_id, "collect"):
            return {"success": False, "message": "当前不能执行采集步骤"}

        task_data["status"] = self.STATUS["COLLECTING"]
        task_data["current_step"] = "collect"
        task_data["progress"] = 20
        task_data["updated_at"] = datetime.now().isoformat()

        # 记录用户动作
        action_desc = "用户执行采集步骤"
        if user_input and user_input.get("modification"):
            action_desc = f"用户修改采集要求并重新采集: {user_input['modification']}"
        task_data["user_actions"].append({
            "action": "execute_collect",
            "timestamp": datetime.now().isoformat(),
            "description": action_desc
        })

        task_description = task_data.get("description", "测试任务")

        # 优先查询已采集入库的素材库
        material_db = self._load_material_db()
        if material_db and not user_input:
            # 检查是否有相关主题的已入库素材
            for key, entry in material_db.items():
                if task_description[:10] in key or key in task_description:
                    logger.info(f"找到已入库的采集素材: {key}")
                    collection_result = entry["data"]
                    collection_result["from_db"] = True
                    collection_result["db_key"] = key
                    break

        if 'collection_result' not in dir() or collection_result is None:
            # 使用真实LLM进行采集分析
            user_preferences = user_input.get("preferences", {}) if user_input else {}
            extra_instruction = ""
            if user_input and user_input.get("modification"):
                extra_instruction = f"\n用户修改意见: {user_input['modification']}"

            try:
                analysis = generate_text(
                    system_prompt="""你是一个专业的内容采集分析师。基于给定的主题，提供热点趋势、受众分析、视觉风格建议。
严格输出JSON格式：
{"hot_trends":[{"topic":"话题","heat":"热度","trend":"上升/稳定/下降"}],"audience_analysis":{"target":"目标人群","age_range":"年龄段","interests":["兴趣点"],"platforms":["推荐平台"]},"visual_style":{"color_tone":"色调建议","mood":"氛围","reference":"参考作品"},"content_suggestions":["内容方向1","内容方向2"]}
只输出JSON，不要其他文字。""",
                    prompt=f"请为以下主题提供采集分析：{task_description}{extra_instruction}",
                    temperature=0.8,
                    max_tokens=2048,
                )
                # 解析JSON
                try:
                    if "```json" in analysis:
                        json_str = analysis.split("```json")[1].split("```")[0].strip()
                    elif "{" in analysis:
                        json_str = analysis[analysis.index("{"):]
                    else:
                        json_str = analysis
                    parsed = json.loads(json_str)
                    collection_result = {
                        "status": "succeeded",
                        "task_description": task_description,
                        "analysis": json.dumps(parsed, ensure_ascii=False, indent=2),
                        "parsed": parsed,
                        "is_real_data": True,
                        "from_db": False,
                        "generated_at": datetime.now().isoformat()
                    }
                except (json.JSONDecodeError, ValueError):
                    collection_result = {
                        "status": "succeeded",
                        "task_description": task_description,
                        "analysis": analysis,
                        "is_real_data": True,
                        "from_db": False,
                        "generated_at": datetime.now().isoformat()
                    }
            except Exception as e:
                logger.warning(f"LLM采集失败，降级到基础采集: {e}")
                collection_result = SimpleRealAgent.generate_collection_result(task_description)
                collection_result["is_real_data"] = False
                collection_result["fallback_reason"] = str(e)
                collection_result["from_db"] = False

            # 结构化入库
            self._save_to_material_db(task_description, collection_result)

        # 保存采集结果
        task_dir = Path(task_data["files"]["task_dir"])
        collection_file = task_dir / "collection_result.json"
        with open(collection_file, 'w', encoding='utf-8') as f:
            json.dump(collection_result, f, ensure_ascii=False, indent=2)

        # 更新任务数据
        task_data["results"]["collection"] = collection_result
        task_data["files"]["collection_file"] = str(collection_file)
        task_data["agents"]["collector"] = {
            "status": "completed",
            "started_at": datetime.now().isoformat(),
            "completed_at": datetime.now().isoformat(),
            "result_summary": "采集完成",
            "can_modify": True,
            "can_regenerate": True
        }
        task_data["status"] = self.STATUS["COLLECTED"]
        task_data["current_step"] = "write"
        task_data["progress"] = 30

        self._save_task(task_id, task_data)

        return {
            "success": True,
            "message": "采集步骤完成" + (" (来自素材库)" if collection_result.get("from_db") else ""),
            "task_id": task_id,
            "result": collection_result,
            "next_step": "write",
            "next_step_description": "下一步：创作分镜脚本",
            "can_modify": True,
            "can_regenerate": True
        }

    def _load_material_db(self) -> Dict[str, Any]:
        """加载已采集入库的素材库"""
        db_path = self.base_dir / "material_db.json"
        if db_path.exists():
            try:
                with open(db_path, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except Exception:
                return {}
        return {}

    def _save_to_material_db(self, key: str, data: Dict) -> None:
        """将采集结果结构化入库"""
        db = self._load_material_db()
        db[key] = {
            "data": data,
            "saved_at": datetime.now().isoformat(),
            "key": key
        }
        db_path = self.base_dir / "material_db.json"
        with open(db_path, 'w', encoding='utf-8') as f:
            json.dump(db, f, ensure_ascii=False, indent=2)

    def _generate_character_images(self, characters: list, images_dir: Path) -> list:
        """为角色列表生成设计图，返回带图片路径的角色列表"""
        char_images = []
        for char in characters:
            name = char.get("name", "")
            desc = char.get("description", "")
            if not name and not desc:
                char["image_path"] = None
                char_images.append(char)
                continue
            # 构造角色设计图提示词
            img_prompt = f"角色设计图，{name}，{desc}，全身像，清晰细节，白色背景，9:16竖屏"
            try:
                result = generate_image(
                    prompt=img_prompt,
                    size="1920x1920",
                    n=1,
                    download_dir=str(images_dir),
                )
                paths = result.get("local_paths", [])
                char["image_path"] = paths[0] if paths else None
                logger.info(f"  角色图生成: {name} -> {char.get('image_path', 'N/A')}")
            except Exception as e:
                logger.warning(f"  角色{name}图片生成失败: {e}")
                char["image_path"] = None
            char_images.append(char)
        return char_images

    def _generate_scene_images(self, scenes: list, images_dir: Path, characters: list = None) -> list:
        """为场景列表生成背景图，返回带图片路径的场景列表
        注意: 背景图不应包含人物(人物在角色图中)，提示词不应包含中文字符(非中文场景)"""
        char_descs = ""
        if characters:
            char_names = ", ".join([c.get("name", "") for c in characters if c.get("name")])
            if char_names:
                char_descs = f"，不要画人物({char_names}等角色在单独的角色图中)"
        for scene in scenes:
            location = scene.get("location", "")
            if not location:
                scene["image_path"] = None
                continue
            # 场景背景图只画环境，不含人物
            img_prompt = f"纯环境背景图，{location}，电影感，氛围感，无人，只画环境建筑道具{char_descs}，9:16竖屏"
            try:
                result = generate_image(
                    prompt=img_prompt,
                    size="1440x2560",
                    n=1,
                    download_dir=str(images_dir),
                )
                paths = result.get("local_paths", [])
                scene["image_path"] = paths[0] if paths else None
                logger.info(f"  场景图生成: {location} -> {scene.get('image_path', 'N/A')}")
            except Exception as e:
                logger.warning(f"  场景{location}图片生成失败: {e}")
                scene["image_path"] = None
        return scenes

    def _generate_shot_ref_images(self, scenes: list, images_dir: Path) -> list:
        """为每个镜头生成参考图(基于image_prompt)，返回带图片路径的scenes"""
        count = 0
        for scene in scenes:
            for shot in scene.get("shots", []):
                image_prompt = shot.get("image_prompt", "")
                if not image_prompt:
                    shot["image_path"] = None
                    continue
                try:
                    result = generate_image(
                        prompt=image_prompt,[... 30537 more characters truncated]


        task_data["updated_at"] = datetime.now().isoformat()

        self._save_task(task_id, task_data)

        # 根据步骤调用相应的执行函数
        if step == "collect":
            # 获取之前的修改作为输入
            modifications = task_data["modifications"].get("collect", [])
            user_input = modifications[-1]["modifications"] if modifications else None
            return self.execute_collect(task_id, user_input)
        elif step == "write" or step == "writer":
            # 获取之前的修改作为输入
            modifications = task_data["modifications"].get("script", [])
            user_input = modifications[-1]["modifications"] if modifications else None
            return self.execute_write(task_id, user_input)
        elif step == "produce" or step == "producer":
            return self.execute_produce(task_id)
        elif step == "review" or step == "publisher":
            # 获取之前的反馈作为输入
            modifications = task_data["modifications"].get("review", [])
            user_feedback = modifications[-1]["user_feedback"] if modifications else None
            return self.execute_review(task_id, user_feedback)
        else:
            return {"success": False, "message": f"未知的步骤: {step}"}

    def get_task_status(self, task_id: str) -> Dict[str, Any]:
        """获取任务状态"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        # 构建状态信息
        status_info = {
            "task_id": task_data["task_id"],
            "status": task_data["status"],
            "current_step": task_data["current_step"],
            "progress": task_data["progress"],
            "description": task_data["description"],
            "created_at": task_data["created_at"],
            "updated_at": task_data["updated_at"],
            "next_step": self.get_next_step(task_id),
            "available_steps": [],
            "agents": {}
        }

        # 检查每个步骤是否可用
        for step, step_name in [("collect", "采集"), ("write", "创作"), ("produce", "制作"), ("review", "审核")]:
            if self.can_execute_step(task_id, step):
                status_info["available_steps"].append({
                    "step": step,
                    "name": step_name,
                    "can_execute": True
                })

        # 添加Agent状态
        for agent_name, agent_data in task_data.get("agents", {}).items():
            status_info["agents"][agent_name] = {
                "status": agent_data.get("status", "pending"),
                "can_modify": agent_data.get("can_modify", False),
                "can_regenerate": agent_data.get("can_regenerate", False),
                "modification_count": agent_data.get("modification_count", 0)
            }

        return {
            "success": True,
            "task": status_info
        }

    def get_step_result(self, task_id: str, step: str) -> Dict[str, Any]:
        """获取指定步骤的结果"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        result_map = {
            "collect": task_data["results"]["collection"],
            "write": task_data["results"]["script"],
            "produce": task_data["results"]["video"],
            "review": task_data["results"]["review"]
        }

        result = result_map.get(step)
        if not result:
            return {"success": False, "message": f"步骤{step}的结果不存在"}

        agent_info = task_data["agents"].get(step, {})
        modifications = task_data["modifications"].get(step, [])

        return {
            "success": True,
            "task_id": task_id,
            "step": step,
            "result": result,
            "can_modify": agent_info.get("can_modify", False),
            "can_regenerate": agent_info.get("can_regenerate", False),
            "modification_count": len(modifications),
            "modifications": modifications
        }

    def _load_task(self, task_id: str) -> Optional[Dict[str, Any]]:
        """加载任务数据"""
        task_file = self.tasks_dir / task_id / "task.json"
        if not task_file.exists():
            return None

        with open(task_file, 'r', encoding='utf-8') as f:
            return json.load(f)

    def _save_task(self, task_id: str, task_data: Dict[str, Any]) -> None:
        """保存任务数据"""
        task_file = self.tasks_dir / task_id / "task.json"
        with open(task_file, 'w', encoding='utf-8') as f:
            json.dump(task_data, f, ensure_ascii=False, indent=2)

    def regenerate_single_shot(self, task_id: str, shot_id: int, user_prompt: Optional[str] = None, mode: Optional[str] = None) -> Dict[str, Any]:
        """重新生成单个镜头的视频"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        video_result = task_data.get("results", {}).get("video", {})
        if not video_result:
            return {"success": False, "message": "视频结果不存在，请先执行制作步骤"}

        shots = video_result.get("shots", [])
        shot_info = None
        for s in shots:
            if s.get("shot_id") == shot_id:
                shot_info = s
                break

        if not shot_info:
            return {"success": False, "message": f"镜头{shot_id}不存在"}

        task_dir = Path(task_data["files"]["task_dir"])
        videos_dir = task_dir / "videos"
        images_dir = task_dir / "images"
        videos_dir.mkdir(parents=True, exist_ok=True)

        # 获取原始镜头信息
        script_data = task_data.get("results", {}).get("script", {})
        scenes = script_data.get("scenes", []) if isinstance(script_data, dict) else []
        characters = script_data.get("characters", []) if isinstance(script_data, dict) else []

        original_shot = None
        parent_scene = None
        for scene in scenes:
            for sh in scene.get("shots", []):
                if sh.get("shot_id") == shot_id:
                    original_shot = sh
                    parent_scene = scene
                    break

        video_prompt = user_prompt or (original_shot.get("video_prompt", original_shot.get("description", "")) if original_shot else "")
        shot_mode = mode or shot_info.get("mode", "text2vid")
        ref_image = self._resolve_shot_ref_image(original_shot or {}, parent_scene or {}, characters) if original_shot else None

        try:
            if shot_mode.startswith("img2vid") and ref_image and Path(ref_image).exists():
                try:
                    result = generate_video_from_image(
                        image_path=ref_image, prompt=video_prompt, ratio="9:16",
                        duration=(original_shot or {}).get("duration_seconds", 5),
                        download_dir=str(videos_dir),
                    )
                except Exception as img_err:
                    if "SensitiveContent" in str(img_err) or "sensitive" in str(img_err).lower():
                        logger.warning(f"单镜头重新生成: 图生视频触发敏感内容检测，降级到文生视频")
                        shot_mode = "text2vid_fallback"
                        result = generate_video(
                            prompt=video_prompt, ratio="9:16",
                            duration=(original_shot or {}).get("duration_seconds", 5),
                            download_dir=str(videos_dir), timeout_wait=600,
                        )
                    else:
                        raise
            else:
                result = generate_video(
                    prompt=video_prompt, ratio="9:16",
                    duration=(original_shot or {}).get("duration_seconds", 5),
                    download_dir=str(videos_dir), timeout_wait=600,
                )

            new_shot = {
                "shot_id": shot_id,
                "status": "succeeded",
                "video_url": result.get("video_url", ""),
                "local_path": result.get("local_path", ""),
                "file_size": result.get("file_size", 0),
                "duration": result.get("duration"),
                "resolution": result.get("resolution"),
                "ref_image": ref_image,  # 保留参考图路径
                "mode": shot_mode,
            }
            # 添加字幕
            if result.get("local_path") and (original_shot or {}).get("dialogue"):
                try:
                    subtitled = self._add_subtitles_to_video(result["local_path"], original_shot)
                    if subtitled:
                        new_shot["local_path"] = subtitled
                        new_shot["has_subtitles"] = True
                except Exception:
                    pass
            # 替换原镜头
            for i, s in enumerate(shots):
                if s.get("shot_id") == shot_id:
                    shots[i] = new_shot
                    break

            # 更新video_files
            video_files = video_result.get("video_files", [])
            if result.get("local_path") and result["local_path"] not in video_files:
                video_files.append(result["local_path"])
            video_result["shots"] = shots
            video_result["video_files"] = video_files
            video_result["stitch_result"] = None  # 拼接过时，需重新拼接

            task_data["results"]["video"] = video_result
            self._save_task(task_id, task_data)

            return {
                "success": True,
                "message": f"镜头{shot_id}重新生成成功",
                "task_id": task_id,
                "shot": new_shot,
                "result": video_result,
            }
        except Exception as e:
            return {"success": False, "message": f"镜头{shot_id}重新生成失败: {e}"}

    def stitch_task_videos(self, task_id: str) -> Dict[str, Any]:
        """拼接任务的所有视频片段 - 只拼接成功镜头的视频(优先字幕版)"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        video_result = task_data.get("results", {}).get("video", {})
        shots = video_result.get("shots", [])

        # 从shots中按顺序取成功的视频路径(优先字幕版)
        video_files = []
        for shot in shots:
            if shot.get("status") != "succeeded":
                continue
            # 优先用字幕版
            lp = shot.get("local_path", "")
            if lp and Path(lp).exists():
                video_files.append(lp)

        if len(video_files) < 2:
            return {"success": False, "message": f"只有{len(video_files)}个成功视频，无需拼接"}

        task_dir = Path(task_data["files"]["task_dir"])
        output_path = str(task_dir / "videos" / "merged_full.mp4")

        try:
            result = stitch_videos(video_files, output_path)
            if result.get("status") == "succeeded":
                # 更新任务数据
                video_result["stitch_result"] = result
                video_result["merged_video"] = result.get("output_path")
                task_data["results"]["video"] = video_result
                task_data["files"]["video_file"] = result.get("output_path", "")
                self._save_task(task_id, task_data)
                return {
                    "success": True,
                    "message": f"视频拼接成功 ({len(video_files)}个片段，{result.get('duration',0):.1f}秒)",
                    "stitch_result": result,
                    "merged_url": result.get("output_path"),
                }
            else:
                return {"success": False, "message": f"拼接失败: {result.get('error', '未知错误')}"}
        except Exception as e:
            return {"success": False, "message": f"视频拼接失败: {e}"}

    def list_tasks(self) -> Dict[str, Any]:
        """列出所有任务"""
        tasks = []
        if self.tasks_dir.exists():
            for task_dir in sorted(self.tasks_dir.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True):
                if task_dir.is_dir():
                    task_file = task_dir / "task.json"
                    if task_file.exists():
                        try:
                            with open(task_file, 'r', encoding='utf-8') as f:
                                td = json.load(f)
                            tasks.append({
                                "task_id": td.get("task_id", task_dir.name),
                                "description": td.get("description", "")[:50],
                                "status": td.get("status", "unknown"),
                                "current_step": td.get("current_step", ""),
                                "progress": td.get("progress", 0),
                                "created_at": td.get("created_at", ""),
                                "updated_at": td.get("updated_at", ""),
                            })
                        except Exception:
                            pass
        return {"success": True, "tasks": tasks, "total": len(tasks)}

    def _add_subtitles_to_video(self, video_path: str, shot: Dict, output_path: str = None) -> Optional[str]:
        """使用ffmpeg为视频添加字幕(从镜头的对白中提取)"""
        dialogue = shot.get("dialogue", "")
        if not dialogue or not video_path or not Path(video_path).exists():
            return None

        if output_path is None:
            p = Path(video_path)
            output_path = str(p.parent / f"{p.stem}_subtitled{p.suffix}")

        # 获取视频时长
        try:
            probe = subprocess.run(
                ["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", video_path],
                capture_output=True, text=True, timeout=10
            )
            duration = float(probe.stdout.strip()) if probe.stdout.strip() else 5.0
        except Exception:
            duration = 5.0

        # 生成SRT字幕文件
        srt_content = f"1\n00:00:00,000 --> 00:00:{int(duration):02d},{int((duration%1)*1000):03d}\n{dialogue}\n"

        with tempfile.NamedTemporaryFile(mode='w', suffix='.srt', delete=False, encoding='utf-8') as f:
            f.write(srt_content)
            srt_path = f.name

        try:
            # 使用ffmpeg subtitles滤镜烧录字幕
            style = "FontSize=24,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,Outline=2,Alignment=2,MarginV=60"
            vf_filter = "subtitles=" + srt_path + ":force_style='" + style + "'"
            cmd = [
                "ffmpeg", "-y", "-i", video_path,
                "-vf", vf_filter,
                "-c:a", "copy",
                output_path
            ]
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
            if result.returncode == 0 and Path(output_path).exists():
                logger.info(f"  字幕添加成功: {output_path}")
                return output_path
            # subtitles滤镜失败，用drawtext
            safe_text = dialogue.replace("'", "").replace(":", "").replace("@", "").replace('"', "")
            drawtext = (
                "drawtext=text='" + safe_text + "'"
                ":fontsize=24:fontcolor=white:borderw=2:bordercolor=black"
                ":x=(w-text_w)/2:y=h-th-60"
            )
            cmd2 = [
                "ffmpeg", "-y", "-i", video_path,
                "-vf", drawtext,
                "-c:a", "copy",
                output_path
            ]
            result2 = subprocess.run(cmd2, capture_output=True, text=True, timeout=60)
            if result2.returncode == 0 and Path(output_path).exists():
                logger.info(f"  字幕添加成功(drawtext): {output_path}")
                return output_path
            logger.warning(f"  字幕添加失败: {result2.stderr[:200]}")
            return None
        except Exception as e:
            logger.warning(f"  字幕添加异常: {e}")
            return None
        finally:
            os.unlink(srt_path)

# 创建全局实例
user_driven_task_system = UserDrivenTaskSystem()


Summary:
## 决策
- **CTO反馈规范**：CTO Agent只反馈自己完成的任务，不反馈其他Agent（采集官/创作官/制作官/审核官/分析官）的工作，除非用户特殊说明
- **Agent调用真实性定义**：指慢剧系统各步骤是否真正协调了对应Agent执行，而非是否调用了真实API
- **Main Agent无反应**：不需要处理，已关闭
- **Skill安装策略**：优先使用skillhub（中国优化），fallback到clawhub
- **switchTask完成态bug根因**：当`current_step='complete'`时，`steps.indexOf(step)=-1`，导致所有步骤按钮被设为pending/disabled。修复逻辑：增加`isCompleted`判断，当任务完成时将所有步骤设为completed并启用查看按钮
- **API测试26项全部通过**：初始6个FAIL均为测试脚本问题（字段名`topic`→`description`、curl解析错误、安全策略正确拦截），系统无bug
- **前端按钮测试发现并修复bug**：已完成任务的"查看结果"按钮全部disabled，现已修复并验证4个步骤的查看结果按钮全部启用且内容正常渲染

## 待办事项
- 继续前端按钮交互测试（已完成"查看结果"4个按钮，还需测试：执行采集/创作/制作/审核、修改要求/修改/重新生成、视频模式切换、拼接视频、任务切换、刷新、图片全屏放大、视频播放模态框、下载视频、修改提示词重生成等）
- 编译5个仍缺`dist/`的插件：`adp-openclaw`、`ddingtalk`、`memory-tencentdb`、`wecom`、`memos-local-openclaw-plugin`
- 编译`openclaw-weixin`插件的`dist/`目录
- 修复systemd网关服务状态不一致问题
- 解决liveness警告（event_loop_delay, cpu高负载）
- 验证网关重启后所有修复是否生效

## 约束/规则
- CTO关注范围：系统架构、配置、服务状态、跨Agent协调问题、基础设施
- CTO只反馈自己的任务，不反馈其他Agent工作
- 所有定时任务必须加载在scheduler里，避免crontab
- 外部AI修复工具可能引入配置污染，修复后必须diff对比验证
- 网关端口已迁移至27038
- 需确保修复不会引入新的回归问题

## 待处理用户请求
- 执行更多测试案例覆盖更多场景（前端按钮交互测试正在进行中）

## 精确标识符
- 配置文件：`/root/.openclaw/openclaw.json`
- 备份目录：`/root/.openclaw-backups/20260513-085024/`、`/root/.openclaw-backups/20260413_234047/`
- 失败投递备份：`/root/.openclaw-backups/delivery-failed-20260513/`
- 工作区：`/root/.openclaw/workspace`
- MEMORY.md路径：`/root/.openclaw/workspace/MEMORY.md`
- models.json：`/root/.openclaw/agents/main/agent/models.json`（已解除immutable，权限`-rw-r--r--`）
- 漫画制作系统：`/root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/`（port 8000 和 8001 运行中）
- 前端页面：`http://127.0.0.1:8000/static/user_driven_coordinator_final.html`
- 前端JS：`/root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/static/user_driven_coordinator.js`（v3.4，已修复switchTask bug和语法错误）
- API测试报告：`/root/.openclaw/workspace/memory/2026-05-13_comprehensive_test_report.md`
- 之前的API测试报告：`/root/.openclaw/workspace/memory/2026-05-13_api_test_report.md`
- 任务追踪：`/root/.openclaw/workspace/memory/2026-05-12_task_tracker.md`
- 日志：`/root/.openclaw/workspace/memory/2026-05-12.md`、`/root/.openclaw/workspace/memory/2026-05-13.md`
- OpenClaw包路径：`/root/.local/share/pnpm/global/5/.pnpm/openclaw@2026.5.7_@types+express@5.0.6/node_modules/openclaw`
- OpenClaw版本：`openclaw@2026.5.7`
- Node.js版本：`22.22.2`
- 网关端口：`27038`
- Dashboard：`http://10.2.0.12:27038/uy475k/`，basePath `/uy475k`
- 服务器IP：`49.232.222.110`
- systemd服务：`openclaw-gateway.service`，文件`~/.config/systemd/user/openclaw-gateway.service`
- 主模型：`arkcode/ark-code-latest`
- Agent列表：`cto`(CTO)、`main`(总控官)、`collector`(采集官)、`writer`(创作官)、`producer`(制作官)、`publisher`(审核官)、`analyzer`(分析官)
- 已安装skills：`requirements-analysis`、`api-test-automation`、`test-master`、`api-case-gen`、`openclaw-api-tester`、`frontend-design-pro`、`find-skills`
- Skills下载源：`https://lightmake.site/api/v1/download?slug=requirements-analysis`等
- 8000端口服务：`user-driven-manhua-studio-api` v3.3.0，health端点`/health`
- 8001端口服务：PID=818456，health端点`/api/system/health`
- 8001端口API端点：`/`、`/api`、`/api/ideas`、`/api/projects`(GET/POST)、`/api/projects/{project_id}`(GET/PUT/DELETE)、`/api/projects/{project_id}/workflow/{step_name}/complete`(POST)、`/api/projects/{project_id}/workflow/{step_name}/start`(POST)、`/api/system/health`(GET)、`/api/system/stats`(GET)、`/api/tasks/create`(POST)、`/api/tasks/user/{user_id}`(GET)、`/api/tasks/{task_id}`(GET)、`/api/tasks/{task_id}/collect`(POST)、`/api/tasks/{task_id}/produce`(POST)、`/api/tasks/{task_id}/review`(POST)、`/api/tasks/{task_id}/status`(GET)、`/api/tasks/{task_id}/write`(POST)
- 已完成任务ID：`task_20260513_011737_b8f72351`（status=reviewed, step=complete, 用于验证前端修复）、`task_20260513_005405_7af44490`、`task_20260511_183227_cb7656ba`
- 前端按钮测试任务：`task_20260513_141639_80b8bf46`（描述"前端按钮功能测试任务-测试后可删除"）
- 前端按钮ID：`btnCreateTask`、`btnRefreshTasks`、`btnExecuteCollect`、`btnViewCollect`、`btnModifyCollect`、`btnRegenerateCollect`、`btnSubmitCollectModification`、`btnCancelCollectModification`、`btnExecuteWrite`、`btnViewWrite`、`btnModifyWrite`、`btnRegenerateWrite`、`btnSaveModification`、`btnCancelModification`、`btnExecuteProduce`、`btnViewProduce`、`btnRegenerateProduce`、`btnExecuteReview`、`btnViewReview`、`btnRegenerateReview`、`btnStitchVideos`、`promptEditorCancel`、`promptEditorConfirm`
- 文件访问安全目录：`/root/data/disk/`（仅此目录允许访问，其他返回403）
- 磁盘使用率：82%
- Weixin插件：`/root/.openclaw/extensions/openclaw-weixin/`（包名`@tencent-weixin/openclaw-weixin` v2.1.8，tsconfig已创建）
- Chrome调试端口：`9222`，browser profile：`openclaw`，cdpPort：`27049`
- 浏览器标签：`manhua-test`(targetId=8, label=manhua-test)，monitor dashboard(targetId=4, port 8081)
- 活跃cron UUID：`3cf13239-476e-4c1e-beb7-fac6eb49b230:run:4ea0736e-5cdc-454d-8224-3533e4937732`
- 漫画制作系统前端版本：HTML标题显示v3.2，JS文件注释v3.4

**Turn Context (split turn):**

## 决定
- 识别任务列表中的测试用例并删除，保留正式任务

## 待办事项
- 从任务列表中删除所有测试用例任务
- 确认正式任务不被误删（特别是 `task_20260512_230112_fe88a8c6` 标注"请勿删除"）

## 约束/规则
- 不得删除标注"请勿删除"的任务
- 仅删除测试用例任务

## 待处理的用户请求
- 删除任务列表中的测试用例

## 精确标识符
- API: `http://127.0.0.1:8000/api/tasks`
- 测试用例任务ID:
  - `task_20260513_144046_cae89fd5` — 前端按钮全面测试-创建任务
  - `task_20260513_141639_80b8bf46` — 前端按钮功能测试任务-测试后可删除
  - `task_20260513_120031_69ee27e5` — 并发测试1
  - `task_20260513_120031_91df398c` — 并发测试3
  - `task_20260513_120031_3a5156ee` — 并发测试2
  - `task_20260513_120031_e2844167` — 测试主题-自动化测试
  - `task_20260513_011737_b8f72351` — CTO规范验证-可删除
  - `task_20260513_005405_7af44490` — 自动化测试任务-可删除
  - `task_20260509_234636_baf70142` — 验证字符串修复后系统能够生成实际的PNG图像文件
  - `task_20260509_232819_940367be` — 直接测试任务执行流程，验证文件生成
  - `task_20260509_231027_0565fb91` — 验证系统修复后能够生成实际的PNG图
- 需保留的测试任务: `task_20260512_230112_fe88a8c6` — 自动化测试任务-请勿删除
- 正式任务ID（不应删除）:
  - `task_20260512_105050_c5581702` — 一个关于苏联笑话的故事
  - `task_20260512_095309_d05f883b` — 一个关于数字艺术创作的故事
  - `task_20260512_083702_5d2eb5b3` — 一个关于数字艺术创作的故事
  - `task_20260511_231038_89960c72` — 一个古代侠客在月光下练剑的微短剧
  - `task_20260511_223615_edae0b69` — 一个关于AI创作的故事
  - `task_20260511_183227_cb7656ba` — 外卖小哥冒雨送餐得到温暖帮助
  - `task_20260511_182252_e825d5f2` — 一个外卖小哥冒雨送餐得到温暖帮助的治愈短剧
  - `task_20260511_181700_2c8d7ae3` — 一个外卖小哥冒雨送餐得到温暖帮助的治愈短剧
  - `task_20260511_181308_58d28db9` — 同上
  - `task_20260511_180244_61239d5e` — 同上短剧
  - `task_20260511_174913_0e9681fc` — 一个外卖小哥冒雨送餐的温暖短剧
  - `task_20260511_173701_c7ea1233` — 同上

## Recent turns preserved verbatim
- User: [OpenClaw heartbeat poll]
- User: [Wed 2026-05-13 14:10 GMT+8] 结合CTO行为规范回答，你的测试案例是否覆盖了 所有按钮的功能？
- User: [Wed 2026-05-13 14:12 GMT+8] 结合CTO行为规范，使用合适的skill，补充缺失的测试案例
- Assistant: 4个"查看结果"按钮全部正常工作！继续测试其他按钮：
[non-text content: toolCall]
- Tool result (browser): {
  "ok": true,
  "targetId": "8",
  "url": "http://127.0.0.1:8000/static/user_driven_coordinator_final.html",
  "result": "{\"collect\":{\"visible\":true,\"hasContent\":true},\"write\":{\"visible\":true,\"hasContent\":true},\"produce\":{\"visible\":true,\"hasContent\":true},\"review\":{\"visible\":true,\"hasContent\":true}}"
}
- Assistant: 看起来title没更新（还是之前的结果）。可能是因为script注入有冲突。让我重新刷新页面后逐个测试：
[non-text content: toolCall]
- Tool result (browser): {
  "ok": true,
  "targetId": "8",
  "url": "http://127.0.0.1:8000/static/user_driven_coordinator_final.html"
}
- Assistant: [non-text content: toolCall]
- Tool result (browser): {
  "ok": true,
  "targetId": "8",
  "url": "http://127.0.0.1:8000/static/user_driven_coordinator_final.html",
  "result": {
    "modifyWriteDisabled": false,
    "modifyCollectDisabled": false,
    "videoModeValue": "auto",
    "writeModVisible": true,
    "writeModHidden": true,
    "collectModVisible": true,
    "collectModHidden": true,
    "videoModeAfter": "img2vid"
  }
}
- Assistant: 所有按钮交互测试通过！最后测试新建任务的完整流程和任务切换：
[non-text content: toolCall]
- Tool result (browser): {
  "ok": true,
  "targetId": "8",
  "url": "http://127.0.0.1:8000/static/user_driven_coordinator_final.html",
  "result": {
    "emptyDescAlert": true,
    "descValue": "前端按钮全面测试-创建任务",
    "taskCreated": true,
    "taskInfoVisible": true,
    "btnExecuteCollectDisabled": false,
    "btnViewCollectDisabled": true,
    "btnExecuteWriteDisabled": true,
    "afterSwitchViewCollect": false,
    "afterSwitchViewWrite": false,
    "afterSwitchViewProduce": false,
    "afterSwitchViewReview": false,
    "currentTaskId": "task_20260511_183227_cb7656ba"
  }
}
- Assistant: 所有前端按钮测试通过！现在关闭浏览器，写测试报告：
[non-text content: toolCall]
- Tool result (browser): {
  "ok": true
}
- Assistant: [non-text content: toolCall]
- Tool result (browser): {
  "status": "error",
  "tool": "browser",
  "error": "Chrome MCP existing-session attach for profile \"user\" timed out after 600ms."
}
- Assistant: [non-text content: toolCall]
- Tool result (browser): {
  "enabled": true,
  "profile": "openclaw",
  "driver": "openclaw",
  "transport": "cdp",
  "running": false,
  "cdpReady": false,
  "cdpHttp": false,
  "pid": null,
  "cdpPort": 27049,
  "cdpUrl": "http://127.0.0.1:27049",
  "chosenBrowser": null,
  "detectedBrowser": "custom",
  "detectedExecutablePath": "/root/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome",
  "detectError": null,
  "userDataDir": null,
  "color": "#FF4500",
  "headless": true,
  "headlessSource": "linux-display-fallback",
  "noSandbox": true,
  "executablePath": "/root/.cache/ms-playwright/chromium-1208/chrome-...
- Assistant: [non-text content: toolCall]
- Tool result (write): Successfully wrote 2979 bytes to /root/.openclaw/workspace/memory/2026-05-13_frontend_button_test_report.md
- Assistant: 现在更新 MEMORY.md，记录这个关键 bug 修复：
[non-text content: toolCall]
- Tool result (read): # MEMORY.md - 结构化长期记忆

**更新时间**: 2026-05-13 11:54
**整理目的**: 按"通用+各个Agent"形式分类，便于调用Agent时参考相关记忆

[882 more lines in file. Use offset=6 to continue.]
- Assistant: [non-text content: toolCall]
- Tool result (exec): 850:### **2026-05-12 会话上下文丢失 + 任务列表不加载（根因：JS函数未闭合）**
- Assistant: [non-text content: toolCall]
- Tool result (edit): Successfully replaced 1 block(s) in /root/.openclaw/workspace/MEMORY.md.
- Assistant: [non-text content: toolCall]
- Tool result (edit): Successfully replaced 1 block(s) in /root/.openclaw/workspace/MEMORY.md.
- Assistant: ---

## CTO任务执行报告

### 发现并修复的Bug

**🔴 P0: 已完成任务"查看结果"按钮全部disabled**

- **根因**: `switchTask()`函数用`steps.indexOf('complete')`查找步骤索引，`'complete'`不在`['collect','write','produce','review']`中，返回-1，导致所有4个步骤被错误设为pending+不可操作
- **修复**: 增加`isCompleted`判断——当任务已完成时，令idx=4(数组长度)，使所有步骤走`completed`分支，按钮全部enabled
- **附带事故**: 编辑时遗漏1个`}`导致JS文件SyntaxError，整个页面功能瘫痪。已用`node -c`验证修复

### 前端按钮测试覆盖（18项）

| 类别 | 通过 | 发现bug |
|------|------|---------|
| 查看结果×4 | 4 | 1(已修复) |
| 修改/取消表单×4 | 4 | 0 |
| 创建任务+空描述校验 | 2 | 0 |
| 任务切换 | 2 | 0 |
| 视频模式切换 | 1 | 0 |
| 刷新任务列表 | 1 | 0 |
| 清空日志 | 1 | 0 |
| 新建任务按钮状态逻辑 | 3 | 0 |

**未测...
- User: [OpenClaw heartbeat poll]
- Assistant: [non-text content: thinking, toolCall]
- Tool result (exec): === Gateway ===
200
=== 8000 ===
healthy
=== 8001 ===
healthy
=== Disk ===
82%
=== systemd ===
active
- Assistant: 所有核心服务正常，磁盘82%安全范围。

HEARTBEAT_OK
- User: [Wed 2026-05-13 14:49 GMT+8] 好的，现在任务列表中有很多测试用例，参考CTO行为规范进行删除

## Tool Failures
- browser: Validation failed for tool "browser": - action: must be equal to one of the allowed values Received arguments: { "action": "act<arg_key><arg_key>kind</arg_key><arg_value>wait", "timeMs": 3000 }

<read-files>
~/.openclaw/plugin-skills/browser-automation/SKILL.md

</read-files>

<modified-files>
/root/.openclaw/workspace/MEMORY.md
/root/.openclaw/workspace/memory/2026-05-13_comprehensive_test_report.md
/root/.openclaw/workspace/memory/2026-05-13_frontend_button_test_report.md
/root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/static/user_driven_coordinator.js

</modified-files>


Continue from the current transcript after the latest tool result. Do not repeat the original user request, and do not rerun completed tools unless the transcript shows they are still needed.


"""
用户主导的任务系统
版本: 3.3.0
功能: 支持分步执行、结果修改、图/文生视频切换、采集入库、字幕添加、视频拼接
"""

import os
import json
import uuid
import time
import logging
import subprocess
import tempfile
from datetime import datetime
from typing import Dict, List, Optional, Any
from pathlib import Path

logger = logging.getLogger(__name__)

# 导入简化的真实Agent数据生成
from simple_real_agent import SimpleRealAgent

# 导入真实API客户端
from real_api_client import generate_video, generate_video_from_image, generate_image, generate_text, stitch_videos, DEFAULT_VIDEO_MODEL, DEFAULT_IMAGE_MODEL

class UserDrivenTaskSystem:
    """用户主导的任务系统 - 支持分步控制和结果修改"""

    def __init__(self, base_dir: str = "/root/data/disk/projects"):
        """
        初始化用户主导任务系统

        Args:
            base_dir: 任务数据存储基础目录
        """
        self.base_dir = Path(base_dir)
        self.tasks_dir = self.base_dir / "tasks"
        self.assets_dir = self.base_dir / "assets"

        # 确保目录存在
        self.tasks_dir.mkdir(parents=True, exist_ok=True)
        self.assets_dir.mkdir(parents=True, exist_ok=True)

        # 任务状态定义
        self.STATUS = {
            "CREATED": "created",
            "WAITING_COLLECT": "waiting_collect",
            "COLLECTING": "collecting",
            "COLLECTED": "collected",
            "WAITING_WRITE": "waiting_write",
            "WRITING": "writing",
            "WRITTEN": "written",
            "WAITING_PRODUCE": "waiting_produce",
            "PRODUCING": "producing",
            "PRODUCED": "produced",
            "WAITING_REVIEW": "waiting_review",
            "REVIEWING": "reviewing",
            "REVIEWED": "reviewed",
            "COMPLETED": "completed",
            "PAUSED": "paused",
            "MODIFYING": "modifying",
            "REGENERATING": "regenerating"
        }

        # Agent定义
        self.AGENTS = {
            "collector": {
                "name": "采集官",
                "description": "负责热点素材采集",
                "weight": 0.16,
                "color": "warning",
                "can_regenerate": True
            },
            "writer": {
                "name": "创作官",
                "description": "负责文案创作和脚本编写",
                "weight": 0.24,
                "color": "info",
                "can_modify": True,
                "can_regenerate": True
            },
            "producer": {
                "name": "制作官",
                "description": "负责视频生成和制作",
                "weight": 0.20,
                "color": "primary",
                "can_regenerate": True
            },
            "publisher": {
                "name": "审核官",
                "description": "负责内容审核和质量控制",
                "weight": 0.13,
                "color": "success",
                "can_modify": True,
                "can_regenerate": True
            }
        }

    def create_task(self, description: str, user_id: str = "default") -> Dict[str, Any]:
        """创建新任务"""
        task_id = f"task_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
        task_dir = self.tasks_dir / task_id
        assets_dir = self.assets_dir / task_id

        task_dir.mkdir(parents=True, exist_ok=True)
        assets_dir.mkdir(parents=True, exist_ok=True)

        task_data = {
            "task_id": task_id,
            "description": description,
            "user_id": user_id,
            "status": self.STATUS["CREATED"],
            "created_at": datetime.now().isoformat(),
            "updated_at": datetime.now().isoformat(),
            "current_step": "collect",
            "progress": 0,
            "user_actions": [],
            "agents": {},
            "results": {
                "collection": None,
                "script": None,
                "storyboard": None,
                "characters": None,
                "backgrounds": None,
                "video": None,
                "review": None
            },
            "modifications": {
                "collection": [],
                "script": [],
                "storyboard": [],
                "characters": [],
                "backgrounds": [],
                "video": [],
                "review": []
            },
            "files": {
                "task_dir": str(task_dir),
                "assets_dir": str(assets_dir),
                "collection_file": None,
                "script_file": None,
                "storyboard_file": None,
                "characters_file": None,
                "backgrounds_file": None,
                "video_file": None,
                "review_file": None
            }
        }

        # 保存任务数据

[1329 more lines in file. Use offset=151 to continue.]


            json.dump(collection_result, f, ensure_ascii=False, indent=2)

        # 更新任务数据
        task_data["results"]["collection"] = collection_result
        task_data["files"]["collection_file"] = str(collection_file)
        task_data["agents"]["collector"] = {
            "status": "completed",
            "started_at": datetime.now().isoformat(),
            "completed_at": datetime.now().isoformat(),
            "result_summary": "采集完成",
            "can_modify": True,
            "can_regenerate": True
        }
        task_data["status"] = self.STATUS["COLLECTED"]
        task_data["current_step"] = "write"
        task_data["progress"] = 30

        self._save_task(task_id, task_data)

        return {
            "success": True,
            "message": "采集步骤完成" + (" (来自素材库)" if collection_result.get("from_db") else ""),
            "task_id": task_id,
            "result": collection_result,
            "next_step": "write",
            "next_step_description": "下一步：创作分镜脚本",
            "can_modify": True,
            "can_regenerate": True
        }

    def _load_material_db(self) -> Dict[str, Any]:
        """加载已采集入库的素材库"""
        db_path = self.base_dir / "material_db.json"
        if db_path.exists():
            try:
                with open(db_path, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except Exception:
                return {}
        return {}

    def _save_to_material_db(self, key: str, data: Dict) -> None:
        """将采集结果结构化入库"""
        db = self._load_material_db()
        db[key] = {
            "data": data,
            "saved_at": datetime.now().isoformat(),
            "key": key
        }
        db_path = self.base_dir / "material_db.json"
        with open(db_path, 'w', encoding='utf-8') as f:
            json.dump(db, f, ensure_ascii=False, indent=2)

    def _generate_character_images(self, characters: list, images_dir: Path) -> list:
        """为角色列表生成设计图，返回带图片路径的角色列表"""
        char_images = []
        for char in characters:
            name = char.get("name", "")
            desc = char.get("description", "")
            if not name and not desc:
                char["image_path"] = None
                char_images.append(char)
                continue
            # 构造角色设计图提示词
            img_prompt = f"角色设计图，{name}，{desc}，全身像，清晰细节，白色背景，9:16竖屏"
            try:
                result = generate_image(
                    prompt=img_prompt,
                    size="1920x1920",
                    n=1,
                    download_dir=str(images_dir),
                )
                paths = result.get("local_paths", [])
                char["image_path"] = paths[0] if paths else None
                logger.info(f"  角色图生成: {name} -> {char.get('image_path', 'N/A')}")
            except Exception as e:
                logger.warning(f"  角色{name}图片生成失败: {e}")
                char["image_path"] = None
            char_images.append(char)
        return char_images

    def _generate_scene_images(self, scenes: list, images_dir: Path, characters: list = None) -> list:
        """为场景列表生成背景图，返回带图片路径的场景列表
        注意: 背景图不应包含人物(人物在角色图中)，提示词不应包含中文字符(非中文场景)"""
        char_descs = ""
        if characters:
            char_names = ", ".join([c.get("name", "") for c in characters if c.get("name")])
            if char_names:
                char_descs = f"，不要画人物({char_names}等角色在单独的角色图中)"
        for scene in scenes:
            location = scene.get("location", "")
            if not location:
                scene["image_path"] = None
                continue
            # 场景背景图只画环境，不含人物
            img_prompt = f"纯环境背景图，{location}，电影感，氛围感，无人，只画环境建筑道具{char_descs}，9:16竖屏"
            try:
                result = generate_image(
                    prompt=img_prompt,
                    size="1440x2560",
                    n=1,
                    download_dir=str(images_dir),
                )
                paths = result.get("local_paths", [])
                scene["image_path"] = paths[0] if paths else None
                logger.info(f"  场景图生成: {location} -> {scene.get('image_path', 'N/A')}")
            except Exception as e:
                logger.warning(f"  场景{location}图片生成失败: {e}")
                scene["image_path"] = None
        return scenes

    def _generate_shot_ref_images(self, scenes: list, images_dir: Path) -> list:
        """为每个镜头生成参考图(基于image_prompt)，返回带图片路径的scenes"""
        count = 0
        for scene in scenes:
            for shot in scene.get("shots", []):
                image_prompt = shot.get("image_prompt", "")
                if not image_prompt:
                    shot["image_path"] = None
                    continue
                try:
                    result = generate_image(
                        prompt=image_prompt,
                        size="1440x2560",
                        n=1,
                        download_dir=str(images_dir),
                    )
                    paths = result.get("local_paths", [])
                    shot["image_path"] = paths[0] if paths else None
                    if shot["image_path"]:
                        count += 1
                except Exception as e:
                    logger.warning(f"  镜头参考图生成失败: {e}")
                    shot["image_path"] = None
        logger.info(f"  镜头参考图生成: {count}张")
        return scenes

    def _resolve_shot_ref_image(self, shot: Dict, scene: Dict, characters: list) -> Optional[str]:
        """为镜头匹配最佳参考图
        优先级: 镜头参考图 > 角色匹配图 > 场景背景图 > None
        """
        # 1. 镜头自带的参考图(由image_prompt生成)
        shot_img = shot.get("image_path")
        if shot_img and Path(shot_img).exists():
            return shot_img

        # 2. 角色名匹配
        shot_desc = shot.get("description", "") + shot.get("video_prompt", "")
        for char in characters:
            char_name = char.get("name", "")
            char_img = char.get("image_path")
            if char_name and char_img and char_name in shot_desc and Path(char_img).exists():
                return char_img

        # 3. 场景背景图
        scene_img = scene.get("image_path")
        if scene_img and Path(scene_img).exists():
            return scene_img

        return None

    def execute_write(self, task_id: str, user_input: Optional[Dict] = None) -> Dict[str, Any]:
        """执行创作步骤 - 生成脚本+角色图+场景图+镜头参考图"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        if not self.can_execute_step(task_id, "write"):
            return {"success": False, "message": "当前不能执行创作步骤"}

        task_data["status"] = self.STATUS["WRITING"]
        task_data["current_step"] = "write"
        task_data["progress"] = 40
        task_data["updated_at"] = datetime.now().isoformat()

        # 记录用户动作
        action_desc = "用户执行创作步骤"
        if user_input and user_input.get("modification"):
            action_desc = "用户修改创作结果并重新生成"
            task_data["modifications"]["script"].append({
                "timestamp": datetime.now().isoformat(),
                "user_input": user_input,
                "version": len(task_data["modifications"]["script"]) + 2
            })

        task_data["user_actions"].append({
            "action": "execute_write",
            "timestamp": datetime.now().isoformat(),
            "description": action_desc
        })

        # 如果有用户输入，用于生成结果
        user_preferences = user_input.get("preferences", {}) if user_input else {}

        # 调用真实LLM生成分镜脚本
        task_description = task_data.get("description", "")
        collection_data = task_data.get("results", {}).get("collection", {})

        try:
            llm_output = generate_text(

[980 more lines in file. Use offset=500 to continue.]


                system_prompt="""你是一个专业的短剧脚本创作AI。创作一个微短剧分镜脚本。
严格输出JSON格式：
{"title":"剧名","genre":"类型","scenes":[{"scene_id":1,"location":"场景地点","shots":[{"shot_id":1,"shot_type":"景别","description":"描述","dialogue":"对白","duration_seconds":5,"video_prompt":"详细中文视频提示词，含画面、运镜、氛围","image_prompt":"详细中文图片提示词，用于生成本镜头的参考图，描述画面构图、角色位置、光线氛围"}]}],"characters":[{"name":"角色名","description":"详细外貌描述，含服装发型配色","personality":"性格"}]}
3个镜头，每个5秒，9:16竖屏。video_prompt和image_prompt都用中文。image_prompt是静态参考图描述，要描述完整画面构图。重要：image_prompt中禁止出现中文汉字、水印、文字等内容，只描述画面视觉元素。只输出JSON，不要任何其他文字。""",
                prompt=f"请基于以下信息创作微短剧分镜脚本：\n主题：{task_description}\n用户偏好：{json.dumps(user_preferences, ensure_ascii=False)}",
                temperature=0.8,
                max_tokens=8192,
            )

            # 解析JSON - 支持截断修复
            try:
                if "```json" in llm_output:
                    json_str = llm_output.split("```json")[1].split("```")[0].strip()
                elif "{" in llm_output:
                    start = llm_output.index("{")
                    json_str = llm_output[start:]
                else:
                    json_str = llm_output

                # 尝试直接解析
                try:
                    script_result = json.loads(json_str)
                except json.JSONDecodeError:
                    # 尝试修复截断的JSON: 找到最后一个完整的}并闭合
                    import re
                    # 统计未闭合的括号
                    opens = json_str.count('[') - json_str.count(']')
                    closes = json_str.count('{') - json_str.count('}')
                    fixed = json_str.rstrip()
                    # 移除尾部不完整的key-value
                    if fixed.endswith(','):
                        fixed = fixed[:-1]
                    # 如果在数组中间截断，先闭合数组
                    for _ in range(max(opens, 0)):
                        fixed += ']'
                    for _ in range(max(closes, 0)):
                        fixed += '}'
                    script_result = json.loads(fixed)

                script_result["is_real_data"] = True
            except (json.JSONDecodeError, ValueError) as je:
                # JSON解析失败，用LLM原文
                logger.warning(f"JSON解析失败: {je}, raw length: {len(llm_output)}")
                script_result = {"raw_script": llm_output, "parse_error": True, "is_real_data": True, "parse_error_detail": str(je)}
        except Exception as e:
            # LLM失败，用备用脚本
            logger.warning(f"LLM脚本生成失败，使用备用脚本: {e}")
            script_result = {
                "title": user_preferences.get("title", task_description[:20]),
                "genre": "短剧",
                "scenes": [
                    {"scene_id": 1, "location": "场景1", "shots": [{"shot_id": 1, "shot_type": "中景", "description": task_description[:50], "duration_seconds": 5, "video_prompt": task_description}]},
                ],
                "characters": [{"name": "主角", "description": "主要角色", "personality": "活跃"}],
                "is_real_data": False,
                "fallback_reason": str(e),
            }

        # =========== 生成角色设计图 + 场景背景图 + 镜头参考图 ===========
        task_dir = Path(task_data["files"]["task_dir"])
        images_dir = task_dir / "images"
        images_dir.mkdir(parents=True, exist_ok=True)

        char_img_count = 0
        scene_img_count = 0
        shot_ref_count = 0

        if isinstance(script_result, dict) and script_result.get("scenes"):
            # 1. 生成角色设计图
            characters = script_result.get("characters", [])
            if characters:
                logger.info(f"开始生成{len(characters)}个角色的设计图...")
                characters = self._generate_character_images(characters, images_dir)
                script_result["characters"] = characters
                char_img_count = sum(1 for c in characters if c.get("image_path"))

            # 2. 生成场景背景图
            scenes = script_result.get("scenes", [])
            if scenes:
                logger.info(f"开始生成{len(scenes)}个场景的背景图...")
                scenes = self._generate_scene_images(scenes, images_dir, characters=characters)
                script_result["scenes"] = scenes
                scene_img_count = sum(1 for s in scenes if s.get("image_path"))

            # 3. 为每个镜头生成参考图(基于image_prompt)
            if scenes:
                logger.info("开始生成镜头参考图...")
                scenes = self._generate_shot_ref_images(scenes, images_dir)
                script_result["scenes"] = scenes
                shot_ref_count = sum(
                    1 for s in scenes
                    for sh in s.get("shots", [])
                    if sh.get("image_path")
                )

        img_summary = f"角色图={char_img_count} 场景图={scene_img_count} 镜头参考图={shot_ref_count}"
        logger.info(f"图片资源生成完成: {img_summary}")

        # 保存创作结果 (task_dir 已在图片生成前定义)
        script_file = task_dir / "script_result.json"
        script_result["generated_at"] = datetime.now().isoformat()
        script_result["version"] = len(task_data["modifications"]["script"]) + 1
        script_result["user_modifications"] = user_preferences
        script_result["image_assets"] = {
            "character_images": char_img_count,
            "scene_images": scene_img_count,
            "shot_ref_images": shot_ref_count,
        }
        with open(script_file, 'w', encoding='utf-8') as f:
            json.dump(script_result, f, ensure_ascii=False, indent=2)

        # 更新任务数据
        task_data["results"]["script"] = script_result
        task_data["results"]["storyboard"] = f"分镜脚本已生成 ({img_summary})" if script_result.get("scenes") else "脚本生成降级"
        task_data["results"]["characters"] = f"角色设计: {char_img_count}/{len(script_result.get('characters', []))}个有图" if script_result.get("characters") else "角色设计降级"
        task_data["results"]["backgrounds"] = f"背景设计: {scene_img_count}个场景有图" if scene_img_count > 0 else "背景设计降级"
        task_data["files"]["script_file"] = str(script_file)
        task_data["files"]["storyboard_file"] = str(script_file)  # 同一个JSON
        task_data["files"]["characters_file"] = str(script_file)
        task_data["files"]["backgrounds_file"] = str(script_file)
        total_shots = sum(len(s.get("shots", [])) for s in script_result.get("scenes", []))
        task_data["agents"]["writer"] = {
            "status": "completed",
            "started_at": datetime.now().isoformat(),
            "completed_at": datetime.now().isoformat(),
            "result_summary": f"创作完成，{total_shots}镜头 + {img_summary}",
            "can_modify": True,
            "can_regenerate": True,
            "modification_count": len(task_data["modifications"]["script"])
        }
        task_data["status"] = self.STATUS["WRITTEN"]
        task_data["current_step"] = "produce"
        task_data["progress"] = 50

        self._save_task(task_id, task_data)

        return {
            "success": True,
            "message": f"创作步骤完成 ({img_summary})",
            "task_id": task_id,
            "result": script_result,
            "next_step": "produce",
            "next_step_description": "下一步：制作视频(图生视频模式)",
            "can_modify": True,
            "can_regenerate": True,
            "modification_count": len(task_data["modifications"]["script"]),
            "image_assets": {"characters": char_img_count, "scenes": scene_img_count, "shot_refs": shot_ref_count}
        }

    def execute_produce(self, task_id: str, video_mode: str = "auto", shot_overrides: Optional[Dict] = None) -> Dict[str, Any]:
        """执行制作步骤 - 支持视频模式选择和单个镜头重生成
        video_mode: 'auto'(自动:有图则图生)/'img2vid'(强制图生)/'text2vid'(强制文生)
        shot_overrides: {shot_id: {"prompt": "修改后的提示词", "mode": "img2vid/text2vid"}}
        """
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        if not self.can_execute_step(task_id, "produce"):
            return {"success": False, "message": "当前不能执行制作步骤"}

        task_data["status"] = self.STATUS["PRODUCING"]
        task_data["current_step"] = "produce"
        task_data["progress"] = 60
        task_data["updated_at"] = datetime.now().isoformat()

        # 记录用户动作
        task_data["user_actions"].append({
            "action": "execute_produce",
            "timestamp": datetime.now().isoformat(),
            "description": "用户执行制作步骤(图生视频模式)"
        })

        # 调用真实视频生成API
        task_dir = Path(task_data["files"]["task_dir"])
        videos_dir = task_dir / "videos"
        images_dir = task_dir / "images"
        videos_dir.mkdir(parents=True, exist_ok=True)
        images_dir.mkdir(parents=True, exist_ok=True)

        # 从脚本结果中提取镜头和角色
        script_data = task_data.get("results", {}).get("script", {})
        scenes = script_data.get("scenes", []) if isinstance(script_data, dict) else []
        characters = script_data.get("characters", []) if isinstance(script_data, dict) else []

        produced_shots = []
        video_files = []
        img2vid_count = 0
        text2vid_count = 0

        # 决定每个镜头的视频模式
        def get_shot_mode(shot_id, has_ref_image):
            """根据video_mode设置决定镜头模式"""
            override = (shot_overrides or {}).get(str(shot_id), {})
            if override.get("mode"):
                return override["mode"]
            if video_mode == "text2vid":
                return "text2vid"
            if video_mode == "img2vid":
                return "img2vid" if has_ref_image else "text2vid"
            # auto: 有图则图生
            return "img2vid" if has_ref_image else "text2vid"

        if scenes:
            # 有LLM生成的分镜脚本，按脚本生成
            total_shots = sum(len(s.get("shots", [])) for s in scenes)
            for scene in scenes:
                for shot in scene.get("shots", []):
                    shot_id = shot.get("shot_id", len(produced_shots) + 1)
                    override = (shot_overrides or {}).get(str(shot_id), {})
                    video_prompt = override.get("prompt") or shot.get("video_prompt", shot.get("description", ""))
                    if not video_prompt:
                        produced_shots.append({"shot_id": shot_id, "status": "skipped", "reason": "无提示词"})
                        continue

                    # 匹配参考图
                    ref_image = self._resolve_shot_ref_image(shot, scene, characters)
                    shot_mode = get_shot_mode(shot_id, ref_image and Path(ref_image).exists())

                    try:
                        if shot_mode == "img2vid" and ref_image and Path(ref_image).exists():
                            logger.info(f"  🎨 图生视频 镜头{shot_id}/{total_shots}: ref={Path(ref_image).name}")
                            try:
                                result = generate_video_from_image(
                                    image_path=ref_image,
                                    prompt=video_prompt,
                                    ratio="9:16",
                                    duration=shot.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),
                                )
                                img2vid_count += 1
                            except Exception as img_err:
                                # 图生视频失败(如敏感内容)，降级到文生视频
                                if "SensitiveContent" in str(img_err) or "sensitive" in str(img_err).lower():
                                    logger.warning(f"  ⚠ 图生视频触发敏感内容检测，降级到文生视频: 镜头{shot_id}")
                                    shot_mode = "text2vid_fallback"
                                else:
                                    raise

                        if shot_mode.startswith("text2vid"):
                            logger.info(f"  ✏ 文生视频 镜头{shot_id}/{total_shots}")
                            # 文生视频需要更详细的描述
                            text_prompt = video_prompt
                            if shot_mode == "text2vid":
                                # 强制文生模式时，补充角色和场景描述
                                desc_parts = []
                                if shot.get("description"):
                                    desc_parts.append(shot["description"])
                                for c in characters:
                                    if c.get("name") and c.get("name") in text_prompt:
                                        desc_parts.append(f"角色{c['name']}: {c.get('description','')}")
                                if scene.get("location"):
                                    desc_parts.append(f"场景: {scene['location']}")
                                if len(desc_parts) > 1:
                                    text_prompt = "，".join(desc_parts) + "，" + video_prompt
                            result = generate_video(
                                prompt=text_prompt,
                                ratio="9:16",
                                duration=shot.get("duration_seconds", 5),
                                download_dir=str(videos_dir),
                                timeout_wait=600,
                            )
                            text2vid_count += 1

                        shot_result = {
                            "shot_id": shot_id,
                            "status": "succeeded",
                            "video_url": result.get("video_url", ""),
                            "local_path": result.get("local_path", ""),
                            "file_size": result.get("file_size", 0),
                            "duration": result.get("duration"),
                            "resolution": result.get("resolution"),
                            "ref_image": ref_image,  # 保留参考图路径，即使降级为text2vid_fallback也方便前端展示
                            "mode": shot_mode,
                        }
                        # 尝试添加字幕
                        if result.get("local_path") and shot.get("dialogue"):
                            try:
                                subtitled = self._add_subtitles_to_video(result["local_path"], shot)
                                if subtitled:
                                    shot_result["local_path"] = subtitled
                                    shot_result["has_subtitles"] = True
                                    # 更新video_files
                                    if subtitled not in video_files:
                                        video_files.append(subtitled)
                            except Exception as sub_err:
                                logger.warning(f"  字幕添加失败: {sub_err}")
                        if result.get("local_path") and result["local_path"] not in video_files:
                            video_files.append(result["local_path"])
                        produced_shots.append(shot_result)
                    except Exception as e:
                        logger.error(f"  镜头{shot_id}失败: {e}")
                        produced_shots.append({
                            "shot_id": shot_id, "status": "failed", "error": str(e),
                            "ref_image": ref_image, "mode": "img2vid" if ref_image else "text2vid"
                        })
        else:
            # 没有分镜脚本，用任务描述直接生成单个视频
            try:
                result = generate_video(

[680 more lines in file. Use offset=800 to continue.]


                    prompt=task_data.get("description", "一段精彩的短剧片段"),
                    ratio="9:16",
                    duration=5,
                    download_dir=str(videos_dir),
                    timeout_wait=600,
                )
                produced_shots.append({
                    "shot_id": 1,
                    "status": "succeeded",
                    "video_url": result.get("video_url", ""),
                    "local_path": result.get("local_path", ""),
                    "file_size": result.get("file_size", 0),
                    "mode": "text2vid",
                })
                text2vid_count += 1
                if result.get("local_path"):
                    video_files.append(result["local_path"])
            except Exception as e:
                produced_shots.append({"shot_id": 1, "status": "failed", "error": str(e), "mode": "text2vid"})

        succeeded = sum(1 for s in produced_shots if s.get("status") == "succeeded")

        # 尝试自动拼接视频
        stitch_result = None
        if len(video_files) > 1:
            try:
                stitch_result = stitch_videos(video_files, str(videos_dir / "merged_full.mp4"))
                if stitch_result.get("status") == "succeeded":
                    logger.info(f"视频自动拼接成功: {stitch_result['output_path']}")
            except Exception as e:
                logger.warning(f"视频自动拼接失败: {e}")

        video_result = {
            "is_real_data": True,
            "total_shots": len(produced_shots),
            "succeeded": succeeded,
            "shots": produced_shots,
            "video_files": video_files,
            "img2vid_count": img2vid_count,
            "text2vid_count": text2vid_count,
            "stitch_result": stitch_result,
            "video_info": {
                "title": script_data.get("title", "短剧") if isinstance(script_data, dict) else "短剧",
                "duration": f"{len(produced_shots) * 5}秒",
                "resolution": "720x1280",
                "format": "mp4",
            },
            "video_mode": video_mode,
            "generated_at": datetime.now().isoformat(),
            "version": 1
        }

        # 确定video_file路径（第一个成功的视频）
        primary_video = video_files[0] if video_files else None

        # 更新任务数据
        task_data["results"]["video"] = video_result
        task_data["files"]["video_file"] = str(primary_video) if primary_video else ""
        task_data["agents"]["producer"] = {
            "status": "completed",
            "started_at": datetime.now().isoformat(),
            "completed_at": datetime.now().isoformat(),
            "result_summary": f"制作完成，{succeeded}/{len(produced_shots)}镜头 (🎨图生视频={img2vid_count} ✏文生视频={text2vid_count})",
            "can_regenerate": True
        }
        task_data["status"] = self.STATUS["PRODUCED"]
        task_data["current_step"] = "review"
        task_data["progress"] = 70

        self._save_task(task_id, task_data)

        return {
            "success": True,
            "message": "制作步骤完成",
            "task_id": task_id,
            "result": video_result,
            "next_step": "review",
            "next_step_description": "下一步：审核质量",
            "can_modify": False,
            "can_regenerate": True
        }

    def execute_review(self, task_id: str, user_feedback: Optional[Dict] = None) -> Dict[str, Any]:
        """执行审核步骤"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        if not self.can_execute_step(task_id, "review"):
            return {"success": False, "message": "当前不能执行审核步骤"}

        task_data["status"] = self.STATUS["REVIEWING"]
        task_data["current_step"] = "review"
        task_data["progress"] = 80
        task_data["updated_at"] = datetime.now().isoformat()

        # 记录用户动作
        action_desc = "用户执行审核步骤"
        if user_feedback:
            action_desc = "用户提供反馈并重新审核"
            task_data["modifications"]["review"].append({
                "timestamp": datetime.now().isoformat(),
                "user_feedback": user_feedback,
                "version": len(task_data["modifications"]["review"]) + 2
            })

        task_data["user_actions"].append({
            "action": "execute_review",
            "timestamp": datetime.now().isoformat(),
            "description": action_desc
        })

        # 真实审核官工作 - 调用LLM进行内容审核
        review_result = self._real_review(task_data, user_feedback)

        # 保存审核结果
        task_dir = Path(task_data["files"]["task_dir"])
        review_file = task_dir / "review_result.json"
        with open(review_file, 'w', encoding='utf-8') as f:
            json.dump(review_result, f, ensure_ascii=False, indent=2)

        # 更新任务数据
        task_data["results"]["review"] = review_result
        task_data["files"]["review_file"] = str(review_file)
        task_data["agents"]["publisher"] = {
            "status": "completed",
            "started_at": datetime.now().isoformat(),
            "completed_at": datetime.now().isoformat(),
            "result_summary": "审核完成，作品质量优秀",
            "can_modify": True,
            "can_regenerate": True,
            "modification_count": len(task_data["modifications"]["review"])
        }
        task_data["status"] = self.STATUS["REVIEWED"]
        task_data["current_step"] = "complete"
        task_data["progress"] = 100

        self._save_task(task_id, task_data)

        return {
            "success": True,
            "message": "审核步骤完成",
            "task_id": task_id,
            "result": review_result,
            "next_step": "complete",
            "next_step_description": "所有步骤完成！",
            "can_modify": True,
            "can_regenerate": True,
            "modification_count": len(task_data["modifications"]["review"])
        }

    def _real_review(self, task_data: Dict, user_feedback: Optional[Dict] = None) -> Dict[str, Any]:
        """调用LLM真实执行审核官工作"""
        from real_api_client import generate_text

        # 收集任务上下文
        collect_result = task_data.get("results", {}).get("collect", {})
        write_result = task_data.get("results", {}).get("write", {})
        produce_result = task_data.get("results", {}).get("produce", {})

        # 构建审核提示词
        task_title = task_data.get("title", "未知任务")
        story_info = ""
        if write_result:
            story_info = f"故事标题: {write_result.get('title', '未知')}\n"
            characters = write_result.get('characters', [])
            if characters:
                story_info += f"角色: {', '.join([c.get('name', '?') for c in characters])}\n"
            scenes = write_result.get('scenes', [])
            if scenes:
                story_info += f"场景数: {len(scenes)}\n"

        video_info = ""
        if produce_result:
            shots = produce_result.get('shots', [])
            succeeded = [s for s in shots if s.get('status') == 'succeeded']
            failed = [s for s in shots if s.get('status') == 'failed']
            video_info = f"视频镜头: {len(succeeded)}成功/{len(failed)}失败\n"
            if produce_result.get('stitch_result'):
                stitch = produce_result['stitch_result']
                video_info += f"拼接状态: {stitch.get('status', '未知')}\n"

        user_feedback_str = ""
        if user_feedback:
            user_feedback_str = f"\n用户反馈: {json.dumps(user_feedback, ensure_ascii=False)}"

        prompt = f"""你是一个专业的漫画短视频审核官，请对以下作品进行全面审核评估。

## 任务信息
标题: {task_title}
{story_info}{video_info}
## 要求
请从以下维度进行专业评估，输出JSON格式：
1. quality_assessment: 质量评估(每项1-10分)
   - overall_quality: 总体质量
   - creativity: 创意性
   - technical_execution: 技术执行
   - storytelling: 叙事能力
   - visual_impact: 视觉冲击力
2. compliance_check: 合规检查

[480 more lines in file. Use offset=1000 to continue.]


   - content_appropriate: 内容是否适当
   - copyright_clear: 版权是否清晰
   - platform_compliant: 是否符合平台规范
   - brand_consistent: 品牌一致性
3. improvement_suggestions: 改进建议列表(3-5条具体建议)
4. publish_recommendation: 发布建议
   - recommended: 是否推荐发布
   - platforms: 推荐平台列表
   - publish_time: 最佳发布时间
   - target_audience: 目标受众
5. final_verdict: 最终裁决
   - status: approved/revision_needed/rejected
   - message: 裁决说明
   - publish_ready: 是否可以发布

只输出JSON，不要其他文字。{user_feedback_str}"""

        try:
            llm_response = generate_text(
                prompt=prompt,
                system_prompt="你是专业的内容审核官，只输出JSON格式。",
                max_tokens=2048,
                temperature=0.3
            )
            # 解析LLM返回的JSON
            llm_response = llm_response.strip()
            if llm_response.startswith('```'):
                llm_response = llm_response.split('\n', 1)[-1].rsplit('```', 1)[0]
            review_result = json.loads(llm_response)
            logger.info("LLM审核完成")
        except Exception as e:
            logger.warning(f"LLM审核失败，使用默认评估: {e}")
            review_result = {
                "quality_assessment": {
                    "overall_quality": 7.0,
                    "creativity": 7.0,
                    "technical_execution": 7.0,
                    "storytelling": 7.0,
                    "visual_impact": 7.0
                },
                "compliance_check": {
                    "content_appropriate": True,
                    "copyright_clear": True,
                    "platform_compliant": True,
                    "brand_consistent": True
                },
                "improvement_suggestions": [
                    "LLM审核服务暂时不可用，请人工复核",
                    "检查视频质量和连贯性",
                    "确认内容合规性"
                ],
                "publish_recommendation": {
                    "recommended": True,
                    "platforms": ["B站", "抖音"],
                    "publish_time": "18:00-20:00",
                    "target_audience": "一般观众"
                },
                "final_verdict": {
                    "status": "approved",
                    "message": "自动审核服务异常，默认通过，建议人工复核",
                    "publish_ready": True
                }
            }

        review_result["generated_at"] = datetime.now().isoformat()
        review_result["version"] = len(task_data["modifications"]["review"]) + 1
        review_result["user_feedback"] = user_feedback
        review_result["llm_powered"] = True
        return review_result

    def modify_result(self, task_id: str, step: str, modifications: Dict[str, Any]) -> Dict[str, Any]:
        """修改指定步骤的结果"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        # 检查是否可以修改
        agent_info = task_data["agents"].get(step)
        if not agent_info or not agent_info.get("can_modify", False):
            return {"success": False, "message": f"不支持修改{step}步骤的结果"}

        # 记录修改
        task_data["modifications"][step].append({
            "timestamp": datetime.now().isoformat(),
            "modifications": modifications,
            "version": len(task_data["modifications"][step]) + 1
        })

        task_data["status"] = self.STATUS["MODIFYING"]
        task_data["updated_at"] = datetime.now().isoformat()

        self._save_task(task_id, task_data)

        return {
            "success": True,
            "message": f"{step}步骤修改已保存",
            "task_id": task_id,
            "modification_count": len(task_data["modifications"][step]),
            "next_action": f"重新执行{step}步骤以应用修改"
        }

    def regenerate_result(self, task_id: str, step: str) -> Dict[str, Any]:
        """重新生成指定步骤的结果"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        # 检查是否可以重新生成
        agent_info = task_data["agents"].get(step)
        if not agent_info or not agent_info.get("can_regenerate", False):
            return {"success": False, "message": f"不支持重新生成{step}步骤的结果"}

        task_data["status"] = self.STATUS["REGENERATING"]
        task_data["updated_at"] = datetime.now().isoformat()

        self._save_task(task_id, task_data)

        # 根据步骤调用相应的执行函数
        if step == "collect":
            # 获取之前的修改作为输入
            modifications = task_data["modifications"].get("collect", [])
            user_input = modifications[-1]["modifications"] if modifications else None
            return self.execute_collect(task_id, user_input)
        elif step == "write" or step == "writer":
            # 获取之前的修改作为输入
            modifications = task_data["modifications"].get("script", [])
            user_input = modifications[-1]["modifications"] if modifications else None
            return self.execute_write(task_id, user_input)
        elif step == "produce" or step == "producer":
            return self.execute_produce(task_id)
        elif step == "review" or step == "publisher":
            # 获取之前的反馈作为输入
            modifications = task_data["modifications"].get("review", [])
            user_feedback = modifications[-1]["user_feedback"] if modifications else None
            return self.execute_review(task_id, user_feedback)
        else:
            return {"success": False, "message": f"未知的步骤: {step}"}

    def get_task_status(self, task_id: str) -> Dict[str, Any]:
        """获取任务状态"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        # 构建状态信息
        status_info = {
            "task_id": task_data["task_id"],
            "status": task_data["status"],
            "current_step": task_data["current_step"],
            "progress": task_data["progress"],
            "description": task_data["description"],
            "created_at": task_data["created_at"],
            "updated_at": task_data["updated_at"],
            "next_step": self.get_next_step(task_id),
            "available_steps": [],
            "agents": {}
        }

        # 检查每个步骤是否可用
        for step, step_name in [("collect", "采集"), ("write", "创作"), ("produce", "制作"), ("review", "审核")]:
            if self.can_execute_step(task_id, step):
                status_info["available_steps"].append({
                    "step": step,
                    "name": step_name,
                    "can_execute": True
                })

        # 添加Agent状态
        for agent_name, agent_data in task_data.get("agents", {}).items():
            status_info["agents"][agent_name] = {
                "status": agent_data.get("status", "pending"),
                "can_modify": agent_data.get("can_modify", False),
                "can_regenerate": agent_data.get("can_regenerate", False),
                "modification_count": agent_data.get("modification_count", 0)
            }

        return {
            "success": True,
            "task": status_info
        }

    def get_step_result(self, task_id: str, step: str) -> Dict[str, Any]:
        """获取指定步骤的结果"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        result_map = {
            "collect": task_data["results"]["collection"],
            "write": task_data["results"]["script"],
            "produce": task_data["results"]["video"],
            "review": task_data["results"]["review"]
        }

        result = result_map.get(step)
        if not result:
            return {"success": False, "message": f"步骤{step}的结果不存在"}

        agent_info = task_data["agents"].get(step, {})
        modifications = task_data["modifications"].get(step, [])

[280 more lines in file. Use offset=1200 to continue.]


Now I have a thorough understanding of the codebase. Let me plan the modifications systematically. This is a large change set - I'll use a sub-agent to implement the backend changes and another for the frontend changes. But actually, given the complexity and interdependencies, let me do this myself methodically.

Let me organize the changes:

**Backend (user_driven_task_system.py + user_driven_api_server.py):**

1. **Create task with duration/channels** - modify CreativeDescription model + create_task method
2. **Delete task** - add delete_task method + DELETE endpoint
3. **Regenerate collect with modification** - fix regenerate_result to pass modification correctly
4. **Write step: character description refresh on modify** - when regenerating character images, also update description
5. **Write step: scene/shot modification support** - add endpoints for modifying scenes/shots
6. **Review step: pass full context to LLM** - fix _real_review to include actual content
7. **Review step: add publish suggestions** - enhance prompt
8. **Produce step: use task's video_mode config** - read from task config
9. **Produce step: step linkage** - re-read latest write results when producing
10. **Stitch with transitions** - add xfade filter

**Frontend (user_driven_coordinator.js + HTML):**

1. Create task form: add duration dropdown + channels multi-select
2. Delete task button
3. Modify collect: modal dialog + history
4. Write modify: show original content, support scene/shot modification
5. Produce: video player, show ref images, remove video mode (moved to create)
6. Review: show publish recommendations
7. View results → expand/collapse

This is massive. Let me use a sub-agent to implement this since it's a long-running task.

Actually, let me think about what's the most efficient approach. I'll implement the changes directly since I understand the codebase well now. Let me start with the backend modifications, then frontend.

Let me start implementing.

现在让我来阅读前端 JS 和 HTML，以便在实现之前完全理解当前的状态：