您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automates applying to frontend web developer jobs on a job board.
// ==UserScript== // @name Joblum Board Auto-Apply // @namespace http://tampermonkey.net/ // @version 1.0 // @description Automates applying to frontend web developer jobs on a job board. // @author You // @match *://ru.joblum.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license MIT // ==/UserScript== ;(function () { 'use strict' // Default settings const defaultSettings = { positiveKeywords: 'frontend, front-end, react, nextjs, next.js, javascript, typescript, vue, angular', negativeKeywords: 'native, .NET, Python, Django, PHP, Laravel, mobile, iOS, Android', autoStart: false } // Job stats let jobStats = { jobsScanned: 0, titleMatches: 0, descriptionMatches: 0, applicationsSubmitted: 0, pagesProcessed: 0 } // State let processedJobUrls = new Set() let currentSearchPageUrl = null let isProcessing = false let stopRequested = false let pageChangeTimer = null // Load settings function loadSettings() { const settings = { positiveKeywords: GM_getValue( 'positiveKeywords', defaultSettings.positiveKeywords ), negativeKeywords: GM_getValue( 'negativeKeywords', defaultSettings.negativeKeywords ), autoStart: GM_getValue('autoStart', defaultSettings.autoStart) } return { positiveKeywords: settings.positiveKeywords .split(',') .map((k) => k.trim().toLowerCase()) .filter((k) => k), negativeKeywords: settings.negativeKeywords .split(',') .map((k) => k.trim().toLowerCase()) .filter((k) => k), autoStart: settings.autoStart } } // Load saved processed URLs function loadProcessedUrls() { const savedUrls = GM_getValue('processedJobUrls', '[]') try { processedJobUrls = new Set(JSON.parse(savedUrls)) } catch (e) { console.error('Error loading processed URLs:', e) processedJobUrls = new Set() } } // Save processed URLs function saveProcessedUrls() { GM_setValue('processedJobUrls', JSON.stringify([...processedJobUrls])) } let POSITIVE_KEYWORDS = loadSettings().positiveKeywords let NEGATIVE_KEYWORDS = loadSettings().negativeKeywords // Save settings function saveSettings(settings) { GM_setValue('positiveKeywords', settings.positiveKeywords) GM_setValue('negativeKeywords', settings.negativeKeywords) GM_setValue('autoStart', settings.autoStart) POSITIVE_KEYWORDS = settings.positiveKeywords .split(',') .map((k) => k.trim().toLowerCase()) .filter((k) => k) NEGATIVE_KEYWORDS = settings.negativeKeywords .split(',') .map((k) => k.trim().toLowerCase()) .filter((k) => k) } // Save stats function saveStats() { GM_setValue('jobStats', JSON.stringify(jobStats)) } // Load stats function loadStats() { const savedStats = GM_getValue('jobStats', null) if (savedStats) { jobStats = JSON.parse(savedStats) } } // Utility to check if text matches criteria function matchesCriteria(text) { if (!text) return false const lowerText = text.toLowerCase() const hasPositive = POSITIVE_KEYWORDS.some((keyword) => lowerText.includes(keyword) ) const hasNegative = NEGATIVE_KEYWORDS.some((keyword) => lowerText.includes(keyword) ) return hasPositive && !hasNegative } // Utility to wait for an element async function waitForElement(selector, timeout = 10000) { const start = Date.now() while (Date.now() - start < timeout) { const element = document.querySelector(selector) if (element) return element await new Promise((resolve) => setTimeout(resolve, 100)) } console.log(`Element not found after timeout: ${selector}`) return null } // Utility to wait for a condition async function waitForCondition(condition, timeout = 10000) { const start = Date.now() while (Date.now() - start < timeout) { if (condition()) return true if (stopRequested) return false await new Promise((resolve) => setTimeout(resolve, 100)) } console.log(`Condition not met after timeout`) return false } // Prevent links from opening in new tabs function preventNewTabs() { document.querySelectorAll('a[target="_blank"]').forEach((anchor) => { anchor.removeAttribute('target') }) } // Check if URL is a search results page function isSearchResultsPage() { return !!document.querySelector('.content-card.card-has-jobs') } // Check if URL is a job details page function isJobDetailsPage() { return !!( document.querySelector('h1.job-title') && document.querySelector('span[itemprop="description"]') ) } // Check if URL is an application form page function isApplicationFormPage() { return !!document.querySelector('form#w1') } // Determine current page type and start appropriate process async function determinePage() { if (stopRequested) return if (isSearchResultsPage()) { console.log('Detected search results page') await processSearchResults() } else if (isJobDetailsPage()) { console.log('Detected job details page') await processJobDetails(currentSearchPageUrl) } else if (isApplicationFormPage()) { console.log('Detected application form page') await processApplicationForm(currentSearchPageUrl) } else { console.log('Not on a recognized job board page') showNotification('Not on a job board page.', 'warning') isProcessing = false updateUI() } } // Utility to delay execution async function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } // Process search results page async function processSearchResults() { if (stopRequested) return preventNewTabs() currentSearchPageUrl = window.location.href console.log('Processing search page:', currentSearchPageUrl) GM_setValue('lastSearchPage', currentSearchPageUrl) // Wait 10 seconds for translation console.log('Waiting 10 seconds for page translation...') await delay(10000) // 10-second delay const jobWrappers = document.querySelectorAll('.result-wrp.row') console.log(`Found ${jobWrappers.length} job wrappers`) const jobs = [] jobStats.pagesProcessed++ saveStats() updateUI() for (const wrapper of jobWrappers) { if (stopRequested) return jobStats.jobsScanned++ const titleElement = wrapper.querySelector('.job-title a') if (titleElement) { const title = titleElement?.title || titleElement.textContent || '' const link = titleElement.href if (matchesCriteria(title)) { jobStats.titleMatches++ if (!processedJobUrls.has(link)) { console.log(`Found matching job: ${title}`) jobs.push({ link, title }) } else { console.log(`Skipping already processed job: ${title}`) } } } saveStats() updateUI() } if (jobs.length === 0) { console.log('No matching jobs found on this page, trying next page') await goToNextPage() return } console.log(`Found ${jobs.length} matching jobs to process`) await processNextJob(jobs, 0) } // Process jobs one by one async function processNextJob(jobs, index) { if (stopRequested || index >= jobs.length) { // If we've processed all jobs, go to next page if (!stopRequested && index >= jobs.length) { await goToNextPage() } return } const job = jobs[index] console.log(`Processing job ${index + 1}/${jobs.length}: ${job.title}`) // Mark as processed to avoid duplicates processedJobUrls.add(job.link) saveProcessedUrls() // Navigate to job details console.log(`Navigating to: ${job.link}`) window.location.href = job.link } // Process job details page async function processJobDetails(returnUrl) { if (stopRequested) return console.log('Processing job details page') preventNewTabs() const titleElement = await waitForElement('h1.job-title') const descriptionElement = await waitForElement( 'span[itemprop="description"]' ) if (!titleElement || !descriptionElement) { console.error('Job title or description not found') returnToSearchPage(returnUrl) return } const jobDetails = { title: titleElement.textContent || '', description: descriptionElement.textContent || '' } if ( matchesCriteria(jobDetails.title) && matchesCriteria(jobDetails.description) ) { jobStats.descriptionMatches++ saveStats() updateUI() console.log('Job matches criteria, attempting to apply') const respondButton = await waitForElement('.btn.btn-apply.btn-warning') if (respondButton) { respondButton.removeAttribute('target') respondButton.click() } else { console.error('Respond button not found') returnToSearchPage(returnUrl) } } else { console.log('Job does not match full criteria') returnToSearchPage(returnUrl) } } // Process application form page async function processApplicationForm(returnUrl) { if (stopRequested) return console.log('Processing application form') const submitButton = await waitForElement( 'button.btn.btn-primary[type="submit"]' ) if (submitButton) { submitButton.click() jobStats.applicationsSubmitted++ saveStats() updateUI() console.log('Application submitted successfully') showNotification('Application submitted!', 'success') // Wait for application submission to complete await waitForCondition( () => !window.location.href.includes('/candidate/apply'), 10000 ) returnToSearchPage(returnUrl) } else { console.error('Submit button not found') showNotification('Failed to submit application.', 'error') returnToSearchPage(returnUrl) } } // Helper function to return to search page and continue processing function returnToSearchPage(returnUrl) { const lastSearchPage = GM_getValue('lastSearchPage', null) if (returnUrl) { console.log(`Returning to search page: ${returnUrl}`) window.location.href = returnUrl } else if (lastSearchPage) { console.log(`Returning to last known search page: ${lastSearchPage}`) window.location.href = lastSearchPage } else { console.error('No return URL provided and no last search page saved') isProcessing = false updateUI() } } // Navigate to next page async function goToNextPage() { if (stopRequested) return console.log('Looking for next page') const nextLink = document.querySelector('.pagination .next a') if (nextLink) { console.log('Found next page link, clicking...') nextLink.click() } else { console.log('No more pages to process') stopRequested = true isProcessing = false updateUI() showNotification('No more pages to process. Process complete!', 'info') } } // Monitor for page changes to automatically continue workflow function setupPageChangeMonitor() { let lastUrl = window.location.href // Clear any existing timer if (pageChangeTimer) { clearInterval(pageChangeTimer) } pageChangeTimer = setInterval(() => { if (window.location.href !== lastUrl) { console.log(`Page changed from ${lastUrl} to ${window.location.href}`) lastUrl = window.location.href // If we're still processing, determine the current page and continue if (isProcessing && !stopRequested) { // Give the page a moment to load setTimeout(() => determinePage(), 1000) } } }, 500) } // Main function async function main() { if (isProcessing) { console.log('Already processing, ignoring start request') return } isProcessing = true stopRequested = false updateUI() showNotification('Workflow started.', 'success') // Load saved processed URLs loadProcessedUrls() try { // Set up page change monitoring setupPageChangeMonitor() // Start processing the current page await determinePage() } catch (error) { console.error('Error in main function:', error) showNotification('An error occurred: ' + error.message, 'error') isProcessing = false updateUI() } } // Reset function function resetStats() { jobStats = { jobsScanned: 0, titleMatches: 0, descriptionMatches: 0, applicationsSubmitted: 0, pagesProcessed: 0 } saveStats() updateUI() showNotification('Statistics reset.', 'info') } // Reset processed jobs function resetProcessedJobs() { processedJobUrls = new Set() saveProcessedUrls() showNotification('Processed jobs list cleared.', 'info') } // Stop processing function stop() { stopRequested = true isProcessing = false if (pageChangeTimer) { clearInterval(pageChangeTimer) pageChangeTimer = null } updateUI() showNotification('Workflow stopped.', 'info') } // Notification system function showNotification(message, type) { const existing = document.querySelector('.job-auto-apply-notification') if (existing) existing.remove() const notification = document.createElement('div') notification.className = `job-auto-apply-notification notification-${type}` notification.textContent = message Object.assign(notification.style, { position: 'fixed', top: '10px', left: '50%', transform: 'translateX(-50%)', padding: '10px 20px', borderRadius: '4px', zIndex: '1000', color: 'white' }) switch (type) { case 'success': notification.style.backgroundColor = '#4CAF50' break case 'error': notification.style.backgroundColor = '#F44336' break case 'warning': notification.style.backgroundColor = '#FF9800' break case 'info': notification.style.backgroundColor = '#2196F3' break } document.body.appendChild(notification) setTimeout(() => notification.remove(), 3000) } // UI Creation function createUI() { GM_addStyle(` .job-auto-apply-panel { position: fixed; top: 10px; right: 10px; width: 350px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 9999; font-family: 'Segoe UI', Arial, sans-serif; padding: 15px; } .job-auto-apply-panel h1 { font-size: 18px; margin: 0 0 10px; color: #333; } .status-container { display: flex; align-items: center; margin-bottom: 15px; padding: 10px; background: #f9f9f9; border-radius: 4px; } .status-indicator { width: 12px; height: 12px; border-radius: 50%; margin-right: 5px; } .status-active { background: #4caf50; } .status-inactive { background: #f44336; } .button-container { display: flex; gap: 10px; margin-bottom: 15px; } .job-auto-apply-panel button { flex: 1; padding: 8px; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; transition: background 0.2s; } .job-auto-apply-panel button:hover { opacity: 0.9; } .job-auto-apply-panel button:active { transform: translateY(1px); } #startButton { background: #4caf50; color: white; } #stopButton { background: #f44336; color: white; } #settingsButton { background: #2196f3; color: white; } .stats-container { background: #f9f9f9; border-radius: 4px; padding: 10px; margin-bottom: 15px; } .stats-title { font-weight: 600; margin-bottom: 8px; } .stat-item { display: flex; justify-content: space-between; margin-bottom: 5px; } .stat-value { font-weight: 600; } .settings-panel { display: none; margin-top: 15px; } .settings-panel textarea { width: 100%; height: 80px; margin-bottom: 10px; border-radius: 4px; border: 1px solid #ddd; padding: 5px; } .settings-panel label { display: block; margin-bottom: 5px; font-weight: 500; } .auto-start-checkbox { margin: 10px 0; } .advanced-container { margin-top: 10px; } .advanced-button-container { display: flex; gap: 10px; margin-top: 10px; } #resetStatsButton { background: #ff9800; color: white; } #resetJobsButton { background: #9c27b0; color: white; } `) const panel = document.createElement('div') panel.className = 'job-auto-apply-panel' panel.innerHTML = ` <h1>Job Application Assistant</h1> <div class="status-container"> <div id="statusIndicator" class="status-indicator status-inactive"></div> <div id="statusText">Workflow is not running</div> </div> <div class="button-container"> <button id="startButton">Start</button> <button id="stopButton">Stop</button> <button id="settingsButton">Settings</button> </div> <div class="stats-container"> <div class="stats-title">Statistics</div> <div class="stat-item"><div>Jobs Scanned:</div><div id="jobsScanned" class="stat-value">0</div></div> <div class="stat-item"><div>Title Matches:</div><div id="titleMatches" class="stat-value">0</div></div> <div class="stat-item"><div>Description Matches:</div><div id="descriptionMatches" class="stat-value">0</div></div> <div class="stat-item"><div>Applications Submitted:</div><div id="applicationsSubmitted" class="stat-value">0</div></div> <div class="stat-item"><div>Pages Processed:</div><div id="pagesProcessed" class="stat-value">0</div></div> </div> <div class="advanced-container"> <div class="advanced-button-container"> <button id="resetStatsButton">Reset Stats</button> <button id="resetJobsButton">Clear Job History</button> </div> </div> <div class="settings-panel" id="settingsPanel"> <label for="positiveKeywords">Positive Keywords:</label> <textarea id="positiveKeywords"></textarea> <label for="negativeKeywords">Negative Keywords:</label> <textarea id="negativeKeywords"></textarea> <div class="auto-start-checkbox"> <input type="checkbox" id="autoStart"> <label for="autoStart">Auto-start on page load</label> </div> <div class="button-container"> <button id="saveSettings">Save</button> <button id="cancelSettings">Cancel</button> </div> </div> ` document.body.appendChild(panel) // Event listeners document ?.getElementById('startButton') ?.addEventListener('click', () => main()) document ?.getElementById('stopButton') ?.addEventListener('click', () => stop()) document ?.getElementById('resetStatsButton') ?.addEventListener('click', () => resetStats()) document ?.getElementById('resetJobsButton') ?.addEventListener('click', () => resetProcessedJobs()) document ?.getElementById('settingsButton') ?.addEventListener('click', () => { const settingsPanel = document.getElementById('settingsPanel') settingsPanel.style.display = settingsPanel?.style.display === 'block' ? 'none' : 'block' if (settingsPanel?.style.display === 'block') { document.getElementById('positiveKeywords').value = GM_getValue( 'positiveKeywords', defaultSettings.positiveKeywords ) document.getElementById('negativeKeywords').value = GM_getValue( 'negativeKeywords', defaultSettings.negativeKeywords ) document.getElementById('autoStart').checked = GM_getValue( 'autoStart', defaultSettings.autoStart ) } }) document.getElementById('saveSettings')?.addEventListener('click', () => { const settings = { positiveKeywords: document .getElementById('positiveKeywords') ?.value.trim(), negativeKeywords: document .getElementById('negativeKeywords') ?.value.trim(), autoStart: document.getElementById('autoStart')?.checked } if (!settings.positiveKeywords) { showNotification( 'Please enter at least one positive keyword.', 'warning' ) return } saveSettings(settings) document.getElementById('settingsPanel').style.display = 'none' showNotification('Settings saved!', 'success') }) document.getElementById('cancelSettings')?.addEventListener('click', () => { document.getElementById('settingsPanel').style.display = 'none' }) } // Update UI function updateUI() { document.getElementById('statusIndicator').className = `status-indicator ${ isProcessing ? 'status-active' : 'status-inactive' }` document.getElementById('statusText').textContent = isProcessing ? 'Workflow is running' : 'Workflow is not running' document.getElementById('jobsScanned').textContent = jobStats.jobsScanned document.getElementById('titleMatches').textContent = jobStats.titleMatches document.getElementById('descriptionMatches').textContent = jobStats.descriptionMatches document.getElementById('applicationsSubmitted').textContent = jobStats.applicationsSubmitted document.getElementById('pagesProcessed').textContent = jobStats.pagesProcessed } // Initialize function initialize() { loadStats() loadProcessedUrls() createUI() updateUI() setupPageChangeMonitor() const settings = loadSettings() if (settings.autoStart && !isProcessing) { // Small delay to ensure page is fully loaded setTimeout(() => main(), 1500) } } initialize() })()