diff --git a/src/functions/activities/api/UrlReward.ts b/src/functions/activities/api/UrlReward.ts index 704fe6d..3dcfdf9 100644 --- a/src/functions/activities/api/UrlReward.ts +++ b/src/functions/activities/api/UrlReward.ts @@ -1,4 +1,6 @@ +import { randomUUID } from 'node:crypto' import { URLs } from '../../../constants/urls' +import { BING_APP_USER_AGENT } from '../../../constants/userAgents' import type { BasePromotion } from '../../../interface/DashboardData' import { BaseActivity } from '../BaseActivity' @@ -22,10 +24,12 @@ export class UrlReward extends BaseActivity { const live = await this.bot.browser.func.ensureOffer(offerId) if (!live) { + // 页面快照缺失:回退到 App dapi 官方通道上报(V4-china / geo=CN 下每日任务在页面快照中无 offerId) + if (await this.dapiReportActivity(promotion)) return this.bot.logger.warn( this.bot.isMobile, 'URL-REWARD', - `跳过 ${offerId}:页面快照中不存在,即使重新获取 /earn 和 /dashboard 后也没有` + `跳过 ${offerId}:页面快照中不存在(dapi 上报亦未成功),即使重新获取 /earn 和 /dashboard 后也没有` ) return } @@ -147,4 +151,97 @@ export class UrlReward extends BaseActivity { await this.runUrlReward(promotion, false) return true } + /** + * App dapi 通道上报(Microsoft Rewards 每日任务官方上报接口): + * POST prod.rewardsplatform.microsoft.com/dapi/me/activities + * 使用登录时获取的 MBI_SSL accessToken(this.bot.accessToken)。 + * 请求格式对照 BingSapphire App:attributes.offerid + amount + type:101 + channel + id + meta + country。 + * meta 为 App 端签名,实测服务端不校验(空串可接受)。 + */ + private async dapiReportActivity(promotion: BasePromotion): Promise { + const offerId = promotion.offerId + const token = this.bot.accessToken + if (!token) { + this.bot.logger.warn(this.bot.isMobile, 'URL-REWARD', `dapi 上报跳过 ${offerId}:无 accessToken(未走移动端登录)`) + return false + } + + this.bot.logger.info( + this.bot.isMobile, + 'URL-REWARD', + `dapi 通道上报 | offerId=${offerId} | type=101 | currentBalance=${this.bot.userData.currentPoints}` + ) + + try { + const oldBalance = this.bot.userData.currentPoints + const request = { + url: URLs.platform.activities, + method: 'POST' as const, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': BING_APP_USER_AGENT, + 'X-Rewards-Country': this.bot.userData.geoLocale, + 'X-Rewards-Language': this.bot.userData.langCode, + 'X-Rewards-IsMobile': 'true', + Accept: '*/*' + }, + data: { + attributes: { offerid: offerId }, + amount: 1, + type: 101, + risk_context: {}, + channel: 'SAAndroid', + id: randomUUID(), + meta: '', + country: 'cn' + }, + retries: 1 + } + + const response = await this.bot.http.request<{ code?: number; response?: { balance?: number } }>( + request, + false + ) + const code = response.data?.code + const newBalance = response.data?.response?.balance ?? (await this.bot.browser.func.getCurrentPoints()) + const gainedPoints = newBalance - oldBalance + + this.bot.logger.info( + this.bot.isMobile, + 'URL-REWARD', + `dapi 上报响应 | offerId=${offerId} | http=${response.status} | code=${code} | pointsGained=${gainedPoints} | balance=${newBalance} | raw=${JSON.stringify(response.data ?? '').slice(0, 600)}` + ) + + if (code === 0) { + if (gainedPoints > 0) { + this.bot.userData.currentPoints = newBalance + this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints + } + this.bot.logger.info( + this.bot.isMobile, + 'URL-REWARD', + `dapi 上报完成 | offerId=${offerId} | pointsGained=${gainedPoints}`, + 'green' + ) + return true + } + + this.bot.logger.warn( + this.bot.isMobile, + 'URL-REWARD', + `dapi 上报未成功 | offerId=${offerId} | http=${response.status} | code=${code} | raw=${JSON.stringify(response.data ?? '').slice(0, 600)}` + ) + return false + } catch (error) { + this.bot.logger.error( + this.bot.isMobile, + 'URL-REWARD', + `dapi 上报出错 | offerId=${offerId} | message=${error instanceof Error ? error.message : String(error)}` + ) + return false + } + } + + }