您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Learns image-based reCAPTCHA & hCaptcha selections (auto-select known, highlight unknown, GUI to manage DB). Stores data via GM_setValue / GM_getValue.
当前为
// ==UserScript== // @name Universal Captcha Learner — FULL (reCAPTCHA + hCaptcha) // @namespace https://example.com // @version 1.3 // @description Learns image-based reCAPTCHA & hCaptcha selections (auto-select known, highlight unknown, GUI to manage DB). Stores data via GM_setValue / GM_getValue. // @author You // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js // @run-at document-end // ==/UserScript== (function(){ 'use strict'; const POLL_INTERVAL = 1500; // ms - how often to scan page for captchas const DB_PREFIX = 'captcha_learner_v1_'; const MAX_IMG_DIM = 160; // scale images for hashing const LOG = true; // set false to reduce console logs // ---------- Utilities ---------- function log(...args){ if(LOG) console.log('[CaptchaLearner]', ...args); } function gmGet(key, def){ try { const v = GM_getValue(key); return typeof v === 'undefined' ? def : v; } catch(e){ return def; } } function gmSet(key, val){ try { GM_setValue(key, val); } catch(e){ console.error(e); } } function gmList(){ try { return GM_listValues(); } catch(e){ return []; } } function gmDelete(key){ try { GM_deleteValue(key); } catch(e){ console.error(e); } } // ---------- UI: info box + control panel ---------- const INFO_ID = 'captcha_learner_info_box'; const PANEL_ID = 'captcha_learner_control_panel'; function ensureStyles(){ if(document.getElementById('captcha_learner_css')) return; const css = ` #${INFO_ID}{ position:fixed; left:14px; top:14px; z-index:2147483647; background:#222; color:#fff; padding:12px 14px; border-radius:10px; font-family:Arial, sans-serif; font-size:13px; box-shadow:0 6px 18px rgba(0,0,0,0.4); max-width:320px; } #${INFO_ID} .title{ font-weight:700; margin-bottom:6px; color:#fff; } #${INFO_ID} .msg{ color:#ddd; font-size:12px; line-height:1.35; } #${PANEL_ID}{ position:fixed; right:14px; bottom:14px; z-index:2147483647; background:#fff; color:#111; padding:12px; border-radius:10px; width:420px; font-family:Arial, sans-serif; font-size:13px; box-shadow:0 6px 18px rgba(0,0,0,0.15); } #${PANEL_ID} .row{ margin-bottom:8px; } #${PANEL_ID} textarea{ width:100%; height:120px; font-family:monospace; font-size:12px; } #captcha_learner_db_list{ max-height:200px; overflow:auto; border:1px solid #eee; padding:6px; border-radius:6px; background:#fafafa; } .cl-entry{ padding:6px; border-bottom:1px dashed #eee; display:flex; justify-content:space-between; align-items:center; } .cl-entry .k{ font-weight:600; color:#333; } .cl-entry button{ margin-left:8px; } .cl-img-thumb{ max-width:60px; max-height:50px; border:1px solid #ccc; margin-right:6px; } .cl-highlight{ outline:3px solid #ff6b6b !important; } `; const style = document.createElement('style'); style.id = 'captcha_learner_css'; style.innerText = css; document.head.appendChild(style); } function ensureInfoBox(){ if(document.getElementById(INFO_ID)) return; ensureStyles(); const div = document.createElement('div'); div.id = INFO_ID; div.innerHTML = '<div class="title">Captcha Learner</div><div class="msg">Scanning page...</div>'; document.body.appendChild(div); } function updateInfo(msg){ ensureInfoBox(); const el = document.getElementById(INFO_ID); el.querySelector('.msg').innerText = msg; } function ensureControlPanel(){ if(document.getElementById(PANEL_ID)) return; ensureStyles(); const panel = document.createElement('div'); panel.id = PANEL_ID; panel.innerHTML = ` <div style="font-weight:700;margin-bottom:8px">Captcha Learner — Settings & DB</div> <div class="row"> <label>Auto-scan interval (ms): <input id="cl_interval" type="number" value="${POLL_INTERVAL}" style="width:100px"></label> <label style="margin-left:10px"><input id="cl_autostart" type="checkbox" checked> Auto</label> </div> <div class="row"> <button id="cl_refresh_btn">Scan now</button> <button id="cl_export_btn">Export DB</button> <button id="cl_import_btn">Import DB</button> <button id="cl_clear_btn" style="float:right;background:#d9534f;color:#fff;border:none;padding:6px 10px;border-radius:6px">Clear DB</button> </div> <div class="row"><div id="captcha_learner_db_list"></div></div> <div class="row"> <div style="font-size:12px;color:#666">Tip: click an unknown image on captcha to teach the label. Stored locally via GM_setValue.</div> </div> <div style="margin-top:6px"><textarea id="cl_export_area" placeholder="Paste JSON here to import"></textarea></div> `; document.body.appendChild(panel); document.getElementById('cl_refresh_btn').addEventListener('click', ()=> scanOnce(true)); document.getElementById('cl_export_btn').addEventListener('click', exportDB); document.getElementById('cl_import_btn').addEventListener('click', importDBFromText); document.getElementById('cl_clear_btn').addEventListener('click', clearDB); } // ---------- Image hashing / base64 ---------- function downscaleImgToCanvas(img){ // returns dataURL resized try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // scale preserving aspect ratio 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(20, Math.round(w * scale)); const nh = Math.max(20, Math.round(h * scale)); canvas.width = nw; canvas.height = nh; ctx.drawImage(img, 0, 0, nw, nh); // use jpeg small quality return canvas.toDataURL('image/jpeg', 0.45); } catch(e){ log('downscale error', e); return null; } } // ---------- DB helpers ---------- function dbKeyForLabel(label){ return DB_PREFIX + label.replace(/\s+/g, '_').toLowerCase(); } function dbGet(label){ return gmGet(dbKeyForLabel(label), []); } function dbAdd(label, dataUrl){ const key = dbKeyForLabel(label); let arr = gmGetValueSafe(key, []); if(!Array.isArray(arr)) arr = []; if(!arr.includes(dataUrl)){ arr.push(dataUrl); gmSet(key, arr); log('DB add', label); } } function dbRemove(label){ gmDelete(dbKeyForLabel(label)); } function dbList(){ const keys = gmList(); const out = []; for(const k of keys){ if(k.startsWith(DB_PREFIX)){ out.push({key:k, label:k.slice(DB_PREFIX.length), count: (gmGetValueSafe(k,[])).length}); } } return out; } function gmGetValueSafe(k, def){ try{ const v = GM_getValue(k); return typeof v === 'undefined' ? def : v; } catch(e){ return def; } } // ---------- Captcha detection & processing ---------- function findReCaptchaWidget(){ // reCAPTCHA image challenge tiles have class rc-image-tile-wrapper containing <img> const tiles = document.querySelectorAll('.rc-imageselect-tile-wrapper img, .rc-image-tile-wrapper img'); const instruct = document.querySelector('.rc-imageselect-instructions, .rc-imageselect-instructions div, .rc-imageselect-desc-no-canonical'); if(tiles && tiles.length && instruct){ return {type:'recaptcha', tiles:Array.from(tiles), label: instruct.innerText.trim()}; } return null; } function findHcaptchaWidget(){ // hCaptcha images are inside .task-image or .image-grid const tiles = document.querySelectorAll('.task-image img, .image-grid img, .challenge-image img'); const instruct = document.querySelector('.prompt-text, .caption-text'); if(tiles && tiles.length && instruct){ return {type:'hcaptcha', tiles:Array.from(tiles), label: instruct.innerText.trim()}; } return null; } function highlightNode(node){ try{ node.classList.add('cl-highlight'); }catch(e){} } function attachLearnClick(imgEl, label){ // on click, add to DB and give feedback const handler = (ev)=>{ ev.stopPropagation(); ev.preventDefault(); const data = downscaleImgToCanvas(imgEl); if(!data) return; dbAdd(label, data); updateInfo('Learned image for "'+label+'" — saved.'); // remove highlight imgEl.classList.remove('cl-highlight'); // remove handler to prevent duplicates imgEl.removeEventListener('click', handler); }; imgEl.addEventListener('click', handler); } function tryAutoSelect(tileImg, label){ const data = downscaleImgToCanvas(tileImg); if(!data) return false; const arr = dbGet(label) || []; if(arr.includes(data)){ // attempt to click the tile container const clickable = tileImg.closest('button, td, div, a, span') || tileImg; try { clickable.click(); } catch(e){ tileImg.click(); } 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{ const ok = tryAutoSelect(img, widget.label); if(ok) matched++; else { highlightNode(img); attachLearnClick(img, widget.label); } }catch(e){ log('process tile error', e); } } if(matched) updateInfo('Auto-selected '+matched+' images for "'+widget.label+'"'); } // ---------- Scanning loop ---------- let POLLER = null; function scanOnce(force){ try{ const rec = findReCaptchaWidget(); if(rec){ processWidget(rec); return; } const h = findHcaptchaWidget(); if(h){ processWidget(h); return; } if(force) updateInfo('No known captcha widget found.'); }catch(e){ log('scanOnce err', e); } } function startPoller(interval){ if(POLLER) clearInterval(POLLER); POLLER = setInterval(()=> scanOnce(false), interval || POLL_INTERVAL); } // ---------- Control panel actions ---------- function renderDBList(){ const container = document.getElementById('captcha_learner_db_list'); if(!container) return; container.innerHTML = ''; const items = dbList(); if(items.length === 0) { container.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'; // show small thumb from first image const arr = gmGetValueSafe(it.key, []); const thumb = document.createElement('img'); thumb.className = 'cl-img-thumb'; thumb.src = arr && arr[0] ? arr[0] : ''; left.appendChild(thumb); const title = document.createElement('div'); title.innerHTML = '<div class="k">'+it.label+'</div><div style="font-size:11px;color:#666">'+it.count+' images</div>'; left.appendChild(title); row.appendChild(left); const controls = document.createElement('div'); const del = document.createElement('button'); del.textContent='Delete'; del.style.padding='6px 8px'; del.addEventListener('click', ()=>{ if(confirm('Delete "'+it.label+'"?')){ dbRemove(it.label); renderDBList(); }}); const view = document.createElement('button'); view.textContent='View'; view.style.marginLeft='8px'; view.style.padding='6px 8px'; view.addEventListener('click', ()=>{ const arr = gmGetValueSafe(it.key,[]); document.getElementById('cl_export_area').value = JSON.stringify({label:it.label, images:arr}, null, 2); }); controls.appendChild(view); controls.appendChild(del); row.appendChild(controls); container.appendChild(row); } } function exportDB(){ // build JSON of all DB items const items = dbList(); const out = {}; for(const it of items){ out[it.label] = gmGetValueSafe(DB_PREFIX + it.label, []); } const txt = JSON.stringify(out, null, 2); document.getElementById('cl_export_area').value = txt; updateInfo('DB exported to textarea. You can copy and save it.'); } function importDBFromText(){ const txt = document.getElementById('cl_export_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]); } updateInfo('Imported DB entries: '+Object.keys(parsed).length); renderDBList(); }catch(e){ alert('Invalid JSON: '+e.message); } } function clearDB(){ if(!confirm('Clear all learned DB entries?')) return; const keys = gmList(); for(const k of keys){ if(k.startsWith(DB_PREFIX)) gmDelete(k); } renderDBList(); updateInfo('DB cleared'); } // ---------- Init ---------- function init(){ ensureInfoBox(); ensureControlPanel(); renderDBList(); // start poller const intervalInput = document.getElementById('cl_interval'); intervalInput.addEventListener('change', ()=> { const v = parseInt(intervalInput.value) || POLL_INTERVAL; startPoller(v); }); document.getElementById('cl_autostart').checked = true; startPoller(POLL_INTERVAL); // refresh db list periodically setInterval(renderDBList, 4000); // one initial scan setTimeout(()=> scanOnce(true), 1500); } // start init(); })();