|
@@ -15,6 +15,7 @@ IM 审批流 — auto_put_ad_mini(飞书直连版)
|
|
|
import asyncio
|
|
import asyncio
|
|
|
import json
|
|
import json
|
|
|
import logging
|
|
import logging
|
|
|
|
|
+import re
|
|
|
import sys
|
|
import sys
|
|
|
import time
|
|
import time
|
|
|
import uuid
|
|
import uuid
|
|
@@ -110,12 +111,14 @@ _feishu = FeishuClient(app_id=FEISHU_APP_ID, app_secret=FEISHU_APP_SECRET)
|
|
|
# 审批表精选列(运营审阅所需的关键指标)
|
|
# 审批表精选列(运营审阅所需的关键指标)
|
|
|
# 列顺序:日期 → 账户ID → 广告ID → 广告消耗 → 决策动作 → 其他关键信息(简洁版)
|
|
# 列顺序:日期 → 账户ID → 广告ID → 广告消耗 → 决策动作 → 其他关键信息(简洁版)
|
|
|
APPROVAL_COLUMNS = [
|
|
APPROVAL_COLUMNS = [
|
|
|
- # 核心标识(前5列,含决策动作)
|
|
|
|
|
- "approval_date", "account_id", "ad_id", "cost_7d_avg", "action",
|
|
|
|
|
|
|
+ # 核心标识(前6列:账户后接代理商)
|
|
|
|
|
+ "approval_date", "account_id", "agent_name", "ad_id", "cost_7d_avg", "action",
|
|
|
# 基础信息
|
|
# 基础信息
|
|
|
"ad_name", "audience_tier", "ad_age_days", "bid_amount",
|
|
"ad_name", "audience_tier", "ad_age_days", "bid_amount",
|
|
|
# 关键指标(使用实际列名)
|
|
# 关键指标(使用实际列名)
|
|
|
"动态ROI_7日均值", "cost_7d_total", "revenue_7d_total",
|
|
"动态ROI_7日均值", "cost_7d_total", "revenue_7d_total",
|
|
|
|
|
+ # 当日裂变指标(取有数据的最近一天)
|
|
|
|
|
+ "open_count_latest", "fission_count_latest", "T0裂变系数_latest",
|
|
|
# 决策详情
|
|
# 决策详情
|
|
|
"dimension", "reason",
|
|
"dimension", "reason",
|
|
|
"recommended_change_pct",
|
|
"recommended_change_pct",
|
|
@@ -409,6 +412,93 @@ def _parse_approval_reply(content: str, all_ad_ids: List[int]) -> Dict:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ═══════════════════════════════════════════
|
|
|
|
|
+# 审批关键词 + 锚点法语义解析
|
|
|
|
|
+# ═══════════════════════════════════════════
|
|
|
|
|
+# 子串匹配(非正则): 移除"ok"避免英文随口语误判("ok吗"/"你说的ok")
|
|
|
|
|
+APPROVE_KEYWORDS = ["同意", "批准", "通过", "执行", "确认"]
|
|
|
|
|
+REJECT_KEYWORDS = ["拒绝", "驳回", "不同意", "不批准"]
|
|
|
|
|
+# 匹配优先级: 先匹配长串(不同意/不批准/驳回)再匹配单字"通过/拒绝",避免"不同意"被截成"同意"
|
|
|
|
|
+_ACTION_PATTERN = re.compile(r"(不同意|不批准|同意|批准|通过|执行|确认|驳回|拒绝)")
|
|
|
|
|
+_ID_PATTERN = re.compile(r"\b(\d{11})\b") # 腾讯广告 ID 11 位
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _parse_anchored_decision(text: str, tier2_ad_ids: List[int]) -> Optional[Dict]:
|
|
|
|
|
+ """锚点法解析审批回复。
|
|
|
|
|
+
|
|
|
|
|
+ 规则(用户确认):
|
|
|
|
|
+ - 显式批准的 ID 才批准, 其他默认拒绝
|
|
|
|
|
+ - 仅"通过"(无 ID) → 全部批准
|
|
|
|
|
+ - 仅"拒绝"(无 ID) → 全部拒绝
|
|
|
|
|
+ - "通过 + 拒绝 X" → 除 X 外全部批准
|
|
|
|
|
+ - "通过 A, 拒绝 B" → A 批准, B/其他默认拒绝
|
|
|
|
|
+ - "拒绝 A" → 全部拒绝(无 approve 信号)
|
|
|
|
|
+
|
|
|
|
|
+ 返回 None 表示无法解析(交回 Agent 处理),否则返回包含 status/approved_ids/rejected_ids 的字典。
|
|
|
|
|
+ """
|
|
|
|
|
+ matches = list(_ACTION_PATTERN.finditer(text))
|
|
|
|
|
+ if not matches:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ approve_ids: set = set()
|
|
|
|
|
+ reject_ids: set = set()
|
|
|
|
|
+ approve_global = False
|
|
|
|
|
+ reject_global = False
|
|
|
|
|
+ reject_verbs = {"不同意", "不批准", "拒绝", "驳回"}
|
|
|
|
|
+
|
|
|
|
|
+ for i, m in enumerate(matches):
|
|
|
|
|
+ verb = m.group(1)
|
|
|
|
|
+ is_reject = verb in reject_verbs
|
|
|
|
|
+ scope_start = m.end()
|
|
|
|
|
+ scope_end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
|
|
|
+ scope = text[scope_start:scope_end]
|
|
|
|
|
+ scope_ids = []
|
|
|
|
|
+ for id_m in _ID_PATTERN.finditer(scope):
|
|
|
|
|
+ ad_id = int(id_m.group(1))
|
|
|
|
|
+ if ad_id in tier2_ad_ids:
|
|
|
|
|
+ scope_ids.append(ad_id)
|
|
|
|
|
+ if scope_ids:
|
|
|
|
|
+ if is_reject:
|
|
|
|
|
+ reject_ids.update(scope_ids)
|
|
|
|
|
+ else:
|
|
|
|
|
+ approve_ids.update(scope_ids)
|
|
|
|
|
+ else:
|
|
|
|
|
+ if is_reject:
|
|
|
|
|
+ reject_global = True
|
|
|
|
|
+ else:
|
|
|
|
|
+ approve_global = True
|
|
|
|
|
+
|
|
|
|
|
+ all_set = set(tier2_ad_ids)
|
|
|
|
|
+
|
|
|
|
|
+ # case 1: 仅全局 approve(无任何 reject 信号、无具体 ID 锚定)
|
|
|
|
|
+ if approve_global and not approve_ids and not reject_ids and not reject_global:
|
|
|
|
|
+ return {"status": "approved", "approved_ids": list(tier2_ad_ids), "rejected_ids": []}
|
|
|
|
|
+
|
|
|
|
|
+ # case 2: 仅全局 reject
|
|
|
|
|
+ if reject_global and not reject_ids and not approve_ids and not approve_global:
|
|
|
|
|
+ return {"status": "rejected", "approved_ids": [], "rejected_ids": list(tier2_ad_ids)}
|
|
|
|
|
+
|
|
|
|
|
+ # case 3: 全局 approve + 显式 reject(如"通过, 拒绝 X")
|
|
|
|
|
+ if approve_global and reject_ids and not approve_ids:
|
|
|
|
|
+ approved = sorted(all_set - reject_ids)
|
|
|
|
|
+ rejected = sorted(reject_ids & all_set)
|
|
|
|
|
+ return {"status": "partial_approved", "approved_ids": approved, "rejected_ids": rejected}
|
|
|
|
|
+
|
|
|
|
|
+ # case 4: 显式 approve_ids → 仅这些批准, 其他默认拒绝(用户规则)
|
|
|
|
|
+ if approve_ids:
|
|
|
|
|
+ approved = sorted(approve_ids & all_set)
|
|
|
|
|
+ rejected = sorted(all_set - approve_ids)
|
|
|
|
|
+ if rejected:
|
|
|
|
|
+ return {"status": "partial_approved", "approved_ids": approved, "rejected_ids": rejected}
|
|
|
|
|
+ return {"status": "approved", "approved_ids": approved, "rejected_ids": []}
|
|
|
|
|
+
|
|
|
|
|
+ # case 5: 仅显式 reject_ids, 无 approve 信号 → 全部默认拒绝
|
|
|
|
|
+ if reject_ids:
|
|
|
|
|
+ return {"status": "rejected", "approved_ids": [], "rejected_ids": list(tier2_ad_ids)}
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# ═══════════════════════════════════════════
|
|
# ═══════════════════════════════════════════
|
|
|
# 工具:发送审批请求(飞书版)
|
|
# 工具:发送审批请求(飞书版)
|
|
|
# ═══════════════════════════════════════════
|
|
# ═══════════════════════════════════════════
|
|
@@ -837,42 +927,14 @@ async def send_approval_request(
|
|
|
})
|
|
})
|
|
|
logger.info("HTTP 轮询收到运营回复: %s", text[:200])
|
|
logger.info("HTTP 轮询收到运营回复: %s", text[:200])
|
|
|
|
|
|
|
|
- # 解析审批结果
|
|
|
|
|
- text_lower = text.lower()
|
|
|
|
|
- is_approved = any(kw in text_lower for kw in ["同意", "批准", "通过", "执行", "ok", "确认"])
|
|
|
|
|
- is_rejected = any(kw in text_lower for kw in ["拒绝", "驳回", "不同意", "不批准"])
|
|
|
|
|
-
|
|
|
|
|
- # 解析具体批准/拒绝的广告ID(如果有)
|
|
|
|
|
- import re
|
|
|
|
|
- mentioned_ids = []
|
|
|
|
|
- for match in re.finditer(r'\b(\d{11})\b', text): # 腾讯广告ID通常11位
|
|
|
|
|
- ad_id = int(match.group(1))
|
|
|
|
|
- if ad_id in tier2_ad_ids:
|
|
|
|
|
- mentioned_ids.append(ad_id)
|
|
|
|
|
-
|
|
|
|
|
- # 确定最终状态和ID列表
|
|
|
|
|
- if is_approved and not is_rejected:
|
|
|
|
|
- # 全部批准或部分批准
|
|
|
|
|
- status = "approved"
|
|
|
|
|
- approved_ids = mentioned_ids if mentioned_ids else tier2_ad_ids
|
|
|
|
|
- rejected_ids = []
|
|
|
|
|
- elif is_rejected and not is_approved:
|
|
|
|
|
- # 全部拒绝
|
|
|
|
|
- status = "rejected"
|
|
|
|
|
- approved_ids = []
|
|
|
|
|
- rejected_ids = tier2_ad_ids
|
|
|
|
|
- elif mentioned_ids:
|
|
|
|
|
- # 提到具体ID:只批准/拒绝这些ID
|
|
|
|
|
- if is_approved:
|
|
|
|
|
- status = "partial_approved"
|
|
|
|
|
- approved_ids = mentioned_ids
|
|
|
|
|
- rejected_ids = [x for x in tier2_ad_ids if x not in mentioned_ids]
|
|
|
|
|
- else:
|
|
|
|
|
- status = "partial_rejected"
|
|
|
|
|
- approved_ids = [x for x in tier2_ad_ids if x not in mentioned_ids]
|
|
|
|
|
- rejected_ids = mentioned_ids
|
|
|
|
|
|
|
+ # 锚点法解析(显式批准的才批准,其他默认拒绝)
|
|
|
|
|
+ anchored = _parse_anchored_decision(text, tier2_ad_ids)
|
|
|
|
|
+ if anchored is not None:
|
|
|
|
|
+ status = anchored["status"]
|
|
|
|
|
+ approved_ids = anchored["approved_ids"]
|
|
|
|
|
+ rejected_ids = anchored["rejected_ids"]
|
|
|
else:
|
|
else:
|
|
|
- # 无法判断,需要Agent处理
|
|
|
|
|
|
|
+ # 无法解析,交回 Agent 处理
|
|
|
status = "unclear"
|
|
status = "unclear"
|
|
|
approved_ids = []
|
|
approved_ids = []
|
|
|
rejected_ids = []
|
|
rejected_ids = []
|