Universal Captcha Learner — FULL (reCAPTCHA + hCaptcha)

Learns image-based reCAPTCHA & hCaptcha selections (auto-select known, highlight unknown, GUI to manage DB). Stores data via GM_setValue / GM_getValue.

当前为 2025-09-30 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();

})();