diff --git a/miuitask_batch.py b/miuitask_batch.py new file mode 100644 index 0000000..a3ce49e --- /dev/null +++ b/miuitask_batch.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +""" +MIUI自动任务分批运行版 +""" + +import os +import glob +import subprocess +import stat +import platform +import sys +import time +import gc +import shutil +import signal +import atexit + + +class MIUITaskQinglong: + """MIUI自动任务分批运行版""" + + # ==================== 用户配置区域 ==================== + # 可直接修改以下参数,无需设置环境变量 + ENABLE_BATCH_MODE = "true" # 是否启用分批处理,"false"=一次处理所有账号 + DEFAULT_BATCH_SIZE = 5 # 每批处理账号数量,建议5-10 + DEFAULT_BATCH_DELAY = 5 # 批次间延迟秒数,建议5-10 + DEFAULT_MAX_RETRIES = 1 # 失败重试次数,建议1-3 + DEFAULT_CONFIG_PATH = "data/config.yaml" # 配置文件路径 + # ==================================================== + + # 系统常量 + TEMP_DIR = "temp_ql" + DATA_DIR = "data" + + # 执行参数限制 + MIN_BATCH_SIZE, MAX_BATCH_SIZE = 1, 10 + MIN_DELAY, MAX_DELAY = 5, 300 + MIN_RETRIES, MAX_RETRIES = 0, 5 + + # 常用字符串 + BATCH_PREFIX = "=== 批次" + CLEANUP_PATTERN = "data_backup_*" + + BINARY_PATTERNS = [ + "miuitask*{arch}*.bin", + "miuitask*.bin", + "miuitask*linux*{arch}*", + "miuitask*linux*" + ] + + LOG_KEYWORDS = [ + 'SUCCESS', 'ERROR', '成功', '失败', '领到', '完成', '任务', + '账号:', '成长值+', '获得', '金币', '系统信息', '项目信息', + '脚本日志', '开始处理账号任务', '正在启动', '构建时间', '当前版本', + '操作系统:', 'Go版本:', '闲鱼ID', '反馈带日志', + 'cookie失效', '登录失败', '网络错误', '配置错误', '任务已完成', + '执行任务', '处理账号', 'Cookie invalid', 'Login failed' + ] + + def __init__(self): + self.enable_batch = os.getenv('ENABLE_BATCH_MODE', self.ENABLE_BATCH_MODE).lower() in ('true', '1', 'yes') + self.batch_size = max(self.MIN_BATCH_SIZE, min(self.MAX_BATCH_SIZE, + int(os.getenv('BATCH_SIZE', str(self.DEFAULT_BATCH_SIZE))))) + self.batch_delay = max(self.MIN_DELAY, min(self.MAX_DELAY, + int(os.getenv('BATCH_DELAY', str(self.DEFAULT_BATCH_DELAY))))) + self.max_retries = max(self.MIN_RETRIES, min(self.MAX_RETRIES, + int(os.getenv('MAX_RETRIES', str(self.DEFAULT_MAX_RETRIES))))) + self.config_file = os.getenv('MIUITASK_CONFIG_PATH', self.DEFAULT_CONFIG_PATH) + + atexit.register(self._cleanup_all) + signal.signal(signal.SIGTERM, self._signal_handler) + signal.signal(signal.SIGINT, self._signal_handler) + + def log(self, message: str): + print(message) + + def _validate_config(self, config): + if not isinstance(config, dict) or not config.get('accounts'): + return False, "配置文件格式错误或缺少accounts" + + accounts = config['accounts'] + if not isinstance(accounts, list) or len(accounts) == 0: + return False, "accounts必须是非空数组" + + for i, account in enumerate(accounts): + if not isinstance(account, dict) or not account.get('uid'): + return False, f"账号{i+1}格式错误或缺少uid" + + has_cookies = account.get('cookies') and isinstance(account['cookies'], dict) + has_password = account.get('password') and account['password'].strip() + + if not has_cookies and not has_password: + return False, f"账号{i+1}缺少有效的cookies或password" + + return True, "配置验证通过" + + + + def _cleanup_all(self): + # 清理临时目录 + if os.path.exists(self.TEMP_DIR): + try: + shutil.rmtree(self.TEMP_DIR) + except Exception: + pass + + # 清理所有备份目录 + for item in glob.glob("data_backup_*"): + try: + if os.path.isdir(item): + shutil.rmtree(item) + except Exception: + pass + + def _signal_handler(self, signum, _): + self._cleanup_all() + sys.exit(128 + signum) + + def get_system_arch(self): + machine = platform.machine().lower() + if machine in ("x86_64", "amd64"): + return "amd64" + elif machine in ("aarch64", "arm64"): + return "arm64" + else: + self.log(f"不支持的架构: {machine}") + sys.exit(1) + + def parse_yaml_robust(self, file_path: str): + self.log(f"解析配置文件: {file_path}") + + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + config = {'accounts': []} + lines = content.split('\n') + + in_accounts = False + current_account = None + current_cookies = None + current_mipay_cookies = None + + for line in lines: + stripped = line.strip() + + if not stripped or stripped.startswith('#'): + continue + + if stripped == 'accounts:': + in_accounts = True + continue + + if in_accounts and not line.startswith(' ') and not line.startswith('\t') and ':' in stripped: + if stripped.split(':')[0].strip() in ['onepush', 'preference']: + break + + if in_accounts: + indent = len(line) - len(line.lstrip()) + + if stripped.startswith('- ') or (stripped.startswith('-') and ':' in stripped): + if current_account: + config['accounts'].append(current_account) + + current_account = {} + current_cookies = None + current_mipay_cookies = None + + if ':' in stripped: + key_value = stripped.split(':', 1)[1].strip().strip('"\'') + if key_value: + current_account['uid'] = key_value + + elif current_account is not None and ':' in stripped and indent > 0: + key, value = stripped.split(':', 1) + key = key.strip() + value = value.strip() + + if key == 'cookies': + current_cookies = {} + current_account['cookies'] = current_cookies + elif key == 'mipay_cookies': + current_mipay_cookies = {} + current_account['mipay_cookies'] = current_mipay_cookies + elif current_cookies is not None and indent > 6: + current_cookies[key] = value.strip('"\'') + elif current_mipay_cookies is not None and indent > 6: + current_mipay_cookies[key] = value.strip('"\'') + else: + clean_value = value.strip('"\'') + if clean_value.lower() in ('true', 'false'): + current_account[key] = clean_value.lower() == 'true' + else: + current_account[key] = clean_value + + if current_account: + config['accounts'].append(current_account) + + valid_accounts = [] + for i, account in enumerate(config['accounts']): + if account.get('uid'): + if 'mipay' not in account: + account['mipay'] = True + if 'community' not in account: + account['community'] = False + + valid_accounts.append(account) + self.log(f"账号 {i+1}: uid={account.get('uid')}") + + config['accounts'] = valid_accounts + self.log(f"找到 {len(valid_accounts)} 个账号") + return config + + except Exception as e: + self.log(f"解析配置文件失败: {e}") + return {'accounts': []} + + def create_batch_config(self, accounts, batch_num: int): + os.makedirs(self.TEMP_DIR, exist_ok=True) + + lines = [] + lines.append("preference:") + lines.append(' twocaptcha_api_key: ""') + lines.append(' twocaptcha_user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"') + lines.append(' twocaptcha_server: ""') + lines.append("") + lines.append("accounts:") + + for account in accounts: + lines.append(f' - uid: "{account.get("uid", "")}"') + lines.append(f' password: "{account.get("password", "")}"') + + if account.get('cookies'): + lines.append(" cookies:") + for k, v in account['cookies'].items(): + lines.append(f' {k}: {v}') + + if account.get('mipay_cookies'): + lines.append(" mipay_cookies:") + for k, v in account['mipay_cookies'].items(): + lines.append(f' {k}: {v}') + + lines.append(f' login_user_agent: "{account.get("login_user_agent", "Mozilla/5.0 (Linux; Android 13)")}"') + lines.append(f' user_agent: "{account.get("user_agent", "Mozilla/5.0 (Linux; Android 13)")}"') + lines.append(f' device: "{account.get("device", "")}"') + lines.append(f' device_model: "{account.get("device_model", "")}"') + lines.append(f' community: {str(account.get("community", False)).lower()}') + lines.append(f' mipay: {str(account.get("mipay", True)).lower()}') + lines.append("") + + lines.append("onepush:") + lines.append(' notifier: ""') + lines.append(" params: {}") + + config_content = '\n'.join(lines) + config_path = os.path.join(self.TEMP_DIR, f"config_batch_{batch_num}.yaml") + + try: + with open(config_path, 'w', encoding='utf-8') as f: + f.write(config_content) + return config_path + except Exception as e: + self.log(f"创建配置文件失败: {e}") + return None + + def find_binary(self): + arch = self.get_system_arch() + patterns = [pattern.format(arch=arch) for pattern in self.BINARY_PATTERNS] + + candidates = [] + for pattern in patterns: + candidates.extend(glob.glob(pattern)) + + if not candidates: + self.log("未找到可执行文件") + return None + + best_file = None + for f in candidates: + if 'linux' in f.lower() and arch in f.lower(): + best_file = f + break + + if not best_file: + for f in candidates: + if 'linux' in f.lower(): + best_file = f + break + + if not best_file: + best_file = candidates[0] + + self.log(f"选择可执行文件: {best_file}") + + try: + current_mode = os.stat(best_file).st_mode + os.chmod(best_file, current_mode | stat.S_IEXEC) + except Exception as e: + self.log(f"设置执行权限失败: {e}") + + return best_file + + def run_batch(self, binary_path: str, config_path: str, batch_num: int): + self.log(f"{self.BATCH_PREFIX} {batch_num} ===") + + try: + env = os.environ.copy() + env['MIUITASK_CONFIG_PATH'] = config_path + + # 使用Popen实现实时输出,无超时限制 + process = subprocess.Popen( + [f"./{binary_path}"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # 合并stderr到stdout + text=True, + env=env, + bufsize=1, + universal_newlines=True + ) + + # 实时读取并输出每一行 + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + line = output.strip() + if line: + self.log(line) + + # 获取最终退出码 + return_code = process.poll() + self.log(f"批次 {batch_num} 完成,退出码: {return_code}") + + success = return_code == 0 + if not success: + self.log(f"批次 {batch_num} 失败") + return success + + except Exception as e: + self.log(f"批次 {batch_num} 异常: {e}") + return False + finally: + try: + if os.path.exists(config_path): + os.remove(config_path) + except Exception: + pass + + def run_all_batches(self): + self.log("=== MIUI自动任务分批运行版启动 ===") + mode = "分批处理" if self.enable_batch else "全量处理" + self.log(f"模式: {mode}, 批次={self.batch_size}, 延迟={self.batch_delay}s, 重试={self.max_retries}") + + binary_path = self.find_binary() + if not binary_path: + return False + + if not os.path.exists(self.config_file): + self.log(f"配置文件不存在: {self.config_file}") + self.log("运行程序创建配置文件...") + + try: + result = subprocess.run( + [f"./{binary_path}"], + capture_output=True, + text=True, + env=os.environ.copy() + ) + + if result.stdout: + for line in result.stdout.split('\n'): + line = line.strip() + if line: + self.log(line) + + if os.path.exists(self.config_file): + self.log("配置文件创建成功") + else: + self.log("配置文件创建失败") + return False + + except Exception as e: + self.log(f"创建配置文件失败: {e}") + return False + + config = self.parse_yaml_robust(self.config_file) + is_valid, message = self._validate_config(config) + if not is_valid: + self.log(f"配置错误: {message}") + return False + + accounts = config.get('accounts', []) + if not accounts: + self.log("配置文件中没有账号,请先配置账号") + return False + + if self.enable_batch: + batches = [] + for i in range(0, len(accounts), self.batch_size): + batches.append(accounts[i:i + self.batch_size]) + self.log(f"分批处理: {len(accounts)} 个账号 → {len(batches)} 批次") + else: + batches = [accounts] + self.log(f"全量处理: {len(accounts)} 个账号") + + total_batches = len(batches) + successful_batches = 0 + failed_batches = [] + + try: + for batch_num, accounts in enumerate(batches, 1): + self.log(f"{self.BATCH_PREFIX} {batch_num}/{total_batches} ===") + uids = [acc.get('uid', 'unknown') for acc in accounts] + self.log(f"账号: {uids}") + + config_path = self.create_batch_config(accounts, batch_num) + if not config_path: + failed_batches.append(batch_num) + continue + + success = False + for attempt in range(self.max_retries + 1): + if attempt > 0: + self.log(f"重试 {attempt + 1}") + time.sleep(10) + # 重试时重新创建配置文件 + config_path = self.create_batch_config(accounts, batch_num) + if not config_path: + break + + success = self.run_batch(binary_path, config_path, batch_num) + if success: + successful_batches += 1 + break + + if not success: + failed_batches.append(batch_num) + + if batch_num < total_batches and self.enable_batch: + gc.collect() + time.sleep(self.batch_delay) + + finally: + self._cleanup_all() + + self.log("=== 执行结果 ===") + self.log(f"总批次: {total_batches}, 成功: {successful_batches}, 失败: {len(failed_batches)}") + + if failed_batches: + self.log(f"失败批次: {failed_batches}") + + success_rate = (successful_batches / total_batches) * 100 if total_batches > 0 else 0 + if success_rate == 100: + self.log(f"🎉 成功率: {success_rate:.1f}%") + else: + self.log(f"成功率: {success_rate:.1f}%") + + return len(failed_batches) == 0 + + +def main(): + try: + runner = MIUITaskQinglong() + success = runner.run_all_batches() + sys.exit(0 if success else 1) + except KeyboardInterrupt: + print("\n用户中断") + sys.exit(130) + except Exception as e: + print(f"异常: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main()