网页通用验证码识别

解放眼睛和双手,自动识别并填入数字,字母(支持大小写),文字验证码。

  1. // ==UserScript==
  2. // @name 网页通用验证码识别
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.0
  5. // @description 解放眼睛和双手,自动识别并填入数字,字母(支持大小写),文字验证码。
  6. // @author 哈士奇
  7.  
  8. // @include http://*
  9. // @include https://*
  10. // @license MIT
  11.  
  12. // @grant unsafeWindow
  13. // @grant GM_addStyle
  14. // @grant GM_listValues
  15. // @grant GM_addValueChangeListener
  16. // @grant GM_removeValueChangeListener
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_deleteValue
  20. // @grant GM_log
  21. // @grant GM_getResourceText
  22. // @grant GM_getResourceURL
  23. // @grant GM_registerMenuCommand
  24. // @grant GM_unregisterMenuCommand
  25. // @grant GM_xmlhttpRequest
  26. // @grant GM_download
  27. // @grant GM_getTab
  28. // @grant GM_saveTab
  29. // @grant GM_getTabs
  30. // @grant GM_notification
  31. // @grant GM_setClipboard
  32. // @grant GM_info
  33. // @grant GM_xmlhttpRequest
  34. // @connect *
  35. // @require https://unpkg.com/vue@2.6.12/dist/vue.js
  36. // @require https://unpkg.com/element-ui/lib/index.js
  37. // @resource elementUIcss https://unpkg.com/element-ui/lib/theme-chalk/index.css
  38.  
  39. // @run-at document-end
  40. // ==/UserScript==
  41.  
  42. (function () {
  43. // GM_setValue('tipsConfig',"")
  44. var elementUIcss = GM_getResourceText("elementUIcss");
  45. var routePrefix = 'http://1.95.154.26:7000'
  46. GM_addStyle(elementUIcss);
  47.  
  48. function getStyle(el) {
  49. // 获取元素样式
  50. if (window.getComputedStyle) {
  51. return window.getComputedStyle(el, null);
  52. } else {
  53. return el.currentStyle;
  54. }
  55. }
  56.  
  57. function init() {
  58. //简化各种api和初始化全局变量
  59. CUR_URL = window.location.href;
  60. DOMAIN = CUR_URL.split("//")[1].split("/")[0];
  61. SLIDE_STORE_KEY = "husky_" + "slidePath" + location.host;
  62. NORMAL_STORE_KEY = "husky_" + "normalPath" + location.host;
  63. selector = document.querySelector.bind(document);
  64. selectorAll = document.querySelectorAll.bind(document);
  65. getItem = localStorage.getItem.bind(localStorage);
  66. setItem = localStorage.setItem.bind(localStorage);
  67. }
  68.  
  69. function getNumber(str) {
  70. return Number(str.split(".")[0].replace(/[^0-9]/gi, ""));
  71. }
  72.  
  73. function isNumber(value) {
  74. if (!value && value !== 0) {
  75. return false;
  76. }
  77. value = Number(value);
  78. return typeof value === "number" && !isNaN(value);
  79. }
  80.  
  81. function getEleTransform(el) {
  82. const style = window.getComputedStyle(el, null);
  83. var transform =
  84. style.getPropertyValue("-webkit-transform") ||
  85. style.getPropertyValue("-moz-transform") ||
  86. style.getPropertyValue("-ms-transform") ||
  87. style.getPropertyValue("-o-transform") ||
  88. style.getPropertyValue("transform") ||
  89. "null";
  90. return transform && transform.split(",")[4];
  91. }
  92.  
  93. class Captcha {
  94. // 识别网页中的验证码
  95. constructor() {
  96. this.imgCache = [];
  97. this.inputTags = [];
  98. this.recommendPath = {};
  99. this.checkTimer = null;
  100. this.listenLoadSuccess = false;
  101.  
  102. window.addEventListener("load", async () => {
  103. this.listenLoadSuccess = true;
  104. this.init();
  105. });
  106. setTimeout(() => {
  107. if (!this.listenLoadSuccess) {
  108. this.listenLoadSuccess = true;
  109. this.init();
  110. }
  111. }, 5000);
  112. }
  113.  
  114. doCheckTask() {
  115. this.findCaptcha();
  116. // this.checkSlideCaptcha();
  117. }
  118. init() {
  119. if (blackListCheck()) {
  120. return;
  121. }
  122. this.manualLocateCaptcha();
  123. this.doCheckTask();
  124.  
  125. const MutationObserver =
  126. window.MutationObserver ||
  127. window.WebKitMutationObserver ||
  128. window.MozMutationObserver;
  129. const body = document.body;
  130.  
  131. const Observer = new MutationObserver((mutations, instance) => {
  132. if (blackListCheck()) {
  133. return;
  134. }
  135. for (let i = 0; i < mutations.length; i++) {
  136. const el = mutations[i].target;
  137. const tagName = mutations[i].target.tagName.toLowerCase();
  138. let checkList = [];
  139. checkList.push(el.getAttribute("id"));
  140. checkList.push(el.className);
  141. checkList.push(el.getAttribute("alt"));
  142. checkList.push(el.getAttribute("src"));
  143. checkList.push(el.getAttribute("name"));
  144. checkList = checkList.filter((item) => item);
  145.  
  146. for (let x = 0; x < checkList.length; x++) {
  147. if (
  148. /.*(code|captcha|验证码|login|点击|verify|yzm|yanzhengma|滑块|拖动|拼图|yidun|slide).*/im.test(
  149. checkList[x].toString().toLowerCase()
  150. ) ||
  151. tagName === "img" ||
  152. tagName === "iframe"
  153. ) {
  154. if (!this.checkTimer) {
  155. this.checkTimer = setTimeout(() => {
  156. this.doCheckTask();
  157. }, 0);
  158. } else {
  159. window.clearTimeout(this.checkTimer);
  160. this.checkTimer = setTimeout(() => {
  161. this.doCheckTask();
  162. }, 2000);
  163. }
  164. return;
  165. }
  166. }
  167. }
  168. });
  169. Observer.observe(body, {
  170. childList: true,
  171. subtree: true,
  172. attributes: true,
  173. });
  174. }
  175. dataURLtoFile(dataURL, filename = "captcha.jpg") {
  176. // base64转图片文件
  177. var arr = dataURL.split(","),
  178. mime =
  179. (arr[0].match(/:(.*?);/) && arr[0].match(/:(.*?);/)[1]) ||
  180. "image/png",
  181. bstr = atob(arr[1]),
  182. n = bstr.length,
  183. u8arr = new Uint8Array(n);
  184. while (n--) {
  185. u8arr[n] = bstr.charCodeAt(n);
  186. }
  187. return new File([u8arr], filename, { type: mime });
  188. }
  189. async getRecommendPath() {
  190. let requestUrl =
  191. routePrefix + "/cssPath?href=" +
  192. location.href.split("?")[0];
  193. try {
  194. GM_xmlhttpRequest({
  195. method: "get",
  196. url: requestUrl,
  197. onload: async (res) => {
  198. if (res.status === 200 && res.response) {
  199. let data = (res.response && JSON.parse(res.response)) || {};
  200. const { path, recommendTimes = 0 } = data;
  201. if (path && recommendTimes) {
  202. let inputSelector = path.split("$$")[0];
  203. let imgSelector = path.split("$$")[1];
  204. if (
  205. selector(inputSelector) &&
  206. selector(imgSelector) &&
  207. selector(imgSelector).getAttribute("src") &&
  208. selector(inputSelector).getAttribute("type") === "text"
  209. ) {
  210. let dataURL = await this.handleImg(selector(imgSelector));
  211. try {
  212. if (!this.hasRequest(dataURL, { record: true })) {
  213. let code = await this.request(
  214. this.dataURLtoFile(dataURL),
  215. this.cssPath(selector(inputSelector)) +
  216. "$$" +
  217. this.cssPath(selector(imgSelector)),
  218. selector(imgSelector).getAttribute("src")
  219. );
  220. if (code) {
  221. selector(inputSelector).value = code;
  222. if (typeof Vue !== "undefined") {
  223. new Vue().$message.success("获取验证码成功");
  224. }
  225. console.log("正在使用共享验证码功能获取验证码");
  226. } else {
  227. console.error("验证码为空,请检查图片是否正确");
  228. }
  229. }
  230. } catch (error) {
  231. console.log(error);
  232. // if (typeof Vue !== "undefined") {
  233. // new Vue().$message.error("获取验证码失败");
  234. // }
  235. }
  236. }
  237. }
  238. }
  239. },
  240. onerror: function (err) {
  241. console.log("推荐路径请求失败:" + err);
  242. },
  243. });
  244. } catch (error) {
  245. console.log(error);
  246. }
  247. }
  248. getCaptchaFeature(el) {
  249. // 获取验证码特征
  250. let checkList = [];
  251. checkList.push(el.getAttribute("id"));
  252. checkList.push(el.className);
  253. checkList.push(el.getAttribute("alt"));
  254. checkList.push(el.getAttribute("src"));
  255. checkList.push(el.getAttribute("name"));
  256.  
  257. return checkList;
  258. }
  259. cssPath = (el) => {
  260. // 获取元素css path
  261. if (!(el instanceof Element)) return;
  262. var path = [];
  263. while (el.nodeType === Node.ELEMENT_NODE) {
  264. var selector = el.nodeName.toLowerCase();
  265. if (el.id) {
  266. selector += "#" + el.id;
  267. path.unshift(selector);
  268. break;
  269. } else {
  270. var sib = el,
  271. nth = 1;
  272. while ((sib = sib.previousElementSibling)) {
  273. if (sib.nodeName.toLowerCase() == selector) nth++;
  274. }
  275. if (nth != 1) selector += ":nth-of-type(" + nth + ")";
  276. }
  277. path.unshift(selector);
  278. el = el.parentNode;
  279. }
  280. return path.join(" > ");
  281. };
  282.  
  283. manualLocateCaptcha() {
  284. let imgs = [];
  285. let inputTags = [];
  286. let cssPathStore = {};
  287. let finish = false;
  288. this.vue = new Vue();
  289. this.isIframe = top !== self;
  290. var onTagClick = (e) => {
  291. let el = e.target;
  292. let tagName = el.tagName;
  293. if (tagName.toLowerCase() === "input") {
  294. let type = el.getAttribute("type");
  295. if (type && type !== "text") {
  296. this.vue.$message.error(
  297. "提醒:当前点击输入框type=" + type + ",请选择文本输入框"
  298. );
  299. } else {
  300. cssPathStore.input = this.cssPath(el);
  301. this.vue.$message.success("您已成功选择输入框");
  302. }
  303. } else {
  304. cssPathStore.img = this.cssPath(el);
  305. this.vue.$message.success("您已成功选择验证码图片");
  306. }
  307. if (cssPathStore.input && cssPathStore.img) {
  308. GM_setValue(NORMAL_STORE_KEY, JSON.stringify(cssPathStore));
  309. imgs.forEach((img) => {
  310. img && img.removeEventListener("click", onTagClick);
  311. }, false);
  312. inputTags.forEach((input) => {
  313. input.removeEventListener("click", onTagClick);
  314. }, false);
  315. setTimeout(() => {
  316. this.vue.$message.success("选择完毕,赶快试试吧");
  317. captchaInstance.doCheckTask();
  318. }, 3000);
  319. finish = true;
  320. }
  321. };
  322. var onMenuClick = (e) => {
  323. if (this.isIframe) {
  324. alert("当前脚本处于iframe中,暂不支持该操作,快让作者优化吧");
  325. return;
  326. }
  327. finish = false;
  328. cssPathStore = {};
  329. GM_deleteValue(NORMAL_STORE_KEY);
  330. this.vue.$alert("接下来请点击验证码图片和输入框", "操作提示", {
  331. confirmButtonText: "确定",
  332. callback: () => {
  333. setTimeout(() => {
  334. imgs.forEach((img) => {
  335. img && img.removeEventListener("click", onTagClick);
  336. }, false);
  337. inputTags.forEach((input) => {
  338. input.removeEventListener("click", onTagClick);
  339. }, false);
  340. if (!finish) {
  341. this.vue.$notify.success({
  342. title: "提示",
  343. message: "已退出手动选择验证码模式。",
  344. offset: 100,
  345. });
  346. }
  347. }, 20000);
  348. },
  349. });
  350.  
  351. // alert("请点击验证码和输入框各一次。");
  352. imgs = [...selectorAll("img")];
  353. inputTags = [...selectorAll("input")];
  354. imgs.forEach((img) => {
  355. img.addEventListener("click", onTagClick);
  356. }, false);
  357. inputTags.forEach((input) => {
  358. input.addEventListener("click", onTagClick);
  359. }, false);
  360. };
  361. GM_registerMenuCommand("手动选择验证码和输入框", onMenuClick);
  362. }
  363. handleImg(img) {
  364. return new Promise((resolve, reject) => {
  365. try {
  366. // 图片没设置跨域,可采用图片转canvas转base64的方式
  367. let dataURL = null;
  368.  
  369. const action = () => {
  370. let canvas = document.createElement("canvas");
  371. canvas.width = img.naturalWidth;
  372. canvas.height = img.naturalHeight;
  373. let ctx = canvas.getContext("2d");
  374. ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight);
  375. dataURL = canvas.toDataURL("image/png");
  376. resolve(dataURL);
  377. };
  378. if (!img.src.includes(";base64,")) {
  379. img.onload = function () {
  380. action();
  381. };
  382. if (img.complete) {
  383. action();
  384. } else {
  385. img.onload = function () {
  386. action();
  387. };
  388. }
  389. } else {
  390. dataURL = img.src;
  391. resolve(dataURL);
  392. }
  393. } catch (error) {
  394. console.error("error:" + error);
  395. // 这块处理比较复杂,待优化
  396. // 图片设置跨域,重新请求图片内容后转base64,相当于替用户点击了“换一张图片”
  397. // if (this.times >= 1) {
  398. // return;
  399. // }
  400. // if (typeof Vue !== "undefined") {
  401. // new Vue().$notify.success({
  402. // title: "温馨提示",
  403. // message: "当前验证码结果可能和图片显示不一致,请放心提交。",
  404. // offset: 100,
  405. // });
  406. // }
  407.  
  408. // this.times++;
  409. // GM_xmlhttpRequest({
  410. // method: "get",
  411. // url: img.src,
  412. // responseType: "blob",
  413. // onload: (res) => {
  414. // if (res.status === 200) {
  415. // let blob = res.response;
  416. // let fileReader = new FileReader();
  417. // fileReader.onloadend = (e) => {
  418. // let base64 = e.target.result;
  419. // resolve(base64);
  420. // };
  421. // fileReader.readAsDataURL(blob);
  422. // } else {
  423. // console.log("图片转换blob失败");
  424. // console.log(res);
  425. // reject();
  426. // }
  427. // },
  428. // onerror: function(err) {
  429. // console.log("图片请求失败:" + err);
  430. // reject();
  431. // },
  432. // });
  433. }
  434. });
  435. }
  436. hasRequest(dataURL, config = {}) {
  437. let startIndex = config.type === "url" ? 0 : dataURL.length - 100;
  438. let imgClips = dataURL.slice(startIndex, dataURL.length);
  439. if (this.imgCache.includes(imgClips)) {
  440. return true;
  441. }
  442. if (config.record) {
  443. this.imgCache.push(imgClips);
  444. }
  445. return false;
  446. }
  447. request(file, path, src) {
  448. try {
  449. if (!file) {
  450. console.error("缺少file参数");
  451. return Promise.reject();
  452. }
  453.  
  454. return new Promise((resolve, reject) => {
  455. let host = location.href;
  456. let href = location.href.split("?")[0].split("#")[0];
  457. if (self === top) {
  458. host = location.host;
  459. }
  460. let formData = new FormData();
  461. let detail = {
  462. // path,
  463. // src,
  464. // host,
  465. href,
  466. };
  467. formData.append("img", file);
  468. formData.append("detail", JSON.stringify(detail));
  469. let requestUrl = routePrefix + '/captcha';
  470. GM_xmlhttpRequest({
  471. method: "post",
  472. url: requestUrl,
  473. data: formData,
  474. onload: function (response) {
  475. let res = JSON.parse(response.response) || {};
  476. console.log({ res })
  477. if (response.status === 429) {
  478. let msg = res.msg || '获取验证码失败';
  479. let date = new Date().getDate();
  480. let tipsConfig = {
  481. date,
  482. times: 1,
  483. };
  484. let cache =
  485. GM_getValue("tipsConfig") &&
  486. JSON.parse(GM_getValue("tipsConfig"));
  487. if (cache && cache.times > 5) {
  488. } else {
  489. if (!cache) {
  490. GM_setValue("tipsConfig", JSON.stringify(tipsConfig));
  491. } else {
  492. cache.times = cache.times + 1;
  493. GM_setValue("tipsConfig", JSON.stringify(cache));
  494. }
  495. if (typeof Vue !== "undefined") {
  496. new Vue().$message.error(msg);
  497. }
  498. }
  499. console.error("获取验证码失败:", res);
  500. reject();
  501. } else {
  502. if (res.data.code) {
  503. resolve(res.data.code);
  504. } else {
  505. console.error("获取验证码失败,验证码为空:", response);
  506. reject();
  507. }
  508. }
  509. },
  510. onerror: function (err) {
  511. console.error(err);
  512. reject();
  513. },
  514. });
  515. });
  516. } catch (error) {
  517. console.log(error);
  518. }
  519. }
  520. async findCaptcha() {
  521. // 先读取用户手动设置的验证码配置
  522. let cache = GM_getValue(NORMAL_STORE_KEY);
  523. let captchaPath = cache && JSON.parse(cache);
  524. if (
  525. captchaPath &&
  526. captchaPath.input &&
  527. captchaPath.img &&
  528. selector(captchaPath.input) &&
  529. selector(captchaPath.img)
  530. ) {
  531. let dataURL = await this.handleImg(selector(captchaPath.img));
  532. try {
  533. if (!this.hasRequest(dataURL, { record: true })) {
  534. let code = await this.request(
  535. this.dataURLtoFile(dataURL),
  536. this.cssPath(selector(captchaPath.input)) +
  537. "$$" +
  538. this.cssPath(selector(captchaPath.img)),
  539. selector(captchaPath.img).getAttribute("src")
  540. );
  541. if (code) {
  542. selector(captchaPath.input).value = code.trim();
  543. console.log("正在使用用户自定义验证码位置数据获取验证码");
  544. return;
  545. } else {
  546. console.error("验证码为空,请检查图片是否正确");
  547. }
  548. }
  549. } catch (error) {
  550. console.log(error);
  551. }
  552. return;
  553. }
  554. // 自动寻找验证码和输入框
  555. let captchaMap = [];
  556. let imgs = [...selectorAll("img")];
  557. imgs.forEach((img) => {
  558. let checkList = [
  559. ...this.getCaptchaFeature(img),
  560. ...this.getCaptchaFeature(img.parentNode),
  561. ];
  562. checkList = checkList.filter((item) => item);
  563. let isInvalid =
  564. ["#", "about:blank"].includes(img.getAttribute("src")) ||
  565. !img.getAttribute("src");
  566.  
  567. for (let i = 0; i < checkList.length; i++) {
  568. if (
  569. /.*(code|captcha|验证码|login|点击|verify|yzm|yanzhengma).*/im.test(
  570. checkList[i].toLowerCase()
  571. ) &&
  572. img.width > 30 &&
  573. img.width < 150 &&
  574. img.height < 80 &&
  575. !isInvalid
  576. ) {
  577. captchaMap.push({ img: img, input: null });
  578. break;
  579. }
  580. }
  581. });
  582. captchaMap.forEach((item) => {
  583. let imgEle = item.img;
  584. let parentNode = imgEle.parentNode;
  585. for (let i = 0; i < 4; i++) {
  586. // 以当前可能是验证码的图片为基点,向上遍历四层查找可能的Input输入框
  587. if (!parentNode) {
  588. return;
  589. }
  590. let inputTags = [...parentNode.querySelectorAll("input")];
  591. if (inputTags.length) {
  592. let input = inputTags.pop();
  593. let type = input.getAttribute("type");
  594. while (type !== "text" && inputTags.length) {
  595. if (type === "password") {
  596. break;
  597. }
  598. input = inputTags.pop();
  599. type = input.getAttribute("type");
  600. }
  601. let inputWidth = getStyle(input).width.replace(/[^0-9]/gi, "");
  602. // let inputHeight = getStyle(input).height.replace(/[^0-9]/gi, "");
  603. if (!type || (type === "text" && inputWidth > 50)) {
  604. // 兼容各种奇葩情况
  605. item.input = input;
  606. break;
  607. }
  608. if (type === "password") {
  609. // 验证码一般在密码框后面,遍历到密码框了就大概率说明没有验证码
  610. break;
  611. }
  612. }
  613. parentNode = parentNode.parentNode;
  614. }
  615. });
  616. // console.log(captchaMap);
  617. if (!captchaMap.length) {
  618. const { path, recommendTimes } = this.recommendPath;
  619. if (path) {
  620. let inputSelector = path.split("$$")[0];
  621. let imgSelector = path.split("$$")[1];
  622. if (selector(inputSelector) && selector(imgSelector)) {
  623. let dataURL = await this.handleImg(selector(imgSelector));
  624. try {
  625. if (!this.hasRequest(dataURL, { record: true })) {
  626. selector(inputSelector).value = await this.request(
  627. this.dataURLtoFile(dataURL),
  628. path,
  629. item.img.getAttribute("src")
  630. );
  631. if (typeof Vue !== "undefined") {
  632. new Vue().$message.success("获取验证码成功");
  633. }
  634. }
  635. } catch (error) {
  636. console.log(error);
  637. // if (typeof Vue !== "undefined") {
  638. // new Vue().$message.error("获取验证码失败");
  639. // }
  640. }
  641. }
  642. }
  643. }
  644. captchaMap = captchaMap.filter((item) => item.input);
  645. captchaMap.forEach(async (item, index) => {
  646. let dataURL = await this.handleImg(item.img);
  647. try {
  648. if (!this.hasRequest(dataURL, { record: true })) {
  649. let code = await this.request(
  650. this.dataURLtoFile(dataURL),
  651. this.cssPath(item.input) + "$$" + this.cssPath(item.img),
  652. item.img.getAttribute("src")
  653. );
  654. if (code) {
  655. item.input.value = code;
  656. if (typeof Vue !== "undefined") {
  657. new Vue().$message.success("获取验证码成功");
  658. }
  659. console.log("正在使用自动寻找验证码功能获取验证码");
  660. } else {
  661. if (index === captchaMap.length - 1) {
  662. // this.getRecommendPath();
  663. }
  664. console.error("验证码为空,请检查图片是否正确");
  665. }
  666. }
  667. } catch (error) {
  668. if (index === captchaMap.length - 1) {
  669. // this.getRecommendPath();
  670. }
  671. console.log(error);
  672. // if (typeof Vue !== "undefined") {
  673. // new Vue().$message.error("获取验证码失败");
  674. // }
  675. }
  676. });
  677. }
  678. getImgViaBlob(url) {
  679. return new Promise((resolve, reject) => {
  680. try {
  681. GM_xmlhttpRequest({
  682. method: "get",
  683. url,
  684. responseType: "blob",
  685. onload: (res) => {
  686. if (res.status === 200) {
  687. let blob = res.response;
  688. let fileReader = new FileReader();
  689. fileReader.onloadend = (e) => {
  690. let base64 = e.target.result;
  691. if (base64.length > 20) {
  692. resolve(base64);
  693. } else {
  694. alert(
  695. "验证码助手:当前网站验证码图片禁止跨域访问,待作者优化。"
  696. );
  697. handleClearMenuClick();
  698. reject("base64图片长度不够");
  699. throw "getImgViaBlob: base64图片长度不够";
  700. }
  701. };
  702. fileReader.readAsDataURL(blob);
  703. } else {
  704. console.log("图片转换blob失败");
  705. console.log(res);
  706. reject();
  707. }
  708. },
  709. onerror: function (err) {
  710. console.log("图片请求失败:" + err);
  711. reject();
  712. },
  713. });
  714. } catch (error) {
  715. console.log(error);
  716. reject();
  717. }
  718. });
  719. }
  720. elDisplay(el) {
  721. if (!el) {
  722. return false;
  723. }
  724.  
  725. while (el) {
  726. if (!(el instanceof Element)) {
  727. return true;
  728. }
  729. if (getStyle(el).display === "none") {
  730. return false;
  731. }
  732. el = el.parentNode;
  733. }
  734. return true;
  735. }
  736. checkSlideCaptcha() {
  737. const check = async () => {
  738. const slideCache =
  739. (GM_getValue(SLIDE_STORE_KEY) &&
  740. JSON.parse(GM_getValue(SLIDE_STORE_KEY))) ||
  741. {};
  742. const { bgImg, targetImg, moveItem } = slideCache;
  743. if (
  744. bgImg &&
  745. targetImg &&
  746. moveItem &&
  747. selector(targetImg) &&
  748. selector(bgImg) &&
  749. selector(moveItem) &&
  750. this.elDisplay(selector(targetImg)) &&
  751. this.elDisplay(selector(bgImg)) &&
  752. this.elDisplay(selector(moveItem))
  753. ) {
  754. const target_url =
  755. selector(targetImg).getAttribute("src") ||
  756. getStyle(selector(targetImg))["background-image"].split('"')[1];
  757. const bg_url =
  758. selector(bgImg).getAttribute("src") ||
  759. getStyle(selector(bgImg))["background-image"].split('"')[1];
  760. if (!this.hasRequest(target_url, { record: true, type: "url" })) {
  761. const target_base64 = await this.getImgViaBlob(target_url);
  762. const bg_base64 = await this.getImgViaBlob(bg_url);
  763. return new Promise(async (resolve, reject) => {
  764. let host = location.href;
  765. let href = location.href.split("?")[0].split("#")[0];
  766. if (self === top) {
  767. host = location.host;
  768. }
  769. let detail = {
  770. path: slideCache,
  771. host,
  772. href,
  773. };
  774. let formData = new FormData();
  775. let requestUrl = routePrefix + "/slideCaptcha";
  776. let targetWidth = getNumber(getStyle(selector(targetImg)).width);
  777. let bgWidth = getNumber(getStyle(selector(bgImg)).width);
  778. formData.append("target_img", this.dataURLtoFile(target_base64));
  779. formData.append("bg_img", this.dataURLtoFile(bg_base64));
  780. formData.append("targetWidth", targetWidth);
  781. formData.append("bgWidth", bgWidth);
  782. formData.append("detail", JSON.stringify(detail));
  783. GM_xmlhttpRequest({
  784. method: "post",
  785. url: requestUrl,
  786. data: formData,
  787. onload: (response) => {
  788. const data = JSON.parse(response.response);
  789. this.moveSideCaptcha(
  790. selector(targetImg),
  791. selector(moveItem),
  792. data.result.target[0]
  793. );
  794. // resolve()
  795. },
  796. onerror: function (err) {
  797. console.error(err);
  798. reject();
  799. },
  800. });
  801. });
  802. }
  803. }
  804. };
  805. check();
  806. // const interval = 3000;
  807. // simulateInterval(check, interval);
  808. }
  809. moveSideCaptcha(targetImg, moveItem, distance) {
  810. if (distance === 0) {
  811. console.log("distance", distance);
  812. return;
  813. }
  814. var btn = moveItem;
  815. let target = targetImg;
  816.  
  817. let varible = null;
  818. let targetLeft = Number(getStyle(target).left.replace("px", "")) || 0;
  819. let targetParentLeft =
  820. Number(getStyle(target.parentNode).left.replace("px", "")) || 0;
  821. let targetTransform = Number(getEleTransform(target)) || 0;
  822. let targetParentTransform =
  823. Number(getEleTransform(target.parentNode)) || 0;
  824.  
  825. var mousedown = document.createEvent("MouseEvents");
  826. var rect = btn.getBoundingClientRect();
  827. var x = rect.x;
  828. var y = rect.y;
  829. mousedown.initMouseEvent(
  830. "mousedown",
  831. true,
  832. true,
  833. document.defaultView,
  834. 0,
  835. x,
  836. y,
  837. x,
  838. y,
  839. false,
  840. false,
  841. false,
  842. false,
  843. 0,
  844. null
  845. );
  846. btn.dispatchEvent(mousedown);
  847.  
  848. var dx = 0;
  849. var dy = 0;
  850. var interval = setInterval(function () {
  851. var mousemove = document.createEvent("MouseEvents");
  852. var _x = x + dx;
  853. var _y = y + dy;
  854. mousemove.initMouseEvent(
  855. "mousemove",
  856. true,
  857. true,
  858. document.defaultView,
  859. 0,
  860. _x,
  861. _y,
  862. _x,
  863. _y,
  864. false,
  865. false,
  866. false,
  867. false,
  868. 0,
  869. null
  870. );
  871. btn.dispatchEvent(mousemove);
  872. btn.dispatchEvent(mousemove);
  873.  
  874. let newTargetLeft =
  875. Number(getStyle(target).left.replace("px", "")) || 0;
  876. let newTargetParentLeft =
  877. Number(getStyle(target.parentNode).left.replace("px", "")) || 0;
  878. let newTargetTransform = Number(getEleTransform(target)) || 0;
  879. let newTargetParentTransform =
  880. Number(getEleTransform(target.parentNode)) || 0;
  881.  
  882. if (newTargetLeft !== targetLeft) {
  883. varible = newTargetLeft;
  884. } else if (newTargetParentLeft !== targetParentLeft) {
  885. varible = newTargetParentLeft;
  886. } else if (newTargetTransform !== targetTransform) {
  887. varible = newTargetTransform;
  888. } else if (newTargetParentTransform != targetParentTransform) {
  889. varible = newTargetParentTransform;
  890. }
  891. if (varible >= distance) {
  892. clearInterval(interval);
  893. var mouseup = document.createEvent("MouseEvents");
  894. mouseup.initMouseEvent(
  895. "mouseup",
  896. true,
  897. true,
  898. document.defaultView,
  899. 0,
  900. _x,
  901. _y,
  902. _x,
  903. _y,
  904. false,
  905. false,
  906. false,
  907. false,
  908. 0,
  909. null
  910. );
  911. setTimeout(() => {
  912. btn.dispatchEvent(mouseup);
  913. }, Math.ceil(Math.random() * 2000));
  914. } else {
  915. if (dx >= distance - 20) {
  916. dx += Math.ceil(Math.random() * 2);
  917. } else {
  918. dx += Math.ceil(Math.random() * 10);
  919. }
  920. let sign = Math.random() > 0.5 ? -1 : 1;
  921. dy += Math.ceil(Math.random() * 3 * sign);
  922. }
  923. }, 10);
  924. setTimeout(() => {
  925. clearInterval(interval);
  926. }, 10000);
  927. }
  928. }
  929.  
  930. function getEleCssPath(el) {
  931. // 获取元素css path
  932. if (!(el instanceof Element)) return;
  933. var path = [];
  934. while (el.nodeType === Node.ELEMENT_NODE) {
  935. var selector = el.nodeName.toLowerCase();
  936. if (el.id) {
  937. selector += "#" + el.id;
  938. path.unshift(selector);
  939. break;
  940. } else {
  941. var sib = el,
  942. nth = 1;
  943. while ((sib = sib.previousElementSibling)) {
  944. if (sib.nodeName.toLowerCase() == selector) nth++;
  945. }
  946. if (nth != 1) selector += ":nth-of-type(" + nth + ")";
  947. }
  948. path.unshift(selector);
  949. el = el.parentNode;
  950. }
  951. return path.join(" > ");
  952. }
  953.  
  954. function handleSlideMenuClick({ isPostmessage } = {}) {
  955. if (top === self) {
  956. alert("请点击滑动验证码的大图片,小图片,滑块。");
  957. }
  958. this.vue = new Vue();
  959. this.isIframe = top !== self;
  960. GM_deleteValue(SLIDE_STORE_KEY);
  961.  
  962. let imgs = [...selectorAll("img")];
  963. let divTags = [...selectorAll("div")];
  964. imgs.forEach((img) => {
  965. img.addEventListener("click", onSlideTagClick);
  966. }, false);
  967. divTags.forEach((input) => {
  968. input.addEventListener("click", onSlideTagClick);
  969. }, false);
  970.  
  971. setTimeout(() => {
  972. imgs.forEach((img) => {
  973. img && img.removeEventListener("click", onSlideTagClick);
  974. }, false);
  975. divTags.forEach((input) => {
  976. input.removeEventListener("click", onSlideTagClick);
  977. }, false);
  978. }, 30000);
  979.  
  980. if (!isPostmessage) {
  981. if (self === top) {
  982. const iframes = [...selectorAll("iframe")];
  983. iframes.forEach((iframe) => {
  984. iframe.contentWindow.postMessage(
  985. {
  986. sign: "husky",
  987. action: "handleSlideMenuClick",
  988. },
  989. "*"
  990. );
  991. });
  992. } else {
  993. window.postMessage(
  994. {
  995. sign: "husky",
  996. action: "handleSlideMenuClick",
  997. },
  998. "*"
  999. );
  1000. }
  1001. }
  1002. }
  1003.  
  1004. let noticeTimer = 0;
  1005.  
  1006. function notice(msg) {
  1007. if (noticeTimer) {
  1008. clearTimeout(noticeTimer);
  1009. } else {
  1010. setTimeout(() => new Vue().$message.success(msg));
  1011. }
  1012. noticeTimer = setTimeout(() => new Vue().$message.success(msg), 1000);
  1013. }
  1014.  
  1015. var onSlideTagClick = (e) => {
  1016. let el = e.target;
  1017. let tagName = el.tagName.toLowerCase();
  1018. let width = Number(getNumber(getStyle(el).width)) || 0;
  1019. const vue = new Vue();
  1020. let height = Number(getNumber(getStyle(el).height)) || 0;
  1021. let position = getStyle(el).position;
  1022. let pathCache =
  1023. (GM_getValue(SLIDE_STORE_KEY) &&
  1024. JSON.parse(GM_getValue(SLIDE_STORE_KEY))) ||
  1025. {};
  1026. if (tagName === "img") {
  1027. if (width >= height && width > 150) {
  1028. let newValue = { ...pathCache, bgImg: getEleCssPath(el) };
  1029. GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
  1030. pathCache = newValue;
  1031. notice("您已成功选择大图片");
  1032. } else if (width < 100 && height >= width - 5) {
  1033. let newValue = { ...pathCache, targetImg: getEleCssPath(el) };
  1034. GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
  1035. pathCache = newValue;
  1036. notice("您已成功选择小图片");
  1037. }
  1038. } else {
  1039. let curEl = el;
  1040. for (let i = 0; i < 3; i++) {
  1041. if (!curEl || curEl === Window) {
  1042. break;
  1043. }
  1044. position = getStyle(curEl).position;
  1045. let bgUrl = getStyle(curEl)["backgroundImage"];
  1046. width = Number(getNumber(getStyle(curEl).width)) || 0;
  1047. height = Number(getNumber(getStyle(curEl).height)) || 0;
  1048.  
  1049. if (position === "absolute" && width < 100 && height < 100) {
  1050. let newValue = { ...pathCache, moveItem: getEleCssPath(curEl) };
  1051. GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
  1052. pathCache = newValue;
  1053. notice("您已成功选择滑块");
  1054. break;
  1055. }
  1056. let reg = /url\("(.+)"\)/im;
  1057.  
  1058. if (bgUrl && bgUrl.match(reg)) {
  1059. if (width >= height && width > 150) {
  1060. let newValue = { ...pathCache, bgImg: getEleCssPath(curEl) };
  1061. GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
  1062. pathCache = newValue;
  1063. notice("您已成功选择大图片");
  1064. break;
  1065. } else if (width < 100 && height >= width - 5) {
  1066. let newValue = { ...pathCache, targetImg: getEleCssPath(curEl) };
  1067. GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
  1068. pathCache = newValue;
  1069. notice("您已成功选择小图片");
  1070. break;
  1071. }
  1072. }
  1073. curEl = curEl.parentNode;
  1074. }
  1075.  
  1076. curEl = el;
  1077. const firstImg = curEl.querySelector("img");
  1078. firstImg && onSlideTagClick({ target: firstImg });
  1079. }
  1080.  
  1081. const finish = Object.keys(pathCache).filter((item) => item).length == 3;
  1082. if (finish) {
  1083. let imgs = [...selectorAll("img")];
  1084. let divTags = [...selectorAll("div")];
  1085. imgs.forEach((img) => {
  1086. img && img.removeEventListener("click", onSlideTagClick);
  1087. }, false);
  1088. divTags.forEach((div) => {
  1089. div.removeEventListener("click", onSlideTagClick);
  1090. }, false);
  1091. setTimeout(() => {
  1092. vue.$message.success("选择完毕,赶快试试吧");
  1093. captchaInstance.doCheckTask();
  1094. }, 3000);
  1095. }
  1096. };
  1097.  
  1098. // GM_registerMenuCommand("手动定位滑动验证码", handleSlideMenuClick);
  1099.  
  1100. function handleClearMenuClick() {
  1101. GM_listValues().forEach((name) => {
  1102. if (name.includes("husky")) {
  1103. GM_deleteValue(name);
  1104. }
  1105. });
  1106. }
  1107.  
  1108. GM_registerMenuCommand("清空所有验证码配置", handleClearMenuClick);
  1109.  
  1110. function cleanCurrentPage() {
  1111. GM_deleteValue(SLIDE_STORE_KEY);
  1112. GM_deleteValue(NORMAL_STORE_KEY);
  1113. }
  1114. GM_registerMenuCommand("清空当前页面验证码配置", cleanCurrentPage);
  1115.  
  1116. let blackListMenuId = null;
  1117.  
  1118. function blackListCheck() {
  1119. let key = location.host + location.pathname + "_black";
  1120. let data = GM_getValue(key) && JSON.parse(GM_getValue(key));
  1121. if (blackListMenuId) {
  1122. GM_unregisterMenuCommand(blackListMenuId);
  1123. }
  1124. if (data) {
  1125. blackListMenuId = GM_registerMenuCommand(
  1126. "标记当前网站有验证码",
  1127. labelWebsite
  1128. );
  1129. } else {
  1130. blackListMenuId = GM_registerMenuCommand(
  1131. "标记当前网站没有验证码",
  1132. labelWebsite
  1133. );
  1134. }
  1135. return data;
  1136. }
  1137.  
  1138. function labelWebsite() {
  1139. let key = location.host + location.pathname + "_black";
  1140. let data = GM_getValue(key) && JSON.parse(GM_getValue(key));
  1141. if (data) {
  1142. GM_setValue(key, "false");
  1143. } else {
  1144. GM_setValue(key, "true");
  1145. }
  1146. notice(
  1147. "操作成功," + (data ? "已标记网站有验证码" : "已标记网站没有验证码")
  1148. );
  1149. if (data) {
  1150. captchaInstance = captchaInstance || new Captcha();
  1151. captchaInstance.init();
  1152. }
  1153. blackListCheck();
  1154. }
  1155. blackListCheck();
  1156.  
  1157. var captchaInstance = null;
  1158.  
  1159. function main() {
  1160. window.addEventListener("DOMContentLoaded", function () {
  1161. init();
  1162. captchaInstance = new Captcha();
  1163. });
  1164. }
  1165.  
  1166. const actions = {
  1167. handleSlideMenuClick: handleSlideMenuClick,
  1168. };
  1169.  
  1170. window.addEventListener(
  1171. "message",
  1172. (event) => {
  1173. const { data = {} } = event || {};
  1174. const { sign, action } = data;
  1175. if (sign === "husky") {
  1176. if (action && actions[action]) {
  1177. actions[action]({ isPostmessage: true });
  1178. }
  1179. }
  1180. },
  1181. false
  1182. );
  1183. main();
  1184. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址