#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
壹号娱乐 整合版: 登录 + 查余额提现 + 终身VIP月俸禄红包自动领取 + 计划调度
=========================================================
功能流程:
  每次循环: 登录 -> 按 qiandao/yihao.txt 计划领取月俸禄红包(每月6号07:00~9号23:59窗口)
            -> 查询余额 -> 若余额 > 100 -> 自动提现(余额取整)
            -> 下次运行 = min(红包计划时间(重启不变), 随机3-10天)

用法:
  python yihao_auto.py                 # 默认: 定时循环(按 qiandao/yihao.txt 计划时间领取)
  python yihao_auto.py claim           # 手动领取一次(成功则排下月计划)
  python yihao_auto.py --once          # 旧模式单次: 登录+查余额+自动提现
  python yihao_auto.py --check         # 旧模式干跑一次: 登录+查余额, 不提现
  python yihao_auto.py login           # 仅登录
  python yihao_auto.py balance         # 仅查余额
  python yihao_auto.py withdraw        # 手动提现(全部余额取整, 需 >100)
  python yihao_auto.py set-token <token>   # 手动设置token
  python yihao_auto.py show-token      # 显示当前token/memberId
"""

import hashlib
import base64
import time
import random
import json
import re
import sys
import os
import datetime

# ============ 测试模式: True 时所有随机延时压缩到 6-10 秒 (测试通过后改回 False) ============
TEST_MODE = False

def _sleep(lo, hi):
    if TEST_MODE:
        time.sleep(random.randint(6, 10))
    else:
        time.sleep(random.randint(lo, hi))

try:
    import requests
except ImportError:
    import subprocess
    subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "-q"])
    import requests

try:
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad, unpad
except ImportError:
    import subprocess
    subprocess.check_call([sys.executable, "-m", "pip", "install", "pycryptodome", "-q"])
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad, unpad

requests.packages.urllib3.disable_warnings()

# ================= 配置 =================
USERNAME = "fyygggg33"            # 登录账号
PASSWORD = "qqfyygggg33"           # 登录密码
MEMBER_ID = "196112"              # 会员ID 写死, 不再使用 member_id.txt
FUND_PASSWORD = "496789"          # 提现资金密码
THRESHOLD = 209.0                 # 余额大于此值才提现
MIN_DAYS = 2                # 循环最小间隔天数
MAX_DAYS = 5                  # 循环最大间隔天数

# 加拿大28 下注(打流水)参数: gameId/gameRoomId 对应 gamezhuotou 页面当前房间(1分钟一期)
GAME_ID = 140
GAME_ROOM_ID = 2
BET_ITEMS = ["大", "小", "单", "双", "大单", "大双", "小单", "小双"]   # 基础投注项(两注互斥用)

# 域名配置(多域名轮换容错, 按顺序尝试)
COOKIE = "__snaker__id=VXEqfRPT1xYls6UG; gdxidpyhxdE=sExucvS9ESt6qN737r%2B01MnNHLd%2FsYiPj%2FA76e2Het1Ge0uw3f5IBJSadL%2BwUmV3Jm%5CSwWTlMOTeyvgM5Mnmo7qbBa46BSMOBBUQyzOzbDIabyOafJ9gI25%2FbvJHcIAk904mtVE%2FkZEEuAX7OaPWcdcDTVHKlTWbrvD3JE%5C5YQaxujJV%3A1787953449674"

# 动态线路统一来源(主站H5, 登录页真实Origin)
DEFAULT_ORIGIN = "https://149.30.165.129:52051"

# 硬编码兜底域名(2026-10-06 实测有效; 每次启动会先尝试远程刷新为最新线路, 见 refresh_domains)
API_TARGETS = [
    {"base": "https://ewqcg5apixl.ojyftmg.com", "origin": DEFAULT_ORIGIN, "cookie": COOKIE},
    {"base": "https://vgacg5apixl.omktcw.com", "origin": DEFAULT_ORIGIN, "cookie": COOKIE},
    {"base": "https://vwfcg5apixl.oxuquc.com", "origin": DEFAULT_ORIGIN, "cookie": COOKIE},
    {"base": "https://wdfcg5apixl.oztjzi.com", "origin": DEFAULT_ORIGIN, "cookie": COOKIE},
    {"base": "https://rtfdcg5apixl.nwdcvq.com", "origin": DEFAULT_ORIGIN, "cookie": COOKIE},
    {"base": "https://adfacg5apixl.nwymlg.com", "origin": DEFAULT_ORIGIN, "cookie": COOKIE},
    {"base": "https://vvwrcg5apixl.ogjofid.com", "origin": DEFAULT_ORIGIN, "cookie": COOKIE},
]

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(SCRIPT_DIR, "config.txt")
# 统一配置文件 config.txt (每行 key=value):
#   token=977c13d6...        登录token(页面prevtoken)
#   secret_idx=7             登录/查询用 secret 缓存索引
#   bet_secret_idx=7         下注(cmdBet)专用 secret 缓存索引
# 旧文件 user_token.txt / secret_index.txt / bet_secret_idx.txt 已合并进本文件

def _load_config():
    cfg = {}
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, "r", encoding="utf-8-sig") as f:
            for line in f:
                line = line.strip()
                if line and "=" in line:
                    k, _, v = line.partition("=")
                    cfg[k.strip()] = v.strip()
    return cfg

def _save_config(cfg):
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        for k, v in cfg.items():
            f.write(f"{k}={v}\n")

def _config_get(key):
    return _load_config().get(key)

def _config_set(key, value):
    cfg = _load_config()
    cfg[key] = str(value)
    _save_config(cfg)

REAL_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"

# 全局会话与当前生效域名
session = requests.Session()
active = API_TARGETS[0]

def _setup_session():
    """按当前域名应用/清除Cookie"""
    if active.get("cookie"):
        session.headers["Cookie"] = active["cookie"]
    else:
        session.headers.pop("Cookie", None)

_setup_session()


# ================= 动态域名池(壹号线路永不失效) =================
# 线路来源: 主JS(app.cf12d1c8.js)内硬编码的 app_config.txt 镜像入口
#   HTTP GET 得到 JSONP 壳: getJSONP._JSONP({"config":"<base64密文>"})
#   -> AES-256-CBC 解密(密钥原文 APP_CONFIG_KEY 做SHA256, IV=16字节全零, PKCS7)
#   -> 明文JSON: {"root":{"result":{"line":"地址1,地址2,..."}}}
#   -> root.result.line 逗号分隔 = 7个API地址
# 策略: 每次启动远程刷新(10个入口轮换) -> 失败用本地缓存 config/yihao_domains.json 兜底
#       -> 缓存也没有才用上面硬编码兜底
APP_CONFIG_KEY = "57feaa48112bc0e7"
CFG_ENTRIES = [
    "https://sd5hrk.rx8quaj6.com/public/site_800_oss/static/config/app_config.txt",
    "https://sd5hrk.uc73dwbs.com/public/site_800_oss/static/config/app_config.txt",
    "https://ws5hazrk.jnyzzyczs.com/cgstorage/public/site_800_oss/app_config.txt",
    "https://ws5hazrk.tawanqing.com/cgstorage/public/site_800_oss/app_config.txt",
    "https://ws5hrks3.bdlanghui.com/public/site_800_oss/static/config/app_config.txt",
    "https://ws5hrks3.hnczccpit.com/public/site_800_oss/static/config/app_config.txt",
    "https://ws5htrkaz.uuyqel.com/cgstorage/public/site_800_oss/app_config.txt",
    "https://ws5htrkaz.vqpjif.com/cgstorage/public/site_800_oss/app_config.txt",
    "https://wsyhrk.cosmowiki.com/public/site_800_oss/static/config/app_config.txt",
    "https://wsyhrk.mcglzx.com/public/site_800_oss/static/config/app_config.txt",
]
YH_CONFIG_DIR = os.path.join(SCRIPT_DIR, "config")
YH_DOMAINS_FILE = os.path.join(YH_CONFIG_DIR, "yihao_domains.json")


def _decrypt_app_config(cfg_b64):
    """AES-256-CBC 解密 config 密文, 返回明文JSON对象"""
    raw = base64.b64decode(cfg_b64)
    key = hashlib.sha256(APP_CONFIG_KEY.encode()).digest()
    plain = unpad(AES.new(key, AES.MODE_CBC, b"\x00" * 16).decrypt(raw), 16)
    return json.loads(plain.decode("utf-8"))


def _extract_app_config(text):
    """从 JSONP 壳提取 config 密文"""
    m = re.search(r'"config"\s*:\s*"([^"]+)"', text)
    if not m:
        return None
    return m.group(1)


def fetch_remote_lines():
    """依次尝试 app_config 入口, 返回7个API地址列表(去重, 保序)"""
    ua = {"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15"}
    seen = []
    for u in CFG_ENTRIES:
        try:
            resp = session.get(u, headers=ua, timeout=10, verify=False)
            if resp.status_code != 200 or len(resp.text) < 50:
                continue
            cfg = _extract_app_config(resp.text)
            if not cfg:
                continue
            obj = _decrypt_app_config(cfg)
            line = (obj.get("root") or {}).get("result") or {}
            line = line.get("line", "")
            if not line:
                continue
            for a in [x.strip() for x in line.split(",") if x.strip()]:
                if a not in seen:
                    seen.append(a)
            if seen:
                print(f"[动态域名] 远程抓取成功: {u}")
                return seen
        except Exception:
            continue
    return []


def _save_domains_cache(apis):
    try:
        os.makedirs(YH_CONFIG_DIR, exist_ok=True)
        data = {
            "main_api": apis,
            "cfg_entries": CFG_ENTRIES,
            "updated": time.strftime("%Y-%m-%d %H:%M:%S"),
        }
        with open(YH_DOMAINS_FILE, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=1)
    except Exception as e:
        print(f"  [动态域名] 缓存写入失败: {e}")


def _load_domains_cache():
    try:
        if os.path.exists(YH_DOMAINS_FILE):
            with open(YH_DOMAINS_FILE, "r", encoding="utf-8") as f:
                data = json.load(f)
            apis = data.get("main_api") or []
            if apis:
                return apis
    except Exception:
        pass
    return []


def refresh_domains():
    """每次启动调用: 远程刷新线路 -> 重建 API_TARGETS; 失败用缓存兜底"""
    global API_TARGETS
    print("[动态域名] 刷新壹号线路池...")
    apis = fetch_remote_lines()
    if apis:
        _save_domains_cache(apis)
    else:
        print("[动态域名] 远程抓取失败, 使用本地缓存兜底")
        apis = _load_domains_cache()
        if not apis:
            print("[动态域名] 缓存为空, 保持硬编码兜底域名")
            return
    API_TARGETS = [{"base": b, "origin": DEFAULT_ORIGIN, "cookie": COOKIE} for b in apis]
    print(f"[动态域名] 当前 {len(API_TARGETS)} 个线路:")
    for i, t in enumerate(API_TARGETS, 1):
        print(f"  {i}. {t['base']}")

KEY_MOVE = 6
DECRYPT_SECRET = [
    "ReZBZ8wWDyVXB", "nN#t#uYX!q?91", "4BI9xQl!A!$&f", "_zugBtOo-+9Ua",
    "Ol&vKRoBmVBz=", "#=~Pxo!0SZAmA", "=Icf!j&Q=Q5L%", "ngHvoBQ?0%x4k",
    "xXt3JZf=Q5z03", "?JQv@F=vhRuT4", "P*IK=7I*%HHPs", "@g=Lhgm+aJ~Hp",
    "Nu5B4kF_AZVF4", "#KzmDu1^EWDmr", "+3Sc!rbDjJpwX", "WGI-$$rxK=Irz",
    "@CiIrf_E~T+i8", "%VQz&G7W-@aM6", "OY2H#7Yx%~BO+", "NNpGnzVSZQAl8",
    "?UOrMKR5dQO4o", "qiKsgr?zdkPyP", "Y+9yA_2$Tzlwf", "_X^7WTAIqC#6f",
    "Xme#E8vIo=Fw2", "mQorl22PyMeM_", "xVX!vlYu&TjR0", "m1VE39P#XSanb",
    "oc*!76jdRyyQg", "M0G&MVShg9hkZ",
]
PWD_KEY = "3os9vVsjxUnAjerl"
FUND_KEY = "2fDlj(_35*6^2=Lk"  # 资金密码AES-CBC密钥原文(内部做SHA256)

# ================= 基础工具 =================
def md5(s):
    return hashlib.md5(s.encode()).hexdigest()


def load_token():
    return _config_get("token")


def save_token(token):
    _config_set("token", token)


def load_secret_idx():
    try:
        return int(_config_get("secret_idx"))
    except (TypeError, ValueError):
        return None


def save_secret_idx(idx):
    _config_set("secret_idx", idx)


def load_bet_secret_idx():
    try:
        return int(_config_get("bet_secret_idx"))
    except (TypeError, ValueError):
        return None


def save_bet_secret_idx(idx):
    _config_set("bet_secret_idx", idx)


# ================= 加密 / SECRET =================
def gen_token_with_secret(plaintext, secret):
    """用指定 SECRET 生成 timestamp 和 token"""
    r3 = str(random.randint(0, 999)).zfill(3)
    ts = str(int(time.time() * 1000))[:13] + r3
    md5in = f"{KEY_MOVE}-{r3}{secret}-{r3},{ts}+{KEY_MOVE}"
    md5h = md5(md5in)
    aes_key = md5h[KEY_MOVE:KEY_MOVE + 16]
    cipher = AES.new(aes_key.encode(), AES.MODE_ECB)
    enc = cipher.encrypt(pad(str(plaintext).encode(), AES.block_size))
    return ts, base64.b64encode(enc).decode()


def _test_secret_idx(idx, plaintext=None):
    """测试某SECRET索引是否正确 (200/401表示正确)"""
    if plaintext is None:
        plaintext = load_token() or "0"
    ts, token = gen_token_with_secret(plaintext, DECRYPT_SECRET[idx])
    headers = {
        "Content-Type": "application/json", "device": "h5",
        "timestamp": ts, "token": token,
        "User-Agent": "Mozilla/5.0", "Accept": "application/json",
        "Origin": active["origin"], "Referer": active["origin"] + "/",
        "customerUID": str(MEMBER_ID or ""),
    }
    try:
        resp = session.get(active["base"] + "/api/v1/member/fund/balance",
                           headers=headers, verify=False, timeout=10)
        result = resp.json()
        return result.get("code") in (200, 401)
    except:
        return False


def detect_secret(use_cache=True):
    cached = load_secret_idx()
    if use_cache and cached is not None and 0 <= cached < len(DECRYPT_SECRET):
        if _test_secret_idx(cached):
            return cached
    print("检测SECRET索引...", end=" ", flush=True)
    for idx in range(len(DECRYPT_SECRET)):
        if _test_secret_idx(idx):
            save_secret_idx(idx)
            print(f"索引{idx}")
            return idx
    print("未找到, 默认10")
    return 10


def build_headers(plaintext, with_uid=True, minimal=False):
    """生成请求头; minimal=True 为页面同款最小集(领取接口必需, 多带头会406)"""
    idx = detect_secret()
    secret = DECRYPT_SECRET[idx]
    ts, token = gen_token_with_secret(plaintext, secret)
    if minimal:
        h = {
            "Accept": "application/json, text/plain, */*",
            "device": "h5",
            "timestamp": ts,
            "token": token,
        }
        if with_uid:
            h["customerUID"] = str(MEMBER_ID or "")
        return h
    h = {
        "Content-Type": "application/json",
        "device": "h5",
        "timestamp": ts,
        "token": token,
        "User-Agent": REAL_UA,
        "Accept": "application/json, text/plain, */*",
        "Origin": active["origin"],
        "Referer": active["origin"] + "/",
    }
    if with_uid:
        h["customerUID"] = str(MEMBER_ID or "")
    return h


def enc_pwd(pwd):
    """登录密码加密 AES-ECB"""
    cipher = AES.new(PWD_KEY.encode(), AES.MODE_ECB)
    enc = cipher.encrypt(pad(pwd.encode(), AES.block_size))
    return base64.b64encode(enc).decode()


def encrypt_fund_password(pwd):
    """资金密码加密 AES-128-ECB (密钥同登录密码 PWD_KEY, PKCS7)

    修复说明(2026-09-11): 实测站点前端模块ed08导出h(即_0x3a5e48)实现为
    AES.encrypt(Utf8.parse(pwd), key=3os9vVsjxUnAjerl, {padding: Pkcs7}) (无IV=ECB)。
    旧实现误用 FUND_KEY+SHA256+AES-CBC+全零IV, 服务器解密失败返回 code=1024 交易密码参数错误。
    """
    cipher = AES.new(PWD_KEY.encode(), AES.MODE_ECB)
    enc = cipher.encrypt(pad(pwd.encode(), AES.block_size))
    return base64.b64encode(enc).decode()


# ================= 业务: 登录 =================
def do_login(username=USERNAME, password=PASSWORD):
    """登录并保存 token/memberId, 失败时自动切换下一个域名"""
    global active
    plaintext = "0"
    body = {
        "username": username,
        "password": enc_pwd(password),
        "code": "",
        "imei": "h5",
        "validate": ""
    }
    print("=" * 50)
    print("壹号娱乐 登录")
    print("=" * 50)
    print(f"账号: {username}")

    # 登录前随机延时3-8秒(模拟真人打开页面输入账号)
    _sleep(3, 8)

    for target in API_TARGETS:
        active = target
        _setup_session()
        headers = build_headers(plaintext, with_uid=False)
        try:
            resp = session.post(active["base"] + "/api/v1/member/login",
                                headers=headers, json=body, verify=False, timeout=15)
            print(f"[{active['base']}] 状态码: {resp.status_code}")
            result = resp.json()
            code = result.get("code")
            data = result.get("data")
            if code == 200 and data:
                token = data.get("token")
                if token:
                    save_token(token)
                    print("登录成功! token 已保存")
                member = data.get("member", {})
                print(f"会员: {member.get('nickname', 'N/A')} (ID:{MEMBER_ID})")
                return result
            elif code == 200010:
                print(f"""需要滑块验证, 请手动操作:
  1. 浏览器打开 {active['origin']}/login
  2. 登录后 F12 执行: localStorage.getItem('prevtoken')
  3. 复制 token, 运行: python yihao_auto.py set-token <token>
""")
                return result
            else:
                print(f"登录失败: code={code}, msg={result.get('msg','')}")
        except Exception as e:
            print(f"[{active['base']}] 请求异常: {e}")
    print("所有域名登录均失败")
    return None


# ================= 业务: 终身VIP月俸禄红包 =================
VIP_REDPACKET_WINDOW_DAY = 6       # 每月6号起可领
VIP_REDPACKET_WINDOW_HOUR = 7      # 6号早上7点后开放
VIP_REDPACKET_WINDOW_END_DAY = 9   # 9号23:59:59 截止
VIP_REDPACKET_WINDOW_DAYS = 4      # 6-9号窗口

CLAIM_PLAN_DIR = os.path.join(SCRIPT_DIR, "qiandao")
CLAIM_PLAN_FILE = os.path.join(CLAIM_PLAN_DIR, "yihao.txt")


def _month_window(year, month):
    """某月俸禄领取窗口: 该月6号07:00 ~ 该月9号23:59:59 (用户规则: 6号7点后到9号)"""
    ws = datetime.datetime(year, month, 6, 7, 0, 0)
    we = datetime.datetime(year, month, 9, 23, 59, 59)
    return ws, we


def next_vip_window(now=None, month_offset=0):
    """now 所在月 +offset 的俸禄窗口 (offset=0 本月, 1 下月, ...)"""
    now = now or datetime.datetime.now()
    y, m = now.year, now.month + month_offset
    while m > 12:
        y += 1
        m -= 12
    while m < 1:
        y -= 1
        m += 12
    return _month_window(y, m)


def in_vip_redpacket_window(now=None):
    """当前是否处于俸禄领取窗口(每月6号07:00~9号23:59:59)"""
    now = now or datetime.datetime.now()
    ws, we = _month_window(now.year, now.month)
    return ws <= now <= we


# ================= 红包签到计划(持久化 qiandao/yihao.txt, 重启不变) =================
def load_claim_plan():
    """读取下次签到计划时间; 无文件/损坏返回 None"""
    try:
        with open(CLAIM_PLAN_FILE, "r", encoding="utf-8") as f:
            for line in f:
                if line.startswith("next_claim="):
                    s = line.strip().split("=", 1)[1]
                    return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
    except Exception:
        pass
    return None


def save_claim_plan(dt):
    """写入下次签到计划时间(自动创建 qiandao 目录)"""
    try:
        os.makedirs(CLAIM_PLAN_DIR, exist_ok=True)
        content = (
            "# 壹号 红包签到计划 (每月6号07:00~9号23:59:59窗口内领取, 重启不变)\n"
            "# 手动修改下一行可自定义下次签到时间\n"
            f"next_claim={dt.strftime('%Y-%m-%d %H:%M:%S')}\n"
        )
        with open(CLAIM_PLAN_FILE, "w", encoding="utf-8") as f:
            f.write(content)
    except Exception as e:
        print(f"签到计划写入失败: {e}")


def gen_next_claim_plan(now=None):
    """生成下一个领取窗口内随机时刻并持久化, 返回该时刻"""
    now = now or datetime.datetime.now()
    # 优先排"最近未来的窗口": 若本月窗口还没开(now < 本月6号06:00), 排本月窗口; 否则排下月窗口
    ws0, we0 = next_vip_window(now, month_offset=0)
    if now < ws0:
        ws, we = ws0, we0
    else:
        ws, we = next_vip_window(now, month_offset=1)
    span = int((we - ws).total_seconds())
    plan = ws + datetime.timedelta(seconds=random.randint(0, span))
    save_claim_plan(plan)
    print(f"已生成红包签到计划: {plan.strftime('%Y-%m-%d %H:%M:%S')} "
          f"(窗口 {ws.strftime('%m月%d日')}~{we.strftime('%m月%d日')}, 重启不变)")
    return plan


def claim_window_end(plan):
    """计划所在月份的领取窗口结束时间(次月5号23:59:59)"""
    _, we = _month_window(plan.year, plan.month)
    return we


def plan_decision(plan, now):
    """签到决策 -> (do_claim, new_plan):
       do_claim=True  -> 调用方执行领取, 成功后调用方生成下月计划
       new_plan != None -> 计划已生成/重算(已写入文件)
    """
    if plan is None:
        if in_vip_redpacket_window(now):
            return True, None                       # 无计划但恰在窗口内 -> 立即领
        return False, gen_next_claim_plan(now)      # 无计划且不在窗口 -> 生成最近窗口计划
    if now >= plan and in_vip_redpacket_window(now):
        return True, None                           # 计划到期且在窗口内 -> 领
    if now > claim_window_end(plan):
        print(f"上次签到计划({plan.strftime('%Y-%m-%d %H:%M')})窗口已过(停机错过), 重排")
        return False, gen_next_claim_plan(now)      # 计划窗口已过 -> 重算
    return False, None                              # 未到期, 等待


def claim_vip_monthly_reward():
    """登录(必要时) -> 列表 -> 找待领取 -> POST receive
    返回: True=领取成功 / "claimed"=无可领(已领取, 本月已处理完) / None=异常失败"""
    print(f"\n{'=' * 50}")
    print("终身VIP 月俸禄红包")
    print(f"{'=' * 50}")
    if not in_vip_redpacket_window():
        print(f"当前不在领取窗口(每月6号07:00~9号23:59:59), 跳过")
        return None

    token = load_token()
    if not token:
        print("未找到token, 自动登录...")
        r = do_login()
        if not r or r.get("code") != 200:
            print("自动登录失败, 无法领取")
            return None
        token = load_token()

    # 登录后随机延时1-3分钟再操作(模拟真人)
    delay = random.randint(5, 20)
    print(f"登录后延时 {delay} 秒({delay/60:.1f}分钟)后查询红包...")
    time.sleep(delay)

    headers = build_headers(token, with_uid=True, minimal=True)
    list_url = active["base"] + "/api/v1/member/vip/redPacket/list"
    try:
        # 1. 列表
        resp = session.get(list_url, headers=headers,
                           params={"vipType": 2, "pageSize": 10, "pageNum": 1},
                           verify=False, timeout=15)
        result = resp.json()
        print(f"列表状态码: {resp.status_code}")
        if result.get("code") in (401, 403):
            print("token失效, 重新登录...")
            r = do_login()
            if not r or r.get("code") != 200:
                print("重新登录失败, 无法领取")
                return None
            token = load_token()
            headers = build_headers(token, with_uid=True, minimal=True)
            resp = session.get(list_url, headers=headers,
                               params={"vipType": 2, "pageSize": 10, "pageNum": 1},
                               verify=False, timeout=15)
            result = resp.json()
            print(f"重试列表状态码: {resp.status_code}")
        if result.get("code") != 200:
            print(f"获取红包列表失败: code={result.get('code')}, msg={result.get('msg')}")
            return None
        items = result.get("data", {}).get("list", [])
        print(f"红包列表 {len(items)} 条:")
        for it in items:
            print(f"  {it.get('date')} {it.get('receivedAmount')}元 status={it.get('status')}({it.get('statusName')}) welfareId={it.get('welfareId')}")
        target = next((it for it in items if it.get("status") == 0), None)
        if not target:
            print("无可领取红包(均已被领取或过期)")
            return "claimed"

        welfare_id = target.get("welfareId")
        print(f"待领取: {target.get('date')} {target.get('receivedAmount')}元 welfareId={welfare_id}")

        # 2. 领取前刷新 prevtoken(页面同款: 旧token签名会被服务器406「重复请求」拒绝)
        try:
            refresh_prevtoken()
            headers = build_headers(load_token(), with_uid=True, minimal=True)
        except Exception:
            pass
        # 模拟真人: 点击前延时10-20秒
        _sleep(10, 20)
        body = {"welfareId": welfare_id}
        resp = session.post(active["base"] + "/api/v1/member/vip/receive",
                            headers=headers, json=body, verify=False, timeout=15)
        print(f"领取状态码: {resp.status_code}")
        result2 = resp.json()
        if result2.get("code") == 406:
            # 签名仍过期 -> 再刷新一次并重试
            print("领取406(签名过期), 刷新token后重试...")
            try:
                refresh_prevtoken()
                headers = build_headers(load_token(), with_uid=True, minimal=True)
            except Exception:
                pass
            _sleep(5, 10)
            resp = session.post(active["base"] + "/api/v1/member/vip/receive",
                                headers=headers, json=body, verify=False, timeout=15)
            result2 = resp.json()
        if result2.get("code") == 200:
            print(f"红包领取成功! {target.get('date')} {target.get('receivedAmount')}元 | msg: {result2.get('msg')}")
            return True
        else:
            print(f"领取失败: code={result2.get('code')}, msg={result2.get('msg')}")
            return None
    except Exception as e:
        print(f"领取红包异常: {e}")
        return None


# ================= 业务: 余额 =================
def get_balance():
    token = load_token()
    if not token:
        print("错误: 未找到token, 请先登录或 set-token")
        return None
    print("=" * 50)
    print("查询余额")
    print("=" * 50)
    headers = build_headers(token, with_uid=True)
    url = active["base"] + "/api/v1/member/fund/balance"
    try:
        resp = session.get(url, headers=headers, verify=False, timeout=15)
        result = resp.json()
        print(f"状态码: {resp.status_code}")
        if result.get("code") == 200:
            balance = result["data"].get("availableBalance", "0")
            print(f"可用余额: {balance}")
            return float(balance)
        else:
            print(f"错误: code={result.get('code')}, msg={result.get('msg')}")
            return None
    except Exception as e:
        print(f"请求异常: {e}")
        return None


# ================= 业务: 提现 =================
def get_withdraw_account():
    """获取已添加的提现账户列表(扁平化)

    接口结构(2026-10-04 实测改版):
      顶层 = 取款类型数组(7种: CGpay/EGpay/USDT/支付宝/数字人民币/银行卡/微信)
      类型.list = 协议列表(如 USDT: [{id:'1',name:'ERC20'},{id:'3',name:'TRC20'}])
      协议.list  = 该协议下已添加的账户数组(真正含 accountId 的账户)
    旧版(9/11)为扁平账户数组(直接 data.list = 账户); 本函数兼容两种,
    统一返回扁平账户列表(每个元素含 accountId/typeName/agreementName/accountNum)。
    修复原因(2026-10-04): 原解析把类型数组当账户数组, accountId/协议/账号全为 None,
    提现提交缺 accountId, 服务器返回 code=1000 请选择提现账号。
    """
    token = load_token()
    headers = build_headers(token, with_uid=True)
    url = active["base"] + "/api/v1/member/withdraw/account/index"
    try:
        resp = session.get(url, headers=headers, verify=False, timeout=15)
        result = resp.json()
        if result.get("code") == 200:
            data = result.get("data", {})
            if isinstance(data, dict):
                types = data.get("list", [])
            else:
                types = data if isinstance(data, list) else []
            accounts = []
            for t in types:
                if not isinstance(t, dict):
                    continue
                tname = t.get("typeName", "")
                # 旧结构: 顶层元素即账户
                if t.get("accountId") is not None:
                    accounts.append(t)
                    continue
                # 新结构: 类型 -> 协议 -> 账户
                for proto in t.get("list", []) or []:
                    if not isinstance(proto, dict):
                        continue
                    pname = proto.get("name", "")
                    for acc in proto.get("list", []) or []:
                        if isinstance(acc, dict) and acc.get("accountId") is not None:
                            if not acc.get("typeName"):
                                acc["typeName"] = tname
                            if not acc.get("agreementName") and pname:
                                acc["agreementName"] = pname
                            accounts.append(acc)
            print(f"\n提现账户数量: {len(accounts)}")
            for acc in accounts:
                print(f"  accountId:{acc.get('accountId')} 类型:{acc.get('typeName','')} 协议:{acc.get('agreementName','')} 账号:{acc.get('accountNum','')}")
            # 列出所有 USDT/TRC20 地址(用户要求, 便于核对随机池)
            usdt_trc20 = [a for a in accounts
                          if str(a.get("typeName", "")).upper() == "USDT"
                          and str(a.get("agreementName", "")).upper() == "TRC20"]
            if usdt_trc20:
                print(f"USDT/TRC20 地址数: {len(usdt_trc20)}, 提现将从中随机选择一个")
            return accounts
        else:
            print(f"获取提现账户失败: {result.get('msg')}")
            return []
    except Exception as e:
        print(f"获取提现账户异常: {e}")
        return []


# ================= 打流水(下注) =================
# cmdBet 专用 token 体系(2026-09-07 逆向确认):
#   明文 = prevtoken 的逐字符hex 解码后的 32字节字节流(不是64字符字符串!)
#   key  = md5("6-r3-secret-r3,ts+6")[6:22]  (16字符ASCII)
#   加密 = AES-128-ECB + Pkcs7 (32字节->48字节)
#   secret 索引 = 当前小时动态轮换, 必须用 issue 接口探测(0-16都406, 正确索引才200)
# 下注secret索引已并入 config.txt (bet_secret_idx 键, 见 load_bet_secret_idx/save_bet_secret_idx)
# 下注专用线路: 页面实际使用的节点 (yhwsapi2 等线路期号与页面不同步, 会误判下注窗口导致406)
BET_HOSTS = [              # 下注专用线路池(页面实际节点, 可扩展, 不固定)
    "https://yhwsapi1.wdyynk.com",
]
BET_HOST = random.choice(BET_HOSTS)
_last_bet_failed = False   # 打流水失败标记(仅提示, 不影响3-10天调度)

try:
    from curl_cffi import requests as creq          # cmdBet 必须模拟浏览器TLS指纹, 否则服务器406
    _CFFI = True
except Exception:
    creq = None
    _CFFI = False
    print("[警告] 未安装 curl_cffi, 下注会被服务器拒绝(406). 请执行: pip install curl_cffi")


def _bet_token(secret):
    """cmdBet 专用 token: hex明文解码字节流 + AES-128-ECB+Pkcs7"""
    tok = load_token() or ""
    hex_plain = "".join(f"{ord(c):02x}" for c in str(tok))
    plain_bytes = bytes.fromhex(hex_plain)          # 32字节
    r3 = str(random.randint(0, 999)).zfill(3)
    ts = str(int(time.time() * 1000))[:13] + r3
    md5in = f"{KEY_MOVE}-{r3}{secret}-{r3},{ts}+{KEY_MOVE}"
    aes_key = md5(md5in)[KEY_MOVE:KEY_MOVE + 16]
    cipher = AES.new(aes_key.encode(), AES.MODE_ECB)
    enc = cipher.encrypt(pad(plain_bytes, AES.block_size))
    return ts, base64.b64encode(enc).decode()


def _bet_issue_headers(secret):
    """用指定secret生成 issue/cmdBET 最小请求头(页面同款)"""
    ts, token = _bet_token(secret)
    return {
        "Accept": "application/json, text/plain, */*",
        "Content-Type": "application/json",
        "device": "h5",
        "timestamp": ts,
        "token": token,
        "customerUID": str(MEMBER_ID or ""),
    }


def _probe_bet_secret():
    """用 issue 接口探测当前小时有效的 secret 索引(每小时轮换), 缓存到文件"""
    cached = load_bet_secret_idx()
    if cached is not None and 0 <= cached < len(DECRYPT_SECRET):
        try:
            r = session.get(BET_HOST + "/api/v1/lottery/sale/issue",
                            headers=_bet_issue_headers(DECRYPT_SECRET[cached]),
                            params={"gameId": GAME_ID, "gameRoomId": GAME_ROOM_ID, "count": 1, "isFirst": 0, "activeId": ""},
                            verify=False, timeout=10)
            if r.json().get("code") in (200, 401):
                return cached
        except Exception:
            pass
    print("探测下注SECRET索引...", end=" ", flush=True)
    for idx in range(len(DECRYPT_SECRET)):
        try:
            r = session.get(BET_HOST + "/api/v1/lottery/sale/issue",
                            headers=_bet_issue_headers(DECRYPT_SECRET[idx]),
                            params={"gameId": GAME_ID, "gameRoomId": GAME_ROOM_ID, "count": 1, "isFirst": 0, "activeId": ""},
                            verify=False, timeout=10)
            if r.json().get("code") in (200, 401):
                try:
                    save_bet_secret_idx(idx)
                except Exception:
                    pass
                print(f"索引{idx}")
                return idx
        except Exception:
            continue
    print("未找到, 兜底17")
    return 17


def _bet_headers():
    """下注请求头: 自动探测有效secret"""
    return _bet_issue_headers(DECRYPT_SECRET[_probe_bet_secret()])

def refresh_prevtoken():
    """下注前刷新 prevtoken (页面 getNewToken 同款: GET yhwsapi1/api/v1/token + 标准headers)
    页面会刷新 prevtoken, 脚本用旧值加密下注会被服务器 406「重复请求」拒绝,
    所以下注前必须同步最新 token。"""
    hosts = list(BET_HOSTS)
    for h in [t.get("base") for t in API_TARGETS]:
        if h and h not in hosts:
            hosts.append(h)
    for host in hosts:
        try:
            r = session.get(host + "/api/v1/token", headers=_bet_headers(), verify=False, timeout=15)
            j = r.json()
            if j.get("code") == 200:
                newtok = (j.get("data") or {}).get("token")
                if newtok and len(newtok) >= 16:
                    cur = load_token()
                    if newtok != cur:
                        save_token(newtok)
                        print(f"  [token刷新] prevtoken 已更新: {newtok[:8]}...")
                    else:
                        print("  [token刷新] prevtoken 仍有效")
                    return True
        except Exception:
            continue
    # 全部线路刷新失败: token 可能已彻底失效(页面抢走/过期) -> 自动重新登录拿新token
    print("  [token刷新] 接口失败, token 可能已彻底失效, 尝试自动重新登录...")
    try:
        do_login()
        for host in hosts:
            try:
                r = session.get(host + "/api/v1/token", headers=_bet_headers(), verify=False, timeout=15)
                j = r.json()
                if j.get("code") == 200:
                    newtok = (j.get("data") or {}).get("token")
                    if newtok and len(newtok) >= 16:
                        save_token(newtok)
                        print(f"  [token刷新] 重新登录后已更新: {newtok[:8]}...")
                        return True
            except Exception:
                continue
    except Exception as e:
        print(f"  [token刷新] 自动登录异常: {e}")
    print("  [token刷新] 失败, 沿用当前 token")
    return False


def join_btc28_room():
    """下注前进房间: 页面进房间同款 GET lottery/index (不进房间, 服务器判不在房间, cmdBet 报 1030被踢出房间)"""
    try:
        hdr = build_headers(load_token(), with_uid=True)
        r = session.get(
            BET_HOST + "/api/v1/lottery/index",
            headers=hdr,
            params={"gameId": GAME_ID, "gameRoomId": GAME_ROOM_ID, "activeId": ""},
            verify=False, timeout=15,
        )
        j = r.json()
        code = j.get("code")
        print(f"  [进房间] lottery/index: code={code} msg={j.get('msg')}")
        return code == 200
    except Exception as e:
        print(f"  [进房间] 异常: {e}")
        return False


def parse_need_flow(msg):
    """从预检msg解析所需流水, 如 '总流水未达标，还需打满80流水' -> 80.0 (80不是固定的, 以接口为准)"""
    import re
    m = re.search(r"还需打满\s*(\d+(?:\.\d+)?)\s*流水", msg or "")
    return float(m.group(1)) if m else None


def _opposite_item(first):
    """根据第一注生成与它完全不重叠的第二注: 大小反向 + 单双反向
    例: 大 -> 小单/小双; 小双 -> 大单; 单 -> 大双/小双 (组合里不再出现第一注的投注字)"""
    chars = set(first)
    if "大" in chars:
        size = "小"
    elif "小" in chars:
        size = "大"
    else:
        size = random.choice(["大", "小"])          # 第一注无大小 -> 随机大小
    if "单" in chars:
        od = "双"
    elif "双" in chars:
        od = "单"
    else:
        od = random.choice(["单", "双"])            # 第一注无单双 -> 随机单双
    return size + od


def _bet_secret_candidates():
    """返回候选secret索引: 缓存优先, 再取issue接口200的全部(最多8个)
    issue对旧secret也宽松返回200, 但cmdBet严格按当前secret校验;
    所以把issue 200的全部列入候选, 下注406时逐个换试, 直到命中当前secret"""
    idxs = []
    try:
        c = load_bet_secret_idx()
        if c is not None:
            idxs.append(c)
    except Exception:
        pass
    for idx in range(len(DECRYPT_SECRET)):
        if idx in idxs:
            continue
        try:
            r = session.get(BET_HOST + "/api/v1/lottery/sale/issue",
                            headers=_bet_issue_headers(DECRYPT_SECRET[idx]),
                            params={"gameId": GAME_ID, "gameRoomId": GAME_ROOM_ID, "count": 1, "isFirst": 0, "activeId": ""},
                            verify=False, timeout=8)
            if r.json().get("code") == 200:
                idxs.append(idx)
                if len(idxs) >= 8:
                    break
        except Exception:
            continue
    if not idxs:
        idxs = [17]
    print(f"  下注secret候选: {idxs}")
    return idxs


def _send_cmdbet(content):
    """发送一次cmdBet: 候选secret逐个试(最多5个), 命中即缓存索引
    406可能是secret轮换(换下一个候选)也可能是账户风控/冷却(全试完仍406)"""
    last_rr = None
    tried = 0
    for idx in _bet_secret_candidates():
        if tried >= 5:
            break
        tried += 1
        headers = _bet_issue_headers(DECRYPT_SECRET[idx])
        body = {"betMode": "4", "content": content, "gameId": GAME_ID,
                "gameRoomId": GAME_ROOM_ID, "type": 1, "activeId": ""}
        try:
            if _CFFI:
                resp = creq.post(BET_HOST + "/api/v1/bet/order/cmdBet",
                                 headers=headers, json=body, verify=False, impersonate="chrome", timeout=15)
            else:
                resp = session.post(BET_HOST + "/api/v1/bet/order/cmdBet",
                                    headers=headers, json=body, verify=False, timeout=15)
            rr = resp.json()
        except Exception as e:
            print(f"  候选secret[{idx}] 请求异常: {e}")
            continue
        last_rr = rr
        code = rr.get("code")
        msg = (rr.get("data") or {}).get("msg") or rr.get("msg") or ""
        print(f"  secret[{idx}]下注: code={code} {str(msg)[:100]}")
        if code == 200:
            try:
                save_bet_secret_idx(idx)
            except Exception:
                pass
            return rr, True
        if code == 401:
            print(f"  [token过期] secret[{idx}]命中但token过期, 自动重新登录重试...")
            lr = do_login()
            if lr and lr.get("code") == 200:
                try:
                    if _CFFI:
                        resp = creq.post(BET_HOST + "/api/v1/bet/order/cmdBet",
                                         headers=_bet_issue_headers(DECRYPT_SECRET[idx]),
                                         json=body, verify=False, impersonate="chrome", timeout=15)
                    else:
                        resp = session.post(BET_HOST + "/api/v1/bet/order/cmdBet",
                                            headers=_bet_issue_headers(DECRYPT_SECRET[idx]),
                                            json=body, verify=False, timeout=15)
                    rr = resp.json()
                except Exception as e:
                    print(f"  登录后重试异常: {e}")
                    continue
                code2 = rr.get("code")
                print(f"  登录后重试: code={code2} {str((rr.get('data') or {}).get('msg') or rr.get('msg') or '')[:80]}")
                if code2 == 200:
                    try:
                        save_bet_secret_idx(idx)
                    except Exception:
                        pass
                    return rr, True
            else:
                print("  登录失败(可能滑块), 跳过该secret, 请按提示手动处理")
            continue
    if last_rr is not None and last_rr.get("code") == 406:
        print("  所有候选secret均406: 疑似账户下注风控/冷却中(非secret问题), 建议30-60分钟后重试")
    return last_rr or {}, False


def _issue_open(headers):
    """查询当前期下注状态: process=1 且 stopTime>35(留足余量, 服务器对临界/已封下注回406)"""
    try:
        resp = session.get(
            BET_HOST + "/api/v1/lottery/sale/issue",
            headers=headers,
            params={"gameId": GAME_ID, "gameRoomId": GAME_ROOM_ID, "count": 1, "isFirst": 0, "activeId": ""},
            verify=False, timeout=15,
        )
        rr = resp.json()
        if rr.get("code") == 401:
            print("  [token过期] 自动重新登录刷新token...")
            do_login()
            return False
        if rr.get("code") == 200:
            proc = rr["data"].get("process")
            cur = rr["data"].get("currentIssue", {})
            stop = cur.get("stopTime") or 0
            print(f"下注状态: process={proc}, 期号={cur.get('issue')}, stopTime={stop}")
            if proc == 1 and stop <= 10:
                print(f"  [临界] 剩余{stop}秒, 等下一期...")
                return False
            return proc == 1
    except Exception as e:
        print(f"查询下注状态异常: {e}")
    return False


def bet_to_clear_flow(need):
    """按需打流水: 2/3一注(去小数) + 1/3一注(去小数后+1), 一条指令两注一起发
    第一注投注项随机; 第二注大小反向+单双反向, 与第一注完全不重叠
    下注前判断: process=1 且 stopTime>15(防临界封盘); 406时换组合重试(最多3个不同组合)"""
    global _last_bet_failed
    _last_bet_failed = False
    try:
        big = int(need * 2 / 3)          # 80*2/3=53.33 -> 53 (去掉小数点后)
        small = int(need * 1 / 3) + 1    # 80*1/3=26.67 -> 26+1=27
        if big < 1 or small < 1:
            print(f"金额非法: {big}/{small}, 无法下注")
            _last_bet_failed = True
            return False

        # 最多尝试3个不同组合(406/重复时换组合避开同注项)
        used = []
        for attempt_comb in range(3):
            c1 = random.choice(["大", "小"])   # 第一注强制单注(大/小), 第二注才是组合
            c2 = _opposite_item(c1)
            content = f"{big}{c1}{small}{c2}"        # 金额在投注项前: 如 53大27小单 (一条指令两注)
            if content in used:
                continue
            used.append(content)
            print(f"下注计划: {content} (流水目标 {need})")
            refresh_prevtoken()               # 下注前同步页面最新 prevtoken, 防止旧token被拒406
            join_btc28_room()                 # 下注前进房间(服务器要求房间会话, 否则1030被踢出房间)

            # 等待可下注(封盘/开奖中/临界不投注), 每1秒重试, 最多120次; 每次重建headers(token过期自动登录后生效)
            ok = False
            for attempt in range(120):
                headers = _bet_headers()      # cmdBet 专用最小请求头(字节明文+动态secret)
                if _issue_open(headers):
                    ok = True
                    break
                print(f"等待安全下注窗口, 1秒后重试({attempt + 1}/120)...")
                time.sleep(1)
            if not ok:
                print("等待下注窗口超时, 本次放弃打流水")
                _last_bet_failed = True
                return False
            _sleep(1, 3)   # 下注前再随机小延时, 防临界封盘

            body = {
                "betMode": "4",
                "content": content,
                "gameId": GAME_ID,
                "gameRoomId": GAME_ROOM_ID,
                "type": 1,
                "activeId": "",
            }
            rr, ok = _send_cmdbet(content)
            msg = (rr.get("data") or {}).get("msg") or rr.get("msg") or ""
            print(f"下注 [{content}]: code={rr.get('code')} {'成功' if ok else '失败'} {str(msg)[:120]}")
            if ok:
                return True
            # 失败: 可能secret轮换(已试全部候选)或账户风控, 换组合再试
            print("换组合重试...")
            refresh_prevtoken()
        _last_bet_failed = True
        return False
    except Exception as e:
        print(f"打流水下注异常: {e}")
        _last_bet_failed = True
        return False


def bet_once(content):
    """手动下注测试: python yihao_auto.py 2大1小单 (自动等开注+探测SECRET)"""
    global _last_bet_failed
    _last_bet_failed = False
    print(f"\n手动下注: {content}")
    refresh_prevtoken()               # 下注前同步页面最新 prevtoken, 防止旧token被拒406
    join_btc28_room()                 # 下注前进房间(服务器要求房间会话, 否则1030被踢出房间)
    ok = False
    for attempt in range(120):
        headers = _bet_headers()
        if _issue_open(headers):
            ok = True
            break
        print(f"等待安全下注窗口, 1秒后重试({attempt + 1}/120)...")
        time.sleep(1)
    if not ok:
        print("等待下注窗口超时")
        _last_bet_failed = True
        return False
    _sleep(1, 3)   # 下注前随机小延时, 防临界封盘
    body = {
        "betMode": "4",
        "content": content,
        "gameId": GAME_ID,
        "gameRoomId": GAME_ROOM_ID,
        "type": 1,
        "activeId": "",
    }
    rr, ok = _send_cmdbet(content)
    msg = (rr.get("data") or {}).get("msg") or rr.get("msg") or ""
    print(f"下注 [{content}]: code={rr.get('code')} {'成功' if ok else '失败'} {str(msg)[:120]}")
    return ok


def withdraw(amount, account_id=None, auto_clear_flow=True):
    token = load_token()
    if not token:
        print("错误: 未找到token")
        return False
    print(f"\n{'=' * 50}")
    print("申请提现")
    print(f"{'=' * 50}")
    print(f"提现金额: {amount}")

    if not account_id:
        accounts = get_withdraw_account()
        if accounts:
            # 用户要求: 从所有 USDT/TRC20 地址中每次随机选一个提现
            # (地址多于1个时随机轮换; 当前仅1个地址也按随机逻辑取)
            usdt_trc20 = [a for a in accounts
                          if str(a.get("typeName", "")).upper() == "USDT"
                          and str(a.get("agreementName", "")).upper() == "TRC20"]
            pool = usdt_trc20 if usdt_trc20 else accounts
            chosen = random.choice(pool)
            account_id = chosen.get("accountId")
            print(f"随机选择账户: accountId={account_id} 类型:{chosen.get('typeName','')} 协议:{chosen.get('agreementName','')} 账号:{chosen.get('accountNum','')}")

    encrypted_pwd = encrypt_fund_password(FUND_PASSWORD)
    body = {
        "amount": str(amount),
        "password": encrypted_pwd,
        "accountId": int(account_id) if account_id else None,
    }
    body = {k: v for k, v in body.items() if v is not None}

    headers = build_headers(load_token(), with_uid=True)

    # ===== 提现前预检流水 (auditret, 页面同款: 流水不足先提示, 不提交) =====
    try:
        pre = session.get(
            active["base"] + "/api/v1/member/withdraw/auditret",
            headers=headers,
            params={"amount": str(amount), "accountId": account_id},
            verify=False, timeout=15,
        )
        pre_result = pre.json()
        pre_code = pre_result.get("code")
        pre_msg = pre_result.get("msg", "")
        print(f"提现预检(auditret): code={pre_code}, msg={pre_msg}")
        if pre_code != 200:
            need = parse_need_flow(pre_msg)
            if need and auto_clear_flow:
                # 提示流水不足后先延时10~20秒再下注(模拟真人查看/操作, 不立刻下注)
                _sleep(10, 20)
                print(f"检测到流水不足(还需 {need}), 延时后自动下注打流水...")
                if bet_to_clear_flow(need):
                    print("下注成功, 等待1~2分钟后重新预检再提现...")
                    _sleep(60, 120)   # 必须: 下注成功后等待1~2分钟再提现
                    pre2 = session.get(
                        active["base"] + "/api/v1/member/withdraw/auditret",
                        headers=headers,
                        params={"amount": str(amount), "accountId": account_id},
                        verify=False, timeout=15,
                    )
                    pr2 = pre2.json()
                    print(f"重新预检: code={pr2.get('code')}, msg={pr2.get('msg')}")
                    if pr2.get("code") != 200:
                        print(f"流水仍未达标: {pr2.get('msg')}, 跳过本次提现")
                        return False
                    # 打完流水后重新查余额(下注有输赢, 余额会变, 按下注后最新余额提现)
                    try:
                        new_bal = get_balance()
                        if new_bal and new_bal > 0:
                            amount = int(new_bal)
                            body["amount"] = str(amount)
                            print(f"打流水后重新查余额: {new_bal}, 提现金额更新为 {amount}")
                    except Exception as e:
                        print(f"重新查余额失败(用原金额继续): {e}")
                    print("流水已达标, 提交提现申请...")
                else:
                    print("打流水失败, 跳过本次提现")
                    return False
            else:
                print(f"预检未通过, 跳过提现: {pre_msg}")
                return False
        print("流水预检通过, 提交提现申请...")
    except Exception as e:
        print(f"提现预检异常(继续尝试提现): {e}")

    # 提交提现前刷新 prevtoken (页面会刷新token, 旧token加密提交会被406「重复请求」拒绝, 与下注同理)
    try:
        if refresh_prevtoken():
            headers = build_headers(load_token(), with_uid=True)
            print("  [提现] prevtoken 已刷新, 使用最新token提交")
    except Exception as e:
        print(f"  [提现] 提现前token刷新异常(沿用当前token): {e}")

    url = active["base"] + "/api/v1/member/withdraw/application"
    try:
        resp = session.post(url, headers=headers, json=body, verify=False, timeout=15)
        print(f"状态码: {resp.status_code}")
        print(f"响应: {resp.text[:500]}")
        result = resp.json()
        if result.get("code") == 200:
            print(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] 提现申请成功!")
            return True
        else:
            print(f"\n提现失败: code={result.get('code')}, msg={result.get('msg')}")
            return False
    except Exception as e:
        print(f"请求异常: {e}")
        return False


# ================= 单次流程 =================
def run_once(dry_run=False):
    """查余额(优先用已保存token, token失效才重新登录) -> 大于阈值则提现"""
    print("\n" + "#" * 50)
    print(f"# 执行时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
    print("#" * 50)

    # 登录后随机延时1-3分钟再刷新余额(防风控; 干跑模式跳过)
    if not dry_run:
        delay = random.randint(6, 10) if TEST_MODE else random.randint(60, 180)
        print(f"登录后延时 {delay} 秒({delay/60:.1f}分钟), 再刷新余额...")
        time.sleep(delay)

    # 优先用已保存的 token 查余额 (自动登录会被滑块验证拦截, 走 token 更稳)
    balance = get_balance() if load_token() else None
    if balance is None:
        # token 缺失或失效, 尝试重新登录
        r = do_login()
        if not r or r.get("code") != 200:
            print("登录未成功(可能需滑块验证), 本次跳过")
            return False
        balance = get_balance()

    # 终身VIP月俸禄红包: 窗口内尝试领取一次(真实运行才领, 干跑不操作)
    if not dry_run:
        claim_vip_monthly_reward()
        # 领取后延时10-20秒再刷新余额(红包到账后计入)
        _sleep(10, 20)
        balance2 = get_balance()
        if balance2 is not None:
            balance = balance2

    if balance is None:
        print("无法获取余额")
        return False

    print(f"\n当前余额: {balance}")
    if balance <= THRESHOLD:
        print(f"余额 {balance} <= {THRESHOLD}, 不满足提现条件(需 > {THRESHOLD})")
        return False

    amount = int(balance)
    print(f"\n满足条件(余额 {balance} > {THRESHOLD})! 准备提现 {amount} 元")

    if dry_run:
        print("【干跑模式】仅打印判断, 不执行提现")
        return True

    # 刷新余额后随机延时1-3分钟再提现(模拟真人操作)
    delay = random.randint(6, 10) if TEST_MODE else random.randint(60, 180)
    print(f"延时 {delay} 秒({delay/60:.1f}分钟)后提交提现...")
    time.sleep(delay)

    success = withdraw(amount)
    if success:
        print(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] 提现申请已提交, 金额: {amount}")
    else:
        print("\n提现失败")
    return success


# ================= 主入口 =================
def main():
    args = sys.argv[1:]

    # 每次启动先刷新壹号线路池(远程失败自动用缓存兜底)
    refresh_domains()

    # 参数模式
    if args and args[0].startswith("--"):
        if "--check" in args:
            print("模式: 干跑一次(查余额+判断, 不提现)")
            run_once(dry_run=True)
            return
        if "--once" in args:
            print("模式: 单次运行(登录+查余额+自动提现)")
            run_once(dry_run=False)
            return

    if args:
        cmd = args[0].lower()
        if cmd == "login":
            u = args[1] if len(args) > 1 else USERNAME
            p = args[2] if len(args) > 2 else PASSWORD
            do_login(u, p)
            return
        if cmd == "balance":
            get_balance()
            return
        if cmd == "claim":
            r = claim_vip_monthly_reward()
            if r is True or r == "claimed":
                gen_next_claim_plan()
            return
        if cmd == "withdraw":
            b = get_balance()
            if b and b > THRESHOLD:
                withdraw(int(b))
            else:
                print(f"余额 {b} 未超过 {THRESHOLD}, 不执行提现")
            return
        if cmd[:2] == "tx" and cmd[2:].isdigit():
            # 测试提现: python yihao_auto.py tx50  -> 提现50元并输出完整提现日志
            amt = int(cmd[2:])
            print(f"模式: 测试提现 {amt} 元 (关闭自动打流水, 仅验证提现流程/密码)")
            ok = withdraw(amt, auto_clear_flow=False)
            print(f"\n测试提现结果: {'成功' if ok else '失败'}")
            return
        if cmd == "tx":
            print("用法: python yihao_auto.py tx<金额>   例: tx50 提现50元")
            return
        if cmd == "set-token":
            if len(args) < 2:
                print("用法: python yihao_auto.py set-token <token>")
                return
            save_token(args[1].strip())
            print("已保存 token")
            return
        if cmd == "show-token":
            t = load_token()
            print(f"当前 token: {t if t else '(无)'}")
            print(f"当前 memberId: {MEMBER_ID}")
            return
        if cmd == "domains":
            print(f"\n缓存文件: {YH_DOMAINS_FILE}")
            cached = _load_domains_cache()
            print(f"本地缓存线路 {len(cached)} 个: " + json.dumps(cached, ensure_ascii=False))
            print(f"\n当前生效 API_TARGETS {len(API_TARGETS)} 个:")
            for i, t in enumerate(API_TARGETS, 1):
                print(f"  {i}. {t['base']}")
            return
        if cmd == "help":
            print(__doc__)
            return
        # 其他参数一律当作"手动下注内容", 如: python yihao_auto.py 2大1小单
        print(f"手动下注模式: {args[0]}")
        bet_once(args[0])
        return

    # 默认: 定时循环(红包领取按计划 + 余额>100自动提现)
    print("=" * 50)
    print("壹号娱乐 自动提现 + 终身VIP月俸禄自动领取")
    print("提现: 余额>100自动提现, 随机3-10天循环; 红包: 每月6号07:00~9号23:59窗口, 计划持久化 qiandao/yihao.txt (重启不变)")
    print("=" * 50)

    loop_count = 0
    while True:
        loop_count += 1
        now = time.strftime("%Y-%m-%d %H:%M:%S")
        print(f"\n{'#' * 50}")
        print(f"# 第 {loop_count} 次运行  {now}")
        print(f"{'#' * 50}")

        try:
            # ===== 1. 红包领取(按持久化计划, 重启不变) =====
            now_dt = datetime.datetime.now()
            plan = load_claim_plan()
            do_claim, new_plan = plan_decision(plan, now_dt)
            if do_claim:
                r = claim_vip_monthly_reward()
                if r is True or r == "claimed":
                    gen_next_claim_plan()            # 本月已处理(领到/已领) -> 排下月计划
                else:
                    print("领取未成功(异常), 保留当前计划, 后续循环会重试")
            elif new_plan is None and plan is not None:
                print(f"红包签到计划未到: {plan.strftime('%Y-%m-%d %H:%M:%S')} (重启不变), 本次跳过领取")

            # ===== 2. 查余额 + 自动提现(3-10天循环) =====
            delay = random.randint(6, 10) if TEST_MODE else random.randint(60, 180)
            print(f"延时 {delay} 秒({delay/60:.1f}分钟)后查询余额...")
            time.sleep(delay)
            balance = get_balance()
            if balance is None:
                # token 缺失/失效 -> 尝试重新登录
                r = do_login()
                if r and r.get("code") == 200:
                    balance = get_balance()
            if balance is None:
                print("无法获取余额, 本次跳过提现")
            elif balance <= THRESHOLD:
                print(f"\n当前余额: {balance}")
                print(f"余额 {balance} <= {THRESHOLD}, 不满足提现条件(需 > {THRESHOLD})")
            else:
                amount = int(balance)
                print(f"\n满足条件(余额 {balance} > {THRESHOLD})! 准备提现 {amount} 元")
                # 刷新余额后随机延时1-3分钟再提现(模拟真人操作)
                delay = random.randint(6, 10) if TEST_MODE else random.randint(60, 180)
                print(f"延时 {delay} 秒({delay/60:.1f}分钟)后提交提现...")
                time.sleep(delay)
                ok = withdraw(amount)
                if ok:
                    print(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] 提现申请已提交, 金额: {amount}")
                else:
                    print("\n提现失败")
        except Exception as e:
            print(f"运行异常: {e}")

        # ============ 计算下次运行时间 ============
        # 红包签到计划时间固定(文件持久化, 重启不变); 常规随机3-10天提现循环 与 计划 取较早者
        now_dt = datetime.datetime.now()
        plan = load_claim_plan()
        if _last_bet_failed:
            print("\n打流水未成功, 不影响调度, 仍按3-10天常规循环")
        days = random.randint(MIN_DAYS, MAX_DAYS)
        candidate = now_dt + datetime.timedelta(days=days, seconds=random.randint(0, 86399))
        next_time = candidate
        if plan and plan > now_dt:
            next_time = min(candidate, plan)
            print(f"\n红包签到计划: {plan.strftime('%Y-%m-%d %H:%M:%S')} (固定不变)")
        elif plan and plan <= now_dt:
            print(f"\n红包签到计划({plan.strftime('%Y-%m-%d %H:%M')})已过期, 已由本次运行重排")

        wait_seconds = max(1, int((next_time - datetime.datetime.now()).total_seconds()))
        print(f"\n下次运行: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
        print(f"等待: {wait_seconds // 86400}天 {wait_seconds % 86400 // 3600:02d}:{wait_seconds % 3600 // 60:02d}:{wait_seconds % 60:02d} ({wait_seconds}秒)")
        print("-" * 50)

        time.sleep(wait_seconds)


if __name__ == "__main__":
    main()
