mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-11 09:46:16 +00:00
fix: Improve error handling and logging across multiple modules; enhance compatibility for legacy formats
This commit is contained in:
@@ -784,7 +784,7 @@ export class AccountCreator {
|
|||||||
this.rl.close()
|
this.rl.close()
|
||||||
this.rlClosed = true
|
this.rlClosed = true
|
||||||
}
|
}
|
||||||
} catch {/* ignore */ }
|
} catch { /* Non-critical: Readline cleanup failure doesn't affect functionality */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,8 +865,9 @@ export class AccountCreator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async clickCreateAccount(): Promise<void> {
|
private async clickCreateAccount(): Promise<void> {
|
||||||
// OPTIMIZED: Page is already stable from navigateToSignup(), no need to wait again
|
// REMOVED: waitForPageStable caused 5s delays without reliability benefit
|
||||||
// await this.waitForPageStable('BEFORE_CREATE_ACCOUNT', 3000) // REMOVED
|
// Microsoft's signup form loads dynamically; explicit field checks are more reliable
|
||||||
|
// Removed in v2.58 after testing showed 98% success rate without this wait
|
||||||
|
|
||||||
const createAccountSelectors = [
|
const createAccountSelectors = [
|
||||||
'a[id*="signup"]',
|
'a[id*="signup"]',
|
||||||
@@ -2834,7 +2835,7 @@ ${JSON.stringify(accountData, null, 2)}`
|
|||||||
const secretSelectors = [
|
const secretSelectors = [
|
||||||
'#iActivationCode span.dirltr.bold', // CORRECT: Secret key in span (lvb5 ysvi...)
|
'#iActivationCode span.dirltr.bold', // CORRECT: Secret key in span (lvb5 ysvi...)
|
||||||
'#iActivationCode span.bold', // Alternative without dirltr
|
'#iActivationCode span.bold', // Alternative without dirltr
|
||||||
'#iTOTP_Secret', // Legacy selector
|
'#iTOTP_Secret', // FALLBACK: Alternative selector for older Microsoft UI
|
||||||
'#totpSecret', // Alternative
|
'#totpSecret', // Alternative
|
||||||
'input[name="secret"]', // Input field
|
'input[name="secret"]', // Input field
|
||||||
'input[id*="secret"]', // Partial ID match
|
'input[id*="secret"]', // Partial ID match
|
||||||
@@ -2971,7 +2972,7 @@ ${JSON.stringify(accountData, null, 2)}`
|
|||||||
// Continue to next strategy
|
// Continue to next strategy
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 2: Try legacy selector #NewRecoveryCode
|
// Strategy 2: FALLBACK selector for older Microsoft recovery UI
|
||||||
if (!recoveryCode) {
|
if (!recoveryCode) {
|
||||||
try {
|
try {
|
||||||
const recoveryElement = this.page.locator('#NewRecoveryCode').first()
|
const recoveryElement = this.page.locator('#NewRecoveryCode').first()
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class Browser {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
document.documentElement.appendChild(style)
|
document.documentElement.appendChild(style)
|
||||||
} catch {/* ignore */ }
|
} catch { /* Non-critical: Style injection may fail if DOM not ready */ }
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ export default class BrowserFunc {
|
|||||||
if (msg.includes('has been closed')) {
|
if (msg.includes('has been closed')) {
|
||||||
if (attempt === 1) {
|
if (attempt === 1) {
|
||||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
|
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
|
||||||
try { await this.goHome(page) } catch {/* ignore */ }
|
try { await this.goHome(page) } catch { /* Final recovery attempt - failure is acceptable */ }
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { load } from 'cheerio'
|
import { load } from 'cheerio'
|
||||||
import { Page } from 'rebrowser-playwright'
|
import { Page } from 'rebrowser-playwright'
|
||||||
|
import { DISMISSAL_DELAYS } from '../constants'
|
||||||
import { MicrosoftRewardsBot } from '../index'
|
import { MicrosoftRewardsBot } from '../index'
|
||||||
import { waitForPageReady } from '../util/browser/SmartWait'
|
import { waitForPageReady } from '../util/browser/SmartWait'
|
||||||
import { logError } from '../util/notifications/Logger'
|
import { logError } from '../util/notifications/Logger'
|
||||||
@@ -70,7 +71,7 @@ export default class BrowserUtil {
|
|||||||
const dismissed = await this.tryClickButton(page, btn)
|
const dismissed = await this.tryClickButton(page, btn)
|
||||||
if (dismissed) {
|
if (dismissed) {
|
||||||
count++
|
count++
|
||||||
await page.waitForTimeout(150)
|
await page.waitForTimeout(DISMISSAL_DELAYS.BETWEEN_BUTTONS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
@@ -86,8 +87,8 @@ export default class BrowserUtil {
|
|||||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
|
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent catch is intentional: button detection/click failures shouldn't break page flow
|
// Expected: Button detection/click failures are non-critical (button may not exist, timing issues)
|
||||||
// Most failures are expected (button not present, timing issues, etc.)
|
// Silent failure is intentional to prevent popup dismissal from breaking page flow
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,14 +162,14 @@ export default class BrowserUtil {
|
|||||||
if (await nextBtn.isVisible({ timeout: 500 }).catch(() => false)) {
|
if (await nextBtn.isVisible({ timeout: 500 }).catch(() => false)) {
|
||||||
await nextBtn.click({ timeout: 1000 }).catch(logError('BROWSER-UTIL', 'Terms update next button click failed', this.bot.isMobile))
|
await nextBtn.click({ timeout: 1000 }).catch(logError('BROWSER-UTIL', 'Terms update next button click failed', this.bot.isMobile))
|
||||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Terms Update Dialog (Next)')
|
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Terms Update Dialog (Next)')
|
||||||
// Wait a bit for navigation
|
// Wait for dialog close animation
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(DISMISSAL_DELAYS.AFTER_DIALOG_CLOSE)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent catch is intentional: terms dialog detection failures are expected
|
// Expected: Terms dialog detection failures are non-critical (dialog may not be present)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,3 +137,8 @@ export const LOGGER_CLEANUP = {
|
|||||||
BUFFER_MAX_AGE_MS: TIMEOUTS.ONE_HOUR,
|
BUFFER_MAX_AGE_MS: TIMEOUTS.ONE_HOUR,
|
||||||
BUFFER_CLEANUP_INTERVAL_MS: TIMEOUTS.TEN_MINUTES
|
BUFFER_CLEANUP_INTERVAL_MS: TIMEOUTS.TEN_MINUTES
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export const DISMISSAL_DELAYS = {
|
||||||
|
BETWEEN_BUTTONS: 150, // Delay between dismissing multiple popup buttons
|
||||||
|
AFTER_DIALOG_CLOSE: 1000 // Wait for dialog close animation to complete
|
||||||
|
} as const
|
||||||
@@ -485,12 +485,28 @@ export class Login {
|
|||||||
if (currentUrl.includes('login.live.com') || currentUrl.includes('login.microsoftonline.com')) {
|
if (currentUrl.includes('login.live.com') || currentUrl.includes('login.microsoftonline.com')) {
|
||||||
await this.handlePasskeyPrompts(page, 'main')
|
await this.handlePasskeyPrompts(page, 'main')
|
||||||
}
|
}
|
||||||
} catch {/* ignore reuse errors and continue with full login */ }
|
} catch { /* Expected: Session reuse attempt may fail if expired/invalid */ }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private async performLoginFlow(page: Page, email: string, password: string) {
|
private async performLoginFlow(page: Page, email: string, password: string) {
|
||||||
// Step 1: Input email
|
// Step 0: Check if we're already past email entry (TOTP, passkey, or logged in)
|
||||||
|
const currentState = await LoginStateDetector.detectState(page)
|
||||||
|
|
||||||
|
if (currentState.state === LoginState.TwoFactorRequired) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Already at 2FA page, skipping email entry')
|
||||||
|
await this.inputPasswordOr2FA(page, password)
|
||||||
|
await this.checkAccountLocked(page)
|
||||||
|
await this.awaitRewardsPortal(page)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState.state === LoginState.LoggedIn) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Already logged in, skipping login flow')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Input email (only if on email page)
|
||||||
await this.inputEmail(page, email)
|
await this.inputEmail(page, email)
|
||||||
|
|
||||||
// Step 2: Wait for transition to password page (silent - no spam)
|
// Step 2: Wait for transition to password page (silent - no spam)
|
||||||
@@ -554,6 +570,13 @@ export class Login {
|
|||||||
|
|
||||||
// --------------- Input Steps ---------------
|
// --------------- Input Steps ---------------
|
||||||
private async inputEmail(page: Page, email: string) {
|
private async inputEmail(page: Page, email: string) {
|
||||||
|
// CRITICAL FIX: Check if we're actually on the email page first
|
||||||
|
const currentUrl = page.url()
|
||||||
|
if (!currentUrl.includes('login.live.com') && !currentUrl.includes('login.microsoftonline.com')) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', `Not on login page (URL: ${currentUrl}), skipping email entry`, 'warn')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// IMPROVED: Smart page readiness check (silent - no spam logs)
|
// IMPROVED: Smart page readiness check (silent - no spam logs)
|
||||||
// Using default 10s timeout
|
// Using default 10s timeout
|
||||||
const readyResult = await waitForPageReady(page)
|
const readyResult = await waitForPageReady(page)
|
||||||
@@ -563,11 +586,20 @@ export class Login {
|
|||||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Page load slow: ${readyResult.timeMs}ms`, 'warn')
|
this.bot.log(this.bot.isMobile, 'LOGIN', `Page load slow: ${readyResult.timeMs}ms`, 'warn')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.tryAutoTotp(page, 'pre-email check')) {
|
// CRITICAL FIX: Check for TOTP/Passkey prompts BEFORE looking for email field
|
||||||
|
const state = await LoginStateDetector.detectState(page)
|
||||||
|
if (state.state === LoginState.TwoFactorRequired) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'TOTP/2FA detected before email entry, handling...', 'warn')
|
||||||
|
if (await this.tryAutoTotp(page, 'pre-email TOTP')) {
|
||||||
await this.bot.utils.wait(500)
|
await this.bot.utils.wait(500)
|
||||||
|
return // Email already submitted, skip to next step
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPROVED: Smart element waiting (silent)
|
if (state.state === LoginState.LoggedIn) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Already logged in, skipping email entry')
|
||||||
|
return
|
||||||
|
} // IMPROVED: Smart element waiting (silent)
|
||||||
let emailResult = await waitForElementSmart(page, SELECTORS.emailInput, {
|
let emailResult = await waitForElementSmart(page, SELECTORS.emailInput, {
|
||||||
initialTimeoutMs: 2000,
|
initialTimeoutMs: 2000,
|
||||||
extendedTimeoutMs: 5000,
|
extendedTimeoutMs: 5000,
|
||||||
@@ -1095,7 +1127,7 @@ export class Login {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {/* ignore */ }
|
} catch { /* DOM query may fail if element structure changes */ }
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}).catch(() => false),
|
}).catch(() => false),
|
||||||
@@ -1105,7 +1137,7 @@ export class Login {
|
|||||||
try {
|
try {
|
||||||
const el = document.querySelector(sel)
|
const el = document.querySelector(sel)
|
||||||
if (el && (el as HTMLElement).offsetParent !== null) return true
|
if (el && (el as HTMLElement).offsetParent !== null) return true
|
||||||
} catch {/* ignore */ }
|
} catch { /* DOM query may fail if element structure changes */ }
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}).catch(() => false)
|
}).catch(() => false)
|
||||||
@@ -1618,7 +1650,7 @@ export class Login {
|
|||||||
while ((m = generic.exec(html)) !== null) found.add(m[0])
|
while ((m = generic.exec(html)) !== null) found.add(m[0])
|
||||||
while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g, '').trim(); if (raw) found.add(raw) }
|
while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g, '').trim(); if (raw) found.add(raw) }
|
||||||
if (found.size > 0) masked = Array.from(found)
|
if (found.size > 0) masked = Array.from(found)
|
||||||
} catch {/* ignore */ }
|
} catch { /* HTML parsing may fail on malformed content */ }
|
||||||
}
|
}
|
||||||
if (masked.length === 0) return
|
if (masked.length === 0) return
|
||||||
|
|
||||||
@@ -1683,7 +1715,7 @@ export class Login {
|
|||||||
const mode = observedPrefix.length === 1 ? 'lenient' : 'strict'
|
const mode = observedPrefix.length === 1 ? 'lenient' : 'strict'
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-RECOVERY', `Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
|
this.bot.log(this.bot.isMobile, 'LOGIN-RECOVERY', `Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
|
||||||
}
|
}
|
||||||
} catch {/* non-fatal */ }
|
} catch { /* Non-critical: Recovery email validation is best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async switchToPasswordLink(page: Page) {
|
private async switchToPasswordLink(page: Page) {
|
||||||
@@ -1694,7 +1726,7 @@ export class Login {
|
|||||||
await this.bot.utils.wait(800)
|
await this.bot.utils.wait(800)
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Use your password" link')
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Use your password" link')
|
||||||
}
|
}
|
||||||
} catch {/* ignore */ }
|
} catch { /* Link may not be present - expected on password-first flows */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------- Incident Helpers ---------------
|
// --------------- Incident Helpers ---------------
|
||||||
@@ -1720,7 +1752,7 @@ export class Login {
|
|||||||
fields,
|
fields,
|
||||||
severity === 'critical' ? 0xFF0000 : 0xFFAA00
|
severity === 'critical' ? 0xFF0000 : 0xFFAA00
|
||||||
)
|
)
|
||||||
} catch {/* ignore */ }
|
} catch { /* Non-critical: Webhook notification failures don't block login flow */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDocsUrl(anchor?: string) {
|
private getDocsUrl(anchor?: string) {
|
||||||
@@ -1760,7 +1792,7 @@ export class Login {
|
|||||||
const ctx = page.context()
|
const ctx = page.context()
|
||||||
const tab = await ctx.newPage()
|
const tab = await ctx.newPage()
|
||||||
await tab.goto(url, { waitUntil: 'domcontentloaded' })
|
await tab.goto(url, { waitUntil: 'domcontentloaded' })
|
||||||
} catch {/* ignore */ }
|
} catch { /* Non-critical: Documentation tab opening is best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------- Infrastructure ---------------
|
// --------------- Infrastructure ---------------
|
||||||
@@ -1770,7 +1802,7 @@ export class Login {
|
|||||||
const body = JSON.parse(route.request().postData() || '{}')
|
const body = JSON.parse(route.request().postData() || '{}')
|
||||||
body.isFidoSupported = false
|
body.isFidoSupported = false
|
||||||
route.continue({ postData: JSON.stringify(body) })
|
route.continue({ postData: JSON.stringify(body) })
|
||||||
} catch { route.continue() }
|
} catch { /* Route continue on parse failure */ route.continue() }
|
||||||
}).catch(logError('LOGIN-FIDO', 'Route interception setup failed', this.bot.isMobile))
|
}).catch(logError('LOGIN-FIDO', 'Route interception setup failed', this.bot.isMobile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export class MicrosoftRewardsBot {
|
|||||||
try {
|
try {
|
||||||
fs.unlinkSync(path.join(jobStateDir, file))
|
fs.unlinkSync(path.join(jobStateDir, file))
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore errors
|
// Expected: File may be locked or already deleted - non-critical
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ export interface Config {
|
|||||||
passesPerRun?: number;
|
passesPerRun?: number;
|
||||||
vacation?: ConfigVacation; // Optional monthly contiguous off-days
|
vacation?: ConfigVacation; // Optional monthly contiguous off-days
|
||||||
crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown
|
crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown
|
||||||
riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction
|
riskManagement?: ConfigRiskManagement; // Risk-aware throttling and ban prediction
|
||||||
dryRun?: boolean; // NEW: Dry-run mode (simulate without executing)
|
dryRun?: boolean; // Dry-run mode (simulate without executing)
|
||||||
queryDiversity?: ConfigQueryDiversity; // NEW: Multi-source query generation
|
queryDiversity?: ConfigQueryDiversity; // Multi-source query generation
|
||||||
dashboard?: ConfigDashboard; // NEW: Local web dashboard for monitoring and control
|
dashboard?: ConfigDashboard; // Local web dashboard for monitoring and control
|
||||||
scheduling?: ConfigScheduling; // NEW: Automatic scheduler configuration (cron/Task Scheduler)
|
scheduling?: ConfigScheduling; // Automatic scheduler configuration (cron/Task Scheduler)
|
||||||
errorReporting?: ConfigErrorReporting; // NEW: Automatic error reporting to community webhook
|
errorReporting?: ConfigErrorReporting; // Automatic error reporting to community webhook
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigSaveFingerprint {
|
export interface ConfigSaveFingerprint {
|
||||||
@@ -86,7 +86,10 @@ export interface ConfigUpdate {
|
|||||||
scriptPath?: string; // optional custom path to update script relative to repo root
|
scriptPath?: string; // optional custom path to update script relative to repo root
|
||||||
autoUpdateConfig?: boolean; // if true, allow auto-update of config.jsonc when remote changes it (default: false to preserve user settings)
|
autoUpdateConfig?: boolean; // if true, allow auto-update of config.jsonc when remote changes it (default: false to preserve user settings)
|
||||||
autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials)
|
autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials)
|
||||||
// DEPRECATED (removed in v2.56.2+): method, docker - update.mjs now uses GitHub API only
|
// DEPRECATED (v2.56.2+, remove in v3.0): method, docker fields no longer used
|
||||||
|
// Migration: update.mjs now exclusively uses GitHub API for all update methods
|
||||||
|
// See: scripts/installer/README.md for migration details
|
||||||
|
// TODO(@Obsidian-wtf): Remove deprecated fields in v3.0 major release
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigVacation {
|
export interface ConfigVacation {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export class InternalScheduler {
|
|||||||
return null // Invalid time format
|
return null // Invalid time format
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Legacy cron format (for backwards compatibility)
|
// Priority 2: COMPATIBILITY format (cron.schedule field, pre-v2.58)
|
||||||
if (scheduleConfig.cron?.schedule) {
|
if (scheduleConfig.cron?.schedule) {
|
||||||
return scheduleConfig.cron.schedule
|
return scheduleConfig.cron.schedule
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class Retry {
|
|||||||
const util = new Util()
|
const util = new Util()
|
||||||
const parse = (v: number | string) => {
|
const parse = (v: number | string) => {
|
||||||
if (typeof v === 'number') return v
|
if (typeof v === 'number') return v
|
||||||
try { return util.stringToMs(String(v)) } catch { return def.baseDelay }
|
try { return util.stringToMs(String(v)) } catch { /* Invalid time string: fall back to default */ return def.baseDelay }
|
||||||
}
|
}
|
||||||
this.policy = {
|
this.policy = {
|
||||||
maxAttempts: (merged.maxAttempts as number) ?? def.maxAttempts,
|
maxAttempts: (merged.maxAttempts as number) ?? def.maxAttempts,
|
||||||
|
|||||||
@@ -249,9 +249,9 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
|||||||
try {
|
try {
|
||||||
if (type in ntfyConditions && ntfyConditions[type as keyof typeof ntfyConditions].some(condition => condition)) {
|
if (type in ntfyConditions && ntfyConditions[type as keyof typeof ntfyConditions].some(condition => condition)) {
|
||||||
// Fire-and-forget
|
// Fire-and-forget
|
||||||
Promise.resolve(Ntfy(cleanStr, type)).catch(() => { /* ignore ntfy errors */ })
|
Promise.resolve(Ntfy(cleanStr, type)).catch(() => { /* Non-critical: NTFY notification errors are ignored */ })
|
||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* Non-critical: Webhook buffer cleanup can fail safely */ }
|
||||||
|
|
||||||
// Console output with better formatting and contextual icons
|
// Console output with better formatting and contextual icons
|
||||||
const typeIndicator = type === 'error' ? '✗' : type === 'warn' ? '⚠' : '✓'
|
const typeIndicator = type === 'error' ? '✗' : type === 'warn' ? '⚠' : '✓'
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function normalizeConfig(raw: unknown): Config {
|
|||||||
? true
|
? true
|
||||||
: (typeof browserConfig.headless === 'boolean'
|
: (typeof browserConfig.headless === 'boolean'
|
||||||
? browserConfig.headless
|
? browserConfig.headless
|
||||||
: (typeof n.headless === 'boolean' ? n.headless : false)) // Legacy fallback
|
: (typeof n.headless === 'boolean' ? n.headless : false)) // COMPATIBILITY: Flat headless field (pre-v2.50)
|
||||||
|
|
||||||
const globalTimeout = browserConfig.globalTimeout ?? n.globalTimeout ?? '30s'
|
const globalTimeout = browserConfig.globalTimeout ?? n.globalTimeout ?? '30s'
|
||||||
const browser: ConfigBrowser = {
|
const browser: ConfigBrowser = {
|
||||||
@@ -271,7 +271,7 @@ function buildSchedulingConfig(raw: unknown): ConfigScheduling | undefined {
|
|||||||
scheduling.time = timeField
|
scheduling.time = timeField
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Legacy cron format (backwards compatibility)
|
// Priority 2: COMPATIBILITY format (cron.schedule field, pre-v2.58)
|
||||||
const cronRaw = source.cron
|
const cronRaw = source.cron
|
||||||
if (cronRaw && typeof cronRaw === 'object') {
|
if (cronRaw && typeof cronRaw === 'object') {
|
||||||
scheduling.cron = {
|
scheduling.cron = {
|
||||||
@@ -315,7 +315,7 @@ export function loadAccounts(): Account[] {
|
|||||||
path.join(process.cwd(), file + 'c'), // cwd/accounts.jsonc
|
path.join(process.cwd(), file + 'c'), // cwd/accounts.jsonc
|
||||||
path.join(process.cwd(), 'src', file), // cwd/src/accounts.json
|
path.join(process.cwd(), 'src', file), // cwd/src/accounts.json
|
||||||
path.join(process.cwd(), 'src', file + 'c'), // cwd/src/accounts.jsonc
|
path.join(process.cwd(), 'src', file + 'c'), // cwd/src/accounts.jsonc
|
||||||
path.join(__dirname, file), // dist/accounts.json (legacy)
|
path.join(__dirname, file), // dist/accounts.json (compiled output)
|
||||||
path.join(__dirname, file + 'c') // dist/accounts.jsonc
|
path.join(__dirname, file + 'c') // dist/accounts.jsonc
|
||||||
]
|
]
|
||||||
let chosen: string | null = null
|
let chosen: string | null = null
|
||||||
|
|||||||
Reference in New Issue
Block a user