""" app/workers/packaging_task.py — 交付打包 Celery 任务 build_delivery_package:查已选文案+图片 → package_exporter → 存路径 """ import json import logging from app.workers.celery_app import celery_app logger = logging.getLogger(__name__) def _get_db(): from app.core.database import SessionLocal return SessionLocal() @celery_app.task( bind=True, name="app.workers.tasks.build_delivery_package", max_retries=2, default_retry_delay=10, queue="packaging", ) def build_delivery_package(self, package_id: int) -> dict: """打包交付任务。查 delivery_packages → 收集笔记 → package_exporter""" logger.info("build_delivery_package start: package_id=%s", package_id) db = _get_db() try: from app.models.task import DeliveryPackage, TextCandidate, ImageCandidate from app.constants.enums import PackageStatus pkg = db.query(DeliveryPackage).filter(DeliveryPackage.id == package_id).first() if not pkg: raise ValueError(f"package_id={package_id} not found") workspace_id = pkg.workspace_id task_id = pkg.task_id from app.core.config import get_settings settings = get_settings() upload_base = settings.UPLOAD_BASE_PATH.rstrip("/") selected_text = db.query(TextCandidate).filter( TextCandidate.task_id == task_id, TextCandidate.is_selected == True, ).first() # 整套全打(倩倩姐2026-06-08拍板):一条笔记的全部图按 seq 排序进包, # 不再只打 is_selected 的封面。北哥6张标准套 seq=1 是 hook 封面,天然排第一。 selected_images = db.query(ImageCandidate).filter( ImageCandidate.task_id == task_id, ).order_by(ImageCandidate.seq).all() if not selected_text: raise ValueError("无已选文案,请先选择文案") text_data = json.loads(selected_text.content or "{}") images_data = [] for ic in selected_images: img_bytes = b"" if ic.url: # url 形如 /uploads/ws/task/file.jpg,本身已含 uploads 前缀。 # 工作目录是 /app,直接 lstrip("/") 当相对路径读,不能再拼 upload_base(会重复 uploads/uploads)。 rel = ic.url.lstrip("/") abs_path = rel try: with open(abs_path, "rb") as f: img_bytes = f.read() except OSError as e: logger.warning("图片读取失败,跳过:%s %s", abs_path, e) images_data.append({ "seq": ic.seq, "role": ic.role.value if hasattr(ic.role, "value") else str(ic.role), "data": img_bytes, }) notes = [{ "title": text_data.get("title", ""), "content": text_data.get("content", ""), "tags": text_data.get("tags", []), "images": images_data, "banned_word_status": (selected_text.banned_word_status.value if hasattr(selected_text.banned_word_status, "value") else str(selected_text.banned_word_status)), }] from app.services.ai_engine.package_exporter import build_delivery_package as do_build # 打包产物放专用目录 uploads/packages/,与图片目录 uploads/{ws}/{task}/ 分开 packages_base = f"{upload_base}/packages" zip_path = do_build(workspace_id, task_id, notes, base_path=packages_base) pkg.package_path = zip_path pkg.download_url = f"/api/v1/delivery-packages/{package_id}/download-file" pkg.status = PackageStatus.READY db.commit() logger.info("delivery package ready: package_id=%s path=%s", package_id, zip_path) return {"package_id": package_id, "status": "ready", "path": zip_path} except Exception as exc: logger.error("build_delivery_package failed: package_id=%s err=%s", package_id, exc) raise self.retry(exc=exc) finally: db.close()