识别并自动填写北#交通大学MIS入口的验证码,使用前需自行申请讯飞印刷文字识别API
// ==UserScript== // @name 识别并自动填写MIS验证码 // @namespace https://github.com/ZiuChen/NO-FLASH-Upload // @version 1.1.0 // @description 识别并自动填写北#交通大学MIS入口的验证码,使用前需自行申请讯飞印刷文字识别API // @author Ziu // @match https://cas.bjtu.edu.cn/* // @connect webapi.xfyun.cn // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @require https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js // @icon https://fastly.jsdelivr.net/gh/ZiuChen/ZiuChen@main/avatar.jpg // @license MIT // ==/UserScript== /** * 使用前请先申请: 讯飞开放## 印刷文字识别接口 申请链接 www.xfyun.cn 每天可免费识别 500 次 * 控制台/我的应用/文字识别/印刷文字识别 获取到 APPID 和 APIKey * apikey存储在本地不会上传到服务器 */ const global = { /** * 讯飞印刷文字识别接口地址 */ hostUrl: 'https://webapi.xfyun.cn/v1/service/v1/ocr/general', /** * APPID */ appid: null, /** * APIKey */ apiKey: null, /** * 验证码图片选择器 */ imgSelector: '.captcha', /** * 验证码输入框选择器 */ inputSelector: '#id_captcha_1' } const console = { log: window.console.log.bind(window.console, '[识别并自动填写MIS验证码]'), error: window.console.error.bind(window.console, '[识别并自动填写MIS验证码]'), warn: window.console.warn.bind(window.console, '[识别并自动填写MIS验证码]'), info: window.console.info.bind(window.console, '[识别并自动填写MIS验证码]') } /** * 获取验证码图片的base64编码 */ async function getCaptchaImage() { const img = document.querySelector(global.imgSelector) if (!img) { alert('未找到验证码图片') return } const canvas = document.createElement('canvas') canvas.width = img.width canvas.height = img.height const ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0) const base64 = canvas.toDataURL() return base64 } /** * blob转base64 */ async function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => { resolve(reader.r###lt) } reader.onerror = reject reader.readAsDataURL(blob) }) } /** * 组装请求头 */ function getReqHeader() { const xParamStr = CryptoJS.enc.Base64.stringify( CryptoJS.enc.Utf8.parse( JSON.stringify({ language: 'cn|en' }) ) ) const timeStamp = parseInt(new Date().getTime() / 1000) // 获取当前时间戳 const xCheckSum = CryptoJS.MD5(global.apiKey + timeStamp + xParamStr).toString() return { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'X-Appid': global.appid, 'X-CurTime': timeStamp + '', 'X-Param': xParamStr, 'X-CheckSum': xCheckSum } } /** * 修正识别字符串 */ function reviseString(ocrR###lt) { const rules = { '*': ['x', 'X', '×'], '/': ['.'], ' ': ['='] // remove } let res = ocrR###lt for (const symbol of Object.keys(rules)) { const rule = rules[symbol] rule.forEach((r) => { if (ocrR###lt.indexOf(r) !== -1) { res = res.replace(r, symbol) } }) } // 修正后的字符串中仍然存在非数字字符 if (res.match(/[^0-9\+\-\*\/\.]/g)) { res = res.replace(/[^0-9\+\-\*\/\.]/g, '') } console.log('originString: ' + ocrR###lt) console.log('rtnString: ' + res) return res } /** * 处理ocr识别传回的字符串 * 执行计算并返回结果 */ function calcR###lt(string) { try { return eval(reviseString(string)) } catch (error) { confirm('计算失败,点击确定重新识别') && location.reload() } } /** * GM_xmlhttpRequest 封装 */ function fetchWithGM(url, options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url, headers: options.headers, data: options.data, responseType: options.responseType || 'json', timeout: options.timeout || 10 * 1000, onload: resolve, onerror: reject, ontimeout: () => reject('请求超时') }) }) } /** * 参数预检查并填充到全局变量 */ function precheck() { // 优先从 GM_getValue 中获取 global.appid = GM_getValue('xf_appid') global.apiKey = GM_getValue('xf_apiKey') if (!global.appid || !global.apiKey) { // 尝试从 localStorage 中获取 global.appid = localStorage.getItem('xf_appid') global.apiKey = localStorage.getItem('xf_apiKey') } if (!global.appid || !global.apiKey) { const appid = prompt('[讯飞印刷文字识别] 请输入appid: ') if (appid) { global.appid = appid GM_setValue('xf_appid', appid) } const apiKey = prompt('[讯飞印刷文字识别] 请输入apiKey: ') if (apiKey) { global.apiKey = apiKey GM_setValue('xf_apiKey', apiKey) } } if (!global.appid || !global.apiKey) { return false } return true } /** * 识别并填充验证码 * @param {*} image base64编码的验证码图片 */ async function captchAndFill(image) { // 将 base64 图片转换为讯飞识别接口所需的格式 image = 'image=' + image.split('base64,')[1] if (!precheck()) { alert('初始化错误,请检查 Key 是否正确输入') } const input = document.querySelector(global.inputSelector) let inputPlaceholder = input?.placeholder inputPlaceholder && (input.placeholder = '正在识别验证码...') try { const res = await fetchWithGM(global.hostUrl, { method: 'POST', headers: getReqHeader(), data: image, responseType: 'json' }) console.log('res', res) const ocrR###lt = res?.response?.data?.block[0]?.line[0]?.word[0]?.content if (!ocrR###lt) { throw new Error('ocrR###lt is invalid', ocrR###lt) } const numberR###lt = calcR###lt(ocrR###lt) if (numberR###lt === undefined) { throw new Error('numberR###lt is invalid', numberR###lt) } const target = document.querySelector(global.inputSelector) if (!target) { alert('未找到验证码输入框') return } target.value = numberR###lt // 填入输入框内 } catch (error) { console.error(error) confirm('识别失败,点击确定重新识别') && location.reload() } input.placeholder = inputPlaceholder } ;(async () => { const base64 = await getCaptchaImage() captchAndFill(base64) // 点击验证码图片时重新识别 document.querySelector(global.imgSelector).addEventListener('click', async () => { const base64 = await getCaptchaImage() captchAndFill(base64) }) })()