支持服务器批量后台命令同步

This commit is contained in:
chaos-zhu 2024-08-01 23:14:47 +08:00
parent 912ad6561d
commit 22c4e2cd46
17 changed files with 804 additions and 34 deletions

View File

@ -15,6 +15,7 @@ module.exports = {
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
emailNotifyDBPath: path.join(process.cwd(),'app/db/email.db'),
scriptsDBPath: path.join(process.cwd(),'app/db/scripts.db'),
onekeyDBPath: path.join(process.cwd(),'app/db/onekey.db'),
apiPrefix: '/api/v1',
logConfig: {
outDir: path.join(process.cwd(),'./app/logs'),

View 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
}

View File

@ -38,3 +38,13 @@ db目录初始化后自动生成
**group.db**
> 服务器分组配置
**scripts.db**
> 脚本库
**onekey.db**
> 批量指令记录

View File

@ -4,6 +4,7 @@ const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controlle
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
const { getScriptList, addScript, updateScriptList, removeScript } = require('../controller/scripts')
const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
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)

View File

@ -5,8 +5,9 @@ const { httpPort } = require('./config')
const middlewares = require('./middlewares')
const wsTerminal = require('./socket/terminal')
const wsSftp = require('./socket/sftp')
const wsHostStatus = require('./socket/host-status')
// const wsHostStatus = require('./socket/host-status')
const wsClientInfo = require('./socket/clients')
const wsOnekey = require('./socket/onekey')
const { throwError } = require('./utils')
const httpServer = () => {
@ -24,7 +25,8 @@ function serverHandler(app, server) {
app.proxy = true // 用于nginx反代时获取真实客户端ip
wsTerminal(server) // 终端
wsSftp(server) // sftp
wsHostStatus(server) // 终端侧边栏host信息
// wsHostStatus(server) // 终端侧边栏host信息(单个host)
wsOnekey(server) // 一键指令
wsClientInfo(server) // 客户端信息
app.context.throwError = throwError // 常用方法挂载全局ctx上
app.use(compose(middlewares))

186
server/app/socket/onekey.js Normal file
View 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 = []
})
})
}

View File

@ -67,7 +67,7 @@ module.exports = (httpServer) => {
authInfo[authType] = await AESDecryptSync(targetHostInfo[authType])
}
consola.info('准备连接终端:', host)
targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
// targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
consola.log('连接信息', { username, port, authType })
sshClient
.on('ready', () => {

View File

@ -1,5 +1,5 @@
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 {
constructor() {
@ -77,3 +77,14 @@ module.exports.ScriptsDB = class ScriptsDB {
return ScriptsDB.instance
}
}
module.exports.OnekeyDB = class OnekeyDB {
constructor() {
if (!OnekeyDB.instance) {
OnekeyDB.instance = new Datastore({ filename: onekeyDBPath, autoload: true })
}
}
getInstance() {
return OnekeyDB.instance
}
}

View File

@ -14,11 +14,14 @@ const {
readGroupList,
writeGroupList,
readScriptList,
writeScriptList
writeScriptList,
readOneKeyRecord,
writeOneKeyRecord,
deleteOneKeyRecord
} = require('./storage')
const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt')
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')
module.exports = {
@ -28,6 +31,7 @@ module.exports = {
randomStr,
getUTCDate,
formatTimestamp,
throttle,
verifyAuthSync,
isProd,
RSADecryptSync,
@ -51,5 +55,8 @@ module.exports = {
readGroupList,
writeGroupList,
readScriptList,
writeScriptList
writeScriptList,
readOneKeyRecord,
writeOneKeyRecord,
deleteOneKeyRecord
}

View File

@ -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 () => {
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 = {
readSSHRecord,
writeSSHRecord,
@ -286,5 +329,8 @@ module.exports = {
readUserEmailList,
writeUserEmailList,
readScriptList,
writeScriptList
writeScriptList,
readOneKeyRecord,
writeOneKeyRecord,
deleteOneKeyRecord
}

View File

@ -204,6 +204,43 @@ function resolvePath(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 = {
getNetIPInfo,
throwError,
@ -211,5 +248,6 @@ module.exports = {
randomStr,
getUTCDate,
formatTimestamp,
resolvePath
resolvePath,
throttle
}

View File

@ -4,7 +4,7 @@
"description": "easynode-server",
"bin": "./bin/www",
"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",
"start": "node ./index.js",
"lint": "eslint . --ext .js,.vue",

View File

@ -34,6 +34,7 @@ module.exports = {
'vue/singleline-html-element-content-newline': 0,
// js
'no-async-promise-executor': 0,
'import/no-extraneous-dependencies': 0,
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',

View File

@ -99,5 +99,11 @@ export default {
},
deleteScript(id) {
return axios({ url: `/script/${ id }`, method: 'delete' })
},
getOnekeyRecord() {
return axios({ url: '/onekey', method: 'get' })
},
deleteOnekeyRecord(ids) {
return axios({ url: '/onekey', method: 'post', data: { ids } })
}
}

View File

@ -67,11 +67,11 @@ let menuList = reactive([
icon: markRaw(ArrowRight),
index: '/scripts'
},
// {
// name: '',
// icon: markRaw(Pointer),
// index: '/onekey'
// },
{
name: '批量指令',
icon: markRaw(Pointer),
index: '/onekey'
},
{
name: '系统设置',
icon: markRaw(Setting),

View File

@ -1,19 +1,445 @@
<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>
</template>
<script>
export default {
name: '',
data() {
return {
}
<script setup>
import { ref, reactive, onMounted, computed, watch, nextTick, getCurrentInstance } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue'
import socketIo from 'socket.io-client'
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() // penddingRecordlist
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>
<style lang="scss" scoped>
</style>
.onekey_container {
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>

View File

@ -143,7 +143,7 @@
<script setup>
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 InfoSide from './info-side.vue'
import Sftp from './sftp.vue'
@ -218,12 +218,6 @@ const handleCommandHost = (host) => {
emit('add-host', host)
}
const handleSyncSession = () => {
isSyncAllSession.value = !isSyncAllSession.value
if (isSyncAllSession.value) $message.success('已开启键盘输入到所有会话')
else $message.info('已关闭键盘输入到所有会话')
}
const handleExecScript = (scriptObj) => {
// console.log(scriptObj.content)
if (!isSyncAllSession.value) return handleInputCommand(scriptObj.content)