From 107540a9feb7eab6254cd07648e2345e22e242ec Mon Sep 17 00:00:00 2001 From: timxx <3337598077@qq.com> Date: Sun, 5 Jul 2026 16:53:02 +0000 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Points=5FBased/D=5FSFSY=5F?= =?UTF-8?q?SJBHD.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Points_Based/D_SFSY_HD_1.1.py | 856 ---------------------------------- Points_Based/D_SFSY_SJBHD.py | 816 ++++++++++++++++++++++++++++++++ 2 files changed, 816 insertions(+), 856 deletions(-) delete mode 100644 Points_Based/D_SFSY_HD_1.1.py create mode 100644 Points_Based/D_SFSY_SJBHD.py diff --git a/Points_Based/D_SFSY_HD_1.1.py b/Points_Based/D_SFSY_HD_1.1.py deleted file mode 100644 index c83226e..0000000 --- a/Points_Based/D_SFSY_HD_1.1.py +++ /dev/null @@ -1,856 +0,0 @@ -# cron: 19 12,20 * * * -# const $ = new Env("顺丰33周年庆活动"); -""" - -顺丰33周年活动 - 完成任务抽勋章 -Author: 爱学习的呆子 -Version: 1.1.0 -Date: 2026-03-19 - -顺丰暗号环境变量:sfsyah - -""" - -import hashlib -import json -import os -import random -import time -from datetime import datetime -from typing import Dict, List, Optional, Any -from urllib.parse import unquote -from concurrent.futures import ThreadPoolExecutor, as_completed -from threading import Lock -import requests -from requests.packages.urllib3.exceptions import InsecureRequestWarning - -# 禁用SSL警告 -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) -inviteId = ['C6E5C3BDD7624520AB869D2AF9E75D95'] - -# ==================== 配置常量 ==================== -PROXY_TIMEOUT = 15 -MAX_PROXY_RETRIES = 5 -REQUEST_RETRY_COUNT = 3 -CONCURRENT_NUM = int(os.getenv('SFBF', '1')) -if CONCURRENT_NUM > 20: - CONCURRENT_NUM = 20 -elif CONCURRENT_NUM < 1: - CONCURRENT_NUM = 1 - -print_lock = Lock() - -# 周年活动配置 -ACTIVITY_CODE = "ANNIVERSARY_2026" -TOKEN = 'wwesldfs29aniversaryvdld29' -SYS_CODE = 'MCS-MIMP-CORE' - -# 暗号环境变量(可选,不设置则自动从API获取答案) -GUESS_ANSWER_ENV = 'sfsyah' - -# 需要跳过的任务类型(需要实际操作的) -SKIP_TASK_TYPES = [ - 'BUY_ADD_VALUE_SERVICE_PACKET', # 通过服务套餐寄件 - 'SEND_INTERNATIONAL_PACKAGE', # 寄一单国际件 - 'LOOK_BIG_PACKAGE_GET_CASH', # 寄大件重货 - 'SEND_SUCCESS_RECALL', # 去寄快递 - 'CHARGE_NEW_EXPRESS_CARD', # 充值新速运通全国卡 - 'CHARGE_COLLECT_ALL', # 充值1000元一键集齐 - 'OPEN_FAMILY_HOME_MUTUAL', # 开通家庭8折互寄权益 - 'BUY_ANNIVERSARY_LIMITED_PACKET', # 33周年宠粉券包 - # 'INTEGRAL_EXCHANGE', # 积分兑换抽勋章次数(已特殊处理) -] - - -# ==================== 日志 ==================== -class Logger: - def __init__(self): - self.messages: List[str] = [] - self.lock = Lock() - - def _log(self, icon: str, msg: str): - line = f"{icon} {msg}" - with print_lock: - print(line) - with self.lock: - self.messages.append(line) - - def info(self, msg): self._log('📝', msg) - def success(self, msg): self._log('✅', msg) - def warning(self, msg): self._log('⚠️', msg) - def error(self, msg): self._log('❌', msg) - def task(self, msg): self._log('🎯', msg) - def medal(self, msg): self._log('🏅', msg) - - -# ==================== 代理管理器 ==================== -class ProxyManager: - def __init__(self, api_url: str): - self.api_url = api_url - - def get_proxy(self) -> Optional[Dict[str, str]]: - try: - if not self.api_url: - return None - response = requests.get(self.api_url, timeout=10) - if response.status_code == 200: - proxy_text = response.text.strip() - if ':' in proxy_text: - proxy = proxy_text if proxy_text.startswith('http') else f'http://{proxy_text}' - # 隐藏认证信息用于显示 - display = proxy - if '@' in proxy: - parts = proxy.split('@') - display = f"http://***:***@{parts[-1]}" - with print_lock: - print(f"✅ 获取代理: {display}") - return {'http': proxy, 'https': proxy} - with print_lock: - print(f"❌ 获取代理失败: HTTP {response.status_code}") - return None - except Exception as e: - with print_lock: - print(f"❌ 获取代理异常: {str(e)[:100]}") - return None - - -# ==================== HTTP客户端 ==================== -class SFHttpClient: - def __init__(self, proxy_manager: ProxyManager): - self.proxy_manager = proxy_manager - self.session = requests.Session() - self.session.verify = False - - proxy = self.proxy_manager.get_proxy() - if proxy: - self.session.proxies = proxy - else: - if self.proxy_manager.api_url: - with print_lock: - print("⚠️ 代理获取失败,将不使用代理") - - self.headers = { - 'Host': 'mcs-mimp-web.sf-express.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf254173b) XWEB/19027', - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - 'channel': 'xcxpart', - 'platform': 'MINI_PROGRAM', - 'accept-language': 'zh-CN,zh;q=0.9', - } - - def _generate_sign(self) -> Dict[str, str]: - timestamp = str(int(round(time.time() * 1000))) - data = f'token={TOKEN}×tamp={timestamp}&sysCode={SYS_CODE}' - signature = hashlib.md5(data.encode()).hexdigest() - return { - 'syscode': SYS_CODE, - 'timestamp': timestamp, - 'signature': signature, - } - - def request(self, url: str, data: Optional[Dict] = None, method: str = 'POST') -> Optional[Dict]: - retry_count = 0 - proxy_retry_count = 0 - - while proxy_retry_count < MAX_PROXY_RETRIES: - # 每次请求重新生成签名 - sign_data = self._generate_sign() - headers = {**self.headers, **sign_data} - - try: - if method == 'POST': - resp = self.session.post(url, headers=headers, json=data or {}, timeout=PROXY_TIMEOUT) - else: - resp = self.session.get(url, headers=headers, timeout=PROXY_TIMEOUT) - resp.raise_for_status() - - try: - result = resp.json() - if result is None: - retry_count += 1 - if retry_count < REQUEST_RETRY_COUNT: - time.sleep(2) - continue - return None - return result - except (json.JSONDecodeError, ValueError): - retry_count += 1 - if retry_count < REQUEST_RETRY_COUNT: - time.sleep(2) - continue - return None - - except requests.exceptions.RequestException as e: - retry_count += 1 - error_str = str(e) - - # 代理相关错误,切换代理 - if 'ProxyError' in error_str or 'SSLError' in error_str or 'ConnectionError' in error_str: - proxy_retry_count += 1 - if proxy_retry_count < MAX_PROXY_RETRIES: - new_proxy = self.proxy_manager.get_proxy() - if new_proxy: - self.session.proxies = new_proxy - retry_count = 0 # 换代理后重置请求重试计数 - time.sleep(2) - continue - - # 普通请求错误,重试 - if retry_count < REQUEST_RETRY_COUNT: - time.sleep(2) - continue - return None - - except Exception: - return None - - return None - - def login(self, url: str) -> tuple: - """登录(兼容URL和CK格式)""" - try: - decoded_input = unquote(url) - if decoded_input.startswith('sessionId=') or '_login_mobile_=' in decoded_input: - cookie_dict = {} - for item in decoded_input.split(';'): - item = item.strip() - if '=' in item: - k, v = item.split('=', 1) - cookie_dict[k] = v - for k, v in cookie_dict.items(): - self.session.cookies.set(k, v, domain='mcs-mimp-web.sf-express.com') - user_id = cookie_dict.get('_login_user_id_', '') - phone = cookie_dict.get('_login_mobile_', '') - return (True, user_id, phone) if phone else (False, '', '') - else: - self.session.get(unquote(url), headers=self.headers, timeout=PROXY_TIMEOUT) - cookies = self.session.cookies.get_dict() - user_id = cookies.get('_login_user_id_', '') - phone = cookies.get('_login_mobile_', '') - return (True, user_id, phone) if phone else (False, '', '') - except Exception as e: - print(f'登录异常: {str(e)}') - return False, '', '' - - -# ==================== 周年活动任务执行器 ==================== -class AnniversaryExecutor: - # MODIFIED: 增加 user_id 参数 - def __init__(self, http: SFHttpClient, logger: Logger, user_id: str): - self.http = http - self.logger = logger - self.user_id = user_id - - # ---------- API 方法 ---------- - - def get_activity_index(self) -> Optional[Dict]: - """获取周年活动首页信息""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026IndexService~index' - resp = self.http.request(url, data={}) - if resp and resp.get('success'): - return resp.get('obj', {}) - return None - - def get_task_list(self) -> Optional[List[Dict]]: - """获取周年活动任务列表""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~activityTaskService~taskList' - data = { - "activityCode": ACTIVITY_CODE, - "channelType": "MINI_PROGRAM" - } - resp = self.http.request(url, data=data) - if resp and resp.get('success'): - return resp.get('obj', []) - else: - error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败' - self.logger.error(f'获取任务列表失败: {error_msg}') - return None - - def finish_task(self, task_code: str) -> bool: - """完成任务(浏览类)""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonRoutePost/memberEs/taskRecord/finishTask' - data = {"taskCode": task_code} - resp = self.http.request(url, data=data) - if resp and resp.get('success'): - return True - return False - - # ========== 新增:专用接口-领取寄件会员权益 ========== - def receive_vip_benefit(self) -> bool: - """领取寄件会员权益(特殊任务)""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberManage~memberEquity~commonEquityReceive' - data = {"key": "surprise_benefit"} - resp = self.http.request(url, data=data) - if resp and resp.get('success'): - self.logger.success(f'[领取寄件会员权益] 完成成功') - # 可选:解析返回的奖励信息 - if resp.get('obj'): - self.logger.info(f'领取详情: {resp["obj"]}') - return True - else: - error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败' - self.logger.warning(f'[领取寄件会员权益] 完成失败: {error_msg}') - return False - # ================================================= - - def fetch_tasks_reward(self) -> Optional[Dict]: - """领取已完成任务的奖励(获得抽勋章次数)""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026TaskService~fetchTasksReward' - data = { - "channelType": "MINI_PROGRAM", - "activityCode": ACTIVITY_CODE - } - resp = self.http.request(url, data=data) - if resp and resp.get('success'): - return resp.get('obj', {}) - else: - error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败' - self.logger.warning(f'领取任务奖励失败: {error_msg}') - return None - - def get_card_status(self) -> Optional[Dict]: - """查看勋章收集状态""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026CardService~cardStatus' - resp = self.http.request(url, data={}) - if resp and resp.get('success'): - return resp.get('obj', {}) - return None - - def claim_medal(self, batch: bool = False) -> Optional[Dict]: - """抽勋章""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026CardService~claim' - data = {"batchClaim": batch} - resp = self.http.request(url, data=data) - if resp and resp.get('success'): - return resp.get('obj', {}) - else: - error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败' - self.logger.error(f'抽勋章失败: {error_msg}') - return None - - def give_countdown_chance(self) -> Optional[Dict]: - """领取倒计时奖励(每日一次免费抽勋章机会)""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026CardService~giveClaimChance' - resp = self.http.request(url, data={}) - if resp and resp.get('success'): - return resp.get('obj', {}) - else: - error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败' - self.logger.warning(f'领取倒计时奖励失败: {error_msg}') - return None - - def get_user_rest_integral(self) -> int: - """查询用户剩余积分""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~activityTaskService~getUserRestIntegral' - resp = self.http.request(url, data={}) - if resp and resp.get('success'): - points = resp.get('obj', 0) - self.logger.info(f'当前可用积分: {points}') - return points - return 0 - - def exchange_points_for_chance(self) -> bool: - """积分兑换抽勋章次数(10积分兑1次)""" - points = self.get_user_rest_integral() - if points < 10: - self.logger.warning(f'积分不足10,无法兑换(当前: {points})') - return False - - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026TaskService~integralExchange' - data = { - "exchangeNum": 1, # 一次兑换1次机会 - "activityCode": ACTIVITY_CODE # 当前活动码 - } - - resp = self.http.request(url, data=data) - if resp and resp.get('success'): - self.logger.success('积分兑换成功,获得1次抽勋章次数') - return True - else: - error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败' - self.logger.warning(f'积分兑换失败: {error_msg}') - return False - - def get_guess_title_list(self) -> Optional[Dict]: - """获取对暗号题目列表(含答案)""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026GuessService~titleList' - resp = self.http.request(url, data={}) - if resp and resp.get('success'): - return resp.get('obj', {}) - return None - - def submit_guess_answer(self, answer: str, period: str = '') -> Optional[Dict]: - """提交暗号答案,返回完整响应""" - url = 'https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026GuessService~answer' - data = {"answerInfo": answer} - if period: - data["period"] = period - resp = self.http.request(url, data=data) - if resp: - self.logger.info(f'[对暗号] 提交响应: {json.dumps(resp, ensure_ascii=False)[:300]}') - if resp.get('success'): - obj = resp.get('obj', {}) - # 检查obj中是否有失败标志 - if isinstance(obj, dict) and obj.get('answerStatus') == 0: - self.logger.warning(f'[对暗号] 答案错误') - return None - return obj - else: - error_msg = resp.get('errorMessage', '未知错误') - self.logger.warning(f'提交暗号失败: {error_msg}') - return None - else: - self.logger.warning(f'提交暗号失败: 请求失败') - return None - - # ---------- 新增:邀请初始化 ---------- - def do_invite(self): - """发送邀请初始化请求(借鉴自第二段代码)""" - try: - # 从全局 inviteId 列表中排除自己的 user_id - available_invites = [inv for inv in inviteId if inv != self.user_id] - if not available_invites: - self.logger.warning("没有可用的邀请码(排除自身后为空),跳过邀请") - return - random_invite = random.choice(available_invites) - # self.logger.info(f"随机选择邀请码: {random_invite} (排除自身 {self.user_id})") - # 构造邀请请求 URL,使用 commonNoLoginPost - url = "https://mcs-mimp-web.sf-express.com/mcs-mimp/commonPost/~memberNonactivity~anniversary2026IndexService~index" - data = {"inviteType": 1, "inviteUserId": random_invite} - resp = self.http.request(url, data=data) - # if resp and resp.get('success'): - # self.logger.success(f"邀请初始化成功,邀请码: {random_invite}") - # else: - # error_msg = resp.get('errorMessage', '未知错误') if resp else '请求失败' - # self.logger.warning(f"邀请初始化失败: {error_msg}") - except Exception as e: - self.logger.error(f"邀请初始化异常: {str(e)}") - - # ---------- 业务逻辑 ---------- - - def do_guess_game(self) -> bool: - """对暗号任务:读取所有日期的题目,按顺序匹配环境变量中的暗号逐日提交""" - self.logger.task('[对暗号赢免单] 开始...') - - # 1. 获取题目列表 - guess_info = self.get_guess_title_list() - if not guess_info: - self.logger.warning('[对暗号] 获取题目列表失败') - return False - - current_period = guess_info.get('currentPeriod', '') - title_list = guess_info.get('guessTitleInfoList', []) - - if not title_list: - self.logger.warning('[对暗号] 题目列表为空') - return False - - # 按日期排序 - title_list.sort(key=lambda x: x.get('period', '')) - - self.logger.info(f'[对暗号] 共 {len(title_list)} 天题目,当前日期: {current_period}') - - # 2. 读取环境变量中的暗号列表(每行一个,按日期顺序对应) - env_answers_raw = os.getenv(GUESS_ANSWER_ENV, '').strip() - env_answers = [a.strip() for a in env_answers_raw.split('\n') if a.strip()] if env_answers_raw else [] - - if env_answers: - self.logger.info(f'[对暗号] 环境变量已设置 {len(env_answers)} 个暗号') - - # 3. 逐日处理 - any_success = False - for i, title in enumerate(title_list): - period = title.get('period', '') - answer_status = title.get('answerStatus') - tip = title.get('tip', '') - - # 已答过的跳过 - if answer_status == 1: - self.logger.success(f'[对暗号] {period} 已作答: {title.get("answerInfo", "")}') - any_success = True - continue - - # 超过当前日期的不处理(未来题目) - if period > current_period: - self.logger.info(f'[对暗号] {period} 尚未开放,跳过') - continue - - # 获取答案:优先环境变量按顺序匹配,其次API返回的answerInfo - answer = '' - if i < len(env_answers): - answer = env_answers[i] - if not answer: - answer = title.get('answerInfo', '') - - if not answer: - self.logger.warning(f'[对暗号] {period} 无法获取答案(提示: {tip})') - self.logger.info(f'[对暗号] 请在环境变量 {GUESS_ANSWER_ENV} 第 {i + 1} 行填写该日暗号') - continue - - self.logger.info(f'[对暗号] {period} 提交答案: {answer}') - - # 提交答案 - result = self.submit_guess_answer(answer, period) - if result is not None: - # 提交后重新获取题目列表验证是否真的成功 - time.sleep(1) - verify_info = self.get_guess_title_list() - if verify_info: - for t in verify_info.get('guessTitleInfoList', []): - if t.get('period') == period: - if t.get('answerStatus') == 1: - self.logger.success(f'[对暗号] {period} 验证通过,答案已录入: {t.get("answerInfo", "")}') - any_success = True - else: - self.logger.warning(f'[对暗号] {period} 验证失败,答案未被采纳 (answerStatus={t.get("answerStatus")})') - break - else: - self.logger.success(f'[对暗号] {period} 提交成功(未能验证)') - any_success = True - else: - self.logger.warning(f'[对暗号] {period} 提交失败') - - time.sleep(1) - - return any_success - - def do_tasks(self, result: Dict) -> None: - """执行所有可自动完成的任务""" - self.logger.info('正在获取周年活动任务列表...') - tasks = self.get_task_list() - if tasks is None: - return - - self.logger.info(f'共发现 {len(tasks)} 个任务') - - for task in tasks: - task_name = task.get('taskName', '未知') - task_type = task.get('taskType', '') - task_code = task.get('taskCode', '') - status = task.get('status') - process = task.get('process', '') - rest_finish = task.get('restFinishTime', 0) - virtual_token = task.get('virtualTokenNum', 0) - - # status=3 表示已完成且已领奖,status=1且restFinishTime=0表示已完成待领奖 - if status == 3 or (status == 1 and rest_finish <= 0): - can_receive = task.get('canReceiveTokenNum', 0) - if can_receive > 0: - self.logger.info(f'[{task_name}] 已完成,待领取 {can_receive} 次抽勋章机会') - else: - self.logger.success(f'[{task_name}] 已完成 ({process})') - continue - - if task_type in SKIP_TASK_TYPES: - self.logger.info(f'[{task_name}] 需要实际操作,跳过') - continue - - # 对暗号单独处理 - if task_type == 'GUESS_GAME_TIP': - if self.do_guess_game(): - result['tasks_completed'] += 1 - continue - - # 积分兑换特殊处理 - if task_type == 'INTEGRAL_EXCHANGE': - self.logger.task(f'[{task_name}] 尝试用积分兑换抽勋章次数...') - if self.exchange_points_for_chance(): - self.logger.success(f'[{task_name}] 积分兑换成功') - result['tasks_completed'] += 1 - continue - - # ========== 新增:领取寄件会员权益特殊处理 ========== - if task_type == 'RECEIVE_VIP_BENEFIT': - self.logger.task(f'[{task_name}] 尝试专用接口领取...') - if self.receive_vip_benefit(): - result['tasks_completed'] += 1 - continue - # ================================================= - - # 有taskCode的任务尝试自动完成(通用接口) - if task_code: - self.logger.task(f'[{task_name}] 尝试完成任务 (taskCode: {task_code})') - if self.finish_task(task_code): - self.logger.success(f'[{task_name}] 完成成功,可获得 {virtual_token} 次抽勋章机会') - result['tasks_completed'] += 1 - else: - self.logger.warning(f'[{task_name}] 完成失败') - time.sleep(1) - else: - self.logger.info(f'[{task_name}] 无taskCode,跳过 ({task_type})') - - def do_fetch_rewards(self, result: Dict) -> None: - """领取所有已完成任务的奖励""" - self.logger.info('领取任务奖励...') - time.sleep(1) - reward_resp = self.fetch_tasks_reward() - if reward_resp: - received = reward_resp.get('receivedAccountList', []) - if received: - for item in received: - currency = item.get('currency', '') - amount = item.get('amount', 0) - task_type = item.get('taskType', '') - self.logger.success(f'领取奖励: {currency} x{amount} (来自: {task_type})') - else: - self.logger.info('无新的任务奖励可领取') - - # 显示累计进度 - accrued = reward_resp.get('accruedTaskAward', {}) - progress = accrued.get('currentProgress', 0) - config = accrued.get('progressConfig', {}) - if config: - milestones = ', '.join([f'{k}个任务得{v}次' for k, v in sorted(config.items(), key=lambda x: int(x[0]))]) - self.logger.info(f'累计完成任务数: {progress} (里程碑: {milestones})') - - def do_countdown_chance(self, result: Dict) -> None: - """领取倒计时奖励(每日免费抽勋章机会)""" - self.logger.info('领取倒计时奖励...') - time.sleep(1) - resp = self.give_countdown_chance() - if resp: - if resp.get('todayCountdownChanceGiven'): - received = resp.get('receivedAccountList', []) - if received: - for item in received: - currency = item.get('currency', '') - amount = item.get('amount', 0) - self.logger.success(f'倒计时奖励: {currency} x{amount}') - else: - self.logger.success('倒计时奖励已领取') - else: - self.logger.info('今日倒计时奖励未到领取时间') - else: - self.logger.warning('倒计时奖励领取失败') - - - def do_claim_medals(self, result: Dict) -> None: - """抽勋章直到没有次数""" - # 先查看当前状态 - card_status = self.get_card_status() - if card_status: - claim_balance = 0 - for acc in card_status.get('currentAccountList', []): - if acc.get('currency') == 'CLAIM_CHANCE': - claim_balance = acc.get('balance', 0) - break - self.logger.info(f'当前可抽勋章次数: {claim_balance}') - - # 显示已有勋章 - medals_owned = [] - for acc in card_status.get('currentAccountList', []): - currency = acc.get('currency', '') - balance = acc.get('balance', 0) - if currency != 'CLAIM_CHANCE' and balance > 0: - medals_owned.append(f'{currency}x{balance}') - if medals_owned: - self.logger.info(f'已有勋章: {", ".join(medals_owned)}') - - if claim_balance <= 0: - self.logger.info('无抽勋章次数,跳过') - return - - # 开始抽勋章 - self.logger.info('开始抽勋章...') - claim_count = 0 - max_claims = 30 - - while claim_count < max_claims: - time.sleep(1) - claim_result = self.claim_medal(batch=False) - if claim_result is None: - break - - received = claim_result.get('receivedAccountList', []) - current_accounts = claim_result.get('currentAccountList', []) - - if received: - for item in received: - currency = item.get('currency', '未知') - amount = item.get('amount', 0) - self.logger.medal(f'抽到勋章: {currency} x{amount}') - result['medals_detail'].append({'type': currency, 'amount': amount}) - result['medals_claimed'] += amount - claim_count += 1 - else: - self.logger.info('没有抽到勋章或无抽取次数') - break - - # 检查剩余次数 - claim_balance = 0 - for acc in current_accounts: - if acc.get('currency') == 'CLAIM_CHANCE': - claim_balance = acc.get('balance', 0) - break - - result['claim_balance'] = claim_balance - self.logger.info(f'剩余抽取次数: {claim_balance}') - - if claim_balance <= 0: - break - - self.logger.info(f'抽勋章完成,本次共抽取 {result["medals_claimed"]} 个勋章') - - def run(self) -> Dict[str, Any]: - """执行周年活动全流程""" - result = { - 'tasks_completed': 0, - 'medals_claimed': 0, - 'medals_detail': [], - 'claim_balance': 0, - } - - # MODIFIED: 在任务开始前先发送邀请 - self.do_invite() - - # 0. 获取活动首页信息 - index_info = self.get_activity_index() - if index_info: - self.logger.info(f'活动时间: {index_info.get("acStartTime", "")} ~ {index_info.get("acEndTime", "")}') - self.logger.info(f'历史寄件数: {index_info.get("sendNum", 0)},累计支付: {index_info.get("payAmount", 0)}元') - - # 1. 执行任务(含对暗号) - self.do_tasks(result) - - # 2. 领取任务奖励(获得抽勋章次数) - self.do_fetch_rewards(result) - - # 3. 抽勋章 - self.do_claim_medals(result) - - return result - - -# ==================== 账号执行 ==================== -def run_account(account_url: str, index: int) -> Dict[str, Any]: - """执行单个账号""" - logger = Logger() - proxy_url = os.getenv('SF_PROXY_API_URL', '') - proxy_manager = ProxyManager(proxy_url) - - # 登录 - http = SFHttpClient(proxy_manager) - retry_count = 0 - login_success = False - phone = '' - user_id = '' - - while retry_count < MAX_PROXY_RETRIES and not login_success: - try: - if retry_count > 0: - http = SFHttpClient(proxy_manager) - success, user_id, phone = http.login(account_url) - if success: - login_success = True - break - except Exception as e: - pass - retry_count += 1 - if retry_count < MAX_PROXY_RETRIES: - time.sleep(2) - - if not login_success: - logger.error(f'账号{index + 1} 登录失败') - return {'success': False, 'phone': '', 'index': index, - 'tasks_completed': 0, 'medals_claimed': 0, 'medals_detail': [], 'claim_balance': 0} - - masked_phone = phone[:3] + "****" + phone[7:] if len(phone) >= 7 else phone - logger.success(f'账号{index + 1}: 【{masked_phone}】登录成功') - - # 随机延迟 - time.sleep(random.uniform(1, 3)) - - # MODIFIED: 将 user_id 传递给 AnniversaryExecutor - executor = AnniversaryExecutor(http, logger, user_id) - activity_result = executor.run() - - return { - 'success': True, - 'phone': phone, - 'index': index, - **activity_result, - } - - -# ==================== 主程序 ==================== -def main(): - env_name = 'sfsyUrl' - env_value = os.getenv(env_name) - if not env_value: - print(f"❌ 未找到环境变量 {env_name},请检查配置") - return - - account_urls = [url.strip() for url in env_value.split('&') if url.strip()] - if not account_urls: - print(f"❌ 环境变量 {env_name} 为空或格式错误") - return - - # 暗号提示 - guess_answer = os.getenv(GUESS_ANSWER_ENV, '') - if guess_answer: - answer_lines = [a.strip() for a in guess_answer.strip().split('\n') if a.strip()] - print(f"🔑 已设置暗号环境变量,共 {len(answer_lines)} 个暗号(按日期顺序对应)") - for i, a in enumerate(answer_lines): - print(f" 第{i+1}天: {a}") - else: - print(f"🔑 未设置暗号环境变量({GUESS_ANSWER_ENV}),将自动从API获取答案") - - random.shuffle(account_urls) - - print("=" * 60) - print(f"🎉 顺丰33周年活动 - 完成任务抽勋章") - print(f"👨‍💻 作者: 爱学习的呆子") - print(f"📱 共获取到 {len(account_urls)} 个账号") - print(f"⚙️ 并发数量: {CONCURRENT_NUM}") - print(f"⏰ 执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print("=" * 60) - - all_results = [] - - if CONCURRENT_NUM <= 1: - for idx, url in enumerate(account_urls): - result = run_account(url, idx) - all_results.append(result) - if idx < len(account_urls) - 1: - print("-" * 60) - time.sleep(2) - else: - with ThreadPoolExecutor(max_workers=CONCURRENT_NUM) as pool: - futures = {pool.submit(run_account, url, idx): idx for idx, url in enumerate(account_urls)} - for future in as_completed(futures): - all_results.append(future.result()) - - all_results.sort(key=lambda x: x['index']) - - # 汇总 - print(f"\n" + "=" * 70) - print(f"📊 周年活动汇总") - print("=" * 70) - print(f"{'序号':<6} {'手机号':<15} {'完成任务数':<12} {'抽到勋章数':<12} {'勋章详情':<20}") - print("-" * 70) - - total_tasks = 0 - total_medals = 0 - - for r in all_results: - idx = r['index'] + 1 - phone = r['phone'][:3] + "****" + r['phone'][7:] if r.get('phone') and len(r['phone']) >= 7 else r.get('phone', '未登录') - tasks = r.get('tasks_completed', 0) - medals = r.get('medals_claimed', 0) - detail = ', '.join([f"{d['type']}x{d['amount']}" for d in r.get('medals_detail', [])]) or '无' - status = "✅" if r['success'] else "❌" - - total_tasks += tasks - total_medals += medals - - print(f"{idx:<6} {phone:<15} {tasks:<12} {medals:<12} {detail:<20} {status}") - - print("-" * 70) - print(f"{'汇总':<6} {'账号: ' + str(len(all_results)):<15} {total_tasks:<12} {total_medals:<12}") - print("=" * 70) - print("\n🎊 所有账号执行完成!") - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/Points_Based/D_SFSY_SJBHD.py b/Points_Based/D_SFSY_SJBHD.py new file mode 100644 index 0000000..442d4b9 --- /dev/null +++ b/Points_Based/D_SFSY_SJBHD.py @@ -0,0 +1,816 @@ +# cron: 19 12,20 * * * +# const $ = new Env("顺丰2026世界杯活动"); +""" + +顺丰2026世界杯活动 - 射门游戏 + 比赛竞猜 + 每日礼物 +Author: Auto-generated (参考端午活动脚本) +Version: 1.2.0 +Date: 2026-07-06 + +更新日志: + 1.2.0 (2026-07-06) 优化邀请逻辑:支持列表内账号互相邀请,显示已邀请好友列表 + 1.1.0 (2026-07-04) 新增下注结算(settleBet)阶段、赔率显示、潜在收益/今日盈亏列 + 1.0.0 (2026-07-03) 初版:每日礼物 + 任务(白名单) + 射门游戏 + 比赛下注 + +活动周期: 2026-07-02 ~ 2026-07-20 +货币: GOLD_COIN (金币) +活动码: WORLD_CUP + +""" + +import hashlib +import json +import os +import random +import re +import time +from datetime import datetime +from typing import Dict, List, Optional, Any +from urllib.parse import unquote +from concurrent.futures import ThreadPoolExecutor, as_completed +from threading import Lock +import requests +from requests.packages.urllib3.exceptions import InsecureRequestWarning + +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + +inviteId = [] # 运行时从账号列表动态提取 + +PROXY_TIMEOUT = 15 +MAX_PROXY_RETRIES = 5 +REQUEST_RETRY_COUNT = 3 +CONCURRENT_NUM = int(os.getenv('SFBF', '1')) +if CONCURRENT_NUM > 20: + CONCURRENT_NUM = 20 +elif CONCURRENT_NUM < 1: + CONCURRENT_NUM = 1 + +# 代理开关(默认启用) +ENABLE_PROXY = os.getenv('ENABLE_PROXY', 'true').lower() == 'true' + +print_lock = Lock() + +ACTIVITY_CODE = "WORLD_CUP" +TOKEN = 'wwesldfs29aniversaryvdld29' +SYS_CODE = 'MCS-MIMP-CORE' + +CURRENCY_NAMES = {'GOLD_COIN': '金币'} +CHANNEL = '26sjbapp' +PLATFORM = 'SFAPP' +CITY_CODE = '021' + +# ===== 活动阶段开关(True=开启 / False=关闭)===== +ENABLE_DAILY_GIFT = True # 每日礼物 +ENABLE_TASKS = True # 任务(仅执行 AUTO_FINISH_TASK_CODES 白名单里的) +ENABLE_GAME = True # 射门游戏 +ENABLE_BET = True # 比赛竞猜(下注) +ENABLE_SETTLE = True # 下注结算(早上跑完后,结算前一天已开赛的下注盈亏) + +# ===== 下注参数 ===== +# 服务端最低下注金币为 10,< 10 会报 notSuccess。修改前请先抓包确认下限。 +BET_COIN = 10 # 每场比赛下注金币数 + +SKIP_TASK_TYPES = [ + 'BUY_ADD_VALUE_SERVICE_PACKET', + 'SEND_INTERNATIONAL_PACKAGE', + 'LOOK_BIG_PACKAGE_GET_CASH', + 'SEND_SUCCESS_RECALL', + 'CHARGE_NEW_EXPRESS_CARD', + 'CHARGE_COLLECT_ALL', + 'OPEN_FAMILY_HOME_MUTUAL', + 'INVITEFRIENDS_PARTAKE_ACTIVITY', + 'BROWSE_INTEGRAL_PLANET', + 'BROWSE_FAMILY_HOME_MUTUAL', +] +# 浏览类任务一律跳过(含 BROWSE_/VIEWPAGE_ 前缀),不发起 finishTask +SKIP_TASK_PREFIXES = ('BROWSE_', 'VIEWPAGE_') + +# 仅对这些 taskCode 直接调用 finishTask +AUTO_FINISH_TASK_CODES = { + '0CD7AFA68009402DBE5BF9D3C10D0115', # 浏览积分商城(验证通过) + '895011E183CE4E61BBACE8A63B98596A', # 去看看互寄8折权益(未验证,可移除) +} + + +class Logger: + def __init__(self): + self.messages: List[str] = [] + self.lock = Lock() + + def _log(self, icon: str, msg: str): + line = f"{icon} {msg}" + with print_lock: + print(line) + with self.lock: + self.messages.append(line) + + def info(self, msg): self._log('📝', msg) + def success(self, msg): self._log('✅', msg) + def warning(self, msg): self._log('⚠️', msg) + def error(self, msg): self._log('❌', msg) + def task(self, msg): self._log('🎯', msg) + def goal(self, msg): self._log('⚽', msg) + + +class ProxyManager: + def __init__(self, api_url: str): + self.api_url = api_url + + def get_proxy(self) -> Optional[Dict[str, str]]: + # 如果代理开关关闭,直接返回 None + if not ENABLE_PROXY: + return None + try: + if not self.api_url: + return None + response = requests.get(self.api_url, timeout=10) + if response.status_code == 200: + # 尝试解析 JSON + try: + data = response.json() + if 'data' in data and 'list' in data['data'] and data['data']['list']: + proxy_info = data['data']['list'][0] + ip = proxy_info.get('ip') + port = proxy_info.get('port') + if ip and port: + proxy = f'http://{ip}:{port}' + with print_lock: + print(f"✅ 获取代理: {proxy}") + return {'http': proxy, 'https': proxy} + except (json.JSONDecodeError, KeyError, IndexError): + # 不是 JSON,按纯文本处理(兼容旧格式) + proxy_text = response.text.strip() + if ':' in proxy_text: + proxy = proxy_text if proxy_text.startswith('http') else f'http://{proxy_text}' + display = proxy + if '@' in proxy: + parts = proxy.split('@') + display = f"http://***:***@{parts[-1]}" + with print_lock: + print(f"✅ 获取代理: {display}") + return {'http': proxy, 'https': proxy} + with print_lock: + print(f"❌ 获取代理失败: 无法解析代理数据") + return None + with print_lock: + print(f"❌ 获取代理失败: HTTP {response.status_code}") + return None + except Exception as e: + with print_lock: + print(f"❌ 获取代理异常: {str(e)[:100]}") + return None + + +class SFHttpClient: + def __init__(self, proxy_manager: ProxyManager): + self.proxy_manager = proxy_manager + self.session = requests.Session() + self.session.verify = False + + # 根据开关决定是否尝试获取代理 + if ENABLE_PROXY: + proxy = self.proxy_manager.get_proxy() + if proxy: + self.session.proxies = proxy + else: + if self.proxy_manager.api_url: + with print_lock: + print("⚠️ 代理获取失败,将不使用代理") + + self.headers = { + 'Host': 'mcs-mimp-web.sf-express.com', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 mediaCode=SFEXPRESSAPP-iOS-ML', + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'channel': CHANNEL, + 'platform': PLATFORM, + 'accept-language': 'zh-CN,zh;q=0.9', + } + + def _generate_sign(self) -> Dict[str, str]: + timestamp = str(int(round(time.time() * 1000))) + data = f'token={TOKEN}×tamp={timestamp}&sysCode={SYS_CODE}' + signature = hashlib.md5(data.encode()).hexdigest() + return { + 'sysCode': SYS_CODE, + 'timestamp': timestamp, + 'signature': signature, + } + + def request(self, url: str, data: Optional[Dict] = None, method: str = 'POST') -> Optional[Dict]: + retry_count = 0 + max_proxy_retries = MAX_PROXY_RETRIES if ENABLE_PROXY else 1 + proxy_retry_count = 0 + + while proxy_retry_count < max_proxy_retries: + sign_data = self._generate_sign() + headers = {**self.headers, **sign_data} + + try: + if method == 'POST': + resp = self.session.post(url, headers=headers, json=data or {}, timeout=PROXY_TIMEOUT) + else: + resp = self.session.get(url, headers=headers, timeout=PROXY_TIMEOUT) + resp.raise_for_status() + + try: + result = resp.json() + if result is None: + retry_count += 1 + if retry_count < REQUEST_RETRY_COUNT: + time.sleep(2) + continue + return None + return result + except (json.JSONDecodeError, ValueError): + retry_count += 1 + if retry_count < REQUEST_RETRY_COUNT: + time.sleep(2) + continue + return None + + except requests.exceptions.RequestException as e: + retry_count += 1 + error_str = str(e) + + if ENABLE_PROXY and ('ProxyError' in error_str or 'SSLError' in error_str or 'ConnectionError' in error_str): + proxy_retry_count += 1 + if proxy_retry_count < MAX_PROXY_RETRIES: + new_proxy = self.proxy_manager.get_proxy() + if new_proxy: + self.session.proxies = new_proxy + retry_count = 0 + time.sleep(2) + continue + + if retry_count < REQUEST_RETRY_COUNT: + time.sleep(2) + continue + return None + + except Exception: + return None + + return None + + def login(self, url: str) -> tuple: + try: + decoded_input = unquote(url) + if decoded_input.startswith('sessionId=') or '_login_mobile_=' in decoded_input: + cookie_dict = {} + for item in decoded_input.split(';'): + item = item.strip() + if '=' in item: + k, v = item.split('=', 1) + cookie_dict[k] = v + for k, v in cookie_dict.items(): + self.session.cookies.set(k, v, domain='mcs-mimp-web.sf-express.com') + user_id = cookie_dict.get('_login_user_id_', '') + phone = cookie_dict.get('_login_mobile_', '') + return (True, user_id, phone) if phone else (False, '', '') + else: + self.session.get(unquote(url), headers=self.headers, timeout=PROXY_TIMEOUT) + cookies = self.session.cookies.get_dict() + user_id = cookies.get('_login_user_id_', '') + phone = cookies.get('_login_mobile_', '') + return (True, user_id, phone) if phone else (False, '', '') + except Exception as e: + print(f'登录异常: {str(e)}') + return False, '', '' + + +class WorldCupExecutor: + BASE_URL = 'https://mcs-mimp-web.sf-express.com/mcs-mimp' + + def __init__(self, http: SFHttpClient, logger: Logger, user_id: str, invite_pool: list = None): + self.http = http + self.logger = logger + self.user_id = user_id + self.invite_pool = invite_pool or [] + self.level_list: List[Dict] = [] + + def _post(self, path: str, data: Optional[Dict] = None) -> Optional[Dict]: + url = f'{self.BASE_URL}{path}' + return self.http.request(url, data=data or {}) + + def get_activity_index(self) -> Optional[Dict]: + resp = self._post('/commonPost/~memberNonactivity~worldCupIndexService~index') + if resp and resp.get('success'): + return resp.get('obj', {}) + return None + + def get_game_index(self) -> Optional[Dict]: + resp = self._post('/commonPost/~memberNonactivity~worldCupGameService~index') + if resp and resp.get('success'): + return resp.get('obj', {}) + return None + + def get_bet_status(self) -> Optional[Dict]: + resp = self._post('/commonPost/~memberNonactivity~worldCupMatchService~betStatus') + if resp and resp.get('success'): + return resp.get('obj', {}) + return None + + def get_daily_gift_status(self) -> Optional[Dict]: + data = {"cityCode": CITY_CODE} + resp = self._post('/commonPost/~memberNonactivity~worldCupDailyService~getDailyGiftStatus', data) + if resp and resp.get('success'): + return resp.get('obj', {}) + return None + + def receive_daily_gift(self) -> Optional[Dict]: + data = {"cityCode": CITY_CODE} + resp = self._post('/commonPost/~memberNonactivity~worldCupDailyService~receiveDailyGift', data) + if resp and resp.get('success'): + return resp.get('obj', {}) + return None + + def get_task_list(self) -> Optional[List[Dict]]: + data = {"activityCode": ACTIVITY_CODE, "channelType": PLATFORM} + resp = self._post('/commonPost/~memberNonactivity~activityTaskService~taskList', data) + if resp and resp.get('success'): + return resp.get('obj', []) + return None + + def finish_task(self, task_code: str) -> bool: + url = f'{self.BASE_URL}/commonPost/~memberEs~taskRecord~finishTask' + data = {"taskCode": task_code} + resp = self.http.request(url, data=data) + return bool(resp and resp.get('success')) + + def fetch_task_reward(self) -> Optional[Dict]: + data = {"channelType": PLATFORM, "activityCode": ACTIVITY_CODE} + resp = self._post('/commonPost/~memberNonactivity~worldCupTaskService~fetchTaskReward', data) + if resp and resp.get('success'): + return resp.get('obj', {}) + return None + + def report_pass(self, level: int, shot_num: int) -> Optional[Dict]: + data = {"level": level, "shotNum": shot_num} + resp = self._post('/commonPost/~memberNonactivity~worldCupGameService~passReport', data) + if resp and resp.get('success'): + return resp.get('obj', {}) + err = resp.get('errorMessage', '未知错误') if resp else '请求失败' + self.logger.warning(f'第{level}关上报失败: {err}') + return None + + def place_bet(self, match_id: str, bet_result: int, bet_coin: int) -> bool: + """下注 + bet_result: 0=主胜 1=平 2=客胜 (与 odds 数组顺序一致) + bet_coin: 下注金币数 + """ + data = {"matchId": match_id, "betResult": bet_result, "betCoin": bet_coin} + resp = self._post('/commonPost/~memberNonactivity~worldCupMatchService~placeBet', data) + if resp and resp.get('success'): + return True + err = resp.get('errorMessage', '未知错误') if resp else '请求失败' + self.logger.warning(f'下注失败(match={match_id}, result={bet_result}): {err}') + return False + + def settle_bet(self) -> Optional[list]: + """结算下注 + 返回已开赛并结算的下注列表;空数组表示今天没有可结算的 + """ + resp = self._post('/commonPost/~memberNonactivity~worldCupMatchService~settleBet', {}) + if resp and resp.get('success'): + return resp.get('obj', []) or [] + err = resp.get('errorMessage', '未知错误') if resp else '请求失败' + self.logger.warning(f'结算请求失败: {err}') + return None + + # ========== 邀请相关方法 ========== + def do_invite(self): + """列表内账号互相邀请""" + try: + pool = self.invite_pool if self.invite_pool else inviteId + available_invites = [inv for inv in pool if inv != self.user_id] + if not available_invites: + self.logger.warning("没有可用的邀请对象(排除自身后为空),跳过邀请") + return + random_invite = random.choice(available_invites) + url = f'{self.BASE_URL}/commonPost/~memberNonactivity~worldCupIndexService~index' + data = {"inviteType": 1, "inviteUserId": random_invite} + resp = self.http.request(url, data=data) + if resp and resp.get('success'): + self.logger.success(f"已向邀请对象发起邀请 (userId: {random_invite})") + else: + err = resp.get('errorMessage', '未知错误') if resp else '请求失败' + self.logger.warning(f"邀请失败: {err}") + except Exception as e: + self.logger.error(f"邀请初始化异常: {str(e)}") + + def get_invite_list(self) -> Optional[List[Dict]]: + """查询已邀请好友列表""" + resp = self._post('/commonPost/~memberNonactivity~worldCupTaskService~taskInviteList', {}) + if resp and resp.get('success'): + return resp.get('obj', []) + err = resp.get('errorMessage', '未知错误') if resp else '请求失败' + self.logger.warning(f"查询邀请列表失败: {err}") + return None + + def do_show_invite_list(self) -> None: + """查询并显示已邀请好友列表""" + invite_list = self.get_invite_list() + if invite_list: + self.logger.success(f"已邀请 {len(invite_list)} 位好友:") + for i, friend in enumerate(invite_list, 1): + mobile = friend.get('mobile', '未知') + invite_date = friend.get('inviteDate', '未知') + # 脱敏显示 + if len(mobile) >= 7: + mobile_masked = mobile[:3] + "****" + mobile[7:] + else: + mobile_masked = mobile + self.logger.info(f" {i}. {mobile_masked} (邀请时间: {invite_date})") + else: + self.logger.info("暂无已邀请好友") + # ================================= + + def do_daily_gift(self, result: Dict) -> None: + self.logger.task('[每日礼物] 检查状态...') + status = self.get_daily_gift_status() + if status is None: + self.logger.warning('[每日礼物] 获取状态失败') + return + if status.get('received'): + self.logger.success('[每日礼物] 今日已领取') + return + if not status.get('canReceive'): + self.logger.info('[每日礼物] 今日不可领取') + return + self.logger.task('[每日礼物] 尝试领取...') + if self.receive_daily_gift(): + self.logger.success('[每日礼物] 领取成功') + result['daily_gift'] = True + else: + self.logger.warning('[每日礼物] 领取失败') + + def do_tasks(self, result: Dict) -> None: + self.logger.info('正在获取世界杯活动任务列表...') + tasks = self.get_task_list() + if tasks is None: + return + self.logger.info(f'共发现 {len(tasks)} 个任务') + + for task in tasks: + task_name = task.get('taskName', '未知') + task_type = task.get('taskType', '') + task_code = task.get('taskCode', '') + status = task.get('status') + rest_finish = task.get('restFinishTime', 0) + can_receive = task.get('canReceiveTokenNum', 0) + + if task_code not in AUTO_FINISH_TASK_CODES: + continue + + self.logger.info(f'[{task_name}]') + + if status == 3 or (status == 1 and rest_finish <= 0): + self.logger.success(f'[{task_name}] 已完成') + continue + + if self.finish_task(task_code): + self.logger.success(f'[{task_name}] 完成成功') + result['tasks_completed'] += 1 + else: + self.logger.warning(f'[{task_name}] 完成失败') + time.sleep(1) + + def do_fetch_rewards(self, result: Dict) -> None: + self.logger.info('领取任务奖励...') + reward_resp = self.fetch_task_reward() + if reward_resp: + received = reward_resp.get('receivedAccountList', []) + if received: + for item in received: + currency = item.get('currency', '') + amount = item.get('amount', 0) + self.logger.success(f'领取: {currency} x{amount}') + else: + self.logger.info('无新奖励可领取') + + def play_game(self, result: Dict) -> None: + self.logger.info('⚽ 开始挑战射门游戏...') + game_info = self.get_game_index() + if game_info is None: + self.logger.warning('获取游戏配置失败') + return + + self.level_list = game_info.get('levelList', []) + cur_stage = game_info.get('curStage', 1) + cur_level = game_info.get('curLevel', 1) + + if not self.level_list: + self.logger.warning('游戏关卡列表为空') + return + + self.logger.info(f'当前进度: 第{cur_stage}阶段 第{cur_level}关') + + for level_cfg in self.level_list: + level_no = level_cfg.get('level') + target = level_cfg.get('target', 0) + reward = level_cfg.get('rewardCoins', 0) + + if level_no < cur_level: + continue + + self.logger.task(f'挑战第{level_no}关(目标 {target} 次进球,奖励 {reward} 金币)') + + pass_result = self.report_pass(level_no, target) + + attempts = 0 + while attempts < 3 and (pass_result is None or not pass_result.get('coinNum', 0) >= reward): + time.sleep(1) + pass_result = self.report_pass(level_no, target) + attempts += 1 + + if pass_result: + result['stages_passed'].append({ + 'level': level_no, + 'coins': pass_result.get('coinNum', 0), + 'reward': reward, + }) + result['game_coins'] += pass_result.get('coinNum', 0) + self.logger.goal(f'第{level_no}关完成,获得 {pass_result.get("coinNum", 0)} 金币') + else: + self.logger.warning(f'第{level_no}关未通过') + + def do_bet(self, result: Dict) -> None: + self.logger.info('⚽ 查询比赛竞猜状态...') + bet_status = self.get_bet_status() + if bet_status is None: + return + + account = bet_status.get('currentAccount', {}) + balance = account.get('balance', 0) + self.logger.info(f'当前金币余额: {balance}') + + matches = bet_status.get('matchList', []) + pending = [ + m for m in matches + if m.get('matchStatus') == 'Fixture' and m.get('recordStatus', 0) == 0 + ] + self.logger.info(f'共 {len(matches)} 场比赛,{len(pending)} 场可下注') + self.logger.info(f'单场下注: {BET_COIN} 金币') + + if balance < BET_COIN: + self.logger.warning(f'余额不足 {BET_COIN} 金币,跳过下注') + result['final_balance'] = balance + result['final_total'] = account.get('totalAmount', 0) + return + + choice_map = {0: '主胜', 1: '平', 2: '客胜'} + + for m in pending: + match_id = m.get('matchId') + team_a = m.get('teamAName', '?') + team_b = m.get('teamBName', '?') + odds_str = m.get('odds', '?,?,?') + bet_result = random.randint(0, 2) + + try: + odds_list = [float(x) for x in odds_str.split(',')] + picked_odds = odds_list[bet_result] if bet_result < len(odds_list) else 0.0 + except (ValueError, IndexError): + picked_odds = 0.0 + + potential_payout = round(BET_COIN * picked_odds, 2) + potential_profit = round(potential_payout - BET_COIN, 2) + + self.logger.task( + f'下注: {team_a} vs {team_b}(赔率 {odds_str})→ ' + f'{choice_map[bet_result]} {BET_COIN} 金币 ' + f'| 命中得 {potential_payout}(净赚 {potential_profit})' + ) + + time.sleep(1) + if self.place_bet(match_id, bet_result=bet_result, bet_coin=BET_COIN): + self.logger.success( + f'下注成功: {team_a} vs {team_b}(赔率 {picked_odds},命中得 {potential_payout})' + ) + result['bets_placed'] += 1 + result['bet_details'].append({ + 'match': f'{team_a} vs {team_b}', + 'choice': choice_map[bet_result], + 'odds': picked_odds, + 'bet_coin': BET_COIN, + 'potential_payout': potential_payout, + 'potential_profit': potential_profit, + }) + else: + self.logger.warning(f'下注失败: {team_a} vs {team_b}') + + status_after = self.get_bet_status() + if status_after: + acc_after = status_after.get('currentAccount', {}) + result['final_balance'] = acc_after.get('balance', 0) + result['final_total'] = acc_after.get('totalAmount', 0) + else: + result['final_balance'] = balance + result['final_total'] = account.get('totalAmount', 0) + + def do_settle(self, result: Dict) -> None: + """结算已开赛下注的实际盈亏""" + self.logger.info('💰 结算昨日下注结果...') + settled = self.settle_bet() + if settled is None: + self.logger.warning('结算接口失败,跳过') + return + + if not settled: + self.logger.info('今日无新结算记录') + return + + choice_map = {0: '主胜', 1: '平', 2: '客胜'} + + total_pnl = 0 + wins = 0 + losses = 0 + + for m in settled: + match_id = m.get('matchId', '?') + team_a = m.get('teamAName', '?') + team_b = m.get('teamBName', '?') + score = f"{m.get('teamAScore', '?')}:{m.get('teamBScore', '?')}" + bet_coin = m.get('betCoin', 0) + bet_result = m.get('betResult', -1) + bet_odds_str = m.get('betOdds', '?,?,?') + win_coin = m.get('betWinCoin', 0) + + pnl = win_coin - bet_coin + total_pnl += pnl + + try: + odds_list = [float(x) for x in bet_odds_str.split(',')] + picked_odds = odds_list[bet_result] if 0 <= bet_result < len(odds_list) else 0.0 + except (ValueError, IndexError): + picked_odds = 0.0 + + if pnl > 0: + wins += 1 + icon = '🎉' + pnl_str = f'+{pnl}' + elif pnl < 0: + losses += 1 + icon = '💔' + pnl_str = f'{pnl}' + else: + icon = '➖' + pnl_str = '0' + + self.logger.info( + f' {icon} {team_a} vs {team_b}({score})→ ' + f'押{choice_map.get(bet_result, "?")} 赔率 {picked_odds} | ' + f'投 {bet_coin} 得 {win_coin} | {pnl_str}' + ) + + result['settle_details'].append({ + 'match': f'{team_a} vs {team_b}', + 'score': score, + 'choice': choice_map.get(bet_result, '?'), + 'bet_coin': bet_coin, + 'win_coin': win_coin, + 'pnl': pnl, + }) + + result['settle_pnl'] = total_pnl + result['settle_wins'] = wins + result['settle_losses'] = losses + + if total_pnl > 0: + self.logger.success(f'结算完成: {wins}胜{losses}负,净赚 +{total_pnl} 金币') + elif total_pnl < 0: + self.logger.warning(f'结算完成: {wins}胜{losses}负,净亏 {total_pnl} 金币') + else: + self.logger.info(f'结算完成: {wins}胜{losses}负,持平') + + status_after = self.get_bet_status() + if status_after: + acc_after = status_after.get('currentAccount', {}) + result['final_balance'] = acc_after.get('balance', 0) + result['final_total'] = acc_after.get('totalAmount', 0) + + def show_status(self) -> None: + self.logger.info('=' * 20 + ' 世界杯活动状态 ' + '=' * 20) + bet_status = self.get_bet_status() + if bet_status: + account = bet_status.get('currentAccount', {}) + self.logger.info(f'金币余额: {account.get("balance", 0)} / 累计: {account.get("totalAmount", 0)}') + + daily = self.get_daily_gift_status() + if daily: + status_str = '已领取' if daily.get('received') else ('可领取' if daily.get('canReceive') else '不可领取') + self.logger.info(f'每日礼物: {status_str}') + + game = self.get_game_index() + if game: + self.logger.info(f'游戏阶段: 第{game.get("curStage", 1)}阶段 第{game.get("curLevel", 1)}关') + + index = self.get_activity_index() + if index: + self.logger.info(f'活动周期: {index.get("acStartTime", "")} ~ {index.get("acEndTime", "")}') + self.logger.info(f'可领金币: {index.get("availableRewardCoins", 0)}') + + self.logger.info('=' * 56) + + def run(self) -> Dict[str, Any]: + result = { + 'tasks_completed': 0, + 'game_coins': 0, + 'stages_passed': [], + 'daily_gift': False, + 'bets_placed': 0, + 'bet_details': [], + 'settle_pnl': 0, + 'settle_wins': 0, + 'settle_losses': 0, + 'settle_details': [], + 'final_balance': 0, + 'final_total': 0, + } + + # 1. 邀请好友(先发起邀请) + self.do_invite() + + # 2. 查询并显示已邀请好友列表 + self.do_show_invite_list() + + # 3. 获取活动首页信息 + index_info = self.get_activity_index() + if index_info: + self.logger.success(f'已加入世界杯活动,可领 {index_info.get("availableRewardCoins", 0)} 金币') + + if ENABLE_DAILY_GIFT: + self.do_daily_gift(result) + if ENABLE_TASKS: + self.do_tasks(result) + self.do_fetch_rewards(result) + if ENABLE_GAME: + self.play_game(result) + if ENABLE_BET: + self.do_bet(result) + else: + self.logger.info('⚽ [开关] 比赛下注已关闭,跳过') + if ENABLE_SETTLE: + self.do_settle(result) + else: + self.logger.info('💰 [开关] 下注结算已关闭,跳过') + + return result + + +def run_account(account_url: str, index: int, invite_pool: List[str]) -> Dict[str, Any]: + logger = Logger() + proxy_url = os.getenv('SF_PROXY_API_URL', '') + proxy_manager = ProxyManager(proxy_url) + + http = SFHttpClient(proxy_manager) + retry_count = 0 + login_success = False + phone = '' + user_id = '' + + while retry_count < MAX_PROXY_RETRIES and not login_success: + try: + if retry_count > 0: + http = SFHttpClient(proxy_manager) + success, user_id, phone = http.login(account_url) + if success: + login_success = True + break + except Exception: + pass + retry_count += 1 + if retry_count < MAX_PROXY_RETRIES: + time.sleep(2) + + if not login_success: + logger.error(f'账号{index + 1} 登录失败') + return { + 'success': False, 'phone': '', 'index': index, + 'tasks_completed': 0, 'game_coins': 0, 'stages_passed': [], + 'daily_gift': False, 'bets_placed': 0, 'bet_details': [], + 'settle_pnl': 0, 'settle_wins': 0, 'settle_losses': 0, 'settle_details': [], + 'final_balance': 0, 'final_total': 0, + } + + masked_phone = phone[:3] + "****" + phone[7:] if len(phone) >= 7 else phone + logger.success(f'账号{index + 1}: 【{masked_phone}】登录成功') + + time.sleep(random.uniform(1, 3)) + + executor = WorldCupExecutor(http, logger, user_id, invite_pool) + activity_result = executor.run() + + return { + 'success': True, + 'phone': phone, + 'index': index, + **activity_result, + } + + +def main(): + # 收集所有 sfsyUrl 相关环境变量(兼容青龙面板同名多变量) + account_urls = [] + invite_pool = [] # \ No newline at end of file