// ==UserScript== // @name Microsoft Rewards 全能自动化助手 // @namespace https://scriptcat.org/ // @version 3.0.0 // @description macOS 液态玻璃悬浮窗。走移动 App 通道(x-rewards-ismobile/appid/BingSapphire UA/options=613/counters)完成签到(type103)、阅读(ENUS_readarticle3_30points)、每日任务与活动卡片(type101)、PC/移动搜索(reportActivity 上报 + 真实浏览器逐字输入)。百度+知乎双热搜源,自动领取积分,进度持久化,防封暂停,精确计时。 // @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_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_notification // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_openInTab // @connect prod.rewardsplatform.microsoft.com // @connect rewards.bing.com // @connect login.live.com // @connect cn.bing.com // @connect www.bing.com // @connect top.baidu.com // @connect www.zhihu.com // @run-at document-end // ==/UserScript== (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', offerRead: 'ENUS_readarticle3_30points', country: 'cn', language: 'zh', 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', uaMobile: '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 EdgA/131.0.0.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 KEYS = { accessToken: 'mr3_access_token', refreshToken: 'mr3_refresh_token', expiresAt: 'mr3_expires_at', settings: 'mr3_settings', progress: 'mr3_progress', pendingSearch: 'mr3_pending_search', }; const PENDING_VALID_MS = 5 * 60 * 1000; const PAUSE_EVERY_N = 5; const PAUSE_DURATION_MS = 10 * 60 * 1000; const SEARCH_TAB_NAME = 'MR_REWARDS_SEARCH_TAB'; const DEFAULT_SETTINGS = { sign: true, read: true, dailySet: true, moreActivities: true, pcSearch: true, mobileSearch: true, readMax: 10, searchInterval: 30, searchCap: 40, randomOrder: false, }; const TASK_LABELS = { sign: '每日签到', read: '每日阅读', dailySet: '每日任务', moreActivities: '更多活动', pcSearch: 'PC 搜索', mobileSearch: '移动搜索', }; /* ==================== 工具 ==================== */ 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 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 todayCookie() { const d = new Date(); return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; } 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}秒`; } function toBool(v) { return v === true || String(v).toLowerCase() === 'true'; } function firstInt() { for (let i = 0; i < arguments.length; i++) { if (arguments[i] === undefined || arguments[i] === null) continue; const n = parseInt(arguments[i], 10); if (Number.isFinite(n)) return n; } return 0; } const uuid = () => { try { if (crypto && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, '').toUpperCase(); } catch (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 log(msg) { console.log('[Rewards助手] ' + msg); const el = document.getElementById('mr-log-area'); if (el) { const line = document.createElement('div'); line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; el.appendChild(line); el.scrollTop = el.scrollHeight; } } function waitForElement(selector, timeoutMs = 10000, root = document) { return new Promise((resolve) => { const start = Date.now(); const check = () => { const el = root.querySelector(selector); if (el) return resolve(el); if (Date.now() - start > timeoutMs) return resolve(null); setTimeout(check, 100); }; check(); }); } /* ==================== 存储 ==================== */ function getSettings() { const stored = GM_getValue(KEYS.settings, {}) || {}; return { ...DEFAULT_SETTINGS, ...stored }; } function saveSettings(s) { GM_setValue(KEYS.settings, s); } function getProgress() { const p = GM_getValue(KEYS.progress, null); if (!p || p.date !== todayStr()) { return { date: todayStr(), completedTasks: [], pcSearchCount: 0, mobileSearchCount: 0, todayPoints: 0, lastBalance: 0, pausedUntil: 0, lastPauseAt: 0 }; } return p; } function saveProgress(p) { GM_setValue(KEYS.progress, p); } /* ==================== XHR ==================== */ function xhr(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', ...options, timeout: options.timeout || 20000, onload(res) { if (res.status >= 200 && res.status < 300) return resolve(res.responseText); reject(new Error(`HTTP ${res.status}`)); }, onerror: () => reject(new Error('网络错误')), ontimeout: () => reject(new Error('请求超时')), }); }); } /* ==================== 授权 ==================== */ const Auth = { async get(force = false) { const at = GM_getValue(KEYS.accessToken, ''); const exp = GM_getValue(KEYS.expiresAt, 0); if (!force && at && Date.now() < exp - 60000) return at; const refresh = GM_getValue(KEYS.refreshToken, ''); if (!force && refresh) { log('🔄 尝试刷新 Token...'); if (await this._request({ grant_type: 'refresh_token', refresh_token: refresh, scope: CFG.scope })) { return GM_getValue(KEYS.accessToken, ''); } log('🟡 Refresh Token 失效,需要重新授权'); } return null; }, async _request(params) { const body = new URLSearchParams({ client_id: CFG.clientId, redirect_uri: CFG.redirectUri, ...params }).toString(); for (let i = 0; i < 3; i++) { try { const text = await xhr({ method: 'POST', url: CFG.tokenUrl, headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: body, timeout: 15000, }); const data = safeJson(text); if (!data) throw new Error('响应非 JSON'); if (data.access_token) { GM_setValue(KEYS.accessToken, data.access_token); if (data.refresh_token) GM_setValue(KEYS.refreshToken, data.refresh_token); GM_setValue(KEYS.expiresAt, Date.now() + (data.expires_in || 3600) * 1000); return true; } if (['invalid_grant', 'invalid_request', 'invalid_client'].includes(data.error)) { if (data.error === 'invalid_grant') GM_setValue(KEYS.refreshToken, ''); return false; } } catch (e) { console.warn('[Rewards助手] Token 请求异常:', e.message); } await sleep(2000); } return false; }, async exchangeCode(code) { return this._request({ grant_type: 'authorization_code', code }); }, openAuthPage() { const url = `${CFG.authUrl}?client_id=${CFG.clientId}&response_type=code&scope=${encodeURIComponent(CFG.scope)}&redirect_uri=${encodeURIComponent(CFG.redirectUri)}`; try { if (typeof GM_openInTab === 'function') GM_openInTab(url, { active: true }); else window.open(url, '_blank'); } catch (e) { window.open(url, '_blank'); } log('🟡 已打开授权页,登录完成后脚本会自动捕获授权码'); }, }; /* ==================== DAPI(移动 App 通道) ==================== */ 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 xhr({ method, url: CFG.dapiBase + path, headers, data: body ? JSON.stringify(body) : undefined }); } catch (e) { if (/401/.test(e.message)) { log('🟡 DAPI 401,Token 已失效,需重新授权'); GM_setValue(KEYS.refreshToken, ''); GM_setValue(KEYS.accessToken, ''); } throw e; } }, _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 }; }, async getQuota() { const text = await this.dapi('?channel=SAAndroid&options=613'); const data = safeJson(text); if (!data || !data.response) throw new Error('DAPI 返回异常'); const resp = data.response; return { pc: this._parseCounter(resp.counters ? resp.counters.pcSearch : null), mobile: this._parseCounter(resp.counters ? resp.counters.mobileSearch : null), balance: Number(resp.balance || 0), raw: data, }; }, async getPromotions() { const text = await this.dapi('?channel=SAAndroid&options=613'); const data = safeJson(text); return (data && data.response && data.response.promotions) || []; }, 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 text = await this.dapi('/activities', { method: 'POST', body }); const data = safeJson(text) || {}; const resp = data.response || {}; return { points: Number(resp.activity && (resp.activity.p || resp.activity.points) || 0), duplicate: !!resp.isDuplicate, raw: resp }; }, async signIn() { const body = { amount: 1, id: activityId(), type: 103, country: CFG.country, channel: CFG.channel }; const text = await this.dapi('/activities', { method: 'POST', body }); const data = safeJson(text) || {}; const resp = data.response || {}; return { points: Number(resp.activity && (resp.activity.p || resp.activity.points) || 0), duplicate: !!resp.isDuplicate, raw: resp }; }, }; /* ==================== 热搜源(百度 + 知乎) ==================== */ async function getHotQueries(count) { const queries = []; try { queries.push(...await fetchHotwords()); log(`🔥 热搜源获取成功,共 ${queries.length} 条`); } catch (e) { log('⚠ 热搜获取失败,使用内置词库'); } while (queries.length < count) queries.push(SEARCH_POOL[queries.length % SEARCH_POOL.length]); return [...new Set(queries)].slice(0, count); } 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) }, ]; for (const src of sources) { try { const text = await xhr({ url: src.url, headers: { 'User-Agent': CFG.uaPC, 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9' }, timeout: 8000, }); const words = src.parse(safeJson(text)) || []; if (words.length > 0) { console.log(`[Rewards助手] ${src.name} 获取 ${words.length} 条`); return words; } } catch (e) { console.warn(`[Rewards助手] ${src.name} 失败:`, e.message); } } throw new Error('所有热搜源均失败'); } /* ==================== 搜索上报(reportActivity) ==================== */ async function reportActivitySearch(query, mobile = false) { const host = 'cn.bing.com'; const ua = mobile ? CFG.uaMobile : CFG.uaPC; const dev = mobile ? 'm' : 'd'; const mkt = '&mkt=zh-CN'; const q = encodeURIComponent(query); const cookie = `_Rwho=u=${dev}&ts=${todayCookie()}`; const searchUrl = `https://${host}/search?q=${q}&form=QBLH${mkt}`; await xhr({ url: searchUrl, headers: { 'user-agent': ua, 'cookie': cookie, 'referer': `https://${host}/?form=QBLH` } }); const ig = uuid(); const headers = { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 'user-agent': ua, 'referer': `https://${host}/?form=QBLH`, 'cookie': cookie, }; const params = `q=${q}&form=QBLH${mkt}`; await xhr({ method: 'POST', url: `https://${host}/rewardsapp/ncheader?ver=88888888&IID=SERP.5047&IG=${ig}&ajaxreq=1`, headers, data: 'wb=1%3bi%3d1%3bv%3d1' }); await xhr({ method: 'POST', url: `https://${host}/rewardsapp/reportActivity?IG=${ig}&IID=SERP.5047&${params}&ajaxreq=1`, headers, data: `url=${encodeURIComponent(searchUrl)}&V=web` }); } /* ==================== 逐字输入模拟真人打字 ==================== */ function setPendingSearch(query) { GM_setValue(KEYS.pendingSearch, JSON.stringify({ query, ts: Date.now() })); } async function handlePendingSearch(raw) { let pending; try { pending = JSON.parse(raw); } catch (e) { GM_setValue(KEYS.pendingSearch, ''); return; } if (!pending || Date.now() - pending.ts > PENDING_VALID_MS) { GM_setValue(KEYS.pendingSearch, ''); return; } const query = pending.query; GM_setValue(KEYS.pendingSearch, ''); const box = await waitForElement('#sb_form_q', 10000); if (!box) { location.href = `https://cn.bing.com/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 })); box.dispatchEvent(new KeyboardEvent('keydown', { key: ch, bubbles: true })); box.dispatchEvent(new KeyboardEvent('keypress', { key: ch, bubbles: true })); box.dispatchEvent(new KeyboardEvent('keyup', { key: ch, bubbles: true })); await sleep(randInt(60, 200)); } await sleep(randInt(300, 700)); const form = document.getElementById('sb_form'); if (form) form.submit(); else location.href = `https://cn.bing.com/search?q=${encodeURIComponent(query)}&form=QBLH`; } /* ==================== 任务实现 ==================== */ async function taskSignIn() { try { const r = await Rewards.signIn(); if (r.points > 0) { log(`📱 App 连签 +${r.points}`); return { ok: true, points: r.points }; } if (r.duplicate) log('📱 App 今日已签入'); else log('📱 App 签入完成(本次无新增积分)'); } catch (e) { log(`🟡 App 签入失败: ${e.message}`); } try { const w = await Rewards.claim101('Gamification_Sapphire_DailyCheckIn', true); if (w.points > 0) log(`📖 网页签到 +${w.points}`); else log('📖 网页签到:今日已完成'); return { ok: true, points: w.points }; } catch (e) { log(`🟡 网页签到失败: ${e.message}`); return { ok: false, points: 0 }; } } async function findReadOffer() { try { const promos = await Rewards.getPromotions(); const cand = promos.find((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' || t.includes('read'); }); return cand ? cand.attributes.offerid : CFG.offerRead; } catch (e) { return CFG.offerRead; } } async function taskRead() { const settings = getSettings(); const offerRead = await findReadOffer(); let cur = 0, max = 30; try { const promos = await Rewards.getPromotions(); const task = promos.find((p) => (p.attributes || {}).offerid === offerRead); if (task && task.attributes) { cur = parseInt(task.attributes.progress) || 0; max = parseInt(task.attributes.max) || 30; } } catch (e) {} if (cur >= max) { log(`📖 阅读已完成 ${cur}/${max}`); return { ok: true, points: 0 }; } log(`📖 阅读进度 ${cur}/${max}(offerid=${offerRead})`); const limit = Math.min(max - cur, settings.readMax || 10); let gained = 0; for (let i = 0; i < limit; i++) { try { const r = await Rewards.claim101(offerRead, false); if (r.duplicate) { log('📖 阅读已达上限'); break; } if (r.points > 0) { gained += r.points; cur++; log(`📖 阅读第 ${i + 1} 篇 +${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 discoverCards() { const cards = []; const seen = new Set(); let promos; try { promos = await Rewards.getPromotions(); } catch (e) { return cards; } for (const p of promos) { const a = p.attributes || {}; const offerId = a.offerid; if (!offerId) continue; const lowerId = offerId.toLowerCase(); if (lowerId.includes('checkin') || lowerId.includes('check-in') || lowerId.includes('dailycheckin')) continue; if (lowerId.includes('readarticle') || lowerId.includes('msnread') || lowerId.includes('readtoearn')) continue; const progress = parseInt(a.progress) || 0; const max = parseInt(a.max) || 1; if (max > 0 && progress >= max) continue; const title = String(p.name || a.title || p.description || ''); const lower = `${title} ${offerId}`.toLowerCase(); if (SKIP_PATTERNS.some((s) => lower.includes(s))) continue; const key = `dapi:${offerId}`; if (seen.has(key)) continue; seen.add(key); cards.push({ offerId, title, type: String(a.type || '').toLowerCase(), url: a.destinationUrl || a.url || '' }); } return cards; } async function claimCards(cards, label) { if (!cards.length) { log(`🧩 ${label}:无待处理项`); return { ok: true, points: 0 }; } log(`🧩 ${label}:发现 ${cards.length} 项`); let ok = 0, gained = 0; for (const card of cards) { try { const r = await Rewards.claim101(card.offerId, true); if (r.duplicate) { log(` ✓ ${card.title || card.offerId} 已完成`); ok++; continue; } if (r.points > 0) { gained += r.points; log(` ✓ ${card.title || card.offerId} +${r.points}`); ok++; } else log(` • ${card.title || card.offerId} +0`); } catch (e) { log(`🟡 卡片失败(${card.offerId}): ${e.message}`); } await randSleep(); } log(`🔵 ${label}:完成 ${ok} 项,+${gained} 积分`); return { ok: ok > 0, points: gained }; } async function taskDailySet() { const cards = (await discoverCards()).filter((c) => c.type === 'urlreward'); return claimCards(cards, '每日任务'); } async function taskMoreActivities() { const cards = (await discoverCards()).filter((c) => c.type !== 'urlreward'); return claimCards(cards, '更多活动'); } async function taskSearch(kind, runner) { const settings = getSettings(); const label = kind === 'pc' ? 'PC 搜索' : '移动搜索'; let quota; try { quota = await Rewards.getQuota(); } catch (e) { log(`🔴 无法获取搜索配额: ${e.message}`); return { ok: false, points: 0 }; } const counter = kind === 'pc' ? quota.pc : quota.mobile; let { progress, max } = counter; if (!max) { log(kind === 'pc' ? '🟡 PC 搜索配额为 0,跳过' : '🟡 本账号无独立移动搜索配额(已由 PC 搜索覆盖)'); return { ok: true, points: 0 }; } if (progress >= max) { log(`✅ ${label}已完成 ${progress}/${max}`); return { ok: true, points: 0 }; } log(`🔍 ${label}进度 ${progress}/${max}`); const span = Math.max(15, settings.searchInterval || 30); const cap = settings.searchCap || 40; let stagnant = 0, last = progress, done = 0; const queries = await getHotQueries(cap + 5); for (const query of queries) { if (!runner.running) break; if (progress >= max) break; if (done >= cap) { log(`🔴 已达单轮搜索上限 ${cap} 次,停止`); break; } // 防封暂停 if (done > 0 && done % PAUSE_EVERY_N === 0 && runner.progress.lastPauseAt !== done) { const until = Date.now() + PAUSE_DURATION_MS; runner.progress.pausedUntil = until; runner.progress.lastPauseAt = done; saveProgress(runner.progress); log(`⏸ 已搜索 ${done} 次,自动暂停 10 分钟防封...`); try { GM_notification({ title: 'Rewards 助手', text: `已搜索 ${done} 次,暂停 10 分钟防封` }); } catch (e) {} const ok = await runner.waitUntil(until); if (!ok) break; runner.progress.pausedUntil = 0; saveProgress(runner.progress); log('▶ 暂停结束,继续搜索'); } log(`🔍 ${label} (${done + 1}): ${query}`); // 1) reportActivity 上报(确定性计分) try { await reportActivitySearch(query, kind === 'mobile'); } catch (e) { log(`🟡 上报失败: ${e.message}`); } // 2) PC 搜索额外用真实浏览器逐字输入(拟人) if (kind === 'pc' && runner.searchWindow && !runner.searchWindow.closed) { try { setPendingSearch(query); runner.searchWindow.location.href = 'https://cn.bing.com/'; } catch (e) { /* 跨域导航失败忽略 */ } } done++; runner.progress[kind === 'pc' ? 'pcSearchCount' : 'mobileSearchCount']++; saveProgress(runner.progress); const waitMs = randInt((span - 10) * 1000, (span + 10) * 1000); const cont = await runner.wait(waitMs); if (!cont) break; if (done % 2 === 0) { try { const q = await Rewards.getQuota(); const c = kind === 'pc' ? q.pc : q.mobile; if (c.max > 0) { progress = c.progress; max = c.max; log(`↻ 服务端进度 ${progress}/${max}(已搜索 ${done} 次)`); if (progress <= last) { stagnant++; if (stagnant >= 3) { log('🔴 进度连续停滞,提前结束'); break; } } else { stagnant = 0; last = progress; } } } catch (e) {} } } try { const q2 = await Rewards.getQuota(); const c2 = kind === 'pc' ? q2.pc : q2.mobile; const gained = Math.max(0, c2.progress - counter.progress); progress = c2.progress; max = c2.max; log(`🔵 ${label}结束:${progress}/${max}(本次 +${gained})`); return { ok: gained > 0 || progress >= max, points: gained }; } catch (e) { return { ok: false, points: 0 }; } } /* ==================== 任务执行器 ==================== */ class TaskRunner { constructor(preOpenedSearchWindow = null) { this.running = false; this.settings = getSettings(); this.progress = getProgress(); this.searchWindow = preOpenedSearchWindow; this.claimed = 0; 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,正在打开授权页...'); Auth.openAuthPage(); return; } log('🟢 Token 就绪'); let balance = 0; try { balance = (await Rewards.getQuota()).balance; } catch (e) {} log(`📊 初始余额:${balance}`); uiState.balance = balance; updateUI(); const order = ['sign', 'dailySet', 'read', 'moreActivities', 'pcSearch', 'mobileSearch'].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; 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(); else if (taskId === 'moreActivities') res = await taskMoreActivities(); else if (taskId === 'pcSearch') res = await taskSearch('pc', this); else if (taskId === 'mobileSearch') res = await taskSearch('mobile', 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(); await claimCards(finalCards, '收尾领取'); try { balance = (await Rewards.getQuota()).balance; } catch (e) {} this.progress.lastBalance = balance; saveProgress(this.progress); uiState.balance = balance; updateUI(); log(`🎉 执行完毕:今日 +${this.points},余额 ${balance}`); try { GM_notification({ title: 'Rewards 助手', text: `执行完毕:今日 +${this.points},余额 ${balance}`, timeout: 8000 }); } catch (e) {} } catch (e) { log(`🔴 任务异常: ${e.message}`); } finally { this.running = false; uiState.running = false; updateUI(); } } stop() { this.running = false; } } /* ==================== UI 状态 ==================== */ let currentRunner = null; let floatVisible = true; let menuCommandId = null; 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(); log(` 余额 ${q.balance} | PC ${q.pc.progress}/${q.pc.max} | 移动 ${q.mobile.progress}/${q.mobile.max}`); const promos = await Rewards.getPromotions(); 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 液态玻璃) ==================== */ function createFloatWindow() { const settings = getSettings(); GM_addStyle(` #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;transition:opacity .3s ease; } #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-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;} `); const container = document.createElement('div'); container.id = 'mr-float-container'; container.innerHTML = `