A8多套打包+M4归档+R5多图:存量功能备份

A8 多套交付包(packaging_task.py):
- 修复交付包只打第1条混乱note的bug,按ImageCandidate.strategy分A/B/C组
- 每组生独立note_0N夹(6图+文案.txt),同seq留最新去重,老数据兼容
- task74端到端验:3套各6图,独立agent7项交叉验证全过

M4 归档(tasks.py/exports.py/前端):
- list_tasks加date_from/date_to/product_id筛选+product_name批量填(防N+1)
- 新增exports.py:产品JSON导出+标杆CSV导出(UTF-8 BOM)
- 前端HistoryFilters日期/产品筛选+产品列+打回原因红banner
- response.py加raise_param_error;独立agent验A1/A2/A9通过

R5 产品多图(product_images.py/020迁移/前端):
- product_images表+5端点(上传/列/改场景/设主图/删图)
- 生图按ROLE_SCENE_PREFERENCE选对应场景图,回落primary
- 前端ProductImageManager多图画廊

R6 账号config拆页(settings/):
- 配置页按角色拆/settings(运营+组长+admin)+/config(仅admin)
- Key只显末4位不显余额(守红线)

核销表对齐真实代码状态:D1改稿框/M7裂变/E12评图分纠偏为已完成(曾漏回写)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-18 17:32:49 +08:00
parent 285791c12f
commit 4bed7425a8
41 changed files with 1211 additions and 236 deletions

View File

@@ -42,47 +42,62 @@ def build_delivery_package(self, package_id: int) -> dict:
settings = get_settings()
upload_base = settings.UPLOAD_BASE_PATH.rstrip("/")
selected_text = db.query(TextCandidate).filter(
# A8 多套打包:按 strategy(A/B/C 三套正交叙事)分组,每套成一篇独立 note。
# 北哥拿到的交付包含完整 3 套note_01/02/03不再只打第 1 条文案。
selected_texts = 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:
).order_by(TextCandidate.id).all()
if not selected_texts:
raise ValueError("无已选文案,请先选择文案")
text_data = json.loads(selected_text.content or "{}")
images_data = []
for ic in selected_images:
# 整套全打:一套内全部图按 seq 排序进包,不只打封面。重生场景同 (strategy,seq)
# 可能多条(新增不删旧),去重取最新(id最大),避免包内重复图。
all_images = db.query(ImageCandidate).filter(
ImageCandidate.task_id == task_id,
).order_by(ImageCandidate.strategy, ImageCandidate.seq).all()
def _read_image(ic) -> dict:
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
# url 已含 uploads 前缀;工作目录 /applstrip 当相对路径读,勿再拼 base(防 uploads/uploads)
try:
with open(abs_path, "rb") as f:
with open(ic.url.lstrip("/"), "rb") as f:
img_bytes = f.read()
except OSError as e:
logger.warning("图片读取失败,跳过:%s %s", abs_path, e)
images_data.append({
logger.warning("图片读取失败,跳过:%s %s", ic.url, e)
return {
"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)),
}]
# 按 strategy 分组A/B/C老数据 strategy=None 归一套,向后兼容)
from collections import OrderedDict
groups: "OrderedDict[str, dict]" = OrderedDict()
for ic in all_images:
slot = groups.setdefault(ic.strategy or "_", {})
prev = slot.get(ic.seq)
if prev is None or ic.id > prev.id:
slot[ic.seq] = ic # 同 seq 留最新
notes = []
for idx, (_strategy, slot) in enumerate(groups.items()):
images_data = [_read_image(slot[k]) for k in sorted(slot)]
# 文案配对:选中文案数≥套数则一套一条;否则各套共用第 1 条
# (图均以第 1 条文案为语境生成,共用合理;多选则尊重运营按套选的文案)
tc = selected_texts[idx] if idx < len(selected_texts) else selected_texts[0]
text_data = json.loads(tc.content or "{}")
notes.append({
"title": text_data.get("title", ""),
"content": text_data.get("content", ""),
"tags": text_data.get("tags", []),
"images": images_data,
"banned_word_status": (tc.banned_word_status.value
if hasattr(tc.banned_word_status, "value")
else str(tc.banned_word_status)),
})
if not notes:
raise ValueError("无图片候选,无法打包")
from app.services.ai_engine.package_exporter import build_delivery_package as do_build
# 打包产物放专用目录 uploads/packages/,与图片目录 uploads/{ws}/{task}/ 分开