Bing Rewards Lite (Win & Mac)

v1.7.1 修复版

// ==UserScript==
// @name         Bing Rewards Lite (Win & Mac)
// @version      1.7.1
// @description  v1.7.1 修复版
// @match        https://*.bing.com/*
// @match        https://*.bing.cn/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_setTimeout
// @grant        GM_clearTimeout
// @grant        GM_setInterval
// @grant        GM_clearInterval
// @connect      api.vvhan.com
// @connect      api-hot.imsyy.top
// @license      MIT
// @namespace    https://greasyfork.org/users/737649
// ==/UserScript==

(function () {
    'use strict';

    /* ===== 基本配置 ===== */
    const ID = 'bru_panel';
    const VER = '1.7.1';
    const TYPE = { min: 35, max: 80 };
    const FALL = ['今日新闻', '天气预报', '电影票房', '体育比分', '股票行情'];

    const BATCH = { min: 5, max: 8 }; // 每次搜索次数
    const SEARCH_DELAY = { min: 120000, max: 240000 }; // 2-4分钟
    const SLEEP_DURATION = { min: 600000, max: 900000 }; // 10-15分钟

    /* ===== 热榜接口 ===== */
    const HOT_APIS = [
        'https://api.vvhan.com/api/hotlist/all',
        'https://api.vvhan.com/api/hotlist/wbHot',
        'https://api-hot.imsyy.top/baidu?num=50'
    ];

    /* ===== 兼容新版 Bing 选择器 ===== */
    const SELECTORS = ['#sb_form_q', '.b_searchbox', 'input[name="q"]', '#searchboxinput', 'textarea[name="q"]'];

    /* ===== 通用工具 ===== */
    const tSet = (typeof GM_setTimeout === 'function' ? GM_setTimeout : setTimeout);
    const tClr = (typeof GM_clearTimeout === 'function' ? GM_clearTimeout : clearTimeout);
    const iSet = (typeof GM_setInterval === 'function' ? GM_setInterval : setInterval);
    const iClr = (typeof GM_clearInterval === 'function' ? GM_clearInterval : clearInterval);
    const z2 = n => (n < 10 ? '0' : '') + n;

    /* ===== 状态记录 ===== */
    const today = new Date().toISOString().slice(0, 10);
    const def = {
        date: today,
        pc: 0,
        ph: 0,
        running: false,
        max_pc: 40,
        max_ph: 30,
        batch_size: 0,
        batch_count: 0,
        batch_searches_done: 0,
        is_sleeping: false
    };
    let rec = { ...def, ...GM_getValue('bru_lite_v17_final', {}) };
    if (rec.date !== today) {
        rec = { ...def, date: today, pc: 0, ph: 0 };
    }
    GM_setValue('bru_lite_v17_final', rec);

    const mobile = /mobile|android|iphone|ipad|touch/i.test(navigator.userAgent);
    const key = mobile ? 'ph' : 'pc';
    let limit = mobile ? rec.max_ph : rec.max_pc;

    /* ===== 热榜词 ===== */
    let HOT = [];
    async function fetchHot() {
        for (const url of HOT_APIS) {
            try {
                const json = await new Promise((ok, err) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url,
                        onload: ({ responseText }) => { try { ok(JSON.parse(responseText)); } catch (e) { err(e); } },
                        onerror: err
                    });
                });
                HOT = parseHot(json);
                if (HOT.length) return;
            } catch { }
        }
        HOT = FALL;
    }
    function parseHot(obj) {
        const words = [];
        const seen = new Set();
        const isWord = s => typeof s === 'string' && s.trim().length > 0 && s.length <= 40;
        const skipRe = /^(微博|知乎|百度|抖音|36氪|哔哩哔哩|IT资讯|虎嗅网|豆瓣|人人都是产品经理|热搜|热榜|API)/;
        function add(s) { s = s.trim(); if (isWord(s) && !skipRe.test(s) && !seen.has(s)) { seen.add(s); words.push(s); } }
        function walk(v) {
            if (Array.isArray(v)) { v.forEach(walk); } else if (v && typeof v === 'object') {
                if (v.title) add(v.title);
                if (v.keyword) add(v.keyword);
                if (v.name) add(v.name);
                if (v.word) add(v.word);
                for (const k of ['list', 'hotList', 'data', 'hot', 'children', 'items']) {
                    if (v[k]) walk(v[k]);
                }
            }
        }
        const root = obj?.data ?? obj;
        walk(root);
        return words;
    }

    /* ===== 样式 ===== */
    GM_addStyle(`#${ID}{position:fixed;top:10px;right:10px;z-index:99999;width:200px;padding:10px;font:11px system-ui,-apple-system,BlinkMacSystemFont,sans-serif;color:#222;background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);}#${ID} button{font-size:10px;background:#007bff;color:#fff;border:none;border-radius:4px;padding:2px 6px;cursor:pointer;}#${ID} input[type="number"]{width:40px;font-size:10px;border:1px solid #ccc;border-radius:4px;padding:2px;}#${ID} input[type="checkbox"]{vertical-align:middle;margin-right:2px;}.bar{height:4px;background:#ddd;border-radius:2px;}.fill{height:100%;background:#007bff;transform-origin:left;transition:transform .3s;}`);

    /* ===== 面板 ===== */
    function buildPanel() {
        if (document.getElementById(ID)) return;
        const box = document.createElement('div');
        box.id = ID;
        box.innerHTML = `
            <div id="drag" style="display:flex;justify-content:space-between;cursor:move;padding-bottom:4px">
                <b>BR Lite</b>
                <div><button id="run">${rec.running ? '暂停' : '启动'}</button><button id="clr">清零</button></div>
            </div>
            <div>
                <div>模式:<b>${mobile ? '手机' : '桌面'}</b></div>
                <div>状态:<b id="sta">${rec.is_sleeping ? '休眠中' : (rec.running ? '运行中' : '已暂停')}</b></div>
                <div>下次:<b id="ctd">--</b>s</div>
                <div>计数:<span id="num">${rec[key]}</span>/<span id="limit">${limit}</span></div>
                <div>批次:<span id="batch">${rec.batch_count}</span>(<span id="batch_done">${rec.batch_searches_done}</span>/<span id="batch_size">${rec.batch_size}</span>)</div>
                <div class="bar"><div class="fill" id="fill"></div></div>
                <div style="font-size:9px">运行:<span id="time">00:00:00</span></div>
                <div class="err" id="err"></div>
            </div>
            <div id="settings" style="margin-top:6px;padding-top:6px;border-top:1px solid #eee;">
                <div><label>桌面<input type="number" id="max_pc" value="${rec.max_pc}"> </label><label>手机<input type="number" id="max_ph" value="${rec.max_ph}"></label><button id="save">保存</button></div>
            </div>
            <div style="font-size:9px;text-align:right">v${VER}</div>`;
        document.body.appendChild(box);
        const drag = document.getElementById('drag');
        let d = false, sx = 0, sy = 0, lx = 0, ly = 0;
        drag.onmousedown = e => { if (e.button !== 0) return; d = true; sx = e.clientX; sy = e.clientY; lx = box.offsetLeft; ly = box.offsetTop; document.onmousemove = ev => { if (!d) return; box.style.left = (lx + ev.clientX - sx) + 'px'; box.style.top = (ly + ev.clientY - sy) + 'px'; }; document.onmouseup = () => { d = false; document.onmousemove = document.onmouseup = null; }; };
        drag.ontouchstart = e => { d = true; sx = e.touches[0].clientX; sy = e.touches[0].clientY; lx = box.offsetLeft; ly = box.offsetTop; document.ontouchmove = ev => { if (!d) return; box.style.left = (lx + ev.touches[0].clientX - sx) + 'px'; box.style.top = (ly + ev.touches[0].clientY - sy) + 'px'; }; document.ontouchend = () => { d = false; document.ontouchmove = document.ontouchend = null; }; };
        document.getElementById('run').onclick = toggle;
        document.getElementById('clr').onclick = () => { rec.pc = rec.ph = rec.batch_count = rec.batch_searches_done = rec.batch_size = 0; GM_setValue('bru_lite_v17_final', rec); updateUI(); };
        document.getElementById('save').onclick = () => { const newPc = +document.getElementById('max_pc').value || rec.max_pc; const newPh = +document.getElementById('max_ph').value || rec.max_ph; if (newPc > 0) rec.max_pc = newPc; if (newPh > 0) rec.max_ph = newPh; GM_setValue('bru_lite_v17_final', rec); limit = mobile ? rec.max_ph : rec.max_pc; updateUI(); document.getElementById('save').textContent = '已保存'; tSet(() => document.getElementById('save').textContent = '保存', 1500); };
    }

    /* ===== UI 渲染 ===== */
    const start = Date.now();
    function updateUI(t = '--') {
        const panel = document.getElementById(ID);
        if (!panel) { buildPanel(); return; }
        const elements = {
            sta: document.getElementById('sta'),
            ctd: document.getElementById('ctd'),
            num: document.getElementById('num'),
            limit: document.getElementById('limit'),
            batch: document.getElementById('batch'),
            batch_done: document.getElementById('batch_done'),
            batch_size: document.getElementById('batch_size'),
            fill: document.getElementById('fill'),
            time: document.getElementById('time'),
            err: document.getElementById('err')
        };
        if (Object.values(elements).some(el => !el)) { console.warn('UI elements missing, rebuilding panel'); buildPanel(); return; }
        elements.sta.textContent = rec.is_sleeping ? '休眠中' : (rec.running ? '运行中' : '已暂停');
        elements.ctd.textContent = rec.running ? t : '--';
        elements.num.textContent = rec[key];
        elements.limit.textContent = limit;
        elements.batch.textContent = rec.batch_count;
        elements.batch_done.textContent = rec.batch_searches_done;
        elements.batch_size.textContent = rec.batch_size;
        elements.fill.style.transform = `scaleX(${Math.min(rec[key] / limit, 1)})`;
        const d = Date.now() - start;
        elements.time.textContent = `${z2(Math.floor(d / 3600000))}:${z2(Math.floor((d % 3600000) / 60000))}:${z2(Math.floor((d % 60000) / 1000))}`;
        if (HOT.length === FALL.length) elements.err.style.display = 'block';
    }

    /* ===== 小工具 ===== */
    function waitSearchDelay() {
        return Math.random() * (SEARCH_DELAY.max - SEARCH_DELAY.min) + SEARCH_DELAY.min;
    }

    function waitSleepDuration() {
        return Math.random() * (SLEEP_DURATION.max - SLEEP_DURATION.min) + SLEEP_DURATION.min;
    }

    function getBatchSize() {
        return Math.floor(Math.random() * (BATCH.max - BATCH.min + 1)) + BATCH.min;
    }

    async function preSearchHesitation() {
        await new Promise(r => tSet(r, 600 + Math.random() * 1400));
    }

    async function typeHuman(inp, str) {
        inp.focus();
        inp.value = '';
        for (const c of str) {
            inp.value += c;
            inp.dispatchEvent(new Event('input', { bubbles: true }));
            if (c === ' ') {
                await new Promise(r => tSet(r, 120 + Math.random() * 150));
            }
            await new Promise(r => tSet(r, TYPE.min + Math.random() * (TYPE.max - TYPE.min)));
        }
    }

    async function advancedSoftScroll() {
        const h = document.body.scrollHeight - innerHeight;
        if (h <= 0) return;
        const scroll = (y, t) => new Promise(r => tSet(() => { window.scrollTo({ top: y, behavior: 'smooth' }); r(); }, t));
        await scroll(h * 0.3, 300);
        await scroll(h * 0.6, 400);
        if (Math.random() < 0.3) await scroll(h * 0.5, 300);
        await scroll(h, 500);
    }

    /* ===== 主循环 & 搜索动作 ===== */
    let loopTimer = 0, ctdTimer = 0;

    async function doSearch() {
        try {
            await preSearchHesitation();
            const kw = HOT[Math.random() * HOT.length | 0];
            if (!kw) return;
            let inp = null;
            for (const sel of SELECTORS) { inp = document.querySelector(sel); if (inp) break; }
            if (inp) {
                await typeHuman(inp, kw);
                ['keydown', 'keypress', 'keyup'].forEach(evt => inp.dispatchEvent(new KeyboardEvent(evt, { key: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true })));
                const form = inp.closest('form');
                if (form) form.submit();
                tSet(() => { if (location.pathname === '/' || location.pathname === '') location.href = `https://www.bing.com/search?q=${encodeURIComponent(kw)}`; }, 800);
            } else {
                location.href = `https://www.bing.com/search?q=${encodeURIComponent(kw)}`;
            }
            rec[key]++;
            rec.batch_searches_done++;
            GM_setValue('bru_lite_v17_final', rec);
            await advancedSoftScroll();
            updateUI();
        } catch (e) {
            console.error('[BR]', e);
            const errEl = document.getElementById('err');
            if (errEl) {
                errEl.style.display = 'block';
                errEl.textContent = '搜索失败,请检查!';
            }
            stop();
        }
    }

    function loop() {
        if (!rec.running) return;
        if (rec[key] >= limit) {
            stop();
            return;
        }
        if (rec.is_sleeping) {
            const sleepTime = waitSleepDuration();
            let c = Math.round(sleepTime / 1000);
            updateUI(c);
            ctdTimer = iSet(() => {
                if (!rec.running || !rec.is_sleeping) {
                    iClr(ctdTimer);
                    return;
                }
                updateUI(--c);
            }, 1000);
            loopTimer = tSet(() => {
                iClr(ctdTimer);
                rec.is_sleeping = false;
                rec.batch_searches_done = 0;
                rec.batch_count++;
                rec.batch_size = getBatchSize();
                GM_setValue('bru_lite_v17_final', rec);
                loop();
            }, sleepTime);
        } else {
            if (rec.batch_searches_done >= rec.batch_size) {
                rec.is_sleeping = true;
                GM_setValue('bru_lite_v17_final', rec);
                loop();
                return;
            }
            const w = waitSearchDelay();
            let c = Math.round(w / 1000);
            updateUI(c);
            ctdTimer = iSet(() => {
                if (!rec.running || rec.is_sleeping) {
                    iClr(ctdTimer);
                    return;
                }
                updateUI(--c);
            }, 1000);
            loopTimer = tSet(async () => {
                iClr(ctdTimer);
                await doSearch();
                loop();
            }, w);
        }
    }

    function startLoop() {
        if (rec[key] >= limit) return;
        rec.running = true;
        if (rec.batch_size === 0) {
            rec.batch_size = getBatchSize();
            rec.batch_count = 1;
        }
        GM_setValue('bru_lite_v17_final', rec);
        loop();
    }

    function stop() {
        rec.running = false;
        rec.is_sleeping = false;
        GM_setValue('bru_lite_v17_final', rec);
        tClr(loopTimer);
        iClr(ctdTimer);
        updateUI();
    }

    function toggle() {
        if (rec.running) {
            stop();
        } else {
            startLoop();
        }
        const runBtn = document.getElementById('run');
        if (runBtn) runBtn.textContent = rec.running ? '暂停' : '启动';
    }

    /* ===== 页面加载 ===== */
    (function () {
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            initScript();
        } else {
            window.addEventListener('load', initScript, { once: true });
        }

        async function initScript() {
            await fetchHot();
            buildPanel();
            updateUI();
            if (rec.running) startLoop();
        }
    })();
})();