From 9704b1b9b2bada62a6bf803116a21651cbc00fbe Mon Sep 17 00:00:00 2001
From: GoldenSection0618 <172290907+GoldenSection0618@users.noreply.github.com>
Date: Sat, 24 Oct 2026 05:30:55 +0000
Subject: [PATCH 1/3] =?UTF-8?q?style:=20=E5=AF=B9=E9=BD=90=E7=AD=BE?=
=?UTF-8?q?=E5=88=B0=E7=8A=B6=E6=80=81=E4=BF=AE=E5=A4=8D=E7=9A=84=E5=85=A8?=
=?UTF-8?q?=E9=87=8F=E6=A0=BC=E5=BC=8F=E6=A3=80=E6=9F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 48 ++++++-------
config.example.json | 5 +-
scripts/api/logParser.js | 42 +++++-------
src/browser/Browser.ts | 17 ++---
src/browser/BrowserFunc.ts | 25 ++-----
src/browser/BrowserUtils.ts | 12 +---
src/browser/MediaBlocker.ts | 6 +-
src/browser/auth/Login.ts | 68 +++++--------------
src/browser/auth/methods/GetACodeLogin.ts | 12 +---
src/browser/auth/methods/MobileAccessLogin.ts | 6 +-
src/browser/auth/methods/PasswordlessLogin.ts | 6 +-
.../auth/methods/RecoveryEmailLogin.ts | 6 +-
src/functions/QueryEngine.ts | 23 +++----
.../activities/api/ActivateSearchPerk.ts | 6 +-
src/functions/activities/api/ClaimReward.ts | 12 +---
.../activities/api/EnsureStreakProtection.ts | 6 +-
src/functions/activities/api/UrlReward.ts | 6 +-
src/functions/activities/app/AppPromotions.ts | 6 +-
src/functions/activities/app/AppReward.ts | 6 +-
src/functions/activities/app/DailyCheckIn.ts | 6 +-
src/functions/activities/app/ReadToEarn.ts | 6 +-
.../activities/experimental/ApiSearch.ts | 6 +-
.../experimental/ApiSearchOnBing.ts | 6 +-
.../activities/experimental/BingSearchApi.ts | 6 +-
.../activities/experimental/EdgeBrowsing.ts | 37 ++--------
src/functions/activities/rewards/DailySet.ts | 6 +-
.../activities/rewards/MorePromotions.ts | 6 +-
.../rewards/PromotionActivityRunner.ts | 10 +--
.../activities/rewards/PunchCards.ts | 6 +-
.../activities/search/BrowserSearch.ts | 12 +---
.../activities/search/BrowserSearchOnBing.ts | 6 +-
.../activities/search/SearchOnBingShared.ts | 12 +---
.../activities/visualSearch/VisualSearch.ts | 30 ++------
.../visualSearch/VisualSearchBrowser.ts | 12 +---
src/index.ts | 45 +++---------
src/logging/ClawBot.ts | 4 +-
src/util/ClawBotClient.ts | 18 ++---
src/util/Humanize.ts | 6 +-
38 files changed, 145 insertions(+), 413 deletions(-)
diff --git a/README.md b/README.md
index 94f82146..37007278 100644
--- a/README.md
+++ b/README.md
@@ -197,9 +197,9 @@ ACCOUNT_1_PASSWORD=your_password
>
> ```yaml
> services:
-> microsoft-rewards-script:
-> env_file:
-> - path: .env
+> microsoft-rewards-script:
+> env_file:
+> - path: .env
> ```
>
> 另外用**列表式** `environment:`(`- KEY=value`)时值**不要加引号**——引号会成为值的一部分(如 `- CRON_SCHEDULE="0 9 * * *"` 会让 cron 表达式带引号而失效);映射式(`KEY: value`)则按 YAML 规则正常加引号。
@@ -268,20 +268,20 @@ docker compose restart # 重启(不重建)
🔵 Core / 核心配置
-| 设置 | 描述 | 默认值 | Docker 环境变量 |
-| --------------------------- | ----------------------------------- | --------------- | ------------------------------------- |
-| `sessionPath` | 存储浏览器会话的目录 | `sessions` | — |
-| `headless` | 在后台运行浏览器 | `false`(可见) | Docker 强制 `true` |
-| `browserChannel` | `chromium`(内置补丁版,推荐)或 `msedge`(系统真实 Edge;⚠️ 实验性:Edge 152 + Playwright 1.6x 访问 rewards.bing.com 会崩溃,见 [playwright#41438](https://github.com/microsoft/playwright/issues/41438),修复后可重试) | `chromium` | `CONFIG_BROWSER_CHANNEL` |
-| `clusters` | 并发账户集群数 | `1` | `CONFIG_CLUSTERS` |
-| `errorDiagnostics` | 出错时保存诊断信息到 `diagnostics/` | `false` | `CONFIG_ERROR_DIAGNOSTICS` |
-| `ensureStreakProtection` | 确保连击保护已开启 | `true` | `CONFIG_ENSURE_STREAK_PROTECTION` |
-| `autoClaimPunchcardRewards` | 自动领取已完成的打卡奖励 | `false` | `CONFIG_AUTO_CLAIM_PUNCHCARD_REWARDS` |
-| `skipNonPointTasks` | 跳过无积分奖励的任务 | `true` | `CONFIG_SKIP_NON_POINT_TASKS` |
-| `prewarmDesktopLogin` | 移动端登录后立即完成桌面端登录并保存会话(需要手机批准的账户可把两次批准集中在运行开头),之后桌面阶段直接复用会话 | `false` | `CONFIG_PREWARM_DESKTOP_LOGIN` |
-| `accountDelay.min` / `.max` | 下一账户开始前的延迟 | `1min` - `3min` | `CONFIG_ACCOUNT_DELAY_MIN` / `_MAX` |
-| `searchOnBingLocalQueries` | ExploreOnBing 活动使用本地词库 | `false` | `CONFIG_SEARCH_ON_BING_LOCAL` |
-| `globalTimeout` | 所有操作的超时时间 | `30sec` | `CONFIG_GLOBAL_TIMEOUT` |
+| 设置 | 描述 | 默认值 | Docker 环境变量 |
+| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------- |
+| `sessionPath` | 存储浏览器会话的目录 | `sessions` | — |
+| `headless` | 在后台运行浏览器 | `false`(可见) | Docker 强制 `true` |
+| `browserChannel` | `chromium`(内置补丁版,推荐)或 `msedge`(系统真实 Edge;⚠️ 实验性:Edge 152 + Playwright 1.6x 访问 rewards.bing.com 会崩溃,见 [playwright#41438](https://github.com/microsoft/playwright/issues/41438),修复后可重试) | `chromium` | `CONFIG_BROWSER_CHANNEL` |
+| `clusters` | 并发账户集群数 | `1` | `CONFIG_CLUSTERS` |
+| `errorDiagnostics` | 出错时保存诊断信息到 `diagnostics/` | `false` | `CONFIG_ERROR_DIAGNOSTICS` |
+| `ensureStreakProtection` | 确保连击保护已开启 | `true` | `CONFIG_ENSURE_STREAK_PROTECTION` |
+| `autoClaimPunchcardRewards` | 自动领取已完成的打卡奖励 | `false` | `CONFIG_AUTO_CLAIM_PUNCHCARD_REWARDS` |
+| `skipNonPointTasks` | 跳过无积分奖励的任务 | `true` | `CONFIG_SKIP_NON_POINT_TASKS` |
+| `prewarmDesktopLogin` | 移动端登录后立即完成桌面端登录并保存会话(需要手机批准的账户可把两次批准集中在运行开头),之后桌面阶段直接复用会话 | `false` | `CONFIG_PREWARM_DESKTOP_LOGIN` |
+| `accountDelay.min` / `.max` | 下一账户开始前的延迟 | `1min` - `3min` | `CONFIG_ACCOUNT_DELAY_MIN` / `_MAX` |
+| `searchOnBingLocalQueries` | ExploreOnBing 活动使用本地词库 | `false` | `CONFIG_SEARCH_ON_BING_LOCAL` |
+| `globalTimeout` | 所有操作的超时时间 | `30sec` | `CONFIG_GLOBAL_TIMEOUT` |
@@ -419,13 +419,13 @@ docker compose restart # 重启(不重建)
降低风控特征的运行策略集合。`humanize.enabled=false`(默认)时以下全部不生效,行为与原版一致。
-| 设置 | 描述 | 默认值 | Docker 环境变量 |
-| -------------------------------------------- | -------------------------------------------------------------------- | ----------------- | ----------------------------------------------------- |
-| `humanize.enabled` | 总开关 | `false` | `CONFIG_HUMANIZE_ENABLED` |
-| `humanize.skipWhenCompletedToday` | 当天已成功运行过(`logs/last-success.txt`)则再次启动时直接退出,配合早晚两次定时实现"兜底轮自动跳过" | `false` | `CONFIG_HUMANIZE_SKIP_WHEN_COMPLETED` |
-| `humanize.quietHours` | 静默时段数组;运行中进入时段自动挂起到结束,启动时落在时段内也等待后再跑。支持 `start > end` 跨午夜 | `[]` | 仅 config.json |
-| `humanize.searchTargetRatio.min` / `.max` | 每轮搜索目标完成度随机区间(移动/桌面各自抽取),保留分向下取整到 3 的倍数 | `0.85` - `1.0` | `CONFIG_HUMANIZE_SEARCH_TARGET_RATIO_MIN` / `_MAX` |
-| `humanize.readToEarnArticles.min` / `.max` | Read-to-Earn 每轮随机阅读篇数上限区间 | `7` - `10` | `CONFIG_HUMANIZE_READ_TO_EARN_MIN` / `_MAX` |
+| 设置 | 描述 | 默认值 | Docker 环境变量 |
+| ------------------------------------------ | ----------------------------------------------------------------------------------------------------- | -------------- | -------------------------------------------------- |
+| `humanize.enabled` | 总开关 | `false` | `CONFIG_HUMANIZE_ENABLED` |
+| `humanize.skipWhenCompletedToday` | 当天已成功运行过(`logs/last-success.txt`)则再次启动时直接退出,配合早晚两次定时实现"兜底轮自动跳过" | `false` | `CONFIG_HUMANIZE_SKIP_WHEN_COMPLETED` |
+| `humanize.quietHours` | 静默时段数组;运行中进入时段自动挂起到结束,启动时落在时段内也等待后再跑。支持 `start > end` 跨午夜 | `[]` | 仅 config.json |
+| `humanize.searchTargetRatio.min` / `.max` | 每轮搜索目标完成度随机区间(移动/桌面各自抽取),保留分向下取整到 3 的倍数 | `0.85` - `1.0` | `CONFIG_HUMANIZE_SEARCH_TARGET_RATIO_MIN` / `_MAX` |
+| `humanize.readToEarnArticles.min` / `.max` | Read-to-Earn 每轮随机阅读篇数上限区间 | `7` - `10` | `CONFIG_HUMANIZE_READ_TO_EARN_MIN` / `_MAX` |
`quietHours` 规则示例(days 支持全名或缩写,未列出的天不静默):
diff --git a/config.example.json b/config.example.json
index 9114ed39..6187be26 100644
--- a/config.example.json
+++ b/config.example.json
@@ -40,10 +40,7 @@
"maxBonusSearches": 110,
"parallelSearching": false,
"clusterSearch": true,
- "queryEngines": [
- "china",
- "local"
- ],
+ "queryEngines": ["china", "local"],
"searchResultVisitTime": "20sec",
"searchDelay": {
"min": "6min",
diff --git a/scripts/api/logParser.js b/scripts/api/logParser.js
index bfdb4402..8b8c3928 100644
--- a/scripts/api/logParser.js
+++ b/scripts/api/logParser.js
@@ -100,14 +100,12 @@ function ensureAccount(state, email) {
// inside the log messages, but the surrounding text is Chinese.
const RE = {
runStart: /^启动微软奖励脚本 \| v(\S+) \| 账户数: (\d+) \| 集群数: (\d+)/,
- accountStart:
- /^开始处理账户: (\S+) \| geoLocale: ([^|]+?)(?: \| locale: (\S+))?(?: \| 缓存区域: (\S+))?\s*$/,
+ accountStart: /^开始处理账户: (\S+) \| geoLocale: ([^|]+?)(?: \| locale: (\S+))?(?: \| 缓存区域: (\S+))?\s*$/,
earnable: /^今日可赚取 \| 移动端: (\d+) \| 浏览器: (\d+) \| App: (\d+) \| (\S+) \| locale: (\S+)\s*$/,
searchSummary: /^搜索汇总 \| 移动端=(-?\d+) \| 桌面端=(-?\d+) \| 额外=(-?\d+) \| 总计=(-?\d+)/,
streakProtection:
/^快照完成 \| offers=(\d+) \| 可上报=(\d+) \| streaks=(\d+) \| 连续保护已启用=(true|false|null) \| 连续保护剩余天数=(\d+|null) \| 连续计数=(\d+|null) \| 等级=([^|]+) \| 账户=(\S+@\S+)$/,
- accountEnd:
- /^账户完成: (\S+) \| 获得积分=(-?\d+) \| 原余额=(\d+) \| 现余额=(\d+) \| 持续秒数=([\d.]+)/,
+ accountEnd: /^账户完成: (\S+) \| 获得积分=(-?\d+) \| 原余额=(\d+) \| 现余额=(\d+) \| 持续秒数=([\d.]+)/,
runEnd: /^全部账户完成 \| 处理账户数=(\d+) \| 获得积分=(-?\d+) \| 原余额=(\d+) \| 现余额=(\d+) \| 运行分钟数=([\d.]+)/,
accountError: /^(\S+@\S+) \| 错误=([\s\S]+)$/,
flowFailed: /(\S+@\S+) 的.*流程失败:/i,
@@ -142,32 +140,34 @@ function pointEventSource(title, message) {
case 'SEARCH-BONUS':
return message.startsWith('必应搜索完成') ? 'bonus' : null
case 'READ-TO-EARN':
- return (message.startsWith('文章后的积分变化') ||
- message.startsWith('未获得积分,停止读文赚积分') ||
- message.startsWith('已阅读第') ||
- message.startsWith('读文赚积分完成')) ? 'read' : null
+ return message.startsWith('文章后的积分变化') ||
+ message.startsWith('未获得积分,停止读文赚积分') ||
+ message.startsWith('已阅读第') ||
+ message.startsWith('读文赚积分完成')
+ ? 'read'
+ : null
case 'DAILY-CHECK-IN':
return message.startsWith('每日签到完成') ? 'checkIn' : null
case 'CLAIM-BONUS-POINTS':
- return (message.startsWith('领取奖励积分完成') || message.startsWith('没有可领取的积分'))
+ return message.startsWith('领取奖励积分完成') || message.startsWith('没有可领取的积分')
? 'claimBonus'
: null
case 'CLAIM-REWARD':
return message.startsWith('奖励已领取') ? 'claimReward' : null
case 'URL-REWARD':
- return (message.startsWith('UrlReward 完成') || message.startsWith('UrlReward 未获得积分'))
+ return message.startsWith('UrlReward 完成') || message.startsWith('UrlReward 未获得积分')
? 'urlReward'
: null
case 'VISUAL-SEARCH':
- return (message.startsWith('每日视觉搜索完成') || message.startsWith('每日视觉搜索已记录'))
+ return message.startsWith('每日视觉搜索完成') || message.startsWith('每日视觉搜索已记录')
? 'visualSearch'
: null
case 'APP-REWARD':
- return (message.startsWith('AppReward 完成') || message.startsWith('AppReward 完成但未获得积分'))
+ return message.startsWith('AppReward 完成') || message.startsWith('AppReward 完成但未获得积分')
? 'appReward'
: null
case 'PUNCHCARD':
- return (message.includes('COMPLETE') || message.includes('in progress')) ? 'punchcard' : null
+ return message.includes('COMPLETE') || message.includes('in progress') ? 'punchcard' : null
case 'SEARCH-ON-BING-SEARCH':
return message.startsWith('SearchOnBing 活动完成') ? 'searchOnBing' : null
default:
@@ -244,9 +244,7 @@ function applyEdgeBrowsing(state, entry) {
if (!account) return null
const message = entry.message ?? ''
- const finalReports = message.startsWith('后台 Edge 浏览活动结束')
- ? numericField(message, 'reports')
- : null
+ const finalReports = message.startsWith('后台 Edge 浏览活动结束') ? numericField(message, 'reports') : null
const progress =
fractionField(message, 'reportsCompleted') ??
fractionField(message, 'report') ??
@@ -303,16 +301,10 @@ function applyEdgeBrowsing(state, entry) {
previous.nextReportInSeconds = null
previous.estimatedRemainingMinutes = 0
previous.waitingForBackground = false
- } else if (
- message === '该账户无法使用 Edge 浏览连击(Browsing Streak)' ||
- message.startsWith('跳过:')
- ) {
+ } else if (message === '该账户无法使用 Edge 浏览连击(Browsing Streak)' || message.startsWith('跳过:')) {
previous.status = 'skipped'
previous.waitingForBackground = false
- } else if (
- message.startsWith('后台 Edge 浏览活动失败') ||
- message.startsWith('意外的后台任务失败')
- ) {
+ } else if (message.startsWith('后台 Edge 浏览活动失败') || message.startsWith('意外的后台任务失败')) {
previous.status = 'failed'
previous.waitingForBackground = false
} else if (message === '后台活动已取消') {
@@ -537,4 +529,4 @@ export function summarizeRunState(state) {
},
accounts
}
-}
\ No newline at end of file
+}
diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts
index f0c8cb97..2a07a831 100644
--- a/src/browser/Browser.ts
+++ b/src/browser/Browser.ts
@@ -131,16 +131,10 @@ class Browser {
this.bot.isMobile,
'SESSION',
`正在恢复已保存的浏览器会话 | Cookie数=${session.storageState.cookies.length} | origins=${session.storageState.origins.length} | 已保存分钟数=${ageMinutes}` +
- (session.expiredCookiesRemoved
- ? ` | 已清除过期Cookie=${session.expiredCookiesRemoved}`
- : '')
+ (session.expiredCookiesRemoved ? ` | 已清除过期Cookie=${session.expiredCookiesRemoved}` : '')
)
} else {
- this.bot.logger.info(
- this.bot.isMobile,
- 'SESSION',
- '未找到已保存的浏览器会话;可能需要登录'
- )
+ this.bot.logger.info(this.bot.isMobile, 'SESSION', '未找到已保存的浏览器会话;可能需要登录')
}
const shouldUseFingerprint = this.bot.isMobile
@@ -181,8 +175,7 @@ class Browser {
}
fingerprint =
- (reuseFingerprint && savedFingerprint) ||
- (await this.generateFingerprint(this.bot.isMobile))
+ (reuseFingerprint && savedFingerprint) || (await this.generateFingerprint(this.bot.isMobile))
const screen = fingerprint.fingerprint.screen
@@ -220,9 +213,7 @@ class Browser {
}
context.on('page', p => {
- p.on('crash', () =>
- this.bot.logger.error(this.bot.isMobile, 'BROWSER', `渲染器崩溃 | ${p.url()}`)
- )
+ p.on('crash', () => this.bot.logger.error(this.bot.isMobile, 'BROWSER', `渲染器崩溃 | ${p.url()}`))
})
context.on('close', () => this.bot.logger.warn(this.bot.isMobile, 'BROWSER', '浏览器上下文已关闭'))
diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts
index b664b0de..6c202b95 100644
--- a/src/browser/BrowserFunc.ts
+++ b/src/browser/BrowserFunc.ts
@@ -130,10 +130,9 @@ export default class BrowserFunc {
const firstTime = !this.botMetricsLoggedPlatforms.has(platform)
this.botMetricsLoggedPlatforms.add(platform)
- const message =
- `机器人检测指标 | serpbotscore=${score ?? '未知'} | 更新时间=${
- scoreUpdated ? String(scoreUpdated).slice(0, 16) : '未知'
- } | userWarnings=${warningNames.length > 0 ? warningNames.join(', ') : '无'}`
+ const message = `机器人检测指标 | serpbotscore=${score ?? '未知'} | 更新时间=${
+ scoreUpdated ? String(scoreUpdated).slice(0, 16) : '未知'
+ } | userWarnings=${warningNames.length > 0 ? warningNames.join(', ') : '无'}`
if (warningNames.length > 0) {
this.bot.logger.warn(this.bot.isMobile, 'GET-DASHBOARD-DATA', message)
@@ -589,11 +588,7 @@ export default class BrowserFunc {
if (isBrowserClosedError(error)) {
this.bot.logger.debug(this.bot.isMobile, 'CLOSE-BROWSER', '浏览器已处于关闭状态。')
} else {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'CLOSE-BROWSER',
- '关闭时遇到错误,但进程仍在退出。'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'CLOSE-BROWSER', '关闭时遇到错误,但进程仍在退出。')
}
}
}
@@ -618,11 +613,7 @@ export default class BrowserFunc {
return await response.text()
}
- this.bot.logger.debug(
- this.bot.isMobile,
- 'REWARDS-PAGE',
- `获取 ${route} 失败 | 状态码=${response.status()}`
- )
+ this.bot.logger.debug(this.bot.isMobile, 'REWARDS-PAGE', `获取 ${route} 失败 | 状态码=${response.status()}`)
} catch (error) {
this.bot.logger.debug(
this.bot.isMobile,
@@ -642,11 +633,7 @@ export default class BrowserFunc {
async checkpointActiveSession(source = 'SESSION-CHECKPOINT'): Promise {
const page = this.getActivePage()
if (!page) {
- this.bot.logger.debug(
- this.bot.isMobile,
- source,
- '无法保存会话检查点,因为没有可用的活动浏览器页面'
- )
+ this.bot.logger.debug(this.bot.isMobile, source, '无法保存会话检查点,因为没有可用的活动浏览器页面')
return false
}
diff --git a/src/browser/BrowserUtils.ts b/src/browser/BrowserUtils.ts
index 6a5d97b1..a3598dc3 100644
--- a/src/browser/BrowserUtils.ts
+++ b/src/browser/BrowserUtils.ts
@@ -50,11 +50,7 @@ export default class BrowserUtils {
if (b) {
const clicked = await this.ghostClick(page, b.selector)
if (clicked) {
- this.bot.logger.debug(
- this.bot.isMobile,
- 'DISMISS-ALL-MESSAGES',
- `已关闭: ${b.label}`
- )
+ this.bot.logger.debug(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `已关闭: ${b.label}`)
}
}
})
@@ -70,11 +66,7 @@ export default class BrowserUtils {
} else {
const accepted = await this.ghostClick(page, '#bnp_btn_accept')
if (accepted) {
- this.bot.logger.debug(
- this.bot.isMobile,
- 'DISMISS-ALL-MESSAGES',
- '已关闭: Bing Overlay Accept'
- )
+ this.bot.logger.debug(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', '已关闭: Bing Overlay Accept')
}
}
await this.bot.utils.wait(250)
diff --git a/src/browser/MediaBlocker.ts b/src/browser/MediaBlocker.ts
index da7521cb..3319b722 100644
--- a/src/browser/MediaBlocker.ts
+++ b/src/browser/MediaBlocker.ts
@@ -30,11 +30,7 @@ export function suspendMediaBlocking(bot: MicrosoftRewardsBot, context: BrowserC
if (!state?.enabled) return
state.enabled = false
- bot.logger.info(
- bot.isMobile,
- 'BROWSER',
- '登录期间媒体加载已放行 | 图片与媒体请求暂不拦截'
- )
+ bot.logger.info(bot.isMobile, 'BROWSER', '登录期间媒体加载已放行 | 图片与媒体请求暂不拦截')
}
export async function configureMediaBlocking(bot: MicrosoftRewardsBot, context: BrowserContext): Promise {
diff --git a/src/browser/auth/Login.ts b/src/browser/auth/Login.ts
index 608c83ef..0a6c0402 100644
--- a/src/browser/auth/Login.ts
+++ b/src/browser/auth/Login.ts
@@ -133,11 +133,7 @@ export class Login {
`相同状态计数: ${sameStateCount}/4 状态为 "${state}"`
)
if (sameStateCount >= 4) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'LOGIN',
- `在状态 "${state}" 停滞4次循环,刷新页面`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'LOGIN', `在状态 "${state}" 停滞4次循环,刷新页面`)
await page.reload({ waitUntil: 'domcontentloaded' })
await this.bot.utils.wait(3000)
sameStateCount = 0
@@ -265,11 +261,7 @@ export class Login {
results.push('FOOTER_ACTION')
} else {
if (footerAction && footerTargetsSpecificProof) {
- this.bot.logger.debug(
- this.bot.isMobile,
- 'DETECT-STATE',
- '页脚指向具体验证方式;保持主登录方式'
- )
+ this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', '页脚指向具体验证方式;保持主登录方式')
}
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', '检测到主无密码登录操作')
results.push('PASSWORDLESS_SEND_CODE')
@@ -473,7 +465,12 @@ export class Login {
const alertCount = await alertEls.count()
let errorMsg = ''
for (let i = 0; i < alertCount; i++) {
- const text = (await alertEls.nth(i).innerText().catch(() => '')).trim()
+ const text = (
+ await alertEls
+ .nth(i)
+ .innerText()
+ .catch(() => '')
+ ).trim()
if (text) {
errorMsg = text
break
@@ -503,11 +500,7 @@ export class Login {
// Enter password - use it only when Microsoft actually presents the password page
case 'PASSWORD_INPUT': {
if (!account.password) {
- this.bot.logger.info(
- this.bot.isMobile,
- 'LOGIN',
- '检测到密码输入页但未配置密码;返回登录方式选择'
- )
+ this.bot.logger.info(this.bot.isMobile, 'LOGIN', '检测到密码输入页但未配置密码;返回登录方式选择')
if (await this.tryClick(page, this.selectors.backButton, 'Back button')) return true
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', '未配置密码且无法返回登录方式选择')
@@ -599,11 +592,7 @@ export class Login {
const confirmed = await this.bot.browser.utils.ghostClick(page, this.selectors.primaryButton)
if (!confirmed) {
this.passwordlessMethodSelected = false
- this.bot.logger.warn(
- this.bot.isMobile,
- 'LOGIN',
- '无法发送Microsoft Authenticator请求'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'LOGIN', '无法发送Microsoft Authenticator请求')
return false
}
await this.waitForIdle(page, 'after Microsoft Authenticator request')
@@ -619,7 +608,6 @@ export class Login {
this.bot.isMobile,
'LOGIN',
'没有可用的非交互登录方式;邮箱验证码回退需要交互式终端输入'
-
)
return false
}
@@ -667,11 +655,7 @@ export class Login {
case 'EMAIL_VERIFICATION_INPUT': {
if (!account.password) {
if (!canPromptForInput()) {
- this.bot.logger.error(
- this.bot.isMobile,
- 'LOGIN',
- '邮箱验证需要已配置的密码或交互式终端输入'
- )
+ this.bot.logger.error(this.bot.isMobile, 'LOGIN', '邮箱验证需要已配置的密码或交互式终端输入')
return false
}
@@ -680,11 +664,7 @@ export class Login {
return true
}
- this.bot.logger.info(
- this.bot.isMobile,
- 'LOGIN',
- '检测到邮箱验证输入;正在检查备选方式'
- )
+ this.bot.logger.info(this.bot.isMobile, 'LOGIN', '检测到邮箱验证输入;正在检查备选方式')
await this.waitForIdle(page, 'on email verification page')
const footerActions = page.locator(this.selectors.footerAction)
@@ -800,11 +780,7 @@ export class Login {
// Enter your code - prefer its alternate-method footer before Back to avoid an email-code loop
case 'OTP_CODE_ENTRY': {
- this.bot.logger.info(
- this.bot.isMobile,
- 'LOGIN',
- '检测到OTP代码输入页面;尝试可用的备选登录方式'
- )
+ this.bot.logger.info(this.bot.isMobile, 'LOGIN', '检测到OTP代码输入页面;尝试可用的备选登录方式')
if (await this.checkSelector(page, this.selectors.footerAction)) {
const clicked = await this.bot.browser.utils.ghostClick(page, this.selectors.footerAction)
@@ -836,11 +812,7 @@ export class Login {
return true
}
- this.bot.logger.debug(
- this.bot.isMobile,
- 'LOGIN',
- 'OTP页脚操作未离开代码输入页;回退到返回按钮'
- )
+ this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'OTP页脚操作未离开代码输入页;回退到返回按钮')
}
}
@@ -857,11 +829,7 @@ export class Login {
case 'UNKNOWN': {
const rawUrl = page.url()
const url = new URL(rawUrl)
- this.bot.logger.warn(
- this.bot.isMobile,
- 'LOGIN',
- `在 ${url.hostname}${url.pathname} 的未知状态,等待中`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'LOGIN', `在 ${url.hostname}${url.pathname} 的未知状态,等待中`)
if (this.bot.config.errorDiagnostics && !this.capturedUnknownUrls.has(rawUrl)) {
this.capturedUnknownUrls.add(rawUrl)
@@ -1020,11 +988,7 @@ export class Login {
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
- this.bot.logger.error(
- this.bot.isMobile,
- 'GET-REWARD-SESSION',
- `获取奖励上下文失败: ${message}`
- )
+ this.bot.logger.error(this.bot.isMobile, 'GET-REWARD-SESSION', `获取奖励上下文失败: ${message}`)
throw new Error(`获取奖励上下文失败: ${message}`)
}
diff --git a/src/browser/auth/methods/GetACodeLogin.ts b/src/browser/auth/methods/GetACodeLogin.ts
index a71b68ce..20adbb65 100644
--- a/src/browser/auth/methods/GetACodeLogin.ts
+++ b/src/browser/auth/methods/GetACodeLogin.ts
@@ -115,17 +115,9 @@ export class CodeLogin {
if (emailProofInput) {
const selectorVariant =
emailProofInput.selector === this.emailVerificationInputSelectors[0] ? 'new' : 'old'
- this.bot.logger.debug(
- this.bot.isMobile,
- 'LOGIN-CODE',
- `使用${selectorVariant}版邮箱验证输入选择器`
- )
+ this.bot.logger.debug(this.bot.isMobile, 'LOGIN-CODE', `使用${selectorVariant}版邮箱验证输入选择器`)
const maskedHint = emailMessage?.match(/[A-Za-z0-9._*+-]+@[A-Za-z0-9.*-]+\.[A-Za-z]{2,}/)?.[0]
- this.bot.logger.info(
- this.bot.isMobile,
- 'LOGIN-CODE',
- '检测到邮箱验证挑战,等待邮箱地址'
- )
+ this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', '检测到邮箱验证挑战,等待邮箱地址')
let emailVerified = false
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
diff --git a/src/browser/auth/methods/MobileAccessLogin.ts b/src/browser/auth/methods/MobileAccessLogin.ts
index 7e60ef8d..b4d2eebf 100644
--- a/src/browser/auth/methods/MobileAccessLogin.ts
+++ b/src/browser/auth/methods/MobileAccessLogin.ts
@@ -51,11 +51,7 @@ export class MobileAccessLogin {
}
if (!code) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'LOGIN-APP',
- '无法解析移动OAuth代码 - 本次运行将跳过应用活动'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', '无法解析移动OAuth代码 - 本次运行将跳过应用活动')
return ''
}
diff --git a/src/browser/auth/methods/PasswordlessLogin.ts b/src/browser/auth/methods/PasswordlessLogin.ts
index 5574bbc6..7d280f46 100644
--- a/src/browser/auth/methods/PasswordlessLogin.ts
+++ b/src/browser/auth/methods/PasswordlessLogin.ts
@@ -96,11 +96,7 @@ export class PasswordlessLogin {
await this.bot.utils.wait(1000)
}
- this.bot.logger.warn(
- this.bot.isMobile,
- 'LOGIN-PASSWORDLESS',
- `${this.approvalTimeoutSeconds} 秒后批准超时!`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'LOGIN-PASSWORDLESS', `${this.approvalTimeoutSeconds} 秒后批准超时!`)
return 'TIMED_OUT'
}
diff --git a/src/browser/auth/methods/RecoveryEmailLogin.ts b/src/browser/auth/methods/RecoveryEmailLogin.ts
index 269839a6..3ec7356f 100644
--- a/src/browser/auth/methods/RecoveryEmailLogin.ts
+++ b/src/browser/auth/methods/RecoveryEmailLogin.ts
@@ -71,11 +71,7 @@ export class RecoveryLogin {
throw new Error('Recovery email is required because interactive stdin is unavailable')
}
- this.bot.logger.info(
- this.bot.isMobile,
- 'LOGIN-RECOVERY',
- '未提供恢复邮箱,将提示用户输入'
- )
+ this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', '未提供恢复邮箱,将提示用户输入')
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
this.bot.logger.info(
diff --git a/src/functions/QueryEngine.ts b/src/functions/QueryEngine.ts
index a1edf53a..d93851c8 100644
--- a/src/functions/QueryEngine.ts
+++ b/src/functions/QueryEngine.ts
@@ -129,11 +129,7 @@ export class QueryCore {
if (!handler) continue
const topics = await Promise.resolve(handler())
- this.bot.logger.debug(
- this.bot.isMobile,
- 'QUERY-MANAGER',
- `源 "${source}" 返回 ${topics.length} 条`
- )
+ this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `源 "${source}" 返回 ${topics.length} 条`)
if (!topics.length) continue
// 主源全量纳入;后续源只随机抽样 FALLBACK_LOCAL_SAMPLE 个补充
@@ -169,11 +165,7 @@ export class QueryCore {
if (shuffle) {
this.bot.utils.shuffleArray(topics)
- this.bot.logger.debug(
- this.bot.isMobile,
- 'QUERY-MANAGER',
- `主题词池已打乱 | 首个="${topics[0] ?? ''}"`
- )
+ this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `主题词池已打乱 | 首个="${topics[0] ?? ''}"`)
}
this.bot.logger.debug(
@@ -531,7 +523,11 @@ export class QueryCore {
const feeds = RSS_FEEDS[site]
if (!feeds) {
- this.bot.logger.warn(this.bot.isMobile, 'SEARCH-RSS', `未知的 RSS 站点 "${site}"(选择器 "${selector}")`)
+ this.bot.logger.warn(
+ this.bot.isMobile,
+ 'SEARCH-RSS',
+ `未知的 RSS 站点 "${site}"(选择器 "${selector}")`
+ )
continue
}
@@ -693,10 +689,7 @@ export class QueryCore {
await maybeBackoff(backoffMultiplier)
const source = fallback[i]!
try {
- const result = await this.fetchChinaHotWords(
- this.buildChinaApiUrl(baseUrl, source, appkey),
- source
- )
+ const result = await this.fetchChinaHotWords(this.buildChinaApiUrl(baseUrl, source, appkey), source)
if (result.length) {
result.forEach(t => titles.add(t))
this.bot.logger.info(
diff --git a/src/functions/activities/api/ActivateSearchPerk.ts b/src/functions/activities/api/ActivateSearchPerk.ts
index cc1ef878..be15ffb0 100644
--- a/src/functions/activities/api/ActivateSearchPerk.ts
+++ b/src/functions/activities/api/ActivateSearchPerk.ts
@@ -49,11 +49,7 @@ export class ActivateSearchPerk extends BaseActivity {
public async activate(data: DashboardData) {
const perk = detectSearchMultiplierPerk(data.dashboard)
if (!perk) {
- this.bot.logger.debug(
- this.bot.isMobile,
- 'ACTIVATE-SEARCH-PERK',
- 'dashboard 上没有搜索倍数加成'
- )
+ this.bot.logger.debug(this.bot.isMobile, 'ACTIVATE-SEARCH-PERK', 'dashboard 上没有搜索倍数加成')
return
}
diff --git a/src/functions/activities/api/ClaimReward.ts b/src/functions/activities/api/ClaimReward.ts
index 4886a580..1d76cf3e 100644
--- a/src/functions/activities/api/ClaimReward.ts
+++ b/src/functions/activities/api/ClaimReward.ts
@@ -8,11 +8,7 @@ export class ClaimReward extends BaseActivity {
const actionId = this.bot.nextActions.reportActivity
if (!actionId) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'CLAIM-REWARD',
- `跳过 ${offerId}:bundle 中未发现 "reportActivity"`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'CLAIM-REWARD', `跳过 ${offerId}:bundle 中未发现 "reportActivity"`)
return
}
@@ -21,11 +17,7 @@ export class ClaimReward extends BaseActivity {
return
}
if (!child.reportable) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'CLAIM-REWARD',
- `跳过 ${offerId}:不可上报(已完成/已锁定/已禁用)`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'CLAIM-REWARD', `跳过 ${offerId}:不可上报(已完成/已锁定/已禁用)`)
return
}
diff --git a/src/functions/activities/api/EnsureStreakProtection.ts b/src/functions/activities/api/EnsureStreakProtection.ts
index b9082c4d..5b40f0a5 100644
--- a/src/functions/activities/api/EnsureStreakProtection.ts
+++ b/src/functions/activities/api/EnsureStreakProtection.ts
@@ -34,11 +34,7 @@ export class EnsureStreakProtection extends BaseActivity {
}
if (before && before.remainingDays === 0) {
- this.bot.logger.info(
- this.bot.isMobile,
- 'ENABLE-STREAK-PROTECTION',
- '没有剩余的保护天数 - 开关已禁用,跳过'
- )
+ this.bot.logger.info(this.bot.isMobile, 'ENABLE-STREAK-PROTECTION', '没有剩余的保护天数 - 开关已禁用,跳过')
return
}
diff --git a/src/functions/activities/api/UrlReward.ts b/src/functions/activities/api/UrlReward.ts
index 704fe6d6..af68cf33 100644
--- a/src/functions/activities/api/UrlReward.ts
+++ b/src/functions/activities/api/UrlReward.ts
@@ -12,11 +12,7 @@ export class UrlReward extends BaseActivity {
const actionId = this.bot.nextActions.reportActivity
if (!actionId) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'URL-REWARD',
- `跳过 ${offerId}:bundle 中未发现 "reportActivity"`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'URL-REWARD', `跳过 ${offerId}:bundle 中未发现 "reportActivity"`)
return
}
diff --git a/src/functions/activities/app/AppPromotions.ts b/src/functions/activities/app/AppPromotions.ts
index 43ff8f63..35147c4d 100644
--- a/src/functions/activities/app/AppPromotions.ts
+++ b/src/functions/activities/app/AppPromotions.ts
@@ -13,11 +13,7 @@ export class AppPromotions extends BaseActivity {
})
if (!pending.length) {
- this.bot.logger.info(
- this.bot.isMobile,
- 'APP-PROMOTIONS',
- '所有"应用推广"项目已完成'
- )
+ this.bot.logger.info(this.bot.isMobile, 'APP-PROMOTIONS', '所有"应用推广"项目已完成')
return
}
diff --git a/src/functions/activities/app/AppReward.ts b/src/functions/activities/app/AppReward.ts
index 0b4cf67f..a7950386 100644
--- a/src/functions/activities/app/AppReward.ts
+++ b/src/functions/activities/app/AppReward.ts
@@ -12,11 +12,7 @@ export class AppReward extends BaseActivity {
public async doAppReward(promotion: Promotion) {
if (!this.bot.accessToken) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'APP-REWARD',
- '跳过:应用访问令牌不可用,此活动需要它!'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'APP-REWARD', '跳过:应用访问令牌不可用,此活动需要它!')
return
}
diff --git a/src/functions/activities/app/DailyCheckIn.ts b/src/functions/activities/app/DailyCheckIn.ts
index 31570343..1d64e5a3 100644
--- a/src/functions/activities/app/DailyCheckIn.ts
+++ b/src/functions/activities/app/DailyCheckIn.ts
@@ -10,11 +10,7 @@ export class DailyCheckIn extends BaseActivity {
public async doDailyCheckIn() {
if (!this.bot.accessToken) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'DAILY-CHECK-IN',
- '跳过:应用访问令牌不可用,此活动需要它!'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'DAILY-CHECK-IN', '跳过:应用访问令牌不可用,此活动需要它!')
return
}
diff --git a/src/functions/activities/app/ReadToEarn.ts b/src/functions/activities/app/ReadToEarn.ts
index 7762fe3e..8518b33c 100644
--- a/src/functions/activities/app/ReadToEarn.ts
+++ b/src/functions/activities/app/ReadToEarn.ts
@@ -8,11 +8,7 @@ import { BaseActivity } from '../BaseActivity'
export class ReadToEarn extends BaseActivity {
public async doReadToEarn() {
if (!this.bot.accessToken) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'READ-TO-EARN',
- '跳过:应用访问令牌不可用,此活动需要它!'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'READ-TO-EARN', '跳过:应用访问令牌不可用,此活动需要它!')
return
}
diff --git a/src/functions/activities/experimental/ApiSearch.ts b/src/functions/activities/experimental/ApiSearch.ts
index 30a2b872..83da44fe 100644
--- a/src/functions/activities/experimental/ApiSearch.ts
+++ b/src/functions/activities/experimental/ApiSearch.ts
@@ -140,11 +140,7 @@ export class ApiSearch extends BaseActivity {
}
if (stagnant >= STAGNANT_LIMIT) {
- this.bot.logger.warn(
- isMobile,
- 'SEARCH-BING',
- `连续 ${STAGNANT_LIMIT} 次搜索未得分,中止`
- )
+ this.bot.logger.warn(isMobile, 'SEARCH-BING', `连续 ${STAGNANT_LIMIT} 次搜索未得分,中止`)
break
}
diff --git a/src/functions/activities/experimental/ApiSearchOnBing.ts b/src/functions/activities/experimental/ApiSearchOnBing.ts
index f2a16956..c18c6539 100644
--- a/src/functions/activities/experimental/ApiSearchOnBing.ts
+++ b/src/functions/activities/experimental/ApiSearchOnBing.ts
@@ -25,11 +25,7 @@ export class ApiSearchOnBing extends BaseActivity {
try {
if (!(await activateSearchOnBing(this.bot, promotion))) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'SEARCH-ON-BING',
- `搜索活动无法激活,中止 | offerId=${offerId}`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'SEARCH-ON-BING', `搜索活动无法激活,中止 | offerId=${offerId}`)
return
}
diff --git a/src/functions/activities/experimental/BingSearchApi.ts b/src/functions/activities/experimental/BingSearchApi.ts
index 321490af..05b74eb9 100644
--- a/src/functions/activities/experimental/BingSearchApi.ts
+++ b/src/functions/activities/experimental/BingSearchApi.ts
@@ -57,11 +57,7 @@ export class BingSearchApi {
const ig = this.readIg(searchResponse.data)
if (!ig) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'SEARCH-REPORT',
- `查询 "${query}" 无 IG - SERP 未按预期返回`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'SEARCH-REPORT', `查询 "${query}" 无 IG - SERP 未按预期返回`)
return { ig: null, ...this.emptyReport(), gained: null }
}
diff --git a/src/functions/activities/experimental/EdgeBrowsing.ts b/src/functions/activities/experimental/EdgeBrowsing.ts
index d95bc062..7ebedce6 100644
--- a/src/functions/activities/experimental/EdgeBrowsing.ts
+++ b/src/functions/activities/experimental/EdgeBrowsing.ts
@@ -74,11 +74,7 @@ export class EdgeBrowsing extends BaseActivity {
if (signal?.aborted) return
if (activation === 'absent') {
- this.bot.logger.info(
- this.bot.isMobile,
- LOG_TAG,
- '该账户无法使用 Edge 浏览连击(Browsing Streak)'
- )
+ this.bot.logger.info(this.bot.isMobile, LOG_TAG, '该账户无法使用 Edge 浏览连击(Browsing Streak)')
return
}
@@ -228,9 +224,7 @@ export class EdgeBrowsing extends BaseActivity {
this.bot.logger.error(
this.bot.isMobile,
LOG_TAG,
- `后台 Edge 浏览活动失败 | message=${
- error instanceof Error ? error.message : String(error)
- }`
+ `后台 Edge 浏览活动失败 | message=${error instanceof Error ? error.message : String(error)}`
)
}
}
@@ -301,30 +295,18 @@ export class EdgeBrowsing extends BaseActivity {
}
if (!offer.hash) {
- this.bot.logger.warn(
- this.bot.isMobile,
- LOG_TAG,
- `激活 offer 存在但缺少 hash | offerId=${offer.offerId}`
- )
+ this.bot.logger.warn(this.bot.isMobile, LOG_TAG, `激活 offer 存在但缺少 hash | offerId=${offer.offerId}`)
return 'failed'
}
if (!offer.reportable && !offer.isLocked) {
- this.bot.logger.warn(
- this.bot.isMobile,
- LOG_TAG,
- `激活 offer 不可操作 | offerId=${offer.offerId}`
- )
+ this.bot.logger.warn(this.bot.isMobile, LOG_TAG, `激活 offer 不可操作 | offerId=${offer.offerId}`)
return 'failed'
}
const actionId = this.bot.nextActions.reportActivity
if (!actionId) {
- this.bot.logger.warn(
- this.bot.isMobile,
- LOG_TAG,
- '跳过激活:bundle 中未发现 "reportActivity" action id'
- )
+ this.bot.logger.warn(this.bot.isMobile, LOG_TAG, '跳过激活:bundle 中未发现 "reportActivity" action id')
return 'failed'
}
@@ -647,11 +629,7 @@ export class EdgeBrowsing extends BaseActivity {
private resolveSettings(profile: AppDashboardData): EdgeBrowsingSettings | null {
const promotion = this.findPromotion(profile)
if (!promotion) {
- this.bot.logger.info(
- this.bot.isMobile,
- LOG_TAG,
- '该账户无法使用 Edge 浏览连击(Browsing Streak)'
- )
+ this.bot.logger.info(this.bot.isMobile, LOG_TAG, '该账户无法使用 Edge 浏览连击(Browsing Streak)')
return null
}
@@ -661,8 +639,7 @@ export class EdgeBrowsing extends BaseActivity {
this.bot.logger.warn(
this.bot.isMobile,
LOG_TAG,
- `跳过:优惠元数据不完整 | offerId=${offerId || 'missing'}` +
- ` | type=${activityType || 'missing'}`
+ `跳过:优惠元数据不完整 | offerId=${offerId || 'missing'}` + ` | type=${activityType || 'missing'}`
)
return null
}
diff --git a/src/functions/activities/rewards/DailySet.ts b/src/functions/activities/rewards/DailySet.ts
index e55a4352..b2337e40 100644
--- a/src/functions/activities/rewards/DailySet.ts
+++ b/src/functions/activities/rewards/DailySet.ts
@@ -13,11 +13,7 @@ export class DailySet extends BaseActivity {
return
}
- this.bot.logger.info(
- this.bot.isMobile,
- 'DAILY-SET',
- `开始解决"每日任务"项目 | remaining=${pending.length}`
- )
+ this.bot.logger.info(this.bot.isMobile, 'DAILY-SET', `开始解决"每日任务"项目 | remaining=${pending.length}`)
await new PromotionActivityRunner(this.bot).run(pending)
this.bot.logger.info(this.bot.isMobile, 'DAILY-SET', '"每日任务"项目处理完毕')
}
diff --git a/src/functions/activities/rewards/MorePromotions.ts b/src/functions/activities/rewards/MorePromotions.ts
index 907d731e..40ea6752 100644
--- a/src/functions/activities/rewards/MorePromotions.ts
+++ b/src/functions/activities/rewards/MorePromotions.ts
@@ -17,11 +17,7 @@ export class MorePromotions extends BaseActivity {
const pending = promotions.filter(promotion => this.isActionable(promotion))
if (!pending.length) {
- this.bot.logger.info(
- this.bot.isMobile,
- 'MORE-PROMOTIONS',
- '所有"更多推广"项目已完成'
- )
+ this.bot.logger.info(this.bot.isMobile, 'MORE-PROMOTIONS', '所有"更多推广"项目已完成')
return
}
diff --git a/src/functions/activities/rewards/PromotionActivityRunner.ts b/src/functions/activities/rewards/PromotionActivityRunner.ts
index 9f2299d3..391cdb3f 100644
--- a/src/functions/activities/rewards/PromotionActivityRunner.ts
+++ b/src/functions/activities/rewards/PromotionActivityRunner.ts
@@ -36,9 +36,7 @@ export class PromotionActivityRunner extends BaseActivity {
this.bot.logger.warn(
this.bot.isMobile,
'ACTIVITY',
- `跳过活动 "${promotion.title}" | offerId=${offerId} | 原因:不支持的类型 "${
- promotion.promotionType
- }"`
+ `跳过活动 "${promotion.title}" | offerId=${offerId} | 原因:不支持的类型 "${promotion.promotionType}"`
)
return
}
@@ -73,10 +71,6 @@ export class PromotionActivityRunner extends BaseActivity {
}
private logDisabled(activity: string, offerId: string): void {
- this.bot.logger.info(
- this.bot.isMobile,
- 'ACTIVITY',
- `跳过 "${activity}"(配置中已禁用) | offerId=${offerId}`
- )
+ this.bot.logger.info(this.bot.isMobile, 'ACTIVITY', `跳过 "${activity}"(配置中已禁用) | offerId=${offerId}`)
}
}
diff --git a/src/functions/activities/rewards/PunchCards.ts b/src/functions/activities/rewards/PunchCards.ts
index 92152db9..36fcf2a8 100644
--- a/src/functions/activities/rewards/PunchCards.ts
+++ b/src/functions/activities/rewards/PunchCards.ts
@@ -236,11 +236,7 @@ export class PunchCards extends BaseActivity {
private async reportQuestChild(child: QuestChild, parentId: string): Promise {
const actionId = this.bot.nextActions.reportActivity
if (!actionId) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'PUNCHCARD',
- `跳过 ${child.offerId}:未发现 "reportActivity"`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'PUNCHCARD', `跳过 ${child.offerId}:未发现 "reportActivity"`)
return
}
if (!child.hash) {
diff --git a/src/functions/activities/search/BrowserSearch.ts b/src/functions/activities/search/BrowserSearch.ts
index 1ae2bd28..3e089244 100644
--- a/src/functions/activities/search/BrowserSearch.ts
+++ b/src/functions/activities/search/BrowserSearch.ts
@@ -281,18 +281,10 @@ class PointsTracker implements SearchTracker {
if (this.missing.totalPoints <= 0) {
if (!this.runOnZeroPoints) {
- this.bot.logger.info(
- this.isMobile,
- this.context,
- '无可赚搜索积分,跳过(runOnZeroPoints 已禁用)'
- )
+ this.bot.logger.info(this.isMobile, this.context, '无可赚搜索积分,跳过(runOnZeroPoints 已禁用)')
return false
}
- this.bot.logger.info(
- this.isMobile,
- this.context,
- '未报告搜索积分,但 runOnZeroPoints 已启用,仍继续搜索'
- )
+ this.bot.logger.info(this.isMobile, this.context, '未报告搜索积分,但 runOnZeroPoints 已启用,仍继续搜索')
}
return true
}
diff --git a/src/functions/activities/search/BrowserSearchOnBing.ts b/src/functions/activities/search/BrowserSearchOnBing.ts
index d1d3d666..1b5eb6f8 100644
--- a/src/functions/activities/search/BrowserSearchOnBing.ts
+++ b/src/functions/activities/search/BrowserSearchOnBing.ts
@@ -25,11 +25,7 @@ export class SearchOnBing extends BaseActivity {
try {
const activated = await activateSearchOnBing(this.bot, promotion)
if (!activated) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'SEARCH-ON-BING',
- `搜索活动无法激活,中止 | offerId=${offerId}`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'SEARCH-ON-BING', `搜索活动无法激活,中止 | offerId=${offerId}`)
return
}
diff --git a/src/functions/activities/search/SearchOnBingShared.ts b/src/functions/activities/search/SearchOnBingShared.ts
index 1c91383d..1f90a46a 100644
--- a/src/functions/activities/search/SearchOnBingShared.ts
+++ b/src/functions/activities/search/SearchOnBingShared.ts
@@ -15,22 +15,14 @@ export async function activateSearchOnBing(bot: MicrosoftRewardsBot, promotion:
const actionId = bot.nextActions.reportActivity
if (!actionId) {
- bot.logger.warn(
- bot.isMobile,
- 'SEARCH-ON-BING-ACTIVATE',
- `跳过 ${offerId}: 未在 bundle 中发现 "reportActivity"`
- )
+ bot.logger.warn(bot.isMobile, 'SEARCH-ON-BING-ACTIVATE', `跳过 ${offerId}: 未在 bundle 中发现 "reportActivity"`)
return false
}
const live = await bot.browser.func.ensureOffer(offerId)
const hash = live?.hash ?? promotion.hash ?? null
if (!hash) {
- bot.logger.warn(
- bot.isMobile,
- 'SEARCH-ON-BING-ACTIVATE',
- `跳过 ${offerId}: 激活活动无可用 hash`
- )
+ bot.logger.warn(bot.isMobile, 'SEARCH-ON-BING-ACTIVATE', `跳过 ${offerId}: 激活活动无可用 hash`)
return false
}
diff --git a/src/functions/activities/visualSearch/VisualSearch.ts b/src/functions/activities/visualSearch/VisualSearch.ts
index 912e76a0..783c1f69 100644
--- a/src/functions/activities/visualSearch/VisualSearch.ts
+++ b/src/functions/activities/visualSearch/VisualSearch.ts
@@ -53,11 +53,7 @@ export class VisualSearch extends BaseActivity {
const available = streak?.isEnabled === true || activation === 'activated' || activation === 'already-active'
if (!available) {
- this.bot.logger.info(
- this.bot.isMobile,
- 'VISUAL-SEARCH',
- '该账户无法使用视觉搜索,跳过'
- )
+ this.bot.logger.info(this.bot.isMobile, 'VISUAL-SEARCH', '该账户无法使用视觉搜索,跳过')
return 0
}
@@ -79,11 +75,7 @@ export class VisualSearch extends BaseActivity {
private logStreakState(streak: StreakState | undefined): void {
if (!streak) {
- this.bot.logger.info(
- this.bot.isMobile,
- 'VISUAL-SEARCH',
- '快照中没有视觉搜索连击 - 回退到激活优惠'
- )
+ this.bot.logger.info(this.bot.isMobile, 'VISUAL-SEARCH', '快照中没有视觉搜索连击 - 回退到激活优惠')
return
}
@@ -94,11 +86,7 @@ export class VisualSearch extends BaseActivity {
)
if (!streak.isEnabled) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'VISUAL-SEARCH',
- '连击存在但未启用 - 开启前搜索不会被记录'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'VISUAL-SEARCH', '连击存在但未启用 - 开启前搜索不会被记录')
}
}
@@ -133,11 +121,7 @@ export class VisualSearch extends BaseActivity {
}
if (!offer.reportable && !offer.isLocked) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'VISUAL-SEARCH',
- `激活优惠无法执行 | offerId=${offer.offerId}`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'VISUAL-SEARCH', `激活优惠无法执行 | offerId=${offer.offerId}`)
return 'failed'
}
@@ -461,11 +445,7 @@ export class VisualSearch extends BaseActivity {
await this.bot.utils.wait(this.bot.utils.randomDelay(3000, 6000))
}
- this.bot.logger.warn(
- this.bot.isMobile,
- 'VISUAL-SEARCH',
- `尝试 ${MAX_ATTEMPTS} 次后每日视觉搜索仍未记分`
- )
+ this.bot.logger.warn(this.bot.isMobile, 'VISUAL-SEARCH', `尝试 ${MAX_ATTEMPTS} 次后每日视觉搜索仍未记分`)
return 0
}
diff --git a/src/functions/activities/visualSearch/VisualSearchBrowser.ts b/src/functions/activities/visualSearch/VisualSearchBrowser.ts
index dc21897a..6943f1d2 100644
--- a/src/functions/activities/visualSearch/VisualSearchBrowser.ts
+++ b/src/functions/activities/visualSearch/VisualSearchBrowser.ts
@@ -113,11 +113,7 @@ export class VisualSearchBrowser {
try {
const page = this.bot.mainDesktopPage
if (!page || page.isClosed()) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'VISUAL-SEARCH-BCID',
- '桌面端页面不可用 - 无法获取视觉搜索'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'VISUAL-SEARCH-BCID', '桌面端页面不可用 - 无法获取视觉搜索')
return null
}
@@ -191,11 +187,7 @@ export class VisualSearchBrowser {
public async getSeedUrls(): Promise {
const page = this.bot.mainDesktopPage
if (!page || page.isClosed()) {
- this.bot.logger.warn(
- this.bot.isMobile,
- 'VISUAL-SEARCH-BCID',
- '桌面端页面不可用 - 使用静态视觉搜索种子'
- )
+ this.bot.logger.warn(this.bot.isMobile, 'VISUAL-SEARCH-BCID', '桌面端页面不可用 - 使用静态视觉搜索种子')
return [STATIC_SEED_URL]
}
diff --git a/src/index.ts b/src/index.ts
index 494496ee..a4b3274c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -177,20 +177,12 @@ export class MicrosoftRewardsBot {
let refreshSucceeded = false
if (!account?.email) {
- this.logger.debug(
- this.isMobile,
- 'CONTEXT-REFRESH',
- `无法刷新奖励上下文 | 原因=${reason} | 账户=不可用`
- )
+ this.logger.debug(this.isMobile, 'CONTEXT-REFRESH', `无法刷新奖励上下文 | 原因=${reason} | 账户=不可用`)
return false
}
try {
- this.logger.warn(
- this.isMobile,
- 'CONTEXT-REFRESH',
- `请求失败后正在刷新奖励浏览器上下文 | 原因=${reason}`
- )
+ this.logger.warn(this.isMobile, 'CONTEXT-REFRESH', `请求失败后正在刷新奖励浏览器上下文 | 原因=${reason}`)
if (!page || page.isClosed()) {
recoverySession = await this.browserFactory.createBrowser(account)
@@ -595,9 +587,10 @@ export class MicrosoftRewardsBot {
)
}
} else {
- const errorDetail = flowError && flowError !== 'undefined'
- ? flowError.replace(/\r?\n/g, ' ').slice(0, 120)
- : '流程失败(未捕获到错误详情)'
+ const errorDetail =
+ flowError && flowError !== 'undefined'
+ ? flowError.replace(/\r?\n/g, ' ').slice(0, 120)
+ : '流程失败(未捕获到错误详情)'
accountStats.push({
email: accountEmail,
@@ -674,9 +667,7 @@ export class MicrosoftRewardsBot {
this.logger.info(
'main',
'ACCOUNT-DELAY',
- `等待 ${(delayMs / 1000).toFixed(1)} 秒后开始下一个账户${
- nextEmail ? ` (${nextEmail})` : ''
- }`
+ `等待 ${(delayMs / 1000).toFixed(1)} 秒后开始下一个账户${nextEmail ? ` (${nextEmail})` : ''}`
)
await this.utils.wait(delayMs)
}
@@ -785,11 +776,7 @@ export class MicrosoftRewardsBot {
desktopSession = await this.createDesktopSession(account)
})
await closeDesktopSession()
- this.logger.info(
- 'main',
- 'FLOW',
- '桌面会话预热完成 | 后续桌面阶段将直接复用已保存会话'
- )
+ this.logger.info('main', 'FLOW', '桌面会话预热完成 | 后续桌面阶段将直接复用已保存会话')
} catch (error) {
this.logger.warn(
'main',
@@ -803,11 +790,7 @@ export class MicrosoftRewardsBot {
if (fullApi) {
await closeMobileSession()
- this.logger.info(
- 'main',
- 'FLOW',
- '移动端登录浏览器已关闭;继续使用已保存的会话和 HTTP 请求'
- )
+ this.logger.info('main', 'FLOW', '移动端登录浏览器已关闭;继续使用已保存的会话和 HTTP 请求')
}
const data: DashboardData = await this.browser.func.getDashboardData()
@@ -926,9 +909,7 @@ export class MicrosoftRewardsBot {
this.logger.error(
this.isMobile,
'EDGE-BROWSING',
- `意外的后台任务失败 | 信息=${
- error instanceof Error ? error.message : String(error)
- }`
+ `意外的后台任务失败 | 信息=${error instanceof Error ? error.message : String(error)}`
)
})
.finally(() => {
@@ -1104,11 +1085,7 @@ export class MicrosoftRewardsBot {
if (edgeBrowsingTask) {
if (!edgeBrowsingFinished) {
- this.logger.info(
- this.isMobile,
- 'EDGE-BROWSING',
- '前台活动已完成;正在等待后台 Edge 浏览活动'
- )
+ this.logger.info(this.isMobile, 'EDGE-BROWSING', '前台活动已完成;正在等待后台 Edge 浏览活动')
}
await edgeBrowsingTask
edgeBrowsingTask = null
diff --git a/src/logging/ClawBot.ts b/src/logging/ClawBot.ts
index 067149d7..e5e754f1 100644
--- a/src/logging/ClawBot.ts
+++ b/src/logging/ClawBot.ts
@@ -37,7 +37,9 @@ export async function sendClawBot(config: WebhookClawBotConfig, content: string)
// context_token 缺失或失效(ret=-2):清除后等待下次启动时重新激活
auth.contextToken = undefined
saveClawBotAuth(auth, config.authFile)
- console.warn('[ClawBot] 推送上下文失效,本次跳过;下次运行时请在微信给「微信 ClawBot」发一条消息完成激活')
+ console.warn(
+ '[ClawBot] 推送上下文失效,本次跳过;下次运行时请在微信给「微信 ClawBot」发一条消息完成激活'
+ )
} else if (result === 'error') {
console.warn('[ClawBot] 发送失败,已跳过(不影响任务运行)')
}
diff --git a/src/util/ClawBotClient.ts b/src/util/ClawBotClient.ts
index e93007d3..3f8ec967 100644
--- a/src/util/ClawBotClient.ts
+++ b/src/util/ClawBotClient.ts
@@ -47,7 +47,8 @@ export function loadClawBotAuth(customPath?: string): ClawBotAuth | null {
accountId: typeof raw.accountId === 'string' ? raw.accountId : '',
userId: raw.userId,
contextToken: typeof raw.contextToken === 'string' && raw.contextToken ? raw.contextToken : undefined,
- getUpdatesBuf: typeof raw.getUpdatesBuf === 'string' && raw.getUpdatesBuf ? raw.getUpdatesBuf : undefined,
+ getUpdatesBuf:
+ typeof raw.getUpdatesBuf === 'string' && raw.getUpdatesBuf ? raw.getUpdatesBuf : undefined,
savedAt: typeof raw.savedAt === 'string' ? raw.savedAt : new Date().toISOString()
}
}
@@ -99,11 +100,7 @@ async function iLinkPost(url: string, data: unknown, opts: { token?: string;
return response.data
}
-async function iLinkGet(
- url: string,
- params: Record,
- opts: { timeout?: number } = {}
-): Promise {
+async function iLinkGet(url: string, params: Record, opts: { timeout?: number } = {}): Promise {
const response = await httpRequest({
method: 'GET',
url,
@@ -221,7 +218,10 @@ export interface ClawBotLoginResult {
* 交互式扫码登录:终端展示二维码,等待手机微信确认。
* 总超时默认 5 分钟;期间处理数字配对码、二维码过期刷新与 IDC 重定向。
*/
-export async function loginClawBotInteractive(customAuthPath?: string, timeoutMs = DEFAULT_LOGIN_TIMEOUT_MS): Promise {
+export async function loginClawBotInteractive(
+ customAuthPath?: string,
+ timeoutMs = DEFAULT_LOGIN_TIMEOUT_MS
+): Promise {
const existing = loadClawBotAuth(customAuthPath)
const localTokenList = existing?.token ? [existing.token] : []
@@ -275,7 +275,9 @@ export async function loginClawBotInteractive(customAuthPath?: string, timeoutMs
}
break
case 'need_verifycode': {
- const prompt = pendingVerifyCode ? '❌ 数字不匹配,请重新输入手机微信显示的数字:' : '请输入手机微信上显示的数字以继续:'
+ const prompt = pendingVerifyCode
+ ? '❌ 数字不匹配,请重新输入手机微信显示的数字:'
+ : '请输入手机微信上显示的数字以继续:'
pendingVerifyCode = await readVerifyCodeFromStdin(prompt)
continue
}
diff --git a/src/util/Humanize.ts b/src/util/Humanize.ts
index c99e6f29..e1ca7f10 100644
--- a/src/util/Humanize.ts
+++ b/src/util/Humanize.ts
@@ -110,11 +110,7 @@ export async function waitForQuietHours(): Promise {
if (waitMs <= 0) return
const resumeAt = new Date(Date.now() + waitMs)
- bot?.logger.info(
- 'main',
- 'HUMANIZE',
- `当前处于静默时段,挂起到 ${formatClock(resumeAt)}(humanize.quietHours)`
- )
+ bot?.logger.info('main', 'HUMANIZE', `当前处于静默时段,挂起到 ${formatClock(resumeAt)}(humanize.quietHours)`)
await new Promise(resolve => setTimeout(resolve, waitMs))
}
}
From 8ae6df50d38bf99126262ed2c10d7daf63ca03a0 Mon Sep 17 00:00:00 2001
From: GoldenSection0618 <172290907+GoldenSection0618@users.noreply.github.com>
Date: Sat, 24 Oct 2026 05:38:57 +0000
Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E6=8C=89=E5=BD=93=E6=97=A5=E7=AD=BE?=
=?UTF-8?q?=E5=88=B0=E8=AE=B0=E5=BD=95=E4=BF=AE=E6=AD=A3=E5=89=A9=E4=BD=99?=
=?UTF-8?q?=E7=A7=AF=E5=88=86=E5=B9=B6=E8=B7=B3=E8=BF=87=E9=87=8D=E5=A4=8D?=
=?UTF-8?q?=E7=AD=BE=E5=88=B0=E8=AF=B7=E6=B1=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
scripts/api/checkInState.test.cjs | 194 +++++++++++++++++++
scripts/api/rewardsPoints.fixture.cjs | 59 ++++++
src/browser/BrowserFunc.ts | 20 +-
src/functions/activities/app/CheckInState.ts | 50 +++++
src/functions/activities/app/DailyCheckIn.ts | 55 +++++-
src/index.ts | 16 +-
src/interface/Points.ts | 4 +-
7 files changed, 370 insertions(+), 28 deletions(-)
create mode 100644 scripts/api/checkInState.test.cjs
create mode 100644 scripts/api/rewardsPoints.fixture.cjs
create mode 100644 src/functions/activities/app/CheckInState.ts
diff --git a/scripts/api/checkInState.test.cjs b/scripts/api/checkInState.test.cjs
new file mode 100644
index 00000000..fc653351
--- /dev/null
+++ b/scripts/api/checkInState.test.cjs
@@ -0,0 +1,194 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+require('../../node_modules/ts-node/register/transpile-only.js')
+const BrowserFunc = require('../../src/browser/BrowserFunc.ts').default
+const { DailyCheckIn } = require('../../src/functions/activities/app/DailyCheckIn.ts')
+const { resolveCheckInState } = require('../../src/functions/activities/app/CheckInState.ts')
+const { createBot: createActivityBot, account } = require('./rewardsPoints.fixture.cjs')
+
+const now = new Date('2026-10-24T05:00:00Z')
+for (const channel of [undefined, 'SAAndroid']) {
+ test(`App dashboard query uses ${channel ?? 'the existing SAIOS default'}`, async t => {
+ const { bot } = createActivityBot(t)
+ bot.http.request = t.mock.fn(async () => ({ data: { response: { promotions: [] } } }))
+ await new BrowserFunc(bot).getAppDashboardData(channel)
+ assert.ok(bot.http.request.mock.calls[0].arguments[0].url.includes(`channel=${channel ?? 'SAIOS'}`))
+ })
+}
+function legacy(overrides = {}) {
+ return {
+ name: 'Gamification_Sapphire_DailyCheckIn_Promotion',
+ attributes: {
+ offerid: 'Gamification_Sapphire_DailyCheckIn',
+ type: 'checkin',
+ progress: '1',
+ day_1_points: '5',
+ day_2_points: '5',
+ last_updated: '2026-10-22T17:00:00Z',
+ ...overrides
+ }
+ }
+}
+function streak(updatedAt = '2026-10-23T17:26:19.6377348+00:00') {
+ return {
+ name: 'Gamification_Streak_Counter_Promotion',
+ attributes: {
+ type: 'streak',
+ activity_progress: '26',
+ last_updated: updatedAt
+ }
+ }
+}
+const recordedLegacy = legacy({ hidden: 'True', last_updated: '2024-10-18T08:21:27.1990138+00:00' })
+
+test('Main leaves the App remaining total unknown when only stale check-in data is available', async t => {
+ t.mock.timers.enable({ apis: ['Date'], now: now.getTime() })
+ const { bot } = createActivityBot(t)
+ bot.config.workers.doDailyCheckIn = true
+ bot.http.request = t.mock.fn(async () => ({ data: { response: { promotions: [recordedLegacy] } } }))
+ bot.browser.func.getAppEarnablePoints = () => new BrowserFunc(bot).getAppEarnablePoints()
+ await bot.Main(account)
+ const message = bot.logger.info.mock.calls.find(call => call.arguments[1] === 'POINTS').arguments[2]
+ assert.match(message, /App: 未知/)
+})
+
+for (const [name, promotions, status, remaining] of [
+ ['recorded current check-in overrides stale hidden promotion', [recordedLegacy, streak()], 'complete', 0],
+ ['stale hidden promotion alone does not invent five points', [recordedLegacy], 'unknown', null],
+ ['missing timestamp remains unknown', [legacy({ last_updated: '' })], 'unknown', null],
+ ['invalid timestamp remains unknown', [legacy({ last_updated: 'invalid' })], 'unknown', null],
+ [
+ 'same day number in a previous month is not today',
+ [legacy({ last_updated: '2026-09-24T01:00:00Z' })],
+ 'pending',
+ 5
+ ],
+ [
+ 'same day number in a previous year is not today',
+ [legacy({ last_updated: '2025-10-24T01:00:00Z' })],
+ 'pending',
+ 5
+ ],
+ ['China date uses the date after UTC midnight offset', [streak()], 'complete', 0],
+ ['previous China date is not already checked in', [legacy(), streak('2026-10-23T15:59:00Z')], 'pending', 5],
+ ['seventh day reads its explicit reward', [legacy({ progress: '6', day_7_points: '20' })], 'pending', 20],
+ ['new seven-day cycle returns to the first day', [legacy({ progress: '7' })], 'pending', 5],
+ [
+ 'future timestamp is not a completed check-in',
+ [legacy({ last_updated: '2026-10-25T05:00:00Z' })],
+ 'unknown',
+ null
+ ],
+ ['invalid progress does not invent points', [legacy({ progress: 'bad' })], 'unknown', null],
+ ['missing promotion remains unknown', [], 'unknown', null]
+]) {
+ test(name, () => {
+ const state = resolveCheckInState(promotions, 'CN', now)
+ assert.equal(state.status, status)
+ assert.equal(state.remaining, remaining)
+ })
+}
+
+for (const completed of [true, false]) {
+ test(`App remaining points ${completed ? 'confirm today complete' : 'stay unknown for stale hidden data'}`, async t => {
+ t.mock.timers.enable({ apis: ['Date'], now: now.getTime() })
+ const { bot } = createActivityBot(t)
+ bot.http.request = t.mock.fn(async () => ({
+ data: {
+ response: {
+ promotions: completed ? [recordedLegacy, streak()] : [recordedLegacy]
+ }
+ }
+ }))
+ const points = await new BrowserFunc(bot).getAppEarnablePoints()
+ assert.equal(points.checkIn, completed ? 0 : null)
+ assert.equal(points.totalEarnablePoints, completed ? 0 : null)
+ })
+}
+
+function checkInBot(t, promotions = [legacy()]) {
+ t.mock.timers.enable({ apis: ['Date'], now: now.getTime() })
+ const { bot } = createActivityBot(t)
+ bot.accessToken = 'offline-token'
+ bot.http.request = t.mock.fn(async () => ({ status: 200, data: { code: 0, response: { balance: 100 } } }))
+ bot.browser.func.getAppDashboardData = t.mock.fn(async () => ({ response: { promotions } }))
+ return bot
+}
+
+test('already checked-in account skips the repeated request', async t => {
+ const bot = checkInBot(t, [recordedLegacy, streak()])
+ const activity = new DailyCheckIn(bot)
+ await activity.doDailyCheckIn()
+
+ assert.equal(bot.http.request.mock.callCount(), 0)
+ assert.equal(bot.userData.gainedPoints, 0)
+})
+
+test('a pending check-in credits only the observed balance change', async t => {
+ const bot = checkInBot(t)
+ bot.http.request = t.mock.fn(async () => ({ status: 200, data: { code: 0, response: { balance: 105 } } }))
+ const activity = new DailyCheckIn(bot)
+ await activity.doDailyCheckIn()
+
+ assert.equal(bot.http.request.mock.callCount(), 1)
+ assert.equal(bot.userData.currentPoints, 105)
+ assert.equal(bot.userData.gainedPoints, 5)
+})
+
+for (const promotions of [[legacy()], []]) {
+ test(`zero balance change with ${promotions.length ? 'pending' : 'unknown'} task state is not called complete`, async t => {
+ const bot = checkInBot(t, promotions)
+ const activity = new DailyCheckIn(bot)
+ await assert.rejects(activity.doDailyCheckIn(), /签到未确认到账/)
+
+ assert.equal(bot.userData.gainedPoints, 0)
+ assert.ok(!bot.logger.info.mock.calls.some(call => /每日签到完成/.test(call.arguments[2])))
+ })
+}
+
+for (const response of [
+ { status: 500, data: { code: 0, response: { balance: 105 } } },
+ { status: 200, data: { code: 9, response: { balance: 105 } } }
+]) {
+ test(`unsuccessful check-in status=${response.status} code=${response.data.code} does not credit points`, async t => {
+ const bot = checkInBot(t)
+ bot.http.request = async () => response
+ await assert.rejects(new DailyCheckIn(bot).doDailyCheckIn(), /签到接口未确认成功/)
+ assert.equal(bot.userData.gainedPoints, 0)
+ })
+}
+
+test('missing response balance is verified by a fresh balance query', async t => {
+ const bot = checkInBot(t)
+ bot.http.request = async () => ({ status: 200, data: { code: 0 } })
+ bot.browser.func.getCurrentPoints = t.mock.fn(async () => 105)
+ await new DailyCheckIn(bot).doDailyCheckIn()
+ assert.equal(bot.browser.func.getCurrentPoints.mock.callCount(), 1)
+ assert.equal(bot.userData.gainedPoints, 5)
+})
+
+test('invalid balance cannot be replaced with a successful zero-point result', async t => {
+ const bot = checkInBot(t)
+ bot.http.request = async () => ({ status: 200, data: { code: 0 } })
+ bot.browser.func.getCurrentPoints = async () => NaN
+ await assert.rejects(new DailyCheckIn(bot).doDailyCheckIn(), /无法取得有效余额/)
+})
+
+test('connection reset is propagated to the caller', async t => {
+ const bot = checkInBot(t)
+ bot.http.request = async () => {
+ throw new Error('ConnectionReset (10054)')
+ }
+ await assert.rejects(new DailyCheckIn(bot).doDailyCheckIn(), /10054/)
+})
+
+test('a post-request completion record confirms the day without inventing a balance gain', async t => {
+ const bot = checkInBot(t)
+ let reads = 0
+ bot.browser.func.getAppDashboardData = async () => ({
+ response: { promotions: ++reads === 1 ? [legacy()] : [streak()] }
+ })
+ await new DailyCheckIn(bot).doDailyCheckIn()
+ assert.equal(bot.userData.gainedPoints, 0)
+ assert.ok(bot.logger.info.mock.calls.some(call => /本次无新增积分/.test(call.arguments[2])))
+})
diff --git a/scripts/api/rewardsPoints.fixture.cjs b/scripts/api/rewardsPoints.fixture.cjs
new file mode 100644
index 00000000..0906385f
--- /dev/null
+++ b/scripts/api/rewardsPoints.fixture.cjs
@@ -0,0 +1,59 @@
+const { MicrosoftRewardsBot } = require('../../src/index.ts')
+
+function createBot(t) {
+ const bot = Object.create(MicrosoftRewardsBot.prototype)
+ const dashboard = {
+ dashboard: {
+ userStatus: { availablePoints: 100, counters: {} },
+ userProfile: { attributes: { country: 'CN' } },
+ dailySetPromotions: {},
+ morePromotions: [],
+ userWarnings: []
+ }
+ }
+ bot.userData = { currentPoints: 100, gainedPoints: 0, geoLocale: 'CN', langCode: 'zh-CN' }
+ bot.nextActions = {}
+ bot.accessToken = ''
+ bot.cookies = { mobile: [], desktop: [] }
+ bot.accountLocale = { country: 'CN' }
+ bot.config = {
+ workers: {},
+ experimental: { apiSearch: false, apiSearchOnBing: false, edgeBrowsing: false },
+ activities: { searchOnBing: false },
+ searchSettings: { parallelSearching: false }
+ }
+ bot.utils = { getFormattedDate: () => '2026-10-24', randomDelay: min => min, wait: t.mock.fn(async () => {}) }
+ bot.logger = Object.fromEntries(['info', 'warn', 'error', 'debug'].map(level => [level, t.mock.fn()]))
+ bot.http = {
+ request: t.mock.fn(async () => {
+ throw new Error('Unexpected HTTP request')
+ }),
+ setDefaultHeaders: () => {}
+ }
+ bot.login = { login: async () => {}, getAppAccessToken: async () => 'test-token' }
+ bot.browserFactory = {
+ createBrowser: async () => ({
+ context: {
+ newPage: async () => ({}),
+ cookies: async () => []
+ },
+ fingerprint: {}
+ })
+ }
+ bot.browser = {
+ func: {
+ getDashboardData: async () => dashboard,
+ getAppDashboardData: async () => ({ response: { promotions: [] } }),
+ getCurrentPoints: t.mock.fn(async () => 100),
+ checkpointActiveSession: async () => {},
+ closeBrowser: async () => {},
+ getBrowserEarnablePoints: async () => ({ desktopSearchPoints: 0, mobileSearchPoints: 0 }),
+ getAppEarnablePoints: async () => ({ totalEarnablePoints: 0 })
+ }
+ }
+ bot.searchManager = { getSearchPoints: async () => ({ doMobile: false, doDesktop: false }) }
+ bot.activities = { doDailyCheckIn: async () => {} }
+ return { bot, dashboard }
+}
+
+module.exports = { createBot, account: { email: 'test@example.invalid', geoLocale: 'CN', langCode: 'zh-CN' } }
diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts
index 6c202b95..f540e67d 100644
--- a/src/browser/BrowserFunc.ts
+++ b/src/browser/BrowserFunc.ts
@@ -1,3 +1,4 @@
+import { resolveCheckInState } from '../functions/activities/app/CheckInState'
import { URLs } from '../constants/urls'
import { BING_APP_USER_AGENT } from '../constants/userAgents'
import type { BrowserContext, Cookie, Page } from 'patchright'
@@ -147,10 +148,10 @@ export default class BrowserFunc {
return error instanceof Error ? error.message : String(error)
}
- async getAppDashboardData(): Promise {
+ async getAppDashboardData(channel = 'SAIOS'): Promise {
try {
const request: HttpRequestConfig = {
- url: URLs.platform.me('SAIOS'),
+ url: URLs.platform.me(channel),
method: 'GET',
headers: {
Authorization: `Bearer ${this.bot.accessToken}`,
@@ -248,7 +249,6 @@ export default class BrowserFunc {
)
let readToEarn = 0
- let checkIn = 0
for (const item of eligibleActivities) {
const attrs = item.attributes
@@ -257,19 +257,11 @@ export default class BrowserFunc {
const pointMax = parseInt(attrs.pointmax ?? '0')
const pointProgress = parseInt(attrs.pointprogress ?? '0')
readToEarn = Math.max(0, pointMax - pointProgress)
- } else if (attrs.type === 'checkin') {
- const progress = parseInt(attrs.progress ?? '0')
- const checkInDay = progress % 7
- const lastUpdated = new Date(attrs.last_updated ?? '')
- const today = new Date()
-
- if (checkInDay < 6 && today.getDate() !== lastUpdated.getDate()) {
- checkIn = parseInt(attrs[`day_${checkInDay + 1}_points`] ?? '0')
- }
}
}
-
- const totalEarnablePoints = readToEarn + checkIn
+ const checkInState = resolveCheckInState(userData.response.promotions, this.bot.userData.geoLocale)
+ const checkIn = checkInState.remaining
+ const totalEarnablePoints = checkIn === null ? null : readToEarn + checkIn
return {
readToEarn,
diff --git a/src/functions/activities/app/CheckInState.ts b/src/functions/activities/app/CheckInState.ts
new file mode 100644
index 00000000..706ded3e
--- /dev/null
+++ b/src/functions/activities/app/CheckInState.ts
@@ -0,0 +1,50 @@
+import type { Promotion } from '../../../interface/AppDashBoardData'
+
+export interface CheckInState {
+ status: 'complete' | 'pending' | 'unknown'
+ remaining: number | null
+ updatedAt: string | null
+ reason: string
+}
+
+function dateKey(value: string | Date, country: string): string | null {
+ const date = new Date(value)
+ if (!Number.isFinite(date.getTime())) return null
+ return new Intl.DateTimeFormat('en-CA', {
+ timeZone: country.toUpperCase() === 'CN' ? 'Asia/Shanghai' : undefined,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ }).format(date)
+}
+
+export function resolveCheckInState(
+ promotions: Pick[],
+ country: string,
+ now = new Date()
+): CheckInState {
+ const today = dateKey(now, country)
+ const checkIn = promotions.find(item => item.attributes.offerid === 'Gamification_Sapphire_DailyCheckIn')
+ const streak = promotions.find(item => item.name === 'Gamification_Streak_Counter_Promotion')
+ for (const item of [streak, checkIn]) {
+ const updatedAt = item?.attributes.last_updated
+ if (updatedAt && dateKey(updatedAt, country) === today) {
+ return { status: 'complete', remaining: 0, updatedAt, reason: '签到记录已更新到今天' }
+ }
+ }
+
+ const attrs = checkIn?.attributes
+ const updatedAt = attrs?.last_updated ?? null
+ const updatedDay = updatedAt ? dateKey(updatedAt, country) : null
+ const unknown = (reason: string): CheckInState => ({ status: 'unknown', remaining: null, updatedAt, reason })
+ if (!attrs || !updatedDay || !today || updatedDay > today) return unknown('缺少有效的签到更新时间')
+ if (attrs.hidden?.toLowerCase() === 'true' || attrs.give_eligible?.toLowerCase() === 'false') {
+ return unknown('旧签到推广不可作为今日可领取额度')
+ }
+ const progress = attrs.progress ? Number(attrs.progress) : NaN
+ if (!Number.isInteger(progress) || progress < 0) return unknown('签到进度不可确认')
+ const points = attrs[`day_${(progress % 7) + 1}_points`]
+ const remaining = points ? Number(points) : NaN
+ if (!Number.isFinite(remaining) || remaining < 0) return unknown('未提供当日签到积分')
+ return { status: 'pending', remaining, updatedAt, reason: '今日尚无签到记录' }
+}
diff --git a/src/functions/activities/app/DailyCheckIn.ts b/src/functions/activities/app/DailyCheckIn.ts
index 1d64e5a3..ec223c15 100644
--- a/src/functions/activities/app/DailyCheckIn.ts
+++ b/src/functions/activities/app/DailyCheckIn.ts
@@ -2,6 +2,7 @@ import { URLs } from '../../../constants/urls'
import type { HttpRequestConfig } from '../../../util/Http'
import { randomUUID } from 'crypto'
import { BaseActivity } from '../BaseActivity'
+import { resolveCheckInState, type CheckInState } from './CheckInState'
export class DailyCheckIn extends BaseActivity {
private gainedPoints: number = 0
@@ -15,15 +16,33 @@ export class DailyCheckIn extends BaseActivity {
}
this.oldBalance = Number(this.bot.userData.currentPoints ?? 0)
+ const before = await this.getCheckInState()
+ if (before.status === 'complete') {
+ this.bot.logger.info(
+ this.bot.isMobile,
+ 'DAILY-CHECK-IN',
+ `今日已签到,跳过重复请求 | updatedAt=${before.updatedAt} | currentBalance=${this.oldBalance}`
+ )
+ return
+ }
this.bot.logger.info(
this.bot.isMobile,
'DAILY-CHECK-IN',
- `开始每日签到 | geo=${this.bot.userData.geoLocale} | currentBalance=${this.oldBalance}`
+ `开始每日签到 | geo=${this.bot.userData.geoLocale} | currentBalance=${this.oldBalance} | state=${before.status} | expected=${before.remaining ?? '未知'}`
)
try {
const response = await this.submitDaily()
+ if (
+ response.status < 200 ||
+ response.status >= 300 ||
+ (response.data?.code !== undefined && response.data.code !== 0)
+ ) {
+ throw new Error(
+ `签到接口未确认成功 | status=${response.status} | code=${response.data?.code ?? 'unknown'}`
+ )
+ }
this.bot.logger.debug(
this.bot.isMobile,
@@ -31,7 +50,12 @@ export class DailyCheckIn extends BaseActivity {
`已收到每日签到响应 | status=${response?.status ?? 'unknown'}`
)
- const newBalance = Number(response?.data?.response?.balance ?? this.oldBalance)
+ const responseBalance = response.data?.response?.balance
+ const newBalance =
+ typeof responseBalance === 'number' && Number.isFinite(responseBalance)
+ ? responseBalance
+ : await this.bot.browser.func.getCurrentPoints()
+ if (!Number.isFinite(newBalance) || newBalance < 0) throw new Error('无法取得有效余额,签到到账状态未知')
this.gainedPoints = newBalance - this.oldBalance
this.bot.logger.debug(
@@ -51,10 +75,16 @@ export class DailyCheckIn extends BaseActivity {
'green'
)
} else {
- this.bot.logger.warn(
+ const after = await this.getCheckInState()
+ if (after.status !== 'complete') {
+ throw new Error(
+ `签到未确认到账 | pointsGained=${this.gainedPoints} | currentBalance=${newBalance} | state=${after.status}`
+ )
+ }
+ this.bot.logger.info(
this.bot.isMobile,
'DAILY-CHECK-IN',
- `每日签到完成但未获得积分 | type=103 | pointsGained=0 | currentBalance=${newBalance}`
+ `已确认今日签到,本次无新增积分 | pointsGained=0 | currentBalance=${newBalance} | updatedAt=${after.updatedAt}`
)
}
} catch (error) {
@@ -63,6 +93,21 @@ export class DailyCheckIn extends BaseActivity {
'DAILY-CHECK-IN',
`每日签到出错 | message=${error instanceof Error ? error.message : String(error)}`
)
+ throw error
+ }
+ }
+
+ private async getCheckInState(): Promise {
+ try {
+ const data = await this.bot.browser.func.getAppDashboardData('SAAndroid')
+ return resolveCheckInState(data.response?.promotions ?? [], this.bot.userData.geoLocale)
+ } catch (error) {
+ this.bot.logger.warn(
+ this.bot.isMobile,
+ 'DAILY-CHECK-IN',
+ `签到状态读取失败 | ${error instanceof Error ? error.message : String(error)}`
+ )
+ return { status: 'unknown', remaining: null, updatedAt: null, reason: '状态读取失败' }
}
}
@@ -109,7 +154,7 @@ export class DailyCheckIn extends BaseActivity {
`正在发送每日签到请求 | type=${jsonData.type} | url=${request.url}`
)
- return this.bot.http.request<{ response?: { balance?: number } }>(request)
+ return this.bot.http.request<{ code?: number; response?: { balance?: number } }>(request)
} catch (error) {
this.bot.logger.error(
this.bot.isMobile,
diff --git a/src/index.ts b/src/index.ts
index a4b3274c..48994697 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -891,7 +891,7 @@ export class MicrosoftRewardsBot {
'POINTS',
`今日可赚取 | 移动端: ${browserEarnable.mobileSearchPoints} | 浏览器: ${
browserEarnable.desktopSearchPoints
- } | App: ${appEarnable?.totalEarnablePoints ?? 0} | ${accountEmail} | locale: ${this.accountLocale.locale}`
+ } | App: ${appEarnable?.totalEarnablePoints ?? '未知'} | ${accountEmail} | locale: ${this.accountLocale.locale}`
)
const parallel = this.config.searchSettings.parallelSearching
@@ -1195,9 +1195,11 @@ async function main(): Promise {
}
}
-main().catch(async error => {
- const tmpBot = new MicrosoftRewardsBot()
- tmpBot.logger.error('main', 'MAIN-ERROR', error as Error)
- await flushAllWebhooks()
- process.exit(1)
-})
+if (require.main === module) {
+ main().catch(async error => {
+ const tmpBot = new MicrosoftRewardsBot()
+ tmpBot.logger.error('main', 'MAIN-ERROR', error as Error)
+ await flushAllWebhooks()
+ process.exit(1)
+ })
+}
diff --git a/src/interface/Points.ts b/src/interface/Points.ts
index e5020560..57fdb3de 100644
--- a/src/interface/Points.ts
+++ b/src/interface/Points.ts
@@ -8,8 +8,8 @@ export interface BrowserEarnablePoints {
export interface AppEarnablePoints {
readToEarn: number
- checkIn: number
- totalEarnablePoints: number
+ checkIn: number | null
+ totalEarnablePoints: number | null
}
export interface MissingSearchPoints {
From 164882f282e77fbc9102eaeb538e75ded93ac103 Mon Sep 17 00:00:00 2001
From: GoldenSection0618 <172290907+GoldenSection0618@users.noreply.github.com>
Date: Sat, 24 Oct 2026 16:21:54 +0800
Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E9=9A=94=E7=A6=BB=E7=AD=BE=E5=88=B0?=
=?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=B9=B6=E4=BF=9D=E7=95=99=E5=90=8E=E7=BB=AD?=
=?UTF-8?q?=E4=BB=BB=E5=8A=A1=E4=B8=8E=E7=A7=AF=E5=88=86=E7=BB=9F=E8=AE=A1?=
=?UTF-8?q?=EF=BC=8C=E5=B0=86=E5=A4=B1=E8=B4=A5=E7=8A=B6=E6=80=81=E4=BC=A0?=
=?UTF-8?q?=E9=80=92=E8=87=B3=E6=9C=80=E7=BB=88=E6=B1=87=E6=80=BB=E5=92=8C?=
=?UTF-8?q?=E9=80=80=E5=87=BA=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
scripts/api/checkInFlow.test.cjs | 213 ++++++++++++++++++++++++++++++
scripts/api/checkInState.test.cjs | 29 ++++
scripts/api/logParser.js | 15 ++-
src/index.ts | 38 ++++--
4 files changed, 277 insertions(+), 18 deletions(-)
create mode 100644 scripts/api/checkInFlow.test.cjs
diff --git a/scripts/api/checkInFlow.test.cjs b/scripts/api/checkInFlow.test.cjs
new file mode 100644
index 00000000..a2b7776e
--- /dev/null
+++ b/scripts/api/checkInFlow.test.cjs
@@ -0,0 +1,213 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+const fs = require('node:fs')
+const cluster = require('node:cluster')
+const { EventEmitter } = require('node:events')
+require('../../node_modules/ts-node/register/transpile-only.js')
+const HttpClient = require('../../src/util/Http.ts').default
+const Utils = require('../../src/util/Utils.ts').default
+const sessionStore = require('../../src/util/SessionStore.ts')
+const { DailyCheckIn } = require('../../src/functions/activities/app/DailyCheckIn.ts')
+const { createBot, account: accountFixture } = require('./rewardsPoints.fixture.cjs')
+const account = { ...accountFixture, proxy: { proxyHttp: false, url: '', port: 0, username: '', password: '' } }
+
+function flowBot(t, mode, failure) {
+ const { bot } = createBot(t)
+ bot.config.workers = { doDailyCheckIn: true, doReadToEarn: true, doMobileSearch: true }
+ bot.config.experimental.apiSearch = mode === 'full-api'
+ const response = async () => {
+ if (failure === 'reset') throw new Error('ConnectionReset (10054)')
+ return { status: 200, data: { code: 0, response: { balance: failure ? 100 : 105 } } }
+ }
+ bot.http.request = t.mock.fn(response)
+ bot.activities.doDailyCheckIn = () => new DailyCheckIn(bot).doDailyCheckIn()
+ const later = []
+ bot.activities.doReadToEarn = async () => {
+ later.push('read')
+ }
+ bot.searchManager.getSearchPoints = async () => ({ doMobile: true, doDesktop: false })
+ bot.searchManager.searchMobile = async () => {
+ later.push('search')
+ return 10
+ }
+ bot.browser.func.getCurrentPoints = async () => 110
+ return { bot, later, response }
+}
+
+for (const mode of ['browser', 'full-api']) {
+ for (const failure of [false, 'reset']) {
+ test(mode + ': check-in ' + (failure || 'success') + ' retains later work and normal intervals', async t => {
+ const { bot, later } = flowBot(t, mode, failure)
+ const result = await bot.Main(account)
+ assert.deepEqual(
+ later,
+ ['read', 'search'],
+ JSON.stringify(bot.logger.error.mock.calls.map(call => call.arguments))
+ )
+ assert.equal(result.collectedPoints, 10)
+ if (failure) assert.match(result.error, /10054/)
+ else assert.equal(result.error, undefined)
+ assert.deepEqual(
+ bot.utils.wait.mock.calls.map(call => call.arguments),
+ [[60000], [60000]]
+ )
+ })
+ }
+}
+
+function isolateRun(t, bot, primary) {
+ const previousPrimary = cluster.isPrimary
+ cluster.isPrimary = primary
+ t.after(() => {
+ cluster.isPrimary = previousPrimary
+ })
+ t.mock.method(sessionStore, 'loadResolvedRegion', () => null)
+ const writes = t.mock.method(fs, 'writeFileSync', () => {})
+ t.mock.method(fs, 'mkdirSync', () => {})
+ const exits = t.mock.method(process, 'exit', () => {})
+ bot.utils = Object.assign(new Utils(), bot.utils)
+ bot.accounts = [account]
+ bot.exitedWorkers = []
+ bot.config.clusters = primary ? 1 : 2
+ bot.config.humanize = { enabled: false }
+ bot.config.webhook = {}
+ bot.config.sessionPath = 'offline-sessions'
+ return { writes, exits }
+}
+
+for (const primary of [true, false]) {
+ for (const failed of [false, true]) {
+ test(
+ (primary ? 'single process' : 'worker') + ': check-in outcome reaches the final result, failed=' + failed,
+ async t => {
+ const { bot, later, response } = flowBot(t, 'browser', failed)
+ const { writes, exits } = isolateRun(t, bot, primary)
+ t.mock.method(HttpClient.prototype, 'request', response)
+ let workerTask
+ const messages = []
+ if (!primary) {
+ t.mock.method(process, 'on', (event, handler) => {
+ if (event === 'message') workerTask = handler
+ return process
+ })
+ const previousSend = process.send
+ process.send = message => {
+ messages.push(message)
+ }
+ t.after(() => {
+ if (previousSend === undefined) delete process.send
+ else process.send = previousSend
+ })
+ }
+ await bot.run()
+ if (!primary) await workerTask({ chunk: [account], runStartTime: Date.now() })
+ assert.deepEqual(
+ later,
+ ['read', 'search'],
+ JSON.stringify(bot.logger.error.mock.calls.map(call => call.arguments))
+ )
+ assert.equal(exits.mock.calls.at(-1).arguments[0], failed ? 1 : 0)
+ assert.equal(writes.mock.callCount(), primary && !failed ? 1 : 0)
+ if (primary) {
+ const footer = bot.logger.info.mock.calls.find(call => call.arguments[1] === 'RUN-END').arguments
+ assert.equal(footer[3], failed ? 'red' : 'green')
+ assert.match(footer[2], /获得积分=10/)
+ if (failed) assert.match(footer[2], /存在失败活动/)
+ } else {
+ const result = messages[0].__stats[0]
+ assert.equal(result.success, !failed)
+ assert.equal(result.initialPoints, 100)
+ assert.equal(result.finalPoints, 110)
+ if (failed) assert.match(result.error, /签到未确认到账/)
+ }
+ }
+ )
+ }
+}
+
+for (const failed of [false, true]) {
+ test('cluster primary preserves an account failure even when the worker exits zero, failed=' + failed, async t => {
+ const { bot } = createBot(t)
+ const { writes, exits } = isolateRun(t, bot, true)
+ bot.config.clusters = 2
+ bot.utils.chunkArray = accounts => [accounts]
+ const worker = new EventEmitter()
+ worker.process = { pid: 12345 }
+ worker.send = () => {}
+ t.mock.method(cluster, 'fork', () => worker)
+ let onExit
+ t.mock.method(cluster, 'on', (event, handler) => {
+ if (event === 'exit') onExit = handler
+ return cluster
+ })
+ let finished
+ const completion = new Promise(resolve => {
+ finished = resolve
+ })
+ exits.mock.mockImplementation(code => {
+ finished(code)
+ })
+ await bot.run()
+ worker.emit('message', {
+ __stats: [
+ {
+ email: account.email,
+ initialPoints: 100,
+ finalPoints: 110,
+ collectedPoints: 10,
+ duration: 1,
+ success: !failed,
+ error: failed ? '签到未确认到账' : undefined
+ }
+ ]
+ })
+ onExit(worker, 0)
+ assert.equal(await completion, failed ? 1 : 0)
+ assert.equal(writes.mock.callCount(), failed ? 0 : 1)
+ const footer = bot.logger.info.mock.calls.find(call => call.arguments[1] === 'RUN-END').arguments
+ assert.equal(footer[3], failed ? 'red' : 'green')
+ assert.match(footer[2], /获得积分=10/)
+ })
+}
+
+for (const app of ['未知', '0']) {
+ test('log parser preserves an App allowance of ' + app, async () => {
+ const { createRunState, parseLogLine, applyLogToRunState, summarizeRunState } = await import('./logParser.js')
+ const state = createRunState()
+ applyLogToRunState(
+ state,
+ parseLogLine(
+ '[2026/10/24 13:00:00] [test] [INFO] MAIN [POINTS] 今日可赚取 | 移动端: 0 | 浏览器: 0 | App: ' +
+ app +
+ ' | test@example.invalid | locale: zh-CN'
+ )
+ )
+ assert.deepEqual(summarizeRunState(state).accounts[0]?.earnable, {
+ mobile: 0,
+ browser: 0,
+ app: app === '未知' ? null : 0
+ })
+ })
+}
+
+for (const failed of [false, true]) {
+ test('log parser retains final points and account outcome, failed=' + failed, async () => {
+ const { createRunState, parseLogLine, applyLogToRunState, summarizeRunState } = await import('./logParser.js')
+ const state = createRunState()
+ const messages = [
+ '[ACCOUNT-END] 账户' +
+ (failed ? '存在失败活动' : '完成') +
+ ': test@example.invalid | 获得积分=10 | 原余额=100 | 现余额=110 | 持续秒数=1',
+ '[RUN-END] ' +
+ (failed ? '账户处理结束,存在失败活动' : '全部账户完成') +
+ ' | 处理账户数=1 | 获得积分=10 | 原余额=100 | 现余额=110 | 运行分钟数=1'
+ ]
+ for (const message of messages)
+ applyLogToRunState(state, parseLogLine('[2026/10/24 13:00:00] [MAIN] [INFO] MAIN ' + message))
+ const summary = summarizeRunState(state)
+ assert.equal(summary.finished, true)
+ assert.equal(summary.collected, 10)
+ assert.equal(summary.accounts[0].success, !failed)
+ assert.equal(summary.accounts[0].finalPoints, 110)
+ })
+}
diff --git a/scripts/api/checkInState.test.cjs b/scripts/api/checkInState.test.cjs
index fc653351..cf8a905a 100644
--- a/scripts/api/checkInState.test.cjs
+++ b/scripts/api/checkInState.test.cjs
@@ -192,3 +192,32 @@ test('a post-request completion record confirms the day without inventing a bala
assert.equal(bot.userData.gainedPoints, 0)
assert.ok(bot.logger.info.mock.calls.some(call => /本次无新增积分/.test(call.arguments[2])))
})
+
+for (const mode of ['browser', 'full-api']) {
+ test(mode + ': unconfirmed check-in preserves later tasks and the failure result', async t => {
+ const { bot } = createActivityBot(t)
+ bot.config.workers = { doDailyCheckIn: true, doReadToEarn: true, doMobileSearch: true }
+ bot.config.experimental.apiSearch = mode === 'full-api'
+ bot.http.request = t.mock.fn(async () => ({ status: 200, data: { code: 0, response: { balance: 100 } } }))
+ bot.activities.doDailyCheckIn = () => new DailyCheckIn(bot).doDailyCheckIn()
+ const later = []
+ bot.activities.doReadToEarn = async () => {
+ later.push('read')
+ }
+ bot.searchManager.getSearchPoints = async () => ({ doMobile: true, doDesktop: false })
+ bot.searchManager.searchMobile = async () => {
+ later.push('search')
+ return 10
+ }
+ bot.browser.func.getCurrentPoints = async () => 110
+ const result = await bot.Main(account)
+ assert.deepEqual(later, ['read', 'search'])
+ assert.match(result.error, /签到未确认到账/)
+ assert.equal(result.initialPoints, 100)
+ assert.equal(result.collectedPoints, 10)
+ assert.deepEqual(
+ bot.utils.wait.mock.calls.map(call => call.arguments),
+ [[60000], [60000]]
+ )
+ })
+}
diff --git a/scripts/api/logParser.js b/scripts/api/logParser.js
index 8b8c3928..0f1c2f22 100644
--- a/scripts/api/logParser.js
+++ b/scripts/api/logParser.js
@@ -101,12 +101,14 @@ function ensureAccount(state, email) {
const RE = {
runStart: /^启动微软奖励脚本 \| v(\S+) \| 账户数: (\d+) \| 集群数: (\d+)/,
accountStart: /^开始处理账户: (\S+) \| geoLocale: ([^|]+?)(?: \| locale: (\S+))?(?: \| 缓存区域: (\S+))?\s*$/,
- earnable: /^今日可赚取 \| 移动端: (\d+) \| 浏览器: (\d+) \| App: (\d+) \| (\S+) \| locale: (\S+)\s*$/,
+ earnable:
+ /^今日可赚取 \| 移动端: (\d+|未提供|未知) \| 浏览器: (\d+|未提供|未知) \| App: (\d+|未提供|未知) \| (\S+) \| locale: (\S+)\s*$/,
searchSummary: /^搜索汇总 \| 移动端=(-?\d+) \| 桌面端=(-?\d+) \| 额外=(-?\d+) \| 总计=(-?\d+)/,
streakProtection:
/^快照完成 \| offers=(\d+) \| 可上报=(\d+) \| streaks=(\d+) \| 连续保护已启用=(true|false|null) \| 连续保护剩余天数=(\d+|null) \| 连续计数=(\d+|null) \| 等级=([^|]+) \| 账户=(\S+@\S+)$/,
- accountEnd: /^账户完成: (\S+) \| 获得积分=(-?\d+) \| 原余额=(\d+) \| 现余额=(\d+) \| 持续秒数=([\d.]+)/,
- runEnd: /^全部账户完成 \| 处理账户数=(\d+) \| 获得积分=(-?\d+) \| 原余额=(\d+) \| 现余额=(\d+) \| 运行分钟数=([\d.]+)/,
+ accountEnd:
+ /^账户(?:完成|存在失败活动): (\S+) \| 获得积分=(-?\d+) \| 原余额=(\d+) \| 现余额=(\d+) \| 持续秒数=([\d.]+)/,
+ runEnd: /^(?:全部账户完成|账户处理结束,存在失败活动) \| 处理账户数=(\d+) \| 获得积分=(-?\d+) \| 原余额=(\d+) \| 现余额=(\d+) \| 运行分钟数=([\d.]+)/,
accountError: /^(\S+@\S+) \| 错误=([\s\S]+)$/,
flowFailed: /(\S+@\S+) 的.*流程失败:/i,
accountDelay: /^等待 ([\d.]+) 秒后开始下一个账户(?: \((\S+@\S+)\))?$/,
@@ -414,7 +416,10 @@ export function applyLogToRunState(state, entry) {
const email = m[4]
const acc = ensureAccount(state, email)
if (acc) {
- acc.earnable = { mobile: Number(m[1]), browser: Number(m[2]), app: Number(m[3]) }
+ const [mobile, browser, app] = m
+ .slice(1, 4)
+ .map(value => (/^\d+$/.test(value) ? Number(value) : null))
+ acc.earnable = { mobile, browser, app }
acc.locale ??= m[5]
}
state.currentEmail = email
@@ -458,7 +463,7 @@ export function applyLogToRunState(state, entry) {
acc.initialPoints = Number(m[3])
acc.finalPoints = Number(m[4])
acc.durationSeconds = Number(m[5])
- acc.success = true
+ acc.success = !msg.startsWith('账户存在失败活动:')
acc.live.gained = Number(m[2])
acc.live.balance = Number(m[4])
const at = eventTime(entry)
diff --git a/src/index.ts b/src/index.ts
index 48994697..ac2a07ea 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -60,6 +60,7 @@ interface AccountRunResult {
initialPoints: number
collectedPoints: number
skippedForBotWarning?: boolean
+ error?: string
}
const executionContext = new AsyncLocalStorage()
@@ -369,6 +370,7 @@ export class MicrosoftRewardsBot {
)
if (this.activeWorkers <= 0) {
+ hadWorkerFailure ||= allAccountStats.some(stat => !stat.success)
const totalCollectedPoints = allAccountStats.reduce((sum, s) => sum + s.collectedPoints, 0)
const totalInitialPoints = allAccountStats.reduce((sum, s) => sum + s.initialPoints, 0)
const totalFinalPoints = allAccountStats.reduce((sum, s) => sum + s.finalPoints, 0)
@@ -377,8 +379,8 @@ export class MicrosoftRewardsBot {
this.logger.info(
'main',
'RUN-END',
- `全部账户完成 | 处理账户数=${allAccountStats.length} | 获得积分=${totalCollectedPoints} | 原余额=${totalInitialPoints} | 现余额=${totalFinalPoints} | 运行分钟数=${totalDurationMinutes}`,
- 'green'
+ `${hadWorkerFailure ? '账户处理结束,存在失败活动' : '全部账户完成'} | 处理账户数=${allAccountStats.length} | 获得积分=${totalCollectedPoints} | 原余额=${totalInitialPoints} | 现余额=${totalFinalPoints} | 运行分钟数=${totalDurationMinutes}`,
+ hadWorkerFailure ? 'red' : 'green'
)
await this.sendPushPlusSummary(allAccountStats, runStartTime, hadWorkerFailure)
@@ -419,7 +421,7 @@ export class MicrosoftRewardsBot {
}
await flushAllWebhooks()
- process.exit(0)
+ process.exit(stats.some(stat => !stat.success) ? 1 : 0)
} catch (error) {
this.logger.error(
'main',
@@ -576,14 +578,15 @@ export class MicrosoftRewardsBot {
finalPoints: accountFinalPoints,
collectedPoints: collectedPoints,
duration: parseFloat(durationSeconds),
- success: true
+ success: !result.error,
+ error: result.error
})
this.logger.info(
'main',
'ACCOUNT-END',
- `账户完成: ${accountEmail} | 获得积分=${collectedPoints} | 原余额=${accountInitialPoints} | 现余额=${accountFinalPoints} | 持续秒数=${durationSeconds}`,
- 'green'
+ `账户${result.error ? '存在失败活动' : '完成'}: ${accountEmail} | 获得积分=${collectedPoints} | 原余额=${accountInitialPoints} | 现余额=${accountFinalPoints} | 持续秒数=${durationSeconds}`,
+ result.error ? 'red' : 'green'
)
}
} else {
@@ -623,6 +626,7 @@ export class MicrosoftRewardsBot {
}
if (this.config.clusters <= 1 && cluster.isPrimary) {
+ const hadFailure = accountStats.some(s => !s.success)
const totalCollectedPoints = accountStats.reduce((sum, s) => sum + s.collectedPoints, 0)
const totalInitialPoints = accountStats.reduce((sum, s) => sum + s.initialPoints, 0)
const totalFinalPoints = accountStats.reduce((sum, s) => sum + s.finalPoints, 0)
@@ -631,18 +635,17 @@ export class MicrosoftRewardsBot {
this.logger.info(
'main',
'RUN-END',
- `全部账户完成 | 处理账户数=${accountStats.length} | 获得积分=${totalCollectedPoints} | 原余额=${totalInitialPoints} | 现余额=${totalFinalPoints} | 运行分钟数=${totalDurationMinutes}`,
- 'green'
+ `${hadFailure ? '账户处理结束,存在失败活动' : '全部账户完成'} | 处理账户数=${accountStats.length} | 获得积分=${totalCollectedPoints} | 原余额=${totalInitialPoints} | 现余额=${totalFinalPoints} | 运行分钟数=${totalDurationMinutes}`,
+ hadFailure ? 'red' : 'green'
)
- const hadFailure = accountStats.some(s => !s.success)
await this.sendPushPlusSummary(accountStats, runStartTime, hadFailure)
await this.sendServerChanSummary(accountStats, runStartTime, hadFailure)
await this.sendClawBotSummary(accountStats, runStartTime, hadFailure)
await flushAllWebhooks()
if (!hadFailure) markRunSucceeded()
- process.exit(0)
+ process.exit(hadFailure ? 1 : 0)
}
return accountStats
@@ -706,6 +709,14 @@ export class MicrosoftRewardsBot {
this.config.workers.doAppPromotions ||
this.config.workers.doReadToEarn
const needsAppAccessToken = this.config.experimental.edgeBrowsing || needsAppActivities
+ let checkInError: string | undefined
+ const runDailyCheckIn = async (): Promise => {
+ try {
+ await this.activities.doDailyCheckIn()
+ } catch (error) {
+ checkInError = error instanceof Error ? error.message : String(error)
+ }
+ }
let mobileSession: BrowserSession | null = null
let desktopSession: BrowserSession | null = null
@@ -958,7 +969,7 @@ export class MicrosoftRewardsBot {
await this.gapBetweenWorkers()
}
if (appAvailable && this.config.workers.doDailyCheckIn) {
- await this.activities.doDailyCheckIn()
+ await runDailyCheckIn()
await this.gapBetweenWorkers()
}
if (appAvailable && this.config.workers.doAppPromotions && appData) {
@@ -990,7 +1001,7 @@ export class MicrosoftRewardsBot {
await this.gapBetweenWorkers()
}
if (appAvailable && this.config.workers.doDailyCheckIn) {
- await this.activities.doDailyCheckIn()
+ await runDailyCheckIn()
await this.gapBetweenWorkers()
}
if (appAvailable && this.config.workers.doAppPromotions && appData) {
@@ -1102,7 +1113,8 @@ export class MicrosoftRewardsBot {
return {
initialPoints,
- collectedPoints: collectedPoints || 0
+ collectedPoints: collectedPoints || 0,
+ error: checkInError
}
})
} finally {