// ==UserScript== // @name Microsoft Bing Rewards Daily Task Script (微软必应奖励每日任务脚本) // @version 26.10.31.1 // @description Brian 自动完成微软必应每日搜索任务,智能积累奖励积分。支持实时进度追踪、热搜关键词、随机行为模拟,安全高效获取 Bing Rewards 积分。 // @author Brian // @match https://*.bing.com/* // @match https://login.live.com/oauth20_desktop.srf* // @license MIT // @icon https://www.bing.com/favicon.ico // @connect top.baidu.com // @connect www.toutiao.com // @connect r.inews.qq.com // @connect m.weibo.cn // @connect login.live.com // @connect prod.rewardsplatform.microsoft.com // @run-at document-end // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_log // @grant GM_openInTab // @grant GM_saveTab // @grant GM_closeTab // @grant GM_deleteValue // ==/UserScript== 'use strict'; // 配置参数 // 用户可配置参数表:参数名 → { key: GM 存储键, default: 默认值 } // CONFIG 的 getter/setter、设置页读取、配置导出/导入均以此表为唯一定义处 const CONFIG_SCHEMA = { // 搜索form参数,⚠️ 手动进入https://cn.bing.com,确保登录后执行几次搜索,根据实际地址栏的form=xxx修改 searchFormParam: { key: 'customSearchFormParam', default: 'QBLH' }, // 面板默认是否收缩 (true=收缩, false=展开) panelDefaultCollapsed: { key: 'customPanelDefaultCollapsed', default: false }, // 最大搜索次数 maxSearches: { key: 'customMaxSearches', default: 20 }, // 是否随机加词,如:人工智能发展 --> 人工1智能发z展 randomAddSearchWords: { key: 'customRandomAddSearchWords', default: false }, // 随机加词因子,控制加词的概率(0-1之间的小数),默认为0.3即30%概率添加字符 randomAddSearchWordsFactor: { key: 'customRandomAddSearchWordsFactor', default: 0.3 }, // 是否随机截词,如:人工1智能发z展 --> 人工1智 randomCutSearchWords: { key: 'customRandomCutSearchWords', default: false }, // 随机截词因子,控制截取的概率(0-1之间的小数),默认为0.2即20%概率截取字符 randomCutSearchWordsFactor: { key: 'customRandomCutSearchWordsFactor', default: 0.2 }, // 是否点击搜索结果链接 clickSearchResults: { key: 'customClickSearchResults', default: false }, // 暂停间隔范围:每执行多少次搜索后暂停一次的区间 pauseIntervalMin: { key: 'customPauseIntervalMin', default: 2 }, pauseIntervalMax: { key: 'customPauseIntervalMax', default: 3 }, // 暂停时间范围(毫秒):每次暂停的持续时间区间 pauseTimeMin: { key: 'customPauseTimeMin', default: 20 * 60 * 1000 }, pauseTimeMax: { key: 'customPauseTimeMax', default: 30 * 60 * 1000 }, // 搜索延迟范围(毫秒):两次搜索之间的随机延迟区间 minDelay: { key: 'customMinDelay', default: 15 * 1000 }, maxDelay: { key: 'customMaxDelay', default: 30 * 1000 }, // 任务点击相关配置(日常任务 + 每日活动共用) tasksScrollDelay: { key: 'customTasksScrollDelay', default: 3000 }, // 卡片点击间隔(毫秒):卡片滚动定位后的等待时间与两次点击之间的间隔,过短会导致服务端未记录活动 tasksClickDelay: { key: 'customTasksClickDelay', default: 2000 }, tasksMaxRetries: { key: 'customTasksMaxRetries', default: 0 }, tasksRetryDelay: { key: 'customTasksRetryDelay', default: 2000 }, tasksCloseTabDelay: { key: 'customTasksCloseTabDelay', default: 1500 }, // 自动点击任务总开关(earn 日常任务 + dashboard 每日活动区域未完成任务共用,默认关闭) autoClickTasks: { key: 'customAutoClickTasks', default: false }, // APP 端每日签到开关 appCheckInEnabled: { key: 'customAppCheckInEnabled', default: false }, // APP 端资讯阅读开关 appReadEnabled: { key: 'customAppReadEnabled', default: false }, // APP 端资讯阅读每日上报上限(篇) appReadDailyLimit: { key: 'customAppReadDailyLimit', default: 10 }, // APP 端设备标识预设(可选值见 APP_CLIENT_PRESETS,默认第一项) appUaPreset: { key: 'customAppUaPreset', default: 'android-16-xiaomi15' } }; const CONFIG = { // ==================== 脚本基础信息 ==================== // 版本号(动态从GM_info获取) get version() { return GM_info?.script?.version || '1.0.0'; }, // ==================== 内部固定参数 (不建议修改) ==================== // 网络请求超时时间(毫秒):热词抓取与 APP 端接口共用的最大等待时间 requestTimeout: 20 * 1000, // 任务页「脚本已注入但超时未完成」的连续次数阈值,达到后当天不再跳转该页面 // (脚本未注入则首次即放弃,不受此阈值影响) taskFlowIncompleteLimit: 3, // 任务标签页停留时长(毫秒):点击任务卡片后保持标签页打开,等目标页上报活动后再关闭 taskTabDwell: 6000, // 主页面等待任务页处理完成的超时(毫秒):需覆盖多张卡片的点击间隔、停留时长与回读校验 taskFlowTimeout: 180 * 1000, // 启动参数标记数组 startParams: ['bingTask', 'runSearch', 'initiateSearch', 'bingSearchMode', 'autoSearch', 'startTask', 'executeSearch', 'launchSearch', 'beginSearch', 'processSearch', 'bingQuest', 'dailyTask', 'searchFlow', 'rewardsTask', 'bingBrowse', 'autoFlow'] }; // 依据参数表为 CONFIG 生成与 GM 存储一一绑定的 getter/setter(键名前缀 custom*) Object.entries(CONFIG_SCHEMA).forEach(([name, def]) => { Object.defineProperty(CONFIG, name, { enumerable: true, get() { return GM_getValue(def.key, def.default); }, set(value) { GM_setValue(def.key, value); } }); }); // 状态管理 const state = { searchWords: [], statusPanel: null, timers: new Set(), isRunning: false, countdownStartTime: 0, countdownDuration: 0, // 面板数据单一数据源(当前搜索词 / 暂停剩余秒),显示内容统一由 derivePanelStatus 派生 panel: { currentWord: '', pauseTimeLeft: null }, isPanelCollapsed: false, // 任务点击相关状态(earn 日常任务 / dashboard 每日活动共用流程) taskFlows: { earn: { clicked: new Set(), retryCount: 0, processing: false }, dashboard: { clicked: new Set(), retryCount: 0, processing: false } }, // 面板渲染状态缓存(避免每秒整块重排) panelSignature: '', panelPageVisible: null, // APP 端任务相关状态(签到 + 资讯阅读) appToken: '', appTasks: { checkInDone: false, checkInPoints: 0, readDone: false, readCurrent: 0, readTotal: 0, authRequired: false, // 当日阅读进度是否已从服务端同步(页面生命周期内,避免重复查询) readProgressSynced: false, // 随机阅读是否正在执行(面板状态提示) readRunning: false } }; // ==================== APP 端任务模块(每日签到 + 资讯阅读) ==================== // APP 端协议常量(Rewards Platform 移动端接口契约) const REWARDS_APP_SPEC = { endpoints: { activityReport: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities', // 阅读进度查询基址,实际请求按当前设备标识的 channel 拼接(见 getAccountProfileUrl) accountProfile: 'https://prod.rewardsplatform.microsoft.com/dapi/me', tokenIssue: 'https://login.live.com/oauth20_token.srf', authorizePage: 'https://login.live.com/oauth20_authorize.srf?client_id=0000000040170455&response_type=code&scope=service::prod.rewardsplatform.microsoft.com::MBI_SSL&redirect_uri=https://login.live.com/oauth20_desktop.srf' }, // 活动上报类型码:103=每日签到,101=资讯阅读 activities: { checkIn: 103, readArticle: 101 }, offers: { readArticle: 'ENUS_readarticle3_30points' }, // 上报区域(国区固定) region: 'cn', // 令牌超过该天数后预防性续期 tokenMaxAgeDays: 7 }; // APP 端设备标识预设表:channel 决定请求通道,version 同时决定 x-rewards-appid 与 UA 尾部构建号 // 维护约定:channel / version / userAgent 尾部构建号三者必须一致 const APP_CLIENT_PRESETS = [ { id: 'android-16-xiaomi15', label: 'Android 16 · Xiaomi 15 Pro', note: '默认,与旧版本行为一致', channel: 'SAAndroid', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (Linux; Android 16; Xiaomi 15 Pro Build/BP1A.250605.012; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Mobile Safari/537.36 BingSapphire/32.6.2110003560' }, { id: 'android-15-pixel9', label: 'Android 15 · Pixel 9 Pro', note: '谷歌原生机型,系统版本略低', channel: 'SAAndroid', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro Build/AP4A.250105.002; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Mobile Safari/537.36 BingSapphire/32.6.2110003560' }, { id: 'android-15-xiaomi14', label: 'Android 15 · Xiaomi 14', note: '国产主流机型,与默认同通道', channel: 'SAAndroid', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (Linux; Android 15; Xiaomi 14 Build/BP1A.250505.005; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Mobile Safari/537.36 BingSapphire/32.6.2110003560' }, { id: 'android-14-huawei-mate60', label: 'Android 14 · HUAWEI Mate 60 Pro', note: '华为机型,国产 ROM 备用', channel: 'SAAndroid', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (Linux; Android 14; ALN-AL00 Build/HUAWEIALN-AL00; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Mobile Safari/537.36 BingSapphire/32.6.2110003560' }, { id: 'android-14-oppo-findx7', label: 'Android 14 · OPPO Find X7 Ultra', note: 'OPPO 旗舰机型,国产 ROM 备用', channel: 'SAAndroid', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (Linux; Android 14; PHY110 Build/UKQ1.231003.002; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Mobile Safari/537.36 BingSapphire/32.6.2110003560' }, { id: 'android-14-galaxy-s24', label: 'Android 14 · Galaxy S24', note: '三星机型,系统版本较低', channel: 'SAAndroid', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (Linux; Android 14; SM-S9210 Build/UP1A.231005.007; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Mobile Safari/537.36 BingSapphire/32.6.2110003560' }, { id: 'android-15-galaxy-s25', label: 'Android 15 · Galaxy S25', note: '三星新款旗舰,系统版本较高', channel: 'SAAndroid', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (Linux; Android 15; SM-S9310 Build/AP3A.250105.008; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Mobile Safari/537.36 BingSapphire/32.6.2110003560' }, { id: 'ios-18-iphone16', label: 'iOS 18 · iPhone 16 Pro', note: 'iOS 通道(SAIOS),个别账号可能不适用', channel: 'SAIOS', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 BingSapphire/32.6.2110003560' }, { id: 'ios-18-iphone15pro', label: 'iOS 18 · iPhone 15 Pro', note: 'iOS 通道备用,系统版本稍低', channel: 'SAIOS', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1 BingSapphire/32.6.2110003560' }, { id: 'ios-17-iphone14pro', label: 'iOS 17 · iPhone 14 Pro', note: 'iOS 通道低版本备用,上报异常时尝试', channel: 'SAIOS', version: '32.6.2110003560', userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.7 Mobile/15E148 Safari/604.1 BingSapphire/32.6.2110003560' } ]; // 默认预设(与 CONFIG_SCHEMA.appUaPreset 的默认值保持一致) const APP_CLIENT_DEFAULT_PRESET = APP_CLIENT_PRESETS[0].id; /** * 读取当前生效的 APP 端设备标识(配置值非法时回退默认预设) * @returns {{presetId:string,label:string,channel:string,appId:string,userAgent:string}} */ function getAppClient() { const preset = APP_CLIENT_PRESETS.find(item => item.id === CONFIG.appUaPreset) || APP_CLIENT_PRESETS[0]; return { presetId: preset.id, label: preset.label, channel: preset.channel, appId: `${preset.channel}/${preset.version}`, userAgent: preset.userAgent }; } // 阅读进度查询地址(channel 随设备标识切换,options 为移动端固定参数) function getAccountProfileUrl() { return `${REWARDS_APP_SPEC.endpoints.accountProfile}?channel=${getAppClient().channel}&options=613`; } /** * GM_xmlhttpRequest 的统一 Promise 封装(APP 端接口与热词抓取共用) * 非 2xx 响应抛出携带状态码的错误,供 APP 层识别 401 等场景 * @param {{method?:string,url:string,headers?:object,data?:string,swallowError?:boolean}} options * swallowError=true 时请求失败 resolve 为空字符串(热词抓取语义:单源失败不中断整体) * @returns {Promise} */ function request(options) { const swallowError = Boolean(options.swallowError); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url: options.url, headers: options.headers || {}, data: options.data, timeout: CONFIG.requestTimeout, onload: res => { if (res.status >= 200 && res.status < 300) { resolve(res.responseText); } else if (swallowError) { resolve(''); } else { reject(new Error(`HTTP ${res.status}`)); } }, onerror: () => (swallowError ? resolve('') : reject(new Error('网络请求失败'))), ontimeout: () => (swallowError ? resolve('') : reject(new Error('请求超时'))) }); }); } /** * APP 端授权管理:授权码捕获 → 令牌兑换/刷新 → 续期 → 401 自动重试 */ const AppAuth = { // 是否为授权落地页(login.live.com 授权跳转后的回调页) isAuthLandingPage() { return location.hostname === 'login.live.com' && location.pathname === '/oauth20_desktop.srf'; }, // 从 URL 中提取授权码 extractAuthCode(url) { try { return new URL(url).searchParams.get('code') || ''; } catch { return ''; } }, /** * 授权落地页处理:仅捕获授权码并落盘,随后通知并关页 * 令牌兑换不在此处执行(页面即将关闭,请求易被中断), * 由设置页轮询或任务执行时的 ensureToken 在长生命周期上下文中完成 */ async handleAuthLanding() { const code = this.extractAuthCode(location.href); if (!code) return; GM_setValue('appPendingAuthCode', code); GM_setValue('appAuthLastError', ''); GM_log('APP授权:授权码已捕获,待主页面兑换令牌'); try { GM_notification({ title: 'APP任务授权', text: '授权码已捕获,即将关闭此页面', timeout: 3000 }); } catch {} setTimeout(() => { try { window.close(); } catch {} }, 500); }, // 打开授权页(由设置页按钮/菜单触发,复用浏览器真实登录态) openAuthorizePage() { GM_openInTab(REWARDS_APP_SPEC.endpoints.authorizePage, { active: true }); }, /** * 令牌兑换/刷新(GET 查询串形式,MSA 端点契约要求必传 client_id) * @param grantType 'authorization_code'(授权码兑换)或 'REFRESH_TOKEN'(刷新令牌续期) * @param credential 授权码或刷新令牌 */ async exchangeToken(grantType, credential) { const params = new URLSearchParams(); params.set('client_id', '0000000040170455'); if (grantType === 'authorization_code') { params.set('grant_type', 'authorization_code'); params.set('code', credential); params.set('redirect_uri', 'https://login.live.com/oauth20_desktop.srf'); } else { params.set('grant_type', 'REFRESH_TOKEN'); params.set('refresh_token', credential); params.set('scope', 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'); } try { const res = await request({ url: `${REWARDS_APP_SPEC.endpoints.tokenIssue}?${params.toString()}` }); const data = utils.safeJsonParse(res, null); if (!data) { GM_setValue('appAuthLastError', '令牌响应非JSON'); return false; } if (data.error) { const reason = `${data.error}${data.error_description ? ' - ' + data.error_description : ''}`; GM_setValue('appAuthLastError', reason); GM_log(`APP任务令牌错误: ${reason}`); if (data.error === 'invalid_grant' || data.error === 'invalid_request') { this.clearCredentials(); } return false; } if (data.access_token && data.refresh_token) { GM_setValue('appRefreshToken', data.refresh_token); GM_setValue('appAccessToken', data.access_token); GM_setValue('appTokenIssuedAt', Date.now()); GM_setValue('appAuthLastError', ''); state.appToken = data.access_token; state.appTasks.authRequired = false; GM_log(`APP授权:令牌已获取(${grantType === 'authorization_code' ? '授权码兑换' : '刷新续期'})`); return true; } GM_setValue('appAuthLastError', '令牌响应缺少字段'); return false; } catch (e) { GM_setValue('appAuthLastError', `请求失败: ${e.message}`); GM_log(`APP任务令牌请求失败: ${e.message}`); if (e.message.includes('400') || e.message.includes('401')) { this.clearCredentials(); } return false; } }, // 清空本地令牌凭据 clearCredentials() { GM_setValue('appRefreshToken', ''); GM_setValue('appAccessToken', ''); GM_setValue('appTokenIssuedAt', 0); GM_setValue('appPendingAuthCode', ''); state.appToken = ''; }, // 当前刷新令牌的持有天数(无记录时视为无穷大) tokenAgeDays() { const issuedAt = GM_getValue('appTokenIssuedAt', 0); return issuedAt > 0 ? (Date.now() - issuedAt) / (24 * 60 * 60 * 1000) : Infinity; }, // 令牌签发时长文案(授权兑换与自动续期都会刷新签发时间,按自然日分级描述) // 无签发记录时返回空串,由调用方省略括号,避免出现 1970 基准的异常天数 issueAgeText() { const issuedAt = GM_getValue('appTokenIssuedAt', 0); if (!(issuedAt > 0)) return ''; const days = utils.getCalendarDayDiff(issuedAt, Date.now()); if (days <= 0) return '今天'; if (days === 1) return '昨天'; return `${days}天前`; }, /** * 确保内存中存在有效令牌: * 补兑换暂存授权码 → 校验有效期(超7天续期)→ 刷新令牌续期 * 全部失败时标记待授权并返回 false */ async ensureToken() { // 补兑换:落地页兑换失败时暂存的授权码 const pendingCode = GM_getValue('appPendingAuthCode', ''); if (!state.appToken && pendingCode) { GM_setValue('appPendingAuthCode', ''); if (await this.exchangeToken('authorization_code', pendingCode)) return true; } // 优先恢复本地缓存的访问令牌(页面跳转后内存令牌丢失,避免每次搜索页都刷新令牌) if (!state.appToken) { const cachedToken = GM_getValue('appAccessToken', ''); if (cachedToken && this.tokenAgeDays() <= REWARDS_APP_SPEC.tokenMaxAgeDays) { state.appToken = cachedToken; } } if (state.appToken) { if (this.tokenAgeDays() > REWARDS_APP_SPEC.tokenMaxAgeDays) { GM_log('APP任务令牌已超7天,提前续期'); state.appToken = ''; } else { return true; } } const refreshToken = GM_getValue('appRefreshToken', ''); if (refreshToken && await this.exchangeToken('REFRESH_TOKEN', refreshToken)) { return true; } state.appTasks.authRequired = true; return false; }, /** * 请求包装:401 时清空令牌、重新刷新并原请求重试一次 * 刷新失败返回 null(标记待授权),其他错误向上抛出 */ async withAuth(requestFn) { if (!state.appToken) return null; try { return await requestFn(state.appToken); } catch (e) { if (e.message && e.message.includes('401')) { GM_log('APP任务令牌过期,尝试刷新后重试'); state.appToken = ''; GM_setValue('appAccessToken', ''); GM_setValue('appTokenIssuedAt', 0); const refreshToken = GM_getValue('appRefreshToken', ''); if (refreshToken && await this.exchangeToken('REFRESH_TOKEN', refreshToken)) { return await requestFn(state.appToken); } state.appTasks.authRequired = true; return null; } throw e; } } }; /** * APP 端业务接口:签到上报、阅读上报、阅读进度查询 */ const AppApi = { // 组装移动端公共请求头(设备标识取当前选中的预设) buildHeaders(extra) { const client = getAppClient(); return Object.assign({ 'content-type': 'application/json; charset=UTF-8', 'user-agent': client.userAgent, 'x-rewards-appid': client.appId, 'x-rewards-ismobile': 'true', 'x-rewards-country': REWARDS_APP_SPEC.region, 'x-rewards-language': 'zh' }, extra || {}); }, // 生成64位hex随机活动ID(模拟移动端活动上报格式) generateActivityId() { if (window.crypto && crypto.getRandomValues) { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); } let id = ''; for (let i = 0; i < 64; i++) id += Math.floor(Math.random() * 16).toString(16); return id; }, // 解析活动上报响应(积分/重复标记/余额) parseActivityResponse(res) { const data = utils.safeJsonParse(res, null); if (!data || !data.response) return null; return { points: Number(data.response.activity?.p || 0), duplicate: Boolean(data.response.isDuplicate), balance: Number(data.response.balance || 0) }; }, // 解析阅读进度(从账户画像的 promotions 中匹配阅读活动) parseReadProgress(res) { const data = utils.safeJsonParse(res, null); const promos = data?.response?.promotions || []; const task = promos.find(p => p.attributes?.offerid === REWARDS_APP_SPEC.offers.readArticle); if (task && task.attributes) { return { current: parseInt(task.attributes.progress) || 0, total: parseInt(task.attributes.max) || 30 }; } return null; }, /** * 每日签到上报(type=103,无 offerid) * 返回 {points, duplicate};isDuplicate/空 activity 视为当日已签(幂等成功) */ async reportCheckIn() { const region = REWARDS_APP_SPEC.region; const channel = getAppClient().channel; try { const res = await AppAuth.withAuth(token => request({ method: 'POST', url: REWARDS_APP_SPEC.endpoints.activityReport, headers: this.buildHeaders({ authorization: `Bearer ${token}`, 'x-rewards-partnerid': 'startapp', 'x-rewards-flights': 'rwgobig' }), data: JSON.stringify({ amount: 1, id: this.generateActivityId(), type: REWARDS_APP_SPEC.activities.checkIn, country: region, channel }) })); if (res === null) return null; const result = this.parseActivityResponse(res); if (result) { // 有积分为成功;无积分(含 isDuplicate/空 activity)按当日已签的幂等成功处理 return { points: result.points, duplicate: result.duplicate || result.points === 0 }; } return null; } catch (e) { GM_log(`APP签到请求失败: ${e.message}`); return null; } }, /** * 单篇资讯阅读上报(type=101,attributes 携带阅读活动标识) * 返回 {points, duplicate};失败返回 null */ async reportArticleRead() { const region = REWARDS_APP_SPEC.region; const channel = getAppClient().channel; try { const res = await AppAuth.withAuth(token => request({ method: 'POST', url: REWARDS_APP_SPEC.endpoints.activityReport, headers: this.buildHeaders({ authorization: `Bearer ${token}` }), data: JSON.stringify({ amount: 1, id: this.generateActivityId(), type: REWARDS_APP_SPEC.activities.readArticle, country: region, channel, attributes: { offerid: REWARDS_APP_SPEC.offers.readArticle } }) })); if (res === null) return null; const result = this.parseActivityResponse(res); if (result && (result.points > 0 || result.duplicate)) { return result; } return null; } catch (e) { GM_log(`APP阅读请求失败: ${e.message}`); return null; } }, // 查询阅读进度(当前/上限) async queryReadProgress() { try { const res = await AppAuth.withAuth(token => request({ url: getAccountProfileUrl(), headers: this.buildHeaders({ authorization: `Bearer ${token}` }) })); if (res === null) return null; return this.parseReadProgress(res); } catch (e) { GM_log(`APP阅读进度查询失败: ${e.message}`); return null; } } }; /** * APP 端任务编排:签到流程、阅读流程、日期戳幂等、重试计数 */ const AppTaskRunner = { // 指定日期戳键是否记录为今天(APP 任务完成态的统一定义) isDoneToday(storageKey) { return GM_getValue(storageKey, 0) === utils.getTodayNum(); }, /** * 读取当日签到结果({done, points}) * 日期与积分合并为单键 {date, points},同写同读,杜绝「日期是今天、积分是昨天」的错配; * 旧版分离键(appCheckInDate/appCheckInPoints)仅用于升级后的首次迁移读取 */ getCheckInResult() { const today = utils.getTodayNum(); const cached = GM_getValue('appCheckInResult', null); if (cached && typeof cached === 'object' && Number(cached.date) === today) { return { done: true, points: Number(cached.points) || 0 }; } if (GM_getValue('appCheckInDate', 0) === today) { return { done: true, points: Number(GM_getValue('appCheckInPoints', 0)) || 0 }; } return { done: false, points: 0 }; }, /** * 原子写入当日签到结果,并清理旧版分离键 * @param {number} points 当日签到已获得的积分 */ setCheckInResult(points) { GM_setValue('appCheckInResult', { date: utils.getTodayNum(), points: Number(points) || 0 }); GM_deleteValue('appCheckInDate'); GM_deleteValue('appCheckInPoints'); }, // 同步当日签到完成状态到内存(供面板渲染) syncCheckInState() { const result = this.getCheckInResult(); state.appTasks.checkInDone = result.done; // 未完成/跨日时一并复位积分,避免面板读到残留旧值 state.appTasks.checkInPoints = result.points; }, getRetryCounters() { return GM_getValue('appTaskRetryCounters', {}); }, // 重试计数跨日清零(保证每日重试额度与完成兜底可靠生效) resetRetryCountersIfNewDay() { const today = utils.getTodayNum(); if (GM_getValue('appTaskRetryDate', 0) !== today) { GM_setValue('appTaskRetryCounters', {}); GM_setValue('appTaskRetryDate', today); } }, bumpRetry(taskName) { const counters = this.getRetryCounters(); counters[taskName] = (counters[taskName] || 0) + 1; GM_setValue('appTaskRetryCounters', counters); }, // 任务重试次数是否未用尽(每任务每日最多2次) retryLeft(taskName) { return (this.getRetryCounters()[taskName] || 0) < 2; }, // APP 任务是否已全部完成(开关关闭的任务视为完成) isAllDone() { const checkInDone = !CONFIG.appCheckInEnabled || this.getCheckInResult().done; const readDone = !CONFIG.appReadEnabled || this.isDoneToday('appReadDate'); return checkInDone && readDone; }, // 当日 APP 阅读已上报篇数(跨页面跳转持久化,受每日上限约束) getReadReportedToday() { return this.isDoneToday('appReadReportedDate') ? GM_getValue('appReadReportedCount', 0) : 0; }, bumpReadReported() { if (!this.isDoneToday('appReadReportedDate')) { GM_setValue('appReadReportedDate', utils.getTodayNum()); GM_setValue('appReadReportedCount', 0); } GM_setValue('appReadReportedCount', GM_getValue('appReadReportedCount', 0) + 1); }, /** * APP 签到流程入口(搜索开始前执行):授权校验 → 签到 → 更新面板 * 资讯阅读不在此处执行,改为每次搜索前随机穿插上报(见 runRandomReads) */ async runCheckInFlow() { if (!CONFIG.appCheckInEnabled) return; this.syncCheckInState(); this.resetRetryCountersIfNewDay(); createStatusPanel(); if (!(await AppAuth.ensureToken())) { GM_log('APP任务待授权,跳过执行(请在设置中点击「开始APP授权」)'); updateStatusPanel(); return; } try { await this.runCheckIn(); } catch (e) { GM_log(`APP签到异常: ${e.message}`); } updateStatusPanel(); }, /** * APP 端任务总入口(搜索完成后的兜底补跑):授权校验 → 签到 → 资讯阅读 * 各任务独立 try/catch 隔离,互不影响也不阻断搜索主任务 */ async runAll() { if (!CONFIG.appCheckInEnabled && !CONFIG.appReadEnabled) return; this.syncCheckInState(); this.resetRetryCountersIfNewDay(); createStatusPanel(); if (!(await AppAuth.ensureToken())) { GM_log('APP任务待授权,跳过执行(请在设置中点击「开始APP授权」)'); updateStatusPanel(); return; } try { await this.runCheckIn(); } catch (e) { GM_log(`APP签到异常: ${e.message}`); } try { await this.runArticleRead(); } catch (e) { GM_log(`APP阅读异常: ${e.message}`); } updateStatusPanel(); }, // 每日签到流程:幂等判断 → 上报 → 结果落盘 async runCheckIn() { if (!CONFIG.appCheckInEnabled) return; if (state.appTasks.checkInDone) { GM_log(`APP签到今日已完成(+${state.appTasks.checkInPoints}积分)`); return; } if (!this.retryLeft('checkIn')) { GM_log('APP签到重试次数已用尽,今日不再执行'); return; } const result = await AppApi.reportCheckIn(); if (result) { // 请求期间可能已被其他标签页签到:以落盘的最新结果为准 const latest = this.getCheckInResult(); // 服务端判定重复/无新增(points=0)时保留当日已有积分,禁止把已记录积分覆盖为 0 const points = result.points > 0 ? result.points : latest.points; this.setCheckInResult(points); state.appTasks.checkInDone = true; state.appTasks.checkInPoints = points; GM_log(result.points > 0 ? `APP签到成功,+${result.points}积分` : `APP签到确认完成(今日已签${points > 0 ? `,保留已有 +${points}积分` : ',无新增积分'})`); } else { this.bumpRetry('checkIn'); GM_log('APP签到失败,稍后重试'); } }, /** * 搜索前随机阅读:随机上报 0-3 篇(受剩余缺口与每日上报上限约束) * 失败不消耗重试预算(预算仅由兜底流程消耗,避免每日约20次调用放大耗尽) * 进度优先从当日 GM 缓存恢复,未缓存时实时查询;当日已完成直接跳过 */ async runRandomReads() { if (!CONFIG.appReadEnabled) return; // 当日已完成:直接跳过(进度二次校验由搜索完成后的兜底流程负责) if (this.isDoneToday('appReadDate')) return; // 重试预算已耗尽(由兜底流程消耗),当日不再尝试 if (!this.retryLeft('read')) return; // 本地无令牌凭据(从未授权/凭据已清除) if (!state.appToken && !GM_getValue('appRefreshToken', '')) return; createStatusPanel(); state.appTasks.readRunning = true; updateStatusPanel(); try { if (!(await AppAuth.ensureToken())) return; // 当日进度未同步:优先恢复缓存,未缓存再实时查询真实进度 if (!state.appTasks.readProgressSynced && !this.restoreCachedReadProgress()) { const progress = await this.syncReadProgress(); if (!progress) { GM_log('APP阅读进度获取失败,跳过本次随机阅读'); return; } if (progress.current >= progress.total) return; } const limitLeft = CONFIG.appReadDailyLimit - this.getReadReportedToday(); const remaining = state.appTasks.readTotal - state.appTasks.readCurrent; const maxBatch = Math.min(remaining, limitLeft); if (maxBatch <= 0) return; // 随机执行 0-3 次 APP 阅读上报 const batch = Math.min(maxBatch, Math.floor(Math.random() * 4)); if (batch <= 0) return; GM_log(`APP阅读进度 ${state.appTasks.readCurrent}/${state.appTasks.readTotal},搜索前随机上报 ${batch} 篇`); await this.reportReadBatch(batch, false); } finally { state.appTasks.readRunning = false; updateStatusPanel(); } }, /** * 从当日 GM 缓存恢复阅读进度到内存(跨页面跳转免重复查询) * 返回是否命中当日缓存 */ restoreCachedReadProgress() { const cache = GM_getValue('appReadProgressCache', null); if (!cache || cache.date !== utils.getTodayNum()) return false; state.appTasks.readCurrent = cache.current; state.appTasks.readTotal = cache.total; state.appTasks.readProgressSynced = true; return true; }, /** * 同步当日真实阅读进度到内存(随机阅读与面板实时渲染共用) * 返回进度对象;查询失败返回 null */ async syncReadProgress() { const today = utils.getTodayNum(); const progress = await AppApi.queryReadProgress(); if (!progress) return null; state.appTasks.readCurrent = progress.current; state.appTasks.readTotal = progress.total; state.appTasks.readProgressSynced = true; // 持久化当日进度(跨页面跳转恢复,避免每次搜索页都重复查询) GM_setValue('appReadProgressCache', { date: today, current: progress.current, total: progress.total }); updateStatusPanel(); if (progress.current >= progress.total) { GM_setValue('appReadDate', today); state.appTasks.readDone = true; GM_log(`APP阅读任务已完成(已验证 ${progress.current}/${progress.total})`); } else if (this.isDoneToday('appReadDate')) { // 误标自愈:日期戳已标记完成但真实进度未达标,重置后继续 GM_log(`APP阅读标记有误(${progress.current}/${progress.total}),重置后继续`); GM_setValue('appReadDate', 0); state.appTasks.readDone = false; } return progress; }, /** * 批量阅读上报:逐篇上报 + 篇间随机间隔,全部完成后落盘日期戳 * @param {number} count 本次上报篇数 * @param {boolean} countRetry 失败时是否消耗重试预算(随机阅读不消耗,兜底补跑消耗) */ async reportReadBatch(count, countRetry = false) { const today = utils.getTodayNum(); for (let i = 0; i < count; i++) { const result = await AppApi.reportArticleRead(); if (!result) { GM_log(`APP阅读第 ${i + 1} 篇上报失败,中止本次循环`); if (countRetry) this.bumpRetry('read'); return; } // 重复上报(服务端未新增计数)不累加本地计数,防止进度漂移 if (!result.duplicate) { state.appTasks.readCurrent++; } this.bumpReadReported(); GM_setValue('appReadProgressCache', { date: today, current: state.appTasks.readCurrent, total: state.appTasks.readTotal }); updateStatusPanel(); if (state.appTasks.readTotal > 0 && state.appTasks.readCurrent >= state.appTasks.readTotal) break; // 篇间随机间隔,模拟真实阅读行为 if (i < count - 1) { await sleep(3000 + Math.floor(Math.random() * 5000)); } } if (state.appTasks.readTotal > 0 && state.appTasks.readCurrent >= state.appTasks.readTotal) { GM_setValue('appReadDate', today); state.appTasks.readDone = true; GM_log('APP阅读任务完成'); updateStatusPanel(); } }, /** * 资讯阅读兜底流程(搜索完成后补跑):实时查询进度 → 缺口循环上报 */ async runArticleRead() { if (!CONFIG.appReadEnabled) return; const progress = await this.syncReadProgress(); if (!progress) { this.bumpRetry('read'); GM_log('APP阅读进度获取失败,稍后重试'); return; } if (progress.current >= progress.total) return; if (!this.retryLeft('read')) { GM_log('APP阅读重试次数已用尽,今日不再执行'); return; } const limitLeft = CONFIG.appReadDailyLimit - this.getReadReportedToday(); const remaining = Math.min(progress.total - progress.current, limitLeft); if (remaining <= 0) { GM_log(`APP阅读已达每日上报上限(${CONFIG.appReadDailyLimit} 篇),今日不再上报`); return; } GM_log(`APP阅读进度 ${progress.current}/${progress.total},本次上报 ${remaining} 篇(每日上限 ${CONFIG.appReadDailyLimit} 篇)`); await this.reportReadBatch(remaining, true); } }; // 工具函数 const utils = { // 清理所有定时器 clearAllTimers() { state.timers.forEach(timer => { clearTimeout(timer); clearInterval(timer); }); state.timers.clear(); }, // 添加定时器到管理集合 addTimer(timer) { state.timers.add(timer); return timer; }, // 随机对搜索词加词,例如:人工智能发展 --> 人工1智能发z展 addRandomCharsToSearchWord(word) { if (!CONFIG.randomAddSearchWords || !word || Math.random() > CONFIG.randomAddSearchWordsFactor) return word; // 控制添加字符的数量,避免过度添加导致词无意义 const maxAdditions = Math.min(3, Math.floor(word.length / 3)); // 最多添加原词长度1/3的随机字符 let result = word; for (let i = 0; i < Math.floor(Math.random() * (maxAdditions + 1)); i++) { // 随机选择插入位置(避开开头和结尾) const insertPos = Math.floor(Math.random() * (result.length - 1)) + 1; // 随机选择要插入的字符 const randomChar = String.fromCharCode( Math.random() > 0.5 ? Math.floor(Math.random() * 10) + 48 : // 数字 0-9 Math.floor(Math.random() * 26) + 97 // 小写字母 a-z ); result = result.slice(0, insertPos) + randomChar + result.slice(insertPos); } return result; }, // 随机对搜索词进行截取,例如:人工1智能发z展 --> 人工1智 cutSearchWordRandomly(word) { if (!CONFIG.randomCutSearchWords || !word || Math.random() > CONFIG.randomCutSearchWordsFactor) return word; // 控制截取长度,保留至少一半的字符 const minLength = Math.max(2, Math.ceil(word.length / 2)); // 至少保留2个字符或一半字符 const maxLength = word.length; // 最大不超过原词长度 if (minLength >= maxLength) return word; // 随机选择截取长度 const cutLength = Math.floor(Math.random() * (maxLength - minLength)) + minLength; return word.substring(0, cutLength); }, // 依次应用加词和截取 processSearchWord(word) { // 先加词 let processedWord = this.addRandomCharsToSearchWord(word); // 再截取 processedWord = this.cutSearchWordRandomly(processedWord); return processedWord; }, // 生成随机延迟 getRandomDelay() { return Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay; }, // 从区间内随机取暂停间隔 getRandomPauseInterval() { return Math.floor(Math.random() * (CONFIG.pauseIntervalMax - CONFIG.pauseIntervalMin + 1)) + CONFIG.pauseIntervalMin; }, // 从区间内随机取暂停时间 getRandomPauseTime() { return Math.floor(Math.random() * (CONFIG.pauseTimeMax - CONFIG.pauseTimeMin + 1)) + CONFIG.pauseTimeMin; }, // 随机选择一个启动参数(每天保持相同值) getRandomStartParam() { // 获取今天的日期字符串(格式:YYYY-MM-DD) const today = utils.getTodayStr(); // 检查是否已经为今天选择了启动参数 const todayStartParamKey = 'todaySelectedStartParam'; const todayStartParamDateKey = 'todaySelectedStartParamDate'; // 如果存储的日期不是今天,则重新选择 if (GM_getValue(todayStartParamDateKey) !== today) { // 随机选择一个新的启动参数 const startParam = CONFIG.startParams[Math.floor(Math.random() * CONFIG.startParams.length)]; // 存储选中的参数及其对应的日期 GM_setValue(todayStartParamKey, startParam); GM_setValue(todayStartParamDateKey, today); return startParam; } else { // 返回当天已选择的参数 return GM_getValue(todayStartParamKey); } }, // 安全JSON解析 safeJsonParse(str, defaultValue = null) { try { return JSON.parse(str); } catch { return defaultValue; } }, // HTML转义(外部内容写入面板前必须转义,防止注入) escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, // 获取本地日期字符串(格式:YYYY-MM-DD) getTodayStr() { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; }, // 获取本地日期数字戳(格式:YYYYMMDD,APP 任务日期比较用,与 getTodayStr 同源) getTodayNum() { return Number(this.getTodayStr().replace(/-/g, '')); }, // 自然日差值(忽略时刻,仅比较本地日期):返回 toTs 相对 fromTs 相差的整天数 getCalendarDayDiff(fromTs, toTs) { const start = new Date(fromTs); start.setHours(0, 0, 0, 0); const end = new Date(toTs); end.setHours(0, 0, 0, 0); // 四舍五入抵消夏令时导致的 23/25 小时日 return Math.round((end - start) / 86400000); }, // 必应站点根地址:跟随当前页面所在域,避免对非国区用户强制跳转 cn 域 getBingOrigin() { return /bing\.com$/i.test(location.hostname) ? location.origin : 'https://cn.bing.com'; }, // Fisher-Yates洗牌算法 shuffleArray(array) { const result = [...array]; // 创建副本以避免修改原数组 for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; // 交换元素 } return result; }, // 生成随机ID generateId() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 11); }, // 获取精确的剩余时间(不受标签页激活状态影响) getAccurateRemainingTime() { if (!state.countdownStartTime || !state.countdownDuration) return 0; const elapsed = Date.now() - state.countdownStartTime; const remaining = Math.max(0, state.countdownDuration - elapsed); return remaining / 1000; // 转换为秒 }, // 秒数格式化为 mm:ss(先向上取整再拆分,避免 59.6 秒被拆成 0:60) formatClock(totalSeconds) { const total = Math.max(0, Math.ceil(totalSeconds)); return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`; }, // 检查页面是否可见 isPageVisible() { return !document.hidden; }, // 页面可见性变化处理 handleVisibilityChange(callback) { document.addEventListener('visibilitychange', () => { if (!document.hidden) { callback(); } }); }, // 获取ISO周数(返回1-53) getWeekNumber(d) { const date = new Date(d); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); const week1 = new Date(date.getFullYear(), 0, 4); return 1 + Math.round(((date - week1) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); }, // 获取当前年份和ISO周数组成的字符串,如 "2026-W27" getWeekString() { const now = new Date(); return now.getFullYear() + '-W' + String(this.getWeekNumber(now)).padStart(2, '0'); } }; // 统一日志入口(输出集中到脚本管理器日志,避免浏览器控制台与 GM 日志两种渠道混用) const log = msg => GM_log(msg); // 延时等待(统一替代散落各处的 new Promise(resolve => setTimeout(resolve, ms))) const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); /** * 为按钮绑定悬停/按压样式(enter/leave/down/up 四态对应的 style 属性集合) */ function addButtonHoverEffects(btn, { enter, leave, down, up }) { if (!btn) return; if (enter) btn.addEventListener('mouseenter', () => Object.assign(btn.style, enter)); if (leave) btn.addEventListener('mouseleave', () => Object.assign(btn.style, leave)); if (down) btn.addEventListener('mousedown', () => Object.assign(btn.style, down)); if (up) btn.addEventListener('mouseup', () => Object.assign(btn.style, up)); } // 搜索词库 const SEARCH_WORDS = [ // 日常生活类 "今天天气怎么样", "附近有什么好吃的", "怎么做红烧肉", "天气预报", "快递查询", "手机丢了怎么办", "忘记密码怎么找回", "如何办理身份证", "地铁线路图", "公交时刻表", "医院挂号流程", "社保怎么交", "个人所得税怎么算", "公积金提取条件", "居住证办理流程", // 购物消费 "淘宝优惠券", "京东白条怎么用", "拼多多靠谱吗", "二手交易平台", "哪个牌子的空调好", "冰箱怎么选", "洗衣机推荐", "扫地机器人测评", "运动鞋品牌对比", "护肤品推荐", "化妆品正品查询", // 美食餐饮 "附近奶茶店", "火锅底料做法", "蛋糕烘焙教程", "减肥餐食谱", "早餐吃什么健康", "外卖平台哪个好", "咖啡机推荐", "空气炸锅食谱", "家常菜做法", "烘焙入门教程", "日料制作", "西餐做法", // 旅游出行 "周末去哪玩", "假期旅游攻略", "机票什么时候买便宜", "酒店比价", "签证办理流程", "自驾游路线推荐", "背包客装备清单", "民宿预订平台", "高铁票怎么抢", "航班延误怎么办", "旅行保险有必要吗", // 学习工作 "Excel技巧大全", "PPT模板下载", "Python入门教程", "英语学习方法", "考研复习资料", "公务员考试条件", "简历怎么写", "面试技巧", "远程办公软件", "时间管理方法", "职场沟通技巧", "副业赚钱项目", "在线课程平台", "编程学习路线", "数据分析工具", // 娱乐休闲 "最近好看的电影", "Netflix推荐剧集", "switch游戏推荐", "Steam打折游戏", "抖音热门视频", "B站up主推荐", "音乐播放器哪个好", "耳机音质对比", "摄影入门教程", "吉他教学视频", "绘画学习app", "手账制作教程", // 健康运动 "健身房怎么选", "瑜伽初学者动作", "跑步姿势纠正", "减脂增肌计划", "失眠怎么办", "颈椎保健操", "护眼方法", "久坐危害", "体检项目有哪些", "疫苗接种预约", "心理咨询哪里好", "中医调理方法", // 科技数码 "WiFi信号增强方法", "电脑卡顿怎么办", "手机电池保养", "数据备份方案", "智能家居设备推荐", "路由器怎么选", "NAS搭建教程", "云服务器价格", "AI工具有哪些", "ChatGPT使用技巧", "VR眼镜值得买吗", "无人机航拍技巧", // 金融理财 "基金定投策略", "股票开户流程", "理财产品对比", "信用卡积分兑换", "房贷利率计算", "养老保险怎么交", "儿童教育金规划", "应急资金准备", "通货膨胀影响", "黄金投资方式", "外汇交易入门", "税务筹划方法", // 家居装修 "小户型装修灵感", "家具购买指南", "除甲醛方法", "智能家居安装", "墙面颜色搭配", "厨房收纳技巧", "卫生间防水处理", "阳台改造方案", "灯具选择建议", "窗帘搭配技巧", "地板材质对比", "装修公司怎么选", // 亲子教育 "早教机构推荐", "儿童绘本清单", "学区房政策", "兴趣班选择", "亲子游目的地", "儿童营养餐", "育儿经验分享", "家庭教育方法", "暑假活动安排", "儿童安全常识", "青少年心理健康", "留学申请流程", // 汽车交通 "新能源汽车补贴", "二手车估值", "驾校报名流程", "违章查询", "车险怎么买划算", "汽车保养周期", "新能源车充电桩", "堵车路段查询", "停车位怎么找", "共享汽车平台", "摩托车驾照考试", "电动车新国标", // 宠物养护 "猫咪喂养指南", "狗狗训练方法", "宠物医院推荐", "猫粮品牌对比", "宠物美容教程", "鱼缸 setup", "鸟笼清洁", "仓鼠饲养注意事项", "宠物保险有必要吗", "流浪猫救助", "宠物寄养服务", "训犬师推荐", // 本地生活 "附近停车场", "药店营业时间", "超市促销信息", "理发店推荐", "洗衣店价格", "修手机的地方", "开锁电话", "搬家公司收费", "家政保洁服务", "管道疏通电话", "家电维修", "宠物洗澡", // 实用工具查询 "汇率换算", "单位转换", "日历农历", "黄道吉日", "成语解释", "诗词鉴赏", "历史事件查询", "名人传记", "地图导航", "翻译软件", "计算器在线", "单位换算器" ]; /** * 构建搜索URL */ function buildSearchUrl(searchWord) { const domain = utils.getBingOrigin(); const form = CONFIG.searchFormParam; const length = searchWord.length; const hitPosition = Math.random() < 0.9 ? 0 : Math.floor(Math.random() * Math.min(length, 5)) + 1; const sc = `${hitPosition}-${length}`; const urlParams = new URLSearchParams({ q: searchWord, form, sp: -1, lq: 0, pq: searchWord, sc, qs: 'n', sk: '', cvid: utils.generateId(), }); const startParam = utils.getRandomStartParam(); return `${domain}/search?${urlParams.toString()}&${startParam}=1`; } /** * 每周首次执行时显示提示(优化UI版 + 道歉语) */ function showWeeklyTip() { const currentWeek = utils.getWeekString(); const storedWeek = GM_getValue('lastWeeklyTipWeek', ''); if (currentWeek === storedWeek) return; // 入场动画关键帧独立注入(原先依赖设置弹窗内的同名关键帧,未打开过弹窗时动画静默失效) if (!document.getElementById('bing-rewards-animations')) { const animStyle = document.createElement('style'); animStyle.id = 'bing-rewards-animations'; animStyle.textContent = '@keyframes dialogSlideIn { from { opacity: 0; transform: translateY(-30px) scale(0.92); } to { opacity: 1; transform: translateY(0) scale(1); } }'; document.head.appendChild(animStyle); } const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.55);z-index:99999;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(6px);animation:fadeIn 0.3s ease;'; overlay.innerHTML = `
⭐
支持作者
您的 Star 是我持续更新的动力

本脚本完全免费,如果对你有帮助,请给作者一个 Star 支持一下!

〒 本通知一周弹一次,如有打扰,非常抱歉。
`; const supportBtn = overlay.querySelector('#weekly-tip-support'); const closeTipBtn = overlay.querySelector('#weekly-tip-close'); addButtonHoverEffects(supportBtn, { enter: { transform: 'scale(1.05)', boxShadow: '0 8px 28px rgba(0, 103, 184, 0.5)' }, leave: { transform: 'scale(1)', boxShadow: '0 6px 20px rgba(0, 103, 184, 0.35)' }, down: { transform: 'scale(0.95)' }, up: { transform: 'scale(1.05)' } }); supportBtn.onclick = () => { window.open('https://idbb98.github.io/microsoft-bing-rewards-daily-task-script/', '_blank'); overlay.remove(); }; addButtonHoverEffects(closeTipBtn, { enter: { background: 'rgba(255,255,255,0.35)', transform: 'scale(1.1)' }, leave: { background: 'rgba(255,255,255,0.2)', transform: 'scale(1)' } }); closeTipBtn.onclick = () => overlay.remove(); document.body.appendChild(overlay); GM_setValue('lastWeeklyTipWeek', currentWeek); } /** * 创建状态面板 */ function createStatusPanel() { if (state.statusPanel) return state.statusPanel; const panel = document.createElement('div'); panel.id = 'bing-rewards-panel'; // 从配置中读取默认展开/收缩状态 const defaultCollapsed = CONFIG.panelDefaultCollapsed; state.isPanelCollapsed = defaultCollapsed; panel.innerHTML = `

Brian Tool

自动化搜索任务助手
🔽
⚙️
✕
页面活跃 v${GM_info.script.version}
`; // 添加CSS动画样式 const styleElement = document.createElement('style'); styleElement.textContent = ` @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(1.2); } } @keyframes slideDown { from { opacity: 0; transform: translateY(-10px); max-height: 0; } to { opacity: 1; transform: translateY(0); max-height: 1000px; } } @keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } /* ===== 基础布局(桌面 >1024px,尺寸/位置全部由 CSS 管理,JS 仅切换状态类) ===== */ #bing-rewards-panel { position: fixed; bottom: 50px; right: 20px; border-radius: 20px; padding: 24px; /* 宽度计算含 padding/border,避免小屏 min-width 撑破视口导致左侧遮挡 */ box-sizing: border-box; min-width: 380px; max-width: 420px; /* 展开态防溢出:内容过多时面板内部滚动 */ max-height: calc(100vh - 80px); overflow-y: auto; box-shadow: 0 16px 48px var(--panel-shadow), 0 0 0 1px var(--panel-border), 0 0 80px var(--panel-primary-glow); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); } /* 自定义滚动条(深浅色主题由变量适配) */ #bing-rewards-panel::-webkit-scrollbar { width: 6px; } #bing-rewards-panel::-webkit-scrollbar-track { background: transparent; } #bing-rewards-panel::-webkit-scrollbar-thumb { background: var(--panel-text-muted); border-radius: 3px; opacity: 0.4; } /* 展开态头部底部留白(收缩态重置,见下方状态类规则) */ #bing-rewards-panel #panel-header { padding-bottom: 16px; } /* 展开态隐藏倒计时(收缩态显示,见下方状态类规则) */ #bing-rewards-panel #panel-countdown { display: none; } /* ===== 收缩状态(所有断点一致交互:类切换即完成视觉转换) ===== */ #bing-rewards-panel.collapsed { padding: 10px 16px; min-width: 200px; width: fit-content; max-height: none; overflow-y: visible; box-shadow: 0 8px 24px var(--panel-shadow), 0 0 0 1px var(--panel-border); } /* 收缩态重置头部底部内边距,消除多余高度 */ #bing-rewards-panel.collapsed #panel-header { padding-bottom: 0; } #bing-rewards-panel.collapsed #panel-body { display: none; } #bing-rewards-panel.collapsed #panel-title-container { display: none; } #bing-rewards-panel.collapsed #panel-countdown { display: block; } /* ===== 响应式断点(@media 平铺写法,兼容不支持 CSS 嵌套的旧浏览器) ===== */ /* 平板横屏/窄桌面 ≤1024px */ @media (max-width: 1024px) { #bing-rewards-panel { right: 16px; max-width: 440px; } #bing-rewards-panel.collapsed { max-width: 60vw; } } /* 平板竖屏/大屏手机 ≤768px */ @media (max-width: 768px) { #bing-rewards-panel { right: 12px; bottom: 24px; min-width: calc(100vw - 48px); max-width: calc(100vw - 48px); border-radius: 16px; max-height: calc(100vh - 48px); } #bing-rewards-panel.collapsed { min-width: 0; max-width: 60vw; } } /* 手机 ≤480px */ @media (max-width: 480px) { #bing-rewards-panel { right: 12px; bottom: 12px; padding: 14px; min-width: calc(100vw - 24px); max-width: calc(100vw - 24px); max-height: calc(100vh - 40px); } #bing-rewards-panel.collapsed { padding: 10px 14px; max-width: 70vw; } /* 面板头部响应式 */ #bing-rewards-panel #panel-header { padding: 0 0 10px 0; gap: 6px; } /* 面板头部标题容器 */ #bing-rewards-panel #panel-title-container h3 { font-size: 14px; } #bing-rewards-panel #panel-title-container div { font-size: 10px; } /* 倒计时响应式 */ #bing-rewards-panel #panel-countdown { font-size: 11px; padding: 5px 10px; margin-left: 8px; } /* 按钮响应式 */ #bing-rewards-panel #panel-toggle-btn, #bing-rewards-panel #panel-settings-btn, #bing-rewards-panel #panel-close-btn { width: 28px; height: 28px; font-size: 14px; } /* 面板底部状态栏响应式 */ #bing-rewards-panel #panel-body > div:last-child { flex-wrap: wrap; gap: 6px; padding-top: 10px; margin-top: 12px; } /* 任务摘要行(单行 4 列)小屏保持单行:列内 ellipsis 收缩防溢出 */ #bing-rewards-panel #panel-content > div { gap: 8px; } } /* 小屏手机 ≤360px */ @media (max-width: 360px) { #bing-rewards-panel { padding: 12px; min-width: calc(100vw - 16px); max-width: calc(100vw - 16px); } #bing-rewards-panel.collapsed { max-width: 72vw; } /* 隐藏副标题,保留主标题 */ #bing-rewards-panel #panel-title-container div { display: none; } #bing-rewards-panel #panel-title-container h3 { font-size: 13px; } #bing-rewards-panel #panel-toggle-btn, #bing-rewards-panel #panel-settings-btn, #bing-rewards-panel #panel-close-btn { width: 26px; height: 26px; font-size: 13px; } } `; document.head.appendChild(styleElement); // 检测系统主题并应用相应的CSS变量 const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; // 定义主题变量 const themeVariables = { light: { '--panel-bg': '#ffffff', '--panel-border': '#e0e0e0', '--panel-shadow': 'rgba(0, 0, 0, 0.1)', '--panel-primary-color': '#0067b8', '--panel-text-primary': '#1a1a1a', '--panel-text-secondary': '#666666', '--panel-text-muted': '#999999', '--panel-progress-bg': '#f0f0f0', '--panel-success-bg': '#f0f9f0', '--panel-success-text': '#107c10', '--panel-warning-bg': '#fff8e6', '--panel-warning-border': '#ffb900', '--panel-warning-color': '#8a6900', '--panel-warning-text': '#8a6900', '--panel-info-bg': '#f0f7ff', '--panel-info-text': '#005a9e', '--panel-hover-bg': '#f5f5f5', '--panel-primary-glow': 'rgba(0, 103, 184, 0.06)' }, dark: { '--panel-bg': '#1e1e1e', '--panel-border': '#3f3f3f', '--panel-shadow': 'rgba(0, 0, 0, 0.4)', '--panel-primary-color': '#4fc3f7', '--panel-text-primary': '#e0e0e0', '--panel-text-secondary': '#b0b0b0', '--panel-text-muted': '#888888', '--panel-progress-bg': '#2d2d2d', '--panel-success-bg': '#1a3a1a', '--panel-success-text': '#4caf50', '--panel-warning-bg': '#3d3520', '--panel-warning-border': '#ffa726', '--panel-warning-color': '#ffd54f', '--panel-warning-text': '#ffd54f', '--panel-info-bg': '#1a2a3a', '--panel-info-text': '#64b5f6', '--panel-hover-bg': '#2a2a2a', '--panel-primary-glow': 'rgba(79, 195, 247, 0.08)' } }; const theme = isDarkMode ? themeVariables.dark : themeVariables.light; // 尺寸/位置/内边距/阴影全部由 CSS 状态类管理(响应式断点统一生效),内联仅保留主题与视觉特性 if (defaultCollapsed) { panel.classList.add('collapsed'); } Object.assign(panel.style, { position: 'fixed', background: theme['--panel-bg'], border: `1px solid ${theme['--panel-border']}`, zIndex: '10000', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', fontSize: '13px', backdropFilter: 'blur(20px) saturate(180%)', WebkitBackdropFilter: 'blur(20px) saturate(180%)', color: theme['--panel-text-primary'], letterSpacing: '-0.2px' }); // 设置CSS变量 Object.entries(theme).forEach(([key, value]) => { panel.style.setProperty(key, value); }); document.body.appendChild(panel); state.statusPanel = panel; // 重置渲染签名,确保新面板首次渲染必定生效 state.panelSignature = ''; // 为展开/收缩按钮添加事件监听器 const toggleBtn = document.getElementById('panel-toggle-btn'); const panelBody = document.getElementById('panel-body'); const panelHeader = document.getElementById('panel-header'); const countdownElement = document.getElementById('panel-countdown'); if (toggleBtn && panelBody && panelHeader) { toggleBtn.addEventListener('click', () => { state.isPanelCollapsed = !state.isPanelCollapsed; // 同步更新 Config 的缓存值 CONFIG.panelDefaultCollapsed = state.isPanelCollapsed; // 状态切换由 CSS 类驱动:body 显隐、标题/倒计时切换、尺寸/阴影/内边距在所有断点下统一生效 panel.classList.toggle('collapsed', state.isPanelCollapsed); const toggleIcon = document.getElementById('toggle-icon'); if (toggleIcon) { toggleIcon.style.transform = state.isPanelCollapsed ? 'rotate(0deg)' : 'rotate(180deg)'; } toggleBtn.title = state.isPanelCollapsed ? '展开面板' : '收起面板'; // 立即更新面板内容以刷新倒计时显示 updateStatusPanel(); // 展开面板时实时获取最新任务数据 if (!state.isPanelCollapsed) { refreshAppTaskPanelData(); } }); // 添加悬停效果(复用通用 helper;箭头图标的旋转状态仅由点击逻辑维护) addButtonHoverEffects(toggleBtn, { enter: { backgroundColor: theme['--panel-hover-bg'], boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }, leave: { backgroundColor: 'transparent', boxShadow: 'none' } }); } // 为设置按钮添加事件监听器 const settingsBtn = document.getElementById('panel-settings-btn'); if (settingsBtn) { settingsBtn.addEventListener('click', () => { showSettingsDialog(theme); }); // 添加悬停效果 addButtonHoverEffects(settingsBtn, { enter: { backgroundColor: theme['--panel-hover-bg'], transform: 'scale(1.1) rotate(30deg)', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }, leave: { backgroundColor: 'transparent', transform: 'scale(1) rotate(0deg)', boxShadow: 'none' }, down: { transform: 'scale(0.95) rotate(30deg)' }, up: { transform: 'scale(1.1) rotate(30deg)' } }); } // 为关闭按钮添加事件监听器 const closeBtn = document.getElementById('panel-close-btn'); if (closeBtn) { closeBtn.addEventListener('click', () => { panel.style.display = 'none'; }); // 添加悬停效果 addButtonHoverEffects(closeBtn, { enter: { backgroundColor: '#ffebee', color: '#f44336', transform: 'scale(1.1) rotate(90deg)', boxShadow: '0 2px 8px rgba(244,67,54,0.2)' }, leave: { backgroundColor: 'transparent', color: theme['--panel-text-secondary'], transform: 'scale(1) rotate(0deg)', boxShadow: 'none' }, down: { transform: 'scale(0.95) rotate(90deg)' }, up: { transform: 'scale(1.1) rotate(90deg)' } }); } // 监听系统主题变化 if (window.matchMedia) { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleThemeChange = (e) => { const newTheme = e.matches ? themeVariables.dark : themeVariables.light; // 更新面板背景 panel.style.background = newTheme['--panel-bg']; panel.style.borderColor = newTheme['--panel-border']; panel.style.color = newTheme['--panel-text-primary']; // 阴影由 CSS 状态类引用变量渲染(--panel-shadow/--panel-border/--panel-primary-glow),更新变量即自动生效 // 更新CSS变量 Object.entries(newTheme).forEach(([key, value]) => { panel.style.setProperty(key, value); }); // 更新按钮颜色 if (toggleBtn) { toggleBtn.style.color = newTheme['--panel-text-secondary']; } if (settingsBtn) { settingsBtn.style.color = newTheme['--panel-text-secondary']; } if (closeBtn) { closeBtn.style.color = newTheme['--panel-text-secondary']; } // 重新渲染面板内容 updateStatusPanel(); }; // 兼容不同浏览器 if (mediaQuery.addEventListener) { mediaQuery.addEventListener('change', handleThemeChange); } else if (mediaQuery.addListener) { mediaQuery.addListener(handleThemeChange); } } // 监听页面可见性变化 utils.handleVisibilityChange(updateStatusPanel); // 面板创建后实时获取任务数据(页面刷新后进度实时同步) refreshAppTaskPanelData(); updateStatusPanel(); return panel; } /** * 显示设置对话框 */ function showSettingsDialog(theme) { // 检查是否已存在对话框 const existingDialog = document.getElementById('settings-dialog'); if (existingDialog) { existingDialog.remove(); } // 每次打开对话框时重新获取最新配置值(CONFIG getter 实时读取 GM 存储,无缓存) const saved = {}; Object.keys(CONFIG_SCHEMA).forEach(name => { saved[name] = CONFIG[name]; }); // 面板展示单位换算:暂停时间毫秒→分钟,搜索延迟毫秒→秒 const savedPauseTimeMin = saved.pauseTimeMin / 60000; const savedPauseTimeMax = saved.pauseTimeMax / 60000; const savedMinDelay = saved.minDelay / 1000; const savedMaxDelay = saved.maxDelay / 1000; // APP 端授权状态(根据本地刷新令牌判断) const appAuthRefreshToken = GM_getValue('appRefreshToken', ''); const appAuthAgeText = AppAuth.issueAgeText(); const appAuthStatusText = appAuthRefreshToken ? `已授权${appAuthAgeText ? `(${appAuthAgeText})` : ''}` : '未授权'; // 版本号(从CONFIG获取) const currentVersion = CONFIG.version; // 表单卡片与提示文案的统一样式串(设置页内数十处复用,避免逐字重复) const cardStyle = `padding:20px;background:${theme['--panel-hover-bg']};border-radius:14px;border:1px solid ${theme['--panel-border']};`; const hintStyle = `margin-top:10px;font-size:12px;color:${theme['--panel-text-muted']};line-height:1.7;display:flex;align-items:flex-start;gap:6px;`; // 创建设置对话框 const dialog = document.createElement('div'); dialog.id = 'settings-dialog'; dialog.innerHTML = `
🌐

Brian Tool

v${currentVersion}

🔍
✕
🔧

基础配置

设置脚本运行的基本参数

▼
💡 登录必应后手动搜索几次,从地址栏获取 form=xxx 参数值
⚠️ 建议 10-20 次,过高易触发风控
💡 脚本加载时的默认显示状态,可随时手动切换
🎲

搜索行为优化

模拟真实用户搜索习惯,降低检测风险

▼
范围:0-1 默认 0.3 (30%)
范围:0-1 默认 0.2 (20%)
💡
使用建议:开启后可混淆搜索输入行为,因子越大触发概率越高;建议保持默认值。
⏸️

智能暂停策略

模拟人类休息节奏,大幅降低检测风险

▼
次
至
次
📝 默认每 2-3 次搜索随机暂停一次,模拟人类节奏
分钟
至
分钟
⚠️ 建议 20-30 分钟,模拟休息节奏、降低账号风险
⏱️

搜索延迟控制

控制搜索间隔时间,避免过于频繁

▼
秒
至
秒
💡 建议 15-30 秒,贴近真人浏览节奏
🎯

任务点击

自动点击 earn 日常任务与 dashboard 每日活动区域未完成任务(共用配置)

▼
毫秒
💡 默认 3000 毫秒,滚动到任务区域后的等待时间
毫秒
💡 默认 2000 毫秒(2 秒),两次任务点击之间的间隔
次
💡 默认 0 次(不重试),未找到任务时的重试次数
毫秒
💡 默认 2000 毫秒(2 秒),每次重试之间的等待时间
毫秒
💡 默认 1500 毫秒(1.5 秒)
📱

APP端任务

以移动端身份静默完成每日签到与资讯阅读,无需打开活动页面

▼
💡 切换后无需重新授权,保存并刷新页面生效;上报异常时换回默认安卓预设
${appAuthRefreshToken ? '✓ ' : '⚠️ '}${appAuthStatusText}
💡 点击后打开微软授权页(需已登录微软账号),授权码自动捕获并兑换,仅需授权一次
📦

配置管理

导出/导入配置,方便备份和迁移

▼
💡 JSON 格式,可用于备份配置或迁移到其他浏览器/设备
`; document.body.appendChild(dialog); // 获取所有元素 const closeBtn = document.getElementById('settings-close-btn'); const cancelBtn = document.getElementById('settings-cancel-btn'); const resetBtn = document.getElementById('settings-reset-btn'); const saveBtn = document.getElementById('settings-save-btn'); const searchFormInput = document.getElementById('search-form-param-input'); const maxSearchesInput = document.getElementById('max-searches-input'); const panelStateRadios = document.getElementsByName('panel-default-state'); const randomAddCheckbox = document.getElementById('random-add-checkbox'); const randomCutCheckbox = document.getElementById('random-cut-checkbox'); const clickSearchResultsCheckbox = document.getElementById('click-search-results-checkbox'); const randomAddFactorInput = document.getElementById('random-add-factor-input'); const randomCutFactorInput = document.getElementById('random-cut-factor-input'); const pauseIntervalMinInput = document.getElementById('pause-interval-min-input'); const pauseIntervalMaxInput = document.getElementById('pause-interval-max-input'); const pauseTimeMinInput = document.getElementById('pause-time-min-input'); const pauseTimeMaxInput = document.getElementById('pause-time-max-input'); const minDelayInput = document.getElementById('min-delay-input'); const maxDelayInput = document.getElementById('max-delay-input'); const tasksScrollDelayInput = document.getElementById('tasks-scroll-delay-input'); const tasksClickDelayInput = document.getElementById('tasks-click-delay-input'); const tasksMaxRetriesInput = document.getElementById('tasks-max-retries-input'); const tasksRetryDelayInput = document.getElementById('tasks-retry-delay-input'); const tasksCloseTabDelayInput = document.getElementById('tasks-close-tab-delay-input'); const autoClickTasksCheckbox = document.getElementById('auto-click-tasks-checkbox'); const appCheckInCheckbox = document.getElementById('app-checkin-checkbox'); const appReadCheckbox = document.getElementById('app-read-checkbox'); const appReadLimitInput = document.getElementById('app-read-limit-input'); const appUaPresetSelect = document.getElementById('app-ua-preset-select'); const appUaPresetMeta = document.getElementById('app-ua-preset-meta'); const appUaPresetNote = document.getElementById('app-ua-preset-note'); const appUaPresetUa = document.getElementById('app-ua-preset-ua'); const appUaPresetToggle = document.getElementById('app-ua-preset-toggle'); const appAuthStartBtn = document.getElementById('app-auth-start-btn'); const appAuthStatusEl = document.getElementById('app-auth-status'); // APP 设备标识预览:默认单行省略,可展开查看完整 UA(最多 3 行滚动区) let uaPreviewExpanded = false; const setUaPreviewExpanded = (expanded) => { if (!appUaPresetUa) return; uaPreviewExpanded = expanded; Object.assign(appUaPresetUa.style, expanded ? { whiteSpace: 'normal', overflowWrap: 'anywhere', maxHeight: '48px', overflowY: 'auto', overflowX: 'hidden' } : { whiteSpace: 'nowrap', overflowWrap: 'normal', maxHeight: 'none', overflowY: 'hidden', overflowX: 'hidden' }); if (appUaPresetToggle) appUaPresetToggle.textContent = expanded ? '收起 ⌃' : '展开 ⌄'; }; const renderUaPresetPreview = () => { const preset = APP_CLIENT_PRESETS.find(item => item.id === appUaPresetSelect.value) || APP_CLIENT_PRESETS[0]; if (appUaPresetMeta) appUaPresetMeta.textContent = `${preset.label} · ${preset.channel}/${preset.version}`; if (appUaPresetNote) appUaPresetNote.textContent = preset.note; if (appUaPresetUa) { appUaPresetUa.textContent = preset.userAgent; appUaPresetUa.title = preset.userAgent; } setUaPreviewExpanded(false); }; appUaPresetSelect.addEventListener('change', renderUaPresetPreview); if (appUaPresetToggle) appUaPresetToggle.addEventListener('click', () => setUaPreviewExpanded(!uaPreviewExpanded)); if (appUaPresetUa) appUaPresetUa.addEventListener('click', () => setUaPreviewExpanded(!uaPreviewExpanded)); renderUaPresetPreview(); // APP 端授权按钮:打开授权页,落地后由脚本自动捕获授权码并兑换令牌 if (appAuthStartBtn) { appAuthStartBtn.addEventListener('click', () => { AppAuth.openAuthorizePage(); }); } // APP 端授权状态实时刷新:轮询本地令牌,暂存授权码自动补兑换,对话框关闭后停止 let appAuthExchangeTried = !GM_getValue('appPendingAuthCode', ''); let appAuthTimer = null; const refreshAppAuthStatus = () => { if (!appAuthStatusEl) return; if (!document.getElementById('settings-dialog')) { if (appAuthTimer) clearInterval(appAuthTimer); return; } const refreshToken = GM_getValue('appRefreshToken', ''); const pendingCode = GM_getValue('appPendingAuthCode', ''); if (refreshToken) { const ageText = AppAuth.issueAgeText(); appAuthStatusEl.textContent = `✓ 已授权${ageText ? `(${ageText})` : ''}`; appAuthStatusEl.title = '时间为令牌最近一次签发/续期时间,用于 7 天自动续期判断'; appAuthStatusEl.style.background = theme['--panel-success-bg']; appAuthStatusEl.style.color = theme['--panel-success-text']; appAuthStatusEl.style.border = `1px solid ${theme['--panel-success-text']}40`; } else if (pendingCode) { appAuthStatusEl.textContent = '⏳ 授权码待兑换…'; appAuthStatusEl.style.background = theme['--panel-warning-bg']; appAuthStatusEl.style.color = theme['--panel-warning-text']; appAuthStatusEl.style.border = `1px solid ${theme['--panel-warning-color']}40`; if (!appAuthExchangeTried) { appAuthExchangeTried = true; GM_setValue('appPendingAuthCode', ''); AppAuth.exchangeToken('authorization_code', pendingCode).then(ok => { GM_log(ok ? 'APP授权:暂存授权码补兑换成功' : 'APP授权:暂存授权码补兑换失败,请重新点击「开始APP授权」'); refreshAppAuthStatus(); }); } } else { const lastError = GM_getValue('appAuthLastError', ''); appAuthStatusEl.textContent = lastError ? `⚠️ 未授权:${lastError}` : '⚠️ 未授权'; appAuthStatusEl.title = lastError || '点击「开始APP授权」完成授权'; appAuthStatusEl.style.background = theme['--panel-warning-bg']; appAuthStatusEl.style.color = theme['--panel-warning-text']; appAuthStatusEl.style.border = `1px solid ${theme['--panel-warning-color']}40`; } }; refreshAppAuthStatus(); appAuthTimer = setInterval(refreshAppAuthStatus, 1500); // 搜索相关元素 const searchInput = document.getElementById('settings-search-input'); const clearSearchBtn = document.getElementById('clear-search-btn'); // 配置管理相关元素 const exportConfigBtn = document.getElementById('export-config-btn'); const importConfigBtn = document.getElementById('import-config-btn'); const configFileInput = document.getElementById('config-file-input'); const closeDialog = () => { if (appAuthTimer) clearInterval(appAuthTimer); dialog.remove(); document.removeEventListener('keydown', handleEsc); }; closeBtn.addEventListener('click', closeDialog); cancelBtn.addEventListener('click', closeDialog); // 点击背景关闭 dialog.querySelector('div').addEventListener('click', (e) => { if (e.target === dialog.querySelector('div')) { closeDialog(); } }); // 设置项搜索功能 const filterSettingsByKeyword = (keyword) => { const sections = dialog.querySelectorAll('.config-section'); let foundCount = 0; sections.forEach(section => { const sectionTitle = section.getAttribute('data-section'); const searchTags = section.querySelectorAll('[data-search-tags]'); let shouldShow = false; // 检查section标题是否匹配 if (sectionTitle && sectionTitle.toLowerCase().includes(keyword.toLowerCase())) { shouldShow = true; } // 检查各个设置项的搜索标签 searchTags.forEach(tagElement => { const tags = tagElement.getAttribute('data-search-tags'); if (tags && tags.toLowerCase().includes(keyword.toLowerCase())) { shouldShow = true; } }); // 检查section内的文本内容 if (!shouldShow) { const textContent = section.textContent.toLowerCase(); if (textContent.includes(keyword.toLowerCase())) { shouldShow = true; } } if (shouldShow) { section.style.display = 'block'; foundCount++; // 确保匹配的section是展开状态 const content = section.querySelector('.section-content'); if (content) { content.style.maxHeight = '1000px'; content.style.opacity = '1'; } const toggle = section.querySelector('.section-toggle'); if (toggle) { toggle.style.transform = 'rotate(0deg)'; } } else { section.style.display = 'none'; } }); // 显示搜索结果提示 const searchResultsHint = document.getElementById('search-results-hint'); if (keyword.trim()) { if (!searchResultsHint) { const hint = document.createElement('div'); hint.id = 'search-results-hint'; hint.style.cssText = ` padding: 12px 16px; background: ${theme['--panel-info-bg']}; border-radius: 10px; margin-bottom: 16px; font-size: 12px; color: ${theme['--panel-text-muted']}; display: flex; align-items: center; gap: 8px; `; hint.innerHTML = `🔍找到 ${foundCount} 个匹配的设置项`; dialog.querySelector('.dialog-content').insertBefore(hint, dialog.querySelector('.dialog-content').firstChild); } else { searchResultsHint.innerHTML = `🔍找到 ${foundCount} 个匹配的设置项`; } } else if (searchResultsHint) { searchResultsHint.remove(); } }; // 导航切换逻辑 const navSettingsBtn = document.getElementById('nav-settings-btn'); const navAboutBtn = document.getElementById('nav-about-btn'); const settingsContent = dialog.querySelector('.dialog-content:not(#about-content)'); const aboutContent = document.getElementById('about-content'); const searchWrapper = document.getElementById('search-wrapper'); const dialogFooter = dialog.querySelector('.dialog-footer'); const switchToSettings = () => { navSettingsBtn.classList.add('active'); navSettingsBtn.style.background = theme['--panel-primary-color']; navSettingsBtn.style.color = '#ffffff'; navAboutBtn.classList.remove('active'); navAboutBtn.style.background = 'transparent'; navAboutBtn.style.color = theme['--panel-text-secondary']; settingsContent.style.display = 'block'; aboutContent.style.display = 'none'; searchWrapper.style.display = 'block'; dialogFooter.style.display = 'flex'; // 清除搜索 searchInput.value = ''; clearSearchBtn.style.display = 'none'; filterSettingsByKeyword(''); }; const switchToAbout = () => { navAboutBtn.classList.add('active'); navAboutBtn.style.background = theme['--panel-primary-color']; navAboutBtn.style.color = '#ffffff'; navSettingsBtn.classList.remove('active'); navSettingsBtn.style.background = 'transparent'; navSettingsBtn.style.color = theme['--panel-text-secondary']; aboutContent.style.display = 'block'; settingsContent.style.display = 'none'; searchWrapper.style.display = 'none'; dialogFooter.style.display = 'none'; }; navSettingsBtn.addEventListener('click', switchToSettings); navAboutBtn.addEventListener('click', switchToAbout); searchInput.addEventListener('input', (e) => { const keyword = e.target.value; filterSettingsByKeyword(keyword); // 显示/隐藏清除按钮 clearSearchBtn.style.display = keyword.trim() ? 'block' : 'none'; }); clearSearchBtn.addEventListener('click', () => { searchInput.value = ''; clearSearchBtn.style.display = 'none'; filterSettingsByKeyword(''); }); // 设置分组折叠/展开功能 const sectionHeaders = dialog.querySelectorAll('.section-header'); sectionHeaders.forEach(header => { header.addEventListener('click', () => { const section = header.closest('.config-section'); const content = section.querySelector('.section-content'); const toggle = section.querySelector('.section-toggle'); if (content.style.maxHeight === '0px' || !content.style.maxHeight) { content.style.maxHeight = '1000px'; content.style.opacity = '1'; toggle.style.transform = 'rotate(0deg)'; } else { content.style.maxHeight = '0px'; content.style.opacity = '0'; toggle.style.transform = 'rotate(-90deg)'; } }); }); // 配置导出功能 exportConfigBtn.addEventListener('click', () => { // 导出全部用户可配置参数(以 CONFIG_SCHEMA 为准) const config = {}; Object.keys(CONFIG_SCHEMA).forEach(name => { config[name] = CONFIG[name]; }); config.exportTime = new Date().toISOString(); config.version = CONFIG.version; const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `BingRewards_config_${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); GM_notification({ title: '配置导出成功', text: '配置文件已保存到本地', timeout: 3000 }); }); // 配置导入功能 importConfigBtn.addEventListener('click', () => { configFileInput.click(); }); configFileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const config = JSON.parse(event.target.result); // 确认导入 if (!confirm(`⚠️ 确认导入配置文件?\n\n这将覆盖当前所有设置。\n\n导入时间: ${config.exportTime || '未知'}\n版本: ${config.version || '未知'}`)) { return; } // 保存配置(以 CONFIG_SCHEMA 为准,仅导入文件中存在的键) Object.keys(CONFIG_SCHEMA).forEach(name => { if (config[name] !== undefined) CONFIG[name] = config[name]; }); GM_notification({ title: '配置导入成功', text: '配置已成功导入,页面将刷新', timeout: 3000 }); closeDialog(); setTimeout(() => window.location.reload(), 2000); } catch (error) { alert('❌ 配置文件格式错误,请确保导入的是有效的JSON配置文件'); log(`配置导入失败: ${error.message}`); } }; reader.readAsText(file); }); // 添加关闭按钮悬停效果 addButtonHoverEffects(closeBtn, { enter: { backgroundColor: theme['--panel-hover-bg'], color: theme['--panel-primary-color'], transform: 'scale(1.1) rotate(90deg)' }, leave: { backgroundColor: 'transparent', color: theme['--panel-text-secondary'], transform: 'scale(1) rotate(0deg)' }, down: { transform: 'scale(0.95) rotate(90deg)' }, up: { transform: 'scale(1.1) rotate(90deg)' } }); // 按钮悬停和点击效果 const buttons = [ { btn: resetBtn, hoverBg: theme['--panel-hover-bg'], hoverBorder: theme['--panel-primary-color'] }, { btn: cancelBtn, hoverBg: theme['--panel-hover-bg'], hoverBorder: theme['--panel-primary-color'] }, { btn: saveBtn, hoverShadow: `0 8px 28px ${theme['--panel-primary-color']}70` } ]; buttons.forEach(({ btn, hoverBg, hoverBorder, hoverShadow }) => { if (!btn) return; btn.addEventListener('mouseenter', () => { btn.style.transform = 'translateY(-2px)'; if (hoverBg) btn.style.backgroundColor = hoverBg; if (hoverBorder) btn.style.borderColor = hoverBorder; if (hoverShadow) btn.style.boxShadow = hoverShadow; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'translateY(0)'; if (hoverBg) btn.style.backgroundColor = btn.id === 'settings-save-btn' ? '' : 'transparent'; if (hoverBorder) btn.style.borderColor = theme['--panel-border']; if (hoverShadow) btn.style.boxShadow = btn.id === 'settings-save-btn' ? `0 6px 20px ${theme['--panel-primary-color']}50` : 'none'; }); btn.addEventListener('mousedown', () => { btn.style.transform = 'translateY(1px) scale(0.98)'; }); btn.addEventListener('mouseup', () => { btn.style.transform = 'translateY(-2px)'; }); // 添加点击波纹效果 btn.addEventListener('click', function(e) { const ripple = document.createElement('span'); const rect = this.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = e.clientX - rect.left - size / 2; const y = e.clientY - rect.top - size / 2; ripple.style.cssText = ` position: absolute; width: ${size}px; height: ${size}px; left: ${x}px; top: ${y}px; background: radial-gradient(circle, ${theme['--panel-primary-color']}40 0%, transparent 70%); border-radius: 50%; transform: scale(0); animation: buttonRipple 0.6s ease-out; pointer-events: none; `; this.appendChild(ripple); setTimeout(() => ripple.remove(), 600); }); }); // 输入框焦点效果 [searchFormInput, maxSearchesInput, randomAddFactorInput, randomCutFactorInput, pauseIntervalMinInput, pauseIntervalMaxInput, pauseTimeMinInput, pauseTimeMaxInput, minDelayInput, maxDelayInput].forEach(input => { input.addEventListener('focus', () => { input.style.borderColor = theme['--panel-primary-color']; input.style.boxShadow = `0 0 0 3px ${theme['--panel-primary-color']}20`; }); input.addEventListener('blur', () => { input.style.borderColor = theme['--panel-border']; input.style.boxShadow = 'none'; }); }); // 卡片(单选/复选)选中态统一渲染:边框、背景与右上角标记,消除重复分支 // marker:'check'=勾选圆标(单选卡片),'badge'=「已启用」标记(复选卡片) const applyCardState = (label, checked, { checkedBg, idleBg, marker }) => { if (!label) return; label.style.borderColor = checked ? theme['--panel-primary-color'] : theme['--panel-border']; label.style.background = checked ? checkedBg : idleBg; const oldMarker = label.querySelector('.badge, .checkmark'); if (oldMarker) oldMarker.remove(); if (!checked) return; const el = document.createElement('div'); if (marker === 'check') { el.className = 'checkmark'; el.style.cssText = `position:absolute;top:8px;right:8px;width:20px;height:20px;border-radius:50%;background:${theme['--panel-primary-color']};display:flex;align-items:center;justify-content:center;pointer-events:none;`; el.innerHTML = '✓'; } else { el.className = 'badge'; el.style.cssText = `position:absolute;top:10px;right:10px;padding:3px 8px;border-radius:6px;background:${theme['--panel-primary-color']};color:#fff;font-size:10px;font-weight:700;`; el.textContent = '已启用'; } label.appendChild(el); }; // Radio 按钮样式更新(初始化与切换共用同一套渲染) const refreshPanelStateCards = () => { Array.from(panelStateRadios).forEach(radio => { const accent = radio.value === 'expanded' ? theme['--panel-success-bg'] : theme['--panel-info-bg']; applyCardState(radio.closest('label'), radio.checked, { checkedBg: `linear-gradient(135deg,${accent},${theme['--panel-hover-bg']})`, idleBg: theme['--panel-bg'], marker: 'check' }); }); }; Array.from(panelStateRadios).forEach(radio => { const label = radio.closest('label'); radio.addEventListener('change', refreshPanelStateCards); // 添加悬停效果 label.addEventListener('mouseenter', () => { if (!radio.checked) { label.style.transform = 'translateY(-2px)'; label.style.boxShadow = `0 4px 12px ${theme['--panel-shadow']}`; } }); label.addEventListener('mouseleave', () => { label.style.transform = 'translateY(0)'; label.style.boxShadow = 'none'; }); }); refreshPanelStateCards(); // Checkbox 卡片样式更新(强调色由 label 的 data-accent 决定,默认 info) [randomAddCheckbox, randomCutCheckbox, clickSearchResultsCheckbox, autoClickTasksCheckbox, appCheckInCheckbox, appReadCheckbox].forEach(checkbox => { const label = checkbox && checkbox.closest('label'); if (!label) return; const accent = label.dataset.accent === 'success' ? theme['--panel-success-bg'] : theme['--panel-info-bg']; checkbox.addEventListener('change', () => { applyCardState(label, checkbox.checked, { checkedBg: `linear-gradient(135deg,${accent},transparent)`, idleBg: theme['--panel-hover-bg'], marker: 'badge' }); }); // 添加悬停效果 label.addEventListener('mouseenter', () => { label.style.transform = 'translateY(-2px)'; label.style.boxShadow = `0 4px 16px ${theme['--panel-shadow']}`; }); label.addEventListener('mouseleave', () => { label.style.transform = 'translateY(0)'; label.style.boxShadow = 'none'; }); }); // 恢复默认按钮 resetBtn.addEventListener('click', () => { // 确认恢复默认设置 if (!confirm('⚠️ 确认恢复所有设置为默认值?\n\n此操作将清除您所有的自定义设置,请确保已备份配置。')) { return; } searchFormInput.value = 'QBLH'; // 设置面板状态为展开 Array.from(panelStateRadios).forEach(r => { r.checked = r.value === 'expanded'; r.dispatchEvent(new Event('change')); }); maxSearchesInput.value = 20; randomAddCheckbox.checked = false; randomAddFactorInput.value = 0.3; randomCutCheckbox.checked = false; randomCutFactorInput.value = 0.2; clickSearchResultsCheckbox.checked = false; pauseIntervalMinInput.value = 2; pauseIntervalMaxInput.value = 3; pauseTimeMinInput.value = 20; pauseTimeMaxInput.value = 30; minDelayInput.value = 15; maxDelayInput.value = 30; tasksScrollDelayInput.value = 3000; tasksClickDelayInput.value = 2000; tasksMaxRetriesInput.value = 0; tasksRetryDelayInput.value = 2000; tasksCloseTabDelayInput.value = 1500; autoClickTasksCheckbox.checked = false; appCheckInCheckbox.checked = false; appReadCheckbox.checked = false; appReadLimitInput.value = 10; appUaPresetSelect.value = APP_CLIENT_DEFAULT_PRESET; renderUaPresetPreview(); // 触发checkbox样式更新 randomAddCheckbox.dispatchEvent(new Event('change')); randomCutCheckbox.dispatchEvent(new Event('change')); clickSearchResultsCheckbox.dispatchEvent(new Event('change')); autoClickTasksCheckbox.dispatchEvent(new Event('change')); appCheckInCheckbox.dispatchEvent(new Event('change')); appReadCheckbox.dispatchEvent(new Event('change')); }); saveBtn.addEventListener('click', () => { const searchFormParam = searchFormInput.value.trim(); const maxSearches = parseInt(maxSearchesInput.value); const panelDefaultCollapsed = Array.from(panelStateRadios).find(r => r.checked).value === 'collapsed'; const randomAdd = randomAddCheckbox.checked; const randomAddFactor = parseFloat(randomAddFactorInput.value); const randomCut = randomCutCheckbox.checked; const randomCutFactor = parseFloat(randomCutFactorInput.value); const clickSearchResults = clickSearchResultsCheckbox.checked; const pauseIntervalMin = parseInt(pauseIntervalMinInput.value); const pauseIntervalMax = parseInt(pauseIntervalMaxInput.value); const pauseTimeMin = parseFloat(pauseTimeMinInput.value) * 60 * 1000; // 转换为毫秒 const pauseTimeMax = parseFloat(pauseTimeMaxInput.value) * 60 * 1000; // 转换为毫秒 const minDelay = parseFloat(minDelayInput.value) * 1000; // 转换为毫秒 const maxDelay = parseFloat(maxDelayInput.value) * 1000; // 转换为毫秒 const tasksScrollDelay = parseInt(tasksScrollDelayInput.value); const tasksClickDelay = parseInt(tasksClickDelayInput.value); const tasksMaxRetries = parseInt(tasksMaxRetriesInput.value); const tasksRetryDelay = parseInt(tasksRetryDelayInput.value); const tasksCloseTabDelay = parseInt(tasksCloseTabDelayInput.value); const autoClickTasks = autoClickTasksCheckbox.checked; const appCheckInEnabled = appCheckInCheckbox.checked; const appReadEnabled = appReadCheckbox.checked; const appReadDailyLimit = parseInt(appReadLimitInput.value); const appUaPreset = appUaPresetSelect.value; // 验证 if (!searchFormParam) { alert('❌ 请输入有效的搜索表单参数!'); return; } if (maxSearches < 1 || maxSearches > 50) { alert('❌ 最大搜索次数应在 1-50 之间!'); return; } if (randomAddFactor < 0 || randomAddFactor > 1) { alert('❌ 加词因子应在 0-1 之间!'); return; } if (randomCutFactor < 0 || randomCutFactor > 1) { alert('❌ 截词因子应在 0-1 之间!'); return; } if (pauseIntervalMin < 1 || pauseIntervalMax < pauseIntervalMin) { alert('❌ 暂停间隔设置不合理!'); return; } if (pauseTimeMin < 60000 || pauseTimeMax < pauseTimeMin) { alert('❌ 暂停时间设置不合理!'); return; } if (minDelay < 5000 || maxDelay < minDelay) { alert('❌ 搜索延迟设置不合理!'); return; } if (tasksScrollDelay < 1000 || tasksScrollDelay > 10000) { alert('❌ 任务滚动等待时间应在 1000-10000 毫秒之间!'); return; } if (tasksClickDelay < 500 || tasksClickDelay > 10000) { alert('❌ 任务点击间隔应在 500-10000 毫秒之间!'); return; } if (tasksMaxRetries < 0 || tasksMaxRetries > 3) { alert('❌ 任务最大重试次数应在 0-3 之间!'); return; } if (tasksRetryDelay < 500 || tasksRetryDelay > 10000) { alert('❌ 任务重试延迟应在 500-10000 毫秒之间!'); return; } if (tasksCloseTabDelay < 1000 || tasksCloseTabDelay > 30000) { alert('❌ 任务关闭延迟应在 1000-30000 毫秒之间!'); return; } if (appReadDailyLimit < 1 || appReadDailyLimit > 30) { alert('❌ 每日阅读上限应在 1-30 篇之间!'); return; } if (!APP_CLIENT_PRESETS.some(preset => preset.id === appUaPreset)) { alert('❌ APP 设备标识无效,请重新选择!'); return; } // 设置变更确认机制 if (!confirm('⚠️ 确认保存设置变更?\n\n保存后页面将自动刷新以应用新配置。')) { return; } // 保存所有配置(经 CONFIG setter 写入对应的 GM 存储键) CONFIG.searchFormParam = searchFormParam; CONFIG.panelDefaultCollapsed = panelDefaultCollapsed; CONFIG.maxSearches = maxSearches; CONFIG.randomAddSearchWords = randomAdd; CONFIG.randomAddSearchWordsFactor = randomAddFactor; CONFIG.randomCutSearchWords = randomCut; CONFIG.randomCutSearchWordsFactor = randomCutFactor; CONFIG.clickSearchResults = clickSearchResults; CONFIG.pauseIntervalMin = pauseIntervalMin; CONFIG.pauseIntervalMax = pauseIntervalMax; CONFIG.pauseTimeMin = pauseTimeMin; CONFIG.pauseTimeMax = pauseTimeMax; CONFIG.minDelay = minDelay; CONFIG.maxDelay = maxDelay; CONFIG.tasksScrollDelay = tasksScrollDelay; CONFIG.tasksClickDelay = tasksClickDelay; CONFIG.tasksMaxRetries = tasksMaxRetries; CONFIG.tasksRetryDelay = tasksRetryDelay; CONFIG.tasksCloseTabDelay = tasksCloseTabDelay; CONFIG.autoClickTasks = autoClickTasks; CONFIG.appCheckInEnabled = appCheckInEnabled; CONFIG.appReadEnabled = appReadEnabled; CONFIG.appReadDailyLimit = appReadDailyLimit; CONFIG.appUaPreset = appUaPreset; // 显示成功提示 alert('✅ 配置已保存!确认后页面将在3秒后刷新以应用新配置...'); // 关闭对话框 closeDialog(); // 延迟刷新页面 setTimeout(() => { window.location.reload(); }, 3000); }); // ESC键关闭 const handleEsc = (e) => { if (e.key === 'Escape') { closeDialog(); } }; document.addEventListener('keydown', handleEsc); } /** * 生成任务状态摘要行:APP签到 / APP阅读 / 日常任务 / 每日活动 单行 4 列显示 * 位于搜索进度上方;APP 任务状态读内存,任务点击状态直接读 GM 存储(跨标签页共享) */ function getTaskSummaryPanelHtml() { if (!CONFIG.appCheckInEnabled && !CONFIG.appReadEnabled && !CONFIG.autoClickTasks) return ''; // 未授权:内存标记待授权,或本地既无令牌也无刷新令牌(从未授权/凭据已清除) const authPending = state.appTasks.authRequired || (!state.appToken && !GM_getValue('appRefreshToken', '')); const colStyle = 'flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;min-width:0;'; const labelStyle = 'color:var(--panel-text-secondary,#666);font-size:11px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;'; const pill = (text, color, bg) => `${text}`; const pillWarn = () => pill('待授权', 'var(--panel-warning-text,#8a6900)', 'var(--panel-warning-bg,#fff8e6)'); const pillMuted = () => pillSkipped('待执行'); const pillSkipped = (text) => pill(text, 'var(--panel-text-muted,#999)', 'var(--panel-hover-bg,#f5f5f5)'); const pillSuccess = (text) => pill(text, 'var(--panel-success-text,#107c10)', 'var(--panel-success-bg,#f0f9f0)'); const cols = []; // 完成态一律优先于授权态:当日已签/已读时,即使令牌失效也照常展示已得积分 if (CONFIG.appCheckInEnabled) { let value; if (state.appTasks.checkInDone) { // 内存未同步时回退落盘结果,避免短暂显示为无积分 const points = state.appTasks.checkInPoints || AppTaskRunner.getCheckInResult().points; value = pillSuccess(points > 0 ? `✓ +${points}分` : '✓ 已签'); } else if (authPending) { value = pillWarn(); } else { value = pillMuted(); } cols.push(`
📱 APP签到${value}
`); } if (CONFIG.appReadEnabled) { let value; if (state.appTasks.readDone || (state.appTasks.readTotal > 0 && state.appTasks.readCurrent >= state.appTasks.readTotal)) { // 缓存缺失(readTotal=0)时降级为通用文案,避免出现矛盾的"✓ 0/30分" value = state.appTasks.readTotal > 0 && state.appTasks.readCurrent > 0 ? pillSuccess(`✓ ${state.appTasks.readCurrent}/${state.appTasks.readTotal}分`) : pillSuccess('✓ 已完成'); } else if (authPending) { value = pillWarn(); } else if (state.appTasks.readTotal > 0) { value = pill(`${state.appTasks.readCurrent}/${state.appTasks.readTotal}分`, 'var(--panel-primary-color,#0067b8)', 'var(--panel-info-bg,#f0f7ff)'); } else { value = pillMuted(); } cols.push(`
📰 APP阅读${value}
`); } if (CONFIG.autoClickTasks) { const flows = [ { name: 'earn', label: '🖱️ 日常任务' }, { name: 'dashboard', label: '📅 每日活动' } ]; flows.forEach(f => { // 四态:已完成 / 未注入(已放弃)/ 连续超时未完成(已放弃)/ 待执行 const skipReason = isTaskFlowSkippedToday(f.name) ? getTaskFlowSkipReason(f.name) : ''; const value = isTaskFlowCompletedToday(f.name) ? pillSuccess('✓ 完成') : (skipReason === 'incomplete' ? pillSkipped('⊘ 超时') : skipReason === 'not-injected' ? pillSkipped('⊘ 不支持') : pillMuted()); cols.push(`
${f.label}${value}
`); }); } return `
${cols.join('')}
`; } /** * 实时刷新面板任务数据(签到状态 + APP 阅读进度) * 面板创建/展开时调用,确保页面刷新后面板信息实时获取 */ async function refreshAppTaskPanelData() { if (!CONFIG.appCheckInEnabled && !CONFIG.appReadEnabled) return; // 签到状态从本地日期戳同步 AppTaskRunner.syncCheckInState(); // 当日阅读已完成:恢复缓存进度即可,不再实时查询(进度二次校验由兜底流程负责) if (CONFIG.appReadEnabled && AppTaskRunner.isDoneToday('appReadDate')) { AppTaskRunner.restoreCachedReadProgress(); state.appTasks.readDone = true; updateStatusPanel(); return; } // 已授权且当日阅读进度未同步时,实时查询服务端进度 if (CONFIG.appReadEnabled && !state.appTasks.readProgressSynced && (state.appToken || GM_getValue('appRefreshToken', ''))) { try { if (await AppAuth.ensureToken()) { await AppTaskRunner.syncReadProgress(); } } catch (e) { // 查询失败保持现有显示,不打断面板渲染 } } updateStatusPanel(); } // 面板状态提示条的配色变体(图标 + 文案由 buildPanelNotice 组装) const PANEL_NOTICE_VARIANTS = { info: { bg: 'var(--panel-info-bg,#f0f7ff)', border: 'var(--panel-primary-color,#0067b8)', color: 'var(--panel-info-text,#005a9e)' }, success: { bg: 'var(--panel-success-bg,#f0f9f0)', border: 'var(--panel-success-text,#107c10)', color: 'var(--panel-success-text,#107c10)' }, muted: { bg: 'var(--panel-hover-bg,#f5f5f5)', border: 'var(--panel-text-muted,#999)', color: 'var(--panel-text-secondary,#666)' } }; /** * 生成面板状态提示条(面板内各状态提示共用,避免重复样式串) * @param {string} icon 图标字符 * @param {string} text 提示文案 * @param {'info'|'success'|'muted'} variant 配色变体 */ function buildPanelNotice(icon, text, variant = 'info') { const c = PANEL_NOTICE_VARIANTS[variant] || PANEL_NOTICE_VARIANTS.info; return `
${icon} ${text}
`; } /** * 更新状态面板 */ function updateStatusPanel() { if (!state.statusPanel) return; const taskStatus = getTaskStatus(); const status = derivePanelStatus(taskStatus); const content = document.getElementById('panel-content'); const pageStatus = document.getElementById('page-status'); const countdownElement = document.getElementById('panel-countdown'); // 更新页面状态指示器(仅在可见性变化时重排,避免每秒重建 DOM) const taskRunningStatus = document.getElementById('task-running-status'); const isVisible = utils.isPageVisible(); if (state.panelPageVisible !== isVisible) { state.panelPageVisible = isVisible; pageStatus.innerHTML = isVisible ? ' 页面活跃' : ' 后台运行'; pageStatus.style.color = isVisible ? 'var(--panel-success-text,#107c10)' : 'var(--panel-text-muted,#666)'; } // 更新任务执行状态显示 if (taskRunningStatus) { taskRunningStatus.style.display = status.running && status.pauseSeconds === 0 && !taskStatus.isCompleted ? 'flex' : 'none'; } const progress = taskStatus.overallProgress; // 收缩态倒计时:文案与配色统一由 derivePanelStatus 派生(展开态隐藏) if (countdownElement) { countdownElement.textContent = state.isPanelCollapsed ? status.countdownText : ''; if (state.isPanelCollapsed) countdownElement.style.color = status.countdownColor; } // 内容区:仅在影响结构的字段变化时重建,秒级刷新交由下方定向更新(避免每秒整块重排) const summaryHtml = getTaskSummaryPanelHtml(); const signature = [ taskStatus.currentCount, taskStatus.maxCount, taskStatus.isCompleted, status.terminated, state.panel.currentWord, status.showNextWord, status.pauseSeconds > 0, status.running, state.appTasks.readRunning, state.appTasks.readCurrent, state.appTasks.readTotal, summaryHtml ].join('|'); if (state.panelSignature !== signature) { state.panelSignature = signature; content.innerHTML = `
${summaryHtml}
📊 搜索进度 ${taskStatus.currentCount}/${taskStatus.maxCount}
${progress > 10 ? '' + progress + '%' : ''}
${state.appTasks.readRunning ? buildPanelNotice('📖', `APP阅读执行中${state.appTasks.readTotal > 0 ? ` ${state.appTasks.readCurrent}/${state.appTasks.readTotal}分` : ''},完成后继续搜索`, 'info') : ''} ${taskStatus.isCompleted ? buildPanelNotice( status.terminated ? '⏹️' : '✅', status.terminated ? '今日任务已终止,可从菜单重新开始' : '今日任务已完成', status.terminated ? 'muted' : 'success' ) : ''} ${status.pauseSeconds > 0 ? `
⏸️
暂停中
` : ''} ${status.showNextWord ? `
🔍 下个搜索词
${utils.escapeHtml(state.panel.currentWord)}
` : ''}
`; } // 秒级字段定向更新(倒计时与暂停剩余时间),仅为刷新数字时不重建整块内容;对应区块隐藏时不写入 if (status.showNextWord) { const etaElement = document.getElementById('next-search-eta'); if (etaElement) etaElement.textContent = status.etaText; } if (status.pauseSeconds > 0) { const pauseElement = document.getElementById('pause-time-left'); if (pauseElement) pauseElement.textContent = status.pauseText; } } /** * 获取热门搜索词 */ async function fetchSearchKeywords() { const cacheKey = 'cache_search_words'; const cached = GM_getValue(cacheKey); if (cached && Date.now() - cached.time < 3600000) { return cached.words; } // 定义热词API源 const sources = [ { name: "今日头条热榜", url: "https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc", parser: data => data.data?.map(item => item.Title?.trim()).filter(Boolean) || [] }, { name: "微博实时热点", url: "https://m.weibo.cn/api/container/getIndex?containerid=106003type%3D25%26t%3D3%26disable_hot%3D1%26filter_type%3Drealtimehot", parser: data => { if (data.data.cards && data.data.cards[0].card_group) { return data.data.cards[0].card_group .map(item => item.desc) .filter(Boolean); } return []; } }, { name: "百度热搜", url: "https://top.baidu.com/api/board?tab=realtime", parser: data => data.data?.cards?.[0]?.content?.map(item => item.word) || [] }, { name: "腾讯新闻热点", url: "https://r.inews.qq.com/gw/event/hot_ranking_list?page_size=50", parser: data => data.idlist?.[0]?.newslist?.map(item => item.title) || [] } ]; const allWords = new Set(); // 使用Set避免重复词 // 并行请求所有API(单源失败不影响整体:swallowError 让失败源返回空串而非中断) const promises = sources.map(source => request({ url: source.url, swallowError: true }).then(text => { if (!text) { GM_log(`${source.name} 请求失败或超时`); return []; } try { const data = utils.safeJsonParse(text, {}); const words = source.parser(data).filter(word => word && word.length >= 2 && word.length <= 30 && !/^[0-9]+$/.test(word) // 过滤纯数字 ); GM_log(`从 ${source.name} 获取到 ${words.length} 个热词`); return words; } catch (e) { GM_log(`解析 ${source.name} 数据失败: ${e.message}`); return []; } }) ); // 等待所有API请求完成 const results = await Promise.all(promises); // 合并所有结果并去重 results.forEach(words => { words.forEach(word => { // 额外过滤条件 if (word && !allWords.has(word)) { allWords.add(word); } }); }); const allWordsArray = Array.from(allWords); GM_log(`总共获取到 ${allWordsArray.length} 个不重复的热词`); // 如果从API获取的词不够,补充本地词库 if (allWordsArray.length < CONFIG.maxSearches) { const remainingCount = CONFIG.maxSearches - allWordsArray.length; const localWords = utils.shuffleArray(SEARCH_WORDS); for (let i = 0; i < remainingCount && i < SEARCH_WORDS.length; i++) { if (!allWords.has(localWords[i])) { allWordsArray.push(localWords[i]); } } } // 随机打乱合并后的词库 const words = utils.shuffleArray(allWordsArray); // 保存到缓存 GM_setValue(cacheKey, { words, time: Date.now() }); return words; } /** * 获取任务状态 */ function getTaskStatus() { // 搜索计数跨天自动归零:仅存储数字会导致次日被判定为已完成而停摆,故附带日期戳比对 const today = utils.getTodayStr(); let searchCount = GM_getValue('searchCount', 0); if (GM_getValue('searchCountDate', '') !== today) { GM_setValue('searchCount', 0); GM_setValue('searchCountDate', today); searchCount = 0; } return { currentCount: searchCount, maxCount: CONFIG.maxSearches, isCompleted: searchCount >= CONFIG.maxSearches, overallProgress: Math.round((searchCount / CONFIG.maxSearches) * 100) }; } // 当日是否被手动终止(与「已完成」区分,跨天自动失效) function isTaskTerminatedToday() { return GM_getValue('searchTerminatedDate', '') === utils.getTodayStr(); } /** * 重置面板数据与倒计时(任务完成 / 手动终止 / 新一轮启动时统一调用,根治残留显示) */ function resetPanelStatus() { state.panel.currentWord = ''; state.panel.pauseTimeLeft = null; state.countdownStartTime = 0; state.countdownDuration = 0; } /** * 派生面板显示状态(收缩态倒计时文案、下个搜索词显示条件与秒级文案) * 跨页面跳转后 state.isRunning 会重置为 false,故以「当日已有进度且未完成」兜底判定进行中 * @param {{currentCount:number,maxCount:number,isCompleted:boolean}} taskStatus */ function derivePanelStatus(taskStatus) { const { currentWord, pauseTimeLeft } = state.panel; const remainingTime = utils.getAccurateRemainingTime(); const pauseSeconds = pauseTimeLeft !== null && pauseTimeLeft > 0 ? Math.ceil(pauseTimeLeft) : 0; const terminated = taskStatus.isCompleted && isTaskTerminatedToday(); const running = state.isRunning || (taskStatus.currentCount > 0 && !taskStatus.isCompleted); let countdownText = ''; let countdownColor = 'var(--panel-text-muted,#999)'; if (terminated) { countdownText = '⏹️ 已终止'; } else if (taskStatus.isCompleted) { countdownText = '✅ 已完成'; countdownColor = 'var(--panel-success-text,#107c10)'; } else if (pauseSeconds > 0) { countdownText = `⏸️ ${utils.formatClock(pauseSeconds)}`; countdownColor = 'var(--panel-warning-text,#8a6900)'; } else if (state.appTasks.readRunning) { countdownText = '📖 阅读中'; countdownColor = 'var(--panel-info-text,#005a9e)'; } else if (running && remainingTime > 0) { countdownText = `⏱️ ${Math.ceil(remainingTime)}s`; countdownColor = 'var(--panel-info-text,#005a9e)'; } else if (running) { countdownText = '⏳ 准备中'; countdownColor = 'var(--panel-info-text,#005a9e)'; } else { // 未开始:当日计数为 0,其余状态已被上面的分支覆盖 countdownText = '⏹️ 待启动'; } return { running, terminated, pauseSeconds, countdownText, countdownColor, // 「下个搜索词」仅在运行中且未完成、非暂停、倒计时有效时显示(修复完成后残留显示) showNextWord: running && !taskStatus.isCompleted && pauseSeconds === 0 && remainingTime > 0 && Boolean(currentWord), etaText: `${Math.ceil(remainingTime)}秒后`, pauseText: `${Math.floor(pauseSeconds / 60)}分${pauseSeconds % 60}秒后继续` }; } /** * 执行搜索任务 */ async function executeSearch() { if (state.isRunning) return; state.isRunning = true; createStatusPanel(); const taskStatus = getTaskStatus(); if (taskStatus.isCompleted) { // 兜底:搜索完成后若 APP 任务未全部完成,补跑确保签到与阅读完成 if ((CONFIG.appCheckInEnabled || CONFIG.appReadEnabled) && !AppTaskRunner.isAllDone()) { await AppTaskRunner.runAll(); } resetPanelStatus(); updateStatusPanel(); // 与面板文案保持一致:手动终止后不再提示「任务已完成」 GM_notification({ title: '任务完成', text: isTaskTerminatedToday() ? '今日任务已终止,可从菜单重新开始' : 'Bing Rewards 任务已完成', timeout: 3000 }); state.isRunning = false; return; } // 更新标题 const title = document.querySelector('title'); if (title) title.textContent = `[${taskStatus.currentCount}/${taskStatus.maxCount}] Brian Tool...`; // 获取搜索词 if (state.searchWords.length === 0) { try { state.searchWords = await fetchSearchKeywords(); } catch { state.searchWords = utils.shuffleArray(SEARCH_WORDS); } } const searchIndex = taskStatus.currentCount % state.searchWords.length; const searchWord = state.searchWords[searchIndex]; // 对搜索词进行处理 const processedSearchWord = utils.processSearchWord(searchWord); const delay = utils.getRandomDelay(); // 设置精确倒计时与当前搜索词(面板数据统一写入 state.panel) state.countdownStartTime = Date.now(); state.countdownDuration = delay; state.panel.currentWord = processedSearchWord; state.panel.pauseTimeLeft = null; // 更新面板 updateStatusPanel(); // 使用精确计时器,不受页面可见性影响 utils.addTimer(setTimeout(() => { utils.clearAllTimers(); // 搜索执行前随机完成 0-3 次 APP 阅读上报(阅读开关开启且当日未完成时),完成后继续搜索 AppTaskRunner.runRandomReads().finally(() => { performSearch(processedSearchWord, taskStatus); }); }, delay)); // 添加一个定期更新面板的定时器(每秒更新一次) utils.addTimer(setInterval(updateStatusPanel, 1000)); } /** * 执行搜索 */ function performSearch(searchWord, taskStatus) { const nextCount = taskStatus.currentCount + 1; GM_setValue('searchCount', nextCount); GM_log(`搜索: ${searchWord} (${nextCount}/${taskStatus.maxCount})`); // 重置倒计时与搜索词(暂停期间不保留「下个搜索词」) resetPanelStatus(); // 随机暂停间隔检查 // 生成本次搜索周期内的暂停间隔(只在首次搜索时确定,之后保持不变直到完成一次完整搜索) let currentPauseInterval = GM_getValue('currentPauseInterval', null); if (currentPauseInterval === null) { currentPauseInterval = utils.getRandomPauseInterval(); GM_setValue('currentPauseInterval', currentPauseInterval); } if (nextCount % currentPauseInterval === 0) { // 每次暂停时生成新的随机暂停时间 const pauseTime = utils.getRandomPauseTime(); const pauseStartTime = Date.now(); state.panel.pauseTimeLeft = pauseTime / 1000; updateStatusPanel(); // 使用精确的暂停计时 utils.addTimer(setInterval(() => { const elapsed = Date.now() - pauseStartTime; state.panel.pauseTimeLeft = Math.max(0, (pauseTime - elapsed) / 1000); updateStatusPanel(); if (state.panel.pauseTimeLeft <= 0) { utils.clearAllTimers(); // 完成暂停后,重新生成下一个暂停间隔 GM_setValue('currentPauseInterval', utils.getRandomPauseInterval()); window.location.href = buildSearchUrl(searchWord); } }, 1000)); } else { window.location.href = buildSearchUrl(searchWord); } } /** * 页面加载完成后执行随机滚动,模拟真实用户行为 * 随机滚动多次,方向(上滑/下滑)和次数都是随机的 */ function randomScrollAfterPageLoad() { // 等待页面内容完全加载(定时器纳入统一管理,终止任务时可彻底回收) utils.addTimer(setTimeout(() => { const scrollHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); const viewportHeight = window.innerHeight; const maxScroll = scrollHeight - viewportHeight; // 如果页面可以滚动 if (maxScroll > 0) { // 随机生成滚动次数(2-5次) const scrollCount = Math.floor(Math.random() * 4) + 2; GM_log(`开始随机滚动,总次数: ${scrollCount}次`); // 执行多次随机滚动 let currentScroll = window.scrollY; for (let i = 0; i < scrollCount; i++) { utils.addTimer(setTimeout(() => { // 随机决定滚动方向:true=下滑,false=上滑 const scrollDown = Math.random() > 0.2; // 随机生成滚动距离(100-800px) const scrollDistance = Math.floor(Math.random() * 700) + 100; // 计算新的滚动位置 let newScrollPosition; if (scrollDown) { // 下滑:当前位置 + 随机距离,不超过最大滚动位置 newScrollPosition = Math.min(currentScroll + scrollDistance, maxScroll); } else { // 上滑:当前位置 - 随机距离,不小于0 newScrollPosition = Math.max(currentScroll - scrollDistance, 0); } // 执行滚动 window.scrollTo({ top: newScrollPosition, behavior: 'smooth' }); // 更新当前位置 currentScroll = newScrollPosition; const direction = scrollDown ? '下滑' : '上滑'; GM_log(`第${i + 1}次滚动: ${direction} ${scrollDistance}px,目标位置: ${newScrollPosition}px`); // 如果是最后一次滚动,滚动结束后检查并点击链接 if (i === scrollCount - 1) { utils.addTimer(setTimeout(() => { checkAndClickSearchResult(); }, 1500)); // 等待滚动动画完成 } }, i * 1000)); // 每次滚动间隔1秒,模拟真实用户操作 } } else { // 页面无法滚动,直接检查并点击链接 checkAndClickSearchResult(); } }, 2500)); // 等待2.5秒让页面内容加载完成 } /** * 检查当前页面是否为搜索结果页且包含启动参数,如果是则点击搜索结果链接 */ function checkAndClickSearchResult() { try { // 前置检查:开关关闭或任务已终止(含手动终止)时不再打开搜索结果页 if (!CONFIG.clickSearchResults || !state.isRunning) return; const isSearchPage = /\/search/.test(window.location.pathname) && /[?&]q=/.test(window.location.search); if (!isSearchPage) return; const startParam = utils.getRandomStartParam(); const urlParams = new URLSearchParams(window.location.search); if (!urlParams.has(startParam)) return; GM_log(`检测到搜索结果页,准备点击链接`); // 尝试从标准搜索结果中查找链接 let targetLink = findLinkFromSearchResults(); // 降级策略:如果未找到,查找页面全部链接 if (!targetLink) { GM_log('未找到标准搜索结果,尝试查找页面全部链接'); targetLink = findAnyValidLinkOnPage(); } if (!targetLink) { GM_log('未找到可点击的有效链接'); return; } GM_log(`点击链接: ${targetLink.href}`); simulateHumanClick(targetLink); } catch (error) { GM_log(`点击搜索结果时出错: ${error.message}`); } } /** * 从标准搜索结果中查找可点击的链接 * @returns {HTMLAnchorElement|null} */ function findLinkFromSearchResults() { const searchResults = Array.from(document.querySelectorAll('li.b_algo')) .filter(result => isElementVisible(result)); if (searchResults.length === 0) { return null; } GM_log(`找到 ${searchResults.length} 个可见的搜索结果`); // 最多尝试5次 const maxAttempts = Math.min(5, searchResults.length); for (let i = 0; i < maxAttempts; i++) { const randomIndex = Math.floor(Math.random() * searchResults.length); const result = searchResults[randomIndex]; const link = findClickableLink(result); if (link && isElementVisible(link)) { GM_log(`成功找到有效链接`); return link; } } return null; } /** * 检查元素是否在当前窗口可见 * @param {Element} element - 要检查的元素 * @returns {boolean} - 元素是否在当前窗口可见 */ function isElementVisible(element) { try { if (!element) { return false; } const rect = element.getBoundingClientRect(); const windowHeight = window.innerHeight || document.documentElement.clientHeight; const windowWidth = window.innerWidth || document.documentElement.clientWidth; // 检查元素是否在视口内 return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= windowHeight && rect.right <= windowWidth ); } catch (error) { GM_log(`检查元素可见性时出错: ${error.message}`); return false; } } /** * 从搜索结果中查找可点击的链接 * @param {Element} result - 搜索结果元素 * @returns {HTMLAnchorElement|null} */ function findClickableLink(result) { if (!result) return null; // 策略1: h2 中的链接(优先直接子元素,其次嵌套) for (const h2 of result.querySelectorAll('h2')) { const link = h2.querySelector(':scope > a[href]') || Array.from(h2.querySelectorAll('a[href]')).find(isValidResultLink); if (link) return link; } // 策略2: 排除辅助链接后的第一个有效链接 const allLinks = result.querySelectorAll('a[href]'); for (const link of allLinks) { if (!link.classList.contains('tilk') && !link.closest('.b_tpcn, .b_attribution, .b_meta') && isValidResultLink(link)) { return link; } } // 策略3: 任意有效链接(保底) return Array.from(allLinks).find(isValidResultLink) || null; } /** * 验证链接是否为有效的搜索结果链接 * @param {HTMLAnchorElement} link * @returns {boolean} */ function isValidResultLink(link) { if (!link || !link.href) return false; const href = link.href; // 必须是 http/https 协议 if (!href.startsWith('http://') && !href.startsWith('https://')) { return false; } // 排除内部链接 if (href.includes('bing.com') || href.includes('msn.com') || href.includes('microsoft.com')) { return false; } return true; } /** * 在页面中查找任意有效的外部链接(降级策略) * @returns {HTMLAnchorElement|null} */ function findAnyValidLinkOnPage() { const allLinks = Array.from(document.querySelectorAll('a[href]')); if (allLinks.length === 0) return null; // 过滤出可见的有效外部链接 const validLinks = allLinks.filter(link => { if (!isValidResultLink(link)) return false; // 排除导航、页脚、侧边栏、广告等区域 if (link.closest('nav, footer, header, #b_header, #b_footer, .b_nav, .b_footer, .b_sideBlade, .ads, .advertisement, #b_context')) { return false; } return isElementVisible(link); }); // 如果没有可见链接,尝试任意有效链接 const candidates = validLinks.length > 0 ? validLinks : allLinks.filter(isValidResultLink); if (candidates.length === 0) return null; // 随机选择一个 return candidates[Math.floor(Math.random() * candidates.length)]; } /** * 模拟人工操作点击链接 * @param {HTMLAnchorElement} link - 要点击的链接元素 */ function simulateHumanClick(link) { try { if (link.dataset.clicked === 'true') { GM_log('链接已被点击,跳过重复操作'); return; } link.dataset.clicked = 'true'; const newTab = GM_openInTab(link.href, { active: false, insert: true, setParent: true }); GM_log('已通过 GM_openInTab 打开链接'); // 关闭定时器不纳入 utils.addTimer:标签页由本函数打开,必须保证无论任务是否终止都能回收 setTimeout(() => { const closeTab = tab => { if (tab && typeof tab.close === 'function') { tab.close(); GM_log('已关闭搜索结果标签页'); return true; } return false; }; try { if (!closeTab(newTab) && typeof GM_saveTab === 'function') { Promise.resolve(GM_saveTab(newTab)) .then(savedTab => closeTab(savedTab)) .catch(() => {}); } } catch (e) { GM_log(`关闭搜索结果标签页失败: ${e.message}`); } }, CONFIG.tasksCloseTabDelay || 1500); } catch (error) { GM_log(`点击出错: ${error.message}`); } } /** * 任务点击流程配置(earn 日常任务与 dashboard 每日活动共用一套点击流程) */ const TASK_FLOW_CONFIG = { earn: { logPrefix: '[EarnTasks]', pageUrl: 'https://rewards.bing.com/earn', completedKey: 'earnTasksCompleted', lastDateKey: 'lastEarnTasksDate', injectedKey: 'earnTasksInjected', injectedDateKey: 'lastEarnTasksInjectedDate', finishedKey: 'earnTasksFinished', incompleteKey: 'earnTasksIncompleteStreak', incompleteDateKey: 'lastEarnTasksIncompleteDate', skipDateKey: 'earnTasksSkipDate', skipReasonKey: 'earnTasksSkipReason', label: '日常任务', notificationTitle: 'Bing Rewards 日常任务' }, dashboard: { logPrefix: '[DashboardTasks]', pageUrl: 'https://rewards.bing.com/dashboard', completedKey: 'dashboardTasksCompleted', lastDateKey: 'lastDashboardTasksDate', injectedKey: 'dashboardTasksInjected', injectedDateKey: 'lastDashboardTasksInjectedDate', finishedKey: 'dashboardTasksFinished', incompleteKey: 'dashboardTasksIncompleteStreak', incompleteDateKey: 'lastDashboardTasksIncompleteDate', skipDateKey: 'dashboardTasksSkipDate', skipReasonKey: 'dashboardTasksSkipReason', label: 'dashboard 每日活动任务', notificationTitle: 'Bing Rewards 每日活动' } }; /** * 判断当前页面是否为 rewards.bing.com 下指定路径的页面 */ function isRewardsPage(pathPrefix) { return window.location.hostname === 'rewards.bing.com' && window.location.pathname.startsWith(pathPrefix); } /** * 查找 moreactivities 区域(earn 页面日常任务容器) */ function findMoreActivitiesSection() { return document.querySelector('#moreactivities') || document.querySelector('section[id*="moreactivities"]') || document.querySelector('[id*="moreActivities"]') || document.querySelector('[id*="more-activities"]'); } /** * 关闭当前标签页(任务页处理完成后调用) */ function closeCurrentTab(logPrefix) { if (typeof GM_closeTab !== 'undefined') { log(`${logPrefix} 关闭当前标签页`); GM_closeTab(); } } /** * 标记任务点击流程当日已完成(完成标记 + 日期戳,供每日仅执行一次判断) */ function markTaskFlowCompleted(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; GM_setValue(conf.completedKey, true); GM_setValue(conf.lastDateKey, utils.getTodayStr()); // 已完成则清零超时计数,避免影响后续「连续未完成」判定 resetTaskFlowIncompleteStreak(flowName); } /** * 任务点击流程当日是否已完成(每日仅执行一次) */ function isTaskFlowCompletedToday(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; return GM_getValue(conf.lastDateKey, '') === utils.getTodayStr() && GM_getValue(conf.completedKey, false); } /** * 标记目标页脚本已注入成功(注入留痕,供主页面判断本次跳转是否有效) */ function markTaskFlowInjected(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; GM_setValue(conf.injectedKey, true); GM_setValue(conf.injectedDateKey, utils.getTodayStr()); } /** * 目标页脚本当日是否已注入成功 */ function isTaskFlowInjectedToday(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; return GM_getValue(conf.injectedDateKey, '') === utils.getTodayStr() && GM_getValue(conf.injectedKey, false); } /** * 标记本次任务页处理已结束(无论是否完成),供主页面立即恢复搜索流程 */ function markTaskFlowFinished(flowName) { GM_setValue(TASK_FLOW_CONFIG[flowName].finishedKey, true); } /** * 记录「已注入但未完成」的一次尝试:达到阈值后当天放弃跳转 */ function recordTaskFlowIncomplete(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; const streak = increaseTaskFlowIncompleteStreak(flowName); if (streak >= CONFIG.taskFlowIncompleteLimit) { markTaskFlowSkippedToday(flowName, 'incomplete'); log(`${conf.label}连续 ${streak} 次未完成,当天后续不再跳转该页面`); } else { log(`${conf.label}未完成(${streak}/${CONFIG.taskFlowIncompleteLimit}),下次搜索继续尝试`); } } /** * 任务页「脚本已注入但超时未完成」的当日连续次数(日期戳跨天自动归零) */ function getTaskFlowIncompleteStreak(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; if (GM_getValue(conf.incompleteDateKey, '') !== utils.getTodayStr()) return 0; return GM_getValue(conf.incompleteKey, 0); } /** * 累加「脚本已注入但超时未完成」次数,返回累加后的连续次数 */ function increaseTaskFlowIncompleteStreak(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; const streak = getTaskFlowIncompleteStreak(flowName) + 1; GM_setValue(conf.incompleteKey, streak); GM_setValue(conf.incompleteDateKey, utils.getTodayStr()); return streak; } /** * 清零「脚本已注入但超时未完成」连续次数 */ function resetTaskFlowIncompleteStreak(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; GM_setValue(conf.incompleteKey, 0); GM_setValue(conf.incompleteDateKey, utils.getTodayStr()); } /** * 标记当日放弃跳转该页面:当天后续不再跳转(日期戳跨天自动失效) * @param {string} reason 放弃原因:'not-injected' 脚本未注入 / 'incomplete' 连续多次超时未完成 */ function markTaskFlowSkippedToday(flowName, reason) { const conf = TASK_FLOW_CONFIG[flowName]; GM_setValue(conf.skipDateKey, utils.getTodayStr()); GM_setValue(conf.skipReasonKey, reason); } /** * 当日是否已放弃跳转该页面 */ function isTaskFlowSkippedToday(flowName) { return GM_getValue(TASK_FLOW_CONFIG[flowName].skipDateKey, '') === utils.getTodayStr(); } /** * 当日放弃跳转的原因:'not-injected' 未注入 / 'incomplete' 连续超时未完成 */ function getTaskFlowSkipReason(flowName) { return GM_getValue(TASK_FLOW_CONFIG[flowName].skipReasonKey, 'not-injected'); } /** * 检查是否需要执行任务页点击,打开对应页面并等待处理完成(earn/dashboard 共用) */ async function checkAndExecuteTasksOnPage(flowName) { const conf = TASK_FLOW_CONFIG[flowName]; const today = utils.getTodayStr(); if (GM_getValue(conf.lastDateKey, '') === today && GM_getValue(conf.completedKey, false)) { log(`今日 ${conf.label}点击已完成,跳过`); return true; } // 当日已放弃跳转(未注入 / 连续超时未完成):不再跳转该页面,避免每次搜索都空等一次超时 if (isTaskFlowSkippedToday(flowName)) { log(`${conf.label}页面当日已放弃跳转(${getTaskFlowSkipReason(flowName) === 'incomplete' ? '连续超时未完成' : '脚本未注入'}),今天不再跳转该页面`); return true; } log(`准备执行 ${conf.label}点击...`); // 本次跳转的证据:目标页脚本注入后置 injected=true,处理结束(无论是否完成)置 finished=true GM_setValue(conf.injectedKey, false); GM_setValue(conf.finishedKey, false); return new Promise((resolve) => { const taskParam = utils.getRandomStartParam(); const taskTab = GM_openInTab(`${conf.pageUrl}?${taskParam}=1`, { active: true, insert: true, setParent: true }); // 完成与超时两个出口互斥:先到者生效,避免超时回调在已完成后重复计数 let settled = false; const settle = () => { if (settled) return false; settled = true; clearInterval(checkInterval); clearTimeout(timeoutTimer); return true; }; const checkInterval = setInterval(() => { const completed = GM_getValue(conf.completedKey, false); const finished = GM_getValue(conf.finishedKey, false); if (completed || finished) { if (!settle()) return; log(`${conf.label}${completed ? '点击已完成' : '本次处理已结束(未标记完成)'}`); resolve(true); } }, 1000); // 超时兜底:防止页面卡死导致主流程阻塞 const timeoutTimer = setTimeout(() => { if (!settle()) return; if (taskTab && typeof taskTab.close === 'function') { try { taskTab.close(); } catch (e) { log(`关闭标签页失败: ${e.message}`); } } // 未注入则当天不再跳转该页面; // 已注入但超时未完成则累计连续次数,达到阈值后同样当天放弃跳转 if (isTaskFlowInjectedToday(flowName)) { const streak = increaseTaskFlowIncompleteStreak(flowName); if (streak >= CONFIG.taskFlowIncompleteLimit) { markTaskFlowSkippedToday(flowName, 'incomplete'); log(`${conf.label}页面已注入但连续 ${streak} 次超时未完成,当天后续不再跳转该页面`); } else { log(`${conf.label}页面脚本已注入但未完成点击(${streak}/${CONFIG.taskFlowIncompleteLimit}),下次搜索继续尝试`); } } else { markTaskFlowSkippedToday(flowName, 'not-injected'); log(`${conf.label}页面脚本未注入成功,当天后续不再跳转该页面`); } resolve(true); }, CONFIG.taskFlowTimeout); }); } /** * 滚动到日常任务区域 */ function scrollToDailyTasks() { log('[EarnTasks] 正在滚动到日常任务区域...'); const moreActivitiesSection = findMoreActivitiesSection(); if (moreActivitiesSection) { moreActivitiesSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); log('[EarnTasks] 已找到并滚动到 moreactivities 区域'); return true; } const dailyTaskSection = document.querySelector('[data-section="dailyset"]') || document.querySelector('[id*="dailyset"]') || document.querySelector('[class*="daily"]') || document.querySelector('[class*="Daily"]') || document.querySelector('.moreActivities') || document.querySelector('[data-bi-slot*="daily"]') || document.querySelector('[data-m*="daily"]'); if (dailyTaskSection) { dailyTaskSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); log('[EarnTasks] 已找到并滚动到日常任务区域'); return true; } const headings = Array.from(document.querySelectorAll('h2, h3, h4, [role="heading"]')); const dailyHeading = headings.find(h => /日常|每日|daily|Daily|Daily\s*Set/i.test(h.textContent || h.innerText) ); if (dailyHeading) { dailyHeading.scrollIntoView({ behavior: 'smooth', block: 'center' }); log('[EarnTasks] 通过标题找到并滚动到日常任务区域'); return true; } window.scrollTo({ top: document.body.scrollHeight * 0.3, behavior: 'smooth' }); log('[EarnTasks] 使用默认滚动位置'); return false; } /** * 判断任务文案中的进度是否为未完成(X/Y 且 X < Y) * 不使用正则后行断言(旧版 Safari 不支持):先捕获候选,再用前后字符排除日期形态(如 8/28/2026) */ function isIncompleteProgress(text) { const candidate = /(\d{1,3})\s*\/\s*(\d{1,4})/g; let match; while ((match = candidate.exec(text)) !== null) { const before = text.charAt(match.index - 1); const after = text.charAt(match.index + match[0].length); if (/[\d/]/.test(before) || /[\d/]/.test(after)) continue; if (match[1] !== match[2]) return true; } return false; } /** * 从任务容器中收集未完成的任务卡片(earn 日常任务与 dashboard 每日活动共用) * 完成态判定、积分识别、去重与任务对象构造统一在此,两处 DOM 差异通过入参注入 * @param {Element|null} container 任务区域容器 * @param {object} opts selector 任务元素选择器 / logPrefix 日志前缀 * skipLocked 跳过锁定态 / progressAware 按 X/Y 进度判定未完成 / skipHref 额外 href 过滤 * @returns {{tasks: Array, candidateCount: number}|null} 容器缺失时返回 null(DOM 结构变化,由调用方结束流程) */ function collectIncompleteTasks(container, opts) { const { selector, logPrefix, skipLocked = false, progressAware = false, skipHref = null } = opts; if (!container) { log(`${logPrefix} 未找到任务区域容器`); return null; } const candidates = container.querySelectorAll(selector); log(`${logPrefix} 在任务区域找到 ${candidates.length} 个可能的任务元素`); // 状态文案(来自 ActivityCard.Status 国际化资源) // completed=已完成, inProgress=正在进行, notStarted=未开始, activated=已激活, locked=已锁定 const completedPattern = /已完成|complete|completed|✓|✔|done|finished/i; const lockedPattern = /已锁定|locked/i; const tasks = []; const processedHrefs = new Set(); const processedElements = new Set(); candidates.forEach(element => { // 跳过已处理的元素 if (processedElements.has(element)) return; processedElements.add(element); const href = element.getAttribute('href'); if (href) { if ((skipHref && skipHref(href)) || processedHrefs.has(href)) return; processedHrefs.add(href); } const taskText = element.textContent || element.innerText || ''; // 跳过文本内容太少的元素(可能是图标或装饰元素) if (taskText.trim().length < 2) return; const ariaLabel = element.getAttribute('aria-label') || ''; // 完成状态检测:文本、子元素 class、aria-label 多重判定 if (completedPattern.test(taskText) || completedPattern.test(ariaLabel) || element.querySelector('[class*="complete"], [class*="Complete"], [class*="done"], [class*="Done"]')) { log(`${logPrefix} 任务已完成,跳过: ${taskText.substring(0, 30)}...`); return; } // 锁定状态任务不可点击,跳过 if (skipLocked && (lockedPattern.test(taskText) || lockedPattern.test(ariaLabel) || element.querySelector('[class*="lock"]') || element.getAttribute('aria-disabled') === 'true')) { log(`${logPrefix} 任务已锁定,跳过: ${taskText.substring(0, 30)}...`); return; } // 积分识别:+N 形式 const pointsMatch = taskText.match(/\+(\d+)/) || element.outerHTML.match(/\+(\d+)/); const points = pointsMatch ? parseInt(pointsMatch[1], 10) : 0; const incompleteByProgress = progressAware && isIncompleteProgress(taskText); const isTaskLink = progressAware && !!href && href.includes('task'); // earn 要求必须有积分;dashboard 允许积分、进度未完成、任务链接任一命中 if (points <= 0 && !incompleteByProgress && !isTaskLink) return; tasks.push({ element: element, href: href, points: points, taskId: href || element.id || element.className || Date.now().toString(36), text: taskText.substring(0, 100) }); log(`${logPrefix} 找到未完成任务: ${taskText.substring(0, 50)}...${points > 0 ? ` (+${points}分)` : ''}`); }); log(`${logPrefix} 共找到 ${tasks.length} 个未完成任务`); return { tasks, candidateCount: candidates.length }; } /** * 点击任务卡片(earn 与 dashboard 共用) */ async function clickTask(task, flowName) { const { logPrefix } = TASK_FLOW_CONFIG[flowName]; const flowState = state.taskFlows[flowName]; if (flowState.clicked.has(task.taskId)) { log(`${logPrefix} 任务 ${task.taskId} 已点击过,跳过`); return false; } log(`${logPrefix} 正在点击任务: ${task.text.substring(0, 50)}...${task.points > 0 ? ` (+${task.points}分)` : ''}`); try { task.element.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 滚动动画结束、卡片位置稳定后再取坐标点击 await sleep(CONFIG.tasksClickDelay); const rect = task.element.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; log(`${logPrefix} 模拟点击任务元素,位置: (${Math.round(x)}, ${Math.round(y)})`); const originalTarget = task.element.getAttribute('target'); const windowName = 'bingTask_' + Date.now(); task.element.setAttribute('target', windowName); const mouseEvents = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']; for (const eventType of mouseEvents) { const event = new MouseEvent(eventType, { bubbles: true, cancelable: true, view: unsafeWindow, clientX: x, clientY: y }); task.element.dispatchEvent(event); } // 等目标标签页创建,再判断是否真的打开了(合成点击可能被浏览器弹窗拦截) await sleep(1000); if (originalTarget) { task.element.setAttribute('target', originalTarget); } else { task.element.removeAttribute('target'); } flowState.clicked.add(task.taskId); let taskWindow = null; try { taskWindow = window.open('', windowName); } catch (e) { log(`${logPrefix} 获取任务标签页失败: ${e.message}`); } // 判断点击是否真的打开了任务页:about:blank 说明只是 window.open 新建的空标签页 let openedByClick = false; if (taskWindow && !taskWindow.closed) { try { openedByClick = taskWindow.location.href !== 'about:blank'; } catch (e) { // 跨域不可读即说明已导航到目标页 openedByClick = true; } } if (openedByClick) { // 任务页已打开:停留一段时间等目标页上报活动,避免秒关导致不计分 log(`${logPrefix} 任务标签页已打开,停留 ${CONFIG.taskTabDwell}ms 后关闭: ${task.taskId}`); await sleep(CONFIG.taskTabDwell); try { taskWindow.close(); log(`${logPrefix} 已关闭任务标签页: ${task.taskId}`); } catch (e) { log(`${logPrefix} 关闭任务标签页失败: ${e.message}`); } } else if (task.href) { // 标签页未打开(被弹窗拦截):关掉可能的空白页,改用管理器打开任务链接 if (taskWindow && !taskWindow.closed) { try { taskWindow.close(); } catch (e) { log(`${logPrefix} 关闭空白标签页失败: ${e.message}`); } } let taskUrl = task.href; try { taskUrl = new URL(task.href, window.location.href).href; } catch (e) { log(`${logPrefix} 任务链接解析失败,按原值使用: ${e.message}`); } log(`${logPrefix} 未检测到任务标签页(可能被弹窗拦截),改用 GM_openInTab 打开: ${taskUrl}`); if (!/^https?:/i.test(taskUrl)) { log(`${logPrefix} 任务链接不是 http(s),仅保留模拟点击: ${taskUrl}`); } else { try { const taskTab = GM_openInTab(taskUrl, { active: true, insert: true }); await sleep(CONFIG.taskTabDwell); if (taskTab && typeof taskTab.close === 'function') { taskTab.close(); } log(`${logPrefix} 已关闭任务标签页: ${taskUrl}`); } catch (e) { log(`${logPrefix} 打开任务链接失败: ${e.message}`); } } } else { log(`${logPrefix} 任务无可用链接,仅保留模拟点击: ${task.taskId}`); } log(`${logPrefix} 成功点击任务: ${task.taskId}`); return true; } catch (error) { log(`${logPrefix} 模拟点击失败: ${error.message}`); try { log(`${logPrefix} 尝试备用方案: element.click()`); task.element.click(); flowState.clicked.add(task.taskId); log(`${logPrefix} 备用方案成功: ${task.taskId}`); await sleep(CONFIG.tasksClickDelay); return true; } catch (fallbackError) { log(`${logPrefix} 备用方案也失败: ${fallbackError.message}`); return false; } } } /** * 处理任务点击(earn 与 dashboard 共用流程) * @param {string} flowName 流程标识:'earn' 或 'dashboard' * @param {Function} findTasks 异步函数,返回 {tasks, candidateCount};返回 null 表示任务区域缺失 */ async function processTasks(flowName, findTasks) { const conf = TASK_FLOW_CONFIG[flowName]; const flowState = state.taskFlows[flowName]; // 当日已完成则跳过(每日仅执行一次) if (isTaskFlowCompletedToday(flowName)) { log(`今日 ${conf.label}点击已完成,跳过`); await sleep(50); closeCurrentTab(conf.logPrefix); return; } if (flowState.processing) { log(`${conf.logPrefix} 正在处理中,跳过`); return; } flowState.processing = true; log(`${conf.logPrefix} 开始处理任务...`); try { let result = await findTasks(); // 任务卡片尚未渲染(候选数为 0)时等待页面加载后重试 while (result && result.tasks.length === 0 && result.candidateCount === 0 && flowState.retryCount < CONFIG.tasksMaxRetries) { flowState.retryCount++; log(`${conf.logPrefix} 等待 ${CONFIG.tasksRetryDelay}ms 后重试 (${flowState.retryCount}/${CONFIG.tasksMaxRetries})`); await sleep(CONFIG.tasksRetryDelay); result = await findTasks(); } // 任务区域缺失(DOM 结构变化):不写完成标记,避免「显示完成但实际未完成」 if (result === null) { log(`${conf.logPrefix} 未找到任务区域(DOM 结构变化),本次不设置完成标记`); recordTaskFlowIncomplete(flowName); await sleep(CONFIG.tasksCloseTabDelay); return; } // 一个任务卡片都未识别到:可能仍在加载或结构变化,同样不写完成标记 if (result.candidateCount === 0) { log(`${conf.logPrefix} 未识别到任何任务卡片,本次不设置完成标记`); recordTaskFlowIncomplete(flowName); await sleep(CONFIG.tasksCloseTabDelay); return; } // 卡片存在但均已完成 if (result.tasks.length === 0) { markTaskFlowCompleted(flowName); log(`${conf.logPrefix} 任务卡片均已完成,已设置完成标记`); await sleep(CONFIG.tasksCloseTabDelay); return; } log(`${conf.logPrefix} 找到 ${result.tasks.length} 个任务,开始逐个处理...`); let clickedCount = 0; for (const task of result.tasks) { if (await clickTask(task, flowName)) clickedCount++; } // 点击后回读校验:仍有未完成任务时补点一次,仍不通过则不写完成标记,交由下次搜索重试 let remaining = await verifyRemainingTasks(findTasks); if (remaining && remaining.length > 0) { log(`${conf.logPrefix} 回读仍有 ${remaining.length} 个未完成任务,补点一次`); flowState.clicked.clear(); for (const task of remaining) { if (await clickTask(task, flowName)) clickedCount++; } remaining = await verifyRemainingTasks(findTasks); } if (!remaining || remaining.length > 0) { const remainText = remaining ? `${remaining.length} 个未完成` : '任务区域读取失败'; log(`${conf.logPrefix} 回读校验未通过(${remainText}),本次不设置完成标记`); recordTaskFlowIncomplete(flowName); if (typeof GM_notification !== 'undefined') { GM_notification({ text: `已点击 ${clickedCount} 个 ${conf.label},${remainText}`, title: conf.notificationTitle, timeout: 3000 }); } await sleep(CONFIG.tasksCloseTabDelay); return; } markTaskFlowCompleted(flowName); log(`${conf.logPrefix} 回读校验通过,已设置完成标记`); log(`${conf.logPrefix} 任务处理完成`); if (typeof GM_notification !== 'undefined') { GM_notification({ text: `已完成 ${clickedCount} 个 ${conf.label} 点击`, title: conf.notificationTitle, timeout: 3000 }); } await sleep(CONFIG.tasksCloseTabDelay); } finally { flowState.processing = false; // 先写结束信号再关标签页:关页可能中断后续脚本执行,避免主流程空等到超时 markTaskFlowFinished(flowName); closeCurrentTab(conf.logPrefix); } } /** * 点击后回读任务区域:返回剩余未完成任务数组;区域缺失时返回 null(视为校验不通过) */ async function verifyRemainingTasks(findTasks) { await sleep(CONFIG.tasksRetryDelay); const result = await findTasks(); return result ? result.tasks : null; } /** * earn 页面任务查找:滚动定位 → 等待加载 → 查找未完成任务卡片 */ async function findAndPrepareEarnTasks() { scrollToDailyTasks(); await sleep(CONFIG.tasksScrollDelay); return collectIncompleteTasks(findMoreActivitiesSection(), { selector: 'a[href][target="_blank"]', logPrefix: TASK_FLOW_CONFIG.earn.logPrefix }); } /** * dashboard 页面任务查找:等待 #dailyset 加载 → 滚动定位 → 查找未完成任务卡片 * 区域缺失时返回 null(与「未找到任务」区分,不重试直接结束) */ async function findAndPrepareDashboardTasks() { // 等待 #dailyset 区域加载,应对页面元素加载延迟 const dailySetSection = await waitForDashboardDailySet(); if (!dailySetSection) { log('[DashboardTasks] DOM 结构变化,未找到 #dailyset 区域,结束处理'); return null; } scrollToDashboardDailySet(dailySetSection); await sleep(CONFIG.tasksScrollDelay); // 任务卡片仍在加载中(检测到 loading 占位符)时不解析,返回空候选由上层重试 const placeholders = dailySetSection.querySelectorAll('.animate-pulse, [class*="pulse"], [class*="skeleton"], [class*="placeholder"]'); if (placeholders.length > 0) { log('[DashboardTasks] 任务卡片仍在加载中(检测到 loading 占位符),返回空候选'); return { tasks: [], candidateCount: 0 }; } return collectIncompleteTasks(dailySetSection, { selector: 'a[href], [role="button"], .card, [class*="card"]', logPrefix: TASK_FLOW_CONFIG.dashboard.logPrefix, skipLocked: true, progressAware: true, skipHref: href => href === '/earn' || href.startsWith('#') }); } /** * 等待 #dailyset 区域加载完成(应对页面元素加载延迟) * 通过轮询 + MutationObserver 双重机制检测,超时后返回 null * 不仅等待 #dailyset 区域出现,还要等待任务卡片实际加载完成 */ function waitForDashboardDailySet(timeout = 25000) { return new Promise((resolve) => { const selectors = [ '#dailyset', 'section[id*="dailyset"]', '[data-section="dailyset"]' ]; const findDailySet = () => selectors.reduce((found, sel) => found || document.querySelector(sel), null); let dailySetSection = findDailySet(); // 检查是否还有 loading 占位符(React 服务端渲染的骨架屏) const hasLoadingPlaceholders = () => { if (!dailySetSection) return true; const placeholders = dailySetSection.querySelectorAll('.animate-pulse, [class*="pulse"], [class*="skeleton"], [class*="placeholder"]'); return placeholders.length > 0; }; // 检查是否有实际的任务链接 const hasTaskLinks = () => { if (!dailySetSection) return false; const links = dailySetSection.querySelectorAll('a[href]:not([href="/earn"])'); return links.length > 0; }; // 立即检查 if (dailySetSection && !hasLoadingPlaceholders()) { log('[DashboardTasks] #dailyset 区域已加载完成'); resolve(dailySetSection); return; } let observer = null; const startTime = Date.now(); const checkAndResolve = () => { dailySetSection = findDailySet(); if (dailySetSection) { // 如果没有 loading 占位符,或者有实际任务链接,说明加载完成 if (!hasLoadingPlaceholders() || hasTaskLinks()) { log('[DashboardTasks] #dailyset 区域及任务卡片已加载完成'); if (observer) observer.disconnect(); resolve(dailySetSection); return true; } } // 检查超时 if (Date.now() - startTime >= timeout) { log('[DashboardTasks] 等待 #dailyset 区域超时'); if (observer) observer.disconnect(); resolve(dailySetSection || findDailySet()); return true; } return false; }; // 立即检查一次 if (checkAndResolve()) return; // 设置轮询检查(每 500ms 检查一次) const pollInterval = setInterval(() => { if (checkAndResolve()) { clearInterval(pollInterval); } }, 500); observer = new MutationObserver(() => { if (checkAndResolve()) { clearInterval(pollInterval); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } /** * 滚动到 dashboard 每日活动区域(#dailyset) */ function scrollToDashboardDailySet(dailySetSection) { log('[DashboardTasks] 正在滚动到每日活动区域...'); if (!dailySetSection) { log('[DashboardTasks] 未传入 #dailyset 区域,尝试重新查找'); return false; } try { dailySetSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); log('[DashboardTasks] 已滚动到 #dailyset 每日活动区域'); return true; } catch (error) { log(`[DashboardTasks] 滚动失败: ${error.message}`); return false; } } /** * 检查并启动任务 */ async function checkAndStartTask() { // 使用每日生成的启动参数(earn/dashboard/搜索 保持一致) const startParam = utils.getRandomStartParam(); const urlParams = new URLSearchParams(window.location.search); // 如果是 rewards.bing.com/earn 页面,执行日常任务点击(开关关闭时不执行) if (isRewardsPage('/earn')) { const hasTaskParam = urlParams.has(startParam); // 脚本已注入本页,留痕供主页面判断跳转是否有效(未注入时主页面当天不再重复跳转) if (hasTaskParam) markTaskFlowInjected('earn'); if (CONFIG.autoClickTasks && hasTaskParam) { log('[EarnTasks] 检测到自动处理标记,开始执行日常任务点击'); await sleep(50); await processTasks('earn', findAndPrepareEarnTasks); } return; } // 如果是 rewards.bing.com/dashboard 页面,执行每日活动区域任务点击(开关关闭时不执行) if (isRewardsPage('/dashboard')) { const hasTaskParam = urlParams.has(startParam); // 脚本已注入本页,留痕供主页面判断跳转是否有效(未注入时主页面当天不再重复跳转) if (hasTaskParam) markTaskFlowInjected('dashboard'); if (CONFIG.autoClickTasks && hasTaskParam) { log('[DashboardTasks] 检测到自动处理标记,开始执行每日活动区域任务点击'); await sleep(50); await processTasks('dashboard', findAndPrepareDashboardTasks); } return; } // 搜索页面的处理逻辑 // 检查是否有当天的启动参数标记 const hasStartParam = urlParams.has(startParam); log(`检查并启动任务: ${startParam}`); if (hasStartParam) { // 开关开启时,先执行 dashboard 每日活动区域任务点击(在 earn 跳转前),再执行 earn 页面日常任务点击 if (CONFIG.autoClickTasks) { await checkAndExecuteTasksOnPage('dashboard'); await checkAndExecuteTasksOnPage('earn'); } // APP 端签到在搜索开始前执行;资讯阅读改为每次搜索执行前随机穿插上报(见 executeSearch) await AppTaskRunner.runCheckInFlow(); // 有启动参数,准备执行搜索任务 setTimeout(executeSearch, 2000); randomScrollAfterPageLoad(); log(`启动任务: ${startParam}`); } } /** * 清除当日任务执行相关缓存(任务点击 / 搜索进度 / APP 签到与阅读),并复位内存状态 */ function clearTodayTaskCache() { // 任务点击:earn 日常任务 + dashboard 每日活动(完成 / 注入 / 结束 / 超时 / 放弃标记) ['earn', 'dashboard'].forEach(flowName => { const conf = TASK_FLOW_CONFIG[flowName]; GM_deleteValue(conf.completedKey); GM_deleteValue(conf.lastDateKey); GM_deleteValue(conf.injectedKey); GM_deleteValue(conf.injectedDateKey); GM_deleteValue(conf.finishedKey); GM_deleteValue(conf.incompleteKey); GM_deleteValue(conf.incompleteDateKey); GM_deleteValue(conf.skipDateKey); GM_deleteValue(conf.skipReasonKey); const flowState = state.taskFlows[flowName]; flowState.clicked.clear(); flowState.retryCount = 0; }); // 搜索进度与当日终止标记 GM_deleteValue('searchCount'); GM_deleteValue('searchCountDate'); GM_deleteValue('searchTerminatedDate'); GM_deleteValue('currentPauseInterval'); GM_deleteValue('cache_search_words'); // APP 签到 / 阅读当日缓存(含旧版分离键与重试计数) GM_deleteValue('appCheckInResult'); GM_deleteValue('appCheckInDate'); GM_deleteValue('appCheckInPoints'); GM_deleteValue('appReadDate'); GM_deleteValue('appReadReportedDate'); GM_deleteValue('appReadReportedCount'); GM_deleteValue('appReadProgressCache'); GM_deleteValue('appTaskRetryCounters'); GM_deleteValue('appTaskRetryDate'); // 内存状态复位,避免面板继续显示清除前的进度 state.appTasks.checkInDone = false; state.appTasks.checkInPoints = 0; state.appTasks.readDone = false; state.appTasks.readCurrent = 0; state.appTasks.readTotal = 0; state.appTasks.readProgressSynced = false; state.appTasks.readRunning = false; updateStatusPanel(); } // 注册菜单命令 GM_registerMenuCommand('🚀 开始任务', () => { GM_setValue('searchCount', 0); // 同步计数日期戳,与 getTaskStatus 的跨天归零逻辑保持一致 GM_setValue('searchCountDate', utils.getTodayStr()); // 重置当前暂停间隔值以开始新的搜索周期 GM_setValue('currentPauseInterval', utils.getRandomPauseInterval()); // 清除热词缓存,确保开始新任务时获取新的热词 GM_deleteValue('cache_search_words'); // 清除当日终止标记,新任务重新计数 GM_deleteValue('searchTerminatedDate'); // 重置日常任务完成标记(当日已完成则跳过,每日仅执行一次) if (!isTaskFlowCompletedToday('earn')) { GM_setValue('earnTasksCompleted', false); } // 重置 dashboard 每日活动任务完成标记(当日已完成则跳过) if (!isTaskFlowCompletedToday('dashboard')) { GM_setValue('dashboardTasksCompleted', false); } // 重置当日放弃跳转标记与超时计数:手动重新开始任务时恢复页面跳转尝试 ['earn', 'dashboard'].forEach(flowName => { GM_deleteValue(TASK_FLOW_CONFIG[flowName].skipDateKey); GM_deleteValue(TASK_FLOW_CONFIG[flowName].skipReasonKey); GM_setValue(TASK_FLOW_CONFIG[flowName].injectedKey, false); resetTaskFlowIncompleteStreak(flowName); }); // 获取当天的启动参数 const startParam = utils.getRandomStartParam(); window.location.href = utils.getBingOrigin() + '/?' + startParam + '=1'; }); GM_registerMenuCommand('🧹 清除当天任务缓存', () => { if (!confirm('⚠️ 确认清除当天任务执行缓存?\n\n将清除:日常任务/每日活动的完成、注入、超时与放弃标记,搜索次数与当日终止标记,APP 签到与阅读进度。\n\n清除后可重新点击「🚀 开始任务」执行。')) { return; } clearTodayTaskCache(); alert('✅ 已清除当天任务缓存,可重新点击「🚀 开始任务」。'); }); GM_registerMenuCommand('⏹️ 终止任务', () => { const taskStatus = getTaskStatus(); GM_setValue('searchCount', taskStatus.maxCount); // 同时清除当前暂停间隔值 GM_setValue('currentPauseInterval', null); // 标记当日手动终止,便于面板区分「已终止」与「已完成」 GM_setValue('searchTerminatedDate', utils.getTodayStr()); utils.clearAllTimers(); state.isRunning = false; resetPanelStatus(); updateStatusPanel(); }); GM_registerMenuCommand('📊 查看/隐藏面板', () => { if (!state.statusPanel) { createStatusPanel(); } else { const panel = state.statusPanel; panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; } }); GM_registerMenuCommand('⚙️ 配置脚本参数', () => { alert('请配置以下参数:\n\n1. searchFormParam: 登录Bing后手动搜索几次,从地址栏获取实际的form参数值\n2. maxSearches: 设置每日最大搜索次数\n3. 其他高级参数可根据需要调整\n\n配置完成后刷新页面开始使用。'); window.open('https://idbb98.github.io/microsoft-bing-rewards-daily-task-script/quickstart/', '_blank'); }); GM_registerMenuCommand('👨‍💻 关于作者', () => { alert('作者:Brian\n版本:' + GM_info.script.version + '\n\n这是一个自动化完成微软必应每日搜索任务的脚本,帮助您轻松积累奖励积分。\n\n如果您觉得这个脚本有用,欢迎给作者点个Star!'); window.open('https://idbb98.github.io/microsoft-bing-rewards-daily-task-script/', '_blank'); }); if (AppAuth.isAuthLandingPage()) { // 授权落地页:捕获授权码并立即兑换令牌,不执行主任务逻辑 AppAuth.handleAuthLanding(); } else { // 每周首次提示 showWeeklyTip(); // 启动脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', checkAndStartTask); } else { checkAndStartTask(); } }