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