diff --git a/mcloud.py b/mcloud.py index ccf9c4a..007c9a9 100644 --- a/mcloud.py +++ b/mcloud.py @@ -1,41 +1,295 @@ -# 脚本名称: [移动云盘] -# 功能描述: [签到 基础任务 果园 云朵大作战] -# 使用说明: -# - [抓包 Cookie:任意Authorization] -# - [例: Basic cGMxxxxgzt@Basic cGMxxxxgzra] -# 环境变量设置: -# - [ydyp_ck] - -# 注: 本脚本仅用于个人学习和交流,请勿用于非法用途。作者不承担由于滥用此脚本所引起的任何责任,请在下载后24小时内删除。 - # cron: 5 12 * * * # const $ = new Env('移动云盘') +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +移动云盘自动签到 v5.0.5 + +包含以下功能: +1. 每日自动签到 (签到/抽奖/摇一摇/新版云朵领取) +2. 多账号并发支持 (环境变量 & 分隔) +3. 云朵中心新版任务自动处理 (上传/分享/AI相机/月任务补传等) +4. 临时文件智能清理与详细日志推送 + +更新说明: + +### 20260503 +v5.0.5: +- 修复分享文件任务流程。 +- 新增去体验AI相机任务。 +- 新增五一福利任务。 + +### 20260425 +v5.0.4: +- 整合 Authorization 自动刷新、本地 token 缓存与过期前续期。 +- 新增账号级 deviceId/token 缓存,支持手动补填 deviceId 并复用。 +- 优化 JWT 失败重试、设备信息与启动提示。 + +### 20260420 +v5.0.3: +- 适配新版签到链路,修复旧接口报错。 +- 接入云朵中心新版任务。 +- 新增 AI 相机、上传清理、分享文件每日自动化与月上传补传能力。 +- 优化启动与任务日志输出。 + +配置说明: +变量名: ydyp +赋值方式: +格式:Authorization值#手机号 + +⚠️ 依赖安装: +pip3 install requests pycryptodome + +定时规则建议 (Cron): +0 0 8,16,20 * * * (每日早中晚执行) + +Author: YaoHuo8648 +Email: zheyizzf@188.com +Update: 2026.05.03 +""" + +import base64 +import hashlib +import json import os import random import re import time -import json -import base64 +import uuid +from datetime import datetime, timezone, timedelta from os import path +from pathlib import Path +from urllib.parse import unquote import requests -ua = 'Mozilla/5.0 (Linux; Android 11; M2012K10C Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/90.0.4430.210 Mobile Safari/537.36 MCloudApp/10.0.1' +try: + from Crypto.Cipher import AES + from Crypto.Util.Padding import pad +except ImportError: + AES = None + pad = None + +SCRIPT_VERSION = '5.0.5' + +TOKEN_STORAGE_FILENAME = '' +DEVICE_ID_STORAGE_FILENAME = '' + +# ⭐ 统一设备信息:iPhone 16 Pro + iOS 18_7 + 版本 12.5.4(抓包真实值) +ua = '' +market_ua = '' +cloud_file_dummy_content = b'0' +cloud_file_dummy_hash = hashlib.sha256(cloud_file_dummy_content).hexdigest() +TOKEN_VALID_TIME = 21600000 +TOKEN_REFRESH_ADVANCE = 24 * 60 * 60 * 1000 +TOKEN_REFRESH_COOLDOWN = 864000 +REFRESH_TOKEN_AES_KEY = 'c7lXOigXahPnTViq' +AI_TOOL_ACCOUNT_AES_KEY = 'xuL97!x7GGxG%8V4' +AI_TOOL_ACCOUNT_AES_IV = '5OuCxk4XNu0NA*%x' +TOKEN_EXPIRE_SECONDS_FALLBACK = 2592000 err_accounts = '' # 异常账号 -err_message = '' # 错误信息 -user_amount = '' # 用户云朵·数量 +all_logs = '' # 所有用户的详细运行日志 (原 err_message) +user_amount = '' # 用户云朵·数量 GLOBAL_DEBUG = False -# Bark推送配置 -# ==================== Bark 推送配置 ==================== -BARK_ICON = "https://gitee.com/hlt1995/BARK_ICON/raw/main/mcloud.png" # 自定义图标 -BARK_GROUP = "移动云盘" # 自定义分组 -PUSH_SWITCH = "1" #推送开关,1开启,0关闭 -# ======================================================= -os.environ["PUSH_SWITCH"] = PUSH_SWITCH +def current_millis(): + return int(time.time() * 1000) + + +def normalize_authorization(token): + token = (token or '').strip() + if token and not token.startswith('Basic '): + return f'Basic {token}' + return token + + +def random_string(length=16): + chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + return ''.join(random.choice(chars) for _ in range(length)) + + +def generate_uuid(): + return ''.join([ + random_string(8), '-', + random_string(4), '-4', + random_string(3), '-', + random.choice('89ab'), random_string(3), '-', + random_string(12) + ]) + + +# 真实抓包 deviceId(base64 格式,iOS iPhone 16 Pro + iOS 18_7) +REAL_DEVICE_ID = '' + +DEVICE_PROFILE_URL = "https://slw.h5cmpassport.com:9090/deviceprofile/v4" +DEVICE_PROFILE_PAYLOAD = { + "appId": "default", + "organization": "FXlyfmWg2AzwbrxDKSv5", + "ep": "WydTnuOv+Rtg/Qj8Q4vnhSXJN4UHQPF2jjs+LVJkD3u8HXglndPAndOgrlmg2Q8q0FUQRZpN0N7e61ebhjw/Gba22ydgOMbBRfbSmKSnNWaACA+MzAX5Q4dd980zPqelMxGVzB3jkr1wGE6cVQkwWFq/xbdnkK/Sh6xrPDjYvho=", + "data": "5b5f2054405d6155102ed35a134758f768e60b7c1ac7af66acb16871d78a099cbcbabb3fb5ebeefe6cbae063407fca585a343ce5bef4f4e4588df42ca8ae8a6504b3646066fd7dc46465a83d510fbb477ba72f7375db7cbcba9b712ec88d85fc8d410536b96ca644c8ca3afbca00e0084ad9709b93b86923bf1fadef48be3e888b52dd2775c180b5b8e7bae139fadf2944f73010be9704daa6f4cf596b4adccff7b5de84e45698b781d963b69fa8ec28e43083512ab5749ea05c4efce14945d647c9f33d6296750ff2ba59bff5b7fcf698ffa146b7fe7e5c405b13100818b53fd034d05edea63c8365d9113bc7d4c0652892fcae75cdb491ae0215fbd822b1877b209fc8c68710badc6915080b7b994fa4b86a8f7b37e929cecbd1c590ad7382beb3ae8b9cc56ed84e927cbe41d8b4b15bbeecc69f5463d402cc2732fe5b76ec201632afbc16228531a65c1810482e4eb48157bc8b23cd363c6809a3fd629e3520514c06a720616e1788fe10203f9ebfa1de24c66213e334e3a3b3ff8a8866b7aefd9b4f2c88d216f45b551d693433940569092f0c7aca25019dc2003e8eab1967ac1dc32b0912701b0abc17e0509bada0cf0fcbe3c5fb64f0d5c6f02303b1540829a301673da89f7460d00190bda07c9b82c263277066f8e7e91c4916f247f9d9fe295a46d16cd087cee865d9e50edeb8e88842c560b09f853b5f89d2d0c4ed160f5bc293f7c69ece9e2d64d7217857fd2d64d57bea1ccea1b52896bb9aaf2ec3baa2421bce8d011813a1b26f0acb3a3cf594298bd725f8da17717b965f85e46a52c758ed1e95218e06f7e96a9f13e4855a0bb4bcf8b5f571887ec58c7438e99f06562414bcb274038fe6ffc1b8991021e35866cef5010184e3fbbd49c19d6020315731e9e57b7cd6a1e8b33c97746a782f9b4a26696966f40324f1ff76d3d1d24bf544230438dc32ab26d6dc107adf9feac34ffbbaa8814cec674e9469de54a714273a47f4fd06561e611f6741a4f0362a3b8821b0c69a3a04ced876fbf1b5fdc58097b1d7087aa2c0df556f8a06288db8c306cda4525d91c0452a0d2747982bd70b31c6905d4e483e8519d4d605af776be2a81224e3a6cc0b6ec49ad2cdb434bd85b5079ff86f68bf5ebb41336f30ec84fe19fabbd10a4422a274a3749d70c6b39cf7cdc1eb0cb228abee2475d16c57635a332628727b76a1fac0b26bf7bbdf4c5b956261919e7d2bd67733656855503670d48fe3680d04b65aac48d99bd47aedb6091c0a6df53be5bd662c1130feb6b469578cb146e1ae004471641fbc028cc06b80cfcdc50f8231e58b4126ab750b1d02eb8ac417b53a5ae50846db9aeadf4f1c98e33228db5143cb3d928217b769eaf32d181320a0bee4805334c28a03995d925b52fda358d19c52e3838c243b8c7d3256337943705c1311526c290fad975b7d7ade4bbc9292dbd7b9c0314715ef3c785a720e674dc23538af333cc6ff541aea70086287a8b4407c66ce673c9a47268de014c876a3a6a577d501285f6f489e2519f51bf4feafe307333a9e077f613527bbe1ce632127df654588410f713bb4a61e050cae618e98cc9adbb77d9df95733449c06e62094f3cdaf2ba39f94223ed7ca63ea4dec37d7283bdd0d2015511e7e57212073a540b308b10d7f85de73865fc2ffbf05a85ae25a7b52f0292236ee75f738add8144c7b2767a2100451363a47c12dfb674bd3ee000fa41565e9fbc60440a629160a2d2a99ec23dccc6815f644a2dd1eb059ab8593d9b04b1b81f5e427570cfc06eba8456b68159e6886843bcf4374b02de2e5be8d900882f78a71c2f3819d2e9c45e64b5d006c7a5914d1482f01ed5c0cfb44c3543656e96b5d91b39cd667af4dc60f44752da28eda57d2453d26a099529a2a38c9b9b2f0a73a69445030321b0a87287f6469f4d585739cded2e79c66df9c949eb7b2b8a8ff78e80a88ca494f3410195e021ec5009f8cd29781f09d58e6f866102072f1cee202c6ce21d72795b47a0ab8464fa54836c36a28ff73828e7a39dd1203d5a051ac4cd22b4f8c9f1e4e9c42f0c85b101b1eb495c0a767697dccab920489fae867ff38c5f917aec269d0ac9a1d6005407db762349d77e990581e19b1912fc975a9cdd2", + "os": "web", + "encode": 5, + "compress": 2, +} + + +def fetch_device_id(): + """从设备指纹接口获取 deviceId,加 B 前缀""" + try: + resp = requests.post( + DEVICE_PROFILE_URL, + json=DEVICE_PROFILE_PAYLOAD, + headers={ + "Content-Type": "application/json;charset=utf-8", + "Origin": "https://m.mcloud.139.com", + "Referer": "https://m.mcloud.139.com/", + }, + timeout=15, + ) + data = resp.json() + raw_id = data.get("detail", {}).get("deviceId", "") + if raw_id: + return "B" + raw_id + except Exception as e: + print(f" 获取设备指纹失败: {e}") + return "" + + +def generate_device_id(): + if REAL_DEVICE_ID: + return REAL_DEVICE_ID + device_info = { + 'deviceId': uuid.uuid4().hex.upper(), + 'brand': 'Apple', + 'model': 'iPhone 16 Pro', + 'system': 'iOS 18.7', + 'timestamp': current_millis() + } + raw = json.dumps(device_info, ensure_ascii=False, separators=(',', ':')) + return base64.b64encode(raw.encode('utf-8')).decode('utf-8') + + +def get_env_device_id(): + return '' + + +def build_x_device_info(device_id, net_type='wifi', terminal_type='8', + version='12.5.4', brand='Apple', model='iPhone 16 Pro', + system='iOS 18.7'): + """构造 x-DeviceInfo 头,与 UA 保持一致""" + return f"{net_type}||{terminal_type}|{version}|{brand}|{model}|{device_id}||{system.lower()}|||||" + + +def aes_encrypt(data, key): + if AES is None or pad is None: + raise ImportError('未安装 pycryptodome,无法执行 Token 自动刷新') + key_bytes = key.encode('utf-8') + if not isinstance(data, str): + data = json.dumps(data, ensure_ascii = False, separators = (',', ':')) + cipher = AES.new(key_bytes, AES.MODE_ECB) + encrypted = cipher.encrypt(pad(data.encode('utf-8'), AES.block_size)) + return base64.b64encode(encrypted).decode('utf-8') + + +def extract_raw_token(token, account): + token = normalize_authorization(token) + if token.startswith('Basic '): + token = token[6:] + try: + decoded = base64.b64decode(token).decode('utf-8') + parts = decoded.split(':') + if len(parts) >= 3: + return parts[2] + except Exception: + pass + return token + + +def build_authorization(account, raw_token): + return f"Basic {base64.b64encode(f'mobile:{account}:{raw_token}'.encode()).decode()}" + + +def parse_expire_time_to_millis(expire_time): + try: + expire_seconds = int(float(expire_time)) + except (TypeError, ValueError): + expire_seconds = TOKEN_EXPIRE_SECONDS_FALLBACK + if expire_seconds <= 0: + expire_seconds = TOKEN_EXPIRE_SECONDS_FALLBACK + return current_millis() + expire_seconds * 1000 + + +def get_storage_base_dir(): + return Path(path.abspath(path.dirname(__file__))) + + +def get_storage_file_path(filename): + return get_storage_base_dir() / filename + + +def get_token_storage_path(): + return get_storage_file_path(TOKEN_STORAGE_FILENAME) + + +def load_token_storage(): + return {'accounts': {}} + + +def save_token_storage(data): + pass + + +# ============ 简易存储兼容层(不再写入 deviceId 缓存文件)============ +def get_device_id_storage_path(): + return get_storage_file_path(DEVICE_ID_STORAGE_FILENAME) + + +def load_device_id_storage(): + storage_path = get_device_id_storage_path() + if not storage_path.exists(): + return {} + try: + with open(storage_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception: + return {} + + +def save_device_id(device_id, account): + pass + + +def save_token_info(account, token, expires_at, last_refresh_at): + pass + + +def ensure_account_storage_entry(account, token=''): + pass + + +def get_token_info(account): + return {} + + +def get_device_id(account): + return '' + + +def print_startup_info(account_count): + print(f"移动云盘自动签到 v{SCRIPT_VERSION}") + print(f"移动云盘共获取到{account_count}个账号") + + +def print_device_id_notice(): + print("变量格式: Authorization值#手机号#deviceId") + + +def print_storage_path_notice(): + pass + # 发送通知 def load_send(): @@ -44,118 +298,289 @@ def load_send(): if path.exists(notify_file): try: + import sys + if cur_path not in sys.path: + sys.path.insert(0, cur_path) from notify import send print("加载通知服务成功!") return send - except ImportError: - print("加载通知服务失败~") + except Exception as e: + print(f"加载通知服务失败: {e}") else: - print("加载通知服务失败~") + print("加载通知服务失败: notify.py不存在") return False class YP: def __init__(self, cookie): - self.notebook_id = None - self.note_token = None - self.note_auth = None - self.click_num = 15 # 定义抽奖次数和摇一摇戳一戳次数 - self.draw = 1 # 抽奖次数,首次免费 - self.session = requests.Session() - - self.timestamp = str(int(round(time.time() * 1000))) - self.cookies = {'sensors_stay_time': self.timestamp} - - self.parse_cookie(cookie) - - if self.account and len(self.account) >= 7: - self.encrypt_account = self.account[:3] + "*" * 4 + self.account[7:] - else: - self.encrypt_account = self.account or "未知账号" - - self.fruit_url = 'https://happy.mail.10086.cn/jsp/cn/garden/' - - self.jwtHeaders = { - 'User-Agent': ua, - 'Accept': '*/*', - 'Host': 'caiyun.feixin.10086.cn:7071', - } - self.treeHeaders = { - 'Host': 'happy.mail.10086.cn', - 'Accept': 'application/json, text/plain, */*', - 'User-Agent': ua, - 'Referer': 'https://happy.mail.10086.cn/jsp/cn/garden/wap/index.html?sourceid=1003', - 'Cookie': '', - } - - def parse_cookie(self, cookie): - if '#' in cookie: - parts = cookie.split("#") - if len(parts) >= 3: - self.Authorization = parts[0] - self.account = parts[1] - self.auth_token = parts[2] - print(f"使用旧格式账号: {self.account[:3] + '*' * 4 + self.account[7:] if len(self.account) >= 7 else self.account}") - return - - self.Authorization = cookie.strip() - self.auth_token = "00" - try: - if self.Authorization.startswith("Basic "): - auth_value = self.Authorization[6:] - else: - auth_value = self.Authorization - - decoded_bytes = base64.b64decode(auth_value) - decoded_str = decoded_bytes.decode('utf-8') - - parts = decoded_str.split(':') - if len(parts) >= 2: - self.account = parts[1] - else: - raise ValueError("无法从CK中解析手机号") - - except Exception as e: - print(f"解析CK失败: {e}") - self.account = "13800138000" - print(f"使用默认手机号: {self.account}") + self.notebook_id = None + self.note_token = None + self.note_auth = None + self.auth_token = None + self.click_num = 15 + self.draw = 1 + self.client_version = '12.5.4' + self.market_base_url = 'https://m.mcloud.139.com' + self.market_source_id = '1097' + self.sso_token = None + self.user_domain_id = '' + self.market_device_id = '' + self.market_x_device_info = '' + self.market_device_from_env = False + self.market_device_is_placeholder = False + self.market_headers = {} + self.market_cookies = {} + self.session = requests.Session() + self.user_log_lines = [] - # 捕获异常 - + self.timestamp = str(int(round(time.time() * 1000))) + self.cookies = {'sensors_stay_time': self.timestamp} + + parts = cookie.split("#") + if len(parts) < 2: + raise ValueError(f"⚠️ 变量值格式错误,需要: Authorization值#手机号#deviceId") + + self.Authorization = normalize_authorization(parts[0]) + self.account = parts[1].strip() + self.init_device_id = parts[2].strip() if len(parts) >= 3 else '' + self.token_storage = load_token_storage() + self.refresh_capable = AES is not None and pad is not None + self.load_persisted_authorization() + self.load_or_create_market_device_profile() + self.auth_token = extract_raw_token(self.Authorization, self.account) + + if len(self.account) >= 11: + self.encrypt_account = self.account[:3] + "****" + self.account[-4:] + else: + self.encrypt_account = self.account + + self.fruit_url = 'https://happy.mail.10086.cn/jsp/cn/garden/' + + self.jwtHeaders = { + 'User-Agent': ua, + 'Accept': '*/*', + 'Host': 'caiyun.feixin.10086.cn:7071', + } + self.treeHeaders = { + 'Host': 'happy.mail.10086.cn', + 'Accept': 'application/json, text/plain, */*', + 'User-Agent': ua, + 'Referer': 'https://happy.mail.10086.cn/jsp/cn/garden/wap/index.html?sourceid=1003', + 'Cookie': '', + } + + except Exception as e: + print(f"{e}") + self.Authorization = None + + def get_storage_record(self): + """获取存储记录,优先从新版简易存储读取""" + # 优先从新版简易存储读取 + new_storage = get_token_info(self.account) + if new_storage.get('token') or new_storage.get('deviceId'): + return new_storage + # 后备:从旧版 token_storage 读取 + return (self.token_storage.get('accounts') or {}).get(self.account, {}) or {} + + def load_persisted_authorization(self): + """加载已保存的 Authorization,优先从简易存储读取""" + # 优先从新版简易存储读取 + new_storage = get_token_info(self.account) + stored_token = normalize_authorization(new_storage.get('token', '')) + if stored_token: + self.Authorization = stored_token + # 同时也写入旧版存储,保持兼容 + token_storage = load_token_storage() + if self.account not in token_storage.get('accounts', {}): + token_storage['accounts'][self.account] = {} + token_storage['accounts'][self.account]['token'] = stored_token + save_token_storage(token_storage) + return + # 后备:从旧版 token_storage 读取 + stored = self.get_storage_record() + stored_token = normalize_authorization(stored.get('token', '')) + if stored_token: + self.Authorization = stored_token + + def load_or_create_market_device_profile(self): + """加载 deviceId:强制从设备指纹接口获取,忽略环境变量和缓存""" + ensure_account_storage_entry(self.account, self.Authorization) + + fetched_id = fetch_device_id() + if fetched_id: + self.market_device_id = fetched_id + self.market_x_device_info = build_x_device_info(fetched_id) + self.market_device_from_env = False + self.market_device_is_placeholder = False + save_device_id(fetched_id, self.account) + return + + self.market_device_id = generate_device_id() + self.market_x_device_info = build_x_device_info(self.market_device_id) + self.market_device_is_placeholder = False + if self.market_device_id: + save_device_id(self.market_device_id, self.account) + + def save_authorization_record(self, token=None, user_domain_id=None, refreshed=False, expires_at=None, + market_device_id=None, x_device_info=None, market_device_generated=None): + token = normalize_authorization(token or self.Authorization) + accounts = self.token_storage.setdefault('accounts', {}) + old_record = accounts.get(self.account, {}) or {} + if not token: + token = normalize_authorization(old_record.get('token', '')) + last_refresh_at = old_record.get('lastRefreshAt', 0) + if refreshed: + last_refresh_at = current_millis() + record_expires_at = expires_at or old_record.get('expiresAt') or (current_millis() + TOKEN_VALID_TIME) + record = { + **old_record, + 'account': self.account, + 'token': token, + 'authToken': extract_raw_token(token, self.account), + 'expiresAt': int(record_expires_at), + 'updatedAt': current_millis(), + 'lastRefreshAt': last_refresh_at, + } + if user_domain_id or old_record.get('userDomainId'): + record['userDomainId'] = user_domain_id or old_record.get('userDomainId') + final_market_device_id = market_device_id or old_record.get('marketDeviceId') + final_x_device_info = x_device_info or old_record.get('xDeviceInfo') + if final_market_device_id: + record['marketDeviceId'] = final_market_device_id + if final_x_device_info: + record['xDeviceInfo'] = final_x_device_info + if market_device_generated is None: + if 'marketDeviceGenerated' in old_record: + record['marketDeviceGenerated'] = old_record.get('marketDeviceGenerated') + else: + record['marketDeviceGenerated'] = bool(market_device_generated) + accounts[self.account] = record + save_token_storage(self.token_storage) + + def should_refresh_authorization(self, force=False): + if force: + return True, '强制刷新' + stored = self.get_storage_record() + now = current_millis() + expires_at = int(stored.get('expiresAt') or 0) + if not stored.get('token'): + return True, '本地无缓存Token' + if now >= expires_at: + return True, '缓存Token已过期' + if expires_at - now <= TOKEN_REFRESH_ADVANCE: + hours = (expires_at - now) / 3600000 + return True, f'将在 {hours:.1f} 小时后过期' + days = (expires_at - now) / 86400000 + return False, f'Token 状态良好(剩余 {days:.1f} 天)' + + def refresh_authorization_token(self, force=False): + need_refresh, reason = self.should_refresh_authorization(force = force) + if not need_refresh: + self.log(f'-Authorization状态: {reason}') + return True + if not self.refresh_capable: + self.log('-未安装 pycryptodome,跳过Authorization自动刷新') + return True + + # 使用微信小程序UA刷新Token(源码:miniprogram|||1.0) + refresh_headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.47(0x18002f2c) NetType/WIFI Language/zh_CN miniProgram/wx4e4ed37286c816c2', + 'x-yun-tid': generate_uuid(), + 'Authorization': self.Authorization, + 'x-yun-api-version': 'v1', + 'x-yun-module-type': '100', + 'x-yun-op-type': '1', + 'x-yun-app-channel': '10214200', + 'x-yun-client-info': '||8||||||||||||', + 'hcy-cool-flag': '1', + } + stored = self.get_storage_record() + if stored.get('userDomainId'): + refresh_headers['x-yun-uni'] = stored['userDomainId'] + + encrypted_data = aes_encrypt({'phoneNumber': self.account}, REFRESH_TOKEN_AES_KEY) + refresh_data = self.request_json('https://user-njs.yun.139.com/user/auth/refreshToken', + headers = refresh_headers, + data = {'data': encrypted_data}, + method = 'POST', + retries = 1) + if not refresh_data: + self.log(f'-Authorization刷新失败: 接口无响应 ({reason})') + return True + code = str(refresh_data.get('code', '')) + data = refresh_data.get('data') + success = refresh_data.get('success', False) + is_success = (code in ('0', '00', '000', '0000')) or bool(success) or (code.startswith('0') and len(code) <= 4) + if not is_success or not isinstance(data, dict): + self.log(f"-Authorization刷新失败: {refresh_data.get('message') or refresh_data.get('msg', '未知错误')} (code={code or 'unknown'})") + return True + raw_token = data.get('token') + if not raw_token: + self.log('-Authorization刷新失败: 响应缺少token') + return True + expires_at = parse_expire_time_to_millis(data.get('expireTime')) + self.Authorization = build_authorization(self.account, raw_token) + self.auth_token = raw_token + self.save_authorization_record(token = self.Authorization, refreshed = True, expires_at = expires_at) + self.log(f'-Authorization自动刷新成功: {reason}') + self.log(f"-Authorization有效期: {datetime.fromtimestamp(expires_at / 1000).strftime('%Y-%m-%d %H:%M:%S')}") + return True + + def sync_token_storage(self): + """同步存储 token 和 deviceId 到简易存储""" + # 保存 token 刷新信息到简易存储 + stored = self.get_storage_record() + expires_at = stored.get('expiresAt', current_millis() + TOKEN_VALID_TIME) + last_refresh_at = stored.get('lastRefreshAt', 0) + save_token_info(self.account, self.Authorization, expires_at, last_refresh_at) + # 仅当账号已手动配置真实 deviceId 时才回写缓存; + # 运行时兜底 deviceId 不落盘,避免覆盖成自动值。 + if self.market_device_id and not self.market_device_is_placeholder: + save_device_id(self.market_device_id, self.account) + # 同时保持旧版存储兼容 + self.save_authorization_record(token = self.Authorization, user_domain_id = self.user_domain_id) + + def log(self, content): + print(content) + self.user_log_lines.append(content) + + + @staticmethod def catch_errors(func): def wrapper(self, *args, **kwargs): try: return func(self, *args, **kwargs) except Exception as e: - global err_message - print("错误:", str(e)) - err_message += f'用户[{self.encrypt_account}]:{e}\n' # 错误信息 + err_str = f"错误: {str(e)}" + print(err_str) + self.user_log_lines.append(err_str) return None return wrapper @catch_errors def run(self): + self.refresh_authorization_token() if self.jwt(): self.signin_status() self.click() self.get_tasklist(url = 'sign_in_3', app_type = 'cloud_app') - print(f'\n☁️ 云朵大作战') - self.cloud_game() - print(f'\n🌳 果园任务') - self.fruitLogin() - print(f'\n📰 公众号任务') + self.log(f'\n📰 公众号任务') self.wxsign() self.shake() self.surplus_num() - print(f'\n🔥 热门任务') + self.log(f'\n🔥 热门任务') self.backup_cloud() - self.open_send() - print(f'\n📧 139邮箱任务') + self.log(f'\n📧 139邮箱任务') self.get_tasklist(url = 'newsign_139mail', app_type = 'email_app') self.receive() + global all_logs + user_log_str = "\n".join(self.user_log_lines) + all_logs += f"用户【{self.encrypt_account}】日志:\n{user_log_str}\n\n" + else: global err_accounts # 失效账号 @@ -167,14 +592,14 @@ class YP: debug = debug if debug is not None else GLOBAL_DEBUG - self.session.headers.update(headers or {}) - if cookies: - self.session.cookies.update(cookies) + request_headers = dict(headers or {}) + request_cookies = dict(cookies or {}) request_args = {'json': data} if isinstance(data, dict) else {'data': data} for attempt in range(retries): try: - response = self.session.request(method, url, params = params, **request_args) + response = self.session.request(method, url, params = params, headers = request_headers or None, + cookies = request_cookies or None, **request_args) response.raise_for_status() if debug: print(f'\n【{url}】响应数据:\n{response.text}') @@ -186,20 +611,855 @@ class YP: return None time.sleep(1) - # 随机延迟默认1-1.5s + def request_json(self, url, headers=None, cookies=None, data=None, params=None, method='GET', debug=None, + retries=5): + response = self.send_request(url, headers = headers, cookies = cookies, data = data, params = params, + method = method, debug = debug, retries = retries) + if response is None: + return None + try: + return response.json() + except ValueError as e: + self.log(f'响应解析失败: {e}') + return None + + @staticmethod + def get_today_sign_state(result): + today_sign_in = result.get('todaySignIn') + if isinstance(today_sign_in, bool): + return today_sign_in + for day in result.get('cal') or []: + if day.get('t'): + return bool(day.get('s')) + return None + + @staticmethod + def extract_user_domain_id(jwt_token): + try: + payload = jwt_token.split('.')[1] + payload += '=' * (-len(payload) % 4) + data = json.loads(base64.urlsafe_b64decode(payload).decode()) + sub = data.get('sub', '') + if isinstance(sub, str): + sub = json.loads(sub) + return sub.get('userDomainId', '') + except (IndexError, KeyError, TypeError, ValueError, json.JSONDecodeError): + return '' + + def build_market_context(self, jwt_token): + self.user_domain_id = self.extract_user_domain_id(jwt_token) + self.market_headers = { + 'User-Agent': market_ua, + 'Accept': '*/*', + 'jwtToken': jwt_token, + 'X-Requested-With': 'com.chinamobile.mcloud', + 'Referer': self.build_market_page_url(), + } + self.market_cookies = {'jwtToken': jwt_token} + if self.user_domain_id: + self.market_cookies['userDomainId'] = self.user_domain_id + self.seed_market_device_cookie() + self.sync_token_storage() + + def build_market_page_url(self, source_id=None): + current_source_id = source_id or self.market_source_id + return f'{self.market_base_url}/portal/mobilecloud/index.html?path=newsignin&sourceid={current_source_id}&enableShare=1&token={self.sso_token or ""}&targetSourceId=001005' + + def get_market_device_id(self): + if self.market_device_id: + return self.market_device_id + for cookie in self.session.cookies: + if cookie.name.startswith('.thumbcache_') and cookie.value: + return unquote(cookie.value) + return '' + + def seed_market_device_cookie(self): + device_id = self.market_device_id + if not device_id: + return + cookie_value = device_id[1:] if self.market_device_from_env and device_id.startswith('B') else device_id + if any(cookie.name.startswith('.thumbcache_') and unquote(cookie.value) == cookie_value + for cookie in self.session.cookies): + return + self.session.cookies.set(f'.thumbcache_{self.account}', cookie_value, domain='m.mcloud.139.com', path='/') + + def build_market_headers(self, extra_headers=None, referer=None): + headers = dict(self.market_headers) + headers['Referer'] = referer or headers.get('Referer') or self.build_market_page_url() + device_id = self.get_market_device_id() + if device_id: + headers['deviceId'] = device_id + if self.market_x_device_info: + headers['x-DeviceInfo'] = self.market_x_device_info + if extra_headers: + headers.update(extra_headers) + return headers + + def build_receive_headers(self, source_id=None): + return self.build_market_headers({ + 'showLoading': 'true', + 'appVersion': f'{self.client_version}.0', + 'activityId': 'sign_in_3', + }, referer = self.build_market_page_url(source_id)) + + def request_market_json(self, url, params=None, data=None, method='GET', debug=None, retries=5, headers=None, + cookies=None): + request_cookies = dict(self.market_cookies) + if cookies: + request_cookies.update(cookies) + return self.request_json(url, headers = self.build_market_headers(headers), cookies = request_cookies, + data = data, params = params, method = method, debug = debug, retries = retries) + + def post_signin_journaling(self, keyword, source_id=None): + current_source_id = source_id or self.market_source_id + payload = f'module=uservisit&optkeyword={keyword}&sourceid={current_source_id}&marketName=sign_in_3' + response = self.send_request(f'{self.market_base_url}/ycloud/visitlog/journaling', + headers = self.build_market_headers( + {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'}, + referer = self.build_market_page_url(current_source_id) + ), + cookies = self.market_cookies, data = payload, method = 'POST', retries = 1) + return response is not None + + def prepare_signin_center_session(self, for_receive=False, source_id=None): + current_source_id = source_id or self.market_source_id + page_url = self.build_market_page_url(current_source_id) + self.send_request(page_url, headers = self.build_market_headers(referer = page_url), + cookies = self.market_cookies, retries = 1) + for keyword in ( + 'newsignin_index_pv', + 'newsignin_index_client', + 'newsignin_index_app_client', + 'newsignin_index_cookie_login', + 'newsignin_index_cookie', + 'newsignin_index_app_cookie_login', + ): + self.post_signin_journaling(keyword, current_source_id) + if for_receive: + self.post_signin_journaling('newsignin_index_receive_type', current_source_id) + return True + + def click_task(self, task_id, key='task'): + return self.request_market_json(f'{self.market_base_url}/market/signin/task/click?key={key}&id={task_id}') + + def get_notice_status(self): + send_data = self.request_json('https://caiyun.feixin.10086.cn/market/msgPushOn/task/status', + headers = self.jwtHeaders) or {} + if send_data.get('code') != 0: + return {} + return send_data.get('result', {}) or {} + + def format_notice_task_log(self, task_name, notice_status): + if not notice_status: + return f'-需手动完成: {task_name}' + push_on = int(notice_status.get('pushOn') or 0) + first_status = int(notice_status.get('firstTaskStatus') or 0) + second_status = int(notice_status.get('secondTaskStatus') or 0) + on_duration = int(notice_status.get('onDuaration') or 0) + total = int(notice_status.get('total') or 31) + if push_on != 1: + return f'-需手动完成: {task_name} (通知未开启)' + if second_status == 3: + return f'-已完成: {task_name}' + if first_status != 3: + return f'-待领取: {task_name} (首日奖励可领取)' + if second_status == 2: + return f'-待领取: {task_name} (已开启{on_duration}/{total}天)' + return f'-进行中: {task_name} (已开启{on_duration}/{total}天)' + + def build_cloud_file_headers(self): + """云盘文件操作请求头:沿用 v5.0.3 已验证可用的上传头,避免 00010014""" + return { + 'x-yun-op-type': '1', + 'x-yun-sub-op-type': '100', + 'x-yun-api-version': 'v1', + 'x-yun-client-info': '6|127.0.0.1|1|12.1.0|realme|RMX5060|BCFF2BBA6881DD8E4971803C63DDB5E4|02-00-00-00-00-00|android 15|1264X2592|zh||||032|0|', + 'x-yun-app-channel': '10000023', + 'Authorization': self.Authorization, + 'Content-Type': 'application/json; charset=UTF-8', + 'User-Agent': 'okhttp/4.12.0', + 'Host': 'personal-kd-njs.yun.139.com', + 'Connection': 'Keep-Alive' + } + + def build_share_headers(self): + """分享文件请求头:沿用 v5.0.3 已验证可用的分享头""" + return { + 'Authorization': self.Authorization, + 'x-yun-api-version': 'v1', + 'x-yun-app-channel': '10000023', + 'x-yun-client-info': f'||9|{self.client_version}|Chrome|143.0.7499.146|codextestshare||Windows 10||zh-CN|||Q2hyb21l||', + 'x-yun-module-type': '100', + 'x-yun-svc-type': '1', + 'x-SvcType': '1', + 'x-yun-channel-source': '10000023', + 'x-huawei-channelSrc': '10000023', + 'Content-Type': 'application/json;charset=UTF-8', + 'CMS-DEVICE': 'default', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', + 'Referer': 'https://yun.139.com/shareweb/', + 'Origin': 'https://yun.139.com', + } + + def create_cloud_file(self, prefix, extension='txt'): + beijing_tz = timezone(timedelta(hours=8)) + now = datetime.now(beijing_tz) + file_size = len(cloud_file_dummy_content) + file_name = f"{prefix}{now.strftime('%Y%m%d_%H%M%S')}.{extension.lstrip('.')}" + payload = { + "contentHash": cloud_file_dummy_hash, + "contentHashAlgorithm": "SHA256", + "contentType": "application/oct-stream", + "fileRenameMode": "force_rename", + "localCreatedAt": now.isoformat(timespec='milliseconds'), + "name": file_name, + "parallelUpload": True, + "parentFileId": "/", + "partInfos": [{ + "end": file_size, + "partNumber": 1, + "partSize": file_size, + "start": 0 + }], + "size": file_size, + "type": "file" + } + response = self.send_request('https://personal-kd-njs.yun.139.com/hcy/file/create', + headers = self.build_cloud_file_headers(), data = payload, method = 'POST') + if not response or response.status_code != 200: + return None + try: + res_json = response.json() + except ValueError: + return None + if not res_json.get("success"): + return None + data = res_json.get("data", {}) + return { + "fileId": data.get("fileId"), + "fileName": data.get("fileName", file_name), + } + + def list_cloud_root_files(self): + items = [] + page_cursor = '' + while True: + response = self.request_json('https://personal-kd-njs.yun.139.com/hcy/file/list', + headers = self.build_cloud_file_headers(), + data = { + 'imageThumbnailStyleList': ['Small', 'Large'], + 'orderBy': 'updated_at', + 'orderDirection': 'DESC', + 'pageInfo': {'pageCursor': page_cursor, 'pageSize': 100}, + 'parentFileId': '/', + }, + method = 'POST') + if not response: + return items + if not response.get('success'): + self.log(f"获取云盘文件列表失败: {response.get('message', '未知错误')}") + return items + data = response.get('data', {}) + items.extend(data.get('items', [])) + page_cursor = data.get('nextPageCursor') or '' + if not page_cursor: + return items + + @staticmethod + def is_cleanup_upload_file(item): + if item.get('type') != 'file' or item.get('parentFileId') != '/': + return False + name = item.get('name', '') + if not (name.endswith('.txt') and (name.startswith('auto_upload_') or name.startswith('auto_share_'))): + if not (name.endswith('.jpg') and name.startswith('auto_mayday_')): + return False + size = item.get('size') + content_hash = item.get('contentHash') + return size in (0, 1, None) or content_hash == cloud_file_dummy_hash + + def trash_cloud_files(self, file_ids): + if not file_ids: + return True + response = self.request_json('https://personal-kd-njs.yun.139.com/hcy/recyclebin/batchTrash', + headers = self.build_cloud_file_headers(), + data = {'fileIds': file_ids}, + method = 'POST') + if not response: + self.log('清理上传文件失败: 接口无响应') + return False + if response.get('success'): + return True + self.log(f"清理上传文件失败: {response.get('message', '未知错误')}") + return False + + def cleanup_uploaded_files(self, current_file=None): + file_ids = [] + if current_file and current_file.get('fileId'): + file_ids.append(current_file['fileId']) + for item in self.list_cloud_root_files(): + if self.is_cleanup_upload_file(item): + file_ids.append(item.get('fileId')) + seen = set() + unique_file_ids = [] + for file_id in file_ids: + if not file_id or file_id in seen: + continue + seen.add(file_id) + unique_file_ids.append(file_id) + if not unique_file_ids: + return True + if self.trash_cloud_files(unique_file_ids): + self.log(f'-已清理上传文件: {len(unique_file_ids)}个') + return True + return False + + def complete_share_file_task(self, task): + share_file = self.create_cloud_file('auto_share_') + if not share_file: + self.log('分享文件失败: 创建临时文件失败') + return None + try: + response = self.request_json('https://yun.139.com/orchestration/personalCloud-rebuild/outlink/v1.0/getOutLink', + headers = self.build_share_headers(), + data = { + 'getOutLinkReq': { + 'subLinkType': 0, + 'encrypt': 1, + 'coIDLst': [share_file.get('fileId')], + 'caIDLst': [], + 'pubType': 1, + 'dedicatedName': share_file.get('fileName', ''), + 'period': 1, + 'periodUnit': 1, + 'viewerLst': [], + 'extInfo': {'isWatermark': 0, 'shareChannel': '3001'}, + 'commonAccountInfo': {'account': self.account, 'accountType': 1}, + } + }, + method = 'POST', retries = 1) + finally: + self.trash_cloud_files([share_file.get('fileId')]) + data = response.get('data', {}) if response else {} + result = data.get('result', {}) or data.get('getOutLinkRes', {}) + outlinks = result.get('getOutLinkResSet') or [] + success = False + if response: + success = ( + response.get('success') and result.get('resultCode') == '0' + ) or ( + str(response.get('code')) == '0' and bool(outlinks) + ) + if not success: + if response: + msg = result.get('resultDesc') or response.get('message') or response.get('msg', '未知错误') + else: + msg = '接口无响应' + self.log(f'分享文件失败: {msg}') + return None + return self.query_cloud_task(task.get('id', 434), 'month') or task + + def build_ai_headers(self, use_client_info=False): + """AI相机请求头,与主UA保持一致""" + device_id = self.market_device_id or generate_device_id() + headers = { + 'Connection': 'keep-alive', + 'sec-ch-ua-platform': '"iOS"', + 'Authorization': self.Authorization, + 'x-yun-api-version': 'v1', + 'x-yun-tid': str(uuid.uuid4()), + 'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="130", "Mobile Safari";v="130"', + 'sec-ch-ua-mobile': '?1', + 'X-Requested-With': 'com.chinamobile.mcloud', + 'Origin': 'https://frontend.mcloud.139.com', + 'Referer': 'https://frontend.mcloud.139.com/', + 'User-Agent': market_ua, + 'Content-Type': 'application/json', + 'Sec-Fetch-Site': 'same-site', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + 'Accept-Encoding': 'gzip, deflate, br, zstd', + 'Accept-Language': 'zh,zh-CN;q=0.9,en-US;q=0.8,en;q=0.7', + } + if use_client_info: + headers['Accept'] = 'text/event-stream' + headers['x-yun-client-info'] = f'4||1|{self.client_version}|Apple|iPhone 16 Pro|{device_id.lstrip("B")}|iOS 18.7|||||' + headers['x-yun-app-channel'] = '101' + return headers + headers['Accept'] = '*/*' + headers['x-DeviceInfo'] = f'||36|{self.client_version}|Apple|iPhone 16 Pro|{device_id.lstrip("B")}|iOS 18.7|||||' + return headers + + def encrypt_ai_tool_account(self): + if AES is None or pad is None: + raise ImportError('未安装 pycryptodome,无法加密AI工具账号') + cipher = AES.new(AI_TOOL_ACCOUNT_AES_KEY.encode('utf-8'), AES.MODE_CBC, AI_TOOL_ACCOUNT_AES_IV.encode('utf-8')) + encrypted = cipher.encrypt(pad(self.account.encode('utf-8'), AES.block_size)) + raw = AI_TOOL_ACCOUNT_AES_IV + base64.b64encode(encrypted).decode('utf-8') + return base64.b64encode(raw.encode('utf-8')).decode('utf-8') + + def build_ai_tool_ad_headers(self): + return { + 'Authorization': self.Authorization, + 'User-Agent': market_ua, + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'Origin': 'https://yun.139.com', + 'Referer': 'https://yun.139.com/aiTools/', + 'x-yun-tid': str(uuid.uuid4()), + } + + def get_ai_camera_sample_base64(self): + sample_path = path.join(path.abspath(path.dirname(__file__)), 'assets', 'ai_camera_sample.jpg') + if not path.exists(sample_path): + return '' + with open(sample_path, 'rb') as file: + return f"data:image/jpg;base64,{base64.b64encode(file.read()).decode()}" + + @staticmethod + def is_ai_chat_success(text): + payloads = [] + for line in (text or '').splitlines(): + if line.startswith('data:'): + payloads.append(line[5:].strip()) + if not payloads and text: + payloads.append(text.strip()) + for payload in payloads: + if not payload or payload == '[DONE]': + continue + try: + data = json.loads(payload) + except ValueError: + continue + if data.get('success') or data.get('code') == '0000': + return True + return False + + def complete_ai_camera_task(self): + if not self.user_domain_id: + self.log('AI相机任务失败: 缺少用户信息') + return False + image_data = self.get_ai_camera_sample_base64() + if not image_data: + self.log('AI相机任务失败: 缺少样图') + return False + recognize_payload = json.dumps({ + 'channelId': '101', + 'userId': self.user_domain_id, + 'recognizeType': '1', + 'base64': image_data, + 'sendType': '2', + 'imageExt': 'jpg', + 'uploadToCloud': True, + 'timeout': 30000, + }, ensure_ascii = False, separators = (',', ':')) + recognize_data = self.request_json('https://ai.yun.139.com/api/image/aiRecognize', + headers = self.build_ai_headers(), + data = recognize_payload, + method = 'POST') + if not recognize_data: + self.log('AI相机识图失败: 接口无响应') + return False + if not recognize_data.get('success'): + self.log(f"AI相机识图失败: {recognize_data.get('message', '未知错误')}") + return False + recognize_result = recognize_data.get('data') or {} + file_id = recognize_result.get('fileId') + if not file_id: + self.log('AI相机识图失败: 缺少文件ID') + return False + task_id = str(recognize_result.get('taskId') or int(time.time() * 1000)) + file_name = f'{int(task_id) + 1}.jpeg' if task_id.isdigit() else f'{task_id}.jpeg' + input_time = datetime.now(timezone(timedelta(hours = 8))).isoformat(timespec = 'milliseconds') + chat_payload = json.dumps({ + 'userId': self.user_domain_id, + 'sessionId': '', + 'applicationType': 'chat', + 'applicationId': '', + 'sourceChannel': '101', + 'dialogueInput': { + 'dialogue': '?', + 'prompt': '', + 'inputTime': input_time, + 'enableForceLlm': False, + 'enableForceNetworkSearch': True, + 'enableModelThinking': False, + 'enableAllNetworkSearch': False, + 'enableKnowledgeAndNetworkSearch': False, + 'enableRegenerate': False, + 'versionInfo': {'h5Version': '2.7.6'}, + 'extInfo': '{}', + 'sortInfo': {}, + 'toolSetting': {'imageToolSetting': {'enableLlmDescribe': True}}, + 'attachment': { + 'attachmentTypeList': [3], + 'fileList': [{'fileId': file_id, 'name': file_name}], + }, + }, + }, ensure_ascii = False, separators = (',', ':')) + chat_response = self.send_request('https://ai.yun.139.com/api/outer/assistant/chat/v2/add', + headers = self.build_ai_headers(use_client_info = True), + data = chat_payload, + method = 'POST') + if not chat_response: + self.log('AI相机对话失败: 接口无响应') + return False + if self.is_ai_chat_success(chat_response.text): + return True + try: + chat_data = chat_response.json() + except ValueError: + chat_data = None + if chat_data and (chat_data.get('success') or chat_data.get('code') == '0000'): + return True + if chat_data: + self.log(f"AI相机对话失败: {chat_data.get('message') or chat_data.get('msg', '未知错误')}") + return False + self.log('AI相机对话失败: 响应解析失败') + return False + + def complete_travel_guide_ai_task(self): + if not self.user_domain_id: + self.log('出行攻略问AI失败: 缺少用户信息') + return False + input_time = datetime.now(timezone(timedelta(hours = 8))).isoformat(timespec = 'milliseconds') + chat_payload = json.dumps({ + 'userId': self.user_domain_id, + 'sessionId': '', + 'applicationType': 'chat', + 'applicationId': '', + 'sourceChannel': '101', + 'dialogueInput': { + 'dialogue': '给我一份五一出行攻略', + 'prompt': '', + 'inputTime': input_time, + 'enableForceLlm': False, + 'enableForceNetworkSearch': True, + 'enableModelThinking': False, + 'enableAllNetworkSearch': False, + 'enableKnowledgeAndNetworkSearch': False, + 'enableRegenerate': False, + 'versionInfo': {'h5Version': '2.7.6'}, + 'extInfo': '{}', + 'sortInfo': {}, + 'toolSetting': {'imageToolSetting': {'enableLlmDescribe': True}}, + 'attachment': {}, + }, + }, ensure_ascii = False, separators = (',', ':')) + chat_response = self.send_request('https://ai.yun.139.com/api/outer/assistant/chat/v2/add', + headers = self.build_ai_headers(use_client_info = True), + data = chat_payload, + method = 'POST') + if not chat_response: + self.log('出行攻略问AI失败: 接口无响应') + return False + response_text = chat_response.text or '' + if chat_response.status_code == 200 and not response_text.strip(): + return True + if self.is_ai_chat_success(response_text): + return True + try: + chat_data = chat_response.json() + except ValueError: + chat_data = None + if chat_data and (chat_data.get('success') or chat_data.get('code') == '0000'): + return True + if chat_data: + self.log(f"出行攻略问AI失败: {chat_data.get('message') or chat_data.get('msg', '未知错误')}") + return False + self.log('出行攻略问AI失败: 响应解析失败') + return False + + @staticmethod + def get_task_progress(task): + progress_parts = [] + currstep = task.get('currstep', 0) + process = task.get('process', 0) + if currstep: + progress_parts.append(f'阶段{currstep}') + if process: + progress_parts.append(f'进度{process}') + if not progress_parts: + return '' + return f" ({','.join(progress_parts)})" + + @staticmethod + def strip_task_name(task): + return re.sub(r'<[^>]+>', '', task.get('name', '')) + + @staticmethod + def get_task_step_types(task): + return set(task.get('stepTypeSet') or []) + + def get_task_click_keys(self, task): + task_id = task.get('id') + currstep = task.get('currstep', 0) + step_types = self.get_task_step_types(task) + if task_id == 409: + if currstep > 0: + return ['task2'] + return ['task', 'task2'] + if 'click' in step_types and currstep == 0: + return ['task'] + return [] + + def get_cloud_task_groups(self): + return [ + ('beiyong1', '\n🎁 五一福利任务'), + ('cloudEmail', '\n📮 联动任务'), + ('time', '\n✨ 新版热门任务'), + ('day', '\n📆 云盘每日任务'), + ('month', '\n📆 云盘每月任务'), + ] + + def query_cloud_task(self, task_id, group='time'): + return_data = self.request_market_json(f'{self.market_base_url}/market/signin/task/taskListV2', params = { + 'marketname': 'sign_in_3', + 'clientVersion': self.client_version, + 'group': group, + }) + if not return_data or return_data.get('code') != 0: + return None + for task in return_data.get('result', {}).get(group, []): + if task.get('id') == task_id: + return task + return None + + def complete_monthly_upload_task(self, task): + target_count = 100 + current_process = int(task.get('process') or 0) + for attempt in range(3): + remaining = max(0, target_count - current_process) + if remaining == 0: + return True + self.log(f'-{"开始" if attempt == 0 else "继续"}补上传进度: 当前{current_process}/{target_count},还需{remaining}次') + success = 0 + for _ in range(remaining): + if self.create_cloud_file('auto_upload_'): + success += 1 + if success: + self.log(f'-批量上传完成: {success}次') + refreshed_task = self.query_cloud_task(task.get('id', 522), 'time') + if not refreshed_task: + return False + refreshed_process = int(refreshed_task.get('process') or 0) + if refreshed_task.get('state') == 'FINISH' or refreshed_process >= target_count: + return True + if refreshed_process <= current_process: + self.log(f'-月上传任务进度: {refreshed_process}/{target_count}') + return False + current_process = refreshed_process + self.log(f'-月上传任务进度: {current_process}/{target_count}') + return False + + def complete_mayday_memory_task(self, task): + target_count = 10 + task_id = task.get('id', 587) + current_process = int(task.get('process') or 0) + remaining = max(0, target_count - current_process) + if remaining == 0: + return True + self.log(f'-开始补五一回忆上传: 当前{current_process}/{target_count},还需{remaining}张') + success = 0 + for _ in range(remaining): + if self.create_cloud_file('auto_mayday_', extension = 'jpg'): + success += 1 + if success: + self.log(f'-五一回忆上传完成: {success}张') + self.click_task(task_id) + refreshed_task = self.query_cloud_task(task_id, 'beiyong1') + if not refreshed_task: + return False + refreshed_process = int(refreshed_task.get('process') or 0) + if refreshed_task.get('state') == 'FINISH' or refreshed_process >= target_count: + return True + self.log(f'-五一回忆任务进度: {refreshed_process}/{target_count}') + return False + + def complete_holiday_nine_grid_task(self): + try: + encrypted_account = self.encrypt_ai_tool_account() + except ImportError: + self.log('生成假期九宫格失败: 缺少加密依赖') + return False + template_data = self.request_json( + 'https://ad.mcloud.139.com/advertapi/adv-config/adv-config/AdInfoFilter/getAdInfos', + headers = self.build_ai_tool_ad_headers(), + data = { + 'account': encrypted_account, + 'adpostid': '66340', + 'channel': '10000023', + 'version': '10.5.0', + 'client': 'iphone', + }, + method = 'POST', + ) + if not template_data: + self.log('生成假期九宫格失败: 模板接口无响应') + return False + if str(template_data.get('returnCode')) != '0': + self.log(f"生成假期九宫格失败: {template_data.get('returnMsg', '未知错误')}") + return False + click_data = self.click_task(589) + if click_data and str(click_data.get('code')) == '0': + return True + msg = click_data.get('msg', '未知错误') if click_data else '接口无响应' + self.log(f'生成假期九宫格失败: {msg}') + return False + + def get_cloud_tasklist_v2(self): + for group, title in self.get_cloud_task_groups(): + return_data = self.request_market_json(f'{self.market_base_url}/market/signin/task/taskListV2', params = { + 'marketname': 'sign_in_3', + 'clientVersion': self.client_version, + 'group': group, + }) + if not return_data: + self.log(f'获取任务列表失败: {group}') + continue + if return_data.get('code') != 0: + self.log(f"获取任务列表失败: {group} {return_data.get('msg', '未知错误')}") + continue + tasks = return_data.get('result', {}).get(group, []) + if not tasks: + continue + self.log(title) + for task in tasks: + self.handle_cloud_v2_task(group, task) + self.cleanup_uploaded_files() + + def handle_cloud_v2_task(self, group, task): + task_id = task.get('id') + task_name = self.strip_task_name(task) + task_status = task.get('state', '') + if task_status == 'FINISH': + print(f'-已完成: {task_name}') + return + if group == 'day' and task_id == 106: + self.log(f'-去完成: {task_name}') + self.do_task(task_id, task_type = 'day', app_type = 'cloud_app') + return + if task_id == 522: + self.log(f'-去完成: {task_name}') + if self.complete_monthly_upload_task(task): + self.log(f'-已完成: {task_name}') + return + refreshed_task = self.query_cloud_task(task_id, group) or task + self.log(f'-需手动完成: {task_name}{self.get_task_progress(refreshed_task)}') + return + if task_id == 434: + self.log(f'-去完成: {task_name}') + refreshed_task = self.complete_share_file_task(task) + if refreshed_task: + refreshed_name = self.strip_task_name(refreshed_task) + if refreshed_task.get('state') == 'FINISH': + self.log(f'-已完成: {refreshed_name}') + return + self.log(f'-分享成功: {refreshed_name}{self.get_task_progress(refreshed_task)}') + return + self.log(f'-需手动完成: {task_name}{self.get_task_progress(task)}') + return + if task_id == 585: + self.log(f'-去完成: {task_name}') + if 'click' in self.get_task_step_types(task) and int(task.get('currstep') or 0) == 0: + click_data = self.click_task(task_id) + if not click_data or click_data.get('code') != 0: + msg = click_data.get('msg', '未知错误') if click_data else '接口无响应' + self.log(f'-任务登记失败: {task_name} {msg}') + return + if self.complete_ai_camera_task(): + refreshed_task = self.query_cloud_task(task_id, group) or task + refreshed_name = self.strip_task_name(refreshed_task) + if refreshed_task.get('state') == 'FINISH': + self.log(f'-已完成: {refreshed_name}') + else: + self.log(f'-AI相机已体验: {refreshed_name}{self.get_task_progress(refreshed_task)}') + return + refreshed_task = self.query_cloud_task(task_id, group) or task + self.log(f'-需手动完成: {task_name}{self.get_task_progress(refreshed_task)}') + return + if task_id == 587: + self.log(f'-去完成: {task_name}') + if 'click' in self.get_task_step_types(task) and int(task.get('currstep') or 0) == 0: + click_data = self.click_task(task_id) + if not click_data or click_data.get('code') != 0: + msg = click_data.get('msg', '未知错误') if click_data else '接口无响应' + self.log(f'-任务登记失败: {task_name} {msg}') + return + if self.complete_mayday_memory_task(task): + refreshed_task = self.query_cloud_task(task_id, group) or task + refreshed_name = self.strip_task_name(refreshed_task) + if refreshed_task.get('state') == 'FINISH': + self.log(f'-已完成: {refreshed_name}') + else: + self.log(f'-五一回忆已上传: {refreshed_name}{self.get_task_progress(refreshed_task)}') + return + refreshed_task = self.query_cloud_task(task_id, group) or task + self.log(f'-需手动完成: {task_name}{self.get_task_progress(refreshed_task)}') + return + if task_id == 588: + self.log(f'-去完成: {task_name}') + if 'click' in self.get_task_step_types(task) and int(task.get('currstep') or 0) == 0: + click_data = self.click_task(task_id) + if not click_data or click_data.get('code') != 0: + msg = click_data.get('msg', '未知错误') if click_data else '接口无响应' + self.log(f'-任务登记失败: {task_name} {msg}') + return + if self.complete_travel_guide_ai_task(): + refreshed_task = self.query_cloud_task(task_id, group) or task + refreshed_name = self.strip_task_name(refreshed_task) + if refreshed_task.get('state') == 'FINISH': + self.log(f'-已完成: {refreshed_name}') + else: + self.log(f'-出行攻略已生成: {refreshed_name}{self.get_task_progress(refreshed_task)}') + return + refreshed_task = self.query_cloud_task(task_id, group) or task + self.log(f'-需手动完成: {task_name}{self.get_task_progress(refreshed_task)}') + return + if task_id == 589: + self.log(f'-去完成: {task_name}') + if 'click' in self.get_task_step_types(task) and int(task.get('currstep') or 0) == 0: + click_data = self.click_task(task_id) + if not click_data or click_data.get('code') != 0: + msg = click_data.get('msg', '未知错误') if click_data else '接口无响应' + self.log(f'-任务登记失败: {task_name} {msg}') + return + if self.complete_holiday_nine_grid_task(): + refreshed_task = self.query_cloud_task(task_id, group) or task + refreshed_name = self.strip_task_name(refreshed_task) + if refreshed_task.get('state') == 'FINISH': + self.log(f'-已完成: {refreshed_name}') + else: + self.log(f'-假期九宫格已生成: {refreshed_name}{self.get_task_progress(refreshed_task)}') + return + refreshed_task = self.query_cloud_task(task_id, group) or task + self.log(f'-需手动完成: {task_name}{self.get_task_progress(refreshed_task)}') + return + if group == 'beiyong1': + self.log(f'-需手动完成: {task_name}{self.get_task_progress(task)}') + return + if task_id == 406: + self.complete_notice_task(task_name) + return + task_keys = self.get_task_click_keys(task) + if task_keys: + self.log(f'-去完成: {task_name}') + for task_key in task_keys: + click_data = self.click_task(task_id, task_key) + if click_data and click_data.get('code') == 0: + continue + msg = click_data.get('msg', '未知错误') if click_data else '接口无响应' + self.log(f'-任务登记失败: {task_name} {msg}') + return + self.log(f'-已登记任务: {task_name}') + return + self.log(f'-需手动完成: {task_name}{self.get_task_progress(task)}') + + def sleep(self, min_delay=1, max_delay=1.5): delay = random.uniform(min_delay, max_delay) time.sleep(delay) - # 日志 - def log_info(self, err_msg=None, amount=None): - global err_message, user_amount - if err_msg is not None: - err_message += f'用户[{self.encrypt_account}]:{err_msg}\n' # 错误信息 - elif amount is not None: - user_amount += f'用户[{self.encrypt_account}]:{amount}\n' # 云朵数量 - # 刷新令牌 def sso(self): sso_url = 'https://orches.yun.139.com/orchestration/auth-rebuild/token/v1.0/querySpecToken' sso_headers = { @@ -210,79 +1470,104 @@ class YP: 'Host': 'orches.yun.139.com' } sso_payload = {"account": self.account, "toSourceId": "001005"} - sso_data = self.send_request(sso_url, headers = sso_headers, data = sso_payload, method = 'POST').json() - + sso_data = self.request_json(sso_url, headers = sso_headers, data = sso_payload, method = 'POST') + if not sso_data: + self.log('刷新Token失败: 接口无响应') + return None if sso_data['success']: refresh_token = sso_data['data']['token'] + self.sso_token = refresh_token return refresh_token else: - print(sso_data['message']) + self.log(f"刷新Token失败: {sso_data.get('message', '未知错误')}") return None - # jwt def jwt(self): - # 获取jwttoken token = self.sso() + if token is None: + self.log('-尝试强制刷新Authorization后重试') + self.refresh_authorization_token(force = True) + token = self.sso() if token is not None: - jwt_url = f"https://caiyun.feixin.10086.cn:7071/portal/auth/tyrzLogin.action?ssoToken={token}" - jwt_data = self.send_request(jwt_url, headers = self.jwtHeaders, method = 'POST').json() - if jwt_data['code'] != 0: - print(jwt_data['msg']) + jwt_data = self.request_json(jwt_url, headers = self.jwtHeaders, method = 'POST') + if not jwt_data: + self.log('JWT获取失败: 接口无响应') return False - self.jwtHeaders['jwtToken'] = jwt_data['result']['token'] - self.cookies['jwtToken'] = jwt_data['result']['token'] + if jwt_data['code'] != 0: + self.log('-尝试强制刷新Authorization后重新获取JWT') + self.refresh_authorization_token(force = True) + token = self.sso() + if token is None: + self.log('-ck可能失效了') + return False + jwt_url = f"https://caiyun.feixin.10086.cn:7071/portal/auth/tyrzLogin.action?ssoToken={token}" + jwt_data = self.request_json(jwt_url, headers = self.jwtHeaders, method = 'POST') + if not jwt_data: + self.log('JWT获取失败: 接口无响应') + return False + if jwt_data['code'] != 0: + self.log(f"JWT获取失败: {jwt_data['msg']}") + return False + jwt_token = jwt_data['result']['token'] + self.jwtHeaders['jwtToken'] = jwt_token + self.cookies['jwtToken'] = jwt_token + self.build_market_context(jwt_token) + self.save_authorization_record(token = self.Authorization, user_domain_id = self.user_domain_id) return True else: - print('-ck可能失效了') + self.log('-ck可能失效了') return False - # 签到查询 @catch_errors def signin_status(self): self.sleep() - check_url = 'https://caiyun.feixin.10086.cn/market/signin/page/info?client=app' - check_data = self.send_request(check_url, headers = self.jwtHeaders, cookies = self.cookies).json() - if check_data['msg'] == 'success': - today_sign_in = check_data['result'].get('todaySignIn', False) + self.prepare_signin_center_session() + check_url = f'{self.market_base_url}/market/signin/page/infoV3' + check_data = self.request_market_json(check_url, params = {'client': 'app'}) + if not check_data: + self.log('查询签到失败: 接口无响应') + return + if check_data.get('code') != 0: + self.log(f"查询签到失败: {check_data.get('msg', '未知错误')}") + return + if self.get_today_sign_state(check_data.get('result', {})): + self.log('✅已签到') + return + signin_data = self.request_market_json(f'{self.market_base_url}/market/signin/page/startSignIn', + params = {'client': 'app'}) + if not signin_data: + self.log('签到失败: 接口无响应') + return + if signin_data.get('code') == 0 and self.get_today_sign_state(signin_data.get('result', {})): + self.log('✅签到成功') + return + latest_data = self.request_market_json(check_url, params = {'client': 'app'}) + if latest_data and latest_data.get('code') == 0 and self.get_today_sign_state(latest_data.get('result', {})): + self.log('✅签到成功') + return + self.log(f"签到失败: {signin_data.get('msg', '未知错误')}") + if signin_data.get('code') in (614, 615) and not get_env_device_id() and not get_device_id(self.account): + self.log('-请检查变量中 deviceId 是否正确') - if today_sign_in: - print('✅已签到') - else: - print('❌ 未签到') - signin_url = 'https://caiyun.feixin.10086.cn/market/manager/commonMarketconfig/getByMarketRuleName?marketName=sign_in_3' - signin_data = self.send_request(signin_url, headers = self.jwtHeaders, - cookies = self.cookies).json() - - if signin_data['msg'] == 'success': - print('✅签到成功') - else: - print(signin_data['msg']) - self.log_info(signin_data['msg']) - else: - print(check_data['msg']) - self.log_info(check_data['msg']) - - # 戳一下 def click(self): - url = "https://caiyun.feixin.10086.cn/market/signin/task/click?key=task&id=319" - successful_click = 0 # 获得次数 + successful_click = 0 try: for _ in range(self.click_num): - return_data = self.send_request(url, headers = self.jwtHeaders, cookies = self.cookies).json() + return_data = self.click_task(319) or {} time.sleep(0.2) if 'result' in return_data: - print(f'✅{return_data["result"]}') + self.log(f'✅戳一戳: {return_data["result"]}') successful_click += 1 if successful_click == 0: - print(f'❌未获得 x {self.click_num}') + print(f'❌戳一戳: 未获得 x {self.click_num}') except Exception as e: print(f'错误信息:{e}') - # 刷新笔记token + @catch_errors def refresh_notetoken(self): note_url = 'http://mnote.caiyun.feixin.10086.cn/noteServer/api/authTokenRefresh.do' @@ -295,7 +1580,7 @@ class YP: 'Charset': 'UTF-8', 'Connection': 'Keep-Alive', 'User-Agent': 'mobile', - 'APP_CP': 'android', + 'APP_CP': 'ios', 'CP_VERSION': '3.2.0', 'x-huawei-channelsrc': '10001400', 'Host': 'mnote.caiyun.feixin.10086.cn', @@ -313,12 +1598,14 @@ class YP: self.note_token = response.headers.get('NOTE_TOKEN') self.note_auth = response.headers.get('APP_AUTH') - # 任务列表 + def get_tasklist(self, url, app_type): + if url == 'sign_in_3' and app_type == 'cloud_app': + self.get_cloud_tasklist_v2() + return url = f'https://caiyun.feixin.10086.cn/market/signin/task/taskList?marketname={url}' return_data = self.send_request(url, headers = self.jwtHeaders, cookies = self.cookies).json() self.sleep() - # 任务列表 task_list = return_data.get('result', {}) try: @@ -327,40 +1614,40 @@ class YP: continue if app_type == 'cloud_app': if task_type == "month": - print('\n📆 云盘每月任务') + self.log('\n📆 云盘每月任务') for month in tasks: task_id = month.get('id') if task_id in [110, 113, 417, 409]: continue - task_name = month.get('name', '') + task_name = re.sub(r'<[^>]+>', '', month.get('name', '')) task_status = month.get('state', '') if task_status == 'FINISH': print(f'-已完成: {task_name}') continue - print(f'-去完成: {task_name}') + self.log(f'-去完成: {task_name}') self.do_task(task_id, task_type = 'month', app_type = 'cloud_app') time.sleep(2) elif task_type == "day": - print('\n📆 云盘每日任务') + self.log('\n📆 云盘每日任务') for day in tasks: task_id = day.get('id') if task_id == 404: continue - task_name = day.get('name') + task_name = re.sub(r'<[^>]+>', '', day.get('name', '')) task_status = day.get('state', '') if task_status == 'FINISH': print(f'-已完成: {task_name}') continue - print(f'-去完成: {task_name}') + self.log(f'-去完成: {task_name}') self.do_task(task_id, task_type = 'day', app_type = 'cloud_app') elif app_type == 'email_app': if task_type == "month": - print('\n📆 139邮箱每月任务') + self.log('\n📆 139邮箱每月任务') for month in tasks: task_id = month.get('id') - task_name = month.get('name', '') + task_name = re.sub(r'<[^>]+>', '', month.get('name', '')) task_status = month.get('state', '') if task_id in [1004, 1005, 1015, 1020]: continue @@ -368,23 +1655,26 @@ class YP: if task_status == 'FINISH': print(f'-已完成: {task_name}') continue - print(f'-去完成: {task_name}') + self.log(f'-去完成: {task_name}') self.do_task(task_id, task_type = 'month', app_type = 'email_app') time.sleep(2) except Exception as e: - print(f'错误信息:{e}') + self.log(f'获取任务列表错误:{e}') + - # 做任务 @catch_errors def do_task(self, task_id, task_type, app_type): self.sleep() - task_url = f'https://caiyun.feixin.10086.cn/market/signin/task/click?key=task&id={task_id}' - self.send_request(task_url, headers = self.jwtHeaders, cookies = self.cookies) + if app_type == 'cloud_app': + self.click_task(task_id) + else: + task_url = f'https://caiyun.feixin.10086.cn/market/signin/task/click?key=task&id={task_id}' + self.send_request(task_url, headers = self.jwtHeaders, cookies = self.cookies) if app_type == 'cloud_app': if task_type == 'day': if task_id == 106: - print('-开始上传文件,默认0kb') + self.log('-开始上传文件,默认0kb') self.updata_file() elif task_id == 107: self.refresh_notetoken() @@ -395,7 +1685,7 @@ class YP: 'Charset': 'UTF-8', 'Connection': 'Keep-Alive', 'User-Agent': 'mobile', - 'APP_CP': 'android', + 'APP_CP': 'ios', 'CP_VERSION': '3.2.0', 'x-huawei-channelsrc': '10001400', 'APP_NUMBER': self.account, @@ -418,73 +1708,21 @@ class YP: self.notebook_id = return_data['notebooks'][0]['notebookId'] print('开始创建笔记') self.create_note(headers) - elif task_type == 'month': - pass - elif app_type == 'email_app': - if task_type == 'month': - pass - # 上传文件 + + @catch_errors def updata_file(self): - url = 'http://ose.caiyun.feixin.10086.cn/richlifeApp/devapp/IUploadAndDownload' - headers = { - 'x-huawei-uploadSrc': '1', - 'x-ClientOprType': '11', - 'Connection': 'keep-alive', - 'x-NetType': '6', - 'x-DeviceInfo': '6|127.0.0.1|1|10.0.1|Xiaomi|M2012K10C|CB63218727431865A48E691BFFDB49A1|02-00-00-00-00-00|android 11|1080X2272|zh||||032|', - 'x-huawei-channelSrc': '10000023', - 'x-MM-Source': '032', - 'x-SvcType': '1', - 'APP_NUMBER': self.account, - 'Authorization': self.Authorization, - 'X-Tingyun-Id': 'p35OnrDoP8k;c=2;r=1955442920;u=43ee994e8c3a6057970124db00b2442c::8B3D3F05462B6E4C', - 'Host': 'ose.caiyun.feixin.10086.cn', - 'User-Agent': 'okhttp/3.11.0', - 'Content-Type': 'application/xml; charset=UTF-8', - 'Accept': '*/*' - } - payload = ''' - - {phone} - 1 - 1 - - - 0 - - - 1 - - C4CA4238A0B923820DCC509A6F75849B - - 0 - 0 - - - - - - 0 - - 2 - - - - - '''.format(phone = self.account) - - response = requests.post(url = url, headers = headers, data = payload) - if response is None: + upload_info = self.create_cloud_file('auto_upload_') + if not upload_info: + self.log('-上传失败: 接口无响应') return - if response.status_code != 200: - return print('-上传失败') - print('-上传文件成功') + self.log(f"-上传文件成功,文件名: {upload_info.get('fileName', '')}") + self.cleanup_uploaded_files(upload_info) + - # 创建笔记 def create_note(self, headers): - note_id = self.get_note_id(32) # 获取随机笔记id + note_id = self.get_note_id(32) createtime = str(int(round(time.time() * 1000))) time.sleep(3) updatetime = str(int(round(time.time() * 1000))) @@ -509,14 +1747,14 @@ class YP: }], "cp": "", "createtime": createtime, - "description": "android", + "description": "ios", "expands": { "noteType": 0 }, "latlng": "", "location": "", "noteid": note_id, - 'notestatus': 0, + "notestatus": 0, "remindtime": "", "remindtype": 1, "revision": "1", @@ -537,17 +1775,17 @@ class YP: } create_note_data = self.send_request(note_url, headers = headers, data = payload, method = "POST") if create_note_data.status_code == 200: - print('-创建笔记成功') + self.log('-创建笔记成功') else: - print('-创建失败') + self.log('-创建失败') + - # 笔记id def get_note_id(self, length): characters = '19f3a063d67e4694ca63a4227ec9a94a19088404f9a28084e3e486b928039a299bf756ebc77aa4f6bfa250308ec6a8be8b63b5271a00350d136d117b8a72f39c5bd15cdfd350cba4271dc797f15412d9f269e666aea5039f5049d00739b320bb9e8585a008b52c1cbd86970cae9476446f3e41871de8d9f6112db94b05e5dc7ea0a942a9daf145ac8e487d3d5cba7cea145680efc64794d43dd15c5062b81e1cda7bf278b9bc4e1b8955846e6bc4b6a61c28f831f81b2270289e5a8a677c3141ddc9868129060c0c3b5ef507fbd46c004f6de346332ef7f05c0094215eae1217ee7c13c8dca6d174cfb49c716dd42903bb4b02d823b5f1ff93c3f88768251b56cc' note_id = ''.join(random.choice(characters) for _ in range(length)) return note_id - # 公众号签到 + @catch_errors def wxsign(self): self.sleep() @@ -555,15 +1793,14 @@ class YP: return_data = self.send_request(url, headers = self.jwtHeaders, cookies = self.cookies).json() if return_data['msg'] != 'success': - return print(return_data['msg']) + return self.log(return_data['msg']) if not return_data['result'].get('todaySignIn'): - return print('❌签到失败,可能未绑定公众号') - return print('✅签到成功') + return self.log('❌签到失败,可能未绑定公众号') + return self.log('✅公众号签到成功') - # 摇一摇 def shake(self): url = "https://caiyun.feixin.10086.cn:7071/market/shake-server/shake/shakeIt?flag=1" - successful_shakes = 0 # 记录成功摇中的次数 + successful_shakes = 0 try: for _ in range(self.click_num): @@ -573,14 +1810,14 @@ class YP: shake_prize_config = return_data["result"].get("shakePrizeconfig") if shake_prize_config: - print(f"🎉摇一摇获得: {shake_prize_config['name']}") + self.log(f"🎉摇一摇获得: {shake_prize_config['name']}") successful_shakes += 1 except Exception as e: print(f'错误信息: {e}') if successful_shakes == 0: print(f'❌未摇中 x {self.click_num}') - # 查询剩余抽奖次数 + @catch_errors def surplus_num(self): self.sleep() @@ -591,7 +1828,7 @@ class YP: if draw_info_data.get('msg') == 'success': remain_num = draw_info_data['result'].get('surplusNumber', 0) - print(f'剩余抽奖次数{remain_num}') + self.log(f'剩余抽奖次数{remain_num}') if remain_num > 50 - self.draw: for _ in range(self.draw): self.sleep() @@ -599,100 +1836,17 @@ class YP: if draw_data.get("code") == 0: prize_name = draw_data["result"].get("prizeName", "") - print("✅抽奖成功,获得:" + prize_name) + self.log("✅抽奖成功,获得:" + prize_name) else: print("❌抽奖失败") - else: - pass + else: - print(draw_info_data.get('msg')) - self.log_info(draw_info_data.get('msg')) + self.log(f"抽奖查询失败: {draw_info_data.get('msg')}") - # 果园专区 - @catch_errors - def fruitLogin(self): - token = self.sso() - if token is not None: - print("-果园专区token刷新成功") - self.sleep() - login_info_url = f'{self.fruit_url}login/caiyunsso.do?token={token}&account={self.account}&targetSourceId=001208&sourceid=1003&enableShare=1' - headers = { - 'Host': 'happy.mail.10086.cn', - 'Upgrade-Insecure-Requests': '1', - 'User-Agent': ua, - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', - 'Referer': 'https://caiyun.feixin.10086.cn:7071/', - 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7' - } - loginInfoData = requests.request("GET", login_info_url, headers = headers) - treeCookie = loginInfoData.request.headers['Cookie'] - self.treeHeaders['cookie'] = treeCookie - do_login_url = f'{self.fruit_url}login/userinfo.do' - doLoginData = self.send_request(do_login_url, headers = self.treeHeaders).json() - if doLoginData.get('result', {}).get('islogin') != 1: - return print('❌果园登录失败') - # 去做果园任务 - self.fruitTask() - else: - print("果园专区token刷新失败") - - # 任务查询 - @catch_errors - def fruitTask(self): - # 签到任务 - check_sign_data = self.send_request(f'{self.fruit_url}task/checkinInfo.do', - headers = self.treeHeaders).json() - if check_sign_data.get('success'): - today_checkin = check_sign_data.get('result', {}).get('todayCheckin', 0) - if today_checkin == 1: - print('-果园今日已签到') - else: - checkin_data = self.send_request(f'{self.fruit_url}task/checkin.do', - headers = self.treeHeaders).json() - if checkin_data.get('result', {}).get('code', '') == 1: - print('-果园签到成功') - self.sleep() - water_data = self.send_request(f'{self.fruit_url}user/clickCartoon.do?cartoonType=widget', - headers = self.treeHeaders).json() - color_data = self.send_request(f'{self.fruit_url}user/clickCartoon.do?cartoonType=color', - headers = self.treeHeaders).json() - given_water = water_data.get('result', {}).get('given', 0) - print(f'-领取每日水滴: {given_water}') - print(f'-每日雨滴:{color_data.get("result").get("msg")}') - else: - print('-果园签到查询失败:', check_sign_data.get('msg', '')) - - # 获取任务列表 - task_list_data = self.send_request(f'{self.fruit_url}task/taskList.do?clientType=PE', - headers = self.treeHeaders).json() - task_state_data = self.send_request(f'{self.fruit_url}task/taskState.do', headers = self.treeHeaders).json() - task_state_result = task_state_data.get('result', []) - - task_list = task_list_data.get('result', []) - - for task in task_list: - task_id = task.get('taskId', '') - task_name = task.get('taskName', '') - water_num = task.get('waterNum', 0) - if task_id == 2002 or task_id == 2003: - continue - - task_state = next( - (state.get('taskState', 0) for state in task_state_result if state.get('taskId') == task_id), 0) - - if task_state == 2: - print(f'-已完成: {task_name}') - else: - self.do_fruit_task(task_name, task_id, water_num) - - # 果树信息 - self.tree_info() - - # 做任务 @catch_errors def do_fruit_task(self, task_name, task_id, water_num): - print(f'-去完成: {task_name}') + self.log(f'-去完成: {task_name}') do_task_url = f'{self.fruit_url}task/doTask.do?taskId={task_id}' do_task_data = self.send_request(do_task_url, headers = self.treeHeaders).json() @@ -701,43 +1855,13 @@ class YP: get_water_data = self.send_request(get_water_url, headers = self.treeHeaders).json() if get_water_data.get('success'): - print(f'-已完成任务获得水滴: {water_num}') + self.log(f'-已完成任务获得水滴: {water_num}') else: - print(f'❌领取失败: {get_water_data.get("msg", "")}') + self.log(f'❌领取失败: {get_water_data.get("msg", "")}') else: - print(f'❌参与任务失败: {do_task_data.get("msg", "")}') + self.log(f'❌参与任务失败: {do_task_data.get("msg", "")}') - # 果树信息 - @catch_errors - def tree_info(self): - treeinfo_url = f'{self.fruit_url}user/treeInfo.do' - treeinfo_data = self.send_request(treeinfo_url, headers = self.treeHeaders).json() - if not treeinfo_data.get('success'): - error_message = treeinfo_data.get('msg', '获取果园任务列表失败') - print(error_message) - else: - collect_water = treeinfo_data.get('result', {}).get('collectWater', 0) - tree_level = treeinfo_data.get('result', {}).get('treeLevel', 0) - print(f'-当前小树等级: {tree_level} 剩余水滴: {collect_water}') - if tree_level in (2, 4, 6, 8): - # 开宝箱 - openbox_url = f'{self.fruit_url}prize/openBox.do' - openbox_data = self.send_request(openbox_url, headers = self.treeHeaders).json() - print(f'- {openbox_data.get("result").get("msg")}') - - watering_amount = collect_water // 20 # 计算需要浇水的次数 - watering_url = f'{self.fruit_url}user/watering.do?isFast=0' - if watering_amount > 0: - for _ in range(watering_amount): - watering_data = self.send_request(watering_url, headers = self.treeHeaders).json() - if watering_data.get('success'): - print('✔️ 浇水成功') - time.sleep(3) - else: - print('-水滴不足!') - - # 云朵大作战 @catch_errors def cloud_game(self): game_info_url = 'https://caiyun.feixin.10086.cn/market/signin/hecheng1T/info?op=info' @@ -750,7 +1874,7 @@ class YP: count = game_info_data.get('result', {}).get('history', {}).get('0', {}).get('count', '') rank = game_info_data.get('result', {}).get('history', {}).get('0', {}).get('rank', '') - print(f'今日剩余游戏次数: {currnum}\n本月排名: {rank} 合成次数: {count}') + self.log(f'今日剩余游戏次数: {currnum} 合成次数: {count}') for _ in range(currnum): self.send_request(bigin_url, headers = self.jwtHeaders, cookies = self.cookies).json() @@ -758,153 +1882,196 @@ class YP: time.sleep(random.randint(10, 15)) end_data = self.send_request(end_url, headers = self.jwtHeaders, cookies = self.cookies).json() if end_data and end_data.get('code', -1) == 0: - print('游戏成功') + self.log('游戏成功') else: print("-获取游戏信息失败") - # 领取云朵 @catch_errors def receive(self): - receive_url = "https://caiyun.feixin.10086.cn/market/signin/page/receive" prize_url = f"https://caiyun.feixin.10086.cn/market/prizeApi/checkPrize/getUserPrizeLogPage?currPage=1&pageSize=15&_={self.timestamp}" - receive_data = self.send_request(receive_url, headers = self.jwtHeaders, cookies = self.cookies).json() + self.prepare_signin_center_session(for_receive = True) + info_data = self.request_market_json(f'{self.market_base_url}/market/signin/page/infoV3', + params = {'client': 'app'}) + if not info_data: + self.log('查询云朵失败: 接口无响应') + return + if info_data.get('code') != 0: + self.log(f"查询云朵失败: {info_data.get('msg', '未知错误')}") + return + info_result = info_data.get('result', {}) + pending_amount = info_result.get('toReceive', 0) + total_amount = info_result.get('total', '') + if pending_amount: + receive_headers = self.build_receive_headers() + receive_cookies = dict(self.market_cookies) + receive_data = self.request_json(f'{self.market_base_url}/market/signin/page/receiveV2', + params = {'client': 'app'}, + headers = receive_headers, + cookies = receive_cookies) + if not receive_data: + self.log('领取云朵失败: 接口无响应') + self.log(f'-当前待领取:{pending_amount}云朵') + elif receive_data.get('code') == 0: + receive_result = receive_data.get('result', {}) + self.log(f'-领取云朵:{receive_result.get("receive", pending_amount)}云朵') + total_amount = receive_result.get('total', total_amount) + else: + latest_info_data = self.request_market_json(f'{self.market_base_url}/market/signin/page/infoV3', + params = {'client': 'app'}) + latest_result = latest_info_data.get('result', {}) if latest_info_data and latest_info_data.get('code') == 0 else {} + latest_pending = latest_result.get('toReceive', pending_amount) + latest_total = latest_result.get('total', total_amount) + pending_delta = pending_amount - latest_pending if isinstance(pending_amount, int) and isinstance(latest_pending, int) else 0 + total_delta = latest_total - total_amount if isinstance(total_amount, int) and isinstance(latest_total, int) else 0 + claimed_amount = total_delta or pending_delta or (pending_amount if latest_pending == 0 else 0) + if claimed_amount > 0: + self.log(f'-领取云朵:{claimed_amount}云朵') + total_amount = latest_total + else: + self.log(f"领取云朵失败: {receive_data.get('msg', '未知错误')}") + if receive_data.get('code') in (614, 615) and not get_env_device_id() and not get_device_id(self.account): + self.log('-请检查变量中 deviceId 是否正确') + self.log(f'-当前待领取:{pending_amount}云朵') + else: + self.log('-当前待领取:0云朵') self.sleep() - prize_data = self.send_request(prize_url, headers = self.jwtHeaders, cookies = self.cookies).json() - result = prize_data.get('result').get('result') + prize_data = self.request_json(prize_url, headers = self.jwtHeaders, cookies = self.cookies) or {} + result = prize_data.get('result', {}).get('result') or [] rewards = '' for value in result: - prizeName = value.get('prizeName') - flag = value.get('flag') - if flag == 1: - rewards += f'-待领取奖品: {prizeName}\n' + if value.get('flag') == 1: + rewards += f'待领取奖品: {value.get("prizeName")}\n' + self.log(f'-当前云朵数量:{total_amount}云朵') + if rewards: + self.log(rewards) + global user_amount + user_amount += f'用户【{self.encrypt_account}】:{total_amount}云朵\n' - receive_amount = receive_data["result"].get("receive", "") - total_amount = receive_data["result"].get("total", "") - print(f'\n-当前待领取:{receive_amount}云朵') - print(f'-当前云朵数量:{total_amount}云朵') - msg = f'云朵数量:{total_amount} \n{rewards}' - self.log_info(amount = msg) - # 备份云朵 @catch_errors def backup_cloud(self): backup_url = 'https://caiyun.feixin.10086.cn/market/backupgift/info' backup_data = self.send_request(backup_url, headers = self.jwtHeaders).json() state = backup_data.get('result', {}).get('state', '') if state == -1: - print('本月未备份,暂无连续备份奖励') + self.log('本月未备份,暂无连续备份奖励') elif state == 0: - print('-领取本月连续备份奖励') + self.log('-领取本月连续备份奖励') cur_url = 'https://caiyun.feixin.10086.cn/market/backupgift/receive' cur_data = self.send_request(cur_url, headers = self.jwtHeaders).json() - print(f'-获得云朵数量:{cur_data.get("result").get("result")}') + self.log(f'-获得云朵数量:{cur_data.get("result").get("result")}') elif state == 1: print('-已领取本月连续备份奖励') self.sleep() - expend_url = 'https://caiyun.feixin.10086.cn/market/signin/page/taskExpansion' # 每月膨胀云朵 + expend_url = 'https://caiyun.feixin.10086.cn/market/signin/page/taskExpansion' expend_data = self.send_request(expend_url, headers = self.jwtHeaders, cookies = self.cookies).json() - - curMonthBackup = expend_data.get('result', {}).get('curMonthBackup', '') # 本月备份 - preMonthBackup = expend_data.get('result', {}).get('preMonthBackup', '') # 上月备份 - curMonthBackupTaskAccept = expend_data.get('result', {}).get('curMonthBackupTaskAccept', '') # 本月是否领取 - nextMonthTaskRecordCount = expend_data.get('result', {}).get('nextMonthTaskRecordCount', '') # 下月备份云朵 - acceptDate = expend_data.get('result', {}).get('acceptDate', '') # 月份 + curMonthBackup = expend_data.get('result', {}).get('curMonthBackup', '') + preMonthBackup = expend_data.get('result', {}).get('preMonthBackup', '') + curMonthBackupTaskAccept = expend_data.get('result', {}).get('curMonthBackupTaskAccept', '') + nextMonthTaskRecordCount = expend_data.get('result', {}).get('nextMonthTaskRecordCount', '') + acceptDate = expend_data.get('result', {}).get('acceptDate', '') if curMonthBackup: - print(f'- 本月已备份,下月可领取膨胀云朵: {nextMonthTaskRecordCount}') + self.log(f'- 本月已备份,下月可领取膨胀云朵: {nextMonthTaskRecordCount}') else: - print('- 本月还未备份,下月暂无膨胀云朵') + self.log('- 本月还未备份,下月暂无膨胀云朵') if preMonthBackup: if curMonthBackupTaskAccept: print('- 上月已备份,膨胀云朵已领取') else: - # 领取 + receive_url = f'https://caiyun.feixin.10086.cn/market/signin/page/receiveTaskExpansion?acceptDate={acceptDate}' receive_data = self.send_request(receive_url, headers = self.jwtHeaders, cookies = self.cookies).json() if receive_data.get("code") != 0: - print(f'-领取失败:{receive_data.get("msg")}') + self.log(f'-领取失败:{receive_data.get("msg")}') else: cloudCount = receive_data.get('result', {}).get('cloudCount', '') - print(f'- 膨胀云朵领取成功: {cloudCount}朵') + self.log(f'- 膨胀云朵领取成功: {cloudCount}朵') else: print('-上月未备份,本月无膨胀云朵领取') - # # 开启备份 - # def open_backup(self): - - # 通知云朵 @catch_errors - def open_send(self): - send_url = 'https://caiyun.feixin.10086.cn/market/msgPushOn/task/status' - send_data = self.send_request(send_url, headers = self.jwtHeaders).json() - - pushOn = send_data.get('result', {}).get('pushOn', '') # 0未开启,1开启,2未领取,3已领取 - firstTaskStatus = send_data.get('result', {}).get('firstTaskStatus', '') - secondTaskStatus = send_data.get('result', {}).get('secondTaskStatus', '') - onDuaration = send_data.get('result', {}).get('onDuaration', '') # 开启时间 - - if pushOn == 1: - reward_url = 'https://caiyun.feixin.10086.cn/market/msgPushOn/task/obtain' - - if firstTaskStatus == 3: - print('- 任务1奖励已领取') + def complete_notice_task(self, task_name): + notice_status = self.get_notice_status() + if not notice_status: + self.log(f'-需手动完成: {task_name}') + return + push_on = int(notice_status.get('pushOn') or 0) + first_status = int(notice_status.get('firstTaskStatus') or 0) + second_status = int(notice_status.get('secondTaskStatus') or 0) + on_duration = int(notice_status.get('onDuaration') or 0) + total = int(notice_status.get('total') or 31) + if push_on != 1: + self.log(f'-需手动完成: {task_name} (通知未开启)') + return + reward_url = 'https://caiyun.feixin.10086.cn/market/msgPushOn/task/obtain' + if first_status != 3: + reward1_data = self.request_json(reward_url, headers = self.jwtHeaders, data = {"type": 1}, method = 'POST') + if reward1_data and reward1_data.get('code') == 0: + self.log(f'-已领取: {task_name} (首日奖励)') else: - # 领取任务1 - print('- 领取任务1奖励') - reward1_data = self.send_request(reward_url, headers = self.jwtHeaders, data = {"type": 1}, - method = "POST").json() - print(reward1_data.get('result', {}).get('description', '')) - - if secondTaskStatus == 2: - # 领取任务2 - print('- 领取任务2奖励') - reward2_data = self.send_request(reward_url, headers = self.jwtHeaders, data = {"type": 2}, - method = "POST").json() - print(reward2_data.get('result', {}).get('description', '')) - - print(f'- 通知已开启天数: {onDuaration}, 满31天可领取奖励') + self.log(f'-待领取: {task_name} (首日奖励)') + if second_status == 2: + reward2_data = self.request_json(reward_url, headers = self.jwtHeaders, data = {"type": 2}, method = 'POST') + if reward2_data and reward2_data.get('code') == 0: + self.log(f'-已领取: {task_name} (连续奖励,已开启{on_duration}/{total}天)') + else: + self.log(f'-待领取: {task_name} (已开启{on_duration}/{total}天)') + elif second_status == 3: + self.log(f'-已完成: {task_name}') else: - print('- 通知权限未开启') + self.log(f'-进行中: {task_name} (已开启{on_duration}/{total}天)') if __name__ == "__main__": - env_name = 'ydyp_ck' + env_name = 'ydyp' token = os.getenv(env_name) - if not token: print(f'⛔️未获取到ck变量:请检查变量 {env_name} 是否填写') exit(0) - cookies = re.split(r'[@\n]', token) - print(f"移动云盘共获取到{len(cookies)}个账号") + cookies = re.split(r'[&]', token) + print_startup_info(len(cookies)) + print_device_id_notice() + print_storage_path_notice() - for i, account_info in enumerate(cookies, start = 1): + for i, account_info in enumerate(cookies, start=1): print(f"\n======== ▷ 第 {i} 个账号 ◁ ========") - YP(account_info).run() - print("\n随机等待5-10s进行下一个账号") - time.sleep(random.randint(5, 10)) + yp_instance = YP(account_info) + + if not yp_instance.Authorization: + print(f"⛔️ 账号 {i} 无效,跳过执行") + continue + + yp_instance.run() + print("\n准备进行下一个账号") + time.sleep(random.randint(1, 3)) - if err_accounts != '': - print(f"\n失效账号:\n{err_accounts}") - else: - print('当前所有账号ck有效') + + + msg = "" + if err_accounts: + msg += f"失效账号:\n{err_accounts}\n" - print(user_amount) + msg += f"任务详情:\n{all_logs}\n" + msg += f"云朵汇总:\n{user_amount}" + + print("\n================ 运行总结 ================") + if err_accounts: + print(f"❌ 失效账号:\n{err_accounts}") + if user_amount: + print(f"☁️ 云朵汇总:\n{user_amount}") + + + msg = msg.replace('-', ' ').replace('.', ' ').replace('!', '!').replace('(', '(').replace(')', ')') + msg = msg.replace('_', ' ').replace('=', ' ').replace('~', ' ').replace('{', ' ').replace('}', ' ').replace('|', ' ') + send = load_send() - if PUSH_SWITCH == '1': - if send: - if err_accounts: - msg = f"⚠️ 失效账号:\n{err_accounts}\n" + user_amount - else: - msg = user_amount - send('☁️ 云朵资产统计', msg) - else: - print('通知服务不可用') - else: - print("推送开关已关闭,不发送推送通知") + if send: + send('中国移动云盘任务信息', msg)