✨ 支持服务器批量后台命令同步
This commit is contained in:
parent
912ad6561d
commit
22c4e2cd46
@ -15,6 +15,7 @@ module.exports = {
|
|||||||
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
|
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
|
||||||
emailNotifyDBPath: path.join(process.cwd(),'app/db/email.db'),
|
emailNotifyDBPath: path.join(process.cwd(),'app/db/email.db'),
|
||||||
scriptsDBPath: path.join(process.cwd(),'app/db/scripts.db'),
|
scriptsDBPath: path.join(process.cwd(),'app/db/scripts.db'),
|
||||||
|
onekeyDBPath: path.join(process.cwd(),'app/db/onekey.db'),
|
||||||
apiPrefix: '/api/v1',
|
apiPrefix: '/api/v1',
|
||||||
logConfig: {
|
logConfig: {
|
||||||
outDir: path.join(process.cwd(),'./app/logs'),
|
outDir: path.join(process.cwd(),'./app/logs'),
|
||||||
|
29
server/app/controller/onekey.js
Normal file
29
server/app/controller/onekey.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const { readOneKeyRecord, deleteOneKeyRecord } = require('../utils')
|
||||||
|
|
||||||
|
async function getOnekeyRecord({ res }) {
|
||||||
|
let data = await readOneKeyRecord()
|
||||||
|
data = data.map(item => {
|
||||||
|
return { ...item, id: item._id }
|
||||||
|
})
|
||||||
|
data?.sort((a, b) => Number(b.date) - Number(a.date))
|
||||||
|
res.success({ data })
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeOnekeyRecord = async ({ res, request }) => {
|
||||||
|
let { body: { ids } } = request
|
||||||
|
let onekeyRecord = await readOneKeyRecord()
|
||||||
|
if (ids === 'ALL') {
|
||||||
|
ids = onekeyRecord.map(item => item._id)
|
||||||
|
await deleteOneKeyRecord(ids)
|
||||||
|
res.success({ data: '移除全部成功' })
|
||||||
|
} else {
|
||||||
|
if (!onekeyRecord.some(item => ids.includes(item._id))) return res.fail({ msg: '批量指令记录ID不存在' })
|
||||||
|
await deleteOneKeyRecord(ids)
|
||||||
|
res.success({ data: '移除成功' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getOnekeyRecord,
|
||||||
|
removeOnekeyRecord
|
||||||
|
}
|
@ -38,3 +38,13 @@ db目录,初始化后自动生成
|
|||||||
**group.db**
|
**group.db**
|
||||||
|
|
||||||
> 服务器分组配置
|
> 服务器分组配置
|
||||||
|
|
||||||
|
|
||||||
|
**scripts.db**
|
||||||
|
|
||||||
|
> 脚本库
|
||||||
|
|
||||||
|
|
||||||
|
**onekey.db**
|
||||||
|
|
||||||
|
> 批量指令记录
|
||||||
|
@ -4,6 +4,7 @@ const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controlle
|
|||||||
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
|
||||||
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
||||||
const { getScriptList, addScript, updateScriptList, removeScript } = require('../controller/scripts')
|
const { getScriptList, addScript, updateScriptList, removeScript } = require('../controller/scripts')
|
||||||
|
const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
|
||||||
|
|
||||||
const ssh = [
|
const ssh = [
|
||||||
{
|
{
|
||||||
@ -165,4 +166,16 @@ const scripts = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
module.exports = [].concat(ssh, host, user, notify, group, scripts)
|
const onekey = [
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/onekey',
|
||||||
|
controller: getOnekeyRecord
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
path: '/onekey',
|
||||||
|
controller: removeOnekeyRecord
|
||||||
|
}
|
||||||
|
]
|
||||||
|
module.exports = [].concat(ssh, host, user, notify, group, scripts, onekey)
|
||||||
|
@ -5,8 +5,9 @@ const { httpPort } = require('./config')
|
|||||||
const middlewares = require('./middlewares')
|
const middlewares = require('./middlewares')
|
||||||
const wsTerminal = require('./socket/terminal')
|
const wsTerminal = require('./socket/terminal')
|
||||||
const wsSftp = require('./socket/sftp')
|
const wsSftp = require('./socket/sftp')
|
||||||
const wsHostStatus = require('./socket/host-status')
|
// const wsHostStatus = require('./socket/host-status')
|
||||||
const wsClientInfo = require('./socket/clients')
|
const wsClientInfo = require('./socket/clients')
|
||||||
|
const wsOnekey = require('./socket/onekey')
|
||||||
const { throwError } = require('./utils')
|
const { throwError } = require('./utils')
|
||||||
|
|
||||||
const httpServer = () => {
|
const httpServer = () => {
|
||||||
@ -24,7 +25,8 @@ function serverHandler(app, server) {
|
|||||||
app.proxy = true // 用于nginx反代时获取真实客户端ip
|
app.proxy = true // 用于nginx反代时获取真实客户端ip
|
||||||
wsTerminal(server) // 终端
|
wsTerminal(server) // 终端
|
||||||
wsSftp(server) // sftp
|
wsSftp(server) // sftp
|
||||||
wsHostStatus(server) // 终端侧边栏host信息
|
// wsHostStatus(server) // 终端侧边栏host信息(单个host)
|
||||||
|
wsOnekey(server) // 一键指令
|
||||||
wsClientInfo(server) // 客户端信息
|
wsClientInfo(server) // 客户端信息
|
||||||
app.context.throwError = throwError // 常用方法挂载全局ctx上
|
app.context.throwError = throwError // 常用方法挂载全局ctx上
|
||||||
app.use(compose(middlewares))
|
app.use(compose(middlewares))
|
||||||
|
186
server/app/socket/onekey.js
Normal file
186
server/app/socket/onekey.js
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
const { Server } = require('socket.io')
|
||||||
|
const { Client: SSHClient } = require('ssh2')
|
||||||
|
const { readHostList, readSSHRecord, verifyAuthSync, AESDecryptSync, writeOneKeyRecord, throttle } = require('../utils')
|
||||||
|
|
||||||
|
const execStatusEnum = {
|
||||||
|
connecting: '连接中',
|
||||||
|
connectFail: '连接失败',
|
||||||
|
executing: '执行中',
|
||||||
|
execSuccess: '执行成功',
|
||||||
|
execFail: '执行失败',
|
||||||
|
execTimeout: '执行超时',
|
||||||
|
socketInterrupt: '执行中断'
|
||||||
|
}
|
||||||
|
|
||||||
|
let isExecuting = false
|
||||||
|
let execResult = []
|
||||||
|
let execClient = []
|
||||||
|
|
||||||
|
function disconnectAllExecClient() {
|
||||||
|
execClient.forEach((sshClient) => {
|
||||||
|
if (sshClient) {
|
||||||
|
sshClient.end()
|
||||||
|
sshClient.destroy()
|
||||||
|
sshClient = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function execShell(socket, sshClient, curRes, resolve) {
|
||||||
|
const throttledDataHandler = throttle((data) => {
|
||||||
|
curRes.status = execStatusEnum.executing
|
||||||
|
curRes.result += data?.toString() || ''
|
||||||
|
socket.emit('output', execResult)
|
||||||
|
}, 500) // 防止内存爆破
|
||||||
|
sshClient.exec(curRes.command, function(err, stream) {
|
||||||
|
if (err) {
|
||||||
|
console.log(curRes.host, '命令执行失败:', err)
|
||||||
|
curRes.status = execStatusEnum.execFail
|
||||||
|
curRes.result += err.toString()
|
||||||
|
socket.emit('output', execResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream
|
||||||
|
.on('close', () => {
|
||||||
|
throttledDataHandler.flush()
|
||||||
|
// console.log('onekey终端执行完成, 关闭连接: ', curRes.host)
|
||||||
|
if (curRes.status === execStatusEnum.executing) {
|
||||||
|
curRes.status = execStatusEnum.execSuccess
|
||||||
|
}
|
||||||
|
socket.emit('output', execResult)
|
||||||
|
resolve(curRes)
|
||||||
|
sshClient.end()
|
||||||
|
})
|
||||||
|
.on('data', (data) => {
|
||||||
|
// console.log(curRes.host, '执行中: \n' + data)
|
||||||
|
// curRes.status = execStatusEnum.executing
|
||||||
|
// curRes.result += data.toString()
|
||||||
|
// socket.emit('output', execResult)
|
||||||
|
throttledDataHandler(data)
|
||||||
|
})
|
||||||
|
.stderr
|
||||||
|
.on('data', (data) => {
|
||||||
|
// console.log(curRes.host, '命令执行过程中产生错误: ' + data)
|
||||||
|
// curRes.status = execStatusEnum.executing
|
||||||
|
// curRes.result += data.toString()
|
||||||
|
// socket.emit('output', execResult)
|
||||||
|
throttledDataHandler(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (httpServer) => {
|
||||||
|
const serverIo = new Server(httpServer, {
|
||||||
|
path: '/onekey',
|
||||||
|
cors: {
|
||||||
|
origin: '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
serverIo.on('connection', (socket) => {
|
||||||
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
|
consola.success('onekey-terminal websocket 已连接')
|
||||||
|
if (isExecuting) {
|
||||||
|
socket.emit('create_fail', '正在执行中, 请稍后再试')
|
||||||
|
socket.disconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isExecuting = true
|
||||||
|
socket.on('create', async ({ hosts, token, command, timeout }) => {
|
||||||
|
const { code } = await verifyAuthSync(token, clientIp)
|
||||||
|
if (code !== 1) {
|
||||||
|
socket.emit('token_verify_fail')
|
||||||
|
socket.disconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
// 超时未执行完成,断开连接
|
||||||
|
disconnectAllExecClient()
|
||||||
|
const { connecting, executing } = execStatusEnum
|
||||||
|
execResult.forEach(item => {
|
||||||
|
// 连接中和执行中的状态设定为超时
|
||||||
|
if ([connecting, executing].includes(item.status)) {
|
||||||
|
item.status = execStatusEnum.execTimeout
|
||||||
|
}
|
||||||
|
})
|
||||||
|
socket.emit('timeout', { reason: `执行超时,已强制终止执行 - 超时时间${ timeout }秒`, result: execResult })
|
||||||
|
socket.disconnect()
|
||||||
|
}, timeout * 1000)
|
||||||
|
console.log('hosts:', hosts)
|
||||||
|
// console.log('token:', token)
|
||||||
|
console.log('command:', command)
|
||||||
|
const hostList = await readHostList()
|
||||||
|
const targetHostsInfo = hostList.filter(item => hosts.some(ip => item.host === ip)) || {}
|
||||||
|
// console.log('targetHostsInfo:', targetHostsInfo)
|
||||||
|
if (!targetHostsInfo.length) return socket.emit('create_fail', `未找到【${ hosts }】服务器信息`)
|
||||||
|
// 查找 hostInfo -> 并发执行
|
||||||
|
socket.emit('ready')
|
||||||
|
let execPromise = targetHostsInfo.map((hostInfo, index) => {
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
let { authType, host, port, username } = hostInfo
|
||||||
|
let authInfo = { host, port, username }
|
||||||
|
let curRes = { command, host, name: hostInfo.name, result: '', status: execStatusEnum.connecting, date: Date.now() - (targetHostsInfo.length - index) } // , execStatusEnum
|
||||||
|
execResult.push(curRes)
|
||||||
|
try {
|
||||||
|
if (authType === 'credential') {
|
||||||
|
let credentialId = await AESDecryptSync(hostInfo['credential'])
|
||||||
|
const sshRecordList = await readSSHRecord()
|
||||||
|
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
||||||
|
authInfo.authType = sshRecord.authType
|
||||||
|
authInfo[authInfo.authType] = await AESDecryptSync(sshRecord[authInfo.authType])
|
||||||
|
} else {
|
||||||
|
authInfo[authType] = await AESDecryptSync(hostInfo[authType])
|
||||||
|
}
|
||||||
|
consola.info('准备连接终端执行一次性指令:', host)
|
||||||
|
consola.log('连接信息', { username, port, authType })
|
||||||
|
let sshClient = new SSHClient()
|
||||||
|
execClient.push(sshClient)
|
||||||
|
sshClient
|
||||||
|
.on('ready', () => {
|
||||||
|
consola.success('连接终端成功:', host)
|
||||||
|
// socket.emit('connect_success', `已连接到终端:${ host }`)
|
||||||
|
execShell(socket, sshClient, curRes, resolve)
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
console.log(err)
|
||||||
|
consola.error('onekey终端连接失败:', err.level)
|
||||||
|
curRes.status = execStatusEnum.connectFail
|
||||||
|
curRes.result += err.message
|
||||||
|
resolve(curRes)
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
...authInfo
|
||||||
|
// debug: (info) => console.log(info)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
consola.error('创建终端错误:', err.message)
|
||||||
|
curRes.status = execStatusEnum.connectFail
|
||||||
|
curRes.result += err.message
|
||||||
|
resolve(curRes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await Promise.all(execPromise)
|
||||||
|
consola.success('onekey执行完成')
|
||||||
|
socket.emit('exec_complete')
|
||||||
|
socket.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('disconnect', async (reason) => {
|
||||||
|
consola.info('onekey终端连接断开:', reason)
|
||||||
|
disconnectAllExecClient()
|
||||||
|
const { execSuccess, connectFail, execFail, execTimeout } = execStatusEnum
|
||||||
|
execResult.forEach(item => {
|
||||||
|
// 非服务端手动断开连接且命令执行状态为非完成\失败\超时, 判定为客户端主动中断
|
||||||
|
if (reason !== 'server namespace disconnect' && ![execSuccess, execFail, execTimeout, connectFail].includes(item.status)) {
|
||||||
|
item.status = execStatusEnum.socketInterrupt
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await writeOneKeyRecord(execResult)
|
||||||
|
isExecuting = false
|
||||||
|
execResult = []
|
||||||
|
execClient = []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -67,7 +67,7 @@ module.exports = (httpServer) => {
|
|||||||
authInfo[authType] = await AESDecryptSync(targetHostInfo[authType])
|
authInfo[authType] = await AESDecryptSync(targetHostInfo[authType])
|
||||||
}
|
}
|
||||||
consola.info('准备连接终端:', host)
|
consola.info('准备连接终端:', host)
|
||||||
targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
|
// targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
|
||||||
consola.log('连接信息', { username, port, authType })
|
consola.log('连接信息', { username, port, authType })
|
||||||
sshClient
|
sshClient
|
||||||
.on('ready', () => {
|
.on('ready', () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const Datastore = require('@seald-io/nedb')
|
const Datastore = require('@seald-io/nedb')
|
||||||
const { credentialsDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath, scriptsDBPath } = require('../config')
|
const { credentialsDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath, scriptsDBPath, onekeyDBPath } = require('../config')
|
||||||
|
|
||||||
module.exports.KeyDB = class KeyDB {
|
module.exports.KeyDB = class KeyDB {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -77,3 +77,14 @@ module.exports.ScriptsDB = class ScriptsDB {
|
|||||||
return ScriptsDB.instance
|
return ScriptsDB.instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.OnekeyDB = class OnekeyDB {
|
||||||
|
constructor() {
|
||||||
|
if (!OnekeyDB.instance) {
|
||||||
|
OnekeyDB.instance = new Datastore({ filename: onekeyDBPath, autoload: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getInstance() {
|
||||||
|
return OnekeyDB.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,11 +14,14 @@ const {
|
|||||||
readGroupList,
|
readGroupList,
|
||||||
writeGroupList,
|
writeGroupList,
|
||||||
readScriptList,
|
readScriptList,
|
||||||
writeScriptList
|
writeScriptList,
|
||||||
|
readOneKeyRecord,
|
||||||
|
writeOneKeyRecord,
|
||||||
|
deleteOneKeyRecord
|
||||||
} = require('./storage')
|
} = require('./storage')
|
||||||
const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt')
|
const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt')
|
||||||
const { verifyAuthSync, isProd } = require('./verify-auth')
|
const { verifyAuthSync, isProd } = require('./verify-auth')
|
||||||
const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp } = require('./tools')
|
const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp, throttle } = require('./tools')
|
||||||
const { emailTransporter, sendEmailToConfList } = require('./email')
|
const { emailTransporter, sendEmailToConfList } = require('./email')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -28,6 +31,7 @@ module.exports = {
|
|||||||
randomStr,
|
randomStr,
|
||||||
getUTCDate,
|
getUTCDate,
|
||||||
formatTimestamp,
|
formatTimestamp,
|
||||||
|
throttle,
|
||||||
verifyAuthSync,
|
verifyAuthSync,
|
||||||
isProd,
|
isProd,
|
||||||
RSADecryptSync,
|
RSADecryptSync,
|
||||||
@ -51,5 +55,8 @@ module.exports = {
|
|||||||
readGroupList,
|
readGroupList,
|
||||||
writeGroupList,
|
writeGroupList,
|
||||||
readScriptList,
|
readScriptList,
|
||||||
writeScriptList
|
writeScriptList,
|
||||||
|
readOneKeyRecord,
|
||||||
|
writeOneKeyRecord,
|
||||||
|
deleteOneKeyRecord
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { KeyDB, HostListDB, SshRecordDB, NotifyDB, GroupDB, EmailNotifyDB, ScriptsDB } = require('./db-class')
|
const { KeyDB, HostListDB, SshRecordDB, NotifyDB, GroupDB, EmailNotifyDB, ScriptsDB, OnekeyDB } = require('./db-class')
|
||||||
|
|
||||||
const readKey = async () => {
|
const readKey = async () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -270,6 +270,49 @@ const writeScriptList = async (list = []) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const readOneKeyRecord = async () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onekeyDB = new OnekeyDB().getInstance()
|
||||||
|
onekeyDB.find({}, (err, docs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('读取onekey record错误: ', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(docs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeOneKeyRecord = async (records =[]) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onekeyDB = new OnekeyDB().getInstance()
|
||||||
|
onekeyDB.insert(records, (err, newDocs) => {
|
||||||
|
if (err) {
|
||||||
|
consola.error('写入新的onekey记录出错:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
onekeyDB.compactDatafile()
|
||||||
|
resolve(newDocs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const deleteOneKeyRecord = async (ids =[]) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const onekeyDB = new OnekeyDB().getInstance()
|
||||||
|
onekeyDB.remove({ _id: { $in: ids } }, { multi: true }, function (err, numRemoved) {
|
||||||
|
if (err) {
|
||||||
|
consola.error('Error deleting onekey record(s):', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
onekeyDB.compactDatafile()
|
||||||
|
resolve(numRemoved)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
readSSHRecord,
|
readSSHRecord,
|
||||||
writeSSHRecord,
|
writeSSHRecord,
|
||||||
@ -286,5 +329,8 @@ module.exports = {
|
|||||||
readUserEmailList,
|
readUserEmailList,
|
||||||
writeUserEmailList,
|
writeUserEmailList,
|
||||||
readScriptList,
|
readScriptList,
|
||||||
writeScriptList
|
writeScriptList,
|
||||||
|
readOneKeyRecord,
|
||||||
|
writeOneKeyRecord,
|
||||||
|
deleteOneKeyRecord
|
||||||
}
|
}
|
@ -204,6 +204,43 @@ function resolvePath(dir, path) {
|
|||||||
return path.resolve(dir, path)
|
return path.resolve(dir, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function throttle(func, limit) {
|
||||||
|
let lastFunc
|
||||||
|
let lastRan
|
||||||
|
let pendingArgs = null
|
||||||
|
|
||||||
|
const runner = () => {
|
||||||
|
func.apply(this, pendingArgs)
|
||||||
|
lastRan = Date.now()
|
||||||
|
pendingArgs = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttled = function() {
|
||||||
|
const context = this
|
||||||
|
const args = arguments
|
||||||
|
pendingArgs = args
|
||||||
|
if (!lastRan || (Date.now() - lastRan >= limit)) {
|
||||||
|
if (lastFunc) {
|
||||||
|
clearTimeout(lastFunc)
|
||||||
|
}
|
||||||
|
runner.apply(context, args)
|
||||||
|
} else {
|
||||||
|
clearTimeout(lastFunc)
|
||||||
|
lastFunc = setTimeout(() => {
|
||||||
|
runner.apply(context, args)
|
||||||
|
}, limit - (Date.now() - lastRan))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throttled.flush = () => {
|
||||||
|
if (pendingArgs) {
|
||||||
|
runner.apply(this, pendingArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return throttled
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getNetIPInfo,
|
getNetIPInfo,
|
||||||
throwError,
|
throwError,
|
||||||
@ -211,5 +248,6 @@ module.exports = {
|
|||||||
randomStr,
|
randomStr,
|
||||||
getUTCDate,
|
getUTCDate,
|
||||||
formatTimestamp,
|
formatTimestamp,
|
||||||
resolvePath
|
resolvePath,
|
||||||
|
throttle
|
||||||
}
|
}
|
@ -4,7 +4,7 @@
|
|||||||
"description": "easynode-server",
|
"description": "easynode-server",
|
||||||
"bin": "./bin/www",
|
"bin": "./bin/www",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"local": "cross-env EXEC_ENV=local nodemon ./app/index.js",
|
"local": "cross-env EXEC_ENV=local nodemon ./app/index.js --max-old-space-size=4096",
|
||||||
"prod": "cross-env EXEC_ENV=production nodemon ./app/index.js",
|
"prod": "cross-env EXEC_ENV=production nodemon ./app/index.js",
|
||||||
"start": "node ./index.js",
|
"start": "node ./index.js",
|
||||||
"lint": "eslint . --ext .js,.vue",
|
"lint": "eslint . --ext .js,.vue",
|
||||||
|
@ -34,6 +34,7 @@ module.exports = {
|
|||||||
'vue/singleline-html-element-content-newline': 0,
|
'vue/singleline-html-element-content-newline': 0,
|
||||||
|
|
||||||
// js
|
// js
|
||||||
|
'no-async-promise-executor': 0,
|
||||||
'import/no-extraneous-dependencies': 0,
|
'import/no-extraneous-dependencies': 0,
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
@ -99,5 +99,11 @@ export default {
|
|||||||
},
|
},
|
||||||
deleteScript(id) {
|
deleteScript(id) {
|
||||||
return axios({ url: `/script/${ id }`, method: 'delete' })
|
return axios({ url: `/script/${ id }`, method: 'delete' })
|
||||||
|
},
|
||||||
|
getOnekeyRecord() {
|
||||||
|
return axios({ url: '/onekey', method: 'get' })
|
||||||
|
},
|
||||||
|
deleteOnekeyRecord(ids) {
|
||||||
|
return axios({ url: '/onekey', method: 'post', data: { ids } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,11 +67,11 @@ let menuList = reactive([
|
|||||||
icon: markRaw(ArrowRight),
|
icon: markRaw(ArrowRight),
|
||||||
index: '/scripts'
|
index: '/scripts'
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// name: '批量指令',
|
name: '批量指令',
|
||||||
// icon: markRaw(Pointer),
|
icon: markRaw(Pointer),
|
||||||
// index: '/onekey'
|
index: '/onekey'
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
name: '系统设置',
|
name: '系统设置',
|
||||||
icon: markRaw(Setting),
|
icon: markRaw(Setting),
|
||||||
|
@ -1,19 +1,445 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="onekey_container">
|
||||||
开发中...
|
<div class="header">
|
||||||
|
<el-button type="primary" @click="addOnekey">
|
||||||
|
批量下发指令
|
||||||
|
</el-button>
|
||||||
|
<el-button v-show="recordList.length" type="danger" @click="handleRemoveAll">
|
||||||
|
删除全部记录
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<!-- default-expand-all -->
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
row-key="id"
|
||||||
|
:expand-row-keys="expandRows"
|
||||||
|
>
|
||||||
|
<el-table-column type="expand">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="detail_content_box">
|
||||||
|
{{ row.result }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="实例">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span style="letter-spacing: 2px;"> {{ row.name }} </span>
|
||||||
|
<span style="letter-spacing: 2px;"> {{ row.host }} </span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="command" label="指令">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span style="letter-spacing: 2px;background: rgba(227, 230, 235, 0.7);color: rgb(54, 52, 52);"> {{ row.command }} </span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="执行结果" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :color="getStatusType(row.status)">
|
||||||
|
<span style="color: rgb(54, 52, 52);">{{ row.status }}</span>
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="!row.pendding"
|
||||||
|
v-show="row.id !== 'own'"
|
||||||
|
:loading="row.loading"
|
||||||
|
type="danger"
|
||||||
|
@click="handleRemove([row.id])"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-dialog
|
||||||
|
v-model="formVisible"
|
||||||
|
width="600px"
|
||||||
|
top="150px"
|
||||||
|
title="批量下发指令"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="clearFormInfo"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="updateFormRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:hide-required-asterisk="true"
|
||||||
|
label-suffix=":"
|
||||||
|
label-width="80px"
|
||||||
|
:show-message="false"
|
||||||
|
>
|
||||||
|
<el-form-item label="实例" prop="hosts">
|
||||||
|
<div class="select_host_wrap">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.hosts"
|
||||||
|
:teleported="false"
|
||||||
|
multiple
|
||||||
|
placeholder=""
|
||||||
|
class="select"
|
||||||
|
clearable
|
||||||
|
tag-type="primary"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="checkAll"
|
||||||
|
:indeterminate="indeterminate"
|
||||||
|
@change="selectAllHost"
|
||||||
|
>
|
||||||
|
全选 <span class="tips">(未配置ssh连接信息的实例不会显示在列表中)</span>
|
||||||
|
</el-checkbox>
|
||||||
|
</template>
|
||||||
|
<el-option
|
||||||
|
v-for="item in hasConfigHostList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.host"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<!-- <el-button type="primary" class="btn" @click="selectAllHost">全选</el-button> -->
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="command" label="指令">
|
||||||
|
<div class="command_wrap">
|
||||||
|
<el-dropdown
|
||||||
|
trigger="click"
|
||||||
|
max-height="50vh"
|
||||||
|
:teleported="false"
|
||||||
|
class="scripts_menu"
|
||||||
|
>
|
||||||
|
<span class="link_text">从脚本库导入...<el-icon><arrow-down /></el-icon></span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-for="item in scriptList" :key="item.id" @click="handleImportScript(item)">
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.command"
|
||||||
|
class="input"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
clearable
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="shell script"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="timeout" label="超时(s)">
|
||||||
|
<el-input
|
||||||
|
v-model.trim.number="formData.timeout"
|
||||||
|
type="number"
|
||||||
|
clearable
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="指令执行超时时间,单位秒,超时自动中断"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span>
|
||||||
|
<el-button @click="formVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="execOnekey">执行</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, reactive, onMounted, computed, watch, nextTick, getCurrentInstance } from 'vue'
|
||||||
name: '',
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
data() {
|
import socketIo from 'socket.io-client'
|
||||||
return {
|
|
||||||
}
|
const { io } = socketIo
|
||||||
|
|
||||||
|
const { proxy: { $api, $notification,$messageBox, $message, $router, $serviceURI, $store } } = getCurrentInstance()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const formVisible = ref(false)
|
||||||
|
const socket = ref(null)
|
||||||
|
let recordList = ref([])
|
||||||
|
let penddingRecord = ref([])
|
||||||
|
let checkAll = ref(false)
|
||||||
|
let indeterminate = ref(false)
|
||||||
|
const updateFormRef = ref(null)
|
||||||
|
|
||||||
|
let formData = reactive({
|
||||||
|
hosts: [],
|
||||||
|
command: 'ping -c 10 google.com',
|
||||||
|
timeout: 60
|
||||||
|
})
|
||||||
|
|
||||||
|
const token = computed(() => $store.token)
|
||||||
|
const hostList = computed(() => $store.hostList)
|
||||||
|
let scriptList = computed(() => $store.scriptList)
|
||||||
|
const hasConfigHostList = computed(() => hostList.value.filter(item => item.isConfig))
|
||||||
|
const tableData = computed(() => {
|
||||||
|
return penddingRecord.value.concat(recordList.value).map(item => {
|
||||||
|
item.loading = false
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const expandRows = computed(() => {
|
||||||
|
let rows = tableData.value.filter(item => item.pendding).map(item => item.id)
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = computed(() => {
|
||||||
|
return {
|
||||||
|
hosts: { required: true, trigger: 'change' },
|
||||||
|
command: { required: true, trigger: 'change' },
|
||||||
|
timeout: { required: true, type: 'number', trigger: 'change' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => formData.hosts, (val) => {
|
||||||
|
if (val.length === 0) {
|
||||||
|
checkAll.value = false
|
||||||
|
indeterminate.value = false
|
||||||
|
} else if (val.length === hasConfigHostList.value.length) {
|
||||||
|
checkAll.value = true
|
||||||
|
indeterminate.value = false
|
||||||
|
} else {
|
||||||
|
indeterminate.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const createExecShell = (hosts = [], command = 'ls', timeout = 60) => {
|
||||||
|
loading.value = true
|
||||||
|
socket.value = io($serviceURI, {
|
||||||
|
path: '/onekey',
|
||||||
|
forceNew: false,
|
||||||
|
reconnectionAttempts: 1
|
||||||
|
})
|
||||||
|
socket.value.on('connect', () => {
|
||||||
|
console.log('onekey socket已连接:', socket.value.id)
|
||||||
|
|
||||||
|
socket.value.on('ready', () => {
|
||||||
|
penddingRecord.value = [] // 每轮执行前清空
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.emit('create', { hosts, token: token.value, command, timeout })
|
||||||
|
|
||||||
|
socket.value.on('output', (result) => {
|
||||||
|
loading.value = false
|
||||||
|
if (Array.isArray(result) && result.length > 0) {
|
||||||
|
// console.log('output', result)
|
||||||
|
result = result.map(item => ({ ...item, pendding: true }))
|
||||||
|
penddingRecord.value = result
|
||||||
|
nextTick(() => {
|
||||||
|
document.querySelectorAll('.detail_content_box').forEach(container => {
|
||||||
|
container.scrollTop = container.scrollHeight
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('timeout', ({ reason, result }) => {
|
||||||
|
$notification({
|
||||||
|
title: '批量指令执行超时',
|
||||||
|
message: reason,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
if (Array.isArray(result) && result.length > 0) {
|
||||||
|
// console.log('output', result)
|
||||||
|
result = result.map(item => ({ ...item, pendding: true }))
|
||||||
|
penddingRecord.value = result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
socket.value.on('create_fail', (reason) => {
|
||||||
|
$notification({
|
||||||
|
title: '批量指令执行失败',
|
||||||
|
message: reason,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('token_verify_fail', () => {
|
||||||
|
$message.error('token验证失败,请重新登录')
|
||||||
|
$router.push('/login')
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('exec_complete', () => {
|
||||||
|
$notification({
|
||||||
|
title: '批量指令执行完成',
|
||||||
|
message: '执行完成',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('disconnect', () => {
|
||||||
|
loading.value = false
|
||||||
|
console.warn('onekey websocket 连接断开')
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('connect_error', (err) => {
|
||||||
|
loading.value = false
|
||||||
|
console.error('onekey websocket 连接错误:', err)
|
||||||
|
$notification({
|
||||||
|
title: 'onekey websocket 连接错误:',
|
||||||
|
message: '请检查socket服务是否正常',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
getOnekeyRecord()
|
||||||
|
})
|
||||||
|
|
||||||
|
let selectAllHost = (val) => {
|
||||||
|
indeterminate.value = false
|
||||||
|
if (val) {
|
||||||
|
formData.hosts = hasConfigHostList.value.map(item => item.host)
|
||||||
|
} else {
|
||||||
|
formData.hosts = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let handleImportScript = (scriptObj) => {
|
||||||
|
formData.command = scriptObj.content
|
||||||
|
}
|
||||||
|
|
||||||
|
let getStatusType = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case '连接中':
|
||||||
|
return '#FFDEAD'
|
||||||
|
case '连接失败':
|
||||||
|
return '#FFCCCC'
|
||||||
|
case '执行中':
|
||||||
|
return '#ADD8E6'
|
||||||
|
case '执行成功':
|
||||||
|
return '#90EE90'
|
||||||
|
case '执行失败':
|
||||||
|
return '#FFCCCC'
|
||||||
|
case '执行超时':
|
||||||
|
return '#FFFFE0'
|
||||||
|
case '执行中断':
|
||||||
|
return '#E6E6FA'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let getOnekeyRecord = async () => {
|
||||||
|
loading.value = true
|
||||||
|
let { data } = await $api.getOnekeyRecord()
|
||||||
|
recordList.value = data
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let addOnekey = () => {
|
||||||
|
formVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function execOnekey() {
|
||||||
|
updateFormRef.value.validate()
|
||||||
|
.then(async () => {
|
||||||
|
let { hosts, command, timeout } = formData
|
||||||
|
timeout = Number(timeout)
|
||||||
|
if (timeout < 1) {
|
||||||
|
return $message.error('超时时间不能小于1秒')
|
||||||
|
}
|
||||||
|
if (hosts.length === 0) {
|
||||||
|
return $message.error('请选择主机')
|
||||||
|
}
|
||||||
|
await getOnekeyRecord() // 获取新纪录前会清空 penddingRecord,所以需要获取一次最新的list
|
||||||
|
createExecShell(hosts, command, timeout)
|
||||||
|
formVisible.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFormInfo = () => {
|
||||||
|
nextTick(() => updateFormRef.value.resetFields())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemove = async (ids = []) => {
|
||||||
|
tableData.value.filter(item => ids.includes(item.id)).forEach(item => item.loading = true)
|
||||||
|
await $api.deleteOnekeyRecord(ids)
|
||||||
|
await getOnekeyRecord()
|
||||||
|
$message.success('success')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveAll = async () => {
|
||||||
|
$messageBox.confirm(`确认删除所有执行记录:${ name }`, 'Warning', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await $api.deleteOnekeyRecord('ALL')
|
||||||
|
penddingRecord.value = []
|
||||||
|
await getOnekeyRecord()
|
||||||
|
$message.success('success')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.onekey_container {
|
||||||
</style>
|
padding: 20px;
|
||||||
|
.header {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.detail_content_box {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-line;
|
||||||
|
line-height: 1.1;
|
||||||
|
background: rgba(227, 230, 235, .7);
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.select_host_wrap {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
.select {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 15px;
|
||||||
|
.tips {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
width: 52px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.command_wrap {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.scripts_menu {
|
||||||
|
:deep(.el-dropdown-menu) {
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.link_text {
|
||||||
|
font-size: var(--el-font-size-base);
|
||||||
|
// color: var(--el-text-color-regular);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 15px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -143,7 +143,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits, computed, defineProps, getCurrentInstance, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, defineEmits, computed, defineProps, getCurrentInstance, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { ArrowDown, FullScreen, Select } from '@element-plus/icons-vue'
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
import TerminalTab from './terminal-tab.vue'
|
import TerminalTab from './terminal-tab.vue'
|
||||||
import InfoSide from './info-side.vue'
|
import InfoSide from './info-side.vue'
|
||||||
import Sftp from './sftp.vue'
|
import Sftp from './sftp.vue'
|
||||||
@ -218,12 +218,6 @@ const handleCommandHost = (host) => {
|
|||||||
emit('add-host', host)
|
emit('add-host', host)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSyncSession = () => {
|
|
||||||
isSyncAllSession.value = !isSyncAllSession.value
|
|
||||||
if (isSyncAllSession.value) $message.success('已开启键盘输入到所有会话')
|
|
||||||
else $message.info('已关闭键盘输入到所有会话')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExecScript = (scriptObj) => {
|
const handleExecScript = (scriptObj) => {
|
||||||
// console.log(scriptObj.content)
|
// console.log(scriptObj.content)
|
||||||
if (!isSyncAllSession.value) return handleInputCommand(scriptObj.content)
|
if (!isSyncAllSession.value) return handleInputCommand(scriptObj.content)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user