const fs = require('fs'); const os = require('os'); const child_process = require("child_process") const iconv = require('iconv-lite') const electron = require('electron') const { NodeVM } = require('vm2') const path = require("path") const util = require("util") const PinyinMatch = require('pinyin-match'); const axios = require('axios'); // axios.defaults.adapter = require('axios/lib/adapters/http') if (!utools.isWindows()) process.env.PATH += ':/usr/local/bin:/usr/local/sbin' // window.startTime = new Date().getTime() const shortCodes = [ open = path => { utools.shellOpenItem(path) }, locate = path => { utools.shellShowItemInFolder(path); }, visit = url => { utools.shellOpenExternal(url); }, system = cmd => { child_process.exec(cmd); }, message = msg => { utools.showNotification(msg) }, keyTap = (key, ...modifier) => utools.simulateKeyboardTap(key, ...modifier), copyTo = text => { electron.clipboard.writeText(text) }, send = text => { copyTo(text); quickcommand.simulatePaste(); } ] quickcommand = { // 模拟复制操作 simulateCopy: function() { var ctlKey = utools.isMacOs() ? 'command' : 'control'; utools.simulateKeyboardTap('c', ctlKey); }, // 模拟粘贴操作 simulatePaste: function() { var ctlKey = utools.isMacOs() ? 'command' : 'control'; utools.simulateKeyboardTap('v', ctlKey); }, // setTimout 不能在 vm2 中使用,同时在 electron 中有 bug sleep: function(ms) { var start = new Date().getTime() try { child_process.execSync(getSleepCodeByShell(ms), { timeout: ms }) } catch (ex) { } var end = new Date().getTime() return (end - start) }, // 重写 setTimeout setTimeout: function(callback, ms) { var start = new Date().getTime() child_process.exec(getSleepCodeByShell(ms), { timeout: ms }, (err, stdout, stderr) => { var end = new Date().getTime() callback(end - start) }) }, // 显示输入框 showInputBox: function (placeHolders, title = '') { return new Promise((reslove, reject) => { placeHolders || (placeHolders = [""]) if (!(placeHolders instanceof Array)) return reject(new TypeError(`应为 Array, 而非 ${typeof placeHolders}`)) utools.setExpendHeight(600) var html = "" var inputBoxNumbers = placeHolders.length for (let i = 0; i < inputBoxNumbers; i++) { html += `` } var result = [] var options = { onBeforeOpen: () => { document.getElementById(`inputBox0`).focus() $('.swal2-content').keydown(function (e) { e.which == 13 && swal.clickConfirm() }) $(".output").is(":parent") ? utools.setExpendHeight(600) : modWindowHeight($('.swal2-popup').outerHeight() + 20) }, title: title, html: html, focusConfirm: false, showCancelButton: true, backdrop: utools.isDarkColors() ? '#ffffff26' : '#bbbbbb80', preConfirm: () => { for (let i = 0; i < inputBoxNumbers; i++) { result.push(document.getElementById(`inputBox${i}`).value) } reslove(result) } } swalOneByOne(options) }); }, // 显示选项按钮 showButtonBox: function (buttons, title = '') { return new Promise((reslove, reject) => { if (!(buttons instanceof Array)) return reject(new TypeError(`应为 Array, 而非 ${typeof buttons}`)) utools.setExpendHeight(600) var html = `` var buttonBoxNumbers = buttons.length for (let i = 0; i < buttonBoxNumbers; i++) { html += `` } var options = { onBeforeOpen: () => { clickButton = i => { reslove({ id: i, text: buttons[i] }) swal.clickConfirm() } $(".output").is(":parent") && utools.setExpendHeight(600) || modWindowHeight($('.swal2-popup').outerHeight() + 20) }, html: html, title: title, backdrop: utools.isDarkColors() ? '#ffffff26' : '#bbbbbb80', showConfirmButton: false } swalOneByOne(options) }); }, // 显示自动消失的提示框 showMessageBox: function (title, icon = "success", time = 3000) { var options = { icon: icon, title: title, toast: true, position: 'top', timer: time, showConfirmButton: false, // onBeforeOpen: () => { // setTimeout(() => { Swal.clickConfirm() }, time); // } } swal.fire(options) }, // 显示选项列表 showSelectList: function (selects, opt = {}) { return new Promise((reslove, reject) => { if (!(selects instanceof Array)) return reject(new TypeError(`应为 Array, 而非 ${typeof selects}`)) opt.optionType || (opt.optionType = 'plaintext') typeof opt.placeholder == 'undefined' && (opt.placeholder = "搜索,支持拼音") typeof opt.enableSearch == 'undefined' && (opt.enableSearch = true) if ($('#quickselect').length) $('#quickselect').remove() let cancelButton = opt.showCancelButton ? '' : '' $("body").append(`
${cancelButton}
`) let item, data = [] selects.forEach((s, i) => { item = {} if (opt.optionType == 'json') { item.text = '' Object.keys(s).forEach(k => item[k] = s[k]) item.id = i s.icon && (item.text += `
`) s.title && (item.text += `
${s.title}
`) s.description && (item.text += `
${s.description}
`) } else { item = {id: i, text: s} } data.push(item) }) $('#selectBox').data('options', data) $('#selectBox').data('type', opt.optionType) var prefer = { // data: data, width: "100%", dropdownParent: $("#quickselect"), closeOnSelect: false, // 支持无限滚动 ajax: { transport: (params, success, failure) => { let cont, pageSize = 50 let term = (params.data.term || '').toLowerCase() let page = (params.data.page || 1) let items = $('#selectBox').data('options') let results = items.filter(x => { if (opt.optionType == 'json') cont = x.title else if (opt.optionType == 'html') cont = x.text.replace(/<[^<>]+>/g, '') else cont = x.text return cont.toLowerCase().includes(term) || PinyinMatch.match(cont, term) }) let paged = results.slice((page - 1) * pageSize, page * pageSize) let options = { results: paged, pagination: { more: results.length >= page * pageSize } } success(options) } }, } // 显示html时不转义标签 if (opt.optionType != 'plaintext') prefer.escapeMarkup = markup => markup $('#selectBox').select2(prefer) $('#selectBox').val(null).trigger('change') $('#selectBox').select2('open') $("#quickselect .select2-search__field").focus() $('#quickselect .select2').hide() opt.optionType == 'plaintext' && $('.select2-results').css({'line-height': '40px'}) modWindowHeight($('.select2-results').outerHeight()) opt.enableSearch && utools.setSubInput(({text})=>{ $("#quickselect .select2-search__field").val(text).trigger('input') modWindowHeight($('.select2-results').outerHeight()) }, opt.placeholder) // 关闭列表 let closeSelect = () => { $('#selectBox').off('select2:select') utools.removeSubInput() $("#quickselect").remove() } $('#selectBox').on('select2:select', function (e) { let result = $('#selectBox').data('options')[$(this).val()] delete result.selected closeSelect() reslove(result) }) $('.circleButton').click(() => { closeSelect() reslove(false) }) }); }, // 更新选项列表 updateSelectList: function (opt, id) { if(!$('#selectBox').length) throw '当前没有选择列表, 请结合 quickcommand.showSelectList 使用' let data = $('#selectBox').data('options') let num = data.length typeof id == 'undefined' && (id = num) if (id > num) throw 'id 不能大于当前列表数' let optionType = $('#selectBox').data('type') let item = { id: id } if (optionType == 'json') { item.text = '' if (!(opt instanceof Object)) throw '更新的选项格式与当前的不一致' Object.keys(opt).forEach(k => item[k] = opt[k]) opt.icon && (item.text += `
`) opt.title && (item.text += `
${opt.title}
`) opt.description && (item.text += `
${opt.description}
`) } else { item.text = opt } data[id] && (data[id] = item) || data.push(item) $('#selectBox').data('options', data).val(null).trigger('change') $("#quickselect .select2-search__field").trigger('input') modWindowHeight($('.select2-results').outerHeight()) }, // 显示文本输入框 showTextAera: function (placeholder = "") { return new Promise((reslove, reject) => { utools.setExpendHeight(600) var html = `
` $("body").append(html) $("#quicktextarea").addClass("fadeInUpWindow") $(".circleButton").click(function () { $("#quicktextarea").addClass("fadeOutDownWindow") setTimeout(() => { $("#quicktextarea").remove() }, 300); reslove($("#quicktextarea > textarea").val()) }) }); }, showConfirmBox: async function (title) { let options = { text: title, icon: 'warning', showCancelButton: true, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: '确定!', cancelButtonText: '手抖...' } let result = await Swal.fire(options) if (result.value) return true; }, // 关闭进程 kill: function (pid, signal = 'SIGTERM') { process.kill(pid, signal) }, // dom 解析 htmlParse: function (html) { return new DOMParser().parseFromString(html, 'text/html') }, // 下载文件 downloadFile: function (url, file = {}) { return new Promise((reslove, reject) => { if (file instanceof Object) file = utools.showSaveDialog(JSON.parse(JSON.stringify(file))) axios({ method: 'get', url: url, responseType: 'arraybuffer' }).then(res => { var filebuffer = Buffer.from(res.data) fs.writeFile(file, filebuffer, err => { if (err) reject(err) else reslove(filebuffer) }) }).catch(err => { reject(err) }) }) }, // 上传文件 uploadFile: function(url, file = {}, name = 'file', formData = {}) { return new Promise((reslove, reject) => { var objfile if (file instanceof File) { objfile = file } else { if (file instanceof Object) file = utools.showOpenDialog(JSON.parse(JSON.stringify(file)))[0] if (!fs.existsSync(file)) return reject('文件不存在') var arraybuffer = fs.readFileSync(file).buffer var objfile = new File([arraybuffer], path.basename(file)) } var form = new FormData(); form.append(name, objfile) var keys = Object.keys(formData) if (keys.length) keys.forEach(k => form.append(k, formData[k])) axios.post(url, form, { headers: { 'accept': 'application/json', 'Content-Type': `multipart/form-data; boundary=${formData._boundary}`, } }).then(res => { reslove(res) }).catch(err => { reject(err) }) }) }, // 载入在线资源 loadRemoteScript: async function (url, forceUpdate = false) { if (!/^((ht|f)tps?):\/\/([\w\-]+(\.[\w\-]+)*\/)*[\w\-]+(\.[\w\-]+)*\/?(\?([\w\-\.,@?^=%&:\/~\+#]*)+)?/.test(url)) throw 'url 不合法' let remote = url let root = path.join(os.tmpdir(), 'qcRemoteScript') if (!fs.existsSync(root)) fs.mkdirSync(root) let local = path.join(root, require('crypto').createHash('md5').update(url).digest('hex')) if (forceUpdate || !fs.existsSync(local)) await this.downloadFile(remote, local) return require(local) } } swalOneByOne = options => { swal.getQueueStep() ? Swal.insertQueueStep(options) : Swal.queue([options]) } pluginInfo = () => { return JSON.parse(fs.readFileSync(path.join(__dirname, 'plugin.json'))) } isDev = () => { return /[a-zA-Z0-9\-]+\.asar/.test(__dirname) ? false : true } let GetFilePath = (Path, File) => { if (isDev()) { return path.join(__dirname, Path, File) } else { return path.join(__dirname.replace(/([a-zA-Z0-9\-]+\.asar)/,'$1.unpacked'), Path, File) } } let getSleepCodeByShell = ms => { var cmd, tempFilePath if (utools.isWindows()) { tempFilePath = getQuickCommandScriptFile('vbs') cmd = `echo set ws=CreateObject("Wscript.Shell") > ${tempFilePath} && echo Wscript.sleep ${ms} >> ${tempFilePath} && cscript /nologo ${tempFilePath}` } else { cmd = `sleep ${ms / 1000}` } return cmd } let modWindowHeight = height => { $('#options').is(':hidden') && utools.setExpendHeight(height > 600 ? 600 : height); } // 屏蔽危险函数 getuToolsLite = () => { var utoolsLite = Object.assign({}, utools) delete utoolsLite.db delete utoolsLite.removeFeature delete utoolsLite.setFeature return utoolsLite } let getSandboxFuns = () => { var sandbox = { utools: getuToolsLite(), quickcommand: quickcommand, electron: electron, fs: fs, path: path, os: os, child_process: child_process, util: util, TextDecoder: TextDecoder, TextEncoder: TextEncoder, URL: URL, URLSearchParams: URLSearchParams, axios: axios, alert: alert, confirm: confirm } shortCodes.forEach(f => { sandbox[f.name] = f }) return sandbox } let createNodeVM = (payload = "") => { var sandbox = getSandboxFuns() sandbox.quickcommand.payload = payload const vm = new NodeVM({ require: { external: true, builtin: ["*"], }, console: 'redirect', env: process.env, sandbox: sandbox, }); return vm } let stringifyAll = item => { var cache = []; var string = JSON.stringify(item, (key, value) => { if (typeof value === 'object' && value !== null) { if (cache.indexOf(value) !== -1) return cache.push(value); } return value; }, '\t') if (string != "{}") return string else return item.toString() } let parseItem = item => { if (typeof item == "object") { if (Buffer.isBuffer(item)) { var bufferString = `[Buffer ${item.slice(0, 50).toString('hex').match(/\w{1,2}/g).join(" ")}` if (item.length > 50) bufferString += `... ${(item.length / 1000).toFixed(2)}kb` return bufferString + ']' } else if (item instanceof ArrayBuffer) { return `ArrayBuffer(${item.byteLength})` } else if (item instanceof Blob) { return `Blob {size: ${item.size}, type: "${item.type}"}` } else { try { return stringifyAll(item) } catch (error) { } } } else if (typeof item == "undefined") { return "undefined" } return item.toString() } runCodeInVm = (cmd, cb, payload = "") => { const vm = createNodeVM(payload) //重定向 console vm.on('console.log', stdout => { console.log(stdout); cb(parseItem(stdout), null) }); vm.on('console.error', stderr => { cb(null, stderr.toString()) }); // 错误处理 try { vm.run(cmd, path.join(__dirname, 'preload.js')); } catch (error) { console.log(error) cb(null, error.toString()) } let cbUnhandledError = e => { removeAllListener() console.log(e) cb(null, e.error.toString()) } let cbUnhandledRejection = e => { removeAllListener() console.log(e) cb(null, e.reason.toString()) } let removeAllListener = () => { window.removeEventListener('error', cbUnhandledError) window.removeEventListener('unhandledrejection', cbUnhandledRejection) delete window.isWatchingError } if (!window.isWatchingError) { window.addEventListener('error', cbUnhandledError) window.addEventListener('unhandledrejection', cbUnhandledRejection) window.isWatchingError = true } } // shell代码提示,当前环境变量下的所有命令 getShellCommand = () => { var shellCommands = localStorage['shellCommands'] if (shellCommands) return localStorage['shellCommands'] = '[]' if(utools.isWindows()) return process.env.PATH.split(':').forEach(d => { fs.readdir(d, (err, files) => { if (!err) { var commands = files.filter(x => x[0] != "." || x[0] != '[') localStorage['shellCommands'] = JSON.stringify(JSON.parse(localStorage['shellCommands']).concat(commands)) } }) }) } // cmd代码提示,当前环境变量下的所有命令 getCmdCommand = () => { var cmdCommands = localStorage['cmdCommands'] if (cmdCommands) return localStorage['cmdCommands'] = '[]' if(!utools.isWindows()) return process.env.Path.split(';').forEach(d => { fs.readdir(d, (err, files) => { if (!err) { var commands = [] files.forEach(x => (x.length > 4 && x.slice(-4) == '.exe') && commands.push(x.slice(0, -4))) localStorage['cmdCommands'] = JSON.stringify(JSON.parse(localStorage['cmdCommands']).concat(commands)) } }) }) } // python 代码提示,已安装的模块以及脚本内导入的模块的属性(方法) getPythonMods = () => { var pyModules = localStorage['pyModules'] if (pyModules) return localStorage['pyModules'] = '[]' child_process.exec(`python -c "print(__import__('sys').path)"`, (err, stdout, stderr) => { if (err) return stdout = JSON.parse(stdout.replace(/'/g, `"`)).forEach(s => { fs.readdir(s, (err, m) => { if (!err) { var mods = [] m.forEach(d => (/\.py$|^[^-.]+$/.test(d)) && (d = d.split('.py')[0]) && (!mods.includes(d)) && mods.push(d)) localStorage['pyModules'] = JSON.stringify(JSON.parse(localStorage['pyModules']).concat(mods)) } }) }) }) } dirPythonMod = (mod, cb) => { child_process.exec(`python -c "print(dir(__import__('${mod}')))"`, (err, stdout, stderr) => { if (err) return cb([]) cb(JSON.parse(stdout.replace(/'/g, `"`)).filter(x => x.slice(0, 2) != '__')) }) } // NodeJs 代码提示,所有在沙箱内支持的对象 getNodeJsCommand = () => { var obj = getSandboxFuns() obj.Buffer = Buffer obj.quickcommand.payload = '' return obj } htmlEncode = (value, raw) => { return raw ? String(value).replace(/&/g, "&").replace(/>/g, ">").replace(/ { var file = getQuickCommandScriptFile('py') fs.writeFile(file, code, { encoding: 'utf8' }, err => { var cmd = `python "${GetFilePath('assets/plugins', 'autopep8.py')}" "${file}"` child_process.exec(cmd, { encoding: "buffer" }, (err, stdout, stderr) => { var codec = utools.isWindows() ? 'cp936' : 'utf8' cb(iconv.decode(stdout, codec).trim()) }) }) } processPlatform = process.platform getQuickCommandScriptFile = ext => { return path.join(os.tmpdir(), `QuickCommandTempScript.${ext}`) } getBase64Ico = async path => { let sourceImage = 'data:image/png;base64,' + fs.readFileSync(path, 'base64') let compressedImage = await getCompressedIco(path) return compressedImage.length > sourceImage.length ? sourceImage : compressedImage } getCompressedIco = async (img, width = 40) => { let compressedImage = await pictureCompress({ img: img, width: width, height: width, type: 'png', quality: 1 }) return compressedImage.img } getDefaultCommands = () => { let baseDir = path.join(__dirname, 'defaults') let defaultCommands = {} fs.readdirSync(baseDir).forEach(f => { defaultCommands[f.slice(0, -5)] = path.join(baseDir, f) }) return defaultCommands } getFileInfo = options => { var file if (options.type == 'file') { file = options.argvs } else if (options.type == 'dialog') { var dialog = utools.showOpenDialog(options.argvs); if (!dialog) return false file = dialog[0] } else { return false } var information = { name: path.basename(file), ext: path.extname(file), path: file } if (options.readfile) { var codec = (information.ext == '.bat' || information == '.ps1') ? 'gbk' : 'utf8' information.data = iconv.decode(fs.readFileSync(file), codec) } return information } getCurrentFolderPathFix = () => { let pwd = utools.getCurrentFolderPath() let pwdFix = pwd ? pwd : path.join(utools.getPath('home'), 'desktop') return pwdFix.replace(/\\/g, '\\\\') } saveFile = (content, file) => { if (file instanceof Object) { file = utools.showSaveDialog(file) } file && fs.writeFileSync(file, content) } yuQueClient = axios.create({ baseURL: 'https://www.yuque.com/api/v2/', headers: { 'Content-Type': 'application/json', // 只读权限 'X-Auth-Token': 'WNrd0Z4kfCZLFrGLVAaas93DZ7sbG6PirKq7VxBL' } }); getSelectFile = hwnd => new Promise((reslove, reject) => { if (utools.isWindows()) { var cmd = `powershell.exe -NoProfile "(New-Object -COM 'Shell.Application').Windows() | Where-Object { $_.HWND -eq ${hwnd} } | Select-Object -Expand Document | select @{ n='SelectItems'; e={$_.SelectedItems()} } | select -Expand SelectItems | select -Expand Path "`; child_process.exec(cmd, { encoding: "buffer" }, (err, stdout, stderr) => { if (err) reject(stderr) else reslove(iconv.decode(stdout, 'GBK').trim().replace(/\\/g, '/')); }) } else { var cmd = `osascript -e 'tell application "Finder" to set selectedItems to selection as alias list if selectedItems is {} then return set parentPath to do shell script "dirname " & quoted form of POSIX path of (item 1 of selectedItems) set pathData to "" repeat with theItem in selectedItems set pathData to pathData & POSIX path of theItem & linefeed end repeat ' ` child_process.exec(cmd, (err, stdout, stderr) => { if (err) reject(stderr) else reslove(stdout.trim()); }); } }) clipboardReadText = () => { return electron.clipboard.readText() }, special = cmd => { // 判断是否 windows 系统 if (cmd.includes('{{isWin}}')) { let repl = utools.isWindows() ? 1 : 0; cmd = cmd.replace(/\{\{isWin\}\}/mg, repl) } // 获取本机唯一ID if (cmd.includes('{{LocalId}}')) { let repl = utools.getLocalId(); cmd = cmd.replace(/\{\{LocalId\}\}/mg, repl) } // 获取浏览器当前链接 if (cmd.includes('{{BrowserUrl}}')) { let repl = utools.getCurrentBrowserUrl(); cmd = cmd.replace(/\{\{BrowserUrl\}\}/mg, repl) } // 获取剪切板的文本 if (cmd.includes('{{ClipText}}')) { let repl = clipboardReadText(); cmd = cmd.replace(/\{\{ClipText\}\}/mg, repl) } // 获取选中的文本 // if (cmd.includes('{{SelectText}}')) { // let repl = getSelectText(); // cmd = cmd.replace(/\{\{SelectText\}\}/mg, repl) // } return cmd; } runCodeFile = (cmd, option, terminal, callback) => { var bin = option.bin, argv = option.argv, ext = option.ext, scptarg = option.scptarg || ""; let script = getQuickCommandScriptFile(ext) // 批处理和 powershell 默认编码为 GBK, 解决批处理的换行问题 if (ext == 'bat' || ext == 'ps1') cmd = iconv.encode(cmd.replace(/\n/g, '\r\n'), 'GBK'); fs.writeFileSync(script, cmd); // var argvs = [script] // if (argv) { // argvs = argv.split(' ') // argvs.push(script); // } var child, cmdline if (bin.slice(-7) == 'csc.exe') { cmdline = `${bin} ${argv} /out:"${script.slice(0, -2) + 'exe'}" "${script}" && "${script.slice(0, -2) + 'exe'}" ${scptarg}` } else if (bin == 'gcc') { var suffix = utools.isWindows() ? '.exe' : '' cmdline = `${bin} ${argv} "${script.slice(0, -2)}" "${script}" && "${script.slice(0, -2) + suffix}" ${scptarg}` } else if (utools.isWindows() && bin == 'bash') { cmdline = `${bin} ${argv} "${script.replace(/\\/g, '/').replace(/C:/i, '/mnt/c')}" ${scptarg}` } else { cmdline = `${bin} ${argv} "${script}" ${scptarg}` } // 在终端中输出 if (terminal) { if (utools.isWindows()) { if (bin.slice(-7) == 'csc.exe' || bin == 'gcc') { cmdline = cmdline.split("&&") cmdline = cmdline[0] + "&& start cmd /k " + cmdline[1] } else { cmdline = `start cmd /k ${cmdline}` } } else if(utools.isMacOs()){ var appleScript = `if application "Terminal" is running then tell application "Terminal" do script "clear;${cmdline.replace(/"/g, `\\"`)}" activate end tell else tell application "Terminal" do script "clear;${cmdline.replace(/"/g, `\\"`)}" in window 1 activate end tell end if`; cmdline = `osascript -e '${appleScript}'` } else { return message('Linux 不支持在终端输出') } } child = child_process.spawn(cmdline, { encoding: 'buffer', shell: true }) // var chunks = [], // err_chunks = []; console.log('running: ' + cmdline); child.stdout.on('data', chunk => { if (option.codec) chunk = iconv.decode(chunk, option.codec) callback(chunk.toString(), null) // chunks.push(chunk) }) child.stderr.on('data', stderr => { if (option.codec) stderr = iconv.decode(stderr, option.codec) callback(null, stderr.toString()) // err_chunks.push(err_chunk) }) // child.on('close', code => { // let stdout = chunks.join(""); // let stderr = err_chunks.join(""); // callback(stdout, stderr) // }) }