Enhance agent.user.js with a robust navigation state management system, introducing new functions for handling navigation state persistence and double execution prevention. Update README.md to reflect architectural changes, task-step system improvements, and detailed state management documentation.
This commit is contained in:
807
agent.user.js
807
agent.user.js
@@ -141,6 +141,7 @@
|
|||||||
key_token: 'baf-agent-token',
|
key_token: 'baf-agent-token',
|
||||||
key_server_mode: 'baf-server-mode',
|
key_server_mode: 'baf-server-mode',
|
||||||
key_bot_status: 'baf-bot-status',
|
key_bot_status: 'baf-bot-status',
|
||||||
|
key_navigation_state: 'baf-navigation-state',
|
||||||
|
|
||||||
getToken: () => GM_getValue(STORAGE.key_token, ''),
|
getToken: () => GM_getValue(STORAGE.key_token, ''),
|
||||||
setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
|
setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
|
||||||
@@ -150,6 +151,23 @@
|
|||||||
|
|
||||||
getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.READY_FOR_NEW_TASKS),
|
getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.READY_FOR_NEW_TASKS),
|
||||||
setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status),
|
setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status),
|
||||||
|
|
||||||
|
// Navigation state management
|
||||||
|
getNavigationState: () => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(GM_getValue(STORAGE.key_navigation_state, 'null'));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setNavigationState: (state) => {
|
||||||
|
GM_setValue(STORAGE.key_navigation_state, JSON.stringify(state));
|
||||||
|
},
|
||||||
|
|
||||||
|
clearNavigationState: () => {
|
||||||
|
GM_setValue(STORAGE.key_navigation_state, 'null');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ====== UTILITY MODULE ======
|
// ====== UTILITY MODULE ======
|
||||||
@@ -168,6 +186,33 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
|
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
|
||||||
|
|
||||||
|
// Guard against double execution during navigation
|
||||||
|
guardDoubleRun: async (ctx, stepLogic) => {
|
||||||
|
// Check if we're currently navigating
|
||||||
|
if (ctx.step_data.navigating) {
|
||||||
|
TL.debug('STEP', 'Currently navigating, skipping step execution');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this step was already completed after navigation
|
||||||
|
if (ctx.step_data.navigation_complete && ctx.step_data.last_completed_step === ctx.current_step_name) {
|
||||||
|
TL.debug('STEP', 'Step already completed after navigation, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stepLogic();
|
||||||
|
|
||||||
|
// Mark step as completed (unless navigation was triggered)
|
||||||
|
if (!ctx.step_data.navigating) {
|
||||||
|
ctx.step_data.last_completed_step = ctx.current_step_name;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', `Error in step ${ctx.current_step_name}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// DOM helpers
|
// DOM helpers
|
||||||
@@ -445,8 +490,645 @@
|
|||||||
ERROR_HANDLING: 'error_handling'
|
ERROR_HANDLING: 'error_handling'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ====== STEP FUNCTIONS ======
|
||||||
|
|
||||||
|
// Login Step Functions
|
||||||
|
const LoginSteps = {
|
||||||
|
matchNavigateToLogin: (url, ctx) => {
|
||||||
|
const isOnLoginPage = url.includes("/login");
|
||||||
|
const isLoggedIn = ctx?.is_logged_in || false;
|
||||||
|
return isOnLoginPage || !isLoggedIn;
|
||||||
|
},
|
||||||
|
|
||||||
|
runNavigateToLogin: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'NavigateToLogin: Starting login process');
|
||||||
|
|
||||||
|
if (!BINANCE.isOnLoginPage()) {
|
||||||
|
BINANCE.navigateToLogin();
|
||||||
|
ctx.step_data.page_navigated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.step_data.page_loaded = true;
|
||||||
|
ctx.step_data.start_time = Date.now();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchSelectQRCode: (url, ctx) => {
|
||||||
|
return url.includes("/login") &&
|
||||||
|
ctx?.step_data?.page_loaded &&
|
||||||
|
ctx?.task_data?.method === LOGIN_METHOD.QR_CODE;
|
||||||
|
},
|
||||||
|
|
||||||
|
runSelectQRCode: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'SelectQRCode: Selecting QR code method');
|
||||||
|
|
||||||
|
const qrButton = document.querySelector('[data-testid="qr-code-tab"], .qr-code-btn, .qr-tab');
|
||||||
|
if (qrButton) {
|
||||||
|
qrButton.click();
|
||||||
|
ctx.step_data.qr_selected = true;
|
||||||
|
TL.debug('STEP', 'QR code method selected');
|
||||||
|
} else {
|
||||||
|
TL.debug('STEP', 'QR code button not found, may already be selected');
|
||||||
|
ctx.step_data.qr_selected = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchWaitForLogin: (url, ctx) => {
|
||||||
|
return !url.includes("/login") && ctx?.step_data?.qr_selected;
|
||||||
|
},
|
||||||
|
|
||||||
|
runWaitForLogin: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'WaitForLogin: Checking login status');
|
||||||
|
|
||||||
|
const isLoggedIn = await BINANCE.isLoggedIn();
|
||||||
|
if (isLoggedIn) {
|
||||||
|
ctx.step_data.login_success = true;
|
||||||
|
ctx.step_data.login_time = Date.now();
|
||||||
|
ctx.done({ success: true, message: 'Login successful' });
|
||||||
|
} else {
|
||||||
|
const elapsed = Date.now() - ctx.step_data.start_time;
|
||||||
|
const timeout = ctx.task_data?.timeout || 300000;
|
||||||
|
|
||||||
|
if (elapsed > timeout) {
|
||||||
|
ctx.done({ success: false, error: 'Login timeout' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Order History Step Functions
|
||||||
|
const OrderHistorySteps = {
|
||||||
|
matchNavigateToOrderHistory: (url, ctx) => {
|
||||||
|
return url.includes("/my/orders/alpha/orderhistory") ||
|
||||||
|
!url.includes("/my/orders/alpha/orderhistory");
|
||||||
|
},
|
||||||
|
|
||||||
|
runNavigateToOrderHistory: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'NavigateToOrderHistory: Navigating to order history page');
|
||||||
|
|
||||||
|
if (!BINANCE.isOnAlphaOrderHistoryPage()) {
|
||||||
|
BINANCE.navigateToAlphaOrderHistory();
|
||||||
|
ctx.step_data.page_navigated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.step_data.page_loaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchExtractOrderData: (url, ctx) => {
|
||||||
|
return url.includes("/my/orders/alpha/orderhistory") &&
|
||||||
|
ctx?.step_data?.page_loaded;
|
||||||
|
},
|
||||||
|
|
||||||
|
runExtractOrderData: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'ExtractOrderData: Extracting order data');
|
||||||
|
|
||||||
|
await TL.delay(2000);
|
||||||
|
|
||||||
|
const orders = [];
|
||||||
|
const orderRows = document.querySelectorAll('.order-row, [data-testid="order-row"]');
|
||||||
|
|
||||||
|
orderRows.forEach((row, index) => {
|
||||||
|
if (index < (ctx.task_data?.limit || 100)) {
|
||||||
|
const order = {
|
||||||
|
id: row.getAttribute('data-order-id') || `order_${index}`,
|
||||||
|
type: row.querySelector('.order-type')?.textContent || 'unknown',
|
||||||
|
status: row.querySelector('.order-status')?.textContent || 'unknown',
|
||||||
|
amount: row.querySelector('.order-amount')?.textContent || '0',
|
||||||
|
date: row.querySelector('.order-date')?.textContent || new Date().toISOString()
|
||||||
|
};
|
||||||
|
orders.push(order);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.step_data.orders = orders;
|
||||||
|
ctx.step_data.extracted = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchSendOrderData: (url, ctx) => {
|
||||||
|
return ctx?.step_data?.extracted;
|
||||||
|
},
|
||||||
|
|
||||||
|
runSendOrderData: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'SendOrderData: Sending order data to server');
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
success: true,
|
||||||
|
order_count: ctx.step_data.orders.length,
|
||||||
|
orders: ctx.step_data.orders
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.done(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Balance Step Functions
|
||||||
|
const BalanceSteps = {
|
||||||
|
matchNavigateToBalance: (url, ctx) => {
|
||||||
|
return url.includes("/alpha/bsc/") || !url.includes("/alpha/bsc/");
|
||||||
|
},
|
||||||
|
|
||||||
|
runNavigateToBalance: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'NavigateToBalance: Navigating to balance page');
|
||||||
|
|
||||||
|
if (!BINANCE.isOnAlphaSwapPage()) {
|
||||||
|
BINANCE.navigateToAlphaSwap();
|
||||||
|
ctx.step_data.page_navigated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.step_data.page_loaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchExtractBalanceData: (url, ctx) => {
|
||||||
|
return url.includes("/alpha/bsc/") && ctx?.step_data?.page_loaded;
|
||||||
|
},
|
||||||
|
|
||||||
|
runExtractBalanceData: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'ExtractBalanceData: Extracting balance data');
|
||||||
|
|
||||||
|
await TL.delay(2000);
|
||||||
|
|
||||||
|
const balances = {};
|
||||||
|
const balanceElements = document.querySelectorAll('.balance-item, [data-testid="balance-item"]');
|
||||||
|
|
||||||
|
balanceElements.forEach(element => {
|
||||||
|
const currency = element.querySelector('.currency')?.textContent || 'unknown';
|
||||||
|
const amount = element.querySelector('.amount')?.textContent || '0';
|
||||||
|
|
||||||
|
if (ctx.task_data?.currencies === null ||
|
||||||
|
ctx.task_data?.currencies?.includes(currency)) {
|
||||||
|
balances[currency] = amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.step_data.balances = balances;
|
||||||
|
ctx.step_data.extracted = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
matchSendBalanceData: (url, ctx) => {
|
||||||
|
return ctx?.step_data?.extracted;
|
||||||
|
},
|
||||||
|
|
||||||
|
runSendBalanceData: async (ctx) => {
|
||||||
|
await TL.guardDoubleRun(ctx, async () => {
|
||||||
|
TL.log('STEP', 'SendBalanceData: Sending balance data to server');
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
success: true,
|
||||||
|
balance_count: Object.keys(ctx.step_data.balances).length,
|
||||||
|
balances: ctx.step_data.balances
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.done(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ====== TASK DEFINITIONS ======
|
||||||
|
const TASK_DEFINITIONS = {
|
||||||
|
[TASK_TYPES.LOGIN]: {
|
||||||
|
id: "login-task",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: "NavigateToLogin",
|
||||||
|
match: LoginSteps.matchNavigateToLogin,
|
||||||
|
run: LoginSteps.runNavigateToLogin
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SelectQRCode",
|
||||||
|
match: LoginSteps.matchSelectQRCode,
|
||||||
|
run: LoginSteps.runSelectQRCode
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WaitForLogin",
|
||||||
|
match: LoginSteps.matchWaitForLogin,
|
||||||
|
run: LoginSteps.runWaitForLogin
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
[TASK_TYPES.GET_ORDER_HISTORY]: {
|
||||||
|
id: "get-order-history-task",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: "NavigateToOrderHistory",
|
||||||
|
match: OrderHistorySteps.matchNavigateToOrderHistory,
|
||||||
|
run: OrderHistorySteps.runNavigateToOrderHistory
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExtractOrderData",
|
||||||
|
match: OrderHistorySteps.matchExtractOrderData,
|
||||||
|
run: OrderHistorySteps.runExtractOrderData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SendOrderData",
|
||||||
|
match: OrderHistorySteps.matchSendOrderData,
|
||||||
|
run: OrderHistorySteps.runSendOrderData
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
[TASK_TYPES.GET_BALANCE]: {
|
||||||
|
id: "get-balance-task",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: "NavigateToBalance",
|
||||||
|
match: BalanceSteps.matchNavigateToBalance,
|
||||||
|
run: BalanceSteps.runNavigateToBalance
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExtractBalanceData",
|
||||||
|
match: BalanceSteps.matchExtractBalanceData,
|
||||||
|
run: BalanceSteps.runExtractBalanceData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SendBalanceData",
|
||||||
|
match: BalanceSteps.matchSendBalanceData,
|
||||||
|
run: BalanceSteps.runSendBalanceData
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ====== STEP RUNNER ======
|
||||||
|
class StepRunner {
|
||||||
|
constructor() {
|
||||||
|
this.currentStepIndex = 0;
|
||||||
|
this.context = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context for task execution
|
||||||
|
createContext(task, taskData) {
|
||||||
|
return {
|
||||||
|
task_id: task.id,
|
||||||
|
task_type: task.type,
|
||||||
|
task_data: taskData,
|
||||||
|
step_data: {},
|
||||||
|
is_logged_in: APP_STATE.getData().is_logged_in,
|
||||||
|
current_page: APP_STATE.getData().current_page,
|
||||||
|
current_step_name: null, // Will be set by executeCurrentStep
|
||||||
|
done: (result) => this.completeTask(result),
|
||||||
|
goto: (url) => this.navigateTo(url),
|
||||||
|
wait: (ms) => this.wait(ms),
|
||||||
|
retry: (fn, maxAttempts = 3) => this.retry(fn, maxAttempts)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to URL
|
||||||
|
navigateTo(url) {
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
TL.error('STEP', 'Invalid URL provided for navigation:', url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle different URL formats
|
||||||
|
let targetUrl = url;
|
||||||
|
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
// Relative URL - append to current domain
|
||||||
|
targetUrl = window.location.origin + url;
|
||||||
|
} else if (!url.startsWith('http')) {
|
||||||
|
// Relative URL without leading slash
|
||||||
|
targetUrl = window.location.origin + '/' + url;
|
||||||
|
}
|
||||||
|
// Absolute URL (starts with http) - use as is
|
||||||
|
|
||||||
|
TL.debug('STEP', `Navigating to: ${targetUrl}`);
|
||||||
|
|
||||||
|
// Mark that we're navigating to avoid double execution
|
||||||
|
this.context.step_data.navigating = true;
|
||||||
|
this.context.step_data.navigation_start = Date.now();
|
||||||
|
this.context.step_data.navigation_target = targetUrl;
|
||||||
|
|
||||||
|
// Save navigation state to storage for persistence across page reload
|
||||||
|
STORAGE.setNavigationState({
|
||||||
|
navigating: true,
|
||||||
|
navigation_start: this.context.step_data.navigation_start,
|
||||||
|
navigation_target: targetUrl,
|
||||||
|
task_id: this.context.task_id,
|
||||||
|
step_index: this.currentStepIndex
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = targetUrl;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', 'Navigation failed:', error);
|
||||||
|
// Clear navigation state on error
|
||||||
|
this.context.step_data.navigating = false;
|
||||||
|
delete this.context.step_data.navigation_start;
|
||||||
|
delete this.context.step_data.navigation_target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for specified milliseconds
|
||||||
|
async wait(ms) {
|
||||||
|
if (ms && ms > 0) {
|
||||||
|
TL.debug('STEP', `Waiting for ${ms}ms`);
|
||||||
|
await TL.delay(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry function with exponential backoff
|
||||||
|
async retry(fn, maxAttempts = 3) {
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
TL.debug('STEP', `Attempt ${attempt} failed:`, error.message);
|
||||||
|
|
||||||
|
if (attempt === maxAttempts) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponential backoff: 1s, 2s, 4s
|
||||||
|
const delay = Math.pow(2, attempt - 1) * 1000;
|
||||||
|
await this.wait(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete task and return to ready state
|
||||||
|
async completeTask(result) {
|
||||||
|
try {
|
||||||
|
TL.log('STEP', `Task completed with result:`, result);
|
||||||
|
|
||||||
|
const data = APP_STATE.getData();
|
||||||
|
if (data.current_task) {
|
||||||
|
// Submit result to server
|
||||||
|
try {
|
||||||
|
await BAF.submitTaskResult(data.current_task.id, result);
|
||||||
|
TL.debug('STEP', 'Task result submitted to server successfully');
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', 'Failed to submit task result to server:', error);
|
||||||
|
// Continue with cleanup even if server submission fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return to ready state
|
||||||
|
await APP_STATE.update({
|
||||||
|
current_task: null,
|
||||||
|
task_data: null,
|
||||||
|
current_step: null,
|
||||||
|
step_data: null,
|
||||||
|
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
||||||
|
is_loading: false,
|
||||||
|
error_message: result.success ? null : result.error
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset step runner state
|
||||||
|
this.currentStepIndex = 0;
|
||||||
|
this.context = null;
|
||||||
|
|
||||||
|
// Clear navigation state
|
||||||
|
STORAGE.clearNavigationState();
|
||||||
|
|
||||||
|
// Check for next task after a short delay
|
||||||
|
setTimeout(() => TaskRunner.checkForNewTasks(), 1000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', 'Error during task completion:', error);
|
||||||
|
// Force return to ready state even if cleanup fails
|
||||||
|
await APP_STATE.update({
|
||||||
|
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
||||||
|
is_loading: false,
|
||||||
|
error_message: 'Task completion error: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute current step
|
||||||
|
async executeCurrentStep() {
|
||||||
|
const data = APP_STATE.getData();
|
||||||
|
if (!data.current_task || !this.context) {
|
||||||
|
TL.debug('STEP', 'No current task or context available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskDefinition = TASK_DEFINITIONS[data.current_task.type];
|
||||||
|
if (!taskDefinition) {
|
||||||
|
TL.error('STEP', `No task definition found for type: ${data.current_task.type}`);
|
||||||
|
await this.completeTask({ success: false, error: 'Unknown task type' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStep = taskDefinition.steps[this.currentStepIndex];
|
||||||
|
if (!currentStep) {
|
||||||
|
TL.error('STEP', `No step found at index: ${this.currentStepIndex}`);
|
||||||
|
await this.completeTask({ success: false, error: 'Step not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set current step name in context
|
||||||
|
this.context.current_step_name = currentStep.name;
|
||||||
|
|
||||||
|
const url = window.location.href;
|
||||||
|
TL.debug('STEP', `Checking step: ${currentStep.name} at URL: ${url}`);
|
||||||
|
|
||||||
|
if (currentStep.match(url, this.context)) {
|
||||||
|
try {
|
||||||
|
TL.log('STEP', `Executing step: ${currentStep.name}`);
|
||||||
|
|
||||||
|
// Update context with current state
|
||||||
|
this.context.is_logged_in = APP_STATE.getData().is_logged_in;
|
||||||
|
this.context.current_page = APP_STATE.getData().current_page;
|
||||||
|
|
||||||
|
// Execute step (guardDoubleRun is already wrapped in step functions)
|
||||||
|
await currentStep.run(this.context);
|
||||||
|
|
||||||
|
// Check if navigation was triggered
|
||||||
|
if (this.context.step_data.navigating) {
|
||||||
|
TL.debug('STEP', 'Navigation triggered, stopping execution');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next step
|
||||||
|
this.currentStepIndex++;
|
||||||
|
await APP_STATE.update({
|
||||||
|
current_step: currentStep.name,
|
||||||
|
step_data: this.context.step_data
|
||||||
|
});
|
||||||
|
|
||||||
|
// Continue with next step if available
|
||||||
|
if (this.currentStepIndex < taskDefinition.steps.length) {
|
||||||
|
TL.debug('STEP', `Moving to next step: ${this.currentStepIndex + 1}/${taskDefinition.steps.length}`);
|
||||||
|
setTimeout(() => this.executeCurrentStep(), 100);
|
||||||
|
} else {
|
||||||
|
// All steps completed
|
||||||
|
TL.log('STEP', 'All steps completed successfully');
|
||||||
|
await this.completeTask({ success: true, message: 'All steps completed' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', `Error executing step ${currentStep.name}:`, error);
|
||||||
|
await this.completeTask({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TL.debug('STEP', `Step ${currentStep.name} conditions not met, waiting for page change...`);
|
||||||
|
// Don't retry immediately, wait for page change or state change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume task from saved state
|
||||||
|
async resumeTask() {
|
||||||
|
const data = APP_STATE.getData();
|
||||||
|
if (!data.current_task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for navigation state
|
||||||
|
const navigationState = STORAGE.getNavigationState();
|
||||||
|
if (navigationState && navigationState.navigating) {
|
||||||
|
await this.handleNavigationResume(navigationState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context from saved state
|
||||||
|
this.context = this.createContext(data.current_task, data.task_data);
|
||||||
|
this.context.step_data = data.step_data || {};
|
||||||
|
|
||||||
|
// Resume from current step
|
||||||
|
await this.executeCurrentStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle navigation resume
|
||||||
|
async handleNavigationResume(navigationState) {
|
||||||
|
TL.log('STEP', 'Resuming after navigation:', navigationState);
|
||||||
|
|
||||||
|
const data = APP_STATE.getData();
|
||||||
|
if (!data.current_task || data.current_task.id !== navigationState.task_id) {
|
||||||
|
TL.warn('STEP', 'Task ID mismatch, clearing navigation state');
|
||||||
|
STORAGE.clearNavigationState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're on the target page
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const targetUrl = navigationState.navigation_target;
|
||||||
|
|
||||||
|
if (currentUrl.includes(targetUrl) || this.isSamePage(currentUrl, targetUrl)) {
|
||||||
|
TL.log('STEP', 'Successfully navigated to target page');
|
||||||
|
|
||||||
|
// Clear navigation state
|
||||||
|
STORAGE.clearNavigationState();
|
||||||
|
|
||||||
|
// Create context and continue
|
||||||
|
this.context = this.createContext(data.current_task, data.task_data);
|
||||||
|
this.context.step_data = data.step_data || {};
|
||||||
|
this.currentStepIndex = navigationState.step_index;
|
||||||
|
|
||||||
|
// Mark navigation as complete
|
||||||
|
this.context.step_data.navigating = false;
|
||||||
|
this.context.step_data.navigation_complete = true;
|
||||||
|
this.context.step_data.navigation_complete_time = Date.now();
|
||||||
|
|
||||||
|
// Continue with next step
|
||||||
|
this.currentStepIndex++;
|
||||||
|
await this.executeCurrentStep();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Still navigating or navigation failed
|
||||||
|
const elapsed = Date.now() - navigationState.navigation_start;
|
||||||
|
const timeout = 10000; // 10 seconds timeout
|
||||||
|
|
||||||
|
if (elapsed > timeout) {
|
||||||
|
TL.error('STEP', 'Navigation timeout, clearing state');
|
||||||
|
STORAGE.clearNavigationState();
|
||||||
|
await this.completeTask({
|
||||||
|
success: false,
|
||||||
|
error: 'Navigation timeout'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
TL.debug('STEP', 'Still navigating, waiting...');
|
||||||
|
// Wait a bit more for navigation to complete
|
||||||
|
setTimeout(() => this.handleNavigationResume(navigationState), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if two URLs represent the same page
|
||||||
|
isSamePage(url1, url2) {
|
||||||
|
try {
|
||||||
|
const parsed1 = new URL(url1);
|
||||||
|
const parsed2 = new URL(url2);
|
||||||
|
|
||||||
|
// Compare hostname and pathname
|
||||||
|
return parsed1.hostname === parsed2.hostname &&
|
||||||
|
parsed1.pathname === parsed2.pathname;
|
||||||
|
} catch {
|
||||||
|
// Fallback to string comparison
|
||||||
|
return url1 === url2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate task definition
|
||||||
|
validateTaskDefinition(taskType) {
|
||||||
|
const taskDefinition = TASK_DEFINITIONS[taskType];
|
||||||
|
if (!taskDefinition) {
|
||||||
|
throw new Error(`No task definition found for type: ${taskType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!taskDefinition.steps || !Array.isArray(taskDefinition.steps)) {
|
||||||
|
throw new Error(`Invalid steps array for task type: ${taskType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDefinition.steps.forEach((step, index) => {
|
||||||
|
if (!step.name || !step.match || !step.run) {
|
||||||
|
throw new Error(`Invalid step at index ${index} for task type: ${taskType}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return taskDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new task
|
||||||
|
async startTask(task, taskData) {
|
||||||
|
try {
|
||||||
|
TL.log('STEP', `Starting task: ${task.type}`, task);
|
||||||
|
|
||||||
|
// Validate task definition
|
||||||
|
this.validateTaskDefinition(task.type);
|
||||||
|
|
||||||
|
this.currentStepIndex = 0;
|
||||||
|
this.context = this.createContext(task, taskData);
|
||||||
|
|
||||||
|
await APP_STATE.update({
|
||||||
|
current_task: task,
|
||||||
|
task_data: taskData,
|
||||||
|
current_step: null,
|
||||||
|
step_data: {},
|
||||||
|
bot_status: BOT_STATUS.PERFORMING_TASKS,
|
||||||
|
is_loading: true,
|
||||||
|
error_message: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start executing steps
|
||||||
|
await this.executeCurrentStep();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
TL.error('STEP', 'Failed to start task:', error);
|
||||||
|
await this.completeTask({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ====== TASK RUNNER ======
|
// ====== TASK RUNNER ======
|
||||||
const TaskRunner = {
|
const TaskRunner = {
|
||||||
|
stepRunner: new StepRunner(),
|
||||||
|
|
||||||
// Helper function to validate enum values
|
// Helper function to validate enum values
|
||||||
validateEnum(value, enumObj, defaultValue) {
|
validateEnum(value, enumObj, defaultValue) {
|
||||||
const validValues = Object.values(enumObj);
|
const validValues = Object.values(enumObj);
|
||||||
@@ -529,18 +1211,11 @@
|
|||||||
const task = response.data.task;
|
const task = response.data.task;
|
||||||
TL.log('TASK', `Received new task: ${task.type}`, task);
|
TL.log('TASK', `Received new task: ${task.type}`, task);
|
||||||
|
|
||||||
// Validate and merge task data with defaults
|
// Validate task data
|
||||||
const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data);
|
const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data);
|
||||||
|
|
||||||
await APP_STATE.update({
|
// Start task using StepRunner
|
||||||
current_task: task,
|
await this.stepRunner.startTask(task, validatedTaskData);
|
||||||
task_data: validatedTaskData,
|
|
||||||
current_step: task.steps?.[0] || null,
|
|
||||||
bot_status: BOT_STATUS.PERFORMING_TASKS
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start executing the task
|
|
||||||
await this.executeTask(task);
|
|
||||||
} else if (response.ok && response.data.no_task) {
|
} else if (response.ok && response.data.no_task) {
|
||||||
TL.debug('TASK', 'No new tasks available');
|
TL.debug('TASK', 'No new tasks available');
|
||||||
// Sleep for interval then check again
|
// Sleep for interval then check again
|
||||||
@@ -553,104 +1228,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async executeTask(task) {
|
// Continue current step (called by observers)
|
||||||
TL.log('TASK', `Executing task: ${task.type}`, task);
|
async continueCurrentStep() {
|
||||||
|
await this.stepRunner.executeCurrentStep();
|
||||||
try {
|
|
||||||
await APP_STATE.update({ is_loading: true });
|
|
||||||
|
|
||||||
let result = null;
|
|
||||||
|
|
||||||
switch (task.type) {
|
|
||||||
case TASK_TYPES.LOGIN:
|
|
||||||
result = await this.executeLoginTask(task);
|
|
||||||
break;
|
|
||||||
case TASK_TYPES.GET_ORDER_HISTORY:
|
|
||||||
result = await this.executeGetOrderHistoryTask(task);
|
|
||||||
break;
|
|
||||||
case TASK_TYPES.GET_BALANCE:
|
|
||||||
result = await this.executeGetBalanceTask(task);
|
|
||||||
break;
|
|
||||||
case TASK_TYPES.SWAP:
|
|
||||||
result = await this.executeSwapTask(task);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown task type: ${task.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit result to server
|
|
||||||
await BAF.submitTaskResult(task.id, result);
|
|
||||||
|
|
||||||
// Task completed, return to ready state
|
|
||||||
await APP_STATE.update({
|
|
||||||
current_task: null,
|
|
||||||
task_data: null,
|
|
||||||
current_step: null,
|
|
||||||
step_data: null,
|
|
||||||
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
|
||||||
is_loading: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for next task after a short delay
|
|
||||||
setTimeout(() => this.checkForNewTasks(), 1000);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
TL.error('TASK', `Task execution failed:`, error);
|
|
||||||
await APP_STATE.update({
|
|
||||||
error_message: error.message,
|
|
||||||
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
|
|
||||||
is_loading: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retry after interval
|
|
||||||
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeLoginTask(task) {
|
|
||||||
const taskData = this.getCurrentTaskData();
|
|
||||||
TL.log('TASK', 'Executing login task', taskData);
|
|
||||||
|
|
||||||
// Implementation for login task with steps
|
|
||||||
// taskData contains: { method: 'qr_code' | 'password', credentials: {...}, timeout: 300000, retry_count: 3 }
|
|
||||||
|
|
||||||
// Example: Update task data during execution
|
|
||||||
await this.updateTaskData({
|
|
||||||
start_time: Date.now(),
|
|
||||||
attempts: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, message: 'Login task completed' };
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeGetOrderHistoryTask(task) {
|
|
||||||
const taskData = this.getCurrentTaskData();
|
|
||||||
TL.log('TASK', 'Executing get order history task', taskData);
|
|
||||||
|
|
||||||
// Implementation for get order history task
|
|
||||||
// taskData contains: { limit: 100, offset: 0, date_range: {...}, order_types: [...], status: [...] }
|
|
||||||
|
|
||||||
return { success: true, order_history: [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeGetBalanceTask(task) {
|
|
||||||
const taskData = this.getCurrentTaskData();
|
|
||||||
TL.log('TASK', 'Executing get balance task', taskData);
|
|
||||||
|
|
||||||
// Implementation for get balance task
|
|
||||||
// taskData contains: { currencies: ['USDT', 'BTC'], include_zero: false, include_locked: true, format: 'json' }
|
|
||||||
|
|
||||||
return { success: true, balance: {} };
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeSwapTask(task) {
|
|
||||||
const taskData = this.getCurrentTaskData();
|
|
||||||
TL.log('TASK', 'Executing swap task', taskData);
|
|
||||||
|
|
||||||
// Implementation for swap task
|
|
||||||
// taskData contains: { from_token: 'USDT', to_token: 'BTC', amount: 100, slippage: 0.5, gas_fee: 'auto', deadline: 300, auto_confirm: true }
|
|
||||||
|
|
||||||
return { success: true, swap_result: {} };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -685,7 +1265,7 @@
|
|||||||
TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`);
|
TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`);
|
||||||
|
|
||||||
// Handle page-specific logic for current task
|
// Handle page-specific logic for current task
|
||||||
if (data.current_task && data.current_step) {
|
if (data.current_task && data.bot_status === BOT_STATUS.PERFORMING_TASKS) {
|
||||||
// Continue with current step based on page change
|
// Continue with current step based on page change
|
||||||
await TaskRunner.continueCurrentStep();
|
await TaskRunner.continueCurrentStep();
|
||||||
}
|
}
|
||||||
@@ -695,7 +1275,7 @@
|
|||||||
TL.debug('OBSERVER', `Login state changed: ${oldValue} -> ${newValue}`);
|
TL.debug('OBSERVER', `Login state changed: ${oldValue} -> ${newValue}`);
|
||||||
|
|
||||||
// Handle login state change for current task
|
// Handle login state change for current task
|
||||||
if (data.current_task && data.current_step) {
|
if (data.current_task && data.bot_status === BOT_STATUS.PERFORMING_TASKS) {
|
||||||
await TaskRunner.continueCurrentStep();
|
await TaskRunner.continueCurrentStep();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -831,8 +1411,11 @@
|
|||||||
|
|
||||||
TL.log(`BAF`, res);
|
TL.log(`BAF`, res);
|
||||||
|
|
||||||
// Start initial task checking if ready
|
// Resume task if performing or start new task checking if ready
|
||||||
if (data.bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) {
|
if (data.bot_status === BOT_STATUS.PERFORMING_TASKS && data.current_task) {
|
||||||
|
TL.log('INIT', 'Resuming interrupted task');
|
||||||
|
TaskRunner.stepRunner.resumeTask();
|
||||||
|
} else if (data.bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) {
|
||||||
TaskRunner.checkForNewTasks();
|
TaskRunner.checkForNewTasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user