/** * 根据自己的习惯整合各个开发者而形成的工具包(@NobyDa, @chavyleung) * 兼容surge,quantumult x,loon,node环境 * 并且加入一些好用的方法 * 方法如下: * isEmpty: 判断字符串是否是空(undefined,null,空串) * getRequestUrl: 获取请求的url(目前仅支持surge和quanx) * getResponseBody: 获取响应体(目前仅支持surge和quanx) * boxJsJsonBuilder:构建最简默认boxjs配置json * randomString: 生成随机字符串 * autoComplete: 自动补齐字符串 * customReplace: 自定义替换 * hash: 字符串做hash * * ⚠️当开启当且仅当执行失败的时候通知选项,请在执行失败的地方执行execFail() * * @param scriptName 脚本名,用于通知时候的标题 * @param scriptId 每个脚本唯一的id,用于存储持久化的时候加入key * @param options 传入一些参数,目前参数如下; * httpApi=ffff@3.3.3.18:6166(这个是默认值,本人surge调试脚本用,可自行修改) * target_boxjs_json_path=/Users/lowking/Desktop/Scripts/lowking.boxjs.json(生成boxjs配置的目标文件路径) * @constructor */ function ToolKit(scriptName, scriptId, options) { return new (class { constructor(scriptName, scriptId, options) { this.tgEscapeCharMapping = { '&': '&', '#': '#' } this.userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.2 Safari/605.1.15` this.prefix = `lk` this.name = scriptName this.id = scriptId this.data = null this.dataFile = this.getRealPath(`${this.prefix}${this.id}.dat`) this.boxJsJsonFile = this.getRealPath(`${this.prefix}${this.id}.boxjs.json`) //surge http api等一些扩展参数 this.options = options //命令行入参 this.isExecComm = false //默认脚本开关 this.isEnableLog = this.getVal(`${this.prefix}IsEnableLog${this.id}`) this.isEnableLog = this.isEmpty(this.isEnableLog) ? true : JSON.parse(this.isEnableLog) this.isNotifyOnlyFail = this.getVal(`${this.prefix}NotifyOnlyFail${this.id}`) this.isNotifyOnlyFail = this.isEmpty(this.isNotifyOnlyFail) ? false : JSON.parse(this.isNotifyOnlyFail) //tg通知开关 this.isEnableTgNotify = this.getVal(`${this.prefix}IsEnableTgNotify${this.id}`) this.isEnableTgNotify = this.isEmpty(this.isEnableTgNotify) ? false : JSON.parse(this.isEnableTgNotify) this.tgNotifyUrl = this.getVal(`${this.prefix}TgNotifyUrl${this.id}`) this.isEnableTgNotify = this.isEnableTgNotify ? !this.isEmpty(this.tgNotifyUrl) : this.isEnableTgNotify //计时部分 this.costTotalStringKey = `${this.prefix}CostTotalString${this.id}` this.costTotalString = this.getVal(this.costTotalStringKey) this.costTotalString = this.isEmpty(this.costTotalString) ? `0,0` : this.costTotalString.replace("\"", "") this.costTotalMs = this.costTotalString.split(",")[0] this.execCount = this.costTotalString.split(",")[1] this.costTotalMs = this.isEmpty(this.costTotalMs) ? 0 : parseInt(this.costTotalMs) this.execCount = this.isEmpty(this.execCount) ? 0 : parseInt(this.execCount) this.logSeparator = '\n██' this.now = new Date() this.startTime = this.now.getTime() this.node = (() => { if (this.isNode()) { const request = require('request') return ({ request }) } else { return (null) } })() this.execStatus = true this.notifyInfo = [] this.log(`${this.name}, 开始执行!`) this.initCache() this.checkRecordRequestBody() this.execComm() } checkRecordRequestBody() { if (!this.isRequest()) { return; } const reqBody = $request.body; if (!reqBody) { return; } const path = $request.path; let cacheKey = this.id + "#" + path.replace("/", "_"); cacheKey = cacheKey.replace("?", "#"); if (this.isQuanX()) $prefs.setValueForKey(reqBody, cacheKey); if (this.isLoon() || this.isSurge()) $persistentStore.write(reqBody, cacheKey); if (this.isNode()) { this.node.fs.writeFileSync( `${cacheKey}.json`, reqBody, { flag: "w" }, (err) => console.log(err) ); } } getRequestBody() { const path = $request.path; let cacheKey = this.id + "#" + path.replace("/", "_"); cacheKey = cacheKey.replace("?", "#"); if (this.isSurge() || this.isLoon()) { return $persistentStore.read(cacheKey); } if (this.isQuanX()) { return $prefs.valueForKey(cacheKey); } if (this.isNode()) { const fpath = `${cacheKey}.json`; if (!this.node.fs.existsSync(fpath)) { return JSON.parse( this.node.fs.readFileSync(fpath) ); } } } // persistence // initialize cache initCache() { const pKey = this.getPersistKey(); if (this.isQuanX()) this.cache = JSON.parse($prefs.valueForKey(pKey) || "{}"); if (this.isLoon() || this.isSurge()) this.cache = JSON.parse($persistentStore.read(pKey) || "{}"); if (this.isNode()) { // create a json for root cache let fpath = "root.json"; if (!this.node.fs.existsSync(fpath)) { this.node.fs.writeFileSync( fpath, JSON.stringify({}), { flag: "wx" }, (err) => console.log(err) ); } this.root = {}; // create a json file with the given name if not exists fpath = `${pKey}.json`; if (!this.node.fs.existsSync(fpath)) { this.node.fs.writeFileSync( fpath, JSON.stringify({}), { flag: "wx" }, (err) => console.log(err) ); this.cache = {}; } else { this.cache = JSON.parse( this.node.fs.readFileSync(`${pKey}.json`) ); } } } getPersistKey() { return `${this.id}#privateCache`; } // store cache persistCache() { const pKey = this.getPersistKey(); const data = JSON.stringify(this.cache, null, 2); if (this.isQuanX()) $prefs.setValueForKey(data, pKey); if (this.isLoon() || this.isSurge()) $persistentStore.write(data, pKey); if (this.isNode()) { this.node.fs.writeFileSync( `${pKey}.json`, data, { flag: "w" }, (err) => console.log(err) ); this.node.fs.writeFileSync( "root.json", JSON.stringify(this.root, null, 2), { flag: "w" }, (err) => console.log(err) ); } } write(data, key) { this.log(`SET ${key}`); if (key.indexOf("#") !== -1) { key = key.substr(1); if (isSurge || this.isLoon()) { return $persistentStore.write(data, key); } if (this.isQuanX()) { return $prefs.setValueForKey(data, key); } if (this.isNode()) { this.root[key] = data; } } else { this.cache[key] = data; } this.persistCache(); } read(key) { this.log(`READ ${key}`); if (key.indexOf("#") !== -1) { key = key.substr(1); if (this.isSurge() || this.isLoon()) { return $persistentStore.read(key); } if (this.isQuanX()) { return $prefs.valueForKey(key); } if (this.isNode()) { return this.root[key]; } } else { return this.cache[key]; } } delete(key) { this.log(`DELETE ${key}`); if (key.indexOf("#") !== -1) { key = key.substr(1); if (this.isSurge() || this.isLoon()) { return $persistentStore.write(null, key); } if (this.isQuanX()) { return $prefs.removeValueForKey(key); } if (this.isNode()) { delete this.root[key]; } } else { delete this.cache[key]; } this.persistCache(); } //当执行命令的目录不是脚本所在目录时,自动把文件路径改成指令传入的路径并返回完整文件路径 getRealPath(fileName) { if (this.isNode()) { let targetPath = process.argv.slice(1, 2)[0].split("/") targetPath[targetPath.length - 1] = fileName return targetPath.join("/") } return fileName } /** * http://boxjs.com/ => http://boxjs.com * http://boxjs.com/app/jd => http://boxjs.com */ getUrlHost(url) { return url.slice(0, url.indexOf('/', 8)) } /** * http://boxjs.com/ => * http://boxjs.com/api/getdata => /api/getdata */ getUrlPath(url) { // 如果以结尾, 去掉最后一个/ const end = url.lastIndexOf('/') === url.length - 1 ? -1 : undefined // slice第二个参数传 undefined 会直接截到最后 // indexOf第二个参数用来跳过前面的 "https://" return url.slice(url.indexOf('/', 8), end) } async execComm() { //支持node命令,实现发送手机测试 if (this.isNode()) { this.comm = process.argv.slice(1) let isHttpApiErr = false if (this.comm[1] == "p") { this.isExecComm = true //phone this.log(`开始执行指令【${this.comm[1]}】=> 发送到手机测试脚本!`); if (this.isEmpty(this.options) || this.isEmpty(this.options.httpApi)) { this.log(`未设置options,使用默认值`) //设置默认值 if (this.isEmpty(this.options)) { this.options = {} } this.options.httpApi = `ffff@10.0.0.9:6166` } else { //判断格式 if (!/.*?@.*?:[0-9]+/.test(this.options.httpApi)) { isHttpApiErr = true this.log(`❌httpApi格式错误!格式:ffff@3.3.3.18:6166`) this.done() } } if (!isHttpApiErr) { this.callApi(this.comm[2]) } } } } callApi(timeout) { // 直接用接收到文件路径,解决在不同目录下都可以使用 node xxxx/xxx.js p 指令发送脚本给手机执行 // let fname = this.getCallerFileNameAndLine().split(":")[0].replace("[", "") let fname = this.comm[0] this.log(`获取【${fname}】内容传给手机`) let scriptStr = '' this.fs = this.fs ? this.fs : require('fs') this.path = this.path ? this.path : require('path') const curDirDataFilePath = this.path.resolve(fname) const rootDirDataFilePath = this.path.resolve(process.cwd(), fname) const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath) const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath) if (isCurDirDataFile || isRootDirDataFile) { const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath try { scriptStr = this.fs.readFileSync(datPath) } catch (e) { scriptStr = '' } } else { scriptStr = '' } let options = { url: `http://${this.options.httpApi.split("@")[1]}/v1/scripting/evaluate`, headers: { "X-Key": `${this.options.httpApi.split("@")[0]}` }, body: { "script_text": `${scriptStr}`, "mock_type": "cron", "timeout": (!this.isEmpty(timeout) && timeout > 5) ? timeout : 5 }, json: true } this.post(options, (_error, _response, _data) => { this.log(`已将脚本【${fname}】发给手机!`) this.done() }) } getCallerFileNameAndLine() { let error try { throw Error('') } catch (err) { error = err } const stack = error.stack const stackArr = stack.split('\n') let callerLogIndex = 1 if (callerLogIndex !== 0) { const callerStackLine = stackArr[callerLogIndex] this.path = this.path ? this.path : require('path') return `[${callerStackLine.substring(callerStackLine.lastIndexOf(this.path.sep) + 1, callerStackLine.lastIndexOf(':'))}]` } else { return '[-]' } } getFunName(fun) { var ret = fun.toString() ret = ret.substr('function '.length) ret = ret.substr(0, ret.indexOf('(')) return ret } boxJsJsonBuilder(info, param) { if (this.isNode()) { let boxjsJsonPath = "/Users/lowking/Desktop/Scripts/lowking.boxjs.json" // 从传入参数param读取配置的boxjs的json文件路径 if (param && param.hasOwnProperty("target_boxjs_json_path")) { boxjsJsonPath = param["target_boxjs_json_path"] } if (!this.fs.existsSync(boxjsJsonPath)) { return } if (!this.isJsonObject(info) || !this.isJsonObject(param)) { this.log("构建BoxJsJson传入参数格式错误,请传入json对象") return } this.log('using node') let needAppendKeys = ["settings", "keys"] const domain = 'https://raw.githubusercontent.com/Orz-3' let boxJsJson = {} let scritpUrl = '#lk{script_url}' if (param && param.hasOwnProperty('script_url')) { scritpUrl = this.isEmpty(param['script_url']) ? "#lk{script_url}" : param['script_url'] } boxJsJson.id = `${this.prefix}${this.id}` boxJsJson.name = this.name boxJsJson.desc_html = `⚠️使用说明
详情【点我查看】` boxJsJson.icons = [`${domain}/mini/master/Alpha/${this.id.toLocaleLowerCase()}.png`, `${domain}/mini/master/Color/${this.id.toLocaleLowerCase()}.png`] boxJsJson.keys = [] boxJsJson.settings = [ { "id": `${this.prefix}IsEnableLog${this.id}`, "name": "开启/关闭日志", "val": true, "type": "boolean", "desc": "默认开启" }, { "id": `${this.prefix}NotifyOnlyFail${this.id}`, "name": "只当执行失败才通知", "val": false, "type": "boolean", "desc": "默认关闭" }, { "id": `${this.prefix}IsEnableTgNotify${this.id}`, "name": "开启/关闭Telegram通知", "val": false, "type": "boolean", "desc": "默认关闭" }, { "id": `${this.prefix}TgNotifyUrl${this.id}`, "name": "Telegram通知地址", "val": "", "type": "text", "desc": "Tg的通知地址,如:https://api.telegram.org/bot-token/sendMessage?chat_id=-100140&parse_mode=Markdown&text=" } ] boxJsJson.author = "#lk{author}" boxJsJson.repo = "#lk{repo}" boxJsJson.script = `${scritpUrl}?raw=true` // 除了settings和keys追加,其他的都覆盖 if (!this.isEmpty(info)) { for (let i in needAppendKeys) { let key = needAppendKeys[i] if (!this.isEmpty(info[key])) { // 处理传入的每项设置 if (key === 'settings') { for (let i = 0; i < info[key].length; i++) { let input = info[key][i] for (let j = 0; j < boxJsJson.settings.length; j++) { let def = boxJsJson.settings[j] if (input.id === def.id) { // id相同,就使用外部传入的配置 boxJsJson.settings.splice(j, 1) } } } } boxJsJson[key] = boxJsJson[key].concat(info[key]) } delete info[key] } } Object.assign(boxJsJson, info) if (this.isNode()) { this.fs = this.fs ? this.fs : require('fs') this.path = this.path ? this.path : require('path') const curDirDataFilePath = this.path.resolve(this.boxJsJsonFile) const rootDirDataFilePath = this.path.resolve(process.cwd(), this.boxJsJsonFile) const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath) const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath) const jsondata = JSON.stringify(boxJsJson, null, '\t') if (isCurDirDataFile) { this.fs.writeFileSync(curDirDataFilePath, jsondata) } else if (isRootDirDataFile) { this.fs.writeFileSync(rootDirDataFilePath, jsondata) } else { this.fs.writeFileSync(curDirDataFilePath, jsondata) } // 写到项目的boxjs订阅json中 let boxjsJson = JSON.parse(this.fs.readFileSync(boxjsJsonPath)) if (boxjsJson.hasOwnProperty("apps") && Array.isArray(boxjsJson["apps"]) && boxjsJson["apps"].length > 0) { let apps = boxjsJson.apps let targetIdx = apps.indexOf(apps.filter((app) => { return app.id == boxJsJson.id })[0]) if (targetIdx >= 0) { boxjsJson.apps[targetIdx] = boxJsJson } else { boxjsJson.apps.push(boxJsJson) } let ret = JSON.stringify(boxjsJson, null, 2) if (!this.isEmpty(param)) { for (const key in param) { let val = '' if (param.hasOwnProperty(key)) { val = param[key] } else if (key === 'author') { val = '@lowking' } else if (key === 'repo') { val = 'https://github.com/lowking/Scripts' } ret = ret.replace(`#lk{${key}}`, val) } } // 全部处理完毕检查是否有漏掉未配置的参数,进行提醒 const regex = /(?:#lk\{)(.+?)(?=\})/ let m = regex.exec(ret) if (m !== null) { this.log('生成BoxJs还有未配置的参数,请参考https://github.com/lowking/Scripts/blob/master/util/example/ToolKitDemo.js#L17-L18传入参数:\n') } let loseParamSet = new Set() while ((m = regex.exec(ret)) !== null) { loseParamSet.add(m[1]) ret = ret.replace(`#lk{${m[1]}}`, ``) } loseParamSet.forEach(p => { console.log(`${p} `) }) this.fs.writeFileSync(boxjsJsonPath, ret) } } } } isJsonObject(obj) { return typeof (obj) == "object" && Object.prototype.toString.call(obj).toLowerCase() == "[object object]" && !obj.length } appendNotifyInfo(info, type) { if (type == 1) { this.notifyInfo = info } else { this.notifyInfo.push(info) } } prependNotifyInfo(info) { this.notifyInfo.splice(0, 0, info) } execFail() { this.execStatus = false } isRequest() { return typeof $request != "undefined" } isSurge() { return typeof $httpClient != "undefined" } isQuanX() { return typeof $task != "undefined" } isLoon() { return typeof $loon != "undefined" } isJSBox() { return typeof $app != "undefined" && typeof $http != "undefined" } isStash() { return 'undefined' !== typeof $environment && $environment['stash-version'] } isNode() { return typeof require == "function" && !this.isJSBox() } async sleep(time) { return new Promise((resolve) => setTimeout(resolve, time)); } async wait(time) { return new Promise((resolve) => setTimeout(resolve, time)); } async delay(time) { return new Promise((resolve) => setTimeout(resolve, time)); } log(message) { if (this.isEnableLog) console.log(`${this.logSeparator}${message}`) } logErr(message) { this.execStatus = true if (this.isEnableLog) { console.log(`${this.logSeparator}${this.name}执行异常:`) console.log(message) console.log('\n' + `${message.message}`) } } msg(subtitle, message, openUrl, mediaUrl) { if (!this.isRequest() && this.isNotifyOnlyFail && this.execStatus) { //开启了当且仅当执行失败的时候通知,并且执行成功了,这时候不通知 } else { if (this.isEmpty(message)) { if (Array.isArray(this.notifyInfo)) { message = this.notifyInfo.join("\n") } else { message = this.notifyInfo } } if (!this.isEmpty(message)) { if (this.isEnableTgNotify) { this.log(`${this.name}Tg通知开始`) //处理特殊字符 for (let key in this.tgEscapeCharMapping) { if (!this.tgEscapeCharMapping.hasOwnProperty(key)) { continue } message = message.replace(key, this.tgEscapeCharMapping[key]) } this.get({ url: encodeURI(`${this.tgNotifyUrl}📌${this.name}` + '\n' + `${message}`) }, (_error, _statusCode, _body) => { this.log(`Tg通知完毕`) }) } else { let options = {} const hasOpenUrl = !this.isEmpty(openUrl) const hasMediaUrl = !this.isEmpty(mediaUrl) if (this.isQuanX()) { if (hasOpenUrl) options["open-url"] = openUrl if (hasMediaUrl) options["media-url"] = mediaUrl $notify(this.name, subtitle, message, options) } if (this.isSurge() || this.isStash()) { if (hasOpenUrl) options["url"] = openUrl $notification.post(this.name, subtitle, message, options) } if (this.isNode()) this.log("⭐️" + this.name + "\n" + subtitle + "\n" + message) if (this.isJSBox()) $push.schedule({ title: this.name, body: subtitle ? subtitle + "\n" + message : message }) } } } } pushWxMsg(summary, content, url, callback = () => { }) { let data = { appToken: "AT_rTc93GQYIdMU8XLRnoJaSea8WkfhSzhX", content: content, summary: summary, contentType: 1, topicIds: [], uids: [ "UID_6P4B00X6Zv8U2oKC0I2R09emxtqq" ], url: "", verifyPay: false }; if (url) { data.url = url; } const headers = this.getJsonDoneHeaders(); headers.Host = 'wxpusher.zjiecode.com'; headers['Content-Type'] = 'application/json;charset=UTF-8'; let options = { url: 'https://wxpusher.zjiecode.com/api/send/message', headers: headers, body: JSON.stringify(data), }; this.post(options, callback); } getVal(key, defaultValue = "") { let value if (this.isSurge() || this.isLoon() || this.isStash()) { value = $persistentStore.read(key) } else if (this.isQuanX()) { value = $prefs.valueForKey(key) } else if (this.isNode()) { this.data = this.loadData() value = process.env[key] || this.data[key] } else { value = (this.data && this.data[key]) || null } return !value ? defaultValue : value } setVal(key, val) { if (this.isSurge() || this.isLoon() || this.isStash()) { return $persistentStore.write(val, key) } else if (this.isQuanX()) { return $prefs.setValueForKey(val, key) } else if (this.isNode()) { this.data = this.loadData() this.data[key] = val this.writeData() return true } else { return (this.data && this.data[key]) || null } } loadData() { if (this.isNode()) { this.fs = this.fs ? this.fs : require('fs') this.path = this.path ? this.path : require('path') const curDirDataFilePath = this.path.resolve(this.dataFile) const rootDirDataFilePath = this.path.resolve(process.cwd(), this.dataFile) const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath) const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath) if (isCurDirDataFile || isRootDirDataFile) { const datPath = isCurDirDataFile ? curDirDataFilePath : rootDirDataFilePath try { return JSON.parse(this.fs.readFileSync(datPath)) } catch (e) { return {} } } else return {} } else return {} } writeData() { if (this.isNode()) { this.fs = this.fs ? this.fs : require('fs') this.path = this.path ? this.path : require('path') const curDirDataFilePath = this.path.resolve(this.dataFile) const rootDirDataFilePath = this.path.resolve(process.cwd(), this.dataFile) const isCurDirDataFile = this.fs.existsSync(curDirDataFilePath) const isRootDirDataFile = !isCurDirDataFile && this.fs.existsSync(rootDirDataFilePath) const jsondata = JSON.stringify(this.data) if (isCurDirDataFile) { this.fs.writeFileSync(curDirDataFilePath, jsondata) } else if (isRootDirDataFile) { this.fs.writeFileSync(rootDirDataFilePath, jsondata) } else { this.fs.writeFileSync(curDirDataFilePath, jsondata) } } } adapterStatus(response) { if (response) { if (response.status) { response["statusCode"] = response.status } else if (response.statusCode) { response["status"] = response.statusCode } } return response } get(options, callback = () => { }) { if (this.isQuanX()) { if (typeof options == "string") options = { url: options } options["method"] = "GET" $task.fetch(options).then(response => { callback(null, this.adapterStatus(response), response.body) }, reason => callback(reason.error, null, null)) } if (this.isSurge() || this.isLoon() || this.isStash()) $httpClient.get(options, (error, response, body) => { callback(error, this.adapterStatus(response), body) }) if (this.isNode()) { this.node.request(options, (error, response, body) => { callback(error, this.adapterStatus(response), body) }) } if (this.isJSBox()) { if (typeof options == "string") options = { url: options } options["header"] = options["headers"] options["handler"] = function (resp) { let error = resp.error if (error) error = JSON.stringify(resp.error) let body = resp.data if (typeof body == "object") body = JSON.stringify(resp.data) callback(error, this.adapterStatus(resp.response), body) } $http.get(options) } } post(options, callback = () => { }) { if (this.isQuanX()) { if (typeof options == "string") options = { url: options } options["method"] = "POST" $task.fetch(options).then(response => { callback(null, this.adapterStatus(response), response.body) }, reason => callback(reason.error, null, null)) } if (this.isSurge() || this.isLoon() || this.isStash()) { $httpClient.post(options, (error, response, body) => { callback(error, this.adapterStatus(response), body) }) } if (this.isNode()) { this.node.request.post(options, (error, response, body) => { callback(error, this.adapterStatus(response), body) }) } if (this.isJSBox()) { if (typeof options == "string") options = { url: options } options["header"] = options["headers"] options["handler"] = function (resp) { let error = resp.error if (error) error = JSON.stringify(resp.error) let body = resp.data if (typeof body == "object") body = JSON.stringify(resp.data) callback(error, this.adapterStatus(resp.response), body) } $http.post(options) } } put(options, callback = () => { }) { if (this.isQuanX()) { // no test if (typeof options == "string") options = { url: options } options["method"] = "PUT" $task.fetch(options).then(response => { callback(null, this.adapterStatus(response), response.body) }, reason => callback(reason.error, null, null)) } if (this.isSurge() || this.isLoon() || this.isStash()) { options.method = "PUT" $httpClient.put(options, (error, response, body) => { callback(error, this.adapterStatus(response), body) }) } if (this.isNode()) { options.method = "PUT" this.node.request.put(options, (error, response, body) => { callback(error, this.adapterStatus(response), body) }) } if (this.isJSBox()) { // no test if (typeof options == "string") options = { url: options } options["header"] = options["headers"] options["handler"] = function (resp) { let error = resp.error if (error) error = JSON.stringify(resp.error) let body = resp.data if (typeof body == "object") body = JSON.stringify(resp.data) callback(error, this.adapterStatus(resp.response), body) } $http.post(options) } } costTime() { let info = `${this.name}执行完毕!` if (this.isNode() && this.isExecComm) { info = `指令【${this.comm[1]}】执行完毕!` } const endTime = new Date().getTime() const ms = endTime - this.startTime const costTime = ms / 1000 this.execCount++ this.costTotalMs += ms this.log(`${info}耗时【${costTime}】秒\n总共执行【${this.execCount}】次,平均耗时【${((this.costTotalMs / this.execCount) / 1000).toFixed(4)}】秒`) this.setVal(this.costTotalStringKey, JSON.stringify(`${this.costTotalMs},${this.execCount}`)) // this.setVal(this.execCountKey, JSON.stringify(0)) // this.setVal(this.costTotalMsKey, JSON.stringify(0)) } done(value = {}) { this.costTime() if (this.isSurge() || this.isQuanX() || this.isLoon() || this.isStash()) { $done(value) } } getRequestUrl() { return $request.url } getResponseBody() { if ($response) { return $response.body } } isGetCookie(reg) { return !!($request.method != 'OPTIONS' && this.getRequestUrl().match(reg)) } isEmpty(obj) { return typeof obj == "undefined" || obj == null || obj == "" || obj == "null" || obj == "undefined" || obj.length === 0 } randomString(len) { len = len || 32 var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890' var maxPos = $chars.length var pwd = '' for (let i = 0; i < len; i++) { pwd += $chars.charAt(Math.floor(Math.random() * maxPos)) } return pwd } /** * 自动补齐字符串 * @param str 原始字符串 * @param prefix 前缀 * @param suffix 后缀 * @param fill 补齐用字符 * @param len 目标补齐长度,不包含前后缀 * @param direction 方向:0往后补齐 * @param ifCode 是否打码 * @param clen 打码长度 * @param startIndex 起始坐标 * @param cstr 打码字符 * @returns {*} */ autoComplete(str, prefix, suffix, fill, len, direction, ifCode, clen, startIndex, cstr) { str += '' if (str.length < len) { while (str.length < len) { if (direction == 0) { str += fill } else { str = fill + str } } } if (ifCode) { let temp = '' for (var i = 0; i < clen; i++) { temp += cstr } str = str.substring(0, startIndex) + temp + str.substring(clen + startIndex) } str = prefix + str + suffix return this.toDBC(str) } /** * @param str 源字符串 "#{code}, #{value}" * @param param 用于替换的数据,结构如下 * @param prefix 前缀 "#{" * @param suffix 后缀 "}" * { * "code": 1, * "value": 2 * } * 按上面的传入,输出为"1, 2" * 对应的#{code}用param里面code的值替换,#{value}也是 * @returns {*|void|string} */ customReplace(str, param, prefix, suffix) { try { if (this.isEmpty(prefix)) { prefix = "#{" } if (this.isEmpty(suffix)) { suffix = "}" } for (let i in param) { str = str.replace(`${prefix}${i}${suffix}`, param[i]) } } catch (e) { this.logErr(e) } return str } toDBC(txtstring) { var tmp = "" for (var i = 0; i < txtstring.length; i++) { if (txtstring.charCodeAt(i) == 32) { tmp = tmp + String.fromCharCode(12288) } else if (txtstring.charCodeAt(i) < 127) { tmp = tmp + String.fromCharCode(txtstring.charCodeAt(i) + 65248) } } return tmp } hash(str) { let h = 0, i, chr for (i = 0; i < str.length; i++) { chr = str.charCodeAt(i) h = (h << 5) - h + chr h |= 0 // Convert to 32bit integer } return String(h) } /** * formatDate y:年 M:月 d:日 q:季 H:时 m:分 s:秒 S:毫秒 */ formatDate(date, format) { let o = { 'M+': date.getMonth() + 1, 'd+': date.getDate(), 'H+': date.getHours(), 'm+': date.getMinutes(), 's+': date.getSeconds(), 'q+': Math.floor((date.getMonth() + 3) / 3), 'S': date.getMilliseconds() } if (/(y+)/.test(format)) format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) for (let k in o) if (new RegExp('(' + k + ')').test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)) return format } /** * parseDate 字符串格式,默认'yyyy-MM-dd',支持如下:y、M、d、H、m、s、S,不支持w和q */ parseDate(str, format) { format = format || 'yyyy-MM-dd'; let obj = { y: 0, M: 1, d: 0, H: 0, h: 0, m: 0, s: 0, S: 0 }; format.replace(/([^yMdHmsS]*?)(([yMdHmsS])\3*)([^yMdHmsS]*?)/g, function (m, $1, $2, $3, $4, idx, old) { str = str.replace(new RegExp($1 + '(\\d{' + $2.length + '})' + $4), function (_m, _$1) { obj[$3] = parseInt(_$1); return ''; }); return ''; }); obj.M--; // 月份是从0开始的,所以要减去1 let date = new Date(obj.y, obj.M, obj.d, obj.H, obj.m, obj.s); if (obj.S !== 0) date.setMilliseconds(obj.S); // 如果设置了毫秒 return date; } objToQueryStr(obj, encode) { let str = '' for (const key in obj) { let value = obj[key] if (value != null && value !== '') { if (typeof value === 'object') { value = JSON.stringify(value) } else if (encode) { value = encodeURIComponent(value) } str += `${key}=${value}&` } } str = str.substring(0, str.length - 1) return str } parseQueryStr(str) { let obj = {} if (str.indexOf("?") > -1) { str = str.split("?")[1] } let arr = str.split("&") for (let i = 0; i < arr.length; i++) { let kv = arr[i].split("=") obj[kv[0]] = kv[1] } return obj } deepClone(obj, newObj) { newObj = newObj || {}; for (let key in obj) { if (typeof obj[key] == 'object') { newObj[key] = (obj[key].constructor === Array) ? [] : {} this.deepClone(obj[key], newObj[key]); } else { newObj[key] = obj[key] } } return newObj; } getBaseDoneHeaders(mixHeaders = {}) { return Object.assign( { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PUT,DELETE', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept' }, mixHeaders ) } getHtmlDoneHeaders() { return this.getBaseDoneHeaders({ 'Content-Type': 'text/html;charset=UTF-8' }) } getJsonDoneHeaders() { return this.getBaseDoneHeaders({ 'Content-Type': 'text/json; charset=utf-8', 'Connection': 'keep-alive' }) } shallowClone(obj) { let clone = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = obj[key]; } } return clone; } // 解析cookie字符串的函数 parseCookies(cookieString) { let dict = {}; cookieString && cookieString.split(';').forEach(function(cookie) { let parts = cookie.split('='); dict[parts.shift().trim()] = decodeURI(parts.join('=')); }); return dict; } // 系列化为cookie字符串 serializeCookies(cookieData) { const parts = []; for(let key in cookieData){ let value = cookieData[key]; let cookiePart = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; parts.push(cookiePart); } return parts.join('; '); } parseSetCookies(cookieString) { const attribKeys = ['Expires', 'Max-Age', 'Domain', 'Path', 'HttpOnly', 'SameSite']; const parts = cookieString.split(';'); let mainKey = null; const retDict = {}; const retData = []; parts.forEach(part => { let pstr = part.trim(); let aKey = null; let aValue = true; if (pstr.includes('=')) { let kvdata = pstr.split('='); aKey = kvdata[0].trim(); aValue = kvdata[1].trim(); } else { aKey = pstr; } if (attribKeys.includes(aKey)) { if (retDict[mainKey]) { retDict[mainKey][aKey] = aValue; let attribs = retDict[mainKey].attribs; attribs[aKey] = aValue; } } else { mainKey = aKey; let attribs = {}; if (mainKey.includes(',')) { const keys = mainKey.split(','); keys.forEach(key => { const k = key.trim(); if (attribKeys.includes(k)) { attribs[k] = true; } else { mainKey = k; } }); } retDict[mainKey] = { name: mainKey, value: aValue, attribs: attribs }; retData.push(retDict[mainKey]); } }); return retData; } base64Encode(str){ let base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let out, i, len; let c1, c2, c3; len = str.length; i = 0; out = ""; while (i < len) { c1 = str.charCodeAt(i++) & 0xff; if (i == len) { out += base64EncodeChars.charAt(c1 >> 2); out += base64EncodeChars.charAt((c1 & 0x3) << 4); out += "=="; break; } c2 = str.charCodeAt(i++); if (i == len) { out += base64EncodeChars.charAt(c1 >> 2); out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); out += base64EncodeChars.charAt((c2 & 0xF) << 2); out += "="; break; } c3 = str.charCodeAt(i++); out += base64EncodeChars.charAt(c1 >> 2); out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); out += base64EncodeChars.charAt(c3 & 0x3F); } return out; } base64Decode(input) { const base64_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; // 确保输入是一个正确的Base64编码字符串 if (/([^\s]+[^0-9a-zA-Z\+\/\=]|[^0-9a-zA-Z\+\/\=]\s+)/.test(input)) { throw new Error('Invalid base64 input'); } let str = input.replace(/\s/g, ''); let output = ''; let chr1, chr2, chr3; let enc1, enc2, enc3, enc4; let i = 0; while (i < str.length) { enc1 = base64_chars.indexOf(str.charAt(i++)); enc2 = base64_chars.indexOf(str.charAt(i++)); enc3 = base64_chars.indexOf(str.charAt(i++)); enc4 = base64_chars.indexOf(str.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 !== 64) { output = output + String.fromCharCode(chr2); } if (enc4 !== 64) { output = output + String.fromCharCode(chr3); } } output = this.utf8Decode(output); return output; } utf8Decode(str_data) { let tmp_arr = [], i = 0, c1 = 0, seqlen = 0; str_data = str_data.replace(/\r\n/g, "\n"); while (i < str_data.length) { c1 = str_data.charCodeAt(i) & 0xFF; seqlen = 0; // Single byte sequence (0xxxxxxx) if (c1 <= 0xBF) { c1 = (c1 & 0x7F); seqlen = 1; } else if (c1 <= 0xDF) { c1 = (c1 & 0x1F); seqlen = 2; } else if (c1 <= 0xEF) { c1 = (c1 & 0x0F); seqlen = 3; } else { c1 = (c1 & 0x07); seqlen = 4; } for (let ai = 1; ai < seqlen; ++ai) { c1 = ((c1 << 0x06) | (str_data.charCodeAt(ai + i) & 0x3F)); } if (seqlen === 4) { c1 -= 0x10000; tmp_arr.push(String.fromCharCode(0xD800 | ((c1 >> 10) & 0x3FF))); tmp_arr.push(String.fromCharCode(0xDC00 | (c1 & 0x3FF))); } else { tmp_arr.push(String.fromCharCode(c1)); } i += seqlen; } return tmp_arr.join(""); } parseJwt(token) { try { const segments = token.split('.'); const base64HeaderUrl = segments[0]; const base64Header = base64HeaderUrl.replace(/-/g, '+').replace(/_/g, '/'); const jsonStrHeader = this.base64Decode(base64Header).replace(/\0/g, ''); const headerData = JSON.parse(jsonStrHeader); const base64PayloadUrl = segments[1]; const base64Payload = base64PayloadUrl.replace(/-/g, '+').replace(/_/g, '/'); const jsonStrPayload = this.base64Decode(base64Payload).replace(/\0/g, ''); const payloadData = JSON.parse(jsonStrPayload); return { header: headerData, payload: payloadData, signature: segments[2], }; } catch (e) { this.log(e); return null; } } })(scriptName, scriptId, options) }