# 模块 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` 切走有几秒到几分钟延迟(腾讯后端聚合状态)