""" 广告智能决策引擎配置 — auto_put_ad_mini 运营可直接修改此文件调整决策参数。 当前模式:智能判断 - 基于 动态 ROI (7日均值) 的精细化决策 - AI 推理结合领域知识 - 三级分类:零消耗待关停(规则)+ 待优化评估(智能)+ 正常运行(规则) """ import os import logging from datetime import date, datetime from pathlib import Path from typing import Optional from zoneinfo import ZoneInfo from agent.core.runner import RunConfig, KnowledgeConfig # 初始化 logger(必须在使用前定义) logger = logging.getLogger(__name__) # 加载 .env 文件(如果存在) try: from dotenv import load_dotenv load_dotenv(Path(__file__).parent / ".env") except ImportError: pass # ═══════════════════════════════════════════ # Agent 运行配置 # ═══════════════════════════════════════════ MAIN_CONFIG = RunConfig( model="anthropic/claude-sonnet-4.5", temperature=0.3, max_iterations=50, name="广告智能调控助手", tools=[ "fetch_creative_data", "merge_creative_data", "calculate_roi_metrics", "calculate_creative_roi", # 创意级动态 ROI(广告级 pause 候选的二次细化) "calculate_portfolio_summary", "get_ads_for_review", "apply_decisions", "query_ad_detail", # Mode 2: 查询广告详情 "modify_decisions", # Mode 3: 修改已有决策 "validate_decisions", "generate_report", # 执行引擎 + IM 审批(已集成阻塞式审批流): "execute_decisions", "check_execution_feedback", "send_approval_request", "check_approval_status", "send_feishu_text_message", # 执行后向您同步 diff / 确认 / 质疑回应 # 飞书文档(报告导入 & 分享): "import_to_feishu", # 注:曾考虑用内置 "agent" 工具按 tier 并行委托子 Agent, # 但框架的 agent 工具只返回文本 summary,主 Agent 拿不回结构化决策, # 会陷入"无法 apply"的死循环。直接在主 Agent 单次输出完成全部 decisions 更可靠。 ], skills=[ "ad-domain", # 业务模型:裂变模型、R值、ROI公式、字段定义 "platform-rules", # 平台硬约束:oCPM学习期、调价上限、数据口径 "decision-strategy", # 决策策略:角色 + 基准 + 候选标记 + 年龄策略 + 7种action + 输出规范 "posterior-wisdom", # 后验经验:学习中断/降价恢复/创意冷启动/置信度分级 ], extra_llm_params={"max_tokens": 32000}, knowledge=KnowledgeConfig( enable_extraction=False, # 从决策过程中提取后验经验(投放后开启) enable_completion_extraction=False, # 完成后总结本轮经验(投放后开启) enable_injection=False, # 决策时自动注入相关历史经验(投放后开启) owner="ad_mini_team", ), ) SKILLS_DIR = str(Path(__file__).parent / "skills") TRACE_STORE_PATH = ".trace" LOG_LEVEL = "INFO" LOG_FILE = None # ═══════════════════════════════════════════ # 时区配置 # ═══════════════════════════════════════════ TIMEZONE = os.getenv("TZ", "Asia/Shanghai") logger.info(f"运行时区:{TIMEZONE}") def now_in_timezone() -> datetime: """返回项目统一业务时区的 aware datetime。""" return datetime.now(ZoneInfo(TIMEZONE)) # ═══════════════════════════════════════════ # V3 数据窗口配置 # ═══════════════════════════════════════════ DATA_WINDOW_DAYS = 14 # 数据采集窗口:14 天历史数据 ROI_CALCULATION_DAYS = 7 # 动态 ROI (7日均值) 计算窗口(保持 7 天) # ═══════════════════════════════════════════ # V3 决策阈值(默认值,可被 SKILL 覆盖) # ═══════════════════════════════════════════ MIN_DAILY_COST = 100 # 日消耗 >= 100元才参与 ROI 计算 MIN_AD_AGE_DAYS = 3 # 广告创建 >= 3天才参与决策(与 min_periods 对齐) ROI_LOW_FACTOR = 0.75 # 动态 ROI (7日均值) < 全体均值 × 0.75 → 关停 NO_SPEND_THRESHOLD = 10 # 7日消耗均值 < 10元 → 关停 STABLE_SPEND_THRESHOLD = 100 # 稳定消耗定义:>100元/天 # ═══════════════════════════════════════════ # 出价调整配置 # ═══════════════════════════════════════════ BID_ADJUSTMENT_ENABLED = True BID_DOWN_ROI_FACTOR = 0.90 # ROI < 均值×0.90 → 考虑降价(低于渠道均值10%) BID_UP_ROI_FACTOR = 1.05 # ROI > 均值×1.05 → 考虑提价(高于渠道均值5%) BID_UP_MAX_SPEND = 1000 # 提价消耗上限:均值消耗<1000才提价(投手经验原文) BID_CHANGE_MIN_PCT = 0.03 # 最小调幅 3%(兼容旧代码) BID_CHANGE_MAX_PCT = 0.10 # 最大单次调幅 10%(兼容旧代码) BID_UP_MIN_PCT = 0.05 # 提价最小幅度 5% BID_UP_MAX_PCT = 0.10 # 提价最大幅度 10% BID_DOWN_MIN_PCT = 0.03 # 降价最小幅度 3% BID_DOWN_MAX_PCT = 0.05 # 降价最大幅度 5% BID_DOWN_MIN_SPEND = 500 # 降价消耗门槛:7日日均消耗≥500元 BID_FLOOR_YUAN = 0.05 # 出价下限(元) BID_CEILING_YUAN = 1.00 # 出价上限(元) # 广告年龄分段(基于决策树图片) COLD_START_DAYS = 3 # 冷启动期(≤3天):极度保护,几乎不干预 EARLY_GROWTH_DAYS = 7 # 早期成长期(4-7天):可提价放量(满足ROI+消耗条件) AD_AGE_MATURE = 7 # 成熟期(>7天):全面调控 # 兼容性(已废弃) AD_AGE_NEWBORN = COLD_START_DAYS # 兼容旧代码 CAUTIOUS_DAYS = EARLY_GROWTH_DAYS # 兼容旧代码 # 高燃烧预警配置 HIGH_BURN_AGE_THRESHOLD = 3 # 广告年龄>3天才检查 HIGH_BURN_COST_THRESHOLD = 300 # 昨日消耗>300元触发预警 ROI_LOW_MIN_YESTERDAY_COST = 300 # 关停消耗门槛:昨日消耗≥300才检查关停(投手经验2.4) # ═══════════════════════════════════════════ # 创意级 pause 细化配置 # ═══════════════════════════════════════════ # 当广告级判 pause 时,先做创意级二次分析:全员低于阈值才真关广告,部分拖累只关差创意 CREATIVE_PAUSE_ENABLED = True # 总开关:False 时全部走广告级 pause(降级路径) CREATIVE_MIN_COST_SHARE = 0.15 # 创意 7 日消耗占比 < 此值视为数据稀疏,不纳入"判死刑" CREATIVE_MIN_AGE_DAYS = 7 # 创意年龄 < 此值视为冷启动,不纳入"判死刑"(对齐广告级 EARLY_GROWTH_DAYS) CREATIVE_MIN_VALID_DAYS = 3 # 创意有效 ROI 数据天数 < 此值视为不充分,不纳入"判死刑" CREATIVE_MIN_REMAINING = 2 # 关停后剩余 eligible 创意数 < 此值,升级为广告级 pause CREATIVE_MAX_PAUSE_COST_SHARE = 0.80 # pause_targets 总占消耗 > 此值,本质是关广告,升级为广告级 pause CREATIVE_RATELIMIT_DAYS = 7 # 同一创意 7 天内不允许重复 pause # ═══════════════════════════════════════════ # 安全护栏配置 # ═══════════════════════════════════════════ GUARDRAILS_ENABLED = True DRY_RUN_MODE = False # 关闭干运行,让护栏正常放行(实际执行由 EXECUTION_ENABLED 控制) MAX_ADJUSTMENTS_PER_AD_PER_DAY = 2 MIN_ADJUSTMENT_INTERVAL_HOURS = 6 MAX_DAILY_CUMULATIVE_CHANGE_PCT = 0.20 # 日累计调幅上限 20% MAX_DAILY_OPS = 10000 # 单日最多操作广告数(实际不限制) DATA_FRESHNESS_MAX_HOURS = 96 # 数据超过 96 小时视为过期(已从48小时放宽至96小时) # ═══════════════════════════════════════════ # 执行引擎配置 # ═══════════════════════════════════════════ # 执行开关(优先级:数据库 > 环境变量 > 默认值False) EXECUTION_ENABLED = False try: from db import get_system_config _db_execution_enabled = get_system_config("execution_enabled", default=None) if _db_execution_enabled is not None: EXECUTION_ENABLED = _db_execution_enabled logger.info(f"✅ 从数据库读取执行开关:{EXECUTION_ENABLED}") else: # 降级到环境变量 _env_execution_enabled = os.getenv("EXECUTION_ENABLED", "").strip().lower() if _env_execution_enabled: EXECUTION_ENABLED = _env_execution_enabled in ("true", "1", "yes") logger.info(f"从环境变量读取执行开关:{EXECUTION_ENABLED}") except Exception as e: logger.warning(f"⚠️ 数据库读取执行开关失败({e}),使用默认值:{EXECUTION_ENABLED}") API_QPS_LIMIT = 8 # 保守QPS(平台上限10) API_MAX_RETRIES = 3 TIER1_MAX_CHANGE_PCT = 0.00 # Tier1自动执行已禁用(改为0%,所有操作都需审批) TIER3_MIN_DAILY_SPEND = 1500 # 高价值广告门槛(元/天) FEEDBACK_CHECK_HOURS = 6 # ═══════════════════════════════════════════ # IM 审批配置(飞书直连) # ═══════════════════════════════════════════ IM_ENABLED = True # IM 主开关(True 时审批消息发飞书) IM_APPROVAL_TIMEOUT_MINUTES = 120 # 审批超时(分钟)— 2小时 IM_APPROVAL_POLL_INTERVAL_SECONDS = 30 # 审批轮询间隔(秒) # 飞书应用凭据("增长投放"机器人)— 优先从环境变量读取 FEISHU_APP_ID = os.getenv("FEISHU_APP_ID", "cli_a955e97067f85cb3") FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET", "NQaG4ci1plXRDTgwCqrLJgMLLoA2tdF8") # 运营审批人飞书信息 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", "oc_7940ec97cde40b245cff9cb606ff1ac7") # 腾讯广告默认账户(测试账户) TENCENT_AD_ACCOUNT_ID = int(os.getenv("TENCENT_AD_ACCOUNT_ID", "80769799")) # ═══════════════════════════════════════════ # 账户白名单配置 # ═══════════════════════════════════════════ # 白名单模式开关(优先级:数据库 > 环境变量) WHITELIST_ENABLED = None WHITELIST_ACCOUNTS = [] # 尝试从数据库读取配置 try: from db import get_whitelist_accounts, get_system_config # 读取白名单开关 WHITELIST_ENABLED = get_system_config("whitelist_enabled", default=None) # 读取白名单账户列表 WHITELIST_ACCOUNTS = get_whitelist_accounts() logger.info(f"✅ 从数据库读取白名单配置:{len(WHITELIST_ACCOUNTS)} 个账户") except Exception as db_error: logger.warning(f"⚠️ 数据库读取失败({db_error}),降级到环境变量配置") # 降级方案1:从环境变量读取 _whitelist_str = os.getenv("WHITELIST_ACCOUNTS", "") if _whitelist_str: # 格式:逗号分隔,如 "80769799,71305011" WHITELIST_ACCOUNTS = [int(x.strip()) for x in _whitelist_str.split(",") if x.strip()] logger.info(f"从环境变量读取白名单:{len(WHITELIST_ACCOUNTS)} 个账户") else: # 降级方案2:从文件读取(可选) _whitelist_file = Path(__file__).parent / "whitelist.json" if _whitelist_file.exists(): import json with open(_whitelist_file) as f: whitelist_data = json.load(f) WHITELIST_ACCOUNTS = whitelist_data.get("accounts", []) logger.info(f"从 whitelist.json 读取白名单:{len(WHITELIST_ACCOUNTS)} 个账户") # 白名单开关降级处理 if WHITELIST_ENABLED is None: WHITELIST_ENABLED = os.getenv("WHITELIST_ENABLED", "true").lower() == "true" # 向后兼容:单账户模式 if not WHITELIST_ACCOUNTS: WHITELIST_ACCOUNTS = [TENCENT_AD_ACCOUNT_ID] logger.info(f"白名单为空,使用单账户模式:{TENCENT_AD_ACCOUNT_ID}") logger.info( f"白名单配置:{'启用' if WHITELIST_ENABLED else '禁用'}," f"账户数={len(WHITELIST_ACCOUNTS)},列表={WHITELIST_ACCOUNTS[:5]}..." ) # ═══════════════════════════════════════════ # 实验范围 Scope(MVP) # ═══════════════════════════════════════════ # 2026-06-08:DB 白名单已直接收窄到 2 个测试账户,不再 override # 单一真相源 = DB account_whitelist 表 enabled=1 的行 # 若要临时收窄,改这里为 {account_id...} EXPERIMENTAL_SCOPE_ACCOUNTS = None EXPERIMENTAL_SCOPE_REASON = "已迁 DB,config 不再 override" if EXPERIMENTAL_SCOPE_ACCOUNTS is not None: _scope_before = len(WHITELIST_ACCOUNTS) WHITELIST_ACCOUNTS = [a for a in WHITELIST_ACCOUNTS if a in EXPERIMENTAL_SCOPE_ACCOUNTS] # 兜底:如 scope 中账户不在 DB 白名单(或 DB 读取失败),直接采用 scope 列表 if not WHITELIST_ACCOUNTS: WHITELIST_ACCOUNTS = list(EXPERIMENTAL_SCOPE_ACCOUNTS) logger.warning( f"⚠️ EXPERIMENTAL_SCOPE {EXPERIMENTAL_SCOPE_ACCOUNTS} 与现有白名单交集为空," f"直接使用 scope 列表" ) logger.info( f"🧪 EXPERIMENTAL_SCOPE 启用:WHITELIST_ACCOUNTS 由 {_scope_before} → " f"{len(WHITELIST_ACCOUNTS)} 个 ({WHITELIST_ACCOUNTS})。原因:{EXPERIMENTAL_SCOPE_REASON}" ) # ═══════════════════════════════════════════ # 待投放账户配置(DB 单一真相源) # ═══════════════════════════════════════════ # 人工只配置: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: try: from configure_creation_accounts import ensure_account_delivery_config_columns ensure_account_delivery_config_columns() except Exception as e: logger.warning("确保账户投放配置列失败:%s", e) 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.bid_scene, c.custom_cost_cap_fen, c.automatic_site_enabled, c.site_set_json AS account_site_set_json, c.location_types_json AS account_location_types_json, c.region_ids_json AS account_region_ids_json, c.age_min, c.age_max, c.config_date, 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 from tools.delivery_config import parse_bid_scene site_set = _json.loads(row.get("account_site_set_json") or row["site_set_json"]) location_types = _json.loads( row["account_location_types_json"] if row.get("account_location_types_json") is not None else row["location_types_json"] ) region_ids = _json.loads( row["account_region_ids_json"] if row.get("account_region_ids_json") is not None else row["region_ids_json"] ) time_series = _json.loads(row["time_series_json"]) automatic_site_enabled = row.get("automatic_site_enabled") 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), "bid_scene": parse_bid_scene(row.get("bid_scene")), "custom_cost_cap_fen": ( int(row["custom_cost_cap_fen"]) if row.get("custom_cost_cap_fen") is not None else None ), "age": [{"min": int(age_min), "max": int(age_max)}], "site_set": site_set, "automatic_site_enabled": bool(automatic_site_enabled) if automatic_site_enabled is not None else False, "location_types": location_types, "region_ids": region_ids, "config_date": ( row["config_date"].isoformat() if row.get("config_date") is not None else None ), "daily_budget_fen": int( row["account_daily_budget_fen"] if row.get("account_daily_budget_fen") is not None else 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(config_date: Optional[date] = None) -> list[int]: """读取指定飞书配置日期启用的账户,作为 Phase 0/1 自动化范围。""" try: from db.connection import get_connection except Exception as e: logger.warning("读取待投放账户失败:DB 模块不可用(%s)", e) return [] conn = get_connection() try: target_config_date = config_date or now_in_timezone().date() 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 AND c.config_date = %s ORDER BY c.id ASC """, (target_config_date,), ) 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 业务确认) # ═══════════════════════════════════════════════════════════════════ # 投放 SOP:广告搭建 = 固定定向 & 人群 & 出价 # 小程序产品:票圈 | 3亿人喜欢的视频平台 # 几乎所有维度都是固定的,LLM 不参与"营销内容/定向/出价类型"决策 # 唯一可变维度:site_set 组合(3 种) + 出价数值(从区间内取) # --- 营销内容(全固定)--- MARKETING_GOAL = "MARKETING_GOAL_USER_GROWTH" MARKETING_SUB_GOAL = "MARKETING_SUB_GOAL_UNKNOWN" # 修正(2026-06-05 真实样本反推):marketing_carrier_type 是 JUMP_PAGE,不是 MINI_PROGRAM_WECHAT # 小程序信息通过 marketing_asset_outer_spec 嵌套传递 MARKETING_CARRIER_TYPE = "MARKETING_CARRIER_TYPE_JUMP_PAGE" MARKETING_CARRIER_NAME = "票圈 | 3亿人喜欢的视频平台" MARKETING_CARRIER_GH_ID = "gh_ecd1ea0b84cf" # 小程序 GH ID MARKETING_CARRIER_WX_APP_ID = "wx89e7eb06478361d7" # 小程序 WX AppID(可能 add 时不传,待 dry run) # marketing_asset_outer_spec 嵌套结构(add 时传) MARKETING_TARGET_TYPE = "MARKETING_TARGET_TYPE_MINI_PROGRAM_WECHAT" MARKETING_ASSET_OUTER_SPEC = { "marketing_target_type": MARKETING_TARGET_TYPE, "marketing_asset_outer_id": MARKETING_CARRIER_GH_ID, } # conversion_id 不传(可选 + 不支持朋友圈版位) # 改用 optimization_goal 直接走 # ═══════════════════════════════════════════════════════════════════ # 账户级 feedback_id 映射(2026-06-05 业务确认) # ═══════════════════════════════════════════════════════════════════ # feedback_id = 监测链接 ID,是账户级配置,广告 add 时必填 # 短期:本字典占位;长期:迁到 DB(扩展 account_whitelist 表加列) # 设计接口:get_account_feedback_id(account_id) → 先查 DB,fallback 本字典 ACCOUNT_FEEDBACK_ID_MAPPING = { # account_id : feedback_id 83846793: 6700703, # 用户 2026-06-05 提供(非人群包账户) 83846804: 6700002, # 用户 2026-06-05 提供(R330+ 人群包账户) } def get_account_feedback_id(account_id: int): """获取账户的 feedback_id(监测链接 ID)。 优先级: 1. DB account_whitelist.feedback_id 2. ad_creation_account_config.feedback_key → feedback_asset_template 自动查找/创建 3. config.py 旧字典 返回 None 时调用方应反问或报错,不能猜测。 """ 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) 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 配套) # 注:2026-06-05 业务确认 — 两个测试账户均走 USER_GROWTH + PAGE_KEY 路线 # 即使 83846804 带人群包,仍用此优化目标,不切换到 BRAND_PROMOTION + CLICK # --- 定向(全固定 SOP)--- FIXED_TARGETING_AGE = [{"min": 45, "max": 66}] # 45-66 岁(自定义) FIXED_TARGETING_GENDER = "ALL" # 不限制(不传 gender) FIXED_TARGETING_LOCATION_TYPES = ["LIVE_IN"] # 常住地 # 地域 region_id 列表 — 从 JSON 读(运营可改 JSON 调整生效地域) # 来源:ad 95205841163 (account 81214386) 实际投放地域反推 # 实际排除:港澳台 + 东三省 + 河南(共 7 个一级行政区) FIXED_TARGETING_REGIONS_JSON_PATH = Path(__file__).parent / "data" / "tencent_constants" / "regions_sop_current.json" try: import json as _json with open(FIXED_TARGETING_REGIONS_JSON_PATH, encoding="utf-8") as _f: _regions_data = _json.load(_f) FIXED_TARGETING_REGION_IDS = [r["id"] for r in _regions_data["list"]] logger.info( f"✅ 从 {FIXED_TARGETING_REGIONS_JSON_PATH.name} 加载 {len(FIXED_TARGETING_REGION_IDS)} 个地域 region_id" ) except FileNotFoundError: FIXED_TARGETING_REGION_IDS = [] logger.warning( f"⚠️ 地域 JSON 未找到:{FIXED_TARGETING_REGIONS_JSON_PATH},新建广告时 targeting.geo_location 为空" ) # 实际排除的一级行政区(给审批表 / 报告人类可读用) EXCLUDED_PROVINCES_SEMANTIC = ["香港", "澳门", "台湾", "辽宁", "吉林", "黑龙江", "河南"] # --- 出价 / 计费 --- BID_MODE = "BID_MODE_OCPM" SMART_BID_TYPE = "SMART_BID_TYPE_CUSTOM" BID_STRATEGY = "BID_STRATEGY_AVERAGE_COST" # 一键起量(auto_acquisition)默认关闭(用户 2026-06-05 确认,与多数样本一致) # 注:腾讯硬约束 — 若启用,budget 必须 >= 20000(200 元) AUTO_ACQUISITION_ENABLED = False AUTO_ACQUISITION_BUDGET_FEN = 20000 # 占位最小值,enabled=False 时不生效 AUTO_DERIVED_CREATIVE_ENABLED = False AIM_SMART_TARGETING_ENABLED = False AIM_SMART_SITE_ENABLED = False DEEP_CONVERSION_SPEC: dict = {} # --- 转化(conversion_id)--- # 用户 2026-06-05 指示:两个测试账户都用 1007(与样本 92067863445 一致) # 长期:迁到 account_whitelist 表加列 conversion_id DEFAULT_CONVERSION_ID = 1007 # --- 搜索场景扩量 · 定向拓展开关(用户 2026-06-05 反推确认)--- # 3 条线上样本均为 CLOSE,我们若不传腾讯默认 OPEN → 与 SOP 不一致 # 用 ad_api 反推得知字段名:search_expand_targeting_switch SEARCH_EXPAND_TARGETING_SWITCH = "SEARCH_EXPAND_TARGETING_SWITCH_CLOSE" # --- 版位(2026-06-09 用户确认:参考 78420850/105832100128 加朋友圈版位)--- AVAILABLE_SITE_SETS = [ "SITE_SET_WECHAT", # 微信公众号 "SITE_SET_WECHAT_PLUGIN", # 微信插件 "SITE_SET_SEARCH_SCENE", # 搜索场景 "SITE_SET_MOMENTS", # 朋友圈 ] # MVP 阶段:单一固定版位组合(差异化先不靠 site_set) SITE_SET_COMBINATIONS = [ ["SITE_SET_WECHAT", "SITE_SET_WECHAT_PLUGIN", "SITE_SET_SEARCH_SCENE", "SITE_SET_MOMENTS"], ] # --- AIM 智能定向(2026-06-11 实测修正:跨接口字段名/枚举值不一致)--- # 文档:https://developers.e.qq.com/v3.0/docs/api/adgroups/update # https://developers.e.qq.com/v3.0/docs/enums#smart_targeting_mode # # write 接口(/adgroups/add + /adgroups/update)字段 = smart_targeting_mode (enum) # · SMART_TARGETING_MANUAL = 手动定向(等价于 AIM 关闭,本期目标) # · 不传该字段 = 腾讯默认开 AIM(实测:get 反查会变成 SMART_TARGETING_AUTO) # · 必须每次显式传(腾讯文档原话:"若选择手动定向需在每次调用接口时显式携带该参数") # # read 接口(/adgroups/get)字段 = smart_targeting_status (只读) # · SMART_TARGETING_NONE = 已关 · SMART_TARGETING_AUTO = 智能定向中 SMART_TARGETING_MODE = "SMART_TARGETING_MANUAL" # --- WECHAT_POSITION 定投场景(2026-07-06 用户确认:扩展公众号 + 小程序位置)--- # 历史:9 项 preset(来自参考广告 77868332)→ 实际生产删除重建走 3 项 → 补 1024797 # → 2026-07-06 扩展公众号文章/订阅号相关位置,并补发现小程序。 # 中文映射通过 tools.scene_spec.get_wechat_position_tags(account_id) 运行时查询(进程内缓存 1h) # # 业务生效场景(公众号内容场景): # 1024789 公众号文章底部 # 1024790 公众号文章中部 # 1024791 公众号文章视频贴片 # 1024792 订阅号消息列表 # 2100802 公众号文章评论区 # # 业务生效场景(小程序流量位): # 1024795 小程序激励式广告 # 1024796 小程序插屏广告 # 1024797 小程序封面广告 # 2100745 发现小程序 # 2100748 小程序原生广告 # # 1024794 小程序 banner 广告已移除: # → /adgroups/update 接口已下线,报 code=1800945 # → 2026-06-30 /adgroups/add 对 84502339 同样报 code=1800945 # # ⚠️ 创建后锁死(2026-06-11 实测复现):wechat_position 一旦创建,update 报 code=36840 # → 要新增场景必须删除重建广告,代价是丢失已挂创意 + 学习数据 WECHAT_POSITION_TARGETED_PRESET = [ # 公众号内容场景 1024789, 1024790, 1024791, 1024792, 2100802, # 小程序场景 1024795, 1024796, 1024797, 2100745, 2100748, ] # 1 账户广告数(2026-06-09 用户确认:2 条,一条有 wechat_position 定投,一条无定投) ADS_PER_ACCOUNT = 2 # --- 时段 / 日期(当前统一为每天 06:00-20:00)--- # 一天 48 段 × 7 天 = 336 位字符串 TIME_SERIES_ONE_DAY = "0" * 12 + "1" * 28 + "0" * 8 TIME_SERIES_DEFAULT = TIME_SERIES_ONE_DAY * 7 # 长度自验 assert len(TIME_SERIES_DEFAULT) == 336, f"time_series 长度错误:{len(TIME_SERIES_DEFAULT)}" DEFAULT_BEGIN_DATE_OFFSET_DAYS = 0 # 创建当日开始 DEFAULT_END_DATE = "0" # 真实样本是字符串 "0",不是 None(腾讯特殊表示长期) # --- 预算(用户 2026-06-05 确认:200 元/广告)--- # 真实样本是 0(不限),但 MVP 阶段我们设硬上限保护 DEFAULT_DAILY_BUDGET_YUAN = 200 # 单广告日预算 DEFAULT_DAILY_BUDGET_FEN = DEFAULT_DAILY_BUDGET_YUAN * 100 # 元 → 分 # --- 出价区间表(按 audience tier label 索引)--- # 来源:用户提供的投放 SOP 出价区间表 AUDIENCE_BID_RANGES = { # tier_label : (min_yuan, max_yuan) "R50_泛惊奇_奇观技艺": (0.35, 0.45), "R50_泛知识_生活科普": (0.35, 0.45), "R50_泛知识_时政历史": (0.35, 0.45), "R50_泛祝福": (0.35, 0.45), "R50_全品类": (0.25, 0.38), "R50_同感个体_个人情感": (0.35, 0.45), "R50_同感个体_退休榜样": (0.35, 0.45), "R500_全品类": (0.38, 0.48), "回流100-180": (0.22, 0.28), "回流180-330": (0.30, 0.40), "回流330+": (0.35, 0.40), "R330+": (0.35, 0.40), # 别名,83846804(Q-R_330+)用 "回流50-100": (0.19, 0.22), "泛人群": (0.19, 0.25), "no_audience_pack": (0.19, 0.25), # 别名,83846793(无人群包)用 } # --- 出价取值策略 --- BID_PICK_STRATEGY = "midpoint" # midpoint / max / min / random COLD_START_BID_PICK_STRATEGY = "midpoint" # 冷启动期取中位(可改 max 抢量) # --- 朋友圈版位专属设置(运营标准模板,待提供)--- FEED_AD_SETTING_HEAD_IMAGE_URL = None # TODO FEED_AD_SETTING_NICK_NAME = None # TODO FEED_AD_SETTING_CONVERSION_BUTTON_TEXT = "查看详情" # 默认 # --- 单账户起步广告条数(Cold Start)--- COLD_START_PER_ACCOUNT_AD_COUNT = 3 # 对应 3 种 site_set 组合 COLD_START_TOTAL_ADS = COLD_START_PER_ACCOUNT_AD_COUNT * len(WHITELIST_ACCOUNTS) # 跟随 DB # ═══════════════════════════════════════════ # 输出路径配置 # ═══════════════════════════════════════════ OUTPUTS_DIR = Path(__file__).parent / "outputs" RAW_DATA_DIR = OUTPUTS_DIR / "raw" # 创意级原始 CSV AD_STATUS_DIR = OUTPUTS_DIR / "ad_status" # 广告状态 CSV REPORTS_DIR = OUTPUTS_DIR / "reports" # 决策报告 EXECUTION_LOG_DIR = OUTPUTS_DIR / "execution_log" # 执行审计日志 DATA_DIR = OUTPUTS_DIR / "data" # 运行时数据(如调整历史) ADJUSTMENT_HISTORY_PATH = DATA_DIR / "adjustment_history.json" # ═══════════════════════════════════════════ # 人群包系数(保留,用于展示) # ═══════════════════════════════════════════ AUDIENCE_COEFFICIENTS = { "R500": 3.0, "R330+": 2.5, "R330": 2.0, "R180": 1.5, "R100": 1.2, "R50": 1.0, "R10": 1.0, "R2": 1.0, "default": 1.0, } # 从广告名称提取 R 值的匹配顺序 AUDIENCE_TIER_PATTERNS = [ ("R500", ["R500", "R_500", "r500"]), ("R330+", ["回流330+", "回流330+-", "回流q330", "330+全品类", "R330+", "R_330+"]), ("R330", ["回流330", "R330", "R_330", "定向330", "r330", "r300"]), ("R180", ["回流180", "R180", "R_180", "定向180", "r180", "r180-330", "r180-300", "R100-180", "R_100-180", "r100-180"]), ("R100", ["回流100", "R100", "R_100", "定向100", "r100", "R50-100"]), ("R50", ["回流50", "R50", "R_50", "r50"]), ("R10", ["R_10", "R10", "r10"]), ("R2", ["R_2", "R2", "r2"]), ] # ═══════════════════════════════════════════════════════════════════ # [MODULE B / 创意搭建] 模块 B 主循环配置 # ═══════════════════════════════════════════════════════════════════ # 模块 B = 给广告挂创意(creative)的子系统,与模块 A(广告新建)对偶。 # 数据流:find_ads_needing_creatives → 关联点过滤 → 召回素材 → POST 创意。 # --- 创意补量目标(单广告期望创意数)--- # 2026-07-09:用户确认最终合格创意目标调整为 8 个 # 生产阶段可继续测试 15-30(腾讯经验下限,MIN_CREATIVES_PER_AD) # 这是 find_ads_needing_creatives 阈值 + 补量目标的**同一个语义变量**,不要拆 TARGET_CREATIVES_PER_AD = int(os.getenv("TARGET_CREATIVES_PER_AD", "8")) CREATIVE_PREPARE_MAX_WORKERS = int(os.getenv("CREATIVE_PREPARE_MAX_WORKERS", "1")) CREATIVE_PREPARE_TASK_BUFFER = int(os.getenv("CREATIVE_PREPARE_TASK_BUFFER", "16")) # --- 主循环 try-fallback 限额(防无限召回)--- # 单广告最多尝试 N 条 landing,超过即放弃此条创意(不影响广告剩余 to_add) # 2026-07-01 用户确认:视频获取/尝试上限 100 条 MAX_LANDING_ATTEMPTS_PER_AD = 100 # 单 landing 最多尝试 N 条素材(召回 top N) 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-5,拦截 6-10。可通过环境变量临时调整。 VIDEO_RISK_MAX_ALLOWED_LEVEL = int(os.getenv("VIDEO_RISK_MAX_ALLOWED_LEVEL", "5")) VIDEO_RISK_API_TIMEOUT_SECONDS = int(os.getenv("VIDEO_RISK_API_TIMEOUT_SECONDS", "10")) # --- 素材召回质量过滤(2026-06-10 用户确认,batchByText 升级)--- # 服务端 ranking 参数只保留 simThreshold=0.8(语义相关),其余加权全部置 0。 # 客户端只用 score >= simThreshold 做硬筛,再按历史消耗 cost 倒序排序。 RECALL_SIM_THRESHOLD = 0.8 RECALL_ALPHA = 0 RECALL_W_CTR = 0 RECALL_W_CVR = 0 RECALL_W_ROI = 0 RECALL_W_OPEN_RATE = 0 RECALL_W_FISSION_RATE = 0 RECALL_DECONSTRUCT_BOOST = 0 RECALL_DAYS = 180 # 投放统计天数(2026-06-10 修正:30→180,跟 admin 后台一致,冷门素材需长周期) RECALL_DISPLAY_K = 30 # 服务端展示条数 RECALL_PARALLEL_MAX_WORKERS = int(os.getenv("RECALL_PARALLEL_MAX_WORKERS", "4")) RECALL_QUERY_LIMIT_PER_VIDEO = int(os.getenv("RECALL_QUERY_LIMIT_PER_VIDEO", "12")) RECALL_MIN_IMPRESSIONS = 0 # 保留兼容配置;当前不作为硬筛。 RECALL_MIN_CTR = 0.0 # 保留兼容配置;当前不作为硬筛。 RECALL_SOURCE_LABELS = ["内部素材"] # 只要内部 MAX_SAME_LANDING_PER_AD_IN_RUN = int(os.getenv("MAX_SAME_LANDING_PER_AD_IN_RUN", "1")) RECALL_CONFIG_CODES_FULL = [ "VIDEO_TOPIC", "VIDEO_INSPIRATION", "VIDEO_PURPOSE", "VIDEO_KEYPOINT", "VIDEO_TITLE", "RESULT_LOG_TOPIC", "RESULT_LOG_THEME", "RESULT_LOG_KEYWORDS", "RESULT_LOG_NARRATION", "INSPIRATION_SUBSTANCE", "KEYPOINT_SUBSTANCE", "PURPOSE_SUBSTANCE", "INSPIRATION_FORM", "KEYPOINT_FORM", "PURPOSE_FORM", ] # --- 创意文案池--- # 2026-07-01 用户确认:创意文案统一使用"打开看看"。 CREATIVE_DESCRIPTION_POOL = [ "打开看看", ] CREATIVE_DESCRIPTION_COUNT_PER_AD = 1 # --- 审批开关 --- # True(默认):Phase 1 准备后写待审批 CSV + 发飞书 sheet → 等运营审批 → Phase 3 POST # False:Phase 1 跑完直接 Phase 3 POST(skip 飞书)— 用于自动化 cron + 信任规则的场景 CREATION_APPROVAL_REQUIRED = True # 审批超时(分钟,与现有调控审批配置对齐) CREATION_APPROVAL_TIMEOUT_MINUTES = 120 # 飞书 chat_id:复用现有调控审批群(FEISHU_OPERATOR_CHAT_ID) # 等创意审批和调控审批要分群时,再加 FEISHU_CREATION_CHAT_ID 覆盖 # ═══════════════════════════════════════════ # 阿里云 SLS 日志上报(2026-06-11 接入,K8s pod 中 SDK 直发) # ═══════════════════════════════════════════ # 从 os.environ 读取(.env 已 load_dotenv),任一缺失 → SLS_ENABLED=False → 不上报,只本地 file SLS_ENDPOINT = os.environ.get("SLS_ENDPOINT", "") # 例 cn-hangzhou.log.aliyuncs.com SLS_ACCESS_KEY_ID = os.environ.get("SLS_ACCESS_KEY_ID", "") # RAM 子账号 AK(只授 Log:PutLogs 权限) SLS_ACCESS_KEY_SECRET = os.environ.get("SLS_ACCESS_KEY_SECRET", "") # 同上的 SK SLS_PROJECT = os.environ.get("SLS_PROJECT", "auto-put-tecent") SLS_LOGSTORE = os.environ.get("SLS_LOGSTORE", "info-log") # 全开:任一缺失则降级为 False,主链路不受影响 SLS_ENABLED = bool(SLS_ENDPOINT and SLS_ACCESS_KEY_ID and SLS_ACCESS_KEY_SECRET and SLS_PROJECT and SLS_LOGSTORE) # 上报等级 — 用户决策(2026-06-11):所有 INFO+ 上报 # 注:material_recall 每个 landing 打 4 条 INFO,日 cron 量级约 5k-20k 条,SLS 流量成本几 RMB/月 SLS_LOG_LEVEL = "INFO" # QueuedLogHandler 内部异步队列参数(SDK 默认 + 微调,避免长连接 idle 断) SLS_BATCH_SIZE_MAX = 1024 # 单次 PutLogs 最多条数 SLS_PUT_WAIT_MS = 2000 # 队列攒到 batch_size 或等 2s flush 一次