// ==UserScript== // @name 微软积分商城签到(修复增强版) // @namespace https://scriptcat.org/zh-CN/script-show-page/8249 // @version 3.2.0 // @description 后台自动完成 Microsoft Rewards 日常任务获取积分:每日签入(PC + App 双通道静默)、新闻阅读、活动卡片领取、PC/移动搜索。内置运行锁、拟人化随机间隔、完成状态提醒与多通道积分通知。说明:脚本只做「任务领取与上报」,Quiz 答题、必应软件/App 类任务仍需人工完成。 // @author 14195843463 // @original-author liyan20001124-byte // @original-license MIT // @original-script https://scriptcat.org/zh-CN/script-show-page/8249 // @icon https://bing.com/th?id=OMR.icon-96.png&pid=Rewards // @homepage https://scriptcat.org/zh-CN/script-show-page/8249 // @supportURL https://scriptcat.org/zh-CN/script-show-page/8249 // @license MIT // @crontab */15 * * * * // @connect bing.com // @connect www.bing.com // @connect cn.bing.com // @connect login.live.com // @connect rewards.bing.com // @connect prod.rewardsplatform.microsoft.com // @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 // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @grant GM_cookie // @grant GM_info // @grant GM_log // @grant GM_registerMenuCommand // @storageName BingRewardsAuto_Shared // @tips 本脚本开源免费,请勿购买。原作基于 MIT 协议二次开发,已标注原作者。 // ==/UserScript== /* global GM_cookie, GM_getValue, GM_setValue, GM_xmlhttpRequest, GM_log, GM_info, GM_notification, GM_openInTab */ /* ============================================================================= * 微软积分商城签到(修复增强版) v3.2.0 * ----------------------------------------------------------------------------- * 来源与授权 * 本脚本基于 liyan20001124-byte 的「微软积分商城签到」(脚本猫 8249,MIT 协议) * 二次开发而成。按 MIT 协议要求,原作者、原协议、原脚本地址已在上方元数据中 * 通过 @original-author / @original-license / @original-script 声明。 * * 代码结构(自上而下) * 1. RewardsError —— 统一错误类型,区分「网络/超时/HTTP/中止」便于决定是否重试 * 2. RewardsAuto —— 全局配置与运行时状态(UA、词池、跳过规则、进度) * 3. Webhooks —— 通知渠道定义(企业微信 / 钉钉 / 飞书 / PushMe / Bark) * 4. Utils —— 通用工具:网络请求封装、重试、错误隔离、日期/随机 * 5. API —— 与微软服务交互的接口层(Token、积分、签入、阅读、搜索) * 6. TaskManager —— 任务编排层:签入 / 阅读 / 活动 / 搜索 及每日状态汇总 * 7. 页面内逻辑 —— 仅在 rewards.bing.com 生效的打卡点击 * 8. 菜单与入口 —— 注册菜单命令、运行锁、启动流程 * * 能力边界(重要,避免误以为脚本「坏了」) * ✅ 已支持:每日签入、新闻阅读上报、活动卡片领取、PC/移动搜索、积分通知 * ⚠️ 不支持:Quiz 答题(只领取不作答)、必应软件/App 任务、Image Creator/Copilot * —— 这些需要真实交互或微软未开放接口,请手动完成 * * 更新日志 * v3.2.0 清理死代码(appActivity / waitForElement / 未使用提示词与跳过规则); * 移除已下线热搜接口 nntool.cc;补充开源署名与结构注释;修正功能描述 * v3.1.0 修复搜索不计分、定时任务不触发、地区检测误杀、卡片领取失败等 15 项问题; * 新增运行锁、任务完成状态提醒 * ========================================================================== */ /* ==UserConfig== Config: keep: title: 持续检测(全部完成后是否继续) type: checkbox default: true lock: title: 锁定国区(非大陆IP自动停止) type: checkbox default: true span: title: 搜索间隔(秒) type: number default: 30 min: 30 unit: ±15秒 api: title: 搜索词接口(offline为随机搜索词) type: select default: offline values: [offline, hot.cnxiaobai.com, hot.baiwumm.com] code: title: 授权码链接 type: textarea description: 粘贴 login.live.com 跳转后的完整URL Tasks: sign: title: 每日签入 type: checkbox default: true read: title: 新闻阅读 type: checkbox default: true promos: title: 活动卡片(含打卡) type: checkbox default: true quiz: title: Quiz 自动答题 type: checkbox default: true search: title: PC/移动搜索 type: checkbox default: true Notice: bro: title: 浏览器通知(当前脚本) type: checkbox default: true wework: title: 企业微信消息推送(群机器人) type: text password: true description: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx dingding: title: 钉钉群机器人(不加签,关键词:#) type: text password: true description: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx feishu: title: 飞书群机器人(不加签,关键词:#) type: text password: true description: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx pushme: title: PushMe(push.i-i.me) type: text password: true description: xxxxxxxxxxxxxxxxxxxx bark: title: Bark(bark.day.app) type: text password: true description: xxxxxxxxxxxxxxxxxxxx ==/UserConfig== */ (function() { 'use strict'; // ===================================================== // 【改动说明】授权码捕获逻辑,继承自 Untitled-1.txt // 当用户在 login.live.com 完成授权时自动捕获 code // ===================================================== if (location.hostname === "login.live.com" && location.pathname === "/oauth20_desktop.srf") { const code = new URLSearchParams(location.search).get("code"); if (code) { GM_setValue("Config.code", location.href); GM_setValue("Config.token", false); GM_notification({ title: "🟢 授权成功", text: "授权码已捕获,可关闭此页" }); try { history.replaceState({}, "", "about:blank"); } catch(_) {} setTimeout(() => { try { window.close(); } catch(_) {} }, 200); } return; } // ===================================================== // 【新增】统一错误类型 // 【说明】用于区分 HTTP 错 / 网络错 / 超时 / 认证失败 // - kind: http | network | timeout | abort | parse | auth // - status: HTTP 状态码(0 表示非 HTTP 层错误) // ===================================================== class RewardsError extends Error { constructor(message, { status = 0, kind = "unknown", cause = null } = {}) { super(message); this.name = "RewardsError"; this.status = status; this.kind = kind; this.cause = cause; } } // ===================================================== // 核心配置与状态管理 // ===================================================== const RewardsAuto = { // 浏览器 UA 配置 ua: { // 【修复】原 mobile UA 中 Chrome/144 与 EdgA/131 版本自相矛盾, // 易被服务端识别为异常指纹。现统一为同版本。 pc: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.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/142.0.0.0 Mobile Safari/537.36 EdgA/142.0.0.0", app: "Mozilla/5.0 (Linux; Android 16; Redmi K20 Pro Build/BP4A.251205.006; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.0.0 Mobile Safari/537.36 BingSapphire/32.6.2110003560", }, // App 端配置(模拟 Bing App / SAAndroid 通道) appConfig: { // App 身份标识,随微软 App 版本变化,失效时表现为 App 侧任务 401 rewardsAppId: "SAAndroid/32.6.2110003560", channel: "SAAndroid", offerIds: { // 阅读任务 id:国区实际为 <市场>_readarticle3_30points, // 运行时由 doRead() 按当前市场动态拼接,此处仅作兜底默认值 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", "天气预报", "今日新闻热点", "美食食谱家常菜", "旅游攻略", "健康养生知识", "科技资讯", "电影推荐", "股票行情", "体育赛事", "历史上的今天", // 【补充】国区账号使用中文词更贴近真实用户行为,降低异常风险 "北京天气预报一周", "上海地铁线路图查询", "家常红烧肉的做法", "广州早茶推荐餐厅", "成都三天旅游路线", "杭州西湖景点门票", "笔记本电脑性价比排行", "固态硬盘选购指南", "机械键盘轴体区别", "显卡驱动更新教程", "手机续航优化技巧", "无线耳机降噪对比", "新能源汽车续航排行", "高铁票改签规则", "国内油价调整最新消息", "公积金提取条件", "个人所得税专项扣除", "社保缴费年限查询", "英语四级报名时间", "考研国家线查询", "公务员考试时间安排", "颈椎病自我缓解方法", "睡眠质量改善技巧", "每日所需饮水量", "跑步减肥正确姿势", "低脂早餐搭配推荐", "秋冬皮肤保湿方法", "基金定投入门知识", "银行大额存单利率", "人民币汇率走势", "NBA常规赛赛程", "世界杯预选赛积分榜", "马拉松训练计划", "篮球鞋选购推荐", "健身增肌饮食搭配", "瑜伽入门基本功", "豆瓣电影高分榜单", "国产剧推荐排行", "经典粤语歌曲合集", "小说免费阅读网站", "春节档电影票房", "演唱会门票开售时间", "家常菜谱大全视频", "电饭煲蛋糕做法", "空气炸锅食谱推荐", "垃圾分类标准查询", "快递物流查询方法", "房屋租赁合同模板", "简历模板免费下载", "劳动合同注意事项", "租房押金退还规定" ], // 热搜 API 配置:默认 offline(用内置词池),最稳;选在线则取实时热搜词 apiConfig: { mode: GM_getValue("Config.api", "offline"), // 【实测 2026-10 接口可用性】 // hot.cnxiaobai.com → 200 正常(已提到首位) // hot.baiwumm.com → 常被 Vercel 安全校验拦截,作为备选 // hot.nntool.cc → 已下线(403),v3.2.0 起连同 @connect 一并移除 arr: [ ["hot.cnxiaobai.com", { url: "https://cnxiaobai.com/DailyHotApi/", hot: ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq-news", "netease-news", "zhihu"], }], ["hot.baiwumm.com", { url: "https://hot.baiwumm.com/api/", hot: ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq", "netease", "zhihu"], }], ], url: "", hot: [], wordList: [], wordIndex: 0, }, // 跳过规则:标题或 offerId 命中以下关键词的卡片一律不领取。 // 这些是需要真实消费/安装/邀请才能完成的任务,强行上报只会白跑或触发风控 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, mobileProgress: 0, mobileMax: 60, sendMSG: "", lastSearchProgress: -1, restrictedTimes: 0, ip: "", ipInfo: "", startTime: 0, } }; // ===================================================== // 通知接口配置 // 【改动说明】由 key 改为 keyName,运行时按名读取,配置修改后无需刷新 // ===================================================== const Webhooks = [ { name: "企业微信", keyName: "Notice.wework", url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=", msg: { "msgtype": "text", "text": { get content() { return `> ${new Date().toLocaleString()}\n\n ## ${GM_info.script.name}\n ${RewardsAuto.state.sendMSG}` } }, }, }, { name: "钉钉", keyName: "Notice.dingding", url: "https://oapi.dingtalk.com/robot/send?access_token=", 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: "飞书", keyName: "Notice.feishu", url: "https://open.feishu.cn/open-apis/bot/v2/hook/", 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", keyName: "Notice.pushme", url: "https://push.i-i.me/?push_key=", msg: { "type": "markdown", "title": `${GM_info.script.name}[#rewards!https://rewards.bing.com/rewards.png]`, get content() { return `\n ${RewardsAuto.state.sendMSG}` } }, }, { name: "Bark", keyName: "Notice.bark", url: "https://api.day.app/", msg: { "group": "rewards", "icon": "https://rewards.bing.com/rewards.png", "title": GM_info.script.name, get markdown() { return `\n ${RewardsAuto.state.sendMSG}` } }, }, ]; // ===================================================== // 工具类 // 【改动说明】 // - xhr 加固:单次结算保护 + RewardsError 分类 // - 新增 xhrRetry:带退避重试(仅对可重试错误) // - 新增 safeRun:任务级错误隔离 // - 新增 fmtErr:统一错误格式化 // ===================================================== const Utils = { log(icon, msg, push = false) { GM_log(`${icon} ${msg}`); if (push && GM_getValue("Notice.bro", true)) { GM_notification({ title: GM_info.script.name + ` ${icon}`, text: msg, onclick: () => GM_openInTab("https://rewards.bing.com/dashboard", { active: true }) }); } if (push) { RewardsAuto.state.sendMSG = `${icon} ${msg}`; this.sendWebhook(); } }, // 发送webhook通知(allSettled,一个失败不影响其它) async sendWebhook() { const jobs = Webhooks.map(async (i) => { const rawKey = GM_getValue(i.keyName, false); // 运行时读取,配置修改立即生效 if (!rawKey) return; const safeKey = String(rawKey).trim(); const targetUrl = safeKey.startsWith("http") ? safeKey : i.url + safeKey; try { await this.xhr({ method: "POST", url: targetUrl, headers: { "content-type": "application/json; charset=UTF-8" }, data: JSON.stringify(i.msg), }); GM_log(`🔵 「${i.name}」消息推送完成`); } catch (e) { GM_log(`🔴 「${i.name}」消息推送出错: ${this.fmtErr(e)}`); } }); await Promise.allSettled(jobs); }, // ===================================================== // 网络请求封装(加固版) // 【特性】 // - 单次结算保护(settled 标志) // - 错误分类:RewardsError(kind, status) // - 支持重定向(301/302/307/308) // - 15 秒默认超时 // ===================================================== xhr(options) { return new Promise((resolve, reject) => { const start = Date.now(); let settled = false; const finish = (fn, arg) => { if (settled) return; settled = true; fn(arg); }; GM_xmlhttpRequest({ ...options, timeout: options.timeout || 15000, onload: (res) => { const cost = ((Date.now() - start) / 1000).toFixed(2); if (res.status >= 200 && res.status < 300) { finish(resolve, res.responseText); } else if ([301, 302, 307, 308].includes(res.status)) { const m = res.responseHeaders?.match(/Location:\s*(.*?)\s*\r?\n/i); finish(resolve, m ? m[1] : false); } else { finish(reject, new RewardsError(`HTTP ${res.status},用时 ${cost} 秒`, { status: res.status, kind: "http" })); } }, onerror: (err) => { const cost = ((Date.now() - start) / 1000).toFixed(2); finish(reject, new RewardsError(`${err?.error || "网络错误"},用时 ${cost} 秒`, { kind: "network", cause: err })); }, ontimeout: () => { const cost = ((Date.now() - start) / 1000).toFixed(2); finish(reject, new RewardsError(`请求超时,用时 ${cost} 秒`, { kind: "timeout" })); }, onabort: () => finish(reject, new RewardsError("请求被中止", { kind: "abort" })) }); }); }, // ===================================================== // 带退避重试的 xhr // 【默认重试错误】网络错误、超时、408、429、5xx // 【不重试】4xx(认证/参数错误,重试无意义) // ===================================================== async xhrRetry(options, { retries = 2, delays = [1500, 4000, 8000], retryOn = ["network", "timeout", 408, 429, 500, 502, 503, 504] } = {}) { let lastErr; for (let attempt = 0; attempt <= retries; attempt++) { try { return await this.xhr(options); } catch (e) { lastErr = e; const isRetryable = (e instanceof RewardsError) ? (retryOn.includes(e.kind) || retryOn.includes(e.status)) : true; if (!isRetryable || attempt === retries) break; await this.delay(delays[attempt] ?? 3000); } } throw lastErr; }, // ===================================================== // 任务级错误隔离 // 【说明】任何子任务抛错都被捕获并记录,不影响后续任务 // ===================================================== async safeRun(label, fn, fallback = null) { try { return await fn(); } catch (e) { GM_log(`🔴 [${label}] ${this.fmtErr(e)}`); return fallback; } }, // 统一错误格式化 fmtErr(e) { if (e instanceof RewardsError) { return `[${e.kind}${e.status ? " " + e.status : ""}] ${e.message}`; } return e?.message || String(e); }, // 随机数范围生成 randomRange(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); }, // 时间戳获取 getTimestamp() { return Date.now(); }, // 获取今日日期数字格式 YYYYMMDD getTodayNum() { const d = new Date(); return Number(`${d.getFullYear()}${String(d.getMonth()+1).padStart(2,"0")}${String(d.getDate()).padStart(2,"0")}`); }, // 获取今日日期字符串格式 M/D/YYYY getTodayStr() { const d = new Date(); return `${d.getMonth()+1}/${d.getDate()}/${d.getFullYear()}`; }, // 生成随机 UUID getRandomUUID() { return crypto.randomUUID().replace(/-/g, "").toUpperCase(); }, // JSON 验证 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)); }, // 等待「文本命中指定关键词」的 DOM 元素出现 // 【用途】rewards.bing.com 页面内的打卡卡片点击(rewards 页面 DOM 为动态渲染, // 脚本又运行在 document-start,必须等节点挂载后才能操作) // 【返回】[{ element, pattern }],超时则 reject 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); }); } }; // ===================================================== // API 交互层 // 【改动说明】 // - getToken 明确区分「认证失败=false」与「网络异常=null」 // - 使用 xhrRetry 提升网络容错 // ===================================================== const API = { // Token 获取 // 返回值语义: // true —— 成功 // false —— 认证层面失败(会清空本地 token) // null —— 网络层面失败(保留 token) async getToken(url, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const res = await Utils.xhrRetry({ url }, { retries: 1 }); if (!Utils.isJSON(res)) { if (attempt < maxRetries) { await Utils.delay(3210); continue; } return false; } const data = JSON.parse(res); if (data.error) { Utils.log("🔴", `Token错误: ${data.error} - ${data.error_description || ''}`); if (["invalid_grant","invalid_request"].includes(data.error)) { GM_setValue("Config.token", false); GM_setValue("Config.code", ""); } return false; } if (data.refresh_token && data.access_token) { GM_setValue("Config.token", data.refresh_token); GM_setValue("Config.tokenTime", Utils.getTimestamp()); RewardsAuto.state.token = data.access_token; return true; } if (attempt < maxRetries) { await Utils.delay(3210); continue; } return false; } catch (e) { const isAuth = (e instanceof RewardsError) && (e.status === 400 || e.status === 401); if (isAuth) { GM_setValue("Config.token", false); GM_setValue("Config.code", ""); return false; } if (attempt < maxRetries) { await Utils.delay(3210 * attempt); continue; } Utils.log("🔴", `Token请求失败: ${Utils.fmtErr(e)}`); return null; } } return null; }, // Token 续期 // 【流程】优先复用本地未过期 token → 失效则用授权码重换 → 仍失败则引导重新授权 // 【注意】App 通道(签入/阅读)全部依赖此 token;PC 通道走浏览器登录态,不受影响 async renewToken() { if (!GM_getValue("Tasks.sign", true) && !GM_getValue("Tasks.read", true)) return true; let refreshToken = GM_getValue("Config.token", false); const tokenTime = GM_getValue("Config.tokenTime", 0); // Token 超过 7 天提前续期 if (tokenTime > 0) { const days = (Utils.getTimestamp() - tokenTime) / (1000 * 60 * 60 * 24); if (days > 7) { Utils.log("🟡", `Token已${Math.floor(days)}天,提前续期`); refreshToken = false; } } const authUrl = "https://login.live.com/oauth20_authorize.srf?client_id=0000000040170455&response_type=code&scope=service::prod.rewardsplatform.microsoft.com::MBI_SSL&redirect_uri=https://login.live.com/oauth20_desktop.srf"; // 自动获取授权码 const fetchCode = async (msg) => { GM_setValue("Config.code", ""); Utils.log("🟡", `${msg},尝试自动获取授权码...`); try { const res = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: authUrl, headers: { "User-Agent": navigator.userAgent }, onload: (r) => resolve(r), onerror: () => reject(new Error("请求失败")), ontimeout: () => reject(new Error("超时")), timeout: 15000 }); }); const finalUrl = res.finalUrl || ""; const code = new URL(finalUrl).searchParams.get("code"); if (code) { Utils.log("🟢", "自动获取授权码成功"); return [code]; } } catch (e) { Utils.log("🟡", `自动获取失败: ${e.message}`); } // 手动授权引导 Utils.log("🟡", "请手动完成授权..."); GM_openInTab(authUrl, { active: true, insert: true, setParent: true }); GM_notification({ text: "完成后粘贴地址栏URL到脚本设置的「授权码链接」", title: "🟡 需要授权", timeout: 0 }); // 等待用户粘贴授权码(最长 3 分钟) for (let i = 0; i < 180; i++) { await Utils.delay(1000); const code = GM_getValue("Config.code", ""); const match = code?.match(/M\.[^&]+/); if (match) { Utils.log("🟢", "授权码获取成功"); return [decodeURIComponent(match[0])]; } } Utils.log("🔴", "授权码获取超时", true); return false; }; if (!refreshToken) { const codeMatch = await fetchCode("检测到授权码为空"); if (!codeMatch) return false; const url = `https://login.live.com/oauth20_token.srf?client_id=0000000040170455&code=${encodeURIComponent(codeMatch[0])}&redirect_uri=https://login.live.com/oauth20_desktop.srf&grant_type=authorization_code`; const token = await this.getToken(url); if (token === null) return false; if (!token) { const retry = await fetchCode("授权码失效"); if (!retry) return false; return await this.renewToken(); } Utils.log("🟢", "Token获取成功!", true); return true; } else { const url = `https://login.live.com/oauth20_token.srf?client_id=0000000040170455&refresh_token=${encodeURIComponent(refreshToken)}&scope=service::prod.rewardsplatform.microsoft.com::MBI_SSL&grant_type=REFRESH_TOKEN`; const token = await this.getToken(url); if (token === null) return false; if (!token) { const retry = await fetchCode("Token失效"); if (!retry) return false; return await this.renewToken(); } return true; } }, // 获取积分仪表盘信息:余额、PC/移动搜索进度、今日得分明细、本月累计 // 【返回 false】解析失败。调用方需区分 false 与「真实 0 分」,否则会误报进度 async getRewardsInfo(maxRetries = 3) { // 优先使用 DAPI API 查询 if (RewardsAuto.state.token) { const apiResult = await Utils.safeRun("DAPI查询", () => this.getSearchQuotaFromAPI(), false); if (apiResult) { Utils.log("🟢", "使用 DAPI API 获取配额成功"); return apiResult; } Utils.log("🟡", "DAPI API 查询失败,回退到页面解析"); } // 回退到页面解析 for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const html = await Utils.xhrRetry({ url: "https://rewards.bing.com/earn" }, { retries: 1 }); const clean = html.replace(/\\"/g, '"'); let balance = 0; let pcMax = 60, pcCur = 0, mobMax = 0, mobCur = 0; let dailyOffer = 0; // 方法1: 从RSC JSON数据中解析 const rscMatch = clean.match(/"pointsCounters":\{[^}]*"pc":\{"max":(\d+),"progress":(\d+)\}[^}]*"mobile":\{"max":(\d+),"progress":(\d+)\}[^}]*"dailyOffer":(\d+)[^}]*"totalPoints":(\d+)/); if (rscMatch) { pcMax = parseInt(rscMatch[1]); pcCur = parseInt(rscMatch[2]); mobMax = parseInt(rscMatch[3]); mobCur = parseInt(rscMatch[4]); dailyOffer = parseInt(rscMatch[5]); balance = parseInt(rscMatch[6]); } else { // 方法2: 传统格式解析 const balMatch = clean.match(/"balance":(\d+)/) || clean.match(/"availablePoints":(\d+)/) || clean.match(/"totalPoints":(\d+)/); if (balMatch) balance = parseInt(balMatch[1]); const idx = clean.indexOf('"pointsCounters":{'); if (idx !== -1) { let start = idx + 17, depth = 0, end = start; for (let i = start; i < clean.length; 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; } catch {} } } if (balance === 0) { const balMatch2 = clean.match(/"balance":(\d+)/) || clean.match(/"availablePoints":(\d+)/); if (balMatch2) balance = parseInt(balMatch2[1]); } // 解析今日积分明细 const todayDetails = []; const activityCardsMatch = clean.match(/"activityCards":\[([^\]]+)\]/); if (activityCardsMatch) { try { const cardsStr = activityCardsMatch[1]; 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 {} } const searchHtmlMatch = clean.match(/必应搜索.*?]*>(\d+)<\/span>.*?]*>\/(\d+)<\/span>/); if (searchHtmlMatch) { todayDetails.push({ title: '必应搜索', points: parseInt(searchHtmlMatch[1]), max: parseInt(searchHtmlMatch[2]) }); } const searchRscMatch = clean.match(/"combinedSearch":\{[^}]*"progress":(\d+)[^}]*"max":(\d+)/); if (searchRscMatch && !todayDetails.some(d => d.title === '必应搜索')) { todayDetails.push({ title: '必应搜索', points: parseInt(searchRscMatch[1]), max: parseInt(searchRscMatch[2]) }); } if (dailyOffer > 0) { todayDetails.push({ title: '优惠', points: dailyOffer }); } const otherActivityRegex = /

([^<]+)<\/p><\/div>]*>(\d+)<\/div>/g; let otherMatch; while ((otherMatch = otherActivityRegex.exec(clean)) !== null) { const title = otherMatch[1]; const points = parseInt(otherMatch[2]); if (points > 0 && !todayDetails.some(d => d.title === title)) { todayDetails.push({ title, points }); } } // 解析历史积分 const history = { month: 0, year: 0, lifetime: 0 }; const historyRscMatch = clean.match(/"pointsHistory":\{[^}]*"thisMonth":\{"earn":(\d+)[^}]*"thisYear":\{"earn":(\d+)[^}]*"lifetime":\{"earn":(\d+)/); if (historyRscMatch) { history.month = parseInt(historyRscMatch[1]); history.year = parseInt(historyRscMatch[2]); history.lifetime = parseInt(historyRscMatch[3]); } else { const monthHtmlMatch = clean.match(/本月.*?(\d[\d,]*)<\/div>/); const yearHtmlMatch = clean.match(/今年.*?(\d[\d,]*)<\/div>/); const lifetimeHtmlMatch = clean.match(/生存期.*?(\d[\d,]*)<\/div>/); const monthJsonMatch = clean.match(/"monthlyPoints":(\d+)/); const yearJsonMatch = clean.match(/"yearlyPoints":(\d+)/); const lifetimeJsonMatch = clean.match(/"lifetimePoints":(\d+)/); if (monthHtmlMatch) history.month = parseInt(monthHtmlMatch[1].replace(/,/g, '')); else if (monthJsonMatch) history.month = parseInt(monthJsonMatch[1]); if (yearHtmlMatch) history.year = parseInt(yearHtmlMatch[1].replace(/,/g, '')); else if (yearJsonMatch) history.year = parseInt(yearJsonMatch[1]); if (lifetimeHtmlMatch) history.lifetime = parseInt(lifetimeHtmlMatch[1].replace(/,/g, '')); else if (lifetimeJsonMatch) history.lifetime = parseInt(lifetimeJsonMatch[1]); } return { balance, pc: { progress: pcCur, max: pcMax }, mobile: { progress: mobCur, max: mobMax }, dailyOffer, todayDetails, history }; } catch (e) { if (attempt < maxRetries) { await Utils.delay(3210); continue; } Utils.log("🔴", `仪表盘获取失败: ${Utils.fmtErr(e)}`); return false; } } return false; }, // App 端签入任务 async signApp() { const region = GM_getValue("Config.lock", true) ? "cn" : RewardsAuto.state.region.toLowerCase(); try { const res = await Utils.xhr({ method: "POST", url: "https://prod.rewardsplatform.microsoft.com/dapi/me/activities", headers: { "content-type": "application/json; charset=UTF-8", "user-agent": RewardsAuto.ua.app, "authorization": `Bearer ${RewardsAuto.state.token}`, "x-rewards-appid": RewardsAuto.appConfig.rewardsAppId, "x-rewards-ismobile": "true", "x-rewards-country": region, "x-rewards-partnerid": "startapp", "x-rewards-flights": "rwgobig" }, data: JSON.stringify({ amount: 1, id: Utils.getRandomUUID(), type: 103, country: region, channel: RewardsAuto.appConfig.channel }) }); if (Utils.isJSON(res)) return JSON.parse(res).response?.activity?.p || 0; } catch (e) { Utils.log("🔴", `App签入失败: ${Utils.fmtErr(e)}`); } return -1; }, // PC 端签入任务 async signPC() { try { const res = await Utils.xhr({ method: "POST", url: "https://rewards.bing.com/api/reportactivity?X-Requested-With=XMLHttpRequest", headers: { "content-type": "application/x-www-form-urlencoded", "user-agent": RewardsAuto.ua.pc, "referer": "https://rewards.bing.com/" }, data: "id=Gamification_DailyCheckIn&hash=1&activityAmount=1" }); if (Utils.isJSON(res)) { const data = JSON.parse(res); return data.points || 0; } } catch (e) { Utils.log("🟡", `PC签入失败: ${Utils.fmtErr(e)}`); } return -1; }, // 获取阅读进度 // 【说明】读取 App 通道的 promotions,按 offerId 找到阅读任务的 progress/max。 // 返回 false 表示查询失败,调用方会跳过本轮阅读而不是误判为已完成。 async getReadProgress() { try { const res = await Utils.xhr({ url: "https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613", headers: { "content-type": "application/json; charset=UTF-8", "user-agent": RewardsAuto.ua.app, "authorization": `Bearer ${RewardsAuto.state.token}`, "x-rewards-appid": RewardsAuto.appConfig.rewardsAppId, "x-rewards-ismobile": "true" } }); if (Utils.isJSON(res)) { const promos = JSON.parse(res).response?.promotions || []; const task = promos.find(x => x.attributes?.offerid === RewardsAuto.appConfig.offerIds.readArticle); if (task && task.attributes) return { progress: parseInt(task.attributes.progress) || 0, max: parseInt(task.attributes.max) || 30 }; } } catch (e) { Utils.log("🔴", `阅读进度获取失败: ${Utils.fmtErr(e)}`); } return false; }, // 通过 DAPI API 获取搜索配额 async getSearchQuotaFromAPI() { try { const res = await Utils.xhr({ url: "https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613", headers: { "content-type": "application/json; charset=UTF-8", "user-agent": RewardsAuto.ua.app, "authorization": `Bearer ${RewardsAuto.state.token}`, "x-rewards-appid": RewardsAuto.appConfig.rewardsAppId, "x-rewards-ismobile": "true" } }); if (Utils.isJSON(res)) { const data = JSON.parse(res); const response = data.response || {}; const counters = response.counters || response.userStatus?.counters; if (!counters) { Utils.log("🟡", "DAPI 未返回 counters,回退到页面解析"); return false; } let pcCur = 0, pcMax = 0; if (counters.pcSearch && counters.pcSearch.length > 0) { pcCur = counters.pcSearch[0].pointProgress || 0; pcMax = counters.pcSearch[0].pointProgressMax || 0; } let mobCur = 0, mobMax = 0; if (counters.mobileSearch && counters.mobileSearch.length > 0) { mobCur = counters.mobileSearch[0].pointProgress || 0; mobMax = counters.mobileSearch[0].pointProgressMax || 0; } if (pcMax === 0 && mobMax === 0) { Utils.log("🟡", "DAPI 返回配额 0/0,回退到页面解析"); return false; } const balance = response.balance || response.userStatus?.availablePoints || 0; Utils.log("📊", `DAPI查询: PC ${pcCur}/${pcMax}, Mobile ${mobCur}/${mobMax}, 积分 ${balance}`); return { balance: balance, pc: { progress: pcCur, max: pcMax }, mobile: { progress: mobCur, max: mobMax }, dailyOffer: 0, todayDetails: [], history: null }; } } catch (e) { Utils.log("🟡", `DAPI查询失败: ${Utils.fmtErr(e)}`); } return false; }, // 执行阅读 async doRead() { const region = GM_getValue("Config.lock", true) ? "cn" : RewardsAuto.state.region.toLowerCase(); // ===================================================== // 【修复】原实现写死 ENUS_readarticle3_30points,这是「英文美国区」 // 的 offerId。国区(zh-CN)账号上报该 id 不会被计入阅读进度, // 表现为阅读任务每天跑但进度不涨。 // 现按所在市场动态生成 offerId,失败再回退原值。 // ===================================================== const market = region === "cn" ? "ZHCN" : region.toUpperCase(); const candidates = [ `${market}_readarticle3_30points`, RewardsAuto.appConfig.offerIds.readArticle, ]; for (const offerid of candidates) { try { await Utils.xhr({ method: "POST", url: "https://prod.rewardsplatform.microsoft.com/dapi/me/activities", headers: { "content-type": "application/json; charset=UTF-8", "user-agent": RewardsAuto.ua.app, "authorization": `Bearer ${RewardsAuto.state.token}`, "x-rewards-appid": RewardsAuto.appConfig.rewardsAppId, "x-rewards-ismobile": "true", "x-rewards-country": region }, data: JSON.stringify({ amount: 1, country: region, id: Utils.getRandomUUID(), type: 101, attributes: { offerid } }) }); return true; } catch (e) { Utils.log("🟡", `阅读上报失败(${offerid}): ${Utils.fmtErr(e)}`); } } return false; }, // 活动卡片发现:抓取 /earn 页面,从 RSC 数据里解析可领取的卡片 // 【返回值语义】 // 数组 —— 解析成功(空数组 = 今天确实没有可领卡片) // null —— 抓取/解析失败,调用方【不得】据此把当天标记为已完成 async discoverCards() { const cards = []; try { const html = await Utils.xhrRetry({ url: "https://rewards.bing.com/earn" }, { retries: 1 }); const clean = html.replace(/\\"/g, '"'); // 方法1: 从RSC数据中解析activityCards数组 const activityCardsMatch = clean.match(/"activityCards":\[([\s\S]*?)\](?=,"|$)/); if (activityCardsMatch) { try { const cardsStr = activityCardsMatch[1]; const cardObjRegex = /\{[^{}]*\}/g; let cardObjMatch; while ((cardObjMatch = cardObjRegex.exec(cardsStr)) !== null) { const cardObj = cardObjMatch[0]; const offerIdMatch = cardObj.match(/"offerId":"([^"]+)"/); const hashMatch = cardObj.match(/"hash":"([^"]+)"/); const pointsMatch = cardObj.match(/"points":(\d+)/); const isCompletedMatch = cardObj.match(/"isCompleted":(true|false)/); if (!offerIdMatch || !hashMatch) continue; const offerId = offerIdMatch[1]; const hash = hashMatch[1]; const points = pointsMatch ? parseInt(pointsMatch[1]) : 0; const isCompleted = isCompletedMatch ? isCompletedMatch[1] === "true" : false; if (isCompleted || points <= 0) continue; const titleMatch = cardObj.match(/"title":"([^"]+)"/); let title = titleMatch ? titleMatch[1] : ""; const skip = RewardsAuto.skipPatterns.some(p => title.toLowerCase().includes(p.toLowerCase()) || offerId.toLowerCase().includes(p.toLowerCase()) ); if (skip) continue; let kind = "unknown"; if (/quiz|trivia/i.test(offerId)) kind = "quiz"; else if (/puzzle/i.test(offerId)) kind = "puzzle"; else if (/image/i.test(offerId)) kind = "image_creator"; else if (/explore|search/i.test(offerId)) kind = "explore_search"; else if (/dailyset|daily/i.test(offerId)) kind = "daily"; else if (/streak/i.test(offerId)) kind = "streak"; else kind = "open_only"; cards.push({ title, points, offerId, hash, kind }); } } catch (e) { Utils.log("🟡", `RSC卡片解析异常: ${e.message}`); } } // 方法2: 传统正则匹配(备用) if (cards.length === 0) { const objRegex = /\{[^{}]*"offerId":"[^"]+"[^{}]*"hash":"[^"]+"[^{}]*\}/g; let objMatch; while ((objMatch = objRegex.exec(clean)) !== null) { const cardObj = objMatch[0]; const offerIdMatch = cardObj.match(/"offerId":"([^"]+)"/); const hashMatch = cardObj.match(/"hash":"([^"]+)"/); const pointsMatch = cardObj.match(/"points":(\d+)/); const isCompletedMatch = cardObj.match(/"isCompleted":(true|false)/); const titleMatch = cardObj.match(/"title":"([^"]+)"/); if (!offerIdMatch || !hashMatch) continue; const offerId = offerIdMatch[1]; const hash = hashMatch[1]; const points = pointsMatch ? parseInt(pointsMatch[1]) : 0; const isCompleted = isCompletedMatch ? isCompletedMatch[1] === "true" : false; const title = titleMatch ? titleMatch[1] : ""; if (isCompleted || points <= 0) continue; const skip = RewardsAuto.skipPatterns.some(p => title.toLowerCase().includes(p.toLowerCase()) || offerId.toLowerCase().includes(p.toLowerCase()) ); if (skip) continue; let kind = "unknown"; if (/quiz|trivia/i.test(offerId)) kind = "quiz"; else if (/puzzle/i.test(offerId)) kind = "puzzle"; else if (/image/i.test(offerId)) kind = "image_creator"; else if (/explore|search/i.test(offerId)) kind = "explore_search"; else if (/dailyset|daily/i.test(offerId)) kind = "daily"; else if (/streak/i.test(offerId)) kind = "streak"; else kind = "open_only"; cards.push({ title, points: parseInt(points), offerId, hash, kind }); } } } catch (e) { Utils.log("🔴", `卡片解析失败: ${Utils.fmtErr(e)}`); // 【修复】失败必须返回 null,与「确实没有卡片」([]) 区分开。 // 原实现一律返回 [],调用方会据此把当天标记为已完成。 return null; } return cards; }, // 领取卡片奖励 async claimCard(card) { // ===================================================== // 【修复】原实现只有一条路径:向 /earn 提交 Next.js Server // Action,且 action id 写死为 "70babbc81d2724f60d29a95c03b3d739cba77cea92"。 // 该 id 由前端构建产物生成,微软每次发版都会变 —— 一旦变化, // 服务端一律拒绝,表现为「每日三任务 / 拼图 / Quiz 全部领不到」。 // 现改为优先调用更稳定的官方 reportactivity 接口, // 失败再回退到原 Server Action,两条路都走不通才算失败。 // ===================================================== try { const res = await Utils.xhr({ method: "POST", url: "https://rewards.bing.com/api/reportactivity?X-Requested-With=XMLHttpRequest", headers: { "content-type": "application/x-www-form-urlencoded", "user-agent": RewardsAuto.ua.pc, "referer": "https://rewards.bing.com/" }, data: `id=${encodeURIComponent(card.offerId)}&hash=${encodeURIComponent(card.hash)}&activityAmount=1&timezoneOffset=-480` }); if (Utils.isJSON(res)) { const d = JSON.parse(res); if (d && (typeof d.points !== "undefined" || d.response || d.activity)) { Utils.log("🟢", `已上报(reportactivity): ${card.title || card.offerId}`); return true; } } } catch (e) { Utils.log("🟡", `reportactivity 失败,回退 Server Action: ${Utils.fmtErr(e)}`); } try { await Utils.xhr({ method: "POST", url: "https://rewards.bing.com/earn", headers: { "content-type": "text/plain;charset=UTF-8", "next-action": "70babbc81d2724f60d29a95c03b3d739cba77cea92", "referer": "https://rewards.bing.com/earn" }, data: JSON.stringify([card.hash, 11, { offerid: card.offerId, isPromotional: "$undefined", timezoneOffset: "-480" }]) }); return true; } catch (e) { Utils.log("🟡", `卡片领取失败: ${Utils.fmtErr(e)}`); return false; } }, // 搜索页面请求 async getSearchPage(query, isMobile = false) { const mkt = GM_getValue("Config.lock", true) ? "&mkt=zh-CN" : ""; const deviceType = isMobile ? "m" : "d"; return Utils.xhr({ url: `https://${RewardsAuto.state.host}/search?q=${encodeURIComponent(query)}&form=QBLH${mkt}`, headers: { "user-agent": isMobile ? RewardsAuto.ua.mobile : RewardsAuto.ua.pc, // 【修复】不再覆盖 Cookie 头,交由 GM_xmlhttpRequest // 自动携带浏览器真实登录态,否则搜索不计分 "referer": `https://${RewardsAuto.state.host}/?form=QBLH`, "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8" } }); }, // 搜索活动上报 async reportSearch(html, query, isMobile = false) { try { const clean = html.replace(/\s/g, ""); // 【修复】原实现只匹配一种写法,页面结构一变就会退化成随机 UUID, // 导致上报被服务端拒绝(不计分)。现增加多套备选模式。 const ig = clean.match(/,IG:"([^"]+)"/)?.[1] || clean.match(/"IG":"([^"]+)"/)?.[1] || clean.match(/IG=([A-F0-9]{16,})/i)?.[1] || Utils.getRandomUUID(); const data = clean.match(/class="b_algo(.*?)href="(.*?)"h="ID=(.*?)">(.*?)<\/h2/); const mkt = GM_getValue("Config.lock", true) ? "&mkt=zh-CN" : ""; const params = `q=${encodeURIComponent(query)}&form=QBLH${mkt}`; const deviceType = isMobile ? "m" : "d"; const headers = { "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "user-agent": isMobile ? RewardsAuto.ua.mobile : RewardsAuto.ua.pc, // 【修复】同样移除覆盖式 Cookie 头,保留真实登录态 "referer": `https://${RewardsAuto.state.host}/?form=QBLH`, "accept": "*/*", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8" }; await Utils.xhr({ method: "POST", url: `https://${RewardsAuto.state.host}/rewardsapp/ncheader?ver=88888888&IID=SERP.5047&IG=${ig}&ajaxreq=1`, headers, data: "wb=1%3bi%3d1%3bv%3d1" }); await Utils.xhr({ method: "POST", url: `https://${RewardsAuto.state.host}/rewardsapp/reportActivity?IG=${ig}&IID=SERP.5047&${params}&ajaxreq=1`, headers, data: `url=${encodeURIComponent(`https://${RewardsAuto.state.host}/search?${params}`)}&V=web` }); if (data) { await Utils.xhr({ url: `https://${RewardsAuto.state.host}/fd/ls/GLinkPingPost.aspx?IG=${ig}&ID=${data[3]}&url=${data[2]}`, headers }); } return true; } catch (e) { Utils.log("🟡", `搜索上报失败: ${Utils.fmtErr(e)}`); return false; } }, // 地区检测(增加重试机制和IP信息获取) async checkRegion(retryCount = 0) { if (!GM_getValue("Config.lock", true)) return true; try { const html = await Utils.xhrRetry({ url: `https://${RewardsAuto.state.host}/` }, { retries: 1 }); if (!html) { if (retryCount < 2) { Utils.log("🟡", `地区检测返回空,第${retryCount + 1}次重试...`); await Utils.randomDelay(3000, 5000); return await this.checkRegion(retryCount + 1); } // 【修复】原逻辑在此返回 false,会导致后续「签入/阅读/活动」 // 全部被跳过。检测无响应多为网络波动,并非真的境外 IP, // 现改为放行,避免误杀全部任务。 Utils.log("🟡", "地区检测无响应,按放行处理(避免网络波动误停任务)"); return true; } const match = html.replace(/\s/g, "").match(/Region:"(.*?)"(.*?)RevIpCC:"(.*?)"/); if (match) { RewardsAuto.state.region = match[3].toUpperCase(); if (RewardsAuto.state.region !== "CN") { await this.getIPInfo(); Utils.log("🔴", `IP非大陆(${RewardsAuto.state.region}),已停止\n${RewardsAuto.state.ipInfo}`, true); return false; } Utils.log("🟢", `地区检测通过: ${RewardsAuto.state.region}`); return true; } if (retryCount < 2) { Utils.log("🟡", `地区检测格式异常,第${retryCount + 1}次重试...`); await Utils.randomDelay(3000, 5000); return await this.checkRegion(retryCount + 1); } Utils.log("🟡", "地区检测格式不匹配,按放行处理"); return true; } catch (e) { if (retryCount < 2) { Utils.log("🟡", `地区检测异常: ${Utils.fmtErr(e)},第${retryCount + 1}次重试...`); await Utils.randomDelay(3000, 5000); return await this.checkRegion(retryCount + 1); } Utils.log("🟡", `地区检测异常(${Utils.fmtErr(e)}),按放行处理`); return true; } }, // ===================================================== // 【新增】登录态自检 // ----------------------------------------------------- // 搜索计分的前提是请求携带 Bing 登录 Cookie。原脚本没有该项 // 检测,未登录时会静默地「跑完但不加分」,用户无从排查。 // 现在在每轮任务开始时自检,未登录则给出明确指引。 // ===================================================== async checkLogin() { try { const html = await Utils.xhrRetry({ url: "https://rewards.bing.com/" }, { retries: 1 }); if (!html) { Utils.log("🟡", "登录检测:无响应(可能为网络波动)"); return false; } if (/login\.live\.com|Sign in to your|id="i0116"|登录你的 Microsoft/i.test(html)) { Utils.log("🔴", "未检测到 Bing 登录态,搜索将不会加分!请在 Edge 浏览器中打开 https://rewards.bing.com 完成登录并保持登录状态。", true); return false; } Utils.log("🟢", "登录态检测通过"); return true; } catch (e) { Utils.log("🟡", `登录检测异常: ${Utils.fmtErr(e)}`); return false; } }, // 获取IP详细信息 async getIPInfo() { try { const qryResult = await Utils.xhr({ url: "https://disp-qryapi.3g.qq.com/v1/dispatch", headers: { "referer": "https://3g.qq.com/" } }); if (qryResult && Utils.isJSON(qryResult)) { const resJSON = JSON.parse(qryResult); RewardsAuto.state.ip = (resJSON.code == 0 && resJSON.extra && resJSON.extra.ip) ? resJSON.extra.ip : ""; let rawInfo = (resJSON.code == 0 && resJSON.ipInfo) ? String(resJSON.ipInfo) : ""; rawInfo = rawInfo.replace(/[#*]+/g, " ").trim(); RewardsAuto.state.ipInfo = rawInfo ? `🌏所在地区:${rawInfo}` : ""; } } catch { console.debug("获取附加 IP 信息失败"); } }, // 获取热搜搜索词 async getHotSearchWord() { const keywords = ["天气预报", "今日新闻", "体育赛事", "股票行情", "电影推荐", "科技资讯", "美食食谱", "旅游攻略", "历史上的今天", "健康常识"]; const baseWord = keywords[Utils.randomRange(0, keywords.length - 1)]; const randomSuffix = Math.random().toString(36).slice(2, 6); let sentence = `${baseWord} ${randomSuffix}`; if (RewardsAuto.apiConfig.mode !== "offline") { if (RewardsAuto.apiConfig.wordIndex < 1 || RewardsAuto.apiConfig.wordList.length < 1) { const apiArr = RewardsAuto.apiConfig.arr; const lastApiIndex = parseInt(GM_getValue("Config.apiIndex", -1)); const filteredArr = apiArr.filter((_, index) => index !== lastApiIndex); const randomIndex = Utils.randomRange(0, filteredArr.length - 1); GM_setValue("Config.apiIndex", randomIndex); const [apiName, apiConfig] = filteredArr[randomIndex]; RewardsAuto.apiConfig.url = apiConfig.url; RewardsAuto.apiConfig.hot = apiConfig.hot; try { const hotSource = RewardsAuto.apiConfig.hot[Utils.randomRange(0, RewardsAuto.apiConfig.hot.length - 1)]; const result = await Utils.xhr({ url: RewardsAuto.apiConfig.url + hotSource }); if (result && Utils.isJSON(result)) { const res = JSON.parse(result); if (res.code == 200) { RewardsAuto.apiConfig.wordIndex = 1; RewardsAuto.apiConfig.wordList = []; for (let i = 0; i < res.data.length; i++) { RewardsAuto.apiConfig.wordList.push(res.data[i].title); } RewardsAuto.apiConfig.wordList.sort(() => Math.random() - 0.5); sentence = RewardsAuto.apiConfig.wordList[RewardsAuto.apiConfig.wordIndex]; sentence = sentence.substring(0, Utils.randomRange(20, 32)); return sentence; } } } catch (e) { Utils.log("🟡", `热搜词获取失败: ${Utils.fmtErr(e)}`); } } else { RewardsAuto.apiConfig.wordIndex++; if (RewardsAuto.apiConfig.wordIndex > RewardsAuto.apiConfig.wordList.length - 1) { RewardsAuto.apiConfig.wordIndex = 0; } sentence = RewardsAuto.apiConfig.wordList[RewardsAuto.apiConfig.wordIndex]; sentence = sentence.substring(0, Utils.randomRange(20, 32)); return sentence; } Utils.log("🟡", "热搜词接口异常,已使用随机搜索词"); } return sentence; }, // 搜索受限检测 async checkSearchRestricted() { const currentTotalSearch = RewardsAuto.state.pcProgress + RewardsAuto.state.mobileProgress; const lastTotalSearch = RewardsAuto.state.lastSearchProgress; if (lastTotalSearch !== -1) { if (currentTotalSearch === lastTotalSearch && currentTotalSearch < (RewardsAuto.state.pcMax + RewardsAuto.state.mobileMax)) { RewardsAuto.state.restrictedTimes++; } else { RewardsAuto.state.restrictedTimes = 0; } } RewardsAuto.state.lastSearchProgress = currentTotalSearch; GM_setValue("Config.lastSearchProgress", currentTotalSearch); GM_setValue("Config.restrictedTimes", RewardsAuto.state.restrictedTimes); if (RewardsAuto.state.restrictedTimes >= 3) { Utils.log("🔴", "搜索受限或账号异常,已中断今日搜索!", true); return true; } return false; } }; // ===================================================== // 任务编排层 // 【设计要点】 // - 每个子任务都经 Utils.safeRun 包裹:单个任务抛错不会中断整轮运行 // - 完成状态按「日期戳」持久化(Config.tasks),跨标签页/跨次运行共享 // - 每项任务单日最多重试 2 次(*Times > 2),避免失败后无限空转 // - runAll 开头/结束各调用一次 reportTaskStatus,让用户明确知道哪些已完成 // ===================================================== const TaskManager = { // 四类任务各自「最近一次完成」的日期戳,格式 YYYYMMDD signDate: 0, readDate: 0, promosDate: 0, searchDate: 0, // signPoint:当日签入累计得分;*Times:当日失败重试计数 signPoint: -1, signTimes: 0, readTimes: 0, promosTimes: 0, // 读取持久化的完成状态。注意 key 从旧版的 Tasks.* 迁到 Config.tasks, // 旧数据不会自动迁移,首次运行视为「未完成」,属预期行为 init() { RewardsAuto.state.dateNowNum = Utils.getTodayNum(); RewardsAuto.state.dateNowStr = Utils.getTodayStr(); const tasks = GM_getValue("Config.tasks", {}); this.signDate = tasks.sign || 0; this.readDate = tasks.read || 0; this.promosDate = tasks.promos || 0; this.searchDate = tasks.search || 0; this.signPoint = GM_getValue("Config.signPoint", -1); }, save() { GM_setValue("Config.tasks", { sign: this.signDate, read: this.readDate, promos: this.promosDate, search: this.searchDate }); }, // 签入任务:App 通道 + PC 通道各上报一次,两条路都走才算完整 // 【说明】App 通道需要有效 token,静默执行不弹窗;PC 通道依赖浏览器登录态 async doSign() { if (!GM_getValue("Tasks.sign", true) || this.signTimes > 2) return; if (this.signPoint >= 0 && this.signDate === RewardsAuto.state.dateNowNum) { Utils.log("✅", `签入已完成(${this.signPoint}积分)`); return; } await Utils.randomDelay(); let totalPoint = 0; // App 端签到(静默执行) const appPoint = await Utils.safeRun("App签入", () => API.signApp(), -1); if (appPoint > 0) { GM_log(`📱 App签入静默成功 +${appPoint}积分`); totalPoint += appPoint; } // PC 端签到 await Utils.randomDelay(2000, 4000); const pcPoint = await Utils.safeRun("PC签入", () => API.signPC(), -1); if (pcPoint >= 0) { Utils.log("💻", `PC签入成功!+${pcPoint}积分`); totalPoint += pcPoint; } if (totalPoint > 0) { this.signPoint = totalPoint; this.signDate = RewardsAuto.state.dateNowNum; GM_setValue("Config.signPoint", totalPoint); this.save(); Utils.log("🔵", `签入任务完成!总积分 +${totalPoint}`, true); } else { this.signTimes++; Utils.log("🟡", `签入失败,稍后重试`); } }, // 阅读任务:先查进度,再按「每 3 点一次上报」补齐剩余次数 // 【说明】微软把阅读计分打包,单次上报 +3,故 needed = ceil(剩余/3) async doRead() { if (!GM_getValue("Tasks.read", true) || this.readTimes > 2) return; if (this.readDate === RewardsAuto.state.dateNowNum) { Utils.log("✅", "阅读任务已完成"); return; } const progress = await Utils.safeRun("阅读进度查询", () => API.getReadProgress(), false); if (!progress) { this.readTimes++; return; } const { progress: cur, max } = progress; Utils.log("📖", `阅读进度: ${cur}/${max}`); if (cur >= max) { this.readDate = RewardsAuto.state.dateNowNum; this.save(); Utils.log("✅", "阅读任务已完成"); return; } const needed = Math.ceil((max - cur) / 3); for (let i = 0; i < needed; i++) { await Utils.safeRun(`阅读#${i+1}`, () => API.doRead()); Utils.log("📖", `阅读 ${i+1}/${needed}`); await Utils.randomDelay(3000, 7000); } this.readDate = RewardsAuto.state.dateNowNum; this.save(); Utils.log("🔵", "阅读任务完成!", true); }, // 活动卡片处理 async doPromos() { if (!GM_getValue("Tasks.promos", true) || this.promosTimes > 2) return; if (this.promosDate === RewardsAuto.state.dateNowNum) { Utils.log("✅", "活动卡片已完成"); return; } Utils.log("🧩", "扫描活动卡片..."); const cards = await Utils.safeRun("扫描活动卡片", () => API.discoverCards(), null); // 【修复】原逻辑把「扫描失败」与「确实没有卡片」同等对待, // 只要解析不出卡片就把当天标记成已完成,此后当天不再重试。 // 这会导致每日三任务/拼图在解析失效时全天静默跳过。 if (cards === null) { Utils.log("🟡", "活动卡片扫描失败(未获取或解析异常),本轮不标记完成,稍后重试"); this.promosTimes++; return; } if (cards.length === 0) { this.promosDate = RewardsAuto.state.dateNowNum; this.save(); Utils.log("✅", "无新活动卡片"); return; } Utils.log("🧩", `发现 ${cards.length} 个卡片`); let ok = 0, fail = 0; for (const card of cards) { Utils.log(" ", `[${card.kind}] ${card.title} +${card.points}p`); if (card.kind === "quiz" && !GM_getValue("Tasks.quiz", true)) continue; await Utils.randomDelay(2000, 4000); const result = await Utils.safeRun("领取卡片", () => API.claimCard(card), false); result ? ok++ : fail++; } this.promosDate = RewardsAuto.state.dateNowNum; this.save(); Utils.log("🔵", `活动完成: ${ok}成功/${fail}失败`, true); }, // 搜索任务处理 async doSearch() { if (!GM_getValue("Tasks.search", true)) return; const info = await Utils.safeRun("获取积分信息", () => API.getRewardsInfo(), false); if (!info) { Utils.log("🔴", "无法获取积分信息"); return; } RewardsAuto.state.pcProgress = info.pc.progress; RewardsAuto.state.pcMax = info.pc.max; RewardsAuto.state.mobileProgress = info.mobile.progress; RewardsAuto.state.mobileMax = info.mobile.max; // 【新增】记录本轮开始前的配额,用于结束时校验是否真的计分 const beforeTotal = info.pc.progress + info.mobile.progress; Utils.log("🔍", `配额: PC ${info.pc.progress}/${info.pc.max}, Mobile ${info.mobile.progress}/${info.mobile.max}`); if (info.mobile.max === 0) { Utils.log("ℹ️", "你的账号没有移动端搜索配额,将只执行 PC 端搜索"); } const hasMobile = info.mobile.max > 0; const pcDone = info.pc.progress >= info.pc.max; const mobileDone = !hasMobile || info.mobile.progress >= info.mobile.max; if (pcDone && mobileDone) { this.searchDate = RewardsAuto.state.dateNowNum; this.save(); const mobileReport = hasMobile ? `\n📱 移动端: ${info.mobile.progress}/${info.mobile.max}` : ""; Utils.log("✅", `搜索配额已满\n💻 PC端: ${info.pc.progress}/${info.pc.max}${mobileReport}`); return; } if (this.searchDate === RewardsAuto.state.dateNowNum) { Utils.log("🟡", "搜索配额未满,继续执行搜索任务"); this.searchDate = 0; } const isRestricted = await Utils.safeRun("搜索受限检测", () => API.checkSearchRestricted(), false); if (isRestricted) { this.searchDate = RewardsAuto.state.dateNowNum; this.save(); return; } // 执行搜索(随机 4-7 次,硬上限 10 次防死循环) const limit = Utils.randomRange(4, 7); const maxIterations = Math.min(limit + 3, 10); const canSearchMobile = RewardsAuto.state.mobileMax > 0; for (let i = 0; i < maxIterations; i++) { if (RewardsAuto.state.pcProgress >= RewardsAuto.state.pcMax && (!canSearchMobile || RewardsAuto.state.mobileProgress >= RewardsAuto.state.mobileMax)) break; let isMobile = false; if (canSearchMobile && RewardsAuto.state.mobileProgress < RewardsAuto.state.mobileMax) { isMobile = Math.random() > 0.6; } if (isMobile) RewardsAuto.state.mobileProgress += 3; else RewardsAuto.state.pcProgress += 3; let query; if (RewardsAuto.apiConfig.mode !== "offline") { query = await Utils.safeRun("获取热搜词", () => API.getHotSearchWord(), RewardsAuto.searchPool[Utils.randomRange(0, RewardsAuto.searchPool.length - 1)]); } else { query = RewardsAuto.searchPool[Utils.randomRange(0, RewardsAuto.searchPool.length - 1)]; } Utils.log("🔍", `[${isMobile?"Mobile":"PC"}] 搜索 ${i+1}/${maxIterations}: ${query}`); await Utils.safeRun(`搜索#${i+1}`, async () => { const html = await API.getSearchPage(query, isMobile); if (html) await API.reportSearch(html, query, isMobile); }); const span = Number(GM_getValue("Config.span", 30)); const wait = Utils.randomRange((span-15)*1000, (span+15)*1000); Utils.log("⏳", `等待 ${wait/1000}秒`); await Utils.delay(wait); } const finalInfo = await Utils.safeRun("最终配额查询", () => API.getRewardsInfo(), false); if (finalInfo) { const hasMobile2 = finalInfo.mobile.max > 0; const pcDone2 = finalInfo.pc.progress >= finalInfo.pc.max; const mobileDone2 = !hasMobile2 || finalInfo.mobile.progress >= finalInfo.mobile.max; if (pcDone2 && mobileDone2) { this.searchDate = RewardsAuto.state.dateNowNum; this.save(); RewardsAuto.state.restrictedTimes = 0; GM_setValue("Config.restrictedTimes", 0); GM_setValue("Config.lastSearchProgress", -1); const mobileReport = hasMobile2 ? `\n📱 移动端: ${finalInfo.mobile.progress}/${finalInfo.mobile.max}` : ""; Utils.log("🔵", `🔍 搜索任务完成!\n💻 PC端: ${finalInfo.pc.progress}/${finalInfo.pc.max}${mobileReport}`, true); } else { const mobileReport = hasMobile2 ? `\n📱 移动端: ${finalInfo.mobile.progress}/${finalInfo.mobile.max}` : ""; Utils.log("🟡", `搜索已执行,配额未满\n💻 PC端: ${finalInfo.pc.progress}/${finalInfo.pc.max}${mobileReport}`); // 【新增】计分校验:本轮确实发了搜索请求,但配额毫无变化 const afterTotal = finalInfo.pc.progress + finalInfo.mobile.progress; if (afterTotal <= beforeTotal) { Utils.log("🔴", `搜索未计分(${beforeTotal} → ${afterTotal})!常见原因:① 浏览器未登录 Bing;② 账号被限制;③ 当日配额已用尽。请打开 https://rewards.bing.com 确认登录状态。`, true); } } } else { Utils.log("🟡", "搜索已执行,但无法获取最终配额状态"); } }, // 主任务调度(任务级隔离) // ===================================================== // 【新增】今日任务完成状态盘点 // ----------------------------------------------------- // 用户诉求:运行时能直观看到「哪些任务已完成、哪些还没做」。 // 原脚本只在个别分支里零散打印,没有统一清单,也没有推送通知。 // 现于每轮任务开始/结束时各盘点一次,并触发通知。 // ===================================================== async reportTaskStatus(stage) { const t = GM_getValue("Config.tasks", {}) || {}; const today = RewardsAuto.state.dateNowNum; const done = (d) => d === today; const mk = (b) => (b ? "✅ 已完成" : "⏳ 未完成"); const title = stage === "start" ? "运行前" : "运行后"; let msg = `📋 今日任务状态(${title})\n`; msg += `├ 每日签入: ${mk(done(t.sign))}\n`; msg += `├ 新闻阅读: ${mk(done(t.read))}\n`; msg += `├ 活动卡片: ${mk(done(t.promos))}\n`; msg += `└ 搜索任务: ${mk(done(t.search))}\n`; try { const info = await Utils.safeRun("状态查询", () => API.getRewardsInfo(), false); if (info) { msg += `\n💰 当前积分: ${info.balance}\n`; msg += `💻 PC搜索: ${info.pc.progress}/${info.pc.max}\n`; msg += `📱 移动搜索: ${info.mobile.progress}/${info.mobile.max}\n`; const remain = (info.pc.max - info.pc.progress) + (info.mobile.max - info.mobile.progress); if (remain > 0) msg += `\n还需搜索约 ${Math.ceil(remain / 3)} 次`; } } catch (e) { /* 状态查询失败不影响主流程 */ } Utils.log("📋", msg, true); }, // 主流程。顺序刻意安排如下,每一步的失败都被隔离,不阻断后续: // 0 盘点 → 1 地区 → 1.5 登录态 → 2 Token → 3 签入/阅读 → 4 活动卡片 // → 5 搜索 → 6 二次扫描 → 7 汇总 → 8 复盘 // 【为什么先查登录态】未登录时搜索也能发出请求,但微软不计分, // 早期版本因此出现「跑了一整天,进度条不动」。提前告警可省下无效运行。 async runAll() { RewardsAuto.state.startTime = Utils.getTimestamp(); Utils.log("🚀", "启动全能自动化任务..."); this.init(); // 0. 运行前:盘点今日已完成了哪些任务 await Utils.safeRun("任务状态盘点", () => this.reportTaskStatus("start")); // 1. 地区检测 const regionOK = await Utils.safeRun("地区检测", () => API.checkRegion(), false); // 1.5 【新增】登录态自检(搜索能否计分的前提) const isLoginOK = await Utils.safeRun("登录检测", () => API.checkLogin(), false); if (!isLoginOK) { Utils.log("🟡", "登录态未确认,本轮搜索可能不加分", true); } // 2. Token 续期 let isTokenOK = false; if (regionOK) { isTokenOK = await Utils.safeRun("Token续期", () => API.renewToken(), false); if (!isTokenOK) { Utils.log("🟡", "Token失败,跳过签入/阅读", true); } } else { Utils.log("🟡", "地区检测失败,尝试执行搜索任务..."); } // 3. 签入 + 阅读(依赖 token) if (regionOK && isTokenOK) { await Utils.safeRun("签入", () => this.doSign()); await Utils.randomDelay(); await Utils.safeRun("阅读", () => this.doRead()); await Utils.randomDelay(); } else if (regionOK) { await Utils.safeRun("签入", () => this.doSign()); await Utils.randomDelay(); } // 4. 活动卡片 await Utils.safeRun("活动卡片", () => this.doPromos()); await Utils.randomDelay(); // 5. 搜索 await Utils.safeRun("搜索", () => this.doSearch()); // 6. 二次扫描 await Utils.safeRun("二次扫描", async () => { Utils.log("🔄", "二次扫描:检查是否有新解锁的卡片..."); await Utils.randomDelay(2000, 4000); const newCards = await API.discoverCards(); // 【修复】兼容 null(扫描失败),避免对 null 取 length 报错 if (!newCards || newCards.length === 0) { Utils.log("✅", "二次扫描:无新卡片"); return; } Utils.log("🧩", `二次扫描发现 ${newCards.length} 个新卡片`); let ok = 0, fail = 0; for (const card of newCards) { Utils.log(" ", `[${card.kind}] ${card.title} +${card.points}p`); if (card.kind === "quiz" && !GM_getValue("Tasks.quiz", true)) continue; await Utils.randomDelay(2000, 4000); const result = await Utils.safeRun("二次扫描领取", () => API.claimCard(card), false); result ? ok++ : fail++; } Utils.log("🔵", `二次扫描完成: ${ok}成功/${fail}失败`); }); // 7. 汇总 await Utils.safeRun("汇总", () => this.reportSummary()); // 8. 运行后:复盘今日任务完成情况 await Utils.safeRun("任务状态复盘", () => this.reportTaskStatus("end")); }, // 汇总通知(从 runAll 末尾提取,方便独立测试) async reportSummary() { const endTime = Utils.getTimestamp(); const totalTime = ((endTime - RewardsAuto.state.startTime) / 1000).toFixed(1); const info = await API.getRewardsInfo(); if (!info) { Utils.log("🎉", `任务执行完成!用时 ${totalTime} 秒`, true); return; } // 推送消息(简洁版) let notifyMsg = `🎉 任务完成!\n`; notifyMsg += `💰 当前积分: ${info.balance}\n`; notifyMsg += `🔍 PC搜索: ${info.pc.progress}/${info.pc.max}\n`; notifyMsg += `📱 移动搜索: ${info.mobile.progress}/${info.mobile.max}\n`; if (info.todayDetails && info.todayDetails.length > 0) { for (const detail of info.todayDetails) { if (detail.title !== '必应搜索') { notifyMsg += `✨ ${detail.title}: +${detail.points}\n`; } } } if (info.history) { notifyMsg += `📅 本月: ${info.history.month.toLocaleString()}\n`; } notifyMsg += `⏱️ 用时: ${totalTime}秒`; RewardsAuto.state.sendMSG = notifyMsg; Utils.log("🎉", notifyMsg, true); // 控制台明细 let detailMsg = `📊 积分明细:\n`; detailMsg += `┌─────────────────────────\n`; detailMsg += `│ 💰 今日积分: ${info.balance}\n`; detailMsg += `├─────────────────────────\n`; detailMsg += `│ 🔍 搜索积分:\n`; detailMsg += `│ 💻 PC端: ${info.pc.progress}/${info.pc.max} ${info.pc.progress >= info.pc.max ? '✅' : '⏳'}\n`; detailMsg += `│ 📱 移动端: ${info.mobile.progress}/${info.mobile.max} ${info.mobile.progress >= info.mobile.max ? '✅' : '⏳'}\n`; detailMsg += `│ 📊 合计: ${info.pc.progress + info.mobile.progress}/${info.pc.max + info.mobile.max}\n`; detailMsg += `├─────────────────────────\n`; if (info.todayDetails && info.todayDetails.length > 0) { detailMsg += `│ 📋 今日活动:\n`; for (const detail of info.todayDetails) { if (detail.max) { detailMsg += `│ 🔍 ${detail.title}: ${detail.points}/${detail.max}\n`; } else { detailMsg += `│ ✨ ${detail.title}: +${detail.points}\n`; } } detailMsg += `├─────────────────────────\n`; } if (info.history) { detailMsg += `│ 📈 历史积分:\n`; detailMsg += `│ 📅 本月: ${info.history.month.toLocaleString()}\n`; detailMsg += `│ 📆 今年: ${info.history.year.toLocaleString()}\n`; detailMsg += `│ 🏆 生存期: ${info.history.lifetime.toLocaleString()}\n`; detailMsg += `├─────────────────────────\n`; } detailMsg += `│ ⏱️ 用时: ${totalTime} 秒\n`; detailMsg += `└─────────────────────────`; Utils.log("📊", detailMsg); } }; // ===================================================== // 打卡 / 拼图任务(仅在 rewards.bing.com 页面内执行) // 【已知限制】早期版本把匹配关键词写死成当季活动名(如「盗贼之海」「五月亮点来袭」), // 活动一换季就永远匹配不到,表现为「拼图 / 5 分小任务从来不完成」。 // 现改为两级匹配:先按关键词精确匹配,匹配不到再按链接结构兜底, // 保证活动改名后仍能点到卡片。 // ===================================================== if (location.hostname === "rewards.bing.com") { const punchCardSelectors = [ "a[href*='punchcard']", "a[href*='quest']", "a[data-rac][href*='earn']", "a.cursor-pointer[href]" ]; // 关键词可按当季活动补充;留空数组则完全走链接兜底 const textPatterns = ["打卡", "Punch card", "punchcard", "盗贼之海", "五月亮点来袭"]; const clickPunchCards = async (depth = 0) => { if (depth > 5) { console.log("[Rewards Auto] 打卡递归深度超限,停止"); return; } const today = Utils.getTodayNum(); const stateKey = "Config.punchCardState"; const dateKey = "Config.punchCardDate"; const savedDate = GM_getValue(dateKey, 0); let state = savedDate === today ? GM_getValue(stateKey, 0) : 0; if (state >= 2) { console.log("[Rewards Auto] 打卡任务已完成"); return; } console.log(`[Rewards Auto] 打卡任务: ${state}/2`); await Utils.randomDelay(3000, 8000); // 第一级:按关键词匹配卡片文本 let found = []; for (const sel of punchCardSelectors) { try { found = await Utils.waitForElementsByText(sel, textPatterns, 10000); if (found.length > 0) break; } catch {} } // 第二级:关键词没命中(活动已换季)时,按链接结构兜底取卡片。 // 此时不再依赖写死的活动名,避免换季后彻底失效 if (found.length === 0) { for (const sel of punchCardSelectors) { const nodes = document.querySelectorAll(sel); if (nodes.length > 0) { found = Array.from(nodes).map(el => ({ element: el, pattern: "兜底匹配" })); console.log(`[Rewards Auto] 关键词未命中,按链接兜底取到 ${found.length} 个卡片`); break; } } } if (found.length === 0) { console.log("[Rewards Auto] 未找到打卡卡片"); return; } if (state < found.length) { const target = found[state]; console.log(`[Rewards Auto] 点击: ${target.pattern}`); await Utils.safeRun("打卡点击", async () => { await Utils.randomDelay(); target.element.click(); GM_setValue(stateKey, state + 1); GM_setValue(dateKey, today); if (state + 1 < 2) { await Utils.randomDelay(5000, 10000); await clickPunchCards(depth + 1); } }); } }; const startPunchCards = () => { setTimeout(() => { const path = location.pathname; if (path.includes("/earn/quest/") || path.includes("punchcard")) { console.log("[Rewards Auto] 打卡详情页"); } else { console.log("[Rewards Auto] 奖励主页,开始打卡..."); Utils.safeRun("打卡任务", () => clickPunchCards()); } }, 3000); }; if (document.readyState === "complete" || document.readyState === "interactive") { startPunchCards(); } else { document.addEventListener("DOMContentLoaded", startPunchCards); } } // ===================================================== // 菜单注册 // ===================================================== GM_registerMenuCommand("🔑 手动授权", () => { GM_openInTab("https://login.live.com/oauth20_authorize.srf?client_id=0000000040170455&response_type=code&scope=service::prod.rewardsplatform.microsoft.com::MBI_SSL&redirect_uri=https://login.live.com/oauth20_desktop.srf", { active: true }); }); GM_registerMenuCommand("📋 粘贴授权码", () => { const code = prompt("粘贴授权页面跳转后的完整URL:"); if (code?.trim()) { GM_setValue("Config.code", code.trim()); alert("已保存!"); } }); GM_registerMenuCommand("📊 Token状态", () => { const token = GM_getValue("Config.token", false); const time = GM_getValue("Config.tokenTime", 0); const days = time > 0 ? Math.floor((Utils.getTimestamp() - time) / 86400000) : "未知"; alert(`Token: ${token?"已保存":"无"}\n年龄: ${days}天\n授权码: ${GM_getValue("Config.code","")?"有":"无"}`); }); GM_registerMenuCommand("🚀 立即运行", () => { Utils.safeRun("手动运行", () => TaskManager.runAll()); }); GM_registerMenuCommand("🔔 配置通知接口", () => { const configNames = [ { key: "Notice.wework", name: "企业微信 Webhook", hint: "群机器人webhook key" }, { key: "Notice.dingding", name: "钉钉机器人 Access Token", hint: "不加签,关键词需包含 #" }, { key: "Notice.feishu", name: "飞书机器人 Webhook", hint: "不加签,关键词需包含 #" }, { key: "Notice.pushme", name: "PushMe Key", hint: "push.i-i.me 推送key" }, { key: "Notice.bark", name: "Bark Key", hint: "bark.day.app 推送key" } ]; let configStr = "🔔 通知接口配置\n"; configStr += "==================\n\n"; configNames.forEach((item, index) => { const saved = GM_getValue(item.key, ""); configStr += `${index + 1}. ${item.name}\n`; configStr += ` 状态: ${saved ? "✅ 已配置" : "❌ 未配置"}\n`; configStr += ` 说明: ${item.hint}\n\n`; }); configStr += "请输入要配置的编号 (1-5),或输入 0 清除所有配置:"; const choice = prompt(configStr); if (!choice) return; const num = parseInt(choice); if (num === 0) { if (confirm("确定要清除所有通知接口配置吗?")) { configNames.forEach(item => GM_setValue(item.key, "")); alert("所有通知接口配置已清除!"); } return; } if (num >= 1 && num <= 5) { const selected = configNames[num - 1]; const current = GM_getValue(selected.key, ""); const newValue = prompt(`配置 ${selected.name}\n\n当前值: ${current || "(空)"}\n\n请输入新的值:`, current); if (newValue !== null) { GM_setValue(selected.key, newValue.trim()); alert(`${selected.name} 已${newValue.trim() ? "配置" : "清除"}!`); } } else { alert("无效的编号!"); } }); GM_registerMenuCommand("📢 测试通知", () => { RewardsAuto.state.sendMSG = "🧪 这是一条测试消息\n如果你看到这条消息,说明通知接口配置成功!"; Utils.log("📢", "测试通知已发送", true); alert("测试消息已发送,请检查各通知渠道!"); }); GM_registerMenuCommand("📋 查看通知状态", () => { const wework = GM_getValue("Notice.wework", ""); const dingding = GM_getValue("Notice.dingding", ""); const feishu = GM_getValue("Notice.feishu", ""); const pushme = GM_getValue("Notice.pushme", ""); const bark = GM_getValue("Notice.bark", ""); let status = "📊 通知接口配置状态:\n\n"; status += `企业微信: ${wework ? "✅ 已配置" : "❌ 未配置"}\n`; status += `钉钉: ${dingding ? "✅ 已配置" : "❌ 未配置"}\n`; status += `飞书: ${feishu ? "✅ 已配置" : "❌ 未配置"}\n`; status += `PushMe: ${pushme ? "✅ 已配置" : "❌ 未配置"}\n`; status += `Bark: ${bark ? "✅ 已配置" : "❌ 未配置"}\n`; alert(status); }); // ===================================================== // 运行锁(修复「定时任务跑着跑着再也不触发」) // ----------------------------------------------------- // 原脚本没有并发保护。一轮任务(签入 + 阅读 + 活动 + 若干次搜索 × 30 秒) // 常常超过 crontab 的 15 分钟间隔,新一轮与上一轮叠加后卡在 running 状态, // 之后所有定时触发都被跳过,表现就是「装完头两天正常,之后彻底不动」。 // 现引入带 TTL 的运行锁: // - 未到期说明上轮仍在跑 → 本轮直接跳过,不叠加; // - TTL 到期自动释放,避免异常退出(崩溃/关标签页)导致永久死锁。 // ===================================================== const RUN_LOCK_KEY = "Config.runLockAt"; const RUN_LOCK_TTL = 11 * 60 * 1000; // 略小于 15 分钟调度间隔 // 返回 true 表示抢到锁可以运行;false 表示上轮未结束,本轮让位 const acquireLock = () => { const now = Date.now(); const at = Number(GM_getValue(RUN_LOCK_KEY, 0)); if (at && (now - at) < RUN_LOCK_TTL) { Utils.log("⏸️", `上一轮任务仍在运行(${Math.round((now - at) / 1000)} 秒前启动),本轮跳过`); return false; } GM_setValue(RUN_LOCK_KEY, now); return true; }; const releaseLock = () => GM_setValue(RUN_LOCK_KEY, 0); const init = () => { try { if (!acquireLock()) return; TaskManager.init(); const isKeep = GM_getValue("Config.keep", true); const checkDone = (enabled, date) => !enabled || date === RewardsAuto.state.dateNowNum; const isAllDone = checkDone(GM_getValue("Tasks.sign"), TaskManager.signDate) && checkDone(GM_getValue("Tasks.read"), TaskManager.readDate) && checkDone(GM_getValue("Tasks.promos"), TaskManager.promosDate) && checkDone(GM_getValue("Tasks.search"), TaskManager.searchDate); if (!isKeep && isAllDone) { Utils.log("💤", "今日任务已全部完成"); return; } // 【修复】原随机延迟 5~95 秒过长,常导致本轮尚未开始 // 就已被下一轮挤占,现缩短为 3~20 秒 const delay = Utils.randomRange(3000, 20000); Utils.log("⏳", `${delay/1000}秒后启动...`); setTimeout(async () => { try { await Utils.safeRun("runAll", () => TaskManager.runAll()); } catch (e) { GM_log(`🔴 runAll 异常: ${Utils.fmtErr(e)}`); } finally { releaseLock(); } }, delay); } catch (e) { releaseLock(); GM_log(`🔴 初始化失败: ${Utils.fmtErr(e)}`); } }; // 清除可能影响搜索的 Cookie(带保护) try { if (typeof GM_cookie === "function") { // 【修复】原域名为 bing.com,与实际请求域 www.bing.com 不一致,清理不生效 GM_cookie("delete", { url: `https://${RewardsAuto.state.host}`, name: "_EDGE_S" }, () => {}); } } catch (e) { GM_log(`🟡 GM_cookie 清理跳过: ${Utils.fmtErr(e)}`); } init(); })();