// ==UserScript==
// @name Bing Rewards Lite (Win & Mac)
// @version 1.7.2
// @description v1.7.2 最终版 | 智能计算轮数,UI优化,逻辑加固,增加耗时预估
// @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.2';
const STORAGE_KEY = 'bru_lite_v172_final_version';
const TYPE = { min: 35, max: 80 };
const FALL = ['今日新闻', '天气预报', '电影票房', '体育比分', '股票行情'];
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' ];
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, searches_per_batch: 4,
sleep_min: 600000, sleep_max: 900000, search_delay_min: 120000, search_delay_max: 240000,
batch_count: 0, batch_searches_done: 0, is_sleeping: false
};
let rec = { ...def, ...GM_getValue(STORAGE_KEY, {}) };
if (rec.date !== today) {
const oldSettings = { ...rec };
rec = { ...def, date: today, pc: 0, ph: 0 };
for (const key of ['max_pc', 'max_ph', 'searches_per_batch', 'sleep_min', 'sleep_max', 'search_delay_min', 'search_delay_max']) {
if (oldSettings[key] !== undefined) rec[key] = oldSettings[key];
}
}
GM_setValue(STORAGE_KEY, 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 loopTimer = 0, ctdTimer = 0;
let HOT = [];
let startTime = Date.now();
/* ===== 核心函数 ===== */
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 (e) {
console.error("BR Lite: Hotlist fetch failed for", url, e);
}
}
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]); }
}
}
walk(obj?.data ?? obj); return words;
}
function startLoop() {
if (rec[key] >= limit) { return stop(); }
rec.running = true;
if (rec.batch_count === 0) rec.batch_count = 1;
GM_setValue(STORAGE_KEY, rec);
const runBtn = document.getElementById('run');
if (runBtn) runBtn.textContent = '暂停';
updateUI();
loop();
}
function stop() {
rec.running = false;
rec.is_sleeping = false;
tClr(loopTimer); iClr(ctdTimer);
GM_setValue(STORAGE_KEY, rec);
const runBtn = document.getElementById('run');
if (runBtn) runBtn.textContent = '启动';
updateUI();
}
function toggle() {
if (rec.running) { stop(); } else { startLoop(); }
}
function updateEstimate() {
const estimateEl = document.getElementById('estimate');
if (!estimateEl) return;
const searchesLeft = limit - rec[key];
if (searchesLeft <= 0) { estimateEl.textContent = '已完成'; return; }
const searchesPerBatch = rec.searches_per_batch;
if (searchesPerBatch <= 0) { estimateEl.textContent = '无效设置'; return; }
const avgSearchMs = (rec.search_delay_min + rec.search_delay_max) / 2;
const avgSleepMs = (rec.sleep_min + rec.sleep_max) / 2;
const numBatchesLeft = Math.ceil(searchesLeft / searchesPerBatch);
const numSleepsLeft = Math.max(0, numBatchesLeft - 1);
const totalMs = (searchesLeft * avgSearchMs) + (numSleepsLeft * avgSleepMs);
const totalMinutes = Math.round(totalMs / 60000);
if (totalMinutes < 1) { estimateEl.textContent = '< 1 分钟'; } else { estimateEl.textContent = `约 ${totalMinutes} 分钟`; }
}
function updateUI(t = '--') {
const panel = document.getElementById(ID); if (!panel) return;
const elements = { sta: document.getElementById('sta'), ctd: document.getElementById('ctd'), num: document.getElementById('num'), limit: document.getElementById('limit'), batch_current: document.getElementById('batch_current'), batch_total: document.getElementById('batch_total'), batch_done: document.getElementById('batch_done'), searches_per_batch_display: document.getElementById('searches_per_batch_display'), fill: document.getElementById('fill'), time: document.getElementById('time') };
if (Object.values(elements).some(el => !el)) return;
const totalBatches = rec.searches_per_batch > 0 ? Math.ceil(limit / rec.searches_per_batch) : 1;
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_current.textContent = rec.batch_count;
elements.batch_total.textContent = totalBatches;
elements.batch_done.textContent = rec.batch_searches_done;
elements.searches_per_batch_display.textContent = rec.searches_per_batch;
elements.fill.style.transform = `scaleX(${limit > 0 ? Math.min(rec[key] / limit, 1) : 0})`;
const d = Date.now() - startTime;
elements.time.textContent = `${z2(Math.floor(d/36e5))}:${z2(Math.floor(d%36e5/6e4))}:${z2(Math.floor(d%6e4/1e3))}`;
updateEstimate();
}
const waitSearchDelay = () => Math.random() * (rec.search_delay_max - rec.search_delay_min) + rec.search_delay_min;
const waitSleepDuration = () => Math.random() * (rec.sleep_max - rec.sleep_min) + rec.sleep_min;
async function doSearch() {
try {
await new Promise(r => tSet(r, 600 + Math.random() * 1400));
const kw = HOT[Math.random() * HOT.length | 0]; if (!kw) { console.error("BR Lite: No keyword found!"); return; }
let inp = null;
for (const sel of SELECTORS) { if (inp = document.querySelector(sel)) break; }
if (inp) {
inp.focus(); inp.value = '';
for (const c of kw) {
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)));
}
['keydown', 'keypress', 'keyup'].forEach(evt => inp.dispatchEvent(new KeyboardEvent(evt, {key:'Enter',keyCode:13,which:13,bubbles:true,cancelable:true})));
inp.closest('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(STORAGE_KEY, rec);
const h = document.body.scrollHeight - innerHeight;
if (h > 0) {
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);
}
updateUI();
} catch (e) { console.error('[BR]', e); stop(); }
}
function loop() {
if (!rec.running || rec[key] >= limit) { return stop(); }
const totalBatches = rec.searches_per_batch > 0 ? Math.ceil(limit / rec.searches_per_batch) : (limit > 0 ? 1 : 0);
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++; GM_setValue(STORAGE_KEY, rec); loop();
}, sleepTime);
} else {
if (rec.batch_searches_done >= rec.searches_per_batch) {
if (rec.batch_count < totalBatches && rec[key] < limit) {
rec.is_sleeping = true; GM_setValue(STORAGE_KEY, rec); loop();
} else { stop(); }
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);
}
}
/* ===== 面板UI及事件绑定 ===== */
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"></b></div>
<div>下次:<b id="ctd"></b>s</div>
<div>计数:<span id="num"></span>/<span id="limit"></span></div>
<div>轮次: <span id="batch_current"></span>/<span id="batch_total"></span> (<span id="batch_done"></span>/<span id="searches_per_batch_display"></span>)</div>
<div class="bar"><div class="fill" id="fill"></div></div>
<div style="font-size:9px">运行:<span id="time"></span></div>
<div style="font-size:9px">预计剩余:<span id="estimate">--</span></div>
</div>
<div id="settings" style="margin-top:6px;padding-top:6px;border-top:1px solid #eee; font-size:10px;">
<div><label>总次数 <input type="number" id="total_searches" value="${limit}"> </label><label>每轮次数 <input type="number" id="searches_per_batch_input" value="${rec.searches_per_batch}"></label></div>
<div style="margin-top:4px;"><label>搜索间隔(秒) <input type="number" id="search_delay_min" value="${rec.search_delay_min / 1000}"> - <input type="number" id="search_delay_max" value="${rec.search_delay_max / 1000}"></label></div>
<div style="margin-top:4px;"><label>轮次休眠(分) <input type="number" id="sleep_min" value="${rec.sleep_min / 60000}"> - <input type="number" id="sleep_max" value="${rec.sleep_max / 60000}"></label></div>
<div style="margin-top:6px;"><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 = () => {
stop();
rec = { ...rec, pc: 0, ph: 0, batch_count: 1, batch_searches_done: 0 };
startTime = Date.now();
GM_setValue(STORAGE_KEY, rec);
updateUI();
};
document.getElementById('save').onclick = () => {
const wasRunning = rec.running;
if (wasRunning) stop();
const getVal = (id, fallback) => parseInt(document.getElementById(id).value) || fallback;
const total = getVal('total_searches', limit);
if (mobile) { rec.max_ph = total; } else { rec.max_pc = total; }
let spb = getVal('searches_per_batch_input', rec.searches_per_batch);
if (spb < 1) spb = 1;
rec.searches_per_batch = spb;
rec.search_delay_min = getVal('search_delay_min', rec.search_delay_min / 1000) * 1000;
rec.search_delay_max = getVal('search_delay_max', rec.search_delay_max / 1000) * 1000;
rec.sleep_min = getVal('sleep_min', rec.sleep_min / 60000) * 60000;
rec.sleep_max = getVal('sleep_max', rec.sleep_max / 60000) * 60000;
limit = total;
if (wasRunning) { rec.batch_searches_done = 0; rec.batch_count = 1;}
GM_setValue(STORAGE_KEY, rec);
const saveBtn = document.getElementById('save');
if (saveBtn) { saveBtn.textContent = '已保存'; tSet(() => saveBtn.textContent = '保存设置', 1500); }
updateUI();
if (wasRunning) startLoop();
};
}
/* ===== 页面加载 ===== */
(async function () {
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 initScript() {
buildPanel();
setTimeout(() => {
updateUI();
if (rec.running) startLoop();
}, 200);
}
await fetchHot();
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initScript();
} else {
window.addEventListener('load', initScript, { once: true });
}
})();
})();