- 产品编辑入口统一走 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>
133 lines
5.1 KiB
Python
133 lines
5.1 KiB
Python
"""
|
||
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)
|