// ==UserScript== // @name 自动识别填充网页验证码-重制版 // @namespace http://tampermonkey.net/ // @version 1.3.0 // @description 自动识别并填写大多数网页的图形验证码。支持通过右键菜单手动添加规则(规则存储在本地),或在无匹配规则时尝试使用通用规则自动识别。集成云码识别API(www.jfbym.com)提高识别率。提供黑名单功能阻止在特定网站运行。支持通过 WebDAV 进行配置的备份、恢复和跨设备同步。通过脚本菜单管理所有设置。 // @author lcymzzZ (Original), Lanxi (Modifier) // @license GPL Licence // @connect https://www.jfbym.com // @connect https://dav.jianguoyun.com/dav // @match http://*/* // @match https://*/* // @match *://*/* // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHBhdGggZD0iTTEyIDI0YzAtMTEuMDUgOC45NS0yMCAyMC0yMHMyMCA4Ljk1IDIwIDIwdjE2YzAgMTEuMDUtOC45NSAyMC0yMCAyMHMtMjAtOC45NS0yMC0yMFptMjAtMTJjLTYuNjI1IDAtMTIgNS4zNzUtMTIgMTJ2MTZjMCA2LjYyNSA1LjM3NSAxMiAxMiAxMnMxMi01LjM3NSAxMi0xMlYyNGMwLTYuNjI1LTUuMzc1LTEyLTEyLTEyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOm5vbnplcm87ZmlsbDojMDAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+ // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addStyle // ==/UserScript== (function () { 'use strict'; const KEY_LOCAL_RULES = 'localRules'; const KEY_BLACKLIST = 'blackList'; const KEY_TOKEN = 'token'; const KEY_FIRST_USE = 'fisrtUse'; const KEY_PRE_CODE = 'preCode'; const KEY_LAST_BALANCE = 'lastBalanceInfo'; const WEBDAV_SYNC_DIR = 'A_veri_code'; const ID_PANEL = 'captchaHelperSettingsPanel'; const ID_BALANCE_SPAN = 'balanceDisplaySpan'; const CLASS_SETTINGS_BUTTON = 'settingsButton'; var element, input; var localRules = {}; var exist = false; var inBlack = false; var useGenericRule = false; var focusListenerAttached = false; var initialCheckRun = false; var fisrtUse = GM_getValue(KEY_FIRST_USE, true); if (fisrtUse) { var mzsm = prompt(`自动识别填充网页验证码 (本地存储+云码版) 首次使用,请阅读并同意以下免责条款。 1. 此脚本仅用于学习研究,您必须在下载后24小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。 2. 请勿将此脚本用于任何商业或非法目的,若违反规定请自行对此负责。 3. 本人对此脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。 4. 任何以任何方式查看此脚本的人或直接或间接使用此脚本的使用者都应仔细阅读此条款。 5. 本人保留随时更改或补充此条款的权利,一旦您使用或复制了此脚本,即视为您已接受此免责条款。 若您同意以上内容,请输入"我已阅读并同意以上内容" 然后开始使用。`, ""); if (mzsm == "我已阅读并同意以上内容") { GM_setValue(KEY_FIRST_USE, false); } else { alert("免责条款未同意,脚本停止运行。\n若不想使用,请自行禁用脚本,以免每个页面都弹出该提示。"); return; } } function fetchAndDisplayBalance() { const token = GM_getValue(KEY_TOKEN); const balanceSpan = document.getElementById(ID_BALANCE_SPAN); if (!token) { alert("请先通过菜单设置云码 Token"); if (balanceSpan) { balanceSpan.textContent = '余额: Token未设置'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } return; } if (balanceSpan) { balanceSpan.textContent = '余额: 查询中...'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } else { } const datas = { "token": token, "type": "score" }; GM_xmlhttpRequest({ method: "POST", url: "http://api.jfbym.com/api/YmServer/getUserInfoApi", data: JSON.stringify(datas), headers: { "Content-Type": "application/json" }, responseType: "json", timeout: 10000, onload: function (response) { try { if (response.status == 200 && response.response) { const respData = response.response; if (respData.data && respData.data.score !== undefined) { if (balanceSpan) { balanceSpan.textContent = `余额: ${respData.data.score}`; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } } else if (respData.code === 10000) { if (balanceSpan) { balanceSpan.textContent = '余额: 未知'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } } else { if (balanceSpan) { balanceSpan.textContent = '余额: 查询失败'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } } } else { if (balanceSpan) { balanceSpan.textContent = '余额: 请求失败'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } } } catch (e) { if (balanceSpan) { balanceSpan.textContent = '余额: 处理错误'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } } }, onerror: function (response) { if (balanceSpan) { balanceSpan.textContent = '余额: 网络错误'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } }, ontimeout: function () { if (balanceSpan) { balanceSpan.textContent = '余额: 请求超时'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } } }); } GM_registerMenuCommand('添加当前页面规则', addRule); GM_registerMenuCommand('清除当前页面规则', delRule); GM_registerMenuCommand('管理脚本设置', showSettingsPanel); GM_setValue(KEY_PRE_CODE, ""); function saveToken() { var helpText = "云码Token是使用本脚本识别验证码的必需凭证。\n请前往 www.jfbym.com 注册并获取。\n文档:https://www.jfbym.com/page-doc/api-project-list.html"; var currentToken = GM_getValue(KEY_TOKEN, ""); var token = prompt(helpText + "\n\n当前Token: " + (currentToken ? currentToken.substring(0, 5) + "..." : "未设置"), currentToken); if (token === null) { return; } if (token === "") { GM_setValue(KEY_TOKEN, ""); alert("Token已清除"); return; } if (token.length < 10) { alert("Token格式似乎不正确,请重新输入。"); return; } alert("Token保存成功"); GM_setValue(KEY_TOKEN, token); } const yunmaErrorCodes = { 10001: "参数错误", 10002: "余额不足", 10003: "无此访问权限 (请检查 Token 或重置)", 10004: "无此验证类型", 10005: "网络拥塞", 10006: "数据包过载", 10007: "服务繁忙", 10008: "网络错误,请稍后重试", 10009: "结果准备中,请稍后再试", 10010: "请求结束 (但未成功?)" }; const captchaTypes = [ { code: '10110', description: '通用数英1-4位(2积分一次,最便宜)' }, { code: '10111', description: '通用数英5-8位(3积分一次)' }, { code: '10112', description: '通用数英9~11位(5积分一次)' }, { code: '10113', description: '通用数英12位及以上(10积分一次)' }, { code: '10103', description: '通用数英1~6位plus (12积分一次,别用)' }, { code: '9001', description: '定制-数英5位~qcs' }, { code: '193', description: '定制-纯数字4位' }, { code: '10114', description: '通用中文字符1~2位' }, { code: '10115', description: '通用中文字符 3~5位' }, { code: '10116', description: '通用中文字符6~8位' }, { code: '10117', description: '通用中文字符9位及以上' }, { code: '10107', description: '定制-XX西游苦行中文字符' }, { code: '50100', description: '通用数字计算题(15积分一次...)' }, { code: '50101', description: '通用中文计算题' }, { code: '452', description: '定制-计算题 cni' } ]; function addRule() { const ruleData = { "url": window.location.href.split("?")[0], "img": "", "input": "", "inputType": "", "type": "", "captchaType": "", "ymType": null }; topNotice('请在验证码图片上点击鼠标 "右"👉 键'); document.oncontextmenu = function (e) { e = e || window.event; e.preventDefault(); let k = null; if (e.target.tagName == "IMG" || e.target.tagName == "GIF") { const imgList = document.getElementsByTagName('img'); for (let i = 0; i < imgList.length; i++) { if (imgList[i] == e.target) { k = i; ruleData.type = "img"; } } } else if (e.target.tagName == "CANVAS") { const imgList = document.getElementsByTagName('canvas'); for (let i = 0; i < imgList.length; i++) { if (imgList[i] == e.target) { k = i; ruleData.type = "canvas"; } } } if (k == null) { topNotice("选择有误,请重新点击验证码图片"); return; } ruleData.img = k; topNotice('请在验证码输入框上点击鼠标 "左"👈 键'); document.onclick = function (e) { e = e || window.event; e.preventDefault(); let k = null; const inputList = document.getElementsByTagName('input'); const textareaList = document.getElementsByTagName('textarea'); if (e.target.tagName == "INPUT") { ruleData.inputType = "input"; for (let i = 0; i < inputList.length; i++) { if (inputList[i] == e.target) { if (inputList[0] && (inputList[0].id == "_w_simile" || inputList[0].id == "black_node")) { k = i - 1; } else { k = i; } } } } else if (e.target.tagName == "TEXTAREA") { ruleData.inputType = "textarea"; for (let i = 0; i < textareaList.length; i++) { if (textareaList[i] == e.target) { k = i; } } } if (k == null) { topNotice("选择有误,请重新点击验证码输入框"); return; } ruleData.input = k; const r = confirm('选择验证码类型\n\n数/英验证码请点击"确定",算术验证码请点击"取消"'); if (r == true) { ruleData.captchaType = "general"; let promptText = "请为当前规则选择一个具体的识别类型(输入代码):\n默认使用 10110 (通用数英1-4位)\n\n"; const defaultType = '10110'; const validCodes = captchaTypes.map(t => t.code); captchaTypes.forEach(type => { promptText += `${type.code}: ${type.description}\n`; }); let selectedYmType = prompt(promptText, defaultType); if (selectedYmType === null) { topNotice("添加规则已取消"); document.oncontextmenu = null; document.onclick = null; return; } if (validCodes.includes(selectedYmType)) { ruleData.ymType = selectedYmType; } else { alert(`无效的选择 '${selectedYmType}',将使用默认类型 ${defaultType}。`); ruleData.ymType = defaultType; } } else { ruleData.captchaType = "math"; ruleData.ymType = '50100'; } addR(ruleData).then((res) => { if (res.status == 200) { topNotice("添加规则成功"); document.oncontextmenu = null; document.onclick = null; start(); } else { topNotice("Error,添加规则失败"); document.oncontextmenu = null; document.onclick = null; } }); } } } function addR(ruleData) { return new Promise((resolve, reject) => { try { let allRules = GM_getValue(KEY_LOCAL_RULES, {}); allRules[ruleData.url] = ruleData; GM_setValue(KEY_LOCAL_RULES, allRules); resolve({ status: 200 }); } catch (error) { reject({ status: 500, error: error }); } }); } function delRule() { const currentUrl = window.location.href.split("?")[0]; const ruleData = { "url": currentUrl }; delR(ruleData).then((res) => { if (res.status === 200) { topNotice("删除规则成功"); exist = false; localRules = {}; } else if (res.status === 404) { topNotice("当前页面无规则可清除"); } else { topNotice(`删除规则时遇到未知状态: ${res.status}`); } }).catch(err => { topNotice("Error,删除规则失败 (存储错误)"); }); } function delR(ruleData) { return new Promise((resolve, reject) => { try { let allRules = GM_getValue(KEY_LOCAL_RULES, {}); if (allRules[ruleData.url]) { delete allRules[ruleData.url]; GM_setValue(KEY_LOCAL_RULES, allRules); resolve({ status: 200 }); } else { resolve({ status: 404 }); } } catch (error) { reject({ status: 500, error: error }); } }); } function processCode(code, ruleYmType, isGeneric = false) { if (!code) return; const preCode = GM_getValue(KEY_PRE_CODE, ""); if (code !== preCode) { GM_setValue(KEY_PRE_CODE, code); p1(code, ruleYmType, isGeneric).then(ans => { if (ans) { writeIn1(ans); } }); } } function getBase64FromImageElement(imgElement, callback) { const src = imgElement.src; if (!src) { callback(null); return; } if (src.startsWith('data:image')) { try { const base64 = src.split("base64,")[1]; callback(base64); } catch (e) { callback(null); } } else if (src.startsWith('blob:')) { fetch(src) .then(response => { if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return response.blob(); }) .then(blob => { const reader = new FileReader(); reader.onloadend = () => { try { const base64 = reader.result.split("base64,")[1]; callback(base64); } catch (e) { callback(null); } }; reader.onerror = (e) => { callback(null); } reader.readAsDataURL(blob); }) .catch(err => { callback(null); }); } else { if (imgElement.complete && imgElement.naturalWidth !== 0) { try { const canvas = document.createElement("canvas"); canvas.width = imgElement.naturalWidth; canvas.height = imgElement.naturalHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height); const base64 = canvas.toDataURL("image/png").split("base64,")[1]; callback(base64); } catch (err) { callback(null); } } else { imgElement.onload = () => { try { const canvas = document.createElement("canvas"); canvas.width = imgElement.naturalWidth; canvas.height = imgElement.naturalHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height); const base64 = canvas.toDataURL("image/png").split("base64,")[1]; callback(base64); } catch (err) { callback(null); } }; imgElement.onerror = () => { callback(null); }; } } } function codeByRule(ymType, isGeneric) { if (!element || !(element instanceof HTMLImageElement)) { return; } if (!input || !(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) { return; } getBase64FromImageElement(element, (base64Code) => { if (base64Code) { processCode(base64Code, ymType, isGeneric); } }); } function canvasRule(ymType, isGeneric) { if (!element || !(element instanceof HTMLCanvasElement)) { return; } if (!input || !(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) { return; } try { const base64Code = element.toDataURL("image/png").split("base64,")[1]; processCode(base64Code, ymType, isGeneric); } catch (err) { } } function writeIn1(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')); const eventList = ['input', 'change', 'focus', 'keypress', 'keyup', 'keydown', 'select']; for (let i = 0; i < eventList.length; i++) { fire(input, eventList[i]); } FireForReact(input, 'change'); input.value = ans; } else if (KeyboardEvent) { input.dispatchEvent(new KeyboardEvent("input")); } } } function compareUrl() { return new Promise((resolve, reject) => { try { const currentUrl = window.location.href.split("?")[0]; let allRules = GM_getValue(KEY_LOCAL_RULES, {}); localRules = allRules[currentUrl]; if (localRules) { resolve(true); } else { localRules = {}; resolve(false); } } catch (error) { localRules = {}; reject(error); } }); } function start() { compareUrl().then((isExist) => { if (isExist) { exist = true; useGenericRule = false; pageChange(); } else { exist = false; useGenericRule = true; pageChange(); } }).catch(error => { exist = false; useGenericRule = true; pageChange(); }); } function findElementAndInput(rule) { let foundElement = null; let foundInput = null; try { const elementType = rule.type; const elementIndex = parseInt(rule.img); const inputType = rule.inputType || 'input'; const inputIndex = parseInt(rule.input); if (isNaN(elementIndex) || isNaN(inputIndex)) { return { element: null, input: null }; } const elements = document.getElementsByTagName(elementType); if (elements && elements.length > elementIndex) { foundElement = elements[elementIndex]; } if (inputType === 'textarea') { const textareas = document.getElementsByTagName('textarea'); if (textareas && textareas.length > inputIndex) { foundInput = textareas[inputIndex]; } } else { const inputs = document.getElementsByTagName('input'); let adjustedInputIndex = inputIndex; if (inputs[0] && (inputs[0].id == "_w_simile" || inputs[0].id == "black_node")) { adjustedInputIndex += 1; } if (inputs && inputs.length > adjustedInputIndex) { foundInput = inputs[adjustedInputIndex]; } } if (elementType === 'img' && !(foundElement instanceof HTMLImageElement)) foundElement = null; if (elementType === 'canvas' && !(foundElement instanceof HTMLCanvasElement)) foundElement = null; if (!(foundInput instanceof HTMLInputElement || foundInput instanceof HTMLTextAreaElement)) foundInput = null; } catch (error) { foundElement = null; foundInput = null; } return { element: foundElement, input: foundInput }; } function deepQuerySelectorAll(selector, rootNode = document.body) { let results = []; try { const lightDomElements = rootNode.querySelectorAll(selector); results = Array.from(lightDomElements); } catch (e) { } try { const allElementsInRoot = rootNode.querySelectorAll('*'); for (const element of allElementsInRoot) { if (element.shadowRoot) { const shadowResults = deepQuerySelectorAll(selector, element.shadowRoot); results = results.concat(shadowResults); } } } catch (e) { } return results; } function findGenericCaptchaElements() { let bestImageElement = null; let bestInputElement = null; let imageScore = -1; let inputScore = -1; const isVisible = (el) => { if (!el) return false; return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0) && window.getComputedStyle(el).display !== 'none'; }; const potentialImages = deepQuerySelectorAll('img, canvas, div'); for (const el of potentialImages) { if (isVisible(el)) { let currentScore = 0; let isCandidate = false; if (el.tagName === 'IMG') { const src = el.src || ''; if (src.startsWith('data:image')) { currentScore = 10; } else { currentScore = 5; } isCandidate = true; } else if (el.tagName === 'CANVAS') { currentScore = 8; isCandidate = true; } else if (el.tagName === 'DIV') { const style = window.getComputedStyle(el); if (style.backgroundImage && style.backgroundImage !== 'none') { const bgImage = style.backgroundImage.toLowerCase(); if (bgImage.includes('captcha') || bgImage.includes('code') || bgImage.includes('verify') || bgImage.includes('validate')) { currentScore = 6; isCandidate = true; } } } if (isCandidate) { const width = el.offsetWidth; const height = el.offsetHeight; if (width >= 50 && width <= 250 && height >= 20 && height <= 100) { currentScore += 3; } const classIdString = `${el.className || ''} ${el.id || ''} ${el.parentElement?.className || ''} ${el.parentElement?.id || ''}`.toLowerCase(); if (classIdString.includes('captcha') || classIdString.includes('code') || classIdString.includes('verify') || classIdString.includes('vcode')) { currentScore += 2; } } if (isCandidate && currentScore > imageScore) { imageScore = currentScore; bestImageElement = el; } } } if (bestImageElement) { } else { } const potentialInputs = deepQuerySelectorAll('input, textarea'); for (const inp of potentialInputs) { if (isVisible(inp)) { let currentScore = 0; let isCandidate = false; const type = (inp.type || '').toLowerCase(); if (inp.tagName === 'TEXTAREA') { currentScore = 5; isCandidate = true; } else if (inp.tagName === 'INPUT' && type !== 'hidden' && type !== 'checkbox' && type !== 'radio' && type !== 'button' && type !== 'submit' && type !== 'reset' && type !== 'file' && type !== 'image') { currentScore = 5; isCandidate = true; } if (isCandidate) { const placeholder = (inp.placeholder || '').toLowerCase(); if (placeholder.includes('验证码') || placeholder.includes('captcha') || placeholder.includes('code') || placeholder.includes('校验码') || placeholder.includes('验证') || placeholder.includes('verif')) { currentScore += 10; } const classIdString = `${inp.className || ''} ${inp.id || ''} ${inp.parentElement?.className || ''} ${inp.parentElement?.id || ''}`.toLowerCase(); if (classIdString.includes('captcha') || classIdString.includes('code') || classIdString.includes('verify') || classIdString.includes('vcode')) { currentScore += 2; } } if (isCandidate && currentScore > inputScore) { inputScore = currentScore; bestInputElement = inp; } } } if (bestInputElement) { } else { } return { element: bestImageElement, input: bestInputElement, imageScore, inputScore }; } function getDistanceBetweenElements(el1, el2) { if (!el1 || !el2) return Infinity; try { const rect1 = el1.getBoundingClientRect(); const rect2 = el2.getBoundingClientRect(); const center1 = { x: rect1.left + rect1.width / 2, y: rect1.top + rect1.height / 2 }; const center2 = { x: rect2.left + rect2.width / 2, y: rect2.top + rect2.height / 2 }; const distance = Math.sqrt(Math.pow(center1.x - center2.x, 2) + Math.pow(center1.y - center2.y, 2)); return distance; } catch (e) { return Infinity; } } let findElementRetryCount = 0; const MAX_FIND_RETRIES = 5; const RETRY_DELAY_MS = 500; const MIN_IMAGE_SCORE = 8; const MIN_INPUT_SCORE = 15; const MAX_DISTANCE_PX = 350; function pageChange() { if (focusListenerAttached) { findElementRetryCount = 0; return; } let findResult = null; if (exist && localRules && localRules.type) { const found = findElementAndInput(localRules); findResult = { element: found.element, input: found.input, imageScore: 99, inputScore: 99 }; } else if (useGenericRule) { findResult = findGenericCaptchaElements(); } else { findElementRetryCount = 0; return; } let isValidPair = false; if (findResult && findResult.element && findResult.input) { if (useGenericRule) { if (findResult.imageScore >= MIN_IMAGE_SCORE && findResult.inputScore >= MIN_INPUT_SCORE) { const distance = getDistanceBetweenElements(findResult.element, findResult.input); if (distance <= MAX_DISTANCE_PX) { isValidPair = true; } } } else { isValidPair = true; } } if (isValidPair) { element = findResult.element; input = findResult.input; if (!focusListenerAttached) { input.addEventListener('focus', handleInputFocus); focusListenerAttached = true; findElementRetryCount = 0; } } else { findElementRetryCount++; if (findElementRetryCount < MAX_FIND_RETRIES) { setTimeout(pageChange, RETRY_DELAY_MS); } else { findElementRetryCount = 0; } } } function handleInputFocus() { if (exist && localRules && localRules.type) { const ruleYmType = localRules.ymType || '10110'; if (localRules.type === 'img') { if (isCORS()) { p2().then(() => { codeByRule(ruleYmType, false); }).catch(err => { topNotice("处理跨域图片时出错。"); }); } else { codeByRule(ruleYmType, false); } } else if (localRules.type === 'canvas') { canvasRule(ruleYmType, false); } } else if (useGenericRule) { if (!element || !input) { return; } const genericYmType = '10110'; if (element.tagName === 'IMG') { if (isCORS()) { p2().then(() => { codeByRule(genericYmType, true); }).catch(err => { topNotice("处理跨域图片时出错(通用规则)。"); }); } else { codeByRule(genericYmType, true); } } else if (element.tagName === 'CANVAS') { canvasRule(genericYmType, true); } } } function topNotice(msg) { const div = document.createElement('div'); div.id = 'topNotice'; div.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 5%; z-index: 9999999999; background: rgba(117,140,148,1); display: flex; justify-content: center; align-items: center; color: #fff; font-family: "Microsoft YaHei"; text-align: center;'; div.innerHTML = msg; div.style.fontSize = 'medium'; document.body.appendChild(div); setTimeout(function () { document.body.removeChild(document.getElementById('topNotice')); }, 3500); } GM_addStyle(` #${ID_PANEL} { position: fixed; top: 50%; /* Center vertically */ left: 50%; transform: translate(-50%, -50%); /* Center horizontally */ width: 1200px; /* Increased width */ max-width: 90%; /* Max width for smaller screens */ max-height: 80vh; /* Limit height */ background-color: #f8f9fa; /* Lighter background */ border: none; /* Remove border */ border-radius: 12px; /* More rounded corners */ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); /* Softer shadow */ z-index: 9999999999; /* 明确字体和颜色 */ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; /* System fonts */ color: #343a40; /* Darker text */ display: flex; /* Use flexbox for layout */ flex-direction: column; /* Stack header, content, footer */ overflow: hidden; /* Prevent content overflow */ box-sizing: border-box; /* 保留这个,通常有益 */ } /* 确保内部元素也使用 border-box */ #${ID_PANEL} *, #${ID_PANEL} *::before, #${ID_PANEL} *::after { box-sizing: inherit; } .settingsPanelHeader { padding: 16px 24px; /* More padding */ background-color: #ffffff; /* White header */ border: none; /* Ensure no inherited border */ border-bottom: 1px solid #dee2e6; /* Lighter border */ border-radius: 12px 12px 0 0; /* Ensure radius applies */ display: flex; justify-content: space-between; align-items: center; font-size: 1.1rem; /* Slightly larger font */ font-weight: 600; /* Bolder */ color: #495057; /* Header text color */ flex-shrink: 0; /* 防止 Header 收缩 */ } .settingsPanelCloseButton { background: none; border: none; font-size: 24px; font-weight: normal; /* Less bold */ cursor: pointer; color: #adb5bd; /* Lighter close button */ padding: 5px; line-height: 1; } .settingsPanelCloseButton:hover { color: #495057; /* Darken on hover */ } .settingsPanelContent { padding: 24px; /* More padding */ background-color: #f8f9fa; /* Match panel background */ overflow-y: auto; /* Enable scrolling for content */ flex-grow: 1; /* Allow content to take available space */ color: #343a40; /* Ensure content text color */ } .settingsSection { margin: 0 0 24px 0; /* Reset margin, apply bottom margin */ padding: 0 0 16px 0; /* Reset padding, apply bottom padding */ border: none; /* Reset border */ border-bottom: 1px solid #e9ecef; /* Separator line */ } .settingsSection:last-child { margin-bottom: 0; /* No margin for last section */ border-bottom: none; /* No line for last section */ } .settingsSection h3 { margin: 0 0 12px 0; /* Reset margin, apply bottom margin */ padding: 0; /* Reset padding */ font-size: 1rem; /* Standard font size */ font-weight: 600; color: #495057; border: none; /* Ensure no border */ background: none; /* Ensure no background */ text-align: left; /* Explicitly set */ } .settingsSection p { /* Style paragraph text */ margin: 0 0 12px 0; /* Reset margin, apply bottom */ padding: 0; /* Reset padding */ line-height: 1.6; color: #6c757d; font-size: 0.9rem; /* Set consistent paragraph font size */ border: none; /* Ensure no border */ background: none; /* Ensure no background */ text-align: left; /* Explicitly set */ } .${CLASS_SETTINGS_BUTTON} { padding: 10px 18px; /* Larger button */ border: none; /* Remove border */ background-color: #007bff; color: #ffffff; /* Ensure text color */ border-radius: 6px; /* Slightly rounded */ cursor: pointer; font-size: 0.9rem; /* Ensure font size */ font-weight: 500; /* Ensure font weight */ font-family: inherit; /* Inherit font from panel */ line-height: normal; /* Normal line height */ text-align: center; /* Ensure text is centered */ vertical-align: middle; /* Align if needed */ transition: background-color 0.2s ease; /* Smooth transition */ display: inline-flex; /* Use inline-flex for button layout */ align-items: center; /* Vertically center content */ justify-content: center; /* Horizontally center content */ margin: 0 5px 5px 0; /* Add some margin for spacing */ } .${CLASS_SETTINGS_BUTTON}:hover { background-color: #0056b3; } .${CLASS_SETTINGS_BUTTON}:disabled { background-color: #ced4da; cursor: not-allowed; } /* Style input fields if you add them later */ .settingsInput { padding: 10px 12px; border: 1px solid #ced4da; /* Explicit border */ border-radius: 6px; width: 100%; /* Full width */ box-sizing: border-box; /* Include padding in width */ margin: 0 0 12px 0; /* Reset margin, add bottom */ font-size: 0.9rem; /* Explicit font size */ font-family: inherit; /* Inherit font */ color: #495057; /* Set text color */ background-color: #ffffff; /* Set background */ } .settingsInput:focus { border-color: #80bdff; outline: none; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } /* Table Styles - Make them more explicit */ #${ID_PANEL} table { width: 100%; border-collapse: collapse; /* Explicitly set */ border-spacing: 0; /* Explicitly set */ margin-top: 10px; table-layout: fixed; font-size: 0.9rem; /* Set base table font size */ background-color: #ffffff; /* Ensure table background */ } /* Base styles for TH and TD */ #${ID_PANEL} .rules-table-th, #${ID_PANEL} .rules-table-td, #${ID_PANEL} .blacklist-table-th, /* Also define base for blacklist table */ #${ID_PANEL} .blacklist-table-td { border: 1px solid #ddd; padding: 10px 12px; vertical-align: middle; text-align: left; color: #343a40; line-height: 1.5; } /* Header specific styles */ #${ID_PANEL} .rules-table-th, #${ID_PANEL} .blacklist-table-th { background-color: #f2f2f2; font-weight: 600; color: #495057; } /* Rules Table Specific Alignments & Widths */ #${ID_PANEL} .rules-table-th.header-url, #${ID_PANEL} .rules-table-td.cell-url { width: 65%; word-break: break-all; /* Only for URL cell */ } #${ID_PANEL} .rules-table-th.header-type, #${ID_PANEL} .rules-table-td.cell-type, #${ID_PANEL} .rules-table-th.header-captcha-type, #${ID_PANEL} .rules-table-td.cell-captcha-type, #${ID_PANEL} .rules-table-th.header-ym-type, #${ID_PANEL} .rules-table-td.cell-ym-type { width: 15%; text-align: center; } #${ID_PANEL} .rules-table-th.header-action, #${ID_PANEL} .rules-table-td.cell-action { width: 20%; text-align: center; vertical-align: middle; /* Ensure vertical alignment */ } /* Action Cell specific layout - Now relies on cell alignment */ /* #${ID_PANEL} .rules-table-td.cell-action { } */ /* Remove .action-button class, apply styles directly to buttons in cell */ /* #${ID_PANEL} .rules-table-td .action-button { padding: 6px 10px; margin: 2px; } */ #${ID_PANEL} .rules-table-td.cell-action .${CLASS_SETTINGS_BUTTON} { padding: 6px 10px; /* Apply padding here */ margin: 2px; /* Apply margin here */ vertical-align: middle; /* Explicitly align buttons */ } /* Simplify delete button style class */ #${ID_PANEL} .rules-table-td .button-delete { background-color: #dc3545; /* Inherits base .settingsButton styles */ } /* Remove old delete button class */ /* #${ID_PANEL} .rules-table-td .action-button-delete { background-color: #dc3545; } */ `); function showSettingsPanel() { if (document.getElementById(ID_PANEL)) { return; } const panel = document.createElement('div'); panel.id = ID_PANEL; const header = document.createElement('div'); header.className = 'settingsPanelHeader'; header.innerHTML = '自动识别填充验证码 - 设置'; const closeButton = document.createElement('button'); closeButton.className = 'settingsPanelCloseButton'; closeButton.innerHTML = '×'; closeButton.onclick = () => { panel.remove(); }; header.appendChild(closeButton); const content = document.createElement('div'); content.className = 'settingsPanelContent'; content.innerHTML = '
正在加载设置...
'; panel.appendChild(header); panel.appendChild(content); document.body.appendChild(panel); renderSettingsContent(content); } const ID_EXPORT_DIALOG = 'captchaHelperExportDialog'; function showExportOptionsDialog() { if (document.getElementById(ID_EXPORT_DIALOG)) { return; } const dialog = document.createElement('div'); dialog.id = ID_EXPORT_DIALOG; dialog.style.position = 'fixed'; dialog.style.left = '50%'; dialog.style.top = '50%'; dialog.style.transform = 'translate(-50%, -50%)'; dialog.style.backgroundColor = 'white'; dialog.style.padding = '20px'; dialog.style.border = '1px solid #ccc'; dialog.style.borderRadius = '8px'; dialog.style.boxShadow = '0 4px 15px rgba(0,0,0,0.2)'; dialog.style.zIndex = '10000000000'; dialog.style.minWidth = '350px'; const header = document.createElement('h3'); header.textContent = '导出配置选项'; header.style.marginTop = '0'; header.style.marginBottom = '15px'; dialog.appendChild(header); const createCheckbox = (id, labelText, isChecked = true) => { const wrapper = document.createElement('div'); wrapper.style.marginBottom = '8px'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = id; checkbox.checked = isChecked; checkbox.style.marginRight = '8px'; const label = document.createElement('label'); label.htmlFor = id; label.textContent = labelText; label.style.cursor = 'pointer'; wrapper.appendChild(checkbox); wrapper.appendChild(label); return wrapper; }; const rulesCheckbox = createCheckbox('export-dialog-rules', '本地规则'); dialog.appendChild(rulesCheckbox); const blacklistCheckbox = createCheckbox('export-dialog-blacklist', '黑名单'); dialog.appendChild(blacklistCheckbox); const credentialsCheckboxWrapper = createCheckbox('export-dialog-credentials', '凭证 (Token, WebDAV)', false); const warningSpan = document.createElement('span'); warningSpan.textContent = ' (警告: 敏感信息!) '; warningSpan.style.color = 'red'; warningSpan.style.fontSize = '0.8em'; credentialsCheckboxWrapper.querySelector('label').appendChild(warningSpan); dialog.appendChild(credentialsCheckboxWrapper); const buttonContainer = document.createElement('div'); buttonContainer.style.marginTop = '20px'; buttonContainer.style.textAlign = 'right'; const cancelButton = document.createElement('button'); cancelButton.textContent = '取消'; cancelButton.className = CLASS_SETTINGS_BUTTON; cancelButton.style.backgroundColor = '#6c757d'; cancelButton.style.marginRight = '10px'; cancelButton.onclick = () => { dialog.remove(); }; buttonContainer.appendChild(cancelButton); const exportButton = document.createElement('button'); exportButton.textContent = '确认导出'; exportButton.className = CLASS_SETTINGS_BUTTON; exportButton.onclick = () => { exportAllData('captcha_helper_config.json'); }; buttonContainer.appendChild(exportButton); dialog.appendChild(buttonContainer); document.body.appendChild(dialog); } function renderSettingsContent(contentElement) { contentElement.innerHTML = ''; // --- 操作说明 Section --- const instructionsSection = document.createElement('div'); instructionsSection.className = 'settingsSection'; instructionsSection.innerHTML = `1. 云码 Token: 必须设置云码 Token (来自 www.jfbym.com) 才能进行识别。点击 "设置/修改 Token" 按钮进行配置。
2. 添加规则: 在网页空白处点击浏览器扩展菜单(油猴图标),选择 "添加当前页面规则"。然后按提示先右键点击验证码图片,再左键点击验证码输入框,最后选择识别类型。
3. 通用规则: 如果当前页面没有匹配的本地规则,脚本会尝试自动查找验证码图片和输入框,并使用默认类型 (10110) 进行识别。如果通用规则识别失败或不准确,请手动添加规则。
4. 黑名单: 在此添加 URL 包含的字符串,脚本将不会在匹配的页面上运行。
5. WebDAV 同步: 配置 WebDAV 服务器信息后,可将规则、黑名单和 Token 备份到云端,或从云端下载合并配置,方便跨设备同步。
6. 导入/导出: 可将规则、黑名单和凭证(可选)导出为 JSON 文件备份,或从 JSON 文件导入配置。
`; // Make links open in new tab instructionsSection.querySelectorAll('a').forEach(a => a.target = '_blank'); contentElement.appendChild(instructionsSection); // --- Token Section --- const tokenSection = document.createElement('div'); tokenSection.className = 'settingsSection'; tokenSection.innerHTML = '规则列表将显示在此处。
'; contentElement.appendChild(rulesSection); renderRulesTable(rulesSection); const blacklistSection = document.createElement('div'); blacklistSection.className = 'settingsSection'; blacklistSection.innerHTML = '黑名单列表将显示在此处。
'; contentElement.appendChild(blacklistSection); renderBlacklistTable(blacklistSection); const webdavSection = document.createElement('div'); webdavSection.className = 'settingsSection'; webdavSection.innerHTML = '点击"从云端下载"按钮以列出可用的备份文件。
'; webdavFlexContainer.appendChild(backupListContainer); webdavSection.appendChild(webdavFlexContainer); contentElement.appendChild(webdavSection); } function saveWebdavSettings() { const url = document.getElementById('webdavUrlInput')?.value.trim() || ''; const user = document.getElementById('webdavUserInput')?.value.trim() || ''; const pass = document.getElementById('webdavPassInput')?.value || ''; if (!url) { alert('请输入 WebDAV 服务器 URL。'); return; } try { new URL(url); } catch (e) { alert('输入的 URL 格式无效。'); return; } GM_setValue('webdavUrl', url); GM_setValue('webdavUser', user); GM_setValue('webdavPass', pass); topNotice('WebDAV 设置已保存。'); } function GM_xhr_WebDAV(options) { return new Promise((resolve, reject) => { const baseUrl = GM_getValue('webdavUrl', ''); const user = GM_getValue('webdavUser', ''); const pass = GM_getValue('webdavPass', ''); if (!baseUrl) { return reject({ status: -1, statusText: 'WebDAV URL not configured' }); } const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'; const targetUrl = normalizedBaseUrl + (options.urlPath || ''); const defaultHeaders = { }; if (user || pass) { defaultHeaders['Authorization'] = 'Basic ' + btoa(user + ':' + pass); } const finalHeaders = { ...defaultHeaders, ...options.headers }; GM_xmlhttpRequest({ method: options.method || 'GET', url: targetUrl, headers: finalHeaders, data: options.data, responseType: options.responseType || 'json', timeout: options.timeout || 30000, onload: function (response) { if (response.status >= 200 && response.status < 300) { resolve(response); } else { resolve(response); } }, onerror: function (response) { reject(response); }, ontimeout: function (response) { reject(response); }, }); }); } function getCurrentTimestampFilename() { const now = new Date(); const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0'); const hours = now.getHours().toString().padStart(2, '0'); const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); return `${year}_${month}_${day}_${hours}_${minutes}_${seconds}.json`; } async function uploadToWebdav() { const webdavUrl = GM_getValue('webdavUrl', ''); if (!webdavUrl) { alert('请先配置并保存 WebDAV 设置。'); return; } topNotice('正在准备上传配置...'); const rulesData = GM_getValue(KEY_LOCAL_RULES, {}); const blacklistData = GM_getValue(KEY_BLACKLIST, []); const yunmaToken = GM_getValue(KEY_TOKEN, ''); const combinedData = { rules: rulesData, blacklist: blacklistData, credentials: { yunmaToken: yunmaToken } }; let jsonData; try { jsonData = JSON.stringify(combinedData, null, 2); } catch (error) { topNotice('错误:无法序列化配置数据。'); return; } const timestampFilename = getCurrentTimestampFilename(); const targetFilePath = WEBDAV_SYNC_DIR + '/' + timestampFilename; const tryUpload = async (filePath, currentJsonData) => { topNotice(`尝试上传配置到 ${filePath}...`); try { const response = await GM_xhr_WebDAV({ method: 'PUT', urlPath: filePath, headers: { 'Content-Type': 'application/json;charset=utf-8' }, data: currentJsonData, responseType: 'text' }); if (response.status === 201 || response.status === 204) { topNotice('配置成功上传到 WebDAV!'); return true; } else if (response.status === 404 || response.status === 409) { topNotice(`上传失败 (${response.status}),尝试创建目录 ${WEBDAV_SYNC_DIR}...`); return false; } else { topNotice(`上传失败,服务器返回状态: ${response.status} ${response.statusText}`); return null; } } catch (error) { topNotice('上传配置时发生网络错误或超时。'); return null; } }; const createDirectory = async (dirPath) => { topNotice(`正在创建目录: ${dirPath}...`); try { const response = await GM_xhr_WebDAV({ method: 'MKCOL', urlPath: dirPath, responseType: 'text' }); if (response.status === 201) { topNotice(`目录 ${dirPath} 创建成功。`); return true; } else if (response.status === 405) { topNotice(`目录 ${dirPath} 已存在。`); return true; } else { topNotice(`创建目录 ${dirPath} 失败: ${response.status} ${response.statusText}`); return false; } } catch (error) { topNotice(`创建目录 ${dirPath} 时发生网络错误或超时。`); return false; } }; const dirCreatedOrExists = await createDirectory(WEBDAV_SYNC_DIR); if (dirCreatedOrExists) { const uploadResult = await tryUpload(targetFilePath, jsonData); if (uploadResult === false) { topNotice('上传配置失败,即使在检查目录后。请检查 WebDAV 服务器或权限。'); } } else { topNotice('无法创建 WebDAV 目录,上传中止。'); } } async function downloadFromWebdav() { const webdavUrl = GM_getValue('webdavUrl', ''); if (!webdavUrl) { alert('请先配置并保存 WebDAV 设置。'); return; } const listContainer = document.getElementById('webdavBackupListContainer'); if (!listContainer) { return; } topNotice(`正在列出云端目录 ${WEBDAV_SYNC_DIR} 中的备份文件...`); try { const propfindResponse = await GM_xhr_WebDAV({ method: 'PROPFIND', urlPath: WEBDAV_SYNC_DIR, headers: { 'Depth': '1', }, responseType: 'text' }); if (propfindResponse.status !== 207) { if (propfindResponse.status === 404) { listContainer.innerHTML = `错误:在云端找不到目录 ${WEBDAV_SYNC_DIR}。
`; topNotice(`错误:在云端找不到目录 ${WEBDAV_SYNC_DIR}。`); } else { listContainer.innerHTML = `列出文件失败,服务器返回状态: ${propfindResponse.status} ${propfindResponse.statusText}
`; topNotice(`列出文件失败,服务器返回状态: ${propfindResponse.status} ${propfindResponse.statusText}`); } return; } const parser = new DOMParser(); const xmlDoc = parser.parseFromString(propfindResponse.responseText, "application/xml"); const parserError = xmlDoc.getElementsByTagName("parsererror"); if (parserError.length > 0) { listContainer.innerHTML = '解析云端目录列表时出错 (XML 格式无效)。
'; topNotice("解析云端目录列表时出错 (XML 格式无效)。"); return; } const hrefs = xmlDoc.getElementsByTagNameNS('DAV:', 'href'); let jsonFiles = []; for (let i = 0; i < hrefs.length; i++) { const hrefText = hrefs[i].textContent; if (hrefText && hrefText.toLowerCase().endsWith('.json')) { const parts = hrefText.split('/').filter(part => part !== ''); if (parts.length >= 2) { const filename = parts[parts.length - 1]; const parentDir = parts[parts.length - 2]; if (parentDir === WEBDAV_SYNC_DIR) { if (/^\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}\.json$/.test(filename)) { jsonFiles.push(filename); } } } } } if (jsonFiles.length === 0) { listContainer.innerHTML = `在云端目录 ${WEBDAV_SYNC_DIR} 中没有找到有效的备份文件 (.json)。
`; topNotice(`在云端目录 ${WEBDAV_SYNC_DIR} 中没有找到有效的备份文件 (.json)。`); return; } jsonFiles.sort().reverse(); listContainer.innerHTML = ''; const table = document.createElement('table'); table.style.width = '100%'; table.style.borderCollapse = 'collapse'; const tbody = table.createTBody(); const filesToDisplay = jsonFiles.slice(0, 30); filesToDisplay.forEach(filename => { const row = tbody.insertRow(); const filenameCell = row.insertCell(); filenameCell.textContent = filename; filenameCell.style.padding = '10px 8px'; filenameCell.style.fontSize = '0.85rem'; filenameCell.style.fontFamily = 'monospace'; filenameCell.style.wordBreak = 'break-all'; filenameCell.style.verticalAlign = 'middle'; filenameCell.style.color = '#343a40'; filenameCell.style.borderBottom = '1px solid #ddd'; const buttonCell = row.insertCell(); buttonCell.style.textAlign = 'right'; buttonCell.style.padding = '8px 5px'; buttonCell.style.width = 'auto'; buttonCell.style.whiteSpace = 'nowrap'; buttonCell.style.verticalAlign = 'middle'; buttonCell.style.borderBottom = '1px solid #ddd'; const btnContainer = document.createElement('div'); btnContainer.style.display = 'inline-flex'; btnContainer.style.gap = '5px'; btnContainer.style.justifyContent = 'flex-end'; const downloadBtn = document.createElement('button'); downloadBtn.textContent = '下载合并'; downloadBtn.title = '下载此备份并合并到本地配置'; downloadBtn.className = CLASS_SETTINGS_BUTTON; downloadBtn.style.padding = '4px 8px'; downloadBtn.style.fontSize = '0.8rem'; downloadBtn.style.margin = '0'; downloadBtn.onclick = () => { downloadBtn.disabled = true; downloadBtn.textContent = '处理中...'; const deleteBtn = downloadBtn.nextSibling; if (deleteBtn) deleteBtn.disabled = true; downloadAndMergeSpecificBackup(filename).finally(() => { if (document.body.contains(downloadBtn)) { downloadBtn.disabled = false; downloadBtn.textContent = '下载合并'; if (deleteBtn && document.body.contains(deleteBtn)) deleteBtn.disabled = false; } }); }; btnContainer.appendChild(downloadBtn); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.title = '从云端删除此备份文件'; deleteBtn.className = CLASS_SETTINGS_BUTTON; deleteBtn.style.padding = '4px 8px'; deleteBtn.style.fontSize = '0.8rem'; deleteBtn.style.margin = '0'; deleteBtn.style.backgroundColor = '#dc3545'; deleteBtn.style.color = 'white'; deleteBtn.onclick = () => { if (confirm(`确定要从云端删除备份文件 "${filename}" 吗?\n此操作不可恢复!`)) { deleteBtn.disabled = true; deleteBtn.textContent = '删除中...'; const siblingDownloadBtn = deleteBtn.previousSibling; if (siblingDownloadBtn) siblingDownloadBtn.disabled = true; deleteSpecificBackup(filename).finally(() => { const currentPanel = document.getElementById(ID_PANEL); if (currentPanel) { downloadFromWebdav(); } else { } }); } }; btnContainer.appendChild(deleteBtn); buttonCell.appendChild(btnContainer); }); listContainer.appendChild(table); topNotice(`成功列出 ${filesToDisplay.length} 个备份文件。`); } catch (error) { listContainer.innerHTML = '处理下载列表时发生错误。
'; if (error && error.statusText) { topNotice('与 WebDAV 服务器通信时发生网络错误或超时。'); } else { topNotice('处理下载列表时发生内部错误。'); } } } async function downloadAndMergeSpecificBackup(selectedFilename) { if (!selectedFilename) { return; } const targetFilePath = WEBDAV_SYNC_DIR + '/' + selectedFilename; topNotice(`正在下载选定的备份文件: ${selectedFilename}...`); try { const getResponse = await GM_xhr_WebDAV({ method: 'GET', urlPath: targetFilePath, responseType: 'json' }); if (getResponse.status === 200) { const downloadedData = getResponse.response; if (typeof downloadedData !== 'object' || downloadedData === null || ( !downloadedData.hasOwnProperty('rules') && !downloadedData.hasOwnProperty('blacklist') && !downloadedData.hasOwnProperty('credentials') )) { topNotice('下载失败:选择的备份文件格式无效或缺少必要数据。'); return; } const rulesFromCloud = downloadedData.rules || {}; const blacklistFromCloud = downloadedData.blacklist || []; const credentialsFromCloud = downloadedData.credentials || {}; if (typeof rulesFromCloud !== 'object' || Array.isArray(rulesFromCloud)) { topNotice('下载失败:选择的备份文件 rules 数据格式必须是一个对象。'); return; } if (!Array.isArray(blacklistFromCloud)) { topNotice('下载失败:选择的备份文件 blacklist 数据格式必须是一个数组。'); return; } const cloudRuleCount = Object.keys(rulesFromCloud).length; const cloudBlacklistCount = blacklistFromCloud.length; const hasCloudToken = credentialsFromCloud.hasOwnProperty('yunmaToken') && credentialsFromCloud.yunmaToken; let confirmMergeMsg = `从云端备份 ${selectedFilename} 下载了 ${cloudRuleCount} 条规则和 ${cloudBlacklistCount} 条黑名单记录。\n`; if (hasCloudToken) { confirmMergeMsg += "备份中还包含云码 Token。\n"; } confirmMergeMsg += "\n确定要合并规则和黑名单,并用备份中的 Token (如有) 覆盖当前 Token 吗?\n(规则和黑名单仅添加,不覆盖。WebDAV设置不会被修改。)"; if (!confirm(confirmMergeMsg)) { topNotice('下载合并操作已取消。'); return; } try { let currentRules = GM_getValue(KEY_LOCAL_RULES, {}); let currentBlacklist = GM_getValue(KEY_BLACKLIST, []); let rulesAdded = 0; let blacklistAdded = 0; for (const url in rulesFromCloud) { if (rulesFromCloud.hasOwnProperty(url) && !currentRules.hasOwnProperty(url)) { currentRules[url] = rulesFromCloud[url]; rulesAdded++; } } for (const item of blacklistFromCloud) { if (!currentBlacklist.includes(item)) { currentBlacklist.push(item); blacklistAdded++; } } GM_setValue(KEY_LOCAL_RULES, currentRules); GM_setValue(KEY_BLACKLIST, currentBlacklist); let tokenImported = false; if (hasCloudToken) { GM_setValue(KEY_TOKEN, credentialsFromCloud.yunmaToken); tokenImported = true; } let noticeMsg = `配置合并完成!从备份 ${selectedFilename} 新增规则: ${rulesAdded}, 新增黑名单: ${blacklistAdded}.`; if (tokenImported) { noticeMsg += " 云码 Token 已从备份中恢复。"; } topNotice(noticeMsg); const settingsPanelContent = document.querySelector(`#${ID_PANEL} .settingsPanelContent`); if (settingsPanelContent) { renderSettingsContent(settingsPanelContent); } else { downloadFromWebdav(); } } catch (saveError) { topNotice('保存合并后的配置时出错。'); } } else if (getResponse.status === 404) { topNotice('下载失败:无法找到选定的备份文件(可能已被删除?)。'); } else { topNotice(`下载选定的备份文件失败,服务器返回状态: ${getResponse.status} ${getResponse.statusText}`); } } catch (error) { if (error && error.statusText) { topNotice('下载指定备份文件时发生网络错误或超时。'); } else { topNotice('处理下载合并操作时发生内部错误。'); } } } async function deleteSpecificBackup(filenameToDelete) { if (!filenameToDelete) { return false; } const targetFilePath = WEBDAV_SYNC_DIR + '/' + filenameToDelete; topNotice(`正在从云端删除备份文件: ${filenameToDelete}...`); try { const deleteResponse = await GM_xhr_WebDAV({ method: 'DELETE', urlPath: targetFilePath, responseType: 'text' }); if (deleteResponse.status === 204) { topNotice(`备份文件 "${filenameToDelete}" 已成功删除。`); return true; } else if (deleteResponse.status === 404) { topNotice(`删除失败:在云端找不到文件 "${filenameToDelete}"(可能已被删除)。`); return false; } else { topNotice(`删除文件 "${filenameToDelete}" 失败,服务器返回: ${deleteResponse.status} ${deleteResponse.statusText}`); return false; } } catch (error) { if (error && error.statusText) { topNotice('删除指定备份文件时发生网络错误或超时。'); } else { topNotice('处理删除操作时发生内部错误。'); } return false; } } function exportAllData(defaultFilename) { const dialog = document.getElementById(ID_EXPORT_DIALOG); if (!dialog) { topNotice("错误:无法找到导出选项对话框。"); return; } const exportRules = dialog.querySelector('#export-dialog-rules')?.checked || false; const exportBlacklist = dialog.querySelector('#export-dialog-blacklist')?.checked || false; let exportCredentials = dialog.querySelector('#export-dialog-credentials')?.checked || false; if (!exportRules && !exportBlacklist && !exportCredentials) { topNotice("请至少选择一个要导出的配置项。"); return; } const combinedData = {}; let exportedItems = []; let yunmaToken = '', webdavUrl = '', webdavUser = '', webdavPass = ''; if (exportRules) { combinedData.rules = GM_getValue(KEY_LOCAL_RULES, {}); if (Object.keys(combinedData.rules).length > 0) { exportedItems.push("规则"); } } if (exportBlacklist) { combinedData.blacklist = GM_getValue(KEY_BLACKLIST, []); if (combinedData.blacklist.length > 0) { exportedItems.push("黑名单"); } } if (exportCredentials) { yunmaToken = GM_getValue(KEY_TOKEN, ''); webdavUrl = GM_getValue('webdavUrl', ''); webdavUser = GM_getValue('webdavUser', ''); webdavPass = GM_getValue('webdavPass', ''); if (!confirm( "请再次确认:您选择导出凭证信息!\n\n" + "导出的文件将包含以下未加密的敏感信息:\n" + `- 云码 Token: ${yunmaToken ? '是' : '否'}\n` + `- WebDAV URL: ${webdavUrl ? '是' : '否'}\n` + `- WebDAV 用户名: ${webdavUser ? '是' : '否'}\n` + `- WebDAV 密码/令牌: ${webdavPass ? '是' : '否'}\n\n` + "请务必妥善保管此文件!确定要继续导出吗?" )) { topNotice("导出凭证操作已取消。将仅导出其他已选项。", 5000); exportCredentials = false; return; } else { if (yunmaToken || webdavUrl || webdavUser || webdavPass) { combinedData.credentials = { yunmaToken: yunmaToken, webdavUrl: webdavUrl, webdavUser: webdavUser, webdavPass: webdavPass }; exportedItems.push("凭证"); } else { exportCredentials = false; } } } if (Object.keys(combinedData).length === 0) { topNotice("未选择任何包含数据的配置项或已取消,操作中止。"); return; } try { const jsonData = JSON.stringify(combinedData, null, 2); const blob = new Blob([jsonData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = defaultFilename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); const finalExportedList = exportedItems.length > 0 ? exportedItems.join(', ') : "空内容"; topNotice(`配置已导出 (${finalExportedList}) 为 ${defaultFilename}` + (exportCredentials ? " (包含敏感凭证!)" : "")); dialog.remove(); } catch (error) { alert('导出配置时发生错误。'); } } function renderRulesTable(containerElement) { containerElement.innerHTML = '暂无本地规则。
'; return; } const table = document.createElement('table'); const thead = table.createTHead(); const headerRow = thead.insertRow(); const headers = ['URL (部分)', '元素类型', '验证码类型', '云码类型', '操作']; const headerClasses = ['header-url', 'header-type', 'header-captcha-type', 'header-ym-type', 'header-action']; headers.forEach((text, index) => { const th = document.createElement('th'); th.textContent = text; th.classList.add('rules-table-th'); if (headerClasses[index]) { th.classList.add(headerClasses[index]); } headerRow.appendChild(th); }); const tbody = table.createTBody(); const cellClasses = ['cell-url', 'cell-type', 'cell-captcha-type', 'cell-ym-type', 'cell-action']; urls.forEach(url => { const rule = allRules[url]; const row = tbody.insertRow(); const createCell = (content, title, index) => { const cell = row.insertCell(); cell.textContent = content; if (title) cell.title = title; cell.classList.add('rules-table-td'); if (cellClasses[index]) { cell.classList.add(cellClasses[index]); } return cell; }; createCell(url.length > 50 ? url.substring(0, 47) + '...' : url, url, 0); createCell(rule.type || 'N/A', null, 1); createCell(rule.captchaType === 'math' ? '算术' : '字符', null, 2); createCell(rule.ymType || (rule.captchaType === 'math' ? '50100' : '10110 (默认)'), null, 3); const actionCell = createCell('', null, 4); const modifyButton = document.createElement('button'); modifyButton.textContent = '修改类型'; modifyButton.className = CLASS_SETTINGS_BUTTON; modifyButton.onclick = () => { modifyRuleTypeFromPanel(url, containerElement); }; actionCell.appendChild(modifyButton); const deleteButton = document.createElement('button'); deleteButton.textContent = '删除'; deleteButton.className = `${CLASS_SETTINGS_BUTTON} button-delete`; deleteButton.onclick = () => { if (confirm(`确定要删除规则 ${url} 吗?`)) { delR({ url: url }).then(res => { if (res.status === 200) { topNotice('规则已删除'); renderRulesTable(containerElement); } else { topNotice('删除规则失败'); } }).catch(err => { topNotice('删除规则时出错'); }); } }; actionCell.appendChild(deleteButton); }); containerElement.appendChild(table); } function renderBlacklistTable(containerElement) { containerElement.innerHTML = '