Просмотр исходного кода

fix(auto_put_ad_mini): 修复飞书审批轮询+强化执行顺序

1. im_approval: 从 send_message 返回值提取 P2P chat_id 用于轮询,
   修复 invalid container_id (230001) 错误,实现单聊来回交互
2. system.prompt: 强化 10 步执行顺序依赖链,明确 fetch→merge→roi
   不可跳步,防止 Agent 乱序调用导致数据缺失
3. config: 启用投放项目群聊 chat_id

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
刘立冬 3 недель назад
Родитель
Сommit
368cfcc255

+ 2 - 2
examples/auto_put_ad_mini/config.py

@@ -143,8 +143,8 @@ FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "NQaG4ci1plXRDTgwCqrLJgMLLoA2
 FEISHU_OPERATOR_OPEN_ID = os.getenv("FEISHU_OPERATOR_OPEN_ID", "ou_498988d823b61ab89c9afe4310f85bb4")
 FEISHU_OPERATOR_CHAT_ID = os.getenv("FEISHU_OPERATOR_CHAT_ID", "oc_88e0a1970a7de02eb5ac225a8b0cedea")
 
-# 投放项目群聊(新增)— 用于接收决策结果通知
-FEISHU_AD_PROJECT_CHAT_ID = os.getenv("FEISHU_AD_PROJECT_CHAT_ID", "")  # 设置为空表示不发送
+# 投放项目群聊 — 用于接收决策结果通知和审批回复
+FEISHU_AD_PROJECT_CHAT_ID = os.getenv("FEISHU_AD_PROJECT_CHAT_ID", "oc_7940ec97cde40b245cff9cb606ff1ac7")
 
 # 腾讯广告默认账户(测试账户)
 TENCENT_AD_ACCOUNT_ID = int(os.getenv("TENCENT_AD_ACCOUNT_ID", "80769799"))

+ 37 - 13
examples/auto_put_ad_mini/prompts/system.prompt

@@ -73,16 +73,28 @@ $system$
 ## 可用工具列表
 
 ### 数据获取类
+- **fetch_creative_data(days, end_date)**
+  - 功能:从ODPS拉取创意级原始数据 + 广告状态快照
+  - 输出:outputs/raw/ 和 outputs/ad_status/ 下的CSV
+  - ⚠️ **必须在 calculate_roi_metrics 之前调用**
+
+- **merge_creative_data(days, force)**
+  - 功能:合并创意数据与广告状态
+  - 输出:outputs/merged/ 下的CSV
+  - 依赖:需要先执行 fetch_creative_data
+  - ⚠️ **必须在 calculate_roi_metrics 之前调用**
+
 - **calculate_roi_metrics(end_date)**
   - 功能:计算f_7日动态ROI、汇总指标
   - 输出:metrics CSV路径
-  - 依赖:需要原始数据(自动调用fetch_creative_data)
+  - 依赖:需要 outputs/merged/ 目录下有最新数据(不会自动拉取!)
+  - ⚠️ **必须先完成 fetch + merge,否则会因数据缺失得到错误结果**
 
 - **get_ads_for_review(metrics_csv)**
   - 功能:分类广告(零消耗/待评估/正常)
   - 输入:metrics CSV路径
   - 输出:分类结果 + thresholds_used
-  - 依赖:需要先执行calculate_roi_metrics
+  - 依赖:需要先执行 calculate_roi_metrics
 
 - **query_ad_detail(ad_id, metrics_csv)**
   - 功能:查询单个广告详情
@@ -119,31 +131,43 @@ $system$
 
 ## 工具编排原则
 
-### 依赖关系(必须遵守)
+### 依赖关系(必须严格遵守执行顺序)
+
+⚠️ **全量分析时,必须按以下顺序执行,不可跳步或调换顺序:**
+
 ```
-calculate_roi_metrics
+Step 1: fetch_creative_data       ← 从ODPS拉取原始数据(必须第一步!)
-get_ads_for_review / query_ad_detail
+Step 2: merge_creative_data       ← 合并创意数据+广告状态
-AI推理决策
+Step 3: calculate_roi_metrics     ← 计算ROI(依赖Step 1+2的数据)
-apply_decisions / modify_decisions
+Step 4: get_ads_for_review        ← ABC三级分类
-validate_decisions
+Step 5: AI推理决策                 ← 对B类广告推理
-send_approval_request
+Step 6: apply_decisions           ← 保存决策
-execute_decisions
+Step 7: validate_decisions        ← 护栏验证
-generate_report
+Step 8: send_approval_request     ← 飞书审批
+    ↓
+Step 9: execute_decisions         ← 执行(审批通过后)
+    ↓
+Step 10: generate_report          ← 生成报告
 ```
 
+**⚠️ 关键约束**:
+- `calculate_roi_metrics` 不会自动拉取数据,它只读取 `outputs/merged/` 目录下已有的文件
+- 如果先调用 `calculate_roi_metrics` 而不先 `fetch + merge`,会因缺少最新数据而得到错误结果
+- **正确做法**:先 `fetch_creative_data` → 再 `merge_creative_data` → 最后 `calculate_roi_metrics`
+
 ### 灵活性原则
 
 **根据用户意图灵活选择工具组合,不死板按固定流程**:
 
-- 用户问"最近效果怎么样?"
-  → calculate_roi_metrics → get_ads_for_review → AI推理 → apply_decisions → validate → 审批 → 执行 → 报告
+- 用户问"最近效果怎么样?" / "分析广告" / "执行完整流程"
+  → fetch_creative_data → merge_creative_data → calculate_roi_metrics → get_ads_for_review → AI推理 → apply_decisions → validate → 审批 → 执行 → 报告
 
 - 用户问"为什么广告XXX被暂停?"
   → 直接读取已有决策文件 → 查找原因 → 解释

+ 33 - 25
examples/auto_put_ad_mini/tools/im_approval.py

@@ -486,24 +486,33 @@ async def send_approval_request(
         feishu_sent = False
         feishu_sent_to_project_chat = False
         sent_time_sec = str(int(time.time()))  # 飞书 API start_time 单位:秒
+        poll_chat_ids = []  # 用于轮询的真正 chat_id(从 send_message 返回值中提取)
         try:
             # 消息 1a:发送到个人(FEISHU_OPERATOR_OPEN_ID)
             if FEISHU_OPERATOR_OPEN_ID:
                 try:
                     result_personal = _feishu.send_message(to=FEISHU_OPERATOR_OPEN_ID, text=message)
                     logger.info("飞书审批消息发送成功(个人): message_id=%s", result_personal.message_id)
+                    # ✅ 关键修复:从返回值提取 P2P chat_id(用于后续轮询)
+                    if hasattr(result_personal, 'chat_id') and result_personal.chat_id:
+                        poll_chat_ids.append(result_personal.chat_id)
+                        logger.info("提取到 P2P chat_id: %s(用于轮询回复)", result_personal.chat_id)
+                    feishu_sent = True
                 except Exception as e:
                     logger.warning("发送到个人失败: %s", e)
 
-            # 消息 1b:发送到投放项目群聊(如果配置了)— 临时禁用
-            # if FEISHU_AD_PROJECT_CHAT_ID:
-            #     try:
-            #         result_project = _feishu.send_message(to=FEISHU_AD_PROJECT_CHAT_ID, text=message)
-            #         feishu_sent_to_project_chat = True
-            #         feishu_sent = True
-            #         logger.info("飞书审批消息发送成功(项目群): message_id=%s", result_project.message_id)
-            #     except Exception as e:
-            #         logger.warning("发送到项目群聊失败: %s", e)
+            # 消息 1b:发送到投放项目群聊(如果配置了)
+            if FEISHU_AD_PROJECT_CHAT_ID:
+                try:
+                    result_project = _feishu.send_message(to=FEISHU_AD_PROJECT_CHAT_ID, text=message)
+                    feishu_sent_to_project_chat = True
+                    feishu_sent = True
+                    logger.info("飞书审批消息发送成功(项目群): message_id=%s", result_project.message_id)
+                    # 群聊 chat_id 本身就是 oc_xxx 格式,可直接用于轮询
+                    if FEISHU_AD_PROJECT_CHAT_ID not in poll_chat_ids:
+                        poll_chat_ids.append(FEISHU_AD_PROJECT_CHAT_ID)
+                except Exception as e:
+                    logger.warning("发送到项目群聊失败: %s", e)
 
             # 消息 2:导入为飞书在线表格(决策详情,含hold参考)
             try:
@@ -561,6 +570,15 @@ async def send_approval_request(
         except Exception as e:
             logger.warning("飞书发消息失败: %s", e)
 
+        # ✅ 兜底:如果 send_message 未返回 P2P chat_id,用 config 中的 FEISHU_OPERATOR_CHAT_ID
+        if not poll_chat_ids and FEISHU_OPERATOR_CHAT_ID:
+            poll_chat_ids.append(FEISHU_OPERATOR_CHAT_ID)
+            logger.info("使用配置中的 FEISHU_OPERATOR_CHAT_ID 兜底: %s", FEISHU_OPERATOR_CHAT_ID)
+
+        # 将 poll_chat_ids 存入请求状态(供 check_approval_status 使用)
+        _approval_requests[request_id]["poll_chat_ids"] = poll_chat_ids
+        logger.info("轮询目标 chat_ids: %s", poll_chat_ids)
+
         # 保存审批消息到文件(备份)
         approval_dir = _MINI_DIR / "outputs" / "approvals"
         approval_dir.mkdir(parents=True, exist_ok=True)
@@ -609,15 +627,8 @@ async def send_approval_request(
 
             # 读取个人和项目群的审批回复
             try:
-                # ✅ 修改:监听个人私聊和项目群聊的消息 — 临时只监听个人
-                chat_ids_to_check = []
-                if FEISHU_OPERATOR_OPEN_ID:
-                    chat_ids_to_check.append(FEISHU_OPERATOR_OPEN_ID)
-                # 临时禁用项目群聊监听
-                # if FEISHU_AD_PROJECT_CHAT_ID:
-                #     chat_ids_to_check.append(FEISHU_AD_PROJECT_CHAT_ID)
-
-                for chat_id in chat_ids_to_check:
+                # ✅ 修复:使用 send_message 返回的真实 chat_id 轮询(非 open_id)
+                for chat_id in poll_chat_ids:
                     result = _feishu.get_message_list(
                         chat_id=chat_id,
                         start_time=sent_time_sec,
@@ -755,13 +766,10 @@ async def check_approval_status(
                 datetime.fromisoformat(request["created_at"]).timestamp()
             ))
 
-            # ✅ 修改:监听个人私聊和项目群聊的消息 — 临时只监听个人
-            chat_ids_to_check = []
-            if FEISHU_OPERATOR_OPEN_ID:
-                chat_ids_to_check.append(FEISHU_OPERATOR_OPEN_ID)
-            # 临时禁用项目群聊监听
-            # if FEISHU_AD_PROJECT_CHAT_ID:
-            #     chat_ids_to_check.append(FEISHU_AD_PROJECT_CHAT_ID)
+            # ✅ 修复:使用请求中存储的真实 chat_id(从 send_message 返回值提取)
+            chat_ids_to_check = request.get("poll_chat_ids", [])
+            if not chat_ids_to_check:
+                logger.warning("请求 %s 没有 poll_chat_ids,可能是旧版请求", request_id)
 
             for chat_id in chat_ids_to_check:
                 result = _feishu.get_message_list(