Files
beige/backend/app/services/ai_engine/package_exporter.py
yangqianqian df1856d793 上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事
- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段);
  修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品
- dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开;
  settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch
- 新增用户管理(users API/admin service/改密页) + alembic 022/023/024
- 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example
- A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:08:13 +08:00

133 lines
5.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
package_exporter.py — 达人素材交付包生成
架构方案§五 1A步骤5按笔记分文件夹 + 图(01/02/03) + 文案.txt + 发布清单 + 合规说明
路径规则:{UPLOAD_ABS_ROOT}/packages/{workspace_id}/{task_id}/note_{n}/
"""
from __future__ import annotations
import json
import logging
import os
import zipfile
from datetime import datetime
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
# 文件夹结构
# uploads/packages/{workspace_id}/{task_id}/
# note_01/
# 01_hook.jpg # 按 seq 序号命名防传错序
# 02_proof.jpg
# 文案.txt # 标题 + 正文 + 标签
# note_02/
# ...
# 📋发布清单.txt
# ✅合规说明.txt
# package.zip # 最终打包文件
def build_delivery_package(
workspace_id: int,
task_id: int,
notes: list[dict], # 每条笔记,含 text_candidate + image_candidates
base_path: str = "uploads/packages",
) -> str:
"""
打包交付,返回 zip 文件的本地路径。
notes 格式:[{
"title": str, "content": str, "tags": list[str],
"images": [{"seq": int, "role": str, "data": bytes}],
"banned_word_status": str, # 合规说明用
}]
"""
package_dir = Path(base_path) / str(workspace_id) / str(task_id)
package_dir.mkdir(parents=True, exist_ok=True)
note_dirs: list[Path] = []
for idx, note in enumerate(notes, start=1):
note_dir = package_dir / f"note_{idx:02d}"
note_dir.mkdir(exist_ok=True)
note_dirs.append(note_dir)
# ── 图片文件(按 seq 序号命名)
for img in sorted(note.get("images", []), key=lambda x: x.get("seq", 0)):
seq = img.get("seq", idx)
role = img.get("role", "img")
fname = f"{seq:02d}_{role}.jpg"
img_data = img.get("data", b"")
if img_data:
(note_dir / fname).write_bytes(img_data)
# ── 文案.txt标题 + 正文 + 标签,达人可直接复制)
tags = note.get("tags") or []
body = note.get("content", "")
# 正文末尾如果 LLM 已写入 #话题 标签,不再重复追加(避免重复)
body_has_tags = bool(tags) and any(
t.strip("#") in body for t in tags if t
)
copy_lines = [
f"【标题】{note.get('title', '')}",
"",
body,
]
if tags and not body_has_tags:
copy_lines += ["", " ".join(tags)]
(note_dir / "文案.txt").write_text("\n".join(copy_lines), encoding="utf-8")
# ── 发布清单.txt
checklist_lines = [
"📋 发布清单",
f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}",
f"任务ID{task_id}",
"",
]
for idx, note in enumerate(notes, start=1):
title = note.get("title", f"笔记{idx}")
n_images = len(note.get("images", []))
checklist_lines.append(f"note_{idx:02d} 标题:{title[:30]} 图片数:{n_images}")
checklist_lines += [
"",
"发布注意事项:",
"- 每条笔记图片按 01/02/03 顺序上传,避免传错序",
"- 文案.txt 中标题/正文/标签已区分,复制对应部分",
"- 品牌词已植入,请勿删除",
"- 不要添加链接(种品牌词,引导天猫搜索成交)",
]
(package_dir / "📋发布清单.txt").write_text("\n".join(checklist_lines), encoding="utf-8")
# ── 合规说明.txt
compliance_lines = [
"✅ 合规说明",
f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}",
"",
"本批次内容已完成以下合规处理:",
"1. 违禁词扫描(美白/祛斑/速效/医用/药妆等)",
"2. 视觉违禁词处理(前后对比/变白等)",
"3. 图片去水印处理C2PA元数据已清除",
"",
"各笔记合规状态:",
]
for idx, note in enumerate(notes, start=1):
status = note.get("banned_word_status", "pass")
status_label = {"pass": "✅通过", "auto_fixed": "✅自动改写", "soft_warn": "⚠️软提示", "hard_block": "❌硬拦截"}.get(status, status)
compliance_lines.append(f"note_{idx:02d}{status_label}")
compliance_lines += [
"",
"C2PA元数据可去除私有像素水印只能削弱不保证100%清除。",
"如有合规疑问,请联系运营团队。",
]
(package_dir / "✅合规说明.txt").write_text("\n".join(compliance_lines), encoding="utf-8")
# ── 打 zip
zip_path = package_dir / "package.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for note_dir in note_dirs:
for fpath in sorted(note_dir.iterdir()):
zf.write(fpath, arcname=f"{note_dir.name}/{fpath.name}")
zf.write(package_dir / "📋发布清单.txt", arcname="📋发布清单.txt")
zf.write(package_dir / "✅合规说明.txt", arcname="✅合规说明.txt")
logger.info("delivery package built: %s (notes=%d)", zip_path, len(notes))
return str(zip_path)