1425 lines
53 KiB
JavaScript
1425 lines
53 KiB
JavaScript
// ==UserScript==
|
|
// @name Binance Alpha Farm Agent
|
|
// @namespace http://baf.thuanle.me
|
|
// @version 2025.08.01
|
|
// @author TL
|
|
// @description Automated trading agent for Binance Alpha Farm
|
|
// @match https://www.binance.com/*
|
|
// @match https://accounts.binance.com/*
|
|
// @run-at document-idle
|
|
// @grant GM_setValue
|
|
// @grant GM_getValue
|
|
// @grant GM_registerMenuCommand
|
|
// @grant GM_notification
|
|
// @grant GM_xmlhttpRequest
|
|
// @grant GM_log
|
|
// @connect baf.thuanle.me
|
|
// @connect localhost
|
|
// @downloadURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
|
|
// @updateURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
|
|
// ==/UserScript==
|
|
|
|
(async () => {
|
|
'use strict';
|
|
|
|
// ====== CONFIGURATION ======
|
|
const CONFIG = {
|
|
heartbeat_interval: 10000,
|
|
task_poll_interval: 10000,
|
|
is_debug: true,
|
|
servers: {
|
|
local: { label: '🏠 Local', url: 'http://localhost:3000' },
|
|
prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' },
|
|
}
|
|
};
|
|
|
|
// ====== STATE MANAGEMENT ======
|
|
class AppState {
|
|
constructor() {
|
|
this.data = {
|
|
// Server configuration
|
|
server_mode: 'prod',
|
|
server: null,
|
|
|
|
// Page state
|
|
current_page: null,
|
|
is_logged_in: false,
|
|
|
|
// Bot status
|
|
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
|
|
|
// Task state
|
|
current_task: null,
|
|
task_data: null,
|
|
current_step: null,
|
|
step_data: null,
|
|
|
|
// UI state
|
|
is_loading: false,
|
|
error_message: null
|
|
};
|
|
|
|
this.observers = new Map();
|
|
this.initialized = false;
|
|
}
|
|
|
|
// Initialize state from storage and page detection
|
|
async initialize() {
|
|
if (this.initialized) return;
|
|
|
|
// Load from storage
|
|
this.data.server_mode = await STORAGE.getServerMode();
|
|
this.data.server = CONFIG.servers[this.data.server_mode] || CONFIG.servers.prod;
|
|
this.data.bot_status = await STORAGE.getBotStatus();
|
|
|
|
// Detect current page and login state
|
|
this.data.current_page = BINANCE.detectPage();
|
|
this.data.is_logged_in = await BINANCE.isLoggedIn();
|
|
|
|
this.initialized = true;
|
|
TL.debug('STATE', 'Initialized', this.data);
|
|
}
|
|
|
|
// Get current state
|
|
getData() {
|
|
return { ...this.data };
|
|
}
|
|
|
|
// Update state with partial update
|
|
async update(updates) {
|
|
const oldData = { ...this.data };
|
|
|
|
// Apply updates
|
|
Object.assign(this.data, updates);
|
|
|
|
// Notify observers
|
|
await this.notifyObservers(oldData, this.data);
|
|
|
|
TL.debug('STATE', 'Updated', updates);
|
|
}
|
|
|
|
// Subscribe to state changes
|
|
subscribe(key, callback) {
|
|
if (!this.observers.has(key)) {
|
|
this.observers.set(key, []);
|
|
}
|
|
this.observers.get(key).push(callback);
|
|
}
|
|
|
|
// Unsubscribe from state changes
|
|
unsubscribe(key, callback) {
|
|
const callbacks = this.observers.get(key);
|
|
if (callbacks) {
|
|
const index = callbacks.indexOf(callback);
|
|
if (index > -1) {
|
|
callbacks.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify all observers
|
|
async notifyObservers(oldData, newData) {
|
|
for (const [key, callbacks] of this.observers) {
|
|
const oldValue = oldData[key];
|
|
const newValue = newData[key];
|
|
|
|
if (oldValue !== newValue) {
|
|
for (const callback of callbacks) {
|
|
try {
|
|
await callback(oldValue, newValue, newData);
|
|
} catch (error) {
|
|
TL.error('STATE', `Observer error for ${key}:`, error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ====== STORAGE MODULE ======
|
|
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',
|
|
|
|
getToken: () => GM_getValue(STORAGE.key_token, ''),
|
|
setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
|
|
|
|
getServerMode: () => GM_getValue(STORAGE.key_server_mode, 'prod'),
|
|
setServerMode: (mode) => GM_setValue(STORAGE.key_server_mode, mode),
|
|
|
|
getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.READY_FOR_NEW_TASKS),
|
|
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 ======
|
|
const TL = {
|
|
debug: (tag, msg, ...args) => CONFIG.is_debug && GM_log(`[TL] [${tag}]\n${msg}`, ...args),
|
|
log: (tag, msg, ...args) => GM_log(`[TL] [${tag}]\n${msg}`, ...args),
|
|
error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${tag}] ❌\n${msg}`, ...args),
|
|
|
|
noti: (title, text, timeout = 2500) => {
|
|
if (typeof GM_notification === 'function') {
|
|
GM_notification({ title, text, timeout });
|
|
return true;
|
|
}
|
|
alert(`${title}\n${text}`);
|
|
return false;
|
|
},
|
|
|
|
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
|
|
TL.dom = {
|
|
isVisible: (el) => {
|
|
if (!el) return false;
|
|
const cs = getComputedStyle(el);
|
|
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
|
|
return (el.offsetWidth + el.offsetHeight) > 0;
|
|
},
|
|
|
|
isDisabled: (el) => {
|
|
return el?.disabled === true || el?.getAttribute('aria-disabled') === 'true';
|
|
},
|
|
|
|
isInViewport: (el) => {
|
|
if (!el) return false;
|
|
const rect = el.getBoundingClientRect();
|
|
return (
|
|
rect.top >= 0 &&
|
|
rect.left >= 0 &&
|
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
);
|
|
},
|
|
|
|
click: (el) => {
|
|
if (el && typeof el.click === 'function') el.click();
|
|
},
|
|
|
|
scrollToView: (el, behavior = 'smooth') => {
|
|
el?.scrollIntoView?.({ behavior, block: 'center' });
|
|
},
|
|
};
|
|
|
|
// Network helpers
|
|
TL.net = {
|
|
gmRequest(url, init = {}) {
|
|
const headersToObject = (h) => {
|
|
const o = {};
|
|
(h || new Headers()).forEach((v, k) => { o[k] = v; });
|
|
return o;
|
|
};
|
|
return new Promise((resolve, reject) => {
|
|
GM_xmlhttpRequest({
|
|
url,
|
|
method: (init.method || 'GET').toUpperCase(),
|
|
headers: headersToObject(init.headers),
|
|
data: init.body,
|
|
onload: (resp) => {
|
|
const text = resp.responseText || '';
|
|
let data = text;
|
|
const isJSON = /content-type:\s*application\/json/i.test(resp.responseHeaders || '');
|
|
if (isJSON) { try { data = JSON.parse(text); } catch { } }
|
|
TL.debug(`net`,`${init.method} ${resp.status} ${url}`, data);
|
|
resolve({
|
|
status: resp.status,
|
|
ok: resp.status >= 200 && resp.status < 300,
|
|
data,
|
|
rawText: text,
|
|
headers: null
|
|
});
|
|
},
|
|
onerror: reject,
|
|
ontimeout: () => reject(new Error('GM_xmlhttpRequest timeout')),
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
// ====== BAF API MODULE ======
|
|
const BAF = {
|
|
getServer: async () => {
|
|
const data = APP_STATE.getData();
|
|
return CONFIG.servers[data.server_mode] || CONFIG.servers.prod;
|
|
},
|
|
|
|
getHost: async () => {
|
|
const s = await BAF.getServer();
|
|
return s.url;
|
|
},
|
|
|
|
request: async (method, path, { params, body, headers } = {}) => {
|
|
const base = await BAF.getHost();
|
|
const url = new URL(path, base);
|
|
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.append(k, v);
|
|
|
|
const h = new Headers(headers || {});
|
|
const token = await STORAGE.getToken();
|
|
if (token && !h.has('Authorization')) h.set('Authorization', `Bearer ${token}`);
|
|
|
|
const init = { method, headers: h };
|
|
if (body !== undefined && body !== null) {
|
|
if (!(body instanceof FormData) && !h.has('Content-Type')) h.set('Content-Type', 'application/json');
|
|
init.body = (h.get('Content-Type') || '').includes('application/json') && typeof body !== 'string'
|
|
? JSON.stringify(body)
|
|
: body;
|
|
}
|
|
return TL.net.gmRequest(url.toString(), init);
|
|
},
|
|
|
|
get: (path, params, init = {}) => BAF.request('GET', path, { params, headers: init.headers }),
|
|
post: (path, body, init = {}) => BAF.request('POST', path, { body, headers: init.headers }),
|
|
|
|
ping: (status) => BAF.post('/agent/ping', status),
|
|
|
|
getTasks: () => BAF.get('/agent/tasks'),
|
|
submitTaskResult: (taskId, result) => BAF.post(`/agent/tasks/${taskId}/result`, result),
|
|
};
|
|
|
|
// ====== BINANCE MODULE ======
|
|
const BINANCE_PAGES = {
|
|
LOGIN: 'login',
|
|
ALPHA_SWAP: 'alpha-swap',
|
|
ALPHA_ORDER_HISTORY: 'alpha-order-history',
|
|
UNKNOWN: 'unknown'
|
|
};
|
|
|
|
const BINANCE = {
|
|
detectPage: () => {
|
|
const pathname = window.location.pathname;
|
|
const hostname = window.location.hostname;
|
|
|
|
if (hostname === 'accounts.binance.com') {
|
|
if (pathname.includes('/login')) {
|
|
return BINANCE_PAGES.LOGIN;
|
|
}
|
|
}
|
|
|
|
if (hostname === 'www.binance.com') {
|
|
if (pathname.includes('/alpha/bsc/')) {
|
|
return BINANCE_PAGES.ALPHA_SWAP;
|
|
}
|
|
if (pathname.includes('/my/orders/alpha/orderhistory')) {
|
|
return BINANCE_PAGES.ALPHA_ORDER_HISTORY;
|
|
}
|
|
}
|
|
|
|
return BINANCE_PAGES.UNKNOWN;
|
|
},
|
|
|
|
isOnLoginPage: () => BINANCE.detectPage() === BINANCE_PAGES.LOGIN,
|
|
isOnAlphaSwapPage: () => BINANCE.detectPage() === BINANCE_PAGES.ALPHA_SWAP,
|
|
isOnAlphaOrderHistoryPage: () => BINANCE.detectPage() === BINANCE_PAGES.ALPHA_ORDER_HISTORY,
|
|
|
|
detectLoginState: () => {
|
|
if (BINANCE.isOnLoginPage()) {
|
|
return false;
|
|
}
|
|
|
|
const loginBtn = document.querySelector('#toLoginPage');
|
|
const regBtn = document.querySelector('#toRegisterPage');
|
|
|
|
if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) return false;
|
|
if (!loginBtn && !regBtn) return true;
|
|
|
|
return null;
|
|
},
|
|
|
|
isLoggedIn: async (timeoutMs = 6000, pollMs = 200) => {
|
|
const deadline = Date.now() + timeoutMs;
|
|
TL.debug(`BINANCE`,`isLoggedIn: Checking login state...`);
|
|
while (Date.now() < deadline) {
|
|
const state = BINANCE.detectLoginState();
|
|
if (state !== null) {
|
|
return state;
|
|
}
|
|
TL.debug(`BINANCE`,`isLoggedIn: not found. Sleeping...`);
|
|
await TL.delay(pollMs);
|
|
}
|
|
|
|
const fallback = BINANCE.detectLoginState() ?? false;
|
|
TL.debug(`BINANCE`,`isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`);
|
|
return fallback;
|
|
},
|
|
|
|
navigateToLogin: () => {
|
|
const loginBtn = document.querySelector('#toLoginPage');
|
|
if (TL.dom.isVisible?.(loginBtn)) {
|
|
TL.dom.click(loginBtn);
|
|
}
|
|
},
|
|
|
|
navigateToAlphaSwap: () => {
|
|
if (!BINANCE.isOnAlphaSwapPage()) {
|
|
window.location.href = 'https://www.binance.com/alpha/bsc/';
|
|
}
|
|
},
|
|
|
|
navigateToAlphaOrderHistory: () => {
|
|
if (!BINANCE.isOnAlphaOrderHistoryPage()) {
|
|
window.location.href = 'https://www.binance.com/en/my/orders/alpha/orderhistory';
|
|
}
|
|
},
|
|
};
|
|
|
|
// ====== TASK & STEP SYSTEM ======
|
|
|
|
// Task Types Enum
|
|
const TASK_TYPES = {
|
|
LOGIN: 'login',
|
|
GET_ORDER_HISTORY: 'get_order_history',
|
|
GET_BALANCE: 'get_balance',
|
|
SWAP: 'swap',
|
|
NO_TASK: 'no_task'
|
|
};
|
|
|
|
// Bot Status Enum
|
|
const BOT_STATUS = {
|
|
READY_FOR_NEW_TASKS: 'ready-for-new-tasks',
|
|
PAUSE_AUTOMATION: 'pause-automation',
|
|
PERFORMING_TASKS: 'performing-tasks'
|
|
};
|
|
|
|
// Login Method Enum
|
|
const LOGIN_METHOD = {
|
|
QR_CODE: 'qr_code',
|
|
PASSWORD: 'password',
|
|
EMAIL: 'email'
|
|
};
|
|
|
|
// Order Type Enum
|
|
const ORDER_TYPE = {
|
|
SWAP: 'swap',
|
|
LIMIT: 'limit',
|
|
MARKET: 'market'
|
|
};
|
|
|
|
// Order Status Enum
|
|
const ORDER_STATUS = {
|
|
COMPLETED: 'completed',
|
|
PENDING: 'pending',
|
|
CANCELLED: 'cancelled'
|
|
};
|
|
|
|
// Balance Format Enum
|
|
const BALANCE_FORMAT = {
|
|
JSON: 'json',
|
|
CSV: 'csv'
|
|
};
|
|
|
|
// Gas Fee Type Enum
|
|
const GAS_FEE_TYPE = {
|
|
AUTO: 'auto',
|
|
FAST: 'fast',
|
|
SLOW: 'slow'
|
|
};
|
|
|
|
const STEP_TYPES = {
|
|
// Login task steps
|
|
NAVIGATE_TO_LOGIN: 'navigate_to_login',
|
|
SELECT_QR_CODE: 'select_qr_code',
|
|
SEND_TO_SERVER: 'send_to_server',
|
|
WAIT_FOR_LOGIN: 'wait_for_login',
|
|
REPORT_RESULT: 'report_result',
|
|
|
|
// Order history task steps
|
|
NAVIGATE_TO_ORDER_HISTORY: 'navigate_to_order_history',
|
|
EXTRACT_ORDER_DATA: 'extract_order_data',
|
|
SEND_ORDER_DATA: 'send_order_data',
|
|
|
|
// Balance task steps
|
|
NAVIGATE_TO_BALANCE: 'navigate_to_balance',
|
|
EXTRACT_BALANCE_DATA: 'extract_balance_data',
|
|
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'
|
|
};
|
|
|
|
// ====== 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 ======
|
|
const TaskRunner = {
|
|
stepRunner: new StepRunner(),
|
|
|
|
// Helper function to validate enum values
|
|
validateEnum(value, enumObj, defaultValue) {
|
|
const validValues = Object.values(enumObj);
|
|
if (validValues.includes(value)) {
|
|
return value;
|
|
}
|
|
TL.debug('TASK', `Invalid enum value: ${value}, using default: ${defaultValue}`);
|
|
return defaultValue;
|
|
},
|
|
|
|
// Helper function to validate and merge task data with defaults
|
|
validateTaskData(taskType, taskData = {}) {
|
|
const mergedData = { ...taskData };
|
|
|
|
// Validate enum fields if present
|
|
if (taskType === TASK_TYPES.LOGIN && mergedData.method) {
|
|
mergedData.method = this.validateEnum(mergedData.method, LOGIN_METHOD, LOGIN_METHOD.QR_CODE);
|
|
}
|
|
|
|
if (taskType === TASK_TYPES.GET_ORDER_HISTORY) {
|
|
if (mergedData.order_types) {
|
|
mergedData.order_types = mergedData.order_types.map(type =>
|
|
this.validateEnum(type, ORDER_TYPE, ORDER_TYPE.SWAP)
|
|
);
|
|
}
|
|
if (mergedData.status) {
|
|
mergedData.status = mergedData.status.map(status =>
|
|
this.validateEnum(status, ORDER_STATUS, ORDER_STATUS.COMPLETED)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (taskType === TASK_TYPES.GET_BALANCE && mergedData.format) {
|
|
mergedData.format = this.validateEnum(mergedData.format, BALANCE_FORMAT, BALANCE_FORMAT.JSON);
|
|
}
|
|
|
|
if (taskType === TASK_TYPES.SWAP && mergedData.gas_fee && typeof mergedData.gas_fee === 'string') {
|
|
mergedData.gas_fee = this.validateEnum(mergedData.gas_fee, GAS_FEE_TYPE, GAS_FEE_TYPE.AUTO);
|
|
}
|
|
|
|
TL.debug('TASK', `Validated task data for ${taskType}:`, mergedData);
|
|
return mergedData;
|
|
},
|
|
|
|
// Helper function to get current task data
|
|
getCurrentTaskData() {
|
|
const data = APP_STATE.getData();
|
|
return data.task_data || {};
|
|
},
|
|
|
|
// Helper function to update task data
|
|
async updateTaskData(newData) {
|
|
const currentData = this.getCurrentTaskData();
|
|
const updatedData = { ...currentData, ...newData };
|
|
await APP_STATE.update({ task_data: updatedData });
|
|
return updatedData;
|
|
},
|
|
|
|
// Helper function to check if current task is of specific type
|
|
isCurrentTaskType(taskType) {
|
|
const data = APP_STATE.getData();
|
|
return data.current_task?.type === taskType;
|
|
},
|
|
|
|
// Helper function to get current task type
|
|
getCurrentTaskType() {
|
|
const data = APP_STATE.getData();
|
|
return data.current_task?.type || null;
|
|
},
|
|
async checkForNewTasks() {
|
|
try {
|
|
const data = APP_STATE.getData();
|
|
if (data.bot_status !== BOT_STATUS.READY_FOR_NEW_TASKS) {
|
|
TL.debug('TASK', 'Bot not ready for new tasks, status:', data.bot_status);
|
|
return;
|
|
}
|
|
|
|
const response = await BAF.getTasks();
|
|
if (response.ok && response.data.task) {
|
|
const task = response.data.task;
|
|
TL.log('TASK', `Received new task: ${task.type}`, task);
|
|
|
|
// Validate task data
|
|
const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data);
|
|
|
|
// Start task using StepRunner
|
|
await this.stepRunner.startTask(task, validatedTaskData);
|
|
} else if (response.ok && response.data.no_task) {
|
|
TL.debug('TASK', 'No new tasks available');
|
|
// Sleep for interval then check again
|
|
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
|
|
}
|
|
} catch (error) {
|
|
TL.error('TASK', 'Failed to check for new tasks:', error);
|
|
// Retry after interval
|
|
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
|
|
}
|
|
},
|
|
|
|
// Continue current step (called by observers)
|
|
async continueCurrentStep() {
|
|
await this.stepRunner.executeCurrentStep();
|
|
}
|
|
};
|
|
|
|
// ====== STATE OBSERVERS ======
|
|
const StateWatchers = {
|
|
// Storage observer - saves state changes to storage
|
|
async onServerModeChange(oldValue, newValue, data) {
|
|
TL.debug('OBSERVER', `Server mode changed: ${oldValue} -> ${newValue}`);
|
|
await STORAGE.setServerMode(newValue);
|
|
|
|
// Update server info
|
|
await APP_STATE.update({
|
|
server: CONFIG.servers[newValue] || CONFIG.servers.prod
|
|
});
|
|
|
|
// Reload page after a short delay
|
|
setTimeout(() => location.reload(), 500);
|
|
},
|
|
|
|
async onBotStatusChange(oldValue, newValue, data) {
|
|
TL.debug('OBSERVER', `Bot status changed: ${oldValue} -> ${newValue}`);
|
|
await STORAGE.setBotStatus(newValue);
|
|
|
|
// Handle status-specific logic
|
|
if (newValue === BOT_STATUS.READY_FOR_NEW_TASKS) {
|
|
// Start checking for new tasks
|
|
TaskRunner.checkForNewTasks();
|
|
}
|
|
},
|
|
|
|
async onCurrentPageChange(oldValue, newValue, data) {
|
|
TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`);
|
|
|
|
// Handle page-specific logic for current task
|
|
if (data.current_task && data.bot_status === BOT_STATUS.PERFORMING_TASKS) {
|
|
// Continue with current step based on page change
|
|
await TaskRunner.continueCurrentStep();
|
|
}
|
|
},
|
|
|
|
async onLoginStateChange(oldValue, newValue, data) {
|
|
TL.debug('OBSERVER', `Login state changed: ${oldValue} -> ${newValue}`);
|
|
|
|
// Handle login state change for current task
|
|
if (data.current_task && data.bot_status === BOT_STATUS.PERFORMING_TASKS) {
|
|
await TaskRunner.continueCurrentStep();
|
|
}
|
|
}
|
|
};
|
|
|
|
// ====== MENU SYSTEM ======
|
|
async function createGM_Menu() {
|
|
const data = APP_STATE.getData();
|
|
const curSrv = data.server;
|
|
|
|
GM_registerMenuCommand(`Server: ${curSrv.label} (${curSrv.url})`, async () => {
|
|
try {
|
|
const next = (data.server_mode === 'local') ? 'prod' : 'local';
|
|
await APP_STATE.update({ server_mode: next });
|
|
|
|
const nsv = CONFIG.servers[next];
|
|
const msg = `Switched to ${nsv.label} (${nsv.url})`;
|
|
TL.debug(`BAF`, msg);
|
|
TL.noti('BAF Server Switched', msg);
|
|
} catch (e) {
|
|
TL.error('BAF', 'switch server error', e);
|
|
TL.noti('BAF Server Switched', `Switch server error: ${e.message}`);
|
|
}
|
|
});
|
|
|
|
GM_registerMenuCommand('Token', async () => {
|
|
try {
|
|
const curToken = await STORAGE.getToken();
|
|
const input = prompt('Bearer token:', curToken || '');
|
|
if (input !== null && input.trim() && input.trim() !== curToken) {
|
|
await STORAGE.setToken(input);
|
|
}
|
|
const s = await BAF.getServer();
|
|
const res = await BAF.ping();
|
|
const resStr =
|
|
`Server: ${s.label} (${s.url})\n` +
|
|
`Status: ${res.ok ? 'Connected ✅' : 'Failed ❌'} (${res.status})`;
|
|
TL.debug(`BAF`, resStr);
|
|
TL.noti('BAF Server', resStr);
|
|
} catch (e) {
|
|
const resStr = `ping error: ${e.message}`;
|
|
TL.error('BAF', resStr);
|
|
TL.noti('BAF Server', resStr);
|
|
}
|
|
});
|
|
|
|
GM_registerMenuCommand('Bot Status', async () => {
|
|
const data = APP_STATE.getData();
|
|
const statusOptions = [
|
|
BOT_STATUS.READY_FOR_NEW_TASKS,
|
|
BOT_STATUS.PAUSE_AUTOMATION,
|
|
BOT_STATUS.PERFORMING_TASKS
|
|
];
|
|
const currentIndex = statusOptions.indexOf(data.bot_status);
|
|
const nextIndex = (currentIndex + 1) % statusOptions.length;
|
|
const nextStatus = statusOptions[nextIndex];
|
|
|
|
await APP_STATE.update({ bot_status: nextStatus });
|
|
TL.noti('Bot Status', `Changed to: ${nextStatus}`);
|
|
});
|
|
|
|
GM_registerMenuCommand('Get Tasks', async () => {
|
|
await TaskRunner.checkForNewTasks();
|
|
});
|
|
}
|
|
|
|
// ====== HEARTBEAT ======
|
|
async function heartbeat_report() {
|
|
try {
|
|
const data = APP_STATE.getData();
|
|
const status = {
|
|
logged_in: data.is_logged_in,
|
|
current_page: data.current_page,
|
|
bot_status: data.bot_status,
|
|
current_task: data.current_task?.type || null,
|
|
task_data: data.task_data || null,
|
|
current_step: data.current_step || null,
|
|
has_current_task: !!data.current_task
|
|
};
|
|
|
|
TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`);
|
|
await BAF.ping(status);
|
|
return status;
|
|
} catch (e) {
|
|
TL.error('HEARTBEAT', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ====== PAGE MONITOR ======
|
|
async function monitorPageChanges() {
|
|
let lastPage = APP_STATE.getData().current_page;
|
|
|
|
setInterval(async () => {
|
|
const currentPage = BINANCE.detectPage();
|
|
if (currentPage !== lastPage) {
|
|
await APP_STATE.update({ current_page: currentPage });
|
|
lastPage = currentPage;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
// ====== INITIALIZATION ======
|
|
const APP_STATE = new AppState();
|
|
|
|
async function initialize() {
|
|
// Initialize state
|
|
await APP_STATE.initialize();
|
|
|
|
// Register observers
|
|
APP_STATE.subscribe('server_mode', StateWatchers.onServerModeChange);
|
|
APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange);
|
|
APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange);
|
|
APP_STATE.subscribe('is_logged_in', StateWatchers.onLoginStateChange);
|
|
|
|
// Create menu
|
|
await createGM_Menu();
|
|
|
|
// Start monitoring
|
|
monitorPageChanges();
|
|
|
|
// Start heartbeat
|
|
setInterval(heartbeat_report, CONFIG.heartbeat_interval);
|
|
|
|
// Welcome message
|
|
const ping_res = await BAF.ping();
|
|
const data = APP_STATE.getData();
|
|
const res =
|
|
`Server: ${data.server.label} (${data.server.url})\n` +
|
|
`Status: ${ping_res.ok ? 'Connected ✅' : 'Failed ❌'} (${ping_res.status})\n` +
|
|
`Page: ${data.current_page || 'unknown'} (${window.location.href})\n` +
|
|
`Bot Status: ${data.bot_status}`;
|
|
|
|
TL.log(`BAF`, res);
|
|
|
|
// Resume task if performing or start new task checking if ready
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Start the application
|
|
await initialize();
|
|
})(); |