""" 养号调度器 — 新号冷启动期自动限制操作量,渐进提升 功能: 1. 新号冷启动期 (前 7 天) 自动限流 2. 每日操作量递增曲线 3. 模拟日常行为 (刷朋友圈、看文章、回消息) 4. 活跃时段分布 (早 8-10, 午 12-14, 晚 19-22) 5. 与 RiskSentinel 联动调整阈值 """ import json import logging import os import random import time from datetime import datetime, timedelta from typing import Dict, List, Optional, Tuple logger = logging.getLogger(__name__) class NurtureScheduler: """养号调度器 — 安全地养熟一个账号""" COLD_START_DAYS = 7 DAILY_LIMITS_CURVE = { 1: {"send_message": 5, "add_friend": 2, "moment_like": 5, "moment_post": 0}, 2: {"send_message": 10, "add_friend": 3, "moment_like": 8, "moment_post": 1}, 3: {"send_message": 18, "add_friend": 5, "moment_like": 12, "moment_post": 1}, 4: {"send_message": 25, "add_friend": 8, "moment_like": 18, "moment_post": 2}, 5: {"send_message": 35, "add_friend": 12, "moment_like": 22, "moment_post": 2}, 6: {"send_message": 45, "add_friend": 15, "moment_like": 28, "moment_post": 3}, 7: {"send_message": 50, "add_friend": 18, "moment_like": 30, "moment_post": 3}, } MATURE_LIMITS = { "send_message": 60, "add_friend": 20, "moment_like": 30, "moment_post": 5, "group_send": 10, "profile_view": 40, } ACTIVE_HOURS: List[Tuple[int, int, float]] = [ (7, 9, 0.6), (9, 12, 0.8), (12, 14, 1.0), (14, 17, 0.7), (17, 19, 0.5), (19, 22, 1.0), (22, 24, 0.4), ] DAILY_BEHAVIORS = [ {"action": "browse_moments", "weight": 3, "duration_min": (2, 8)}, {"action": "read_article", "weight": 2, "duration_min": (1, 5)}, {"action": "check_messages", "weight": 4, "duration_min": (1, 3)}, {"action": "browse_discover", "weight": 1, "duration_min": (2, 6)}, ] def __init__(self, state_file: str = "nurture_state.json"): self.state_file = state_file self._state = self._load_state() def register_account(self, account_id: str): """注册一个新的养号账号""" if account_id not in self._state: self._state[account_id] = { "start_date": datetime.now().isoformat(), "day_counters": {}, "total_ops": {}, } self._save_state() logger.info(f"注册养号: {account_id}") def get_account_day(self, account_id: str) -> int: """获取账号处于第几天""" info = self._state.get(account_id) if not info: return 999 # 未注册视为成熟号 start = datetime.fromisoformat(info["start_date"]) delta = (datetime.now() - start).days + 1 return delta def is_cold_start(self, account_id: str) -> bool: """是否在冷启动期""" return self.get_account_day(account_id) <= self.COLD_START_DAYS def get_daily_limit(self, account_id: str, action: str) -> int: """获取该账号今日某操作的上限""" day = self.get_account_day(account_id) if day > self.COLD_START_DAYS: return self.MATURE_LIMITS.get(action, 100) day_limits = self.DAILY_LIMITS_CURVE.get(day, self.DAILY_LIMITS_CURVE[7]) return day_limits.get(action, self.MATURE_LIMITS.get(action, 100)) def can_operate(self, account_id: str, action: str) -> dict: """ 检查账号当前是否允许执行某操作 Returns: {"allowed": bool, "reason": str, "day": int, "limit": int, "used": int} """ day = self.get_account_day(account_id) limit = self.get_daily_limit(account_id, action) today_key = datetime.now().strftime("%Y-%m-%d") info = self._state.get(account_id, {}) counters = info.get("day_counters", {}).get(today_key, {}) used = counters.get(action, 0) if not self._is_active_hour(): return { "allowed": False, "reason": "当前不在活跃时段", "day": day, "limit": limit, "used": used, } if used >= limit: return { "allowed": False, "reason": f"今日已达上限 ({used}/{limit})", "day": day, "limit": limit, "used": used, } return { "allowed": True, "reason": "", "day": day, "limit": limit, "used": used, } def record_operation(self, account_id: str, action: str): """记录一次操作""" if account_id not in self._state: self.register_account(account_id) today_key = datetime.now().strftime("%Y-%m-%d") info = self._state[account_id] if "day_counters" not in info: info["day_counters"] = {} if today_key not in info["day_counters"]: info["day_counters"][today_key] = {} counters = info["day_counters"][today_key] counters[action] = counters.get(action, 0) + 1 if "total_ops" not in info: info["total_ops"] = {} info["total_ops"][action] = info["total_ops"].get(action, 0) + 1 self._save_state() def get_nurture_plan(self, account_id: str) -> List[dict]: """ 生成今日养号行为计划 — 穿插在正式操作之间执行 """ plan = [] hour = datetime.now().hour activity_weight = self._get_hour_weight(hour) behavior_count = max(1, int(random.uniform(2, 5) * activity_weight)) selected = random.choices( self.DAILY_BEHAVIORS, weights=[b["weight"] for b in self.DAILY_BEHAVIORS], k=behavior_count, ) for behavior in selected: dur_min = random.uniform(*behavior["duration_min"]) delay_min = random.uniform(5, 30) plan.append({ "action": behavior["action"], "duration_sec": int(dur_min * 60), "delay_before_sec": int(delay_min * 60), "hour_weight": activity_weight, }) return plan def get_risk_sentinel_overrides(self, account_id: str) -> Dict[str, Tuple[int, int]]: """ 返回适合该账号当前阶段的 RiskSentinel 阈值覆盖 用法: sentinel = RiskSentinel(custom_limits=scheduler.get_risk_sentinel_overrides(acct)) """ overrides = {} for action in self.MATURE_LIMITS: limit = self.get_daily_limit(account_id, action) overrides[action] = (3600, limit) return overrides def get_stats(self, account_id: str) -> dict: """获取养号统计""" info = self._state.get(account_id) if not info: return {"registered": False} day = self.get_account_day(account_id) today_key = datetime.now().strftime("%Y-%m-%d") counters = info.get("day_counters", {}).get(today_key, {}) return { "registered": True, "account_id": account_id, "day": day, "cold_start": day <= self.COLD_START_DAYS, "today_counters": counters, "total_ops": info.get("total_ops", {}), "start_date": info["start_date"], } # ---- 内部方法 ---- def _is_active_hour(self) -> bool: """当前是否在活跃时段""" hour = datetime.now().hour for start, end, weight in self.ACTIVE_HOURS: if start <= hour < end and weight >= 0.3: return True return False @staticmethod def _get_hour_weight(hour: int) -> float: """获取当前小时的活跃权重""" for start, end, weight in NurtureScheduler.ACTIVE_HOURS: if start <= hour < end: return weight return 0.1 def _load_state(self) -> dict: if os.path.exists(self.state_file): try: with open(self.state_file, "r") as f: return json.load(f) except Exception as e: logger.warning(f"加载养号状态失败: {e}") return {} def _save_state(self): try: with open(self.state_file, "w") as f: json.dump(self._state, f, ensure_ascii=False, indent=2) except Exception as e: logger.warning(f"保存养号状态失败: {e}")