diff --git a/Points_Based/ZGLT_Loader_1.0.9.py b/Points_Based/ZGLT_Loader_1.0.9.py new file mode 100644 index 0000000..4b1b8d6 --- /dev/null +++ b/Points_Based/ZGLT_Loader_1.0.9.py @@ -0,0 +1,7061 @@ +# -*- coding: utf-8 -*- +""" +中国联通 Python 版 v1.0.9 + +包含以下功能: +1. 首页签到 (话费红包/积分) +2. 联通祝福 (各类抽奖) +3. 天天领现金 (每日打卡/立减金) +4. 权益超市 (任务/抽奖/浇水/领奖/全局库存缓存) +5. 安全管家 (日常任务/积分领取) +6. 联通云盘 (签到/AI互动/文件上传/抽奖活动/重复清理) +7. 联通阅读 (自动获取书籍/心跳阅读/抽奖/查红包) +8. 联通爱听 (积分任务/自动签到/阅读挂机/分享任务) +9. 沃云手机 (签到/任务/抽奖) +10. 区域专区 (自动识别安徽超级星期五/辽宁福利魔方/新疆/河南/云南执行特有任务) + +更新说明: + +### 20260430 +v1.0.9: +- 云盘:新增测速抽奖与多账号组队。 +- 云盘:新增抽奖记录查询,优化推送内容。 +- 云盘:移除过期拼图、家乡活动。 +- 推送:新增通知开关。 + +### 20260426 +v1.0.8: +- 通通乡村:新增任务模块,支持签到、浏览、垃圾分类与农场任务。 +- 通通乡村:补充农场新手任务,防卡死处理。 +- 云智手机:更新活动code。 + +### 20260331 +v1.0.7: +- 区域专区:新增安徽联通"超级星期五"抢红包,支持自定义面额。 +- 联通爱听:修复积分弹窗空响应导致脚本崩溃。 + +### 20260326 +v1.0.6: +- 区域专区:新增辽宁联通"福利魔方"自动化签到与资产明细展示。 +- 安全管家:重构接入最新多类拦截选项及助手安全积分获取通道。 +- 权益超市:增加查抢话费记录 `receiveTime` 空值防报错处理。 + +配置说明: +1. 账号变量 (chinaUnicomCookie): + 赋值方式有三种: + a. 填账号密码 (自动获取Token - 推荐): + export chinaUnicomCookie="18600000000#123456" + b. 填Token#AppId (免密模式 - 推荐): + export chinaUnicomCookie="a3e4c1ff2xxxxxxxxx#912d30xxxxxx" + c. 仅填Token (旧模式): + export chinaUnicomCookie="a3e4c1ff2xxxxxxxxx" + (多账号用 & 或 换行 隔开) + +2. 代理设置 (可选): + export UNICOM_PROXY_API="你的代理提取链接" (支持 JSON/TXT 格式,自动识别) + export UNICOM_PROXY_TYPE="socks5" (可选 http 或 socks5,默认 socks5) + +3. 特殊功能设置: + export UNICOM_GRAB_AMOUNT="5" : (可选) 抢兑面额 (默认5,自动匹配含"5元"或"5话费"的奖品) + export UNICOM_TEST_MODE="query" : (可选) 仅查询模式,跳过任务执行只查询资产 + export UNICOM_AH_FRIDAY_AMOUNT="50" : (可选) 安徽超级星期五抢红包面额 (如50=抢50元红包, 不填则不执行) + +定时规则建议 (Cron): +0 58 9,17 * * * (抢兑专用: 需 sign_config.run_grab_coupon=True,建议提前2分钟启动,脚本自动精准等待) +0 58 9 * * 5 (安徽超级星期五: 需设置 UNICOM_AH_FRIDAY_AMOUNT,每周五9:58启动) +0 7,20 * * * (推荐:每天早晚7点/20点各跑一次,覆盖绝大部分签到任务) + +From: YaoHuo8648 +Email: zheyizzf@188.com +Update: 2026.04.30 +""" +import os +import sys +import json +import time +import random +import re +import hashlib +import hmac +import base64 +import logging +import requests +import uuid +import string +import tempfile +from datetime import datetime +try: + sys.stdout.reconfigure(encoding='utf-8') +except: + pass +from urllib.parse import urlparse, parse_qs, urlencode, unquote, quote +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry +from Crypto.Cipher import AES, PKCS1_v1_5 +from Crypto.PublicKey import RSA +from Crypto.Util.Padding import pad, unpad +# ======================================== +# 全局配置 (globalConfig) +# true=开启, false=关闭 +# ======================================== +globalConfig = { + # --- 1. 功能总开关 (True=开启, False=关闭) --- + "enable_sign": True, # 首页签到 (🔺总开关, 含签到/任务/抢话费券) + "enable_ttlxj": True, # 天天领现金 + "enable_ttxc": True, # 通通乡村 + "enable_ltzf": True, # 联通祝福 + "enable_woread": False, # 联通阅读 + "enable_security": True, # 安全管家 + "enable_ltyp": True, # 联通云盘 + "enable_market": True, # 权益超市 (🔺总开关, 必须开启内部功能才能运行) + "enable_aiting": True, # 联通爱听 + "enable_wostore": True, # 沃云手机 + "enable_regional": True, # 区域专区 + "enable_notify": True, # 推送通知 + + # --- ✅ 签到区内部细分开关 --- + "sign_config": { + "run_grab_coupon": False, # False = 关闭抢话费券 (True=开启抢兑, 需配合 UNICOM_GRAB_AMOUNT 设置面额) + }, + + # --- 🛒 权益超市内部细分开关 (按需修改到这里) --- + "market_config": { + "run_water": True, # False = 关闭浇水 + "run_task": True, # False = 关闭做任务(浏览/分享) + "run_member_center": True, # False = 关闭浏览会员中心得积分 + "run_draw": True, # True = 开启抽奖 + "run_claim": True, # True = 开启自动领奖(建议开启, 不领白不领) + }, + + # --- 🏷️ 区域专区内部细分开关 --- + "regional_config": { + "run_ah_friday": True, # True = 开启安徽超级星期五 (需配合 UNICOM_AH_FRIDAY_AMOUNT 设置面额) + }, + + # --- 2. 设备ID配置 --- + "refresh_device_id": False, # False:使用缓存ID, True:强制刷新 +} +COMMON_CONSTANTS = { + "UA": "Dalvik/2.1.0 (Linux; U; Android 12; Mi 10 Pro MIUI/21.11.3);unicom{version:android@11.0802}", + "MARKET_UA": "Dalvik/2.1.0 (Linux; U; Android 12; Mi 10 Pro MIUI/21.11.3);unicom{version:android@11.0802}", + "MARKET_H5_UA": "Mozilla/5.0 (Linux; Android 10; MI 8 Build/QKQ1.190828.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/143.0.7499.146 Mobile Safari/537.36; unicom{version:android@11.0802,desmobile:0};devicetype{deviceBrand:Xiaomi,deviceModel:MI 8}", + "APP_VERSION": "android@11.0802", +} +MARKET_MEMBER_CENTER_PAGE_ID = "s782351687947921408" +MARKET_MEMBER_CENTER_DISTRIBUTE_ID = "D1161369893988319232" +MARKET_MEMBER_CENTER_PARTNERS_ID = "1703" +MARKET_MEMBER_CENTER_CLIENT_TYPE = "marketUnicom" +MARKET_MEMBER_CENTER_TASK_CODE = "s769153426294495232" +XJ_ACTIVITY_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +XJ_ACTIVITY_YEAR = os.environ.get("XJ_ACTIVITY_YEAR", str(datetime.now().year)) +XJ_ACTIVITY_MONTH = os.environ.get("XJ_ACTIVITY_MONTH", XJ_ACTIVITY_MONTHS[datetime.now().month - 1]) +XJ_ACTIVITY_ID = f"{XJ_ACTIVITY_MONTH}{XJ_ACTIVITY_YEAR}Act" +XJ_MONTHLY_DRAW_ATTEMPT_COUNT = max(int(os.environ.get("UNICOM_ATTEMPT_COUNT", "1") or "1"), 1) +XJ_USER_AGENT = os.environ.get( + "XJ_USER_AGENT", + "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 " + "(KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0701};ltst;OSVersion/16.2" +) +UNICOM_CLOUD_UPLOAD_TIMEOUT = int(os.environ.get("UNICOM_CLOUD_UPLOAD_TIMEOUT", "120") or "120") +UNICOM_CLOUD_UPLOAD_PROGRESS_BYTES = int(os.environ.get("UNICOM_CLOUD_UPLOAD_PROGRESS_BYTES", "6376590") or "6376590") +WOCARE_CONSTANTS = { + "serviceLife": "wocareMBHServiceLife1", + "anotherApiKey": "beea1c7edf7c4989b2d3621c4255132f", + "anotherEncryptionKey": "f4cd4ffeb5554586acf65ba7110534f5", + "minRetries": "1" +} +WOCARE_ACTIVITIES = [ + {"name": "星座配对", "id": 2}, + {"name": "大转盘", "id": 3}, + {"name": "盲盒抽奖", "id": 4} +] +AITING_BASE_URL = "https://pcc.woread.com.cn" +AITING_SIGN_KEY_APPKEY = "7ZxQ9rT3wE5sB2dF" +AITING_SIGN_KEY_API = "woread!@#qwe1234" +AITING_SIGN_KEY_REQUERTID = "46iCw24ewAZbNkK6" +AITING_CLIENT_KEY = "1" +AITING_AES_KEY = "j2K81755sxV12wFx" +AITING_AES_IV = "16-Bytes--String" +WOREAD_KEY = "woreadst^&*12345" +ADDREADTIME_AES_KEY = "UNS#READDAY39COM" +YUNNAN_LIFE_BASE_URL = "https://wsm.wx.yn10010.com" +YUNNAN_LIFE_ACT_ID = "47191519589909" +YUNNAN_LIFE_SIGN_SALT = "ltynsh@sd23kjkgj2mbnfa0" +YUNNAN_LIFE_ACCESS_KEY = "ltynsh" +YUNNAN_LIFE_TO_URL = "https://wsm.wx.yn10010.com/micropage/orderPages/newYear/2025newYearsDay?channelId=1001010" +YUNNAN_LIFE_TASKS = [ + {"taskName": "每日签到", "taskCode": "DAILY_SIGN"}, + {"taskName": "浏览年终大回馈,好礼多多", "taskCode": "BROWSE_5TOWNS"}, +] +TTXC_BASE_URL = "https://epay.10010.com/cu-ca-game-front" +TTXC_APP_BASE_URL = "https://epay.10010.com/cu-ca-app-front" +TTXC_CHANNEL = "225" +TTXC_REFERER = "https://epay.10010.com/cu-ca-game-web/index.html?channel=qdqp" +TTXC_GARBAGE_WAIT_SECONDS = int(os.environ.get("UNICOM_TTXC_GARBAGE_WAIT", "28") or "28") +TTXC_GROW_MAX_CHARGE_PER_LAND = int(os.environ.get("UNICOM_TTXC_GROW_MAX_CHARGE_PER_LAND", "20") or "20") +TTXC_HARVEST_WAIT_SECONDS = int(os.environ.get("UNICOM_TTXC_HARVEST_WAIT", "3") or "3") +TTXC_NEWBIE_STEPS = ["G01", "G02", "G03", "G03_2", "G04", "G05", "G09", "G10", "G11", "G12"] +GRAB_AMOUNT = os.environ.get("UNICOM_GRAB_AMOUNT", "5") +AH_FRIDAY_AMOUNT = os.environ.get("UNICOM_AH_FRIDAY_AMOUNT", "") +AH_FRIDAY_BASE_URL = "http://123.138.11.116:8080" +AH_FRIDAY_SECKILL_TIMES = int(os.environ.get("UNICOM_AH_FRIDAY_TIMES", "50") or "50") +AH_FRIDAY_INTERVAL = float(os.environ.get("UNICOM_AH_FRIDAY_INTERVAL", "0.3") or "0.3") +WOSTORE_CLOUD_ACTIVITY_CODE = "HD2026033000125" +WOSTORE_CLOUD_TIMEOUT = int(os.environ.get("UNICOM_WOSTORE_TIMEOUT", "15") or "15") +WOSTORE_CLOUD_RETRIES = int(os.environ.get("UNICOM_WOSTORE_RETRIES", "3") or "3") +CLOUD_SPEED_ACTIVITY_ID = "Mjc=" +CLOUD_SPEED_TEAM_CONTEXT = {} +UNICOM_TOKEN_CACHE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "unicom_token_cache.json") +if "UNICOM_PROXY_API" not in os.environ: + os.environ.pop("http_proxy", None) + os.environ.pop("https_proxy", None) + os.environ.pop("HTTP_PROXY", None) + os.environ.pop("HTTPS_PROXY", None) +LOGIN_PUB_KEY = """-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDc+CZK9bBA9IU+gZUOc6FUGu7yO9WpTNB0PzmgFBh96Mg1WrovD1oqZ+eIF4LjvxKXGOdI79JRdve9NPhQo07+uqGQgE4imwNnRx7PFtCRryiIEcUoavuNtuRVoBAm6qdB0SrctgaqGfLgKvZHOnwTjyNqjBUxzMeQlEC2czEMSwIDAQAB +-----END PUBLIC KEY-----""" + +def mask_str(s): + try: + s = str(s) + if len(s) == 11 and s.isdigit(): + return s[:3] + "****" + s[7:] + elif s.startswith("enc_"): + return s + elif len(s) > 11: + return s[:6] + "******" + s[-6:] + return s + except: + return s + + +def safe_int(value, default=0): + try: + return int(str(value).strip()) + except Exception: + return default + +class FailoverSession: + """包装 requests.Session,自动为所有请求添加代理故障转移""" + RETRIABLE_KEYWORDS = ("Max retries exceeded", "timed out", "connection", "SOCKS", "ProxyError", "ConnectionError") + + def __init__(self, session, owner): + self._session = session + self._owner = owner # UserService 实例引用 + + def __getattr__(self, name): + return getattr(self._session, name) + + def _should_failover(self, err_msg): + if not os.environ.get("UNICOM_PROXY_API"): + return False + err_lower = err_msg.lower() + return any(kw.lower() in err_lower for kw in self.RETRIABLE_KEYWORDS) + + def _has_streaming_payload(self, kwargs): + if kwargs.get("files"): + return True + data = kwargs.get("data") + return hasattr(data, "read") + + def request(self, method, url, **kwargs): + try: + return self._session.request(method, url, **kwargs) + except Exception as e: + if self._should_failover(str(e)): + self._owner.log(f"⚠️ [自动故障转移] {url} 请求异常: {e}") + self._owner.failover_proxy() + if self._has_streaming_payload(kwargs): + raise + return self._session.request(method, url, **kwargs) + raise + + def get(self, url, **kwargs): + return self.request("GET", url, **kwargs) + + def post(self, url, **kwargs): + return self.request("POST", url, **kwargs) + +class UserService: + def __init__(self, index, config_str): + self.index = index + self.valid = False + self.notify_logs = [] + raw_session = requests.Session() + import socket + + class SourceAddressAdapter(HTTPAdapter): + + def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs): + pool_kwargs['source_address'] = ('0.0.0.0', 0) + super(SourceAddressAdapter, self).init_poolmanager(connections, maxsize, block, **pool_kwargs) + + def get_connection(self, url, proxies=None): + return super(SourceAddressAdapter, self).get_connection(url, proxies) + retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504]) + adapter = SourceAddressAdapter(max_retries=retries) + raw_session.mount('http://', adapter) + raw_session.mount('https://', adapter) + raw_session.headers.update({ + "User-Agent": COMMON_CONSTANTS["UA"], + "Connection": "keep-alive" + }) + raw_session.verify = False + import urllib3 + urllib3.disable_warnings() + self.session = FailoverSession(raw_session, self) + self.account_mobile = "" + self.mobile = "" + self.account_password = "" + self.token_online = "" + self.token_refresh = "" + self.cookie = "" + self.appId = "" + self.city_info = [] + self.last_read_submission_time = 0 + if globalConfig.get("refresh_device_id", False): + self.uuid = str(uuid.uuid4()).replace('-', '') + else: + self.uuid = os.environ.get("chinaUnicomUuid") or str(uuid.uuid4()).replace('-', '') + self.unicomTokenId = self.random_string(32) + self.tokenId_cookie = "chinaunicom-" + self.random_string(32, string.ascii_uppercase + string.digits) + self.ecs_token = "" + self.rptId = "" + self.ttxc_newbie_list = None + self.ttxc_nick_name = "" + self.sec_ai_share_key = "" + self.sec_share_task_code = "" + self.sec_share_task_name = "联通助理-分享AI助手对话" + self.sec_pending_claim_tasks = {} + self.init_account(config_str) + + def _parse_proxy_response(self, text): + """解析代理API响应,支持JSON和文本格式,提取ip/port/user/pass""" + text = text.strip() + + def extract(d): + if not d or not d.get('ip') or not d.get('port'): + return None + return { + 'ip': str(d['ip']), + 'port': int(d['port']), + 'user': str(d.get('account') or d.get('user') or ''), + 'pass': str(d.get('password') or d.get('pass') or '') + } + try: + json_start = text.find('{') + json_end = text.rfind('}') + if json_start != -1 and json_end != -1: + data = json.loads(text[json_start:json_end + 1]) + if data.get('ip') and data.get('port'): + return extract(data) + if data.get('data'): + inner = data['data'] + if isinstance(inner, dict) and inner.get('list') and isinstance(inner['list'], list) and len(inner['list']) > 0: + return extract(inner['list'][0]) + if isinstance(inner, list) and len(inner) > 0: + return extract(inner[0]) + if isinstance(inner, dict) and inner.get('ip'): + return extract(inner) + if data.get('result') and isinstance(data['result'], dict) and data['result'].get('ip'): + return extract(data['result']) + except: + pass + m = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[:\s\t]+(\d{1,5})', text) + if m: + return {'ip': m.group(1), 'port': int(m.group(2)), 'user': '', 'pass': ''} + return None + + def configure_proxy(self): + proxy_api = os.environ.get("UNICOM_PROXY_API") + if not proxy_api: + return + proxy_type = os.environ.get("UNICOM_PROXY_TYPE", "socks5").lower() + max_retries = 5 + for attempt in range(1, max_retries + 1): + try: + if attempt > 1: + self.log(f"🔄 [第{attempt}次] 重试获取代理IP ({proxy_type})...") + time.sleep(2) + else: + self.log(f"正在获取代理IP (模式: {proxy_type})...") + res = requests.get(proxy_api, timeout=10) + if res.status_code != 200: + self.log(f"⚠️ 获取代理失败: HTTP {res.status_code}") + continue + proxy_info = self._parse_proxy_response(res.text) + if not proxy_info: + preview = res.text[:100] + "..." if len(res.text) > 100 else res.text + self.log(f"❌ 提取失败: 无法识别代理格式 (内容: {preview})") + continue + ip, port = proxy_info['ip'], proxy_info['port'] + user, pwd = proxy_info['user'], proxy_info['pass'] + if user and pwd: + proxy_url = f"{proxy_type}://{quote(user)}:{quote(pwd)}@{ip}:{port}" + log_msg = f"{proxy_type}://***:***@{ip}:{port}" + else: + proxy_url = f"{proxy_type}://{ip}:{port}" + log_msg = proxy_url + self.log(f"🔍 提取成功: {log_msg}") + test_proxies = {"http": proxy_url, "https": proxy_url} + try: + requests.get("https://www.baidu.com", proxies=test_proxies, timeout=3) + self.session.proxies.update(test_proxies) + self.log("✅ 代理连通性测试通过") + return + except Exception as te: + self.log(f"⚠️ 代理测试失败: {te}") + except Exception as e: + self.log(f"❌ 请求代理API异常: {e}") + self.log(f"🚫 重试{max_retries}次均失败,回退至本地IP") + + def failover_proxy(self): + proxy_api = os.environ.get("UNICOM_PROXY_API") + if not proxy_api: + return False + self.log("⚠️ [故障转移] 检测到网络不稳定,正在检查当前代理是否存活...") + try: + requests.get("https://www.baidu.com", proxies=self.session.proxies, timeout=3) + self.log("✅ [故障转移] 经测试当前IP仍有效,继续复用,暂不提取新IP。") + time.sleep(1) + return True + except Exception as e: + self.log(f"❌ [故障转移] 当前代理已失效 ({e}),准备更换新IP...") + time.sleep(2) + self.configure_proxy() + return True + + def init_account(self, config_str): + parts = config_str.split('#') + if len(parts) >= 2 and len(parts[0]) == 11 and parts[0].isdigit() and len(parts[1]) < 50: + self.account_mobile = parts[0] + self.account_password = parts[1] + else: + self.token_online = parts[0].strip() + if len(self.token_online) == 11 and self.token_online.isdigit(): + self.account_mobile = self.token_online + self.token_online = "" # Reset, allow load_token_from_cache to fill it + self.log(f"识别到纯手机号模式: {mask_str(self.account_mobile)}") + if len(parts) > 1: + self.appId = parts[1].strip() + if len(parts) > 2 and parts[2]: + potential_mobile = parts[2].strip() + if potential_mobile.isdigit() and len(potential_mobile)==11: + self.account_mobile = potential_mobile + self.unicomTokenId = str(uuid.uuid4()).replace('-', '') # simplified + self.tokenId_cookie = "chinaunicom-" + str(uuid.uuid4()).replace('-', '').upper() # simplified + self.cookie_string = f"TOKENID_COOKIE={self.tokenId_cookie}; UNICOM_TOKENID={self.unicomTokenId}; sdkuuid={self.unicomTokenId}" + self.update_session_cookies() + + def update_session_cookies(self): + if self.cookie_string: + cookies = {} + for item in self.cookie_string.split(';'): + if '=' in item: + k, v = item.split('=', 1) + cookies[k.strip()] = v.strip() + self.session.cookies.update(cookies) + extra_cookies = {} + if self.token_online: + extra_cookies['token_online'] = self.token_online + if self.appId: + extra_cookies['appId'] = self.appId + if extra_cookies: + self.session.cookies.update(extra_cookies) + + def log(self, msg, notify=False): + prefix = f"账号[{self.index}]" + full_msg = f"{prefix}{msg}" + log_line = f"[{datetime.now().strftime('%H:%M:%S')}] {full_msg}" + print(log_line) + if notify: + self.notify_logs.append(str(msg)) + + def request_direct(self, method, url, **kwargs): + session = requests.Session() + session.trust_env = False + session.verify = False + try: + return session.request(method, url, **kwargs) + finally: + session.close() + + def rsa_encrypt(self, val): + self.log(f"正在进行 RSA 加密...") + try: + random_str = ''.join(str(random.randint(0, 9)) for _ in range(6)) + text = str(val) + random_str + data = text.encode('utf-8') + key_pem = LOGIN_PUB_KEY.encode() + recipient_key = RSA.import_key(key_pem) + cipher_rsa = PKCS1_v1_5.new(recipient_key) + enc_data = cipher_rsa.encrypt(data) + return base64.b64encode(enc_data).decode('utf-8') + except Exception as e: + self.log(f"RSA加密失败: {str(e)}") + return "" + + def generate_appid(self): + + def rnd(): return str(random.randint(0, 9)) + return (f"{rnd()}f{rnd()}af" + f"{rnd()}{rnd()}ad" + f"{rnd()}912d306b5053abf90c7ebbb695887bc" + f"870ae0706d573c348539c26c5c0a878641fcc0d3e90acb9be1e6ef858a" + f"59af546f3c826988332376b7d18c8ea2398ee3a9c3db947e2471d32a49") + rnd() + rnd() + + def unicom_login(self): + self.log(f"正在使用账号 {mask_str(self.account_mobile)} 进行登录...") + if not self.appId: + self.appId = self.generate_appid() + self.log(f"生成临时 AppId: {self.appId[:15]}...") + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + try: + payload = { + "version": COMMON_CONSTANTS["APP_VERSION"], + "mobile": self.rsa_encrypt(self.account_mobile), + "reqtime": timestamp, + "deviceModel": "Android", + "netWay": "Wifi", + "isR4": "0", + "password": self.rsa_encrypt(self.account_password), + "appId": self.appId + } + url = "https://m.client.10010.com/mobileService/login.htm" + res = self.session.post(url, data=payload) + result = res.json() + if result.get('code') in ['0', '0000']: + if result.get('token_online'): + self.token_online = result['token_online'] + self.log("✅ 登录接口验证通过") + return True + else: + self.log("❌ 登录响应中未找到 token_online") + else: + self.log(f"❌ 登录失败: {result.get('desc')} (Code: {result.get('code')})") + except Exception as e: + self.log(f"❌ 登录过程异常: {str(e)}") + return False + + def request(self, method, url, **kwargs): + try: + current_cookies = self.session.cookies.get_dict() + if self.cookie_string: + for item in self.cookie_string.split(';'): + if '=' in item: + k, v = item.split('=', 1) + current_cookies[k.strip()] = v.strip() + cookie_header = "; ".join([f"{k}={v}" for k, v in current_cookies.items()]) + if cookie_header: + if 'headers' not in kwargs: + kwargs['headers'] = {} + kwargs['headers']['Cookie'] = cookie_header + timeout = kwargs.get('timeout', 10) + if 'timeout' in kwargs: del kwargs['timeout'] + response = self.session.request(method, url, timeout=timeout, **kwargs) + if response.status_code >= 400: + self.log(f"请求 {url} 返回状态码 {response.status_code}") + return response + except Exception as e: + self.log(f"请求 {url} 异常: {str(e)}") + return None + + def load_token_from_cache(self): + if not self.account_mobile: + return False + if not os.path.exists(UNICOM_TOKEN_CACHE_PATH): + return False + try: + with open(UNICOM_TOKEN_CACHE_PATH, 'r', encoding='utf-8') as f: + cache = json.load(f) + user_cache = cache.get(self.account_mobile) + if user_cache and user_cache.get('token_online'): + if (datetime.now().timestamp() * 1000) - user_cache.get('timestamp', 0) < 12 * 60 * 60 * 1000: + self.token_online = user_cache['token_online'] + self.appId = user_cache.get('appId', self.appId) + self.city_info = user_cache.get('city_info', []) + self.update_session_cookies() + self.log(f"♻️ [缓存复用] 成功加载本地 Token ({user_cache.get('time')})") + return True + except Exception as e: + pass + return False + + def save_token_to_cache(self): + if not self.account_mobile: + return + cache = {} + if os.path.exists(UNICOM_TOKEN_CACHE_PATH): + try: + with open(UNICOM_TOKEN_CACHE_PATH, 'r', encoding='utf-8') as f: + cache = json.load(f) + except: pass + now = datetime.now() + cache[self.account_mobile] = { + "token_online": self.token_online, + "appId": self.appId, + "city_info": getattr(self, 'city_info', []), + "cookieString": "", + "timestamp": int(now.timestamp() * 1000), + "time": now.strftime('%Y-%m-%d %H:%M:%S') + } + try: + with open(UNICOM_TOKEN_CACHE_PATH, 'w', encoding='utf-8') as f: + json.dump(cache, f, indent=2, ensure_ascii=False) + self.log("💾 [缓存保存] Token 已写入本地文件") + except Exception as e: + self.log(f"❌ 保存缓存失败: {str(e)}") + + def get_city_info(self): + try: + url = "https://m.client.10010.com/mobileService/business/get/getCity" + res = self.session.post(url, data={}, timeout=10).json() + if res.get('code') == '200' and res.get('list'): + self.city_info = res.get('list') + return True + return False + except: + return False + + def queryRemain(self): + try: + if not self.ecs_token: + if not self.onLine(): + self.log("❌ 无法获取 ecs_token,跳过查询") + return + self.log("==== 资产查询 ====") + self.log("正在查询套餐余量...") + url = "https://m.client.10010.com/servicequerybusiness/balancenew/accountBalancenew.htm" + headers = { + "User-Agent": COMMON_CONSTANTS["MARKET_UA"], + "Cookie": f"ecs_token={self.ecs_token}" + } + res = self.request("get", url, headers=headers) + if not res: return + result = res.json() + if result.get('code') == '0000': + current_balance = "0.00" + real_time_fee = "0.00" + if result.get('curntbalancecust'): + current_balance = str(result['curntbalancecust']) + if result.get('realfeecust'): + real_time_fee = str(result['realfeecust']) + self.log(f"💰 [资产-话费] 当前余额: {current_balance}元, 实时话费: {real_time_fee}元", notify=True) + pkg_list = result.get('realTimeFeeSpecialFlagThree', []) + if pkg_list and isinstance(pkg_list, list): + self.log(f" 📋 [套餐详情]:", notify=True) + for item in pkg_list: + sub_items = item.get('subItems', []) + if sub_items: + for sub in sub_items: + bill = sub.get('bill', {}) + if bill: + name = bill.get('integrateitem', '未知项') + fee = bill.get('realfee', '0.00') + self.log(f" - {name}: {fee}元", notify=True) + else: + msg = result.get('desc') or result.get('msg') or "未知错误" + self.log(f"套餐余量查询失败: {msg}") + except Exception as e: + self.log(f"queryRemain 异常: {str(e)}") + + def onLine(self): + if not self.token_online: + self.log("❌ 缺少 token_online,无法执行 onLine") + return False + try: + url = "https://m.client.10010.com/mobileService/onLine.htm" + data = { + 'isFirstInstall': '1', + 'netWay': 'Wifi', + 'version': 'android@11.0000', + 'token_online': self.token_online, + 'provinceChanel': 'general', + 'deviceModel': 'ALN-AL10', + 'step': 'dingshi', + 'androidId': '291a7deb1d716b5a', + 'reqtime': int(time.time() * 1000) + } + if self.appId: + data['appId'] = self.appId + res = self.request('post', url, data=data) + if not res: return False + result = res.json() + code = result.get('code') + if code == '0' or code == 0: + self.valid = True + desmobile = result.get('desmobile', '') + if len(desmobile) == 11 and desmobile.isdigit(): + self.account_mobile = desmobile + self.mobile = desmobile + elif desmobile.startswith("enc_"): + if not self.account_mobile: + self.log("⚠️ 注意: 服务端返回了加密手机号且未配置本地手机号") + self.log("登录成功") + self.city_info = result.get('list', []) + self.ecs_token = result.get('ecs_token') + return True + else: + self.log(f"登录失败[{code}]: {result.get('msg')}") + return False + except Exception as e: + self.log(f"onLine 异常: {str(e)}") + return False + + def gettaskip(self): + orderId = self.random_string(32).upper() + try: + url = "https://m.client.10010.com/taskcallback/topstories/gettaskip" + data = { + "mobile": self.account_mobile, + "orderId": orderId + } + self.request("post", url, data=data) + except Exception as e: + pass + return orderId + + def sign_getContinuous(self, is_query_only=False): + try: + url = "https://activity.10010.com/sixPalaceGridTurntableLottery/signin/getContinuous" + params = { + "taskId": "", + "channel": "wode", + "imei": self.uuid + } + res = self.request("get", url, params=params) + if not res: return + result = res.json() + code = result.get('code') + if code == "0000": + todayIsSignIn = result.get('data', {}).get('todayIsSignIn', 'n') + self.log(f"签到区今天{'已' if todayIsSignIn == 'y' else '未'}签到", notify=True) + if todayIsSignIn == 'y': + pass + else: + if not is_query_only: + time.sleep(1) + self.sign_daySign() + else: + self.log("签到区: [查询模式] 跳过自动打卡") + else: + self.log(f"签到区查询签到状态失败[{code}]: {result.get('desc', '')}") + except Exception as e: + self.log(f"sign_getContinuous 异常: {str(e)}") + + def sign_daySign(self): + try: + url = "https://activity.10010.com/sixPalaceGridTurntableLottery/signin/daySign" + res = self.request("post", url, data={}) + if not res: return + result = res.json() + code = result.get('code') + if code == "0000": + data = result.get('data', {}) + msg = f"签到区签到成功: [{data.get('statusDesc', '')}]{data.get('redSignMessage', '')}" + self.log(msg) + elif code == "0002" and "已经签到" in result.get('desc', ''): + self.log("签到区签到成功: 今日已完成签到!") + else: + self.log(f"签到区签到失败[{code}]: {result.get('desc', '')}") + except Exception as e: + self.log(f"sign_daySign 异常: {str(e)}") + + def sign_getTelephone(self, is_initial=False, silent=False): + try: + url = "https://act.10010.com/SigninApp/convert/getTelephone" + res = self.request("post", url, data={}) + if not res: return None + result = res.json() + status = result.get('status') + if status == "0000" and result.get('data'): + tel_val = result['data'].get('telephone', 0) + try: + current_amount = float(tel_val) + except: + current_amount = 0.0 + if silent: + return current_amount + if is_initial: + msg = f"签到区-话费红包: 运行前总额 {current_amount:.2f}元" + self.sign_initial_amount = current_amount + else: + if hasattr(self, 'sign_initial_amount'): + increase = current_amount - self.sign_initial_amount + self.log(f"签到区-话费红包: 本次运行增加 {increase:.2f}元", notify=True) + msg = f"签到区-话费红包: 总额 {current_amount:.2f}元" + exp_val = result['data'].get('needexpNumber', 0) + try: + exp_num = float(exp_val) + except: + exp_num = 0.0 + if exp_num > 0: + msg += f",其中 {result['data'].get('needexpNumber', '0')}元 将于 {result['data'].get('month', '')}月底到期" + self.log(msg, notify=not is_initial) + return current_amount + else: + if not silent: + self.log(f"签到区查询话费红包失败[{status}]: {result.get('msg', '')}") + return None + except Exception as e: + if not silent: + self.log(f"sign_getTelephone 异常: {str(e)}") + return None + + def sign_getTaskList(self): + try: + url = "https://activity.10010.com/sixPalaceGridTurntableLottery/task/taskList" + headers = {"Referer": "https://img.client.10010.com/"} + for i in range(30): + res = self.request("get", url, params={"type": "2"}, headers=headers, timeout=10) + if not res: return + result = res.json() + code = result.get('code') + if code == "0329" or "火爆" in result.get('desc', ''): + self.log("签到区: 系统繁忙(0329),停止后续尝试") + break + if code != "0000": + self.log(f"签到区-任务中心: 获取任务列表失败[{code}]: {result.get('desc', '')}") + return + tag_list = result.get('data', {}).get('tagList', []) or [] + task_list = result.get('data', {}).get('taskList', []) or [] + all_tasks = task_list + [t for tag in tag_list for t in tag.get('taskDTOList', [])] + all_tasks = [t for t in all_tasks if t] + if not all_tasks: + if i == 0: self.log("签到区-任务中心: 当前无任何任务。") + break + do_task = next((t for t in all_tasks if t.get('taskState') == '1' and t.get('taskType') == '5'), None) + if do_task: + self.log(f"签到区-任务中心: 开始执行任务 [{do_task.get('taskName')}]") + self.sign_doTaskFromList(do_task) + time.sleep(3) + continue + claim_task = next((t for t in all_tasks if t.get('taskState') == '0'), None) + if claim_task: + self.log(f"签到区-任务中心: 发现可领取奖励的任务 [{claim_task.get('taskName')}]") + self.sign_getTaskReward(claim_task.get('id')) + time.sleep(2) + continue + if i == 0: + self.log("签到区-任务中心: 没有可执行或可领取的任务。") + else: + self.log("签到区-任务中心: 所有任务处理完毕。") + break + except Exception as e: + self.log(f"sign_getTaskList 异常: {str(e)}") + + def sign_doTaskFromList(self, task): + try: + if task.get('url') and task['url'] != '1' and task['url'].startswith('http'): + self.request("get", task['url'], headers={"Referer": "https://img.client.10010.com/"}) + self.log(f"签到区-任务中心: 浏览页面 [{task.get('taskName')}]") + time.sleep(random.uniform(5, 7)) + orderId = self.gettaskip() + url = "https://activity.10010.com/sixPalaceGridTurntableLottery/task/completeTask" + params = { + "taskId": task.get('id'), + "orderId": orderId, + "systemCode": "QDQD" + } + res = self.request("get", url, params=params) + if not res: return + result = res.json() + code = result.get('code') + if code == "0000": + self.log(f"签到区-任务中心: ✅ 任务 [{task.get('taskName')}] 已完成") + else: + self.log(f"签到区-任务中心: ❌ 任务 [{task.get('taskName')}] 完成失败[{code}]: {result.get('desc', '未知错误')}") + except Exception as e: + self.log(f"sign_doTaskFromList 异常: {str(e)}") + + def sign_getTaskReward(self, task_id): + try: + url = "https://activity.10010.com/sixPalaceGridTurntableLottery/task/getTaskReward" + res = self.request("get", url, params={"taskId": task_id}) + if not res: return + result = res.json() + code = result.get('code') + if code == "0000": + data = result.get('data', {}) + if data.get('code') == '0000': + self.log(f"签到区-领取奖励: [{data.get('prizeName', '')}] {data.get('prizeNameRed', '')}") + else: + self.log(f"签到区-领取奖励失败[{data.get('code')}]: {result.get('desc') or data.get('desc')}") + else: + self.log(f"签到区-领取奖励失败[{code}]: {result.get('desc', '')}") + except Exception as e: + self.log(f"sign_getTaskReward 异常: {str(e)}") + + def sign_grabCoupon(self): + sc = globalConfig.get("sign_config", {}) + if not sc.get("run_grab_coupon", False): + return + self.log(f"⚔️ [抢兑阶段] 正在检查目标: {GRAB_AMOUNT}元 话费券...") + candidates = [] + try: + url = "https://act.10010.com/SigninApp/new_convert/prizeList" + headers = {"Origin": "https://img.client.10010.com"} + res = self.request("post", url, headers=headers) + if res: + list_res = res.json() + if list_res.get('status') == "0000": + details = list_res.get('data', {}).get('datails', {}) + tab_items = details.get('tabItems', []) + self.log(f"📋 [调试] 共获取到 {len(tab_items)} 个场次数据") + for tab in tab_items: + products = tab.get('timeLimitQuanListData', []) + round_time_str = tab.get('time', '') + round_date = None + try: + if round_time_str and ":" in round_time_str: + now = datetime.now() + date_str = now.strftime('%Y/%m/%d') + full_time_str = f"{date_str} {round_time_str}" + if len(round_time_str) <= 8: + round_date = datetime.strptime(full_time_str, "%Y/%m/%d %H:%M") + else: + round_date = datetime.strptime(round_time_str, "%Y-%m-%d %H:%M:%S") + except: + pass + for item in products: + p_name = item.get('product_name', '') + if str(GRAB_AMOUNT) in p_name and ("元" in p_name or "话费" in p_name): + self.log(f" ✅ 发现目标: {p_name} (ID: {item.get('product_id')})") + candidates.append({ + "id": item.get('product_id'), + "name": p_name, + "typeCode": item.get('type_code') or '0', + "timeStr": round_time_str, + "startTime": round_date, + "itemData": item + }) + except Exception as e: + self.log(f"❌ 获取奖品列表失败: {str(e)}") + if not candidates: + self.log(f"⚠️ 未在任何场次中匹配到名为 '{GRAB_AMOUNT}元' 的奖品。") + return + now = datetime.now() + best_candidate = None + min_diff = float('inf') + for cand in candidates: + start_time = cand['startTime'] + if not start_time: continue + diff = (start_time - now).total_seconds() + score = 0 + if diff > 0: + score = diff + elif diff > -600: + score = abs(diff) + 10000 + else: + score = abs(diff) + 90000 + if score < min_diff: + min_diff = score + best_candidate = cand + if not best_candidate: + best_candidate = candidates[0] + self.log(f"🎯 最终锁定场次: [{best_candidate['timeStr']}] {best_candidate['name']}") + if best_candidate['startTime']: + start_time = best_candidate['startTime'] + wait_seconds = (start_time - datetime.now()).total_seconds() + if wait_seconds > 0: + if wait_seconds > 300: + self.log(f"⏳ 距离开抢还有 {wait_seconds:.1f} 秒,大于5分钟,暂不等待。建议在临近时间(如提前2分钟)再运行脚本。") + return + self.log(f"⏳ 正在等待开抢... (剩余 {wait_seconds:.1f} 秒)") + while (best_candidate['startTime'] - datetime.now()).total_seconds() > 0.5: + time.sleep(0.5) + else: + self.log(f"⚡ 当前时间已超过场次时间 {abs(wait_seconds):.1f}s,直接抢兑!") + self.sign_grab_execute(best_candidate) + + def sign_grab_execute(self, candidate): + for i in range(1, 6): + self.log(f"🔥 [第{i}次冲击] 发起兑换请求...") + try: + data = { + "product_id": candidate['id'], + "typeCode": candidate['typeCode'] + } + url = "https://act.10010.com/SigninApp/convert/prizeConvert" + headers = { + "Origin": "https://img.client.10010.com", + "Referer": "https://img.client.10010.com/", + "X-Requested-With": "com.sinovatech.unicom.ui" + } + res = self.request("post", url, data=data, headers=headers) + if not res: continue + result = res.json() + uuid_val = result.get('data', {}).get('uuid') + status = result.get('status') + if status == "0000" and uuid_val: + self.log(f"📝 [提交成功] 获取到工单号: {uuid_val},正在查询最终结果...") + check_url = "https://act.10010.com/SigninApp/convert/prizeConvertResult" + check_data = { "uuid": uuid_val } + check_res = self.request("post", check_url, data=check_data, headers=headers) + if not check_res: continue + final_res = check_res.json() + final_status = final_res.get('status') + if final_status == "0000": + self.log(f"🎉🎉🎉 [抢兑成功] 恭喜!已成功抢到目标奖品! 🎉🎉🎉", notify=True) + return + else: + err_code = final_res.get('data', {}).get('errorCode', '') + msg = final_res.get('msg', '') or final_res.get('message', '未知原因') + detail_msg = final_res.get('data', {}).get('rightBtn', {}).get('name', '') + log_msg = f"💔 [抢兑失败] 状态: {final_status}" + if err_code: log_msg += f" | 错误码: {err_code}" + if detail_msg: log_msg += f" | 详情: {detail_msg}" + log_msg += f" | 提示: {msg}" + self.log(log_msg, notify=True) + else: + self.log(f"📝 提交结果: {result.get('msg') or result.get('message') or json.dumps(result)}") + time.sleep(0.2) + except Exception as e: + self.log(f"❌ 抢兑异常: {str(e)}") + + def get_wocare_body(self, apiCode, requestData={}): + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + str(int(datetime.now().microsecond / 1000)).zfill(3) + encodedContent = base64.b64encode(json.dumps(requestData, separators=(',', ':')).encode('utf-8')).decode('utf-8') + body = { + "version": WOCARE_CONSTANTS["minRetries"], + "apiCode": apiCode, + "channelId": WOCARE_CONSTANTS["anotherApiKey"], + "transactionId": timestamp + self.random_string(6, "0123456789"), + "timeStamp": timestamp, + "messageContent": encodedContent + } + params_array = [] + for key in sorted(body.keys()): + params_array.append(f"{key}={body[key]}") + params_array.append(f"sign={WOCARE_CONSTANTS['anotherEncryptionKey']}") + sign_str = "&".join(params_array) + body["sign"] = hashlib.md5(sign_str.encode('utf-8')).hexdigest() + return body + + def wocare_api(self, apiCode, requestData={}): + try: + url = f"https://wocare.unisk.cn/api/v1/{apiCode}" + body = self.get_wocare_body(apiCode, requestData) + res = self.request("post", url, data=body) + if not res: return None + result = res.json() + if result.get("messageContent"): + try: + content = result["messageContent"] + content = content.replace('\n', '').replace('\r', '').replace(' ', '') + content = content.replace('-', '+').replace('_', '/') + missing_padding = len(content) % 4 + if missing_padding: + content += '=' * (4 - missing_padding) + try: + decoded_bytes = base64.b64decode(content) + decoded_str = decoded_bytes.decode('utf-8') + except UnicodeDecodeError: + decoded_str = decoded_bytes.decode('utf-8', errors='replace') + except Exception as e: + decoded_str = "{}" + try: + decoded = json.loads(decoded_str, strict=False) + except: + decoded_str = re.sub(r'[\x00-\x1f\x7f]', '', decoded_str) + try: + decoded = json.loads(decoded_str, strict=False) + except: + decoded = {} + if isinstance(decoded, dict): + if "data" in decoded: + result["data"] = decoded["data"] + else: + result["data"] = decoded + if "resultMsg" in decoded: + result["resultMsg"] = decoded["resultMsg"] + if "resultCode" in decoded: + result["resultCode"] = decoded["resultCode"] + except Exception as e: + self.log(f"联通祝福: 解析返回失败: {str(e)}") + return result + except Exception as e: + self.log(f"wocare_api 异常: {str(e)}") + return None + + def wocare_getToken(self, ticket): + try: + url = "https://wocare.unisk.cn/mbh/getToken" + params = { + "channelType": WOCARE_CONSTANTS["serviceLife"], + "type": "02", + "ticket": ticket, + "version": COMMON_CONSTANTS["APP_VERSION"], + "timestamp": datetime.now().strftime('%Y%m%d%H%M%S') + str(int(datetime.now().microsecond / 1000)).zfill(3), + "desmobile": self.account_mobile, + "num": "0", + "postage": self.random_string(32), + "homePage": "home", + "duanlianjieabc": "qAz2m", + "userNumber": self.account_mobile + } + res = self.session.get(url, params=params, allow_redirects=False, timeout=15) + if res.status_code == 302: + location = res.headers.get("Location", "") + if location: + parsed = urlparse(location) + sid = parse_qs(parsed.query).get("sid", [None])[0] + if not sid: + sid = parse_qs(parsed.query).get("uuid", [None])[0] + if sid: + self.log(f"联通祝福: 未找到sid,使用uuid替代: {sid}") + if sid: + self.wocare_sid = sid + return self.wocare_loginmbh() + else: + self.log(f"联通祝福: 没有获取到sid或uuid, Location: {location}") + else: + self.log("联通祝福: 没有获取到location") + else: + self.log(f"联通祝福: 获取sid失败[{res.status_code}]") + except Exception as e: + self.log(f"wocare_getToken 异常: {str(e)}") + return False + + def wocare_loginmbh(self): + try: + apiCode = "loginmbh" + requestData = { + "sid": self.wocare_sid, + "channelType": WOCARE_CONSTANTS["serviceLife"], + "apiCode": apiCode + } + result = self.wocare_api(apiCode, requestData) + if not result: return False + responseResult = result + resultCode = responseResult.get("resultCode", "-1") + if resultCode == "0000": + self.wocare_token = responseResult.get("data", {}).get("token") + self.log("联通祝福: 登录成功") + return True + else: + msg = responseResult.get("resultMsg") or responseResult.get("resultDesc") or "" + self.log(f"联通祝福: 登录失败[{resultCode}]: {msg}") + except Exception as e: + self.log(f"wocare_loginmbh 异常: {str(e)}") + return False + + def wocare_getDrawTask(self, activity): + try: + apiCode = "getDrawTask" + requestData = { + "token": self.wocare_token, + "channelType": WOCARE_CONSTANTS["serviceLife"], + "type": activity["id"], + "apiCode": apiCode + } + result = self.wocare_api(apiCode, requestData) + responseResult = result if result else {} + resultCode = responseResult.get("resultCode", "-1") + if resultCode == "0000": + taskList = responseResult.get("data", {}).get("taskList", []) or [] + if not taskList: + pass + else: + self.log(f"联通祝福: [{activity['name']}] 查询到 {len(taskList)} 个任务") + for task in taskList: + ts = task.get("taskStatus") + if str(ts) == "0" or not ts: + self.wocare_completeTask(activity, task) + else: + msg = responseResult.get("resultMsg") or responseResult.get("resultDesc") or "" + self.log(f"联通祝福: [{activity['name']}]查询任务失败[{resultCode}]: {msg}") + except Exception as e: + self.log(f"wocare_getDrawTask 异常: {str(e)}") + + def wocare_completeTask(self, activity, task, taskStep="1"): + try: + taskTitle = task.get("title", "") + action = "领取任务" if taskStep == "1" else "完成任务" + apiCode = "completeTask" + requestData = { + "token": self.wocare_token, + "channelType": WOCARE_CONSTANTS["serviceLife"], + "task": task.get("id"), + "taskStep": taskStep, + "type": activity["id"], + "apiCode": apiCode + } + result = self.wocare_api(apiCode, requestData) + responseResult = result if result else {} + resultCode = responseResult.get("resultCode", "-1") + if resultCode == "0000": + self.log(f"联通祝福: {action}[{taskTitle}]成功") + if taskStep == "1": + time.sleep(1) + self.wocare_completeTask(activity, task, "4") + else: + msg = responseResult.get("resultMsg") or responseResult.get("resultDesc") or "" + self.log(f"联通祝福: [{activity['name']}]{action}[{taskTitle}]失败[{resultCode}]: {msg}") + except Exception as e: + self.log(f"wocare_completeTask 异常: {str(e)}") + + def wocare_getSpecificityBanner(self): + try: + apiCode = "getSpecificityBanner" + requestData = { + "token": self.wocare_token, + "apiCode": apiCode + } + result = self.wocare_api(apiCode, requestData) + responseResult = result if result else {} + resultCode = responseResult.get("resultCode", "-1") + if resultCode == "0000": + bannerList = responseResult.get("data", []) or [] + if not bannerList: + self.log(f"联通祝福: 获取动态 Banner 列表为空,接口明细: {responseResult}") + for banner in bannerList: + if str(banner.get("activityStatus")) == "0" and str(banner.get("isDeleted")) == "0": + self.wocare_getDrawTask(banner) + self.wocare_loadInit(banner) + else: + msg = responseResult.get("resultMsg") or responseResult.get("resultDesc", "") + self.log(f"联通祝福: 进入活动失败[{resultCode}]: {msg}") + except Exception as e: + self.log(f"wocare_getSpecificityBanner 异常: {str(e)}") + + def wocare_loadInit(self, activity): + try: + apiCode = "loadInit" + requestData = { + "token": self.wocare_token, + "channelType": WOCARE_CONSTANTS["serviceLife"], + "type": activity["id"], + "apiCode": apiCode + } + result = self.wocare_api(apiCode, requestData) + responseResult = result if result else {} + resultCode = responseResult.get("resultCode", "-1") + if resultCode == "0000": + responseData = responseResult.get("data", {}) or {} + activeModuleGroupId = responseData.get("zActiveModuleGroupId") + drawCount = 0 + aid = activity["id"] + if aid == 2: + isPartake = responseData.get("data", {}).get("isPartake") or 0 + if not isPartake: + drawCount = 1 + elif aid == 3: + drawCount = int(responseData.get("raffleCountValue", 0) or 0) + elif aid == 4: + drawCount = int(responseData.get("mhRaffleCountValue", 0) or 0) + if drawCount > 0: + self.log(f"联通祝福: [{activity['name']}] 可抽奖次数 {drawCount}") + else: + self.log(f"联通祝福: [{activity['name']}] 今日已无抽奖机会") + while drawCount > 0: + time.sleep(2) + self.wocare_luckDraw(activity, activeModuleGroupId) + drawCount -= 1 + else: + msg = responseResult.get("resultMsg") or responseResult.get("resultDesc") or "" + self.log(f"联通祝福: [{activity['name']}]查询活动失败[{resultCode}]: {msg}") + except Exception as e: + self.log(f"wocare_loadInit 异常: {str(e)}") + + def wocare_luckDraw(self, activity, activeModuleGroupId): + try: + apiCode = "luckDraw" + requestData = { + "token": self.wocare_token, + "channelType": WOCARE_CONSTANTS["serviceLife"], + "zActiveModuleGroupId": activeModuleGroupId, + "type": activity["id"], + "apiCode": apiCode + } + result = self.wocare_api(apiCode, requestData) + responseResult = result if result else {} + resultCode = responseResult.get("resultCode", "-1") + if resultCode == "0000": + resultData = responseResult.get("data", {}) or {} + drawResultCode = resultData.get("resultCode", "-1") + if drawResultCode == "0000": + prize = resultData.get("data", {}).get("prize", {}) + prizeName = prize.get("prizeName", "") + prizeDesc = prize.get("prizeDesc", "") + self.log(f"联通祝福: [{activity['name']}]抽奖: {prizeName}[{prizeDesc}]", notify=True) + else: + msg = responseResult.get("resultMsg") or responseResult.get("resultDesc") or "" + if msg.lower() == "success": + self.log(f"联通祝福: [{activity['name']}] 未中奖 (继续努力)") + else: + self.log(f"联通祝福: [{activity['name']}] 抽奖并未中奖: {msg}") + else: + msg = responseResult.get("resultMsg") or responseResult.get("resultDesc") or "" + if msg.lower() == "success": + self.log(f"联通祝福: [{activity['name']}] 未中奖 (继续努力)") + else: + self.log(f"联通祝福: [{activity['name']}] 抽奖异常[{resultCode}]: {msg}") + except Exception as e: + self.log(f"wocare_luckDraw 异常: {str(e)}") + + def parse_jwt_payload(self, token): + try: + payload = token.split('.')[1] + padding = len(payload) % 4 + if padding: + payload += '=' * (4 - padding) + payload = payload.replace('-', '+').replace('_', '/') + decoded_bytes = base64.b64decode(payload) + return json.loads(decoded_bytes.decode('utf-8')) + except Exception as e: + self.log(f"JWT Decode Error: {e}") + return {} + + def generate_market_signature_headers(self, user_token, query_string="", json_body=""): + try: + token = user_token.replace('Bearer ', '') + payload = self.parse_jwt_payload(token) + login_id = payload.get('loginId', '') + app_secret = hashlib.md5(f"al:ak:{login_id}".encode('utf-8')).hexdigest() + nonce = str(uuid.uuid4()) + message = f"{login_id}{app_secret}{nonce}{query_string or ''}{json_body or ''}" + signature = base64.b64encode( + hmac.new( + app_secret.encode('utf-8'), + message.encode('utf-8'), + digestmod=hashlib.sha256 + ).digest() + ).decode('utf-8') + return { + 'X-User-Id': login_id, + 'X-Nonce': nonce, + 'X-Timestamp': str(int(time.time() * 1000)), + 'X-Signature': signature, + 'Content-Type': 'application/json' + } + except Exception as e: + self.log(f"Signature Generation Error: {e}") + return {} + + def generate_market_watering_signature_headers(self, user_token, xbsosjl, login_id, request_ts): + try: + message = f"td:433:tp{xbsosjl}td:334:et{login_id}td:334:et{request_ts}td:334:et" + signature = base64.b64encode( + hmac.new( + str(login_id).encode('utf-8'), + message.encode('utf-8'), + digestmod=hashlib.sha256, + ).digest() + ).decode('utf-8') + return {'X-Signature': signature} + except Exception as e: + self.log(f"Market Watering Signature Error: {e}") + return {} + + def get_market_headers(self, user_token): + return { + 'User-Agent': COMMON_CONSTANTS['MARKET_UA'], + 'Authorization': f"Bearer {user_token}", + 'Content-Type': 'application/json', + 'X-Requested-With': 'com.sinovatech.unicom.ui' + } + + def market_get_ticket(self): + self.log("权益超市: 正在获取 ticket...") + target_url = "https://contact.bol.wo.cn/market" + res = self.openPlatLineNew(target_url) + if res and 'ticket' in res: + self.log("权益超市: 获取ticket成功") + return res['ticket'] + self.log("权益超市: 获取ticket失败") + return None + + def market_get_user_token(self, ticket): + url = f"https://backward.bol.wo.cn/prod-api/auth/marketUnicomLogin?ticket={ticket}" + headers = { + 'User-Agent': COMMON_CONSTANTS['MARKET_UA'], + 'Connection': "Keep-Alive", + 'Accept-Encoding': "gzip", + } + for attempt in range(1, 4): + try: + self.log(f"权益超市: 正在获取 userToken...{f' (第{attempt}次重试)' if attempt > 1 else ''}") + res = self.session.post(url, headers=headers, timeout=30).json() + if res.get('code') == 200: + user_token = res.get('data', {}).get('token') + if user_token: + self.log("权益超市: 获取userToken成功") + return user_token + self.log(f"权益超市: 获取userToken失败: {res.get('msg')}") + except Exception as e: + self.log(f"权益超市: 获取userToken异常: {e}") + if attempt < 3: + self.log(f"权益超市: 等待5秒后重试...") + time.sleep(5) + return None + + def query_market_watering_status(self, user_token): + try: + status_url = "https://backward.bol.wo.cn/prod-api/promotion/activityTask/getMultiCycleProcess?activityId=13" + headers = self.get_market_headers(user_token) + res = self.session.get(status_url, headers=headers).json() + if res.get('code') == 200: + data = res.get('data', {}) + triggered_time = data.get('triggeredTime', 0) + trigger_time = data.get('triggerTime', 0) + create_date = data.get('createDate', '') + self.log(f"权益超市-浇花当前状况: 进度 {triggered_time}/{trigger_time}", notify=True) + if triggered_time >= trigger_time: + self.log("权益超市-浇花: 🌟 您有鲜花权益待领取! (连续浇花已满) 🌟", notify=True) + else: + today_str = datetime.now().strftime('%Y-%m-%d') + last_watered = create_date.split(' ')[0] if create_date else '' + if today_str == last_watered: + self.log(f"权益超市-浇花: 今日已浇水 (最后: {create_date})", notify=True) + else: + self.log("权益超市-浇花: 今日尚未浇水。") + else: + self.log(f"权益超市-浇花查验: 查询状态失败: {res.get('msg')}") + except Exception as e: + self.log(f"权益超市-浇花查验: 异常: {e}") + + def market_watering_task(self, user_token): + self.log("权益超市: 浇花任务开始...") + try: + status_url = "https://backward.bol.wo.cn/prod-api/promotion/activityTask/getMultiCycleProcess?activityId=13" + headers = self.get_market_headers(user_token) + res = self.session.get(status_url, headers=headers).json() + if res.get('code') != 200: + self.log(f"权益超市-浇花: ❌ 失败: 获取状态失败: {res.get('msg')}", notify=True) + return + data = res.get('data', {}) + before_triggered = safe_int(data.get('triggeredTime', 0)) + trigger_time = safe_int(data.get('triggerTime', 0)) + create_date = data.get('createDate', '') + today_str = datetime.now().strftime('%Y-%m-%d') + last_watered = create_date.split(' ')[0] if create_date else '' + if today_str == last_watered: + self.log(f"权益超市-浇花: 今日已浇水 ({before_triggered}/{trigger_time})", notify=True) + return + if before_triggered >= trigger_time: + self.log(f"权益超市-浇花: 🌟 已达领奖条件 ({before_triggered}/{trigger_time})", notify=True) + return + token = user_token.replace('Bearer ', '') + payload = self.parse_jwt_payload(token) + login_id = payload.get('loginId', '') + if not login_id: + self.log("权益超市-浇花: ❌ 失败: 无法获取登录标识", notify=True) + return + xbsosjl = "Y1mN8fNYktY0" + request_ts = str(int(time.time() * 1000)) + query_string = f"xbsosjl={xbsosjl}&timeVerRan={request_ts}&diceid={login_id}" + watering_url = f"https://backward.bol.wo.cn/prod-api/promotion/activityTaskShare/checkWatering?{query_string}" + req_headers = { + 'Authorization': f"Bearer {token}", + 'X-Signature': self.generate_market_watering_signature_headers( + user_token, xbsosjl, login_id, request_ts + ).get('X-Signature', ''), + 'User-Agent': COMMON_CONSTANTS['MARKET_H5_UA'], + 'Content-Type': 'application/json', + 'Origin': 'https://contact.bol.wo.cn', + 'Referer': 'https://contact.bol.wo.cn/', + 'X-Requested-With': 'com.sinovatech.unicom.ui', + 'Accept': '*/*', + } + water_res = self.session.post(watering_url, headers=req_headers, data="{}").json() + if water_res.get('code') != 200: + self.log(f"权益超市-浇花: ❌ 失败: {water_res.get('msg')}", notify=True) + return + time.sleep(1) + check_res = self.session.get(status_url, headers=headers).json() + if check_res.get('code') != 200: + self.log( + f"权益超市-浇花: ✅ 浇水成功 (当前进度约 {before_triggered}/{trigger_time},APP 可能稍后刷新)", + notify=True + ) + return + check_data = check_res.get('data', {}) + after_triggered = safe_int(check_data.get('triggeredTime', before_triggered)) + after_trigger_time = safe_int(check_data.get('triggerTime', trigger_time)) or trigger_time + if after_triggered != before_triggered: + self.log( + f"权益超市-浇花: ✅ 浇水成功 ({before_triggered}/{after_trigger_time} → {after_triggered}/{after_trigger_time})", + notify=True + ) + return + self.log( + f"权益超市-浇花: ✅ 浇水成功 (当前进度约 {before_triggered}/{trigger_time},APP 可能稍后刷新)", + notify=True + ) + except Exception as e: + self.log(f"权益超市-浇花: ❌ 失败: {e}", notify=True) + + def market_get_raffle(self, user_token): + self.log("权益超市: 正在查询奖品池...") + try: + timestamp = int(time.time() * 1000) + query_string = f"id=12&timeVerRan={timestamp}" + json_body = "{}" + sig_headers = self.generate_market_signature_headers(user_token, query_string, json_body) + url = f"https://backward.bol.wo.cn/prod-api/promotion/home/raffleActivity/prizeList?{query_string}" + headers = self.get_market_headers(user_token) + headers.update(sig_headers) + headers['Referer'] = 'https://contact.bol.wo.cn/market' + headers['Origin'] = 'https://contact.bol.wo.cn' + res = self.session.post(url, headers=headers, data=json_body).json() + if res.get('code') == 200 and isinstance(res.get('data'), list): + keywords = ['月卡', '月会员', '月度', 'VIP月', '一个月', '周卡'] + exclude = ['5G宽视界', '沃视频'] + live_prizes = [] + for p in res['data']: + vip_prob = float(p.get('probabilityVip') or p.get('newVipProbability') or 0) + norm_prob = float(p.get('probability') or 0) + name = p.get('name', '') + daily_limit = int(p.get('dailyPrizeLimit') or 0) + match = any(k in name for k in keywords) + not_excluded = not any(e in name for e in exclude) + has_stock = daily_limit > 0 + has_chance = norm_prob > 0 or vip_prob > 0 + if match and not_excluded and has_stock and has_chance: + live_prizes.append(p) + total_limit = int(p.get('quantity') or 0) + self.log(f"权益超市: 【{name}】监测到放水 (日库存:{daily_limit}, 总库存:{total_limit}, 普通概率:{(norm_prob * 100):.4f}%, VIP概率:{(vip_prob * 100):.4f}%)") + if live_prizes: + return True + self.log("权益超市: 📢 未监测到高价值权益放水") + return False + except Exception as e: + self.log(f"权益超市: 查询奖品池异常: {e}") + return False + + def market_get_raffle_count(self, user_token): + try: + timestamp = int(time.time() * 1000) + query_string = f"id=12&channel=unicomTab&timeVerRan={timestamp}" + json_body = "{}" + sig_headers = self.generate_market_signature_headers(user_token, query_string, json_body) + url = f"https://backward.bol.wo.cn/prod-api/promotion/home/raffleActivity/getUserRaffleCountExt?{query_string}" + headers = self.get_market_headers(user_token) + headers.update(sig_headers) + headers['Referer'] = 'https://contact.bol.wo.cn/market' + headers['Origin'] = 'https://contact.bol.wo.cn' + res = self.session.post(url, headers=headers, data=json_body).json() + count = 0 + if res.get('code') == 200: + data = res.get('data') + if isinstance(data, dict): + count = int(data.get('raffleCount') or 0) + else: + count = int(data or 0) + if count > 0: + self.log(f"权益超市: ✅ 当前抽奖次数: {count}") + for i in range(count): + self.log(f"权益超市: 🎯 第 {i+1} 次抽奖...") + if not self.market_user_raffle(user_token): + break + time.sleep(3 + random.random() * 2) + else: + self.log("权益超市: 当前无抽奖次数") + except Exception as e: + self.log(f"权益超市: 查询抽奖次数异常: {e}") + + def market_user_raffle(self, user_token): + try: + timestamp = int(time.time() * 1000) + query_string = f"id=12&channel=unicomTab&timeVerRan={timestamp}" + json_body = "{}" + sig_headers = self.generate_market_signature_headers(user_token, query_string, json_body) + url = f"https://backward.bol.wo.cn/prod-api/promotion/home/raffleActivity/userRaffle?{query_string}" + headers = self.get_market_headers(user_token) + headers.update(sig_headers) + headers['Referer'] = 'https://contact.bol.wo.cn/market' + res = self.session.post(url, headers=headers, data=json_body).json() + if res.get('code') == 200: + data = res.get('data', {}) + prize_name = data.get('prizesName', '') + message = data.get('message') or res.get('msg') or "" + if prize_name and "谢谢参与" not in prize_name: + self.log(f"权益超市: 🎉 抽奖成功: {prize_name}", notify=True) + return True + self.log(f"权益超市: 💨 未中奖: {message}", notify=True) + return True + self.log(f"权益超市: 抽奖失败: {res.get('msg')}") + return False + except Exception as e: + self.log(f"权益超市: 抽奖异常: {e}") + return False + + def market_get_all_tasks(self, ecs_token, user_token): + url = "https://backward.bol.wo.cn/prod-api/promotion/activityTask/getAllActivityTasks?activityId=12" + headers = { + "Authorization": f"Bearer {user_token}", + "User-Agent": COMMON_CONSTANTS["MARKET_UA"], + "Origin": "https://contact.bol.wo.cn", + "Referer": "https://contact.bol.wo.cn/", + "Cookie": f"ecs_token={ecs_token}" + } + for attempt in range(1, 4): + try: + self.log(f"权益超市: 正在获取任务列表...{f' (第{attempt}次重试)' if attempt > 1 else ''}") + res = self.session.get(url, headers=headers, timeout=15).json() + if res.get('code') == 200: + tasks = res.get('data', {}).get('activityTaskUserDetailVOList', []) + self.log(f"权益超市: 成功获取到 {len(tasks)} 个任务") + return tasks + self.log(f"权益超市: 查询任务列表失败: {res.get('msg')}") + except Exception as e: + self.log(f"权益超市: 获取任务列表异常: {e}") + if attempt < 3: + self.log("权益超市: 等待5秒后重试...") + time.sleep(5) + return [] + + def market_do_share_list(self, share_list, user_token): + self.log("权益超市: 开始执行任务...") + for task in share_list: + name = task.get('name', '') + param = task.get('param1', '') + trigger_time = task.get('triggerTime', 0) + triggered_time = task.get('triggeredTime', 0) + if any(k in name for k in ["购买", "秒杀"]): + self.log(f"权益超市: 🚫 {name} [跳过]") + continue + if triggered_time >= trigger_time: + self.log(f"权益超市: ✅ {name} [已完成]") + continue + url = "" + if any(k in name for k in ["浏览", "查看"]): + url = f"https://backward.bol.wo.cn/prod-api/promotion/activityTaskShare/checkView?checkKey={param}" + elif "分享" in name: + url = f"https://backward.bol.wo.cn/prod-api/promotion/activityTaskShare/checkShare?checkKey={param}" + if url: + try: + headers = { + "Authorization": f"Bearer {user_token}", + "User-Agent": COMMON_CONSTANTS["MARKET_UA"], + "Origin": "https://contact.bol.wo.cn", + "Referer": "https://contact.bol.wo.cn/" + } + res = self.session.post(url, json={}, headers=headers, timeout=15).json() + if res.get('code') == 200: + self.log(f"权益超市: ✅ {name} [执行成功]") + else: + self.log(f"权益超市: ❌ {name} [执行失败]: {res.get('msg')}") + except Exception as e: + self.log(f"权益超市: ❌ {name} [执行异常]: {e}") + time.sleep(2) + + def market_get_points_ticket(self, user_token): + try: + res = self.session.get( + "https://backward.bol.wo.cn/prod-api/auth/getTicket?channel=pointsPlatform", + headers={ + "Authorization": f"Bearer {user_token}", + "User-Agent": COMMON_CONSTANTS["MARKET_UA"], + }, + timeout=15, + ).json() + if res.get("code") == 200 and res.get("data"): + return res.get("data") + self.log(f"权益超市-会员中心: 获取 points ticket 失败: {res.get('msg') or res}") + except Exception as e: + self.log(f"权益超市-会员中心: 获取 points ticket 异常: {e}") + return None + + def market_member_center_base_headers(self, points_ticket): + referer = ( + f"https://m.jf.10010.com/ts-mobile/well/{MARKET_MEMBER_CENTER_PAGE_ID}" + f"?distributeId={MARKET_MEMBER_CENTER_DISTRIBUTE_ID}" + f"&partnersId={MARKET_MEMBER_CENTER_PARTNERS_ID}" + f"&clientType={MARKET_MEMBER_CENTER_CLIENT_TYPE}" + f"&ticket={points_ticket}" + ) + return { + "origin": "https://m.jf.10010.com", + "clienttype": MARKET_MEMBER_CENTER_CLIENT_TYPE, + "ticket": points_ticket, + "partnersid": MARKET_MEMBER_CENTER_PARTNERS_ID, + "content-type": "application/json;charset=UTF-8", + "pageid": MARKET_MEMBER_CENTER_PAGE_ID, + "Accept": "application/json, text/plain, */*", + "Referer": referer, + "User-Agent": COMMON_CONSTANTS["MARKET_H5_UA"], + "X-Requested-With": "com.sinovatech.unicom.ui", + } + + def market_get_secret_key_jf(self, points_ticket): + if ( + getattr(self, "market_jf_secretKey", None) + and getattr(self, "market_jf_ticket", None) == points_ticket + ): + return self.market_jf_secretKey + try: + res = self.session.get( + "https://m.jf.10010.com/jf-external-application/jftask/getSecretKey", + headers=self.market_member_center_base_headers(points_ticket), + timeout=10, + ).json() + secret = res.get("data", {}).get("secretKey") + if res.get("code") == "0000" and secret: + self.market_jf_ticket = points_ticket + self.market_jf_secretKey = secret.encode("utf-8") + return self.market_jf_secretKey + self.log(f"权益超市-会员中心: getSecretKey 失败: {res}") + except Exception as e: + self.log(f"权益超市-会员中心: getSecretKey 异常: {e}") + return None + + def market_build_signature_headers_jf(self, points_ticket): + secret_key = self.market_get_secret_key_jf(points_ticket) + if not secret_key: + return {} + request_ts = str(round(time.time() * 1000)) + nonce = ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyz', k=8)) + signature = hmac.new( + secret_key, + f"{nonce}{request_ts}".encode("utf-8"), + hashlib.sha256, + ).hexdigest() + return { + "x-request-timestamp": request_ts, + "x-request-nonce": nonce, + "x-request-signature": signature, + } + + def market_member_center_headers(self, points_ticket, with_sign=False): + headers = self.market_member_center_base_headers(points_ticket) + if with_sign: + headers.update(self.market_build_signature_headers_jf(points_ticket)) + return headers + + def market_prepare_member_center_context(self, points_ticket): + signed_headers = self.market_member_center_headers(points_ticket, with_sign=True) + try: + self.session.post( + "https://m.jf.10010.com/jf-external-application/page/query", + json={ + "activityId": MARKET_MEMBER_CENTER_PAGE_ID, + "distributeId": MARKET_MEMBER_CENTER_DISTRIBUTE_ID, + "partnersId": MARKET_MEMBER_CENTER_PARTNERS_ID, + }, + headers=signed_headers, + timeout=10, + ) + except Exception as e: + self.log(f"权益超市-会员中心: page/query 预热异常: {e}") + try: + self.session.post( + "https://m.jf.10010.com/jf-external-application/jftask/userInfo", + json={}, + headers=self.market_member_center_headers(points_ticket, with_sign=True), + timeout=10, + ) + except Exception as e: + self.log(f"权益超市-会员中心: userInfo 预热异常: {e}") + + def market_member_center_finish_code(self, task): + return safe_int(task.get("finish", task.get("status", 0)), 0) + + def market_member_center_finish_text(self, task): + finish_text = str(task.get("finishText", "")).strip() + if finish_text: + return finish_text + return { + 0: "未完成", + 99: "待领取", + 100: "已领取", + }.get(self.market_member_center_finish_code(task), "未知状态") + + def market_query_member_center_task(self, points_ticket): + try: + res = self.session.post( + "https://m.jf.10010.com/jf-external-application/jftask/taskDetail", + json={}, + headers=self.market_member_center_headers(points_ticket, with_sign=True), + timeout=10, + ).json() + if res.get("code") != "0000": + self.log(f"权益超市-会员中心: 查询任务失败: {res}") + return None + task_list = res.get("data", {}).get("taskDetail", {}).get("taskList", []) + return next( + (task for task in task_list if str(task.get("taskCode")) == MARKET_MEMBER_CENTER_TASK_CODE), + None, + ) + except Exception as e: + self.log(f"权益超市-会员中心: 查询任务异常: {e}") + return None + + def market_wait_member_center_task_state(self, points_ticket, expected_codes, attempts=4, delay=2): + task = None + for idx in range(1, attempts + 1): + task = self.market_query_member_center_task(points_ticket) + if task: + finish_code = self.market_member_center_finish_code(task) + finish_text = self.market_member_center_finish_text(task) + text_matches = ( + (finish_text == "待领取" and 99 in expected_codes) + or (finish_text == "已领取" and 100 in expected_codes) + ) + if finish_code in expected_codes or text_matches: + return task + self.log( + f"权益超市-会员中心: 第{idx}次回查状态 {finish_text}/{finish_code}," + f"本月进度 {safe_int(task.get('finishCount'), 0)}/{safe_int(task.get('needCount'), 0)}" + ) + if idx < attempts: + time.sleep(delay) + self.market_prepare_member_center_context(points_ticket) + return task + + def market_mark_member_center_browse_done(self, user_token, task_fix_id): + try: + headers = { + "Authorization": f"Bearer {user_token}", + "Origin": "https://contact.bol.wo.cn", + "Referer": "https://contact.bol.wo.cn/", + "Content-Type": "application/json", + "Accept": "*/*", + "User-Agent": COMMON_CONSTANTS["MARKET_H5_UA"], + "X-Requested-With": "com.sinovatech.unicom.ui", + } + detail = self.session.get( + f"https://backward.bol.wo.cn/prod-api/promotion/activityTask/getActivityTaskDetailByFixId?taskFixId={task_fix_id}", + headers=headers, + timeout=10, + ).json() + if detail.get("code") != 200: + self.log(f"权益超市-会员中心: 获取任务详情失败: {detail.get('msg') or detail}") + return False + task_data = detail.get("data") or {} + check_key = task_data.get("param1") + wait_seconds = max(safe_int(task_data.get("content"), 17), 15) + if not check_key: + self.log("权益超市-会员中心: 未拿到 checkKey,跳过浏览任务") + return False + self.log(f"权益超市-会员中心: 模拟浏览会员中心 {wait_seconds} 秒") + time.sleep(wait_seconds) + check = self.session.post( + f"https://backward.bol.wo.cn/prod-api/promotion/activityTaskShare/checkView?checkKey={check_key}", + json={}, + headers=headers, + timeout=10, + ).json() + if check.get("code") == 200 and check.get("data") is True: + self.log("权益超市-会员中心: 浏览完成,任务已进入待领取") + return True + self.log(f"权益超市-会员中心: checkView 失败: {check.get('msg') or check}") + except Exception as e: + self.log(f"权益超市-会员中心: 浏览任务异常: {e}") + return False + + def market_receive_member_center_points(self, points_ticket): + try: + res = self.session.post( + "https://m.jf.10010.com/jf-external-application/jfmarkettask/receive", + json={"taskCode": MARKET_MEMBER_CENTER_TASK_CODE}, + headers=self.market_member_center_headers(points_ticket, with_sign=True), + timeout=10, + ).json() + if res.get("code") == "0000": + score = res.get("data", {}).get("score", "未知积分") + title = res.get("data", {}).get("title", "领取成功") + self.log(f"权益超市-会员中心: ✅ {title},获得 {score}", notify=True) + return True + self.log(f"权益超市-会员中心: 领取失败: {res.get('msg') or res}") + except Exception as e: + self.log(f"权益超市-会员中心: 领取异常: {e}") + return False + + def market_member_center_task(self, user_token): + self.log("权益超市-会员中心: 开始检查浏览任务") + points_ticket = self.market_get_points_ticket(user_token) + if not points_ticket: + return + self.market_prepare_member_center_context(points_ticket) + task = self.market_query_member_center_task(points_ticket) + if not task: + self.log("权益超市-会员中心: 未找到目标任务") + return + finish_code = self.market_member_center_finish_code(task) + finish_text = self.market_member_center_finish_text(task) + finish_count = safe_int(task.get("finishCount"), 0) + need_count = safe_int(task.get("needCount"), 0) + self.log( + f"权益超市-会员中心: 当前状态 {finish_text}/{finish_code}," + f"本月进度 {finish_count}/{need_count}" + ) + if finish_count >= need_count: + self.log("权益超市-会员中心: 本月次数已达上限") + return + if finish_code == 100 or finish_text == "已领取": + self.log("权益超市-会员中心: 今日已领取,跳过") + return + if finish_code == 0 or finish_text == "未完成": + jump_url = str(task.get("jumpUrl", "")).strip() + match = re.search(r"taskFixId=(\d+)", jump_url) + task_fix_id = match.group(1) if match else "90" + if not self.market_mark_member_center_browse_done(user_token, task_fix_id): + return + self.market_prepare_member_center_context(points_ticket) + task = self.market_wait_member_center_task_state(points_ticket, {99, 100}, attempts=4, delay=2) + if not task: + return + finish_code = self.market_member_center_finish_code(task) + finish_text = self.market_member_center_finish_text(task) + self.log( + f"权益超市-会员中心: 浏览后状态 {finish_text}/{finish_code}," + f"本月进度 {safe_int(task.get('finishCount'), 0)}/{safe_int(task.get('needCount'), 0)}" + ) + if finish_code == 99 or finish_text == "待领取": + self.market_receive_member_center_points(points_ticket) + elif finish_code != 100: + self.log("权益超市-会员中心: 状态未及时刷新,尝试直接领奖兜底") + if self.market_receive_member_center_points(points_ticket): + return + self.log("权益超市-会员中心: 直接领奖兜底失败,跳过") + + def market_task(self, is_query_only=False): + self.log("==== 权益超市 ====") + ticket = self.market_get_ticket() + if not ticket: + return + user_token = self.market_get_user_token(ticket) + if not user_token: + return + if is_query_only: + self.query_market_watering_status(user_token) + self.query_market_raffle_records(user_token) + self.query_phone_recharge_records(user_token) + return + mc = globalConfig.get("market_config", {}) + if mc.get("run_water", True): + self.market_watering_task(user_token) + time.sleep(2) + else: + self.log("权益超市-浇水: ⏭️ 已被总开关关闭,跳过") + if mc.get("run_task", True): + if hasattr(self, 'ecs_token'): + share_list = self.market_get_all_tasks(self.ecs_token, user_token) + if share_list: + self.market_do_share_list(share_list, user_token) + else: + self.log("权益超市: 缺 ecs_token, 跳过通用任务列表") + else: + self.log("权益超市-做任务: ⏭️ 已被总开关关闭,跳过") + if mc.get("run_member_center", True): + time.sleep(2) + self.market_member_center_task(user_token) + else: + self.log("权益超市-会员中心: ⏭️ 已被子开关关闭,跳过") + if mc.get("run_draw", True): + if self.market_get_raffle(user_token): + self.market_get_raffle_count(user_token) + else: + self.log("权益超市-抽奖: ⏭️ 已被总开关关闭,跳过") + if mc.get("run_claim", False): + self.log("权益超市-领奖: 自动领奖已开启") + self.query_phone_recharge_records(user_token) + else: + self.log("权益超市-领奖: ⏭️ 未开启自动领奖") + self.query_market_raffle_records(user_token) + self.query_phone_recharge_records(user_token) + + def init_cloud_urls(self): + if not hasattr(self, 'cloudDiskUrls'): + self.cloudDiskUrls = { + 'onLine': "https://m.client.10010.com/mobileService/onLine.htm", + 'getTicketByNative': "https://m.client.10010.com/edop_ng/getTicketByNative", + 'userticket': "https://panservice.mail.wo.cn/api-user/api/user/ticket", + 'ltypDispatcher': "https://panservice.mail.wo.cn/wohome/dispatcher", + 'query': "https://m.jf.10010.com/jf-external-application/page/query", + 'taskDetail': "https://m.jf.10010.com/jf-external-application/jftask/taskDetail", + 'taskRecords': "https://m.jf.10010.com/jf-external-application/jftask/taskRecords", + 'dosign': "https://m.jf.10010.com/jf-external-application/jftask/sign", + 'upload2C': "https://tjupload.pan.wo.cn/openapi/client/upload2C", + 'doPopUp': "https://m.jf.10010.com/jf-external-application/jftask/popUp", + 'toFinish': "https://m.jf.10010.com/jf-external-application/jftask/toFinish", + 'lottery': "https://panservice.mail.wo.cn/activity/lottery", + 'userInfo': "https://m.jf.10010.com/jf-external-application/jftask/userInfo", + 'ai_query': "https://panservice.mail.wo.cn/wohome/ai/assistant/query", + 'lottery_times': "https://panservice.mail.wo.cn/activity/lottery/lottery-times", + 'lottery_record': "https://panservice.mail.wo.cn/activity/lottery/recordList", + 'speed_team_create': "https://panservice.mail.wo.cn/activity/team/createTeam", + 'speed_team_token': "https://panservice.mail.wo.cn/activity/team/generateUserTeamToken", + 'speed_team_join': "https://panservice.mail.wo.cn/activity/team/joinTeam", + 'speed_team_member': "https://panservice.mail.wo.cn/activity/teamMember/isUserInTeam", + 'speed_task_activate': "https://panservice.mail.wo.cn/activity/task/activate", + 'aiMoveFile': "https://panservice.mail.wo.cn/wohome/open/v1/ai/moveFile2SystemFolder", + 'getScanState': "https://s.pan.wo.cn/wohome/intelligentClean/getScanStateAndResult", + 'getCleanData': "https://s.pan.wo.cn/wohome/intelligentClean/getCleanData", + 'batchClean': "https://s.pan.wo.cn/wohome/intelligentClean/batchClean", + 'secretKey': "https://m.jf.10010.com/jf-external-application/jftask/getSecretKey", + 'taskFinish': "https://panservice.mail.wo.cn/activity/member-point/v1/task/finish", + } + + def cloudRequest(self, url_name, payload, is_changer=False, method='post', custom_headers=None): + self.init_cloud_urls() + url = self.cloudDiskUrls.get(url_name) + if not url: + self.log(f"云盘无效的URL名称: {url_name}") + return {'result': None, 'headers': None} + headers = { + 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0301}", + 'Connection': "Keep-Alive", + 'Accept-Encoding': "gzip", + } + if custom_headers: + headers.update(custom_headers) + if url_name in ['dosign', 'userInfo', 'doPopUp', 'toFinish', 'taskDetail', 'taskRecords']: + if not getattr(self.cloudDisk, 'userticket', None): + self.log(f"云盘 [{{url_name}}] userticket 未获取") + return {'result': None, 'headers': None} + headers['ticket'] = self.cloudDisk.userticket + headers['content-type'] = "application/json;charset=UTF-8" + headers['partnersid'] = "1649" + headers['origin'] = "https://m.jf.10010.com" + if getattr(self.cloudDisk, 'jeaId', None): + headers['Cookie'] = f"_jea_id={self.cloudDisk.jeaId};" + if url_name in ['dosign', 'toFinish']: + sig_headers = self.build_signature_headers_cloud() + if sig_headers: + headers.update(sig_headers) + if is_changer: + headers['clienttype'] = "yunpan_unicom_applet" + headers['x-requested-with'] = "com.sinovatech.unicom.ui" + if url_name == 'toFinish': + headers['User-Agent'] = "Mozilla/5.0 (Linux; Android 12; Redmi K30 Pro Build/SKQ1.220303.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.39 Mobile Safari/537.36/woapp LianTongYunPan/4.0.4 (Android 12)" + headers['clienttype'] = "yunpan_android" + headers['x-requested-with'] = "com.chinaunicom.bol.cloudapp" + else: + headers['clienttype'] = "yunpan_android" + headers['x-requested-with'] = "com.sinovatech.unicom.ui" + elif url_name == 'ai_query': + model_id = payload.get('modelId', 1) + headers.update({ + 'accept': 'text/event-stream', + 'X-YP-Access-Token': self.cloudDisk.userToken, + 'X-YP-App-Version': '5.0.12', + 'X-YP-Client-Id': '1001000035', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 9; SM-N9810 Build/PQ3A.190705.11211540; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36/woapp LianTongYunPan/5.0.12 (Android 9)', + 'Content-Type': 'application/json', + 'Origin': 'https://panservice.mail.wo.cn', + 'X-Requested-With': 'com.chinaunicom.bol.cloudapp', + 'Referer': f"https://panservice.mail.wo.cn/h5/wocloud_ai/?modelType={model_id}&clientId=1001000035&touchpoint=300300010001&token={self.cloudDisk.userToken}", + }) + elif url_name == 'lottery_times': + method = 'get' + headers.update({ + 'X-YP-Access-Token': self.cloudDisk.userToken, 'source-type': 'woapi', 'clientId': '1001000165', + 'token': self.cloudDisk.userToken, 'X-YP-Client-Id': '1001000165', + }) + elif url_name == 'aiMoveFile': + headers.update({ + 'X-YP-Device-Id': 'yOH1Y2/Ck5tBHRRBEAPCoGRGBOHCob7I', + 'app-type': 'liantongyunpanapp', + 'Access-Token': self.cloudDisk.userToken, + 'Client-Id': '1001000035', + 'App-Version': 'yp-app/5.1.0', + 'Sys-Version': 'Android/15', + 'User-Agent': 'LianTongYunPan/5.1.0 (Android 15)', + 'X-YP-Client-Id': '1001000035', + 'X-YP-Access-Token': self.cloudDisk.userToken, + 'oaid': '00000000', + 'Content-Type': 'application/json;charset=utf-8', + 'Origin': 'https://panservice.mail.wo.cn', + }) + for attempt in range(1, 4): + try: + if method == 'get': + res = self.session.get(url, params=payload, headers=headers, timeout=15) + else: + res = self.session.post(url, json=payload, headers=headers, timeout=15) + if url_name == 'ai_query': + return {'result': None, 'body': res.text, 'headers': res.headers} + try: + res_json = res.json() + return {'result': res_json, 'headers': res.headers, 'status': res.status_code} + except: + return {'result': res.text, 'headers': res.headers, 'status': res.status_code} + except Exception as e: + err_msg = str(e) + if attempt < 3 and os.environ.get("UNICOM_PROXY_API") and ("Max retries exceeded" in err_msg or "timed out" in err_msg.lower() or "connection" in err_msg.lower() or "SOCKS" in err_msg): + self.log(f"cloudRequest [{url_name}] 网络异常触发故障转移({err_msg}), 正在更换代理...") + self.failover_proxy() + continue + if attempt == 3: + self.log(f"cloudRequest Exception [{url_name}]: {e}") + return {'result': None, 'headers': None, 'status': 599} + self.log(f"cloudRequest [{url_name}] 网络异常({e}), 重试第{attempt}次...") + time.sleep(2) + + def encrypt_data_cloud(self, data, key, iv="wNSOYIB1k1DjY5lA"): + key_padded = key.ljust(16)[:16] + cipher = AES.new(key_padded.encode(), AES.MODE_CBC, iv.encode()) + padded = pad(data.encode(), AES.block_size, style="pkcs7") + return base64.b64encode(cipher.encrypt(padded)).decode() + + def getTicketByNative_cloud(self): + for attempt in range(1, 4): + try: + url = f"{self.cloudDiskUrls['getTicketByNative']}?appId=edop_unicom_d67b3e30&token={self.ecs_token}" + headers = { + 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0301}", + 'Connection': "Keep-Alive", + 'Accept-Encoding': "gzip", + } + res = self.session.get(url, headers=headers).json() + if res.get('ticket'): + self.cloudDisk.ticket = res['ticket'] + return res['ticket'] + elif str(res.get('code')) == "9999": + self.log(f"getTicketByNative_cloud 票据失效或被拦截: {res}") + except Exception as e: + err_msg = str(e) + if attempt < 3 and os.environ.get("UNICOM_PROXY_API") and ("Max retries exceeded" in err_msg or "timed out" in err_msg.lower() or "connection" in err_msg.lower() or "SOCKS" in err_msg): + self.log(f"getTicketByNative_cloud 第{attempt}次异常触发故障转移: {err_msg}") + self.failover_proxy() + continue + self.log(f"getTicketByNative_cloud 第{attempt}次重试 - 异常: {e}") + time.sleep(2) + return None + + def get_ltypDispatcher_cloud(self, ticket): + for attempt in range(1, 4): + try: + timestamp = str(int(time.time() * 1000)) + result_rnd = str(random.randint(123456, 199999)) + string_to_hash = "HandheldHallAutoLoginV2" + timestamp + result_rnd + "wohome" + sign = hashlib.md5(string_to_hash.encode()).hexdigest() + payload = { + "header": { + "key": "HandheldHallAutoLoginV2", + "resTime": timestamp, + "reqSeq": result_rnd, + "channel": "wohome", + "version": "", + "sign": sign + }, + "body": { + "clientId": "1001000003", + "ticket": ticket + } + } + url = self.cloudDiskUrls['ltypDispatcher'] + headers = {'User-Agent': "Dalvik/2.1.0 (Linux; U; Android 12; leijun Pro Build/SKQ1.22013.001);unicom{version:android@11.0702}"} + res = self.session.post(url, json=payload, headers=headers).json() + token = res.get('RSP', {}).get('DATA', {}).get('token') + if token: + self.cloudDisk.userToken = token + return token + except Exception as e: + err_msg = str(e) + if attempt < 3 and os.environ.get("UNICOM_PROXY_API") and ("Max retries exceeded" in err_msg or "timed out" in err_msg.lower() or "connection" in err_msg.lower() or "SOCKS" in err_msg): + self.log(f"get_ltypDispatcher_cloud 第{attempt}次异常触发故障转移: {err_msg}") + self.failover_proxy() + continue + self.log(f"get_ltypDispatcher_cloud 第{attempt}次重试 - 异常: {e}") + time.sleep(2) + return None + + def get_userticket_cloud(self, is_changer=False): + if not getattr(self.cloudDisk, 'userToken', None): + self.log("云盘任务: 获取userticket失败, userToken未获取") + return None + headers = {} + if is_changer: + headers = { + 'User-Agent': "LianTongYunPan/4.0.4 (Android 12)", + 'app-type': "liantongyunpanapp", + 'Client-Id': "1001000035", + 'App-Version': "yp-app/4.0.4", + 'Sys-Version': "Android/12", + 'X-YP-Client-Id': "1001000035", + 'X-YP-Access-Token': self.cloudDisk.userToken, + } + else: + headers = { + 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0301}", + 'Content-Type': 'application/json', + 'X-YP-Access-Token': self.cloudDisk.userToken, + 'accesstoken': self.cloudDisk.userToken, + 'token': self.cloudDisk.userToken, + 'clientId': "1001000003", + 'X-YP-Client-Id': "1001000003", + 'source-type': "woapi", + 'app-type': "unicom" + } + for attempt in range(1, 4): + try: + res = self.session.post(self.cloudDiskUrls['userticket'], json={}, headers=headers, timeout=15).json() + if res and isinstance(res, dict) and res.get('result', {}).get('ticket'): + self.cloudDisk.userticket = res['result']['ticket'] + return self.cloudDisk.userticket + else: + self.log(f"get_userticket_cloud failed: {res}") + return None + except Exception as e: + self.log(f"[get_userticket_cloud] 请求异常[{e}],重试第{attempt}次") + time.sleep(2) + return None + + def get_userInfo_cloud(self): + if not self.get_userticket_cloud(False): return + data = self.cloudRequest('userInfo', {}, False, 'post') + res = data.get('result') + headers = data.get('headers') + if headers: + cookie = headers.get('Set-Cookie', '') + match = re.search(r'_jea_id=([^;]+)', cookie) + if match: + self.cloudDisk.jeaId = match.group(1) + if res and res.get('data'): + avail = res['data'].get('availableScore') + today_earn = res['data'].get('todayEarnScore', 0) + if not hasattr(self.cloudDisk, 'initial_avail'): + self.cloudDisk.initial_avail = avail + self.log(f"云盘任务: 运行前 - 今日已赚: {today_earn}, 可用积分: {avail}") + else: + earned = int(avail) - int(self.cloudDisk.initial_avail) + self.log(f"云盘任务: 运行后 - 今日已赚: {today_earn}, 可用: {avail}, 本次获得: {earned}", notify=True) + + def do_ai_interaction_cloud(self, taskCode, taskName): + self.log(f"云盘任务: 执行AI通通查询请求...") + payload = { + "input": "你好", + "platform": 2, + "modelId": 0, + "tag": 21, + "subTag": 210000, + "conversationId": "", + "knowledgeId": "", + "referFileInfo": [] + } + data = self.cloudRequest('ai_query', payload, False, 'post') + body = data.get('body', '') + if body and ('"finish":1' in body or 'success' in body): + self.log(f"云盘任务: ✅ [{taskName}] 互动成功") + self.doPopUp_cloud(taskCode, taskName, False) + return True + self.log(f"云盘任务: ❌ [{taskName}] 互动失败") + return False + + def get_cloud_upload_file_path(self): + custom_path = os.environ.get("UNICOM_CLOUD_UPLOAD_FILE", "").strip() + if custom_path: + full_path = os.path.abspath(custom_path) + if os.path.isfile(full_path): + return full_path + self.log(f"云盘任务: 上传文件不存在: {full_path}") + return None + seed_path = os.path.join(tempfile.gettempdir(), "unicom_cloud_upload_seed.jpg") + target_size = max(UNICOM_CLOUD_UPLOAD_PROGRESS_BYTES, 1024) + if not os.path.exists(seed_path) or os.path.getsize(seed_path) != target_size: + seed_bytes = base64.b64decode("/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxAQEBAQEBAPEA8PEA8QDw8PDw8QDw8QFREWFhURFRUYHSggGBolGxUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGxAQGy0lICYtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAAEAAgMBIgACEQEDEQH/xAAXAAADAQAAAAAAAAAAAAAAAAAAAQID/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB6A//xAAXEAEAAwAAAAAAAAAAAAAAAAABAAIR/9oACAEBAAEFAkqf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwEf/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJf/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABPyFf/9k=") + with open(seed_path, "wb") as f: + f.write(seed_bytes) + if target_size > len(seed_bytes): + f.seek(target_size - 1) + f.write(b"\0") + return seed_path + + def get_cloud_upload_progress_bytes(self, file_size=0): + progress_bytes = max(int(UNICOM_CLOUD_UPLOAD_PROGRESS_BYTES or 0), 1) + if file_size and int(file_size) > 0: + return min(int(file_size), progress_bytes) + return progress_bytes + + def parse_cloud_size_to_bytes(self, value): + match = re.search(r'(\d+(?:\.\d+)?)\s*([KMGT])', str(value).upper()) + if not match: + return 0 + unit_power = {'K': 1, 'M': 2, 'G': 3, 'T': 4} + return int(float(match.group(1)) * (1024 ** unit_power[match.group(2)])) + + def get_cloud_upload_times(self, task, file_size): + progress_list = task.get('taskExtend', {}).get('taskProgressVOList', []) or [] + targets = [] + for item in progress_list: + size_bytes = self.parse_cloud_size_to_bytes(item.get('progressName')) + if size_bytes > 0: + targets.append(size_bytes) + finished = max(int(task.get('finishCount', 0) or 0), 0) + stage_goal = finished + 1 + progress_bytes = self.get_cloud_upload_progress_bytes(file_size) + if targets: + stage_goal = min(stage_goal, len(targets)) + final_target = targets[stage_goal - 1] + completed_target = targets[min(finished, len(targets)) - 1] if finished > 0 else 0 + remaining_bytes = max(final_target - completed_target, 0) + if remaining_bytes <= 0: + return 0, stage_goal + return max((remaining_bytes + progress_bytes - 1) // progress_bytes, 1), stage_goal + required = max(int(task.get('needCount', 0) or 0), 0) + if required > 0: + stage_goal = min(stage_goal, required) + remaining = max(stage_goal - finished, 0) + return remaining, stage_goal + + def query_cloud_task_list_cloud(self): + if not self.get_userticket_cloud(False): + return [] + data = self.cloudRequest('taskDetail', {}, False, 'post') + if not isinstance(data, dict): + self.log("云盘任务: taskDetail 返回结构异常,已跳过本轮任务列表") + return [] + res = data.get('result') + if not isinstance(res, dict): + body = str(res).replace('\r', ' ').replace('\n', ' ').strip()[:120] + self.log(f"云盘任务: taskDetail 返回异常,已跳过本轮任务列表 (status={data.get('status')}, body={body or 'None'})") + return [] + task_detail = res.get('data', {}).get('taskDetail', {}) + if not isinstance(task_detail, dict): + self.log("云盘任务: taskDetail 数据结构异常,已跳过本轮任务列表") + return [] + return task_detail.get('taskList', []) or [] + + def query_task_records_cloud(self, cursor=""): + if not self.get_userticket_cloud(False): + return [] + data = self.cloudRequest('taskRecords', {"cursor": cursor}, False, 'post') + if not isinstance(data, dict): + self.log("云盘任务: taskRecords 返回结构异常,已跳过积分明细查询") + return [] + res = data.get('result') + if not isinstance(res, dict): + body = str(res).replace('\r', ' ').replace('\n', ' ').strip()[:120] + self.log(f"云盘任务: taskRecords 返回异常,已跳过积分明细查询 (status={data.get('status')}, body={body or 'None'})") + return [] + return res.get('data', []) or [] + + def init_cloud_task_records_state(self): + records = self.query_task_records_cloud("") + self.cloudDisk.knownTaskRecordIds = {str(item.get('id')) for item in records if item.get('id')} + + def match_new_cloud_task_record(self, task_name, before_ids=None): + records = self.query_task_records_cloud("") + known_ids = set(before_ids if before_ids is not None else getattr(self.cloudDisk, 'knownTaskRecordIds', set())) + new_record = None + for item in records: + record_id = str(item.get('id') or '') + if not record_id or record_id in known_ids: + continue + if item.get('taskName') == task_name and not new_record: + new_record = item + known_ids.add(record_id) + self.cloudDisk.knownTaskRecordIds = known_ids + return new_record + + def get_cloud_task_by_code_cloud(self, task_code): + if not task_code: + return None + for task in self.query_cloud_task_list_cloud(): + if task.get('taskCode') == task_code: + return task + return None + + def finalize_generic_task_cloud(self, task_code, task_name): + current_task = self.get_cloud_task_by_code_cloud(task_code) + if not isinstance(current_task, dict): + return + finish_text = current_task.get('finishText') + finished = int(current_task.get('finishCount', 0) or 0) + required = int(current_task.get('needCount', 0) or 0) + if finish_text == "待领取": + self.doPopUp_cloud(task_code, task_name, False) + return + if finish_text in ["已完成", "已领取"] or (required > 0 and finished >= required): + record = self.match_new_cloud_task_record(task_name) + if record: + self.log(f"云盘任务: ✅ [{task_name}] 完成, 获得积分: {record.get('earnScoreDesc')}") + else: + self.log(f"云盘任务: ✅ [{task_name}] 已完成") + + def get_cloud_upload_name_cloud(self): + return os.environ.get("UNICOM_CLOUD_UPLOAD_FILENAME", "8648").strip() or "8648" + + def doUpload_cloud(self, taskCode, taskName, prefix="云盘任务", notify=True): + token = getattr(self.cloudDisk, 'userToken', '') + upload_path = self.get_cloud_upload_file_path() + if not token or not upload_path: + return False + file_size = os.path.getsize(upload_path) + progress_file_size = self.get_cloud_upload_progress_bytes(file_size) + file_name = self.get_cloud_upload_name_cloud() + headers = { + 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0", + 'Accept-Encoding': "gzip, deflate, br, zstd", + 'Origin': "https://pan.wo.cn", + 'Referer': "https://pan.wo.cn/", + 'Accept-Language': "zh-CN,zh;q=0.9", + 'Sec-Fetch-Site': "same-site", + 'Sec-Fetch-Mode': "cors", + 'Sec-Fetch-Dest': "empty", + } + for attempt in range(1, 3): + request_time = str(int(time.time() * 1000)) + file_info = self.encrypt_data_cloud(json.dumps({ + "spaceType": "0", + "directoryId": "0", + "batchNo": datetime.now().strftime("%Y%m%d"), + "fileName": file_name, + "fileSize": progress_file_size, + "fileType": "1", + }, ensure_ascii=False, separators=(',', ':')), token) + try: + with open(upload_path, 'rb') as file_obj: + files = { + "uniqueId": (None, f"{request_time}_{''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=6))}"), + "accessToken": (None, token), + "fileName": (None, file_name), + "psToken": (None, "undefined"), + "fileSize": (None, str(file_size)), + "totalPart": (None, "1"), + "partSize": (None, str(file_size)), + "partIndex": (None, "1"), + "channel": (None, "wocloud"), + "directoryId": (None, "0"), + "fileInfo": (None, file_info), + "file": (file_name, file_obj, "image/jpeg"), + } + res = self.request_direct("POST", self.cloudDiskUrls['upload2C'], headers=headers, files=files, timeout=UNICOM_CLOUD_UPLOAD_TIMEOUT) + res_json = {} + try: + res_json = res.json() + except: + pass + meta_code = str(res_json.get('meta', {}).get('code', '')) + code = str(res_json.get('code', '')) + if res.status_code == 200 and (not res_json or meta_code in ('200', '0', '0000') or code in ('200', '0', '0000')): + self.cloudDisk.uploadedFileCount = int(getattr(self.cloudDisk, 'uploadedFileCount', 0) or 0) + 1 + self.log(f"{prefix}: [{taskName}] 上传成功") + if taskCode: + time.sleep(1) + self.doPopUp_cloud(taskCode, taskName, False, notify=notify) + return True + if attempt < 2 and res.status_code >= 500: + self.log(f"{prefix}: [{taskName}] 上传返回 {res.status_code},重建请求重试一次") + time.sleep(2) + continue + self.log(f"{prefix}: ❌ [{taskName}] 上传失败: HTTP {res.status_code} {res_json if res_json else res.text[:200]}") + return False + except Exception as e: + if attempt < 2: + self.log(f"{prefix}: [{taskName}] 上传异常,重建请求重试一次: {e}") + time.sleep(2) + continue + self.log(f"{prefix}: ❌ [{taskName}] 上传异常: {e}") + return False + + def doPopUp_cloud(self, taskCode, taskName, is_changer, notify=True): + if not self.get_userticket_cloud(is_changer): return + known_ids = set(getattr(self.cloudDisk, 'knownTaskRecordIds', set())) + time.sleep(5) + data = self.cloudRequest('doPopUp', {}, is_changer, 'post') + res = data.get('result') + if not isinstance(res, dict): + res = {} + code = res.get('meta', {}).get('code') + code2 = res.get('code') + if str(code) == "0000" or str(code) == "0" or str(code2) == "0000" or str(code2) == "0": + record = self.match_new_cloud_task_record(taskName, known_ids) + if record: + score_desc = record.get('earnScoreDesc') or res.get('data', {}).get('score', 0) + self.log(f"云盘任务: ✅ [{taskName}] 完成, 获得积分: {score_desc}", notify=notify) + return + score = res.get('data', {}).get('score', 0) + if str(score) not in ('', '0', '0积分'): + self.log(f"云盘任务: ✅ [{taskName}] 领取到积分: {score},但未在积分明细匹配到当前任务", notify=notify) + else: + self.log(f"云盘任务: ✅ [{taskName}] 完成", notify=notify) + else: + self.log(f"云盘任务: ❌ [{taskName}] 领取奖励失败: {res}") + + def toFinish_cloud(self, taskCode, taskName, is_changer): + if not self.get_userticket_cloud(is_changer): return False + data = self.cloudRequest('toFinish', {'taskCode': taskCode}, is_changer, 'post') + res = data.get('result') + if not isinstance(res, dict): + res = {} + if res.get('code') == "0000": return True + return False + + def dosign_cloud(self, taskCode, taskName): + if not self.get_userticket_cloud(False): return + data = self.cloudRequest('dosign', {'taskCode': taskCode}, False, 'post') + res = data.get('result') + if not isinstance(res, dict): + res = {} + if "0000" in str(res.get('code')) and res.get('data', {}).get('score'): + self.log(f"云盘任务: ✅ [{taskName}] 完成, 获得积分: {res['data']['score']}", notify=True) + else: + self.log(f"云盘任务: ❌ [{taskName}] 失败: {res}") + + def upload_specific_file_quick_cloud(self): + token = getattr(self.cloudDisk, 'userToken', '') + if not token: + return False, None + quick_transfer_url = "https://b.smartont.net/openapi/transfer/quickTransfer" + headers = { + "access-token": token, + "User-Agent": "okhttp-okgo/jeasonlzy LianTongYunPan/5.0.7 (Android 15)", + "client-Id": "1001000035", + "app-version": "yp-app/5.0.7", + "Content-Type": "application/json" + } + body = { + "batchNo": ''.join(random.choices(string.hexdigits.upper(), k=32)), + "directoryId": "0", + "fileName": "南网在线_4.3.128.apk", + "fileSize": "223892168", + "fileType": "5", + "sha256": "479f9fe75fd218c0c9f9b8038fcfabcc5068094ceaad4bce3443dff304526656", + "spaceType": "0", + "autoRename": 1, + } + try: + res = self.session.post(quick_transfer_url, headers=headers, json=body, timeout=15) + if res.status_code == 200: + resp_json = res.json() + if isinstance(resp_json, dict) and str(resp_json.get("meta", {}).get("code")) in ["0000", "0"]: + if resp_json.get("result", {}).get("hasFile") in [1, None, "1"]: + fid = resp_json.get("result", {}).get("woCloudId") + if fid: + return True, fid + except Exception as e: + self.log(f"云盘任务: 秒传请求异常: {e}") + return False, None + + def get_taskDetail_cloud(self): + taskList = self.query_cloud_task_list_cloud() + if taskList: + names = [t.get('taskName', '?') for t in taskList] + self.log(f"云盘任务: 任务列表({len(taskList)}): {', '.join(names)}") + else: + self.log("云盘任务: 任务列表为空") + return + for task in taskList: + time.sleep(0.5) + tName = task.get('taskName', '') + tCode = task.get('taskCode') + finishText = task.get('finishText') + finished = int(task.get('finishCount', 0)) + required = int(task.get('needCount', 0)) + if finishText == "待领取": + self.log(f"云盘任务: [{tName}] 待领取") + self.doPopUp_cloud(tCode, tName, False) + continue + if finishText in ["已完成", "已领取"] or task.get('finishState', False) == True or (required > 0 and finished >= required): + self.log(f"云盘任务: ✅ [{tName}] 已完成") + continue + self.log(f"云盘任务: 开始执行 [{tName}] 进度: {finished}/{required}") + if "签到" in tName: + self.toFinish_cloud(tCode, tName, False) + self.dosign_cloud(tCode, tName) + elif "与AI通通互动" in tName: + self.toFinish_cloud(tCode, tName, False) + self.do_ai_interaction_cloud(tCode, tName) + elif "微信备份" in tName or "通讯录备份" in tName: + self.log(f"云盘任务: [{tName}] 暂未适配,当前缺少该任务专用协议,先跳过") + elif "当月上传容量满1GB" in tName: + self.toFinish_cloud(tCode, tName, False) + self.log(f"云盘任务: 开始执行1GB秒传任务...") + upload_ok = 0 + for i in range(5): + success, fid = self.upload_specific_file_quick_cloud() + if success and fid: + upload_ok += 1 + self.log(f"云盘任务: 第{i + 1}/5次秒传成功") + time.sleep(random.uniform(1, 2)) + self.delete_root_files_cloud([{'id': fid, 'type': '1'}]) + else: + self.log(f"云盘任务: 第{i + 1}/5次秒传失败") + time.sleep(random.uniform(2, 4)) + current_task = self.get_cloud_task_by_code_cloud(tCode) + current_finishText = current_task.get('finishText') if current_task else finishText + if current_finishText in ["已完成", "已领取"]: + self.log(f"云盘任务: ✅ [{tName}] 1GB秒传任务已完成", notify=True) + else: + self.log(f"云盘任务: [{tName}] 秒传结束,当前状态: {current_finishText}", notify=True) + else: + self.run_generic_cloud_task(tCode, tName) + + def query_all_files_cloud(self, space_type="0", parent_directory_id="0", page_num=0, page_size=500): + token = getattr(self.cloudDisk, 'userToken', '') + if not token: + return {} + res = self.request_wohome_dispatcher_cloud("QueryAllFiles", { + "clientId": "1001000035", + "spaceType": str(space_type), + "sortRule": "0", + "parentDirectoryId": str(parent_directory_id), + "pageNum": str(page_num), + "pageSize": int(page_size), + }, timeout=15) + rsp = res.get('RSP', {}) + if str(rsp.get('RSP_CODE')) != '0000' or not rsp.get('DATA'): + return {} + try: + key_padded = token.ljust(16)[:16] + cipher = AES.new(key_padded.encode(), AES.MODE_CBC, b"wNSOYIB1k1DjY5lA") + plain = unpad(cipher.decrypt(base64.b64decode(rsp['DATA'])), AES.block_size, style="pkcs7").decode('utf-8', errors='ignore') + return json.loads(plain) + except Exception as e: + self.log(f"云盘任务: 查询根目录文件失败: {e}") + return {} + + def request_wohome_dispatcher_cloud(self, key, param, timeout=15, client_id="1001000035"): + token = getattr(self.cloudDisk, 'userToken', '') + if not token: + return {} + timestamp = str(int(time.time() * 1000)) + req_seq = str(random.randint(10000, 99999)) + payload = { + "header": { + "key": key, + "resTime": timestamp, + "reqSeq": req_seq, + "channel": "wohome", + "version": "", + "sign": hashlib.md5(f"{key}{timestamp}{req_seq}wohome".encode()).hexdigest().upper(), + }, + "body": { + "param": self.encrypt_data_cloud(json.dumps(param, ensure_ascii=False, separators=(',', ':')), token), + "secret": True, + }, + } + headers = { + 'User-Agent': 'LianTongYunPan/5.1.2 (Android 10)', + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'Access-Token': token, + 'accesstoken': token, + 'Client-Id': str(client_id), + } + try: + return self.session.post(self.cloudDiskUrls['ltypDispatcher'], json=payload, headers=headers, timeout=timeout).json() + except Exception as e: + self.log(f"云盘任务: [{key}] 请求失败: {e}") + return {} + + def list_upload_named_files_cloud(self, max_pages=4): + upload_name = self.get_cloud_upload_name_cloud().strip() + if not upload_name: + return [] + pattern = re.compile(rf"^{re.escape(upload_name)}(?:\(\d+\))?(?:\.[^.]+)?$") + matched = [] + seen = set() + page_num = 0 + while page_num < max_pages: + data = self.query_all_files_cloud("0", "0", page_num, 500) + page_files = data.get('files') or [] + if not page_files: + break + for item in page_files: + file_id = item.get('id') + file_name = str(item.get('name', '')).strip() + if file_id and file_id not in seen and pattern.match(file_name): + seen.add(file_id) + matched.append(item) + if len(page_files) < 500: + break + page_num += 1 + return matched + + def delete_root_files_cloud(self, items, space_type="0"): + targets = [] + for item in items or []: + item_id = str(item.get('id', '')).strip() + if not item_id: + continue + targets.append((item_id, str(item.get('type', '1')) == '0')) + deleted = 0 + for offset in range(0, len(targets), 100): + batch = targets[offset:offset + 100] + dir_list = [item_id for item_id, is_dir in batch if is_dir] + file_list = [item_id for item_id, is_dir in batch if not is_dir] + if not dir_list and not file_list: + continue + res = self.request_wohome_dispatcher_cloud("DeleteFile", { + "spaceType": str(space_type), + "vipLevel": "0", + "dirList": dir_list, + "fileList": file_list, + "clientId": "1001000035", + }, timeout=20) + rsp = res.get('RSP', {}) + batch_idx = offset // 100 + 1 + if str(rsp.get('RSP_CODE')) == '0000': + deleted += len(batch) + self.log(f"云盘任务: 第{batch_idx}批根目录删除成功,共{len(batch)}个文件") + else: + self.log(f"云盘任务: 第{batch_idx}批根目录删除失败: {rsp.get('RSP_DESC') or res}") + time.sleep(1) + return deleted + + def clean_duplicate_files_cloud(self): + token = getattr(self.cloudDisk, 'userToken', '') + if not token: + return + self.log("云盘任务: 开始清理云盘重复文件") + cloud_headers = { + 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) LianTongYunPan/5.1.0 (iPhone; iOS 16.6)", + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'Accept-Encoding': 'br;q=1.0, gzip;q=0.9, deflate;q=0.8', + 'Access-Token': token, 'X-YP-Access-Token': token, + 'Client-Id': '1001000035', 'X-YP-Client-Id': '1001000035', + 'App-Version': 'yp-app/5.1.0', 'app-type': 'liantongyunpanapp', + 'Sys-Version': 'iOS/16.6', + } + uploaded_count = int(getattr(self.cloudDisk, 'uploadedFileCount', 0) or 0) + retry_count = 6 if uploaded_count > 0 else 1 + task_id = "" + file_ids = [] + for attempt in range(1, retry_count + 1): + try: + res = self.session.post( + self.cloudDiskUrls['getScanState'], json={ + "pathLevelList": [{"levelType": "space", "levelName": "个人云", "busId": "0"}] + }, headers=cloud_headers, timeout=10, + ).json() + except Exception as e: + self.log(f"云盘任务: 获取扫描状态失败: {e}") + return + if res.get('meta', {}).get('code') != '200': + self.log("云盘任务: 获取扫描状态失败") + return + task_id = "" + for item in res.get('result', {}).get('subTaskList', []): + if item.get('taskId'): + task_id = item['taskId'] + break + if task_id: + file_ids = [] + page = max_page = 1 + while page <= max_page: + try: + page_res = self.session.post( + self.cloudDiskUrls['getCleanData'], json={ + "pageNum": page, "taskId": task_id, "type": 3, "pageSize": 50, + }, headers=cloud_headers, timeout=10, + ).json() + except Exception as e: + self.log(f"云盘任务: 获取第{page}页清理数据失败: {e}") + return + if page_res.get('meta', {}).get('code') != '200': + break + max_page = page_res.get('result', {}).get('maxPageNum', 1) + for group in page_res.get('result', {}).get('fileGroupList', []): + for fi, file_item in enumerate(group.get('fileList', [])): + if fi <= 0 or not file_item.get('fileId'): + continue + file_ids.append({"fileId": file_item['fileId'], "spaceType": file_item.get('spaceType', '0')}) + page += 1 + if file_ids: + self.log(f"云盘任务: 第{attempt}次重复扫描完成,共{len(file_ids)}个重复文件") + break + if attempt < retry_count: + wait_seconds = min(5 + (attempt - 1) * 2, 12) + self.log(f"云盘任务: 第{attempt}次重复扫描未发现可清理文件,{wait_seconds}秒后重试") + time.sleep(wait_seconds) + if not file_ids: + named_files = self.list_upload_named_files_cloud() if uploaded_count > 0 else [] + if named_files: + preview = "、".join(item.get('name', '') for item in named_files[:6]).strip("、") + more = "..." if len(named_files) > 6 else "" + self.log(f"云盘任务: 智能清理未识别到重复项,但根目录检测到{len(named_files)}个[{self.get_cloud_upload_name_cloud()}]系列文件: {preview}{more}") + deleted = self.delete_root_files_cloud(named_files) + self.cloudDisk.uploadedFileCount = 0 + if deleted: + self.log(f"云盘任务: 已通过官方删除接口清理{deleted}个[{self.get_cloud_upload_name_cloud()}]系列文件") + else: + self.log(f"云盘任务: [{self.get_cloud_upload_name_cloud()}]系列文件删除失败") + else: + self.cloudDisk.uploadedFileCount = 0 + self.log("云盘任务: 无重复文件") + return + for offset in range(0, len(file_ids), 100): + batch = file_ids[offset:offset + 100] + batch_idx = offset // 100 + 1 + try: + batch_res = self.session.post( + self.cloudDiskUrls['batchClean'], json={ + "fileList": batch, "taskType": 3, "taskId": task_id, + }, headers=cloud_headers, timeout=30, + ).json() + code = batch_res.get('meta', {}).get('code') + self.log(f"云盘任务: 第{batch_idx}批清理: {'成功' if code == '200' else '失败'}") + except Exception as e: + self.log(f"云盘任务: 第{batch_idx}批清理失败: {e}") + time.sleep(2) + named_files = self.list_upload_named_files_cloud() if uploaded_count > 0 else [] + if named_files: + preview = "、".join(item.get('name', '') for item in named_files[:6]).strip("、") + more = "..." if len(named_files) > 6 else "" + self.log(f"云盘任务: 智能清理后根目录仍检测到{len(named_files)}个[{self.get_cloud_upload_name_cloud()}]系列文件: {preview}{more}") + deleted = self.delete_root_files_cloud(named_files) + if deleted: + self.log(f"云盘任务: 已通过官方删除接口补充清理{deleted}个[{self.get_cloud_upload_name_cloud()}]系列文件") + self.cloudDisk.uploadedFileCount = 0 + self.log("云盘任务: 云盘重复文件清理完成") + + def build_cloud_lottery_headers(self, activity_id=CLOUD_SPEED_ACTIVITY_ID, share_token=""): + token = getattr(self.cloudDisk, 'userToken', '') + if not token: + return {} + referer = f"https://panservice.mail.wo.cn/h5/mobile/speed/start?activityId={quote(activity_id)}&touchpoint=300300010005&token={token}" + if share_token: + referer = f"https://panservice.mail.wo.cn/h5/mobile/speed/start?shareInfo={quote(share_token)}" + return { + 'User-Agent': "Mozilla/5.0 (Linux; Android 9; 2210132C Build/PQ3A.190605.10201411; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36/woapp LianTongYunPan/5.3.0 (Android 9)", + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'source-type': 'woapi', + 'clientId': '1001000035', + 'Client-Id': '1001000035', + 'token': token, + 'accesstoken': token, + 'Access-Token': token, + 'X-YP-Access-Token': token, + 'X-YP-Client-Id': '1001000035', + 'X-Requested-With': 'com.chinaunicom.bol.cloudapp', + 'Origin': 'https://panservice.mail.wo.cn', + 'Referer': referer, + } + + def get_cloud_lottery_draw_count(self, times_res): + result = times_res.get('result') if isinstance(times_res, dict) else None + if isinstance(result, int): + return result + if not isinstance(result, dict): + return 0 + for key in ['times', 'lotteryTimes', 'freeTimes', 'drawTimes', 'count']: + if key in result: + try: + return int(result.get(key) or 0) + except: + return 0 + return 0 + + def is_cloud_lottery_notify_prize(self, prize_name): + name = str(prize_name or '').strip() + if not name: + return False + return name != "暂无抽奖记录" + + def query_cloud_lottery_times_cloud(self, activity_id, headers=None): + if not activity_id: + return None + use_headers = dict(headers or self.build_cloud_lottery_headers(activity_id)) + if not use_headers: + return None + use_headers['requestTime'] = str(int(time.time() * 1000)) + res = self.session.get(self.cloudDiskUrls['lottery_times'], params={"activityId": activity_id}, headers=use_headers, timeout=10).json() + self.cloudDisk.lotteryTimesResult = res + return res + + def query_cloud_lottery_record_cloud(self, activity_id, limit=5): + headers = self.build_cloud_lottery_headers(activity_id) + if not activity_id or not headers: + return [] + try: + res = self.session.get(self.cloudDiskUrls['lottery_record'], params={"activityId": activity_id}, headers=headers, timeout=10).json() + if str(res.get('meta', {}).get('code')) == '200': + record_list = res.get('result') or [] + if not record_list: + self.log("云盘任务: 测速抽奖记录: 暂无记录") + return [] + display_records = record_list[:limit] + self.log(f"云盘任务: 测速抽奖记录: 最近 {len(display_records)} 条") + for item in display_records: + prize = item.get('prizeName') or '未知奖品' + create_time = item.get('createTime') or '' + status = item.get('isAccountTxt') or '' + suffix = f" | {status}" if status else "" + self.log(f"云盘任务: 测速抽奖记录: {create_time} | {prize}{suffix}") + return display_records + except Exception as e: + self.log(f"云盘任务: 查询抽奖记录异常: {e}") + return [] + + def get_cloud_lottery_activity_id_cloud(self): + if getattr(self.cloudDisk, 'lotteryActivityId', None): + return self.cloudDisk.lotteryActivityId + headers = self.build_cloud_lottery_headers(CLOUD_SPEED_ACTIVITY_ID) + if not headers: + return None + activity_id = os.environ.get("UNICOM_CLOUD_LOTTERY_ACTIVITY_ID", "").strip() or CLOUD_SPEED_ACTIVITY_ID + try: + res = self.query_cloud_lottery_times_cloud(activity_id, headers) + meta_code = str(res.get('meta', {}).get('code')) + if meta_code == '200': + self.cloudDisk.lotteryActivityId = activity_id + self.cloudDisk.lotteryTimesResult = res + self.log("云盘任务: 开始执行测速抽奖活动") + return activity_id + self.log(f"云盘任务: 测速抽奖活动[{activity_id}] 无效: {res}") + except Exception as e: + self.log(f"云盘任务: 查询测速抽奖活动[{activity_id}]失败: {e}") + return None + + def cloud_speed_post(self, url_name, payload, headers=None): + use_headers = dict(headers or self.build_cloud_lottery_headers()) + use_headers['requestTime'] = str(int(time.time() * 1000)) + return self.session.post(self.cloudDiskUrls[url_name], json=payload, headers=use_headers, timeout=10).json() + + def get_cloud_speed_mobile(self): + return str(getattr(self, 'account_mobile', '') or getattr(self, 'mobile', '') or '').strip() + + def get_cloud_speed_team_status(self, activity_id, headers): + res = self.cloud_speed_post('speed_team_member', {"activityId": activity_id}, headers) + if str(res.get('meta', {}).get('code')) != '200': + return False, "" + result = res.get('result') or {} + return bool(result.get('isInTeam')), str(result.get('teamId') or '') + + def create_cloud_speed_team(self, activity_id, headers): + mobile = self.get_cloud_speed_mobile() + team_name = mobile[-4:] if mobile else str(self.index) + res = self.cloud_speed_post('speed_team_create', {"activityId": activity_id, "teamName": team_name}, headers) + if str(res.get('meta', {}).get('code')) != '200': + self.log(f"云盘任务: 测速活动创建战队失败: {res}") + return "" + time.sleep(1) + in_team, team_id = self.get_cloud_speed_team_status(activity_id, headers) + if in_team and team_id: + self.log(f"云盘任务: 测速活动已创建战队 {team_id}") + return team_id + self.log("云盘任务: 测速活动创建战队后未获取到战队ID") + return "" + + def generate_cloud_speed_invite(self, activity_id, team_id, headers): + mobile = self.get_cloud_speed_mobile() + if not team_id or not mobile: + return "" + body = {"activityId": activity_id, "teamId": int(team_id) if str(team_id).isdigit() else team_id, "inviterUserId": mobile} + res = self.cloud_speed_post('speed_team_token', body, headers) + token = res.get('result') if isinstance(res, dict) else "" + if str(res.get('meta', {}).get('code')) == '200' and token: + CLOUD_SPEED_TEAM_CONTEXT.clear() + CLOUD_SPEED_TEAM_CONTEXT.update({"activityId": activity_id, "teamId": str(team_id), "signToken": token, "inviterUserId": mobile}) + self.log(f"云盘任务: 测速活动战队邀请已生成") + return token + self.log(f"云盘任务: 测速活动生成邀请失败: {res}") + return "" + + def join_cloud_speed_team(self, activity_id, headers): + invite = CLOUD_SPEED_TEAM_CONTEXT if CLOUD_SPEED_TEAM_CONTEXT.get("activityId") == activity_id else {} + team_id = str(invite.get("teamId") or "") + sign_token = invite.get("signToken") or "" + mobile = self.get_cloud_speed_mobile() + if not team_id or not sign_token or not mobile: + return False + join_headers = self.build_cloud_lottery_headers(activity_id, sign_token) + res = self.cloud_speed_post('speed_team_join', { + "activityId": activity_id, + "teamId": team_id, + "currentUserId": mobile, + "signToken": sign_token, + }, join_headers) + if str(res.get('meta', {}).get('code')) == '200': + self.log(f"云盘任务: 测速活动已加入战队 {team_id}") + return True + self.log(f"云盘任务: 测速活动加入战队失败: {res}") + return False + + def activate_cloud_speed_task(self, activity_id, headers): + try: + res = self.cloud_speed_post('speed_task_activate', {"activityId": activity_id}, headers) + if str(res.get('meta', {}).get('code')) == '200': + self.log("云盘任务: 测速活动任务已激活") + return True + self.log(f"云盘任务: 测速活动任务激活失败: {res}") + except Exception as e: + self.log(f"云盘任务: 测速活动任务激活异常: {e}") + return False + + def ensure_cloud_speed_team_cloud(self, activity_id, headers): + in_team, team_id = self.get_cloud_speed_team_status(activity_id, headers) + if in_team and team_id: + self.log(f"云盘任务: 测速活动已在战队 {team_id}") + if self.index == 1 or not CLOUD_SPEED_TEAM_CONTEXT.get("signToken"): + self.generate_cloud_speed_invite(activity_id, team_id, headers) + return True + if CLOUD_SPEED_TEAM_CONTEXT.get("activityId") == activity_id: + return self.join_cloud_speed_team(activity_id, headers) + team_id = self.create_cloud_speed_team(activity_id, headers) + if not team_id: + return False + self.generate_cloud_speed_invite(activity_id, team_id, headers) + return True + + def draw_lottery_cloud(self): + activity_id = self.get_cloud_lottery_activity_id_cloud() + if not activity_id: + self.log("云盘任务: 未找到有效测速抽奖活动") + return + headers = self.build_cloud_lottery_headers(activity_id) + if not headers: + return + try: + if not self.ensure_cloud_speed_team_cloud(activity_id, headers): + self.log("云盘任务: 测速活动战队准备失败,跳过抽奖") + return + self.activate_cloud_speed_task(activity_id, headers) + times_res = self.query_cloud_lottery_times_cloud(activity_id, headers) + times_code = str(times_res.get('meta', {}).get('code')) if isinstance(times_res, dict) else '' + if times_code != '200': + self.log(f"云盘任务: 查询测速抽奖次数失败: {times_res}") + return + draw_count = self.get_cloud_lottery_draw_count(times_res) + if draw_count <= 0: + self.log("云盘任务: 测速抽奖当前无抽奖次数") + return + for _ in range(draw_count): + draw_headers = dict(headers) + draw_headers['requestTime'] = str(int(time.time() * 1000)) + res = self.session.post(self.cloudDiskUrls['lottery'], json={"activityId": activity_id}, headers=draw_headers, timeout=10).json() + if str(res.get('meta', {}).get('code')) == '92000017': + self.log("云盘任务: 测速抽奖今日已抽") + return + if 'result' in res: + prize_name = res['result'].get('prizeName', '') + self.log(f"云盘任务: 测速抽奖获得: {prize_name}", notify=self.is_cloud_lottery_notify_prize(prize_name)) + continue + self.log(f"云盘任务: 测速抽奖无结果: {res}") + self.query_cloud_lottery_record_cloud(activity_id) + except Exception as e: + self.log(f"云盘任务: 测速抽奖失败: {e}") + + def get_secret_key_cloud(self): + if getattr(self.cloudDisk, 'secretKey', None): + return self.cloudDisk.secretKey + if not getattr(self.cloudDisk, 'userticket', None) or not getattr(self.cloudDisk, 'jeaId', None): + return None + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', + 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) LianTongYunPan/4.0.2 (iPhone; iOS 16.6)", + 'Accept-Encoding': 'gzip, deflate, br', + 'Connection': 'keep-alive', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + 'Origin': 'https://m.jf.10010.com', + 'Host': 'm.jf.10010.com', + 'clienttype': 'yunpan_iOS', + 'partnersid': '1649', + 'ticket': self.cloudDisk.userticket, + 'Cookie': f"_jea_id={self.cloudDisk.jeaId};", + } + try: + res = self.session.get(self.cloudDiskUrls['secretKey'], headers=headers, timeout=10).json() + secret = res.get('data', {}).get('secretKey') + if res.get('code') == '0000' and secret: + self.cloudDisk.secretKey = secret.encode('utf-8') + self.log("云盘任务: secretKey 获取成功") + return self.cloudDisk.secretKey + self.log(f"云盘任务: getSecretKey 失败: {res}") + except Exception as e: + self.log(f"云盘任务: getSecretKey 异常: {e}") + return None + + def build_signature_headers_cloud(self): + secret_key = self.get_secret_key_cloud() + if not secret_key: + return {} + request_ts = str(round(time.time() * 1000)) + nonce = ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyz', k=8)) + signature = hmac.new( + secret_key, f"{nonce}{request_ts}".encode('utf-8'), hashlib.sha256, + ).hexdigest() + return { + 'x-request-timestamp': request_ts, + 'x-request-nonce': nonce, + 'x-request-signature': signature, + } + + def run_generic_cloud_task(self, task_code, task_name): + self.log(f"云盘任务: [{task_name}] 尝试通用完成接口") + self.toFinish_cloud(task_code, task_name, False) + time.sleep(2) + self.handle_unknown_task_cloud(task_code, task_name) + time.sleep(3) + self.finalize_generic_task_cloud(task_code, task_name) + + def handle_unknown_task_cloud(self, task_code, task_name=""): + token = getattr(self.cloudDisk, 'userToken', '') + if not token: + return False + headers = { + 'User-Agent': 'LianTongYunPan/5.0.4 (iOS 16.3)', + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'Accept-Encoding': 'br;q=1.0, gzip;q=0.9, deflate;q=0.8', + 'Access-Token': token, 'X-YP-Access-Token': token, + 'Client-Id': '1001000035', 'X-YP-Client-Id': '1001000035', + 'App-Version': 'yp-app/5.0.4', 'app-type': 'liantongyunpanapp', + 'Sys-Version': 'iOS/16.3', + 'Accept-Language': 'zh-Hans-CN;q=1.0', + } + prefix = f"云盘任务: [{task_name}]" if task_name else "云盘任务: 未知任务" + try: + res = self.session.post( + self.cloudDiskUrls['taskFinish'], + json={"taskCode": task_code, "taskStatus": {"isBackUp": "1"}}, + headers=headers, + timeout=10, + ).json() + meta_code = str(res.get('meta', {}).get('code')) + if meta_code == '90003600': + self.log(f"{prefix} 处理完成") + return True + if meta_code in ('200', '0') or str(res.get('code')) in ('200', '0', '0000'): + self.log(f"{prefix} 处理完成: {res.get('msg', res.get('meta', {}).get('message', '成功'))}") + return True + self.log(f"{prefix} 处理失败: {res}") + except Exception as e: + self.log(f"云盘任务: [{task_name or task_code}] 处理失败: {e}") + return False + + def ltyp_task(self, is_query_only=False): + self.log("==== 联通云盘任务 ====") + self.init_cloud_urls() + class CloudDiskState: pass + self.cloudDisk = CloudDiskState() + if not self.ecs_token: + self.log("云盘任务: 缺少 ecs_token,跳过。") + return + ticket = self.getTicketByNative_cloud() + if not ticket: return + if not hasattr(self, 'city_info') or not self.city_info: + self.get_city_info() + token = self.get_ltypDispatcher_cloud(ticket) + if not token: return + time.sleep(0.5) + self.get_userInfo_cloud() + self.init_cloud_task_records_state() + if is_query_only: + self.log("云盘任务: [查询模式] 跳过任务执行...") + self.get_userInfo_cloud() + return + time.sleep(0.5) + self.get_secret_key_cloud() + self.get_taskDetail_cloud() + time.sleep(0.5) + self.get_userInfo_cloud() + time.sleep(2) + self.draw_lottery_cloud() + time.sleep(2) + self.clean_duplicate_files_cloud() + + def getTicketByNative_sec(self): + for attempt in range(1, 4): + try: + url = f"https://m.client.10010.com/edop_ng/getTicketByNative?token={self.ecs_token}&appId=edop_unicom_3a6cc75a" + city_code = "" + cookie_str = f"PvSessionId={datetime.now().strftime('%Y%m%d%H%M%S')}{self.unicomTokenId};c_mobile={self.account_mobile}; c_version=iphone_c@11.0800; city=036|{city_code}|90063345|-99;devicedId={self.unicomTokenId}; ecs_token={self.ecs_token};t3_token=" + headers = { + "Cookie": cookie_str, + "Accept": "*/*", + "Connection": "keep-alive", + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "ChinaUnicom4.x/12.3.1 (com.chinaunicom.mobilebusiness; build:77; iOS 16.6.0) Alamofire/4.7.3 unicom{version:iphone_c@12.0301}", + "Accept-Language": "zh-Hans-CN;q=1.0" + } + res = self.session.get(url, headers=headers, timeout=10) + if res.status_code != 200: + self.log(f"安全管家: getTicketByNative_sec http请求失败 {res.status_code}") + return + try: + result = res.json() + except: + self.log(f"安全管家: getTicketByNative_sec json解析失败: {res.text[:100]}") + return + self.sec_ticket1 = result.get('ticket') + if self.sec_ticket1: + return + else: + self.log(f"安全管家: getTicketByNative_sec 失败 - {result}") + except Exception as e: + err_msg = str(e) + if attempt < 3 and os.environ.get("UNICOM_PROXY_API") and ("Max retries exceeded" in err_msg or "timed out" in err_msg.lower() or "connection" in err_msg.lower() or "SOCKS" in err_msg): + self.log(f"安全管家: getTicketByNative_sec 第{attempt}次异常触发故障转移: {err_msg}") + self.failover_proxy() + continue + self.log(f"安全管家: getTicketByNative_sec 第{attempt}次重试 - 异常: {e}") + time.sleep(2) + + def getAuthToken_sec(self): + if not getattr(self, 'sec_ticket1', None): + self.log("安全管家 getAuthToken_sec 缺少 ticket1,跳过") + return + try: + url = "https://uca.wo116114.com/api/v1/auth/ticket?product_line=uasp&entry_point=h5&entry_point_id=edop_unicom_3a6cc75a" + headers = { + "User-Agent": "ChinaUnicom4.x/12.3.1 (com.chinaunicom.mobilebusiness; build:77; iOS 16.6.0) Alamofire/4.7.3 unicom{version:iphone_c@12.0301}", + "Content-Type": "application/json", + "clientType": "uasp_unicom_applet" + } + data = { "productId": "", "type": 1, "ticket": self.sec_ticket1 } + res = self.session.post(url, json=data, headers=headers).json() + if res.get('data'): + self.sec_token = res['data'].get('access_token') + else: + self.log(f"安全管家: getAuthToken_sec 失败 - {res}") + except Exception as e: + self.log(f"安全管家: getAuthToken_sec 异常: {e}") + + def getTicketForJF_sec(self): + if not getattr(self, 'sec_token', None): + self.log("安全管家 getTicketForJF_sec 缺少 token,跳过") + return + try: + url1 = "https://uca.wo116114.com/api/v1/auth/getTicket?product_line=uasp&entry_point=h5&entry_point_id=edop_unicom_3a6cc75a" + headers1 = { + "User-Agent": "ChinaUnicom4.x/12.3.1 (com.chinaunicom.mobilebusiness; build:77; iOS 16.6.0) Alamofire/4.7.3 unicom{version:iphone_c@12.0301}", + "Content-Type": "application/json", + "auth-sa-token": self.sec_token, + "clientType": "uasp_unicom_applet" + } + data1 = { "productId": "91311616", "phone": self.account_mobile } + res1 = self.session.post(url1, json=data1, headers=headers1).json() + if res1.get('data'): + self.sec_ticket = res1['data'].get('ticket') + else: + self.log("安全管家获取积分票据失败") + return + url2 = "https://m.jf.10010.com/jf-external-application/page/query" + headers2 = { + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0301};ltst;OSVersion/16.6", + "partnersid": "1702", + "ticket": unquote(self.sec_ticket), + "clienttype": "uasp_unicom_applet", + } + if hasattr(self, 'sec_jeaId'): + headers2["Cookie"] = f"_jea_id={self.sec_jeaId}" + res2 = self.session.post(url2, json={"activityId": "s747395186896173056", "partnersId": "1702"}, headers=headers2) + res2 = self.session.post(url2, json={"activityId": "s747395186896173056", "partnersId": "1702"}, headers=headers2) + for cookie in self.session.cookies: + if cookie.name == '_jea_id': + self.sec_jeaId = cookie.value + if 'Set-Cookie' in res2.headers: + match = re.search(r'_jea_id=([^;]+)', res2.headers['Set-Cookie']) + if match: + self.sec_jeaId = match.group(1) + except Exception as e: + self.log(f"安全管家: getTicketForJF_sec 异常: {e}") + + def get_secret_key_sec(self, silent=False): + if getattr(self, 'sec_secretKey', None): + return self.sec_secretKey + if not getattr(self, 'sec_ticket', None): + return None + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0301};ltst;OSVersion/16.6", + 'Accept-Encoding': 'gzip, deflate, br', + 'Connection': 'keep-alive', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + 'Origin': 'https://m.jf.10010.com', + 'Host': 'm.jf.10010.com', + 'clienttype': 'uasp_unicom_applet', + 'partnersid': '1702', + 'ticket': unquote(self.sec_ticket), + } + if hasattr(self, 'sec_jeaId') and self.sec_jeaId: + headers['Cookie'] = f"_jea_id={self.sec_jeaId};" + try: + res = self.session.get("https://m.jf.10010.com/jf-external-application/jftask/getSecretKey", headers=headers, timeout=10).json() + secret = res.get('data', {}).get('secretKey') + if res.get('code') == '0000' and secret: + self.sec_secretKey = secret.encode('utf-8') + if not silent: + self.log("secretKey 获取成功") + return self.sec_secretKey + self.log(f"安全管家: getSecretKey 失败: {res}") + except Exception as e: + self.log(f"安全管家: getSecretKey 异常: {e}") + return None + + def build_signature_headers_sec(self): + secret_key = self.get_secret_key_sec() + if not secret_key: + return {} + request_ts = str(round(time.time() * 1000)) + nonce = ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyz', k=8)) + signature = hmac.new( + secret_key, f"{nonce}{request_ts}".encode('utf-8'), hashlib.sha256, + ).hexdigest() + return { + 'x-request-timestamp': request_ts, + 'x-request-nonce': nonce, + 'x-request-signature': signature, + } + + def sec_uca_post(self, url_path, body): + try: + headers = { + "clientType": "uasp_unicom_applet", + "auth-sa-token": self.sec_token, + "Content-Type": "application/json", + "Accept": "*", + "User-Agent": "ChinaUnicom4.x/12.3.1 (com.chinaunicom.mobilebusiness; build:77; iOS 16.6.0) Alamofire/4.7.3 unicom{version:iphone_c@12.0301}" + } + return self.session.post(url_path, json=body, headers=headers, timeout=10).json() + except Exception as e: + self.log(f"安全管家: uca_post 异常: {e}") + return None + + def addToBlacklist_sec(self): + url = "https://uca.wo116114.com/sjgj/woAssistant/umm/configs/v1/config?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6" + self.sec_uca_post(url, { + "productId": "91242950", "operationType": 1, "type": 1, + "contents": [{"checked": True, "configTime": None, "nickname": None, "contentTag": "疑似诈骗", "content": "13088330789"}] + }) + time.sleep(2) + self.sec_uca_post(url, { + "productId": "91242950", "blacklistSource": 0, "type": 1, "operationType": 0, + "contents": [{"contentTag": "疑似诈骗", "content": "13088330789"}] + }) + + def markPhoneNumber_sec(self): + url = "https://uca.wo116114.com/sjgj/unicomAssistant/uasp/configs/v1/addressBook/saveTagPhone?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6" + self.sec_uca_post(url, {"productId": "91311616", "status": 0, "tagIds": [26], "tagPhoneNo": "13088330789"}) + + def syncAddressBook_sec(self): + url = "https://uca.wo116114.com/sjgj/unicomAssistant/uasp/configs/v1/addressBookBatchConfig?product_line=uasp&entry_point=h5&entry_point_id=edop_unicom_3a6cc75a" + self.sec_uca_post(url, { + "opType": "1", "productId": "91311616", + "addressBookDTOList": [{"addressBookName": "可乐", "addressBookPhoneNo": "13105750575"}] + }) + + def setInterceptionRules_sec(self): + url = "https://uca.wo116114.com/sjgj/woAssistant/umm/configs/v1/config?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6" + self.sec_uca_post(url, { + "productId": "91311616", "type": 3, "operationType": 0, + "contents": [{"icon": "alerting", "content": "1", "contentName": "响一声", "contentTag": "8", "name": "rings-once"}] + }) + time.sleep(2) + self.sec_uca_post(url, { + "productId": "91311616", "type": 3, "operationType": 0, + "contents": [{"icon": "alerting", "content": "0", "contentName": "响一声", "contentTag": "8", "name": "rings-once"}] + }) + + def viewWeeklyReport_sec(self): + base = "https://uca.wo116114.com/sjgj/unicomAssistant/uasp" + body = {"productId": "91311616"} + self.sec_uca_post(f"{base}/configs/v1/weeklySwitchStatus?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6", body) + self.sec_uca_post(f"{base}/report/v1/queryKeyData?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6", body) + self.sec_uca_post(f"{base}/report/v1/weeklySummary?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6", body) + + def zhushou_sec(self): + try: + headers = { + "auth-sa-token": self.sec_token, "token": self.sec_token, + "Content-Type": "application/json", "Accept": "*", + "User-Agent": "ChinaUnicom4.x/12.3.1 (com.chinaunicom.mobilebusiness; build:77; iOS 16.6.0) Alamofire/4.7.3 unicom{version:iphone_c@12.0301}" + } + self.session.post("https://ims.wo116114.com/api/AiAssistant/autoReply", + json={"history": [], "message": "1", "promptId": 10000}, headers=headers, timeout=10) + except Exception as e: + self.log(f"安全管家: 智能助手异常: {e}") + + def daijie_sec(self): + try: + headers = { + "auth-sa-token": self.sec_token, "token": self.sec_token, "Authorization": self.sec_token, + "Content-Type": "application/json", + "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)" + } + self.session.post("https://ims.wo116114.com/api/Assistant/assis_save", json={ + "page_type": 1, "old_ainumber": "XF0", "level": 3, "dialog": "0", + "opertype": 1, "videoimage": "", "speechtype": "06", "ainumber": "BD1" + }, headers=headers, timeout=10) + except Exception as e: + self.log(f"安全管家: 代接助理异常: {e}") + + def anquanfen_sec(self): + url = "https://uca.wo116114.com/sjgj/woAssistant/umm/configs/v1/config?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6" + score_url = "https://uca.wo116114.com/sjgj/unicomAssistant/uasp/report/v1/queryScore?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6" + off_body = {"productId": "91351080", "type": 3, "operationType": 0, + "contents": [{"icon": "phone-fraud", "content": "1", "contentName": "疑似诈骗", "contentTag": "0", "name": "fraud"}]} + on_body = {"productId": "91351080", "type": 3, "operationType": 0, + "contents": [{"contentTag": "0", "content": "0"}]} + self.sec_uca_post(url, off_body) + time.sleep(2) + self.sec_uca_post(score_url, {"productId": "91311616"}) + time.sleep(2) + self.sec_uca_post(url, on_body) + time.sleep(2) + self.sec_uca_post(score_url, {"productId": "91311616"}) + time.sleep(2) + self.sec_uca_post(url, off_body) + + def haoduan_sec(self): + url = "https://uca.wo116114.com/sjgj/woAssistant/umm/configs/v1/config?product_line=uasp&entry_point=h5&entry_point_id=wxdefbc1986dc757a6" + item_off = {"checked": True, "content": "1", "contentName": "拦截400开头的10位特服号码", "contentTag": "1"} + item_on = {"checked": False, "content": "0", "contentName": "拦截400开头的10位特服号码", "contentTag": "1"} + base = {"productId": "91351080", "type": 7, "operationType": 0} + self.sec_uca_post(url, {**base, "contents": [item_off]}) + time.sleep(2) + self.sec_uca_post(url, {**base, "contents": [item_on]}) + time.sleep(2) + self.sec_uca_post(url, {**base, "contents": [item_off]}) + + def ojbk_sec(self, taskCode): + try: + url = "https://m.jf.10010.com/jf-external-application/jftask/taskFinish" + headers = self._sec_jf_headers() + self.session.post(url, json={"taskCode": taskCode}, headers=headers, timeout=10) + except Exception as e: + self.log(f"安全管家: 活动浏览异常: {e}") + + def sec_wo_ai_headers(self, use_mobile=False, override_token=None): + ua_pc = ( + "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(0xf2541818) XWEB/19201 miniProgram/wx1e83eef922822ee0" + ) + ua_mobile = ( + "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3 like Mac OS X) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 " + "MicroMessenger/8.0.69(0x1800452f) NetType/WIFI Language/zh_CN " + "miniProgram/wx1e83eef922822ee0" + ) + return { + "Accept-Language": "zh-CN,zh;q=0.9", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "Origin": "https://ai.wo.cn", + "Authorization": override_token or self.sec_token, + "Content-Type": "application/json", + "Accept": "application/json, text/plain, */*", + "User-Agent": ua_mobile if use_mobile else ua_pc, + } + + def sec_get_knowledge_id(self): + try: + response = self.session.post( + "https://ai.wo.cn/web-tongtong/knowledge/getKnowledgeList", + headers=self.sec_wo_ai_headers(), + data=json.dumps({"appType": 1}), + timeout=10, + ) + res = response.json() + if res.get("code") == 0 and res.get("data"): + return res["data"][0].get("id") + self.log(f"获取知识库ID失败:{res.get('msg') or res}") + except Exception as e: + self.log(f"获取知识库ID异常:{e}") + return "" + + def upload_knowledge_file_sec(self): + try: + kid = self.sec_get_knowledge_id() + if not kid: + return False + upload_headers = { + k: v for k, v in self.sec_wo_ai_headers().items() + if k.lower() != "content-type" + } + files = {"file": ("task_upload.txt", b" ", "text/plain")} + data = { + "knowledgeId": kid, + "fileName": "task_upload.txt", + "fileType": "text/plain", + "fileSize": "1", + "currentPartSize": "1", + "currentIndex": "1", + "totalPart": "1", + "spaceType": "0", + } + response = self.session.post( + "https://ai.wo.cn/web-tongtong/knowledge/uploadLocalFileToKnowledge", + headers=upload_headers, + files=files, + data=data, + timeout=15, + ) + res = response.json() + if res.get("code") == 0: + self.log("上传知识库文件成功") + return True + self.log(f"上传知识库文件失败:{res.get('msg') or res}") + return False + except Exception as e: + self.log(f"安全管家: 上传知识库文件异常: {e}") + return False + + def sec_get_chat_list(self): + try: + headers = self.sec_wo_ai_headers() + headers["Referer"] = "https://ai.wo.cn/wxMini" + response = self.session.get( + "https://ai.wo.cn/web-tongtong/historyChat/list", + headers=headers, + timeout=10, + ) + res = response.json() + if res.get("code") == 0: + return ((res.get("data") or {}).get("content") or []) + if res.get("msg"): + self.log(f"获取AI对话历史失败:{res.get('msg')}") + except Exception as e: + self.log(f"获取AI对话历史异常:{e}") + return [] + + def sec_send_ai_chat(self): + try: + session_id = f"mmru{''.join(str(random.randint(0, 9)) for _ in range(10))}" + request_id = f"rqid_mmru{''.join(str(random.randint(0, 9)) for _ in range(10))}" + headers = self.sec_wo_ai_headers(use_mobile=True) + headers["Accept"] = "text/event-stream" + headers["Referer"] = "https://ai.wo.cn/wxMini/psychologicalApp/chat?id=1&type=ruole" + payload = { + "modelKey": "87e622d9e488", + "message": "帮我推荐1个必吃饭店", + "deepThink": False, + "webSearch": False, + "attachments": [], + "imgSize": 0, + "sessionId": session_id, + "requestId": request_id, + "promptKey": "", + "knowledgeId": "", + "ragSearch": False, + "moduleType": 12, + } + response = self.session.post( + "https://ai.wo.cn/web-tongtong/chat/chatReplyV2", + headers=headers, + data=json.dumps(payload), + timeout=60, + stream=True, + ) + for _ in response.iter_lines(): + pass + if response.ok: + self.log("已发送AI对话") + return True + self.log(f"发送AI对话失败:HTTP {response.status_code}") + except Exception as e: + self.log(f"发送AI对话异常:{e}") + return False + + def sec_get_share_key(self, session_id): + try: + headers = self.sec_wo_ai_headers() + headers["Referer"] = "https://ai.wo.cn/wxMini" + response = self.session.post( + "https://ai.wo.cn/web-tongtong/historyChat/shareDetail", + headers=headers, + data=json.dumps({"sessionId": session_id}), + timeout=10, + ) + res = response.json() + if res.get("code") == 0 and res.get("data"): + return str(res["data"]) + self.log(f"获取分享key失败:{res.get('msg') or res}") + except Exception as e: + self.log(f"获取分享key异常:{e}") + return "" + + def sec_view_share_detail(self, key, view_token): + try: + response = self.session.post( + "https://ai.wo.cn/web-tongtong/historyChat/getShareDetail", + headers=self.sec_wo_ai_headers(use_mobile=True, override_token=view_token), + data=json.dumps({"key": key, "pageSize": 10, "pageNum": 1}), + timeout=10, + ) + res = response.json() + if res.get("code") == 0: + self.log("查看分享对话成功") + return True + self.log(f"查看分享对话失败:{res.get('msg') or res}") + except Exception as e: + self.log(f"查看分享对话异常:{e}") + return False + + def share_ai_chat_sec(self, taskCode=""): + try: + content = self.sec_get_chat_list() + if not content: + self.log("暂无AI对话历史,先发送一条对话...") + if not self.sec_send_ai_chat(): + return False + time.sleep(2) + content = self.sec_get_chat_list() + if not content: + self.log("仍无AI对话历史,跳过分享任务") + return False + session_id = content[0].get("chatSessionId", "") + if not session_id: + self.log("获取分享key失败:缺少sessionId") + return False + share_key = self.sec_get_share_key(session_id) + if not share_key: + return False + self.sec_ai_share_key = share_key + if taskCode: + self.sec_share_task_code = taskCode + short_key = share_key[:8] + "..." if len(share_key) > 8 else share_key + self.log(f"AI对话分享key获取成功: {short_key}") + return True + except Exception as e: + self.log(f"安全管家: 分享AI对话异常: {e}") + return False + + def role_chat_sec(self): + try: + session_id = f"mmrp{''.join(str(random.randint(0, 9)) for _ in range(10))}" + request_id = f"rqid_mmrp{''.join(str(random.randint(0, 9)) for _ in range(10))}" + headers = self.sec_wo_ai_headers() + headers["Accept"] = "text/event-stream" + headers["Referer"] = "https://ai.wo.cn/wxMini/psychologicalApp/chat?id=1&type=ruole" + response = self.session.post( + "https://ai.wo.cn/web-tongtong/lxzn/chat", + headers=headers, + data=json.dumps({ + "sessionId": session_id, + "requestId": request_id, + "roleId": 1, + "message": "我有拖延症,好多事情不想做。", + }), + timeout=60, + stream=True, + ) + for _ in response.iter_lines(): + pass + if not response.ok: + self.log(f"角色助手对话失败:HTTP {response.status_code}") + return False + self.log("角色助手对话完成") + return True + except Exception as e: + self.log(f"安全管家: 角色助手对话异常: {e}") + return False + + def sec_finalize_share_ai_task(self): + if not self.sec_share_task_code: + return False + self.sec_track_pending_claim(self.sec_share_task_code, self.sec_share_task_name) + latest_task = self.sec_refresh_task_snapshot( + self.sec_share_task_code, + self.sec_share_task_name, + retries=5, + delay=2, + ) + if not latest_task: + self.sec_refresh_security_context(refresh_secret=True) + latest_task = self.sec_refresh_task_snapshot( + self.sec_share_task_code, + self.sec_share_task_name, + retries=5, + delay=2, + ) + if not latest_task: + self.log("联通助理-分享AI助手对话:互看后未查询到任务状态") + return False + finish_count, need_count, finish_text = self.sec_parse_task(latest_task) + if finish_text == "待领取" or (need_count > 0 and finish_count >= need_count): + self.log("联通助理-分享AI助手对话:互看完成,尝试领取奖励") + receive_state = self.receivePoints_sec(self.sec_share_task_code, self.sec_share_task_name) + if receive_state in ("received", "auto"): + self.getUserInfo_sec() + return True + if receive_state == "pending": + self.sec_recover_pending_claims(rounds=2, delay=6, refresh_context=True) + if self.sec_share_task_code not in self.sec_pending_claim_tasks: + return True + self.log(f"联通助理-分享AI助手对话:互看后状态 {finish_count}/{need_count} - {finish_text}") + return False + + def _sec_jf_headers(self, with_signature=False): + headers = { + "ticket": unquote(self.sec_ticket), + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0301};ltst;OSVersion/16.6", + "partnersid": "1702", + "clienttype": "uasp_unicom_applet", + "Content-Type": "application/json", + "Accept": "application/json, text/plain, */*", + "Origin": "https://m.jf.10010.com", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + } + if hasattr(self, 'sec_jeaId') and self.sec_jeaId: + headers["Cookie"] = f"_jea_id={self.sec_jeaId};" + if with_signature: + headers.update(self.build_signature_headers_sec()) + return headers + + def update_sec_jea_id(self, response=None): + jea_id = "" + if response is not None: + cookie = response.headers.get("Set-Cookie", "") + match = re.search(r"_jea_id=([^;]+)", cookie) + if match: + jea_id = match.group(1) + if not jea_id: + for cookie_item in self.session.cookies: + if cookie_item.name == "_jea_id": + jea_id = cookie_item.value + break + if jea_id: + self.sec_jeaId = jea_id + return jea_id + + def sec_query_task_list(self): + url = "https://m.jf.10010.com/jf-external-application/jftask/taskDetail" + last_error = "" + for attempt in range(1, 4): + response = None + try: + response = self.session.post(url, json={}, headers=self._sec_jf_headers(), timeout=15) + self.update_sec_jea_id(response) + res = response.json() + task_detail = ((res or {}).get("data") or {}).get("taskDetail") or {} + return task_detail.get("taskList", []) + except ValueError: + preview = ((response.text if response is not None else "") or "").strip().replace("\n", " ") + last_error = f"非JSON响应[{attempt}/3]: {preview[:60] or 'empty'}" + except Exception as e: + last_error = str(e) + if attempt < 3: + time.sleep(2) + self.log(f"联通助理任务列表查询异常: {last_error}") + return [] + + def sec_parse_task(self, task): + finish_count = safe_int(task.get("finishCount", 0)) + need_count = safe_int(task.get("needCount", 0)) + finish_text = task.get("finishText") or task.get("taskStatusName") or task.get("taskStatusDesc") or "未知状态" + return finish_count, need_count, finish_text + + def sec_supports_delayed_claim(self, task_name): + delayed_keywords = ( + "上传知识库文件", + "分享AI助手对话", + "角色助手对话", + ) + return any(keyword in task_name for keyword in delayed_keywords) + + def sec_track_pending_claim(self, task_code, task_name): + if task_code and task_name and self.sec_supports_delayed_claim(task_name): + self.sec_pending_claim_tasks[task_code] = task_name + + def sec_untrack_pending_claim(self, task_code): + if task_code: + self.sec_pending_claim_tasks.pop(task_code, None) + + def sec_refresh_security_context(self, refresh_secret=False): + try: + self.sec_ticket1 = "" + self.sec_token = "" + self.sec_ticket = "" + self.getTicketByNative_sec() + if not getattr(self, 'sec_ticket1', None): + return False + self.getAuthToken_sec() + if not getattr(self, 'sec_token', None): + return False + self.getTicketForJF_sec() + if not getattr(self, 'sec_ticket', None): + return False + if refresh_secret: + self.sec_secretKey = None + if not self.get_secret_key_sec(silent=True): + return False + return True + except Exception as e: + self.log(f"联通助理上下文刷新异常: {e}") + return False + + def sec_get_task_snapshot(self, task_code="", task_name=""): + for task in self.sec_query_task_list(): + if task_code and task.get("taskCode") == task_code: + return task + if task_name and task.get("taskName") == task_name: + return task + return None + + def sec_refresh_task_snapshot(self, task_code, task_name, retries=3, delay=2): + latest_task = None + for attempt in range(retries): + if attempt: + time.sleep(delay) + latest_task = self.sec_get_task_snapshot(task_code, task_name) + if not latest_task: + continue + finish_count, need_count, finish_text = self.sec_parse_task(latest_task) + if finish_text == "待领取" or (need_count > 0 and finish_count >= need_count): + return latest_task + return latest_task + + def sec_should_manual_finish(self, task_name): + manual_keywords = ( + "新增亲情守护成员", + "新增宽带绑定", + "语音提醒", + "反诈险领取", + "设置日程提醒", + ) + return any(keyword in task_name for keyword in manual_keywords) + + def sec_wait_seconds(self, task_name): + if any(keyword in task_name for keyword in ("上传知识库文件", "分享AI助手对话", "角色助手对话")): + return 5 + if any(keyword in task_name for keyword in ("添加黑名单", "骚扰拦截设置", "安全分提升", "号段拦截")): + return 8 + return 4 + + def receivePoints_sec(self, taskCode, taskName=""): + url = "https://m.jf.10010.com/jf-external-application/jftask/receive" + last_error = "" + for attempt in range(1, 3): + response = None + try: + headers = self._sec_jf_headers(with_signature=True) + response = self.session.post(url, json={"taskCode": taskCode}, headers=headers, timeout=10) + self.update_sec_jea_id(response) + res = response.json() + score = str((res.get("data") or {}).get("score") or "").strip() + msg = str(res.get("msg") or "").strip() + if score: + self.sec_untrack_pending_claim(taskCode) + self.log(f"安全管家: 领取{score}成功", notify=True) + return "received" + if "任务未完成" in msg or "不可领取" in msg: + self.log("领取任务未完成") + return "pending" + if "自动发放" in msg or "已领取" in msg: + self.sec_untrack_pending_claim(taskCode) + self.log("任务已完成且奖励已领取") + return "auto" + if msg: + self.log(f"领取失败:{msg}") + return "failed" + self.log(f"领取失败:{res}") + return "failed" + except ValueError: + preview = ((response.text if response is not None else "") or "").strip().replace("\n", " ") + last_error = f"非JSON响应[{attempt}/2]: {preview[:60] or 'empty'}" + except Exception as e: + last_error = str(e) + if attempt < 2: + time.sleep(2) + self.log(f"安全管家: 领取积分异常: {last_error}") + return "error" + + def finishTask_sec(self, taskCode, taskName): + try: + url = "https://m.jf.10010.com/jf-external-application/jftask/toFinish" + headers = self._sec_jf_headers(with_signature=True) + response = self.session.post(url, json={"taskCode": taskCode}, headers=headers, timeout=10) + self.update_sec_jea_id(response) + action_map = { + "添加黑名单": self.addToBlacklist_sec, + "号码标记": self.markPhoneNumber_sec, + "同步通讯录": self.syncAddressBook_sec, + "骚扰拦截设置": self.setInterceptionRules_sec, + "智能助手": self.zhushou_sec, + "代接助理": self.daijie_sec, + "安全分": self.anquanfen_sec, + "号段拦截": self.haoduan_sec, + "查看周报": self.viewWeeklyReport_sec, + "活动浏览": lambda: self.ojbk_sec(taskCode), + "上传知识库文件": self.upload_knowledge_file_sec, + "分享AI助手对话": lambda: self.share_ai_chat_sec(taskCode), + "角色助手对话": self.role_chat_sec, + } + for key, action in action_map.items(): + if key in taskName: + result = action() + return False if result is False else True + self.log(f"任务 {taskName} 需要手动完成") + return False + except Exception as e: + self.log(f"安全管家: finishTask异常: {e}") + return False + + def signIn_sec(self, taskCode): + try: + url = "https://m.jf.10010.com/jf-external-application/jftask/sign" + headers = self._sec_jf_headers(with_signature=True) + response = self.session.post(url, json={"taskCode": taskCode}, headers=headers, timeout=10) + self.update_sec_jea_id(response) + res = response.json() + if res.get("code") == "0000": + return True + self.log(f"签到失败:{res.get('msg') if res else '状态未知'}") + return False + except Exception as e: + self.log(f"安全管家: 签到异常: {e}") + return False + + def executeAllTasks_sec(self): + try: + task_list = self.sec_query_task_list() + if not task_list: + self.log("联通助理任务列表查询失败") + return + for task in task_list: + task_name = task.get("taskName", "") + task_code = task.get("taskCode", "") + finish_count, need_count, finish_text = self.sec_parse_task(task) + self.log(f"{task_name}:{finish_count}/{need_count} - {finish_text}") + if not task_code or need_count <= 0: + self.log("---------------------") + continue + if finish_count >= need_count: + if finish_text == "待领取": + time.sleep(2) + receive_state = self.receivePoints_sec(task_code, task_name) + if receive_state == "pending": + self.sec_track_pending_claim(task_code, task_name) + else: + self.log("任务已完成且奖励已领取") + self.log("---------------------") + continue + remaining = max(need_count - finish_count, 1) + self.log(f"任务未完成,需要再执行 {remaining} 次") + if self.sec_should_manual_finish(task_name): + self.log(f"任务 {task_name} 需要手动完成") + self.log("---------------------") + continue + for i in range(remaining): + try: + if i: + time.sleep(2) + handled = self.signIn_sec(task_code) if "签到" in task_name else self.finishTask_sec(task_code, task_name) + if not handled: + break + wait_seconds = self.sec_wait_seconds(task_name) + if wait_seconds > 0: + time.sleep(wait_seconds) + latest_task = self.sec_refresh_task_snapshot(task_code, task_name, retries=3, delay=2) + if latest_task: + finish_count, need_count, finish_text = self.sec_parse_task(latest_task) + self.log(f"第 {i + 1} 次执行{task_name}任务完成") + receive_state = self.receivePoints_sec(task_code, task_name) + if receive_state == "pending": + self.sec_track_pending_claim(task_code, task_name) + if receive_state in ("received", "auto"): + break + if need_count > 0 and finish_count >= need_count: + self.log("任务已完成且奖励已领取") + break + except Exception as e: + self.log(f"执行 {task_name} 失败:{e}") + break + self.log("---------------------") + except Exception as e: + self.log(f"联通助理任务执行异常: {e}") + + def getUserInfo_sec(self): + url = "https://m.jf.10010.com/jf-external-application/jftask/userInfo" + last_error = "" + for attempt in range(1, 3): + response = None + try: + headers = self._sec_jf_headers() + response = self.session.post(url, json={}, headers=headers, timeout=10) + self.update_sec_jea_id(response) + res = response.json() + if not res or res.get('code') != '0000' or not res.get('data'): + self.log(f"安全管家: 查询积分失败: {res.get('msg') if res else '无响应'}") + return + current = int(res['data'].get('availableScore', 0)) + today = res['data'].get('todayEarnScore', 0) + if not hasattr(self, 'sec_oldJFPoints') or self.sec_oldJFPoints is None: + self.sec_oldJFPoints = current + self.log(f"当前积分:{current},今日已赚 {today}") + else: + gained = current - self.sec_oldJFPoints + user_label = mask_str(self.mobile) if self.mobile else f"账号[{self.index}]" + self.log(f"安全管家: 用户{user_label}积分变动:{self.sec_oldJFPoints} → {current} | 新增: {gained}", notify=True) + return + except ValueError: + preview = ((response.text if response is not None else "") or "").strip().replace("\n", " ") + last_error = f"非JSON响应[{attempt}/2]: {preview[:60] or 'empty'}" + except Exception as e: + last_error = str(e) + if attempt < 2: + time.sleep(2) + self.log(f"安全管家: 查询积分异常: {last_error}") + + def sec_recover_pending_claims(self, rounds=2, delay=12, refresh_context=False): + if not self.sec_pending_claim_tasks: + return False + recovered = False + last_status_map = {} + self.log(f"联通助理:待补领任务 {len(self.sec_pending_claim_tasks)} 个,开始补查") + for attempt in range(rounds): + if attempt: + time.sleep(delay) + if not self.sec_pending_claim_tasks: + break + if refresh_context and not self.sec_refresh_security_context(refresh_secret=True): + continue + for task_code, task_name in list(self.sec_pending_claim_tasks.items()): + latest_task = self.sec_refresh_task_snapshot(task_code, task_name, retries=3, delay=2) + if not latest_task: + status_key = "missing" + if last_status_map.get(task_code) != status_key: + self.log(f"{task_name}:补查未取到任务状态") + last_status_map[task_code] = status_key + continue + finish_count, need_count, finish_text = self.sec_parse_task(latest_task) + if finish_text == "待领取" or (need_count > 0 and finish_count >= need_count): + self.log(f"{task_name}:补查后尝试领取") + receive_state = self.receivePoints_sec(task_code, task_name) + if receive_state in ("received", "auto"): + recovered = True + last_status_map.pop(task_code, None) + continue + else: + status_key = f"{finish_count}/{need_count}-{finish_text}" + if last_status_map.get(task_code) != status_key: + self.log(f"{task_name}:补查状态 {finish_count}/{need_count} - {finish_text}") + last_status_map[task_code] = status_key + if recovered: + self.getUserInfo_sec() + if self.sec_pending_claim_tasks: + names = "、".join(self.sec_pending_claim_tasks.values()) + self.log(f"联通助理:仍待补领 {names}") + return recovered + + def securityButlerTask(self, is_query_only=False): + self.log("==== 联通安全管家 ====") + if not self.ecs_token: + self.log("安全管家: 缺少 ecs_token,跳过") + return + try: + self.getTicketByNative_sec() + if not getattr(self, 'sec_ticket1', None): return + self.getAuthToken_sec() + if not getattr(self, 'sec_token', None): return + self.getTicketForJF_sec() + if not getattr(self, 'sec_ticket', None): return + self.sec_oldJFPoints = None + self.getUserInfo_sec() + if is_query_only: + self.log("联通助理积分:[查询模式] 跳过任务执行") + return + self.get_secret_key_sec() + self.executeAllTasks_sec() + self.log("等待积分到账,等待一会...") + self.sec_recover_pending_claims(rounds=2, delay=12, refresh_context=True) + time.sleep(3) + self.getUserInfo_sec() + except Exception as e: + self.log(f"安全管家: 异常: {e}") + + def aiting_query_integral(self): + url = "https://m.jf.10010.com/jf-external-application/jftask/userInfo" + response = self.session.post(url, json={}, headers=self.aiting_jf_headers()) + self.update_aiting_jea_id(response) + res = response.json() + if res.get('code') == '0000': + data = res.get('data', {}) + self.log(f"爱听任务: 积分概览 - 今日已赚 {data.get('todayEarnScore')}, 当前余额 {data.get('availableScore')}", notify=True) + + def aiting_jf_headers(self, with_signature=False): + headers = { + 'ticket': unquote(self.aiting_biz_ticket), + 'pageid': 's789081246969976832', + 'clienttype': 'aiting_android', + 'partnersid': '1706', + 'content-type': 'application/json;charset=UTF-8', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; Redmi K30 Pro Build/SKQ1.220303.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.159 Mobile Safari/537.36 WoReaderApp/Android', + 'Origin': 'https://m.jf.10010.com', + 'Host': 'm.jf.10010.com', + } + jea_id = getattr(self, 'aiting_jeaId', '') + if jea_id: + headers['Cookie'] = f"_jea_id={jea_id};" + if with_signature: + headers.update(self.build_signature_headers_aiting()) + return headers + + def update_aiting_jea_id(self, response=None): + jea_id = '' + if response is not None: + cookie = response.headers.get('Set-Cookie', '') + match = re.search(r'_jea_id=([^;]+)', cookie) + if match: + jea_id = match.group(1) + if not jea_id: + for cookie_item in self.session.cookies: + if cookie_item.name == '_jea_id': + jea_id = cookie_item.value + break + if jea_id: + self.aiting_jeaId = jea_id + return jea_id + + def get_secret_key_aiting(self): + if getattr(self, 'aiting_secretKey', None): + return self.aiting_secretKey + try: + self.update_aiting_jea_id() + res = self.session.get( + "https://m.jf.10010.com/jf-external-application/jftask/getSecretKey", + headers=self.aiting_jf_headers(), + timeout=10, + ) + self.update_aiting_jea_id(res) + data = res.json() + secret = data.get('data', {}).get('secretKey') + if data.get('code') == '0000' and secret: + self.aiting_secretKey = secret.encode('utf-8') + return self.aiting_secretKey + self.log(f"爱听任务: getSecretKey 失败: {data}") + except Exception as e: + self.log(f"爱听任务: getSecretKey 异常: {e}") + return None + + def build_signature_headers_aiting(self): + secret_key = self.get_secret_key_aiting() + if not secret_key: + return {} + request_ts = str(round(time.time() * 1000)) + nonce = ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyz', k=8)) + signature = hmac.new( + secret_key, f"{nonce}{request_ts}".encode('utf-8'), hashlib.sha256, + ).hexdigest() + return { + 'x-request-timestamp': request_ts, + 'x-request-nonce': nonce, + 'x-request-signature': signature, + } + + def ltzf_task(self): + self.log("==== 联通祝福 ====") + base_url = "https://wocare.unisk.cn/mbh/getToken" + params = { + "channelType": WOCARE_CONSTANTS["serviceLife"], + "homePage": "home", + "duanlianjieabc": "qAz2m" + } + targetUrl = f"{base_url}?{urlencode(params)}" + res = self.openPlatLineNew(targetUrl) + if not res or 'ticket' not in res: + self.log("联通祝福: 获取Ticket失败") + return + ticket = res['ticket'] + if not self.wocare_getToken(ticket): + self.log("联通祝福: 获取Wocare Token失败") + return + self.wocare_getSpecificityBanner() + wocare_activities = [ + {"name": "星座配对", "id": 2}, + {"name": "大转盘", "id": 3}, + {"name": "盲盒抽奖", "id": 4} + ] + for activity in wocare_activities: + self.wocare_getDrawTask(activity) + self.wocare_loadInit(activity) + + def openPlatLineNew(self, to_url): + try: + base_url = "https://m.client.10010.com/mobileService/openPlatform/openPlatLineNew.htm" + params = {"to_url": to_url} + for attempt in range(1, 4): + try: + res = self.session.get(base_url, params=params, allow_redirects=False, timeout=15) + break + except Exception as e: + err_msg = str(e) + if attempt < 3 and os.environ.get("UNICOM_PROXY_API") and ("Max retries exceeded" in err_msg or "timed out" in err_msg.lower() or "connection" in err_msg.lower() or "SOCKS" in err_msg): + self.log(f"openPlatLineNew 第{attempt}次异常触发故障转移: {err_msg}") + self.failover_proxy() + continue + self.log(f"openPlatLineNew 第{attempt}次重试 - 异常: {e}") + if attempt == 3: + return None + time.sleep(2) + if res.status_code == 302 and 'Location' in res.headers: + loc = res.headers['Location'] + parsed = urlparse(loc) + qs = parse_qs(parsed.query) + ticket = qs.get('ticket', [''])[0] + type_val = qs.get('type', [''])[0] + if ticket: + return {'ticket': ticket, 'type': type_val, 'loc': loc} + else: + self.log("openPlatLineNew: 重定向URL中无ticket") + else: + self.log(f"openPlatLineNew: 状态码{res.status_code} (期望302)") + except Exception as e: + self.log(f"openPlatLineNew 异常: {str(e)}") + return None + + def random_string(self, length, chars=string.ascii_letters + string.digits): + return ''.join(random.choice(chars) for _ in range(length)) + + def get_bizchannelinfo(self): + info = { + "bizChannelCode": "225", + "disriBiz": "party", + "unionSessionId": "", + "stType": "", + "stDesmobile": "", + "source": "", + "rptId": self.rptId, + "ticket": "", + "tongdunTokenId": self.tokenId_cookie, + "xindunTokenId": self.unicomTokenId + } + return json.dumps(info) + + def get_epay_authinfo(self): + info = { + "mobile": "", + "sessionId": getattr(self, 'sessionId', ''), + "tokenId": getattr(self, 'tokenId', ''), + "userId": "" + } + return json.dumps(info) + + def ttlxj_task(self, is_query_only=False): + self.log("==== 天天领现金 ====") + for attempt in range(1, 31): + try: + ticket_res = self.openPlatLineNew("https://epay.10010.com/ci-mps-st-web/ttlxj/") + if not ticket_res or not ticket_res.get('ticket'): + if attempt < 30: + self.log(f"天天领现金: 获取Ticket失败,正在重试 ({attempt}/30)...") + time.sleep(2) + continue + else: + self.log("天天领现金: 获取Ticket失败,已达最大重试次数,跳过任务") + return + ticket = ticket_res['ticket'] + type_val = ticket_res['type'] + if self.ttlxj_authorize(ticket, type_val, ticket_res['loc']): + if self.ttlxj_auth_check(): + if is_query_only: + self.ttlxj_query_available() + return + self.ttlxj_do_tasks() + self.ttlxj_query_available() + break + else: + if attempt < 30: + self.log(f"天天领现金: 授权失败,正在重试 ({attempt}/30)...") + time.sleep(2) + else: + self.log("天天领现金: 授权失败,已达最大重试次数") + except Exception as e: + if attempt < 30: + self.log(f"天天领现金: 任务异常 ({e}),正在重试 ({attempt}/30)...") + time.sleep(2) + else: + self.log(f"天天领现金: 任务异常: {e}") + + def ttlxj_authorize(self, ticket, type_val, referer_url): + try: + url = "https://epay.10010.com/woauth2/v2/authorize" + headers = { + "Origin": "https://epay.10010.com", + "Referer": referer_url + } + payload = { + "response_type": "rptid", + "client_id": "73b138fd-250c-4126-94e2-48cbcc8b9cbe", + "redirect_uri": "https://epay.10010.com/ci-mps-st-web/", + "login_hint": { + "credential_type": "st_ticket", + "credential": ticket, + "st_type": type_val, + "force_logout": True, + "source": "app_sjyyt" + }, + "device_info": { + "token_id": f"chinaunicom-pro-{int(time.time()*1000)}-{self.random_string(13)}", + "trace_id": self.random_string(32) + } + } + res = self.session.post(url, json=payload, headers=headers, timeout=10) + if res.status_code == 200: + return True + else: + self.log(f"天天领现金: Authorize失败[{res.status_code}]: {res.text}") + return False + except Exception as e: + self.log(f"ttlxj_authorize error: {e}") + return False + + def ttlxj_auth_check(self): + try: + url = "https://epay.10010.com/ps-pafs-auth-front/v1/auth/check" + headers = { + "bizchannelinfo": self.get_bizchannelinfo() + } + res = self.session.post(url, headers=headers, json={}, timeout=10) + data = res.json() + code = data.get("code") + if code == "0000": + auth_info = data.get("data", {}).get("authInfo", {}) + self.sessionId = auth_info.get("sessionId", "") + self.tokenId = auth_info.get("tokenId", "") + self.epay_userId = auth_info.get("userId", "") + return True + elif code == "2101000100": + login_url = data.get("data", {}).get("woauth_login_url") + if login_url: + return self.ttlxj_login(login_url) + else: + self.log(f"天天领现金: AuthCheck失败[{code}]: {data.get('msg')}") + return False + except Exception as e: + self.log(f"ttlxj_auth_check error: {e}") + return False + + def ttlxj_login(self, login_url): + try: + full_url = f"{login_url}https://epay.10010.com/ci-mcss-party-web/clockIn/?bizFrom=225&bizChannelCode=225" + res = self.session.get(full_url, allow_redirects=False, timeout=10) + if res.status_code == 302 and 'Location' in res.headers: + loc = res.headers['Location'] + parsed = urlparse(loc) + qs = parse_qs(parsed.query) + rptid = qs.get('rptid', [''])[0] + if rptid: + self.rptId = rptid + return self.ttlxj_auth_check() + else: + self.log("天天领现金: Login跳转后无rptid") + else: + self.log(f"天天领现金: Login失败[{res.status_code}]") + return False + except Exception as e: + self.log(f"ttlxj_login error: {e}") + return False + + def ttlxj_do_tasks(self): + info_url = "https://epay.10010.com/ci-mcss-party-front/v1/ttlxj/userDrawInfo" + headers = { + "bizchannelinfo": self.get_bizchannelinfo(), + "authinfo": self.get_epay_authinfo() + } + res = self.request("post", info_url, json={}, headers=headers) + if not res: return + data = res.json() + if data.get('code') == '0000': + day_of_week = data.get("data", {}).get("dayOfWeek", "") + draw_key = f"day{day_of_week}" + has_not_clocked_in = data.get("data", {}).get(draw_key) == "1" + if has_not_clocked_in: + self.log(f"天天领现金: 今天未打卡", notify=True) + today_js = (datetime.now().weekday() + 1) % 7 + draw_type = "C" if today_js == 0 else "B" + self.ttlxj_unifyDrawNew(draw_type) + else: + self.log(f"天天领现金: 今天已打卡", notify=True) + else: + self.log(f"天天领现金: 查询失败: {data.get('msg')}") + + def ttlxj_unifyDrawNew(self, draw_type): + draw_url = "https://epay.10010.com/ci-mcss-party-front/v1/ttlxj/unifyDrawNew" + headers = { + "bizchannelinfo": self.get_bizchannelinfo(), + "authinfo": self.get_epay_authinfo() + } + req_data = { + "drawType": draw_type, + "bizFrom": "225", + "activityId": "TTLXJ20210330" + } + res = self.request("post", draw_url, data=req_data, headers=headers) + if not res: return + data = res.json() + if data.get('code') == '0000': + prize = data.get('data', {}).get('prizeName', '未知奖品') + self.log(f"天天领现金: 抽奖成功: {prize}", notify=True) + else: + self.log(f"天天领现金: 抽奖失败: {data.get('msg')}") + + def ttlxj_query_available(self): + avail_url = "https://epay.10010.com/ci-mcss-party-front/v1/ttlxj/queryAvailable" + headers = { + "bizchannelinfo": self.get_bizchannelinfo(), + "authinfo": self.get_epay_authinfo() + } + res = self.request("post", avail_url, json={}, headers=headers) + if not res: return + data = res.json() + if data.get('code') == '0000': + d = data.get('data', {}) + amount_raw = int(d.get('availableAmount', '0')) + amount_yuan = f"{amount_raw / 100:.2f}" + msg = f"天天领现金: 可用立减金: {amount_yuan}元" + seven_day = int(d.get('sevenDayExpireAmount', 0)) + if seven_day > 0: + msg += f", 7天内过期立减金: {seven_day / 100:.2f}元" + min_exp_amt = int(d.get('minExpireAmount', 0)) + min_exp_date = d.get('minExpireDate') + if min_exp_amt > 0 and min_exp_date: + msg += f", 最早过期立减金: {min_exp_amt / 100:.2f}元 -- {min_exp_date}过期" + self.log(msg, notify=True) + else: + self.log(f"天天领现金: 查询余额失败: {data.get('msg')}") + + def ttxc_headers(self, auth=True, ecs=False): + headers = { + "user-agent": COMMON_CONSTANTS["MARKET_H5_UA"], + "content-type": "application/json", + "accept": "*/*", + "origin": "https://epay.10010.com", + "referer": TTXC_REFERER, + "x-requested-with": "com.sinovatech.unicom.ui", + } + if auth and getattr(self, "ttxc_token", ""): + headers["authorization"] = self.ttxc_token + if ecs and self.ecs_token: + headers["Cookie"] = f"ecs_token={self.ecs_token}" + return headers + + def ttxc_post(self, path, payload=None, auth=True, with_user=True, ecs=False): + data = dict(payload or {}) + if with_user: + data.setdefault("userId", getattr(self, "ttxc_user_id", "")) + data.setdefault("channel", TTXC_CHANNEL) + res = self.request("post", f"{TTXC_BASE_URL}{path}", json=data, headers=self.ttxc_headers(auth=auth, ecs=ecs), timeout=10) + if not res: + return {} + try: + return res.json() + except Exception: + return {} + + def ttxc_init_ttgame(self): + self.session.cookies.set("ecs_token", self.ecs_token) + url = f"{TTXC_APP_BASE_URL}/v1/login/ttGame?channel={TTXC_CHANNEL}&rptId=" + data = {} + for attempt in range(1, 4): + data = self.ttxc_json(self.request("post", url, json={"unicomTokenId": self.unicomTokenId}, headers=self.ttxc_headers(auth=False, ecs=True), timeout=10)) + if data.get("code") == "0000": + return True + if data.get("code") == "4003" and data.get("data") and self.ttxc_finish_woauth(data.get("data")): + data = self.ttxc_json(self.request("post", url, json={"unicomTokenId": self.unicomTokenId}, headers=self.ttxc_headers(auth=False, ecs=True), timeout=10)) + if data.get("code") == "0000": + return True + if attempt < 3: + time.sleep(2) + self.log(f"通通乡村: 初始化失败[{data.get('code')}]: {data.get('msg', '')}") + return False + + def ttxc_json(self, res): + if not res: + return {} + try: + return res.json() + except Exception: + return {} + + def ttxc_finish_woauth(self, login_url): + headers = { + "Referer": "https://epay.10010.com/", + "User-Agent": COMMON_CONSTANTS["MARKET_H5_UA"], + } + res = self.request("get", login_url, headers=headers, timeout=10) + if not res: + return False + match = re.search(r'var token = "([^"]+)"', res.text or "") + if not match: + return False + next_url = ( + "https://epay.10010.com/woauth2/after-collected-device-digest" + f"?deviceDigestTraceId=&deviceDigestTokenId=&token={quote(match.group(1))}&source=app_sjyyt" + ) + referer = login_url + for _ in range(6): + res = self.request("get", next_url, headers={"Referer": referer, "User-Agent": COMMON_CONSTANTS["MARKET_H5_UA"]}, allow_redirects=False, timeout=10) + if not res: + return False + location = res.headers.get("Location", "") + if not location: + return res.status_code == 200 + referer = next_url + next_url = location + return False + + def ttxc_login(self, update_nick=True): + if not self.ecs_token: + self.onLine() + if not self.ecs_token: + self.log("通通乡村: 缺少 ecs_token,跳过") + return False + if not self.ttxc_init_ttgame(): + return False + data = self.ttxc_post("/user/v1/login", auth=False, with_user=False, ecs=True) + if data.get("code") != 0: + self.log(f"通通乡村: 登录失败[{data.get('code')}]: {data.get('msg', '')}") + return False + user = data.get("data") or {} + self.ttxc_user_id = user.get("userId", "") + self.ttxc_token = data.get("token", "") + self.ttxc_charge_level = user.get("chargeLevel") or {} + self.ttxc_newbie_list = user.get("newbieList") + self.ttxc_nick_name = user.get("nickName") or "" + if not self.ttxc_user_id or not self.ttxc_token: + self.log("通通乡村: 登录响应缺少 userId/token") + return False + carbon = self.ttxc_charge_level.get("carbonNum", 0) + eco = self.ttxc_charge_level.get("ecologyAmount", 0) + self.log(f"通通乡村: 登录成功,碳能量{carbon}g,生态值{eco}", notify=True) + if update_nick and not self.ttxc_nick_name and self.ttxc_newbie_done(): + self.ttxc_update_nick() + return True + + def ttxc_update_nick(self): + nick = (self.account_mobile or self.mobile or "")[-4:] or str(random.randint(1000, 9999)) + data = self.ttxc_post("/user/v1/updateNick", {"nickName": nick}) + if data.get("code") == 0: + self.ttxc_nick_name = nick + self.log(f"通通乡村: 已设置昵称 {nick}") + return True + self.log(f"通通乡村: 设置昵称失败[{data.get('code')}]: {data.get('msg', '')}") + return False + + def ttxc_newbie_done(self): + steps = getattr(self, "ttxc_newbie_list", None) + return not isinstance(steps, list) or all(step in steps for step in TTXC_NEWBIE_STEPS) + + def ttxc_newbie_mark(self, step): + target = [] + for item in TTXC_NEWBIE_STEPS: + target.append(item) + if item == step: + break + data = self.ttxc_post("/user/v1/newbie", {"newbieList": target, "type": 1}) + if data.get("code") == 0: + self.ttxc_newbie_list = data.get("data") or target + return True + self.log(f"通通乡村: 新手步骤{step}失败[{data.get('code')}]: {data.get('msg', '')}") + return False + + def ttxc_newbie_need(self, step): + steps = getattr(self, "ttxc_newbie_list", None) + return isinstance(steps, list) and step not in steps + + def ttxc_first_newbie_land(self, lands=None): + lands = lands if lands is not None else self.ttxc_get_lands() + active = next((land for land in lands if land.get("status") in [2, 3] and (land.get("plant") or {}).get("plantId")), None) + if active: + return active + return next((land for land in lands if land.get("status") == 1), None) + + def ttxc_newbie_charge_land(self): + lands = self.ttxc_get_lands() + active = [land for land in lands if land.get("status") == 3 and (land.get("plant") or {}).get("plantId")] + return next((land for land in active if str((land.get("plant") or {}).get("curLevel")) in ["0", "1"]), None) or (active[0] if active else None) + + def ttxc_harvest_land(self, land, newbie=False): + if not land: + return None + plant = land.get("plant") or {} + plant_id = plant.get("plantId") + land_index = land.get("landIndex") + if not plant_id or not land_index: + return None + if land.get("status") == 2 and TTXC_HARVEST_WAIT_SECONDS > 0: + time.sleep(TTXC_HARVEST_WAIT_SECONDS) + path = "/plant/v1/newHarvest" if newbie else "/plant/v1/harvest" + data = self.ttxc_post(path, {"landIndex": land_index, "plantId": plant_id}) + if data.get("code") == 0: + self.log(f"通通乡村: 地块{land_index}收获成功") + return data.get("data") or {"landIndex": land_index, "status": 1, "plant": None} + self.log(f"通通乡村: 地块{land_index}收获失败[{data.get('code')}]: {data.get('msg', '')}") + return None + + def ttxc_newbie_task(self): + if self.ttxc_newbie_done(): + return False + need_farm = any(self.ttxc_newbie_need(step) for step in ["G03", "G03_2", "G04", "G05", "G09", "G10"]) + lands = self.ttxc_get_lands() if need_farm else [] + plant_id = "" + current = self.ttxc_first_newbie_land(lands) if need_farm else None + if need_farm: + self.ttxc_post("/client/v1/plant/type", {}) + plant_id = self.ttxc_get_plant_id() + if not plant_id: + self.log("通通乡村: 新手任务缺少作物ID") + return False + if self.ttxc_newbie_need("G03") and not self.ttxc_newbie_mark("G03"): + return False + if self.ttxc_newbie_need("G03_2"): + has_crop = current and current.get("status") in [2, 3] and (current.get("plant") or {}).get("plantId") + if not has_crop: + data = self.ttxc_post("/client/v1/plant/buy", {"plantId": plant_id, "gameCfgId": ""}) + if data.get("code") != 0: + self.log(f"通通乡村: 新手购买作物失败[{data.get('code')}]: {data.get('msg', '')}") + return False + if not self.ttxc_newbie_mark("G03_2"): + return False + if self.ttxc_newbie_need("G04"): + if not current or not current.get("landIndex"): + self.log("通通乡村: 新手任务缺少可种植地块") + return False + if not (current.get("status") in [2, 3] and (current.get("plant") or {}).get("plantId")): + data = self.ttxc_post("/plant/v1/planting", {"landIndex": current.get("landIndex"), "plantId": plant_id}) + if data.get("code") != 0: + self.log(f"通通乡村: 新手种植失败[{data.get('code')}]: {data.get('msg', '')}") + return False + current = data.get("data") or {"landIndex": current.get("landIndex"), "status": 3, "plant": {"plantId": plant_id}} + if not self.ttxc_newbie_mark("G04"): + return False + if self.ttxc_newbie_need("G05"): + current = current if current and current.get("plant") else self.ttxc_first_newbie_land() + current = self.ttxc_charge_land(current) + if not current or not self.ttxc_newbie_mark("G05"): + return False + if self.ttxc_newbie_need("G09"): + plant = (current or {}).get("plant") or {} + level = str(plant.get("curLevel") or "") + if not (current and current.get("status") == 3 and plant.get("plantId") and level in ["", "0", "1"]): + current = self.ttxc_newbie_charge_land() + current = self.ttxc_charge_land(current, mock=1) + if not current or not self.ttxc_newbie_mark("G09"): + return False + if self.ttxc_newbie_need("G10"): + current = current if current and current.get("plant") else self.ttxc_first_newbie_land() + if not self.ttxc_harvest_land(current, newbie=True) or not self.ttxc_newbie_mark("G10"): + return False + if self.ttxc_newbie_need("G11") and not self.ttxc_newbie_mark("G11"): + return False + if self.ttxc_newbie_need("G12"): + if not self.ttxc_nick_name and not self.ttxc_update_nick(): + return False + if not self.ttxc_newbie_mark("G12"): + return False + self.log("通通乡村: 新手任务已完成") + return True + + def ttxc_sign(self, is_query_only=False): + info = self.ttxc_post("/client/v1/sign/info", {}) + code = (info.get("data") or {}).get("signinCode") + if not code: + self.log("通通乡村: 获取签到码失败") + return + user = self.ttxc_post("/client/v1/sign/user", {"code": code}) + last_time = str((user.get("data") or {}).get("lastSigninTime") or "") + signed = last_time[:10] == datetime.now().strftime("%Y-%m-%d") + if signed: + self.log("通通乡村: 今日已签到", notify=True) + return + if is_query_only: + self.log("通通乡村: 今日未签到", notify=True) + return + data = self.ttxc_post("/client/v1/sign/signIn", {"code": code}) + if data.get("code") == 0: + sign_data = data.get("data") or {} + keep_value = safe_int(sign_data.get("keepSigninValue") or sign_data.get("lastKeepSigninValue") or sign_data.get("totalSigninValue")) + award_items = (info.get("data") or {}).get("awards") or [] + energy = 0 + for item in award_items: + if item.get("awardType") == "KEEP" and safe_int(item.get("signinValue")) == keep_value: + energy = safe_int(item.get("carbonEnergyAmount")) + break + if not energy: + charge_level = data.get("chargeLevel") or {} + before = safe_int(getattr(self, "ttxc_charge_level", {}).get("carbonNum")) + after = safe_int(charge_level.get("carbonNum")) + energy = max(after - before, 0) + if data.get("chargeLevel"): + self.ttxc_charge_level = data.get("chargeLevel") or self.ttxc_charge_level + msg = f"通通乡村: 签到成功 +{energy}g" if energy else "通通乡村: 签到成功" + self.log(msg, notify=True) + else: + self.log(f"通通乡村: 签到失败[{data.get('code')}]: {data.get('msg', '')}") + + def ttxc_get_tasks(self): + data = self.ttxc_post("/client/v1/task/list", {}) + if data.get("code") != 0: + self.log(f"通通乡村: 获取任务列表失败[{data.get('code')}]: {data.get('msg', '')}") + return [] + tasks = [] + for group in data.get("data") or []: + for task in group.get("taskList") or []: + task["taskGroupName"] = group.get("taskGroupName", "") + tasks.append(task) + return tasks + + def ttxc_finish_task(self, task): + task_id = task.get("taskCode") + if not task_id: + return False + data = self.ttxc_post("/client/v1/task/finish", {"taskId": task_id}) + name = task.get("taskTitle", task_id) + if data.get("code") == 0: + reward = task.get("carbonEnergyAmount") or 0 + self.log(f"通通乡村: 领取[{name}]成功 +{reward}g") + return True + self.log(f"通通乡村: 领取[{name}]失败[{data.get('code')}]: {data.get('msg', '')}") + return False + + def ttxc_do_task(self, task): + data = self.ttxc_post("/client/v1/task/do", {"taskId": task.get("taskCode")}) + name = task.get("taskTitle", task.get("taskCode", "")) + if data.get("code") == 0: + self.log(f"通通乡村: 已执行[{name}]") + return True + self.log(f"通通乡村: 执行[{name}]失败[{data.get('code')}]: {data.get('msg', '')}") + return False + + def ttxc_claim_ready_tasks(self, tasks, claimed=None): + if claimed is None: + claimed = set() + count = 0 + for task in tasks: + task_id = task.get("taskCode") + if task.get("taskStatus") == "UNCLA" and task_id not in claimed: + if self.ttxc_finish_task(task): + claimed.add(task_id) + count += 1 + return count + + def ttxc_do_jump_tasks(self, tasks): + count = 0 + for task in tasks: + if task.get("taskType") == "GAME" and task.get("taskStatus") == "UNDO" and task.get("jumpUrl"): + if self.ttxc_do_task(task): + count += 1 + time.sleep(1) + return count + + def ttxc_do_garbage_task(self, tasks): + task = next((t for t in tasks if t.get("taskType") == "GAME" and t.get("taskStatus") == "UNDO" and "垃圾分类" in t.get("taskTitle", "")), None) + if not task: + return False + start = self.ttxc_post("/user/v1/start", {}) + answer_no = (start.get("data") or {}).get("answerNo") + if not answer_no: + self.log("通通乡村: 垃圾分类开始失败") + return False + time.sleep(TTXC_GARBAGE_WAIT_SECONDS) + data = self.ttxc_post("/user/v1/finish", {"answerNo": answer_no}) + if data.get("code") == 0: + self.log("通通乡村: 垃圾分类已通关") + return True + self.log(f"通通乡村: 垃圾分类通关失败[{data.get('code')}]: {data.get('msg', '')}") + return False + + def ttxc_prepare_newbie_energy(self, claimed=None): + if claimed is None: + claimed = set() + self.ttxc_sign() + tasks = self.ttxc_get_tasks() + self.ttxc_claim_ready_tasks(tasks, claimed) + self.ttxc_do_jump_tasks(tasks) + self.ttxc_do_garbage_task(tasks) + tasks = self.ttxc_get_tasks() + self.ttxc_claim_ready_tasks(tasks, claimed) + + def ttxc_get_lands(self): + land = safe_int(getattr(self, "ttxc_charge_level", {}).get("land"), 4) + data = self.ttxc_post("/plant/v1/user", {"land": land}) + if data.get("code") != 0: + self.log(f"通通乡村: 获取土地失败[{data.get('code')}]: {data.get('msg', '')}") + return [] + return data.get("data") or [] + + def ttxc_get_plant_id(self): + data = self.ttxc_post("/client/v1/plant/page", {"itemType": "SPE", "pageNum": 1, "pageSize": 20}) + items = (data.get("data") or {}).get("list") or [] + return items[0].get("itemNo", "") if items else "" + + def ttxc_plant_land(self, land_index, plant_id=None): + plant_id = plant_id or self.ttxc_get_plant_id() + if not plant_id or not land_index: + return None + self.ttxc_post("/client/v1/plant/buy", {"plantId": plant_id, "gameCfgId": ""}) + data = self.ttxc_post("/plant/v1/planting", {"landIndex": land_index, "plantId": plant_id}) + if data.get("code") == 0: + self.log(f"通通乡村: 已在地块{land_index}种植作物") + return {"landIndex": land_index, "status": 3, "plant": {"plantId": plant_id}} + self.log(f"通通乡村: 地块{land_index}种植失败[{data.get('code')}]: {data.get('msg', '')}") + return None + + def ttxc_ensure_planted_lands(self, lands, needed=None): + active = [l for l in lands if l.get("status") in [2, 3] and (l.get("plant") or {}).get("plantId")] + empty = [l for l in lands if l.get("status") == 1] + if not empty: + return active + plant_id = self.ttxc_get_plant_id() + if not plant_id: + return active + for land in empty: + planted = self.ttxc_plant_land(land.get("landIndex"), plant_id) + if planted: + active.append(planted) + return active + + def ttxc_charge_land(self, land, mock=None): + if not land: + return False + plant = land.get("plant") or {} + plant_id = plant.get("plantId") + land_index = land.get("landIndex") + if not plant_id or not land_index: + return False + data = self.ttxc_post("/plant/v1/charge", {"landIndex": land_index, "plantId": plant_id, "mock": mock}) + if data.get("code") == 0: + self.log(f"通通乡村: 地块{land_index}充能成功") + result = data.get("data") or {} + if result and not result.get("plant"): + result["plant"] = plant + return result or land + self.log(f"通通乡村: 地块{land_index}充能失败[{data.get('code')}]: {data.get('msg', '')}") + return None + + def ttxc_harvest_and_replant(self, land): + harvested = self.ttxc_harvest_land(land) + return self.ttxc_plant_land(land.get("landIndex")) if harvested and land else None + + def ttxc_grow_land_to_harvest(self, land): + current = land + if current.get("status") == 2: + self.ttxc_harvest_and_replant(current) + return + charged = 0 + while current.get("status") == 3 and charged < TTXC_GROW_MAX_CHARGE_PER_LAND: + current = self.ttxc_charge_land(current) + if not current: + return + charged += 1 + if current.get("status") == 2: + self.ttxc_harvest_and_replant(current) + return + if charged < TTXC_GROW_MAX_CHARGE_PER_LAND: + time.sleep(1) + if current.get("status") == 3: + self.log(f"通通乡村: 地块{current.get('landIndex')}催熟达到上限,跳过") + + def ttxc_replace_land(self, lands, updated): + land_index = updated.get("landIndex") + for i, land in enumerate(lands): + if land.get("landIndex") == land_index: + lands[i] = updated + return + lands.append(updated) + + def ttxc_complete_charge_task(self, active, remaining): + while remaining > 0: + immature = [land for land in active if land.get("status") == 3 and (land.get("plant") or {}).get("plantId")] + if not immature: + self.log("通通乡村: 未成熟作物不足,提前结束10次充能补足") + return + progressed = False + for land in immature: + if remaining <= 0: + return + result = self.ttxc_charge_land(land) + if result: + self.ttxc_replace_land(active, result) + remaining -= 1 + progressed = True + time.sleep(1) + if not progressed: + self.log("通通乡村: 充能未成功,提前结束10次充能补足") + return + + def ttxc_farm_tasks(self, tasks): + charge_task = next((t for t in tasks if "10次作物充能" in t.get("taskTitle", "")), None) + land_task = next((t for t in tasks if "三块不同" in t.get("taskTitle", "")), None) + harvest_task = next((t for t in tasks if "收获一次作物" in t.get("taskTitle", "")), None) + if not charge_task and not land_task and not harvest_task: + return + charge_pending = charge_task if (charge_task or {}).get("taskStatus") == "UNDO" else None + land_pending = land_task if (land_task or {}).get("taskStatus") == "UNDO" else None + harvest_pending = harvest_task if (harvest_task or {}).get("taskStatus") == "UNDO" else None + if not charge_pending and not land_pending and not harvest_pending: + return + lands = self.ttxc_get_lands() + active = self.ttxc_ensure_planted_lands(lands) + need_land = max(safe_int((land_pending or {}).get("finishValue")) - safe_int((land_pending or {}).get("doneValue")), 0) + if harvest_pending and not charge_pending and not land_pending: + for land in active: + if land.get("status") == 2: + self.ttxc_harvest_and_replant(land) + if not active: + self.log("通通乡村: 没有可充能作物") + return + charged = 0 + for i, land in enumerate(active[:need_land]): + result = self.ttxc_charge_land(land) + if result: + active[i] = result + charged += 1 + time.sleep(1) + need_charge = max(safe_int((charge_pending or {}).get("finishValue")) - safe_int((charge_pending or {}).get("doneValue")) - charged, 0) + self.ttxc_complete_charge_task(active, need_charge) + for land in active: + self.ttxc_grow_land_to_harvest(land) + + def ttxc_task(self, is_query_only=False): + self.log("==== 通通乡村 ====") + try: + if not self.ttxc_login(update_nick=not is_query_only): + return + claimed = set() + if not is_query_only: + if not self.ttxc_newbie_done(): + self.ttxc_prepare_newbie_energy(claimed) + if not self.ttxc_newbie_done() and not self.ttxc_newbie_task(): + return + self.ttxc_sign(is_query_only=is_query_only) + tasks = self.ttxc_get_tasks() + if is_query_only: + todo = sum(1 for t in tasks if t.get("taskStatus") == "UNDO") + claim = sum(1 for t in tasks if t.get("taskStatus") == "UNCLA") + self.log(f"通通乡村: 待做{todo}个,可领取{claim}个", notify=True) + return + self.ttxc_claim_ready_tasks(tasks, claimed) + self.ttxc_do_jump_tasks(tasks) + self.ttxc_do_garbage_task(tasks) + self.ttxc_farm_tasks(tasks) + tasks = self.ttxc_get_tasks() + self.ttxc_claim_ready_tasks(tasks, claimed) + except Exception as e: + self.log(f"通通乡村异常: {e}") + + def aiting_get_aes(self, data, key): + iv_str = "16-Bytes--String" + key_bytes = key[:16].encode('utf-8') + iv_bytes = iv_str[:16].encode('utf-8') + text = json.dumps(data, separators=(',', ':')) if isinstance(data, (dict, list)) else str(data) + padded_data = pad(text.encode('utf-8'), 16) + cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) + ciphertext = cipher.encrypt(padded_data) + hex_str = ciphertext.hex() + return base64.b64encode(hex_str.encode('utf-8')).decode('utf-8') + + def aiting_aes_encrypt(self, data, key, iv): + key_bytes = key.encode('utf-8') + iv_bytes = iv.encode('utf-8') + text = json.dumps(data, separators=(',', ':')) if isinstance(data, (dict, list)) else str(data) + padded_data = pad(text.encode('utf-8'), 16) + cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) + ciphertext = cipher.encrypt(padded_data) + hex_str = ciphertext.hex().upper() + return base64.b64encode(hex_str.encode('utf-8')).decode('utf-8') + + def aiting_md5(self, text): + return hashlib.md5(text.encode('utf-8')).hexdigest() + + def aiting_generate_sign(self, params, key): + sorted_keys = sorted(params.keys()) + sign_str = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) + final_str = f"{sign_str}&key={key}" + return self.aiting_md5(final_str) + + def aiting_timestamp(self): + return str(int(time.time() * 1000)) + + def aiting_nonce(self): + return str(random.randint(100000, 999999)) + + def aiting_generate_woid(self, imei): + random6 = ''.join(random.choices(string.ascii_letters + string.digits, k=6)) + imei8 = imei[:8] if len(imei) >= 8 else imei.ljust(8, '0') + random4 = ''.join(random.choices(string.ascii_letters + string.digits, k=4)) + random2 = ''.join(random.choices(string.ascii_letters + string.digits, k=2)) + return f"WOA{random6}{imei8}LOT{random4}LV{random2}" + + def aiting_calculate_clientconfirm(self, userid, imei): + plaintext = f"android{userid}{imei}" + return self.aiting_aes_encrypt(plaintext, AITING_AES_KEY, AITING_AES_IV) + + def aiting_calculate_passcode(self, timestamp, phone): + return self.aiting_md5(timestamp + phone + AITING_CLIENT_KEY) + + def aiting_build_statisticsinfo(self, userid, useraccount, imei, clientconfirm): + params = { + 'channelid': '28015001', + 'sid': ''.join(random.choices(string.ascii_letters + string.digits + "_-", k=20)), + 'eid': ''.join(random.choices(string.ascii_letters + string.digits + "_", k=20)), + 'osversion': 'Android12', + 'clientallid': '000000100000000000058.0.2.1225', + 'display': '2400_1080', + 'ip': '192.168.3.24', + 'nettypename': 'wifi', + 'version': '802', + 'versionname': '8.0.2', + 'terminalName': 'Redmi', + 'terminalType': 'Redmi_K30_Pro', + 'udid': 'null', + 'woid': self.aiting_generate_woid(imei), + 'useraccount': useraccount, + 'userid': userid, + 'clientconfirm': clientconfirm + } + return '&'.join([f"{k}={params[k]}" for k in params]) + + def generate_random_imei(self): + tac = ''.join([str(random.randint(0, 9)) for _ in range(8)]) + snr = ''.join([str(random.randint(0, 9)) for _ in range(6)]) + imei_raw = tac + snr + digits = [int(d) for d in imei_raw] + for i in range(len(digits) - 1, -1, -2): + digits[i] *= 2 + if digits[i] > 9: digits[i] -= 9 + total = sum(digits) + check_digit = (10 - (total % 10)) % 10 + return imei_raw + str(check_digit) + + def aiting_woread_login(self, phone): + access_token = "ODZERTZCMjA1NTg1MTFFNDNFMThDRDYw" + token_enc = "" + if self.token_online: + token_enc = self.aiting_get_aes(self.token_online, WOREAD_KEY) + else: + self.log("阅读专区: 未找到 token_online,尝试仅使用手机号登录") + phone_enc = self.aiting_get_aes(phone, WOREAD_KEY) + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + if token_enc: + inner_data = { + "tokenOnline": token_enc, + "phone": phone_enc, + "timestamp": timestamp + } + else: + inner_data = { + "phone": phone_enc, + "timestamp": timestamp + } + sign_result = self.aiting_get_aes(inner_data, WOREAD_KEY) + url = "https://10010.woread.com.cn/ng_woread_service/rest/account/login" + body = {"sign": sign_result} + headers = { + "User-Agent": "Mozilla/5.0 (Linux; Android 11; Redmi Note 10 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.159 Mobile Safari/537.36", + "accesstoken": access_token, + "Content-Type": "application/json;charset=UTF-8", + "Origin": "https://10010.woread.com.cn" + } + res = self.session.post(url, json=body, headers=headers).json() + if res.get("code") == "0000": + return res.get("data", {}).get("token") + self.log(f"爱听登录: 沃阅读登录失败: {res}") + return None + + def aiting_get_jwt_token(self, statisticsinfo): + timestamp = self.aiting_timestamp() + sign_params = { + 'clientSource': '3', + 'clientId': 'android', + 'source': '3', + 'timestamp': timestamp + } + sign_val = self.aiting_generate_sign(sign_params, AITING_SIGN_KEY_APPKEY) + client_id_const = "395DEDE9C1D6FE11B7C9C0D82B353E74" + client_id_b64 = base64.b64encode(client_id_const.encode('utf-8')).decode('utf-8') + body = { + 'clientSource': '3', + 'clientId': client_id_b64, + 'source': '3', + 'timestamp': timestamp, + 'sign': sign_val + } + url = f"{AITING_BASE_URL}/oauth/client/appkey" + headers = { + 'Skip-Authorization-Check': 'true', + 'statisticsinfo': statisticsinfo, + "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 12; Redmi K30 Pro Build/SKQ1.220303.001)" + } + try: + res = self.session.post(url, json=body, headers=headers).json() + if res.get("code") == "0000" and res.get("key"): + return res.get("key") + self.log(f"爱听登录: 获取JWT失败: {res}") + except Exception as e: + self.log(f"爱听登录: 获取JWT异常: {e}") + return None + + def aiting_api_login(self, phone, useraccount, jwt_token, statisticsinfo): + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + passcode = self.aiting_calculate_passcode(timestamp, phone) + query_params_list = [ + 'networktype=3', 'ua=Redmi+K30+Pro', 'isencode=false', + 'clientversion=8.0.2', 'versionname=Android_1_1080x2356', + 'channelid=28015001', 'userlabelisencode=0', 'validatecode=', 'sid=', + f"timestamp={timestamp}", f"passcode={passcode}" + ] + query_str = '&'.join(query_params_list) + final_account = useraccount + url = f"{AITING_BASE_URL}/mainrest/rest/read/user/ulogin/3/{final_account}/1/1/0?{query_str}" + req_time = self.aiting_timestamp() + nonce = self.aiting_nonce() + sign_params = { + 'jwt': jwt_token, + 'nonestr': nonce, + 'osversion': 'Android12', + 'terminalName': 'Redmi', + 'timestamp': req_time + } + sorted_keys = sorted(sign_params.keys()) + sign_str = '&'.join([f"{k}={sign_params[k]}" for k in sorted_keys]) + requertid = self.aiting_md5(f"{sign_str}&key={AITING_SIGN_KEY_REQUERTID}") + headers = { + 'statisticsinfo': statisticsinfo, + 'requerttime': req_time, + 'nonestr': nonce, + 'requertid': requertid, + 'AuthorizationClient': f"Bearer {jwt_token}", + 'User-Agent': 'okhttp/4.9.0' + } + try: + res = self.session.get(url, headers=headers).json() + if res.get("code") == "0000" and res.get("message"): + msg = res.get("message") + token = msg.get("token") + userid = msg.get("userid") + if msg.get("accountinfo"): + token = msg.get("accountinfo", {}).get("token") or token + userid = msg.get("accountinfo", {}).get("userid") or userid + return {"token": token, "userid": userid} + self.log(f"爱听登录: 业务API登录失败: {res}") + except Exception as e: + self.log(f"爱听登录: 业务API异常: {e}") + return None + + def aiting_login_flow(self): + self.log("正在执行爱听登录流程...") + woread_token = self.aiting_woread_login(self.mobile) + if not woread_token: return False + self.aiting_woread_token = woread_token + imei = self.generate_random_imei() + userid = self.mobile + useraccount = self.mobile + clientconfirm = self.aiting_calculate_clientconfirm(userid, imei) + statisticsinfo = self.aiting_build_statisticsinfo(userid, useraccount, imei, clientconfirm) + self.aiting_statisticsinfo = statisticsinfo + jwt = self.aiting_get_jwt_token(statisticsinfo) + if not jwt: return False + self.aiting_jwt = jwt + login_data = self.aiting_api_login(self.mobile, useraccount, jwt, statisticsinfo) + if not login_data: return False + self.aiting_biz_token = login_data.get('token') + self.aiting_base_userid = login_data.get('userid') or self.mobile + self.log(f"✅ 爱听业务登录成功! Token已获取") + biz_ticket = self.aiting_get_ticket() + if biz_ticket: + self.aiting_biz_ticket = biz_ticket + return True + return False + + def aiting_get_ticket(self): + url = f"{AITING_BASE_URL}/activity/rest/unicom/points/getInfoTicket" + timestamp = self.aiting_timestamp() + sign_params = { + "token": self.aiting_biz_token, + "timestamp": timestamp, + "userid": self.aiting_base_userid + } + sign_val = self.aiting_generate_sign(sign_params, AITING_SIGN_KEY_API) + body = { + "sign": sign_val, + "timestamp": timestamp, + "token": self.aiting_biz_token, + "userid": self.aiting_base_userid + } + nonce = self.aiting_nonce() + head_sign_params = { + 'jwt': self.aiting_jwt, + 'nonestr': nonce, + 'osversion': 'Android12', + 'terminalName': 'Redmi', + 'timestamp': timestamp + } + sorted_keys = sorted(head_sign_params.keys()) + sign_str = '&'.join([f"{k}={head_sign_params[k]}" for k in sorted_keys]) + final_sign_str = f"{sign_str}&key={AITING_SIGN_KEY_REQUERTID}" + requertid = self.aiting_md5(final_sign_str) + headers = { + "AuthorizationClient": f"Bearer {self.aiting_jwt}", + "statisticsinfo": self.aiting_statisticsinfo, + "requerttime": timestamp, + "nonestr": nonce, + "requertid": requertid + } + try: + res = self.session.post(url, json=body, headers=headers).json() + if res.get("code") == "0000": + msg = res.get("message", "") + if "ticket=" in msg: + match = re.search(r'ticket=([^&]+)', msg) + if match: + return match.group(1) + return msg # Fallback if message is ticket itself? No, standard is URL. + self.log(f"爱听登录: 获取Ticket失败: {res}") + except Exception as e: + self.log(f"爱听登录: 获取Ticket异常: {e}") + return None + + def jf_get_task_detail(self, ticket): + url = "https://m.jf.10010.com/jf-external-application/jftask/taskDetail" + headers = self.aiting_jf_headers() + headers['Referer'] = f"https://m.jf.10010.com/jf-external-application/index.html?ticket={ticket}&pageID=s789081246969976832" + response = self.session.post(url, json={}, headers=headers) + self.update_aiting_jea_id(response) + try: + res = response.json() + except Exception: + self.log(f" ⚠️ 积分任务列表响应非JSON (状态码{response.status_code})") + return [] + return res.get("data", {}).get("taskDetail", {}).get("taskList", []) + + def jf_to_finish(self, ticket, task_code): + url = "https://m.jf.10010.com/jf-external-application/jftask/toFinish" + response = self.session.post( + url, + json={'taskCode': task_code}, + headers=self.aiting_jf_headers(with_signature=True), + ) + self.update_aiting_jea_id(response) + + def jf_pop_up(self, ticket): + url = "https://m.jf.10010.com/jf-external-application/jftask/popUp" + response = self.session.post(url, json={}, headers=self.aiting_jf_headers()) + self.update_aiting_jea_id(response) + try: + res = response.json() + except Exception: + self.log(f" └─ ⚠️ 积分弹窗响应非JSON (状态码{response.status_code})") + return {} + if isinstance(res, dict): + if res.get('code') == "0000" and res.get('data', {}).get('score'): + self.log(f" └─ 🎉 爱听任务: 获得 {res['data']['score']}", notify=True) + elif res.get('code') != "0000": + self.log(f" └─ 📝 积分弹窗返回: {res.get('desc', res)}") + return res + + def aiting_complete_task_api(self, type_val): + timestamp = self.aiting_timestamp() + nonce = self.aiting_nonce() + sign_params = {'jwt': self.aiting_jwt, 'nonestr': nonce, 'osversion': 'Android12', 'terminalName': 'Redmi', 'timestamp': timestamp} + sign_str = '&'.join([f"{k}={sign_params[k]}" for k in sorted(sign_params.keys())]) + requertid = self.aiting_md5(f"{sign_str}&key={AITING_SIGN_KEY_REQUERTID}") + body_params = {'source': '3', 'timestamp': timestamp, 'token': self.aiting_woread_token, 'type': str(type_val), 'userid': self.aiting_base_userid} + body_str = '&'.join([f"{k}={body_params[k]}" for k in sorted(body_params.keys())]) + sign = self.aiting_md5(f"{body_str}&key={AITING_SIGN_KEY_API}") + url = f"{AITING_BASE_URL}/activity/rest/unicom/points/completiontask" + payload = {**body_params, 'sign': sign} + headers = { + 'AuthorizationClient': f"Bearer {self.aiting_jwt}", + 'requerttime': timestamp, + 'nonestr': nonce, + 'requertid': requertid, + 'statisticsinfo': self.aiting_statisticsinfo + } + self.session.post(url, json=payload, headers=headers) + + def aiting_get_secretkey(self): + timestamp = self.aiting_timestamp() + nonce = self.aiting_nonce() + sign_params = {'jwt': self.aiting_jwt, 'nonestr': nonce, 'osversion': 'Android12', 'terminalName': 'Redmi', 'timestamp': timestamp} + sign_str = '&'.join([f"{k}={sign_params[k]}" for k in sorted(sign_params.keys())]) + requertid = self.aiting_md5(f"{sign_str}&key={AITING_SIGN_KEY_REQUERTID}") + url = f"https://woread.com.cn/rest/read/statistics/getsecretkey/3/{self.aiting_base_userid}" + headers = { + 'AuthorizationClient': f"Bearer {self.aiting_jwt}", + 'requerttime': timestamp, 'nonestr': nonce, 'requertid': requertid, + 'statisticsinfo': self.aiting_statisticsinfo, 'User-Agent': 'okhttp/4.9.0' + } + params = {'token': self.aiting_woread_token} + res = self.session.get(url, params=params, headers=headers).json() + if res.get("code") == "0000": + return res.get("message") + return None + + def aiting_add_read_time(self, read_time_seconds): + secretkey = self.aiting_get_secretkey() + if not secretkey: return + timestamp = self.aiting_timestamp() + count_time_str = str(read_time_seconds * 1000) + book_id = "4524960" + data_obj = { + "userid": self.aiting_base_userid, + "counttime": count_time_str, + "timestamp": timestamp, + "secretkey": secretkey, + "cntindex": book_id, + "cnttype": 1, + "readtype": 1 + } + encrypted = self.aiting_aes_encrypt(data_obj, ADDREADTIME_AES_KEY, AITING_AES_IV) + nonce = self.aiting_nonce() + sign_params = {'jwt': self.aiting_jwt, 'nonestr': nonce, 'osversion': 'Android12', 'terminalName': 'Redmi', 'timestamp': timestamp} + sign_str = '&'.join([f"{k}={sign_params[k]}" for k in sorted(sign_params.keys())]) + requertid = self.aiting_md5(f"{sign_str}&key={AITING_SIGN_KEY_REQUERTID}") + url = f"https://woread.com.cn/rest/read/statistics/addreadtime/3/{encrypted}" + random_uuid = str(uuid.uuid4()).replace('-', '') + body = { + "channelid": "28015001", "creadertime": datetime.now().strftime("%y%m%d%H%M%S"), + "imei": self.generate_random_imei(), + "list": { "cntindex": book_id, "cnttype": 1, "readtime": count_time_str, "readtype": 1 }, + "list1": [{ "cntindex": book_id, "cnttype": 1, "readtime": count_time_str, "readtype": 1 }], + "listentimes": count_time_str, "uuid": random_uuid + } + headers = { + 'AuthorizationClient': f"Bearer {self.aiting_jwt}", + 'requerttime': timestamp, 'nonestr': nonce, 'requertid': requertid, + 'statisticsinfo': self.aiting_statisticsinfo, 'User-Agent': 'okhttp/4.9.0' + } + res = self.session.post(url, json=body, headers=headers) + if res.status_code == 200: + self.last_read_submission_time = time.time() + self.log(f"✅ 阅读时长上报成功 ({read_time_seconds}s)") + + def aiting_new_read_add(self): + timestamp = self.aiting_timestamp() + nonce = self.aiting_nonce() + sign_params = {'jwt': self.aiting_jwt, 'nonestr': nonce, 'osversion': 'Android12', 'terminalName': 'Redmi', 'timestamp': timestamp} + sign_str = '&'.join([f"{k}={sign_params[k]}" for k in sorted(sign_params.keys())]) + requertid = self.aiting_md5(f"{sign_str}&key={AITING_SIGN_KEY_REQUERTID}") + url = f"https://woread.com.cn/rest/read/new/newreadadd/3/{self.aiting_base_userid}/{self.aiting_woread_token}" + params = {'isfreeLimt': '0', 'isgray': 'true'} + body = {"source": 3, "cntindex": "4524960", "chapterallindex": "100136247350", "readtype": 3} + headers = { + 'AuthorizationClient': f"Bearer {self.aiting_jwt}", 'requerttime': timestamp, 'nonestr': nonce, 'requertid': requertid, 'statisticsinfo': self.aiting_statisticsinfo, 'User-Agent': 'Redmi K30 Pro' + } + self.session.post(url, params=params, json=body, headers=headers) + + def aiting_task(self, is_query_only=False): + self.log("==== 联通爱听任务 ====") + if not self.aiting_login_flow(): + self.log("爱听任务: 登录失败,跳过") + return + self.log("爱听任务: 登录成功,正在获取任务列表...") + try: + self.aiting_query_integral() + except: pass + task_list = self.jf_get_task_detail(self.aiting_biz_ticket) + done_list = [t for t in task_list if t.get('finish') == 1] + printed_names = set() + for t in done_list: + name = t.get('taskName') + if name not in printed_names: + self.log(f" ✅ {name} ({t.get('finishCount')}/{t.get('needCount')})") + printed_names.add(name) + todo_list = [t for t in task_list if t.get('finish') == 0 and "邀请" not in t.get('taskName', '')] + if not todo_list: + self.log("爱听任务: ✅ 所有任务已完成") + if is_query_only: + self.log("爱听任务: [查询模式] 跳过任务执行...") + return + self.log(f"爱听任务: 发现 {len(todo_list)} 个待办任务") + if is_query_only: + self.log("爱听任务: [查询模式] 跳过任务执行...") + return + read_tasks = [t for t in todo_list if ("阅读" in t.get('taskName','') or "听读" in t.get('taskName','')) and "邀请" not in t.get('taskName','')] + for task in read_tasks: + remaining = int(task.get('needCount', 1)) - int(task.get('finishCount', 0)) + if remaining <= 0: continue + self.log(f"执行阅读任务: {task.get('taskName')} (剩余 {remaining} 次)") + for i in range(remaining): + self.jf_to_finish(self.aiting_biz_ticket, task.get('taskCode')) + self.log(f" └─ 第 {i + 1}/{remaining} 次: 极速提交中...") + self.aiting_new_read_add() + time.sleep(5) + self.aiting_add_read_time(120) + time.sleep(2) + self.jf_pop_up(self.aiting_biz_ticket) + other_tasks = [t for t in todo_list if not any(x in t.get('taskName','') for x in ["通知", "阅读", "听读", "邀请", "签到"])] + notify_task = next((t for t in todo_list if "通知" in t.get('taskName','')), None) + if notify_task: + self.log(f"执行通知任务: {notify_task.get('taskName')}") + self.jf_to_finish(self.aiting_biz_ticket, notify_task.get('taskCode')) + time.sleep(1) + self.aiting_complete_task_api(2) + time.sleep(2) + self.jf_pop_up(self.aiting_biz_ticket) + for task in other_tasks: + remaining = int(task.get('needCount', 1)) - int(task.get('finishCount', 0)) + if remaining <= 0: continue + self.log(f"执行通用任务: {task.get('taskName')} (剩余 {remaining} 次)") + for i in range(remaining): + self.jf_to_finish(self.aiting_biz_ticket, task.get('taskCode')) + time.sleep(1.5) + self.aiting_complete_task_api(4) # Type 4 + time.sleep(2) + self.jf_pop_up(self.aiting_biz_ticket) + try: + self.aiting_query_integral() + except: pass + + def wostore_cloud_login(self, ticket): + try: + url1 = "https://member.zlhz.wostore.cn/wcy_member/yunPhone/h5Awake/businessHall" + body1 = { + "cpId": "91002997", "channelId": "ST-Zujian001-gs", "ticket": ticket, + "env": "prod", "transId": "S2ndpage1235+开福袋!+F1+CJDD00D0001+iphone_c@12.0801", + "qkActId": None + } + headers1 = {"Origin": "https://h5forphone.wostore.cn", "Content-Type": "application/json"} + json_data = json.dumps(body1, separators=(',', ':'), ensure_ascii=True) + res1 = self.session.post(url1, data=json_data, headers=headers1, timeout=15).json() + if str(res1.get("code")) != "0": + msg = res1.get("msg", str(res1)) + self.log(f"沃云手机: 登录第一步失败 - {msg}") + return None + redirect_url = res1.get("data", {}).get("url", "") + match = re.search(r'token=([^&]+)', redirect_url) + if not match: + if "protocol" in redirect_url or "sign" in redirect_url: + self.log("沃云手机: 未开通业务 (检测到协议签署跳转),跳过") + else: + self.log(f"沃云手机: 无法提取 Token, 跳转URL: {redirect_url}") + return None + first_token = match.group(1) + time.sleep(1) + url2 = "https://uphone.wostore.cn/h5api/activity-service/user/login" + body2 = { + "identityType": "cloudPhoneLogin", "code": first_token, "channelId": "ST-Zujian001-gs", + "activityId": WOSTORE_CLOUD_ACTIVITY_CODE, "device": "device" + } + headers2 = {"Origin": "https://uphone.wostore.cn", "X-USR-TOKEN": first_token} + res2 = {} + for attempt in range(1, WOSTORE_CLOUD_RETRIES + 1): + try: + res2 = self.session.post(url2, json=body2, headers=headers2, timeout=WOSTORE_CLOUD_TIMEOUT).json() + break + except Exception as e: + if attempt >= WOSTORE_CLOUD_RETRIES: + raise + self.log(f"沃云手机: 登录第二步超时重试({attempt}/{WOSTORE_CLOUD_RETRIES}) - {e}") + time.sleep(2) + if str(res2.get("code")) == "200": + user_token = res2.get("data", {}).get("user_token") + return {"firstToken": first_token, "user_token": user_token} + else: + self.log(f"沃云手机: 登录第二步失败 - {res2.get('msg', str(res2))}") + return None + except Exception as e: + self.log(f"沃云手机: 登录异常 {e}") + return None + + def wostore_cloud_sign(self, user_token): + try: + url = "https://uphone.wostore.cn/h5api/activity-service/points/v1/sign" + body = {"activityCode": "Points_Sign_2507"} + headers = {"X-USR-TOKEN": user_token, "Origin": "https://uphone.wostore.cn"} + res = self.session.post(url, json=body, headers=headers).json() + if res.get("code") == 200: + self.log("沃云手机: 积分签到成功", notify=True) + else: + pass # Fail silently or log if needed context + except Exception: + pass + + def wostore_cloud_task_list(self, user_token): + try: + url = "https://uphone.wostore.cn/h5api/activity-service/user/task/list" + body = {"activityCode": WOSTORE_CLOUD_ACTIVITY_CODE} + headers = {"X-USR-TOKEN": user_token} + self.session.post(url, json=body, headers=headers) + except Exception: + pass + + def wostore_cloud_get_chance(self, user_token, task_code): + try: + url = "https://uphone.wostore.cn/h5api/activity-service/user/task/raffle/get" + body = {"activityCode": WOSTORE_CLOUD_ACTIVITY_CODE, "taskCode": task_code} + headers = {"X-USR-TOKEN": user_token} + self.session.post(url, json=body, headers=headers) + except Exception: + pass + + def wostore_cloud_draw(self, user_token): + try: + url = "https://uphone.wostore.cn/h5api/activity-service/lottery" + body = {"activityCode": WOSTORE_CLOUD_ACTIVITY_CODE} + headers = {"X-USR-TOKEN": user_token} + res = self.session.post(url, json=body, headers=headers).json() + if res.get("code") == 200: + prize = res.get("data", {}).get("prizeName", "未中奖") + self.log(f"沃云手机: 抽奖结果 - {prize}", notify=True) + else: + self.log(f"沃云手机: 抽奖失败 - {res.get('msg', str(res))}") + except Exception as e: + self.log(f"沃云手机: 抽奖异常 {e}") + + def wostore_cloud_task(self, is_query_only=False): + self.log("==== 沃云手机 ====") + if is_query_only: + self.log("沃云手机: [查询模式] 此平台暂无资产或余额可供查询", notify=True) + return + target_url = "https://h5forphone.wostore.cn/cloudPhone/dialogCloudPhone.html?channel_id=ST-Zujian001-gs&cp_id=91002997" + ticket_res = self.openPlatLineNew(target_url) + if not ticket_res: + self.log("沃云手机: 获取入口 Ticket 失败") + return + ticket = ticket_res + if isinstance(ticket, dict): + ticket = ticket.get('ticket') + if not ticket: + self.log("沃云手机: 获取入口 Ticket 失败 (为空)") + return + tokens = self.wostore_cloud_login(ticket) + if not tokens: + self.log("沃云手机: 登录失败,跳过后续任务") + return + user_token = tokens['user_token'] + self.wostore_cloud_sign(user_token) + time.sleep(2) + self.wostore_cloud_task_list(user_token) + time.sleep(1) + self.wostore_cloud_get_chance(user_token, "2508-01") + time.sleep(2) + self.wostore_cloud_draw(user_token) + + def regional_task(self, is_query_only=False): + """区域专区任务入口""" + is_xinjiang = False + is_henan = False + is_yunnan = False + is_liaoning = False + is_anhui = False + if hasattr(self, 'city_info') and self.city_info and isinstance(self.city_info, list): + try: + for city in self.city_info: + pro_name = city.get('proName', '') + if "新疆" in pro_name: is_xinjiang = True + if "河南" in pro_name: is_henan = True + if "云南" in pro_name: is_yunnan = True + if "辽宁" in pro_name: is_liaoning = True + if "安徽" in pro_name: is_anhui = True + except: pass + rc = globalConfig.get("regional_config", {}) + if is_query_only: + self.log("==== 区域专区 (查询模式) ====") + if is_xinjiang: + self.log("新疆专区: [查询模式] 跳过每日打卡,尝试查询每月抽奖记录") + try: + ticket_res = self.openPlatLineNew("https://zy100.xj169.com/touchpoint/openapi/jumpHandRoom1G?source=155&type=02") + if ticket_res and ticket_res.get("ticket"): + token = self.xj_get_token(ticket_res.get("ticket")) + if token: + self.xj_query_monthly_draw_records(token) + except Exception as e: + self.log(f"新疆专区: [查询模式] 查询每月抽奖记录异常 {e}") + if is_henan: + is_signed = self.shangdu_get_sign_status() + if is_signed is True: + self.log("河南商都: [状态查询] 今日已签到") + elif is_signed is False: + self.log("河南商都: [状态查询] 今日未签到") + else: + self.log("河南商都: [状态查询] 查询失败") + if is_yunnan: + self.yunnan_life_task(is_query_only=True) + if is_liaoning: + self.ln_flmf_task(is_query_only=True) + if is_anhui and AH_FRIDAY_AMOUNT: + self.log(f"安徽超级星期五: [查询模式] 目标面额{AH_FRIDAY_AMOUNT}元 (仅周五10点执行)") + return + if is_xinjiang: + self.log("==== 新疆专区 ====") + self.xj_task_main() + if is_henan: + self.log("==== 河南商都 ====") + self.shangdu_task_main() + if is_yunnan: + self.log("==== 云南生活 ====") + self.yunnan_life_task() + if is_liaoning: + self.log("==== 辽宁福利魔方 ====") + self.ln_flmf_task() + if is_anhui and AH_FRIDAY_AMOUNT and rc.get("run_ah_friday", True): + self.log("==== 安徽超级星期五 ====") + self.ah_friday_task() + + def yunnan_life_base_headers(self, token=None, extra=None): + headers = { + "Referer": "https://wsm.wx.yn10010.com/", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "Content-Type": "application/json;charset=UTF-8", + "Accept-Language": "zh-CN,en-US;q=0.8", + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.1001};ltst;OSVersion/16.6", + } + if token: + headers["token"] = token + if extra: + headers.update(extra) + return headers + + def yunnan_life_calc_sign(self, payload): + parts = [] + for key in sorted(payload.keys()): + value = payload[key] + if isinstance(value, dict): + encoded = quote(json.dumps(value, ensure_ascii=False, separators=(',', ':')), safe="") + else: + encoded = quote(str(value), safe="") + parts.append(f"{key}={encoded}") + raw = "&".join(parts).lower() + YUNNAN_LIFE_SIGN_SALT + return hashlib.md5(hashlib.md5(raw.encode('utf-8')).hexdigest().encode('utf-8')).hexdigest() + + def yunnan_life_signed_headers(self, token, payload): + return self.yunnan_life_base_headers(token, { + "Origin": YUNNAN_LIFE_BASE_URL, + "accessKeyId": YUNNAN_LIFE_ACCESS_KEY, + "time": str(round(time.time() * 1000)), + "sign": self.yunnan_life_calc_sign(payload), + }) + + def yunnan_life_get_ticket(self): + if not self.ecs_token: + return None + try: + res = self.session.get( + "https://m.client.10010.com/mobileService/openPlatform/openPlatLineNew.htm", + params={ + "to_url": YUNNAN_LIFE_TO_URL, + "amp;s": "100000425", + "amp;boothCode": "YN-QCQYCS245", + "amp;boothAccessMode": "24", + }, + headers={ + "Cookie": f"ecs_token={self.ecs_token}", + "Referer": "https://wsm.wx.yn10010.com/", + "Accept-Language": "zh-CN,zh-Hans;q=0.9", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Site": "cross-site", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "User-Agent": self.yunnan_life_base_headers().get("User-Agent"), + }, + allow_redirects=False, + timeout=15, + ) + location = res.headers.get("Location", "") + match = re.search(r'ticket=([^&]+)', location) + return match.group(1) if match else None + except Exception as e: + self.log(f"云南生活: 获取 ticket 异常: {e}") + return None + + def yunnan_life_get_token(self, ticket): + if not ticket: + return None + try: + resp = self.session.get( + f"{YUNNAN_LIFE_BASE_URL}/2b2c-mobile/getPhoneNumber", + params={"ticket": ticket}, + headers=self.yunnan_life_base_headers(extra={"Content-Type": "application/json;charset=gb2312"}), + timeout=15, + ) + token = resp.headers.get("token") or resp.headers.get("Token") + if not token: + try: + data = resp.json() + except Exception: + data = {} + token = data.get("token") or data.get("data", {}).get("token") + if not token: + self.log(f"云南生活: 未找到 token,响应: {resp.text[:160]}") + return None + return token if str(token).startswith("Bearer ") else f"Bearer {token}" + except Exception as e: + self.log(f"云南生活: 获取 token 异常: {e}") + return None + + def yunnan_life_login(self): + ticket = self.yunnan_life_get_ticket() + if not ticket: + self.log("云南生活: 获取 ticket 失败") + return None + token = self.yunnan_life_get_token(ticket) + if not token: + self.log("云南生活: 获取 token 失败") + return None + return token + + def yunnan_life_do_task(self, token, payload): + task_name = payload.get("taskName", payload.get("taskCode", "未知任务")) + try: + res = self.session.post( + f"{YUNNAN_LIFE_BASE_URL}/2b2c-mobile/activity/task/addTaskUser", + data=json.dumps(payload, ensure_ascii=False, separators=(',', ':')), + headers=self.yunnan_life_signed_headers(token, payload), + timeout=15, + ).json() + if res.get("resultCode") == "0000": + self.log(f"云南生活: ✅ {task_name}") + else: + self.log(f"云南生活: ❌ {task_name}: {res.get('resultMsg', '')}") + except Exception as e: + self.log(f"云南生活: [{task_name}] 异常: {e}") + + def yunnan_life_do_lottery(self, token, times=2): + payload = {"actId": YUNNAN_LIFE_ACT_ID, "boothCode": ""} + headers = self.yunnan_life_base_headers(token, {"Origin": YUNNAN_LIFE_BASE_URL}) + for i in range(times): + try: + res = self.session.post( + f"{YUNNAN_LIFE_BASE_URL}/2b2c-mobile/acttmpl/lottery/actLuckyDrawy", + data=json.dumps(payload, ensure_ascii=False, separators=(',', ':')), + headers=headers, + timeout=15, + ).json() + if res.get("resultCode") == "0000": + self.log(f"云南生活: ✅ 第{i + 1}次抽奖请求成功") + else: + self.log(f"云南生活: ❌ 第{i + 1}次抽奖失败: {res.get('resultMsg', '')}") + except Exception as e: + self.log(f"云南生活: 第{i + 1}次抽奖异常: {e}") + if i < times - 1: + time.sleep(2) + + def yunnan_life_get_lottery_results(self, token): + try: + resp = self.session.get( + f"{YUNNAN_LIFE_BASE_URL}/2b2c-mobile/acttmpl/lottery/getUserRecordListActInfo", + params={"actId": YUNNAN_LIFE_ACT_ID, "periodId": YUNNAN_LIFE_ACT_ID}, + headers=self.yunnan_life_base_headers(token, {"Content-Type": "application/json;charset=gb2312"}), + timeout=15, + ) + data = resp.json() + today = datetime.now().strftime("%Y-%m-%d") + awards = [] + for item in data.get("data", {}).get("recordList", []): + if str(item.get("createTime", "")).startswith(today): + awards.append(item.get("awardName", "未知")) + if awards: + for award in awards: + self.log(f"云南生活: 🎁 抽奖结果 - {award}", notify=True) + else: + self.log("云南生活: 今日暂无抽奖记录") + except Exception as e: + self.log(f"云南生活: 查询抽奖结果异常: {e}") + + def yunnan_life_get_bean_balance(self, token): + try: + payload = {} + res = self.session.post( + f"{YUNNAN_LIFE_BASE_URL}/user/beans/api/getTotalAvailableBeansByPhone", + data=json.dumps(payload, ensure_ascii=False, separators=(',', ':')), + headers=self.yunnan_life_signed_headers(token, payload), + timeout=15, + ).json() + if res.get("resultCode") == "0000": + self.log(f"云南生活: 💰 当前云豆余额: {res.get('data', 0)}", notify=True) + else: + self.log(f"云南生活: 获取云豆失败: {res.get('resultMsg', '')}") + except Exception as e: + self.log(f"云南生活: 查询云豆异常: {e}") + + def yunnan_life_task(self, is_query_only=False): + token = self.yunnan_life_login() + if not token: + return + if is_query_only: + self.log("云南生活: [查询模式] 查询云豆余额") + self.yunnan_life_get_bean_balance(token) + return + for task in YUNNAN_LIFE_TASKS: + self.yunnan_life_do_task(token, task) + time.sleep(2) + self.yunnan_life_do_lottery(token, times=2) + self.yunnan_life_get_lottery_results(token) + self.yunnan_life_get_bean_balance(token) + + def xj_task_main(self): + ticket_res = self.openPlatLineNew("https://zy100.xj169.com/touchpoint/openapi/jumpHandRoom1G?source=155&type=02") + if not ticket_res or not ticket_res.get("ticket"): + self.log("新疆专区: 获取入口 ticket 失败") + return + token = self.xj_get_token(ticket_res.get("ticket")) + if token: + self.xj_do_draw(token, "Jan2026Act") + day = datetime.now().day + if 19 <= day <= 25: + self.xj_usersday_task(token) + self.xj_monthly_draw_task(token) + + def xj_get_token(self, ticket): + try: + url = "https://zy100.xj169.com/touchpoint/openapi/getTokenAndCity" + if isinstance(ticket, dict): + ticket = ticket.get("ticket") + data = {"ticket": ticket} + headers = { + "Referer": f"https://zy100.xj169.com/touchpoint/openapi/jumpHandRoom1G?source=155&type=02&ticket={ticket}", + "User-Agent": XJ_USER_AGENT, + } + res = self.session.post(url, data=data, headers=headers).json() + result = res.get('result', {}) + if result.get('code') == 0 and result.get('data', {}).get('token'): + return result.get('data', {}).get('token') + token = res.get("data", {}).get("token") + if token: + return token + return None + except Exception as e: + self.log(f"新疆专区: 获取 token 异常 {e}") + return None + + def xj_do_draw(self, token, act_id): + try: + url = f"https://zy100.xj169.com/touchpoint/openapi/marchAct/draw_{act_id}" + data = {"activityId": f"daka{act_id}", "prizeId": ""} + headers = {"userToken": token, "User-Agent": XJ_USER_AGENT} + res = self.session.post(url, data=data, headers=headers).json() + msg = res.get('result', {}).get('msg') or res.get('result', {}).get('data') or "失败" + self.log(f"新疆专区: 每日打卡 - {msg}", notify=True) + except Exception as e: + self.log(f"新疆专区: 打卡异常 {e}") + + def xj_usersday_task(self, token): + try: + url = "https://zy100.xj169.com/touchpoint/openapi/marchAct/draw_UsersDay2025Act" + data = {"activityId": "usersDay2025Act", "prizeId": "hfq_twenty"} + headers = {"userToken": token, "User-Agent": XJ_USER_AGENT} + res = self.session.post(url, data=data, headers=headers).json() + msg = res.get('result', {}).get('msg') or res.get('result', {}).get('data') or "失败" + self.log(f"新疆客户日: 秒杀结果 - {msg}", notify=True) + except Exception as e: + self.log(f"新疆客户日: 秒杀异常 {e}") + + def xj_monthly_draw_once(self, token): + headers = { + "User-Agent": XJ_USER_AGENT, + "userToken": token, + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + } + payload = {"activityId": XJ_ACTIVITY_ID, "prizeId": "", "commHighFlag": "false"} + try: + res = self.session.post( + f"https://zy100.xj169.com/touchpoint/openapi/themeAct/draw_{XJ_ACTIVITY_ID}", + data=payload, + headers=headers, + timeout=10, + ).json() + code = res.get("code") + msg = str(res.get("msg", "")) + msg_type = str(res.get("msgType", "")) + data = res.get("data", "") + if code == "ERROR": + data_str = str(data) + if "已用完" in data_str or "已抽完" in data_str or msg_type == "101": + return "done", f"今日机会已用尽 ({data_str or msg or '无可用次数'})" + if "频率过高" in msg: + return "done", "接口频率限制" + if "缺少参数" in msg: + return "invalid", "token 已失效" + return "done", f"抽奖失败: {data_str or msg or '未知错误'}" + if code == "SUCCESS": + if msg == "thanks1": + return "continue", f"未中奖 ({data or msg})" + return "won", f"中奖: {data or '未知奖品'}" + if str(code) == "401": + return "invalid", "token 已失效" + return "continue", f"未中奖 ({msg or data or code})" + except Exception as e: + return "error", f"请求异常: {e}" + + def xj_query_monthly_draw_records(self, token): + headers = { + "User-Agent": XJ_USER_AGENT, + "userToken": token, + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + } + try: + res = self.session.post( + "https://zy100.xj169.com/touchpoint/openapi/drawAct/getPrizesScroll", + data={"activityId": XJ_ACTIVITY_ID}, + headers=headers, + timeout=10, + ).json() + data = res.get("data", []) + if not data: + self.log("新疆专区: 每月抽奖暂无中奖记录") + return + if isinstance(data, dict): + data = [data] + if isinstance(data, list) and data and isinstance(data[0], str): + for item in data[:5]: + self.log(f"新疆专区: 每月抽奖记录 - {item}", notify=True) + return + displayed = 0 + for item in data: + if not isinstance(item, dict): + continue + prize_name = item.get("prizeName") or item.get("prizeId") or "未知奖品" + draw_ts = safe_int(item.get("drawDate"), 0) + draw_date = datetime.fromtimestamp(draw_ts / 1000).strftime("%m-%d") if draw_ts else "未知时间" + self.log(f"新疆专区: 每月抽奖记录 - {prize_name} ({draw_date})", notify=True) + displayed += 1 + if displayed >= 5: + break + if displayed == 0: + self.log("新疆专区: 每月抽奖暂无可展示记录") + except Exception as e: + self.log(f"新疆专区: 查询每月抽奖记录异常 {e}") + + def xj_monthly_draw_task(self, token): + self.log(f"新疆专区: 每月抽奖活动 {XJ_ACTIVITY_ID}") + for i in range(XJ_MONTHLY_DRAW_ATTEMPT_COUNT): + status, msg = self.xj_monthly_draw_once(token) + self.log( + f"新疆专区: 每月抽奖第{i + 1}次 - {msg}", + notify=status == "won", + ) + if status in {"done", "won", "invalid"}: + break + time.sleep(random.uniform(1, 2)) + self.xj_query_monthly_draw_records(token) + + def shangdu_get_sign_status(self): + try: + url = "https://app.shangdu.com/monthlyBenefit/v1/signIn/queryCumulativeSignAxis" + headers = { + "Origin": "https://app.shangdu.com", + "Referer": "https://app.shangdu.com/monthlyBenefit/index.html", + "edop_flag": "0", "Content-Type": "application/json" + } + res = self.session.post(url, json={}, headers=headers).json() + if res.get('result', {}).get('code') == "0000": + return res.get('result', {}).get('data', {}).get('todaySignFlag') == "1" + return None + except: return None + + def shangdu_sign_retry(self): + try: + url = "https://app.shangdu.com/monthlyBenefit/v1/signIn/userSignIn" + headers = { + "Origin": "https://app.shangdu.com", + "Referer": "https://app.shangdu.com/monthlyBenefit/index.html", + "edop_flag": "0", "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/json" + } + res = self.session.post(url, json={}, headers=headers).json() + code = res.get('result', {}).get('code') + data = res.get('result', {}).get('data', {}) + if code == "0000": + prize = data.get('prizeResp', {}).get('prizeName') + if prize: self.log(f"河南商都: 签到成功(重试) - 获得 {prize}", notify=True) + else: self.log("河南商都: 签到成功(重试)") + elif code == "0019": + self.log("河南商都: 重试仍返回重复签到") + else: + self.log(f"河南商都: A签到重试失败 - {res.get('result', {}).get('msg')}") + except Exception as e: + self.log(f"河南商都: 签到重试异常 {e}") + + def shangdu_task_main(self): + if not self.ecs_token: return + url = f"https://m.client.10010.com/edop_ng/getTicketByNative?appId=edop_unicom_4b80047a&token={self.ecs_token}" + res = self.session.get(url).json() + ticket = res.get('result', {}).get('ticket') + if not ticket: + self.log("河南商都: 获取Ticket失败") + return + login_url = f"https://app.shangdu.com/monthlyBenefit/v1/common/config?ticket={ticket}" + headers_login = { + "Origin": "https://app.shangdu.com", + "Referer": "https://app.shangdu.com/monthlyBenefit/index.html", + "edop_flag": "0", "Accept": "application/json, text/plain, */*" + } + self.session.get(login_url, headers=headers_login) + time.sleep(1.5) + sign_url = "https://app.shangdu.com/monthlyBenefit/v1/signIn/userSignIn" + headers_sign = { + "Origin": "https://app.shangdu.com", + "Referer": "https://app.shangdu.com/monthlyBenefit/index.html", + "edop_flag": "0", "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/json" + } + res_sign = self.session.post(sign_url, json={}, headers=headers_sign).json() + code = res_sign.get('result', {}).get('code') + data = res_sign.get('result', {}).get('data', {}) + if code == "0000": + if data.get('value') == "0001": + self.log("河南商都: 签到失败 - Cookie无效") + else: + prize = data.get('prizeResp', {}).get('prizeName', '已签到') + self.log(f"河南商都: 签到结果 - {prize}", notify=True) + elif code == "0019": + time.sleep(1) + is_signed = self.shangdu_get_sign_status() + if is_signed is True: + self.log("河南商都: 今日已签到") + elif is_signed is False: + self.log("河南商都: 状态未签到但返回重复,尝试重试...") + time.sleep(2) + self.shangdu_sign_retry() + else: + self.log("河南商都: 今日已签到 (状态未知)") + else: + self.log(f"河南商都: 签到失败 - {code} : {res_sign.get('result', {}).get('msg')}") + + def ln_flmf_get_sid(self): + """辽宁福利魔方: 通过 openPlatLineNew → autoLogin 获取 sid""" + try: + ticket_res = self.openPlatLineNew("https://weixin.linktech.hk/lv-web/handHall/autoLogin?actcode=sign") + if not ticket_res or not ticket_res.get('ticket'): + self.log("辽宁福利魔方: 获取ticket失败") + return None + ticket = ticket_res['ticket'] + type_val = ticket_res.get('type', '06') + mobile = getattr(self, 'account_mobile', getattr(self, 'mobile', '')) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + postage = hashlib.md5(f"{mobile}{timestamp}".encode()).hexdigest() + login_url = "https://weixin.linktech.hk/lv-web/handHall/autoLogin" + params = { + "actcode": "sign", + "type": type_val, + "ticket": ticket, + "version": COMMON_CONSTANTS["APP_VERSION"], + "timestamp": timestamp, + "desmobile": mobile, + "num": "0", + "postage": postage, + "userNumber": mobile + } + res = self.session.get(login_url, params=params, allow_redirects=False, timeout=15) + if res.status_code != 302 or 'Location' not in res.headers: + self.log(f"辽宁福利魔方: autoLogin期望302, 实际{res.status_code}") + return None + loc = res.headers['Location'] + sid_match = re.search(r'sid[=%]3[Dd]?([a-f0-9]{32})', loc) + if not sid_match: + parsed = urlparse(unquote(loc)) + qs = parse_qs(parsed.query) + params_val = qs.get('params', [''])[0] + if 'sid=' in params_val: + inner_qs = parse_qs(params_val) + sid = inner_qs.get('sid', [''])[0] + else: + sid = qs.get('sid', [''])[0] + else: + sid = sid_match.group(1) + if sid and len(sid) == 32: + self.log(f"辽宁福利魔方: 获取sid成功 ({sid[:8]}...)") + return sid + self.log(f"辽宁福利魔方: 重定向中未找到sid") + except Exception as e: + self.log(f"辽宁福利魔方: 获取sid异常 - {e}") + return None + + def ln_flmf_api(self, sid, endpoint, extra_data=None): + """辽宁福利魔方: 通用API调用""" + url = f"https://weixin.linktech.hk/lv-apiaccess/welfareCenter/{endpoint}" + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Origin": "https://weixin.linktech.hk", + "Referer": f"https://weixin.linktech.hk/app/flmf/LV-202111-04/moreShatter?sid={sid}&actcode=welfareCenter", + "User-Agent": "Mozilla/5.0 (Linux; Android 10; MI 8) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/143.0.7499.146 Mobile Safari/537.36; unicom{version:android@11.0802}" + } + data = f"sid={sid}&actcode=welfareCenter" + if extra_data: + data += f"&{extra_data}" + try: + res = self.session.post(url, headers=headers, data=data, timeout=15).json() + return res + except Exception as e: + self.log(f"辽宁福利魔方: {endpoint} 请求异常 - {e}") + return None + + def ln_flmf_task(self, is_query_only=False): + """辽宁福利魔方: 主入口""" + sid = self.ln_flmf_get_sid() + if not sid: + return + res = self.ln_flmf_api(sid, "addUser") + if not res or res.get('resultCode') != '0000': + self.log(f"辽宁福利魔方: 用户初始化失败 - {(res or {}).get('resultMsg', '无响应')}") + return + time.sleep(1) + init_res = self.ln_flmf_api(sid, "signInInit") + if init_res and init_res.get('resultCode') == '0000': + init_data = init_res.get('data', {}) + is_signed = init_data.get('isSigned', 0) + consecutive = init_data.get('consecutiveDays', 0) + if is_signed: + self.log(f"辽宁福利魔方: 今日已签到 (连续{consecutive}天)") + elif is_query_only: + self.log(f"辽宁福利魔方: 今日未签到 (连续{consecutive}天)") + else: + time.sleep(1) + sign_res = self.ln_flmf_api(sid, "signIn") + if sign_res and sign_res.get('resultCode') == '0000': + self.log(f"辽宁福利魔方: ✅ 签到成功 (连续{consecutive + 1}天)", notify=True) + else: + self.log(f"辽宁福利魔方: 签到失败 - {(sign_res or {}).get('resultMsg', '无响应')}") + else: + self.log(f"辽宁福利魔方: 查询签到状态失败 - {(init_res or {}).get('resultMsg', '无响应')}") + time.sleep(1) + info_res = self.ln_flmf_api(sid, "getUserInfo") + if info_res and info_res.get('resultCode') == '0000': + info = info_res.get('data', {}) + wobi = info.get('woBi', 0) + sign_times = info.get('signTimes', 0) + member_wobi = info.get('memberwobi', 0) + member_trun = info.get('membertrun', 0) + rights_num = info.get('rightsNum', '0') + self.log(f"辽宁福利魔方: 沃币{wobi} | 累计签到{sign_times}天 | 会员碎片{member_wobi} | 等级{member_trun} | 权益{rights_num}次", notify=True) + if is_query_only: + return + time.sleep(1) + task_res = self.ln_flmf_api(sid, "taskList", "refresh=0&nowTask=") + if task_res and task_res.get('resultCode') == '0000': + groups = task_res.get('data', {}).get('taskInfoList', []) + for group in groups: + tasks = group.get('taskInfoList', []) + for t in tasks: + status = "✅" if t.get('done', 0) > 0 else "⏳" + self.log(f"辽宁福利魔方: {status} {t.get('taskName')} ({t.get('done', 0)}/{t.get('count', 0)})") + + def ah_friday_get_entry(self): + """安徽超级星期五: 获取活动入口ticket""" + try: + entry_url = f"{AH_FRIDAY_BASE_URL}/wxopen/hh/activity/superFriday/index?chnlId=app-ty&type=02" + ticket_res = self.openPlatLineNew(entry_url) + if not ticket_res or not ticket_res.get('ticket'): + self.log("安徽超级星期五: 获取入口ticket失败") + return None + ticket = ticket_res['ticket'] + mobile = getattr(self, 'account_mobile', getattr(self, 'mobile', '')) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + postage = hashlib.md5(f"{mobile}{timestamp}".encode()).hexdigest() + page_url = f"{AH_FRIDAY_BASE_URL}/wxopen/hh/activity/superFriday/index" + params = { + "chnlId": "app-ty", + "type": "02", + "ticket": ticket, + "version": COMMON_CONSTANTS["APP_VERSION"], + "timestamp": timestamp, + "desmobile": mobile, + "num": "0", + "postage": postage, + "userNumber": mobile + } + headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "User-Agent": COMMON_CONSTANTS["UA"], + } + res = self.session.get(page_url, params=params, headers=headers, timeout=15) + act_ticket = res.cookies.get('ticket', '') + if not act_ticket: + for hist_resp in getattr(res, 'history', []): + ck = hist_resp.cookies.get('ticket', '') + if ck: + act_ticket = ck + break + sc = hist_resp.headers.get('Set-Cookie', '') + m = re.search(r'ticket=([^;,\s]+)', sc) + if m: + act_ticket = m.group(1) + break + if not act_ticket: + act_ticket = self.session.cookies.get('ticket', domain='') or self.session.cookies.get('ticket', '') + if not act_ticket: + cookie_header = res.headers.get('Set-Cookie', '') + m = re.search(r'ticket=([^;,\s]+)', cookie_header) + if m: + act_ticket = m.group(1) + if not act_ticket: + m = re.search(r'ticket[=:]\s*["\']?([a-zA-Z0-9_\-]{8,})', res.text) + if m: + act_ticket = m.group(1) + if not act_ticket: + self.log(f"安徽超级星期五: 页面未返回独立ticket,使用入口ticket兜底") + act_ticket = ticket + self.log(f"安徽超级星期五: 获取活动ticket成功 ({act_ticket[:12]}...)") + return { + "ticket": act_ticket, + "mobile": mobile, + "timestamp": timestamp, + "postage": postage, + "app_ticket": ticket, + } + except Exception as e: + self.log(f"安徽超级星期五: 获取入口异常 - {e}") + return None + + def ah_friday_get_items(self, entry_info): + """安徽超级星期五: 获取奖品列表并匹配目标面额""" + try: + url = f"{AH_FRIDAY_BASE_URL}/wxopen/app-activity/AHSecKill/querySecKillInfo" + headers = { + "Accept": "application/json, text/plain, */*", + "Content-Type": "application/json", + "Origin": AH_FRIDAY_BASE_URL, + "Cookie": f"ticket={entry_info['ticket']}", + "User-Agent": COMMON_CONSTANTS["UA"], + } + res = self.session.post(url, json={}, headers=headers, timeout=10) + result = res.json() + if not result.get('success') and not result.get('data'): + self.log(f"安徽超级星期五: 查询奖品列表失败 - {result.get('alertMsg', '未知')}") + return None + items = result.get('data', {}).get('itemList', []) + if not items: + items = result.get('data', []) if isinstance(result.get('data'), list) else [] + target_amount = str(AH_FRIDAY_AMOUNT) + for item in items: + item_code = item.get('itemCode', '') + item_name = item.get('itemName', '') + if target_amount in item_name or f"hb{target_amount}" in item_code: + key_val = item.get('key', '') + self.log(f"安徽超级星期五: 匹配到目标 [{item_name}] (code: {item_code})") + return { + "itemCode": item_code, + "itemName": item_name, + "key": key_val, + } + item_code = f"AWARD_AHFridaySecKill_10_hb{target_amount}" + self.log(f"安徽超级星期五: 未从列表匹配到{target_amount}元, 使用默认itemCode: {item_code}") + return {"itemCode": item_code, "itemName": f"{target_amount}元红包", "key": ""} + except Exception as e: + self.log(f"安徽超级星期五: 查询奖品异常 - {e}") + item_code = f"AWARD_AHFridaySecKill_10_hb{AH_FRIDAY_AMOUNT}" + return {"itemCode": item_code, "itemName": f"{AH_FRIDAY_AMOUNT}元红包", "key": ""} + + def ah_friday_seckill(self, entry_info, item_info): + """安徽超级星期五: 批量抢购""" + ticket = entry_info['ticket'] + item_code = item_info['itemCode'] + key_val = item_info.get('key', '') + mobile = entry_info['mobile'] + timestamp_str = entry_info['timestamp'] + postage = entry_info['postage'] + referer = ( + f"{AH_FRIDAY_BASE_URL}/wxopen/hh/activity/superFriday/index" + f"?chnlId=app-ty&type=02&ticket={entry_info['app_ticket']}" + f"&version={COMMON_CONSTANTS['APP_VERSION']}×tamp={timestamp_str}" + f"&desmobile={mobile}&num=0&postage={postage}&userNumber={mobile}" + ) + success_count = 0 + fail_count = 0 + self.log(f"安徽超级星期五: 开始批量抢购 [{item_info['itemName']}],共{AH_FRIDAY_SECKILL_TIMES}次") + for i in range(1, AH_FRIDAY_SECKILL_TIMES + 1): + try: + ts = str(int(time.time() * 1000)) + params = { + "ticket": ticket, + "itemCode": item_code, + "time": ts, + } + if key_val: + params["key"] = key_val + url = f"{AH_FRIDAY_BASE_URL}/wxopen/app-activity/AHSecKill/lotteryAction" + headers = { + "Accept": "application/json, text/plain, */*", + "Content-Type": "application/json", + "Origin": AH_FRIDAY_BASE_URL, + "Referer": referer, + "Cookie": f"ticket={ticket}", + "User-Agent": COMMON_CONSTANTS["UA"], + "Connection": "keep-alive", + } + res = self.session.post(url, params=params, json={}, headers=headers, timeout=5) + data = res.json() + if data.get('success'): + success_count += 1 + self.log(f"安徽超级星期五: 🎉 第{i}次抢购成功!{json.dumps(data, ensure_ascii=False)}", notify=True) + return True + else: + fail_count += 1 + alert = data.get('alertMsg', '') + if i <= 3 or i % 20 == 0: + self.log(f"安徽超级星期五: 第{i}次 - {alert or data.get('statusCode', '未知')}") + if "已抢完" in alert or "已结束" in alert or "已领取" in alert: + self.log(f"安徽超级星期五: ⚠️ {alert},停止抢购") + break + except Exception as e: + fail_count += 1 + if i <= 3: + self.log(f"安徽超级星期五: 第{i}次异常 - {e}") + if i < AH_FRIDAY_SECKILL_TIMES: + time.sleep(AH_FRIDAY_INTERVAL) + self.log(f"安徽超级星期五: 抢购完成 (共{AH_FRIDAY_SECKILL_TIMES}次, 失败{fail_count}次)", notify=True) + return False + + def ah_friday_task(self): + """安徽超级星期五: 主入口""" + if not AH_FRIDAY_AMOUNT: + return + rc = globalConfig.get("regional_config", {}) + if not rc.get("run_ah_friday", True): + self.log("安徽超级星期五: ⏭️ 已被子开关关闭,跳过") + return + weekday = datetime.now().weekday() + if weekday != 4: + self.log(f"安徽超级星期五: 今天不是周五 (当前周{weekday + 1}),跳过") + return + self.log(f"安徽超级星期五: 🎯 目标面额 {AH_FRIDAY_AMOUNT}元") + entry_info = self.ah_friday_get_entry() + if not entry_info: + return + item_info = self.ah_friday_get_items(entry_info) + if not item_info: + return + now = datetime.now() + target = now.replace(hour=10, minute=0, second=0, microsecond=0) + wait_seconds = (target - now).total_seconds() + if wait_seconds > 300: + self.log(f"安徽超级星期五: ⏳ 距10:00还有 {wait_seconds:.0f}秒,大于5分钟,建议临近时启动") + return + if wait_seconds > 0: + self.log(f"安徽超级星期五: ⏳ 等待开抢 (剩余 {wait_seconds:.1f}秒)...") + while (datetime.now().replace(hour=10, minute=0, second=0, microsecond=0) - datetime.now()).total_seconds() > 0.3: + time.sleep(0.1) + self.log("安徽超级星期五: ⚡ 时间到!开始抢购!") + else: + self.log(f"安徽超级星期五: ⚡ 已过10点 {abs(wait_seconds):.1f}秒,直接抢购!") + self.ah_friday_seckill(entry_info, item_info) + + def woread_encrypt(self, data): + try: + key = b'woreadst^&*12345' + iv = b'16-Bytes--String' + cipher = AES.new(key, AES.MODE_CBC, iv) + if isinstance(data, dict): + data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False) + else: + data_str = str(data) + pad_len = 16 - (len(data_str.encode('utf-8')) % 16) + data_str = data_str + chr(pad_len) * pad_len + ciphertext = cipher.encrypt(data_str.encode('utf-8')) + hex_str = ciphertext.hex() + return base64.b64encode(hex_str.encode('utf-8')).decode('utf-8') + except Exception as e: + self.log(f"woread_encrypt error: {e}") + return "" + + def woread_auth(self): + try: + product_id = "10000002" + secret_key = "7k1HcDL8RKvc" + timestamp = str(round(time.time() * 1000)) + sign_str = f"{product_id}{secret_key}{timestamp}" + md5_hash = hashlib.md5(sign_str.encode('utf-8')).hexdigest() + date_str = datetime.now().strftime('%Y%m%d%H%M%S') + crypt_text_obj = {"timestamp": date_str} + encoded_sign = self.woread_encrypt(crypt_text_obj) + url = f"https://10010.woread.com.cn/ng_woread_service/rest/app/auth/{product_id}/{timestamp}/{md5_hash}" + headers = { + "Content-Type": "application/json", + "User-Agent": COMMON_CONSTANTS['UA'], + } + res = self.session.post(url, json={"sign": encoded_sign}, headers=headers).json() + if res.get('code') == "0000": + self.woread_accesstoken = res.get('data', {}).get('accesstoken') + return True + else: + self.log(f"阅读专区认证失败: {res.get('message')}") + return False + except Exception as e: + self.log(f"woread_auth error: {e}") + return False + + def woread_login(self): + try: + if not hasattr(self, 'woread_accesstoken') or not self.woread_accesstoken: + if not self.woread_auth(): + return False + if not self.token_online: + self.log("阅读专区: 缺少 token_online,无法登录") + return False + token_enc = self.woread_encrypt(self.token_online) + phone_str = self.account_mobile if self.account_mobile else "13800000000" + phone_enc = self.woread_encrypt(phone_str) + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + inner_json = json.dumps({ + "tokenOnline": token_enc, + "phone": phone_enc, + "timestamp": timestamp + }, separators=(',', ':'), ensure_ascii=False) + encoded_sign = self.woread_encrypt(inner_json) + url = "https://10010.woread.com.cn/ng_woread_service/rest/account/login" + headers = { + "Content-Type": "application/json", + "User-Agent": COMMON_CONSTANTS['UA'], + } + if hasattr(self, 'woread_accesstoken') and self.woread_accesstoken: + headers["accesstoken"] = self.woread_accesstoken + res = self.session.post(url, json={"sign": encoded_sign}, headers=headers, timeout=15).json() + if res.get('code') == "0000": + data = res.get('data', {}) + self.woread_token = data.get('token') + self.woread_userid = data.get('userid') + self.woread_userindex = data.get('userindex') + self.woread_verifycode = data.get('verifycode') + if data.get('phone'): + self.mobile = data['phone'] + self.log("阅读专区: 登录成功") + return True + else: + self.log(f"阅读专区登录失败: {res.get('message')}") + return False + except Exception as e: + self.log(f"woread_login error: {e}") + return False + + def woread_queryTicketAccount(self): + try: + if not hasattr(self, 'woread_token') or not self.woread_token: + if not self.woread_login(): + return + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + params = { + "timestamp": timestamp, + "phone": self.mobile if self.mobile else "", + "token": self.woread_token + } + sign = self.woread_encrypt(params) + url = "https://10010.woread.com.cn/ng_woread_service/rest/phone/vouchers/queryTicketAccount" + headers = { + "Content-Type": "application/json", + "User-Agent": COMMON_CONSTANTS['UA'], + } + if hasattr(self, 'woread_accesstoken') and self.woread_accesstoken: + headers["accesstoken"] = self.woread_accesstoken + res = self.session.post(url, json={"sign": sign}, headers=headers).json() + if res.get('code') == "0000": + data = res.get('data', {}) + usable_num = int(data.get('usableNum', 0)) + balance_yuan = "{:.2f}".format(usable_num / 100) + self.log(f"💰 [资产-阅读红包] 余额: {balance_yuan}元", notify=True) + else: + self.log(f"阅读红包查询失败: {res.get('message')}") + except Exception as e: + self.log(f"woread_queryTicketAccount error: {e}") + + + def woread_get_book_info(self): + try: + url1 = "https://10010.woread.com.cn/ng_woread_service/rest/basics/recommposdetail/14856" + headers = { + "User-Agent": COMMON_CONSTANTS['UA'], + "accesstoken": self.woread_accesstoken + } + res1 = self.session.get(url1, headers=headers) + try: + res1 = res1.json() + except: + self.log(f"阅读专区: 获取书架响应非JSON: {res1.text[:100]}") + return False + if res1.get('code') == '0000': + msg_list = res1.get('data', {}).get('booklist', {}).get('message', []) + if msg_list: + self.wr_catid = msg_list[0].get('catindex') + self.wr_cntindex = msg_list[0].get('cntindex') + bind_info = res1.get('data', {}).get('bindinfo', []) + if bind_info: + self.wr_cardid = bind_info[0].get('recommposiindex') + else: + self.log("阅读专区: 获取书架失败") + return False + if not getattr(self, 'wr_cntindex', None): return False + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + param = { + "curPage": 1, "limit": 30, "index": self.wr_cntindex, "sort": 0, "finishFlag": 1, + "timestamp": timestamp, + "phone": self.mobile if self.mobile else "", + "token": getattr(self, 'woread_token', ''), + "userid": getattr(self, 'woread_userid', ''), + "userId": getattr(self, 'woread_userid', ''), + "userIndex": getattr(self, 'woread_userindex', ''), + "verifyCode": getattr(self, 'woread_verifycode', '') + } + sign = self.woread_encrypt(param) + url2 = "https://10010.woread.com.cn/ng_woread_service/rest/cnt/chalist" + res2_raw = self.session.post(url2, json={"sign": sign}, headers=headers) + try: + res2 = res2_raw.json() + except: + self.log(f"阅读专区: 获取章节响应非JSON: {res2_raw.text[:100]}") + return False + lst = res2.get('list', []) or res2.get('data', {}).get('list', []) + if lst: + content = lst[0].get('charptercontent', []) + if content: + self.wr_chapterallindex = content[0].get('chapterallindex') + self.wr_chapterid = content[0].get('chapterid') + return True + return False + except Exception as e: + self.log(f"阅读专区: 获取书籍信息异常: {e}") + return False + + def woread_read_process(self): + if not self.woread_get_book_info(): + self.log("阅读专区: 无法获取书籍信息,跳过阅读") + return + headers = { + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0301}", + "accesstoken": self.woread_accesstoken + } + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + phone = self.mobile if self.mobile else "" + token = getattr(self, 'woread_token', '') + userid = getattr(self, 'woread_userid', '') + userindex = getattr(self, 'woread_userindex', '') + verifycode = getattr(self, 'woread_verifycode', '') + common_params = { + "timestamp": timestamp, + "phone": phone, + "token": token, + "userid": userid, + "userId": userid, + "userIndex": userindex, + "userAccount": phone, + "verifyCode": verifycode + } + param = { + "chapterAllIndex": self.wr_chapterallindex, + "cntIndex": self.wr_cntindex, + "cntTypeFlag": "1", + **common_params + } + sign = self.woread_encrypt(param) + hb_url = f"https://10010.woread.com.cn/ng_woread_service/rest/cnt/wordsDetail?catid={self.wr_catid}&cardid={self.wr_cardid}&cntindex={self.wr_cntindex}&chapterallindex={self.wr_chapterallindex}&chapterseno=1" + self.session.post(hb_url, json={"sign": sign}, headers=headers) + add_param = { + "readTime": "2", + "cntIndex": self.wr_cntindex, + "cntType": "1", + "catid": "0", + "pageIndex": "", + "cardid": self.wr_cardid, + "cntindex": self.wr_cntindex, + "cnttype": "1", + "chapterallindex": self.wr_chapterallindex, + "chapterseno": "1", + "channelid": "", + "chapterid": self.wr_chapterid, + "readtype": 1, + "isend": "0", + **common_params + } + add_sign = self.woread_encrypt(add_param) + add_url = "https://10010.woread.com.cn/ng_woread_service/rest/history/addReadTime" + res = self.session.post(add_url, json={"sign": add_sign}, headers=headers).json() + res_code = str(res.get('code', '')) + res_msg = str(res.get('message', '')) + if res_code == '0000': + self.log("阅读专区: 模拟阅读成功") + elif res_code == '9999' or '9999' in res_msg or '不存在阅读记录' in res_msg: + # addReadTime 返回9999不影响实际阅读结果 + self.log("阅读专区: 模拟阅读成功(阅读记录已提交)") + else: + self.log(f"阅读专区: 模拟阅读失败: {res_msg or res}") + + + + def woread_draw_new(self): + try: + headers = { + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0301}", + "accesstoken": self.woread_accesstoken + } + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + param = { + "activeindex": "8051", + "timestamp": timestamp, "phone": self.mobile if self.mobile else "", "token": self.woread_token + } + sign = self.woread_encrypt(param) + url = "https://10010.woread.com.cn/ng_woread_service/rest/basics/doDraw" + res = self.session.post(url, json={"sign": sign}, headers=headers).json() + if res.get('code') == '0000': + prize = res.get('data', {}).get('prizedesc') + if prize: + self.log(f"阅读专区: 抽奖成功: {prize}", notify=True) + else: + self.log("阅读专区: 抽奖完成 (未中奖)") + else: + self.log(f"阅读专区: 抽奖失败: {res.get('message')}") + except Exception as e: + self.log(f"woread_draw_new error: {e}") + + def woread_task(self): + self.log("==== 联通阅读 ====") + if not self.woread_login(): + self.log("阅读专区: 登录失败,跳过任务") + return + self.woread_queryTicketAccount() + self.woread_read_process() + time.sleep(3) + self.woread_draw_new() + + def query_market_raffle_records(self, user_token): + self.log("权益超市: 正在查询抽奖记录...") + try: + url = "https://backward.bol.wo.cn/prod-api/market/contactReceive/queryReceiveRecord" + headers = { + "Authorization": f"Bearer {user_token}", + "User-Agent": COMMON_CONSTANTS["MARKET_UA"], + "Origin": "https://contact.bol.wo.cn", + "Referer": "https://contact.bol.wo.cn/" + } + mobile = getattr(self, "account_mobile", getattr(self, "mobile", "")) + payload = { + "isReceive": None, + "receiveStatus": None, + "limit": 20, + "page": 1, + "mobile": mobile, + "businessSources": ["3", "4", "5", "6", "99"], + "isPromotion": 1, + "returnFormatType": 1 + } + res = self.session.post(url, json=payload, headers=headers).json() + if res.get('code') == 200: + records = res.get('data', {}).get('recordObjs', []) + if records: + display_records = records[:10] + self.log(f"权益超市: 最近 {len(display_records)} 条抽奖记录:", notify=True) + for item in display_records: + self.log(f" - [{item.get('receiveTime') or ''}] {item.get('recordName')}", notify=True) + else: + self.log("权益超市: 无近期抽奖记录。") + else: + self.log(f"权益超市: 查询抽奖记录失败: {res.get('msg')}") + except Exception as e: + self.log(f"query_market_raffle_records error: {e}") + + def query_phone_recharge_records(self, user_token): + self.log("权益超市: 正在查询本月话费抢购记录...") + try: + url = "https://backward.bol.wo.cn/prod-api/market/contactReceive/queryReceiveRecord" + headers = { + "Authorization": f"Bearer {user_token}", + "User-Agent": COMMON_CONSTANTS["MARKET_UA"], + "Origin": "https://contact.bol.wo.cn", + "Referer": "https://contact.bol.wo.cn/" + } + mobile = getattr(self, "account_mobile", getattr(self, "mobile", "")) + payload = { + "isReceive": None, + "receiveStatus": None, + "limit": 50, + "page": 1, + "mobile": mobile, + "businessSources": ["3", "4", "5", "6", "99"], + "isPromotion": 1, + "returnFormatType": 1 + } + res = self.session.post(url, json=payload, headers=headers).json() + if res.get('code') == 200: + records = res.get('data', {}).get('recordObjs', []) + total_amount = 0.0 + current_month = datetime.now().strftime('%Y-%m') + count = 0 + for item in records: + create_time = item.get('receiveTime') or '' + name = item.get('recordName', '') + if not create_time or current_month not in create_time: + continue + if any(k in name for k in ['话费', '充值', '红包']): + match = re.search(r'(\d+(\.\d+)?)元', name) + if match: + amount = float(match.group(1)) + total_amount += amount + count += 1 + if count > 0: + self.log(f"💰 [资产-抢购] 本月权益超市话费累计: {total_amount:.2f}元", notify=True) + else: + self.log("权益超市: 本月暂无话费抢购记录") + else: + self.log(f"权益超市: 查询话费记录失败: {res.get('msg')}") + except Exception as e: + self.log(f"query_phone_recharge_records error: {e}") + + def sign_query_my_prizes(self): + self.log("正在查询账户明细 (抢兑)...") + try: + url = "https://act.10010.com/SigninApp/convert/phoneDetails" + form = { + "log_type": "1", + "number": "1", + "list_num": "" + } + headers = {"Origin": "https://img.client.10010.com"} + res = self.request("post", url, data=form, headers=headers) + if not res: return + result = res.json() + if result.get('status') == '0000': + data = result.get('data', {}).get('detailedBO', []) + if data and isinstance(data, list): + logged_count = 0 + for item in data: + if logged_count >= 5: break + remark = item.get('remark', '') + buss_name = item.get('from_bussname', '') + if "兑换" in remark or "兑换" in buss_name: + if logged_count == 0: + self.log(f"📋 [账户明细] 最近 5 条记录:", notify=True) + order_time = item.get('order_time', '') + amount = item.get('booksNumber') or item.get('books_number') or "0" + self.log(f" 🎁 [抢兑] {order_time} | {remark} (变动:{amount})", notify=True) + logged_count += 1 + if logged_count == 0: + self.log("[账户明细] 暂无兑换记录") + else: + self.log("[账户明细] 暂无兑换记录") + else: + self.log(f"[账户明细] 查询异常: {result.get('msg', 'Result Error')}") + except Exception as e: + self.log(f"sign_query_my_prizes error: {e}") + + def sign_task_main(self): + self.log("==== 签到区 ====") + self.sign_getTelephone(is_initial=True) + self.sign_getContinuous(is_query_only=False) + self.sign_getTaskList() + sc = globalConfig.get("sign_config", {}) + if sc.get("run_grab_coupon", False): + self.sign_grabCoupon() + else: + self.log("签到区-抢话费券: ⏭️ 已被子开关关闭,跳过") + self.sign_getTelephone() + self.sign_query_my_prizes() + + def execute_daily_tasks(self, query_only=False): + if query_only: + self.log("📋 [查询模式] 仅查询资产,跳过任务执行", notify=True) + try: + self.queryRemain() + if globalConfig.get("enable_sign", True): + try: + self.sign_getContinuous(is_query_only=True) + self.sign_getTelephone() + except Exception as e: + self.log(f"首页签到查询异常: {e}") + try: + self.sign_query_my_prizes() + except Exception as e: + self.log(f"抢兑记录查询异常: {e}") + if globalConfig.get("enable_ttlxj", True): + try: + self.ttlxj_task(is_query_only=True) + except Exception as e: + self.log(f"天天领现金查询异常: {e}") + if globalConfig.get("enable_ttxc", True): + try: + self.ttxc_task(is_query_only=True) + except Exception as e: + self.log(f"通通乡村查询异常: {e}") + if globalConfig.get("enable_market", True): + try: + self.market_task(is_query_only=True) + except Exception as e: + self.log(f"权益超市查询异常: {e}") + if globalConfig.get("enable_woread", True): + try: + self.woread_queryTicketAccount() + except Exception as e: + self.log(f"联通阅读查询异常: {e}") + if globalConfig.get("enable_aiting", True): + try: + self.aiting_task(is_query_only=True) + except Exception as e: + self.log(f"联通爱听查询异常: {e}") + if globalConfig.get("enable_security", True): + try: + self.securityButlerTask(is_query_only=True) + except Exception as e: + self.log(f"安全管家查询异常: {e}") + if globalConfig.get("enable_ltyp", True): + try: + self.ltyp_task(is_query_only=True) + except Exception as e: + self.log(f"联通云盘查询异常: {e}") + if globalConfig.get("enable_wostore", True): + try: + self.wostore_cloud_task(is_query_only=True) + except Exception as e: + self.log(f"沃云手机查询异常: {e}") + if globalConfig.get("enable_regional", True): + try: + self.regional_task(is_query_only=True) + except Exception as e: + pass + except Exception as e: + self.log(f"查询异常: {e}") + return + if globalConfig.get("enable_sign", True): + self.sign_task_main() + else: + self.log("==== 签到区 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_ltzf", True): + self.ltzf_task() + else: + self.log("==== 联通祝福 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_ttlxj", True): + self.ttlxj_task() + else: + self.log("==== 天天领现金 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_ttxc", True): + self.ttxc_task() + else: + self.log("==== 通通乡村 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_market", True): + self.market_task() + else: + self.log("==== 权益超市 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_woread", True): + self.woread_task() + else: + self.log("==== 联通阅读 ====") + self.log("⏭️ 已被总开关关闭,跳过") + need_cooldown = globalConfig.get("enable_woread", True) and globalConfig.get("enable_aiting", True) + if need_cooldown: + self.log("⏳ 等待120秒(阅读冷却:联通限制两次阅读间隔2分钟)...") + time.sleep(120) + if globalConfig.get("enable_aiting", True): + self.aiting_task() + else: + self.log("==== 联通爱听 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_security", True): + self.securityButlerTask() + else: + self.log("==== 安全管家 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_ltyp", True): + self.ltyp_task() + else: + self.log("==== 联通云盘 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_wostore", True): + self.wostore_cloud_task() + else: + self.log("==== 沃云手机 ====") + self.log("⏭️ 已被总开关关闭,跳过") + if globalConfig.get("enable_regional", True): + self.regional_task() + else: + self.log("==== 区域专区 ====") + self.log("⏭️ 已被总开关关闭,跳过") + +def cross_view_security_share_keys(users): + participants = [ + u for u in users + if getattr(u, "sec_ai_share_key", "") and getattr(u, "sec_token", "") and getattr(u, "sec_share_task_code", "") + ] + if not participants: + return + if len(participants) < 2: + participants[0].log("联通助理-分享AI助手对话:仅 1 个账号拿到分享key,跳过跨账号互看") + return + print("") + print("========= 联通助理-开始跨账号查看AI分享对话 =========") + n = len(participants) + for i, viewer in enumerate(participants): + target = participants[(i + 1) % n] + viewer.log(f"联通助理-分享AI助手对话:查看账号[{target.index}]分享") + try: + viewer.sec_view_share_detail(target.sec_ai_share_key, viewer.sec_token) + except Exception as e: + viewer.log(f"联通助理-分享AI助手对话:互看异常 {e}") + time.sleep(2) + for u in participants: + try: + u.sec_refresh_security_context(refresh_secret=True) + u.sec_finalize_share_ai_task() + u.sec_recover_pending_claims(rounds=2, delay=6, refresh_context=True) + except Exception as e: + u.log(f"联通助理-分享AI助手对话:互看后领奖异常 {e}") + +def do_notify(users): + if not globalConfig.get("enable_notify", True): + print("推送通知已关闭") + return + notify_content = [] + for u in users: + if u.notify_logs: + phone = u.mobile or u.account_mobile + phone_str = mask_str(phone) if phone else "" + notify_content.append(f"【账号{u.index}】{phone_str}") + notify_content.extend(u.notify_logs) + notify_content.append("") + if notify_content: + content = "\n".join(notify_content) + try: + from notify import send + send("中国联通", content) + print(f"推送成功 (内容长度: {len(content)})") + except Exception as e: + print(f"推送失败,可能未配置 notify.py: {str(e)}") + else: + print("无推送内容") + +def main(): + global GRAB_AMOUNT + print(f"[{datetime.now().strftime('%H:%M:%S')}] [Script Start] chinaUnicom Python v1.0.9") + cookies = os.environ.get("chinaUnicomCookie", "") + if not cookies: + print("[-] 未在环境变量 chinaUnicomCookie 中找到配置") + sys.exit(1) + accounts = [c for c in re.split(r'[&\n]', cookies) if c.strip()] + print(f"[{datetime.now().strftime('%H:%M:%S')}] 发现 {len(accounts)} 个账号") + print("") + users = [] + for idx, config in enumerate(accounts): + u = UserService(idx + 1, config.strip()) + users.append(u) + if u.appId: + print(f"账号[{idx+1}] 识别到 Token#AppId 模式,使用自定义AppId: {u.appId}") + elif u.account_mobile: + print(f"账号[{idx+1}] 识别到账号密码模式: {mask_str(u.account_mobile)}") + try: + if u.token_online: + u.get_city_info() + except: pass + print(f"共找到{len(accounts)}个账号") + print("") + env_amount = os.environ.get("UNICOM_GRAB_AMOUNT", "") + if env_amount and env_amount.isdigit(): + GRAB_AMOUNT = int(env_amount) + query_only = os.environ.get("UNICOM_TEST_MODE", "").strip().lower() == "query" + if query_only: + print("[Test Mode] 仅查询模式,跳过任务执行") + sc = globalConfig.get("sign_config", {}) + mc = globalConfig.get("market_config", {}) + rc = globalConfig.get("regional_config", {}) + grab_mode = False + ah_friday_grab = False + hour = datetime.now().hour + current_min = datetime.now().minute + is_friday = datetime.now().weekday() == 4 + if not query_only: + if sc.get("run_grab_coupon", False) and globalConfig.get("enable_sign", True): + if hour in [9, 17] and (58 <= current_min <= 59): + grab_mode = True + if (AH_FRIDAY_AMOUNT and is_friday and rc.get("run_ah_friday", True) + and globalConfig.get("enable_regional", True) + and hour == 9 and (58 <= current_min <= 59)): + ah_friday_grab = True + grab_mode = True + print("-" * 36) + switch_map = [ + ("enable_sign", "首页签到"), + ("enable_ltzf", "联通祝福"), + ("enable_ttlxj", "天天领现金"), + ("enable_ttxc", "通通乡村"), + ("enable_market", "权益超市"), + ("enable_woread", "联通阅读"), + ("enable_aiting", "联通爱听"), + ("enable_security", "安全管家"), + ("enable_ltyp", "联通云盘"), + ("enable_wostore", "沃云手机"), + ("enable_regional", "区域专区"), + ] + for key, label in switch_map: + enabled = globalConfig.get(key, True) + if grab_mode: + if key == "enable_sign" and sc.get("run_grab_coupon", False): + status = "运行(仅抢兑)" + elif key == "enable_regional" and ah_friday_grab: + status = "运行(安徽抢红包)" + else: + status = "跳过(抢兑模式)" + elif query_only: + status = "仅查询" if enabled else "关闭" + else: + status = "运行" if enabled else "关闭" + print(f"{label}设置为: {status}") + if key == "enable_sign" and enabled and not query_only: + print(f" └─ 抢话费券: {'开启' if sc.get('run_grab_coupon', False) else '关闭'}") + if key == "enable_regional" and enabled and not query_only: + ah_status = "开启" if rc.get("run_ah_friday", True) and AH_FRIDAY_AMOUNT else "关闭" + print(f" └─ 安徽超级星期五: {ah_status}" + (f" (面额{AH_FRIDAY_AMOUNT}元)" if AH_FRIDAY_AMOUNT else "")) + if key == "enable_market" and enabled and not query_only and not grab_mode: + print(f" └─ 浇水: {'开启' if mc.get('run_water', True) else '关闭'}") + print(f" └─ 做任务: {'开启' if mc.get('run_task', True) else '关闭'}") + print(f" └─ 会员中心: {'开启' if mc.get('run_member_center', True) else '关闭'}") + print(f" └─ 抽奖: {'开启' if mc.get('run_draw', True) else '关闭'}") + print(f" └─ 自动领奖: {'开启' if mc.get('run_claim', False) else '关闭'}") + print(f"推送通知设置为: {'开启' if globalConfig.get('enable_notify', True) else '关闭'}") + print(f"设备ID刷新: {'强制刷新' if globalConfig.get('refresh_device_id', False) else '使用缓存'}") + print("-" * 36) + print("") + if grab_mode: + print(f"⏰ [自动触发] 检测到抢兑时间点 ({hour}:{current_min:02d}),进入并发抢兑模式") + tasks_desc = [] + if sc.get("run_grab_coupon", False) and globalConfig.get("enable_sign", True): + tasks_desc.append(f"{GRAB_AMOUNT}元话费券") + if ah_friday_grab: + tasks_desc.append(f"安徽{AH_FRIDAY_AMOUNT}元红包") + print(f"🚨🚨🚨 [抢兑模式已启动] 目标: {' + '.join(tasks_desc)} 🚨🚨🚨") + print("") + from concurrent.futures import ThreadPoolExecutor + + def run_grab_task(u): + u.configure_proxy() + if not u.token_online and u.account_mobile: + u.load_token_from_cache() + is_valid = u.onLine() + if not is_valid and u.account_mobile and u.account_password: + u.unicom_login() + is_valid = u.onLine() + if is_valid: + u.save_token_to_cache() + sub_futures = [] + with ThreadPoolExecutor(max_workers=2) as sub_executor: + if sc.get("run_grab_coupon", False) and globalConfig.get("enable_sign", True): + sub_futures.append(sub_executor.submit(u.sign_grabCoupon)) + if ah_friday_grab: + sub_futures.append(sub_executor.submit(u.ah_friday_task)) + for f in sub_futures: + try: + f.result() + except Exception as e: + u.log(f"抢兑子任务异常: {e}") + else: + u.log("登录流程失败,跳过该账号") + + print(f"🚀 [并发模式] 启动 {len(accounts)} 个账号同时抢兑...") + with ThreadPoolExecutor(max_workers=len(accounts)) as executor: + futures = [executor.submit(run_grab_task, u) for u in users] + for future in futures: + try: + future.result() + except Exception as e: + print(f"[-] Thread Error: {e}") + do_notify(users) + return + print("🚀 开始串行执行日常任务...") + print("") + for u in users: + print("") + print(f"🔄 正在初始化账号[{u.index}]...") + u.configure_proxy() + if not u.token_online and u.account_mobile: + u.load_token_from_cache() + if not u.token_online and u.account_mobile and u.account_password: + u.unicom_login() + if u.onLine(): + u.save_token_to_cache() + print("") + print(f"------------------ 账号[{u.index}][{mask_str(u.account_mobile)}] ------------------") + print("") + u.execute_daily_tasks(query_only=query_only) + print("⏳ 账号处理完毕,等待 2 秒...") + time.sleep(2) + else: + u.log("登录流程失败,跳过该账号") + cross_view_security_share_keys(users) + do_notify(users) +if __name__ == "__main__": + main()