您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Learns image-based reCAPTCHA & hCaptcha selections (auto-select known, highlight unknown, GUI to manage DB). Panel is movable/collapsible and position is remembered. Can auto-clear DB on start.
// ==UserScript== // @name Universal Captcha Learner — FULL with Movable Panel & Auto-Clear // @namespace https://example.com // @version 1.4 // @description Learns image-based reCAPTCHA & hCaptcha selections (auto-select known, highlight unknown, GUI to manage DB). Panel is movable/collapsible and position is remembered. Can auto-clear DB on start. // @author You // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @run-at document-end // ==/UserScript== (function () { 'use strict'; /* ----------------- Конфигурация ----------------- */ const DB_PREFIX = 'captcha_learner_v1_'; const DEFAULT_POLL = 1500; // ms const MAX_IMG_DIM = 160; // px (для downscale) const MIN_IMG_DIM = 20; const AUTO_START_DEFAULT = true; const SETTINGS_KEY = 'cl_settings_v1'; /* ------------------------------------------------ */ // ---------- helpers для GM storage ---------- function gmSet(key, val) { try { GM_setValue(key, val); } catch (e) { console.error(e); } } function gmGet(key, def) { try { const v = GM_getValue(key); return typeof v === 'undefined' ? def : v; } catch (e) { return def; } } function gmList() { try { return GM_listValues(); } catch (e) { return []; } } function gmDelete(key) { try { GM_deleteValue(key); } catch (e) { console.error(e); } } // Загрузить/сохранить общие настройки панели function loadSettings() { const defaultSettings = { pollInterval: DEFAULT_POLL, autoStart: AUTO_START_DEFAULT, autoClearOnStart: false, collapsed: false, left: null, top: null }; try { const s = GM_getValue(SETTINGS_KEY); if (!s) { gmSet(SETTINGS_KEY, defaultSettings); return defaultSettings; } return Object.assign({}, defaultSettings, s); } catch (e) { return defaultSettings; } } function saveSettings(s) { gmSet(SETTINGS_KEY, s); } let SETTINGS = loadSettings(); // ---------- UI: стили, панель (movable, collapse) ---------- const INFO_ID = 'cl_info_box'; const PANEL_ID = 'cl_panel'; function ensureStyles() { if (document.getElementById('cl_css')) return; const css = ` #${INFO_ID}{ position:fixed; left:14px; top:14px; z-index:2147483647; background:#111; color:#fff; padding:10px 12px; border-radius:10px; font-family:Arial, sans-serif; font-size:13px; box-shadow:0 6px 20px rgba(0,0,0,0.4); max-width:340px;} #${PANEL_ID}{ position:fixed; right:14px; bottom:14px; z-index:2147483647; width:420px; font-family:Arial, sans-serif; font-size:13px; box-shadow:0 6px 20px rgba(0,0,0,0.2); border-radius:10px; overflow:hidden; background:#fff;} #${PANEL_ID} .header{ background:#0f4c81; color:#fff; padding:8px 10px; cursor:move; display:flex; align-items:center; justify-content:space-between;} #${PANEL_ID} .body{ padding:10px; background:#fff; color:#111;} #${PANEL_ID} input[type="number"]{ width:90px; } #${PANEL_ID} textarea{ width:100%; height:120px; font-family:monospace; font-size:12px; margin-top:6px; } #cl_db_list{ max-height:200px; overflow:auto; border:1px solid #eee; padding:6px; border-radius:6px; background:#fafafa; } .cl_entry{ display:flex; justify-content:space-between; align-items:center; padding:6px; border-bottom:1px dashed #eee; } .cl_thumb{ max-width:54px; max-height:44px; border:1px solid #ccc; margin-right:8px; } .cl_highlight{ outline:3px solid #ff9a3c !important; } .cl_btn{ padding:6px 8px; margin-left:6px; border-radius:6px; border:none; cursor:pointer; } .cl_btn_red{ background:#d9534f; color:#fff; } .cl_btn_primary{ background:#0f4c81; color:#fff; } `; const st = document.createElement('style'); st.id = 'cl_css'; st.innerText = css; document.head.appendChild(st); } function ensureInfoBox() { if (document.getElementById(INFO_ID)) return; ensureStyles(); const el = document.createElement('div'); el.id = INFO_ID; el.innerHTML = `<div style="font-weight:700;margin-bottom:6px">Captcha Learner</div><div id="cl_info_msg">Сканирование страницы...</div>`; document.body.appendChild(el); } function updateInfo(msg) { ensureInfoBox(); const el = document.getElementById('cl_info_msg'); if (el) el.textContent = msg; } // Позиция и состояние панели function ensurePanel() { if (document.getElementById(PANEL_ID)) return; ensureStyles(); const panel = document.createElement('div'); panel.id = PANEL_ID; panel.innerHTML = ` <div class="header"><div style="font-weight:700">Captcha Learner — Controls</div> <div> <button id="cl_toggle_btn" class="cl_btn cl_btn_primary">–</button> </div> </div> <div class="body" id="cl_body"> <div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;"> <label>Interval(ms): <input id="cl_interval" type="number" value="${SETTINGS.pollInterval}" /></label> <label><input id="cl_autostart" type="checkbox" ${SETTINGS.autoStart ? 'checked' : ''}/> Auto</label> <label style="margin-left:8px"><input id="cl_autoclear" type="checkbox" ${SETTINGS.autoClearOnStart ? 'checked' : ''}/> Auto-clear on start</label> </div> <div style="display:flex;gap:8px;margin-bottom:8px;"> <button id="cl_scan_btn" class="cl_btn cl_btn_primary">Scan now</button> <button id="cl_export_btn" class="cl_btn">Export DB</button> <button id="cl_import_btn" class="cl_btn">Import DB</button> <button id="cl_clear_btn" class="cl_btn cl_btn_red">Clear DB</button> </div> <div id="cl_db_list" style="margin-bottom:8px"></div> <textarea id="cl_import_area" placeholder='JSON for import / exported JSON will appear here'></textarea> </div> `; document.body.appendChild(panel); // restore position if saved if (SETTINGS.left && SETTINGS.top) { panel.style.left = SETTINGS.left; panel.style.top = SETTINGS.top; panel.style.right = 'auto'; panel.style.bottom = 'auto'; } // collapsed state const body = panel.querySelector('#cl_body'); const toggleBtn = panel.querySelector('#cl_toggle_btn'); function setCollapsed(v) { SETTINGS.collapsed = !!v; saveSettings(SETTINGS); body.style.display = v ? 'none' : 'block'; toggleBtn.textContent = v ? '+' : '–'; } setCollapsed(SETTINGS.collapsed); toggleBtn.addEventListener('click', () => setCollapsed(!SETTINGS.collapsed)); // drag let dragging = false, dx = 0, dy = 0; const header = panel.querySelector('.header'); header.addEventListener('mousedown', (e) => { dragging = true; dx = e.clientX - panel.offsetLeft; dy = e.clientY - panel.offsetTop; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!dragging) return; panel.style.left = (e.clientX - dx) + 'px'; panel.style.top = (e.clientY - dy) + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { if (dragging) { dragging = false; SETTINGS.left = panel.style.left; SETTINGS.top = panel.style.top; saveSettings(SETTINGS); } }); // controls hookup document.getElementById('cl_scan_btn').addEventListener('click', () => scanOnce(true)); document.getElementById('cl_export_btn').addEventListener('click', exportDB); document.getElementById('cl_import_btn').addEventListener('click', importDBFromTextarea); document.getElementById('cl_clear_btn').addEventListener('click', () => { if (!confirm('Clear ALL learned DB entries?')) return; clearDB(); }); // settings inputs document.getElementById('cl_interval').addEventListener('change', (e) => { const v = Math.max(200, parseInt(e.target.value) || DEFAULT_POLL); SETTINGS.pollInterval = v; saveSettings(SETTINGS); restartPoller(); }); document.getElementById('cl_autostart').addEventListener('change', (e) => { SETTINGS.autoStart = !!e.target.checked; saveSettings(SETTINGS); if (SETTINGS.autoStart) startPoller(); else stopPoller(); }); document.getElementById('cl_autoclear').addEventListener('change', (e) => { SETTINGS.autoClearOnStart = !!e.target.checked; saveSettings(SETTINGS); }); } // ---------- DB helpers ---------- function dbKey(label) { return DB_PREFIX + label.replace(/\s+/g, '_').toLowerCase(); } function dbGet(label) { return gmGet(dbKey(label), []); } function dbSet(label, arr) { gmSet(dbKey(label), arr); } function dbAdd(label, dataUrl) { const key = dbKey(label); let arr = gmGet(key, []); if (!Array.isArray(arr)) arr = []; if (!arr.includes(dataUrl)) { arr.push(dataUrl); gmSet(key, arr); } } function dbRemove(label) { gmDelete(dbKey(label)); } function dbList() { const keys = gmList(); const out = []; for (const k of keys) { if (k.startsWith(DB_PREFIX)) { const label = k.slice(DB_PREFIX.length); const arr = gmGet(k, []); out.push({ label: label, count: (Array.isArray(arr) ? arr.length : 0) }); } } return out; } function clearDB() { const keys = gmList(); for (const k of keys) { if (k.startsWith(DB_PREFIX)) gmDelete(k); } renderDBList(); updateInfo('DB cleared'); } function exportDB() { const items = dbList(); const out = {}; for (const it of items) { out[it.label] = gmGet(DB_PREFIX + it.label, []); } document.getElementById('cl_import_area').value = JSON.stringify(out, null, 2); updateInfo('Exported DB to textarea'); } function importDBFromTextarea() { const txt = document.getElementById('cl_import_area').value.trim(); if (!txt) return alert('Paste JSON into textarea first'); try { const parsed = JSON.parse(txt); for (const k in parsed) { const key = DB_PREFIX + k; gmSet(key, parsed[k]); } renderDBList(); updateInfo('Imported ' + Object.keys(parsed).length + ' entries'); } catch (e) { alert('Invalid JSON: ' + e.message); } } function renderDBList() { const c = document.getElementById('cl_db_list'); if (!c) return; c.innerHTML = ''; const items = dbList(); if (items.length === 0) { c.innerHTML = '<div style="color:#888;padding:8px">DB empty</div>'; return; } for (const it of items) { const row = document.createElement('div'); row.className = 'cl_entry'; const left = document.createElement('div'); left.style.display = 'flex'; left.style.alignItems = 'center'; const arr = gmGet(DB_PREFIX + it.label, []); const thumb = document.createElement('img'); thumb.className = 'cl_thumb'; thumb.src = arr && arr[0] ? arr[0] : ''; left.appendChild(thumb); const t = document.createElement('div'); t.innerHTML = `<div style="font-weight:700">${it.label}</div><div style="font-size:12px;color:#666">${it.count} images</div>`; left.appendChild(t); row.appendChild(left); const btns = document.createElement('div'); const view = document.createElement('button'); view.textContent = 'View'; view.className='cl_btn'; view.onclick = () => { document.getElementById('cl_import_area').value = JSON.stringify({ label: it.label, images: gmGet(DB_PREFIX + it.label, []) }, null, 2); }; const del = document.createElement('button'); del.textContent = 'Delete'; del.className='cl_btn cl_btn_red'; del.onclick = () => { if (confirm('Delete "' + it.label + '"?')) { dbRemove(it.label); renderDBList(); } }; btns.appendChild(view); btns.appendChild(del); row.appendChild(btns); c.appendChild(row); } } // ---------- image downscale & signature ---------- function downscaleToDataUrl(img) { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const w = img.naturalWidth || img.width; const h = img.naturalHeight || img.height; const scale = Math.min(1, MAX_IMG_DIM / Math.max(w, h)); const nw = Math.max(MIN_IMG_DIM, Math.round(w * scale)); const nh = Math.max(MIN_IMG_DIM, Math.round(h * scale)); canvas.width = nw; canvas.height = nh; ctx.drawImage(img, 0, 0, nw, nh); return canvas.toDataURL('image/jpeg', 0.45); } catch (e) { console.error('downscale error', e); return null; } } // ---------- detection of widgets ---------- function findRecaptcha() { // standard tiles const tiles = document.querySelectorAll('.rc-imageselect-tile-wrapper img, .rc-image-tile-wrapper img'); // instruction node const instr = document.querySelector('.rc-imageselect-instructions, .rc-imageselect-desc-no-canonical, .rc-imageselect-instructions div'); if (tiles && tiles.length && instr) { return { type: 'recaptcha', tiles: Array.from(tiles), label: instr.innerText.trim() }; } return null; } function findHcaptcha() { const tiles = document.querySelectorAll('.task-image img, .image-grid img, .challenge-image img'); const instr = document.querySelector('.prompt-text, .caption-text'); if (tiles && tiles.length && instr) { return { type: 'hcaptcha', tiles: Array.from(tiles), label: instr.innerText.trim() }; } return null; } function highlight(el) { try { el.classList.add('cl_highlight'); } catch (e) {} } function unhighlight(el) { try { el.classList.remove('cl_highlight'); } catch (e) {} } function attachLearnHandler(img, label) { const handler = function (ev) { ev.stopPropagation(); ev.preventDefault(); const data = downscaleToDataUrl(img); if (!data) return; dbAdd(label, data); updateInfo('Learned image for "' + label + '"'); unhighlight(img); img.removeEventListener('click', handler); renderDBList(); }; img.addEventListener('click', handler); } function tryAutoClick(img, label) { const data = downscaleToDataUrl(img); if (!data) return false; const arr = dbGet(label) || []; if (arr.includes(data)) { // try to click container or img itself const container = img.closest('button, td, div, a, span') || img; try { container.click(); } catch (e) { try { img.click(); } catch (e2) {} } return true; } return false; } function processWidget(widget) { if (!widget) return; updateInfo(widget.type + ' — "' + widget.label + '" — ' + widget.tiles.length + ' images'); let matched = 0; for (const img of widget.tiles) { try { unhighlight(img); const ok = tryAutoClick(img, widget.label); if (ok) matched++; else { highlight(img); attachLearnHandler(img, widget.label); } } catch (e) { console.error('process tile error', e); } } if (matched) updateInfo('Auto-selected ' + matched + ' images for "' + widget.label + '"'); renderDBList(); } // ---------- scanning loop ---------- let POLLER = null; function scanOnce(force) { try { const r = findRecaptcha(); if (r) { processWidget(r); return; } const h = findHcaptcha(); if (h) { processWidget(h); return; } if (force) updateInfo('No image captcha found on page'); } catch (e) { console.error('scanOnce', e); } } function startPoller() { stopPoller(); POLLER = setInterval(() => scanOnce(false), SETTINGS.pollInterval || DEFAULT_POLL); updateInfo('Polling every ' + (SETTINGS.pollInterval || DEFAULT_POLL) + ' ms'); } function stopPoller() { if (POLLER) { clearInterval(POLLER); POLLER = null; updateInfo('Polling stopped'); } } function restartPoller() { if (SETTINGS.autoStart) startPoller(); } // ---------- init ---------- function init() { ensureInfoBox(); ensurePanel(); renderDBList(); // auto-clear on start? if (SETTINGS.autoClearOnStart) { if (confirm('Auto-clear DB on start is enabled. Clear DB now?')) { clearDB(); } } // auto start poller if enabled if (SETTINGS.autoStart) startPoller(); // initial scan after short delay setTimeout(() => scanOnce(true), 1200); // refresh DB list periodically (in case learned) setInterval(renderDBList, 3500); } // run init(); })();