// ==UserScript==
// @name Microsoft Bing Rewards Daily Task Script (微软必应奖励每日任务脚本)
// @version 26.8.31.6
// @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 },
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 端区域锁定(开启后请求固定走国区)
appRegionLock: { key: 'customAppRegionLock', default: true }
};
const CONFIG = {
// ==================== 脚本基础信息 ====================
// 版本号(动态从GM_info获取)
get version() {
return GM_info?.script?.version || '1.0.0';
},
// ==================== 内部固定参数 (不建议修改) ====================
// 网络请求超时时间(毫秒):获取热门搜索词的最大等待时间
requestTimeout: 20 * 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,
// 任务点击相关状态(earn 日常任务 / dashboard 每日活动共用流程)
taskFlows: {
earn: { clicked: new Set(), retryCount: 0, processing: false },
dashboard: { clicked: new Set(), retryCount: 0, processing: false }
},
// APP 端任务相关状态(签到 + 资讯阅读)
appToken: '',
appTasks: {
checkInDone: false,
readDone: false,
readCurrent: 0,
readTotal: 0,
authRequired: false,
// 当日阅读进度是否已从服务端同步(页面生命周期内,避免重复查询)
readProgressSynced: false,
// 随机阅读是否正在执行(面板状态提示)
readRunning: false
}
};
// 旧参数迁移逻辑(一次性执行):将 earnTasks* 和 dashboardTasks* 参数合并为统一的 tasks* 参数
// 优先级规则:优先使用 earnTasks* 的值(它存在更早,更可能反映用户意图),若未设置则使用 dashboardTasks*,最后使用默认值
function migrateOldTaskParams() {
const params = ['ScrollDelay', 'MaxRetries', 'RetryDelay', 'CloseTabDelay'];
let migrated = false;
params.forEach(param => {
const newKey = 'customTasks' + param;
const oldEarnKey = 'customEarnTasks' + param;
const oldDashboardKey = 'customDashboardTasks' + param;
if (GM_getValue(newKey, undefined) !== undefined) {
return;
}
const earnVal = GM_getValue(oldEarnKey, undefined);
const dashboardVal = GM_getValue(oldDashboardKey, undefined);
if (earnVal !== undefined) {
GM_setValue(newKey, earnVal);
migrated = true;
} else if (dashboardVal !== undefined) {
GM_setValue(newKey, dashboardVal);
migrated = true;
}
try {
GM_deleteValue(oldEarnKey);
GM_deleteValue(oldDashboardKey);
} catch (e) {
console.log(`[Migration] 清理旧参数失败(非致命): ${e.message}`);
}
});
if (migrated) {
console.log('[Migration] 已完成任务点击参数迁移');
}
}
migrateOldTaskParams();
// ==================== APP 端任务模块(每日签到 + 资讯阅读) ====================
// APP 端协议常量(Rewards Platform 移动端接口契约)
const REWARDS_APP_SPEC = {
endpoints: {
activityReport: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
accountProfile: 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613',
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'
},
client: {
appId: 'SAAndroid/32.6.2110003560',
channel: 'SAAndroid',
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'
},
// 活动上报类型码:103=每日签到,101=资讯阅读
activities: {
checkIn: 103,
readArticle: 101
},
offers: {
checkIn: 'Gamification_Sapphire_DailyCheckIn',
readArticle: 'ENUS_readarticle3_30points'
},
// 令牌超过该天数后预防性续期
tokenMaxAgeDays: 7,
requestTimeout: 15 * 1000
};
/**
* APP 端网络请求封装(GM_xmlhttpRequest 的 Promise 形态)
* 非 2xx 响应抛出携带状态码的错误,供上层识别 401 等场景
*/
function appHttpRequest(options) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: options.method || 'GET',
url: options.url,
headers: options.headers || {},
data: options.data,
timeout: REWARDS_APP_SPEC.requestTimeout,
onload: res => {
if (res.status >= 200 && res.status < 300) {
resolve(res.responseText);
} else {
reject(new Error(`HTTP ${res.status}`));
}
},
onerror: () => reject(new Error('网络请求失败')),
ontimeout: () => 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 {}
try { history.replaceState({}, '', 'about:blank'); } 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 appHttpRequest({
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;
},
/**
* 确保内存中存在有效令牌:
* 补兑换暂存授权码 → 校验有效期(超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 = {
// 请求区域(锁定时固定国区)
getRegion() {
return CONFIG.appRegionLock ? 'cn' : String(GM_getValue('appAccountRegion', 'cn')).toLowerCase();
},
// 组装移动端公共请求头
buildHeaders(extra) {
return Object.assign({
'content-type': 'application/json; charset=UTF-8',
'user-agent': REWARDS_APP_SPEC.client.userAgent,
'x-rewards-appid': REWARDS_APP_SPEC.client.appId,
'x-rewards-ismobile': 'true',
'x-rewards-country': this.getRegion(),
'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 = this.getRegion();
try {
const res = await AppAuth.withAuth(token => appHttpRequest({
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: REWARDS_APP_SPEC.client.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 = this.getRegion();
try {
const res = await AppAuth.withAuth(token => appHttpRequest({
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: REWARDS_APP_SPEC.client.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 => appHttpRequest({
url: REWARDS_APP_SPEC.endpoints.accountProfile,
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 = {
// 当日日期戳(YYYYMMDD 数字形式)
getTodayNum() {
const now = new Date();
return Number(`${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`);
},
// 同步当日签到完成状态到内存(供面板渲染)
syncCheckInState() {
const today = this.getTodayNum();
state.appTasks.checkInDone = GM_getValue('appCheckInDate', 0) === today;
if (state.appTasks.checkInDone) {
state.appTasks.checkInPoints = GM_getValue('appCheckInPoints', 0);
}
},
getRetryCounters() {
return GM_getValue('appTaskRetryCounters', {});
},
// 重试计数跨日清零(保证每日重试额度与完成兜底可靠生效)
resetRetryCountersIfNewDay() {
const today = this.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 today = this.getTodayNum();
const checkInDone = !CONFIG.appCheckInEnabled || GM_getValue('appCheckInDate', 0) === today;
const readDone = !CONFIG.appReadEnabled || GM_getValue('appReadDate', 0) === today;
return checkInDone && readDone;
},
// 当日 APP 阅读已上报篇数(跨页面跳转持久化,受每日上限约束)
getReadReportedToday() {
if (GM_getValue('appReadReportedDate', 0) !== this.getTodayNum()) return 0;
return GM_getValue('appReadReportedCount', 0);
},
bumpReadReported() {
if (GM_getValue('appReadReportedDate', 0) !== this.getTodayNum()) {
GM_setValue('appReadReportedDate', this.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;
const today = this.getTodayNum();
if (state.appTasks.checkInDone) {
GM_log(`APP签到今日已完成(+${GM_getValue('appCheckInPoints', 0)}积分)`);
return;
}
if (!this.retryLeft('checkIn')) {
GM_log('APP签到重试次数已用尽,今日不再执行');
return;
}
const result = await AppApi.reportCheckIn();
if (result) {
const points = result.points || 0;
GM_setValue('appCheckInDate', today);
GM_setValue('appCheckInPoints', points);
state.appTasks.checkInDone = true;
state.appTasks.checkInPoints = points;
GM_log(points > 0
? `APP签到成功,+${points}积分`
: 'APP签到确认完成(今日已签,无新增积分)');
} else {
this.bumpRetry('checkIn');
GM_log('APP签到失败,稍后重试');
}
},
/**
* 搜索前随机阅读:随机上报 0-3 篇(受剩余缺口与每日上报上限约束)
* 失败不消耗重试预算(预算仅由兜底流程消耗,避免每日约20次调用放大耗尽)
* 进度优先从当日 GM 缓存恢复,未缓存时实时查询;当日已完成直接跳过
*/
async runRandomReads() {
if (!CONFIG.appReadEnabled) return;
const today = this.getTodayNum();
// 当日已完成:直接跳过(进度二次校验由搜索完成后的兜底流程负责)
if (GM_getValue('appReadDate', 0) === today) 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 !== this.getTodayNum()) return false;
state.appTasks.readCurrent = cache.current;
state.appTasks.readTotal = cache.total;
state.appTasks.readProgressSynced = true;
return true;
},
/**
* 同步当日真实阅读进度到内存(随机阅读与面板实时渲染共用)
* 返回进度对象;查询失败返回 null
*/
async syncReadProgress() {
const today = this.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 (GM_getValue('appReadDate', 0) === today) {
// 误标自愈:日期戳已标记完成但真实进度未达标,重置后继续
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 = this.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 new Promise(resolve => setTimeout(resolve, 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);
console.log(`Selected start parameter for today: ${startParam}`);
return startParam;
} else {
// 返回当天已选择的参数
const startParam = GM_getValue(todayStartParamKey);
console.log(`Using previously selected start parameter: ${startParam}`);
return startParam;
}
},
// 安全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')}`;
},
// 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).substr(2, 9);
},
// 获取精确的剩余时间(不受标签页激活状态影响)
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; // 转换为秒
},
// 检查页面是否可见
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');
}
};
/**
* 为按钮绑定悬停/按压样式(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 = 'https://cn.bing.com';
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;
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;
`;
const card = document.createElement('div');
card.style.cssText = `
background: #ffffff;
border-radius: 20px;
padding: 0;
max-width: 440px;
width: 92%;
box-shadow: 0 24px 80px rgba(0,0,0,0.3);
position: relative;
overflow: hidden;
animation: dialogSlideIn 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
`;
const header = document.createElement('div');
header.style.cssText = `
background: linear-gradient(135deg, #0067b8, #00bcf2);
padding: 28px 28px 20px;
color: #fff;
text-align: center;
`;
header.innerHTML = `
⭐
支持作者
您的 Star 是我持续更新的动力
`;
card.appendChild(header);
const body = document.createElement('div');
body.style.cssText = `
padding: 24px 28px 80px 28px;
position: relative;
`;
body.innerHTML = `
本脚本完全免费,如果对你有帮助,请给作者一个 Star 支持一下!
〒 本通知一周弹一次,如有打扰,非常抱歉。
`;
card.appendChild(body);
const btn = document.createElement('button');
btn.textContent = '前往支持';
btn.style.cssText = `
position: absolute;
bottom: 24px;
right: 28px;
padding: 12px 28px;
background: linear-gradient(135deg, #0067b8, #00bcf2);
color: #fff;
border: none;
border-radius: 40px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.2s;
box-shadow: 0 6px 20px rgba(0, 103, 184, 0.35);
letter-spacing: 0.3px;
`;
addButtonHoverEffects(btn, {
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)' }
});
btn.onclick = () => {
window.open('https://idbb98.github.io/microsoft-bing-rewards-daily-task-script/', '_blank');
overlay.remove();
};
body.appendChild(btn);
const closeBtn = document.createElement('button');
closeBtn.textContent = '✕';
closeBtn.style.cssText = `
position: absolute;
top: 12px;
right: 16px;
background: rgba(255,255,255,0.2);
border: none;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 18px;
color: #fff;
cursor: pointer;
transition: background 0.2s, transform 0.15s;
display: flex;
align-items: center;
justify-content: center;
`;
addButtonHoverEffects(closeBtn, {
enter: { background: 'rgba(255,255,255,0.35)', transform: 'scale(1.1)' },
leave: { background: 'rgba(255,255,255,0.2)', transform: 'scale(1)' }
});
closeBtn.onclick = () => overlay.remove();
header.appendChild(closeBtn);
overlay.appendChild(card);
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 = `
页面活跃
正在执行搜索任务...
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-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-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;
// 为展开/收缩按钮添加事件监听器
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();
}
});
// 添加悬停效果
toggleBtn.addEventListener('mouseenter', () => {
toggleBtn.style.backgroundColor = theme['--panel-hover-bg'];
toggleBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
const toggleIcon = document.getElementById('toggle-icon');
if (toggleIcon) {
toggleIcon.style.transform = state.isPanelCollapsed ? 'scale(1.1) rotate(0deg)' : 'scale(1.1) rotate(180deg)';
}
});
toggleBtn.addEventListener('mouseleave', () => {
toggleBtn.style.backgroundColor = 'transparent';
toggleBtn.style.boxShadow = 'none';
const toggleIcon = document.getElementById('toggle-icon');
if (toggleIcon) {
toggleIcon.style.transform = state.isPanelCollapsed ? 'scale(1) rotate(0deg)' : 'scale(1) rotate(180deg)';
}
});
toggleBtn.addEventListener('mousedown', () => {
const toggleIcon = document.getElementById('toggle-icon');
if (toggleIcon) {
toggleIcon.style.transform = state.isPanelCollapsed ? 'scale(0.95) rotate(0deg)' : 'scale(0.95) rotate(180deg)';
}
});
toggleBtn.addEventListener('mouseup', () => {
const toggleIcon = document.getElementById('toggle-icon');
if (toggleIcon) {
toggleIcon.style.transform = state.isPanelCollapsed ? 'scale(1.1) rotate(0deg)' : 'scale(1.1) rotate(180deg)';
}
});
}
// 为设置按钮添加事件监听器
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 appAuthIssuedAt = GM_getValue('appTokenIssuedAt', 0);
const appAuthStatusText = appAuthRefreshToken
? `已授权(${Math.floor((Date.now() - appAuthIssuedAt) / 86400000)}天前)`
: '未授权';
console.log('📋 加载最新设置:', {
searchFormParam: saved.searchFormParam,
panelCollapsed: saved.panelDefaultCollapsed,
maxSearches: saved.maxSearches,
randomAdd: saved.randomAddSearchWords,
randomAddFactor: saved.randomAddSearchWordsFactor,
randomCut: saved.randomCutSearchWords,
randomCutFactor: saved.randomCutSearchWordsFactor,
clickSearchResults: saved.clickSearchResults,
pauseInterval: `${saved.pauseIntervalMin}-${saved.pauseIntervalMax}`,
pauseTime: `${savedPauseTimeMin}-${savedPauseTimeMax}分钟`,
delay: `${savedMinDelay}-${savedMaxDelay}秒`,
autoClickTasks: saved.autoClickTasks
});
// 版本号(从CONFIG获取)
const currentVersion = CONFIG.version;
// 创建设置对话框
const dialog = document.createElement('div');
dialog.id = 'settings-dialog';
dialog.innerHTML = `
🚀
Bing Rewards 自动任务脚本
📌
版本: v${currentVersion}
👤
作者: Brian
✨
功能说明
-
🔍 自动搜索 - 自动执行必应搜索任务,获取每日积分
-
🎯 智能优化 - 支持随机加词、截词功能,模拟真实搜索行为
-
⏱️ 智能延迟 - 可配置的搜索间隔和暂停时间,避免触发风控
-
📊 进度追踪 - 实时显示任务进度和剩余时间
📜
使用条款与协议
本脚本仅供学习和个人使用。使用本脚本即表示您同意以下条款:
- 本脚本仅用于个人学习和研究目的
- 请勿用于商业用途或大规模部署
- 使用本脚本需遵守微软必应服务条款
- 作者不对使用本脚本造成的任何后果负责
- 建议合理使用,避免过度频繁操作
💬
联系与支持
如果您遇到问题或有改进建议,欢迎随时联系!
`;
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 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 appAuthStartBtn = document.getElementById('app-auth-start-btn');
const appAuthStatusEl = document.getElementById('app-auth-status');
// 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 issuedAt = GM_getValue('appTokenIssuedAt', 0);
const pendingCode = GM_getValue('appPendingAuthCode', '');
if (refreshToken) {
const days = Math.max(0, Math.floor((Date.now() - issuedAt) / 86400000));
appAuthStatusEl.textContent = `✓ 已授权(${days}天前)`;
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 performSearch = (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';
performSearch('');
};
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;
performSearch(keyword);
// 显示/隐藏清除按钮
clearSearchBtn.style.display = keyword.trim() ? 'block' : 'none';
});
clearSearchBtn.addEventListener('click', () => {
searchInput.value = '';
clearSearchBtn.style.display = 'none';
performSearch('');
});
// 设置分组折叠/展开功能
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: '配置文件已保存到本地',
icon: '📥',
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];
});
// 向后兼容:从旧配置格式(earnTasks* / dashboardTasks*)迁移到新统一格式(tasks*)
// 优先级:新格式 tasks* > 旧格式 earnTasks* > 旧格式 dashboardTasks*
['ScrollDelay', 'MaxRetries', 'RetryDelay', 'CloseTabDelay'].forEach(suffix => {
if (config['tasks' + suffix] === undefined) {
const oldVal = config['earnTasks' + suffix] !== undefined
? config['earnTasks' + suffix]
: config['dashboardTasks' + suffix];
if (oldVal !== undefined) CONFIG['tasks' + suffix] = oldVal;
}
});
GM_notification({
title: '配置导入成功',
text: '配置已成功导入,页面将刷新',
icon: '📤',
timeout: 3000
});
closeDialog();
setTimeout(() => window.location.reload(), 2000);
} catch (error) {
alert('❌ 配置文件格式错误,请确保导入的是有效的JSON配置文件');
console.error('配置导入失败:', error);
}
};
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';
});
});
// Radio按钮样式更新
Array.from(panelStateRadios).forEach(radio => {
const label = radio.closest('label');
// 初始化:为已选中的 radio 添加勾选标记
if (radio.checked) {
const isExpanded = radio.value === 'expanded';
label.style.borderColor = theme['--panel-primary-color'];
label.style.background = isExpanded ? `linear-gradient(135deg,${theme['--panel-success-bg']},${theme['--panel-hover-bg']})` : `linear-gradient(135deg,${theme['--panel-info-bg']},${theme['--panel-hover-bg']})`;
const checkmark = document.createElement('div');
checkmark.className = 'checkmark';
checkmark.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;`;
checkmark.innerHTML = '✓';
label.appendChild(checkmark);
}
radio.addEventListener('change', () => {
// 遍历所有radio,更新它们的样式和勾选标记
Array.from(panelStateRadios).forEach(r => {
const lbl = r.closest('label');
if (!lbl) return;
// 先移除旧的勾选标记
const oldCheckmark = lbl.querySelector('.checkmark');
if (oldCheckmark) {
oldCheckmark.remove();
}
if (r.checked) {
// 选中状态:更新样式并添加勾选标记
const isExpanded = r.value === 'expanded';
lbl.style.borderColor = theme['--panel-primary-color'];
lbl.style.background = isExpanded ? `linear-gradient(135deg,${theme['--panel-success-bg']},${theme['--panel-hover-bg']})` : `linear-gradient(135deg,${theme['--panel-info-bg']},${theme['--panel-hover-bg']})`;
// 添加新的勾选标记
const checkmark = document.createElement('div');
checkmark.className = 'checkmark';
checkmark.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;`;
checkmark.innerHTML = '✓';
lbl.appendChild(checkmark);
} else {
// 未选中状态:恢复默认样式
lbl.style.borderColor = theme['--panel-border'];
lbl.style.background = theme['--panel-bg'];
}
});
});
// 添加悬停效果
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';
});
});
// Checkbox卡片样式更新
[randomAddCheckbox, randomCutCheckbox, clickSearchResultsCheckbox, autoClickTasksCheckbox, appCheckInCheckbox, appReadCheckbox].forEach(checkbox => {
if (!checkbox) return;
const label = checkbox.closest('label');
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
label.style.borderColor = theme['--panel-primary-color'];
let bgColor = theme['--panel-info-bg'];
if (checkbox.id === 'random-cut-checkbox') {
bgColor = theme['--panel-success-bg'];
} else if (checkbox.id === 'click-search-results-checkbox') {
bgColor = theme['--panel-success-bg'];
} else if (checkbox.id === 'auto-click-tasks-checkbox') {
bgColor = theme['--panel-success-bg'];
} else if (checkbox.id === 'app-checkin-checkbox') {
bgColor = theme['--panel-success-bg'];
} else if (checkbox.id === 'app-read-checkbox') {
bgColor = theme['--panel-success-bg'];
}
label.style.background = `linear-gradient(135deg,${bgColor},transparent)`;
// 添加已启用标记
let badge = label.querySelector('.badge');
if (!badge) {
badge = document.createElement('div');
badge.className = 'badge';
badge.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;`;
badge.textContent = '已启用';
label.appendChild(badge);
}
} else {
label.style.borderColor = theme['--panel-border'];
label.style.background = theme['--panel-hover-bg'];
// 移除已启用标记
const badge = label.querySelector('.badge');
if (badge) badge.remove();
}
});
// 添加悬停效果
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;
tasksMaxRetriesInput.value = 0;
tasksRetryDelayInput.value = 2000;
tasksCloseTabDelayInput.value = 1500;
autoClickTasksCheckbox.checked = false;
appCheckInCheckbox.checked = false;
appReadCheckbox.checked = false;
appReadLimitInput.value = 10;
// 触发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 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);
// 验证
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 (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 (!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.tasksMaxRetries = tasksMaxRetries;
CONFIG.tasksRetryDelay = tasksRetryDelay;
CONFIG.tasksCloseTabDelay = tasksCloseTabDelay;
CONFIG.autoClickTasks = autoClickTasks;
CONFIG.appCheckInEnabled = appCheckInEnabled;
CONFIG.appReadEnabled = appReadEnabled;
CONFIG.appReadDailyLimit = appReadDailyLimit;
console.log('💾 保存设置:', {
searchFormParam,
panelDefaultCollapsed,
maxSearches,
randomAdd,
randomAddFactor,
randomCut,
randomCutFactor,
clickSearchResults,
pauseInterval: `${pauseIntervalMin}-${pauseIntervalMax}`,
pauseTime: `${pauseTimeMin/60000}-${pauseTimeMax/60000}分钟`,
delay: `${minDelay/1000}-${maxDelay/1000}秒`,
tasks: {
scrollDelay: `${tasksScrollDelay}ms`,
maxRetries: tasksMaxRetries,
retryDelay: `${tasksRetryDelay}ms`,
closeTabDelay: `${tasksCloseTabDelay}ms`
},
autoClickTasks,
appTasks: {
checkInEnabled: appCheckInEnabled,
readEnabled: appReadEnabled,
readDailyLimit: appReadDailyLimit
}
});
// 显示成功提示
alert('✅ 配置已保存!确认后页面将在3秒后刷新以应用新配置...');
// 关闭对话框
closeDialog();
// 延迟刷新页面
setTimeout(() => {
window.location.reload();
}, 3000);
});
// ESC键关闭
const handleEsc = (e) => {
if (e.key === 'Escape') {
closeDialog();
document.removeEventListener('keydown', handleEsc);
}
};
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 = () => pill('待执行', '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 (authPending) {
value = pillWarn();
} else if (state.appTasks.checkInDone) {
const points = state.appTasks.checkInPoints || GM_getValue('appCheckInPoints', 0);
value = pillSuccess(points > 0 ? `✓ +${points}分` : '✓ 已签');
} else {
value = pillMuted();
}
cols.push(`📱 APP签到${value}
`);
}
if (CONFIG.appReadEnabled) {
let value;
if (authPending) {
value = pillWarn();
} else 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 (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 = [
{ label: '🖱️ 日常任务', done: isTaskFlowCompletedToday('earn') },
{ label: '📅 每日活动', done: isTaskFlowCompletedToday('dashboard') }
];
flows.forEach(f => {
cols.push(`${f.label}${
f.done ? pillSuccess('✓ 完成') : pillMuted()
}
`);
});
}
return `
${cols.join('')}
`;
}
/**
* 实时刷新面板任务数据(签到状态 + APP 阅读进度)
* 面板创建/展开时调用,确保页面刷新后面板信息实时获取
*/
async function refreshAppTaskPanelData() {
if (!CONFIG.appCheckInEnabled && !CONFIG.appReadEnabled) return;
// 签到状态从本地日期戳同步
AppTaskRunner.syncCheckInState();
// 当日阅读已完成:恢复缓存进度即可,不再实时查询(进度二次校验由兜底流程负责)
if (CONFIG.appReadEnabled && GM_getValue('appReadDate', 0) === AppTaskRunner.getTodayNum()) {
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();
}
/**
* 更新状态面板
*/
function updateStatusPanel(data = {}) {
if (!state.statusPanel) return;
const taskStatus = getTaskStatus();
const content = document.getElementById('panel-content');
const pageStatus = document.getElementById('page-status');
const countdownElement = document.getElementById('panel-countdown');
const { currentWord = '', pauseTimeLeft = null } = data;
// 更新页面状态指示器
const taskRunningStatus = document.getElementById('task-running-status');
if (utils.isPageVisible()) {
pageStatus.innerHTML = ' 页面活跃';
pageStatus.style.color = 'var(--panel-success-text,#107c10)';
} else {
pageStatus.innerHTML = ' 后台运行';
pageStatus.style.color = 'var(--panel-text-muted,#666)';
}
// 更新任务执行状态显示
if (taskRunningStatus) {
if (state.isRunning && !pauseTimeLeft && !taskStatus.isCompleted) {
taskRunningStatus.style.display = 'flex';
} else {
taskRunningStatus.style.display = 'none';
}
}
const progress = taskStatus.overallProgress;
// 计算剩余时间(使用精确计时)
const remainingTime = utils.getAccurateRemainingTime();
// 更新收缩状态的倒计时显示
if (countdownElement) {
if (state.isPanelCollapsed) {
// 面板收缩时显示倒计时
if (taskStatus.isCompleted) {
// 任务已完成
countdownElement.textContent = '✅ 已完成';
countdownElement.style.color = 'var(--panel-success-text,#107c10)';
} else if (pauseTimeLeft !== null && pauseTimeLeft > 0) {
// 暂停中 - 显示暂停倒计时
const minutes = Math.floor(pauseTimeLeft / 60);
const seconds = Math.round(pauseTimeLeft % 60);
countdownElement.textContent = `⏸️ ${minutes}:${seconds.toString().padStart(2, '0')}`;
countdownElement.style.color = 'var(--panel-warning-text,#8a6900)';
} else if (remainingTime > 0 && state.isRunning) {
// 执行中 - 显示下次搜索倒计时
countdownElement.textContent = `⏱️ ${remainingTime.toFixed(0)}s`;
countdownElement.style.color = 'var(--panel-info-text,#005a9e)';
} else if (!state.isRunning) {
// 未运行
countdownElement.textContent = '⏹️ 已停止';
countdownElement.style.color = 'var(--panel-text-muted,#999)';
} else {
countdownElement.textContent = '';
}
} else {
// 面板展开时隐藏倒计时
countdownElement.textContent = '';
}
}
content.innerHTML = `
${getTaskSummaryPanelHtml()}
📊 搜索进度
${taskStatus.currentCount}/${taskStatus.maxCount}
${progress > 10 ? '' + progress + '%' : ''}
${state.appTasks.readRunning ? `
📖
APP阅读执行中${state.appTasks.readTotal > 0 ? ` ${state.appTasks.readCurrent}/${state.appTasks.readTotal}分` : ''},完成后继续搜索
` : ''}
${taskStatus.isCompleted ? `
✅
今日任务已完成
` : ''}
${pauseTimeLeft !== null ? `
⏸️
暂停中
${Math.floor(pauseTimeLeft/60)}分${Math.round(pauseTimeLeft%60)}秒后继续
` : ''}
${!pauseTimeLeft && currentWord && remainingTime > 0 ? `
🔍 下个搜索词
${remainingTime.toFixed(0)}秒后
${utils.escapeHtml(currentWord)}
` : ''}
`;
}
/**
* 获取热门搜索词
*/
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
.filter(item => item.desc && !item.desc.match(/[\u4e00-\u9fa5]/) || item.desc.match(/[\u4e00-\u9fa5]/))
.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
const promises = sources.map(source =>
new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET",
url: source.url,
timeout: CONFIG.requestTimeout,
onload: res => {
if (res.status === 200) {
try {
const data = utils.safeJsonParse(res.responseText, {});
const words = source.parser(data).filter(word =>
word &&
word.length >= 2 &&
word.length <= 30 &&
!/^[0-9]+$/.test(word) // 过滤纯数字
);
GM_log(`从 ${source.name} 获取到 ${words.length} 个热词`);
resolve(words);
} catch (e) {
GM_log(`解析 ${source.name} 数据失败: ${e.message}`);
resolve([]);
}
} else {
GM_log(`${source.name} 请求失败: HTTP ${res.status}`);
resolve([]);
}
},
onerror: () => {
GM_log(`${source.name} 请求出错`);
resolve([]);
},
ontimeout: () => {
GM_log(`${source.name} 请求超时`);
resolve([]);
}
});
})
);
// 等待所有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 searchCount = GM_getValue('searchCount', 0);
return {
currentCount: searchCount,
maxCount: CONFIG.maxSearches,
isCompleted: searchCount >= CONFIG.maxSearches,
overallProgress: Math.round((searchCount / CONFIG.maxSearches) * 100)
};
}
/**
* 执行搜索任务
*/
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();
}
updateStatusPanel();
GM_notification({ text: "Bing Rewards 任务已完成", title: "任务完成", 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.countdownStartTime = Date.now();
state.countdownDuration = delay;
// 更新面板
updateStatusPanel({ currentWord: processedSearchWord });
// 使用精确计时器,不受页面可见性影响
utils.addTimer(setTimeout(() => {
utils.clearAllTimers();
// 搜索执行前随机完成 0-3 次 APP 阅读上报(阅读开关开启且当日未完成时),完成后继续搜索
AppTaskRunner.runRandomReads().finally(() => {
performSearch(processedSearchWord, taskStatus);
});
}, delay));
// 添加一个定期更新面板的定时器(每秒更新一次)
utils.addTimer(setInterval(() => {
updateStatusPanel({ currentWord: processedSearchWord });
}, 1000));
}
/**
* 执行搜索
*/
function performSearch(searchWord, taskStatus) {
const nextCount = taskStatus.currentCount + 1;
const counterKey = 'searchCount';
GM_setValue(counterKey, nextCount);
GM_log(`搜索: ${searchWord} (${nextCount}/${taskStatus.maxCount})`);
// 重置倒计时状态
state.countdownStartTime = 0;
state.countdownDuration = 0;
// 随机暂停间隔检查
// 生成本次搜索周期内的暂停间隔(只在首次搜索时确定,之后保持不变直到完成一次完整搜索)
let currentPauseInterval = GM_getValue('currentPauseInterval', null);
if(currentPauseInterval === null) {
currentPauseInterval = utils.getRandomPauseInterval();
GM_setValue('currentPauseInterval', currentPauseInterval);
}
if (nextCount % currentPauseInterval === 0) {
// 每次暂停时生成新的随机暂停时间
const pauseTime = utils.getRandomPauseTime();
let pauseTimeLeft = pauseTime / 1000;
updateStatusPanel({ pauseTimeLeft });
// 使用精确的暂停计时
const pauseStartTime = Date.now();
const pauseTimer = utils.addTimer(setInterval(() => {
const elapsed = Date.now() - pauseStartTime;
pauseTimeLeft = Math.max(0, (pauseTime - elapsed) / 1000);
updateStatusPanel({ pauseTimeLeft });
if (pauseTimeLeft <= 0) {
utils.clearAllTimers();
// 完成暂停后,重新生成下一个暂停间隔
const newPauseInterval = utils.getRandomPauseInterval();
GM_setValue('currentPauseInterval', newPauseInterval);
window.location.href = buildSearchUrl(searchWord);
}
}, 1000));
} else {
window.location.href = buildSearchUrl(searchWord);
}
}
/**
* 页面加载完成后执行随机滚动,模拟真实用户行为
* 随机滚动多次,方向(上滑/下滑)和次数都是随机的
*/
function randomScrollAfterPageLoad() {
// 等待页面内容完全加载
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++) {
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) {
setTimeout(() => {
checkAndClickSearchResult();
}, 1500); // 等待滚动动画完成
}
}, i * 1000); // 每次滚动间隔1秒,模拟真实用户操作
}
} else {
// 页面无法滚动,直接检查并点击链接
checkAndClickSearchResult();
}
}, 2500); // 等待2.5秒让页面内容加载完成
}
/**
* 检查当前页面是否为搜索结果页且包含启动参数,如果是则点击搜索结果链接
*/
function checkAndClickSearchResult() {
try {
// 前置检查
if (!CONFIG.clickSearchResults) 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}`);
console.error(error);
}
}
/**
* 从标准搜索结果中查找可点击的链接
* @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';
if (typeof GM_openInTab !== 'undefined') {
const newTab = GM_openInTab(link.href, {
active: false,
insert: true,
setParent: true
});
GM_log('已通过 GM_openInTab 打开链接');
const closeDelay = CONFIG.tasksCloseTabDelay || 1500;
setTimeout(() => {
try {
if (newTab && typeof newTab.close === 'function') {
newTab.close();
GM_log('已通过 tab.close() 关闭搜索结果标签页');
} else if (typeof GM_closeTab !== 'undefined') {
GM_saveTab(newTab).then(savedTab => {
if (savedTab && typeof savedTab.close === 'function') {
savedTab.close();
GM_log('已通过 savedTab.close() 关闭搜索结果标签页');
}
}).catch(() => {});
}
} catch (e) {
GM_log(`关闭搜索结果标签页失败: ${e.message}`);
}
}, closeDelay);
} else {
const tempLink = document.createElement('a');
tempLink.href = link.href;
tempLink.target = '_blank';
tempLink.rel = 'noopener noreferrer';
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
GM_log('已通过临时链接打开(GM_openInTab 不可用)');
}
} catch (error) {
GM_log(`点击出错: ${error.message}`);
console.error(error);
}
}
/**
* 任务点击流程配置(earn 日常任务与 dashboard 每日活动共用一套点击流程)
* dashboard 的 label 含前导空格,用于保持日志文案与历史版本一致
*/
const TASK_FLOW_CONFIG = {
earn: {
logPrefix: '[EarnTasks]',
pageUrl: 'https://rewards.bing.com/earn',
completedKey: 'earnTasksCompleted',
lastDateKey: 'lastEarnTasksDate',
label: '日常任务',
notificationTitle: 'Bing Rewards 日常任务'
},
dashboard: {
logPrefix: '[DashboardTasks]',
pageUrl: 'https://rewards.bing.com/dashboard',
completedKey: 'dashboardTasksCompleted',
lastDateKey: 'lastDashboardTasksDate',
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') {
console.log(`${logPrefix} 关闭当前标签页`);
GM_closeTab();
}
}
/**
* 标记任务点击流程当日已完成(完成标记 + 日期戳,供每日仅执行一次判断)
*/
function markTaskFlowCompleted(flowName) {
const conf = TASK_FLOW_CONFIG[flowName];
GM_setValue(conf.completedKey, true);
GM_setValue(conf.lastDateKey, utils.getTodayStr());
}
/**
* 任务点击流程当日是否已完成(每日仅执行一次)
*/
function isTaskFlowCompletedToday(flowName) {
const conf = TASK_FLOW_CONFIG[flowName];
return GM_getValue(conf.lastDateKey, '') === utils.getTodayStr() &&
GM_getValue(conf.completedKey, false);
}
/**
* 检查是否需要执行任务页点击,打开对应页面并等待处理完成(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)) {
console.log(`今日${conf.label}点击已完成,跳过`);
return true;
}
console.log(`准备执行${conf.label}点击...`);
return new Promise((resolve) => {
const taskParam = utils.getRandomStartParam();
const taskTab = GM_openInTab(`${conf.pageUrl}?${taskParam}=1`, {
active: true,
insert: true,
setParent: true
});
const checkInterval = setInterval(() => {
if (GM_getValue(conf.completedKey, false)) {
clearInterval(checkInterval);
GM_setValue(conf.lastDateKey, today);
console.log(`${conf.label}点击已完成`);
resolve(true);
}
}, 1000);
// 超时兜底:防止页面卡死导致主流程阻塞
setTimeout(() => {
clearInterval(checkInterval);
if (taskTab && typeof taskTab.close === 'function') {
try {
taskTab.close();
} catch (e) {
console.log('关闭标签页失败:', e);
}
}
resolve(true);
}, 30000);
});
}
/**
* 滚动到日常任务区域
*/
function scrollToDailyTasks() {
console.log('[EarnTasks] 正在滚动到日常任务区域...');
const moreActivitiesSection = findMoreActivitiesSection();
if (moreActivitiesSection) {
moreActivitiesSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
console.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' });
console.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' });
console.log('[EarnTasks] 通过标题找到并滚动到日常任务区域');
return true;
}
window.scrollTo({
top: document.body.scrollHeight * 0.3,
behavior: 'smooth'
});
console.log('[EarnTasks] 使用默认滚动位置');
return false;
}
/**
* 查找未完成的有积分任务卡片
*/
function findIncompleteTaskCards() {
console.log('[EarnTasks] 正在查找未完成的任务卡片...');
const moreActivitiesSection = findMoreActivitiesSection();
if (!moreActivitiesSection) {
console.log('[EarnTasks] 未找到 moreactivities 区域');
return [];
}
console.log('[EarnTasks] 找到 moreactivities 区域,开始查找任务卡片');
const taskCards = moreActivitiesSection.querySelectorAll('a[href][target="_blank"]');
console.log(`[EarnTasks] 在 moreactivities 区域找到 ${taskCards.length} 个可能的任务链接`);
const incompleteTasks = [];
const processedHrefs = new Set();
taskCards.forEach((taskLink, index) => {
const href = taskLink.getAttribute('href');
if (!href || processedHrefs.has(href)) {
return;
}
const taskText = taskLink.textContent || taskLink.innerText || '';
const taskHtml = taskLink.outerHTML;
const isCompleted = /已完成|complete|completed|✓|✔|done|finished/i.test(taskText) ||
taskLink.querySelector('[class*="complete"]') ||
taskLink.querySelector('[class*="Complete"]') ||
taskLink.querySelector('[class*="done"]') ||
taskLink.querySelector('[class*="Done"]') ||
taskLink.getAttribute('aria-label')?.includes('完成');
if (isCompleted) {
console.log(`[EarnTasks] 任务已完成,跳过: ${taskText.substring(0, 30)}...`);
return;
}
const pointsMatch = taskText.match(/\+(\d+)/) ||
taskHtml.match(/\+(\d+)/);
if (!pointsMatch) {
return;
}
const points = parseInt(pointsMatch[1]);
if (points <= 0) {
return;
}
processedHrefs.add(href);
const taskId = href;
incompleteTasks.push({
element: taskLink,
href: href,
points: points,
taskId: taskId,
text: taskText.substring(0, 100)
});
console.log(`[EarnTasks] 找到未完成任务: ${taskText.substring(0, 50)}... (+${points}分)`);
});
console.log(`[EarnTasks] 共找到 ${incompleteTasks.length} 个未完成的有积分任务`);
return incompleteTasks;
}
/**
* 点击任务卡片(earn 与 dashboard 共用)
*/
async function clickTask(task, flowName) {
const { logPrefix } = TASK_FLOW_CONFIG[flowName];
const flowState = state.taskFlows[flowName];
if (flowState.clicked.has(task.taskId)) {
console.log(`${logPrefix} 任务 ${task.taskId} 已点击过,跳过`);
return false;
}
console.log(`${logPrefix} 正在点击任务: ${task.text.substring(0, 50)}...${task.points > 0 ? ` (+${task.points}分)` : ''}`);
try {
task.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await new Promise(resolve => setTimeout(resolve, 50));
const rect = task.element.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
console.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 new Promise(resolve => setTimeout(resolve, 100));
flowState.clicked.add(task.taskId);
console.log(`${logPrefix} 成功点击任务: ${task.taskId}`);
try {
const taskWindow = window.open('', windowName);
if (taskWindow && !taskWindow.closed) {
taskWindow.close();
console.log(`${logPrefix} 已关闭任务标签页: ${task.taskId}`);
}
} catch (e) {
console.log(`${logPrefix} 关闭任务标签页失败: ${e.message}`);
}
if (originalTarget) {
task.element.setAttribute('target', originalTarget);
} else {
task.element.removeAttribute('target');
}
return true;
} catch (error) {
console.log(`${logPrefix} 模拟点击失败: ${error.message}`);
try {
console.log(`${logPrefix} 尝试备用方案: element.click()`);
task.element.click();
flowState.clicked.add(task.taskId);
console.log(`${logPrefix} 备用方案成功: ${task.taskId}`);
await new Promise(resolve => setTimeout(resolve, 50));
return true;
} catch (fallbackError) {
console.log(`${logPrefix} 备用方案也失败: ${fallbackError.message}`);
return false;
}
}
}
/**
* 处理任务点击(earn 与 dashboard 共用流程)
* @param {string} flowName 流程标识:'earn' 或 'dashboard'
* @param {Function} findTasks 异步函数,返回待办任务数组;返回 null 表示任务区域缺失,直接结束
*/
async function processTasks(flowName, findTasks) {
const conf = TASK_FLOW_CONFIG[flowName];
const flowState = state.taskFlows[flowName];
// 当日已完成则跳过(每日仅执行一次)
if (isTaskFlowCompletedToday(flowName)) {
console.log(`今日${conf.label}点击已完成,跳过`);
await new Promise(resolve => setTimeout(resolve, 50));
closeCurrentTab(conf.logPrefix);
return;
}
if (flowState.processing) {
console.log(`${conf.logPrefix} 正在处理中,跳过`);
return;
}
flowState.processing = true;
console.log(`${conf.logPrefix} 开始处理任务...`);
const tasks = await findTasks();
// 任务区域缺失(DOM 结构变化),直接标记完成并关闭页面
if (tasks === null) {
markTaskFlowCompleted(flowName);
flowState.processing = false;
await new Promise(resolve => setTimeout(resolve, 50));
closeCurrentTab(conf.logPrefix);
return;
}
if (tasks.length === 0) {
console.log(`${conf.logPrefix} 没有找到未完成任务`);
// 未找到任务时等待页面加载后重试
if (flowState.retryCount < CONFIG.tasksMaxRetries) {
flowState.retryCount++;
console.log(`${conf.logPrefix} 等待 ${CONFIG.tasksRetryDelay}ms 后重试 (${flowState.retryCount}/${CONFIG.tasksMaxRetries})`);
await new Promise(resolve => setTimeout(resolve, CONFIG.tasksRetryDelay));
flowState.processing = false;
return processTasks(flowName, findTasks);
}
console.log(`${conf.logPrefix} 已达到最大重试次数,确认没有未完成任务`);
GM_setValue(conf.completedKey, true);
console.log(`${conf.logPrefix} 已设置完成标记`);
flowState.processing = false;
await new Promise(resolve => setTimeout(resolve, 50));
closeCurrentTab(conf.logPrefix);
return;
}
console.log(`${conf.logPrefix} 找到 ${tasks.length} 个任务,开始逐个处理...`);
for (const task of tasks) {
await clickTask(task, flowName);
}
markTaskFlowCompleted(flowName);
console.log(`${conf.logPrefix} 已设置完成标记`);
flowState.processing = false;
console.log(`${conf.logPrefix} 任务处理完成`);
if (typeof GM_notification !== 'undefined') {
GM_notification({
text: `已完成 ${flowState.clicked.size} 个${conf.label}点击`,
title: conf.notificationTitle,
timeout: 3000
});
}
await new Promise(resolve => setTimeout(resolve, CONFIG.tasksCloseTabDelay));
closeCurrentTab(conf.logPrefix);
}
/**
* earn 页面任务查找:滚动定位 → 等待加载 → 查找未完成任务卡片
*/
async function findAndPrepareEarnTasks() {
scrollToDailyTasks();
await new Promise(resolve => setTimeout(resolve, CONFIG.tasksScrollDelay));
return findIncompleteTaskCards();
}
/**
* dashboard 页面任务查找:等待 #dailyset 加载 → 滚动定位 → 查找未完成任务卡片
* 区域缺失时返回 null(与「未找到任务」区分,不重试直接结束)
*/
async function findAndPrepareDashboardTasks() {
// 等待 #dailyset 区域加载,应对页面元素加载延迟
const dailySetSection = await waitForDashboardDailySet();
if (!dailySetSection) {
console.log('[DashboardTasks] DOM 结构变化,未找到 #dailyset 区域,结束处理');
return null;
}
scrollToDashboardDailySet(dailySetSection);
await new Promise(resolve => setTimeout(resolve, CONFIG.tasksScrollDelay));
return findIncompleteDashboardTasks(dailySetSection);
}
/**
* 等待 #dailyset 区域加载完成(应对页面元素加载延迟)
* 通过轮询 + MutationObserver 双重机制检测,超时后返回 null
* 不仅等待 #dailyset 区域出现,还要等待任务卡片实际加载完成
*/
function waitForDashboardDailySet(timeout = 25000) {
return new Promise((resolve) => {
const selectors = [
'#dailyset',
'section[id="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()) {
console.log('[DashboardTasks] #dailyset 区域已加载完成');
resolve(dailySetSection);
return;
}
let observer = null;
const startTime = Date.now();
const checkAndResolve = () => {
dailySetSection = findDailySet();
if (dailySetSection) {
// 如果没有 loading 占位符,或者有实际任务链接,说明加载完成
if (!hasLoadingPlaceholders() || hasTaskLinks()) {
console.log('[DashboardTasks] #dailyset 区域及任务卡片已加载完成');
if (observer) observer.disconnect();
resolve(dailySetSection);
return true;
}
}
// 检查超时
if (Date.now() - startTime >= timeout) {
console.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) {
console.log('[DashboardTasks] 正在滚动到每日活动区域...');
if (!dailySetSection) {
console.log('[DashboardTasks] 未传入 #dailyset 区域,尝试重新查找');
return false;
}
try {
dailySetSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
console.log('[DashboardTasks] 已滚动到 #dailyset 每日活动区域');
return true;
} catch (error) {
console.log(`[DashboardTasks] 滚动失败: ${error.message}`);
return false;
}
}
/**
* 查找 #dailyset 区域内未完成的任务卡片
* 复用 earn 页面任务识别逻辑,针对 dashboard 的 DOM 结构与状态文案进行调整
*/
function findIncompleteDashboardTasks(dailySetSection) {
console.log('[DashboardTasks] 正在查找未完成的任务卡片...');
if (!dailySetSection) {
console.log('[DashboardTasks] #dailyset 区域不存在');
return [];
}
// 检查是否还有 loading 占位符
const placeholders = dailySetSection.querySelectorAll('.animate-pulse, [class*="pulse"], [class*="skeleton"], [class*="placeholder"]');
if (placeholders.length > 0) {
console.log('[DashboardTasks] 任务卡片仍在加载中(检测到 loading 占位符),返回空数组');
return [];
}
// 支持多种任务卡片选择器:a[href] 链接以及可能的其他卡片结构
const taskCards = dailySetSection.querySelectorAll('a[href], [role="button"], .card, [class*="card"]');
console.log(`[DashboardTasks] 在 #dailyset 区域找到 ${taskCards.length} 个可能的任务元素`);
const incompleteTasks = [];
const processedHrefs = new Set();
const processedElements = new Set();
// dashboard 的完成状态文案(来自 ActivityCard.Status 国际化资源)
// completed=已完成, inProgress=正在进行, notStarted=未开始, activated=已激活, locked=已锁定
const completedPattern = /已完成|complete|completed|✓|✔|done|finished/i;
const lockedPattern = /已锁定|locked/i;
// 通过进度条判断未完成任务(格式如 "X/Y");边界断言排除完整日期形态(如 8/28/2026)
const progressPattern = /(? {
// 跳过已处理的元素
if (processedElements.has(taskElement)) return;
processedElements.add(taskElement);
const href = taskElement.getAttribute('href');
// 如果是链接元素,检查是否需要跳过
if (href) {
if (href === '/earn' || href.startsWith('#') || processedHrefs.has(href)) {
return;
}
processedHrefs.add(href);
}
const taskText = taskElement.textContent || taskElement.innerText || '';
const taskHtml = taskElement.outerHTML;
const ariaLabel = taskElement.getAttribute('aria-label') || '';
const role = taskElement.getAttribute('role') || '';
// 跳过文本内容太少的元素(可能是图标或装饰元素)
if (taskText.trim().length < 2) return;
// 完成状态检测:文本、子元素 class、aria-label 多重判定
const isCompleted = completedPattern.test(taskText) ||
completedPattern.test(ariaLabel) ||
taskElement.querySelector('[class*="complete"]') ||
taskElement.querySelector('[class*="Complete"]') ||
taskElement.querySelector('[class*="done"]') ||
taskElement.querySelector('[class*="Done"]');
if (isCompleted) {
console.log(`[DashboardTasks] 任务已完成,跳过: ${taskText.substring(0, 30)}...`);
return;
}
// 锁定状态任务不可点击,跳过
const isLocked = lockedPattern.test(taskText) ||
lockedPattern.test(ariaLabel) ||
taskElement.querySelector('[class*="lock"]') ||
taskElement.getAttribute('aria-disabled') === 'true';
if (isLocked) {
console.log(`[DashboardTasks] 任务已锁定,跳过: ${taskText.substring(0, 30)}...`);
return;
}
// 通过进度条判断未完成任务(格式如 "X/Y")
const progressMatch = taskText.match(progressPattern);
let isIncompleteByProgress = false;
if (progressMatch && progressMatch[1] !== progressMatch[2]) {
isIncompleteByProgress = true;
}
// 积分识别:+N 形式
const pointsMatch = taskText.match(/\+(\d+)/) || taskHtml.match(/\+(\d+)/);
const points = pointsMatch ? parseInt(pointsMatch[1]) : 0;
// 只有当有积分或通过进度识别为未完成时,才视为可点击任务
if (points > 0 || isIncompleteByProgress || (href && href.includes('task'))) {
const taskId = href || taskElement.id || taskElement.className || Date.now().toString(36);
incompleteTasks.push({
element: taskElement,
href: href,
points: points,
taskId: taskId,
text: taskText.substring(0, 100)
});
console.log(`[DashboardTasks] 找到未完成任务: ${taskText.substring(0, 50)}...${points > 0 ? ` (+${points}分)` : ''}`);
}
});
console.log(`[DashboardTasks] 共找到 ${incompleteTasks.length} 个未完成任务`);
return incompleteTasks;
}
/**
* 检查并启动任务
*/
async function checkAndStartTask() {
// 使用每日生成的启动参数(earn/dashboard/搜索 保持一致)
const startParam = utils.getRandomStartParam();
const urlParams = new URLSearchParams(window.location.search);
// 如果是 rewards.bing.com/earn 页面,执行日常任务点击(开关关闭时不执行)
if (isRewardsPage('/earn')) {
if (CONFIG.autoClickTasks && urlParams.has(startParam)) {
console.log('[EarnTasks] 检测到自动处理标记,开始执行日常任务点击');
await new Promise(resolve => setTimeout(resolve, 50));
await processTasks('earn', findAndPrepareEarnTasks);
}
return;
}
// 如果是 rewards.bing.com/dashboard 页面,执行每日活动区域任务点击(开关关闭时不执行)
if (isRewardsPage('/dashboard')) {
if (CONFIG.autoClickTasks && urlParams.has(startParam)) {
console.log('[DashboardTasks] 检测到自动处理标记,开始执行每日活动区域任务点击');
await new Promise(resolve => setTimeout(resolve, 50));
await processTasks('dashboard', findAndPrepareDashboardTasks);
}
return;
}
// 搜索页面的处理逻辑
// 检查是否有当天的启动参数标记
const hasStartParam = urlParams.has(startParam);
console.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();
console.log(`启动任务: ${startParam}`);
} else {
// createStatusPanel();
}
}
// 注册菜单命令
GM_registerMenuCommand('🚀 开始任务', () => {
GM_setValue('searchCount', 0);
// 重置当前暂停间隔值以开始新的搜索周期
GM_setValue('currentPauseInterval', utils.getRandomPauseInterval());
// 清除热词缓存,确保开始新任务时获取新的热词
GM_deleteValue('cache_search_words');
// 重置日常任务完成标记(当日已完成则跳过,每日仅执行一次)
if (!isTaskFlowCompletedToday('earn')) {
GM_setValue('earnTasksCompleted', false);
}
// 重置 dashboard 每日活动任务完成标记(当日已完成则跳过)
if (!isTaskFlowCompletedToday('dashboard')) {
GM_setValue('dashboardTasksCompleted', false);
}
// 获取当天的启动参数
const startParam = utils.getRandomStartParam();
window.location.href = 'https://www.bing.com/?' + startParam + '=1';
});
GM_registerMenuCommand('⏹️ 终止任务', () => {
const taskStatus = getTaskStatus();
const counterKey = 'searchCount';
GM_setValue(counterKey, taskStatus.maxCount);
// 同时清除当前暂停间隔值
GM_setValue('currentPauseInterval', null);
utils.clearAllTimers();
state.isRunning = false;
state.countdownStartTime = 0;
state.countdownDuration = 0;
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();
}
}