// ==UserScript== // @name Rewards Auto Script(微软奖励自动化智能最新版) // @namespace https://github.com/yt2399/Rewards-auto-Script // @version 1.3.1 // @author yt2399 // @icon https://bing.com/th?id=OMR.icon-96.png&pid=Rewards // @homepage https://github.com/yt2399/Rewards-auto-Script // @homepageURL https://github.com/yt2399/Rewards-auto-Script // @supportURL https://github.com/yt2399/Rewards-auto-Script/issues // @source https://github.com/yt2399/Rewards-auto-Script // @license GPL-3.0-or-later // @tips 此脚本为开源免费使用,请勿购买。官方地址:https://github.com/yt2399/Rewards-auto-Script // @description 自动执行 Daily Set、More Promotions、Punchcard、搜索型活动和 PC 搜索,支持断点续跑与微信 ClawBot 结果推送 // @match https://rewards.bing.com/* // @match https://www.bing.com/* // @match https://cn.bing.com/* // @run-at document-idle // @noframes // @connect rewards.bing.com // @connect www.bing.com // @connect cn.bing.com // @connect raw.githubusercontent.com // @connect www.pushplus.plus // @grant GM_info // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_openInTab // @grant GM_xmlhttpRequest // @storageName rewards-auto-script // ==/UserScript== "use strict"; (() => { // src/adapters/gm.ts function modernApi() { return typeof GM !== "undefined" ? GM : void 0; } async function gmGetValue(key, defaultValue) { const api = modernApi(); if (api?.getValue) return await api.getValue(key, defaultValue); if (typeof GM_getValue === "function") return GM_getValue(key, defaultValue); return defaultValue; } async function gmSetValue(key, value) { const api = modernApi(); if (api?.setValue) { await api.setValue(key, value); return; } if (typeof GM_setValue === "function") GM_setValue(key, value); } async function gmDeleteValue(key) { const api = modernApi(); if (api?.deleteValue) { await api.deleteValue(key); return; } if (typeof GM_deleteValue === "function") GM_deleteValue(key); } function gmRegisterMenu(name, listener) { const api = modernApi(); if (api?.registerMenuCommand) { api.registerMenuCommand(name, listener); return; } if (typeof GM_registerMenuCommand === "function") GM_registerMenuCommand(name, listener); } async function gmNotify(title, text) { const details = { title, text, timeout: 8e3 }; const api = modernApi(); if (api?.notification) { await api.notification(details); return; } if (typeof GM_notification === "function") GM_notification(details); } async function gmOpenTab(url, active = false) { const options = { active, insert: true, setParent: true }; const api = modernApi(); if (api?.openInTab) return await api.openInTab(url, options); if (typeof GM_openInTab === "function") return GM_openInTab(url, options); window.open(url, "_blank", "noopener,noreferrer"); return null; } async function gmRequest(details) { return await new Promise((resolve, reject) => { let settled = false; const finish = (response) => { if (settled) return; settled = true; const raw = response.response ?? response.responseText; let data = raw; if (details.responseType === "json" && typeof raw === "string") { try { data = JSON.parse(raw); } catch (error) { reject(new Error(`无法解析 JSON:${error instanceof Error ? error.message : String(error)}`)); return; } } if (response.status < 200 || response.status >= 300) { reject(new Error(`HTTP ${response.status} ${response.statusText}`)); return; } resolve({ status: response.status, data, finalUrl: response.finalUrl }); }; const fail = (response) => { if (settled) return; settled = true; reject(new Error(`请求失败:HTTP ${response.status || 0} ${response.statusText || ""}`.trim())); }; const requestDetails = { ...details, onload: finish, onerror: fail, ontimeout: fail }; const api = modernApi(); if (api?.xmlHttpRequest) { const maybePromise = api.xmlHttpRequest(requestDetails); if (maybePromise && typeof maybePromise.then === "function") { void maybePromise.then(finish, reject); } return; } if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest(requestDetails); return; } fetch(details.url, { method: details.method, headers: details.headers, body: details.data, credentials: details.anonymous ? "omit" : "include" }).then(async (response) => { const responseText = await response.text(); finish({ status: response.status, statusText: response.statusText, response: responseText, responseText, finalUrl: response.url }); }).catch(reject); }); } // src/core/storage.ts var STORAGE_KEYS = { config: "mrsu:config:v1", runState: "mrsu:run-state:v1", logs: "mrsu:logs:v1", runRequest: "mrsu:run-request:v1", lastScheduledDate: "mrsu:last-scheduled-date:v1" }; var DEFAULT_CONFIG = { enabledTasks: { dashboard: true, "daily-set": true, "more-promotions": true, punchcards: true, "desktop-search": true }, searchQuerySource: "upstream", customSearchQueries: "", searchDelayMinSeconds: 120, searchDelayMaxSeconds: 180, maxDesktopSearches: 50, maxStagnantSearches: 10, skipZeroPointOffers: true, autoClaimPunchcardRewards: false, clawBotPushEnabled: false, clawBotPushToken: "", scheduleEnabled: false, scheduleTime: "09:15" }; var SEARCH_QUERY_SOURCES = [ "upstream", "bing-news-cn", "bing-news-global", "custom" ]; function normalizeConfig(value) { const merged = { ...DEFAULT_CONFIG, ...value, enabledTasks: { ...DEFAULT_CONFIG.enabledTasks, ...value?.enabledTasks ?? {} } }; const finiteNumber = (candidate, fallback) => { const number = Number(candidate); return Number.isFinite(number) ? Math.round(number) : fallback; }; const minDelay = Math.max( 120, Math.min(1800, finiteNumber(merged.searchDelayMinSeconds, DEFAULT_CONFIG.searchDelayMinSeconds)) ); return { ...merged, searchQuerySource: SEARCH_QUERY_SOURCES.includes(merged.searchQuerySource) ? merged.searchQuerySource : DEFAULT_CONFIG.searchQuerySource, customSearchQueries: String(merged.customSearchQueries ?? ""), clawBotPushToken: String(merged.clawBotPushToken ?? "").trim(), searchDelayMinSeconds: minDelay, searchDelayMaxSeconds: Math.max( minDelay, Math.min( 1800, finiteNumber(merged.searchDelayMaxSeconds, Math.max(DEFAULT_CONFIG.searchDelayMaxSeconds, minDelay)) ) ), maxDesktopSearches: Math.max( 1, Math.min(100, finiteNumber(merged.maxDesktopSearches, DEFAULT_CONFIG.maxDesktopSearches)) ), maxStagnantSearches: Math.max( 1, Math.min(30, finiteNumber(merged.maxStagnantSearches, DEFAULT_CONFIG.maxStagnantSearches)) ), scheduleTime: /^\d{2}:\d{2}$/.test(merged.scheduleTime) ? merged.scheduleTime : "09:15" }; } async function loadConfig() { return normalizeConfig(await gmGetValue(STORAGE_KEYS.config, null)); } async function saveConfig(config) { await gmSetValue(STORAGE_KEYS.config, normalizeConfig(config)); } async function loadRunState() { return await gmGetValue(STORAGE_KEYS.runState, null); } async function saveRunState(state) { state.updatedAt = Date.now(); await gmSetValue(STORAGE_KEYS.runState, state); } async function loadLogs() { return await gmGetValue(STORAGE_KEYS.logs, []); } async function saveLogs(logs) { await gmSetValue(STORAGE_KEYS.logs, logs.slice(-200)); } async function loadRunRequest() { return await gmGetValue(STORAGE_KEYS.runRequest, null); } async function clearRunRequest() { await gmDeleteValue(STORAGE_KEYS.runRequest); } // src/core/utils.ts function localDateKey(date = /* @__PURE__ */ new Date()) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } function dashboardDateKey(date = /* @__PURE__ */ new Date()) { return `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}/${date.getFullYear()}`; } function randomToken() { const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); return [...bytes].map((value) => value.toString(16).padStart(2, "0")).join(""); } function randomInt(min, max) { const low = Math.ceil(Math.min(min, max)); const high = Math.floor(Math.max(min, max)); return Math.floor(Math.random() * (high - low + 1)) + low; } function wait(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function desktopSearchRemaining(dashboard) { return remainingFromCounters(dashboard.userStatus.counters.pcSearch); } function remainingFromCounters(counters) { return counters?.reduce( (sum, counter) => sum + Math.max(0, Number(counter.pointProgressMax) - Number(counter.pointProgress)), 0 ) ?? 0; } function allPromotions(dashboard) { return [ ...Object.values(dashboard.dailySetPromotions ?? {}).flat(), ...dashboard.morePromotions ?? [], ...dashboard.morePromotionsWithoutPromotionalItems ?? [], ...dashboard.promotionalItems ?? [], ...dashboard.promotionalItem ? [dashboard.promotionalItem] : [] ]; } function findPromotion(dashboard, offerId) { return allPromotions(dashboard).find((promotion) => promotion.offerId === offerId); } function promotionComplete(promotion) { if (!promotion) return false; return Boolean( promotion.complete || Number(promotion.pointProgressMax ?? 0) > 0 && Number(promotion.pointProgress ?? 0) >= Number(promotion.pointProgressMax ?? 0) ); } function dedupePromotions(promotions) { return [...new Map(promotions.filter((item) => item?.offerId).map((item) => [item.offerId, item])).values()]; } function createTaskResult(id, enabled) { return { id, status: enabled ? "pending" : "skipped", message: enabled ? "等待执行" : "已在设置中关闭", processed: 0, succeeded: 0, failed: 0 }; } function isSameController(url, token) { return url.hash.includes(`mrs-run=${token}`) || url.hash.includes(`mrs-schedule=${token}`); } function controllerHash(token) { return `#mrs-run=${encodeURIComponent(token)}`; } // src/core/logger.ts var LOG_EVENT = "mrsu:log"; var Logger = class { async write(level, scope, message) { const entry = { id: randomToken(), time: Date.now(), level, scope, message }; const logs = await loadLogs(); logs.push(entry); await saveLogs(logs); window.dispatchEvent(new CustomEvent(LOG_EVENT, { detail: entry })); const method = level === "debug" ? "debug" : level === "info" ? "info" : level === "warn" ? "warn" : "error"; console[method](`[Rewards Userscript][${scope}] ${message}`); } debug(scope, message) { return this.write("debug", scope, message); } info(scope, message) { return this.write("info", scope, message); } warn(scope, message) { return this.write("warn", scope, message); } error(scope, message) { return this.write("error", scope, message); } }; function onLog(listener) { const handler = (event) => listener(event.detail); window.addEventListener(LOG_EVENT, handler); return () => window.removeEventListener(LOG_EVENT, handler); } var logger = new Logger(); // src/core/run-state.ts var TASK_ORDER = [ "dashboard", "daily-set", "more-promotions", "punchcards", "desktop-search" ]; function createRunState(config, source, controllerToken = randomToken()) { return { version: 1, runId: randomToken(), controllerToken, dateKey: localDateKey(), source, status: "running", currentTask: null, taskOrder: [...TASK_ORDER], taskResults: { dashboard: createTaskResult("dashboard", config.enabledTasks.dashboard), "daily-set": createTaskResult("daily-set", config.enabledTasks["daily-set"]), "more-promotions": createTaskResult("more-promotions", config.enabledTasks["more-promotions"]), punchcards: createTaskResult("punchcards", config.enabledTasks.punchcards), "desktop-search": createTaskResult("desktop-search", config.enabledTasks["desktop-search"]) }, processedOfferIds: [], startedAt: Date.now(), updatedAt: Date.now() }; } function nextRunnableTask(state) { return state.taskOrder.find((taskId) => { const status = state.taskResults[taskId].status; return status === "pending" || status === "running"; }) ?? null; } function emitRunState(state) { window.dispatchEvent(new CustomEvent("mrsu:state", { detail: state })); } function onRunState(listener) { const handler = (event) => listener(event.detail); window.addEventListener("mrsu:state", handler); return () => window.removeEventListener("mrsu:state", handler); } // src/rewards/react-parser.ts function concatFlightChunks(html) { const pages = typeof html === "string" ? [html] : html; const pattern = /self\.__next_f\.push\(\[1,\s*"((?:[^"\\]|\\.)*)"\]\)/g; let combined = ""; for (const page of pages) { for (const match of page.matchAll(pattern)) { try { combined += JSON.parse(`"${match[1]}"`); } catch { } } combined += "\n"; } return combined; } function extractObjects(combined, anchor) { const objects = []; const anchorKey = anchor.replace(/^"|"$/g, ""); let cursor = 0; while (cursor < combined.length) { const anchorIndex = combined.indexOf(anchor, cursor); if (anchorIndex === -1) break; cursor = anchorIndex + anchor.length; let start = combined.lastIndexOf("{", anchorIndex); while (start !== -1) { let depth = 0; let end = -1; let inString = false; let escaped = false; for (let index = start; index < combined.length; index++) { const character = combined[index]; if (escaped) { escaped = false; continue; } if (inString && character === "\\") { escaped = true; continue; } if (character === '"') { inString = !inString; continue; } if (inString) continue; if (character === "{") depth++; if (character === "}") { depth--; if (depth === 0) { end = index; break; } } } if (end >= anchorIndex) { try { const parsed = JSON.parse( combined.slice(start, end + 1).replace(/"\$undefined"/g, "null") ); if (Object.prototype.hasOwnProperty.call(parsed, anchorKey)) { objects.push(parsed); cursor = Math.max(cursor, end + 1); break; } } catch { } } start = combined.lastIndexOf("{", start - 1); } } return objects; } function todayStamp(date = /* @__PURE__ */ new Date()) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } function normalizeOfferDate(rawDate) { if (typeof rawDate !== "string") return null; const match = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(rawDate); if (!match) return null; return `${match[3]}-${match[1]}-${match[2]}`; } function parseOffers(html) { const combined = concatFlightChunks(html); const offers = /* @__PURE__ */ new Map(); const today = todayStamp(); for (const object of extractObjects(combined, '"offerId"')) { const offerId = typeof object.offerId === "string" ? object.offerId : ""; if (!offerId) continue; const attributes = object.attributes && typeof object.attributes === "object" ? object.attributes : {}; const hash = typeof object.hash === "string" ? object.hash : null; const isCompleted = (object.isCompleted ?? object.complete) === true; const isLocked = object.isLocked === true; const date = normalizeOfferDate(object.date); const activityTypeNumber = Number(object.activityType ?? object.activity_type ?? attributes.activity_type); const promotionalValue = object.isPromotional ?? attributes.promotional; const isPromotional = promotionalValue === true || typeof promotionalValue === "string" && promotionalValue.toLowerCase() === "true"; const candidate = { offerId, hash, title: typeof object.title === "string" ? object.title : "", description: typeof object.description === "string" ? object.description : "", points: Number(object.points ?? object.pointProgressMax ?? 0), promotionSubtype: typeof object.promotionSubtype === "string" ? object.promotionSubtype : null, destination: typeof object.destination === "string" ? object.destination : typeof object.destinationUrl === "string" ? object.destinationUrl : "", isCompleted, isPromotional, isLocked, date, activityType: Number.isInteger(activityTypeNumber) && activityTypeNumber > 0 ? activityTypeNumber : null, reportable: Boolean(hash && !isCompleted && !isLocked && (date === null || date <= today)) }; const existing = offers.get(offerId); if (!existing) { offers.set(offerId, candidate); continue; } existing.hash ??= candidate.hash; existing.title ||= candidate.title; existing.description ||= candidate.description; existing.points ||= candidate.points; existing.promotionSubtype ??= candidate.promotionSubtype; existing.destination ||= candidate.destination; existing.isCompleted ||= candidate.isCompleted; existing.isPromotional ||= candidate.isPromotional; existing.isLocked ||= candidate.isLocked; existing.date ??= candidate.date; existing.activityType ??= candidate.activityType; existing.reportable = Boolean( existing.hash && !existing.isCompleted && !existing.isLocked && (existing.date === null || existing.date <= today) ); } return [...offers.values()]; } function parseQuestChildren(html) { const combined = concatFlightChunks(html); const children = []; const seen = /* @__PURE__ */ new Set(); for (const object of extractObjects(combined, '"offerId"')) { const offerId = typeof object.offerId === "string" ? object.offerId : ""; if (!offerId || !offerId.toLowerCase().includes("pcchild") || seen.has(offerId)) continue; seen.add(offerId); const hash = typeof object.hash === "string" ? object.hash : null; const isCompleted = (object.isCompleted ?? object.complete) === true; const isLocked = object.isLocked === true; const isDisabled = object.isDisabled === true; children.push({ offerId, hash, points: Number(object.points ?? object.pointProgressMax ?? 0), isCompleted, isLocked, isDisabled, reportable: Boolean(hash && !isCompleted && !isLocked && !isDisabled) }); } return children; } function isParentQuestId(offerId) { const normalized = offerId.toLowerCase(); return !normalized.includes("pcchild") && (normalized.includes("pcparent") || normalized.includes("punchcard")); } function parseQuestParents(...htmls) { const combined = htmls.map((html) => concatFlightChunks(html)).join(""); const anchors = []; const patterns = [ /\/earn\/quest\/([A-Za-z0-9_]+)/g, /"id":"quest_([A-Za-z0-9_]+)"/g, /([A-Za-z0-9_]*pcparent[A-Za-z0-9_]*)/gi ]; for (const pattern of patterns) { for (const match of combined.matchAll(pattern)) { const id = match[1] ?? match[0]; if (id) anchors.push({ id, index: match.index ?? 0 }); } } anchors.sort((left, right) => left.index - right.index); const parents = /* @__PURE__ */ new Map(); for (let index = 0; index < anchors.length; index++) { const anchor = anchors[index]; if (!anchor || !isParentQuestId(anchor.id)) continue; const nextIndex = anchors[index + 1]?.index ?? combined.length; const region = combined.slice(anchor.index, Math.min(nextIndex, anchor.index + 3e3)); const title = region.match(/"alt":"((?:[^"\\]|\\.)*)"/)?.[1] ?? region.match(/"title":"((?:[^"\\]|\\.)*)"/)?.[1] ?? ""; const pointsMatch = region.match(/\["\+","([\d,]+)"\]/); const taskMatch = region.match(/(\d+)\s*\/\s*(\d+)\s*tasks/i); const existing = parents.get(anchor.id); parents.set(anchor.id, { offerId: anchor.id, title: existing?.title || title, pointProgressMax: existing?.pointProgressMax || Number(pointsMatch?.[1]?.replace(/,/g, "") ?? 0), complete: Boolean(existing?.complete) || Boolean(taskMatch && Number(taskMatch[1]) >= Number(taskMatch[2]) && Number(taskMatch[2]) > 0) }); } return [...parents.values()]; } function extractBuildId(html) { const combined = concatFlightChunks(html); return html.match(/[?&](?:amp;)?dpl=([A-Za-z0-9._-]+)/i)?.[1] ?? combined.match(/[?&](?:amp;)?dpl=([A-Za-z0-9._-]+)/i)?.[1] ?? combined.match(/"buildId":"([A-Za-z0-9._-]+)"/)?.[1] ?? combined.match(/"b":"([A-Za-z0-9._-]{8,})"/)?.[1] ?? html.match(/\/_next\/static\/([A-Za-z0-9._-]+)\//)?.[1] ?? null; } function extractActionIds(jsText) { const byName = {}; const all = /* @__PURE__ */ new Set(); const hex = "[a-f0-9]{40,64}"; const ignoredNames = /* @__PURE__ */ new Set(["callServer", "findSourceMapURL", "encodeFormAction"]); const callPattern = new RegExp(`createServerReference\\s*\\)?\\s*\\(\\s*"(${hex})"([\\s\\S]{0,400}?)\\)`, "g"); for (const match of jsText.matchAll(callPattern)) { const id = match[1]; if (!id) continue; all.add(id); const candidates = [...(match[2] ?? "").matchAll(/"([A-Za-z_$][\w$]*)"/g)].map((candidate) => candidate[1]).filter((name2) => Boolean(name2 && !ignoredNames.has(name2))); const name = candidates.at(-1); if (name) byName[name] = id; } const barePattern = new RegExp(`createServerReference\\s*\\)?\\s*\\(\\s*"(${hex})"`, "g"); for (const match of jsText.matchAll(barePattern)) { if (match[1]) all.add(match[1]); } const actionPattern = new RegExp(`\\$ACTION_ID_(${hex})`, "g"); for (const match of jsText.matchAll(actionPattern)) { if (match[1]) all.add(match[1]); } return { byName, all: [...all] }; } function extractInitialChunkPaths(htmls) { const paths = /* @__PURE__ */ new Set(); const pattern = /(?:\/_next\/)?(static\/chunks\/[\w\-./()]+?\.js)/g; for (const html of htmls) { for (const match of html.matchAll(pattern)) { if (match[1]) paths.add(`/_next/${match[1]}`); } } return [...paths]; } function extractDynamicChunkPaths(js) { const paths = /* @__PURE__ */ new Set(); const builder = /static\/chunks\/"\s*\+\s*\w+\s*\+\s*"([-.])"\s*\+\s*\{([\s\S]*?)\}\s*\[/g; for (const match of js.matchAll(builder)) { const separator = match[1]; if (!separator) continue; for (const pair of (match[2] ?? "").matchAll(/(\d+)\s*:\s*"([a-f0-9]+)"/g)) { if (pair[1] && pair[2]) paths.add(`/_next/static/chunks/${pair[1]}${separator}${pair[2]}.js`); } } if (!paths.size) { for (const pair of js.matchAll(/\b(\d{2,6}):"([a-f0-9]{12,})"/g)) { if (!pair[1] || !pair[2]) continue; paths.add(`/_next/static/chunks/${pair[1]}-${pair[2]}.js`); paths.add(`/_next/static/chunks/${pair[1]}.${pair[2]}.js`); } } return [...paths]; } function createRewardsSnapshot(earnHtml, dashboardHtml) { const offers = parseOffers([earnHtml, dashboardHtml]); return { offers, reportable: offers.filter((offer) => offer.reportable), questParents: parseQuestParents(earnHtml, dashboardHtml), buildId: extractBuildId(earnHtml) }; } function routerStateTree(segment) { return encodeURIComponent( JSON.stringify([ "", { children: [ "(nav)", { children: [segment, { children: ["__PAGE__", {}, null, null, 0] }, null, null, 0] }, null, null, 0 ] }, null, null, 16 ]) ); } function questRouterStateTree(questId) { return encodeURIComponent( JSON.stringify([ "", { children: [ "(nav)", { children: [ "earn", { children: [ "quest", { children: [ ["questId", questId, "d", null], { children: ["__PAGE__", {}, null, null, 0] }, null, null, 0 ] }, null, null, 0 ] }, null, null, 0 ] }, null, null, 0 ] }, null, null, 16 ]) ); } // src/rewards/client.ts var REWARDS_ORIGIN = "https://rewards.bing.com"; var EARN_URL = `${REWARDS_ORIGIN}/earn`; var DASHBOARD_URL = `${REWARDS_ORIGIN}/dashboard`; var DASHBOARD_API_URL = `${REWARDS_ORIGIN}/api/getuserinfo`; var SEARCH_QUERIES_URL = "https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/v4/src/functions/search-queries.json"; var ACTIVITY_QUERIES_URL = "https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/v4/src/functions/bing-search-activity-queries.json"; var BING_NEWS_RSS_URLS = { "bing-news-cn": "https://www.bing.com/news/search?q=%E7%83%AD%E7%82%B9&format=rss&setlang=zh-cn&cc=cn", "bing-news-global": "https://www.bing.com/news/search?q=top%20stories&format=rss&setlang=en-us&cc=us" }; var FALLBACK_SEARCH_QUERIES = [ "今日天气", "世界历史上的今天", "如何学习一门新语言", "常见植物养护方法", "计算机基础知识", "健康早餐食谱", "经典电影推荐", "城市公共交通规划", "人工智能基础概念", "家庭收纳技巧", "太阳系行星介绍", "提高阅读效率的方法" ]; async function sameOriginText(url) { const response = await fetch(url, { credentials: "include", headers: { Accept: "text/html,application/xhtml+xml" }, redirect: "follow" }); if (!response.ok) throw new Error(`${url} 返回 HTTP ${response.status}`); if (!response.url.startsWith(REWARDS_ORIGIN)) throw new Error("Rewards 页面跳转到了登录页,请先登录"); return await response.text(); } function serverActionAcknowledged(response) { return /^\d+:true\s*$/m.test(response); } function uniqueQueries(items) { return [...new Set(items.map((item) => item.replace(/\s+/g, " ").trim()).filter(Boolean))]; } function parseBingNewsRss(xml) { const documentNode = new DOMParser().parseFromString(xml, "application/xml"); if (documentNode.querySelector("parsererror")) return []; return uniqueQueries( [...documentNode.querySelectorAll("item > title")].map( (node) => (node.textContent ?? "").replace(/\s+[-–—|]\s+[^-–—|]+$/, "").trim() ) ); } function expandTrendingQueries(titles, source) { return uniqueQueries( titles.flatMap( (title) => source === "bing-news-cn" ? [title, `${title} 最新消息`, `${title} 事件进展`] : [title, `${title} latest news`, `${title} updates`] ) ); } var RewardsClient = class { async getDashboard() { if (location.origin === REWARDS_ORIGIN) { const response2 = await fetch(DASHBOARD_API_URL, { credentials: "include", headers: { Accept: "application/json" } }); if (!response2.ok) throw new Error(`Dashboard API 返回 HTTP ${response2.status}`); const data = await response2.json(); if (!data?.dashboard?.userStatus) throw new Error("Dashboard API 未返回登录用户数据"); return data; } const response = await gmRequest({ method: "GET", url: DASHBOARD_API_URL, headers: { Accept: "application/json" }, responseType: "json", timeout: 2e4, anonymous: false }); if (!response.data?.dashboard?.userStatus) throw new Error("跨页面 Dashboard 请求未返回登录用户数据"); return response.data; } async bootstrap() { if (location.origin !== REWARDS_ORIGIN) throw new Error("只能在 Rewards 页面初始化任务上下文"); const [earnHtml, dashboardHtml] = await Promise.all([ sameOriginText(EARN_URL), sameOriginText(DASHBOARD_URL) ]); if (!/]*\bid=["']dailyset["']/i.test(`${earnHtml} ${dashboardHtml}`)) { throw new Error("Rewards 页面没有渲染 Daily Set,请确认账号已登录且页面可正常访问"); } const actionIds = await this.resolveActionIds([earnHtml, dashboardHtml]); const snapshot = createRewardsSnapshot(earnHtml, dashboardHtml); const deploymentId = extractBuildId(earnHtml); await logger.info( "初始化", `读取到 ${snapshot.offers.length} 个任务,${snapshot.reportable.length} 个可上报任务,发现 ${Object.keys(actionIds).length} 个 Server Action` ); return { earnHtml, dashboardHtml, snapshot, actionIds, deploymentId }; } async resolveActionIds(htmls) { const scripts = /* @__PURE__ */ new Map(); const initialPaths = extractInitialChunkPaths(htmls); await Promise.all( initialPaths.map(async (path) => { try { scripts.set(path, await sameOriginText(`${REWARDS_ORIGIN}${path}`)); } catch { } }) ); const dynamicPaths = /* @__PURE__ */ new Set(); for (const script of scripts.values()) { for (const path of extractDynamicChunkPaths(script)) { if (!scripts.has(path)) dynamicPaths.add(path); } } await Promise.all( [...dynamicPaths].map(async (path) => { try { scripts.set(path, await sameOriginText(`${REWARDS_ORIGIN}${path}`)); } catch { } }) ); const actionIds = {}; for (const script of scripts.values()) Object.assign(actionIds, extractActionIds(script).byName); return actionIds; } async reportActivity(bootstrap, offer, options) { const actionId = bootstrap.actionIds.reportActivity; if (!actionId) throw new Error("当前 Rewards 版本没有发现 reportActivity Server Action"); const questUrl = options?.questId ? `${EARN_URL}/quest/${encodeURIComponent(options.questId)}` : EARN_URL; const routerTree = options?.questId ? questRouterStateTree(options.questId) : routerStateTree("earn"); return await this.reportServerAction( actionId, [ offer.hash, offer.activityType ?? 11, { offerid: offer.offerId, isPromotional: offer.isPromotional ? true : "$undefined", timezoneOffset: String((/* @__PURE__ */ new Date()).getTimezoneOffset()) } ], questUrl, routerTree, bootstrap.deploymentId ); } async reportServerAction(actionId, body, url, routerTree, deploymentId) { const response = await fetch(url, { method: "POST", credentials: "include", referrer: url, headers: { Accept: "text/x-component", "Content-Type": "text/plain;charset=UTF-8", "Next-Action": actionId, "Next-Router-State-Tree": routerTree, ...deploymentId ? { "X-Deployment-Id": deploymentId } : {} }, body: JSON.stringify(body) }); const text = await response.text(); return { status: response.status, acknowledged: response.ok && serverActionAcknowledged(text) }; } async getDesktopQueryPool(source = "upstream", customSearchQueries = "") { if (source === "custom") { const custom = uniqueQueries(customSearchQueries.split(/[\r\n,,]+/)); if (custom.length) return custom; await logger.warn("搜索词", "自定义搜索词为空,将使用上游随机搜索词库"); return await this.getUpstreamQueryPool(); } if (source === "bing-news-cn" || source === "bing-news-global") { try { const response = await gmRequest({ method: "GET", url: BING_NEWS_RSS_URLS[source], responseType: "text", timeout: 2e4, anonymous: true }); const titles = parseBingNewsRss(response.data); if (titles.length) { const queries = expandTrendingQueries(titles, source); await logger.info( "搜索词", `已加载 ${titles.length} 条 Bing 热点新闻,生成 ${queries.length} 个搜索词` ); return queries; } throw new Error("RSS 未返回可用标题"); } catch (error) { await logger.warn("搜索词", `Bing 热点加载失败,将使用上游随机搜索词库:${String(error)}`); return await this.getUpstreamQueryPool(); } } return await this.getUpstreamQueryPool(); } async getUpstreamQueryPool() { try { const response = await gmRequest({ method: "GET", url: SEARCH_QUERIES_URL, responseType: "json", timeout: 2e4, anonymous: true }); if (Array.isArray(response.data) && response.data.length) return response.data; } catch (error) { await logger.warn("搜索词", `远程搜索词加载失败,将使用内置备用列表:${String(error)}`); } return FALLBACK_SEARCH_QUERIES; } async getActivityQueries(promotion) { try { const response = await gmRequest({ method: "GET", url: ACTIVITY_QUERIES_URL, responseType: "json", timeout: 2e4, anonymous: true }); const normalizedTitle = normalizeText(promotion.title ?? ""); const match = response.data.find((item) => normalizeText(item.title) === normalizedTitle); if (match?.queries.length) return match.queries; } catch (error) { await logger.warn("活动搜索", `活动搜索词加载失败,将使用标题和说明:${String(error)}`); } const title = (promotion.title ?? "").trim(); const description = (promotion.description ?? "").trim(); const derived = description.replace( /^\s*(?:search(?:\s+on\s+bing|\s+bing|\s+the\s+web)?\s+for|look\s+up|find|explore|discover)\b[\s:]+/i, "" ).replace(/^["'“”‘’]+|["'“”‘’]+$/g, "").replace(/[.!?]+$/g, "").trim(); return [...new Set([derived, title, description].filter(Boolean))]; } }; function normalizeText(value) { return value.normalize("NFD").trim().toLowerCase().replace(/[^\x20-\x7E]/g, "").replace(/[?!]/g, ""); } var rewardsClient = new RewardsClient(); var rewardsUrls = { origin: REWARDS_ORIGIN, earn: EARN_URL, dashboard: DASHBOARD_URL, quest: (offerId) => `${EARN_URL}/quest/${encodeURIComponent(offerId)}` }; // src/notifications/clawbot.ts var PUSHPLUS_SEND_URL = "https://www.pushplus.plus/send"; var TASK_LABELS = { dashboard: "积分与状态", "daily-set": "Daily Set", "more-promotions": "More Promotions", punchcards: "Punchcard", "desktop-search": "桌面搜索" }; var STATUS_LABELS = { pending: "等待", running: "执行中", completed: "完成", skipped: "已关闭", failed: "失败", stopped: "已停止" }; async function sendClawBotMessage(token, title, content) { const normalizedToken = token.trim(); if (!normalizedToken) throw new Error("请先填写 pushplus Token"); const response = await gmRequest({ method: "POST", url: `${PUSHPLUS_SEND_URL}/${encodeURIComponent(normalizedToken)}`, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ token: normalizedToken, title, content, channel: "clawbot", template: "txt" }), responseType: "json", timeout: 2e4, anonymous: true }); if (response.data?.code !== 200) { throw new Error(response.data?.msg || `pushplus 返回状态码 ${response.data?.code ?? "unknown"}`); } } function buildRunOutcomeMessage(state) { const outcome = state.status === "completed" ? "任务完成" : "任务失败"; const gained = Math.max( 0, Number(state.currentPoints ?? state.initialPoints ?? 0) - Number(state.initialPoints ?? 0) ); const taskLines = state.taskOrder.map((taskId) => { const result = state.taskResults[taskId]; return `- ${TASK_LABELS[taskId]}:${STATUS_LABELS[result.status] ?? result.status};${result.message}`; }); const lines = [ `状态:${STATUS_LABELS[state.status] ?? state.status}`, `执行方式:${state.source === "manual" ? "手动执行" : "ScriptCat 定时执行"}`, `当前积分:${state.currentPoints ?? "未知"}`, `本轮积分变化:+${gained}`, "", "任务明细:", ...taskLines ]; if (state.error) lines.push("", `失败原因:${state.error}`); return { title: `Rewards Auto Script - ${outcome}`, content: lines.join("\n") }; } // src/core/engine.ts var BING_SEARCH_URL = "https://www.bing.com/search"; function shuffled(items) { const result = [...items]; for (let index = result.length - 1; index > 0; index--) { const target = randomInt(0, index); [result[index], result[target]] = [result[target], result[index]]; } return result; } function taskLabel(taskId) { return { dashboard: "Dashboard", "daily-set": "Daily Set", "more-promotions": "More Promotions", punchcards: "Punchcard", "desktop-search": "桌面搜索" }[taskId]; } var RunEngine = class { constructor(config) { this.config = config; } async start(source, controllerToken) { const state = createRunState(this.config, source, controllerToken); await saveRunState(state); emitRunState(state); if (location.origin === rewardsUrls.origin) { history.replaceState(null, "", `${location.pathname}${location.search}${controllerHash(state.controllerToken)}`); } await logger.info("运行", `开始${source === "manual" ? "手动" : "ScriptCat 定时"}任务`); await this.resume(state); return state; } async resume(providedState) { const state = providedState ?? await loadRunState(); if (!state || !["running", "waiting-page"].includes(state.status)) return; if (state.dateKey !== localDateKey()) { state.status = "stopped"; state.error = "检测到跨日的旧任务,已停止以避免重复执行"; state.finishedAt = Date.now(); await this.persist(state); return; } if (!isSameController(new URL(location.href), state.controllerToken)) return; if (location.origin === rewardsUrls.origin) { await this.resumeRewards(state); return; } if (location.hostname === "www.bing.com" || location.hostname === "cn.bing.com") { try { await this.resumeBing(state); } catch (error) { await this.failRun(state, error); } } } async stop() { const state = await loadRunState(); if (!state || !["running", "waiting-page"].includes(state.status)) return; state.status = "stopped"; state.finishedAt = Date.now(); state.error = "用户手动停止"; await this.persist(state); await logger.warn("运行", "任务已手动停止"); } async resumeRewards(state) { try { state.status = "running"; await this.persist(state); const [dashboardData, bootstrap] = await Promise.all([ rewardsClient.getDashboard(), rewardsClient.bootstrap() ]); state.currentPoints = dashboardData.dashboard.userStatus.availablePoints; state.initialPoints ??= state.currentPoints; await this.persist(state); if (state.pendingPromotion) { await this.finishPendingPromotion(state, dashboardData); } while (state.status === "running") { const taskId = nextRunnableTask(state); if (!taskId) { state.status = "completed"; state.currentTask = null; state.finishedAt = Date.now(); await this.persist(state); const gained = Math.max(0, Number(state.currentPoints ?? 0) - Number(state.initialPoints ?? 0)); await logger.info("运行", `本轮任务结束,积分变化 +${gained}`); await this.notifyRunOutcome(state); return; } state.currentTask = taskId; const task = state.taskResults[taskId]; task.status = "running"; task.startedAt ??= Date.now(); task.message = "正在执行"; await this.persist(state); await logger.info(taskLabel(taskId), "开始执行"); if (taskId === "dashboard") { await this.runDashboard(state, dashboardData); } else if (taskId === "daily-set") { const navigated = await this.runPromotionTask(state, taskId, dashboardData, bootstrap); if (navigated) return; } else if (taskId === "more-promotions") { const navigated = await this.runPromotionTask(state, taskId, dashboardData, bootstrap); if (navigated) return; } else if (taskId === "punchcards") { await this.runPunchcards(state, dashboardData, bootstrap); } else { const navigated = await this.startDesktopSearch(state, dashboardData); if (navigated) return; } } } catch (error) { await this.failRun(state, error); } } async runDashboard(state, data) { const points = data.dashboard.userStatus.availablePoints; const remaining = desktopSearchRemaining(data.dashboard); state.currentPoints = points; this.completeTask(state.taskResults.dashboard, `当前积分 ${points},桌面搜索剩余 ${remaining} 分`, 1, 1, 0); await this.persist(state); } promotionList(taskId, data) { if (taskId === "daily-set") { return data.dashboard.dailySetPromotions?.[dashboardDateKey()] ?? []; } return dedupePromotions([ ...data.dashboard.morePromotions ?? [], ...data.dashboard.morePromotionsWithoutPromotionalItems ?? [] ]); } promotionIsEligible(taskId, promotion) { if (promotionComplete(promotion)) return false; if (Number(promotion.pointProgressMax ?? 0) <= 0 && this.config.skipZeroPointOffers) return false; if (promotion.exclusiveLockedFeatureStatus === "locked") return false; if (!promotion.promotionType) return false; if (taskId === "more-promotions" && Number(promotion.priority ?? 0) < 0 && promotion.exclusiveLockedFeatureStatus !== "unlocked") { return false; } return String(promotion.attributes?.promotional ?? "").toLowerCase() !== "true"; } async runPromotionTask(state, taskId, initialData, bootstrap) { const task = state.taskResults[taskId]; const promotions = this.promotionList(taskId, initialData).filter( (promotion) => this.promotionIsEligible(taskId, promotion) ); let dashboardData = initialData; for (const promotion of promotions) { if (state.processedOfferIds.includes(promotion.offerId)) continue; task.processed++; const type = (promotion.promotionType ?? "").toLowerCase(); const isSearchOnBing = (promotion.name ?? "").toLowerCase().includes("exploreonbing"); if (type !== "urlreward") { state.processedOfferIds.push(promotion.offerId); task.message = `已跳过不支持的活动类型 ${type || "unknown"}`; await logger.warn(taskLabel(taskId), `${promotion.title || promotion.offerId}:不支持 ${type || "unknown"}`); await this.persist(state); continue; } const live = bootstrap.snapshot.offers.find((offer) => offer.offerId === promotion.offerId); const hash = live?.hash ?? promotion.hash ?? null; if (!hash || live?.isCompleted || live?.isLocked) { state.processedOfferIds.push(promotion.offerId); task.failed++; task.message = `${promotion.title || promotion.offerId} 缺少可用任务哈希`; await logger.warn(taskLabel(taskId), task.message); await this.persist(state); continue; } const report = await rewardsClient.reportActivity(bootstrap, { offerId: promotion.offerId, hash, activityType: live?.activityType ?? promotion.activityType ?? 11, isPromotional: live?.isPromotional }); if (isSearchOnBing && report.acknowledged) { const queries = shuffled(await rewardsClient.getActivityQueries(promotion)); if (!queries.length) { state.processedOfferIds.push(promotion.offerId); task.failed++; await logger.warn(taskLabel(taskId), `${promotion.title || promotion.offerId} 没有可用搜索词`); await this.persist(state); continue; } state.pendingPromotion = { taskId, offerId: promotion.offerId }; state.searchJob = { mode: "promotion", queries, index: 0, stagnant: 0, initialRemaining: 0, lastRemaining: 0, initialPoints: state.currentPoints ?? 0, offerId: promotion.offerId, offerTitle: promotion.title, parentTask: taskId, returnUrl: rewardsUrls.earn }; task.message = `正在完成搜索活动:${promotion.title || promotion.offerId}`; await logger.info(taskLabel(taskId), task.message); await this.navigateToCurrentSearch(state); return true; } await wait(randomInt(2e3, 4e3)); dashboardData = await rewardsClient.getDashboard(); const updated = findPromotion(dashboardData.dashboard, promotion.offerId); const succeeded = report.acknowledged || promotionComplete(updated); state.processedOfferIds.push(promotion.offerId); state.currentPoints = dashboardData.dashboard.userStatus.availablePoints; if (succeeded) { task.succeeded++; await logger.info( taskLabel(taskId), `已完成 ${promotion.title || promotion.offerId},HTTP ${report.status}` ); } else { task.failed++; await logger.warn(taskLabel(taskId), `${promotion.title || promotion.offerId} 未得到完成确认`); } await this.persist(state); await wait(randomInt(1500, 3e3)); } this.completeTask( task, promotions.length ? `处理 ${task.processed} 项,成功 ${task.succeeded} 项,失败 ${task.failed} 项` : "没有待处理项目", task.processed, task.succeeded, task.failed ); await this.persist(state); return false; } async finishPendingPromotion(state, data) { const pending = state.pendingPromotion; if (!pending) return; const task = state.taskResults[pending.taskId]; const promotion = findPromotion(data.dashboard, pending.offerId); if (promotionComplete(promotion)) { task.succeeded++; await logger.info(taskLabel(pending.taskId), `搜索活动已完成:${promotion?.title || pending.offerId}`); } else { task.failed++; await logger.warn(taskLabel(pending.taskId), `搜索活动未确认完成:${promotion?.title || pending.offerId}`); } if (!state.processedOfferIds.includes(pending.offerId)) state.processedOfferIds.push(pending.offerId); state.pendingPromotion = void 0; state.searchJob = void 0; state.currentPoints = data.dashboard.userStatus.availablePoints; await this.persist(state); } async runPunchcards(state, data, bootstrap) { const task = state.taskResults.punchcards; const apiCards = new Map( (data.dashboard.punchCards ?? []).filter((card) => card.parentPromotion?.offerId).map((card) => [card.parentPromotion.offerId, card]) ); const parents = new Map( bootstrap.snapshot.questParents.map((parent) => [parent.offerId, parent]) ); for (const [offerId, card] of apiCards) { if (!parents.has(offerId)) { parents.set(offerId, { offerId, title: card.parentPromotion?.title ?? "", pointProgressMax: Number(card.parentPromotion?.pointProgressMax ?? 0), complete: Boolean(card.parentPromotion?.complete) }); } } const incomplete = [...parents.values()].filter((parent) => { if (parent.complete) return false; return !this.config.skipZeroPointOffers || parent.pointProgressMax > 0; }); for (const parent of incomplete) { try { const response = await fetch(rewardsUrls.quest(parent.offerId), { credentials: "include", headers: { Accept: "text/html,application/xhtml+xml" } }); if (!response.ok) throw new Error(`Quest 页面返回 HTTP ${response.status}`); const children = parseQuestChildren(await response.text()); const apiChildren = new Map( (apiCards.get(parent.offerId)?.childPromotions ?? []).filter((child) => child.offerId).map((child) => [child.offerId, child]) ); for (const child of children) { if (!child.reportable || !child.hash) continue; const apiChild = apiChildren.get(child.offerId); if (this.isSearchQuotaChild(child.offerId, apiChild)) continue; if (this.isClaimChild(child.offerId, apiChild) && !this.config.autoClaimPunchcardRewards) { await logger.info("Punchcard", `${child.offerId} 已可领取,按设置保留为手动领取`); continue; } task.processed++; const report = await rewardsClient.reportActivity( bootstrap, { offerId: child.offerId, hash: child.hash, activityType: 11 }, { questId: parent.offerId } ); if (report.acknowledged) task.succeeded++; else task.failed++; await logger.info( "Punchcard", `${parent.title || parent.offerId} / ${child.offerId}:${report.acknowledged ? "已确认" : "未确认"}` ); await this.persist(state); await wait(randomInt(2e3, 4e3)); } } catch (error) { task.failed++; await logger.warn("Punchcard", `${parent.title || parent.offerId}:${String(error)}`); } } this.completeTask( task, incomplete.length ? `处理 ${incomplete.length} 个任务卡,子任务成功 ${task.succeeded},失败 ${task.failed}` : "没有待处理 Punchcard", task.processed, task.succeeded, task.failed ); await this.persist(state); } isSearchQuotaChild(offerId, promotion) { const type = (promotion?.promotionType ?? "").toLowerCase(); const attributeType = String(promotion?.attributes?.type ?? "").toLowerCase(); const progressMax = Number(promotion?.pointProgressMax ?? 0); return type === "search" || attributeType === "search" || progressMax > 1 || /search/i.test(offerId) && /(day|streak|\dx)/i.test(offerId); } isClaimChild(offerId, promotion) { const destination = (promotion?.destinationUrl ?? "").toLowerCase(); return /\/redeem\//.test(destination) || /(redeem|claim|(?= job.queries.length) { state.searchJob = void 0; state.status = "waiting-page"; await this.persist(state); location.assign(`${job.returnUrl}${controllerHash(state.controllerToken)}`); return; } await this.navigateToCurrentSearch(state); return; } const remaining = desktopSearchRemaining(data.dashboard); const gained = Math.max(0, job.lastRemaining - remaining); job.stagnant = gained > 0 ? 0 : job.stagnant + 1; job.lastRemaining = remaining; job.index++; const finished = remaining <= 0 || job.index >= job.queries.length || job.index >= this.config.maxDesktopSearches || job.stagnant >= this.config.maxStagnantSearches; await logger.info( "桌面搜索", `本次增加 ${gained} 分,剩余 ${remaining} 分,连续未增加 ${job.stagnant}/${this.config.maxStagnantSearches}` ); if (finished) { const task = state.taskResults["desktop-search"]; const performed = job.index; const earned = Math.max(0, job.initialRemaining - remaining); this.completeTask( task, remaining <= 0 ? `完成 ${performed} 次搜索,获得 ${earned} 分` : `搜索已停止:完成 ${performed} 次,获得 ${earned} 分,仍剩余 ${remaining} 分`, performed, earned > 0 ? performed : 0, remaining > 0 ? 1 : 0 ); state.searchJob = void 0; state.status = "waiting-page"; await this.persist(state); location.assign(`${job.returnUrl}${controllerHash(state.controllerToken)}`); return; } await this.persist(state); await this.navigateToCurrentSearch(state); } completeTask(task, message, processed, succeeded, failed) { task.status = "completed"; task.message = message; task.processed = processed; task.succeeded = succeeded; task.failed = failed; task.finishedAt = Date.now(); } async persist(state) { await saveRunState(state); emitRunState(state); } async failRun(state, error) { state.status = "failed"; state.error = error instanceof Error ? error.message : String(error); state.finishedAt = Date.now(); const taskId = state.currentTask; if (taskId) { const task = state.taskResults[taskId]; task.status = "failed"; task.message = state.error; task.failed++; task.finishedAt = Date.now(); } await this.persist(state); await logger.error("运行", state.error); await this.notifyRunOutcome(state); } async notifyRunOutcome(state) { if (!this.config.clawBotPushEnabled || !this.config.clawBotPushToken || state.notificationSentAt) { return; } try { const message = buildRunOutcomeMessage(state); await sendClawBotMessage(this.config.clawBotPushToken, message.title, message.content); state.notificationSentAt = Date.now(); await this.persist(state); await logger.info("微信 ClawBot", "本轮任务结果已通过 pushplus 发送"); } catch (error) { await logger.warn( "微信 ClawBot", `推送失败:${error instanceof Error ? error.message : String(error)}` ); } } }; // src/ui/panel.ts var TASK_LABELS2 = { dashboard: "积分与状态", "daily-set": "Daily Set", "more-promotions": "More Promotions", punchcards: "Punchcard", "desktop-search": "桌面搜索" }; var STATUS_LABELS2 = { pending: "等待", running: "执行中", completed: "完成", skipped: "关闭", failed: "失败", idle: "空闲", "waiting-page": "等待页面", stopped: "已停止" }; var ControlPanel = class { constructor(config, actions) { this.config = config; this.actions = actions; const existing = document.getElementById("mrsu-control-panel"); existing?.remove(); this.host = document.createElement("div"); this.host.id = "mrsu-control-panel"; this.shadow = this.host.attachShadow({ mode: "open" }); document.documentElement.append(this.host); } host; shadow; state = null; logs = []; expanded = true; settingsOpen = false; async mount() { ; [this.state, this.logs] = await Promise.all([loadRunState(), loadLogs()]); this.render(); onRunState((state) => { this.state = state; this.render(); }); onLog((entry) => { this.logs = [...this.logs, entry].slice(-200); this.render(); }); } setConfig(config) { this.config = config; this.render(); } render() { const state = this.state; const running = Boolean(state && ["running", "waiting-page"].includes(state.status)); const taskRows = state ? state.taskOrder.map((taskId) => { const result = state.taskResults[taskId]; return `
${TASK_LABELS2[taskId]} ${STATUS_LABELS2[result.status] ?? result.status} ${escapeHtml(result.message)}
`; }).join("") : '
尚未运行任务
'; const recentLogs = this.logs.slice(-20).reverse().map( (entry) => `
${escapeHtml(entry.scope)} ${escapeHtml(entry.message)}
` ).join(""); this.shadow.innerHTML = `
Rewards Auto Script ${escapeHtml("scriptcat-page")} · v${escapeHtml("1.3.0")}
状态:${state ? STATUS_LABELS2[state.status] ?? state.status : "空闲"} 积分:${state?.currentPoints ?? "—"}
${taskRows}
运行日志(${this.logs.length})
${recentLogs || '
暂无日志
'}
${this.settingsOpen ? this.settingsMarkup() : ""} `; this.bindEvents(); } settingsMarkup() { const checked = (value) => value ? "checked" : ""; return ` `; } bindEvents() { this.shadow.querySelector('[data-action="toggle"]')?.addEventListener("click", () => { this.expanded = !this.expanded; this.render(); }); this.shadow.querySelector('[data-action="start"]')?.addEventListener("click", () => { void this.actions.onStart(); }); this.shadow.querySelector('[data-action="stop"]')?.addEventListener("click", () => { void this.actions.onStop(); }); this.shadow.querySelector('[data-action="open-rewards"]')?.addEventListener("click", () => { this.actions.onOpenRewards(); }); this.shadow.querySelector('[data-action="settings"]')?.addEventListener("click", () => { this.settingsOpen = true; this.render(); }); this.shadow.querySelectorAll('[data-action="close-settings"]').forEach((button) => { button.addEventListener("click", () => { this.settingsOpen = false; this.render(); }); }); const form = this.shadow.querySelector("form.settings"); this.shadow.querySelector('[data-action="test-clawbot"]')?.addEventListener("click", () => { const tokenInput = form?.elements.namedItem("clawBotPushToken"); void this.actions.onTestClawBot(tokenInput?.value ?? ""); }); form?.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(form); const numberValue = (name, fallback, min, max) => { const value = Number(data.get(name)); return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.round(value))) : fallback; }; const minDelay = numberValue("searchDelayMinSeconds", 120, 120, 1800); const maxDelay = numberValue("searchDelayMaxSeconds", 180, minDelay, 1800); const next = { ...this.config, enabledTasks: { dashboard: data.has("dashboard"), "daily-set": data.has("daily-set"), "more-promotions": data.has("more-promotions"), punchcards: data.has("punchcards"), "desktop-search": data.has("desktop-search") }, searchQuerySource: String(data.get("searchQuerySource") || "upstream"), customSearchQueries: String(data.get("customSearchQueries") || ""), searchDelayMinSeconds: Math.min(minDelay, maxDelay), searchDelayMaxSeconds: Math.max(minDelay, maxDelay), maxDesktopSearches: numberValue("maxDesktopSearches", 50, 1, 100), maxStagnantSearches: numberValue("maxStagnantSearches", 10, 1, 30), skipZeroPointOffers: data.has("skipZeroPointOffers"), autoClaimPunchcardRewards: data.has("autoClaimPunchcardRewards"), clawBotPushEnabled: data.has("clawBotPushEnabled"), clawBotPushToken: String(data.get("clawBotPushToken") || "").trim(), scheduleEnabled: data.has("scheduleEnabled"), scheduleTime: String(data.get("scheduleTime") || "09:15") }; void this.actions.onSaveConfig(next).then(() => { this.settingsOpen = false; this.render(); }); }); } }; function escapeHtml(value) { return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); } var PANEL_CSS = ` :host { all: initial; } * { box-sizing: border-box; } .panel { position: fixed; right: 18px; bottom: 18px; z-index: 2147483646; width: 370px; color: #e8e8e8; background: #1e1e1e; border: 1px solid #3a3a3a; border-radius: 12px; overflow: hidden; box-shadow: 0 18px 60px rgba(0,0,0,.42); font: 13px/1.45 "Segoe UI", "Microsoft YaHei", sans-serif; } .panel.collapsed { width: 280px; } .panel.collapsed .body { display: none; } header { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; background: #272727; } header div { display: flex; flex-direction: column; gap: 2px; } header strong { font-size: 14px; } header small { color: #9d9d9d; font-size: 11px; } .icon { width: 28px; min-width: 28px; padding: 3px; font-size: 18px; } .body { padding: 12px; } .summary { display: flex; justify-content: space-between; margin-bottom: 8px; } .tasks { display: grid; gap: 6px; } .task-row { display: grid; grid-template-columns: 108px 52px 1fr; align-items: center; gap: 7px; } .task-row small { color: #aaa; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .badge { text-align: center; border-radius: 10px; padding: 1px 6px; background: #3b3b3b; font-size: 11px; } .badge.running { color: #b9ddff; background: #173c5d; } .badge.completed { color: #baf1ca; background: #17472a; } .badge.failed { color: #ffb8b8; background: #571e1e; } .badge.skipped { color: #bbb; } .actions { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; } button { appearance: none; border: 1px solid #4a4a4a; border-radius: 6px; padding: 6px 10px; color: #eee; background: #303030; cursor: pointer; font: inherit; } button:hover { background: #3a3a3a; } button:disabled { opacity: .45; cursor: not-allowed; } button.primary { color: white; background: #1769aa; border-color: #2a7bbb; } details { margin-top: 10px; } summary { cursor: pointer; color: #bbb; } .logs { max-height: 210px; overflow: auto; margin-top: 7px; border-top: 1px solid #333; } .log { display: grid; grid-template-columns: 64px 72px 1fr; gap: 5px; padding: 5px 0; border-bottom: 1px solid #2c2c2c; font-size: 11px; } .log time { color: #858585; } .log b { overflow: hidden; text-overflow: ellipsis; } .log.warn b, .log.warn span { color: #ffd79a; } .log.error b, .log.error span { color: #ffaaaa; } .empty { color: #888; padding: 8px 0; } .modal-backdrop { position: fixed; inset: 0; z-index: 2147483647; display: grid; place-items: center; background: rgba(0,0,0,.58); font: 13px/1.45 "Segoe UI", "Microsoft YaHei", sans-serif; } .settings { width: min(520px, calc(100vw - 28px)); max-height: calc(100vh - 28px); overflow: auto; padding: 0 16px 16px; color: #eee; background: #1e1e1e; border: 1px solid #444; border-radius: 12px; box-shadow: 0 18px 70px rgba(0,0,0,.5); } .settings header { margin: 0 -16px 14px; border-radius: 12px 12px 0 0; } .settings label { display: grid; gap: 5px; margin: 9px 0; } .settings label.check { display: flex; align-items: center; gap: 8px; } .settings input[type="number"], .settings input[type="time"], .settings input[type="password"], .settings select, .settings textarea { width: 100%; padding: 7px; color: #eee; background: #292929; border: 1px solid #4a4a4a; border-radius: 6px; } .settings textarea { resize: vertical; min-height: 76px; font: inherit; } .settings input[type="checkbox"] { width: 16px; height: 16px; } .settings .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 12px; } .settings p { color: #aaa; font-size: 11px; } .settings a { color: #7cc4ff; text-decoration: none; } .settings a:hover { text-decoration: underline; } .settings hr { border: 0; border-top: 1px solid #383838; margin: 14px 0; } @media (max-width: 520px) { .panel { right: 8px; bottom: 8px; width: calc(100vw - 16px); } .settings .grid { grid-template-columns: 1fr; } } `; // src/entries/page-main.ts async function runPageEntry() { let config = await loadConfig(); let engine = new RunEngine(config); const panel = new ControlPanel(config, { onStart: async () => { if (location.origin !== rewardsUrls.origin) { await gmNotify("Rewards Auto Script", "请先打开 Rewards 页面,再从控制面板开始任务"); await gmOpenTab(rewardsUrls.earn, true); return; } const activeState = await loadRunState(); if (activeState && activeState.dateKey === localDateKey() && ["running", "waiting-page"].includes(activeState.status)) { await gmNotify("Rewards Auto Script", "已有一轮任务正在运行,请先停止或回到原任务页面"); return; } config = await loadConfig(); engine = new RunEngine(config); await engine.start("manual"); }, onStop: async () => { await engine.stop(); }, onSaveConfig: async (next) => { config = normalizeConfig(next); await saveConfig(config); engine = new RunEngine(config); panel.setConfig(config); await logger.info("设置", "设置已保存"); }, onTestClawBot: async (token) => { try { await sendClawBotMessage( token, "Rewards Auto Script - 测试通知", `微信 ClawBot 推送配置正常。 测试时间:${(/* @__PURE__ */ new Date()).toLocaleString()}` ); await gmNotify("Rewards Auto Script", "微信 ClawBot 测试消息已发送"); await logger.info("微信 ClawBot", "测试消息已通过 pushplus 发送"); } catch (error) { const message = error instanceof Error ? error.message : String(error); await gmNotify("Rewards Auto Script", `微信 ClawBot 测试失败:${message}`); await logger.error("微信 ClawBot", `测试推送失败:${message}`); } }, onOpenRewards: () => { void gmOpenTab(rewardsUrls.earn, true); } }); await panel.mount(); gmRegisterMenu("开始 Microsoft Rewards 页面任务", () => { if (location.origin !== rewardsUrls.origin) { void gmOpenTab(rewardsUrls.earn, true); return; } void (async () => { const activeState = await loadRunState(); if (activeState && activeState.dateKey === localDateKey() && ["running", "waiting-page"].includes(activeState.status)) { await gmNotify("Rewards Auto Script", "已有一轮任务正在运行"); return; } config = await loadConfig(); engine = new RunEngine(config); await engine.start("manual"); })(); }); gmRegisterMenu("停止当前 Rewards 任务", () => void engine.stop()); gmRegisterMenu("打开 Microsoft Rewards", () => void gmOpenTab(rewardsUrls.earn, true)); const request = await loadRunRequest(); if (request && Date.now() - request.requestedAt < 30 * 60 * 1e3 && location.origin === rewardsUrls.origin && location.hash.includes(`mrs-schedule=${request.token}`)) { await clearRunRequest(); await engine.start("scriptcat-schedule", request.token); return; } const state = await loadRunState(); if (state && ["running", "waiting-page"].includes(state.status)) { const expectedHash = controllerHash(state.controllerToken).slice(1); if (location.hash.includes(expectedHash)) await engine.resume(state); } } // src/entries/scriptcat-page.user.ts void runPageEntry(); })();