// ==UserScript== // @name Rewards Fusion - Single File // @namespace local.rewards-fusion.single // @version 1.1.0 // @description 双源参考单文件后台版. 脚本猫每天09:00-20:00自动一次, 菜单随时手动运行, 搜索目标20-30次, 无需保持任务网页打开. // @author kunkun; Netsky; local maintainers // @license GPL-3.0-or-later // @homepage https://github.com/kun520ovo/microsoft-rewards-refactored // @source https://github.com/TheNetsky/Microsoft-Rewards-Script/tree/v5 // @noframes // @storageName RewardsFusion_Local_v1 // @crontab * 9-20 once * * // @connect www.bing.com // @connect cn.bing.com // @connect bing.com // @connect rewards.bing.com // @connect prod.rewardsplatform.microsoft.com // @connect login.live.com // @connect login.microsoftonline.com // @connect hot.baiwumm.com // @connect hotapi.nntool.cc // @connect cnxiaobai.com // @connect qyapi.weixin.qq.com // @connect oapi.dingtalk.com // @connect open.feishu.cn // @connect push.i-i.me // @connect api.day.app // @grant GM_listValues // @grant GM.deleteValue // @grant GM_getValue // @grant GM.setValue // @grant GM_info // @grant GM_log // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_addValueChangeListener // ==/UserScript== /* * Combined local adaptation: GPL-3.0-or-later. * Accounting/result logic adapted from TheNetsky/Microsoft-Rewards-Script. * Selected engine modules retain their original MIT permission notice: * * MIT License - Copyright (c) 2026 kunkun * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * This combined work is free software under the GNU General Public License, * version 3 or (at your option) any later version, without warranty. * See https://www.gnu.org/licenses/gpl-3.0.html and the project LICENSE. */ /* ==UserConfig== Config: lock: title: 地区锁 (地区未知也暂停, 可关闭) type: checkbox default: true api: title: 搜索词来源 type: select default: offline values: [offline, online] legacyUserInfo: title: 启用旧getuserinfo接口作为辅助 type: checkbox default: false Tasks: sign: title: 签到与搜索打卡核验 type: checkbox default: true read: title: App阅读 (需要授权) type: checkbox default: true promos: title: 活动卡片及二次扫描 type: checkbox default: true quiz: title: 尝试可识别的Quiz活动 type: checkbox default: true search: title: PC搜索 type: checkbox default: true daily: title: 每日活动服务器核验 (不点击网页) type: checkbox default: true Safety: authOnly: title: 仅验证授权 (暂停积分任务) type: checkbox default: false persistToken: title: 保存刷新令牌 type: checkbox default: true Notice: bro: title: 浏览器通知 type: checkbox default: true external: title: 允许向已配置渠道发送任务摘要 type: checkbox default: false wework: title: 企业微信机器人Key type: text password: true dingding: title: 钉钉机器人AccessToken type: text password: true feishu: title: 飞书机器人Key type: text password: true pushme: title: PushMe Key type: text password: true bark: title: Bark Key type: text password: true ==/UserConfig== */ return (async function() { "use strict"; // Section: core/platform const Storage = { pending: new Set(), errors: [], get: (key, fallback) => GM_getValue(key, fallback), set(key, value) { return this.write(key, () => GM.setValue(key, value)); }, write(key, operation) { let timer; const write = (async () => { if (typeof GM === 'undefined' || typeof GM.setValue !== 'function') { throw new Error('需要支持 GM.setValue 的脚本猫环境'); } try { await Promise.race([operation(), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`保存确认超时: ${key} (15秒)`)), 15000); })]); } finally { if (timer !== undefined) clearTimeout(timer); } })(); this.pending.add(write); void write.then(() => this.pending.delete(write), error => { this.pending.delete(write); this.errors.push(error); }); return write; }, remove(key) { return typeof GM.deleteValue === 'function' ? this.write(key, () => GM.deleteValue(key)) : this.set(key, null); }, keys: () => GM_listValues(), async flush() { await Promise.allSettled([...this.pending]); if (this.errors.length) { const error = this.errors.shift(); this.errors.length = 0; throw error; } } }; const Platform = { get location() { return typeof location === 'undefined' ? undefined : location; }, get window() { return typeof window === 'undefined' ? undefined : window; }, get document() { return typeof document === 'undefined' ? undefined : document; }, get history() { return typeof history === 'undefined' ? undefined : history; }, get sessionStorage() { return typeof sessionStorage === 'undefined' ? undefined : sessionStorage; }, get info() { return GM_info; }, request: options => GM_xmlhttpRequest(options), log: message => GM_log(Safety.redact(message)), notify: options => GM_notification({ ...options, text: Safety.redact(options.text || '') }), openTab(url, options = {}) { if (!Safety.navigation(url)) throw new Error('拒绝打开非允许的任务或授权页面'); return GM_openInTab(url, options); }, registerMenu: (name, callback, options) => GM_registerMenuCommand(name, callback, options), alert: message => Platform.log(message) }; // Section: core/accounting // Adapted from TheNetsky/Microsoft-Rewards-Script v5: PointAccounting, ActivityResult, // PromotionEligibility. GPL-3.0-or-later; see THIRD_PARTY_NOTICES.md and upstream/manifest.json. const Accounting = { pointValue(value) { if (typeof value !== 'number' && (typeof value !== 'string' || !value.trim())) return null; const points = Number(value); return Number.isFinite(points) && points >= 0 ? points : null; }, pointDelta(before, after) { const start = this.pointValue(before), end = this.pointValue(after); return start !== null && end !== null && end >= start ? end - start : null; }, creditedPoints(progress, before, after) { const credited = this.pointDelta(before, after); if (progress === 0 && credited !== 0) return null; return progress !== null && credited !== null && credited >= progress ? progress : null; }, result(status, reason, offerId, points = status === 'skipped' ? 0 : null) { const measured = this.pointValue(points); return { status, reason, offerId, operationId: crypto.randomUUID(), pointsGained: measured ?? 0, pointsKnown: measured !== null }; }, eligible(card) { const text = `${card.offerId || ''} ${card.title || ''}`; if (/WW_Rewards_offerlist_500_points_202503|Gamification_Sapphire_DailyCheckIn/i.test(text)) return false; return !card.isCompleted && !card.isHidden && !card.isPromotional && card.exclusiveLockedFeatureStatus !== 'locked' && !(card.date && Date.parse(card.date) > Date.now()); }, counts(results) { const out = { confirmed: 0, unconfirmed: 0, skipped: 0, failed: 0 }; for (const result of results) out[result.status]++; return out; } }; // Section: core/security const Safety = { microsoft: new Set(['www.bing.com', 'cn.bing.com', 'bing.com', 'rewards.bing.com', 'prod.rewardsplatform.microsoft.com', 'login.live.com', 'login.microsoftonline.com']), public: new Map([['hot.baiwumm.com', '/api/'], ['cnxiaobai.com', '/DailyHotApi/'], ['hotapi.nntool.cc', '/']]), webhook: new Map([['Notice.wework', 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key='], ['Notice.dingding', 'https://oapi.dingtalk.com/robot/send?access_token='], ['Notice.feishu', 'https://open.feishu.cn/open-apis/bot/v2/hook/'], ['Notice.pushme', 'https://push.i-i.me/?push_key='], ['Notice.bark', 'https://api.day.app/']]), url(raw) { try { const url = new URL(raw); if (url.protocol !== 'https:' || url.username || url.password || url.port) return null; return url; } catch { return null; } }, spending(raw, depth = 0) { if (depth > 3) return true; const url = this.url(raw); if (!url) return true; let path = url.pathname; try { for (let n = 0; n < 3; n++) path = decodeURIComponent(path); } catch { return true; } if (/\/(?:redeem|redemption|order|orders|orderhistory|donate|donation|sweepstakes|raffle|checkout|goal)(?:[/.\-_]|$)/i.test(path)) return true; for (const [key, value] of url.searchParams) { if (/^(?:url|next|redirect|returnurl|return_url|redirect_uri|destination|target)$/i.test(key)) { try { const nested = new URL(value, url.origin); if (this.spending(nested.href, depth + 1)) return true; } catch { return true; } } } return false; }, request(raw, method = 'GET') { const u = this.url(raw); if (!u || this.spending(raw)) throw new Error('请求地址或路由不允许'); if ([...u.searchParams.keys()].some(key => /^(?:code|refresh_token|access_token)$/i.test(key))) throw new Error('授权凭据不得放入请求网址'); method = method.toUpperCase(); if (method === 'GET' && u.hostname === 'login.live.com' && u.pathname === '/oauth20_authorize.srf') { const pending = Storage.get('Auth.pending', null); if (pending?.state && Number.isFinite(pending.expiresAt) && pending.expiresAt > Date.now() && u.searchParams.get('state') === pending.state && u.searchParams.get('client_id') === '0000000040170455' && u.searchParams.get('scope') === 'service::prod.rewardsplatform.microsoft.com::MBI_SSL' && u.searchParams.get('response_type') === 'code' && u.searchParams.get('redirect_uri') === 'https://login.live.com/oauth20_desktop.srf') return 'authorize'; throw new Error('授权探测请求与当前授权不匹配或已过期'); } if (['www.bing.com', 'cn.bing.com', 'bing.com'].includes(u.hostname) && method === 'GET' && ['/', '/search', '/rewardsapp/ncheader', '/rewardsapp/reportActivity', '/rewards/panelflyout/getuserinfo'].includes(u.pathname)) return 'web'; if (u.hostname === 'rewards.bing.com' && ((method === 'GET' && (['/', '/earn', '/dashboard', '/api/getuserinfo'].includes(u.pathname) || /^\/_next\/static\/[\w./-]+\.js$/.test(u.pathname) || /^\/earn\/quest\/[\w-]+$/.test(u.pathname))) || (method === 'POST' && u.pathname === '/earn'))) return 'web'; if (u.hostname === 'prod.rewardsplatform.microsoft.com' && ((method === 'GET' && u.pathname === '/dapi/me') || (method === 'POST' && u.pathname === '/dapi/me/activities'))) return 'app'; if (method === 'POST' && ((u.hostname === 'login.microsoftonline.com' && u.pathname === '/consumers/oauth2/v2.0/token') || (u.hostname === 'login.live.com' && u.pathname === '/oauth20_token.srf'))) return 'token'; if (method === 'GET' && this.public.has(u.hostname) && u.pathname.startsWith(this.public.get(u.hostname))) return 'public'; if (method === 'POST' && Storage.get('Notice.external', false)) { for (const [key, prefix] of this.webhook) { const value = String(Storage.get(key, '') || '').trim(); if (value && !/[\s/?&#]/.test(value) && raw === prefix + encodeURIComponent(value)) return 'webhook'; } } throw new Error('请求用途不在允许清单中'); }, navigation(raw) { const u = this.url(raw); if (!u || this.spending(raw)) return false; if (u.hostname === 'login.live.com' && u.pathname === '/oauth20_authorize.srf') { return u.searchParams.get('client_id') === '0000000040170455' && u.searchParams.get('redirect_uri') === 'https://login.live.com/oauth20_desktop.srf'; } return ['www.bing.com', 'cn.bing.com', 'rewards.bing.com'].includes(u.hostname) && ['/', '/search', '/earn', '/dashboard'].includes(u.pathname); }, card(card) { if (!Accounting.eligible(card)) return false; if (/兑换|捐赠|抽奖|消耗|購買|购买|redeem|donat|sweepstake|raffle|spend\s+points|purchase/i.test(`${card.title || ''} ${card.offerId || ''}`)) return false; return !card.destinationUrl || (this.url(card.destinationUrl) && !this.spending(card.destinationUrl) && ['www.bing.com', 'cn.bing.com', 'rewards.bing.com'].includes(new URL(card.destinationUrl).hostname)); }, redact(value) { let text = String(value ?? ''); for (const key of ['Config.token', 'Config.code', ...this.webhook.keys()]) { const secret = Storage.get(key, ''); if (typeof secret === 'string' && secret.length >= 4) text = text.split(secret).join('[REDACTED]'); } return text.replace(/Bearer\s+[^\s,;"']+/gi, 'Bearer [REDACTED]') .replace(/([?&](?:code|state|refresh_token|access_token|key|push_key)=)[^&#\s]*/gi, '$1[REDACTED]'); } }; const HttpTransport = { text(options, onResponse = null) { return new Promise((resolve, reject) => { let handle, timer, settled = false, purpose; const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(value); }; try { const method = (options.method || 'GET').toUpperCase(); purpose = Safety.request(options.url, method); const headers = {}; for (const [key, value] of Object.entries(options.headers || {})) { const lower = key.toLowerCase(); if (['cookie', 'user-agent'].includes(lower)) continue; if (/authorization|x-rewards/i.test(lower) && purpose !== 'app') throw new Error('禁止向此用途发送微软凭据'); if (purpose === 'public' && !['accept'].includes(lower)) continue; headers[key] = value; } if (purpose === 'public' && options.data) throw new Error('公开词源请求不接受账号数据'); if (purpose === 'app' && method === 'POST') { const body = JSON.parse(options.data || '{}'); if (![101, 103].includes(body.type) || Number(body.amount) !== 1 || (body.type === 101 && !body.attributes?.offerid)) throw new Error('只允许上报一项签到或阅读活动'); if (/redeem|donat|sweepstake|purchase/i.test(body.attributes?.offerid || '')) throw new Error('拒绝积分消费活动'); } timer = setTimeout(() => { finish(new Error('网络请求超时 (15秒)')); try { handle?.abort?.(); } catch {} }, 15000); handle = Platform.request({ method, url: options.url, headers, data: options.data, timeout: 15000, anonymous: ['public', 'webhook'].includes(purpose), redirect: purpose === 'authorize' ? 'manual' : 'error', onload: async response => { if (settled) return; try { if (purpose === 'authorize') { const final = Safety.url(response.finalUrl || options.url); if (!final || final.hostname !== 'login.live.com' || !['/oauth20_authorize.srf', '/oauth20_desktop.srf'].includes(final.pathname)) throw new Error('授权响应来源不允许'); } else if (Safety.request(response.finalUrl || options.url, method) !== purpose) throw new Error('响应跳转用途不匹配'); const authRedirect = purpose === 'authorize' && [301, 302, 303, 307, 308].includes(response.status); if (!authRedirect && (response.status < 200 || response.status >= 300)) throw Object.assign(new Error(`HTTP ${response.status}`), { status: response.status }); if (onResponse) await onResponse(); finish(null, options.fullResponse ? response : response.responseText); } catch (error) { finish(new Error(Safety.redact(error.message))); } }, onerror: () => finish(new Error('网络错误或跳转已拦截')), ontimeout: () => finish(new Error('网络请求超时 (15秒)')), onabort: () => finish(new Error('网络请求已中止')), onredirect: purpose === 'authorize' ? undefined : () => { finish(new Error('请求跳转已拦截')); try { handle?.abort?.(); } catch {} } }); } catch (error) { finish(new Error(Safety.redact(error.message))); } }); } }; const RunGuard = { isStopError(error) { return ['WEB_SESSION_STOPPED', 'TASK_RUN_DATE_CHANGED', 'TASK_RUN_LOCK_LOST', 'RUN_REPLACED'].includes(error?.code); }, markLockLost() { LockState.lost = true; Platform.log('运行租约失效, 已停止旧任务'); }, assertLock() { if (LockState.pageLeaving || (LockState.held && (LockState.lost || Storage.get(LockState.key, null)?.id !== LockState.id || Number(Storage.get(LockState.key, null)?.expiresAt || 0) <= Date.now() || Number(Storage.get(LockState.releasedPrefix + LockState.id, 0)) > Date.now()))) { throw Object.assign(new Error('运行租约失效或页面已离开'), { code: 'TASK_RUN_LOCK_LOST' }); } }, assertActive() { if (SessionGuard.isStopped()) throw SessionGuard.error(); this.assertLock(); RunDateGuard.assertCurrent(); if (Runner.current && Storage.get('Schedule.latest', null)?.runId !== Runner.current.runId) { throw Object.assign(new Error('当前轮次已被替换, 停止旧任务'), { code: 'RUN_REPLACED' }); } } }; const HttpClient = { async text(options) { RunGuard.assertActive(); const purpose = Safety.request(options.url, options.method || 'GET'); if (['web', 'app'].includes(purpose) && !options.sessionProbe) await WebSession.ensure(); RunGuard.assertActive(); return HttpTransport.text(options, () => RunGuard.assertActive()); } }; const Wait = { delay(ms) { RunGuard.assertActive(); return new Promise((resolve, reject) => setTimeout(() => { try { RunGuard.assertActive(); resolve(); } catch (error) { reject(error); } }, ms)); }, randomDelay(min = 3000, max = 8000) { return this.delay(Utils.randomRange(min, max)); } }; const Logger = { log(icon, text) { Platform.log(`${icon} ${text}`); return true; } }; const Notice = { pending: new Set(), log(icon, text, push = false) { Logger.log(icon, text); if (push && Storage.get('Notice.bro', true)) { try { Platform.notify({ title: Platform.info.script.name, text: `${icon} ${text}` }); } catch {} } if (push && Storage.get('Notice.external', false)) { const pending = this.sendWebhook(Safety.redact(`${icon} ${text}`)); this.pending.add(pending); void pending.finally(() => this.pending.delete(pending)); } }, async sendWebhook(message) { await Promise.all([...Safety.webhook].map(async ([key, prefix]) => { const secret = String(Storage.get(key, '') || '').trim(); if (!secret || /[\s/?&#]/.test(secret)) return; const data = key === 'Notice.wework' ? { msgtype: 'text', text: { content: message } } : key === 'Notice.dingding' ? { msgtype: 'text', text: { content: `# ${message}` } } : key === 'Notice.feishu' ? { msg_type: 'text', content: { text: message } } : key === 'Notice.pushme' ? { title: 'Rewards Fusion', content: message, type: 'markdown' } : { title: 'Rewards Fusion', body: message }; try { await HttpTransport.text({ method: 'POST', url: prefix + encodeURIComponent(secret), headers: { 'content-type': 'application/json' }, data: JSON.stringify(data) }); } catch (error) { Platform.log(`通知发送失败: ${error.message}`); } })); }, async flush() { await Promise.allSettled([...this.pending]); } }; // Section: vendor/kun-engine // Selected modules from kun520ovo/microsoft-rewards-refactored, v1.0.4. // Copyright (c) 2026 kunkun. MIT; see licenses/kun520ovo-MIT.txt. // Generated by tools/vendor.mjs. No upstream entrypoint, token flow or page auto-claim is included. const RewardsAuto = { // 代码和用户设置均开启时才锁区。 lockRegion: true, // 桌面、移动网页和 App 的 User-Agent。 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: "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: { // App 固定参数;阅读服务可改用服务器返回的 offerId。 rewardsAppId: "SAAndroid/32.6.2110003560", channel: "SAAndroid", offerIds: { readArticle: "ENUS_readarticle3_30points", } }, searchPool: [ "agentic AI tools for everyday work 2026", "multimodal AI assistants voice and vision", "on device AI laptops comparison 2026", "AI smart glasses live translation", "generative AI video tools comparison 2026", "AI search engines with source citations", "digital twins for smart factories", "quantum computing breakthroughs 2026", "brain computer interface clinical trials 2026", "passkeys and passwordless account security", "home battery storage systems comparison 2026", "solid state EV battery production progress", "vehicle to home bidirectional charging", "hydrogen fuel cell trucks latest developments", "smart rings sleep tracking accuracy", "remote patient monitoring wearable devices", "assistive technology for independent living", "robot lawn mowers navigation comparison", "Nintendo Switch 2 best games 2026", "handheld gaming PCs battery life comparison", "cloud gaming services performance 2026", "FIFA World Cup 2026 highlights and best goals", "Formula 1 2026 technical regulations explained", "Milano Cortina 2026 Winter Olympics highlights", "2026 tennis Grand Slam season highlights", "James Webb Space Telescope discoveries 2026", "Artemis II mission latest updates", "sustainable aviation fuel commercial flights", "slow travel destinations 2026", "2026 science fiction movies and streaming series", "2026 十月新番追番指南", "2026 热门国漫推荐", "动漫角色人气排行", "二次元高清动漫壁纸", "动漫手办新品与收藏", "2026 漫展时间与 Cosplay 展示", "原神最新版本活动攻略", "崩坏星穹铁道角色配队攻略", "绝区零新角色培养建议", "鸣潮最新版本探索攻略", "2026 Steam 热门游戏推荐", "2026 国产单机游戏新作", "DeepSeek 最新模型与本地部署", "2026 国产开源大模型对比", "AI 编程助手代码质量评测", "ComfyUI 二次元绘画工作流", "AI 动漫角色一致性生成", "AI 音乐生成与编曲教程", "2026 旗舰手机影像与续航对比", "国产显卡与游戏性能评测", "宇树机器人最新产品演示", "2026 折叠屏手机选购", "6G 通信与卫星直连技术", "2026 游戏显示器 OLED 与 Mini LED 对比", "汉服美女国风写真", "旗袍美女人像摄影", "2026 女明星红毯造型", "气质美女日常穿搭", "美女高清手机壁纸", "美女人像摄影姿势" ], // 在线模式自动轮换热搜来源。 apiConfig: { mode: Storage.get("Config.api", "online"), sources: [ { url: "https://hot.baiwumm.com/api/", hot: ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq", "netease", "zhihu"], }, { url: "https://cnxiaobai.com/DailyHotApi/", hot: ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq-news", "netease-news", "zhihu"], }, { url: "https://hotapi.nntool.cc/", hot: ["weibo", "douyin", "baidu", "toutiao", "thepaper", "qq-news", "netease-news", "zhihu"], }, ], }, 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" ], }; const RunState = { token: false, region: "CN", host: "www.bing.com", dateNowNum: 0, dateNowStr: "", ipInfo: "", startTime: 0, forceRun: false, }; const SearchState = { lastSearchProgress: -1, restrictedTimes: 0, searchTrackingDate: undefined, wordList: [], wordIndex: 0 }; const ReadingState = { progress: 0, max: 30 }; const DataParsers = { // 有效用户数据须含可识别积分,且无明确未登录标记。 validUserInfo(data) { if (!data || typeof data !== "object" || Array.isArray(data)) return false; const dashboard = data.dashboard || data, status = dashboard.userStatus; if (!status || typeof status !== "object" || Array.isArray(status)) return false; if ([data, dashboard, status].some(item => ["isAuthenticated", "isSignedIn", "isLoggedIn"].some(key => item[key] === false))) return false; const points = status.availablePoints ?? dashboard.availablePoints ?? data.balance; return (typeof points === "number" || typeof points === "string" && /^\d+$/.test(points)) && Number.isSafeInteger(Number(points)) && Number(points) >= 0; }, booleanFlag(value) { return value === true || value === "true" || value === 1 || value === "1" ? true : value === false || value === "false" || value === 0 || value === "0" ? false : null; }, // 仅解析非负安全整数。 parseNonNegativeInteger(value, trimString = true) { if (typeof value !== "number" && typeof value !== "string") return NaN; if (typeof value === "string" && !/^\d+$/.test(trimString ? value.trim() : value)) return NaN; const number = Number(value); return Number.isSafeInteger(number) && number >= 0 ? number : NaN; }, // 取首个有效余额;允许 0,全无效时返回 null。 accountBalance(...values) { for (const value of values) { const balance = DataParsers.parseNonNegativeInteger(value); if (Number.isFinite(balance)) return balance; } return null; }, userInfoBalance(data) { const dashboard = data?.dashboard || data; return this.accountBalance(dashboard?.userStatus?.availablePoints, dashboard?.availablePoints, data?.balance); }, dapiBalance(response) { return this.accountBalance(response?.balance, response?.userStatus?.availablePoints); }, sumSearchCounters(items) { if (!Array.isArray(items)) return { progress: 0, max: 0 }; return items.reduce((acc, item) => { acc.progress += Number(item?.pointProgress || 0); acc.max += Number(item?.pointProgressMax || item?.pointMax || 0); return acc; }, { progress: 0, max: 0 }); }, // 解码 Flight 字符串分片,不执行页面脚本。 flightChunks(html) { return [...String(html || "").matchAll( /self\.__next_f\.push\(\s*\[\s*1\s*,\s*("(?:\\.|[^"\\])*")\s*\]\s*\)/g )].map(match => { try { return JSON.parse(match[1]); } catch (_) { return ""; } }); }, // 按 partner、活动计数和当日标记识别连签,忽略公共文案。 parseStreakData(html) { const tasks = new Map(); let bonus = null; const visit = value => { if (!value || typeof value !== "object") return; if (!Array.isArray(value)) { const partner = String(value.partner || "").trim().toLowerCase(); const completed = Number(value.activitiesCompleted); const total = Number(value.activitiesTotal); if (partner && Number.isFinite(completed) && Number.isFinite(total) && Object.prototype.hasOwnProperty.call(value, "isCurrentDayCompleted")) { tasks.set(partner, { partner, title: String(value.title || ""), activitiesCompleted: completed, activitiesTotal: total, completedDays: Number(value.completedDays), totalDays: Number(value.totalDays), isCurrentDayCompleted: value.isCurrentDayCompleted === true, isEnabled: value.isEnabled !== false }); } const stampProgress = Number(value.activityProgress); const stampTotal = Number(value.activitiesTotal); const reward = Number(value.streakBonus); if (!partner && Number.isFinite(stampProgress) && Number.isFinite(stampTotal) && stampTotal > 0 && Number.isFinite(reward)) { bonus = { progress: stampProgress, total: stampTotal, reward }; } } for (const child of Object.values(value)) visit(child); }; const flight = DataParsers.flightChunks(html).join(""); for (const line of flight.split("\n")) { const record = line.match(/^[a-f0-9]+:(.*)$/i); if (!record) continue; try { visit(JSON.parse(record[1])); } catch (_) {} } return { tasks: [...tasks.values()], bonus }; }, // 官网 PC 打卡对应 bing 搜索连续打卡,App 对应 bingapp;未下发时保留未知。 signInProgress(tasks) { const completed = partner => { const task = tasks.find(item => item.partner === partner && item.isEnabled); if (!task) return null; return task.isCurrentDayCompleted === true || (task.activitiesTotal > 0 && task.activitiesCompleted >= task.activitiesTotal); }; return { app: completed("bingapp"), pc: completed("bing") }; } }; const SearchQuota = { /** * 创建含数据来源的查询结果。 * @param {SearchQuotaResult["status"]} status * @param {string} source * @param {SearchProgress|null} [pc=null] * @param {SearchProgress|null} [mobile=null] * @returns {SearchQuotaResult} */ create(status, source, pc = null, mobile = null) { return { status, source, pc, mobile }; }, // 搜索执行、受限判断和汇总共用此配额检查。 hasProgress(value) { return Number.isFinite(value?.pc?.progress) && value.pc.progress >= 0 && Number.isFinite(value.pc.max) && value.pc.max > 0; }, // 区分缺失、异常和 0/0;仅正上限为 available。 fromPC(value, source, mobile = null) { if (value === undefined) return this.create("missing", source); const progress = DataParsers.parseNonNegativeInteger(value?.progress); const max = DataParsers.parseNonNegativeInteger(value?.max); if (!Number.isFinite(progress) || !Number.isFinite(max) || (max === 0 && progress !== 0)) { return this.create("unknown", source); } return this.create(max > 0 ? "available" : "zero", source, { progress, max }, mobile); }, fromCounters(items, source) { if (items === undefined || Array.isArray(items) && items.length === 0) return this.create("missing", source); if (!Array.isArray(items)) return this.create("unknown", source); for (const item of items) { const max = (item?.pointProgressMax || item?.pointMax) ?? item?.pointProgressMax; if (!Number.isFinite(DataParsers.parseNonNegativeInteger(item?.pointProgress)) || !Number.isFinite(DataParsers.parseNonNegativeInteger(max))) return this.create("unknown", source); } return this.fromPC(DataParsers.sumSearchCounters(items), source); }, // 优先选可用配额,异常结果不覆盖有效结果。 select(results) { for (const status of ["available", "zero", "unknown", "missing", "failed"]) { const found = results.find(result => result.status === status); if (found) return found; } return this.create("unknown", "none"); } }; const RewardsPage = { prepare(html) { const flight = DataParsers.flightChunks(html); return { html, text: flight.length ? flight.join("") : html.replace(/\\"/g, '"') }; }, // 按括号和引号截取 JSON,跳过字符串内的括号。 field(text, name) { const match = new RegExp(`"${name}"\\s*:\\s*`).exec(text); if (!match) return undefined; const start = match.index + match[0].length; if (text[start] !== "{" && text[start] !== "[") return null; let depth = 0, quoted = false, escaped = false; for (let index = start; index < text.length; index++) { const char = text[index]; if (quoted) { if (escaped) escaped = false; else if (char === "\\") escaped = true; else if (char === '"') quoted = false; continue; } if (char === '"') quoted = true; else if (char === "{" || char === "[") depth++; else if (char === "}" || char === "]") { if (--depth === 0) { try { return JSON.parse(text.slice(start, index + 1)); } catch (_) { return null; } } } } return null; }, // 配额来源:结构化 PC 数据、页面表格、combinedSearch。 searchQuota(page) { const counters = this.field(page.text, "pointsCounters"); const mobile = SearchQuota.fromPC(counters?.mobile, "page").pc; const primary = counters === null ? SearchQuota.create("unknown", "page") : SearchQuota.fromPC(counters?.pc, "page", mobile); if (primary.status === "available") return primary; const candidates = [primary]; const row = page.html.match(/

\s*必应搜索\s*<\/p>\s*<\/div>\s*]*justify-self-end)[^>]*>([\s\S]{0,500}?)<\/div>/i); const table = row && (row[1].match(/]*>([\d,]+)<\/span>\s*]*>\s*\/\s*([\d,]+)\s*<\/span>/i) || row[1].match(/([\d,]+)\s*\/\s*([\d,]+)/)); if (table) candidates.push(SearchQuota.fromPC({ progress: table[1].replace(/,/g, ""), max: table[2].replace(/,/g, "") }, "page-table", mobile)); candidates.push(SearchQuota.fromPC(this.field(page.text, "combinedSearch"), "page-combined", mobile)); return SearchQuota.select(candidates); }, balance(page) { for (const field of ["availablePoints", "balance"]) { const pattern = new RegExp(`"${field}"\\s*:\\s*(?:"\\s*(\\d+)\\s*"|(\\d+))(?=\\s*[,}])`); const match = page.text.match(pattern); const balance = DataParsers.accountBalance(match?.[1], match?.[2]); if (balance !== null) return balance; } return null; }, dailyOffer(page) { const counters = this.field(page.text, "pointsCounters"); const points = DataParsers.parseNonNegativeInteger(counters?.dailyOffer); return Number.isFinite(points) ? points : 0; }, activityDetails(page, pageQuota, dailyOffer) { const details = []; const visit = value => { if (!value || typeof value !== "object") return; const points = DataParsers.parseNonNegativeInteger(value.points); if (typeof value.title === "string" && value.isCompleted === true && points > 0) { details.push({ title: value.title, points }); } for (const child of Object.values(value)) visit(child); }; visit(this.field(page.text, "activityCards")); if (["page-table", "page-combined"].includes(pageQuota.source) && pageQuota.pc) { details.push({ title: "必应搜索", points: pageQuota.pc.progress, max: pageQuota.pc.max }); } if (dailyOffer > 0) details.push({ title: "优惠", points: dailyOffer }); const pattern = /

([^<]+)<\/p><\/div>]*>(\d+)<\/div>/g; let match; while ((match = pattern.exec(page.html)) !== null) { const points = parseInt(match[2], 10); if (points > 0 && !details.some(detail => detail.title === match[1])) details.push({ title: match[1], points }); } return details; }, history(page) { const structured = this.field(page.text, "pointsHistory"); const history = {}; for (const [key, field, label, legacyField] of [ ["month", "thisMonth", "本月", "monthlyPoints"], ["year", "thisYear", "今年", "yearlyPoints"], ["lifetime", "lifetime", "生存期", "lifetimePoints"] ]) { const htmlMatch = page.html.match(new RegExp(`${label}.*?(\\d[\\d,]*)<\\/div>`)); const jsonMatch = page.text.match(new RegExp(`"${legacyField}":(\\d+)`)); history[key] = DataParsers.accountBalance(structured?.[field]?.earn, htmlMatch?.[1].replace(/,/g, ""), jsonMatch?.[1]) ?? 0; } return history; } }; const TaskResult = { /** * 默认不重试;completed/skipped 始终不可重试。 * @param {TaskExecutionResult["status"]} status * @param {string} reason * @param {boolean} [retryable=false] * @returns {TaskExecutionResult} */ create(status, reason, retryable = false) { return { status, reason, retryable: retryable === true && ["pending", "failed", "unknown"].includes(status) }; }, isValid(result) { return !!result && ["completed", "pending", "failed", "unknown", "skipped"].includes(result.status) && typeof result.reason === "string" && result.reason.length > 0 && typeof result.retryable === "boolean"; } }; const TaskState = { signDate: 0, readDate: 0, promosDate: 0, searchDate: 0, streakDays: 0, appSignDate: 0, pcSignDate: 0, signPoint: -1, signTimes: 0, readTimes: 0, cardAttempted: new Set(), cardResults: new Map() }; const SessionGuard = { stopped: false, logoutCleared: false, resetId: Storage.get("Config.sessionResetId", ""), error() { return Object.assign(new Error("网页登录检查未通过,任务已停止"), { code: "WEB_SESSION_STOPPED" }); }, isStopped() { if (!this.stopped && Storage.get("Config.sessionResetId", "") !== this.resetId) { this.stop("其他页面已退出登录"); } return this.stopped; }, stop(reason, confirmedLogout = false) { const firstStop = !this.stopped; const firstLogout = confirmedLogout && !this.logoutCleared; if (firstStop || firstLogout) { this.stopped = true; RunState.token = false; ReadingState.progress = 0; if (firstLogout) { this.logoutCleared = true; // 仅确认退出时清理持久数据;状态不明只停本实例。 const cleared = { "Config.token": false, "Config.tokenTime": 0, "Config.code": "", "Config.tasks": {}, "Config.signPoint": -1, "Config.dailySetCompleted": 0, "Config.dailySetProcessed": [], "Config.punchCardDetailDate": 0, "Config.punchCardDetailState": 0, "Config.punchCardDate": 0, "Config.punchCardState": 0, "Config.lastSearchProgress": -1, "Config.restrictedTimes": 0, "Config.searchTrackingDate": 0 }; for (const [key, value] of Object.entries(cleared)) Storage.set(key, value); // 更新登录代次,使其他页面在下个检查点停止旧任务。 Storage.set("Config.sessionResetId", `${Date.now()}-${Math.random().toString(36).slice(2)}`); } Platform.log(`🔒 ${reason},已停止全部任务。请确认 Bing 登录后刷新重试。`); } return this.error(); } }; const Utils = { isRegionLockEnabled() { if (RewardsAuto.lockRegion !== true) return false; // 兼容布尔、数字及字符串形式的关闭设置。 const saved = Storage.get("Config.lock", true); if (typeof saved === "string") { const value = saved.trim().toLowerCase(); if (value === "false" || value === "0") return false; } return saved !== false && saved !== 0; }, randomRange(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); }, // 任务按本地日历日记录 YYYYMMDD,避免 UTC 换日偏差。 getTodayNum() { const d = new Date(); return Number(`${d.getFullYear()}${String(d.getMonth()+1).padStart(2,"0")}${String(d.getDate()).padStart(2,"0")}`); }, getTodayStr() { const d = new Date(); return `${d.getMonth()+1}/${d.getDate()}/${d.getFullYear()}`; }, 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; } } }; const RunDateGuard = { date: 0, begin(date = Utils.getTodayNum()) { this.date = date; }, end() { this.date = 0; }, hasChanged() { return this.date !== 0 && this.date !== Utils.getTodayNum(); }, assertCurrent() { if (this.hasChanged()) { throw Object.assign(new Error("日期已变化,停止本轮任务,等待下一轮重新核验"), { code: "TASK_RUN_DATE_CHANGED" }); } } }; const WebSession = { checking: null, verified: false, isWebRequest(url) { const host = new URL(url).hostname; return ["rewards.bing.com", "www.bing.com", "cn.bing.com", "bing.com"].includes(host); }, isBingHost(host) { return ["www.bing.com", "cn.bing.com", "bing.com"].includes(host); }, probeUrl() { const current = typeof Platform.location !== "undefined" ? Platform.location.hostname : ""; const host = this.isBingHost(current) ? current : this.isBingHost(RunState.host) ? RunState.host : "www.bing.com"; return `https://${host}/?_rauto_session=${Date.now()}`; }, needsLogin(url) { const host = new URL(url).hostname; return this.isWebRequest(url) || ["prod.rewardsplatform.microsoft.com", "login.live.com"].includes(host); }, // 用新获取的 Bing 页面判断登录、退出或未知。 readBingLogin(html) { const markup = String(html || "") .replace(//g, "") .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, ""); const nodes = [...markup.matchAll(/<([a-z][\w:-]*)\b[^>]*>/gi)]; const attribute = (node, name) => { const match = node?.[0].match(new RegExp('(?:^|\\s)' + name + '\\s*=\\s*(?:"([^"]*)"|\'([^\']*)\'|([^\\s>]+))', "i")); return (match?.[1] ?? match?.[2] ?? match?.[3] ?? "") .replace(/"|"/gi, '"').replace(/'|'/gi, "'").replace(/&/gi, "&"); }; const byId = id => nodes.find(node => attribute(node, "id") === id); const visible = node => !!node && attribute(node, "aria-hidden") !== "true" && !/(?:display\s*:\s*none|visibility\s*:\s*hidden)/i.test(attribute(node, "style")) && !/(?:^|\s)b_hide(?:\s|$)/.test(attribute(node, "class")) && !/\shidden(?:\s|=|>)/i.test(node[0]); const text = node => { if (!node) return ""; const start = node.index + node[0].length; const rest = markup.slice(start, start + 2000); const end = rest.search(new RegExp('', "i")); return end < 0 ? "" : rest.slice(0, end).replace(/<[^>]*>/g, "") .replace(/ | /gi, " ").trim(); }; const name = byId("id_n"), signIn = byId("id_s"); const accountVisible = visible(name) && !!text(name); const signInVisible = visible(signIn) && !!text(signIn); const medal = byId("rh_rwm") || nodes.find(node => attribute(node, "data-rewards-widget") === "medallion"); let rewardsSignedIn = false; const raw = attribute(medal, "data-content"); if (raw) { try { const content = JSON.parse(raw.trim().startsWith("{") ? raw : atob(raw)); // 非 Rewards 成员不代表已退出 Bing。 rewardsSignedIn = content.isRewardsUser === true || content.isAccountLogin === true; } catch (_) {} } const positive = accountVisible || rewardsSignedIn; const state = positive && !signInVisible ? "signed-in" : signInVisible && !positive ? "signed-out" : "unknown"; return { state, accountVisible, signInVisible, rewardsSignedIn }; }, // 仅复用在途探针;结束后下次重新请求。 async ensure() { RunGuard.assertActive(); if (this.checking) return this.checking; this.checking = (async () => { const url = this.probeUrl(); try { const response = await HttpTransport.text({ url, fullResponse: true, headers: { "cache-control": "no-cache", "user-agent": RewardsAuto.ua.pc, "referer": new URL(url).origin + "/" } }); RunGuard.assertActive(); const destination = new URL(response.finalUrl || url); if (["login.live.com", "login.microsoftonline.com"].includes(destination.hostname) || this.isBingHost(destination.hostname) && /^\/fd\/auth\/(?:signin|signout)(?:\/|$)/i.test(destination.pathname)) { throw SessionGuard.stop("Bing 网页已退出登录", true); } if (!this.isBingHost(destination.hostname)) throw SessionGuard.stop("Bing 登录检查跳转到了其他网站"); const login = this.readBingLogin(response.responseText); if (login.state === "signed-out") throw SessionGuard.stop("Bing 网页已退出登录", true); if (login.state !== "signed-in") { throw SessionGuard.stop("Bing 页面未提供明确的登录状态,暂时无法确认"); } RunGuard.assertActive(); if (!this.verified) { this.verified = true; Logger.log("🟢", "Bing 网页登录已确认"); } return true; } catch (e) { if (RunGuard.isStopError(e)) throw e; // 网络或页面异常只停止任务,不清除授权。 throw SessionGuard.stop(`暂时无法确认 Bing 登录状态${e.status ? `(HTTP ${e.status})` : ""}`); } })(); try { return await this.checking; } finally { this.checking = null; } } }; const RequestHeaders = { app(token, country, overrides = {}) { return { "content-type": "application/json; charset=UTF-8", "user-agent": RewardsAuto.ua.app, "authorization": `Bearer ${token}`, "x-rewards-appid": RewardsAuto.appConfig.rewardsAppId, "x-rewards-ismobile": "true", "x-rewards-country": country, "x-rewards-language": "zh", ...overrides }; } }; const ReadingService = { _readOfferId: "", _readOfferLogDate: 0, async getReadProgress() { const region = Utils.isRegionLockEnabled() ? "cn" : RunState.region.toLowerCase(); try { const res = await AppTokenSession.withTokenRetry(token => HttpClient.text({ url: "https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613", headers: RequestHeaders.app(token, region) })); if (Utils.isJSON(res)) { const payload = JSON.parse(res); const promos = Array.isArray(payload.response?.promotions) ? payload.response.promotions : []; const configuredOfferId = String(RewardsAuto.appConfig.offerIds.readArticle || "").trim(); const getAttributes = item => item?.attributes && typeof item.attributes === "object" ? item.attributes : item || {}; const getOfferId = item => String(getAttributes(item).offerid || getAttributes(item).offerId || "").trim(); const parseCounter = value => DataParsers.parseNonNegativeInteger(value); const configuredLower = configuredOfferId.toLowerCase(); const task = promos.find(item => getOfferId(item).toLowerCase() === configuredLower) || promos .map(item => ({ item, id: getOfferId(item), attrs: getAttributes(item) })) .filter(({ id, attrs }) => id && /(read|article|阅读)/i.test(`${id} ${attrs.title || attrs.name || ""}`)) .sort((a, b) => { const aMax = parseCounter(a.attrs.max); const bMax = parseCounter(b.attrs.max); return (Number.isFinite(bMax) ? bMax : 0) - (Number.isFinite(aMax) ? aMax : 0); })[0]?.item; if (task) { const attrs = getAttributes(task); const readOfferId = getOfferId(task); const progress = parseCounter(attrs.progress); const max = parseCounter(attrs.max); if (!Number.isFinite(progress) || !Number.isFinite(max) || max <= 0) { Notice.log("🟡", "阅读进度字段无效,无法确认完成状态"); return false; } ReadingState.progress = progress; ReadingState.max = max; this._readOfferId = readOfferId || configuredOfferId; if (this._readOfferId.toLowerCase() !== configuredLower && this._readOfferLogDate !== RunState.dateNowNum) { this._readOfferLogDate = RunState.dateNowNum; Notice.log("🟡", `阅读任务使用动态 offerId: ${this._readOfferId}(配置值 ${configuredOfferId} 已不匹配)`); } Notice.log("📊", `阅读进度查询: ${progress}/${max} (offerid: ${readOfferId})`); return { progress, max }; } const ids = promos.map(getOfferId).filter(Boolean).slice(0, 20); Notice.log("🟡", `阅读任务未找到 (配置 offerid: ${configuredOfferId})${ids.length ? `;DAPI offerid: ${ids.join(", ")}` : ";DAPI 未返回 promotions"}`); } else { Notice.log("🟡", `DAPI 响应不是 JSON: ${String(res).substring(0, 100)}`); } } catch (e) { if (RunGuard.isStopError(e)) throw e; Notice.log("🔴", `阅读进度获取失败: ${e.message}`); } return false; }, // 单次上报成功后,仍需查进度确认当天完成。 async doRead() { const region = Utils.isRegionLockEnabled() ? "cn" : RunState.region.toLowerCase(); try { const offerId = this._readOfferId || RewardsAuto.appConfig.offerIds.readArticle; // 拼接两个 UUID,生成 56 位十六进制活动 ID。 const id = Utils.getRandomUUID() + Utils.getRandomUUID().slice(0, 24); const res = await AppTokenSession.withTokenRetry(token => HttpClient.text({ method: "POST", url: "https://prod.rewardsplatform.microsoft.com/dapi/me/activities", headers: RequestHeaders.app(token, region, { "content-type": "application/json; charset=utf-8" }), data: JSON.stringify({ amount: 1, country: region, id: id, type: 101, attributes: { offerid: offerId } }) })); if (Utils.isJSON(res)) { const data = JSON.parse(res); if (data.error || data.success === false || data.response?.success === false) { Notice.log("🟡", `阅读接口拒绝上报 (${offerId}): ${String(data.error || data.response?.message || "success=false").slice(0, 160)}`); return null; } if (!data.response?.activity && !data.response?.isDuplicate) { Notice.log("🟡", `阅读接口响应未确认成功 (${offerId}): ${String(res).slice(0, 240)}`); return null; } const points = data.response?.activity?.p || 0; const isDuplicate = data.response?.isDuplicate || false; return { points, isDuplicate }; } return null; } catch (e) { if (RunGuard.isStopError(e)) throw e; Notice.log("🔴", `阅读请求失败: ${e.message}`); return false; } } }; const CardService = { _cardActionKey: undefined, _cardActionId: undefined, _cardActionAsset: undefined, // 优先用锁定字段,再查 offerId;notsupported 不代表锁定。 cardLockReason(card) { const locked = DataParsers.booleanFlag(card.isLocked ?? card.locked); const unlocked = DataParsers.booleanFlag(card.isUnlocked); const state = String(card.state ?? card.status ?? "").toLowerCase(); const exclusiveState = String(card.exclusiveLockedFeatureStatus || "").toLowerCase(); if (locked === true || unlocked === false || /^(locked|unavailable|disabled)$/.test(state) || exclusiveState === "locked") { return "服务器标记为锁定或暂不可用"; } if (locked === false || unlocked === true || exclusiveState === "unlocked" || /^(unlocked|available|active)$/.test(state)) return ""; if (/(?:^|[_-])locked(?:[_-]|$)/i.test(String(card.offerId || ""))) { return "服务器标记为锁定或暂不可用"; } return ""; }, // 按忽略大小写的 offerId 去重。 cardKey(card) { return String(card.offerId || "").trim().toLowerCase(); }, /** * 统一卡片字段;无活动编号返回 null。 * @param {Object|null} item 原始卡片。 * @param {string} source 数据来源。 * @returns {RewardCard|null} */ normalizeCard(item, source) { if (!item || typeof item !== "object") return null; const offerId = String(item.offerId || item.offerid || item.offer_id || item.id || item.name || "").trim(); if (!offerId) return null; const done = [item.isCompleted, item.complete, item.completed] .map(value => DataParsers.booleanFlag(value)).find(v => v !== null); const max = Number(item.pointProgressMax || 0), progress = Number(item.pointProgress); const state = String(item.state || item.status || "").toLowerCase(); // 按完成标记、状态和进度判断,未知为 null。 const isCompleted = done === true || /^(completed|claimed)$/.test(state) || (max > 0 && progress >= max) ? true : done === false || (max > 0 && Number.isFinite(progress)) ? false : null; const title = String(item.title || item.name || item.description || ""); const text = `${offerId} ${title}`; // 类别仅用于开关和日志,上报流程共用。 const kind = /quiz|trivia/i.test(text) ? "quiz" : /puzzle/i.test(text) ? "puzzle" : /image/i.test(text) ? "image_creator" : /explore|search/i.test(text) ? "explore_search" : /dailyset|daily/i.test(text) ? "daily" : /streak/i.test(text) ? "streak" : "open_only"; let destinationUrl = ""; try { const target = new URL(item.destinationUrl || item.destination || item.url || "", "https://rewards.bing.com/earn"); if ((item.destinationUrl || item.destination || item.url) && target.protocol === "https:") destinationUrl = target.href; } catch (_) {} return { ...item, offerId, title, kind, source, isCompleted, hash: String(item.hash || item.activityId || ""), points: Number(item.points ?? item.pointProgressMax ?? item.max ?? 0), isPromotional: DataParsers.booleanFlag(item.isPromotional), type: Number.isInteger(item.type) ? item.type : 11, pageUrl: "https://rewards.bing.com/earn", destinationUrl }; }, // 优先读取 Flight 卡片,旧页面属性作补充。 pageCardData(html) { let text = String(html || ""); const flight = DataParsers.flightChunks(text); if (flight.length) { text = flight.join(""); } const cards = []; let recognized = false; const names = /"(activityCards|promotionCards|promotions|dailySet|dailySetItems|cards)"\s*:\s*\[/g; for (const match of text.matchAll(names)) { const start = match.index + match[0].length - 1; let depth = 0, quoted = false, escaped = false; for (let i = start; i < text.length; i++) { const ch = text[i]; if (quoted) { if (escaped) escaped = false; else if (ch === "\\") escaped = true; else if (ch === '"') quoted = false; } else if (ch === '"') quoted = true; else if (ch === "[") depth++; else if (ch === "]" && --depth === 0) { try { const items = JSON.parse(text.slice(start, i + 1)); const activityItems = items.filter(item => item && (item.offerId || item.offerid || item.offer_id)); if (activityItems.length || /^(activityCards|promotionCards|promotions)$/.test(match[1])) { recognized = true; cards.push(...items); } } catch (_) {} break; } } } // 旧属性只补活动标识,完成状态仍为未知。 if (!recognized) { for (const m of text.matchAll(/data-offer-id="([^"]+)"[^>]*data-hash="([^"]+)"/gi)) { cards.push({ offerId: m[1], hash: m[2], points: 1 }); } recognized = cards.length > 0; } return { cards, recognized }; }, extractCardAction(source) { // 按参数边界匹配 reportActivity,避免误取其他 Action。 const text = String(source || ""), ids = new Set(); const calls = /\bcreateServerReference(?:\s*["']\s*\])?\s*\)?\s*\(/g; for (const match of text.matchAll(calls)) { const start = match.index + match[0].length; const args = []; let depth = 0, quote = "", escaped = false, argumentStart = start, closed = false; for (let i = start; i < Math.min(text.length, start + 8192); i++) { const ch = text[i]; if (quote) { if (escaped) escaped = false; else if (ch === "\\") escaped = true; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'" || ch === "`") { quote = ch; continue; } if (ch === "/" && text[i + 1] === "*") { const end = text.indexOf("*/", i + 2); if (end < 0) break; i = end + 1; continue; } if (ch === "/" && text[i + 1] === "/") { const end = text.indexOf("\n", i + 2); if (end < 0) break; i = end; continue; } if (ch === ")" && depth === 0) { const last = text.slice(argumentStart, i).trim(); if (last) args.push(last); closed = true; break; } if (ch === "," && depth === 0) { args.push(text.slice(argumentStart, i).trim()); argumentStart = i + 1; } else if ("([{".includes(ch)) depth++; else if (")]}".includes(ch)) { if (--depth < 0) break; } else if (ch === ";" && depth === 0) break; } const id = args[0]?.match(/^["']([a-f0-9]{40,})["']$/i); if (closed && args.length === 5 && id && /^["']reportActivity["']$/.test(args[4])) ids.add(id[1]); } return ids.size === 1 ? [...ids][0] : ""; }, cardActionText(source) { return String(source || "").replace(/\\(?:\/|u002f)/gi, "/") .replace(/\\u0026|&/gi, "&").replace(/\\"|"/g, '"'); }, cardActionChunks(source, baseUrl = "https://rewards.bing.com/earn") { const urls = new Set(), base = new URL(baseUrl); // 只收集官网 chunks 及其脚本依赖。 for (const match of this.cardActionText(source).matchAll(/["']([^"'<>\\\s]+\.js(?:\?[^"'<>\\\s]*)?)["']/g)) { try { let path = match[1]; if (/^\/?static\/chunks\//.test(path)) path = "/_next/" + path.replace(/^\//, ""); else if (path.startsWith("_next/")) path = "/" + path; const url = new URL(path, base); if (url.origin !== "https://rewards.bing.com" || url.username || url.password || !url.pathname.startsWith("/_next/static/chunks/") || !url.pathname.endsWith(".js")) continue; if (!url.searchParams.has("dpl") && base.searchParams.has("dpl")) { url.searchParams.set("dpl", base.searchParams.get("dpl")); } url.hash = ""; urls.add(url.href); } catch (_) {} } return [...urls]; }, // 每轮清 Action 缓存,保留上次命中的 JS 地址。 beginCardRun() { this._cardActionKey = null; this._cardActionId = ""; }, async resolveCardAction(html) { RunGuard.assertActive(); const clean = this.cardActionText(html), inlineId = this.extractCardAction(clean); const urls = this.cardActionChunks(clean); const key = [inlineId, ...urls.slice().sort()].join("\n"); // 缓存仅限本轮同组资源,换包后重新识别。 if (this._cardActionKey === key && this._cardActionId) return this._cardActionId; this._cardActionKey = null; this._cardActionId = ""; const remember = (id, asset) => { RunGuard.assertActive(); this._cardActionKey = key; this._cardActionId = id; this._cardActionAsset = asset; Notice.log("🟢", `动态 reportActivity: ${id}(来源:${asset || "活动页面"})`); return id; }; if (inlineId) return remember(inlineId, ""); // 每批 4 个,最多 128 个 JS;45 秒后不再开新批次。 urls.sort((a, b) => Number(b === this._cardActionAsset) - Number(a === this._cardActionAsset)); const seen = new Set(urls), startedAt = Date.now(); const maxFiles = 128, maxTime = 45000; let checked = 0, failed = 0, firstError = ""; while (checked < urls.length && checked < maxFiles && Date.now() - startedAt < maxTime) { const batch = await Promise.all(urls.slice(checked, Math.min(checked + 4, maxFiles)).map(async url => { try { const source = await HttpClient.text({ url, headers: { "cache-control": "no-cache" } }); return { url, source, id: this.extractCardAction(source) }; } catch (e) { if (RunGuard.isStopError(e)) throw e; return { url, source: "", id: "", error: String(e.message || e) }; } })); checked += batch.length; for (const result of batch) { if (!result.error) continue; failed++; if (!firstError) firstError = `${new URL(result.url).pathname.split("/").pop()}: ${result.error.slice(0, 120)}`; } const found = batch.find(result => result.id); if (found) return remember(found.id, found.url); for (const result of batch) { for (const url of this.cardActionChunks(result.source, result.url)) { if (seen.has(url)) continue; seen.add(url); urls.push(url); } } } const detail = urls.length ? `发现 ${urls.length} 个 JS,已检查 ${checked} 个,请求失败 ${failed} 个` : "页面未提供可识别的官网 JS 依赖"; const limit = checked < urls.length ? ";已达到本轮扫描上限" : ""; Notice.log("🟡", `未识别到官网 reportActivity ID:${detail}${limit}${firstError ? `;${firstError}` : ""};保留活动未完成,下次扫描重新识别`); return ""; }, cardActionResult(response) { const inspect = value => { if (typeof value === "boolean") return value; if (!value || typeof value !== "object" || Array.isArray(value)) return null; if (value.error || value.success === false || value.ok === false) return false; return value.success === true || value.ok === true ? true : null; }; try { const direct = inspect(typeof response === "string" ? JSON.parse(response) : response); if (direct !== null) return direct; } catch (_) {} // 只取 RSC 根记录引用的 Action 结果,忽略其他成功字段。 const records = new Map(); for (const line of String(response || "").split(/\r?\n/)) { const m = line.match(/^([a-f0-9]+):(.*)$/i); if (!m) continue; try { records.set(m[1], m[2].startsWith("E{") ? { error: true } : JSON.parse(m[2])); } catch (_) {} } let value = records.get("0")?.a; for (let depth = 0; depth < 6; depth++) { const ref = typeof value === "string" && value.match(/^\$@?([a-f0-9]+)$/i); if (!ref) return inspect(value); value = records.get(ref[1]); } return null; }, // 返回扫描快照:识别状态、待处理卡片、全部状态及 Action 就绪标记。 async discoverCards({ quiet = false, resolveAction = true } = {}) { const states = new Map(); let html = "", pageOk = false, userInfoOk = false; const merge = (item, source) => { const card = this.normalizeCard(item, source); if (!card) return; const key = this.cardKey(card), previous = states.get(key); if (!previous || source === "page") states.set(key, card); }; try { const data = await UserInfoSession.get({ quiet }); if (data) { const dashboard = data.dashboard || data; const daily = dashboard.dailySetPromotions; const more = dashboard.morePromotions || dashboard.promotions; userInfoOk = Array.isArray(more) || (!!daily && typeof daily === "object" && !Array.isArray(daily)); const now = new Date(); const dates = new Set([`${now.getMonth() + 1}/${now.getDate()}/${now.getFullYear()}`, `${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}/${now.getFullYear()}`]); for (const date of dates) { if (Array.isArray(daily?.[date])) for (const item of daily[date]) merge(item, "getuserinfo"); } if (Array.isArray(more)) for (const item of more) merge(item, "getuserinfo"); } } catch (e) { if (RunGuard.isStopError(e)) throw e; if (!quiet) Notice.log("🟡", `getuserinfo 活动解析跳过: ${e.message}`); } // 先恢复 getuserinfo 会话,再读 earn,避免使用旧 hash。 try { html = await HttpClient.text({ url: "https://rewards.bing.com/earn", headers: { "cache-control": "no-cache" } }); const page = this.pageCardData(html); pageOk = page.recognized; for (const item of page.cards) merge(item, "page"); } catch (e) { if (RunGuard.isStopError(e)) throw e; if (!quiet) Notice.log("🟡", `活动页面获取失败: ${e.message}`); } const cards = []; for (const card of states.values()) { const lockReason = this.cardLockReason(card); if (lockReason) { if (!quiet) Notice.log("🔒", `跳过卡片(${card.offerId}): ${lockReason}`); continue; } if (card.isCompleted === true || !Number.isFinite(card.points) || card.points <= 0) continue; if (RewardsAuto.skipPatterns.some(pattern => `${card.offerId} ${card.title}`.toLowerCase().includes(pattern.toLowerCase()))) continue; cards.push(card); } // 明确空列表算识别成功;两个来源都无法识别才失败。 const ok = pageOk || userInfoOk; if (!ok && !quiet) Notice.log("🟡", "未取得可识别的活动数据,不能判断活动是否完成"); if (cards.length && resolveAction) await this.resolveCardAction(html); return { ok, states, cards, actionReady: Boolean(this._cardActionId) }; }, /** * 提交后复查服务器完成状态。 * @param {RewardCard} card 待提交卡片。 * @param {boolean} [allowHashRetry=true] 明确失败且 hash 更新时允许补试一次。 * @returns {Promise} true 已完成,false 未确认,null 跳过锁定项。 */ async claimCard(card, allowHashRetry = true) { const lockReason = this.cardLockReason(card); if (lockReason) { Notice.log("🔒", `跳过卡片(${card.offerId}): ${lockReason}`); return null; } if (card.isCompleted === true) return true; if (!card.offerId || !card.hash) { Notice.log("🟡", `卡片领取失败(${card.offerId || "未知活动"}): 缺少活动编号或 hash`); return false; } const nextAction = this._cardActionId; if (!nextAction) { Notice.log("🟡", `卡片未提交(${card.offerId}): 当前官网 reportActivity ID 未确认`); return false; } let result = null, error = ""; try { const response = await HttpClient.text({ method: "POST", url: "https://rewards.bing.com/earn", headers: { "accept": "text/x-component", "content-type": "text/plain;charset=UTF-8", "next-action": nextAction, "referer": "https://rewards.bing.com/earn", "origin": "https://rewards.bing.com" }, data: JSON.stringify([card.hash, card.type ?? 11, { offerid: card.offerId, isPromotional: card.isPromotional == null || card.isPromotional === "$undefined" ? "$undefined" : String(card.isPromotional), timezoneOffset: new Date().getTimezoneOffset().toString() }]), anonymous: false }); result = this.cardActionResult(response); } catch (e) { if (RunGuard.isStopError(e)) throw e; error = e.message; } // 响应未知或请求报错也复查服务器,已完成则确认。 await Wait.delay(1000); const scan = await this.discoverCards({ quiet: true, resolveAction: false }); const completed = scan?.ok ? scan.states.get(this.cardKey(card))?.isCompleted : null; if (completed === true) { Notice.log("✅", `活动已由服务器确认完成: ${card.title || card.offerId}`); return true; } const refreshed = scan?.ok ? scan.states.get(this.cardKey(card)) : null; // 仅明确失败、仍未完成且 hash 更新时补试一次。 if (allowHashRetry && result === false && completed === false && refreshed && !this.cardLockReason(refreshed) && this.cardKey(refreshed) === this.cardKey(card) && refreshed.hash && refreshed.hash !== card.hash) { Notice.log("🔄", `卡片上报未成功且 hash 已刷新,补试一次(${card.offerId})`); return this.claimCard(refreshed, false); } const reason = error || (result === false ? "Server Action 返回未成功" : result === true ? "上报已接受" : "上报响应未确认"); Notice.log("🟡", `卡片未确认(${card.offerId}): ${reason};${completed === false ? "服务器仍显示未完成" : "无法确认服务器完成状态"}`); return false; } }; const RewardsQuery = { async getRewardsPage(maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const html = await HttpClient.text({ url: "https://rewards.bing.com/earn" }); if (typeof html !== "string") throw new Error("earn 页面无效"); return RewardsPage.prepare(html); } catch (e) { if (RunGuard.isStopError(e)) throw e; if (attempt < maxRetries) { await Wait.delay(3210); continue; } Notice.log("🔴", `仪表盘获取失败: ${e.message}`); } } return null; }, // 同次查询复用来源数据,下次核验重新请求。 async getSearchSnapshot(maxRetries = 3) { const page = await this.getRewardsPage(maxRetries); // earn 请求失败直接返回;仅页面无配额时用接口兜底。 if (!page) return { page: null, quota: SearchQuota.create("failed", "page"), data: null }; const pageQuota = RewardsPage.searchQuota(page); if (pageQuota.source === "page-table" && pageQuota.pc) { Notice.log("🔍", `页面表格配额: PC ${pageQuota.pc.progress}/${pageQuota.pc.max}`); } const candidates = [{ quota: pageQuota, data: null }]; if (pageQuota.status !== "available") { const userInfo = await this.querySearchQuotaFromUserInfo(); candidates.push(userInfo); if (userInfo.quota.status === "available") { Notice.log("🟢", "页面未命中搜索配额,使用 getuserinfo 兜底"); } else if (RunState.token) { const dapi = await this.querySearchQuotaFromAPI(); candidates.push(dapi); if (dapi.quota.status === "available") Notice.log("🟢", "页面未命中搜索配额,使用 DAPI 兜底"); } } const quota = SearchQuota.select(candidates.map(candidate => candidate.quota)); const selected = candidates.find(candidate => candidate.quota === quota); return { page, pageQuota, quota, data: selected?.data || null }; }, async getSearchQuota(maxRetries = 3) { return (await this.getSearchSnapshot(maxRetries)).quota; }, // 汇总复用本次查询,阅读采用最近确认的进度。 async getRewardsInfo(maxRetries = 3) { const { page, pageQuota, quota, data } = await this.getSearchSnapshot(maxRetries); if (!page) return false; const sourceBalance = quota.source === "getuserinfo" ? DataParsers.userInfoBalance(data) : quota.source === "dapi" ? DataParsers.dapiBalance(data) : null; const dailyOffer = RewardsPage.dailyOffer(page); const todayDetails = RewardsPage.activityDetails(page, pageQuota, dailyOffer); if (quota.source === "getuserinfo" && quota.pc && !todayDetails.some(detail => detail.title === "必应搜索")) { todayDetails.push({ title: "必应搜索", points: quota.pc.progress, max: quota.pc.max }); } return { balance: DataParsers.accountBalance(sourceBalance, RewardsPage.balance(page)), pc: quota.pc, mobile: quota.mobile, searchStatus: quota.status, searchSource: quota.source, readProgress: ReadingState.progress, readMax: ReadingState.max, dailyOffer, todayDetails, history: RewardsPage.history(page) }; }, async querySearchQuotaFromUserInfo() { try { const data = await UserInfoSession.get(); if (!data) return { quota: SearchQuota.create("failed", "getuserinfo"), data: null }; const dashboard = data.dashboard || data; const quota = SearchQuota.fromCounters(dashboard.userStatus?.counters?.pcSearch, "getuserinfo"); if (quota.status === "available") Notice.log("📊", `getuserinfo查询: PC ${quota.pc.progress}/${quota.pc.max}`); return { quota, data }; } catch (e) { if (RunGuard.isStopError(e)) throw e; if (Storage.get("Config.debugDAPI", false)) Notice.log("🟡", `getuserinfo 查询失败: ${e.message}`); return { quota: SearchQuota.create("failed", "getuserinfo"), data: null }; } }, async querySearchQuotaFromAPI() { const region = Utils.isRegionLockEnabled() ? "cn" : RunState.region.toLowerCase(); try { const res = await AppTokenSession.withTokenRetry(token => HttpClient.text({ url: "https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613", headers: RequestHeaders.app(token, region) })); if (!Utils.isJSON(res)) return { quota: SearchQuota.create("unknown", "dapi"), data: null }; const data = JSON.parse(res); const response = data?.response; if (!response || typeof response !== "object") return { quota: SearchQuota.create("unknown", "dapi"), data: null }; const promos = Array.isArray(response.promotions) ? response.promotions : []; if (Storage.get("Config.debugDAPI", false)) { const promoNames = promos.map(p => p.name || p.attributes?.offerid || "?").join(", "); Notice.log("🔵", `DAPI promotions(${promos.length}): ${promoNames}`); for (let i = 0; i < Math.min(promos.length, 5); i++) { const p = promos[i]; const attrs = p.attributes || {}; const attrStr = Object.entries(attrs).map(([k, v]) => `${k}=${v}`).join(", ").slice(0, 300); Notice.log("🔵", `DAPI promo[${i}] ${p.name}: ${attrStr}`); } } const counters = response.counters || response.userStatus?.counters; const quota = SearchQuota.fromCounters(counters?.pcSearch, "dapi"); if (quota.status === "available") { Notice.log("📊", `DAPI查询: PC ${quota.pc.progress}/${quota.pc.max}`); } else if (Storage.get("Config.debugDAPI", false)) { if (!counters) Notice.log("🟡", "DAPI 未返回 counters,搜索配额未确认"); else if (quota.status === "zero") Notice.log("🟡", "DAPI 返回配额 0,保留未完成状态"); } return { quota, data: response }; } catch (e) { if (RunGuard.isStopError(e)) throw e; Notice.log("🟡", `DAPI查询失败: ${e.message}`); return { quota: SearchQuota.create("failed", "dapi"), data: null }; } }, // 余额先查 DAPI,再查 getuserinfo;均无有效值则返回 null。 async getBalance() { const region = Utils.isRegionLockEnabled() ? "cn" : RunState.region.toLowerCase(); try { const res = await AppTokenSession.withTokenRetry(token => HttpClient.text({ url: "https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=105", headers: RequestHeaders.app(token, region) })); if (Utils.isJSON(res)) { const data = JSON.parse(res); const balance = DataParsers.dapiBalance(data.response); if (balance !== null) return balance; } } catch (e) { if (RunGuard.isStopError(e)) throw e; } try { const data2 = await UserInfoSession.get(); if (data2) { const balance = DataParsers.userInfoBalance(data2); if (balance !== null) return balance; } } catch (e) { if (RunGuard.isStopError(e)) throw e; } return null; } }; const SearchService = { // 按设备选请求头和 Cookie,主任务使用桌面模式。 async getSearchPage(query, isMobile = false) { const mkt = Utils.isRegionLockEnabled() ? "&mkt=zh-CN" : ""; const deviceType = isMobile ? "m" : "d"; return HttpClient.text({ url: `https://${RunState.host}/search?q=${encodeURIComponent(query)}&form=QBLH${mkt}`, headers: { "user-agent": isMobile ? RewardsAuto.ua.mobile : RewardsAuto.ua.pc, "cookie": `_Rwho=u=${deviceType}&ts=${RunState.dateNowStr}`, "referer": `https://${RunState.host}/?form=QBLH` } }); }, // 搜索后提交两种上报,积分由后续配额查询确认。 async reportSearch(query, isMobile = false) { try { const ig = Utils.getRandomUUID(); const mkt = Utils.isRegionLockEnabled() ? "&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, "referer": `https://${RunState.host}/?form=QBLH`, "cookie": `_Rwho=u=${deviceType}&ts=${RunState.dateNowStr}` }; await HttpClient.text({ method: "POST", url: `https://${RunState.host}/rewardsapp/ncheader?ver=88888888&IID=SERP.5047&IG=${ig}&ajaxreq=1`, headers, data: "wb=1%3bi%3d1%3bv%3d1" }); await HttpClient.text({ method: "POST", url: `https://${RunState.host}/rewardsapp/reportActivity?IG=${ig}&IID=SERP.5047&${params}&ajaxreq=1`, headers, data: `url=${encodeURIComponent(`https://${RunState.host}/search?${params}`)}&V=web` }); return true; } catch (e) { if (RunGuard.isStopError(e)) throw e; Notice.log("🟡", `搜索上报失败: ${e.message}`); return false; } }, // 在线词源获取失败时,回退到内置词库。 async getHotSearchWord() { if (RewardsAuto.apiConfig.mode === "online") { if (SearchState.wordIndex < 1 || SearchState.wordList.length < 1) { // 随机轮换来源,跳过上次接口并保存索引。 const sources = RewardsAuto.apiConfig.sources; const lastApiIndex = parseInt(Storage.get("Config.apiIndex", -1)); const candidates = sources .map((entry, index) => ({ entry, index })) .filter(item => item.index !== lastApiIndex); const selected = candidates[Utils.randomRange(0, candidates.length - 1)]; Storage.set("Config.apiIndex", selected.index); const apiConfig = selected.entry; try { const hotSource = apiConfig.hot[Utils.randomRange(0, apiConfig.hot.length - 1)]; const result = await HttpClient.text({ url: apiConfig.url + hotSource }); if (result && Utils.isJSON(result)) { const res = JSON.parse(result); if (res.code == 200) { SearchState.wordIndex = 1; SearchState.wordList = []; for (let i = 0; i < res.data.length; i++) { SearchState.wordList.push(res.data[i].title); } // 打乱词列表,随机截短标题。 SearchState.wordList.sort(() => Math.random() - 0.5); const sentence = SearchState.wordList[SearchState.wordIndex]; if (typeof sentence === "string" && sentence.trim()) { return sentence.substring(0, Utils.randomRange(20, 32)); } } } } catch (e) { if (RunGuard.isStopError(e)) throw e; Notice.log("🟡", `热搜词获取失败: ${e.message}`); } } else { SearchState.wordIndex++; if (SearchState.wordIndex > SearchState.wordList.length - 1) { SearchState.wordIndex = 0; } const sentence = SearchState.wordList[SearchState.wordIndex]; if (typeof sentence === "string" && sentence.trim()) { return sentence.substring(0, Utils.randomRange(20, 32)); } } Notice.log("🟡", "热搜词接口异常,已从内置词库随机选词"); } return RewardsAuto.searchPool[Utils.randomRange(0, RewardsAuto.searchPool.length - 1)]; }, // 恢复当天搜索状态。 restoreSearchTracking() { const today = Utils.getTodayNum(); const sameDay = Number(Storage.get("Config.searchTrackingDate", 0)) === today; const progress = Number(Storage.get("Config.lastSearchProgress", -1)); const times = Number(Storage.get("Config.restrictedTimes", 0)); SearchState.searchTrackingDate = today; SearchState.lastSearchProgress = sameDay && Number.isSafeInteger(progress) && progress >= 0 ? progress : -1; SearchState.restrictedTimes = sameDay && SearchState.lastSearchProgress >= 0 && Number.isSafeInteger(times) && times >= 0 ? times : 0; }, // 服务器进度连续 3 次未增长时暂停搜索。 async checkSearchRestricted(info, countNoProgress = true) { if (info === undefined) info = await RewardsQuery.getSearchQuota(); if (!SearchQuota.hasProgress(info)) return false; if (SearchState.searchTrackingDate !== Utils.getTodayNum()) this.restoreSearchTracking(); const currentTotal = info.pc.progress; const lastTotal = SearchState.lastSearchProgress; // 启动查询只建基线,搜索后核验才计未增长次数。 if (!countNoProgress && currentTotal !== lastTotal) { SearchState.restrictedTimes = 0; } else if (lastTotal !== -1) { if (currentTotal <= lastTotal && currentTotal < info.pc.max) { if (countNoProgress) SearchState.restrictedTimes++; } else { SearchState.restrictedTimes = 0; } } // 批次核验保留最高进度,避免回落恢复被误算为增长。 SearchState.lastSearchProgress = countNoProgress ? Math.max(currentTotal, lastTotal) : currentTotal; Storage.set("Config.lastSearchProgress", SearchState.lastSearchProgress); Storage.set("Config.restrictedTimes", SearchState.restrictedTimes); Storage.set("Config.searchTrackingDate", SearchState.searchTrackingDate); if (SearchState.restrictedTimes >= 3) { Notice.log("🔴", "搜索连续 3 次核验未增长,可能受限或暂未入账,已暂停搜索(保留未完成状态)", true); return true; } return false; }, // 配额已满时清除受限追踪,完成日期由任务层保存。 clearCompletedTracking() { SearchState.restrictedTimes = 0; SearchState.lastSearchProgress = -1; Storage.set("Config.restrictedTimes", 0); Storage.set("Config.lastSearchProgress", -1); } }; const AppSignIn = { async readDailyProgress() { try { const response = await HttpClient.text({ url: "https://rewards.bing.com/earn", fullResponse: true, headers: { "cache-control": "no-cache", "user-agent": RewardsAuto.ua.pc } }); const source = new URL(response.finalUrl || "https://rewards.bing.com/earn"); if (source.origin !== "https://rewards.bing.com" || !/^\/(?:earn|dashboard)\/?$/.test(source.pathname) || response.status < 200 || response.status >= 300) { throw new Error("Rewards 页面未返回可核验的当日打卡数据"); } return DataParsers.signInProgress(DataParsers.parseStreakData(response.responseText).tasks); } catch (e) { if (RunGuard.isStopError(e)) throw e; Notice.log("🟡", `签到状态暂未确认: ${e.message}`); return { app: null, pc: null }; } }, async signApp() { const region = Utils.isRegionLockEnabled() ? "cn" : RunState.region.toLowerCase(); try { const res = await AppTokenSession.withTokenRetry(token => HttpClient.text({ method: "POST", url: "https://prod.rewardsplatform.microsoft.com/dapi/me/activities", headers: RequestHeaders.app(token, region, { "x-rewards-partnerid": "startapp", "x-rewards-flights": "rwgobig" }), data: JSON.stringify({ amount: 1, id: Utils.getRandomUUID() + Utils.getRandomUUID().slice(0, 24), type: 103, country: region, channel: RewardsAuto.appConfig.channel }) })); if (Utils.isJSON(res)) { const data = JSON.parse(res); const response = data.response || {}; if (response.activity) return Number(response.activity.p || response.activity.points || 0); if (response.isDuplicate || response.activity === null) return 0; Notice.log("🟡", `App签入响应未确认: ${String(res).slice(0, 120)}`); } } catch (e) { if (RunGuard.isStopError(e)) throw e; Notice.log("🔴", `App签入失败: ${e.message}`); } return -1; } }; const TaskRecords = { load(target) { const tasks = Storage.get("Config.tasks", {}); const today = RunState.dateNowNum || Utils.getTodayNum(); target.appSignDate = tasks.signApp || 0; target.pcSignDate = tasks.signPC || 0; // 旧版本只保存任一端成功的 sign 日期,不能作为两端都完成的依据。 target.signDate = target.appSignDate === today && target.pcSignDate === today ? today : 0; target.readDate = tasks.read || 0; target.promosDate = tasks.promos || 0; target.searchDate = tasks.search || 0; target.streakDays = tasks.streakDays || 0; target.signPoint = target.appSignDate === today ? DataParsers.accountBalance(Storage.get("Config.signPoint", -1)) ?? 0 : -1; target.signTimes = target.readTimes = 0; target.savedTasks = { sign: tasks.sign || 0, signApp: tasks.signApp || 0, signPC: tasks.signPC || 0, read: tasks.read || 0, promos: tasks.promos || 0, search: tasks.search || 0, streakDays: tasks.streakDays || 0 }; }, save(target) { RunGuard.assertActive(); const values = { sign: target.signDate, signApp: target.appSignDate, signPC: target.pcSignDate, read: target.readDate, promos: target.promosDate, search: target.searchDate, streakDays: target.streakDays }; const changes = {}; for (const [key, value] of Object.entries(values)) { if (value !== target.savedTasks?.[key]) changes[key] = value; } // 存储合并非原子操作,跨页互斥依赖运行锁。 Storage.set("Config.tasks", { ...Storage.get("Config.tasks", {}), ...changes }); target.savedTasks = values; }, saveSignPoint(value) { RunGuard.assertActive(); Storage.set("Config.signPoint", value); }, // 未知状态保留已确认记录;明确未完成则撤回对应日期。 updateSignProgress(target, progress) { RunGuard.assertActive(); const today = RunState.dateNowNum; if (typeof progress.app === "boolean") target.appSignDate = progress.app ? today : 0; if (typeof progress.pc === "boolean") target.pcSignDate = progress.pc ? today : 0; target.signDate = target.appSignDate === today && target.pcSignDate === today ? today : 0; if (target.appSignDate !== today) target.signPoint = -1; else if (target.signPoint < 0) target.signPoint = 0; this.saveSignPoint(target.signPoint); this.save(target); } }; const LockState = { prefix: "Config.taskRunLock.", releasedPrefix: "Config.taskRunReleased.", pageExitStorageKey: "RewardsAuto.taskRunPageExit", ttl: 10 * 60 * 1000, heartbeatMs: 30000, key: "", id: "", held: false, lost: false, acquiring: false, timer: null, renewing: null, pageLeaving: false, pageExitListener: null }; const TaskRunLock = { installPageExitHandler() { if (LockState.pageExitListener || typeof Platform.window === "undefined" || typeof Platform.window.addEventListener !== "function") return; LockState.pageExitListener = event => { // 往返缓存保留实例;普通离页撤销运行权。 if (event.persisted !== true) this.handlePageExit(); }; Platform.window.addEventListener("pagehide", LockState.pageExitListener); }, revokeLease(record) { const pending = []; // 离页时立即写撤销标记,阻止迟到的续租。 try { pending.push(Storage.set(LockState.releasedPrefix + record.id, record.expiresAt)); } catch (e) { pending.push(Promise.reject(e)); } try { if (Storage.get(record.key, null)?.id === record.id) pending.push(Storage.remove(record.key)); } catch (e) { pending.push(Promise.reject(e)); } return Promise.all(pending); }, handlePageExit() { if (LockState.pageLeaving) return; Startup.cancelRetry(); LockState.pageLeaving = LockState.lost = true; LockState.held = false; if (LockState.timer !== null) clearTimeout(LockState.timer); LockState.timer = null; if (!LockState.key || !LockState.id) return; const record = { key: LockState.key, id: LockState.id, expiresAt: Date.now() + LockState.ttl }; // 暂存离页记录,刷新后补做撤销。 try { Platform.sessionStorage.setItem(LockState.pageExitStorageKey, JSON.stringify(record)); } catch (_) {} this.revokeLease(record).catch(() => {}); }, async clearPageExitLock() { let raw, record; try { raw = Platform.sessionStorage.getItem(LockState.pageExitStorageKey); if (!raw) return; record = JSON.parse(raw); } catch (_) { return; } const valid = typeof record?.id === "string" && record.id && record.key === LockState.prefix + record.id && Number.isFinite(record.expiresAt) && record.expiresAt > Date.now(); if (valid) { // 刷新后补撤销旧锁,弥补离页写入未完成。 await this.revokeLease(record); RunGuard.assertActive(); Platform.log("📄 已清理刷新前的本页运行锁"); } try { if (Platform.sessionStorage.getItem(LockState.pageExitStorageKey) === raw) Platform.sessionStorage.removeItem(LockState.pageExitStorageKey); } catch (_) {} }, // 读取同一登录代次的有效候选,清理失效记录。 async records() { const now = Date.now(); const allKeys = await Storage.keys(); const released = new Set(); await Promise.all(allKeys.filter(key => key.startsWith(LockState.releasedPrefix)).map(async key => { const expiresAt = Number(await Storage.get(key, 0)); if (Number.isFinite(expiresAt) && expiresAt > now) { released.add(key.slice(LockState.releasedPrefix.length)); } else { await Storage.remove(key); } })); const records = await Promise.all(allKeys.filter(key => key.startsWith(LockState.prefix)).map(async key => { const record = await Storage.get(key, null); if (!record || record.id !== key.slice(LockState.prefix.length)) return null; // 撤销标记优先,迟到续租不能恢复运行权。 if (released.has(record.id) || !Number.isFinite(Number(record.expiresAt)) || record.sessionResetId !== SessionGuard.resetId || Number(record.expiresAt) + LockState.ttl <= now) { await Storage.remove(key); return null; } if (Number(record.expiresAt) <= now || !Number.isSafeInteger(record.ticket) || record.ticket < 0 || (record.ticket === 0 && record.choosing !== true)) return null; return { ...record, key }; })); return records.filter(Boolean); }, async acquire({ force = false } = {}) { if (LockState.key || LockState.acquiring) return false; LockState.acquiring = true; LockState.lost = false; this.installPageExitHandler(); try { await this.clearPageExitLock(); RunGuard.assertActive(); // 手动运行撤销旧租约,旧任务在下个检查点停止。 if (force) { const previous = await this.records(); RunGuard.assertActive(); await Promise.all(previous.map(record => this.revokeLease({ ...record, expiresAt: Date.now() + LockState.ttl }))); RunGuard.assertActive(); } LockState.id = Date.now() + "-" + Utils.getRandomUUID(); LockState.key = LockState.prefix + LockState.id; await Storage.set(LockState.key, { id: LockState.id, choosing: true, ticket: 0, force, expiresAt: Date.now() + LockState.ttl, sessionResetId: SessionGuard.resetId }); RunGuard.assertActive(); const records = await this.records(); RunGuard.assertActive(); const own = records.find(record => record.key === LockState.key); if (!own || own.expiresAt <= Date.now()) return false; const ticket = Math.max(0, ...records.map(record => record.ticket)) + 1; await Storage.set(LockState.key, { ...own, choosing: false, ticket, expiresAt: Date.now() + LockState.ttl }); // 候选选号完成后,按票号和实例 ID 排序。 for (let attempt = 0; attempt < 20; attempt++) { RunGuard.assertActive(); const active = await this.records(); RunGuard.assertActive(); if (!active.some(record => record.key === LockState.key)) return false; const others = active.filter(record => record.key !== LockState.key); // 手动候选优先;同类候选按票号排序。 if (!force && others.some(record => record.force === true)) return false; const contenders = force ? others.filter(record => record.force === true) : others; if (contenders.some(record => record.choosing === true)) { await Wait.delay(100); continue; } if (contenders.some(record => record.ticket < ticket || record.ticket === ticket && record.id < LockState.id)) return false; if (force && others.length > contenders.length) { // 清理选号期间新增的自动候选,再复查运行权。 await Promise.all(others.filter(record => record.force !== true).map(record => this.revokeLease({ ...record, expiresAt: Date.now() + LockState.ttl }))); continue; } LockState.held = true; this.scheduleHeartbeat(); return true; } return false; } finally { LockState.acquiring = false; if (!LockState.held) await this.release(); } }, scheduleHeartbeat() { if (!LockState.held || LockState.lost || LockState.pageLeaving) return; LockState.timer = setTimeout(() => { LockState.timer = null; LockState.renewing = this.renew().catch(() => RunGuard.markLockLost()).finally(() => { LockState.renewing = null; this.scheduleHeartbeat(); }); }, LockState.heartbeatMs); }, async renew() { const key = LockState.key; const record = await Storage.get(key, null); if (!LockState.held || LockState.key !== key) return; RunGuard.assertActive(); await Storage.set(key, { ...record, expiresAt: Date.now() + LockState.ttl }); }, // 等待续租结束,仅删除本实例的锁。 async release() { LockState.held = false; if (LockState.timer !== null) clearTimeout(LockState.timer); LockState.timer = null; if (LockState.renewing) await LockState.renewing.catch(() => {}); const key = LockState.key, id = LockState.id; try { if (key && (await Storage.get(key, null))?.id === id) await Storage.remove(key); } finally { LockState.key = LockState.id = ""; LockState.lost = false; LockState.renewing = null; } } }; // Section: vendor/kun-actions // Adapted upstream actions. Copyright (c) 2026 kunkun, MIT. const EngineActions = { async doSign() { if (!Storage.get("Tasks.sign", true)) return TaskResult.create("skipped", "sign_disabled"); if (TaskState.signTimes > 2) return TaskResult.create("failed", "sign_attempts_exhausted"); const progress = await AppSignIn.readDailyProgress(); TaskRecords.updateSignProgress(TaskState, progress); if (TaskState.appSignDate === RunState.dateNowNum) { Notice.log("📱", "App签入已成功,跳过重复提交"); } else { await Wait.randomDelay(); const appPoint = await AppSignIn.signApp(); if (Number.isFinite(appPoint) && appPoint >= 0) { TaskState.signPoint = appPoint; TaskRecords.updateSignProgress(TaskState, { app: true }); Notice.log("📱", appPoint > 0 ? `App签入静默成功 +${appPoint}积分` : "App签入已确认,无新增积分"); } } if (TaskState.pcSignDate === RunState.dateNowNum) { Notice.log("💻", "pc签入已成功,跳过重复提交"); } else { Notice.log("📅", progress.pc === false ? "PC打卡尚未完成,搜索结束后复核" : "PC打卡状态暂未确认,搜索结束后复核"); } if (TaskState.signDate === RunState.dateNowNum) { Notice.log("🔵", "App签到与PC打卡均已确认完成", true); return TaskResult.create("completed", "sign_confirmed"); } // PC 打卡依赖搜索;不在搜索前反复等待重试。 if (TaskState.appSignDate === RunState.dateNowNum) return TaskResult.create("pending", "pc_sign_waiting_search"); TaskState.signTimes++; return TaskResult.create("unknown", "app_sign_unconfirmed", true); }, async doPromos(secondScan = false) { if (!Storage.get("Tasks.promos", true)) return TaskResult.create("skipped", "promos_disabled"); Notice.log(secondScan ? "🔄" : "🧩", secondScan ? "二次扫描:检查是否有新解锁的卡片..." : "扫描活动卡片..."); let scan = await CardService.discoverCards(); if (!scan.ok) { TaskState.promosDate = 0; TaskRecords.save(TaskState); Notice.log("🟡", "活动状态查询失败,保留未完成状态"); return TaskResult.create("unknown", "card_scan_unavailable"); } const enabled = card => card.kind !== "quiz" || Storage.get("Tasks.quiz", true); const pending = scan.cards.filter(enabled); if (pending.length && !scan.actionReady) { TaskState.promosDate = 0; TaskRecords.save(TaskState); Notice.log("🟡", "当前官网 reportActivity ID 未确认,活动卡片保留未完成,稍后重新识别"); return TaskResult.create("unknown", "card_action_unavailable"); } const fresh = pending.filter(card => !TaskState.cardAttempted.has(CardService.cardKey(card))); Notice.log("🧩", secondScan ? `二次扫描:${fresh.length} 个新卡片,${pending.length - fresh.length} 个本轮已尝试` : `发现 ${pending.length} 个待完成卡片(已按活动编号去重)`); for (const card of fresh) { const key = CardService.cardKey(card); TaskState.cardAttempted.add(key); TaskState.cardResults.set(key, false); Notice.log(" ", `[${card.kind}] ${card.title} +${card.points}p`); await Wait.randomDelay(3000, 8000); TaskState.cardResults.set(key, await CardService.claimCard(card) === true); } // 提交后整批复查,吸收延迟入账和页面完成结果。 if (fresh.length) scan = await CardService.discoverCards({ quiet: true, resolveAction: false }); if (scan?.ok) { for (const key of TaskState.cardResults.keys()) { const completed = scan.states.get(key)?.isCompleted; if (typeof completed === "boolean") TaskState.cardResults.set(key, completed); } } const ok = [...TaskState.cardResults.values()].filter(Boolean).length; const unconfirmed = TaskState.cardResults.size - ok; const count = scan.cards.filter(enabled).length; // 扫描成功、无待办且本轮尝试均确认,才记完成。 TaskState.promosDate = scan?.ok && count === 0 && unconfirmed === 0 ? RunState.dateNowNum : 0; TaskRecords.save(TaskState); Notice.log(TaskState.promosDate ? "✅" : "🟡", TaskState.promosDate ? `活动卡片已验证完成,本轮 ${ok} 个已确认` : `活动卡片尚未完成:本轮 ${ok} 个已确认/${unconfirmed} 个未确认;${scan?.ok ? `服务器还有 ${count} 个待完成` : "最终状态查询失败"}`, !secondScan); if (TaskState.promosDate) return TaskResult.create("completed", "cards_verified"); const unknown = !scan?.ok || [...TaskState.cardResults].some(([key, completed]) => !completed && typeof scan.states.get(key)?.isCompleted !== "boolean"); return TaskResult.create(unknown ? "unknown" : "pending", unknown ? "card_completion_unavailable" : "cards_incomplete"); }, }; // Section: core/auth // OAuth request layout follows TheNetsky v5 MobileAccessLogin, adapted for GM APIs. const Authorization = { validating: false, client: '0000000040170455', scope: 'service::prod.rewardsplatform.microsoft.com::MBI_SSL', callback: 'https://login.live.com/oauth20_desktop.srf', tokenEndpoint: 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token', async begin() { const state = crypto.randomUUID(); await Storage.set('Auth.pending', { state, expiresAt: Date.now() + 180000 }); await Storage.set('Config.code', ''); const url = new URL('https://login.live.com/oauth20_authorize.srf'); for (const [key, value] of Object.entries({ client_id: this.client, response_type: 'code', scope: this.scope, redirect_uri: this.callback, state })) url.searchParams.set(key, value); Platform.openTab(url.href, { active: true }); Notice.log('🔑', '请在打开的微软页面完成授权, 后台会检测授权结果; 也可用菜单提交回调网址, 不要将其发到聊天或第三方'); return state; }, isCallback(raw) { const url = Safety.url(raw); return Boolean(url && url.origin + url.pathname === this.callback); }, async capture(raw) { if (!this.isCallback(raw)) return false; const url = new URL(raw), pending = Storage.get('Auth.pending', null); const code = url.searchParams.get('code'); if (!code) return false; if (!pending?.state || !Number.isFinite(pending.expiresAt) || pending.expiresAt <= Date.now() || url.searchParams.get('state') !== pending.state) { Notice.log('🔴', '授权回调校验失败或已过期, 请重新从菜单发起授权'); return false; } await Storage.set('Config.code', code); await Storage.set('Config.codeTime', Date.now()); try { if (Platform.location?.href && this.isCallback(Platform.location.href)) { Platform.history?.replaceState({}, '', this.callback + '?removed=true'); } } catch {} Notice.log('🟢', '已接收并校验授权码, 等待后台交换令牌'); return true; }, async probe() { const pending = Storage.get('Auth.pending', null); if (!pending?.state || pending.expiresAt <= Date.now()) return false; const url = new URL('https://login.live.com/oauth20_authorize.srf'); for (const [key, value] of Object.entries({ client_id: this.client, response_type: 'code', scope: this.scope, redirect_uri: this.callback, state: pending.state })) url.searchParams.set(key, value); const response = await HttpTransport.text({ url: url.href, fullResponse: true }); const location = String(response.responseHeaders || '').match(/^location:\s*(.*?)\s*$/im)?.[1]; const target = location ? new URL(location, url.href).href : response.finalUrl; if (!target || !this.isCallback(target)) return false; return await this.capture(target) && await this.renew(); }, async submit(raw) { if (typeof raw !== 'string' || !raw.trim() || !this.isCallback(raw.trim())) { Notice.log('🔴', '请仅在脚本猫输入框提交微软授权回调网址, 不接受单独授权码或其他地址'); return false; } if (!await this.capture(raw.trim())) return false; const ok = await this.renew(); Notice.log(ok ? '🟢' : '🔴', ok ? '授权验证通过, 未执行积分任务' : '授权交换未完成'); return ok; }, async clear() { await Storage.set('Auth.revision', Storage.get('Auth.revision', 0) + 1); RunState.token = false; for (const [key, value] of Object.entries({ 'Config.code': '', 'Config.token': '', 'Config.codeTime': 0, 'Config.tokenTime': 0, 'Auth.pending': null })) await Storage.set(key, value); }, async exchange(fields) { const revision = Storage.get('Auth.revision', 0); const body = new URLSearchParams({ client_id: this.client, scope: this.scope, redirect_uri: this.callback, ...fields }); const response = JSON.parse(await HttpTransport.text({ method: 'POST', url: this.tokenEndpoint, headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: body.toString() })); if (!response.access_token || response.error) { if (response.error === 'invalid_grant' && fields.grant_type === 'refresh_token' && Storage.get('Auth.revision', 0) === revision && Storage.get('Config.token', '') === fields.refresh_token) { await Storage.set('Config.token', ''); } Notice.log('🔴', '令牌交换未成功, 请重新授权'); return false; } if (Storage.get('Auth.revision', 0) !== revision) return false; await Storage.set('Config.code', ''); await Storage.set('Auth.pending', null); await Storage.set('Config.token', Storage.get('Safety.persistToken', true) ? (response.refresh_token || '') : ''); if (Storage.get('Auth.revision', 0) !== revision) { await Storage.set('Config.token', ''); return false; } await Storage.set('Config.tokenTime', Date.now()); RunState.token = response.access_token; return true; }, async renew(force = false) { if (RunState.token && !force) return true; const refresh = Storage.get('Config.token', ''); try { const code = Storage.get('Config.code', ''); const time = Storage.get('Config.codeTime', 0); if (code && time > 0 && Date.now() - time < 180000) { return await this.exchange({ grant_type: 'authorization_code', code }); } if (refresh) return await this.exchange({ grant_type: 'refresh_token', refresh_token: refresh }); Notice.log('🟡', 'App 授权未就绪, 请先使用“仅验证授权”菜单; 网页任务仍可核验'); } catch (error) { Notice.log('🔴', `授权请求失败: ${error.message}`); } return false; }, async validate() { if (this.validating || Runner.running || Runner.starting) { Notice.log('🟡', '已有授权验证或任务正在执行'); return false; } this.validating = true; try { if (await this.renew()) { Notice.log('🟢', '授权验证通过, 未执行积分任务'); return true; } await this.begin(); const deadline = Date.now() + 180000; let lastError = ''; while (Date.now() < deadline) { try { if (RunState.token || (Storage.get('Config.code', '') && await this.renew()) || await this.probe()) { Notice.log('🟢', '授权验证通过, 未执行积分任务'); return true; } } catch (error) { lastError = Safety.redact(error.message); } await new Promise(resolve => setTimeout(resolve, 2000)); } Notice.log('🔴', `授权等待结束, 可从菜单重新发起或提交有效回调网址${lastError ? '; ' + lastError : ''}`); return false; } finally { this.validating = false; } } }; const AppTokenSession = { renewToken: () => Authorization.renew(), async withTokenRetry(request) { if (!RunState.token) return null; try { return await request(RunState.token); } catch (error) { if (!/HTTP 401/.test(error.message)) throw error; RunState.token = false; return await Authorization.renew(true) ? request(RunState.token) : null; } } }; // Section: core/schedule const Schedule = { current: null, attemptingDate: '', clock() { const parts = Object.fromEntries(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()).map(part => [part.type, part.value])); const minute = Number(parts.hour) * 60 + Number(parts.minute); return { date: `${parts.year}-${parts.month}-${parts.day}`, dateNum: Number(`${parts.year}${parts.month}${parts.day}`), dateStr: `${Number(parts.month)}/${Number(parts.day)}/${parts.year}`, inWindow: minute >= 540 && minute <= 1200 }; }, reason(mode) { if (mode === 'manual') return ''; const now = this.clock(); if (!now.inWindow) return '不在北京时间 09:00-20:00 的自动启动时段内'; if (this.attemptingDate === now.date || Storage.get('Schedule.automatic', null)?.date === now.date) return '今天已自动启动, 如需重跑请使用手动菜单'; return ''; }, async claim(mode) { mode = mode === 'manual' ? 'manual' : 'automatic'; const reason = this.reason(mode); if (reason) { Notice.log('📅', reason); return null; } const record = { mode, date: this.clock().date, startedAt: Date.now(), runId: crypto.randomUUID(), searchTarget: Utils.randomRange(20, 30), status: 'running' }; Notice.log('💾', '保存运行记录, 每项最多等待15秒确认'); if (mode === 'automatic') { this.attemptingDate = record.date; await Storage.set('Schedule.automatic', record); } await Storage.set('Schedule.latest', record); this.current = record; return record; }, async finish(record, failed) { const value = { ...record, status: failed ? 'failed' : 'finished', endedAt: Date.now() }; if (Storage.get('Schedule.latest', null)?.runId === record.runId) await Storage.set('Schedule.latest', value); if (record.mode === 'automatic' && Storage.get('Schedule.automatic', null)?.runId === record.runId) await Storage.set('Schedule.automatic', value); this.current = null; }, }; // Pin calendar semantics for the adapted upstream modules as well as the scheduler. Utils.getTodayNum = () => Schedule.clock().dateNum; Utils.getTodayStr = () => Schedule.clock().dateStr; RewardsAuto.apiConfig.mode = Storage.get('Config.api', 'offline'); // Section: services/tasks const UserInfoSession = { // /earn is primary in v5. Legacy getuserinfo is explicit opt-in, not a silent login flow. async get() { if (!Storage.get('Config.legacyUserInfo', false)) return null; try { const data = JSON.parse(await HttpClient.text({ url: 'https://rewards.bing.com/api/getuserinfo?type=1' })); return DataParsers.validUserInfo(data) ? data : null; } catch (error) { if (RunGuard.isStopError(error)) throw error; return null; } } }; const RegionService = { async check() { if (!Utils.isRegionLockEnabled()) { Notice.log('🔓', '地区锁已关闭'); return true; } try { const html = await HttpClient.text({ url: `https://${RunState.host}/` }); const country = html.match(/RevIpCC\s*:\s*["']([a-z]{2})["']/i)?.[1]?.toUpperCase(); if (!country) { Notice.log('🔴', '地区未知, 已停止; 这不能证明 IP 在境外'); return false; } RunState.region = country; if (country !== 'CN') { Notice.log('🔴', `页面识别地区为 ${country}, 地区锁已停止任务`); return false; } Notice.log('🟢', '地区检查通过: CN'); return true; } catch (error) { Notice.log('🔴', `地区检查未完成: ${error.message}`); return false; } } }; // The upstream dynamic JS is inspected as text only; never evaluated or loaded as a script. const originalDiscover = CardService.discoverCards; CardService.discoverCards = async function(options) { const scan = await originalDiscover.call(this, options); scan.cards = scan.cards.filter(card => { const allowed = Safety.card(card); if (!allowed) Notice.log('🔒', `跳过不允许的活动: ${card.offerId}`); return allowed; }); return scan; }; const originalClaim = CardService.claimCard; CardService.claimCard = async function(card, retry) { if (!Safety.card(card)) return null; const result = await originalClaim.call(this, card, retry); Runner.results.push(Accounting.result(result === true ? 'confirmed' : result === null ? 'skipped' : 'unconfirmed', result === true ? 'offer-complete' : 'offer-not-confirmed', card.offerId)); return result; }; const TaskActions = { doSign: (...args) => EngineActions.doSign(...args), doPromos: (...args) => EngineActions.doPromos(...args), async doRead() { if (!Storage.get('Tasks.read', true)) return TaskResult.create('skipped', 'read_disabled'); let progress = await ReadingService.getReadProgress(); if (!progress) return TaskResult.create('unknown', 'read_progress_unavailable'); for (let count = 0; count < 10 && progress.progress < progress.max; count++) { RunGuard.assertActive(); if (!await ReadingService.doRead()) return TaskResult.create('unknown', 'read_report_unconfirmed'); await Wait.randomDelay(); const after = await ReadingService.getReadProgress(); if (!after || after.max !== progress.max) return TaskResult.create('unknown', 'read_verification_unavailable'); progress = after; Notice.log('📖', `服务器阅读进度: ${progress.progress}/${progress.max}`); } if (progress.progress >= progress.max) { TaskState.readDate = RunState.dateNowNum; TaskRecords.save(TaskState); return TaskResult.create('completed', 'read_verified'); } return TaskResult.create('pending', 'read_incomplete'); }, async doSearch() { if (!Storage.get('Tasks.search', true)) return TaskResult.create('skipped', 'search_disabled'); let info = await RewardsQuery.getSearchQuota(); if (!SearchQuota.hasProgress(info)) return TaskResult.create('unknown', 'search_quota_unavailable'); const maximum = info.pc.max; while (info.pc.progress < maximum && Runner.searches < Runner.current.searchTarget) { if (await SearchService.checkSearchRestricted(info, false)) return TaskResult.create('pending', 'search_restricted'); const remaining = Runner.current.searchTarget - Runner.searches; const batchSize = Math.min(5, remaining, Math.max(1, Math.ceil((maximum - info.pc.progress) / 3))); for (let n = 0; n < batchSize; n++) { RunGuard.assertActive(); const query = RewardsAuto.apiConfig.mode === 'online' ? await SearchService.getHotSearchWord() : RewardsAuto.searchPool[Utils.randomRange(0, RewardsAuto.searchPool.length - 1)]; Runner.searches++; Notice.log('🔍', `搜索 ${Runner.searches}/${Runner.current.searchTarget}: ${query}`); try { await SearchService.getSearchPage(query, false); await SearchService.reportSearch(query, false); } catch (error) { if (RunGuard.isStopError(error)) throw error; Notice.log('🟡', `搜索请求未确认: ${error.message}`); } await Wait.delay(Utils.randomRange(15000, 45000)); } const after = await RewardsQuery.getSearchQuota(); if (!SearchQuota.hasProgress(after) || after.pc.max !== maximum || after.pc.progress < info.pc.progress) { return TaskResult.create('unknown', 'search_counter_changed_or_unavailable'); } Notice.log('📊', `服务器搜索配额: ${after.pc.progress}/${after.pc.max}`); if (await SearchService.checkSearchRestricted(after)) return TaskResult.create('pending', 'search_restricted'); info = after; } if (info.pc.progress >= maximum) { TaskState.searchDate = RunState.dateNowNum; TaskRecords.save(TaskState); SearchService.clearCompletedTracking(); return TaskResult.create('completed', 'search_quota_verified'); } return TaskResult.create('pending', 'round_search_target_reached'); }, async doDailySet() { if (!Storage.get('Tasks.daily', true)) return TaskResult.create('skipped', 'daily_disabled'); const scan = await CardService.discoverCards({ quiet: true }); if (!scan.ok) return TaskResult.create('unknown', 'daily_server_data_unavailable'); const cards = scan.cards.filter(card => card.kind === 'daily' || /dailyset/i.test(card.offerId)); if (!cards.length) { Notice.log('📅', '未发现支持后台提交的每日活动, 不启动网页点击'); return TaskResult.create('skipped', 'no_supported_daily_offer'); } for (const card of cards) { const key = CardService.cardKey(card); if (TaskState.cardAttempted.has(key)) continue; TaskState.cardAttempted.add(key); TaskState.cardResults.set(key, await CardService.claimCard(card) === true); } const after = await CardService.discoverCards({ quiet: true, resolveAction: false }); const completed = after.ok && cards.every(card => after.states.get(CardService.cardKey(card))?.isCompleted === true); return TaskResult.create(completed ? 'completed' : 'pending', completed ? 'daily_server_verified' : 'daily_not_confirmed'); }, async inspectStreaks() { const page = await RewardsQuery.getRewardsPage(); if (!page) return TaskResult.create('unknown', 'streak_page_unavailable'); const streaks = DataParsers.parseStreakData(page.html).tasks; Notice.log('📅', `连签信息已读取, 识别到 ${Array.isArray(streaks) ? streaks.length : 0} 项; 不自动激活额外权益`); return TaskResult.create('skipped', 'streak_observation_only'); }, async detectPendingPoints() { const html = await HttpClient.text({ url: 'https://rewards.bing.com/dashboard' }); if (/可领取|可領取|claim\s+(?:your\s+)?points/i.test(html)) { Notice.log('🎁', '页面有待领取积分提示, 请自行查看并手动领取', true); return TaskResult.create('pending', 'manual_claim_required'); } return TaskResult.create('skipped', 'no_claim_marker_detected'); } }; // Section: runtime const Runner = { running: false, starting: false, current: null, results: [], searches: 0, init() { RunState.dateNowNum = Utils.getTodayNum(); RunState.dateNowStr = Utils.getTodayStr(); RunState.startTime = Date.now(); RunState.forceRun = false; SessionGuard.stopped = false; SessionGuard.logoutCleared = false; SessionGuard.resetId = Storage.get('Config.sessionResetId', ''); WebSession.verified = false; TaskState.signTimes = TaskState.readTimes = 0; TaskState.cardAttempted.clear(); TaskState.cardResults.clear(); TaskRecords.load(TaskState); SearchService.restoreSearchTracking(); CardService.beginCardRun(); ReadingState.progress = 0; ReadingState.max = 30; this.results = []; this.searches = 0; }, async task(name, action) { Notice.log('▶', `开始: ${name}`); try { const result = await action(); const status = ({ completed: 'confirmed', pending: 'unconfirmed', unknown: 'unconfirmed', skipped: 'skipped', failed: 'failed' })[result?.status] || 'unconfirmed'; this.results.push({ ...Accounting.result(status, result?.reason || 'result_unknown'), task: name }); Notice.log('📋', `${name}: ${status} (${result?.reason || 'unknown'})`); return result; } catch (error) { if (RunGuard.isStopError(error)) throw error; this.results.push({ ...Accounting.result('failed', 'task_error'), task: name, message: Safety.redact(error.message) }); Notice.log('🔴', `${name}失败: ${error.message}`); } }, async run(mode = 'automatic') { if (this.running || Authorization.validating || (this.starting && mode === 'manual')) { Notice.log('🟡', '已有任务在运行或等待启动, 跳过重复点击'); return false; } if (Storage.get('Safety.authOnly', false)) { Notice.log('🔑', '仅验证授权已开启, 积分任务暂停; 请使用授权菜单'); return false; } this.running = true; let record, failed = false, before = null; try { await Storage.flush(); // Every matched page shares the same candidate leases. Manual requests never steal a lease. if (!await TaskRunLock.acquire()) { Notice.log('🟡', '其他页面正在运行, 本页跳过'); return false; } record = await Schedule.claim(mode); if (!record) return false; this.current = record; this.init(); RunDateGuard.begin(RunState.dateNowNum); Notice.log('🚀', `${record.mode === 'manual' ? '手动' : '自动'}轮次开始, 搜索目标 ${record.searchTarget} 次`); await WebSession.ensure(); if (!await RegionService.check()) throw new Error('地区检查未通过, 本轮停止'); const tokenOK = await Authorization.renew(); before = await RewardsQuery.getBalance(); await this.task('签到', () => TaskActions.doSign()); if (tokenOK) await this.task('阅读', () => TaskActions.doRead()); else this.results.push({ ...Accounting.result('skipped', 'app_token_unavailable'), task: '阅读' }); await this.task('活动卡片', () => TaskActions.doPromos()); await this.task('搜索', () => TaskActions.doSearch()); await this.task('签到复核', () => TaskActions.doSign()); await this.task('连签观察', () => TaskActions.inspectStreaks()); await this.task('每日活动接口', () => TaskActions.doDailySet()); if (Storage.get('Tasks.promos', true)) await this.task('二次扫描', () => TaskActions.doPromos(true)); await this.task('待领取积分检查', () => TaskActions.detectPendingPoints()); const after = await RewardsQuery.getBalance(); const report = { ...record, endedAt: Date.now(), searches: this.searches, balanceBefore: Accounting.pointValue(before), balanceAfter: Accounting.pointValue(after), observedBalanceDelta: Accounting.pointDelta(before, after), counts: Accounting.counts(this.results), results: this.results }; await Storage.set('Report.latest', report); Notice.log('📊', `本轮结束, 搜索尝试 ${this.searches}/${record.searchTarget}; 账户余额变化 ${report.observedBalanceDelta ?? '未知'} (不等于逐项收益)`, true); return report; } catch (error) { failed = true; Notice.log('🔴', `本轮停止: ${error.message}`); throw new Error(Safety.redact(error.message)); } finally { try { await Notice.flush(); await Storage.flush(); if (record) await Schedule.finish(record, failed); await Storage.flush(); } finally { try { if (LockState.key) await TaskRunLock.release(); } finally { RunDateGuard.end(); this.current = null; this.running = false; } } } }, async automatic() { if (Storage.get('Safety.authOnly', false)) { Notice.log('🔑', '仅验证授权模式, 自动任务暂停'); return; } const reason = Schedule.reason('automatic'); if (reason) { Notice.log('📅', reason); return; } this.starting = true; try { const delay = Utils.randomRange(5000, 60000); Notice.log('⏳', `${delay / 1000}秒后自动启动`); await new Promise(resolve => setTimeout(resolve, delay)); Notice.log('⏱', '启动延时结束'); return await this.run('automatic'); } finally { this.starting = false; } } }; // Compatibility hook referenced by the preserved lease module; no page polling loop. const Startup = { cancelRetry() {} }; // Section: entry const App = { menu(name, action, options) { Platform.registerMenu(name, async inputValue => { try { return await action(inputValue); } catch (error) { Notice.log('🔴', error.message); } }, options); }, register() { this.menu('🚀 手动运行一轮 (随时, 可重复)', () => Runner.run('manual')); this.menu('🔑 仅验证授权 (不执行积分任务)', () => Authorization.validate()); this.menu('📋 提交微软授权回调网址 (备用)', value => Authorization.submit(value), { inputType: 'text', inputLabel: '只在此提交微软回调网址, 不要发送到聊天', inputPlaceholder: 'https://login.live.com/oauth20_desktop.srf?...', inputDefaultValue: '' }); this.menu('🧪 存储通信自检', async () => { Notice.log('💾', '开始存储自检, 最多等待15秒'); await Storage.set('Diagnostics.probe', { id: crypto.randomUUID(), time: Date.now() }); Notice.log('🟢', '存储通信自检通过, 未执行积分任务'); }); this.menu('📋 查看最近运行报告', () => Platform.alert(JSON.stringify(Storage.get('Report.latest', {}), null, 2))); this.menu('清除本地授权凭据', () => Authorization.clear()); }, async start() { this.register(); return Runner.automatic(); } }; return App.start(); })();