feat: Implement internal scheduler for automatic daily execution with timezone detection

This commit is contained in:
2025-11-13 15:19:35 +01:00
parent 2959fc8c73
commit 3b06b4ae83
9 changed files with 637 additions and 80 deletions

30
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"https-proxy-agent": "^7.0.6",
"luxon": "^3.5.0",
"ms": "^2.1.3",
"node-cron": "3.0.3",
"playwright": "1.52.0",
"rebrowser-playwright": "1.52.0",
"socks-proxy-agent": "^8.0.5",
@@ -30,6 +31,7 @@
"@types/express": "^4.17.25",
"@types/ms": "^0.7.34",
"@types/node": "^20.19.24",
"@types/node-cron": "3.0.11",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.57.0",
@@ -452,6 +454,13 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/node-cron": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz",
"integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@@ -2879,6 +2888,18 @@
"node": ">= 0.6"
}
},
"node_modules/node-cron": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
"license": "ISC",
"dependencies": {
"uuid": "8.3.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -4005,6 +4026,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",

View File

@@ -57,6 +57,7 @@
"@types/express": "^4.17.25",
"@types/ms": "^0.7.34",
"@types/node": "^20.19.24",
"@types/node-cron": "3.0.11",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.57.0",
@@ -75,10 +76,11 @@
"https-proxy-agent": "^7.0.6",
"luxon": "^3.5.0",
"ms": "^2.1.3",
"node-cron": "3.0.3",
"playwright": "1.52.0",
"rebrowser-playwright": "1.52.0",
"socks-proxy-agent": "^8.0.5",
"ts-node": "^10.9.2",
"ws": "^8.18.3"
}
}
}

View File

@@ -138,25 +138,12 @@
"host": "127.0.0.1"
},
// === SCHEDULING ===
// See docs/getting-started.md for setup instructions
// Automatic daily execution at specified time
// The bot will run once per day at the time you specify
// Time is based on YOUR computer/server timezone (automatically detected)
"scheduling": {
"enabled": false,
"type": "auto",
"cron": {
"schedule": "0 9 * * *",
"workingDirectory": "",
"nodePath": "",
"logFile": "",
"user": ""
},
"taskScheduler": {
"taskName": "Microsoft-Rewards-Bot",
"schedule": "09:00",
"frequency": "daily",
"workingDirectory": "",
"runAsUser": true,
"highestPrivileges": false
}
"enabled": false, // Set to true to enable automatic daily runs
"time": "09:00" // Time in 24h format (HH:MM) - e.g., "09:00" = 9 AM, "21:30" = 9:30 PM
},
// === UPDATES ===
"update": {

View File

@@ -26,6 +26,8 @@ import { DesktopFlow } from './flows/DesktopFlow'
import { MobileFlow } from './flows/MobileFlow'
import { SummaryReporter, type AccountResult } from './flows/SummaryReporter'
import { InternalScheduler } from './scheduler/InternalScheduler'
import { DISCORD, TIMEOUTS } from './constants'
import { Account } from './interface/Account'
@@ -110,9 +112,6 @@ export class MicrosoftRewardsBot {
if (this.config.jobState?.enabled !== false) {
this.accountJobState = new JobState(this.config)
}
// Note: Legacy SchedulerManager removed - use OS scheduler (cron/Task Scheduler) instead
// See docs/schedule.md for configuration
}
private shouldSkipAccount(email: string, dayKey: string): boolean {
@@ -879,6 +878,9 @@ async function main(): Promise<void> {
const crashState = { restarts: 0 }
const config = rewardsBot.config
// Scheduler instance (initialized in bootstrap if enabled)
let scheduler: InternalScheduler | null = null
// Auto-start dashboard if enabled in config
if (config.dashboard?.enabled) {
const { DashboardServer } = await import('./dashboard/server')
@@ -908,22 +910,26 @@ async function main(): Promise<void> {
const errorMsg = reason instanceof Error ? reason.message : String(reason)
const stack = reason instanceof Error ? reason.stack : undefined
log('main', 'FATAL', `UnhandledRejection: ${errorMsg}${stack ? `\nStack: ${stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error')
stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval
scheduler?.stop() // Stop scheduler before exit
stopWebhookCleanup()
gracefulExit(1)
})
process.on('uncaughtException', (err: Error) => {
log('main', 'FATAL', `UncaughtException: ${err.message}${err.stack ? `\nStack: ${err.stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error')
stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval
scheduler?.stop() // Stop scheduler before exit
stopWebhookCleanup()
gracefulExit(1)
})
process.on('SIGTERM', () => {
log('main', 'SHUTDOWN', 'Received SIGTERM, shutting down gracefully...', 'log')
stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval
scheduler?.stop() // Stop scheduler before exit
stopWebhookCleanup()
gracefulExit(0)
})
process.on('SIGINT', () => {
log('main', 'SHUTDOWN', 'Received SIGINT (Ctrl+C), shutting down gracefully...', 'log')
stopWebhookCleanup() // CLEANUP FIX: Stop webhook cleanup interval
scheduler?.stop() // Stop scheduler before exit
stopWebhookCleanup()
gracefulExit(0)
})
}
@@ -1025,6 +1031,34 @@ async function main(): Promise<void> {
log('main', 'UPDATE', `Update check failed (continuing): ${updateError instanceof Error ? updateError.message : String(updateError)}`, 'warn')
}
// Check if scheduling is enabled
if (config.scheduling?.enabled) {
// Initialize scheduler
scheduler = new InternalScheduler(config, async () => {
try {
await rewardsBot.initialize()
await rewardsBot.run()
} catch (error) {
log('main', 'SCHEDULER-TASK', `Scheduled run failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
throw error // Re-throw for scheduler retry logic
}
})
const schedulerStarted = scheduler.start()
if (schedulerStarted) {
log('main', 'MAIN', 'Bot running in scheduled mode. Process will stay alive.', 'log', 'green')
log('main', 'MAIN', 'Press CTRL+C to stop the scheduler and exit.', 'log', 'cyan')
// Keep process alive - scheduler handles execution
return
} else {
log('main', 'MAIN', 'Scheduler failed to start. Running one-time execution instead.', 'warn')
scheduler = null
// Continue with one-time execution below
}
}
// One-time execution (scheduling disabled or failed to start)
await rewardsBot.initialize()
await rewardsBot.run()
} catch (e) {

View File

@@ -193,22 +193,10 @@ export interface ConfigDashboard {
}
export interface ConfigScheduling {
enabled?: boolean; // enable automatic schedule configuration
type?: 'auto' | 'cron' | 'task-scheduler'; // auto-detect or force specific type
cron?: {
schedule?: string; // cron expression (default: "0 9 * * *")
workingDirectory?: string; // project root path (auto-detected if empty)
nodePath?: string; // path to node executable (auto-detected if empty)
logFile?: string; // optional log file path
user?: string; // optional specific user for crontab
};
taskScheduler?: {
taskName?: string; // task name in Windows Task Scheduler
schedule?: string; // time in 24h format (e.g., "09:00")
frequency?: 'daily' | 'weekly' | 'once'; // task frequency
workingDirectory?: string; // project root path (auto-detected if empty)
runAsUser?: boolean; // run under current user
highestPrivileges?: boolean; // request highest privileges
enabled?: boolean; // Enable automatic daily scheduling
time?: string; // Daily execution time in 24h format (HH:MM) - e.g., "09:00" for 9 AM (RECOMMENDED)
cron?: { // Legacy cron format (for backwards compatibility) - DEPRECATED
schedule?: string; // Cron expression - e.g., "0 9 * * *" for 9 AM daily
};
}

View File

@@ -0,0 +1,286 @@
import cron from 'node-cron'
import type { Config } from '../interface/Config'
import { log } from '../util/notifications/Logger'
/**
* Internal Scheduler for automatic bot execution
* Uses node-cron internally but provides simple time-based scheduling
*
* Features:
* - Simple time-based scheduling (e.g., "09:00" = daily at 9 AM)
* - Automatic timezone detection (uses your computer/server timezone)
* - Overlap protection (prevents concurrent runs)
* - Error recovery with retries
* - Clean shutdown handling
* - Cross-platform (Windows, Linux, Mac)
*/
export class InternalScheduler {
private cronJob: cron.ScheduledTask | null = null
private config: Config
private taskCallback: () => Promise<void>
private isRunning: boolean = false
private lastRunTime: Date | null = null
constructor(config: Config, taskCallback: () => Promise<void>) {
this.config = config
this.taskCallback = taskCallback
}
/**
* Start the scheduler if enabled in config
* @returns true if scheduler started successfully, false otherwise
*/
public start(): boolean {
const scheduleConfig = this.config.scheduling
// Validation checks
if (!scheduleConfig?.enabled) {
log('main', 'SCHEDULER', 'Internal scheduler disabled (scheduling.enabled = false)')
return false
}
// Get schedule from simple time format (e.g., "09:00") or fallback to cron format
const schedule = this.parseSchedule(scheduleConfig)
if (!schedule) {
log('main', 'SCHEDULER', 'Invalid schedule format. Use time in HH:MM format (e.g., "09:00" for 9 AM)', 'error')
return false
}
// Validate cron expression
if (!cron.validate(schedule)) {
log('main', 'SCHEDULER', `Invalid schedule: "${schedule}"`, 'error')
return false
}
try {
const timezone = this.detectTimezone()
this.cronJob = cron.schedule(schedule, async () => {
await this.runScheduledTask()
}, {
scheduled: true,
timezone
})
const displayTime = scheduleConfig.time || this.extractTimeFromCron(schedule)
log('main', 'SCHEDULER', '✓ Internal scheduler started', 'log', 'green')
log('main', 'SCHEDULER', ` Daily run time: ${displayTime}`, 'log', 'cyan')
log('main', 'SCHEDULER', ` Timezone: ${timezone}`, 'log', 'cyan')
log('main', 'SCHEDULER', ` Next run: ${this.getNextRunTime()}`, 'log', 'cyan')
return true
} catch (error) {
log('main', 'SCHEDULER', `Failed to start scheduler: ${error instanceof Error ? error.message : String(error)}`, 'error')
return false
}
}
/**
* Parse schedule from config - supports simple time format (HH:MM) or cron expression
* @returns Cron expression string
*/
private parseSchedule(scheduleConfig: { time?: string; cron?: { schedule?: string } }): string | null {
// Priority 1: Simple time format (e.g., "09:00")
if (scheduleConfig.time) {
const timeMatch = /^(\d{1,2}):(\d{2})$/.exec(scheduleConfig.time.trim())
if (timeMatch) {
const hours = parseInt(timeMatch[1]!, 10)
const minutes = parseInt(timeMatch[2]!, 10)
if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
// Convert to cron: "minute hour * * *" = daily at specified time
return `${minutes} ${hours} * * *`
}
}
return null // Invalid time format
}
// Priority 2: Legacy cron format (for backwards compatibility)
if (scheduleConfig.cron?.schedule) {
return scheduleConfig.cron.schedule
}
// Default: 9 AM daily
return '0 9 * * *'
}
/**
* Extract readable time from cron expression (for display purposes)
*/
private extractTimeFromCron(cronExpr: string): string {
const parts = cronExpr.split(' ')
if (parts.length >= 2) {
const minute = parts[0]
const hour = parts[1]
if (minute && hour && minute !== '*' && hour !== '*') {
return `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`
}
}
return cronExpr
}
/**
* Execute the scheduled task with overlap protection and retry logic
*/
private async runScheduledTask(): Promise<void> {
// Overlap protection
if (this.isRunning) {
log('main', 'SCHEDULER', 'Skipping scheduled run: previous task still running', 'warn')
return
}
const maxRetries = this.config.crashRecovery?.maxRestarts ?? 1
let attempts = 0
while (attempts <= maxRetries) {
try {
this.isRunning = true
this.lastRunTime = new Date()
log('main', 'SCHEDULER', '⏰ Scheduled run triggered', 'log', 'cyan')
await this.taskCallback()
log('main', 'SCHEDULER', '✓ Scheduled run completed successfully', 'log', 'green')
log('main', 'SCHEDULER', ` Next run: ${this.getNextRunTime()}`, 'log', 'cyan')
return // Success - exit retry loop
} catch (error) {
attempts++
const errorMsg = error instanceof Error ? error.message : String(error)
log('main', 'SCHEDULER', `Scheduled run failed (attempt ${attempts}/${maxRetries + 1}): ${errorMsg}`, 'error')
if (attempts <= maxRetries) {
const backoff = (this.config.crashRecovery?.backoffBaseMs ?? 2000) * attempts
log('main', 'SCHEDULER', `Retrying in ${backoff}ms...`, 'warn')
await new Promise(resolve => setTimeout(resolve, backoff))
} else {
log('main', 'SCHEDULER', `Max retries (${maxRetries + 1}) exceeded. Waiting for next scheduled run.`, 'error')
}
} finally {
this.isRunning = false
}
}
}
/**
* Stop the scheduler gracefully
*/
public stop(): void {
if (this.cronJob) {
this.cronJob.stop()
log('main', 'SCHEDULER', 'Scheduler stopped', 'warn')
this.cronJob = null
}
}
/**
* Get the next scheduled run time
*/
private getNextRunTime(): string {
if (!this.cronJob) return 'unknown'
try {
const scheduleConfig = this.config.scheduling
const timezone = this.detectTimezone()
// Get the cron schedule being used
let cronSchedule: string
if (scheduleConfig?.time) {
cronSchedule = this.parseSchedule(scheduleConfig) || '0 9 * * *'
} else if (scheduleConfig?.cron?.schedule) {
cronSchedule = scheduleConfig.cron.schedule
} else {
cronSchedule = '0 9 * * *'
}
// Calculate next run based on cron expression
const now = new Date()
const parts = cronSchedule.split(' ')
if (parts.length !== 5) {
return 'invalid schedule format'
}
// Simple next-run calculation for daily schedules
const [minute, hour] = parts
if (!minute || !hour || minute === '*' || hour === '*') {
return 'varies (see schedule configuration)'
}
const targetHour = parseInt(hour, 10)
const targetMinute = parseInt(minute, 10)
if (isNaN(targetHour) || isNaN(targetMinute)) {
return 'complex schedule'
}
const next = new Date(now)
next.setHours(targetHour, targetMinute, 0, 0)
// If time already passed today, move to tomorrow
if (next <= now) {
next.setDate(next.getDate() + 1)
}
return next.toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZone: timezone
})
} catch {
return 'unable to calculate'
}
}
/**
* Detect system timezone
*/
private detectTimezone(): string {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
} catch {
return 'UTC'
}
}
/**
* Check if scheduler is active
*/
public isActive(): boolean {
return this.cronJob !== null
}
/**
* Get scheduler status for monitoring
*/
public getStatus(): {
active: boolean
isRunning: boolean
lastRun: string | null
nextRun: string
} {
return {
active: this.isActive(),
isRunning: this.isRunning,
lastRun: this.lastRunTime ? this.lastRunTime.toLocaleString() : null,
nextRun: this.getNextRunTime()
}
}
/**
* Trigger an immediate run (useful for manual triggers or dashboard)
*/
public async triggerNow(): Promise<void> {
log('main', 'SCHEDULER', 'Manual trigger requested', 'log', 'cyan')
await this.runScheduledTask()
}
}

259
src/scheduler/README.md Normal file
View File

@@ -0,0 +1,259 @@
# Internal Scheduler
## Overview
The **Internal Scheduler** is a cross-platform scheduling system built into the bot. It automatically runs the bot daily at a specified time - **no external cron setup required, no Task Scheduler configuration needed**.
## Features
**Simple time format** - Just specify "09:00" for 9 AM
**Automatic timezone detection** - Uses your computer/server timezone automatically
**Cross-platform** - Works on Windows, Linux, and macOS
**Zero external configuration** - No cron or Task Scheduler setup required
**Overlap protection** - Prevents concurrent runs
**Automatic retry** - Retries failed runs with exponential backoff
**Clean shutdown** - Gracefully stops on CTRL+C or SIGTERM
## Quick Start
### 1. Enable Scheduling
Edit `src/config.jsonc`:
```jsonc
{
"scheduling": {
"enabled": true,
"time": "09:00" // Daily at 9:00 AM (your local time)
}
}
```
### 2. Run the Bot
```bash
npm start
```
The bot will:
- Start the internal scheduler
- Detect your timezone automatically
- Keep the process alive
- Execute automatically daily at the specified time
- Log each scheduled run
### 3. Stop the Bot
Press **CTRL+C** to stop the scheduler and exit cleanly.
## Time Format Examples
| Time | Description |
|------|-------------|
| `"09:00"` | Daily at 9:00 AM |
| `"21:30"` | Daily at 9:30 PM |
| `"00:00"` | Daily at midnight |
| `"12:00"` | Daily at noon |
| `"06:15"` | Daily at 6:15 AM |
**Note:** Always use 24-hour format (HH:MM). The scheduler automatically uses your system's timezone.
## Configuration Options
### `scheduling.enabled`
- **Type:** `boolean`
- **Default:** `false`
- **Description:** Enable/disable automatic scheduling
### `scheduling.time`
- **Type:** `string`
- **Format:** `"HH:MM"` (24-hour format)
- **Default:** `"09:00"`
- **Description:** Daily execution time in your local timezone
- **Examples:** `"09:00"`, `"21:30"`, `"00:00"`
### Legacy `scheduling.cron.schedule` (deprecated)
- **Type:** `string` (cron expression)
- **Description:** Advanced cron format (e.g., `"0 9 * * *"`)
- **Note:** Supported for backwards compatibility. Use `time` instead for simplicity.
## Timezone Detection
The scheduler **automatically detects your timezone** using your computer/server's system settings:
- **Windows:** Uses Windows timezone settings
- **Linux/Mac:** Uses system timezone (usually from `/etc/localtime`)
- **Docker:** Uses container timezone (set via `TZ` environment variable)
**You don't need to configure timezone manually!** The bot logs the detected timezone on startup:
```
✓ Internal scheduler started
Daily run time: 09:00
Timezone: America/New_York
Next run: Thu, Jan 18, 2024, 09:00 AM
```
## How It Works
1. **Initialization**
- Bot reads `config.scheduling`
- Detects your timezone automatically
- Creates scheduler for daily execution
- Validates time format
2. **Execution**
- Scheduler waits until next scheduled time
- Triggers bot execution (`initialize()` + `run()`)
- Logs start/completion/errors
- Calculates next run time
3. **Error Handling**
- Failed runs are retried (using `config.crashRecovery` settings)
- Overlap protection prevents concurrent runs
- Errors are logged but don't stop the scheduler
4. **Shutdown**
- SIGINT/SIGTERM signals stop the scheduler
- Current task completes before exit
- Cleanup handlers run normally
## Advantages Over OS Schedulers
| Feature | Internal Scheduler | OS Scheduler |
|---------|-------------------|--------------|
| Setup complexity | ✅ Simple | ❌ Manual config |
| Time format | ✅ Simple (09:00) | ❌ Complex cron |
| Windows support | ✅ Auto | ❌ Task Scheduler setup |
| Linux support | ✅ Auto | ⚠️ crontab config |
| Timezone handling | ✅ Automatic | ❌ Manual |
| Centralized logs | ✅ Yes | ❌ Separate logs |
| Dashboard integration | ✅ Possible | ❌ No |
| Process management | ⚠️ Must stay alive | ✅ Cron handles |
## Docker Users
Docker users can **continue using cron** if preferred (see `docker/` directory). The internal scheduler is **optional** and doesn't interfere with Docker cron setup.
**Docker Timezone Setup:**
```yaml
# docker/compose.yaml
environment:
- TZ=America/New_York # Set your timezone
- RUN_ON_START=true
```
## Troubleshooting
### Scheduler doesn't start
- Check `scheduling.enabled` is `true`
- Verify `time` is in HH:MM format (e.g., `"09:00"`)
- Look for errors in console output
### Wrong timezone detected
- Check your system timezone settings
- For Docker, set `TZ` environment variable
- Verify in logs: "Timezone: YOUR_TIMEZONE"
### Runs don't trigger at expected time
- Verify time format is 24-hour (e.g., `"21:00"` not `"9:00 PM"`)
- Check system clock is correct
- Wait for the next scheduled time (check logs for "Next run")
### Process exits unexpectedly
- Check `jobState.autoResetOnComplete` is `true` (for scheduled runs)
- Review crash logs
- Ensure no SIGTERM/SIGINT signals from system
## Advanced: Cron Format (Legacy)
For users who need complex schedules (e.g., multiple times per day, specific weekdays), you can use the legacy cron format:
```jsonc
{
"scheduling": {
"enabled": true,
"cron": {
"schedule": "0 9,21 * * *" // Daily at 9 AM and 9 PM
}
}
}
```
**Cron Expression Examples:**
| Schedule | Cron Expression | Description |
|----------|----------------|-------------|
| Every day at 9 AM | `0 9 * * *` | Once daily |
| Every 6 hours | `0 */6 * * *` | 4 times a day |
| Twice daily (9 AM, 9 PM) | `0 9,21 * * *` | Morning & evening |
| Weekdays at 8 AM | `0 8 * * 1-5` | Monday-Friday only |
| Every 30 minutes | `*/30 * * * *` | 48 times a day |
**Note:** Use [crontab.guru](https://crontab.guru) to create and validate cron expressions.
## API Reference
### `InternalScheduler.start()`
Starts the scheduler. Returns `true` if successful.
### `InternalScheduler.stop()`
Stops the scheduler gracefully.
### `InternalScheduler.isActive()`
Returns `true` if scheduler is running.
### `InternalScheduler.getStatus()`
Returns scheduler status object:
```typescript
{
active: boolean
isRunning: boolean
lastRun: string | null
nextRun: string
}
```
### `InternalScheduler.triggerNow()`
Manually trigger an immediate run (useful for dashboard).
## Related Files
- **Implementation:** `src/scheduler/InternalScheduler.ts`
- **Integration:** `src/index.ts` (main function)
- **Config:** `src/config.jsonc`
- **Interface:** `src/interface/Config.ts` (ConfigScheduling)
## Example: Complete Setup
```jsonc
// src/config.jsonc
{
"scheduling": {
"enabled": true,
"time": "09:00" // Daily at 9 AM (automatic timezone)
},
"jobState": {
"enabled": true,
"autoResetOnComplete": true // Reset state after each scheduled run
},
"crashRecovery": {
"enabled": true,
"maxRetries": 3 // Retry failed runs up to 3 times
}
}
```
Then run:
```bash
npm start
```
You'll see:
```
✓ Internal scheduler started
Daily run time: 09:00
Timezone: America/New_York
Next run: Thu, Jan 18, 2024, 09:00 AM
Bot is ready and waiting for scheduled execution...
```

View File

@@ -257,44 +257,25 @@ function extractStringField(obj: unknown, key: string): string | undefined {
return undefined
}
function extractBooleanField(obj: unknown, key: string): boolean | undefined {
if (obj && typeof obj === 'object' && key in obj) {
const value = (obj as Record<string, unknown>)[key]
return typeof value === 'boolean' ? value : undefined
}
return undefined
}
function buildSchedulingConfig(raw: unknown): ConfigScheduling | undefined {
if (!raw || typeof raw !== 'object') return undefined
const source = raw as Record<string, unknown>
const scheduling: ConfigScheduling = {
enabled: source.enabled === true,
type: typeof source.type === 'string' ? source.type as ConfigScheduling['type'] : undefined
enabled: source.enabled === true
}
// Priority 1: Simple time format (recommended)
const timeField = extractStringField(source, 'time')
if (timeField) {
scheduling.time = timeField
}
// Priority 2: Legacy cron format (backwards compatibility)
const cronRaw = source.cron
if (cronRaw && typeof cronRaw === 'object') {
scheduling.cron = {
schedule: extractStringField(cronRaw, 'schedule'),
workingDirectory: extractStringField(cronRaw, 'workingDirectory'),
nodePath: extractStringField(cronRaw, 'nodePath'),
logFile: extractStringField(cronRaw, 'logFile'),
user: extractStringField(cronRaw, 'user')
}
}
const taskRaw = source.taskScheduler
if (taskRaw && typeof taskRaw === 'object') {
const taskSource = taskRaw as Record<string, unknown>
scheduling.taskScheduler = {
taskName: extractStringField(taskRaw, 'taskName'),
schedule: extractStringField(taskRaw, 'schedule'),
frequency: typeof taskSource.frequency === 'string' ? taskSource.frequency as 'daily' | 'weekly' | 'once' : undefined,
workingDirectory: extractStringField(taskRaw, 'workingDirectory'),
runAsUser: extractBooleanField(taskRaw, 'runAsUser'),
highestPrivileges: extractBooleanField(taskRaw, 'highestPrivileges')
schedule: extractStringField(cronRaw, 'schedule')
}
}

View File

@@ -179,16 +179,6 @@ export class StartupValidator {
}
private validateConfig(config: Config): void {
const maybeSchedule = (config as unknown as { schedule?: unknown }).schedule
if (maybeSchedule !== undefined) {
this.addWarning(
'config',
'Legacy schedule settings detected in config.jsonc.',
'Remove schedule.* entries and use your operating system scheduler.',
'docs/schedule.md'
)
}
// Headless mode in Docker
if (process.env.FORCE_HEADLESS === '1' && config.browser?.headless === false) {
this.addWarning(