// ==UserScript== // @name 自动识别填充网页验证码 // @namespace http://tampermonkey.net/ // @version 1.0.3 // @description 可以自动识别和填充数字,英文的验证码,目前支持黑白名单,全局开关,速率限制。同时,为了避免数据安全风险,可以自定义Anticap和ddddocr的api地址,改成自部署的ocr识别 // @author trah01 // @license MIT // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @connect 127.0.0.1 // @connect * // @downloadURL https://update.greasyfork.icu/scripts/571759/%E8%87%AA%E5%8A%A8%E8%AF%86%E5%88%AB%E5%A1%AB%E5%85%85%E7%BD%91%E9%A1%B5%E9%AA%8C%E8%AF%81%E7%A0%81.user.js // @updateURL https://update.greasyfork.icu/scripts/571759/%E8%87%AA%E5%8A%A8%E8%AF%86%E5%88%AB%E5%A1%AB%E5%85%85%E7%BD%91%E9%A1%B5%E9%AA%8C%E8%AF%81%E7%A0%81.meta.js // ==/UserScript== (function () { 'use strict'; // 默认配置管理 const config = { apiProvider: GM_getValue('apiProvider', 'anticap'), // 'anticap' 或者是 'ddddocr' ocrApiUrl: GM_getValue('ocrApiUrl', 'http://127.0.0.1:8000/api/ocr'), ocrApiUrlDdddocr: GM_getValue('ocrApiUrlDdddocr', 'http://127.0.0.1:9898/ocr'), apiToken: GM_getValue('apiToken', ''), globalEnable: GM_getValue('globalEnable', true), rules: GM_getValue('rules', {}), rateLimit: GM_getValue('rateLimit', 30) }; // 注册油猴菜单 GM_registerMenuCommand(`打开可视化设置面板`, () => { createSettingsPanel(); }); function createSettingsPanel() { if (document.getElementById('autocaptcha-settings-root')) return; const container = document.createElement('div'); container.id = 'autocaptcha-settings-root'; container.style.position = 'fixed'; container.style.top = '0'; container.style.left = '0'; container.style.width = '100vw'; container.style.height = '100vh'; container.style.zIndex = '2147483647'; // 始终在最上层 container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.justifyContent = 'center'; container.style.backgroundColor = 'rgba(0, 0, 0, 0.4)'; container.style.backdropFilter = 'blur(4px)'; container.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'; const shadow = container.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ` * { box-sizing: border-box; } .panel { background: #ffffff; width: 380px; max-height: 80vh; overflow-y: auto; border-radius: 12px; padding: 24px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); } .panel::-webkit-scrollbar { width: 6px; } .panel::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); border-radius: 3px; } @media (prefers-color-scheme: dark) { .panel { background: #202020; color: #eee; box-shadow: 0 10px 30px rgba(0,0,0,0.5); } .panel::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); } .header h2 { color: #eee !important; } .form-group label { color: #bbb !important; } .form-group input[type="text"], .form-group input[type="number"], .form-group select { background: #333; color: #fff; border-color: #444 !important; } .close-btn:hover { color: #fff !important; } .slider { background-color: #555 !important; } } @keyframes slideIn { from { transform: translateY(20px) scale(0.95); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; } .header h2 { margin: 0; font-size: 18px; font-weight: 600; color: #333; } .close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #888; transition: color 0.2s; padding: 0; line-height: 1; } .close-btn:hover { color: #333; } .form-group { margin-bottom: 16px; } .form-group label { display: block; font-size: 14px; margin-bottom: 8px; font-weight: 500; color: #555;} .form-group input[type="text"], .form-group input[type="number"], .form-group select { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s; } .form-group input[type="text"]:focus, .form-group input[type="number"]:focus, .form-group select:focus { border-color: #0078D4 !important; box-shadow: 0 0 0 2px rgba(0, 120, 212, 0.2); } .switch { position: relative; display: inline-block; width: 44px; height: 24px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; } .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } input:checked + .slider { background-color: #0078D4 !important; } input:checked + .slider:before { transform: translateX(20px); } .switch-wrapper { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;} .switch-label { font-size: 15px; font-weight: 500; } .save-btn { width: 100%; padding: 12px; background: #0078D4; color: white; border: none; border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s; margin-top: 16px; } .save-btn:hover { background: #0060A8; } .save-btn:active { transform: scale(0.98); } .site-card { background: #f8f9fa; border-radius: 8px; padding: 16px; margin-bottom: 24px; border: 1px solid #eee; } .site-info { margin-bottom: 12px; font-size: 13px; color: #555; display: flex; justify-content: space-between; align-items: center; } .site-info strong { color: #222; font-size: 14px; max-width: 210px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .status-badge { padding: 4px 8px; border-radius: 4px; font-weight: 600; font-size: 12px; background: rgba(0,0,0,0.05); } .actions-row { display: flex; gap: 8px; } .action-btn { flex: 1; padding: 8px 0; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; transition: background 0.2s; } .action-btn.success { background: #e8f5e9; color: #107C10; } .action-btn.success:hover { background: #c8e6c9; } .action-btn.danger { background: #fce4e4; color: #D13438; } .action-btn.danger:hover { background: #f8caca; } .action-btn.default { background: #f0f0f0; color: #333; } .action-btn.default:hover { background: #e0e0e0; } @media (prefers-color-scheme: dark) { .site-card { background: #2d2d2d; border-color: #3d3d3d; } .site-info { color: #aaa; } .site-info strong { color: #eee; } .status-badge { background: rgba(255,255,255,0.1); } .action-btn.success { background: rgba(16, 124, 16, 0.2); } .action-btn.success:hover { background: rgba(16, 124, 16, 0.3); } .action-btn.danger { background: rgba(209, 52, 56, 0.2); } .action-btn.danger:hover { background: rgba(209, 52, 56, 0.3); } .action-btn.default { background: #444; color: #eee; } .action-btn.default:hover { background: #555; } } .list-container { max-height: 160px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; margin-top: 4px; padding-right: 2px; } .list-container::-webkit-scrollbar { width: 6px; } .list-container::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; } .list-item { display: flex; align-items: center; justify-content: space-between; background: #fff; border: 1px solid #eee; border-radius: 6px; padding: 6px 6px 6px 12px; font-size: 13px; } .list-item .item-domain { flex: 1; min-width: 0; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 8px; } .item-remove { flex-shrink: 0; width: 24px; height: 24px; border: none; border-radius: 50%; background: transparent; color: #888; font-size: 18px; line-height: 1; cursor: pointer; transition: background 0.15s, color 0.15s; padding: 0; } .item-remove:hover { background: rgba(209, 52, 56, 0.12); color: #D13438; } .empty-hint { padding: 10px 0 6px; text-align: center; color: #999; font-size: 13px; } @media (prefers-color-scheme: dark) { .list-container::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); } .list-item { background: #262626; border-color: #3a3a3a; } .list-item .item-domain { color: #ddd; } .item-remove { color: #aaa; } .item-remove:hover { background: rgba(209, 52, 56, 0.25); color: #ff7075; } .empty-hint { color: #777; } } .nav-btn { width: 100%; display: flex; align-items: center; justify-content: space-between; background: #f8f9fa; border: 1px solid #eee; border-radius: 8px; padding: 12px 16px; margin-bottom: 24px; font-size: 14px; font-weight: 500; color: #333; cursor: pointer; font-family: inherit; text-align: left; transition: background 0.2s, border-color 0.2s, transform 0.1s; } .nav-btn:hover { background: #eef1f4; border-color: #dcdee1; } .nav-btn:active { transform: scale(0.995); } .nav-btn + .nav-btn { margin-top: -16px; } .nav-btn .nav-arrow { display: inline-flex; align-items: center; gap: 8px; color: #888; font-size: 12px; font-weight: 500; } .nav-btn .nav-arrow .whitelist-count, .nav-btn .nav-arrow .blacklist-count { background: rgba(0,0,0,0.05); padding: 2px 8px; border-radius: 10px; color: #555; min-width: 18px; text-align: center; } .nav-btn .nav-arrow::after { content: '›'; font-size: 20px; color: #999; line-height: 1; } .back-btn { background: none; border: none; font-size: 22px; cursor: pointer; color: #888; padding: 0 4px; line-height: 1; font-family: inherit; transition: color 0.2s; } .back-btn:hover { color: #333; } .list-meta { font-size: 12px; color: #888; margin: -8px 0 12px; } .list-container-full { max-height: 360px; } @media (prefers-color-scheme: dark) { .nav-btn { background: #2a2a2a; border-color: #3a3a3a; color: #eee; } .nav-btn:hover { background: #333; border-color: #444; } .nav-btn .nav-arrow { color: #aaa; } .nav-btn .nav-arrow .whitelist-count, .nav-btn .nav-arrow .blacklist-count { background: rgba(255,255,255,0.08); color: #ddd; } .nav-btn .nav-arrow::after { color: #888; } .back-btn { color: #aaa; } .back-btn:hover { color: #fff; } .list-meta { color: #aaa; } } `; const currentDomain = location.hostname; const currentRule = config.rules[currentDomain]; let statusText = "未设置"; let statusColor = ""; if (currentRule === 'whitelist') { statusText = "已加白名单"; statusColor = "color: #107C10;"; } else if (currentRule === 'blacklist') { statusText = "已加黑名单"; statusColor = "color: #D13438;"; } const wrapper = document.createElement('div'); wrapper.innerHTML = `

验证码识别配置

当前网站:${currentDomain} ${statusText}
启用自动识别
`; shadow.appendChild(style); shadow.appendChild(wrapper); document.body.appendChild(container); // 当前站点状态徽章刷新 const refreshSiteStatusBadge = () => { const badge = shadow.getElementById('siteStatusBadge'); if (!badge) return; const rule = config.rules[currentDomain]; if (rule === 'whitelist') { badge.textContent = "已加白名单"; badge.style.color = "#107C10"; } else if (rule === 'blacklist') { badge.textContent = "已加黑名单"; badge.style.color = "#D13438"; } else { badge.textContent = "未设置"; badge.style.color = ""; } }; // 通用规则列表刷新工厂:白名单 / 黑名单共用一套渲染逻辑 const makeRuleListRefresher = (ruleType, listId, countSelector, emptyHint) => () => { const list = shadow.getElementById(listId); const counters = shadow.querySelectorAll(countSelector); if (!list) return; const matched = Object.keys(config.rules) .filter(domain => config.rules[domain] === ruleType) .sort(); counters.forEach(el => { el.textContent = matched.length; }); list.innerHTML = ''; if (matched.length === 0) { const empty = document.createElement('div'); empty.className = 'empty-hint'; empty.textContent = emptyHint; list.appendChild(empty); return; } for (const domain of matched) { const row = document.createElement('div'); row.className = 'list-item'; const label = document.createElement('span'); label.className = 'item-domain'; label.title = domain; label.textContent = domain; const removeBtn = document.createElement('button'); removeBtn.className = 'item-remove'; removeBtn.title = '移除'; removeBtn.textContent = '×'; removeBtn.addEventListener('click', () => { delete config.rules[domain]; GM_setValue('rules', config.rules); refreshWhitelistList(); refreshBlacklistList(); if (domain === currentDomain) refreshSiteStatusBadge(); }); row.appendChild(label); row.appendChild(removeBtn); list.appendChild(row); } }; const refreshWhitelistList = makeRuleListRefresher('whitelist', 'whitelistList', '.whitelist-count', '尚未添加白名单域名'); const refreshBlacklistList = makeRuleListRefresher('blacklist', 'blacklistList', '.blacklist-count', '尚未添加黑名单域名'); // 黑白名单交互逻辑 const updateSiteRule = (rule) => { if (rule) { config.rules[currentDomain] = rule; } else { delete config.rules[currentDomain]; } GM_setValue('rules', config.rules); refreshSiteStatusBadge(); refreshWhitelistList(); refreshBlacklistList(); if (isSiteEnabled()) findAndSolveCaptcha(); }; shadow.getElementById('btnWhitelist').addEventListener('click', () => updateSiteRule('whitelist')); shadow.getElementById('btnBlacklist').addEventListener('click', () => updateSiteRule('blacklist')); shadow.getElementById('btnRemoveList').addEventListener('click', () => updateSiteRule(null)); refreshWhitelistList(); refreshBlacklistList(); // 视图切换:主视图 / 白名单视图 / 黑名单视图 const showView = (name) => { const main = shadow.getElementById('mainView'); const wl = shadow.getElementById('whitelistView'); const bl = shadow.getElementById('blacklistView'); if (!main || !wl || !bl) return; main.style.display = name === 'main' ? 'block' : 'none'; wl.style.display = name === 'whitelist' ? 'block' : 'none'; bl.style.display = name === 'blacklist' ? 'block' : 'none'; }; shadow.getElementById('openWhitelistBtn').addEventListener('click', () => { refreshWhitelistList(); showView('whitelist'); }); shadow.getElementById('openBlacklistBtn').addEventListener('click', () => { refreshBlacklistList(); showView('blacklist'); }); shadow.getElementById('backBtnWl').addEventListener('click', () => showView('main')); shadow.getElementById('backBtnBl').addEventListener('click', () => showView('main')); // API 提供商切换联动 const apiProviderSelect = shadow.getElementById('apiProvider'); const groupAnticap = shadow.getElementById('group-anticap'); const groupDdddocr = shadow.getElementById('group-ddddocr'); const groupToken = shadow.getElementById('group-token'); apiProviderSelect.addEventListener('change', (e) => { const val = e.target.value; if (val === 'anticap') { groupAnticap.style.display = 'block'; groupToken.style.display = 'block'; groupDdddocr.style.display = 'none'; } else { groupAnticap.style.display = 'none'; groupToken.style.display = 'none'; groupDdddocr.style.display = 'block'; } }); const closePanel = () => { container.style.opacity = '0'; container.style.transition = 'opacity 0.2s'; setTimeout(() => document.body.removeChild(container), 200); }; container.addEventListener('click', (e) => { if (e.composedPath()[0] === container) closePanel(); }); shadow.querySelectorAll('.close-btn').forEach(btn => btn.addEventListener('click', closePanel)); shadow.getElementById('saveBtn').addEventListener('click', () => { config.globalEnable = shadow.getElementById('globalEnable').checked; config.apiProvider = shadow.getElementById('apiProvider').value; config.ocrApiUrl = shadow.getElementById('ocrApiUrl').value; config.ocrApiUrlDdddocr = shadow.getElementById('ocrApiUrlDdddocr').value; config.apiToken = shadow.getElementById('apiToken').value; config.rateLimit = parseInt(shadow.getElementById('rateLimit').value, 10) || 30; GM_setValue('globalEnable', config.globalEnable); GM_setValue('apiProvider', config.apiProvider); GM_setValue('ocrApiUrl', config.ocrApiUrl); GM_setValue('ocrApiUrlDdddocr', config.ocrApiUrlDdddocr); GM_setValue('apiToken', config.apiToken); GM_setValue('rateLimit', config.rateLimit); closePanel(); if (config.globalEnable) findAndSolveCaptcha(); }); } function debugLog(msg) { console.log(`% c[Auto Captcha Script]`, `color: #4CAF50; font - weight: bold`, msg); } function fire(element, eventName) { var event = document.createEvent("HTMLEvents"); event.initEvent(eventName, true, true); element.dispatchEvent(event); } function FireForReact(element, eventName) { try { let env = new Event(eventName); element.dispatchEvent(env); var funName = Object.keys(element).find(p => Object.keys(element[p]).find(f => f.toLowerCase().endsWith(eventName))); if (funName != undefined) { element[funName].onChange(env) } } catch (e) { } } function autoFillInput(input, ans) { debugLog(`Starting autoFillInput for value: ${ans} `); ans = ans.replace(/\s+/g, ""); // 移除多余空白符 if (input.tagName === "TEXTAREA") { input.innerHTML = ans; } else { input.value = ans; if (typeof (InputEvent) !== "undefined") { input.value = ans; input.dispatchEvent(new InputEvent('input')); var eventList = ['input', 'change', 'focus', 'keypress', 'keyup', 'keydown', 'select']; for (var i = 0; i < eventList.length; i++) { fire(input, eventList[i]); } FireForReact(input, 'change'); input.value = ans; } else if (KeyboardEvent) { input.dispatchEvent(new KeyboardEvent("input")); } } } const GIF_SOURCE_PATTERN = /\.gif(?:$|[?#])/i; const GIF_QUERY_PATTERN = /[?&](?:format|type|ext|mime|imgtype)=gif(?:$|[&#])/i; const BASE64_MARKER = ';base64,'; function getImageSource(img) { return img.currentSrc || img.src || ''; } function extractBase64Payload(src) { if (typeof src !== 'string' || !src.startsWith('data:image')) { return null; } const markerIndex = src.indexOf(BASE64_MARKER); if (markerIndex === -1) { return null; } return src.slice(markerIndex + BASE64_MARKER.length); } function shouldPreserveOriginalBytes(src) { if (typeof src !== 'string' || !src) { return false; } return /^data:image\/gif/i.test(src) || GIF_SOURCE_PATTERN.test(src) || GIF_QUERY_PATTERN.test(src); } function normalizeBase64(base64) { if (typeof base64 !== 'string') { return ''; } return base64.replace(/\s+/g, ''); } function detectImageTypeFromBase64(base64) { const normalized = normalizeBase64(base64); if (normalized.startsWith('R0lGOD')) return 'image/gif'; if (normalized.startsWith('iVBORw0KGgo')) return 'image/png'; if (normalized.startsWith('/9j/')) return 'image/jpeg'; if (normalized.startsWith('UklGR')) return 'image/webp'; return null; } function base64ToBytes(base64) { const binary = atob(normalizeBase64(base64)); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) { bytes[i] = binary.charCodeAt(i); } return bytes; } function getFrameIndexes(frameCount, maxFrameSamples) { if (!frameCount || frameCount <= maxFrameSamples) { return Array.from({ length: frameCount || 0 }, (_, index) => index); } const indexes = []; for (let i = 0; i < maxFrameSamples; i += 1) { const index = Math.floor((i * (frameCount - 1)) / (maxFrameSamples - 1)); if (indexes[indexes.length - 1] !== index) { indexes.push(index); } } return indexes; } // 从首帧的四角/边缘采样估算背景亮度(Rec. 601 Y 通道),用于判定验证码配色类型 function estimateBackgroundLuminance(frame, width, height) { if (!frame || !frame.length || !width || !height) return 255; const samples = []; const pushSample = (x, y) => { const idx = (y * width + x) * 4; if (idx + 3 >= frame.length) return; if (frame[idx + 3] === 0) return; // 忽略全透明像素 samples.push(0.299 * frame[idx] + 0.587 * frame[idx + 1] + 0.114 * frame[idx + 2]); }; const rim = Math.max(1, Math.min(3, Math.floor(Math.min(width, height) / 20))); for (let i = 0; i < rim; i += 1) { pushSample(i, 0); pushSample(width - 1 - i, 0); pushSample(i, height - 1); pushSample(width - 1 - i, height - 1); pushSample(i, Math.floor(height / 2)); pushSample(width - 1 - i, Math.floor(height / 2)); } if (samples.length === 0) return 255; let sum = 0; for (const v of samples) sum += v; return sum / samples.length; } // 自适应多帧合并:浅底深字 → 保留最暗像素;深底浅字 → 保留最亮像素 function mergeAnimatedFrameData(frames, width, height) { const validFrames = Array.isArray(frames) ? frames.filter(frame => frame && frame.length) : []; if (validFrames.length === 0) { return { data: new Uint8ClampedArray(), strategy: 'noop', luminance: 0 }; } const bgLuminance = estimateBackgroundLuminance(validFrames[0], width, height); const pickBrightest = bgLuminance < 128; const merged = new Uint8ClampedArray(validFrames[0]); for (let frameIndex = 1; frameIndex < validFrames.length; frameIndex += 1) { const frame = validFrames[frameIndex]; if (frame.length !== merged.length) { throw new Error('All frames must have the same pixel length.'); } for (let i = 0; i < frame.length; i += 4) { const sourceAlpha = frame[i + 3]; const targetAlpha = merged[i + 3]; if (sourceAlpha === 0) continue; if (targetAlpha === 0) { merged[i] = frame[i]; merged[i + 1] = frame[i + 1]; merged[i + 2] = frame[i + 2]; merged[i + 3] = sourceAlpha; continue; } if (pickBrightest) { merged[i] = Math.max(merged[i], frame[i]); merged[i + 1] = Math.max(merged[i + 1], frame[i + 1]); merged[i + 2] = Math.max(merged[i + 2], frame[i + 2]); } else { merged[i] = Math.min(merged[i], frame[i]); merged[i + 1] = Math.min(merged[i + 1], frame[i + 1]); merged[i + 2] = Math.min(merged[i + 2], frame[i + 2]); } merged[i + 3] = Math.max(targetAlpha, sourceAlpha); } } return { data: merged, strategy: pickBrightest ? 'brightest' : 'darkest', luminance: bgLuminance }; } async function flattenAnimatedGifForOcr(base64) { if (typeof ImageDecoder === 'undefined') { debugLog(`ImageDecoder is unavailable. Sending original GIF bytes to OCR.`); return base64; } try { if (typeof ImageDecoder.isTypeSupported === 'function') { const supported = await ImageDecoder.isTypeSupported('image/gif'); if (!supported) { debugLog(`ImageDecoder does not support GIF. Sending original GIF bytes to OCR.`); return base64; } } const decoder = new ImageDecoder({ data: base64ToBytes(base64), type: 'image/gif' }); await decoder.completed; const track = decoder.tracks.selectedTrack; if (!track || !track.animated || track.frameCount <= 1) { decoder.close(); return base64; } const frameIndexes = getFrameIndexes(track.frameCount, 10); const workingCanvas = document.createElement('canvas'); const workingContext = workingCanvas.getContext('2d', { willReadFrequently: true }); let width = 0; let height = 0; const framePixels = []; for (const frameIndex of frameIndexes) { const result = await decoder.decode({ frameIndex }); const frame = result.image; if (!width || !height) { width = frame.displayWidth || frame.codedWidth; height = frame.displayHeight || frame.codedHeight; workingCanvas.width = width; workingCanvas.height = height; } workingContext.clearRect(0, 0, width, height); workingContext.drawImage(frame, 0, 0, width, height); framePixels.push(workingContext.getImageData(0, 0, width, height).data); frame.close(); } decoder.close(); const mergeResult = mergeAnimatedFrameData(framePixels, width, height); if (!mergeResult.data.length) { return base64; } const outputCanvas = document.createElement('canvas'); outputCanvas.width = width; outputCanvas.height = height; const outputContext = outputCanvas.getContext('2d'); outputContext.putImageData(new ImageData(mergeResult.data, width, height), 0, 0); debugLog(`Flattened animated GIF (${track.frameCount} frames, sampled ${frameIndexes.length}, strategy=${mergeResult.strategy}, bgLuminance=${mergeResult.luminance.toFixed(1)}) into merged PNG for OCR.`); return outputCanvas.toDataURL('image/png').split(',')[1]; } catch (e) { debugLog(`Animated GIF preprocessing failed. Sending original GIF bytes. Error: ${e.message} `); return base64; } } // 将图片转换成 Base64 async function getBase64Image(img) { const src = getImageSource(img); const embeddedPayload = extractBase64Payload(src); if (embeddedPayload) { const imageType = detectImageTypeFromBase64(embeddedPayload); if (imageType === 'image/gif') { return flattenAnimatedGifForOcr(embeddedPayload); } return embeddedPayload; } if (shouldPreserveOriginalBytes(src)) { debugLog(`GIF source detected. Preserving original bytes through GM_xmlhttpRequest.`); return flattenAnimatedGifForOcr(await fetchOriginalImageBase64(src)); } try { const canvas = document.createElement('canvas'); canvas.width = img.width || img.naturalWidth; canvas.height = img.height || img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const dataURL = canvas.toDataURL('image/png'); return dataURL.split(',')[1]; } catch (e) { debugLog(`Canvas extraction failed, fallback to GM_xmlhttpRequest: ${e.message} `); } const base64 = await fetchOriginalImageBase64(src); if (detectImageTypeFromBase64(base64) === 'image/gif') { return flattenAnimatedGifForOcr(base64); } return base64; } // 备选方案:使用游猴脚本请求,可以绕过跨域限制 function fetchOriginalImageBase64(url) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', onload: function (response) { try { let binary = ''; const bytes = new Uint8Array(response.response); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } resolve(btoa(binary)); } catch (err) { debugLog(`Fetch image process failed: ${err.message} `); resolve(null); } }, onerror: () => { debugLog(`Fetch image network error.`); resolve(null); } }); }); } // 调用 OCR 接口 function recognizeCaptcha(base64) { return new Promise((resolve) => { const logApiUrl = config.apiProvider === 'ddddocr' ? config.ocrApiUrlDdddocr : config.ocrApiUrl; debugLog(`Starting OCR request to ${logApiUrl}...`); const headers = { 'Content-Type': 'application/json' }; if (config.apiToken && config.apiProvider !== 'ddddocr') { headers['Authorization'] = `Bearer ${config.apiToken} `; } let requestBody = {}; if (config.apiProvider === 'ddddocr') { requestBody = { image: base64, probability: false, colors: [], custom_color_ranges: null }; } else { requestBody = { img_base64: base64 }; } GM_xmlhttpRequest({ method: 'POST', url: logApiUrl, headers: headers, data: JSON.stringify(requestBody), timeout: 15000, onload: function (response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); let resultText = data.result || data.text || data.data; if (!resultText && typeof data === 'string') { resultText = data; } if (resultText) { resolve({ success: true, text: resultText }); } else { resolve({ success: false, error: 'Empty result' }); } } catch (e) { resolve({ success: false, error: 'JSON parse error: ' + e.message }); } } else { resolve({ success: false, error: 'HTTP status ' + response.status }); } }, onerror: function () { resolve({ success: false, error: 'Network error' }); }, ontimeout: function () { resolve({ success: false, error: 'Request timeout' }); } }); }); } function isSiteEnabled() { const siteRule = config.rules[location.hostname]; if (siteRule === 'blacklist') return false; if (siteRule !== 'whitelist' && !config.globalEnable) return false; return true; } function isRateLimited() { let history = GM_getValue('requestHistory', []); const now = Date.now(); history = history.filter(timestamp => now - timestamp < 60000); if (history.length >= config.rateLimit) { return true; } history.push(now); GM_setValue('requestHistory', history); return false; } // 组合逻辑 async function handleCaptchaInput(captchaInput, captchaImg) { if (!isSiteEnabled()) return; // 确保不会对于同一张 URL 进行重复并发请求 if (captchaInput.dataset.solvingSrc === captchaImg.src) return; captchaInput.dataset.solvingSrc = captchaImg.src; if (isRateLimited()) { debugLog(`Rate limit exceeded (${config.rateLimit} req/min).`); delete captchaInput.dataset.solvingSrc; captchaImg.style.border = '2px solid #FF9800'; captchaImg.title = '请求受限:超过每分钟速率限制'; return; } try { debugLog(`Getting original image as Base64...`); const base64 = await getBase64Image(captchaImg); if (!base64) { delete captchaInput.dataset.solvingSrc; return; } // 识别缓存,防止同一张图片重复消耗接口资源 if (captchaInput.dataset.solvedBase64 === base64) { delete captchaInput.dataset.solvingSrc; return; } const response = await recognizeCaptcha(base64); if (response && response.success && response.text) { autoFillInput(captchaInput, response.text); debugLog(`Filled successfully with: ${response.text} `); captchaInput.dataset.solvedBase64 = base64; delete captchaInput.dataset.solvingSrc; // 给图片加上标识位 captchaImg.style.border = '2px solid #4CAF50'; captchaImg.title = `已被自动识别为:${response.text} `; } else { debugLog(`Request failed.Response logic aborted.`); delete captchaInput.dataset.solvingSrc; captchaImg.style.border = '2px solid #F44336'; } } catch (e) { debugLog(`Handle Captcha exception: ${e.message} `); delete captchaInput.dataset.solvingSrc; } } const CAPTCHA_PATTERN = /code|captcha|yzm|check|random|veri|验证码|看不清|换一张/i; function matchAttributes(element, attrList) { for (let attr of attrList) { let val = element[attr] || element.getAttribute(attr) || ""; if (typeof val === 'string' && CAPTCHA_PATTERN.test(val)) return true; if (val.baseVal && CAPTCHA_PATTERN.test(val.baseVal)) return true; } return false; } // 获取图片的真实渲染尺寸(优先用布局尺寸,后退至原始尺寸) function getRenderedSize(element) { const rect = element.getBoundingClientRect ? element.getBoundingClientRect() : { width: 0, height: 0 }; const width = rect.width || element.naturalWidth || element.offsetWidth || 0; const height = rect.height || element.naturalHeight || element.offsetHeight || 0; return { width, height }; } // 匹配是不是验证码图片(正向:图片属性含关键词) function isCodeImg(element) { if (!element) return false; const { width, height } = getRenderedSize(element); if (height >= 100) return false; // 仅在宽高都已知时排除正方形,避免 0×0 未渲染图被误排 if (width > 0 && height > 0 && height === width) return false; return matchAttributes(element, ["id", "title", "alt", "name", "className", "src"]); } // 形状很像验证码:高 16-100px,宽 40-400px,宽高比 1.2-10 function isLikelyCaptchaShape(element) { if (!element) return false; const { width, height } = getRenderedSize(element); if (height < 16 || height > 100) return false; if (width < 40 || width > 400) return false; const ratio = width / height; return ratio >= 1.2 && ratio <= 10; } // 匹配是不是输入框 function isCaptchaInput(element) { const ignoreTypes = ['hidden', 'submit', 'button', 'checkbox', 'radio']; if (ignoreTypes.includes(element.type)) return false; return matchAttributes(element, ["placeholder", "alt", "title", "id", "className", "name"]); } const INPUT_SELECTOR = 'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]), textarea'; // 过滤掉隐藏/禁用/只读等无法填写的输入框 function isUsableInput(element) { if (!element) return false; const ignoreTypes = ['hidden', 'submit', 'button', 'checkbox', 'radio']; if (ignoreTypes.includes(element.type)) return false; if (element.readOnly || element.disabled) return false; const rect = element.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return false; const style = window.getComputedStyle(element); if (style.visibility === 'hidden' || style.display === 'none') return false; return true; } // 计算两个矩形之间的最短边距:重叠返回 0,同轴向时仅取轴向间距 function rectDistance(a, b) { const dx = Math.max(a.left - b.right, b.left - a.right, 0); const dy = Math.max(a.top - b.bottom, b.top - a.bottom, 0); return Math.hypot(dx, dy); } // 在一批候选输入框中,结合语义、几何距离与 DOM 顺序挑选最优匹配 function pickBestInput(img, inputs) { const usable = inputs.filter(isUsableInput); if (usable.length === 0) return null; // 具有 captcha/验证码 等语义特征的输入框优先 const semantic = usable.filter(isCaptchaInput); const pool = semantic.length > 0 ? semantic : usable; const imgRect = img.getBoundingClientRect(); const hasGeometry = imgRect.width > 0 && imgRect.height > 0; if (!hasGeometry) { // 图片尚未布局时,退化为 DOM 顺序:优先第一个紧跟图片之后的输入框 for (const input of pool) { const relation = img.compareDocumentPosition(input); if ((relation & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) return input; } return pool[pool.length - 1]; } let best = null; let bestScore = Infinity; for (const input of pool) { const dist = rectDistance(imgRect, input.getBoundingClientRect()); // 表单中验证码输入框一般在图片之后,给予轻量顺序倾向 const relation = img.compareDocumentPosition(input); const followsImg = (relation & Node.DOCUMENT_POSITION_FOLLOWING) !== 0; const orderPenalty = followsImg ? 0 : 20; const score = dist + orderPenalty; if (score < bestScore) { bestScore = score; best = input; } } return best; } // 逐级向上寻找最近的输入框,同时兼容左右布局与上下布局 function findClosestInput(img) { let parent = img.parentElement; let depth = 0; // 先在最多5层祖先容器内找候选,再按几何距离 + 语义打分挑选 while (parent && depth < 5) { const inputs = Array.from(parent.querySelectorAll(INPUT_SELECTOR)); const best = pickBestInput(img, inputs); if (best) return best; parent = parent.parentElement; depth++; } // 兜底方案:全局范围内按距离挑选 const allInputs = Array.from(document.querySelectorAll(INPUT_SELECTOR)); return pickBestInput(img, allInputs); } // 检测DOM中新的图片等资源 async function findAndSolveCaptcha() { if (!isSiteEnabled()) return; const images = document.querySelectorAll('img'); for (let img of images) { // 正向路径:图片自身属性命中关键词 let isCaptcha = isCodeImg(img); let targetInput = null; // 兑底路径:图片无关键词,但形状像验证码,且最近输入框命中验证码语义 if (!isCaptcha && isLikelyCaptchaShape(img)) { targetInput = findClosestInput(img); if (targetInput && isCaptchaInput(targetInput)) { isCaptcha = true; debugLog(`Shape+neighbor heuristic matched captcha image: ${img.currentSrc || img.src || '[inline]'}`); } } if (!isCaptcha) continue; if (!targetInput) targetInput = findClosestInput(img); if (targetInput) { handleCaptchaInput(targetInput, img); } } } // 监听DOM变动并节流执行验证码判定 let timeoutId = null; const observer = new MutationObserver(() => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(findAndSolveCaptcha, 500); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); // 初始页面加载检测 window.addEventListener('load', () => { setTimeout(findAndSolveCaptcha, 500); }); })();