diff --git a/README.md b/README.md index 94f8214..3700727 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 9114ed3..6187be2 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/package.json b/package.json index 2fe69fd..cb11433 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "test:log-parser": "node --test scripts/api/logParser.test.js", + "test:worker-gaps": "node --test scripts/api/workerGaps.test.cjs", "format": "prettier --write .", "format:check": "prettier --check .", "clear-diagnostics": "rimraf diagnostics", diff --git a/scripts/api/logParser.js b/scripts/api/logParser.js index bfdb440..8b8c392 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/scripts/api/workerGaps.fixture.cjs b/scripts/api/workerGaps.fixture.cjs new file mode 100644 index 0000000..e3eacbc --- /dev/null +++ b/scripts/api/workerGaps.fixture.cjs @@ -0,0 +1,140 @@ +const { MicrosoftRewardsBot } = require('../../src/index.ts') + +const stages = [ + 'doEnsureStreakProtection', + 'doDailySet', + 'doActivateSearchPerk', + 'doMorePromotions', + 'doDailyCheckIn', + 'doAppPromotions', + 'doReadToEarn', + 'doPunchCardsMobile', + 'doPunchCardsDesktop', + 'doClaimBonusPoints' +] +const postWaitStages = [ + 'doDailySet', + 'doActivateSearchPerk', + 'doMorePromotions', + 'doDailyCheckIn', + 'doAppPromotions', + 'doReadToEarn', + 'doPunchCardsMobile' +] +const account = { email: 'offline@example.invalid', geoLocale: 'CN', langCode: 'zh-CN' } + +function promotion(overrides = {}) { + return { + offerId: 'offline-offer', + title: 'Offline task', + promotionType: 'urlreward', + complete: false, + pointProgress: 0, + pointProgressMax: 10, + priority: 1, + attributes: {}, + ...overrides + } +} + +function createActivityBot(t) { + const dashboard = { + dashboard: { + dailySetPromotions: {}, + morePromotions: [], + morePromotionsWithoutPromotionalItems: [], + punchCards: [], + userWarnings: [], + userStatus: { availablePoints: 100 }, + userProfile: { attributes: { country: 'CN' } } + } + } + const bot = { + isMobile: false, + accessToken: '', + nextActions: {}, + cookies: { desktop: [] }, + userData: { currentPoints: 100, gainedPoints: 0, geoLocale: 'CN', langCode: 'zh-CN' }, + config: { + workers: { doPunchCards: true }, + skipNonPointTasks: true, + searchSettings: { readDelay: { min: 1000, max: 2000 } } + }, + logger: Object.fromEntries(['info', 'warn', 'error', 'debug'].map(level => [level, t.mock.fn()])), + utils: { + getFormattedDate: () => '2026-10-24', + randomDelay: t.mock.fn(min => min), + wait: t.mock.fn(async () => {}) + }, + http: { + request: t.mock.fn(async () => ({ status: 200, data: { response: { balance: 100 } } })), + setDefaultHeaders: t.mock.fn() + }, + activities: { doAppReward: t.mock.fn(async () => {}) }, + browser: { + func: { + getDashboardData: t.mock.fn(async () => dashboard), + getAppDashboardData: t.mock.fn(async () => ({ response: { promotions: [] } })), + getCurrentPoints: t.mock.fn(async () => 100), + reportServerAction: t.mock.fn(async () => ({ status: 200, acknowledged: true })), + ensureOffer: t.mock.fn(async () => null), + getRewardsPageHtml: t.mock.fn(async () => 'offline-html'), + checkpointActiveSession: t.mock.fn(async () => {}), + closeBrowser: t.mock.fn(async () => {}), + getBrowserEarnablePoints: t.mock.fn(async () => ({ mobileSearchPoints: 0, desktopSearchPoints: 0 })), + getAppEarnablePoints: t.mock.fn(async () => ({ totalEarnablePoints: 0 })) + }, + react: { + getStreakProtection: () => ({ isProtectionOn: true, remainingDays: 3 }), + routerStateTree: () => 'offline-router-state' + } + } + } + return { bot, dashboard } +} + +function createFlowBot(t, mode, executed = []) { + const { bot, dashboard } = createActivityBot(t) + delete bot.isMobile + Object.setPrototypeOf(bot, MicrosoftRewardsBot.prototype) + const events = [] + bot.config.ensureStreakProtection = true + bot.config.workers = Object.fromEntries( + [ + 'doDailySet', + 'doActivateSearchPerk', + 'doMorePromotions', + 'doDailyCheckIn', + 'doAppPromotions', + 'doReadToEarn', + 'doPunchCards', + 'doClaimBonusPoints' + ].map(name => [name, true]) + ) + bot.config.experimental = { apiSearch: mode === 'full-api', apiSearchOnBing: false, edgeBrowsing: false } + bot.config.activities = { searchOnBing: false } + bot.config.searchSettings.parallelSearching = false + bot.accountLocale = { country: 'CN' } + bot.login = { login: async () => {}, getAppAccessToken: async () => 'offline-token' } + bot.browserFactory = { + createBrowser: t.mock.fn(async () => ({ + context: { newPage: async () => ({}), cookies: async () => [] }, + fingerprint: {} + })) + } + bot.searchManager = { getSearchPoints: async () => ({ doMobile: false, doDesktop: false }) } + if (executed.includes('doClaimBonusPoints')) bot.nextActions.reportClaimAllPoints = 'offline-claim-action' + bot.activities = Object.fromEntries( + stages.map(name => [ + name, + t.mock.fn(async () => { + events.push(name) + return executed.includes(name) + }) + ]) + ) + bot.utils.wait = t.mock.fn(async delay => events.push(['wait', delay])) + return { bot, dashboard, events } +} + +module.exports = { stages, postWaitStages, account, promotion, createActivityBot, createFlowBot } diff --git a/scripts/api/workerGaps.test.cjs b/scripts/api/workerGaps.test.cjs new file mode 100644 index 0000000..f66695e --- /dev/null +++ b/scripts/api/workerGaps.test.cjs @@ -0,0 +1,392 @@ +const assert = require('node:assert/strict') +const test = require('node:test') + +require('../../node_modules/ts-node/register/transpile-only.js') +const Activities = require('../../src/functions/Activities.ts').default +const { PromotionActivityRunner } = require('../../src/functions/activities/rewards/PromotionActivityRunner.ts') +const { + stages, + postWaitStages, + account, + promotion, + createActivityBot, + createFlowBot +} = require('./workerGaps.fixture.cjs') + +function questChild(overrides = {}) { + return { + offerId: 'pcchild-pending', + hash: 'offline-hash', + reportable: true, + isLocked: false, + isDisabled: false, + isCompleted: false, + ...overrides + } +} + +function setQuests(bot, parents, children) { + bot.browser.react.snapshotQuestList = () => parents ?? [] + bot.browser.react.snapshotQuestPage = () => children + bot.browser.react.questRouterStateTree = () => 'offline-router-state' + if (parents === null) bot.browser.func.getRewardsPageHtml = async () => null + bot.activities.doClaimReward = new Activities(bot).doClaimReward +} + +for (const [name, items, expected] of [ + ['empty', [], []], + ['completed', [promotion({ complete: true })], []], + ['zero-point', [promotion({ pointProgressMax: 0 })], []], + ['pending', [promotion()], ['offline-offer']], + ['mixed', [promotion({ complete: true, offerId: 'completed' }), promotion()], ['offline-offer']] +]) { + for (const method of ['doDailySet', 'doMorePromotions']) { + test(`${method}: ${name} tasks report execution only when actionable`, async t => { + const { bot, dashboard } = createActivityBot(t) + dashboard.dashboard.dailySetPromotions['2026-10-24'] = items + dashboard.dashboard.morePromotions = items + const run = t.mock.method(PromotionActivityRunner.prototype, 'run', async () => true) + const didExecute = await new Activities(bot)[method](dashboard) + assert.equal(didExecute, expected.length > 0) + assert.equal(run.mock.callCount(), expected.length > 0 ? 1 : 0) + if (expected.length) + assert.deepEqual( + run.mock.calls[0].arguments[0].map(item => item.offerId), + expected + ) + assert.equal(bot.http.request.mock.callCount(), 0) + assert.equal(bot.utils.wait.mock.callCount(), 0) + }) + } +} + +test('more promotions merge both feeds and execute a duplicated pending offer only once', async t => { + const { bot, dashboard } = createActivityBot(t) + dashboard.dashboard.morePromotions = [promotion({ complete: true, offerId: 'done' }), promotion()] + dashboard.dashboard.morePromotionsWithoutPromotionalItems = [promotion(), promotion({ offerId: 'second' })] + const run = t.mock.method(PromotionActivityRunner.prototype, 'run', async () => true) + assert.equal(await new Activities(bot).doMorePromotions(dashboard), true) + assert.deepEqual( + run.mock.calls[0].arguments[0].map(item => item.offerId), + ['offline-offer', 'second'] + ) +}) + +test('a completed invocation does not inherit execution from the previous invocation', async t => { + const { bot, dashboard } = createActivityBot(t) + const activities = new Activities(bot) + const run = t.mock.method(PromotionActivityRunner.prototype, 'run', async () => true) + dashboard.dashboard.morePromotions = [promotion()] + assert.equal(await activities.doMorePromotions(dashboard), true) + dashboard.dashboard.morePromotions[0].complete = true + assert.equal(await activities.doMorePromotions(dashboard), false) + assert.equal(run.mock.callCount(), 1) +}) + +for (const [name, attributes, expected] of [ + ['empty', null, false], + ['completed', { complete: 'True', type: 'sapphire', offerid: 'app-done' }, false], + ['unsupported', { complete: 'False', type: 'other', offerid: 'app-other' }, false], + ['missing offer id', { complete: 'False', type: 'sapphire' }, false], + ['pending', { complete: 'False', type: 'sapphire', offerid: 'app-pending' }, true] +]) { + test(`app promotions: ${name} tasks return the execution state`, async t => { + const { bot } = createActivityBot(t) + const promotions = attributes ? [{ attributes }] : [] + assert.equal(await new Activities(bot).doAppPromotions({ response: { promotions } }), expected) + assert.equal(bot.activities.doAppReward.mock.callCount(), expected ? 1 : 0) + assert.equal(bot.utils.wait.mock.callCount(), 0) + }) +} + +for (const method of ['doPunchCardsMobile', 'doPunchCardsDesktop']) { + for (const [name, parents, children, reports, autoClaim, actionAvailable] of [ + ['unavailable', null, [], 0], + ['empty', [], [], 0], + ['completed', [promotion({ complete: true })], [questChild()], 0], + ['zero-point', [promotion({ pointProgressMax: 0 })], [questChild()], 0], + ['no children', [promotion()], [], 0], + ['locked', [promotion()], [questChild({ reportable: false, isLocked: true })], 0], + ['disabled', [promotion()], [questChild({ reportable: false, isDisabled: true })], 0], + ['child completed', [promotion()], [questChild({ reportable: false, isCompleted: true })], 0], + ['missing hash', [promotion()], [questChild({ reportable: false, hash: null })], 0], + ['search quota', [promotion()], [questChild({ offerId: 'pcchild-search-day' })], 0], + ['manual claim', [promotion()], [questChild({ offerId: 'pcchild-claim' })], 0, false], + ['missing action', [promotion()], [questChild()], 0, false, false], + ['claim missing action', [promotion()], [questChild({ offerId: 'pcchild-claim' })], 0, true, false], + ['pending', [promotion()], [questChild()], 1], + ['automatic claim', [promotion()], [questChild({ offerId: 'pcchild-claim' })], 1, true], + ['mixed', [promotion()], [questChild({ reportable: false }), questChild()], 1] + ]) { + test(`${method}: ${name} quests execute only when actionable`, async t => { + const { bot, dashboard } = createActivityBot(t) + setQuests(bot, parents, children) + bot.nextActions.reportActivity = actionAvailable === false ? '' : 'offline-action' + bot.config.autoClaimPunchcardRewards = Boolean(autoClaim) + assert.equal( + await new Activities(bot)[method](dashboard), + method === 'doPunchCardsMobile' ? reports > 0 : undefined + ) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), reports) + assert.deepEqual( + bot.utils.wait.mock.calls.map(call => call.arguments), + reports ? [[5000]] : [] + ) + assert.equal(bot.http.request.mock.callCount(), 0) + }) + } +} + +test('an attempted punchcard request still counts as work when its response fails', async t => { + const { bot, dashboard } = createActivityBot(t) + setQuests(bot, [promotion()], [questChild()]) + bot.nextActions.reportActivity = 'offline-action' + bot.browser.func.reportServerAction.mock.mockImplementation(async () => { + throw new Error('ConnectionReset (10054)') + }) + assert.equal(await new Activities(bot).doPunchCardsMobile(dashboard), true) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), 1) + assert.deepEqual( + bot.utils.wait.mock.calls.map(call => call.arguments), + [[5000]] + ) +}) + +for (const method of ['doDailyCheckIn', 'doReadToEarn']) { + for (const available of [false, true]) { + test(`${method}: ${available ? 'attempted zero-gain activity counts as work' : 'missing token skips without work'}`, async t => { + const { bot } = createActivityBot(t) + bot.accessToken = available ? 'offline-token' : '' + assert.equal(await new Activities(bot)[method](), available) + assert.equal(bot.http.request.mock.callCount(), available ? 1 : 0) + }) + } +} + +for (const available of [false, true]) { + test(`claim bonus: ${available ? 'submitted action counts as work' : 'missing action skips without work'}`, async t => { + const { bot } = createActivityBot(t) + if (available) bot.nextActions.reportClaimAllPoints = 'offline-action' + assert.equal(await new Activities(bot).doClaimBonusPoints(), undefined) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), available ? 1 : 0) + }) +} + +for (const [name, state, expected] of [ + ['already enabled', { isProtectionOn: true, remainingDays: 3 }, false], + ['no remaining days', { isProtectionOn: false, remainingDays: 0 }, false], + ['pending', { isProtectionOn: false, remainingDays: 3 }, true] +]) { + test(`streak protection: ${name} submits an action only when needed`, async t => { + const { bot } = createActivityBot(t) + bot.nextActions.reportSetStreakProtection = 'offline-action' + bot.reactSnapshot = { streakProtection: state } + assert.equal(await new Activities(bot).doEnsureStreakProtection(), undefined) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), expected ? 1 : 0) + }) +} + +for (const reportable of [false, true]) { + test(`search perk: ${reportable ? 'pending activation counts as work' : 'activated offer skips without work'}`, async t => { + const { bot, dashboard } = createActivityBot(t) + dashboard.dashboard.promotionalItem = promotion({ attributes: { searchMultiplier: '2' } }) + bot.nextActions.reportActivity = 'offline-action' + bot.browser.func.ensureOffer = async () => ({ reportable, hash: 'offline-hash' }) + assert.equal(await new Activities(bot).doActivateSearchPerk(dashboard), reportable) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), reportable ? 1 : 0) + }) +} + +for (const mode of ['browser', 'full-api']) { + for (const [name, children, reports, autoClaim, actionAvailable] of [ + ['all children skipped', [questChild({ reportable: false }), questChild({ reportable: false })], 0], + ['mixed children', [questChild({ reportable: false }), questChild()], 1], + ['manual claim', [questChild({ offerId: 'pcchild-claim' })], 0, false], + ['automatic claim', [questChild({ offerId: 'pcchild-claim' })], 1, true], + ['missing action', [questChild()], 0, false, false] + ]) { + test(`${mode}: real punchcards with ${name} preserve only intervals for submitted work`, async t => { + const { bot, events } = createFlowBot(t, mode) + setQuests(bot, [promotion()], children) + bot.nextActions.reportActivity = actionAvailable === false ? '' : 'offline-action' + bot.config.autoClaimPunchcardRewards = Boolean(autoClaim) + bot.activities.doPunchCardsMobile = new Activities(bot).doPunchCardsMobile + await bot.Main(account) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), reports) + assert.deepEqual( + bot.utils.wait.mock.calls.map(call => call.arguments), + reports ? [[5000], [60000]] : [] + ) + if (reports) assert.ok(events.findIndex(event => Array.isArray(event) && event[1] === 60000) >= 0) + assert.equal(bot.http.request.mock.callCount(), 0) + }) + } + + test(`${mode}: ten skipped stages produce no worker waits`, async t => { + const { bot, events } = createFlowBot(t, mode) + await bot.Main(account) + assert.deepEqual([...events].sort(), [...stages].sort()) + assert.equal(bot.utils.wait.mock.callCount(), 0) + assert.equal(bot.utils.randomDelay.mock.callCount(), 0) + }) + + for (const stage of stages) { + const hasWait = postWaitStages.includes(stage) || stage === 'doClaimBonusPoints' + test(`${mode}: ${stage} preserves its original ${hasWait ? 'single interval' : 'absence of an interval'}`, async t => { + const { bot, events } = createFlowBot(t, mode, [stage]) + await bot.Main(account) + assert.deepEqual( + bot.utils.randomDelay.mock.calls.map(call => call.arguments), + hasWait ? [[60000, 240000]] : [] + ) + assert.deepEqual( + bot.utils.wait.mock.calls.map(call => call.arguments), + hasWait ? [[60000]] : [] + ) + if (hasWait) { + const offset = stage === 'doClaimBonusPoints' ? -1 : 1 + assert.deepEqual(events[events.indexOf(stage) + offset], ['wait', 60000]) + } + if (stage === 'doClaimBonusPoints') assert.equal(events.at(-1), stage) + }) + } + + test(`${mode}: multiple executed stages retain the original number of intervals`, async t => { + const { bot } = createFlowBot(t, mode, stages) + await bot.Main(account) + assert.equal(bot.utils.wait.mock.callCount(), postWaitStages.length + 1) + assert.deepEqual( + bot.utils.randomDelay.mock.calls.map(call => call.arguments), + Array.from({ length: postWaitStages.length + 1 }, () => [60000, 240000]) + ) + }) + + test(`${mode}: disabling workers calls no activities and adds no waits`, async t => { + const { bot, events } = createFlowBot(t, mode, stages) + bot.config.ensureStreakProtection = false + for (const name of Object.keys(bot.config.workers)) bot.config.workers[name] = false + await bot.Main(account) + assert.deepEqual(events, []) + }) + + for (const completed of [false, true]) { + test(`${mode}: real ${completed ? 'completed' : 'empty'} promotion workers pass through Main without waiting`, async t => { + const { bot, dashboard } = createFlowBot(t, mode) + const real = new Activities(bot) + for (const method of ['doDailySet', 'doMorePromotions', 'doAppPromotions']) + bot.activities[method] = real[method] + if (completed) { + dashboard.dashboard.dailySetPromotions['2026-10-24'] = [promotion({ complete: true })] + dashboard.dashboard.morePromotions = [promotion({ complete: true })] + bot.browser.func.getAppDashboardData = async () => ({ + response: { + promotions: [ + { + attributes: { complete: 'True', type: 'sapphire', offerid: 'completed-app-offer' } + } + ] + } + }) + } + await bot.Main(account) + assert.equal(bot.utils.wait.mock.callCount(), 0) + assert.equal(bot.http.request.mock.callCount(), 0) + }) + } + + test(`${mode}: a pending promotion retains its interval through the real activity wrapper`, async t => { + const { bot, dashboard } = createFlowBot(t, mode) + dashboard.dashboard.morePromotions = [promotion()] + bot.activities.doMorePromotions = new Activities(bot).doMorePromotions + const run = t.mock.method(PromotionActivityRunner.prototype, 'run', async () => true) + await bot.Main(account) + assert.equal(run.mock.callCount(), 1) + assert.deepEqual( + bot.utils.wait.mock.calls.map(call => call.arguments), + [[60000]] + ) + }) + + test(`${mode}: a thrown activity does not become a skip or continue to a later stage`, async t => { + const { bot, events } = createFlowBot(t, mode) + const failure = new Error('offline activity failure') + bot.activities.doMorePromotions = async () => { + throw failure + } + await assert.rejects(bot.Main(account), error => error === failure) + assert.ok(!events.includes('doDailyCheckIn')) + assert.equal(bot.utils.wait.mock.callCount(), 0) + assert.ok(bot.browser.func.closeBrowser.mock.callCount() > 0) + }) +} + +for (const mode of ['browser', 'full-api']) { + for (const stage of ['doDailySet', 'doMorePromotions']) { + test(mode + ': ' + stage + ' skips the interval when all promotions are disabled', async t => { + const { bot, dashboard } = createFlowBot(t, mode) + bot.config.activities = { urlReward: false, searchOnBing: false } + dashboard.dashboard.dailySetPromotions['2026-10-24'] = [promotion()] + dashboard.dashboard.morePromotions = [promotion()] + bot.activities[stage] = new Activities(bot)[stage] + bot.activities.doUrlReward = t.mock.fn(async () => true) + await bot.Main(account) + assert.equal(bot.activities.doUrlReward.mock.callCount(), 0) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), 0) + assert.equal(bot.utils.wait.mock.callCount(), 0) + }) + } +} + +for (const mode of ['browser', 'full-api']) { + for (const [name, reportable, actionAvailable, expected] of [ + ['unreportable offers', false, true, []], + ['missing action', true, false, []], + ['submitted offers', true, true, [15000, 60000]] + ]) { + test(mode + ': real promotion submissions retain only applicable intervals for ' + name, async t => { + const { bot, dashboard } = createFlowBot(t, mode) + bot.config.activities = { urlReward: true, searchOnBing: false } + dashboard.dashboard.morePromotions = [promotion(), promotion({ offerId: 'second' })] + bot.nextActions.reportActivity = actionAvailable ? 'offline-action' : '' + bot.browser.func.ensureOffer = async () => ({ reportable, hash: 'offline-hash', points: 10 }) + bot.activities.doMorePromotions = new Activities(bot).doMorePromotions + bot.activities.doUrlReward = new Activities(bot).doUrlReward + await bot.Main(account) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), expected.length ? 2 : 0) + assert.deepEqual( + bot.utils.wait.mock.calls.map(call => call.arguments[0]), + expected + ) + }) + } +} + +for (const method of ['doDailySet', 'doMorePromotions']) { + test(method + ': unsupported promotions are skipped without inter-promotion waits', async t => { + const { bot, dashboard } = createActivityBot(t) + bot.config.activities = { urlReward: true, searchOnBing: true } + const items = [ + promotion({ promotionType: 'unsupported' }), + promotion({ offerId: 'second', promotionType: 'unsupported' }) + ] + dashboard.dashboard.dailySetPromotions['2026-10-24'] = items + dashboard.dashboard.morePromotions = items + assert.equal(await new Activities(bot)[method](dashboard), false) + assert.equal(bot.utils.wait.mock.callCount(), 0) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), 0) + }) +} + +for (const apiSearchOnBing of [false, true]) { + test('SearchOnBing activation reports attempts even when unacknowledged, api=' + apiSearchOnBing, async t => { + const { bot } = createActivityBot(t) + bot.config.experimental = { apiSearchOnBing } + bot.nextActions.reportActivity = 'offline-action' + bot.browser.func.ensureOffer = async () => ({ hash: 'offline-hash' }) + bot.browser.func.reportServerAction.mock.mockImplementation(async () => ({ status: 200, acknowledged: false })) + const page = { goto: async () => {} } + assert.equal(await new Activities(bot).doSearchOnBing(promotion(), page), true) + assert.equal(bot.browser.func.reportServerAction.mock.callCount(), 1) + assert.equal(bot.http.request.mock.callCount(), 0) + }) +} diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index f0c8cb9..2a07a83 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 b664b0d..6c202b9 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 6a5d97b..a3598dc 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 da7521c..3319b72 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 608c83e..0a6c040 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 a71b68c..20adbb6 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 7e60ef8..b4d2eeb 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 5574bbc..7d280f4 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 269839a..3ec7356 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/Activities.ts b/src/functions/Activities.ts index 371459e..1e2f470 100644 --- a/src/functions/Activities.ts +++ b/src/functions/Activities.ts @@ -49,33 +49,43 @@ export default class Activities { return await new BrowserSearch(this.bot).doBonusSearches(page) } - doSearchOnBing = async (promotion: BasePromotion, page: Page): Promise => { + doSearchOnBing = async (promotion: BasePromotion, page: Page): Promise => { if (this.bot.config.experimental.apiSearchOnBing) { - await new ApiSearchOnBing(this.bot).doSearchOnBing(promotion) - return + const activity = new ApiSearchOnBing(this.bot) + await activity.doSearchOnBing(promotion) + return activity.didExecute() } - await new BrowserSearchOnBing(this.bot).doSearchOnBing(promotion, page) + const activity = new BrowserSearchOnBing(this.bot) + await activity.doSearchOnBing(promotion, page) + return activity.didExecute() } - doDailySet = async (data: DashboardData): Promise => { - await new DailySet(this.bot).run(data) + doDailySet = async (data: DashboardData): Promise => { + const activity = new DailySet(this.bot) + await activity.run(data) + return activity.didExecute() } - doMorePromotions = async (data: DashboardData): Promise => { - await new MorePromotions(this.bot).run(data) + doMorePromotions = async (data: DashboardData): Promise => { + const activity = new MorePromotions(this.bot) + await activity.run(data) + return activity.didExecute() } - doPunchCardsMobile = async (data: DashboardData): Promise => { - await new PunchCards(this.bot).runMobile(data) + doPunchCardsMobile = async (data: DashboardData): Promise => { + const activity = new PunchCards(this.bot) + await activity.runMobile(data) + return activity.didExecute() } doPunchCardsDesktop = async (): Promise => { await new PunchCards(this.bot).runDesktop() } - doUrlReward = async (promotion: BasePromotion): Promise => { - const urlReward = new UrlReward(this.bot) - await urlReward.doUrlReward(promotion) + doUrlReward = async (promotion: BasePromotion): Promise => { + const activity = new UrlReward(this.bot) + await activity.doUrlReward(promotion) + return activity.didExecute() } doClaimBonusPoints = async (): Promise => { @@ -88,14 +98,16 @@ export default class Activities { await ensureStreakProtection.ensureStreakProtection() } - doClaimReward = async (child: QuestChild, parentId: string): Promise => { + doClaimReward = async (child: QuestChild, parentId: string): Promise => { const claimReward = new ClaimReward(this.bot) await claimReward.claimReward(child, parentId) + return claimReward.didExecute() } - doActivateSearchPerk = async (data: DashboardData): Promise => { - const activateSearchPerk = new ActivateSearchPerk(this.bot) - await activateSearchPerk.activate(data) + doActivateSearchPerk = async (data: DashboardData): Promise => { + const activity = new ActivateSearchPerk(this.bot) + await activity.activate(data) + return activity.didExecute() } doVisualSearch = async (data: DashboardData): Promise => { @@ -108,18 +120,22 @@ export default class Activities { await urlReward.doAppReward(promotion) } - doReadToEarn = async (): Promise => { - const readToEarn = new ReadToEarn(this.bot) - await readToEarn.doReadToEarn() + doReadToEarn = async (): Promise => { + const activity = new ReadToEarn(this.bot) + await activity.doReadToEarn() + return activity.didExecute() } - doDailyCheckIn = async (): Promise => { - const dailyCheckIn = new DailyCheckIn(this.bot) - await dailyCheckIn.doDailyCheckIn() + doDailyCheckIn = async (): Promise => { + const activity = new DailyCheckIn(this.bot) + await activity.doDailyCheckIn() + return activity.didExecute() } - doAppPromotions = async (data: AppDashboardData): Promise => { - await new AppPromotions(this.bot).run(data) + doAppPromotions = async (data: AppDashboardData): Promise => { + const activity = new AppPromotions(this.bot) + await activity.run(data) + return activity.didExecute() } doEdgeBrowsing = async (data: DashboardData, signal?: AbortSignal): Promise => { diff --git a/src/functions/QueryEngine.ts b/src/functions/QueryEngine.ts index a1edf53..d93851c 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/BaseActivity.ts b/src/functions/activities/BaseActivity.ts index 3bceadb..79a0a80 100644 --- a/src/functions/activities/BaseActivity.ts +++ b/src/functions/activities/BaseActivity.ts @@ -2,8 +2,17 @@ import type { MicrosoftRewardsBot } from '../../index' export abstract class BaseActivity { protected readonly bot: MicrosoftRewardsBot + private executed = false constructor(bot: MicrosoftRewardsBot) { this.bot = bot } + + protected markExecuted(): void { + this.executed = true + } + + public didExecute(): boolean { + return this.executed + } } diff --git a/src/functions/activities/api/ActivateSearchPerk.ts b/src/functions/activities/api/ActivateSearchPerk.ts index cc1ef87..3db2a13 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 } @@ -94,6 +90,7 @@ export class ActivateSearchPerk extends BaseActivity { 'ACTIVATE-SEARCH-PERK', `正在激活 ${perk.multiplier}x 搜索加成 | offerId=${perk.offerId} | geo=${this.bot.userData.geoLocale}` ) + this.markExecuted() try { const { status, acknowledged } = await this.bot.browser.func.reportServerAction(actionId, [ diff --git a/src/functions/activities/api/ClaimReward.ts b/src/functions/activities/api/ClaimReward.ts index 4886a58..48304ca 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 } @@ -39,6 +31,7 @@ export class ClaimReward extends BaseActivity { try { const questUrl = URLs.rewards.quest(parentId) + this.markExecuted() const { status, acknowledged } = await this.bot.browser.func.reportServerAction( actionId, [ diff --git a/src/functions/activities/api/EnsureStreakProtection.ts b/src/functions/activities/api/EnsureStreakProtection.ts index b9082c4..5b40f0a 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 704fe6d..079e13a 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 } @@ -62,6 +58,7 @@ export class UrlReward extends BaseActivity { ) try { + this.markExecuted() const { status, acknowledged, availablePoints } = await this.bot.browser.func.reportServerAction( actionId, [ diff --git a/src/functions/activities/app/AppPromotions.ts b/src/functions/activities/app/AppPromotions.ts index 43ff8f6..21b63d0 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 } @@ -26,6 +22,7 @@ export class AppPromotions extends BaseActivity { 'APP-PROMOTIONS', `开始解决"应用推广"项目 | remaining=${pending.length}` ) + this.markExecuted() for (const [index, promotion] of pending.entries()) { await this.bot.activities.doAppReward(promotion) if (index < pending.length - 1) { diff --git a/src/functions/activities/app/AppReward.ts b/src/functions/activities/app/AppReward.ts index 0b4cf67..a795038 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 3157034..40439bd 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 } @@ -25,6 +21,7 @@ export class DailyCheckIn extends BaseActivity { 'DAILY-CHECK-IN', `开始每日签到 | geo=${this.bot.userData.geoLocale} | currentBalance=${this.oldBalance}` ) + this.markExecuted() try { const response = await this.submitDaily() diff --git a/src/functions/activities/app/ReadToEarn.ts b/src/functions/activities/app/ReadToEarn.ts index 7762fe3..44aca3c 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 } @@ -25,6 +21,7 @@ export class ReadToEarn extends BaseActivity { 'READ-TO-EARN', `开始读文赚积分 | geo=${this.bot.userData.geoLocale} | delayRange=${delayMin}-${delayMax} | currentBalance=${startBalance}` ) + this.markExecuted() try { const jsonData = { diff --git a/src/functions/activities/experimental/ApiSearch.ts b/src/functions/activities/experimental/ApiSearch.ts index 30a2b87..83da44f 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 f2a1695..9506b25 100644 --- a/src/functions/activities/experimental/ApiSearchOnBing.ts +++ b/src/functions/activities/experimental/ApiSearchOnBing.ts @@ -24,12 +24,8 @@ export class ApiSearchOnBing extends BaseActivity { ) try { - if (!(await activateSearchOnBing(this.bot, promotion))) { - this.bot.logger.warn( - this.bot.isMobile, - 'SEARCH-ON-BING', - `搜索活动无法激活,中止 | offerId=${offerId}` - ) + if (!(await activateSearchOnBing(this.bot, promotion, () => this.markExecuted()))) { + 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 321490a..05b74eb 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 d95bc06..7ebedce 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 e55a435..f3e17b8 100644 --- a/src/functions/activities/rewards/DailySet.ts +++ b/src/functions/activities/rewards/DailySet.ts @@ -13,12 +13,8 @@ export class DailySet extends BaseActivity { return } - 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', `开始解决"每日任务"项目 | remaining=${pending.length}`) + if (await new PromotionActivityRunner(this.bot).run(pending)) this.markExecuted() 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 907d731..4ba6b10 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 } @@ -30,7 +26,7 @@ export class MorePromotions extends BaseActivity { 'MORE-PROMOTIONS', `开始解决"更多推广"项目 | remaining=${pending.length}` ) - await new PromotionActivityRunner(this.bot).run(pending) + if (await new PromotionActivityRunner(this.bot).run(pending)) this.markExecuted() this.bot.logger.info(this.bot.isMobile, 'MORE-PROMOTIONS', '"更多推广"项目处理完毕') } diff --git a/src/functions/activities/rewards/PromotionActivityRunner.ts b/src/functions/activities/rewards/PromotionActivityRunner.ts index 9f2299d..f7a837d 100644 --- a/src/functions/activities/rewards/PromotionActivityRunner.ts +++ b/src/functions/activities/rewards/PromotionActivityRunner.ts @@ -2,10 +2,11 @@ import { BaseActivity } from '../BaseActivity' import type { BasePromotion } from '../../../interface/DashboardData' export class PromotionActivityRunner extends BaseActivity { - public async run(promotions: BasePromotion[]): Promise { + public async run(promotions: BasePromotion[]): Promise { for (const [index, promotion] of promotions.entries()) { try { - await this.runPromotion(promotion) + if (!(await this.runPromotion(promotion))) continue + this.markExecuted() if (index < promotions.length - 1) { await this.bot.utils.wait(this.bot.utils.randomDelay(15000, 60000)) } @@ -19,9 +20,10 @@ export class PromotionActivityRunner extends BaseActivity { ) } } + return this.didExecute() } - private async runPromotion(promotion: BasePromotion): Promise { + private async runPromotion(promotion: BasePromotion): Promise { const type = promotion.promotionType?.toLowerCase() ?? '' const name = promotion.name?.toLowerCase() ?? '' const offerId = promotion.offerId @@ -36,21 +38,19 @@ 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 + return false } const isSearchOnBing = name.includes('exploreonbing') if (isSearchOnBing && !this.bot.config.activities.searchOnBing) { this.logDisabled('SearchOnBing', offerId) - return + return false } if (!isSearchOnBing && !this.bot.config.activities.urlReward) { this.logDisabled('UrlReward', offerId) - return + return false } if (isSearchOnBing) { @@ -60,8 +60,7 @@ export class PromotionActivityRunner extends BaseActivity { `发现活动类型 "SearchOnBing" | 标题="${promotion.title}" | offerId=${offerId}` ) const page = this.bot.isMobile ? this.bot.mainMobilePage : this.bot.mainDesktopPage - await this.bot.activities.doSearchOnBing(promotion, page) - return + return this.bot.activities.doSearchOnBing(promotion, page) } this.bot.logger.info( @@ -69,14 +68,10 @@ export class PromotionActivityRunner extends BaseActivity { 'ACTIVITY', `发现活动类型 "UrlReward" | 标题="${promotion.title}" | offerId=${offerId}` ) - await this.bot.activities.doUrlReward(promotion) + return this.bot.activities.doUrlReward(promotion) } 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 92152db..6239b77 100644 --- a/src/functions/activities/rewards/PunchCards.ts +++ b/src/functions/activities/rewards/PunchCards.ts @@ -68,7 +68,6 @@ export class PunchCards extends BaseActivity { 'PUNCHCARD', `发现 ${pending.length} 个未完成任务 | apiMatched=${pending.filter(parent => apiById.has(parent.offerId)).length}` ) - for (const parent of pending) { try { await this.solvePunchCard(parent, apiById.get(parent.offerId)) @@ -187,9 +186,14 @@ export class PunchCards extends BaseActivity { ) continue } - await this.bot.activities.doClaimReward(child, parentId) - } else { - await this.reportQuestChild(child, parentId) + if (!(await this.bot.activities.doClaimReward(child, parentId))) { + remaining += 1 + continue + } + this.markExecuted() + } else if (!(await this.reportQuestChild(child, parentId))) { + remaining += 1 + continue } reported += 1 @@ -233,24 +237,21 @@ export class PunchCards extends BaseActivity { } } - private async reportQuestChild(child: QuestChild, parentId: string): Promise { + 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"` - ) - return + this.bot.logger.warn(this.bot.isMobile, 'PUNCHCARD', `跳过 ${child.offerId}:未发现 "reportActivity"`) + return false } if (!child.hash) { this.bot.logger.warn(this.bot.isMobile, 'PUNCHCARD', `跳过 ${child.offerId}:没有有效的 hash`) - return + return false } const oldBalance = this.bot.userData.currentPoints try { const questUrl = URLs.rewards.quest(parentId) + this.markExecuted() const { status, acknowledged } = await this.bot.browser.func.reportServerAction( actionId, [ @@ -292,6 +293,7 @@ export class PunchCards extends BaseActivity { }` ) } + return true } private isSearchQuotaChild(offerId: string, promotion?: BasePromotion): boolean { diff --git a/src/functions/activities/search/BrowserSearch.ts b/src/functions/activities/search/BrowserSearch.ts index 1ae2bd2..3e08924 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 d1d3d66..6d41057 100644 --- a/src/functions/activities/search/BrowserSearchOnBing.ts +++ b/src/functions/activities/search/BrowserSearchOnBing.ts @@ -23,13 +23,9 @@ export class SearchOnBing extends BaseActivity { ) try { - const activated = await activateSearchOnBing(this.bot, promotion) + const activated = await activateSearchOnBing(this.bot, promotion, () => this.markExecuted()) 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 1c91383..34f04de 100644 --- a/src/functions/activities/search/SearchOnBingShared.ts +++ b/src/functions/activities/search/SearchOnBingShared.ts @@ -10,31 +10,28 @@ interface ActivityQueries { queries: string[] } -export async function activateSearchOnBing(bot: MicrosoftRewardsBot, promotion: BasePromotion): Promise { +export async function activateSearchOnBing( + bot: MicrosoftRewardsBot, + promotion: BasePromotion, + onSubmit?: () => void +): Promise { const offerId = promotion.offerId 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 } try { + onSubmit?.() const { status, acknowledged } = await bot.browser.func.reportServerAction(actionId, [ hash, 11, diff --git a/src/functions/activities/visualSearch/VisualSearch.ts b/src/functions/activities/visualSearch/VisualSearch.ts index 912e76a..783c1f6 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 dc21897..6943f1d 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 494496e..ad3facf 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(() => { @@ -941,12 +922,10 @@ export class MicrosoftRewardsBot { await this.activities.doEnsureStreakProtection() } if (this.config.workers.doPunchCards) { - await this.activities.doPunchCardsMobile(data) - await this.gapBetweenWorkers() + if (await this.activities.doPunchCardsMobile(data)) await this.gapBetweenWorkers() } if (this.config.workers.doActivateSearchPerk) { - await this.activities.doActivateSearchPerk(data) - await this.gapBetweenWorkers() + if (await this.activities.doActivateSearchPerk(data)) await this.gapBetweenWorkers() } const plan = await this.searchManager.getSearchPoints() @@ -969,24 +948,19 @@ export class MicrosoftRewardsBot { } if (this.config.workers.doDailySet) { - await this.activities.doDailySet(data) - await this.gapBetweenWorkers() + if (await this.activities.doDailySet(data)) await this.gapBetweenWorkers() } if (this.config.workers.doMorePromotions) { - await this.activities.doMorePromotions(data) - await this.gapBetweenWorkers() + if (await this.activities.doMorePromotions(data)) await this.gapBetweenWorkers() } if (appAvailable && this.config.workers.doDailyCheckIn) { - await this.activities.doDailyCheckIn() - await this.gapBetweenWorkers() + if (await this.activities.doDailyCheckIn()) await this.gapBetweenWorkers() } if (appAvailable && this.config.workers.doAppPromotions && appData) { - await this.activities.doAppPromotions(appData) - await this.gapBetweenWorkers() + if (await this.activities.doAppPromotions(appData)) await this.gapBetweenWorkers() } if (appAvailable && this.config.workers.doReadToEarn) { - await this.activities.doReadToEarn() - await this.gapBetweenWorkers() + if (await this.activities.doReadToEarn()) await this.gapBetweenWorkers() } if (doMobileSearch) mobilePoints = await this.searchManager.searchMobile(account) @@ -997,32 +971,25 @@ export class MicrosoftRewardsBot { await this.activities.doEnsureStreakProtection() } if (this.config.workers.doDailySet) { - await this.activities.doDailySet(data) - await this.gapBetweenWorkers() + if (await this.activities.doDailySet(data)) await this.gapBetweenWorkers() } if (this.config.workers.doActivateSearchPerk) { - await this.activities.doActivateSearchPerk(data) - await this.gapBetweenWorkers() + if (await this.activities.doActivateSearchPerk(data)) await this.gapBetweenWorkers() } if (this.config.workers.doMorePromotions) { - await this.activities.doMorePromotions(data) - await this.gapBetweenWorkers() + if (await this.activities.doMorePromotions(data)) await this.gapBetweenWorkers() } if (appAvailable && this.config.workers.doDailyCheckIn) { - await this.activities.doDailyCheckIn() - await this.gapBetweenWorkers() + if (await this.activities.doDailyCheckIn()) await this.gapBetweenWorkers() } if (appAvailable && this.config.workers.doAppPromotions && appData) { - await this.activities.doAppPromotions(appData) - await this.gapBetweenWorkers() + if (await this.activities.doAppPromotions(appData)) await this.gapBetweenWorkers() } if (appAvailable && this.config.workers.doReadToEarn) { - await this.activities.doReadToEarn() - await this.gapBetweenWorkers() + if (await this.activities.doReadToEarn()) await this.gapBetweenWorkers() } if (this.config.workers.doPunchCards) { - await this.activities.doPunchCardsMobile(data) - await this.gapBetweenWorkers() + if (await this.activities.doPunchCardsMobile(data)) await this.gapBetweenWorkers() } const plan = await this.searchManager.getSearchPoints() @@ -1098,17 +1065,13 @@ export class MicrosoftRewardsBot { ) if (this.config.workers.doClaimBonusPoints) { - await this.gapBetweenWorkers() + if (this.nextActions.reportClaimAllPoints) await this.gapBetweenWorkers() await this.activities.doClaimBonusPoints() } if (edgeBrowsingTask) { if (!edgeBrowsingFinished) { - this.logger.info( - this.isMobile, - 'EDGE-BROWSING', - '前台活动已完成;正在等待后台 Edge 浏览活动' - ) + this.logger.info(this.isMobile, 'EDGE-BROWSING', '前台活动已完成;正在等待后台 Edge 浏览活动') } await edgeBrowsingTask edgeBrowsingTask = null @@ -1218,9 +1181,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/logging/ClawBot.ts b/src/logging/ClawBot.ts index 067149d..e5e754f 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 e93007d..3f8ec96 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 c99e6f2..e1ca7f1 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)) } }