// ==UserScript== // @name Microsoft Rewards 全能自动化助手 // @namespace https://scriptcat.org/ // @version 4.1.0 // @description ⚠ 违反 Microsoft Rewards 服务条款,存在账号封号、积分清零风险,仅用于前端技术学习研究,禁止在正式账号运行。macOS 液态玻璃悬浮窗;移动 App 通道签到/阅读/每日任务/更多活动;PC 搜索真实浏览器逐字输入;百度+知乎+微博三源热搜(12h 本地缓存+熔断);OAuth state 防 CSRF、Token 高强度混淆(可逆非加密)、三级日志+导出、风控等级诊断、动态拟人节奏。v4.1.0:修复 ScriptCat 跨域 API 优先级、增加连通性自检、修正风控请求头、精简授权校验。 // @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_getValue // @grant GM_setValue // @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,违反微软服务协议,存在 ║ * ║ 账号封禁、积分清零风险,仅用于前端技术学习研究,禁止在 ║ * ║ 正式账号运行。使用即视为自愿承担全部后果。 ║ * ╚═══════════════════════════════════════════════════════════════╝ * * 更新日志: * v4.1.0 ScriptCat 跨域 API 优先级反转、连通性自检、修正 Sec-Fetch-Site、 * PKCE 自动降级、UA 轮换与版本校验、状态面板主动刷新、预计耗时提示。 * v4.0.0 架构重构:模块化、风控模拟、三级日志、熔断、分级容错、诊断面板。 * v3.3.0 菜单去重、阅读 channel、CSS 单次注入、401 自动重授权、Store.delete 兼容。 * v3.2.0 移动 App 通道、热搜三源、PC 真实搜索、state 防 CSRF、Token 混淆。 * * 适配说明:ScriptCat / Tampermonkey 4.x+,需授予全部 @grant 权限。 */ (function () { 'use strict'; /* ==================== 模块一:常量配置 ==================== */ const CONFIG = { 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', // 移动 App 身份(与 UA 的 BingSapphire 版本需一致) channel: 'SAAndroid', country: 'cn', language: 'zh', rewardsHome: 'https://rewards.bing.com/', bingHome: 'https://cn.bing.com/', // 必应 App 专用 UA(移动通道身份,保持稳定,不可随机化) 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', uaPcPool: [ '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', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', ], uaMobilePool: [ 'Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36 EdgA/131.0.0.0', 'Mozilla/5.0 (Linux; Android 14; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36', 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1', ], acceptLanguagePool: ['zh-CN,zh;q=0.9,en;q=0.8', 'zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.5'], }; /* * PKCE 模式:true / false / 'auto'(默认 auto)。 * - true:强制 S256。 * - false:关闭。 * - auto:先尝试带 code_challenge;若交换 code 时服务端返回 * unsupported_challenge_method,自动去掉 code_verifier 重试一次。 * 旧 login.live.com 端点可能不支持 PKCE,auto 模式可自适应新旧端点。 * state 防 CSRF 始终强制开启。 */ const ENABLE_PKCE = 'auto'; /** 从 GM_info 派生混淆密钥,避免硬编码(可逆,非密码学加密,仅防肉眼) */ function deriveKey() { const info = (typeof GM_info !== 'undefined') ? GM_info : null; const seed = (info && info.script && info.script.uuid) || `${(info && info.script && info.script.name) || 'rewards'}-${(info && info.script && info.script.version) || 'v4.1'}`; const salt = CONFIG.clientId; const src = seed + salt; let h = 0; for (let i = 0; i < src.length; i++) h = ((h << 5) - h + src.charCodeAt(i)) | 0; return Math.abs(h).toString(36) + '#Rewards'; } const OBF_KEYS = [deriveKey(), '9fKq2@Rewards#v4.0']; 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', ]; const CN_READ_OFFER_CANDIDATES = [ 'ZHCN_readarticle3_30points', 'ZHCN_readarticle_30points', 'ENUS_readarticle3_30points', 'ENUS_readarticle_30points', ]; // 存储键(mr32_* 保持向后兼容) 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', hotCache: 'mr32_hot_cache', hotFailures: 'mr32_hot_failures', logs: 'mr32_logs', envNotified: 'mr32_env_notified', }; 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, 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 CONST = { dashboardCacheTtl: 30 * 1000, hotCacheTtl: 12 * 60 * 60 * 1000, hotCircuitFailures: 2, hotCircuitCooldown: 30 * 60 * 1000, maxLogEntries: 500, maxLogHistory: 2000, pendingValidMs: 5 * 60 * 1000, searchTabName: 'MR_REWARDS_SEARCH_TAB', tokenExpireMargin: 60 * 1000, maxRetry: 3, reauthCooldown: 30 * 1000, requestTimeout: 20000, hotTimeout: 8000, tokenTimeout: 15000, rateLimitPerMin: 12, }; /* ==================== 模块二:工具函数 ==================== */ const Util = { sleep: (ms) => new Promise((r) => setTimeout(r, ms)), randInt: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, clamp: (n, lo, hi) => Math.min(hi, Math.max(lo, n)), pick: (arr) => arr[Math.floor(Math.random() * arr.length)], now: () => Date.now(), todayStr() { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }, formatDuration(ms) { if (ms <= 0) return '0秒'; const s = Math.floor(ms / 1000), m = Math.floor(s / 60); if (m >= 60) return `${Math.floor(m / 60)}小时${m % 60}分`; if (m > 0) return `${m}分${s % 60}秒`; return `${s}秒`; }, 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; }, toBool(v) { return typeof v === 'boolean' ? v : String(v).toLowerCase() === 'true'; }, safeJson(text, fallback = null) { try { return JSON.parse(text); } catch (e) { return fallback; } }, uuid() { try { if (crypto && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, '').toUpperCase(); } catch (e) { Logger.warn('randomUUID 不可用,回退 Math.random', e); } let s = ''; for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16); return s.toUpperCase(); }, activityId() { return (this.uuid() + this.uuid()).slice(0, 64); }, randomString(len = 64) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; const buf = new Uint8Array(len); try { crypto.getRandomValues(buf); } catch (e) { Logger.warn('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 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(/=+$/, ''); }, 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 ''; }, maskSecret(s) { return String(s).replace(/([?&](?:code|access_token|refresh_token|id_token|state)=)[^&\s"]+/gi, '$1***') .replace(/("?(?:access_token|refresh_token|id_token)"?\s*[:=]\s*")[^"]*(")/gi, '$1***$2') .replace(/\beyJ[A-Za-z0-9_-]{10,}\b/g, '***JWT***'); }, async idle(minMs = 800, maxMs = 3000) { await this.sleep(this.randInt(minMs, maxMs)); }, dynamicInterval(baseMs, index = 0, jitterRatio = 0.3) { const factor = 1 + (index % 6) * 0.04; const jitter = 1 - jitterRatio + Math.random() * 2 * jitterRatio; return Math.round(baseMs * factor * jitter); }, }; /* ==================== 模块三:日志 ==================== */ const Logger = { _history: [], info(msg) { this._emit('INFO', msg); }, warn(msg, err) { this._emit('WARN', msg, err); }, error(msg, err) { this._emit('ERROR', msg, err); }, _emit(level, msg, err) { const text = Util.maskSecret(String(msg)); const stamp = `[${new Date().toLocaleTimeString()}]`; const full = `[Rewards助手][${level}] ${text}`; if (level === 'ERROR') console.error(full, err || ''); else if (level === 'WARN') console.warn(full); else console.log(full); if (err && err.stack) console.debug(err.stack); const el = document.getElementById('mr-log-area'); if (el) { const line = document.createElement('div'); line.className = level === 'ERROR' ? 'mr-log-error' : level === 'WARN' ? 'mr-log-warn' : ''; line.textContent = `${stamp} [${level}] ${text}`; el.appendChild(line); while (el.childElementCount > CONST.maxLogEntries) el.removeChild(el.firstElementChild); el.scrollTop = el.scrollHeight; } this._history.push({ ts: Date.now(), level, text }); if (this._history.length > CONST.maxLogHistory) this._history.splice(0, this._history.length - CONST.maxLogHistory); try { Storage.set(KEYS.logs, this._history.slice(-CONST.maxLogHistory)); } catch (e) { /* 存储失败不影响主流程 */ } }, restore() { const saved = Storage.get(KEYS.logs, []); if (Array.isArray(saved)) this._history = saved.slice(-CONST.maxLogHistory); }, export() { const body = this._history.map((l) => `[${new Date(l.ts).toLocaleString()}] [${l.level}] ${l.text}`).join('\n'); const blob = new Blob([body], { type: 'text/plain;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `rewards-log-${Util.todayStr()}.log`; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); this.info('日志已导出'); }, }; /* ==================== 模块四:持久化存储(仅同步 API) ==================== */ const Storage = { get(key, def) { try { return GM_getValue(key, def); } catch (e) { Logger.warn('GM_getValue 失败', e); return def; } }, set(key, val) { try { GM_setValue(key, val); } catch (e) { Logger.warn('GM_setValue 失败', e); } }, delete(key) { if (typeof GM_deleteValue === 'function') { try { GM_deleteValue(key); return; } catch (e) { Logger.warn('GM_deleteValue 失败', e); } } try { GM_setValue(key, null); } catch (e) { Logger.warn('Store.delete 失败', e); } }, }; // Token 混淆(双密钥异或 + 反转,可逆,非密码学加密,仅防肉眼) const TokenCipher = { obfuscate(str) { if (!str) return ''; try { let s = str; for (let r = 0; r < OBF_KEYS.length; r++) { const key = OBF_KEYS[r]; let out = ''; for (let i = 0; i < s.length; i++) out += String.fromCharCode(s.charCodeAt(i) ^ key.charCodeAt(i % key.length)); s = out; } s = s.split('').reverse().join(''); return btoa(s); } catch (e) { Logger.warn('obfuscate 失败,返回原文', e); return str; } }, deobfuscate(enc) { if (!enc) return ''; try { let s = atob(enc).split('').reverse().join(''); for (let r = OBF_KEYS.length - 1; r >= 0; r--) { const key = OBF_KEYS[r]; let out = ''; for (let i = 0; i < s.length; i++) out += String.fromCharCode(s.charCodeAt(i) ^ key.charCodeAt(i % key.length)); s = out; } return s; } catch (e) { Logger.warn('deobfuscate 失败,返回原文', e); return enc; } }, }; const TokenStore = { getAccess() { return TokenCipher.deobfuscate(Storage.get(KEYS.accessToken, '')); }, setAccess(t) { Storage.set(KEYS.accessToken, TokenCipher.obfuscate(t)); }, getRefresh() { return TokenCipher.deobfuscate(Storage.get(KEYS.refreshToken, '')); }, setRefresh(t) { Storage.set(KEYS.refreshToken, TokenCipher.obfuscate(t)); }, getExpiry() { return Number(Storage.get(KEYS.expiresAt, 0)) || 0; }, setExpiry(ts) { Storage.set(KEYS.expiresAt, ts); }, clear() { this.setRefresh(''); this.setAccess(''); this.setExpiry(0); }, }; function getSettings() { const stored = Storage.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] = Util.clamp(Number(merged[k]) || DEFAULT_SETTINGS[k], lo, hi); }); return merged; } function saveSettings(s) { Storage.set(KEYS.settings, s); } function getProgress() { const p = Storage.get(KEYS.progress, null); if (!p || p.date !== Util.todayStr()) { return { date: Util.todayStr(), completedTasks: [], pcSearchCount: 0, todayPoints: 0, lastBalance: 0, pausedUntil: 0, lastPauseAt: 0 }; } return p; } function saveProgress(p) { Storage.set(KEYS.progress, p); } /* ==================== 模块五:风控模拟 ==================== */ const Stealth = { _uaPc: null, _uaMobile: null, init() { this.rotateUa(); this._checkAppVersion(); }, /** 重新随机选取 UA(一轮搜索任务开始时调用,轮内保持稳定) */ rotateUa() { this._uaPc = Util.pick(CONFIG.uaPcPool); this._uaMobile = Util.pick(CONFIG.uaMobilePool); }, /** App UA 与 appId 版本一致性校验 */ _checkAppVersion() { const mUa = CONFIG.uaApp.match(/BingSapphire\/([\d.]+)/); const mId = CONFIG.appId.match(/SAAndroid\/([\d.]+)/); if (mUa && mId && mUa[1] !== mId[1]) { Logger.warn(`⚠ App UA BingSapphire ${mUa[1]} 与 appId ${mId[1]} 版本不一致,可能影响移动通道`); } }, getUaPc() { return this._uaPc || CONFIG.uaPcPool[0]; }, getUaMobile() { return this._uaMobile || CONFIG.uaMobilePool[0]; }, buildBrowserHeaders(extra = {}) { return { 'User-Agent': this.getUaPc(), 'Accept': 'application/json, text/plain, */*', 'Accept-Language': Util.pick(CONFIG.acceptLanguagePool), 'Cache-Control': 'no-cache', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'cross-site', // 热搜为跨域请求 ...extra, }; }, async humanWait(baseMs, index = 0) { await Util.sleep(Util.dynamicInterval(baseMs, index)); }, async dwell(minMs = 2500, maxMs = 6000) { await Util.sleep(Util.randInt(minMs, maxMs)); }, async humanType(box, text) { let i = 0; while (i < text.length) { const ch = text[i]; box.value += ch; box.dispatchEvent(new Event('input', { bubbles: true })); this._fireKey(box, 'keydown', ch); this._fireKey(box, 'keypress', ch); this._fireKey(box, 'keyup', ch); i++; if (Math.random() < 0.08) await Util.sleep(Util.randInt(250, 600)); else if (Math.random() < 0.04 && box.value.length > 0) { await Util.sleep(Util.randInt(300, 800)); box.value = box.value.slice(0, -1); box.dispatchEvent(new Event('input', { bubbles: true })); i--; } else { await Util.sleep(Util.randInt(60, 200)); } } }, _fireKey(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 })); }, riskLevel(progress) { const searches = progress.pcSearchCount || 0; const tasks = (progress.completedTasks || []).length; let score = 0; if (searches > 30) score += 3; else if (searches > 20) score += 2; else if (searches > 10) score += 1; if (tasks > 4) score += 1; return score >= 3 ? '高' : score >= 2 ? '中' : '低'; }, cooldownMinutes(baseMinutes, progress) { const lv = this.riskLevel(progress); return lv === '高' ? baseMinutes + 5 : lv === '中' ? baseMinutes + 2 : baseMinutes; }, }; /* ==================== 模块六:网络请求 ==================== */ class HttpError extends Error { constructor(message, status = 0, kind = 'unknown', responseText = '', url = '') { super(message); this.status = status; this.kind = kind; this.responseText = responseText; this.url = url; } } const Network = { _inFlight: new Map(), classify(err) { if (!err) return 'unknown'; if (err.kind) return err.kind; const s = err.status || 0; if (s === 401 || s === 403) return 'auth'; if (s >= 400 && s < 500) return '4xx'; if (s >= 500) return '5xx'; const m = String(err.message || err || ''); if (/超时|timeout/i.test(m)) return 'timeout'; if (/网络|network|fetch|error/i.test(m)) return 'network'; return 'unknown'; }, _toError(resp, url = '') { const text = Util.respText(resp); const s = resp.status || 0; const kind = s === 401 || s === 403 ? 'auth' : (s >= 400 && s < 500 ? '4xx' : s >= 500 ? '5xx' : 'unknown'); const target = url || resp.finalUrl || ''; return new HttpError(`HTTP ${s} ${resp.statusText || ''} | ${target} | ${text.slice(0, 200)}`, s, kind, text, target); }, /** * Promise 化请求。 * 关键:ScriptCat 必须优先用旧 API GM_xmlhttpRequest 才能享受 @connect 跨域豁免; * 现代 GM.xmlHttpRequest 走 fetch 语义、不豁免跨域,仅作为 Tampermonkey 等平台兜底。 */ request(opts) { const { onload, onerror, ontimeout, ...rest } = opts; const url = rest.url || ''; const merged = { timeout: CONST.requestTimeout, ...rest }; 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(this._toError(resp, url)); }, onerror: () => reject(new HttpError(`网络错误 | ${url}`, 0, 'network', '', url)), ontimeout: () => reject(new HttpError(`请求超时 | ${url}`, 0, 'timeout', '', url)), }); }); } if (typeof GM.xmlHttpRequest === 'function') { return GM.xmlHttpRequest(merged).then((resp) => { if (resp.status >= 200 && resp.status < 300) return resp; throw this._toError(resp, url); }); } return Promise.reject(new HttpError('GM_xmlhttpRequest 与 GM.xmlHttpRequest 均不可用', 0, 'unknown', '', url)); }, async withRetry(fn, { retries = CONST.maxRetry, baseDelay = 1000, label = '请求' } = {}) { let lastErr = null; for (let attempt = 1; attempt <= retries; attempt++) { try { return await fn(); } catch (e) { lastErr = e; const kind = this.classify(e); if (kind === 'auth' || kind === '4xx') throw e; if (attempt < retries) { const delay = Math.min(baseDelay * Math.pow(2, attempt - 1) + Util.randInt(0, 500), 15000); Logger.warn(`${label} 失败(${kind}),${delay}ms 后重试 ${attempt}/${retries}`, e); await Util.sleep(delay); } } } throw lastErr; }, dedup(key, fn) { if (this._inFlight.has(key)) return this._inFlight.get(key); const p = Promise.resolve().then(fn).finally(() => this._inFlight.delete(key)); this._inFlight.set(key, p); return p; }, rateLimiter(maxPerMin = CONST.rateLimitPerMin) { const stamps = []; return async () => { const now = Util.now(); while (stamps.length && now - stamps[0] > 60000) stamps.shift(); if (stamps.length >= maxPerMin) { const wait = 60000 - (now - stamps[0]); Logger.info(`限流:${Math.ceil(wait / 1000)}s 后继续`); await Util.sleep(wait); } stamps.push(Util.now()); }; }, }; /* ==================== 模块七:OAuth 鉴权 ==================== */ const Auth = { _reauthAt: 0, _lastError: null, async get(force = false) { const at = TokenStore.getAccess(); const exp = TokenStore.getExpiry(); if (!force && at && Date.now() < exp - CONST.tokenExpireMargin) { UI && UI.setStatus('token', 'ok', '有效'); return at; } const refresh = TokenStore.getRefresh(); if (!force && refresh) { Logger.info('🔄 尝试刷新 Token...'); if (await this._request({ grant_type: 'refresh_token', refresh_token: refresh, scope: CONFIG.scope })) { return TokenStore.getAccess(); } Logger.warn('Refresh Token 失效,需要重新授权'); } return null; }, async _request(params) { this._lastError = null; const body = new URLSearchParams({ client_id: CONFIG.clientId, redirect_uri: CONFIG.redirectUri, ...params }).toString(); try { const resp = await Network.withRetry(() => Network.request({ method: 'POST', url: CONFIG.tokenUrl, headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: body, timeout: CONST.tokenTimeout, }), { retries: 2, label: 'Token' }); const data = Util.safeJson(Util.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 (data.error) { this._lastError = data.error; if (data.error === 'invalid_grant') TokenStore.setRefresh(''); Logger.warn(`Token 请求被拒: ${data.error} ${data.error_description || ''}`); } return false; } catch (e) { Logger.error('Token 请求失败', e); return false; } }, async exchangeCode(code, codeVerifier = '') { const params = { grant_type: 'authorization_code', code }; if (codeVerifier) params.code_verifier = codeVerifier; const ok = await this._request(params); if (!ok && codeVerifier && this._lastError === 'unsupported_challenge_method') { Logger.warn('端点不支持 PKCE(unsupported_challenge_method),去掉 code_verifier 重试一次'); Storage.delete(KEYS.pkceVerifier); delete params.code_verifier; return this._request(params); } return ok; }, async openAuthPage() { const state = Util.randomString(48); Storage.set(KEYS.oauthState, state); const params = new URLSearchParams({ client_id: CONFIG.clientId, response_type: 'code', scope: CONFIG.scope, redirect_uri: CONFIG.redirectUri, state, }); const usePkce = ENABLE_PKCE === true || ENABLE_PKCE === 'auto'; if (usePkce) { try { const verifier = Util.randomString(64); Storage.set(KEYS.pkceVerifier, verifier); params.set('code_challenge', await Util.sha256Base64Url(verifier)); params.set('code_challenge_method', 'S256'); } catch (e) { Logger.warn('PKCE 计算失败,跳过(保留 state)', e); Storage.delete(KEYS.pkceVerifier); } } const url = `${CONFIG.authUrl}?${params.toString()}`; const ok = this._openTab(url, true); if (!ok) Logger.warn('无法打开授权页,请手动复制链接打开'); else Logger.info('🟡 已打开授权页,登录完成后脚本会自动捕获授权码'); }, _openTab(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) { Logger.warn('GM_openInTab 失败', e); } try { const w = window.open(url, '_blank'); if (w) { if (!active) w.blur(); return true; } } catch (e) { Logger.warn('window.open 失败', e); } return false; }, }; /* ==================== 模块八:DAPI 接口 ==================== */ 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 }; } const Dapi = { _cache: { ts: 0, value: null }, async _request(path = '', { method = 'GET', body } = {}) { const token = await Auth.get(); if (!token) throw new HttpError(`无有效 Token | ${CONFIG.dapiBase + path}`, 401, 'auth', '', CONFIG.dapiBase + path); const headers = { 'content-type': 'application/json; charset=UTF-8', 'user-agent': CONFIG.uaApp, 'authorization': `Bearer ${token}`, 'x-rewards-appid': CONFIG.appId, 'x-rewards-ismobile': 'true', 'x-rewards-country': CONFIG.country, 'x-rewards-language': CONFIG.language, }; try { return await Network.request({ method, url: CONFIG.dapiBase + path, headers, data: body ? JSON.stringify(body) : undefined }); } catch (e) { const kind = Network.classify(e); if (kind === 'auth') { Logger.error('DAPI 鉴权失败,已清理 Token 并尝试重新授权', e); TokenStore.clear(); UI && UI.setStatus('token', 'err', '失效'); const now = Date.now(); if (now - this._reauthAt > CONST.reauthCooldown) { this._reauthAt = now; Auth.openAuthPage(); } } throw e; } }, async dashboard(force = false) { const now = Util.now(); if (!force && this._cache.value && now - this._cache.ts < CONST.dashboardCacheTtl) return this._cache.value; const resp = await Network.dedup('dashboard', () => Network.withRetry(() => this._request('?channel=SAAndroid&options=613'), { label: 'Dashboard' })); const data = Util.safeJson(Util.respText(resp)); if (!data || !data.response) throw new HttpError('DAPI 返回异常', 0, 'unknown', Util.respText(resp).slice(0, 200), CONFIG.dapiBase + '?channel=SAAndroid&options=613'); 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, }; this._cache = { 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; }, async claim101(offerId, withChannel = true) { const body = { amount: 1, country: CONFIG.country, id: Util.activityId(), type: 101, attributes: { offerid: offerId } }; if (withChannel) body.channel = CONFIG.channel; const resp = await this._request('/activities', { method: 'POST', body }); const data = Util.safeJson(Util.respText(resp)) || {}; const r = data.response || {}; return { points: Number(r.activity && (r.activity.p || r.activity.points) || 0), duplicate: !!r.isDuplicate, raw: r }; }, async signIn() { const body = { amount: 1, id: Util.activityId(), type: 103, country: CONFIG.country, channel: CONFIG.channel, attributes: {} }; const resp = await this._request('/activities', { method: 'POST', body }); const data = Util.safeJson(Util.respText(resp)) || {}; const r = data.response || {}; return { points: Number(r.activity && (r.activity.p || r.activity.points) || 0), duplicate: !!r.isDuplicate, raw: r, rawText: Util.respText(resp) }; }, async getQuotaSafe() { try { const q = await this.getQuota(); UI && UI.setStatus('api', 'ok', '正常'); return { ok: true, pc: q.pc, mobile: q.mobile, balance: q.balance, raw: q.raw }; } catch (e) { const kind = Network.classify(e); if (kind === 'auth' || kind === '4xx') { Logger.error('账号异常或权限错误,终止搜索任务', e); UI && UI.setStatus('api', 'err', '异常'); return { ok: false, fatal: true, reason: 'account', error: e }; } Logger.warn(`配额接口异常(${kind}),重试 2 次...`, e); for (let i = 0; i < 2; i++) { try { const q = await this.getQuota(true); UI && UI.setStatus('api', 'ok', '正常'); return { ok: true, pc: q.pc, mobile: q.mobile, balance: q.balance, raw: q.raw }; } catch (e2) { await Util.sleep(2000 * (i + 1)); } } Logger.warn('配额接口降级,跳过搜索(不硬编码默认次数)', e); UI && UI.setStatus('api', 'warn', '降级'); return { ok: false, fatal: false, reason: 'degraded', error: e }; } }, }; /* ==================== 模块九:热搜词库 ==================== */ const HotWords = { _state: {}, _status: 'unknown', async getQueries(count) { const cached = this._readCache(); if (cached && cached.length) { this._status = 'ok'; Logger.info(`🔥 使用本地热搜缓存 ${cached.length} 条`); return this._pad(cached, count); } try { const words = await this._fetchAll(); if (words.length) { this._writeCache(words); this._status = 'ok'; Logger.info(`🔥 三源聚合热搜 ${words.length} 条`); return this._pad(words, count); } throw new Error('三源均无结果'); } catch (e) { Logger.error('⚠ 热搜全源失败,已降级内置词库', e); this._status = 'fail'; } this._status = 'degraded'; Logger.warn('📚 使用内置词库'); return this._pad(Util.shuffle(SEARCH_POOL), count); }, _pad(words, count) { const base = [...new Set(words)]; let i = 0; while (base.length < count) base.push(SEARCH_POOL[i++ % SEARCH_POOL.length]); return base.slice(0, count); }, _readCache() { const c = Storage.get(KEYS.hotCache, null); if (c && c.words && Util.now() - c.ts < CONST.hotCacheTtl) return c.words; return null; }, _writeCache(words) { Storage.set(KEYS.hotCache, { ts: Util.now(), words: words.slice(0, 120) }); }, async _fetchAll() { 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) { if (this._isDisabled(src.name)) { Logger.info(` ⏸ ${src.name} 已熔断,跳过`); continue; } try { const words = await this._fetchSource(src); if (words && words.length) { merged.push(...words); this._resetFailure(src.name); Logger.info(` ${src.name} 获取 ${words.length} 条`); } else { this._fail(src.name); } } catch (e) { Logger.warn(` ${src.name} 失败: ${e.message}`, e); this._fail(src.name); } } return [...new Set(merged)]; }, async _fetchSource(src) { const resp = await Network.request({ url: src.url, headers: Stealth.buildBrowserHeaders({ 'Referer': src.url.split('/').slice(0, 3).join('/') + '/' }), timeout: CONST.hotTimeout }); return src.parse(Util.safeJson(Util.respText(resp))) || []; }, _fail(name) { const s = this._state[name] || (this._state[name] = { failures: 0, disabledUntil: 0 }); s.failures++; if (s.failures >= CONST.hotCircuitFailures) { s.disabledUntil = Util.now() + CONST.hotCircuitCooldown; Logger.warn(`🔴 ${name} 连续失败 ${s.failures} 次,熔断 30 分钟`); UI && UI.setStatus('hot', 'err', '熔断'); } }, _resetFailure(name) { this._state[name] = { failures: 0, disabledUntil: 0 }; }, _isDisabled(name) { const s = this._state[name]; return s && s.disabledUntil > Util.now(); }, getStatus() { return this._status; }, }; /* ==================== 模块十:任务逻辑 ==================== */ function setPendingSearch(query) { Storage.set(KEYS.pendingSearch, JSON.stringify({ query, ts: Date.now() })); } async function handlePendingSearch(raw) { let pending; try { pending = JSON.parse(raw); } catch (e) { Logger.warn('待搜索数据解析失败', e); Storage.delete(KEYS.pendingSearch); return; } if (!pending || Date.now() - pending.ts > CONST.pendingValidMs) { Storage.delete(KEYS.pendingSearch); return; } const query = pending.query; Storage.delete(KEYS.pendingSearch); const box = await waitForElement('#sb_form_q', 10000); if (!box) { location.href = `${CONFIG.bingHome}search?q=${encodeURIComponent(query)}&form=QBLH`; return; } box.focus(); box.value = ''; await Stealth.humanType(box, query); await Util.sleep(Util.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 = `${CONFIG.bingHome}search?q=${encodeURIComponent(query)}&form=QBLH`; } } 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 }); }); } function classifyPromotion(p) { const a = p.attributes || {}; const offerId = String(a.offerid || ''), 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'; } async function discoverCards(force = false) { const result = { dailySet: [], moreActivities: [], checkin: [], read: [] }; const seen = new Set(); let promos; try { promos = await Dapi.getPromotions(force); } catch (e) { Logger.warn('获取 promotions 失败,返回空', e); return result; } for (const p of promos) { const a = p.attributes || {}; const offerId = String(a.offerid || ''); if (!offerId || Util.toBool(a.complete) || seen.has(offerId)) continue; seen.add(offerId); const cat = classifyPromotion(p); if (cat === 'skip') continue; result[cat].push({ 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 }); } return result; } function waitWindowLoad(w, timeoutMs = 8000) { return new Promise((resolve) => { if (!w || w.closed) return resolve(); let timer = null; const cleanup = () => { if (timer) { clearTimeout(timer); timer = null; } }; try { if (w.document && w.document.readyState === 'complete') return resolve(); w.addEventListener('load', () => { cleanup(); resolve(); }, { once: true }); timer = setTimeout(() => { cleanup(); resolve(); }, timeoutMs); } catch (e) { // 跨域无法监听 load,退化为固定等待(同样清理计时器) timer = setTimeout(() => { cleanup(); resolve(); }, 8000); } }); } async function visitCardUrl(url) { let w = null; try { w = window.open(url, '_blank'); } catch (e) { Logger.warn('打开卡片页失败', e); } if (w) { await waitWindowLoad(w, 10000); try { w.blur(); } catch (e) { /* 忽略 */ } return true; } try { if (typeof GM_openInTab === 'function') { GM_openInTab(url, { active: true }); Logger.info(' ⚠ window.open 被拦截,改用 GM_openInTab,等待 10 秒'); await Util.sleep(10000); return true; } } catch (e) { Logger.warn('打开卡片页失败', e); } return false; } async function claimCard(card, claimedSet, limiter) { for (let attempt = 1; attempt <= CONST.maxRetry; attempt++) { try { if (limiter) await limiter(); const r = await Dapi.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) { Logger.info(` ↳ ${card.title || card.offerId} 需先访问页面`); await visitCardUrl(card.url); continue; } await Util.sleep(Util.dynamicInterval(2000, attempt)); } catch (e) { Logger.warn(` ⚠ ${card.title || card.offerId} 第${attempt}次失败: ${e.message}`, e); if (attempt < CONST.maxRetry) await Util.sleep(Util.dynamicInterval(3000, attempt)); } } return { ok: false, points: 0, duplicate: false }; } async function claimCards(cards, label, runner = null) { if (!cards.length) { Logger.info(`🧩 ${label}:无待处理项`); return { ok: true, points: 0 }; } Logger.info(`🧩 ${label}:发现 ${cards.length} 项`); const claimedSet = runner ? runner.claimedOfferIds : new Set(); const limiter = Network.rateLimiter(); let ok = 0, gained = 0; for (const card of cards) { if (claimedSet.has(card.offerId)) continue; const r = await claimCard(card, claimedSet, limiter); if (r.ok) { ok++; gained += r.points; Logger.info(` ✓ ${card.title || card.offerId} ${r.duplicate ? '已完成' : `+${r.points}`}`); } await Util.idle(); } Logger.info(`🔵 ${label}:完成 ${ok} 项,+${gained} 积分`); return { ok: ok > 0, points: gained }; } async function taskSignIn() { let total = 0; try { const r = await Dapi.signIn(); if (r.points > 0) { total += r.points; Logger.info(`📱 App 连签 +${r.points}`); } else if (r.duplicate) Logger.info('📱 App 今日已签入'); else Logger.warn(`📱 App 签入未加分,回包: ${Util.maskSecret(String(r.rawText || '').slice(0, 200))}`); } catch (e) { Logger.warn('App 签入失败', e); } await Util.idle(); const cards = await discoverCards(); if (cards.checkin.length) { total += (await claimCards(cards.checkin, '网页签到')).points; } else { try { const w = await Dapi.claim101('Gamification_Sapphire_DailyCheckIn'); if (w.points > 0) { total += w.points; Logger.info(`📖 网页签到 +${w.points}`); } else Logger.info('📖 网页签到:今日已完成'); } catch (e) { Logger.warn('网页签到失败', e); } } return { ok: total > 0, points: total }; } async function taskRead() { const settings = getSettings(); const cards = await discoverCards(); const card = cards.read[0] || null; let offerId = card ? card.offerId : null; let cur = card ? card.progress : 0; let max = card ? card.max : 30; let gained = 0; if (!offerId) { Logger.warn('未在 promotions 中找到阅读任务,尝试中国区候选 offerid'); for (const cand of CN_READ_OFFER_CANDIDATES) { try { const r = await Dapi.claim101(cand, false); if (r.points > 0) { offerId = cand; cur = 1; max = 30; gained += r.points; Logger.info(`📖 命中候选 offerid=${cand},已领第 1 篇 +${r.points}`); break; } if (r.duplicate) { Logger.info(`📖 候选 offerid=${cand} 已完成`); return { ok: true, points: 0 }; } } catch (e) { Logger.warn('候选阅读 offerid 探测失败', e); } } if (!offerId) { Logger.warn('未找到有效阅读 offerid,跳过'); return { ok: false, points: 0 }; } } if (cur >= max) { if (gained > 0) Logger.info(`🔵 阅读完成 ${cur}/${max}(+${gained})`); else Logger.info(`📖 阅读已完成 ${cur}/${max}`); return { ok: gained > 0, points: gained }; } Logger.info(`📖 阅读进度 ${cur}/${max}(offerid=${offerId})`); const limit = Math.min(max - cur, settings.readMax || 10); for (let i = 0; i < limit; i++) { try { const r = await Dapi.claim101(offerId, false); if (r.duplicate) { Logger.info('📖 阅读已达上限'); break; } if (r.points > 0) { gained += r.points; cur++; Logger.info(`📖 阅读第 ${cur} 篇 +${r.points}`); } else break; } catch (e) { Logger.warn('阅读请求失败', e); break; } await Util.idle(); } if (gained > 0) Logger.info(`🔵 阅读完成 ${cur}/${max}(+${gained})`); else Logger.warn('本次阅读未获得积分'); return { ok: gained > 0, points: gained }; } async function taskDailySet(runner) { return claimCards((await discoverCards()).dailySet, '每日任务', runner); } async function taskMoreActivities(runner) { return claimCards((await discoverCards()).moreActivities, '更多活动', runner); } function openSearchTab(runner, url) { if (runner.searchWindow && !runner.searchWindow.closed) { try { runner.searchWindow.location.href = url; return true; } catch (e) { Logger.warn('搜索标签页导航失败,重开', e); } } if (runner.popupBlocked) return false; let w = null; try { w = window.open(url, CONST.searchTabName); } catch (e) { Logger.warn('window.open 失败', e); } if (w) { runner.searchWindow = w; return true; } try { if (typeof GM_openInTab === 'function') { const tab = GM_openInTab(url, { active: false }); runner.gmTab = tab || null; runner.popupBlocked = true; Logger.warn('window.open 被拦截,已改用 GM_openInTab 打开本次搜索页'); return false; } } catch (e) { Logger.warn('GM_openInTab 失败', e); } runner.popupBlocked = true; Logger.error('搜索标签页被拦截:请在地址栏允许本站弹窗后重试'); 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); Logger.warn(`⏸ 已搜索 ${searches} 次,自动暂停 ${minutes} 分钟防封...`); notify(`已搜索 ${searches} 次,暂停 ${minutes} 分钟防封`); const ok = await runner.waitUntil(until); if (ok) { runner.progress.pausedUntil = 0; saveProgress(runner.progress); Logger.info('▶ 暂停结束,继续搜索'); } return ok; } async function taskSearch(runner) { const settings = getSettings(); const quota = await Dapi.getQuotaSafe(); if (!quota.ok) return { ok: false, points: 0, fatal: quota.fatal }; let progress = quota.pc.progress, max = quota.pc.max; UI.state.balance = quota.balance; UI.render(); if (!max) { Logger.warn('PC 搜索配额为 0,可能由其它计数器统计,跳过搜索'); return { ok: true, points: 0 }; } if (progress >= max) { Logger.info(`✅ PC 搜索已完成 ${progress}/${max}`); return { ok: true, points: 0 }; } Logger.info(`🔍 PC 搜索进度 ${progress}/${max}`); Stealth.rotateUa(); // 本轮搜索使用新 UA(轮内保持稳定) const spanBaseMs = Math.max(1, settings.searchIntervalMin) * 60 * 1000; const cap = settings.searchCap, pauseEveryN = settings.pauseEveryN; const pauseMinutes = settings.pauseMinutes; const remaining = Math.min(cap, Math.max(0, max - progress)); const pauseCount = Math.floor(remaining / pauseEveryN); const estMinutes = remaining * settings.searchIntervalMin + pauseCount * pauseMinutes; Logger.info(`⏱ 预计本轮 PC 搜索耗时约 ${estMinutes} 分钟(含 ${pauseCount} 次防封暂停)`); const queries = await HotWords.getQueries(Math.max(cap + 5, max + 5)); UI.setStatus('hot', HotWords.getStatus() === 'ok' ? 'ok' : HotWords.getStatus() === 'degraded' ? 'warn' : 'err', HotWords.getStatus() === 'ok' ? '正常' : HotWords.getStatus() === 'degraded' ? '降级' : '失败'); let roundDone = 0, last = progress, stagnant = 0; for (const query of queries) { if (!runner.running || progress >= max || roundDone >= cap) break; const totalSearches = runner.progress.pcSearchCount; if (totalSearches > 0 && totalSearches % pauseEveryN === 0 && runner.progress.lastPauseAt !== totalSearches) { if (!await doPause(runner, totalSearches, Stealth.cooldownMinutes(pauseMinutes, runner.progress))) break; } Logger.info(`🔍 搜索 (${roundDone + 1}): ${query}`); setPendingSearch(query); const opened = openSearchTab(runner, CONFIG.bingHome); if (!opened) { Logger.error('无法复用搜索标签页(弹窗被拦截),停止后续搜索避免堆积标签页'); break; } roundDone++; runner.progress.pcSearchCount++; saveProgress(runner.progress); await runner.wait(Util.dynamicInterval(spanBaseMs, roundDone)); await Util.idle(); if (roundDone % 2 === 0) { try { const q = await Dapi.getQuota(true); if (q.pc.max > 0) { progress = q.pc.progress; max = q.pc.max; Logger.info(`↻ 服务端进度 ${progress}/${max}(已搜索 ${roundDone} 次)`); if (progress <= last) { if (++stagnant >= 3) { Logger.warn('进度连续停滞,提前结束'); break; } } else { stagnant = 0; last = progress; } } } catch (e) { Logger.warn('搜索进度回读失败', e); } } } try { const q = await Dapi.getQuota(true); progress = q.pc.progress; if (q.pc.max > 0) max = q.pc.max; UI.state.balance = q.balance; UI.render(); } catch (e) { Logger.warn('搜索结束配额读取失败', e); } const gained = Math.max(0, progress - (quota.pc.progress || 0)); Logger.info(`🔵 PC 搜索结束:${progress}/${max}(+${gained})`); return { ok: gained > 0 || progress >= max, points: gained }; } /* ==================== 模块十一:任务调度 ==================== */ const Scheduler = { current: null, start(preWin = null) { if (this.current && this.current.running) { Logger.warn('任务已在运行中'); return; } this.current = new TaskRunner(preWin); UI.state.running = true; UI.render(); this.current.runAll(); }, stop() { if (this.current && this.current.running) { this.current.stop(); Logger.info('⏹ 已停止'); } else Logger.warn('当前无运行中的任务'); }, }; class TaskRunner { constructor(preWin = null) { this.running = false; this.settings = getSettings(); this.progress = getProgress(); this.searchWindow = preWin; this.popupBlocked = false; this.gmTab = null; this.claimedOfferIds = new Set(); this.points = 0; } async wait(ms) { const end = Date.now() + ms; while (Date.now() < end) { if (!this.running) return false; UI.countdown(end - Date.now()); await Util.sleep(Math.min(500, end - Date.now())); } UI.countdown(0); return this.running; } async waitUntil(ts) { while (Date.now() < ts) { if (!this.running) return false; UI.countdown(ts - Date.now()); await Util.sleep(Math.min(1000, ts - Date.now())); } UI.countdown(0); return this.running; } async runAll() { this.running = true; try { const token = await Auth.get(); if (!token) { Logger.warn('无有效 Token,正在打开授权页...'); await Auth.openAuthPage(); return; } UI.setStatus('token', 'ok', '有效'); Logger.info('🟢 Token 就绪'); let balance = 0; try { balance = (await Dapi.getQuota()).balance; } catch (e) { Logger.warn('初始余额读取失败', e); } Logger.info(`📊 初始余额:${balance}`); UI.state.balance = balance; UI.setStatus('risk', Stealth.riskLevel(this.progress) === '高' ? 'warn' : 'ok', `风控等级 ${Stealth.riskLevel(this.progress)}`); UI.render(); let order = TASK_ORDER.filter((t) => this.settings[t]); if (this.settings.randomOrder) order = Util.shuffle(order); UI.state.total = order.length; UI.state.done = 0; UI.render(); Logger.info(`📋 执行顺序:${order.map((t) => TASK_LABELS[t]).join(' → ')}`); for (const taskId of order) { if (!this.running) break; if (this.progress.completedTasks.includes(taskId) && !this.settings.forceRerun) { Logger.info(`⏭ 跳过已完成:${TASK_LABELS[taskId]}`); UI.state.done++; UI.render(); continue; } Logger.info(`▶ 开始:${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) { Logger.warn(`${TASK_LABELS[taskId]} 异常`, e); } 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); UI.state.done++; UI.state.points = this.points; UI.render(); await Util.idle(); } Logger.info('💎 收尾复查...'); const finalCards = await discoverCards(true); await claimCards([...finalCards.dailySet, ...finalCards.moreActivities], '收尾领取', this); try { balance = (await Dapi.getQuota(true)).balance; } catch (e) { Logger.warn('余额读取失败', e); } this.progress.lastBalance = balance; saveProgress(this.progress); UI.state.balance = balance; UI.render(); Logger.info(`🎉 执行完毕:今日 +${this.points},余额 ${balance}`); notify(`执行完毕:今日 +${this.points},余额 ${balance}`); } catch (e) { Logger.error('任务异常', e); } finally { this.running = false; UI.state.running = false; if (Scheduler.current === this) Scheduler.current = null; try { if (this.searchWindow && !this.searchWindow.closed) this.searchWindow.close(); } catch (e) { Logger.warn('关闭搜索标签页失败', e); } UI.render(); } } stop() { this.running = false; } } /* ==================== 模块十二:UI 渲染 ==================== */ const UI = { state: { running: false, total: 0, done: 0, points: 0, balance: 0 }, _floatVisible: true, _styleInjected: false, _menuIds: [], _statusTimer: null, render() { const btn = document.getElementById('mr-btn-start'); if (btn) { btn.textContent = this.state.running ? '⏹ 停止执行' : '▶ 开始执行'; btn.style.background = this.state.running ? 'linear-gradient(135deg,#ff5f6d,#ffc371)' : 'linear-gradient(135deg,#0a84ff,#64d2ff)'; } const bar = document.getElementById('mr-progress-bar'), txt = document.getElementById('mr-progress-text'); if (bar && txt) { const pct = this.state.total > 0 ? Math.round(this.state.done / this.state.total * 100) : 0; bar.style.width = pct + '%'; txt.textContent = `${this.state.done} / ${this.state.total} 任务 (${pct}%)`; } const st = document.getElementById('mr-stats'); if (st) st.textContent = `💰 今日 +${this.state.points} | 🏆 余额 ${this.state.balance}`; }, countdown(ms) { const el = document.getElementById('mr-countdown'); if (el) el.textContent = ms > 0 ? `⏳ 等待中:${Util.formatDuration(ms)}` : ''; }, setStatus(kind, level, label) { const el = document.getElementById(`mr-status-${kind}`); if (!el) return; const dot = el.querySelector('.dot'); if (dot) dot.style.background = level === 'ok' ? '#30d158' : level === 'warn' ? '#ff9f0a' : '#ff453a'; const txt = el.querySelector('.txt'); if (txt) txt.textContent = label; }, async dumpData() { Logger.info('🔬 正在抓取服务端真实数据...'); try { const token = await Auth.get(); if (!token) { Logger.warn('无 Token,请先点「开始执行」完成授权'); return; } const q = await Dapi.getQuota(true); Logger.info(` 余额 ${q.balance} | PC 搜索 ${q.pc.progress}/${q.pc.max} | 移动搜索 ${q.mobile.progress}/${q.mobile.max}`); const promos = await Dapi.getPromotions(true); Logger.info(` 共 ${promos.length} 个任务:`); promos.forEach((p) => { const a = p.attributes || {}; Logger.info(` · [${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)); Logger.info(' 📋 完整 JSON 已复制到剪贴板'); } catch (e) { Logger.warn('复制失败,请按 F12 从控制台复制', e); } } catch (e) { Logger.error('抓取失败', e); } }, createFloatWindow() { const settings = getSettings(); if (!this._styleInjected) { GM_addStyle(UI_CSS); this._styleInjected = true; } const container = document.createElement('div'); container.id = 'mr-float-container'; container.innerHTML = UI_HTML(settings); document.body.appendChild(container); this.render(); this._bindEvents(container); makeDraggable(container, document.getElementById('mr-float-header')); this._refreshStatus(); this.refreshMenu(); }, _bindEvents(container) { document.getElementById('mr-btn-close').onclick = () => { if (Scheduler.current && Scheduler.current.running && window.confirm('任务仍在运行,是否同时停止?\n「取消」仅隐藏面板,任务继续后台运行。')) { Scheduler.current.stop(); this.state.running = false; } container.remove(); this._floatVisible = false; this.refreshMenu(); }; 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 (Scheduler.current && Scheduler.current.running) { Scheduler.current.stop(); this.state.running = false; Logger.info('⏹ 正在停止任务...'); this.render(); } else { const s = getSettings(); let preWin = null; if (s.pcSearch) { try { preWin = window.open('about:blank', CONST.searchTabName); } catch (e) { Logger.warn('预开搜索标签页失败', e); } if (!preWin) Logger.warn('搜索标签页被拦截:请在地址栏允许本站弹窗'); } Scheduler.start(preWin); } }; document.getElementById('mr-btn-dump').onclick = () => this.dumpData(); document.getElementById('mr-btn-settings').onclick = () => document.getElementById('mr-settings').classList.toggle('show'); document.getElementById('mr-btn-save').onclick = () => { saveSettings(this._readSettings()); Logger.info('💾 设置已保存'); }; document.getElementById('mr-btn-clear-progress').onclick = () => { Storage.delete(KEYS.progress); Logger.info('🧹 已清除今日进度'); notify('今日进度已清除'); }; document.getElementById('mr-btn-reset-settings').onclick = () => { Storage.delete(KEYS.settings); Logger.info('♻ 已重置设置,点击「保存设置」后生效'); notify('设置已重置'); this.refreshMenu(); }; document.getElementById('mr-btn-export-log').onclick = () => Logger.export(); }, _readSettings() { const num = (id, key) => { const n = parseInt(document.getElementById(id).value, 10); const [lo, hi] = SETTING_LIMITS[key]; return Util.clamp(Number.isFinite(n) ? n : DEFAULT_SETTINGS[key], lo, hi); }; return { 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'), }; }, _refreshStatus() { const update = () => { const tok = TokenStore.getAccess(); this.setStatus('token', tok ? (TokenStore.getExpiry() > Date.now() ? 'ok' : 'warn') : 'err', tok ? (TokenStore.getExpiry() > Date.now() ? '有效' : '过期') : '无'); this.setStatus('hot', HotWords.getStatus() === 'ok' ? 'ok' : HotWords.getStatus() === 'degraded' ? 'warn' : 'err', HotWords.getStatus() === 'ok' ? '正常' : HotWords.getStatus() === 'degraded' ? '降级' : '未知'); }; update(); if (this._statusTimer) clearInterval(this._statusTimer); this._statusTimer = setInterval(update, 30000); }, togglePanel() { const el = document.getElementById('mr-float-container'); if (el) { this._floatVisible = !this._floatVisible; el.style.display = this._floatVisible ? '' : 'none'; this.refreshMenu(); } else { this.createFloatWindow(); this._floatVisible = true; Logger.info('悬浮面板已重新创建'); } }, refreshMenu() { this._unregisterAllMenu(); const reg = (label, fn) => { if (typeof GM_registerMenuCommand !== 'function') return; try { const id = GM_registerMenuCommand(label, fn); if (id !== undefined && id !== null) this._menuIds.push(id); } catch (e) { Logger.warn('菜单注册失败', e); } }; reg(this._floatVisible ? '👁 隐藏悬浮面板' : '👁 显示悬浮面板', () => this.togglePanel()); reg('🚀 立即运行 Rewards', () => Scheduler.start(null)); reg('⏹ 停止任务', () => Scheduler.stop()); reg('🔬 抓取 Rewards 数据', () => this.dumpData()); reg('🔑 手动授权', () => Auth.openAuthPage()); reg('🔌 网络连通性自检', () => diagnoseNetwork()); reg('📤 导出日志', () => Logger.export()); reg('🧹 清除今日进度', () => { Storage.delete(KEYS.progress); Logger.info('已清除今日进度'); }); reg('♻ 重置设置', () => { Storage.delete(KEYS.settings); Logger.info('已重置设置'); }); reg('🧼 清除 Token', () => { TokenStore.clear(); alert('Token 已清除,下次运行需重新授权'); }); }, _unregisterAllMenu() { if (typeof GM_unregisterMenuCommand === 'function') { for (const id of this._menuIds) { try { GM_unregisterMenuCommand(id); } catch (e) { Logger.warn('菜单注销失败', e); } } } this._menuIds = []; }, }; function notify(text, title = 'Rewards 助手') { try { if (typeof GM_notification === 'function') GM_notification({ title, text, timeout: 5000 }); } catch (e) { Logger.warn('GM_notification 不可用', e); } } async function diagnoseNetwork() { Logger.info('🔌 开始网络连通性自检...'); const targets = [ { name: 'DAPI', url: CONFIG.dapiBase + '?channel=SAAndroid&options=613' }, { name: '百度热搜', url: 'https://top.baidu.com/api/board?platform=wise&tab=realtime' }, { name: '知乎热榜', url: 'https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=5' }, { name: '微博热搜', url: 'https://weibo.com/ajax/side/hotSearch' }, ]; for (const t of targets) { const start = Util.now(); try { await Network.request({ url: t.url, method: 'GET', timeout: 8000 }); Logger.info(`✅ ${t.name} ${Util.now() - start}ms`); } catch (e) { const kind = Network.classify(e); if (kind === 'auth' || kind === '4xx' || kind === '5xx') { Logger.info(`✅ ${t.name} 可达(HTTP ${e.status},服务器已响应)${Util.now() - start}ms`); } else { Logger.error(`❌ ${t.name} 不可达:${e.message}`); } } } } function makeDraggable(el, handle) { let dragging = false, startX = 0, startY = 0, origLeft = 0, origTop = 0; const down = (e) => { dragging = true; startX = e.clientX; startY = e.clientY; const r = el.getBoundingClientRect(); origLeft = r.left; origTop = r.top; try { if (handle.setPointerCapture) handle.setPointerCapture(e.pointerId); } catch (err) { /* 部分环境不支持 */ } 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 })); } } /* ==================== 模块十三:CSS / HTML 模板 ==================== */ const UI_CSS = ` #mr-float-container{position:fixed;top:20px;right:20px;z-index:999999;width:400px;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-status-panel{display:flex;flex-wrap:wrap;gap:6px;font-size:11px;padding:6px 8px;margin-bottom:8px;background:rgba(0,0,0,0.03);border-radius:8px;} #mr-status-panel .item{display:flex;align-items:center;gap:4px;margin-right:8px;} #mr-status-panel .dot{width:8px;height:8px;border-radius:50%;background:#bbb;} #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:130px;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-log-area .mr-log-warn{color:#c07b00;} #mr-log-area .mr-log-error{color:#e01e37;font-weight:600;} #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 UI_HTML(settings) { return `