// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: deep-gray; icon-glyph: code-branch; // // 「小件件」 // 开发环境,用于小组件调用 // https://x.im3x.cn // https://github.com/im3x/Scriptables // // 组件基础类 const RUNTIME_VERSION = 20201209 class Base { constructor (arg="") { this.arg = arg this._actions = {} this.init() } init (widgetFamily = config.widgetFamily) { // 组件大小:small,medium,large this.widgetFamily = widgetFamily // 系统设置的key,这里分为三个类型: // 1. 全局 // 2. 不同尺寸的小组件 // 3. 不同尺寸+小组件自定义的参数 // 当没有key2时,获取key1,没有key1获取全局key的设置 // this.SETTING_KEY = this.md5(Script.name()+'@'+this.widgetFamily+"@"+this.arg) // this.SETTING_KEY1 = this.md5(Script.name()+'@'+this.widgetFamily) this.SETTING_KEY = this.md5(Script.name()) // 文件管理器 // 提示:缓存数据不要用这个操作,这个是操作源码目录的,缓存建议存放在local temp目录中 this.FILE_MGR = FileManager[module.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local']() // 本地,用于存储图片等 this.FILE_MGR_LOCAL = FileManager.local() this.BACKGROUND_KEY = this.FILE_MGR_LOCAL.joinPath(this.FILE_MGR_LOCAL.documentsDirectory(), `bg_${this.SETTING_KEY}.jpg`) // this.BACKGROUND_KEY1 = this.FILE_MGR_LOCAL.joinPath(this.FILE_MGR_LOCAL.documentsDirectory(), `bg_${this.SETTING_KEY1}.jpg`) // this.BACKGROUND_KEY2 = this.FILE_MGR_LOCAL.joinPath(this.FILE_MGR_LOCAL.documentsDirectory(), `bg_${this.SETTING_KEY2}.jpg`) // // 插件设置 this.settings = this.getSettings() } /** * 注册点击操作菜单 * @param {string} name 操作函数名 * @param {func} func 点击后执行的函数 */ registerAction (name, func) { this._actions[name] = func.bind(this) } /** * 生成操作回调URL,点击后执行本脚本,并触发相应操作 * @param {string} name 操作的名称 * @param {string} data 传递的数据 */ actionUrl (name = '', data = '') { let u = URLScheme.forRunningScript() let q = `act=${encodeURIComponent(name)}&data=${encodeURIComponent(data)}&__arg=${encodeURIComponent(this.arg)}&__size=${this.widgetFamily}` let result = '' if (u.includes('run?')) { result = `${u}&${q}` } else { result = `${u}?${q}` } return result } /** * base64 编码字符串 * @param {string} str 要编码的字符串 */ base64Encode (str) { const data = Data.fromString(str) return data.toBase64String() } /** * base64解码数据 返回字符串 * @param {string} b64 base64编码的数据 */ base64Decode (b64) { const data = Data.fromBase64String(b64) return data.toRawString() } /** * md5 加密字符串 * @param {string} str 要加密成md5的数据 */ md5 (str) { function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u;n[t>>5]|=128<>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h>5]>>>e%32&255);return t}function h(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e>5]|=(255&n.charCodeAt(e/8))<>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16>shg_sum;pixels[yi+1]=(g_sum*mul_sum)>>shg_sum;pixels[yi+2]=(b_sum*mul_sum)>>shg_sum;r_sum-=r_out_sum;g_sum-=g_out_sum;b_sum-=b_out_sum;r_out_sum-=stackIn.r;g_out_sum-=stackIn.g;b_out_sum-=stackIn.b;p=(yw+((p=x+radius+1)>shg_sum;pixels[p+1]=(g_sum*mul_sum)>>shg_sum;pixels[p+2]=(b_sum*mul_sum)>>shg_sum;r_sum-=r_out_sum;g_sum-=g_out_sum;b_sum-=b_out_sum;r_out_sum-=stackIn.r;g_out_sum-=stackIn.g;b_out_sum-=stackIn.b;p=(x+(((p=y+radiusPlus1) 0.5 ? d / (2 - max - min) : d / (max + min); switch(max){ case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [h, s, l]; } function hslToRgb(h, s, l){ var r, g, b; if(s == 0){ r = g = b = l; // achromatic }else{ var hue2rgb = function hue2rgb(p, q, t){ if(t < 0) t += 1; if(t > 1) t -= 1; if(t < 1/6) return p + (q - p) * 6 * t; if(t < 1/2) return q; if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } function lightBlur(hsl) { // Adjust the luminance. let lumCalc = 0.35 + (0.3 / hsl[2]); if (lumCalc < 1) { lumCalc = 1; } else if (lumCalc > 3.3) { lumCalc = 3.3; } const l = hsl[2] * lumCalc; // Adjust the saturation. const colorful = 2 * hsl[1] * l; const s = hsl[1] * colorful * 1.5; return [hsl[0],s,l]; } function darkBlur(hsl) { // Adjust the saturation. const colorful = 2 * hsl[1] * hsl[2]; const s = hsl[1] * (1 - hsl[2]) * 3; return [hsl[0],s,hsl[2]]; } // Set up the canvas. const img = document.getElementById("blurImg"); const canvas = document.getElementById("mainCanvas"); const w = img.naturalWidth; const h = img.naturalHeight; canvas.style.width = w + "px"; canvas.style.height = h + "px"; canvas.width = w; canvas.height = h; const context = canvas.getContext("2d"); context.clearRect( 0, 0, w, h ); context.drawImage( img, 0, 0 ); // Get the image data from the context. var imageData = context.getImageData(0,0,w,h); var pix = imageData.data; var isDark = "${style}" == "dark"; var imageFunc = isDark ? darkBlur : lightBlur; for (let i=0; i < pix.length; i+=4) { // Convert to HSL. let hsl = rgbToHsl(pix[i],pix[i+1],pix[i+2]); // Apply the image function. hsl = imageFunc(hsl); // Convert back to RGB. const rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); // Put the values back into the data. pix[i] = rgb[0]; pix[i+1] = rgb[1]; pix[i+2] = rgb[2]; } // Draw over the old image. context.putImageData(imageData,0,0); // Blur the image. stackBlurCanvasRGB("mainCanvas", 0, 0, w, h, ${blur}); // Perform the additional processing for dark images. if (isDark) { // Draw the hard light box over it. context.globalCompositeOperation = "hard-light"; context.fillStyle = "rgba(55,55,55,0.2)"; context.fillRect(0, 0, w, h); // Draw the soft light box over it. context.globalCompositeOperation = "soft-light"; context.fillStyle = "rgba(55,55,55,1)"; context.fillRect(0, 0, w, h); // Draw the regular box over it. context.globalCompositeOperation = "source-over"; context.fillStyle = "rgba(55,55,55,0.4)"; context.fillRect(0, 0, w, h); // Otherwise process light images. } else { context.fillStyle = "rgba(255,255,255,0.4)"; context.fillRect(0, 0, w, h); } // Return a base64 representation. canvas.toDataURL(); ` // Convert the images and create the HTML. let blurImgData = Data.fromPNG(img).toBase64String() let html = ` ` // Make the web view and get its return value. let view = new WebView() await view.loadHTML(html) let returnValue = await view.evaluateJavaScript(js) // Remove the data type from the string and convert to data. let imageDataString = returnValue.slice(22) let imageData = Data.fromBase64String(imageDataString) // Convert to image and crop before returning. let imageFromData = Image.fromData(imageData) // return cropImage(imageFromData) return imageFromData } // Pixel sizes and positions for widgets on all supported phones. function phoneSizes() { let phones = { // 12 and 12 Pro "2532": { small: 474, medium: 1014, large: 1062, left: 78, right: 618, top: 231, middle: 819, bottom: 1407 }, // 11 Pro Max, XS Max "2688": { small: 507, medium: 1080, large: 1137, left: 81, right: 654, top: 228, middle: 858, bottom: 1488 }, // 11, XR "1792": { small: 338, medium: 720, large: 758, left: 54, right: 436, top: 160, middle: 580, bottom: 1000 }, // 11 Pro, XS, X "2436": { small: 465, medium: 987, large: 1035, left: 69, right: 591, top: 213, middle: 783, bottom: 1353 }, // Plus phones "2208": { small: 471, medium: 1044, large: 1071, left: 99, right: 672, top: 114, middle: 696, bottom: 1278 }, // SE2 and 6/6S/7/8 "1334": { small: 296, medium: 642, large: 648, left: 54, right: 400, top: 60, middle: 412, bottom: 764 }, // SE1 "1136": { small: 282, medium: 584, large: 622, left: 30, right: 332, top: 59, middle: 399, bottom: 399 }, // 11 and XR in Display Zoom mode "1624": { small: 310, medium: 658, large: 690, left: 46, right: 394, top: 142, middle: 522, bottom: 902 }, // Plus in Display Zoom mode "2001" : { small: 444, medium: 963, large: 972, left: 81, right: 600, top: 90, middle: 618, bottom: 1146 } } return phones } var message message = title || "开始之前,请先前往桌面,截取空白界面的截图。然后回来继续" let exitOptions = ["我已截图","前去截图 >"] let shouldExit = await generateAlert(message,exitOptions) if (shouldExit) return // Get screenshot and determine phone size. let img = await Photos.fromLibrary() let height = img.size.height let phone = phoneSizes()[height] if (!phone) { message = "好像您选择的照片不是正确的截图,或者您的机型我们暂时不支持。点击确定前往社区讨论" let _id = await generateAlert(message,["帮助", "取消"]) if (_id===0) Safari.openInApp('https://support.qq.com/products/287371', false) return } // Prompt for widget size and position. message = "截图中要设置透明背景组件的尺寸类型是?" let sizes = ["小尺寸","中尺寸","大尺寸"] let size = await generateAlert(message,sizes) let widgetSize = sizes[size] message = "要设置透明背景的小组件在哪个位置?" message += (height == 1136 ? " (备注:当前设备只支持两行小组件,所以下边选项中的「中间」和「底部」的选项是一致的)" : "") // Determine image crop based on phone size. let crop = { w: "", h: "", x: "", y: "" } if (widgetSize == "小尺寸") { crop.w = phone.small crop.h = phone.small let positions = ["左上角","右上角","中间左","中间右","左下角","右下角"] let _posotions = ["Top left","Top right","Middle left","Middle right","Bottom left","Bottom right"] let position = await generateAlert(message,positions) // Convert the two words into two keys for the phone size dictionary. let keys = _posotions[position].toLowerCase().split(' ') crop.y = phone[keys[0]] crop.x = phone[keys[1]] } else if (widgetSize == "中尺寸") { crop.w = phone.medium crop.h = phone.small // Medium and large widgets have a fixed x-value. crop.x = phone.left let positions = ["顶部","中间","底部"] let _positions = ["Top","Middle","Bottom"] let position = await generateAlert(message,positions) let key = _positions[position].toLowerCase() crop.y = phone[key] } else if(widgetSize == "大尺寸") { crop.w = phone.medium crop.h = phone.large crop.x = phone.left let positions = ["顶部","底部"] let position = await generateAlert(message,positions) // Large widgets at the bottom have the "middle" y-value. crop.y = position ? phone.middle : phone.top } // 透明/模糊选项 message = "需要给背景图片加什么显示效果?" let blurOptions = ["透明", "白色 模糊", "黑色 模糊"] let blurred = await generateAlert(message, blurOptions) // Crop image and finalize the widget. if (blurred) { const style = (blurred === 1) ? 'light' : 'dark' img = await blurImage(img, style) } let imgCrop = cropImage(img, new Rect(crop.x,crop.y,crop.w,crop.h)) return imgCrop } /** * 弹出一个通知 * @param {string} title 通知标题 * @param {string} body 通知内容 * @param {string} url 点击后打开的URL */ async notify (title, body, url, opts = {}) { let n = new Notification() n = Object.assign(n, opts); n.title = title n.body = body if (url) n.openURL = url return await n.schedule() } /** * 给图片加一层半透明遮罩 * @param {Image} img 要处理的图片 * @param {string} color 遮罩背景颜色 * @param {float} opacity 透明度 */ async shadowImage (img, color = '#000000', opacity = 0.7) { let ctx = new DrawContext() // 获取图片的尺寸 ctx.size = img.size ctx.drawImageInRect(img, new Rect(0, 0, img.size['width'], img.size['height'])) ctx.setFillColor(new Color(color, opacity)) ctx.fillRect(new Rect(0, 0, img.size['width'], img.size['height'])) let res = await ctx.getImage() return res } /** * 获取当前插件的设置 * @param {boolean} json 是否为json格式 */ getSettings(json=true){ let res=json?{}:"" let cache="" // if (global && Keychain.contains(this.SETTING_KEY2)) { // cache = Keychain.get(this.SETTING_KEY2) // } else if (Keychain.contains(this.SETTING_KEY)) { // cache = Keychain.get(this.SETTING_KEY) // } else if (Keychain.contains(this.SETTING_KEY1)) { // cache = Keychain.get(this.SETTING_KEY1) // } else if (Keychain.contains(this.SETTING_KEY2)){ if (Keychain.contains(this.SETTING_KEY)) { cache= Keychain.get(this.SETTING_KEY) } if (json){ try { res=JSON.parse(cache) } catch (e) {} }else{ res=cache } return res } /** * 存储当前设置 * @param {bool} notify 是否通知提示 */ saveSettings(notify=true){ let res= (typeof this.settings==="object")?JSON.stringify(this.settings):String(this.settings) Keychain.set(this.SETTING_KEY, res) if (notify) this.notify("设置成功","桌面组件稍后将自动刷新") } /** * 获取当前插件是否有自定义背景图片 * @reutrn img | false */ getBackgroundImage () { // 如果有KEY则优先加载,key>key1>key2 // key2是全局 let result = null if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY)) { result = Image.fromFile(this.BACKGROUND_KEY) // } else if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY1)) { // result = Image.fromFile(this.BACKGROUND_KEY1) // } else if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY2)) { // result = Image.fromFile(this.BACKGROUND_KEY2) } return result } /** * 设置当前组件的背景图片 * @param {image} img */ setBackgroundImage (img, notify = true) { if (!img) { // 移除背景 if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY)) { this.FILE_MGR_LOCAL.remove(this.BACKGROUND_KEY) // } else if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY1)) { // this.FILE_MGR_LOCAL.remove(this.BACKGROUND_KEY1) // } else if (this.FILE_MGR_LOCAL.fileExists(this.BACKGROUND_KEY2)) { // this.FILE_MGR_LOCAL.remove(this.BACKGROUND_KEY2) } if (notify) this.notify("移除成功", "小组件背景图片已移除,稍后刷新生效") } else { // 设置背景 // 全部设置一遍, this.FILE_MGR_LOCAL.writeImage(this.BACKGROUND_KEY, img) // this.FILE_MGR_LOCAL.writeImage(this.BACKGROUND_KEY1, img) // this.FILE_MGR_LOCAL.writeImage(this.BACKGROUND_KEY2, img) if (notify) this.notify("设置成功", "小组件背景图片已设置!稍后刷新生效") } } } // @base.end // 运行环境 // @running.start const Running = async (Widget, default_args = "") => { let M = null // 判断hash是否和当前设备匹配 if (config.runsInWidget) { M = new Widget(args.widgetParameter || '') const W = await M.render() Script.setWidget(W) Script.complete() } else { let { act, data, __arg, __size } = args.queryParameters M = new Widget(__arg || default_args || '') if (__size) M.init(__size) if (!act || !M['_actions']) { // 弹出选择菜单 const actions = M['_actions'] const _actions = [ async () => { Safari.openInApp("https://support.qq.com/products/287371", false) } ] const alert = new Alert() alert.title = M.name alert.message = M.desc alert.addAction("反馈交流") for (let _ in actions) { alert.addAction(_) _actions.push(actions[_]) } alert.addCancelAction("取消操作") const idx = await alert.presentSheet() if (_actions[idx]) { const func = _actions[idx] await func() } return } let _tmp = act.split('-').map(_ => _[0].toUpperCase() + _.substr(1)).join('') let _act = `action${_tmp}` if (M[_act] && typeof M[_act] === 'function') { const func = M[_act].bind(M) await func(data) } } } // @running.end // 测试环境 const Testing = async (Widget, default_args = "") => { let M = null // 判断hash是否和当前设备匹配 if (config.runsInWidget) { M = new Widget(args.widgetParameter || '') const W = await M.render() Script.setWidget(W) Script.complete() } else { let { act, data, __arg, __size } = args.queryParameters M = new Widget(__arg || default_args || '') if (__size) M.init(__size) if (!act || !M['_actions']) { // 弹出选择菜单 const actions = M['_actions'] const _actions = [ // 远程开发 async () => { // 1. 获取服务器ip const a = new Alert() a.title = "服务器 IP" a.message = "请输入远程开发服务器(电脑)IP地址" let xjj_debug_server = "192.168.1.3" if (Keychain.contains("xjj_debug_server")) { xjj_debug_server = Keychain.get("xjj_debug_server") } a.addTextField("server-ip", xjj_debug_server) a.addAction("连接") a.addCancelAction("取消") const id = await a.presentAlert() if (id === -1) return const ip = a.textFieldValue(0) // 保存到本地 Keychain.set("xjj_debug_server", ip) const server_api = `http://${ip}:5566` // 2. 发送当前文件到远程服务器 const SELF_FILE = module.filename.replace('「小件件」开发环境', Script.name()) const req = new Request(`${server_api}/sync`) req.method = "POST" req.addFileToMultipart(SELF_FILE, "Widget", Script.name()) try { const res = await req.loadString() if (res !== "ok") { return M.notify("连接失败", res) } } catch (e) { return M.notify("连接错误", e.message) } M.notify("连接成功", "编辑文件后保存即可进行下一步预览操作") // 重写console.log方法,把数据传递到nodejs const rconsole_log = async (data, t = 'log') => { const _req = new Request(`${server_api}/console`) _req.method = "POST" _req.headers = { 'Content-Type': 'application/json' } _req.body = JSON.stringify({ t, data }) return await _req.loadString() } const lconsole_log = console.log.bind(console) const lconsole_warn = console.warn.bind(console) const lconsole_error = console.error.bind(console) console.log = d => { lconsole_log(d) rconsole_log(d, 'log') } console.warn = d => { lconsole_warn(d) rconsole_log(d, 'warn') } console.error = d => { lconsole_error(d) rconsole_log(d, 'error') } // 3. 同步 while (1) { let _res = "" try { const _req = new Request(`${server_api}/sync?name=${encodeURIComponent(Script.name())}`) _res = await _req.loadString() } catch (e) { M.notify("停止调试", "与开发服务器的连接已终止") break } if (_res === "stop") { console.log("[!] 停止同步") break } else if (_res === "no") { // console.log("[-] 没有更新内容") } else if (_res.length > 0) { M.notify("同步成功", "新文件已同步,大小:" + _res.length) // 重新加载组件 // 1. 读取当前源码 const _code = _res.split('// @组件代码开始')[1].split('// @组件代码结束')[0] // 2. 解析 widget class let NewWidget = null try { const _func = new Function(`const _Debugger = Base => {\n${_code}\nreturn Widget\n}\nreturn _Debugger`) NewWidget = _func()(Base) } catch (e) { M.notify("解析失败", e.message) } if (!NewWidget) continue; // 3. 重新执行 widget class delete M; M = new NewWidget(__arg || default_args || '') if (__size) M.init(__size) // 写入文件 FileManager.local().writeString(SELF_FILE, _res) // 执行预览 let i = await _actions[1](true) if (i === (4+Object.keys(actions).length)) break } } }, // 预览组件 async (debug = false) => { let a = new Alert() a.title = "预览组件" a.message = "测试桌面组件在各种尺寸下的显示效果" a.addAction("小尺寸 Small") a.addAction("中尺寸 Medium") a.addAction("大尺寸 Large") a.addAction("全部 All") a.addCancelAction("取消操作") const funcs = [] if (debug) { for (let _ in actions) { a.addAction(_) funcs.push(actions[_].bind(M)) } a.addDestructiveAction("停止调试") } let i = await a.presentSheet() if (i === -1) return let w switch (i) { case 0: M.widgetFamily = 'small' w = await M.render() await w.presentSmall() break; case 1: M.widgetFamily = 'medium' w = await M.render() await w.presentMedium() break case 2: M.widgetFamily = 'large' w = await M.render() await w.presentLarge() break case 3: M.widgetFamily = 'small' w = await M.render() await w.presentSmall() M.widgetFamily = 'medium' w = await M.render() await w.presentMedium() M.widgetFamily = 'large' w = await M.render() await w.presentLarge() break default: const func = funcs[i - 4]; if (func) await func(); break; } return i }, // 复制源码 async () => { const SELF_FILE = module.filename.replace('「小件件」开发环境', Script.name()) const source = FileManager.local().readString(SELF_FILE) Pasteboard.copyString(source) await M.notify("复制成功", "当前脚本的源代码已复制到剪贴板!") }, async () => { Safari.openInApp("https://www.kancloud.cn/im3x/scriptable/content", false) }, async () => { Safari.openInApp("https://support.qq.com/products/287371", false) } ] const alert = new Alert() alert.title = M.name alert.message = M.desc alert.addAction("远程开发") alert.addAction("预览组件") alert.addAction("复制源码") alert.addAction("开发文档") alert.addAction("反馈交流") for (let _ in actions) { alert.addAction(_) _actions.push(actions[_]) } alert.addCancelAction("取消操作") const idx = await alert.presentSheet() if (_actions[idx]) { const func = _actions[idx] await func() } return } let _tmp = act.split('-').map(_ => _[0].toUpperCase() + _.substr(1)).join('') let _act = `action${_tmp}` if (M[_act] && typeof M[_act] === 'function') { const func = M[_act].bind(M) await func(data) } } } module.exports = { Base, Testing, Running, } // 自更新 // 流程: // 1. 获取远程gitee仓库的本文件代码 // 2. 对比sha,如果和本地存储的不一致,则下载 // 3. 下载保存,存储sha // 4. 更新时间为每小时一次 // ;(async () => { const UPDATE_KEY = "XJJ_UPDATE_AT" let UPDATED_AT = 0 const UPDATE_FILE = '「小件件」开发环境.js' const FILE_MGR = FileManager[module.filename.includes('Documents/iCloud~') ? 'iCloud' : 'local']() if (Keychain.contains(UPDATE_KEY)) { UPDATED_AT = parseInt(Keychain.get(UPDATE_KEY)) } if (UPDATED_AT > (+new Date - 1000*60*60)) return console.warn('[-] 1 小时内已检查过更新') console.log('[*] 检测开发环境是否有更新..') const req = new Request('https://gitee.com/im3x/Scriptables/raw/v2-dev/package.json') const res = await req.loadJSON() console.log(`[+] 远程开发环境版本:${res['runtime_ver']}`) if (res['runtime_ver'] === RUNTIME_VERSION) return console.warn('[-] 远程版本一致,暂无更新') console.log('[+] 开始更新开发环境..') const REMOTE_REQ = new Request('https://gitee.com/im3x/Scriptables/raw/v2-dev/Scripts/%E3%80%8C%E5%B0%8F%E4%BB%B6%E4%BB%B6%E3%80%8D%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83.js') const REMOTE_RES = await REMOTE_REQ.load() FILE_MGR.write(FILE_MGR.joinPath(FILE_MGR.documentsDirectory(), UPDATE_FILE), REMOTE_RES); const n = new Notification() n.title = "更新成功" n.body = "「小件件」开发环境已自动更新!" n.schedule() UPDATED_AT = +new Date Keychain.set(UPDATE_KEY, String(UPDATED_AT)) })()