diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bd791..d812966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [2.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-10-xx) + +### Features + +* 不再对同IP:PORT的实例进行校验 +* 支持同IP任意端口的服务器录入 +* 支持关闭所有终端连接 +* TODO: 兼容移动端展示 + ## [2.2.4](https://github.com/chaos-zhu/easynode/releases) (2024-08-31) ### Features diff --git a/server/app/controller/host.js b/server/app/controller/host.js index bcb0320..6a1ac44 100644 --- a/server/app/controller/host.js +++ b/server/app/controller/host.js @@ -24,17 +24,16 @@ async function addHost({ }) { let { body: { - name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark, - port, username, authType, password, privateKey, credential, command, tempKey + name, host, index, expired, expiredNotify, group, consoleUrl, remark, + port: newPort, username, authType, password, privateKey, credential, command, tempKey } } = request // console.log(request) - if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' }) + if (!host || !name) return res.fail({ msg: 'missing params: name or host' }) let hostList = await readHostList() - if (hostList?.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` }) let record = { - name, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark, - port, username, authType, password, privateKey, credential, command + name, host, index, expired, expiredNotify, group, consoleUrl, remark, + port: newPort, username, authType, password, privateKey, credential, command } if (record[authType]) { const clearTempKey = await RSADecryptSync(tempKey) @@ -53,6 +52,7 @@ async function updateHost({ res, request }) { let { body: { hosts, + id, host: newHost, name: newName, index, oldHost, expired, expiredNotify, group, consoleUrl, remark, port, username, authType, password, privateKey, credential, command, tempKey } @@ -61,10 +61,9 @@ async function updateHost({ res, request }) { if (isBatch) { if (!hosts.length) return res.fail({ msg: 'hosts为空' }) let hostList = await readHostList() - // console.log('批量修改实例') let newHostList = [] for (let oldRecord of hostList) { - let record = hosts.find(item => item.host === oldRecord.host) + let record = hosts.find(item => item.id === oldRecord._id) if (!record) { newHostList.push(oldRecord) continue @@ -94,7 +93,7 @@ async function updateHost({ res, request }) { } if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试添加实例` }) - let idx = hostList.findIndex(({ host }) => host === oldHost) + let idx = hostList.findIndex(({ _id }) => _id === id) const oldRecord = hostList[idx] // 如果存在原认证方式则保存下来 if (!record[authType] && oldRecord[authType]) { @@ -115,16 +114,10 @@ async function updateHost({ res, request }) { async function removeHost({ res, request }) { - let { body: { host } } = request + let { body: { ids } } = request let hostList = await readHostList() - if (Array.isArray(host)) { - hostList = hostList.filter(item => !host.includes(item.host)) - // if (hostList.length === 0) return res.fail({ msg: '没有可删除的实例' }) - } else { - let hostIdx = hostList.findIndex(item => item.host === host) - if (hostIdx === -1) return res.fail({ msg: `${ host }不存在` }) - hostList.splice(hostIdx, 1) - } + if (!Array.isArray(ids)) return res.fail({ msg: '参数错误' }) + hostList = hostList.filter(({ id }) => !ids.includes(id)) writeHostList(hostList) res.success({ data: '已移除' }) } @@ -135,9 +128,9 @@ async function importHost({ let { body: { importHost, isEasyNodeJson = false } } = request if (!Array.isArray(importHost)) return res.fail({ msg: '参数错误' }) let hostList = await readHostList() - // 过滤已存在的host - let hostListSet = new Set(hostList.map(item => item.host)) - let newHostList = importHost.filter(item => !hostListSet.has(item.host)) + // 考虑到批量导入可能会重复太多,先过滤已存在的host:port + let hostListSet = new Set(hostList.map(({ host, port }) => `${ host }:${ port }`)) + let newHostList = importHost.filter(({ host, port }) => !hostListSet.has(`${ host }:${ port }`)) let newHostListLen = newHostList.length if (newHostListLen === 0) return res.fail({ msg: '导入的实例已存在' }) @@ -159,7 +152,6 @@ async function importHost({ item.index = newHostListLen - index return Object.assign(item, { ...extraFiels }) }) - } hostList.push(...newHostList) writeHostList(hostList) diff --git a/server/app/socket/onekey.js b/server/app/socket/onekey.js index 94b36ee..2cdbb92 100644 --- a/server/app/socket/onekey.js +++ b/server/app/socket/onekey.js @@ -104,7 +104,7 @@ module.exports = (httpServer) => { return } isExecuting = true - socket.on('create', async ({ hosts, token, command, timeout }) => { + socket.on('create', async ({ hostIds, token, command, timeout }) => { const { code } = await verifyAuthSync(token, requestIP) if (code !== 1) { socket.emit('token_verify_fail') @@ -126,13 +126,13 @@ module.exports = (httpServer) => { socket.disconnect() disconnectAllExecClient() }, timeout * 1000) - console.log('hosts:', hosts) + console.log('hostIds:', hostIds) // console.log('token:', token) console.log('command:', command) const hostList = await readHostList() - const targetHostsInfo = hostList.filter(item => hosts.some(ip => item.host === ip)) || {} + const targetHostsInfo = hostList.filter(item => hostIds.some(id => item._id === id)) || {} // console.log('targetHostsInfo:', targetHostsInfo) - if (!targetHostsInfo.length) return socket.emit('create_fail', `未找到【${ hosts }】服务器信息`) + if (!targetHostsInfo.length) return socket.emit('create_fail', `未找到【${ hostIds }】服务器信息`) // 查找 hostInfo -> 并发执行 socket.emit('ready') let execPromise = targetHostsInfo.map((hostInfo, index) => { @@ -141,7 +141,7 @@ module.exports = (httpServer) => { setTimeout(() => reject('执行超时'), timeout * 1000) 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 + let curRes = { command, host, port, name: hostInfo.name, result: '', status: execStatusEnum.connecting, date: Date.now() - (targetHostsInfo.length - index) } // , execStatusEnum execResult.push(curRes) try { if (authType === 'credential') { diff --git a/web/src/views/onekey/index.vue b/web/src/views/onekey/index.vue index 9f0e9d3..75513f4 100644 --- a/web/src/views/onekey/index.vue +++ b/web/src/views/onekey/index.vue @@ -34,8 +34,9 @@ @@ -81,10 +82,10 @@ label-width="80px" :show-message="false" > - +
@@ -182,7 +183,7 @@ let timeRemaining = ref(0) const isClient = ref(false) let formData = reactive({ - hosts: [], + hostIds: [], command: '', timeout: 120 }) @@ -206,13 +207,13 @@ const expandRows = computed(() => { const rules = computed(() => { return { - hosts: { required: true, trigger: 'change' }, + hostIds: { required: true, trigger: 'change' }, command: { required: true, trigger: 'change' }, timeout: { required: true, type: 'number', trigger: 'change' } } }) -watch(() => formData.hosts, (val) => { +watch(() => formData.hostIds, (val) => { if (val.length === 0) { checkAll.value = false indeterminate.value = false @@ -224,7 +225,7 @@ watch(() => formData.hosts, (val) => { } }) -const createExecShell = (hosts = [], command = 'ls', timeout = 60) => { +const createExecShell = (hostIds = [], command = 'ls', timeout = 60) => { loading.value = true timeRemaining.value = Number(formData.timeout) let timer = null @@ -243,7 +244,7 @@ const createExecShell = (hosts = [], command = 'ls', timeout = 60) => { pendingRecord.value = [] // 每轮执行前清空 }) - socket.value.emit('create', { hosts, token: token.value, command, timeout }) + socket.value.emit('create', { hostIds, token: token.value, command, timeout }) socket.value.on('output', (result) => { loading.value = false @@ -320,9 +321,9 @@ onMounted(async () => { let selectAllHost = (val) => { indeterminate.value = false if (val) { - formData.hosts = hasConfigHostList.value.map(item => item.host) + formData.hostIds = hasConfigHostList.value.map(item => item.id) } else { - formData.hosts = [] + formData.hostIds = [] } } @@ -366,16 +367,16 @@ let addOnekey = () => { function execOnekey() { updateFormRef.value.validate() .then(async () => { - let { hosts, command, timeout } = formData + let { hostIds, command, timeout } = formData timeout = Number(timeout) if (timeout < 1) { return $message.error('超时时间不能小于1秒') } - if (hosts.length === 0) { + if (hostIds.length === 0) { return $message.error('请选择主机') } await getOnekeyRecord() // 获取新纪录前会清空 pendingRecord,所以需要获取一次最新的list - createExecShell(hosts, command, timeout) + createExecShell(hostIds, command, timeout) formVisible.value = false }) } @@ -407,11 +408,12 @@ const handleRemoveAll = async () => { onActivated(async () => { await nextTick() - const { host, execClientInstallScript } = route.query - if (!host) return + const { hostIds, execClientInstallScript } = route.query + if (!hostIds) return if (execClientInstallScript === 'true') { let clientInstallScript = 'curl -o- https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash\n' - createExecShell(host.split(','), clientInstallScript, 300) + console.log(hostIds.split(',')) + createExecShell(hostIds.split(','), clientInstallScript, 300) // $messageBox.confirm(`准备安装客户端服务监控应用:${ host }`, 'Warning', { // confirmButtonText: '确定', // cancelButtonText: '取消', diff --git a/web/src/views/server/components/host-card.vue b/web/src/views/server/components/host-card.vue deleted file mode 100644 index b4ee8db..0000000 --- a/web/src/views/server/components/host-card.vue +++ /dev/null @@ -1,392 +0,0 @@ - - - - - - - diff --git a/web/src/views/server/components/host-table.vue b/web/src/views/server/components/host-table.vue index 1ee691b..c94dea1 100644 --- a/web/src/views/server/components/host-table.vue +++ b/web/src/views/server/components/host-table.vue @@ -3,7 +3,7 @@ { } const handleSSH = async (row) => { - let { host } = row - $router.push({ path: '/terminal', query: { host } }) + let { id } = row + $router.push({ path: '/terminal', query: { hostIds: id } }) } const handleOnekey = async (row) => { - let { host, isConfig } = row + let { id, isConfig } = row if (!isConfig) { $message({ message: '请先配置SSH连接信息', @@ -143,7 +143,7 @@ const handleOnekey = async (row) => { handleUpdate(row) return } - $router.push({ path: '/onekey', query: { host, execClientInstallScript: 'true' } }) + $router.push({ path: '/onekey', query: { hostIds: id, execClientInstallScript: 'true' } }) } let defaultSortLocal = localStorage.getItem('host_table_sort') @@ -175,13 +175,13 @@ defineExpose({ clearSelection }) -const handleRemoveHost = async ({ host }) => { +const handleRemoveHost = async ({ id }) => { $messageBox.confirm('确认删除实例', 'Warning', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(async () => { - let { data } = await $api.removeHost({ host }) + let { data } = await $api.removeHost({ ids: [id,] }) $message({ message: data, type: 'success', diff --git a/web/src/views/server/index.vue b/web/src/views/server/index.vue index 5873690..0b7970d 100644 --- a/web/src/views/server/index.vue +++ b/web/src/views/server/index.vue @@ -121,10 +121,10 @@ let collectSelectHost = () => { let handleBatchSSH = () => { collectSelectHost() if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例') - let ips = selectHosts.value.filter(item => item.isConfig).map(item => item.host) - if (!ips.length) return $message.warning('所选实例未配置ssh连接信息') - if (ips.length < selectHosts.value.length) $message.warning('部分实例未配置ssh连接信息,已忽略') - $router.push({ path: '/terminal', query: { host: ips.join(',') } }) + let ids = selectHosts.value.filter(item => item.isConfig).map(item => item.id) + if (!ids.length) return $message.warning('所选实例未配置ssh连接信息') + if (ids.length < selectHosts.value.length) $message.warning('部分实例未配置ssh连接信息,已忽略') + $router.push({ path: '/terminal', query: { hostIds: ids.join(',') } }) } let handleBatchModify = async () => { @@ -137,7 +137,7 @@ let handleBatchModify = async () => { let handleBatchRemove = async () => { collectSelectHost() if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例') - let ips = selectHosts.value.map(item => item.host) + let ids = selectHosts.value.map(item => item.id) let names = selectHosts.value.map(item => item.name) $messageBox.confirm(() => h('p', { style: 'line-height: 18px;' }, `确认删除\n${ names.join(', ') }吗?`), 'Warning', { @@ -145,7 +145,7 @@ let handleBatchRemove = async () => { cancelButtonText: '取消', type: 'warning' }).then(async () => { - let { data } = await $api.removeHost({ host: ips }) + let { data } = await $api.removeHost({ ids }) $message({ message: data, type: 'success', center: true }) selectHosts.value = [] await handleUpdateList() @@ -161,8 +161,8 @@ let handleUpdateHost = (defaultData) => { let handleBatchOnekey = async () => { collectSelectHost() if (!selectHosts.value.length) return $message.warning('请选择要批量操作的实例') - let ips = selectHosts.value.map(item => item.host).join(',') - $router.push({ path: '/onekey', query: { host: ips, execClientInstallScript: 'true' } }) + let ids = selectHosts.value.map(item => item.id).join(',') + $router.push({ path: '/onekey', query: { hostIds: ids, execClientInstallScript: 'true' } }) } let handleBatchExport = () => { diff --git a/web/src/views/terminal/index.vue b/web/src/views/terminal/index.vue index a786a3b..cb2c517 100644 --- a/web/src/views/terminal/index.vue +++ b/web/src/views/terminal/index.vue @@ -101,9 +101,9 @@ const handleUpdateList = async ({ host }) => { onActivated(async () => { await nextTick() - const { host } = route.query - if (!host) return - let targetHosts = hostList.value.filter(item => host.includes(item.host)).map(item => { + const { hostIds } = route.query + if (!hostIds) return + let targetHosts = hostList.value.filter(item => hostIds.includes(item.id)).map(item => { const { name, host } = item return { key: randomStr(16), name, host, status: CONNECTING } })