From 02a8f030b6fb4fe109e9d9509d3e6c934d43465d Mon Sep 17 00:00:00 2001 From: thuanle Date: Sat, 2 Aug 2025 02:17:42 +0700 Subject: [PATCH] finish login --- README.md | 14 +- agent.user.js | 1283 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 875 insertions(+), 422 deletions(-) diff --git a/README.md b/README.md index d47e65b..499d8d7 100644 --- a/README.md +++ b/README.md @@ -126,10 +126,10 @@ async function initialize() { ... } ```javascript const TASK_TYPES = { LOGIN: 'login', - GET_ORDER_HISTORY: 'get-order-history', - GET_BALANCE: 'get-balance', - SWAP: 'swap', - NO_TASK: 'no-task' + GET_ORDER_HISTORY: 'get_order_history', + GET_BALANCE: 'get_balance', + SWAP: 'swap', + NO_TASK: 'no_task' }; ``` @@ -253,8 +253,8 @@ if (bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) { class AppState { data = { // Server configuration - server_mode: 'prod', - server: null, + server_type: 'prod', + server_url: null, // Page state current_page: null, @@ -425,7 +425,7 @@ const CONFIG = { ```javascript const STORAGE = { key_token: 'baf-agent-token', - key_server_mode: 'baf-server-mode', + key_server_type: 'baf-server-type', key_bot_status: 'baf-bot-status', key_navigation_state: 'baf-navigation-state' }; diff --git a/agent.user.js b/agent.user.js index 2a3e9be..0524c56 100644 --- a/agent.user.js +++ b/agent.user.js @@ -19,8 +19,15 @@ // @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'; + 'use strict'; // ====== CONFIGURATION ====== const CONFIG = { @@ -38,27 +45,31 @@ constructor() { this.data = { // Server configuration - server_mode: 'prod', - server: null, - + server_type: 'prod', + server_url: null, + // Page state current_page: null, is_logged_in: false, - + // Bot status - bot_status: BOT_STATUS.READY_FOR_NEW_TASKS, - + 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 + error_message: null, + + // Popup state + popup_position: 4, // 1: top-left, 2: top-right, 3: bottom-left, 4: bottom-right + popup_visible: true }; - + this.observers = new Map(); this.initialized = false; } @@ -66,21 +77,22 @@ // Initialize state from storage and page detection async initialize() { if (this.initialized) return; - + this.initialized = true; + // Load from storage - this.data.server_mode = await STORAGE.getServerMode(); - this.data.server = CONFIG.servers[this.data.server_mode] || CONFIG.servers.prod; + 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(); + // 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 }; } @@ -88,17 +100,14 @@ // 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, []); @@ -106,7 +115,6 @@ this.observers.get(key).push(callback); } - // Unsubscribe from state changes unsubscribe(key, callback) { const callbacks = this.observers.get(key); if (callbacks) { @@ -122,7 +130,7 @@ for (const [key, callbacks] of this.observers) { const oldValue = oldData[key]; const newValue = newData[key]; - + if (oldValue !== newValue) { for (const callback of callbacks) { try { @@ -139,20 +147,21 @@ // ====== STORAGE MODULE ====== const STORAGE = { key_token: 'baf-agent-token', - key_server_mode: 'baf-server-mode', + 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', 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), + 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.READY_FOR_NEW_TASKS), + getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.WAITING), setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status), - // Navigation state management getNavigationState: () => { try { return JSON.parse(GM_getValue(STORAGE.key_navigation_state, 'null')); @@ -160,20 +169,27 @@ 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) }; // ====== 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), + 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) => { @@ -186,7 +202,7 @@ }, delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)), - + // Guard against double execution during navigation guardDoubleRun: async (ctx, stepLogic) => { // Check if we're currently navigating @@ -203,7 +219,7 @@ 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; @@ -267,7 +283,7 @@ 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); + TL.debug(`net`, `${init.method} ${resp.status} ${url}\n${init.body}`, data); resolve({ status: resp.status, ok: resp.status >= 200 && resp.status < 300, @@ -287,7 +303,7 @@ const BAF = { getServer: async () => { const data = APP_STATE.getData(); - return CONFIG.servers[data.server_mode] || CONFIG.servers.prod; + return CONFIG.servers[data.server_type] || CONFIG.servers.prod; }, getHost: async () => { @@ -318,74 +334,150 @@ post: (path, body, init = {}) => BAF.request('POST', path, { body, headers: init.headers }), ping: (status) => BAF.post('/agent/ping', status), - - getTasks: () => BAF.get('/agent/tasks'), + + 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: () => { - const pathname = window.location.pathname; - const hostname = window.location.hostname; + if (!SafetyGuard.check('detectPage')) return BINANCE_PAGES.IGNORE; - if (hostname === 'accounts.binance.com') { - if (pathname.includes('/login')) { - return BINANCE_PAGES.LOGIN; + 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; } } - - 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; - } - } - + + TL.debug('PAGE_DETECT', msg + `No pattern matched: ${hostname}${pathname}`); 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, + 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 (BINANCE.isOnLoginPage()) { + 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; - } + } - const loginBtn = document.querySelector('#toLoginPage'); - const regBtn = document.querySelector('#toRegisterPage'); + // 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'); - if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) return false; - if (!loginBtn && !regBtn) return true; + 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 = 6000, pollMs = 200) => { + isLoggedIn: async (timeoutMs = 30000, pollMs = 500) => { const deadline = Date.now() + timeoutMs; - TL.debug(`BINANCE`,`isLoggedIn: Checking login state...`); while (Date.now() < deadline) { const state = BINANCE.detectLoginState(); if (state !== null) { + TL.debug(`BINANCE-LOGIN`, `isLoggedIn: ${state ? 'true' : 'false'}`); return state; } - TL.debug(`BINANCE`,`isLoggedIn: not found. Sleeping...`); + TL.debug(`BINANCE-LOGIN`, `isLoggedIn: not found. Sleeping...`); await TL.delay(pollMs); } const fallback = BINANCE.detectLoginState() ?? false; - TL.debug(`BINANCE`,`isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`); + TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`); return fallback; }, @@ -393,13 +485,14 @@ 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/'; - } + 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: () => { @@ -407,10 +500,28 @@ 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', @@ -422,16 +533,15 @@ // Bot Status Enum const BOT_STATUS = { - READY_FOR_NEW_TASKS: 'ready-for-new-tasks', - PAUSE_AUTOMATION: 'pause-automation', - PERFORMING_TASKS: 'performing-tasks' + 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', - PASSWORD: 'password', - EMAIL: 'email' }; // Order Type Enum @@ -468,240 +578,127 @@ 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; + // // 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(); + // 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; + 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; - } - }); + // 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' }); - } - } - }); + // is } }; // 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); - }); - } + //TODO: Implement + }; // 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); - }); - } + //TODO: Implement }; // ====== TASK DEFINITIONS ====== const TASK_DEFINITIONS = { [TASK_TYPES.LOGIN]: { - id: "login-task", + id: TASK_TYPES.LOGIN, steps: [ { name: "NavigateToLogin", @@ -722,44 +719,30 @@ }, [TASK_TYPES.GET_ORDER_HISTORY]: { - id: "get-order-history-task", + id: TASK_TYPES.GET_ORDER_HISTORY, 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 - } + //TODO: Implement ] }, [TASK_TYPES.GET_BALANCE]: { - id: "get-balance-task", + id: TASK_TYPES.GET_BALANCE, 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 - } + //TODO: Implement + ] + }, + + [TASK_TYPES.SWAP]: { + id: TASK_TYPES.SWAP, + steps: [ + //TODO: Implement + ] + }, + + [TASK_TYPES.NO_TASK]: { + id: TASK_TYPES.NO_TASK, + steps: [ + //TODO: Implement ] } }; @@ -798,7 +781,7 @@ try { // Handle different URL formats let targetUrl = url; - + if (url.startsWith('/')) { // Relative URL - append to current domain targetUrl = window.location.origin + url; @@ -807,14 +790,14 @@ 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, @@ -823,9 +806,9 @@ 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 @@ -850,11 +833,11 @@ 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); @@ -866,7 +849,7 @@ 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 @@ -878,14 +861,14 @@ // Continue with cleanup even if server submission fails } } - - // Return to ready state - await APP_STATE.update({ + + // Return to waiting 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, + bot_status: BOT_STATUS.WAITING, is_loading: false, error_message: result.success ? null : result.error }); @@ -893,18 +876,18 @@ // 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, + await APP_STATE.update({ + bot_status: BOT_STATUS.WAITING, is_loading: false, error_message: 'Task completion error: ' + error.message }); @@ -913,12 +896,25 @@ // 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}`); @@ -938,31 +934,31 @@ 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({ + 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}`); @@ -999,7 +995,7 @@ // 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(); } @@ -1007,7 +1003,7 @@ // 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'); @@ -1018,38 +1014,38 @@ // 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' + await this.completeTask({ + success: false, + error: 'Navigation timeout' }); } else { TL.debug('STEP', 'Still navigating, waiting...'); @@ -1064,10 +1060,10 @@ try { const parsed1 = new URL(url1); const parsed2 = new URL(url2); - + // Compare hostname and pathname - return parsed1.hostname === parsed2.hostname && - parsed1.pathname === parsed2.pathname; + return parsed1.hostname === parsed2.hostname && + parsed1.pathname === parsed2.pathname; } catch { // Fallback to string comparison return url1 === url2; @@ -1098,26 +1094,26 @@ 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({ + + await APP_STATE.update({ current_task: task, task_data: taskData, current_step: null, step_data: {}, - bot_status: BOT_STATUS.PERFORMING_TASKS, + 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 }); @@ -1142,33 +1138,33 @@ // 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 => + 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 => + 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; }, @@ -1199,21 +1195,23 @@ 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.READY_FOR_NEW_TASKS) { + 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.getTasks(); + 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) { @@ -1237,15 +1235,15 @@ // ====== STATE OBSERVERS ====== const StateWatchers = { // Storage observer - saves state changes to storage - async onServerModeChange(oldValue, newValue, data) { + async onServerTypeChange(oldValue, newValue, data) { TL.debug('OBSERVER', `Server mode changed: ${oldValue} -> ${newValue}`); - await STORAGE.setServerMode(newValue); - + await STORAGE.setServerType(newValue); + // Update server info - await APP_STATE.update({ - server: CONFIG.servers[newValue] || CONFIG.servers.prod + await APP_STATE.update({ + server_url: CONFIG.servers[newValue] || CONFIG.servers.prod }); - + // Reload page after a short delay setTimeout(() => location.reload(), 500); }, @@ -1253,44 +1251,106 @@ 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) { + 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.PERFORMING_TASKS) { + if (data.current_task && data.bot_status === BOT_STATUS.RUNNING) { // 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(); + // 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; - + const curSrv = data.server_url; + 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 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); @@ -1322,28 +1382,96 @@ } }); - GM_registerMenuCommand('Bot Status', async () => { + // Bot Status Menu với icon và tên task + const getStatusDisplay = () => { 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}`); + const taskName = data.current_task?.type || ''; + + switch (data.bot_status) { + case BOT_STATUS.WAITING: + return `💤 Waiting for new task`; + case BOT_STATUS.RUNNING: + return `▶️ Running: ${taskName}`; + case BOT_STATUS.PAUSED: + return `⏸️ Paused: ${taskName}`; + case BOT_STATUS.STOPPED: + return `⏹️ Stopped`; + default: + return `❓ Unknown status`; + } + }; + + // Popup toggle menu + const popupData = APP_STATE.getData(); + const popupStatus = popupData.popup_visible ? '👁️ Hide Popup' : '👁️ Show Popup'; + GM_registerMenuCommand(popupStatus, () => { + UI.statusOverlay.toggle(); }); - GM_registerMenuCommand('Get Tasks', async () => { - await TaskRunner.checkForNewTasks(); + + + + // Debug tools + GM_registerMenuCommand('🔍 Current Status', () => { + const detectedPage = APP_STATE.getData().current_page; + const isLoggedIn = APP_STATE.getData().is_logged_in; + const currentUrl = window.location.href; + + const status = `Page: ${detectedPage}\nLogin: ${isLoggedIn ? 'Yes' : 'No'}\nURL: ${currentUrl}`; + + console.log('Current Status:', status); + alert(status); }); + + GM_registerMenuCommand('🔐 Debug Login State', () => { + const isInIframe = window.top !== window; + const loginState = BINANCE.detectLoginState(); + + // Check all possible login indicators + const loginIndicators = { + 'In iframe': isInIframe, + 'Login button visible': TL.dom.isVisible?.(document.querySelector('#toLoginPage')), + 'Register button visible': TL.dom.isVisible?.(document.querySelector('#toRegisterPage')), + 'Account icon found': !!document.querySelector('.header-account-icon'), + 'Deposit button found': !!document.querySelector('.deposit-btn'), + 'Wallet button found': !!document.querySelector('#ba-wallet'), + 'User menu found': !!document.querySelector('.header-dropdown-menu'), + 'Dashboard link found': !!document.querySelector('a[href*="/my/dashboard"]'), + 'Detected login state': loginState + }; + + const debugInfo = Object.entries(loginIndicators) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + + console.log('=== LOGIN STATE DEBUG ==='); + console.log(debugInfo); + alert(`Login State Debug:\n\n${debugInfo}`); + }); + + GM_registerMenuCommand('🔍 Debug URL Sources', () => { + const mainUrl = window.location.href; + const frames = Array.from(document.querySelectorAll('iframe')).map(frame => frame.src); + const popups = window.opener ? window.opener.location.href : null; + + const debugInfo = ` +Main Window: ${mainUrl} +Frames: ${frames.length > 0 ? frames.join('\n') : 'None'} +Popup Opener: ${popups || 'None'} + `; + + console.log('=== URL SOURCES DEBUG ==='); + console.log(debugInfo); + alert(`URL Sources Debug:\n\n${debugInfo}`); + }); + + } // ====== HEARTBEAT ====== async function heartbeat_report() { + if (!SafetyGuard.check('heartbeat_report')) return null; + try { const data = APP_STATE.getData(); const status = { @@ -1353,10 +1481,9 @@ 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)}`); + // TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`); await BAF.ping(status); return status; } catch (e) { @@ -1365,58 +1492,384 @@ } } - // ====== 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; + // ====== 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: () => { + if (!UI.statusOverlay.element) return; + + 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'; + + // Get page info + const pageDisplay = data.current_page || 'unknown'; + + // Get login status + const loginStatus = data.is_logged_in ? '✅ Logged in' : '❌ Not logged in'; + + // Get SafetyGuard status + const safetyStatus = SafetyGuard.getStatus(); + const safetyDisplay = safetyStatus.enabled ? '🛡️ Safe' : '🚨 Blocked'; + + // Build content + const content = ` +
+ BAF Agent Status +
+
+ ${statusDisplay} +
+
+ ${safetyDisplay} +
+
+ Task: ${taskName} +
+
+ Step: ${stepName} +
+
+ Page: ${pageDisplay} +
+
+ ${loginStatus} +
+
+ Server: ${serverLabel} +
+ ${data.error_message ? `
Error: ${data.error_message}
` : ''} + `; + + // 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); + } + }, + + // 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(); + } } - }, 1000); - } + }, + + // ====== 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 = ` +
+
${message}
+ `; + + 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() { + + async function initialize() { // Initialize state await APP_STATE.initialize(); - + // Register observers - APP_STATE.subscribe('server_mode', StateWatchers.onServerModeChange); + APP_STATE.subscribe('server_type', StateWatchers.onServerTypeChange); APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange); APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange); - APP_STATE.subscribe('is_logged_in', StateWatchers.onLoginStateChange); - + 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(); - - // Start monitoring - monitorPageChanges(); - + + // 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.label} (${data.server.url})\n` + + `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.PERFORMING_TASKS && data.current_task) { + 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.READY_FOR_NEW_TASKS) { + } 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'); } }