Enhance agent.user.js with a robust navigation state management system, introducing new functions for handling navigation state persistence and double execution prevention. Update README.md to reflect architectural changes, task-step system improvements, and detailed state management documentation.
This commit is contained in:
888
README.md
888
README.md
@@ -1,473 +1,575 @@
|
|||||||
# Binance Alpha Farm Agent
|
# Binance Alpha Farm Agent
|
||||||
|
|
||||||
Automated trading agent for Binance Alpha Farm with Task-Step architecture.
|
Automated trading agent for Binance Alpha Farm with robust state management and task-step architecture.
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
|
||||||
|
- [System Architecture](#system-architecture)
|
||||||
|
- [Code Structure & Module Order](#code-structure--module-order)
|
||||||
|
- [Task-Step System](#task-step-system)
|
||||||
|
- [Bot Status Flow](#bot-status-flow)
|
||||||
|
- [State Management](#state-management)
|
||||||
|
- [Navigation State Management](#navigation-state-management)
|
||||||
|
- [Helper Functions](#helper-functions)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [API Endpoints](#api-endpoints)
|
||||||
|
- [Development Notes](#development-notes)
|
||||||
|
|
||||||
## 🏗️ System Architecture
|
## 🏗️ System Architecture
|
||||||
|
|
||||||
### Core Components
|
The agent follows a **hybrid architecture** combining centralized task definitions with dedicated step execution:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ AppState │ │ TaskRunner │ │ StepRunner │
|
||||||
|
│ (State Mgmt) │◄──►│ (Task Mgmt) │◄──►│ (Step Exec) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ StateWatchers │ │ TASK_DEFINITIONS│ │ Step Functions │
|
||||||
|
│ (Observers) │ │ (Task Config) │ │ (Step Logic) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components:
|
||||||
- **AppState**: Centralized state management with observer pattern
|
- **AppState**: Centralized state management with observer pattern
|
||||||
- **TaskRunner**: Task execution engine
|
- **TaskRunner**: Handles task polling and coordination
|
||||||
- **StateWatchers**: Observers for state changes
|
- **StepRunner**: Executes individual steps with navigation handling
|
||||||
- **BAF API**: Server communication module
|
- **StateWatchers**: React to state changes (page, login, bot status)
|
||||||
- **BINANCE**: Binance page detection and navigation
|
- **TASK_DEFINITIONS**: Centralized task configurations
|
||||||
|
- **Step Functions**: Modular step logic (LoginSteps, OrderHistorySteps, BalanceSteps)
|
||||||
|
|
||||||
## 📋 Task-Step System
|
## 📁 Code Structure & Module Order
|
||||||
|
|
||||||
|
The code follows this specific order for maintainability:
|
||||||
|
|
||||||
|
### 1. **Configuration & Constants**
|
||||||
|
```javascript
|
||||||
|
// ====== CONFIGURATION ======
|
||||||
|
const CONFIG = { ... }
|
||||||
|
|
||||||
|
// ====== ENUMS & CONSTANTS ======
|
||||||
|
const BOT_STATUS = { ... }
|
||||||
|
const TASK_TYPES = { ... }
|
||||||
|
const STEP_TYPES = { ... }
|
||||||
|
const LOGIN_METHOD = { ... }
|
||||||
|
const ORDER_TYPE = { ... }
|
||||||
|
const ORDER_STATUS = { ... }
|
||||||
|
const BALANCE_FORMAT = { ... }
|
||||||
|
const GAS_FEE_TYPE = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **State Management**
|
||||||
|
```javascript
|
||||||
|
// ====== STATE MANAGEMENT ======
|
||||||
|
class AppState { ... }
|
||||||
|
|
||||||
|
// ====== STORAGE MODULE ======
|
||||||
|
const STORAGE = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Utility Modules**
|
||||||
|
```javascript
|
||||||
|
// ====== TL UTILITY MODULE ======
|
||||||
|
const TL = { ... }
|
||||||
|
|
||||||
|
// ====== BAF API MODULE ======
|
||||||
|
const BAF = { ... }
|
||||||
|
|
||||||
|
// ====== BINANCE PAGE MODULE ======
|
||||||
|
const BINANCE = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Step Functions (Modular Logic)**
|
||||||
|
```javascript
|
||||||
|
// ====== STEP FUNCTIONS ======
|
||||||
|
const LoginSteps = { ... }
|
||||||
|
const OrderHistorySteps = { ... }
|
||||||
|
const BalanceSteps = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Task Definitions**
|
||||||
|
```javascript
|
||||||
|
// ====== TASK DEFINITIONS ======
|
||||||
|
const TASK_DEFINITIONS = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. **Core Execution Engine**
|
||||||
|
```javascript
|
||||||
|
// ====== STEP RUNNER ======
|
||||||
|
class StepRunner { ... }
|
||||||
|
|
||||||
|
// ====== TASK RUNNER ======
|
||||||
|
const TaskRunner = { ... }
|
||||||
|
|
||||||
|
// ====== STATE WATCHERS ======
|
||||||
|
const StateWatchers = { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. **UI & Initialization**
|
||||||
|
```javascript
|
||||||
|
// ====== UI & MENU ======
|
||||||
|
async function createGM_Menu() { ... }
|
||||||
|
|
||||||
|
// ====== MONITORING ======
|
||||||
|
async function heartbeat_report() { ... }
|
||||||
|
async function monitorPageChanges() { ... }
|
||||||
|
|
||||||
|
// ====== INITIALIZATION ======
|
||||||
|
async function initialize() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Task-Step System
|
||||||
|
|
||||||
### Task Types
|
### Task Types
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
TASK_TYPES = {
|
const TASK_TYPES = {
|
||||||
LOGIN: 'login',
|
LOGIN: 'login',
|
||||||
GET_ORDER_HISTORY: 'get_order_history',
|
GET_ORDER_HISTORY: 'get-order-history',
|
||||||
GET_BALANCE: 'get_balance',
|
GET_BALANCE: 'get-balance',
|
||||||
SWAP: 'swap',
|
SWAP: 'swap',
|
||||||
NO_TASK: 'no_task'
|
NO_TASK: 'no-task'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step Types by Task
|
||||||
|
|
||||||
|
#### **Login Task**
|
||||||
|
```javascript
|
||||||
|
const STEP_TYPES = {
|
||||||
|
LOGIN: {
|
||||||
|
NAVIGATE_TO_LOGIN: 'NavigateToLogin',
|
||||||
|
SELECT_QR_CODE: 'SelectQRCode',
|
||||||
|
WAIT_FOR_LOGIN: 'WaitForLogin'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Order History Task**
|
||||||
|
```javascript
|
||||||
|
const STEP_TYPES = {
|
||||||
|
GET_ORDER_HISTORY: {
|
||||||
|
NAVIGATE_TO_ORDER_HISTORY: 'NavigateToOrderHistory',
|
||||||
|
EXTRACT_ORDER_DATA: 'ExtractOrderData',
|
||||||
|
SEND_ORDER_DATA: 'SendOrderData'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Balance Task**
|
||||||
|
```javascript
|
||||||
|
const STEP_TYPES = {
|
||||||
|
GET_BALANCE: {
|
||||||
|
NAVIGATE_TO_BALANCE: 'NavigateToBalance',
|
||||||
|
EXTRACT_BALANCE_DATA: 'ExtractBalanceData',
|
||||||
|
SEND_BALANCE_DATA: 'SendBalanceData'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task Data Examples
|
||||||
|
|
||||||
|
#### **Login Task**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: TASK_TYPES.LOGIN,
|
||||||
|
id: "login_123",
|
||||||
|
data: {
|
||||||
|
method: LOGIN_METHOD.QR_CODE,
|
||||||
|
timeout: 300000, // 5 minutes
|
||||||
|
retry_count: 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step Types
|
#### **Order History Task**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
STEP_TYPES = {
|
{
|
||||||
// Login task steps
|
type: TASK_TYPES.GET_ORDER_HISTORY,
|
||||||
NAVIGATE_TO_LOGIN: 'navigate_to_login',
|
id: "order_history_456",
|
||||||
SELECT_QR_CODE: 'select_qr_code',
|
data: {
|
||||||
SEND_TO_SERVER: 'send_to_server',
|
limit: 50,
|
||||||
WAIT_FOR_LOGIN: 'wait_for_login',
|
date_from: "2024-01-01",
|
||||||
REPORT_RESULT: 'report_result',
|
date_to: "2024-12-31",
|
||||||
|
status: ORDER_STATUS.COMPLETED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// Order history task steps
|
#### **Balance Task**
|
||||||
NAVIGATE_TO_ORDER_HISTORY: 'navigate_to_order_history',
|
```javascript
|
||||||
EXTRACT_ORDER_DATA: 'extract_order_data',
|
{
|
||||||
SEND_ORDER_DATA: 'send_order_data',
|
type: TASK_TYPES.GET_BALANCE,
|
||||||
|
id: "balance_789",
|
||||||
// Balance task steps
|
data: {
|
||||||
NAVIGATE_TO_BALANCE: 'navigate_to_balance',
|
currencies: ["USDT", "BTC", "ETH"],
|
||||||
EXTRACT_BALANCE_DATA: 'extract_balance_data',
|
format: BALANCE_FORMAT.DECIMAL
|
||||||
SEND_BALANCE_DATA: 'send_balance_data',
|
}
|
||||||
|
|
||||||
// Swap task steps
|
|
||||||
NAVIGATE_TO_SWAP: 'navigate_to_swap',
|
|
||||||
FILL_SWAP_FORM: 'fill_swap_form',
|
|
||||||
CONFIRM_SWAP: 'confirm_swap',
|
|
||||||
WAIT_FOR_SWAP: 'wait_for_swap',
|
|
||||||
|
|
||||||
// Common steps
|
|
||||||
WAIT: 'wait',
|
|
||||||
ERROR_HANDLING: 'error_handling'
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤖 Bot Status Flow
|
## 🤖 Bot Status Flow
|
||||||
|
|
||||||
### Status Types
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
BOT_STATUS = {
|
|
||||||
READY_FOR_NEW_TASKS: 'ready-for-new-tasks',
|
|
||||||
PAUSE_AUTOMATION: 'pause-automation',
|
|
||||||
PERFORMING_TASKS: 'performing-tasks'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Main Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Initialize │
|
|
||||||
└─────────┬───────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Check Bot Status│
|
|
||||||
└─────────┬───────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Ready for Tasks │────▶│ Ask Server │
|
|
||||||
└─────────┬───────┘ │ for Tasks │
|
|
||||||
│ └─────────┬───────┘
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Sleep │ │ No Task Found │
|
|
||||||
│ (Interval) │◀────│ │
|
|
||||||
└─────────┬───────┘ └─────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Check Bot Status│────▶│ Task Received │
|
|
||||||
└─────────┬───────┘ └─────────┬───────┘
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌─────────────────┐ ┌─────────────────┐
|
|
||||||
│ Ready for Tasks │ │ Perform Task │
|
|
||||||
└─────────────────┘ └─────────┬───────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Task Complete │
|
|
||||||
└─────────┬───────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ Return to Ready │
|
|
||||||
└─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status Transitions
|
### Status Transitions
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ ready-for-new- │────►│ performing- │────►│ ready-for-new- │
|
||||||
|
│ tasks │ │ tasks │ │ tasks │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│ │ ▲
|
||||||
|
▼ ▼ │
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ pause- │◄────┤ Task Complete │─────────────┘
|
||||||
|
│ automation │ │ or Error │
|
||||||
|
└─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
1. **READY_FOR_NEW_TASKS**
|
### Flow Description:
|
||||||
- Poll server for new tasks
|
1. **ready-for-new-tasks**: Bot polls server for new tasks
|
||||||
- Sleep for `task_poll_interval` if no tasks
|
2. **performing-tasks**: Bot executes task steps
|
||||||
- Transition to `PERFORMING_TASKS` when task received
|
3. **pause-automation**: Bot stops (manual or error)
|
||||||
|
4. **Back to ready-for-new-tasks**: After task completion or resume
|
||||||
2. **PAUSE_AUTOMATION**
|
|
||||||
- Stop all automation
|
|
||||||
- No polling or task execution
|
|
||||||
- Manual intervention required
|
|
||||||
|
|
||||||
3. **PERFORMING_TASKS**
|
|
||||||
- Execute current task step by step
|
|
||||||
- Update task data during execution
|
|
||||||
- Transition back to `READY_FOR_NEW_TASKS` when complete
|
|
||||||
|
|
||||||
## 📊 Task Data Examples
|
|
||||||
|
|
||||||
### Login Task
|
|
||||||
|
|
||||||
|
### Task Polling Logic:
|
||||||
```javascript
|
```javascript
|
||||||
{
|
// Poll only when ready-for-new-tasks
|
||||||
type: TASK_TYPES.LOGIN,
|
if (bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) {
|
||||||
id: "task_123",
|
const task = await BAF.getTask();
|
||||||
data: {
|
if (task.type === TASK_TYPES.NO_TASK) {
|
||||||
method: LOGIN_METHOD.QR_CODE, // 'qr_code' | 'password' | 'email'
|
// Sleep for interval before next poll
|
||||||
credentials: {
|
await delay(CONFIG.task_poll_interval);
|
||||||
email: 'user@example.com', // For password method
|
} else {
|
||||||
password: 'password123' // For password method
|
// Start task execution
|
||||||
},
|
bot_status = BOT_STATUS.PERFORMING_TASKS;
|
||||||
timeout: 300000, // 5 minutes timeout
|
}
|
||||||
retry_count: 3
|
|
||||||
},
|
|
||||||
steps: [
|
|
||||||
STEP_TYPES.NAVIGATE_TO_LOGIN,
|
|
||||||
STEP_TYPES.SELECT_QR_CODE,
|
|
||||||
STEP_TYPES.SEND_TO_SERVER,
|
|
||||||
STEP_TYPES.WAIT_FOR_LOGIN,
|
|
||||||
STEP_TYPES.REPORT_RESULT
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Get Order History Task
|
## 🎯 State Management
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
type: TASK_TYPES.GET_ORDER_HISTORY,
|
|
||||||
id: "task_124",
|
|
||||||
data: {
|
|
||||||
limit: 100,
|
|
||||||
offset: 0,
|
|
||||||
date_range: {
|
|
||||||
start_date: '2024-01-01',
|
|
||||||
end_date: '2024-12-31'
|
|
||||||
},
|
|
||||||
order_types: [
|
|
||||||
ORDER_TYPE.SWAP,
|
|
||||||
ORDER_TYPE.LIMIT,
|
|
||||||
ORDER_TYPE.MARKET
|
|
||||||
],
|
|
||||||
status: [
|
|
||||||
ORDER_STATUS.COMPLETED,
|
|
||||||
ORDER_STATUS.PENDING,
|
|
||||||
ORDER_STATUS.CANCELLED
|
|
||||||
]
|
|
||||||
},
|
|
||||||
steps: [
|
|
||||||
STEP_TYPES.NAVIGATE_TO_ORDER_HISTORY,
|
|
||||||
STEP_TYPES.EXTRACT_ORDER_DATA,
|
|
||||||
STEP_TYPES.SEND_ORDER_DATA
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get Balance Task
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
type: TASK_TYPES.GET_BALANCE,
|
|
||||||
id: "task_125",
|
|
||||||
data: {
|
|
||||||
currencies: ['USDT', 'BTC', 'ETH'], // null for all currencies
|
|
||||||
include_zero: false,
|
|
||||||
include_locked: true,
|
|
||||||
format: BALANCE_FORMAT.JSON // 'json' | 'csv'
|
|
||||||
},
|
|
||||||
steps: [
|
|
||||||
STEP_TYPES.NAVIGATE_TO_BALANCE,
|
|
||||||
STEP_TYPES.EXTRACT_BALANCE_DATA,
|
|
||||||
STEP_TYPES.SEND_BALANCE_DATA
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Swap Task
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
type: TASK_TYPES.SWAP,
|
|
||||||
id: "task_126",
|
|
||||||
data: {
|
|
||||||
from_token: 'USDT',
|
|
||||||
to_token: 'BTC',
|
|
||||||
amount: 100,
|
|
||||||
slippage: 0.5, // percentage
|
|
||||||
gas_fee: GAS_FEE_TYPE.AUTO, // 'auto' | 'fast' | 'slow' | number
|
|
||||||
deadline: 300, // seconds
|
|
||||||
auto_confirm: true
|
|
||||||
},
|
|
||||||
steps: [
|
|
||||||
STEP_TYPES.NAVIGATE_TO_SWAP,
|
|
||||||
STEP_TYPES.FILL_SWAP_FORM,
|
|
||||||
STEP_TYPES.CONFIRM_SWAP,
|
|
||||||
STEP_TYPES.WAIT_FOR_SWAP
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Enums & Constants
|
|
||||||
|
|
||||||
### Login Method
|
|
||||||
```javascript
|
|
||||||
LOGIN_METHOD = {
|
|
||||||
QR_CODE: 'qr_code',
|
|
||||||
PASSWORD: 'password',
|
|
||||||
EMAIL: 'email'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Order Type
|
|
||||||
```javascript
|
|
||||||
ORDER_TYPE = {
|
|
||||||
SWAP: 'swap',
|
|
||||||
LIMIT: 'limit',
|
|
||||||
MARKET: 'market'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Order Status
|
|
||||||
```javascript
|
|
||||||
ORDER_STATUS = {
|
|
||||||
COMPLETED: 'completed',
|
|
||||||
PENDING: 'pending',
|
|
||||||
CANCELLED: 'cancelled'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Balance Format
|
|
||||||
```javascript
|
|
||||||
BALANCE_FORMAT = {
|
|
||||||
JSON: 'json',
|
|
||||||
CSV: 'csv'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gas Fee Type
|
|
||||||
```javascript
|
|
||||||
GAS_FEE_TYPE = {
|
|
||||||
AUTO: 'auto',
|
|
||||||
FAST: 'fast',
|
|
||||||
SLOW: 'slow'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Page Detection
|
|
||||||
|
|
||||||
### Supported Pages
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
BINANCE_PAGES = {
|
|
||||||
LOGIN: 'login',
|
|
||||||
ALPHA_SWAP: 'alpha-swap',
|
|
||||||
ALPHA_ORDER_HISTORY: 'alpha-order-history',
|
|
||||||
UNKNOWN: 'unknown'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Page URLs
|
|
||||||
|
|
||||||
- **Login**: `https://accounts.binance.com/login`
|
|
||||||
- **Alpha Swap**: `https://www.binance.com/alpha/bsc/`
|
|
||||||
- **Alpha Order History**: `https://www.binance.com/en/my/orders/alpha/orderhistory`
|
|
||||||
|
|
||||||
## 🔄 State Management
|
|
||||||
|
|
||||||
### AppState Structure
|
### AppState Structure
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
class AppState {
|
||||||
|
data = {
|
||||||
// Server configuration
|
// Server configuration
|
||||||
server_mode: 'prod',
|
server_mode: 'prod',
|
||||||
server: { label: '🌐 Prod', url: 'https://baf.thuanle.me' },
|
server: null,
|
||||||
|
|
||||||
// Page state
|
// Page state
|
||||||
current_page: 'alpha-swap',
|
current_page: null,
|
||||||
is_logged_in: true,
|
is_logged_in: false,
|
||||||
|
|
||||||
// Bot status
|
// Bot status
|
||||||
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
||||||
|
|
||||||
// Task state
|
// Task state
|
||||||
current_task: { type: 'login', id: 'task_123', ... },
|
current_task: null,
|
||||||
task_data: { method: 'qr_code', timeout: 300000, ... },
|
task_data: null,
|
||||||
current_step: 'navigate_to_login',
|
current_step: null,
|
||||||
step_data: { attempts: 0, start_time: 1234567890 },
|
step_data: null,
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
error_message: null
|
error_message: null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Observer Pattern
|
### Observer Pattern
|
||||||
|
|
||||||
State changes trigger observers:
|
|
||||||
|
|
||||||
- **server_mode** → Save to storage, reload page
|
|
||||||
- **bot_status** → Save to storage, start/stop task polling
|
|
||||||
- **current_page** → Continue current step if task active
|
|
||||||
- **is_logged_in** → Continue current step if task active
|
|
||||||
|
|
||||||
## 🛠️ Helper Functions
|
|
||||||
|
|
||||||
### TaskRunner Helpers
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Validate enum values
|
|
||||||
TaskRunner.validateEnum(value, enumObj, defaultValue)
|
|
||||||
|
|
||||||
// Validate and merge task data
|
|
||||||
TaskRunner.validateTaskData(taskType, taskData)
|
|
||||||
|
|
||||||
// Get current task data
|
|
||||||
TaskRunner.getCurrentTaskData()
|
|
||||||
|
|
||||||
// Update task data during execution
|
|
||||||
TaskRunner.updateTaskData(newData)
|
|
||||||
|
|
||||||
// Check current task type
|
|
||||||
TaskRunner.isCurrentTaskType(TASK_TYPES.LOGIN)
|
|
||||||
|
|
||||||
// Get current task type
|
|
||||||
TaskRunner.getCurrentTaskType()
|
|
||||||
```
|
|
||||||
|
|
||||||
### State Helpers
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Get current state
|
|
||||||
APP_STATE.getData()
|
|
||||||
|
|
||||||
// Update state (triggers observers)
|
|
||||||
APP_STATE.update({ bot_status: BOT_STATUS.PERFORMING_TASKS })
|
|
||||||
|
|
||||||
// Subscribe to state changes
|
// Subscribe to state changes
|
||||||
APP_STATE.subscribe('bot_status', callback)
|
APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange);
|
||||||
|
APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange);
|
||||||
|
APP_STATE.subscribe('is_logged_in', StateWatchers.onLoginStateChange);
|
||||||
|
|
||||||
// Unsubscribe from state changes
|
// Notify observers on state change
|
||||||
APP_STATE.unsubscribe('bot_status', callback)
|
await APP_STATE.update({ bot_status: BOT_STATUS.PERFORMING_TASKS });
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎮 Menu Commands
|
## 🧭 Navigation State Management
|
||||||
|
|
||||||
### Available Commands
|
|
||||||
|
|
||||||
1. **Server**: Switch between local/prod servers
|
|
||||||
2. **Token**: Configure Bearer token for API access
|
|
||||||
3. **Bot Status**: Cycle through bot statuses (ready/pause/performing)
|
|
||||||
4. **Get Tasks**: Manually check for new tasks
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Access via Tampermonkey menu
|
|
||||||
// Right-click extension icon → Binance Alpha Farm Agent
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📡 API Endpoints
|
|
||||||
|
|
||||||
### Server Communication
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Ping server with status
|
|
||||||
BAF.ping(status)
|
|
||||||
|
|
||||||
// Get new task
|
|
||||||
BAF.getTasks()
|
|
||||||
|
|
||||||
// Submit task result
|
|
||||||
BAF.submitTaskResult(taskId, result)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status Payload
|
|
||||||
|
|
||||||
|
### Navigation State Structure
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
logged_in: true,
|
navigating: true,
|
||||||
current_page: 'alpha-swap',
|
navigation_start: 1703123456789,
|
||||||
bot_status: 'ready-for-new-tasks',
|
navigation_target: "https://accounts.binance.com/login",
|
||||||
current_task: 'login',
|
task_id: "task_123",
|
||||||
task_data: { method: 'qr_code', ... },
|
step_index: 0
|
||||||
current_step: 'navigate_to_login',
|
|
||||||
has_current_task: true
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Configuration
|
### Navigation Flow
|
||||||
|
```
|
||||||
### CONFIG Object
|
1. Step executes → ctx.goto('/login')
|
||||||
|
↓
|
||||||
|
2. Mark navigating = true
|
||||||
|
↓
|
||||||
|
3. Save navigation state to storage
|
||||||
|
↓
|
||||||
|
4. window.location.href = '/login'
|
||||||
|
↓
|
||||||
|
5. Page reloads, userscript restarts
|
||||||
|
↓
|
||||||
|
6. resumeTask() → detect navigation state
|
||||||
|
↓
|
||||||
|
7. handleNavigationResume() → check target page
|
||||||
|
↓
|
||||||
|
8. Skip navigation step, continue next step
|
||||||
|
```
|
||||||
|
|
||||||
|
### guardDoubleRun Helper
|
||||||
```javascript
|
```javascript
|
||||||
CONFIG = {
|
// Prevents double execution during navigation
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
// Step logic here
|
||||||
|
if (!BINANCE.isOnLoginPage()) {
|
||||||
|
BINANCE.navigateToLogin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Helper Functions
|
||||||
|
|
||||||
|
### TL Utility Module
|
||||||
|
```javascript
|
||||||
|
const TL = {
|
||||||
|
// Logging
|
||||||
|
log: (level, message, data) => { ... },
|
||||||
|
debug: (level, message, data) => { ... },
|
||||||
|
error: (level, message, error) => { ... },
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
|
||||||
|
notification: (title, text, timeout) => { ... },
|
||||||
|
|
||||||
|
// Navigation guard
|
||||||
|
guardDoubleRun: async (ctx, stepLogic) => { ... },
|
||||||
|
|
||||||
|
// DOM helpers
|
||||||
|
dom: {
|
||||||
|
isVisible: (el) => { ... },
|
||||||
|
isDisabled: (el) => { ... },
|
||||||
|
click: (el) => { ... },
|
||||||
|
scrollToView: (el, behavior) => { ... }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Network helpers
|
||||||
|
net: {
|
||||||
|
gmRequest: (url, init) => { ... }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### TaskRunner Helpers
|
||||||
|
```javascript
|
||||||
|
const TaskRunner = {
|
||||||
|
// Task validation
|
||||||
|
validateEnum: (value, enumObj, defaultValue) => { ... },
|
||||||
|
validateTaskData: (taskType, taskData) => { ... },
|
||||||
|
|
||||||
|
// Task state
|
||||||
|
getCurrentTaskData: () => { ... },
|
||||||
|
updateTaskData: (newData) => { ... },
|
||||||
|
isCurrentTaskType: (taskType) => { ... },
|
||||||
|
|
||||||
|
// Task execution
|
||||||
|
checkForNewTasks: async () => { ... },
|
||||||
|
continueCurrentStep: async () => { ... }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### StepRunner Context
|
||||||
|
```javascript
|
||||||
|
// Context passed to step functions
|
||||||
|
{
|
||||||
|
task_id: "task_123",
|
||||||
|
task_type: TASK_TYPES.LOGIN,
|
||||||
|
task_data: { method: LOGIN_METHOD.QR_CODE },
|
||||||
|
step_data: {},
|
||||||
|
is_logged_in: false,
|
||||||
|
current_page: BINANCE_PAGES.LOGIN,
|
||||||
|
current_step_name: "NavigateToLogin",
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
done: (result) => this.completeTask(result),
|
||||||
|
goto: (url) => this.navigateTo(url),
|
||||||
|
wait: (ms) => this.wait(ms),
|
||||||
|
retry: (fn, maxAttempts) => this.retry(fn, maxAttempts)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
### Server Configuration
|
||||||
|
```javascript
|
||||||
|
const CONFIG = {
|
||||||
heartbeat_interval: 10000, // 10 seconds
|
heartbeat_interval: 10000, // 10 seconds
|
||||||
task_poll_interval: 10000, // 10 seconds
|
task_poll_interval: 10000, // 10 seconds
|
||||||
is_debug: true,
|
is_debug: true,
|
||||||
servers: {
|
servers: {
|
||||||
local: { label: '🏠 Local', url: 'http://localhost:3000' },
|
local: {
|
||||||
prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' }
|
label: '🏠 Local',
|
||||||
|
url: 'http://localhost:3000'
|
||||||
|
},
|
||||||
|
prod: {
|
||||||
|
label: '🌐 Prod',
|
||||||
|
url: 'https://baf.thuanle.me'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Getting Started
|
### Storage Keys
|
||||||
|
```javascript
|
||||||
|
const STORAGE = {
|
||||||
|
key_token: 'baf-agent-token',
|
||||||
|
key_server_mode: 'baf-server-mode',
|
||||||
|
key_bot_status: 'baf-bot-status',
|
||||||
|
key_navigation_state: 'baf-navigation-state'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
1. Install Tampermonkey browser extension
|
## 🌐 API Endpoints
|
||||||
2. Install the user script
|
|
||||||
3. Configure server token via menu
|
|
||||||
4. Set bot status to "ready-for-new-tasks"
|
|
||||||
5. Bot will automatically start polling for tasks
|
|
||||||
|
|
||||||
## 📝 Development Notes
|
### BAF API Methods
|
||||||
|
```javascript
|
||||||
|
const BAF = {
|
||||||
|
// Server management
|
||||||
|
getServer: async () => { ... },
|
||||||
|
getHost: async () => { ... },
|
||||||
|
|
||||||
### Adding New Task Types
|
// API requests
|
||||||
|
request: async (method, path, options) => { ... },
|
||||||
|
|
||||||
1. Add to `TASK_TYPES` enum
|
// Task management
|
||||||
2. Define task data structure in `TASK_DATA_EXAMPLES`
|
getTask: async () => { ... },
|
||||||
3. Add validation in `validateTaskData()`
|
submitTaskResult: async (taskId, result) => { ... },
|
||||||
4. Implement execution method in `TaskRunner`
|
|
||||||
5. Define steps in `STEP_TYPES`
|
// Heartbeat
|
||||||
|
heartbeat: async (data) => { ... }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Page Detection
|
||||||
|
```javascript
|
||||||
|
const BINANCE = {
|
||||||
|
// Page detection
|
||||||
|
detectPage: () => { ... },
|
||||||
|
isOnLoginPage: () => { ... },
|
||||||
|
isOnAlphaSwapPage: () => { ... },
|
||||||
|
isOnAlphaOrderHistoryPage: () => { ... },
|
||||||
|
isLoggedIn: async () => { ... },
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
navigateToLogin: () => { ... },
|
||||||
|
navigateToAlphaSwap: () => { ... },
|
||||||
|
navigateToAlphaOrderHistory: () => { ... }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Development Notes
|
||||||
|
|
||||||
|
### Adding New Tasks
|
||||||
|
|
||||||
|
1. **Define Task Type**:
|
||||||
|
```javascript
|
||||||
|
const TASK_TYPES = {
|
||||||
|
// ... existing types
|
||||||
|
NEW_TASK: 'new-task'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Step Functions**:
|
||||||
|
```javascript
|
||||||
|
const NewTaskSteps = {
|
||||||
|
matchStep1: (url, ctx) => { /* match logic */ },
|
||||||
|
runStep1: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
// Step logic
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add Task Definition**:
|
||||||
|
```javascript
|
||||||
|
const TASK_DEFINITIONS = {
|
||||||
|
[TASK_TYPES.NEW_TASK]: {
|
||||||
|
id: "new-task",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: "Step1",
|
||||||
|
match: NewTaskSteps.matchStep1,
|
||||||
|
run: NewTaskSteps.runStep1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
### Adding New Steps
|
### Adding New Steps
|
||||||
|
|
||||||
1. Add to `STEP_TYPES` enum
|
1. **Add Step Type**:
|
||||||
2. Implement step logic in task execution
|
```javascript
|
||||||
3. Update step data during execution
|
const STEP_TYPES = {
|
||||||
4. Handle step transitions
|
NEW_TASK: {
|
||||||
|
// ... existing steps
|
||||||
|
NEW_STEP: 'NewStep'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
### Error Handling
|
2. **Implement Step Functions**:
|
||||||
|
```javascript
|
||||||
|
const NewTaskSteps = {
|
||||||
|
// ... existing steps
|
||||||
|
matchNewStep: (url, ctx) => {
|
||||||
|
return url.includes("/target-page") && ctx?.step_data?.previous_step_completed;
|
||||||
|
},
|
||||||
|
|
||||||
- Invalid enum values fallback to defaults
|
runNewStep: async (ctx) => {
|
||||||
- Task execution errors return bot to ready state
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
- Network errors trigger retry after interval
|
// Step logic here
|
||||||
- Page navigation errors handled by observers
|
ctx.step_data.new_step_completed = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update Task Definition**:
|
||||||
|
```javascript
|
||||||
|
[TASK_TYPES.NEW_TASK]: {
|
||||||
|
id: "new-task",
|
||||||
|
steps: [
|
||||||
|
// ... existing steps
|
||||||
|
{
|
||||||
|
name: "NewStep",
|
||||||
|
match: NewTaskSteps.matchNewStep,
|
||||||
|
run: NewTaskSteps.runNewStep
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Always use `guardDoubleRun`** for step functions to prevent double execution
|
||||||
|
2. **Use enums** for type safety (TASK_TYPES, LOGIN_METHOD, etc.)
|
||||||
|
3. **Validate task data** using `TaskRunner.validateTaskData`
|
||||||
|
4. **Handle navigation** using `ctx.goto()` instead of direct `window.location`
|
||||||
|
5. **Use context helpers** (`ctx.wait()`, `ctx.retry()`) for robust execution
|
||||||
|
6. **Log appropriately** using `TL.log()`, `TL.debug()`, `TL.error()`
|
||||||
|
7. **Update step_data** to track progress and avoid duplicate work
|
||||||
|
8. **Handle errors gracefully** with try-catch and proper error reporting
|
||||||
|
|
||||||
|
### Debugging Tips
|
||||||
|
|
||||||
|
1. **Enable debug mode**: Set `CONFIG.is_debug = true`
|
||||||
|
2. **Check navigation state**: Use `STORAGE.getNavigationState()`
|
||||||
|
3. **Monitor state changes**: Watch `APP_STATE.getData()` values
|
||||||
|
4. **Check step execution**: Look for `[STEP]` logs in console
|
||||||
|
5. **Verify page detection**: Use `BINANCE.detectPage()` to check current page
|
||||||
|
6. **Test step matching**: Use step `match` functions directly
|
||||||
|
7. **Check task data**: Use `TaskRunner.getCurrentTaskData()`
|
||||||
807
agent.user.js
807
agent.user.js
@@ -141,6 +141,7 @@
|
|||||||
key_token: 'baf-agent-token',
|
key_token: 'baf-agent-token',
|
||||||
key_server_mode: 'baf-server-mode',
|
key_server_mode: 'baf-server-mode',
|
||||||
key_bot_status: 'baf-bot-status',
|
key_bot_status: 'baf-bot-status',
|
||||||
|
key_navigation_state: 'baf-navigation-state',
|
||||||
|
|
||||||
getToken: () => GM_getValue(STORAGE.key_token, ''),
|
getToken: () => GM_getValue(STORAGE.key_token, ''),
|
||||||
setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
|
setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
|
||||||
@@ -150,6 +151,23 @@
|
|||||||
|
|
||||||
getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.READY_FOR_NEW_TASKS),
|
getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.READY_FOR_NEW_TASKS),
|
||||||
setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status),
|
setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status),
|
||||||
|
|
||||||
|
// Navigation state management
|
||||||
|
getNavigationState: () => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(GM_getValue(STORAGE.key_navigation_state, 'null'));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setNavigationState: (state) => {
|
||||||
|
GM_setValue(STORAGE.key_navigation_state, JSON.stringify(state));
|
||||||
|
},
|
||||||
|
|
||||||
|
clearNavigationState: () => {
|
||||||
|
GM_setValue(STORAGE.key_navigation_state, 'null');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ====== UTILITY MODULE ======
|
// ====== UTILITY MODULE ======
|
||||||
@@ -168,6 +186,33 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
|
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
|
||||||
|
|
||||||
|
// Guard against double execution during navigation
|
||||||
|
guardDoubleRun: async (ctx, stepLogic) => {
|
||||||
|
// Check if we're currently navigating
|
||||||
|
if (ctx.step_data.navigating) {
|
||||||
|
TL.debug('STEP', 'Currently navigating, skipping step execution');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this step was already completed after navigation
|
||||||
|
if (ctx.step_data.navigation_complete && ctx.step_data.last_completed_step === ctx.current_step_name) {
|
||||||
|
TL.debug('STEP', 'Step already completed after navigation, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stepLogic();
|
||||||
|
|
||||||
|
// Mark step as completed (unless navigation was triggered)
|
||||||
|
if (!ctx.step_data.navigating) {
|
||||||
|
ctx.step_data.last_completed_step = ctx.current_step_name;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', `Error in step ${ctx.current_step_name}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// DOM helpers
|
// DOM helpers
|
||||||
@@ -445,8 +490,645 @@
|
|||||||
ERROR_HANDLING: 'error_handling'
|
ERROR_HANDLING: 'error_handling'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ====== STEP FUNCTIONS ======
|
||||||
|
|
||||||
|
// Login Step Functions
|
||||||
|
const LoginSteps = {
|
||||||
|
matchNavigateToLogin: (url, ctx) => {
|
||||||
|
const isOnLoginPage = url.includes("/login");
|
||||||
|
const isLoggedIn = ctx?.is_logged_in || false;
|
||||||
|
return isOnLoginPage || !isLoggedIn;
|
||||||
|
},
|
||||||
|
|
||||||
|
runNavigateToLogin: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'NavigateToLogin: Starting login process');
|
||||||
|
|
||||||
|
if (!BINANCE.isOnLoginPage()) {
|
||||||
|
BINANCE.navigateToLogin();
|
||||||
|
ctx.step_data.page_navigated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.step_data.page_loaded = true;
|
||||||
|
ctx.step_data.start_time = Date.now();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchSelectQRCode: (url, ctx) => {
|
||||||
|
return url.includes("/login") &&
|
||||||
|
ctx?.step_data?.page_loaded &&
|
||||||
|
ctx?.task_data?.method === LOGIN_METHOD.QR_CODE;
|
||||||
|
},
|
||||||
|
|
||||||
|
runSelectQRCode: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'SelectQRCode: Selecting QR code method');
|
||||||
|
|
||||||
|
const qrButton = document.querySelector('[data-testid="qr-code-tab"], .qr-code-btn, .qr-tab');
|
||||||
|
if (qrButton) {
|
||||||
|
qrButton.click();
|
||||||
|
ctx.step_data.qr_selected = true;
|
||||||
|
TL.debug('STEP', 'QR code method selected');
|
||||||
|
} else {
|
||||||
|
TL.debug('STEP', 'QR code button not found, may already be selected');
|
||||||
|
ctx.step_data.qr_selected = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchWaitForLogin: (url, ctx) => {
|
||||||
|
return !url.includes("/login") && ctx?.step_data?.qr_selected;
|
||||||
|
},
|
||||||
|
|
||||||
|
runWaitForLogin: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'WaitForLogin: Checking login status');
|
||||||
|
|
||||||
|
const isLoggedIn = await BINANCE.isLoggedIn();
|
||||||
|
if (isLoggedIn) {
|
||||||
|
ctx.step_data.login_success = true;
|
||||||
|
ctx.step_data.login_time = Date.now();
|
||||||
|
ctx.done({ success: true, message: 'Login successful' });
|
||||||
|
} else {
|
||||||
|
const elapsed = Date.now() - ctx.step_data.start_time;
|
||||||
|
const timeout = ctx.task_data?.timeout || 300000;
|
||||||
|
|
||||||
|
if (elapsed > timeout) {
|
||||||
|
ctx.done({ success: false, error: 'Login timeout' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Order History Step Functions
|
||||||
|
const OrderHistorySteps = {
|
||||||
|
matchNavigateToOrderHistory: (url, ctx) => {
|
||||||
|
return url.includes("/my/orders/alpha/orderhistory") ||
|
||||||
|
!url.includes("/my/orders/alpha/orderhistory");
|
||||||
|
},
|
||||||
|
|
||||||
|
runNavigateToOrderHistory: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'NavigateToOrderHistory: Navigating to order history page');
|
||||||
|
|
||||||
|
if (!BINANCE.isOnAlphaOrderHistoryPage()) {
|
||||||
|
BINANCE.navigateToAlphaOrderHistory();
|
||||||
|
ctx.step_data.page_navigated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.step_data.page_loaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchExtractOrderData: (url, ctx) => {
|
||||||
|
return url.includes("/my/orders/alpha/orderhistory") &&
|
||||||
|
ctx?.step_data?.page_loaded;
|
||||||
|
},
|
||||||
|
|
||||||
|
runExtractOrderData: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'ExtractOrderData: Extracting order data');
|
||||||
|
|
||||||
|
await TL.delay(2000);
|
||||||
|
|
||||||
|
const orders = [];
|
||||||
|
const orderRows = document.querySelectorAll('.order-row, [data-testid="order-row"]');
|
||||||
|
|
||||||
|
orderRows.forEach((row, index) => {
|
||||||
|
if (index < (ctx.task_data?.limit || 100)) {
|
||||||
|
const order = {
|
||||||
|
id: row.getAttribute('data-order-id') || `order_${index}`,
|
||||||
|
type: row.querySelector('.order-type')?.textContent || 'unknown',
|
||||||
|
status: row.querySelector('.order-status')?.textContent || 'unknown',
|
||||||
|
amount: row.querySelector('.order-amount')?.textContent || '0',
|
||||||
|
date: row.querySelector('.order-date')?.textContent || new Date().toISOString()
|
||||||
|
};
|
||||||
|
orders.push(order);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.step_data.orders = orders;
|
||||||
|
ctx.step_data.extracted = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchSendOrderData: (url, ctx) => {
|
||||||
|
return ctx?.step_data?.extracted;
|
||||||
|
},
|
||||||
|
|
||||||
|
runSendOrderData: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'SendOrderData: Sending order data to server');
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
success: true,
|
||||||
|
order_count: ctx.step_data.orders.length,
|
||||||
|
orders: ctx.step_data.orders
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.done(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Balance Step Functions
|
||||||
|
const BalanceSteps = {
|
||||||
|
matchNavigateToBalance: (url, ctx) => {
|
||||||
|
return url.includes("/alpha/bsc/") || !url.includes("/alpha/bsc/");
|
||||||
|
},
|
||||||
|
|
||||||
|
runNavigateToBalance: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'NavigateToBalance: Navigating to balance page');
|
||||||
|
|
||||||
|
if (!BINANCE.isOnAlphaSwapPage()) {
|
||||||
|
BINANCE.navigateToAlphaSwap();
|
||||||
|
ctx.step_data.page_navigated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.step_data.page_loaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchExtractBalanceData: (url, ctx) => {
|
||||||
|
return url.includes("/alpha/bsc/") && ctx?.step_data?.page_loaded;
|
||||||
|
},
|
||||||
|
|
||||||
|
runExtractBalanceData: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'ExtractBalanceData: Extracting balance data');
|
||||||
|
|
||||||
|
await TL.delay(2000);
|
||||||
|
|
||||||
|
const balances = {};
|
||||||
|
const balanceElements = document.querySelectorAll('.balance-item, [data-testid="balance-item"]');
|
||||||
|
|
||||||
|
balanceElements.forEach(element => {
|
||||||
|
const currency = element.querySelector('.currency')?.textContent || 'unknown';
|
||||||
|
const amount = element.querySelector('.amount')?.textContent || '0';
|
||||||
|
|
||||||
|
if (ctx.task_data?.currencies === null ||
|
||||||
|
ctx.task_data?.currencies?.includes(currency)) {
|
||||||
|
balances[currency] = amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.step_data.balances = balances;
|
||||||
|
ctx.step_data.extracted = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchSendBalanceData: (url, ctx) => {
|
||||||
|
return ctx?.step_data?.extracted;
|
||||||
|
},
|
||||||
|
|
||||||
|
runSendBalanceData: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'SendBalanceData: Sending balance data to server');
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
success: true,
|
||||||
|
balance_count: Object.keys(ctx.step_data.balances).length,
|
||||||
|
balances: ctx.step_data.balances
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.done(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ====== TASK DEFINITIONS ======
|
||||||
|
const TASK_DEFINITIONS = {
|
||||||
|
[TASK_TYPES.LOGIN]: {
|
||||||
|
id: "login-task",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: "NavigateToLogin",
|
||||||
|
match: LoginSteps.matchNavigateToLogin,
|
||||||
|
run: LoginSteps.runNavigateToLogin
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SelectQRCode",
|
||||||
|
match: LoginSteps.matchSelectQRCode,
|
||||||
|
run: LoginSteps.runSelectQRCode
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WaitForLogin",
|
||||||
|
match: LoginSteps.matchWaitForLogin,
|
||||||
|
run: LoginSteps.runWaitForLogin
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
[TASK_TYPES.GET_ORDER_HISTORY]: {
|
||||||
|
id: "get-order-history-task",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: "NavigateToOrderHistory",
|
||||||
|
match: OrderHistorySteps.matchNavigateToOrderHistory,
|
||||||
|
run: OrderHistorySteps.runNavigateToOrderHistory
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExtractOrderData",
|
||||||
|
match: OrderHistorySteps.matchExtractOrderData,
|
||||||
|
run: OrderHistorySteps.runExtractOrderData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SendOrderData",
|
||||||
|
match: OrderHistorySteps.matchSendOrderData,
|
||||||
|
run: OrderHistorySteps.runSendOrderData
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
[TASK_TYPES.GET_BALANCE]: {
|
||||||
|
id: "get-balance-task",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: "NavigateToBalance",
|
||||||
|
match: BalanceSteps.matchNavigateToBalance,
|
||||||
|
run: BalanceSteps.runNavigateToBalance
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExtractBalanceData",
|
||||||
|
match: BalanceSteps.matchExtractBalanceData,
|
||||||
|
run: BalanceSteps.runExtractBalanceData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SendBalanceData",
|
||||||
|
match: BalanceSteps.matchSendBalanceData,
|
||||||
|
run: BalanceSteps.runSendBalanceData
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ====== STEP RUNNER ======
|
||||||
|
class StepRunner {
|
||||||
|
constructor() {
|
||||||
|
this.currentStepIndex = 0;
|
||||||
|
this.context = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context for task execution
|
||||||
|
createContext(task, taskData) {
|
||||||
|
return {
|
||||||
|
task_id: task.id,
|
||||||
|
task_type: task.type,
|
||||||
|
task_data: taskData,
|
||||||
|
step_data: {},
|
||||||
|
is_logged_in: APP_STATE.getData().is_logged_in,
|
||||||
|
current_page: APP_STATE.getData().current_page,
|
||||||
|
current_step_name: null, // Will be set by executeCurrentStep
|
||||||
|
done: (result) => this.completeTask(result),
|
||||||
|
goto: (url) => this.navigateTo(url),
|
||||||
|
wait: (ms) => this.wait(ms),
|
||||||
|
retry: (fn, maxAttempts = 3) => this.retry(fn, maxAttempts)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to URL
|
||||||
|
navigateTo(url) {
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
TL.error('STEP', 'Invalid URL provided for navigation:', url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle different URL formats
|
||||||
|
let targetUrl = url;
|
||||||
|
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
// Relative URL - append to current domain
|
||||||
|
targetUrl = window.location.origin + url;
|
||||||
|
} else if (!url.startsWith('http')) {
|
||||||
|
// Relative URL without leading slash
|
||||||
|
targetUrl = window.location.origin + '/' + url;
|
||||||
|
}
|
||||||
|
// Absolute URL (starts with http) - use as is
|
||||||
|
|
||||||
|
TL.debug('STEP', `Navigating to: ${targetUrl}`);
|
||||||
|
|
||||||
|
// Mark that we're navigating to avoid double execution
|
||||||
|
this.context.step_data.navigating = true;
|
||||||
|
this.context.step_data.navigation_start = Date.now();
|
||||||
|
this.context.step_data.navigation_target = targetUrl;
|
||||||
|
|
||||||
|
// Save navigation state to storage for persistence across page reload
|
||||||
|
STORAGE.setNavigationState({
|
||||||
|
navigating: true,
|
||||||
|
navigation_start: this.context.step_data.navigation_start,
|
||||||
|
navigation_target: targetUrl,
|
||||||
|
task_id: this.context.task_id,
|
||||||
|
step_index: this.currentStepIndex
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = targetUrl;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', 'Navigation failed:', error);
|
||||||
|
// Clear navigation state on error
|
||||||
|
this.context.step_data.navigating = false;
|
||||||
|
delete this.context.step_data.navigation_start;
|
||||||
|
delete this.context.step_data.navigation_target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for specified milliseconds
|
||||||
|
async wait(ms) {
|
||||||
|
if (ms && ms > 0) {
|
||||||
|
TL.debug('STEP', `Waiting for ${ms}ms`);
|
||||||
|
await TL.delay(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry function with exponential backoff
|
||||||
|
async retry(fn, maxAttempts = 3) {
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
TL.debug('STEP', `Attempt ${attempt} failed:`, error.message);
|
||||||
|
|
||||||
|
if (attempt === maxAttempts) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponential backoff: 1s, 2s, 4s
|
||||||
|
const delay = Math.pow(2, attempt - 1) * 1000;
|
||||||
|
await this.wait(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete task and return to ready state
|
||||||
|
async completeTask(result) {
|
||||||
|
try {
|
||||||
|
TL.log('STEP', `Task completed with result:`, result);
|
||||||
|
|
||||||
|
const data = APP_STATE.getData();
|
||||||
|
if (data.current_task) {
|
||||||
|
// Submit result to server
|
||||||
|
try {
|
||||||
|
await BAF.submitTaskResult(data.current_task.id, result);
|
||||||
|
TL.debug('STEP', 'Task result submitted to server successfully');
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', 'Failed to submit task result to server:', error);
|
||||||
|
// Continue with cleanup even if server submission fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return to ready state
|
||||||
|
await APP_STATE.update({
|
||||||
|
current_task: null,
|
||||||
|
task_data: null,
|
||||||
|
current_step: null,
|
||||||
|
step_data: null,
|
||||||
|
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
||||||
|
is_loading: false,
|
||||||
|
error_message: result.success ? null : result.error
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset step runner state
|
||||||
|
this.currentStepIndex = 0;
|
||||||
|
this.context = null;
|
||||||
|
|
||||||
|
// Clear navigation state
|
||||||
|
STORAGE.clearNavigationState();
|
||||||
|
|
||||||
|
// Check for next task after a short delay
|
||||||
|
setTimeout(() => TaskRunner.checkForNewTasks(), 1000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', 'Error during task completion:', error);
|
||||||
|
// Force return to ready state even if cleanup fails
|
||||||
|
await APP_STATE.update({
|
||||||
|
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
||||||
|
is_loading: false,
|
||||||
|
error_message: 'Task completion error: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute current step
|
||||||
|
async executeCurrentStep() {
|
||||||
|
const data = APP_STATE.getData();
|
||||||
|
if (!data.current_task || !this.context) {
|
||||||
|
TL.debug('STEP', 'No current task or context available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskDefinition = TASK_DEFINITIONS[data.current_task.type];
|
||||||
|
if (!taskDefinition) {
|
||||||
|
TL.error('STEP', `No task definition found for type: ${data.current_task.type}`);
|
||||||
|
await this.completeTask({ success: false, error: 'Unknown task type' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStep = taskDefinition.steps[this.currentStepIndex];
|
||||||
|
if (!currentStep) {
|
||||||
|
TL.error('STEP', `No step found at index: ${this.currentStepIndex}`);
|
||||||
|
await this.completeTask({ success: false, error: 'Step not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set current step name in context
|
||||||
|
this.context.current_step_name = currentStep.name;
|
||||||
|
|
||||||
|
const url = window.location.href;
|
||||||
|
TL.debug('STEP', `Checking step: ${currentStep.name} at URL: ${url}`);
|
||||||
|
|
||||||
|
if (currentStep.match(url, this.context)) {
|
||||||
|
try {
|
||||||
|
TL.log('STEP', `Executing step: ${currentStep.name}`);
|
||||||
|
|
||||||
|
// Update context with current state
|
||||||
|
this.context.is_logged_in = APP_STATE.getData().is_logged_in;
|
||||||
|
this.context.current_page = APP_STATE.getData().current_page;
|
||||||
|
|
||||||
|
// Execute step (guardDoubleRun is already wrapped in step functions)
|
||||||
|
await currentStep.run(this.context);
|
||||||
|
|
||||||
|
// Check if navigation was triggered
|
||||||
|
if (this.context.step_data.navigating) {
|
||||||
|
TL.debug('STEP', 'Navigation triggered, stopping execution');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next step
|
||||||
|
this.currentStepIndex++;
|
||||||
|
await APP_STATE.update({
|
||||||
|
current_step: currentStep.name,
|
||||||
|
step_data: this.context.step_data
|
||||||
|
});
|
||||||
|
|
||||||
|
// Continue with next step if available
|
||||||
|
if (this.currentStepIndex < taskDefinition.steps.length) {
|
||||||
|
TL.debug('STEP', `Moving to next step: ${this.currentStepIndex + 1}/${taskDefinition.steps.length}`);
|
||||||
|
setTimeout(() => this.executeCurrentStep(), 100);
|
||||||
|
} else {
|
||||||
|
// All steps completed
|
||||||
|
TL.log('STEP', 'All steps completed successfully');
|
||||||
|
await this.completeTask({ success: true, message: 'All steps completed' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', `Error executing step ${currentStep.name}:`, error);
|
||||||
|
await this.completeTask({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TL.debug('STEP', `Step ${currentStep.name} conditions not met, waiting for page change...`);
|
||||||
|
// Don't retry immediately, wait for page change or state change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume task from saved state
|
||||||
|
async resumeTask() {
|
||||||
|
const data = APP_STATE.getData();
|
||||||
|
if (!data.current_task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for navigation state
|
||||||
|
const navigationState = STORAGE.getNavigationState();
|
||||||
|
if (navigationState && navigationState.navigating) {
|
||||||
|
await this.handleNavigationResume(navigationState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context from saved state
|
||||||
|
this.context = this.createContext(data.current_task, data.task_data);
|
||||||
|
this.context.step_data = data.step_data || {};
|
||||||
|
|
||||||
|
// Resume from current step
|
||||||
|
await this.executeCurrentStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle navigation resume
|
||||||
|
async handleNavigationResume(navigationState) {
|
||||||
|
TL.log('STEP', 'Resuming after navigation:', navigationState);
|
||||||
|
|
||||||
|
const data = APP_STATE.getData();
|
||||||
|
if (!data.current_task || data.current_task.id !== navigationState.task_id) {
|
||||||
|
TL.warn('STEP', 'Task ID mismatch, clearing navigation state');
|
||||||
|
STORAGE.clearNavigationState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're on the target page
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const targetUrl = navigationState.navigation_target;
|
||||||
|
|
||||||
|
if (currentUrl.includes(targetUrl) || this.isSamePage(currentUrl, targetUrl)) {
|
||||||
|
TL.log('STEP', 'Successfully navigated to target page');
|
||||||
|
|
||||||
|
// Clear navigation state
|
||||||
|
STORAGE.clearNavigationState();
|
||||||
|
|
||||||
|
// Create context and continue
|
||||||
|
this.context = this.createContext(data.current_task, data.task_data);
|
||||||
|
this.context.step_data = data.step_data || {};
|
||||||
|
this.currentStepIndex = navigationState.step_index;
|
||||||
|
|
||||||
|
// Mark navigation as complete
|
||||||
|
this.context.step_data.navigating = false;
|
||||||
|
this.context.step_data.navigation_complete = true;
|
||||||
|
this.context.step_data.navigation_complete_time = Date.now();
|
||||||
|
|
||||||
|
// Continue with next step
|
||||||
|
this.currentStepIndex++;
|
||||||
|
await this.executeCurrentStep();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Still navigating or navigation failed
|
||||||
|
const elapsed = Date.now() - navigationState.navigation_start;
|
||||||
|
const timeout = 10000; // 10 seconds timeout
|
||||||
|
|
||||||
|
if (elapsed > timeout) {
|
||||||
|
TL.error('STEP', 'Navigation timeout, clearing state');
|
||||||
|
STORAGE.clearNavigationState();
|
||||||
|
await this.completeTask({
|
||||||
|
success: false,
|
||||||
|
error: 'Navigation timeout'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
TL.debug('STEP', 'Still navigating, waiting...');
|
||||||
|
// Wait a bit more for navigation to complete
|
||||||
|
setTimeout(() => this.handleNavigationResume(navigationState), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if two URLs represent the same page
|
||||||
|
isSamePage(url1, url2) {
|
||||||
|
try {
|
||||||
|
const parsed1 = new URL(url1);
|
||||||
|
const parsed2 = new URL(url2);
|
||||||
|
|
||||||
|
// Compare hostname and pathname
|
||||||
|
return parsed1.hostname === parsed2.hostname &&
|
||||||
|
parsed1.pathname === parsed2.pathname;
|
||||||
|
} catch {
|
||||||
|
// Fallback to string comparison
|
||||||
|
return url1 === url2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate task definition
|
||||||
|
validateTaskDefinition(taskType) {
|
||||||
|
const taskDefinition = TASK_DEFINITIONS[taskType];
|
||||||
|
if (!taskDefinition) {
|
||||||
|
throw new Error(`No task definition found for type: ${taskType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!taskDefinition.steps || !Array.isArray(taskDefinition.steps)) {
|
||||||
|
throw new Error(`Invalid steps array for task type: ${taskType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDefinition.steps.forEach((step, index) => {
|
||||||
|
if (!step.name || !step.match || !step.run) {
|
||||||
|
throw new Error(`Invalid step at index ${index} for task type: ${taskType}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return taskDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new task
|
||||||
|
async startTask(task, taskData) {
|
||||||
|
try {
|
||||||
|
TL.log('STEP', `Starting task: ${task.type}`, task);
|
||||||
|
|
||||||
|
// Validate task definition
|
||||||
|
this.validateTaskDefinition(task.type);
|
||||||
|
|
||||||
|
this.currentStepIndex = 0;
|
||||||
|
this.context = this.createContext(task, taskData);
|
||||||
|
|
||||||
|
await APP_STATE.update({
|
||||||
|
current_task: task,
|
||||||
|
task_data: taskData,
|
||||||
|
current_step: null,
|
||||||
|
step_data: {},
|
||||||
|
bot_status: BOT_STATUS.PERFORMING_TASKS,
|
||||||
|
is_loading: true,
|
||||||
|
error_message: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start executing steps
|
||||||
|
await this.executeCurrentStep();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', 'Failed to start task:', error);
|
||||||
|
await this.completeTask({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ====== TASK RUNNER ======
|
// ====== TASK RUNNER ======
|
||||||
const TaskRunner = {
|
const TaskRunner = {
|
||||||
|
stepRunner: new StepRunner(),
|
||||||
|
|
||||||
// Helper function to validate enum values
|
// Helper function to validate enum values
|
||||||
validateEnum(value, enumObj, defaultValue) {
|
validateEnum(value, enumObj, defaultValue) {
|
||||||
const validValues = Object.values(enumObj);
|
const validValues = Object.values(enumObj);
|
||||||
@@ -529,18 +1211,11 @@
|
|||||||
const task = response.data.task;
|
const task = response.data.task;
|
||||||
TL.log('TASK', `Received new task: ${task.type}`, task);
|
TL.log('TASK', `Received new task: ${task.type}`, task);
|
||||||
|
|
||||||
// Validate and merge task data with defaults
|
// Validate task data
|
||||||
const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data);
|
const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data);
|
||||||
|
|
||||||
await APP_STATE.update({
|
// Start task using StepRunner
|
||||||
current_task: task,
|
await this.stepRunner.startTask(task, validatedTaskData);
|
||||||
task_data: validatedTaskData,
|
|
||||||
current_step: task.steps?.[0] || null,
|
|
||||||
bot_status: BOT_STATUS.PERFORMING_TASKS
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start executing the task
|
|
||||||
await this.executeTask(task);
|
|
||||||
} else if (response.ok && response.data.no_task) {
|
} else if (response.ok && response.data.no_task) {
|
||||||
TL.debug('TASK', 'No new tasks available');
|
TL.debug('TASK', 'No new tasks available');
|
||||||
// Sleep for interval then check again
|
// Sleep for interval then check again
|
||||||
@@ -553,104 +1228,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async executeTask(task) {
|
// Continue current step (called by observers)
|
||||||
TL.log('TASK', `Executing task: ${task.type}`, task);
|
async continueCurrentStep() {
|
||||||
|
await this.stepRunner.executeCurrentStep();
|
||||||
try {
|
|
||||||
await APP_STATE.update({ is_loading: true });
|
|
||||||
|
|
||||||
let result = null;
|
|
||||||
|
|
||||||
switch (task.type) {
|
|
||||||
case TASK_TYPES.LOGIN:
|
|
||||||
result = await this.executeLoginTask(task);
|
|
||||||
break;
|
|
||||||
case TASK_TYPES.GET_ORDER_HISTORY:
|
|
||||||
result = await this.executeGetOrderHistoryTask(task);
|
|
||||||
break;
|
|
||||||
case TASK_TYPES.GET_BALANCE:
|
|
||||||
result = await this.executeGetBalanceTask(task);
|
|
||||||
break;
|
|
||||||
case TASK_TYPES.SWAP:
|
|
||||||
result = await this.executeSwapTask(task);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown task type: ${task.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit result to server
|
|
||||||
await BAF.submitTaskResult(task.id, result);
|
|
||||||
|
|
||||||
// Task completed, return to ready state
|
|
||||||
await APP_STATE.update({
|
|
||||||
current_task: null,
|
|
||||||
task_data: null,
|
|
||||||
current_step: null,
|
|
||||||
step_data: null,
|
|
||||||
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
|
||||||
is_loading: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for next task after a short delay
|
|
||||||
setTimeout(() => this.checkForNewTasks(), 1000);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
TL.error('TASK', `Task execution failed:`, error);
|
|
||||||
await APP_STATE.update({
|
|
||||||
error_message: error.message,
|
|
||||||
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
|
||||||
is_loading: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retry after interval
|
|
||||||
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeLoginTask(task) {
|
|
||||||
const taskData = this.getCurrentTaskData();
|
|
||||||
TL.log('TASK', 'Executing login task', taskData);
|
|
||||||
|
|
||||||
// Implementation for login task with steps
|
|
||||||
// taskData contains: { method: 'qr_code' | 'password', credentials: {...}, timeout: 300000, retry_count: 3 }
|
|
||||||
|
|
||||||
// Example: Update task data during execution
|
|
||||||
await this.updateTaskData({
|
|
||||||
start_time: Date.now(),
|
|
||||||
attempts: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, message: 'Login task completed' };
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeGetOrderHistoryTask(task) {
|
|
||||||
const taskData = this.getCurrentTaskData();
|
|
||||||
TL.log('TASK', 'Executing get order history task', taskData);
|
|
||||||
|
|
||||||
// Implementation for get order history task
|
|
||||||
// taskData contains: { limit: 100, offset: 0, date_range: {...}, order_types: [...], status: [...] }
|
|
||||||
|
|
||||||
return { success: true, order_history: [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeGetBalanceTask(task) {
|
|
||||||
const taskData = this.getCurrentTaskData();
|
|
||||||
TL.log('TASK', 'Executing get balance task', taskData);
|
|
||||||
|
|
||||||
// Implementation for get balance task
|
|
||||||
// taskData contains: { currencies: ['USDT', 'BTC'], include_zero: false, include_locked: true, format: 'json' }
|
|
||||||
|
|
||||||
return { success: true, balance: {} };
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeSwapTask(task) {
|
|
||||||
const taskData = this.getCurrentTaskData();
|
|
||||||
TL.log('TASK', 'Executing swap task', taskData);
|
|
||||||
|
|
||||||
// Implementation for swap task
|
|
||||||
// taskData contains: { from_token: 'USDT', to_token: 'BTC', amount: 100, slippage: 0.5, gas_fee: 'auto', deadline: 300, auto_confirm: true }
|
|
||||||
|
|
||||||
return { success: true, swap_result: {} };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -685,7 +1265,7 @@
|
|||||||
TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`);
|
TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`);
|
||||||
|
|
||||||
// Handle page-specific logic for current task
|
// Handle page-specific logic for current task
|
||||||
if (data.current_task && data.current_step) {
|
if (data.current_task && data.bot_status === BOT_STATUS.PERFORMING_TASKS) {
|
||||||
// Continue with current step based on page change
|
// Continue with current step based on page change
|
||||||
await TaskRunner.continueCurrentStep();
|
await TaskRunner.continueCurrentStep();
|
||||||
}
|
}
|
||||||
@@ -695,7 +1275,7 @@
|
|||||||
TL.debug('OBSERVER', `Login state changed: ${oldValue} -> ${newValue}`);
|
TL.debug('OBSERVER', `Login state changed: ${oldValue} -> ${newValue}`);
|
||||||
|
|
||||||
// Handle login state change for current task
|
// Handle login state change for current task
|
||||||
if (data.current_task && data.current_step) {
|
if (data.current_task && data.bot_status === BOT_STATUS.PERFORMING_TASKS) {
|
||||||
await TaskRunner.continueCurrentStep();
|
await TaskRunner.continueCurrentStep();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -831,8 +1411,11 @@
|
|||||||
|
|
||||||
TL.log(`BAF`, res);
|
TL.log(`BAF`, res);
|
||||||
|
|
||||||
// Start initial task checking if ready
|
// Resume task if performing or start new task checking if ready
|
||||||
if (data.bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) {
|
if (data.bot_status === BOT_STATUS.PERFORMING_TASKS && data.current_task) {
|
||||||
|
TL.log('INIT', 'Resuming interrupted task');
|
||||||
|
TaskRunner.stepRunner.resumeTask();
|
||||||
|
} else if (data.bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) {
|
||||||
TaskRunner.checkForNewTasks();
|
TaskRunner.checkForNewTasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user