7062 lines
333 KiB
Python
7062 lines
333 KiB
Python
# -*- 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()
|