// ==UserScript== // @name Microsoft Rewards Helper - Page Companion // @namespace local.microsoft-rewards-userscript.pages // @version 3.0.9 // @description 配套页面助手. 捕获微软授权回调, 仅处理主脚本本轮有效的页面指令. 需与同版本后台主脚本共同使用. // @author liyan20001124-byte // @icon https://bing.com/th?id=OMR.icon-96.png&pid=Rewards // @homepage https://scriptcat.org/zh-CN/script-show-page/6241 // @source https://scriptcat.org/zh-CN/script-show-page/6241 // @noframes // @license MIT // @storageName MicrosoftRewardsLocal_Maintained_v1 // @connect bing.com // @connect login.live.com // @connect rewards.bing.com // @connect prod.rewardsplatform.microsoft.com // @connect hotapi.nntool.cc // @connect hot.baiwumm.com // @connect cnxiaobai.com // @connect disp-qryapi.3g.qq.com // @connect qyapi.weixin.qq.com // @connect oapi.dingtalk.com // @connect open.feishu.cn // @connect push.i-i.me // @connect api.day.app // @match https://login.live.com/oauth20_desktop.srf* // @match https://rewards.bing.com/* // @match https://www.bing.com/* // @match https://cn.bing.com/* // @run-at document-start // @inject-into content // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @grant GM.setValue // @grant GM_info // @grant GM_log // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @tips 本地维护分支, 非原作者官方更新. 安装前停用原版. 待领取积分由用户手动领取. // ==/UserScript== /* global GM_getValue, GM_setValue, GM_xmlhttpRequest, GM_log, GM_info, GM_notification, GM_openInTab, GM_registerMenuCommand, GM_addValueChangeListener */ /* Configuration is owned by the background main script. */ return (function() { 'use strict'; const page = typeof location === "undefined" ? null : location; const hasDOM = typeof document !== "undefined" && typeof document.querySelector === "function"; // Security boundaries are shared by requests, links, menus and task entrypoints. const Security = { microsoftOrigins: new Set([ "https://bing.com", "https://www.bing.com", "https://cn.bing.com", "https://rewards.bing.com", "https://login.live.com", "https://prod.rewardsplatform.microsoft.com" ]), publicOrigins: { hot: new Set(["https://hotapi.nntool.cc", "https://hot.baiwumm.com", "https://cnxiaobai.com"]), ip: new Set(["https://disp-qryapi.3g.qq.com"]), webhook: new Set(["https://qyapi.weixin.qq.com", "https://oapi.dingtalk.com", "https://open.feishu.cn", "https://push.i-i.me", "https://api.day.app"]) }, blockedFragments: ["/redeem", "/redemption", "/order", "/donat", "/sweepstakes", "/referandearn", "/goal"], blockedWords: /兑换|赎回|捐赠|抽奖|购买|消耗积分|使用积分|redeem|redemption|donat|sweepstake|purchase|spend\s+points|set\s+goal/i, isBlocked(value) { let text = String(value || "").toLowerCase(); for (let depth = 0; depth < 4; depth++) { if (this.blockedFragments.some(fragment => text.includes(fragment))) return true; try { const decoded = decodeURIComponent(text); if (decoded === text) break; text = decoded.toLowerCase(); } catch (_) { return true; } } return false; }, url(raw, purpose = "microsoft") { const url = new URL(raw); if (url.protocol !== "https:" || url.username || url.password || this.isBlocked(url.href)) { throw new Error("安全策略拒绝此请求地址"); } const origins = purpose === "microsoft" ? this.microsoftOrigins : this.publicOrigins[purpose]; if (!origins || !origins.has(url.origin)) throw new Error("请求目标不在当前用途的允许列表内"); return url; }, pageAllowed() { return !page || !this.isBlocked(page.href || page.pathname || ""); }, redact(value) { let text = String(value ?? ""); for (const key of ["Config.token", "Config.code", "Notice.wework", "Notice.dingding", "Notice.feishu", "Notice.pushme", "Notice.bark"]) { const secret = GM_getValue(key, ""); if (typeof secret === "string" && secret.length >= 4) { text = text.split(secret).join("[REDACTED]"); text = text.split(encodeURIComponent(secret)).join("[REDACTED]"); } } if (typeof RewardsAuto !== "undefined" && RewardsAuto.state.token) { text = text.split(RewardsAuto.state.token).join("[REDACTED]"); } return text.replace(/([?&](?:code|state|refresh_token|access_token|push_key|key)=)[^\s]*/gi, "$1[REDACTED]") .replace(/Bearer\s+[^\s,;]+/gi, "Bearer [REDACTED]"); }, clearCredentials() { GM_setValue("Auth.revision", GM_getValue("Auth.revision", 0) + 1); for (const key of ["Config.code", "Config.token"]) GM_setValue(key, ""); GM_setValue("Config.codeTime", 0); GM_setValue("Config.tokenTime", 0); GM_setValue("Auth.pending", null); RewardsAuto.state.token = false; }, async persist(key, value) { if (typeof GM === "undefined" || typeof GM.setValue !== "function") { GM_setValue(key, value); return; } // A locally visible value does not prove the manager acknowledged its write. // Keep acknowledgement mandatory, but never leave execution waiting indefinitely. const write = GM.setValue(key, value); let timeout; try { await Promise.race([ write, new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error( `存储确认超时 (${key}, 15秒), 已停止后续操作; 已写入的当天记录保留, 不会自动重跑` )), 15000); }) ]); } finally { if (timeout !== undefined) clearTimeout(timeout); } }, async authUrl() { const state = crypto.randomUUID(); await this.persist("Config.code", ""); await this.persist("Config.codeTime", 0); await this.persist("Auth.pending", { state, expiresAt: Date.now() + 180000 }); const url = new URL("https://login.live.com/oauth20_authorize.srf"); url.search = new URLSearchParams({ client_id: "0000000040170455", response_type: "code", scope: "service::prod.rewardsplatform.microsoft.com::MBI_SSL", redirect_uri: "https://login.live.com/oauth20_desktop.srf", state }).toString(); return url.href; }, parseAuthorization(raw) { const url = this.url(raw); const pending = GM_getValue("Auth.pending", null); if (url.origin !== "https://login.live.com" || url.pathname !== "/oauth20_desktop.srf" || !pending || pending.expiresAt <= Date.now() || url.searchParams.get("state") !== pending.state) { throw new Error("授权回调不匹配或已过期, 请重新发起授权"); } const code = url.searchParams.get("code"); if (!code || code.length > 4096) throw new Error("授权码无效"); return code; }, webhookUrl(entry) { const key = String(entry.key || "").trim(); const expected = new URL(entry.url); const url = this.url(/^https?:/i.test(key) ? key : entry.url + encodeURIComponent(key), "webhook"); if (url.origin !== expected.origin || !url.pathname.startsWith(expected.pathname)) { throw new Error("推送地址与所选服务不匹配"); } return url.href; }, safeActivityLink(element) { if (!hasDOM || !this.pageAllowed() || !DailySchedule.allowPageWork() || !element || element.getAttribute("aria-disabled") === "true" || element.getAttribute("data-disabled") === "true") return false; const text = `${element.textContent || ""} ${element.getAttribute("aria-label") || ""}`; if (this.blockedWords.test(text)) return false; try { const raw = element.getAttribute("href"); if (!raw) return false; const url = this.url(new URL(raw, "https://rewards.bing.com/").href); if (url.origin === "https://rewards.bing.com") { return /^\/earn\/(?:quest|punchcard)(?:\/|$)/i.test(url.pathname) || /^\/punchcard(?:\/|$)/i.test(url.pathname); } return ["https://www.bing.com", "https://cn.bing.com", "https://bing.com"].includes(url.origin) && url.pathname === "/search" && Boolean(url.searchParams.get("q")); } catch (_) { return false; } }, clickActivity(element) { if (!this.safeActivityLink(element)) return false; element.click(); return true; }, safeCard(card) { if (!this.pageAllowed() || !card || !card.offerId || !card.hash || !(Number(card.points) > 0)) return false; if (this.blockedWords.test(`${card.title || ""} ${card.offerId}`)) return false; if (RewardsAuto.skipPatterns.some(pattern => `${card.title || ""} ${card.offerId}`.toLowerCase().includes(pattern.toLowerCase()))) return false; if (card.url) { try { const url = this.url(new URL(card.url, "https://rewards.bing.com/").href); return ["https://rewards.bing.com", "https://www.bing.com", "https://cn.bing.com", "https://bing.com"].includes(url.origin); } catch (_) { return false; } } return true; } }; const DailySchedule = { key: "Schedule.lastRun", autoKey: "Schedule.lastAutoRun", currentRun: null, claiming: false, attemptDate: "", clock() { const parts = new Intl.DateTimeFormat("en-GB", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(new Date()); const values = Object.fromEntries(parts.map(part => [part.type, part.value])); const minute = Number(values.hour) * 60 + Number(values.minute); return { date: `${values.year}-${values.month}-${values.day}`, dateNum: Number(`${values.year}${values.month}${values.day}`), month: Number(values.month), day: Number(values.day), year: Number(values.year), // Minute-level scheduling: 20:00 is the last eligible minute; 20:01 is excluded. inWindow: minute >= 9 * 60 && minute <= 20 * 60 }; }, automaticRecord() { const automatic = GM_getValue(this.autoKey, null); const latest = GM_getValue(this.key, null); // v3.0.8 and earlier only allowed one round; preserve those records as automatic. if (latest && latest.mode !== "manual" && (!automatic || latest.date > automatic.date)) return latest; return automatic; }, reason(mode = "automatic") { if (mode === "manual") return ""; const clock = this.clock(); if (!clock.inWindow) return "不在北京时间 09:00-20:00 的启动时间段内"; if (this.attemptDate === clock.date || this.automaticRecord()?.date === clock.date) return "今天已自动启动过一轮, 自动任务跳过; 如需重跑请使用脚本菜单的手动运行"; return ""; }, notice(reason) { const marker = `${this.clock().date}:${reason}`; if (GM_getValue("Schedule.lastNotice", "") === marker) return; GM_setValue("Schedule.lastNotice", marker); Utils.log("📅", reason); }, async claim(mode = "automatic") { if (this.claiming) return null; this.claiming = true; mode = mode === "manual" ? "manual" : "automatic"; const claim = async () => { const reason = this.reason(mode); if (reason) { this.notice(reason); return null; } const record = { date: this.clock().date, runId: crypto.randomUUID(), status: "running", mode, startedAt: Date.now(), searchTarget: Utils.randomRange(20, 30) }; Utils.log("💾", "正在保存本日启动记录, 每项最多等待15秒确认..."); if (mode === "automatic") { // Reserve automatic eligibility independently from the latest manual round. this.attemptDate = record.date; await Security.persist(this.autoKey, record); } else { const automatic = this.automaticRecord(); if (automatic && GM_getValue(this.autoKey, null)?.runId !== automatic.runId) { await Security.persist(this.autoKey, automatic); } } await Security.persist(this.key, record); if (GM_getValue(this.key, null)?.runId !== record.runId) return null; this.currentRun = record; return record; }; try { // ScriptCat's sandbox may expose Web Locks while forbidding their use. // Cron once, this local guard and the persisted daily record protect starts. return await claim(); } finally { this.claiming = false; } }, async finish(record, failed) { try { if (GM_getValue(this.key, null)?.runId === record.runId) { const finished = { ...record, status: failed ? "failed" : "finished", endedAt: Date.now() }; await Security.persist(this.key, finished); if (record.mode === "automatic" && GM_getValue(this.autoKey, null)?.runId === record.runId) { await Security.persist(this.autoKey, finished); } } } finally { this.currentRun = null; } }, async command(action) { const record = this.currentRun; if (!record || GM_getValue(this.key, null)?.runId !== record.runId) return false; const issuedAt = Date.now(); await Security.persist("BingRewards_cmd", { action, processed: false, commandId: crypto.randomUUID(), runId: record.runId, date: record.date, issuedAt, expiresAt: issuedAt + 10 * 60 * 1000 }); return true; }, allowPageWork(command = GM_getValue("BingRewards_cmd", null)) { const record = GM_getValue(this.key, null); return Boolean(command && record && !command.processed && command.commandId && ["clickDailySet", "claimPoints"].includes(command.action) && command.runId === record.runId && command.date === record.date && ["running", "finished"].includes(record.status) && Number.isFinite(command.issuedAt) && Number.isFinite(command.expiresAt) && command.issuedAt >= record.startedAt && command.issuedAt <= Date.now() && command.expiresAt > Date.now() && command.expiresAt - command.issuedAt <= 10 * 60 * 1000); } }; // No startup, network activity or automatic clicks on redemption-related pages. if (!Security.pageAllowed()) return; if (page?.hostname === "login.live.com" && page.pathname === "/oauth20_desktop.srf") { const callbackParameters = new URL(page.href).searchParams; if (!callbackParameters.has("code")) { if (callbackParameters.has("error")) GM_log("微软授权未完成, 请从主脚本重新发起授权"); return; } return (async () => { try { const code = Security.parseAuthorization(page.href); await Security.persist("Config.code", code); await Security.persist("Config.codeTime", Date.now()); GM_log("页面助手已捕获授权码, 等待后台完成令牌交换"); if (GM_getValue("Notice.bro", true)) { GM_notification({ title: "授权码已捕获", text: "等待后台完成授权, 可以关闭此页" }); } // Replace query text only after the callback has been validated and persisted. if (typeof history !== "undefined") history.replaceState({}, "", page.pathname); } catch (_) { GM_log("授权回调未被接受, 请重新发起授权并确认页面助手已启用"); } })(); } if (GM_getValue("Safety.authOnly", false)) return; const RewardsAuto = { // UA: pc=Edge桌面, mobile=Edge移动, app=BingSapphire真机抓包 ua: { pc: "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", mobile: "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", // App 端 UA(来自真实抓包数据,Redmi K20 Pro + BingSapphire) app: "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", }, appConfig: { rewardsAppId: "SAAndroid/32.6.2110003560", channel: "SAAndroid", // 渠道:Android 版 Bing App offerIds: { dailyCheckIn: "Gamification_Sapphire_DailyCheckIn", // 每日签到标识 readArticle: "ENUS_readarticle3_30points", // 阅读任务标识 } }, searchPool: [ "what is the weather forecast tomorrow", "how do I make sourdough bread at home", "where can I find cheap flights to tokyo", "why is the sky blue scientific explanation", "how to learn rust programming in 2026", "what time does the world cup final start", "how to fix a leaky kitchen faucet step by step", "what are the best vr games of 2026", "how to start a vegetable garden in spring", "where to watch new movies this week", "how to take care of a bonsai tree", "what is the difference between python async and threading", "how to meditate properly for beginners", "what is the origin of halloween traditions", "how do solar panels actually work", "what is the best mechanical keyboard for typing", "how to tie a windsor knot tie", "what causes northern lights aurora borealis", "how to brew the perfect espresso at home", "what are the symptoms of vitamin d deficiency", "how to sleep better naturally tonight", "why do cats purr when they are happy", // 地点/新闻/购物意图 "best coffee shops in san francisco downtown", "italian restaurants near times square", "tokyo cherry blossom season 2026 forecast", "rtx 5070 ti benchmark vs rtx 4080 super", "iphone 17 release date and features", "tesla stock price today nasdaq", "best noise cancelling headphones under 300", "fastest electric cars 0 to 60 mph", "vintage camera brands collectors guide", "budget gaming laptop with rtx 4070 2026", // 操作指南/食谱 "easy chocolate chip cookies recipe from scratch", "30 minute home workout routine no equipment", "stretching exercises for lower back pain relief", "easy origami crane folding instructions", "git rebase vs merge which one to use", "markdown cheat sheet with examples", "japanese hiragana chart pronunciation", "ancient rome history quick overview", "pomodoro technique for focus and productivity", "healthy breakfast ideas under 10 minutes", // 中文搜索词 "天气预报", "今日新闻热点", "美食食谱家常菜", "旅游攻略", "健康养生知识", "科技资讯", "电影推荐", "股票行情", "体育赛事", "历史上的今天" ], // 热搜API配置 apiConfig: { mode: GM_getValue("Config.api", "offline"), arr: [ ["hot.baiwumm.com", { url: "https://hot.baiwumm.com/api/", hot: ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq", "netease", "zhihu"], }], ["hot.cnxiaobai.com", { url: "https://cnxiaobai.com/DailyHotApi/", hot: ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq-news", "netease-news", "zhihu"], }], ["hot.nntool.cc", { url: "https://hotapi.nntool.cc/", hot: ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq-news", "netease-news", "zhihu"], }], ], url: "", hot: [], wordList: [], wordIndex: 0, }, skipPatterns: [ "referral", "refer and earn", "sweepstake", "entries", "install the", "set bing as your default", "bing wallpaper", "punch card", "ancient coin", "sea of thieves", "rewards extension", "redemption goal", "order history", "claim your gift", "shop to earn", "set goal", "Available tomorrow", "Offer is Locked", "Earn -1 points" ], state: { token: false, region: "CN", host: "www.bing.com", dateNowNum: 0, dateNowStr: "", pcProgress: 0, pcMax: 90, readProgress: 0, readMax: 30, sendMSG: "", lastSearchProgress: -1, restrictedTimes: 0, ip: "", ipInfo: "", startTime: 0, } }; const Webhooks = [ { name: "企业微信", url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=", key: GM_getValue("Notice.wework", false), msg: { "msgtype": "text", "text": { get content() { return `> ${new Date().toLocaleString()}\n\n ## ${GM_info.script.name}\n ${RewardsAuto.state.sendMSG}` } }, }, }, { name: "钉钉", url: "https://oapi.dingtalk.com/robot/send?access_token=", key: GM_getValue("Notice.dingding", false), msg: { "msgtype": "markdown", "markdown": { "title": GM_info.script.name, get text() { return `> ${new Date().toLocaleString()}\n ### ${GM_info.script.name}\n ${RewardsAuto.state.sendMSG}` } }, }, }, { name: "飞书", url: "https://open.feishu.cn/open-apis/bot/v2/hook/", key: GM_getValue("Notice.feishu", false), msg: { "msg_type": "interactive", "card": { "schema": "2.0", "header": { "title": { "tag": "plain_text", "content": GM_info.script.name }, "template": "orange" }, "body": { "elements": [{ "tag": "markdown", "text_align": "center", get content() { return `#### ${new Date().toLocaleString()}\n ${RewardsAuto.state.sendMSG}` } }] } } }, }, { name: "PushMe", url: "https://push.i-i.me/?push_key=", key: GM_getValue("Notice.pushme", false), msg: { "type": "markdown", "title": `${GM_info.script.name}[#rewards!https://rewards.bing.com/rewards.png]`, get content() { return `\n ${RewardsAuto.state.sendMSG}` } }, }, { name: "Bark", url: "https://api.day.app/", key: GM_getValue("Notice.bark", false), msg: { "group": "rewards", "icon": "https://rewards.bing.com/rewards.png", "title": GM_info.script.name, get markdown() { return `\n ${RewardsAuto.state.sendMSG}` } }, }, ]; const Utils = { pendingWebhooks: new Set(), // 日志输出(带通知支持) log(icon, msg, push = false) { msg = Security.redact(msg); GM_log(`${icon} ${msg}`); if (push && GM_getValue("Notice.bro", true)) { try { GM_notification({ title: GM_info.script.name + ` ${icon}`, text: msg, onclick: () => GM_openInTab("https://rewards.bing.com/dashboard", { active: true }) }); } catch(_) {} } // 发送到外部通知接口 if (push) { RewardsAuto.state.sendMSG = `${icon} ${msg}`; const pending = this.sendWebhook().catch(() => GM_log("外部推送失败")); this.pendingWebhooks.add(pending); void pending.then(() => this.pendingWebhooks.delete(pending)); } }, async flushWebhooks() { while (this.pendingWebhooks.size > 0) { await Promise.allSettled([...this.pendingWebhooks]); } }, // 发送webhook通知 async sendWebhook() { if (!GM_getValue("Safety.externalNotice", false)) return; await Promise.all(Webhooks.map(async (i) => { if (!i.key) return; try { const targetUrl = Security.webhookUrl(i); const result = await this.xhr({ method: "POST", url: targetUrl, purpose: "webhook", headers: { "content-type": "application/json; charset=UTF-8", }, data: Security.redact(JSON.stringify(i.msg)), }); if (result) GM_log(`🔵 「${i.name}」消息推送完成`); } catch (e) { GM_log(`🔴 「${i.name}」消息推送出错: ${Security.redact(e.message)}`); } })); }, // Reject redirects before transmission; public requests never receive account cookies or headers. xhr(options) { return new Promise((resolve, reject) => { let url; const { purpose = "microsoft", ...request } = options; try { if (!Security.pageAllowed()) throw new Error("当前页面禁止自动请求"); url = Security.url(request.url, purpose); if (url.searchParams.has("code") || url.searchParams.has("refresh_token") || (purpose === "microsoft" && url.searchParams.has("access_token"))) { throw new Error("敏感凭据不得放入请求 URL"); } if (purpose !== "microsoft") { if (request.cookie || request.user || request.password) throw new Error("第三方请求不得携带账户凭据"); for (const name of Object.keys(request.headers || {})) { if (!/^content-type$/i.test(name)) throw new Error("第三方请求不得携带账户请求头"); } for (const secret of [RewardsAuto.state.token, GM_getValue("Config.token", ""), GM_getValue("Config.code", "")]) { if (typeof secret === "string" && secret.length >= 4 && [url.href, String(request.data || "")].some(value => value.includes(secret) || value.includes(encodeURIComponent(secret)))) { throw new Error("禁止向第三方发送微软授权凭据"); } } if (purpose === "hot" && !GM_getValue("Config.api", "offline").startsWith("hot.")) throw new Error("在线热搜尚未启用"); if (purpose === "webhook" && !GM_getValue("Safety.externalNotice", false)) throw new Error("外部推送尚未启用"); } } catch (error) { reject(error); return; } const start = Date.now(); GM_xmlhttpRequest({ ...request, url: url.href, anonymous: purpose !== "microsoft", redirect: "error", timeout: 15000, onload: (res) => { try { const final = Security.url(res.finalUrl || url.href, purpose); if (final.href !== url.href) throw new Error("请求发生了未允许的跳转"); if (res.status >= 200 && res.status < 300) resolve(res.responseText); else reject(new Error(`HTTP ${res.status}, 用时 ${((Date.now() - start) / 1000).toFixed(2)} 秒`)); } catch (error) { reject(error); } }, onerror: () => reject(new Error("网络错误或重定向已被拦截")), onabort: () => reject(new Error("请求已取消")), ontimeout: () => reject(new Error("请求超时")) }); }); }, randomRange(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); }, getTimestamp() { return Date.now(); }, getTodayNum() { return DailySchedule.clock().dateNum; }, getTodayStr() { const d = DailySchedule.clock(); return `${d.month}/${d.day}/${d.year}`; }, getRandomUUID() { return crypto.randomUUID().replace(/-/g, "").toUpperCase(); }, isJSON(s) { try { const j = JSON.parse(s); return Array.isArray(j) || (typeof j === "object" && j !== null); } catch { return false; } }, delay(ms) { return new Promise(r => setTimeout(r, ms)); }, // 操作节流:所有操作间必须使用随机延迟 randomDelay(min = 3000, max = 8000) { return this.delay(this.randomRange(min, max)); }, waitForElement(selector, timeout = 30000) { return new Promise((resolve, reject) => { const element = document.querySelector(selector); if (element) return resolve(element); const observer = new MutationObserver((_, obs) => { const el = document.querySelector(selector); if (el) { obs.disconnect(); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); const el = document.querySelector(selector); el ? resolve(el) : reject(new Error(`等待元素超时: ${selector}`)); }, timeout); }); }, waitForElementsByText(containerSelector, textPatterns, timeout = 30000) { return new Promise((resolve, reject) => { const findElements = () => { const containers = document.querySelectorAll(containerSelector); const results = []; for (const container of containers) { const text = container.textContent || ""; for (const pattern of textPatterns) { if (text.includes(pattern)) { results.push({ element: container, pattern }); break; } } } return results; }; const found = findElements(); if (found.length > 0) return resolve(found); const observer = new MutationObserver((_, obs) => { const found = findElements(); if (found.length > 0) { obs.disconnect(); resolve(found); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); const found = findElements(); found.length > 0 ? resolve(found) : reject(new Error("等待文本元素超时")); }, timeout); }); } }; const API = { async getToken(parameters, maxRetries = 3) { const revision = GM_getValue("Auth.revision", 0); for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const res = await Utils.xhr({ method: "POST", url: "https://login.live.com/oauth20_token.srf", headers: { "content-type": "application/x-www-form-urlencoded;charset=UTF-8" }, data: new URLSearchParams({ client_id: "0000000040170455", ...parameters }).toString() }); if (GM_getValue("Auth.revision", 0) !== revision) return false; if (!Utils.isJSON(res)) return false; const data = JSON.parse(res); if (data.error) { Utils.log("🔴", "微软授权失败, 请重新授权"); return false; } if (data.access_token) { RewardsAuto.state.token = data.access_token; GM_setValue("Config.token", GM_getValue("Safety.persistToken", true) ? (data.refresh_token || "") : ""); GM_setValue("Config.tokenTime", Date.now()); GM_setValue("Config.code", ""); GM_setValue("Config.codeTime", 0); GM_setValue("Auth.pending", null); return true; } return false; } catch (error) { if (/HTTP 40[01]/.test(error.message) || attempt === maxRetries) return false; await Utils.delay(3210); } } return false; }, async withTokenRetry(requestFn) { if (!RewardsAuto.state.token) return null; try { return await requestFn(RewardsAuto.state.token); } catch (error) { if (!error.message?.includes("401")) throw error; RewardsAuto.state.token = false; // Keep the refresh token for the retry rather than forcing an unnecessary login. if (!await this.renewToken()) return null; return requestFn(RewardsAuto.state.token); } }, async renewToken() { if (!GM_getValue("Tasks.sign", true) && !GM_getValue("Tasks.read", true)) return true; let refreshToken = GM_getValue("Config.token", ""); if (!GM_getValue("Safety.persistToken", true)) { GM_setValue("Config.token", ""); refreshToken = ""; } if (refreshToken) { const ok = await this.getToken({ refresh_token: refreshToken, scope: "service::prod.rewardsplatform.microsoft.com::MBI_SSL", grant_type: "refresh_token" }); if (ok) return true; GM_setValue("Config.token", ""); } // One bounded authorization attempt, with state validation in the callback. const existing = GM_getValue("Auth.pending", null); if (!existing || existing.expiresAt <= Date.now()) { const authUrl = await Security.authUrl(); GM_openInTab(authUrl, { active: true, insert: true, setParent: true }); } Utils.log("🔑", "请在微软页面完成授权, 回调页会自动捕获授权码", true); for (let i = 0; i < 180; i++) { await Utils.delay(1000); const code = GM_getValue("Config.code", ""); const pending = GM_getValue("Auth.pending", null); const codeTime = GM_getValue("Config.codeTime", 0); if (!pending || pending.expiresAt <= Date.now()) break; if (!code || Date.now() - codeTime >= 180000) continue; const ok = await this.getToken({ code, redirect_uri: "https://login.live.com/oauth20_desktop.srf", grant_type: "authorization_code" }); GM_setValue("Config.code", ""); GM_setValue("Config.codeTime", 0); GM_setValue("Auth.pending", null); return ok; } GM_setValue("Auth.pending", null); GM_setValue("Config.code", ""); GM_setValue("Config.codeTime", 0); Utils.log("🔴", "授权等待结束, 可从菜单重新发起", true); return false; }, // 优先从 earn 页面“今日积分”表格解析,DAPI 只做兜底 async getRewardsInfo(maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const html = await Utils.xhr({ url: "https://rewards.bing.com/earn" }); const clean = html.replace(/\\"/g, '"'); // 尝试从Next.js RSC数据中解析 let balance = 0; let pcMax = 60, pcCur = 0, mobMax = 0, mobCur = 0; let dailyOffer = 0; let searchQuotaFound = false; // 从 RSC JSON 数据中解析 pointsCounters(兼容有/无 mobile 字段、不同字段顺序) const pcIdx = clean.indexOf('"pointsCounters":{'); if (pcIdx !== -1) { let start = pcIdx + 17, depth = 0, end = start; for (let i = start; i < clean.length && i < start + 500; i++) { if (clean[i] === '{') depth++; if (clean[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } } } try { const pts = JSON.parse(clean.substring(start, end)); pcMax = pts.pc?.max ?? 60; pcCur = pts.pc?.progress ?? 0; mobMax = pts.mobile?.max ?? 0; mobCur = pts.mobile?.progress ?? 0; dailyOffer = pts.dailyOffer ?? 0; if (pts.totalPoints != null) balance = Number(pts.totalPoints); } catch {} } // 补充获取 balance if (balance === 0) { const balMatch = clean.match(/"balance":(\d+)/) || clean.match(/"availablePoints":(\d+)/); if (balMatch) balance = parseInt(balMatch[1]); } // 解析今日积分明细 const todayDetails = []; // 从RSC数据中解析活动卡片(括号深度匹配,兼容嵌套数组) const acIdx = clean.indexOf('"activityCards":['); if (acIdx !== -1) { let acStart = acIdx + 16, acDepth = 0, acEnd = acStart; for (let i = acStart; i < clean.length && i < acStart + 5000; i++) { if (clean[i] === '[') acDepth++; if (clean[i] === ']') { acDepth--; if (acDepth === 0) { acEnd = i + 1; break; } } } try { const cardsStr = clean.substring(acStart, acEnd); const cardRegex = /"title":"([^"]+)".*?"points":(\d+).*?"isCompleted":(true|false)/g; let cardMatch; while ((cardMatch = cardRegex.exec(cardsStr)) !== null) { const title = cardMatch[1]; const points = parseInt(cardMatch[2]); const isCompleted = cardMatch[3] === "true"; if (points > 0 && isCompleted) { todayDetails.push({ title, points }); } } } catch {} } // 方法2: 从HTML中解析搜索进度 const toNum = value => parseInt(String(value).replace(/,/g, ''), 10) || 0; const searchRowMatch = clean.match(/
\s*必应搜索\s*<\/p>\s*<\/div>\s*
([^<]+)<\/p><\/div>