const SET_VALUE_REGEX = /http:\/\/(www\.)?magic\.js\/value\/write/ const GET_VALUE_REGEX = /http:\/\/(www\.)?magic\.js\/value\/read/ const DEL_VALUE_REGEX = /http:\/\/(www\.)?magic\.js\/value\/del/ const SCRIPT_NAME = "MagicJS"; let body = {} let magicJS = MagicJS(SCRIPT_NAME, "INFO"); async function Main(){ if (magicJS.isRequest){ if (SET_VALUE_REGEX.test(magicJS.request.url)){ try{ let key = magicJS.request.url.match(/key=([^&]*)/)[1] let val = magicJS.request.url.match(/val=([^&]*)/)[1] let session = magicJS.request.url.match(/session=([^&]*)/) session = !!session? session[1] : ''; magicJS.write(key, val, session); if (magicJS.read(key, session) == val){ magicJS.notify('变量写入成功'); body = {'success': true, 'msg': '变量写入成功', 'key': key, 'val': val, 'session': session} } else{ magicJS.notify('变量写入失败'); body = {'success': false, 'msg': '变量写入失败', 'key': key, 'val': magicJS.read(key, session), 'session': session} } } catch (err){ magicJS.notify('变量写入失败'); body = {'success': false, 'msg': '变量写入失败'}; } } else if (GET_VALUE_REGEX.test(magicJS.request.url)){ try{ let key = magicJS.request.url.match(/key=([^&]*)/)[1] let session = magicJS.request.url.match(/session=([^&]*)/) session = !!session? session[1] : ''; val = magicJS.read(key, session); magicJS.notify('读取变量成功'); body = {'success': true, 'msg': '读取变量成功', 'key': key, 'val': val, 'session': session} } catch (err){ magicJS.notify('读取变量失败'); body = {'success': false, 'msg': '读取变量失败'}; } } else if (DEL_VALUE_REGEX.test(magicJS.request.url)){ try{ let key = magicJS.request.url.match(/key=([^&]*)/)[1] let session = magicJS.request.url.match(/session=([^&]*)/) session = !!session? session[1] : ''; val = magicJS.del(key, session); if (!!magicJS.read(key, session)){ magicJS.notify('删除变量失败'); body = {'success': true, 'msg': '删除变量失败', 'key': key, 'session': session} } else{ magicJS.notify('删除变量成功'); body = {'success': true, 'msg': '删除变量成功', 'key': key, 'session': session} } } catch (err){ magicJS.notify('删除变量失败'); body = {'success': false, 'msg': '删除变量失败'}; } } else{ magicJS.notify('请求格式错误'); body = {'success': false, 'msg': '请求格式错误'}; } body = JSON.stringify(body); let resp = {} if (magicJS.isSurge || magicJS.isLoon){ resp = { response: { status: 200, body: body, headers: { 'Content-type': 'application/json;charset=utf-8' } } } } if (magicJS.isQuanX){ resp = { body: body, headers: { 'Content-type': 'application/json;charset=utf-8' }, status: "HTTP/1.1 200 OK" } } magicJS.done(resp); } } Main(); function MagicJS(scriptName='MagicJS', logLevel='INFO'){ return new class{ constructor(){ this.version = '2.2.3.1' this.scriptName = scriptName; this.logLevels = {DEBUG: 5, INFO: 4, NOTIFY: 3, WARNING: 2, ERROR: 1, CRITICAL: 0, NONE: -1}; this.isLoon = typeof $loon !== 'undefined'; this.isQuanX = typeof $task !== 'undefined'; this.isJSBox = typeof $drive !== 'undefined'; this.isNode = typeof module !== 'undefined' && !this.isJSBox; this.isSurge = typeof $httpClient !== 'undefined' && !this.isLoon; this.node = {'request': undefined, 'fs': undefined, 'data': {}}; this.iOSUserAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1'; this.pcUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36 Edg/84.0.522.59'; this.logLevel = logLevel; this._barkUrl = ''; if (this.isNode){ this.node.fs = require('fs'); this.node.request = require('request'); try{ this.node.fs.accessSync('./magic.json', this.node.fs.constants.R_OK | this.node.fs.constants.W_OK); } catch(err){ this.node.fs.writeFileSync('./magic.json', '{}', {encoding: 'utf8'}) } this.node.data = require('./magic.json'); } else if (this.isJSBox){ if (!$file.exists('drive://MagicJS')){ $file.mkdir('drive://MagicJS'); } if (!$file.exists('drive://MagicJS/magic.json')){ $file.write({ data: $data({string: '{}'}), path: 'drive://MagicJS/magic.json' }) } } } set barkUrl(url){this._barkUrl = url.replace(/\/+$/g, '');} set logLevel(level) {this._logLevel = typeof level === 'string'? level.toUpperCase(): 'DEBUG'}; get logLevel() {return this._logLevel}; get isRequest() {return typeof $request !== 'undefined' && typeof $response === 'undefined'} get isResponse() {return typeof $response !== 'undefined' } get request() {return typeof $request !== 'undefined' ? $request : undefined } get response() { if (typeof $response !== 'undefined'){ if ($response.hasOwnProperty('status')) $response['statusCode'] = $response['status'] if ($response.hasOwnProperty('statusCode')) $response['status'] = $response['statusCode'] return $response; } else{ return undefined; } } get platform(){ if (this.isSurge) return "Surge" else if (this.isQuanX) return "Quantumult X" else if (this.isLoon) return "Loon" else if (this.isJSBox) return "JSBox" else if (this.isNode) return "Node.js" else return "unknown" } read(key, session=''){ let val = ''; // 读取原始数据 if (this.isSurge || this.isLoon) { val = $persistentStore.read(key); } else if (this.isQuanX) { val = $prefs.valueForKey(key); } else if (this.isNode){ val = this.node.data; } else if (this.isJSBox){ val = $file.read('drive://MagicJS/magic.json').string; } try { // Node 和 JSBox数据处理 if (this.isNode) val = val[key] if (this.isJSBox) val = JSON.parse(val)[key]; // 带Session的情况 if (!!session){ if(typeof val === 'string') val = JSON.parse(val); val = !!val && typeof val === 'object' ? val[session]: null; } } catch (err){ this.logError(err); val = !!session? {} : null; this.del(key); } if (typeof val === 'undefined') val = null; try {if(!!val && typeof val === 'string') val = JSON.parse(val)} catch(err) {} this.logDebug(`READ DATA [${key}]${!!session? `[${session}]`: ''}(${typeof val})\n${JSON.stringify(val)}`); return val; }; write(key, val, session=''){ let data = !!session ? {} : ''; // 读取原先存储的JSON格式数据 if (!!session && (this.isSurge || this.isLoon)) { data = $persistentStore.read(key); } else if (!!session && this.isQuanX) { data = $prefs.valueForKey(key); } else if (this.isNode){ data = this.node.data; } else if (this.isJSBox){ data = JSON.parse($file.read('drive://MagicJS/magic.json').string); } if (!!session){ // 有Session,所有数据都是Object try { if (typeof data === 'string') data = JSON.parse(data) data = typeof data === 'object' && !!data ? data : {}; } catch(err){ this.logError(err); this.del(key); data = {}; }; if (this.isJSBox || this.isNode){ // 构造数据 if (!data.hasOwnProperty(key) || typeof data[key] != 'object'){ data[key] = {}; } if (!data[key].hasOwnProperty(session)){ data[key][session] = null; } // 写入或删除数据 if (typeof val === 'undefined'){ delete data[key][session]; } else{ data[key][session] = val; } } else { // 写入或删除数据 if (typeof val === 'undefined'){ delete data[session]; } else{ data[session] = val; } } } // 没有Session时 else{ if (this.isNode || this.isJSBox){ // 删除数据 if (typeof val === 'undefined'){ delete data[key]; } else{ data[key] = val; } } else{ // 删除数据 if (typeof val === 'undefined'){ data = null; } else{ data = val; } } } // 数据回写 if (typeof data === 'object') data = JSON.stringify(data); if (this.isSurge || this.isLoon) { $persistentStore.write(data, key); } else if (this.isQuanX) { $prefs.setValueForKey(data, key); } else if (this.isNode){ this.node.fs.writeFileSync('./magic.json', data) } else if (this.isJSBox){ $file.write({data: $data({string: data}), path: 'drive://MagicJS/magic.json'}); } this.logDebug(`WRITE DATA [${key}]${!!session? `[${session}]`: ''}(${typeof val})\n${JSON.stringify(val)}`); }; del(key, session=''){ this.logDebug(`DELETE KEY [${key}]${!!session ? `[${session}]`:''}`); this.write(key, null, session); } /** * iOS系统通知 * @param {*} title 通知标题 * @param {*} subTitle 通知副标题 * @param {*} body 通知内容 * @param {*} opts 通知选项,目前支持传入超链接或Object * Surge不支持通知选项,Loon和QuantumultX支持打开URL和多媒体通知 * opts "applestore://" 打开Apple Store * opts "https://www.apple.com.cn/" 打开Apple.com.cn * opts {'open-url': 'https://www.apple.com.cn/'} 打开Apple.com.cn * opts {'open-url': 'https://www.apple.com.cn/', 'media-url': 'https://raw.githubusercontent.com/Orz-3/mini/master/Apple.png'} 打开Apple.com.cn,显示一个苹果Logo */ notify(title=this.scriptName, subTitle='', body='', opts=''){ this.logNotify(`title:${title}\nsubTitle:${subTitle}\nbody:${body}\noptions:${typeof opts === 'object'? JSON.stringify(opts) : opts}`); let convertOptions = (_opts) =>{ let newOpts = {}; if (typeof _opts === 'string'){ if (this.isLoon) newOpts = {'openUrl': _opts}; else if (this.isQuanX) newOpts = {'open-url': _opts}; } else if (typeof _opts === 'object'){ if (this.isLoon){ newOpts['openUrl'] = !!_opts['open-url']? _opts['open-url']: ''; newOpts['mediaUrl'] = !!_opts['media-url']? _opts['media-url']: ''; } else if (this.isQuanX) newOpts = !!_opts['open-url'] || !!_opts['media-url'] ? _opts : {}; } return newOpts; } opts = convertOptions(opts); // 支持单个参数通知 if (arguments.length == 1){ title = this.scriptName; subTitle = '', body = arguments[0]; } if (this.isSurge){ $notification.post(title, subTitle, body); } else if (this.isLoon){ if (!!opts) $notification.post(title, subTitle, body, opts); else $notification.post(title, subTitle, body); } else if (this.isQuanX) { $notify(title, subTitle, body, opts); } else if (this.isNode) { if (!!this._barkUrl){ let content = encodeURI(`${title}/${subTitle}\n${body}`) this.get(`${this._barkUrl}/${content}`, ()=>{}); } } else if (this.isJSBox){ let push = { title: title, body: !!subTitle ? `${subTitle}\n${body}` : body, } $push.schedule(push); } } log(msg, level="INFO"){ if (!(this.logLevels[this._logLevel] < this.logLevels[level.toUpperCase()])) console.log(`[${level}] [${this.scriptName}]\n${msg}\n`); } logDebug(msg){ this.log(msg, "DEBUG"); } logInfo(msg){ this.log(msg, "INFO"); } logNotify(msg){ this.log(msg, "NOTIFY"); } logWarning(msg){ this.log(msg, "WARNING"); } logError(msg){ this.log(msg, "ERROR"); } /** * 对传入的Http Options根据不同环境进行适配 * @param {*} options */ adapterHttpOptions(options, method){ let _options = typeof options === 'object'? Object.assign({}, options): {'url': options, 'headers': {}}; if (_options.hasOwnProperty('header') && !_options.hasOwnProperty('headers')){ _options['headers'] = _options['header']; delete _options['header']; } // 规范化的headers const headersMap = { 'accept': 'Accept', 'accept-ch': 'Accept-CH', 'accept-charset': 'Accept-Charset', 'accept-features': 'Accept-Features', 'accept-encoding': 'Accept-Encoding', 'accept-language': 'Accept-Language', 'accept-ranges': 'Accept-Ranges', 'access-control-allow-credentials': 'Access-Control-Allow-Credentials', 'access-control-allow-origin': 'Access-Control-Allow-Origin', 'access-control-allow-methods': 'Access-Control-Allow-Methods', 'access-control-allow-headers': 'Access-Control-Allow-Headers', 'access-control-max-age': 'Access-Control-Max-Age', 'access-control-expose-headers': 'Access-Control-Expose-Headers', 'access-control-request-method': 'Access-Control-Request-Method', 'access-control-request-headers': 'Access-Control-Request-Headers', 'age': 'Age', 'allow': 'Allow', 'alternates': 'Alternates', 'authorization': 'Authorization', 'cache-control': 'Cache-Control', 'connection': 'Connection', 'content-encoding': 'Content-Encoding', 'content-language': 'Content-Language', 'content-length': 'Content-Length', 'content-location': 'Content-Location', 'content-md5': 'Content-MD5', 'content-range': 'Content-Range', 'content-security-policy': 'Content-Security-Policy', 'content-type': 'Content-Type', 'cookie': 'Cookie', 'dnt': 'DNT', 'date': 'Date', 'etag': 'ETag', 'expect': 'Expect', 'expires': 'Expires', 'from': 'From', 'host': 'Host', 'if-match': 'If-Match', 'if-modified-since': 'If-Modified-Since', 'if-none-match': 'If-None-Match', 'if-range': 'If-Range', 'if-unmodified-since': 'If-Unmodified-Since', 'last-event-id': 'Last-Event-ID', 'last-modified': 'Last-Modified', 'link': 'Link', 'location': 'Location', 'max-forwards': 'Max-Forwards', 'negotiate': 'Negotiate', 'origin': 'Origin', 'pragma': 'Pragma', 'proxy-authenticate': 'Proxy-Authenticate', 'proxy-authorization': 'Proxy-Authorization', 'range': 'Range', 'referer': 'Referer', 'retry-after': 'Retry-After', 'sec-websocket-extensions': 'Sec-Websocket-Extensions', 'sec-websocket-key': 'Sec-Websocket-Key', 'sec-websocket-origin': 'Sec-Websocket-Origin', 'sec-websocket-protocol': 'Sec-Websocket-Protocol', 'sec-websocket-version': 'Sec-Websocket-Version', 'server': 'Server', 'set-cookie': 'Set-Cookie', 'set-cookie2': 'Set-Cookie2', 'strict-transport-security': 'Strict-Transport-Security', 'tcn': 'TCN', 'te': 'TE', 'trailer': 'Trailer', 'transfer-encoding': 'Transfer-Encoding', 'upgrade': 'Upgrade', 'user-agent': 'User-Agent', 'variant-vary': 'Variant-Vary', 'vary': 'Vary', 'via': 'Via', 'warning': 'Warning', 'www-authenticate': 'WWW-Authenticate', 'x-content-duration': 'X-Content-Duration', 'x-content-security-policy': 'X-Content-Security-Policy', 'x-dnsprefetch-control': 'X-DNSPrefetch-Control', 'x-frame-options': 'X-Frame-Options', 'x-requested-with': 'X-Requested-With', 'x-surge-skip-scripting':'X-Surge-Skip-Scripting' } if (typeof _options.headers === 'object'){ for (let key in _options.headers){ if (headersMap[key]) { _options.headers[headersMap[key]] = _options.headers[key]; delete _options.headers[key]; } } } // 自动补完User-Agent,减少请求特征 if (!!!_options.headers || typeof _options.headers !== 'object' || !!!_options.headers['User-Agent']){ if (!!!_options.headers || typeof _options.headers !== 'object') _options.headers = {}; if (this.isNode) _options.headers['User-Agent'] = this.pcUserAgent; else _options.headers['User-Agent'] = this.iOSUserAgent } // 判断是否跳过脚本处理 let skipScripting = false; if ((typeof _options['opts'] === 'object' && (_options['opts']['hints'] === true || _options['opts']['Skip-Scripting'] === true)) || (typeof _options['headers'] === 'object' && _options['headers']['X-Surge-Skip-Scripting'] === true)){ skipScripting = true; } if (!skipScripting){ if (this.isSurge) _options.headers['X-Surge-Skip-Scripting'] = false; else if (this.isLoon) _options.headers['X-Requested-With'] = 'XMLHttpRequest'; else if (this.isQuanX){ if (typeof _options['opts'] !== 'object') _options.opts = {}; _options.opts['hints'] = false; } } // 对请求数据做清理 if (!this.isSurge || skipScripting) delete _options.headers['X-Surge-Skip-Scripting']; if (!this.isQuanX && _options.hasOwnProperty('opts')) delete _options['opts']; if (this.isQuanX && _options.hasOwnProperty('opts')) delete _options['opts']['Skip-Scripting']; // GET请求将body转换成QueryString(beta) if (method === 'GET' && !this.isNode && !!_options.body){ let qs = Object.keys(_options.body).map(key=>{ if (typeof _options.body === 'undefined') return '' return `${encodeURIComponent(key)}=${encodeURIComponent(_options.body[key])}` }).join('&'); if (_options.url.indexOf('?') < 0) _options.url += '?' if (_options.url.lastIndexOf('&')+1 != _options.url.length && _options.url.lastIndexOf('?')+1 != _options.url.length) _options.url += '&' _options.url += qs; delete _options.body; } // 适配多环境 if (this.isQuanX){ if (_options.hasOwnProperty('body') && typeof _options['body'] !== 'string') _options['body'] = JSON.stringify(_options['body']); _options['method'] = method; } else if (this.isNode){ delete _options.headers['Accept-Encoding']; if (typeof _options.body === 'object'){ if (method === 'GET'){ _options.qs = _options.body; delete _options.body } else if (method === 'POST'){ _options['json'] = true; _options.body = _options.body; } } } else if (this.isJSBox){ _options['header'] = _options['headers']; delete _options['headers'] } return _options; } /** * Http客户端发起GET请求 * @param {*} options * @param {*} callback * options可配置参数headers和opts,用于判断由脚本发起的http请求是否跳过脚本处理。 * 支持Surge和Quantumult X两种配置方式。 * 以下几种配置会跳过脚本处理,options没有opts或opts的值不匹配,则不跳过脚本处理 * {opts:{"hints": true}} * {opts:{"Skip-Scripting": true}} * {headers: {"X-Surge-Skip-Scripting": true}} */ get(options, callback){ let _options = this.adapterHttpOptions(options, 'GET'); this.logDebug(`HTTP GET: ${JSON.stringify(_options)}`); if (this.isSurge || this.isLoon) { $httpClient.get(_options, callback); } else if (this.isQuanX) { $task.fetch(_options).then( resp => { resp['status'] = resp.statusCode callback(null, resp, resp.body) }, reason => callback(reason.error, null, null), ) } else if(this.isNode){ return this.node.request.get(_options, callback); } else if(this.isJSBox){ _options['handler'] = (resp)=>{ let err = resp.error? JSON.stringify(resp.error) : undefined; let data = typeof resp.data === 'object' ? JSON.stringify(resp.data) : resp.data; callback(err, resp.response, data); } $http.get(_options); } } /** * Http客户端发起POST请求 * @param {*} options * @param {*} callback * options可配置参数headers和opts,用于判断由脚本发起的http请求是否跳过脚本处理。 * 支持Surge和Quantumult X两种配置方式。 * 以下几种配置会跳过脚本处理,options没有opts或opts的值不匹配,则不跳过脚本处理 * {opts:{"hints": true}} * {opts:{"Skip-Scripting": true}} * {headers: {"X-Surge-Skip-Scripting": true}} */ post(options, callback){ let _options = this.adapterHttpOptions(options, 'POST'); this.logDebug(`HTTP POST: ${JSON.stringify(_options)}`); if (this.isSurge || this.isLoon) { $httpClient.post(_options, callback); } else if (this.isQuanX) { $task.fetch(_options).then( resp => { resp['status'] = resp.statusCode callback(null, resp, resp.body) }, reason => {callback(reason.error, null, null)} ) } else if(this.isNode){ return this.node.request.post(_options, callback); } else if(this.isJSBox){ _options['handler'] = (resp)=>{ let err = resp.error? JSON.stringify(resp.error) : undefined; let data = typeof resp.data === 'object' ? JSON.stringify(resp.data) : resp.data; callback(err, resp.response, data); } $http.post(_options); } } done(value = {}){ if (typeof $done !== 'undefined'){ $done(value); } } isToday(day){ if (day == null){ return false; } else{ let today = new Date(); if (typeof day == 'string'){ day = new Date(day); } if (today.getFullYear() == day.getFullYear() && today.getMonth() == day.getMonth() && today.getDay() == day.getDay()){ return true; } else{ return false; } } } isNumber(val) { return parseFloat(val).toString() === "NaN"? false: true; } /** * 对await执行中出现的异常进行捕获并返回,避免写过多的try catch语句 * 示例:let [err,val] = await magicJS.attempt(func(), 'defaultvalue'); * 或者:let [err, [val1,val2]] = await magicJS.attempt(func(), ['defaultvalue1', 'defaultvalue2']); * @param {*} promise Promise 对象 * @param {*} defaultValue 出现异常时返回的默认值 * @returns 返回两个值,第一个值为异常,第二个值为执行结果 */ attempt(promise, defaultValue=null){ return promise.then((args)=>{return [null, args]}).catch(ex=>{this.logError(ex); return [ex, defaultValue]})}; /** * 重试方法 * @param {*} fn 需要重试的函数 * @param {number} [retries=5] 重试次数 * @param {number} [interval=0] 每次重试间隔 * @param {function} [callback=null] 函数没有异常时的回调,会将函数执行结果result传入callback,根据result的值进行判断,如果需要再次重试,在callback中throw一个异常,适用于函数本身没有异常但仍需重试的情况。 * @returns 返回一个Promise对象 */ retry(fn, retries=5, interval=0, callback=null) { return (...args)=>{ return new Promise((resolve, reject) =>{ function _retry(...args){ Promise.resolve().then(()=>fn.apply(this,args)).then( result => { if (typeof callback === 'function'){ Promise.resolve().then(()=>callback(result)).then(()=>{resolve(result)}).catch(ex=>{ this.logError(ex); if (retries >= 1 && interval > 0){ setTimeout(() => _retry.apply(this, args), interval); } else if (retries >= 1) { _retry.apply(this, args); } else{ reject(ex); } retries --; }); } else{ resolve(result); } } ).catch(ex=>{ this.logError(ex); if (retries >= 1 && interval > 0){ setTimeout(() => _retry.apply(this, args), interval); } else if (retries >= 1) { _retry.apply(this, args); } else{ reject(ex); } retries --; }) } _retry.apply(this, args); }); }; } formatTime(time, fmt="yyyy-MM-dd hh:mm:ss") { var o = { "M+": time.getMonth() + 1, "d+": time.getDate(), "h+": time.getHours(), "m+": time.getMinutes(), "s+": time.getSeconds(), "q+": Math.floor((time.getMonth() + 3) / 3), "S": time.getMilliseconds() }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (time.getFullYear() + "").substr(4 - RegExp.$1.length)); for (let k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; }; now(){ return this.formatTime(new Date(), "yyyy-MM-dd hh:mm:ss"); } today(){ return this.formatTime(new Date(), "yyyy-MM-dd"); } sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); } }(scriptName); }