// ==UserScript== // @name Microsoft Rewards 全能自动化助手 // @namespace https://scriptcat.org/ // @version 3.3.0 // @description ⚠ 本脚本可能违反 Microsoft Rewards 服务条款,存在封号风险,仅供学习研究,请自行承担后果。macOS 液态玻璃悬浮窗;移动 App 通道完成签到/阅读/每日任务/更多活动;PC 搜索用真实浏览器逐字输入模拟真人打字,词源聚合百度+知乎+微博三热搜源;OAuth 带 state 防 CSRF、Token 混淆存储(非加密)、进度持久化、防封暂停、精确计时。 // @author you // @license MIT // @match https://cn.bing.com/* // @match https://www.bing.com/* // @match https://rewards.bing.com/* // @match https://login.live.com/oauth20_desktop.srf* // @grant GM.xmlHttpRequest // @grant GM_xmlhttpRequest // @grant GM.getValue // @grant GM_getValue // @grant GM.setValue // @grant GM_setValue // @grant GM.deleteValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_notification // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_openInTab // @connect prod.rewardsplatform.microsoft.com // @connect login.live.com // @connect cn.bing.com // @connect www.bing.com // @connect top.baidu.com // @connect www.zhihu.com // @connect weibo.com // @run-at document-end // @noframes // ==/UserScript== /* * ⚠ 免责声明: * 本脚本自动操作 Microsoft Rewards,可能违反其服务条款,可能导致账号被封禁。 * 仅供学习、研究浏览器扩展/用户脚本技术使用。请自行评估并承担全部风险。 * 不要用于任何违反当地法律法规或平台规则的用途。 * * 安全说明: * - Token 仅做 XOR+Base64 混淆(非密码学加密),不能视为安全存储。 * - OAuth 使用 state 防 CSRF;PKCE 默认关闭(见 ENABLE_PKCE 注释)。 */ (function () { 'use strict'; /* ==================== 常量 ==================== */ const CFG = { clientId: '0000000040170455', scope: 'service::prod.rewardsplatform.microsoft.com::MBI_SSL', redirectUri: 'https://login.live.com/oauth20_desktop.srf', authUrl: 'https://login.live.com/oauth20_authorize.srf', tokenUrl: 'https://login.live.com/oauth20_token.srf', dapiBase: 'https://prod.rewardsplatform.microsoft.com/dapi/me', appId: 'SAAndroid/32.6.2110003560', channel: 'SAAndroid', country: 'cn', language: 'zh', rewardsHome: 'https://rewards.bing.com/', bingHome: 'https://cn.bing.com/', uaApp: 'Mozilla/5.0 (Linux; Android 16; Redmi K20 Pro Build/BP4A.251205.006; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Mobile Safari/537.36 BingSapphire/32.6.2110003560', uaPC: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0', }; /* * PKCE(code_challenge / code_verifier)默认关闭。 * 原因:本项目走旧版 login.live.com/oauth20_authorize.srf 端点,该端点不支持 * code_challenge(PKCE 仅在 login.microsoftonline.com 新端点支持)。强开会收到 * invalid_request 导致授权失败。state 防 CSRF 始终开启,已满足最低安全要求。 * 若你确认所用端点支持 PKCE,把下面的 false 改为 true 即可启用 S256。 */ const ENABLE_PKCE = false; // Token 混淆密钥(XOR + Base64,仅防肉眼,非密码学加密) const OBFUSCATION_KEY = 'MR#2024$Rewards!SALT'; const SEARCH_POOL = [ '天气预报', '今日新闻热点', '美食食谱家常菜', '旅游攻略', '健康养生知识', '科技资讯', '电影推荐', '股票行情', '体育赛事', '历史上的今天', '人工智能', '编程教程', '学习方法', '职场技能', '理财知识', 'how to learn rust programming', 'why is the sky blue', 'best noise cancelling headphones', 'easy chocolate chip cookies recipe', 'how do solar panels work', 'how to sleep better naturally', 'pomodoro technique for focus', 'healthy breakfast ideas', 'git rebase vs merge', 'how to start a vegetable garden', 'what is the weather forecast tomorrow', ]; const SKIP_PATTERNS = [ 'referral', 'refer and earn', 'sweepstake', 'entries', 'install the', 'set bing as your default', 'bing wallpaper', 'punch card', 'rewards extension', 'redemption goal', 'order history', 'claim your gift', 'shop to earn', 'set goal', 'available tomorrow', 'offer is locked', ]; // 中国区阅读 offerid 候选(动态查找失败时按序尝试) const CN_READ_OFFER_CANDIDATES = [ 'ZHCN_readarticle3_30points', 'ZHCN_readarticle_30points', 'ENUS_readarticle3_30points', 'ENUS_readarticle_30points', ]; const KEYS = { accessToken: 'mr32_access_token', refreshToken: 'mr32_refresh_token', expiresAt: 'mr32_expires_at', oauthState: 'mr32_oauth_state', pkceVerifier: 'mr32_pkce_verifier', settings: 'mr32_settings', progress: 'mr32_progress', pendingSearch: 'mr32_pending_search', }; const DEFAULT_SETTINGS = { sign: true, read: true, dailySet: true, moreActivities: true, pcSearch: true, forceRerun: false, randomOrder: false, readMax: 10, // 阅读最多连领篇数 searchIntervalMin: 3, // 搜索间隔(分钟) searchCap: 40, // 单轮搜索上限(次) pauseEveryN: 5, // 每 N 次搜索暂停一次 pauseMinutes: 10, // 暂停分钟数 }; const SETTING_LIMITS = { readMax: [1, 30], searchIntervalMin: [1, 120], searchCap: [5, 200], pauseEveryN: [1, 50], pauseMinutes: [1, 180], }; const TASK_LABELS = { sign: '每日签到', read: '每日阅读', dailySet: '每日任务', moreActivities: '更多活动', pcSearch: 'PC 搜索', }; const TASK_ORDER = ['sign', 'dailySet', 'read', 'moreActivities', 'pcSearch']; const CACHE_TTL_MS = 30 * 1000; const MAX_LOG_ENTRIES = 500; const PENDING_VALID_MS = 5 * 60 * 1000; const SEARCH_TAB_NAME = 'MR_REWARDS_SEARCH_TAB'; const TOKEN_EXPIRE_MARGIN_MS = 60 * 1000; const MAX_RETRY = 3; const REAUTH_COOLDOWN_MS = 30 * 1000; /* ==================== 工具函数 ==================== */ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; const randSleep = (min = 2000, max = 5000) => sleep(randInt(min, max)); const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n)); const shuffle = (arr) => { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; }; function todayStr() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } function formatDuration(ms) { if (ms <= 0) return '0秒'; const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); if (m >= 60) return `${Math.floor(m / 60)}小时${m % 60}分`; if (m > 0) return `${m}分${s % 60}秒`; return `${s}秒`; } /** 兼容 boolean / "true" / "false" 字符串 */ function toBool(v) { if (typeof v === 'boolean') return v; return String(v).toLowerCase() === 'true'; } function uuid() { try { if (crypto && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, '').toUpperCase(); } catch (e) { console.warn('[Rewards助手] randomUUID 不可用,回退 Math.random:', e); } let s = ''; for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16); return s.toUpperCase(); } const activityId = () => (uuid() + uuid()).slice(0, 64); const safeJson = (text, fallback = null) => { try { return JSON.parse(text); } catch (e) { return fallback; } }; function randomString(len = 64) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; const buf = new Uint8Array(len); try { crypto.getRandomValues(buf); } catch (e) { console.warn('[Rewards助手] getRandomValues 不可用,回退 Math.random:', e); for (let i = 0; i < len; i++) buf[i] = Math.floor(Math.random() * 256); } let out = ''; for (let i = 0; i < len; i++) out += chars[buf[i] % chars.length]; return out; } async function sha256Base64Url(str) { const data = new TextEncoder().encode(str); const digest = await crypto.subtle.digest('SHA-256', data); const bytes = new Uint8Array(digest); let bin = ''; bytes.forEach((b) => { bin += String.fromCharCode(b); }); return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } /* ---------- 混淆(仅防肉眼,非加密) ---------- */ function obfuscate(str) { if (!str) return ''; try { let bytes = ''; for (let i = 0; i < str.length; i++) { bytes += String.fromCharCode(str.charCodeAt(i) ^ OBFUSCATION_KEY.charCodeAt(i % OBFUSCATION_KEY.length)); } return btoa(bytes); } catch (e) { console.warn('[Rewards助手] obfuscate 失败,返回原文:', e); return str; } } function deobfuscate(enc) { if (!enc) return ''; try { const bytes = atob(enc); let out = ''; for (let i = 0; i < bytes.length; i++) { out += String.fromCharCode(bytes.charCodeAt(i) ^ OBFUSCATION_KEY.charCodeAt(i % OBFUSCATION_KEY.length)); } return out; } catch (e) { console.warn('[Rewards助手] deobfuscate 失败,返回原文:', e); return enc; } } /* ==================== 存储(兼容新旧 API) ==================== */ const Store = { get(key, def) { try { return GM_getValue(key, def); } catch (e) { console.warn('[Rewards助手] GM_getValue 失败:', e); return def; } }, set(key, val) { try { GM_setValue(key, val); } catch (e) { console.warn('[Rewards助手] GM_setValue 失败:', e); } }, delete(key) { if (typeof GM_deleteValue === 'function') { try { GM_deleteValue(key); return; } catch (e) { console.warn('[Rewards助手] GM_deleteValue 失败:', e); } } if (typeof GM.deleteValue === 'function') { try { GM.deleteValue(key); return; } catch (e) { console.warn('[Rewards助手] GM.deleteValue 失败:', e); } } try { GM_setValue(key, null); } catch (e) { console.warn('[Rewards助手] Store.delete 失败:', e); } }, async getAsync(key, def) { if (typeof GM.getValue === 'function') { try { return await GM.getValue(key, def); } catch (e) { console.warn('[Rewards助手] GM.getValue 失败:', e); } } return this.get(key, def); }, async setAsync(key, val) { if (typeof GM.setValue === 'function') { try { await GM.setValue(key, val); return; } catch (e) { console.warn('[Rewards助手] GM.setValue 失败:', e); } } this.set(key, val); }, }; /* ---------- Token 混淆存取 ---------- */ const TokenStore = { getAccess() { return deobfuscate(Store.get(KEYS.accessToken, '')); }, setAccess(t) { Store.set(KEYS.accessToken, obfuscate(t)); }, getRefresh() { return deobfuscate(Store.get(KEYS.refreshToken, '')); }, setRefresh(t) { Store.set(KEYS.refreshToken, obfuscate(t)); }, getExpiry() { return Number(Store.get(KEYS.expiresAt, 0)) || 0; }, setExpiry(ts) { Store.set(KEYS.expiresAt, ts); }, }; function getSettings() { const stored = Store.get(KEYS.settings, {}) || {}; const merged = { ...DEFAULT_SETTINGS, ...stored }; Object.keys(SETTING_LIMITS).forEach((k) => { if (merged[k] === undefined || merged[k] === null) return; const [lo, hi] = SETTING_LIMITS[k]; merged[k] = clamp(Number(merged[k]) || DEFAULT_SETTINGS[k], lo, hi); }); return merged; } function saveSettings(s) { Store.set(KEYS.settings, s); } function getProgress() { const p = Store.get(KEYS.progress, null); if (!p || p.date !== todayStr()) { return { date: todayStr(), completedTasks: [], pcSearchCount: 0, todayPoints: 0, lastBalance: 0, pausedUntil: 0, lastPauseAt: 0, }; } return p; } function saveProgress(p) { Store.set(KEYS.progress, p); } /* ==================== 日志(限量防泄漏) ==================== */ function log(msg) { console.log('[Rewards助手] ' + msg); const el = document.getElementById('mr-log-area'); if (!el) return; const line = document.createElement('div'); line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; el.appendChild(line); while (el.childElementCount > MAX_LOG_ENTRIES) el.removeChild(el.firstElementChild); el.scrollTop = el.scrollHeight; } function notify(text, title = 'Rewards 助手') { try { if (typeof GM_notification !== 'function') return; GM_notification({ title, text, timeout: 5000 }); } catch (e) { try { GM_notification({ title, text }); } catch (e2) { console.warn('[Rewards助手] GM_notification 不可用:', e2); } } } /* ==================== 等待元素(MutationObserver) ==================== */ function waitForElement(selector, timeoutMs = 10000, root = document) { return new Promise((resolve) => { const existing = root.querySelector(selector); if (existing) return resolve(existing); let observer = null; const timer = setTimeout(() => { if (observer) observer.disconnect(); resolve(null); }, timeoutMs); observer = new MutationObserver((_, obs) => { const el = root.querySelector(selector); if (el) { clearTimeout(timer); obs.disconnect(); resolve(el); } }); observer.observe(root, { childList: true, subtree: true }); }); } /* ==================== HTTP(现代/旧 API 兼容) ==================== */ /** 兼容不同平台响应字段:优先 responseText,回退 response */ function respText(resp) { if (resp && typeof resp.responseText === 'string') return resp.responseText; if (resp && typeof resp.response === 'string') return resp.response; if (resp && resp.response != null) { try { return JSON.stringify(resp.response); } catch (e) { return String(resp.response); } } return ''; } function makeHttpError(resp) { const snippet = respText(resp).slice(0, 200); return new Error(`HTTP ${resp.status} ${resp.statusText || ''} | ${snippet}`); } /** * Promise 化请求:优先现代 GM.xmlHttpRequest,回退旧 GM_xmlhttpRequest。 * @returns {Promise<{status:number,statusText:string,responseText:string,response:any}>} */ function httpRequest(opts) { const { onload, onerror, ontimeout, ...rest } = opts; const merged = { timeout: 20000, ...rest }; if (typeof GM.xmlHttpRequest === 'function') { return GM.xmlHttpRequest(merged).then((resp) => { if (resp.status >= 200 && resp.status < 300) return resp; throw makeHttpError(resp); }); } if (typeof GM_xmlhttpRequest === 'function') { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...merged, onload: (resp) => { if (resp.status >= 200 && resp.status < 300) resolve(resp); else reject(makeHttpError(resp)); }, onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')), }); }); } return Promise.reject(new Error('GM.xmlHttpRequest 不可用')); } /* ==================== 授权(state + 可选 PKCE) ==================== */ const Auth = { async get(force = false) { const at = TokenStore.getAccess(); const exp = TokenStore.getExpiry(); if (!force && at && Date.now() < exp - TOKEN_EXPIRE_MARGIN_MS) return at; const refresh = TokenStore.getRefresh(); if (!force && refresh) { log('🔄 尝试刷新 Token...'); if (await this._request({ grant_type: 'refresh_token', refresh_token: refresh, scope: CFG.scope })) { return TokenStore.getAccess(); } log('🟡 Refresh Token 失效,需要重新授权'); } return null; }, async _request(params) { const body = new URLSearchParams({ client_id: CFG.clientId, redirect_uri: CFG.redirectUri, ...params, }).toString(); let lastErr = null; for (let attempt = 1; attempt <= MAX_RETRY; attempt++) { try { const resp = await httpRequest({ method: 'POST', url: CFG.tokenUrl, headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: body, timeout: 15000, }); const data = safeJson(respText(resp)); if (!data) throw new Error('响应非 JSON'); if (data.access_token) { TokenStore.setAccess(data.access_token); if (data.refresh_token) TokenStore.setRefresh(data.refresh_token); TokenStore.setExpiry(Date.now() + (data.expires_in || 3600) * 1000); return true; } if (['invalid_grant', 'invalid_request', 'invalid_client', 'unauthorized_client'].includes(data.error)) { if (data.error === 'invalid_grant') TokenStore.setRefresh(''); log(`🟡 Token 请求被拒: ${data.error} ${data.error_description || ''}`); return false; } lastErr = new Error(data.error_description || data.error || '未知错误'); } catch (e) { lastErr = e; } if (attempt < MAX_RETRY) await sleep(2000 * attempt); } log(`🔴 Token 请求失败: ${lastErr ? lastErr.message : '未知'}`); return false; }, async exchangeCode(code, codeVerifier = '') { const params = { grant_type: 'authorization_code', code }; if (codeVerifier) params.code_verifier = codeVerifier; return this._request(params); }, async openAuthPage() { const state = randomString(32); Store.set(KEYS.oauthState, state); const params = new URLSearchParams({ client_id: CFG.clientId, response_type: 'code', scope: CFG.scope, redirect_uri: CFG.redirectUri, state, }); if (ENABLE_PKCE) { try { const verifier = randomString(64); const challenge = await sha256Base64Url(verifier); Store.set(KEYS.pkceVerifier, verifier); params.set('code_challenge', challenge); params.set('code_challenge_method', 'S256'); } catch (e) { log('⚠ PKCE 计算失败,跳过(保留 state)'); Store.delete(KEYS.pkceVerifier); } } const url = `${CFG.authUrl}?${params.toString()}`; const ok = openInTab(url, true); if (!ok) log('⚠ 无法打开授权页,请手动复制链接打开'); else log('🟡 已打开授权页,登录完成后脚本会自动捕获授权码'); }, }; /** 打开标签页:优先 GM_openInTab(先试对象参数,再试 boolean),回退 window.open */ function openInTab(url, active = true) { try { if (typeof GM_openInTab === 'function') { try { GM_openInTab(url, { active }); return true; } catch (e1) { GM_openInTab(url, active); return true; } } } catch (e) { console.warn('[Rewards助手] GM_openInTab 失败:', e); } try { const w = window.open(url, '_blank'); if (w) { if (!active) w.blur(); return true; } } catch (e) { console.warn('[Rewards助手] window.open 失败:', e); } return false; } /** 等待窗口 load(跨域无法监听时退化为固定等待) */ function waitWindowLoad(w, timeoutMs = 8000) { return new Promise((resolve) => { if (!w || w.closed) return resolve(); try { if (w.document && w.document.readyState === 'complete') return resolve(); w.addEventListener('load', () => resolve(), { once: true }); setTimeout(resolve, timeoutMs); } catch (e) { // 跨域窗口无法访问 document/addEventListener,退化为固定等待 setTimeout(resolve, 8000); } }); } /** 打开卡片目标页并尽量等待加载完成(“先访问再领取”) */ async function visitCardUrl(url) { let w = null; try { w = window.open(url, '_blank'); } catch (e) { console.warn('[Rewards助手] 打开卡片页失败:', e); w = null; } if (w) { await waitWindowLoad(w, 10000); try { w.blur(); } catch (e) { /* ignore */ } return true; } try { if (typeof GM_openInTab === 'function') { GM_openInTab(url, { active: true }); log(' ⚠ window.open 被拦截,改用 GM_openInTab,等待 10 秒'); await sleep(10000); return true; } } catch (e) { console.warn('[Rewards助手] 打开卡片页失败:', e); } return false; } /* ==================== DAPI(移动 App 通道 + 缓存) ==================== */ function parseCounter(field) { if (Array.isArray(field)) { return field.reduce((acc, it) => ({ progress: acc.progress + Number(it.pointProgress ?? it.progress ?? 0), max: acc.max + Number(it.pointProgressMax ?? it.pointMax ?? it.max ?? 0), }), { progress: 0, max: 0 }); } if (field && typeof field === 'object') { return { progress: Number(field.pointProgress ?? field.progress ?? 0), max: Number(field.pointProgressMax ?? field.pointMax ?? field.max ?? 0), }; } return { progress: 0, max: 0 }; } let dashboardCache = { ts: 0, value: null }; const Rewards = { async dapi(path = '', { method = 'GET', body } = {}) { const token = await Auth.get(); if (!token) throw new Error('无有效 Token'); const headers = { 'content-type': 'application/json; charset=UTF-8', 'user-agent': CFG.uaApp, 'authorization': `Bearer ${token}`, 'x-rewards-appid': CFG.appId, 'x-rewards-ismobile': 'true', 'x-rewards-country': CFG.country, 'x-rewards-language': CFG.language, }; try { return await httpRequest({ method, url: CFG.dapiBase + path, headers, data: body ? JSON.stringify(body) : undefined, }); } catch (e) { if (/401/.test(e.message)) { log('🟡 DAPI 401,Token 已失效,正在打开授权页重新授权...'); TokenStore.setRefresh(''); TokenStore.setAccess(''); const now = Date.now(); if (now - reAuthTriggeredAt > REAUTH_COOLDOWN_MS) { reAuthTriggeredAt = now; Auth.openAuthPage(); } } throw e; } }, /** 一次请求同时产出配额 + promotions,30 秒缓存,消除重复请求 */ async dashboard(force = false) { const now = Date.now(); if (!force && dashboardCache.value && now - dashboardCache.ts < CACHE_TTL_MS) { return dashboardCache.value; } const resp = await this.dapi('?channel=SAAndroid&options=613'); const data = safeJson(respText(resp)); if (!data || !data.response) throw new Error('DAPI 返回异常'); const r = data.response; const value = { pc: parseCounter(r.counters ? r.counters.pcSearch : null), mobile: parseCounter(r.counters ? r.counters.mobileSearch : null), balance: Number(r.balance || 0), promotions: r.promotions || [], raw: data, }; dashboardCache = { ts: now, value }; return value; }, async getQuota(force = false) { const d = await this.dashboard(force); return { pc: d.pc, mobile: d.mobile, balance: d.balance, raw: d.raw }; }, async getPromotions(force = false) { const d = await this.dashboard(force); return d.promotions; }, /** * type 101 领取(urlreward / checkin / readarticle 等)。 * @param {string} offerId * @param {boolean} withChannel 阅读任务传 false,其余保持 true */ async claim101(offerId, withChannel = true) { const body = { amount: 1, country: CFG.country, id: activityId(), type: 101, attributes: { offerid: offerId }, }; if (withChannel) body.channel = CFG.channel; const resp = await this.dapi('/activities', { method: 'POST', body }); const data = safeJson(respText(resp)) || {}; const r = data.response || {}; return { points: Number(r.activity && (r.activity.p || r.activity.points) || 0), duplicate: !!r.isDuplicate, raw: r, }; }, /** type 103 每日签入(必应 App 连签) */ async signIn() { const body = { amount: 1, id: activityId(), type: 103, country: CFG.country, channel: CFG.channel, attributes: {}, }; const resp = await this.dapi('/activities', { method: 'POST', body }); const data = safeJson(respText(resp)) || {}; const r = data.response || {}; return { points: Number(r.activity && (r.activity.p || r.activity.points) || 0), duplicate: !!r.isDuplicate, raw: r, rawText: respText(resp), }; }, }; /* ==================== 热搜源(三源聚合) ==================== */ async function fetchFromSource(src) { const resp = await httpRequest({ url: src.url, headers: { 'User-Agent': CFG.uaPC, 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Referer': src.url.split('/').slice(0, 3).join('/') + '/', }, timeout: 8000, }); return src.parse(safeJson(respText(resp))) || []; } async function fetchHotwords() { const sources = [ { name: '百度热搜', url: 'https://top.baidu.com/api/board?platform=wise&tab=realtime', parse: (d) => (d?.data?.cards || []).flatMap((c) => (c.content || []).map((i) => i.word || i.query || i.title).filter(Boolean)), }, { name: '知乎热榜', url: 'https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50', parse: (d) => (d?.data || []).map((i) => i.target?.title || i.target?.titleArea?.text).filter(Boolean), }, { name: '微博热搜', url: 'https://weibo.com/ajax/side/hotSearch', parse: (d) => (d?.data?.realtime || []).map((i) => i.word).filter(Boolean), }, ]; const merged = []; for (const src of sources) { try { const words = await fetchFromSource(src); if (words && words.length) { merged.push(...words); log(` ${src.name} 获取 ${words.length} 条`); } } catch (e) { log(` ${src.name} 失败: ${e.message}`); } } return [...new Set(merged)]; } async function getHotQueries(count) { let queries = []; try { queries = await fetchHotwords(); if (queries.length) log(`🔥 三源聚合热搜 ${queries.length} 条`); } catch (e) { log('⚠ 热搜获取失败,使用内置词库'); } if (!queries.length) { queries = shuffle(SEARCH_POOL); log('📚 使用内置词库'); } while (queries.length < count) queries.push(SEARCH_POOL[queries.length % SEARCH_POOL.length]); return [...new Set(queries)].slice(0, count); } /* ==================== 逐字输入模拟真人打字 ==================== */ function setPendingSearch(query) { Store.set(KEYS.pendingSearch, JSON.stringify({ query, ts: Date.now() })); } function fireKeyEvent(target, type, ch) { const keyCode = ch.length === 1 ? ch.charCodeAt(0) : 0; const code = /^[a-zA-Z]$/.test(ch) ? `Key${ch.toUpperCase()}` : (ch === ' ' ? 'Space' : ''); target.dispatchEvent(new KeyboardEvent(type, { key: ch, code, keyCode, which: keyCode, bubbles: true, cancelable: true, })); } async function handlePendingSearch(raw) { let pending; try { pending = JSON.parse(raw); } catch (e) { console.warn('[Rewards助手] 待搜索数据解析失败:', e); Store.delete(KEYS.pendingSearch); return; } if (!pending || Date.now() - pending.ts > PENDING_VALID_MS) { Store.delete(KEYS.pendingSearch); return; } const query = pending.query; Store.delete(KEYS.pendingSearch); const box = await waitForElement('#sb_form_q', 10000); if (!box) { location.href = `${CFG.bingHome}search?q=${encodeURIComponent(query)}&form=QBLH`; return; } box.focus(); box.value = ''; box.dispatchEvent(new Event('input', { bubbles: true })); for (const ch of query) { box.value += ch; box.dispatchEvent(new Event('input', { bubbles: true })); fireKeyEvent(box, 'keydown', ch); fireKeyEvent(box, 'keypress', ch); fireKeyEvent(box, 'keyup', ch); await sleep(randInt(60, 200)); } await sleep(randInt(300, 700)); const form = document.getElementById('sb_form'); if (form && typeof form.requestSubmit === 'function') { form.requestSubmit(); } else { const btn = document.querySelector('#sb_form input[type="submit"], #sb_form button[type="submit"], #search_icon, .search.icon.tooltip'); if (btn) btn.click(); else location.href = `${CFG.bingHome}search?q=${encodeURIComponent(query)}&form=QBLH`; } } /* ==================== 任务:分类与领取 ==================== */ function classifyPromotion(p) { const a = p.attributes || {}; const offerId = String(a.offerid || ''); const lowerId = offerId.toLowerCase(); const title = String(p.name || a.title || p.description || '').toLowerCase(); const type = String(a.type || '').toLowerCase(); if (lowerId.includes('checkin') || lowerId.includes('check-in') || lowerId.includes('dailycheckin') || type === 'checkin') return 'checkin'; if (lowerId.includes('readarticle') || lowerId.includes('msnread') || lowerId.includes('readtoearn') || type === 'msnreadearn') return 'read'; if (SKIP_PATTERNS.some((s) => `${title} ${lowerId}`.includes(s))) return 'skip'; if (type === 'urlreward') return 'dailySet'; return 'moreActivities'; } /** * 扫描并分类所有未完成的活动卡片。 * @returns {Promise<{dailySet:Array, moreActivities:Array, checkin:Array, read:Array}>} */ async function discoverCards(force = false) { const result = { dailySet: [], moreActivities: [], checkin: [], read: [] }; const seen = new Set(); let promos; try { promos = await Rewards.getPromotions(force); } catch (e) { log(`🟡 获取 promotions 失败: ${e.message}`); return result; } for (const p of promos) { const a = p.attributes || {}; const offerId = String(a.offerid || ''); if (!offerId) continue; if (toBool(a.complete)) continue; if (seen.has(offerId)) continue; seen.add(offerId); const cat = classifyPromotion(p); if (cat === 'skip') continue; const card = { offerId, title: String(p.name || a.title || p.description || ''), type: String(a.type || '').toLowerCase(), url: String(a.destinationUrl || a.url || ''), progress: parseInt(a.progress) || 0, max: parseInt(a.max) || 1, }; result[cat].push(card); } return result; } /** * 领取单个卡片:带重试;返回 0 且带目标 URL 时先访问页面再重试。 */ async function claimCard(card, claimedSet = null) { for (let attempt = 1; attempt <= MAX_RETRY; attempt++) { try { const r = await Rewards.claim101(card.offerId); if (r.points > 0) { if (claimedSet) claimedSet.add(card.offerId); return { ok: true, points: r.points, duplicate: false }; } if (r.duplicate) { if (claimedSet) claimedSet.add(card.offerId); return { ok: true, points: 0, duplicate: true }; } if (attempt === 1 && card.url) { log(` ↳ ${card.title || card.offerId} 需先访问页面`); await visitCardUrl(card.url); continue; } await sleep(randInt(2000, 4000)); } catch (e) { log(` ⚠ ${card.title || card.offerId} 第${attempt}次失败: ${e.message}`); if (attempt < MAX_RETRY) await sleep(randInt(3000, 6000)); } } return { ok: false, points: 0, duplicate: false }; } async function claimCards(cards, label, runner = null) { if (!cards.length) { log(`🧩 ${label}:无待处理项`); return { ok: true, points: 0 }; } log(`🧩 ${label}:发现 ${cards.length} 项`); const claimedSet = runner ? runner.claimedOfferIds : new Set(); let ok = 0, gained = 0; for (const card of cards) { if (claimedSet.has(card.offerId)) continue; const r = await claimCard(card, claimedSet); if (r.ok) { ok++; gained += r.points; log(` ✓ ${card.title || card.offerId} ${r.duplicate ? '已完成' : `+${r.points}`}`); } await randSleep(); } log(`🔵 ${label}:完成 ${ok} 项,+${gained} 积分`); return { ok: ok > 0, points: gained }; } /* ==================== 任务:签到 / 阅读 / 每日任务 / 更多活动 ==================== */ async function taskSignIn() { let total = 0; // 1) App 端签入(type 103) try { const r = await Rewards.signIn(); if (r.points > 0) { total += r.points; log(`📱 App 连签 +${r.points}`); } else if (r.duplicate) log('📱 App 今日已签入'); else log(`📱 App 签入未加分,回包: ${String(r.rawText || '').slice(0, 200)}`); } catch (e) { log(`🟡 App 签入失败: ${e.message}`); } await randSleep(); // 2) 网页端签到(type 101) const cards = await discoverCards(); if (cards.checkin.length) { const r = await claimCards(cards.checkin, '网页签到'); total += r.points; } else { try { const w = await Rewards.claim101('Gamification_Sapphire_DailyCheckIn'); if (w.points > 0) { total += w.points; log(`📖 网页签到 +${w.points}`); } else log('📖 网页签到:今日已完成'); } catch (e) { log(`🟡 网页签到失败: ${e.message}`); } } return { ok: total > 0, points: total }; } /** 动态查找阅读任务(含进度),找不到返回 null */ async function findReadOffer() { try { const promos = await Rewards.getPromotions(); const candidates = promos.filter((p) => { const a = p.attributes || {}; const id = String(a.offerid || '').toLowerCase(); const t = String(a.type || '').toLowerCase(); return id.includes('readarticle') || id.includes('msnread') || id.includes('readtoearn') || t === 'msnreadearn'; }); const open = candidates.find((p) => !toBool((p.attributes || {}).complete)); const pick = open || candidates[0]; if (pick && pick.attributes && pick.attributes.offerid) { const a = pick.attributes; return { offerId: a.offerid, progress: parseInt(a.progress) || 0, max: parseInt(a.max) || 30 }; } } catch (e) { console.warn('[Rewards助手] findReadOffer 失败:', e); } return null; } async function taskRead() { const settings = getSettings(); const read = await findReadOffer(); let offerId = read ? read.offerId : null; let cur = read ? read.progress : 0; let max = read ? read.max : 30; let gained = 0; if (!offerId) { log('⚠ 未在 promotions 中找到阅读任务,尝试中国区候选 offerid'); for (const cand of CN_READ_OFFER_CANDIDATES) { try { const r = await Rewards.claim101(cand, false); if (r.points > 0) { offerId = cand; cur = 1; // 探测已领第 1 篇,计入进度,避免重复领取 max = 30; gained += r.points; log(`📖 命中候选 offerid=${cand},已领第 1 篇 +${r.points}`); break; } if (r.duplicate) { log(`📖 候选 offerid=${cand} 已完成`); return { ok: true, points: 0 }; } } catch (e) { console.warn('[Rewards助手] 候选阅读 offerid 探测失败:', cand, e); } } if (!offerId) { log('🟡 未找到有效阅读 offerid,跳过'); return { ok: false, points: 0 }; } } if (cur >= max) { if (gained > 0) log(`🔵 阅读完成 ${cur}/${max}(本次 +${gained})`); else log(`📖 阅读已完成 ${cur}/${max}`); return { ok: gained > 0, points: gained }; } log(`📖 阅读进度 ${cur}/${max}(offerid=${offerId})`); const limit = Math.min(max - cur, settings.readMax || 10); for (let i = 0; i < limit; i++) { try { const r = await Rewards.claim101(offerId, false); if (r.duplicate) { log('📖 阅读已达上限'); break; } if (r.points > 0) { gained += r.points; cur++; log(`📖 阅读第 ${cur} 篇 +${r.points}`); } else break; } catch (e) { log(`🟡 阅读请求失败: ${e.message}`); break; } await randSleep(); } if (gained > 0) log(`🔵 阅读完成 ${cur}/${max}(本次 +${gained})`); else log('🟡 本次阅读未获得积分'); return { ok: gained > 0, points: gained }; } async function taskDailySet(runner) { const cards = await discoverCards(); return claimCards(cards.dailySet, '每日任务', runner); } async function taskMoreActivities(runner) { const cards = await discoverCards(); return claimCards(cards.moreActivities, '更多活动', runner); } /* ==================== 任务:PC 搜索 ==================== */ /** 打开/复用搜索标签页;弹窗被拦截时降级 GM_openInTab 并停止后续重复打开 */ function openSearchTab(runner, url) { if (runner.searchWindow && !runner.searchWindow.closed) { try { runner.searchWindow.location.href = url; return true; } catch (e) { console.warn('[Rewards助手] 搜索标签页导航失败,尝试重开:', e); } } if (runner.popupBlocked) return false; // 已知被拦截,不再重试 window.open let w = null; try { w = window.open(url, SEARCH_TAB_NAME); } catch (e) { console.warn('[Rewards助手] window.open 失败:', e); w = null; } if (w) { runner.searchWindow = w; return true; } try { if (typeof GM_openInTab === 'function') { const tab = GM_openInTab(url, { active: false }); runner.gmTab = tab || null; // 平台可能返回 tab 对象/ID,尽量记录 runner.popupBlocked = true; log('⚠ window.open 被拦截,已改用 GM_openInTab 打开本次搜索页'); return false; } } catch (e) { console.warn('[Rewards助手] GM_openInTab 失败:', e); } runner.popupBlocked = true; log('🔴 搜索标签页被拦截:请在地址栏允许本站弹窗后重试'); return false; } async function doPause(runner, searches, minutes) { const until = Date.now() + minutes * 60 * 1000; runner.progress.pausedUntil = until; runner.progress.lastPauseAt = searches; saveProgress(runner.progress); log(`⏸ 已搜索 ${searches} 次,自动暂停 ${minutes} 分钟防封...`); notify(`已搜索 ${searches} 次,暂停 ${minutes} 分钟防封`); const ok = await runner.waitUntil(until); if (ok) { runner.progress.pausedUntil = 0; saveProgress(runner.progress); log('▶ 暂停结束,继续搜索'); } return ok; } async function taskSearch(runner) { const settings = getSettings(); let progress = 0, max = 0; try { const q = await Rewards.getQuota(); progress = q.pc.progress; max = q.pc.max; uiState.balance = q.balance; updateUI(); } catch (e) { log(`⚠ 无法读取搜索配额: ${e.message}`); } const initialProgress = progress; if (!max) { max = 30; log('⚠ 未读到 PC 搜索配额,按默认 30 次执行'); } if (progress >= max) { log(`✅ PC 搜索已完成 ${progress}/${max}`); return { ok: true, points: 0 }; } log(`🔍 PC 搜索进度 ${progress}/${max}`); const spanMs = Math.max(1, settings.searchIntervalMin) * 60 * 1000; const cap = settings.searchCap; const pauseEveryN = settings.pauseEveryN; const pauseMinutes = settings.pauseMinutes; const queries = await getHotQueries(Math.max(cap + 5, max + 5)); let roundDone = 0, last = progress, stagnant = 0; for (const query of queries) { if (!runner.running) break; if (progress >= max) break; if (roundDone >= cap) { log(`🔴 已达单轮搜索上限 ${cap} 次,停止`); break; } // 防封暂停(基于持久化的全局计数,重启不丢) const totalSearches = runner.progress.pcSearchCount; if (totalSearches > 0 && totalSearches % pauseEveryN === 0 && runner.progress.lastPauseAt !== totalSearches) { if (!await doPause(runner, totalSearches, pauseMinutes)) break; } log(`🔍 搜索 (${roundDone + 1}): ${query}`); setPendingSearch(query); const opened = openSearchTab(runner, CFG.bingHome); if (!opened) { // 弹窗被拦截且无法复用标签页:避免堆积过多标签页,停止后续搜索 log('🔴 无法复用搜索标签页(弹窗被拦截)。已由 GM_openInTab 打开本次搜索,为避免堆积标签页后续停止。请允许本站弹窗后重跑。'); break; } roundDone++; runner.progress.pcSearchCount++; saveProgress(runner.progress); if (!await runner.wait(spanMs)) break; if (roundDone % 2 === 0) { try { const q = await Rewards.getQuota(true); // 强制刷新 if (q.pc.max > 0) { progress = q.pc.progress; max = q.pc.max; log(`↻ 服务端进度 ${progress}/${max}(已搜索 ${roundDone} 次)`); if (progress <= last) { stagnant++; if (stagnant >= 3) { log('🔴 进度连续停滞,提前结束'); break; } } else { stagnant = 0; last = progress; } } } catch (e) { console.warn('[Rewards助手] 搜索进度回读失败:', e); } } } try { const q = await Rewards.getQuota(true); progress = q.pc.progress; if (q.pc.max > 0) max = q.pc.max; uiState.balance = q.balance; updateUI(); } catch (e) { console.warn('[Rewards助手] 搜索结束配额读取失败:', e); } const gained = Math.max(0, progress - initialProgress); log(`🔵 PC 搜索结束:${progress}/${max}(本次 +${gained})`); return { ok: gained > 0 || progress >= max, points: gained }; } /* ==================== 任务执行器 ==================== */ class TaskRunner { constructor(preOpenedSearchWindow = null) { this.running = false; this.settings = getSettings(); this.progress = getProgress(); this.searchWindow = preOpenedSearchWindow; this.popupBlocked = false; // window.open 是否已被拦截 this.gmTab = null; // GM_openInTab 返回(tab 对象/ID,尽量记录) this.claimedOfferIds = new Set(); // 本轮已领 offer,收尾复查去重 this.points = 0; } async wait(ms) { const end = Date.now() + ms; while (Date.now() < end) { if (!this.running) return false; updateCountdown(end - Date.now()); await sleep(Math.min(500, end - Date.now())); } updateCountdown(0); return this.running; } async waitUntil(ts) { while (Date.now() < ts) { if (!this.running) return false; updateCountdown(ts - Date.now()); await sleep(Math.min(1000, ts - Date.now())); } updateCountdown(0); return this.running; } async runAll() { this.running = true; try { const token = await Auth.get(); if (!token) { log('🔴 无有效 Token,正在打开授权页...'); await Auth.openAuthPage(); return; } log('🟢 Token 就绪'); let balance = 0; try { balance = (await Rewards.getQuota()).balance; } catch (e) { console.warn('[Rewards助手] 初始余额读取失败:', e); } log(`📊 初始余额:${balance}`); uiState.balance = balance; updateUI(); let order = TASK_ORDER.filter((t) => this.settings[t]); if (this.settings.randomOrder) order = shuffle(order); uiState.total = order.length; uiState.done = 0; updateUI(); log(`📋 执行顺序:${order.map((t) => TASK_LABELS[t]).join(' → ')}`); for (const taskId of order) { if (!this.running) break; // 跳过已完成任务(可 forceRerun 强制重跑) if (this.progress.completedTasks.includes(taskId) && !this.settings.forceRerun) { log(`⏭ 跳过已完成:${TASK_LABELS[taskId]}`); uiState.done++; updateUI(); continue; } log(`▶ 开始:${TASK_LABELS[taskId]}`); let res = { ok: false, points: 0 }; try { if (taskId === 'sign') res = await taskSignIn(); else if (taskId === 'read') res = await taskRead(); else if (taskId === 'dailySet') res = await taskDailySet(this); else if (taskId === 'moreActivities') res = await taskMoreActivities(this); else if (taskId === 'pcSearch') res = await taskSearch(this); } catch (e) { log(`🟡 ${TASK_LABELS[taskId]} 异常: ${e.message}`); } if (res.ok && !this.progress.completedTasks.includes(taskId)) { this.progress.completedTasks.push(taskId); } this.points += res.points || 0; this.progress.todayPoints = this.points; saveProgress(this.progress); uiState.done++; uiState.points = this.points; updateUI(); await randSleep(); } // 收尾复查(强制刷新,去重) log('💎 收尾复查...'); const finalCards = await discoverCards(true); const remaining = [...finalCards.dailySet, ...finalCards.moreActivities]; await claimCards(remaining, '收尾领取', this); try { balance = (await Rewards.getQuota(true)).balance; } catch (e) { console.warn('[Rewards助手] 余额读取失败:', e); } this.progress.lastBalance = balance; saveProgress(this.progress); uiState.balance = balance; updateUI(); log(`🎉 执行完毕:今日 +${this.points},余额 ${balance}`); notify(`执行完毕:今日 +${this.points},余额 ${balance}`); } catch (e) { log(`🔴 任务异常: ${e.message}`); } finally { this.running = false; uiState.running = false; if (currentRunner === this) currentRunner = null; try { if (this.searchWindow && !this.searchWindow.closed) this.searchWindow.close(); } catch (e) { console.warn('[Rewards助手] 关闭搜索标签页失败:', e); } updateUI(); } } stop() { this.running = false; } } /* ==================== UI 状态 ==================== */ let currentRunner = null; let floatVisible = true; let styleInjected = false; let reAuthTriggeredAt = 0; let menuCommandIds = []; let uiState = { running: false, total: 0, done: 0, points: 0, balance: 0 }; function updateUI() { const btn = document.getElementById('mr-btn-start'); if (btn) { btn.textContent = uiState.running ? '⏹ 停止执行' : '▶ 开始执行'; btn.style.background = uiState.running ? 'linear-gradient(135deg,#ff5f6d,#ffc371)' : 'linear-gradient(135deg,#0a84ff,#64d2ff)'; } const bar = document.getElementById('mr-progress-bar'); const txt = document.getElementById('mr-progress-text'); if (bar && txt) { const pct = uiState.total > 0 ? Math.round((uiState.done / uiState.total) * 100) : 0; bar.style.width = pct + '%'; txt.textContent = `${uiState.done} / ${uiState.total} 任务 (${pct}%)`; } const st = document.getElementById('mr-stats'); if (st) st.textContent = `💰 今日 +${uiState.points} | 🏆 余额 ${uiState.balance}`; } function updateCountdown(ms) { const el = document.getElementById('mr-countdown'); if (el) el.textContent = ms > 0 ? `⏳ 等待中:${formatDuration(ms)}` : ''; } /* ==================== 诊断 ==================== */ async function dumpData() { log('🔬 正在抓取服务端真实数据...'); try { const token = await Auth.get(); if (!token) { log('🔴 无 Token,请先点「开始执行」完成授权'); return; } const q = await Rewards.getQuota(true); log(` 余额 ${q.balance} | PC 搜索 ${q.pc.progress}/${q.pc.max} | 移动搜索 ${q.mobile.progress}/${q.mobile.max}`); const promos = await Rewards.getPromotions(true); log(` 共 ${promos.length} 个任务:`); promos.forEach((p) => { const a = p.attributes || {}; log(` · [${a.type || '?'}] ${p.name || a.title || a.offerid} | offerid=${a.offerid || '-'} | complete=${a.complete} | ${a.progress || 0}/${a.max || 0}`); }); try { await navigator.clipboard.writeText(JSON.stringify(q.raw, null, 2)); log(' 📋 完整 JSON 已复制到剪贴板'); } catch (e) { log(' ⚠ 复制失败,请按 F12 从控制台复制'); } console.log('[Rewards助手] DAPI 原始数据', q.raw); } catch (e) { log('🔴 抓取失败: ' + e.message); } } /* ==================== 悬浮窗(macOS 液态玻璃) ==================== */ const FLOAT_CSS = ` #mr-float-container{ position:fixed;top:20px;right:20px;z-index:999999;width:390px; max-height:calc(100vh - 40px);display:flex;flex-direction:column; font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",sans-serif; font-size:13px;color:#1c1c1e; background:linear-gradient(135deg,rgba(255,255,255,0.62),rgba(255,255,255,0.30)); backdrop-filter:blur(40px) saturate(180%); -webkit-backdrop-filter:blur(40px) saturate(180%); border:1px solid rgba(255,255,255,0.55);border-radius:18px; box-shadow:0 10px 40px rgba(0,0,0,0.18),inset 0 1px 0 rgba(255,255,255,0.6); overflow:hidden;} #mr-float-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px; cursor:move;background:rgba(255,255,255,0.35);border-bottom:1px solid rgba(255,255,255,0.3);} #mr-float-header .title{font-weight:600;font-size:14px;} #mr-float-header .btns{display:flex;gap:4px;} #mr-float-header .btns span{cursor:pointer;opacity:0.55;padding:2px 7px;border-radius:7px;font-size:14px;line-height:1;} #mr-float-header .btns span:hover{opacity:1;background:rgba(128,128,128,0.15);} #mr-float-body{padding:14px 16px;overflow-y:auto;overflow-x:hidden;flex:1 1 auto;min-height:0;} #mr-float-body::-webkit-scrollbar{width:5px;} #mr-float-body::-webkit-scrollbar-thumb{background:rgba(128,128,128,0.3);border-radius:3px;} #mr-warning{font-size:10px;line-height:1.5;color:#b35900;background:rgba(255,149,0,0.12); border:1px solid rgba(255,149,0,0.3);border-radius:8px;padding:6px 8px;margin-bottom:8px;} #mr-progress-text{font-size:11px;opacity:0.7;text-align:center;margin-bottom:6px;} #mr-progress-track{height:6px;background:rgba(0,0,0,0.08);border-radius:3px;overflow:hidden;} #mr-progress-bar{height:100%;width:0;background:linear-gradient(90deg,#0a84ff,#30d158);border-radius:3px;transition:width .4s;} #mr-countdown{font-size:11px;text-align:center;padding:5px 0;color:#ff9f0a;font-weight:500;min-height:18px;} #mr-log-area{height:140px;overflow-y:auto;font-size:11px;line-height:1.6;padding:8px 10px;margin:8px 0; background:rgba(255,255,255,0.35);border:1px solid rgba(255,255,255,0.3);border-radius:10px; font-family:"SF Mono",Menlo,Consolas,monospace;} #mr-log-area::-webkit-scrollbar{width:4px;} #mr-log-area::-webkit-scrollbar-thumb{background:rgba(128,128,128,0.3);border-radius:2px;} #mr-stats{font-size:11px;opacity:0.8;text-align:center;padding:4px 0;} .mr-btn{display:block;width:100%;padding:9px 16px;border:none;border-radius:12px;font-size:13px;font-weight:500; cursor:pointer;color:#fff;background:linear-gradient(135deg,#0a84ff,#64d2ff);margin-top:7px; box-shadow:0 2px 8px rgba(10,132,255,0.35);transition:transform .15s ease,filter .15s ease;} .mr-btn:hover{filter:brightness(1.08);transform:translateY(-1px);} .mr-btn.sec{background:rgba(255,255,255,0.4);color:#1c1c1e;box-shadow:none;border:1px solid rgba(255,255,255,0.4);} .mr-btn.sec:hover{background:rgba(255,255,255,0.6);} .mr-row{display:flex;align-items:center;justify-content:space-between;padding:5px 0;font-size:12px;} .mr-row label{opacity:0.85;} .mr-row input[type=number]{width:58px;padding:4px 7px;border:1px solid rgba(0,0,0,0.12);border-radius:8px;font-size:12px;background:rgba(255,255,255,0.5);} .mr-row input[type=checkbox]{width:16px;height:16px;accent-color:#0a84ff;} #mr-settings{display:none;margin-top:10px;padding-top:10px;border-top:1px solid rgba(255,255,255,0.3);} #mr-settings.show{display:block;} .mr-sec-title{font-size:10px;font-weight:600;opacity:0.5;text-transform:uppercase;letter-spacing:.5px;margin:6px 0 2px;} `; function createFloatWindow() { const settings = getSettings(); if (!styleInjected) { GM_addStyle(FLOAT_CSS); styleInjected = true; } const container = document.createElement('div'); container.id = 'mr-float-container'; container.innerHTML = `
🎯 Rewards 助手
─ ✕
⚠ 本脚本可能违反 Microsoft Rewards 服务条款,存在封号风险,仅供学习研究,请自行承担后果。Token 仅做混淆存储(非加密)。
0 / 0 任务
💰 今日 +0 | 🏆 余额 0
任务开关
搜索设置
`; document.body.appendChild(container); updateUI(); // 关闭:运行中则询问是否停止 document.getElementById('mr-btn-close').onclick = () => { if (currentRunner && currentRunner.running) { if (window.confirm('任务仍在运行,是否同时停止?\n「取消」仅隐藏面板,任务继续后台运行。')) { currentRunner.stop(); uiState.running = false; } } container.remove(); floatVisible = false; refreshMenuCommands(); }; let minimized = false; document.getElementById('mr-btn-min').onclick = () => { minimized = !minimized; document.getElementById('mr-float-body').style.display = minimized ? 'none' : 'block'; }; document.getElementById('mr-btn-start').onclick = () => { if (currentRunner && currentRunner.running) { currentRunner.stop(); uiState.running = false; log('⏹ 正在停止任务...'); updateUI(); } else { const s = getSettings(); let preWin = null; // 同步预开搜索标签页,避免异步 window.open 被拦截 if (s.pcSearch) { try { preWin = window.open('about:blank', SEARCH_TAB_NAME); } catch (e) { console.warn('[Rewards助手] 预开搜索标签页失败:', e); preWin = null; } if (!preWin) log('⚠ 搜索标签页被拦截:请在地址栏允许本站弹窗,否则 PC 搜索无法执行'); } startRunner(preWin); } }; document.getElementById('mr-btn-dump').onclick = () => dumpData(); document.getElementById('mr-btn-settings').onclick = () => document.getElementById('mr-settings').classList.toggle('show'); document.getElementById('mr-btn-save').onclick = () => { const num = (id, key) => { const n = parseInt(document.getElementById(id).value, 10); const [lo, hi] = SETTING_LIMITS[key]; return clamp(Number.isFinite(n) ? n : DEFAULT_SETTINGS[key], lo, hi); }; saveSettings({ sign: document.getElementById('s-sign').checked, read: document.getElementById('s-read').checked, dailySet: document.getElementById('s-dailyset').checked, moreActivities: document.getElementById('s-more').checked, pcSearch: document.getElementById('s-pc').checked, forceRerun: document.getElementById('s-force').checked, randomOrder: document.getElementById('s-random').checked, readMax: num('s-readmax', 'readMax'), searchIntervalMin: num('s-span', 'searchIntervalMin'), searchCap: num('s-cap', 'searchCap'), pauseEveryN: num('s-pausen', 'pauseEveryN'), pauseMinutes: num('s-pausemin', 'pauseMinutes'), }); log('💾 设置已保存'); }; document.getElementById('mr-btn-clear-progress').onclick = () => { Store.delete(KEYS.progress); log('🧹 已清除今日进度'); notify('今日进度已清除'); }; document.getElementById('mr-btn-reset-settings').onclick = () => { Store.delete(KEYS.settings); log('♻ 已重置设置,下次打开生效'); notify('设置已重置'); }; makeDraggable(container, document.getElementById('mr-float-header')); refreshMenuCommands(); } /** 拖拽:Pointer Events + setPointerCapture(支持移出窗口、触摸) */ function makeDraggable(el, handle) { let dragging = false; let startX = 0, startY = 0, origLeft = 0, origTop = 0; const down = (e) => { dragging = true; startX = e.clientX; startY = e.clientY; const rect = el.getBoundingClientRect(); origLeft = rect.left; origTop = rect.top; try { if (handle.setPointerCapture) handle.setPointerCapture(e.pointerId); } catch (err) { /* 部分环境不支持 pointer capture */ } if (e.cancelable) e.preventDefault(); }; const move = (e) => { if (!dragging) return; el.style.left = (origLeft + (e.clientX - startX)) + 'px'; el.style.top = (origTop + (e.clientY - startY)) + 'px'; el.style.right = 'auto'; }; const up = (e) => { dragging = false; try { if (handle.releasePointerCapture) handle.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ } }; if (window.PointerEvent) { handle.addEventListener('pointerdown', down); handle.addEventListener('pointermove', move); handle.addEventListener('pointerup', up); handle.addEventListener('pointercancel', up); } else { handle.addEventListener('mousedown', (e) => down({ ...e, pointerId: 1 })); document.addEventListener('mousemove', (e) => move({ ...e })); document.addEventListener('mouseup', (e) => up({ ...e, pointerId: 1 })); handle.addEventListener('touchstart', (e) => { const t = e.touches[0]; down({ ...e, pointerId: 1, clientX: t.clientX, clientY: t.clientY }); }, { passive: false }); handle.addEventListener('touchmove', (e) => { const t = e.touches[0]; move({ ...e, clientX: t.clientX, clientY: t.clientY }); e.preventDefault(); }, { passive: false }); handle.addEventListener('touchend', (e) => up({ ...e, pointerId: 1 })); } } /* ==================== 启动器 ==================== */ function startRunner(preWin = null) { if (currentRunner && currentRunner.running) { log('🟡 任务已在运行中'); return; } const runner = new TaskRunner(preWin); currentRunner = runner; uiState.running = true; updateUI(); runner.runAll(); } /* ==================== ScriptCat 菜单 ==================== */ function registerMenuCommandSafe(label, fn) { if (typeof GM_registerMenuCommand !== 'function') return; try { const id = GM_registerMenuCommand(label, fn); if (id !== undefined && id !== null) menuCommandIds.push(id); } catch (e) { console.warn('[Rewards助手] 菜单注册失败:', label, e); } } function unregisterAllMenuCommands() { if (typeof GM_unregisterMenuCommand === 'function') { for (const id of menuCommandIds) { try { GM_unregisterMenuCommand(id); } catch (e) { console.warn('[Rewards助手] 菜单注销失败:', id, e); } } } menuCommandIds = []; } function toggleFloatPanel() { const el = document.getElementById('mr-float-container'); if (el) { floatVisible = !floatVisible; el.style.display = floatVisible ? '' : 'none'; refreshMenuCommands(); } else { createFloatWindow(); // 内部会 refreshMenuCommands floatVisible = true; log('悬浮面板已重新创建'); } } function refreshMenuCommands() { unregisterAllMenuCommands(); registerMenuCommandSafe(floatVisible ? '👁 隐藏悬浮面板' : '👁 显示悬浮面板', toggleFloatPanel); registerMenuCommandSafe('🚀 立即运行 Rewards', () => startRunner(null)); registerMenuCommandSafe('⏹ 停止任务', () => { if (currentRunner && currentRunner.running) { currentRunner.stop(); log('⏹ 已停止'); } else log('🟡 当前无运行中的任务'); }); registerMenuCommandSafe('🔬 抓取 Rewards 数据', () => dumpData()); registerMenuCommandSafe('🔑 手动授权', () => Auth.openAuthPage()); registerMenuCommandSafe('🧹 清除今日进度', () => { Store.delete(KEYS.progress); log('已清除今日进度'); }); registerMenuCommandSafe('♻ 重置设置', () => { Store.delete(KEYS.settings); log('已重置设置'); }); registerMenuCommandSafe('🧼 清除 Token', () => { TokenStore.setRefresh(''); TokenStore.setAccess(''); TokenStore.setExpiry(0); alert('Token 已清除,下次运行需重新授权'); }); } /* ==================== 授权回调 ==================== */ async function handleAuthRedirect() { const url = new URL(location.href); const error = url.searchParams.get('error'); if (error) { notify('授权被拒绝:' + error); return; } const code = url.searchParams.get('code'); if (!code) return; const state = url.searchParams.get('state'); const savedState = Store.get(KEYS.oauthState, ''); if (savedState && (!state || state !== savedState)) { notify('⚠ state 校验失败,可能存在 CSRF 攻击,已中止授权'); return; } const verifier = Store.get(KEYS.pkceVerifier, ''); const ok = await Auth.exchangeCode(code, verifier); if (ok) { Store.delete(KEYS.oauthState); Store.delete(KEYS.pkceVerifier); notify('授权成功!可关闭此页'); setTimeout(() => { location.href = CFG.rewardsHome; }, 1200); } else { notify('授权码交换 Token 失败,请重试'); } } /* ==================== 入口 ==================== */ function main() { if (location.hostname === 'login.live.com' && location.pathname === '/oauth20_desktop.srf') { handleAuthRedirect(); return; } if (location.hostname === 'cn.bing.com' || location.hostname === 'www.bing.com') { const pending = Store.get(KEYS.pendingSearch, ''); if (pending) { const run = () => handlePendingSearch(pending); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); else run(); return; } if (location.pathname === '/' || location.pathname === '') { const init = () => { if (document.body && !document.getElementById('mr-float-container')) createFloatWindow(); else if (!document.body) setTimeout(init, 300); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); } return; } if (location.hostname === 'rewards.bing.com') { const init = () => { if (!document.body) { setTimeout(init, 300); return; } if (document.getElementById('mr-float-container')) return; createFloatWindow(); log('助手已就绪。首次使用请点「▶ 开始执行」完成授权。'); log('点「🔬 抓取 Rewards 数据」可查看服务端真实任务清单。'); const p = getProgress(); if (p.completedTasks.length || p.pcSearchCount) { log(`🔄 今日进度:${p.completedTasks.length} 任务完成,PC 搜索 ${p.pcSearchCount} 次`); } }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); } } main(); })();