|
@@ -10,6 +10,7 @@
|
|
|
import os
|
|
import os
|
|
|
import logging
|
|
import logging
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
|
|
+from typing import Optional
|
|
|
from agent.core.runner import RunConfig, KnowledgeConfig
|
|
from agent.core.runner import RunConfig, KnowledgeConfig
|
|
|
|
|
|
|
|
# 初始化 logger(必须在使用前定义)
|
|
# 初始化 logger(必须在使用前定义)
|
|
@@ -276,16 +277,171 @@ if EXPERIMENTAL_SCOPE_ACCOUNTS is not None:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════
|
|
# ═══════════════════════════════════════════
|
|
|
-# 一账一包映射(账户级 audience pack · 2026-06-05 业务确认)
|
|
|
|
|
|
|
+# 待投放账户配置(DB 单一真相源)
|
|
|
# ═══════════════════════════════════════════
|
|
# ═══════════════════════════════════════════
|
|
|
-# 用户明确约束:一个账户只用一个人群包,即使有多个广告也不变
|
|
|
|
|
-# audience_pack_id 由运营提供;LLM 不参与选择
|
|
|
|
|
-# 字段格式:account_id -> (audience_pack_id, audience_tier_label)
|
|
|
|
|
-# - audience_pack_id 为 None 表示该账户不使用人群包(仅靠地域/年龄/性别自然定向)
|
|
|
|
|
-ACCOUNT_AUDIENCE_PACK_MAPPING = {
|
|
|
|
|
- 83846793: (None, "no_audience_pack"), # 不传 custom_audience,按"泛人群"出价
|
|
|
|
|
- 83846804: (44722088, "R330+"), # Q-R_330+_WX_UNIONID_2026-06-02 (865 万人)
|
|
|
|
|
-}
|
|
|
|
|
|
|
+# 人工只配置:account_id / audience_name / bid_min_fen / bid_max_fen / age_min / age_max / delivery_version。
|
|
|
|
|
+# 系统自动解析 audience_pack_id、授权并验证目标账户可见后再创建广告。
|
|
|
|
|
+# - audience_name="泛人群" 表示不使用 custom_audience,无需授权。
|
|
|
|
|
+# - bid_amount_fen 若为空,按 bid_min_fen/bid_max_fen 和 BID_PICK_STRATEGY 取值。
|
|
|
|
|
+def get_account_creation_config(account_id: int) -> dict:
|
|
|
|
|
+ """从 DB 读取账户级广告创建配置。
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ {
|
|
|
|
|
+ account_id, audience_name, audience_pack_id,
|
|
|
|
|
+ audience_tier_label, bid_amount_fen,
|
|
|
|
|
+ bid_min_fen, bid_max_fen, age, delivery_version, ...
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Raises:
|
|
|
|
|
+ ValueError: 账户未配置、未启用,或缺少必要人工配置。
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ from db.connection import get_connection
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"无法读取 account_id {account_id} 的创建配置: DB 模块不可用({e})"
|
|
|
|
|
+ ) from e
|
|
|
|
|
+
|
|
|
|
|
+ conn = get_connection()
|
|
|
|
|
+ try:
|
|
|
|
|
+ with conn.cursor() as cur:
|
|
|
|
|
+ cur.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT c.account_id, c.enabled, c.delivery_version,
|
|
|
|
|
+ c.audience_name, c.audience_pack_id,
|
|
|
|
|
+ c.audience_tier_label, c.bid_min_fen, c.bid_max_fen,
|
|
|
|
|
+ c.bid_amount_fen, c.age_min, c.age_max,
|
|
|
|
|
+ c.daily_budget_fen AS account_daily_budget_fen,
|
|
|
|
|
+ t.site_set_json, t.location_types_json, t.region_ids_json,
|
|
|
|
|
+ t.daily_budget_fen, t.time_series_json,
|
|
|
|
|
+ w.enabled AS whitelist_enabled
|
|
|
|
|
+ FROM ad_creation_account_config c
|
|
|
|
|
+ JOIN account_whitelist w ON w.account_id = c.account_id
|
|
|
|
|
+ JOIN ad_delivery_template t
|
|
|
|
|
+ ON t.delivery_version = c.delivery_version
|
|
|
|
|
+ AND t.enabled = TRUE
|
|
|
|
|
+ WHERE c.account_id=%s
|
|
|
|
|
+ """,
|
|
|
|
|
+ (account_id,),
|
|
|
|
|
+ )
|
|
|
|
|
+ row = cur.fetchone()
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+
|
|
|
|
|
+ if not row:
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"account_id {account_id} 未写入 ad_creation_account_config,不能创建广告"
|
|
|
|
|
+ )
|
|
|
|
|
+ if not row.get("whitelist_enabled"):
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"account_id {account_id} 未在 account_whitelist 启用,不能创建广告"
|
|
|
|
|
+ )
|
|
|
|
|
+ if not row.get("enabled"):
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"account_id {account_id} 在 ad_creation_account_config 中未启用,不能创建广告"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ audience_name = (row.get("audience_name") or "").strip()
|
|
|
|
|
+ if not audience_name:
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"account_id {account_id} 缺少 audience_name。"
|
|
|
|
|
+ "请先人工配置待投放人群包名称;泛人群账户写 audience_name='泛人群'"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ bid_amount_fen = row.get("bid_amount_fen")
|
|
|
|
|
+ bid_min_fen = row.get("bid_min_fen")
|
|
|
|
|
+ bid_max_fen = row.get("bid_max_fen")
|
|
|
|
|
+ if bid_amount_fen is None:
|
|
|
|
|
+ if bid_min_fen is None or bid_max_fen is None:
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"account_id {account_id} 缺少 bid_amount_fen 或 bid_min_fen/bid_max_fen"
|
|
|
|
|
+ )
|
|
|
|
|
+ if int(bid_min_fen) > int(bid_max_fen):
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"account_id {account_id} 出价范围非法:"
|
|
|
|
|
+ f" bid_min_fen={bid_min_fen} > bid_max_fen={bid_max_fen}"
|
|
|
|
|
+ )
|
|
|
|
|
+ if BID_PICK_STRATEGY == "max":
|
|
|
|
|
+ bid_amount_fen = int(bid_max_fen)
|
|
|
|
|
+ elif BID_PICK_STRATEGY == "min":
|
|
|
|
|
+ bid_amount_fen = int(bid_min_fen)
|
|
|
|
|
+ else:
|
|
|
|
|
+ bid_amount_fen = int(round((int(bid_min_fen) + int(bid_max_fen)) / 2))
|
|
|
|
|
+
|
|
|
|
|
+ pack_id = row.get("audience_pack_id")
|
|
|
|
|
+ age_min = row.get("age_min")
|
|
|
|
|
+ age_max = row.get("age_max")
|
|
|
|
|
+ if age_min is None or age_max is None:
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"account_id {account_id} 缺少 age_min/age_max"
|
|
|
|
|
+ )
|
|
|
|
|
+ if int(age_min) > int(age_max):
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"account_id {account_id} 年龄范围非法: age_min={age_min} > age_max={age_max}"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ import json as _json
|
|
|
|
|
+ site_set = _json.loads(row["site_set_json"])
|
|
|
|
|
+ location_types = _json.loads(row["location_types_json"])
|
|
|
|
|
+ region_ids = _json.loads(row["region_ids_json"])
|
|
|
|
|
+ time_series = _json.loads(row["time_series_json"])
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "account_id": int(row["account_id"]),
|
|
|
|
|
+ "delivery_version": row["delivery_version"],
|
|
|
|
|
+ "audience_name": audience_name,
|
|
|
|
|
+ "audience_pack_id": int(pack_id) if pack_id is not None else None,
|
|
|
|
|
+ "audience_tier_label": row.get("audience_tier_label") or audience_name,
|
|
|
|
|
+ "bid_min_fen": int(bid_min_fen) if bid_min_fen is not None else None,
|
|
|
|
|
+ "bid_max_fen": int(bid_max_fen) if bid_max_fen is not None else None,
|
|
|
|
|
+ "bid_amount_fen": int(bid_amount_fen),
|
|
|
|
|
+ "age": [{"min": int(age_min), "max": int(age_max)}],
|
|
|
|
|
+ "site_set": site_set,
|
|
|
|
|
+ "location_types": location_types,
|
|
|
|
|
+ "region_ids": region_ids,
|
|
|
|
|
+ "daily_budget_fen": int(row.get("account_daily_budget_fen") or row["daily_budget_fen"]),
|
|
|
|
|
+ "time_series": time_series,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def get_account_audience_pack(account_id: int) -> tuple[Optional[int], str]:
|
|
|
|
|
+ """兼容旧调用:返回 (audience_pack_id, audience_tier_label)。"""
|
|
|
|
|
+ cfg = get_account_creation_config(account_id)
|
|
|
|
|
+ return cfg["audience_pack_id"], cfg["audience_tier_label"]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def get_creation_account_ids() -> list[int]:
|
|
|
|
|
+ """读取当前启用的待投放账户,作为 Phase 0 创建广告的账户范围。"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ from db.connection import get_connection
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning("读取待投放账户失败:DB 模块不可用(%s)", e)
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
|
|
+ conn = get_connection()
|
|
|
|
|
+ try:
|
|
|
|
|
+ with conn.cursor() as cur:
|
|
|
|
|
+ cur.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT c.account_id
|
|
|
|
|
+ FROM ad_creation_account_config c
|
|
|
|
|
+ JOIN account_whitelist w ON w.account_id = c.account_id
|
|
|
|
|
+ JOIN ad_delivery_template t
|
|
|
|
|
+ ON t.delivery_version = c.delivery_version
|
|
|
|
|
+ AND t.enabled = TRUE
|
|
|
|
|
+ WHERE c.enabled = TRUE
|
|
|
|
|
+ AND w.enabled = TRUE
|
|
|
|
|
+ ORDER BY c.id ASC
|
|
|
|
|
+ """
|
|
|
|
|
+ )
|
|
|
|
|
+ rows = cur.fetchall()
|
|
|
|
|
+ return [int(r["account_id"]) for r in rows]
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning("读取待投放账户失败:%s", e)
|
|
|
|
|
+ return []
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
# ═══════════════════════════════════════════════════════════════════
|
|
|
# [CREATION SOP] 广告搭建 SOP 固定参数(2026-06-05 业务确认)
|
|
# [CREATION SOP] 广告搭建 SOP 固定参数(2026-06-05 业务确认)
|
|
@@ -330,12 +486,227 @@ ACCOUNT_FEEDBACK_ID_MAPPING = {
|
|
|
def get_account_feedback_id(account_id: int):
|
|
def get_account_feedback_id(account_id: int):
|
|
|
"""获取账户的 feedback_id(监测链接 ID)。
|
|
"""获取账户的 feedback_id(监测链接 ID)。
|
|
|
|
|
|
|
|
- 优先级:DB(将来) > config.py 字典 > None
|
|
|
|
|
|
|
+ 优先级:
|
|
|
|
|
+ 1. DB account_whitelist.feedback_id
|
|
|
|
|
+ 2. ad_creation_account_config.feedback_key → feedback_asset_template 自动查找/创建
|
|
|
|
|
+ 3. config.py 旧字典
|
|
|
|
|
+
|
|
|
返回 None 时调用方应反问或报错,不能猜测。
|
|
返回 None 时调用方应反问或报错,不能猜测。
|
|
|
"""
|
|
"""
|
|
|
- # TODO: 后续接入 DB,从 account_whitelist 表读
|
|
|
|
|
|
|
+ db_feedback_id = _get_account_feedback_id_from_db(account_id)
|
|
|
|
|
+ if db_feedback_id is not None:
|
|
|
|
|
+ return db_feedback_id
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ created_id = _ensure_account_feedback_id(account_id)
|
|
|
|
|
+ if created_id is not None:
|
|
|
|
|
+ return created_id
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "自动获取/创建 account_id %s 的 feedback_id 失败, fallback config 字典:%s",
|
|
|
|
|
+ account_id, e,
|
|
|
|
|
+ )
|
|
|
return ACCOUNT_FEEDBACK_ID_MAPPING.get(account_id)
|
|
return ACCOUNT_FEEDBACK_ID_MAPPING.get(account_id)
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+def _get_account_feedback_id_from_db(account_id: int) -> Optional[int]:
|
|
|
|
|
+ """从 account_whitelist 读取账户级 feedback_id。"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ from db.connection import get_connection
|
|
|
|
|
+ conn = get_connection()
|
|
|
|
|
+ try:
|
|
|
|
|
+ with conn.cursor() as cur:
|
|
|
|
|
+ cur.execute(
|
|
|
|
|
+ "SELECT feedback_id FROM account_whitelist WHERE account_id=%s",
|
|
|
|
|
+ (account_id,),
|
|
|
|
|
+ )
|
|
|
|
|
+ row = cur.fetchone()
|
|
|
|
|
+ if row and row.get("feedback_id") is not None:
|
|
|
|
|
+ return int(row["feedback_id"])
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "读取 account_id %s 的 DB feedback_id 失败, fallback config 字典:%s",
|
|
|
|
|
+ account_id, e,
|
|
|
|
|
+ )
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _ensure_account_feedback_id(account_id: int) -> Optional[int]:
|
|
|
|
|
+ """按账户绑定的 feedback_key 查找或新建 DataNexus 监测链接组。
|
|
|
|
|
+
|
|
|
|
|
+ feedback_url 是全局模板;feedback_id 是账户级结果,拿到后写回
|
|
|
|
|
+ account_whitelist.feedback_id。
|
|
|
|
|
+ """
|
|
|
|
|
+ import json as _json
|
|
|
|
|
+ import time as _time
|
|
|
|
|
+ import uuid as _uuid
|
|
|
|
|
+ from urllib.parse import urlencode as _urlencode
|
|
|
|
|
+
|
|
|
|
|
+ import httpx as _httpx
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ from db.connection import get_connection
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ raise RuntimeError(f"DB 模块不可用:{e}") from e
|
|
|
|
|
+
|
|
|
|
|
+ conn = get_connection()
|
|
|
|
|
+ try:
|
|
|
|
|
+ with conn.cursor() as cur:
|
|
|
|
|
+ cur.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT f.feedback_key, f.feedback_name, f.second_category_type,
|
|
|
|
|
+ f.feedback_type, f.feedback_url
|
|
|
|
|
+ FROM ad_creation_account_config c
|
|
|
|
|
+ JOIN feedback_asset_template f
|
|
|
|
|
+ ON f.feedback_key = c.feedback_key
|
|
|
|
|
+ AND f.enabled = TRUE
|
|
|
|
|
+ WHERE c.account_id=%s
|
|
|
|
|
+ AND c.enabled = TRUE
|
|
|
|
|
+ """,
|
|
|
|
|
+ (account_id,),
|
|
|
|
|
+ )
|
|
|
|
|
+ tpl = cur.fetchone()
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+
|
|
|
|
|
+ if not tpl:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ feedback_name = str(tpl["feedback_name"])
|
|
|
|
|
+ second_category_type = str(tpl["second_category_type"])
|
|
|
|
|
+ feedback_type = str(tpl["feedback_type"])
|
|
|
|
|
+ feedback_url = str(tpl["feedback_url"])
|
|
|
|
|
+
|
|
|
|
|
+ token = _get_datanexus_access_token(account_id)
|
|
|
|
|
+ common = {
|
|
|
|
|
+ "access_token": token,
|
|
|
|
|
+ "timestamp": str(int(_time.time())),
|
|
|
|
|
+ "nonce": _uuid.uuid4().hex,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ page = 1
|
|
|
|
|
+ while True:
|
|
|
|
|
+ params = dict(common)
|
|
|
|
|
+ params.update({"account_id": account_id, "page": page, "page_size": 100})
|
|
|
|
|
+ resp = _httpx.get(
|
|
|
|
|
+ f"https://api.e.qq.com/v3.0/feedback_info/get?{_urlencode(params)}",
|
|
|
|
|
+ timeout=30,
|
|
|
|
|
+ ).json()
|
|
|
|
|
+ if resp.get("code") != 0:
|
|
|
|
|
+ raise RuntimeError(
|
|
|
|
|
+ f"feedback_info/get 失败 code={resp.get('code')} "
|
|
|
|
|
+ f"msg={resp.get('message_cn') or resp.get('message')}"
|
|
|
|
|
+ )
|
|
|
|
|
+ data = resp.get("data") or {}
|
|
|
|
|
+ items = data.get("list") or []
|
|
|
|
|
+ for item in items:
|
|
|
|
|
+ if item.get("second_category_type") != second_category_type:
|
|
|
|
|
+ continue
|
|
|
|
|
+ url_infos = item.get("url_info_list") or []
|
|
|
|
|
+ for info in url_infos:
|
|
|
|
|
+ if info.get("feedback_type") == feedback_type and info.get("feedback_url") == feedback_url:
|
|
|
|
|
+ feedback_id = int(item["feedback_id"])
|
|
|
|
|
+ _write_account_feedback_id(account_id, feedback_id)
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "[feedback] account=%d 复用已有 feedback_id=%s name=%s",
|
|
|
|
|
+ account_id, feedback_id, item.get("feedback_name"),
|
|
|
|
|
+ )
|
|
|
|
|
+ return feedback_id
|
|
|
|
|
+ if item.get("feedback_name") == feedback_name:
|
|
|
|
|
+ feedback_id = int(item["feedback_id"])
|
|
|
|
|
+ _write_account_feedback_id(account_id, feedback_id)
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "[feedback] account=%d 按名称复用已有 feedback_id=%s name=%s",
|
|
|
|
|
+ account_id, feedback_id, item.get("feedback_name"),
|
|
|
|
|
+ )
|
|
|
|
|
+ return feedback_id
|
|
|
|
|
+
|
|
|
|
|
+ page_info = data.get("page_info") or resp.get("page_info") or {}
|
|
|
|
|
+ total_page = int(page_info.get("total_page") or page)
|
|
|
|
|
+ if page >= total_page:
|
|
|
|
|
+ break
|
|
|
|
|
+ page += 1
|
|
|
|
|
+
|
|
|
|
|
+ add_params = dict(common)
|
|
|
|
|
+ add_params["nonce"] = _uuid.uuid4().hex
|
|
|
|
|
+ body = {
|
|
|
|
|
+ "account_id": account_id,
|
|
|
|
|
+ "feedback_name": feedback_name,
|
|
|
|
|
+ "second_category_type": second_category_type,
|
|
|
|
|
+ "url_info_list": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "feedback_type": feedback_type,
|
|
|
|
|
+ "feedback_url": feedback_url,
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ }
|
|
|
|
|
+ add_resp = _httpx.post(
|
|
|
|
|
+ f"https://api.e.qq.com/v3.0/feedback_info/add?{_urlencode(add_params)}",
|
|
|
|
|
+ json=body,
|
|
|
|
|
+ timeout=30,
|
|
|
|
|
+ ).json()
|
|
|
|
|
+ if add_resp.get("code") != 0:
|
|
|
|
|
+ raise RuntimeError(
|
|
|
|
|
+ f"feedback_info/add 失败 code={add_resp.get('code')} "
|
|
|
|
|
+ f"msg={add_resp.get('message_cn') or add_resp.get('message')} "
|
|
|
|
|
+ f"body={_json.dumps(body, ensure_ascii=False)[:300]}"
|
|
|
|
|
+ )
|
|
|
|
|
+ feedback_id = int(((add_resp.get("data") or {}).get("feedback_info") or {})["feedback_id"])
|
|
|
|
|
+ _write_account_feedback_id(account_id, feedback_id)
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "[feedback] account=%d 新建 feedback_id=%s feedback_key=%s",
|
|
|
|
|
+ account_id, feedback_id, tpl.get("feedback_key"),
|
|
|
|
|
+ )
|
|
|
|
|
+ return feedback_id
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _get_datanexus_access_token(account_id: int) -> str:
|
|
|
|
|
+ """获取 DataNexus API access_token。
|
|
|
|
|
+
|
|
|
|
|
+ 当前内部 token API 实测可用于 /feedback_info/get;如后续拆分 DataNexus
|
|
|
|
|
+ 专用 token,可通过 DATANEXUS_ACCESS_TOKEN 或 DATANEXUS_TOKEN_API 覆盖。
|
|
|
|
|
+ """
|
|
|
|
|
+ import httpx as _httpx
|
|
|
|
|
+
|
|
|
|
|
+ static_token = os.getenv("DATANEXUS_ACCESS_TOKEN", "").strip()
|
|
|
|
|
+ if static_token:
|
|
|
|
|
+ return static_token
|
|
|
|
|
+
|
|
|
|
|
+ token_api = os.getenv("DATANEXUS_TOKEN_API") or os.getenv(
|
|
|
|
|
+ "TENCENT_AD_TOKEN_API",
|
|
|
|
|
+ "https://api.piaoquantv.com/ad/put/tencent/getAccessToken",
|
|
|
|
|
+ )
|
|
|
|
|
+ resp = _httpx.get(token_api, params={"accountId": account_id}, timeout=10)
|
|
|
|
|
+ resp.raise_for_status()
|
|
|
|
|
+ token = resp.text.strip()
|
|
|
|
|
+ if not token or len(token) <= 10:
|
|
|
|
|
+ raise RuntimeError("DataNexus token 返回异常")
|
|
|
|
|
+ return token
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _write_account_feedback_id(account_id: int, feedback_id: int) -> None:
|
|
|
|
|
+ """把账户级 DataNexus feedback_id 写回白名单。"""
|
|
|
|
|
+ from db.connection import get_connection
|
|
|
|
|
+
|
|
|
|
|
+ conn = get_connection()
|
|
|
|
|
+ try:
|
|
|
|
|
+ with conn.cursor() as cur:
|
|
|
|
|
+ cur.execute(
|
|
|
|
|
+ """
|
|
|
|
|
+ UPDATE account_whitelist
|
|
|
|
|
+ SET feedback_id=%s,
|
|
|
|
|
+ updated_by=%s,
|
|
|
|
|
+ updated_at=CURRENT_TIMESTAMP
|
|
|
|
|
+ WHERE account_id=%s
|
|
|
|
|
+ """,
|
|
|
|
|
+ (feedback_id, "auto-feedback-init", account_id),
|
|
|
|
|
+ )
|
|
|
|
|
+ conn.commit()
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+
|
|
|
OPTIMIZATION_GOAL = "OPTIMIZATIONGOAL_PROMOTION_VIEW_KEY_PAGE" # 关键页面访问次数(USER_GROWTH 配套)
|
|
OPTIMIZATION_GOAL = "OPTIMIZATIONGOAL_PROMOTION_VIEW_KEY_PAGE" # 关键页面访问次数(USER_GROWTH 配套)
|
|
|
# 注:2026-06-05 业务确认 — 两个测试账户均走 USER_GROWTH + PAGE_KEY 路线
|
|
# 注:2026-06-05 业务确认 — 两个测试账户均走 USER_GROWTH + PAGE_KEY 路线
|
|
|
# 即使 83846804 带人群包,仍用此优化目标,不切换到 BRAND_PROMOTION + CLICK
|
|
# 即使 83846804 带人群包,仍用此优化目标,不切换到 BRAND_PROMOTION + CLICK
|
|
@@ -415,7 +786,7 @@ SITE_SET_COMBINATIONS = [
|
|
|
SMART_TARGETING_MODE = "SMART_TARGETING_MANUAL"
|
|
SMART_TARGETING_MODE = "SMART_TARGETING_MANUAL"
|
|
|
|
|
|
|
|
# --- WECHAT_POSITION 定投场景(2026-06-11 用户确认:生产 5 项小程序版位)---
|
|
# --- WECHAT_POSITION 定投场景(2026-06-11 用户确认:生产 5 项小程序版位)---
|
|
|
-# 历史:9 项 preset(来自参考广告 77868332)→ 实际生产删除重建走 3 项 → 本期补 1024797/1024794 共 5 项
|
|
|
|
|
|
|
+# 历史:9 项 preset(来自参考广告 77868332)→ 实际生产删除重建走 3 项 → 本期补 1024797。
|
|
|
# 中文映射通过 tools.scene_spec.get_wechat_position_tags(account_id) 运行时查询(进程内缓存 1h)
|
|
# 中文映射通过 tools.scene_spec.get_wechat_position_tags(account_id) 运行时查询(进程内缓存 1h)
|
|
|
#
|
|
#
|
|
|
# 业务生效场景(全部小程序流量位):
|
|
# 业务生效场景(全部小程序流量位):
|
|
@@ -423,17 +794,15 @@ SMART_TARGETING_MODE = "SMART_TARGETING_MANUAL"
|
|
|
# 1024796 小程序插屏广告
|
|
# 1024796 小程序插屏广告
|
|
|
# 2100748 小程序原生广告
|
|
# 2100748 小程序原生广告
|
|
|
# 1024797 小程序封面广告(2026-06-11 新增)
|
|
# 1024797 小程序封面广告(2026-06-11 新增)
|
|
|
-# 1024794 小程序banner广告(2026-06-11 新增 — 见下警告)
|
|
|
|
|
#
|
|
#
|
|
|
-# ⚠️ 1024794 banner 警告(2026-06-11 实测,用户决策保留):
|
|
|
|
|
-# → /adgroups/update 接口已下线,报 code=1800945 "banner 广告选项已下线,请选择其他选项"
|
|
|
|
|
-# → /adgroups/add 接口未实测,scene_spec_tags/get 仍返回此 ID(账户级仍开放)
|
|
|
|
|
-# → 若下次 Phase 0 POST add 也报 1800945,从此列表移除 1024794 即可
|
|
|
|
|
|
|
+# 1024794 小程序 banner 广告已移除:
|
|
|
|
|
+# → /adgroups/update 接口已下线,报 code=1800945
|
|
|
|
|
+# → 2026-06-30 /adgroups/add 对 84502339 同样报 code=1800945
|
|
|
#
|
|
#
|
|
|
# ⚠️ 创建后锁死(2026-06-11 实测复现):wechat_position 一旦创建,update 报 code=36840
|
|
# ⚠️ 创建后锁死(2026-06-11 实测复现):wechat_position 一旦创建,update 报 code=36840
|
|
|
# → 要新增场景必须删除重建广告,代价是丢失已挂创意 + 学习数据
|
|
# → 要新增场景必须删除重建广告,代价是丢失已挂创意 + 学习数据
|
|
|
WECHAT_POSITION_TARGETED_PRESET = [
|
|
WECHAT_POSITION_TARGETED_PRESET = [
|
|
|
- 1024795, 1024796, 2100748, 1024797, 1024794,
|
|
|
|
|
|
|
+ 1024795, 1024796, 2100748, 1024797,
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
# 1 账户广告数(2026-06-09 用户确认:2 条,一条有 wechat_position 定投,一条无定投)
|
|
# 1 账户广告数(2026-06-09 用户确认:2 条,一条有 wechat_position 定投,一条无定投)
|
|
@@ -535,17 +904,47 @@ AUDIENCE_TIER_PATTERNS = [
|
|
|
# 数据流:find_ads_needing_creatives → 关联点过滤 → 召回素材 → POST 创意。
|
|
# 数据流:find_ads_needing_creatives → 关联点过滤 → 召回素材 → POST 创意。
|
|
|
|
|
|
|
|
# --- 创意补量目标(单广告期望创意数)---
|
|
# --- 创意补量目标(单广告期望创意数)---
|
|
|
-# 2026-06-09:用户确认改 3(每广告 3 条创意)
|
|
|
|
|
|
|
+# 2026-07-01:用户确认最终合格创意不少于 4 个
|
|
|
# 生产阶段长期应回到 15(腾讯经验下限,MIN_CREATIVES_PER_AD)
|
|
# 生产阶段长期应回到 15(腾讯经验下限,MIN_CREATIVES_PER_AD)
|
|
|
# 这是 find_ads_needing_creatives 阈值 + 补量目标的**同一个语义变量**,不要拆
|
|
# 这是 find_ads_needing_creatives 阈值 + 补量目标的**同一个语义变量**,不要拆
|
|
|
-TARGET_CREATIVES_PER_AD = 3
|
|
|
|
|
|
|
+TARGET_CREATIVES_PER_AD = 4
|
|
|
|
|
|
|
|
# --- 主循环 try-fallback 限额(防无限召回)---
|
|
# --- 主循环 try-fallback 限额(防无限召回)---
|
|
|
# 单广告最多尝试 N 条 landing,超过即放弃此条创意(不影响广告剩余 to_add)
|
|
# 单广告最多尝试 N 条 landing,超过即放弃此条创意(不影响广告剩余 to_add)
|
|
|
-# 2026-06-10 用户要求:扩到 30(从 10),因为 used_landing_ids 全局共享后池子越来越紧
|
|
|
|
|
-MAX_LANDING_ATTEMPTS_PER_AD = 30
|
|
|
|
|
|
|
+# 2026-07-01 用户确认:视频获取/尝试上限 100 条
|
|
|
|
|
+MAX_LANDING_ATTEMPTS_PER_AD = 100
|
|
|
# 单 landing 最多尝试 N 条素材(召回 top N)
|
|
# 单 landing 最多尝试 N 条素材(召回 top N)
|
|
|
MAX_MATERIAL_PER_LANDING = 10
|
|
MAX_MATERIAL_PER_LANDING = 10
|
|
|
|
|
+# 内容服务返回的内容品类黑名单。为空则不过滤;多个品类用英文逗号分隔。
|
|
|
|
|
+LANDING_EXCLUDED_CATEGORIES = {
|
|
|
|
|
+ v.strip()
|
|
|
|
|
+ for v in os.getenv("LANDING_EXCLUDED_CATEGORIES", "早中晚好,祝福音乐").split(",")
|
|
|
|
|
+ if v.strip()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# --- 承接视频风险审核(2026-06-29 接入)---
|
|
|
|
|
+# 在 xcx/save 之前调用 piaoquantv 风险标签接口。高风险视频直接跳过,继续尝试下一条 landing。
|
|
|
|
|
+VIDEO_RISK_CHECK_ENABLED = True
|
|
|
|
|
+VIDEO_RISK_API_URL = os.getenv(
|
|
|
|
|
+ "VIDEO_RISK_API_URL",
|
|
|
|
|
+ "https://longvideoapi.piaoquantv.com/longvideoapi/openapi/video/getVideoTagIds",
|
|
|
|
|
+)
|
|
|
|
|
+# 风险等级映射:tag_id -> level,level 越高风险越大。
|
|
|
|
|
+VIDEO_RISK_TAG_LEVELS = {
|
|
|
|
|
+ "85856": 1,
|
|
|
|
|
+ "85862": 2,
|
|
|
|
|
+ "85863": 3,
|
|
|
|
|
+ "85864": 4,
|
|
|
|
|
+ "85865": 5,
|
|
|
|
|
+ "85866": 6,
|
|
|
|
|
+ "85867": 7,
|
|
|
|
|
+ "85868": 8,
|
|
|
|
|
+ "85869": 9,
|
|
|
|
|
+ "85870": 10,
|
|
|
|
|
+}
|
|
|
|
|
+# 默认仅允许 0/1,拦截 2-10。可通过环境变量临时调整。
|
|
|
|
|
+VIDEO_RISK_MAX_ALLOWED_LEVEL = int(os.getenv("VIDEO_RISK_MAX_ALLOWED_LEVEL", "1"))
|
|
|
|
|
+VIDEO_RISK_API_TIMEOUT_SECONDS = int(os.getenv("VIDEO_RISK_API_TIMEOUT_SECONDS", "10"))
|
|
|
|
|
|
|
|
# --- 素材召回质量过滤(2026-06-10 用户确认,batchByText 升级)---
|
|
# --- 素材召回质量过滤(2026-06-10 用户确认,batchByText 升级)---
|
|
|
# 服务端 ranking 参数:simThreshold=0.7(语义相关),alpha=0(不要 sim 加权)
|
|
# 服务端 ranking 参数:simThreshold=0.7(语义相关),alpha=0(不要 sim 加权)
|
|
@@ -573,17 +972,11 @@ RECALL_CONFIG_CODES_FULL = [
|
|
|
"INSPIRATION_FORM", "KEYPOINT_FORM", "PURPOSE_FORM",
|
|
"INSPIRATION_FORM", "KEYPOINT_FORM", "PURPOSE_FORM",
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
-# --- 创意文案池(2026-06-09 用户确认,照搬示例 78420850 反查值)---
|
|
|
|
|
-# 示例 78420850 的 13 条创意全部 ACTIVE,description 都用同一组 3 句通用文案
|
|
|
|
|
-# 我们旧策略(用素材标题)实测 DENIED + SUSPEND,改为照搬示例文案池
|
|
|
|
|
|
|
+# --- 创意文案池---
|
|
|
|
|
+# 2026-07-01 用户确认:创意文案统一使用"打开看看"。
|
|
|
CREATIVE_DESCRIPTION_POOL = [
|
|
CREATIVE_DESCRIPTION_POOL = [
|
|
|
"打开看看",
|
|
"打开看看",
|
|
|
- "究竟怎么回事?打开看看",
|
|
|
|
|
- "竟有这事?",
|
|
|
|
|
]
|
|
]
|
|
|
-# 每条创意从池中随机选 N 条作为 description
|
|
|
|
|
-# 2026-06-10 用户确认:N=1 — 每条创意随机抽 1 条文案,不再 3 条一起
|
|
|
|
|
-# (跟示例 78420850 的"3 条一起"策略不同 — 用户明确要求多样性)
|
|
|
|
|
CREATIVE_DESCRIPTION_COUNT_PER_AD = 1
|
|
CREATIVE_DESCRIPTION_COUNT_PER_AD = 1
|
|
|
|
|
|
|
|
# --- 审批开关 ---
|
|
# --- 审批开关 ---
|
|
@@ -618,4 +1011,3 @@ SLS_LOG_LEVEL = "INFO"
|
|
|
# QueuedLogHandler 内部异步队列参数(SDK 默认 + 微调,避免长连接 idle 断)
|
|
# QueuedLogHandler 内部异步队列参数(SDK 默认 + 微调,避免长连接 idle 断)
|
|
|
SLS_BATCH_SIZE_MAX = 1024 # 单次 PutLogs 最多条数
|
|
SLS_BATCH_SIZE_MAX = 1024 # 单次 PutLogs 最多条数
|
|
|
SLS_PUT_WAIT_MS = 2000 # 队列攒到 batch_size 或等 2s flush 一次
|
|
SLS_PUT_WAIT_MS = 2000 # 队列攒到 batch_size 或等 2s flush 一次
|
|
|
-
|
|
|