Microsoft Rewards助手

自动完成Microsoft Rewards必应搜索任务,基于Async/Await重构,模拟真人操作,支持SPA,状态记忆。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Microsoft Rewards助手
// @version      4.0.0
// @description  自动完成Microsoft Rewards必应搜索任务,基于Async/Await重构,模拟真人操作,支持SPA,状态记忆。
// @author       Sentaku1129
// @match        *://*.bing.com/*
// @license      GUN GPLv3
// @icon         https://www.bing.com/favicon.ico
// @run-at       document-end
// @grant        none
// @namespace    https://greasyfork.org/users/1029902
// ==/UserScript==

(function () {
    'use strict';

    // 解析积分数据
    function parseRewardsFromDoc(doc) {
        const cards = doc.querySelectorAll('.promo_cont');
        if (!cards.length) return null;

        let pc = { c: 0, m: 0 }, mb = { c: 0, m: 0 };

        cards.forEach(card => {
            const text = card.innerText;
            const isMobile = text.includes('移动') || text.includes('Mobile');
            const row = card.querySelector('.daily_search_row');

            // 1. 未完成 (进度条)
            if (row) {
                const match = row.innerText.match(/(\d+)\s*\/\s*(\d+)/);
                if (match) {
                    const [_, current, max] = match.map(Number);
                    if (isMobile || max === 60) mb = { c: current, m: max };
                    else pc = { c: current, m: max };
                }
                return;
            }

            // 2. 已完成
            if (text.includes('已获得') || text.includes('earned')) {
                const match = text.match(/(\d+)/);
                if (match) {
                    const points = parseInt(match[1]);
                    if (isMobile || points === 60) mb = { c: points, m: points };
                    else if (points >= 90) pc = { c: points, m: points };
                }
            }
        });

        return { pc, mb };
    }

    // Iframe 通信处理
    if (window.self !== window.top) {
        window.addEventListener('message', (e) => {
            if (e.data === 'REQUEST_REWARDS_DATA') {
                const result = parseRewardsFromDoc(document);
                if (result) window.top.postMessage({ type: 'REWARDS_DATA_RESPONSE', payload: result }, '*');
            }
        });
        return;
    }

    // --- 工具函数 ---
    const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
    const randomSleep = (min, max) => sleep(Math.floor(Math.random() * (max - min + 1) + min) * 1000);

    // --- 主题配置 ---
    const theme = {
        main: "#0078d4",
        accent: "#4CAF50",
        error: "#f44336",
        bg: "rgba(255, 255, 255, 0.95)",
        fg: "#333",
        border: "#e0e0e0",
        shadow: "0 8px 30px rgba(0,0,0,0.12)",
        font: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`
    };

    // --- 默认配置与状态 ---
    const DEFAULT_CONFIG = {
        restTime: 5 * 60, // 5分钟
        scrollTime: 8,    // 滚动持续时间
        waitTime: 10,     // 搜索间隔
        maxNoProgress: 3, // 最大无进度次数
        randomTime: 5     // 随机时间偏移(秒)
    };

    let state = {
        isSearching: false,
        isCollapsed: true,
        usedTerms: [],
        termsPool: [],
        progress: { current: 0, total: 0, lastChecked: 0, noProgressCount: 0, completed: false },
        rewards: { balance: "---", today: 0, limit: 0, pc: { c: 0, m: 0 }, mobile: { c: 0, m: 0 } },
        config: { ...DEFAULT_CONFIG },
        timer: null
    };

    const STORAGE_KEY_CONFIG = 'rewardsHelper_config_v3.4';
    const STORAGE_KEY_TASK = 'rewardsHelper_task_v3.4';
    const FALLBACK_WORDS = [
        "天气", "新闻", "壁纸", "翻译", "地图", "汇率", "日历", "计算器", "电影", "音乐", "NBA", "游戏", "美食", "旅游", "股票", "小说", "科技", "数码", "汽车", "房产",
        "Python教程", "Java入门", "C++编程", "Linux命令", "Docker容器", "Kubernetes", "人工智能", "机器学习", "深度学习", "神经网络",
        "唐诗三百首", "宋词精选", "红楼梦", "三国演义", "水浒传", "西游记", "鲁迅全集", "金庸小说", "古龙小说", "科幻小说",
        "宫保鸡丁做法", "红烧肉食谱", "清蒸鱼技巧", "火锅底料", "奶茶制作", "咖啡冲泡", "烘焙入门", "健康饮食", "减肥食谱", "素食主义",
        "北京旅游攻略", "上海景点", "广州美食", "深圳科技园", "成都火锅", "重庆夜景", "西安兵马俑", "杭州西湖", "苏州园林", "三亚海滩"
    ];

    // --- 持久化存储 ---
    function saveState() {
        try {
            localStorage.setItem(STORAGE_KEY_CONFIG, JSON.stringify({
                isCollapsed: state.isCollapsed,
                config: state.config
            }));
            sessionStorage.setItem(STORAGE_KEY_TASK, JSON.stringify({
                isSearching: state.isSearching,
                usedTerms: state.usedTerms.slice(-50)
            }));
        } catch (e) { console.error("保存状态失败", e); }
    }

    function loadState() {
        try {
            const savedConfig = JSON.parse(localStorage.getItem(STORAGE_KEY_CONFIG));
            if (savedConfig) {
                state.isCollapsed = savedConfig.isCollapsed || false;
                if (savedConfig.config) state.config = { ...DEFAULT_CONFIG, ...savedConfig.config };
            }
            const savedTask = JSON.parse(sessionStorage.getItem(STORAGE_KEY_TASK));
            if (savedTask) {
                state.isSearching = savedTask.isSearching || false;
                state.usedTerms = savedTask.usedTerms || [];
            }
        } catch (e) { console.error("读取状态失败", e); }
    }

    // --- 核心逻辑:积分获取 ---
    async function fetchRewards() {
        logStatus('查询积分中...');

        // 尝试获取总分
        for (let i = 0; i < 5; i++) {
            const el = document.querySelector('#id_rc, .points-container');
            if (el) {
                state.rewards.balance = el.innerText;
                updateUI();
                break;
            }
            await sleep(500);
        }

        return new Promise((resolve) => {
            // 查找挂件
            let widget = null;
            const findWidget = setInterval(() => {
                widget = document.querySelector('.b_clickarea, #id_rh');
                if (widget) {
                    clearInterval(findWidget);
                    startProcess();
                }
            }, 500);

            setTimeout(() => {
                if (!widget) {
                    clearInterval(findWidget);
                    logStatus('未找到积分挂件');
                    resolve();
                }
            }, 5000);

            function startProcess() {
                const flyout = document.getElementById('rewid-f');
                const isVisible = flyout && flyout.style.display !== 'none' && flyout.style.visibility !== 'hidden';

                if (!isVisible) {
                    // 仅在搜索页或首页点击
                    const isSearch = location.search.includes('q=') || location.pathname.includes('/search');
                    if (state.isSearching || !isSearch) {
                        logStatus('展开积分面板...');
                        widget.click();
                        setTimeout(() => parseCards(isVisible), 2000);
                    } else {
                        resolve();
                    }
                } else {
                    parseCards(isVisible);
                }
            }

            function parseCards(wasVisible) {
                let attempts = 0;
                const checkInterval = setInterval(() => {
                    attempts++;
                    const iframe = document.querySelector('#rewid-f iframe');

                    if (iframe) {
                        // 尝试直接读取
                        try {
                            const doc = iframe.contentDocument || iframe.contentWindow.document;
                            const result = parseRewardsFromDoc(doc);
                            if (result) return handleSuccess(result);
                        } catch (e) {}

                        // 尝试PostMessage
                        iframe.contentWindow.postMessage('REQUEST_REWARDS_DATA', '*');
                    }

                    if (attempts > 20) {
                        clearInterval(checkInterval);
                        window.removeEventListener('message', messageHandler);
                        logStatus('查询超时');
                        resolve();
                    }
                }, 500);

                const messageHandler = (e) => {
                    if (e.data.type === 'REWARDS_DATA_RESPONSE') handleSuccess(e.data.payload);
                };
                window.addEventListener('message', messageHandler);

                function handleSuccess(data) {
                    clearInterval(checkInterval);
                    window.removeEventListener('message', messageHandler);

                    const { pc, mb } = data;
                    const balanceEl = document.querySelector('#id_rc, .points-container');

                    state.rewards.balance = balanceEl ? balanceEl.innerText : "---";
                    state.rewards.pc = pc;
                    state.rewards.mobile = mb;
                    state.rewards.today = pc.c + mb.c;
                    state.rewards.limit = pc.m + mb.m;

                    logStatus(`积分: ${state.rewards.today}/${state.rewards.limit} (PC:${pc.c}/${pc.m})`);

                    if (state.isSearching) {
                        if (state.rewards.limit > 0 && state.rewards.today >= state.rewards.limit) {
                            logStatus("任务已完成!");
                            toggleSearch();
                        } else if (pc.m > 0 && pc.c >= pc.m && mb.c < mb.m) {
                            logStatus("PC完成,尝试移动端...");
                        }
                    }

                    updateUI();
                    if (!wasVisible) widget.click(); // 恢复状态
                    resolve();
                }
            }
        });
    }

    // --- 核心逻辑:数据获取 ---
    function grabSearchTerms() {
        const suggestElements = document.querySelectorAll('.richrsrailsuggestion_text, .sa_tm_text, .sa_sg .sa_tm');
        const pageTerms = Array.from(suggestElements).map(el => el.innerText).filter(t => t);

        let iframeTerms = [];
        const iframe = document.querySelector('iframe#b_context');
        if (iframe) {
            try {
                const doc = iframe.contentDocument || iframe.contentWindow.document;
                const spans = doc.querySelectorAll('.ss_items_wrapper span');
                iframeTerms = Array.from(spans).map(s => s.innerText).filter(t => t);
            } catch (e) {}
        }

        state.termsPool = [...new Set([...pageTerms, ...iframeTerms, ...FALLBACK_WORDS])];
        updateUI();
    }

    function getNextTerm() {
        const available = state.termsPool.filter(t => !state.usedTerms.includes(t));
        if (available.length === 0) {
            return `Bing搜索 ${Math.floor(Math.random() * 10000)}`;
        }
        return available[Math.floor(Math.random() * available.length)];
    }

    // --- 核心逻辑:模拟操作 ---
    async function performSearch(term) {
        const input = document.getElementById('sb_form_q');
        const form = document.getElementById('sb_form');
        if (!input || !form) return false;

        input.value = term;
        input.dispatchEvent(new Event('input', { bubbles: true }));
        input.dispatchEvent(new Event('change', { bubbles: true }));
        await sleep(300);

        const submitBtn = document.getElementById('sb_form_go') || document.querySelector('label[for="sb_form_go"]');
        const beforeUrl = location.href;
        state.usedTerms.push(term);
        saveState();

        if (form.target === '_blank') form.target = '_self';

        if (submitBtn) submitBtn.click();
        else form.submit();

        return new Promise(resolve => {
            let checks = 0;
            const checker = setInterval(() => {
                checks++;
                if (location.href !== beforeUrl) {
                    clearInterval(checker);
                    resolve(true);
                }
                if (checks > 20) {
                    clearInterval(checker);
                    resolve(true);
                }
            }, 500);
        });
    }

    async function simulateHumanScroll() {
        logStatus('模拟浏览中...');
        const baseTime = state.config.scrollTime;
        const randomAdd = Math.floor(Math.random() * (state.config.randomTime + 1));
        const totalTime = baseTime + randomAdd;
        const startTime = Date.now();

        while (Date.now() - startTime < totalTime * 1000) {
            const direction = Math.random() > 0.3 ? 1 : -1;
            const distance = Math.floor(Math.random() * 400) + 100;

            window.scrollBy({ top: distance * direction, behavior: 'smooth' });
            updateCountdown(Math.ceil((totalTime * 1000 - (Date.now() - startTime)) / 1000), '浏览页面');
            await sleep(1500 + Math.random() * 1500);
        }
        window.scrollTo({ top: 0, behavior: 'smooth' });
    }

    // --- 自动化主流程 ---
    async function runLoop() {
        if (!state.isSearching) return;

        logStatus("正在检查积分...");
        await fetchRewards();
        if (!state.isSearching) return;

        grabSearchTerms();
        const term = getNextTerm();
        logStatus(`准备搜索: ${term}`);

        if (state.config.randomTime > 0) await randomSleep(1, 3);

        await performSearch(term);
        await sleep(3000);

        if (state.isSearching) {
            logStatus("搜索后复查积分...");
            await fetchRewards();
            if (!state.isSearching) return;
        }

        if (state.isSearching) await simulateHumanScroll();

        if (state.isSearching) {
            const baseWait = state.config.waitTime;
            const randomAdd = Math.floor(Math.random() * (state.config.randomTime + 1));
            const totalWait = baseWait + randomAdd;

            for (let i = totalWait; i > 0; i--) {
                if (!state.isSearching) break;
                updateCountdown(i, '等待下次搜索');
                await sleep(1000);
            }
            if (state.isSearching) setTimeout(runLoop, 100);
        }
    }

    function toggleSearch() {
        state.isSearching = !state.isSearching;
        saveState();
        updateUI();
        if (state.isSearching) {
            runLoop();
        } else {
            logStatus("已停止");
            updateCountdown(0, "");
        }
    }

    // --- UI 构建与更新 ---
    function injectStyles() {
        const style = document.createElement('style');
        style.innerHTML = `
            #rh-container {
                position: fixed; top: 80px; right: 20px; width: 300px;
                background: ${theme.bg}; backdrop-filter: blur(10px);
                border: 1px solid ${theme.border}; border-radius: 12px;
                box-shadow: ${theme.shadow}; z-index: 99999;
                font-family: ${theme.font}; font-size: 14px; color: ${theme.fg};
                transition: all 0.3s ease; overflow: hidden;
            }
            #rh-header {
                background: ${theme.main}; color: white; padding: 12px 16px;
                font-weight: 600; display: flex; justify-content: space-between;
                align-items: center; cursor: move; user-select: none;
            }
            #rh-content { padding: 16px; }
            .rh-btn {
                width: 100%; padding: 10px; border: none; border-radius: 8px;
                font-weight: 600; cursor: pointer; transition: 0.2s; margin-top: 10px;
            }
            .rh-btn.start { background: ${theme.main}; color: white; }
            .rh-btn.start:hover { background: #0063b1; }
            .rh-btn.stop { background: ${theme.error}; color: white; }
            .rh-btn.stop:hover { background: #d32f2f; }
            .rh-row { display: flex; justify-content: space-between; margin-bottom: 8px; align-items: center;}
            .rh-input { width: 50px; padding: 4px; border: 1px solid #ccc; border-radius: 4px; text-align: center; }
            .rh-status { font-size: 12px; color: #666; margin-top: 5px; min-height: 1.5em;}
            .rh-tag { background: #f0f0f0; padding: 2px 6px; border-radius: 4px; font-size: 12px; color: #555; margin-right: 4px; display: inline-block;}
            .hidden { display: none !important; }
        `;
        document.head.appendChild(style);
    }

    function createUI() {
        injectStyles();
        const container = document.createElement('div');
        container.id = 'rh-container';
        container.innerHTML = `
            <div id="rh-header">
                <span>🏆 Rewards 助手 Pro</span>
                <div style="display:flex;gap:10px;">
                    <span id="rh-toggle" style="cursor:pointer;font-size:18px;">-</span>
                    <span id="rh-close" style="cursor:pointer;font-size:18px;">×</span>
                </div>
            </div>
            <div id="rh-content">
                <div class="rh-row">
                    <span>运行状态</span>
                    <span id="rh-state-text" style="font-weight:bold;color:${theme.main}">待机</span>
                </div>
                <div class="rh-row">
                    <span>当前积分</span>
                    <span id="rh-points" style="font-weight:bold;color:#E65100">---</span>
                </div>
                <div class="rh-row">
                    <span>今日搜索</span>
                    <div style="flex:1;margin-left:10px;display:flex;flex-direction:column;justify-content:center;">
                        <div style="background:#eee;height:6px;border-radius:3px;overflow:hidden;">
                            <div id="rh-progress-bar" style="width:0%;height:100%;background:${theme.accent};transition:width 0.5s;"></div>
                        </div>
                        <div style="display:flex;justify-content:space-between;font-size:10px;margin-top:2px;">
                            <span id="rh-search-progress" style="font-weight:bold;color:#2E7D32">--- / ---</span>
                            <span id="rh-progress-percent">0%</span>
                        </div>
                    </div>
                </div>
                <div id="rh-countdown" class="rh-row" style="color:${theme.accent};font-weight:bold;display:none;">
                    ⏳ <span id="rh-countdown-text"></span>
                </div>
                <div class="rh-status" id="rh-log">就绪...</div>

                <div id="rh-settings">
                    <hr style="border:0;border-top:1px solid #eee;margin:10px 0;">
                    <div class="rh-row">
                        <label>搜索间隔(秒)</label>
                        <input type="number" id="cfg-wait" class="rh-input" value="${state.config.waitTime}">
                    </div>
                    <div class="rh-row">
                        <label>浏览时长(秒)</label>
                        <input type="number" id="cfg-scroll" class="rh-input" value="${state.config.scrollTime}">
                    </div>
                    <div class="rh-row">
                        <label>随机偏移(秒)</label>
                        <input type="number" id="cfg-random" class="rh-input" value="${state.config.randomTime}">
                    </div>
                </div>

                <button id="rh-action-btn" class="rh-btn start">开始任务</button>
            </div>
        `;
        document.body.appendChild(container);

        // 绑定事件
        const dragHeader = container.querySelector('#rh-header');
        makeDraggable(container, dragHeader);

        document.getElementById('rh-close').onclick = () => container.style.display = 'none';
        document.getElementById('rh-toggle').onclick = () => {
            state.isCollapsed = !state.isCollapsed;
            saveState();
            updateUI();
        };

        const actionBtn = document.getElementById('rh-action-btn');
        actionBtn.onclick = toggleSearch;

        // 配置绑定
        const bindInput = (id, key) => {
            const el = document.getElementById(id);
            el.onchange = () => {
                state.config[key] = parseInt(el.value) || DEFAULT_CONFIG[key];
                saveState();
            };
        };
        bindInput('cfg-wait', 'waitTime');
        bindInput('cfg-scroll', 'scrollTime');
        bindInput('cfg-random', 'randomTime');

        updateUI();
    }

    function updateUI() {
        const content = document.getElementById('rh-settings');
        const toggleBtn = document.getElementById('rh-toggle');
        const actionBtn = document.getElementById('rh-action-btn');
        const stateText = document.getElementById('rh-state-text');
        const pointsText = document.getElementById('rh-points');
        const progressText = document.getElementById('rh-search-progress');
        const progressBar = document.getElementById('rh-progress-bar');
        const progressPercent = document.getElementById('rh-progress-percent');

        if (state.isCollapsed) {
            content.classList.add('hidden');
            toggleBtn.textContent = '+';
        } else {
            content.classList.remove('hidden');
            toggleBtn.textContent = '-';
        }

        // 更新积分显示
        if (pointsText) pointsText.textContent = state.rewards.balance;
        if (progressText && progressBar && progressPercent) {
            if (state.rewards.limit > 0) {
                const percent = Math.round((state.rewards.today / state.rewards.limit) * 100);
                progressText.textContent = `${state.rewards.today} / ${state.rewards.limit}`;
                progressBar.style.width = `${percent}%`;
                progressPercent.textContent = `${percent}%`;

                // 颜色变化:接近完成变绿,否则蓝色
                progressBar.style.background = percent >= 100 ? theme.accent : theme.main;

                // 悬浮显示详情
                const pc = state.rewards.pc;
                const mb = state.rewards.mobile;
                progressText.title = `PC: ${pc.c}/${pc.m} | Mobile: ${mb.c}/${mb.m}`;
                progressText.style.cursor = "help";
            } else {
                progressText.textContent = "--- / ---";
                progressBar.style.width = "0%";
                progressPercent.textContent = "0%";
            }
        }        if (state.isSearching) {
            actionBtn.textContent = "停止任务";
            actionBtn.className = "rh-btn stop";
            stateText.textContent = "运行中";
            stateText.style.color = theme.accent;
        } else {
            actionBtn.textContent = "开始任务";
            actionBtn.className = "rh-btn start";
            stateText.textContent = "待机";
            stateText.style.color = "#999";
        }
    }

    function logStatus(msg) {
        const el = document.getElementById('rh-log');
        if (el) el.textContent = msg;
    }

    function updateCountdown(sec, action) {
        const div = document.getElementById('rh-countdown');
        const text = document.getElementById('rh-countdown-text');
        if (sec > 0) {
            div.style.display = 'flex';
            text.textContent = `${action}: ${sec}s`;
        } else {
            div.style.display = 'none';
        }
    }

    function makeDraggable(el, handle) {
        let isDragging = false;
        let startX, startY, initialLeft, initialTop;

        handle.addEventListener('mousedown', e => {
            if (e.target !== handle && e.target.id !== 'rh-header') return;
            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;
            const rect = el.getBoundingClientRect();
            initialLeft = rect.left;
            initialTop = rect.top;
            el.style.right = 'auto'; // 清除right定位,改用left
            el.style.left = initialLeft + 'px';
            el.style.top = initialTop + 'px';
            document.body.style.userSelect = 'none';
        });

        document.addEventListener('mousemove', e => {
            if (!isDragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            el.style.left = `${initialLeft + dx}px`;
            el.style.top = `${initialTop + dy}px`;
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            document.body.style.userSelect = '';
        });
    }

    // --- 启动入口 ---
    window.addEventListener('load', () => {
        loadState();
        createUI();
        fetchRewards(); // 初始获取积分

        // 自动恢复运行 (如果是刷新页面导致的)
        if (state.isSearching) {
            logStatus("页面恢复,准备继续...");
            // 增加随机延迟,避免“瞬间”开始,模拟用户思考
            const delay = 2000 + Math.floor(Math.random() * (state.config.randomTime * 1000 + 1));
            setTimeout(runLoop, delay);
        }
    });

})();