Files
binance-alpha-farm-agent/agent.user.js

1843 lines
70 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==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==
if (window.top !== window) {
GM_log(`[TL] ❌ Skipping in iframe ${window.location.href}`);
return;
}
GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.');
(async () => {
'use strict';
// ====== CONFIGURATION ======
const CONFIG = {
heartbeat_interval: 10000,
task_poll_interval: 10000,
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_type: 'prod',
server_url: null,
server_last_seen: null,
// Page state
current_page: null,
is_logged_in: false,
// Bot status
bot_status: BOT_STATUS.WAITING,
// Task state
current_task: null,
task_data: null,
current_step: null,
step_data: null,
// UI state
is_loading: false,
error_message: null,
// Popup state
popup_position: 4, // 1: top-left, 2: top-right, 3: bottom-left, 4: bottom-right
popup_visible: true,
// Debug state
is_debug: true
};
this.observers = new Map();
this.initialized = false;
}
// Initialize state from storage and page detection
async initialize() {
if (this.initialized) return;
this.initialized = true;
// Load from storage
this.data.server_type = await STORAGE.getServerType();
this.data.server_url = CONFIG.servers[this.data.server_type] || CONFIG.servers.prod;
this.data.bot_status = await STORAGE.getBotStatus();
this.data.popup_position = await STORAGE.getPopupPosition();
this.data.popup_visible = await STORAGE.getPopupVisible();
this.data.is_debug = await STORAGE.getDebug();
// Detect current page and login state
this.data.current_page = BINANCE.detectPage();
this.data.is_logged_in = await BINANCE.isLoggedIn();
TL.debug('STATE', 'Initialized', this.data);
}
getData() {
return { ...this.data };
}
// Update state with partial update
async update(updates) {
const oldData = { ...this.data };
Object.assign(this.data, updates);
await this.notifyObservers(oldData, this.data);
TL.debug('STATE', 'Updated', updates);
}
subscribe(key, callback) {
if (!this.observers.has(key)) {
this.observers.set(key, []);
}
this.observers.get(key).push(callback);
}
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_type: 'baf-server-type',
key_bot_status: 'baf-bot-status',
key_navigation_state: 'baf-navigation-state',
key_popup_position: 'baf-popup-position',
key_popup_visible: 'baf-popup-visible',
key_debug: 'baf-debug',
getToken: () => GM_getValue(STORAGE.key_token, ''),
setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
getServerType: () => GM_getValue(STORAGE.key_server_type, 'prod'),
setServerType: (type) => GM_setValue(STORAGE.key_server_type, type),
getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.WAITING),
setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status),
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');
},
getPopupPosition: () => GM_getValue(STORAGE.key_popup_position, 4),
setPopupPosition: (position) => GM_setValue(STORAGE.key_popup_position, position),
getPopupVisible: () => GM_getValue(STORAGE.key_popup_visible, true),
setPopupVisible: (visible) => GM_setValue(STORAGE.key_popup_visible, visible),
getDebug: () => GM_getValue(STORAGE.key_debug, true),
setDebug: (debug) => GM_setValue(STORAGE.key_debug, debug)
};
// ====== UTILITY MODULE ======
const TL = {
debug: (tag, msg, ...args) => APP_STATE.getData().is_debug && GM_log(`[TL] [${tag}]\n${msg}`, ...args),
log: (tag, msg, ...args) => GM_log(`[TL] [${tag}]\n${msg}`, ...args),
warn: (tag, msg, ...args) => GM_log(`[TL] [WARN] [${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}\n${init.body}`, 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_type] || 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),
getTask: (status) => BAF.get('/agent/task', status),
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',
WALLET_EARN: 'wallet-earn',
WALLET_FUNDING: 'wallet-funding',
WALLET_SPOT: 'wallet-spot',
IGNORE: 'ignore',
UNKNOWN: 'unknown'
};
const BINANCE_PAGE_PATTERNS = [
{
host: 'accounts.binance.com',
patterns: [
{ includes: '/login', page: BINANCE_PAGES.LOGIN }
]
},
{
host: 'www.binance.com',
patterns: [
{ includes: '/alpha/bsc/', page: BINANCE_PAGES.ALPHA_SWAP },
{ includes: '/my/orders/alpha/orderhistory', page: BINANCE_PAGES.ALPHA_ORDER_HISTORY },
{ includes: '/my/wallet/account/earn', page: BINANCE_PAGES.WALLET_EARN },
{ includes: '/my/wallet/funding', page: BINANCE_PAGES.WALLET_FUNDING },
{ includes: '/my/wallet/account/main', page: BINANCE_PAGES.WALLET_SPOT }
]
}
];
const BINANCE = {
detectPage: () => {
if (!SafetyGuard.check('detectPage')) return BINANCE_PAGES.IGNORE;
const { hostname, pathname } = window.location;
if (window.top !== window) {
TL.debug('BINANCE-PAGE-DETECT', 'Call from iframe ❌');
return BINANCE_PAGES.IGNORE;
}
const hostConfig = BINANCE_PAGE_PATTERNS.find(cfg => cfg.host === hostname);
if (!hostConfig) {
return BINANCE_PAGES.UNKNOWN;
}
// Log current URL details
const msg = `Current URL: ${window.location.href}\n` +
`Hostname: ${hostname}, Pathname: ${pathname}\n` +
`window.top === window : ${window.top === window}\n`;
// Check patterns in order (most specific first)
for (const pattern of hostConfig.patterns) {
if (pathname.includes(pattern.includes)) {
TL.debug('PAGE_DETECT', msg +
`Matched pattern: ${pattern.includes} -> ${pattern.page}`);
return pattern.page;
}
}
TL.debug('PAGE_DETECT', msg + `No pattern matched: ${hostname}${pathname}`);
return BINANCE_PAGES.UNKNOWN;
},
isOnLoginPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.LOGIN,
isOnAlphaSwapPage: (contractAddress) => {
const page = APP_STATE.getData().current_page;
return page === BINANCE_PAGES.ALPHA_SWAP && (!contractAddress || window.location.pathname.includes(contractAddress));
},
isOnAlphaOrderHistoryPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.ALPHA_ORDER_HISTORY,
isOnWalletEarnPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_EARN,
isOnWalletFundingPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_FUNDING,
isOnWalletSpotPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_SPOT,
detectLoginState: () => {
if (!SafetyGuard.check('detectLoginState')) return null;
if (window.top !== window) {
TL.debug('BINANCE-LOGIN', 'In iframe - cannot determine login state');
return null;
}
if (BINANCE.isOnLoginPage()) {
return false;
}
// otherwise
// if (BINANCE.isOnAlphaSwapPage()) {
// Method 1: Check for login/register buttons (indicates NOT logged in)
const loginBtn = document.querySelector('#toLoginPage, [data-testid="login-button"], .login-btn');
const regBtn = document.querySelector('#toRegisterPage, [data-testid="register-button"], .register-btn');
let msg = `loginBtn: ${TL.dom.isVisible?.(loginBtn)} regBtn: ${TL.dom.isVisible?.(regBtn)}\n`;
// If login/register buttons are visible, definitely not logged in
if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) {
TL.debug('BINANCE-LOGIN', msg + 'Login/Register buttons visible -> NOT logged in');
return false;
}
// Method 2: Check for user account elements (indicates logged in)
const dashboardLink = document.querySelector('a[href*="/my/dashboard"]');
const accountIcon = document.querySelector('.header-account-icon');
const walletBtn = document.querySelector('#ba-wallet');
const isDashboardVisible = dashboardLink && TL.dom.isVisible?.(dashboardLink);
const isAccountIconVisible = !!accountIcon;
const isWalletVisible = walletBtn && TL.dom.isVisible?.(walletBtn);
// msg += `dashboard: ${dashboardLink}, accountIcon: ${accountIcon}, walletBtn: ${walletBtn}\n`;
msg += `Visible: dashboard: ${isDashboardVisible ? '✓' : '✗'}, accountIcon: ${isAccountIconVisible ? '✓' : '✗'}, walletBtn: ${isWalletVisible ? '✓' : '✗'}\n`;
// If we see user account elements, likely logged in
if (isDashboardVisible && isAccountIconVisible && isWalletVisible) {
TL.debug('BINANCE-LOGIN', msg + 'User account elements found -> LIKELY logged in');
return true;
}
TL.debug('BINANCE-LOGIN', msg + 'Cannot determine login state - returning null');
return null;
},
isLoggedIn: async (timeoutMs = 30000, pollMs = 500) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const state = BINANCE.detectLoginState();
if (state !== null) {
TL.debug(`BINANCE-LOGIN`, `isLoggedIn: ${state ? 'true' : 'false'}`);
return state;
}
TL.debug(`BINANCE-LOGIN`, `isLoggedIn: not found. Sleeping...`);
await TL.delay(pollMs);
}
const fallback = BINANCE.detectLoginState() ?? false;
TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`);
return fallback;
},
navigateToLogin: () => {
const loginBtn = document.querySelector('#toLoginPage');
if (TL.dom.isVisible?.(loginBtn)) {
TL.dom.click(loginBtn);
}
},
navigateToAlphaSwap: (contractAddress) => {
if (BINANCE.isOnAlphaSwapPage(contractAddress)) return;
window.location.href = contractAddress
? `https://www.binance.com/en/alpha/bsc/${contractAddress}`
: `https://www.binance.com/en/alpha`;
},
navigateToAlphaOrderHistory: () => {
if (!BINANCE.isOnAlphaOrderHistoryPage()) {
window.location.href = 'https://www.binance.com/en/my/orders/alpha/orderhistory';
}
},
navigateToWalletEarn: () => {
if (!BINANCE.isOnWalletEarnPage()) {
window.location.href = 'https://www.binance.com/en/my/wallet/account/earn';
}
},
navigateToWalletFunding: () => {
if (!BINANCE.isOnWalletFundingPage()) {
window.location.href = 'https://www.binance.com/en/my/wallet/funding';
}
},
navigateToWalletSpot: () => {
if (!BINANCE.isOnWalletSpotPage()) {
window.location.href = 'https://www.binance.com/en/my/wallet/account/main';
}
}
};
// ====== 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 = {
WAITING: 'waiting', // 💤 waiting for new task
RUNNING: 'running', // ▶️ đang chạy task
PAUSED: 'paused', // ⏸️ đang pause
STOPPED: 'stopped' // ⏹️ đã stop
};
// Login Method Enum
const LOGIN_METHOD = {
QR_CODE: 'qr_code',
};
// 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;
// // // Only navigate to login if:
// // // 1. Already on login page, OR
// // // 2. Not logged in AND on a page that doesn't require login
// // if (isOnLoginPage) {
// // return true;
// // }
// // if (!isLoggedIn) {
// // // Check if we're on a page that requires login
// // // const currentPage = BINANCE.detectPage();
// // const requiresLoginPages = [
// // BINANCE_PAGES.ALPHA_SWAP,
// // BINANCE_PAGES.ALPHA_ORDER_HISTORY,
// // BINANCE_PAGES.WALLET_EARN,
// // BINANCE_PAGES.WALLET_FUNDING,
// // BINANCE_PAGES.WALLET_SPOT
// // ];
// // // If we're on a page that requires login but not logged in,
// // // don't navigate to login (let the page handle it)
// // if (requiresLoginPages.includes(currentPage)) {
// // return false;
// // }
// // // Only navigate to login if on a public page
// return true;
// }
return false;
},
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) => {
// is
}
};
// Order History Step Functions
const OrderHistorySteps = {
//TODO: Implement
};
// Balance Step Functions
const BalanceSteps = {
//TODO: Implement
};
// ====== TASK DEFINITIONS ======
const TASK_DEFINITIONS = {
[TASK_TYPES.LOGIN]: {
id: TASK_TYPES.LOGIN,
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: TASK_TYPES.GET_ORDER_HISTORY,
steps: [
//TODO: Implement
]
},
[TASK_TYPES.GET_BALANCE]: {
id: TASK_TYPES.GET_BALANCE,
steps: [
//TODO: Implement
]
},
[TASK_TYPES.SWAP]: {
id: TASK_TYPES.SWAP,
steps: [
//TODO: Implement
]
},
[TASK_TYPES.NO_TASK]: {
id: TASK_TYPES.NO_TASK,
steps: [
//TODO: Implement
]
}
};
// ====== 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 waiting state
await APP_STATE.update({
current_task: null,
task_data: null,
current_step: null,
step_data: null,
bot_status: BOT_STATUS.WAITING,
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.WAITING,
is_loading: false,
error_message: 'Task completion error: ' + error.message
});
}
}
// Execute current step
async executeCurrentStep() {
if (!SafetyGuard.check('executeCurrentStep')) return;
const data = APP_STATE.getData();
if (!data.current_task || !this.context) {
TL.debug('STEP', 'No current task or context available');
return;
}
// Check if bot is paused or stopped
if (data.bot_status === BOT_STATUS.PAUSED) {
TL.debug('STEP', 'Bot is paused - skipping step execution');
return;
}
if (data.bot_status === BOT_STATUS.STOPPED) {
TL.debug('STEP', 'Bot is stopped - skipping step execution');
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.RUNNING,
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() {
if (!SafetyGuard.check('checkForNewTasks')) return;
try {
const data = APP_STATE.getData();
if (data.bot_status !== BOT_STATUS.WAITING && data.bot_status !== BOT_STATUS.RUNNING) {
TL.debug('TASK', 'Bot not ready for new tasks, status:', data.bot_status);
return;
}
const response = await BAF.getTask();
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 onServerTypeChange(oldValue, newValue, data) {
TL.debug('OBSERVER', `Server mode changed: ${oldValue} -> ${newValue}`);
await STORAGE.setServerType(newValue);
// Update server info
await APP_STATE.update({
server_url: 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.WAITING) {
// Start checking for new tasks
TaskRunner.checkForNewTasks();
} else if (newValue === BOT_STATUS.RUNNING) {
// Continue with current task or start new task checking
if (data.current_task) {
// Continue current task
TaskRunner.continueCurrentStep();
} else {
// Start checking for new tasks
TaskRunner.checkForNewTasks();
}
} else if (newValue === BOT_STATUS.PAUSED) {
// Pause current operations
TL.debug('OBSERVER', 'Bot paused - stopping task operations');
} else if (newValue === BOT_STATUS.STOPPED) {
// Stop all operations
TL.debug('OBSERVER', 'Bot stopped - clearing current task');
await APP_STATE.update({
current_task: null,
task_data: null,
current_step: null,
step_data: null,
is_loading: false,
error_message: null
});
}
// Refresh menu to show new status
setTimeout(() => createGM_Menu(), 100);
},
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.RUNNING) {
// Continue with current step based on page change
await TaskRunner.continueCurrentStep();
}
},
// Popup observers
async onPopupPositionChange(oldValue, newValue, data) {
TL.debug('OBSERVER', `Popup position changed: ${oldValue} -> ${newValue}`);
UI.statusOverlay.updatePosition();
},
async onPopupVisibleChange(oldValue, newValue, data) {
TL.debug('OBSERVER', `Popup visibility changed: ${oldValue} -> ${newValue}`);
if (newValue) {
UI.statusOverlay.show();
} else {
UI.statusOverlay.hide();
}
},
// General state change observer for popup content updates
async onAnyStateChange(oldValue, newValue, data) {
// Update popup content when any relevant state changes
if (UI.statusOverlay.element && data.popup_visible) {
UI.statusOverlay.updateContent();
}
}
};
// ====== MENU SYSTEM ======
let menuCreated = false;
async function createGM_Menu() {
if (menuCreated) {
// Clear existing menu items if recreating
// Note: GM_registerMenuCommand doesn't have a clear method,
// so we'll just set a flag to prevent recreation
return;
}
menuCreated = true;
GM_registerMenuCommand('🔍 Safety Guard Status', () => {
const status = SafetyGuard.getStatus();
alert(`Safety Guard Status:\n\n${status.message}\nEnabled: ${status.enabled}`);
});
GM_registerMenuCommand(' - ✅ Enable Operations', () => {
SafetyGuard.enable();
});
GM_registerMenuCommand(' - 🚨 Block Operations', () => {
SafetyGuard.disable();
});
const data = APP_STATE.getData();
const curSrv = data.server_url;
GM_registerMenuCommand(`🌐 Server: ${curSrv.label} (${curSrv.url})`, async () => {
try {
const next = (data.server_type === 'local') ? 'prod' : 'local';
await APP_STATE.update({ server_type: 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);
}
});
// Popup toggle menu
GM_registerMenuCommand("👁️ Toggle Popup", () => {
UI.statusOverlay.toggle();
});
}
// ====== HEARTBEAT ======
async function heartbeat_report() {
if (!SafetyGuard.check('heartbeat_report')) return null;
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,
};
// TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`);
const res = await BAF.ping(status);
if (res.ok) {
APP_STATE.update({ server_last_seen: new Date() });
}
return status;
} catch (e) {
TL.error('HEARTBEAT', e.message);
return null;
}
}
// ====== UI SYSTEM ======
const UI = {
// ====== STATUS OVERLAY ======
statusOverlay: {
element: null,
// Get position CSS based on position number
getPositionCSS: (position) => {
switch (position) {
case 1: return { top: '10px', left: '10px', bottom: 'auto', right: 'auto' }; // top-left
case 2: return { top: '10px', right: '10px', bottom: 'auto', left: 'auto' }; // top-right
case 3: return { bottom: '10px', left: '10px', top: 'auto', right: 'auto' }; // bottom-left
case 4: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' }; // bottom-right
default: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' };
}
},
// Create status overlay element
create: () => {
if (UI.statusOverlay.element) {
document.body.removeChild(UI.statusOverlay.element);
}
const overlay = document.createElement('div');
overlay.id = 'baf-status-overlay';
overlay.style.cssText = `
position: fixed;
z-index: 999999;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
line-height: 1.4;
min-width: 200px;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
user-select: none;
`;
// Add position toggle button
const positionBtn = document.createElement('div');
positionBtn.style.cssText = `
position: absolute;
top: -8px;
right: -8px;
width: 20px;
height: 20px;
background: #007bff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 10px;
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
`;
positionBtn.innerHTML = '⇄';
positionBtn.title = 'Change position';
positionBtn.addEventListener('click', () => {
const data = APP_STATE.getData();
const newPosition = (data.popup_position % 4) + 1;
APP_STATE.update({ popup_position: newPosition });
STORAGE.setPopupPosition(newPosition);
UI.statusOverlay.updatePosition();
});
positionBtn.addEventListener('mouseenter', () => {
positionBtn.style.transform = 'scale(1.5)';
positionBtn.style.background = '#0056b3';
});
positionBtn.addEventListener('mouseleave', () => {
positionBtn.style.transform = 'scale(1)';
positionBtn.style.background = '#007bff';
});
overlay.appendChild(positionBtn);
UI.statusOverlay.element = overlay;
document.body.appendChild(overlay);
return overlay;
},
// Update overlay position
updatePosition: () => {
if (!UI.statusOverlay.element) return;
const data = APP_STATE.getData();
const positionCSS = UI.statusOverlay.getPositionCSS(data.popup_position);
Object.assign(UI.statusOverlay.element.style, positionCSS);
},
// Update overlay content
updateContent: () => {
const data = APP_STATE.getData();
const taskName = data.current_task?.type || 'No task';
const stepName = data.current_step || 'No step';
// Get status display
let statusDisplay = '';
switch (data.bot_status) {
case BOT_STATUS.WAITING:
statusDisplay = '💤 Waiting';
break;
case BOT_STATUS.RUNNING:
statusDisplay = '▶️ Running';
break;
case BOT_STATUS.PAUSED:
statusDisplay = '⏸️ Paused';
break;
case BOT_STATUS.STOPPED:
statusDisplay = '⏹️ Stopped';
break;
default:
statusDisplay = '❓ Unknown';
}
// Get server info
const serverLabel = data.server_url?.label || 'Unknown';
// data.server_last_seen < 1min == connected
const serverConnected = data.server_last_seen && new Date() - data.server_last_seen < 60000 ? '🟢' : '🔴';
// Get page info
const pageDisplay = data.current_page || 'unknown';
// Get login status
const loginStatus = data.is_logged_in ? '✅' : '❌';
// Get SafetyGuard status
const safetyStatus = SafetyGuard.getStatus();
// Build content
const content = `
<div style="margin-bottom: 8px;"><strong>BAF Agent Status</strong></div>
<div style="margin-bottom: 4px;">
<span id="safety-toggle-btn" style="color: ${safetyStatus.enabled ? '#28a745' : '#dc3545'}; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid ${safetyStatus.enabled ? '#28a745' : '#dc3545'}; background: ${safetyStatus.enabled ? 'rgba(40, 167, 69, 0.1)' : 'rgba(220, 53, 69, 0.1)'}; transition: all 0.2s ease;" title="Click to toggle safety status">🛟 Safety: ${safetyStatus.enabled ? '🛡️ Safe' : '🚨 Blocked'}</span>
</div>
<div style="margin-bottom: 4px;">
<span id="debug-toggle-btn" style="color: ${data.is_debug ? '#17a2b8' : '#6c757d'}; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid ${data.is_debug ? '#17a2b8' : '#6c757d'}; background: ${data.is_debug ? 'rgba(23, 162, 184, 0.1)' : 'rgba(108, 117, 125, 0.1)'}; transition: all 0.2s ease;" title="Click to toggle debug mode">🐛 Debug: ${data.is_debug ? '✔️ ON' : '❌ OFF'}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #007bff;">Bot Status: ${statusDisplay}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #e83e8c;">🌐 Server: ${serverLabel} ${serverConnected}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #fd7e14;">👤 Login:${loginStatus}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #ffc107;">Task: ${taskName}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #17a2b8;">Step: ${stepName}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #6f42c1;">Page: ${pageDisplay}</span>
</div>
${data.error_message ? `<div style="color: #dc3545; margin-top: 4px;">Error: ${data.error_message}</div>` : ''}
`;
// Update content (excluding position button)
const contentDiv = UI.statusOverlay.element.querySelector('.baf-content') || document.createElement('div');
contentDiv.className = 'baf-content';
contentDiv.innerHTML = content;
if (!UI.statusOverlay.element.querySelector('.baf-content')) {
UI.statusOverlay.element.appendChild(contentDiv);
}
// Add event listener for safety toggle button
const toggleBtn = contentDiv.querySelector('#safety-toggle-btn');
if (toggleBtn) {
toggleBtn.onclick = () => {
const currentStatus = SafetyGuard.getStatus();
if (currentStatus.enabled) {
SafetyGuard.disable();
} else {
SafetyGuard.enable();
}
// Update the content to reflect the change
UI.statusOverlay.updateContent();
};
}
// Add event listener for debug toggle button
const debugToggleBtn = contentDiv.querySelector('#debug-toggle-btn');
if (debugToggleBtn) {
debugToggleBtn.onclick = () => {
const currentData = APP_STATE.getData();
const newDebugState = !currentData.is_debug;
APP_STATE.update({ is_debug: newDebugState });
STORAGE.setDebug(newDebugState);
// Update the content to reflect the change
UI.statusOverlay.updateContent();
};
}
},
// Show status overlay
show: () => {
if (!UI.statusOverlay.element) {
UI.statusOverlay.create();
}
UI.statusOverlay.updatePosition();
UI.statusOverlay.updateContent();
UI.statusOverlay.element.style.display = 'block';
},
// Hide status overlay
hide: () => {
if (UI.statusOverlay.element) {
UI.statusOverlay.element.style.display = 'none';
}
},
// Toggle status overlay
toggle: () => {
const data = APP_STATE.getData();
if (data.popup_visible) {
UI.statusOverlay.hide();
APP_STATE.update({ popup_visible: false });
STORAGE.setPopupVisible(false);
} else {
UI.statusOverlay.show();
APP_STATE.update({ popup_visible: true });
STORAGE.setPopupVisible(true);
}
},
// Initialize status overlay
init: () => {
const data = APP_STATE.getData();
if (data.popup_visible) {
UI.statusOverlay.show();
}
}
},
// ====== NOTIFICATION SYSTEM ======
notification: {
// Show success notification
success: (title, message, duration = 3000) => {
TL.noti(title, message, duration);
},
// Show error notification
error: (title, message, duration = 5000) => {
TL.noti(`${title}`, message, duration);
},
// Show warning notification
warning: (title, message, duration = 4000) => {
TL.noti(`⚠️ ${title}`, message, duration);
},
// Show info notification
info: (title, message, duration = 3000) => {
TL.noti(` ${title}`, message, duration);
}
},
// ====== LOADING INDICATOR ======
loading: {
element: null,
// Show loading indicator
show: (message = 'Loading...') => {
if (UI.loading.element) {
UI.loading.hide();
}
const loading = document.createElement('div');
loading.id = 'baf-loading-indicator';
loading.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000000;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
`;
loading.innerHTML = `
<div style="margin-bottom: 10px;">⏳</div>
<div>${message}</div>
`;
UI.loading.element = loading;
document.body.appendChild(loading);
},
// Hide loading indicator
hide: () => {
if (UI.loading.element) {
document.body.removeChild(UI.loading.element);
UI.loading.element = null;
}
}
}
};
// ====== SAFETY GUARD SYSTEM ======
const SafetyGuard = {
status: true, // true = safe to proceed, false = blocked
// Check if it's safe to proceed with an action
check: (actionName = 'unknown') => {
return SafetyGuard.status; // true = safe to proceed
},
// Enable safety (allow actions)
enable: () => {
SafetyGuard.status = true;
TL.noti('✅ Safety Guard', 'Bot operations enabled', 3000);
TL.debug('SAFETY_GUARD', 'Safety guard enabled');
},
// Disable safety (block all actions)
disable: () => {
SafetyGuard.status = false;
TL.noti('🚨 Safety Guard', 'Bot operations blocked', 10000);
TL.debug('SAFETY_GUARD', 'Safety guard disabled');
},
// Get current status
getStatus: () => ({
enabled: SafetyGuard.status,
message: SafetyGuard.status ? '✅ Operations Enabled' : '🚨 Operations Blocked'
})
};
// ====== INITIALIZATION ======
const APP_STATE = new AppState();
async function initialize() {
// Initialize state
await APP_STATE.initialize();
// Register observers
APP_STATE.subscribe('server_type', StateWatchers.onServerTypeChange);
APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange);
APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange);
APP_STATE.subscribe('popup_position', StateWatchers.onPopupPositionChange);
APP_STATE.subscribe('popup_visible', StateWatchers.onPopupVisibleChange);
// Register general state observer for popup content updates
APP_STATE.subscribe('current_task', StateWatchers.onAnyStateChange);
APP_STATE.subscribe('current_step', StateWatchers.onAnyStateChange);
APP_STATE.subscribe('error_message', StateWatchers.onAnyStateChange);
APP_STATE.subscribe('bot_status', StateWatchers.onAnyStateChange);
// Create menu
await createGM_Menu();
// Initialize popup helper
UI.statusOverlay.init();
// Start heartbeat
setInterval(heartbeat_report, CONFIG.heartbeat_interval);
// Update popup content periodically
setInterval(() => {
if (UI.statusOverlay.element && APP_STATE.getData().popup_visible) {
UI.statusOverlay.updateContent();
}
}, 1000); // Update every 2 seconds
// Welcome message
const ping_res = await BAF.ping();
const data = APP_STATE.getData();
const res =
`Server: ${data.server_url.label} (${data.server_url.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.RUNNING && data.current_task) {
TL.log('INIT', 'Resuming interrupted task');
TaskRunner.stepRunner.resumeTask();
} else if (data.bot_status === BOT_STATUS.WAITING) {
TaskRunner.checkForNewTasks();
} else if (data.bot_status === BOT_STATUS.PAUSED) {
TL.log('INIT', 'Bot is paused - waiting for resume');
} else if (data.bot_status === BOT_STATUS.STOPPED) {
TL.log('INIT', 'Bot is stopped - waiting for start');
}
}
// Start the application
await initialize();
})();