# 模块 B 创意搭建 — Code Review TODO

> 来源:2026-06-08 端到端打通后的工程 review。
> 状态:端到端可单广告挂创意,但生产批量上线前必须做完 P0。
> 验证基线:`83846793 / 106275052398` 已成功挂上 `10030789982`(已手动删除)。

## P0 — 必须修(影响业务正确性)

### P0-NEW-1. 生产 cron 化前必须修:Phase 0 广告池"真已满"判定 + 唯一性预校验(2026-06-10 发现)
- 现状:
  - `phase0_create_ads` 检查 `configured_status == AD_STATUS_NORMAL` 判定"广告满了"
  - **失败的 SUSPEND/DENIED 广告不算 NORMAL** → Phase 0 视为"未满" → enumerate 重复 candidate
  - candidate 走完飞书审批 → 真 POST → 撞腾讯唯一性 reject `code 1901634`(腾讯文档:删除后 2 年内仍占唯一性槽位)
  - 运营每天审批表都是"建失败"的广告,体验差
- 触发场景:
  - 任意 Phase 0 POST 失败留下的 SUSPEND 广告
  - 任意运营在腾讯后台手动暂停的广告
  - 跨日 cron 不断重试建同 SOP 广告
- 修复方向:
  1. **Phase 0 广告池判定**:不只看 NORMAL,要算**所有未删除广告**(含 SUSPEND/DENIED)— 它们仍占唯一性槽位
  2. **fingerprint 预校验集成**:在 `enumerate_new_ad_candidates` 调用前,反查账户所有现存广告(含已删除 `is_deleted=True`),按 6 字段 + targeting 算指纹 set,enumerate 时 skip 已存在指纹
  3. **不主动 delete**:主循环只读、判定、跳过,不删除腾讯广告(开发期手动调试除外)
- 相关:整个 P0 章原本就有的 "唯一性预校验" 工作,本条把它升级为 cron 化前的硬阻塞

### P0-NEW-2. 调试期手动删广告 ≠ 生产清理机制(2026-06-10 自我警醒)
- 现状:开发期我用 `_post('/adgroups/delete', ...)` 手动清场重测,这只是调试手段
- 生产 cron 启动后**绝不能**靠开发者手动删 — 出问题应该靠 P0-NEW-1 主循环跳过
- 文档化:写进 K8s cronjob_creation.yaml 部署 README,提醒"不要手动 delete 腾讯广告"



### ~~P0-1. `DEFAULT_JUMP_PATH` 改动态生成~~ ✅ 已完成(2026-06-08)
- 实现方式:接入 piaoquantv `xcx/save` 接口
- 落地:新建 `tools/landing_plan.py` 封装接口;`creative_creation.py` 删 `DEFAULT_JUMP_PATH` 常量
- `mini_program_path` 改为来自 `xcx/save` 返回的 `pageUrl`
- 验证:两次调用 plan_id=8061 → 8062,page_url 每次新生成

### ~~P0-2. `build_creative_name` 防重名~~ ✅ 由 P0-1 副作用解决(2026-06-08)
- 原方案:客户端加 uuid 进 hash 输入
- 实际方案:`xcx/save` 返回的 `pageUrl` 内嵌 `rootSourceId`,直接用作 `dynamic_creative_name`
- 命名生成职责完全交由服务侧管,客户端不再持有命名逻辑
- 落地:删 `tools/creative_creation.py::build_creative_name` 函数

## P1 — 生产稳定性

### ~~P1-1. `upload_image_to_account` 迁移到 `tools/ad_api.py`~~ ✅ 已完成(2026-06-08)
- 落地:`tools/ad_api.py` 加 `images_add(account_id, image_url) -> str`(sync public,与 `_post/_get/_check` 同层)
- `tools/creative_creation.py` 删 `upload_image_to_account`,改 import `from tools.ad_api import images_add`
- 验证:同账户重复调用返回相同 `image_id`(MD5 幂等)
- 复用:模块 A 后续挂"广告主图"也用同一函数

### ~~P1-2. 幂等保护(防止重复创建)~~ ✅ 已完成(2026-06-08)
- 落地:`tools/creative_creation.py::find_existing_creative_by_image(account_id, adgroup_id, image_id)`
- 判等键:**同 adgroup + 同 material_image_id**(不按 creative_name,因为 name 每次 xcx/save 都新)
- 接入:`create_creative_for_ad(..., skip_if_exists=True)` 默认开启,POST 前先反查 → 命中直接返回已有 `dynamic_creative_id`
- 验证:None 路径反查 `image_id=999999999999999` 返回 None(命中路径因当前 adgroup 无创意 skip,逻辑同 None 路径反向)
- 副作用:每次创建多 1 次 `/dynamic_creatives/get` 调用(QPS 多消耗 1,在 P1-3 接限流时算入)

### ~~P1-3. 接入 `execution_engine` 的 QPS + retry~~ ⏸ 暂缓(2026-06-08 用户决策)
- 暂缓原因:单账户串行挂创意场景,实际 QPS 远低于腾讯 10/s 硬限,不必预先处理
- piaoquantv 侧已确认无限流(用户 2026-06-08 确认)
- 现状评估:链路 3 次腾讯调用(`/dynamic_creatives/get` + `/images/add` + `/dynamic_creatives/add`)+ 1 次 piaoquantv `xcx/save`,单条创意挂载 < 1s
- 触发条件:**如果**未来真撞限(腾讯返回 limit 错误码)→ 在 `ad_api.py::_post / _get` 加一层全局 sync token bucket 即可(execution_engine.TokenBucket 是 async 版,需要 sync 改造)
- 不在调用方加 — 改一处而非每个 caller 改

### P1-4. 定义领域异常
- 现状:全部 `RuntimeError`,调用方无法区分错误类型
- 方案:
  - `ImageUploadError(account_id, image_url, tencent_code)`
  - `MissingBrandError(account_id)`
  - `CreativeRejectError(adgroup_id, tencent_code, field)`

## P2 — 架构清晰度

### P2-1. 抽 `tools/account_assets_repo.py`
- 集中:`get_account_brand` + 后续 `get_account_audience_pack` + `get_account_jump_template` 等
- 现状:DB 访问散落各 tool

### P2-2. 腾讯枚举集中
- `tools/ad_api.py` 加 `class TencentAdStatus / TencentDeliveryMode / TencentCreativeType`
- 替换:`"AD_STATUS_NORMAL" / "DELIVERY_MODE_COMPONENT" / "DYNAMIC_CREATIVE_TYPE_PROGRAM"` 等 magic string

### P2-3. 完善 logging 链路
- 召回 / 上传 / build / POST / 反查 全程 `step=xxx duration_ms=yyy`

### P2-4. 模块 B 主循环 + 协作 hook
- `find_ads_needing_creatives(account_id) -> list[ad_id]`(filter `system_status=CREATIVE_EMPTY` 或 `creative_count < MIN`)
- 配合 plan 阶段 f 的 `execute_creation_once.py` 主入口

## P3 — 可演进性

### P3-1. `build_creative_request_body` 引入 `CreativeContext` dataclass
- 把 brand / jump_path / button_text 等 SOP 字段打包,减少 8 参数

### P3-2. 83846804 账户跑同样流程验证
- brand 已就绪(`40915884255`),DB 已填,直接可用

### P3-3. 加模块 B README
- 位置:`tools/README_creative_creation.md`
- 内容:数据流图 / 接口列表 / 已知腾讯错误码表 / 故障排查清单

### P3-4. 图片 Content-Type 自动检测
- 现状:硬编码 `image/jpeg`
- 方案:`mimetypes.guess_type(url)` 或读 magic bytes

### P3-6. 飞书 IM 消息升级为 interactive 卡片(2026-06-09 发现 unfurling 不可靠)
- 现状:`feishu_doc._send_link_message` 走 `msg_type=text`,完全依赖飞书客户端 link unfurling 自动展开 sheet 预览
- 实测问题:同一段代码,**飞书后端 link preview 服务行为变化**导致原本能展开的消息现在不展开(用户 2026-06-09 反馈连他原有调控审批消息也不展开了)
- 影响范围:**调控审批 + 模块 B 创意审批共用同一段代码**,改动会同时改善两个子系统
- 升级方案:`msg_type=interactive`,卡片 JSON 显式定义标题/摘要/按钮,飞书客户端**强制渲染**,不依赖 unfurling
- 终极方案:用飞书的 share_sheet / sheet embed 消息类型(待查文档),IM 客户端 100% 强制展开 sheet 预览
- 工程量:50-100 行代码,改 `_send_link_message` 一处

### P3-5. 飞书审批表素材预览 — 升级嵌入图(write-images API)
- 现状:cell 用 `=HYPERLINK(material.cover, "查看素材")` 公式,运营点击在浏览器看图
- 已知问题:`rescdn.yishihui.com` 有 Referer ACL 防盗链,飞书 sheet 点链接首次访问会 403,运营需要"右键新标签"或粘贴 URL 重试
- 升级方案:调飞书 `POST /open-apis/sheets/v2/spreadsheets/{token}/values_image`,把素材图二进制流嵌入 cell,运营 review 时直接看图
- 风险点:飞书 server 拉图是否被 yishihui CDN 拦截待验证;如被拦,需跟 yishihui 运维协调 CDN 白名单(加 feishu.cn 或放行空 Referer)
- 触发条件:挂创意量上百条/天时再升级 — 当前 ≤ 5 条/天,运营 5 次点击代价 < 工程协调成本

### P3-7. AI 生成素材画面主体多样性优化
- 现象:近期 AI 生成素材里背景经常是中老年人物,且几乎每张都有人物主体。
- 初步判断:当前 prompt 多次强调目标用户为中老年、生活场景、家庭/邻里/父母子女关系,图片模型容易把"目标用户"误理解为"画面必须出现老年人"。
- 优化方向:
  - 区分"目标用户"和"画面主体":目标用户是 45-75 岁用户,但画面主体可以是物品、清单、场景、事件现场、道具特写、讲解场面、自然/工业过程等。
  - 不强制人物:只有 pattern 或视频主题确实需要人物情绪/关系时才出现人物。
  - 不强制老人形象:即使有人物,也可以是中年人、家庭成员、工作人员、邻里、讲解者或背影/手部等,不必总是正脸老人。
  - 后续评估 prompt 是否过度绑定"真实生活化/家庭场景",避免所有素材都长成同一类家庭老人封面。

## 已知腾讯错误码 — 排查表

| code | 含义 | 修复方向 |
|---|---|---|
| 18001 | 缺失必填参数 `image_id` | image 组件不能用 `image_url`,必须先上传拿 `image_id` |
| 1800269 | 品牌形象必填 | 必须传 `brand` 组件 + `brand_name + brand_image_id` |
| 1530003 | 图片不存在或已删除 | `image_id` 跨账户不可复用,必须按账户独立上传 |
| 1801159 | 提交的组件无法匹配到创意形式(图片素材比例/长宽/大小/时长不符) | 召回素材不一定符合腾讯创意规格,需 try-fallback;长期方案:召回阶段加图片尺寸校验,或限定 URL 模式仅取 `adPutTencent/image/` 路径 |

## 已验证过的非业务事实

- 同账户图片 MD5 幂等:重复上传同图返回相同 `image_id`(无需本地缓存)
- 跨账户图片不可复用:同图在不同账户下 `image_id` 不同
- 普通素材审核:`DYNAMIC_CREATIVE_STATUS_PENDING` → 2-4 小时
- 广告 `system_status` 由 `CREATIVE_EMPTY` 切走有几秒到几分钟延迟(腾讯后端聚合状态)
