🏠 Home 

网页通用验证码识别

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


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