/**
 * 根据自己的习惯整合各个开发者而形成的工具包（@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 = `⚠️使用说明</br>详情【<a href='${scritpUrl}?raw=true'><font class='red--text'>点我查看</font></a>】`
                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)
}
