const $ = new Env('BoxJs') // 为 eval 准备的上下文环境 const $eval_env = {} $.version = '0.19.10' $.versionType = 'beta' // 发出的请求需要需要 Surge、QuanX 的 rewrite $.isNeedRewrite = true /** * =================================== * 持久化属性: BoxJs 自有的数据结构 * =================================== */ // 存储`用户偏好` $.KEY_usercfgs = 'chavy_boxjs_userCfgs' // 存储`应用会话` $.KEY_sessions = 'chavy_boxjs_sessions' // 存储`页面缓存` $.KEY_web_cache = 'chavy_boxjs_web_cache' // 存储`应用订阅缓存` $.KEY_app_subCaches = 'chavy_boxjs_app_subCaches' // 存储`全局备份` (弃用, 改用 `chavy_boxjs_backups`) $.KEY_globalBaks = 'chavy_boxjs_globalBaks' // 存储`备份索引` $.KEY_backups = 'chavy_boxjs_backups' // 存储`当前会话` (配合切换会话, 记录当前切换到哪个会话) $.KEY_cursessions = 'chavy_boxjs_cur_sessions' /** * =================================== * 持久化属性: BoxJs 公开的数据结构 * =================================== */ // 存储用户访问`BoxJs`时使用的域名 $.KEY_boxjs_host = 'boxjs_host' // 请求响应体 (返回至页面的结果) $.json = $.name // `接口`类请求的响应体 $.html = $.name // `页面`类请求的响应体 // 页面源码地址 $.web = `https://cdn.jsdelivr.net/gh/chavyleung/scripts@${ $.version }/box/chavy.boxjs.html?_=${new Date().getTime()}` // 版本说明地址 (Release Note) $.ver = `https://raw.githubusercontent.com/chavyleung/scripts/master/box/release/box.release.json` !(async () => { // 勿扰模式 $.isMute = [true, 'true'].includes($.getdata('@chavy_boxjs_userCfgs.isMute')) // 请求路径 $.path = getPath($request.url) // 请求参数 /api/save?id=xx&name=xx => {id: 'xx', name: 'xx'} const [, query] = $.path.split('?') $.queries = query ? query.split('&').reduce((obj, cur) => { const [key, val] = cur.split('=') obj[key] = val return obj }, {}) : {} // 请求类型: GET $.isGet = $request.method === 'GET' // 请求类型: POST $.isPost = $request.method === 'POST' // 请求类型: OPTIONS $.isOptions = $request.method === 'OPTIONS' // 请求类型: page、api、query $.type = 'page' // 查询请求: /query/xxx $.isQuery = $.isGet && /^\/query\/.*?/.test($.path) // 接口请求: /api/xxx $.isApi = $.isPost && /^\/api\/.*?/.test($.path) // 页面请求: /xxx $.isPage = $.isGet && !$.isQuery && !$.isApi // 升级用户数据 upgradeUserData() // 升级备份数据 upgradeGlobalBaks() // 处理预检请求 if ($.isOptions) { $.type = 'options' await handleOptions() } // 处理`页面`请求 else if ($.isPage) { $.type = 'page' await handlePage() } // 处理`查询`请求 else if ($.isQuery) { $.type = 'query' await handleQuery() } // 处理`接口`请求 else if ($.isApi) { $.type = 'api' await handleApi() } })() .catch((e) => $.logErr(e)) .finally(() => doneBox()) /** * http://boxjs.com/ => `http://boxjs.com` * http://boxjs.com/app/jd => `http://boxjs.com` */ function getHost(url) { return url.slice(0, url.indexOf('/', 8)) } /** * http://boxjs.com/ => `` * http://boxjs.com/api/getdata => `/api/getdata` */ function getPath(url) { // 如果以`/`结尾, 去掉最后一个`/` const end = url.lastIndexOf('/') === url.length - 1 ? -1 : undefined // slice第二个参数传 undefined 会直接截到最后 // indexOf第二个参数用来跳过前面的 "https://" return url.slice(url.indexOf('/', 8), end) } /** * =================================== * 处理前端请求 * =================================== */ /** * 处理`页面`请求 */ async function handlePage() { // 获取 BoxJs 数据 const boxdata = getBoxData() boxdata.syscfgs.isDebugMode = false // 调试模式: 是否每次都获取新的页面 const isDebugWeb = [true, 'true'].includes( $.getdata('@chavy_boxjs_userCfgs.isDebugWeb') ) const debugger_web = $.getdata('@chavy_boxjs_userCfgs.debugger_web') const cache = $.getjson($.KEY_web_cache, null) // 如果没有开启调试模式,且当前版本与缓存版本一致,且直接取缓存 if (!isDebugWeb && cache && cache.version === $.version) { $.html = cache.cache } // 如果开启了调试模式,并指定了 `debugger_web` 则从指定的地址获取页面 else { if (isDebugWeb && debugger_web) { // 调试地址后面拼时间缀, 避免 GET 缓存 const isQueryUrl = debugger_web.includes('?') $.web = `${debugger_web}${ isQueryUrl ? '&' : '?' }_=${new Date().getTime()}` boxdata.syscfgs.isDebugMode = true console.log(`[WARN] 调试模式: $.web = : ${$.web}`) } // 如果调用这个方法来获取缓存, 且标记为`非调试模式` const getcache = () => { console.log(`[ERROR] 调试模式: 正在使用缓存的页面!`) boxdata.syscfgs.isDebugMode = false return $.getjson($.KEY_web_cache).cache } await $.http.get($.web).then( (resp) => { if (/BoxJs<\/title>/.test(resp.body)) { // 返回页面源码, 并马上存储到持久化仓库 $.html = resp.body const cache = { version: $.version, cache: $.html } $.setjson(cache, $.KEY_web_cache) } else { // 如果返回的页面源码不是预期的, 则从持久化仓库中获取 $.html = getcache() } }, // 如果获取页面源码失败, 则从持久化仓库中获取 () => ($.html = getcache()) ) } // 根据偏好设置, 替换首屏颜色 (如果是`auto`则交由页面自适应) const theme = $.getdata('@chavy_boxjs_userCfgs.theme') if (theme === 'light') { $.html = $.html.replace('#121212', '#fff') } else if (theme === 'dark') { $.html = $.html.replace('#fff', '#121212') } /** * 后端渲染数据, 感谢 https://t.me/eslint 提供帮助 * * 如果直接渲染到 box: null 会出现双向绑定问题 * 所以先渲染到 `boxServerData: null` 再由前端 `this.box = this.boxServerData` 实现双向绑定 */ $.html = $.html.replace( 'boxServerData: null', 'boxServerData:' + JSON.stringify(boxdata) ) // 调试模式支持 vue Devtools (只有在同时开启调试模式和指定了调试地址才生效) // vue.min.js 生效时, 会导致 @click="window.open()" 报 "window" is not defined 错误 if (isDebugWeb && debugger_web) { $.html = $.html.replace('vue.min.js', 'vue.js') } } /** * 处理`查询`请求 */ async function handleQuery() { const referer = $request.headers.referer || $request.headers.Referer if (!/^https?:\/\/(.+\.)?boxjs\.(com|net)\//.test(referer)) { const isMuteQueryAlert = [true, 'true'].includes( $.getdata('@chavy_boxjs_userCfgs.isMuteQueryAlert') ) if (!isMuteQueryAlert) { // 关闭静默状态 const _isMute = $.isMute $.isMute = false $.msg( $.name, '❗️发现有脚本或人正在读取你的数据', [ '请注意数据安全, 你可以: ', '1. 在 BoxJs 的脚本日志中查看详情', '2. 在 BoxJs 的页面 (侧栏) 中 "不显示查询警告"' ].join('\n') ) // 还原静默状态 $.isMute = _isMute } $.log( [ '', '❗️❗️❗️ 发现有脚本或人正在读取你的数据 ❗️❗️❗️', JSON.stringify($request), '' ].join('\n') ) } const [, query] = $.path.split('/query') if (/^\/boxdata/.test(query)) { $.json = getBoxData() } else if (/^\/baks/.test(query)) { const [, backupId] = query.split('/baks/') $.json = $.getjson(backupId) } else if (/^\/versions$/.test(query)) { await getVersions(true) } else if (/^\/data/.test(query)) { const [, dataKey] = query.split('/data/') $.json = { key: dataKey, val: $.getdata(dataKey) } } } /** * 处理 API 请求 */ async function handleApi() { const [, api] = $.path.split('/api') if (api === '/save' || api.startsWith('/save?')) { await apiSave() } else if (api === '/addAppSub') { await apiAddAppSub() } else if (api === '/reloadAppSub') { await apiReloadAppSub() } else if (api === '/delGlobalBak') { await apiDelGlobalBak() } else if (api === '/updateGlobalBak') { await apiUpdateGlobalBak() } else if (api === '/saveGlobalBak') { await apiSaveGlobalBak() } else if (api === '/impGlobalBak') { await apiImpGlobalBak() } else if (api === '/revertGlobalBak') { await apiRevertGlobalBak() } else if (api === '/runScript') { await apiRunScript() } else if (api === '/saveData') { await apiSaveData() } else if (api === '/surge') { await apiSurge() } else if (api === '/update') { await apiUpdate() } } async function handleOptions() {} /** * =================================== * 获取基础数据 * =================================== */ function getBoxData() { const datas = {} const extraDatas = $.getdata(`${$.KEY_usercfgs.replace('#', '@')}.gist_cache_key`) || [] extraDatas.forEach((key) => { datas[key] = $.getdata(key) }) const usercfgs = getUserCfgs() const sessions = getAppSessions() const curSessions = getCurSessions() const sysapps = getSystemApps() const syscfgs = getSystemCfgs() const appSubCaches = getAppSubCaches() const globalbaks = getGlobalBaks() // 把 `内置应用`和`订阅应用` 里需要持久化属性放到`datas` sysapps.forEach((app) => Object.assign(datas, getAppDatas(app))) usercfgs.appsubs.forEach((sub) => { const subcache = appSubCaches[sub.url] if (subcache && subcache.apps && Array.isArray(subcache.apps)) { subcache.apps.forEach((app) => Object.assign(datas, getAppDatas(app))) } }) const box = { datas, usercfgs, sessions, curSessions, sysapps, syscfgs, appSubCaches, globalbaks } return box } /** * 获取系统配置 */ function getSystemCfgs() { // prettier-ignore return { env: $.isStash() ? 'Stash' : $.isShadowrocket() ? 'Shadowrocket' : $.isLoon() ? 'Loon' : $.isQuanX() ? 'QuanX' : $.isSurge() ? 'Surge' : 'Node', version: $.version, versionType: $.versionType, envs: [ { id: 'Surge', icons: ['https://raw.githubusercontent.com/Orz-3/mini/none/surge.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/surge.png'] }, { id: 'QuanX', icons: ['https://raw.githubusercontent.com/Orz-3/mini/none/quanX.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/quantumultx.png'] }, { id: 'Loon', icons: ['https://raw.githubusercontent.com/Orz-3/mini/none/loon.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/loon.png'] }, { id: 'Shadowrocket', icons: ['https://raw.githubusercontent.com/Orz-3/mini/master/Alpha/shadowrocket.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/shadowrocket.png'] }, { id: 'Stash', icons: ['https://raw.githubusercontent.com/Orz-3/mini/master/Alpha/stash.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/stash.png'] } ], chavy: { id: 'ChavyLeung', icon: 'https://avatars3.githubusercontent.com/u/29748519', repo: 'https://github.com/chavyleung/scripts' }, senku: { id: 'GideonSenku', icon: 'https://avatars1.githubusercontent.com/u/39037656', repo: 'https://github.com/GideonSenku' }, id77: { id: 'id77', icon: 'https://avatars0.githubusercontent.com/u/9592236', repo: 'https://github.com/id77' }, orz3: { id: 'Orz-3', icon: 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/Orz-3.png', repo: 'https://github.com/Orz-3/' }, boxjs: { id: 'BoxJs', show: false, icon: 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/box.png', icons: ['https://raw.githubusercontent.com/Orz-3/mini/master/Alpha/box.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/box.png'], repo: 'https://github.com/chavyleung/scripts' }, defaultIcons: ['https://raw.githubusercontent.com/Orz-3/mini/master/Alpha/appstore.png', 'https://raw.githubusercontent.com/Orz-3/mini/master/Color/appstore.png'] } } /** * 获取内置应用 */ function getSystemApps() { // prettier-ignore const sysapps = [ { id: 'BoxSetting', name: '偏好设置', descs: ['可手动执行一些抹掉数据的脚本', '可设置明暗两种主题下的主色调', '可设置壁纸清单'], keys: [ '@chavy_boxjs_userCfgs.httpapi', '@chavy_boxjs_userCfgs.bgimg', '@chavy_boxjs_userCfgs.http_backend', '@chavy_boxjs_userCfgs.color_dark_primary', '@chavy_boxjs_userCfgs.color_light_primary' ], settings: [ { id: '@chavy_boxjs_userCfgs.httpapis', name: 'HTTP-API (Surge)', val: '', type: 'textarea', placeholder: ',examplekey@127.0.0.1:6166', autoGrow: true, rows: 2, persistentHint:true, desc: '示例: ,examplekey@127.0.0.1:6166! 注意: 以逗号开头, 逗号分隔多个地址, 可加回车' }, { id: '@chavy_boxjs_userCfgs.httpapi_timeout', name: 'HTTP-API Timeout (Surge)', val: 20, type: 'number', persistentHint:true, desc: '如果脚本作者指定了超时时间, 会优先使用脚本指定的超时时间.' }, { id: '@chavy_boxjs_userCfgs.http_backend', name: 'HTTP Backend (Quantumult X)', val: '', type: 'text',placeholder: 'http://127.0.0.1:9999', persistentHint:true, desc: '示例: http://127.0.0.1:9999 ! 注意: 必须是以 http 开头的完整路径, 不能是 / 结尾' }, { id: '@chavy_boxjs_userCfgs.debugger_webs', name: '调试地址', val: 'Dev体验,https://raw.githubusercontent.com/chavyleung/scripts/boxjs.dev/box/chavy.boxjs.html', type: 'textarea', placeholder: '每行一个配置,用逗号分割每个配置的名字和链接:配置,url', persistentHint:true, autoGrow: true, rows: 2, desc: '逗号分隔名字和链接, 回车分隔多个地址' }, { id: '@chavy_boxjs_userCfgs.bgimgs', name: '背景图片清单', val: '无,\n跟随系统,跟随系统\nlight,http://api.btstu.cn/sjbz/zsy.php\ndark,https://uploadbeta.com/api/pictures/random\n妹子,http://api.btstu.cn/sjbz/zsy.php', type: 'textarea', placeholder: '无,{回车} 跟随系统,跟随系统{回车} light,图片地址{回车} dark,图片地址{回车} 妹子,图片地址', persistentHint:true, autoGrow: true, rows: 2, desc: '逗号分隔名字和链接, 回车分隔多个地址' }, { id: '@chavy_boxjs_userCfgs.bgimg', name: '背景图片', val: '', type: 'text', placeholder: 'http://api.btstu.cn/sjbz/zsy.php', persistentHint:true, desc: '输入背景图标的在线链接' }, { id: '@chavy_boxjs_userCfgs.changeBgImgEnterDefault', name: '手势进入壁纸模式默认背景图片', val: '', type: 'text', placeholder: '填写上面背景图片清单的值', persistentHint:true, desc: '' }, { id: '@chavy_boxjs_userCfgs.changeBgImgOutDefault', name: '手势退出壁纸模式默认背景图片', val: '', type: 'text', placeholder: '填写上面背景图片清单的值', persistentHint:true, desc: '' }, { id: '@chavy_boxjs_userCfgs.color_light_primary', name: '明亮色调', canvas: true, val: '#F7BB0E', type: 'colorpicker', desc: '' }, { id: '@chavy_boxjs_userCfgs.color_dark_primary', name: '暗黑色调', canvas: true, val: '#2196F3', type: 'colorpicker', desc: '' } ], scripts: [ { name: "抹掉:所有缓存", script: "https://raw.githubusercontent.com/chavyleung/scripts/master/box/scripts/boxjs.revert.caches.js" }, { name: "抹掉:收藏应用", script: "https://raw.githubusercontent.com/chavyleung/scripts/master/box/scripts/boxjs.revert.usercfgs.favapps.js" }, { name: "抹掉:用户偏好", script: "https://raw.githubusercontent.com/chavyleung/scripts/master/box/scripts/boxjs.revert.usercfgs.js" }, { name: "抹掉:所有会话", script: "https://raw.githubusercontent.com/chavyleung/scripts/master/box/scripts/boxjs.revert.usercfgs.sessions.js" }, { name: "抹掉:所有备份", script: "https://raw.githubusercontent.com/chavyleung/scripts/master/box/scripts/boxjs.revert.baks.js" }, { name: "抹掉:BoxJs (注意备份)", script: "https://raw.githubusercontent.com/chavyleung/scripts/master/box/scripts/boxjs.revert.boxjs.js" } ], author: '@chavyleung', repo: 'https://github.com/chavyleung/scripts/blob/master/box/switcher/box.switcher.js', icons: [ 'https://raw.githubusercontent.com/chavyleung/scripts/master/box/icons/BoxSetting.mini.png', 'https://raw.githubusercontent.com/chavyleung/scripts/master/box/icons/BoxSetting.png' ] }, { id: 'BoxSwitcher', name: '会话切换', desc: '打开静默运行后, 切换会话将不再发出系统通知 \n注: 不影响日志记录', keys: [], settings: [{ id: 'CFG_BoxSwitcher_isSilent', name: '静默运行', val: false, type: 'boolean', desc: '切换会话时不发出系统通知!' }], author: '@chavyleung', repo: 'https://github.com/chavyleung/scripts/blob/master/box/switcher/box.switcher.js', icons: [ 'https://raw.githubusercontent.com/chavyleung/scripts/master/box/icons/BoxSwitcher.mini.png', 'https://raw.githubusercontent.com/chavyleung/scripts/master/box/icons/BoxSwitcher.png' ], script: 'https://raw.githubusercontent.com/chavyleung/scripts/master/box/switcher/box.switcher.js' }, { id: "BoxGist", name: "Gist备份", keys: [ "@gist.token", "@gist.username", "@gist.split", "@gist.revision_options", "@gist.backup_type" ], author: "@dompling", repo: "https://github.com/dompling/Script/tree/master/gist", icons: [ "https://raw.githubusercontent.com/Former-Years/icon/master/github-bf.png", "https://raw.githubusercontent.com/Former-Years/icon/master/github-bf.png" ], descs_html: [ "<h2>Token的获取方式</h2>", "<ol>头像菜单 -></ol>", "<ol>Settings -></ol>", "<ol>Developer settings -></ol>", "<ol>Personal access tokens -></ol>", "<ol>Generate new token -></ol>", "<ol>在里面找到 gist 勾选提交</ol>", "<h2>Gist Revision Id</h2>", "<ol>打开Gist项目</ol>", "<ol>默认为Code,选择Revisions</ol>", "<ol>找到需要恢复的版本文件</ol>", "<ol>点击右上角【...】>【View file】</ol>", "<ol>浏览器地址最后一串为 RevisionId</ol>" ], scripts: [ { name: "备份 Gist", script: "https://raw.githubusercontent.com/dompling/Script/master/gist/backup.js" }, { name: "从 Gist 恢复", script: "https://raw.githubusercontent.com/dompling/Script/master/gist/restore.js" }, { name: "更新历史版本", script: "https://raw.githubusercontent.com/dompling/Script/master/gist/commit.js" } ], settings: [ { id: "@gist.split", name: "用户数据分段", val: null, type: "number", placeholder: "用户数据过大时,请进行拆分防止内存警告⚠️", desc: "值为数字,拆分段数比如 2 就拆分成两个 datas." }, { id: "@gist.revision_id", type: "modalSelects", name: "历史版本RevisionId", desc: "不填写时,默认获取最新,恢复后会自动清空。选择无内容时,请运行上方更新历史版本", items: "@gist.revision_options" }, { id: "@gist.backup_type", name: "备份/恢复内容", val: "usercfgs,datas,sessions,curSessions,backups,appSubCaches", type: "checkboxes", items: [ { key: "usercfgs", label: "用户偏好" }, { key: "datas", label: "用户数据" }, { key: "sessions", label: "应用会话" }, { key: "curSessions", label: "当前会话" }, { key: "backups", label: "备份索引" }, { key: "appSubCaches", label: "应用订阅缓存" } ] }, { id: "@gist.username", name: "用户名", val: null, type: "text", placeholder: "github 用户名", desc: "必填" }, { id: "@gist.token", name: "Personal access tokens", val: null, type: "text", placeholder: "github personal access tokens", desc: "必填" } ] } ] return sysapps } /** * 获取用户配置 */ function getUserCfgs() { const defcfgs = { gist_cache_key: [], favapps: [], appsubs: [], viewkeys: [], isPinedSearchBar: true, httpapi: 'examplekey@127.0.0.1:6166', http_backend: '' } const usercfgs = Object.assign(defcfgs, $.getjson($.KEY_usercfgs, {})) // 处理异常数据:删除所有为 null 的订阅 if (usercfgs.appsubs.includes(null)) { usercfgs.appsubs = usercfgs.appsubs.filter((sub) => sub) $.setjson(usercfgs, $.KEY_usercfgs) } return usercfgs } /** * 获取`应用订阅`缓存 */ function getAppSubCaches() { return $.getjson($.KEY_app_subCaches, {}) } /** * 获取全局备份列表 */ function getGlobalBaks() { let backups = $.getjson($.KEY_backups, []) // 处理异常数据:删除所有为 null 的备份 if (backups.includes(null)) { backups = backups.filter((bak) => bak) $.setjson(backups, $.KEY_backups) } return backups } /** * 获取版本清单 */ function getVersions() { return $.http.get($.ver).then( (resp) => { try { $.json = $.toObj(resp.body) } catch { $.json = {} } }, () => ($.json = {}) ) } /** * 获取应用会话 */ function getAppSessions() { return $.getjson($.KEY_sessions, []) || [] } /** * 获取当前切换到哪个会话 */ function getCurSessions() { return $.getjson($.KEY_cursessions, {}) || {} } /** * =================================== * 接口类函数 * =================================== */ function getAppDatas(app) { const datas = {} const nulls = [null, undefined, 'null', 'undefined'] if (app.keys && Array.isArray(app.keys)) { app.keys.forEach((key) => { const val = $.getdata(key) datas[key] = nulls.includes(val) ? null : val }) } if (app.settings && Array.isArray(app.settings)) { app.settings.forEach((setting) => { const key = setting.id const val = $.getdata(key) datas[key] = nulls.includes(val) ? null : val }) } return datas } function dealKey(str) { const [rootKey, delIndex] = str.split('.') if (rootKey && rootKey.indexOf('@') > -1 && delIndex !== undefined) { const key = rootKey.replace('@', '') const datas = JSON.parse($.getdata(key)) if (Array.isArray(datas) && delIndex <= datas.length - 1) { datas.splice(delIndex, 1) $.setdata(JSON.stringify(datas), key) } } } async function apiSave() { const data = $.toObj($request.body) if (Array.isArray(data)) { data.forEach((dat) => { if (dat.val === null) { dealKey(dat.key) } else { $.setdata(dat.val, dat.key) } }) } else { if (data.val === null) { dealKey(data.key) } else { $.setdata(data.val, data.key) } } const appId = $.queries['appid'] if (appId) { updateCurSesssions(appId, data) } $.json = getBoxData() } async function apiUpdate() { const data = $.toObj($request.body) const path = data.path.split('.') const val = data.val const key = path.shift() if (data.val && path.join('.')) { switch (key) { case 'usercfgs': const usercfgs = getUserCfgs() update(usercfgs, path.join('.'), val) $.setjson(usercfgs, $.KEY_usercfgs) break default: break } } $.json = getBoxData() } async function apiAddAppSub() { const sub = $.toObj($request.body) // 添加订阅 const usercfgs = getUserCfgs() usercfgs.appsubs.push(sub) $.setjson(usercfgs, $.KEY_usercfgs) // 加载订阅缓存 await reloadAppSubCache(sub.url) $.json = getBoxData() } async function apiReloadAppSub() { const sub = $.toObj($request.body) if (sub) { await reloadAppSubCache(sub.url) } else { await reloadAppSubCaches() } $.json = getBoxData() } async function apiDelGlobalBak() { const backup = $.toObj($request.body) const backups = $.getjson($.KEY_backups, []) const bakIdx = backups.findIndex((b) => b.id === backup.id) if (bakIdx > -1) { backups.splice(bakIdx, 1) $.setdata('', backup.id) $.setjson(backups, $.KEY_backups) } $.json = getBoxData() } async function apiUpdateGlobalBak() { const { id: backupId, name: backupName } = $.toObj($request.body) const backups = $.getjson($.KEY_backups, []) const backup = backups.find((b) => b.id === backupId) if (backup) { backup.name = backupName $.setjson(backups, $.KEY_backups) } $.json = getBoxData() } async function apiRevertGlobalBak() { const { id: bakcupId } = $.toObj($request.body) const backup = $.getjson(bakcupId) if (backup) { const { chavy_boxjs_sysCfgs, chavy_boxjs_sysApps, chavy_boxjs_sessions, chavy_boxjs_userCfgs, chavy_boxjs_cur_sessions, chavy_boxjs_app_subCaches, ...datas } = backup $.setdata(JSON.stringify(chavy_boxjs_sessions), $.KEY_sessions) $.setdata(JSON.stringify(chavy_boxjs_userCfgs), $.KEY_usercfgs) $.setdata(JSON.stringify(chavy_boxjs_cur_sessions), $.KEY_cursessions) $.setdata(JSON.stringify(chavy_boxjs_app_subCaches), $.KEY_app_subCaches) const isNull = (val) => [undefined, null, 'null', 'undefined', ''].includes(val) Object.keys(datas).forEach((datkey) => $.setdata(isNull(datas[datkey]) ? '' : `${datas[datkey]}`, datkey) ) } const boxdata = getBoxData() $.json = boxdata } async function apiSaveGlobalBak() { const backups = $.getjson($.KEY_backups, []) const boxdata = getBoxData() const backup = $.toObj($request.body) const backupData = {} backupData['chavy_boxjs_userCfgs'] = boxdata.usercfgs backupData['chavy_boxjs_sessions'] = boxdata.sessions backupData['chavy_boxjs_cur_sessions'] = boxdata.curSessions backupData['chavy_boxjs_app_subCaches'] = boxdata.appSubCaches Object.assign(backupData, boxdata.datas) backups.push(backup) $.setjson(backups, $.KEY_backups) $.setjson(backupData, backup.id) $.json = getBoxData() } async function apiImpGlobalBak() { const backups = $.getjson($.KEY_backups, []) const backup = $.toObj($request.body) const backupData = backup.bak delete backup.bak backups.push(backup) $.setjson(backups, $.KEY_backups) $.setjson(backupData, backup.id) $.json = getBoxData() } async function apiRunScript() { // 取消勿扰模式 $.isMute = false const opts = $.toObj($request.body) const httpapi = $.getdata('@chavy_boxjs_userCfgs.httpapi') const ishttpapi = /.*?@.*?:[0-9]+/.test(httpapi) let script_text = null if (opts.isRemote) { await $.getScript(opts.url).then((script) => (script_text = script)) } else { script_text = opts.script } if (opts.argument) { script_text = `globalThis.$argument=\`${opts.argument}\`;${script_text}` } if ( $.isSurge() && !$.isLoon() && !$.isShadowrocket() && !$.isStash() && ishttpapi ) { const runOpts = { timeout: opts.timeout } await $.runScript(script_text, runOpts).then( (resp) => ($.json = JSON.parse(resp)) ) } else { const result = await new Promise((resolve) => { $eval_env.resolve = resolve // 避免被执行脚本误认为是 rewrite 环境 // 所以需要 `$request = undefined` $eval_env.request = $request $request = undefined // 重写 console.log, 把日志记录到 $eval_env.cached_logs $eval_env.cached_logs = [] console.cloned_log = console.log console.log = (l) => { console.cloned_log(l) $eval_env.cached_logs.push(l) } // 重写脚本内的 $done, 调用 $done() 即是调用 $eval_env.resolve() script_text = script_text.replace(/\$done/g, '$eval_env.resolve') script_text = script_text.replace(/\$\.done/g, '$eval_env.resolve') try { eval(script_text) } catch (e) { $eval_env.cached_logs.push(e) resolve() } }) // 还原 console.log console.log = console.cloned_log // 还原 $request $request = $eval_env.request // 返回数据 $.json = { result, output: $eval_env.cached_logs.join('\n') } } } async function apiSurge() { const opts = $.toObj($request.body) const httpapi = $.getdata('@chavy_boxjs_userCfgs.httpapi') const ishttpapi = /.*?@.*?:[0-9]+/.test(httpapi) if ( $.isSurge() && !$.isLoon() && !$.isShadowrocket() && !$.isStash() && ishttpapi ) { const [key, prefix] = httpapi.split('@') opts.url = `http://${prefix}/${opts.url}` opts.headers = { 'X-Key': key, 'Accept': 'application/json, text/plain, */*' } await new Promise((resolve) => { $[opts.method.toLowerCase()](opts, (_, __, resp) => { $.json = JSON.parse(resp) resolve($.json) }) }) } } async function apiSaveData() { const { key: dataKey, val: dataVal } = $.toObj($request.body) $.setdata(dataVal, dataKey) $.json = { key: dataKey, val: $.getdata(dataKey) } } /** * =================================== * 工具类函数 * =================================== */ function reloadAppSubCache(url) { // 地址后面拼时间缀, 避免 GET 缓存 const requrl = `${url}${ url.includes('?') ? '&' : '?' }_=${new Date().getTime()}` return $.http.get(requrl).then((resp) => { try { const subcaches = getAppSubCaches() subcaches[url] = $.toObj(resp.body) subcaches[url].updateTime = new Date() // 仅缓存存在 id 的订阅 Object.keys(subcaches).forEach(key => { if (!subcaches[key].hasOwnProperty('id')) { delete subcaches[key] } }) $.setjson(subcaches, $.KEY_app_subCaches) $.log(`更新订阅, 成功! ${url}`) } catch (e) { $.logErr(e) $.log(`更新订阅, 失败! ${url}`) } }) } function update(obj, path, value) { const keys = path.split('.') let current = obj for (let i = 0; i < keys.length - 1; i++) { if (!current[keys[i]]) { current[keys[i]] = {} } current = current[keys[i]] } current[keys[keys.length - 1]] = value } async function reloadAppSubCaches() { $.msg($.name, '更新订阅: 开始!') const reloadActs = [] const usercfgs = getUserCfgs() usercfgs.appsubs.forEach((sub) => { reloadActs.push(reloadAppSubCache(sub.url)) }) await Promise.all(reloadActs) $.log(`全部订阅, 完成!`) const endTime = new Date().getTime() const costTime = (endTime - $.startTime) / 1000 $.msg($.name, `更新订阅: 完成! 🕛 ${costTime} 秒`) } function upgradeUserData() { const usercfgs = getUserCfgs() // 如果存在`usercfgs.appsubCaches`则需要升级数据 const isNeedUpgrade = !!usercfgs.appsubCaches if (isNeedUpgrade) { // 迁移订阅缓存至独立的持久化空间 $.setjson(usercfgs.appsubCaches, $.KEY_app_subCaches) // 移除用户偏好中的订阅缓存 delete usercfgs.appsubCaches usercfgs.appsubs.forEach((sub) => { delete sub._raw delete sub.apps delete sub.isErr delete sub.updateTime }) } if (isNeedUpgrade) { $.setjson(usercfgs, $.KEY_usercfgs) } } /** * 升级备份数据 * * 升级前: 把所有备份都存到一个持久化空间 * 升级后: 把每个备份都独立存到一个空间, `$.KEY_backups` 仅记录必要的数据索引 */ function upgradeGlobalBaks() { let oldbaks = $.getdata($.KEY_globalBaks) let newbaks = $.getjson($.KEY_backups, []) const isEmpty = (bak) => [undefined, null, ''].includes(bak) const isExistsInNew = (backupId) => newbaks.find((bak) => bak.id === backupId) // 存在旧备份数据时, 升级备份数据格式 if (!isEmpty(oldbaks)) { oldbaks = JSON.parse(oldbaks) oldbaks.forEach((bak) => { if (isEmpty(bak)) return if (isEmpty(bak.bak)) return if (isExistsInNew(bak.id)) return console.log(`正在迁移: ${bak.name}`) const backupId = bak.id const backupData = bak.bak // 删除旧的备份数据, 仅保留索引信息 delete bak.bak newbaks.push(bak) // 提取旧备份数据, 存入独立的持久化空间 $.setjson(backupData, backupId) }) $.setjson(newbaks, $.KEY_backups) } // 清空所有旧备份的数据 $.setdata('', $.KEY_globalBaks) } function updateCurSesssions(appId, data) { if (!appId) { console.log(`[updateCurSesssions] 跳过! 没有指定 appId!`) return } const curSessions = getCurSessions() const curSessionId = curSessions[appId] if (!curSessionId) { console.log( `[updateCurSesssions] 跳过! 应用 [${appId}] 找不到当前会话, 请先应用会话!` ) return } const sessions = getAppSessions() const session = sessions.find((session) => session.id === curSessionId) if (!session) { console.log( `[updateCurSesssions] 跳过! 应用 [${appId}] 找不到当前会话, 请先应用会话!` ) return } session.datas = data $.setjson(sessions, $.KEY_sessions) } /** * =================================== * 结束类函数 * =================================== */ function doneBox() { // 记录当前使用哪个域名访问 $.setdata(getHost($request.url), $.KEY_boxjs_host) if ($.isOptions) doneOptions() else if ($.isPage) donePage() else if ($.isQuery) doneQuery() else if ($.isApi) doneApi() else $.done() } function 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 ) } function getHtmlDoneHeaders() { return getBaseDoneHeaders({ 'Content-Type': 'text/html;charset=UTF-8' }) } function getJsonDoneHeaders() { return getBaseDoneHeaders({ 'Content-Type': 'application/json; charset=utf-8' }) } function doneOptions() { const headers = getBaseDoneHeaders() if ($.isQuanX()) $.done({ headers }) else $.done({ response: { headers } }) } function donePage() { const headers = getHtmlDoneHeaders() if ($.isQuanX()) $.done({ status: 'HTTP/1.1 200', headers, body: $.html }) else $.done({ response: { status: 200, headers, body: $.html } }) } function doneQuery() { $.json = $.toStr($.json) const headers = getJsonDoneHeaders() if ($.isQuanX()) $.done({ status: 'HTTP/1.1 200', headers, body: $.json }) else $.done({ response: { status: 200, headers, body: $.json } }) } function doneApi() { $.json = $.toStr($.json) const headers = getJsonDoneHeaders() if ($.isQuanX()) $.done({ status: 'HTTP/1.1 200', headers, body: $.json }) else $.done({ response: { status: 200, headers, body: $.json } }) } /** * EnvJs */ // prettier-ignore function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;"POST"===e&&(s=this.post);const i=new Promise(((e,i)=>{s.call(this,t,((t,s,o)=>{t?i(t):e(s)}))}));return t.timeout?((t,e=1e3)=>Promise.race([t,new Promise(((t,s)=>{setTimeout((()=>{s(new Error("请求超时"))}),e)}))]))(i,t.timeout):i}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.logLevels={debug:0,info:1,warn:2,error:3},this.logLevelPrefixs={debug:"[DEBUG] ",info:"[INFO] ",warn:"[WARN] ",error:"[ERROR] "},this.logLevel="info",this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`🔔${this.name}, 开始!`)}getEnv(){return"undefined"!=typeof $environment&&$environment["surge-version"]?"Surge":"undefined"!=typeof $environment&&$environment["stash-version"]?"Stash":"undefined"!=typeof module&&module.exports?"Node.js":"undefined"!=typeof $task?"Quantumult X":"undefined"!=typeof $loon?"Loon":"undefined"!=typeof $rocket?"Shadowrocket":void 0}isNode(){return"Node.js"===this.getEnv()}isQuanX(){return"Quantumult X"===this.getEnv()}isSurge(){return"Surge"===this.getEnv()}isLoon(){return"Loon"===this.getEnv()}isShadowrocket(){return"Shadowrocket"===this.getEnv()}isStash(){return"Stash"===this.getEnv()}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null,...s){try{return JSON.stringify(t,...s)}catch{return e}}getjson(t,e){let s=e;if(this.getdata(t))try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise((e=>{this.get({url:t},((t,s,i)=>e(i)))}))}runScript(t,e){return new Promise((s=>{let i=this.getdata("@chavy_boxjs_userCfgs.httpapi");i=i?i.replace(/\n/g,"").trim():i;let o=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");o=o?1*o:20,o=e&&e.timeout?e.timeout:o;const[r,a]=i.split("@"),n={url:`http://${a}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:o},headers:{"X-Key":r,Accept:"*/*"},policy:"DIRECT",timeout:o};this.post(n,((t,e,i)=>s(i)))})).catch((t=>this.logErr(t)))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e);if(!s&&!i)return{};{const i=s?t:e;try{return JSON.parse(this.fs.readFileSync(i))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e),o=JSON.stringify(this.data);s?this.fs.writeFileSync(t,o):i?this.fs.writeFileSync(e,o):this.fs.writeFileSync(t,o)}}lodash_get(t,e,s){const i=e.replace(/\[(\d+)\]/g,".$1").split(".");let o=t;for(const t of i)if(o=Object(o)[t],void 0===o)return s;return o}lodash_set(t,e,s){return Object(t)!==t||(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce(((t,s,i)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[i+1])>>0==+e[i+1]?[]:{}),t)[e[e.length-1]]=s),t}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,i]=/^@(.*?)\.(.*?)$/.exec(t),o=s?this.getval(s):"";if(o)try{const t=JSON.parse(o);e=t?this.lodash_get(t,i,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,i,o]=/^@(.*?)\.(.*?)$/.exec(e),r=this.getval(i),a=i?"null"===r?null:r||"{}":"{}";try{const e=JSON.parse(a);this.lodash_set(e,o,t),s=this.setval(JSON.stringify(e),i)}catch(e){const r={};this.lodash_set(r,o,t),s=this.setval(JSON.stringify(r),i)}}else s=this.setval(t,e);return s}getval(t){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.read(t);case"Quantumult X":return $prefs.valueForKey(t);case"Node.js":return this.data=this.loaddata(),this.data[t];default:return this.data&&this.data[t]||null}}setval(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.write(t,e);case"Quantumult X":return $prefs.setValueForKey(t,e);case"Node.js":return this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0;default:return this.data&&this.data[e]||null}}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.cookie&&void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar)))}get(t,e=(()=>{})){switch(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"],delete t.headers["content-type"],delete t.headers["content-length"]),t.params&&(t.url+="?"+this.queryStr(t.params)),void 0===t.followRedirect||t.followRedirect||((this.isSurge()||this.isLoon())&&(t["auto-redirect"]=!1),this.isQuanX()&&(t.opts?t.opts.redirection=!1:t.opts={redirection:!1})),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,((t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,i)}));break;case"Quantumult X":this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then((t=>{const{statusCode:s,statusCode:i,headers:o,body:r,bodyBytes:a}=t;e(null,{status:s,statusCode:i,headers:o,body:r,bodyBytes:a},r,a)}),(t=>e(t&&t.error||"UndefinedError")));break;case"Node.js":let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",((t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}})).then((t=>{const{statusCode:i,statusCode:o,headers:r,rawBody:a}=t,n=s.decode(a,this.encoding);e(null,{status:i,statusCode:o,headers:r,rawBody:a,body:n},n)}),(t=>{const{message:i,response:o}=t;e(i,o,o&&s.decode(o.rawBody,this.encoding))}));break}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";switch(t.body&&t.headers&&!t.headers["Content-Type"]&&!t.headers["content-type"]&&(t.headers["content-type"]="application/x-www-form-urlencoded"),t.headers&&(delete t.headers["Content-Length"],delete t.headers["content-length"]),void 0===t.followRedirect||t.followRedirect||((this.isSurge()||this.isLoon())&&(t["auto-redirect"]=!1),this.isQuanX()&&(t.opts?t.opts.redirection=!1:t.opts={redirection:!1})),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,((t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,i)}));break;case"Quantumult X":t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then((t=>{const{statusCode:s,statusCode:i,headers:o,body:r,bodyBytes:a}=t;e(null,{status:s,statusCode:i,headers:o,body:r,bodyBytes:a},r,a)}),(t=>e(t&&t.error||"UndefinedError")));break;case"Node.js":let i=require("iconv-lite");this.initGotEnv(t);const{url:o,...r}=t;this.got[s](o,r).then((t=>{const{statusCode:s,statusCode:o,headers:r,rawBody:a}=t,n=i.decode(a,this.encoding);e(null,{status:s,statusCode:o,headers:r,rawBody:a,body:n},n)}),(t=>{const{message:s,response:o}=t;e(s,o,o&&i.decode(o.rawBody,this.encoding))}));break}}time(t,e=null){const s=e?new Date(e):new Date;let i={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in i)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?i[e]:("00"+i[e]).substr((""+i[e]).length)));return t}queryStr(t){let e="";for(const s in t){let i=t[s];null!=i&&""!==i&&("object"==typeof i&&(i=JSON.stringify(i)),e+=`${s}=${i}&`)}return e=e.substring(0,e.length-1),e}msg(e=t,s="",i="",o={}){const r=t=>{const{$open:e,$copy:s,$media:i,$mediaMime:o}=t;switch(typeof t){case void 0:return t;case"string":switch(this.getEnv()){case"Surge":case"Stash":default:return{url:t};case"Loon":case"Shadowrocket":return t;case"Quantumult X":return{"open-url":t};case"Node.js":return}case"object":switch(this.getEnv()){case"Surge":case"Stash":case"Shadowrocket":default:{const r={};let a=t.openUrl||t.url||t["open-url"]||e;a&&Object.assign(r,{action:"open-url",url:a});let n=t["update-pasteboard"]||t.updatePasteboard||s;if(n&&Object.assign(r,{action:"clipboard",text:n}),i){let t,e,s;if(i.startsWith("http"))t=i;else if(i.startsWith("data:")){const[t]=i.split(";"),[,o]=i.split(",");e=o,s=t.replace("data:","")}else{e=i,s=(t=>{const e={JVBERi0:"application/pdf",R0lGODdh:"image/gif",R0lGODlh:"image/gif",iVBORw0KGgo:"image/png","/9j/":"image/jpg"};for(var s in e)if(0===t.indexOf(s))return e[s];return null})(i)}Object.assign(r,{"media-url":t,"media-base64":e,"media-base64-mime":o??s})}return Object.assign(r,{"auto-dismiss":t["auto-dismiss"],sound:t.sound}),r}case"Loon":{const s={};let o=t.openUrl||t.url||t["open-url"]||e;o&&Object.assign(s,{openUrl:o});let r=t.mediaUrl||t["media-url"];return i?.startsWith("http")&&(r=i),r&&Object.assign(s,{mediaUrl:r}),console.log(JSON.stringify(s)),s}case"Quantumult X":{const o={};let r=t["open-url"]||t.url||t.openUrl||e;r&&Object.assign(o,{"open-url":r});let a=t["media-url"]||t.mediaUrl;i?.startsWith("http")&&(a=i),a&&Object.assign(o,{"media-url":a});let n=t["update-pasteboard"]||t.updatePasteboard||s;return n&&Object.assign(o,{"update-pasteboard":n}),console.log(JSON.stringify(o)),o}case"Node.js":return}default:return}};if(!this.isMute)switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:$notification.post(e,s,i,r(o));break;case"Quantumult X":$notify(e,s,i,r(o));break;case"Node.js":break}if(!this.isMuteLog){let t=["","==============📣系统通知📣=============="];t.push(e),s&&t.push(s),i&&t.push(i),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}debug(...t){this.logLevels[this.logLevel]<=this.logLevels.debug&&(t.length>0&&(this.logs=[...this.logs,...t]),console.log(`${this.logLevelPrefixs.debug}${t.map((t=>t??String(t))).join(this.logSeparator)}`))}info(...t){this.logLevels[this.logLevel]<=this.logLevels.info&&(t.length>0&&(this.logs=[...this.logs,...t]),console.log(`${this.logLevelPrefixs.info}${t.map((t=>t??String(t))).join(this.logSeparator)}`))}warn(...t){this.logLevels[this.logLevel]<=this.logLevels.warn&&(t.length>0&&(this.logs=[...this.logs,...t]),console.log(`${this.logLevelPrefixs.warn}${t.map((t=>t??String(t))).join(this.logSeparator)}`))}error(...t){this.logLevels[this.logLevel]<=this.logLevels.error&&(t.length>0&&(this.logs=[...this.logs,...t]),console.log(`${this.logLevelPrefixs.error}${t.map((t=>t??String(t))).join(this.logSeparator)}`))}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.map((t=>t??String(t))).join(this.logSeparator))}logErr(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:this.log("",`❗️${this.name}, 错误!`,e,t);break;case"Node.js":this.log("",`❗️${this.name}, 错误!`,e,void 0!==t.message?t.message:t,t.stack);break}}wait(t){return new Promise((e=>setTimeout(e,t)))}done(t={}){const e=((new Date).getTime()-this.startTime)/1e3;switch(this.log("",`🔔${this.name}, 结束! 🕛 ${e} 秒`),this.log(),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:$done(t);break;case"Node.js":process.exit(1)}}}(t,e)}