// ==UserScript== // @name 自动识别填充网页验证码-重制版 // @namespace http://tampermonkey.net/ // @version 1.2.0 // @description 自动识别填写大部分网站的验证码 (本地存储+云码) // @author lcymzzZ (Original), Lanxi (Modifier) // @license GPL Licence // @connect https://www.jfbym.com // @connect https://dav.jianguoyun.com/dav // @match http://*/* // @match https://*/* // @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 noRuleFound = 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 { console.warn("无法找到 balanceDisplaySpan 元素来显示状态。"); } 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); } console.warn("查询余额成功但缺少 score 字段:", respData); } else { if (balanceSpan) { balanceSpan.textContent = '余额: 查询失败'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } console.error("查询余额 API 返回错误:", respData); } } 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); } console.error("处理余额响应时出错:", e, response.responseText); } }, onerror: function (response) { if (balanceSpan) { balanceSpan.textContent = '余额: 网络错误'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } console.error("查询余额网络错误:", response); }, ontimeout: function () { if (balanceSpan) { balanceSpan.textContent = '余额: 请求超时'; GM_setValue(KEY_LAST_BALANCE, balanceSpan.textContent); } console.error("查询余额请求超时"); } }); } //添加菜单 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) { console.error("保存规则到本地存储失败:", 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,删除规则失败 (存储错误)"); console.error("Error deleting rule:", err); }); } //删除规则(请求) -> 改为本地存储 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) { console.error("从本地存储删除规则失败:", error); reject({ status: 500, error: error }); } }); } function processCode(code, ruleYmType) { if (!code) return; const preCode = GM_getValue(KEY_PRE_CODE, ""); if (code !== preCode) { GM_setValue(KEY_PRE_CODE, code); p1(code, ruleYmType).then(ans => { if (ans) { writeIn1(ans); } }); } } function getBase64FromImageElement(imgElement, callback) { const src = imgElement.src; if (!src) { console.warn("Image source is missing."); callback(null); return; } if (src.startsWith('data:image')) { try { const base64 = src.split("base64,")[1]; callback(base64); } catch (e) { console.error("Error splitting data URI:", 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) { console.error("Error reading blob as data URL:", e); callback(null); } }; reader.onerror = (e) => { console.error("Error reading blob:", e); callback(null); } reader.readAsDataURL(blob); }) .catch(err => { console.error("Error fetching/reading blob URL:", src, 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) { console.error("Error converting loaded image to base64:", 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) { console.error("Error converting image to base64 on load:", err); callback(null); } }; imgElement.onerror = () => { console.error("Error loading image source:", imgElement.src); callback(null); }; } } } //按已存规则填充 function codeByRule() { if (!element || !(element instanceof HTMLImageElement)) { console.error("codeByRule: Invalid image element.", element); return; } if (!input || !(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) { console.error("codeByRule: Invalid input element.", input); return; } getBase64FromImageElement(element, (base64Code) => { if (base64Code) { processCode(base64Code, localRules.ymType); } }); } function canvasRule() { if (!element || !(element instanceof HTMLCanvasElement)) { console.error("canvasRule: Invalid canvas element.", element); return; } if (!input || !(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) { console.error("canvasRule: Invalid input element.", input); return; } try { const base64Code = element.toDataURL("image/png").split("base64,")[1]; processCode(base64Code, localRules.ymType); } catch (err) { console.error("Error getting base64 from canvas:", 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) { console.error("compareUrl: 读取本地规则失败:", error); localRules = {}; reject(error); } }); } //开始识别 function start() { compareUrl().then((isExist) => { if (isExist) { exist = true; noRuleFound = false; /* if (localRules["type"] == "img") { } else if (localRules["type"] == "canvas") { } */ } else { noRuleFound = true; } }).catch(error => { console.error("start: compareUrl rejected:", error); noRuleFound = true; console.warn("【自动识别填充验证码】加载本地规则失败,脚本停止运行此页面。"); }); } 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)) { console.error("Rule contains invalid index:", rule); 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) { console.error("Error finding element/input for rule:", rule, error); foundElement = null; foundInput = null; } return { element: foundElement, input: foundInput }; } //页面变化执行函数 - Now primarily finds elements and attaches focus listener function pageChange() { if (focusListenerAttached) { return; } if (exist && localRules && localRules.type) { const found = findElementAndInput(localRules); if (found.element && found.input) { element = found.element; input = found.input; if (!focusListenerAttached) { input.addEventListener('focus', handleInputFocus); focusListenerAttached = true; } } else { } } } function handleInputFocus() { if (!localRules || !localRules.type) { console.warn('[调试] handleInputFocus: No local rule or rule type found, cannot proceed.'); return; } if (localRules.type === 'img') { if (isCORS()) { p2().then(() => { codeByRule(); }).catch(err => console.error("p2 failed in handleInputFocus (img):", err)); } else { codeByRule(); } } else if (localRules.type === 'canvas') { canvasRule(); } } 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); } function renderSettingsContent(contentElement) { contentElement.innerHTML = ''; const tokenSection = document.createElement('div'); tokenSection.className = 'settingsSection'; tokenSection.innerHTML = '

云码 Token

'; const tokenLineContainer = document.createElement('div'); tokenLineContainer.style.display = 'flex'; tokenLineContainer.style.alignItems = 'center'; tokenLineContainer.style.justifyContent = 'space-between'; tokenLineContainer.style.marginBottom = '10px'; let currentToken = GM_getValue(KEY_TOKEN, ""); const tokenDisplay = document.createElement('p'); tokenDisplay.style.wordBreak = 'break-all'; tokenDisplay.textContent = `当前 Token: ${currentToken ? currentToken : '未设置'}`; tokenDisplay.style.margin = '0'; tokenLineContainer.appendChild(tokenDisplay); const balanceQueryContainer = document.createElement('div'); balanceQueryContainer.style.display = 'flex'; balanceQueryContainer.style.alignItems = 'center'; const balanceDisplaySpan = document.createElement('span'); balanceDisplaySpan.id = ID_BALANCE_SPAN; balanceDisplaySpan.textContent = GM_getValue(KEY_LAST_BALANCE, '余额: --'); balanceDisplaySpan.style.marginRight = '20px'; balanceDisplaySpan.style.fontSize = '0.9rem'; balanceDisplaySpan.style.color = '#6c757d'; balanceQueryContainer.appendChild(balanceDisplaySpan); const queryBalanceButton = document.createElement('button'); queryBalanceButton.textContent = '查询余额'; queryBalanceButton.className = CLASS_SETTINGS_BUTTON; queryBalanceButton.onclick = fetchAndDisplayBalance; balanceQueryContainer.appendChild(queryBalanceButton); tokenLineContainer.appendChild(balanceQueryContainer); tokenSection.appendChild(tokenLineContainer); const editTokenButton = document.createElement('button'); editTokenButton.textContent = '设置/修改 Token'; editTokenButton.className = CLASS_SETTINGS_BUTTON; editTokenButton.onclick = () => { saveToken(); renderSettingsContent(contentElement); }; tokenSection.appendChild(editTokenButton); const exportConfigButton = document.createElement('button'); exportConfigButton.textContent = '导出配置'; exportConfigButton.className = CLASS_SETTINGS_BUTTON; exportConfigButton.style.marginLeft = '10px'; exportConfigButton.onclick = () => exportAllData('captcha_config.json'); const importConfigButton = document.createElement('button'); importConfigButton.textContent = '导入配置'; importConfigButton.className = CLASS_SETTINGS_BUTTON; importConfigButton.style.marginLeft = '10px'; const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json,application/json'; fileInput.style.display = 'none'; importConfigButton.onclick = () => { fileInput.click(); }; fileInput.onchange = (event) => { const file = event.target.files[0]; if (file) { importAllData(file, contentElement); } event.target.value = null; }; tokenSection.appendChild(exportConfigButton); tokenSection.appendChild(importConfigButton); tokenSection.appendChild(fileInput); contentElement.appendChild(tokenSection); const rulesSection = document.createElement('div'); rulesSection.className = 'settingsSection'; rulesSection.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 = '

WebDAV 云同步

'; const webdavFlexContainer = document.createElement('div'); webdavFlexContainer.style.display = 'flex'; webdavFlexContainer.style.gap = '20px'; const webdavLeftPanel = document.createElement('div'); webdavLeftPanel.style.flex = '1'; webdavLeftPanel.style.minWidth = '300px'; const webdavUrlLabel = document.createElement('label'); webdavUrlLabel.textContent = '服务器 URL:'; webdavUrlLabel.style.display = 'block'; webdavUrlLabel.style.marginBottom = '5px'; const webdavUrlInput = document.createElement('input'); webdavUrlInput.id = 'webdavUrlInput'; webdavUrlInput.type = 'url'; webdavUrlInput.placeholder = '例如: https://dav.jianguoyun.com/dav/ (请确保以 / 结尾)'; webdavUrlInput.className = 'settingsInput'; webdavUrlInput.value = GM_getValue('webdavUrl', ''); webdavLeftPanel.appendChild(webdavUrlLabel); webdavLeftPanel.appendChild(webdavUrlInput); const webdavUserLabel = document.createElement('label'); webdavUserLabel.textContent = '用户名:'; webdavUserLabel.style.display = 'block'; webdavUserLabel.style.marginBottom = '5px'; const webdavUserInput = document.createElement('input'); webdavUserInput.id = 'webdavUserInput'; webdavUserInput.type = 'text'; webdavUserInput.className = 'settingsInput'; webdavUserInput.value = GM_getValue('webdavUser', ''); webdavLeftPanel.appendChild(webdavUserLabel); webdavLeftPanel.appendChild(webdavUserInput); const webdavPassLabel = document.createElement('label'); webdavPassLabel.textContent = '密码/应用令牌:'; webdavPassLabel.style.display = 'block'; webdavPassLabel.style.marginBottom = '5px'; const webdavPassInput = document.createElement('input'); webdavPassInput.id = 'webdavPassInput'; webdavPassInput.type = 'password'; webdavPassInput.className = 'settingsInput'; webdavPassInput.value = GM_getValue('webdavPass', ''); webdavLeftPanel.appendChild(webdavPassLabel); webdavLeftPanel.appendChild(webdavPassInput); const buttonContainer = document.createElement('div'); buttonContainer.style.marginTop = '10px'; const saveWebdavButton = document.createElement('button'); saveWebdavButton.textContent = '保存 WebDAV 设置'; saveWebdavButton.className = CLASS_SETTINGS_BUTTON; saveWebdavButton.onclick = saveWebdavSettings; buttonContainer.appendChild(saveWebdavButton); const uploadButton = document.createElement('button'); uploadButton.textContent = '上传到云端'; uploadButton.className = CLASS_SETTINGS_BUTTON; uploadButton.style.marginLeft = '10px'; uploadButton.onclick = uploadToWebdav; buttonContainer.appendChild(uploadButton); const downloadButton = document.createElement('button'); downloadButton.textContent = '从云端下载'; downloadButton.className = CLASS_SETTINGS_BUTTON; downloadButton.style.marginLeft = '10px'; downloadButton.onclick = downloadFromWebdav; buttonContainer.appendChild(downloadButton); webdavLeftPanel.appendChild(buttonContainer); webdavFlexContainer.appendChild(webdavLeftPanel); const backupListContainer = document.createElement('div'); backupListContainer.id = 'webdavBackupListContainer'; backupListContainer.style.flex = '1'; backupListContainer.style.minWidth = '300px'; backupListContainer.style.border = '1px solid #e9ecef'; backupListContainer.style.borderRadius = '6px'; backupListContainer.style.padding = '10px'; backupListContainer.style.maxHeight = '280px'; backupListContainer.style.overflowY = 'auto'; backupListContainer.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 }; console.debug(`[WebDAV Request] ${options.method} ${targetUrl}`, options); GM_xmlhttpRequest({ method: options.method || 'GET', url: targetUrl, headers: finalHeaders, data: options.data, responseType: options.responseType || 'json', timeout: options.timeout || 30000, onload: function(response) { console.debug(`[WebDAV Response] ${options.method} ${targetUrl} - Status: ${response.status}`, response); if (response.status >= 200 && response.status < 300) { resolve(response); } else { resolve(response); } }, onerror: function(response) { console.error(`[WebDAV Error] ${options.method} ${targetUrl} - Network Error`, response); reject(response); }, ontimeout: function(response) { console.error(`[WebDAV Error] ${options.method} ${targetUrl} - Timeout`, 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('错误:无法序列化配置数据。'); console.error('Error stringifying config data:', error); 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}`); console.error('WebDAV upload failed:', response); return null; } } catch (error) { topNotice('上传配置时发生网络错误或超时。'); console.error("WebDAV upload network error or timeout:", error); 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}`); console.error('WebDAV MKCOL failed:', response); return false; } } catch (error) { topNotice(`创建目录 ${dirPath} 时发生网络错误或超时。`); console.error("WebDAV MKCOL network error or timeout:", error); 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) { console.error("Cannot find webdavBackupListContainer element."); 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}。`); console.error(`PROPFIND failed: Directory ${WEBDAV_SYNC_DIR} not found (404)`); } else { listContainer.innerHTML = `

列出文件失败,服务器返回状态: ${propfindResponse.status} ${propfindResponse.statusText}

`; topNotice(`列出文件失败,服务器返回状态: ${propfindResponse.status} ${propfindResponse.statusText}`); console.error('WebDAV PROPFIND failed:', propfindResponse); } 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 格式无效)。"); console.error("XML parsing error:", parserError[0].textContent); 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 { console.log("Panel closed, not refreshing list after delete attempt."); } }); } }; btnContainer.appendChild(deleteBtn); buttonCell.appendChild(btnContainer); }); listContainer.appendChild(table); topNotice(`成功列出 ${filesToDisplay.length} 个备份文件。`); } catch (error) { listContainer.innerHTML = '

处理下载列表时发生错误。

'; if (error && error.statusText) { topNotice('与 WebDAV 服务器通信时发生网络错误或超时。'); console.error("WebDAV communication error:", error); } else { topNotice('处理下载列表时发生内部错误。'); console.error("Internal error during download list process:", error); } } } async function downloadAndMergeSpecificBackup(selectedFilename) { if (!selectedFilename) { console.error("downloadAndMergeSpecificBackup called without a filename."); 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('下载失败:选择的备份文件格式无效或缺少必要数据。'); console.error('Invalid downloaded data format:', downloadedData); return; } const rulesFromCloud = downloadedData.rules || {}; const blacklistFromCloud = downloadedData.blacklist || []; const credentialsFromCloud = downloadedData.credentials || {}; if (typeof rulesFromCloud !== 'object' || Array.isArray(rulesFromCloud)) { topNotice('下载失败:选择的备份文件 rules 数据格式必须是一个对象。'); console.error('Invalid rules format in downloaded data:', rulesFromCloud); return; } if (!Array.isArray(blacklistFromCloud)) { topNotice('下载失败:选择的备份文件 blacklist 数据格式必须是一个数组。'); console.error('Invalid blacklist format in downloaded data:', blacklistFromCloud); 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 { console.warn("无法找到设置面板内容区域来刷新显示。"); } } catch (saveError) { topNotice('保存合并后的配置时出错。'); console.error('保存合并配置失败 (下载后):', saveError); } } else if (getResponse.status === 404) { topNotice('下载失败:无法找到选定的备份文件(可能已被删除?)。'); console.warn(`WebDAV GET failed: File not found at ${targetFilePath}`); } else { topNotice(`下载选定的备份文件失败,服务器返回状态: ${getResponse.status} ${getResponse.statusText}`); console.error('WebDAV GET failed:', getResponse); } } catch (error) { if (error && error.statusText) { topNotice('下载指定备份文件时发生网络错误或超时。'); console.error("WebDAV GET communication error:", error); } else { topNotice('处理下载合并操作时发生内部错误。'); console.error("Internal error during download/merge process:", error); } } } async function deleteSpecificBackup(filenameToDelete) { if (!filenameToDelete) { console.error("deleteSpecificBackup called without a filename."); 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}"(可能已被删除)。`); console.warn(`WebDAV DELETE failed: File not found at ${targetFilePath}`); return false; } else { topNotice(`删除文件 "${filenameToDelete}" 失败,服务器返回: ${deleteResponse.status} ${deleteResponse.statusText}`); console.error('WebDAV DELETE failed:', deleteResponse); return false; } } catch (error) { if (error && error.statusText) { topNotice('删除指定备份文件时发生网络错误或超时。'); console.error("WebDAV DELETE communication error:", error); } else { topNotice('处理删除操作时发生内部错误。'); console.error("Internal error during delete process:", error); } return false; } } function exportAllData(defaultFilename) { const rulesData = GM_getValue(KEY_LOCAL_RULES, {}); const blacklistData = GM_getValue(KEY_BLACKLIST, []); const yunmaToken = GM_getValue(KEY_TOKEN, ''); const webdavUrl = GM_getValue('webdavUrl', ''); const webdavUser = GM_getValue('webdavUser', ''); const webdavPass = GM_getValue('webdavPass', ''); const hasRules = rulesData && Object.keys(rulesData).length > 0; const hasBlacklist = blacklistData && blacklistData.length > 0; const hasCredentials = yunmaToken || webdavUrl || webdavUser || webdavPass; if (!hasRules && !hasBlacklist && !hasCredentials) { alert('没有可导出的配置(规则、黑名单和凭证均为空)。'); return; } if (!confirm( "警告:您将导出包含以下信息的配置文件:\n" + "- 本地规则\n" + "- 黑名单\n" + `- 云码 Token ${yunmaToken ? '(已设置)' : '(未设置)'}\n` + `- WebDAV URL ${webdavUrl ? '(已设置)' : '(未设置)'}\n` + `- WebDAV 用户名 ${webdavUser ? '(已设置)' : '(未设置)'}\n` + `- WebDAV 密码/令牌 ${webdavPass ? '(已设置)' : '(未设置)'}\n\n` + "导出的文件将包含您的敏感凭证(Token, WebDAV密码等)且未加密!\n" + "请务必妥善保管此文件,切勿分享!\n\n" + "确定要继续导出吗?" )) { topNotice("导出操作已取消。"); return; } const combinedData = { rules: rulesData, blacklist: blacklistData, credentials: { yunmaToken: yunmaToken, webdavUrl: webdavUrl, webdavUser: webdavUser, webdavPass: webdavPass } }; 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); topNotice('包含凭证的配置已导出为 ' + defaultFilename); } catch (error) { alert('导出配置时发生错误。'); console.error('导出配置失败:', error); } } function renderRulesTable(containerElement) { containerElement.innerHTML = '

本地规则

'; const allRules = GM_getValue(KEY_LOCAL_RULES, {}); const urls = Object.keys(allRules); if (urls.length === 0) { 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('删除规则时出错'); console.error("删除规则时出错:", err); }); } }; actionCell.appendChild(deleteButton); }); containerElement.appendChild(table); } function renderBlacklistTable(containerElement) { containerElement.innerHTML = '

黑名单

'; const addButton = document.createElement('button'); addButton.textContent = '添加黑名单条目'; addButton.className = CLASS_SETTINGS_BUTTON; addButton.style.marginBottom = '15px'; addButton.onclick = () => { const newItem = prompt("请输入要加入黑名单的字符串(URL中包含该字符串的网页将被阻止运行脚本):"); if (newItem === null) return; if (newItem.trim() === "") { alert("输入的字符串不能为空。"); return; } let blackList = GM_getValue(KEY_BLACKLIST, []); if (blackList.includes(newItem)) { alert("该字符串已在黑名单中。"); } else { blackList.push(newItem); try { GM_setValue(KEY_BLACKLIST, blackList); topNotice('黑名单添加成功,刷新页面或重新导航后生效。'); renderBlacklistTable(containerElement); } catch (e) { alert('保存黑名单失败!'); console.error("保存黑名单失败:", e); } } }; containerElement.appendChild(addButton); const blackList = GM_getValue(KEY_BLACKLIST, []); if (blackList.length === 0) { const noItemsMessage = document.createElement('p'); noItemsMessage.textContent = '暂无黑名单条目。'; noItemsMessage.style.marginTop = '10px'; containerElement.appendChild(noItemsMessage); return; } const table = document.createElement('table'); table.style.width = '100%'; table.style.borderCollapse = 'collapse'; table.style.marginTop = '10px'; table.style.tableLayout = 'fixed'; const thead = table.createTHead(); const headerRow = thead.insertRow(); const headers = ['字符串', '操作']; headers.forEach((text, index) => { const th = document.createElement('th'); th.textContent = text; th.style.border = '1px solid #ddd'; th.style.padding = '10px 12px'; th.style.textAlign = 'left'; th.style.backgroundColor = '#f2f2f2'; th.style.verticalAlign = 'middle'; if (index === 0) th.style.width = '85%'; else if (index === 1) { th.style.textAlign = 'center'; th.style.width = '15%'; } headerRow.appendChild(th); }); const tbody = table.createTBody(); blackList.forEach((item, index) => { const row = tbody.insertRow(); const itemCell = row.insertCell(); itemCell.textContent = item; itemCell.style.border = '1px solid #ddd'; itemCell.style.padding = '10px 12px'; itemCell.style.wordBreak = 'break-all'; itemCell.style.verticalAlign = 'middle'; const actionCell = row.insertCell(); actionCell.style.border = '1px solid #ddd'; actionCell.style.padding = '10px 12px'; actionCell.style.textAlign = 'center'; actionCell.style.verticalAlign = 'middle'; const removeButton = document.createElement('button'); removeButton.textContent = '移除'; removeButton.className = 'settingsButton'; removeButton.style.backgroundColor = '#dc3545'; removeButton.style.padding = '6px 10px'; removeButton.style.margin = '2px'; removeButton.onclick = () => { let currentBlackList = GM_getValue(KEY_BLACKLIST, []); const currentIndex = currentBlackList.indexOf(item); if (currentIndex === -1) { alert("无法找到要移除的条目,请刷新设置面板重试。"); return; } if (confirm(`确定要从黑名单中移除 "${item}" 吗?`)) { currentBlackList.splice(currentIndex, 1); try { GM_setValue(KEY_BLACKLIST, currentBlackList); topNotice('黑名单条目已移除,刷新页面或重新导航后生效。'); renderBlacklistTable(containerElement); } catch (e) { alert('移除黑名单条目失败!'); console.error("移除黑名单失败:", e); } } }; actionCell.appendChild(removeButton); }); containerElement.appendChild(table); } function importAllData(file, contentElementToRerender) { const reader = new FileReader(); reader.onload = (e) => { try { const importedData = JSON.parse(e.target.result); if (typeof importedData !== 'object' || importedData === null || (!importedData.hasOwnProperty('rules') && !importedData.hasOwnProperty('blacklist') && !importedData.hasOwnProperty('credentials'))) { alert('导入失败:文件格式无效或缺少必要的 rules/blacklist/credentials 数据。'); return; } const rulesToImport = importedData.rules || {}; const blacklistToImport = importedData.blacklist || []; const credentialsToImport = importedData.credentials || {}; if (typeof rulesToImport !== 'object' || Array.isArray(rulesToImport)) { alert('导入失败:rules 数据格式必须是一个对象。'); return; } if (!Array.isArray(blacklistToImport)) { alert('导入失败:blacklist 数据格式必须是一个数组。'); return; } if (typeof credentialsToImport !== 'object' || Array.isArray(credentialsToImport)) { alert('导入失败:credentials 数据格式必须是一个对象。'); return; } let confirmMsg = "确定要导入配置吗?\n\n将合并以下数据到现有配置中(重复项将被跳过):\n"; confirmMsg += `- ${Object.keys(rulesToImport).length} 条规则\n`; confirmMsg += `- ${blacklistToImport.length} 条黑名单记录\n`; let credentialsFound = false; if (credentialsToImport.yunmaToken) { confirmMsg += "- 云码 Token\n"; credentialsFound = true; } if (credentialsToImport.webdavUrl) { confirmMsg += "- WebDAV URL\n"; credentialsFound = true; } if (credentialsToImport.webdavUser) { confirmMsg += "- WebDAV 用户名\n"; credentialsFound = true; } if (credentialsToImport.webdavPass) { confirmMsg += "- WebDAV 密码/令牌\n"; credentialsFound = true; } if (credentialsFound) { confirmMsg += "\n警告:导入的凭证将覆盖您当前的设置!"; } if (confirm(confirmMsg)) { try { let currentRules = GM_getValue(KEY_LOCAL_RULES, {}); let currentBlacklist = GM_getValue(KEY_BLACKLIST, []); let rulesAdded = 0; let blacklistAdded = 0; let credentialsImported = false; for (const url in rulesToImport) { if (rulesToImport.hasOwnProperty(url) && !currentRules.hasOwnProperty(url)) { currentRules[url] = rulesToImport[url]; rulesAdded++; } } for (const item of blacklistToImport) { if (!currentBlacklist.includes(item)) { currentBlacklist.push(item); blacklistAdded++; } } GM_setValue(KEY_LOCAL_RULES, currentRules); GM_setValue(KEY_BLACKLIST, currentBlacklist); if (credentialsToImport.hasOwnProperty('yunmaToken')) { GM_setValue(KEY_TOKEN, credentialsToImport.yunmaToken); credentialsImported = true; } if (credentialsToImport.hasOwnProperty('webdavUrl')) { GM_setValue('webdavUrl', credentialsToImport.webdavUrl); credentialsImported = true; } if (credentialsToImport.hasOwnProperty('webdavUser')) { GM_setValue('webdavUser', credentialsToImport.webdavUser); credentialsImported = true; } if (credentialsToImport.hasOwnProperty('webdavPass')) { GM_setValue('webdavPass', credentialsToImport.webdavPass); credentialsImported = true; } let noticeMsg = `配置导入合并完成!新增规则: ${rulesAdded}, 新增黑名单: ${blacklistAdded}.`; if (credentialsImported) { noticeMsg += " 凭证已导入并覆盖现有设置。"; } topNotice(noticeMsg); if (contentElementToRerender) { renderSettingsContent(contentElementToRerender); } } catch (saveError) { alert('保存导入后的配置时出错。'); console.error('保存导入配置失败:', saveError); } } else { topNotice('导入已取消。'); } } catch (parseError) { alert('导入失败:无法解析 JSON 文件。'); console.error('解析导入文件失败:', parseError); } }; reader.onerror = (e) => { alert('导入失败:读取文件时出错。'); console.error('读取导入文件失败:', e); }; reader.readAsText(file); } function debounce(func, wait, immediate) { let timeout; return function () { const context = this, args = arguments; const later = function () { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } //监听页面变化 (Single Main Observer) const targetNode = document.body; const config = { attributes: true, childList: true, subtree: true }; const debouncedPageChange = debounce(function () { if (inBlack) { return; } if (noRuleFound) { return; } try { pageChange(); } catch (err) { console.error("pageChange error:", err); } }, 500); const observer = new MutationObserver(debouncedPageChange); observer.observe(targetNode, config); //监听url变化 setTimeout(function () { if (inBlack) return; let tempUrl = window.location.href; setInterval(function () { if (tempUrl != window.location.href) { tempUrl = window.location.href; exist = false; noRuleFound = false; element = null; input = null; localRules = {}; focusListenerAttached = false; start(); } }, 500) }, 500) if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", runInitialCheck); } else { runInitialCheck(); } function runInitialCheck() { if (initialCheckRun) return; initialCheckRun = true; let url = window.location.href; let blackList = GM_getValue(KEY_BLACKLIST, []); inBlack = blackList.some(function (blackItem) { return url.includes(blackItem); }); if (inBlack) { return; } else { start(); } } function modifyRuleTypeFromPanel(ruleUrl, settingsContentElement) { let allRules = GM_getValue(KEY_LOCAL_RULES, {}); let ruleToModify = allRules[ruleUrl]; if (!ruleToModify) { alert(`错误:找不到 URL 为 ${ruleUrl} 的规则。`); return; } let availableTypesForPrompt = captchaTypes; let promptTitle = "修改识别类型:"; let defaultPromptValue = ruleToModify.ymType || '10110'; if (ruleToModify.captchaType === 'math') { alert(`规则 ${ruleUrl} 为算术验证码,固定使用类型 ${ruleToModify.ymType || '50100'}。 如需更改为字符类型,请删除规则后重新添加。`); return; } let currentYmType = ruleToModify.ymType || '未设置 (将使用 10110)'; let promptText = `${promptTitle}\n规则 URL: ${ruleUrl}\n当前类型: ${currentYmType}\n\n请输入新的类型代码 (留空使用默认 10110): `; let defaultType = '10110'; let validCodes = availableTypesForPrompt.map(t => t.code); availableTypesForPrompt.forEach(type => { promptText += `${type.code}: ${type.description}\n`; }); let selectedYmType = prompt(promptText, defaultPromptValue); if (selectedYmType === null) { topNotice("修改已取消"); return; } let finalYmType = defaultType; if (selectedYmType === "") { } else if (validCodes.includes(selectedYmType)) { finalYmType = selectedYmType; } else { alert(`无效的类型代码 '${selectedYmType}'。未作修改。`); return; } ruleToModify.ymType = finalYmType; ruleToModify.captchaType = 'general'; try { allRules[ruleUrl] = ruleToModify; GM_setValue(KEY_LOCAL_RULES, allRules); topNotice(`规则 ${ruleUrl} 的识别类型已更新为: ${finalYmType}。`); renderSettingsContent(settingsContentElement); } catch (error) { console.error("保存更新后的规则失败 (from panel):", error); topNotice("错误:保存更新后的规则失败!"); } } //识别验证码(自定义规则) -> 改为调用云码 API function p1(code, ruleYmType = null) { let token = GM_getValue(KEY_TOKEN); if (!token) { topNotice("请先通过菜单设置云码 Token"); return Promise.resolve(null); } if (!ruleYmType) { topNotice("错误:当前规则未指定识别类型 (ymType),无法识别。请通过菜单\"修改当前规则识别类型\"进行设置。"); console.warn("[识别中止] 当前规则缺少 ymType:", localRules); return Promise.resolve(null); } let ymTypeToUse = ruleYmType; const datas = { "image": String(code), "type": ymTypeToUse, "token": token, }; return new Promise((resolve, reject) => { let api_t0 = performance.now(); GM_xhr_WebDAV({ method: "POST", url: "http://api.jfbym.com/api/YmServer/customApi", data: JSON.stringify(datas), headers: { "Content-Type": "application/json", }, responseType: "json", timeout: 20000, }).then(response => { let api_t1 = performance.now(); try { if (response.status == 200 && response.response) { const respData = response.response; if (respData.data && respData.data.time) { } if (respData.code === 10000 && respData.data && respData.data.code === 0) { const result = respData.data.data; return resolve(result); } else { let errorCode = respData.code; let errorMsg = yunmaErrorCodes[errorCode]; if (!errorMsg && respData.data && respData.data.code !== 0) { errorCode = respData.data.code; errorMsg = yunmaErrorCodes[errorCode]; } let displayMsg = `云码识别失败: ${errorMsg || respData.msg || '未知错误'}`; displayMsg += ` (Code: ${errorCode || 'N/A'})`; if (respData.data && respData.data.msg && respData.data.msg !== respData.msg) { displayMsg += ` (详情: ${respData.data.msg})`; } topNotice(displayMsg); console.error("云码识别失败:", respData); } } else { topNotice("云码 API 请求失败,状态码: " + response.status); console.error("云码 API 请求失败:", response); } } catch (e) { topNotice("处理云码响应时出错"); console.error("处理云码响应时出错:", e, response.responseText); } return resolve(null); }).catch(error => { let api_t1 = performance.now(); topNotice("云码 API 请求网络错误"); console.error("云码 API 请求网络错误:", error); return resolve(null); }); }); } //判断是否跨域 function isCORS() { try { if (element && element.tagName === 'IMG' && element.src) { if (element.src.startsWith('http:') || element.src.startsWith('https:')) { try { const imgOrigin = new URL(element.src).origin; if (imgOrigin !== window.location.origin) { return true; } } catch (urlError) { console.warn("isCORS check failed to parse element.src:", element.src, urlError); return false; } } } // return false; } catch (err) { console.warn("isCORS check failed unexpectedly:", err); return false; } } //将url转换为base64(解决跨域问题) function p2() { let p2_t0 = performance.now(); return new Promise((resolve, reject) => { GM_xhr_WebDAV({ method: "GET", url: element.src, headers: { 'Content-Type': 'application/json; charset=utf-8', 'path': window.location.href }, responseType: "blob", timeout: 15000, }).then(response => { let blob = response.response; let reader = new FileReader(); reader.onloadend = (e) => { let data = e.target.result; element.src = data; let p2_t1 = performance.now(); return resolve(data); } reader.readAsDataURL(blob); }).catch(error => { let p2_t1 = performance.now(); reject(new Error("Cross-origin fetch failed")); }); }); } //此段逻辑借鉴Crab大佬的代码,十分感谢 function fire(element, eventName) { const event = document.createEvent("HTMLEvents"); event.initEvent(eventName, true, true); element.dispatchEvent(event); } function FireForReact(element, eventName) { try { const env = new Event(eventName); element.dispatchEvent(env); const 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) { } } })();