""" app/services/fission_service.py — 第11环裂变 扇出 service 1爆款 → N套完整笔记包。每套=一个独立 GenerationTask(回填 source_fission_id), 复用主管道 run_generation_pipeline 生成文案+配图。 🔴 占位档位规则见 fission_prompt.py,真实措辞待北哥定义(倩倩姐2026-06-16先打通)。 """ import json import logging from sqlalchemy.orm import Session from app.core.response import raise_business from app.middleware.workspace_guard import CurrentUser from app.models.fission import FissionTask from app.models.task import GenerationTask from app.services.ai_engine.fission_prompt import valid_level from app.services.task_service import _check_user_has_key, enqueue_generation logger = logging.getLogger(__name__) MAX_FANOUT = 5 # 每用户并发上限5(红线),裂变扇出数受同等约束 def create_fission( db: Session, current_user: CurrentUser, source_task_id: int, reference_level: str, fanout_count: int, ) -> tuple[int, list[int]]: """ 从一个源任务裂变出 N 套子任务。 返回 (fission_id, [子task_id,...])。 """ _check_user_has_key(db, current_user.user_id, current_user.workspace_id) level = valid_level(reference_level) n = max(1, min(fanout_count or 3, MAX_FANOUT)) # 读源任务(取产品+主题+已选文案作为爆款源) src = db.query(GenerationTask).filter( GenerationTask.id == source_task_id, GenerationTask.workspace_id == current_user.workspace_id, ).first() if not src: raise_business("裂变源任务不存在") source_note = {"title": src.theme or "", "content": _extract_source_copy(db, src)} # 建 fission_task 记录 ft = FissionTask( workspace_id=current_user.workspace_id, source_note=json.dumps(source_note, ensure_ascii=False), reference_level=level, fanout_count=n, status="generating", ) db.add(ft); db.commit(); db.refresh(ft) # 扇出 N 个子任务(回填 source_fission_id),复用主管道 task_ids: list[int] = [] for _ in range(n): sub = GenerationTask( workspace_id=current_user.workspace_id, product_id=src.product_id, operator_id=current_user.user_id, theme=src.theme, text_count=src.text_count, image_count=src.image_count, track="ai", need_product_image=src.need_product_image, status="pending", source_fission_id=ft.id, ) db.add(sub); db.commit(); db.refresh(sub) enqueue_generation(sub.id) task_ids.append(sub.id) logger.info("fission created id=%s level=%s fanout=%s tasks=%s", ft.id, level, n, task_ids) return ft.id, task_ids def _extract_source_copy(db: Session, src: GenerationTask) -> str: """取源任务已选/最高分文案当爆款正文;无则用主题兜底。""" from app.models.task import TextCandidate c = (db.query(TextCandidate) .filter(TextCandidate.task_id == src.id) .order_by(TextCandidate.is_selected.desc(), TextCandidate.id.asc()) .first()) if c and c.content: return c.content return src.theme or ""